@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/repl-ink.js CHANGED
@@ -3,12 +3,15 @@ import { render, Box, Text, useInput, useApp } from 'ink';
3
3
  import SelectInput from 'ink-select-input';
4
4
  import Spinner from 'ink-spinner';
5
5
  import { execSync } from 'child_process';
6
- import { readFileSync } from 'fs';
6
+ import { readFileSync, existsSync } from 'fs';
7
7
  import path from 'path';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { ProjectInitiator } from './init.js';
10
+ import { DocumentationBuilder } from './build-docs.js';
10
11
  import { UpdateChecker } from './update-checker.js';
11
12
  import { UpdateInstaller } from './update-installer.js';
13
+ import { CommandLogger } from './command-logger.js';
14
+ import { BackgroundProcessManager } from './process-manager.js';
12
15
 
13
16
  const __filename = fileURLToPath(import.meta.url);
14
17
  const __dirname = path.dirname(__filename);
@@ -27,6 +30,16 @@ function getVersion() {
27
30
  return _cachedVersion;
28
31
  }
29
32
 
33
+ // Create process manager instance (singleton pattern)
34
+ let globalProcessManager = null;
35
+
36
+ function getProcessManager() {
37
+ if (!globalProcessManager) {
38
+ globalProcessManager = new BackgroundProcessManager();
39
+ }
40
+ return globalProcessManager;
41
+ }
42
+
30
43
  // ASCII art letter definitions (4 chars wide, 6 rows tall; I is 3 wide)
31
44
  const LOGO_LETTERS = {
32
45
  'A': [' ██ ', '█ █', '█ █', '████', '█ █', '█ █'],
@@ -66,19 +79,25 @@ function renderLogo(text) {
66
79
  // Banner component with ASCII art logo
67
80
  const Banner = () => {
68
81
  const version = getVersion();
69
- const logoLines = renderLogo('AGILE VIBE CODING');
82
+ const agileLines = renderLogo('AGILE');
83
+ const vibeLines = renderLogo('VIBE CODING');
70
84
 
71
- return React.createElement(Box, { flexDirection: 'column', marginBottom: 1 },
85
+ return React.createElement(Box, { flexDirection: 'column' },
72
86
  React.createElement(Text, null, ' '),
73
- ...logoLines.map((line, i) =>
74
- React.createElement(Text, { bold: true, color: LOGO_COLORS[i] }, ' ' + line)
75
- ),
87
+ ...agileLines.map((agileLine, i) => {
88
+ const vibeLine = vibeLines[i];
89
+ return React.createElement(Text, { key: i },
90
+ React.createElement(Text, { bold: true, color: LOGO_COLORS[i] }, agileLine),
91
+ React.createElement(Text, { bold: true, color: 'white' }, ' ' + vibeLine)
92
+ );
93
+ }),
76
94
  React.createElement(Text, null, ' '),
77
- React.createElement(Text, null, ` v${version} │ AI-powered Agile development framework`),
95
+ React.createElement(Text, null, `v${version} │ AI-powered Agile development framework`),
78
96
  React.createElement(Text, null, ' '),
79
- React.createElement(Text, { bold: true, color: 'red' }, ' ⚠️ UNDER DEVELOPMENT - DO NOT USE ⚠️'),
97
+ React.createElement(Text, { bold: true, color: 'red' }, 'UNDER DEVELOPMENT - DO NOT USE'),
80
98
  React.createElement(Text, null, ' '),
81
- React.createElement(Text, { dimColor: true }, ' Type / to see commands')
99
+ React.createElement(Text, { dimColor: true }, 'Type / to see commands'),
100
+ React.createElement(Text, null, ' ')
82
101
  );
83
102
  };
84
103
 
@@ -109,35 +128,355 @@ const LoadingSpinner = ({ message }) => {
109
128
  );
110
129
  };
111
130
 
112
- // Command selector component with number shortcuts
131
+ // Questionnaire questions definition
132
+ const questionnaireQuestions = [
133
+ {
134
+ key: 'MISSION_STATEMENT',
135
+ title: 'Mission Statement',
136
+ guidance: 'Describe the core purpose and value proposition of your application',
137
+ example: 'A platform to streamline team collaboration through real-time messaging and task management'
138
+ },
139
+ {
140
+ key: 'TARGET_USERS',
141
+ title: 'Target Users',
142
+ guidance: 'Who will use this application? List user types and their roles',
143
+ example: 'Small business owners, Team managers, Remote workers'
144
+ },
145
+ {
146
+ key: 'INITIAL_SCOPE',
147
+ title: 'Initial Scope',
148
+ guidance: 'What are the main features and functional areas? Focus on MVP',
149
+ example: 'User authentication, Real-time chat, Task boards, File sharing'
150
+ },
151
+ {
152
+ key: 'TECHNICAL_CONSIDERATIONS',
153
+ title: 'Technical Considerations',
154
+ guidance: 'Tech stack, infrastructure, performance requirements, scalability needs',
155
+ example: 'React frontend, Node.js backend, PostgreSQL database, AWS hosting'
156
+ },
157
+ {
158
+ key: 'SECURITY_AND_COMPLIANCE_REQUIREMENTS',
159
+ title: 'Security & Compliance',
160
+ guidance: 'Security measures, data privacy, regulatory compliance (GDPR, HIPAA, etc.)',
161
+ example: 'End-to-end encryption, GDPR compliance, SOC 2 Type II certification'
162
+ }
163
+ ];
164
+
165
+ // Question display component
166
+ const QuestionDisplay = ({ question, index, total, editMode }) => {
167
+ return React.createElement(Box, { flexDirection: 'column', flexShrink: 0 },
168
+ React.createElement(Text, { bold: true, color: 'cyan' },
169
+ `\n📝 Question ${index + 1} of ${total}: ${question.title}${editMode ? ' [EDIT MODE]' : ''}`
170
+ ),
171
+ React.createElement(Text, { dimColor: true },
172
+ ` ${question.guidance}`
173
+ ),
174
+ React.createElement(Text, { italic: true, dimColor: true },
175
+ ` Example: "${question.example}"`
176
+ ),
177
+ React.createElement(Text, { dimColor: true },
178
+ `\n Enter your response (press Enter twice when done, or Enter immediately to skip):\n`
179
+ ),
180
+ editMode && React.createElement(Text, { dimColor: true },
181
+ `\n Ctrl+P: Previous | Ctrl+N: Next | Ctrl+X: Exit Edit Mode`
182
+ )
183
+ );
184
+ };
185
+
186
+ // Multi-line input component with line numbers and character count
187
+ const MultiLineInput = ({ lines, showLineNumbers = true, showCharCount = true }) => {
188
+ if (lines.length === 0) {
189
+ return React.createElement(Box, { flexDirection: 'column', flexShrink: 0 },
190
+ React.createElement(Text, { dimColor: true },
191
+ ' > ',
192
+ React.createElement(Text, { inverse: true }, ' ')
193
+ )
194
+ );
195
+ }
196
+
197
+ const totalChars = lines.join('\n').length;
198
+ // Fixed width for line numbers to prevent layout shift (supports up to 999 lines)
199
+ const lineNumberWidth = 3;
200
+
201
+ return React.createElement(Box, { flexDirection: 'column', flexShrink: 0, overflow: 'hidden' },
202
+ ...lines.map((line, idx) => {
203
+ const lineNum = showLineNumbers ? `${String(idx + 1).padStart(lineNumberWidth, ' ')} │ ` : '';
204
+ const prefix = idx === lines.length - 1 ? ' > ' : ' ';
205
+ const isLastLine = idx === lines.length - 1;
206
+
207
+ return React.createElement(Text, { key: idx },
208
+ isLastLine ? prefix + lineNum + line : ' ' + lineNum + line,
209
+ isLastLine && React.createElement(Text, { inverse: true }, ' ')
210
+ );
211
+ }),
212
+ showCharCount && React.createElement(Box, { flexShrink: 0 },
213
+ React.createElement(Text, { dimColor: true },
214
+ `\n ${totalChars} characters`
215
+ )
216
+ )
217
+ );
218
+ };
219
+
220
+ // Questionnaire progress component
221
+ const QuestionnaireProgress = ({ current, total, answers, lastSave }) => {
222
+ const answered = Object.keys(answers).length;
223
+ const saveTime = lastSave ? lastSave.toLocaleTimeString() : 'Never';
224
+
225
+ return React.createElement(Box, { flexDirection: 'column', flexShrink: 0 },
226
+ React.createElement(Text, { dimColor: true },
227
+ `\n Progress: ${answered}/${total} questions answered | Current: ${current + 1}/${total}`
228
+ ),
229
+ React.createElement(Text, { dimColor: true },
230
+ ` Last saved: ${saveTime}`
231
+ )
232
+ );
233
+ };
234
+
235
+ // Keyboard shortcuts help component
236
+ const QuestionnaireHelp = ({ editMode }) => {
237
+ if (editMode) {
238
+ return React.createElement(Box, { flexShrink: 0, paddingX: 1, borderStyle: 'round', borderColor: 'gray' },
239
+ React.createElement(Box, { flexDirection: 'column' },
240
+ React.createElement(Text, { bold: true, dimColor: true }, '\nEdit Mode Shortcuts:'),
241
+ React.createElement(Text, { dimColor: true }, ' Ctrl+P: Previous question'),
242
+ React.createElement(Text, { dimColor: true }, ' Ctrl+N: Next question'),
243
+ React.createElement(Text, { dimColor: true }, ' Ctrl+X: Exit edit mode'),
244
+ React.createElement(Text, { dimColor: true }, ' Escape: Cancel questionnaire')
245
+ )
246
+ );
247
+ }
248
+
249
+ return React.createElement(Box, { flexShrink: 0, paddingX: 1, borderStyle: 'round', borderColor: 'gray' },
250
+ React.createElement(Box, { flexDirection: 'column' },
251
+ React.createElement(Text, { bold: true, dimColor: true }, '\nKeyboard Shortcuts:'),
252
+ React.createElement(Text, { dimColor: true }, ' Enter: New line'),
253
+ React.createElement(Text, { dimColor: true }, ' Enter × 2: Submit answer and move to next question'),
254
+ React.createElement(Text, { dimColor: true }, ' Enter (on empty): Skip question (use AI suggestion)'),
255
+ React.createElement(Text, { dimColor: true }, ' Backspace: Delete character (works across lines)'),
256
+ React.createElement(Text, { dimColor: true }, ' Ctrl+E: Enter edit mode to modify previous answers'),
257
+ React.createElement(Text, { dimColor: true }, ' Escape: Cancel questionnaire')
258
+ )
259
+ );
260
+ };
261
+
262
+ // Answers preview component
263
+ const AnswersPreview = ({ answers, questions }) => {
264
+ return React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
265
+ React.createElement(Text, { bold: true, color: 'cyan' },
266
+ '\n📋 Review Your Answers\n'
267
+ ),
268
+ ...questions.map((question, idx) => {
269
+ const answer = answers[question.key] || '(Skipped - will use AI suggestion)';
270
+ const lines = answer.split('\n');
271
+
272
+ return React.createElement(Box, { key: idx, flexDirection: 'column', marginBottom: 1 },
273
+ React.createElement(Text, { bold: true },
274
+ `${idx + 1}. ${question.title}`
275
+ ),
276
+ ...lines.map((line, lineIdx) =>
277
+ React.createElement(Text, { key: lineIdx, dimColor: !answers[question.key] },
278
+ ` ${line}`
279
+ )
280
+ )
281
+ );
282
+ }),
283
+ React.createElement(Box, { marginTop: 1 },
284
+ React.createElement(Text, { dimColor: true },
285
+ '\n Press Enter to submit | Ctrl+E to edit answers | Escape to cancel\n'
286
+ )
287
+ )
288
+ );
289
+ };
290
+
291
+ // Remove confirmation component
292
+ const RemoveConfirmation = ({ contents, confirmInput }) => {
293
+ return React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
294
+ React.createElement(Text, { bold: true, color: 'red' },
295
+ '\n🗑️ Remove AVC Project Structure\n'
296
+ ),
297
+ React.createElement(Text, { color: 'yellow' },
298
+ '⚠️ WARNING: This is a DESTRUCTIVE operation!\n'
299
+ ),
300
+ React.createElement(Text, null,
301
+ 'The following will be PERMANENTLY DELETED:\n'
302
+ ),
303
+ contents.length > 0 && React.createElement(Box, { flexDirection: 'column', marginY: 1 },
304
+ React.createElement(Text, { bold: true }, '📁 .avc/ folder contents:'),
305
+ ...contents.map((item, idx) =>
306
+ React.createElement(Text, { key: idx, dimColor: true },
307
+ ` • ${item}`
308
+ )
309
+ )
310
+ ),
311
+ React.createElement(Text, { color: 'red' },
312
+ '\n❌ All project definitions, epics, stories, tasks, and documentation will be lost.'
313
+ ),
314
+ React.createElement(Text, { color: 'red' },
315
+ '❌ All VitePress documentation will be deleted.'
316
+ ),
317
+ React.createElement(Text, { color: 'red' },
318
+ '❌ This action CANNOT be undone.\n'
319
+ ),
320
+ React.createElement(Text, { dimColor: true },
321
+ 'ℹ️ Note: The .env file will NOT be deleted.\n'
322
+ ),
323
+ React.createElement(Box, { borderStyle: 'round', borderColor: 'yellow', paddingX: 1, marginY: 1 },
324
+ React.createElement(Box, { flexDirection: 'column' },
325
+ React.createElement(Text, { bold: true, color: 'yellow' },
326
+ 'To confirm deletion, type exactly: delete all'
327
+ ),
328
+ React.createElement(Text, { dimColor: true },
329
+ 'To cancel, press Escape or type anything else'
330
+ )
331
+ )
332
+ ),
333
+ React.createElement(Box, { marginTop: 1 },
334
+ React.createElement(Text, null, 'Confirmation: '),
335
+ React.createElement(Text, null, confirmInput),
336
+ React.createElement(Text, { inverse: true }, ' ')
337
+ ),
338
+ React.createElement(Box, { marginTop: 1 },
339
+ React.createElement(Text, { dimColor: true },
340
+ '\nPress Enter to confirm | Escape to cancel\n'
341
+ )
342
+ )
343
+ );
344
+ };
345
+
346
+ // Kill external process confirmation component
347
+ const KillProcessConfirmation = ({ processInfo, confirmInput }) => {
348
+ return React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
349
+ React.createElement(Text, { bold: true, color: 'yellow' },
350
+ '\n⚠️ External Process Using Port\n'
351
+ ),
352
+ React.createElement(Text, null,
353
+ `Port ${processInfo.port} is currently in use by an external process:\n`
354
+ ),
355
+ React.createElement(Box, { flexDirection: 'column', marginY: 1, paddingX: 2 },
356
+ React.createElement(Text, { bold: true },
357
+ `Process: ${processInfo.command}`
358
+ ),
359
+ React.createElement(Text, { dimColor: true },
360
+ `PID: ${processInfo.pid}`
361
+ )
362
+ ),
363
+ React.createElement(Text, { color: 'red' },
364
+ '⚠️ This is NOT an AVC documentation server.\n'
365
+ ),
366
+ React.createElement(Text, null,
367
+ 'Killing this process may cause unexpected behavior if it\'s part of\n'
368
+ ),
369
+ React.createElement(Text, null,
370
+ 'another application or service you\'re running.\n'
371
+ ),
372
+ React.createElement(Box, { borderStyle: 'round', borderColor: 'yellow', paddingX: 1, marginY: 1 },
373
+ React.createElement(Box, { flexDirection: 'column' },
374
+ React.createElement(Text, { bold: true, color: 'yellow' },
375
+ 'Do you want to kill this process anyway?'
376
+ ),
377
+ React.createElement(Text, { dimColor: true },
378
+ 'Type "kill" to confirm | Type "no" or press Escape to cancel'
379
+ )
380
+ )
381
+ ),
382
+ React.createElement(Box, { marginTop: 1 },
383
+ React.createElement(Text, null, 'Response: '),
384
+ React.createElement(Text, null, confirmInput),
385
+ React.createElement(Text, { inverse: true }, ' ')
386
+ ),
387
+ React.createElement(Box, { marginTop: 1 },
388
+ React.createElement(Text, { dimColor: true },
389
+ '\nPress Enter to submit | Escape to cancel\n'
390
+ )
391
+ )
392
+ );
393
+ };
394
+
395
+ // Command selector component with number shortcuts and groups
113
396
  const CommandSelector = ({ onSelect, onCancel, filter }) => {
114
- const allCommands = [
115
- { label: '/init Initialize an AVC project (Sponsor Call ceremony)', value: '/init', key: '1' },
116
- { label: '/status Show current project status', value: '/status', key: '2' },
117
- { label: '/help Show this help message', value: '/help', key: '3' },
118
- { label: '/version Show version information', value: '/version', key: '4' },
119
- { label: '/exit Exit AVC interactive mode', value: '/exit', key: '5' }
397
+ const [selectedIndex, setSelectedIndex] = useState(0);
398
+
399
+ // Organized commands by group (with aliases for filtering)
400
+ const commandGroups = [
401
+ {
402
+ name: 'Project Setup',
403
+ commands: [
404
+ { label: '/init Initialize an AVC project', value: '/init', aliases: ['/i'] },
405
+ { label: '/documentation Build and serve documentation', value: '/documentation', aliases: ['/d'] },
406
+ { label: '/remove Remove AVC project structure', value: '/remove', aliases: ['/rm'] }
407
+ ]
408
+ },
409
+ {
410
+ name: 'Ceremonies',
411
+ commands: [
412
+ { label: '/sponsor-call Run Sponsor Call ceremony', value: '/sponsor-call', aliases: ['/sc'] }
413
+ ]
414
+ },
415
+ {
416
+ name: 'Monitoring',
417
+ commands: [
418
+ { label: '/processes View background processes', value: '/processes', aliases: ['/p'] },
419
+ { label: '/status Show current project status', value: '/status', aliases: ['/s'] }
420
+ ]
421
+ },
422
+ {
423
+ name: 'Information',
424
+ commands: [
425
+ { label: '/help Show this help message', value: '/help', aliases: ['/h'] },
426
+ { label: '/version Show version information', value: '/version', aliases: ['/v'] }
427
+ ]
428
+ },
429
+ {
430
+ name: 'System',
431
+ commands: [
432
+ { label: '/restart Restart AVC', value: '/restart', aliases: [] },
433
+ { label: '/exit Exit AVC interactive mode', value: '/exit', aliases: ['/q', '/quit'] }
434
+ ]
435
+ }
120
436
  ];
121
437
 
122
- // Filter commands if filter is provided
123
- const commands = filter
124
- ? allCommands.filter(c => c.value.startsWith(filter.toLowerCase()))
125
- : allCommands;
438
+ // Flatten all commands
439
+ const allCommands = commandGroups.flatMap(g => g.commands);
440
+
441
+ // Filter commands if filter is provided and not just "/"
442
+ // Match by command value OR any of its aliases
443
+ const shouldShowGroups = !filter || filter === '/';
444
+ const filteredCommands = shouldShowGroups
445
+ ? null
446
+ : allCommands.filter(c => {
447
+ const lowerFilter = filter.toLowerCase();
448
+ return c.value.startsWith(lowerFilter) ||
449
+ c.aliases.some(alias => alias.startsWith(lowerFilter));
450
+ });
126
451
 
127
- // Add number prefix to labels
128
- const commandsWithNumbers = commands.map((cmd, idx) => ({
129
- ...cmd,
130
- label: `[${idx + 1}] ${cmd.label}`
131
- }));
452
+ const commands = filteredCommands || allCommands;
132
453
 
133
454
  useInput((input, key) => {
134
455
  if (key.escape) {
135
456
  onCancel();
457
+ return;
136
458
  }
137
- // Number shortcuts
138
- const num = parseInt(input);
139
- if (num >= 1 && num <= commands.length) {
140
- onSelect(commands[num - 1]);
459
+ if (key.upArrow) {
460
+ setSelectedIndex(Math.max(0, selectedIndex - 1));
461
+ } else if (key.downArrow) {
462
+ setSelectedIndex(Math.min(commands.length - 1, selectedIndex + 1));
463
+ } else if (key.return) {
464
+ // Only call onSelect if there are commands and selected item exists
465
+ if (commands.length > 0 && commands[selectedIndex]) {
466
+ onSelect(commands[selectedIndex]);
467
+ } else if (filter && filter.startsWith('/')) {
468
+ // No commands available, but user typed a command - execute it to show unknown command error
469
+ onSelect({ value: filter });
470
+ } else {
471
+ // No filter or non-command input, just cancel
472
+ onCancel();
473
+ }
474
+ } else {
475
+ // Number shortcuts
476
+ const num = parseInt(input);
477
+ if (num >= 1 && num <= commands.length) {
478
+ onSelect(commands[num - 1]);
479
+ }
141
480
  }
142
481
  }, { isActive: true });
143
482
 
@@ -148,9 +487,51 @@ const CommandSelector = ({ onSelect, onCancel, filter }) => {
148
487
  );
149
488
  }
150
489
 
490
+ // Render grouped or filtered view
491
+ if (filteredCommands) {
492
+ // Filtered view - flat list
493
+ return React.createElement(Box, { flexDirection: 'column' },
494
+ ...filteredCommands.map((cmd, idx) =>
495
+ React.createElement(Text, {
496
+ key: cmd.value,
497
+ color: idx === selectedIndex ? 'green' : 'white'
498
+ }, `${idx === selectedIndex ? '› ' : ' '}[${idx + 1}] ${cmd.label}`)
499
+ ),
500
+ React.createElement(Text, { dimColor: true }, '\n(Use arrows, number keys, or Esc to cancel)')
501
+ );
502
+ }
503
+
504
+ // Grouped view
505
+ let commandIndex = 0;
506
+ const groupElements = [];
507
+
508
+ commandGroups.forEach((group, groupIdx) => {
509
+ // Group header
510
+ groupElements.push(
511
+ React.createElement(Text, {
512
+ key: `header-${groupIdx}`,
513
+ bold: true,
514
+ color: 'cyan',
515
+ dimColor: true
516
+ }, groupIdx === 0 ? `── ${group.name.toUpperCase()} ──` : `\n── ${group.name.toUpperCase()} ──`)
517
+ );
518
+
519
+ // Group commands
520
+ group.commands.forEach((cmd) => {
521
+ const isSelected = commandIndex === selectedIndex;
522
+ groupElements.push(
523
+ React.createElement(Text, {
524
+ key: cmd.value,
525
+ color: isSelected ? 'green' : 'white'
526
+ }, `${isSelected ? '› ' : ' '}[${commandIndex + 1}] ${cmd.label}`)
527
+ );
528
+ commandIndex++;
529
+ });
530
+ });
531
+
151
532
  return React.createElement(Box, { flexDirection: 'column' },
152
- React.createElement(SelectInput, { items: commandsWithNumbers, onSelect: onSelect }),
153
- React.createElement(Text, { dimColor: true }, '(Use arrows, number keys, or Esc to cancel)')
533
+ ...groupElements,
534
+ React.createElement(Text, { dimColor: true }, '\n(Use arrows, number keys, or Esc to cancel)')
154
535
  );
155
536
  };
156
537
 
@@ -163,19 +544,224 @@ const HistoryHint = ({ hasHistory }) => {
163
544
  );
164
545
  };
165
546
 
547
+ // Process viewer component
548
+ const ProcessViewer = ({ processes, onSelect, onCancel }) => {
549
+ const [selectedIndex, setSelectedIndex] = useState(0);
550
+
551
+ const processList = Array.from(processes.values()).sort(
552
+ (a, b) => new Date(b.startTime) - new Date(a.startTime)
553
+ );
554
+
555
+ useInput((input, key) => {
556
+ // Arrow up
557
+ if (key.upArrow) {
558
+ setSelectedIndex(Math.max(0, selectedIndex - 1));
559
+ return;
560
+ }
561
+
562
+ // Arrow down
563
+ if (key.downArrow) {
564
+ setSelectedIndex(Math.min(processList.length - 1, selectedIndex + 1));
565
+ return;
566
+ }
567
+
568
+ // Enter - select process
569
+ if (key.return && processList.length > 0) {
570
+ onSelect(processList[selectedIndex]);
571
+ return;
572
+ }
573
+
574
+ // Escape - cancel
575
+ if (key.escape) {
576
+ onCancel();
577
+ return;
578
+ }
579
+ }, { isActive: true });
580
+
581
+ if (processList.length === 0) {
582
+ return React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
583
+ React.createElement(Text, null, '\n📦 Background Processes\n'),
584
+ React.createElement(Text, { dimColor: true }, ' No background processes running\n'),
585
+ React.createElement(Text, { dimColor: true }, ' Press Esc to return\n')
586
+ );
587
+ }
588
+
589
+ const manager = getProcessManager();
590
+
591
+ return React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
592
+ React.createElement(Text, null, '\n📦 Background Processes\n'),
593
+ React.createElement(Text, { dimColor: true },
594
+ ` Select a process to view details (${processList.length} total)\n`
595
+ ),
596
+ React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
597
+ ...processList.map((process, idx) => {
598
+ const isSelected = idx === selectedIndex;
599
+ const statusIcon = process.status === 'running' ? '▶' :
600
+ process.status === 'stopped' ? '⏸' :
601
+ process.status === 'exited' ? '✓' : '✗';
602
+ const uptime = manager.formatUptime(manager.getUptime(process.id));
603
+
604
+ return React.createElement(Box, { key: process.id },
605
+ React.createElement(Text, {
606
+ bold: isSelected,
607
+ color: isSelected ? 'cyan' : undefined,
608
+ inverse: isSelected
609
+ },
610
+ ` ${isSelected ? '>' : ' '} ${statusIcon} ${process.name} - ${uptime}`
611
+ )
612
+ );
613
+ })
614
+ ),
615
+ React.createElement(Box, { marginTop: 1 },
616
+ React.createElement(Text, { dimColor: true },
617
+ '\n ↑/↓: Navigate | Enter: View details | Esc: Cancel\n'
618
+ )
619
+ )
620
+ );
621
+ };
622
+
623
+ // Process details viewer component
624
+ const ProcessDetailsViewer = ({ process, onBack, onStop }) => {
625
+ const [autoScroll, setAutoScroll] = useState(true);
626
+
627
+ useInput((input, key) => {
628
+ // Escape or Backspace - go back
629
+ if (key.escape || key.backspace) {
630
+ onBack();
631
+ return;
632
+ }
633
+
634
+ // S - stop process
635
+ if (input === 's' && process.status === 'running') {
636
+ onStop(process.id);
637
+ return;
638
+ }
639
+
640
+ // Space - toggle auto-scroll
641
+ if (input === ' ') {
642
+ setAutoScroll(!autoScroll);
643
+ return;
644
+ }
645
+ }, { isActive: true });
646
+
647
+ const manager = getProcessManager();
648
+ const uptime = manager.formatUptime(manager.getUptime(process.id));
649
+
650
+ // Get recent output (last 50 lines)
651
+ const recentOutput = process.output.slice(-50);
652
+
653
+ const statusColor = process.status === 'running' ? 'green' :
654
+ process.status === 'stopped' ? 'yellow' :
655
+ process.status === 'exited' ? 'blue' : 'red';
656
+
657
+ return React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
658
+ // Header
659
+ React.createElement(Text, { bold: true },
660
+ `\n📦 ${process.name}\n`
661
+ ),
662
+
663
+ // Process info
664
+ React.createElement(Box, { flexDirection: 'column', marginBottom: 1 },
665
+ React.createElement(Text, null,
666
+ ` Status: `
667
+ ),
668
+ React.createElement(Text, { color: statusColor, bold: true },
669
+ process.status.toUpperCase()
670
+ ),
671
+ React.createElement(Text, null,
672
+ `\n PID: ${process.pid || 'N/A'}`
673
+ ),
674
+ React.createElement(Text, null,
675
+ `\n Uptime: ${uptime}`
676
+ ),
677
+ React.createElement(Text, null,
678
+ `\n Command: ${process.command}`
679
+ ),
680
+ React.createElement(Text, { dimColor: true },
681
+ `\n Working dir: ${process.cwd}`
682
+ ),
683
+ process.exitCode !== null && React.createElement(Text, null,
684
+ `\n Exit code: ${process.exitCode}`
685
+ )
686
+ ),
687
+
688
+ // Output section
689
+ React.createElement(Text, { bold: true },
690
+ '\n📄 Recent Output (last 50 lines):\n'
691
+ ),
692
+
693
+ recentOutput.length === 0
694
+ ? React.createElement(Text, { dimColor: true }, ' No output yet\n')
695
+ : React.createElement(Box, { flexDirection: 'column', borderStyle: 'single', paddingX: 1 },
696
+ ...recentOutput.map((line, idx) =>
697
+ React.createElement(Text, {
698
+ key: idx,
699
+ color: line.type === 'stderr' ? 'red' : undefined,
700
+ dimColor: line.type === 'stderr'
701
+ },
702
+ line.text
703
+ )
704
+ )
705
+ ),
706
+
707
+ // Actions
708
+ React.createElement(Box, { marginTop: 1 },
709
+ React.createElement(Text, { dimColor: true },
710
+ '\n '
711
+ ),
712
+ process.status === 'running' && React.createElement(Text, null,
713
+ 'S: Stop process | '
714
+ ),
715
+ React.createElement(Text, null,
716
+ 'Esc: Back to list'
717
+ )
718
+ )
719
+ );
720
+ };
721
+
166
722
  // Input display with cursor
167
723
  const InputWithCursor = ({ input }) => {
168
- return React.createElement(Box, null,
724
+ return React.createElement(Box, { flexShrink: 0 },
169
725
  React.createElement(Text, null, '> '),
170
726
  React.createElement(Text, null, input),
171
727
  React.createElement(Text, { inverse: true }, ' ')
172
728
  );
173
729
  };
174
730
 
175
- // Bottom-right status display (version + update info)
176
- const BottomRightStatus = () => {
731
+ // Process status indicator
732
+ const ProcessStatusIndicator = ({ processes }) => {
733
+ const runningProcesses = Array.from(processes.values()).filter(
734
+ p => p.status === 'running'
735
+ );
736
+
737
+ if (runningProcesses.length === 0) return null;
738
+
739
+ if (runningProcesses.length === 1) {
740
+ const process = runningProcesses[0];
741
+ return React.createElement(Text, { dimColor: true },
742
+ `📦 ${process.name} running`
743
+ );
744
+ }
745
+
746
+ return React.createElement(Text, { dimColor: true },
747
+ `📦 ${runningProcesses.length} processes running`
748
+ );
749
+ };
750
+
751
+ // Bottom-right status display (version + update info + processes)
752
+ const BottomRightStatus = ({ backgroundProcesses }) => {
177
753
  const version = getVersion();
178
- const [updateState, setUpdateState] = useState(null);
754
+
755
+ // Option B: Pre-initialize updateState synchronously to prevent post-mount re-render
756
+ const [updateState, setUpdateState] = useState(() => {
757
+ try {
758
+ const checker = new UpdateChecker();
759
+ return checker.readState();
760
+ } catch (error) {
761
+ return null;
762
+ }
763
+ });
764
+
179
765
  const [width, setWidth] = useState(process.stdout.columns || 80);
180
766
 
181
767
  useEffect(() => {
@@ -187,7 +773,8 @@ const BottomRightStatus = () => {
187
773
  return () => process.stdout.off('resize', handleResize);
188
774
  }, []);
189
775
 
190
- // Poll update state every 2 seconds
776
+ // Option F: Defer polling to prevent initial layout shifts
777
+ // Start polling after 500ms delay to allow initial render to stabilize
191
778
  useEffect(() => {
192
779
  const checker = new UpdateChecker();
193
780
 
@@ -196,10 +783,14 @@ const BottomRightStatus = () => {
196
783
  setUpdateState(state);
197
784
  };
198
785
 
199
- updateStatus(); // Initial read
786
+ // Defer initial update check to prevent cursor positioning issues
787
+ const initialTimeout = setTimeout(updateStatus, 500);
200
788
  const interval = setInterval(updateStatus, 2000);
201
789
 
202
- return () => clearInterval(interval);
790
+ return () => {
791
+ clearTimeout(initialTimeout);
792
+ clearInterval(interval);
793
+ };
203
794
  }, []);
204
795
 
205
796
  // Build status text
@@ -222,6 +813,28 @@ const BottomRightStatus = () => {
222
813
  }
223
814
  }
224
815
 
816
+ // Check for running processes (priority: processes > updates > version)
817
+ const runningCount = Array.from(backgroundProcesses.values()).filter(
818
+ p => p.status === 'running'
819
+ ).length;
820
+
821
+ if (runningCount > 0) {
822
+ const processIndicator = React.createElement(ProcessStatusIndicator, { processes: backgroundProcesses });
823
+ // Get the text content to calculate padding
824
+ const runningProcesses = Array.from(backgroundProcesses.values()).filter(p => p.status === 'running');
825
+ const processText = runningCount === 1
826
+ ? `📦 ${runningProcesses[0].name} running`
827
+ : `📦 ${runningCount} processes running`;
828
+ const processPadding = Math.max(0, width - processText.length - 2);
829
+
830
+ return React.createElement(Box, { justifyContent: 'flex-end' },
831
+ React.createElement(Text, { dimColor: true },
832
+ ' '.repeat(processPadding),
833
+ processText
834
+ )
835
+ );
836
+ }
837
+
225
838
  // Calculate padding to align to the right
226
839
  const padding = Math.max(0, width - statusText.length - 2);
227
840
 
@@ -244,6 +857,31 @@ const App = () => {
244
857
  const [isExecuting, setIsExecuting] = useState(false);
245
858
  const [executingMessage, setExecutingMessage] = useState('');
246
859
 
860
+ // Questionnaire state
861
+ const [questionnaireActive, setQuestionnaireActive] = useState(false);
862
+ const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
863
+ const [questionnaireAnswers, setQuestionnaireAnswers] = useState({});
864
+ const [currentAnswer, setCurrentAnswer] = useState([]);
865
+ const [emptyLineCount, setEmptyLineCount] = useState(0);
866
+ const [editMode, setEditMode] = useState(false);
867
+ const [showPreview, setShowPreview] = useState(false);
868
+ const [lastAutoSave, setLastAutoSave] = useState(null);
869
+
870
+ // Remove confirmation state
871
+ const [removeConfirmActive, setRemoveConfirmActive] = useState(false);
872
+ const [removeConfirmInput, setRemoveConfirmInput] = useState('');
873
+ const [avcContents, setAvcContents] = useState([]);
874
+
875
+ // Kill external process confirmation state
876
+ const [killConfirmActive, setKillConfirmActive] = useState(false);
877
+ const [killConfirmInput, setKillConfirmInput] = useState('');
878
+ const [processToKill, setProcessToKill] = useState(null); // { pid, command, port }
879
+
880
+ // Background process state
881
+ const [backgroundProcesses, setBackgroundProcesses] = useState(new Map());
882
+ const [processViewerActive, setProcessViewerActive] = useState(false);
883
+ const [selectedProcessId, setSelectedProcessId] = useState(null);
884
+
247
885
  // Start update checker on mount
248
886
  useEffect(() => {
249
887
  const checker = new UpdateChecker();
@@ -265,13 +903,71 @@ const App = () => {
265
903
  }, 5000); // Wait 5 seconds after startup to avoid blocking
266
904
  }, []);
267
905
 
268
- // Available commands for Tab completion
906
+ // Auto-save questionnaire progress every 30 seconds
907
+ useEffect(() => {
908
+ if (!questionnaireActive) return;
909
+
910
+ const interval = setInterval(() => {
911
+ autoSaveProgress();
912
+ }, 30000); // 30 seconds
913
+
914
+ return () => clearInterval(interval);
915
+ }, [questionnaireActive, questionnaireAnswers, currentQuestionIndex, currentAnswer]);
916
+
917
+ // Sync background processes state
918
+ useEffect(() => {
919
+ const manager = getProcessManager();
920
+
921
+ const updateProcesses = () => {
922
+ const processes = new Map();
923
+ for (const p of manager.getAllProcesses()) {
924
+ processes.set(p.id, p);
925
+ }
926
+ setBackgroundProcesses(processes);
927
+ };
928
+
929
+ // Initial sync
930
+ updateProcesses();
931
+
932
+ // Listen for process events
933
+ manager.on('process-started', updateProcesses);
934
+ manager.on('process-stopped', updateProcesses);
935
+ manager.on('process-exited', updateProcesses);
936
+ manager.on('process-error', updateProcesses);
937
+
938
+ return () => {
939
+ manager.removeListener('process-started', updateProcesses);
940
+ manager.removeListener('process-stopped', updateProcesses);
941
+ manager.removeListener('process-exited', updateProcesses);
942
+ manager.removeListener('process-error', updateProcesses);
943
+ };
944
+ }, []);
945
+
946
+ // Cleanup background processes on unmount/exit
947
+ useEffect(() => {
948
+ return () => {
949
+ const manager = getProcessManager();
950
+ const running = manager.getRunningProcesses();
951
+
952
+ if (running.length > 0) {
953
+ console.log('\n🛑 Stopping background processes...\n');
954
+ manager.stopAll();
955
+ }
956
+ };
957
+ }, []);
958
+
959
+ // Available commands for Tab completion (including aliases)
269
960
  const allCommands = [
270
- '/init',
271
- '/status',
272
- '/help',
273
- '/version',
274
- '/exit'
961
+ '/init', '/i',
962
+ '/sponsor-call', '/sc',
963
+ '/status', '/s',
964
+ '/remove', '/rm',
965
+ '/documentation', '/d',
966
+ '/processes', '/p',
967
+ '/help', '/h',
968
+ '/version', '/v',
969
+ '/restart',
970
+ '/exit', '/q', '/quit'
275
971
  ];
276
972
 
277
973
  // Handle Tab key autocomplete
@@ -280,16 +976,28 @@ const App = () => {
280
976
  if (!input.startsWith('/')) return;
281
977
 
282
978
  // Filter commands that match the current input
979
+ // Exclude exact matches to prefer longer forms (e.g., /init over /i)
283
980
  const matches = allCommands.filter(cmd =>
284
- cmd.toLowerCase().startsWith(input.toLowerCase())
981
+ cmd.toLowerCase().startsWith(input.toLowerCase()) &&
982
+ cmd.toLowerCase() !== input.toLowerCase()
285
983
  );
286
984
 
985
+ // If no matches (meaning current input is exact match), try without exclusion
986
+ if (matches.length === 0) {
987
+ const exactMatches = allCommands.filter(cmd =>
988
+ cmd.toLowerCase().startsWith(input.toLowerCase())
989
+ );
990
+ // If there are other commands besides the exact match, use them
991
+ if (exactMatches.length > 1) {
992
+ matches.push(...exactMatches.filter(cmd => cmd.toLowerCase() !== input.toLowerCase()));
993
+ }
994
+ }
995
+
287
996
  // If exactly one match, complete to that command
288
997
  if (matches.length === 1) {
289
998
  setInput(matches[0]);
290
999
  // Show selector if not already shown
291
1000
  if (mode !== 'selector') {
292
- setOutput('');
293
1001
  setMode('selector');
294
1002
  }
295
1003
  }
@@ -313,7 +1021,6 @@ const App = () => {
313
1021
 
314
1022
  // Show selector if not already shown
315
1023
  if (mode !== 'selector') {
316
- setOutput('');
317
1024
  setMode('selector');
318
1025
  }
319
1026
  }
@@ -327,111 +1034,213 @@ const App = () => {
327
1034
  '/q': '/exit',
328
1035
  '/quit': '/exit',
329
1036
  '/i': '/init',
330
- '/s': '/status'
1037
+ '/s': '/status',
1038
+ '/sc': '/sponsor-call',
1039
+ '/rm': '/remove',
1040
+ '/d': '/documentation',
1041
+ '/p': '/processes'
331
1042
  };
332
1043
  return aliases[cmd.toLowerCase()] || cmd;
333
1044
  };
334
1045
 
335
1046
  // Handle command execution
336
1047
  const executeCommand = async (cmd) => {
337
- const command = resolveAlias(cmd.trim());
1048
+ try {
1049
+ const command = resolveAlias(cmd.trim());
338
1050
 
339
- if (!command) {
340
- setMode('prompt');
341
- setInput('');
342
- return;
343
- }
1051
+ if (!command) {
1052
+ setMode('prompt');
1053
+ setInput('');
1054
+ return;
1055
+ }
344
1056
 
345
- // Add to history (avoid duplicates of last command)
346
- if (command && (commandHistory.length === 0 || commandHistory[commandHistory.length - 1] !== command)) {
347
- setCommandHistory([...commandHistory, command]);
348
- }
349
- setHistoryIndex(-1);
1057
+ // Add to history (avoid duplicates of last command)
1058
+ if (command && (commandHistory.length === 0 || commandHistory[commandHistory.length - 1] !== command)) {
1059
+ setCommandHistory([...commandHistory, command]);
1060
+ }
1061
+ setHistoryIndex(-1);
350
1062
 
351
- setMode('executing');
352
- setIsExecuting(true);
353
- setOutput(''); // Clear previous output
1063
+ setMode('executing');
1064
+ setIsExecuting(true);
354
1065
 
355
- try {
356
- switch (command.toLowerCase()) {
357
- case '/help':
358
- setExecutingMessage('Loading help...');
359
- setOutput(showHelp());
360
- break;
361
-
362
- case '/version':
363
- setExecutingMessage('Loading version info...');
364
- setOutput(showVersion());
365
- break;
366
-
367
- case '/exit':
368
- setIsExecuting(false);
369
- setOutput('\n👋 Thanks for using AVC!\n');
370
- setTimeout(() => {
371
- exit();
372
- process.exit(0);
373
- }, 500);
374
- return;
1066
+ // Add command to output history (don't clear previous output)
1067
+ setOutput(output + `\n> ${command}\n`);
375
1068
 
376
- case '/init':
377
- setExecutingMessage('Initializing project...');
378
- await runInit();
379
- break;
1069
+ // Create command logger
1070
+ const commandName = command.replace('/', '').toLowerCase();
1071
+ let logger = null;
380
1072
 
381
- case '/status':
382
- setExecutingMessage('Checking project status...');
383
- await runStatus();
384
- break;
1073
+ // Check if .avc folder exists
1074
+ const avcPath = path.join(process.cwd(), '.avc');
1075
+ const avcExists = existsSync(avcPath);
385
1076
 
386
- case '/restart':
387
- setIsExecuting(false);
388
- setOutput('\n🔄 Restarting AVC...\n');
389
- setTimeout(() => {
390
- exit();
391
- try {
392
- execSync(process.argv.join(' '), { stdio: 'inherit' });
393
- } catch { }
394
- process.exit(0);
395
- }, 500);
396
- return;
1077
+ // Only create logger for commands that do actual work
1078
+ // For /init, always create logger (it creates .avc)
1079
+ // For other commands, only create logger if .avc already exists
1080
+ if (['/init', '/sponsor-call', '/status', '/remove'].includes(command.toLowerCase())) {
1081
+ if (command.toLowerCase() === '/init' || avcExists) {
1082
+ logger = new CommandLogger(commandName);
1083
+ logger.start();
1084
+ }
1085
+ }
397
1086
 
398
- default:
399
- if (command.startsWith('/')) {
400
- setOutput(`\n❌ Unknown command: ${command}\n Type /help to see available commands\n Tip: Try /h for help, /v for version, /q to exit\n`);
401
- } else {
402
- setOutput(`\n💡 Commands must start with /\n Example: /init, /status, /help\n Tip: Type / and press Enter to see all commands\n`);
1087
+ try {
1088
+ switch (command.toLowerCase()) {
1089
+ case '/help':
1090
+ setExecutingMessage('Loading help...');
1091
+ setOutput(prev => prev + showHelp());
1092
+ break;
1093
+
1094
+ case '/version':
1095
+ setExecutingMessage('Loading version info...');
1096
+ setOutput(prev => prev + showVersion());
1097
+ break;
1098
+
1099
+ case '/exit':
1100
+ setIsExecuting(false);
1101
+ const manager = getProcessManager();
1102
+ const running = manager.getRunningProcesses();
1103
+
1104
+ if (running.length > 0) {
1105
+ setOutput(prev => prev + '\n🛑 Stopping background processes...\n');
1106
+ const stopped = manager.stopAll();
1107
+ setOutput(prev => prev + ` Stopped ${stopped} process(es)\n\n`);
1108
+ }
1109
+
1110
+ setOutput(prev => prev + '\n👋 Thanks for using AVC!\n');
1111
+ setTimeout(() => {
1112
+ exit();
1113
+ process.exit(0);
1114
+ }, 500);
1115
+ return;
1116
+
1117
+ case '/init':
1118
+ case '/i':
1119
+ setExecutingMessage('Initializing project structure...');
1120
+ await runInit();
1121
+ break;
1122
+
1123
+ case '/sponsor-call':
1124
+ case '/sc':
1125
+ setExecutingMessage('Running Sponsor Call ceremony...');
1126
+ await runSponsorCall();
1127
+ break;
1128
+
1129
+ case '/status':
1130
+ case '/s':
1131
+ setExecutingMessage('Checking project status...');
1132
+ await runStatus();
1133
+ break;
1134
+
1135
+ case '/remove':
1136
+ case '/rm':
1137
+ setExecutingMessage('Preparing to remove AVC structure...');
1138
+ await runRemove();
1139
+ break;
1140
+
1141
+ case '/documentation':
1142
+ case '/d':
1143
+ setExecutingMessage('Building documentation...');
1144
+ await runBuildDocumentation();
1145
+ break;
1146
+
1147
+ case '/processes':
1148
+ case '/p':
1149
+ setProcessViewerActive(true);
1150
+ setMode('process-viewer');
1151
+ break;
1152
+
1153
+ case '/restart':
1154
+ setIsExecuting(false);
1155
+ const restartManager = getProcessManager();
1156
+ const runningOnRestart = restartManager.getRunningProcesses();
1157
+
1158
+ if (runningOnRestart.length > 0) {
1159
+ setOutput(prev => prev + '\n🛑 Stopping background processes...\n');
1160
+ const stopped = restartManager.stopAll();
1161
+ setOutput(prev => prev + ` Stopped ${stopped} process(es)\n\n`);
1162
+ }
1163
+
1164
+ setOutput(prev => prev + '🔄 Restarting AVC...\n');
1165
+ setTimeout(() => {
1166
+ exit();
1167
+ try {
1168
+ execSync(process.argv.join(' '), { stdio: 'inherit' });
1169
+ } catch { }
1170
+ process.exit(0);
1171
+ }, 500);
1172
+ return;
1173
+
1174
+ default:
1175
+ if (command.startsWith('/')) {
1176
+ setOutput(prev => prev + `\n❌ Unknown command: ${command}\n Type /help to see available commands\n Tip: Try /h for help, /v for version, /q to exit\n`);
1177
+ } else {
1178
+ setOutput(prev => prev + `\n💡 Commands must start with /\n Example: /init, /status, /help\n Tip: Type / and press Enter to see all commands\n`);
1179
+ }
1180
+ }
1181
+ } catch (error) {
1182
+ setOutput(prev => prev + `\n❌ Error: ${error.message}\n`);
1183
+ } finally {
1184
+ // Stop logger and show log file location
1185
+ if (logger) {
1186
+ logger.stop();
1187
+ const logPath = logger.getLogPath();
1188
+ if (logPath) {
1189
+ console.log(`\n📝 Command log saved: ${logPath}`);
403
1190
  }
1191
+
1192
+ // Cleanup old logs (keep last 10 per command)
1193
+ CommandLogger.cleanupOldLogs();
1194
+ }
404
1195
  }
405
- } catch (error) {
406
- setOutput(`\n❌ Error: ${error.message}\n`);
407
- }
408
1196
 
409
- // Return to prompt mode
410
- setIsExecuting(false);
411
- setTimeout(() => {
1197
+ // Return to prompt mode (unless in special mode like process-viewer or kill-confirm)
1198
+ setIsExecuting(false);
1199
+ setTimeout(() => {
1200
+ // Clear input first to prevent UI corruption
1201
+ setInput('');
1202
+
1203
+ // Only return to prompt if not in a special viewer/confirmation mode
1204
+ setMode((currentMode) => {
1205
+ if (currentMode === 'process-viewer' || currentMode === 'kill-confirm') {
1206
+ return currentMode; // Keep special modes
1207
+ }
1208
+ return 'prompt';
1209
+ });
1210
+ }, 100);
1211
+ } catch (outerError) {
1212
+ // Handle any unexpected errors
1213
+ console.error('Unexpected error in executeCommand:', outerError);
1214
+ setOutput(prev => prev + `\n❌ Unexpected error: ${outerError.message}\n Please try again or type /help for assistance\n`);
1215
+ setIsExecuting(false);
412
1216
  setMode('prompt');
413
1217
  setInput('');
414
- }, 100);
1218
+ }
415
1219
  };
416
1220
 
417
1221
  const showHelp = () => {
418
1222
  return `
419
1223
  📚 Available Commands:
420
1224
 
421
- /init (or /i) Initialize an AVC project (Sponsor Call ceremony)
422
- /status (or /s) Show current project status
423
- /help (or /h) Show this help message
424
- /version (or /v) Show version information
425
- /restart Restart AVC (Ctrl+R)
426
- /exit (or /q) Exit AVC interactive mode
1225
+ /init (or /i) Create AVC project structure and config files
1226
+ /documentation (/d) Build and serve project documentation
1227
+ /sponsor-call (/sc) Run Sponsor Call ceremony
1228
+ /status (or /s) Show current project status
1229
+ /processes (/p) View and manage background processes
1230
+ /remove (or /rm) Remove AVC project structure
1231
+ /help (or /h) Show this help message
1232
+ /version (or /v) Show version information
1233
+ /restart Restart AVC (Ctrl+R)
1234
+ /exit (or /q) Exit AVC interactive mode
427
1235
 
428
1236
  💡 Tips:
429
1237
  - Type / and press Enter to see interactive command selector
430
1238
  - Use arrow keys (↑/↓) to navigate command history
431
1239
  - Use Tab key to auto-complete commands
432
- - Use number keys (1-5) to quickly select commands from the menu
1240
+ - Use number keys to quickly select commands from the menu
433
1241
  - Press Esc to cancel command selector or dismiss notifications
434
1242
  - Press Ctrl+R to restart after updates
1243
+ - Background processes are shown in bottom-right corner
435
1244
  `;
436
1245
  };
437
1246
 
@@ -445,7 +1254,6 @@ const App = () => {
445
1254
  };
446
1255
 
447
1256
  const runInit = async () => {
448
- setOutput('\n'); // Empty line before init output
449
1257
  const initiator = new ProjectInitiator();
450
1258
 
451
1259
  // Capture console.log output
@@ -461,11 +1269,137 @@ const App = () => {
461
1269
  console.log = originalLog;
462
1270
  }
463
1271
 
464
- setOutput(logs.join('\n') + '\n');
1272
+ setOutput(prev => prev + logs.join('\n') + '\n');
1273
+ };
1274
+
1275
+ const runSponsorCall = async () => {
1276
+ const initiator = new ProjectInitiator();
1277
+
1278
+ // Check if project is initialized
1279
+ if (!initiator.isAvcProject()) {
1280
+ setOutput(prev => prev + '\n❌ Project not initialized\n\n');
1281
+ setOutput(prev => prev + ' Please run /init first to create the project structure.\n\n');
1282
+ return;
1283
+ }
1284
+
1285
+ const progressPath = initiator.sponsorCallProgressPath;
1286
+
1287
+ // Check for incomplete progress
1288
+ if (initiator.hasIncompleteProgress(progressPath)) {
1289
+ const savedProgress = initiator.readProgress(progressPath);
1290
+
1291
+ if (savedProgress && savedProgress.stage !== 'completed') {
1292
+ // TODO: Add user confirmation for resume
1293
+ // For now, auto-resume
1294
+ console.log('Resuming from saved progress...');
1295
+
1296
+ // Resume from saved progress
1297
+ setQuestionnaireActive(true);
1298
+ setCurrentQuestionIndex(savedProgress.currentQuestionIndex || 0);
1299
+ setQuestionnaireAnswers(savedProgress.collectedValues || {});
1300
+ setCurrentAnswer(savedProgress.currentAnswer ? savedProgress.currentAnswer.split('\n') : []);
1301
+ setOutput(prev => prev + '\n🎯 Sponsor Call Ceremony - Resuming from saved progress\n');
1302
+ return;
1303
+ }
1304
+ }
1305
+
1306
+ // Start fresh
1307
+ setQuestionnaireActive(true);
1308
+ setCurrentQuestionIndex(0);
1309
+ setQuestionnaireAnswers({});
1310
+ setCurrentAnswer([]);
1311
+ setEmptyLineCount(0);
1312
+ setEditMode(false);
1313
+ setShowPreview(false);
1314
+ setOutput(prev => prev + '\n🎯 Sponsor Call Ceremony - Interactive Questionnaire\n');
1315
+ };
1316
+
1317
+ const runSponsorCallWithAnswers = async (answers) => {
1318
+ const initiator = new ProjectInitiator();
1319
+
1320
+ // Capture console output
1321
+ const originalLog = console.log;
1322
+ const originalError = console.error;
1323
+ let logs = [];
1324
+ console.log = (...args) => logs.push(args.join(' '));
1325
+ console.error = (...args) => logs.push(args.join(' '));
1326
+
1327
+ try {
1328
+ // Pass answers to ceremony
1329
+ await initiator.sponsorCallWithAnswers(answers);
1330
+ } finally {
1331
+ console.log = originalLog;
1332
+ console.error = originalError;
1333
+ }
1334
+
1335
+ setOutput(prev => prev + logs.join('\n') + '\n');
1336
+ };
1337
+
1338
+ const saveQuestionnaireAnswer = (key, value) => {
1339
+ setQuestionnaireAnswers({
1340
+ ...questionnaireAnswers,
1341
+ [key]: value
1342
+ });
1343
+ };
1344
+
1345
+ const moveToNextQuestion = () => {
1346
+ setCurrentAnswer([]);
1347
+ setEmptyLineCount(0);
1348
+
1349
+ if (currentQuestionIndex < questionnaireQuestions.length - 1) {
1350
+ setCurrentQuestionIndex(currentQuestionIndex + 1);
1351
+ } else {
1352
+ // All questions answered - show preview
1353
+ submitQuestionnaire();
1354
+ }
1355
+ };
1356
+
1357
+ const submitQuestionnaire = () => {
1358
+ // Show preview instead of immediate submission
1359
+ setShowPreview(true);
1360
+ };
1361
+
1362
+ const confirmSubmission = async () => {
1363
+ setShowPreview(false);
1364
+ setQuestionnaireActive(false);
1365
+ setMode('executing');
1366
+ setIsExecuting(true);
1367
+ setExecutingMessage('Generating project document with AI...');
1368
+
1369
+ // Call ceremony with pre-filled answers
1370
+ await runSponsorCallWithAnswers(questionnaireAnswers);
1371
+
1372
+ // Return to prompt mode
1373
+ setIsExecuting(false);
1374
+ setTimeout(() => {
1375
+ setMode('prompt');
1376
+ setInput('');
1377
+ }, 100);
1378
+ };
1379
+
1380
+ const autoSaveProgress = () => {
1381
+ if (!questionnaireActive) return;
1382
+
1383
+ try {
1384
+ const initiator = new ProjectInitiator();
1385
+ const progress = {
1386
+ stage: 'questionnaire',
1387
+ totalQuestions: questionnaireQuestions.length,
1388
+ answeredQuestions: Object.keys(questionnaireAnswers).length,
1389
+ collectedValues: questionnaireAnswers,
1390
+ currentQuestionIndex,
1391
+ currentAnswer: currentAnswer.join('\n'),
1392
+ lastUpdate: new Date().toISOString()
1393
+ };
1394
+
1395
+ initiator.writeProgress(progress, initiator.sponsorCallProgressPath);
1396
+ setLastAutoSave(new Date());
1397
+ } catch (error) {
1398
+ // Silently fail auto-save
1399
+ }
465
1400
  };
466
1401
 
467
1402
  const runStatus = async () => {
468
- setOutput('\n'); // Empty line before status output
469
1403
  const initiator = new ProjectInitiator();
470
1404
 
471
1405
  // Capture console.log output
@@ -481,7 +1415,220 @@ const App = () => {
481
1415
  console.log = originalLog;
482
1416
  }
483
1417
 
484
- setOutput(logs.join('\n') + '\n');
1418
+ setOutput(prev => prev + logs.join('\n') + '\n');
1419
+ };
1420
+
1421
+ const runRemove = async () => {
1422
+ const initiator = new ProjectInitiator();
1423
+
1424
+ // Check if project is initialized
1425
+ if (!initiator.isAvcProject()) {
1426
+ setOutput(prev => prev + '\n⚠️ No AVC project found in this directory.\n\nNothing to remove.\n');
1427
+ return;
1428
+ }
1429
+
1430
+ // Get AVC contents to display
1431
+ const contents = initiator.getAvcContents();
1432
+ setAvcContents(contents);
1433
+
1434
+ // Activate remove confirmation mode
1435
+ setRemoveConfirmActive(true);
1436
+ setRemoveConfirmInput('');
1437
+ };
1438
+
1439
+ const confirmRemove = async () => {
1440
+ setRemoveConfirmActive(false);
1441
+ setMode('executing');
1442
+ setIsExecuting(true);
1443
+ setExecutingMessage('Deleting AVC project structure...');
1444
+
1445
+ const initiator = new ProjectInitiator();
1446
+
1447
+ // Capture console.log output
1448
+ const originalLog = console.log;
1449
+ let logs = [];
1450
+ console.log = (...args) => {
1451
+ logs.push(args.join(' '));
1452
+ };
1453
+
1454
+ try {
1455
+ // Perform the deletion
1456
+ const deletedItems = initiator.getAvcContents();
1457
+
1458
+ // Delete .avc folder
1459
+ const fs = await import('fs');
1460
+ fs.rmSync(initiator.avcDir, { recursive: true, force: true });
1461
+
1462
+ logs.push('✅ Successfully deleted:\n');
1463
+ logs.push(' 📁 .avc/ folder and all contents:');
1464
+ deletedItems.forEach(item => {
1465
+ logs.push(` • ${item}`);
1466
+ });
1467
+ logs.push('');
1468
+
1469
+ // Check for .env file
1470
+ const path = await import('path');
1471
+ const envPath = path.join(initiator.projectRoot, '.env');
1472
+ if (fs.existsSync(envPath)) {
1473
+ logs.push('ℹ️ Manual cleanup reminder:\n');
1474
+ logs.push(' The .env file was NOT deleted and still contains:');
1475
+ logs.push(' • ANTHROPIC_API_KEY');
1476
+ logs.push(' • GEMINI_API_KEY');
1477
+ logs.push(' • (and any other API keys you added)\n');
1478
+ }
1479
+
1480
+ logs.push('✅ AVC project structure has been completely removed.\n');
1481
+ logs.push('You can re-initialize anytime by running /init\n');
1482
+ } catch (error) {
1483
+ logs.push(`❌ Error during deletion: ${error.message}\n`);
1484
+ logs.push('The .avc folder may be partially deleted.');
1485
+ logs.push('You may need to manually remove it.\n');
1486
+ } finally {
1487
+ console.log = originalLog;
1488
+ }
1489
+
1490
+ setOutput(prev => prev + logs.join('\n'));
1491
+
1492
+ // Return to prompt mode
1493
+ setIsExecuting(false);
1494
+ setTimeout(() => {
1495
+ setMode('prompt');
1496
+ setInput('');
1497
+ }, 100);
1498
+ };
1499
+
1500
+ const runBuildDocumentation = async () => {
1501
+ const builder = new DocumentationBuilder();
1502
+ const manager = getProcessManager();
1503
+
1504
+ // Check if project is initialized
1505
+ if (!builder.hasDocumentation()) {
1506
+ setOutput(prev => prev + '\n❌ Documentation not found\n\n');
1507
+ setOutput(prev => prev + ' Please run /init first to create documentation structure.\n\n');
1508
+ return;
1509
+ }
1510
+
1511
+ const port = builder.getPort();
1512
+
1513
+ // Check if documentation server is already running (managed process)
1514
+ const runningProcesses = manager.getRunningProcesses();
1515
+ const existingDocServer = runningProcesses.find(p => p.name === 'Documentation Server');
1516
+
1517
+ if (existingDocServer) {
1518
+ // We have a managed process - check if it's actually running
1519
+ const portInUse = await builder.isPortInUse(port);
1520
+
1521
+ if (portInUse) {
1522
+ // Managed process exists and port is in use - restart it
1523
+ setOutput(prev => prev + '\n🔄 Documentation server already running, restarting...\n\n');
1524
+ manager.stopProcess(existingDocServer.id);
1525
+
1526
+ // Clean up stopped/finished processes
1527
+ manager.cleanupFinished();
1528
+
1529
+ // Wait a bit for the port to be released
1530
+ await new Promise(resolve => setTimeout(resolve, 1000));
1531
+ } else {
1532
+ // Managed process exists but port is not in use - it died, clean up
1533
+ setOutput(prev => prev + '\n⚠️ Previous documentation server died, starting new one...\n\n');
1534
+ manager.stopProcess(existingDocServer.id);
1535
+
1536
+ // Clean up stopped/finished processes
1537
+ manager.cleanupFinished();
1538
+ }
1539
+ } else {
1540
+ // No managed process - check if port is in use by external process
1541
+ const portInUse = await builder.isPortInUse(port);
1542
+
1543
+ if (portInUse) {
1544
+ // Port is in use by external process - find and kill it
1545
+ const processInfo = await builder.findProcessUsingPort(port);
1546
+
1547
+ if (processInfo) {
1548
+ // Found the process using the port - check if it's AVC documentation
1549
+ const isOurDocs = await builder.isDocumentationServer(port);
1550
+
1551
+ if (isOurDocs) {
1552
+ // It's confirmed to be AVC documentation server - safe to kill
1553
+ setOutput(prev => prev + '\n⚠️ AVC documentation server already running (external process)\n');
1554
+ setOutput(prev => prev + ` Process: ${processInfo.command} (PID: ${processInfo.pid})\n`);
1555
+ setOutput(prev => prev + ' Killing external process and restarting...\n\n');
1556
+
1557
+ // Try to kill the process
1558
+ const killed = await builder.killProcess(processInfo.pid);
1559
+
1560
+ if (!killed) {
1561
+ // Failed to kill (permission denied, etc.)
1562
+ setOutput(prev => prev + `❌ Failed to kill process ${processInfo.pid}\n\n`);
1563
+ setOutput(prev => prev + ` Unable to stop the process (permission denied or process protected).\n`);
1564
+ setOutput(prev => prev + ` Please manually stop the process or change the port.\n\n`);
1565
+ setOutput(prev => prev + ` To change the port, edit .avc/avc.json:\n`);
1566
+ setOutput(prev => prev + ` {\n`);
1567
+ setOutput(prev => prev + ` "settings": {\n`);
1568
+ setOutput(prev => prev + ` "documentation": {\n`);
1569
+ setOutput(prev => prev + ` "port": 5173\n`);
1570
+ setOutput(prev => prev + ` }\n`);
1571
+ setOutput(prev => prev + ` }\n`);
1572
+ setOutput(prev => prev + ` }\n\n`);
1573
+ return;
1574
+ }
1575
+
1576
+ setOutput(prev => prev + '✓ Process killed successfully\n\n');
1577
+
1578
+ // Remove from process manager if it was a managed process
1579
+ manager.removeProcessByPid(processInfo.pid);
1580
+
1581
+ // Wait for port to be released
1582
+ await new Promise(resolve => setTimeout(resolve, 1000));
1583
+ } else {
1584
+ // It's NOT AVC documentation - ask user if they want to kill it anyway
1585
+ setProcessToKill({
1586
+ pid: processInfo.pid,
1587
+ command: processInfo.command,
1588
+ port: port
1589
+ });
1590
+ setKillConfirmActive(true);
1591
+ setMode('kill-confirm');
1592
+ return;
1593
+ }
1594
+ } else {
1595
+ // Port is in use but couldn't find the process (rare case)
1596
+ setOutput(prev => prev + `\n❌ Port ${port} is in use but process could not be identified\n\n`);
1597
+ setOutput(prev => prev + ` To change the port, edit .avc/avc.json:\n`);
1598
+ setOutput(prev => prev + ` {\n`);
1599
+ setOutput(prev => prev + ` "settings": {\n`);
1600
+ setOutput(prev => prev + ` "documentation": {\n`);
1601
+ setOutput(prev => prev + ` "port": 5173\n`);
1602
+ setOutput(prev => prev + ` }\n`);
1603
+ setOutput(prev => prev + ` }\n`);
1604
+ setOutput(prev => prev + ` }\n\n`);
1605
+ return;
1606
+ }
1607
+ }
1608
+ }
1609
+
1610
+ // Build documentation first
1611
+ setOutput(prev => prev + '\n📚 Building documentation...\n');
1612
+
1613
+ try {
1614
+ await builder.build();
1615
+ setOutput(prev => prev + '✓ Documentation built successfully\n\n');
1616
+ } catch (error) {
1617
+ setOutput(prev => prev + `\n❌ Error: ${error.message}\n\n`);
1618
+ return;
1619
+ }
1620
+
1621
+ // Start preview server in background
1622
+ const processId = manager.startProcess({
1623
+ name: 'Documentation Server',
1624
+ command: 'npx',
1625
+ args: ['vitepress', 'preview', '--port', String(port)],
1626
+ cwd: builder.docsDir
1627
+ });
1628
+
1629
+ setOutput(prev => prev + '📦 Starting documentation server in background...\n');
1630
+ setOutput(prev => prev + ` URL: http://localhost:${port}\n`);
1631
+ setOutput(prev => prev + ` View process output: /processes\n\n`);
485
1632
  };
486
1633
 
487
1634
  // Handle keyboard input in prompt mode
@@ -490,7 +1637,16 @@ const App = () => {
490
1637
 
491
1638
  // Handle Ctrl+R for restart (re-exec current process)
492
1639
  if (key.ctrl && inputChar === 'r') {
493
- setOutput('\n🔄 Restarting AVC...\n');
1640
+ const ctrlRManager = getProcessManager();
1641
+ const runningOnCtrlR = ctrlRManager.getRunningProcesses();
1642
+
1643
+ if (runningOnCtrlR.length > 0) {
1644
+ setOutput(prev => prev + '\n🛑 Stopping background processes...\n');
1645
+ const stopped = ctrlRManager.stopAll();
1646
+ setOutput(prev => prev + ` Stopped ${stopped} process(es)\n\n`);
1647
+ }
1648
+
1649
+ setOutput(prev => prev + '🔄 Restarting AVC...\n');
494
1650
  setTimeout(() => {
495
1651
  exit();
496
1652
  try {
@@ -501,8 +1657,16 @@ const App = () => {
501
1657
  return;
502
1658
  }
503
1659
 
504
- // Handle Esc to dismiss update notification
1660
+ // Handle Esc to clear input or dismiss update notification
505
1661
  if (key.escape) {
1662
+ // If there's input, clear it
1663
+ if (input.length > 0) {
1664
+ setInput('');
1665
+ setHistoryIndex(-1);
1666
+ return;
1667
+ }
1668
+
1669
+ // If no input, dismiss update notification (if any)
506
1670
  const checker = new UpdateChecker();
507
1671
  const state = checker.readState();
508
1672
  if (state.updateAvailable && !state.userDismissed) {
@@ -546,7 +1710,6 @@ const App = () => {
546
1710
  if (input === '/' || input.startsWith('/')) {
547
1711
  // If just "/" or partial command, show/stay in selector
548
1712
  if (input === '/') {
549
- setOutput(''); // Clear previous output
550
1713
  setMode('selector');
551
1714
  setInput(''); // Clear input when entering selector
552
1715
  } else {
@@ -581,7 +1744,6 @@ const App = () => {
581
1744
 
582
1745
  // Show selector immediately when "/" is typed
583
1746
  if (newInput === '/' || (newInput.startsWith('/') && newInput.length > 1)) {
584
- setOutput(''); // Clear previous output
585
1747
  setMode('selector');
586
1748
  }
587
1749
  }
@@ -616,25 +1778,441 @@ const App = () => {
616
1778
  }
617
1779
  }, { isActive: mode === 'selector' });
618
1780
 
619
- // Render output - show spinner during execution, or output after completion
1781
+ // Questionnaire mode input handler
1782
+ useInput((inputChar, key) => {
1783
+ if (!questionnaireActive || showPreview) return;
1784
+
1785
+ const currentQuestion = questionnaireQuestions[currentQuestionIndex];
1786
+
1787
+ // Handle Ctrl+E to enter edit mode
1788
+ if (key.ctrl && inputChar === 'e') {
1789
+ setEditMode(true);
1790
+ return;
1791
+ }
1792
+
1793
+ // Handle Ctrl+P (previous question) when in edit mode
1794
+ if (editMode && key.ctrl && inputChar === 'p') {
1795
+ if (currentQuestionIndex > 0) {
1796
+ const newIndex = currentQuestionIndex - 1;
1797
+ setCurrentQuestionIndex(newIndex);
1798
+ const prevKey = questionnaireQuestions[newIndex].key;
1799
+ const prevAnswer = questionnaireAnswers[prevKey] || '';
1800
+ setCurrentAnswer(prevAnswer ? prevAnswer.split('\n') : []);
1801
+ }
1802
+ return;
1803
+ }
1804
+
1805
+ // Handle Ctrl+N (next question) when in edit mode
1806
+ if (editMode && key.ctrl && inputChar === 'n') {
1807
+ if (currentQuestionIndex < questionnaireQuestions.length - 1) {
1808
+ const newIndex = currentQuestionIndex + 1;
1809
+ setCurrentQuestionIndex(newIndex);
1810
+ const nextKey = questionnaireQuestions[newIndex].key;
1811
+ const nextAnswer = questionnaireAnswers[nextKey] || '';
1812
+ setCurrentAnswer(nextAnswer ? nextAnswer.split('\n') : []);
1813
+ }
1814
+ return;
1815
+ }
1816
+
1817
+ // Handle Ctrl+X to exit edit mode
1818
+ if (editMode && key.ctrl && inputChar === 'x') {
1819
+ setEditMode(false);
1820
+ return;
1821
+ }
1822
+
1823
+ // Handle Enter key
1824
+ if (key.return) {
1825
+ const currentLineText = currentAnswer[currentAnswer.length - 1] || '';
1826
+
1827
+ // Empty line on first input = skip question
1828
+ if (currentAnswer.length === 0 && currentLineText === '') {
1829
+ console.log(' Skipping - will use AI suggestion...');
1830
+ saveQuestionnaireAnswer(currentQuestion.key, null);
1831
+ moveToNextQuestion();
1832
+ return;
1833
+ }
1834
+
1835
+ // Empty line after text = increment empty line counter
1836
+ if (currentLineText === '') {
1837
+ const newEmptyCount = emptyLineCount + 1;
1838
+ setEmptyLineCount(newEmptyCount);
1839
+
1840
+ // Two consecutive empty lines = done with this answer
1841
+ if (newEmptyCount >= 1) {
1842
+ const answerText = currentAnswer.slice(0, -1).join('\n').trim();
1843
+ saveQuestionnaireAnswer(currentQuestion.key, answerText);
1844
+ moveToNextQuestion();
1845
+ return;
1846
+ }
1847
+ } else {
1848
+ // Non-empty line, add new line for next input
1849
+ setCurrentAnswer([...currentAnswer, '']);
1850
+ setEmptyLineCount(0);
1851
+ }
1852
+
1853
+ return;
1854
+ }
1855
+
1856
+ // Handle Backspace
1857
+ if (key.backspace || key.delete) {
1858
+ const lastLineIndex = currentAnswer.length - 1;
1859
+ const lastLine = currentAnswer[lastLineIndex] || '';
1860
+
1861
+ if (lastLine.length > 0) {
1862
+ // Remove last character from current line
1863
+ const newLines = [...currentAnswer];
1864
+ newLines[lastLineIndex] = lastLine.slice(0, -1);
1865
+ setCurrentAnswer(newLines);
1866
+ } else if (lastLineIndex > 0) {
1867
+ // Remove empty line, go back to previous line
1868
+ setCurrentAnswer(currentAnswer.slice(0, -1));
1869
+ }
1870
+ return;
1871
+ }
1872
+
1873
+ // Handle Escape (cancel questionnaire)
1874
+ if (key.escape) {
1875
+ setQuestionnaireActive(false);
1876
+ setCurrentQuestionIndex(0);
1877
+ setQuestionnaireAnswers({});
1878
+ setCurrentAnswer([]);
1879
+ setMode('prompt');
1880
+ setOutput(prev => prev + '\n❌ Questionnaire cancelled\n');
1881
+ return;
1882
+ }
1883
+
1884
+ // Regular character input
1885
+ if (inputChar && !key.ctrl && !key.meta) {
1886
+ const lastLineIndex = currentAnswer.length - 1;
1887
+ const lastLine = currentAnswer[lastLineIndex] || '';
1888
+
1889
+ const newLines = [...currentAnswer];
1890
+ if (newLines.length === 0) {
1891
+ newLines.push(inputChar);
1892
+ } else {
1893
+ newLines[lastLineIndex] = lastLine + inputChar;
1894
+ }
1895
+
1896
+ setCurrentAnswer(newLines);
1897
+ setEmptyLineCount(0); // Reset on any character input
1898
+ }
1899
+ }, { isActive: questionnaireActive && !showPreview });
1900
+
1901
+ // Preview mode input handler
1902
+ useInput((inputChar, key) => {
1903
+ if (!showPreview) return;
1904
+
1905
+ // Enter to confirm submission
1906
+ if (key.return) {
1907
+ confirmSubmission();
1908
+ return;
1909
+ }
1910
+
1911
+ // Ctrl+E to go back and edit
1912
+ if (key.ctrl && inputChar === 'e') {
1913
+ setShowPreview(false);
1914
+ setEditMode(true);
1915
+ setCurrentQuestionIndex(0);
1916
+ const firstKey = questionnaireQuestions[0].key;
1917
+ const firstAnswer = questionnaireAnswers[firstKey] || '';
1918
+ setCurrentAnswer(firstAnswer ? firstAnswer.split('\n') : []);
1919
+ return;
1920
+ }
1921
+
1922
+ // Escape to cancel
1923
+ if (key.escape) {
1924
+ setShowPreview(false);
1925
+ setQuestionnaireActive(false);
1926
+ setCurrentQuestionIndex(0);
1927
+ setQuestionnaireAnswers({});
1928
+ setCurrentAnswer([]);
1929
+ setMode('prompt');
1930
+ setOutput(prev => prev + '\n❌ Questionnaire cancelled\n');
1931
+ return;
1932
+ }
1933
+ }, { isActive: showPreview });
1934
+
1935
+ // Remove confirmation input handler
1936
+ useInput((inputChar, key) => {
1937
+ if (!removeConfirmActive) return;
1938
+
1939
+ // Handle Enter key
1940
+ if (key.return) {
1941
+ if (removeConfirmInput.trim() === 'delete all') {
1942
+ // Confirmation matched - proceed with deletion
1943
+ confirmRemove();
1944
+ } else {
1945
+ // Confirmation didn't match - cancel
1946
+ setRemoveConfirmActive(false);
1947
+ setRemoveConfirmInput('');
1948
+ setMode('prompt');
1949
+ setOutput(prev => prev + '\n❌ Operation cancelled.\n\nNo files were deleted.\n');
1950
+ }
1951
+ return;
1952
+ }
1953
+
1954
+ // Handle Backspace
1955
+ if (key.backspace || key.delete) {
1956
+ setRemoveConfirmInput(removeConfirmInput.slice(0, -1));
1957
+ return;
1958
+ }
1959
+
1960
+ // Handle Escape (cancel)
1961
+ if (key.escape) {
1962
+ setRemoveConfirmActive(false);
1963
+ setRemoveConfirmInput('');
1964
+ setMode('prompt');
1965
+ setOutput('\n❌ Operation cancelled.\n\nNo files were deleted.\n');
1966
+ return;
1967
+ }
1968
+
1969
+ // Regular character input
1970
+ if (inputChar && !key.ctrl && !key.meta) {
1971
+ setRemoveConfirmInput(removeConfirmInput + inputChar);
1972
+ }
1973
+ }, { isActive: removeConfirmActive });
1974
+
1975
+ // Kill external process confirmation input handler
1976
+ useInput(async (inputChar, key) => {
1977
+ if (!killConfirmActive) return;
1978
+
1979
+ // Handle Enter key
1980
+ if (key.return) {
1981
+ const input = killConfirmInput.trim().toLowerCase();
1982
+
1983
+ if (input === 'kill') {
1984
+ // User confirmed - kill the process
1985
+ setKillConfirmActive(false);
1986
+ setKillConfirmInput('');
1987
+ setMode('executing');
1988
+ setIsExecuting(true);
1989
+ setExecutingMessage('Killing external process...');
1990
+
1991
+ const builder = new DocumentationBuilder(process.cwd());
1992
+ const killed = await builder.killProcess(processToKill.pid);
1993
+
1994
+ if (!killed) {
1995
+ // Failed to kill - reset mode and show error
1996
+ setIsExecuting(false);
1997
+ setMode('prompt');
1998
+ setOutput(prev => prev + `\n❌ Failed to kill process ${processToKill.pid}\n\n`);
1999
+ setOutput(prev => prev + ` Unable to stop the process (permission denied or process protected).\n`);
2000
+ setOutput(prev => prev + ` Please manually stop the process or change the port.\n\n`);
2001
+ setOutput(prev => prev + ` To change the port, edit .avc/avc.json:\n`);
2002
+ setOutput(prev => prev + ` {\n`);
2003
+ setOutput(prev => prev + ` "settings": {\n`);
2004
+ setOutput(prev => prev + ` "documentation": {\n`);
2005
+ setOutput(prev => prev + ` "port": 5173\n`);
2006
+ setOutput(prev => prev + ` }\n`);
2007
+ setOutput(prev => prev + ` }\n`);
2008
+ setOutput(prev => prev + ` }\n\n`);
2009
+ return;
2010
+ }
2011
+
2012
+ setOutput(prev => prev + `\n✓ Process ${processToKill.pid} killed successfully\n\n`);
2013
+
2014
+ // Remove from process manager if it was managed
2015
+ const manager = getProcessManager();
2016
+ manager.removeProcessByPid(processToKill.pid);
2017
+
2018
+ // Wait for port to be released
2019
+ await new Promise(resolve => setTimeout(resolve, 1000));
2020
+
2021
+ // Now proceed with building and starting documentation
2022
+ setOutput(prev => prev + '📚 Building documentation...\n');
2023
+ setExecutingMessage('Building documentation...');
2024
+
2025
+ try {
2026
+ await builder.build();
2027
+ setOutput(prev => prev + '✓ Documentation built successfully\n\n');
2028
+
2029
+ setExecutingMessage('Starting documentation server...');
2030
+
2031
+ const port = builder.getPort();
2032
+ const manager = getProcessManager();
2033
+
2034
+ const processId = manager.startProcess({
2035
+ name: 'Documentation Server',
2036
+ command: 'npx',
2037
+ args: ['vitepress', 'preview', '--port', String(port)],
2038
+ cwd: builder.docsDir
2039
+ });
2040
+
2041
+ setOutput(prev => prev + '📦 Starting documentation server in background...\n');
2042
+ setOutput(prev => prev + ` URL: http://localhost:${port}\n`);
2043
+ setOutput(prev => prev + ` View process output: /processes\n\n`);
2044
+ } catch (error) {
2045
+ setOutput(prev => prev + `\n❌ Error: ${error.message}\n\n`);
2046
+ } finally {
2047
+ setIsExecuting(false);
2048
+ setMode('prompt');
2049
+ }
2050
+
2051
+ } else if (input === 'no' || input === 'n' || input === '') {
2052
+ // User cancelled
2053
+ setKillConfirmActive(false);
2054
+ setKillConfirmInput('');
2055
+ setMode('prompt');
2056
+ setOutput(prev => prev + '\n❌ Operation cancelled.\n\n');
2057
+ setOutput(prev => prev + ` To change the port, edit .avc/avc.json:\n`);
2058
+ setOutput(prev => prev + ` {\n`);
2059
+ setOutput(prev => prev + ` "settings": {\n`);
2060
+ setOutput(prev => prev + ` "documentation": {\n`);
2061
+ setOutput(prev => prev + ` "port": 5173\n`);
2062
+ setOutput(prev => prev + ` }\n`);
2063
+ setOutput(prev => prev + ` }\n`);
2064
+ setOutput(prev => prev + ` }\n\n`);
2065
+ } else {
2066
+ // Invalid input
2067
+ setKillConfirmActive(false);
2068
+ setKillConfirmInput('');
2069
+ setMode('prompt');
2070
+ setOutput(prev => prev + '\n❌ Invalid response. Operation cancelled.\n\n');
2071
+ }
2072
+ return;
2073
+ }
2074
+
2075
+ // Handle Backspace
2076
+ if (key.backspace || key.delete) {
2077
+ setKillConfirmInput(killConfirmInput.slice(0, -1));
2078
+ return;
2079
+ }
2080
+
2081
+ // Handle Escape (cancel)
2082
+ if (key.escape) {
2083
+ setKillConfirmActive(false);
2084
+ setKillConfirmInput('');
2085
+ setMode('prompt');
2086
+ setOutput(prev => prev + '\n❌ Operation cancelled.\n\n');
2087
+ return;
2088
+ }
2089
+
2090
+ // Regular character input
2091
+ if (inputChar && !key.ctrl && !key.meta) {
2092
+ setKillConfirmInput(killConfirmInput + inputChar);
2093
+ }
2094
+ }, { isActive: killConfirmActive });
2095
+
2096
+ // Process viewer handlers
2097
+ const handleProcessSelect = (process) => {
2098
+ setSelectedProcessId(process.id);
2099
+ };
2100
+
2101
+ const handleProcessViewerCancel = () => {
2102
+ setProcessViewerActive(false);
2103
+ setSelectedProcessId(null);
2104
+ setMode('prompt');
2105
+ setInput('');
2106
+ };
2107
+
2108
+ const handleProcessDetailsBack = () => {
2109
+ setSelectedProcessId(null);
2110
+ };
2111
+
2112
+ const handleProcessStop = (processId) => {
2113
+ const manager = getProcessManager();
2114
+ manager.stopProcess(processId);
2115
+ setOutput(prev => prev + `\n✓ Process stopped\n`);
2116
+ // Stay on details view to see final output
2117
+ };
2118
+
2119
+ // Render process viewer
2120
+ const renderProcessViewer = () => {
2121
+ if (!processViewerActive) return null;
2122
+
2123
+ if (selectedProcessId) {
2124
+ const process = backgroundProcesses.get(selectedProcessId);
2125
+ if (!process) {
2126
+ // Process no longer exists
2127
+ setSelectedProcessId(null);
2128
+ return null;
2129
+ }
2130
+
2131
+ return React.createElement(ProcessDetailsViewer, {
2132
+ process,
2133
+ onBack: handleProcessDetailsBack,
2134
+ onStop: handleProcessStop
2135
+ });
2136
+ }
2137
+
2138
+ return React.createElement(ProcessViewer, {
2139
+ processes: backgroundProcesses,
2140
+ onSelect: handleProcessSelect,
2141
+ onCancel: handleProcessViewerCancel
2142
+ });
2143
+ };
2144
+
2145
+ // Render output - show questionnaire, preview, remove confirmation, spinner, or output
620
2146
  const renderOutput = () => {
2147
+ // Show kill confirmation if active
2148
+ if (killConfirmActive) {
2149
+ return React.createElement(Box, { marginY: 1 },
2150
+ React.createElement(KillProcessConfirmation, {
2151
+ processInfo: processToKill,
2152
+ confirmInput: killConfirmInput
2153
+ })
2154
+ );
2155
+ }
2156
+
2157
+ // Show remove confirmation if active
2158
+ if (removeConfirmActive) {
2159
+ return React.createElement(Box, { marginY: 1 },
2160
+ React.createElement(RemoveConfirmation, {
2161
+ contents: avcContents,
2162
+ confirmInput: removeConfirmInput
2163
+ })
2164
+ );
2165
+ }
2166
+
2167
+ // Show preview if active
2168
+ if (showPreview) {
2169
+ return React.createElement(Box, { marginY: 1 },
2170
+ React.createElement(Text, null, output),
2171
+ React.createElement(AnswersPreview, {
2172
+ answers: questionnaireAnswers,
2173
+ questions: questionnaireQuestions
2174
+ })
2175
+ );
2176
+ }
2177
+
2178
+ // Show questionnaire if active
2179
+ if (questionnaireActive) {
2180
+ const currentQuestion = questionnaireQuestions[currentQuestionIndex];
2181
+
2182
+ return React.createElement(Box, { flexDirection: 'column', flexShrink: 0, overflow: 'hidden' },
2183
+ React.createElement(Text, null, output),
2184
+ React.createElement(QuestionDisplay, {
2185
+ question: currentQuestion,
2186
+ index: currentQuestionIndex,
2187
+ total: questionnaireQuestions.length,
2188
+ editMode
2189
+ }),
2190
+ React.createElement(MultiLineInput, {
2191
+ lines: currentAnswer,
2192
+ showLineNumbers: true,
2193
+ showCharCount: true
2194
+ }),
2195
+ React.createElement(QuestionnaireProgress, {
2196
+ current: currentQuestionIndex,
2197
+ total: questionnaireQuestions.length,
2198
+ answers: questionnaireAnswers,
2199
+ lastSave: lastAutoSave
2200
+ }),
2201
+ React.createElement(QuestionnaireHelp, { editMode })
2202
+ );
2203
+ }
2204
+
621
2205
  // Show spinner while executing
622
2206
  if (isExecuting) {
623
- return React.createElement(React.Fragment, null,
624
- React.createElement(Separator),
625
- React.createElement(Box, { marginY: 1 },
626
- React.createElement(LoadingSpinner, { message: executingMessage })
627
- )
2207
+ return React.createElement(Box, { marginY: 1, flexShrink: 0 },
2208
+ React.createElement(LoadingSpinner, { message: executingMessage })
628
2209
  );
629
2210
  }
630
2211
 
631
2212
  // Show output if available (even after returning to prompt mode)
632
2213
  if (output) {
633
- return React.createElement(React.Fragment, null,
634
- React.createElement(Separator),
635
- React.createElement(Box, { marginY: 1 },
636
- React.createElement(Text, null, output)
637
- )
2214
+ return React.createElement(Box, { marginY: 1, flexShrink: 0 },
2215
+ React.createElement(Text, null, output)
638
2216
  );
639
2217
  }
640
2218
 
@@ -645,16 +2223,19 @@ const App = () => {
645
2223
  const renderSelector = () => {
646
2224
  if (mode !== 'selector') return null;
647
2225
 
648
- return React.createElement(React.Fragment, null,
649
- React.createElement(Separator),
650
- React.createElement(Box, { flexDirection: 'column', marginY: 1 },
651
- React.createElement(Box, { marginBottom: 1 },
652
- React.createElement(InputWithCursor, { input: input })
653
- ),
2226
+ return React.createElement(Box, { flexDirection: 'column', flexShrink: 0 },
2227
+ React.createElement(InputWithCursor, { input: input }),
2228
+ React.createElement(Box, { marginTop: 1, flexShrink: 0 },
654
2229
  React.createElement(CommandSelector, {
655
2230
  filter: input,
656
2231
  onSelect: (item) => {
657
- executeCommand(item.value);
2232
+ if (item && item.value) {
2233
+ executeCommand(item.value);
2234
+ } else {
2235
+ // Invalid item, return to prompt
2236
+ setMode('prompt');
2237
+ setInput('');
2238
+ }
658
2239
  },
659
2240
  onCancel: () => {
660
2241
  setMode('prompt');
@@ -667,98 +2248,82 @@ const App = () => {
667
2248
 
668
2249
  // Render prompt when in prompt mode
669
2250
  const renderPrompt = () => {
670
- if (mode !== 'prompt') return null;
2251
+ if (mode !== 'prompt' || questionnaireActive || showPreview || removeConfirmActive || killConfirmActive || processViewerActive) return null;
671
2252
 
672
- return React.createElement(React.Fragment, null,
673
- React.createElement(Separator),
674
- React.createElement(Box, { flexDirection: 'column' },
675
- React.createElement(InputWithCursor, { input: input }),
676
- React.createElement(HistoryHint, { hasHistory: commandHistory.length > 0 })
677
- ),
678
- React.createElement(Separator)
2253
+ return React.createElement(Box, { flexDirection: 'column', flexShrink: 0 },
2254
+ React.createElement(InputWithCursor, { input: input }),
2255
+ React.createElement(HistoryHint, { hasHistory: commandHistory.length > 0 })
679
2256
  );
680
2257
  };
681
2258
 
682
- return React.createElement(Box, { flexDirection: 'column' },
2259
+ return React.createElement(Box, { flexDirection: 'column', overflow: 'hidden' },
683
2260
  React.createElement(Banner),
684
2261
  renderOutput(),
2262
+ renderProcessViewer(),
685
2263
  renderSelector(),
686
2264
  renderPrompt(),
687
- React.createElement(BottomRightStatus)
2265
+ !questionnaireActive && !showPreview && !removeConfirmActive && !killConfirmActive && !processViewerActive && React.createElement(BottomRightStatus, { backgroundProcesses })
688
2266
  );
689
2267
  };
690
2268
 
691
2269
  // Export render function
692
2270
  export function startRepl() {
693
- console.clear();
694
- render(React.createElement(App));
695
- }
2271
+ // Set environment variable to indicate REPL mode
2272
+ process.env.AVC_REPL_MODE = 'true';
2273
+
2274
+ // Set up signal handlers for graceful shutdown
2275
+ const cleanupAndExit = (signal) => {
2276
+ const manager = getProcessManager();
2277
+ const running = manager.getRunningProcesses();
2278
+
2279
+ if (running.length > 0) {
2280
+ console.log('\n\n🛑 Stopping background processes...');
2281
+ const stopped = manager.stopAll();
2282
+ console.log(` Stopped ${stopped} process(es)\n`);
2283
+ }
696
2284
 
697
- // Export non-interactive command execution function
698
- export async function executeCommand(cmd) {
699
- const aliases = {
700
- '/h': '/help',
701
- '/v': '/version',
702
- '/q': '/exit',
703
- '/quit': '/exit',
704
- '/i': '/init',
705
- '/s': '/status'
2285
+ console.log('👋 Thanks for using AVC!\n');
2286
+ process.exit(0);
706
2287
  };
707
2288
 
708
- const command = (aliases[cmd.toLowerCase()] || cmd).toLowerCase();
709
-
710
- try {
711
- switch (command) {
712
- case '/help':
713
- console.log(`
714
- 📚 Available Commands:
715
-
716
- /init (or /i) Initialize an AVC project (Sponsor Call ceremony)
717
- /status (or /s) Show current project status
718
- /help (or /h) Show this help message
719
- /version (or /v) Show version information
720
- /exit (or /q) Exit AVC interactive mode
721
-
722
- 💡 Tips:
723
- - Run 'avc' without arguments to start interactive mode
724
- - Use Tab key to auto-complete commands
725
- - Use arrow keys (↑/↓) to navigate command history
726
- `);
727
- process.exit(0);
728
-
729
- case '/version':
730
- const version = getVersion();
731
- console.log(`
732
- 🎯 AVC Framework v${version}
733
- Agile Vibe Coding - AI-powered development framework
734
- https://agilevibecoding.org
735
- `);
736
- process.exit(0);
737
-
738
- case '/exit':
739
- console.log('\n👋 Thanks for using AVC!\n');
740
- process.exit(0);
2289
+ // Handle Ctrl+C (SIGINT)
2290
+ process.on('SIGINT', () => {
2291
+ cleanupAndExit('SIGINT');
2292
+ });
2293
+
2294
+ // Handle kill signal (SIGTERM)
2295
+ process.on('SIGTERM', () => {
2296
+ cleanupAndExit('SIGTERM');
2297
+ });
2298
+
2299
+ // Handle uncaught exceptions
2300
+ process.on('uncaughtException', (error) => {
2301
+ console.error('\n\n❌ Uncaught exception:', error.message);
2302
+ const manager = getProcessManager();
2303
+ const running = manager.getRunningProcesses();
2304
+
2305
+ if (running.length > 0) {
2306
+ console.log('\n🛑 Stopping background processes...');
2307
+ manager.stopAll();
2308
+ }
741
2309
 
742
- case '/init':
743
- const initiator = new ProjectInitiator();
744
- await initiator.init();
745
- process.exit(0);
2310
+ process.exit(1);
2311
+ });
746
2312
 
747
- case '/status':
748
- const statusInitiator = new ProjectInitiator();
749
- statusInitiator.status();
750
- process.exit(0);
2313
+ // Handle unhandled promise rejections
2314
+ process.on('unhandledRejection', (reason, promise) => {
2315
+ console.error('\n\n❌ Unhandled promise rejection:', reason);
2316
+ const manager = getProcessManager();
2317
+ const running = manager.getRunningProcesses();
751
2318
 
752
- default:
753
- if (command.startsWith('/')) {
754
- console.error(`\n❌ Unknown command: ${command}\n Type 'avc help' to see available commands\n`);
755
- } else {
756
- console.error(`\n💡 Commands must start with /\n Example: avc init, avc status, avc help\n`);
757
- }
758
- process.exit(1);
2319
+ if (running.length > 0) {
2320
+ console.log('\n🛑 Stopping background processes...');
2321
+ manager.stopAll();
759
2322
  }
760
- } catch (error) {
761
- console.error(`\n❌ Error: ${error.message}\n`);
2323
+
762
2324
  process.exit(1);
763
- }
2325
+ });
2326
+
2327
+ console.clear();
2328
+ render(React.createElement(App));
764
2329
  }