claude-code-arcane 2.9.5 → 2.9.7

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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [2.9.7](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.9.6...v2.9.7) (2026-09-08)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * make a first-time npx install survivable ([964ef8e](https://github.com/SebastianLuser/claude-code-arcane/commit/964ef8eef9d4ea6a1e593eac189bceb79650174d))
7
+
8
+ ## [2.9.6](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.9.5...v2.9.6) (2026-09-07)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **rules:** make code-comments the only authority on comments ([99bb84e](https://github.com/SebastianLuser/claude-code-arcane/commit/99bb84e7612630ae67d4942b378ccd1881d4cf64))
14
+
1
15
  ## [2.9.5](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.9.4...v2.9.5) (2026-09-07)
2
16
 
3
17
 
package/README.md CHANGED
@@ -6,6 +6,13 @@ A configuration harness installable via `npx`. Pick a **profile** that matches y
6
6
 
7
7
  ---
8
8
 
9
+ ## Requirements
10
+
11
+ - **Node 20+** and **npm 7+** — `npx` needs npm 7 to resolve the package name to its `arcane` bin.
12
+ - **Nothing to clone or download.** `npx` fetches what it needs. Run it inside **your own project**.
13
+
14
+ ---
15
+
9
16
  ## Quick Start
10
17
 
11
18
  ```bash
@@ -21,6 +28,19 @@ claude
21
28
 
22
29
  That's it. Your project now has skills, agents, hooks, rules, and permissions tailored for your stack.
23
30
 
31
+ > **Do not install from a downloaded copy of this repo.** `install` targets the current directory, so
32
+ > running it inside a `claude-code-arcane-*` folder from the Releases page deploys the whole profile
33
+ > into that throwaway folder instead of your project.
34
+
35
+ Every command in this README uses the `npx` form, which always works. Bare `arcane` is the *bin* name
36
+ and only reaches your `PATH` after a global install — so if you prefer the shorter form, ask for it
37
+ explicitly first:
38
+
39
+ ```bash
40
+ npm install -g claude-code-arcane
41
+ arcane install backend-ts+agile # same as the npx line above
42
+ ```
43
+
24
44
  ---
25
45
 
26
46
  ## How It Works
package/dist/cli.js CHANGED
@@ -4,8 +4,10 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/commands/install.ts
7
+ import fs10 from "fs";
7
8
  import path12 from "path";
8
9
  import chalk2 from "chalk";
10
+ import * as p2 from "@clack/prompts";
9
11
 
10
12
  // src/profiles.ts
11
13
  import fs from "fs";
@@ -48,9 +50,9 @@ function groupByCategory(profiles) {
48
50
  const known = new Set(CATEGORY_ORDER.map((c) => c.id));
49
51
  const groups = CATEGORY_ORDER.map((c) => ({
50
52
  ...c,
51
- profiles: profiles.filter((p2) => p2.category === c.id)
53
+ profiles: profiles.filter((p3) => p3.category === c.id)
52
54
  }));
53
- const rest = profiles.filter((p2) => !known.has(p2.category));
55
+ const rest = profiles.filter((p3) => !known.has(p3.category));
54
56
  if (rest.length > 0) {
55
57
  groups.push({ id: "other", label: "Other", profiles: rest });
56
58
  }
@@ -419,18 +421,18 @@ function copyDirSync(src, dest) {
419
421
  function ensureDir(dir) {
420
422
  fs3.mkdirSync(dir, { recursive: true });
421
423
  }
422
- function fileExists(p2) {
423
- return fs3.existsSync(p2);
424
+ function fileExists(p3) {
425
+ return fs3.existsSync(p3);
424
426
  }
425
- function readJsonSync(p2) {
426
- return JSON.parse(fs3.readFileSync(p2, "utf-8"));
427
+ function readJsonSync(p3) {
428
+ return JSON.parse(fs3.readFileSync(p3, "utf-8"));
427
429
  }
428
- function writeJsonSync(p2, data) {
429
- fs3.writeFileSync(p2, JSON.stringify(data, null, 2) + "\n", "utf-8");
430
+ function writeJsonSync(p3, data) {
431
+ fs3.writeFileSync(p3, JSON.stringify(data, null, 2) + "\n", "utf-8");
430
432
  }
431
- function isSymlinkOrJunction(p2) {
433
+ function isSymlinkOrJunction(p3) {
432
434
  try {
433
- return fs3.lstatSync(p2).isSymbolicLink();
435
+ return fs3.lstatSync(p3).isSymbolicLink();
434
436
  } catch {
435
437
  return false;
436
438
  }
@@ -440,11 +442,11 @@ function getPackageVersion() {
440
442
  const pkg = readJsonSync(pkgPath);
441
443
  return pkg.version;
442
444
  }
443
- function safeRemove(p2) {
444
- if (isSymlinkOrJunction(p2)) {
445
- fs3.unlinkSync(p2);
446
- } else if (fs3.existsSync(p2)) {
447
- fs3.rmSync(p2, { recursive: true, force: true });
445
+ function safeRemove(p3) {
446
+ if (isSymlinkOrJunction(p3)) {
447
+ fs3.unlinkSync(p3);
448
+ } else if (fs3.existsSync(p3)) {
449
+ fs3.rmSync(p3, { recursive: true, force: true });
448
450
  }
449
451
  }
450
452
 
@@ -466,8 +468,8 @@ function isGranular(raw) {
466
468
  }
467
469
  function agentSourcePath(root, raw) {
468
470
  const { division, agent } = parseAgentEntry(raw);
469
- const p2 = agent ? path4.join(root, "agents", division, `${agent}.md`) : path4.join(root, "agents", division);
470
- return fs4.existsSync(p2) ? p2 : null;
471
+ const p3 = agent ? path4.join(root, "agents", division, `${agent}.md`) : path4.join(root, "agents", division);
472
+ return fs4.existsSync(p3) ? p3 : null;
471
473
  }
472
474
  function copyAgentEntry(root, claudeDir, raw) {
473
475
  const src = agentSourcePath(root, raw);
@@ -510,9 +512,9 @@ function manifestPath(target) {
510
512
  return path5.join(target, ".claude", MANIFEST_FILE);
511
513
  }
512
514
  function readManifest(target) {
513
- const p2 = manifestPath(target);
514
- if (!fileExists(p2)) return null;
515
- return readJsonSync(p2);
515
+ const p3 = manifestPath(target);
516
+ if (!fileExists(p3)) return null;
517
+ return readJsonSync(p3);
516
518
  }
517
519
  function writeManifest(target, merged, profileCommand, packageRoot, worktreeOrOpts) {
518
520
  let opts = {};
@@ -1274,27 +1276,27 @@ function clean(target) {
1274
1276
  if (fs7.existsSync(manifestFile)) {
1275
1277
  const data = JSON.parse(fs7.readFileSync(manifestFile, "utf-8"));
1276
1278
  const profiles = (data.profiles ?? []).filter(
1277
- (p2) => p2 !== "core"
1279
+ (p3) => p3 !== "core"
1278
1280
  );
1279
1281
  console.log(` Current profile: ${profiles.join(" + ")}`);
1280
1282
  console.log(` Installed: ${data.installed_at ?? "unknown"}`);
1281
1283
  const shared = data.worktree?.shared_dirs ?? [];
1282
1284
  for (const dir of shared) {
1283
- const p2 = path8.join(claudeDir, dir);
1284
- if (isSymlinkOrJunction(p2)) {
1285
- fs7.unlinkSync(p2);
1285
+ const p3 = path8.join(claudeDir, dir);
1286
+ if (isSymlinkOrJunction(p3)) {
1287
+ fs7.unlinkSync(p3);
1286
1288
  console.log(` [ok] Unlinked shared ${dir}/`);
1287
1289
  }
1288
1290
  }
1289
1291
  }
1290
1292
  for (const sub of ["hooks", "docs", "skills", "agents", "rules"]) {
1291
- const p2 = path8.join(claudeDir, sub);
1292
- safeRemove(p2);
1293
+ const p3 = path8.join(claudeDir, sub);
1294
+ safeRemove(p3);
1293
1295
  }
1294
1296
  const remaining = fs7.readdirSync(claudeDir);
1295
1297
  for (const f of remaining) {
1296
- const p2 = path8.join(claudeDir, f);
1297
- safeRemove(p2);
1298
+ const p3 = path8.join(claudeDir, f);
1299
+ safeRemove(p3);
1298
1300
  }
1299
1301
  if (fs7.existsSync(claudeDir)) {
1300
1302
  fs7.rmSync(claudeDir, { recursive: true, force: true });
@@ -1631,12 +1633,12 @@ function registryPath() {
1631
1633
  return path11.join(arcaneHome(), "installations.json");
1632
1634
  }
1633
1635
  function readRegistryFile() {
1634
- const p2 = registryPath();
1635
- if (!fileExists(p2)) {
1636
+ const p3 = registryPath();
1637
+ if (!fileExists(p3)) {
1636
1638
  return { version: REGISTRY_VERSION, installations: [] };
1637
1639
  }
1638
1640
  try {
1639
- const data = readJsonSync(p2);
1641
+ const data = readJsonSync(p3);
1640
1642
  return {
1641
1643
  version: REGISTRY_VERSION,
1642
1644
  installations: Array.isArray(data.installations) ? data.installations : []
@@ -1674,6 +1676,36 @@ function pruneRegistry() {
1674
1676
  }
1675
1677
 
1676
1678
  // src/commands/install.ts
1679
+ var PACKAGE_NAME = "claude-code-arcane";
1680
+ function isArcaneSourceTree(dir) {
1681
+ try {
1682
+ const pkg = JSON.parse(
1683
+ fs10.readFileSync(path12.join(dir, "package.json"), "utf-8")
1684
+ );
1685
+ return pkg.name === PACKAGE_NAME;
1686
+ } catch {
1687
+ return false;
1688
+ }
1689
+ }
1690
+ async function refusesSelfInstall(target, profileExpr, opts) {
1691
+ if (!isArcaneSourceTree(target)) return false;
1692
+ console.log(
1693
+ chalk2.yellow("\n This directory is Arcane's own source tree, not a project.")
1694
+ );
1695
+ console.log(chalk2.dim(` Target: ${target}`));
1696
+ if (opts.force || opts.dryRun) return false;
1697
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
1698
+ const proceed = interactive ? await p2.confirm({ message: "Install here anyway?", initialValue: false }) : false;
1699
+ if (!p2.isCancel(proceed) && proceed) return false;
1700
+ console.log(
1701
+ chalk2.dim(
1702
+ `
1703
+ cd to your project first, then: npx ${PACKAGE_NAME} install ${profileExpr ?? "<profile>"}`
1704
+ )
1705
+ );
1706
+ console.log(chalk2.dim(" Or pass --force to install here on purpose.\n"));
1707
+ return true;
1708
+ }
1677
1709
  async function installCommand(profileExpr, opts) {
1678
1710
  const source = await resolveContentSource({
1679
1711
  source: opts.source ?? "auto",
@@ -1682,6 +1714,7 @@ async function installCommand(profileExpr, opts) {
1682
1714
  const root = await source.getContentRoot();
1683
1715
  const profilesDir = path12.join(root, "profiles");
1684
1716
  const target = path12.resolve(opts.target ?? process.cwd());
1717
+ if (await refusesSelfInstall(target, profileExpr, opts)) return;
1685
1718
  if (!profileExpr) {
1686
1719
  const profiles = listProfiles(profilesDir);
1687
1720
  if (process.stdin.isTTY && process.stdout.isTTY) {
@@ -1692,9 +1725,9 @@ async function installCommand(profileExpr, opts) {
1692
1725
  console.log(chalk2.bold("\nAvailable profiles (combine freely with +):\n"));
1693
1726
  for (const group of groupByCategory(profiles)) {
1694
1727
  console.log(chalk2.cyan(` ${group.label}:`));
1695
- for (const p2 of group.profiles) {
1728
+ for (const p3 of group.profiles) {
1696
1729
  console.log(
1697
- ` ${chalk2.green(p2.name.padEnd(20))} ${p2.description}`
1730
+ ` ${chalk2.green(p3.name.padEnd(20))} ${p3.description}`
1698
1731
  );
1699
1732
  }
1700
1733
  console.log();
@@ -1752,18 +1785,18 @@ Installing profile: ${chalk2.cyan(profileExpr)}`)
1752
1785
  }
1753
1786
 
1754
1787
  // src/commands/add.ts
1755
- import fs11 from "fs";
1788
+ import fs12 from "fs";
1756
1789
  import path14 from "path";
1757
1790
  import chalk3 from "chalk";
1758
1791
 
1759
1792
  // src/skills-catalog.ts
1760
- import fs10 from "fs";
1793
+ import fs11 from "fs";
1761
1794
  import path13 from "path";
1762
1795
  var FRONTMATTER_BYTES = 4096;
1763
1796
  function listSkills(skillsDir) {
1764
- if (!fs10.existsSync(skillsDir)) return [];
1765
- return fs10.readdirSync(skillsDir, { withFileTypes: true }).filter(
1766
- (d) => d.isDirectory() && !d.name.startsWith("_") && fs10.existsSync(path13.join(skillsDir, d.name, "SKILL.md"))
1797
+ if (!fs11.existsSync(skillsDir)) return [];
1798
+ return fs11.readdirSync(skillsDir, { withFileTypes: true }).filter(
1799
+ (d) => d.isDirectory() && !d.name.startsWith("_") && fs11.existsSync(path13.join(skillsDir, d.name, "SKILL.md"))
1767
1800
  ).map((d) => ({
1768
1801
  name: d.name,
1769
1802
  description: readDescription(path13.join(skillsDir, d.name, "SKILL.md"))
@@ -1787,13 +1820,13 @@ function readDescription(skillFile) {
1787
1820
  return inline.replace(/^["']|["']$/g, "").trim();
1788
1821
  }
1789
1822
  function readHead(file) {
1790
- const fd = fs10.openSync(file, "r");
1823
+ const fd = fs11.openSync(file, "r");
1791
1824
  try {
1792
1825
  const buf = Buffer.alloc(FRONTMATTER_BYTES);
1793
- const read = fs10.readSync(fd, buf, 0, FRONTMATTER_BYTES, 0);
1826
+ const read = fs11.readSync(fd, buf, 0, FRONTMATTER_BYTES, 0);
1794
1827
  return buf.subarray(0, read).toString("utf-8");
1795
1828
  } finally {
1796
- fs10.closeSync(fd);
1829
+ fs11.closeSync(fd);
1797
1830
  }
1798
1831
  }
1799
1832
 
@@ -1836,7 +1869,7 @@ async function addCommand(items = []) {
1836
1869
  if (item.startsWith("+")) {
1837
1870
  const profileName = item.slice(1);
1838
1871
  const profilePath = path14.join(root, "profiles", `${profileName}.yaml`);
1839
- if (!fs11.existsSync(profilePath)) {
1872
+ if (!fs12.existsSync(profilePath)) {
1840
1873
  console.error(chalk3.red(` Profile '${profileName}' not found`));
1841
1874
  continue;
1842
1875
  }
@@ -1850,9 +1883,9 @@ async function addCommand(items = []) {
1850
1883
  for (const rule of profile.rules.universal) {
1851
1884
  if (!manifest.installed_rules.includes(rule)) {
1852
1885
  const src = path14.join(root, "rules", `${rule}.md`);
1853
- if (fs11.existsSync(src)) {
1886
+ if (fs12.existsSync(src)) {
1854
1887
  ensureDir(path14.join(claudeDir, "rules"));
1855
- fs11.copyFileSync(src, path14.join(claudeDir, "rules", `${rule}.md`));
1888
+ fs12.copyFileSync(src, path14.join(claudeDir, "rules", `${rule}.md`));
1856
1889
  manifest.installed_rules.push(rule);
1857
1890
  addedRules.push(rule);
1858
1891
  }
@@ -1861,9 +1894,9 @@ async function addCommand(items = []) {
1861
1894
  for (const rule of profile.rules.gamedev) {
1862
1895
  if (!manifest.installed_rules.includes(rule)) {
1863
1896
  const src = path14.join(root, "rules", "gamedev", `${rule}.md`);
1864
- if (fs11.existsSync(src)) {
1897
+ if (fs12.existsSync(src)) {
1865
1898
  ensureDir(path14.join(claudeDir, "rules"));
1866
- fs11.copyFileSync(src, path14.join(claudeDir, "rules", `${rule}.md`));
1899
+ fs12.copyFileSync(src, path14.join(claudeDir, "rules", `${rule}.md`));
1867
1900
  manifest.installed_rules.push(rule);
1868
1901
  addedRules.push(rule);
1869
1902
  }
@@ -1880,8 +1913,8 @@ async function addCommand(items = []) {
1880
1913
  }
1881
1914
  if (profileName === "statusline") {
1882
1915
  const statuslineSrc = path14.join(root, "hooks", "statusline.sh");
1883
- if (fs11.existsSync(statuslineSrc)) {
1884
- fs11.copyFileSync(statuslineSrc, path14.join(claudeDir, "statusline.sh"));
1916
+ if (fs12.existsSync(statuslineSrc)) {
1917
+ fs12.copyFileSync(statuslineSrc, path14.join(claudeDir, "statusline.sh"));
1885
1918
  statuslineAdded = true;
1886
1919
  }
1887
1920
  }
@@ -1890,7 +1923,7 @@ async function addCommand(items = []) {
1890
1923
  }
1891
1924
  if (!manifest.profiles.includes(profileName)) {
1892
1925
  manifest.profiles.push(profileName);
1893
- manifest.profile_command = manifest.profiles.filter((p2) => p2 !== "core").join("+");
1926
+ manifest.profile_command = manifest.profiles.filter((p3) => p3 !== "core").join("+");
1894
1927
  }
1895
1928
  if (statuslineAdded) {
1896
1929
  addStatuslineToSettings(claudeDir);
@@ -1994,14 +2027,14 @@ function printCatalog(root, installedProfiles, installedSkills) {
1994
2027
  function addSkill(root, target, skill, installed) {
1995
2028
  if (installed.includes(skill)) return "skipped";
1996
2029
  const src = path14.join(root, "skills", skill);
1997
- if (!fs11.existsSync(src)) return "not-found";
2030
+ if (!fs12.existsSync(src)) return "not-found";
1998
2031
  const dst = path14.join(target, ".claude", "skills", skill);
1999
2032
  copyDirSync(src, dst);
2000
2033
  return "added";
2001
2034
  }
2002
2035
  function mergePermissions(claudeDir, newPerms) {
2003
2036
  const settingsPath = path14.join(claudeDir, "settings.json");
2004
- if (!fs11.existsSync(settingsPath)) return;
2037
+ if (!fs12.existsSync(settingsPath)) return;
2005
2038
  const settings = readJsonSync(settingsPath);
2006
2039
  const perms = settings.permissions ?? { allow: [], deny: [] };
2007
2040
  const allowSet = new Set(perms.allow);
@@ -2015,7 +2048,7 @@ function mergePermissions(claudeDir, newPerms) {
2015
2048
  }
2016
2049
  function addStatuslineToSettings(claudeDir) {
2017
2050
  const settingsPath = path14.join(claudeDir, "settings.json");
2018
- if (!fs11.existsSync(settingsPath)) return;
2051
+ if (!fs12.existsSync(settingsPath)) return;
2019
2052
  const settings = readJsonSync(settingsPath);
2020
2053
  if (settings.statusLine) return;
2021
2054
  settings.statusLine = {
@@ -2026,7 +2059,7 @@ function addStatuslineToSettings(claudeDir) {
2026
2059
  }
2027
2060
 
2028
2061
  // src/commands/remove.ts
2029
- import fs12 from "fs";
2062
+ import fs13 from "fs";
2030
2063
  import path15 from "path";
2031
2064
  import chalk4 from "chalk";
2032
2065
  var CORE_SKILLS = [
@@ -2097,8 +2130,8 @@ async function removeCommand(items, opts = {}) {
2097
2130
  const totalRemoved = removedSkills.length + removedAgents.length + removedProfiles.length;
2098
2131
  console.log(chalk4.bold(`
2099
2132
  Removed ${totalRemoved} items:`));
2100
- for (const p2 of removedProfiles)
2101
- console.log(chalk4.magenta(` [ok] +${p2} (profile)`));
2133
+ for (const p3 of removedProfiles)
2134
+ console.log(chalk4.magenta(` [ok] +${p3} (profile)`));
2102
2135
  for (const s of removedSkills)
2103
2136
  console.log(chalk4.green(` [ok] ${s} (skill)`));
2104
2137
  for (const a of removedAgents)
@@ -2114,8 +2147,8 @@ function removeSkill(target, skill, manifest) {
2114
2147
  return "skipped";
2115
2148
  }
2116
2149
  const skillDir = path15.join(target, ".claude", "skills", skill);
2117
- if (!fs12.existsSync(skillDir)) return "skipped";
2118
- fs12.rmSync(skillDir, { recursive: true, force: true });
2150
+ if (!fs13.existsSync(skillDir)) return "skipped";
2151
+ fs13.rmSync(skillDir, { recursive: true, force: true });
2119
2152
  manifest.installed_skills = manifest.installed_skills.filter(
2120
2153
  (s) => s !== skill
2121
2154
  );
@@ -2135,7 +2168,7 @@ function removeProfile(root, target, profileName, manifest) {
2135
2168
  return { removed: false, skills: [], agents: [] };
2136
2169
  }
2137
2170
  const profilePath = path15.join(root, "profiles", `${profileName}.yaml`);
2138
- if (!fs12.existsSync(profilePath)) {
2171
+ if (!fs13.existsSync(profilePath)) {
2139
2172
  console.warn(
2140
2173
  chalk4.yellow(` WARNING: Profile '${profileName}.yaml' not found.`)
2141
2174
  );
@@ -2143,14 +2176,14 @@ function removeProfile(root, target, profileName, manifest) {
2143
2176
  }
2144
2177
  const profile = parseProfile(profilePath);
2145
2178
  const remainingProfiles = manifest.profiles.filter(
2146
- (p2) => p2 !== profileName
2179
+ (p3) => p3 !== profileName
2147
2180
  );
2148
2181
  const sharedSkills = /* @__PURE__ */ new Set();
2149
2182
  const sharedAgents = /* @__PURE__ */ new Set();
2150
2183
  const sharedRules = /* @__PURE__ */ new Set();
2151
2184
  for (const rp of remainingProfiles) {
2152
2185
  const rpPath = path15.join(root, "profiles", `${rp}.yaml`);
2153
- if (!fs12.existsSync(rpPath)) continue;
2186
+ if (!fs13.existsSync(rpPath)) continue;
2154
2187
  const rpDef = parseProfile(rpPath);
2155
2188
  rpDef.skills.forEach((s) => sharedSkills.add(s));
2156
2189
  rpDef.agents.forEach((a) => sharedAgents.add(a));
@@ -2162,8 +2195,8 @@ function removeProfile(root, target, profileName, manifest) {
2162
2195
  if (sharedSkills.has(skill)) continue;
2163
2196
  if (CORE_SKILLS.includes(skill)) continue;
2164
2197
  const skillDir = path15.join(target, ".claude", "skills", skill);
2165
- if (fs12.existsSync(skillDir)) {
2166
- fs12.rmSync(skillDir, { recursive: true, force: true });
2198
+ if (fs13.existsSync(skillDir)) {
2199
+ fs13.rmSync(skillDir, { recursive: true, force: true });
2167
2200
  removedSkills.push(skill);
2168
2201
  }
2169
2202
  manifest.installed_skills = manifest.installed_skills.filter(
@@ -2187,8 +2220,8 @@ function removeProfile(root, target, profileName, manifest) {
2187
2220
  ]) {
2188
2221
  if (sharedRules.has(rule)) continue;
2189
2222
  const ruleFile = path15.join(target, ".claude", "rules", `${rule}.md`);
2190
- if (fs12.existsSync(ruleFile)) {
2191
- fs12.rmSync(ruleFile);
2223
+ if (fs13.existsSync(ruleFile)) {
2224
+ fs13.rmSync(ruleFile);
2192
2225
  }
2193
2226
  manifest.installed_rules = manifest.installed_rules.filter(
2194
2227
  (r) => r !== rule
@@ -2196,27 +2229,27 @@ function removeProfile(root, target, profileName, manifest) {
2196
2229
  }
2197
2230
  if (profileName === "statusline") {
2198
2231
  const statuslineFile = path15.join(target, ".claude", "statusline.sh");
2199
- if (fs12.existsSync(statuslineFile)) {
2200
- fs12.rmSync(statuslineFile);
2232
+ if (fs13.existsSync(statuslineFile)) {
2233
+ fs13.rmSync(statuslineFile);
2201
2234
  }
2202
2235
  removeStatuslineFromSettings(path15.join(target, ".claude"));
2203
2236
  }
2204
2237
  manifest.profiles = remainingProfiles;
2205
- manifest.profile_command = remainingProfiles.filter((p2) => p2 !== "core").join("+");
2238
+ manifest.profile_command = remainingProfiles.filter((p3) => p3 !== "core").join("+");
2206
2239
  return { removed: true, skills: removedSkills, agents: removedAgents };
2207
2240
  }
2208
2241
  function removeStatuslineFromSettings(claudeDir) {
2209
2242
  const settingsPath = path15.join(claudeDir, "settings.json");
2210
- if (!fs12.existsSync(settingsPath)) return;
2211
- const settings = JSON.parse(fs12.readFileSync(settingsPath, "utf-8"));
2243
+ if (!fs13.existsSync(settingsPath)) return;
2244
+ const settings = JSON.parse(fs13.readFileSync(settingsPath, "utf-8"));
2212
2245
  if (settings.statusLine) {
2213
2246
  delete settings.statusLine;
2214
- fs12.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
2247
+ fs13.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
2215
2248
  }
2216
2249
  }
2217
2250
 
2218
2251
  // src/commands/list.ts
2219
- import fs13 from "fs";
2252
+ import fs14 from "fs";
2220
2253
  import path16 from "path";
2221
2254
  import chalk5 from "chalk";
2222
2255
  async function listCommand() {
@@ -2232,15 +2265,15 @@ async function listCommand() {
2232
2265
  console.log(chalk5.dim("Profiles combine freely: install a+b+c\n"));
2233
2266
  for (const group of groupByCategory(profiles)) {
2234
2267
  console.log(chalk5.cyan(`${group.label}:`));
2235
- for (const p2 of group.profiles) {
2236
- const tag = manifest?.profiles.includes(p2.name) ? chalk5.green(" [installed]") : "";
2268
+ for (const p3 of group.profiles) {
2269
+ const tag = manifest?.profiles.includes(p3.name) ? chalk5.green(" [installed]") : "";
2237
2270
  console.log(
2238
- ` ${chalk5.green(p2.name.padEnd(20))} ${p2.description}${tag}`
2271
+ ` ${chalk5.green(p3.name.padEnd(20))} ${p3.description}${tag}`
2239
2272
  );
2240
2273
  }
2241
2274
  console.log();
2242
2275
  }
2243
- if (fs13.existsSync(skillsDir)) {
2276
+ if (fs14.existsSync(skillsDir)) {
2244
2277
  const allSkills = listSkills(skillsDir).map((s) => s.name);
2245
2278
  console.log(chalk5.bold(`
2246
2279
  === Skills (${allSkills.length}) ===
@@ -2271,7 +2304,7 @@ async function statusCommand() {
2271
2304
  console.log(chalk6.dim("Run `npx claude-code-arcane install <profile>` to install."));
2272
2305
  return;
2273
2306
  }
2274
- const profiles = manifest.profiles.filter((p2) => p2 !== "core");
2307
+ const profiles = manifest.profiles.filter((p3) => p3 !== "core");
2275
2308
  console.log(chalk6.bold("\n=== Arcane Status ===\n"));
2276
2309
  console.log(` Profiles: ${chalk6.cyan(profiles.join(" + "))}`);
2277
2310
  console.log(` Installed: ${manifest.installed_at}`);
@@ -2307,20 +2340,23 @@ async function statusCommand() {
2307
2340
  }
2308
2341
 
2309
2342
  // src/commands/update.ts
2310
- import fs14 from "fs";
2343
+ import fs15 from "fs";
2311
2344
  import path17 from "path";
2312
2345
  import os3 from "os";
2313
2346
  import chalk7 from "chalk";
2314
2347
 
2315
2348
  // src/self-update.ts
2316
2349
  import { spawnSync } from "child_process";
2317
- var PACKAGE_NAME = "claude-code-arcane";
2350
+ var PACKAGE_NAME2 = "claude-code-arcane";
2318
2351
  function isGloballyInstalled() {
2319
2352
  const root = getPackageRoot().replace(/\\/g, "/");
2320
2353
  if (root.includes("/_npx/")) return false;
2321
2354
  if (!root.includes("/node_modules/")) return false;
2322
2355
  return true;
2323
2356
  }
2357
+ function cliInvocation() {
2358
+ return isGloballyInstalled() ? "arcane" : `npx ${PACKAGE_NAME2}`;
2359
+ }
2324
2360
  async function selfUpdateNpm(opts = {}) {
2325
2361
  const fromVersion = safeVersion();
2326
2362
  if (opts.selfUpdate === false) {
@@ -2349,7 +2385,7 @@ async function selfUpdateNpm(opts = {}) {
2349
2385
  return { updated: false, skipped: false, reason, fromVersion };
2350
2386
  }
2351
2387
  function npmInstallCommand(platform = process.platform) {
2352
- const args = ["install", "-g", `${PACKAGE_NAME}@latest`];
2388
+ const args = ["install", "-g", `${PACKAGE_NAME2}@latest`];
2353
2389
  if (platform === "win32") {
2354
2390
  return { command: ["npm", ...args].join(" "), args: [], shell: true };
2355
2391
  }
@@ -2385,17 +2421,22 @@ async function updateCommand(opts) {
2385
2421
  }
2386
2422
  }
2387
2423
  const cwd = process.cwd();
2388
- if (fs14.existsSync(manifestPath(cwd))) {
2424
+ if (fs15.existsSync(manifestPath(cwd))) {
2389
2425
  registerInstallation(cwd);
2390
2426
  }
2391
2427
  const targets = pruneRegistry().map((e) => e.path);
2392
2428
  const globalTarget = os3.homedir();
2393
- if (!process.env.VITEST && fs14.existsSync(manifestPath(globalTarget)) && !targets.includes(globalTarget)) {
2429
+ if (!process.env.VITEST && fs15.existsSync(manifestPath(globalTarget)) && !targets.includes(globalTarget)) {
2394
2430
  targets.push(globalTarget);
2395
2431
  }
2396
2432
  if (targets.length === 0) {
2397
2433
  if (!opts.quiet) {
2398
- console.log(chalk7.red("\nNo Arcane installations found. Run 'arcane install' first."));
2434
+ console.log(
2435
+ chalk7.red(
2436
+ `
2437
+ No Arcane installations found. Run \`${cliInvocation()} install\` first.`
2438
+ )
2439
+ );
2399
2440
  }
2400
2441
  return;
2401
2442
  }
@@ -2438,7 +2479,11 @@ async function updateTarget(target, opts, sharedSource) {
2438
2479
  const manifest = readManifest(target);
2439
2480
  if (!manifest) {
2440
2481
  if (!opts.quiet) {
2441
- console.log(chalk7.red("No Arcane installation found. Run 'arcane install' first."));
2482
+ console.log(
2483
+ chalk7.red(
2484
+ `No Arcane installation found. Run \`${cliInvocation()} install\` first.`
2485
+ )
2486
+ );
2442
2487
  }
2443
2488
  return { target, status: "no-manifest", updated: 0, skipped: 0, removed: 0 };
2444
2489
  }
@@ -2770,7 +2815,7 @@ function applyUpdates(items, root, claudeDir, merged, force) {
2770
2815
  function syncUnhashedFiles(root, claudeDir, merged, dryRun) {
2771
2816
  const targets = [];
2772
2817
  const statuslineDst = path17.join(claudeDir, "statusline.sh");
2773
- if (merged.loaded.includes("statusline") || fs14.existsSync(statuslineDst)) {
2818
+ if (merged.loaded.includes("statusline") || fs15.existsSync(statuslineDst)) {
2774
2819
  targets.push({
2775
2820
  src: path17.join(root, "hooks", "statusline.sh"),
2776
2821
  dst: statuslineDst,
@@ -2795,10 +2840,10 @@ function syncUnhashedFiles(root, claudeDir, merged, dryRun) {
2795
2840
  }
2796
2841
  if (merged.rules.gamedev.length > 0) {
2797
2842
  const templatesSrc = path17.join(root, "templates", "gamedev");
2798
- if (fs14.existsSync(templatesSrc)) {
2799
- for (const rel of fs14.readdirSync(templatesSrc, { recursive: true })) {
2843
+ if (fs15.existsSync(templatesSrc)) {
2844
+ for (const rel of fs15.readdirSync(templatesSrc, { recursive: true })) {
2800
2845
  const src = path17.join(templatesSrc, rel);
2801
- if (!fs14.statSync(src).isFile()) continue;
2846
+ if (!fs15.statSync(src).isFile()) continue;
2802
2847
  targets.push({
2803
2848
  src,
2804
2849
  dst: path17.join(claudeDir, "skills", "_templates", rel),
@@ -2809,30 +2854,30 @@ function syncUnhashedFiles(root, claudeDir, merged, dryRun) {
2809
2854
  }
2810
2855
  const changed = [];
2811
2856
  for (const t of targets) {
2812
- if (!fs14.existsSync(t.src)) continue;
2813
- const src = fs14.readFileSync(t.src);
2814
- if (fs14.existsSync(t.dst) && src.equals(fs14.readFileSync(t.dst))) continue;
2857
+ if (!fs15.existsSync(t.src)) continue;
2858
+ const src = fs15.readFileSync(t.src);
2859
+ if (fs15.existsSync(t.dst) && src.equals(fs15.readFileSync(t.dst))) continue;
2815
2860
  changed.push(t.label);
2816
2861
  if (dryRun) continue;
2817
- if (fs14.existsSync(t.dst)) {
2862
+ if (fs15.existsSync(t.dst)) {
2818
2863
  const bak = `${t.dst}.bak`;
2819
- if (fs14.existsSync(bak)) fs14.rmSync(bak, { force: true });
2820
- fs14.renameSync(t.dst, bak);
2864
+ if (fs15.existsSync(bak)) fs15.rmSync(bak, { force: true });
2865
+ fs15.renameSync(t.dst, bak);
2821
2866
  }
2822
- fs14.mkdirSync(path17.dirname(t.dst), { recursive: true });
2823
- fs14.copyFileSync(t.src, t.dst);
2867
+ fs15.mkdirSync(path17.dirname(t.dst), { recursive: true });
2868
+ fs15.copyFileSync(t.src, t.dst);
2824
2869
  }
2825
2870
  const activeStyle = merged.output_styles[0];
2826
2871
  if (activeStyle) {
2827
2872
  const settingsPath = path17.join(claudeDir, "settings.json");
2828
- if (fs14.existsSync(settingsPath)) {
2873
+ if (fs15.existsSync(settingsPath)) {
2829
2874
  try {
2830
- const settings = JSON.parse(fs14.readFileSync(settingsPath, "utf-8"));
2875
+ const settings = JSON.parse(fs15.readFileSync(settingsPath, "utf-8"));
2831
2876
  if (settings.outputStyle !== activeStyle) {
2832
2877
  changed.push(`settings.json (outputStyle: ${activeStyle})`);
2833
2878
  if (!dryRun) {
2834
2879
  settings.outputStyle = activeStyle;
2835
- fs14.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
2880
+ fs15.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
2836
2881
  `, "utf-8");
2837
2882
  }
2838
2883
  }
@@ -2845,29 +2890,29 @@ function syncUnhashedFiles(root, claudeDir, merged, dryRun) {
2845
2890
  function applyItem(item, root, claudeDir) {
2846
2891
  const src = getSourcePath(item, root);
2847
2892
  const dst = getInstalledPath(item, claudeDir);
2848
- if (!src || !fs14.existsSync(src)) return;
2893
+ if (!src || !fs15.existsSync(src)) return;
2849
2894
  if (item.type === "skill" || item.type === "agent") {
2850
- if (fs14.existsSync(dst)) {
2851
- fs14.rmSync(dst, { recursive: true, force: true });
2895
+ if (fs15.existsSync(dst)) {
2896
+ fs15.rmSync(dst, { recursive: true, force: true });
2852
2897
  }
2853
2898
  copyDirSync(src, dst);
2854
2899
  } else {
2855
2900
  const dir = path17.dirname(dst);
2856
- if (!fs14.existsSync(dir)) {
2857
- fs14.mkdirSync(dir, { recursive: true });
2901
+ if (!fs15.existsSync(dir)) {
2902
+ fs15.mkdirSync(dir, { recursive: true });
2858
2903
  }
2859
- fs14.copyFileSync(src, dst);
2904
+ fs15.copyFileSync(src, dst);
2860
2905
  }
2861
2906
  }
2862
2907
  function quarantineItem(item, claudeDir) {
2863
2908
  const installed = getInstalledPath(item, claudeDir);
2864
- if (!fs14.existsSync(installed)) return;
2909
+ if (!fs15.existsSync(installed)) return;
2865
2910
  const quarantine = path17.join(claudeDir, TRASH_DIR, `${item.type}s`, item.name);
2866
2911
  ensureDir(path17.dirname(quarantine));
2867
- if (fs14.existsSync(quarantine)) {
2868
- fs14.rmSync(quarantine, { recursive: true, force: true });
2912
+ if (fs15.existsSync(quarantine)) {
2913
+ fs15.rmSync(quarantine, { recursive: true, force: true });
2869
2914
  }
2870
- fs14.renameSync(installed, quarantine);
2915
+ fs15.renameSync(installed, quarantine);
2871
2916
  }
2872
2917
  function getSourcePath(item, root) {
2873
2918
  switch (item.type) {
@@ -2875,7 +2920,7 @@ function getSourcePath(item, root) {
2875
2920
  return path17.join(root, "skills", item.name);
2876
2921
  case "rule": {
2877
2922
  const direct = path17.join(root, "rules", item.name);
2878
- if (fs14.existsSync(direct)) return direct;
2923
+ if (fs15.existsSync(direct)) return direct;
2879
2924
  return path17.join(root, "rules", "gamedev", item.name);
2880
2925
  }
2881
2926
  case "agent":
@@ -2930,7 +2975,7 @@ async function cleanCommand(opts) {
2930
2975
  }
2931
2976
 
2932
2977
  // src/commands/worktree.ts
2933
- import fs15 from "fs";
2978
+ import fs16 from "fs";
2934
2979
  import path18 from "path";
2935
2980
  import { execSync as execSync2 } from "child_process";
2936
2981
  import chalk9 from "chalk";
@@ -2943,7 +2988,7 @@ async function worktreeCommand(branch, opts) {
2943
2988
  }
2944
2989
  const repoRoot = wtInfo.isWorktree ? wtInfo.mainWorktreePath : cwd;
2945
2990
  const worktreePath = opts.path ? path18.resolve(opts.path) : defaultWorktreePath(repoRoot, branch);
2946
- if (fs15.existsSync(worktreePath)) {
2991
+ if (fs16.existsSync(worktreePath)) {
2947
2992
  console.error(
2948
2993
  chalk9.red(`Path already exists: ${worktreePath}`)
2949
2994
  );
@@ -3032,7 +3077,7 @@ async function worktreeCommand(branch, opts) {
3032
3077
  "skills",
3033
3078
  "worktree-isolation"
3034
3079
  );
3035
- if (fs15.existsSync(isolateScript)) {
3080
+ if (fs16.existsSync(isolateScript)) {
3036
3081
  console.log(" Applying worktree isolation...");
3037
3082
  try {
3038
3083
  execSync2(
@@ -3067,7 +3112,7 @@ async function worktreeCommand(branch, opts) {
3067
3112
  }
3068
3113
 
3069
3114
  // src/commands/global.ts
3070
- import fs16 from "fs";
3115
+ import fs17 from "fs";
3071
3116
  import path19 from "path";
3072
3117
  import os4 from "os";
3073
3118
  import chalk10 from "chalk";
@@ -3087,7 +3132,7 @@ async function globalCommand(opts) {
3087
3132
  }
3088
3133
  function showStatus() {
3089
3134
  console.log(chalk10.bold("\n=== Arcane Global Status ===\n"));
3090
- const hasScripts = fs16.existsSync(
3135
+ const hasScripts = fs17.existsSync(
3091
3136
  path19.join(SCRIPTS_DIR, "apply.py")
3092
3137
  );
3093
3138
  console.log(
@@ -3112,26 +3157,26 @@ function installGlobal() {
3112
3157
  "worktree-isolation",
3113
3158
  "scripts"
3114
3159
  );
3115
- if (!fs16.existsSync(scriptsSrc)) {
3160
+ if (!fs17.existsSync(scriptsSrc)) {
3116
3161
  console.error(
3117
3162
  chalk10.red(" worktree-isolation scripts not found in package.")
3118
3163
  );
3119
3164
  process.exit(1);
3120
3165
  }
3121
- fs16.mkdirSync(SCRIPTS_DIR, { recursive: true });
3166
+ fs17.mkdirSync(SCRIPTS_DIR, { recursive: true });
3122
3167
  for (const file of ["apply.py", "lib.py"]) {
3123
3168
  const src = path19.join(scriptsSrc, file);
3124
3169
  const dst = path19.join(SCRIPTS_DIR, file);
3125
- if (!fs16.existsSync(src)) {
3170
+ if (!fs17.existsSync(src)) {
3126
3171
  console.warn(chalk10.yellow(` WARN: ${file} not found, skipping`));
3127
3172
  continue;
3128
3173
  }
3129
- if (fs16.existsSync(dst) && fs16.readFileSync(src).equals(fs16.readFileSync(dst))) {
3174
+ if (fs17.existsSync(dst) && fs17.readFileSync(src).equals(fs17.readFileSync(dst))) {
3130
3175
  console.log(chalk10.dim(` [skip] ${file} (already up-to-date)`));
3131
3176
  } else {
3132
- fs16.copyFileSync(src, dst);
3177
+ fs17.copyFileSync(src, dst);
3133
3178
  if (process.platform !== "win32") {
3134
- fs16.chmodSync(dst, 493);
3179
+ fs17.chmodSync(dst, 493);
3135
3180
  }
3136
3181
  console.log(chalk10.green(` [ok] ${file} -> ${SCRIPTS_DIR}`));
3137
3182
  }
@@ -3155,8 +3200,8 @@ function installGlobal() {
3155
3200
  }
3156
3201
  function removeGlobal() {
3157
3202
  console.log(chalk10.bold("\nRemoving Arcane global hooks...\n"));
3158
- if (fs16.existsSync(SCRIPTS_DIR)) {
3159
- fs16.rmSync(SCRIPTS_DIR, { recursive: true, force: true });
3203
+ if (fs17.existsSync(SCRIPTS_DIR)) {
3204
+ fs17.rmSync(SCRIPTS_DIR, { recursive: true, force: true });
3160
3205
  console.log(chalk10.green(" [ok] Removed worktree-isolation scripts"));
3161
3206
  } else {
3162
3207
  console.log(chalk10.dim(" [skip] Scripts not found"));
@@ -3170,16 +3215,16 @@ function removeGlobal() {
3170
3215
  console.log(chalk10.bold.green("\n Global hooks removed.\n"));
3171
3216
  }
3172
3217
  function readSettings() {
3173
- if (!fs16.existsSync(SETTINGS_PATH)) return {};
3218
+ if (!fs17.existsSync(SETTINGS_PATH)) return {};
3174
3219
  try {
3175
- return JSON.parse(fs16.readFileSync(SETTINGS_PATH, "utf-8"));
3220
+ return JSON.parse(fs17.readFileSync(SETTINGS_PATH, "utf-8"));
3176
3221
  } catch {
3177
3222
  return {};
3178
3223
  }
3179
3224
  }
3180
3225
  function writeSettings(settings) {
3181
- fs16.mkdirSync(path19.dirname(SETTINGS_PATH), { recursive: true });
3182
- fs16.writeFileSync(
3226
+ fs17.mkdirSync(path19.dirname(SETTINGS_PATH), { recursive: true });
3227
+ fs17.writeFileSync(
3183
3228
  SETTINGS_PATH,
3184
3229
  JSON.stringify(settings, null, 2) + "\n",
3185
3230
  "utf-8"
@@ -3236,7 +3281,7 @@ function removeHookFromSettings() {
3236
3281
  }
3237
3282
 
3238
3283
  // src/update-check.ts
3239
- import fs17 from "fs";
3284
+ import fs18 from "fs";
3240
3285
  import path20 from "path";
3241
3286
  import chalk11 from "chalk";
3242
3287
  function checkFile() {
@@ -3246,7 +3291,7 @@ var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
3246
3291
  var GITHUB_OWNER2 = "SebastianLuser";
3247
3292
  var GITHUB_REPO2 = "Claude-Code-Arcane";
3248
3293
  var GITHUB_BRANCH2 = "main";
3249
- var PACKAGE_NAME2 = "claude-code-arcane";
3294
+ var PACKAGE_NAME3 = "claude-code-arcane";
3250
3295
  var SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
3251
3296
  function compareSemver(a, b) {
3252
3297
  const release = (v) => v.trim().replace(/^v/, "").split(/[-+]/)[0];
@@ -3315,7 +3360,7 @@ function printUpdateNotice(result) {
3315
3360
  Arcane CLI update: ${result.cli_version} \u2192 ${result.latest_cli_version}`
3316
3361
  )
3317
3362
  );
3318
- const how = isGloballyInstalled() ? ` Run: npm install -g ${PACKAGE_NAME2}@latest` : ` You are on an older CLI. Latest: ${PACKAGE_NAME2}@${result.latest_cli_version}`;
3363
+ const how = isGloballyInstalled() ? ` Run: npm install -g ${PACKAGE_NAME3}@latest` : ` You are on an older CLI. Latest: ${PACKAGE_NAME3}@${result.latest_cli_version}`;
3319
3364
  console.log(chalk11.dim(how));
3320
3365
  }
3321
3366
  if (result.content_update_available ?? true) {
@@ -3325,7 +3370,9 @@ function printUpdateNotice(result) {
3325
3370
  Arcane content update: ${result.local_version} \u2192 ${result.remote_sha}`
3326
3371
  )
3327
3372
  );
3328
- console.log(chalk11.dim(" Run: arcane update (--dry-run to preview)"));
3373
+ console.log(
3374
+ chalk11.dim(` Run: ${cliInvocation()} update (--dry-run to preview)`)
3375
+ );
3329
3376
  }
3330
3377
  console.log("");
3331
3378
  }
@@ -3351,7 +3398,7 @@ async function getLatestCommitSha() {
3351
3398
  async function getLatestNpmVersion() {
3352
3399
  try {
3353
3400
  const response = await fetch(
3354
- `https://registry.npmjs.org/${PACKAGE_NAME2}/latest`,
3401
+ `https://registry.npmjs.org/${PACKAGE_NAME3}/latest`,
3355
3402
  {
3356
3403
  headers: { "User-Agent": "arcane-cli", Accept: "application/json" },
3357
3404
  signal: AbortSignal.timeout(5e3)
@@ -3367,8 +3414,8 @@ async function getLatestNpmVersion() {
3367
3414
  function readCachedCheck() {
3368
3415
  try {
3369
3416
  const file = checkFile();
3370
- if (!fs17.existsSync(file)) return null;
3371
- return JSON.parse(fs17.readFileSync(file, "utf-8"));
3417
+ if (!fs18.existsSync(file)) return null;
3418
+ return JSON.parse(fs18.readFileSync(file, "utf-8"));
3372
3419
  } catch {
3373
3420
  return null;
3374
3421
  }
@@ -3377,10 +3424,10 @@ function writeCachedCheck(result) {
3377
3424
  try {
3378
3425
  const file = checkFile();
3379
3426
  const dir = path20.dirname(file);
3380
- if (!fs17.existsSync(dir)) {
3381
- fs17.mkdirSync(dir, { recursive: true });
3427
+ if (!fs18.existsSync(dir)) {
3428
+ fs18.mkdirSync(dir, { recursive: true });
3382
3429
  }
3383
- fs17.writeFileSync(file, JSON.stringify(result, null, 2) + "\n");
3430
+ fs18.writeFileSync(file, JSON.stringify(result, null, 2) + "\n");
3384
3431
  } catch {
3385
3432
  }
3386
3433
  }
@@ -55,10 +55,9 @@ Si podés hacerlo con la stdlib de Python, la escotilla no aplica.
55
55
 
56
56
  ## Comentarios
57
57
 
58
- - **Escribí el porqué, no el qué.** El código ya dice qué hace.
59
- - **Doc comments para APIs públicas.** Las privadas no los necesitan.
60
- - **TODOs con owner y fecha:** `// TODO(@alice, 2026-05-01): descripción`
61
- - **Anti-pattern:** comentarios que explican el obvio (`// increment counter` sobre `counter++`)
58
+ La regla vive en `.claude/rules/code-comments.md` y es la única autoridad.
59
+ No dupliques criterios acá: dos listas sobre comentarios siempre terminan
60
+ contradiciéndose, y gana la permisiva.
62
61
 
63
62
  ---
64
63
 
@@ -145,17 +144,6 @@ Antes de marcar trabajo como completo:
145
144
 
146
145
  ---
147
146
 
148
- ## Comments / Docs Anti-Patterns
147
+ ## Comments / Docs
149
148
 
150
- **NO escribir:**
151
- - Comentarios que describen WHAT (el código ya dice)
152
- - Comentarios referenciando el task/PR actual ("added for JIRA-123")
153
- - Docstrings multi-párrafo en funciones simples
154
- - Comentarios `// removed X` para código borrado
155
- - `// used by Y` (el tooling lo dice mejor)
156
-
157
- **SÍ escribir comentarios cuando:**
158
- - Explican el WHY que no es obvio
159
- - Documentan workaround de un bug específico (con link al bug)
160
- - Marcan invariantes o preconditions no-obvios
161
- - APIs públicas consumidas por terceros
149
+ Ver `.claude/rules/code-comments.md`.
@@ -7,6 +7,16 @@ CHECK_FILE="$HOME/.arcane/last-check.json"
7
7
  CHECK_INTERVAL=14400 # 4 hours in seconds
8
8
  GITHUB_API="https://api.github.com/repos/SebastianLuser/Claude-Code-Arcane/commits/main"
9
9
 
10
+ # `arcane` is the bin name, so it only exists on PATH after `npm i -g`. The
11
+ # README installs with npx, so most projects running this hook have no `arcane`
12
+ # to run: printing it hands them "'arcane' is not recognized" instead of an
13
+ # update. Unlike the CLI, a hook can test the real thing rather than infer it.
14
+ if command -v arcane &>/dev/null; then
15
+ ARCANE_CMD="arcane"
16
+ else
17
+ ARCANE_CMD="npx claude-code-arcane"
18
+ fi
19
+
10
20
  main() {
11
21
  if [ ! -f ".claude/arcane-manifest.json" ]; then
12
22
  exit 0
@@ -34,7 +44,7 @@ except:
34
44
 
35
45
  case "$LAST_CHECK" in
36
46
  UPDATE_AVAILABLE)
37
- echo "Arcane update available. Run: arcane update"
47
+ echo "Arcane update available. Run: $ARCANE_CMD update"
38
48
  exit 0
39
49
  ;;
40
50
  UP_TO_DATE)
@@ -61,10 +71,18 @@ except:
61
71
  print('unknown')
62
72
  " 2>/dev/null)
63
73
 
74
+ # Parity with isContentUpdateAvailable() in src/update-check.ts: the remote content
75
+ # identity is a commit SHA, so only a SHA-shaped local version can be compared against
76
+ # it. A bundled install — which is every `npx claude-code-arcane install` — records a
77
+ # semver instead, and "2.9.6" never equals a SHA. The CLI got this guard; the hook did
78
+ # not, so it announced an update on every single session no matter how current the
79
+ # install was, and pointed at a command npx users do not have.
64
80
  UPDATE_AVAILABLE="false"
65
- if [ "$LOCAL_VERSION" != "$REMOTE_SHA" ] && ! echo "$REMOTE_SHA" | grep -q "^$LOCAL_VERSION"; then
81
+ if ! echo "$LOCAL_VERSION" | grep -qiE '^[0-9a-f]{7,40}$'; then
82
+ UPDATE_AVAILABLE="false"
83
+ elif [ "$LOCAL_VERSION" != "$REMOTE_SHA" ] && ! echo "$REMOTE_SHA" | grep -q "^$LOCAL_VERSION"; then
66
84
  UPDATE_AVAILABLE="true"
67
- echo "Arcane update available: $LOCAL_VERSION → $REMOTE_SHA. Run: arcane update"
85
+ echo "Arcane update available: $LOCAL_VERSION → $REMOTE_SHA. Run: $ARCANE_CMD update"
68
86
  fi
69
87
 
70
88
  mkdir -p "$HOME/.arcane" 2>/dev/null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-arcane",
3
- "version": "2.9.5",
3
+ "version": "2.9.7",
4
4
  "description": "Skills, agents, hooks and rules for Claude Code — installable via npx claude-code-arcane",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,13 +50,19 @@ Comments are written in **English**, whatever language the conversation is in.
50
50
 
51
51
  ## Write
52
52
 
53
- - **The WHY that the code cannot show.** A constant that looks arbitrary, an ordering that
54
- matters, a deliberate break from the surrounding pattern.
53
+ **The default is no comment.** The code says what it does and the commit says why it changed.
54
+ A new comment has to earn its place; it is never the neutral choice.
55
+
56
+ The whole list of exceptions:
57
+
58
+ - **Doc comments on public APIs** consumed from outside the module, one line. Private ones
59
+ need none.
55
60
  - **Workarounds**, with a link to the upstream bug.
56
- - **Non-obvious invariants or preconditions** a caller has to respect.
57
- - **Doc comments on public APIs** consumed from outside the module. Private ones need none.
58
61
  - **`TODO(@owner, YYYY-MM-DD): description`.** Never a bare `TODO`.
59
62
 
63
+ A comment shaped as a comparison with what used to be there — "X and not Y", "no longer X",
64
+ "now uses X" — is session narration wearing a technical disguise. It belongs in the commit.
65
+
60
66
  ## Shape
61
67
 
62
68
  One line, unless the reason honestly needs two. A comment that needs a paragraph to explain
@@ -15,7 +15,7 @@ paths:
15
15
  - All JSON / YAML / TOML files must parse cleanly — malformed config blocks the build
16
16
  - File naming: lowercase with hyphens or underscores, following `[domain]-[name].[ext]` pattern
17
17
  - Every schema-bearing file must have a documented schema (JSON Schema, OpenAPI, Zod, Pydantic — whatever the stack uses)
18
- - Numeric values with business meaning must include a comment or companion doc explaining unit and intent (`timeout_ms`, not bare `timeout`)
18
+ - Numeric values with business meaning carry the unit in the key (`timeout_ms`, not bare `timeout`)
19
19
  - Use consistent key naming within a file: camelCase for JSON, snake_case for YAML if that's the project convention — don't mix
20
20
  - No orphaned entries — every record must be referenced by code, another config, or explicitly marked as a default/example
21
21
  - Version config files when making breaking schema changes (`v1/`, `v2/` or explicit `schema_version` field)
@@ -9,7 +9,7 @@ paths:
9
9
  - All engine APIs must be thread-safe OR explicitly documented as single-thread-only
10
10
  - Profile before AND after every optimization — document the measured numbers
11
11
  - Engine code must NEVER depend on gameplay code (strict dependency direction: engine <- gameplay)
12
- - Every public API must have usage examples in its doc comment
12
+ - Public API doc comments stay to one line (see `code-comments`); usage examples belong in the docs, not the source
13
13
  - Changes to public interfaces require a deprecation period and migration guide
14
14
  - Use RAII / deterministic cleanup for all resources
15
15
  - All engine systems must support graceful degradation
@@ -18,7 +18,7 @@ visual quality, performance, and cross-platform compatibility.
18
18
  ## Code Quality
19
19
  - All uniforms/parameters must have descriptive names and appropriate hints
20
20
  - Group related parameters (Unity: `[Header]`, Unreal: Category)
21
- - Comment non-obvious calculations (especially math-heavy sections)
21
+ - Name non-obvious calculations rather than commenting them; comments follow `code-comments`
22
22
  - No magic numbers — use named constants or documented uniform values
23
23
 
24
24
  ## Performance Requirements
@@ -40,7 +40,7 @@ The CLI does the same thing interactively: `arcane add` with no args opens the w
40
40
  ## Steps
41
41
 
42
42
  1. Read `.claude/arcane-manifest.json` to find the `source` path
43
- 2. If no manifest, error: "Run `arcane install` first"
43
+ 2. If no manifest, error: "Run `npx claude-code-arcane install` first"
44
44
  3. For each argument:
45
45
  - If it starts with `+`, it's a profile name:
46
46
  - Read `profiles/<name>.yaml` from source
@@ -36,10 +36,33 @@ This will remove:
36
36
  - Delete `.claude/docs/` directory
37
37
  - Delete `.claude/statusline.sh`
38
38
  - Delete `.claude/arcane-manifest.json`
39
- 6. Report: "Arcane removed from project. Run `arcane install` to reinstall."
39
+ 6. Report what went, what stayed, and how to come back:
40
+
41
+ ```
42
+ Removed Arcane from project:
43
+ [ok] 94 skills
44
+ [ok] 8 rules
45
+ [ok] 4 agent dirs
46
+ [ok] hooks/, settings.json, arcane-manifest.json
47
+
48
+ Preserved (not installed by Arcane):
49
+ CLAUDE.md, settings.local.json
50
+
51
+ Run `npx claude-code-arcane install <profile>` to reinstall.
52
+ COMPLETE
53
+ ```
40
54
 
41
55
  ## Safety
42
56
 
43
57
  - ALWAYS ask for confirmation before deleting
44
58
  - If `.claude/` contains files NOT from Arcane (e.g., CLAUDE.md, user configs), preserve them
45
59
  - Only delete directories and files that Arcane created (listed in manifest or known paths)
60
+
61
+ ## Next step
62
+
63
+ This removes **everything**. If the goal was narrower, one of these is the right tool instead:
64
+
65
+ - `/arcane-remove +profile` — drop one profile and only the assets exclusive to it
66
+ - `/arcane-remove <skill>` — drop individual skills
67
+ - `/arcane-status` — check what is installed before deciding
68
+ - `npx claude-code-arcane install <profile>` — reinstall with a different combination