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.
- package/README.md +142 -0
- package/VBOUNCE_MANIFEST.md +404 -0
- package/bin/vbounce.mjs +882 -0
- package/brains/AGENTS.md +71 -0
- package/brains/CHANGELOG.md +398 -0
- package/brains/CLAUDE.md +90 -0
- package/brains/GEMINI.md +102 -0
- package/brains/SETUP.md +195 -0
- package/brains/claude-agents/architect.md +226 -0
- package/brains/claude-agents/developer.md +133 -0
- package/brains/claude-agents/devops.md +267 -0
- package/brains/claude-agents/explorer.md +157 -0
- package/brains/claude-agents/qa.md +225 -0
- package/brains/claude-agents/scribe.md +171 -0
- package/brains/copilot/copilot-instructions.md +54 -0
- package/brains/cursor-rules/vbounce-docs.mdc +45 -0
- package/brains/cursor-rules/vbounce-process.mdc +51 -0
- package/brains/cursor-rules/vbounce-rules.mdc +29 -0
- package/brains/windsurf/.windsurfrules +35 -0
- package/docs/HOTFIX_EDGE_CASES.md +37 -0
- package/docs/IMPROVEMENT.md +46 -0
- package/docs/agent-skill-profiles.docx +0 -0
- package/docs/icons/alert.svg +1 -0
- package/docs/icons/beaker.svg +1 -0
- package/docs/icons/book.svg +1 -0
- package/docs/icons/git-branch.svg +1 -0
- package/docs/icons/git-merge.svg +1 -0
- package/docs/icons/graph.svg +1 -0
- package/docs/icons/light-bulb.svg +1 -0
- package/docs/icons/logo.svg +9 -0
- package/docs/icons/pencil.svg +1 -0
- package/docs/icons/rocket.svg +1 -0
- package/docs/icons/shield.svg +1 -0
- package/docs/icons/sync.svg +1 -0
- package/docs/icons/terminal.svg +1 -0
- package/docs/icons/tools.svg +1 -0
- package/docs/icons/zap.svg +1 -0
- package/docs/images/bounce_loop_diagram.png +0 -0
- package/docs/vbounce-os-manual.docx +0 -0
- package/package.json +48 -0
- package/scripts/close_sprint.mjs +134 -0
- package/scripts/complete_story.mjs +121 -0
- package/scripts/count_tokens.mjs +494 -0
- package/scripts/doctor.mjs +144 -0
- package/scripts/hotfix_manager.sh +157 -0
- package/scripts/init_gate_config.sh +151 -0
- package/scripts/init_sprint.mjs +129 -0
- package/scripts/post_sprint_improve.mjs +486 -0
- package/scripts/pre_gate_common.sh +576 -0
- package/scripts/pre_gate_runner.sh +176 -0
- package/scripts/prep_arch_context.mjs +178 -0
- package/scripts/prep_qa_context.mjs +152 -0
- package/scripts/prep_sprint_context.mjs +141 -0
- package/scripts/prep_sprint_summary.mjs +154 -0
- package/scripts/product_graph.mjs +387 -0
- package/scripts/product_impact.mjs +167 -0
- package/scripts/sprint_trends.mjs +160 -0
- package/scripts/suggest_improvements.mjs +363 -0
- package/scripts/update_state.mjs +132 -0
- package/scripts/validate_bounce_readiness.mjs +152 -0
- package/scripts/validate_report.mjs +165 -0
- package/scripts/validate_sprint_plan.mjs +117 -0
- package/scripts/validate_state.mjs +99 -0
- package/scripts/vdoc_match.mjs +269 -0
- package/scripts/vdoc_staleness.mjs +199 -0
- package/scripts/verify_framework.mjs +122 -0
- package/scripts/verify_framework.sh +13 -0
- package/skills/agent-team/SKILL.md +579 -0
- package/skills/agent-team/references/cleanup.md +42 -0
- package/skills/agent-team/references/delivery-sync.md +43 -0
- package/skills/agent-team/references/discovery.md +97 -0
- package/skills/agent-team/references/git-strategy.md +52 -0
- package/skills/agent-team/references/mid-sprint-triage.md +85 -0
- package/skills/agent-team/references/report-naming.md +34 -0
- package/skills/doc-manager/SKILL.md +444 -0
- package/skills/file-organization/SKILL.md +146 -0
- package/skills/file-organization/TEST-RESULTS.md +193 -0
- package/skills/file-organization/evals/evals.json +41 -0
- package/skills/file-organization/references/gitignore-template.md +53 -0
- package/skills/file-organization/references/quick-checklist.md +48 -0
- package/skills/improve/SKILL.md +296 -0
- package/skills/lesson/SKILL.md +136 -0
- package/skills/product-graph/SKILL.md +102 -0
- package/skills/react-best-practices/SKILL.md +3014 -0
- package/skills/react-best-practices/rules/_sections.md +46 -0
- package/skills/react-best-practices/rules/_template.md +28 -0
- package/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/skills/react-best-practices/rules/advanced-init-once.md +42 -0
- package/skills/react-best-practices/rules/advanced-use-latest.md +39 -0
- package/skills/react-best-practices/rules/async-api-routes.md +38 -0
- package/skills/react-best-practices/rules/async-defer-await.md +80 -0
- package/skills/react-best-practices/rules/async-dependencies.md +51 -0
- package/skills/react-best-practices/rules/async-parallel.md +28 -0
- package/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/skills/react-best-practices/rules/bundle-conditional.md +31 -0
- package/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/skills/react-best-practices/rules/bundle-preload.md +50 -0
- package/skills/react-best-practices/rules/client-event-listeners.md +74 -0
- package/skills/react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
- package/skills/react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/skills/react-best-practices/rules/js-cache-storage.md +70 -0
- package/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/skills/react-best-practices/rules/js-early-exit.md +50 -0
- package/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/skills/react-best-practices/rules/js-index-maps.md +37 -0
- package/skills/react-best-practices/rules/js-length-check-first.md +49 -0
- package/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/skills/react-best-practices/rules/rendering-activity.md +26 -0
- package/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/skills/react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/skills/react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/skills/react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/skills/react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/skills/react-best-practices/rules/rerender-memo.md +44 -0
- package/skills/react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/skills/react-best-practices/rules/rerender-transitions.md +40 -0
- package/skills/react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/skills/react-best-practices/rules/server-auth-actions.md +96 -0
- package/skills/react-best-practices/rules/server-cache-lru.md +41 -0
- package/skills/react-best-practices/rules/server-cache-react.md +76 -0
- package/skills/react-best-practices/rules/server-dedup-props.md +65 -0
- package/skills/react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/skills/react-best-practices/rules/server-serialization.md +38 -0
- package/skills/vibe-code-review/SKILL.md +70 -0
- package/skills/vibe-code-review/references/deep-audit.md +259 -0
- package/skills/vibe-code-review/references/pr-review.md +234 -0
- package/skills/vibe-code-review/references/quick-scan.md +178 -0
- package/skills/vibe-code-review/references/report-template.md +189 -0
- package/skills/vibe-code-review/references/trend-check.md +224 -0
- package/skills/vibe-code-review/scripts/generate-snapshot.sh +89 -0
- package/skills/vibe-code-review/scripts/pr-analyze.sh +180 -0
- package/skills/write-skill/SKILL.md +133 -0
- package/templates/bug.md +100 -0
- package/templates/change_request.md +105 -0
- package/templates/charter.md +144 -0
- package/templates/delivery_plan.md +44 -0
- package/templates/epic.md +203 -0
- package/templates/hotfix.md +58 -0
- package/templates/risk_registry.md +87 -0
- package/templates/roadmap.md +174 -0
- package/templates/spike.md +143 -0
- package/templates/sprint.md +134 -0
- package/templates/sprint_context.md +61 -0
- package/templates/sprint_report.md +215 -0
- package/templates/story.md +193 -0
package/bin/vbounce.mjs
ADDED
|
@@ -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
|
+
}
|