@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.
Files changed (76) hide show
  1. package/README.md +95 -0
  2. package/dist/commands/cluster/get-ip.d.ts +2 -0
  3. package/dist/commands/cluster/get-ip.js +32 -0
  4. package/dist/commands/cluster/get-type.d.ts +2 -0
  5. package/dist/commands/cluster/get-type.js +26 -0
  6. package/dist/commands/cluster/index.d.ts +2 -0
  7. package/dist/commands/cluster/index.js +10 -0
  8. package/dist/commands/completion.d.ts +2 -0
  9. package/dist/commands/completion.js +108 -0
  10. package/dist/commands/config.d.ts +5 -0
  11. package/dist/commands/config.js +327 -0
  12. package/dist/commands/generate/config.d.ts +145 -0
  13. package/dist/commands/generate/config.js +253 -0
  14. package/dist/commands/generate/generators/agent.d.ts +2 -0
  15. package/dist/commands/generate/generators/agent.js +156 -0
  16. package/dist/commands/generate/generators/index.d.ts +6 -0
  17. package/dist/commands/generate/generators/index.js +6 -0
  18. package/dist/commands/generate/generators/marketplace.d.ts +2 -0
  19. package/dist/commands/generate/generators/marketplace.js +304 -0
  20. package/dist/commands/generate/generators/mcpserver.d.ts +25 -0
  21. package/dist/commands/generate/generators/mcpserver.js +350 -0
  22. package/dist/commands/generate/generators/project.d.ts +2 -0
  23. package/dist/commands/generate/generators/project.js +784 -0
  24. package/dist/commands/generate/generators/query.d.ts +2 -0
  25. package/dist/commands/generate/generators/query.js +213 -0
  26. package/dist/commands/generate/generators/team.d.ts +2 -0
  27. package/dist/commands/generate/generators/team.js +407 -0
  28. package/dist/commands/generate/index.d.ts +24 -0
  29. package/dist/commands/generate/index.js +357 -0
  30. package/dist/commands/generate/templateDiscovery.d.ts +30 -0
  31. package/dist/commands/generate/templateDiscovery.js +94 -0
  32. package/dist/commands/generate/templateEngine.d.ts +78 -0
  33. package/dist/commands/generate/templateEngine.js +368 -0
  34. package/dist/commands/generate/utils/nameUtils.d.ts +35 -0
  35. package/dist/commands/generate/utils/nameUtils.js +110 -0
  36. package/dist/commands/generate/utils/projectUtils.d.ts +28 -0
  37. package/dist/commands/generate/utils/projectUtils.js +133 -0
  38. package/dist/components/DashboardCLI.d.ts +3 -0
  39. package/dist/components/DashboardCLI.js +149 -0
  40. package/dist/components/GeneratorUI.d.ts +3 -0
  41. package/dist/components/GeneratorUI.js +167 -0
  42. package/dist/components/statusChecker.d.ts +48 -0
  43. package/dist/components/statusChecker.js +251 -0
  44. package/dist/config.d.ts +42 -0
  45. package/dist/config.js +243 -0
  46. package/dist/index.d.ts +2 -0
  47. package/dist/index.js +67 -0
  48. package/dist/lib/arkClient.d.ts +32 -0
  49. package/dist/lib/arkClient.js +43 -0
  50. package/dist/lib/cluster.d.ts +8 -0
  51. package/dist/lib/cluster.js +134 -0
  52. package/dist/lib/config.d.ts +82 -0
  53. package/dist/lib/config.js +223 -0
  54. package/dist/lib/consts.d.ts +10 -0
  55. package/dist/lib/consts.js +15 -0
  56. package/dist/lib/errors.d.ts +56 -0
  57. package/dist/lib/errors.js +208 -0
  58. package/dist/lib/exec.d.ts +5 -0
  59. package/dist/lib/exec.js +20 -0
  60. package/dist/lib/gatewayManager.d.ts +24 -0
  61. package/dist/lib/gatewayManager.js +85 -0
  62. package/dist/lib/kubernetes.d.ts +28 -0
  63. package/dist/lib/kubernetes.js +122 -0
  64. package/dist/lib/progress.d.ts +128 -0
  65. package/dist/lib/progress.js +273 -0
  66. package/dist/lib/security.d.ts +37 -0
  67. package/dist/lib/security.js +295 -0
  68. package/dist/lib/types.d.ts +37 -0
  69. package/dist/lib/types.js +1 -0
  70. package/dist/lib/wrappers/git.d.ts +2 -0
  71. package/dist/lib/wrappers/git.js +43 -0
  72. package/dist/ui/MainMenu.d.ts +3 -0
  73. package/dist/ui/MainMenu.js +116 -0
  74. package/dist/ui/statusFormatter.d.ts +9 -0
  75. package/dist/ui/statusFormatter.js +47 -0
  76. 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
+ }