claude-code-arcane 2.6.1 → 2.7.1

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
@@ -265,6 +265,113 @@ async function runInstallWizard(profiles, detections) {
265
265
  );
266
266
  return names;
267
267
  }
268
+ async function runAddWizard(input) {
269
+ const installedProfiles = new Set(input.installedProfiles);
270
+ const installedSkills = new Set(input.installedSkills);
271
+ const addableProfiles = input.profiles.filter(
272
+ (pr) => !installedProfiles.has(pr.name)
273
+ );
274
+ const addableSkills = input.skills.filter((s) => !installedSkills.has(s.name));
275
+ p.intro(chalk.cyan("claude-code-arcane add"));
276
+ const current = input.installedProfiles.filter((n) => n !== "core");
277
+ p.note(
278
+ [
279
+ `Profiles: ${current.length > 0 ? chalk.green(current.join(", ")) : chalk.dim("core only")}`,
280
+ `Skills: ${chalk.green(String(installedSkills.size))} installed, ${chalk.dim(`${addableSkills.length} available`)}`
281
+ ].join("\n"),
282
+ "Current installation"
283
+ );
284
+ if (addableProfiles.length === 0 && addableSkills.length === 0) {
285
+ p.outro(chalk.dim("Everything in the catalog is already installed."));
286
+ return null;
287
+ }
288
+ const kind = await pickKind(addableProfiles.length, addableSkills.length);
289
+ if (kind === null) return null;
290
+ const items = kind === "profiles" ? await pickProfiles(addableProfiles) : await pickSkills(addableSkills);
291
+ if (items === null) return null;
292
+ if (items.length === 0) {
293
+ p.outro(
294
+ chalk.dim(
295
+ "Nothing selected. Run again, or add directly: npx claude-code-arcane add +testing docker-setup"
296
+ )
297
+ );
298
+ return null;
299
+ }
300
+ const expr = items.join(" ");
301
+ const ok = await p.confirm({ message: `Add ${chalk.cyan(expr)}?` });
302
+ if (p.isCancel(ok) || !ok) {
303
+ p.cancel("Add cancelled.");
304
+ return null;
305
+ }
306
+ p.outro(
307
+ chalk.dim(`Next time, skip the menu: npx claude-code-arcane add ${expr}`)
308
+ );
309
+ return items;
310
+ }
311
+ async function pickKind(profileCount, skillCount) {
312
+ if (profileCount === 0) return "skills";
313
+ if (skillCount === 0) return "profiles";
314
+ const kind = await p.select({
315
+ message: "What do you want to add?",
316
+ options: [
317
+ {
318
+ value: "profiles",
319
+ label: "Profiles",
320
+ hint: `${profileCount} not installed \u2014 brings its skills, rules and agents`
321
+ },
322
+ {
323
+ value: "skills",
324
+ label: "Individual skills",
325
+ hint: `${skillCount} not installed \u2014 search by name`
326
+ }
327
+ ]
328
+ });
329
+ if (p.isCancel(kind)) {
330
+ p.cancel("Add cancelled.");
331
+ return null;
332
+ }
333
+ return kind;
334
+ }
335
+ async function pickProfiles(addable) {
336
+ const options = {};
337
+ for (const group of groupByCategory(addable)) {
338
+ options[group.label] = group.profiles.map((pr) => ({
339
+ value: pr.name,
340
+ label: pr.name,
341
+ hint: hint(pr.description)
342
+ }));
343
+ }
344
+ const selected = await p.groupMultiselect({
345
+ message: "Select profiles to add (space to toggle, enter to confirm)",
346
+ options,
347
+ required: false
348
+ });
349
+ if (p.isCancel(selected)) {
350
+ p.cancel("Add cancelled.");
351
+ return null;
352
+ }
353
+ return selected.map((name) => `+${name}`);
354
+ }
355
+ async function pickSkills(addable) {
356
+ const selected = await p.autocompleteMultiselect({
357
+ message: "Search skills to add (type to filter, space to toggle)",
358
+ options: addable.map((s) => ({
359
+ value: s.name,
360
+ label: s.name,
361
+ hint: hint(s.description)
362
+ })),
363
+ required: false
364
+ });
365
+ if (p.isCancel(selected)) {
366
+ p.cancel("Add cancelled.");
367
+ return null;
368
+ }
369
+ return selected;
370
+ }
371
+ function hint(description, max = 90) {
372
+ const flat = description.replace(/\s+/g, " ").trim();
373
+ return flat.length > max ? `${flat.slice(0, max - 3)}...` : flat;
374
+ }
268
375
 
269
376
  // src/installer.ts
270
377
  import fs6 from "fs";
@@ -1528,10 +1635,53 @@ Installing profile: ${chalk2.cyan(profileExpr)}`)
1528
1635
  }
1529
1636
 
1530
1637
  // src/commands/add.ts
1638
+ import fs10 from "fs";
1639
+ import path13 from "path";
1640
+ import chalk3 from "chalk";
1641
+
1642
+ // src/skills-catalog.ts
1531
1643
  import fs9 from "fs";
1532
1644
  import path12 from "path";
1533
- import chalk3 from "chalk";
1534
- async function addCommand(items) {
1645
+ var FRONTMATTER_BYTES = 4096;
1646
+ function listSkills(skillsDir) {
1647
+ if (!fs9.existsSync(skillsDir)) return [];
1648
+ return fs9.readdirSync(skillsDir, { withFileTypes: true }).filter(
1649
+ (d) => d.isDirectory() && !d.name.startsWith("_") && fs9.existsSync(path12.join(skillsDir, d.name, "SKILL.md"))
1650
+ ).map((d) => ({
1651
+ name: d.name,
1652
+ description: readDescription(path12.join(skillsDir, d.name, "SKILL.md"))
1653
+ })).sort((a, b) => a.name.localeCompare(b.name));
1654
+ }
1655
+ function readDescription(skillFile) {
1656
+ const head = readHead(skillFile);
1657
+ const lines = head.split(/\r?\n/);
1658
+ const idx = lines.findIndex((l) => /^description:/.test(l));
1659
+ if (idx === -1) return "";
1660
+ const inline = lines[idx].slice("description:".length).trim();
1661
+ if (inline === "|" || inline === ">" || /^[|>][-+]?$/.test(inline)) {
1662
+ const block = [];
1663
+ for (const line of lines.slice(idx + 1)) {
1664
+ if (line.trim() === "") continue;
1665
+ if (!/^\s/.test(line)) break;
1666
+ block.push(line.trim());
1667
+ }
1668
+ return block.join(" ");
1669
+ }
1670
+ return inline.replace(/^["']|["']$/g, "").trim();
1671
+ }
1672
+ function readHead(file) {
1673
+ const fd = fs9.openSync(file, "r");
1674
+ try {
1675
+ const buf = Buffer.alloc(FRONTMATTER_BYTES);
1676
+ const read = fs9.readSync(fd, buf, 0, FRONTMATTER_BYTES, 0);
1677
+ return buf.subarray(0, read).toString("utf-8");
1678
+ } finally {
1679
+ fs9.closeSync(fd);
1680
+ }
1681
+ }
1682
+
1683
+ // src/commands/add.ts
1684
+ async function addCommand(items = []) {
1535
1685
  const target = process.cwd();
1536
1686
  const source = await resolveContentSource({ quiet: true });
1537
1687
  const root = await source.getContentRoot();
@@ -1544,7 +1694,21 @@ async function addCommand(items) {
1544
1694
  );
1545
1695
  process.exit(1);
1546
1696
  }
1547
- const claudeDir = path12.join(target, ".claude");
1697
+ if (items.length === 0) {
1698
+ if (!(process.stdin.isTTY && process.stdout.isTTY)) {
1699
+ printCatalog(root, manifest.profiles, manifest.installed_skills);
1700
+ return;
1701
+ }
1702
+ const selected = await runAddWizard({
1703
+ profiles: listProfiles(path13.join(root, "profiles")),
1704
+ skills: listSkills(path13.join(root, "skills")),
1705
+ installedProfiles: manifest.profiles,
1706
+ installedSkills: manifest.installed_skills
1707
+ });
1708
+ if (!selected) return;
1709
+ items = selected;
1710
+ }
1711
+ const claudeDir = path13.join(target, ".claude");
1548
1712
  const added = [];
1549
1713
  const skipped = [];
1550
1714
  const notFound = [];
@@ -1554,8 +1718,8 @@ async function addCommand(items) {
1554
1718
  for (const item of items) {
1555
1719
  if (item.startsWith("+")) {
1556
1720
  const profileName = item.slice(1);
1557
- const profilePath = path12.join(root, "profiles", `${profileName}.yaml`);
1558
- if (!fs9.existsSync(profilePath)) {
1721
+ const profilePath = path13.join(root, "profiles", `${profileName}.yaml`);
1722
+ if (!fs10.existsSync(profilePath)) {
1559
1723
  console.error(chalk3.red(` Profile '${profileName}' not found`));
1560
1724
  continue;
1561
1725
  }
@@ -1568,10 +1732,10 @@ async function addCommand(items) {
1568
1732
  }
1569
1733
  for (const rule of profile.rules.universal) {
1570
1734
  if (!manifest.installed_rules.includes(rule)) {
1571
- const src = path12.join(root, "rules", `${rule}.md`);
1572
- if (fs9.existsSync(src)) {
1573
- ensureDir(path12.join(claudeDir, "rules"));
1574
- fs9.copyFileSync(src, path12.join(claudeDir, "rules", `${rule}.md`));
1735
+ const src = path13.join(root, "rules", `${rule}.md`);
1736
+ if (fs10.existsSync(src)) {
1737
+ ensureDir(path13.join(claudeDir, "rules"));
1738
+ fs10.copyFileSync(src, path13.join(claudeDir, "rules", `${rule}.md`));
1575
1739
  manifest.installed_rules.push(rule);
1576
1740
  addedRules.push(rule);
1577
1741
  }
@@ -1579,10 +1743,10 @@ async function addCommand(items) {
1579
1743
  }
1580
1744
  for (const rule of profile.rules.gamedev) {
1581
1745
  if (!manifest.installed_rules.includes(rule)) {
1582
- const src = path12.join(root, "rules", "gamedev", `${rule}.md`);
1583
- if (fs9.existsSync(src)) {
1584
- ensureDir(path12.join(claudeDir, "rules"));
1585
- fs9.copyFileSync(src, path12.join(claudeDir, "rules", `${rule}.md`));
1746
+ const src = path13.join(root, "rules", "gamedev", `${rule}.md`);
1747
+ if (fs10.existsSync(src)) {
1748
+ ensureDir(path13.join(claudeDir, "rules"));
1749
+ fs10.copyFileSync(src, path13.join(claudeDir, "rules", `${rule}.md`));
1586
1750
  manifest.installed_rules.push(rule);
1587
1751
  addedRules.push(rule);
1588
1752
  }
@@ -1590,9 +1754,9 @@ async function addCommand(items) {
1590
1754
  }
1591
1755
  for (const agentDir of profile.agents) {
1592
1756
  if (!manifest.installed_agents.includes(agentDir)) {
1593
- const src = path12.join(root, "agents", agentDir);
1594
- if (fs9.existsSync(src)) {
1595
- const dst = path12.join(claudeDir, "agents", agentDir);
1757
+ const src = path13.join(root, "agents", agentDir);
1758
+ if (fs10.existsSync(src)) {
1759
+ const dst = path13.join(claudeDir, "agents", agentDir);
1596
1760
  copyDirSync(src, dst);
1597
1761
  manifest.installed_agents.push(agentDir);
1598
1762
  addedAgents.push(agentDir);
@@ -1600,9 +1764,9 @@ async function addCommand(items) {
1600
1764
  }
1601
1765
  }
1602
1766
  if (profileName === "statusline") {
1603
- const statuslineSrc = path12.join(root, "hooks", "statusline.sh");
1604
- if (fs9.existsSync(statuslineSrc)) {
1605
- fs9.copyFileSync(statuslineSrc, path12.join(claudeDir, "statusline.sh"));
1767
+ const statuslineSrc = path13.join(root, "hooks", "statusline.sh");
1768
+ if (fs10.existsSync(statuslineSrc)) {
1769
+ fs10.copyFileSync(statuslineSrc, path13.join(claudeDir, "statusline.sh"));
1606
1770
  statuslineAdded = true;
1607
1771
  }
1608
1772
  }
@@ -1636,8 +1800,12 @@ async function addCommand(items) {
1636
1800
  };
1637
1801
  writeManifest(target, merged, manifest.profile_command, root);
1638
1802
  const totalAdded = added.length + addedRules.length + addedAgents.length + (statuslineAdded ? 1 : 0);
1639
- console.log(chalk3.bold(`
1640
- Added ${totalAdded} items:`));
1803
+ console.log(
1804
+ chalk3.bold(
1805
+ totalAdded === 0 ? "\nNothing added." : `
1806
+ Added ${totalAdded} items:`
1807
+ )
1808
+ );
1641
1809
  for (const s of added) console.log(chalk3.green(` [ok] skill: ${s}`));
1642
1810
  for (const r of addedRules) console.log(chalk3.green(` [ok] rule: ${r}`));
1643
1811
  for (const a of addedAgents) console.log(chalk3.green(` [ok] agents: ${a}/`));
@@ -1646,18 +1814,54 @@ Added ${totalAdded} items:`));
1646
1814
  console.log(chalk3.dim(` [skip] ${s} (already installed)`));
1647
1815
  for (const s of notFound)
1648
1816
  console.log(chalk3.red(` [miss] ${s} (not found in source)`));
1817
+ if (totalAdded > 0) {
1818
+ console.log(
1819
+ chalk3.dim(
1820
+ `
1821
+ Now installed: ${manifest.installed_skills.length} skills, ${manifest.installed_rules.length} rules, ${manifest.installed_agents.length} agents.`
1822
+ )
1823
+ );
1824
+ }
1825
+ }
1826
+ function printCatalog(root, installedProfiles, installedSkills) {
1827
+ const profiles = listProfiles(path13.join(root, "profiles")).filter(
1828
+ (pr) => !installedProfiles.includes(pr.name)
1829
+ );
1830
+ const skills = listSkills(path13.join(root, "skills")).filter(
1831
+ (sk) => !installedSkills.includes(sk.name)
1832
+ );
1833
+ if (profiles.length === 0 && skills.length === 0) {
1834
+ console.log(chalk3.dim("\nEverything in the catalog is already installed.\n"));
1835
+ return;
1836
+ }
1837
+ console.log(chalk3.bold("\nProfiles you can add:\n"));
1838
+ for (const group of groupByCategory(profiles)) {
1839
+ console.log(chalk3.cyan(` ${group.label}:`));
1840
+ for (const pr of group.profiles) {
1841
+ console.log(
1842
+ ` ${chalk3.green(`+${pr.name}`.padEnd(20))} ${pr.description}`
1843
+ );
1844
+ }
1845
+ console.log();
1846
+ }
1847
+ console.log(
1848
+ chalk3.dim(` Plus ${skills.length} individual skills \u2014 see \`arcane list\`.`)
1849
+ );
1850
+ console.log(
1851
+ chalk3.dim(" Usage: npx claude-code-arcane add +testing docker-setup\n")
1852
+ );
1649
1853
  }
1650
1854
  function addSkill(root, target, skill, installed) {
1651
1855
  if (installed.includes(skill)) return "skipped";
1652
- const src = path12.join(root, "skills", skill);
1653
- if (!fs9.existsSync(src)) return "not-found";
1654
- const dst = path12.join(target, ".claude", "skills", skill);
1856
+ const src = path13.join(root, "skills", skill);
1857
+ if (!fs10.existsSync(src)) return "not-found";
1858
+ const dst = path13.join(target, ".claude", "skills", skill);
1655
1859
  copyDirSync(src, dst);
1656
1860
  return "added";
1657
1861
  }
1658
1862
  function mergePermissions(claudeDir, newPerms) {
1659
- const settingsPath = path12.join(claudeDir, "settings.json");
1660
- if (!fs9.existsSync(settingsPath)) return;
1863
+ const settingsPath = path13.join(claudeDir, "settings.json");
1864
+ if (!fs10.existsSync(settingsPath)) return;
1661
1865
  const settings = readJsonSync(settingsPath);
1662
1866
  const perms = settings.permissions ?? { allow: [], deny: [] };
1663
1867
  const allowSet = new Set(perms.allow);
@@ -1670,8 +1874,8 @@ function mergePermissions(claudeDir, newPerms) {
1670
1874
  writeJsonSync(settingsPath, settings);
1671
1875
  }
1672
1876
  function addStatuslineToSettings(claudeDir) {
1673
- const settingsPath = path12.join(claudeDir, "settings.json");
1674
- if (!fs9.existsSync(settingsPath)) return;
1877
+ const settingsPath = path13.join(claudeDir, "settings.json");
1878
+ if (!fs10.existsSync(settingsPath)) return;
1675
1879
  const settings = readJsonSync(settingsPath);
1676
1880
  if (settings.statusLine) return;
1677
1881
  settings.statusLine = {
@@ -1682,8 +1886,8 @@ function addStatuslineToSettings(claudeDir) {
1682
1886
  }
1683
1887
 
1684
1888
  // src/commands/remove.ts
1685
- import fs10 from "fs";
1686
- import path13 from "path";
1889
+ import fs11 from "fs";
1890
+ import path14 from "path";
1687
1891
  import chalk4 from "chalk";
1688
1892
  var CORE_SKILLS = [
1689
1893
  "commit",
@@ -1748,7 +1952,7 @@ async function removeCommand(items, opts = {}) {
1748
1952
  }
1749
1953
  manifest.total_skills = manifest.installed_skills.length;
1750
1954
  manifest.total_rules = manifest.installed_rules.length;
1751
- const mp = path13.join(target, ".claude", "arcane-manifest.json");
1955
+ const mp = path14.join(target, ".claude", "arcane-manifest.json");
1752
1956
  writeJsonSync(mp, manifest);
1753
1957
  const totalRemoved = removedSkills.length + removedAgents.length + removedProfiles.length;
1754
1958
  console.log(chalk4.bold(`
@@ -1769,9 +1973,9 @@ function removeSkill(target, skill, manifest) {
1769
1973
  );
1770
1974
  return "skipped";
1771
1975
  }
1772
- const skillDir = path13.join(target, ".claude", "skills", skill);
1773
- if (!fs10.existsSync(skillDir)) return "skipped";
1774
- fs10.rmSync(skillDir, { recursive: true, force: true });
1976
+ const skillDir = path14.join(target, ".claude", "skills", skill);
1977
+ if (!fs11.existsSync(skillDir)) return "skipped";
1978
+ fs11.rmSync(skillDir, { recursive: true, force: true });
1775
1979
  manifest.installed_skills = manifest.installed_skills.filter(
1776
1980
  (s) => s !== skill
1777
1981
  );
@@ -1790,8 +1994,8 @@ function removeProfile(root, target, profileName, manifest) {
1790
1994
  );
1791
1995
  return { removed: false, skills: [], agents: [] };
1792
1996
  }
1793
- const profilePath = path13.join(root, "profiles", `${profileName}.yaml`);
1794
- if (!fs10.existsSync(profilePath)) {
1997
+ const profilePath = path14.join(root, "profiles", `${profileName}.yaml`);
1998
+ if (!fs11.existsSync(profilePath)) {
1795
1999
  console.warn(
1796
2000
  chalk4.yellow(` WARNING: Profile '${profileName}.yaml' not found.`)
1797
2001
  );
@@ -1805,8 +2009,8 @@ function removeProfile(root, target, profileName, manifest) {
1805
2009
  const sharedAgents = /* @__PURE__ */ new Set();
1806
2010
  const sharedRules = /* @__PURE__ */ new Set();
1807
2011
  for (const rp of remainingProfiles) {
1808
- const rpPath = path13.join(root, "profiles", `${rp}.yaml`);
1809
- if (!fs10.existsSync(rpPath)) continue;
2012
+ const rpPath = path14.join(root, "profiles", `${rp}.yaml`);
2013
+ if (!fs11.existsSync(rpPath)) continue;
1810
2014
  const rpDef = parseProfile(rpPath);
1811
2015
  rpDef.skills.forEach((s) => sharedSkills.add(s));
1812
2016
  rpDef.agents.forEach((a) => sharedAgents.add(a));
@@ -1817,9 +2021,9 @@ function removeProfile(root, target, profileName, manifest) {
1817
2021
  for (const skill of profile.skills) {
1818
2022
  if (sharedSkills.has(skill)) continue;
1819
2023
  if (CORE_SKILLS.includes(skill)) continue;
1820
- const skillDir = path13.join(target, ".claude", "skills", skill);
1821
- if (fs10.existsSync(skillDir)) {
1822
- fs10.rmSync(skillDir, { recursive: true, force: true });
2024
+ const skillDir = path14.join(target, ".claude", "skills", skill);
2025
+ if (fs11.existsSync(skillDir)) {
2026
+ fs11.rmSync(skillDir, { recursive: true, force: true });
1823
2027
  removedSkills.push(skill);
1824
2028
  }
1825
2029
  manifest.installed_skills = manifest.installed_skills.filter(
@@ -1829,9 +2033,9 @@ function removeProfile(root, target, profileName, manifest) {
1829
2033
  const removedAgents = [];
1830
2034
  for (const agent of profile.agents) {
1831
2035
  if (sharedAgents.has(agent)) continue;
1832
- const agentDir = path13.join(target, ".claude", "agents", agent);
1833
- if (fs10.existsSync(agentDir)) {
1834
- fs10.rmSync(agentDir, { recursive: true, force: true });
2036
+ const agentDir = path14.join(target, ".claude", "agents", agent);
2037
+ if (fs11.existsSync(agentDir)) {
2038
+ fs11.rmSync(agentDir, { recursive: true, force: true });
1835
2039
  removedAgents.push(agent);
1836
2040
  }
1837
2041
  manifest.installed_agents = manifest.installed_agents.filter(
@@ -1843,44 +2047,44 @@ function removeProfile(root, target, profileName, manifest) {
1843
2047
  ...profile.rules.gamedev
1844
2048
  ]) {
1845
2049
  if (sharedRules.has(rule)) continue;
1846
- const ruleFile = path13.join(target, ".claude", "rules", `${rule}.md`);
1847
- if (fs10.existsSync(ruleFile)) {
1848
- fs10.rmSync(ruleFile);
2050
+ const ruleFile = path14.join(target, ".claude", "rules", `${rule}.md`);
2051
+ if (fs11.existsSync(ruleFile)) {
2052
+ fs11.rmSync(ruleFile);
1849
2053
  }
1850
2054
  manifest.installed_rules = manifest.installed_rules.filter(
1851
2055
  (r) => r !== rule
1852
2056
  );
1853
2057
  }
1854
2058
  if (profileName === "statusline") {
1855
- const statuslineFile = path13.join(target, ".claude", "statusline.sh");
1856
- if (fs10.existsSync(statuslineFile)) {
1857
- fs10.rmSync(statuslineFile);
2059
+ const statuslineFile = path14.join(target, ".claude", "statusline.sh");
2060
+ if (fs11.existsSync(statuslineFile)) {
2061
+ fs11.rmSync(statuslineFile);
1858
2062
  }
1859
- removeStatuslineFromSettings(path13.join(target, ".claude"));
2063
+ removeStatuslineFromSettings(path14.join(target, ".claude"));
1860
2064
  }
1861
2065
  manifest.profiles = remainingProfiles;
1862
2066
  manifest.profile_command = remainingProfiles.filter((p2) => p2 !== "core").join("+");
1863
2067
  return { removed: true, skills: removedSkills, agents: removedAgents };
1864
2068
  }
1865
2069
  function removeStatuslineFromSettings(claudeDir) {
1866
- const settingsPath = path13.join(claudeDir, "settings.json");
1867
- if (!fs10.existsSync(settingsPath)) return;
1868
- const settings = JSON.parse(fs10.readFileSync(settingsPath, "utf-8"));
2070
+ const settingsPath = path14.join(claudeDir, "settings.json");
2071
+ if (!fs11.existsSync(settingsPath)) return;
2072
+ const settings = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
1869
2073
  if (settings.statusLine) {
1870
2074
  delete settings.statusLine;
1871
- fs10.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
2075
+ fs11.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1872
2076
  }
1873
2077
  }
1874
2078
 
1875
2079
  // src/commands/list.ts
1876
- import fs11 from "fs";
1877
- import path14 from "path";
2080
+ import fs12 from "fs";
2081
+ import path15 from "path";
1878
2082
  import chalk5 from "chalk";
1879
2083
  async function listCommand() {
1880
2084
  const source = await resolveContentSource({ quiet: true });
1881
2085
  const root = await source.getContentRoot();
1882
- const profilesDir = path14.join(root, "profiles");
1883
- const skillsDir = path14.join(root, "skills");
2086
+ const profilesDir = path15.join(root, "profiles");
2087
+ const skillsDir = path15.join(root, "skills");
1884
2088
  const target = process.cwd();
1885
2089
  const manifest = readManifest(target);
1886
2090
  const profiles = listProfiles(profilesDir);
@@ -1897,10 +2101,8 @@ async function listCommand() {
1897
2101
  }
1898
2102
  console.log();
1899
2103
  }
1900
- if (fs11.existsSync(skillsDir)) {
1901
- const allSkills = fs11.readdirSync(skillsDir, { withFileTypes: true }).filter(
1902
- (d) => d.isDirectory() && !d.name.startsWith("_") && fs11.existsSync(path14.join(skillsDir, d.name, "SKILL.md"))
1903
- ).map((d) => d.name).sort();
2104
+ if (fs12.existsSync(skillsDir)) {
2105
+ const allSkills = listSkills(skillsDir).map((s) => s.name);
1904
2106
  console.log(chalk5.bold(`
1905
2107
  === Skills (${allSkills.length}) ===
1906
2108
  `));
@@ -1966,8 +2168,8 @@ async function statusCommand() {
1966
2168
  }
1967
2169
 
1968
2170
  // src/commands/update.ts
1969
- import fs12 from "fs";
1970
- import path15 from "path";
2171
+ import fs13 from "fs";
2172
+ import path16 from "path";
1971
2173
  import os4 from "os";
1972
2174
  import chalk7 from "chalk";
1973
2175
 
@@ -1994,22 +2196,26 @@ async function selfUpdateNpm(opts = {}) {
1994
2196
  if (opts.dryRun) {
1995
2197
  return { updated: false, skipped: true, reason: "dry-run", fromVersion };
1996
2198
  }
1997
- const npm = process.platform === "win32" ? "npm.cmd" : "npm";
1998
- const result = spawnSync(
1999
- npm,
2000
- ["install", "-g", `${PACKAGE_NAME}@latest`],
2001
- {
2002
- stdio: opts.quiet ? "ignore" : "inherit",
2003
- encoding: "utf-8",
2004
- timeout: 12e4
2005
- }
2006
- );
2199
+ const npm = npmInstallCommand();
2200
+ const result = spawnSync(npm.command, npm.args, {
2201
+ stdio: opts.quiet ? "ignore" : "inherit",
2202
+ encoding: "utf-8",
2203
+ timeout: 12e4,
2204
+ shell: npm.shell
2205
+ });
2007
2206
  if (result.status === 0) {
2008
2207
  return { updated: true, skipped: false, fromVersion };
2009
2208
  }
2010
2209
  const reason = result.error ? result.error.message : `npm exited with code ${result.status ?? "unknown"}`;
2011
2210
  return { updated: false, skipped: false, reason, fromVersion };
2012
2211
  }
2212
+ function npmInstallCommand(platform = process.platform) {
2213
+ const args = ["install", "-g", `${PACKAGE_NAME}@latest`];
2214
+ if (platform === "win32") {
2215
+ return { command: ["npm", ...args].join(" "), args: [], shell: true };
2216
+ }
2217
+ return { command: "npm", args, shell: false };
2218
+ }
2013
2219
  function safeVersion() {
2014
2220
  try {
2015
2221
  return getPackageVersion();
@@ -2039,12 +2245,12 @@ async function updateCommand(opts) {
2039
2245
  }
2040
2246
  }
2041
2247
  const cwd = process.cwd();
2042
- if (fs12.existsSync(manifestPath(cwd))) {
2248
+ if (fs13.existsSync(manifestPath(cwd))) {
2043
2249
  registerInstallation(cwd);
2044
2250
  }
2045
2251
  const targets = pruneRegistry().map((e) => e.path);
2046
2252
  const globalTarget = os4.homedir();
2047
- if (!process.env.VITEST && fs12.existsSync(manifestPath(globalTarget)) && !targets.includes(globalTarget)) {
2253
+ if (!process.env.VITEST && fs13.existsSync(manifestPath(globalTarget)) && !targets.includes(globalTarget)) {
2048
2254
  targets.push(globalTarget);
2049
2255
  }
2050
2256
  if (targets.length === 0) {
@@ -2120,11 +2326,11 @@ Update available: ${chalk7.red(installedVersion)} -> ${chalk7.green(currentVersi
2120
2326
  );
2121
2327
  }
2122
2328
  const profileNames = manifest.profile_command.split("+").filter(Boolean);
2123
- const profilesDir = path15.join(root, "profiles");
2329
+ const profilesDir = path16.join(root, "profiles");
2124
2330
  const merged = mergeProfiles(profilesDir, profileNames);
2125
2331
  const allRules = [...merged.rules.universal, ...merged.rules.gamedev];
2126
2332
  const sourceHashes = computeSourceHashes(root, merged.skills, allRules, merged.agents);
2127
- const claudeDir = path15.join(target, ".claude");
2333
+ const claudeDir = path16.join(target, ".claude");
2128
2334
  const installedHashes = computeContentHashes(claudeDir);
2129
2335
  const items = computeUpdatePlan(
2130
2336
  manifest.content_hashes ?? null,
@@ -2317,48 +2523,48 @@ function applyUpdates(items, root, claudeDir, merged, force) {
2317
2523
  function applyItem(item, root, claudeDir) {
2318
2524
  const src = getSourcePath(item, root);
2319
2525
  const dst = getInstalledPath(item, claudeDir);
2320
- if (!src || !fs12.existsSync(src)) return;
2526
+ if (!src || !fs13.existsSync(src)) return;
2321
2527
  if (item.type === "skill" || item.type === "agent") {
2322
- if (fs12.existsSync(dst)) {
2323
- fs12.rmSync(dst, { recursive: true, force: true });
2528
+ if (fs13.existsSync(dst)) {
2529
+ fs13.rmSync(dst, { recursive: true, force: true });
2324
2530
  }
2325
2531
  copyDirSync(src, dst);
2326
2532
  } else {
2327
- const dir = path15.dirname(dst);
2328
- if (!fs12.existsSync(dir)) {
2329
- fs12.mkdirSync(dir, { recursive: true });
2533
+ const dir = path16.dirname(dst);
2534
+ if (!fs13.existsSync(dir)) {
2535
+ fs13.mkdirSync(dir, { recursive: true });
2330
2536
  }
2331
- fs12.copyFileSync(src, dst);
2537
+ fs13.copyFileSync(src, dst);
2332
2538
  }
2333
2539
  }
2334
2540
  function backupItem(item, claudeDir) {
2335
2541
  const installed = getInstalledPath(item, claudeDir);
2336
- if (!fs12.existsSync(installed)) return;
2542
+ if (!fs13.existsSync(installed)) return;
2337
2543
  const backupPath = installed + ".bak";
2338
- if (fs12.existsSync(backupPath)) {
2339
- fs12.rmSync(backupPath, { recursive: true, force: true });
2544
+ if (fs13.existsSync(backupPath)) {
2545
+ fs13.rmSync(backupPath, { recursive: true, force: true });
2340
2546
  }
2341
- fs12.renameSync(installed, backupPath);
2547
+ fs13.renameSync(installed, backupPath);
2342
2548
  }
2343
2549
  function removeItem(item, claudeDir) {
2344
2550
  const installed = getInstalledPath(item, claudeDir);
2345
- if (fs12.existsSync(installed)) {
2346
- fs12.rmSync(installed, { recursive: true, force: true });
2551
+ if (fs13.existsSync(installed)) {
2552
+ fs13.rmSync(installed, { recursive: true, force: true });
2347
2553
  }
2348
2554
  }
2349
2555
  function getSourcePath(item, root) {
2350
2556
  switch (item.type) {
2351
2557
  case "skill":
2352
- return path15.join(root, "skills", item.name);
2558
+ return path16.join(root, "skills", item.name);
2353
2559
  case "rule": {
2354
- const direct = path15.join(root, "rules", item.name);
2355
- if (fs12.existsSync(direct)) return direct;
2356
- return path15.join(root, "rules", "gamedev", item.name);
2560
+ const direct = path16.join(root, "rules", item.name);
2561
+ if (fs13.existsSync(direct)) return direct;
2562
+ return path16.join(root, "rules", "gamedev", item.name);
2357
2563
  }
2358
2564
  case "agent":
2359
- return path15.join(root, "agents", item.name);
2565
+ return path16.join(root, "agents", item.name);
2360
2566
  case "hook":
2361
- return path15.join(root, "hooks", item.name);
2567
+ return path16.join(root, "hooks", item.name);
2362
2568
  default:
2363
2569
  return null;
2364
2570
  }
@@ -2366,13 +2572,13 @@ function getSourcePath(item, root) {
2366
2572
  function getInstalledPath(item, claudeDir) {
2367
2573
  switch (item.type) {
2368
2574
  case "skill":
2369
- return path15.join(claudeDir, "skills", item.name);
2575
+ return path16.join(claudeDir, "skills", item.name);
2370
2576
  case "rule":
2371
- return path15.join(claudeDir, "rules", item.name);
2577
+ return path16.join(claudeDir, "rules", item.name);
2372
2578
  case "agent":
2373
- return path15.join(claudeDir, "agents", item.name);
2579
+ return path16.join(claudeDir, "agents", item.name);
2374
2580
  case "hook":
2375
- return path15.join(claudeDir, "hooks", item.name);
2581
+ return path16.join(claudeDir, "hooks", item.name);
2376
2582
  }
2377
2583
  }
2378
2584
 
@@ -2407,8 +2613,8 @@ async function cleanCommand(opts) {
2407
2613
  }
2408
2614
 
2409
2615
  // src/commands/worktree.ts
2410
- import fs13 from "fs";
2411
- import path16 from "path";
2616
+ import fs14 from "fs";
2617
+ import path17 from "path";
2412
2618
  import { execSync as execSync2 } from "child_process";
2413
2619
  import chalk9 from "chalk";
2414
2620
  async function worktreeCommand(branch, opts) {
@@ -2419,8 +2625,8 @@ async function worktreeCommand(branch, opts) {
2419
2625
  process.exit(1);
2420
2626
  }
2421
2627
  const repoRoot = wtInfo.isWorktree ? wtInfo.mainWorktreePath : cwd;
2422
- const worktreePath = opts.path ? path16.resolve(opts.path) : defaultWorktreePath(repoRoot, branch);
2423
- if (fs13.existsSync(worktreePath)) {
2628
+ const worktreePath = opts.path ? path17.resolve(opts.path) : defaultWorktreePath(repoRoot, branch);
2629
+ if (fs14.existsSync(worktreePath)) {
2424
2630
  console.error(
2425
2631
  chalk9.red(`Path already exists: ${worktreePath}`)
2426
2632
  );
@@ -2467,7 +2673,7 @@ async function worktreeCommand(branch, opts) {
2467
2673
  }
2468
2674
  console.log(" Installing Arcane...");
2469
2675
  const root = getPackageRoot();
2470
- const profilesDir = path16.join(root, "profiles");
2676
+ const profilesDir = path17.join(root, "profiles");
2471
2677
  const profileNames = profileExpr.split("+").filter(Boolean);
2472
2678
  const merged = mergeProfiles(profilesDir, profileNames);
2473
2679
  const shareFrom = opts.share !== false ? cwd : void 0;
@@ -2503,13 +2709,13 @@ async function worktreeCommand(branch, opts) {
2503
2709
  }
2504
2710
  }
2505
2711
  if (opts.isolate) {
2506
- const isolateScript = path16.join(
2712
+ const isolateScript = path17.join(
2507
2713
  worktreePath,
2508
2714
  ".claude",
2509
2715
  "skills",
2510
2716
  "worktree-isolation"
2511
2717
  );
2512
- if (fs13.existsSync(isolateScript)) {
2718
+ if (fs14.existsSync(isolateScript)) {
2513
2719
  console.log(" Applying worktree isolation...");
2514
2720
  try {
2515
2721
  execSync2(
@@ -2544,13 +2750,13 @@ async function worktreeCommand(branch, opts) {
2544
2750
  }
2545
2751
 
2546
2752
  // src/commands/global.ts
2547
- import fs14 from "fs";
2548
- import path17 from "path";
2753
+ import fs15 from "fs";
2754
+ import path18 from "path";
2549
2755
  import os5 from "os";
2550
2756
  import chalk10 from "chalk";
2551
- var CLAUDE_HOME = path17.join(os5.homedir(), ".claude");
2552
- var SCRIPTS_DIR = path17.join(CLAUDE_HOME, "scripts", "worktree-isolation");
2553
- var SETTINGS_PATH = path17.join(CLAUDE_HOME, "settings.json");
2757
+ var CLAUDE_HOME = path18.join(os5.homedir(), ".claude");
2758
+ var SCRIPTS_DIR = path18.join(CLAUDE_HOME, "scripts", "worktree-isolation");
2759
+ var SETTINGS_PATH = path18.join(CLAUDE_HOME, "settings.json");
2554
2760
  async function globalCommand(opts) {
2555
2761
  if (opts.status) {
2556
2762
  showStatus();
@@ -2564,8 +2770,8 @@ async function globalCommand(opts) {
2564
2770
  }
2565
2771
  function showStatus() {
2566
2772
  console.log(chalk10.bold("\n=== Arcane Global Status ===\n"));
2567
- const hasScripts = fs14.existsSync(
2568
- path17.join(SCRIPTS_DIR, "apply.py")
2773
+ const hasScripts = fs15.existsSync(
2774
+ path18.join(SCRIPTS_DIR, "apply.py")
2569
2775
  );
2570
2776
  console.log(
2571
2777
  ` Worktree scripts: ${hasScripts ? chalk10.green("installed") : chalk10.dim("not installed")}`
@@ -2583,32 +2789,32 @@ function showStatus() {
2583
2789
  function installGlobal() {
2584
2790
  console.log(chalk10.bold("\nInstalling Arcane global hooks...\n"));
2585
2791
  const root = getPackageRoot();
2586
- const scriptsSrc = path17.join(
2792
+ const scriptsSrc = path18.join(
2587
2793
  root,
2588
2794
  "skills",
2589
2795
  "worktree-isolation",
2590
2796
  "scripts"
2591
2797
  );
2592
- if (!fs14.existsSync(scriptsSrc)) {
2798
+ if (!fs15.existsSync(scriptsSrc)) {
2593
2799
  console.error(
2594
2800
  chalk10.red(" worktree-isolation scripts not found in package.")
2595
2801
  );
2596
2802
  process.exit(1);
2597
2803
  }
2598
- fs14.mkdirSync(SCRIPTS_DIR, { recursive: true });
2804
+ fs15.mkdirSync(SCRIPTS_DIR, { recursive: true });
2599
2805
  for (const file of ["apply.py", "lib.py"]) {
2600
- const src = path17.join(scriptsSrc, file);
2601
- const dst = path17.join(SCRIPTS_DIR, file);
2602
- if (!fs14.existsSync(src)) {
2806
+ const src = path18.join(scriptsSrc, file);
2807
+ const dst = path18.join(SCRIPTS_DIR, file);
2808
+ if (!fs15.existsSync(src)) {
2603
2809
  console.warn(chalk10.yellow(` WARN: ${file} not found, skipping`));
2604
2810
  continue;
2605
2811
  }
2606
- if (fs14.existsSync(dst) && fs14.readFileSync(src).equals(fs14.readFileSync(dst))) {
2812
+ if (fs15.existsSync(dst) && fs15.readFileSync(src).equals(fs15.readFileSync(dst))) {
2607
2813
  console.log(chalk10.dim(` [skip] ${file} (already up-to-date)`));
2608
2814
  } else {
2609
- fs14.copyFileSync(src, dst);
2815
+ fs15.copyFileSync(src, dst);
2610
2816
  if (process.platform !== "win32") {
2611
- fs14.chmodSync(dst, 493);
2817
+ fs15.chmodSync(dst, 493);
2612
2818
  }
2613
2819
  console.log(chalk10.green(` [ok] ${file} -> ${SCRIPTS_DIR}`));
2614
2820
  }
@@ -2632,8 +2838,8 @@ function installGlobal() {
2632
2838
  }
2633
2839
  function removeGlobal() {
2634
2840
  console.log(chalk10.bold("\nRemoving Arcane global hooks...\n"));
2635
- if (fs14.existsSync(SCRIPTS_DIR)) {
2636
- fs14.rmSync(SCRIPTS_DIR, { recursive: true, force: true });
2841
+ if (fs15.existsSync(SCRIPTS_DIR)) {
2842
+ fs15.rmSync(SCRIPTS_DIR, { recursive: true, force: true });
2637
2843
  console.log(chalk10.green(" [ok] Removed worktree-isolation scripts"));
2638
2844
  } else {
2639
2845
  console.log(chalk10.dim(" [skip] Scripts not found"));
@@ -2647,16 +2853,16 @@ function removeGlobal() {
2647
2853
  console.log(chalk10.bold.green("\n Global hooks removed.\n"));
2648
2854
  }
2649
2855
  function readSettings() {
2650
- if (!fs14.existsSync(SETTINGS_PATH)) return {};
2856
+ if (!fs15.existsSync(SETTINGS_PATH)) return {};
2651
2857
  try {
2652
- return JSON.parse(fs14.readFileSync(SETTINGS_PATH, "utf-8"));
2858
+ return JSON.parse(fs15.readFileSync(SETTINGS_PATH, "utf-8"));
2653
2859
  } catch {
2654
2860
  return {};
2655
2861
  }
2656
2862
  }
2657
2863
  function writeSettings(settings) {
2658
- fs14.mkdirSync(path17.dirname(SETTINGS_PATH), { recursive: true });
2659
- fs14.writeFileSync(
2864
+ fs15.mkdirSync(path18.dirname(SETTINGS_PATH), { recursive: true });
2865
+ fs15.writeFileSync(
2660
2866
  SETTINGS_PATH,
2661
2867
  JSON.stringify(settings, null, 2) + "\n",
2662
2868
  "utf-8"
@@ -2713,11 +2919,11 @@ function removeHookFromSettings() {
2713
2919
  }
2714
2920
 
2715
2921
  // src/update-check.ts
2716
- import fs15 from "fs";
2717
- import path18 from "path";
2922
+ import fs16 from "fs";
2923
+ import path19 from "path";
2718
2924
  import os6 from "os";
2719
2925
  import chalk11 from "chalk";
2720
- var CHECK_FILE = path18.join(os6.homedir(), ".arcane", "last-check.json");
2926
+ var CHECK_FILE = path19.join(os6.homedir(), ".arcane", "last-check.json");
2721
2927
  var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
2722
2928
  var GITHUB_OWNER2 = "SebastianLuser";
2723
2929
  var GITHUB_REPO2 = "Claude-Code-Arcane";
@@ -2842,19 +3048,19 @@ async function getLatestNpmVersion() {
2842
3048
  }
2843
3049
  function readCachedCheck() {
2844
3050
  try {
2845
- if (!fs15.existsSync(CHECK_FILE)) return null;
2846
- return JSON.parse(fs15.readFileSync(CHECK_FILE, "utf-8"));
3051
+ if (!fs16.existsSync(CHECK_FILE)) return null;
3052
+ return JSON.parse(fs16.readFileSync(CHECK_FILE, "utf-8"));
2847
3053
  } catch {
2848
3054
  return null;
2849
3055
  }
2850
3056
  }
2851
3057
  function writeCachedCheck(result) {
2852
3058
  try {
2853
- const dir = path18.dirname(CHECK_FILE);
2854
- if (!fs15.existsSync(dir)) {
2855
- fs15.mkdirSync(dir, { recursive: true });
3059
+ const dir = path19.dirname(CHECK_FILE);
3060
+ if (!fs16.existsSync(dir)) {
3061
+ fs16.mkdirSync(dir, { recursive: true });
2856
3062
  }
2857
- fs15.writeFileSync(CHECK_FILE, JSON.stringify(result, null, 2) + "\n");
3063
+ fs16.writeFileSync(CHECK_FILE, JSON.stringify(result, null, 2) + "\n");
2858
3064
  } catch {
2859
3065
  }
2860
3066
  }
@@ -2872,9 +3078,9 @@ program.command("install [profile]").description(
2872
3078
  ).option("--source <source>", "Content source: auto, github, or bundled", "auto").action(async (profile, opts) => {
2873
3079
  await installCommand(profile, opts);
2874
3080
  });
2875
- program.command("add <items...>").description(
2876
- "Add skills or profiles. Use +profileName for profiles, skillName for individual skills."
2877
- ).action(async (items) => {
3081
+ program.command("add [items...]").description(
3082
+ "Add skills or profiles. Use +profileName for profiles, skillName for individual skills. Without args, opens an interactive picker."
3083
+ ).action(async (items = []) => {
2878
3084
  await addCommand(items);
2879
3085
  });
2880
3086
  program.command("remove <items...>").description(