vbounce-engine 2.5.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.
Files changed (165) hide show
  1. package/README.md +142 -0
  2. package/VBOUNCE_MANIFEST.md +404 -0
  3. package/bin/vbounce.mjs +882 -0
  4. package/brains/AGENTS.md +71 -0
  5. package/brains/CHANGELOG.md +398 -0
  6. package/brains/CLAUDE.md +90 -0
  7. package/brains/GEMINI.md +102 -0
  8. package/brains/SETUP.md +195 -0
  9. package/brains/claude-agents/architect.md +226 -0
  10. package/brains/claude-agents/developer.md +133 -0
  11. package/brains/claude-agents/devops.md +267 -0
  12. package/brains/claude-agents/explorer.md +157 -0
  13. package/brains/claude-agents/qa.md +225 -0
  14. package/brains/claude-agents/scribe.md +171 -0
  15. package/brains/copilot/copilot-instructions.md +54 -0
  16. package/brains/cursor-rules/vbounce-docs.mdc +45 -0
  17. package/brains/cursor-rules/vbounce-process.mdc +51 -0
  18. package/brains/cursor-rules/vbounce-rules.mdc +29 -0
  19. package/brains/windsurf/.windsurfrules +35 -0
  20. package/docs/HOTFIX_EDGE_CASES.md +37 -0
  21. package/docs/IMPROVEMENT.md +46 -0
  22. package/docs/agent-skill-profiles.docx +0 -0
  23. package/docs/icons/alert.svg +1 -0
  24. package/docs/icons/beaker.svg +1 -0
  25. package/docs/icons/book.svg +1 -0
  26. package/docs/icons/git-branch.svg +1 -0
  27. package/docs/icons/git-merge.svg +1 -0
  28. package/docs/icons/graph.svg +1 -0
  29. package/docs/icons/light-bulb.svg +1 -0
  30. package/docs/icons/logo.svg +9 -0
  31. package/docs/icons/pencil.svg +1 -0
  32. package/docs/icons/rocket.svg +1 -0
  33. package/docs/icons/shield.svg +1 -0
  34. package/docs/icons/sync.svg +1 -0
  35. package/docs/icons/terminal.svg +1 -0
  36. package/docs/icons/tools.svg +1 -0
  37. package/docs/icons/zap.svg +1 -0
  38. package/docs/images/bounce_loop_diagram.png +0 -0
  39. package/docs/vbounce-os-manual.docx +0 -0
  40. package/package.json +48 -0
  41. package/scripts/close_sprint.mjs +134 -0
  42. package/scripts/complete_story.mjs +121 -0
  43. package/scripts/count_tokens.mjs +494 -0
  44. package/scripts/doctor.mjs +144 -0
  45. package/scripts/hotfix_manager.sh +157 -0
  46. package/scripts/init_gate_config.sh +151 -0
  47. package/scripts/init_sprint.mjs +129 -0
  48. package/scripts/post_sprint_improve.mjs +486 -0
  49. package/scripts/pre_gate_common.sh +576 -0
  50. package/scripts/pre_gate_runner.sh +176 -0
  51. package/scripts/prep_arch_context.mjs +178 -0
  52. package/scripts/prep_qa_context.mjs +152 -0
  53. package/scripts/prep_sprint_context.mjs +141 -0
  54. package/scripts/prep_sprint_summary.mjs +154 -0
  55. package/scripts/product_graph.mjs +387 -0
  56. package/scripts/product_impact.mjs +167 -0
  57. package/scripts/sprint_trends.mjs +160 -0
  58. package/scripts/suggest_improvements.mjs +363 -0
  59. package/scripts/update_state.mjs +132 -0
  60. package/scripts/validate_bounce_readiness.mjs +152 -0
  61. package/scripts/validate_report.mjs +165 -0
  62. package/scripts/validate_sprint_plan.mjs +117 -0
  63. package/scripts/validate_state.mjs +99 -0
  64. package/scripts/vdoc_match.mjs +269 -0
  65. package/scripts/vdoc_staleness.mjs +199 -0
  66. package/scripts/verify_framework.mjs +122 -0
  67. package/scripts/verify_framework.sh +13 -0
  68. package/skills/agent-team/SKILL.md +579 -0
  69. package/skills/agent-team/references/cleanup.md +42 -0
  70. package/skills/agent-team/references/delivery-sync.md +43 -0
  71. package/skills/agent-team/references/discovery.md +97 -0
  72. package/skills/agent-team/references/git-strategy.md +52 -0
  73. package/skills/agent-team/references/mid-sprint-triage.md +85 -0
  74. package/skills/agent-team/references/report-naming.md +34 -0
  75. package/skills/doc-manager/SKILL.md +444 -0
  76. package/skills/file-organization/SKILL.md +146 -0
  77. package/skills/file-organization/TEST-RESULTS.md +193 -0
  78. package/skills/file-organization/evals/evals.json +41 -0
  79. package/skills/file-organization/references/gitignore-template.md +53 -0
  80. package/skills/file-organization/references/quick-checklist.md +48 -0
  81. package/skills/improve/SKILL.md +296 -0
  82. package/skills/lesson/SKILL.md +136 -0
  83. package/skills/product-graph/SKILL.md +102 -0
  84. package/skills/react-best-practices/SKILL.md +3014 -0
  85. package/skills/react-best-practices/rules/_sections.md +46 -0
  86. package/skills/react-best-practices/rules/_template.md +28 -0
  87. package/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  88. package/skills/react-best-practices/rules/advanced-init-once.md +42 -0
  89. package/skills/react-best-practices/rules/advanced-use-latest.md +39 -0
  90. package/skills/react-best-practices/rules/async-api-routes.md +38 -0
  91. package/skills/react-best-practices/rules/async-defer-await.md +80 -0
  92. package/skills/react-best-practices/rules/async-dependencies.md +51 -0
  93. package/skills/react-best-practices/rules/async-parallel.md +28 -0
  94. package/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
  95. package/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
  96. package/skills/react-best-practices/rules/bundle-conditional.md +31 -0
  97. package/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
  98. package/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  99. package/skills/react-best-practices/rules/bundle-preload.md +50 -0
  100. package/skills/react-best-practices/rules/client-event-listeners.md +74 -0
  101. package/skills/react-best-practices/rules/client-localstorage-schema.md +71 -0
  102. package/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
  103. package/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
  104. package/skills/react-best-practices/rules/js-batch-dom-css.md +107 -0
  105. package/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
  106. package/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
  107. package/skills/react-best-practices/rules/js-cache-storage.md +70 -0
  108. package/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
  109. package/skills/react-best-practices/rules/js-early-exit.md +50 -0
  110. package/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
  111. package/skills/react-best-practices/rules/js-index-maps.md +37 -0
  112. package/skills/react-best-practices/rules/js-length-check-first.md +49 -0
  113. package/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
  114. package/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
  115. package/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
  116. package/skills/react-best-practices/rules/rendering-activity.md +26 -0
  117. package/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  118. package/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
  119. package/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
  120. package/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  121. package/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  122. package/skills/react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  123. package/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
  124. package/skills/react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  125. package/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
  126. package/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
  127. package/skills/react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  128. package/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
  129. package/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
  130. package/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  131. package/skills/react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  132. package/skills/react-best-practices/rules/rerender-memo.md +44 -0
  133. package/skills/react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  134. package/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  135. package/skills/react-best-practices/rules/rerender-transitions.md +40 -0
  136. package/skills/react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  137. package/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
  138. package/skills/react-best-practices/rules/server-auth-actions.md +96 -0
  139. package/skills/react-best-practices/rules/server-cache-lru.md +41 -0
  140. package/skills/react-best-practices/rules/server-cache-react.md +76 -0
  141. package/skills/react-best-practices/rules/server-dedup-props.md +65 -0
  142. package/skills/react-best-practices/rules/server-parallel-fetching.md +83 -0
  143. package/skills/react-best-practices/rules/server-serialization.md +38 -0
  144. package/skills/vibe-code-review/SKILL.md +70 -0
  145. package/skills/vibe-code-review/references/deep-audit.md +259 -0
  146. package/skills/vibe-code-review/references/pr-review.md +234 -0
  147. package/skills/vibe-code-review/references/quick-scan.md +178 -0
  148. package/skills/vibe-code-review/references/report-template.md +189 -0
  149. package/skills/vibe-code-review/references/trend-check.md +224 -0
  150. package/skills/vibe-code-review/scripts/generate-snapshot.sh +89 -0
  151. package/skills/vibe-code-review/scripts/pr-analyze.sh +180 -0
  152. package/skills/write-skill/SKILL.md +133 -0
  153. package/templates/bug.md +100 -0
  154. package/templates/change_request.md +105 -0
  155. package/templates/charter.md +144 -0
  156. package/templates/delivery_plan.md +44 -0
  157. package/templates/epic.md +203 -0
  158. package/templates/hotfix.md +58 -0
  159. package/templates/risk_registry.md +87 -0
  160. package/templates/roadmap.md +174 -0
  161. package/templates/spike.md +143 -0
  162. package/templates/sprint.md +134 -0
  163. package/templates/sprint_context.md +61 -0
  164. package/templates/sprint_report.md +215 -0
  165. package/templates/story.md +193 -0
@@ -0,0 +1,882 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import crypto from 'crypto';
6
+ import { fileURLToPath } from 'url';
7
+ import readline from 'readline';
8
+
9
+ import { execSync, spawnSync } from 'child_process';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ const pkgRoot = path.join(__dirname, '..');
14
+
15
+ const args = process.argv.slice(2);
16
+ const command = args[0];
17
+ const sub = args[1];
18
+
19
+ if (command === '-v' || command === '--version') {
20
+ const pkgPath = path.join(pkgRoot, 'package.json');
21
+ if (fs.existsSync(pkgPath)) {
22
+ const pkgInfo = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
23
+ console.log(`v${pkgInfo.version}`);
24
+ } else {
25
+ console.log('Version information unavailable.');
26
+ }
27
+ process.exit(0);
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Script runner helper
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Run a Node.js script from the scripts/ directory with forwarded args.
36
+ * @param {string} scriptName - filename inside scripts/ (e.g. 'doctor.mjs')
37
+ * @param {string[]} scriptArgs - additional CLI arguments
38
+ */
39
+ function runScript(scriptName, scriptArgs = []) {
40
+ const scriptPath = path.join(pkgRoot, 'scripts', scriptName);
41
+ if (!fs.existsSync(scriptPath)) {
42
+ console.error(`Error: Script not found: scripts/${scriptName}`);
43
+ console.error('Run `vbounce doctor` to check which scripts are missing.');
44
+ process.exit(1);
45
+ }
46
+ const result = spawnSync(process.execPath, [scriptPath, ...scriptArgs], {
47
+ stdio: 'inherit',
48
+ cwd: process.cwd()
49
+ });
50
+ process.exit(result.status ?? 0);
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Utility for interactive prompt (used by install)
55
+ // ---------------------------------------------------------------------------
56
+ const rl = readline.createInterface({
57
+ input: process.stdin,
58
+ output: process.stdout
59
+ });
60
+
61
+ const askQuestion = (query) => new Promise(resolve => rl.question(query, resolve));
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Help text
65
+ // ---------------------------------------------------------------------------
66
+
67
+ function displayHelp() {
68
+ console.log(`
69
+ V-Bounce Engine CLI
70
+
71
+ Usage:
72
+ vbounce install <platform> Install V-Bounce Engine into a project
73
+ vbounce uninstall Remove V-Bounce Engine from a project
74
+ vbounce state show Show current sprint state
75
+ vbounce state update <storyId> <state|--qa-bounce>
76
+ vbounce sprint init <sprintId> <deliveryId> [--stories STORY-001,...]
77
+ vbounce sprint close <sprintId>
78
+ vbounce story complete <storyId> [options]
79
+ vbounce validate report <file> Validate agent report YAML
80
+ vbounce validate state Validate state.json
81
+ vbounce validate sprint [file] Validate Sprint Plan
82
+ vbounce validate ready <storyId> Pre-bounce gate check
83
+ vbounce prep qa <storyId> Generate QA context pack
84
+ vbounce prep arch <storyId> Generate Architect context pack
85
+ vbounce prep sprint <sprintId> Generate Sprint context pack
86
+ vbounce tokens Show token usage for current session
87
+ vbounce tokens --all Show per-subagent token breakdown
88
+ vbounce tokens --sprint S-XX Aggregate tokens from all stories in a sprint
89
+ vbounce tokens --json JSON output for reports
90
+ vbounce graph [generate] Generate product document graph
91
+ vbounce graph impact <DOC-ID> Show what's affected by a document change
92
+ vbounce docs match --story <ID> Match story scope against vdoc manifest
93
+ vbounce docs check <sprintId> Detect stale vdocs and generate Scribe task
94
+ vbounce trends Cross-sprint trend analysis
95
+ vbounce suggest <sprintId> Generate improvement suggestions
96
+ vbounce improve <sprintId> Run full self-improvement pipeline
97
+ vbounce doctor Validate all configs and state files
98
+
99
+ Install Platforms:
100
+ claude : Installs CLAUDE.md and Claude Code subagents
101
+ cursor : Installs modular .cursor/rules/
102
+ gemini : Installs GEMINI.md and Antigravity skills
103
+ codex : Installs AGENTS.md for OpenAI Codex
104
+ vscode : Installs standard system prompt for Copilot
105
+ copilot : Alias for vscode
106
+ `);
107
+ process.exit(0);
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Route commands
112
+ // ---------------------------------------------------------------------------
113
+
114
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
115
+ rl.close();
116
+ displayHelp();
117
+ }
118
+
119
+ // -- state --
120
+ if (command === 'state') {
121
+ rl.close();
122
+ if (sub === 'show') {
123
+ runScript('update_state.mjs', ['--show']);
124
+ } else if (sub === 'update') {
125
+ // vbounce state update <storyId> <newState|--qa-bounce>
126
+ runScript('update_state.mjs', args.slice(2));
127
+ } else {
128
+ console.error(`Unknown state subcommand: ${sub}`);
129
+ console.error('Usage: vbounce state show | vbounce state update <storyId> <state|--qa-bounce>');
130
+ process.exit(1);
131
+ }
132
+ }
133
+
134
+ // -- sprint --
135
+ if (command === 'sprint') {
136
+ rl.close();
137
+ if (sub === 'init') {
138
+ runScript('init_sprint.mjs', args.slice(2));
139
+ } else if (sub === 'close') {
140
+ runScript('close_sprint.mjs', args.slice(2));
141
+ } else {
142
+ console.error(`Unknown sprint subcommand: ${sub}`);
143
+ console.error('Usage: vbounce sprint init <sprintId> <deliveryId> | vbounce sprint close <sprintId>');
144
+ process.exit(1);
145
+ }
146
+ }
147
+
148
+ // -- story --
149
+ if (command === 'story') {
150
+ rl.close();
151
+ if (sub === 'complete') {
152
+ runScript('complete_story.mjs', args.slice(2));
153
+ } else {
154
+ console.error(`Unknown story subcommand: ${sub}`);
155
+ console.error('Usage: vbounce story complete <storyId> [options]');
156
+ process.exit(1);
157
+ }
158
+ }
159
+
160
+ // -- validate --
161
+ if (command === 'validate') {
162
+ rl.close();
163
+ if (sub === 'report') {
164
+ runScript('validate_report.mjs', args.slice(2));
165
+ } else if (sub === 'state') {
166
+ runScript('validate_state.mjs', args.slice(2));
167
+ } else if (sub === 'sprint') {
168
+ runScript('validate_sprint_plan.mjs', args.slice(2));
169
+ } else if (sub === 'ready') {
170
+ runScript('validate_bounce_readiness.mjs', args.slice(2));
171
+ } else {
172
+ console.error(`Unknown validate subcommand: ${sub}`);
173
+ console.error('Usage: vbounce validate report <file> | state | sprint [file] | ready <storyId>');
174
+ process.exit(1);
175
+ }
176
+ }
177
+
178
+ // -- prep --
179
+ if (command === 'prep') {
180
+ rl.close();
181
+ if (sub === 'qa') {
182
+ runScript('prep_qa_context.mjs', args.slice(2));
183
+ } else if (sub === 'arch') {
184
+ runScript('prep_arch_context.mjs', args.slice(2));
185
+ } else if (sub === 'sprint') {
186
+ runScript('prep_sprint_context.mjs', args.slice(2));
187
+ } else {
188
+ console.error(`Unknown prep subcommand: ${sub}`);
189
+ console.error('Usage: vbounce prep qa <storyId> | arch <storyId> | sprint <sprintId>');
190
+ process.exit(1);
191
+ }
192
+ }
193
+
194
+ // -- trends --
195
+ if (command === 'trends') {
196
+ rl.close();
197
+ runScript('sprint_trends.mjs', args.slice(1));
198
+ }
199
+
200
+ // -- suggest --
201
+ if (command === 'suggest') {
202
+ rl.close();
203
+ runScript('suggest_improvements.mjs', args.slice(1));
204
+ }
205
+
206
+ // -- improve --
207
+ if (command === 'improve') {
208
+ rl.close();
209
+ // Full pipeline: analyze → trends → suggest
210
+ const sprintArg = args[1];
211
+ if (!sprintArg) {
212
+ console.error('Usage: vbounce improve S-XX');
213
+ process.exit(1);
214
+ }
215
+ // Run trends first
216
+ const trendsPath = path.join(pkgRoot, 'scripts', 'sprint_trends.mjs');
217
+ if (fs.existsSync(trendsPath)) {
218
+ console.log('Step 1/2: Running cross-sprint trend analysis...');
219
+ spawnSync(process.execPath, [trendsPath], { stdio: 'inherit', cwd: process.cwd() });
220
+ }
221
+ // Run suggest (which internally runs post_sprint_improve.mjs)
222
+ console.log('\nStep 2/2: Running improvement analyzer + suggestions...');
223
+ runScript('suggest_improvements.mjs', [sprintArg]);
224
+ }
225
+
226
+ // -- tokens --
227
+ if (command === 'tokens') {
228
+ rl.close();
229
+ runScript('count_tokens.mjs', args.slice(1));
230
+ }
231
+
232
+ // -- graph --
233
+ if (command === 'graph') {
234
+ rl.close();
235
+ if (sub === 'impact') {
236
+ runScript('product_impact.mjs', args.slice(2));
237
+ } else if (!sub || sub === 'generate') {
238
+ runScript('product_graph.mjs', args.slice(2));
239
+ } else {
240
+ console.error(`Unknown graph subcommand: ${sub}`);
241
+ console.error('Usage: vbounce graph [generate] | vbounce graph impact <DOC-ID>');
242
+ process.exit(1);
243
+ }
244
+ }
245
+
246
+ // -- docs --
247
+ if (command === 'docs') {
248
+ rl.close();
249
+ if (sub === 'match') {
250
+ runScript('vdoc_match.mjs', args.slice(2));
251
+ } else if (sub === 'check') {
252
+ runScript('vdoc_staleness.mjs', args.slice(2));
253
+ } else {
254
+ console.error(`Unknown docs subcommand: ${sub}`);
255
+ console.error('Usage: vbounce docs match --story <ID> | vbounce docs check <sprintId>');
256
+ process.exit(1);
257
+ }
258
+ }
259
+
260
+ // -- doctor --
261
+ if (command === 'doctor') {
262
+ rl.close();
263
+ runScript('doctor.mjs', args.slice(1));
264
+ }
265
+
266
+ // ---------------------------------------------------------------------------
267
+ // uninstall command
268
+ // ---------------------------------------------------------------------------
269
+
270
+ if (command === 'uninstall') {
271
+ const CWD = process.cwd();
272
+ const metaPath = path.join(CWD, '.vbounce', 'install-meta.json');
273
+
274
+ if (!fs.existsSync(metaPath)) {
275
+ rl.close();
276
+ console.log('\nV-Bounce is not installed in this project (no .vbounce/install-meta.json found).\n');
277
+ process.exit(0);
278
+ }
279
+
280
+ let meta;
281
+ try {
282
+ meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
283
+ } catch {
284
+ rl.close();
285
+ console.error('Error: Could not parse .vbounce/install-meta.json');
286
+ process.exit(1);
287
+ }
288
+
289
+ console.log(`\n🗑️ Uninstalling V-Bounce Engine \x1b[36m${meta.version}\x1b[0m (platform: ${meta.platform})\n`);
290
+
291
+ // Framework files to remove (always safe)
292
+ const frameworkRemovals = [];
293
+ if (meta.files) {
294
+ for (const f of meta.files) {
295
+ if (fs.existsSync(path.join(CWD, f))) {
296
+ frameworkRemovals.push(f);
297
+ }
298
+ }
299
+ }
300
+
301
+ // Platform brain files
302
+ const brainFiles = {
303
+ claude: ['CLAUDE.md', '.claude/agents'],
304
+ cursor: ['.cursor/rules'],
305
+ gemini: ['GEMINI.md', '.agents/skills'],
306
+ codex: ['AGENTS.md'],
307
+ vscode: ['.github/copilot-instructions.md'],
308
+ copilot: ['.github/copilot-instructions.md']
309
+ };
310
+ const platformFiles = (brainFiles[meta.platform] || []).filter(f => fs.existsSync(path.join(CWD, f)));
311
+
312
+ console.log('Will remove (framework files):');
313
+ for (const f of [...frameworkRemovals, ...platformFiles]) {
314
+ console.log(` \x1b[31m✖\x1b[0m ${f}`);
315
+ }
316
+
317
+ // User data that needs a prompt
318
+ const userData = [];
319
+ if (fs.existsSync(path.join(CWD, 'LESSONS.md'))) userData.push('LESSONS.md');
320
+ if (fs.existsSync(path.join(CWD, 'product_plans'))) userData.push('product_plans/');
321
+ if (fs.existsSync(path.join(CWD, '.vbounce', 'archive'))) userData.push('.vbounce/archive/');
322
+ if (fs.existsSync(path.join(CWD, 'vdocs'))) userData.push('vdocs/');
323
+
324
+ if (userData.length > 0) {
325
+ console.log('\n\x1b[33mUser data (will ask separately):\x1b[0m');
326
+ for (const f of userData) {
327
+ console.log(` \x1b[33m?\x1b[0m ${f}`);
328
+ }
329
+ }
330
+
331
+ console.log('');
332
+
333
+ askQuestion('Proceed with uninstall? [y/N] ').then(async answer => {
334
+ if (answer.trim().toLowerCase() !== 'y' && answer.trim().toLowerCase() !== 'yes') {
335
+ rl.close();
336
+ console.log('\n❌ Uninstall cancelled.\n');
337
+ process.exit(0);
338
+ }
339
+
340
+ // Remove framework files
341
+ console.log('\n🗑️ Removing framework files...');
342
+ for (const f of [...frameworkRemovals, ...platformFiles]) {
343
+ const fullPath = path.join(CWD, f);
344
+ if (fs.existsSync(fullPath)) {
345
+ const stats = fs.statSync(fullPath);
346
+ if (stats.isDirectory()) {
347
+ fs.rmSync(fullPath, { recursive: true, force: true });
348
+ } else {
349
+ fs.unlinkSync(fullPath);
350
+ }
351
+ console.log(` \x1b[32m✓\x1b[0m Removed ${f}`);
352
+ }
353
+ }
354
+
355
+ // Ask about user data
356
+ if (userData.length > 0) {
357
+ const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
358
+ const dataAnswer = await new Promise(resolve =>
359
+ rl2.question('\nAlso remove your project data (LESSONS.md, product_plans/, archive/, vdocs/)? [y/N] ', resolve)
360
+ );
361
+ rl2.close();
362
+
363
+ if (dataAnswer.trim().toLowerCase() === 'y' || dataAnswer.trim().toLowerCase() === 'yes') {
364
+ for (const f of userData) {
365
+ const fullPath = path.join(CWD, f.replace(/\/$/, ''));
366
+ if (fs.existsSync(fullPath)) {
367
+ const stats = fs.statSync(fullPath);
368
+ if (stats.isDirectory()) {
369
+ fs.rmSync(fullPath, { recursive: true, force: true });
370
+ } else {
371
+ fs.unlinkSync(fullPath);
372
+ }
373
+ console.log(` \x1b[32m✓\x1b[0m Removed ${f}`);
374
+ }
375
+ }
376
+ } else {
377
+ console.log(' Kept user data.');
378
+ }
379
+ }
380
+
381
+ // Clean up .vbounce/ directory (remove install-meta last)
382
+ const vbounceDir = path.join(CWD, '.vbounce');
383
+ if (fs.existsSync(vbounceDir)) {
384
+ fs.rmSync(vbounceDir, { recursive: true, force: true });
385
+ console.log(` \x1b[32m✓\x1b[0m Removed .vbounce/`);
386
+ }
387
+
388
+ rl.close();
389
+ console.log('\n✅ V-Bounce Engine uninstalled.\n');
390
+ });
391
+ }
392
+
393
+ // ---------------------------------------------------------------------------
394
+ // install command
395
+ // ---------------------------------------------------------------------------
396
+
397
+ if (command === 'install') {
398
+ const targetPlatform = sub?.toLowerCase();
399
+
400
+ if (!targetPlatform) {
401
+ rl.close();
402
+ displayHelp();
403
+ }
404
+
405
+ const CWD = process.cwd();
406
+ const pkgVersion = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8')).version;
407
+
408
+ // Map vbounce platform names to vdoc platform names
409
+ const vdocPlatformMap = {
410
+ claude: 'claude',
411
+ cursor: 'cursor',
412
+ gemini: 'gemini',
413
+ codex: 'agents',
414
+ vscode: 'vscode',
415
+ copilot: 'vscode'
416
+ };
417
+
418
+ const platformMappings = {
419
+ claude: [
420
+ { src: 'brains/CLAUDE.md', dest: 'CLAUDE.md' },
421
+ { src: 'brains/claude-agents', dest: '.claude/agents' },
422
+ { src: 'templates', dest: '.vbounce/templates' },
423
+ { src: 'skills', dest: '.vbounce/skills' },
424
+ { src: 'scripts', dest: '.vbounce/scripts' },
425
+ { src: 'VBOUNCE_MANIFEST.md', dest: '.vbounce/VBOUNCE_MANIFEST.md' }
426
+ ],
427
+ cursor: [
428
+ { src: 'brains/cursor-rules', dest: '.cursor/rules' },
429
+ { src: 'templates', dest: '.vbounce/templates' },
430
+ { src: 'skills', dest: '.vbounce/skills' },
431
+ { src: 'scripts', dest: '.vbounce/scripts' },
432
+ { src: 'VBOUNCE_MANIFEST.md', dest: '.vbounce/VBOUNCE_MANIFEST.md' }
433
+ ],
434
+ gemini: [
435
+ { src: 'brains/GEMINI.md', dest: 'GEMINI.md' },
436
+ { src: 'templates', dest: '.vbounce/templates' },
437
+ { src: 'skills', dest: '.vbounce/skills' },
438
+ { src: 'skills', dest: '.agents/skills' },
439
+ { src: 'scripts', dest: '.vbounce/scripts' },
440
+ { src: 'VBOUNCE_MANIFEST.md', dest: '.vbounce/VBOUNCE_MANIFEST.md' }
441
+ ],
442
+ codex: [
443
+ { src: 'brains/AGENTS.md', dest: 'AGENTS.md' },
444
+ { src: 'templates', dest: '.vbounce/templates' },
445
+ { src: 'skills', dest: '.vbounce/skills' },
446
+ { src: 'scripts', dest: '.vbounce/scripts' },
447
+ { src: 'VBOUNCE_MANIFEST.md', dest: '.vbounce/VBOUNCE_MANIFEST.md' }
448
+ ],
449
+ vscode: [
450
+ { src: 'brains/CLAUDE.md', dest: '.github/copilot-instructions.md' },
451
+ { src: 'templates', dest: '.vbounce/templates' },
452
+ { src: 'skills', dest: '.vbounce/skills' },
453
+ { src: 'scripts', dest: '.vbounce/scripts' },
454
+ { src: 'VBOUNCE_MANIFEST.md', dest: '.vbounce/VBOUNCE_MANIFEST.md' }
455
+ ],
456
+ copilot: [
457
+ { src: 'brains/CLAUDE.md', dest: '.github/copilot-instructions.md' },
458
+ { src: 'templates', dest: '.vbounce/templates' },
459
+ { src: 'skills', dest: '.vbounce/skills' },
460
+ { src: 'scripts', dest: '.vbounce/scripts' },
461
+ { src: 'VBOUNCE_MANIFEST.md', dest: '.vbounce/VBOUNCE_MANIFEST.md' }
462
+ ]
463
+ };
464
+
465
+ const mapping = platformMappings[targetPlatform];
466
+
467
+ if (!mapping) {
468
+ rl.close();
469
+ console.error(`Error: Unsupported platform '${targetPlatform}'.\n`);
470
+ displayHelp();
471
+ }
472
+
473
+ // ---------------------------------------------------------------------------
474
+ // Upgrade-safe install helpers
475
+ // ---------------------------------------------------------------------------
476
+
477
+ const META_PATH = path.join(CWD, '.vbounce', 'install-meta.json');
478
+ const BACKUPS_DIR = path.join(CWD, '.vbounce', 'backups');
479
+ const OLD_META_PATH = path.join(CWD, '.bounce', 'install-meta.json');
480
+
481
+ /** Compute MD5 hash of a single file's contents. */
482
+ function computeFileHash(filePath) {
483
+ const content = fs.readFileSync(filePath);
484
+ return crypto.createHash('md5').update(content).digest('hex');
485
+ }
486
+
487
+ /** Compute a combined hash for a directory by hashing all files sorted by relative path. */
488
+ function computeDirHash(dirPath) {
489
+ const hash = crypto.createHash('md5');
490
+ const entries = [];
491
+
492
+ function walk(dir, rel) {
493
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
494
+ const fullPath = path.join(dir, entry.name);
495
+ const relPath = path.join(rel, entry.name);
496
+ if (entry.isDirectory()) {
497
+ walk(fullPath, relPath);
498
+ } else {
499
+ entries.push({ relPath, fullPath });
500
+ }
501
+ }
502
+ }
503
+
504
+ walk(dirPath, '');
505
+ entries.sort((a, b) => a.relPath.localeCompare(b.relPath));
506
+ for (const e of entries) {
507
+ hash.update(e.relPath);
508
+ hash.update(fs.readFileSync(e.fullPath));
509
+ }
510
+ return hash.digest('hex');
511
+ }
512
+
513
+ /** Compute hash for a path (file or directory). */
514
+ function computeHash(p) {
515
+ const stats = fs.statSync(p);
516
+ return stats.isDirectory() ? computeDirHash(p) : computeFileHash(p);
517
+ }
518
+
519
+ /** Count files in a path (1 for a file, recursive count for a directory). */
520
+ function countFiles(p) {
521
+ const stats = fs.statSync(p);
522
+ if (!stats.isDirectory()) return 1;
523
+ let count = 0;
524
+ function walk(dir) {
525
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
526
+ if (entry.isDirectory()) walk(path.join(dir, entry.name));
527
+ else count++;
528
+ }
529
+ }
530
+ walk(p);
531
+ return count;
532
+ }
533
+
534
+ /** Read install-meta.json, returns null if missing. Checks new path first, falls back to old .bounce/ path. */
535
+ function readInstallMeta() {
536
+ for (const p of [META_PATH, OLD_META_PATH]) {
537
+ if (fs.existsSync(p)) {
538
+ try {
539
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
540
+ } catch {
541
+ continue;
542
+ }
543
+ }
544
+ }
545
+ return null;
546
+ }
547
+
548
+ /**
549
+ * Migrate from old root-level layout (skills/, templates/, scripts/, .bounce/) to .vbounce/.
550
+ * Returns true if migration was performed.
551
+ */
552
+ function migrateOldLayout() {
553
+ const oldPaths = [
554
+ { old: 'skills', new: '.vbounce/skills' },
555
+ { old: 'templates', new: '.vbounce/templates' },
556
+ { old: 'scripts', new: '.vbounce/scripts' },
557
+ ];
558
+
559
+ const needsMigration = oldPaths.some(p =>
560
+ fs.existsSync(path.join(CWD, p.old)) && !fs.existsSync(path.join(CWD, p.new))
561
+ );
562
+
563
+ if (!needsMigration && !fs.existsSync(path.join(CWD, '.bounce'))) return false;
564
+
565
+ console.log('\n🔄 Migrating from old layout to .vbounce/...');
566
+ fs.mkdirSync(path.join(CWD, '.vbounce'), { recursive: true });
567
+
568
+ // Move framework directories
569
+ for (const p of oldPaths) {
570
+ const oldPath = path.join(CWD, p.old);
571
+ const newPath = path.join(CWD, p.new);
572
+ if (fs.existsSync(oldPath) && !fs.existsSync(newPath)) {
573
+ fs.cpSync(oldPath, newPath, { recursive: true });
574
+ fs.rmSync(oldPath, { recursive: true, force: true });
575
+ console.log(` \x1b[32m✓\x1b[0m ${p.old}/ → ${p.new}/`);
576
+ }
577
+ }
578
+
579
+ // Move .bounce/ contents to .vbounce/ (preserve everything)
580
+ const oldBounce = path.join(CWD, '.bounce');
581
+ if (fs.existsSync(oldBounce)) {
582
+ for (const entry of fs.readdirSync(oldBounce, { withFileTypes: true })) {
583
+ const src = path.join(oldBounce, entry.name);
584
+ const dest = path.join(CWD, '.vbounce', entry.name);
585
+ if (!fs.existsSync(dest)) {
586
+ if (entry.isDirectory()) {
587
+ fs.cpSync(src, dest, { recursive: true });
588
+ } else {
589
+ fs.copyFileSync(src, dest);
590
+ }
591
+ }
592
+ }
593
+ fs.rmSync(oldBounce, { recursive: true, force: true });
594
+ console.log(` \x1b[32m✓\x1b[0m .bounce/ → .vbounce/`);
595
+ }
596
+
597
+ // Move old MANIFEST.md if exists
598
+ const oldManifest = path.join(CWD, 'MANIFEST.md');
599
+ if (fs.existsSync(oldManifest)) {
600
+ fs.rmSync(oldManifest, { force: true });
601
+ console.log(` \x1b[32m✓\x1b[0m Removed old MANIFEST.md (replaced by .vbounce/VBOUNCE_MANIFEST.md)`);
602
+ }
603
+
604
+ return true;
605
+ }
606
+
607
+ /** Write install-meta.json. */
608
+ function writeInstallMeta(version, platform, files, hashes) {
609
+ const meta = {
610
+ version,
611
+ platform,
612
+ installed_at: new Date().toISOString(),
613
+ files,
614
+ hashes
615
+ };
616
+ fs.mkdirSync(path.dirname(META_PATH), { recursive: true });
617
+ fs.writeFileSync(META_PATH, JSON.stringify(meta, null, 2) + '\n');
618
+ }
619
+
620
+ /** Backup files to .vbounce/backups/<version>/. Removes previous backup first. */
621
+ function backupFiles(version, paths) {
622
+ // Remove previous backup (keep only one)
623
+ if (fs.existsSync(BACKUPS_DIR)) {
624
+ for (const entry of fs.readdirSync(BACKUPS_DIR, { withFileTypes: true })) {
625
+ if (entry.isDirectory()) {
626
+ fs.rmSync(path.join(BACKUPS_DIR, entry.name), { recursive: true, force: true });
627
+ }
628
+ }
629
+ }
630
+
631
+ const backupDir = path.join(BACKUPS_DIR, version);
632
+ fs.mkdirSync(backupDir, { recursive: true });
633
+
634
+ for (const relPath of paths) {
635
+ const src = path.join(CWD, relPath);
636
+ const dest = path.join(backupDir, relPath);
637
+
638
+ if (!fs.existsSync(src)) continue;
639
+
640
+ const stats = fs.statSync(src);
641
+ if (stats.isDirectory()) {
642
+ fs.mkdirSync(dest, { recursive: true });
643
+ fs.cpSync(src, dest, { recursive: true });
644
+ } else {
645
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
646
+ fs.copyFileSync(src, dest);
647
+ }
648
+ }
649
+
650
+ return backupDir;
651
+ }
652
+
653
+ /**
654
+ * Classify files into unchanged, modified, and newFiles.
655
+ * - unchanged: dest exists and matches what was installed (safe to overwrite)
656
+ * - modified: dest exists but differs from what was installed (user changed it)
657
+ * - newFiles: dest does not exist
658
+ */
659
+ function classifyFiles(mappingRules, meta) {
660
+ const unchanged = [];
661
+ const modified = [];
662
+ const newFiles = [];
663
+
664
+ for (const rule of mappingRules) {
665
+ const sourcePath = path.join(pkgRoot, rule.src);
666
+ const destPath = path.join(CWD, rule.dest);
667
+
668
+ if (!fs.existsSync(sourcePath)) continue;
669
+
670
+ if (!fs.existsSync(destPath)) {
671
+ newFiles.push(rule);
672
+ continue;
673
+ }
674
+
675
+ // Dest exists — classify as unchanged or modified
676
+ if (!meta || !meta.hashes || !meta.hashes[rule.dest]) {
677
+ // No metadata (first upgrade) — treat as modified to be safe
678
+ modified.push(rule);
679
+ continue;
680
+ }
681
+
682
+ const currentHash = computeHash(destPath);
683
+ const installedHash = meta.hashes[rule.dest];
684
+
685
+ if (currentHash === installedHash) {
686
+ unchanged.push(rule);
687
+ } else {
688
+ modified.push(rule);
689
+ }
690
+ }
691
+
692
+ return { unchanged, modified, newFiles };
693
+ }
694
+
695
+ // ---------------------------------------------------------------------------
696
+ // Begin install flow
697
+ // ---------------------------------------------------------------------------
698
+
699
+ const meta = readInstallMeta();
700
+ const isUpgrade = meta !== null;
701
+
702
+ // Migrate from old layout if needed (skills/, templates/, scripts/ at root → .vbounce/)
703
+ const migrated = migrateOldLayout();
704
+
705
+ if (isUpgrade) {
706
+ console.log(`\n🚀 V-Bounce Engine \x1b[36m${pkgVersion}\x1b[0m (upgrading from \x1b[33m${meta.version}\x1b[0m)\n`);
707
+ } else {
708
+ console.log(`\n🚀 Preparing to install V-Bounce Engine \x1b[36m${pkgVersion}\x1b[0m for \x1b[36m${targetPlatform}\x1b[0m...\n`);
709
+ }
710
+
711
+ const { unchanged, modified, newFiles } = classifyFiles(mapping, meta);
712
+
713
+ if (unchanged.length > 0) {
714
+ console.log('Will update (unchanged by you):');
715
+ for (const rule of unchanged) {
716
+ const destPath = path.join(CWD, rule.dest);
717
+ const n = countFiles(destPath);
718
+ const label = n > 1 ? `(${n} files)` : '';
719
+ console.log(` \x1b[32m✓\x1b[0m ${rule.dest} ${label}`);
720
+ }
721
+ }
722
+
723
+ if (modified.length > 0) {
724
+ const backupLabel = isUpgrade ? `.vbounce/backups/${meta.version}/` : '.vbounce/backups/pre-install/';
725
+ console.log(`\nModified by you (backed up to ${backupLabel}):`);
726
+ for (const rule of modified) {
727
+ console.log(` \x1b[33m⚠\x1b[0m ${rule.dest}`);
728
+ }
729
+ }
730
+
731
+ if (newFiles.length > 0) {
732
+ console.log('\nNew in this version:');
733
+ for (const rule of newFiles) {
734
+ console.log(` \x1b[32m+\x1b[0m ${rule.dest}`);
735
+ }
736
+ }
737
+
738
+ if (unchanged.length === 0 && modified.length === 0 && newFiles.length === 0) {
739
+ rl.close();
740
+ console.log('Nothing to install — all source files are missing from the package.');
741
+ process.exit(0);
742
+ }
743
+
744
+ console.log('');
745
+
746
+ askQuestion('Proceed with installation? [y/N] ').then(async answer => {
747
+ rl.close();
748
+ const confirmation = answer.trim().toLowerCase();
749
+
750
+ if (confirmation !== 'y' && confirmation !== 'yes') {
751
+ console.log('\n❌ Installation cancelled.\n');
752
+ process.exit(0);
753
+ }
754
+
755
+ // Backup modified files before overwriting
756
+ if (modified.length > 0) {
757
+ const backupVersion = isUpgrade ? meta.version : 'pre-install';
758
+ const backupDir = backupFiles(backupVersion, modified.map(r => r.dest));
759
+ console.log(`\n📂 Backed up modified files to ${path.relative(CWD, backupDir)}/`);
760
+ }
761
+
762
+ console.log('\n📦 Installing files...');
763
+
764
+ const installedFiles = [];
765
+ const hashes = {};
766
+
767
+ for (const rule of [...unchanged, ...modified, ...newFiles]) {
768
+ const sourcePath = path.join(pkgRoot, rule.src);
769
+ const destPath = path.join(CWD, rule.dest);
770
+
771
+ if (!fs.existsSync(sourcePath)) continue;
772
+
773
+ const stats = fs.statSync(sourcePath);
774
+ if (stats.isDirectory()) {
775
+ fs.mkdirSync(destPath, { recursive: true });
776
+ fs.cpSync(sourcePath, destPath, { recursive: true, force: true });
777
+ } else {
778
+ const destDir = path.dirname(destPath);
779
+ if (!fs.existsSync(destDir)) {
780
+ fs.mkdirSync(destDir, { recursive: true });
781
+ }
782
+ fs.copyFileSync(sourcePath, destPath);
783
+ }
784
+
785
+ // Record hash of what we just installed (from source)
786
+ hashes[rule.dest] = computeHash(sourcePath);
787
+ installedFiles.push(rule.dest);
788
+ console.log(` \x1b[32m✓\x1b[0m ${rule.dest}`);
789
+ }
790
+
791
+ // Create LESSONS.md if missing
792
+ const lessonsPath = path.join(CWD, 'LESSONS.md');
793
+ if (!fs.existsSync(lessonsPath)) {
794
+ fs.writeFileSync(lessonsPath, '# Lessons Learned\n\nProject-specific lessons recorded after each story merge. Read this before writing code.\n');
795
+ console.log(` \x1b[32m✓\x1b[0m LESSONS.md (created)`);
796
+ }
797
+
798
+ // Write install metadata
799
+ writeInstallMeta(pkgVersion, targetPlatform, installedFiles, hashes);
800
+ console.log(` \x1b[32m✓\x1b[0m .vbounce/install-meta.json`);
801
+
802
+ // Deploy .vbounce/.gitignore for mixed committed/runtime content
803
+ const vbounceGitignore = path.join(CWD, '.vbounce', '.gitignore');
804
+ if (!fs.existsSync(vbounceGitignore)) {
805
+ fs.writeFileSync(vbounceGitignore, [
806
+ '# V-Bounce runtime files (not tracked in git)',
807
+ 'state.json',
808
+ 'install-meta.json',
809
+ 'backups/',
810
+ 'reports/',
811
+ 'gate-checks.json',
812
+ 'sprint-context-*',
813
+ 'qa-context-*',
814
+ 'arch-context-*',
815
+ 'product-graph.json',
816
+ 'improvement-*',
817
+ 'trends.md',
818
+ 'scribe-task-*',
819
+ 'sprint-report-*',
820
+ 'context-packs/',
821
+ '',
822
+ '# Archive is committed (audit trail)',
823
+ '!archive/',
824
+ ''
825
+ ].join('\n'));
826
+ console.log(` \x1b[32m✓\x1b[0m .vbounce/.gitignore`);
827
+ }
828
+
829
+ console.log('\n⚙️ Installing dependencies...');
830
+ try {
831
+ const deps = ['js-yaml', 'marked', 'commander'];
832
+ console.log(` Running: npm install ${deps.join(' ')}`);
833
+ execSync(`npm install ${deps.join(' ')}`, { stdio: 'inherit', cwd: CWD });
834
+ console.log(' \x1b[32m✓\x1b[0m Dependencies installed.');
835
+ } catch (err) {
836
+ console.error(' \x1b[31m✖\x1b[0m Failed to install dependencies. You may need to run it manually.');
837
+ }
838
+
839
+ // vdoc integration
840
+ const vdocPlatform = vdocPlatformMap[targetPlatform];
841
+ if (vdocPlatform) {
842
+ const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
843
+ const vdocAnswer = await new Promise(resolve => rl2.question('\n📝 Install vdoc (AI-powered documentation generator)? [y/N] ', resolve));
844
+ rl2.close();
845
+
846
+ if (vdocAnswer.trim().toLowerCase() === 'y' || vdocAnswer.trim().toLowerCase() === 'yes') {
847
+ console.log(`\n📝 Installing vdoc for ${vdocPlatform}...`);
848
+ try {
849
+ execSync(`npx @sandrinio/vdoc install ${vdocPlatform}`, { stdio: 'inherit', cwd: CWD });
850
+ console.log(' \x1b[32m✓\x1b[0m vdoc installed.');
851
+ } catch (err) {
852
+ console.error(` \x1b[31m✖\x1b[0m Failed to install vdoc. Run manually: npx @sandrinio/vdoc install ${vdocPlatform}`);
853
+ }
854
+ } else {
855
+ console.log(`\n Skipped. You can install later: npx @sandrinio/vdoc install ${vdocPlatform}`);
856
+ }
857
+ }
858
+
859
+ // Auto-run doctor to verify installation
860
+ console.log('\n🩺 Running doctor to verify installation...');
861
+ const doctorPath = path.join(CWD, '.vbounce', 'scripts', 'doctor.mjs');
862
+ if (fs.existsSync(doctorPath)) {
863
+ const result = spawnSync(process.execPath, [doctorPath], {
864
+ stdio: 'inherit',
865
+ cwd: CWD
866
+ });
867
+ if (result.status !== 0) {
868
+ console.error('\n \x1b[33m⚠\x1b[0m Doctor reported issues. Review the output above.');
869
+ }
870
+ } else {
871
+ console.log(' \x1b[33m⚠\x1b[0m Doctor script not found — skipping verification.');
872
+ }
873
+
874
+ console.log('\n✅ V-Bounce Engine successfully installed! Welcome to the team.\n');
875
+ });
876
+
877
+ } else {
878
+ // Unknown command fallthrough
879
+ rl.close();
880
+ console.error(`Unknown command: ${command}`);
881
+ displayHelp();
882
+ }