@hanzlaa/rcode 2.1.0 → 2.3.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/CONTRIBUTING.md +138 -0
- package/README.md +83 -19
- package/cli/install.js +687 -80
- package/cli/uninstall.js +8 -0
- package/dist/rcode.js +19777 -0
- package/package.json +17 -4
- package/rihal/DOCS-AUDIT.md +14 -0
- package/rihal/agents/rihal-code-reviewer.md +1 -1
- package/rihal/agents/rihal-codebase-mapper.md +1 -1
- package/rihal/agents/rihal-docs-auditor.md +1 -1
- package/rihal/agents/rihal-edge-case-hunter.md +1 -1
- package/rihal/agents/rihal-executor.md +1 -1
- package/rihal/agents/rihal-hussain-pm.md +1 -0
- package/rihal/agents/rihal-nyquist-auditor.md +1 -1
- package/rihal/agents/rihal-phase-researcher.md +1 -2
- package/rihal/agents/rihal-planner.md +1 -1
- package/rihal/agents/rihal-roadmapper.md +1 -0
- package/rihal/agents/rihal-security-adversary.md +1 -1
- package/rihal/agents/rihal-security-auditor.md +1 -1
- package/rihal/agents/rihal-sprint-checker.md +1 -1
- package/rihal/agents/rihal-verifier.md +1 -1
- package/rihal/bin/lib/roadmap.cjs +2 -3
- package/rihal/bin/rihal-tools.cjs +153 -36
- package/rihal/brain/sources.yaml +7 -4
- package/rihal/commands/audit.md +8 -0
- package/rihal/commands/checkpoint-preview.md +13 -0
- package/rihal/commands/config.md +4 -4
- package/rihal/commands/prfaq.md +15 -0
- package/rihal/commands/settings.md +2 -2
- package/rihal/references/agent-contracts.md +12 -0
- package/rihal/references/karpathy-guidelines-full.md +79 -0
- package/rihal/references/karpathy-guidelines.md +8 -76
- package/rihal/references/model-profile-resolution.md +8 -0
- package/rihal/references/phase-argument-parsing.md +11 -0
- package/rihal/references/revision-loop.md +11 -0
- package/rihal/references/universal-anti-patterns.md +15 -0
- package/rihal/skills/actions/1-analysis/rihal-prfaq/SKILL.md +10 -0
- package/rihal/skills/actions/2-plan/rihal-create-epics-and-stories/SKILL.md +3 -1
- package/rihal/skills/actions/2-plan/rihal-create-milestone/SKILL.md +3 -1
- package/rihal/skills/actions/2-plan/rihal-create-milestone/steps/step-10-complete.md +1 -1
- package/rihal/skills/actions/2-plan/rihal-create-prd/SKILL.md +13 -0
- package/rihal/skills/actions/2-plan/rihal-create-story/SKILL.md +4 -2
- package/rihal/skills/actions/4-implementation/rihal-checkpoint-preview/SKILL.md +10 -0
- package/rihal/skills/actions/4-implementation/rihal-sprint-planning/SKILL.md +3 -1
- package/rihal/skills/agents/hussain-pm/SKILL.md +8 -0
- package/rihal/skills/agents/hussain-sm/SKILL.md +8 -0
- package/rihal/templates/UAT.md +29 -0
- package/rihal/templates/milestone.md +2 -0
- package/rihal/templates/sprint.md +11 -28
- package/rihal/templates/summary.md +30 -0
- package/rihal/templates/verification-report.md +28 -0
- package/rihal/workflows/audit-milestone.md +34 -2
- package/rihal/workflows/audit.md +172 -0
- package/rihal/workflows/autonomous.md +67 -0
- package/rihal/workflows/checkpoint-preview.md +7 -0
- package/rihal/workflows/council.md +3 -1
- package/rihal/workflows/dashboard.md +2 -2
- package/rihal/workflows/debug.md +8 -1
- package/rihal/workflows/diagnose-issues.md +34 -0
- package/rihal/workflows/do.md +47 -3
- package/rihal/workflows/execute-sprint.md +11 -4
- package/rihal/workflows/execute.md +9 -3
- package/rihal/workflows/install.md +2 -2
- package/rihal/workflows/karpathy-audit.md +7 -14
- package/rihal/workflows/pause-work.md +7 -1
- package/rihal/workflows/prfaq.md +7 -0
- package/rihal/workflows/profile-user.md +2 -2
- package/rihal/workflows/progress.md +1 -1
- package/rihal/workflows/settings.md +116 -118
- package/rihal/workflows/sprint-planning.md +39 -8
- package/rihal/workflows/status.md +6 -1
- package/rihal/workflows/ui-phase.md +3 -3
- package/rihal/workflows/update.md +80 -22
- package/rihal/workflows/validate-phase.md +7 -1
- package/rihal/agents/rihal-ui-designer.md +0 -6
- package/rihal/workflows/config.md +0 -105
package/cli/install.js
CHANGED
|
@@ -32,18 +32,22 @@
|
|
|
32
32
|
* .planning/
|
|
33
33
|
* council-sessions/ (empty dir, populated on first council run)
|
|
34
34
|
*
|
|
35
|
-
*
|
|
35
|
+
* Bundled packages (devDeps, inlined by esbuild in dist/rcode.js):
|
|
36
|
+
* picocolors, nanospinner, fast-glob, zod, semver, diff
|
|
36
37
|
*
|
|
37
38
|
* Usage:
|
|
38
39
|
* node cli/install.js [target-project-dir]
|
|
39
40
|
* node cli/install.js --help
|
|
40
41
|
*
|
|
41
42
|
* Flags:
|
|
42
|
-
* --force
|
|
43
|
-
* --yes
|
|
44
|
-
* --user <name>
|
|
45
|
-
* --project <name>
|
|
46
|
-
* --language <lang>
|
|
43
|
+
* --force overwrite existing files without prompting
|
|
44
|
+
* --yes non-interactive, accept defaults
|
|
45
|
+
* --user <name> set user_name in config.yaml (default: $USER)
|
|
46
|
+
* --project <name> set project_name in config.yaml (default: basename of target)
|
|
47
|
+
* --language <lang> set communication_language (default: English)
|
|
48
|
+
* --show-diff print full unified diff for preserved files during update
|
|
49
|
+
* --diff-stat print +N -N summary for preserved files (default on update)
|
|
50
|
+
* --accept-all overwrite all user-modified files with source version
|
|
47
51
|
*/
|
|
48
52
|
|
|
49
53
|
const fs = require('fs');
|
|
@@ -51,9 +55,51 @@ const path = require('path');
|
|
|
51
55
|
const crypto = require('crypto');
|
|
52
56
|
const os = require('os');
|
|
53
57
|
|
|
58
|
+
// Bundled packages — devDeps inlined by esbuild, loaded from node_modules in dev.
|
|
59
|
+
const pc = require('picocolors');
|
|
60
|
+
const { createSpinner } = require('nanospinner');
|
|
61
|
+
const fg = require('fast-glob');
|
|
62
|
+
const { z } = require('zod');
|
|
63
|
+
const semver = require('semver');
|
|
64
|
+
const { createTwoFilesPatch } = require('diff');
|
|
65
|
+
|
|
66
|
+
// Output helpers: always respect NO_COLOR / non-TTY (picocolors handles this).
|
|
67
|
+
const ok = (s) => pc.green('✓') + ' ' + s;
|
|
68
|
+
const fail = (s) => pc.red('✗') + ' ' + s;
|
|
69
|
+
const warn = (s) => pc.yellow('⚠') + ' ' + s;
|
|
70
|
+
const info = (s) => pc.cyan('→') + ' ' + s;
|
|
71
|
+
const dim = (s) => pc.dim(s);
|
|
72
|
+
const bold = (s) => pc.bold(s);
|
|
73
|
+
|
|
54
74
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
55
75
|
const SOURCE_ROOT = path.join(PACKAGE_ROOT, 'rihal');
|
|
56
76
|
|
|
77
|
+
// Zod schema for .rihal/config.yaml validation (#250).
|
|
78
|
+
const ConfigSchema = z.object({
|
|
79
|
+
user_name: z.string().min(1),
|
|
80
|
+
project_name: z.string().min(1),
|
|
81
|
+
communication_language: z.string().default('English'),
|
|
82
|
+
mode: z.enum(['guided', 'yolo'], {
|
|
83
|
+
errorMap: () => ({ message: 'expected "guided" or "yolo"' }),
|
|
84
|
+
}).default('guided'),
|
|
85
|
+
model_profile: z.string().optional(),
|
|
86
|
+
commit_planning: z.boolean().optional(),
|
|
87
|
+
rihal_source_path: z.string().optional(),
|
|
88
|
+
workflow: z.object({
|
|
89
|
+
research_by_default: z.boolean().optional(),
|
|
90
|
+
plan_checker: z.boolean().optional(),
|
|
91
|
+
post_execute_gates: z.boolean().optional(),
|
|
92
|
+
ui_safety_gate: z.boolean().optional(),
|
|
93
|
+
nyquist_validation: z.boolean().optional(),
|
|
94
|
+
}).optional(),
|
|
95
|
+
output: z.object({
|
|
96
|
+
verbose: z.boolean().optional(),
|
|
97
|
+
}).optional(),
|
|
98
|
+
git: z.object({
|
|
99
|
+
branching_strategy: z.string().optional(),
|
|
100
|
+
}).optional(),
|
|
101
|
+
}).passthrough();
|
|
102
|
+
|
|
57
103
|
/**
|
|
58
104
|
* Parse command-line args into a normalized options object.
|
|
59
105
|
*/
|
|
@@ -71,6 +117,19 @@ function parseArgs(argv) {
|
|
|
71
117
|
ide: 'claude', // claude, cursor, gemini (copilot = TODO)
|
|
72
118
|
help: false,
|
|
73
119
|
modules: [], // --module core --module execution or empty = all
|
|
120
|
+
// #189 — planning commit policy. null = ask interactively (or default true under --yes).
|
|
121
|
+
// Set true by --commit-planning, false by --no-commit-planning or --ignore-planning.
|
|
122
|
+
commitPlanning: null,
|
|
123
|
+
// #232 — non-destructive update. Preserves files the user modified after install.
|
|
124
|
+
nonDestructive: false,
|
|
125
|
+
// #232 — force-overwrite always wins.
|
|
126
|
+
forceOverwrite: false,
|
|
127
|
+
// #251 — diff display flags
|
|
128
|
+
showDiff: false,
|
|
129
|
+
diffStat: false,
|
|
130
|
+
acceptAll: false,
|
|
131
|
+
// #252 — skip update-notifier check
|
|
132
|
+
noUpdateCheck: false,
|
|
74
133
|
};
|
|
75
134
|
const positional = [];
|
|
76
135
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -85,6 +144,14 @@ function parseArgs(argv) {
|
|
|
85
144
|
else if (arg === '--mode') opts.mode = argv[++i];
|
|
86
145
|
else if (arg === '--ide') opts.ide = argv[++i];
|
|
87
146
|
else if (arg === '--module') opts.modules.push(argv[++i]);
|
|
147
|
+
else if (arg === '--commit-planning') opts.commitPlanning = true;
|
|
148
|
+
else if (arg === '--no-commit-planning' || arg === '--ignore-planning') opts.commitPlanning = false;
|
|
149
|
+
else if (arg === '--non-destructive') opts.nonDestructive = true;
|
|
150
|
+
else if (arg === '--force-overwrite') opts.forceOverwrite = true;
|
|
151
|
+
else if (arg === '--show-diff') opts.showDiff = true; // #251 full unified diff
|
|
152
|
+
else if (arg === '--diff-stat') opts.diffStat = true; // #251 +N -N summary (default)
|
|
153
|
+
else if (arg === '--accept-all') opts.acceptAll = true; // #251 overwrite all preserved
|
|
154
|
+
else if (arg === '--no-update-check') opts.noUpdateCheck = true; // #252
|
|
88
155
|
else if (!arg.startsWith('--')) positional.push(arg);
|
|
89
156
|
}
|
|
90
157
|
if (positional[0]) {
|
|
@@ -95,6 +162,30 @@ function parseArgs(argv) {
|
|
|
95
162
|
return opts;
|
|
96
163
|
}
|
|
97
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Resolve commit-planning preference — CLI flag wins, then interactive
|
|
167
|
+
* prompt (when TTY + not --yes), else GSD-style default: true.
|
|
168
|
+
* #189.
|
|
169
|
+
*/
|
|
170
|
+
async function resolveCommitPlanning(opts) {
|
|
171
|
+
if (opts.commitPlanning !== null) return opts.commitPlanning;
|
|
172
|
+
if (opts.yes || !process.stdin.isTTY) return true; // non-interactive default
|
|
173
|
+
|
|
174
|
+
const readline = require('readline');
|
|
175
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
176
|
+
const prompt = (q) => new Promise(r => rl.question(q, a => r(a)));
|
|
177
|
+
console.log('');
|
|
178
|
+
console.log('📋 .planning/ holds PRDs, roadmaps, sprints, SUMMARY files.');
|
|
179
|
+
console.log(' Commit them to git, or keep them local?');
|
|
180
|
+
console.log('');
|
|
181
|
+
console.log(' [Y] Commit — collaborators see the same plans (default, recommended)');
|
|
182
|
+
console.log(' [n] Gitignore — planning stays local (good for sensitive PRDs)');
|
|
183
|
+
console.log('');
|
|
184
|
+
const answer = (await prompt(' Commit planning artifacts? [Y/n]: ')).trim().toLowerCase();
|
|
185
|
+
rl.close();
|
|
186
|
+
return !(answer === 'n' || answer === 'no');
|
|
187
|
+
}
|
|
188
|
+
|
|
98
189
|
function printHelp() {
|
|
99
190
|
console.log(`
|
|
100
191
|
Rihal Code installer
|
|
@@ -159,17 +250,32 @@ function getPathsForIde(ide, target) {
|
|
|
159
250
|
}
|
|
160
251
|
|
|
161
252
|
/**
|
|
162
|
-
*
|
|
253
|
+
* Walk a directory and return absolute file paths. Uses fast-glob so
|
|
254
|
+
* symlink cycles are never followed and patterns can be excluded via
|
|
255
|
+
* .rihalignore files (#249).
|
|
163
256
|
*/
|
|
164
|
-
function walkFiles(dir) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
257
|
+
function walkFiles(dir, extraIgnore = []) {
|
|
258
|
+
if (!fs.existsSync(dir)) return [];
|
|
259
|
+
return fg.sync('**/*', {
|
|
260
|
+
cwd: dir,
|
|
261
|
+
dot: true,
|
|
262
|
+
onlyFiles: true,
|
|
263
|
+
followSymbolicLinks: false,
|
|
264
|
+
ignore: extraIgnore,
|
|
265
|
+
}).map((rel) => path.join(dir, rel));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Read .rihalignore patterns from a given root directory.
|
|
270
|
+
* Returns an array of glob-style ignore patterns (same syntax as .gitignore).
|
|
271
|
+
*/
|
|
272
|
+
function readRihalIgnore(root) {
|
|
273
|
+
const ignoreFile = path.join(root, '.rihalignore');
|
|
274
|
+
if (!fs.existsSync(ignoreFile)) return [];
|
|
275
|
+
return fs.readFileSync(ignoreFile, 'utf8')
|
|
276
|
+
.split('\n')
|
|
277
|
+
.map((l) => l.trim())
|
|
278
|
+
.filter((l) => l && !l.startsWith('#'));
|
|
173
279
|
}
|
|
174
280
|
|
|
175
281
|
function sha256(buffer) {
|
|
@@ -284,6 +390,146 @@ function seedStarterPlanning(target, projectName) {
|
|
|
284
390
|
return true;
|
|
285
391
|
}
|
|
286
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Ensure the target project's .gitignore has the rcode-managed block.
|
|
395
|
+
*
|
|
396
|
+
* Idempotent via a sentinel comment line. On first install, appends a block
|
|
397
|
+
* that separates:
|
|
398
|
+
* - installed methodology files (ignored; re-install to refresh)
|
|
399
|
+
* - user's project config, state, and planning artifacts (committable)
|
|
400
|
+
*
|
|
401
|
+
* If the user already has a block (marker present) we leave their customizations
|
|
402
|
+
* alone. This function is best-effort — never throws. A missing .gitignore
|
|
403
|
+
* is created. A read/write error is logged and install continues.
|
|
404
|
+
*
|
|
405
|
+
* Returns: { action: 'created' | 'appended' | 'already-present' | 'skipped-error' }
|
|
406
|
+
*/
|
|
407
|
+
function ensureRcodeGitignore(target, options = {}) {
|
|
408
|
+
const commitPlanning = options.commitPlanning !== false; // default true
|
|
409
|
+
const BEGIN = '# ===== rcode-managed gitignore block (npx @hanzlaa/rcode install) =====';
|
|
410
|
+
const END = '# ===== end rcode-managed gitignore block =====';
|
|
411
|
+
|
|
412
|
+
const lines = [
|
|
413
|
+
'',
|
|
414
|
+
BEGIN,
|
|
415
|
+
'# Added automatically on first rcode install. Idempotent — safe to re-run.',
|
|
416
|
+
'# Edit `commit_planning` in .rihal/config.yaml to flip planning-artifact tracking.',
|
|
417
|
+
'',
|
|
418
|
+
'# Installed methodology files (regenerate with: npx @hanzlaa/rcode install)',
|
|
419
|
+
'.claude/',
|
|
420
|
+
'.rihal/bin/',
|
|
421
|
+
'.rihal/workflows/',
|
|
422
|
+
'.rihal/references/',
|
|
423
|
+
'.rihal/commands/',
|
|
424
|
+
'.rihal/skills/',
|
|
425
|
+
'',
|
|
426
|
+
'# Pulled Rihal brain content (refresh with: rcode brain pull)',
|
|
427
|
+
'.rihal/brain/rihal-github/',
|
|
428
|
+
'.rihal/brain/rihal-docs/',
|
|
429
|
+
'.rihal/brain/best-practices/',
|
|
430
|
+
'',
|
|
431
|
+
'# Runtime noise',
|
|
432
|
+
'.rihal/state.json.lock',
|
|
433
|
+
'.planning/debug/',
|
|
434
|
+
'.planning/_backup/',
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
if (!commitPlanning) {
|
|
438
|
+
lines.push(
|
|
439
|
+
'',
|
|
440
|
+
'# Planning artifacts — kept local (commit_planning: false)',
|
|
441
|
+
'.planning/'
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
lines.push(
|
|
446
|
+
'',
|
|
447
|
+
'# What you DO commit:',
|
|
448
|
+
'# .rihal/config.yaml - project mode/language/profile/commit_planning',
|
|
449
|
+
'# .rihal/state.json - decisions, roadmap pointer, blockers',
|
|
450
|
+
'# .rihal/brain/sources.yaml - brain source manifest',
|
|
451
|
+
commitPlanning
|
|
452
|
+
? '# .planning/ - PRD, roadmap, sprints, SUMMARY.md files'
|
|
453
|
+
: '# (planning artifacts are NOT committed — see commit_planning in config)',
|
|
454
|
+
END,
|
|
455
|
+
''
|
|
456
|
+
);
|
|
457
|
+
const BLOCK = lines.join('\n');
|
|
458
|
+
|
|
459
|
+
const gitignorePath = path.join(target, '.gitignore');
|
|
460
|
+
try {
|
|
461
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
462
|
+
fs.writeFileSync(gitignorePath, BLOCK);
|
|
463
|
+
return { action: 'created' };
|
|
464
|
+
}
|
|
465
|
+
const existing = fs.readFileSync(gitignorePath, 'utf8');
|
|
466
|
+
// Replace existing rcode block using indexOf (regex escaping on the
|
|
467
|
+
// sentinel is fiddly — indexOf is deterministic and easier to audit).
|
|
468
|
+
function spliceBlock(text, newBlock) {
|
|
469
|
+
const start = text.indexOf(BEGIN);
|
|
470
|
+
if (start < 0) return null;
|
|
471
|
+
const endIdx = text.indexOf(END, start);
|
|
472
|
+
if (endIdx < 0) return null;
|
|
473
|
+
let sliceStart = start;
|
|
474
|
+
if (sliceStart > 0 && text[sliceStart - 1] === '\n') sliceStart -= 1;
|
|
475
|
+
let sliceEnd = endIdx + END.length;
|
|
476
|
+
if (text[sliceEnd] === '\n') sliceEnd += 1;
|
|
477
|
+
return text.slice(0, sliceStart) + newBlock + text.slice(sliceEnd);
|
|
478
|
+
}
|
|
479
|
+
if (existing.includes(BEGIN)) {
|
|
480
|
+
const rewritten = spliceBlock(existing, BLOCK);
|
|
481
|
+
if (rewritten !== null && rewritten !== existing) {
|
|
482
|
+
fs.writeFileSync(gitignorePath, rewritten);
|
|
483
|
+
return { action: 'updated' };
|
|
484
|
+
}
|
|
485
|
+
return { action: 'already-present' };
|
|
486
|
+
}
|
|
487
|
+
fs.writeFileSync(gitignorePath, existing + BLOCK);
|
|
488
|
+
return { action: 'appended' };
|
|
489
|
+
} catch (err) {
|
|
490
|
+
return { action: 'skipped-error', error: err.message };
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Install brain scaffold (sources.yaml + README.md) into .rihal/brain/ on target.
|
|
496
|
+
* Actual brain content lands after `brain pull` runs.
|
|
497
|
+
* Closes #188 — previously the package's rihal/brain/sources.yaml was never
|
|
498
|
+
* copied to the target at all, leaving brain pull permanently broken.
|
|
499
|
+
*/
|
|
500
|
+
function installBrainScaffold(packageRoot, target) {
|
|
501
|
+
const srcDir = path.join(packageRoot, 'rihal', 'brain');
|
|
502
|
+
const destDir = path.join(target, '.rihal', 'brain');
|
|
503
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
504
|
+
let copied = 0;
|
|
505
|
+
for (const name of ['sources.yaml', 'README.md']) {
|
|
506
|
+
const src = path.join(srcDir, name);
|
|
507
|
+
const dest = path.join(destDir, name);
|
|
508
|
+
if (fs.existsSync(src) && !fs.existsSync(dest)) {
|
|
509
|
+
fs.copyFileSync(src, dest);
|
|
510
|
+
copied++;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// Also pre-seed the best-practices subfolder from the package's
|
|
514
|
+
// rihal/skills/_shared/ so a fresh install has working brain content
|
|
515
|
+
// immediately, even before brain pull runs against real upstream URLs.
|
|
516
|
+
const sharedSrc = path.join(packageRoot, 'rihal', 'skills', '_shared');
|
|
517
|
+
if (fs.existsSync(sharedSrc)) {
|
|
518
|
+
const bpDest = path.join(destDir, 'best-practices');
|
|
519
|
+
fs.mkdirSync(bpDest, { recursive: true });
|
|
520
|
+
for (const entry of fs.readdirSync(sharedSrc, { withFileTypes: true })) {
|
|
521
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
522
|
+
const dest = path.join(bpDest, entry.name);
|
|
523
|
+
if (!fs.existsSync(dest)) {
|
|
524
|
+
fs.copyFileSync(path.join(sharedSrc, entry.name), dest);
|
|
525
|
+
copied++;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return copied;
|
|
531
|
+
}
|
|
532
|
+
|
|
287
533
|
/**
|
|
288
534
|
* Install v1-style skills into the target project.
|
|
289
535
|
*
|
|
@@ -575,6 +821,67 @@ function generateFilesManifest(plan, target) {
|
|
|
575
821
|
return rows.map((r) => r.join(',')).join('\n') + '\n';
|
|
576
822
|
}
|
|
577
823
|
|
|
824
|
+
/**
|
|
825
|
+
* Orphan sweep — remove files that were part of a previous install but aren't
|
|
826
|
+
* in the current plan. Reads `.rihal/_config/files-manifest.csv` from the
|
|
827
|
+
* previous install and computes the diff against the new plan.
|
|
828
|
+
*
|
|
829
|
+
* Closes #196 — without this, upgrading rcode leaves stale skill/command
|
|
830
|
+
* files around that show up as ghost slash commands in the IDE.
|
|
831
|
+
*
|
|
832
|
+
* Deliberately conservative:
|
|
833
|
+
* - Only removes files that appeared in the PREVIOUS manifest.
|
|
834
|
+
* - Never removes files the user created themselves.
|
|
835
|
+
* - Never touches .rihal/config.yaml, .rihal/state.json, or .planning/.
|
|
836
|
+
*
|
|
837
|
+
* Returns the number of orphan files removed.
|
|
838
|
+
*/
|
|
839
|
+
function sweepStaleInstalledFiles(target, newPlan) {
|
|
840
|
+
const manifestPath = path.join(target, '.rihal', '_config', 'files-manifest.csv');
|
|
841
|
+
if (!fs.existsSync(manifestPath)) return 0;
|
|
842
|
+
|
|
843
|
+
let oldRels;
|
|
844
|
+
try {
|
|
845
|
+
const rows = fs.readFileSync(manifestPath, 'utf8').split('\n').slice(1).filter(Boolean);
|
|
846
|
+
oldRels = rows.map(r => r.split(',')[0]).filter(Boolean);
|
|
847
|
+
} catch {
|
|
848
|
+
return 0;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const newRelsSet = new Set(newPlan.map(e => e.rel.split(path.sep).join('/')));
|
|
852
|
+
// Safety — never sweep these, even if they somehow landed in the manifest.
|
|
853
|
+
const neverSweep = /^(\.rihal\/config\.yaml|\.rihal\/state\.json|\.rihal\/state\.json\.lock|\.planning\/|\.rihal\/brain\/sources\.yaml)/;
|
|
854
|
+
|
|
855
|
+
let removed = 0;
|
|
856
|
+
const emptyCandidateDirs = new Set();
|
|
857
|
+
for (const rel of oldRels) {
|
|
858
|
+
if (newRelsSet.has(rel)) continue;
|
|
859
|
+
if (neverSweep.test(rel)) continue;
|
|
860
|
+
const full = path.join(target, rel);
|
|
861
|
+
try {
|
|
862
|
+
if (fs.existsSync(full)) {
|
|
863
|
+
fs.rmSync(full, { force: true });
|
|
864
|
+
emptyCandidateDirs.add(path.dirname(full));
|
|
865
|
+
removed += 1;
|
|
866
|
+
}
|
|
867
|
+
} catch {
|
|
868
|
+
// ignore individual failures — sweep is best-effort
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Remove any now-empty parent dirs (bottom-up, so nested emptiness cascades).
|
|
873
|
+
const dirsSortedDeep = Array.from(emptyCandidateDirs).sort((a, b) => b.length - a.length);
|
|
874
|
+
for (const dir of dirsSortedDeep) {
|
|
875
|
+
try {
|
|
876
|
+
if (fs.existsSync(dir) && fs.readdirSync(dir).length === 0) {
|
|
877
|
+
fs.rmdirSync(dir);
|
|
878
|
+
}
|
|
879
|
+
} catch {}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return removed;
|
|
883
|
+
}
|
|
884
|
+
|
|
578
885
|
function readPackageVersion() {
|
|
579
886
|
try {
|
|
580
887
|
const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'));
|
|
@@ -587,13 +894,18 @@ function readPackageVersion() {
|
|
|
587
894
|
function generateInstallManifest(opts) {
|
|
588
895
|
const version = readPackageVersion();
|
|
589
896
|
const newModules = opts.modules.length > 0 ? opts.modules : listAvailableModules();
|
|
590
|
-
// Merge with existing manifest if present
|
|
897
|
+
// Merge with existing manifest if present; capture previous_version for rollback (#253).
|
|
591
898
|
let existingModules = [];
|
|
899
|
+
let previousVersion = null;
|
|
592
900
|
const existingPath = path.join(opts.target, '.rihal', '_config', 'manifest.yaml');
|
|
593
901
|
if (fs.existsSync(existingPath)) {
|
|
594
902
|
const text = fs.readFileSync(existingPath, 'utf8');
|
|
595
903
|
let inModules = false;
|
|
596
904
|
for (const line of text.split('\n')) {
|
|
905
|
+
if (line.startsWith('version:')) {
|
|
906
|
+
const v = line.replace('version:', '').trim();
|
|
907
|
+
if (semver.valid(v) && v !== version) previousVersion = v;
|
|
908
|
+
}
|
|
597
909
|
if (line.startsWith('modules:')) { inModules = true; continue; }
|
|
598
910
|
if (inModules && line.trim().startsWith('-')) { existingModules.push(line.trim().slice(1).trim()); }
|
|
599
911
|
else if (inModules && !line.startsWith(' ')) { inModules = false; }
|
|
@@ -601,16 +913,14 @@ function generateInstallManifest(opts) {
|
|
|
601
913
|
}
|
|
602
914
|
const allModules = [...new Set([...existingModules, ...newModules])];
|
|
603
915
|
const moduleLines = allModules.map((m) => ` - ${m}`).join('\n');
|
|
604
|
-
|
|
916
|
+
const lines = [
|
|
605
917
|
'# Rihal v2 install manifest',
|
|
606
918
|
`version: ${version}`,
|
|
607
919
|
`installDate: ${new Date().toISOString()}`,
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
'',
|
|
613
|
-
].join('\n');
|
|
920
|
+
];
|
|
921
|
+
if (previousVersion) lines.push(`previous_version: ${previousVersion}`);
|
|
922
|
+
lines.push('modules:', moduleLines, 'ides:', ' - claude-code', '');
|
|
923
|
+
return lines.join('\n');
|
|
614
924
|
}
|
|
615
925
|
|
|
616
926
|
function sanitizeYamlValue(val) {
|
|
@@ -626,6 +936,7 @@ function generateConfigYaml(opts) {
|
|
|
626
936
|
`communication_language: "${sanitizeYamlValue(opts.language)}"`,
|
|
627
937
|
`mode: "${sanitizeYamlValue(opts.mode)}"`,
|
|
628
938
|
`model_profile: "balanced"`,
|
|
939
|
+
`commit_planning: ${opts.commitPlanning !== false}`,
|
|
629
940
|
`rihal_source_path: "${sanitizeYamlValue(path.dirname(path.dirname(process.argv[1])))}/"`,
|
|
630
941
|
'workflow:',
|
|
631
942
|
' research_by_default: false',
|
|
@@ -638,6 +949,57 @@ function generateConfigYaml(opts) {
|
|
|
638
949
|
].join('\n');
|
|
639
950
|
}
|
|
640
951
|
|
|
952
|
+
/**
|
|
953
|
+
* Validate a parsed config.yaml object against ConfigSchema (#250).
|
|
954
|
+
* Returns { valid: true } or { valid: false, errors: string[] }.
|
|
955
|
+
*/
|
|
956
|
+
function validateConfig(data) {
|
|
957
|
+
const result = ConfigSchema.safeParse(data);
|
|
958
|
+
if (result.success) return { valid: true };
|
|
959
|
+
const errors = result.error.issues.map((issue) => {
|
|
960
|
+
const field = issue.path.join('.');
|
|
961
|
+
return ` ${field || '(root)'}: ${issue.message}`;
|
|
962
|
+
});
|
|
963
|
+
return { valid: false, errors };
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Parse a minimal YAML key:value file into a plain object.
|
|
968
|
+
* Only handles scalar values — sufficient for config.yaml.
|
|
969
|
+
*/
|
|
970
|
+
function parseSimpleYaml(text) {
|
|
971
|
+
const obj = {};
|
|
972
|
+
let currentParent = null;
|
|
973
|
+
for (const raw of text.split('\n')) {
|
|
974
|
+
const line = raw.replace(/#.*$/, '');
|
|
975
|
+
if (!line.trim()) continue;
|
|
976
|
+
const indent = line.match(/^(\s*)/)[1].length;
|
|
977
|
+
if (indent === 0) {
|
|
978
|
+
const colonAt = line.indexOf(':');
|
|
979
|
+
if (colonAt === -1) continue;
|
|
980
|
+
const key = line.slice(0, colonAt).trim();
|
|
981
|
+
let val = line.slice(colonAt + 1).trim();
|
|
982
|
+
if (val === '') { currentParent = key; obj[key] = {}; continue; }
|
|
983
|
+
currentParent = null;
|
|
984
|
+
if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
|
|
985
|
+
if (val.startsWith("'") && val.endsWith("'")) val = val.slice(1, -1);
|
|
986
|
+
if (val === 'true') val = true;
|
|
987
|
+
else if (val === 'false') val = false;
|
|
988
|
+
obj[key] = val;
|
|
989
|
+
} else if (currentParent && indent > 0) {
|
|
990
|
+
const colonAt = line.indexOf(':');
|
|
991
|
+
if (colonAt === -1) continue;
|
|
992
|
+
const key = line.slice(0, colonAt).trim();
|
|
993
|
+
let val = line.slice(colonAt + 1).trim();
|
|
994
|
+
if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
|
|
995
|
+
if (val === 'true') val = true;
|
|
996
|
+
else if (val === 'false') val = false;
|
|
997
|
+
obj[currentParent][key] = val;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return obj;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
641
1003
|
/**
|
|
642
1004
|
* Convert a markdown command/agent file to Cursor's .mdc format.
|
|
643
1005
|
* Wraps the file with Cursor-specific rules frontmatter.
|
|
@@ -652,18 +1014,56 @@ function convertToCursorMdc(sourceText) {
|
|
|
652
1014
|
/**
|
|
653
1015
|
* Main install routine. Copies files, generates manifests, writes config.
|
|
654
1016
|
*/
|
|
655
|
-
function install(opts) {
|
|
1017
|
+
async function install(opts) {
|
|
656
1018
|
if (opts.help) { printHelp(); return 0; }
|
|
657
1019
|
|
|
658
|
-
|
|
1020
|
+
// Resolve commit-planning preference (interactive prompt or flag) — #189.
|
|
1021
|
+
opts.commitPlanning = await resolveCommitPlanning(opts);
|
|
1022
|
+
|
|
1023
|
+
const pkgVersion = readPackageVersion();
|
|
1024
|
+
console.log(`\n🕌 ${bold('Rihal Code')} ${pc.cyan('v' + pkgVersion)} ${dim('→')} ${opts.target}`);
|
|
1025
|
+
|
|
1026
|
+
// Detect an existing install and surface it (#195).
|
|
1027
|
+
const existingManifestPath = path.join(opts.target, '.rihal', '_config', 'manifest.yaml');
|
|
1028
|
+
if (fs.existsSync(existingManifestPath)) {
|
|
1029
|
+
const m = fs.readFileSync(existingManifestPath, 'utf8').match(/^version:\s*(.+)$/m);
|
|
1030
|
+
const existingVersion = m ? m[1].trim() : 'unknown';
|
|
1031
|
+
const isUpgrade = semver.valid(existingVersion) && semver.valid(pkgVersion)
|
|
1032
|
+
? semver.lt(existingVersion, pkgVersion)
|
|
1033
|
+
: existingVersion !== pkgVersion;
|
|
1034
|
+
if (isUpgrade) {
|
|
1035
|
+
console.log(' ' + info(`Upgrading ${pc.dim('v' + existingVersion)} → ${pc.green('v' + pkgVersion)} (config + state + .planning preserved)`));
|
|
1036
|
+
} else {
|
|
1037
|
+
console.log(' ' + info(`Refreshing v${existingVersion} (config + state + .planning preserved)`));
|
|
1038
|
+
}
|
|
1039
|
+
if (!opts.force) {
|
|
1040
|
+
console.log(dim(' Pass --force to also sweep orphaned files from the previous version.'));
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
659
1043
|
if (!fs.existsSync(SOURCE_ROOT)) {
|
|
660
1044
|
console.error(`✖ Source tree not found at ${SOURCE_ROOT}. Running from wrong dir?`);
|
|
661
1045
|
return 1;
|
|
662
1046
|
}
|
|
663
1047
|
|
|
664
|
-
// Validate IDE
|
|
1048
|
+
// Validate IDE — structured error for unsupported editors (#197).
|
|
665
1049
|
if (!['claude', 'cursor', 'gemini'].includes(opts.ide)) {
|
|
666
|
-
console.error(`✖
|
|
1050
|
+
console.error(`✖ --ide ${opts.ide} is not supported in v${readPackageVersion()}.`);
|
|
1051
|
+
console.error('');
|
|
1052
|
+
console.error(' Currently supported:');
|
|
1053
|
+
console.error(' claude — Claude Code native (recommended)');
|
|
1054
|
+
console.error(' cursor — Cursor IDE');
|
|
1055
|
+
console.error(' gemini — Gemini CLI');
|
|
1056
|
+
console.error('');
|
|
1057
|
+
console.error(' Tracked for v3.0 (see issue #182):');
|
|
1058
|
+
console.error(' vscode — VS Code native extension');
|
|
1059
|
+
console.error(' jetbrains — IntelliJ / PyCharm');
|
|
1060
|
+
console.error(' zed — Zed editor');
|
|
1061
|
+
console.error('');
|
|
1062
|
+
if (/^(vscode|vs-code|code)$/i.test(opts.ide)) {
|
|
1063
|
+
console.error(' Workaround: if you use VS Code WITH the Claude Code extension,');
|
|
1064
|
+
console.error(' run `--ide claude` — the extension reads from .claude/ too.');
|
|
1065
|
+
console.error('');
|
|
1066
|
+
}
|
|
667
1067
|
return 1;
|
|
668
1068
|
}
|
|
669
1069
|
|
|
@@ -698,46 +1098,103 @@ function install(opts) {
|
|
|
698
1098
|
console.log(` Modules: ${opts.modules.join(', ')}`);
|
|
699
1099
|
}
|
|
700
1100
|
|
|
701
|
-
//
|
|
1101
|
+
// Orphan sweep — remove files from previous install not in the new plan (#196).
|
|
1102
|
+
// Runs on --force only, to preserve user-edited or hand-dropped files on regular installs.
|
|
1103
|
+
let sweptOrphans = 0;
|
|
1104
|
+
if (opts.force) {
|
|
1105
|
+
sweptOrphans = sweepStaleInstalledFiles(opts.target, plan);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Load previous manifest for non-destructive mode (#232).
|
|
1109
|
+
// Map<rel, expectedHashFromPriorInstall> — if a file's current hash matches
|
|
1110
|
+
// its expected-from-prior-install hash, the user hasn't touched it → safe
|
|
1111
|
+
// to overwrite. If hashes differ, user customized it → preserve.
|
|
1112
|
+
const priorManifest = new Map();
|
|
1113
|
+
if (opts.nonDestructive) {
|
|
1114
|
+
const manifestPath = path.join(opts.target, '.rihal', '_config', 'files-manifest.csv');
|
|
1115
|
+
if (fs.existsSync(manifestPath)) {
|
|
1116
|
+
try {
|
|
1117
|
+
const lines = fs.readFileSync(manifestPath, 'utf8').split('\n').slice(1).filter(Boolean);
|
|
1118
|
+
for (const line of lines) {
|
|
1119
|
+
const [rel, hash] = line.split(',');
|
|
1120
|
+
if (rel && hash) priorManifest.set(rel, hash);
|
|
1121
|
+
}
|
|
1122
|
+
} catch {
|
|
1123
|
+
// best-effort — if manifest is malformed, fall back to behaving like fresh install
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Copy files — spinner gives feedback on long installs (#248).
|
|
702
1129
|
let copied = 0;
|
|
703
1130
|
let skipped = 0;
|
|
1131
|
+
let preserved = 0;
|
|
1132
|
+
const preservedFiles = [];
|
|
1133
|
+
const preservedDiffs = []; // { rel, insertions, deletions, patch } for #251
|
|
1134
|
+
const spinner = createSpinner(dim(`Installing ${plan.length} files…`), { color: 'cyan' }).start();
|
|
1135
|
+
|
|
704
1136
|
for (const entry of plan) {
|
|
705
1137
|
const destPath = path.join(opts.target, entry.rel);
|
|
1138
|
+
const relForward = entry.rel.split(path.sep).join('/');
|
|
706
1139
|
ensureDir(path.dirname(destPath));
|
|
707
|
-
|
|
1140
|
+
|
|
1141
|
+
// Non-destructive guard (#232): preserve user-modified files.
|
|
1142
|
+
// --accept-all (#251) overrides: treat all files as pristine.
|
|
1143
|
+
if (opts.nonDestructive && !opts.forceOverwrite && !opts.acceptAll && fs.existsSync(destPath)) {
|
|
1144
|
+
const priorHash = priorManifest.get(relForward);
|
|
1145
|
+
if (priorHash) {
|
|
1146
|
+
const installedContent = fs.readFileSync(destPath, 'utf8');
|
|
1147
|
+
const currentHash = sha256(Buffer.from(installedContent));
|
|
1148
|
+
if (currentHash !== priorHash) {
|
|
1149
|
+
// Compute diff stat for display (#251)
|
|
1150
|
+
const srcContent = fs.readFileSync(entry.src, 'utf8');
|
|
1151
|
+
const patch = createTwoFilesPatch(relForward, relForward, installedContent, srcContent, 'installed', 'source');
|
|
1152
|
+
let ins = 0, del = 0;
|
|
1153
|
+
for (const line of patch.split('\n')) {
|
|
1154
|
+
if (line.startsWith('+') && !line.startsWith('+++')) ins++;
|
|
1155
|
+
if (line.startsWith('-') && !line.startsWith('---')) del++;
|
|
1156
|
+
}
|
|
1157
|
+
preserved += 1;
|
|
1158
|
+
preservedFiles.push(relForward);
|
|
1159
|
+
preservedDiffs.push({ rel: relForward, insertions: ins, deletions: del, patch });
|
|
1160
|
+
skipped += 1;
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
// Hash matches prior install → pristine → safe to overwrite
|
|
1164
|
+
}
|
|
1165
|
+
// No prior hash → new file in this plan → install normally
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
if (fs.existsSync(destPath) && !opts.force && !opts.forceOverwrite) {
|
|
708
1169
|
const existingHash = sha256(fs.readFileSync(destPath));
|
|
709
1170
|
const sourceHash = sha256(fs.readFileSync(entry.src));
|
|
710
1171
|
if (existingHash === sourceHash) { skipped++; continue; }
|
|
711
|
-
if (!opts.yes) {
|
|
712
|
-
|
|
1172
|
+
if (!opts.yes && !opts.nonDestructive) {
|
|
1173
|
+
spinner.stop();
|
|
1174
|
+
console.warn(' ' + warn(`${entry.rel} differs from package version — use --force-overwrite to overwrite`));
|
|
1175
|
+
spinner.start();
|
|
713
1176
|
skipped++;
|
|
714
1177
|
continue;
|
|
715
1178
|
}
|
|
716
1179
|
}
|
|
717
1180
|
|
|
718
|
-
|
|
719
|
-
if (fs.existsSync(destPath) && opts.force) {
|
|
1181
|
+
if (fs.existsSync(destPath) && opts.forceOverwrite) {
|
|
720
1182
|
const existing = fs.readFileSync(destPath);
|
|
721
1183
|
const incoming = fs.readFileSync(entry.src);
|
|
722
1184
|
if (!existing.equals(incoming)) {
|
|
723
|
-
|
|
1185
|
+
spinner.update({ text: dim(`overwriting ${entry.rel}`) });
|
|
724
1186
|
}
|
|
725
1187
|
}
|
|
726
1188
|
|
|
727
|
-
// Read source file
|
|
728
1189
|
let content = fs.readFileSync(entry.src, 'utf8');
|
|
729
|
-
|
|
730
|
-
// Convert to Cursor .mdc format if needed
|
|
731
|
-
if (entry.cursor) {
|
|
732
|
-
content = convertToCursorMdc(content);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// Write to destination
|
|
1190
|
+
if (entry.cursor) content = convertToCursorMdc(content);
|
|
736
1191
|
fs.writeFileSync(destPath, content, 'utf8');
|
|
737
1192
|
if (entry.executable) fs.chmodSync(destPath, 0o755);
|
|
738
1193
|
copied++;
|
|
739
1194
|
}
|
|
740
1195
|
|
|
1196
|
+
spinner.success({ text: ok(`${copied} files installed`) });
|
|
1197
|
+
|
|
741
1198
|
// Write .rihal/_config/manifest.yaml + agent-manifest.csv + files-manifest.csv
|
|
742
1199
|
const configDir = path.join(opts.target, '.rihal', '_config');
|
|
743
1200
|
ensureDir(configDir);
|
|
@@ -765,6 +1222,18 @@ function install(opts) {
|
|
|
765
1222
|
if (!fs.existsSync(configPath)) {
|
|
766
1223
|
fs.writeFileSync(configPath, generateConfigYaml(opts));
|
|
767
1224
|
}
|
|
1225
|
+
// Validate config.yaml with zod schema (#250) — warn but never block install.
|
|
1226
|
+
try {
|
|
1227
|
+
const configText = fs.readFileSync(configPath, 'utf8');
|
|
1228
|
+
const configData = parseSimpleYaml(configText);
|
|
1229
|
+
const validation = validateConfig(configData);
|
|
1230
|
+
if (!validation.valid) {
|
|
1231
|
+
console.log('');
|
|
1232
|
+
console.log(' ' + warn('config.yaml has validation errors:'));
|
|
1233
|
+
for (const e of validation.errors) console.log(pc.yellow(e));
|
|
1234
|
+
console.log(dim(' → Edit .rihal/config.yaml to fix, then run /rihal:status'));
|
|
1235
|
+
}
|
|
1236
|
+
} catch { /* best-effort */ }
|
|
768
1237
|
|
|
769
1238
|
// Seed .rihal/state.json (skip if already exists — don't overwrite on re-install unless --reset)
|
|
770
1239
|
if (!fs.existsSync(stateDest)) {
|
|
@@ -801,6 +1270,14 @@ function install(opts) {
|
|
|
801
1270
|
// Seed .planning/ with starter ROADMAP + STATE so workflows work immediately
|
|
802
1271
|
const starterSeeded = seedStarterPlanning(opts.target, opts.projectName);
|
|
803
1272
|
|
|
1273
|
+
// Install brain scaffolding at .rihal/brain/ (sources.yaml + README).
|
|
1274
|
+
// Actual brain content lands after first brain pull runs.
|
|
1275
|
+
installBrainScaffold(PACKAGE_ROOT, opts.target);
|
|
1276
|
+
|
|
1277
|
+
// Ensure .gitignore separates installed methodology from committable artifacts.
|
|
1278
|
+
// Reads opts.commitPlanning to decide whether .planning/ is in the ignore block.
|
|
1279
|
+
const gitignoreReport = ensureRcodeGitignore(opts.target, { commitPlanning: opts.commitPlanning });
|
|
1280
|
+
|
|
804
1281
|
// Pull Rihal brain content (v2.0 — issue #158).
|
|
805
1282
|
// Runs rihal-tools brain pull as a child process. Placeholder URLs
|
|
806
1283
|
// are skipped gracefully so this does not fail a fresh install.
|
|
@@ -823,53 +1300,185 @@ function install(opts) {
|
|
|
823
1300
|
|
|
824
1301
|
// Summary
|
|
825
1302
|
console.log('');
|
|
826
|
-
console.log(
|
|
827
|
-
if (
|
|
828
|
-
console.log(
|
|
1303
|
+
if (opts.force && sweptOrphans > 0) console.log(' ' + info(`${sweptOrphans} stale files swept`));
|
|
1304
|
+
if (opts.force && existedBefore) {
|
|
1305
|
+
console.log(' ' + warn('config.yaml and state.json preserved (pass --reset to wipe)'));
|
|
829
1306
|
}
|
|
830
1307
|
if (brainReport && brainReport.ok) {
|
|
831
1308
|
const pulledCount = (brainReport.pulled || []).length;
|
|
832
1309
|
const skippedCount = (brainReport.skipped || []).length;
|
|
833
|
-
console.log(`
|
|
834
|
-
(skippedCount ? `, ${skippedCount} skipped (placeholder URLs
|
|
1310
|
+
console.log(' ' + ok(`Brain: ${pulledCount} source${pulledCount === 1 ? '' : 's'} pulled` +
|
|
1311
|
+
(skippedCount ? `, ${skippedCount} skipped (placeholder URLs)` : '')));
|
|
835
1312
|
} else if (brainReport && brainReport.error) {
|
|
836
|
-
console.log(`
|
|
1313
|
+
console.log(' ' + dim(`Brain: skipped (${brainReport.error})`));
|
|
837
1314
|
}
|
|
838
|
-
if (
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1315
|
+
if (gitignoreReport) {
|
|
1316
|
+
const gitMsg = {
|
|
1317
|
+
'created': '.gitignore created with rcode block',
|
|
1318
|
+
'appended': '.gitignore updated — rcode block appended',
|
|
1319
|
+
'already-present': '.gitignore rcode block already present',
|
|
1320
|
+
'updated': '.gitignore rcode block refreshed',
|
|
1321
|
+
'skipped-error': `.gitignore skipped (${gitignoreReport.error})`,
|
|
1322
|
+
}[gitignoreReport.action] || '.gitignore unchanged';
|
|
1323
|
+
console.log(' ' + dim(gitMsg));
|
|
842
1324
|
}
|
|
1325
|
+
if (skipped > 0) console.log(' ' + dim(`${skipped} files skipped (unchanged)`));
|
|
1326
|
+
|
|
1327
|
+
// Diff display for preserved files (#251)
|
|
1328
|
+
if (preserved > 0 && opts.nonDestructive) {
|
|
1329
|
+
console.log('');
|
|
1330
|
+
console.log(' ' + warn(`${preserved} file${preserved === 1 ? '' : 's'} preserved (modified since install):`));
|
|
1331
|
+
for (const d of preservedDiffs.slice(0, 10)) {
|
|
1332
|
+
const stat = pc.green(`+${d.insertions}`) + ' ' + pc.red(`-${d.deletions}`);
|
|
1333
|
+
console.log(` ${dim(d.rel)} ${stat}`);
|
|
1334
|
+
if (opts.showDiff && d.patch) {
|
|
1335
|
+
for (const line of d.patch.split('\n').slice(4)) { // skip file headers
|
|
1336
|
+
if (line.startsWith('+')) process.stdout.write(pc.green(line) + '\n');
|
|
1337
|
+
else if (line.startsWith('-')) process.stdout.write(pc.red(line) + '\n');
|
|
1338
|
+
else if (line.startsWith('@')) process.stdout.write(pc.cyan(line) + '\n');
|
|
1339
|
+
else process.stdout.write(dim(line) + '\n');
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
if (preservedDiffs.length > 10) console.log(dim(` … and ${preservedDiffs.length - 10} more`));
|
|
1344
|
+
console.log(dim(' To overwrite: re-run with --force-overwrite | To see full diffs: --show-diff'));
|
|
1345
|
+
console.log('');
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// Count installed agents + commands dynamically (#190).
|
|
1349
|
+
const agentsDir = path.join(opts.target, '.claude', 'agents');
|
|
1350
|
+
const commandsDir = path.join(opts.target, '.claude', 'commands', 'rihal');
|
|
1351
|
+
let agentCount = 0, commandCount = 0;
|
|
1352
|
+
try {
|
|
1353
|
+
if (fs.existsSync(agentsDir)) {
|
|
1354
|
+
agentCount = fs.readdirSync(agentsDir).filter(f => f.startsWith('rihal-') && f.endsWith('.md')).length;
|
|
1355
|
+
}
|
|
1356
|
+
if (fs.existsSync(commandsDir)) {
|
|
1357
|
+
commandCount = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md')).length;
|
|
1358
|
+
}
|
|
1359
|
+
} catch {}
|
|
1360
|
+
|
|
1361
|
+
const version = readPackageVersion();
|
|
843
1362
|
console.log('');
|
|
844
|
-
console.log(`
|
|
845
|
-
console.log(`
|
|
846
|
-
console.log(`
|
|
847
|
-
console.log(`
|
|
848
|
-
console.log('');
|
|
849
|
-
console.log(' Agents installed (first-class subagents):');
|
|
850
|
-
console.log(' 🧭 rihal-sadiq — Director of Strategy');
|
|
851
|
-
console.log(' 🏗️ rihal-waleed — CTO');
|
|
852
|
-
console.log(' 🛡️ rihal-fatima — QA Lead');
|
|
1363
|
+
console.log(` ${bold('Version:')} ${pc.cyan('@hanzlaa/rcode@' + version)}`);
|
|
1364
|
+
console.log(` ${bold('IDE:')} ${opts.ide}`);
|
|
1365
|
+
console.log(` ${bold('Language:')} ${opts.language} ${dim('(change in .rihal/config.yaml)')}`);
|
|
1366
|
+
console.log(` ${bold('Mode:')} ${opts.mode} ${dim('(guided=confirm at gates, yolo=autonomous)')}`);
|
|
1367
|
+
console.log(` ${bold('Planning:')} ${opts.commitPlanning !== false ? 'committed' : 'gitignored'} ${dim('(flip: rihal-tools gitignore refresh)')}`);
|
|
853
1368
|
console.log('');
|
|
854
|
-
console.log('
|
|
855
|
-
console.log('
|
|
856
|
-
console.log('
|
|
857
|
-
console.log(' /rihal:insert-phase — insert decimal phase for urgent work');
|
|
1369
|
+
console.log(` ${bold('Agents:')} ${pc.green(String(agentCount))} in .claude/agents/`);
|
|
1370
|
+
console.log(` ${bold('Commands:')} ${pc.green(String(commandCount))} slash commands in .claude/commands/rihal/`);
|
|
1371
|
+
if (skillsInstalled > 0) console.log(` ${bold('Skills:')} ${pc.green(String(skillsInstalled))} phrase-activated in .claude/skills/`);
|
|
858
1372
|
console.log('');
|
|
859
1373
|
if (starterSeeded) {
|
|
860
|
-
console.log('
|
|
1374
|
+
console.log(' ' + ok('Starter planning scaffolded in .planning/ (ROADMAP, STATE, PROJECT)'));
|
|
861
1375
|
console.log('');
|
|
862
1376
|
}
|
|
863
|
-
console.log('
|
|
1377
|
+
console.log(` ${bold('Next:')}`);
|
|
864
1378
|
console.log(` cd ${opts.target}`);
|
|
865
|
-
console.log(' claude
|
|
866
|
-
console.log(' /rihal:
|
|
867
|
-
console.log(' /rihal:do
|
|
868
|
-
console.log(' /rihal:council <
|
|
1379
|
+
console.log(' claude # start Claude Code (reload window if already open)');
|
|
1380
|
+
console.log(' /rihal:progress # where you are, what\'s next');
|
|
1381
|
+
console.log(' /rihal:do # interactive command picker');
|
|
1382
|
+
console.log(' /rihal:council <q> # multi-agent strategic answer');
|
|
1383
|
+
console.log('');
|
|
1384
|
+
console.log(dim(' Refresh anytime:'));
|
|
1385
|
+
console.log(dim(' npx @hanzlaa/rcode@latest install # pull the latest rcode + brain'));
|
|
1386
|
+
console.log(dim(` /rihal:update v${version} # pin rcode to a specific version`));
|
|
869
1387
|
console.log('');
|
|
870
|
-
console.log('
|
|
1388
|
+
console.log(' ' + warn('If your IDE is already open, reload the window to refresh skills/commands.'));
|
|
1389
|
+
console.log(dim(' Claude Code / VS Code / Cursor: Cmd+Shift+P → Reload Window'));
|
|
871
1390
|
console.log('');
|
|
872
|
-
|
|
1391
|
+
|
|
1392
|
+
// Lightweight update check (#252) — async background, never blocks install.
|
|
1393
|
+
// Suppressed in non-TTY / CI or when --no-update-check is passed.
|
|
1394
|
+
if (!opts.noUpdateCheck && process.stdout.isTTY && !process.env.CI && !process.env.RIHAL_NO_UPDATE_NOTIFIER) {
|
|
1395
|
+
const { execFile } = require('child_process');
|
|
1396
|
+
execFile('npm', ['view', '@hanzlaa/rcode', 'version', '--json'], { timeout: 4000 }, (err, stdout) => {
|
|
1397
|
+
if (err) return;
|
|
1398
|
+
try {
|
|
1399
|
+
const latest = JSON.parse(stdout.trim());
|
|
1400
|
+
if (semver.valid(latest) && semver.gt(latest, version)) {
|
|
1401
|
+
console.log('');
|
|
1402
|
+
console.log(' ╭──────────────────────────────────────────────────────╮');
|
|
1403
|
+
console.log(` │ ${pc.yellow('Update available:')} ${pc.dim(version)} → ${pc.green(latest)}${' '.repeat(Math.max(0, 20 - version.length - latest.length))} │`);
|
|
1404
|
+
console.log(' │ Run: npx @hanzlaa/rcode@latest install . │');
|
|
1405
|
+
console.log(' ╰──────────────────────────────────────────────────────╯');
|
|
1406
|
+
console.log('');
|
|
1407
|
+
}
|
|
1408
|
+
} catch { /* ignore parse errors */ }
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// Health check — smoke test that the install actually works (#193).
|
|
1413
|
+
const healthPass = runInstallHealthCheck(opts.target, { agentCount, commandCount, skillsInstalled });
|
|
1414
|
+
return healthPass ? 0 : 1;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* Run a 5-point smoke test against the fresh install. Closes #193.
|
|
1419
|
+
* Returns true if all pass, false if any critical check failed.
|
|
1420
|
+
* Prints a clean ✓/✖ line per check.
|
|
1421
|
+
*/
|
|
1422
|
+
function runInstallHealthCheck(target, counts) {
|
|
1423
|
+
console.log(` ${bold('Health check:')}`);
|
|
1424
|
+
const { execFileSync } = require('child_process');
|
|
1425
|
+
let fails = 0;
|
|
1426
|
+
|
|
1427
|
+
function check(label, fn) {
|
|
1428
|
+
try {
|
|
1429
|
+
const out = fn();
|
|
1430
|
+
console.log(` ${ok(label)}${out ? dim(' — ' + out) : ''}`);
|
|
1431
|
+
} catch (err) {
|
|
1432
|
+
fails += 1;
|
|
1433
|
+
console.log(` ${fail(label)} ${pc.red('—')} ${String(err.message || err).slice(0, 120)}`);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
check('rihal-tools.cjs runs', () => {
|
|
1438
|
+
const toolsPath = path.join(target, '.rihal', 'bin', 'rihal-tools.cjs');
|
|
1439
|
+
if (!fs.existsSync(toolsPath)) throw new Error('bin/rihal-tools.cjs not installed');
|
|
1440
|
+
execFileSync('node', ['-c', toolsPath], { stdio: 'pipe' });
|
|
1441
|
+
return 'syntax ok';
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
check('.rihal/config.yaml present', () => {
|
|
1445
|
+
const p = path.join(target, '.rihal', 'config.yaml');
|
|
1446
|
+
if (!fs.existsSync(p)) throw new Error('missing');
|
|
1447
|
+
const text = fs.readFileSync(p, 'utf8');
|
|
1448
|
+
if (!/user_name:|project_name:/.test(text)) throw new Error('config.yaml incomplete');
|
|
1449
|
+
return `${fs.statSync(p).size} bytes`;
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
check('.rihal/state.json parses', () => {
|
|
1453
|
+
const p = path.join(target, '.rihal', 'state.json');
|
|
1454
|
+
if (!fs.existsSync(p)) throw new Error('missing');
|
|
1455
|
+
JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
1456
|
+
return 'valid JSON';
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
check('agents installed', () => {
|
|
1460
|
+
if ((counts.agentCount || 0) < 20) throw new Error(`only ${counts.agentCount} agents (expected ≥ 20)`);
|
|
1461
|
+
return `${counts.agentCount}`;
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
check('skills + commands installed', () => {
|
|
1465
|
+
const issues = [];
|
|
1466
|
+
if ((counts.skillsInstalled || 0) < 20) issues.push(`${counts.skillsInstalled} skills`);
|
|
1467
|
+
if ((counts.commandCount || 0) < 20) issues.push(`${counts.commandCount} commands`);
|
|
1468
|
+
if (issues.length) throw new Error(`low count: ${issues.join(', ')}`);
|
|
1469
|
+
return `${counts.skillsInstalled} skills + ${counts.commandCount} commands`;
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
if (fails > 0) {
|
|
1473
|
+
console.log('');
|
|
1474
|
+
console.log(' ' + fail(`${fails} health check${fails === 1 ? '' : 's'} failed — install may be broken.`));
|
|
1475
|
+
console.log(dim(' Debug: node .rihal/bin/rihal-tools.cjs state read && ls -la .rihal/'));
|
|
1476
|
+
console.log(dim(' Reinstall: npx @hanzlaa/rcode install . --force'));
|
|
1477
|
+
console.log('');
|
|
1478
|
+
return false;
|
|
1479
|
+
}
|
|
1480
|
+
console.log('');
|
|
1481
|
+
return true;
|
|
873
1482
|
}
|
|
874
1483
|
|
|
875
1484
|
async function main() {
|
|
@@ -909,9 +1518,7 @@ async function main() {
|
|
|
909
1518
|
}
|
|
910
1519
|
}
|
|
911
1520
|
|
|
912
|
-
|
|
913
|
-
process.exit(install(opts));
|
|
914
|
-
} catch (err) {
|
|
1521
|
+
install(opts).then(code => process.exit(code)).catch(err => {
|
|
915
1522
|
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
916
1523
|
console.error(`✖ Permission denied: ${err.path || err.message}`);
|
|
917
1524
|
process.exit(1);
|
|
@@ -923,7 +1530,7 @@ async function main() {
|
|
|
923
1530
|
console.error(`✖ Install failed: ${err.message}`);
|
|
924
1531
|
if (process.env.DEBUG) console.error(err.stack);
|
|
925
1532
|
process.exit(1);
|
|
926
|
-
}
|
|
1533
|
+
});
|
|
927
1534
|
}
|
|
928
1535
|
|
|
929
1536
|
if (require.main === module) main();
|
|
@@ -933,10 +1540,10 @@ if (require.main === module) main();
|
|
|
933
1540
|
* Converts the index.js-style (args, ctx) signature into a cli/install.js
|
|
934
1541
|
* parseArgs-compatible argv and runs install().
|
|
935
1542
|
*/
|
|
936
|
-
function runFromCli(args /* , ctx */) {
|
|
1543
|
+
async function runFromCli(args /* , ctx */) {
|
|
937
1544
|
const argv = Array.isArray(args) ? args : [];
|
|
938
1545
|
const opts = parseArgs(argv);
|
|
939
|
-
const code = install(opts);
|
|
1546
|
+
const code = await install(opts);
|
|
940
1547
|
if (code !== 0) process.exit(code);
|
|
941
1548
|
}
|
|
942
1549
|
|