@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
570 lines • 22.4 kB
JavaScript
/**
* Google Vertex AI Setup Command
*
* Supports three authentication methods:
* - Method 1: File Path (GOOGLE_APPLICATION_CREDENTIALS)
* - Method 2: JSON String (GOOGLE_SERVICE_ACCOUNT_KEY)
* - Method 3: Individual Vars (GOOGLE_AUTH_CLIENT_EMAIL + GOOGLE_AUTH_PRIVATE_KEY)
*
* All methods require GOOGLE_VERTEX_PROJECT
* Optional: GOOGLE_VERTEX_LOCATION (defaults to 'us-central1')
*/
import fs from "fs";
import path from "path";
import os from "os";
import inquirer from "inquirer";
import chalk from "chalk";
import ora from "ora";
import { logger } from "../../lib/utils/logger.js";
var AuthMethod;
(function (AuthMethod) {
AuthMethod["FILE_PATH"] = "file-path";
AuthMethod["JSON_STRING"] = "json-string";
AuthMethod["INDIVIDUAL_VARS"] = "individual-vars";
})(AuthMethod || (AuthMethod = {}));
const AUTH_METHOD_NAMES = {
[AuthMethod.FILE_PATH]: "Method 1: File Path",
[AuthMethod.JSON_STRING]: "Method 2: JSON String",
[AuthMethod.INDIVIDUAL_VARS]: "Method 3: Individual Vars",
};
export async function handleGCPSetup(argv) {
try {
const options = {
checkOnly: argv.check || false,
interactive: !argv.nonInteractive,
};
logger.always(chalk.blue("🔍 Checking environment..."));
// Step 1: Detect current authentication method status
const status = detectAuthMethodStatus();
// Step 2: Display current status
displayAuthStatus(status);
// Check-only mode - show status and exit
if (options.checkOnly) {
const completeMethod = getCompleteMethod(status);
if (completeMethod) {
logger.always(chalk.green("✅ Google Vertex setup complete with " +
AUTH_METHOD_NAMES[completeMethod]));
if (status.common.hasProject) {
logger.always(` Project: ${process.env.GOOGLE_VERTEX_PROJECT}`);
}
if (status.common.hasLocation) {
logger.always(` Location: ${process.env.GOOGLE_VERTEX_LOCATION}`);
}
else {
logger.always(` Location: us-central1 (default)`);
}
}
return;
}
// Step 3: Check if any method is complete and offer to reconfigure
const completeMethod = getCompleteMethod(status);
if (completeMethod) {
logger.always(chalk.green("✅ Current setup: " +
AUTH_METHOD_NAMES[completeMethod] +
" (Complete)"));
if (status.common.hasProject) {
logger.always(` Project: ${process.env.GOOGLE_VERTEX_PROJECT}`);
}
if (status.common.hasLocation) {
logger.always(` Location: ${process.env.GOOGLE_VERTEX_LOCATION}`);
}
else {
logger.always(` Location: us-central1 (default)`);
}
const { reconfigure } = await inquirer.prompt([
{
type: "confirm",
name: "reconfigure",
message: "Setup is already complete. Do you want to reconfigure or switch methods?",
default: false,
},
]);
if (!reconfigure) {
logger.always(chalk.blue("👍 Keeping existing configuration."));
return;
}
}
// Step 4: Interactive setup
if (!options.interactive) {
logger.always(chalk.yellow("⚠️ Non-interactive mode: setup incomplete"));
return;
}
// Step 5: Method selection
const selectedMethod = await selectAuthMethod(status);
logger.always(chalk.blue(`👉 You selected ${AUTH_METHOD_NAMES[selectedMethod]}. Completing setup...`));
// Step 6: Prompt for missing values
const config = await promptForMissingValues(selectedMethod, status);
// Step 7: Update .env file
await updateEnvFile(selectedMethod, config);
// Step 8: Success message
logger.always(chalk.green(`✅ Google Vertex setup complete with ${AUTH_METHOD_NAMES[selectedMethod]}`));
logger.always(` Project: ${config.project}`);
logger.always(` Location: ${config.location || "us-central1"}`);
}
catch (error) {
logger.error(chalk.red("❌ GCP setup failed:"));
logger.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
process.exit(1);
}
}
/**
* Detect the current status of all authentication methods
*/
function detectAuthMethodStatus() {
const hasCredentials = !!process.env.GOOGLE_APPLICATION_CREDENTIALS;
const hasServiceAccountKey = !!process.env.GOOGLE_SERVICE_ACCOUNT_KEY;
const hasClientEmail = !!process.env.GOOGLE_AUTH_CLIENT_EMAIL;
const hasPrivateKey = !!process.env.GOOGLE_AUTH_PRIVATE_KEY;
const hasProject = !!process.env.GOOGLE_VERTEX_PROJECT;
const hasLocation = !!process.env.GOOGLE_VERTEX_LOCATION;
const status = {
method1: {
complete: hasCredentials && hasProject,
hasCredentials,
missingVars: [],
},
method2: {
complete: hasServiceAccountKey && hasProject,
hasServiceAccountKey,
missingVars: [],
},
method3: {
complete: hasClientEmail && hasPrivateKey && hasProject,
hasClientEmail,
hasPrivateKey,
missingVars: [],
},
common: {
hasProject,
hasLocation,
missingVars: [],
},
};
// Calculate missing variables for each method
if (!hasCredentials) {
status.method1.missingVars.push("GOOGLE_APPLICATION_CREDENTIALS");
}
if (!hasProject) {
status.method1.missingVars.push("GOOGLE_VERTEX_PROJECT");
}
if (!hasServiceAccountKey) {
status.method2.missingVars.push("GOOGLE_SERVICE_ACCOUNT_KEY");
}
if (!hasProject) {
status.method2.missingVars.push("GOOGLE_VERTEX_PROJECT");
}
if (!hasClientEmail) {
status.method3.missingVars.push("GOOGLE_AUTH_CLIENT_EMAIL");
}
if (!hasPrivateKey) {
status.method3.missingVars.push("GOOGLE_AUTH_PRIVATE_KEY");
}
if (!hasProject) {
status.method3.missingVars.push("GOOGLE_VERTEX_PROJECT");
}
if (!hasProject) {
status.common.missingVars.push("GOOGLE_VERTEX_PROJECT");
}
return status;
}
/**
* Display the current authentication status
*/
function displayAuthStatus(status) {
if (status.method1.complete) {
logger.always(chalk.green("✔ Method 1: Complete"));
}
else if (status.method1.hasCredentials) {
logger.always(chalk.yellow(`⚠ Method 1: Partially set (missing ${status.method1.missingVars.join(", ")})`));
}
else {
logger.always(chalk.red("✘ Method 1: Not set"));
}
if (status.method2.complete) {
logger.always(chalk.green("✔ Method 2: Complete"));
}
else if (status.method2.hasServiceAccountKey) {
logger.always(chalk.yellow(`⚠ Method 2: Partially set (missing ${status.method2.missingVars.join(", ")})`));
}
else {
logger.always(chalk.red("✘ Method 2: Not set"));
}
if (status.method3.complete) {
logger.always(chalk.green("✔ Method 3: Complete"));
}
else if (status.method3.hasClientEmail || status.method3.hasPrivateKey) {
logger.always(chalk.yellow(`⚠ Method 3: Partially set (missing ${status.method3.missingVars.join(", ")})`));
}
else {
logger.always(chalk.red("✘ Method 3: Not set"));
}
}
/**
* Check if any authentication method is complete
*/
function getCompleteMethod(status) {
if (status.method1.complete) {
return AuthMethod.FILE_PATH;
}
if (status.method2.complete) {
return AuthMethod.JSON_STRING;
}
if (status.method3.complete) {
return AuthMethod.INDIVIDUAL_VARS;
}
return null;
}
/**
* Let user select authentication method
*/
async function selectAuthMethod(status) {
// Check for partially filled methods
const partiallyFilledMethods = [];
if (status.method1.hasCredentials && !status.method1.complete) {
partiallyFilledMethods.push({
method: AuthMethod.FILE_PATH,
name: "Method 1",
count: 1,
});
}
if (status.method2.hasServiceAccountKey && !status.method2.complete) {
partiallyFilledMethods.push({
method: AuthMethod.JSON_STRING,
name: "Method 2",
count: 1,
});
}
if ((status.method3.hasClientEmail || status.method3.hasPrivateKey) &&
!status.method3.complete) {
const count = (status.method3.hasClientEmail ? 1 : 0) +
(status.method3.hasPrivateKey ? 1 : 0);
partiallyFilledMethods.push({
method: AuthMethod.INDIVIDUAL_VARS,
name: "Method 3",
count,
});
}
// If there's a partially filled method, suggest continuing with it
if (partiallyFilledMethods.length > 0) {
const partial = partiallyFilledMethods[0];
const totalVars = partial.method === AuthMethod.INDIVIDUAL_VARS ? 2 : 1;
logger.always(chalk.yellow(`\nYou already have ${partial.count}/${totalVars} values set for ${partial.name}.`));
const { continueWithPartial } = await inquirer.prompt([
{
type: "confirm",
name: "continueWithPartial",
message: `Do you want to continue with ${partial.name} or pick another method?`,
default: true,
},
]);
if (continueWithPartial) {
return partial.method;
}
}
// Present method selection
const { method } = await inquirer.prompt([
{
type: "select",
name: "method",
message: "Which authentication method would you like to use?",
choices: [
{
name: "File Path (Recommended for local development)",
value: AuthMethod.FILE_PATH,
},
{
name: "JSON String (Good for containers/cloud)",
value: AuthMethod.JSON_STRING,
},
{
name: "Individual Vars (Good for CI/CD)",
value: AuthMethod.INDIVIDUAL_VARS,
},
],
},
]);
return method;
}
/**
* Prompt user for missing values based on selected method
*/
async function promptForMissingValues(method, status) {
const config = {};
switch (method) {
case AuthMethod.FILE_PATH:
if (!status.method1.hasCredentials) {
// Try to auto-detect ADC file first
const adcPath = path.join(os.homedir(), ".config", "gcloud", "application_default_credentials.json");
if (fs.existsSync(adcPath)) {
logger.always(chalk.green("✔ Found Application Default Credentials"));
logger.always(chalk.blue(` Location: ${adcPath}`));
config.credentialsPath = adcPath;
}
else {
const { credentialsPath } = await inquirer.prompt([
{
type: "input",
name: "credentialsPath",
message: "Enter the path to your Google Cloud credentials JSON file:",
validate: validateCredentialsFile,
transformer: (input) => input.replace(/^~/, os.homedir()),
},
]);
config.credentialsPath = credentialsPath.replace(/^~/, os.homedir());
}
}
break;
case AuthMethod.JSON_STRING:
if (!status.method2.hasServiceAccountKey) {
const { serviceAccountKey } = await inquirer.prompt([
{
type: "password",
mask: "*",
name: "serviceAccountKey",
message: "Enter your service account JSON as a string:",
validate: validateServiceAccountJSON,
},
]);
config.serviceAccountKey = serviceAccountKey;
}
break;
case AuthMethod.INDIVIDUAL_VARS:
if (!status.method3.hasClientEmail) {
const { clientEmail } = await inquirer.prompt([
{
type: "input",
name: "clientEmail",
message: "Enter your service account client email (format: name@project.iam.gserviceaccount.com):",
validate: (input) => {
if (!input.trim()) {
return "Client email is required";
}
// Check for basic email format
if (!input.includes("@")) {
return "Invalid email format. Expected: service-account@project-id.iam.gserviceaccount.com";
}
// Check for Google service account domain
if (!input.endsWith(".iam.gserviceaccount.com")) {
return "Invalid service account email. Must end with '.iam.gserviceaccount.com'\nExample: my-service-account@my-project.iam.gserviceaccount.com";
}
// Validate the structure: name@project.iam.gserviceaccount.com
const emailPattern = /^[a-z0-9-]+@[a-z0-9-]+\.iam\.gserviceaccount\.com$/;
if (!emailPattern.test(input.trim())) {
return "Invalid format. Expected: service-account-name@project-id.iam.gserviceaccount.com\nExample: my-service-account@my-project-123.iam.gserviceaccount.com";
}
return true;
},
},
]);
config.clientEmail = clientEmail.trim();
}
if (!status.method3.hasPrivateKey) {
const { privateKey } = await inquirer.prompt([
{
type: "password",
mask: "*",
name: "privateKey",
message: "Enter your service account private key:",
validate: (input) => {
if (!input.trim()) {
return "Private key is required";
}
if (!input.includes("BEGIN PRIVATE KEY") ||
!input.includes("END PRIVATE KEY")) {
return "Invalid private key format. Should include BEGIN and END markers.";
}
return true;
},
},
]);
config.privateKey = privateKey.trim();
}
break;
}
// Always prompt for project if missing
if (!status.common.hasProject) {
const { project } = await inquirer.prompt([
{
type: "input",
name: "project",
message: "Enter your Google Cloud Project ID:",
validate: (input) => {
if (!input.trim()) {
return "Project ID is required";
}
if (!/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/.test(input.trim())) {
return "Invalid project ID format. Must be 6-30 characters, start with lowercase letter, contain only lowercase letters, numbers, and hyphens.";
}
return true;
},
},
]);
config.project = project.trim();
}
else {
config.project = process.env.GOOGLE_VERTEX_PROJECT;
}
// Always prompt for location if missing, with default
if (!status.common.hasLocation) {
const { location } = await inquirer.prompt([
{
type: "input",
name: "location",
message: "Enter your Google Vertex AI location (or 'global' for global endpoint):",
default: "us-central1",
validate: (input) => {
if (!input.trim()) {
return "Location is required";
}
return true;
},
},
]);
config.location = location.trim();
}
else {
config.location = process.env.GOOGLE_VERTEX_LOCATION;
}
return config;
}
/**
* Validate credentials file path
*/
function validateCredentialsFile(input) {
if (!input.trim()) {
return "Credentials path is required";
}
const expandedPath = input.replace(/^~/, os.homedir());
if (!fs.existsSync(expandedPath)) {
return `File not found: ${expandedPath}`;
}
try {
const content = fs.readFileSync(expandedPath, "utf8");
const parsed = JSON.parse(content);
if (!parsed.client_email || !parsed.private_key) {
return "Invalid service account file: missing client_email or private_key";
}
return true;
}
catch {
return "Invalid JSON file";
}
}
/**
* Validate service account JSON string
*/
function validateServiceAccountJSON(input) {
if (!input.trim()) {
return "Service account JSON is required";
}
try {
const parsed = JSON.parse(input.trim());
if (!parsed.client_email || !parsed.private_key) {
return "Invalid service account JSON: missing client_email or private_key";
}
return true;
}
catch {
return "Invalid JSON format";
}
}
/**
* Update .env file with selected authentication method
*/
async function updateEnvFile(method, config) {
const envPath = path.join(process.cwd(), ".env");
const spinner = ora("💾 Updating .env file...").start();
try {
let envContent = "";
// Read existing .env file if it exists
if (fs.existsSync(envPath)) {
envContent = fs.readFileSync(envPath, "utf8");
}
// Parse existing environment variables
const envLines = envContent.split("\n");
const existingVars = new Map();
const otherLines = [];
for (const line of envLines) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith("#")) {
const equalsIndex = trimmed.indexOf("=");
if (equalsIndex > 0) {
const key = trimmed.substring(0, equalsIndex);
const value = trimmed.substring(equalsIndex + 1);
existingVars.set(key, value);
}
else {
otherLines.push(line);
}
}
else {
otherLines.push(line);
}
}
// Remove all Google Vertex auth variables first (clean slate)
existingVars.delete("GOOGLE_APPLICATION_CREDENTIALS");
existingVars.delete("GOOGLE_SERVICE_ACCOUNT_KEY");
existingVars.delete("GOOGLE_AUTH_CLIENT_EMAIL");
existingVars.delete("GOOGLE_AUTH_PRIVATE_KEY");
// Set variables for selected method
switch (method) {
case AuthMethod.FILE_PATH:
if (config.credentialsPath) {
existingVars.set("GOOGLE_APPLICATION_CREDENTIALS", config.credentialsPath);
}
break;
case AuthMethod.JSON_STRING:
if (config.serviceAccountKey) {
existingVars.set("GOOGLE_SERVICE_ACCOUNT_KEY", config.serviceAccountKey);
}
break;
case AuthMethod.INDIVIDUAL_VARS:
if (config.clientEmail) {
existingVars.set("GOOGLE_AUTH_CLIENT_EMAIL", config.clientEmail);
}
if (config.privateKey) {
const escaped = config.privateKey
.replace(/\r?\n/g, "\\n")
.replace(/"/g, '\\"');
existingVars.set("GOOGLE_AUTH_PRIVATE_KEY", `"${escaped}"`);
}
break;
}
// Always set project and location
if (config.project) {
existingVars.set("GOOGLE_VERTEX_PROJECT", config.project);
}
if (config.location) {
existingVars.set("GOOGLE_VERTEX_LOCATION", config.location);
}
// Reconstruct .env content preserving structure
const newEnvLines = [];
// Add non-variable lines first (comments, empty lines)
for (const line of otherLines) {
newEnvLines.push(line);
}
// Add separator comment for Google Vertex if needed
if (!envContent.includes("GOOGLE VERTEX AI CONFIGURATION")) {
if (newEnvLines.length > 0 &&
newEnvLines[newEnvLines.length - 1].trim()) {
newEnvLines.push("");
}
newEnvLines.push("# GOOGLE VERTEX AI CONFIGURATION");
}
// Add all environment variables
for (const [key, value] of existingVars.entries()) {
newEnvLines.push(`${key}=${value}`);
}
// Write updated content
const finalContent = newEnvLines.join("\n") + (newEnvLines.length > 0 ? "\n" : "");
fs.writeFileSync(envPath, finalContent, "utf8");
spinner.succeed(chalk.green("✔ .env file updated successfully"));
}
catch (error) {
spinner.fail(chalk.red("❌ Failed to update .env file"));
logger.error(chalk.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
throw error;
}
}
//# sourceMappingURL=setup-gcp.js.map