@agile-vibe-coding/avc 0.1.0 → 0.1.1

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/cli/init.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import dotenv from 'dotenv';
3
4
  import fs from 'fs';
4
5
  import path from 'path';
5
6
  import { fileURLToPath } from 'url';
@@ -19,11 +20,20 @@ const __dirname = path.dirname(__filename);
19
20
  */
20
21
 
21
22
  class ProjectInitiator {
22
- constructor() {
23
- this.projectRoot = process.cwd();
23
+ constructor(projectRoot = null) {
24
+ this.projectRoot = projectRoot || process.cwd();
24
25
  this.avcDir = path.join(this.projectRoot, '.avc');
25
26
  this.avcConfigPath = path.join(this.avcDir, 'avc.json');
26
- this.progressPath = path.join(this.avcDir, 'init-progress.json');
27
+ // Progress files are ceremony-specific
28
+ this.initProgressPath = path.join(this.avcDir, 'init-progress.json');
29
+ this.sponsorCallProgressPath = path.join(this.avcDir, 'sponsor-call-progress.json');
30
+
31
+ // Load environment variables from project .env file
32
+ // Use override: true to reload even if already set (user may have edited .env)
33
+ dotenv.config({
34
+ path: path.join(this.projectRoot, '.env'),
35
+ override: true
36
+ });
27
37
  }
28
38
 
29
39
  /**
@@ -33,6 +43,47 @@ class ProjectInitiator {
33
43
  return path.basename(this.projectRoot);
34
44
  }
35
45
 
46
+ /**
47
+ * Get the current AVC package version
48
+ */
49
+ getAvcVersion() {
50
+ try {
51
+ const packagePath = path.join(__dirname, '../package.json');
52
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
53
+ return packageJson.version;
54
+ } catch (error) {
55
+ return 'unknown';
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Deep merge objects - adds new keys, preserves existing values
61
+ * @param {Object} target - The target object (user's config)
62
+ * @param {Object} source - The source object (default config)
63
+ * @returns {Object} Merged object
64
+ */
65
+ deepMerge(target, source) {
66
+ const result = { ...target };
67
+
68
+ for (const key in source) {
69
+ if (source.hasOwnProperty(key)) {
70
+ if (key in result) {
71
+ // Key exists in target
72
+ if (typeof source[key] === 'object' && !Array.isArray(source[key]) && source[key] !== null) {
73
+ // Recursively merge objects
74
+ result[key] = this.deepMerge(result[key], source[key]);
75
+ }
76
+ // else: Keep existing value (don't overwrite)
77
+ } else {
78
+ // New key - add it
79
+ result[key] = source[key];
80
+ }
81
+ }
82
+ }
83
+
84
+ return result;
85
+ }
86
+
36
87
  /**
37
88
  * Check if .avc folder exists
38
89
  */
@@ -61,23 +112,45 @@ class ProjectInitiator {
61
112
  }
62
113
 
63
114
  /**
64
- * Create avc.json with default settings
115
+ * Create or update avc.json with default settings
116
+ * Merges new attributes from version updates while preserving existing values
65
117
  */
66
118
  createAvcConfig() {
67
- if (!this.hasAvcConfig()) {
68
- const defaultConfig = {
69
- version: '1.0.0',
70
- projectName: this.getProjectName(),
71
- framework: 'avc',
72
- created: new Date().toISOString(),
73
- settings: {
74
- contextScopes: ['epic', 'story', 'task', 'subtask'],
75
- workItemStatuses: ['ready', 'pending', 'implementing', 'implemented', 'testing', 'completed', 'blocked', 'feedback'],
76
- agentTypes: ['product-owner', 'server', 'client', 'infrastructure', 'testing'],
77
- model: 'claude-sonnet-4-5-20250929'
78
- }
79
- };
119
+ const defaultConfig = {
120
+ version: '1.0.0',
121
+ avcVersion: this.getAvcVersion(),
122
+ projectName: this.getProjectName(),
123
+ framework: 'avc',
124
+ created: new Date().toISOString(),
125
+ settings: {
126
+ contextScopes: ['epic', 'story', 'task', 'subtask'],
127
+ workItemStatuses: ['ready', 'pending', 'implementing', 'implemented', 'testing', 'completed', 'blocked', 'feedback'],
128
+ agentTypes: ['product-owner', 'server', 'client', 'infrastructure', 'testing'],
129
+ documentation: {
130
+ port: 4173
131
+ },
132
+ ceremonies: [
133
+ {
134
+ name: 'sponsor-call',
135
+ defaultModel: 'claude-sonnet-4-5-20250929',
136
+ provider: 'claude',
137
+ agents: [
138
+ {
139
+ name: 'documentation',
140
+ instruction: 'documentation.md',
141
+ stage: 'enhancement'
142
+ }
143
+ ],
144
+ guidelines: {
145
+ technicalConsiderations: 'Use AWS serverless stack with Lambda functions for compute, API Gateway for REST APIs, DynamoDB for database, S3 for storage. Use CloudFormation for infrastructure definition and AWS CodePipeline/CodeBuild for CI/CD deployment.'
146
+ }
147
+ }
148
+ ]
149
+ }
150
+ };
80
151
 
152
+ if (!this.hasAvcConfig()) {
153
+ // Create new config
81
154
  fs.writeFileSync(
82
155
  this.avcConfigPath,
83
156
  JSON.stringify(defaultConfig, null, 2),
@@ -86,8 +159,35 @@ class ProjectInitiator {
86
159
  console.log('✓ Created .avc/avc.json configuration file');
87
160
  return true;
88
161
  }
89
- console.log('✓ .avc/avc.json already exists');
90
- return false;
162
+
163
+ // Config exists - check for merge
164
+ try {
165
+ const existingConfig = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
166
+
167
+ // Merge: add new keys, keep existing values
168
+ const mergedConfig = this.deepMerge(existingConfig, defaultConfig);
169
+
170
+ // Update avcVersion to track CLI version
171
+ mergedConfig.avcVersion = this.getAvcVersion();
172
+ mergedConfig.updated = new Date().toISOString();
173
+
174
+ // Check if anything changed
175
+ const existingJson = JSON.stringify(existingConfig, null, 2);
176
+ const mergedJson = JSON.stringify(mergedConfig, null, 2);
177
+
178
+ if (existingJson !== mergedJson) {
179
+ fs.writeFileSync(this.avcConfigPath, mergedJson, 'utf8');
180
+ console.log('✓ Updated .avc/avc.json with new configuration attributes');
181
+ return true;
182
+ }
183
+
184
+ console.log('✓ .avc/avc.json is up to date');
185
+ return false;
186
+ } catch (error) {
187
+ console.error(`⚠️ Warning: Could not merge avc.json: ${error.message}`);
188
+ console.log('✓ .avc/avc.json already exists (merge skipped)');
189
+ return false;
190
+ }
91
191
  }
92
192
 
93
193
  /**
@@ -113,7 +213,9 @@ class ProjectInitiator {
113
213
  # Get your key at: https://console.anthropic.com/settings/keys
114
214
  ANTHROPIC_API_KEY=
115
215
 
116
- # Add other API keys below as needed
216
+ # Google Gemini API Key (alternative LLM provider)
217
+ # Get your key at: https://aistudio.google.com/app/apikey
218
+ GEMINI_API_KEY=
117
219
  `;
118
220
  fs.writeFileSync(envPath, envContent, 'utf8');
119
221
  console.log('✓ Created .env file for API keys');
@@ -138,34 +240,195 @@ ANTHROPIC_API_KEY=
138
240
  gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
139
241
  }
140
242
 
141
- // Check if .env is already in .gitignore
142
- if (gitignoreContent.includes('.env')) {
143
- console.log('.env already in .gitignore');
144
- return;
243
+ // Items to add to gitignore
244
+ const itemsToIgnore = [
245
+ { pattern: '.env', comment: 'Environment variables' },
246
+ { pattern: '.avc/documentation/.vitepress/dist', comment: 'VitePress build output' },
247
+ { pattern: '.avc/documentation/.vitepress/cache', comment: 'VitePress cache' },
248
+ { pattern: '.avc/logs', comment: 'Command execution logs' }
249
+ ];
250
+
251
+ let newContent = gitignoreContent;
252
+ let addedItems = [];
253
+
254
+ for (const item of itemsToIgnore) {
255
+ if (!newContent.includes(item.pattern)) {
256
+ if (!newContent.endsWith('\n') && newContent.length > 0) {
257
+ newContent += '\n';
258
+ }
259
+ if (!newContent.includes(`# ${item.comment}`)) {
260
+ newContent += `\n# ${item.comment}\n`;
261
+ }
262
+ newContent += `${item.pattern}\n`;
263
+ addedItems.push(item.pattern);
264
+ }
265
+ }
266
+
267
+ if (addedItems.length > 0) {
268
+ fs.writeFileSync(gitignorePath, newContent, 'utf8');
269
+ console.log(`✓ Added to .gitignore: ${addedItems.join(', ')}`);
270
+ } else {
271
+ console.log('✓ .gitignore already up to date');
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Create VitePress documentation setup
277
+ */
278
+ createVitePressSetup() {
279
+ const docsDir = path.join(this.avcDir, 'documentation');
280
+ const vitepressDir = path.join(docsDir, '.vitepress');
281
+ const publicDir = path.join(docsDir, 'public');
282
+
283
+ // Create directory structure
284
+ if (!fs.existsSync(vitepressDir)) {
285
+ fs.mkdirSync(vitepressDir, { recursive: true });
286
+ console.log('✓ Created .avc/documentation/.vitepress/ folder');
287
+ } else {
288
+ console.log('✓ .avc/documentation/.vitepress/ folder already exists');
289
+ }
290
+
291
+ if (!fs.existsSync(publicDir)) {
292
+ fs.mkdirSync(publicDir, { recursive: true });
293
+ console.log('✓ Created .avc/documentation/public/ folder');
294
+ } else {
295
+ console.log('✓ .avc/documentation/public/ folder already exists');
296
+ }
297
+
298
+ // Create VitePress config
299
+ const configPath = path.join(vitepressDir, 'config.mts');
300
+ if (!fs.existsSync(configPath)) {
301
+ const templatePath = path.join(__dirname, 'templates/vitepress-config.mts.template');
302
+ let configContent = fs.readFileSync(templatePath, 'utf8');
303
+ configContent = configContent.replace('{{PROJECT_NAME}}', this.getProjectName());
304
+ fs.writeFileSync(configPath, configContent, 'utf8');
305
+ console.log('✓ Created .avc/documentation/.vitepress/config.mts');
306
+ } else {
307
+ console.log('✓ .avc/documentation/.vitepress/config.mts already exists');
308
+ }
309
+
310
+ // Create initial index.md
311
+ const indexPath = path.join(docsDir, 'index.md');
312
+ if (!fs.existsSync(indexPath)) {
313
+ const indexContent = `# ${this.getProjectName()}
314
+
315
+ ## Project Status
316
+
317
+ This project is being developed using the [Agile Vibe Coding](https://agilevibecoding.org) framework.
318
+
319
+ **Current Stage**: Initial Setup
320
+
321
+ Project documentation will be generated automatically as the project is defined and developed.
322
+
323
+ ## About This Documentation
324
+
325
+ This site provides comprehensive documentation about **${this.getProjectName()}**, including:
326
+
327
+ - Project overview and objectives
328
+ - Feature specifications organized by epics and stories
329
+ - Technical architecture and design decisions
330
+ - Implementation progress and status
331
+
332
+ Documentation is automatically updated from the AVC project structure as development progresses.
333
+
334
+ ## Getting Started with AVC
335
+
336
+ If you're new to Agile Vibe Coding, visit the [AVC Documentation](https://agilevibecoding.org) to learn about:
337
+
338
+ - [CLI Commands](https://agilevibecoding.org/commands) - Available commands and their usage
339
+ - [Installation Guide](https://agilevibecoding.org/install) - Setup instructions
340
+ - [Framework Overview](https://agilevibecoding.org) - Core concepts and workflow
341
+
342
+ ---
343
+
344
+ *Documentation powered by [Agile Vibe Coding](https://agilevibecoding.org)*
345
+ `;
346
+ fs.writeFileSync(indexPath, indexContent, 'utf8');
347
+ console.log('✓ Created .avc/documentation/index.md');
348
+ } else {
349
+ console.log('✓ .avc/documentation/index.md already exists');
350
+ }
351
+
352
+ // Update package.json with VitePress scripts
353
+ this.updatePackageJsonForVitePress();
354
+ }
355
+
356
+ /**
357
+ * Update package.json with VitePress dependencies and scripts
358
+ */
359
+ updatePackageJsonForVitePress() {
360
+ const packagePath = path.join(this.projectRoot, 'package.json');
361
+
362
+ let packageJson;
363
+ if (fs.existsSync(packagePath)) {
364
+ packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
365
+ } else {
366
+ packageJson = {
367
+ name: this.getProjectName(),
368
+ version: '1.0.0',
369
+ private: true
370
+ };
145
371
  }
146
372
 
147
- // Add .env to .gitignore
148
- const newContent = gitignoreContent
149
- ? `${gitignoreContent}\n# Environment variables\n.env\n`
150
- : '# Environment variables\n.env\n';
373
+ // Add scripts
374
+ if (!packageJson.scripts) {
375
+ packageJson.scripts = {};
376
+ }
151
377
 
152
- fs.writeFileSync(gitignorePath, newContent, 'utf8');
153
- console.log(' Added .env to .gitignore');
378
+ const scriptsToAdd = {
379
+ 'docs:dev': 'vitepress dev .avc/documentation',
380
+ 'docs:build': 'vitepress build .avc/documentation',
381
+ 'docs:preview': 'vitepress preview .avc/documentation'
382
+ };
383
+
384
+ let addedScripts = [];
385
+ for (const [name, command] of Object.entries(scriptsToAdd)) {
386
+ if (!packageJson.scripts[name]) {
387
+ packageJson.scripts[name] = command;
388
+ addedScripts.push(name);
389
+ }
390
+ }
391
+
392
+ // Add devDependencies
393
+ if (!packageJson.devDependencies) {
394
+ packageJson.devDependencies = {};
395
+ }
396
+
397
+ let addedDeps = false;
398
+ if (!packageJson.devDependencies.vitepress) {
399
+ packageJson.devDependencies.vitepress = '^1.6.4';
400
+ addedDeps = true;
401
+ }
402
+
403
+ // Write package.json
404
+ fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
405
+
406
+ if (addedScripts.length > 0 || addedDeps) {
407
+ console.log('✓ Updated package.json with VitePress configuration');
408
+ if (addedScripts.length > 0) {
409
+ console.log(` Added scripts: ${addedScripts.join(', ')}`);
410
+ }
411
+ if (addedDeps) {
412
+ console.log(' Added devDependency: vitepress');
413
+ }
414
+ } else {
415
+ console.log('✓ package.json already has VitePress configuration');
416
+ }
154
417
  }
155
418
 
156
419
  /**
157
- * Check if there's an incomplete init in progress
420
+ * Check if there's an incomplete ceremony in progress
158
421
  */
159
- hasIncompleteInit() {
160
- return fs.existsSync(this.progressPath);
422
+ hasIncompleteProgress(progressPath) {
423
+ return fs.existsSync(progressPath);
161
424
  }
162
425
 
163
426
  /**
164
427
  * Read progress from file
165
428
  */
166
- readProgress() {
429
+ readProgress(progressPath) {
167
430
  try {
168
- const content = fs.readFileSync(this.progressPath, 'utf8');
431
+ const content = fs.readFileSync(progressPath, 'utf8');
169
432
  return JSON.parse(content);
170
433
  } catch (error) {
171
434
  return null;
@@ -175,28 +438,104 @@ ANTHROPIC_API_KEY=
175
438
  /**
176
439
  * Write progress to file
177
440
  */
178
- writeProgress(progress) {
441
+ writeProgress(progress, progressPath) {
179
442
  if (!fs.existsSync(this.avcDir)) {
180
443
  fs.mkdirSync(this.avcDir, { recursive: true });
181
444
  }
182
- fs.writeFileSync(this.progressPath, JSON.stringify(progress, null, 2), 'utf8');
445
+ fs.writeFileSync(progressPath, JSON.stringify(progress, null, 2), 'utf8');
183
446
  }
184
447
 
185
448
  /**
186
- * Clear progress file (init completed successfully)
449
+ * Clear progress file (ceremony completed successfully)
187
450
  */
188
- clearProgress() {
189
- if (fs.existsSync(this.progressPath)) {
190
- fs.unlinkSync(this.progressPath);
451
+ clearProgress(progressPath) {
452
+ if (fs.existsSync(progressPath)) {
453
+ fs.unlinkSync(progressPath);
191
454
  }
192
455
  }
193
456
 
194
457
 
458
+ /**
459
+ * Validate that the configured provider's API key is present and working
460
+ */
461
+ async validateProviderApiKey() {
462
+ // Import LLMProvider dynamically to avoid circular dependencies
463
+ const { LLMProvider } = await import('./llm-provider.js');
464
+
465
+ // Check if config file exists
466
+ if (!fs.existsSync(this.avcConfigPath)) {
467
+ return {
468
+ valid: false,
469
+ message: 'Configuration file not found at .avc/avc.json.\n Please run /init first to set up your project.'
470
+ };
471
+ }
472
+
473
+ // Read provider config from avc.json
474
+ const config = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
475
+ const ceremony = config.settings?.ceremonies?.[0];
476
+
477
+ if (!ceremony) {
478
+ return {
479
+ valid: false,
480
+ message: 'No ceremonies configured in .avc/avc.json.\n Please check your configuration.'
481
+ };
482
+ }
483
+
484
+ const providerName = ceremony.provider || 'claude';
485
+ const modelName = ceremony.defaultModel || 'claude-sonnet-4-5-20250929';
486
+
487
+ // Check which env var is required
488
+ const envVarMap = {
489
+ 'claude': 'ANTHROPIC_API_KEY',
490
+ 'gemini': 'GEMINI_API_KEY'
491
+ };
492
+
493
+ const requiredEnvVar = envVarMap[providerName];
494
+ if (!requiredEnvVar) {
495
+ return {
496
+ valid: false,
497
+ message: `Unknown provider "${providerName}".\n Supported providers: claude, gemini`
498
+ };
499
+ }
500
+
501
+ // Check if API key is set in environment
502
+ if (!process.env[requiredEnvVar]) {
503
+ return {
504
+ valid: false,
505
+ message: `${requiredEnvVar} not found in .env file.\n\n Steps to fix:\n 1. Open .env file in the current directory\n 2. Add your API key: ${requiredEnvVar}=your-key-here\n 3. Save the file and run /init again\n\n Get your API key:\n ${providerName === 'claude' ? '• https://console.anthropic.com/settings/keys' : '• https://aistudio.google.com/app/apikey'}`
506
+ };
507
+ }
508
+
509
+ console.log(`\n🔑 Validating ${providerName} API key...`);
510
+
511
+ // Test the API key with a minimal call
512
+ let result;
513
+ try {
514
+ result = await LLMProvider.validate(providerName, modelName);
515
+ } catch (error) {
516
+ return {
517
+ valid: false,
518
+ message: `${requiredEnvVar} validation failed.\n\n Error: ${error.message}\n\n This could be due to:\n • Network connectivity issues\n • API service temporarily unavailable\n • Invalid API key\n\n Please check your connection and try again.`
519
+ };
520
+ }
521
+
522
+ if (!result.valid) {
523
+ const errorMsg = result.error || 'Unknown error';
524
+ return {
525
+ valid: false,
526
+ message: `${requiredEnvVar} is set but API call failed.\n\n Error: ${errorMsg}\n\n Steps to fix:\n 1. Verify your API key is correct in .env file\n 2. Check that the key has not expired\n 3. Ensure you have API credits/quota available\n\n Get a new API key if needed:\n ${providerName === 'claude' ? '• https://console.anthropic.com/settings/keys' : '• https://aistudio.google.com/app/apikey'}`
527
+ };
528
+ }
529
+
530
+ console.log(`✓ API key validated successfully\n`);
531
+ return { valid: true };
532
+ }
533
+
195
534
  /**
196
535
  * Generate project document via Sponsor Call ceremony
197
536
  */
198
- async generateProjectDocument(progress = null) {
199
- const processor = new TemplateProcessor(this.progressPath);
537
+ async generateProjectDocument(progress = null, progressPath = null, nonInteractive = false) {
538
+ const processor = new TemplateProcessor(progressPath || this.sponsorCallProgressPath, nonInteractive);
200
539
  await processor.processTemplate(progress);
201
540
  }
202
541
 
@@ -208,46 +547,152 @@ ANTHROPIC_API_KEY=
208
547
  }
209
548
 
210
549
  /**
211
- * Initialize the AVC project
550
+ * Initialize the AVC project structure (no API keys required)
551
+ * Creates .avc folder, avc.json config, .env file, and gitignore entry
212
552
  */
213
553
  async init() {
214
- console.log('\n🚀 AVC Project Initiator - Sponsor Call Ceremony\n');
554
+ console.log('\n🚀 AVC Project Initiator\n');
555
+ console.log(`Project directory: ${this.projectRoot}\n`);
556
+
557
+ if (this.isAvcProject()) {
558
+ // Project already initialized
559
+ console.log('✓ AVC project already initialized');
560
+ console.log('\nProject is ready to use.');
561
+ return;
562
+ }
563
+
564
+ // Suppress all console output during initialization
565
+ const originalLog = console.log;
566
+ console.log = () => {};
567
+
568
+ try {
569
+ // Create project structure silently
570
+ this.createAvcFolder();
571
+ this.createAvcConfig();
572
+ this.createEnvFile();
573
+ this.addToGitignore();
574
+ this.createVitePressSetup();
575
+ } finally {
576
+ console.log = originalLog;
577
+ }
578
+
579
+ console.log('\n✅ AVC project initialized!\n');
580
+ console.log('Next steps:');
581
+ console.log(' 1. Add your API key(s) to .env file');
582
+ console.log(' • ANTHROPIC_API_KEY for Claude');
583
+ console.log(' • GEMINI_API_KEY for Gemini');
584
+ console.log(' 2. Run /sponsor-call to start\n');
585
+ }
586
+
587
+ /**
588
+ * Run Sponsor Call ceremony with pre-filled answers from REPL questionnaire
589
+ * Used when all answers are collected via REPL UI
590
+ */
591
+ async sponsorCallWithAnswers(answers) {
592
+ console.log('\n🎯 Sponsor Call Ceremony\n');
215
593
  console.log(`Project directory: ${this.projectRoot}\n`);
216
594
 
595
+ // Check if project is initialized
596
+ if (!this.isAvcProject()) {
597
+ console.log('❌ Project not initialized\n');
598
+ console.log(' Please run /init first to create the project structure.\n');
599
+ return;
600
+ }
601
+
602
+ const progressPath = this.sponsorCallProgressPath;
603
+
604
+ console.log('Starting Sponsor Call ceremony with provided answers...\n');
605
+
606
+ // Count answers provided
607
+ const answeredCount = Object.values(answers).filter(v => v !== null && v !== '').length;
608
+ console.log(`📊 Received ${answeredCount}/5 answers from questionnaire\n`);
609
+
610
+ // Validate API key before starting ceremony
611
+ console.log('Step 1/3: Validating API configuration...');
612
+ const validationResult = await this.validateProviderApiKey();
613
+ if (!validationResult.valid) {
614
+ console.log('\n❌ API Key Validation Failed\n');
615
+ console.log(` ${validationResult.message}\n`);
616
+ return;
617
+ }
618
+
619
+ // Create progress with pre-filled answers
620
+ console.log('Step 2/3: Processing questionnaire answers...');
621
+ const progress = {
622
+ stage: 'questionnaire',
623
+ totalQuestions: 5,
624
+ answeredQuestions: 5,
625
+ collectedValues: answers,
626
+ lastUpdate: new Date().toISOString()
627
+ };
628
+ this.writeProgress(progress, progressPath);
629
+
630
+ // Generate project document with pre-filled answers
631
+ console.log('Step 3/3: Generating project document...');
632
+ await this.generateProjectDocument(progress, progressPath, true); // nonInteractive = true
633
+
634
+ // Mark as completed and clean up
635
+ progress.stage = 'completed';
636
+ progress.lastUpdate = new Date().toISOString();
637
+ this.writeProgress(progress, progressPath);
638
+ this.clearProgress(progressPath);
639
+
640
+ console.log('\n✅ Project defined successfully!');
641
+ console.log('\nNext steps:');
642
+ console.log(' 1. Review .avc/project/doc.md for your project definition');
643
+ console.log(' 2. Review .avc/avc.json configuration');
644
+ }
645
+
646
+ /**
647
+ * Run Sponsor Call ceremony to define project with AI assistance
648
+ * Requires API keys to be configured in .env file
649
+ */
650
+ async sponsorCall() {
651
+ console.log('\n🎯 Sponsor Call Ceremony\n');
652
+ console.log(`Project directory: ${this.projectRoot}\n`);
653
+
654
+ // Check if running in REPL mode
655
+ const isReplMode = process.env.AVC_REPL_MODE === 'true';
656
+ if (isReplMode) {
657
+ // REPL mode is handled by repl-ink.js questionnaire display
658
+ // This code path shouldn't be reached from REPL
659
+ console.log('⚠️ Unexpected: Ceremony called directly from REPL');
660
+ return;
661
+ }
662
+
663
+ // Check if project is initialized
664
+ if (!this.isAvcProject()) {
665
+ console.log('❌ Project not initialized\n');
666
+ console.log(' Please run /init first to create the project structure.\n');
667
+ return; // Don't exit in REPL mode
668
+ }
669
+
217
670
  let progress = null;
671
+ const progressPath = this.sponsorCallProgressPath;
218
672
 
219
- // Check for incomplete initialization
220
- if (this.hasIncompleteInit()) {
221
- progress = this.readProgress();
673
+ // Check for incomplete ceremony
674
+ if (this.hasIncompleteProgress(progressPath)) {
675
+ progress = this.readProgress(progressPath);
222
676
 
223
677
  if (progress && progress.stage !== 'completed') {
224
- console.log('⚠️ Found incomplete initialization from previous session');
678
+ console.log('⚠️ Found incomplete ceremony from previous session');
225
679
  console.log(` Last activity: ${new Date(progress.lastUpdate).toLocaleString()}`);
226
680
  console.log(` Stage: ${progress.stage}`);
227
681
  console.log(` Progress: ${progress.answeredQuestions || 0}/${progress.totalQuestions || 0} questions answered`);
228
682
  console.log('\n▶️ Continuing from where you left off...\n');
229
683
  }
230
- } else if (this.isAvcProject()) {
231
- // No incomplete progress but project exists - already initialized
232
- console.log('✓ AVC project already initialized');
233
- console.log('\nProject is ready to use.');
234
- return;
235
684
  } else {
236
685
  // Fresh start
237
- console.log('Initializing AVC project...\n');
686
+ console.log('Starting Sponsor Call ceremony...\n');
238
687
  }
239
688
 
240
- // Create .avc folder
241
- this.createAvcFolder();
242
-
243
- // Create avc.json
244
- this.createAvcConfig();
245
-
246
- // Create .env file for API keys
247
- this.createEnvFile();
248
-
249
- // Add .env to .gitignore if git repository
250
- this.addToGitignore();
689
+ // Validate API key before starting ceremony
690
+ const validationResult = await this.validateProviderApiKey();
691
+ if (!validationResult.valid) {
692
+ console.log('\n❌ API Key Validation Failed\n');
693
+ console.log(` ${validationResult.message}\n`);
694
+ return; // Don't exit in REPL mode
695
+ }
251
696
 
252
697
  // Save initial progress
253
698
  if (!progress) {
@@ -258,25 +703,24 @@ ANTHROPIC_API_KEY=
258
703
  collectedValues: {},
259
704
  lastUpdate: new Date().toISOString()
260
705
  };
261
- this.writeProgress(progress);
706
+ this.writeProgress(progress, progressPath);
262
707
  }
263
708
 
264
709
  // Generate project document via Sponsor Call ceremony
265
- await this.generateProjectDocument(progress);
710
+ await this.generateProjectDocument(progress, progressPath, isReplMode);
266
711
 
267
712
  // Mark as completed and clean up
268
713
  progress.stage = 'completed';
269
714
  progress.lastUpdate = new Date().toISOString();
270
- this.writeProgress(progress);
271
- this.clearProgress();
715
+ this.writeProgress(progress, progressPath);
716
+ this.clearProgress(progressPath);
272
717
 
273
- console.log('\n✅ AVC project initialized successfully!');
718
+ console.log('\n✅ Project defined successfully!');
274
719
  console.log('\nNext steps:');
275
- console.log(' 1. Add your ANTHROPIC_API_KEY to .env file');
276
- console.log(' 2. Review .avc/project/doc.md for your project definition');
277
- console.log(' 3. Review .avc/avc.json configuration');
278
- console.log(' 4. Create your project context and work items');
279
- console.log(' 5. Use AI agents to implement features');
720
+ console.log(' 1. Review .avc/project/doc.md for your project definition');
721
+ console.log(' 2. Review .avc/avc.json configuration');
722
+ console.log(' 3. Create your project context and work items');
723
+ console.log(' 4. Use AI agents to implement features');
280
724
  }
281
725
 
282
726
  /**
@@ -297,6 +741,184 @@ ANTHROPIC_API_KEY=
297
741
  console.log('\nRun "avc init" to initialize the project.');
298
742
  }
299
743
  }
744
+
745
+ /**
746
+ * Remove AVC project structure (destructive operation)
747
+ * Requires confirmation by typing "delete all"
748
+ */
749
+ async remove() {
750
+ console.log('\n🗑️ Remove AVC Project Structure\n');
751
+ console.log(`Project directory: ${this.projectRoot}\n`);
752
+
753
+ // Check if project is initialized
754
+ if (!this.isAvcProject()) {
755
+ console.log('⚠️ No AVC project found in this directory.\n');
756
+ console.log('Nothing to remove.\n');
757
+ return;
758
+ }
759
+
760
+ // Show what will be deleted
761
+ console.log('⚠️ WARNING: This is a DESTRUCTIVE operation!\n');
762
+ console.log('The following will be PERMANENTLY DELETED:\n');
763
+
764
+ // List contents of .avc folder
765
+ const avcContents = this.getAvcContents();
766
+ if (avcContents.length > 0) {
767
+ console.log('📁 .avc/ folder contents:');
768
+ avcContents.forEach(item => {
769
+ console.log(` • ${item}`);
770
+ });
771
+ console.log('');
772
+ }
773
+
774
+ console.log('❌ All project definitions, epics, stories, tasks, and documentation will be lost.');
775
+ console.log('❌ All VitePress documentation will be deleted.');
776
+ console.log('❌ This action CANNOT be undone.\n');
777
+
778
+ // Check for .env file
779
+ const envPath = path.join(this.projectRoot, '.env');
780
+ const hasEnvFile = fs.existsSync(envPath);
781
+ if (hasEnvFile) {
782
+ console.log('ℹ️ Note: The .env file will NOT be deleted.');
783
+ console.log(' You may want to manually remove API keys if no longer needed.\n');
784
+ }
785
+
786
+ // Check if running in REPL mode
787
+ const isReplMode = process.env.AVC_REPL_MODE === 'true';
788
+
789
+ if (isReplMode) {
790
+ // In REPL mode, interactive confirmation is handled by repl-ink.js
791
+ // This code path shouldn't be reached from REPL
792
+ console.log('⚠️ Unexpected: Remove called directly from REPL');
793
+ console.log('Interactive confirmation should be handled by REPL interface.');
794
+ return;
795
+ }
796
+
797
+ console.log('─'.repeat(60));
798
+ console.log('To confirm deletion, type exactly: delete all');
799
+ console.log('To cancel, type anything else or press Ctrl+C');
800
+ console.log('─'.repeat(60));
801
+ console.log('');
802
+
803
+ // Create readline interface for confirmation
804
+ const readline = await import('readline');
805
+ const rl = readline.createInterface({
806
+ input: process.stdin,
807
+ output: process.stdout
808
+ });
809
+
810
+ return new Promise((resolve) => {
811
+ rl.question('Confirmation: ', (answer) => {
812
+ rl.close();
813
+ console.log('');
814
+
815
+ if (answer.trim() === 'delete all') {
816
+ // Proceed with deletion
817
+ console.log('🗑️ Deleting AVC project structure...\n');
818
+
819
+ try {
820
+ // Get list of what's being deleted before deletion
821
+ const deletedItems = this.getAvcContents();
822
+
823
+ // Delete .avc folder
824
+ fs.rmSync(this.avcDir, { recursive: true, force: true });
825
+
826
+ console.log('✅ Successfully deleted:\n');
827
+ console.log(' 📁 .avc/ folder and all contents:');
828
+ deletedItems.forEach(item => {
829
+ console.log(` • ${item}`);
830
+ });
831
+ console.log('');
832
+
833
+ // Reminder about .env file
834
+ if (hasEnvFile) {
835
+ console.log('ℹ️ Manual cleanup reminder:\n');
836
+ console.log(' The .env file was NOT deleted and still contains:');
837
+ console.log(' • ANTHROPIC_API_KEY');
838
+ console.log(' • GEMINI_API_KEY');
839
+ console.log(' • (and any other API keys you added)\n');
840
+ console.log(' If these API keys are not used elsewhere in your project,');
841
+ console.log(' you may want to manually delete the .env file or remove');
842
+ console.log(' the unused keys.\n');
843
+ }
844
+
845
+ console.log('✅ AVC project structure has been completely removed.\n');
846
+ console.log('You can re-initialize anytime by running /init\n');
847
+
848
+ resolve();
849
+ } catch (error) {
850
+ console.log(`❌ Error during deletion: ${error.message}\n`);
851
+ console.log('The .avc folder may be partially deleted.');
852
+ console.log('You may need to manually remove it.\n');
853
+ resolve();
854
+ }
855
+ } else {
856
+ // Cancellation
857
+ console.log('❌ Operation cancelled.\n');
858
+ console.log('No files were deleted.\n');
859
+ resolve();
860
+ }
861
+ });
862
+ });
863
+ }
864
+
865
+ /**
866
+ * Get list of contents in .avc folder for display
867
+ */
868
+ getAvcContents() {
869
+ const contents = [];
870
+
871
+ try {
872
+ if (!fs.existsSync(this.avcDir)) {
873
+ return contents;
874
+ }
875
+
876
+ // Read .avc directory
877
+ const items = fs.readdirSync(this.avcDir);
878
+
879
+ items.forEach(item => {
880
+ const itemPath = path.join(this.avcDir, item);
881
+ const stat = fs.statSync(itemPath);
882
+
883
+ if (stat.isDirectory()) {
884
+ // Count items in subdirectories
885
+ const subItems = this.countItemsRecursive(itemPath);
886
+ contents.push(`${item}/ (${subItems} items)`);
887
+ } else {
888
+ contents.push(item);
889
+ }
890
+ });
891
+ } catch (error) {
892
+ // Ignore errors, return what we have
893
+ }
894
+
895
+ return contents;
896
+ }
897
+
898
+ /**
899
+ * Recursively count items in a directory
900
+ */
901
+ countItemsRecursive(dirPath) {
902
+ let count = 0;
903
+
904
+ try {
905
+ const items = fs.readdirSync(dirPath);
906
+ count += items.length;
907
+
908
+ items.forEach(item => {
909
+ const itemPath = path.join(dirPath, item);
910
+ const stat = fs.statSync(itemPath);
911
+
912
+ if (stat.isDirectory()) {
913
+ count += this.countItemsRecursive(itemPath);
914
+ }
915
+ });
916
+ } catch (error) {
917
+ // Ignore errors
918
+ }
919
+
920
+ return count;
921
+ }
300
922
  }
301
923
 
302
924
  // Export for use in REPL
@@ -311,11 +933,17 @@ if (import.meta.url === `file://${process.argv[1]}`) {
311
933
  case 'init':
312
934
  initiator.init();
313
935
  break;
936
+ case 'sponsor-call':
937
+ initiator.sponsorCall();
938
+ break;
314
939
  case 'status':
315
940
  initiator.status();
316
941
  break;
942
+ case 'remove':
943
+ initiator.remove();
944
+ break;
317
945
  default:
318
- console.log('Unknown command. Available commands: init, status');
946
+ console.log('Unknown command. Available commands: init, sponsor-call, status, remove');
319
947
  process.exit(1);
320
948
  }
321
949
  }