viepilot 1.0.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/LICENSE +23 -0
  3. package/README.md +550 -0
  4. package/bin/viepilot.cjs +222 -0
  5. package/bin/vp-tools.cjs +912 -0
  6. package/dev-install.sh +109 -0
  7. package/docs/README.md +125 -0
  8. package/docs/advanced-usage.md +366 -0
  9. package/docs/api/README.md +12 -0
  10. package/docs/api/graphql-schema.md +5 -0
  11. package/docs/api/kafka-events.md +5 -0
  12. package/docs/api/rest-api.md +19 -0
  13. package/docs/api/websocket-api.md +5 -0
  14. package/docs/dev/architecture.md +226 -0
  15. package/docs/dev/cli-reference.md +324 -0
  16. package/docs/dev/contributing.md +195 -0
  17. package/docs/dev/deployment.md +204 -0
  18. package/docs/dev/getting-started.md +16 -0
  19. package/docs/dev/testing.md +171 -0
  20. package/docs/dev/ui-components-library.md +36 -0
  21. package/docs/getting-started.md +163 -0
  22. package/docs/skills-reference.md +399 -0
  23. package/docs/troubleshooting.md +297 -0
  24. package/docs/user/faq.md +117 -0
  25. package/docs/user/features/autonomous-mode.md +111 -0
  26. package/docs/user/features/checkpoint-recovery.md +76 -0
  27. package/docs/user/features/debug-mode.md +77 -0
  28. package/docs/user/features/ui-direction.md +29 -0
  29. package/docs/user/quick-start.md +157 -0
  30. package/docs/videos/01-installation.md +113 -0
  31. package/docs/videos/02-first-project.md +132 -0
  32. package/docs/videos/03-autonomous-mode.md +147 -0
  33. package/install.sh +144 -0
  34. package/lib/cli-shared.cjs +108 -0
  35. package/package.json +78 -0
  36. package/skills/vp-audit/SKILL.md +140 -0
  37. package/skills/vp-auto/SKILL.md +204 -0
  38. package/skills/vp-brainstorm/SKILL.md +75 -0
  39. package/skills/vp-crystallize/SKILL.md +175 -0
  40. package/skills/vp-debug/SKILL.md +96 -0
  41. package/skills/vp-docs/SKILL.md +258 -0
  42. package/skills/vp-evolve/SKILL.md +165 -0
  43. package/skills/vp-pause/SKILL.md +150 -0
  44. package/skills/vp-request/SKILL.md +250 -0
  45. package/skills/vp-resume/SKILL.md +141 -0
  46. package/skills/vp-rollback/SKILL.md +116 -0
  47. package/skills/vp-status/SKILL.md +137 -0
  48. package/skills/vp-task/SKILL.md +139 -0
  49. package/skills/vp-ui-components/SKILL.md +64 -0
  50. package/templates/phase/PHASE-STATE.md +35 -0
  51. package/templates/phase/SPEC.md +40 -0
  52. package/templates/phase/SUMMARY.md +67 -0
  53. package/templates/phase/TASK.md +101 -0
  54. package/templates/phase/VERIFICATION.md +49 -0
  55. package/templates/project/AI-GUIDE.md +114 -0
  56. package/templates/project/ARCHITECTURE.md +70 -0
  57. package/templates/project/CHANGELOG.md +36 -0
  58. package/templates/project/CONTRIBUTING.md +154 -0
  59. package/templates/project/CONTRIBUTORS.md +41 -0
  60. package/templates/project/PROJECT-CONTEXT.md +74 -0
  61. package/templates/project/PROJECT-META.md +133 -0
  62. package/templates/project/README.md +197 -0
  63. package/templates/project/ROADMAP.md +56 -0
  64. package/templates/project/SYSTEM-RULES.md +368 -0
  65. package/templates/project/TRACKER.md +50 -0
  66. package/ui-components/INDEX.md +9 -0
  67. package/ui-components/base/button/README.md +8 -0
  68. package/ui-components/base/button/metadata.json +8 -0
  69. package/ui-components/base/card/README.md +8 -0
  70. package/ui-components/base/card/metadata.json +8 -0
  71. package/ui-components/base/input/README.md +8 -0
  72. package/ui-components/base/input/metadata.json +8 -0
  73. package/workflows/audit.md +549 -0
  74. package/workflows/autonomous.md +425 -0
  75. package/workflows/brainstorm.md +257 -0
  76. package/workflows/crystallize.md +418 -0
  77. package/workflows/debug.md +241 -0
  78. package/workflows/documentation.md +587 -0
  79. package/workflows/evolve.md +258 -0
  80. package/workflows/pause-work.md +255 -0
  81. package/workflows/request.md +534 -0
  82. package/workflows/resume-work.md +226 -0
  83. package/workflows/rollback.md +202 -0
  84. package/workflows/ui-components.md +109 -0
@@ -0,0 +1,912 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ViePilot CLI Tools
5
+ * Helper utilities for state management and workflow operations
6
+ *
7
+ * @package viepilot
8
+ * @author Trần Thành Nhân
9
+ * @license MIT
10
+ * @version 0.1.0
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const readline = require('readline');
16
+
17
+ const {
18
+ validators,
19
+ levenshteinDistance,
20
+ findProjectRoot,
21
+ VIEPILOT_DIR,
22
+ } = require(path.join(__dirname, '../lib/cli-shared.cjs'));
23
+
24
+ // ============================================================================
25
+ // Output Formatting (TTY-aware)
26
+ // ============================================================================
27
+
28
+ const isTTY = process.stdout.isTTY;
29
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
30
+
31
+ const colors = {
32
+ reset: isTTY ? '\x1b[0m' : '',
33
+ red: isTTY ? '\x1b[31m' : '',
34
+ green: isTTY ? '\x1b[32m' : '',
35
+ yellow: isTTY ? '\x1b[33m' : '',
36
+ blue: isTTY ? '\x1b[34m' : '',
37
+ cyan: isTTY ? '\x1b[36m' : '',
38
+ gray: isTTY ? '\x1b[90m' : '',
39
+ bold: isTTY ? '\x1b[1m' : '',
40
+ };
41
+
42
+ function formatError(message, hint = null) {
43
+ let output = `${colors.red}✖ Error:${colors.reset} ${message}`;
44
+ if (hint) {
45
+ output += `\n${colors.gray} Hint: ${hint}${colors.reset}`;
46
+ }
47
+ return output;
48
+ }
49
+
50
+ function formatSuccess(message) {
51
+ return `${colors.green}✔${colors.reset} ${message}`;
52
+ }
53
+
54
+ function formatWarning(message) {
55
+ return `${colors.yellow}⚠${colors.reset} ${message}`;
56
+ }
57
+
58
+ function formatInfo(message) {
59
+ return `${colors.blue}ℹ${colors.reset} ${message}`;
60
+ }
61
+
62
+ function validateArgs(validations) {
63
+ for (const validation of validations) {
64
+ if (!validation.valid) {
65
+ console.error(formatError(validation.error, validation.hint));
66
+ process.exit(1);
67
+ }
68
+ }
69
+ }
70
+
71
+ // ============================================================================
72
+ // Interactive Prompts
73
+ // ============================================================================
74
+
75
+ async function confirm(message, defaultYes = false) {
76
+ if (!isInteractive) {
77
+ console.log(formatWarning(`Non-interactive mode, assuming ${defaultYes ? 'yes' : 'no'}`));
78
+ return defaultYes;
79
+ }
80
+
81
+ const rl = readline.createInterface({
82
+ input: process.stdin,
83
+ output: process.stdout,
84
+ });
85
+
86
+ const defaultHint = defaultYes ? '[Y/n]' : '[y/N]';
87
+
88
+ return new Promise((resolve) => {
89
+ rl.question(`${colors.yellow}?${colors.reset} ${message} ${colors.gray}${defaultHint}${colors.reset} `, (answer) => {
90
+ rl.close();
91
+ const normalized = answer.trim().toLowerCase();
92
+ if (normalized === '') {
93
+ resolve(defaultYes);
94
+ } else {
95
+ resolve(normalized === 'y' || normalized === 'yes');
96
+ }
97
+ });
98
+ });
99
+ }
100
+
101
+ async function select(message, options) {
102
+ if (!isInteractive) {
103
+ console.log(formatWarning('Non-interactive mode, using first option'));
104
+ return options[0].value;
105
+ }
106
+
107
+ const rl = readline.createInterface({
108
+ input: process.stdin,
109
+ output: process.stdout,
110
+ });
111
+
112
+ console.log(`\n${colors.cyan}?${colors.reset} ${message}\n`);
113
+ options.forEach((opt, i) => {
114
+ console.log(` ${colors.bold}${i + 1}.${colors.reset} ${opt.label}`);
115
+ });
116
+ console.log();
117
+
118
+ return new Promise((resolve) => {
119
+ rl.question(`${colors.gray}Enter choice (1-${options.length}):${colors.reset} `, (answer) => {
120
+ rl.close();
121
+ const index = parseInt(answer, 10) - 1;
122
+ if (index >= 0 && index < options.length) {
123
+ resolve(options[index].value);
124
+ } else {
125
+ console.log(formatWarning('Invalid choice, using first option'));
126
+ resolve(options[0].value);
127
+ }
128
+ });
129
+ });
130
+ }
131
+
132
+ // ============================================================================
133
+ // Utility Functions
134
+ // ============================================================================
135
+
136
+ function readJson(filePath) {
137
+ try {
138
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
139
+ } catch (e) {
140
+ return null;
141
+ }
142
+ }
143
+
144
+ function writeJson(filePath, data) {
145
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
146
+ }
147
+
148
+ function readMarkdown(filePath) {
149
+ try {
150
+ return fs.readFileSync(filePath, 'utf8');
151
+ } catch (e) {
152
+ return null;
153
+ }
154
+ }
155
+
156
+ function currentTimestamp(format = 'iso') {
157
+ const now = new Date();
158
+ if (format === 'iso') {
159
+ return now.toISOString();
160
+ } else if (format === 'date') {
161
+ return now.toISOString().split('T')[0];
162
+ } else if (format === 'full') {
163
+ return now.toISOString().replace('T', ' ').split('.')[0];
164
+ }
165
+ return now.toISOString();
166
+ }
167
+
168
+ // ============================================================================
169
+ // Commands
170
+ // ============================================================================
171
+
172
+ const commands = {
173
+ /**
174
+ * Initialize or get project state
175
+ */
176
+ init: (args) => {
177
+ const projectCheck = validators.requireProjectRoot();
178
+ validateArgs([projectCheck]);
179
+ const projectRoot = projectCheck.value;
180
+
181
+ const trackerPath = path.join(projectRoot, VIEPILOT_DIR, 'TRACKER.md');
182
+ const roadmapPath = path.join(projectRoot, VIEPILOT_DIR, 'ROADMAP.md');
183
+ const handoffPath = path.join(projectRoot, VIEPILOT_DIR, 'HANDOFF.json');
184
+
185
+ const result = {
186
+ project_root: projectRoot,
187
+ viepilot_dir: path.join(projectRoot, VIEPILOT_DIR),
188
+ tracker_exists: fs.existsSync(trackerPath),
189
+ roadmap_exists: fs.existsSync(roadmapPath),
190
+ handoff_exists: fs.existsSync(handoffPath),
191
+ };
192
+
193
+ if (result.handoff_exists) {
194
+ result.handoff = readJson(handoffPath);
195
+ }
196
+
197
+ console.log(formatSuccess('Project found'));
198
+ console.log(JSON.stringify(result, null, 2));
199
+ },
200
+
201
+ /**
202
+ * Get current timestamp
203
+ */
204
+ 'current-timestamp': (args) => {
205
+ const format = args[0] || 'iso';
206
+ const raw = args.includes('--raw');
207
+
208
+ if (args[0] && !args.includes('--raw')) {
209
+ const formatCheck = validators.isValidTimestampFormat(format);
210
+ validateArgs([formatCheck]);
211
+ }
212
+
213
+ const ts = currentTimestamp(format);
214
+ if (raw) {
215
+ console.log(ts);
216
+ } else {
217
+ console.log(JSON.stringify({ timestamp: ts }));
218
+ }
219
+ },
220
+
221
+ /**
222
+ * Get phase information
223
+ */
224
+ 'phase-info': (args) => {
225
+ const phaseNum = args[0];
226
+
227
+ // Validate phase number
228
+ const phaseCheck = validators.isPositiveInteger(phaseNum, 'Phase number');
229
+ const projectCheck = validators.requireProjectRoot();
230
+ validateArgs([
231
+ phaseNum ? phaseCheck : { valid: false, error: 'Phase number is required', hint: 'Usage: vp-tools phase-info <phase_number>' },
232
+ projectCheck
233
+ ]);
234
+
235
+ const projectRoot = projectCheck.value;
236
+ const phasesDir = path.join(projectRoot, VIEPILOT_DIR, 'phases');
237
+
238
+ if (!fs.existsSync(phasesDir)) {
239
+ console.error(formatError('No phases directory found', 'Run /vp-crystallize first to create phases'));
240
+ process.exit(1);
241
+ }
242
+
243
+ const phaseDirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(String(phaseCheck.value).padStart(2, '0')));
244
+
245
+ if (phaseDirs.length === 0) {
246
+ console.error(formatError(`Phase ${phaseNum} not found`, `Available phases in ${phasesDir}`));
247
+ process.exit(1);
248
+ }
249
+
250
+ const phaseDir = path.join(phasesDir, phaseDirs[0]);
251
+ const specPath = path.join(phaseDir, 'SPEC.md');
252
+ const statePath = path.join(phaseDir, 'PHASE-STATE.md');
253
+ const tasksDir = path.join(phaseDir, 'tasks');
254
+
255
+ const result = {
256
+ phase_number: phaseCheck.value,
257
+ phase_dir: phaseDir,
258
+ phase_slug: phaseDirs[0],
259
+ has_spec: fs.existsSync(specPath),
260
+ has_state: fs.existsSync(statePath),
261
+ tasks: [],
262
+ };
263
+
264
+ if (fs.existsSync(tasksDir)) {
265
+ result.tasks = fs.readdirSync(tasksDir)
266
+ .filter(f => f.endsWith('.md'))
267
+ .map(f => ({
268
+ file: f,
269
+ path: path.join(tasksDir, f),
270
+ }));
271
+ result.task_count = result.tasks.length;
272
+ }
273
+
274
+ console.log(formatSuccess(`Phase ${phaseCheck.value} found`));
275
+ console.log(JSON.stringify(result, null, 2));
276
+ },
277
+
278
+ /**
279
+ * Update task status
280
+ */
281
+ 'task-status': (args) => {
282
+ const phaseNum = args[0];
283
+ const taskNum = args[1];
284
+ const status = args[2];
285
+
286
+ // Validate all inputs
287
+ const phaseCheck = validators.isPositiveInteger(phaseNum, 'Phase number');
288
+ const taskCheck = validators.isPositiveInteger(taskNum, 'Task number');
289
+ const statusCheck = status ? validators.isValidStatus(status) : { valid: false, error: 'Status is required', hint: 'Valid: not_started, in_progress, done, skipped, blocked' };
290
+ const projectCheck = validators.requireProjectRoot();
291
+
292
+ validateArgs([
293
+ phaseNum ? phaseCheck : { valid: false, error: 'Phase number is required', hint: 'Usage: vp-tools task-status <phase> <task> <status>' },
294
+ taskNum ? taskCheck : { valid: false, error: 'Task number is required', hint: 'Usage: vp-tools task-status <phase> <task> <status>' },
295
+ statusCheck,
296
+ projectCheck
297
+ ]);
298
+
299
+ console.log(formatSuccess(`Task ${phaseCheck.value}.${taskCheck.value} → ${statusCheck.value}`));
300
+ console.log(JSON.stringify({
301
+ updated: true,
302
+ phase: phaseCheck.value,
303
+ task: taskCheck.value,
304
+ status: statusCheck.value,
305
+ timestamp: currentTimestamp(),
306
+ }, null, 2));
307
+ },
308
+
309
+ /**
310
+ * Create git commit with standard format
311
+ */
312
+ commit: (args) => {
313
+ const message = args[0];
314
+ const filesArg = args.indexOf('--files');
315
+ let files = [];
316
+
317
+ if (filesArg !== -1) {
318
+ files = args.slice(filesArg + 1);
319
+ }
320
+
321
+ // Validate commit message
322
+ const msgCheck = validators.isNonEmptyString(message, 'Commit message');
323
+ validateArgs([msgCheck]);
324
+
325
+ console.log(formatInfo(`Commit: ${msgCheck.value.substring(0, 50)}${msgCheck.value.length > 50 ? '...' : ''}`));
326
+ console.log(JSON.stringify({
327
+ command: 'git',
328
+ args: ['commit', '-m', msgCheck.value],
329
+ files: files,
330
+ }, null, 2));
331
+ },
332
+
333
+ /**
334
+ * Calculate progress
335
+ */
336
+ progress: (args) => {
337
+ const projectCheck = validators.requireProjectRoot();
338
+ validateArgs([projectCheck]);
339
+ const projectRoot = projectCheck.value;
340
+
341
+ const phasesDir = path.join(projectRoot, VIEPILOT_DIR, 'phases');
342
+ if (!fs.existsSync(phasesDir)) {
343
+ console.log(formatWarning('No phases found'));
344
+ console.log(JSON.stringify({ phases: [], overall: 0 }));
345
+ return;
346
+ }
347
+
348
+ const phases = fs.readdirSync(phasesDir)
349
+ .filter(d => fs.statSync(path.join(phasesDir, d)).isDirectory())
350
+ .sort()
351
+ .map(d => {
352
+ const statePath = path.join(phasesDir, d, 'PHASE-STATE.md');
353
+ const specPath = path.join(phasesDir, d, 'SPEC.md');
354
+ const tasksDir = path.join(phasesDir, d, 'tasks');
355
+
356
+ let taskCount = 0;
357
+ let completedCount = 0;
358
+
359
+ // Count tasks from SPEC.md table (more reliable)
360
+ if (fs.existsSync(specPath)) {
361
+ const content = readMarkdown(specPath);
362
+ const taskMatches = content.match(/\|\s*\d+\.\d+\s*\|/g);
363
+ taskCount = taskMatches ? taskMatches.length : 0;
364
+ }
365
+
366
+ // Fallback to tasks directory
367
+ if (taskCount === 0 && fs.existsSync(tasksDir)) {
368
+ taskCount = fs.readdirSync(tasksDir).filter(f => f.endsWith('.md')).length;
369
+ }
370
+
371
+ // Parse PHASE-STATE.md for completed tasks
372
+ if (fs.existsSync(statePath)) {
373
+ const content = readMarkdown(statePath);
374
+ const doneMatches = content.match(/✅\s*(Done|done|DONE)/gi);
375
+ completedCount = doneMatches ? doneMatches.length : 0;
376
+ }
377
+
378
+ const progress = taskCount > 0 ? Math.round((completedCount / taskCount) * 100) : 0;
379
+ const progressBar = createProgressBar(progress);
380
+
381
+ return {
382
+ name: d,
383
+ tasks: taskCount,
384
+ completed: completedCount,
385
+ progress,
386
+ progressBar,
387
+ };
388
+ });
389
+
390
+ const totalTasks = phases.reduce((sum, p) => sum + p.tasks, 0);
391
+ const totalCompleted = phases.reduce((sum, p) => sum + p.completed, 0);
392
+ const overall = totalTasks > 0 ? Math.round((totalCompleted / totalTasks) * 100) : 0;
393
+
394
+ // Display formatted progress
395
+ console.log(`\n${colors.bold}Project Progress${colors.reset}\n`);
396
+ phases.forEach(p => {
397
+ const status = p.progress === 100 ? colors.green + '✅' : p.progress > 0 ? colors.yellow + '🔄' : colors.gray + '⏳';
398
+ console.log(` ${status}${colors.reset} ${p.name.padEnd(30)} ${p.progressBar} ${String(p.progress).padStart(3)}%`);
399
+ });
400
+ console.log(` ${'─'.repeat(55)}`);
401
+ console.log(` ${colors.bold}Overall${colors.reset}${' '.repeat(24)} ${createProgressBar(overall)} ${String(overall).padStart(3)}%\n`);
402
+
403
+ console.log(JSON.stringify({
404
+ phases: phases.map(p => ({ name: p.name, tasks: p.tasks, completed: p.completed, progress: p.progress })),
405
+ total_tasks: totalTasks,
406
+ total_completed: totalCompleted,
407
+ overall,
408
+ }, null, 2));
409
+ },
410
+
411
+ /**
412
+ * Version management
413
+ */
414
+ version: (args) => {
415
+ const action = args[0] || 'get';
416
+ const projectCheck = validators.requireProjectRoot();
417
+ validateArgs([projectCheck]);
418
+ const projectRoot = projectCheck.value;
419
+
420
+ const trackerPath = path.join(projectRoot, VIEPILOT_DIR, 'TRACKER.md');
421
+ const content = readMarkdown(trackerPath);
422
+
423
+ if (!content) {
424
+ console.error(formatError('Cannot read TRACKER.md', 'File may be corrupted or missing'));
425
+ process.exit(1);
426
+ }
427
+
428
+ // Extract current version
429
+ const versionMatch = content.match(/```\n([\d.]+[-\w.]*)\n```/);
430
+ const currentVersion = versionMatch ? versionMatch[1] : '0.0.0';
431
+
432
+ if (action === 'get') {
433
+ console.log(formatInfo(`Current version: ${colors.bold}${currentVersion}${colors.reset}`));
434
+ console.log(JSON.stringify({ version: currentVersion }));
435
+ return;
436
+ }
437
+
438
+ if (action === 'bump') {
439
+ const type = args[1] || 'patch';
440
+ const typeCheck = validators.isValidBumpType(type);
441
+ validateArgs([typeCheck]);
442
+
443
+ const parts = currentVersion.replace(/-.*/, '').split('.').map(Number);
444
+
445
+ switch (typeCheck.value) {
446
+ case 'major':
447
+ parts[0]++;
448
+ parts[1] = 0;
449
+ parts[2] = 0;
450
+ break;
451
+ case 'minor':
452
+ parts[1]++;
453
+ parts[2] = 0;
454
+ break;
455
+ case 'patch':
456
+ parts[2]++;
457
+ break;
458
+ }
459
+
460
+ const newVersion = parts.join('.');
461
+ console.log(formatSuccess(`Version bump: ${currentVersion} → ${colors.bold}${newVersion}${colors.reset} (${typeCheck.value})`));
462
+ console.log(JSON.stringify({
463
+ old_version: currentVersion,
464
+ new_version: newVersion,
465
+ bump_type: typeCheck.value,
466
+ }, null, 2));
467
+ return;
468
+ }
469
+
470
+ // Unknown action
471
+ console.error(formatError(`Unknown action: "${action}"`, 'Valid actions: get, bump'));
472
+ process.exit(1);
473
+ },
474
+
475
+ /**
476
+ * Reset task or phase state (destructive - requires confirmation)
477
+ */
478
+ reset: async (args) => {
479
+ const target = args[0];
480
+ const force = args.includes('--force') || args.includes('-f');
481
+
482
+ if (!target) {
483
+ console.error(formatError('Reset target required', 'Usage: vp-tools reset <task|phase|all> [--force]'));
484
+ process.exit(1);
485
+ }
486
+
487
+ const validTargets = ['task', 'phase', 'all'];
488
+ if (!validTargets.includes(target)) {
489
+ console.error(formatError(`Invalid reset target: "${target}"`, `Valid targets: ${validTargets.join(', ')}`));
490
+ process.exit(1);
491
+ }
492
+
493
+ const projectCheck = validators.requireProjectRoot();
494
+ validateArgs([projectCheck]);
495
+
496
+ const warnings = {
497
+ task: 'This will reset the current task status to not_started',
498
+ phase: 'This will reset ALL tasks in the current phase',
499
+ all: 'This will reset ALL phases and tasks to not_started',
500
+ };
501
+
502
+ console.log(formatWarning(warnings[target]));
503
+
504
+ if (!force) {
505
+ const confirmed = await confirm(`Are you sure you want to reset ${target}?`, false);
506
+ if (!confirmed) {
507
+ console.log(formatInfo('Reset cancelled'));
508
+ process.exit(0);
509
+ }
510
+ }
511
+
512
+ console.log(formatSuccess(`Reset ${target} completed`));
513
+ console.log(JSON.stringify({ reset: target, timestamp: currentTimestamp() }, null, 2));
514
+ },
515
+
516
+ /**
517
+ * Clean generated files (destructive - requires confirmation)
518
+ */
519
+ clean: async (args) => {
520
+ const force = args.includes('--force') || args.includes('-f');
521
+ const dryRun = args.includes('--dry-run');
522
+
523
+ const projectCheck = validators.requireProjectRoot();
524
+ validateArgs([projectCheck]);
525
+ const projectRoot = projectCheck.value;
526
+
527
+ const filesToClean = [
528
+ path.join(projectRoot, VIEPILOT_DIR, 'HANDOFF.json'),
529
+ ];
530
+
531
+ console.log(formatWarning('Files to be removed:'));
532
+ filesToClean.forEach(f => {
533
+ if (fs.existsSync(f)) {
534
+ console.log(` ${colors.red}✖${colors.reset} ${path.relative(projectRoot, f)}`);
535
+ }
536
+ });
537
+
538
+ if (dryRun) {
539
+ console.log(formatInfo('Dry run - no files removed'));
540
+ return;
541
+ }
542
+
543
+ if (!force) {
544
+ const confirmed = await confirm('Delete these files?', false);
545
+ if (!confirmed) {
546
+ console.log(formatInfo('Clean cancelled'));
547
+ process.exit(0);
548
+ }
549
+ }
550
+
551
+ let removed = 0;
552
+ filesToClean.forEach(f => {
553
+ if (fs.existsSync(f)) {
554
+ fs.unlinkSync(f);
555
+ removed++;
556
+ }
557
+ });
558
+
559
+ console.log(formatSuccess(`Cleaned ${removed} file(s)`));
560
+ },
561
+
562
+ /**
563
+ * List and manage checkpoints (git tags)
564
+ */
565
+ checkpoints: (args) => {
566
+ const projectCheck = validators.requireProjectRoot();
567
+ validateArgs([projectCheck]);
568
+ const projectRoot = projectCheck.value;
569
+
570
+ const { execSync } = require('child_process');
571
+
572
+ try {
573
+ const tags = execSync('git tag -l "vp-*" --sort=-creatordate', {
574
+ cwd: projectRoot,
575
+ encoding: 'utf8'
576
+ }).trim().split('\n').filter(t => t);
577
+
578
+ if (tags.length === 0) {
579
+ console.log(formatWarning('No checkpoints found'));
580
+ return;
581
+ }
582
+
583
+ console.log(`\n${colors.bold}ViePilot Checkpoints${colors.reset}\n`);
584
+ console.log(` ${colors.gray}TAG${' '.repeat(25)}COMMIT DATE${colors.reset}`);
585
+ console.log(` ${'─'.repeat(55)}`);
586
+
587
+ tags.slice(0, 20).forEach(tag => {
588
+ try {
589
+ const info = execSync(`git log -1 --format="%h %ci" ${tag}`, {
590
+ cwd: projectRoot,
591
+ encoding: 'utf8'
592
+ }).trim();
593
+ const [hash, ...dateParts] = info.split(' ');
594
+ const date = dateParts.slice(0, 2).join(' ');
595
+
596
+ let icon = '📌';
597
+ if (tag.includes('-complete')) icon = '✅';
598
+ else if (tag.includes('-done')) icon = '✔️';
599
+ else if (tag.includes('-backup')) icon = '💾';
600
+
601
+ console.log(` ${icon} ${tag.padEnd(26)} ${hash} ${date}`);
602
+ } catch (e) {
603
+ console.log(` 📌 ${tag.padEnd(26)} ${colors.gray}(no info)${colors.reset}`);
604
+ }
605
+ });
606
+
607
+ if (tags.length > 20) {
608
+ console.log(`\n ${colors.gray}... and ${tags.length - 20} more${colors.reset}`);
609
+ }
610
+
611
+ console.log(`\n Total: ${tags.length} checkpoints\n`);
612
+
613
+ } catch (error) {
614
+ console.error(formatError('Failed to list checkpoints', error.message));
615
+ process.exit(1);
616
+ }
617
+ },
618
+
619
+ /**
620
+ * Check for potential conflicts
621
+ */
622
+ conflicts: (args) => {
623
+ const projectCheck = validators.requireProjectRoot();
624
+ validateArgs([projectCheck]);
625
+ const projectRoot = projectCheck.value;
626
+
627
+ const { execSync } = require('child_process');
628
+
629
+ try {
630
+ // Check for uncommitted changes
631
+ const status = execSync('git status --porcelain', {
632
+ cwd: projectRoot,
633
+ encoding: 'utf8'
634
+ }).trim();
635
+
636
+ const changes = status.split('\n').filter(l => l);
637
+
638
+ if (changes.length === 0) {
639
+ console.log(formatSuccess('No conflicts detected - working directory clean'));
640
+ return;
641
+ }
642
+
643
+ console.log(`\n${colors.bold}Potential Conflicts${colors.reset}\n`);
644
+
645
+ const conflicts = {
646
+ modified: [],
647
+ untracked: [],
648
+ deleted: [],
649
+ staged: [],
650
+ };
651
+
652
+ changes.forEach(line => {
653
+ const [status, ...fileParts] = line.trim().split(/\s+/);
654
+ const file = fileParts.join(' ');
655
+
656
+ if (status.includes('M')) conflicts.modified.push(file);
657
+ else if (status === '??') conflicts.untracked.push(file);
658
+ else if (status.includes('D')) conflicts.deleted.push(file);
659
+ else if (status.includes('A') || status.includes('R')) conflicts.staged.push(file);
660
+ });
661
+
662
+ if (conflicts.modified.length > 0) {
663
+ console.log(` ${colors.yellow}Modified files:${colors.reset}`);
664
+ conflicts.modified.forEach(f => console.log(` ${colors.yellow}M${colors.reset} ${f}`));
665
+ }
666
+
667
+ if (conflicts.untracked.length > 0) {
668
+ console.log(` ${colors.blue}Untracked files:${colors.reset}`);
669
+ conflicts.untracked.forEach(f => console.log(` ${colors.blue}?${colors.reset} ${f}`));
670
+ }
671
+
672
+ if (conflicts.deleted.length > 0) {
673
+ console.log(` ${colors.red}Deleted files:${colors.reset}`);
674
+ conflicts.deleted.forEach(f => console.log(` ${colors.red}D${colors.reset} ${f}`));
675
+ }
676
+
677
+ if (conflicts.staged.length > 0) {
678
+ console.log(` ${colors.green}Staged files:${colors.reset}`);
679
+ conflicts.staged.forEach(f => console.log(` ${colors.green}A${colors.reset} ${f}`));
680
+ }
681
+
682
+ console.log(`\n ${colors.gray}Total: ${changes.length} file(s) with changes${colors.reset}\n`);
683
+
684
+ console.log(formatWarning('Resolve these before running /vp-auto or /vp-rollback'));
685
+
686
+ } catch (error) {
687
+ console.error(formatError('Failed to check conflicts', error.message));
688
+ process.exit(1);
689
+ }
690
+ },
691
+
692
+ /**
693
+ * Save current state for precise resume
694
+ */
695
+ 'save-state': (args) => {
696
+ const projectCheck = validators.requireProjectRoot();
697
+ validateArgs([projectCheck]);
698
+ const projectRoot = projectCheck.value;
699
+
700
+ const handoffPath = path.join(projectRoot, VIEPILOT_DIR, 'HANDOFF.json');
701
+ const trackerPath = path.join(projectRoot, VIEPILOT_DIR, 'TRACKER.md');
702
+
703
+ // Read current state
704
+ let handoff = {};
705
+ if (fs.existsSync(handoffPath)) {
706
+ handoff = readJson(handoffPath);
707
+ }
708
+
709
+ // Get current git info
710
+ const { execSync } = require('child_process');
711
+ let gitInfo = {};
712
+ try {
713
+ gitInfo.head = execSync('git rev-parse HEAD', { cwd: projectRoot, encoding: 'utf8' }).trim();
714
+ gitInfo.branch = execSync('git branch --show-current', { cwd: projectRoot, encoding: 'utf8' }).trim();
715
+ gitInfo.status = execSync('git status --porcelain', { cwd: projectRoot, encoding: 'utf8' }).trim();
716
+ } catch (e) {
717
+ gitInfo.error = e.message;
718
+ }
719
+
720
+ // Update handoff with precise state
721
+ handoff.updated_at = currentTimestamp();
722
+ handoff.git = gitInfo;
723
+ handoff.resume_point = {
724
+ timestamp: currentTimestamp(),
725
+ cwd: process.cwd(),
726
+ node_version: process.version,
727
+ };
728
+
729
+ // Save
730
+ writeJson(handoffPath, handoff);
731
+
732
+ console.log(formatSuccess('State saved for resume'));
733
+ console.log(JSON.stringify({
734
+ saved_at: handoff.updated_at,
735
+ phase: handoff.phase,
736
+ task: handoff.task,
737
+ git_head: gitInfo.head?.substring(0, 7),
738
+ }, null, 2));
739
+ },
740
+
741
+ /**
742
+ * Help
743
+ */
744
+ help: (args) => {
745
+ const command = args[0];
746
+
747
+ const commandHelp = {
748
+ init: {
749
+ usage: 'vp-tools init',
750
+ description: 'Get project initialization state and verify .viepilot/ exists',
751
+ examples: ['vp-tools init'],
752
+ },
753
+ 'current-timestamp': {
754
+ usage: 'vp-tools current-timestamp [format] [--raw]',
755
+ description: 'Get current timestamp in specified format',
756
+ options: [
757
+ 'format: iso (default), date, full',
758
+ '--raw: Output only the timestamp string',
759
+ ],
760
+ examples: [
761
+ 'vp-tools current-timestamp',
762
+ 'vp-tools current-timestamp full --raw',
763
+ ],
764
+ },
765
+ 'phase-info': {
766
+ usage: 'vp-tools phase-info <phase_number>',
767
+ description: 'Get information about a specific phase',
768
+ examples: [
769
+ 'vp-tools phase-info 1',
770
+ 'vp-tools phase-info 2',
771
+ ],
772
+ },
773
+ 'task-status': {
774
+ usage: 'vp-tools task-status <phase> <task> <status>',
775
+ description: 'Update the status of a task',
776
+ options: [
777
+ 'status: not_started, in_progress, done, skipped, blocked',
778
+ ],
779
+ examples: [
780
+ 'vp-tools task-status 1 1 in_progress',
781
+ 'vp-tools task-status 1 2 done',
782
+ ],
783
+ },
784
+ commit: {
785
+ usage: 'vp-tools commit "<message>" [--files <file1> <file2>...]',
786
+ description: 'Generate git commit command with standard format',
787
+ examples: [
788
+ 'vp-tools commit "feat(cli): add validation"',
789
+ 'vp-tools commit "fix(core): resolve bug" --files src/index.js',
790
+ ],
791
+ },
792
+ progress: {
793
+ usage: 'vp-tools progress',
794
+ description: 'Calculate and display overall project progress',
795
+ examples: ['vp-tools progress'],
796
+ },
797
+ version: {
798
+ usage: 'vp-tools version [get|bump] [major|minor|patch]',
799
+ description: 'Version management - get current or bump version',
800
+ examples: [
801
+ 'vp-tools version',
802
+ 'vp-tools version get',
803
+ 'vp-tools version bump minor',
804
+ ],
805
+ },
806
+ };
807
+
808
+ if (command && commandHelp[command]) {
809
+ const help = commandHelp[command];
810
+ console.log(`\n${colors.bold}${command}${colors.reset}\n`);
811
+ console.log(` ${help.description}\n`);
812
+ console.log(` ${colors.cyan}Usage:${colors.reset} ${help.usage}\n`);
813
+ if (help.options) {
814
+ console.log(` ${colors.cyan}Options:${colors.reset}`);
815
+ help.options.forEach(opt => console.log(` ${opt}`));
816
+ console.log();
817
+ }
818
+ console.log(` ${colors.cyan}Examples:${colors.reset}`);
819
+ help.examples.forEach(ex => console.log(` ${colors.gray}$${colors.reset} ${ex}`));
820
+ console.log();
821
+ return;
822
+ }
823
+
824
+ console.log(`
825
+ ${colors.bold}ViePilot CLI Tools${colors.reset}
826
+ ${colors.gray}Helper utilities for state management and workflow operations${colors.reset}
827
+
828
+ ${colors.cyan}Usage:${colors.reset}
829
+ vp-tools <command> [options]
830
+
831
+ ${colors.cyan}Commands:${colors.reset}
832
+ ${colors.bold}init${colors.reset} Get project initialization state
833
+ ${colors.bold}current-timestamp${colors.reset} Get current timestamp (iso|date|full) [--raw]
834
+ ${colors.bold}phase-info${colors.reset} <N> Get phase N information
835
+ ${colors.bold}task-status${colors.reset} <P> <T> <S> Update task T in phase P to status S
836
+ ${colors.bold}commit${colors.reset} <msg> [--files] Create git commit command
837
+ ${colors.bold}progress${colors.reset} Calculate overall progress
838
+ ${colors.bold}version${colors.reset} [get|bump] Version management
839
+ ${colors.bold}reset${colors.reset} <target> [-f] Reset task/phase/all state (interactive)
840
+ ${colors.bold}clean${colors.reset} [-f] [--dry-run] Clean generated files (interactive)
841
+ ${colors.bold}checkpoints${colors.reset} List all ViePilot checkpoints (git tags)
842
+ ${colors.bold}conflicts${colors.reset} Check for potential conflicts
843
+ ${colors.bold}save-state${colors.reset} Save current state for precise resume
844
+ ${colors.bold}help${colors.reset} [command] Show help (optionally for specific command)
845
+
846
+ ${colors.cyan}Examples:${colors.reset}
847
+ ${colors.gray}$${colors.reset} vp-tools init
848
+ ${colors.gray}$${colors.reset} vp-tools phase-info 1
849
+ ${colors.gray}$${colors.reset} vp-tools progress
850
+ ${colors.gray}$${colors.reset} vp-tools version bump minor
851
+ ${colors.gray}$${colors.reset} vp-tools help phase-info
852
+
853
+ ${colors.gray}Run 'vp-tools help <command>' for detailed help on a specific command.${colors.reset}
854
+ `);
855
+ },
856
+ };
857
+
858
+ // Helper function for progress bars
859
+ function createProgressBar(percent, width = 10) {
860
+ const filled = Math.round(percent / (100 / width));
861
+ const empty = width - filled;
862
+ return `[${colors.green}${'█'.repeat(filled)}${colors.gray}${'░'.repeat(empty)}${colors.reset}]`;
863
+ }
864
+
865
+ // ============================================================================
866
+ // Main
867
+ // ============================================================================
868
+
869
+ async function main() {
870
+ const args = process.argv.slice(2);
871
+ const command = args[0];
872
+
873
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
874
+ commands.help(args.slice(1));
875
+ return;
876
+ }
877
+
878
+ if (commands[command]) {
879
+ try {
880
+ const result = commands[command](args.slice(1));
881
+ if (result instanceof Promise) {
882
+ await result;
883
+ }
884
+ } catch (error) {
885
+ console.error(formatError(`Command failed: ${error.message}`));
886
+ process.exit(1);
887
+ }
888
+ return;
889
+ }
890
+
891
+ // Suggest similar commands
892
+ const available = Object.keys(commands);
893
+ const similar = available.filter(cmd =>
894
+ cmd.includes(command) || command.includes(cmd) ||
895
+ levenshteinDistance(cmd, command) <= 2
896
+ );
897
+
898
+ let hint = `Available commands: ${available.join(', ')}`;
899
+ if (similar.length > 0 && similar.length < available.length) {
900
+ hint = `Did you mean: ${similar.join(', ')}?`;
901
+ }
902
+
903
+ console.error(formatError(`Unknown command: "${command}"`, hint));
904
+ process.exit(1);
905
+ }
906
+
907
+ if (require.main === module) {
908
+ main().catch((err) => {
909
+ console.error(formatError(`Unexpected error: ${err.message}`));
910
+ process.exit(1);
911
+ });
912
+ }