@hanzlaa/rcode 3.4.16 → 3.4.18
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/doctor.js +76 -1
- package/cli/index.js +52 -0
- package/cli/install.js +7 -0
- package/cli/nuke.js +404 -0
- package/dist/rcode.js +445 -1
- package/package.json +1 -1
- package/rihal/bin/rihal-tools.cjs +5 -2
- package/rihal/workflows/execute.md +2 -1
- package/rihal/workflows/plan.md +3 -2
package/cli/doctor.js
CHANGED
|
@@ -259,6 +259,77 @@ function runCompliance(packageRoot) {
|
|
|
259
259
|
return failing;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
// ---------- Duplicate-installation check ----------
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Detect rcode/rihal-code installations across all known package managers.
|
|
266
|
+
* Multiple installs cause duplicate slash commands in Claude Code (#637-#644).
|
|
267
|
+
* Returns { count, installs[], warnings[] }.
|
|
268
|
+
*/
|
|
269
|
+
function checkDuplicateInstalls() {
|
|
270
|
+
const os = require('os');
|
|
271
|
+
const home = os.homedir();
|
|
272
|
+
const installs = [];
|
|
273
|
+
|
|
274
|
+
// Resolve every global node_modules dir we know about.
|
|
275
|
+
const dirs = [];
|
|
276
|
+
for (const cmd of [['npm', ['root', '-g']], ['pnpm', ['root', '-g']]]) {
|
|
277
|
+
try {
|
|
278
|
+
const r = spawnSync(cmd[0], cmd[1], { encoding: 'utf8' });
|
|
279
|
+
if (r.status === 0 && r.stdout.trim()) dirs.push({ manager: cmd[0], dir: r.stdout.trim() });
|
|
280
|
+
} catch { /* not installed */ }
|
|
281
|
+
}
|
|
282
|
+
// Also scan all nvm node versions — npm root -g only covers the active one.
|
|
283
|
+
const nvmRoot = path.join(home, '.nvm', 'versions', 'node');
|
|
284
|
+
if (fs.existsSync(nvmRoot)) {
|
|
285
|
+
for (const v of fs.readdirSync(nvmRoot)) {
|
|
286
|
+
const dir = path.join(nvmRoot, v, 'lib', 'node_modules');
|
|
287
|
+
if (fs.existsSync(dir) && !dirs.some(d => d.dir === dir)) {
|
|
288
|
+
dirs.push({ manager: `nvm/${v}`, dir });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
for (const { manager, dir } of dirs) {
|
|
294
|
+
for (const scope of ['@hanzlaa', '@hanzlahabib']) {
|
|
295
|
+
const scopeDir = path.join(dir, scope);
|
|
296
|
+
if (!fs.existsSync(scopeDir)) continue;
|
|
297
|
+
for (const pkg of fs.readdirSync(scopeDir)) {
|
|
298
|
+
if (pkg === 'rcode' || pkg === 'rihal-code' || pkg.startsWith('rihal')) {
|
|
299
|
+
let version = 'unknown';
|
|
300
|
+
try {
|
|
301
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(scopeDir, pkg, 'package.json'), 'utf8'));
|
|
302
|
+
version = pkgJson.version;
|
|
303
|
+
} catch {}
|
|
304
|
+
installs.push({ manager, scope, pkg, version, dir: path.join(scopeDir, pkg) });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return { count: installs.length, installs };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function printDuplicateChecks(result) {
|
|
314
|
+
if (result.count === 0) {
|
|
315
|
+
console.log(` ✓ No global rcode installs found`);
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
if (result.count === 1) {
|
|
319
|
+
const i = result.installs[0];
|
|
320
|
+
console.log(` ✓ Single global install: [${i.manager}] ${i.scope}/${i.pkg}@${i.version}`);
|
|
321
|
+
return 0;
|
|
322
|
+
}
|
|
323
|
+
// Multiple installs — DUPLICATE — this causes the duplicate-commands bug.
|
|
324
|
+
console.log(` ✗ ${result.count} global installs detected — this causes duplicate slash commands!`);
|
|
325
|
+
for (const i of result.installs) {
|
|
326
|
+
console.log(` [${i.manager}] ${i.scope}/${i.pkg}@${i.version}`);
|
|
327
|
+
console.log(` ${i.dir}`);
|
|
328
|
+
}
|
|
329
|
+
console.log(`\n Fix: rcode nuke --yes && npm install -g @hanzlaa/rcode`);
|
|
330
|
+
return 1;
|
|
331
|
+
}
|
|
332
|
+
|
|
262
333
|
// ---------- Entrypoint ----------
|
|
263
334
|
|
|
264
335
|
module.exports = function doctor(args, { packageRoot }) {
|
|
@@ -270,10 +341,14 @@ module.exports = function doctor(args, { packageRoot }) {
|
|
|
270
341
|
const checks = runPreflight(cwd, packageRoot);
|
|
271
342
|
const preflightFailures = printChecks(checks);
|
|
272
343
|
|
|
344
|
+
console.log(`\nDuplicate installations:`);
|
|
345
|
+
const dupResult = checkDuplicateInstalls();
|
|
346
|
+
const duplicateFailures = printDuplicateChecks(dupResult);
|
|
347
|
+
|
|
273
348
|
console.log(`\nPackage compliance:`);
|
|
274
349
|
const complianceFailures = runCompliance(packageRoot);
|
|
275
350
|
|
|
276
|
-
const totalFailures = preflightFailures + complianceFailures;
|
|
351
|
+
const totalFailures = preflightFailures + complianceFailures + duplicateFailures;
|
|
277
352
|
console.log();
|
|
278
353
|
if (totalFailures === 0) {
|
|
279
354
|
console.log(`✅ All checks passed.`);
|
package/cli/index.js
CHANGED
|
@@ -25,6 +25,7 @@ const COMMANDS = {
|
|
|
25
25
|
update: require('./update'),
|
|
26
26
|
uninstall: require('./uninstall'),
|
|
27
27
|
remove: require('./uninstall'), // alias
|
|
28
|
+
nuke: require('./nuke'), // full cleanup across all package managers + global state
|
|
28
29
|
dashboard: require('./dashboard'),
|
|
29
30
|
serve: require('./dashboard'),
|
|
30
31
|
digest: require('./digest'),
|
|
@@ -57,6 +58,10 @@ Usage:
|
|
|
57
58
|
update Refresh skill files (backs up .rihal/ state first)
|
|
58
59
|
uninstall Remove Rihal Code from the current project
|
|
59
60
|
remove Alias for uninstall
|
|
61
|
+
nuke Wipe ALL rihal/rcode installs everywhere (global packages,
|
|
62
|
+
binaries, ~/.claude/* rihal artifacts, ~/.rihal/, project artifacts)
|
|
63
|
+
Default = dry-run. Pass --yes to remove. Pass --include-planning
|
|
64
|
+
to also remove .planning/ in CWD.
|
|
60
65
|
config Get/set project configuration (project_name, user_name, etc.)
|
|
61
66
|
context Memory bank freshness (--check | --refresh | --install-hook)
|
|
62
67
|
github-sync Sync .rihal/ phases/epics/stories to GitHub (dry-run default)
|
|
@@ -86,9 +91,56 @@ Documentation: https://github.com/hanzlahabib/rihal-code
|
|
|
86
91
|
`.trim());
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
/**
|
|
95
|
+
* npm 10+ suppresses postinstall script output during global installs, so users
|
|
96
|
+
* who run `npm install -g @hanzlaa/rcode` see only "added 1 package" with no
|
|
97
|
+
* confirmation that 100+ commands and skills were installed. We detect a fresh
|
|
98
|
+
* install on the first `rcode <anything>` invocation by checking for a marker
|
|
99
|
+
* file under ~/.rihal/, print a one-time welcome banner, then drop the marker.
|
|
100
|
+
*/
|
|
101
|
+
function maybeShowFirstRunBanner() {
|
|
102
|
+
const os = require('os');
|
|
103
|
+
const home = os.homedir();
|
|
104
|
+
const markerDir = path.join(home, '.rihal');
|
|
105
|
+
const marker = path.join(markerDir, '.welcome-shown');
|
|
106
|
+
if (fs.existsSync(marker)) return;
|
|
107
|
+
|
|
108
|
+
// Only show banner if global install actually ran — i.e. ~/.claude/commands/
|
|
109
|
+
// has rihal-*.md files. Otherwise this is a developer running from source.
|
|
110
|
+
const globalCommands = path.join(home, '.claude', 'commands');
|
|
111
|
+
let hasGlobalRihal = false;
|
|
112
|
+
try {
|
|
113
|
+
hasGlobalRihal = fs.existsSync(globalCommands) &&
|
|
114
|
+
fs.readdirSync(globalCommands).some(f => f.startsWith('rihal-') && f.endsWith('.md'));
|
|
115
|
+
} catch { /* unreadable */ }
|
|
116
|
+
if (!hasGlobalRihal) return;
|
|
117
|
+
|
|
118
|
+
console.log(`\n🕌 Rihal Code v${PACKAGE_JSON.version} — first run detected.\n`);
|
|
119
|
+
console.log(` ✓ ${countGlobalRihal(globalCommands)} slash commands installed → ~/.claude/commands/`);
|
|
120
|
+
console.log(` ✓ All /rihal-* commands available in every Claude Code project.`);
|
|
121
|
+
console.log(`\n To set up a project: cd my-project && rcode install`);
|
|
122
|
+
console.log(` Show all commands: rcode help`);
|
|
123
|
+
console.log(` Diagnose issues: rcode doctor\n`);
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
fs.mkdirSync(markerDir, { recursive: true });
|
|
127
|
+
fs.writeFileSync(marker, `installed ${PACKAGE_JSON.version} at ${new Date().toISOString()}\n`);
|
|
128
|
+
} catch { /* if we can't write the marker, banner shows again next time — annoying but not broken */ }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function countGlobalRihal(dir) {
|
|
132
|
+
try {
|
|
133
|
+
return fs.readdirSync(dir).filter(f => f.startsWith('rihal-') && f.endsWith('.md')).length;
|
|
134
|
+
} catch { return 0; }
|
|
135
|
+
}
|
|
136
|
+
|
|
89
137
|
async function main() {
|
|
90
138
|
const [, , command = 'help', ...args] = process.argv;
|
|
91
139
|
|
|
140
|
+
// Show first-run banner before dispatching — npm hides postinstall output,
|
|
141
|
+
// so this is the user's first visible confirmation that the install worked.
|
|
142
|
+
maybeShowFirstRunBanner();
|
|
143
|
+
|
|
92
144
|
const handler = COMMANDS[command];
|
|
93
145
|
if (!handler) {
|
|
94
146
|
console.error(`Unknown command: ${command}`);
|
package/cli/install.js
CHANGED
|
@@ -177,6 +177,13 @@ function parseArgs(argv) {
|
|
|
177
177
|
opts.target = path.resolve(positional[0]);
|
|
178
178
|
opts.targetProvided = true;
|
|
179
179
|
}
|
|
180
|
+
// --global without an explicit target means "install to ~/.claude/" — i.e.
|
|
181
|
+
// home dir. Without this, running `rcode install --global` from inside a
|
|
182
|
+
// project directory wrote rihal artifacts to that project, not to the user's
|
|
183
|
+
// home where Claude Code reads global commands from.
|
|
184
|
+
if (opts.global && !opts.targetProvided) {
|
|
185
|
+
opts.target = os.homedir();
|
|
186
|
+
}
|
|
180
187
|
if (!opts.projectName) opts.projectName = path.basename(opts.target);
|
|
181
188
|
return opts;
|
|
182
189
|
}
|
package/cli/nuke.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rcode nuke — one-shot full cleanup
|
|
3
|
+
*
|
|
4
|
+
* Removes EVERY trace of rihal/rcode from the system:
|
|
5
|
+
* - Global npm/pnpm/yarn/bun installs (both @hanzlaa/rcode and legacy @hanzlahabib/rihal-code)
|
|
6
|
+
* - Global binaries (rcode, rihal, rihal-code) in all known PATH dirs
|
|
7
|
+
* - Global Claude Code artifacts (~/.claude/commands/rihal*, ~/.claude/agents/rihal-*, ~/.claude/skills/rihal-*)
|
|
8
|
+
* - Global state (~/.rihal/)
|
|
9
|
+
* - Project-level artifacts in CWD (.claude/commands/rihal*, .claude/agents/rihal-*, .rihal/, .planning/ optional)
|
|
10
|
+
*
|
|
11
|
+
* Default mode: dry-run — prints what *would* be removed.
|
|
12
|
+
* Pass --yes to actually remove. Pass --include-planning to also remove .planning/ in CWD.
|
|
13
|
+
*
|
|
14
|
+
* Why this exists: gaps #1-#4 in the duplicate-commands incident (May 2026).
|
|
15
|
+
* - Multiple package managers can hold separate copies invisibly to each other.
|
|
16
|
+
* - rcode uninstall only cleans the project, not global package installs.
|
|
17
|
+
* - Users had no single command to fully reset.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const { spawnSync } = require('child_process');
|
|
26
|
+
|
|
27
|
+
function exists(p) {
|
|
28
|
+
try { fs.accessSync(p); return true; } catch { return false; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readDirSafe(p) {
|
|
32
|
+
try { return fs.readdirSync(p); } catch { return []; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve where each package manager keeps its global node_modules.
|
|
37
|
+
* Returns a list of { manager, dir } — dir may not exist.
|
|
38
|
+
*/
|
|
39
|
+
function getGlobalNodeModulesDirs() {
|
|
40
|
+
const home = os.homedir();
|
|
41
|
+
const candidates = [];
|
|
42
|
+
|
|
43
|
+
// npm — npm root -g resolves to the active node version's lib/node_modules.
|
|
44
|
+
try {
|
|
45
|
+
const r = spawnSync('npm', ['root', '-g'], { encoding: 'utf8' });
|
|
46
|
+
if (r.status === 0 && r.stdout.trim()) candidates.push({ manager: 'npm', dir: r.stdout.trim() });
|
|
47
|
+
} catch { /* npm not installed */ }
|
|
48
|
+
|
|
49
|
+
// pnpm — `pnpm root -g`
|
|
50
|
+
try {
|
|
51
|
+
const r = spawnSync('pnpm', ['root', '-g'], { encoding: 'utf8' });
|
|
52
|
+
if (r.status === 0 && r.stdout.trim()) candidates.push({ manager: 'pnpm', dir: r.stdout.trim() });
|
|
53
|
+
} catch { /* pnpm not installed */ }
|
|
54
|
+
|
|
55
|
+
// yarn classic — `yarn global dir` returns the parent; node_modules is inside.
|
|
56
|
+
try {
|
|
57
|
+
const r = spawnSync('yarn', ['global', 'dir'], { encoding: 'utf8' });
|
|
58
|
+
if (r.status === 0 && r.stdout.trim()) {
|
|
59
|
+
candidates.push({ manager: 'yarn', dir: path.join(r.stdout.trim(), 'node_modules') });
|
|
60
|
+
}
|
|
61
|
+
} catch { /* yarn not installed */ }
|
|
62
|
+
|
|
63
|
+
// bun — `bun pm bin -g` returns the bin dir; sibling install/global has node_modules.
|
|
64
|
+
try {
|
|
65
|
+
const r = spawnSync('bun', ['pm', 'bin', '-g'], { encoding: 'utf8' });
|
|
66
|
+
if (r.status === 0 && r.stdout.trim()) {
|
|
67
|
+
// bun layout: ~/.bun/install/global/node_modules + ~/.bun/bin
|
|
68
|
+
candidates.push({ manager: 'bun', dir: path.join(home, '.bun', 'install', 'global', 'node_modules') });
|
|
69
|
+
}
|
|
70
|
+
} catch { /* bun not installed */ }
|
|
71
|
+
|
|
72
|
+
// Hardcoded fallbacks for stale/dead version managers (nvm versions that npm root doesn't know about).
|
|
73
|
+
const nvmRoot = path.join(home, '.nvm', 'versions', 'node');
|
|
74
|
+
if (exists(nvmRoot)) {
|
|
75
|
+
for (const v of readDirSafe(nvmRoot)) {
|
|
76
|
+
const dir = path.join(nvmRoot, v, 'lib', 'node_modules');
|
|
77
|
+
if (exists(dir) && !candidates.some(c => c.dir === dir)) {
|
|
78
|
+
candidates.push({ manager: `nvm/${v}`, dir });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return candidates;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* For a given global node_modules dir, return a list of rihal/rcode package paths.
|
|
88
|
+
* Looks for both @hanzlaa/rcode (current) and @hanzlahabib/rihal-code (legacy).
|
|
89
|
+
*/
|
|
90
|
+
function findRihalPackages(globalNodeModules) {
|
|
91
|
+
const found = [];
|
|
92
|
+
for (const scope of ['@hanzlaa', '@hanzlahabib']) {
|
|
93
|
+
const scopeDir = path.join(globalNodeModules, scope);
|
|
94
|
+
if (!exists(scopeDir)) continue;
|
|
95
|
+
for (const pkg of readDirSafe(scopeDir)) {
|
|
96
|
+
// Match rcode, rihal-code, or anything starting with rihal
|
|
97
|
+
if (pkg === 'rcode' || pkg === 'rihal-code' || pkg.startsWith('rihal')) {
|
|
98
|
+
found.push({ scope, pkg, dir: path.join(scopeDir, pkg) });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return found;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resolve global bin directories where rcode/rihal/rihal-code may live.
|
|
107
|
+
*/
|
|
108
|
+
function getGlobalBinDirs() {
|
|
109
|
+
const home = os.homedir();
|
|
110
|
+
const dirs = new Set();
|
|
111
|
+
|
|
112
|
+
// npm prefix bin
|
|
113
|
+
try {
|
|
114
|
+
const r = spawnSync('npm', ['prefix', '-g'], { encoding: 'utf8' });
|
|
115
|
+
if (r.status === 0 && r.stdout.trim()) dirs.add(path.join(r.stdout.trim(), 'bin'));
|
|
116
|
+
} catch {}
|
|
117
|
+
|
|
118
|
+
// pnpm bin
|
|
119
|
+
try {
|
|
120
|
+
const r = spawnSync('pnpm', ['bin', '-g'], { encoding: 'utf8' });
|
|
121
|
+
if (r.status === 0 && r.stdout.trim()) dirs.add(r.stdout.trim());
|
|
122
|
+
} catch {}
|
|
123
|
+
|
|
124
|
+
// yarn bin
|
|
125
|
+
try {
|
|
126
|
+
const r = spawnSync('yarn', ['global', 'bin'], { encoding: 'utf8' });
|
|
127
|
+
if (r.status === 0 && r.stdout.trim()) dirs.add(r.stdout.trim());
|
|
128
|
+
} catch {}
|
|
129
|
+
|
|
130
|
+
// bun
|
|
131
|
+
try {
|
|
132
|
+
const r = spawnSync('bun', ['pm', 'bin', '-g'], { encoding: 'utf8' });
|
|
133
|
+
if (r.status === 0 && r.stdout.trim()) dirs.add(r.stdout.trim());
|
|
134
|
+
} catch {}
|
|
135
|
+
|
|
136
|
+
// Hardcoded fallbacks
|
|
137
|
+
dirs.add(path.join(home, '.local', 'share', 'pnpm'));
|
|
138
|
+
dirs.add(path.join(home, '.local', 'bin'));
|
|
139
|
+
dirs.add(path.join(home, '.bun', 'bin'));
|
|
140
|
+
|
|
141
|
+
// nvm bins
|
|
142
|
+
const nvmRoot = path.join(home, '.nvm', 'versions', 'node');
|
|
143
|
+
if (exists(nvmRoot)) {
|
|
144
|
+
for (const v of readDirSafe(nvmRoot)) {
|
|
145
|
+
dirs.add(path.join(nvmRoot, v, 'bin'));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return [...dirs].filter(exists);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const RCODE_BINS = ['rcode', 'rihal', 'rihal-code'];
|
|
153
|
+
|
|
154
|
+
function findRihalBins(binDir) {
|
|
155
|
+
const found = [];
|
|
156
|
+
for (const name of RCODE_BINS) {
|
|
157
|
+
const p = path.join(binDir, name);
|
|
158
|
+
if (exists(p)) {
|
|
159
|
+
let target = null;
|
|
160
|
+
try { target = fs.readlinkSync(p); } catch {}
|
|
161
|
+
// Only flag if it's clearly ours (target points at rcode/rihal-code package).
|
|
162
|
+
// If it's not a symlink, include it anyway — bare scripts in pnpm bin etc.
|
|
163
|
+
if (!target || /rcode|rihal-code|@hanzlaa|@hanzlahabib/.test(target)) {
|
|
164
|
+
found.push({ name, path: p, target });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return found;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Find rihal-related artifacts in a Claude Code config dir (~/.claude or .claude in CWD).
|
|
173
|
+
*/
|
|
174
|
+
function findClaudeArtifacts(claudeDir) {
|
|
175
|
+
const found = [];
|
|
176
|
+
if (!exists(claudeDir)) return found;
|
|
177
|
+
|
|
178
|
+
// .claude/commands/rihal-*.md (claude-style root)
|
|
179
|
+
const cmdRoot = path.join(claudeDir, 'commands');
|
|
180
|
+
if (exists(cmdRoot)) {
|
|
181
|
+
for (const f of readDirSafe(cmdRoot)) {
|
|
182
|
+
if (f.startsWith('rihal-') && (f.endsWith('.md') || f.endsWith('.mdc'))) {
|
|
183
|
+
found.push({ kind: 'command', path: path.join(cmdRoot, f) });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// .claude/commands/rihal/ (vscode-style subdir)
|
|
187
|
+
const rihalSubdir = path.join(cmdRoot, 'rihal');
|
|
188
|
+
if (exists(rihalSubdir)) {
|
|
189
|
+
found.push({ kind: 'commands-dir', path: rihalSubdir });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// .claude/agents/rihal-*.md
|
|
194
|
+
const agentsDir = path.join(claudeDir, 'agents');
|
|
195
|
+
if (exists(agentsDir)) {
|
|
196
|
+
for (const f of readDirSafe(agentsDir)) {
|
|
197
|
+
if (f.startsWith('rihal-') && (f.endsWith('.md') || f.endsWith('.mdc'))) {
|
|
198
|
+
found.push({ kind: 'agent', path: path.join(agentsDir, f) });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// .claude/skills/rihal-*
|
|
204
|
+
const skillsDir = path.join(claudeDir, 'skills');
|
|
205
|
+
if (exists(skillsDir)) {
|
|
206
|
+
for (const d of readDirSafe(skillsDir)) {
|
|
207
|
+
if (d.startsWith('rihal-')) {
|
|
208
|
+
found.push({ kind: 'skill-dir', path: path.join(skillsDir, d) });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return found;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function buildPlan({ includePlanning }) {
|
|
217
|
+
const home = os.homedir();
|
|
218
|
+
const cwd = process.cwd();
|
|
219
|
+
const plan = {
|
|
220
|
+
packages: [],
|
|
221
|
+
bins: [],
|
|
222
|
+
globalClaude: [],
|
|
223
|
+
globalRihal: null,
|
|
224
|
+
projectClaude: [],
|
|
225
|
+
projectRihal: null,
|
|
226
|
+
projectPlanning: null,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Global node_modules packages
|
|
230
|
+
for (const { manager, dir } of getGlobalNodeModulesDirs()) {
|
|
231
|
+
for (const pkg of findRihalPackages(dir)) {
|
|
232
|
+
plan.packages.push({ manager, ...pkg });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Global binaries
|
|
237
|
+
for (const binDir of getGlobalBinDirs()) {
|
|
238
|
+
for (const bin of findRihalBins(binDir)) {
|
|
239
|
+
plan.bins.push({ binDir, ...bin });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Global Claude artifacts (~/.claude/)
|
|
244
|
+
plan.globalClaude = findClaudeArtifacts(path.join(home, '.claude'));
|
|
245
|
+
|
|
246
|
+
// Global state (~/.rihal/)
|
|
247
|
+
const globalRihal = path.join(home, '.rihal');
|
|
248
|
+
if (exists(globalRihal)) plan.globalRihal = globalRihal;
|
|
249
|
+
|
|
250
|
+
// Project-level (CWD only — never recurse, user may have many projects)
|
|
251
|
+
plan.projectClaude = findClaudeArtifacts(path.join(cwd, '.claude'));
|
|
252
|
+
const projectRihal = path.join(cwd, '.rihal');
|
|
253
|
+
if (exists(projectRihal) && cwd !== home) plan.projectRihal = projectRihal;
|
|
254
|
+
|
|
255
|
+
if (includePlanning) {
|
|
256
|
+
const projectPlanning = path.join(cwd, '.planning');
|
|
257
|
+
if (exists(projectPlanning) && cwd !== home) plan.projectPlanning = projectPlanning;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return plan;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function printPlan(plan, { dryRun }) {
|
|
264
|
+
const banner = dryRun ? '[DRY RUN — pass --yes to remove]' : '[REMOVING]';
|
|
265
|
+
console.log(`\n🔥 rcode nuke ${banner}\n`);
|
|
266
|
+
|
|
267
|
+
let total = 0;
|
|
268
|
+
const section = (title, items) => {
|
|
269
|
+
if (!items || (Array.isArray(items) && items.length === 0)) return;
|
|
270
|
+
console.log(`${title}`);
|
|
271
|
+
if (Array.isArray(items)) {
|
|
272
|
+
for (const item of items) {
|
|
273
|
+
console.log(` • ${item.description || item.path || item.dir || JSON.stringify(item)}`);
|
|
274
|
+
total++;
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
console.log(` • ${items}`);
|
|
278
|
+
total++;
|
|
279
|
+
}
|
|
280
|
+
console.log('');
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
section('📦 Global packages (node_modules):', plan.packages.map(p => ({
|
|
284
|
+
description: `[${p.manager}] ${p.scope}/${p.pkg} → ${p.dir}`,
|
|
285
|
+
})));
|
|
286
|
+
|
|
287
|
+
section('🔗 Global binaries:', plan.bins.map(b => ({
|
|
288
|
+
description: `${b.path}${b.target ? ` → ${b.target}` : ''}`,
|
|
289
|
+
})));
|
|
290
|
+
|
|
291
|
+
section('🤖 ~/.claude/ artifacts:', plan.globalClaude.map(a => ({
|
|
292
|
+
description: `[${a.kind}] ${a.path}`,
|
|
293
|
+
})));
|
|
294
|
+
|
|
295
|
+
if (plan.globalRihal) section('🗂️ ~/.rihal/ (global state):', plan.globalRihal);
|
|
296
|
+
|
|
297
|
+
section('🤖 ./.claude/ artifacts (current project):', plan.projectClaude.map(a => ({
|
|
298
|
+
description: `[${a.kind}] ${a.path}`,
|
|
299
|
+
})));
|
|
300
|
+
|
|
301
|
+
if (plan.projectRihal) section('🗂️ ./.rihal/ (project state):', plan.projectRihal);
|
|
302
|
+
if (plan.projectPlanning) section('📋 ./.planning/ (your work — only with --include-planning):', plan.projectPlanning);
|
|
303
|
+
|
|
304
|
+
if (total === 0) {
|
|
305
|
+
console.log(' ✓ nothing to remove — system is clean.\n');
|
|
306
|
+
} else {
|
|
307
|
+
console.log(` Total: ${total} item(s).\n`);
|
|
308
|
+
}
|
|
309
|
+
return total;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function rmrf(p) {
|
|
313
|
+
try { fs.rmSync(p, { recursive: true, force: true }); return true; }
|
|
314
|
+
catch (err) { console.warn(` ⚠ failed to remove ${p}: ${err.message}`); return false; }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function uninstallPackage(manager, scope, pkg) {
|
|
318
|
+
const fullName = `${scope}/${pkg}`;
|
|
319
|
+
const cmd = manager === 'pnpm' ? ['pnpm', ['remove', '-g', fullName]]
|
|
320
|
+
: manager === 'yarn' ? ['yarn', ['global', 'remove', fullName]]
|
|
321
|
+
: manager === 'bun' ? ['bun', ['remove', '-g', fullName]]
|
|
322
|
+
: ['npm', ['uninstall', '-g', fullName]];
|
|
323
|
+
try {
|
|
324
|
+
const r = spawnSync(cmd[0], cmd[1], { stdio: 'pipe', encoding: 'utf8' });
|
|
325
|
+
return r.status === 0;
|
|
326
|
+
} catch { return false; }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function executePlan(plan) {
|
|
330
|
+
let removed = 0;
|
|
331
|
+
|
|
332
|
+
// Try clean uninstall via package manager first (cleans bins automatically)
|
|
333
|
+
for (const p of plan.packages) {
|
|
334
|
+
const manager = p.manager.startsWith('nvm/') ? 'npm' : p.manager;
|
|
335
|
+
process.stdout.write(` ${manager} uninstall ${p.scope}/${p.pkg} ... `);
|
|
336
|
+
const ok = uninstallPackage(manager, p.scope, p.pkg);
|
|
337
|
+
console.log(ok ? '✓' : '⚠ failed via PM, falling back to rm');
|
|
338
|
+
if (!ok) rmrf(p.dir);
|
|
339
|
+
removed++;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Force-remove any leftover bins (broken symlinks etc.)
|
|
343
|
+
for (const b of plan.bins) {
|
|
344
|
+
if (exists(b.path)) {
|
|
345
|
+
if (rmrf(b.path)) { console.log(` ✓ removed bin ${b.path}`); removed++; }
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Claude artifacts (global)
|
|
350
|
+
for (const a of plan.globalClaude) {
|
|
351
|
+
if (rmrf(a.path)) { console.log(` ✓ removed ${a.path}`); removed++; }
|
|
352
|
+
}
|
|
353
|
+
if (plan.globalRihal && rmrf(plan.globalRihal)) {
|
|
354
|
+
console.log(` ✓ removed ${plan.globalRihal}`); removed++;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Claude artifacts (project)
|
|
358
|
+
for (const a of plan.projectClaude) {
|
|
359
|
+
if (rmrf(a.path)) { console.log(` ✓ removed ${a.path}`); removed++; }
|
|
360
|
+
}
|
|
361
|
+
if (plan.projectRihal && rmrf(plan.projectRihal)) {
|
|
362
|
+
console.log(` ✓ removed ${plan.projectRihal}`); removed++;
|
|
363
|
+
}
|
|
364
|
+
if (plan.projectPlanning && rmrf(plan.projectPlanning)) {
|
|
365
|
+
console.log(` ✓ removed ${plan.projectPlanning}`); removed++;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return removed;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
module.exports = function nuke(args = []) {
|
|
372
|
+
const dryRun = !args.includes('--yes') && !args.includes('-y');
|
|
373
|
+
const includePlanning = args.includes('--include-planning');
|
|
374
|
+
|
|
375
|
+
// Safety: detect if CWD is the rcode source repo itself.
|
|
376
|
+
const cwdPkgJson = path.join(process.cwd(), 'package.json');
|
|
377
|
+
if (exists(cwdPkgJson)) {
|
|
378
|
+
try {
|
|
379
|
+
const pkg = JSON.parse(fs.readFileSync(cwdPkgJson, 'utf8'));
|
|
380
|
+
if (pkg.name === '@hanzlaa/rcode' || pkg.name === '@hanzlahabib/rihal-code') {
|
|
381
|
+
console.log('\n⚠ You are inside the rcode source repo.');
|
|
382
|
+
console.log(' Nuke would remove this repo\'s .claude/, .rihal/, and possibly .planning/.');
|
|
383
|
+
console.log(' That is your source code — almost certainly not what you want.');
|
|
384
|
+
console.log(' Run nuke from a different directory, or pass --i-know-what-im-doing to override.\n');
|
|
385
|
+
if (!args.includes('--i-know-what-im-doing')) return;
|
|
386
|
+
}
|
|
387
|
+
} catch { /* package.json unreadable, ignore */ }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const plan = buildPlan({ includePlanning });
|
|
391
|
+
const total = printPlan(plan, { dryRun });
|
|
392
|
+
|
|
393
|
+
if (total === 0 || dryRun) {
|
|
394
|
+
if (dryRun && total > 0) {
|
|
395
|
+
console.log('To actually remove these, re-run with: rcode nuke --yes');
|
|
396
|
+
console.log('To also remove .planning/ (your work): rcode nuke --yes --include-planning\n');
|
|
397
|
+
}
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
console.log('Executing...\n');
|
|
402
|
+
const removed = executePlan(plan);
|
|
403
|
+
console.log(`\n✅ Done. Removed ${removed} item(s). Reinstall: npm install -g @hanzlaa/rcode\n`);
|
|
404
|
+
};
|
package/dist/rcode.js
CHANGED
|
@@ -15063,6 +15063,9 @@ var require_install = __commonJS({
|
|
|
15063
15063
|
opts.target = path2.resolve(positional[0]);
|
|
15064
15064
|
opts.targetProvided = true;
|
|
15065
15065
|
}
|
|
15066
|
+
if (opts.global && !opts.targetProvided) {
|
|
15067
|
+
opts.target = os.homedir();
|
|
15068
|
+
}
|
|
15066
15069
|
if (!opts.projectName) opts.projectName = path2.basename(opts.target);
|
|
15067
15070
|
return opts;
|
|
15068
15071
|
}
|
|
@@ -17976,6 +17979,340 @@ To reinstall later:`);
|
|
|
17976
17979
|
}
|
|
17977
17980
|
});
|
|
17978
17981
|
|
|
17982
|
+
// cli/nuke.js
|
|
17983
|
+
var require_nuke = __commonJS({
|
|
17984
|
+
"cli/nuke.js"(exports2, module2) {
|
|
17985
|
+
"use strict";
|
|
17986
|
+
var fs2 = require("fs");
|
|
17987
|
+
var os = require("os");
|
|
17988
|
+
var path2 = require("path");
|
|
17989
|
+
var { spawnSync } = require("child_process");
|
|
17990
|
+
function exists(p) {
|
|
17991
|
+
try {
|
|
17992
|
+
fs2.accessSync(p);
|
|
17993
|
+
return true;
|
|
17994
|
+
} catch {
|
|
17995
|
+
return false;
|
|
17996
|
+
}
|
|
17997
|
+
}
|
|
17998
|
+
function readDirSafe(p) {
|
|
17999
|
+
try {
|
|
18000
|
+
return fs2.readdirSync(p);
|
|
18001
|
+
} catch {
|
|
18002
|
+
return [];
|
|
18003
|
+
}
|
|
18004
|
+
}
|
|
18005
|
+
function getGlobalNodeModulesDirs() {
|
|
18006
|
+
const home = os.homedir();
|
|
18007
|
+
const candidates = [];
|
|
18008
|
+
try {
|
|
18009
|
+
const r = spawnSync("npm", ["root", "-g"], { encoding: "utf8" });
|
|
18010
|
+
if (r.status === 0 && r.stdout.trim()) candidates.push({ manager: "npm", dir: r.stdout.trim() });
|
|
18011
|
+
} catch {
|
|
18012
|
+
}
|
|
18013
|
+
try {
|
|
18014
|
+
const r = spawnSync("pnpm", ["root", "-g"], { encoding: "utf8" });
|
|
18015
|
+
if (r.status === 0 && r.stdout.trim()) candidates.push({ manager: "pnpm", dir: r.stdout.trim() });
|
|
18016
|
+
} catch {
|
|
18017
|
+
}
|
|
18018
|
+
try {
|
|
18019
|
+
const r = spawnSync("yarn", ["global", "dir"], { encoding: "utf8" });
|
|
18020
|
+
if (r.status === 0 && r.stdout.trim()) {
|
|
18021
|
+
candidates.push({ manager: "yarn", dir: path2.join(r.stdout.trim(), "node_modules") });
|
|
18022
|
+
}
|
|
18023
|
+
} catch {
|
|
18024
|
+
}
|
|
18025
|
+
try {
|
|
18026
|
+
const r = spawnSync("bun", ["pm", "bin", "-g"], { encoding: "utf8" });
|
|
18027
|
+
if (r.status === 0 && r.stdout.trim()) {
|
|
18028
|
+
candidates.push({ manager: "bun", dir: path2.join(home, ".bun", "install", "global", "node_modules") });
|
|
18029
|
+
}
|
|
18030
|
+
} catch {
|
|
18031
|
+
}
|
|
18032
|
+
const nvmRoot = path2.join(home, ".nvm", "versions", "node");
|
|
18033
|
+
if (exists(nvmRoot)) {
|
|
18034
|
+
for (const v of readDirSafe(nvmRoot)) {
|
|
18035
|
+
const dir = path2.join(nvmRoot, v, "lib", "node_modules");
|
|
18036
|
+
if (exists(dir) && !candidates.some((c) => c.dir === dir)) {
|
|
18037
|
+
candidates.push({ manager: `nvm/${v}`, dir });
|
|
18038
|
+
}
|
|
18039
|
+
}
|
|
18040
|
+
}
|
|
18041
|
+
return candidates;
|
|
18042
|
+
}
|
|
18043
|
+
function findRihalPackages(globalNodeModules) {
|
|
18044
|
+
const found = [];
|
|
18045
|
+
for (const scope of ["@hanzlaa", "@hanzlahabib"]) {
|
|
18046
|
+
const scopeDir = path2.join(globalNodeModules, scope);
|
|
18047
|
+
if (!exists(scopeDir)) continue;
|
|
18048
|
+
for (const pkg of readDirSafe(scopeDir)) {
|
|
18049
|
+
if (pkg === "rcode" || pkg === "rihal-code" || pkg.startsWith("rihal")) {
|
|
18050
|
+
found.push({ scope, pkg, dir: path2.join(scopeDir, pkg) });
|
|
18051
|
+
}
|
|
18052
|
+
}
|
|
18053
|
+
}
|
|
18054
|
+
return found;
|
|
18055
|
+
}
|
|
18056
|
+
function getGlobalBinDirs() {
|
|
18057
|
+
const home = os.homedir();
|
|
18058
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
18059
|
+
try {
|
|
18060
|
+
const r = spawnSync("npm", ["prefix", "-g"], { encoding: "utf8" });
|
|
18061
|
+
if (r.status === 0 && r.stdout.trim()) dirs.add(path2.join(r.stdout.trim(), "bin"));
|
|
18062
|
+
} catch {
|
|
18063
|
+
}
|
|
18064
|
+
try {
|
|
18065
|
+
const r = spawnSync("pnpm", ["bin", "-g"], { encoding: "utf8" });
|
|
18066
|
+
if (r.status === 0 && r.stdout.trim()) dirs.add(r.stdout.trim());
|
|
18067
|
+
} catch {
|
|
18068
|
+
}
|
|
18069
|
+
try {
|
|
18070
|
+
const r = spawnSync("yarn", ["global", "bin"], { encoding: "utf8" });
|
|
18071
|
+
if (r.status === 0 && r.stdout.trim()) dirs.add(r.stdout.trim());
|
|
18072
|
+
} catch {
|
|
18073
|
+
}
|
|
18074
|
+
try {
|
|
18075
|
+
const r = spawnSync("bun", ["pm", "bin", "-g"], { encoding: "utf8" });
|
|
18076
|
+
if (r.status === 0 && r.stdout.trim()) dirs.add(r.stdout.trim());
|
|
18077
|
+
} catch {
|
|
18078
|
+
}
|
|
18079
|
+
dirs.add(path2.join(home, ".local", "share", "pnpm"));
|
|
18080
|
+
dirs.add(path2.join(home, ".local", "bin"));
|
|
18081
|
+
dirs.add(path2.join(home, ".bun", "bin"));
|
|
18082
|
+
const nvmRoot = path2.join(home, ".nvm", "versions", "node");
|
|
18083
|
+
if (exists(nvmRoot)) {
|
|
18084
|
+
for (const v of readDirSafe(nvmRoot)) {
|
|
18085
|
+
dirs.add(path2.join(nvmRoot, v, "bin"));
|
|
18086
|
+
}
|
|
18087
|
+
}
|
|
18088
|
+
return [...dirs].filter(exists);
|
|
18089
|
+
}
|
|
18090
|
+
var RCODE_BINS = ["rcode", "rihal", "rihal-code"];
|
|
18091
|
+
function findRihalBins(binDir) {
|
|
18092
|
+
const found = [];
|
|
18093
|
+
for (const name of RCODE_BINS) {
|
|
18094
|
+
const p = path2.join(binDir, name);
|
|
18095
|
+
if (exists(p)) {
|
|
18096
|
+
let target = null;
|
|
18097
|
+
try {
|
|
18098
|
+
target = fs2.readlinkSync(p);
|
|
18099
|
+
} catch {
|
|
18100
|
+
}
|
|
18101
|
+
if (!target || /rcode|rihal-code|@hanzlaa|@hanzlahabib/.test(target)) {
|
|
18102
|
+
found.push({ name, path: p, target });
|
|
18103
|
+
}
|
|
18104
|
+
}
|
|
18105
|
+
}
|
|
18106
|
+
return found;
|
|
18107
|
+
}
|
|
18108
|
+
function findClaudeArtifacts(claudeDir) {
|
|
18109
|
+
const found = [];
|
|
18110
|
+
if (!exists(claudeDir)) return found;
|
|
18111
|
+
const cmdRoot = path2.join(claudeDir, "commands");
|
|
18112
|
+
if (exists(cmdRoot)) {
|
|
18113
|
+
for (const f of readDirSafe(cmdRoot)) {
|
|
18114
|
+
if (f.startsWith("rihal-") && (f.endsWith(".md") || f.endsWith(".mdc"))) {
|
|
18115
|
+
found.push({ kind: "command", path: path2.join(cmdRoot, f) });
|
|
18116
|
+
}
|
|
18117
|
+
}
|
|
18118
|
+
const rihalSubdir = path2.join(cmdRoot, "rihal");
|
|
18119
|
+
if (exists(rihalSubdir)) {
|
|
18120
|
+
found.push({ kind: "commands-dir", path: rihalSubdir });
|
|
18121
|
+
}
|
|
18122
|
+
}
|
|
18123
|
+
const agentsDir = path2.join(claudeDir, "agents");
|
|
18124
|
+
if (exists(agentsDir)) {
|
|
18125
|
+
for (const f of readDirSafe(agentsDir)) {
|
|
18126
|
+
if (f.startsWith("rihal-") && (f.endsWith(".md") || f.endsWith(".mdc"))) {
|
|
18127
|
+
found.push({ kind: "agent", path: path2.join(agentsDir, f) });
|
|
18128
|
+
}
|
|
18129
|
+
}
|
|
18130
|
+
}
|
|
18131
|
+
const skillsDir = path2.join(claudeDir, "skills");
|
|
18132
|
+
if (exists(skillsDir)) {
|
|
18133
|
+
for (const d of readDirSafe(skillsDir)) {
|
|
18134
|
+
if (d.startsWith("rihal-")) {
|
|
18135
|
+
found.push({ kind: "skill-dir", path: path2.join(skillsDir, d) });
|
|
18136
|
+
}
|
|
18137
|
+
}
|
|
18138
|
+
}
|
|
18139
|
+
return found;
|
|
18140
|
+
}
|
|
18141
|
+
function buildPlan({ includePlanning }) {
|
|
18142
|
+
const home = os.homedir();
|
|
18143
|
+
const cwd = process.cwd();
|
|
18144
|
+
const plan = {
|
|
18145
|
+
packages: [],
|
|
18146
|
+
bins: [],
|
|
18147
|
+
globalClaude: [],
|
|
18148
|
+
globalRihal: null,
|
|
18149
|
+
projectClaude: [],
|
|
18150
|
+
projectRihal: null,
|
|
18151
|
+
projectPlanning: null
|
|
18152
|
+
};
|
|
18153
|
+
for (const { manager, dir } of getGlobalNodeModulesDirs()) {
|
|
18154
|
+
for (const pkg of findRihalPackages(dir)) {
|
|
18155
|
+
plan.packages.push({ manager, ...pkg });
|
|
18156
|
+
}
|
|
18157
|
+
}
|
|
18158
|
+
for (const binDir of getGlobalBinDirs()) {
|
|
18159
|
+
for (const bin of findRihalBins(binDir)) {
|
|
18160
|
+
plan.bins.push({ binDir, ...bin });
|
|
18161
|
+
}
|
|
18162
|
+
}
|
|
18163
|
+
plan.globalClaude = findClaudeArtifacts(path2.join(home, ".claude"));
|
|
18164
|
+
const globalRihal = path2.join(home, ".rihal");
|
|
18165
|
+
if (exists(globalRihal)) plan.globalRihal = globalRihal;
|
|
18166
|
+
plan.projectClaude = findClaudeArtifacts(path2.join(cwd, ".claude"));
|
|
18167
|
+
const projectRihal = path2.join(cwd, ".rihal");
|
|
18168
|
+
if (exists(projectRihal) && cwd !== home) plan.projectRihal = projectRihal;
|
|
18169
|
+
if (includePlanning) {
|
|
18170
|
+
const projectPlanning = path2.join(cwd, ".planning");
|
|
18171
|
+
if (exists(projectPlanning) && cwd !== home) plan.projectPlanning = projectPlanning;
|
|
18172
|
+
}
|
|
18173
|
+
return plan;
|
|
18174
|
+
}
|
|
18175
|
+
function printPlan(plan, { dryRun }) {
|
|
18176
|
+
const banner = dryRun ? "[DRY RUN \u2014 pass --yes to remove]" : "[REMOVING]";
|
|
18177
|
+
console.log(`
|
|
18178
|
+
\u{1F525} rcode nuke ${banner}
|
|
18179
|
+
`);
|
|
18180
|
+
let total = 0;
|
|
18181
|
+
const section = (title, items) => {
|
|
18182
|
+
if (!items || Array.isArray(items) && items.length === 0) return;
|
|
18183
|
+
console.log(`${title}`);
|
|
18184
|
+
if (Array.isArray(items)) {
|
|
18185
|
+
for (const item of items) {
|
|
18186
|
+
console.log(` \u2022 ${item.description || item.path || item.dir || JSON.stringify(item)}`);
|
|
18187
|
+
total++;
|
|
18188
|
+
}
|
|
18189
|
+
} else {
|
|
18190
|
+
console.log(` \u2022 ${items}`);
|
|
18191
|
+
total++;
|
|
18192
|
+
}
|
|
18193
|
+
console.log("");
|
|
18194
|
+
};
|
|
18195
|
+
section("\u{1F4E6} Global packages (node_modules):", plan.packages.map((p) => ({
|
|
18196
|
+
description: `[${p.manager}] ${p.scope}/${p.pkg} \u2192 ${p.dir}`
|
|
18197
|
+
})));
|
|
18198
|
+
section("\u{1F517} Global binaries:", plan.bins.map((b) => ({
|
|
18199
|
+
description: `${b.path}${b.target ? ` \u2192 ${b.target}` : ""}`
|
|
18200
|
+
})));
|
|
18201
|
+
section("\u{1F916} ~/.claude/ artifacts:", plan.globalClaude.map((a) => ({
|
|
18202
|
+
description: `[${a.kind}] ${a.path}`
|
|
18203
|
+
})));
|
|
18204
|
+
if (plan.globalRihal) section("\u{1F5C2}\uFE0F ~/.rihal/ (global state):", plan.globalRihal);
|
|
18205
|
+
section("\u{1F916} ./.claude/ artifacts (current project):", plan.projectClaude.map((a) => ({
|
|
18206
|
+
description: `[${a.kind}] ${a.path}`
|
|
18207
|
+
})));
|
|
18208
|
+
if (plan.projectRihal) section("\u{1F5C2}\uFE0F ./.rihal/ (project state):", plan.projectRihal);
|
|
18209
|
+
if (plan.projectPlanning) section("\u{1F4CB} ./.planning/ (your work \u2014 only with --include-planning):", plan.projectPlanning);
|
|
18210
|
+
if (total === 0) {
|
|
18211
|
+
console.log(" \u2713 nothing to remove \u2014 system is clean.\n");
|
|
18212
|
+
} else {
|
|
18213
|
+
console.log(` Total: ${total} item(s).
|
|
18214
|
+
`);
|
|
18215
|
+
}
|
|
18216
|
+
return total;
|
|
18217
|
+
}
|
|
18218
|
+
function rmrf(p) {
|
|
18219
|
+
try {
|
|
18220
|
+
fs2.rmSync(p, { recursive: true, force: true });
|
|
18221
|
+
return true;
|
|
18222
|
+
} catch (err) {
|
|
18223
|
+
console.warn(` \u26A0 failed to remove ${p}: ${err.message}`);
|
|
18224
|
+
return false;
|
|
18225
|
+
}
|
|
18226
|
+
}
|
|
18227
|
+
function uninstallPackage(manager, scope, pkg) {
|
|
18228
|
+
const fullName = `${scope}/${pkg}`;
|
|
18229
|
+
const cmd = manager === "pnpm" ? ["pnpm", ["remove", "-g", fullName]] : manager === "yarn" ? ["yarn", ["global", "remove", fullName]] : manager === "bun" ? ["bun", ["remove", "-g", fullName]] : ["npm", ["uninstall", "-g", fullName]];
|
|
18230
|
+
try {
|
|
18231
|
+
const r = spawnSync(cmd[0], cmd[1], { stdio: "pipe", encoding: "utf8" });
|
|
18232
|
+
return r.status === 0;
|
|
18233
|
+
} catch {
|
|
18234
|
+
return false;
|
|
18235
|
+
}
|
|
18236
|
+
}
|
|
18237
|
+
function executePlan(plan) {
|
|
18238
|
+
let removed = 0;
|
|
18239
|
+
for (const p of plan.packages) {
|
|
18240
|
+
const manager = p.manager.startsWith("nvm/") ? "npm" : p.manager;
|
|
18241
|
+
process.stdout.write(` ${manager} uninstall ${p.scope}/${p.pkg} ... `);
|
|
18242
|
+
const ok = uninstallPackage(manager, p.scope, p.pkg);
|
|
18243
|
+
console.log(ok ? "\u2713" : "\u26A0 failed via PM, falling back to rm");
|
|
18244
|
+
if (!ok) rmrf(p.dir);
|
|
18245
|
+
removed++;
|
|
18246
|
+
}
|
|
18247
|
+
for (const b of plan.bins) {
|
|
18248
|
+
if (exists(b.path)) {
|
|
18249
|
+
if (rmrf(b.path)) {
|
|
18250
|
+
console.log(` \u2713 removed bin ${b.path}`);
|
|
18251
|
+
removed++;
|
|
18252
|
+
}
|
|
18253
|
+
}
|
|
18254
|
+
}
|
|
18255
|
+
for (const a of plan.globalClaude) {
|
|
18256
|
+
if (rmrf(a.path)) {
|
|
18257
|
+
console.log(` \u2713 removed ${a.path}`);
|
|
18258
|
+
removed++;
|
|
18259
|
+
}
|
|
18260
|
+
}
|
|
18261
|
+
if (plan.globalRihal && rmrf(plan.globalRihal)) {
|
|
18262
|
+
console.log(` \u2713 removed ${plan.globalRihal}`);
|
|
18263
|
+
removed++;
|
|
18264
|
+
}
|
|
18265
|
+
for (const a of plan.projectClaude) {
|
|
18266
|
+
if (rmrf(a.path)) {
|
|
18267
|
+
console.log(` \u2713 removed ${a.path}`);
|
|
18268
|
+
removed++;
|
|
18269
|
+
}
|
|
18270
|
+
}
|
|
18271
|
+
if (plan.projectRihal && rmrf(plan.projectRihal)) {
|
|
18272
|
+
console.log(` \u2713 removed ${plan.projectRihal}`);
|
|
18273
|
+
removed++;
|
|
18274
|
+
}
|
|
18275
|
+
if (plan.projectPlanning && rmrf(plan.projectPlanning)) {
|
|
18276
|
+
console.log(` \u2713 removed ${plan.projectPlanning}`);
|
|
18277
|
+
removed++;
|
|
18278
|
+
}
|
|
18279
|
+
return removed;
|
|
18280
|
+
}
|
|
18281
|
+
module2.exports = function nuke(args = []) {
|
|
18282
|
+
const dryRun = !args.includes("--yes") && !args.includes("-y");
|
|
18283
|
+
const includePlanning = args.includes("--include-planning");
|
|
18284
|
+
const cwdPkgJson = path2.join(process.cwd(), "package.json");
|
|
18285
|
+
if (exists(cwdPkgJson)) {
|
|
18286
|
+
try {
|
|
18287
|
+
const pkg = JSON.parse(fs2.readFileSync(cwdPkgJson, "utf8"));
|
|
18288
|
+
if (pkg.name === "@hanzlaa/rcode" || pkg.name === "@hanzlahabib/rihal-code") {
|
|
18289
|
+
console.log("\n\u26A0 You are inside the rcode source repo.");
|
|
18290
|
+
console.log(" Nuke would remove this repo's .claude/, .rihal/, and possibly .planning/.");
|
|
18291
|
+
console.log(" That is your source code \u2014 almost certainly not what you want.");
|
|
18292
|
+
console.log(" Run nuke from a different directory, or pass --i-know-what-im-doing to override.\n");
|
|
18293
|
+
if (!args.includes("--i-know-what-im-doing")) return;
|
|
18294
|
+
}
|
|
18295
|
+
} catch {
|
|
18296
|
+
}
|
|
18297
|
+
}
|
|
18298
|
+
const plan = buildPlan({ includePlanning });
|
|
18299
|
+
const total = printPlan(plan, { dryRun });
|
|
18300
|
+
if (total === 0 || dryRun) {
|
|
18301
|
+
if (dryRun && total > 0) {
|
|
18302
|
+
console.log("To actually remove these, re-run with: rcode nuke --yes");
|
|
18303
|
+
console.log("To also remove .planning/ (your work): rcode nuke --yes --include-planning\n");
|
|
18304
|
+
}
|
|
18305
|
+
return;
|
|
18306
|
+
}
|
|
18307
|
+
console.log("Executing...\n");
|
|
18308
|
+
const removed = executePlan(plan);
|
|
18309
|
+
console.log(`
|
|
18310
|
+
\u2705 Done. Removed ${removed} item(s). Reinstall: npm install -g @hanzlaa/rcode
|
|
18311
|
+
`);
|
|
18312
|
+
};
|
|
18313
|
+
}
|
|
18314
|
+
});
|
|
18315
|
+
|
|
17979
18316
|
// cli/dashboard.js
|
|
17980
18317
|
var require_dashboard = __commonJS({
|
|
17981
18318
|
"cli/dashboard.js"(exports2, module2) {
|
|
@@ -18530,6 +18867,65 @@ var require_doctor = __commonJS({
|
|
|
18530
18867
|
}
|
|
18531
18868
|
return failing;
|
|
18532
18869
|
}
|
|
18870
|
+
function checkDuplicateInstalls() {
|
|
18871
|
+
const os = require("os");
|
|
18872
|
+
const home = os.homedir();
|
|
18873
|
+
const installs = [];
|
|
18874
|
+
const dirs = [];
|
|
18875
|
+
for (const cmd of [["npm", ["root", "-g"]], ["pnpm", ["root", "-g"]]]) {
|
|
18876
|
+
try {
|
|
18877
|
+
const r = spawnSync(cmd[0], cmd[1], { encoding: "utf8" });
|
|
18878
|
+
if (r.status === 0 && r.stdout.trim()) dirs.push({ manager: cmd[0], dir: r.stdout.trim() });
|
|
18879
|
+
} catch {
|
|
18880
|
+
}
|
|
18881
|
+
}
|
|
18882
|
+
const nvmRoot = path2.join(home, ".nvm", "versions", "node");
|
|
18883
|
+
if (fs2.existsSync(nvmRoot)) {
|
|
18884
|
+
for (const v of fs2.readdirSync(nvmRoot)) {
|
|
18885
|
+
const dir = path2.join(nvmRoot, v, "lib", "node_modules");
|
|
18886
|
+
if (fs2.existsSync(dir) && !dirs.some((d) => d.dir === dir)) {
|
|
18887
|
+
dirs.push({ manager: `nvm/${v}`, dir });
|
|
18888
|
+
}
|
|
18889
|
+
}
|
|
18890
|
+
}
|
|
18891
|
+
for (const { manager, dir } of dirs) {
|
|
18892
|
+
for (const scope of ["@hanzlaa", "@hanzlahabib"]) {
|
|
18893
|
+
const scopeDir = path2.join(dir, scope);
|
|
18894
|
+
if (!fs2.existsSync(scopeDir)) continue;
|
|
18895
|
+
for (const pkg of fs2.readdirSync(scopeDir)) {
|
|
18896
|
+
if (pkg === "rcode" || pkg === "rihal-code" || pkg.startsWith("rihal")) {
|
|
18897
|
+
let version = "unknown";
|
|
18898
|
+
try {
|
|
18899
|
+
const pkgJson = JSON.parse(fs2.readFileSync(path2.join(scopeDir, pkg, "package.json"), "utf8"));
|
|
18900
|
+
version = pkgJson.version;
|
|
18901
|
+
} catch {
|
|
18902
|
+
}
|
|
18903
|
+
installs.push({ manager, scope, pkg, version, dir: path2.join(scopeDir, pkg) });
|
|
18904
|
+
}
|
|
18905
|
+
}
|
|
18906
|
+
}
|
|
18907
|
+
}
|
|
18908
|
+
return { count: installs.length, installs };
|
|
18909
|
+
}
|
|
18910
|
+
function printDuplicateChecks(result) {
|
|
18911
|
+
if (result.count === 0) {
|
|
18912
|
+
console.log(` \u2713 No global rcode installs found`);
|
|
18913
|
+
return 0;
|
|
18914
|
+
}
|
|
18915
|
+
if (result.count === 1) {
|
|
18916
|
+
const i = result.installs[0];
|
|
18917
|
+
console.log(` \u2713 Single global install: [${i.manager}] ${i.scope}/${i.pkg}@${i.version}`);
|
|
18918
|
+
return 0;
|
|
18919
|
+
}
|
|
18920
|
+
console.log(` \u2717 ${result.count} global installs detected \u2014 this causes duplicate slash commands!`);
|
|
18921
|
+
for (const i of result.installs) {
|
|
18922
|
+
console.log(` [${i.manager}] ${i.scope}/${i.pkg}@${i.version}`);
|
|
18923
|
+
console.log(` ${i.dir}`);
|
|
18924
|
+
}
|
|
18925
|
+
console.log(`
|
|
18926
|
+
Fix: rcode nuke --yes && npm install -g @hanzlaa/rcode`);
|
|
18927
|
+
return 1;
|
|
18928
|
+
}
|
|
18533
18929
|
module2.exports = function doctor(args, { packageRoot }) {
|
|
18534
18930
|
const cwd = process.cwd();
|
|
18535
18931
|
console.log(`
|
|
@@ -18539,9 +18935,13 @@ var require_doctor = __commonJS({
|
|
|
18539
18935
|
const checks = runPreflight(cwd, packageRoot);
|
|
18540
18936
|
const preflightFailures = printChecks(checks);
|
|
18541
18937
|
console.log(`
|
|
18938
|
+
Duplicate installations:`);
|
|
18939
|
+
const dupResult = checkDuplicateInstalls();
|
|
18940
|
+
const duplicateFailures = printDuplicateChecks(dupResult);
|
|
18941
|
+
console.log(`
|
|
18542
18942
|
Package compliance:`);
|
|
18543
18943
|
const complianceFailures = runCompliance(packageRoot);
|
|
18544
|
-
const totalFailures = preflightFailures + complianceFailures;
|
|
18944
|
+
const totalFailures = preflightFailures + complianceFailures + duplicateFailures;
|
|
18545
18945
|
console.log();
|
|
18546
18946
|
if (totalFailures === 0) {
|
|
18547
18947
|
console.log(`\u2705 All checks passed.`);
|
|
@@ -20483,6 +20883,8 @@ var COMMANDS = {
|
|
|
20483
20883
|
uninstall: require_uninstall(),
|
|
20484
20884
|
remove: require_uninstall(),
|
|
20485
20885
|
// alias
|
|
20886
|
+
nuke: require_nuke(),
|
|
20887
|
+
// full cleanup across all package managers + global state
|
|
20486
20888
|
dashboard: require_dashboard(),
|
|
20487
20889
|
serve: require_dashboard(),
|
|
20488
20890
|
digest: require_digest(),
|
|
@@ -20514,6 +20916,10 @@ Usage:
|
|
|
20514
20916
|
update Refresh skill files (backs up .rihal/ state first)
|
|
20515
20917
|
uninstall Remove Rihal Code from the current project
|
|
20516
20918
|
remove Alias for uninstall
|
|
20919
|
+
nuke Wipe ALL rihal/rcode installs everywhere (global packages,
|
|
20920
|
+
binaries, ~/.claude/* rihal artifacts, ~/.rihal/, project artifacts)
|
|
20921
|
+
Default = dry-run. Pass --yes to remove. Pass --include-planning
|
|
20922
|
+
to also remove .planning/ in CWD.
|
|
20517
20923
|
config Get/set project configuration (project_name, user_name, etc.)
|
|
20518
20924
|
context Memory bank freshness (--check | --refresh | --install-hook)
|
|
20519
20925
|
github-sync Sync .rihal/ phases/epics/stories to GitHub (dry-run default)
|
|
@@ -20542,8 +20948,46 @@ Getting started:
|
|
|
20542
20948
|
Documentation: https://github.com/hanzlahabib/rihal-code
|
|
20543
20949
|
`.trim());
|
|
20544
20950
|
}
|
|
20951
|
+
function maybeShowFirstRunBanner() {
|
|
20952
|
+
const os = require("os");
|
|
20953
|
+
const home = os.homedir();
|
|
20954
|
+
const markerDir = path.join(home, ".rihal");
|
|
20955
|
+
const marker = path.join(markerDir, ".welcome-shown");
|
|
20956
|
+
if (fs.existsSync(marker)) return;
|
|
20957
|
+
const globalCommands = path.join(home, ".claude", "commands");
|
|
20958
|
+
let hasGlobalRihal = false;
|
|
20959
|
+
try {
|
|
20960
|
+
hasGlobalRihal = fs.existsSync(globalCommands) && fs.readdirSync(globalCommands).some((f) => f.startsWith("rihal-") && f.endsWith(".md"));
|
|
20961
|
+
} catch {
|
|
20962
|
+
}
|
|
20963
|
+
if (!hasGlobalRihal) return;
|
|
20964
|
+
console.log(`
|
|
20965
|
+
\u{1F54C} Rihal Code v${PACKAGE_JSON.version} \u2014 first run detected.
|
|
20966
|
+
`);
|
|
20967
|
+
console.log(` \u2713 ${countGlobalRihal(globalCommands)} slash commands installed \u2192 ~/.claude/commands/`);
|
|
20968
|
+
console.log(` \u2713 All /rihal-* commands available in every Claude Code project.`);
|
|
20969
|
+
console.log(`
|
|
20970
|
+
To set up a project: cd my-project && rcode install`);
|
|
20971
|
+
console.log(` Show all commands: rcode help`);
|
|
20972
|
+
console.log(` Diagnose issues: rcode doctor
|
|
20973
|
+
`);
|
|
20974
|
+
try {
|
|
20975
|
+
fs.mkdirSync(markerDir, { recursive: true });
|
|
20976
|
+
fs.writeFileSync(marker, `installed ${PACKAGE_JSON.version} at ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
20977
|
+
`);
|
|
20978
|
+
} catch {
|
|
20979
|
+
}
|
|
20980
|
+
}
|
|
20981
|
+
function countGlobalRihal(dir) {
|
|
20982
|
+
try {
|
|
20983
|
+
return fs.readdirSync(dir).filter((f) => f.startsWith("rihal-") && f.endsWith(".md")).length;
|
|
20984
|
+
} catch {
|
|
20985
|
+
return 0;
|
|
20986
|
+
}
|
|
20987
|
+
}
|
|
20545
20988
|
async function main() {
|
|
20546
20989
|
const [, , command = "help", ...args] = process.argv;
|
|
20990
|
+
maybeShowFirstRunBanner();
|
|
20547
20991
|
const handler = COMMANDS[command];
|
|
20548
20992
|
if (!handler) {
|
|
20549
20993
|
console.error(`Unknown command: ${command}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzlaa/rcode",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.18",
|
|
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": {
|
|
@@ -2390,6 +2390,11 @@ function cmdState(subArgs) {
|
|
|
2390
2390
|
// Format B — heading style: ## Phase 01 — Name / ### Phase 01: Name
|
|
2391
2391
|
// Milestone heading is also matched in any of: "## Milestone M1", "## Milestone v1.0 — Name",
|
|
2392
2392
|
// "**Milestone: v1.0 — Name**".
|
|
2393
|
+
|
|
2394
|
+
// Issue #651 — must be declared in outer scope. The prune step at end of
|
|
2395
|
+
// sync references seenNums even when roadmap_exists is false (no-op prune
|
|
2396
|
+
// path), causing 'seenNums is not defined' crash.
|
|
2397
|
+
const seenNums = new Set();
|
|
2393
2398
|
if (parsed.roadmap_exists) {
|
|
2394
2399
|
const roadmap = fs.readFileSync(roadmapPath, 'utf8');
|
|
2395
2400
|
const milestoneMatches = [
|
|
@@ -2425,8 +2430,6 @@ function cmdState(subArgs) {
|
|
|
2425
2430
|
parsed.phases_normalized = beforeClean - cleaned.length;
|
|
2426
2431
|
state.phases = cleaned;
|
|
2427
2432
|
|
|
2428
|
-
const seenNums = new Set();
|
|
2429
|
-
|
|
2430
2433
|
const upsertPhase = (phaseNum, phaseName, phaseGoal) => {
|
|
2431
2434
|
if (!/^\d/.test(phaseNum)) return;
|
|
2432
2435
|
if (phaseName.toLowerCase() === 'phase') return;
|
|
@@ -573,7 +573,8 @@ If `CODE_REVIEW_ENABLED` is `"false"`: display "Code review skipped (workflow.co
|
|
|
573
573
|
REVIEWER_MODEL=$(node ".rihal/bin/rihal-tools.cjs" resolve-model code-reviewer 2>/dev/null | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{console.log(JSON.parse(d).model)}catch{console.log('')}})" || echo "sonnet")
|
|
574
574
|
REVIEWER_MODEL=${REVIEWER_MODEL:-sonnet}
|
|
575
575
|
REVIEWER_SKILLS=$(node ".rihal/bin/rihal-tools.cjs" agent-skills rihal-code-reviewer 2>/dev/null || echo "")
|
|
576
|
-
|
|
576
|
+
# Issue #652 — no leading zeros. Variable name kept for backward compat in this workflow.
|
|
577
|
+
PADDED="${PHASE_NUMBER}"
|
|
577
578
|
REVIEW_FILE="${PHASE_DIR}/${PADDED}-REVIEW.md"
|
|
578
579
|
```
|
|
579
580
|
|
package/rihal/workflows/plan.md
CHANGED
|
@@ -209,8 +209,9 @@ Exit workflow.
|
|
|
209
209
|
|
|
210
210
|
```bash
|
|
211
211
|
EXISTING_PLAN_COUNT=$(ls "${PHASE_DIR}"/*-SPRINT.md 2>/dev/null | wc -l | tr -d ' ')
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
# Issue #652 — no leading zeros in planning artifacts. Phase 8 not 08, plan 2 not 02.
|
|
213
|
+
NEXT_PLAN_NUMBER=$((EXISTING_PLAN_COUNT + 1))
|
|
214
|
+
PADDED_PHASE="${PHASE}"
|
|
214
215
|
GAP_PLAN_FILENAME="${PADDED_PHASE}-${NEXT_PLAN_NUMBER}-SPRINT.md"
|
|
215
216
|
GAP_PLAN_PATH="${PHASE_DIR}/${GAP_PLAN_FILENAME}"
|
|
216
217
|
```
|