@iservu-inc/adf-cli 0.17.0 → 0.17.5

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.
@@ -1,467 +1,545 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
- const inquirer = require('inquirer');
4
- const chalk = require('chalk');
5
- const ora = require('ora');
6
- const {
7
- detectProjectType,
8
- getWorkflowRecommendation
9
- } = require('../utils/project-detector');
10
- const FrameworkDetector = require('../utils/framework-detector');
11
- const ContextExtractor = require('../utils/context-extractor');
12
- const SynthesisEngine = require('../analysis/synthesis-engine');
13
- const HeuristicGapAnalyzer = require('../analysis/heuristic-gap-analyzer');
14
- const AIGapAnalyzer = require('../analysis/ai-gap-analyzer');
15
- const DynamicQuestionGenerator = require('../analysis/dynamic-question-generator');
16
- const Interviewer = require('../frameworks/interviewer');
17
- const SessionManager = require('../frameworks/session-manager');
18
- const { deployToTool } = require('./deploy');
19
- const { configureAIProvider, loadEnvIntoProcess, getEnvFilePath } = require('../ai/ai-config');
20
-
21
- async function init(options) {
22
- console.log(chalk.cyan.bold('\nšŸš€ AgentDevFramework - Software Development Requirements\n'));
23
-
24
- const cwd = process.cwd();
25
- const adfDir = path.join(cwd, '.adf');
26
-
27
- // Load .env file if it exists (for API keys)
28
- const envPath = getEnvFilePath(cwd);
29
- if (await fs.pathExists(envPath)) {
30
- loadEnvIntoProcess(envPath);
31
- }
32
-
33
- // Detect existing frameworks for potential synthesis
34
- const detectedFrameworks = await FrameworkDetector.detect(cwd);
35
- const hasOtherFrameworks = detectedFrameworks.filter(f => f !== 'adf').length > 0;
36
-
37
- if (hasOtherFrameworks) {
38
- const frameworkNames = {
39
- 'agent-native': 'Agent-Native',
40
- 'openspec': 'OpenSpec',
41
- 'specification-driven': 'Specification-Driven'
42
- };
43
-
44
- const displayNames = detectedFrameworks
45
- .filter(f => f !== 'adf')
46
- .map(f => frameworkNames[f] || f);
47
-
48
- console.log(chalk.cyan('šŸ“¦ Existing Development Frameworks Detected:'));
49
- console.log(chalk.gray(` ${displayNames.join(', ')}\n`));
50
-
51
- const { action } = await inquirer.prompt([
52
- {
53
- type: 'list',
54
- name: 'action',
55
- message: 'How would you like to proceed?',
56
- choices: [
57
- {
58
- name: 'Synthesize & Augment (Seamless Merge - Recommended)',
59
- value: 'synthesize'
60
- },
61
- {
62
- name: 'Start Fresh (Ignore existing frameworks)',
63
- value: 'fresh'
64
- },
65
- new inquirer.Separator(),
66
- {
67
- name: chalk.gray('← Exit'),
68
- value: 'exit'
69
- }
70
- ]
71
- }
72
- ]);
73
-
74
- if (action === 'exit') return;
75
-
76
- if (action === 'synthesize') {
77
- const spinner = ora('Synthesizing project context...').start();
78
- try {
79
- const context = await ContextExtractor.extract(cwd, detectedFrameworks);
80
- const sessionPath = await SynthesisEngine.createAugmentedSession(cwd, context, 'balanced');
81
- spinner.succeed(chalk.green(`āœ“ Project synthesized into new session: ${path.basename(sessionPath)}`));
82
-
83
- console.log(chalk.yellow('\nšŸ’” Starting Adaptive Augmentation Interview to fill knowledge gaps...\n'));
84
-
85
- // Load AI config for the interviewer
86
- const aiConfig = await configureAIProvider(cwd);
87
- const sessionProgress = await fs.readJson(path.join(sessionPath, '_progress.json'));
88
-
89
- // Perform Knowledge Gap Analysis
90
- console.log(chalk.cyan('šŸ” Analyzing for knowledge gaps...'));
91
- const hGaps = HeuristicGapAnalyzer.analyze(context, 'balanced');
92
-
93
- let aiGaps = [];
94
- if (aiConfig) {
95
- const AIClient = require('../ai/ai-client');
96
- const aiClient = new AIClient(aiConfig);
97
- const aiAnalyzer = new AIGapAnalyzer(aiClient);
98
- aiGaps = await aiAnalyzer.analyze(context);
99
- }
100
-
101
- const dynamicQuestions = DynamicQuestionGenerator.generate(hGaps, aiGaps);
102
-
103
- if (dynamicQuestions.length === 0) {
104
- console.log(chalk.green('āœ“ Context is comprehensive. No additional questions needed.'));
105
- const interviewer = new Interviewer('balanced', cwd, {
106
- sessionId: path.basename(sessionPath),
107
- sessionPath,
108
- progress: sessionProgress
109
- }, aiConfig);
110
- await interviewer.generateOutputs();
111
- return;
112
- }
113
-
114
- console.log(chalk.yellow(`\nšŸ’” Identified ${dynamicQuestions.length} gaps. Starting targeted interview...\n`));
115
-
116
- const interviewer = new Interviewer('balanced', cwd, {
117
- sessionId: path.basename(sessionPath),
118
- sessionPath,
119
- progress: sessionProgress
120
- }, aiConfig, dynamicQuestions);
121
-
122
- await interviewer.start();
123
- return;
124
- } catch (error) {
125
- spinner.fail(chalk.red(`Synthesis failed: ${error.message}`));
126
- console.log(chalk.yellow('Falling back to standard initialization...\n'));
127
- }
128
- }
129
- }
130
-
131
- // Check for resumable sessions FIRST (before asking to overwrite)
132
- const sessionManager = new SessionManager(cwd);
133
- const existingSession = await sessionManager.promptToResume();
134
-
135
- if (existingSession) {
136
- // Resume existing session
137
- // Check if session has AI config (from resumed session)
138
- let aiConfig = existingSession.progress.aiConfig;
139
-
140
- if (aiConfig) {
141
- // We have AI config from session, but need to verify API key exists
142
- const apiKey = process.env[aiConfig.envVar];
143
- if (!apiKey) {
144
- console.log(chalk.yellow(`\nāš ļø Previous session used ${aiConfig.providerName}`));
145
- console.log(chalk.yellow(`Please configure API key to resume...\n`));
146
- aiConfig = await configureAIProvider(cwd);
147
- } else {
148
- // Add API key to config (it's not stored in session for security)
149
- aiConfig.apiKey = apiKey;
150
- console.log(chalk.green(`\nāœ“ Resuming with ${aiConfig.providerName} (${aiConfig.model})\n`));
151
- }
152
- } else {
153
- // Old session without AI config, configure now
154
- console.log(chalk.yellow('\nāš ļø This session was created before AI provider integration.'));
155
- console.log(chalk.yellow('Please configure AI provider to continue...\n'));
156
- aiConfig = await configureAIProvider(cwd);
157
- }
158
-
159
- const interviewer = new Interviewer(existingSession.progress.framework || 'balanced', cwd, existingSession, aiConfig);
160
- const sessionPath = await interviewer.start();
161
-
162
- if (sessionPath === 'back') {
163
- // User requested to go back to main menu
164
- // Fall through to existing content check
165
- } else {
166
- console.log(chalk.green.bold('\n✨ Requirements gathering complete!\n'));
167
- console.log(chalk.cyan(`šŸ“ Session saved to: ${sessionPath}\n`));
168
- return;
169
- }
170
- }
171
-
172
- // Check if already initialized with actual content
173
- if (await fs.pathExists(adfDir)) {
174
- // Check for meaningful content (not just .env file)
175
- const hasContent = await hasMeaningfulContent(adfDir);
176
-
177
- if (hasContent) {
178
- // Show what exists
179
- const existingContent = await getExistingContent(adfDir);
180
- console.log(chalk.cyan('\nšŸ“¦ Existing ADF Project Detected\n'));
181
-
182
- if (existingContent.sessions > 0) {
183
- console.log(chalk.gray(` Sessions: ${existingContent.sessions} session(s)`));
184
- }
185
- if (existingContent.outputs > 0) {
186
- console.log(chalk.gray(` Outputs: ${existingContent.outputs} file(s)`));
187
- }
188
- if (existingContent.learning) {
189
- console.log(chalk.gray(` Learning data: Present`));
190
- }
191
- console.log('');
192
-
193
- const { action } = await inquirer.prompt([
194
- {
195
- type: 'list',
196
- name: 'action',
197
- message: 'What would you like to do?',
198
- choices: [
199
- {
200
- name: 'Continue with Existing Project',
201
- value: 'continue',
202
- short: 'Continue'
203
- },
204
- {
205
- name: 'Reset this Project (delete all data)',
206
- value: 'reset',
207
- short: 'Reset'
208
- },
209
- new inquirer.Separator(),
210
- {
211
- name: chalk.gray('← Don\'t change & Exit'),
212
- value: 'exit',
213
- short: 'Exit'
214
- }
215
- ],
216
- default: 'continue'
217
- }
218
- ]);
219
-
220
- if (action === 'exit') {
221
- console.log(chalk.gray('\n← Exited without changes.\n'));
222
- return;
223
- }
224
-
225
- if (action === 'reset') {
226
- // Confirm deletion
227
- const { confirmReset } = await inquirer.prompt([
228
- {
229
- type: 'confirm',
230
- name: 'confirmReset',
231
- message: chalk.red('āš ļø This will permanently delete all sessions and data. Continue?'),
232
- default: false
233
- }
234
- ]);
235
-
236
- if (!confirmReset) {
237
- console.log(chalk.gray('\n← Reset cancelled. Exited without changes.\n'));
238
- return;
239
- }
240
-
241
- await fs.remove(adfDir);
242
- console.log(chalk.yellow('\nāœ“ Project reset. Starting fresh...\n'));
243
- } else if (action === 'continue') {
244
- // Continue with existing project - show sessions and prompt to resume
245
- console.log(chalk.green('\nāœ“ Continuing with existing project...\n'));
246
-
247
- // The SessionManager.promptToResume() was already called above (line 29)
248
- // But it returned null (no resumable sessions), so let them see what exists
249
- // and choose to start a new session or view existing outputs
250
-
251
- const { continueAction } = await inquirer.prompt([
252
- {
253
- type: 'list',
254
- name: 'continueAction',
255
- message: 'What would you like to do?',
256
- choices: [
257
- {
258
- name: 'Start a new session (keeps existing data)',
259
- value: 'new-session',
260
- short: 'New Session'
261
- },
262
- {
263
- name: chalk.gray('← Exit'),
264
- value: 'exit',
265
- short: 'Exit'
266
- }
267
- ],
268
- default: 'new-session'
269
- }
270
- ]);
271
-
272
- if (continueAction === 'exit') {
273
- console.log(chalk.gray('\n← Exited.\n'));
274
- return;
275
- }
276
-
277
- // Continue to start a new session below (fall through)
278
- console.log('');
279
- }
280
- } else {
281
- // Only .env file exists - safe to continue without prompting
282
- console.log(chalk.gray('āœ“ Using existing .adf directory\n'));
283
- }
284
- }
285
-
286
- // Detect project type
287
- const spinner = ora('Detecting project type...').start();
288
- const projectType = await detectProjectType(cwd);
289
- spinner.succeed(`Project type: ${chalk.green(projectType.type)}`);
290
-
291
- // Configure AI provider BEFORE workflow selection
292
- console.log('');
293
- const aiConfig = await configureAIProvider(cwd);
294
-
295
- // Determine workflow/framework
296
- let workflow;
297
-
298
- if (options.rapid) {
299
- workflow = 'rapid';
300
- console.log(chalk.blue('\nUsing: Level 1: Rapid (Agent-Native) - from --rapid flag'));
301
- } else if (options.balanced) {
302
- workflow = 'balanced';
303
- console.log(chalk.blue('\nUsing: Level 2: Balanced (OpenSpec) - from --balanced flag'));
304
- } else if (options.comprehensive) {
305
- workflow = 'comprehensive';
306
- console.log(chalk.blue('\nUsing: Level 3: Comprehensive (Agent-Native) - from --comprehensive flag'));
307
- } else {
308
- // Interactive workflow selection
309
- workflow = await getWorkflowRecommendation(projectType);
310
- }
311
-
312
- // Create .adf directory
313
- await fs.ensureDir(adfDir);
314
-
315
- // AI already configured above - pass to interviewer
316
- // Start interview
317
- const interviewer = new Interviewer(workflow, cwd, null, aiConfig);
318
- const sessionPath = await interviewer.start();
319
-
320
- // Show completion message
321
- console.log(chalk.cyan('šŸ“‹ Requirements Complete!\n'));
322
- console.log(chalk.gray(` āœ“ Files saved to: ${sessionPath}/outputs/`));
323
- console.log(chalk.gray(` āœ“ You can review your requirements anytime\n`));
324
-
325
- // Generate A2A agent cards
326
- try {
327
- const { generateA2A } = require('../generators');
328
- await generateA2A(sessionPath, cwd, workflow);
329
- console.log(chalk.gray(' āœ“ A2A agent cards generated'));
330
- } catch (error) {
331
- console.warn(chalk.yellow(` ⚠ Could not generate A2A cards: ${error.message}`));
332
- }
333
-
334
- // Optional: Deploy to tool
335
- if (options.tool) {
336
- console.log('');
337
- await deployToTool(options.tool, { silent: false });
338
- } else {
339
- const { deployNow } = await inquirer.prompt([
340
- {
341
- type: 'confirm',
342
- name: 'deployNow',
343
- message: 'Automatically deploy to your IDE? (I\'ll configure everything for you)',
344
- default: true
345
- }
346
- ]);
347
-
348
- if (deployNow) {
349
- const { tools } = await inquirer.prompt([
350
- {
351
- type: 'checkbox',
352
- name: 'tools',
353
- message: 'Select tools (space to select, enter to confirm):',
354
- choices: [
355
- { name: 'Windsurf', value: 'windsurf' },
356
- { name: 'Cursor', value: 'cursor' },
357
- { name: 'VSCode/Copilot', value: 'vscode' },
358
- { name: 'Claude Code', value: 'claude-code' },
359
- { name: 'Gemini CLI', value: 'gemini-cli' }
360
- ],
361
- validate: (answer) => {
362
- if (answer.length === 0) {
363
- return 'You must choose at least one tool.';
364
- }
365
- return true;
366
- }
367
- }
368
- ]);
369
-
370
- // Deploy to each selected tool
371
- for (const tool of tools) {
372
- console.log('');
373
- await deployToTool(tool, { silent: false });
374
- }
375
- }
376
- }
377
-
378
- console.log(chalk.green.bold('\nāœ… All done! Happy coding! šŸš€\n'));
379
- }
380
-
381
- /**
382
- * Check if .adf directory has meaningful content
383
- * Returns true if there are session files, progress files, or outputs
384
- * Returns false if only .env or empty
385
- */
386
- async function hasMeaningfulContent(adfDir) {
387
- try {
388
- const contents = await fs.readdir(adfDir);
389
-
390
- // Filter out .env and other config files that don't represent sessions
391
- const meaningfulFiles = contents.filter(item => {
392
- return item !== '.env' &&
393
- item !== '.gitignore' &&
394
- !item.startsWith('.');
395
- });
396
-
397
- if (meaningfulFiles.length === 0) {
398
- return false;
399
- }
400
-
401
- // Check for session directories or files
402
- for (const item of meaningfulFiles) {
403
- const itemPath = path.join(adfDir, item);
404
- const stats = await fs.stat(itemPath);
405
-
406
- if (stats.isDirectory()) {
407
- // Check if directory has files (sessions/, outputs/, etc.)
408
- const dirContents = await fs.readdir(itemPath);
409
- if (dirContents.length > 0) {
410
- return true;
411
- }
412
- } else if (stats.isFile()) {
413
- // Any non-config file indicates content
414
- return true;
415
- }
416
- }
417
-
418
- return false;
419
- } catch (error) {
420
- // If error reading, assume no content
421
- return false;
422
- }
423
- }
424
-
425
- /**
426
- * Get detailed information about existing ADF content
427
- * Returns object with counts and details
428
- */
429
- async function getExistingContent(adfDir) {
430
- const result = {
431
- sessions: 0,
432
- outputs: 0,
433
- learning: false,
434
- details: []
435
- };
436
-
437
- try {
438
- const contents = await fs.readdir(adfDir);
439
-
440
- for (const item of contents) {
441
- const itemPath = path.join(adfDir, item);
442
- const stats = await fs.stat(itemPath);
443
-
444
- if (stats.isDirectory()) {
445
- if (item === 'sessions') {
446
- // Count session directories
447
- const sessions = await fs.readdir(itemPath);
448
- result.sessions = sessions.filter(s => !s.startsWith('.')).length;
449
- } else if (item === 'outputs') {
450
- // Count output files
451
- const outputs = await fs.readdir(itemPath);
452
- result.outputs = outputs.filter(o => !o.startsWith('.')).length;
453
- } else if (item === 'learning') {
454
- // Check for learning data
455
- const learningFiles = await fs.readdir(itemPath);
456
- result.learning = learningFiles.length > 0;
457
- }
458
- }
459
- }
460
-
461
- return result;
462
- } catch (error) {
463
- return result;
464
- }
465
- }
466
-
467
- module.exports = init;
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const inquirer = require('inquirer');
4
+ const chalk = require('chalk');
5
+ const ora = require('ora');
6
+ const {
7
+ detectProjectType,
8
+ getWorkflowRecommendation
9
+ } = require('../utils/project-detector');
10
+ const FrameworkDetector = require('../utils/framework-detector');
11
+ const ContextExtractor = require('../utils/context-extractor');
12
+ const SynthesisEngine = require('../analysis/synthesis-engine');
13
+ const HeuristicGapAnalyzer = require('../analysis/heuristic-gap-analyzer');
14
+ const AIGapAnalyzer = require('../analysis/ai-gap-analyzer');
15
+ const DynamicQuestionGenerator = require('../analysis/dynamic-question-generator');
16
+ const Interviewer = require('../frameworks/interviewer');
17
+ const SessionManager = require('../frameworks/session-manager');
18
+ const { deployToTool } = require('./deploy');
19
+ const { configureAIProvider, loadEnvIntoProcess, getEnvFilePath } = require('../ai/ai-config');
20
+
21
+ async function init(options) {
22
+ console.log(chalk.cyan.bold('\nšŸš€ AgentDevFramework - Software Development Requirements\n'));
23
+
24
+ const cwd = process.cwd();
25
+ const adfDir = path.join(cwd, '.adf');
26
+
27
+ // Load .env file if it exists (for API keys)
28
+ const envPath = getEnvFilePath(cwd);
29
+ if (await fs.pathExists(envPath)) {
30
+ loadEnvIntoProcess(envPath);
31
+ }
32
+
33
+ // Detect existing frameworks for potential synthesis
34
+ const detectedFrameworks = await FrameworkDetector.detect(cwd);
35
+ const hasOtherFrameworks = detectedFrameworks.filter(f => f !== 'adf').length > 0;
36
+
37
+ if (hasOtherFrameworks) {
38
+ const frameworkNames = {
39
+ 'agent-native': 'Agent-Native',
40
+ 'openspec': 'OpenSpec',
41
+ 'specification-driven': 'Specification-Driven',
42
+ 'gemini-conductor': 'Gemini CLI Conductor'
43
+ };
44
+
45
+ const displayNames = detectedFrameworks
46
+ .filter(f => f !== 'adf')
47
+ .map(f => frameworkNames[f] || f);
48
+
49
+ console.log(chalk.cyan('šŸ“¦ Existing Development Frameworks Detected:'));
50
+ console.log(chalk.gray(` ${displayNames.join(', ')}\n`));
51
+
52
+ const { action } = await inquirer.prompt([
53
+ {
54
+ type: 'list',
55
+ name: 'action',
56
+ message: 'How would you like to proceed?',
57
+ choices: [
58
+ {
59
+ name: 'Synthesize & Augment (Seamless Merge - Recommended)',
60
+ value: 'synthesize'
61
+ },
62
+ {
63
+ name: 'Start Fresh (Ignore existing frameworks)',
64
+ value: 'fresh'
65
+ },
66
+ new inquirer.Separator(),
67
+ {
68
+ name: chalk.gray('← Exit'),
69
+ value: 'exit'
70
+ }
71
+ ]
72
+ }
73
+ ]);
74
+
75
+ if (action === 'exit') return;
76
+
77
+ if (action === 'synthesize') {
78
+ const spinner = ora('Synthesizing project context...').start();
79
+ try {
80
+ const context = await ContextExtractor.extract(cwd, detectedFrameworks);
81
+ const sessionPath = await SynthesisEngine.createAugmentedSession(cwd, context, 'balanced');
82
+ spinner.succeed(chalk.green(`āœ“ Project synthesized into new session: ${path.basename(sessionPath)}`));
83
+
84
+ console.log(chalk.yellow('\nšŸ’” Starting Adaptive Augmentation Interview to fill knowledge gaps...\n'));
85
+
86
+ // Load AI config for the interviewer
87
+ const aiConfig = await configureAIProvider(cwd);
88
+ const sessionProgress = await fs.readJson(path.join(sessionPath, '_progress.json'));
89
+
90
+ // Perform Knowledge Gap Analysis
91
+ console.log(chalk.cyan('šŸ” Analyzing for knowledge gaps...'));
92
+ const hGaps = HeuristicGapAnalyzer.analyze(context, 'balanced');
93
+
94
+ let aiGaps = [];
95
+ if (aiConfig) {
96
+ const AIClient = require('../ai/ai-client');
97
+ const aiClient = new AIClient(aiConfig);
98
+ const aiAnalyzer = new AIGapAnalyzer(aiClient);
99
+ aiGaps = await aiAnalyzer.analyze(context);
100
+ }
101
+
102
+ const dynamicQuestions = DynamicQuestionGenerator.generate(hGaps, aiGaps);
103
+
104
+ if (dynamicQuestions.length === 0) {
105
+ console.log(chalk.green('āœ“ Context is comprehensive. No additional questions needed.'));
106
+ const interviewer = new Interviewer('balanced', cwd, {
107
+ sessionId: path.basename(sessionPath),
108
+ sessionPath,
109
+ progress: sessionProgress
110
+ }, aiConfig);
111
+ await interviewer.generateOutputs();
112
+ return;
113
+ }
114
+
115
+ console.log(chalk.yellow(`\nšŸ’” Identified ${dynamicQuestions.length} gaps. Starting targeted interview...\n`));
116
+
117
+ const interviewer = new Interviewer('balanced', cwd, {
118
+ sessionId: path.basename(sessionPath),
119
+ sessionPath,
120
+ progress: sessionProgress
121
+ }, aiConfig, dynamicQuestions);
122
+
123
+ await interviewer.start();
124
+ return;
125
+ } catch (error) {
126
+ spinner.fail(chalk.red(`Synthesis failed: ${error.message}`));
127
+ console.log(chalk.yellow('Falling back to standard initialization...\n'));
128
+ }
129
+ }
130
+ }
131
+
132
+ // Check for resumable sessions FIRST (before asking to overwrite)
133
+ const sessionManager = new SessionManager(cwd);
134
+ const existingSession = await sessionManager.promptToResume();
135
+
136
+ if (existingSession) {
137
+ // Resume existing session
138
+ // Check if session has AI config (from resumed session)
139
+ let aiConfig = existingSession.progress.aiConfig;
140
+
141
+ if (aiConfig) {
142
+ // We have AI config from session, but need to verify API key exists
143
+ const apiKey = process.env[aiConfig.envVar];
144
+ if (!apiKey) {
145
+ console.log(chalk.yellow(`\nāš ļø Previous session used ${aiConfig.providerName}`));
146
+ console.log(chalk.yellow(`Please configure API key to resume...\n`));
147
+ aiConfig = await configureAIProvider(cwd);
148
+ } else {
149
+ // Add API key to config (it's not stored in session for security)
150
+ aiConfig.apiKey = apiKey;
151
+ console.log(chalk.green(`\nāœ“ Resuming with ${aiConfig.providerName} (${aiConfig.model})\n`));
152
+ }
153
+ } else {
154
+ // Old session without AI config, configure now
155
+ console.log(chalk.yellow('\nāš ļø This session was created before AI provider integration.'));
156
+ console.log(chalk.yellow('Please configure AI provider to continue...\n'));
157
+ aiConfig = await configureAIProvider(cwd);
158
+ }
159
+
160
+ const interviewer = new Interviewer(existingSession.progress.framework || 'balanced', cwd, existingSession, aiConfig);
161
+ const sessionPath = await interviewer.start();
162
+
163
+ if (sessionPath === 'back') {
164
+ // User requested to go back to main menu
165
+ // Fall through to existing content check
166
+ } else {
167
+ console.log(chalk.green.bold('\n✨ Requirements gathering complete!\n'));
168
+ console.log(chalk.cyan(`šŸ“ Session saved to: ${sessionPath}\n`));
169
+ return;
170
+ }
171
+ }
172
+
173
+ // Check if already initialized with actual content
174
+ if (await fs.pathExists(adfDir)) {
175
+ // Check for meaningful content (not just .env file)
176
+ const hasContent = await hasMeaningfulContent(adfDir);
177
+
178
+ if (hasContent) {
179
+ // Show what exists
180
+ const existingContent = await getExistingContent(adfDir);
181
+ console.log(chalk.cyan('\nšŸ“¦ Existing ADF Project Detected\n'));
182
+
183
+ if (existingContent.sessions > 0) {
184
+ console.log(chalk.gray(` Sessions: ${existingContent.sessions} session(s)`));
185
+ }
186
+ if (existingContent.outputs > 0) {
187
+ console.log(chalk.gray(` Outputs: ${existingContent.outputs} file(s)`));
188
+ }
189
+ if (existingContent.learning) {
190
+ console.log(chalk.gray(` Learning data: Present`));
191
+ }
192
+ console.log('');
193
+
194
+ const { action } = await inquirer.prompt([
195
+ {
196
+ type: 'list',
197
+ name: 'action',
198
+ message: 'What would you like to do?',
199
+ choices: [
200
+ {
201
+ name: 'Continue with Existing Project',
202
+ value: 'continue',
203
+ short: 'Continue'
204
+ },
205
+ {
206
+ name: 'Reset this Project (delete all data)',
207
+ value: 'reset',
208
+ short: 'Reset'
209
+ },
210
+ new inquirer.Separator(),
211
+ {
212
+ name: chalk.gray('← Don\'t change & Exit'),
213
+ value: 'exit',
214
+ short: 'Exit'
215
+ }
216
+ ],
217
+ default: 'continue'
218
+ }
219
+ ]);
220
+
221
+ if (action === 'exit') {
222
+ console.log(chalk.gray('\n← Exited without changes.\n'));
223
+ return;
224
+ }
225
+
226
+ if (action === 'reset') {
227
+ // Confirm deletion
228
+ const { confirmReset } = await inquirer.prompt([
229
+ {
230
+ type: 'confirm',
231
+ name: 'confirmReset',
232
+ message: chalk.red('āš ļø This will permanently delete all sessions and data. Continue?'),
233
+ default: false
234
+ }
235
+ ]);
236
+
237
+ if (!confirmReset) {
238
+ console.log(chalk.gray('\n← Reset cancelled. Exited without changes.\n'));
239
+ return;
240
+ }
241
+
242
+ await fs.remove(adfDir);
243
+ console.log(chalk.yellow('\nāœ“ Project reset. Starting fresh...\n'));
244
+ } else if (action === 'continue') {
245
+ // Continue with existing project - show sessions with details
246
+ console.log(chalk.green('\nāœ“ Continuing with existing project...\n'));
247
+
248
+ const detailedSessions = await sessionManager.getSessionsWithDetails();
249
+
250
+ // Build session choices: in-progress first, then completed-with-gaps
251
+ const resumable = detailedSessions
252
+ .filter(s => s.progress.status === 'in-progress' && s.progress.canResume)
253
+ .sort((a, b) => new Date(b.progress.lastUpdated) - new Date(a.progress.lastUpdated));
254
+
255
+ const withGaps = detailedSessions
256
+ .filter(s => s.progress.status === 'completed' && s.hasGaps)
257
+ .sort((a, b) => new Date(b.progress.lastUpdated) - new Date(a.progress.lastUpdated));
258
+
259
+ const sessionChoices = [];
260
+
261
+ for (const s of resumable) {
262
+ const framework = (s.progress.framework || 'rapid').toUpperCase();
263
+ const date = new Date(s.progress.lastUpdated).toLocaleDateString();
264
+ const pct = s.totalQuestions > 0
265
+ ? Math.round((s.answeredCount / s.totalQuestions) * 100) : 0;
266
+ sessionChoices.push({
267
+ name: `Resume: ${framework} | ${date} | ${pct}% complete (${s.answeredCount}/${s.totalQuestions} questions)`,
268
+ value: { action: 'resume', sessionId: s.sessionId },
269
+ short: `Resume ${s.sessionId}`
270
+ });
271
+ }
272
+
273
+ for (const s of withGaps) {
274
+ const framework = (s.progress.framework || 'rapid').toUpperCase();
275
+ const date = new Date(s.progress.lastUpdated).toLocaleDateString();
276
+ sessionChoices.push({
277
+ name: `Fill gaps: ${framework} | ${date} | ${s.unansweredCount} unanswered questions`,
278
+ value: { action: 'fill-gaps', sessionId: s.sessionId },
279
+ short: `Fill gaps ${s.sessionId}`
280
+ });
281
+ }
282
+
283
+ if (sessionChoices.length > 0) {
284
+ sessionChoices.push(new inquirer.Separator());
285
+ }
286
+
287
+ sessionChoices.push({
288
+ name: 'Start a new session (keeps existing data)',
289
+ value: { action: 'new-session' },
290
+ short: 'New Session'
291
+ });
292
+ sessionChoices.push({
293
+ name: chalk.gray('← Exit'),
294
+ value: { action: 'exit' },
295
+ short: 'Exit'
296
+ });
297
+
298
+ const { continueChoice } = await inquirer.prompt([
299
+ {
300
+ type: 'list',
301
+ name: 'continueChoice',
302
+ message: 'Select a session or start new:',
303
+ choices: sessionChoices
304
+ }
305
+ ]);
306
+
307
+ if (continueChoice.action === 'exit') {
308
+ console.log(chalk.gray('\n← Exited.\n'));
309
+ return;
310
+ }
311
+
312
+ if (continueChoice.action === 'resume' || continueChoice.action === 'fill-gaps') {
313
+ let selectedSession;
314
+
315
+ if (continueChoice.action === 'fill-gaps') {
316
+ // Reopen completed session so interviewer treats it as resumable
317
+ selectedSession = await sessionManager.reopenSession(continueChoice.sessionId);
318
+ } else {
319
+ selectedSession = detailedSessions.find(s => s.sessionId === continueChoice.sessionId);
320
+ }
321
+
322
+ // Configure AI provider (reuse pattern from session resume above)
323
+ let aiConfig = selectedSession.progress.aiConfig;
324
+
325
+ if (aiConfig) {
326
+ const apiKey = process.env[aiConfig.envVar];
327
+ if (!apiKey) {
328
+ console.log(chalk.yellow(`\nāš ļø Previous session used ${aiConfig.providerName}`));
329
+ console.log(chalk.yellow(`Please configure API key to continue...\n`));
330
+ aiConfig = await configureAIProvider(cwd);
331
+ } else {
332
+ aiConfig.apiKey = apiKey;
333
+ console.log(chalk.green(`\nāœ“ Resuming with ${aiConfig.providerName} (${aiConfig.model})\n`));
334
+ }
335
+ } else {
336
+ console.log(chalk.yellow('\nPlease configure AI provider to continue...\n'));
337
+ aiConfig = await configureAIProvider(cwd);
338
+ }
339
+
340
+ const interviewer = new Interviewer(
341
+ selectedSession.progress.framework || 'balanced',
342
+ cwd,
343
+ selectedSession,
344
+ aiConfig
345
+ );
346
+ const sessionPath = await interviewer.start();
347
+
348
+ if (sessionPath !== 'back') {
349
+ console.log(chalk.green.bold('\n✨ Requirements gathering complete!\n'));
350
+ console.log(chalk.cyan(`šŸ“ Session saved to: ${sessionPath}\n`));
351
+ }
352
+ return;
353
+ }
354
+
355
+ // new-session: fall through to workflow selection below
356
+ console.log('');
357
+ }
358
+ } else {
359
+ // Only .env file exists - safe to continue without prompting
360
+ console.log(chalk.gray('āœ“ Using existing .adf directory\n'));
361
+ }
362
+ }
363
+
364
+ // Detect project type
365
+ const spinner = ora('Detecting project type...').start();
366
+ const projectType = await detectProjectType(cwd);
367
+ spinner.succeed(`Project type: ${chalk.green(projectType.type)}`);
368
+
369
+ // Configure AI provider BEFORE workflow selection
370
+ console.log('');
371
+ const aiConfig = await configureAIProvider(cwd);
372
+
373
+ // Determine workflow/framework
374
+ let workflow;
375
+
376
+ if (options.rapid) {
377
+ workflow = 'rapid';
378
+ console.log(chalk.blue('\nUsing: Level 1: Rapid (Agent-Native) - from --rapid flag'));
379
+ } else if (options.balanced) {
380
+ workflow = 'balanced';
381
+ console.log(chalk.blue('\nUsing: Level 2: Balanced (OpenSpec) - from --balanced flag'));
382
+ } else if (options.comprehensive) {
383
+ workflow = 'comprehensive';
384
+ console.log(chalk.blue('\nUsing: Level 3: Comprehensive (Agent-Native) - from --comprehensive flag'));
385
+ } else {
386
+ // Interactive workflow selection
387
+ workflow = await getWorkflowRecommendation(projectType);
388
+ }
389
+
390
+ // Create .adf directory
391
+ await fs.ensureDir(adfDir);
392
+
393
+ // AI already configured above - pass to interviewer
394
+ // Start interview
395
+ const interviewer = new Interviewer(workflow, cwd, null, aiConfig);
396
+ const sessionPath = await interviewer.start();
397
+
398
+ // Show completion message
399
+ console.log(chalk.cyan('šŸ“‹ Requirements Complete!\n'));
400
+ console.log(chalk.gray(` āœ“ Files saved to: ${sessionPath}/outputs/`));
401
+ console.log(chalk.gray(` āœ“ You can review your requirements anytime\n`));
402
+
403
+ // Generate A2A agent cards
404
+ try {
405
+ const { generateA2A } = require('../generators');
406
+ await generateA2A(sessionPath, cwd, workflow);
407
+ console.log(chalk.gray(' āœ“ A2A agent cards generated'));
408
+ } catch (error) {
409
+ console.warn(chalk.yellow(` ⚠ Could not generate A2A cards: ${error.message}`));
410
+ }
411
+
412
+ // Optional: Deploy to tool
413
+ if (options.tool) {
414
+ console.log('');
415
+ await deployToTool(options.tool, { silent: false });
416
+ } else {
417
+ const { deployNow } = await inquirer.prompt([
418
+ {
419
+ type: 'confirm',
420
+ name: 'deployNow',
421
+ message: 'Automatically deploy to your IDE? (I\'ll configure everything for you)',
422
+ default: true
423
+ }
424
+ ]);
425
+
426
+ if (deployNow) {
427
+ const { tools } = await inquirer.prompt([
428
+ {
429
+ type: 'checkbox',
430
+ name: 'tools',
431
+ message: 'Select tools (space to select, enter to confirm):',
432
+ choices: [
433
+ { name: 'Windsurf', value: 'windsurf' },
434
+ { name: 'Cursor', value: 'cursor' },
435
+ { name: 'VSCode/Copilot', value: 'vscode' },
436
+ { name: 'Claude Code', value: 'claude-code' },
437
+ { name: 'Gemini CLI', value: 'gemini-cli' }
438
+ ],
439
+ validate: (answer) => {
440
+ if (answer.length === 0) {
441
+ return 'You must choose at least one tool.';
442
+ }
443
+ return true;
444
+ }
445
+ }
446
+ ]);
447
+
448
+ // Deploy to each selected tool
449
+ for (const tool of tools) {
450
+ console.log('');
451
+ await deployToTool(tool, { silent: false });
452
+ }
453
+ }
454
+ }
455
+
456
+ console.log(chalk.green.bold('\nāœ… All done! Happy coding! šŸš€\n'));
457
+ }
458
+
459
+ /**
460
+ * Check if .adf directory has meaningful content
461
+ * Returns true if there are session files, progress files, or outputs
462
+ * Returns false if only .env or empty
463
+ */
464
+ async function hasMeaningfulContent(adfDir) {
465
+ try {
466
+ const contents = await fs.readdir(adfDir);
467
+
468
+ // Filter out .env and other config files that don't represent sessions
469
+ const meaningfulFiles = contents.filter(item => {
470
+ return item !== '.env' &&
471
+ item !== '.gitignore' &&
472
+ !item.startsWith('.');
473
+ });
474
+
475
+ if (meaningfulFiles.length === 0) {
476
+ return false;
477
+ }
478
+
479
+ // Check for session directories or files
480
+ for (const item of meaningfulFiles) {
481
+ const itemPath = path.join(adfDir, item);
482
+ const stats = await fs.stat(itemPath);
483
+
484
+ if (stats.isDirectory()) {
485
+ // Check if directory has files (sessions/, outputs/, etc.)
486
+ const dirContents = await fs.readdir(itemPath);
487
+ if (dirContents.length > 0) {
488
+ return true;
489
+ }
490
+ } else if (stats.isFile()) {
491
+ // Any non-config file indicates content
492
+ return true;
493
+ }
494
+ }
495
+
496
+ return false;
497
+ } catch (error) {
498
+ // If error reading, assume no content
499
+ return false;
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Get detailed information about existing ADF content
505
+ * Returns object with counts and details
506
+ */
507
+ async function getExistingContent(adfDir) {
508
+ const result = {
509
+ sessions: 0,
510
+ outputs: 0,
511
+ learning: false,
512
+ details: []
513
+ };
514
+
515
+ try {
516
+ const contents = await fs.readdir(adfDir);
517
+
518
+ for (const item of contents) {
519
+ const itemPath = path.join(adfDir, item);
520
+ const stats = await fs.stat(itemPath);
521
+
522
+ if (stats.isDirectory()) {
523
+ if (item === 'sessions') {
524
+ // Count session directories
525
+ const sessions = await fs.readdir(itemPath);
526
+ result.sessions = sessions.filter(s => !s.startsWith('.')).length;
527
+ } else if (item === 'outputs') {
528
+ // Count output files
529
+ const outputs = await fs.readdir(itemPath);
530
+ result.outputs = outputs.filter(o => !o.startsWith('.')).length;
531
+ } else if (item === 'learning') {
532
+ // Check for learning data
533
+ const learningFiles = await fs.readdir(itemPath);
534
+ result.learning = learningFiles.length > 0;
535
+ }
536
+ }
537
+ }
538
+
539
+ return result;
540
+ } catch (error) {
541
+ return result;
542
+ }
543
+ }
544
+
545
+ module.exports = init;