claude-code-arcane 2.9.1 → 2.9.2
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 +7 -0
- package/dist/cli.js +107 -23
- package/docs/USER-GUIDE.md +16 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [2.9.2](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.9.1...v2.9.2) (2026-09-07)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **update:** only remove content the manifest owns ([d6eec05](https://github.com/SebastianLuser/claude-code-arcane/commit/d6eec05475bb491c8853078d9920c7dc4544e7b3))
|
|
7
|
+
|
|
1
8
|
## [2.9.1](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.9.0...v2.9.1) (2026-09-04)
|
|
2
9
|
|
|
3
10
|
|
package/dist/cli.js
CHANGED
|
@@ -742,6 +742,7 @@ function computeContentHashes(claudeDir) {
|
|
|
742
742
|
const skillsDir = path7.join(claudeDir, "skills");
|
|
743
743
|
if (fs6.existsSync(skillsDir)) {
|
|
744
744
|
for (const entry of fs6.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
745
|
+
if (entry.name.startsWith("_")) continue;
|
|
745
746
|
if (entry.isDirectory()) {
|
|
746
747
|
const dirHash = hashDirectory(path7.join(skillsDir, entry.name));
|
|
747
748
|
const combined = createHash("sha256").update(JSON.stringify(dirHash)).digest("hex");
|
|
@@ -2315,6 +2316,7 @@ function safeVersion() {
|
|
|
2315
2316
|
}
|
|
2316
2317
|
|
|
2317
2318
|
// src/commands/update.ts
|
|
2319
|
+
var TRASH_DIR = ".arcane-trash";
|
|
2318
2320
|
async function updateCommand(opts) {
|
|
2319
2321
|
if (opts.here) {
|
|
2320
2322
|
await updateTarget(process.cwd(), opts);
|
|
@@ -2444,7 +2446,9 @@ Update available: ${chalk7.red(installedVersion)} -> ${chalk7.green(currentVersi
|
|
|
2444
2446
|
const items = computeUpdatePlan(
|
|
2445
2447
|
manifest.content_hashes ?? null,
|
|
2446
2448
|
installedHashes,
|
|
2447
|
-
sourceHashes
|
|
2449
|
+
sourceHashes,
|
|
2450
|
+
makeOwnershipCheck(manifest),
|
|
2451
|
+
granularAgentDivisions(merged.agents)
|
|
2448
2452
|
);
|
|
2449
2453
|
const updates = items.filter((i) => i.action === "update" || i.action === "add" || i.action === "conflict");
|
|
2450
2454
|
const skipped = items.filter((i) => i.action === "skip-customized" || i.action === "skip-unchanged");
|
|
@@ -2459,7 +2463,7 @@ Update available: ${chalk7.red(installedVersion)} -> ${chalk7.green(currentVersi
|
|
|
2459
2463
|
arcane_version: currentVersion,
|
|
2460
2464
|
source_version: currentVersion,
|
|
2461
2465
|
updated_at: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
2462
|
-
content_hashes: installedHashes,
|
|
2466
|
+
content_hashes: stripForeign(installedHashes, items),
|
|
2463
2467
|
installed_skills: merged.skills,
|
|
2464
2468
|
installed_rules: allRules,
|
|
2465
2469
|
installed_agents: merged.agents,
|
|
@@ -2500,7 +2504,7 @@ Refreshed ${unhashed.length} file(s); no other changes to apply.`) : chalk7.gree
|
|
|
2500
2504
|
arcane_version: currentVersion,
|
|
2501
2505
|
source_version: currentVersion,
|
|
2502
2506
|
updated_at: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
2503
|
-
content_hashes: newHashes,
|
|
2507
|
+
content_hashes: stripForeign(newHashes, items),
|
|
2504
2508
|
installed_skills: merged.skills,
|
|
2505
2509
|
installed_rules: allRules,
|
|
2506
2510
|
installed_agents: merged.agents,
|
|
@@ -2530,7 +2534,56 @@ function printGeneralSummary(results, isDryRun) {
|
|
|
2530
2534
|
${prefix}Done. ${results.length} installation${results.length === 1 ? "" : "s"} processed:`));
|
|
2531
2535
|
console.log(` ${chalk7.green(updated)} ${isDryRun ? "would change" : "updated"}, ${chalk7.dim(unchanged + " unchanged")}`);
|
|
2532
2536
|
}
|
|
2533
|
-
function
|
|
2537
|
+
function makeOwnershipCheck(manifest) {
|
|
2538
|
+
const hashes = manifest.content_hashes;
|
|
2539
|
+
if (hashes) {
|
|
2540
|
+
return (type, name) => name in (hashes[type] ?? {});
|
|
2541
|
+
}
|
|
2542
|
+
const skills = new Set(manifest.installed_skills ?? []);
|
|
2543
|
+
const rules = new Set(
|
|
2544
|
+
(manifest.installed_rules ?? []).map((r) => r.endsWith(".md") ? r : `${r}.md`)
|
|
2545
|
+
);
|
|
2546
|
+
const agents = new Set(
|
|
2547
|
+
(manifest.installed_agents ?? []).map((a) => parseAgentEntry(a).division)
|
|
2548
|
+
);
|
|
2549
|
+
return (type, name) => {
|
|
2550
|
+
switch (type) {
|
|
2551
|
+
case "skills":
|
|
2552
|
+
return skills.has(name);
|
|
2553
|
+
case "rules":
|
|
2554
|
+
return rules.has(name);
|
|
2555
|
+
case "agents":
|
|
2556
|
+
return agents.has(name);
|
|
2557
|
+
case "hooks":
|
|
2558
|
+
return true;
|
|
2559
|
+
}
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
function granularAgentDivisions(entries) {
|
|
2563
|
+
const whole = /* @__PURE__ */ new Set();
|
|
2564
|
+
const partial = /* @__PURE__ */ new Set();
|
|
2565
|
+
for (const raw of entries) {
|
|
2566
|
+
const { division, agent } = parseAgentEntry(raw);
|
|
2567
|
+
(agent ? partial : whole).add(division);
|
|
2568
|
+
}
|
|
2569
|
+
for (const division of whole) partial.delete(division);
|
|
2570
|
+
return partial;
|
|
2571
|
+
}
|
|
2572
|
+
function stripForeign(hashes, items) {
|
|
2573
|
+
const stripped = {
|
|
2574
|
+
skills: { ...hashes.skills },
|
|
2575
|
+
rules: { ...hashes.rules },
|
|
2576
|
+
agents: { ...hashes.agents },
|
|
2577
|
+
hooks: { ...hashes.hooks }
|
|
2578
|
+
};
|
|
2579
|
+
for (const item of items) {
|
|
2580
|
+
if (item.action !== "keep-foreign") continue;
|
|
2581
|
+
const bucket = `${item.type}s`;
|
|
2582
|
+
delete stripped[bucket][item.name];
|
|
2583
|
+
}
|
|
2584
|
+
return stripped;
|
|
2585
|
+
}
|
|
2586
|
+
function computeUpdatePlan(manifestHashes, installedHashes, sourceHashes, owned, exemptAgentDivisions = /* @__PURE__ */ new Set()) {
|
|
2534
2587
|
const items = [];
|
|
2535
2588
|
for (const contentType of ["skills", "rules", "agents", "hooks"]) {
|
|
2536
2589
|
const itemType = contentType === "skills" ? "skill" : contentType === "rules" ? "rule" : contentType === "agents" ? "agent" : "hook";
|
|
@@ -2543,10 +2596,11 @@ function computeUpdatePlan(manifestHashes, installedHashes, sourceHashes) {
|
|
|
2543
2596
|
...Object.keys(source)
|
|
2544
2597
|
]);
|
|
2545
2598
|
for (const key of allKeys) {
|
|
2599
|
+
if (contentType === "agents" && exemptAgentDivisions.has(key)) continue;
|
|
2546
2600
|
const mHash = manifest[key] ?? null;
|
|
2547
2601
|
const iHash = installed[key] ?? null;
|
|
2548
2602
|
const sHash = source[key] ?? null;
|
|
2549
|
-
const action = resolveAction(mHash, iHash, sHash);
|
|
2603
|
+
const action = resolveAction(mHash, iHash, sHash, owned(contentType, key));
|
|
2550
2604
|
if (action) {
|
|
2551
2605
|
items.push({ type: itemType, name: key, action });
|
|
2552
2606
|
}
|
|
@@ -2554,12 +2608,13 @@ function computeUpdatePlan(manifestHashes, installedHashes, sourceHashes) {
|
|
|
2554
2608
|
}
|
|
2555
2609
|
return items;
|
|
2556
2610
|
}
|
|
2557
|
-
function resolveAction(manifestHash, installedHash, sourceHash) {
|
|
2611
|
+
function resolveAction(manifestHash, installedHash, sourceHash, owned) {
|
|
2558
2612
|
if (!sourceHash && !installedHash) return null;
|
|
2559
2613
|
if (sourceHash && !installedHash) return "add";
|
|
2560
|
-
if (!sourceHash && installedHash) return "remove";
|
|
2614
|
+
if (!sourceHash && installedHash) return owned ? "remove" : "keep-foreign";
|
|
2561
2615
|
if (!manifestHash) {
|
|
2562
2616
|
if (installedHash === sourceHash) return "skip-unchanged";
|
|
2617
|
+
if (!owned) return "conflict";
|
|
2563
2618
|
return "update";
|
|
2564
2619
|
}
|
|
2565
2620
|
if (manifestHash === installedHash && installedHash === sourceHash) return "skip-unchanged";
|
|
@@ -2575,6 +2630,7 @@ function printUpdateSummary(items, isDryRun) {
|
|
|
2575
2630
|
const conflicts = items.filter((i) => i.action === "conflict");
|
|
2576
2631
|
const customized = items.filter((i) => i.action === "skip-customized");
|
|
2577
2632
|
const removed = items.filter((i) => i.action === "remove");
|
|
2633
|
+
const foreign = items.filter((i) => i.action === "keep-foreign");
|
|
2578
2634
|
if (updates.length > 0) {
|
|
2579
2635
|
console.log(`
|
|
2580
2636
|
${prefix}${chalk7.cyan("Will update:")}`);
|
|
@@ -2591,14 +2647,23 @@ ${prefix}${chalk7.cyan("Will add:")}`);
|
|
|
2591
2647
|
}
|
|
2592
2648
|
if (removed.length > 0) {
|
|
2593
2649
|
console.log(`
|
|
2594
|
-
${prefix}${chalk7.cyan("Will remove
|
|
2650
|
+
${prefix}${chalk7.cyan("Will remove")} ${chalk7.dim(`(moved to .claude/${TRASH_DIR}/)`)}:`);
|
|
2595
2651
|
for (const item of removed) {
|
|
2596
2652
|
console.log(` ${chalk7.red("-")} ${item.type}: ${item.name}`);
|
|
2597
2653
|
}
|
|
2598
2654
|
}
|
|
2599
|
-
if (
|
|
2655
|
+
if (foreign.length > 0) {
|
|
2600
2656
|
console.log(`
|
|
2601
|
-
${
|
|
2657
|
+
${chalk7.cyan("Left alone")} ${chalk7.dim("(not installed by Arcane)")}:`);
|
|
2658
|
+
for (const item of foreign) {
|
|
2659
|
+
console.log(` ${chalk7.dim("=")} ${item.type}: ${item.name}`);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
if (conflicts.length > 0) {
|
|
2663
|
+
console.log(
|
|
2664
|
+
`
|
|
2665
|
+
${prefix}${chalk7.yellow("Conflicts (will update)")} ${chalk7.dim(`(old copy to .claude/${TRASH_DIR}/)`)}:`
|
|
2666
|
+
);
|
|
2602
2667
|
for (const item of conflicts) {
|
|
2603
2668
|
console.log(` ${chalk7.yellow("!")} ${item.type}: ${item.name}`);
|
|
2604
2669
|
}
|
|
@@ -2613,6 +2678,7 @@ ${prefix}${chalk7.dim("Skipped (locally customized):")}`);
|
|
|
2613
2678
|
}
|
|
2614
2679
|
function applyUpdates(items, root, claudeDir, merged, force) {
|
|
2615
2680
|
for (const item of items) {
|
|
2681
|
+
if (item.action === "keep-foreign") continue;
|
|
2616
2682
|
if (item.action === "skip-unchanged" || item.action === "skip-customized") {
|
|
2617
2683
|
if (item.action === "skip-customized" && force) {
|
|
2618
2684
|
applyItem(item, root, claudeDir);
|
|
@@ -2620,7 +2686,7 @@ function applyUpdates(items, root, claudeDir, merged, force) {
|
|
|
2620
2686
|
continue;
|
|
2621
2687
|
}
|
|
2622
2688
|
if (item.action === "conflict") {
|
|
2623
|
-
|
|
2689
|
+
quarantineItem(item, claudeDir);
|
|
2624
2690
|
applyItem(item, root, claudeDir);
|
|
2625
2691
|
continue;
|
|
2626
2692
|
}
|
|
@@ -2629,7 +2695,7 @@ function applyUpdates(items, root, claudeDir, merged, force) {
|
|
|
2629
2695
|
continue;
|
|
2630
2696
|
}
|
|
2631
2697
|
if (item.action === "remove") {
|
|
2632
|
-
|
|
2698
|
+
quarantineItem(item, claudeDir);
|
|
2633
2699
|
}
|
|
2634
2700
|
}
|
|
2635
2701
|
}
|
|
@@ -2650,6 +2716,29 @@ function syncUnhashedFiles(root, claudeDir, merged, dryRun) {
|
|
|
2650
2716
|
label: `output-styles/${style}.md`
|
|
2651
2717
|
});
|
|
2652
2718
|
}
|
|
2719
|
+
for (const raw of merged.agents) {
|
|
2720
|
+
const { division, agent } = parseAgentEntry(raw);
|
|
2721
|
+
if (!agent) continue;
|
|
2722
|
+
targets.push({
|
|
2723
|
+
src: path17.join(root, "agents", division, `${agent}.md`),
|
|
2724
|
+
dst: path17.join(claudeDir, "agents", division, `${agent}.md`),
|
|
2725
|
+
label: `agents/${division}/${agent}.md`
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2728
|
+
if (merged.rules.gamedev.length > 0) {
|
|
2729
|
+
const templatesSrc = path17.join(root, "templates", "gamedev");
|
|
2730
|
+
if (fs14.existsSync(templatesSrc)) {
|
|
2731
|
+
for (const rel of fs14.readdirSync(templatesSrc, { recursive: true })) {
|
|
2732
|
+
const src = path17.join(templatesSrc, rel);
|
|
2733
|
+
if (!fs14.statSync(src).isFile()) continue;
|
|
2734
|
+
targets.push({
|
|
2735
|
+
src,
|
|
2736
|
+
dst: path17.join(claudeDir, "skills", "_templates", rel),
|
|
2737
|
+
label: `skills/_templates/${String(rel).split(path17.sep).join("/")}`
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2653
2742
|
const changed = [];
|
|
2654
2743
|
for (const t of targets) {
|
|
2655
2744
|
if (!fs14.existsSync(t.src)) continue;
|
|
@@ -2702,20 +2791,15 @@ function applyItem(item, root, claudeDir) {
|
|
|
2702
2791
|
fs14.copyFileSync(src, dst);
|
|
2703
2792
|
}
|
|
2704
2793
|
}
|
|
2705
|
-
function
|
|
2794
|
+
function quarantineItem(item, claudeDir) {
|
|
2706
2795
|
const installed = getInstalledPath(item, claudeDir);
|
|
2707
2796
|
if (!fs14.existsSync(installed)) return;
|
|
2708
|
-
const
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
fs14.renameSync(installed, backupPath);
|
|
2713
|
-
}
|
|
2714
|
-
function removeItem(item, claudeDir) {
|
|
2715
|
-
const installed = getInstalledPath(item, claudeDir);
|
|
2716
|
-
if (fs14.existsSync(installed)) {
|
|
2717
|
-
fs14.rmSync(installed, { recursive: true, force: true });
|
|
2797
|
+
const quarantine = path17.join(claudeDir, TRASH_DIR, `${item.type}s`, item.name);
|
|
2798
|
+
ensureDir(path17.dirname(quarantine));
|
|
2799
|
+
if (fs14.existsSync(quarantine)) {
|
|
2800
|
+
fs14.rmSync(quarantine, { recursive: true, force: true });
|
|
2718
2801
|
}
|
|
2802
|
+
fs14.renameSync(installed, quarantine);
|
|
2719
2803
|
}
|
|
2720
2804
|
function getSourcePath(item, root) {
|
|
2721
2805
|
switch (item.type) {
|
package/docs/USER-GUIDE.md
CHANGED
|
@@ -146,10 +146,26 @@ Shows installed profiles, skill count, rules, agents, installation date, and wor
|
|
|
146
146
|
```bash
|
|
147
147
|
npx claude-code-arcane update
|
|
148
148
|
|
|
149
|
+
# Only this repo, instead of every registered installation
|
|
150
|
+
npx claude-code-arcane update --here
|
|
151
|
+
|
|
152
|
+
# Preview
|
|
153
|
+
npx claude-code-arcane update --dry-run
|
|
154
|
+
|
|
149
155
|
# Quiet mode (for hooks — fails silently)
|
|
150
156
|
npx claude-code-arcane update --quiet
|
|
151
157
|
```
|
|
152
158
|
|
|
159
|
+
**What it will and will not touch.** Arcane only updates or removes content its own
|
|
160
|
+
manifest records having installed. A skill or agent you wrote by hand in `.claude/`
|
|
161
|
+
is reported as `Left alone (not installed by Arcane)` and never modified — including
|
|
162
|
+
under `--force`, which only exists to overwrite *Arcane's* files that you edited
|
|
163
|
+
locally. Files you edited that Arcane does own are skipped as `customized`.
|
|
164
|
+
|
|
165
|
+
When a skill really does leave the profile, it is moved to `.claude/.arcane-trash/`
|
|
166
|
+
rather than deleted, so a wrong call is recoverable. The trash sits outside
|
|
167
|
+
`.claude/skills/`, so a removed skill stops loading immediately.
|
|
168
|
+
|
|
153
169
|
### `npx claude-code-arcane clean` — Remove Arcane from a project
|
|
154
170
|
|
|
155
171
|
```bash
|