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