@agents-at-scale/ark 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -0
- package/dist/commands/cluster/get-ip.d.ts +2 -0
- package/dist/commands/cluster/get-ip.js +32 -0
- package/dist/commands/cluster/get-type.d.ts +2 -0
- package/dist/commands/cluster/get-type.js +26 -0
- package/dist/commands/cluster/index.d.ts +2 -0
- package/dist/commands/cluster/index.js +10 -0
- package/dist/commands/completion.d.ts +2 -0
- package/dist/commands/completion.js +108 -0
- package/dist/commands/config.d.ts +5 -0
- package/dist/commands/config.js +327 -0
- package/dist/commands/generate/config.d.ts +145 -0
- package/dist/commands/generate/config.js +253 -0
- package/dist/commands/generate/generators/agent.d.ts +2 -0
- package/dist/commands/generate/generators/agent.js +156 -0
- package/dist/commands/generate/generators/index.d.ts +6 -0
- package/dist/commands/generate/generators/index.js +6 -0
- package/dist/commands/generate/generators/marketplace.d.ts +2 -0
- package/dist/commands/generate/generators/marketplace.js +304 -0
- package/dist/commands/generate/generators/mcpserver.d.ts +25 -0
- package/dist/commands/generate/generators/mcpserver.js +350 -0
- package/dist/commands/generate/generators/project.d.ts +2 -0
- package/dist/commands/generate/generators/project.js +784 -0
- package/dist/commands/generate/generators/query.d.ts +2 -0
- package/dist/commands/generate/generators/query.js +213 -0
- package/dist/commands/generate/generators/team.d.ts +2 -0
- package/dist/commands/generate/generators/team.js +407 -0
- package/dist/commands/generate/index.d.ts +24 -0
- package/dist/commands/generate/index.js +357 -0
- package/dist/commands/generate/templateDiscovery.d.ts +30 -0
- package/dist/commands/generate/templateDiscovery.js +94 -0
- package/dist/commands/generate/templateEngine.d.ts +78 -0
- package/dist/commands/generate/templateEngine.js +368 -0
- package/dist/commands/generate/utils/nameUtils.d.ts +35 -0
- package/dist/commands/generate/utils/nameUtils.js +110 -0
- package/dist/commands/generate/utils/projectUtils.d.ts +28 -0
- package/dist/commands/generate/utils/projectUtils.js +133 -0
- package/dist/components/DashboardCLI.d.ts +3 -0
- package/dist/components/DashboardCLI.js +149 -0
- package/dist/components/GeneratorUI.d.ts +3 -0
- package/dist/components/GeneratorUI.js +167 -0
- package/dist/components/statusChecker.d.ts +48 -0
- package/dist/components/statusChecker.js +251 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.js +243 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +67 -0
- package/dist/lib/arkClient.d.ts +32 -0
- package/dist/lib/arkClient.js +43 -0
- package/dist/lib/cluster.d.ts +8 -0
- package/dist/lib/cluster.js +134 -0
- package/dist/lib/config.d.ts +82 -0
- package/dist/lib/config.js +223 -0
- package/dist/lib/consts.d.ts +10 -0
- package/dist/lib/consts.js +15 -0
- package/dist/lib/errors.d.ts +56 -0
- package/dist/lib/errors.js +208 -0
- package/dist/lib/exec.d.ts +5 -0
- package/dist/lib/exec.js +20 -0
- package/dist/lib/gatewayManager.d.ts +24 -0
- package/dist/lib/gatewayManager.js +85 -0
- package/dist/lib/kubernetes.d.ts +28 -0
- package/dist/lib/kubernetes.js +122 -0
- package/dist/lib/progress.d.ts +128 -0
- package/dist/lib/progress.js +273 -0
- package/dist/lib/security.d.ts +37 -0
- package/dist/lib/security.js +295 -0
- package/dist/lib/types.d.ts +37 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/wrappers/git.d.ts +2 -0
- package/dist/lib/wrappers/git.js +43 -0
- package/dist/ui/MainMenu.d.ts +3 -0
- package/dist/ui/MainMenu.js +116 -0
- package/dist/ui/statusFormatter.d.ts +9 -0
- package/dist/ui/statusFormatter.js +47 -0
- package/package.json +62 -0
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { execa } from 'execa';
|
|
6
|
+
import { TemplateEngine } from '../templateEngine.js';
|
|
7
|
+
import { TemplateDiscovery } from '../templateDiscovery.js';
|
|
8
|
+
import { toKebabCase, validateNameStrict, isValidKubernetesName, } from '../utils/nameUtils.js';
|
|
9
|
+
import { getInquirerProjectTypeChoices, GENERATOR_DEFAULTS, CLI_CONFIG, } from '../config.js';
|
|
10
|
+
import { SecurityUtils } from '../../../lib/security.js';
|
|
11
|
+
import { EnhancedPrompts, ProgressIndicator } from '../../../lib/progress.js';
|
|
12
|
+
export function createProjectGenerator() {
|
|
13
|
+
return {
|
|
14
|
+
name: 'project',
|
|
15
|
+
description: 'Generate a new agent project from template',
|
|
16
|
+
templatePath: 'templates/project',
|
|
17
|
+
generate: async (name, destination, options) => {
|
|
18
|
+
const generator = new ProjectGenerator();
|
|
19
|
+
await generator.generate(name, destination, options);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
class ProjectGenerator {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.templateDiscovery = new TemplateDiscovery();
|
|
26
|
+
this.templateEngine = new TemplateEngine();
|
|
27
|
+
// Get path to samples directory
|
|
28
|
+
const templatesPath = this.templateDiscovery.getTemplatePath('');
|
|
29
|
+
this.samplesPath = path.resolve(templatesPath, '../samples');
|
|
30
|
+
}
|
|
31
|
+
async isGitAvailable() {
|
|
32
|
+
try {
|
|
33
|
+
await execa('git', ['--version'], { stdio: 'ignore' });
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async generate(name, destination, options) {
|
|
41
|
+
const progress = new ProgressIndicator('ARK Agent Project Generator');
|
|
42
|
+
// Add steps to progress indicator
|
|
43
|
+
progress.addStep('prerequisites', 'Checking prerequisites');
|
|
44
|
+
progress.addStep('configuration', 'Gathering project configuration');
|
|
45
|
+
if (!options.skipModels) {
|
|
46
|
+
progress.addStep('models', 'Configuring model providers');
|
|
47
|
+
}
|
|
48
|
+
if (!options.skipGit) {
|
|
49
|
+
progress.addStep('git', 'Setting up git repository');
|
|
50
|
+
}
|
|
51
|
+
progress.addStep('generation', 'Generating project files');
|
|
52
|
+
progress.addStep('completion', 'Finalizing project setup');
|
|
53
|
+
try {
|
|
54
|
+
// Check prerequisites
|
|
55
|
+
progress.startStep('prerequisites');
|
|
56
|
+
await this.checkPrerequisites();
|
|
57
|
+
progress.completeStep('prerequisites', 'Prerequisites validated');
|
|
58
|
+
// Get project configuration
|
|
59
|
+
progress.startStep('configuration');
|
|
60
|
+
const config = await this.getProjectConfig(name, destination, options);
|
|
61
|
+
progress.completeStep('configuration', `Project "${config.name}" configured`);
|
|
62
|
+
// Discover and configure models (only if not skipped)
|
|
63
|
+
if (config.configureModels) {
|
|
64
|
+
progress.startStep('models');
|
|
65
|
+
await this.configureModels(config);
|
|
66
|
+
progress.completeStep('models', `Model provider: ${config.selectedModels || 'none'}`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
progress.skipStep('models', 'Model configuration skipped');
|
|
70
|
+
}
|
|
71
|
+
// Configure git if requested (only if not skipped)
|
|
72
|
+
if (config.initGit) {
|
|
73
|
+
progress.startStep('git');
|
|
74
|
+
await this.configureGit(config);
|
|
75
|
+
progress.completeStep('git', 'Git repository configured');
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
progress.skipStep('git', 'Git setup skipped');
|
|
79
|
+
}
|
|
80
|
+
// Generate the project
|
|
81
|
+
progress.startStep('generation');
|
|
82
|
+
await this.generateProject(config);
|
|
83
|
+
progress.completeStep('generation', 'Project files created');
|
|
84
|
+
// Finalize
|
|
85
|
+
progress.startStep('completion');
|
|
86
|
+
this.showNextSteps(config);
|
|
87
|
+
progress.completeStep('completion', 'Project ready');
|
|
88
|
+
progress.complete('Project generation');
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
// Find the current step and mark it as failed
|
|
92
|
+
const currentStep = progress['steps'].find((s) => s.status === 'running');
|
|
93
|
+
if (currentStep) {
|
|
94
|
+
progress.failStep(currentStep.name, `Failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async checkPrerequisites() {
|
|
100
|
+
const requirements = [];
|
|
101
|
+
// Check for git (required for project initialization if git is enabled)
|
|
102
|
+
try {
|
|
103
|
+
await execa('git', ['--version'], { stdio: 'ignore' });
|
|
104
|
+
requirements.push({ tool: 'git', available: true, required: false });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
requirements.push({ tool: 'git', available: false, required: false });
|
|
108
|
+
EnhancedPrompts.showWarning('Git not found - git features will be disabled');
|
|
109
|
+
}
|
|
110
|
+
// Check for deployment tools (optional for project generation)
|
|
111
|
+
const deploymentTools = ['kubectl', 'helm'];
|
|
112
|
+
const missingDeploymentTools = [];
|
|
113
|
+
for (const tool of deploymentTools) {
|
|
114
|
+
try {
|
|
115
|
+
await execa(tool, ['--version'], { stdio: 'ignore' });
|
|
116
|
+
requirements.push({ tool, available: true, required: false });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
requirements.push({ tool, available: false, required: false });
|
|
120
|
+
missingDeploymentTools.push(tool);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (missingDeploymentTools.length > 0) {
|
|
124
|
+
EnhancedPrompts.showInfo(`Optional tools not found: ${missingDeploymentTools.join(', ')}`);
|
|
125
|
+
EnhancedPrompts.showTip('Install kubectl and helm later to deploy your project to a cluster');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async getProjectConfig(name, destination, options) {
|
|
129
|
+
EnhancedPrompts.showSeparator('Project Configuration');
|
|
130
|
+
// Use command line options if provided, otherwise prompt
|
|
131
|
+
let projectType = options.projectType;
|
|
132
|
+
let parentDir = destination;
|
|
133
|
+
let namespace = options.namespace || name;
|
|
134
|
+
// Validate project type if provided
|
|
135
|
+
if (projectType &&
|
|
136
|
+
projectType !== 'empty' &&
|
|
137
|
+
projectType !== 'with-samples') {
|
|
138
|
+
throw new Error(`Invalid project type: ${projectType}. Must be 'empty' or 'with-samples'`);
|
|
139
|
+
}
|
|
140
|
+
// Validate and normalize namespace
|
|
141
|
+
namespace = toKebabCase(namespace);
|
|
142
|
+
validateNameStrict(namespace, 'namespace');
|
|
143
|
+
// Only prompt if in interactive mode and missing required options
|
|
144
|
+
if (options.interactive || !options.projectType || !options.namespace) {
|
|
145
|
+
const prompts = [];
|
|
146
|
+
if (!options.projectType) {
|
|
147
|
+
prompts.push({
|
|
148
|
+
...CLI_CONFIG.prompts.projectType,
|
|
149
|
+
choices: getInquirerProjectTypeChoices(),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (!destination) {
|
|
153
|
+
prompts.push({
|
|
154
|
+
...CLI_CONFIG.prompts.parentDir,
|
|
155
|
+
default: destination,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (!options.namespace) {
|
|
159
|
+
prompts.push({
|
|
160
|
+
...CLI_CONFIG.prompts.namespace,
|
|
161
|
+
default: GENERATOR_DEFAULTS.getDefaultNamespace(name),
|
|
162
|
+
validate: (input) => {
|
|
163
|
+
const trimmed = input.trim();
|
|
164
|
+
if (!trimmed) {
|
|
165
|
+
return 'Namespace cannot be empty';
|
|
166
|
+
}
|
|
167
|
+
if (!isValidKubernetesName(trimmed)) {
|
|
168
|
+
const suggested = toKebabCase(trimmed);
|
|
169
|
+
return `Namespace must be lowercase kebab-case (suggested: "${suggested}")`;
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
},
|
|
173
|
+
filter: (input) => toKebabCase(input),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (prompts.length > 0) {
|
|
177
|
+
const answers = await inquirer.prompt(prompts);
|
|
178
|
+
projectType = answers.projectType || projectType;
|
|
179
|
+
parentDir = answers.parentDir || parentDir;
|
|
180
|
+
namespace = answers.namespace || namespace;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Ensure projectType has a value
|
|
184
|
+
if (!projectType) {
|
|
185
|
+
throw new Error('Project type is required. Use --project-type <empty|with-samples> or run in interactive mode.');
|
|
186
|
+
}
|
|
187
|
+
const projectPath = path.join(parentDir, name);
|
|
188
|
+
// Check if directory exists
|
|
189
|
+
if (fs.existsSync(projectPath)) {
|
|
190
|
+
const overwrite = await inquirer.prompt([
|
|
191
|
+
{
|
|
192
|
+
type: 'confirm',
|
|
193
|
+
name: 'overwrite',
|
|
194
|
+
message: `Directory ${projectPath} already exists. Remove and continue?`,
|
|
195
|
+
default: false,
|
|
196
|
+
},
|
|
197
|
+
]);
|
|
198
|
+
if (overwrite.overwrite) {
|
|
199
|
+
fs.rmSync(projectPath, { recursive: true, force: true });
|
|
200
|
+
console.log(chalk.green('ā
Removed existing directory'));
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
throw new Error('Project creation cancelled');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
name: name,
|
|
208
|
+
namespace,
|
|
209
|
+
destination: projectPath,
|
|
210
|
+
projectType: projectType,
|
|
211
|
+
selectedModels: options.selectedModels || '',
|
|
212
|
+
initGit: !options.skipGit,
|
|
213
|
+
configureModels: !options.skipModels,
|
|
214
|
+
createCommit: options.gitCreateCommit || false,
|
|
215
|
+
gitUserName: options.gitUserName,
|
|
216
|
+
gitUserEmail: options.gitUserEmail,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
async configureModels(config) {
|
|
220
|
+
console.log(chalk.cyan('š Model Provider Configuration\n'));
|
|
221
|
+
// Skip model configuration for empty projects
|
|
222
|
+
if (config.projectType === 'empty') {
|
|
223
|
+
console.log(chalk.gray('āļø Skipping model configuration (empty project)'));
|
|
224
|
+
config.selectedModels = 'none';
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// If models already configured via command line, skip interactive prompts
|
|
228
|
+
if (config.selectedModels && config.selectedModels !== '') {
|
|
229
|
+
console.log(chalk.green(`ā
Using pre-configured model: ${config.selectedModels}`));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const models = await this.discoverModels();
|
|
233
|
+
if (models.length === 0) {
|
|
234
|
+
console.log(chalk.yellow('ā ļø No models found in samples/models/'));
|
|
235
|
+
config.selectedModels = '';
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
console.log('Select which model configurations to include:\n');
|
|
239
|
+
// Show available models
|
|
240
|
+
const choices = models.map((model, index) => ({
|
|
241
|
+
name: `${model.name} - ${model.description}${index === 0 ? ' (recommended)' : ''}`,
|
|
242
|
+
value: model.name,
|
|
243
|
+
short: model.name,
|
|
244
|
+
}));
|
|
245
|
+
choices.push({ name: 'All models (copy everything)', value: 'all', short: 'all' }, {
|
|
246
|
+
name: 'Skip for now (configure manually later)',
|
|
247
|
+
value: 'none',
|
|
248
|
+
short: 'none',
|
|
249
|
+
});
|
|
250
|
+
const modelAnswer = await inquirer.prompt([
|
|
251
|
+
{
|
|
252
|
+
type: 'list',
|
|
253
|
+
name: 'selectedModel',
|
|
254
|
+
message: 'Choose model configuration:',
|
|
255
|
+
choices,
|
|
256
|
+
default: models[0]?.name || 'none',
|
|
257
|
+
},
|
|
258
|
+
]);
|
|
259
|
+
config.selectedModels = modelAnswer.selectedModel;
|
|
260
|
+
}
|
|
261
|
+
async discoverModels() {
|
|
262
|
+
const models = [];
|
|
263
|
+
const modelsPath = path.join(this.samplesPath, 'models');
|
|
264
|
+
if (!fs.existsSync(modelsPath)) {
|
|
265
|
+
return models;
|
|
266
|
+
}
|
|
267
|
+
const modelFiles = fs
|
|
268
|
+
.readdirSync(modelsPath)
|
|
269
|
+
.filter((file) => file.endsWith('.yaml'))
|
|
270
|
+
.sort((a, b) => {
|
|
271
|
+
// Put 'default' first
|
|
272
|
+
if (a === 'default.yaml')
|
|
273
|
+
return -1;
|
|
274
|
+
if (b === 'default.yaml')
|
|
275
|
+
return 1;
|
|
276
|
+
return a.localeCompare(b);
|
|
277
|
+
});
|
|
278
|
+
for (const file of modelFiles) {
|
|
279
|
+
const modelPath = path.join(modelsPath, file);
|
|
280
|
+
const content = fs.readFileSync(modelPath, 'utf-8');
|
|
281
|
+
const name = path.basename(file, '.yaml');
|
|
282
|
+
const envVars = this.extractEnvVars(content);
|
|
283
|
+
const description = this.getModelDescription(name, content);
|
|
284
|
+
models.push({ name, envVars, description });
|
|
285
|
+
}
|
|
286
|
+
return models;
|
|
287
|
+
}
|
|
288
|
+
extractEnvVars(content) {
|
|
289
|
+
const matches = content.match(/\$\{([^}]+)\}/g) || [];
|
|
290
|
+
return [...new Set(matches.map((match) => match.slice(2, -1)))];
|
|
291
|
+
}
|
|
292
|
+
getModelEnvConfigs() {
|
|
293
|
+
return {
|
|
294
|
+
default: {
|
|
295
|
+
apiKey: 'AZURE_API_KEY',
|
|
296
|
+
baseUrl: 'AZURE_BASE_URL',
|
|
297
|
+
defaultBaseUrl: 'https://your-resource.openai.azure.com',
|
|
298
|
+
additionalVars: [
|
|
299
|
+
{
|
|
300
|
+
name: 'AZURE_API_VERSION',
|
|
301
|
+
defaultValue: '2024-12-01-preview',
|
|
302
|
+
description: 'Azure OpenAI API version',
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
},
|
|
306
|
+
claude: {
|
|
307
|
+
apiKey: 'CLAUDE_API_KEY',
|
|
308
|
+
baseUrl: 'CLAUDE_BASE_URL',
|
|
309
|
+
defaultBaseUrl: 'https://api.anthropic.com/v1/',
|
|
310
|
+
},
|
|
311
|
+
openai: {
|
|
312
|
+
apiKey: 'OPENAI_API_KEY',
|
|
313
|
+
baseUrl: 'OPENAI_BASE_URL',
|
|
314
|
+
defaultBaseUrl: 'https://api.openai.com/v1',
|
|
315
|
+
},
|
|
316
|
+
gemini: {
|
|
317
|
+
apiKey: 'GEMINI_API_KEY',
|
|
318
|
+
baseUrl: 'GEMINI_BASE_URL',
|
|
319
|
+
defaultBaseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai/',
|
|
320
|
+
},
|
|
321
|
+
azure: {
|
|
322
|
+
apiKey: 'AZURE_API_KEY',
|
|
323
|
+
baseUrl: 'AZURE_BASE_URL',
|
|
324
|
+
defaultBaseUrl: 'https://your-resource.openai.azure.com',
|
|
325
|
+
additionalVars: [
|
|
326
|
+
{
|
|
327
|
+
name: 'AZURE_API_VERSION',
|
|
328
|
+
defaultValue: '2024-12-01-preview',
|
|
329
|
+
description: 'Azure OpenAI API version',
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
getModelDescription(name, content) {
|
|
336
|
+
// Extract description from comments or use defaults
|
|
337
|
+
const commentMatch = content.match(/^#\s*(.+)/m);
|
|
338
|
+
if (commentMatch && !commentMatch[1].includes('Make sure to use')) {
|
|
339
|
+
return commentMatch[1];
|
|
340
|
+
}
|
|
341
|
+
// Fallback descriptions
|
|
342
|
+
const descriptions = {
|
|
343
|
+
default: 'Azure OpenAI (recommended for quick start)',
|
|
344
|
+
claude: 'Anthropic Claude via OpenAI API',
|
|
345
|
+
gemini: 'Google Gemini via OpenAI API',
|
|
346
|
+
aigw: 'AI Gateway (managed platform)',
|
|
347
|
+
};
|
|
348
|
+
return descriptions[name] || `Model: ${name}`;
|
|
349
|
+
}
|
|
350
|
+
async configureGit(config) {
|
|
351
|
+
console.log(chalk.cyan('š Git Repository Configuration\n'));
|
|
352
|
+
// Check if git is available
|
|
353
|
+
const gitAvailable = await this.isGitAvailable();
|
|
354
|
+
if (!gitAvailable) {
|
|
355
|
+
console.log(chalk.yellow('ā ļø Git not available - skipping git configuration'));
|
|
356
|
+
config.initGit = false;
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
// Check if git is configured
|
|
360
|
+
try {
|
|
361
|
+
await execa('git', ['config', 'user.name'], { stdio: 'pipe' });
|
|
362
|
+
await execa('git', ['config', 'user.email'], { stdio: 'pipe' });
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
console.log(chalk.yellow('ā ļø Git user not configured. Run: git config --global user.name "Your Name" && git config --global user.email "your.email@example.com"'));
|
|
366
|
+
}
|
|
367
|
+
const gitAnswers = await inquirer.prompt([
|
|
368
|
+
{
|
|
369
|
+
type: 'confirm',
|
|
370
|
+
name: 'initGit',
|
|
371
|
+
message: 'Initialize git repository with initial commit?',
|
|
372
|
+
default: true,
|
|
373
|
+
},
|
|
374
|
+
]);
|
|
375
|
+
config.initGit = gitAnswers.initGit;
|
|
376
|
+
config.createCommit = gitAnswers.initGit; // Always create commit if initializing git
|
|
377
|
+
}
|
|
378
|
+
async generateProject(config) {
|
|
379
|
+
console.log(chalk.cyan(CLI_CONFIG.messages.generatingProject));
|
|
380
|
+
// Set template variables
|
|
381
|
+
const variables = {
|
|
382
|
+
projectName: config.name,
|
|
383
|
+
namespace: config.namespace,
|
|
384
|
+
PROJECT_NAME: config.name,
|
|
385
|
+
NAMESPACE: config.namespace,
|
|
386
|
+
authorName: config.gitUserName || 'Your Team',
|
|
387
|
+
authorEmail: config.gitUserEmail || 'your-team@example.com',
|
|
388
|
+
projectType: config.projectType,
|
|
389
|
+
};
|
|
390
|
+
this.templateEngine.setVariables(variables);
|
|
391
|
+
// Copy template
|
|
392
|
+
const templatePath = this.templateDiscovery.getTemplatePath('project');
|
|
393
|
+
// Configure exclude patterns (no sample files since they're now dynamic)
|
|
394
|
+
const excludePatterns = ['.git', 'node_modules', '.DS_Store'];
|
|
395
|
+
await this.templateEngine.processTemplate(templatePath, config.destination, {
|
|
396
|
+
createDirectories: true,
|
|
397
|
+
exclude: excludePatterns,
|
|
398
|
+
});
|
|
399
|
+
// Copy sample templates if 'with-samples' project type
|
|
400
|
+
if (config.projectType === 'with-samples') {
|
|
401
|
+
await this.copySampleTemplates(config);
|
|
402
|
+
}
|
|
403
|
+
// Copy models if selected
|
|
404
|
+
if (config.selectedModels && config.selectedModels !== 'none') {
|
|
405
|
+
await this.copyModelsFromTemplates(config);
|
|
406
|
+
}
|
|
407
|
+
// Clean up .keep files from directories that now have content
|
|
408
|
+
await this.cleanupKeepFiles(config);
|
|
409
|
+
// Create .env file
|
|
410
|
+
await this.createEnvFile(config);
|
|
411
|
+
// Setup git if requested
|
|
412
|
+
if (config.initGit) {
|
|
413
|
+
await this.setupGit(config);
|
|
414
|
+
}
|
|
415
|
+
// Show a clean summary
|
|
416
|
+
console.log(chalk.green('\nā
Project structure created'));
|
|
417
|
+
if (config.projectType === 'with-samples') {
|
|
418
|
+
console.log(chalk.green('ā
Sample agents, teams, and queries added'));
|
|
419
|
+
}
|
|
420
|
+
if (config.selectedModels && config.selectedModels !== 'none') {
|
|
421
|
+
console.log(chalk.green('ā
Model configuration added'));
|
|
422
|
+
}
|
|
423
|
+
console.log(chalk.green('ā
Environment file created'));
|
|
424
|
+
if (config.initGit) {
|
|
425
|
+
console.log(chalk.green('ā
Git repository initialized'));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async copyModelsFromTemplates(config) {
|
|
429
|
+
const modelsDestination = path.join(config.destination, 'models');
|
|
430
|
+
// Ensure models directory exists
|
|
431
|
+
if (!fs.existsSync(modelsDestination)) {
|
|
432
|
+
fs.mkdirSync(modelsDestination, { recursive: true });
|
|
433
|
+
}
|
|
434
|
+
// Clear existing models (except .keep)
|
|
435
|
+
const existingFiles = fs.readdirSync(modelsDestination);
|
|
436
|
+
for (const file of existingFiles) {
|
|
437
|
+
if (file.endsWith('.yaml')) {
|
|
438
|
+
fs.unlinkSync(path.join(modelsDestination, file));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// Map "default" to "azure" for template type
|
|
442
|
+
let templateType = config.selectedModels;
|
|
443
|
+
if (templateType === 'default') {
|
|
444
|
+
templateType = 'azure';
|
|
445
|
+
}
|
|
446
|
+
// Copy specific model template, always named "default"
|
|
447
|
+
await this.copyModelFromTemplate(config, 'default', templateType);
|
|
448
|
+
}
|
|
449
|
+
async copySampleTemplates(config) {
|
|
450
|
+
console.log(chalk.blue('š Adding sample content...'));
|
|
451
|
+
// Temporarily set sample variables
|
|
452
|
+
const originalVariables = this.templateEngine.getVariables();
|
|
453
|
+
const templatesBasePath = this.templateDiscovery.getTemplatePath('');
|
|
454
|
+
// Generate agent and team samples first
|
|
455
|
+
const basicSampleTypes = ['agent', 'team'];
|
|
456
|
+
for (const sampleType of basicSampleTypes) {
|
|
457
|
+
try {
|
|
458
|
+
// Set sample-specific template variables
|
|
459
|
+
const sampleVariables = {
|
|
460
|
+
...this.templateEngine.getVariables(),
|
|
461
|
+
agentName: 'sample',
|
|
462
|
+
teamName: 'sample',
|
|
463
|
+
modelName: 'default', // Use 'default' for sample model
|
|
464
|
+
};
|
|
465
|
+
this.templateEngine.setVariables(sampleVariables);
|
|
466
|
+
const sampleTemplatePath = path.join(templatesBasePath, sampleType);
|
|
467
|
+
// Check if the sample template directory exists
|
|
468
|
+
if (!fs.existsSync(sampleTemplatePath)) {
|
|
469
|
+
console.log(chalk.yellow(`ā ļø Sample template not found: ${sampleType}`));
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
// Get the destination directory for this sample type (with proper pluralization)
|
|
473
|
+
const pluralMap = {
|
|
474
|
+
agent: 'agents',
|
|
475
|
+
team: 'teams',
|
|
476
|
+
query: 'queries',
|
|
477
|
+
model: 'models',
|
|
478
|
+
};
|
|
479
|
+
const destinationDir = path.join(config.destination, pluralMap[sampleType] || `${sampleType}s`);
|
|
480
|
+
// Ensure destination directory exists
|
|
481
|
+
if (!fs.existsSync(destinationDir)) {
|
|
482
|
+
fs.mkdirSync(destinationDir, { recursive: true });
|
|
483
|
+
}
|
|
484
|
+
// Process all template files in this sample type directory
|
|
485
|
+
await this.templateEngine.processTemplate(sampleTemplatePath, destinationDir, {
|
|
486
|
+
createDirectories: false, // We already created the directory
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
console.log(chalk.yellow(`ā ļø Failed to copy sample ${sampleType}: ${error}`));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// Generate sample queries for both agent and team
|
|
494
|
+
await this.copySampleQueries(config, templatesBasePath);
|
|
495
|
+
// Restore original variables
|
|
496
|
+
this.templateEngine.setVariables(originalVariables);
|
|
497
|
+
// Handle sample model separately - only if no models were configured
|
|
498
|
+
if (!config.selectedModels || config.selectedModels === 'none') {
|
|
499
|
+
await this.copySampleModel(config);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async copySampleQueries(config, templatesBasePath) {
|
|
503
|
+
try {
|
|
504
|
+
const queryTemplatePath = path.join(templatesBasePath, 'query');
|
|
505
|
+
// Check if the query template directory exists
|
|
506
|
+
if (!fs.existsSync(queryTemplatePath)) {
|
|
507
|
+
console.log(chalk.yellow(`ā ļø Sample template not found: query`));
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const queriesDir = path.join(config.destination, 'queries');
|
|
511
|
+
// Ensure queries directory exists
|
|
512
|
+
if (!fs.existsSync(queriesDir)) {
|
|
513
|
+
fs.mkdirSync(queriesDir, { recursive: true });
|
|
514
|
+
}
|
|
515
|
+
// Generate query for agent
|
|
516
|
+
const agentQueryVariables = {
|
|
517
|
+
...this.templateEngine.getVariables(),
|
|
518
|
+
queryName: 'sample-agent',
|
|
519
|
+
targetType: 'agent',
|
|
520
|
+
targetName: 'sample',
|
|
521
|
+
inputMessage: `Hello! Can you help me understand what you can do for the ${this.templateEngine.getVariables().projectName || 'sample'} project?`,
|
|
522
|
+
};
|
|
523
|
+
this.templateEngine.setVariables(agentQueryVariables);
|
|
524
|
+
await this.templateEngine.processTemplate(queryTemplatePath, queriesDir, {
|
|
525
|
+
createDirectories: false,
|
|
526
|
+
});
|
|
527
|
+
// Generate query for team
|
|
528
|
+
const teamQueryVariables = {
|
|
529
|
+
...this.templateEngine.getVariables(),
|
|
530
|
+
queryName: 'sample-team',
|
|
531
|
+
targetType: 'team',
|
|
532
|
+
targetName: 'sample',
|
|
533
|
+
inputMessage: `Hello team! Can you collaborate to help me understand how you work together for the ${this.templateEngine.getVariables().projectName || 'sample'} project?`,
|
|
534
|
+
};
|
|
535
|
+
this.templateEngine.setVariables(teamQueryVariables);
|
|
536
|
+
await this.templateEngine.processTemplate(queryTemplatePath, queriesDir, {
|
|
537
|
+
createDirectories: false,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
console.log(chalk.yellow(`ā ļø Failed to copy sample queries: ${error}`));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async copySampleModel(config) {
|
|
545
|
+
await this.copyModelFromTemplate(config, 'default', 'azure');
|
|
546
|
+
}
|
|
547
|
+
async copyModelFromTemplate(config, modelName, templateType) {
|
|
548
|
+
try {
|
|
549
|
+
// Set model-specific template variables
|
|
550
|
+
const modelVariables = {
|
|
551
|
+
...this.templateEngine.getVariables(),
|
|
552
|
+
modelName: modelName,
|
|
553
|
+
};
|
|
554
|
+
// Temporarily set model variables
|
|
555
|
+
const originalVariables = this.templateEngine.getVariables();
|
|
556
|
+
this.templateEngine.setVariables(modelVariables);
|
|
557
|
+
const templatesBasePath = this.templateDiscovery.getTemplatePath('');
|
|
558
|
+
const modelTemplatePath = path.join(templatesBasePath, 'models', `${templateType}.yaml`);
|
|
559
|
+
if (!fs.existsSync(modelTemplatePath)) {
|
|
560
|
+
console.log(chalk.yellow(`ā ļø Model template not found: ${templateType}`));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const modelsDestination = path.join(config.destination, 'models');
|
|
564
|
+
// Ensure models directory exists
|
|
565
|
+
if (!fs.existsSync(modelsDestination)) {
|
|
566
|
+
fs.mkdirSync(modelsDestination, { recursive: true });
|
|
567
|
+
}
|
|
568
|
+
// Process the specific model template
|
|
569
|
+
const outputFileName = `${modelName}.yaml`;
|
|
570
|
+
const outputPath = path.join(modelsDestination, outputFileName);
|
|
571
|
+
await this.templateEngine.processFile(modelTemplatePath, outputPath, {
|
|
572
|
+
skipIfExists: false,
|
|
573
|
+
baseDir: config.destination,
|
|
574
|
+
});
|
|
575
|
+
// Restore original variables
|
|
576
|
+
this.templateEngine.setVariables(originalVariables);
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
console.log(chalk.yellow(`ā ļø Failed to copy model template: ${error}`));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
async cleanupKeepFiles(config) {
|
|
583
|
+
// Directories that might contain .keep files
|
|
584
|
+
const directoriesToCheck = [
|
|
585
|
+
'agents',
|
|
586
|
+
'teams',
|
|
587
|
+
'queries',
|
|
588
|
+
'models',
|
|
589
|
+
'docs',
|
|
590
|
+
'tools',
|
|
591
|
+
'tests/unit',
|
|
592
|
+
'tests/e2e',
|
|
593
|
+
];
|
|
594
|
+
for (const dirPath of directoriesToCheck) {
|
|
595
|
+
const fullDirPath = path.join(config.destination, dirPath);
|
|
596
|
+
// Skip if directory doesn't exist
|
|
597
|
+
if (!fs.existsSync(fullDirPath)) {
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
try {
|
|
601
|
+
const files = fs.readdirSync(fullDirPath);
|
|
602
|
+
const keepFile = path.join(fullDirPath, '.keep');
|
|
603
|
+
// Check if .keep file exists and there are other files
|
|
604
|
+
const hasKeepFile = files.includes('.keep');
|
|
605
|
+
const hasOtherFiles = files.some((file) => file !== '.keep');
|
|
606
|
+
if (hasKeepFile && hasOtherFiles) {
|
|
607
|
+
fs.unlinkSync(keepFile);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
// Log but don't fail the generation if we can't clean up a .keep file
|
|
612
|
+
console.log(chalk.yellow(`ā ļø Could not clean up .keep file in ${dirPath}: ${error}`));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
async createEnvFile(config) {
|
|
617
|
+
const envPath = path.join(config.destination, '.env');
|
|
618
|
+
// Validate and sanitize project values
|
|
619
|
+
const sanitizedName = SecurityUtils.sanitizeEnvironmentValue(config.name, 'PROJECT_NAME');
|
|
620
|
+
const sanitizedNamespace = SecurityUtils.sanitizeEnvironmentValue(config.namespace, 'NAMESPACE');
|
|
621
|
+
// Generate dynamic environment content based on selected models
|
|
622
|
+
let envContent = `# Project Configuration
|
|
623
|
+
# Generated by ARK CLI - Do not edit the project values below
|
|
624
|
+
PROJECT_NAME=${sanitizedName}
|
|
625
|
+
NAMESPACE=${sanitizedNamespace}
|
|
626
|
+
|
|
627
|
+
# Model Configuration (used for environment variable substitution in model YAML files)
|
|
628
|
+
# Security Note: Keep these keys secret and never commit them to version control
|
|
629
|
+
|
|
630
|
+
`;
|
|
631
|
+
// Get model environment configurations
|
|
632
|
+
const modelEnvConfigs = this.getModelEnvConfigs();
|
|
633
|
+
// Determine which models to include
|
|
634
|
+
let modelsToInclude = [];
|
|
635
|
+
if (config.selectedModels === 'all') {
|
|
636
|
+
modelsToInclude = Object.keys(modelEnvConfigs);
|
|
637
|
+
}
|
|
638
|
+
else if (config.selectedModels && config.selectedModels !== 'none') {
|
|
639
|
+
modelsToInclude = [config.selectedModels];
|
|
640
|
+
}
|
|
641
|
+
// Generate environment variables for selected models
|
|
642
|
+
if (modelsToInclude.length > 0) {
|
|
643
|
+
for (const modelName of modelsToInclude) {
|
|
644
|
+
const modelConfig = modelEnvConfigs[modelName];
|
|
645
|
+
if (!modelConfig)
|
|
646
|
+
continue;
|
|
647
|
+
envContent += `# ${modelName.charAt(0).toUpperCase() + modelName.slice(1)} Configuration\n`;
|
|
648
|
+
// API Key (if applicable)
|
|
649
|
+
if (modelConfig.apiKey) {
|
|
650
|
+
envContent += `${modelConfig.apiKey}="your-${modelName}-api-key-here"\n`;
|
|
651
|
+
}
|
|
652
|
+
// Base URL (if applicable)
|
|
653
|
+
if (modelConfig.baseUrl && modelConfig.defaultBaseUrl) {
|
|
654
|
+
envContent += `${modelConfig.baseUrl}="${modelConfig.defaultBaseUrl}"\n`;
|
|
655
|
+
}
|
|
656
|
+
// Additional variables
|
|
657
|
+
if (modelConfig.additionalVars) {
|
|
658
|
+
for (const additionalVar of modelConfig.additionalVars) {
|
|
659
|
+
const value = additionalVar.defaultValue
|
|
660
|
+
? `"${additionalVar.defaultValue}"`
|
|
661
|
+
: `"your-${additionalVar.name.toLowerCase().replace(/_/g, '-')}-here"`;
|
|
662
|
+
envContent += `${additionalVar.name}=${value}`;
|
|
663
|
+
if (additionalVar.description) {
|
|
664
|
+
envContent += ` # ${additionalVar.description}`;
|
|
665
|
+
}
|
|
666
|
+
envContent += '\n';
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
envContent += '\n';
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
// If no models selected, show commented examples
|
|
674
|
+
envContent += `# Uncomment and configure the appropriate environment variables for your model provider:
|
|
675
|
+
|
|
676
|
+
# Default/Azure Configuration
|
|
677
|
+
# AZURE_API_KEY="your-azure-api-key-here"
|
|
678
|
+
# AZURE_BASE_URL="https://your-resource.openai.azure.com"
|
|
679
|
+
# AZURE_API_VERSION="2024-12-01-preview"
|
|
680
|
+
|
|
681
|
+
# Claude Configuration
|
|
682
|
+
# CLAUDE_API_KEY="your-claude-api-key-here"
|
|
683
|
+
# CLAUDE_BASE_URL="https://api.anthropic.com/v1/"
|
|
684
|
+
|
|
685
|
+
# OpenAI Configuration
|
|
686
|
+
# OPENAI_API_KEY="your-openai-api-key-here"
|
|
687
|
+
# OPENAI_BASE_URL="https://api.openai.com/v1"
|
|
688
|
+
|
|
689
|
+
# Gemini Configuration
|
|
690
|
+
# GEMINI_API_KEY="your-gemini-api-key-here"
|
|
691
|
+
# GEMINI_BASE_URL="https://generativelanguage.googleapis.com/v1beta/openai/"
|
|
692
|
+
|
|
693
|
+
`;
|
|
694
|
+
}
|
|
695
|
+
envContent += `# Additional configuration
|
|
696
|
+
# DEBUG=false
|
|
697
|
+
# LOG_LEVEL=info
|
|
698
|
+
`;
|
|
699
|
+
// Write file securely
|
|
700
|
+
await SecurityUtils.writeFileSafe(envPath, envContent, config.destination);
|
|
701
|
+
console.log(chalk.green(`š Created environment file: ${envPath}`));
|
|
702
|
+
console.log(chalk.yellow(`ā ļø Remember to set your API keys in ${path.basename(envPath)}`));
|
|
703
|
+
}
|
|
704
|
+
async setupGit(config) {
|
|
705
|
+
// Double-check git availability
|
|
706
|
+
const gitAvailable = await this.isGitAvailable();
|
|
707
|
+
if (!gitAvailable) {
|
|
708
|
+
console.log(chalk.yellow('ā ļø Git not available - skipping git setup'));
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
console.log(chalk.cyan('š Setting up git repository...'));
|
|
712
|
+
const cwd = config.destination;
|
|
713
|
+
// Initialize git
|
|
714
|
+
await execa('git', ['init'], { cwd });
|
|
715
|
+
// Add files
|
|
716
|
+
await execa('git', ['add', '.'], { cwd });
|
|
717
|
+
// Create initial commit if requested
|
|
718
|
+
if (config.createCommit) {
|
|
719
|
+
const commitMessage = `Initial commit from agents-at-scale template
|
|
720
|
+
|
|
721
|
+
Project: ${config.name}
|
|
722
|
+
Model Provider: ${config.selectedModels}
|
|
723
|
+
Namespace: ${config.namespace}
|
|
724
|
+
|
|
725
|
+
Generated with ARK CLI generator`;
|
|
726
|
+
await execa('git', ['commit', '-m', commitMessage], { cwd });
|
|
727
|
+
console.log(chalk.green('ā
Created initial git commit'));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
showNextSteps(config) {
|
|
731
|
+
// Large, prominent success message
|
|
732
|
+
console.log(chalk.green('\nš Project Created Successfully!\n'));
|
|
733
|
+
console.log(chalk.cyan(`š ${config.destination}\n`));
|
|
734
|
+
// Show next steps based on project type
|
|
735
|
+
const steps = [
|
|
736
|
+
{
|
|
737
|
+
desc: 'Navigate to your new project directory',
|
|
738
|
+
cmd: `cd ${config.destination}`,
|
|
739
|
+
},
|
|
740
|
+
];
|
|
741
|
+
if (config.projectType === 'empty') {
|
|
742
|
+
steps.push({ desc: 'Add YAML files to agents/, teams/, queries/ directories' }, { desc: 'Copy model configurations from samples/models/' }, { desc: 'Edit .env file to set your API keys' }, { desc: 'Deploy your project', cmd: 'make quickstart' });
|
|
743
|
+
}
|
|
744
|
+
else if (config.selectedModels && config.selectedModels !== 'none') {
|
|
745
|
+
steps.push({ desc: 'Edit .env file to set your API keys' }, { desc: 'Load environment variables', cmd: 'source .env' }, { desc: 'Deploy your project', cmd: 'make quickstart' }, {
|
|
746
|
+
desc: 'Test your deployment',
|
|
747
|
+
cmd: `kubectl get query sample-team-query -w --namespace ${config.namespace}`,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
steps.push({ desc: 'Copy model configurations from samples/models/' }, { desc: 'Edit .env file to set your API keys' }, { desc: 'Deploy your project', cmd: 'make quickstart' });
|
|
752
|
+
}
|
|
753
|
+
console.log(chalk.magenta.bold('š NEXT STEPS:\n'));
|
|
754
|
+
let stepNumber = 1;
|
|
755
|
+
steps.forEach((step) => {
|
|
756
|
+
if (step === '') {
|
|
757
|
+
console.log(); // Empty line for separation
|
|
758
|
+
}
|
|
759
|
+
else if (typeof step === 'string' && step.startsWith('ā¢')) {
|
|
760
|
+
// Skip the bullet points - we'll handle commands separately
|
|
761
|
+
}
|
|
762
|
+
else if (typeof step === 'object' && step !== null && 'desc' in step) {
|
|
763
|
+
// Handle step objects with description and optional command
|
|
764
|
+
console.log(chalk.yellow.bold(` ā¶ ${stepNumber}.`) +
|
|
765
|
+
' ' +
|
|
766
|
+
chalk.cyan.bold(step.desc));
|
|
767
|
+
if (step.cmd) {
|
|
768
|
+
console.log(chalk.yellow(` ${step.cmd}`));
|
|
769
|
+
}
|
|
770
|
+
console.log(); // Add space between steps
|
|
771
|
+
stepNumber++;
|
|
772
|
+
}
|
|
773
|
+
else if (typeof step === 'string') {
|
|
774
|
+
// Handle old string format
|
|
775
|
+
console.log(chalk.yellow.bold(` ā¶ ${stepNumber}.`) +
|
|
776
|
+
' ' +
|
|
777
|
+
chalk.cyan.bold(step));
|
|
778
|
+
console.log(); // Add space between steps
|
|
779
|
+
stepNumber++;
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
console.log(chalk.green('\nš Happy building with Agents at Scale!\n'));
|
|
783
|
+
}
|
|
784
|
+
}
|