@agile-vibe-coding/avc 0.1.0
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/LICENSE +21 -0
- package/README.md +3 -0
- package/cli/index.js +28 -0
- package/cli/init.js +321 -0
- package/cli/logger.js +138 -0
- package/cli/repl-ink.js +764 -0
- package/cli/repl-old.js +353 -0
- package/cli/template-processor.js +491 -0
- package/cli/templates/project.md +62 -0
- package/cli/update-checker.js +218 -0
- package/cli/update-installer.js +184 -0
- package/cli/update-notifier.js +170 -0
- package/package.json +58 -0
package/cli/repl-ink.js
ADDED
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { render, Box, Text, useInput, useApp } from 'ink';
|
|
3
|
+
import SelectInput from 'ink-select-input';
|
|
4
|
+
import Spinner from 'ink-spinner';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { ProjectInitiator } from './init.js';
|
|
10
|
+
import { UpdateChecker } from './update-checker.js';
|
|
11
|
+
import { UpdateInstaller } from './update-installer.js';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
// Get version from package.json (cached once at startup)
|
|
17
|
+
let _cachedVersion = null;
|
|
18
|
+
function getVersion() {
|
|
19
|
+
if (_cachedVersion) return _cachedVersion;
|
|
20
|
+
try {
|
|
21
|
+
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
|
22
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
23
|
+
_cachedVersion = packageJson.version;
|
|
24
|
+
} catch {
|
|
25
|
+
_cachedVersion = 'unknown';
|
|
26
|
+
}
|
|
27
|
+
return _cachedVersion;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ASCII art letter definitions (4 chars wide, 6 rows tall; I is 3 wide)
|
|
31
|
+
const LOGO_LETTERS = {
|
|
32
|
+
'A': [' ██ ', '█ █', '█ █', '████', '█ █', '█ █'],
|
|
33
|
+
'G': [' ██ ', '█ ', '█ ', '█ ██', '█ █', ' ██ '],
|
|
34
|
+
'I': ['███', ' █ ', ' █ ', ' █ ', ' █ ', '███'],
|
|
35
|
+
'L': ['█ ', '█ ', '█ ', '█ ', '█ ', '████'],
|
|
36
|
+
'E': ['████', '█ ', '███ ', '█ ', '█ ', '████'],
|
|
37
|
+
'V': ['█ █', '█ █', '█ █', '█ █', ' ██ ', ' ██ '],
|
|
38
|
+
'B': ['███ ', '█ █', '███ ', '█ █', '█ █', '███ '],
|
|
39
|
+
'C': [' ███', '█ ', '█ ', '█ ', '█ ', ' ███'],
|
|
40
|
+
'O': [' ██ ', '█ █', '█ █', '█ █', '█ █', ' ██ '],
|
|
41
|
+
'D': ['███ ', '█ █', '█ █', '█ █', '█ █', '███ '],
|
|
42
|
+
'N': ['█ █', '██ █', '█ ██', '█ █', '█ █', '█ █'],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Gradient colors top to bottom
|
|
46
|
+
const LOGO_COLORS = ['#04e762', '#f5b700', '#dc0073', '#008bf8', '#dc0073', '#04e762'];
|
|
47
|
+
|
|
48
|
+
function renderLogo(text) {
|
|
49
|
+
const HEIGHT = 6;
|
|
50
|
+
const words = text.split(' ');
|
|
51
|
+
const lines = [];
|
|
52
|
+
|
|
53
|
+
for (let row = 0; row < HEIGHT; row++) {
|
|
54
|
+
const wordParts = words.map(word => {
|
|
55
|
+
return [...word].map(ch => {
|
|
56
|
+
const letter = LOGO_LETTERS[ch.toUpperCase()];
|
|
57
|
+
return letter ? letter[row] : ' ';
|
|
58
|
+
}).join(' ');
|
|
59
|
+
});
|
|
60
|
+
lines.push(wordParts.join(' '));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return lines;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Banner component with ASCII art logo
|
|
67
|
+
const Banner = () => {
|
|
68
|
+
const version = getVersion();
|
|
69
|
+
const logoLines = renderLogo('AGILE VIBE CODING');
|
|
70
|
+
|
|
71
|
+
return React.createElement(Box, { flexDirection: 'column', marginBottom: 1 },
|
|
72
|
+
React.createElement(Text, null, ' '),
|
|
73
|
+
...logoLines.map((line, i) =>
|
|
74
|
+
React.createElement(Text, { bold: true, color: LOGO_COLORS[i] }, ' ' + line)
|
|
75
|
+
),
|
|
76
|
+
React.createElement(Text, null, ' '),
|
|
77
|
+
React.createElement(Text, null, ` v${version} │ AI-powered Agile development framework`),
|
|
78
|
+
React.createElement(Text, null, ' '),
|
|
79
|
+
React.createElement(Text, { bold: true, color: 'red' }, ' ⚠️ UNDER DEVELOPMENT - DO NOT USE ⚠️'),
|
|
80
|
+
React.createElement(Text, null, ' '),
|
|
81
|
+
React.createElement(Text, { dimColor: true }, ' Type / to see commands')
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Separator line component
|
|
86
|
+
const Separator = () => {
|
|
87
|
+
const [width, setWidth] = useState(process.stdout.columns || 80);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
const handleResize = () => {
|
|
91
|
+
setWidth(process.stdout.columns || 80);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
process.stdout.on('resize', handleResize);
|
|
95
|
+
return () => process.stdout.off('resize', handleResize);
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
return React.createElement(Text, null, '─'.repeat(width));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Loading spinner component
|
|
102
|
+
const LoadingSpinner = ({ message }) => {
|
|
103
|
+
return React.createElement(Box, null,
|
|
104
|
+
React.createElement(Text, { color: 'green' },
|
|
105
|
+
React.createElement(Spinner, { type: 'dots' }),
|
|
106
|
+
' ',
|
|
107
|
+
message
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Command selector component with number shortcuts
|
|
113
|
+
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' }
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
// Filter commands if filter is provided
|
|
123
|
+
const commands = filter
|
|
124
|
+
? allCommands.filter(c => c.value.startsWith(filter.toLowerCase()))
|
|
125
|
+
: allCommands;
|
|
126
|
+
|
|
127
|
+
// Add number prefix to labels
|
|
128
|
+
const commandsWithNumbers = commands.map((cmd, idx) => ({
|
|
129
|
+
...cmd,
|
|
130
|
+
label: `[${idx + 1}] ${cmd.label}`
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
useInput((input, key) => {
|
|
134
|
+
if (key.escape) {
|
|
135
|
+
onCancel();
|
|
136
|
+
}
|
|
137
|
+
// Number shortcuts
|
|
138
|
+
const num = parseInt(input);
|
|
139
|
+
if (num >= 1 && num <= commands.length) {
|
|
140
|
+
onSelect(commands[num - 1]);
|
|
141
|
+
}
|
|
142
|
+
}, { isActive: true });
|
|
143
|
+
|
|
144
|
+
if (commands.length === 0) {
|
|
145
|
+
return React.createElement(Box, { flexDirection: 'column' },
|
|
146
|
+
React.createElement(Text, { color: 'yellow' }, 'No matching commands'),
|
|
147
|
+
React.createElement(Text, { dimColor: true }, '(Press Esc to cancel)')
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
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)')
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Command history display
|
|
158
|
+
const HistoryHint = ({ hasHistory }) => {
|
|
159
|
+
if (!hasHistory) return null;
|
|
160
|
+
|
|
161
|
+
return React.createElement(Text, { dimColor: true, italic: true },
|
|
162
|
+
'(↑/↓ for history)'
|
|
163
|
+
);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Input display with cursor
|
|
167
|
+
const InputWithCursor = ({ input }) => {
|
|
168
|
+
return React.createElement(Box, null,
|
|
169
|
+
React.createElement(Text, null, '> '),
|
|
170
|
+
React.createElement(Text, null, input),
|
|
171
|
+
React.createElement(Text, { inverse: true }, ' ')
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Bottom-right status display (version + update info)
|
|
176
|
+
const BottomRightStatus = () => {
|
|
177
|
+
const version = getVersion();
|
|
178
|
+
const [updateState, setUpdateState] = useState(null);
|
|
179
|
+
const [width, setWidth] = useState(process.stdout.columns || 80);
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
const handleResize = () => {
|
|
183
|
+
setWidth(process.stdout.columns || 80);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
process.stdout.on('resize', handleResize);
|
|
187
|
+
return () => process.stdout.off('resize', handleResize);
|
|
188
|
+
}, []);
|
|
189
|
+
|
|
190
|
+
// Poll update state every 2 seconds
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
const checker = new UpdateChecker();
|
|
193
|
+
|
|
194
|
+
const updateStatus = () => {
|
|
195
|
+
const state = checker.readState();
|
|
196
|
+
setUpdateState(state);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
updateStatus(); // Initial read
|
|
200
|
+
const interval = setInterval(updateStatus, 2000);
|
|
201
|
+
|
|
202
|
+
return () => clearInterval(interval);
|
|
203
|
+
}, []);
|
|
204
|
+
|
|
205
|
+
// Build status text
|
|
206
|
+
let statusText = `v${version}`;
|
|
207
|
+
let statusColor = 'gray';
|
|
208
|
+
|
|
209
|
+
if (updateState && updateState.updateAvailable && !updateState.userDismissed) {
|
|
210
|
+
if (updateState.updateReady) {
|
|
211
|
+
statusText = `v${version} → v${updateState.downloadedVersion} ready! (Ctrl+R)`;
|
|
212
|
+
statusColor = 'green';
|
|
213
|
+
} else if (updateState.updateStatus === 'downloading') {
|
|
214
|
+
statusText = `v${version} → v${updateState.latestVersion} downloading...`;
|
|
215
|
+
statusColor = 'blue';
|
|
216
|
+
} else if (updateState.updateStatus === 'failed') {
|
|
217
|
+
statusText = `v${version} | Update failed`;
|
|
218
|
+
statusColor = 'red';
|
|
219
|
+
} else if (updateState.updateStatus === 'pending' || updateState.updateStatus === 'idle') {
|
|
220
|
+
statusText = `v${version} → v${updateState.latestVersion} available`;
|
|
221
|
+
statusColor = 'yellow';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Calculate padding to align to the right
|
|
226
|
+
const padding = Math.max(0, width - statusText.length - 2);
|
|
227
|
+
|
|
228
|
+
return React.createElement(Box, { justifyContent: 'flex-end' },
|
|
229
|
+
React.createElement(Text, { dimColor: statusColor === 'gray', color: statusColor === 'gray' ? undefined : statusColor },
|
|
230
|
+
' '.repeat(padding),
|
|
231
|
+
statusText
|
|
232
|
+
)
|
|
233
|
+
);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Main App component
|
|
237
|
+
const App = () => {
|
|
238
|
+
const { exit } = useApp();
|
|
239
|
+
const [mode, setMode] = useState('prompt'); // 'prompt' | 'selector' | 'executing'
|
|
240
|
+
const [input, setInput] = useState('');
|
|
241
|
+
const [output, setOutput] = useState('');
|
|
242
|
+
const [commandHistory, setCommandHistory] = useState([]);
|
|
243
|
+
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
244
|
+
const [isExecuting, setIsExecuting] = useState(false);
|
|
245
|
+
const [executingMessage, setExecutingMessage] = useState('');
|
|
246
|
+
|
|
247
|
+
// Start update checker on mount
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
const checker = new UpdateChecker();
|
|
250
|
+
const installer = new UpdateInstaller();
|
|
251
|
+
|
|
252
|
+
// Perform first update check immediately
|
|
253
|
+
checker.checkForUpdates().catch(() => {
|
|
254
|
+
// Silently fail
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Start background checker (checks every hour after first check)
|
|
258
|
+
checker.startBackgroundChecker();
|
|
259
|
+
|
|
260
|
+
// Auto-trigger update installation if available
|
|
261
|
+
setTimeout(() => {
|
|
262
|
+
installer.autoTriggerUpdate().catch(() => {
|
|
263
|
+
// Silently fail
|
|
264
|
+
});
|
|
265
|
+
}, 5000); // Wait 5 seconds after startup to avoid blocking
|
|
266
|
+
}, []);
|
|
267
|
+
|
|
268
|
+
// Available commands for Tab completion
|
|
269
|
+
const allCommands = [
|
|
270
|
+
'/init',
|
|
271
|
+
'/status',
|
|
272
|
+
'/help',
|
|
273
|
+
'/version',
|
|
274
|
+
'/exit'
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
// Handle Tab key autocomplete
|
|
278
|
+
const handleTabComplete = () => {
|
|
279
|
+
// Only autocomplete if input starts with "/"
|
|
280
|
+
if (!input.startsWith('/')) return;
|
|
281
|
+
|
|
282
|
+
// Filter commands that match the current input
|
|
283
|
+
const matches = allCommands.filter(cmd =>
|
|
284
|
+
cmd.toLowerCase().startsWith(input.toLowerCase())
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// If exactly one match, complete to that command
|
|
288
|
+
if (matches.length === 1) {
|
|
289
|
+
setInput(matches[0]);
|
|
290
|
+
// Show selector if not already shown
|
|
291
|
+
if (mode !== 'selector') {
|
|
292
|
+
setOutput('');
|
|
293
|
+
setMode('selector');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// If multiple matches, complete to common prefix
|
|
297
|
+
else if (matches.length > 1) {
|
|
298
|
+
// Find common prefix
|
|
299
|
+
let commonPrefix = matches[0];
|
|
300
|
+
for (let i = 1; i < matches.length; i++) {
|
|
301
|
+
let j = 0;
|
|
302
|
+
while (j < commonPrefix.length && j < matches[i].length &&
|
|
303
|
+
commonPrefix[j].toLowerCase() === matches[i][j].toLowerCase()) {
|
|
304
|
+
j++;
|
|
305
|
+
}
|
|
306
|
+
commonPrefix = commonPrefix.substring(0, j);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// If common prefix is longer than current input, use it
|
|
310
|
+
if (commonPrefix.length > input.length) {
|
|
311
|
+
setInput(commonPrefix);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Show selector if not already shown
|
|
315
|
+
if (mode !== 'selector') {
|
|
316
|
+
setOutput('');
|
|
317
|
+
setMode('selector');
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Command aliases
|
|
323
|
+
const resolveAlias = (cmd) => {
|
|
324
|
+
const aliases = {
|
|
325
|
+
'/h': '/help',
|
|
326
|
+
'/v': '/version',
|
|
327
|
+
'/q': '/exit',
|
|
328
|
+
'/quit': '/exit',
|
|
329
|
+
'/i': '/init',
|
|
330
|
+
'/s': '/status'
|
|
331
|
+
};
|
|
332
|
+
return aliases[cmd.toLowerCase()] || cmd;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// Handle command execution
|
|
336
|
+
const executeCommand = async (cmd) => {
|
|
337
|
+
const command = resolveAlias(cmd.trim());
|
|
338
|
+
|
|
339
|
+
if (!command) {
|
|
340
|
+
setMode('prompt');
|
|
341
|
+
setInput('');
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
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);
|
|
350
|
+
|
|
351
|
+
setMode('executing');
|
|
352
|
+
setIsExecuting(true);
|
|
353
|
+
setOutput(''); // Clear previous output
|
|
354
|
+
|
|
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;
|
|
375
|
+
|
|
376
|
+
case '/init':
|
|
377
|
+
setExecutingMessage('Initializing project...');
|
|
378
|
+
await runInit();
|
|
379
|
+
break;
|
|
380
|
+
|
|
381
|
+
case '/status':
|
|
382
|
+
setExecutingMessage('Checking project status...');
|
|
383
|
+
await runStatus();
|
|
384
|
+
break;
|
|
385
|
+
|
|
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;
|
|
397
|
+
|
|
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`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
} catch (error) {
|
|
406
|
+
setOutput(`\n❌ Error: ${error.message}\n`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Return to prompt mode
|
|
410
|
+
setIsExecuting(false);
|
|
411
|
+
setTimeout(() => {
|
|
412
|
+
setMode('prompt');
|
|
413
|
+
setInput('');
|
|
414
|
+
}, 100);
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const showHelp = () => {
|
|
418
|
+
return `
|
|
419
|
+
📚 Available Commands:
|
|
420
|
+
|
|
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
|
|
427
|
+
|
|
428
|
+
💡 Tips:
|
|
429
|
+
- Type / and press Enter to see interactive command selector
|
|
430
|
+
- Use arrow keys (↑/↓) to navigate command history
|
|
431
|
+
- Use Tab key to auto-complete commands
|
|
432
|
+
- Use number keys (1-5) to quickly select commands from the menu
|
|
433
|
+
- Press Esc to cancel command selector or dismiss notifications
|
|
434
|
+
- Press Ctrl+R to restart after updates
|
|
435
|
+
`;
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const showVersion = () => {
|
|
439
|
+
const version = getVersion();
|
|
440
|
+
return `
|
|
441
|
+
🎯 AVC Framework v${version}
|
|
442
|
+
Agile Vibe Coding - AI-powered development framework
|
|
443
|
+
https://agilevibecoding.org
|
|
444
|
+
`;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const runInit = async () => {
|
|
448
|
+
setOutput('\n'); // Empty line before init output
|
|
449
|
+
const initiator = new ProjectInitiator();
|
|
450
|
+
|
|
451
|
+
// Capture console.log output
|
|
452
|
+
const originalLog = console.log;
|
|
453
|
+
let logs = [];
|
|
454
|
+
console.log = (...args) => {
|
|
455
|
+
logs.push(args.join(' '));
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
await initiator.init();
|
|
460
|
+
} finally {
|
|
461
|
+
console.log = originalLog;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
setOutput(logs.join('\n') + '\n');
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const runStatus = async () => {
|
|
468
|
+
setOutput('\n'); // Empty line before status output
|
|
469
|
+
const initiator = new ProjectInitiator();
|
|
470
|
+
|
|
471
|
+
// Capture console.log output
|
|
472
|
+
const originalLog = console.log;
|
|
473
|
+
let logs = [];
|
|
474
|
+
console.log = (...args) => {
|
|
475
|
+
logs.push(args.join(' '));
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
initiator.status();
|
|
480
|
+
} finally {
|
|
481
|
+
console.log = originalLog;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
setOutput(logs.join('\n') + '\n');
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// Handle keyboard input in prompt mode
|
|
488
|
+
useInput((inputChar, key) => {
|
|
489
|
+
if (mode !== 'prompt') return;
|
|
490
|
+
|
|
491
|
+
// Handle Ctrl+R for restart (re-exec current process)
|
|
492
|
+
if (key.ctrl && inputChar === 'r') {
|
|
493
|
+
setOutput('\n🔄 Restarting AVC...\n');
|
|
494
|
+
setTimeout(() => {
|
|
495
|
+
exit();
|
|
496
|
+
try {
|
|
497
|
+
execSync(process.argv.join(' '), { stdio: 'inherit' });
|
|
498
|
+
} catch { }
|
|
499
|
+
process.exit(0);
|
|
500
|
+
}, 500);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Handle Esc to dismiss update notification
|
|
505
|
+
if (key.escape) {
|
|
506
|
+
const checker = new UpdateChecker();
|
|
507
|
+
const state = checker.readState();
|
|
508
|
+
if (state.updateAvailable && !state.userDismissed) {
|
|
509
|
+
state.userDismissed = true;
|
|
510
|
+
state.dismissedAt = new Date().toISOString();
|
|
511
|
+
checker.writeState(state);
|
|
512
|
+
}
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Handle up/down arrows for history
|
|
517
|
+
if (key.upArrow && commandHistory.length > 0) {
|
|
518
|
+
const newIndex = historyIndex === -1
|
|
519
|
+
? commandHistory.length - 1
|
|
520
|
+
: Math.max(0, historyIndex - 1);
|
|
521
|
+
setHistoryIndex(newIndex);
|
|
522
|
+
setInput(commandHistory[newIndex]);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (key.downArrow && historyIndex !== -1) {
|
|
527
|
+
const newIndex = historyIndex + 1;
|
|
528
|
+
if (newIndex >= commandHistory.length) {
|
|
529
|
+
setHistoryIndex(-1);
|
|
530
|
+
setInput('');
|
|
531
|
+
} else {
|
|
532
|
+
setHistoryIndex(newIndex);
|
|
533
|
+
setInput(commandHistory[newIndex]);
|
|
534
|
+
}
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Handle Tab key for autocomplete
|
|
539
|
+
if (key.tab) {
|
|
540
|
+
handleTabComplete();
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Handle Enter key
|
|
545
|
+
if (key.return) {
|
|
546
|
+
if (input === '/' || input.startsWith('/')) {
|
|
547
|
+
// If just "/" or partial command, show/stay in selector
|
|
548
|
+
if (input === '/') {
|
|
549
|
+
setOutput(''); // Clear previous output
|
|
550
|
+
setMode('selector');
|
|
551
|
+
setInput(''); // Clear input when entering selector
|
|
552
|
+
} else {
|
|
553
|
+
// Execute the typed command or selected command
|
|
554
|
+
executeCommand(input);
|
|
555
|
+
}
|
|
556
|
+
} else {
|
|
557
|
+
// Execute command
|
|
558
|
+
executeCommand(input);
|
|
559
|
+
}
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Handle backspace
|
|
564
|
+
if (key.backspace || key.delete) {
|
|
565
|
+
const newInput = input.slice(0, -1);
|
|
566
|
+
setInput(newInput);
|
|
567
|
+
setHistoryIndex(-1);
|
|
568
|
+
|
|
569
|
+
// If we're in selector mode and user deletes the "/", exit selector
|
|
570
|
+
if (mode === 'selector' && !newInput.startsWith('/')) {
|
|
571
|
+
setMode('prompt');
|
|
572
|
+
}
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Handle character input
|
|
577
|
+
if (inputChar) {
|
|
578
|
+
const newInput = input + inputChar;
|
|
579
|
+
setInput(newInput);
|
|
580
|
+
setHistoryIndex(-1);
|
|
581
|
+
|
|
582
|
+
// Show selector immediately when "/" is typed
|
|
583
|
+
if (newInput === '/' || (newInput.startsWith('/') && newInput.length > 1)) {
|
|
584
|
+
setOutput(''); // Clear previous output
|
|
585
|
+
setMode('selector');
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}, { isActive: mode === 'prompt' });
|
|
589
|
+
|
|
590
|
+
// Handle keyboard input in selector mode
|
|
591
|
+
useInput((inputChar, key) => {
|
|
592
|
+
if (mode !== 'selector') return;
|
|
593
|
+
|
|
594
|
+
// Handle Tab key for autocomplete
|
|
595
|
+
if (key.tab) {
|
|
596
|
+
handleTabComplete();
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Handle backspace
|
|
601
|
+
if (key.backspace || key.delete) {
|
|
602
|
+
const newInput = input.slice(0, -1);
|
|
603
|
+
setInput(newInput);
|
|
604
|
+
|
|
605
|
+
// If user deletes the "/", exit selector
|
|
606
|
+
if (!newInput.startsWith('/')) {
|
|
607
|
+
setMode('prompt');
|
|
608
|
+
}
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Handle character input (typing continues to filter)
|
|
613
|
+
if (inputChar && !key.ctrl && !key.meta) {
|
|
614
|
+
const newInput = input + inputChar;
|
|
615
|
+
setInput(newInput);
|
|
616
|
+
}
|
|
617
|
+
}, { isActive: mode === 'selector' });
|
|
618
|
+
|
|
619
|
+
// Render output - show spinner during execution, or output after completion
|
|
620
|
+
const renderOutput = () => {
|
|
621
|
+
// Show spinner while executing
|
|
622
|
+
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
|
+
)
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Show output if available (even after returning to prompt mode)
|
|
632
|
+
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
|
+
)
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return null;
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
// Render selector when in selector mode
|
|
645
|
+
const renderSelector = () => {
|
|
646
|
+
if (mode !== 'selector') return null;
|
|
647
|
+
|
|
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
|
+
),
|
|
654
|
+
React.createElement(CommandSelector, {
|
|
655
|
+
filter: input,
|
|
656
|
+
onSelect: (item) => {
|
|
657
|
+
executeCommand(item.value);
|
|
658
|
+
},
|
|
659
|
+
onCancel: () => {
|
|
660
|
+
setMode('prompt');
|
|
661
|
+
setInput('');
|
|
662
|
+
}
|
|
663
|
+
})
|
|
664
|
+
)
|
|
665
|
+
);
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
// Render prompt when in prompt mode
|
|
669
|
+
const renderPrompt = () => {
|
|
670
|
+
if (mode !== 'prompt') return null;
|
|
671
|
+
|
|
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)
|
|
679
|
+
);
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
return React.createElement(Box, { flexDirection: 'column' },
|
|
683
|
+
React.createElement(Banner),
|
|
684
|
+
renderOutput(),
|
|
685
|
+
renderSelector(),
|
|
686
|
+
renderPrompt(),
|
|
687
|
+
React.createElement(BottomRightStatus)
|
|
688
|
+
);
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
// Export render function
|
|
692
|
+
export function startRepl() {
|
|
693
|
+
console.clear();
|
|
694
|
+
render(React.createElement(App));
|
|
695
|
+
}
|
|
696
|
+
|
|
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'
|
|
706
|
+
};
|
|
707
|
+
|
|
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);
|
|
741
|
+
|
|
742
|
+
case '/init':
|
|
743
|
+
const initiator = new ProjectInitiator();
|
|
744
|
+
await initiator.init();
|
|
745
|
+
process.exit(0);
|
|
746
|
+
|
|
747
|
+
case '/status':
|
|
748
|
+
const statusInitiator = new ProjectInitiator();
|
|
749
|
+
statusInitiator.status();
|
|
750
|
+
process.exit(0);
|
|
751
|
+
|
|
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);
|
|
759
|
+
}
|
|
760
|
+
} catch (error) {
|
|
761
|
+
console.error(`\n❌ Error: ${error.message}\n`);
|
|
762
|
+
process.exit(1);
|
|
763
|
+
}
|
|
764
|
+
}
|