@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/README.md +2 -0
- package/cli/agents/documentation.md +302 -0
- package/cli/build-docs.js +277 -0
- package/cli/command-logger.js +208 -0
- package/cli/index.js +3 -25
- package/cli/init.js +705 -77
- package/cli/llm-claude.js +27 -0
- package/cli/llm-gemini.js +30 -0
- package/cli/llm-provider.js +63 -0
- package/cli/logger.js +32 -5
- package/cli/process-manager.js +261 -0
- package/cli/repl-ink.js +1784 -219
- package/cli/template-processor.js +274 -73
- package/cli/templates/vitepress-config.mts.template +33 -0
- package/package.json +17 -3
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
|
|
82
|
+
const agileLines = renderLogo('AGILE');
|
|
83
|
+
const vibeLines = renderLogo('VIBE CODING');
|
|
70
84
|
|
|
71
|
-
return React.createElement(Box, { flexDirection: 'column'
|
|
85
|
+
return React.createElement(Box, { flexDirection: 'column' },
|
|
72
86
|
React.createElement(Text, null, ' '),
|
|
73
|
-
...
|
|
74
|
-
|
|
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, `
|
|
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' }, '
|
|
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 }, '
|
|
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
|
-
//
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
{
|
|
119
|
-
|
|
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
|
-
//
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
//
|
|
176
|
-
const
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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 () =>
|
|
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
|
-
//
|
|
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
|
-
'/
|
|
272
|
-
'/
|
|
273
|
-
'/
|
|
274
|
-
'/
|
|
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
|
-
|
|
1048
|
+
try {
|
|
1049
|
+
const command = resolveAlias(cmd.trim());
|
|
338
1050
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
1051
|
+
if (!command) {
|
|
1052
|
+
setMode('prompt');
|
|
1053
|
+
setInput('');
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
344
1056
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
setOutput(''); // Clear previous output
|
|
1063
|
+
setMode('executing');
|
|
1064
|
+
setIsExecuting(true);
|
|
354
1065
|
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
break;
|
|
1069
|
+
// Create command logger
|
|
1070
|
+
const commandName = command.replace('/', '').toLowerCase();
|
|
1071
|
+
let logger = null;
|
|
380
1072
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
break;
|
|
1073
|
+
// Check if .avc folder exists
|
|
1074
|
+
const avcPath = path.join(process.cwd(), '.avc');
|
|
1075
|
+
const avcExists = existsSync(avcPath);
|
|
385
1076
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
setOutput(
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
}
|
|
1218
|
+
}
|
|
415
1219
|
};
|
|
416
1220
|
|
|
417
1221
|
const showHelp = () => {
|
|
418
1222
|
return `
|
|
419
1223
|
📚 Available Commands:
|
|
420
1224
|
|
|
421
|
-
/init (or /i)
|
|
422
|
-
/
|
|
423
|
-
/
|
|
424
|
-
/
|
|
425
|
-
/
|
|
426
|
-
/
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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(
|
|
624
|
-
React.createElement(
|
|
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(
|
|
634
|
-
React.createElement(
|
|
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(
|
|
649
|
-
React.createElement(
|
|
650
|
-
React.createElement(Box, {
|
|
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
|
-
|
|
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(
|
|
673
|
-
React.createElement(
|
|
674
|
-
React.createElement(
|
|
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
|
-
|
|
694
|
-
|
|
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
|
-
|
|
698
|
-
|
|
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
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
await initiator.init();
|
|
745
|
-
process.exit(0);
|
|
2310
|
+
process.exit(1);
|
|
2311
|
+
});
|
|
746
2312
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
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
|
}
|