claude-code-arcane 2.9.2 → 2.9.4
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 +15 -0
- package/dist/cli.js +129 -74
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## [2.9.4](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.9.3...v2.9.4) (2026-09-07)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **add:** keep content_hashes instead of rebuilding the manifest ([1ecce98](https://github.com/SebastianLuser/claude-code-arcane/commit/1ecce98ebcb8a480dd4b748f41606badec9fb0fd))
|
|
7
|
+
* **cache:** resolve the arcane home in one place, honouring ARCANE_HOME ([4a7a24d](https://github.com/SebastianLuser/claude-code-arcane/commit/4a7a24d6a28df1abfe94be75f590c79d8428dca5))
|
|
8
|
+
|
|
9
|
+
## [2.9.3](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.9.2...v2.9.3) (2026-09-07)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* **update:** resolve the content version once per run, never fabricate it ([ef78ecf](https://github.com/SebastianLuser/claude-code-arcane/commit/ef78ecf6c5ab17f1a51ea6ae27ef9fa161594798))
|
|
15
|
+
|
|
1
16
|
## [2.9.2](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.9.1...v2.9.2) (2026-09-07)
|
|
2
17
|
|
|
3
18
|
|
package/dist/cli.js
CHANGED
|
@@ -391,6 +391,10 @@ import path8 from "path";
|
|
|
391
391
|
import { fileURLToPath } from "url";
|
|
392
392
|
import path3 from "path";
|
|
393
393
|
import fs3 from "fs";
|
|
394
|
+
import os from "os";
|
|
395
|
+
function arcaneHome() {
|
|
396
|
+
return process.env.ARCANE_HOME ?? path3.join(os.homedir(), ".arcane");
|
|
397
|
+
}
|
|
394
398
|
function getPackageRoot() {
|
|
395
399
|
const thisFile = fileURLToPath(import.meta.url);
|
|
396
400
|
return path3.resolve(path3.dirname(thisFile), "..");
|
|
@@ -1310,15 +1314,18 @@ import os2 from "os";
|
|
|
1310
1314
|
// src/cache.ts
|
|
1311
1315
|
import fs8 from "fs";
|
|
1312
1316
|
import path9 from "path";
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1317
|
+
function cacheDir() {
|
|
1318
|
+
return path9.join(arcaneHome(), "cache");
|
|
1319
|
+
}
|
|
1316
1320
|
function getCachePath(version) {
|
|
1317
|
-
return path9.join(
|
|
1321
|
+
return path9.join(cacheDir(), version);
|
|
1322
|
+
}
|
|
1323
|
+
function isUsableContentRoot(dir) {
|
|
1324
|
+
return fs8.existsSync(path9.join(dir, "profiles")) && fs8.existsSync(path9.join(dir, "skills"));
|
|
1318
1325
|
}
|
|
1319
1326
|
function isCached(version) {
|
|
1320
1327
|
const versionDir = getCachePath(version);
|
|
1321
|
-
return fs8.existsSync(versionDir) && fs8.existsSync(path9.join(versionDir, ".cache-meta.json"));
|
|
1328
|
+
return fs8.existsSync(versionDir) && fs8.existsSync(path9.join(versionDir, ".cache-meta.json")) && isUsableContentRoot(versionDir);
|
|
1322
1329
|
}
|
|
1323
1330
|
function getCachedContentRoot(version) {
|
|
1324
1331
|
if (!isCached(version)) return null;
|
|
@@ -1347,9 +1354,9 @@ function storeInCache(version, contentDir) {
|
|
|
1347
1354
|
return versionDir;
|
|
1348
1355
|
}
|
|
1349
1356
|
function pruneCache(keepCount = 3) {
|
|
1350
|
-
if (!fs8.existsSync(
|
|
1351
|
-
const entries = fs8.readdirSync(
|
|
1352
|
-
const metaPath = path9.join(
|
|
1357
|
+
if (!fs8.existsSync(cacheDir())) return;
|
|
1358
|
+
const entries = fs8.readdirSync(cacheDir(), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => {
|
|
1359
|
+
const metaPath = path9.join(cacheDir(), e.name, ".cache-meta.json");
|
|
1353
1360
|
let cachedAt = 0;
|
|
1354
1361
|
if (fs8.existsSync(metaPath)) {
|
|
1355
1362
|
try {
|
|
@@ -1362,9 +1369,24 @@ function pruneCache(keepCount = 3) {
|
|
|
1362
1369
|
return { name: e.name, cachedAt };
|
|
1363
1370
|
}).sort((a, b) => b.cachedAt - a.cachedAt);
|
|
1364
1371
|
for (const entry of entries.slice(keepCount)) {
|
|
1365
|
-
fs8.rmSync(path9.join(
|
|
1372
|
+
fs8.rmSync(path9.join(cacheDir(), entry.name), { recursive: true, force: true });
|
|
1366
1373
|
}
|
|
1367
1374
|
}
|
|
1375
|
+
function listCachedVersions() {
|
|
1376
|
+
if (!fs8.existsSync(cacheDir())) return [];
|
|
1377
|
+
return fs8.readdirSync(cacheDir(), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => {
|
|
1378
|
+
const metaPath = path9.join(cacheDir(), e.name, ".cache-meta.json");
|
|
1379
|
+
let cachedAt = "unknown";
|
|
1380
|
+
if (fs8.existsSync(metaPath)) {
|
|
1381
|
+
try {
|
|
1382
|
+
const meta = JSON.parse(fs8.readFileSync(metaPath, "utf-8"));
|
|
1383
|
+
cachedAt = meta.cached_at;
|
|
1384
|
+
} catch {
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return { version: e.name, cachedAt };
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1368
1390
|
function copyDirRecursive(src, dest) {
|
|
1369
1391
|
fs8.mkdirSync(dest, { recursive: true });
|
|
1370
1392
|
for (const entry of fs8.readdirSync(src, { withFileTypes: true })) {
|
|
@@ -1439,22 +1461,18 @@ var GitHubContentSource = class {
|
|
|
1439
1461
|
fs9.rmSync(tmpDir, { recursive: true, force: true });
|
|
1440
1462
|
}
|
|
1441
1463
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
return false;
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
async getVersion() {
|
|
1464
|
+
/**
|
|
1465
|
+
* The head commit of the branch, or null when GitHub cannot answer.
|
|
1466
|
+
*
|
|
1467
|
+
* One request, memoized, shared by isAvailable() and getVersion(). It used to be
|
|
1468
|
+
* two - a HEAD to answer "is it reachable?" and a GET to read the sha - against an
|
|
1469
|
+
* endpoint that allows 60 requests an hour unauthenticated. That doubled the cost of
|
|
1470
|
+
* every resolution and, worse, opened a window between the two calls: the HEAD spent
|
|
1471
|
+
* the last request of the hour, the GET came back 403, and the caller had already
|
|
1472
|
+
* committed to the github source. That window is where the fabricated versions came
|
|
1473
|
+
* from.
|
|
1474
|
+
*/
|
|
1475
|
+
async resolveVersion() {
|
|
1458
1476
|
if (this.resolvedVersion) return this.resolvedVersion;
|
|
1459
1477
|
try {
|
|
1460
1478
|
const response = await fetch(
|
|
@@ -1467,17 +1485,36 @@ var GitHubContentSource = class {
|
|
|
1467
1485
|
signal: AbortSignal.timeout(1e4)
|
|
1468
1486
|
}
|
|
1469
1487
|
);
|
|
1470
|
-
if (!response.ok)
|
|
1471
|
-
throw new Error(`GitHub API returned ${response.status}`);
|
|
1472
|
-
}
|
|
1488
|
+
if (!response.ok) return null;
|
|
1473
1489
|
const data = await response.json();
|
|
1474
1490
|
this.resolvedVersion = data.sha.substring(0, 12);
|
|
1475
1491
|
return this.resolvedVersion;
|
|
1476
1492
|
} catch {
|
|
1477
|
-
|
|
1478
|
-
return this.resolvedVersion;
|
|
1493
|
+
return null;
|
|
1479
1494
|
}
|
|
1480
1495
|
}
|
|
1496
|
+
async isAvailable() {
|
|
1497
|
+
return await this.resolveVersion() !== null;
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Throws when the version cannot be read, rather than inventing one.
|
|
1501
|
+
*
|
|
1502
|
+
* It used to answer `github-${Date.now()}` on any failure, and that string went on to
|
|
1503
|
+
* be an identity: it stamped `source_version` in the manifest, named a cache directory,
|
|
1504
|
+
* and made the install permanently un-diffable against any real version - it never
|
|
1505
|
+
* matched again, so every later run reported an update and no run ever settled. A
|
|
1506
|
+
* transient 403 became durable state on disk. Callers that cannot proceed without a
|
|
1507
|
+
* version now fall back to the cache, which knows a real one.
|
|
1508
|
+
*/
|
|
1509
|
+
async getVersion() {
|
|
1510
|
+
const version = await this.resolveVersion();
|
|
1511
|
+
if (!version) {
|
|
1512
|
+
throw new Error(
|
|
1513
|
+
"Could not read the content version from GitHub (offline, or the unauthenticated API rate limit is exhausted - it resets hourly)."
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
return version;
|
|
1517
|
+
}
|
|
1481
1518
|
async downloadAndExtract(destDir) {
|
|
1482
1519
|
const tarballUrl = `https://api.github.com/repos/${this.owner}/${this.repo}/tarball/${this.branch}`;
|
|
1483
1520
|
const response = await fetch(tarballUrl, {
|
|
@@ -1548,7 +1585,9 @@ async function resolveContentSource(opts = {}) {
|
|
|
1548
1585
|
return github;
|
|
1549
1586
|
}
|
|
1550
1587
|
if (preference === "github") {
|
|
1551
|
-
throw new Error(
|
|
1588
|
+
throw new Error(
|
|
1589
|
+
"GitHub source requested but not available. Check your internet connection, or wait for the unauthenticated API rate limit to reset (hourly)."
|
|
1590
|
+
);
|
|
1552
1591
|
}
|
|
1553
1592
|
const cached = findLatestCache();
|
|
1554
1593
|
if (cached) {
|
|
@@ -1578,31 +1617,16 @@ async function resolveContentSourceForVersion(version, opts = {}) {
|
|
|
1578
1617
|
return source;
|
|
1579
1618
|
}
|
|
1580
1619
|
function findLatestCache() {
|
|
1581
|
-
const
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
if (fs9.existsSync(metaPath)) {
|
|
1587
|
-
try {
|
|
1588
|
-
const meta = JSON.parse(fs9.readFileSync(metaPath, "utf-8"));
|
|
1589
|
-
cachedAt = new Date(meta.cached_at).getTime();
|
|
1590
|
-
} catch {
|
|
1591
|
-
cachedAt = 0;
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
return { name: e.name, cachedAt };
|
|
1595
|
-
}).sort((a, b) => b.cachedAt - a.cachedAt);
|
|
1596
|
-
return entries.length > 0 ? entries[0].name : null;
|
|
1620
|
+
const usable = listCachedVersions().filter((entry) => isCached(entry.version)).map((entry) => ({
|
|
1621
|
+
version: entry.version,
|
|
1622
|
+
cachedAt: new Date(entry.cachedAt).getTime() || 0
|
|
1623
|
+
})).sort((a, b) => b.cachedAt - a.cachedAt);
|
|
1624
|
+
return usable.length > 0 ? usable[0].version : null;
|
|
1597
1625
|
}
|
|
1598
1626
|
|
|
1599
1627
|
// src/registry.ts
|
|
1600
1628
|
import path11 from "path";
|
|
1601
|
-
import os3 from "os";
|
|
1602
1629
|
var REGISTRY_VERSION = 1;
|
|
1603
|
-
function arcaneHome() {
|
|
1604
|
-
return process.env.ARCANE_HOME ?? path11.join(os3.homedir(), ".arcane");
|
|
1605
|
-
}
|
|
1606
1630
|
function registryPath() {
|
|
1607
1631
|
return path11.join(arcaneHome(), "installations.json");
|
|
1608
1632
|
}
|
|
@@ -1881,16 +1905,11 @@ async function addCommand(items = []) {
|
|
|
1881
1905
|
manifest.installed_skills.push(...added);
|
|
1882
1906
|
manifest.total_skills = manifest.installed_skills.length;
|
|
1883
1907
|
manifest.total_rules = manifest.installed_rules.length;
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
hooks: [],
|
|
1890
|
-
output_styles: [],
|
|
1891
|
-
permissions: { allow: [], deny: [] }
|
|
1892
|
-
};
|
|
1893
|
-
writeManifest(target, merged, manifest.profile_command, root);
|
|
1908
|
+
persistAdditions(target, claudeDir, manifest, {
|
|
1909
|
+
skills: added,
|
|
1910
|
+
rules: addedRules,
|
|
1911
|
+
agents: addedAgents
|
|
1912
|
+
});
|
|
1894
1913
|
const totalAdded = added.length + addedRules.length + addedAgents.length + (statuslineAdded ? 1 : 0);
|
|
1895
1914
|
console.log(
|
|
1896
1915
|
chalk3.bold(
|
|
@@ -1915,6 +1934,35 @@ Added ${totalAdded} items:`
|
|
|
1915
1934
|
);
|
|
1916
1935
|
}
|
|
1917
1936
|
}
|
|
1937
|
+
function persistAdditions(target, claudeDir, manifest, addedItems) {
|
|
1938
|
+
const updates = {
|
|
1939
|
+
profiles: manifest.profiles,
|
|
1940
|
+
profile_command: manifest.profile_command,
|
|
1941
|
+
installed_skills: manifest.installed_skills,
|
|
1942
|
+
installed_rules: manifest.installed_rules,
|
|
1943
|
+
installed_agents: manifest.installed_agents,
|
|
1944
|
+
total_skills: manifest.total_skills,
|
|
1945
|
+
total_rules: manifest.total_rules,
|
|
1946
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z")
|
|
1947
|
+
};
|
|
1948
|
+
const hashes = manifest.content_hashes;
|
|
1949
|
+
if (hashes) {
|
|
1950
|
+
const fresh = computeContentHashes(claudeDir);
|
|
1951
|
+
for (const skill of addedItems.skills) {
|
|
1952
|
+
if (fresh.skills[skill]) hashes.skills[skill] = fresh.skills[skill];
|
|
1953
|
+
}
|
|
1954
|
+
for (const rule of addedItems.rules) {
|
|
1955
|
+
const key = rule.endsWith(".md") ? rule : `${rule}.md`;
|
|
1956
|
+
if (fresh.rules[key]) hashes.rules[key] = fresh.rules[key];
|
|
1957
|
+
}
|
|
1958
|
+
for (const entry of addedItems.agents) {
|
|
1959
|
+
const { division } = parseAgentEntry(entry);
|
|
1960
|
+
if (fresh.agents[division]) hashes.agents[division] = fresh.agents[division];
|
|
1961
|
+
}
|
|
1962
|
+
updates.content_hashes = hashes;
|
|
1963
|
+
}
|
|
1964
|
+
updateManifestFields(target, updates);
|
|
1965
|
+
}
|
|
1918
1966
|
function printCatalog(root, installedProfiles, installedSkills) {
|
|
1919
1967
|
const profiles = listProfiles(path14.join(root, "profiles")).filter(
|
|
1920
1968
|
(pr) => !installedProfiles.includes(pr.name)
|
|
@@ -2261,7 +2309,7 @@ async function statusCommand() {
|
|
|
2261
2309
|
// src/commands/update.ts
|
|
2262
2310
|
import fs14 from "fs";
|
|
2263
2311
|
import path17 from "path";
|
|
2264
|
-
import
|
|
2312
|
+
import os3 from "os";
|
|
2265
2313
|
import chalk7 from "chalk";
|
|
2266
2314
|
|
|
2267
2315
|
// src/self-update.ts
|
|
@@ -2341,7 +2389,7 @@ async function updateCommand(opts) {
|
|
|
2341
2389
|
registerInstallation(cwd);
|
|
2342
2390
|
}
|
|
2343
2391
|
const targets = pruneRegistry().map((e) => e.path);
|
|
2344
|
-
const globalTarget =
|
|
2392
|
+
const globalTarget = os3.homedir();
|
|
2345
2393
|
if (!process.env.VITEST && fs14.existsSync(manifestPath(globalTarget)) && !targets.includes(globalTarget)) {
|
|
2346
2394
|
targets.push(globalTarget);
|
|
2347
2395
|
}
|
|
@@ -2357,6 +2405,10 @@ async function updateCommand(opts) {
|
|
|
2357
2405
|
Updating ${targets.length} installation${targets.length === 1 ? "" : "s"}...`)
|
|
2358
2406
|
);
|
|
2359
2407
|
}
|
|
2408
|
+
const sharedSource = await resolveContentSource({
|
|
2409
|
+
source: opts.source ?? "auto",
|
|
2410
|
+
quiet: opts.quiet
|
|
2411
|
+
});
|
|
2360
2412
|
const results = [];
|
|
2361
2413
|
for (const target of targets) {
|
|
2362
2414
|
if (!opts.quiet) {
|
|
@@ -2364,7 +2416,7 @@ Updating ${targets.length} installation${targets.length === 1 ? "" : "s"}...`)
|
|
|
2364
2416
|
\u2022 ${target}`));
|
|
2365
2417
|
}
|
|
2366
2418
|
try {
|
|
2367
|
-
results.push(await updateTarget(target, opts));
|
|
2419
|
+
results.push(await updateTarget(target, opts, sharedSource));
|
|
2368
2420
|
} catch (err) {
|
|
2369
2421
|
if (!opts.quiet) {
|
|
2370
2422
|
console.log(chalk7.red(` Update failed: ${err.message}`));
|
|
@@ -2382,7 +2434,7 @@ Updating ${targets.length} installation${targets.length === 1 ? "" : "s"}...`)
|
|
|
2382
2434
|
printGeneralSummary(results, opts.dryRun ?? false);
|
|
2383
2435
|
}
|
|
2384
2436
|
}
|
|
2385
|
-
async function updateTarget(target, opts) {
|
|
2437
|
+
async function updateTarget(target, opts, sharedSource) {
|
|
2386
2438
|
const manifest = readManifest(target);
|
|
2387
2439
|
if (!manifest) {
|
|
2388
2440
|
if (!opts.quiet) {
|
|
@@ -2390,7 +2442,7 @@ async function updateTarget(target, opts) {
|
|
|
2390
2442
|
}
|
|
2391
2443
|
return { target, status: "no-manifest", updated: 0, skipped: 0, removed: 0 };
|
|
2392
2444
|
}
|
|
2393
|
-
const contentSource = await resolveContentSource({
|
|
2445
|
+
const contentSource = sharedSource ?? await resolveContentSource({
|
|
2394
2446
|
source: opts.source ?? "auto",
|
|
2395
2447
|
quiet: opts.quiet
|
|
2396
2448
|
});
|
|
@@ -3001,9 +3053,9 @@ async function worktreeCommand(branch, opts) {
|
|
|
3001
3053
|
// src/commands/global.ts
|
|
3002
3054
|
import fs16 from "fs";
|
|
3003
3055
|
import path19 from "path";
|
|
3004
|
-
import
|
|
3056
|
+
import os4 from "os";
|
|
3005
3057
|
import chalk10 from "chalk";
|
|
3006
|
-
var CLAUDE_HOME = path19.join(
|
|
3058
|
+
var CLAUDE_HOME = path19.join(os4.homedir(), ".claude");
|
|
3007
3059
|
var SCRIPTS_DIR = path19.join(CLAUDE_HOME, "scripts", "worktree-isolation");
|
|
3008
3060
|
var SETTINGS_PATH = path19.join(CLAUDE_HOME, "settings.json");
|
|
3009
3061
|
async function globalCommand(opts) {
|
|
@@ -3170,9 +3222,10 @@ function removeHookFromSettings() {
|
|
|
3170
3222
|
// src/update-check.ts
|
|
3171
3223
|
import fs17 from "fs";
|
|
3172
3224
|
import path20 from "path";
|
|
3173
|
-
import os6 from "os";
|
|
3174
3225
|
import chalk11 from "chalk";
|
|
3175
|
-
|
|
3226
|
+
function checkFile() {
|
|
3227
|
+
return path20.join(arcaneHome(), "last-check.json");
|
|
3228
|
+
}
|
|
3176
3229
|
var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
|
|
3177
3230
|
var GITHUB_OWNER2 = "SebastianLuser";
|
|
3178
3231
|
var GITHUB_REPO2 = "Claude-Code-Arcane";
|
|
@@ -3297,19 +3350,21 @@ async function getLatestNpmVersion() {
|
|
|
3297
3350
|
}
|
|
3298
3351
|
function readCachedCheck() {
|
|
3299
3352
|
try {
|
|
3300
|
-
|
|
3301
|
-
|
|
3353
|
+
const file = checkFile();
|
|
3354
|
+
if (!fs17.existsSync(file)) return null;
|
|
3355
|
+
return JSON.parse(fs17.readFileSync(file, "utf-8"));
|
|
3302
3356
|
} catch {
|
|
3303
3357
|
return null;
|
|
3304
3358
|
}
|
|
3305
3359
|
}
|
|
3306
3360
|
function writeCachedCheck(result) {
|
|
3307
3361
|
try {
|
|
3308
|
-
const
|
|
3362
|
+
const file = checkFile();
|
|
3363
|
+
const dir = path20.dirname(file);
|
|
3309
3364
|
if (!fs17.existsSync(dir)) {
|
|
3310
3365
|
fs17.mkdirSync(dir, { recursive: true });
|
|
3311
3366
|
}
|
|
3312
|
-
fs17.writeFileSync(
|
|
3367
|
+
fs17.writeFileSync(file, JSON.stringify(result, null, 2) + "\n");
|
|
3313
3368
|
} catch {
|
|
3314
3369
|
}
|
|
3315
3370
|
}
|