@hanzlaa/rcode 3.4.24 → 3.4.25

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
@@ -324,6 +324,11 @@ function detectIdeSignals(target) {
324
324
  * actually wanted cursor or gemini.
325
325
  */
326
326
  async function resolveIde(opts) {
327
+ // Issue #692: when the wizard has already collected opts.ides (interactive
328
+ // run from main()), resolveIde was re-prompting because it only checked
329
+ // opts.ideProvided (set by --ide flag, not by the wizard). Honor any
330
+ // pre-existing array result so we don't double-prompt.
331
+ if (Array.isArray(opts.ides) && opts.ides.length > 0) return opts.ides;
327
332
  if (opts.ideProvided) return [opts.ide]; // user passed --ide, respect it
328
333
  if (opts.noPrompt || opts.global) return ['claude']; // auto-install: always claude
329
334
  if (opts.yes || !process.stdin.isTTY) {
@@ -1491,6 +1496,86 @@ async function install(opts) {
1491
1496
  return 2;
1492
1497
  }
1493
1498
 
1499
+ // Issue #691: file lock prevents concurrent installs from racing on the
1500
+ // same .rihal/_config/manifest.yaml + files-manifest.csv. Without it, two
1501
+ // parallel runs (two terminals, postinstall + manual install, etc.) can
1502
+ // each write a manifest the OTHER doesn't see → orphan sweep on the next
1503
+ // install deletes files the other run considered legit.
1504
+ let releaseLock = () => {};
1505
+ if (!opts.global) {
1506
+ const lockResult = acquireInstallLock(opts.target);
1507
+ if (!lockResult.ok) {
1508
+ console.log('');
1509
+ console.log(' ' + warn(`Another install is already running here (PID ${lockResult.pid}).`));
1510
+ console.log(' ' + dim(` Lock: ${lockResult.lockPath}`));
1511
+ console.log(' ' + dim(' If the other process crashed, delete the lock file and retry:'));
1512
+ console.log(' ' + dim(` rm ${lockResult.lockPath}`));
1513
+ console.log('');
1514
+ return 3;
1515
+ }
1516
+ releaseLock = lockResult.release;
1517
+ // Make sure the lock is released even if install throws unexpectedly.
1518
+ process.on('exit', releaseLock);
1519
+ }
1520
+
1521
+ try {
1522
+ return await installInner(opts);
1523
+ } finally {
1524
+ releaseLock();
1525
+ process.removeListener('exit', releaseLock);
1526
+ }
1527
+ }
1528
+
1529
+ /**
1530
+ * Acquire an exclusive install lock at .rihal/.install.lock (issue #691).
1531
+ *
1532
+ * Returns:
1533
+ * { ok: true, release: () => void } lock acquired
1534
+ * { ok: false, pid: number, lockPath: string } another process holds it
1535
+ *
1536
+ * Stale-lock detection: if the recorded PID is not alive, the lock is
1537
+ * reclaimed automatically.
1538
+ */
1539
+ function acquireInstallLock(target) {
1540
+ const lockDir = path.join(target, '.rihal');
1541
+ const lockPath = path.join(lockDir, '.install.lock');
1542
+ try {
1543
+ fs.mkdirSync(lockDir, { recursive: true });
1544
+ } catch { /* fall through; openSync will fail with a clearer error */ }
1545
+
1546
+ function isAlive(pid) {
1547
+ try { process.kill(pid, 0); return true; } catch { return false; }
1548
+ }
1549
+
1550
+ for (let attempt = 0; attempt < 2; attempt++) {
1551
+ try {
1552
+ const fd = fs.openSync(lockPath, 'wx'); // exclusive create
1553
+ fs.writeSync(fd, String(process.pid));
1554
+ fs.closeSync(fd);
1555
+ return {
1556
+ ok: true,
1557
+ release: () => {
1558
+ try { fs.unlinkSync(lockPath); } catch {}
1559
+ },
1560
+ };
1561
+ } catch (err) {
1562
+ if (err.code !== 'EEXIST') throw err;
1563
+ // Lock exists — check if holder is alive.
1564
+ let pid = 0;
1565
+ try { pid = parseInt(fs.readFileSync(lockPath, 'utf8'), 10); } catch {}
1566
+ if (pid && !isAlive(pid)) {
1567
+ // Stale lock — remove and retry once.
1568
+ try { fs.unlinkSync(lockPath); } catch {}
1569
+ continue;
1570
+ }
1571
+ return { ok: false, pid, lockPath };
1572
+ }
1573
+ }
1574
+ // Should be unreachable, but degrade gracefully.
1575
+ return { ok: false, pid: 0, lockPath };
1576
+ }
1577
+
1578
+ async function installInner(opts) {
1494
1579
  const pkgVersion = readPackageVersion();
1495
1580
 
1496
1581
  // Header banner — only shown for interactive runs to keep CI/non-TTY logs terse.
@@ -2423,6 +2508,11 @@ async function runInstallWizard(opts) {
2423
2508
  });
2424
2509
  if (isCancel(editorChoices)) { cancel('Installation cancelled.'); process.exit(0); }
2425
2510
  opts.ides = editorChoices;
2511
+ // Issue #692: keep opts.ide and opts.ides consistent so downstream callers
2512
+ // that historically read either field see the same answer. Mark provided
2513
+ // so any later resolveIde call exits early.
2514
+ opts.ide = editorChoices[0];
2515
+ opts.ideProvided = true;
2426
2516
 
2427
2517
  // ── 3. Communication language ─────────────────────────────────────────
2428
2518
  const langChoice = await select({
package/cli/uninstall.js CHANGED
@@ -220,35 +220,29 @@ function buildPlan(cwd, editors) {
220
220
  }
221
221
 
222
222
  /**
223
- * List of action-skill names the installer places in .claude/skills/.
224
- * These do NOT start with `rihal-` (e.g., `rihal-domain-research` does, but
225
- * for safety we also keep a known list).
223
+ * Names of action-skill directories the installer places under .claude/skills/.
224
+ *
225
+ * Issue #693: this used to be a hardcoded array of 23 names that drifted from
226
+ * the source the moment anyone added or removed a skill in `rihal/skills/`.
227
+ * We now derive it from the package's own manifest (cli/lib/manifest.cjs)
228
+ * with a static fallback for the rare case where the manifest module isn't
229
+ * resolvable from the uninstall context.
226
230
  */
227
- const KNOWN_ACTION_SKILLS = [
228
- 'rihal-check-implementation-readiness',
229
- 'rihal-code-review',
230
- 'rihal-correct-course',
231
- 'rihal-create-architecture',
232
- 'rihal-create-epics-and-stories',
233
- 'rihal-create-prd',
234
- 'rihal-create-story',
235
- 'rihal-create-ux-design',
236
- 'rihal-dev-story',
237
- 'rihal-document-project',
238
- 'rihal-domain-research',
239
- 'rihal-edit-prd',
240
- 'rihal-frontend-design',
241
- 'rihal-generate-project-context',
242
- 'rihal-market-research',
243
- 'rihal-product-brief',
244
- 'rihal-qa-generate-e2e-tests',
245
- 'rihal-retrospective',
246
- 'rihal-sprint-planning',
247
- 'rihal-sprint-status',
248
- 'rihal-technical-research',
249
- 'rihal-validate-prd',
250
- 'rihal-clone-website',
251
- ];
231
+ function discoverKnownActionSkills() {
232
+ try {
233
+ const { readPackageManifest } = require(path.join(__dirname, 'lib', 'manifest.cjs'));
234
+ const packageRoot = path.resolve(__dirname, '..');
235
+ const pkg = readPackageManifest(packageRoot);
236
+ if (pkg && pkg.actions instanceof Set && pkg.actions.size > 0) {
237
+ return Array.from(pkg.actions);
238
+ }
239
+ } catch { /* fall through to static list */ }
240
+ // Static fallback — kept minimal, only the names that don't start with
241
+ // 'rihal-' would actually need this list since we already match
242
+ // 'rihal-*' via prefix. This is defensive only.
243
+ return [];
244
+ }
245
+ const KNOWN_ACTION_SKILLS = discoverKnownActionSkills();
252
246
 
253
247
  function isKnownSkillName(name) {
254
248
  return KNOWN_ACTION_SKILLS.includes(name);
@@ -425,9 +419,15 @@ async function runUninstall(args) {
425
419
  const opts = parseArgs(args);
426
420
  const cwd = process.cwd();
427
421
 
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'];
428
428
  const editors = opts.editor
429
- ? (opts.editor === 'all' ? ['claude', 'cursor', 'windsurf', 'antigravity'] : [opts.editor])
430
- : ['claude', 'cursor', 'windsurf', 'antigravity'];
429
+ ? (opts.editor === 'all' ? SUPPORTED_EDITORS : [opts.editor])
430
+ : SUPPORTED_EDITORS;
431
431
 
432
432
  console.log(`\n🕌 Rihal Code — Uninstall\n`);
433
433
  console.log(` Project: ${cwd}`);
package/dist/rcode.js CHANGED
@@ -15158,6 +15158,7 @@ var require_install = __commonJS({
15158
15158
  return signals;
15159
15159
  }
15160
15160
  async function resolveIde(opts) {
15161
+ if (Array.isArray(opts.ides) && opts.ides.length > 0) return opts.ides;
15161
15162
  if (opts.ideProvided) return [opts.ide];
15162
15163
  if (opts.noPrompt || opts.global) return ["claude"];
15163
15164
  if (opts.yes || !process.stdin.isTTY) {
@@ -16046,6 +16047,78 @@ ${BLOCK}`, { mode: 493 });
16046
16047
  console.log("");
16047
16048
  return 2;
16048
16049
  }
16050
+ let releaseLock = () => {
16051
+ };
16052
+ if (!opts.global) {
16053
+ const lockResult = acquireInstallLock(opts.target);
16054
+ if (!lockResult.ok) {
16055
+ console.log("");
16056
+ console.log(" " + warn(`Another install is already running here (PID ${lockResult.pid}).`));
16057
+ console.log(" " + dim(` Lock: ${lockResult.lockPath}`));
16058
+ console.log(" " + dim(" If the other process crashed, delete the lock file and retry:"));
16059
+ console.log(" " + dim(` rm ${lockResult.lockPath}`));
16060
+ console.log("");
16061
+ return 3;
16062
+ }
16063
+ releaseLock = lockResult.release;
16064
+ process.on("exit", releaseLock);
16065
+ }
16066
+ try {
16067
+ return await installInner(opts);
16068
+ } finally {
16069
+ releaseLock();
16070
+ process.removeListener("exit", releaseLock);
16071
+ }
16072
+ }
16073
+ function acquireInstallLock(target) {
16074
+ const lockDir = path2.join(target, ".rihal");
16075
+ const lockPath = path2.join(lockDir, ".install.lock");
16076
+ try {
16077
+ fs2.mkdirSync(lockDir, { recursive: true });
16078
+ } catch {
16079
+ }
16080
+ function isAlive(pid) {
16081
+ try {
16082
+ process.kill(pid, 0);
16083
+ return true;
16084
+ } catch {
16085
+ return false;
16086
+ }
16087
+ }
16088
+ for (let attempt = 0; attempt < 2; attempt++) {
16089
+ try {
16090
+ const fd = fs2.openSync(lockPath, "wx");
16091
+ fs2.writeSync(fd, String(process.pid));
16092
+ fs2.closeSync(fd);
16093
+ return {
16094
+ ok: true,
16095
+ release: () => {
16096
+ try {
16097
+ fs2.unlinkSync(lockPath);
16098
+ } catch {
16099
+ }
16100
+ }
16101
+ };
16102
+ } catch (err) {
16103
+ if (err.code !== "EEXIST") throw err;
16104
+ let pid = 0;
16105
+ try {
16106
+ pid = parseInt(fs2.readFileSync(lockPath, "utf8"), 10);
16107
+ } catch {
16108
+ }
16109
+ if (pid && !isAlive(pid)) {
16110
+ try {
16111
+ fs2.unlinkSync(lockPath);
16112
+ } catch {
16113
+ }
16114
+ continue;
16115
+ }
16116
+ return { ok: false, pid, lockPath };
16117
+ }
16118
+ }
16119
+ return { ok: false, pid: 0, lockPath };
16120
+ }
16121
+ async function installInner(opts) {
16049
16122
  const pkgVersion = readPackageVersion();
16050
16123
  const isInteractive = process.stdin.isTTY && !opts.yes;
16051
16124
  if (isInteractive) printInstallHeader(pkgVersion);
@@ -16796,6 +16869,8 @@ commit_planning: ${desired}
16796
16869
  process.exit(0);
16797
16870
  }
16798
16871
  opts.ides = editorChoices;
16872
+ opts.ide = editorChoices[0];
16873
+ opts.ideProvided = true;
16799
16874
  const langChoice = await select({
16800
16875
  message: "Communication language?",
16801
16876
  options: [
@@ -17779,31 +17854,19 @@ var require_uninstall = __commonJS({
17779
17854
  }
17780
17855
  return plan;
17781
17856
  }
17782
- var KNOWN_ACTION_SKILLS = [
17783
- "rihal-check-implementation-readiness",
17784
- "rihal-code-review",
17785
- "rihal-correct-course",
17786
- "rihal-create-architecture",
17787
- "rihal-create-epics-and-stories",
17788
- "rihal-create-prd",
17789
- "rihal-create-story",
17790
- "rihal-create-ux-design",
17791
- "rihal-dev-story",
17792
- "rihal-document-project",
17793
- "rihal-domain-research",
17794
- "rihal-edit-prd",
17795
- "rihal-frontend-design",
17796
- "rihal-generate-project-context",
17797
- "rihal-market-research",
17798
- "rihal-product-brief",
17799
- "rihal-qa-generate-e2e-tests",
17800
- "rihal-retrospective",
17801
- "rihal-sprint-planning",
17802
- "rihal-sprint-status",
17803
- "rihal-technical-research",
17804
- "rihal-validate-prd",
17805
- "rihal-clone-website"
17806
- ];
17857
+ function discoverKnownActionSkills() {
17858
+ try {
17859
+ const { readPackageManifest } = require(path2.join(__dirname, "lib", "manifest.cjs"));
17860
+ const packageRoot = path2.resolve(__dirname, "..");
17861
+ const pkg = readPackageManifest(packageRoot);
17862
+ if (pkg && pkg.actions instanceof Set && pkg.actions.size > 0) {
17863
+ return Array.from(pkg.actions);
17864
+ }
17865
+ } catch {
17866
+ }
17867
+ return [];
17868
+ }
17869
+ var KNOWN_ACTION_SKILLS = discoverKnownActionSkills();
17807
17870
  function isKnownSkillName(name) {
17808
17871
  return KNOWN_ACTION_SKILLS.includes(name);
17809
17872
  }
@@ -17918,7 +17981,8 @@ var require_uninstall = __commonJS({
17918
17981
  async function runUninstall(args) {
17919
17982
  const opts = parseArgs(args);
17920
17983
  const cwd = process.cwd();
17921
- const editors = opts.editor ? opts.editor === "all" ? ["claude", "cursor", "windsurf", "antigravity"] : [opts.editor] : ["claude", "cursor", "windsurf", "antigravity"];
17984
+ const SUPPORTED_EDITORS = ["claude", "cursor", "gemini", "vscode", "antigravity"];
17985
+ const editors = opts.editor ? opts.editor === "all" ? SUPPORTED_EDITORS : [opts.editor] : SUPPORTED_EDITORS;
17922
17986
  console.log(`
17923
17987
  \u{1F54C} Rihal Code \u2014 Uninstall
17924
17988
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzlaa/rcode",
3
- "version": "3.4.24",
3
+ "version": "3.4.25",
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": {