@hanzlaa/rcode 3.4.20 → 3.4.22
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/cli/generate-command-skills.cjs +33 -2
- package/cli/index.js +8 -8
- package/cli/install.js +161 -24
- package/cli/lib/manifest.cjs +19 -0
- package/cli/set-profile.js +5 -5
- package/cli/show-model.js +3 -3
- package/cli/uninstall.js +61 -9
- package/dist/rcode.js +37 -3
- package/package.json +1 -1
- package/rihal/bin/rihal-tools.cjs +96 -0
- package/rihal/state.json +5 -6
- package/rihal/workflows/new-project.md +50 -8
|
@@ -149,7 +149,7 @@ Identical to \`/rihal-${cmdName}\`. See the workflow file for the canonical outp
|
|
|
149
149
|
`;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
function main(packageRoot, targetSkillsDir, version) {
|
|
152
|
+
function main(packageRoot, targetSkillsDir, version, options = {}) {
|
|
153
153
|
if (!fs.existsSync(targetSkillsDir)) {
|
|
154
154
|
fs.mkdirSync(targetSkillsDir, { recursive: true });
|
|
155
155
|
}
|
|
@@ -158,11 +158,20 @@ function main(packageRoot, targetSkillsDir, version) {
|
|
|
158
158
|
const commandsDir = path.join(packageRoot, 'rihal', 'commands');
|
|
159
159
|
if (!fs.existsSync(commandsDir)) {
|
|
160
160
|
console.warn(`[generate-command-skills] commands dir not found: ${commandsDir}`);
|
|
161
|
-
return { generated: 0, skipped: 0 };
|
|
161
|
+
return { generated: 0, skipped: 0, skippedGlobal: 0 };
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
// Issue #679: skip stubs whose name already exists in ~/.claude/skills/.
|
|
165
|
+
// Otherwise the slash picker shows the same /rihal-* twice.
|
|
166
|
+
const os = require('os');
|
|
167
|
+
const globalSkillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
168
|
+
const globalSkills = (options.skipGlobalDuplicates && fs.existsSync(globalSkillsDir))
|
|
169
|
+
? new Set(fs.readdirSync(globalSkillsDir).filter(n => n.startsWith('rihal-')))
|
|
170
|
+
: new Set();
|
|
171
|
+
|
|
164
172
|
let generated = 0;
|
|
165
173
|
let skipped = 0;
|
|
174
|
+
let skippedGlobal = 0;
|
|
166
175
|
|
|
167
176
|
for (const file of fs.readdirSync(commandsDir)) {
|
|
168
177
|
if (!file.endsWith('.md')) continue;
|
|
@@ -177,6 +186,28 @@ function main(packageRoot, targetSkillsDir, version) {
|
|
|
177
186
|
continue;
|
|
178
187
|
}
|
|
179
188
|
|
|
189
|
+
if (globalSkills.has(skillName)) {
|
|
190
|
+
// Global skill of same name exists — installing a sidebar stub here would
|
|
191
|
+
// duplicate the entry in Claude Code's slash picker. Also clean up any
|
|
192
|
+
// previously-generated stub with the same name.
|
|
193
|
+
const existingStub = path.join(targetSkillsDir, skillName);
|
|
194
|
+
if (fs.existsSync(existingStub)) {
|
|
195
|
+
try {
|
|
196
|
+
const existingFile = path.join(existingStub, 'SKILL.md');
|
|
197
|
+
if (fs.existsSync(existingFile)) {
|
|
198
|
+
const text = fs.readFileSync(existingFile, 'utf8');
|
|
199
|
+
// Only delete previously-generated stubs (marker present) — never
|
|
200
|
+
// user customizations.
|
|
201
|
+
if (/^generated:\s*true/m.test(text)) {
|
|
202
|
+
fs.rmSync(existingStub, { recursive: true, force: true });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch { /* non-fatal */ }
|
|
206
|
+
}
|
|
207
|
+
skippedGlobal++;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
180
211
|
const cmdPath = path.join(commandsDir, file);
|
|
181
212
|
const cmdText = fs.readFileSync(cmdPath, 'utf8');
|
|
182
213
|
const cmdFm = parseFrontmatter(cmdText);
|
package/cli/index.js
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
* Rihal Code CLI
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* npx @
|
|
7
|
-
* npx @
|
|
8
|
-
* npx @
|
|
9
|
-
* npx @
|
|
10
|
-
* npx @
|
|
11
|
-
* npx @
|
|
12
|
-
* npx @
|
|
13
|
-
* npx @
|
|
6
|
+
* npx @hanzlaa/rcode init → scaffold .rihal/ in current project
|
|
7
|
+
* npx @hanzlaa/rcode dashboard → start the Diwan view-only dashboard
|
|
8
|
+
* npx @hanzlaa/rcode serve → alias for dashboard
|
|
9
|
+
* npx @hanzlaa/rcode digest → print compact agent digests
|
|
10
|
+
* npx @hanzlaa/rcode team → list the team roster
|
|
11
|
+
* npx @hanzlaa/rcode doctor → compliance check
|
|
12
|
+
* npx @hanzlaa/rcode version → print version
|
|
13
|
+
* npx @hanzlaa/rcode help → this message
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
const path = require('path');
|
package/cli/install.js
CHANGED
|
@@ -275,7 +275,7 @@ function printInstallHeader(targetVersion) {
|
|
|
275
275
|
pc.cyan('│') + ' ' + dim('A persistent context-brain for your editor') + ' ' + pc.cyan('│'),
|
|
276
276
|
pc.cyan('│') + ' ' + pc.cyan('│'),
|
|
277
277
|
pc.cyan('│') + ' ' + dim('version ') + pc.green('v' + v) + ' ' + pc.cyan('│'),
|
|
278
|
-
pc.cyan('│') + ' ' + dim('docs ') + 'github.com/
|
|
278
|
+
pc.cyan('│') + ' ' + dim('docs ') + 'github.com/hanzlahabib/rihal-code ' + pc.cyan('│'),
|
|
279
279
|
pc.cyan('│') + ' ' + pc.cyan('│'),
|
|
280
280
|
pc.cyan('╰───────────────────────────────────────────────────────────╯'),
|
|
281
281
|
'',
|
|
@@ -370,11 +370,32 @@ async function resolveIde(opts) {
|
|
|
370
370
|
async function resolveCommitPlanning(opts) {
|
|
371
371
|
if (opts.commitPlanning !== null) return opts.commitPlanning;
|
|
372
372
|
if (opts.noPrompt || opts.global) return false; // global install: no planning artifacts
|
|
373
|
-
if (opts.yes || !process.stdin.isTTY) return true; // non-interactive default
|
|
374
373
|
|
|
374
|
+
// Issue #685: on re-install, read the existing .rihal/config.yaml and use
|
|
375
|
+
// its commit_planning value as the default. Otherwise the new prompt
|
|
376
|
+
// answer overwrites .gitignore but NOT config.yaml, leaving two sources of
|
|
377
|
+
// truth that silently diverge. Users on re-install almost always want to
|
|
378
|
+
// KEEP their existing setting unless they explicitly pass --commit-planning.
|
|
379
|
+
let existingValue = null;
|
|
380
|
+
try {
|
|
381
|
+
const cfgPath = path.join(opts.target, '.rihal', 'config.yaml');
|
|
382
|
+
if (fs.existsSync(cfgPath)) {
|
|
383
|
+
const cfg = fs.readFileSync(cfgPath, 'utf8');
|
|
384
|
+
const m = cfg.match(/^commit_planning:\s*(true|false)\s*$/m);
|
|
385
|
+
if (m) existingValue = m[1] === 'true';
|
|
386
|
+
}
|
|
387
|
+
} catch { /* fall through to prompt */ }
|
|
388
|
+
|
|
389
|
+
if (opts.yes || !process.stdin.isTTY) {
|
|
390
|
+
return existingValue !== null ? existingValue : true; // honor existing on re-install
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const initialValue = existingValue === false ? 'gitignore' : 'commit';
|
|
375
394
|
const choice = await clack.select({
|
|
376
|
-
message:
|
|
377
|
-
|
|
395
|
+
message: existingValue !== null
|
|
396
|
+
? '📋 .planning/ tracking — current setting preserved unless you change it.'
|
|
397
|
+
: '📋 .planning/ holds PRDs, roadmaps, sprints, SUMMARY files. How should they be tracked?',
|
|
398
|
+
initialValue,
|
|
378
399
|
options: [
|
|
379
400
|
{ value: 'commit', label: 'Commit', hint: 'collaborators see the same plans (recommended)' },
|
|
380
401
|
{ value: 'gitignore', label: 'Gitignore', hint: 'planning stays local (good for sensitive PRDs)' },
|
|
@@ -548,7 +569,15 @@ function seedStarterPlanning(target, projectName) {
|
|
|
548
569
|
const today = new Date().toISOString().slice(0, 10);
|
|
549
570
|
const name = projectName || path.basename(target);
|
|
550
571
|
|
|
572
|
+
// Stub planning files: clearly marked as install templates so users (and
|
|
573
|
+
// /rihal-new-project Step 0.5 detection) can tell them apart from real
|
|
574
|
+
// planning artifacts. See issues #670 #671 #676.
|
|
575
|
+
const STUB_BANNER =
|
|
576
|
+
`<!-- INSTALL STUB — overwritten by /rihal-new-project. Delete this file or run\n` +
|
|
577
|
+
` /rihal-new-project before committing. See https://github.com/hanzlahabib/rihal-code/issues/670 -->\n\n`;
|
|
578
|
+
|
|
551
579
|
fs.writeFileSync(projectPath,
|
|
580
|
+
STUB_BANNER +
|
|
552
581
|
`# ${name}\n\n` +
|
|
553
582
|
`**One-line:** Describe what this project is in one sentence.\n\n` +
|
|
554
583
|
`## Vision\n\n` +
|
|
@@ -558,6 +587,7 @@ function seedStarterPlanning(target, projectName) {
|
|
|
558
587
|
);
|
|
559
588
|
|
|
560
589
|
fs.writeFileSync(roadmapPath,
|
|
590
|
+
STUB_BANNER +
|
|
561
591
|
`# ${name} — Roadmap\n\n` +
|
|
562
592
|
`**Milestone: M1 — Initial Delivery** (v1.0)\n` +
|
|
563
593
|
`Started: ${today} · Current\n\n` +
|
|
@@ -572,6 +602,7 @@ function seedStarterPlanning(target, projectName) {
|
|
|
572
602
|
);
|
|
573
603
|
|
|
574
604
|
fs.writeFileSync(statePath,
|
|
605
|
+
STUB_BANNER +
|
|
575
606
|
`# ${name} — State\n\n` +
|
|
576
607
|
`**Last updated:** ${today}\n` +
|
|
577
608
|
`**Milestone:** M1 — Initial Delivery\n` +
|
|
@@ -580,27 +611,33 @@ function seedStarterPlanning(target, projectName) {
|
|
|
580
611
|
`---\n\n` +
|
|
581
612
|
`## Decisions\n\n_None yet._\n\n` +
|
|
582
613
|
`## Blockers\n\n_None._\n\n` +
|
|
583
|
-
`## Next Action\n\
|
|
614
|
+
`## Next Action\n\nRun \`/rihal-new-project <description>\` to bootstrap, or \`/rihal-sprint-planning\` once a real phase exists.\n`
|
|
584
615
|
);
|
|
585
616
|
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
//
|
|
617
|
+
// Issue #670: do NOT pre-seed .rihal/state.json with a fake project +
|
|
618
|
+
// "Setup & Scaffolding" phase. That made every fresh install look like a
|
|
619
|
+
// real initialized project and broke /rihal-new-project Step 0.5 detection.
|
|
620
|
+
//
|
|
621
|
+
// Write a minimal shell with _seeded_stub:true so:
|
|
622
|
+
// - rihal-tools doesn't have to re-init on first call (avoids race)
|
|
623
|
+
// - /rihal-new-project Step 0.5 (issue #671) can detect "stub" reliably
|
|
624
|
+
// - sprint tools that previously relied on phase 01 will surface a clear
|
|
625
|
+
// "no phases yet — run /rihal-new-project first" error instead of
|
|
626
|
+
// silently operating on a fake phase
|
|
589
627
|
const rihalStateJson = path.join(target, '.rihal', 'state.json');
|
|
590
628
|
if (!fs.existsSync(rihalStateJson)) {
|
|
591
629
|
const now = new Date().toISOString();
|
|
592
630
|
const state = {
|
|
593
631
|
version: '1',
|
|
594
|
-
project:
|
|
632
|
+
project: null,
|
|
633
|
+
_seeded_stub: true,
|
|
595
634
|
created: now,
|
|
596
635
|
updated: now,
|
|
597
|
-
current_phase:
|
|
636
|
+
current_phase: null,
|
|
598
637
|
current_plan: 0,
|
|
599
638
|
current_sprint: null,
|
|
600
|
-
milestone:
|
|
601
|
-
phases: [
|
|
602
|
-
{ id: '01', name: 'Setup & Scaffolding', status: 'planned' }
|
|
603
|
-
],
|
|
639
|
+
milestone: null,
|
|
640
|
+
phases: [],
|
|
604
641
|
executions: [],
|
|
605
642
|
decisions: [],
|
|
606
643
|
blockers: [],
|
|
@@ -857,16 +894,26 @@ function installBrainScaffold(packageRoot, target) {
|
|
|
857
894
|
*
|
|
858
895
|
* A skill is marked internal by adding `internal: true` to its SKILL.md frontmatter.
|
|
859
896
|
*/
|
|
860
|
-
function installSkills(packageRoot, target) {
|
|
897
|
+
function installSkills(packageRoot, target, options = {}) {
|
|
861
898
|
const skillsSource = path.join(packageRoot, 'rihal/skills');
|
|
862
899
|
const skillsDest = path.join(target, '.claude/skills');
|
|
863
900
|
const internalDest = path.join(target, '.rihal/skills');
|
|
864
901
|
|
|
865
|
-
if (!fs.existsSync(skillsSource)) return 0;
|
|
902
|
+
if (!fs.existsSync(skillsSource)) return { count: 0, skippedGlobal: 0 };
|
|
866
903
|
fs.mkdirSync(skillsDest, { recursive: true });
|
|
867
904
|
fs.mkdirSync(internalDest, { recursive: true });
|
|
868
905
|
|
|
906
|
+
// Issue #679: when ~/.claude/skills/<name>/ already exists with the rihal-
|
|
907
|
+
// prefix, Claude Code reads from BOTH global and project, showing every
|
|
908
|
+
// /rihal-* twice in the slash picker. Skip the project copy for any rihal-*
|
|
909
|
+
// skill that already lives in the global skills dir.
|
|
910
|
+
const globalSkillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
911
|
+
const globalRihalSkills = (options.skipGlobalDuplicates && fs.existsSync(globalSkillsDir))
|
|
912
|
+
? new Set(fs.readdirSync(globalSkillsDir).filter(n => n.startsWith('rihal-')))
|
|
913
|
+
: new Set();
|
|
914
|
+
|
|
869
915
|
let count = 0;
|
|
916
|
+
let skippedGlobal = 0;
|
|
870
917
|
|
|
871
918
|
function isInternalSkill(skillDir) {
|
|
872
919
|
const skillMd = path.join(skillDir, 'SKILL.md');
|
|
@@ -875,6 +922,13 @@ function installSkills(packageRoot, target) {
|
|
|
875
922
|
return /^internal:\s*true\s*$/m.test(text);
|
|
876
923
|
}
|
|
877
924
|
|
|
925
|
+
function hasLocalOverride(destDir) {
|
|
926
|
+
if (!fs.existsSync(destDir)) return false;
|
|
927
|
+
try {
|
|
928
|
+
return fs.readdirSync(destDir).some(f => f.endsWith('.local.md'));
|
|
929
|
+
} catch { return false; }
|
|
930
|
+
}
|
|
931
|
+
|
|
878
932
|
function walkForSkills(dir) {
|
|
879
933
|
if (!fs.existsSync(dir)) return;
|
|
880
934
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -885,9 +939,23 @@ function installSkills(packageRoot, target) {
|
|
|
885
939
|
const destName = entry.name.startsWith('rihal-')
|
|
886
940
|
? entry.name
|
|
887
941
|
: `rihal-${entry.name}`;
|
|
888
|
-
const
|
|
942
|
+
const internal = isInternalSkill(src);
|
|
943
|
+
const dest = internal
|
|
889
944
|
? path.join(internalDest, destName) // internal → .rihal/skills/
|
|
890
945
|
: path.join(skillsDest, destName); // user-facing → .claude/skills/
|
|
946
|
+
|
|
947
|
+
// Skip user-facing (non-internal) rihal-* skills when the same name
|
|
948
|
+
// exists globally — UNLESS the user has a *.local.md override on the
|
|
949
|
+
// project copy, in which case we always preserve their customization.
|
|
950
|
+
if (!internal && globalRihalSkills.has(destName) && !hasLocalOverride(dest)) {
|
|
951
|
+
// Also remove the existing project copy (left over from previous
|
|
952
|
+
// installs that didn't dedup) so it stops showing in the picker.
|
|
953
|
+
if (fs.existsSync(dest)) {
|
|
954
|
+
try { fs.rmSync(dest, { recursive: true, force: true }); } catch { /* non-fatal */ }
|
|
955
|
+
}
|
|
956
|
+
skippedGlobal++;
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
891
959
|
copyDirRecursive(src, dest);
|
|
892
960
|
count++;
|
|
893
961
|
} else {
|
|
@@ -900,7 +968,7 @@ function installSkills(packageRoot, target) {
|
|
|
900
968
|
walkForSkills(path.join(skillsSource, bucket));
|
|
901
969
|
}
|
|
902
970
|
|
|
903
|
-
return count;
|
|
971
|
+
return { count, skippedGlobal };
|
|
904
972
|
}
|
|
905
973
|
|
|
906
974
|
/**
|
|
@@ -1409,6 +1477,18 @@ function convertToCursorMdc(sourceText) {
|
|
|
1409
1477
|
async function install(opts) {
|
|
1410
1478
|
if (opts.help) { printHelp(); return 0; }
|
|
1411
1479
|
|
|
1480
|
+
// Issue #680: --reset alone is a footgun — silently does nothing. Fail
|
|
1481
|
+
// fast with a clear message before any work happens.
|
|
1482
|
+
if (opts.reset && !opts.force) {
|
|
1483
|
+
console.log('');
|
|
1484
|
+
console.log(' ' + warn('--reset has no effect without --force.'));
|
|
1485
|
+
console.log(' ' + dim(' --reset wipes config.yaml and state.json. To prevent accidental data loss,'));
|
|
1486
|
+
console.log(' ' + dim(' it must be paired with --force. Re-run as:'));
|
|
1487
|
+
console.log(' ' + dim(' rcode install --reset --force'));
|
|
1488
|
+
console.log('');
|
|
1489
|
+
return 2;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1412
1492
|
const pkgVersion = readPackageVersion();
|
|
1413
1493
|
|
|
1414
1494
|
// Header banner — only shown for interactive runs to keep CI/non-TTY logs terse.
|
|
@@ -1738,8 +1818,10 @@ async function install(opts) {
|
|
|
1738
1818
|
const configDir = path.join(opts.target, '.rihal', '_config');
|
|
1739
1819
|
ensureDir(configDir);
|
|
1740
1820
|
fs.writeFileSync(path.join(configDir, 'manifest.yaml'), generateInstallManifest(opts));
|
|
1741
|
-
// Install skills + sidebar stubs globally
|
|
1742
|
-
|
|
1821
|
+
// Install skills + sidebar stubs globally — never dedup against globals,
|
|
1822
|
+
// because in --global mode the target IS the global dir.
|
|
1823
|
+
const skillsResult = installSkills(PACKAGE_ROOT, opts.target);
|
|
1824
|
+
let skillsInstalled = skillsResult.count;
|
|
1743
1825
|
try {
|
|
1744
1826
|
const { main: generateCommandSkills } = require(path.join(PACKAGE_ROOT, 'cli', 'generate-command-skills.cjs'));
|
|
1745
1827
|
const stubsDir = path.join(opts.target, '.claude', 'skills');
|
|
@@ -1815,6 +1897,7 @@ async function install(opts) {
|
|
|
1815
1897
|
plan.length = 0;
|
|
1816
1898
|
filtered.forEach(e => plan.push(e));
|
|
1817
1899
|
}
|
|
1900
|
+
|
|
1818
1901
|
} catch { /* non-fatal — skip detection on permission errors */ }
|
|
1819
1902
|
}
|
|
1820
1903
|
|
|
@@ -1839,11 +1922,34 @@ async function install(opts) {
|
|
|
1839
1922
|
} else if (opts.force && (fs.existsSync(configPath) || fs.existsSync(stateDest))) {
|
|
1840
1923
|
existedBefore = true;
|
|
1841
1924
|
}
|
|
1925
|
+
// Note: --reset without --force is rejected at the top of install() (#680).
|
|
1842
1926
|
|
|
1843
1927
|
// Write .rihal/config.yaml (user_name, project_name, language, mode)
|
|
1844
1928
|
// Note: config.yaml is user data and should NOT be overwritten on --force (unless --reset)
|
|
1845
1929
|
if (!fs.existsSync(configPath)) {
|
|
1846
1930
|
fs.writeFileSync(configPath, generateConfigYaml(opts));
|
|
1931
|
+
} else {
|
|
1932
|
+
// Issue #685: re-install path. config.yaml is preserved BUT if the user
|
|
1933
|
+
// just changed commit_planning via the prompt/flag, .gitignore will be
|
|
1934
|
+
// rewritten with the new value while config.yaml keeps the old one,
|
|
1935
|
+
// creating a silent drift. Update only commit_planning in-place
|
|
1936
|
+
// (preserve everything else the user may have customized).
|
|
1937
|
+
try {
|
|
1938
|
+
const before = fs.readFileSync(configPath, 'utf8');
|
|
1939
|
+
const desired = opts.commitPlanning !== false;
|
|
1940
|
+
const re = /^commit_planning:\s*(true|false)\s*$/m;
|
|
1941
|
+
const match = before.match(re);
|
|
1942
|
+
const currentInFile = match ? match[1] === 'true' : null;
|
|
1943
|
+
if (match && currentInFile !== desired) {
|
|
1944
|
+
const updated = before.replace(re, `commit_planning: ${desired}`);
|
|
1945
|
+
fs.writeFileSync(configPath, updated);
|
|
1946
|
+
console.log(' ' + dim(`Updated commit_planning in config.yaml (${currentInFile} → ${desired}) — closes #685.`));
|
|
1947
|
+
} else if (!match) {
|
|
1948
|
+
// Older config without the key — append it so the next read finds it.
|
|
1949
|
+
const appended = before.replace(/\n*$/, '') + `\ncommit_planning: ${desired}\n`;
|
|
1950
|
+
fs.writeFileSync(configPath, appended);
|
|
1951
|
+
}
|
|
1952
|
+
} catch { /* best-effort — never fail install on this */ }
|
|
1847
1953
|
}
|
|
1848
1954
|
// Validate config.yaml with zod schema (#250) — warn but never block install.
|
|
1849
1955
|
try {
|
|
@@ -1901,7 +2007,16 @@ async function install(opts) {
|
|
|
1901
2007
|
|
|
1902
2008
|
// Install v1-style phrase-activated skills (scaffold-project, create-prd,
|
|
1903
2009
|
// retrospective, etc.) into .claude/skills/ alongside the v2 agents/commands.
|
|
1904
|
-
|
|
2010
|
+
// Issue #679: skip rihal-* skills that already exist in ~/.claude/skills/
|
|
2011
|
+
// (global precedence) so the slash picker doesn't show every command twice.
|
|
2012
|
+
// Reuse the isProjectInstall flag declared earlier in this scope.
|
|
2013
|
+
const skillsResult = installSkills(PACKAGE_ROOT, opts.target, {
|
|
2014
|
+
skipGlobalDuplicates: isProjectInstall,
|
|
2015
|
+
});
|
|
2016
|
+
let skillsInstalled = skillsResult.count;
|
|
2017
|
+
if (skillsResult.skippedGlobal > 0) {
|
|
2018
|
+
console.log(' ' + dim(`Skipped ${skillsResult.skippedGlobal} project-level rihal skills (global ones in ~/.claude/skills/ take precedence) — closes #679.`));
|
|
2019
|
+
}
|
|
1905
2020
|
|
|
1906
2021
|
// Generate install-time skill stubs that mirror sidebar-worthy slash commands.
|
|
1907
2022
|
// Source codebase stays clean — these stubs only exist at the install
|
|
@@ -1910,11 +2025,16 @@ async function install(opts) {
|
|
|
1910
2025
|
try {
|
|
1911
2026
|
const { main: generateCommandSkills } = require(path.join(PACKAGE_ROOT, 'cli', 'generate-command-skills.cjs'));
|
|
1912
2027
|
const stubsDir = path.join(opts.target, '.claude', 'skills');
|
|
1913
|
-
const result = generateCommandSkills(PACKAGE_ROOT, stubsDir, readPackageVersion()
|
|
2028
|
+
const result = generateCommandSkills(PACKAGE_ROOT, stubsDir, readPackageVersion(), {
|
|
2029
|
+
skipGlobalDuplicates: isProjectInstall,
|
|
2030
|
+
});
|
|
1914
2031
|
if (result.generated > 0) {
|
|
1915
2032
|
console.log(' ' + dim(`${result.generated} sidebar skill stub${result.generated === 1 ? '' : 's'} generated for command discoverability`));
|
|
1916
2033
|
skillsInstalled += result.generated;
|
|
1917
2034
|
}
|
|
2035
|
+
if (result.skippedGlobal > 0) {
|
|
2036
|
+
console.log(' ' + dim(`Skipped ${result.skippedGlobal} sidebar stub${result.skippedGlobal === 1 ? '' : 's'} that duplicate global ~/.claude/skills/ — closes #679.`));
|
|
2037
|
+
}
|
|
1918
2038
|
} catch (err) {
|
|
1919
2039
|
// Non-fatal: install succeeds without sidebar stubs
|
|
1920
2040
|
console.log(' ' + dim(`(sidebar stub generation skipped: ${err.message})`));
|
|
@@ -2023,6 +2143,7 @@ async function install(opts) {
|
|
|
2023
2143
|
const agentsDir = idePaths.agentsDir;
|
|
2024
2144
|
const commandsDir = idePaths.commandsDir;
|
|
2025
2145
|
let agentCount = 0, commandCount = 0;
|
|
2146
|
+
let agentsFromGlobal = false, commandsFromGlobal = false;
|
|
2026
2147
|
try {
|
|
2027
2148
|
if (fs.existsSync(agentsDir)) {
|
|
2028
2149
|
agentCount = fs.readdirSync(agentsDir).filter(f => (f.startsWith('rihal-') || f.startsWith('rcode-')) && (f.endsWith('.md') || f.endsWith('.mdc'))).length;
|
|
@@ -2034,6 +2155,22 @@ async function install(opts) {
|
|
|
2034
2155
|
: f => f.endsWith('.md') || f.endsWith('.mdc');
|
|
2035
2156
|
commandCount = fs.readdirSync(commandsDir).filter(commandFilter).length;
|
|
2036
2157
|
}
|
|
2158
|
+
// Issue #669 — when global precedence applied (project copies were
|
|
2159
|
+
// intentionally removed), count from ~/.claude/ instead so the summary
|
|
2160
|
+
// doesn't lie about the install state.
|
|
2161
|
+
if (agentCount === 0 || commandCount === 0) {
|
|
2162
|
+
const os = require('os');
|
|
2163
|
+
const homeAgents = path.join(os.homedir(), '.claude/agents');
|
|
2164
|
+
const homeCommands = path.join(os.homedir(), '.claude/commands');
|
|
2165
|
+
if (agentCount === 0 && fs.existsSync(homeAgents)) {
|
|
2166
|
+
const n = fs.readdirSync(homeAgents).filter(f => f.startsWith('rihal-') && f.endsWith('.md')).length;
|
|
2167
|
+
if (n > 0) { agentCount = n; agentsFromGlobal = true; }
|
|
2168
|
+
}
|
|
2169
|
+
if (commandCount === 0 && fs.existsSync(homeCommands)) {
|
|
2170
|
+
const n = fs.readdirSync(homeCommands).filter(f => f.startsWith('rihal-') && f.endsWith('.md')).length;
|
|
2171
|
+
if (n > 0) { commandCount = n; commandsFromGlobal = true; }
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2037
2174
|
} catch {}
|
|
2038
2175
|
|
|
2039
2176
|
const version = readPackageVersion();
|
|
@@ -2047,8 +2184,8 @@ async function install(opts) {
|
|
|
2047
2184
|
// Show the actual install paths so cursor/gemini/antigravity output is accurate
|
|
2048
2185
|
const relAgents = path.relative(opts.target, idePaths.agentsDir) || idePaths.agentsDir;
|
|
2049
2186
|
const relCommands = path.relative(opts.target, idePaths.commandsDir) || idePaths.commandsDir;
|
|
2050
|
-
console.log(` ${bold('Agents:')} ${pc.green(String(agentCount))} in ${relAgents}
|
|
2051
|
-
console.log(` ${bold('Commands:')} ${pc.green(String(commandCount))} slash commands in ${relCommands}
|
|
2187
|
+
console.log(` ${bold('Agents:')} ${pc.green(String(agentCount))} in ${agentsFromGlobal ? '~/.claude/agents/ (global)' : relAgents + '/'}`);
|
|
2188
|
+
console.log(` ${bold('Commands:')} ${pc.green(String(commandCount))} slash commands in ${commandsFromGlobal ? '~/.claude/commands/ (global)' : relCommands + '/'}`);
|
|
2052
2189
|
if (skillsInstalled > 0) console.log(` ${bold('Skills:')} ${pc.green(String(skillsInstalled))} phrase-activated`);
|
|
2053
2190
|
console.log('');
|
|
2054
2191
|
if (starterSeeded) {
|
package/cli/lib/manifest.cjs
CHANGED
|
@@ -124,6 +124,25 @@ function verifyClaudeInstall(cwd, packageRoot) {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
// Issue #664 — global precedence fallback.
|
|
128
|
+
// The installer (cli/install.js ~line 1773) intentionally removes project-
|
|
129
|
+
// level .claude/agents/rihal-*.md when the user's ~/.claude/ already has
|
|
130
|
+
// them, to avoid duplicate commands. Without this fallback the verifier
|
|
131
|
+
// reports 0 agents on every successful install in that scenario.
|
|
132
|
+
if (installedAgents.size === 0) {
|
|
133
|
+
try {
|
|
134
|
+
const os = require('os');
|
|
135
|
+
const globalAgentsDir = path.join(os.homedir(), '.claude/agents');
|
|
136
|
+
if (fs.existsSync(globalAgentsDir)) {
|
|
137
|
+
for (const f of fs.readdirSync(globalAgentsDir)) {
|
|
138
|
+
if (f.startsWith('rihal-') && f.endsWith('.md')) {
|
|
139
|
+
installedAgents.add(f.replace(/^rihal-/, '').replace(/\.md$/, ''));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch { /* non-fatal — permission errors etc. */ }
|
|
144
|
+
}
|
|
145
|
+
|
|
127
146
|
// Actions: .claude/skills/<bare-name>/ — exclude rihal-* dirs (those are
|
|
128
147
|
// either agent stubs or command stubs, never action skills).
|
|
129
148
|
const allInstalled = readInstalledDirs(skillsDir);
|
package/cli/set-profile.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* rihal-code set-profile — change the model profile for the current project
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
|
-
* npx @
|
|
6
|
-
* npx @
|
|
7
|
-
* npx @
|
|
8
|
-
* npx @
|
|
9
|
-
* npx @
|
|
5
|
+
* npx @hanzlaa/rcode set-profile balanced
|
|
6
|
+
* npx @hanzlaa/rcode set-profile quality
|
|
7
|
+
* npx @hanzlaa/rcode set-profile budget
|
|
8
|
+
* npx @hanzlaa/rcode set-profile inherit
|
|
9
|
+
* npx @hanzlaa/rcode set-profile # show current
|
|
10
10
|
*
|
|
11
11
|
* This is a thin wrapper over `rihal-code config model_profile <name>`.
|
|
12
12
|
* All config read/write goes through cli/lib/config.cjs, which handles
|
package/cli/show-model.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* rihal-code show-model — print the resolved model for an agent (or all agents)
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
|
-
* npx @
|
|
6
|
-
* npx @
|
|
7
|
-
* npx @
|
|
5
|
+
* npx @hanzlaa/rcode show-model # all agents in current profile
|
|
6
|
+
* npx @hanzlaa/rcode show-model waleed # single agent
|
|
7
|
+
* npx @hanzlaa/rcode show-model --profile=quality # different profile
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const {
|
package/cli/uninstall.js
CHANGED
|
@@ -250,8 +250,15 @@ function isKnownSkillName(name) {
|
|
|
250
250
|
/**
|
|
251
251
|
* Build the list of files/dirs (relative to cwd) that the uninstall plan
|
|
252
252
|
* will delete or mutate. Used to feed `tar --files-from=-`.
|
|
253
|
+
*
|
|
254
|
+
* @param {object} plan — uninstall plan
|
|
255
|
+
* @param {string} cwd — project root
|
|
256
|
+
* @param {object} [options]
|
|
257
|
+
* @param {boolean} [options.purge=false] — when true, also include .rihal/
|
|
258
|
+
* and .planning/ in the backup so --purge users can recover state.json,
|
|
259
|
+
* decisions, and planning artifacts. Issue #683.
|
|
253
260
|
*/
|
|
254
|
-
function planToPathList(plan, cwd) {
|
|
261
|
+
function planToPathList(plan, cwd, options = {}) {
|
|
255
262
|
const paths = [];
|
|
256
263
|
|
|
257
264
|
for (const name of plan.claude.skills) {
|
|
@@ -278,6 +285,26 @@ function planToPathList(plan, cwd) {
|
|
|
278
285
|
paths.push('AGENTS.md');
|
|
279
286
|
}
|
|
280
287
|
|
|
288
|
+
// Issue #683: --purge wipes .rihal/ AND .planning/ but the backup never
|
|
289
|
+
// included them. User loses state.json, decisions, planning artifacts with
|
|
290
|
+
// no recovery. Add them when purging — but EXCLUDE .rihal/backups/ itself
|
|
291
|
+
// (we'd be writing into the dir we're tar-ing).
|
|
292
|
+
if (options.purge) {
|
|
293
|
+
const rihalDir = path.join(cwd, '.rihal');
|
|
294
|
+
if (fs.existsSync(rihalDir)) {
|
|
295
|
+
// Walk one level deep and add everything except backups/
|
|
296
|
+
try {
|
|
297
|
+
for (const entry of fs.readdirSync(rihalDir)) {
|
|
298
|
+
if (entry === 'backups') continue;
|
|
299
|
+
paths.push(path.join('.rihal', entry));
|
|
300
|
+
}
|
|
301
|
+
} catch { /* fall through; ok=false from tar will warn */ }
|
|
302
|
+
}
|
|
303
|
+
if (fs.existsSync(path.join(cwd, '.planning'))) {
|
|
304
|
+
paths.push('.planning');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
281
308
|
return paths;
|
|
282
309
|
}
|
|
283
310
|
|
|
@@ -288,8 +315,8 @@ function planToPathList(plan, cwd) {
|
|
|
288
315
|
* (tar missing, no paths, etc.); the caller should warn the user but may
|
|
289
316
|
* still proceed since the user already confirmed the destructive action.
|
|
290
317
|
*/
|
|
291
|
-
function createBackup(cwd, plan) {
|
|
292
|
-
const paths = planToPathList(plan, cwd);
|
|
318
|
+
function createBackup(cwd, plan, options = {}) {
|
|
319
|
+
const paths = planToPathList(plan, cwd, { purge: options.purge === true });
|
|
293
320
|
if (paths.length === 0) {
|
|
294
321
|
return { ok: false, warning: 'nothing to back up' };
|
|
295
322
|
}
|
|
@@ -301,11 +328,17 @@ function createBackup(cwd, plan) {
|
|
|
301
328
|
return { ok: false, warning: 'tar not available on this system' };
|
|
302
329
|
}
|
|
303
330
|
|
|
304
|
-
|
|
331
|
+
// Issue #683: when --purge wipes .rihal/, a backup written into
|
|
332
|
+
// .rihal/backups/ would be deleted moments later. Write to a sibling
|
|
333
|
+
// .rihal-backups/ at the project root instead so the backup survives.
|
|
334
|
+
// For non-purge runs, keep the historical .rihal/backups/ location.
|
|
335
|
+
const backupsDir = options.purge
|
|
336
|
+
? path.join(cwd, '.rihal-backups')
|
|
337
|
+
: path.join(cwd, '.rihal/backups');
|
|
305
338
|
try {
|
|
306
339
|
fs.mkdirSync(backupsDir, { recursive: true });
|
|
307
340
|
} catch (err) {
|
|
308
|
-
return { ok: false, warning: `could not create .
|
|
341
|
+
return { ok: false, warning: `could not create ${path.relative(cwd, backupsDir)}/: ${err.message}` };
|
|
309
342
|
}
|
|
310
343
|
|
|
311
344
|
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
@@ -489,10 +522,15 @@ async function runUninstall(args) {
|
|
|
489
522
|
|
|
490
523
|
// Create a timestamped backup before doing anything destructive.
|
|
491
524
|
// Non-fatal on failure — the user already confirmed, we just warn.
|
|
525
|
+
// Issue #683: --purge backs up .rihal/ and .planning/ too so users can
|
|
526
|
+
// recover state.json, decisions log, and planning artifacts.
|
|
492
527
|
console.log();
|
|
493
|
-
const backup = createBackup(cwd, plan);
|
|
528
|
+
const backup = createBackup(cwd, plan, { purge: opts.purge === true });
|
|
494
529
|
if (backup.ok) {
|
|
495
530
|
console.log(` 💾 backup created: ${backup.path}`);
|
|
531
|
+
if (opts.purge) {
|
|
532
|
+
console.log(' includes .rihal/ and .planning/ (state, decisions, planning artifacts)');
|
|
533
|
+
}
|
|
496
534
|
} else {
|
|
497
535
|
console.log(` ⚠ no backup created (${backup.warning}) — continuing anyway`);
|
|
498
536
|
}
|
|
@@ -649,15 +687,29 @@ async function runUninstall(args) {
|
|
|
649
687
|
|
|
650
688
|
// Strip the rcode-managed block from .gitignore. The installer writes
|
|
651
689
|
// a fenced block; we remove it cleanly without touching user lines.
|
|
690
|
+
//
|
|
691
|
+
// Issue #684: previous regex `/\n?# rcode[\s\S]*?(?=\n\n|\n$|$)/g` was a
|
|
692
|
+
// footgun — it matched ANY user line starting with "# rcode" (e.g.
|
|
693
|
+
// "# rcode notes", "# rcode is great") and greedily consumed everything
|
|
694
|
+
// up to the next blank line, silently nuking user content.
|
|
695
|
+
//
|
|
696
|
+
// Three shapes have ever shipped:
|
|
697
|
+
// 1. Current (install.js:653-654): "# ===== rcode-managed gitignore block ... =====" ... "# ===== end rcode-managed gitignore block ====="
|
|
698
|
+
// 2. Old fenced markers: "# >>> rihal-code >>>" ... "# <<< rihal-code <<<"
|
|
699
|
+
// 3. Hypothetical legacy single-line "# rcode" — never actually
|
|
700
|
+
// committed by any installer version we can find. Removed.
|
|
701
|
+
//
|
|
702
|
+
// Both kept patterns require BOTH sentinel markers to be present —
|
|
703
|
+
// user content with "# rcode" prefix is now safe.
|
|
652
704
|
const gitignorePath = path.join(cwd, '.gitignore');
|
|
653
705
|
if (fs.existsSync(gitignorePath)) {
|
|
654
706
|
try {
|
|
655
707
|
const before = fs.readFileSync(gitignorePath, 'utf8');
|
|
656
|
-
// Match either fenced markers or the legacy "# rcode" header through to
|
|
657
|
-
// the next blank line — both shapes the installer has used historically.
|
|
658
708
|
const stripped = before
|
|
709
|
+
// Current shape (install.js BEGIN/END markers — exact match).
|
|
710
|
+
.replace(/\n?# ===== rcode-managed gitignore block[\s\S]*?# ===== end rcode-managed gitignore block =====\n?/g, '\n')
|
|
711
|
+
// Legacy >>> / <<< fenced shape.
|
|
659
712
|
.replace(/\n?# >>> rihal-code >>>[\s\S]*?# <<< rihal-code <<<\n?/g, '\n')
|
|
660
|
-
.replace(/\n?# rcode[\s\S]*?(?=\n\n|\n$|$)/g, '\n')
|
|
661
713
|
.replace(/\n{3,}/g, '\n\n');
|
|
662
714
|
if (stripped !== before) {
|
|
663
715
|
fs.writeFileSync(gitignorePath, stripped);
|
package/dist/rcode.js
CHANGED
|
@@ -15129,7 +15129,7 @@ var require_install = __commonJS({
|
|
|
15129
15129
|
pc.cyan("\u2502") + " " + dim("A persistent context-brain for your editor") + " " + pc.cyan("\u2502"),
|
|
15130
15130
|
pc.cyan("\u2502") + " " + pc.cyan("\u2502"),
|
|
15131
15131
|
pc.cyan("\u2502") + " " + dim("version ") + pc.green("v" + v) + " " + pc.cyan("\u2502"),
|
|
15132
|
-
pc.cyan("\u2502") + " " + dim("docs ") + "github.com/
|
|
15132
|
+
pc.cyan("\u2502") + " " + dim("docs ") + "github.com/hanzlahabib/rihal-code " + pc.cyan("\u2502"),
|
|
15133
15133
|
pc.cyan("\u2502") + " " + pc.cyan("\u2502"),
|
|
15134
15134
|
pc.cyan("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"),
|
|
15135
15135
|
""
|
|
@@ -16485,6 +16485,7 @@ ${BLOCK}`);
|
|
|
16485
16485
|
const agentsDir = idePaths.agentsDir;
|
|
16486
16486
|
const commandsDir = idePaths.commandsDir;
|
|
16487
16487
|
let agentCount = 0, commandCount = 0;
|
|
16488
|
+
let agentsFromGlobal = false, commandsFromGlobal = false;
|
|
16488
16489
|
try {
|
|
16489
16490
|
if (fs2.existsSync(agentsDir)) {
|
|
16490
16491
|
agentCount = fs2.readdirSync(agentsDir).filter((f) => (f.startsWith("rihal-") || f.startsWith("rcode-")) && (f.endsWith(".md") || f.endsWith(".mdc"))).length;
|
|
@@ -16493,6 +16494,25 @@ ${BLOCK}`);
|
|
|
16493
16494
|
const commandFilter = primaryIde === "claude" ? (f) => f.startsWith("rihal-") && (f.endsWith(".md") || f.endsWith(".mdc")) : (f) => f.endsWith(".md") || f.endsWith(".mdc");
|
|
16494
16495
|
commandCount = fs2.readdirSync(commandsDir).filter(commandFilter).length;
|
|
16495
16496
|
}
|
|
16497
|
+
if (agentCount === 0 || commandCount === 0) {
|
|
16498
|
+
const os2 = require("os");
|
|
16499
|
+
const homeAgents = path2.join(os2.homedir(), ".claude/agents");
|
|
16500
|
+
const homeCommands = path2.join(os2.homedir(), ".claude/commands");
|
|
16501
|
+
if (agentCount === 0 && fs2.existsSync(homeAgents)) {
|
|
16502
|
+
const n = fs2.readdirSync(homeAgents).filter((f) => f.startsWith("rihal-") && f.endsWith(".md")).length;
|
|
16503
|
+
if (n > 0) {
|
|
16504
|
+
agentCount = n;
|
|
16505
|
+
agentsFromGlobal = true;
|
|
16506
|
+
}
|
|
16507
|
+
}
|
|
16508
|
+
if (commandCount === 0 && fs2.existsSync(homeCommands)) {
|
|
16509
|
+
const n = fs2.readdirSync(homeCommands).filter((f) => f.startsWith("rihal-") && f.endsWith(".md")).length;
|
|
16510
|
+
if (n > 0) {
|
|
16511
|
+
commandCount = n;
|
|
16512
|
+
commandsFromGlobal = true;
|
|
16513
|
+
}
|
|
16514
|
+
}
|
|
16515
|
+
}
|
|
16496
16516
|
} catch {
|
|
16497
16517
|
}
|
|
16498
16518
|
const version = readPackageVersion();
|
|
@@ -16505,8 +16525,8 @@ ${BLOCK}`);
|
|
|
16505
16525
|
console.log("");
|
|
16506
16526
|
const relAgents = path2.relative(opts.target, idePaths.agentsDir) || idePaths.agentsDir;
|
|
16507
16527
|
const relCommands = path2.relative(opts.target, idePaths.commandsDir) || idePaths.commandsDir;
|
|
16508
|
-
console.log(` ${bold("Agents:")} ${pc.green(String(agentCount))} in ${relAgents}
|
|
16509
|
-
console.log(` ${bold("Commands:")} ${pc.green(String(commandCount))} slash commands in ${relCommands}
|
|
16528
|
+
console.log(` ${bold("Agents:")} ${pc.green(String(agentCount))} in ${agentsFromGlobal ? "~/.claude/agents/ (global)" : relAgents + "/"}`);
|
|
16529
|
+
console.log(` ${bold("Commands:")} ${pc.green(String(commandCount))} slash commands in ${commandsFromGlobal ? "~/.claude/commands/ (global)" : relCommands + "/"}`);
|
|
16510
16530
|
if (skillsInstalled > 0) console.log(` ${bold("Skills:")} ${pc.green(String(skillsInstalled))} phrase-activated`);
|
|
16511
16531
|
console.log("");
|
|
16512
16532
|
if (starterSeeded) {
|
|
@@ -17070,6 +17090,20 @@ var require_manifest = __commonJS({
|
|
|
17070
17090
|
}
|
|
17071
17091
|
}
|
|
17072
17092
|
}
|
|
17093
|
+
if (installedAgents.size === 0) {
|
|
17094
|
+
try {
|
|
17095
|
+
const os = require("os");
|
|
17096
|
+
const globalAgentsDir = path2.join(os.homedir(), ".claude/agents");
|
|
17097
|
+
if (fs2.existsSync(globalAgentsDir)) {
|
|
17098
|
+
for (const f of fs2.readdirSync(globalAgentsDir)) {
|
|
17099
|
+
if (f.startsWith("rihal-") && f.endsWith(".md")) {
|
|
17100
|
+
installedAgents.add(f.replace(/^rihal-/, "").replace(/\.md$/, ""));
|
|
17101
|
+
}
|
|
17102
|
+
}
|
|
17103
|
+
}
|
|
17104
|
+
} catch {
|
|
17105
|
+
}
|
|
17106
|
+
}
|
|
17073
17107
|
const allInstalled = readInstalledDirs(skillsDir);
|
|
17074
17108
|
const actionsInstalled = new Set(
|
|
17075
17109
|
[...allInstalled].filter((n) => !n.startsWith("rihal-"))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzlaa/rcode",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.22",
|
|
4
4
|
"description": "rcode — the memory bank for AI-driven SaaS teams. Persistent project context, distinctive engineering personas, and phase-based workflows. Built by Rihal. Works in Claude Code, Cursor, Gemini, VS Code, and Antigravity.",
|
|
5
5
|
"main": "cli/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -985,6 +985,26 @@ function cmdState(subArgs) {
|
|
|
985
985
|
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
986
986
|
}
|
|
987
987
|
|
|
988
|
+
// Issue #681: auto-clear the install-time _seeded_stub marker once the
|
|
989
|
+
// state has graduated to a real project (project field set + at least one
|
|
990
|
+
// real phase OR REQUIREMENTS.md present). project-status (#675) reads
|
|
991
|
+
// _seeded_stub; if no writer ever clears it, every project stays "stub"
|
|
992
|
+
// forever and downstream workflows misroute.
|
|
993
|
+
if (state._seeded_stub === true) {
|
|
994
|
+
const phases = Array.isArray(state.phases) ? state.phases : [];
|
|
995
|
+
const firstPhaseName = phases[0]?.name || '';
|
|
996
|
+
const hasRealPhase = phases.length > 1 ||
|
|
997
|
+
(firstPhaseName && firstPhaseName !== 'Setup & Scaffolding');
|
|
998
|
+
const hasRequirements = (() => {
|
|
999
|
+
try {
|
|
1000
|
+
return fs.existsSync(path.join(PROJECT_ROOT, '.planning', 'REQUIREMENTS.md'));
|
|
1001
|
+
} catch { return false; }
|
|
1002
|
+
})();
|
|
1003
|
+
if ((state.project && hasRealPhase) || hasRequirements) {
|
|
1004
|
+
delete state._seeded_stub;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
988
1008
|
state.updated = new Date().toISOString();
|
|
989
1009
|
fs.mkdirSync(RIHAL_DIR, { recursive: true });
|
|
990
1010
|
const lockPath = statePath + '.lock';
|
|
@@ -1069,6 +1089,23 @@ function cmdState(subArgs) {
|
|
|
1069
1089
|
return state;
|
|
1070
1090
|
}
|
|
1071
1091
|
|
|
1092
|
+
// --- clear-stub --- (issue #681)
|
|
1093
|
+
// Explicit way to flip _seeded_stub off. Useful for /rihal-new-project once
|
|
1094
|
+
// PROJECT.md / REQUIREMENTS.md / ROADMAP.md are committed. The auto-clear in
|
|
1095
|
+
// writeState() also handles this, but having an explicit subcommand lets
|
|
1096
|
+
// workflows be self-documenting and idempotent.
|
|
1097
|
+
if (sub === 'clear-stub') {
|
|
1098
|
+
if (!fs.existsSync(statePath)) {
|
|
1099
|
+
return { ok: false, error: 'No state.json — nothing to clear.' };
|
|
1100
|
+
}
|
|
1101
|
+
const state = readState();
|
|
1102
|
+
if (!state) return { ok: false, error: 'state.json unreadable' };
|
|
1103
|
+
const wasStub = state._seeded_stub === true;
|
|
1104
|
+
if (wasStub) delete state._seeded_stub;
|
|
1105
|
+
writeState(state);
|
|
1106
|
+
return { ok: true, was_stub: wasStub, project: state.project || null };
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1072
1109
|
// --- init ---
|
|
1073
1110
|
if (sub === 'init') {
|
|
1074
1111
|
let existing;
|
|
@@ -5278,6 +5315,62 @@ function cmdSummaryExtract(args) {
|
|
|
5278
5315
|
* Hides internal machinery (lock metadata, full history) from callers
|
|
5279
5316
|
* that only need a render-ready summary.
|
|
5280
5317
|
*/
|
|
5318
|
+
/**
|
|
5319
|
+
* cmdProjectStatus — classify project lifecycle state into one of:
|
|
5320
|
+
* uninstalled — no .rihal/config.yaml
|
|
5321
|
+
* uninitialized — config present, no state.json
|
|
5322
|
+
* stub — install-seeded scaffolding only (issue #670)
|
|
5323
|
+
* real — /rihal-new-project has run
|
|
5324
|
+
*
|
|
5325
|
+
* Real-project signals (any → real):
|
|
5326
|
+
* - .planning/REQUIREMENTS.md exists
|
|
5327
|
+
* - .planning/research/ directory exists
|
|
5328
|
+
* - state.phases.length > 1
|
|
5329
|
+
* - first phase name ≠ "Setup & Scaffolding"
|
|
5330
|
+
*
|
|
5331
|
+
* Closes #675 — single source of truth for "is this project initialized."
|
|
5332
|
+
*/
|
|
5333
|
+
function cmdProjectStatus() {
|
|
5334
|
+
const configPath = path.join(RIHAL_DIR, 'config.yaml');
|
|
5335
|
+
const statePath = path.join(RIHAL_DIR, 'state.json');
|
|
5336
|
+
const planningDir = path.join(PROJECT_ROOT, '.planning');
|
|
5337
|
+
|
|
5338
|
+
if (!fs.existsSync(configPath)) return { ok: true, status: 'uninstalled' };
|
|
5339
|
+
if (!fs.existsSync(statePath)) return { ok: true, status: 'uninitialized' };
|
|
5340
|
+
|
|
5341
|
+
let state;
|
|
5342
|
+
try { state = JSON.parse(fs.readFileSync(statePath, 'utf8')); }
|
|
5343
|
+
catch (e) { return { ok: false, error: `invalid state.json: ${e.message}` }; }
|
|
5344
|
+
|
|
5345
|
+
const hasRequirements = fs.existsSync(path.join(planningDir, 'REQUIREMENTS.md'));
|
|
5346
|
+
const hasResearch = fs.existsSync(path.join(planningDir, 'research'));
|
|
5347
|
+
const phases = state.phases || [];
|
|
5348
|
+
const phaseCountReal = phases.length > 1;
|
|
5349
|
+
const firstPhaseName = phases[0]?.name || '';
|
|
5350
|
+
const phaseNameReal = firstPhaseName && firstPhaseName !== 'Setup & Scaffolding';
|
|
5351
|
+
|
|
5352
|
+
const isReal = hasRequirements || hasResearch || phaseCountReal || phaseNameReal;
|
|
5353
|
+
const isStub = state._seeded_stub === true || !state.project || !isReal;
|
|
5354
|
+
|
|
5355
|
+
let status;
|
|
5356
|
+
if (isReal) status = 'real';
|
|
5357
|
+
else if (isStub) status = 'stub';
|
|
5358
|
+
else status = 'uninitialized';
|
|
5359
|
+
|
|
5360
|
+
return {
|
|
5361
|
+
ok: true,
|
|
5362
|
+
status,
|
|
5363
|
+
signals: {
|
|
5364
|
+
project: state.project || null,
|
|
5365
|
+
seeded_stub: state._seeded_stub === true,
|
|
5366
|
+
has_requirements: hasRequirements,
|
|
5367
|
+
has_research: hasResearch,
|
|
5368
|
+
phase_count: phases.length,
|
|
5369
|
+
first_phase_name: firstPhaseName || null,
|
|
5370
|
+
},
|
|
5371
|
+
};
|
|
5372
|
+
}
|
|
5373
|
+
|
|
5281
5374
|
function cmdStateSnapshot() {
|
|
5282
5375
|
const statePath = path.join(RIHAL_DIR, 'state.json');
|
|
5283
5376
|
if (!fs.existsSync(statePath)) return { ok: true, state: null };
|
|
@@ -5652,6 +5745,9 @@ async function main() {
|
|
|
5652
5745
|
case 'agent-skills':
|
|
5653
5746
|
result = cmdAgentInfo(args[0]);
|
|
5654
5747
|
break;
|
|
5748
|
+
case 'project-status':
|
|
5749
|
+
result = cmdProjectStatus();
|
|
5750
|
+
break;
|
|
5655
5751
|
case 'version':
|
|
5656
5752
|
console.log(readPackageVersion());
|
|
5657
5753
|
return;
|
package/rihal/state.json
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "1",
|
|
3
|
-
"project":
|
|
3
|
+
"project": null,
|
|
4
|
+
"_seeded_stub": true,
|
|
4
5
|
"created": "__INSTALL_DATE__",
|
|
5
6
|
"updated": "__INSTALL_DATE__",
|
|
6
|
-
"current_phase":
|
|
7
|
+
"current_phase": null,
|
|
7
8
|
"current_plan": 0,
|
|
8
9
|
"current_sprint": null,
|
|
9
|
-
"milestone":
|
|
10
|
-
"phases": [
|
|
11
|
-
{ "id": "01", "name": "Setup & Scaffolding", "status": "planned" }
|
|
12
|
-
],
|
|
10
|
+
"milestone": null,
|
|
11
|
+
"phases": [],
|
|
13
12
|
"executions": [],
|
|
14
13
|
"decisions": [],
|
|
15
14
|
"blockers": [],
|
|
@@ -88,25 +88,67 @@ Valid Rihal subagent types (use exact names — do not fall back to 'general-pur
|
|
|
88
88
|
- rihal-roadmapper — Creates phased execution roadmaps
|
|
89
89
|
</available_agent_types>
|
|
90
90
|
|
|
91
|
-
## Step 0.5 — Detect existing project (redirect)
|
|
91
|
+
## Step 0.5 — Detect existing project (stub-aware redirect)
|
|
92
92
|
|
|
93
|
-
Before any processing,
|
|
93
|
+
Before any processing, classify the project state into one of:
|
|
94
|
+
|
|
95
|
+
- **none** — no `.rihal/state.json`, no `.planning/` → proceed
|
|
96
|
+
- **stub** — install-seeded scaffolding only (issue #670) → proceed (overwrite stub)
|
|
97
|
+
- **real** — a previous `/rihal-new-project` ran here → guard, unless `--force`
|
|
94
98
|
|
|
95
99
|
```bash
|
|
96
|
-
|
|
100
|
+
# --force / --reinit bypasses the guard entirely (issue #672).
|
|
101
|
+
# --auto implies --force on stub state (issue #674).
|
|
102
|
+
FORCE=false
|
|
103
|
+
case " $ARGUMENTS " in
|
|
104
|
+
*" --force "*|*" --reinit "*) FORCE=true ;;
|
|
105
|
+
esac
|
|
106
|
+
|
|
107
|
+
# Single source of truth: rihal-tools project-status returns one of
|
|
108
|
+
# uninstalled | uninitialized | stub | real
|
|
109
|
+
# (see issue #675 for the contract). Falls back to `none` when
|
|
110
|
+
# rihal-tools is unavailable so the workflow still proceeds.
|
|
111
|
+
PROJECT_STATE=$(node .rihal/bin/rihal-tools.cjs project-status 2>/dev/null \
|
|
112
|
+
| node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{try{console.log(JSON.parse(s).status||'none')}catch{console.log('none')}})" \
|
|
113
|
+
|| echo "none")
|
|
114
|
+
[ "$PROJECT_STATE" = "uninstalled" ] || [ "$PROJECT_STATE" = "uninitialized" ] && PROJECT_STATE="none"
|
|
97
115
|
```
|
|
98
116
|
|
|
99
|
-
If
|
|
117
|
+
**If `PROJECT_STATE=real` and `FORCE=false`:** show the guard:
|
|
100
118
|
|
|
101
119
|
```
|
|
102
120
|
⚠ A rihal project already exists here.
|
|
103
121
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
122
|
+
Quick actions:
|
|
123
|
+
/rihal-status check current state
|
|
124
|
+
/rihal-next find next action
|
|
125
|
+
/rihal-add-phase add a phase to the current milestone
|
|
126
|
+
|
|
127
|
+
To start over (overwrites .planning/* and .rihal/state.json):
|
|
128
|
+
/rihal-new-project --force <description>
|
|
129
|
+
rcode install --reset nuclear option — wipes config + state
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
STOP — do not proceed.
|
|
133
|
+
|
|
134
|
+
**If `PROJECT_STATE=stub` (issue #670 install scaffolding):** print a one-liner and proceed:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
ℹ Install stub detected — overwriting with real project setup.
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**If `PROJECT_STATE=none`:** proceed silently.
|
|
141
|
+
|
|
142
|
+
**If `PROJECT_STATE=real` and `FORCE=true`:** create a rollback tag, then proceed:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
if git rev-parse --git-dir >/dev/null 2>&1; then
|
|
146
|
+
TAG="pre-rihal-rewrite-$(date +%Y%m%d-%H%M%S)"
|
|
147
|
+
git tag "$TAG" 2>/dev/null && echo "ℹ Rollback tag created: $TAG"
|
|
148
|
+
fi
|
|
107
149
|
```
|
|
108
150
|
|
|
109
|
-
|
|
151
|
+
In interactive mode (not `--auto`), confirm via AskUserQuestion before overwriting. In `--auto` or YOLO mode, proceed without confirmation.
|
|
110
152
|
|
|
111
153
|
<auto_mode>
|
|
112
154
|
|