@hanzlaa/rcode 4.4.4 → 4.6.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/AGENTS.md +11 -4
- package/CLAUDE.md +11 -4
- package/CONTRIBUTING.md +6 -1
- package/cli/doctor.js +122 -0
- package/cli/index.js +4 -0
- package/cli/install.js +8 -0
- package/cli/lib/namespace-migrate.cjs +216 -0
- package/cli/migrate-namespace.js +61 -0
- package/cli/update.js +19 -0
- package/dist/rcode.js +215 -208
- package/package.json +14 -12
- package/rcode/agents/rcode-mariam.md +6 -0
- package/rcode/agents/rcode-sadiq.md +6 -0
- package/rcode/agents/rcode-waleed.md +6 -0
- package/rcode/bin/lib/brain.cjs +353 -0
- package/rcode/bin/lib/gitignore.cjs +126 -0
- package/rcode/bin/lib/memory-drift.cjs +237 -0
- package/rcode/bin/lib/memory-select.cjs +263 -0
- package/rcode/bin/lib/progress.cjs +440 -0
- package/rcode/bin/lib/state-reader.cjs +127 -0
- package/rcode/bin/lib/summary.cjs +82 -0
- package/rcode/bin/rcode-hooks.cjs +230 -64
- package/rcode/bin/rcode-tools.cjs +106 -985
- package/rcode/data/intent-table.json +19 -19
- package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/templates/heartbeat.sh +0 -0
- package/rcode/skills/agents/mariam-marketing/SKILL.md +1 -0
- package/rcode/skills/agents/sadiq-analyst/SKILL.md +1 -0
- package/rcode/skills/agents/waleed-architect/SKILL.md +1 -0
- package/rcode/templates/memory/INDEX.md +6 -1
- package/rcode/templates/settings-hooks.json +12 -1
- package/rcode/workflows/council.md +29 -1
- package/rcode/workflows/do.md +2 -2
- package/rcode/workflows/enable-hooks.md +3 -2
- package/rcode/workflows/new-project-roadmap.md +2 -2
- package/server/dashboard.js +6 -3
- package/server/lib/html/client/components/App.js +1 -1
- package/server/lib/html/client/components/OrchPanel.js +2 -2
- package/server/lib/html/client/components/XtermPanel.js +98 -23
- package/server/lib/html/client/orchestrator.js +28 -15
- package/server/lib/html/client/views/OrchestrationView.js +247 -169
- package/server/lib/html/css.js +596 -227
- package/server/lib/html/shell.js +9 -3
- package/server/orchestrator.js +3 -4
package/AGENTS.md
CHANGED
|
@@ -68,8 +68,8 @@ If a user says "just keep going" or "don't stop until done", that authorization
|
|
|
68
68
|
|
|
69
69
|
## Dashboard Server Rules
|
|
70
70
|
|
|
71
|
-
-
|
|
72
|
-
-
|
|
71
|
+
- `server/dashboard.js` is the entry point; server-side logic lives in `server/lib/` (`api.js`, `scanner.js`, `html/`) and `server/orchestrator.js` — split across files, not single-file
|
|
72
|
+
- No external framework dependency on the server or client — the client is a Preact SPA built on a vendored copy (`server/lib/html/client/vendor/preact.js`), not an npm package; keep it that way
|
|
73
73
|
- View-only — NEVER add write endpoints, POST handlers, or database code
|
|
74
74
|
- Test after every change: `node server/dashboard.js` must start cleanly
|
|
75
75
|
|
|
@@ -84,9 +84,16 @@ If a user says "just keep going" or "don't stop until done", that authorization
|
|
|
84
84
|
node cli/doctor.js # full preflight + compliance
|
|
85
85
|
node --test test/artifact-schema.test.cjs # schema validators only
|
|
86
86
|
```
|
|
87
|
-
- Run grep checks before committing renames or refactors
|
|
87
|
+
- Run grep checks before committing renames or refactors. Scope to `TODO:`
|
|
88
|
+
and code files only — `rcode/`, `docs/`, and skill/workflow markdown
|
|
89
|
+
legitimately discuss the concept of TODOs (stub-detection guides, the
|
|
90
|
+
`add-todo`/`check-todos` commands, checklists); an unscoped `-i "TODO"`
|
|
91
|
+
grep will always be red on this repo:
|
|
88
92
|
```bash
|
|
89
|
-
grep -rn
|
|
93
|
+
grep -rn "TODO:" rcode docs examples README.md server \
|
|
94
|
+
--include="*.js" --include="*.cjs" --include="*.mjs" --include="*.ts"
|
|
95
|
+
# should be empty, or only match deliberate string/regex literals
|
|
96
|
+
# (e.g. rcode/bin/rcode-hooks.cjs's hedging-language detector)
|
|
90
97
|
```
|
|
91
98
|
- Test the dashboard server boots without errors before committing dashboard changes
|
|
92
99
|
|
package/CLAUDE.md
CHANGED
|
@@ -58,8 +58,8 @@ If a user says "just keep going" or "don't stop until done", that authorization
|
|
|
58
58
|
|
|
59
59
|
## Dashboard Server Rules
|
|
60
60
|
|
|
61
|
-
-
|
|
62
|
-
-
|
|
61
|
+
- `server/dashboard.js` is the entry point; server-side logic lives in `server/lib/` (`api.js`, `scanner.js`, `html/`) and `server/orchestrator.js` — split across files, not single-file
|
|
62
|
+
- No external framework dependency on the server or client — the client is a Preact SPA built on a vendored copy (`server/lib/html/client/vendor/preact.js`), not an npm package; keep it that way
|
|
63
63
|
- View-only — NEVER add write endpoints, POST handlers, or database code
|
|
64
64
|
- Test after every change: `node server/dashboard.js` must start cleanly
|
|
65
65
|
|
|
@@ -74,9 +74,16 @@ If a user says "just keep going" or "don't stop until done", that authorization
|
|
|
74
74
|
grep -q "^## Examples" "$f" || echo "MISSING: $f"
|
|
75
75
|
done
|
|
76
76
|
```
|
|
77
|
-
- Run grep checks before committing renames or refactors
|
|
77
|
+
- Run grep checks before committing renames or refactors. Scope to `TODO:`
|
|
78
|
+
and code files only — `rcode/`, `docs/`, and skill/workflow markdown
|
|
79
|
+
legitimately discuss the concept of TODOs (stub-detection guides, the
|
|
80
|
+
`add-todo`/`check-todos` commands, checklists); an unscoped `-i "TODO"`
|
|
81
|
+
grep will always be red on this repo:
|
|
78
82
|
```bash
|
|
79
|
-
grep -rn
|
|
83
|
+
grep -rn "TODO:" rcode docs examples README.md server \
|
|
84
|
+
--include="*.js" --include="*.cjs" --include="*.mjs" --include="*.ts"
|
|
85
|
+
# should be empty, or only match deliberate string/regex literals
|
|
86
|
+
# (e.g. rcode/bin/rcode-hooks.cjs's hedging-language detector)
|
|
80
87
|
```
|
|
81
88
|
- Test the dashboard server boots without errors before committing dashboard changes
|
|
82
89
|
|
package/CONTRIBUTING.md
CHANGED
|
@@ -399,7 +399,12 @@ chore(github): import Siraaj commit and PR rules
|
|
|
399
399
|
```
|
|
400
400
|
4. **Grep for regressions:**
|
|
401
401
|
```bash
|
|
402
|
-
|
|
402
|
+
# Scope to "TODO:" and code files — rcode/, docs/, and skill/workflow
|
|
403
|
+
# markdown legitimately discuss TODOs (stub-detection guides, the
|
|
404
|
+
# add-todo/check-todos commands); an unscoped -i "TODO" grep is always red.
|
|
405
|
+
grep -rn "TODO:" rcode docs examples README.md server \
|
|
406
|
+
--include="*.js" --include="*.cjs" --include="*.mjs" --include="*.ts"
|
|
407
|
+
# should be empty, or only match deliberate string/regex literals
|
|
403
408
|
grep -rn "ahmed" rcode docs README.md server | grep -v "ahmed-hassani\|Ahmed Al Hassani" # should be empty
|
|
404
409
|
```
|
|
405
410
|
|
package/cli/doctor.js
CHANGED
|
@@ -17,6 +17,8 @@ const path = require('path');
|
|
|
17
17
|
const { spawnSync } = require('child_process');
|
|
18
18
|
const { verifyInstall, formatReport } = require('./lib/manifest.cjs');
|
|
19
19
|
const { checkStaleness } = require('./lib/memory-bank.cjs');
|
|
20
|
+
const { homedir } = require('./lib/homedir.cjs');
|
|
21
|
+
const { scanNamespaceDuplication } = require('./lib/namespace-migrate.cjs');
|
|
20
22
|
const {
|
|
21
23
|
parseFrontmatter,
|
|
22
24
|
validateSkillFrontmatter,
|
|
@@ -102,6 +104,29 @@ function commandAvailable(cmd) {
|
|
|
102
104
|
return result.status === 0;
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Detect phases stuck at status 'executing' while a numerically later phase
|
|
109
|
+
* has already reached 'complete'. That combination only happens when an
|
|
110
|
+
* executor crashed/was killed without updating state.json — a real phase
|
|
111
|
+
* would never be marked complete while an earlier one is still mid-run.
|
|
112
|
+
* Read-only; used by `rcode doctor` to flag stale state for manual repair.
|
|
113
|
+
*/
|
|
114
|
+
function findStuckExecutingPhases(state) {
|
|
115
|
+
const phases = Array.isArray(state?.phases) ? state.phases : [];
|
|
116
|
+
const executing = phases.filter((p) => p && p.status === 'executing');
|
|
117
|
+
if (executing.length === 0) return [];
|
|
118
|
+
const maxCompleteNumber = phases
|
|
119
|
+
.filter((p) => p && p.status === 'complete')
|
|
120
|
+
.map((p) => parseFloat(p.number))
|
|
121
|
+
.filter((n) => !Number.isNaN(n))
|
|
122
|
+
.reduce((max, n) => Math.max(max, n), -Infinity);
|
|
123
|
+
if (maxCompleteNumber === -Infinity) return [];
|
|
124
|
+
return executing.filter((p) => {
|
|
125
|
+
const n = parseFloat(p.number);
|
|
126
|
+
return !Number.isNaN(n) && n < maxCompleteNumber;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
105
130
|
// ---------- Preflight checks ----------
|
|
106
131
|
|
|
107
132
|
/**
|
|
@@ -192,6 +217,36 @@ function runPreflight(cwd, packageRoot) {
|
|
|
192
217
|
: 'not found (github-sync unavailable)',
|
|
193
218
|
});
|
|
194
219
|
|
|
220
|
+
// 5b. Namespace duplication (#954) — legacy rihal-* entries whose rcode-*
|
|
221
|
+
// twin already exists, plus unprefixed/cross-scope command dupes. Doesn't
|
|
222
|
+
// require .rcode/ init since it inspects .claude/ directly. Read-only —
|
|
223
|
+
// fix path is `rcode update` or `rcode migrate-namespace --yes`.
|
|
224
|
+
try {
|
|
225
|
+
const nsDup = scanNamespaceDuplication(cwd, homedir());
|
|
226
|
+
if (nsDup.totalCount > 0) {
|
|
227
|
+
checks.push({
|
|
228
|
+
label: 'Namespace duplication',
|
|
229
|
+
status: 'warn',
|
|
230
|
+
message:
|
|
231
|
+
`${nsDup.totalCount} duplicate registration(s) — legacy skills: ${nsDup.legacySkillCount}, ` +
|
|
232
|
+
`legacy commands: ${nsDup.legacyCommandCount}, unprefixed: ${nsDup.unprefixedCount}, ` +
|
|
233
|
+
`cross-scope: ${nsDup.crossScopeCount} (run \`rcode migrate-namespace --yes\` or \`rcode update\`)`,
|
|
234
|
+
});
|
|
235
|
+
} else {
|
|
236
|
+
checks.push({
|
|
237
|
+
label: 'Namespace duplication',
|
|
238
|
+
status: 'ok',
|
|
239
|
+
message: 'no duplicate rihal-*/rcode-* registrations found',
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
} catch (e) {
|
|
243
|
+
checks.push({
|
|
244
|
+
label: 'Namespace duplication',
|
|
245
|
+
status: 'warn',
|
|
246
|
+
message: `could not scan: ${e.message}`,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
195
250
|
// 6. Agent manifest drift (only if .rcode/ is initialized — indicates installed editors)
|
|
196
251
|
if (fs.existsSync(rcodeDir)) {
|
|
197
252
|
const editors = [];
|
|
@@ -254,6 +309,69 @@ function runPreflight(cwd, packageRoot) {
|
|
|
254
309
|
message: `STALE — ${staleness.reasons[0]}${staleness.reasons.length > 1 ? ` (+${staleness.reasons.length - 1} more)` : ''}`,
|
|
255
310
|
});
|
|
256
311
|
}
|
|
312
|
+
|
|
313
|
+
// 8. .rcode/data/intent-table.json presence (#952/#954). Installer ships
|
|
314
|
+
// rcode/data/ into consumer projects as of #952; if it's missing, the
|
|
315
|
+
// prompt-router degrades to a no-op with no visible error. Warn-only —
|
|
316
|
+
// the fail-open behavior in rcode-hooks.cjs keeps hooks working either way.
|
|
317
|
+
const intentTablePath = path.join(rcodeDir, 'data', 'intent-table.json');
|
|
318
|
+
checks.push({
|
|
319
|
+
label: 'intent-table.json',
|
|
320
|
+
status: fs.existsSync(intentTablePath) ? 'ok' : 'warn',
|
|
321
|
+
message: fs.existsSync(intentTablePath)
|
|
322
|
+
? path.relative(cwd, intentTablePath)
|
|
323
|
+
: 'missing — run `npx @hanzlaa/rcode update` to sync .rcode/data/',
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// 9. Stale phase state — a phase stuck 'executing' while a numerically
|
|
327
|
+
// later phase has already reached 'complete' means an executor died
|
|
328
|
+
// without updating state.json. Warn-only; fix is manual state repair.
|
|
329
|
+
const statePath = path.join(rcodeDir, 'state.json');
|
|
330
|
+
if (fs.existsSync(statePath)) {
|
|
331
|
+
try {
|
|
332
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
333
|
+
const stuck = findStuckExecutingPhases(state);
|
|
334
|
+
checks.push({
|
|
335
|
+
label: 'Phase state',
|
|
336
|
+
status: stuck.length > 0 ? 'warn' : 'ok',
|
|
337
|
+
message:
|
|
338
|
+
stuck.length > 0
|
|
339
|
+
? `${stuck.length} phase(s) stuck 'executing' while a later phase is complete: ${stuck.map((p) => p.number).join(', ')}`
|
|
340
|
+
: 'no stuck phases',
|
|
341
|
+
});
|
|
342
|
+
} catch (e) {
|
|
343
|
+
checks.push({
|
|
344
|
+
label: 'Phase state',
|
|
345
|
+
status: 'warn',
|
|
346
|
+
message: `state.json unreadable: ${e.message}`,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 10. Memory INDEX.md staleness — separate from the fingerprint-based
|
|
352
|
+
// "Memory bank" check above (#7), which tracks .rcode/context/. This is
|
|
353
|
+
// a simple wall-clock check on .rcode/memory/INDEX.md, the distilled
|
|
354
|
+
// memory index every workflow reads.
|
|
355
|
+
const memoryIndexPath = path.join(rcodeDir, 'memory', 'INDEX.md');
|
|
356
|
+
if (fs.existsSync(memoryIndexPath)) {
|
|
357
|
+
const ageDays = Math.floor(
|
|
358
|
+
(Date.now() - fs.statSync(memoryIndexPath).mtimeMs) / (24 * 60 * 60 * 1000),
|
|
359
|
+
);
|
|
360
|
+
checks.push({
|
|
361
|
+
label: 'Memory INDEX.md',
|
|
362
|
+
status: ageDays > 30 ? 'warn' : 'ok',
|
|
363
|
+
message:
|
|
364
|
+
ageDays > 30
|
|
365
|
+
? `STALE — last updated ${ageDays}d ago (run /rcode-memory-update)`
|
|
366
|
+
: `fresh — updated ${ageDays}d ago`,
|
|
367
|
+
});
|
|
368
|
+
} else {
|
|
369
|
+
checks.push({
|
|
370
|
+
label: 'Memory INDEX.md',
|
|
371
|
+
status: 'warn',
|
|
372
|
+
message: 'missing (run /rcode-memory-init)',
|
|
373
|
+
});
|
|
374
|
+
}
|
|
257
375
|
}
|
|
258
376
|
|
|
259
377
|
return checks;
|
|
@@ -479,3 +597,7 @@ module.exports = function doctor(args, { packageRoot }) {
|
|
|
479
597
|
console.log();
|
|
480
598
|
process.exit(totalFailures > 0 ? 1 : 0);
|
|
481
599
|
};
|
|
600
|
+
|
|
601
|
+
// Re-exports for unit tests.
|
|
602
|
+
module.exports.findStuckExecutingPhases = findStuckExecutingPhases;
|
|
603
|
+
module.exports.runPreflight = runPreflight;
|
package/cli/index.js
CHANGED
|
@@ -27,6 +27,7 @@ const COMMANDS = {
|
|
|
27
27
|
uninstall: require('./uninstall'),
|
|
28
28
|
remove: require('./uninstall'), // alias
|
|
29
29
|
nuke: require('./nuke'), // full cleanup across all package managers + global state
|
|
30
|
+
'migrate-namespace': require('./migrate-namespace'), // clean up legacy rihal-* dupes (#954)
|
|
30
31
|
dashboard: require('./dashboard'),
|
|
31
32
|
serve: require('./dashboard'),
|
|
32
33
|
digest: require('./digest'),
|
|
@@ -73,6 +74,9 @@ Usage:
|
|
|
73
74
|
binaries, ~/.claude/* rcode artifacts, ~/.rcode/, project artifacts)
|
|
74
75
|
Default = dry-run. Pass --yes to remove. Pass --include-planning
|
|
75
76
|
to also remove .planning/ in CWD.
|
|
77
|
+
migrate-namespace Back up + remove legacy rihal-* skills/commands whose
|
|
78
|
+
rcode-* twin exists, plus unprefixed/cross-scope command dupes.
|
|
79
|
+
Default = dry-run. Pass --yes to remove. Idempotent.
|
|
76
80
|
config Get/set project configuration (project_name, user_name, etc.)
|
|
77
81
|
context Memory bank freshness (--check | --refresh | --install-hook)
|
|
78
82
|
github-sync Sync .rcode/ phases/epics/stories to GitHub (dry-run default)
|
package/cli/install.js
CHANGED
|
@@ -888,6 +888,7 @@ function ensureRcodeGitignore(target, options = {}) {
|
|
|
888
888
|
'# Installed methodology files (regenerate with: npx @hanzlaa/rcode install)',
|
|
889
889
|
'.claude/',
|
|
890
890
|
'.rcode/bin/',
|
|
891
|
+
'.rcode/data/',
|
|
891
892
|
'.rcode/workflows/',
|
|
892
893
|
'.rcode/references/',
|
|
893
894
|
'.rcode/commands/',
|
|
@@ -1310,6 +1311,12 @@ function buildInstallPlan(ide = 'claude', target = process.cwd()) {
|
|
|
1310
1311
|
plan.push({ src: f, rel: path.join(relBin, rel), executable: f.endsWith('.cjs') });
|
|
1311
1312
|
}
|
|
1312
1313
|
|
|
1314
|
+
// .rcode/data/
|
|
1315
|
+
for (const f of walkFiles(path.join(SOURCE_ROOT, 'data'))) {
|
|
1316
|
+
const rel = path.relative(path.join(SOURCE_ROOT, 'data'), f);
|
|
1317
|
+
plan.push({ src: f, rel: path.join('.rcode', 'data', rel) });
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1313
1320
|
// .rcode/templates/projects/ — starter templates consumed by /rcode-from-template
|
|
1314
1321
|
const projectTemplatesSrc = path.join(SOURCE_ROOT, 'templates', 'projects');
|
|
1315
1322
|
const relProjectTemplates = path.relative(target, path.join(target, '.rcode', 'templates', 'projects'));
|
|
@@ -1418,6 +1425,7 @@ function filterPlanByModules(plan, moduleNames) {
|
|
|
1418
1425
|
// Always include bin/ (shared infrastructure, not module-specific)
|
|
1419
1426
|
return plan.filter((entry) => {
|
|
1420
1427
|
if (entry.rel.startsWith(path.join('.rcode', 'bin'))) return true;
|
|
1428
|
+
if (entry.rel.startsWith(path.join('.rcode', 'data'))) return true;
|
|
1421
1429
|
return allowed.has(entry.rel);
|
|
1422
1430
|
});
|
|
1423
1431
|
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Legacy rihal-* → rcode-* namespace cleanup (#954).
|
|
4
|
+
*
|
|
5
|
+
* The rcode↔rihal rename left rihal-* skills/commands installed alongside
|
|
6
|
+
* their rcode-* twins everywhere — every session loaded each command 2-4x.
|
|
7
|
+
* This module scans for safe-to-remove legacy artifacts (a rihal-* entry is
|
|
8
|
+
* only ever flagged when its rcode-* twin already exists — never delete
|
|
9
|
+
* something with no replacement) and, on request, backs them up under
|
|
10
|
+
* ~/.claude/.rcode-backup/<timestamp>/ before deleting.
|
|
11
|
+
*
|
|
12
|
+
* Read-only scan functions (findLegacyRihalArtifacts, findUnprefixedTwinDupes,
|
|
13
|
+
* findCrossScopeDupes, scanNamespaceDuplication) are reused by `rcode doctor`
|
|
14
|
+
* for report-only checks. The destructive migrateNamespace() is reused by
|
|
15
|
+
* `rcode update` and `rcode migrate-namespace`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
function listDirSafe(dir) {
|
|
22
|
+
if (!fs.existsSync(dir)) return [];
|
|
23
|
+
try {
|
|
24
|
+
return fs.readdirSync(dir, { withFileTypes: true });
|
|
25
|
+
} catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Legacy rihal-* skill dirs / command files whose rcode-* twin exists in
|
|
32
|
+
* the SAME .claude dir. Read-only.
|
|
33
|
+
*/
|
|
34
|
+
function findLegacyRihalArtifacts(claudeDir) {
|
|
35
|
+
const skills = [];
|
|
36
|
+
const skillsDir = path.join(claudeDir, 'skills');
|
|
37
|
+
for (const entry of listDirSafe(skillsDir)) {
|
|
38
|
+
if (!entry.isDirectory() || !entry.name.startsWith('rihal-')) continue;
|
|
39
|
+
const twinName = 'rcode-' + entry.name.slice('rihal-'.length);
|
|
40
|
+
if (fs.existsSync(path.join(skillsDir, twinName))) {
|
|
41
|
+
skills.push({
|
|
42
|
+
name: entry.name,
|
|
43
|
+
twin: twinName,
|
|
44
|
+
srcPath: path.join(skillsDir, entry.name),
|
|
45
|
+
kind: 'skill',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const commands = [];
|
|
51
|
+
const commandsDir = path.join(claudeDir, 'commands');
|
|
52
|
+
for (const entry of listDirSafe(commandsDir)) {
|
|
53
|
+
if (!entry.isFile() || !entry.name.startsWith('rihal-') || !entry.name.endsWith('.md')) continue;
|
|
54
|
+
const twinName = 'rcode-' + entry.name.slice('rihal-'.length);
|
|
55
|
+
if (fs.existsSync(path.join(commandsDir, twinName))) {
|
|
56
|
+
commands.push({
|
|
57
|
+
name: entry.name,
|
|
58
|
+
twin: twinName,
|
|
59
|
+
srcPath: path.join(commandsDir, entry.name),
|
|
60
|
+
kind: 'command',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { skills, commands };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Unprefixed command files (e.g. `do.md`) that duplicate an already-installed
|
|
70
|
+
* `rcode-do.md` twin in the SAME .claude/commands dir. Read-only.
|
|
71
|
+
*/
|
|
72
|
+
function findUnprefixedTwinDupes(claudeDir) {
|
|
73
|
+
const dupes = [];
|
|
74
|
+
const commandsDir = path.join(claudeDir, 'commands');
|
|
75
|
+
for (const entry of listDirSafe(commandsDir)) {
|
|
76
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
77
|
+
if (entry.name.startsWith('rcode-') || entry.name.startsWith('rihal-')) continue;
|
|
78
|
+
const twinName = 'rcode-' + entry.name;
|
|
79
|
+
if (fs.existsSync(path.join(commandsDir, twinName))) {
|
|
80
|
+
dupes.push({
|
|
81
|
+
name: entry.name,
|
|
82
|
+
twin: twinName,
|
|
83
|
+
srcPath: path.join(commandsDir, entry.name),
|
|
84
|
+
kind: 'command',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return dupes;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Unprefixed command basenames registered in BOTH project and global scope.
|
|
93
|
+
* Project always wins — the returned entries point at the GLOBAL copy,
|
|
94
|
+
* which is the one safe to remove (the project copy stays canonical).
|
|
95
|
+
*/
|
|
96
|
+
function findCrossScopeDupes(projectClaudeDir, globalClaudeDir) {
|
|
97
|
+
const dupes = [];
|
|
98
|
+
if (path.resolve(projectClaudeDir) === path.resolve(globalClaudeDir)) return dupes;
|
|
99
|
+
const projectCommandsDir = path.join(projectClaudeDir, 'commands');
|
|
100
|
+
const globalCommandsDir = path.join(globalClaudeDir, 'commands');
|
|
101
|
+
const projectNames = new Set(
|
|
102
|
+
listDirSafe(projectCommandsDir)
|
|
103
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
104
|
+
.map((e) => e.name),
|
|
105
|
+
);
|
|
106
|
+
for (const entry of listDirSafe(globalCommandsDir)) {
|
|
107
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
108
|
+
if (projectNames.has(entry.name)) {
|
|
109
|
+
dupes.push({
|
|
110
|
+
name: entry.name,
|
|
111
|
+
srcPath: path.join(globalCommandsDir, entry.name),
|
|
112
|
+
keptPath: path.join(projectCommandsDir, entry.name),
|
|
113
|
+
kind: 'command',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return dupes;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Full duplication report across project + global scopes. Pure read — safe
|
|
122
|
+
* to call from `rcode doctor` on every run.
|
|
123
|
+
*/
|
|
124
|
+
function scanNamespaceDuplication(projectDir, homeDir) {
|
|
125
|
+
const projectClaudeDir = path.join(projectDir, '.claude');
|
|
126
|
+
const globalClaudeDir = path.join(homeDir, '.claude');
|
|
127
|
+
|
|
128
|
+
const projectLegacy = findLegacyRihalArtifacts(projectClaudeDir);
|
|
129
|
+
const globalLegacy = findLegacyRihalArtifacts(globalClaudeDir);
|
|
130
|
+
const projectUnprefixed = findUnprefixedTwinDupes(projectClaudeDir);
|
|
131
|
+
const globalUnprefixed = findUnprefixedTwinDupes(globalClaudeDir);
|
|
132
|
+
const crossScope = findCrossScopeDupes(projectClaudeDir, globalClaudeDir);
|
|
133
|
+
|
|
134
|
+
const legacySkillCount = projectLegacy.skills.length + globalLegacy.skills.length;
|
|
135
|
+
const legacyCommandCount = projectLegacy.commands.length + globalLegacy.commands.length;
|
|
136
|
+
const unprefixedCount = projectUnprefixed.length + globalUnprefixed.length;
|
|
137
|
+
const crossScopeCount = crossScope.length;
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
legacySkillCount,
|
|
141
|
+
legacyCommandCount,
|
|
142
|
+
unprefixedCount,
|
|
143
|
+
crossScopeCount,
|
|
144
|
+
totalCount: legacySkillCount + legacyCommandCount + unprefixedCount + crossScopeCount,
|
|
145
|
+
detail: { projectLegacy, globalLegacy, projectUnprefixed, globalUnprefixed, crossScope },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Move `srcPath` under `backupRoot/<scope>/<skills|commands>/`, then delete
|
|
151
|
+
* the original. Idempotent: if srcPath no longer exists (already migrated
|
|
152
|
+
* in an earlier pass, or removed by an earlier step in this same pass),
|
|
153
|
+
* this is a no-op that returns false.
|
|
154
|
+
*/
|
|
155
|
+
function backupAndRemove(srcPath, backupRoot, scope, kind) {
|
|
156
|
+
if (!fs.existsSync(srcPath)) return false;
|
|
157
|
+
const destDir = path.join(backupRoot, scope, kind === 'skill' ? 'skills' : 'commands');
|
|
158
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
159
|
+
const dest = path.join(destDir, path.basename(srcPath));
|
|
160
|
+
fs.cpSync(srcPath, dest, { recursive: true });
|
|
161
|
+
fs.rmSync(srcPath, { recursive: true, force: true });
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Execute the cleanup: back up + remove every legacy rihal-* artifact and
|
|
167
|
+
* unprefixed/cross-scope duplicate found by scanNamespaceDuplication(), for
|
|
168
|
+
* real. Idempotent — running twice in a row removes 0 the second time
|
|
169
|
+
* because the scan re-runs fresh each call and finds nothing left to flag.
|
|
170
|
+
*
|
|
171
|
+
* Backups always land under ~/.claude/.rcode-backup/<timestamp>/ regardless
|
|
172
|
+
* of whether the artifact came from the project or global scope, so a
|
|
173
|
+
* single rollback location covers everything from one migrate run.
|
|
174
|
+
*/
|
|
175
|
+
function migrateNamespace(projectDir, homeDir) {
|
|
176
|
+
const scan = scanNamespaceDuplication(projectDir, homeDir);
|
|
177
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
178
|
+
const backupRoot = path.join(homeDir, '.claude', '.rcode-backup', ts);
|
|
179
|
+
|
|
180
|
+
const summary = {
|
|
181
|
+
backupDir: null,
|
|
182
|
+
removed: { legacySkills: 0, legacyCommands: 0, unprefixedDupes: 0, crossScopeDupes: 0 },
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const removeAll = (items, scope, kind) => {
|
|
186
|
+
let count = 0;
|
|
187
|
+
for (const item of items) {
|
|
188
|
+
if (backupAndRemove(item.srcPath, backupRoot, scope, kind)) count++;
|
|
189
|
+
}
|
|
190
|
+
return count;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
summary.removed.legacySkills =
|
|
194
|
+
removeAll(scan.detail.projectLegacy.skills, 'project', 'skill') +
|
|
195
|
+
removeAll(scan.detail.globalLegacy.skills, 'global', 'skill');
|
|
196
|
+
summary.removed.legacyCommands =
|
|
197
|
+
removeAll(scan.detail.projectLegacy.commands, 'project', 'command') +
|
|
198
|
+
removeAll(scan.detail.globalLegacy.commands, 'global', 'command');
|
|
199
|
+
summary.removed.unprefixedDupes =
|
|
200
|
+
removeAll(scan.detail.projectUnprefixed, 'project', 'command') +
|
|
201
|
+
removeAll(scan.detail.globalUnprefixed, 'global', 'command');
|
|
202
|
+
summary.removed.crossScopeDupes = removeAll(scan.detail.crossScope, 'global', 'command');
|
|
203
|
+
|
|
204
|
+
const totalRemoved = Object.values(summary.removed).reduce((a, b) => a + b, 0);
|
|
205
|
+
if (totalRemoved > 0) summary.backupDir = backupRoot;
|
|
206
|
+
|
|
207
|
+
return summary;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = {
|
|
211
|
+
findLegacyRihalArtifacts,
|
|
212
|
+
findUnprefixedTwinDupes,
|
|
213
|
+
findCrossScopeDupes,
|
|
214
|
+
scanNamespaceDuplication,
|
|
215
|
+
migrateNamespace,
|
|
216
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rcode migrate-namespace — clean up legacy rihal-* skills/commands whose
|
|
3
|
+
* rcode-* twin already exists (#954), plus unprefixed/cross-scope command
|
|
4
|
+
* duplicates. Backs everything up under ~/.claude/.rcode-backup/<ts>/
|
|
5
|
+
* before removing. Idempotent — running twice removes nothing the second
|
|
6
|
+
* time.
|
|
7
|
+
*
|
|
8
|
+
* Default is dry-run (prints what WOULD be removed). Pass --yes to execute.
|
|
9
|
+
* `rcode update` also runs this automatically after refreshing files.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { homedir } = require('./lib/homedir.cjs');
|
|
14
|
+
const { scanNamespaceDuplication, migrateNamespace } = require('./lib/namespace-migrate.cjs');
|
|
15
|
+
|
|
16
|
+
function parseArgs(args) {
|
|
17
|
+
return { yes: args.includes('--yes') || args.includes('-y') };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function printScanSummary(scan) {
|
|
21
|
+
console.log(`\n🕌 rcode — Namespace Migration\n`);
|
|
22
|
+
if (scan.totalCount === 0) {
|
|
23
|
+
console.log(` ✓ No legacy rihal-* or duplicate command registrations found.\n`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
console.log(` Found ${scan.totalCount} artifact(s) to migrate:`);
|
|
27
|
+
console.log(` legacy rihal-* skills: ${scan.legacySkillCount}`);
|
|
28
|
+
console.log(` legacy rihal-* commands: ${scan.legacyCommandCount}`);
|
|
29
|
+
console.log(` unprefixed dupes: ${scan.unprefixedCount}`);
|
|
30
|
+
console.log(` cross-scope dupes: ${scan.crossScopeCount} (global copy shadowed by project)`);
|
|
31
|
+
console.log();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = function migrateNamespaceCommand(args = []) {
|
|
35
|
+
const opts = parseArgs(args);
|
|
36
|
+
const cwd = process.cwd();
|
|
37
|
+
const home = homedir();
|
|
38
|
+
|
|
39
|
+
const scan = scanNamespaceDuplication(cwd, home);
|
|
40
|
+
printScanSummary(scan);
|
|
41
|
+
if (scan.totalCount === 0) return;
|
|
42
|
+
|
|
43
|
+
if (!opts.yes) {
|
|
44
|
+
console.log(` [DRY RUN] Nothing removed. Pass --yes to back up and remove these files.\n`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const summary = migrateNamespace(cwd, home);
|
|
49
|
+
const total = Object.values(summary.removed).reduce((a, b) => a + b, 0);
|
|
50
|
+
console.log(` ✓ Removed ${total} artifact(s):`);
|
|
51
|
+
console.log(` legacy skills: ${summary.removed.legacySkills}`);
|
|
52
|
+
console.log(` legacy commands: ${summary.removed.legacyCommands}`);
|
|
53
|
+
console.log(` unprefixed: ${summary.removed.unprefixedDupes}`);
|
|
54
|
+
console.log(` cross-scope: ${summary.removed.crossScopeDupes}`);
|
|
55
|
+
if (summary.backupDir) {
|
|
56
|
+
console.log(` 💾 backup: ${path.relative(home, summary.backupDir)} (under ~/.claude/.rcode-backup/)`);
|
|
57
|
+
}
|
|
58
|
+
console.log();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
module.exports.parseArgs = parseArgs;
|
package/cli/update.js
CHANGED
|
@@ -446,6 +446,25 @@ async function runUpdate(args, { packageRoot, packageJson }) {
|
|
|
446
446
|
}
|
|
447
447
|
console.log(` ✓ ${path.relative(cwd, configPath)} → installed_version: ${packageVersion}`);
|
|
448
448
|
|
|
449
|
+
// ------ Legacy namespace cleanup (#954) ------
|
|
450
|
+
// Backs up + removes legacy rihal-* skills/commands whose rcode-* twin
|
|
451
|
+
// already exists, plus unprefixed/cross-scope command dupes. Idempotent —
|
|
452
|
+
// safe to run on every update even when there's nothing to migrate.
|
|
453
|
+
try {
|
|
454
|
+
const { migrateNamespace } = require('./lib/namespace-migrate.cjs');
|
|
455
|
+
const { homedir: getHomedir } = require('./lib/homedir.cjs');
|
|
456
|
+
const nsResult = migrateNamespace(cwd, getHomedir());
|
|
457
|
+
const nsTotal = Object.values(nsResult.removed).reduce((a, b) => a + b, 0);
|
|
458
|
+
if (nsTotal > 0) {
|
|
459
|
+
console.log(` 🧹 migrated ${nsTotal} legacy rihal-*/duplicate command registration(s)`);
|
|
460
|
+
if (nsResult.backupDir) {
|
|
461
|
+
console.log(` backup: ${nsResult.backupDir}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
} catch (err) {
|
|
465
|
+
console.log(` ⚠ namespace migration skipped: ${err.message}`);
|
|
466
|
+
}
|
|
467
|
+
|
|
449
468
|
// ------ Verify manifest ------
|
|
450
469
|
console.log();
|
|
451
470
|
const { reports, hasDrift } = verifyInstall(cwd, packageRoot, editors);
|