@hanzlaa/rcode 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/cli/install.js +408 -33
- package/package.json +1 -1
- package/rihal/bin/rihal-tools.cjs +142 -5
- package/rihal/brain/sources.yaml +7 -4
- package/rihal/workflows/dashboard.md +2 -2
- package/rihal/workflows/install.md +2 -2
- package/rihal/workflows/progress.md +1 -1
- package/rihal/workflows/status.md +1 -1
package/README.md
CHANGED
|
@@ -54,22 +54,27 @@ It's not a chatbot. It's a methodology.
|
|
|
54
54
|
|
|
55
55
|
## Install — one command
|
|
56
56
|
|
|
57
|
-
In any project directory:
|
|
57
|
+
In any project directory (existing codebase OR empty folder):
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
60
|
npx @hanzlaa/rcode install
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
[Live on npm](https://www.npmjs.com/package/@hanzlaa/rcode) as `@hanzlaa/rcode` · current version `v2.1.0`. See [`docs/install.md`](docs/install.md) for flavors (module subsets, IDE options, version pinning, yolo mode).
|
|
64
|
+
|
|
65
|
+
One unified installer. Pure file shipping, no runtime dependencies. Installs into:
|
|
64
66
|
|
|
65
67
|
- `.rihal/` — config, workflows, references, bin (Rihal infrastructure)
|
|
66
68
|
- `.claude/agents/` — 44 first-class subagents
|
|
67
69
|
- `.claude/commands/rihal/` — 93 slash commands
|
|
68
70
|
- `.claude/skills/` — 58 phrase-activated skills (scaffold-project, create-prd, retrospective, etc.)
|
|
71
|
+
- `rihal/brain/` — Rihal standards pulled from upstream (PR / commit / architecture docs)
|
|
69
72
|
- `.planning/` — where your artifacts land (council sessions, plans, chains, summaries)
|
|
70
73
|
|
|
71
74
|
Restart Claude Code (or your IDE), type `/`, and every `rihal:*` command appears.
|
|
72
75
|
|
|
76
|
+
Update anytime with `npx @hanzlaa/rcode update` (or `/rihal:update` inside a Claude session).
|
|
77
|
+
|
|
73
78
|
### Then begin the rihla
|
|
74
79
|
|
|
75
80
|
```
|
package/cli/install.js
CHANGED
|
@@ -71,6 +71,9 @@ function parseArgs(argv) {
|
|
|
71
71
|
ide: 'claude', // claude, cursor, gemini (copilot = TODO)
|
|
72
72
|
help: false,
|
|
73
73
|
modules: [], // --module core --module execution or empty = all
|
|
74
|
+
// #189 — planning commit policy. null = ask interactively (or default true under --yes).
|
|
75
|
+
// Set true by --commit-planning, false by --no-commit-planning or --ignore-planning.
|
|
76
|
+
commitPlanning: null,
|
|
74
77
|
};
|
|
75
78
|
const positional = [];
|
|
76
79
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -85,6 +88,8 @@ function parseArgs(argv) {
|
|
|
85
88
|
else if (arg === '--mode') opts.mode = argv[++i];
|
|
86
89
|
else if (arg === '--ide') opts.ide = argv[++i];
|
|
87
90
|
else if (arg === '--module') opts.modules.push(argv[++i]);
|
|
91
|
+
else if (arg === '--commit-planning') opts.commitPlanning = true;
|
|
92
|
+
else if (arg === '--no-commit-planning' || arg === '--ignore-planning') opts.commitPlanning = false;
|
|
88
93
|
else if (!arg.startsWith('--')) positional.push(arg);
|
|
89
94
|
}
|
|
90
95
|
if (positional[0]) {
|
|
@@ -95,6 +100,30 @@ function parseArgs(argv) {
|
|
|
95
100
|
return opts;
|
|
96
101
|
}
|
|
97
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Resolve commit-planning preference — CLI flag wins, then interactive
|
|
105
|
+
* prompt (when TTY + not --yes), else GSD-style default: true.
|
|
106
|
+
* #189.
|
|
107
|
+
*/
|
|
108
|
+
async function resolveCommitPlanning(opts) {
|
|
109
|
+
if (opts.commitPlanning !== null) return opts.commitPlanning;
|
|
110
|
+
if (opts.yes || !process.stdin.isTTY) return true; // non-interactive default
|
|
111
|
+
|
|
112
|
+
const readline = require('readline');
|
|
113
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
114
|
+
const prompt = (q) => new Promise(r => rl.question(q, a => r(a)));
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log('📋 .planning/ holds PRDs, roadmaps, sprints, SUMMARY files.');
|
|
117
|
+
console.log(' Commit them to git, or keep them local?');
|
|
118
|
+
console.log('');
|
|
119
|
+
console.log(' [Y] Commit — collaborators see the same plans (default, recommended)');
|
|
120
|
+
console.log(' [n] Gitignore — planning stays local (good for sensitive PRDs)');
|
|
121
|
+
console.log('');
|
|
122
|
+
const answer = (await prompt(' Commit planning artifacts? [Y/n]: ')).trim().toLowerCase();
|
|
123
|
+
rl.close();
|
|
124
|
+
return !(answer === 'n' || answer === 'no');
|
|
125
|
+
}
|
|
126
|
+
|
|
98
127
|
function printHelp() {
|
|
99
128
|
console.log(`
|
|
100
129
|
Rihal Code installer
|
|
@@ -284,6 +313,146 @@ function seedStarterPlanning(target, projectName) {
|
|
|
284
313
|
return true;
|
|
285
314
|
}
|
|
286
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Ensure the target project's .gitignore has the rcode-managed block.
|
|
318
|
+
*
|
|
319
|
+
* Idempotent via a sentinel comment line. On first install, appends a block
|
|
320
|
+
* that separates:
|
|
321
|
+
* - installed methodology files (ignored; re-install to refresh)
|
|
322
|
+
* - user's project config, state, and planning artifacts (committable)
|
|
323
|
+
*
|
|
324
|
+
* If the user already has a block (marker present) we leave their customizations
|
|
325
|
+
* alone. This function is best-effort — never throws. A missing .gitignore
|
|
326
|
+
* is created. A read/write error is logged and install continues.
|
|
327
|
+
*
|
|
328
|
+
* Returns: { action: 'created' | 'appended' | 'already-present' | 'skipped-error' }
|
|
329
|
+
*/
|
|
330
|
+
function ensureRcodeGitignore(target, options = {}) {
|
|
331
|
+
const commitPlanning = options.commitPlanning !== false; // default true
|
|
332
|
+
const BEGIN = '# ===== rcode-managed gitignore block (npx @hanzlaa/rcode install) =====';
|
|
333
|
+
const END = '# ===== end rcode-managed gitignore block =====';
|
|
334
|
+
|
|
335
|
+
const lines = [
|
|
336
|
+
'',
|
|
337
|
+
BEGIN,
|
|
338
|
+
'# Added automatically on first rcode install. Idempotent — safe to re-run.',
|
|
339
|
+
'# Edit `commit_planning` in .rihal/config.yaml to flip planning-artifact tracking.',
|
|
340
|
+
'',
|
|
341
|
+
'# Installed methodology files (regenerate with: npx @hanzlaa/rcode install)',
|
|
342
|
+
'.claude/',
|
|
343
|
+
'.rihal/bin/',
|
|
344
|
+
'.rihal/workflows/',
|
|
345
|
+
'.rihal/references/',
|
|
346
|
+
'.rihal/commands/',
|
|
347
|
+
'.rihal/skills/',
|
|
348
|
+
'',
|
|
349
|
+
'# Pulled Rihal brain content (refresh with: rcode brain pull)',
|
|
350
|
+
'.rihal/brain/rihal-github/',
|
|
351
|
+
'.rihal/brain/rihal-docs/',
|
|
352
|
+
'.rihal/brain/best-practices/',
|
|
353
|
+
'',
|
|
354
|
+
'# Runtime noise',
|
|
355
|
+
'.rihal/state.json.lock',
|
|
356
|
+
'.planning/debug/',
|
|
357
|
+
'.planning/_backup/',
|
|
358
|
+
];
|
|
359
|
+
|
|
360
|
+
if (!commitPlanning) {
|
|
361
|
+
lines.push(
|
|
362
|
+
'',
|
|
363
|
+
'# Planning artifacts — kept local (commit_planning: false)',
|
|
364
|
+
'.planning/'
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
lines.push(
|
|
369
|
+
'',
|
|
370
|
+
'# What you DO commit:',
|
|
371
|
+
'# .rihal/config.yaml - project mode/language/profile/commit_planning',
|
|
372
|
+
'# .rihal/state.json - decisions, roadmap pointer, blockers',
|
|
373
|
+
'# .rihal/brain/sources.yaml - brain source manifest',
|
|
374
|
+
commitPlanning
|
|
375
|
+
? '# .planning/ - PRD, roadmap, sprints, SUMMARY.md files'
|
|
376
|
+
: '# (planning artifacts are NOT committed — see commit_planning in config)',
|
|
377
|
+
END,
|
|
378
|
+
''
|
|
379
|
+
);
|
|
380
|
+
const BLOCK = lines.join('\n');
|
|
381
|
+
|
|
382
|
+
const gitignorePath = path.join(target, '.gitignore');
|
|
383
|
+
try {
|
|
384
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
385
|
+
fs.writeFileSync(gitignorePath, BLOCK);
|
|
386
|
+
return { action: 'created' };
|
|
387
|
+
}
|
|
388
|
+
const existing = fs.readFileSync(gitignorePath, 'utf8');
|
|
389
|
+
// Replace existing rcode block using indexOf (regex escaping on the
|
|
390
|
+
// sentinel is fiddly — indexOf is deterministic and easier to audit).
|
|
391
|
+
function spliceBlock(text, newBlock) {
|
|
392
|
+
const start = text.indexOf(BEGIN);
|
|
393
|
+
if (start < 0) return null;
|
|
394
|
+
const endIdx = text.indexOf(END, start);
|
|
395
|
+
if (endIdx < 0) return null;
|
|
396
|
+
let sliceStart = start;
|
|
397
|
+
if (sliceStart > 0 && text[sliceStart - 1] === '\n') sliceStart -= 1;
|
|
398
|
+
let sliceEnd = endIdx + END.length;
|
|
399
|
+
if (text[sliceEnd] === '\n') sliceEnd += 1;
|
|
400
|
+
return text.slice(0, sliceStart) + newBlock + text.slice(sliceEnd);
|
|
401
|
+
}
|
|
402
|
+
if (existing.includes(BEGIN)) {
|
|
403
|
+
const rewritten = spliceBlock(existing, BLOCK);
|
|
404
|
+
if (rewritten !== null && rewritten !== existing) {
|
|
405
|
+
fs.writeFileSync(gitignorePath, rewritten);
|
|
406
|
+
return { action: 'updated' };
|
|
407
|
+
}
|
|
408
|
+
return { action: 'already-present' };
|
|
409
|
+
}
|
|
410
|
+
fs.writeFileSync(gitignorePath, existing + BLOCK);
|
|
411
|
+
return { action: 'appended' };
|
|
412
|
+
} catch (err) {
|
|
413
|
+
return { action: 'skipped-error', error: err.message };
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Install brain scaffold (sources.yaml + README.md) into .rihal/brain/ on target.
|
|
419
|
+
* Actual brain content lands after `brain pull` runs.
|
|
420
|
+
* Closes #188 — previously the package's rihal/brain/sources.yaml was never
|
|
421
|
+
* copied to the target at all, leaving brain pull permanently broken.
|
|
422
|
+
*/
|
|
423
|
+
function installBrainScaffold(packageRoot, target) {
|
|
424
|
+
const srcDir = path.join(packageRoot, 'rihal', 'brain');
|
|
425
|
+
const destDir = path.join(target, '.rihal', 'brain');
|
|
426
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
427
|
+
let copied = 0;
|
|
428
|
+
for (const name of ['sources.yaml', 'README.md']) {
|
|
429
|
+
const src = path.join(srcDir, name);
|
|
430
|
+
const dest = path.join(destDir, name);
|
|
431
|
+
if (fs.existsSync(src) && !fs.existsSync(dest)) {
|
|
432
|
+
fs.copyFileSync(src, dest);
|
|
433
|
+
copied++;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// Also pre-seed the best-practices subfolder from the package's
|
|
437
|
+
// rihal/skills/_shared/ so a fresh install has working brain content
|
|
438
|
+
// immediately, even before brain pull runs against real upstream URLs.
|
|
439
|
+
const sharedSrc = path.join(packageRoot, 'rihal', 'skills', '_shared');
|
|
440
|
+
if (fs.existsSync(sharedSrc)) {
|
|
441
|
+
const bpDest = path.join(destDir, 'best-practices');
|
|
442
|
+
fs.mkdirSync(bpDest, { recursive: true });
|
|
443
|
+
for (const entry of fs.readdirSync(sharedSrc, { withFileTypes: true })) {
|
|
444
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
445
|
+
const dest = path.join(bpDest, entry.name);
|
|
446
|
+
if (!fs.existsSync(dest)) {
|
|
447
|
+
fs.copyFileSync(path.join(sharedSrc, entry.name), dest);
|
|
448
|
+
copied++;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return copied;
|
|
454
|
+
}
|
|
455
|
+
|
|
287
456
|
/**
|
|
288
457
|
* Install v1-style skills into the target project.
|
|
289
458
|
*
|
|
@@ -575,6 +744,67 @@ function generateFilesManifest(plan, target) {
|
|
|
575
744
|
return rows.map((r) => r.join(',')).join('\n') + '\n';
|
|
576
745
|
}
|
|
577
746
|
|
|
747
|
+
/**
|
|
748
|
+
* Orphan sweep — remove files that were part of a previous install but aren't
|
|
749
|
+
* in the current plan. Reads `.rihal/_config/files-manifest.csv` from the
|
|
750
|
+
* previous install and computes the diff against the new plan.
|
|
751
|
+
*
|
|
752
|
+
* Closes #196 — without this, upgrading rcode leaves stale skill/command
|
|
753
|
+
* files around that show up as ghost slash commands in the IDE.
|
|
754
|
+
*
|
|
755
|
+
* Deliberately conservative:
|
|
756
|
+
* - Only removes files that appeared in the PREVIOUS manifest.
|
|
757
|
+
* - Never removes files the user created themselves.
|
|
758
|
+
* - Never touches .rihal/config.yaml, .rihal/state.json, or .planning/.
|
|
759
|
+
*
|
|
760
|
+
* Returns the number of orphan files removed.
|
|
761
|
+
*/
|
|
762
|
+
function sweepStaleInstalledFiles(target, newPlan) {
|
|
763
|
+
const manifestPath = path.join(target, '.rihal', '_config', 'files-manifest.csv');
|
|
764
|
+
if (!fs.existsSync(manifestPath)) return 0;
|
|
765
|
+
|
|
766
|
+
let oldRels;
|
|
767
|
+
try {
|
|
768
|
+
const rows = fs.readFileSync(manifestPath, 'utf8').split('\n').slice(1).filter(Boolean);
|
|
769
|
+
oldRels = rows.map(r => r.split(',')[0]).filter(Boolean);
|
|
770
|
+
} catch {
|
|
771
|
+
return 0;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const newRelsSet = new Set(newPlan.map(e => e.rel.split(path.sep).join('/')));
|
|
775
|
+
// Safety — never sweep these, even if they somehow landed in the manifest.
|
|
776
|
+
const neverSweep = /^(\.rihal\/config\.yaml|\.rihal\/state\.json|\.rihal\/state\.json\.lock|\.planning\/|\.rihal\/brain\/sources\.yaml)/;
|
|
777
|
+
|
|
778
|
+
let removed = 0;
|
|
779
|
+
const emptyCandidateDirs = new Set();
|
|
780
|
+
for (const rel of oldRels) {
|
|
781
|
+
if (newRelsSet.has(rel)) continue;
|
|
782
|
+
if (neverSweep.test(rel)) continue;
|
|
783
|
+
const full = path.join(target, rel);
|
|
784
|
+
try {
|
|
785
|
+
if (fs.existsSync(full)) {
|
|
786
|
+
fs.rmSync(full, { force: true });
|
|
787
|
+
emptyCandidateDirs.add(path.dirname(full));
|
|
788
|
+
removed += 1;
|
|
789
|
+
}
|
|
790
|
+
} catch {
|
|
791
|
+
// ignore individual failures — sweep is best-effort
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Remove any now-empty parent dirs (bottom-up, so nested emptiness cascades).
|
|
796
|
+
const dirsSortedDeep = Array.from(emptyCandidateDirs).sort((a, b) => b.length - a.length);
|
|
797
|
+
for (const dir of dirsSortedDeep) {
|
|
798
|
+
try {
|
|
799
|
+
if (fs.existsSync(dir) && fs.readdirSync(dir).length === 0) {
|
|
800
|
+
fs.rmdirSync(dir);
|
|
801
|
+
}
|
|
802
|
+
} catch {}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return removed;
|
|
806
|
+
}
|
|
807
|
+
|
|
578
808
|
function readPackageVersion() {
|
|
579
809
|
try {
|
|
580
810
|
const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'));
|
|
@@ -626,6 +856,7 @@ function generateConfigYaml(opts) {
|
|
|
626
856
|
`communication_language: "${sanitizeYamlValue(opts.language)}"`,
|
|
627
857
|
`mode: "${sanitizeYamlValue(opts.mode)}"`,
|
|
628
858
|
`model_profile: "balanced"`,
|
|
859
|
+
`commit_planning: ${opts.commitPlanning !== false}`,
|
|
629
860
|
`rihal_source_path: "${sanitizeYamlValue(path.dirname(path.dirname(process.argv[1])))}/"`,
|
|
630
861
|
'workflow:',
|
|
631
862
|
' research_by_default: false',
|
|
@@ -652,18 +883,53 @@ function convertToCursorMdc(sourceText) {
|
|
|
652
883
|
/**
|
|
653
884
|
* Main install routine. Copies files, generates manifests, writes config.
|
|
654
885
|
*/
|
|
655
|
-
function install(opts) {
|
|
886
|
+
async function install(opts) {
|
|
656
887
|
if (opts.help) { printHelp(); return 0; }
|
|
657
888
|
|
|
658
|
-
|
|
889
|
+
// Resolve commit-planning preference (interactive prompt or flag) — #189.
|
|
890
|
+
opts.commitPlanning = await resolveCommitPlanning(opts);
|
|
891
|
+
|
|
892
|
+
console.log(`\n🕌 Rihal Code v${readPackageVersion()} installer → ${opts.target}`);
|
|
893
|
+
|
|
894
|
+
// Detect an existing install and surface it (#195).
|
|
895
|
+
const existingManifestPath = path.join(opts.target, '.rihal', '_config', 'manifest.yaml');
|
|
896
|
+
if (fs.existsSync(existingManifestPath)) {
|
|
897
|
+
const m = fs.readFileSync(existingManifestPath, 'utf8').match(/^version:\s*(.+)$/m);
|
|
898
|
+
const existingVersion = m ? m[1].trim() : 'unknown';
|
|
899
|
+
const newVersion = readPackageVersion();
|
|
900
|
+
if (existingVersion === newVersion) {
|
|
901
|
+
console.log(` ↻ Existing install at v${existingVersion} — refreshing (config + state + .planning preserved).`);
|
|
902
|
+
} else {
|
|
903
|
+
console.log(` ⬆ Existing install at v${existingVersion} — upgrading to v${newVersion} (config + state + .planning preserved).`);
|
|
904
|
+
}
|
|
905
|
+
if (!opts.force) {
|
|
906
|
+
console.log(' Pass --force to also sweep orphaned files from the previous version.');
|
|
907
|
+
}
|
|
908
|
+
}
|
|
659
909
|
if (!fs.existsSync(SOURCE_ROOT)) {
|
|
660
910
|
console.error(`✖ Source tree not found at ${SOURCE_ROOT}. Running from wrong dir?`);
|
|
661
911
|
return 1;
|
|
662
912
|
}
|
|
663
913
|
|
|
664
|
-
// Validate IDE
|
|
914
|
+
// Validate IDE — structured error for unsupported editors (#197).
|
|
665
915
|
if (!['claude', 'cursor', 'gemini'].includes(opts.ide)) {
|
|
666
|
-
console.error(`✖
|
|
916
|
+
console.error(`✖ --ide ${opts.ide} is not supported in v${readPackageVersion()}.`);
|
|
917
|
+
console.error('');
|
|
918
|
+
console.error(' Currently supported:');
|
|
919
|
+
console.error(' claude — Claude Code native (recommended)');
|
|
920
|
+
console.error(' cursor — Cursor IDE');
|
|
921
|
+
console.error(' gemini — Gemini CLI');
|
|
922
|
+
console.error('');
|
|
923
|
+
console.error(' Tracked for v3.0 (see issue #182):');
|
|
924
|
+
console.error(' vscode — VS Code native extension');
|
|
925
|
+
console.error(' jetbrains — IntelliJ / PyCharm');
|
|
926
|
+
console.error(' zed — Zed editor');
|
|
927
|
+
console.error('');
|
|
928
|
+
if (/^(vscode|vs-code|code)$/i.test(opts.ide)) {
|
|
929
|
+
console.error(' Workaround: if you use VS Code WITH the Claude Code extension,');
|
|
930
|
+
console.error(' run `--ide claude` — the extension reads from .claude/ too.');
|
|
931
|
+
console.error('');
|
|
932
|
+
}
|
|
667
933
|
return 1;
|
|
668
934
|
}
|
|
669
935
|
|
|
@@ -698,6 +964,13 @@ function install(opts) {
|
|
|
698
964
|
console.log(` Modules: ${opts.modules.join(', ')}`);
|
|
699
965
|
}
|
|
700
966
|
|
|
967
|
+
// Orphan sweep — remove files from previous install not in the new plan (#196).
|
|
968
|
+
// Runs on --force only, to preserve user-edited or hand-dropped files on regular installs.
|
|
969
|
+
let sweptOrphans = 0;
|
|
970
|
+
if (opts.force) {
|
|
971
|
+
sweptOrphans = sweepStaleInstalledFiles(opts.target, plan);
|
|
972
|
+
}
|
|
973
|
+
|
|
701
974
|
// Copy files
|
|
702
975
|
let copied = 0;
|
|
703
976
|
let skipped = 0;
|
|
@@ -801,6 +1074,14 @@ function install(opts) {
|
|
|
801
1074
|
// Seed .planning/ with starter ROADMAP + STATE so workflows work immediately
|
|
802
1075
|
const starterSeeded = seedStarterPlanning(opts.target, opts.projectName);
|
|
803
1076
|
|
|
1077
|
+
// Install brain scaffolding at .rihal/brain/ (sources.yaml + README).
|
|
1078
|
+
// Actual brain content lands after first brain pull runs.
|
|
1079
|
+
installBrainScaffold(PACKAGE_ROOT, opts.target);
|
|
1080
|
+
|
|
1081
|
+
// Ensure .gitignore separates installed methodology from committable artifacts.
|
|
1082
|
+
// Reads opts.commitPlanning to decide whether .planning/ is in the ignore block.
|
|
1083
|
+
const gitignoreReport = ensureRcodeGitignore(opts.target, { commitPlanning: opts.commitPlanning });
|
|
1084
|
+
|
|
804
1085
|
// Pull Rihal brain content (v2.0 — issue #158).
|
|
805
1086
|
// Runs rihal-tools brain pull as a child process. Placeholder URLs
|
|
806
1087
|
// are skipped gracefully so this does not fail a fresh install.
|
|
@@ -823,10 +1104,7 @@ function install(opts) {
|
|
|
823
1104
|
|
|
824
1105
|
// Summary
|
|
825
1106
|
console.log('');
|
|
826
|
-
console.log(`
|
|
827
|
-
if (skillsInstalled > 0) {
|
|
828
|
-
console.log(` Skills: ${skillsInstalled} phrase-activated (in .claude/skills/)`);
|
|
829
|
-
}
|
|
1107
|
+
console.log(` Files: ${copied} installed` + (opts.force && sweptOrphans > 0 ? `, ${sweptOrphans} stale swept` : ''));
|
|
830
1108
|
if (brainReport && brainReport.ok) {
|
|
831
1109
|
const pulledCount = (brainReport.pulled || []).length;
|
|
832
1110
|
const skippedCount = (brainReport.skipped || []).length;
|
|
@@ -835,26 +1113,50 @@ function install(opts) {
|
|
|
835
1113
|
} else if (brainReport && brainReport.error) {
|
|
836
1114
|
console.log(` Brain: skipped (${brainReport.error})`);
|
|
837
1115
|
}
|
|
1116
|
+
if (gitignoreReport) {
|
|
1117
|
+
const gitMsg = {
|
|
1118
|
+
'created': '.gitignore created with rcode block',
|
|
1119
|
+
'appended': '.gitignore updated — rcode block appended',
|
|
1120
|
+
'already-present': '.gitignore rcode block already present',
|
|
1121
|
+
'updated': '.gitignore rcode block refreshed',
|
|
1122
|
+
'skipped-error': `.gitignore skipped (${gitignoreReport.error})`,
|
|
1123
|
+
}[gitignoreReport.action] || '.gitignore unchanged';
|
|
1124
|
+
console.log(` Gitignore: ${gitMsg}`);
|
|
1125
|
+
}
|
|
838
1126
|
if (skipped > 0) console.log(` Skipped: ${skipped} (already present, unchanged)`);
|
|
839
1127
|
if (opts.force && existedBefore) {
|
|
840
1128
|
console.log(' ⚠ Preserved: .rihal/config.yaml and .rihal/state.json');
|
|
841
1129
|
console.log(' Pass --reset to wipe and re-init those too.');
|
|
842
1130
|
}
|
|
1131
|
+
|
|
1132
|
+
// Count installed agents + commands dynamically (#190).
|
|
1133
|
+
const agentsDir = path.join(opts.target, '.claude', 'agents');
|
|
1134
|
+
const commandsDir = path.join(opts.target, '.claude', 'commands', 'rihal');
|
|
1135
|
+
let agentCount = 0, commandCount = 0;
|
|
1136
|
+
try {
|
|
1137
|
+
if (fs.existsSync(agentsDir)) {
|
|
1138
|
+
agentCount = fs.readdirSync(agentsDir).filter(f => f.startsWith('rihal-') && f.endsWith('.md')).length;
|
|
1139
|
+
}
|
|
1140
|
+
if (fs.existsSync(commandsDir)) {
|
|
1141
|
+
commandCount = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md')).length;
|
|
1142
|
+
}
|
|
1143
|
+
} catch {}
|
|
1144
|
+
|
|
843
1145
|
console.log('');
|
|
844
|
-
console.log(`
|
|
845
|
-
console.log(`
|
|
846
|
-
console.log(`
|
|
847
|
-
console.log(`
|
|
848
|
-
console.log(
|
|
849
|
-
console.log(
|
|
850
|
-
console.log(' 🧭 rihal-sadiq — Director of Strategy');
|
|
851
|
-
console.log(' 🏗️ rihal-waleed — CTO');
|
|
852
|
-
console.log(' 🛡️ rihal-fatima — QA Lead');
|
|
1146
|
+
console.log(` Version: @hanzlaa/rcode@${readPackageVersion()}`);
|
|
1147
|
+
console.log(` IDE: ${opts.ide}`);
|
|
1148
|
+
console.log(` Language: ${opts.language} (change in .rihal/config.yaml)`);
|
|
1149
|
+
console.log(` Mode: ${opts.mode} (guided=confirm at gates, yolo=autonomous)`);
|
|
1150
|
+
console.log(` Profile: balanced`);
|
|
1151
|
+
console.log(` Planning: ${opts.commitPlanning !== false ? 'committed' : 'gitignored'} (flip: rihal-tools gitignore refresh)`);
|
|
853
1152
|
console.log('');
|
|
854
|
-
console.log(
|
|
855
|
-
console.log(
|
|
856
|
-
console.log(
|
|
857
|
-
console.log(
|
|
1153
|
+
console.log(` Agents: ${agentCount} installed in .claude/agents/ (e.g. rihal-sadiq, rihal-waleed, rihal-fatima)`);
|
|
1154
|
+
console.log(` Full roster: node .rihal/bin/rihal-tools.cjs list-agents`);
|
|
1155
|
+
console.log(` Commands: ${commandCount} slash commands in .claude/commands/rihal/ (e.g. /rihal:council, /rihal:create-prd, /rihal:progress)`);
|
|
1156
|
+
console.log(` Full list: ls .claude/commands/rihal/`);
|
|
1157
|
+
if (skillsInstalled > 0) {
|
|
1158
|
+
console.log(` Skills: ${skillsInstalled} phrase-activated in .claude/skills/`);
|
|
1159
|
+
}
|
|
858
1160
|
console.log('');
|
|
859
1161
|
if (starterSeeded) {
|
|
860
1162
|
console.log(' ✓ Starter planning scaffolded in .planning/ (ROADMAP, STATE, PROJECT)');
|
|
@@ -862,14 +1164,89 @@ function install(opts) {
|
|
|
862
1164
|
}
|
|
863
1165
|
console.log(' Next:');
|
|
864
1166
|
console.log(` cd ${opts.target}`);
|
|
865
|
-
console.log(' claude
|
|
866
|
-
console.log(' /rihal:
|
|
867
|
-
console.log(' /rihal:do
|
|
868
|
-
console.log(' /rihal:council <
|
|
1167
|
+
console.log(' claude # start Claude Code (reload window if already open)');
|
|
1168
|
+
console.log(' /rihal:progress # where you are, what\'s next');
|
|
1169
|
+
console.log(' /rihal:do # interactive command picker');
|
|
1170
|
+
console.log(' /rihal:council <q> # multi-agent strategic answer');
|
|
869
1171
|
console.log('');
|
|
870
|
-
console.log('
|
|
1172
|
+
console.log(' Refresh anytime:');
|
|
1173
|
+
console.log(' npx @hanzlaa/rcode@latest install # pull the latest rcode + brain');
|
|
1174
|
+
console.log(' /rihal:update v2.2.0 # pin rcode to a specific version');
|
|
871
1175
|
console.log('');
|
|
872
|
-
|
|
1176
|
+
console.log(' ⚠ If your IDE is already open, reload the window to refresh skills/commands.');
|
|
1177
|
+
console.log(' Claude Code / VS Code / Cursor: Cmd+Shift+P → Reload Window');
|
|
1178
|
+
console.log('');
|
|
1179
|
+
|
|
1180
|
+
// Health check — smoke test that the install actually works (#193).
|
|
1181
|
+
const healthPass = runInstallHealthCheck(opts.target, { agentCount, commandCount, skillsInstalled });
|
|
1182
|
+
return healthPass ? 0 : 1;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Run a 5-point smoke test against the fresh install. Closes #193.
|
|
1187
|
+
* Returns true if all pass, false if any critical check failed.
|
|
1188
|
+
* Prints a clean ✓/✖ line per check.
|
|
1189
|
+
*/
|
|
1190
|
+
function runInstallHealthCheck(target, counts) {
|
|
1191
|
+
console.log(' Health check:');
|
|
1192
|
+
const { execFileSync } = require('child_process');
|
|
1193
|
+
let fails = 0;
|
|
1194
|
+
|
|
1195
|
+
function check(label, fn) {
|
|
1196
|
+
try {
|
|
1197
|
+
const out = fn();
|
|
1198
|
+
console.log(` ✓ ${label}${out ? ' — ' + out : ''}`);
|
|
1199
|
+
} catch (err) {
|
|
1200
|
+
fails += 1;
|
|
1201
|
+
console.log(` ✖ ${label} — ${String(err.message || err).slice(0, 120)}`);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
check('rihal-tools.cjs runs', () => {
|
|
1206
|
+
const toolsPath = path.join(target, '.rihal', 'bin', 'rihal-tools.cjs');
|
|
1207
|
+
if (!fs.existsSync(toolsPath)) throw new Error('bin/rihal-tools.cjs not installed');
|
|
1208
|
+
execFileSync('node', ['-c', toolsPath], { stdio: 'pipe' });
|
|
1209
|
+
return 'syntax ok';
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
check('.rihal/config.yaml present', () => {
|
|
1213
|
+
const p = path.join(target, '.rihal', 'config.yaml');
|
|
1214
|
+
if (!fs.existsSync(p)) throw new Error('missing');
|
|
1215
|
+
const text = fs.readFileSync(p, 'utf8');
|
|
1216
|
+
if (!/user_name:|project_name:/.test(text)) throw new Error('config.yaml incomplete');
|
|
1217
|
+
return `${fs.statSync(p).size} bytes`;
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
check('.rihal/state.json parses', () => {
|
|
1221
|
+
const p = path.join(target, '.rihal', 'state.json');
|
|
1222
|
+
if (!fs.existsSync(p)) throw new Error('missing');
|
|
1223
|
+
JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
1224
|
+
return 'valid JSON';
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
check('agents installed', () => {
|
|
1228
|
+
if ((counts.agentCount || 0) < 20) throw new Error(`only ${counts.agentCount} agents (expected ≥ 20)`);
|
|
1229
|
+
return `${counts.agentCount}`;
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
check('skills + commands installed', () => {
|
|
1233
|
+
const issues = [];
|
|
1234
|
+
if ((counts.skillsInstalled || 0) < 20) issues.push(`${counts.skillsInstalled} skills`);
|
|
1235
|
+
if ((counts.commandCount || 0) < 20) issues.push(`${counts.commandCount} commands`);
|
|
1236
|
+
if (issues.length) throw new Error(`low count: ${issues.join(', ')}`);
|
|
1237
|
+
return `${counts.skillsInstalled} skills + ${counts.commandCount} commands`;
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
if (fails > 0) {
|
|
1241
|
+
console.log('');
|
|
1242
|
+
console.log(` ✖ ${fails} health check${fails === 1 ? '' : 's'} failed — install may be broken.`);
|
|
1243
|
+
console.log(' Debug: node .rihal/bin/rihal-tools.cjs state read && ls -la .rihal/');
|
|
1244
|
+
console.log(' Reinstall: npx @hanzlaa/rcode install . --force');
|
|
1245
|
+
console.log('');
|
|
1246
|
+
return false;
|
|
1247
|
+
}
|
|
1248
|
+
console.log('');
|
|
1249
|
+
return true;
|
|
873
1250
|
}
|
|
874
1251
|
|
|
875
1252
|
async function main() {
|
|
@@ -909,9 +1286,7 @@ async function main() {
|
|
|
909
1286
|
}
|
|
910
1287
|
}
|
|
911
1288
|
|
|
912
|
-
|
|
913
|
-
process.exit(install(opts));
|
|
914
|
-
} catch (err) {
|
|
1289
|
+
install(opts).then(code => process.exit(code)).catch(err => {
|
|
915
1290
|
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
916
1291
|
console.error(`✖ Permission denied: ${err.path || err.message}`);
|
|
917
1292
|
process.exit(1);
|
|
@@ -923,7 +1298,7 @@ async function main() {
|
|
|
923
1298
|
console.error(`✖ Install failed: ${err.message}`);
|
|
924
1299
|
if (process.env.DEBUG) console.error(err.stack);
|
|
925
1300
|
process.exit(1);
|
|
926
|
-
}
|
|
1301
|
+
});
|
|
927
1302
|
}
|
|
928
1303
|
|
|
929
1304
|
if (require.main === module) main();
|
|
@@ -933,10 +1308,10 @@ if (require.main === module) main();
|
|
|
933
1308
|
* Converts the index.js-style (args, ctx) signature into a cli/install.js
|
|
934
1309
|
* parseArgs-compatible argv and runs install().
|
|
935
1310
|
*/
|
|
936
|
-
function runFromCli(args /* , ctx */) {
|
|
1311
|
+
async function runFromCli(args /* , ctx */) {
|
|
937
1312
|
const argv = Array.isArray(args) ? args : [];
|
|
938
1313
|
const opts = parseArgs(argv);
|
|
939
|
-
const code = install(opts);
|
|
1314
|
+
const code = await install(opts);
|
|
940
1315
|
if (code !== 0) process.exit(code);
|
|
941
1316
|
}
|
|
942
1317
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzlaa/rcode",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Rihal Code (rcode) — installable context-brain for Rihalians. 44 agents, 93 slash commands, 58 skills, pullable Rihal standards. Unified install for Claude Code, Cursor, and Gemini.",
|
|
5
5
|
"main": "cli/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -2805,8 +2805,26 @@ function cmdNotesCount() {
|
|
|
2805
2805
|
*/
|
|
2806
2806
|
function cmdBrain(args) {
|
|
2807
2807
|
const sub = args[0] || 'help';
|
|
2808
|
-
|
|
2809
|
-
|
|
2808
|
+
// sources.yaml lives under .rihal/brain/ in user installs (v2.2+).
|
|
2809
|
+
// Older installs may have it at rihal/brain/ (pre-v2.2) — fall back for compat.
|
|
2810
|
+
let sourcesPath = path.join(RIHAL_DIR, 'brain', 'sources.yaml');
|
|
2811
|
+
let brainDir = path.join(RIHAL_DIR, 'brain');
|
|
2812
|
+
if (!fs.existsSync(sourcesPath)) {
|
|
2813
|
+
const legacyPath = path.join(PROJECT_ROOT, 'rihal', 'brain', 'sources.yaml');
|
|
2814
|
+
if (fs.existsSync(legacyPath)) {
|
|
2815
|
+
sourcesPath = legacyPath;
|
|
2816
|
+
brainDir = path.join(PROJECT_ROOT, 'rihal', 'brain');
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
// Resolve a source's dest directory relative to brainDir.
|
|
2821
|
+
// Accepts legacy absolute-looking values ("rihal/brain/rihal-github/") by
|
|
2822
|
+
// stripping any leading "rihal/brain/" so the resolved path sits inside the
|
|
2823
|
+
// chosen brainDir. New sources.yaml should use bare names ("rihal-github/").
|
|
2824
|
+
function resolveDest(dest) {
|
|
2825
|
+
const trimmed = String(dest || '').replace(/^rihal\/brain\//, '').replace(/^\/+/, '');
|
|
2826
|
+
return path.join(brainDir, trimmed);
|
|
2827
|
+
}
|
|
2810
2828
|
|
|
2811
2829
|
if (!fs.existsSync(sourcesPath)) {
|
|
2812
2830
|
return {
|
|
@@ -2913,7 +2931,7 @@ function cmdBrain(args) {
|
|
|
2913
2931
|
if (sub === 'status') {
|
|
2914
2932
|
const report = { ok: true, sources: [] };
|
|
2915
2933
|
for (const s of sources) {
|
|
2916
|
-
const destPath =
|
|
2934
|
+
const destPath = resolveDest(s.dest);
|
|
2917
2935
|
const exists = fs.existsSync(destPath);
|
|
2918
2936
|
report.sources.push({
|
|
2919
2937
|
name: s.name,
|
|
@@ -2947,7 +2965,7 @@ function cmdBrain(args) {
|
|
|
2947
2965
|
|
|
2948
2966
|
if (repo === 'self') {
|
|
2949
2967
|
// In-repo copy — use rsync-ish node copy from paths under project root.
|
|
2950
|
-
const destPath =
|
|
2968
|
+
const destPath = resolveDest(s.dest);
|
|
2951
2969
|
fs.mkdirSync(destPath, { recursive: true });
|
|
2952
2970
|
const paths = Array.isArray(s.paths) ? s.paths : [];
|
|
2953
2971
|
let copied = 0;
|
|
@@ -2989,7 +3007,7 @@ function cmdBrain(args) {
|
|
|
2989
3007
|
const paths = Array.isArray(s.paths) ? s.paths : [];
|
|
2990
3008
|
execSync(`git -C "${tmp}" sparse-checkout set ${paths.map(p => `"${p}"`).join(' ')}`, { stdio: 'pipe' });
|
|
2991
3009
|
|
|
2992
|
-
const destPath =
|
|
3010
|
+
const destPath = resolveDest(s.dest);
|
|
2993
3011
|
fs.mkdirSync(destPath, { recursive: true });
|
|
2994
3012
|
// Copy everything the sparse checkout materialized.
|
|
2995
3013
|
function copyTree(src, dst) {
|
|
@@ -3301,6 +3319,121 @@ function cmdStateSnapshot() {
|
|
|
3301
3319
|
};
|
|
3302
3320
|
}
|
|
3303
3321
|
|
|
3322
|
+
/**
|
|
3323
|
+
* cmdGitignore — re-render the rcode-managed block in .gitignore based on
|
|
3324
|
+
* current config (specifically commit_planning from .rihal/config.yaml).
|
|
3325
|
+
*
|
|
3326
|
+
* Subcommands:
|
|
3327
|
+
* gitignore refresh rewrite the rcode block in-place
|
|
3328
|
+
* gitignore status report current commit_planning + block presence
|
|
3329
|
+
*
|
|
3330
|
+
* Mirrors the logic in cli/install.js ensureRcodeGitignore — kept in sync
|
|
3331
|
+
* by convention. Any change to the block format should update both.
|
|
3332
|
+
* Closes #189 — runtime toggle for commit_planning.
|
|
3333
|
+
*/
|
|
3334
|
+
function cmdGitignore(args) {
|
|
3335
|
+
const sub = args[0] || 'refresh';
|
|
3336
|
+
const gitignorePath = path.join(PROJECT_ROOT, '.gitignore');
|
|
3337
|
+
const configPath = path.join(RIHAL_DIR, 'config.yaml');
|
|
3338
|
+
|
|
3339
|
+
// Read commit_planning from config; default true if missing.
|
|
3340
|
+
let commitPlanning = true;
|
|
3341
|
+
if (fs.existsSync(configPath)) {
|
|
3342
|
+
const cfg = fs.readFileSync(configPath, 'utf8');
|
|
3343
|
+
const m = cfg.match(/^\s*commit_planning:\s*(true|false)\s*$/m);
|
|
3344
|
+
if (m) commitPlanning = (m[1] === 'true');
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
const BEGIN = '# ===== rcode-managed gitignore block (npx @hanzlaa/rcode install) =====';
|
|
3348
|
+
const END = '# ===== end rcode-managed gitignore block =====';
|
|
3349
|
+
|
|
3350
|
+
if (sub === 'status') {
|
|
3351
|
+
const exists = fs.existsSync(gitignorePath);
|
|
3352
|
+
const hasBlock = exists && fs.readFileSync(gitignorePath, 'utf8').includes(BEGIN);
|
|
3353
|
+
return {
|
|
3354
|
+
ok: true,
|
|
3355
|
+
gitignore_exists: exists,
|
|
3356
|
+
block_present: hasBlock,
|
|
3357
|
+
commit_planning: commitPlanning,
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
if (sub !== 'refresh') {
|
|
3362
|
+
return { ok: false, error: `Unknown gitignore subcommand: ${sub}. Try: refresh | status` };
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
const lines = [
|
|
3366
|
+
'',
|
|
3367
|
+
BEGIN,
|
|
3368
|
+
'# Added automatically on rcode install. Idempotent — safe to re-run.',
|
|
3369
|
+
'# Edit `commit_planning` in .rihal/config.yaml, then: rihal-tools gitignore refresh',
|
|
3370
|
+
'',
|
|
3371
|
+
'# Installed methodology files (regenerate with: npx @hanzlaa/rcode install)',
|
|
3372
|
+
'.claude/',
|
|
3373
|
+
'.rihal/bin/',
|
|
3374
|
+
'.rihal/workflows/',
|
|
3375
|
+
'.rihal/references/',
|
|
3376
|
+
'.rihal/commands/',
|
|
3377
|
+
'.rihal/skills/',
|
|
3378
|
+
'',
|
|
3379
|
+
'# Pulled Rihal brain content (refresh with: rcode brain pull)',
|
|
3380
|
+
'.rihal/brain/rihal-github/',
|
|
3381
|
+
'.rihal/brain/rihal-docs/',
|
|
3382
|
+
'.rihal/brain/best-practices/',
|
|
3383
|
+
'',
|
|
3384
|
+
'# Runtime noise',
|
|
3385
|
+
'.rihal/state.json.lock',
|
|
3386
|
+
'.planning/debug/',
|
|
3387
|
+
'.planning/_backup/',
|
|
3388
|
+
];
|
|
3389
|
+
if (!commitPlanning) {
|
|
3390
|
+
lines.push('', '# Planning artifacts — kept local (commit_planning: false)', '.planning/');
|
|
3391
|
+
}
|
|
3392
|
+
lines.push(
|
|
3393
|
+
'',
|
|
3394
|
+
'# What you DO commit:',
|
|
3395
|
+
'# .rihal/config.yaml - project mode/language/profile/commit_planning',
|
|
3396
|
+
'# .rihal/state.json - decisions, roadmap pointer, blockers',
|
|
3397
|
+
'# .rihal/brain/sources.yaml - brain source manifest',
|
|
3398
|
+
commitPlanning
|
|
3399
|
+
? '# .planning/ - PRD, roadmap, sprints, SUMMARY.md files'
|
|
3400
|
+
: '# (planning artifacts are NOT committed — see commit_planning in config)',
|
|
3401
|
+
END,
|
|
3402
|
+
''
|
|
3403
|
+
);
|
|
3404
|
+
const BLOCK = lines.join('\n');
|
|
3405
|
+
|
|
3406
|
+
/** Replace the rcode block in text using indexOf — safer than regex. */
|
|
3407
|
+
function spliceBlock(existing, newBlock) {
|
|
3408
|
+
const start = existing.indexOf(BEGIN);
|
|
3409
|
+
if (start < 0) return null;
|
|
3410
|
+
const endIdx = existing.indexOf(END, start);
|
|
3411
|
+
if (endIdx < 0) return null;
|
|
3412
|
+
// Include trailing newline after END if present, and leading newline before BEGIN.
|
|
3413
|
+
let sliceStart = start;
|
|
3414
|
+
if (sliceStart > 0 && existing[sliceStart - 1] === '\n') sliceStart -= 1;
|
|
3415
|
+
let sliceEnd = endIdx + END.length;
|
|
3416
|
+
if (existing[sliceEnd] === '\n') sliceEnd += 1;
|
|
3417
|
+
return existing.slice(0, sliceStart) + newBlock + existing.slice(sliceEnd);
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3420
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
3421
|
+
fs.writeFileSync(gitignorePath, BLOCK);
|
|
3422
|
+
return { ok: true, action: 'created', commit_planning: commitPlanning };
|
|
3423
|
+
}
|
|
3424
|
+
const existing = fs.readFileSync(gitignorePath, 'utf8');
|
|
3425
|
+
if (existing.includes(BEGIN)) {
|
|
3426
|
+
const rewritten = spliceBlock(existing, BLOCK);
|
|
3427
|
+
if (rewritten !== null && rewritten !== existing) {
|
|
3428
|
+
fs.writeFileSync(gitignorePath, rewritten);
|
|
3429
|
+
return { ok: true, action: 'updated', commit_planning: commitPlanning };
|
|
3430
|
+
}
|
|
3431
|
+
return { ok: true, action: 'no-change', commit_planning: commitPlanning };
|
|
3432
|
+
}
|
|
3433
|
+
fs.writeFileSync(gitignorePath, existing + BLOCK);
|
|
3434
|
+
return { ok: true, action: 'appended', commit_planning: commitPlanning };
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3304
3437
|
function cmdFindFiles(rawArgs) {
|
|
3305
3438
|
const flags = {};
|
|
3306
3439
|
const parts = rawArgs.split(/\s+/).filter(p => p);
|
|
@@ -3455,6 +3588,10 @@ async function main() {
|
|
|
3455
3588
|
result = cmdStateSnapshot();
|
|
3456
3589
|
break;
|
|
3457
3590
|
}
|
|
3591
|
+
case 'gitignore': {
|
|
3592
|
+
result = cmdGitignore(args);
|
|
3593
|
+
break;
|
|
3594
|
+
}
|
|
3458
3595
|
case 'agent-skills':
|
|
3459
3596
|
result = cmdAgentInfo(args[0]);
|
|
3460
3597
|
break;
|
package/rihal/brain/sources.yaml
CHANGED
|
@@ -34,7 +34,7 @@ sources:
|
|
|
34
34
|
- docs/issue-standards.md
|
|
35
35
|
- docs/commit-standards.md
|
|
36
36
|
- docs/review-checklist.md
|
|
37
|
-
dest: rihal
|
|
37
|
+
dest: rihal-github/
|
|
38
38
|
|
|
39
39
|
- name: rihal-docs
|
|
40
40
|
description: >
|
|
@@ -47,13 +47,16 @@ sources:
|
|
|
47
47
|
- "guides/**/*.md"
|
|
48
48
|
- "standards/**/*.md"
|
|
49
49
|
- "playbooks/**/*.md"
|
|
50
|
-
dest: rihal
|
|
50
|
+
dest: rihal-docs/
|
|
51
51
|
|
|
52
52
|
- name: rihal-best-practices
|
|
53
53
|
description: >
|
|
54
54
|
Hanzla's one-year-of-usage best practices — in-repo, version-
|
|
55
|
-
controlled alongside the rest of Rihal Code.
|
|
55
|
+
controlled alongside the rest of Rihal Code. Resolved from the
|
|
56
|
+
installed .rihal/skills/_shared/ dir (or the package's own
|
|
57
|
+
rihal/skills/_shared/ when running inside the rihal-code repo).
|
|
56
58
|
repo: self
|
|
57
59
|
paths:
|
|
60
|
+
- .rihal/skills/_shared/**/*.md
|
|
58
61
|
- rihal/skills/_shared/**/*.md
|
|
59
|
-
dest:
|
|
62
|
+
dest: best-practices/
|
|
@@ -27,14 +27,14 @@ Check for the dashboard server in priority order:
|
|
|
27
27
|
|
|
28
28
|
1. `./server/dashboard.js` (when inside the rihal-code source repo)
|
|
29
29
|
2. `./.rihal/lib/server/dashboard.js` (installed package copy)
|
|
30
|
-
3. `$(npm root -g)/@
|
|
30
|
+
3. `$(npm root -g)/@hanzlaa/rcode/server/dashboard.js` (global install)
|
|
31
31
|
|
|
32
32
|
Store the resolved path as `$DASHBOARD`.
|
|
33
33
|
|
|
34
34
|
If none found, print:
|
|
35
35
|
```
|
|
36
36
|
❌ Dashboard script not found.
|
|
37
|
-
Run `npx
|
|
37
|
+
Run `npx @hanzlaa/rcode install` to install the package, or check you're inside a project with .rihal/.
|
|
38
38
|
```
|
|
39
39
|
Exit.
|
|
40
40
|
|
|
@@ -50,8 +50,8 @@ The installer needs to be called with the `--module` flag. Detect the rihal-code
|
|
|
50
50
|
# Try local dev first, then global
|
|
51
51
|
if [ -f ./cli/install-v2.js ]; then
|
|
52
52
|
node ./cli/install-v2.js . --module {name} --force --yes
|
|
53
|
-
elif [ -f "$(npm root -g)/@
|
|
54
|
-
node "$(npm root -g)/@
|
|
53
|
+
elif [ -f "$(npm root -g)/@hanzlaa/rcode/cli/install-v2.js" ]; then
|
|
54
|
+
node "$(npm root -g)/@hanzlaa/rcode/cli/install-v2.js" . --module {name} --force --yes
|
|
55
55
|
else
|
|
56
56
|
echo "Cannot find rihal-code package. Install it globally or run from the repo."
|
|
57
57
|
exit 1
|
|
@@ -179,6 +179,6 @@ If `SNAPSHOT.routes[]` is empty or only has fallback entries, print:
|
|
|
179
179
|
|
|
180
180
|
## On Error
|
|
181
181
|
|
|
182
|
-
- **CLI missing:** "Rihal Code install missing or stale. Run: npx @
|
|
182
|
+
- **CLI missing:** "Rihal Code install missing or stale. Run: npx @hanzlaa/rcode install"
|
|
183
183
|
- **CLI returns `ok: false`:** surface the CLI's error verbatim. Do not attempt to compensate — the CLI's failures are the source of truth on what's wrong.
|
|
184
184
|
- **Network-dependent insights:** there should be none. Insights are computed from local state + disk only.
|
|
@@ -111,6 +111,6 @@ Group routes by letter. If multiple routes share a letter, list them indented. I
|
|
|
111
111
|
|
|
112
112
|
## On Error
|
|
113
113
|
|
|
114
|
-
- **CLI not found:** "Rihal Code install missing. Run: npx @
|
|
114
|
+
- **CLI not found:** "Rihal Code install missing. Run: npx @hanzlaa/rcode install"
|
|
115
115
|
- **state.json invalid JSON:** report the CLI's exact error string — the CLI already has a clean error shape.
|
|
116
116
|
- **Unexpected shape:** fall back to the banner + "State present but unreadable. Try: node .rihal/bin/rihal-tools.cjs state read"
|