claude-code-arcane 2.9.1 → 2.9.3
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/dist/cli.js +153 -48
- package/docs/USER-GUIDE.md +16 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [2.9.3](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.9.2...v2.9.3) (2026-09-07)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **update:** resolve the content version once per run, never fabricate it ([ef78ecf](https://github.com/SebastianLuser/claude-code-arcane/commit/ef78ecf6c5ab17f1a51ea6ae27ef9fa161594798))
|
|
7
|
+
|
|
8
|
+
## [2.9.2](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.9.1...v2.9.2) (2026-09-07)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **update:** only remove content the manifest owns ([d6eec05](https://github.com/SebastianLuser/claude-code-arcane/commit/d6eec05475bb491c8853078d9920c7dc4544e7b3))
|
|
14
|
+
|
|
1
15
|
## [2.9.1](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.9.0...v2.9.1) (2026-09-04)
|
|
2
16
|
|
|
3
17
|
|
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");
|
|
@@ -1438,22 +1439,18 @@ var GitHubContentSource = class {
|
|
|
1438
1439
|
fs9.rmSync(tmpDir, { recursive: true, force: true });
|
|
1439
1440
|
}
|
|
1440
1441
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
return false;
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
async getVersion() {
|
|
1442
|
+
/**
|
|
1443
|
+
* The head commit of the branch, or null when GitHub cannot answer.
|
|
1444
|
+
*
|
|
1445
|
+
* One request, memoized, shared by isAvailable() and getVersion(). It used to be
|
|
1446
|
+
* two - a HEAD to answer "is it reachable?" and a GET to read the sha - against an
|
|
1447
|
+
* endpoint that allows 60 requests an hour unauthenticated. That doubled the cost of
|
|
1448
|
+
* every resolution and, worse, opened a window between the two calls: the HEAD spent
|
|
1449
|
+
* the last request of the hour, the GET came back 403, and the caller had already
|
|
1450
|
+
* committed to the github source. That window is where the fabricated versions came
|
|
1451
|
+
* from.
|
|
1452
|
+
*/
|
|
1453
|
+
async resolveVersion() {
|
|
1457
1454
|
if (this.resolvedVersion) return this.resolvedVersion;
|
|
1458
1455
|
try {
|
|
1459
1456
|
const response = await fetch(
|
|
@@ -1466,17 +1463,36 @@ var GitHubContentSource = class {
|
|
|
1466
1463
|
signal: AbortSignal.timeout(1e4)
|
|
1467
1464
|
}
|
|
1468
1465
|
);
|
|
1469
|
-
if (!response.ok)
|
|
1470
|
-
throw new Error(`GitHub API returned ${response.status}`);
|
|
1471
|
-
}
|
|
1466
|
+
if (!response.ok) return null;
|
|
1472
1467
|
const data = await response.json();
|
|
1473
1468
|
this.resolvedVersion = data.sha.substring(0, 12);
|
|
1474
1469
|
return this.resolvedVersion;
|
|
1475
1470
|
} catch {
|
|
1476
|
-
|
|
1477
|
-
return this.resolvedVersion;
|
|
1471
|
+
return null;
|
|
1478
1472
|
}
|
|
1479
1473
|
}
|
|
1474
|
+
async isAvailable() {
|
|
1475
|
+
return await this.resolveVersion() !== null;
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Throws when the version cannot be read, rather than inventing one.
|
|
1479
|
+
*
|
|
1480
|
+
* It used to answer `github-${Date.now()}` on any failure, and that string went on to
|
|
1481
|
+
* be an identity: it stamped `source_version` in the manifest, named a cache directory,
|
|
1482
|
+
* and made the install permanently un-diffable against any real version - it never
|
|
1483
|
+
* matched again, so every later run reported an update and no run ever settled. A
|
|
1484
|
+
* transient 403 became durable state on disk. Callers that cannot proceed without a
|
|
1485
|
+
* version now fall back to the cache, which knows a real one.
|
|
1486
|
+
*/
|
|
1487
|
+
async getVersion() {
|
|
1488
|
+
const version = await this.resolveVersion();
|
|
1489
|
+
if (!version) {
|
|
1490
|
+
throw new Error(
|
|
1491
|
+
"Could not read the content version from GitHub (offline, or the unauthenticated API rate limit is exhausted - it resets hourly)."
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
return version;
|
|
1495
|
+
}
|
|
1480
1496
|
async downloadAndExtract(destDir) {
|
|
1481
1497
|
const tarballUrl = `https://api.github.com/repos/${this.owner}/${this.repo}/tarball/${this.branch}`;
|
|
1482
1498
|
const response = await fetch(tarballUrl, {
|
|
@@ -1547,7 +1563,9 @@ async function resolveContentSource(opts = {}) {
|
|
|
1547
1563
|
return github;
|
|
1548
1564
|
}
|
|
1549
1565
|
if (preference === "github") {
|
|
1550
|
-
throw new Error(
|
|
1566
|
+
throw new Error(
|
|
1567
|
+
"GitHub source requested but not available. Check your internet connection, or wait for the unauthenticated API rate limit to reset (hourly)."
|
|
1568
|
+
);
|
|
1551
1569
|
}
|
|
1552
1570
|
const cached = findLatestCache();
|
|
1553
1571
|
if (cached) {
|
|
@@ -2315,6 +2333,7 @@ function safeVersion() {
|
|
|
2315
2333
|
}
|
|
2316
2334
|
|
|
2317
2335
|
// src/commands/update.ts
|
|
2336
|
+
var TRASH_DIR = ".arcane-trash";
|
|
2318
2337
|
async function updateCommand(opts) {
|
|
2319
2338
|
if (opts.here) {
|
|
2320
2339
|
await updateTarget(process.cwd(), opts);
|
|
@@ -2355,6 +2374,10 @@ async function updateCommand(opts) {
|
|
|
2355
2374
|
Updating ${targets.length} installation${targets.length === 1 ? "" : "s"}...`)
|
|
2356
2375
|
);
|
|
2357
2376
|
}
|
|
2377
|
+
const sharedSource = await resolveContentSource({
|
|
2378
|
+
source: opts.source ?? "auto",
|
|
2379
|
+
quiet: opts.quiet
|
|
2380
|
+
});
|
|
2358
2381
|
const results = [];
|
|
2359
2382
|
for (const target of targets) {
|
|
2360
2383
|
if (!opts.quiet) {
|
|
@@ -2362,7 +2385,7 @@ Updating ${targets.length} installation${targets.length === 1 ? "" : "s"}...`)
|
|
|
2362
2385
|
\u2022 ${target}`));
|
|
2363
2386
|
}
|
|
2364
2387
|
try {
|
|
2365
|
-
results.push(await updateTarget(target, opts));
|
|
2388
|
+
results.push(await updateTarget(target, opts, sharedSource));
|
|
2366
2389
|
} catch (err) {
|
|
2367
2390
|
if (!opts.quiet) {
|
|
2368
2391
|
console.log(chalk7.red(` Update failed: ${err.message}`));
|
|
@@ -2380,7 +2403,7 @@ Updating ${targets.length} installation${targets.length === 1 ? "" : "s"}...`)
|
|
|
2380
2403
|
printGeneralSummary(results, opts.dryRun ?? false);
|
|
2381
2404
|
}
|
|
2382
2405
|
}
|
|
2383
|
-
async function updateTarget(target, opts) {
|
|
2406
|
+
async function updateTarget(target, opts, sharedSource) {
|
|
2384
2407
|
const manifest = readManifest(target);
|
|
2385
2408
|
if (!manifest) {
|
|
2386
2409
|
if (!opts.quiet) {
|
|
@@ -2388,7 +2411,7 @@ async function updateTarget(target, opts) {
|
|
|
2388
2411
|
}
|
|
2389
2412
|
return { target, status: "no-manifest", updated: 0, skipped: 0, removed: 0 };
|
|
2390
2413
|
}
|
|
2391
|
-
const contentSource = await resolveContentSource({
|
|
2414
|
+
const contentSource = sharedSource ?? await resolveContentSource({
|
|
2392
2415
|
source: opts.source ?? "auto",
|
|
2393
2416
|
quiet: opts.quiet
|
|
2394
2417
|
});
|
|
@@ -2444,7 +2467,9 @@ Update available: ${chalk7.red(installedVersion)} -> ${chalk7.green(currentVersi
|
|
|
2444
2467
|
const items = computeUpdatePlan(
|
|
2445
2468
|
manifest.content_hashes ?? null,
|
|
2446
2469
|
installedHashes,
|
|
2447
|
-
sourceHashes
|
|
2470
|
+
sourceHashes,
|
|
2471
|
+
makeOwnershipCheck(manifest),
|
|
2472
|
+
granularAgentDivisions(merged.agents)
|
|
2448
2473
|
);
|
|
2449
2474
|
const updates = items.filter((i) => i.action === "update" || i.action === "add" || i.action === "conflict");
|
|
2450
2475
|
const skipped = items.filter((i) => i.action === "skip-customized" || i.action === "skip-unchanged");
|
|
@@ -2459,7 +2484,7 @@ Update available: ${chalk7.red(installedVersion)} -> ${chalk7.green(currentVersi
|
|
|
2459
2484
|
arcane_version: currentVersion,
|
|
2460
2485
|
source_version: currentVersion,
|
|
2461
2486
|
updated_at: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
2462
|
-
content_hashes: installedHashes,
|
|
2487
|
+
content_hashes: stripForeign(installedHashes, items),
|
|
2463
2488
|
installed_skills: merged.skills,
|
|
2464
2489
|
installed_rules: allRules,
|
|
2465
2490
|
installed_agents: merged.agents,
|
|
@@ -2500,7 +2525,7 @@ Refreshed ${unhashed.length} file(s); no other changes to apply.`) : chalk7.gree
|
|
|
2500
2525
|
arcane_version: currentVersion,
|
|
2501
2526
|
source_version: currentVersion,
|
|
2502
2527
|
updated_at: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
2503
|
-
content_hashes: newHashes,
|
|
2528
|
+
content_hashes: stripForeign(newHashes, items),
|
|
2504
2529
|
installed_skills: merged.skills,
|
|
2505
2530
|
installed_rules: allRules,
|
|
2506
2531
|
installed_agents: merged.agents,
|
|
@@ -2530,7 +2555,56 @@ function printGeneralSummary(results, isDryRun) {
|
|
|
2530
2555
|
${prefix}Done. ${results.length} installation${results.length === 1 ? "" : "s"} processed:`));
|
|
2531
2556
|
console.log(` ${chalk7.green(updated)} ${isDryRun ? "would change" : "updated"}, ${chalk7.dim(unchanged + " unchanged")}`);
|
|
2532
2557
|
}
|
|
2533
|
-
function
|
|
2558
|
+
function makeOwnershipCheck(manifest) {
|
|
2559
|
+
const hashes = manifest.content_hashes;
|
|
2560
|
+
if (hashes) {
|
|
2561
|
+
return (type, name) => name in (hashes[type] ?? {});
|
|
2562
|
+
}
|
|
2563
|
+
const skills = new Set(manifest.installed_skills ?? []);
|
|
2564
|
+
const rules = new Set(
|
|
2565
|
+
(manifest.installed_rules ?? []).map((r) => r.endsWith(".md") ? r : `${r}.md`)
|
|
2566
|
+
);
|
|
2567
|
+
const agents = new Set(
|
|
2568
|
+
(manifest.installed_agents ?? []).map((a) => parseAgentEntry(a).division)
|
|
2569
|
+
);
|
|
2570
|
+
return (type, name) => {
|
|
2571
|
+
switch (type) {
|
|
2572
|
+
case "skills":
|
|
2573
|
+
return skills.has(name);
|
|
2574
|
+
case "rules":
|
|
2575
|
+
return rules.has(name);
|
|
2576
|
+
case "agents":
|
|
2577
|
+
return agents.has(name);
|
|
2578
|
+
case "hooks":
|
|
2579
|
+
return true;
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
function granularAgentDivisions(entries) {
|
|
2584
|
+
const whole = /* @__PURE__ */ new Set();
|
|
2585
|
+
const partial = /* @__PURE__ */ new Set();
|
|
2586
|
+
for (const raw of entries) {
|
|
2587
|
+
const { division, agent } = parseAgentEntry(raw);
|
|
2588
|
+
(agent ? partial : whole).add(division);
|
|
2589
|
+
}
|
|
2590
|
+
for (const division of whole) partial.delete(division);
|
|
2591
|
+
return partial;
|
|
2592
|
+
}
|
|
2593
|
+
function stripForeign(hashes, items) {
|
|
2594
|
+
const stripped = {
|
|
2595
|
+
skills: { ...hashes.skills },
|
|
2596
|
+
rules: { ...hashes.rules },
|
|
2597
|
+
agents: { ...hashes.agents },
|
|
2598
|
+
hooks: { ...hashes.hooks }
|
|
2599
|
+
};
|
|
2600
|
+
for (const item of items) {
|
|
2601
|
+
if (item.action !== "keep-foreign") continue;
|
|
2602
|
+
const bucket = `${item.type}s`;
|
|
2603
|
+
delete stripped[bucket][item.name];
|
|
2604
|
+
}
|
|
2605
|
+
return stripped;
|
|
2606
|
+
}
|
|
2607
|
+
function computeUpdatePlan(manifestHashes, installedHashes, sourceHashes, owned, exemptAgentDivisions = /* @__PURE__ */ new Set()) {
|
|
2534
2608
|
const items = [];
|
|
2535
2609
|
for (const contentType of ["skills", "rules", "agents", "hooks"]) {
|
|
2536
2610
|
const itemType = contentType === "skills" ? "skill" : contentType === "rules" ? "rule" : contentType === "agents" ? "agent" : "hook";
|
|
@@ -2543,10 +2617,11 @@ function computeUpdatePlan(manifestHashes, installedHashes, sourceHashes) {
|
|
|
2543
2617
|
...Object.keys(source)
|
|
2544
2618
|
]);
|
|
2545
2619
|
for (const key of allKeys) {
|
|
2620
|
+
if (contentType === "agents" && exemptAgentDivisions.has(key)) continue;
|
|
2546
2621
|
const mHash = manifest[key] ?? null;
|
|
2547
2622
|
const iHash = installed[key] ?? null;
|
|
2548
2623
|
const sHash = source[key] ?? null;
|
|
2549
|
-
const action = resolveAction(mHash, iHash, sHash);
|
|
2624
|
+
const action = resolveAction(mHash, iHash, sHash, owned(contentType, key));
|
|
2550
2625
|
if (action) {
|
|
2551
2626
|
items.push({ type: itemType, name: key, action });
|
|
2552
2627
|
}
|
|
@@ -2554,12 +2629,13 @@ function computeUpdatePlan(manifestHashes, installedHashes, sourceHashes) {
|
|
|
2554
2629
|
}
|
|
2555
2630
|
return items;
|
|
2556
2631
|
}
|
|
2557
|
-
function resolveAction(manifestHash, installedHash, sourceHash) {
|
|
2632
|
+
function resolveAction(manifestHash, installedHash, sourceHash, owned) {
|
|
2558
2633
|
if (!sourceHash && !installedHash) return null;
|
|
2559
2634
|
if (sourceHash && !installedHash) return "add";
|
|
2560
|
-
if (!sourceHash && installedHash) return "remove";
|
|
2635
|
+
if (!sourceHash && installedHash) return owned ? "remove" : "keep-foreign";
|
|
2561
2636
|
if (!manifestHash) {
|
|
2562
2637
|
if (installedHash === sourceHash) return "skip-unchanged";
|
|
2638
|
+
if (!owned) return "conflict";
|
|
2563
2639
|
return "update";
|
|
2564
2640
|
}
|
|
2565
2641
|
if (manifestHash === installedHash && installedHash === sourceHash) return "skip-unchanged";
|
|
@@ -2575,6 +2651,7 @@ function printUpdateSummary(items, isDryRun) {
|
|
|
2575
2651
|
const conflicts = items.filter((i) => i.action === "conflict");
|
|
2576
2652
|
const customized = items.filter((i) => i.action === "skip-customized");
|
|
2577
2653
|
const removed = items.filter((i) => i.action === "remove");
|
|
2654
|
+
const foreign = items.filter((i) => i.action === "keep-foreign");
|
|
2578
2655
|
if (updates.length > 0) {
|
|
2579
2656
|
console.log(`
|
|
2580
2657
|
${prefix}${chalk7.cyan("Will update:")}`);
|
|
@@ -2591,14 +2668,23 @@ ${prefix}${chalk7.cyan("Will add:")}`);
|
|
|
2591
2668
|
}
|
|
2592
2669
|
if (removed.length > 0) {
|
|
2593
2670
|
console.log(`
|
|
2594
|
-
${prefix}${chalk7.cyan("Will remove
|
|
2671
|
+
${prefix}${chalk7.cyan("Will remove")} ${chalk7.dim(`(moved to .claude/${TRASH_DIR}/)`)}:`);
|
|
2595
2672
|
for (const item of removed) {
|
|
2596
2673
|
console.log(` ${chalk7.red("-")} ${item.type}: ${item.name}`);
|
|
2597
2674
|
}
|
|
2598
2675
|
}
|
|
2599
|
-
if (
|
|
2676
|
+
if (foreign.length > 0) {
|
|
2600
2677
|
console.log(`
|
|
2601
|
-
${
|
|
2678
|
+
${chalk7.cyan("Left alone")} ${chalk7.dim("(not installed by Arcane)")}:`);
|
|
2679
|
+
for (const item of foreign) {
|
|
2680
|
+
console.log(` ${chalk7.dim("=")} ${item.type}: ${item.name}`);
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
if (conflicts.length > 0) {
|
|
2684
|
+
console.log(
|
|
2685
|
+
`
|
|
2686
|
+
${prefix}${chalk7.yellow("Conflicts (will update)")} ${chalk7.dim(`(old copy to .claude/${TRASH_DIR}/)`)}:`
|
|
2687
|
+
);
|
|
2602
2688
|
for (const item of conflicts) {
|
|
2603
2689
|
console.log(` ${chalk7.yellow("!")} ${item.type}: ${item.name}`);
|
|
2604
2690
|
}
|
|
@@ -2613,6 +2699,7 @@ ${prefix}${chalk7.dim("Skipped (locally customized):")}`);
|
|
|
2613
2699
|
}
|
|
2614
2700
|
function applyUpdates(items, root, claudeDir, merged, force) {
|
|
2615
2701
|
for (const item of items) {
|
|
2702
|
+
if (item.action === "keep-foreign") continue;
|
|
2616
2703
|
if (item.action === "skip-unchanged" || item.action === "skip-customized") {
|
|
2617
2704
|
if (item.action === "skip-customized" && force) {
|
|
2618
2705
|
applyItem(item, root, claudeDir);
|
|
@@ -2620,7 +2707,7 @@ function applyUpdates(items, root, claudeDir, merged, force) {
|
|
|
2620
2707
|
continue;
|
|
2621
2708
|
}
|
|
2622
2709
|
if (item.action === "conflict") {
|
|
2623
|
-
|
|
2710
|
+
quarantineItem(item, claudeDir);
|
|
2624
2711
|
applyItem(item, root, claudeDir);
|
|
2625
2712
|
continue;
|
|
2626
2713
|
}
|
|
@@ -2629,7 +2716,7 @@ function applyUpdates(items, root, claudeDir, merged, force) {
|
|
|
2629
2716
|
continue;
|
|
2630
2717
|
}
|
|
2631
2718
|
if (item.action === "remove") {
|
|
2632
|
-
|
|
2719
|
+
quarantineItem(item, claudeDir);
|
|
2633
2720
|
}
|
|
2634
2721
|
}
|
|
2635
2722
|
}
|
|
@@ -2650,6 +2737,29 @@ function syncUnhashedFiles(root, claudeDir, merged, dryRun) {
|
|
|
2650
2737
|
label: `output-styles/${style}.md`
|
|
2651
2738
|
});
|
|
2652
2739
|
}
|
|
2740
|
+
for (const raw of merged.agents) {
|
|
2741
|
+
const { division, agent } = parseAgentEntry(raw);
|
|
2742
|
+
if (!agent) continue;
|
|
2743
|
+
targets.push({
|
|
2744
|
+
src: path17.join(root, "agents", division, `${agent}.md`),
|
|
2745
|
+
dst: path17.join(claudeDir, "agents", division, `${agent}.md`),
|
|
2746
|
+
label: `agents/${division}/${agent}.md`
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
if (merged.rules.gamedev.length > 0) {
|
|
2750
|
+
const templatesSrc = path17.join(root, "templates", "gamedev");
|
|
2751
|
+
if (fs14.existsSync(templatesSrc)) {
|
|
2752
|
+
for (const rel of fs14.readdirSync(templatesSrc, { recursive: true })) {
|
|
2753
|
+
const src = path17.join(templatesSrc, rel);
|
|
2754
|
+
if (!fs14.statSync(src).isFile()) continue;
|
|
2755
|
+
targets.push({
|
|
2756
|
+
src,
|
|
2757
|
+
dst: path17.join(claudeDir, "skills", "_templates", rel),
|
|
2758
|
+
label: `skills/_templates/${String(rel).split(path17.sep).join("/")}`
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2653
2763
|
const changed = [];
|
|
2654
2764
|
for (const t of targets) {
|
|
2655
2765
|
if (!fs14.existsSync(t.src)) continue;
|
|
@@ -2702,20 +2812,15 @@ function applyItem(item, root, claudeDir) {
|
|
|
2702
2812
|
fs14.copyFileSync(src, dst);
|
|
2703
2813
|
}
|
|
2704
2814
|
}
|
|
2705
|
-
function
|
|
2815
|
+
function quarantineItem(item, claudeDir) {
|
|
2706
2816
|
const installed = getInstalledPath(item, claudeDir);
|
|
2707
2817
|
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 });
|
|
2818
|
+
const quarantine = path17.join(claudeDir, TRASH_DIR, `${item.type}s`, item.name);
|
|
2819
|
+
ensureDir(path17.dirname(quarantine));
|
|
2820
|
+
if (fs14.existsSync(quarantine)) {
|
|
2821
|
+
fs14.rmSync(quarantine, { recursive: true, force: true });
|
|
2718
2822
|
}
|
|
2823
|
+
fs14.renameSync(installed, quarantine);
|
|
2719
2824
|
}
|
|
2720
2825
|
function getSourcePath(item, root) {
|
|
2721
2826
|
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
|