@nestbox-ai/cli
Version:
The cli tools that helps developers to build agents
273 lines (237 loc) • 11.4 kB
text/typescript
import { Command } from "commander";
import { withTokenRefresh } from "../../utils/error";
import chalk from "chalk";
import ora from "ora";
import { resolveProject } from "../../utils/project";
import fs from "fs";
import {
createZipFromDirectory,
findProjectRoot,
isTypeScriptProject,
loadNestboxConfig,
runPredeployScripts,
} from "../../utils/agent";
import axios from "axios";
import { AgentType } from "../../types/agentType";
import path from "path";
import { getAuthToken } from "../../utils/auth";
import { createApis } from "./apiUtils";
export function registerDeployCommand(agentCommand: Command): void {
agentCommand
.command("deploy")
.description("Deploy an AI agent to the Nestbox platform")
.option("--agent <agentName>", "Agent name to deploy")
.option("--chatbot <chatbotName>", "Chatbot name to deploy")
.requiredOption("--instance <instanceName>", "Instance name")
.option(
"--zip <zipFileOrDirPath>",
"Path to the zip file or directory to upload"
)
.option(
"--project <projectName>",
"Project name (defaults to the current project)"
)
.option("--entry <entryFunction>", "Entry function name")
.option("--log", "Show detailed logs during deployment")
.action(async (options) => {
try {
const {
agent: agentName,
chatbot: chatbotName,
instance: instanceName,
zip: customZipPath,
entry,
log,
} = options;
// Ensure either agent or chatbot is provided, but not both
if ((!agentName && !chatbotName) || (agentName && chatbotName)) {
console.error(
chalk.red("Please provide either --agent OR --chatbot option, but not both.")
);
return;
}
let apis = createApis();
// Find project root
const projectRoot = await findProjectRoot();
console.log(chalk.blue(`Project root detected at: ${projectRoot}`));
// Main deployment logic with token refresh
await withTokenRefresh(
async () => {
// Resolve project
const projectData = await resolveProject(apis.projectsApi, options);
// Determine if we're deploying an agent or chatbot
const isAgent = !!agentName;
const resourceName = isAgent ? agentName : chatbotName;
const resourceType = isAgent ? "Agent" : "Chatbot";
const agentType = isAgent ? AgentType.REGULAR : "CHAT";
// Get agents data and find agent/chatbot by name
const agentsData: any = await apis.agentsApi.machineAgentControllerGetMachineAgentByProjectId(
projectData.id,
0,
10,
agentType
);
const targetAgent = agentsData.data.machineAgents.find(
(agent: any) => agent.agentName === resourceName
);
if (!targetAgent) {
console.error(
chalk.red(`${resourceType} with name "${resourceName}" not found in project "${projectData.name}".`)
);
console.log(chalk.yellow(`Available ${resourceType.toLowerCase()}s:`));
agentsData.data.machineAgents.forEach((agent: any) => {
console.log(chalk.yellow(` - ${agent.agentName} (ID: ${agent.id})`));
});
return;
}
// Get instance data and find instance by name
const instanceData: any = await apis.instanceApi.machineInstancesControllerGetMachineInstanceByUserId(
projectData.id,
0,
10
);
const targetInstance = instanceData.data.machineInstances.find(
(instance: any) => instance.instanceName === instanceName
);
if (!targetInstance) {
console.error(
chalk.red(`Instance with name "${instanceName}" not found in project "${projectData.name}".`)
);
console.log(chalk.yellow("Available instances:"));
instanceData.data.machineInstances.forEach((instance: any) => {
console.log(chalk.yellow(` - ${instance.instanceName} (ID: ${instance.id})`));
});
return;
}
// Extract IDs
const agentId = targetAgent.id;
const resolvedEntry = entry || targetAgent.entryFunctionName || "main";
const instanceId = targetInstance.id;
// Load nestbox.config.json
const config = loadNestboxConfig(projectRoot);
// Start the deployment process
const spinner = ora(
`Preparing to deploy ${resourceType.toLowerCase()} ${agentId} to instance ${instanceId}...`
).start();
try {
let zipFilePath;
if (customZipPath) {
// Process custom zip path
if (!fs.existsSync(customZipPath)) {
spinner.fail(`Path not found: ${customZipPath}`);
return;
}
const stats = fs.statSync(customZipPath);
if (stats.isFile()) {
if (!customZipPath.toLowerCase().endsWith(".zip")) {
spinner.fail(`File is not a zip archive: ${customZipPath}`);
return;
}
spinner.text = `Using provided zip file: ${customZipPath}`;
zipFilePath = customZipPath;
} else if (stats.isDirectory()) {
// Process directory
spinner.text = `Processing directory: ${customZipPath}`;
const isTypeScript = isTypeScriptProject(customZipPath);
if (isTypeScript && (config?.agent?.predeploy || config?.agents?.predeploy)) {
const predeployScripts = config?.agent?.predeploy || config?.agents?.predeploy;
spinner.text = `Running predeploy scripts on target directory...`;
await runPredeployScripts(predeployScripts, customZipPath);
}
spinner.text = `Creating zip archive from directory ${customZipPath}...`;
zipFilePath = createZipFromDirectory(customZipPath);
spinner.text = `Directory zipped successfully to ${zipFilePath}`;
}
} else {
// Use project root
spinner.text = `Using project root: ${projectRoot}`;
const isTypeScript = isTypeScriptProject(projectRoot);
if (isTypeScript && (config?.agent?.predeploy || config?.agents?.predeploy)) {
const predeployScripts = config?.agent?.predeploy || config?.agents?.predeploy;
spinner.text = `Running predeploy scripts on project root...`;
await runPredeployScripts(predeployScripts, projectRoot);
}
spinner.text = `Creating zip archive from project root ${projectRoot}...`;
zipFilePath = createZipFromDirectory(projectRoot);
spinner.text = `Directory zipped successfully to ${zipFilePath}`;
}
spinner.text = `Deploying ${resourceType.toLowerCase()} ${agentId} to instance ${instanceId}...`;
// Prepare deployment
const authToken = getAuthToken();
const baseUrl = authToken?.serverUrl?.endsWith("/")
? authToken.serverUrl.slice(0, -1)
: authToken?.serverUrl;
const { default: FormData } = await import("form-data");
const form = new FormData();
form.append("file", fs.createReadStream(zipFilePath));
form.append("machineAgentId", agentId.toString());
form.append("instanceId", instanceId.toString());
form.append("entryFunctionName", resolvedEntry);
form.append("isSourceCodeUpdate", "true");
form.append("projectId", projectData.id);
if (log) {
console.log(chalk.blue("Form Details "));
console.log(chalk.blue(` - File: ${path.basename(zipFilePath)}`));
console.log(chalk.blue(` - Agent ID: ${agentId}`));
console.log(chalk.blue(` - Instance ID: ${instanceId}`));
console.log(chalk.blue(` - Entry Function: ${resolvedEntry}`));
console.log(chalk.blue(` - Project ID: ${projectData.id}`));
}
const axiosInstance = axios.create({
baseURL: baseUrl,
headers: {
...form.getHeaders(),
Authorization: authToken?.token,
},
});
const endpoint = `/projects/${projectData.id}/agents/${agentId}`;
spinner.text = `Deploy ${resourceType.toLowerCase()} ${agentName}...`;
const res = await axiosInstance.patch(endpoint, form);
if (!customZipPath && zipFilePath && fs.existsSync(zipFilePath)) {
fs.unlinkSync(zipFilePath);
}
if (log) {
console.log(chalk.blue("\nDeployment request:"));
console.log(chalk.blue(` URL: ${baseUrl}${endpoint}`));
console.log(chalk.blue(` Method: PATCH`));
console.log(chalk.blue(` File: ${path.basename(zipFilePath)}`));
console.log(chalk.blue(` Response status: ${res.status} ${res.statusText}`));
const lines = res.data.logEntries || [];
console.log(chalk.blue(` Deployment log entries (${lines.length} lines):`));
lines.forEach((line: any) => {
console.log(chalk.blue(` - [${line.type} ${line.timestamp}] ${line.message} `));
});
}
spinner.succeed("Successfully deployed");
console.log(chalk.green(`${resourceType} deployed successfully!`));
console.log(chalk.cyan(`📍 Instance: ${instanceName}`));
console.log(chalk.cyan(`🤖 Agent: ${agentName} (${agentId})`));
console.log(chalk.cyan(`⚙️ Entry: ${resolvedEntry}`));
console.log(chalk.cyan(`🔄 Process: ${res.data.processName}`));
} catch (error: any) {
spinner.fail(`Failed to deploy ${resourceType.toLowerCase()}`);
throw error;
}
},
() => {
apis = createApis();
}
);
} catch (error: any) {
if (error.message && error.message.includes('Authentication')) {
console.error(chalk.red(error.message));
} else if (error.response) {
console.error(
chalk.red(
`API Error (${error.response.status}): ${error.response.data?.message || "Unknown error"}`
)
);
if (error.response.data) {
console.error(chalk.red(`Error Data: ${JSON.stringify(error.response.data, null, 2)}`));
}
} else {
console.error(chalk.red("Error:"), error.message || "Unknown error");
}
}
});
}