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