claude-code-arcane 2.9.6 → 2.9.8

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.8](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.9.7...v2.9.8) (2026-09-09)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **statusline:** point the global install at the script it owns ([1a8aada](https://github.com/SebastianLuser/claude-code-arcane/commit/1a8aada612e7f2bad2d60450306c0d51887f6b65))
7
+
8
+ ## [2.9.7](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.9.6...v2.9.7) (2026-09-08)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * make a first-time npx install survivable ([964ef8e](https://github.com/SebastianLuser/claude-code-arcane/commit/964ef8eef9d4ea6a1e593eac189bceb79650174d))
14
+
1
15
  ## [2.9.6](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.9.5...v2.9.6) (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
  }
@@ -395,6 +397,14 @@ import os from "os";
395
397
  function arcaneHome() {
396
398
  return process.env.ARCANE_HOME ?? path3.join(os.homedir(), ".arcane");
397
399
  }
400
+ var RELATIVE_STATUSLINE_COMMAND = "bash .claude/statusline.sh";
401
+ function statuslineCommand(target) {
402
+ if (path3.resolve(target) !== path3.resolve(os.homedir())) {
403
+ return RELATIVE_STATUSLINE_COMMAND;
404
+ }
405
+ const script = path3.join(target, ".claude", "statusline.sh").replace(/\\/g, "/");
406
+ return `bash "${script}"`;
407
+ }
398
408
  function getPackageRoot() {
399
409
  const thisFile = fileURLToPath(import.meta.url);
400
410
  return path3.resolve(path3.dirname(thisFile), "..");
@@ -419,18 +429,18 @@ function copyDirSync(src, dest) {
419
429
  function ensureDir(dir) {
420
430
  fs3.mkdirSync(dir, { recursive: true });
421
431
  }
422
- function fileExists(p2) {
423
- return fs3.existsSync(p2);
432
+ function fileExists(p3) {
433
+ return fs3.existsSync(p3);
424
434
  }
425
- function readJsonSync(p2) {
426
- return JSON.parse(fs3.readFileSync(p2, "utf-8"));
435
+ function readJsonSync(p3) {
436
+ return JSON.parse(fs3.readFileSync(p3, "utf-8"));
427
437
  }
428
- function writeJsonSync(p2, data) {
429
- fs3.writeFileSync(p2, JSON.stringify(data, null, 2) + "\n", "utf-8");
438
+ function writeJsonSync(p3, data) {
439
+ fs3.writeFileSync(p3, JSON.stringify(data, null, 2) + "\n", "utf-8");
430
440
  }
431
- function isSymlinkOrJunction(p2) {
441
+ function isSymlinkOrJunction(p3) {
432
442
  try {
433
- return fs3.lstatSync(p2).isSymbolicLink();
443
+ return fs3.lstatSync(p3).isSymbolicLink();
434
444
  } catch {
435
445
  return false;
436
446
  }
@@ -440,11 +450,11 @@ function getPackageVersion() {
440
450
  const pkg = readJsonSync(pkgPath);
441
451
  return pkg.version;
442
452
  }
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 });
453
+ function safeRemove(p3) {
454
+ if (isSymlinkOrJunction(p3)) {
455
+ fs3.unlinkSync(p3);
456
+ } else if (fs3.existsSync(p3)) {
457
+ fs3.rmSync(p3, { recursive: true, force: true });
448
458
  }
449
459
  }
450
460
 
@@ -466,8 +476,8 @@ function isGranular(raw) {
466
476
  }
467
477
  function agentSourcePath(root, raw) {
468
478
  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;
479
+ const p3 = agent ? path4.join(root, "agents", division, `${agent}.md`) : path4.join(root, "agents", division);
480
+ return fs4.existsSync(p3) ? p3 : null;
471
481
  }
472
482
  function copyAgentEntry(root, claudeDir, raw) {
473
483
  const src = agentSourcePath(root, raw);
@@ -510,9 +520,9 @@ function manifestPath(target) {
510
520
  return path5.join(target, ".claude", MANIFEST_FILE);
511
521
  }
512
522
  function readManifest(target) {
513
- const p2 = manifestPath(target);
514
- if (!fileExists(p2)) return null;
515
- return readJsonSync(p2);
523
+ const p3 = manifestPath(target);
524
+ if (!fileExists(p3)) return null;
525
+ return readJsonSync(p3);
516
526
  }
517
527
  function writeManifest(target, merged, profileCommand, packageRoot, worktreeOrOpts) {
518
528
  let opts = {};
@@ -1241,7 +1251,7 @@ var Installer = class {
1241
1251
  if (this.hasStatusline) {
1242
1252
  settings.statusLine = {
1243
1253
  type: "command",
1244
- command: "bash .claude/statusline.sh"
1254
+ command: statuslineCommand(this.target)
1245
1255
  };
1246
1256
  }
1247
1257
  const activeStyle = this.merged.output_styles[0];
@@ -1274,27 +1284,27 @@ function clean(target) {
1274
1284
  if (fs7.existsSync(manifestFile)) {
1275
1285
  const data = JSON.parse(fs7.readFileSync(manifestFile, "utf-8"));
1276
1286
  const profiles = (data.profiles ?? []).filter(
1277
- (p2) => p2 !== "core"
1287
+ (p3) => p3 !== "core"
1278
1288
  );
1279
1289
  console.log(` Current profile: ${profiles.join(" + ")}`);
1280
1290
  console.log(` Installed: ${data.installed_at ?? "unknown"}`);
1281
1291
  const shared = data.worktree?.shared_dirs ?? [];
1282
1292
  for (const dir of shared) {
1283
- const p2 = path8.join(claudeDir, dir);
1284
- if (isSymlinkOrJunction(p2)) {
1285
- fs7.unlinkSync(p2);
1293
+ const p3 = path8.join(claudeDir, dir);
1294
+ if (isSymlinkOrJunction(p3)) {
1295
+ fs7.unlinkSync(p3);
1286
1296
  console.log(` [ok] Unlinked shared ${dir}/`);
1287
1297
  }
1288
1298
  }
1289
1299
  }
1290
1300
  for (const sub of ["hooks", "docs", "skills", "agents", "rules"]) {
1291
- const p2 = path8.join(claudeDir, sub);
1292
- safeRemove(p2);
1301
+ const p3 = path8.join(claudeDir, sub);
1302
+ safeRemove(p3);
1293
1303
  }
1294
1304
  const remaining = fs7.readdirSync(claudeDir);
1295
1305
  for (const f of remaining) {
1296
- const p2 = path8.join(claudeDir, f);
1297
- safeRemove(p2);
1306
+ const p3 = path8.join(claudeDir, f);
1307
+ safeRemove(p3);
1298
1308
  }
1299
1309
  if (fs7.existsSync(claudeDir)) {
1300
1310
  fs7.rmSync(claudeDir, { recursive: true, force: true });
@@ -1631,12 +1641,12 @@ function registryPath() {
1631
1641
  return path11.join(arcaneHome(), "installations.json");
1632
1642
  }
1633
1643
  function readRegistryFile() {
1634
- const p2 = registryPath();
1635
- if (!fileExists(p2)) {
1644
+ const p3 = registryPath();
1645
+ if (!fileExists(p3)) {
1636
1646
  return { version: REGISTRY_VERSION, installations: [] };
1637
1647
  }
1638
1648
  try {
1639
- const data = readJsonSync(p2);
1649
+ const data = readJsonSync(p3);
1640
1650
  return {
1641
1651
  version: REGISTRY_VERSION,
1642
1652
  installations: Array.isArray(data.installations) ? data.installations : []
@@ -1674,6 +1684,36 @@ function pruneRegistry() {
1674
1684
  }
1675
1685
 
1676
1686
  // src/commands/install.ts
1687
+ var PACKAGE_NAME = "claude-code-arcane";
1688
+ function isArcaneSourceTree(dir) {
1689
+ try {
1690
+ const pkg = JSON.parse(
1691
+ fs10.readFileSync(path12.join(dir, "package.json"), "utf-8")
1692
+ );
1693
+ return pkg.name === PACKAGE_NAME;
1694
+ } catch {
1695
+ return false;
1696
+ }
1697
+ }
1698
+ async function refusesSelfInstall(target, profileExpr, opts) {
1699
+ if (!isArcaneSourceTree(target)) return false;
1700
+ console.log(
1701
+ chalk2.yellow("\n This directory is Arcane's own source tree, not a project.")
1702
+ );
1703
+ console.log(chalk2.dim(` Target: ${target}`));
1704
+ if (opts.force || opts.dryRun) return false;
1705
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
1706
+ const proceed = interactive ? await p2.confirm({ message: "Install here anyway?", initialValue: false }) : false;
1707
+ if (!p2.isCancel(proceed) && proceed) return false;
1708
+ console.log(
1709
+ chalk2.dim(
1710
+ `
1711
+ cd to your project first, then: npx ${PACKAGE_NAME} install ${profileExpr ?? "<profile>"}`
1712
+ )
1713
+ );
1714
+ console.log(chalk2.dim(" Or pass --force to install here on purpose.\n"));
1715
+ return true;
1716
+ }
1677
1717
  async function installCommand(profileExpr, opts) {
1678
1718
  const source = await resolveContentSource({
1679
1719
  source: opts.source ?? "auto",
@@ -1682,6 +1722,7 @@ async function installCommand(profileExpr, opts) {
1682
1722
  const root = await source.getContentRoot();
1683
1723
  const profilesDir = path12.join(root, "profiles");
1684
1724
  const target = path12.resolve(opts.target ?? process.cwd());
1725
+ if (await refusesSelfInstall(target, profileExpr, opts)) return;
1685
1726
  if (!profileExpr) {
1686
1727
  const profiles = listProfiles(profilesDir);
1687
1728
  if (process.stdin.isTTY && process.stdout.isTTY) {
@@ -1692,9 +1733,9 @@ async function installCommand(profileExpr, opts) {
1692
1733
  console.log(chalk2.bold("\nAvailable profiles (combine freely with +):\n"));
1693
1734
  for (const group of groupByCategory(profiles)) {
1694
1735
  console.log(chalk2.cyan(` ${group.label}:`));
1695
- for (const p2 of group.profiles) {
1736
+ for (const p3 of group.profiles) {
1696
1737
  console.log(
1697
- ` ${chalk2.green(p2.name.padEnd(20))} ${p2.description}`
1738
+ ` ${chalk2.green(p3.name.padEnd(20))} ${p3.description}`
1698
1739
  );
1699
1740
  }
1700
1741
  console.log();
@@ -1752,18 +1793,18 @@ Installing profile: ${chalk2.cyan(profileExpr)}`)
1752
1793
  }
1753
1794
 
1754
1795
  // src/commands/add.ts
1755
- import fs11 from "fs";
1796
+ import fs12 from "fs";
1756
1797
  import path14 from "path";
1757
1798
  import chalk3 from "chalk";
1758
1799
 
1759
1800
  // src/skills-catalog.ts
1760
- import fs10 from "fs";
1801
+ import fs11 from "fs";
1761
1802
  import path13 from "path";
1762
1803
  var FRONTMATTER_BYTES = 4096;
1763
1804
  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"))
1805
+ if (!fs11.existsSync(skillsDir)) return [];
1806
+ return fs11.readdirSync(skillsDir, { withFileTypes: true }).filter(
1807
+ (d) => d.isDirectory() && !d.name.startsWith("_") && fs11.existsSync(path13.join(skillsDir, d.name, "SKILL.md"))
1767
1808
  ).map((d) => ({
1768
1809
  name: d.name,
1769
1810
  description: readDescription(path13.join(skillsDir, d.name, "SKILL.md"))
@@ -1787,13 +1828,13 @@ function readDescription(skillFile) {
1787
1828
  return inline.replace(/^["']|["']$/g, "").trim();
1788
1829
  }
1789
1830
  function readHead(file) {
1790
- const fd = fs10.openSync(file, "r");
1831
+ const fd = fs11.openSync(file, "r");
1791
1832
  try {
1792
1833
  const buf = Buffer.alloc(FRONTMATTER_BYTES);
1793
- const read = fs10.readSync(fd, buf, 0, FRONTMATTER_BYTES, 0);
1834
+ const read = fs11.readSync(fd, buf, 0, FRONTMATTER_BYTES, 0);
1794
1835
  return buf.subarray(0, read).toString("utf-8");
1795
1836
  } finally {
1796
- fs10.closeSync(fd);
1837
+ fs11.closeSync(fd);
1797
1838
  }
1798
1839
  }
1799
1840
 
@@ -1836,7 +1877,7 @@ async function addCommand(items = []) {
1836
1877
  if (item.startsWith("+")) {
1837
1878
  const profileName = item.slice(1);
1838
1879
  const profilePath = path14.join(root, "profiles", `${profileName}.yaml`);
1839
- if (!fs11.existsSync(profilePath)) {
1880
+ if (!fs12.existsSync(profilePath)) {
1840
1881
  console.error(chalk3.red(` Profile '${profileName}' not found`));
1841
1882
  continue;
1842
1883
  }
@@ -1850,9 +1891,9 @@ async function addCommand(items = []) {
1850
1891
  for (const rule of profile.rules.universal) {
1851
1892
  if (!manifest.installed_rules.includes(rule)) {
1852
1893
  const src = path14.join(root, "rules", `${rule}.md`);
1853
- if (fs11.existsSync(src)) {
1894
+ if (fs12.existsSync(src)) {
1854
1895
  ensureDir(path14.join(claudeDir, "rules"));
1855
- fs11.copyFileSync(src, path14.join(claudeDir, "rules", `${rule}.md`));
1896
+ fs12.copyFileSync(src, path14.join(claudeDir, "rules", `${rule}.md`));
1856
1897
  manifest.installed_rules.push(rule);
1857
1898
  addedRules.push(rule);
1858
1899
  }
@@ -1861,9 +1902,9 @@ async function addCommand(items = []) {
1861
1902
  for (const rule of profile.rules.gamedev) {
1862
1903
  if (!manifest.installed_rules.includes(rule)) {
1863
1904
  const src = path14.join(root, "rules", "gamedev", `${rule}.md`);
1864
- if (fs11.existsSync(src)) {
1905
+ if (fs12.existsSync(src)) {
1865
1906
  ensureDir(path14.join(claudeDir, "rules"));
1866
- fs11.copyFileSync(src, path14.join(claudeDir, "rules", `${rule}.md`));
1907
+ fs12.copyFileSync(src, path14.join(claudeDir, "rules", `${rule}.md`));
1867
1908
  manifest.installed_rules.push(rule);
1868
1909
  addedRules.push(rule);
1869
1910
  }
@@ -1880,8 +1921,8 @@ async function addCommand(items = []) {
1880
1921
  }
1881
1922
  if (profileName === "statusline") {
1882
1923
  const statuslineSrc = path14.join(root, "hooks", "statusline.sh");
1883
- if (fs11.existsSync(statuslineSrc)) {
1884
- fs11.copyFileSync(statuslineSrc, path14.join(claudeDir, "statusline.sh"));
1924
+ if (fs12.existsSync(statuslineSrc)) {
1925
+ fs12.copyFileSync(statuslineSrc, path14.join(claudeDir, "statusline.sh"));
1885
1926
  statuslineAdded = true;
1886
1927
  }
1887
1928
  }
@@ -1890,10 +1931,10 @@ async function addCommand(items = []) {
1890
1931
  }
1891
1932
  if (!manifest.profiles.includes(profileName)) {
1892
1933
  manifest.profiles.push(profileName);
1893
- manifest.profile_command = manifest.profiles.filter((p2) => p2 !== "core").join("+");
1934
+ manifest.profile_command = manifest.profiles.filter((p3) => p3 !== "core").join("+");
1894
1935
  }
1895
1936
  if (statuslineAdded) {
1896
- addStatuslineToSettings(claudeDir);
1937
+ addStatuslineToSettings(target, claudeDir);
1897
1938
  }
1898
1939
  } else {
1899
1940
  const result = addSkill(root, target, item, manifest.installed_skills);
@@ -1994,14 +2035,14 @@ function printCatalog(root, installedProfiles, installedSkills) {
1994
2035
  function addSkill(root, target, skill, installed) {
1995
2036
  if (installed.includes(skill)) return "skipped";
1996
2037
  const src = path14.join(root, "skills", skill);
1997
- if (!fs11.existsSync(src)) return "not-found";
2038
+ if (!fs12.existsSync(src)) return "not-found";
1998
2039
  const dst = path14.join(target, ".claude", "skills", skill);
1999
2040
  copyDirSync(src, dst);
2000
2041
  return "added";
2001
2042
  }
2002
2043
  function mergePermissions(claudeDir, newPerms) {
2003
2044
  const settingsPath = path14.join(claudeDir, "settings.json");
2004
- if (!fs11.existsSync(settingsPath)) return;
2045
+ if (!fs12.existsSync(settingsPath)) return;
2005
2046
  const settings = readJsonSync(settingsPath);
2006
2047
  const perms = settings.permissions ?? { allow: [], deny: [] };
2007
2048
  const allowSet = new Set(perms.allow);
@@ -2013,20 +2054,20 @@ function mergePermissions(claudeDir, newPerms) {
2013
2054
  settings.permissions = perms;
2014
2055
  writeJsonSync(settingsPath, settings);
2015
2056
  }
2016
- function addStatuslineToSettings(claudeDir) {
2057
+ function addStatuslineToSettings(target, claudeDir) {
2017
2058
  const settingsPath = path14.join(claudeDir, "settings.json");
2018
- if (!fs11.existsSync(settingsPath)) return;
2059
+ if (!fs12.existsSync(settingsPath)) return;
2019
2060
  const settings = readJsonSync(settingsPath);
2020
2061
  if (settings.statusLine) return;
2021
2062
  settings.statusLine = {
2022
2063
  type: "command",
2023
- command: "bash .claude/statusline.sh"
2064
+ command: statuslineCommand(target)
2024
2065
  };
2025
2066
  writeJsonSync(settingsPath, settings);
2026
2067
  }
2027
2068
 
2028
2069
  // src/commands/remove.ts
2029
- import fs12 from "fs";
2070
+ import fs13 from "fs";
2030
2071
  import path15 from "path";
2031
2072
  import chalk4 from "chalk";
2032
2073
  var CORE_SKILLS = [
@@ -2097,8 +2138,8 @@ async function removeCommand(items, opts = {}) {
2097
2138
  const totalRemoved = removedSkills.length + removedAgents.length + removedProfiles.length;
2098
2139
  console.log(chalk4.bold(`
2099
2140
  Removed ${totalRemoved} items:`));
2100
- for (const p2 of removedProfiles)
2101
- console.log(chalk4.magenta(` [ok] +${p2} (profile)`));
2141
+ for (const p3 of removedProfiles)
2142
+ console.log(chalk4.magenta(` [ok] +${p3} (profile)`));
2102
2143
  for (const s of removedSkills)
2103
2144
  console.log(chalk4.green(` [ok] ${s} (skill)`));
2104
2145
  for (const a of removedAgents)
@@ -2114,8 +2155,8 @@ function removeSkill(target, skill, manifest) {
2114
2155
  return "skipped";
2115
2156
  }
2116
2157
  const skillDir = path15.join(target, ".claude", "skills", skill);
2117
- if (!fs12.existsSync(skillDir)) return "skipped";
2118
- fs12.rmSync(skillDir, { recursive: true, force: true });
2158
+ if (!fs13.existsSync(skillDir)) return "skipped";
2159
+ fs13.rmSync(skillDir, { recursive: true, force: true });
2119
2160
  manifest.installed_skills = manifest.installed_skills.filter(
2120
2161
  (s) => s !== skill
2121
2162
  );
@@ -2135,7 +2176,7 @@ function removeProfile(root, target, profileName, manifest) {
2135
2176
  return { removed: false, skills: [], agents: [] };
2136
2177
  }
2137
2178
  const profilePath = path15.join(root, "profiles", `${profileName}.yaml`);
2138
- if (!fs12.existsSync(profilePath)) {
2179
+ if (!fs13.existsSync(profilePath)) {
2139
2180
  console.warn(
2140
2181
  chalk4.yellow(` WARNING: Profile '${profileName}.yaml' not found.`)
2141
2182
  );
@@ -2143,14 +2184,14 @@ function removeProfile(root, target, profileName, manifest) {
2143
2184
  }
2144
2185
  const profile = parseProfile(profilePath);
2145
2186
  const remainingProfiles = manifest.profiles.filter(
2146
- (p2) => p2 !== profileName
2187
+ (p3) => p3 !== profileName
2147
2188
  );
2148
2189
  const sharedSkills = /* @__PURE__ */ new Set();
2149
2190
  const sharedAgents = /* @__PURE__ */ new Set();
2150
2191
  const sharedRules = /* @__PURE__ */ new Set();
2151
2192
  for (const rp of remainingProfiles) {
2152
2193
  const rpPath = path15.join(root, "profiles", `${rp}.yaml`);
2153
- if (!fs12.existsSync(rpPath)) continue;
2194
+ if (!fs13.existsSync(rpPath)) continue;
2154
2195
  const rpDef = parseProfile(rpPath);
2155
2196
  rpDef.skills.forEach((s) => sharedSkills.add(s));
2156
2197
  rpDef.agents.forEach((a) => sharedAgents.add(a));
@@ -2162,8 +2203,8 @@ function removeProfile(root, target, profileName, manifest) {
2162
2203
  if (sharedSkills.has(skill)) continue;
2163
2204
  if (CORE_SKILLS.includes(skill)) continue;
2164
2205
  const skillDir = path15.join(target, ".claude", "skills", skill);
2165
- if (fs12.existsSync(skillDir)) {
2166
- fs12.rmSync(skillDir, { recursive: true, force: true });
2206
+ if (fs13.existsSync(skillDir)) {
2207
+ fs13.rmSync(skillDir, { recursive: true, force: true });
2167
2208
  removedSkills.push(skill);
2168
2209
  }
2169
2210
  manifest.installed_skills = manifest.installed_skills.filter(
@@ -2187,8 +2228,8 @@ function removeProfile(root, target, profileName, manifest) {
2187
2228
  ]) {
2188
2229
  if (sharedRules.has(rule)) continue;
2189
2230
  const ruleFile = path15.join(target, ".claude", "rules", `${rule}.md`);
2190
- if (fs12.existsSync(ruleFile)) {
2191
- fs12.rmSync(ruleFile);
2231
+ if (fs13.existsSync(ruleFile)) {
2232
+ fs13.rmSync(ruleFile);
2192
2233
  }
2193
2234
  manifest.installed_rules = manifest.installed_rules.filter(
2194
2235
  (r) => r !== rule
@@ -2196,27 +2237,27 @@ function removeProfile(root, target, profileName, manifest) {
2196
2237
  }
2197
2238
  if (profileName === "statusline") {
2198
2239
  const statuslineFile = path15.join(target, ".claude", "statusline.sh");
2199
- if (fs12.existsSync(statuslineFile)) {
2200
- fs12.rmSync(statuslineFile);
2240
+ if (fs13.existsSync(statuslineFile)) {
2241
+ fs13.rmSync(statuslineFile);
2201
2242
  }
2202
2243
  removeStatuslineFromSettings(path15.join(target, ".claude"));
2203
2244
  }
2204
2245
  manifest.profiles = remainingProfiles;
2205
- manifest.profile_command = remainingProfiles.filter((p2) => p2 !== "core").join("+");
2246
+ manifest.profile_command = remainingProfiles.filter((p3) => p3 !== "core").join("+");
2206
2247
  return { removed: true, skills: removedSkills, agents: removedAgents };
2207
2248
  }
2208
2249
  function removeStatuslineFromSettings(claudeDir) {
2209
2250
  const settingsPath = path15.join(claudeDir, "settings.json");
2210
- if (!fs12.existsSync(settingsPath)) return;
2211
- const settings = JSON.parse(fs12.readFileSync(settingsPath, "utf-8"));
2251
+ if (!fs13.existsSync(settingsPath)) return;
2252
+ const settings = JSON.parse(fs13.readFileSync(settingsPath, "utf-8"));
2212
2253
  if (settings.statusLine) {
2213
2254
  delete settings.statusLine;
2214
- fs12.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
2255
+ fs13.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
2215
2256
  }
2216
2257
  }
2217
2258
 
2218
2259
  // src/commands/list.ts
2219
- import fs13 from "fs";
2260
+ import fs14 from "fs";
2220
2261
  import path16 from "path";
2221
2262
  import chalk5 from "chalk";
2222
2263
  async function listCommand() {
@@ -2232,15 +2273,15 @@ async function listCommand() {
2232
2273
  console.log(chalk5.dim("Profiles combine freely: install a+b+c\n"));
2233
2274
  for (const group of groupByCategory(profiles)) {
2234
2275
  console.log(chalk5.cyan(`${group.label}:`));
2235
- for (const p2 of group.profiles) {
2236
- const tag = manifest?.profiles.includes(p2.name) ? chalk5.green(" [installed]") : "";
2276
+ for (const p3 of group.profiles) {
2277
+ const tag = manifest?.profiles.includes(p3.name) ? chalk5.green(" [installed]") : "";
2237
2278
  console.log(
2238
- ` ${chalk5.green(p2.name.padEnd(20))} ${p2.description}${tag}`
2279
+ ` ${chalk5.green(p3.name.padEnd(20))} ${p3.description}${tag}`
2239
2280
  );
2240
2281
  }
2241
2282
  console.log();
2242
2283
  }
2243
- if (fs13.existsSync(skillsDir)) {
2284
+ if (fs14.existsSync(skillsDir)) {
2244
2285
  const allSkills = listSkills(skillsDir).map((s) => s.name);
2245
2286
  console.log(chalk5.bold(`
2246
2287
  === Skills (${allSkills.length}) ===
@@ -2271,7 +2312,7 @@ async function statusCommand() {
2271
2312
  console.log(chalk6.dim("Run `npx claude-code-arcane install <profile>` to install."));
2272
2313
  return;
2273
2314
  }
2274
- const profiles = manifest.profiles.filter((p2) => p2 !== "core");
2315
+ const profiles = manifest.profiles.filter((p3) => p3 !== "core");
2275
2316
  console.log(chalk6.bold("\n=== Arcane Status ===\n"));
2276
2317
  console.log(` Profiles: ${chalk6.cyan(profiles.join(" + "))}`);
2277
2318
  console.log(` Installed: ${manifest.installed_at}`);
@@ -2307,20 +2348,23 @@ async function statusCommand() {
2307
2348
  }
2308
2349
 
2309
2350
  // src/commands/update.ts
2310
- import fs14 from "fs";
2351
+ import fs15 from "fs";
2311
2352
  import path17 from "path";
2312
2353
  import os3 from "os";
2313
2354
  import chalk7 from "chalk";
2314
2355
 
2315
2356
  // src/self-update.ts
2316
2357
  import { spawnSync } from "child_process";
2317
- var PACKAGE_NAME = "claude-code-arcane";
2358
+ var PACKAGE_NAME2 = "claude-code-arcane";
2318
2359
  function isGloballyInstalled() {
2319
2360
  const root = getPackageRoot().replace(/\\/g, "/");
2320
2361
  if (root.includes("/_npx/")) return false;
2321
2362
  if (!root.includes("/node_modules/")) return false;
2322
2363
  return true;
2323
2364
  }
2365
+ function cliInvocation() {
2366
+ return isGloballyInstalled() ? "arcane" : `npx ${PACKAGE_NAME2}`;
2367
+ }
2324
2368
  async function selfUpdateNpm(opts = {}) {
2325
2369
  const fromVersion = safeVersion();
2326
2370
  if (opts.selfUpdate === false) {
@@ -2349,7 +2393,7 @@ async function selfUpdateNpm(opts = {}) {
2349
2393
  return { updated: false, skipped: false, reason, fromVersion };
2350
2394
  }
2351
2395
  function npmInstallCommand(platform = process.platform) {
2352
- const args = ["install", "-g", `${PACKAGE_NAME}@latest`];
2396
+ const args = ["install", "-g", `${PACKAGE_NAME2}@latest`];
2353
2397
  if (platform === "win32") {
2354
2398
  return { command: ["npm", ...args].join(" "), args: [], shell: true };
2355
2399
  }
@@ -2385,17 +2429,22 @@ async function updateCommand(opts) {
2385
2429
  }
2386
2430
  }
2387
2431
  const cwd = process.cwd();
2388
- if (fs14.existsSync(manifestPath(cwd))) {
2432
+ if (fs15.existsSync(manifestPath(cwd))) {
2389
2433
  registerInstallation(cwd);
2390
2434
  }
2391
2435
  const targets = pruneRegistry().map((e) => e.path);
2392
2436
  const globalTarget = os3.homedir();
2393
- if (!process.env.VITEST && fs14.existsSync(manifestPath(globalTarget)) && !targets.includes(globalTarget)) {
2437
+ if (!process.env.VITEST && fs15.existsSync(manifestPath(globalTarget)) && !targets.includes(globalTarget)) {
2394
2438
  targets.push(globalTarget);
2395
2439
  }
2396
2440
  if (targets.length === 0) {
2397
2441
  if (!opts.quiet) {
2398
- console.log(chalk7.red("\nNo Arcane installations found. Run 'arcane install' first."));
2442
+ console.log(
2443
+ chalk7.red(
2444
+ `
2445
+ No Arcane installations found. Run \`${cliInvocation()} install\` first.`
2446
+ )
2447
+ );
2399
2448
  }
2400
2449
  return;
2401
2450
  }
@@ -2438,7 +2487,11 @@ async function updateTarget(target, opts, sharedSource) {
2438
2487
  const manifest = readManifest(target);
2439
2488
  if (!manifest) {
2440
2489
  if (!opts.quiet) {
2441
- console.log(chalk7.red("No Arcane installation found. Run 'arcane install' first."));
2490
+ console.log(
2491
+ chalk7.red(
2492
+ `No Arcane installation found. Run \`${cliInvocation()} install\` first.`
2493
+ )
2494
+ );
2442
2495
  }
2443
2496
  return { target, status: "no-manifest", updated: 0, skipped: 0, removed: 0 };
2444
2497
  }
@@ -2770,7 +2823,7 @@ function applyUpdates(items, root, claudeDir, merged, force) {
2770
2823
  function syncUnhashedFiles(root, claudeDir, merged, dryRun) {
2771
2824
  const targets = [];
2772
2825
  const statuslineDst = path17.join(claudeDir, "statusline.sh");
2773
- if (merged.loaded.includes("statusline") || fs14.existsSync(statuslineDst)) {
2826
+ if (merged.loaded.includes("statusline") || fs15.existsSync(statuslineDst)) {
2774
2827
  targets.push({
2775
2828
  src: path17.join(root, "hooks", "statusline.sh"),
2776
2829
  dst: statuslineDst,
@@ -2795,10 +2848,10 @@ function syncUnhashedFiles(root, claudeDir, merged, dryRun) {
2795
2848
  }
2796
2849
  if (merged.rules.gamedev.length > 0) {
2797
2850
  const templatesSrc = path17.join(root, "templates", "gamedev");
2798
- if (fs14.existsSync(templatesSrc)) {
2799
- for (const rel of fs14.readdirSync(templatesSrc, { recursive: true })) {
2851
+ if (fs15.existsSync(templatesSrc)) {
2852
+ for (const rel of fs15.readdirSync(templatesSrc, { recursive: true })) {
2800
2853
  const src = path17.join(templatesSrc, rel);
2801
- if (!fs14.statSync(src).isFile()) continue;
2854
+ if (!fs15.statSync(src).isFile()) continue;
2802
2855
  targets.push({
2803
2856
  src,
2804
2857
  dst: path17.join(claudeDir, "skills", "_templates", rel),
@@ -2809,35 +2862,42 @@ function syncUnhashedFiles(root, claudeDir, merged, dryRun) {
2809
2862
  }
2810
2863
  const changed = [];
2811
2864
  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;
2865
+ if (!fs15.existsSync(t.src)) continue;
2866
+ const src = fs15.readFileSync(t.src);
2867
+ if (fs15.existsSync(t.dst) && src.equals(fs15.readFileSync(t.dst))) continue;
2815
2868
  changed.push(t.label);
2816
2869
  if (dryRun) continue;
2817
- if (fs14.existsSync(t.dst)) {
2870
+ if (fs15.existsSync(t.dst)) {
2818
2871
  const bak = `${t.dst}.bak`;
2819
- if (fs14.existsSync(bak)) fs14.rmSync(bak, { force: true });
2820
- fs14.renameSync(t.dst, bak);
2872
+ if (fs15.existsSync(bak)) fs15.rmSync(bak, { force: true });
2873
+ fs15.renameSync(t.dst, bak);
2821
2874
  }
2822
- fs14.mkdirSync(path17.dirname(t.dst), { recursive: true });
2823
- fs14.copyFileSync(t.src, t.dst);
2875
+ fs15.mkdirSync(path17.dirname(t.dst), { recursive: true });
2876
+ fs15.copyFileSync(t.src, t.dst);
2824
2877
  }
2825
- const activeStyle = merged.output_styles[0];
2826
- if (activeStyle) {
2827
- const settingsPath = path17.join(claudeDir, "settings.json");
2828
- if (fs14.existsSync(settingsPath)) {
2829
- try {
2830
- const settings = JSON.parse(fs14.readFileSync(settingsPath, "utf-8"));
2831
- if (settings.outputStyle !== activeStyle) {
2832
- changed.push(`settings.json (outputStyle: ${activeStyle})`);
2833
- if (!dryRun) {
2834
- settings.outputStyle = activeStyle;
2835
- fs14.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
2878
+ const settingsPath = path17.join(claudeDir, "settings.json");
2879
+ if (fs15.existsSync(settingsPath)) {
2880
+ try {
2881
+ const settings = JSON.parse(fs15.readFileSync(settingsPath, "utf-8"));
2882
+ let dirty = false;
2883
+ const activeStyle = merged.output_styles[0];
2884
+ if (activeStyle && settings.outputStyle !== activeStyle) {
2885
+ changed.push(`settings.json (outputStyle: ${activeStyle})`);
2886
+ settings.outputStyle = activeStyle;
2887
+ dirty = true;
2888
+ }
2889
+ const wanted = statuslineCommand(path17.dirname(claudeDir));
2890
+ const current = settings.statusLine;
2891
+ if (wanted !== RELATIVE_STATUSLINE_COMMAND && current && current.command !== wanted) {
2892
+ changed.push("settings.json (statusLine path)");
2893
+ settings.statusLine = { ...current, command: wanted };
2894
+ dirty = true;
2895
+ }
2896
+ if (dirty && !dryRun) {
2897
+ fs15.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
2836
2898
  `, "utf-8");
2837
- }
2838
- }
2839
- } catch {
2840
2899
  }
2900
+ } catch {
2841
2901
  }
2842
2902
  }
2843
2903
  return changed;
@@ -2845,29 +2905,29 @@ function syncUnhashedFiles(root, claudeDir, merged, dryRun) {
2845
2905
  function applyItem(item, root, claudeDir) {
2846
2906
  const src = getSourcePath(item, root);
2847
2907
  const dst = getInstalledPath(item, claudeDir);
2848
- if (!src || !fs14.existsSync(src)) return;
2908
+ if (!src || !fs15.existsSync(src)) return;
2849
2909
  if (item.type === "skill" || item.type === "agent") {
2850
- if (fs14.existsSync(dst)) {
2851
- fs14.rmSync(dst, { recursive: true, force: true });
2910
+ if (fs15.existsSync(dst)) {
2911
+ fs15.rmSync(dst, { recursive: true, force: true });
2852
2912
  }
2853
2913
  copyDirSync(src, dst);
2854
2914
  } else {
2855
2915
  const dir = path17.dirname(dst);
2856
- if (!fs14.existsSync(dir)) {
2857
- fs14.mkdirSync(dir, { recursive: true });
2916
+ if (!fs15.existsSync(dir)) {
2917
+ fs15.mkdirSync(dir, { recursive: true });
2858
2918
  }
2859
- fs14.copyFileSync(src, dst);
2919
+ fs15.copyFileSync(src, dst);
2860
2920
  }
2861
2921
  }
2862
2922
  function quarantineItem(item, claudeDir) {
2863
2923
  const installed = getInstalledPath(item, claudeDir);
2864
- if (!fs14.existsSync(installed)) return;
2924
+ if (!fs15.existsSync(installed)) return;
2865
2925
  const quarantine = path17.join(claudeDir, TRASH_DIR, `${item.type}s`, item.name);
2866
2926
  ensureDir(path17.dirname(quarantine));
2867
- if (fs14.existsSync(quarantine)) {
2868
- fs14.rmSync(quarantine, { recursive: true, force: true });
2927
+ if (fs15.existsSync(quarantine)) {
2928
+ fs15.rmSync(quarantine, { recursive: true, force: true });
2869
2929
  }
2870
- fs14.renameSync(installed, quarantine);
2930
+ fs15.renameSync(installed, quarantine);
2871
2931
  }
2872
2932
  function getSourcePath(item, root) {
2873
2933
  switch (item.type) {
@@ -2875,7 +2935,7 @@ function getSourcePath(item, root) {
2875
2935
  return path17.join(root, "skills", item.name);
2876
2936
  case "rule": {
2877
2937
  const direct = path17.join(root, "rules", item.name);
2878
- if (fs14.existsSync(direct)) return direct;
2938
+ if (fs15.existsSync(direct)) return direct;
2879
2939
  return path17.join(root, "rules", "gamedev", item.name);
2880
2940
  }
2881
2941
  case "agent":
@@ -2930,7 +2990,7 @@ async function cleanCommand(opts) {
2930
2990
  }
2931
2991
 
2932
2992
  // src/commands/worktree.ts
2933
- import fs15 from "fs";
2993
+ import fs16 from "fs";
2934
2994
  import path18 from "path";
2935
2995
  import { execSync as execSync2 } from "child_process";
2936
2996
  import chalk9 from "chalk";
@@ -2943,7 +3003,7 @@ async function worktreeCommand(branch, opts) {
2943
3003
  }
2944
3004
  const repoRoot = wtInfo.isWorktree ? wtInfo.mainWorktreePath : cwd;
2945
3005
  const worktreePath = opts.path ? path18.resolve(opts.path) : defaultWorktreePath(repoRoot, branch);
2946
- if (fs15.existsSync(worktreePath)) {
3006
+ if (fs16.existsSync(worktreePath)) {
2947
3007
  console.error(
2948
3008
  chalk9.red(`Path already exists: ${worktreePath}`)
2949
3009
  );
@@ -3032,7 +3092,7 @@ async function worktreeCommand(branch, opts) {
3032
3092
  "skills",
3033
3093
  "worktree-isolation"
3034
3094
  );
3035
- if (fs15.existsSync(isolateScript)) {
3095
+ if (fs16.existsSync(isolateScript)) {
3036
3096
  console.log(" Applying worktree isolation...");
3037
3097
  try {
3038
3098
  execSync2(
@@ -3067,7 +3127,7 @@ async function worktreeCommand(branch, opts) {
3067
3127
  }
3068
3128
 
3069
3129
  // src/commands/global.ts
3070
- import fs16 from "fs";
3130
+ import fs17 from "fs";
3071
3131
  import path19 from "path";
3072
3132
  import os4 from "os";
3073
3133
  import chalk10 from "chalk";
@@ -3087,7 +3147,7 @@ async function globalCommand(opts) {
3087
3147
  }
3088
3148
  function showStatus() {
3089
3149
  console.log(chalk10.bold("\n=== Arcane Global Status ===\n"));
3090
- const hasScripts = fs16.existsSync(
3150
+ const hasScripts = fs17.existsSync(
3091
3151
  path19.join(SCRIPTS_DIR, "apply.py")
3092
3152
  );
3093
3153
  console.log(
@@ -3112,26 +3172,26 @@ function installGlobal() {
3112
3172
  "worktree-isolation",
3113
3173
  "scripts"
3114
3174
  );
3115
- if (!fs16.existsSync(scriptsSrc)) {
3175
+ if (!fs17.existsSync(scriptsSrc)) {
3116
3176
  console.error(
3117
3177
  chalk10.red(" worktree-isolation scripts not found in package.")
3118
3178
  );
3119
3179
  process.exit(1);
3120
3180
  }
3121
- fs16.mkdirSync(SCRIPTS_DIR, { recursive: true });
3181
+ fs17.mkdirSync(SCRIPTS_DIR, { recursive: true });
3122
3182
  for (const file of ["apply.py", "lib.py"]) {
3123
3183
  const src = path19.join(scriptsSrc, file);
3124
3184
  const dst = path19.join(SCRIPTS_DIR, file);
3125
- if (!fs16.existsSync(src)) {
3185
+ if (!fs17.existsSync(src)) {
3126
3186
  console.warn(chalk10.yellow(` WARN: ${file} not found, skipping`));
3127
3187
  continue;
3128
3188
  }
3129
- if (fs16.existsSync(dst) && fs16.readFileSync(src).equals(fs16.readFileSync(dst))) {
3189
+ if (fs17.existsSync(dst) && fs17.readFileSync(src).equals(fs17.readFileSync(dst))) {
3130
3190
  console.log(chalk10.dim(` [skip] ${file} (already up-to-date)`));
3131
3191
  } else {
3132
- fs16.copyFileSync(src, dst);
3192
+ fs17.copyFileSync(src, dst);
3133
3193
  if (process.platform !== "win32") {
3134
- fs16.chmodSync(dst, 493);
3194
+ fs17.chmodSync(dst, 493);
3135
3195
  }
3136
3196
  console.log(chalk10.green(` [ok] ${file} -> ${SCRIPTS_DIR}`));
3137
3197
  }
@@ -3155,8 +3215,8 @@ function installGlobal() {
3155
3215
  }
3156
3216
  function removeGlobal() {
3157
3217
  console.log(chalk10.bold("\nRemoving Arcane global hooks...\n"));
3158
- if (fs16.existsSync(SCRIPTS_DIR)) {
3159
- fs16.rmSync(SCRIPTS_DIR, { recursive: true, force: true });
3218
+ if (fs17.existsSync(SCRIPTS_DIR)) {
3219
+ fs17.rmSync(SCRIPTS_DIR, { recursive: true, force: true });
3160
3220
  console.log(chalk10.green(" [ok] Removed worktree-isolation scripts"));
3161
3221
  } else {
3162
3222
  console.log(chalk10.dim(" [skip] Scripts not found"));
@@ -3170,16 +3230,16 @@ function removeGlobal() {
3170
3230
  console.log(chalk10.bold.green("\n Global hooks removed.\n"));
3171
3231
  }
3172
3232
  function readSettings() {
3173
- if (!fs16.existsSync(SETTINGS_PATH)) return {};
3233
+ if (!fs17.existsSync(SETTINGS_PATH)) return {};
3174
3234
  try {
3175
- return JSON.parse(fs16.readFileSync(SETTINGS_PATH, "utf-8"));
3235
+ return JSON.parse(fs17.readFileSync(SETTINGS_PATH, "utf-8"));
3176
3236
  } catch {
3177
3237
  return {};
3178
3238
  }
3179
3239
  }
3180
3240
  function writeSettings(settings) {
3181
- fs16.mkdirSync(path19.dirname(SETTINGS_PATH), { recursive: true });
3182
- fs16.writeFileSync(
3241
+ fs17.mkdirSync(path19.dirname(SETTINGS_PATH), { recursive: true });
3242
+ fs17.writeFileSync(
3183
3243
  SETTINGS_PATH,
3184
3244
  JSON.stringify(settings, null, 2) + "\n",
3185
3245
  "utf-8"
@@ -3236,7 +3296,7 @@ function removeHookFromSettings() {
3236
3296
  }
3237
3297
 
3238
3298
  // src/update-check.ts
3239
- import fs17 from "fs";
3299
+ import fs18 from "fs";
3240
3300
  import path20 from "path";
3241
3301
  import chalk11 from "chalk";
3242
3302
  function checkFile() {
@@ -3246,7 +3306,7 @@ var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
3246
3306
  var GITHUB_OWNER2 = "SebastianLuser";
3247
3307
  var GITHUB_REPO2 = "Claude-Code-Arcane";
3248
3308
  var GITHUB_BRANCH2 = "main";
3249
- var PACKAGE_NAME2 = "claude-code-arcane";
3309
+ var PACKAGE_NAME3 = "claude-code-arcane";
3250
3310
  var SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
3251
3311
  function compareSemver(a, b) {
3252
3312
  const release = (v) => v.trim().replace(/^v/, "").split(/[-+]/)[0];
@@ -3315,7 +3375,7 @@ function printUpdateNotice(result) {
3315
3375
  Arcane CLI update: ${result.cli_version} \u2192 ${result.latest_cli_version}`
3316
3376
  )
3317
3377
  );
3318
- const how = isGloballyInstalled() ? ` Run: npm install -g ${PACKAGE_NAME2}@latest` : ` You are on an older CLI. Latest: ${PACKAGE_NAME2}@${result.latest_cli_version}`;
3378
+ const how = isGloballyInstalled() ? ` Run: npm install -g ${PACKAGE_NAME3}@latest` : ` You are on an older CLI. Latest: ${PACKAGE_NAME3}@${result.latest_cli_version}`;
3319
3379
  console.log(chalk11.dim(how));
3320
3380
  }
3321
3381
  if (result.content_update_available ?? true) {
@@ -3325,7 +3385,9 @@ function printUpdateNotice(result) {
3325
3385
  Arcane content update: ${result.local_version} \u2192 ${result.remote_sha}`
3326
3386
  )
3327
3387
  );
3328
- console.log(chalk11.dim(" Run: arcane update (--dry-run to preview)"));
3388
+ console.log(
3389
+ chalk11.dim(` Run: ${cliInvocation()} update (--dry-run to preview)`)
3390
+ );
3329
3391
  }
3330
3392
  console.log("");
3331
3393
  }
@@ -3351,7 +3413,7 @@ async function getLatestCommitSha() {
3351
3413
  async function getLatestNpmVersion() {
3352
3414
  try {
3353
3415
  const response = await fetch(
3354
- `https://registry.npmjs.org/${PACKAGE_NAME2}/latest`,
3416
+ `https://registry.npmjs.org/${PACKAGE_NAME3}/latest`,
3355
3417
  {
3356
3418
  headers: { "User-Agent": "arcane-cli", Accept: "application/json" },
3357
3419
  signal: AbortSignal.timeout(5e3)
@@ -3367,8 +3429,8 @@ async function getLatestNpmVersion() {
3367
3429
  function readCachedCheck() {
3368
3430
  try {
3369
3431
  const file = checkFile();
3370
- if (!fs17.existsSync(file)) return null;
3371
- return JSON.parse(fs17.readFileSync(file, "utf-8"));
3432
+ if (!fs18.existsSync(file)) return null;
3433
+ return JSON.parse(fs18.readFileSync(file, "utf-8"));
3372
3434
  } catch {
3373
3435
  return null;
3374
3436
  }
@@ -3377,10 +3439,10 @@ function writeCachedCheck(result) {
3377
3439
  try {
3378
3440
  const file = checkFile();
3379
3441
  const dir = path20.dirname(file);
3380
- if (!fs17.existsSync(dir)) {
3381
- fs17.mkdirSync(dir, { recursive: true });
3442
+ if (!fs18.existsSync(dir)) {
3443
+ fs18.mkdirSync(dir, { recursive: true });
3382
3444
  }
3383
- fs17.writeFileSync(file, JSON.stringify(result, null, 2) + "\n");
3445
+ fs18.writeFileSync(file, JSON.stringify(result, null, 2) + "\n");
3384
3446
  } catch {
3385
3447
  }
3386
3448
  }
@@ -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.6",
3
+ "version": "2.9.8",
4
4
  "description": "Skills, agents, hooks and rules for Claude Code — installable via npx claude-code-arcane",
5
5
  "type": "module",
6
6
  "bin": {
@@ -57,7 +57,6 @@ hooks:
57
57
  - session-stop.sh
58
58
  - log-agent.sh
59
59
  - log-agent-stop.sh
60
- - statusline.sh
61
60
 
62
61
  permissions:
63
62
  allow:
@@ -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