claude-code-arcane 1.1.1 → 1.2.0

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/dist/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/commands/install.ts
7
- import path9 from "path";
7
+ import path10 from "path";
8
8
  import chalk from "chalk";
9
9
 
10
10
  // src/profiles.ts
@@ -1207,6 +1207,59 @@ function findLatestCache() {
1207
1207
  return entries.length > 0 ? entries[0].name : null;
1208
1208
  }
1209
1209
 
1210
+ // src/registry.ts
1211
+ import path9 from "path";
1212
+ import os3 from "os";
1213
+ var REGISTRY_VERSION = 1;
1214
+ function arcaneHome() {
1215
+ return process.env.ARCANE_HOME ?? path9.join(os3.homedir(), ".arcane");
1216
+ }
1217
+ function registryPath() {
1218
+ return path9.join(arcaneHome(), "installations.json");
1219
+ }
1220
+ function readRegistryFile() {
1221
+ const p = registryPath();
1222
+ if (!fileExists(p)) {
1223
+ return { version: REGISTRY_VERSION, installations: [] };
1224
+ }
1225
+ try {
1226
+ const data = readJsonSync(p);
1227
+ return {
1228
+ version: REGISTRY_VERSION,
1229
+ installations: Array.isArray(data.installations) ? data.installations : []
1230
+ };
1231
+ } catch {
1232
+ return { version: REGISTRY_VERSION, installations: [] };
1233
+ }
1234
+ }
1235
+ function writeRegistryFile(registry) {
1236
+ try {
1237
+ ensureDir(arcaneHome());
1238
+ writeJsonSync(registryPath(), registry);
1239
+ } catch {
1240
+ }
1241
+ }
1242
+ function registerInstallation(target) {
1243
+ const abs = path9.resolve(target);
1244
+ const registry = readRegistryFile();
1245
+ if (registry.installations.some((e) => e.path === abs)) {
1246
+ return;
1247
+ }
1248
+ registry.installations.push({
1249
+ path: abs,
1250
+ registered_at: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z")
1251
+ });
1252
+ writeRegistryFile(registry);
1253
+ }
1254
+ function pruneRegistry() {
1255
+ const registry = readRegistryFile();
1256
+ const alive = registry.installations.filter((e) => fileExists(manifestPath(e.path)));
1257
+ if (alive.length !== registry.installations.length) {
1258
+ writeRegistryFile({ version: REGISTRY_VERSION, installations: alive });
1259
+ }
1260
+ return alive;
1261
+ }
1262
+
1210
1263
  // src/commands/install.ts
1211
1264
  async function installCommand(profileExpr, opts) {
1212
1265
  const source = await resolveContentSource({
@@ -1214,8 +1267,8 @@ async function installCommand(profileExpr, opts) {
1214
1267
  quiet: !profileExpr
1215
1268
  });
1216
1269
  const root = await source.getContentRoot();
1217
- const profilesDir = path9.join(root, "profiles");
1218
- const target = path9.resolve(opts.target ?? process.cwd());
1270
+ const profilesDir = path10.join(root, "profiles");
1271
+ const target = path10.resolve(opts.target ?? process.cwd());
1219
1272
  if (!profileExpr) {
1220
1273
  const profiles = listProfiles(profilesDir);
1221
1274
  const bases = profiles.filter((p) => p.type === "base");
@@ -1277,13 +1330,14 @@ Installing profile: ${chalk.cyan(profileExpr)}`)
1277
1330
  });
1278
1331
  installer.run(profileExpr, worktreeMeta);
1279
1332
  if (!opts.dryRun) {
1333
+ registerInstallation(target);
1280
1334
  console.log(chalk.green("\n Installation complete."));
1281
1335
  }
1282
1336
  }
1283
1337
 
1284
1338
  // src/commands/add.ts
1285
1339
  import fs8 from "fs";
1286
- import path10 from "path";
1340
+ import path11 from "path";
1287
1341
  import chalk2 from "chalk";
1288
1342
  async function addCommand(items) {
1289
1343
  const target = process.cwd();
@@ -1298,7 +1352,7 @@ async function addCommand(items) {
1298
1352
  );
1299
1353
  process.exit(1);
1300
1354
  }
1301
- const claudeDir = path10.join(target, ".claude");
1355
+ const claudeDir = path11.join(target, ".claude");
1302
1356
  const added = [];
1303
1357
  const skipped = [];
1304
1358
  const notFound = [];
@@ -1308,7 +1362,7 @@ async function addCommand(items) {
1308
1362
  for (const item of items) {
1309
1363
  if (item.startsWith("+")) {
1310
1364
  const profileName = item.slice(1);
1311
- const profilePath = path10.join(root, "profiles", `${profileName}.yaml`);
1365
+ const profilePath = path11.join(root, "profiles", `${profileName}.yaml`);
1312
1366
  if (!fs8.existsSync(profilePath)) {
1313
1367
  console.error(chalk2.red(` Profile '${profileName}' not found`));
1314
1368
  continue;
@@ -1322,10 +1376,10 @@ async function addCommand(items) {
1322
1376
  }
1323
1377
  for (const rule of profile.rules.universal) {
1324
1378
  if (!manifest.installed_rules.includes(rule)) {
1325
- const src = path10.join(root, "rules", `${rule}.md`);
1379
+ const src = path11.join(root, "rules", `${rule}.md`);
1326
1380
  if (fs8.existsSync(src)) {
1327
- ensureDir(path10.join(claudeDir, "rules"));
1328
- fs8.copyFileSync(src, path10.join(claudeDir, "rules", `${rule}.md`));
1381
+ ensureDir(path11.join(claudeDir, "rules"));
1382
+ fs8.copyFileSync(src, path11.join(claudeDir, "rules", `${rule}.md`));
1329
1383
  manifest.installed_rules.push(rule);
1330
1384
  addedRules.push(rule);
1331
1385
  }
@@ -1333,10 +1387,10 @@ async function addCommand(items) {
1333
1387
  }
1334
1388
  for (const rule of profile.rules.gamedev) {
1335
1389
  if (!manifest.installed_rules.includes(rule)) {
1336
- const src = path10.join(root, "rules", "gamedev", `${rule}.md`);
1390
+ const src = path11.join(root, "rules", "gamedev", `${rule}.md`);
1337
1391
  if (fs8.existsSync(src)) {
1338
- ensureDir(path10.join(claudeDir, "rules"));
1339
- fs8.copyFileSync(src, path10.join(claudeDir, "rules", `${rule}.md`));
1392
+ ensureDir(path11.join(claudeDir, "rules"));
1393
+ fs8.copyFileSync(src, path11.join(claudeDir, "rules", `${rule}.md`));
1340
1394
  manifest.installed_rules.push(rule);
1341
1395
  addedRules.push(rule);
1342
1396
  }
@@ -1344,9 +1398,9 @@ async function addCommand(items) {
1344
1398
  }
1345
1399
  for (const agentDir of profile.agents) {
1346
1400
  if (!manifest.installed_agents.includes(agentDir)) {
1347
- const src = path10.join(root, "agents", agentDir);
1401
+ const src = path11.join(root, "agents", agentDir);
1348
1402
  if (fs8.existsSync(src)) {
1349
- const dst = path10.join(claudeDir, "agents", agentDir);
1403
+ const dst = path11.join(claudeDir, "agents", agentDir);
1350
1404
  copyDirSync(src, dst);
1351
1405
  manifest.installed_agents.push(agentDir);
1352
1406
  addedAgents.push(agentDir);
@@ -1354,9 +1408,9 @@ async function addCommand(items) {
1354
1408
  }
1355
1409
  }
1356
1410
  if (profileName === "statusline") {
1357
- const statuslineSrc = path10.join(root, "hooks", "statusline.sh");
1411
+ const statuslineSrc = path11.join(root, "hooks", "statusline.sh");
1358
1412
  if (fs8.existsSync(statuslineSrc)) {
1359
- fs8.copyFileSync(statuslineSrc, path10.join(claudeDir, "statusline.sh"));
1413
+ fs8.copyFileSync(statuslineSrc, path11.join(claudeDir, "statusline.sh"));
1360
1414
  statuslineAdded = true;
1361
1415
  }
1362
1416
  }
@@ -1403,14 +1457,14 @@ Added ${totalAdded} items:`));
1403
1457
  }
1404
1458
  function addSkill(root, target, skill, installed) {
1405
1459
  if (installed.includes(skill)) return "skipped";
1406
- const src = path10.join(root, "skills", skill);
1460
+ const src = path11.join(root, "skills", skill);
1407
1461
  if (!fs8.existsSync(src)) return "not-found";
1408
- const dst = path10.join(target, ".claude", "skills", skill);
1462
+ const dst = path11.join(target, ".claude", "skills", skill);
1409
1463
  copyDirSync(src, dst);
1410
1464
  return "added";
1411
1465
  }
1412
1466
  function mergePermissions(claudeDir, newPerms) {
1413
- const settingsPath = path10.join(claudeDir, "settings.json");
1467
+ const settingsPath = path11.join(claudeDir, "settings.json");
1414
1468
  if (!fs8.existsSync(settingsPath)) return;
1415
1469
  const settings = readJsonSync(settingsPath);
1416
1470
  const perms = settings.permissions ?? { allow: [], deny: [] };
@@ -1424,7 +1478,7 @@ function mergePermissions(claudeDir, newPerms) {
1424
1478
  writeJsonSync(settingsPath, settings);
1425
1479
  }
1426
1480
  function addStatuslineToSettings(claudeDir) {
1427
- const settingsPath = path10.join(claudeDir, "settings.json");
1481
+ const settingsPath = path11.join(claudeDir, "settings.json");
1428
1482
  if (!fs8.existsSync(settingsPath)) return;
1429
1483
  const settings = readJsonSync(settingsPath);
1430
1484
  if (settings.statusLine) return;
@@ -1437,7 +1491,7 @@ function addStatuslineToSettings(claudeDir) {
1437
1491
 
1438
1492
  // src/commands/remove.ts
1439
1493
  import fs9 from "fs";
1440
- import path11 from "path";
1494
+ import path12 from "path";
1441
1495
  import chalk3 from "chalk";
1442
1496
  var CORE_SKILLS = [
1443
1497
  "commit",
@@ -1500,7 +1554,7 @@ async function removeCommand(items) {
1500
1554
  }
1501
1555
  manifest.total_skills = manifest.installed_skills.length;
1502
1556
  manifest.total_rules = manifest.installed_rules.length;
1503
- const mp = path11.join(target, ".claude", "arcane-manifest.json");
1557
+ const mp = path12.join(target, ".claude", "arcane-manifest.json");
1504
1558
  writeJsonSync(mp, manifest);
1505
1559
  const totalRemoved = removedSkills.length + removedAgents.length + removedProfiles.length;
1506
1560
  console.log(chalk3.bold(`
@@ -1521,7 +1575,7 @@ function removeSkill(target, skill, manifest) {
1521
1575
  );
1522
1576
  return "skipped";
1523
1577
  }
1524
- const skillDir = path11.join(target, ".claude", "skills", skill);
1578
+ const skillDir = path12.join(target, ".claude", "skills", skill);
1525
1579
  if (!fs9.existsSync(skillDir)) return "skipped";
1526
1580
  fs9.rmSync(skillDir, { recursive: true, force: true });
1527
1581
  manifest.installed_skills = manifest.installed_skills.filter(
@@ -1542,7 +1596,7 @@ function removeProfile(root, target, profileName, manifest) {
1542
1596
  );
1543
1597
  return { removed: false, skills: [], agents: [] };
1544
1598
  }
1545
- const profilePath = path11.join(root, "profiles", `${profileName}.yaml`);
1599
+ const profilePath = path12.join(root, "profiles", `${profileName}.yaml`);
1546
1600
  if (!fs9.existsSync(profilePath)) {
1547
1601
  console.warn(
1548
1602
  chalk3.yellow(` WARNING: Profile '${profileName}.yaml' not found.`)
@@ -1557,7 +1611,7 @@ function removeProfile(root, target, profileName, manifest) {
1557
1611
  const sharedAgents = /* @__PURE__ */ new Set();
1558
1612
  const sharedRules = /* @__PURE__ */ new Set();
1559
1613
  for (const rp of remainingProfiles) {
1560
- const rpPath = path11.join(root, "profiles", `${rp}.yaml`);
1614
+ const rpPath = path12.join(root, "profiles", `${rp}.yaml`);
1561
1615
  if (!fs9.existsSync(rpPath)) continue;
1562
1616
  const rpDef = parseProfile(rpPath);
1563
1617
  rpDef.skills.forEach((s) => sharedSkills.add(s));
@@ -1569,7 +1623,7 @@ function removeProfile(root, target, profileName, manifest) {
1569
1623
  for (const skill of profile.skills) {
1570
1624
  if (sharedSkills.has(skill)) continue;
1571
1625
  if (CORE_SKILLS.includes(skill)) continue;
1572
- const skillDir = path11.join(target, ".claude", "skills", skill);
1626
+ const skillDir = path12.join(target, ".claude", "skills", skill);
1573
1627
  if (fs9.existsSync(skillDir)) {
1574
1628
  fs9.rmSync(skillDir, { recursive: true, force: true });
1575
1629
  removedSkills.push(skill);
@@ -1581,7 +1635,7 @@ function removeProfile(root, target, profileName, manifest) {
1581
1635
  const removedAgents = [];
1582
1636
  for (const agent of profile.agents) {
1583
1637
  if (sharedAgents.has(agent)) continue;
1584
- const agentDir = path11.join(target, ".claude", "agents", agent);
1638
+ const agentDir = path12.join(target, ".claude", "agents", agent);
1585
1639
  if (fs9.existsSync(agentDir)) {
1586
1640
  fs9.rmSync(agentDir, { recursive: true, force: true });
1587
1641
  removedAgents.push(agent);
@@ -1595,7 +1649,7 @@ function removeProfile(root, target, profileName, manifest) {
1595
1649
  ...profile.rules.gamedev
1596
1650
  ]) {
1597
1651
  if (sharedRules.has(rule)) continue;
1598
- const ruleFile = path11.join(target, ".claude", "rules", `${rule}.md`);
1652
+ const ruleFile = path12.join(target, ".claude", "rules", `${rule}.md`);
1599
1653
  if (fs9.existsSync(ruleFile)) {
1600
1654
  fs9.rmSync(ruleFile);
1601
1655
  }
@@ -1604,18 +1658,18 @@ function removeProfile(root, target, profileName, manifest) {
1604
1658
  );
1605
1659
  }
1606
1660
  if (profileName === "statusline") {
1607
- const statuslineFile = path11.join(target, ".claude", "statusline.sh");
1661
+ const statuslineFile = path12.join(target, ".claude", "statusline.sh");
1608
1662
  if (fs9.existsSync(statuslineFile)) {
1609
1663
  fs9.rmSync(statuslineFile);
1610
1664
  }
1611
- removeStatuslineFromSettings(path11.join(target, ".claude"));
1665
+ removeStatuslineFromSettings(path12.join(target, ".claude"));
1612
1666
  }
1613
1667
  manifest.profiles = remainingProfiles;
1614
1668
  manifest.profile_command = remainingProfiles.filter((p) => p !== "core").join("+");
1615
1669
  return { removed: true, skills: removedSkills, agents: removedAgents };
1616
1670
  }
1617
1671
  function removeStatuslineFromSettings(claudeDir) {
1618
- const settingsPath = path11.join(claudeDir, "settings.json");
1672
+ const settingsPath = path12.join(claudeDir, "settings.json");
1619
1673
  if (!fs9.existsSync(settingsPath)) return;
1620
1674
  const settings = JSON.parse(fs9.readFileSync(settingsPath, "utf-8"));
1621
1675
  if (settings.statusLine) {
@@ -1626,13 +1680,13 @@ function removeStatuslineFromSettings(claudeDir) {
1626
1680
 
1627
1681
  // src/commands/list.ts
1628
1682
  import fs10 from "fs";
1629
- import path12 from "path";
1683
+ import path13 from "path";
1630
1684
  import chalk4 from "chalk";
1631
1685
  async function listCommand() {
1632
1686
  const source = await resolveContentSource({ quiet: true });
1633
1687
  const root = await source.getContentRoot();
1634
- const profilesDir = path12.join(root, "profiles");
1635
- const skillsDir = path12.join(root, "skills");
1688
+ const profilesDir = path13.join(root, "profiles");
1689
+ const skillsDir = path13.join(root, "skills");
1636
1690
  const target = process.cwd();
1637
1691
  const manifest = readManifest(target);
1638
1692
  const profiles = listProfiles(profilesDir);
@@ -1656,7 +1710,7 @@ async function listCommand() {
1656
1710
  }
1657
1711
  if (fs10.existsSync(skillsDir)) {
1658
1712
  const allSkills = fs10.readdirSync(skillsDir, { withFileTypes: true }).filter(
1659
- (d) => d.isDirectory() && !d.name.startsWith("_") && fs10.existsSync(path12.join(skillsDir, d.name, "SKILL.md"))
1713
+ (d) => d.isDirectory() && !d.name.startsWith("_") && fs10.existsSync(path13.join(skillsDir, d.name, "SKILL.md"))
1660
1714
  ).map((d) => d.name).sort();
1661
1715
  console.log(chalk4.bold(`
1662
1716
  === Skills (${allSkills.length}) ===
@@ -1724,16 +1778,130 @@ async function statusCommand() {
1724
1778
 
1725
1779
  // src/commands/update.ts
1726
1780
  import fs11 from "fs";
1727
- import path13 from "path";
1781
+ import path14 from "path";
1782
+ import os4 from "os";
1728
1783
  import chalk6 from "chalk";
1784
+
1785
+ // src/self-update.ts
1786
+ import { spawnSync } from "child_process";
1787
+ var PACKAGE_NAME = "claude-code-arcane";
1788
+ function isGloballyInstalled() {
1789
+ const root = getPackageRoot().replace(/\\/g, "/");
1790
+ if (root.includes("/_npx/")) return false;
1791
+ if (!root.includes("/node_modules/")) return false;
1792
+ return true;
1793
+ }
1794
+ async function selfUpdateNpm(opts = {}) {
1795
+ const fromVersion = safeVersion();
1796
+ if (opts.selfUpdate === false) {
1797
+ return { updated: false, skipped: true, reason: "disabled (--no-self-update)", fromVersion };
1798
+ }
1799
+ if (process.env.VITEST || process.env.ARCANE_SOURCE) {
1800
+ return { updated: false, skipped: true, reason: "dev/test environment", fromVersion };
1801
+ }
1802
+ if (!isGloballyInstalled()) {
1803
+ return { updated: false, skipped: true, reason: "not a global npm install", fromVersion };
1804
+ }
1805
+ if (opts.dryRun) {
1806
+ return { updated: false, skipped: true, reason: "dry-run", fromVersion };
1807
+ }
1808
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
1809
+ const result = spawnSync(
1810
+ npm,
1811
+ ["install", "-g", `${PACKAGE_NAME}@latest`],
1812
+ {
1813
+ stdio: opts.quiet ? "ignore" : "inherit",
1814
+ encoding: "utf-8",
1815
+ timeout: 12e4
1816
+ }
1817
+ );
1818
+ if (result.status === 0) {
1819
+ return { updated: true, skipped: false, fromVersion };
1820
+ }
1821
+ const reason = result.error ? result.error.message : `npm exited with code ${result.status ?? "unknown"}`;
1822
+ return { updated: false, skipped: false, reason, fromVersion };
1823
+ }
1824
+ function safeVersion() {
1825
+ try {
1826
+ return getPackageVersion();
1827
+ } catch {
1828
+ return void 0;
1829
+ }
1830
+ }
1831
+
1832
+ // src/commands/update.ts
1729
1833
  async function updateCommand(opts) {
1730
- const target = process.cwd();
1834
+ if (opts.here) {
1835
+ await updateTarget(process.cwd(), opts);
1836
+ return;
1837
+ }
1838
+ const self = await selfUpdateNpm({
1839
+ quiet: opts.quiet,
1840
+ dryRun: opts.dryRun,
1841
+ selfUpdate: opts.selfUpdate
1842
+ });
1843
+ if (!opts.quiet) {
1844
+ if (self.updated) {
1845
+ console.log(chalk6.green("npm package updated to latest."));
1846
+ } else if (self.skipped && self.reason !== "dev/test environment") {
1847
+ console.log(chalk6.dim(`npm self-update skipped (${self.reason}).`));
1848
+ } else if (!self.skipped) {
1849
+ console.log(chalk6.yellow(`npm self-update failed: ${self.reason}`));
1850
+ }
1851
+ }
1852
+ const cwd = process.cwd();
1853
+ if (fs11.existsSync(manifestPath(cwd))) {
1854
+ registerInstallation(cwd);
1855
+ }
1856
+ const targets = pruneRegistry().map((e) => e.path);
1857
+ const globalTarget = os4.homedir();
1858
+ if (!process.env.VITEST && fs11.existsSync(manifestPath(globalTarget)) && !targets.includes(globalTarget)) {
1859
+ targets.push(globalTarget);
1860
+ }
1861
+ if (targets.length === 0) {
1862
+ if (!opts.quiet) {
1863
+ console.log(chalk6.red("\nNo Arcane installations found. Run 'arcane install' first."));
1864
+ }
1865
+ return;
1866
+ }
1867
+ if (!opts.quiet) {
1868
+ console.log(
1869
+ chalk6.bold(`
1870
+ Updating ${targets.length} installation${targets.length === 1 ? "" : "s"}...`)
1871
+ );
1872
+ }
1873
+ const results = [];
1874
+ for (const target of targets) {
1875
+ if (!opts.quiet) {
1876
+ console.log(chalk6.bold.cyan(`
1877
+ \u2022 ${target}`));
1878
+ }
1879
+ try {
1880
+ results.push(await updateTarget(target, opts));
1881
+ } catch (err) {
1882
+ if (!opts.quiet) {
1883
+ console.log(chalk6.red(` Update failed: ${err.message}`));
1884
+ }
1885
+ results.push({
1886
+ target,
1887
+ status: "no-changes",
1888
+ updated: 0,
1889
+ skipped: 0,
1890
+ removed: 0
1891
+ });
1892
+ }
1893
+ }
1894
+ if (!opts.quiet) {
1895
+ printGeneralSummary(results, opts.dryRun ?? false);
1896
+ }
1897
+ }
1898
+ async function updateTarget(target, opts) {
1731
1899
  const manifest = readManifest(target);
1732
1900
  if (!manifest) {
1733
1901
  if (!opts.quiet) {
1734
1902
  console.log(chalk6.red("No Arcane installation found. Run 'arcane install' first."));
1735
1903
  }
1736
- return;
1904
+ return { target, status: "no-manifest", updated: 0, skipped: 0, removed: 0 };
1737
1905
  }
1738
1906
  const contentSource = await resolveContentSource({
1739
1907
  source: opts.source ?? "auto",
@@ -1746,7 +1914,15 @@ async function updateCommand(opts) {
1746
1914
  if (!opts.quiet) {
1747
1915
  console.log(chalk6.green(`Already up to date (v${currentVersion}).`));
1748
1916
  }
1749
- return;
1917
+ return {
1918
+ target,
1919
+ status: "up-to-date",
1920
+ fromVersion: installedVersion,
1921
+ toVersion: currentVersion,
1922
+ updated: 0,
1923
+ skipped: 0,
1924
+ removed: 0
1925
+ };
1750
1926
  }
1751
1927
  if (!opts.quiet) {
1752
1928
  console.log(
@@ -1755,11 +1931,11 @@ Update available: ${chalk6.red(installedVersion)} -> ${chalk6.green(currentVersi
1755
1931
  );
1756
1932
  }
1757
1933
  const profileNames = manifest.profile_command.split("+").filter(Boolean);
1758
- const profilesDir = path13.join(root, "profiles");
1934
+ const profilesDir = path14.join(root, "profiles");
1759
1935
  const merged = mergeProfiles(profilesDir, profileNames);
1760
1936
  const allRules = [...merged.rules.universal, ...merged.rules.gamedev];
1761
1937
  const sourceHashes = computeSourceHashes(root, merged.skills, allRules, merged.agents);
1762
- const claudeDir = path13.join(target, ".claude");
1938
+ const claudeDir = path14.join(target, ".claude");
1763
1939
  const installedHashes = computeContentHashes(claudeDir);
1764
1940
  const items = computeUpdatePlan(
1765
1941
  manifest.content_hashes ?? null,
@@ -1776,10 +1952,26 @@ Update available: ${chalk6.red(installedVersion)} -> ${chalk6.green(currentVersi
1776
1952
  if (!opts.quiet) {
1777
1953
  console.log(chalk6.green("\nNo changes to apply."));
1778
1954
  }
1779
- return;
1955
+ return {
1956
+ target,
1957
+ status: "no-changes",
1958
+ fromVersion: installedVersion,
1959
+ toVersion: currentVersion,
1960
+ updated: 0,
1961
+ skipped: skipped.length,
1962
+ removed: 0
1963
+ };
1780
1964
  }
1781
1965
  if (opts.dryRun) {
1782
- return;
1966
+ return {
1967
+ target,
1968
+ status: "dry-run",
1969
+ fromVersion: installedVersion,
1970
+ toVersion: currentVersion,
1971
+ updated: updates.length,
1972
+ skipped: skipped.length,
1973
+ removed: removed.length
1974
+ };
1783
1975
  }
1784
1976
  applyUpdates(items, root, claudeDir, merged, opts.force ?? false);
1785
1977
  const newHashes = computeContentHashes(claudeDir);
@@ -1799,6 +1991,23 @@ Update available: ${chalk6.red(installedVersion)} -> ${chalk6.green(currentVersi
1799
1991
  Updated to v${currentVersion}.`));
1800
1992
  console.log(` ${updates.length} updated, ${skipped.length} skipped, ${removed.length} removed`);
1801
1993
  }
1994
+ return {
1995
+ target,
1996
+ status: "updated",
1997
+ fromVersion: installedVersion,
1998
+ toVersion: currentVersion,
1999
+ updated: updates.length,
2000
+ skipped: skipped.length,
2001
+ removed: removed.length
2002
+ };
2003
+ }
2004
+ function printGeneralSummary(results, isDryRun) {
2005
+ const prefix = isDryRun ? chalk6.yellow("[dry-run] ") : "";
2006
+ const updated = results.filter((r) => r.status === "updated" || r.status === "dry-run").length;
2007
+ const unchanged = results.filter((r) => r.status === "up-to-date" || r.status === "no-changes").length;
2008
+ console.log(chalk6.bold(`
2009
+ ${prefix}Done. ${results.length} installation${results.length === 1 ? "" : "s"} processed:`));
2010
+ console.log(` ${chalk6.green(updated)} ${isDryRun ? "would change" : "updated"}, ${chalk6.dim(unchanged + " unchanged")}`);
1802
2011
  }
1803
2012
  function computeUpdatePlan(manifestHashes, installedHashes, sourceHashes) {
1804
2013
  const items = [];
@@ -1913,7 +2122,7 @@ function applyItem(item, root, claudeDir) {
1913
2122
  }
1914
2123
  copyDirSync(src, dst);
1915
2124
  } else {
1916
- const dir = path13.dirname(dst);
2125
+ const dir = path14.dirname(dst);
1917
2126
  if (!fs11.existsSync(dir)) {
1918
2127
  fs11.mkdirSync(dir, { recursive: true });
1919
2128
  }
@@ -1938,16 +2147,16 @@ function removeItem(item, claudeDir) {
1938
2147
  function getSourcePath(item, root) {
1939
2148
  switch (item.type) {
1940
2149
  case "skill":
1941
- return path13.join(root, "skills", item.name);
2150
+ return path14.join(root, "skills", item.name);
1942
2151
  case "rule": {
1943
- const direct = path13.join(root, "rules", item.name);
2152
+ const direct = path14.join(root, "rules", item.name);
1944
2153
  if (fs11.existsSync(direct)) return direct;
1945
- return path13.join(root, "rules", "gamedev", item.name);
2154
+ return path14.join(root, "rules", "gamedev", item.name);
1946
2155
  }
1947
2156
  case "agent":
1948
- return path13.join(root, "agents", item.name);
2157
+ return path14.join(root, "agents", item.name);
1949
2158
  case "hook":
1950
- return path13.join(root, "hooks", item.name);
2159
+ return path14.join(root, "hooks", item.name);
1951
2160
  default:
1952
2161
  return null;
1953
2162
  }
@@ -1955,13 +2164,13 @@ function getSourcePath(item, root) {
1955
2164
  function getInstalledPath(item, claudeDir) {
1956
2165
  switch (item.type) {
1957
2166
  case "skill":
1958
- return path13.join(claudeDir, "skills", item.name);
2167
+ return path14.join(claudeDir, "skills", item.name);
1959
2168
  case "rule":
1960
- return path13.join(claudeDir, "rules", item.name);
2169
+ return path14.join(claudeDir, "rules", item.name);
1961
2170
  case "agent":
1962
- return path13.join(claudeDir, "agents", item.name);
2171
+ return path14.join(claudeDir, "agents", item.name);
1963
2172
  case "hook":
1964
- return path13.join(claudeDir, "hooks", item.name);
2173
+ return path14.join(claudeDir, "hooks", item.name);
1965
2174
  }
1966
2175
  }
1967
2176
 
@@ -1997,7 +2206,7 @@ async function cleanCommand(opts) {
1997
2206
 
1998
2207
  // src/commands/worktree.ts
1999
2208
  import fs12 from "fs";
2000
- import path14 from "path";
2209
+ import path15 from "path";
2001
2210
  import { execSync as execSync2 } from "child_process";
2002
2211
  import chalk8 from "chalk";
2003
2212
  async function worktreeCommand(branch, opts) {
@@ -2008,7 +2217,7 @@ async function worktreeCommand(branch, opts) {
2008
2217
  process.exit(1);
2009
2218
  }
2010
2219
  const repoRoot = wtInfo.isWorktree ? wtInfo.mainWorktreePath : cwd;
2011
- const worktreePath = opts.path ? path14.resolve(opts.path) : defaultWorktreePath(repoRoot, branch);
2220
+ const worktreePath = opts.path ? path15.resolve(opts.path) : defaultWorktreePath(repoRoot, branch);
2012
2221
  if (fs12.existsSync(worktreePath)) {
2013
2222
  console.error(
2014
2223
  chalk8.red(`Path already exists: ${worktreePath}`)
@@ -2056,7 +2265,7 @@ async function worktreeCommand(branch, opts) {
2056
2265
  }
2057
2266
  console.log(" Installing Arcane...");
2058
2267
  const root = getPackageRoot();
2059
- const profilesDir = path14.join(root, "profiles");
2268
+ const profilesDir = path15.join(root, "profiles");
2060
2269
  const profileNames = profileExpr.split("+").filter(Boolean);
2061
2270
  const merged = mergeProfiles(profilesDir, profileNames);
2062
2271
  const shareFrom = opts.share !== false ? cwd : void 0;
@@ -2070,6 +2279,7 @@ async function worktreeCommand(branch, opts) {
2070
2279
  is_worktree: true,
2071
2280
  main_worktree: repoRoot
2072
2281
  });
2282
+ registerInstallation(worktreePath);
2073
2283
  console.log(chalk8.green(" [ok] Arcane installed"));
2074
2284
  if (opts.installDeps) {
2075
2285
  const pm = detectPackageManager(worktreePath);
@@ -2091,7 +2301,7 @@ async function worktreeCommand(branch, opts) {
2091
2301
  }
2092
2302
  }
2093
2303
  if (opts.isolate) {
2094
- const isolateScript = path14.join(
2304
+ const isolateScript = path15.join(
2095
2305
  worktreePath,
2096
2306
  ".claude",
2097
2307
  "skills",
@@ -2133,12 +2343,12 @@ async function worktreeCommand(branch, opts) {
2133
2343
 
2134
2344
  // src/commands/global.ts
2135
2345
  import fs13 from "fs";
2136
- import path15 from "path";
2137
- import os3 from "os";
2346
+ import path16 from "path";
2347
+ import os5 from "os";
2138
2348
  import chalk9 from "chalk";
2139
- var CLAUDE_HOME = path15.join(os3.homedir(), ".claude");
2140
- var SCRIPTS_DIR = path15.join(CLAUDE_HOME, "scripts", "worktree-isolation");
2141
- var SETTINGS_PATH = path15.join(CLAUDE_HOME, "settings.json");
2349
+ var CLAUDE_HOME = path16.join(os5.homedir(), ".claude");
2350
+ var SCRIPTS_DIR = path16.join(CLAUDE_HOME, "scripts", "worktree-isolation");
2351
+ var SETTINGS_PATH = path16.join(CLAUDE_HOME, "settings.json");
2142
2352
  async function globalCommand(opts) {
2143
2353
  if (opts.status) {
2144
2354
  showStatus();
@@ -2153,7 +2363,7 @@ async function globalCommand(opts) {
2153
2363
  function showStatus() {
2154
2364
  console.log(chalk9.bold("\n=== Arcane Global Status ===\n"));
2155
2365
  const hasScripts = fs13.existsSync(
2156
- path15.join(SCRIPTS_DIR, "apply.py")
2366
+ path16.join(SCRIPTS_DIR, "apply.py")
2157
2367
  );
2158
2368
  console.log(
2159
2369
  ` Worktree scripts: ${hasScripts ? chalk9.green("installed") : chalk9.dim("not installed")}`
@@ -2171,7 +2381,7 @@ function showStatus() {
2171
2381
  function installGlobal() {
2172
2382
  console.log(chalk9.bold("\nInstalling Arcane global hooks...\n"));
2173
2383
  const root = getPackageRoot();
2174
- const scriptsSrc = path15.join(
2384
+ const scriptsSrc = path16.join(
2175
2385
  root,
2176
2386
  "skills",
2177
2387
  "worktree-isolation",
@@ -2185,8 +2395,8 @@ function installGlobal() {
2185
2395
  }
2186
2396
  fs13.mkdirSync(SCRIPTS_DIR, { recursive: true });
2187
2397
  for (const file of ["apply.py", "lib.py"]) {
2188
- const src = path15.join(scriptsSrc, file);
2189
- const dst = path15.join(SCRIPTS_DIR, file);
2398
+ const src = path16.join(scriptsSrc, file);
2399
+ const dst = path16.join(SCRIPTS_DIR, file);
2190
2400
  if (!fs13.existsSync(src)) {
2191
2401
  console.warn(chalk9.yellow(` WARN: ${file} not found, skipping`));
2192
2402
  continue;
@@ -2243,7 +2453,7 @@ function readSettings() {
2243
2453
  }
2244
2454
  }
2245
2455
  function writeSettings(settings) {
2246
- fs13.mkdirSync(path15.dirname(SETTINGS_PATH), { recursive: true });
2456
+ fs13.mkdirSync(path16.dirname(SETTINGS_PATH), { recursive: true });
2247
2457
  fs13.writeFileSync(
2248
2458
  SETTINGS_PATH,
2249
2459
  JSON.stringify(settings, null, 2) + "\n",
@@ -2302,10 +2512,10 @@ function removeHookFromSettings() {
2302
2512
 
2303
2513
  // src/update-check.ts
2304
2514
  import fs14 from "fs";
2305
- import path16 from "path";
2306
- import os4 from "os";
2515
+ import path17 from "path";
2516
+ import os6 from "os";
2307
2517
  import chalk10 from "chalk";
2308
- var CHECK_FILE = path16.join(os4.homedir(), ".arcane", "last-check.json");
2518
+ var CHECK_FILE = path17.join(os6.homedir(), ".arcane", "last-check.json");
2309
2519
  var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
2310
2520
  var GITHUB_OWNER2 = "SebastianLuser";
2311
2521
  var GITHUB_REPO2 = "Claude-Code-Arcane";
@@ -2381,7 +2591,7 @@ function readCachedCheck() {
2381
2591
  }
2382
2592
  function writeCachedCheck(result) {
2383
2593
  try {
2384
- const dir = path16.dirname(CHECK_FILE);
2594
+ const dir = path17.dirname(CHECK_FILE);
2385
2595
  if (!fs14.existsSync(dir)) {
2386
2596
  fs14.mkdirSync(dir, { recursive: true });
2387
2597
  }
@@ -2419,7 +2629,9 @@ program.command("list").description("List all available profiles and skills.").a
2419
2629
  program.command("status").description("Show current Arcane installation status.").action(async () => {
2420
2630
  await statusCommand();
2421
2631
  });
2422
- program.command("update").description("Update installed Arcane content to latest version.").option("-q, --quiet", "Quiet mode for hook usage").option("-n, --dry-run", "Show what would change without applying").option("-f, --force", "Overwrite even locally modified files").option("--source <source>", "Content source: auto, github, or bundled", "auto").action(async (opts) => {
2632
+ program.command("update").description(
2633
+ "Update everything: the global npm package + all registered Arcane installations."
2634
+ ).option("-q, --quiet", "Quiet mode for hook usage").option("-n, --dry-run", "Show what would change without applying").option("-f, --force", "Overwrite even locally modified files").option("--here", "Update only the current repo (legacy per-repo behavior)").option("--no-self-update", "Skip updating the global npm package").option("--source <source>", "Content source: auto, github, or bundled", "auto").action(async (opts) => {
2423
2635
  await updateCommand(opts);
2424
2636
  });
2425
2637
  program.command("clean").description("Remove Arcane installation from current project.").option("-f, --force", "Skip confirmation").action(async (opts) => {