@hanzlaa/rcode 3.4.27 → 3.4.29

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 CHANGED
@@ -24,7 +24,7 @@ If a user says "just keep going" or "don't stop until done", that authorization
24
24
 
25
25
  - Follow [Conventional Commits](https://www.conventionalcommits.org/) format: `type(scope): subject`
26
26
  - Types allowed: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `perf`, `revert`
27
- - Scopes allowed: `agents`, `skills`, `workflows`, `templates`, `dashboard`, `docs`, `config`, `github`, `commands`, `memory`, `brand`, `cli`, `ci`, `release`, `meta`, `tasks`, `migrations`, `refs`, `state`, `hooks`, `install`, `parity`, `triggers`, `dogfood`, `namespace`, `planning`, `insights`, `help`, `roadmap`, `session`, `audits`, `execute`, `executor`, `plan`, `planner`, `readme`, `sync`, `sprint`, `agent-exp`, `extensibility`, `lens-audit`, `tiers`, plus numeric phase/sprint scopes (e.g. `docs(15)`, `feat(8.3)`)
27
+ - Scopes allowed: `agents`, `skills`, `workflows`, `templates`, `dashboard`, `docs`, `config`, `github`, `commands`, `memory`, `brand`, `cli`, `ci`, `release`, `meta`, `tasks`, `migrations`, `refs`, `state`, `hooks`, `install`, `parity`, `triggers`, `dogfood`, `namespace`, `planning`, `insights`, `help`, `roadmap`, `session`, `audits`, `execute`, `executor`, `plan`, `planner`, `readme`, `sync`, `sprint`, `agent-exp`, `extensibility`, `lens-audit`, `tiers`, `build`, `council`, `doctor`, `postinstall`, `progress`, `security`, `tools`, `uninstall`, `update`, `test`, plus numeric phase/sprint scopes (e.g. `docs(15)`, `feat(8.3)`)
28
28
  - Subject: lowercase first letter, imperative mood, no trailing period, under 72 chars
29
29
  - **NEVER add Claude/AI attribution to commit messages.** No "Generated with Claude Code", no "Co-Authored-By: Claude", no "🤖 Generated". The user does not want this.
30
30
  - **NEVER use `--no-verify`** to bypass hooks. If hooks fail, fix the underlying issue.
package/CONTRIBUTING.md CHANGED
@@ -298,6 +298,16 @@ We use [Conventional Commits](https://www.conventionalcommits.org/) format. The
298
298
  - `extensibility` — extensibility and plugin hooks
299
299
  - `lens-audit` — 15-lens audit system and lenses
300
300
  - `tiers` — TIERS.md and tier-related documentation
301
+ - `build` — `scripts/build.cjs`, esbuild config, bundle artifacts
302
+ - `council` — `/rihal-council` workflow + spawning logic
303
+ - `doctor` — `cli/doctor.js` health checks
304
+ - `postinstall` — `cli/postinstall.js` lifecycle hook
305
+ - `progress` — `/rihal-progress` workflow
306
+ - `security` — security guardrails (symlink guards, integrity checks)
307
+ - `test` — test files under `test/` (test-only changes)
308
+ - `tools` — `rihal/bin/rihal-tools.cjs` subcommands
309
+ - `uninstall` — `cli/uninstall.js` flow
310
+ - `update` — `cli/update.js` flow
301
311
  - `<phase-id>` — numeric phase scope when committing inside a phase (e.g. `docs(15)`, `feat(8.3)`)
302
312
  - `<sprint-id>` — numeric sprint scope inside a phase (e.g. `feat(15.1)`)
303
313
 
package/cli/install.js CHANGED
@@ -649,13 +649,28 @@ function seedStarterPlanning(target, projectName) {
649
649
  // - sprint tools that previously relied on phase 01 will surface a clear
650
650
  // "no phases yet — run /rihal-new-project first" error instead of
651
651
  // silently operating on a fake phase
652
+ //
653
+ // Issue #705: only mark _seeded_stub when the planning ROADMAP is also
654
+ // a stub. If the user manually deletes state.json but has real
655
+ // .planning/ROADMAP.md (no INSTALL STUB banner), seeding _seeded_stub
656
+ // would mis-classify a real project as fresh and let /rihal-new-project
657
+ // overwrite it. Guard with the banner check.
652
658
  const rihalStateJson = path.join(target, '.rihal', 'state.json');
659
+ function planningRoadmapIsStub() {
660
+ const rmPath = path.join(target, '.planning', 'ROADMAP.md');
661
+ if (!fs.existsSync(rmPath)) return true; // missing → fresh install case
662
+ try {
663
+ const text = fs.readFileSync(rmPath, 'utf8');
664
+ return text.includes('<!-- INSTALL STUB');
665
+ } catch { return true; }
666
+ }
653
667
  if (!fs.existsSync(rihalStateJson)) {
654
668
  const now = new Date().toISOString();
669
+ const isStubProject = planningRoadmapIsStub();
655
670
  const state = {
656
671
  version: '1',
657
672
  project: null,
658
- _seeded_stub: true,
673
+ ...(isStubProject ? { _seeded_stub: true } : {}),
659
674
  created: now,
660
675
  updated: now,
661
676
  current_phase: null,
@@ -1260,7 +1275,7 @@ function generateAgentManifest(plan, target) {
1260
1275
  * Generate files-manifest.csv with SHA256 per installed file. Used by
1261
1276
  * update/doctor to detect drift. Columns: rel, sha256, size.
1262
1277
  */
1263
- function generateFilesManifest(plan, target, { mergeExistingManifest = false } = {}) {
1278
+ function generateFilesManifest(plan, target, { mergeExistingManifest = false, extraScanDirs = [] } = {}) {
1264
1279
  const rows = [['rel', 'sha256', 'size']];
1265
1280
  const newRels = new Set();
1266
1281
 
@@ -1273,6 +1288,34 @@ function generateFilesManifest(plan, target, { mergeExistingManifest = false } =
1273
1288
  newRels.add(rel);
1274
1289
  }
1275
1290
 
1291
+ // Issue #702: skills installed via installSkills() and sidebar stubs
1292
+ // generated by cli/generate-command-skills.cjs are NOT in the install plan
1293
+ // (they're walked from rihal/skills/ separately and copied directly).
1294
+ // Without this scan, files-manifest.csv was missing the largest category
1295
+ // of installed files — orphan sweep + doctor drift detection were blind
1296
+ // to renamed/removed skills.
1297
+ function walkScanDir(absDir) {
1298
+ if (!fs.existsSync(absDir)) return;
1299
+ for (const entry of fs.readdirSync(absDir, { withFileTypes: true })) {
1300
+ const full = path.join(absDir, entry.name);
1301
+ if (entry.isDirectory()) {
1302
+ walkScanDir(full);
1303
+ } else if (entry.isFile()) {
1304
+ const rel = path.relative(target, full).split(path.sep).join('/');
1305
+ if (newRels.has(rel)) continue; // already in plan
1306
+ // Skip files outside the project root (defense-in-depth — extraScanDirs
1307
+ // is a code-controlled set, but cheap to verify).
1308
+ if (rel.startsWith('..') || path.isAbsolute(rel)) continue;
1309
+ try {
1310
+ const buf = fs.readFileSync(full);
1311
+ rows.push([rel, sha256(buf), String(buf.length)]);
1312
+ newRels.add(rel);
1313
+ } catch { /* unreadable file — skip */ }
1314
+ }
1315
+ }
1316
+ }
1317
+ for (const scan of extraScanDirs) walkScanDir(scan);
1318
+
1276
1319
  // Merge old manifest entries that are still on disk but not in the current
1277
1320
  // plan — this keeps orphaned files traceable by doctor/uninstall even when
1278
1321
  // --force sweep was not run. Without this, a re-install without --force
@@ -1336,20 +1379,26 @@ function sweepStaleInstalledFiles(target, newPlan) {
1336
1379
 
1337
1380
  let removed = 0;
1338
1381
  const emptyCandidateDirs = new Set();
1382
+ // Issue #703: a tampered or malformed CSV could contain a rel like
1383
+ // '../../etc/passwd'. path.join collapses '..' segments and could escape
1384
+ // the project root. Use safeRmSync's project-root containment check —
1385
+ // any rel whose realpath escapes target is refused with reason='outside-root'.
1386
+ const targetRoot = path.resolve(target);
1339
1387
  for (const rel of oldRels) {
1340
1388
  if (newRelsSet.has(rel)) continue;
1341
1389
  if (neverSweep.test(rel)) continue;
1342
1390
  if (isLocalOverride(rel)) continue; // #382 — never sweep user-owned overrides
1391
+ // Reject relative paths that obviously try to escape before even hitting fs.
1392
+ if (rel.includes('..') || path.isAbsolute(rel)) continue;
1343
1393
  const full = path.join(target, rel);
1344
- try {
1345
- if (fs.existsSync(full)) {
1346
- fs.rmSync(full, { force: true });
1347
- emptyCandidateDirs.add(path.dirname(full));
1348
- removed += 1;
1349
- }
1350
- } catch {
1351
- // ignore individual failures — sweep is best-effort
1394
+ if (!fs.existsSync(full)) continue;
1395
+ const result = safeRmSync(full, targetRoot);
1396
+ if (result.ok) {
1397
+ emptyCandidateDirs.add(path.dirname(full));
1398
+ removed += 1;
1352
1399
  }
1400
+ // outside-root / lstat / unlink failures are silently skipped — sweep is
1401
+ // best-effort and we never want to abort the install on a single bad row.
1353
1402
  }
1354
1403
 
1355
1404
  // Remove any now-empty parent dirs (bottom-up, so nested emptiness cascades).
@@ -2073,9 +2122,28 @@ async function installInner(opts) {
2073
2122
  const stateSrc = path.join(SOURCE_ROOT, 'state.json');
2074
2123
  if (fs.existsSync(stateSrc)) {
2075
2124
  const now = new Date().toISOString();
2076
- const stateContent = fs.readFileSync(stateSrc, 'utf8')
2125
+ let stateContent = fs.readFileSync(stateSrc, 'utf8')
2077
2126
  .replace(/__PROJECT_NAME__/g, opts.projectName)
2078
2127
  .replace(/__INSTALL_DATE__/g, now);
2128
+
2129
+ // Issue #705: the template ships with _seeded_stub:true. If the user
2130
+ // already has a real planning ROADMAP (no INSTALL STUB banner) but
2131
+ // state.json is missing (manually deleted), restoring with the stub
2132
+ // marker would mis-classify a real project as fresh. Strip the marker
2133
+ // when ROADMAP exists and isn't itself a stub.
2134
+ const rmPath = path.join(opts.target, '.planning', 'ROADMAP.md');
2135
+ if (fs.existsSync(rmPath)) {
2136
+ try {
2137
+ const rm = fs.readFileSync(rmPath, 'utf8');
2138
+ if (!rm.includes('<!-- INSTALL STUB')) {
2139
+ // Remove "_seeded_stub": true, line. JSON is small + flat enough
2140
+ // to do this with a regex; matches whether the field is followed
2141
+ // by a comma or sits as the last key.
2142
+ stateContent = stateContent.replace(/^\s*"_seeded_stub":\s*true,?\s*\n/m, '');
2143
+ }
2144
+ } catch { /* fall through with stub marker — safe default */ }
2145
+ }
2146
+
2079
2147
  ensureDir(path.dirname(stateDest));
2080
2148
  writeFileAtomic(stateDest, stateContent);
2081
2149
  }
@@ -2101,13 +2169,12 @@ async function installInner(opts) {
2101
2169
  const globalAgentsDir = path.join(os.homedir(), '.rihal', 'agents');
2102
2170
  ensureDir(globalAgentsDir);
2103
2171
 
2104
- // files-manifest.csv written LAST so it includes itself's siblings
2105
- // (but not itself, since hashing a file referencing its own hash is
2106
- // self-referential nonsense).
2107
- fs.writeFileSync(
2108
- path.join(configDir, 'files-manifest.csv'),
2109
- generateFilesManifest(plan, opts.target, { mergeExistingManifest: !opts.force }),
2110
- );
2172
+ // Issue #702: files-manifest.csv used to be written here, BEFORE
2173
+ // installSkills + generateCommandSkills ran. The 100+ skill files those
2174
+ // functions install were therefore invisible to sweepStaleInstalledFiles
2175
+ // and doctor's drift detection. Manifest generation moved below to AFTER
2176
+ // all skill installations complete, with extraScanDirs covering both
2177
+ // .claude/skills/ and .rihal/skills/ on disk.
2111
2178
 
2112
2179
  // Install v1-style phrase-activated skills (scaffold-project, create-prd,
2113
2180
  // retrospective, etc.) into .claude/skills/ alongside the v2 agents/commands.
@@ -2144,6 +2211,20 @@ async function installInner(opts) {
2144
2211
  console.log(' ' + dim(`(sidebar stub generation skipped: ${err.message})`));
2145
2212
  }
2146
2213
 
2214
+ // Issue #702: write files-manifest.csv NOW, after all installs complete.
2215
+ // extraScanDirs picks up the skills + sidebar stubs that aren't in the
2216
+ // plan array.
2217
+ fs.writeFileSync(
2218
+ path.join(configDir, 'files-manifest.csv'),
2219
+ generateFilesManifest(plan, opts.target, {
2220
+ mergeExistingManifest: !opts.force,
2221
+ extraScanDirs: [
2222
+ path.join(opts.target, '.claude', 'skills'),
2223
+ path.join(opts.target, '.rihal', 'skills'),
2224
+ ],
2225
+ }),
2226
+ );
2227
+
2147
2228
  // Seed .planning/ with starter ROADMAP + STATE so workflows work immediately
2148
2229
  const starterSeeded = seedStarterPlanning(opts.target, opts.projectName);
2149
2230
 
@@ -2167,10 +2248,14 @@ async function installInner(opts) {
2167
2248
  const { execFileSync } = require('child_process');
2168
2249
  const toolsPath = path.join(opts.target, '.rihal', 'bin', 'rihal-tools.cjs');
2169
2250
  if (fs.existsSync(toolsPath)) {
2251
+ // Issue #706: 60s timeout — without it, a slow upstream URL hangs the
2252
+ // entire install indefinitely. Brain pull is best-effort, so a timeout
2253
+ // failure is treated identically to any other pull failure (caught below).
2170
2254
  const out = execFileSync('node', [toolsPath, 'brain', 'pull'], {
2171
2255
  cwd: opts.target,
2172
2256
  encoding: 'utf8',
2173
2257
  stdio: ['ignore', 'pipe', 'pipe'],
2258
+ timeout: 60_000,
2174
2259
  });
2175
2260
  try { brainReport = JSON.parse(out); } catch {}
2176
2261
  }
@@ -109,7 +109,12 @@ function diffSet(editor, kind, expected, installed) {
109
109
  * makes doctor report drift like "agents 119/23" when nothing is wrong.
110
110
  * That's why the agent count comes from .claude/agents/, not .claude/skills/.
111
111
  */
112
- function verifyClaudeInstall(cwd, packageRoot) {
112
+ function verifyClaudeInstall(cwd, packageRoot, options = {}) {
113
+ // Issue #698: tests assert against an isolated tempdir cwd. The global
114
+ // fallback (#664) makes that impossible because it reads the contributor's
115
+ // real ~/.claude/. Tests can pass { globalFallback: false } to disable it.
116
+ // Default remains true to preserve the runtime behavior introduced in #664.
117
+ const globalFallback = options.globalFallback !== false;
113
118
  const pkg = readPackageManifest(packageRoot);
114
119
  const agentsDir = path.join(cwd, '.claude/agents');
115
120
  const skillsDir = path.join(cwd, '.claude/skills');
@@ -129,7 +134,7 @@ function verifyClaudeInstall(cwd, packageRoot) {
129
134
  // level .claude/agents/rihal-*.md when the user's ~/.claude/ already has
130
135
  // them, to avoid duplicate commands. Without this fallback the verifier
131
136
  // reports 0 agents on every successful install in that scenario.
132
- if (installedAgents.size === 0) {
137
+ if (installedAgents.size === 0 && globalFallback) {
133
138
  try {
134
139
  const os = require('os');
135
140
  const globalAgentsDir = path.join(os.homedir(), '.claude/agents');
@@ -143,12 +148,13 @@ function verifyClaudeInstall(cwd, packageRoot) {
143
148
  } catch { /* non-fatal — permission errors etc. */ }
144
149
  }
145
150
 
146
- // Actions: .claude/skills/<bare-name>/ exclude rihal-* dirs (those are
147
- // either agent stubs or command stubs, never action skills).
151
+ // Actions: .claude/skills/rihal-<name>/. installSkills (cli/install.js)
152
+ // prefixes every action with rihal-, and readPackageManifest does the
153
+ // same — so both sides are normalized. The previous version filtered OUT
154
+ // rihal-* dirs which excluded ALL real actions and made the diff always
155
+ // report "everything missing." Compare directly against the prefixed set.
148
156
  const allInstalled = readInstalledDirs(skillsDir);
149
- const actionsInstalled = new Set(
150
- [...allInstalled].filter((n) => !n.startsWith('rihal-'))
151
- );
157
+ const actionsInstalled = new Set([...allInstalled].filter((n) => pkg.actions.has(n)));
152
158
 
153
159
  return [
154
160
  diffSet('claude', 'agents', pkg.agents, installedAgents),
package/cli/uninstall.js CHANGED
@@ -152,11 +152,22 @@ function buildPlan(cwd, editors) {
152
152
  cursor: [],
153
153
  windsurf: [],
154
154
  antigravity: [],
155
+ gemini: [], // #706 — added when --editor=gemini or --editor=all
156
+ vscode: [], // #706 — vscode marker dir cleanup (commands share .claude/)
155
157
  agentsMd: null, // null = no section; 'present' = section present
156
158
  stateDir: null, // null = missing; { files: N } = present
157
159
  planningDir: null, // null = missing; { files: N } = present
158
160
  };
159
161
 
162
+ // Issue #706: vscode and gemini are in SUPPORTED_IDES but uninstall.js had
163
+ // no branches for them. vscode shares .claude/ for commands+agents+skills
164
+ // — fold into the claude branch. gemini has its own .gemini/rihal/ tree.
165
+ if (editors.includes('vscode')) {
166
+ if (!editors.includes('claude')) editors.push('claude'); // share scan
167
+ const markerDir = path.join(cwd, '.vscode/rihal');
168
+ if (fs.existsSync(markerDir)) plan.vscode.push('.vscode/rihal');
169
+ }
170
+
160
171
  if (editors.includes('claude')) {
161
172
  const skillsDir = path.join(cwd, '.claude/skills');
162
173
  if (fs.existsSync(skillsDir)) {
@@ -212,6 +223,20 @@ function buildPlan(cwd, editors) {
212
223
  }
213
224
  }
214
225
 
226
+ if (editors.includes('gemini')) {
227
+ // #706 — gemini installs to .gemini/rihal/{agents,commands}
228
+ for (const sub of ['agents', 'commands']) {
229
+ const dir = path.join(cwd, '.gemini', 'rihal', sub);
230
+ if (fs.existsSync(dir)) {
231
+ for (const name of fs.readdirSync(dir)) {
232
+ if (name.startsWith('rihal-') || name.endsWith('.md')) {
233
+ plan.gemini.push(path.join('.gemini/rihal', sub, name));
234
+ }
235
+ }
236
+ }
237
+ }
238
+ }
239
+
215
240
  // Check AGENTS.md for Rihal section
216
241
  const agentsMdPath = path.join(cwd, 'AGENTS.md');
217
242
  if (fs.existsSync(agentsMdPath)) {
@@ -284,7 +309,23 @@ function planToPathList(plan, cwd, options = {}) {
284
309
  for (const name of plan.claude.skills) {
285
310
  paths.push(path.join('.claude/skills', name));
286
311
  }
287
- if (plan.claude.commands.length > 0) {
312
+ // Issue #704: claude IDE installs slash commands as flat
313
+ // .claude/commands/rihal-*.md files (post-#697 layout). The previous
314
+ // backup only added the legacy '.claude/commands/rihal' subdir, so on
315
+ // any modern claude install the tarball was missing every slash command.
316
+ // Add each flat file individually if present, plus the legacy subdir.
317
+ for (const name of plan.claude.commands) {
318
+ // plan.claude.commands holds entries from BOTH layouts:
319
+ // - 'rihal-foo.md' (claude flat)
320
+ // - 'foo.md' (vscode subdir)
321
+ // Disambiguate by the rihal- prefix.
322
+ if (name.startsWith('rihal-') && name.endsWith('.md')) {
323
+ paths.push(path.join('.claude/commands', name));
324
+ }
325
+ }
326
+ // Legacy vscode-style subdir is added once if any subdir entries exist.
327
+ const hasSubdirCommand = plan.claude.commands.some(n => !n.startsWith('rihal-'));
328
+ if (hasSubdirCommand) {
288
329
  paths.push('.claude/commands/rihal');
289
330
  }
290
331
  for (const name of plan.claude.agents) {
@@ -299,6 +340,13 @@ function planToPathList(plan, cwd, options = {}) {
299
340
  for (const name of plan.antigravity) {
300
341
  paths.push(path.join('.antigravity/agents', name));
301
342
  }
343
+ // #706 — gemini paths are already relative (built that way in buildPlan).
344
+ if (Array.isArray(plan.gemini)) {
345
+ for (const rel of plan.gemini) paths.push(rel);
346
+ }
347
+ if (Array.isArray(plan.vscode)) {
348
+ for (const rel of plan.vscode) paths.push(rel);
349
+ }
302
350
  // AGENTS.md is mutated (stripped), not deleted — but we back it up so the
303
351
  // user can restore the stripped content.
304
352
  if (plan.agentsMd && fs.existsSync(path.join(cwd, 'AGENTS.md'))) {
@@ -638,6 +686,30 @@ async function runUninstall(args) {
638
686
  if (n > 0) console.log(` ✓ removed ${n} Antigravity agents`);
639
687
  }
640
688
 
689
+ // #706 — gemini removal (.gemini/rihal/{agents,commands})
690
+ if (editors.includes('gemini')) {
691
+ let n = 0;
692
+ for (const sub of ['agents', 'commands']) {
693
+ const dir = path.join(cwd, '.gemini', 'rihal', sub);
694
+ n += removeMatching(dir, (name) => name.startsWith('rihal-') || name.endsWith('.md'));
695
+ }
696
+ removed += n;
697
+ if (n > 0) console.log(` ✓ removed ${n} Gemini files`);
698
+ }
699
+
700
+ // #706 — vscode marker dir cleanup. Commands+skills+agents share .claude/
701
+ // and were already removed under the claude branch.
702
+ if (editors.includes('vscode')) {
703
+ const markerDir = path.join(cwd, '.vscode/rihal');
704
+ if (fs.existsSync(markerDir)) {
705
+ const r = safeRmSync(markerDir, path.resolve(cwd));
706
+ if (r.ok) {
707
+ removed += 1;
708
+ console.log(` ✓ removed .vscode/rihal/ marker`);
709
+ }
710
+ }
711
+ }
712
+
641
713
  // Strip AGENTS.md section
642
714
  if (plan.agentsMd) {
643
715
  const agentsMdPath = path.join(cwd, 'AGENTS.md');
package/cli/update.js CHANGED
@@ -33,10 +33,51 @@ const path = require('path');
33
33
  const { spawnSync } = require('child_process');
34
34
  const clack = require('@clack/prompts');
35
35
  const { PromptAbortError } = require('./lib/prompts.cjs');
36
- const { writeJsonAtomic } = require('./lib/fsutil.cjs');
36
+ const { writeFileAtomic } = require('./lib/fsutil.cjs');
37
37
  const { verifyInstall, formatReport } = require('./lib/manifest.cjs');
38
38
  const install = require('./install');
39
39
 
40
+ /**
41
+ * Issue #701: update.js used to read .rihal/config.json with JSON.parse, but
42
+ * the installer writes .rihal/config.yaml. So `rcode update` errored on
43
+ * every real install. Read config.yaml with the same minimal parser the
44
+ * installer uses, and write it back as YAML preserving every other field.
45
+ */
46
+ function readConfigYaml(configPath) {
47
+ const text = fs.readFileSync(configPath, 'utf8');
48
+ const obj = {};
49
+ for (const raw of text.split('\n')) {
50
+ const line = raw.replace(/#.*$/, '').trimEnd();
51
+ if (!line) continue;
52
+ if (line.startsWith(' ')) continue; // ignore nested keys (rare; preserved on disk via raw text path)
53
+ const colonAt = line.indexOf(':');
54
+ if (colonAt === -1) continue;
55
+ const key = line.slice(0, colonAt).trim();
56
+ let val = line.slice(colonAt + 1).trim();
57
+ if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
58
+ if (val === 'true') val = true;
59
+ else if (val === 'false') val = false;
60
+ obj[key] = val;
61
+ }
62
+ return { obj, raw: text };
63
+ }
64
+
65
+ /**
66
+ * Surgically update a single key in a YAML config without rewriting the
67
+ * whole file (preserves comments, ordering, and nested keys we don't parse).
68
+ * If the key doesn't exist, append it.
69
+ */
70
+ function setYamlKey(rawText, key, value) {
71
+ const re = new RegExp(`^${key}:\\s*.*$`, 'm');
72
+ const replacement = typeof value === 'string'
73
+ ? `${key}: "${value.replace(/"/g, '\\"')}"`
74
+ : `${key}: ${value}`;
75
+ if (re.test(rawText)) {
76
+ return rawText.replace(re, replacement);
77
+ }
78
+ return rawText.replace(/\n*$/, '') + `\n${replacement}\n`;
79
+ }
80
+
40
81
  function parseArgs(args) {
41
82
  const opts = { yes: false };
42
83
  for (const arg of args) {
@@ -51,12 +92,29 @@ function parseArgs(args) {
51
92
  */
52
93
  function detectInstalledEditors(cwd) {
53
94
  const editors = [];
54
- if (fs.existsSync(path.join(cwd, '.claude/skills'))) {
55
- const hasRihal = fs
56
- .readdirSync(path.join(cwd, '.claude/skills'))
57
- .some((n) => n.startsWith('rihal-'));
58
- if (hasRihal) editors.push('claude');
59
- }
95
+
96
+ // Issue #701: post-#679 dedup leaves .claude/skills/ empty when ~/.claude/
97
+ // already has the rihal-* set. Detect "claude install" by looking at
98
+ // .rihal/config.yaml as the canonical signal — if config exists, the
99
+ // project ran rcode install at least once for claude. The presence of
100
+ // any commands/agents/skills then becomes secondary evidence.
101
+ const os = require('os');
102
+ const homeSkills = path.join(os.homedir(), '.claude/skills');
103
+ const projectClaude = (
104
+ (fs.existsSync(path.join(cwd, '.claude/skills')) &&
105
+ fs.readdirSync(path.join(cwd, '.claude/skills')).some(n => n.startsWith('rihal-'))) ||
106
+ (fs.existsSync(path.join(cwd, '.claude/agents')) &&
107
+ fs.readdirSync(path.join(cwd, '.claude/agents')).some(n => n.startsWith('rihal-'))) ||
108
+ (fs.existsSync(path.join(cwd, '.claude/commands')) &&
109
+ fs.readdirSync(path.join(cwd, '.claude/commands')).some(n => n.startsWith('rihal-')))
110
+ );
111
+ const globalClaude = (
112
+ fs.existsSync(homeSkills) &&
113
+ fs.readdirSync(homeSkills).some(n => n.startsWith('rihal-'))
114
+ );
115
+ const configIndicatesClaude = fs.existsSync(path.join(cwd, '.rihal/config.yaml'));
116
+ if (projectClaude || (configIndicatesClaude && globalClaude)) editors.push('claude');
117
+
60
118
  if (fs.existsSync(path.join(cwd, '.cursor/rules'))) {
61
119
  const hasRihal = fs
62
120
  .readdirSync(path.join(cwd, '.cursor/rules'))
@@ -241,7 +299,13 @@ async function runUpdate(args, { packageRoot, packageJson }) {
241
299
  const packageVersion = packageJson?.version || '0.0.0';
242
300
 
243
301
  // ------ Sanity: must be installed ------
244
- const configPath = path.join(cwd, '.rihal/config.json');
302
+ // Issue #701: installer writes .rihal/config.yaml, not config.json.
303
+ // Tolerate both shapes for users who upgrade across the rename window,
304
+ // but the canonical path is YAML.
305
+ const configYamlPath = path.join(cwd, '.rihal/config.yaml');
306
+ const configJsonPath = path.join(cwd, '.rihal/config.json');
307
+ const configPath = fs.existsSync(configYamlPath) ? configYamlPath : configJsonPath;
308
+
245
309
  if (!fs.existsSync(configPath)) {
246
310
  console.error(`\n❌ Rihal Code is not installed in this directory.`);
247
311
  console.error(` To install: rcode install\n`);
@@ -249,10 +313,18 @@ async function runUpdate(args, { packageRoot, packageJson }) {
249
313
  }
250
314
 
251
315
  let config;
316
+ let configRaw = '';
317
+ const isYaml = configPath.endsWith('.yaml');
252
318
  try {
253
- config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
319
+ if (isYaml) {
320
+ const parsed = readConfigYaml(configPath);
321
+ config = parsed.obj;
322
+ configRaw = parsed.raw;
323
+ } else {
324
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
325
+ }
254
326
  } catch (err) {
255
- console.error(`\n❌ .rihal/config.json is not valid JSON: ${err.message}\n`);
327
+ console.error(`\n❌ ${path.basename(configPath)} is not parseable: ${err.message}\n`);
256
328
  process.exit(1);
257
329
  }
258
330
 
@@ -356,10 +428,18 @@ async function runUpdate(args, { packageRoot, packageJson }) {
356
428
  console.log(` ✓ [${supportedIdes.join(', ')}] → refreshed via install.js`);
357
429
  }
358
430
 
359
- // ------ Update installed_version in config.json (atomic) ------
360
- config.installed_version = packageVersion;
361
- writeJsonAtomic(configPath, config);
362
- console.log(` ✓ .rihal/config.json → installed_version: ${packageVersion}`);
431
+ // ------ Update installed_version in config (atomic) ------
432
+ // Issue #701: write back in the same format we read. YAML uses surgical
433
+ // single-key replace so other fields, comments, ordering survive.
434
+ if (isYaml) {
435
+ const updated = setYamlKey(configRaw, 'installed_version', packageVersion);
436
+ writeFileAtomic(configPath, updated);
437
+ } else {
438
+ config.installed_version = packageVersion;
439
+ const { writeJsonAtomic } = require('./lib/fsutil.cjs');
440
+ writeJsonAtomic(configPath, config);
441
+ }
442
+ console.log(` ✓ ${path.relative(cwd, configPath)} → installed_version: ${packageVersion}`);
363
443
 
364
444
  // ------ Verify manifest ------
365
445
  console.log();
package/dist/rcode.js CHANGED
@@ -15073,7 +15073,8 @@ var require_manifest = __commonJS({
15073
15073
  extra
15074
15074
  };
15075
15075
  }
15076
- function verifyClaudeInstall(cwd, packageRoot) {
15076
+ function verifyClaudeInstall(cwd, packageRoot, options = {}) {
15077
+ const globalFallback = options.globalFallback !== false;
15077
15078
  const pkg = readPackageManifest(packageRoot);
15078
15079
  const agentsDir = path2.join(cwd, ".claude/agents");
15079
15080
  const skillsDir = path2.join(cwd, ".claude/skills");
@@ -15085,7 +15086,7 @@ var require_manifest = __commonJS({
15085
15086
  }
15086
15087
  }
15087
15088
  }
15088
- if (installedAgents.size === 0) {
15089
+ if (installedAgents.size === 0 && globalFallback) {
15089
15090
  try {
15090
15091
  const os = require("os");
15091
15092
  const globalAgentsDir = path2.join(os.homedir(), ".claude/agents");
@@ -15100,9 +15101,7 @@ var require_manifest = __commonJS({
15100
15101
  }
15101
15102
  }
15102
15103
  const allInstalled = readInstalledDirs(skillsDir);
15103
- const actionsInstalled = new Set(
15104
- [...allInstalled].filter((n) => !n.startsWith("rihal-"))
15105
- );
15104
+ const actionsInstalled = new Set([...allInstalled].filter((n) => pkg.actions.has(n)));
15106
15105
  return [
15107
15106
  diffSet("claude", "agents", pkg.agents, installedAgents),
15108
15107
  diffSet("claude", "actions", pkg.actions, actionsInstalled)
@@ -15654,12 +15653,23 @@ Run \`/rihal-new-project <description>\` to bootstrap, or \`/rihal-sprint-planni
15654
15653
  `
15655
15654
  );
15656
15655
  const rihalStateJson = path2.join(target, ".rihal", "state.json");
15656
+ function planningRoadmapIsStub() {
15657
+ const rmPath = path2.join(target, ".planning", "ROADMAP.md");
15658
+ if (!fs2.existsSync(rmPath)) return true;
15659
+ try {
15660
+ const text = fs2.readFileSync(rmPath, "utf8");
15661
+ return text.includes("<!-- INSTALL STUB");
15662
+ } catch {
15663
+ return true;
15664
+ }
15665
+ }
15657
15666
  if (!fs2.existsSync(rihalStateJson)) {
15658
15667
  const now = (/* @__PURE__ */ new Date()).toISOString();
15668
+ const isStubProject = planningRoadmapIsStub();
15659
15669
  const state = {
15660
15670
  version: "1",
15661
15671
  project: null,
15662
- _seeded_stub: true,
15672
+ ...isStubProject ? { _seeded_stub: true } : {},
15663
15673
  created: now,
15664
15674
  updated: now,
15665
15675
  current_phase: null,
@@ -16103,7 +16113,7 @@ ${BLOCK}`, { mode: 493 });
16103
16113
  }
16104
16114
  return rows.map((r) => r.join(",")).join("\n") + "\n";
16105
16115
  }
16106
- function generateFilesManifest(plan, target, { mergeExistingManifest = false } = {}) {
16116
+ function generateFilesManifest(plan, target, { mergeExistingManifest = false, extraScanDirs = [] } = {}) {
16107
16117
  const rows = [["rel", "sha256", "size"]];
16108
16118
  const newRels = /* @__PURE__ */ new Set();
16109
16119
  for (const entry of plan) {
@@ -16114,6 +16124,26 @@ ${BLOCK}`, { mode: 493 });
16114
16124
  rows.push([rel, sha256(buf), String(buf.length)]);
16115
16125
  newRels.add(rel);
16116
16126
  }
16127
+ function walkScanDir(absDir) {
16128
+ if (!fs2.existsSync(absDir)) return;
16129
+ for (const entry of fs2.readdirSync(absDir, { withFileTypes: true })) {
16130
+ const full = path2.join(absDir, entry.name);
16131
+ if (entry.isDirectory()) {
16132
+ walkScanDir(full);
16133
+ } else if (entry.isFile()) {
16134
+ const rel = path2.relative(target, full).split(path2.sep).join("/");
16135
+ if (newRels.has(rel)) continue;
16136
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) continue;
16137
+ try {
16138
+ const buf = fs2.readFileSync(full);
16139
+ rows.push([rel, sha256(buf), String(buf.length)]);
16140
+ newRels.add(rel);
16141
+ } catch {
16142
+ }
16143
+ }
16144
+ }
16145
+ }
16146
+ for (const scan of extraScanDirs) walkScanDir(scan);
16117
16147
  if (mergeExistingManifest) {
16118
16148
  const manifestPath = path2.join(target, ".rihal", "_config", "files-manifest.csv");
16119
16149
  if (fs2.existsSync(manifestPath)) {
@@ -16149,18 +16179,18 @@ ${BLOCK}`, { mode: 493 });
16149
16179
  const isLocalOverride = (rel) => /\.local\.(md|mdc|json|yaml|yml|toml|js|ts)$/.test(rel);
16150
16180
  let removed = 0;
16151
16181
  const emptyCandidateDirs = /* @__PURE__ */ new Set();
16182
+ const targetRoot = path2.resolve(target);
16152
16183
  for (const rel of oldRels) {
16153
16184
  if (newRelsSet.has(rel)) continue;
16154
16185
  if (neverSweep.test(rel)) continue;
16155
16186
  if (isLocalOverride(rel)) continue;
16187
+ if (rel.includes("..") || path2.isAbsolute(rel)) continue;
16156
16188
  const full = path2.join(target, rel);
16157
- try {
16158
- if (fs2.existsSync(full)) {
16159
- fs2.rmSync(full, { force: true });
16160
- emptyCandidateDirs.add(path2.dirname(full));
16161
- removed += 1;
16162
- }
16163
- } catch {
16189
+ if (!fs2.existsSync(full)) continue;
16190
+ const result = safeRmSync(full, targetRoot);
16191
+ if (result.ok) {
16192
+ emptyCandidateDirs.add(path2.dirname(full));
16193
+ removed += 1;
16164
16194
  }
16165
16195
  }
16166
16196
  const dirsSortedDeep = Array.from(emptyCandidateDirs).sort((a, b) => b.length - a.length);
@@ -16764,7 +16794,17 @@ commit_planning: ${desired}
16764
16794
  const stateSrc = path2.join(SOURCE_ROOT, "state.json");
16765
16795
  if (fs2.existsSync(stateSrc)) {
16766
16796
  const now = (/* @__PURE__ */ new Date()).toISOString();
16767
- const stateContent = fs2.readFileSync(stateSrc, "utf8").replace(/__PROJECT_NAME__/g, opts.projectName).replace(/__INSTALL_DATE__/g, now);
16797
+ let stateContent = fs2.readFileSync(stateSrc, "utf8").replace(/__PROJECT_NAME__/g, opts.projectName).replace(/__INSTALL_DATE__/g, now);
16798
+ const rmPath = path2.join(opts.target, ".planning", "ROADMAP.md");
16799
+ if (fs2.existsSync(rmPath)) {
16800
+ try {
16801
+ const rm = fs2.readFileSync(rmPath, "utf8");
16802
+ if (!rm.includes("<!-- INSTALL STUB")) {
16803
+ stateContent = stateContent.replace(/^\s*"_seeded_stub":\s*true,?\s*\n/m, "");
16804
+ }
16805
+ } catch {
16806
+ }
16807
+ }
16768
16808
  ensureDir(path2.dirname(stateDest));
16769
16809
  writeFileAtomic(stateDest, stateContent);
16770
16810
  }
@@ -16782,10 +16822,6 @@ commit_planning: ${desired}
16782
16822
  }
16783
16823
  const globalAgentsDir = path2.join(os.homedir(), ".rihal", "agents");
16784
16824
  ensureDir(globalAgentsDir);
16785
- fs2.writeFileSync(
16786
- path2.join(configDir, "files-manifest.csv"),
16787
- generateFilesManifest(plan, opts.target, { mergeExistingManifest: !opts.force })
16788
- );
16789
16825
  const skillsResult = installSkills(PACKAGE_ROOT2, opts.target, {
16790
16826
  skipGlobalDuplicates: isProjectInstall
16791
16827
  });
@@ -16809,6 +16845,16 @@ commit_planning: ${desired}
16809
16845
  } catch (err) {
16810
16846
  console.log(" " + dim(`(sidebar stub generation skipped: ${err.message})`));
16811
16847
  }
16848
+ fs2.writeFileSync(
16849
+ path2.join(configDir, "files-manifest.csv"),
16850
+ generateFilesManifest(plan, opts.target, {
16851
+ mergeExistingManifest: !opts.force,
16852
+ extraScanDirs: [
16853
+ path2.join(opts.target, ".claude", "skills"),
16854
+ path2.join(opts.target, ".rihal", "skills")
16855
+ ]
16856
+ })
16857
+ );
16812
16858
  const starterSeeded = seedStarterPlanning(opts.target, opts.projectName);
16813
16859
  installBrainScaffold(PACKAGE_ROOT2, opts.target);
16814
16860
  const gitignoreReport = ensureRcodeGitignore(opts.target, { commitPlanning: opts.commitPlanning });
@@ -16821,7 +16867,8 @@ commit_planning: ${desired}
16821
16867
  const out = execFileSync("node", [toolsPath, "brain", "pull"], {
16822
16868
  cwd: opts.target,
16823
16869
  encoding: "utf8",
16824
- stdio: ["ignore", "pipe", "pipe"]
16870
+ stdio: ["ignore", "pipe", "pipe"],
16871
+ timeout: 6e4
16825
16872
  });
16826
16873
  try {
16827
16874
  brainReport = JSON.parse(out);
@@ -17420,9 +17467,37 @@ var require_update = __commonJS({
17420
17467
  var { spawnSync } = require("child_process");
17421
17468
  var clack = require_dist3();
17422
17469
  var { PromptAbortError } = require_prompts();
17423
- var { writeJsonAtomic } = require_fsutil();
17470
+ var { writeFileAtomic } = require_fsutil();
17424
17471
  var { verifyInstall, formatReport } = require_manifest();
17425
17472
  var install = require_install();
17473
+ function readConfigYaml(configPath) {
17474
+ const text = fs2.readFileSync(configPath, "utf8");
17475
+ const obj = {};
17476
+ for (const raw of text.split("\n")) {
17477
+ const line = raw.replace(/#.*$/, "").trimEnd();
17478
+ if (!line) continue;
17479
+ if (line.startsWith(" ")) continue;
17480
+ const colonAt = line.indexOf(":");
17481
+ if (colonAt === -1) continue;
17482
+ const key = line.slice(0, colonAt).trim();
17483
+ let val = line.slice(colonAt + 1).trim();
17484
+ if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
17485
+ if (val === "true") val = true;
17486
+ else if (val === "false") val = false;
17487
+ obj[key] = val;
17488
+ }
17489
+ return { obj, raw: text };
17490
+ }
17491
+ function setYamlKey(rawText, key, value) {
17492
+ const re = new RegExp(`^${key}:\\s*.*$`, "m");
17493
+ const replacement = typeof value === "string" ? `${key}: "${value.replace(/"/g, '\\"')}"` : `${key}: ${value}`;
17494
+ if (re.test(rawText)) {
17495
+ return rawText.replace(re, replacement);
17496
+ }
17497
+ return rawText.replace(/\n*$/, "") + `
17498
+ ${replacement}
17499
+ `;
17500
+ }
17426
17501
  function parseArgs(args) {
17427
17502
  const opts = { yes: false };
17428
17503
  for (const arg of args) {
@@ -17432,10 +17507,12 @@ var require_update = __commonJS({
17432
17507
  }
17433
17508
  function detectInstalledEditors(cwd) {
17434
17509
  const editors = [];
17435
- if (fs2.existsSync(path2.join(cwd, ".claude/skills"))) {
17436
- const hasRihal = fs2.readdirSync(path2.join(cwd, ".claude/skills")).some((n) => n.startsWith("rihal-"));
17437
- if (hasRihal) editors.push("claude");
17438
- }
17510
+ const os = require("os");
17511
+ const homeSkills = path2.join(os.homedir(), ".claude/skills");
17512
+ const projectClaude = fs2.existsSync(path2.join(cwd, ".claude/skills")) && fs2.readdirSync(path2.join(cwd, ".claude/skills")).some((n) => n.startsWith("rihal-")) || fs2.existsSync(path2.join(cwd, ".claude/agents")) && fs2.readdirSync(path2.join(cwd, ".claude/agents")).some((n) => n.startsWith("rihal-")) || fs2.existsSync(path2.join(cwd, ".claude/commands")) && fs2.readdirSync(path2.join(cwd, ".claude/commands")).some((n) => n.startsWith("rihal-"));
17513
+ const globalClaude = fs2.existsSync(homeSkills) && fs2.readdirSync(homeSkills).some((n) => n.startsWith("rihal-"));
17514
+ const configIndicatesClaude = fs2.existsSync(path2.join(cwd, ".rihal/config.yaml"));
17515
+ if (projectClaude || configIndicatesClaude && globalClaude) editors.push("claude");
17439
17516
  if (fs2.existsSync(path2.join(cwd, ".cursor/rules"))) {
17440
17517
  const hasRihal = fs2.readdirSync(path2.join(cwd, ".cursor/rules")).some((n) => n.startsWith("rihal-") && n.endsWith(".mdc"));
17441
17518
  if (hasRihal) editors.push("cursor");
@@ -17561,8 +17638,8 @@ var require_update = __commonJS({
17561
17638
  }
17562
17639
  if (changed) {
17563
17640
  content = content.replace(/\n---\n+$/, "\n");
17564
- const { writeFileAtomic } = require_fsutil();
17565
- writeFileAtomic(agentsMdPath, content);
17641
+ const { writeFileAtomic: writeFileAtomic2 } = require_fsutil();
17642
+ writeFileAtomic2(agentsMdPath, content);
17566
17643
  }
17567
17644
  }
17568
17645
  module2.exports = async function update(args, { packageRoot, packageJson }) {
@@ -17581,7 +17658,9 @@ var require_update = __commonJS({
17581
17658
  const cwd = process.cwd();
17582
17659
  const opts = parseArgs(args);
17583
17660
  const packageVersion = packageJson?.version || "0.0.0";
17584
- const configPath = path2.join(cwd, ".rihal/config.json");
17661
+ const configYamlPath = path2.join(cwd, ".rihal/config.yaml");
17662
+ const configJsonPath = path2.join(cwd, ".rihal/config.json");
17663
+ const configPath = fs2.existsSync(configYamlPath) ? configYamlPath : configJsonPath;
17585
17664
  if (!fs2.existsSync(configPath)) {
17586
17665
  console.error(`
17587
17666
  \u274C Rihal Code is not installed in this directory.`);
@@ -17590,11 +17669,19 @@ var require_update = __commonJS({
17590
17669
  process.exit(1);
17591
17670
  }
17592
17671
  let config;
17672
+ let configRaw = "";
17673
+ const isYaml = configPath.endsWith(".yaml");
17593
17674
  try {
17594
- config = JSON.parse(fs2.readFileSync(configPath, "utf8"));
17675
+ if (isYaml) {
17676
+ const parsed = readConfigYaml(configPath);
17677
+ config = parsed.obj;
17678
+ configRaw = parsed.raw;
17679
+ } else {
17680
+ config = JSON.parse(fs2.readFileSync(configPath, "utf8"));
17681
+ }
17595
17682
  } catch (err) {
17596
17683
  console.error(`
17597
- \u274C .rihal/config.json is not valid JSON: ${err.message}
17684
+ \u274C ${path2.basename(configPath)} is not parseable: ${err.message}
17598
17685
  `);
17599
17686
  process.exit(1);
17600
17687
  }
@@ -17682,9 +17769,15 @@ var require_update = __commonJS({
17682
17769
  });
17683
17770
  console.log(` \u2713 [${supportedIdes.join(", ")}] \u2192 refreshed via install.js`);
17684
17771
  }
17685
- config.installed_version = packageVersion;
17686
- writeJsonAtomic(configPath, config);
17687
- console.log(` \u2713 .rihal/config.json \u2192 installed_version: ${packageVersion}`);
17772
+ if (isYaml) {
17773
+ const updated = setYamlKey(configRaw, "installed_version", packageVersion);
17774
+ writeFileAtomic(configPath, updated);
17775
+ } else {
17776
+ config.installed_version = packageVersion;
17777
+ const { writeJsonAtomic } = require_fsutil();
17778
+ writeJsonAtomic(configPath, config);
17779
+ }
17780
+ console.log(` \u2713 ${path2.relative(cwd, configPath)} \u2192 installed_version: ${packageVersion}`);
17688
17781
  console.log();
17689
17782
  const { reports, hasDrift } = verifyInstall(cwd, packageRoot, editors);
17690
17783
  if (hasDrift) {
@@ -17791,6 +17884,10 @@ var require_uninstall = __commonJS({
17791
17884
  cursor: [],
17792
17885
  windsurf: [],
17793
17886
  antigravity: [],
17887
+ gemini: [],
17888
+ // #706 — added when --editor=gemini or --editor=all
17889
+ vscode: [],
17890
+ // #706 — vscode marker dir cleanup (commands share .claude/)
17794
17891
  agentsMd: null,
17795
17892
  // null = no section; 'present' = section present
17796
17893
  stateDir: null,
@@ -17798,6 +17895,11 @@ var require_uninstall = __commonJS({
17798
17895
  planningDir: null
17799
17896
  // null = missing; { files: N } = present
17800
17897
  };
17898
+ if (editors.includes("vscode")) {
17899
+ if (!editors.includes("claude")) editors.push("claude");
17900
+ const markerDir = path2.join(cwd, ".vscode/rihal");
17901
+ if (fs2.existsSync(markerDir)) plan.vscode.push(".vscode/rihal");
17902
+ }
17801
17903
  if (editors.includes("claude")) {
17802
17904
  const skillsDir = path2.join(cwd, ".claude/skills");
17803
17905
  if (fs2.existsSync(skillsDir)) {
@@ -17835,6 +17937,18 @@ var require_uninstall = __commonJS({
17835
17937
  plan.antigravity = fs2.readdirSync(agDir).filter((name) => name.startsWith("rihal-"));
17836
17938
  }
17837
17939
  }
17940
+ if (editors.includes("gemini")) {
17941
+ for (const sub of ["agents", "commands"]) {
17942
+ const dir = path2.join(cwd, ".gemini", "rihal", sub);
17943
+ if (fs2.existsSync(dir)) {
17944
+ for (const name of fs2.readdirSync(dir)) {
17945
+ if (name.startsWith("rihal-") || name.endsWith(".md")) {
17946
+ plan.gemini.push(path2.join(".gemini/rihal", sub, name));
17947
+ }
17948
+ }
17949
+ }
17950
+ }
17951
+ }
17838
17952
  const agentsMdPath = path2.join(cwd, "AGENTS.md");
17839
17953
  if (fs2.existsSync(agentsMdPath)) {
17840
17954
  const content = fs2.readFileSync(agentsMdPath, "utf8");
@@ -17881,7 +17995,13 @@ var require_uninstall = __commonJS({
17881
17995
  for (const name of plan.claude.skills) {
17882
17996
  paths.push(path2.join(".claude/skills", name));
17883
17997
  }
17884
- if (plan.claude.commands.length > 0) {
17998
+ for (const name of plan.claude.commands) {
17999
+ if (name.startsWith("rihal-") && name.endsWith(".md")) {
18000
+ paths.push(path2.join(".claude/commands", name));
18001
+ }
18002
+ }
18003
+ const hasSubdirCommand = plan.claude.commands.some((n) => !n.startsWith("rihal-"));
18004
+ if (hasSubdirCommand) {
17885
18005
  paths.push(".claude/commands/rihal");
17886
18006
  }
17887
18007
  for (const name of plan.claude.agents) {
@@ -17896,6 +18016,12 @@ var require_uninstall = __commonJS({
17896
18016
  for (const name of plan.antigravity) {
17897
18017
  paths.push(path2.join(".antigravity/agents", name));
17898
18018
  }
18019
+ if (Array.isArray(plan.gemini)) {
18020
+ for (const rel of plan.gemini) paths.push(rel);
18021
+ }
18022
+ if (Array.isArray(plan.vscode)) {
18023
+ for (const rel of plan.vscode) paths.push(rel);
18024
+ }
17899
18025
  if (plan.agentsMd && fs2.existsSync(path2.join(cwd, "AGENTS.md"))) {
17900
18026
  paths.push("AGENTS.md");
17901
18027
  }
@@ -18150,6 +18276,25 @@ var require_uninstall = __commonJS({
18150
18276
  removed += n;
18151
18277
  if (n > 0) console.log(` \u2713 removed ${n} Antigravity agents`);
18152
18278
  }
18279
+ if (editors.includes("gemini")) {
18280
+ let n = 0;
18281
+ for (const sub of ["agents", "commands"]) {
18282
+ const dir = path2.join(cwd, ".gemini", "rihal", sub);
18283
+ n += removeMatching(dir, (name) => name.startsWith("rihal-") || name.endsWith(".md"));
18284
+ }
18285
+ removed += n;
18286
+ if (n > 0) console.log(` \u2713 removed ${n} Gemini files`);
18287
+ }
18288
+ if (editors.includes("vscode")) {
18289
+ const markerDir = path2.join(cwd, ".vscode/rihal");
18290
+ if (fs2.existsSync(markerDir)) {
18291
+ const r = safeRmSync(markerDir, path2.resolve(cwd));
18292
+ if (r.ok) {
18293
+ removed += 1;
18294
+ console.log(` \u2713 removed .vscode/rihal/ marker`);
18295
+ }
18296
+ }
18297
+ }
18153
18298
  if (plan.agentsMd) {
18154
18299
  const agentsMdPath = path2.join(cwd, "AGENTS.md");
18155
18300
  const stripped = stripRihalFromAgentsMd(agentsMdPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzlaa/rcode",
3
- "version": "3.4.27",
3
+ "version": "3.4.29",
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": {