@hanzlaa/rcode 3.4.25 → 3.4.27

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/install.js CHANGED
@@ -57,7 +57,7 @@ const os = require('os');
57
57
 
58
58
  // Atomic write helper (#687) + symlink-safe rmSync (#688) — protect against
59
59
  // Ctrl+C mid-write and malicious symlink-traversal during dedup/cleanup.
60
- const { writeFileAtomic, safeRmSync } = require(path.join(__dirname, 'lib', 'fsutil.cjs'));
60
+ const { writeFileAtomic, safeRmSync } = require('./lib/fsutil.cjs');
61
61
 
62
62
  // Bundled packages — devDeps inlined by esbuild, loaded from node_modules in dev.
63
63
  const pc = require('picocolors');
@@ -79,6 +79,18 @@ const bold = (s) => pc.bold(s);
79
79
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
80
80
  const SOURCE_ROOT = path.join(PACKAGE_ROOT, 'rihal');
81
81
 
82
+ /**
83
+ * Single source of truth for supported IDEs (#697 — W4.3).
84
+ *
85
+ * Order matters: this is the order used in detection, prompts, and error
86
+ * messages. Anywhere code used to inline `['claude','cursor','gemini',
87
+ * 'vscode','antigravity']` it now references this constant. Adding a new
88
+ * IDE is now: append here, add a case to getPathsForIde, add a signal to
89
+ * detectIdeSignals, plus a row to runInstallWizard's multiselect — three
90
+ * sites instead of ten.
91
+ */
92
+ const SUPPORTED_IDES = Object.freeze(['claude', 'cursor', 'gemini', 'vscode', 'antigravity']);
93
+
82
94
  // Zod schema for .rihal/config.yaml validation (#250).
83
95
  const ConfigSchema = z.object({
84
96
  user_name: z.string().min(1),
@@ -338,12 +350,16 @@ async function resolveIde(opts) {
338
350
  // nothing detected. (Note: opts.ide defaults to 'claude' from parseArgs,
339
351
  // so check opts.ideProvided not opts.ide to honor real --ide overrides.)
340
352
  const signals = detectIdeSignals(opts.target);
341
- const detected = ['claude', 'cursor', 'gemini', 'vscode', 'antigravity'].filter(k => signals[k]);
353
+ const detected = SUPPORTED_IDES.filter(k => signals[k]);
342
354
  return detected.length > 0 ? detected : ['claude'];
343
355
  }
344
356
 
345
357
  const signals = detectIdeSignals(opts.target);
346
- const detected = ['claude', 'cursor', 'gemini', 'vscode'].filter(k => signals[k]);
358
+ // Antigravity is intentionally excluded from the interactive auto-detect
359
+ // because it's experimental and we don't want to opt-in users without
360
+ // explicit consent. Use SUPPORTED_IDES.filter(k => k !== 'antigravity')
361
+ // to keep the inclusion criteria self-documenting.
362
+ const detected = SUPPORTED_IDES.filter(k => k !== 'antigravity' && signals[k]);
347
363
 
348
364
  // Pre-select detected IDEs, or default to claude
349
365
  const initialValues = detected.length > 0 ? detected : ['claude'];
@@ -1613,7 +1629,7 @@ async function installInner(opts) {
1613
1629
  }
1614
1630
 
1615
1631
  // Validate IDE(s) — structured error for unsupported editors (#197).
1616
- const SUPPORTED_IDES = ['claude', 'cursor', 'gemini', 'vscode', 'antigravity'];
1632
+ // SUPPORTED_IDES is the module-level constant (#697 / W4.3).
1617
1633
  const unsupported = opts.ides.filter(ide => !SUPPORTED_IDES.includes(ide));
1618
1634
  if (unsupported.length > 0) {
1619
1635
  console.error(`✖ --ide ${unsupported.join(', ')} is not supported in v${readPackageVersion()}.`);
@@ -2359,7 +2375,7 @@ function runInstallHealthCheck(target, counts) {
2359
2375
  // in cli/lib/manifest.cjs already does this; we mirror its result here.
2360
2376
  let expected = { agents: 20, skills: 20, commands: 20 };
2361
2377
  try {
2362
- const { readPackageManifest } = require(path.join(__dirname, 'lib', 'manifest.cjs'));
2378
+ const { readPackageManifest } = require('./lib/manifest.cjs');
2363
2379
  const pkgManifest = readPackageManifest(PACKAGE_ROOT);
2364
2380
  if (pkgManifest && pkgManifest.agents instanceof Set && pkgManifest.actions instanceof Set) {
2365
2381
  // Tolerate ~10% loss vs source — global precedence, .local.md
@@ -2597,3 +2613,4 @@ module.exports = runFromCli;
2597
2613
  module.exports.parseArgs = parseArgs;
2598
2614
  module.exports.buildInstallPlan = buildInstallPlan;
2599
2615
  module.exports.install = install;
2616
+ module.exports.SUPPORTED_IDES = SUPPORTED_IDES;
@@ -13,42 +13,52 @@
13
13
  const os = require('os');
14
14
  const path = require('path');
15
15
 
16
- // Skip in CI or test environments
17
- if (process.env.CI || process.env.NODE_ENV === 'test') {
18
- process.exit(0);
19
- }
20
-
21
- // Only auto-install when invoked as a global package (npm install -g).
22
- // A local devDependency install should not touch the user's ~/.claude/.
23
- const isGlobalInstall = (() => {
16
+ /**
17
+ * Decide whether the current postinstall invocation represents a GLOBAL
18
+ * `npm install -g @hanzlaa/rcode` (true) or a transitive devDep install
19
+ * inside someone's project (false).
20
+ *
21
+ * Pure function takes its inputs explicitly so tests can drive every
22
+ * branch without needing to mutate process.env / __dirname / process.cwd.
23
+ *
24
+ * @param {object} env process.env-like
25
+ * @param {string} dirname __dirname of the postinstall script
26
+ * @param {string} cwd process.cwd() at invocation time
27
+ */
28
+ function isGlobalInstall(env, dirname, cwd) {
24
29
  try {
25
- // npm sets npm_config_global=true for global installs
26
- if (process.env.npm_config_global === 'true') return true;
27
- // pnpm sets npm_config_global too, but check PNPM_HOME as a fallback
28
- if (process.env.PNPM_HOME && __dirname.startsWith(process.env.PNPM_HOME)) return true;
29
- // Check if __dirname is inside a known global node_modules path.
30
- // Covers: /usr/local/lib, /usr/lib, ~/.nvm/.../lib, ~/.pnpm/..., ~/.yarn/...
30
+ if (env.npm_config_global === 'true') return true;
31
+ if (env.PNPM_HOME && dirname.startsWith(env.PNPM_HOME)) return true;
31
32
  const globalPatterns = [
32
- /\/node_modules\/@hanzlaa\/rcode/, // any global node_modules
33
- /[/\\]lib[/\\]node_modules[/\\]/, // /usr/local/lib/node_modules
34
- /\.nvm[/\\]versions[/\\]/, // nvm
35
- /\.pnpm[/\\]/, // pnpm global store
36
- /\.yarn[/\\]global/, // yarn global
33
+ /\/node_modules\/@hanzlaa\/rcode/,
34
+ /[/\\]lib[/\\]node_modules[/\\]/,
35
+ /\.nvm[/\\]versions[/\\]/,
36
+ /\.pnpm[/\\]/,
37
+ /\.yarn[/\\]global/,
37
38
  ];
38
- if (globalPatterns.some((re) => re.test(__dirname))) return true;
39
- // Last resort: package is NOT inside a project's local node_modules
40
- // (local installs have .../project/node_modules/@hanzlaa/rcode/cli)
41
- const localNodeModules = path.join(process.cwd(), 'node_modules');
42
- if (!__dirname.startsWith(localNodeModules)) return true;
39
+ if (globalPatterns.some((re) => re.test(dirname))) return true;
40
+ const localNodeModules = path.join(cwd, 'node_modules');
41
+ if (!dirname.startsWith(localNodeModules)) return true;
43
42
  return false;
44
43
  } catch {
45
44
  return false;
46
45
  }
47
- })();
46
+ }
47
+
48
+ // Skip in CI or test environments. Tests that import this module bypass
49
+ // the top-level effect by checking require.main !== module.
50
+ if (require.main === module) {
51
+ if (process.env.CI || process.env.NODE_ENV === 'test') {
52
+ process.exit(0);
53
+ }
54
+ runPostInstall();
55
+ }
56
+
57
+ function runPostInstall() {
48
58
 
49
59
  const globalTarget = path.join(os.homedir(), '.claude');
50
60
 
51
- if (isGlobalInstall) {
61
+ if (isGlobalInstall(process.env, __dirname, process.cwd())) {
52
62
  // Spawn dist/rcode.js (fully bundled — no devDep requires) to do the global
53
63
  // install. Calling cli/install.js directly fails in global npm installs because
54
64
  // devDependencies (picocolors, semver, etc.) are not installed for global packages.
@@ -101,3 +111,7 @@ More:
101
111
  Docs: https://github.com/hanzlahabib/rihal-code
102
112
  `);
103
113
  }
114
+
115
+ } // end runPostInstall
116
+
117
+ module.exports = { isGlobalInstall };
package/cli/uninstall.js CHANGED
@@ -68,6 +68,25 @@ function isLocalOverride(name) {
68
68
  return /\.local\.(md|mdc|json|yaml|yml|toml|js|ts)$/.test(name);
69
69
  }
70
70
 
71
+ /**
72
+ * Strip the rcode-managed block from a .gitignore string.
73
+ *
74
+ * Pure function (no fs) so it can be unit-tested independently. Issue #684
75
+ * fixed the over-broad legacy regex; this helper centralises the logic so
76
+ * any future shape change has exactly one site to update.
77
+ *
78
+ * Both supported shapes require BOTH the opener AND the closer to match —
79
+ * user comments starting with "# rcode" are safe.
80
+ */
81
+ function stripRihalGitignoreBlock(text) {
82
+ return text
83
+ // Current shape (install.js BEGIN/END markers — exact match).
84
+ .replace(/\n?# ===== rcode-managed gitignore block[\s\S]*?# ===== end rcode-managed gitignore block =====\n?/g, '\n')
85
+ // Legacy >>> / <<< fenced shape.
86
+ .replace(/\n?# >>> rihal-code >>>[\s\S]*?# <<< rihal-code <<<\n?/g, '\n')
87
+ .replace(/\n{3,}/g, '\n\n');
88
+ }
89
+
71
90
  /**
72
91
  * Walk a directory and remove all files/subdirs whose name matches a predicate.
73
92
  * Returns the number of entries removed. Always skips local overrides (#382).
@@ -230,7 +249,7 @@ function buildPlan(cwd, editors) {
230
249
  */
231
250
  function discoverKnownActionSkills() {
232
251
  try {
233
- const { readPackageManifest } = require(path.join(__dirname, 'lib', 'manifest.cjs'));
252
+ const { readPackageManifest } = require('./lib/manifest.cjs');
234
253
  const packageRoot = path.resolve(__dirname, '..');
235
254
  const pkg = readPackageManifest(packageRoot);
236
255
  if (pkg && pkg.actions instanceof Set && pkg.actions.size > 0) {
@@ -419,15 +438,13 @@ async function runUninstall(args) {
419
438
  const opts = parseArgs(args);
420
439
  const cwd = process.cwd();
421
440
 
422
- // Issue #693: keep the IDE list in sync with the installer. The installer
423
- // ships claude/cursor/gemini/vscode/antigravity. The previous uninstaller
424
- // list (claude/cursor/windsurf/antigravity) was missing gemini + vscode
425
- // and included windsurf (which the installer never writes). Result: a
426
- // user with vscode-style commands could never `rcode uninstall`.
427
- const SUPPORTED_EDITORS = ['claude', 'cursor', 'gemini', 'vscode', 'antigravity'];
441
+ // Issue #693 + #697 (W4.3): keep the IDE list in sync with the installer
442
+ // by importing the single source of truth. Adding an IDE to install.js
443
+ // SUPPORTED_IDES is now the only edit needed for parity.
444
+ const { SUPPORTED_IDES } = require('./install.js');
428
445
  const editors = opts.editor
429
- ? (opts.editor === 'all' ? SUPPORTED_EDITORS : [opts.editor])
430
- : SUPPORTED_EDITORS;
446
+ ? (opts.editor === 'all' ? Array.from(SUPPORTED_IDES) : [opts.editor])
447
+ : Array.from(SUPPORTED_IDES);
431
448
 
432
449
  console.log(`\n🕌 Rihal Code — Uninstall\n`);
433
450
  console.log(` Project: ${cwd}`);
@@ -731,12 +748,7 @@ async function runUninstall(args) {
731
748
  if (fs.existsSync(gitignorePath)) {
732
749
  try {
733
750
  const before = fs.readFileSync(gitignorePath, 'utf8');
734
- const stripped = before
735
- // Current shape (install.js BEGIN/END markers — exact match).
736
- .replace(/\n?# ===== rcode-managed gitignore block[\s\S]*?# ===== end rcode-managed gitignore block =====\n?/g, '\n')
737
- // Legacy >>> / <<< fenced shape.
738
- .replace(/\n?# >>> rihal-code >>>[\s\S]*?# <<< rihal-code <<<\n?/g, '\n')
739
- .replace(/\n{3,}/g, '\n\n');
751
+ const stripped = stripRihalGitignoreBlock(before);
740
752
  if (stripped !== before) {
741
753
  fs.writeFileSync(gitignorePath, stripped);
742
754
  console.log(` ✓ stripped rcode block from .gitignore (--purge)`);
@@ -773,6 +785,14 @@ async function runUninstall(args) {
773
785
  console.log(` rcode install`);
774
786
  }
775
787
 
788
+ // Re-exports for unit tests (W3.2 — issue #694 follow-up). The default
789
+ // export remains the async runner; these are attached afterwards so pure
790
+ // functions can be exercised without spawning a child process.
791
+ module.exports.isLocalOverride = isLocalOverride;
792
+ module.exports.planToPathList = planToPathList;
793
+ module.exports.discoverKnownActionSkills = discoverKnownActionSkills;
794
+ module.exports.stripRihalGitignoreBlock = stripRihalGitignoreBlock;
795
+
776
796
  // Direct invocation — allow `node cli/uninstall.js [flags]` to run end-to-end.
777
797
  // When called via cli/index.js, module.exports is invoked directly.
778
798
  if (require.main === module) {
package/cli/update.js CHANGED
@@ -383,3 +383,7 @@ async function runUpdate(args, { packageRoot, packageJson }) {
383
383
  }
384
384
  console.log();
385
385
  }
386
+
387
+ // Re-exports for unit tests (W3.4 — issue #694 follow-up).
388
+ module.exports.parseArgs = parseArgs;
389
+ module.exports.detectInstalledEditors = detectInstalledEditors;
package/dist/rcode.js CHANGED
@@ -5,6 +5,88 @@ var __commonJS = (cb, mod) => function __require() {
5
5
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
6
6
  };
7
7
 
8
+ // cli/lib/fsutil.cjs
9
+ var require_fsutil = __commonJS({
10
+ "cli/lib/fsutil.cjs"(exports2, module2) {
11
+ var crypto = require("crypto");
12
+ var fs2 = require("fs");
13
+ var path2 = require("path");
14
+ function writeFileAtomic(filePath, content, opts = {}) {
15
+ const { encoding = "utf8", mode } = opts;
16
+ const dir = path2.dirname(filePath);
17
+ fs2.mkdirSync(dir, { recursive: true });
18
+ const tmpPath = path2.join(
19
+ dir,
20
+ `.${path2.basename(filePath)}.tmp-${process.pid}-${crypto.randomBytes(8).toString("hex")}`
21
+ );
22
+ let fd;
23
+ try {
24
+ fd = fs2.openSync(tmpPath, "wx", mode ?? 420);
25
+ fs2.writeSync(fd, content, 0, encoding);
26
+ fs2.fsyncSync(fd);
27
+ fs2.closeSync(fd);
28
+ fd = null;
29
+ fs2.renameSync(tmpPath, filePath);
30
+ } catch (err) {
31
+ if (fd !== null && fd !== void 0) {
32
+ try {
33
+ fs2.closeSync(fd);
34
+ } catch {
35
+ }
36
+ }
37
+ try {
38
+ fs2.unlinkSync(tmpPath);
39
+ } catch {
40
+ }
41
+ throw err;
42
+ }
43
+ }
44
+ function writeJsonAtomic(filePath, obj, opts = {}) {
45
+ const content = JSON.stringify(obj, null, 2) + "\n";
46
+ writeFileAtomic(filePath, content, opts);
47
+ }
48
+ function safeRmSync(targetPath, projectRoot) {
49
+ let stats;
50
+ try {
51
+ stats = fs2.lstatSync(targetPath);
52
+ } catch (err) {
53
+ if (err.code === "ENOENT") return { ok: true, reason: "missing" };
54
+ return { ok: false, reason: `lstat: ${err.message}` };
55
+ }
56
+ if (stats.isSymbolicLink()) {
57
+ try {
58
+ fs2.unlinkSync(targetPath);
59
+ return { ok: true, reason: "symlink-unlinked" };
60
+ } catch (err) {
61
+ return { ok: false, reason: `unlink: ${err.message}` };
62
+ }
63
+ }
64
+ const root = path2.resolve(projectRoot);
65
+ let resolved;
66
+ try {
67
+ resolved = fs2.realpathSync(targetPath);
68
+ } catch (err) {
69
+ return { ok: false, reason: `realpath: ${err.message}` };
70
+ }
71
+ const relative = path2.relative(root, resolved);
72
+ if (relative.startsWith("..") || path2.isAbsolute(relative)) {
73
+ return { ok: false, reason: "outside-root" };
74
+ }
75
+ try {
76
+ fs2.rmSync(resolved, { recursive: true, force: true });
77
+ return { ok: true };
78
+ } catch (err) {
79
+ return { ok: false, reason: `rmSync: ${err.message}` };
80
+ }
81
+ }
82
+ module2.exports = {
83
+ writeFileAtomic,
84
+ writeJsonAtomic,
85
+ safeRmSync
86
+ };
87
+ }
88
+ });
89
+
8
90
  // node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js
9
91
  var require_picocolors = __commonJS({
10
92
  "node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js"(exports2, module2) {
@@ -14939,6 +15021,180 @@ ${e__default.gray(d)} ${t}
14939
15021
  }
14940
15022
  });
14941
15023
 
15024
+ // cli/lib/manifest.cjs
15025
+ var require_manifest = __commonJS({
15026
+ "cli/lib/manifest.cjs"(exports2, module2) {
15027
+ var fs2 = require("fs");
15028
+ var path2 = require("path");
15029
+ function readPackageManifest(packageRoot) {
15030
+ const skillsRoot = path2.join(packageRoot, "rihal/skills");
15031
+ const manifest = { agents: /* @__PURE__ */ new Set(), actions: /* @__PURE__ */ new Set() };
15032
+ const agentsDir = path2.join(skillsRoot, "agents");
15033
+ if (fs2.existsSync(agentsDir)) {
15034
+ for (const entry of fs2.readdirSync(agentsDir, { withFileTypes: true })) {
15035
+ if (entry.isDirectory()) manifest.agents.add(entry.name);
15036
+ }
15037
+ }
15038
+ function walkActions(dir) {
15039
+ if (!fs2.existsSync(dir)) return;
15040
+ for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
15041
+ if (!entry.isDirectory()) continue;
15042
+ const full = path2.join(dir, entry.name);
15043
+ if (fs2.existsSync(path2.join(full, "SKILL.md"))) {
15044
+ const installedName = entry.name.startsWith("rihal-") ? entry.name : `rihal-${entry.name}`;
15045
+ manifest.actions.add(installedName);
15046
+ } else {
15047
+ walkActions(full);
15048
+ }
15049
+ }
15050
+ }
15051
+ walkActions(path2.join(skillsRoot, "actions"));
15052
+ return manifest;
15053
+ }
15054
+ function readInstalledDirs(dir, prefix = null) {
15055
+ if (!fs2.existsSync(dir)) return /* @__PURE__ */ new Set();
15056
+ const names = /* @__PURE__ */ new Set();
15057
+ for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
15058
+ if (!entry.isDirectory()) continue;
15059
+ if (prefix && !entry.name.startsWith(prefix)) continue;
15060
+ names.add(prefix ? entry.name.slice(prefix.length) : entry.name);
15061
+ }
15062
+ return names;
15063
+ }
15064
+ function diffSet(editor, kind, expected, installed) {
15065
+ const missing = [...expected].filter((x) => !installed.has(x)).sort();
15066
+ const extra = [...installed].filter((x) => !expected.has(x)).sort();
15067
+ return {
15068
+ editor,
15069
+ kind,
15070
+ expectedCount: expected.size,
15071
+ installedCount: installed.size,
15072
+ missing,
15073
+ extra
15074
+ };
15075
+ }
15076
+ function verifyClaudeInstall(cwd, packageRoot) {
15077
+ const pkg = readPackageManifest(packageRoot);
15078
+ const agentsDir = path2.join(cwd, ".claude/agents");
15079
+ const skillsDir = path2.join(cwd, ".claude/skills");
15080
+ const installedAgents = /* @__PURE__ */ new Set();
15081
+ if (fs2.existsSync(agentsDir)) {
15082
+ for (const f of fs2.readdirSync(agentsDir)) {
15083
+ if (f.startsWith("rihal-") && f.endsWith(".md")) {
15084
+ installedAgents.add(f.replace(/^rihal-/, "").replace(/\.md$/, ""));
15085
+ }
15086
+ }
15087
+ }
15088
+ if (installedAgents.size === 0) {
15089
+ try {
15090
+ const os = require("os");
15091
+ const globalAgentsDir = path2.join(os.homedir(), ".claude/agents");
15092
+ if (fs2.existsSync(globalAgentsDir)) {
15093
+ for (const f of fs2.readdirSync(globalAgentsDir)) {
15094
+ if (f.startsWith("rihal-") && f.endsWith(".md")) {
15095
+ installedAgents.add(f.replace(/^rihal-/, "").replace(/\.md$/, ""));
15096
+ }
15097
+ }
15098
+ }
15099
+ } catch {
15100
+ }
15101
+ }
15102
+ const allInstalled = readInstalledDirs(skillsDir);
15103
+ const actionsInstalled = new Set(
15104
+ [...allInstalled].filter((n) => !n.startsWith("rihal-"))
15105
+ );
15106
+ return [
15107
+ diffSet("claude", "agents", pkg.agents, installedAgents),
15108
+ diffSet("claude", "actions", pkg.actions, actionsInstalled)
15109
+ ];
15110
+ }
15111
+ function verifyRulesInstall(editor, cwd, packageRoot) {
15112
+ const pkg = readPackageManifest(packageRoot);
15113
+ const rulesDir = path2.join(
15114
+ cwd,
15115
+ editor === "cursor" ? ".cursor/rules" : ".windsurf/rules"
15116
+ );
15117
+ const installed = /* @__PURE__ */ new Set();
15118
+ if (fs2.existsSync(rulesDir)) {
15119
+ for (const file of fs2.readdirSync(rulesDir)) {
15120
+ if (!file.startsWith("rihal-") || !file.endsWith(".mdc")) continue;
15121
+ if (file === "rihal-code.mdc") continue;
15122
+ installed.add(file.replace(/^rihal-/, "").replace(/\.mdc$/, ""));
15123
+ }
15124
+ }
15125
+ const digestsDir = path2.join(packageRoot, "rihal/digests");
15126
+ const expected = /* @__PURE__ */ new Set();
15127
+ if (fs2.existsSync(digestsDir)) {
15128
+ for (const file of fs2.readdirSync(digestsDir)) {
15129
+ if (!file.endsWith(".md") || file === "README.md") continue;
15130
+ expected.add(file.replace(/\.md$/, ""));
15131
+ }
15132
+ }
15133
+ return [diffSet(editor, "rules", expected, installed)];
15134
+ }
15135
+ function verifyAntigravityInstall(cwd, packageRoot) {
15136
+ const agentsDir = path2.join(cwd, ".antigravity/agents");
15137
+ const installed = /* @__PURE__ */ new Set();
15138
+ if (fs2.existsSync(agentsDir)) {
15139
+ for (const file of fs2.readdirSync(agentsDir)) {
15140
+ if (!file.startsWith("rihal-") || !file.endsWith(".md")) continue;
15141
+ installed.add(file.replace(/^rihal-/, "").replace(/\.md$/, ""));
15142
+ }
15143
+ }
15144
+ const digestsDir = path2.join(packageRoot, "rihal/digests");
15145
+ const expected = /* @__PURE__ */ new Set();
15146
+ if (fs2.existsSync(digestsDir)) {
15147
+ for (const file of fs2.readdirSync(digestsDir)) {
15148
+ if (!file.endsWith(".md") || file === "README.md") continue;
15149
+ expected.add(file.replace(/\.md$/, ""));
15150
+ }
15151
+ }
15152
+ return [diffSet("antigravity", "agents", expected, installed)];
15153
+ }
15154
+ function verifyInstall(cwd, packageRoot, editors) {
15155
+ const reports = [];
15156
+ if (editors.includes("claude")) {
15157
+ reports.push(...verifyClaudeInstall(cwd, packageRoot));
15158
+ }
15159
+ if (editors.includes("cursor")) {
15160
+ reports.push(...verifyRulesInstall("cursor", cwd, packageRoot));
15161
+ }
15162
+ if (editors.includes("windsurf")) {
15163
+ reports.push(...verifyRulesInstall("windsurf", cwd, packageRoot));
15164
+ }
15165
+ if (editors.includes("antigravity")) {
15166
+ reports.push(...verifyAntigravityInstall(cwd, packageRoot));
15167
+ }
15168
+ const hasDrift = reports.some((r) => r.missing.length > 0 || r.extra.length > 0);
15169
+ return { reports, hasDrift };
15170
+ }
15171
+ function formatReport(reports) {
15172
+ const lines = [];
15173
+ for (const r of reports) {
15174
+ const symbol = r.missing.length === 0 && r.extra.length === 0 ? "\u2713" : "\u26A0";
15175
+ lines.push(
15176
+ ` ${symbol} ${r.editor.padEnd(12)} ${r.kind.padEnd(8)} ${r.installedCount}/${r.expectedCount}`
15177
+ );
15178
+ if (r.missing.length > 0) {
15179
+ lines.push(` missing: ${r.missing.join(", ")}`);
15180
+ }
15181
+ if (r.extra.length > 0) {
15182
+ lines.push(` extra: ${r.extra.join(", ")}`);
15183
+ }
15184
+ }
15185
+ return lines.join("\n");
15186
+ }
15187
+ module2.exports = {
15188
+ readPackageManifest,
15189
+ verifyInstall,
15190
+ verifyClaudeInstall,
15191
+ verifyRulesInstall,
15192
+ verifyAntigravityInstall,
15193
+ formatReport
15194
+ };
15195
+ }
15196
+ });
15197
+
14942
15198
  // cli/install.js
14943
15199
  var require_install = __commonJS({
14944
15200
  "cli/install.js"(exports2, module2) {
@@ -14946,7 +15202,7 @@ var require_install = __commonJS({
14946
15202
  var path2 = require("path");
14947
15203
  var crypto = require("crypto");
14948
15204
  var os = require("os");
14949
- var { writeFileAtomic, safeRmSync } = require(path2.join(__dirname, "lib", "fsutil.cjs"));
15205
+ var { writeFileAtomic, safeRmSync } = require_fsutil();
14950
15206
  var pc = require_picocolors();
14951
15207
  var { createSpinner } = require_dist();
14952
15208
  var fg = require_out4();
@@ -14962,6 +15218,7 @@ var require_install = __commonJS({
14962
15218
  var bold = (s) => pc.bold(s);
14963
15219
  var PACKAGE_ROOT2 = path2.resolve(__dirname, "..");
14964
15220
  var SOURCE_ROOT = path2.join(PACKAGE_ROOT2, "rihal");
15221
+ var SUPPORTED_IDES = Object.freeze(["claude", "cursor", "gemini", "vscode", "antigravity"]);
14965
15222
  var ConfigSchema = z.object({
14966
15223
  user_name: z.string().min(1),
14967
15224
  project_name: z.string().min(1),
@@ -15163,11 +15420,11 @@ var require_install = __commonJS({
15163
15420
  if (opts.noPrompt || opts.global) return ["claude"];
15164
15421
  if (opts.yes || !process.stdin.isTTY) {
15165
15422
  const signals2 = detectIdeSignals(opts.target);
15166
- const detected2 = ["claude", "cursor", "gemini", "vscode", "antigravity"].filter((k) => signals2[k]);
15423
+ const detected2 = SUPPORTED_IDES.filter((k) => signals2[k]);
15167
15424
  return detected2.length > 0 ? detected2 : ["claude"];
15168
15425
  }
15169
15426
  const signals = detectIdeSignals(opts.target);
15170
- const detected = ["claude", "cursor", "gemini", "vscode"].filter((k) => signals[k]);
15427
+ const detected = SUPPORTED_IDES.filter((k) => k !== "antigravity" && signals[k]);
15171
15428
  const initialValues = detected.length > 0 ? detected : ["claude"];
15172
15429
  const choices = await clack.multiselect({
15173
15430
  message: "\u{1F3AF} Which editor(s) will you use rcode with?",
@@ -16144,7 +16401,6 @@ ${BLOCK}`, { mode: 493 });
16144
16401
  console.error(`\u2716 Source tree not found at ${SOURCE_ROOT}. Running from wrong dir?`);
16145
16402
  return 1;
16146
16403
  }
16147
- const SUPPORTED_IDES = ["claude", "cursor", "gemini", "vscode", "antigravity"];
16148
16404
  const unsupported = opts.ides.filter((ide) => !SUPPORTED_IDES.includes(ide));
16149
16405
  if (unsupported.length > 0) {
16150
16406
  console.error(`\u2716 --ide ${unsupported.join(", ")} is not supported in v${readPackageVersion()}.`);
@@ -16738,7 +16994,7 @@ commit_planning: ${desired}
16738
16994
  let fails = 0;
16739
16995
  let expected = { agents: 20, skills: 20, commands: 20 };
16740
16996
  try {
16741
- const { readPackageManifest } = require(path2.join(__dirname, "lib", "manifest.cjs"));
16997
+ const { readPackageManifest } = require_manifest();
16742
16998
  const pkgManifest = readPackageManifest(PACKAGE_ROOT2);
16743
16999
  if (pkgManifest && pkgManifest.agents instanceof Set && pkgManifest.actions instanceof Set) {
16744
17000
  const tolerate = (n) => Math.max(1, Math.floor(n * 0.9));
@@ -16948,6 +17204,7 @@ commit_planning: ${desired}
16948
17204
  module2.exports.parseArgs = parseArgs;
16949
17205
  module2.exports.buildInstallPlan = buildInstallPlan;
16950
17206
  module2.exports.install = install;
17207
+ module2.exports.SUPPORTED_IDES = SUPPORTED_IDES;
16951
17208
  }
16952
17209
  });
16953
17210
 
@@ -17155,262 +17412,6 @@ var require_prompts = __commonJS({
17155
17412
  }
17156
17413
  });
17157
17414
 
17158
- // cli/lib/fsutil.cjs
17159
- var require_fsutil = __commonJS({
17160
- "cli/lib/fsutil.cjs"(exports2, module2) {
17161
- var crypto = require("crypto");
17162
- var fs2 = require("fs");
17163
- var path2 = require("path");
17164
- function writeFileAtomic(filePath, content, opts = {}) {
17165
- const { encoding = "utf8", mode } = opts;
17166
- const dir = path2.dirname(filePath);
17167
- fs2.mkdirSync(dir, { recursive: true });
17168
- const tmpPath = path2.join(
17169
- dir,
17170
- `.${path2.basename(filePath)}.tmp-${process.pid}-${crypto.randomBytes(8).toString("hex")}`
17171
- );
17172
- let fd;
17173
- try {
17174
- fd = fs2.openSync(tmpPath, "wx", mode ?? 420);
17175
- fs2.writeSync(fd, content, 0, encoding);
17176
- fs2.fsyncSync(fd);
17177
- fs2.closeSync(fd);
17178
- fd = null;
17179
- fs2.renameSync(tmpPath, filePath);
17180
- } catch (err) {
17181
- if (fd !== null && fd !== void 0) {
17182
- try {
17183
- fs2.closeSync(fd);
17184
- } catch {
17185
- }
17186
- }
17187
- try {
17188
- fs2.unlinkSync(tmpPath);
17189
- } catch {
17190
- }
17191
- throw err;
17192
- }
17193
- }
17194
- function writeJsonAtomic(filePath, obj, opts = {}) {
17195
- const content = JSON.stringify(obj, null, 2) + "\n";
17196
- writeFileAtomic(filePath, content, opts);
17197
- }
17198
- function safeRmSync(targetPath, projectRoot) {
17199
- let stats;
17200
- try {
17201
- stats = fs2.lstatSync(targetPath);
17202
- } catch (err) {
17203
- if (err.code === "ENOENT") return { ok: true, reason: "missing" };
17204
- return { ok: false, reason: `lstat: ${err.message}` };
17205
- }
17206
- if (stats.isSymbolicLink()) {
17207
- try {
17208
- fs2.unlinkSync(targetPath);
17209
- return { ok: true, reason: "symlink-unlinked" };
17210
- } catch (err) {
17211
- return { ok: false, reason: `unlink: ${err.message}` };
17212
- }
17213
- }
17214
- const root = path2.resolve(projectRoot);
17215
- let resolved;
17216
- try {
17217
- resolved = fs2.realpathSync(targetPath);
17218
- } catch (err) {
17219
- return { ok: false, reason: `realpath: ${err.message}` };
17220
- }
17221
- const relative = path2.relative(root, resolved);
17222
- if (relative.startsWith("..") || path2.isAbsolute(relative)) {
17223
- return { ok: false, reason: "outside-root" };
17224
- }
17225
- try {
17226
- fs2.rmSync(resolved, { recursive: true, force: true });
17227
- return { ok: true };
17228
- } catch (err) {
17229
- return { ok: false, reason: `rmSync: ${err.message}` };
17230
- }
17231
- }
17232
- module2.exports = {
17233
- writeFileAtomic,
17234
- writeJsonAtomic,
17235
- safeRmSync
17236
- };
17237
- }
17238
- });
17239
-
17240
- // cli/lib/manifest.cjs
17241
- var require_manifest = __commonJS({
17242
- "cli/lib/manifest.cjs"(exports2, module2) {
17243
- var fs2 = require("fs");
17244
- var path2 = require("path");
17245
- function readPackageManifest(packageRoot) {
17246
- const skillsRoot = path2.join(packageRoot, "rihal/skills");
17247
- const manifest = { agents: /* @__PURE__ */ new Set(), actions: /* @__PURE__ */ new Set() };
17248
- const agentsDir = path2.join(skillsRoot, "agents");
17249
- if (fs2.existsSync(agentsDir)) {
17250
- for (const entry of fs2.readdirSync(agentsDir, { withFileTypes: true })) {
17251
- if (entry.isDirectory()) manifest.agents.add(entry.name);
17252
- }
17253
- }
17254
- function walkActions(dir) {
17255
- if (!fs2.existsSync(dir)) return;
17256
- for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
17257
- if (!entry.isDirectory()) continue;
17258
- const full = path2.join(dir, entry.name);
17259
- if (fs2.existsSync(path2.join(full, "SKILL.md"))) {
17260
- const installedName = entry.name.startsWith("rihal-") ? entry.name : `rihal-${entry.name}`;
17261
- manifest.actions.add(installedName);
17262
- } else {
17263
- walkActions(full);
17264
- }
17265
- }
17266
- }
17267
- walkActions(path2.join(skillsRoot, "actions"));
17268
- return manifest;
17269
- }
17270
- function readInstalledDirs(dir, prefix = null) {
17271
- if (!fs2.existsSync(dir)) return /* @__PURE__ */ new Set();
17272
- const names = /* @__PURE__ */ new Set();
17273
- for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
17274
- if (!entry.isDirectory()) continue;
17275
- if (prefix && !entry.name.startsWith(prefix)) continue;
17276
- names.add(prefix ? entry.name.slice(prefix.length) : entry.name);
17277
- }
17278
- return names;
17279
- }
17280
- function diffSet(editor, kind, expected, installed) {
17281
- const missing = [...expected].filter((x) => !installed.has(x)).sort();
17282
- const extra = [...installed].filter((x) => !expected.has(x)).sort();
17283
- return {
17284
- editor,
17285
- kind,
17286
- expectedCount: expected.size,
17287
- installedCount: installed.size,
17288
- missing,
17289
- extra
17290
- };
17291
- }
17292
- function verifyClaudeInstall(cwd, packageRoot) {
17293
- const pkg = readPackageManifest(packageRoot);
17294
- const agentsDir = path2.join(cwd, ".claude/agents");
17295
- const skillsDir = path2.join(cwd, ".claude/skills");
17296
- const installedAgents = /* @__PURE__ */ new Set();
17297
- if (fs2.existsSync(agentsDir)) {
17298
- for (const f of fs2.readdirSync(agentsDir)) {
17299
- if (f.startsWith("rihal-") && f.endsWith(".md")) {
17300
- installedAgents.add(f.replace(/^rihal-/, "").replace(/\.md$/, ""));
17301
- }
17302
- }
17303
- }
17304
- if (installedAgents.size === 0) {
17305
- try {
17306
- const os = require("os");
17307
- const globalAgentsDir = path2.join(os.homedir(), ".claude/agents");
17308
- if (fs2.existsSync(globalAgentsDir)) {
17309
- for (const f of fs2.readdirSync(globalAgentsDir)) {
17310
- if (f.startsWith("rihal-") && f.endsWith(".md")) {
17311
- installedAgents.add(f.replace(/^rihal-/, "").replace(/\.md$/, ""));
17312
- }
17313
- }
17314
- }
17315
- } catch {
17316
- }
17317
- }
17318
- const allInstalled = readInstalledDirs(skillsDir);
17319
- const actionsInstalled = new Set(
17320
- [...allInstalled].filter((n) => !n.startsWith("rihal-"))
17321
- );
17322
- return [
17323
- diffSet("claude", "agents", pkg.agents, installedAgents),
17324
- diffSet("claude", "actions", pkg.actions, actionsInstalled)
17325
- ];
17326
- }
17327
- function verifyRulesInstall(editor, cwd, packageRoot) {
17328
- const pkg = readPackageManifest(packageRoot);
17329
- const rulesDir = path2.join(
17330
- cwd,
17331
- editor === "cursor" ? ".cursor/rules" : ".windsurf/rules"
17332
- );
17333
- const installed = /* @__PURE__ */ new Set();
17334
- if (fs2.existsSync(rulesDir)) {
17335
- for (const file of fs2.readdirSync(rulesDir)) {
17336
- if (!file.startsWith("rihal-") || !file.endsWith(".mdc")) continue;
17337
- if (file === "rihal-code.mdc") continue;
17338
- installed.add(file.replace(/^rihal-/, "").replace(/\.mdc$/, ""));
17339
- }
17340
- }
17341
- const digestsDir = path2.join(packageRoot, "rihal/digests");
17342
- const expected = /* @__PURE__ */ new Set();
17343
- if (fs2.existsSync(digestsDir)) {
17344
- for (const file of fs2.readdirSync(digestsDir)) {
17345
- if (!file.endsWith(".md") || file === "README.md") continue;
17346
- expected.add(file.replace(/\.md$/, ""));
17347
- }
17348
- }
17349
- return [diffSet(editor, "rules", expected, installed)];
17350
- }
17351
- function verifyAntigravityInstall(cwd, packageRoot) {
17352
- const agentsDir = path2.join(cwd, ".antigravity/agents");
17353
- const installed = /* @__PURE__ */ new Set();
17354
- if (fs2.existsSync(agentsDir)) {
17355
- for (const file of fs2.readdirSync(agentsDir)) {
17356
- if (!file.startsWith("rihal-") || !file.endsWith(".md")) continue;
17357
- installed.add(file.replace(/^rihal-/, "").replace(/\.md$/, ""));
17358
- }
17359
- }
17360
- const digestsDir = path2.join(packageRoot, "rihal/digests");
17361
- const expected = /* @__PURE__ */ new Set();
17362
- if (fs2.existsSync(digestsDir)) {
17363
- for (const file of fs2.readdirSync(digestsDir)) {
17364
- if (!file.endsWith(".md") || file === "README.md") continue;
17365
- expected.add(file.replace(/\.md$/, ""));
17366
- }
17367
- }
17368
- return [diffSet("antigravity", "agents", expected, installed)];
17369
- }
17370
- function verifyInstall(cwd, packageRoot, editors) {
17371
- const reports = [];
17372
- if (editors.includes("claude")) {
17373
- reports.push(...verifyClaudeInstall(cwd, packageRoot));
17374
- }
17375
- if (editors.includes("cursor")) {
17376
- reports.push(...verifyRulesInstall("cursor", cwd, packageRoot));
17377
- }
17378
- if (editors.includes("windsurf")) {
17379
- reports.push(...verifyRulesInstall("windsurf", cwd, packageRoot));
17380
- }
17381
- if (editors.includes("antigravity")) {
17382
- reports.push(...verifyAntigravityInstall(cwd, packageRoot));
17383
- }
17384
- const hasDrift = reports.some((r) => r.missing.length > 0 || r.extra.length > 0);
17385
- return { reports, hasDrift };
17386
- }
17387
- function formatReport(reports) {
17388
- const lines = [];
17389
- for (const r of reports) {
17390
- const symbol = r.missing.length === 0 && r.extra.length === 0 ? "\u2713" : "\u26A0";
17391
- lines.push(
17392
- ` ${symbol} ${r.editor.padEnd(12)} ${r.kind.padEnd(8)} ${r.installedCount}/${r.expectedCount}`
17393
- );
17394
- if (r.missing.length > 0) {
17395
- lines.push(` missing: ${r.missing.join(", ")}`);
17396
- }
17397
- if (r.extra.length > 0) {
17398
- lines.push(` extra: ${r.extra.join(", ")}`);
17399
- }
17400
- }
17401
- return lines.join("\n");
17402
- }
17403
- module2.exports = {
17404
- readPackageManifest,
17405
- verifyInstall,
17406
- verifyClaudeInstall,
17407
- verifyRulesInstall,
17408
- verifyAntigravityInstall,
17409
- formatReport
17410
- };
17411
- }
17412
- });
17413
-
17414
17415
  // cli/update.js
17415
17416
  var require_update = __commonJS({
17416
17417
  "cli/update.js"(exports2, module2) {
@@ -17703,6 +17704,8 @@ var require_update = __commonJS({
17703
17704
  }
17704
17705
  console.log();
17705
17706
  }
17707
+ module2.exports.parseArgs = parseArgs;
17708
+ module2.exports.detectInstalledEditors = detectInstalledEditors;
17706
17709
  }
17707
17710
  });
17708
17711
 
@@ -17746,6 +17749,9 @@ var require_uninstall = __commonJS({
17746
17749
  function isLocalOverride(name) {
17747
17750
  return /\.local\.(md|mdc|json|yaml|yml|toml|js|ts)$/.test(name);
17748
17751
  }
17752
+ function stripRihalGitignoreBlock(text) {
17753
+ return text.replace(/\n?# ===== rcode-managed gitignore block[\s\S]*?# ===== end rcode-managed gitignore block =====\n?/g, "\n").replace(/\n?# >>> rihal-code >>>[\s\S]*?# <<< rihal-code <<<\n?/g, "\n").replace(/\n{3,}/g, "\n\n");
17754
+ }
17749
17755
  function removeMatching(dir, predicate) {
17750
17756
  if (!fs2.existsSync(dir)) return 0;
17751
17757
  let count = 0;
@@ -17856,7 +17862,7 @@ var require_uninstall = __commonJS({
17856
17862
  }
17857
17863
  function discoverKnownActionSkills() {
17858
17864
  try {
17859
- const { readPackageManifest } = require(path2.join(__dirname, "lib", "manifest.cjs"));
17865
+ const { readPackageManifest } = require_manifest();
17860
17866
  const packageRoot = path2.resolve(__dirname, "..");
17861
17867
  const pkg = readPackageManifest(packageRoot);
17862
17868
  if (pkg && pkg.actions instanceof Set && pkg.actions.size > 0) {
@@ -17981,8 +17987,8 @@ var require_uninstall = __commonJS({
17981
17987
  async function runUninstall(args) {
17982
17988
  const opts = parseArgs(args);
17983
17989
  const cwd = process.cwd();
17984
- const SUPPORTED_EDITORS = ["claude", "cursor", "gemini", "vscode", "antigravity"];
17985
- const editors = opts.editor ? opts.editor === "all" ? SUPPORTED_EDITORS : [opts.editor] : SUPPORTED_EDITORS;
17990
+ const { SUPPORTED_IDES } = require_install();
17991
+ const editors = opts.editor ? opts.editor === "all" ? Array.from(SUPPORTED_IDES) : [opts.editor] : Array.from(SUPPORTED_IDES);
17986
17992
  console.log(`
17987
17993
  \u{1F54C} Rihal Code \u2014 Uninstall
17988
17994
  `);
@@ -18219,7 +18225,7 @@ var require_uninstall = __commonJS({
18219
18225
  if (fs2.existsSync(gitignorePath)) {
18220
18226
  try {
18221
18227
  const before = fs2.readFileSync(gitignorePath, "utf8");
18222
- const stripped = before.replace(/\n?# ===== rcode-managed gitignore block[\s\S]*?# ===== end rcode-managed gitignore block =====\n?/g, "\n").replace(/\n?# >>> rihal-code >>>[\s\S]*?# <<< rihal-code <<<\n?/g, "\n").replace(/\n{3,}/g, "\n\n");
18228
+ const stripped = stripRihalGitignoreBlock(before);
18223
18229
  if (stripped !== before) {
18224
18230
  fs2.writeFileSync(gitignorePath, stripped);
18225
18231
  console.log(` \u2713 stripped rcode block from .gitignore (--purge)`);
@@ -18248,6 +18254,10 @@ var require_uninstall = __commonJS({
18248
18254
  To reinstall later:`);
18249
18255
  console.log(` rcode install`);
18250
18256
  }
18257
+ module2.exports.isLocalOverride = isLocalOverride;
18258
+ module2.exports.planToPathList = planToPathList;
18259
+ module2.exports.discoverKnownActionSkills = discoverKnownActionSkills;
18260
+ module2.exports.stripRihalGitignoreBlock = stripRihalGitignoreBlock;
18251
18261
  if (require.main === module2) {
18252
18262
  module2.exports(process.argv.slice(2)).catch((err) => {
18253
18263
  if (err instanceof PromptAbortError) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzlaa/rcode",
3
- "version": "3.4.25",
3
+ "version": "3.4.27",
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": {