@haus-tech/haus-workflow 1.5.8 → 1.6.0
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 +6 -0
- package/dist/cli.js +1043 -350
- package/library/catalog/manifest.json +45 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -29,12 +29,15 @@ async function runCommand(command, args = [], options = {}) {
|
|
|
29
29
|
// non-zero exits are returned, not thrown
|
|
30
30
|
...options
|
|
31
31
|
});
|
|
32
|
+
const killed = result.timedOut === true || result.isCanceled === true || Boolean(result.signal);
|
|
32
33
|
return {
|
|
33
34
|
command,
|
|
34
35
|
args,
|
|
35
36
|
stdout: String(result.stdout ?? ""),
|
|
36
|
-
stderr: String(
|
|
37
|
-
|
|
37
|
+
stderr: String(
|
|
38
|
+
result.stderr ?? (result.timedOut === true ? `timed out after ${options.timeout}ms` : "")
|
|
39
|
+
),
|
|
40
|
+
exitCode: result.exitCode ?? (killed || result.failed === true ? 1 : 0)
|
|
38
41
|
};
|
|
39
42
|
} catch (error2) {
|
|
40
43
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
@@ -44,9 +47,9 @@ async function runCommand(command, args = [], options = {}) {
|
|
|
44
47
|
async function runGit(args, options = {}) {
|
|
45
48
|
return runCommand("git", args, options);
|
|
46
49
|
}
|
|
47
|
-
async function commandExists(command) {
|
|
50
|
+
async function commandExists(command, timeoutMs = 1e4) {
|
|
48
51
|
try {
|
|
49
|
-
const result = await runCommand(command, ["--version"]);
|
|
52
|
+
const result = await runCommand(command, ["--version"], { timeout: timeoutMs });
|
|
50
53
|
return result.exitCode === 0;
|
|
51
54
|
} catch {
|
|
52
55
|
return false;
|
|
@@ -60,11 +63,11 @@ var init_exec = __esm({
|
|
|
60
63
|
|
|
61
64
|
// src/cli.ts
|
|
62
65
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
63
|
-
import
|
|
66
|
+
import path73 from "node:path";
|
|
64
67
|
import { Command } from "commander";
|
|
65
68
|
|
|
66
69
|
// src/commands/apply.ts
|
|
67
|
-
import
|
|
70
|
+
import path25 from "node:path";
|
|
68
71
|
import checkbox from "@inquirer/checkbox";
|
|
69
72
|
|
|
70
73
|
// src/catalog/load-catalog.ts
|
|
@@ -216,6 +219,11 @@ function hausPath(root, ...parts) {
|
|
|
216
219
|
function claudePath(root, ...parts) {
|
|
217
220
|
return path2.join(root, ".claude", ...parts);
|
|
218
221
|
}
|
|
222
|
+
function claudeConfigDir() {
|
|
223
|
+
const override = process.env.CLAUDE_CONFIG_DIR;
|
|
224
|
+
if (override && override.trim().length > 0) return override;
|
|
225
|
+
return path2.join(os.homedir(), ".claude");
|
|
226
|
+
}
|
|
219
227
|
function displayPath(root, targetPath) {
|
|
220
228
|
const rel = path2.relative(root, targetPath).replace(/\\/g, "/");
|
|
221
229
|
if (rel && !rel.startsWith("../") && rel !== "..") {
|
|
@@ -894,7 +902,15 @@ function parseManifest(json) {
|
|
|
894
902
|
if (formerIdFailures.length > 0) {
|
|
895
903
|
return { ok: false, error: formerIdFailures[0] };
|
|
896
904
|
}
|
|
897
|
-
return {
|
|
905
|
+
return {
|
|
906
|
+
ok: true,
|
|
907
|
+
manifest: {
|
|
908
|
+
version: root.version,
|
|
909
|
+
items,
|
|
910
|
+
...root.globalPlugins !== void 0 ? { globalPlugins: root.globalPlugins } : {}
|
|
911
|
+
},
|
|
912
|
+
warnings
|
|
913
|
+
};
|
|
898
914
|
}
|
|
899
915
|
|
|
900
916
|
// src/catalog/remote-catalog/manifest.ts
|
|
@@ -1484,13 +1500,16 @@ async function syncRemoteCatalog() {
|
|
|
1484
1500
|
warn("Remote catalog fetch failed \u2014 using bundled catalog");
|
|
1485
1501
|
return { newItems: [], refreshed: [], unchanged: 0, failed: [] };
|
|
1486
1502
|
}
|
|
1487
|
-
const { version, items } = manifest;
|
|
1503
|
+
const { version, items, globalPlugins } = manifest;
|
|
1488
1504
|
const cacheDir = getCacheDir();
|
|
1489
1505
|
try {
|
|
1490
1506
|
await fs6.ensureDir(cacheDir);
|
|
1491
1507
|
await fs6.writeFile(
|
|
1492
1508
|
path8.join(cacheDir, "manifest.json"),
|
|
1493
|
-
|
|
1509
|
+
// globalPlugins must round-trip into the cache: the cache is what `haus update`
|
|
1510
|
+
// and `haus refresh` read pins from, so dropping it here would make every
|
|
1511
|
+
// catalog pin bump invisible and pin resolution fall back to the bundled default.
|
|
1512
|
+
`${JSON.stringify({ version, items, ...globalPlugins !== void 0 ? { globalPlugins } : {} }, null, 2)}
|
|
1494
1513
|
`,
|
|
1495
1514
|
"utf8"
|
|
1496
1515
|
);
|
|
@@ -1544,14 +1563,20 @@ async function loadCatalogContext(root) {
|
|
|
1544
1563
|
return {
|
|
1545
1564
|
items: data2?.items ?? [],
|
|
1546
1565
|
contentRoot: path9.dirname(envPath),
|
|
1547
|
-
source: "fixture"
|
|
1566
|
+
source: "fixture",
|
|
1567
|
+
globalPlugins: data2?.globalPlugins
|
|
1548
1568
|
};
|
|
1549
1569
|
}
|
|
1550
1570
|
const cacheDir = getCacheDir();
|
|
1551
1571
|
const cacheManifestPath = path9.join(cacheDir, "manifest.json");
|
|
1552
1572
|
const cacheData = await readJson(cacheManifestPath);
|
|
1553
1573
|
if (cacheData?.items?.length) {
|
|
1554
|
-
return {
|
|
1574
|
+
return {
|
|
1575
|
+
items: cacheData.items,
|
|
1576
|
+
contentRoot: cacheDir,
|
|
1577
|
+
source: "cache",
|
|
1578
|
+
globalPlugins: cacheData.globalPlugins
|
|
1579
|
+
};
|
|
1555
1580
|
}
|
|
1556
1581
|
const localManifest = path9.join(root, "library/catalog/manifest.json");
|
|
1557
1582
|
const localData = await readJson(localManifest);
|
|
@@ -1559,7 +1584,8 @@ async function loadCatalogContext(root) {
|
|
|
1559
1584
|
return {
|
|
1560
1585
|
items: localData.items,
|
|
1561
1586
|
contentRoot: path9.dirname(localManifest),
|
|
1562
|
-
source: "local"
|
|
1587
|
+
source: "local",
|
|
1588
|
+
globalPlugins: localData.globalPlugins
|
|
1563
1589
|
};
|
|
1564
1590
|
}
|
|
1565
1591
|
const packageManifest = path9.join(packageRoot(), "library/catalog/manifest.json");
|
|
@@ -1567,7 +1593,8 @@ async function loadCatalogContext(root) {
|
|
|
1567
1593
|
return {
|
|
1568
1594
|
items: data?.items ?? [],
|
|
1569
1595
|
contentRoot: path9.dirname(packageManifest),
|
|
1570
|
-
source: "bundled"
|
|
1596
|
+
source: "bundled",
|
|
1597
|
+
globalPlugins: data?.globalPlugins
|
|
1571
1598
|
};
|
|
1572
1599
|
}
|
|
1573
1600
|
async function loadCatalog(root) {
|
|
@@ -1857,6 +1884,26 @@ function mergeRecommendationWarnings(context, isLinkedWorktree = false) {
|
|
|
1857
1884
|
return [.../* @__PURE__ */ new Set([...statusLines, ...context.warnings, ...riskLines])];
|
|
1858
1885
|
}
|
|
1859
1886
|
|
|
1887
|
+
// src/recommender/superseded-by-plugin.ts
|
|
1888
|
+
var RULES = [
|
|
1889
|
+
{
|
|
1890
|
+
pinId: "superpowers",
|
|
1891
|
+
originSourceId: "superpowers-pcvelz",
|
|
1892
|
+
idPrefix: "haus.superpowers-"
|
|
1893
|
+
}
|
|
1894
|
+
];
|
|
1895
|
+
function isItemSupersededByPlugin(item, activePinIds) {
|
|
1896
|
+
if (item.originSourceId === void 0) return false;
|
|
1897
|
+
return RULES.some(
|
|
1898
|
+
(rule) => activePinIds.includes(rule.pinId) && rule.originSourceId === item.originSourceId
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
function isSupersededByPlugin(lockId, activePinIds) {
|
|
1902
|
+
return RULES.some(
|
|
1903
|
+
(rule) => (activePinIds === void 0 || activePinIds.includes(rule.pinId)) && lockId.startsWith(rule.idPrefix)
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1860
1907
|
// src/update/hash-installed.ts
|
|
1861
1908
|
import path11 from "node:path";
|
|
1862
1909
|
import fg2 from "fast-glob";
|
|
@@ -2025,13 +2072,22 @@ async function readManifest() {
|
|
|
2025
2072
|
async function writeManifest(manifest) {
|
|
2026
2073
|
await writeJson(hausManifestPath(), manifest);
|
|
2027
2074
|
}
|
|
2028
|
-
function
|
|
2075
|
+
async function recordManifestPlugins(plugins) {
|
|
2076
|
+
if (plugins.length === 0) return;
|
|
2077
|
+
const manifest = await readManifest();
|
|
2078
|
+
if (!manifest) return;
|
|
2079
|
+
const byId = new Map((manifest.plugins ?? []).map((p) => [p.id, p]));
|
|
2080
|
+
for (const plugin of plugins) byId.set(plugin.id, plugin);
|
|
2081
|
+
await writeManifest({ ...manifest, plugins: [...byId.values()] });
|
|
2082
|
+
}
|
|
2083
|
+
function buildManifest(source, files, hooks, plugins) {
|
|
2029
2084
|
return {
|
|
2030
2085
|
_schema: MANIFEST_SCHEMA,
|
|
2031
2086
|
source,
|
|
2032
2087
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2033
2088
|
files,
|
|
2034
|
-
hooks
|
|
2089
|
+
hooks,
|
|
2090
|
+
...plugins && plugins.length > 0 ? { plugins } : {}
|
|
2035
2091
|
};
|
|
2036
2092
|
}
|
|
2037
2093
|
|
|
@@ -2287,8 +2343,8 @@ function buildDenyRules() {
|
|
|
2287
2343
|
for (const command of DENY_COMMANDS) {
|
|
2288
2344
|
rules.push(`Bash(${command}:*)`);
|
|
2289
2345
|
}
|
|
2290
|
-
for (const
|
|
2291
|
-
const pattern = DENY_DIRS.has(
|
|
2346
|
+
for (const path74 of DENY_PATHS) {
|
|
2347
|
+
const pattern = DENY_DIRS.has(path74) ? `${path74}/**` : path74;
|
|
2292
2348
|
for (const tool of FILE_TOOLS) {
|
|
2293
2349
|
rules.push(`${tool}(${pattern})`);
|
|
2294
2350
|
}
|
|
@@ -2371,6 +2427,7 @@ import path14 from "node:path";
|
|
|
2371
2427
|
import fg3 from "fast-glob";
|
|
2372
2428
|
import fs10 from "fs-extra";
|
|
2373
2429
|
var SUPERPOWERS_ORIGIN_SOURCE_ID = "superpowers-pcvelz";
|
|
2430
|
+
var SUPERPOWERS_SHARED_INSTALL_REL = ".claude/skills/shared";
|
|
2374
2431
|
var PATH_REWRITES = [
|
|
2375
2432
|
["skills/shared/", ".claude/skills/shared/"]
|
|
2376
2433
|
];
|
|
@@ -3246,6 +3303,10 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
3246
3303
|
warn(`Skipping ${item.id}: reviewStatus is deprecated`);
|
|
3247
3304
|
continue;
|
|
3248
3305
|
}
|
|
3306
|
+
if (isItemSupersededByPlugin(manifestItem, opts.activePluginIds ?? [])) {
|
|
3307
|
+
warn(`Skipping ${item.id}: provided by an installed global plugin`);
|
|
3308
|
+
continue;
|
|
3309
|
+
}
|
|
3249
3310
|
const manifestItemTags = (manifestItem.tags ?? []).map((t) => t.toLowerCase());
|
|
3250
3311
|
if (UNSUPPORTED.some((x) => manifestItemTags.includes(x))) {
|
|
3251
3312
|
warn(`Skipping ${item.id}: unsupported stack policy`);
|
|
@@ -3371,7 +3432,13 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
3371
3432
|
`${curatedReviewStatusSkips} curated items skipped: reviewStatus is not approved \u2014 possible catalog field rename upstream`
|
|
3372
3433
|
);
|
|
3373
3434
|
}
|
|
3374
|
-
await cleanupStaleCatalogItems(
|
|
3435
|
+
await cleanupStaleCatalogItems(
|
|
3436
|
+
root,
|
|
3437
|
+
cleanupManifestById,
|
|
3438
|
+
dryRun,
|
|
3439
|
+
opts.quiet,
|
|
3440
|
+
opts.activePluginIds ?? []
|
|
3441
|
+
);
|
|
3375
3442
|
const refuseDestructiveLockChange = recommendationUnusable && prevLock.length > 0 && !opts.force;
|
|
3376
3443
|
if (opts.prune) {
|
|
3377
3444
|
if (refuseDestructiveLockChange) {
|
|
@@ -3515,25 +3582,46 @@ async function resolveExistingTrackedPaths(root, itemId, relPaths, warnFn) {
|
|
|
3515
3582
|
}
|
|
3516
3583
|
return existing;
|
|
3517
3584
|
}
|
|
3518
|
-
async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet) {
|
|
3585
|
+
async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activePluginIds = []) {
|
|
3519
3586
|
const say = quiet ? () => {
|
|
3520
3587
|
} : log;
|
|
3521
3588
|
const prevLock = await readJson(hausPath(root, "haus.lock.json"));
|
|
3522
3589
|
if (!prevLock?.length) return;
|
|
3590
|
+
const supersededEntries = prevLock.filter(
|
|
3591
|
+
(entry) => entry.id && isSupersededByPlugin(entry.id, activePluginIds)
|
|
3592
|
+
);
|
|
3593
|
+
let keepShared = false;
|
|
3594
|
+
for (const entry of supersededEntries) {
|
|
3595
|
+
const relPaths = entry.paths ?? [];
|
|
3596
|
+
if (relPaths.length === 0) continue;
|
|
3597
|
+
const existing = await resolveExistingTrackedPaths(root, entry.id, relPaths, warn);
|
|
3598
|
+
if (existing.length === 0) continue;
|
|
3599
|
+
if (entry.hash === void 0) {
|
|
3600
|
+
keepShared = true;
|
|
3601
|
+
continue;
|
|
3602
|
+
}
|
|
3603
|
+
const currentHash = await hashInstalledPaths(
|
|
3604
|
+
root,
|
|
3605
|
+
existing.map((e) => e.rel)
|
|
3606
|
+
);
|
|
3607
|
+
if (currentHash !== entry.hash) keepShared = true;
|
|
3608
|
+
}
|
|
3523
3609
|
for (const entry of prevLock) {
|
|
3524
3610
|
if (!entry.id) continue;
|
|
3525
3611
|
const manifestItem = manifestById.get(entry.id);
|
|
3526
3612
|
const removedFromManifest = manifestItem === void 0;
|
|
3527
3613
|
const deprecated = manifestItem?.reviewStatus === "deprecated";
|
|
3528
|
-
|
|
3614
|
+
const superseded = isSupersededByPlugin(entry.id, activePluginIds);
|
|
3615
|
+
if (!removedFromManifest && !deprecated && !superseded) continue;
|
|
3529
3616
|
const relPaths = entry.paths ?? [];
|
|
3530
3617
|
if (relPaths.length === 0) continue;
|
|
3531
3618
|
const existing = await resolveExistingTrackedPaths(root, entry.id, relPaths, warn);
|
|
3532
3619
|
if (existing.length === 0) continue;
|
|
3533
|
-
const pruneReason = deprecated ? "deprecated" : "stale";
|
|
3620
|
+
const pruneReason = superseded ? "superseded" : deprecated ? "deprecated" : "stale";
|
|
3621
|
+
const label = superseded ? "Superseded" : deprecated ? "Deprecated" : "Stale";
|
|
3534
3622
|
if (entry.hash === void 0) {
|
|
3535
3623
|
warn(
|
|
3536
|
-
`${
|
|
3624
|
+
`${label} catalog item ${entry.id} has no lock hash \u2014 leaving in place: ${existing.map((e) => e.rel).join(", ")}`
|
|
3537
3625
|
);
|
|
3538
3626
|
continue;
|
|
3539
3627
|
}
|
|
@@ -3543,11 +3631,28 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet) {
|
|
|
3543
3631
|
);
|
|
3544
3632
|
if (currentHash !== entry.hash) {
|
|
3545
3633
|
warn(
|
|
3546
|
-
`${
|
|
3634
|
+
`${label} catalog item ${entry.id} was modified locally \u2014 leaving in place${superseded ? " (the global plugin provides it; delete the copy manually if you no longer want it)" : ""}: ${existing.map((e) => e.rel).join(", ")}`
|
|
3547
3635
|
);
|
|
3548
3636
|
continue;
|
|
3549
3637
|
}
|
|
3550
|
-
|
|
3638
|
+
const removable = superseded ? existing.filter(({ rel }) => {
|
|
3639
|
+
const isShared = rel.split(path20.sep).join("/") === SUPERPOWERS_SHARED_INSTALL_REL;
|
|
3640
|
+
if (isShared && keepShared) {
|
|
3641
|
+
say(
|
|
3642
|
+
`Keeping ${displayPath(root, path20.join(root, rel))} \u2014 a locally-edited superpowers skill still uses it`
|
|
3643
|
+
);
|
|
3644
|
+
return false;
|
|
3645
|
+
}
|
|
3646
|
+
return true;
|
|
3647
|
+
}) : existing;
|
|
3648
|
+
if (superseded && !dryRun && removable.length > 0) {
|
|
3649
|
+
await backupBeforePrune(
|
|
3650
|
+
root,
|
|
3651
|
+
removable.map((e) => e.abs),
|
|
3652
|
+
say
|
|
3653
|
+
);
|
|
3654
|
+
}
|
|
3655
|
+
for (const { abs } of removable) {
|
|
3551
3656
|
if (dryRun) {
|
|
3552
3657
|
say(`[dry-run] would remove ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
3553
3658
|
continue;
|
|
@@ -3786,16 +3891,431 @@ async function untrackMachineLocalArtifacts(root, dryRun, notify) {
|
|
|
3786
3891
|
return untracked;
|
|
3787
3892
|
}
|
|
3788
3893
|
|
|
3894
|
+
// src/install/ensure-global-plugins.ts
|
|
3895
|
+
import path22 from "node:path";
|
|
3896
|
+
init_exec();
|
|
3897
|
+
|
|
3898
|
+
// src/install/global-plugins.ts
|
|
3899
|
+
var DEFAULT_GLOBAL_PLUGINS = [
|
|
3900
|
+
{
|
|
3901
|
+
id: "superpowers",
|
|
3902
|
+
plugin: "superpowers-extended-cc",
|
|
3903
|
+
marketplace: "superpowers-extended-cc-marketplace",
|
|
3904
|
+
url: "https://github.com/pcvelz/superpowers.git",
|
|
3905
|
+
ref: "v6.4.0"
|
|
3906
|
+
}
|
|
3907
|
+
];
|
|
3908
|
+
var MANAGED_SCOPE = "user";
|
|
3909
|
+
function pluginId(pin) {
|
|
3910
|
+
return `${pin.plugin}@${pin.marketplace}`;
|
|
3911
|
+
}
|
|
3912
|
+
function canonicalUrl(url) {
|
|
3913
|
+
return url.trim().toLowerCase().replace(/\.git$/, "").replace(/\/+$/, "");
|
|
3914
|
+
}
|
|
3915
|
+
function isNonEmptyString3(value) {
|
|
3916
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
3917
|
+
}
|
|
3918
|
+
function normalizeMarketplaceSource(marketplace) {
|
|
3919
|
+
if (marketplace.source === "git" && isNonEmptyString3(marketplace.url)) {
|
|
3920
|
+
return { url: canonicalUrl(marketplace.url), ref: marketplace.ref };
|
|
3921
|
+
}
|
|
3922
|
+
if (marketplace.source === "github" && isNonEmptyString3(marketplace.repo)) {
|
|
3923
|
+
return { url: canonicalUrl(`https://github.com/${marketplace.repo}`), ref: marketplace.ref };
|
|
3924
|
+
}
|
|
3925
|
+
return null;
|
|
3926
|
+
}
|
|
3927
|
+
function resolveGlobalPlugins(manifest, deps = {}) {
|
|
3928
|
+
const warn2 = deps.warn ?? warn;
|
|
3929
|
+
const declared = manifest?.globalPlugins;
|
|
3930
|
+
if (declared === void 0) return [...DEFAULT_GLOBAL_PLUGINS];
|
|
3931
|
+
if (!Array.isArray(declared)) {
|
|
3932
|
+
warn2("Catalog `globalPlugins` is not an array \u2014 falling back to the bundled pin.");
|
|
3933
|
+
return [...DEFAULT_GLOBAL_PLUGINS];
|
|
3934
|
+
}
|
|
3935
|
+
const pins = [];
|
|
3936
|
+
for (const entry of declared) {
|
|
3937
|
+
if (!isValidPin(entry)) {
|
|
3938
|
+
const label = entry !== null && typeof entry === "object" && isNonEmptyString3(entry.id) ? entry.id : JSON.stringify(entry);
|
|
3939
|
+
warn2(`Skipping malformed catalog globalPlugins entry: ${label}`);
|
|
3940
|
+
continue;
|
|
3941
|
+
}
|
|
3942
|
+
if (!isImmutableRef(entry.ref)) {
|
|
3943
|
+
warn2(
|
|
3944
|
+
`Refusing catalog pin ${entry.id}: ref "${entry.ref}" is not a release tag \u2014 haus only installs immutable, reviewed plugin content.`
|
|
3945
|
+
);
|
|
3946
|
+
continue;
|
|
3947
|
+
}
|
|
3948
|
+
pins.push({
|
|
3949
|
+
id: entry.id,
|
|
3950
|
+
plugin: entry.plugin,
|
|
3951
|
+
marketplace: entry.marketplace,
|
|
3952
|
+
url: entry.url,
|
|
3953
|
+
ref: entry.ref
|
|
3954
|
+
});
|
|
3955
|
+
}
|
|
3956
|
+
return pins;
|
|
3957
|
+
}
|
|
3958
|
+
function isValidPin(entry) {
|
|
3959
|
+
if (entry === null || typeof entry !== "object") return false;
|
|
3960
|
+
const candidate = entry;
|
|
3961
|
+
return isNonEmptyString3(candidate.id) && isNonEmptyString3(candidate.plugin) && isNonEmptyString3(candidate.marketplace) && isNonEmptyString3(candidate.url) && isNonEmptyString3(candidate.ref);
|
|
3962
|
+
}
|
|
3963
|
+
var RELEASE_TAG = /^v?\d+(\.\d+)*([-+][0-9A-Za-z.-]+)?$/;
|
|
3964
|
+
function isImmutableRef(ref) {
|
|
3965
|
+
return RELEASE_TAG.test(ref.trim());
|
|
3966
|
+
}
|
|
3967
|
+
function findManagedInstall(state, pin) {
|
|
3968
|
+
const id = pluginId(pin);
|
|
3969
|
+
return state.installed.find((p) => p.id === id && (p.scope ?? MANAGED_SCOPE) === MANAGED_SCOPE);
|
|
3970
|
+
}
|
|
3971
|
+
function isPluginActive(state, pin) {
|
|
3972
|
+
if (!state.available) return false;
|
|
3973
|
+
const id = pluginId(pin);
|
|
3974
|
+
return state.installed.some((p) => p.id === id && p.enabled !== false);
|
|
3975
|
+
}
|
|
3976
|
+
function planPluginActions(desired, state) {
|
|
3977
|
+
return desired.map((pin) => planOne(pin, state));
|
|
3978
|
+
}
|
|
3979
|
+
function planOne(pin, state) {
|
|
3980
|
+
if (!state.available) {
|
|
3981
|
+
return {
|
|
3982
|
+
pin,
|
|
3983
|
+
kind: "unavailable",
|
|
3984
|
+
steps: [],
|
|
3985
|
+
reason: "the `claude` CLI is not on PATH \u2014 plugin state is unknown"
|
|
3986
|
+
};
|
|
3987
|
+
}
|
|
3988
|
+
const marketplace = state.marketplaces.find((m) => m.name === pin.marketplace);
|
|
3989
|
+
const normalized = marketplace ? normalizeMarketplaceSource(marketplace) : null;
|
|
3990
|
+
const marketplaceMatches = normalized !== null && normalized.url === canonicalUrl(pin.url) && normalized.ref === pin.ref;
|
|
3991
|
+
const installed = findManagedInstall(state, pin);
|
|
3992
|
+
const disabled = installed?.enabled === false;
|
|
3993
|
+
const enableStep = disabled ? ["plugin-enable"] : [];
|
|
3994
|
+
if (!installed) {
|
|
3995
|
+
const steps = marketplaceMatches ? ["plugin-install"] : ["marketplace-add", "plugin-install"];
|
|
3996
|
+
return {
|
|
3997
|
+
pin,
|
|
3998
|
+
kind: "install",
|
|
3999
|
+
steps,
|
|
4000
|
+
reason: marketplace ? `plugin ${pluginId(pin)} is not installed at ${MANAGED_SCOPE} scope` : `marketplace ${pin.marketplace} is not configured`
|
|
4001
|
+
};
|
|
4002
|
+
}
|
|
4003
|
+
if (!marketplaceMatches) {
|
|
4004
|
+
return {
|
|
4005
|
+
pin,
|
|
4006
|
+
kind: "repin",
|
|
4007
|
+
steps: [
|
|
4008
|
+
"marketplace-add",
|
|
4009
|
+
"marketplace-update",
|
|
4010
|
+
"plugin-uninstall",
|
|
4011
|
+
"plugin-install",
|
|
4012
|
+
...enableStep
|
|
4013
|
+
],
|
|
4014
|
+
reason: `marketplace ${pin.marketplace} does not track the pinned ref ${pin.ref}`,
|
|
4015
|
+
...normalized?.ref !== void 0 ? { overriddenRef: normalized.ref } : {}
|
|
4016
|
+
};
|
|
4017
|
+
}
|
|
4018
|
+
const versionsDiffer = isNonEmptyString3(marketplace?.pluginVersion) && isNonEmptyString3(installed.version) && marketplace.pluginVersion !== installed.version;
|
|
4019
|
+
if (versionsDiffer) {
|
|
4020
|
+
return {
|
|
4021
|
+
pin,
|
|
4022
|
+
kind: "repin",
|
|
4023
|
+
steps: ["marketplace-update", "plugin-uninstall", "plugin-install", ...enableStep],
|
|
4024
|
+
reason: `installed ${installed.version} does not match marketplace ${marketplace?.pluginVersion} at ref ${pin.ref}`
|
|
4025
|
+
};
|
|
4026
|
+
}
|
|
4027
|
+
if (disabled) {
|
|
4028
|
+
return {
|
|
4029
|
+
pin,
|
|
4030
|
+
kind: "re-enable",
|
|
4031
|
+
steps: ["plugin-enable"],
|
|
4032
|
+
reason: `plugin ${pluginId(pin)} is disabled \u2014 haus workflows depend on it`
|
|
4033
|
+
};
|
|
4034
|
+
}
|
|
4035
|
+
return { pin, kind: "up-to-date", steps: [], reason: `pinned to ${pin.ref}` };
|
|
4036
|
+
}
|
|
4037
|
+
function pluginDriftStatus(actions, opts) {
|
|
4038
|
+
if (actions.length === 0) return false;
|
|
4039
|
+
if (actions.some((a) => a.kind === "unavailable")) return null;
|
|
4040
|
+
return actions.some((a) => {
|
|
4041
|
+
if (a.kind === "install" || a.kind === "re-enable") return true;
|
|
4042
|
+
if (a.kind === "repin") return opts.matchRef;
|
|
4043
|
+
return false;
|
|
4044
|
+
});
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
// src/install/ensure-global-plugins.ts
|
|
4048
|
+
async function loadCatalogPluginPins(root) {
|
|
4049
|
+
const ctx = await loadCatalogContext(root);
|
|
4050
|
+
return resolveGlobalPlugins({ globalPlugins: ctx.globalPlugins });
|
|
4051
|
+
}
|
|
4052
|
+
async function activeGlobalPluginIds(root, deps = {}) {
|
|
4053
|
+
try {
|
|
4054
|
+
const [pins, state] = await Promise.all([loadCatalogPluginPins(root), probePluginEnv(deps)]);
|
|
4055
|
+
if (!state.available) return [];
|
|
4056
|
+
return pins.filter((pin) => isPluginActive(state, pin)).map((pin) => pin.id);
|
|
4057
|
+
} catch {
|
|
4058
|
+
return [];
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
var EXEC_TIMEOUT_MS = 12e4;
|
|
4062
|
+
var EXEC_ENV = { GIT_TERMINAL_PROMPT: "0" };
|
|
4063
|
+
function globalPluginsDisabled() {
|
|
4064
|
+
return process.env.HAUS_NO_GLOBAL_PLUGINS === "1";
|
|
4065
|
+
}
|
|
4066
|
+
var realExec = (args) => runCommand("claude", args, {
|
|
4067
|
+
timeout: EXEC_TIMEOUT_MS,
|
|
4068
|
+
env: EXEC_ENV,
|
|
4069
|
+
extendEnv: true
|
|
4070
|
+
});
|
|
4071
|
+
function parseInstalled(stdout) {
|
|
4072
|
+
try {
|
|
4073
|
+
const parsed = JSON.parse(stdout);
|
|
4074
|
+
const list = Array.isArray(parsed) ? parsed : parsed !== null && typeof parsed === "object" ? parsed.installed : void 0;
|
|
4075
|
+
if (!Array.isArray(list)) return null;
|
|
4076
|
+
return list.flatMap((entry) => {
|
|
4077
|
+
const item = entry;
|
|
4078
|
+
if (typeof item.id !== "string") return [];
|
|
4079
|
+
return [
|
|
4080
|
+
{
|
|
4081
|
+
id: item.id,
|
|
4082
|
+
...typeof item.version === "string" ? { version: item.version } : {},
|
|
4083
|
+
...typeof item.scope === "string" ? { scope: item.scope } : {},
|
|
4084
|
+
...typeof item.enabled === "boolean" ? { enabled: item.enabled } : {}
|
|
4085
|
+
}
|
|
4086
|
+
];
|
|
4087
|
+
});
|
|
4088
|
+
} catch {
|
|
4089
|
+
return null;
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
async function readMarketplaceRefs(configDir) {
|
|
4093
|
+
const settings = await readJson(path22.join(configDir, "settings.json"));
|
|
4094
|
+
const out = {};
|
|
4095
|
+
for (const [name, entry] of Object.entries(settings?.extraKnownMarketplaces ?? {})) {
|
|
4096
|
+
const ref = entry?.source?.ref;
|
|
4097
|
+
out[name] = typeof ref === "string" ? ref : void 0;
|
|
4098
|
+
}
|
|
4099
|
+
return out;
|
|
4100
|
+
}
|
|
4101
|
+
async function readClonedPluginVersion(installLocation) {
|
|
4102
|
+
const manifest = await readJson(
|
|
4103
|
+
path22.join(installLocation, ".claude-plugin", "plugin.json")
|
|
4104
|
+
);
|
|
4105
|
+
return typeof manifest?.version === "string" ? manifest.version : void 0;
|
|
4106
|
+
}
|
|
4107
|
+
async function parseMarketplaces(stdout, configDir) {
|
|
4108
|
+
let rows = [];
|
|
4109
|
+
try {
|
|
4110
|
+
const parsed = JSON.parse(stdout);
|
|
4111
|
+
if (!Array.isArray(parsed)) return null;
|
|
4112
|
+
rows = parsed;
|
|
4113
|
+
} catch {
|
|
4114
|
+
return null;
|
|
4115
|
+
}
|
|
4116
|
+
const refs = await readMarketplaceRefs(configDir);
|
|
4117
|
+
const out = [];
|
|
4118
|
+
for (const row of rows) {
|
|
4119
|
+
if (typeof row.name !== "string") continue;
|
|
4120
|
+
const installLocation = typeof row.installLocation === "string" ? row.installLocation : void 0;
|
|
4121
|
+
out.push({
|
|
4122
|
+
name: row.name,
|
|
4123
|
+
...typeof row.source === "string" ? { source: row.source } : {},
|
|
4124
|
+
...typeof row.url === "string" ? { url: row.url } : {},
|
|
4125
|
+
...typeof row.repo === "string" ? { repo: row.repo } : {},
|
|
4126
|
+
...refs[row.name] !== void 0 ? { ref: refs[row.name] } : {},
|
|
4127
|
+
...installLocation ? { pluginVersion: await readClonedPluginVersion(installLocation) } : {}
|
|
4128
|
+
});
|
|
4129
|
+
}
|
|
4130
|
+
return out;
|
|
4131
|
+
}
|
|
4132
|
+
async function probePluginEnv(deps = {}) {
|
|
4133
|
+
const exec = deps.exec ?? realExec;
|
|
4134
|
+
const isAvailable = deps.isAvailable ?? (() => commandExists("claude"));
|
|
4135
|
+
const warn2 = deps.warn ?? warn;
|
|
4136
|
+
if (globalPluginsDisabled() && deps.exec === void 0) {
|
|
4137
|
+
return { available: false, installed: [], marketplaces: [] };
|
|
4138
|
+
}
|
|
4139
|
+
if (!await isAvailable()) return { available: false, installed: [], marketplaces: [] };
|
|
4140
|
+
const [listed, markets] = await Promise.all([
|
|
4141
|
+
exec(["plugin", "list", "--json"]),
|
|
4142
|
+
exec(["plugin", "marketplace", "list", "--json"])
|
|
4143
|
+
]);
|
|
4144
|
+
if (listed.exitCode !== 0 || markets.exitCode !== 0) {
|
|
4145
|
+
warn2(
|
|
4146
|
+
`Could not read Claude Code plugin state (${listed.exitCode !== 0 ? "plugin list" : "marketplace list"} exited ${listed.exitCode !== 0 ? listed.exitCode : markets.exitCode}) \u2014 leaving global plugins untouched.`
|
|
4147
|
+
);
|
|
4148
|
+
return { available: false, installed: [], marketplaces: [] };
|
|
4149
|
+
}
|
|
4150
|
+
const configDir = deps.configDir ?? claudeConfigDir();
|
|
4151
|
+
const installed = parseInstalled(listed.stdout);
|
|
4152
|
+
const marketplaces = await parseMarketplaces(markets.stdout, configDir);
|
|
4153
|
+
if (installed === null || marketplaces === null) {
|
|
4154
|
+
warn2("Claude Code returned unreadable plugin state \u2014 leaving global plugins untouched.");
|
|
4155
|
+
return { available: false, installed: [], marketplaces: [] };
|
|
4156
|
+
}
|
|
4157
|
+
return { available: true, installed, marketplaces };
|
|
4158
|
+
}
|
|
4159
|
+
function stepArgs(step, pin) {
|
|
4160
|
+
const id = pluginId(pin);
|
|
4161
|
+
switch (step) {
|
|
4162
|
+
case "marketplace-add":
|
|
4163
|
+
return ["plugin", "marketplace", "add", `${pin.url}#${pin.ref}`, "--scope", "user"];
|
|
4164
|
+
case "marketplace-update":
|
|
4165
|
+
return ["plugin", "marketplace", "update", pin.marketplace];
|
|
4166
|
+
case "plugin-uninstall":
|
|
4167
|
+
return ["plugin", "uninstall", id, "--scope", "user"];
|
|
4168
|
+
case "plugin-install":
|
|
4169
|
+
return ["plugin", "install", id, "--scope", "user"];
|
|
4170
|
+
case "plugin-update":
|
|
4171
|
+
return ["plugin", "update", id, "--scope", "user"];
|
|
4172
|
+
case "plugin-enable":
|
|
4173
|
+
return ["plugin", "enable", id, "--scope", "user"];
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
var RESTART_STEPS = /* @__PURE__ */ new Set([
|
|
4177
|
+
"plugin-uninstall",
|
|
4178
|
+
"plugin-install",
|
|
4179
|
+
"plugin-update",
|
|
4180
|
+
"plugin-enable"
|
|
4181
|
+
]);
|
|
4182
|
+
function manualCommands(pin) {
|
|
4183
|
+
return [
|
|
4184
|
+
`claude plugin marketplace add ${pin.url}#${pin.ref}`,
|
|
4185
|
+
`claude plugin install ${pluginId(pin)}`
|
|
4186
|
+
];
|
|
4187
|
+
}
|
|
4188
|
+
async function ensureGlobalPlugins(pins, deps = {}) {
|
|
4189
|
+
const exec = deps.exec ?? realExec;
|
|
4190
|
+
const log2 = deps.log ?? log;
|
|
4191
|
+
const warn2 = deps.warn ?? warn;
|
|
4192
|
+
const dryRun = deps.dryRun ?? false;
|
|
4193
|
+
if (globalPluginsDisabled() && deps.exec === void 0) {
|
|
4194
|
+
return { actions: [], ensured: [], failed: [], unavailable: true, restartRequired: false };
|
|
4195
|
+
}
|
|
4196
|
+
const state = await probePluginEnv(deps);
|
|
4197
|
+
const actions = planPluginActions(pins, state);
|
|
4198
|
+
const result = {
|
|
4199
|
+
actions,
|
|
4200
|
+
ensured: [],
|
|
4201
|
+
failed: [],
|
|
4202
|
+
unavailable: !state.available,
|
|
4203
|
+
restartRequired: false
|
|
4204
|
+
};
|
|
4205
|
+
for (const action of actions) {
|
|
4206
|
+
if (action.kind === "unavailable") {
|
|
4207
|
+
warn2(
|
|
4208
|
+
`Claude Code CLI not found \u2014 cannot manage the ${action.pin.id} plugin. Install it manually:`
|
|
4209
|
+
);
|
|
4210
|
+
manualCommands(action.pin).forEach((cmd) => warn2(` ${cmd}`));
|
|
4211
|
+
continue;
|
|
4212
|
+
}
|
|
4213
|
+
if (action.kind === "up-to-date") {
|
|
4214
|
+
result.ensured.push(action.pin.id);
|
|
4215
|
+
continue;
|
|
4216
|
+
}
|
|
4217
|
+
if (action.overriddenRef !== void 0 && action.overriddenRef !== action.pin.ref) {
|
|
4218
|
+
warn2(
|
|
4219
|
+
`Re-pointing marketplace ${action.pin.marketplace} from ${action.overriddenRef} to the haus-pinned ${action.pin.ref} \u2014 haus manages this plugin's version.`
|
|
4220
|
+
);
|
|
4221
|
+
}
|
|
4222
|
+
if (dryRun) {
|
|
4223
|
+
log2(`[dry-run] would ${action.kind} ${action.pin.id}: ${action.reason}`);
|
|
4224
|
+
action.steps.forEach(
|
|
4225
|
+
(step) => log2(`[dry-run] claude ${stepArgs(step, action.pin).join(" ")}`)
|
|
4226
|
+
);
|
|
4227
|
+
continue;
|
|
4228
|
+
}
|
|
4229
|
+
let ok = true;
|
|
4230
|
+
for (const step of action.steps) {
|
|
4231
|
+
const args = stepArgs(step, action.pin);
|
|
4232
|
+
const run = await exec(args);
|
|
4233
|
+
if (run.exitCode !== 0) {
|
|
4234
|
+
ok = false;
|
|
4235
|
+
warn2(`\`claude ${args.join(" ")}\` failed: ${run.stderr.trim() || `exit ${run.exitCode}`}`);
|
|
4236
|
+
break;
|
|
4237
|
+
}
|
|
4238
|
+
if (RESTART_STEPS.has(step)) result.restartRequired = true;
|
|
4239
|
+
}
|
|
4240
|
+
if (ok) {
|
|
4241
|
+
result.ensured.push(action.pin.id);
|
|
4242
|
+
log2(
|
|
4243
|
+
`Global plugin ${action.pin.id} ${action.kind === "install" ? "installed" : "updated"} (${action.pin.ref}).`
|
|
4244
|
+
);
|
|
4245
|
+
} else {
|
|
4246
|
+
result.failed.push(action.pin.id);
|
|
4247
|
+
warn2(`Could not set up the ${action.pin.id} plugin. Run manually:`);
|
|
4248
|
+
manualCommands(action.pin).forEach((cmd) => warn2(` ${cmd}`));
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4251
|
+
if (result.restartRequired && !dryRun) {
|
|
4252
|
+
log2("Restart Claude Code to load the updated plugin.");
|
|
4253
|
+
}
|
|
4254
|
+
return result;
|
|
4255
|
+
}
|
|
4256
|
+
async function removeGlobalPlugins(plugins, deps = {}) {
|
|
4257
|
+
const exec = deps.exec ?? realExec;
|
|
4258
|
+
const warn2 = deps.warn ?? warn;
|
|
4259
|
+
const log2 = deps.log ?? log;
|
|
4260
|
+
const result = { removed: [], failed: [], unavailable: false };
|
|
4261
|
+
if (plugins.length === 0) return result;
|
|
4262
|
+
if (globalPluginsDisabled() && deps.exec === void 0) {
|
|
4263
|
+
return { ...result, unavailable: true };
|
|
4264
|
+
}
|
|
4265
|
+
const state = await probePluginEnv(deps);
|
|
4266
|
+
if (!state.available) {
|
|
4267
|
+
result.unavailable = true;
|
|
4268
|
+
warn2("Claude Code CLI not found \u2014 leaving the haus-managed global plugin(s) in place.");
|
|
4269
|
+
return result;
|
|
4270
|
+
}
|
|
4271
|
+
const scopeKey = (id, scope) => `${id}@@${scope ?? "user"}`;
|
|
4272
|
+
const stillInstalled = new Set(state.installed.map((p) => scopeKey(p.id, p.scope)));
|
|
4273
|
+
for (const pin of plugins) {
|
|
4274
|
+
const id = pluginId(pin);
|
|
4275
|
+
const managedKey = scopeKey(id, "user");
|
|
4276
|
+
if (deps.dryRun) {
|
|
4277
|
+
if (stillInstalled.has(managedKey))
|
|
4278
|
+
log2(`[dry-run] would remove global plugin ${pin.id} (${id})`);
|
|
4279
|
+
continue;
|
|
4280
|
+
}
|
|
4281
|
+
if (stillInstalled.has(managedKey)) {
|
|
4282
|
+
const uninstalled = await exec(["plugin", "uninstall", id, "--scope", "user"]);
|
|
4283
|
+
if (uninstalled.exitCode !== 0) {
|
|
4284
|
+
result.failed.push(pin.id);
|
|
4285
|
+
warn2(
|
|
4286
|
+
`Could not remove plugin ${id}: ${uninstalled.stderr.trim() || `exit ${uninstalled.exitCode}`}`
|
|
4287
|
+
);
|
|
4288
|
+
continue;
|
|
4289
|
+
}
|
|
4290
|
+
stillInstalled.delete(managedKey);
|
|
4291
|
+
result.removed.push(pin.id);
|
|
4292
|
+
}
|
|
4293
|
+
const marketplaceStillConfigured = state.marketplaces.some((m) => m.name === pin.marketplace);
|
|
4294
|
+
const othersFromMarketplace = [...stillInstalled].some(
|
|
4295
|
+
(key) => key.split("@@")[0]?.endsWith(`@${pin.marketplace}`)
|
|
4296
|
+
);
|
|
4297
|
+
if (marketplaceStillConfigured && !othersFromMarketplace) {
|
|
4298
|
+
const removedMarket = await exec(["plugin", "marketplace", "remove", pin.marketplace]);
|
|
4299
|
+
if (removedMarket.exitCode !== 0) {
|
|
4300
|
+
warn2(
|
|
4301
|
+
`Could not remove marketplace ${pin.marketplace}: ${removedMarket.stderr.trim() || `exit ${removedMarket.exitCode}`}`
|
|
4302
|
+
);
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
}
|
|
4306
|
+
return result;
|
|
4307
|
+
}
|
|
4308
|
+
|
|
3789
4309
|
// src/refs/fetch-refs.ts
|
|
3790
|
-
import
|
|
4310
|
+
import path24 from "node:path";
|
|
3791
4311
|
import fs21 from "fs-extra";
|
|
3792
4312
|
|
|
3793
4313
|
// src/refs/cache-meta.ts
|
|
3794
|
-
import
|
|
4314
|
+
import path23 from "node:path";
|
|
3795
4315
|
import fs20 from "fs-extra";
|
|
3796
4316
|
var META_FILENAME = "cache-meta.json";
|
|
3797
4317
|
async function readCacheMeta(cacheDir) {
|
|
3798
|
-
const metaPath =
|
|
4318
|
+
const metaPath = path23.join(cacheDir, META_FILENAME);
|
|
3799
4319
|
try {
|
|
3800
4320
|
const raw = await fs20.readFile(metaPath, "utf8");
|
|
3801
4321
|
return JSON.parse(raw);
|
|
@@ -3805,7 +4325,7 @@ async function readCacheMeta(cacheDir) {
|
|
|
3805
4325
|
}
|
|
3806
4326
|
async function writeCacheMeta(cacheDir, meta) {
|
|
3807
4327
|
await fs20.ensureDir(cacheDir);
|
|
3808
|
-
const metaPath =
|
|
4328
|
+
const metaPath = path23.join(cacheDir, META_FILENAME);
|
|
3809
4329
|
await fs20.writeFile(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf8");
|
|
3810
4330
|
}
|
|
3811
4331
|
|
|
@@ -3826,8 +4346,8 @@ function isLlmsTxtUrl(ref) {
|
|
|
3826
4346
|
async function fetchSingleRef(url, cacheDir, meta) {
|
|
3827
4347
|
const existing = meta[url];
|
|
3828
4348
|
const headers = {};
|
|
3829
|
-
const safeFile = existing?.file && existing.file ===
|
|
3830
|
-
const cachedFileExists = safeFile && await fs21.pathExists(
|
|
4349
|
+
const safeFile = existing?.file && existing.file === path24.basename(existing.file);
|
|
4350
|
+
const cachedFileExists = safeFile && await fs21.pathExists(path24.join(cacheDir, existing.file));
|
|
3831
4351
|
if (cachedFileExists) {
|
|
3832
4352
|
if (existing?.etag) headers["If-None-Match"] = existing.etag;
|
|
3833
4353
|
if (existing?.lastModified) headers["If-Modified-Since"] = existing.lastModified;
|
|
@@ -3839,7 +4359,7 @@ async function fetchSingleRef(url, cacheDir, meta) {
|
|
|
3839
4359
|
const text = await res.text();
|
|
3840
4360
|
const file = `${urlToSlug(url)}.md`;
|
|
3841
4361
|
await fs21.ensureDir(cacheDir);
|
|
3842
|
-
await fs21.writeFile(
|
|
4362
|
+
await fs21.writeFile(path24.join(cacheDir, file), text, "utf8");
|
|
3843
4363
|
meta[url] = {
|
|
3844
4364
|
url,
|
|
3845
4365
|
etag: res.headers.get("etag") ?? void 0,
|
|
@@ -3862,8 +4382,8 @@ async function pruneOrphanedRefs(cacheDir, keepUrls) {
|
|
|
3862
4382
|
if (keepUrls.has(url)) continue;
|
|
3863
4383
|
delete meta[url];
|
|
3864
4384
|
removed++;
|
|
3865
|
-
const safeFile = typeof entry.file === "string" && entry.file ===
|
|
3866
|
-
if (safeFile) await fs21.remove(
|
|
4385
|
+
const safeFile = typeof entry.file === "string" && entry.file === path24.basename(entry.file);
|
|
4386
|
+
if (safeFile) await fs21.remove(path24.join(cacheDir, entry.file)).catch(() => {
|
|
3867
4387
|
});
|
|
3868
4388
|
}
|
|
3869
4389
|
if (removed > 0) await writeCacheMeta(cacheDir, meta);
|
|
@@ -3885,10 +4405,10 @@ async function fetchRefsForItems(items, cacheDir) {
|
|
|
3885
4405
|
const outcome = await fetchSingleRef(url, cacheDir, meta);
|
|
3886
4406
|
if (outcome.result === "fetched") {
|
|
3887
4407
|
summary.fetched++;
|
|
3888
|
-
summary.cachedFiles[url] =
|
|
4408
|
+
summary.cachedFiles[url] = path24.join(cacheDir, outcome.file);
|
|
3889
4409
|
} else if (outcome.result === "unchanged") {
|
|
3890
4410
|
summary.unchanged++;
|
|
3891
|
-
if (meta[url]?.file) summary.cachedFiles[url] =
|
|
4411
|
+
if (meta[url]?.file) summary.cachedFiles[url] = path24.join(cacheDir, meta[url].file);
|
|
3892
4412
|
} else {
|
|
3893
4413
|
summary.failed++;
|
|
3894
4414
|
summary.failedUrls.push(url);
|
|
@@ -3907,11 +4427,17 @@ function parseIdList(value) {
|
|
|
3907
4427
|
const raw = Array.isArray(value) ? value : [value];
|
|
3908
4428
|
return raw.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v.length > 0);
|
|
3909
4429
|
}
|
|
4430
|
+
function normalizeOnly(only) {
|
|
4431
|
+
if (!only) return void 0;
|
|
4432
|
+
const list = Array.isArray(only) ? only : only.split(/[\s,]+/);
|
|
4433
|
+
const cleaned = list.map((s) => s.trim()).filter(Boolean);
|
|
4434
|
+
return cleaned.length > 0 ? cleaned : void 0;
|
|
4435
|
+
}
|
|
3910
4436
|
|
|
3911
4437
|
// src/commands/apply.ts
|
|
3912
4438
|
var STALE_CACHE_THRESHOLD_DAYS = 7;
|
|
3913
4439
|
async function cacheHasItems() {
|
|
3914
|
-
const data = await readJson(
|
|
4440
|
+
const data = await readJson(path25.join(getCacheDir(), "manifest.json"));
|
|
3915
4441
|
return Array.isArray(data?.items) && data.items.length > 0;
|
|
3916
4442
|
}
|
|
3917
4443
|
function installableRecommendedItems(recommended) {
|
|
@@ -4015,7 +4541,9 @@ async function runApply(options) {
|
|
|
4015
4541
|
const files = await writeClaudeFiles(root, isDryRun, selectedIds, {
|
|
4016
4542
|
refillConfig: options.refillConfig,
|
|
4017
4543
|
force: options.force,
|
|
4018
|
-
prune: options.prune
|
|
4544
|
+
prune: options.prune,
|
|
4545
|
+
// Copies a global plugin already provides are removed here (issue #272).
|
|
4546
|
+
activePluginIds: await activeGlobalPluginIds(root)
|
|
4019
4547
|
});
|
|
4020
4548
|
await writeGitignore(root, isDryRun);
|
|
4021
4549
|
await untrackMachineLocalArtifacts(root, isDryRun, warn);
|
|
@@ -4062,7 +4590,7 @@ async function runApply(options) {
|
|
|
4062
4590
|
}
|
|
4063
4591
|
|
|
4064
4592
|
// src/commands/backups.ts
|
|
4065
|
-
import
|
|
4593
|
+
import path26 from "node:path";
|
|
4066
4594
|
import fs22 from "fs-extra";
|
|
4067
4595
|
|
|
4068
4596
|
// src/utils/prompts.ts
|
|
@@ -4099,7 +4627,7 @@ async function listBackups(root) {
|
|
|
4099
4627
|
for (const name of names) {
|
|
4100
4628
|
const kind = classify(name);
|
|
4101
4629
|
if (!kind) continue;
|
|
4102
|
-
const absPath =
|
|
4630
|
+
const absPath = path26.join(dir, name);
|
|
4103
4631
|
const stat = await fs22.lstat(absPath);
|
|
4104
4632
|
if (stat.isSymbolicLink()) continue;
|
|
4105
4633
|
const expectDirectory = kind !== "lock";
|
|
@@ -4129,10 +4657,10 @@ async function runList(root) {
|
|
|
4129
4657
|
}
|
|
4130
4658
|
}
|
|
4131
4659
|
async function isUnsafeWriteTarget(root, target) {
|
|
4132
|
-
const segments =
|
|
4660
|
+
const segments = path26.relative(root, target).split(path26.sep).filter(Boolean);
|
|
4133
4661
|
let current = root;
|
|
4134
4662
|
for (let i = 0; i < segments.length; i++) {
|
|
4135
|
-
current =
|
|
4663
|
+
current = path26.join(current, segments[i]);
|
|
4136
4664
|
const isLastSegment = i === segments.length - 1;
|
|
4137
4665
|
try {
|
|
4138
4666
|
const stat = await fs22.lstat(current);
|
|
@@ -4196,7 +4724,7 @@ async function collectFilesRecursive(dir) {
|
|
|
4196
4724
|
const skippedOther = [];
|
|
4197
4725
|
const names = await fs22.readdir(dir);
|
|
4198
4726
|
for (const name of names) {
|
|
4199
|
-
const abs =
|
|
4727
|
+
const abs = path26.join(dir, name);
|
|
4200
4728
|
const stat = await fs22.lstat(abs);
|
|
4201
4729
|
if (stat.isSymbolicLink()) {
|
|
4202
4730
|
skippedLinks.push(abs);
|
|
@@ -4219,12 +4747,12 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
4219
4747
|
const { files, skippedLinks, skippedOther } = await collectFilesRecursive(entry.absPath);
|
|
4220
4748
|
if (skippedLinks.length > 0) {
|
|
4221
4749
|
warn(
|
|
4222
|
-
`Skipped ${skippedLinks.length} symlink(s) inside backup ${entry.id} \u2014 never followed: ${skippedLinks.map((abs) =>
|
|
4750
|
+
`Skipped ${skippedLinks.length} symlink(s) inside backup ${entry.id} \u2014 never followed: ${skippedLinks.map((abs) => path26.relative(entry.absPath, abs)).join(", ")}`
|
|
4223
4751
|
);
|
|
4224
4752
|
}
|
|
4225
4753
|
if (skippedOther.length > 0) {
|
|
4226
4754
|
warn(
|
|
4227
|
-
`Skipped ${skippedOther.length} non-regular file(s) inside backup ${entry.id} (not restorable): ${skippedOther.map((abs) =>
|
|
4755
|
+
`Skipped ${skippedOther.length} non-regular file(s) inside backup ${entry.id} (not restorable): ${skippedOther.map((abs) => path26.relative(entry.absPath, abs)).join(", ")}`
|
|
4228
4756
|
);
|
|
4229
4757
|
}
|
|
4230
4758
|
if (files.length === 0) {
|
|
@@ -4234,8 +4762,8 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
4234
4762
|
const safeFiles = [];
|
|
4235
4763
|
const unsafeTargets = [];
|
|
4236
4764
|
for (const abs of files) {
|
|
4237
|
-
const rel =
|
|
4238
|
-
const target =
|
|
4765
|
+
const rel = path26.relative(entry.absPath, abs);
|
|
4766
|
+
const target = path26.join(root, rel);
|
|
4239
4767
|
if (await isUnsafeWriteTarget(root, target)) unsafeTargets.push(rel);
|
|
4240
4768
|
else safeFiles.push(abs);
|
|
4241
4769
|
}
|
|
@@ -4252,14 +4780,14 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
4252
4780
|
const vanished = [];
|
|
4253
4781
|
const toRestore = [];
|
|
4254
4782
|
for (const abs of safeFiles) {
|
|
4255
|
-
const rel =
|
|
4783
|
+
const rel = path26.relative(entry.absPath, abs);
|
|
4256
4784
|
const backupFileStat = await lstatIfRegularFile(abs);
|
|
4257
4785
|
if (!backupFileStat) {
|
|
4258
4786
|
vanished.push(rel);
|
|
4259
4787
|
continue;
|
|
4260
4788
|
}
|
|
4261
4789
|
toRestore.push(abs);
|
|
4262
|
-
const target =
|
|
4790
|
+
const target = path26.join(root, rel);
|
|
4263
4791
|
if (await fs22.pathExists(target)) {
|
|
4264
4792
|
const targetStat = await fs22.stat(target);
|
|
4265
4793
|
if (targetStat.mtimeMs > backupFileStat.mtimeMs) {
|
|
@@ -4300,13 +4828,13 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
4300
4828
|
let restoredCount = 0;
|
|
4301
4829
|
const skippedAtWrite = [];
|
|
4302
4830
|
for (const abs of toRestore) {
|
|
4303
|
-
const rel =
|
|
4304
|
-
const target =
|
|
4831
|
+
const rel = path26.relative(entry.absPath, abs);
|
|
4832
|
+
const target = path26.join(root, rel);
|
|
4305
4833
|
if (!await lstatIfRegularFile(abs) || await isUnsafeWriteTarget(root, target)) {
|
|
4306
4834
|
skippedAtWrite.push(rel);
|
|
4307
4835
|
continue;
|
|
4308
4836
|
}
|
|
4309
|
-
await fs22.ensureDir(
|
|
4837
|
+
await fs22.ensureDir(path26.dirname(target));
|
|
4310
4838
|
await fs22.copy(abs, target, { overwrite: true });
|
|
4311
4839
|
restoredCount++;
|
|
4312
4840
|
}
|
|
@@ -4445,18 +4973,19 @@ async function runCatalogAudit() {
|
|
|
4445
4973
|
}
|
|
4446
4974
|
|
|
4447
4975
|
// src/commands/doctor.ts
|
|
4448
|
-
import
|
|
4976
|
+
import path40 from "node:path";
|
|
4449
4977
|
import fs30 from "fs-extra";
|
|
4450
4978
|
|
|
4451
4979
|
// src/claude/skill-reference-check.ts
|
|
4452
|
-
import
|
|
4980
|
+
import path27 from "node:path";
|
|
4453
4981
|
import fg4 from "fast-glob";
|
|
4454
4982
|
import fs23 from "fs-extra";
|
|
4455
4983
|
var REFERENCE_PATTERN = /\b([a-z][a-z0-9]*(?:-[a-z0-9]+)+):([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\b(?!:\/\/)/g;
|
|
4456
4984
|
function stripFencedCodeBlocks(text) {
|
|
4457
4985
|
return text.replace(/```[\s\S]*?```/g, (block) => block.replace(/[^\n]/g, " "));
|
|
4458
4986
|
}
|
|
4459
|
-
async function findUnresolvedSkillReferences(root) {
|
|
4987
|
+
async function findUnresolvedSkillReferences(root, options = {}) {
|
|
4988
|
+
const resolvableNamespaces = new Set(options.pluginNamespaces ?? []);
|
|
4460
4989
|
const skillsRoot = claudePath(root, "skills");
|
|
4461
4990
|
if (!await fs23.pathExists(skillsRoot)) return [];
|
|
4462
4991
|
const skillDirs = (await fs23.readdir(skillsRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory() && entry.name !== "shared").map((entry) => entry.name);
|
|
@@ -4465,15 +4994,16 @@ async function findUnresolvedSkillReferences(root) {
|
|
|
4465
4994
|
const issues = [];
|
|
4466
4995
|
for (const rel of files) {
|
|
4467
4996
|
if (rel.startsWith("shared/")) continue;
|
|
4468
|
-
const abs =
|
|
4997
|
+
const abs = path27.join(skillsRoot, rel);
|
|
4469
4998
|
const raw = await fs23.readFile(abs, "utf8");
|
|
4470
4999
|
const lines = stripFencedCodeBlocks(raw).split("\n");
|
|
4471
5000
|
lines.forEach((line2, idx) => {
|
|
4472
5001
|
for (const match of line2.matchAll(REFERENCE_PATTERN)) {
|
|
4473
|
-
const [reference, , bareName] = match;
|
|
5002
|
+
const [reference, namespace, bareName] = match;
|
|
4474
5003
|
if (!reference || !bareName) continue;
|
|
5004
|
+
if (namespace && resolvableNamespaces.has(namespace)) continue;
|
|
4475
5005
|
issues.push({
|
|
4476
|
-
file:
|
|
5006
|
+
file: path27.join(".claude/skills", rel),
|
|
4477
5007
|
line: idx + 1,
|
|
4478
5008
|
reference,
|
|
4479
5009
|
bareName,
|
|
@@ -4486,11 +5016,11 @@ async function findUnresolvedSkillReferences(root) {
|
|
|
4486
5016
|
}
|
|
4487
5017
|
|
|
4488
5018
|
// src/decisions/doctor.ts
|
|
4489
|
-
import
|
|
5019
|
+
import path32 from "node:path";
|
|
4490
5020
|
import fs27 from "fs-extra";
|
|
4491
5021
|
|
|
4492
5022
|
// src/decisions/check.ts
|
|
4493
|
-
import
|
|
5023
|
+
import path31 from "node:path";
|
|
4494
5024
|
import fs26 from "fs-extra";
|
|
4495
5025
|
|
|
4496
5026
|
// src/decisions/dep-bump.ts
|
|
@@ -4550,7 +5080,7 @@ function isDependencyBumpOnly(beforeText, afterText) {
|
|
|
4550
5080
|
|
|
4551
5081
|
// src/decisions/diff.ts
|
|
4552
5082
|
init_exec();
|
|
4553
|
-
import
|
|
5083
|
+
import path28 from "node:path";
|
|
4554
5084
|
function isPackageJsonPath(file) {
|
|
4555
5085
|
return file === "package.json" || file.endsWith("/package.json");
|
|
4556
5086
|
}
|
|
@@ -4611,7 +5141,7 @@ async function resolvePackageJsonPair(root, file, opts) {
|
|
|
4611
5141
|
return { before: before2, after: after2 };
|
|
4612
5142
|
}
|
|
4613
5143
|
const before = await readBlob(root, `${range}:${file}`) ?? empty;
|
|
4614
|
-
const after = await readText(
|
|
5144
|
+
const after = await readText(path28.join(root, file)) ?? empty;
|
|
4615
5145
|
return { before, after };
|
|
4616
5146
|
}
|
|
4617
5147
|
async function collectDiffStats(root, opts) {
|
|
@@ -4682,21 +5212,21 @@ function firstMatchingRegex(filePath, patterns) {
|
|
|
4682
5212
|
}
|
|
4683
5213
|
|
|
4684
5214
|
// src/decisions/paths.ts
|
|
4685
|
-
import
|
|
5215
|
+
import path29 from "node:path";
|
|
4686
5216
|
import fs24 from "fs-extra";
|
|
4687
5217
|
async function resolveDecisionsDir(root) {
|
|
4688
|
-
const primary =
|
|
5218
|
+
const primary = path29.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
4689
5219
|
if (await fs24.pathExists(primary)) return primary;
|
|
4690
|
-
const legacy =
|
|
5220
|
+
const legacy = path29.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
4691
5221
|
if (await fs24.pathExists(legacy)) return legacy;
|
|
4692
5222
|
return primary;
|
|
4693
5223
|
}
|
|
4694
5224
|
function relativeDecisionPath(decisionsDir, root) {
|
|
4695
|
-
return
|
|
5225
|
+
return path29.relative(root, decisionsDir).replace(/\\/g, "/");
|
|
4696
5226
|
}
|
|
4697
5227
|
|
|
4698
5228
|
// src/decisions/validate.ts
|
|
4699
|
-
import
|
|
5229
|
+
import path30 from "node:path";
|
|
4700
5230
|
import fs25 from "fs-extra";
|
|
4701
5231
|
function validateDecisionContent(content2) {
|
|
4702
5232
|
const errors = [];
|
|
@@ -4726,14 +5256,14 @@ function decisionNumberFromFile(name) {
|
|
|
4726
5256
|
return match?.[1] ?? null;
|
|
4727
5257
|
}
|
|
4728
5258
|
async function validateIndexSync(decisionsDir, decisionFiles) {
|
|
4729
|
-
const readmePath =
|
|
5259
|
+
const readmePath = path30.join(decisionsDir, "README.md");
|
|
4730
5260
|
if (!await fs25.pathExists(readmePath)) {
|
|
4731
5261
|
return ["docs/decisions/README.md is missing"];
|
|
4732
5262
|
}
|
|
4733
5263
|
const readme = await readText(readmePath) ?? "";
|
|
4734
5264
|
const errors = [];
|
|
4735
5265
|
for (const file of decisionFiles) {
|
|
4736
|
-
const num = decisionNumberFromFile(
|
|
5266
|
+
const num = decisionNumberFromFile(path30.basename(file));
|
|
4737
5267
|
if (num && !indexReferencesNumber(readme, num)) {
|
|
4738
5268
|
errors.push(`README.md missing index row for ADR-${num}`);
|
|
4739
5269
|
}
|
|
@@ -4828,22 +5358,22 @@ async function checkSatisfaction(input2) {
|
|
|
4828
5358
|
const errors = [];
|
|
4829
5359
|
const decisionsPrefix = `${input2.decisionsRel}/`;
|
|
4830
5360
|
const newDecisionFiles = input2.changedFiles.filter(
|
|
4831
|
-
(f) => f.startsWith(decisionsPrefix) && isDecisionFileName(
|
|
5361
|
+
(f) => f.startsWith(decisionsPrefix) && isDecisionFileName(path31.basename(f))
|
|
4832
5362
|
);
|
|
4833
5363
|
if (newDecisionFiles.length === 0) {
|
|
4834
5364
|
errors.push(`no new or updated decision under ${input2.decisionsRel}/`);
|
|
4835
5365
|
return errors;
|
|
4836
5366
|
}
|
|
4837
|
-
const decisionsDir =
|
|
5367
|
+
const decisionsDir = path31.join(input2.root, input2.decisionsRel);
|
|
4838
5368
|
for (const rel of newDecisionFiles) {
|
|
4839
|
-
const abs =
|
|
5369
|
+
const abs = path31.join(input2.root, rel);
|
|
4840
5370
|
const content2 = await readText(abs) ?? "";
|
|
4841
5371
|
const valid = validateDecisionContent(content2);
|
|
4842
5372
|
if (!valid.ok) errors.push(...valid.errors.map((e) => `${rel}: ${e}`));
|
|
4843
5373
|
}
|
|
4844
5374
|
const indexErrors = await validateIndexSync(
|
|
4845
5375
|
decisionsDir,
|
|
4846
|
-
newDecisionFiles.map((f) =>
|
|
5376
|
+
newDecisionFiles.map((f) => path31.basename(f))
|
|
4847
5377
|
);
|
|
4848
5378
|
errors.push(...indexErrors);
|
|
4849
5379
|
return errors;
|
|
@@ -4899,7 +5429,7 @@ ${opts.commitMessages ?? ""}`;
|
|
|
4899
5429
|
changedFiles: stats.files
|
|
4900
5430
|
});
|
|
4901
5431
|
if (satisfactionErrors.length > 0 && decisionsRel !== legacyRel) {
|
|
4902
|
-
const legacyExists = await fs26.pathExists(
|
|
5432
|
+
const legacyExists = await fs26.pathExists(path31.join(root, legacyRel));
|
|
4903
5433
|
if (legacyExists && !await fs26.pathExists(decisionsDir)) {
|
|
4904
5434
|
satisfactionErrors = await checkSatisfaction({
|
|
4905
5435
|
root,
|
|
@@ -4930,8 +5460,8 @@ async function listDecisionNumbers(decisionsDir) {
|
|
|
4930
5460
|
// src/decisions/doctor.ts
|
|
4931
5461
|
async function auditDecisionsLayout(root) {
|
|
4932
5462
|
const findings = [];
|
|
4933
|
-
const primary =
|
|
4934
|
-
const legacy =
|
|
5463
|
+
const primary = path32.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
5464
|
+
const legacy = path32.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
4935
5465
|
const hasPrimary = await fs27.pathExists(primary);
|
|
4936
5466
|
const hasLegacy = await fs27.pathExists(legacy);
|
|
4937
5467
|
if (!hasPrimary && hasLegacy) {
|
|
@@ -4946,7 +5476,7 @@ async function auditDecisionsLayout(root) {
|
|
|
4946
5476
|
if (!hasPrimary) return findings;
|
|
4947
5477
|
const decisionsDir = await resolveDecisionsDir(root);
|
|
4948
5478
|
const rel = relativeDecisionPath(decisionsDir, root);
|
|
4949
|
-
const readmePath =
|
|
5479
|
+
const readmePath = path32.join(decisionsDir, "README.md");
|
|
4950
5480
|
if (!await fs27.pathExists(readmePath)) {
|
|
4951
5481
|
findings.push({
|
|
4952
5482
|
level: "advisory",
|
|
@@ -4971,7 +5501,7 @@ async function auditDecisionsLayout(root) {
|
|
|
4971
5501
|
});
|
|
4972
5502
|
}
|
|
4973
5503
|
}
|
|
4974
|
-
const claude = await readText(
|
|
5504
|
+
const claude = await readText(path32.join(root, "CLAUDE.md"));
|
|
4975
5505
|
if (claude && !claude.includes(`@${rel}/README.md`)) {
|
|
4976
5506
|
findings.push({
|
|
4977
5507
|
level: "advisory",
|
|
@@ -5002,11 +5532,11 @@ async function auditDecisionsLayout(root) {
|
|
|
5002
5532
|
}
|
|
5003
5533
|
|
|
5004
5534
|
// src/scanner/read-context.ts
|
|
5005
|
-
import
|
|
5535
|
+
import path39 from "node:path";
|
|
5006
5536
|
import fs29 from "fs-extra";
|
|
5007
5537
|
|
|
5008
5538
|
// src/commands/workspace/config.ts
|
|
5009
|
-
import
|
|
5539
|
+
import path33 from "node:path";
|
|
5010
5540
|
import YAML from "yaml";
|
|
5011
5541
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
5012
5542
|
function parseWorkspaceConfig(text) {
|
|
@@ -5029,11 +5559,11 @@ function parseWorkspaceConfig(text) {
|
|
|
5029
5559
|
};
|
|
5030
5560
|
}
|
|
5031
5561
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
5032
|
-
return parseWorkspaceConfig(await readText(
|
|
5562
|
+
return parseWorkspaceConfig(await readText(path33.join(workspaceRoot, WORKSPACE_FILE)));
|
|
5033
5563
|
}
|
|
5034
5564
|
|
|
5035
5565
|
// src/workspace/members.ts
|
|
5036
|
-
import
|
|
5566
|
+
import path34 from "node:path";
|
|
5037
5567
|
var REPOS_MANIFEST_FILE = "repos.manifest.json";
|
|
5038
5568
|
var REPOS_LOCAL_FILE = "repos.local.json";
|
|
5039
5569
|
var MemberConfigError = class extends Error {
|
|
@@ -5046,14 +5576,14 @@ var MemberConfigError = class extends Error {
|
|
|
5046
5576
|
};
|
|
5047
5577
|
function resolveAbsPath(workspaceRoot, folder, overrides) {
|
|
5048
5578
|
const override = overrides[folder];
|
|
5049
|
-
if (override) return
|
|
5050
|
-
return
|
|
5579
|
+
if (override) return path34.resolve(workspaceRoot, override);
|
|
5580
|
+
return path34.resolve(workspaceRoot, folder);
|
|
5051
5581
|
}
|
|
5052
5582
|
function isSelfMember(workspaceRoot, member) {
|
|
5053
|
-
return
|
|
5583
|
+
return path34.resolve(member.absPath) === path34.resolve(workspaceRoot);
|
|
5054
5584
|
}
|
|
5055
5585
|
async function readPathOverrides(workspaceRoot) {
|
|
5056
|
-
const filePath =
|
|
5586
|
+
const filePath = path34.join(workspaceRoot, REPOS_LOCAL_FILE);
|
|
5057
5587
|
const result = await readJsonDetailed(filePath);
|
|
5058
5588
|
if (result.status === "missing") return {};
|
|
5059
5589
|
if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
|
|
@@ -5069,7 +5599,7 @@ async function readPathOverrides(workspaceRoot) {
|
|
|
5069
5599
|
return overrides;
|
|
5070
5600
|
}
|
|
5071
5601
|
async function readFromWorkspaceYaml(workspaceRoot, overrides) {
|
|
5072
|
-
const filePath =
|
|
5602
|
+
const filePath = path34.join(workspaceRoot, WORKSPACE_FILE);
|
|
5073
5603
|
const text = await readText(filePath);
|
|
5074
5604
|
if (text === void 0) return void 0;
|
|
5075
5605
|
const config = parseWorkspaceConfig(text);
|
|
@@ -5082,7 +5612,7 @@ async function readFromWorkspaceYaml(workspaceRoot, overrides) {
|
|
|
5082
5612
|
}));
|
|
5083
5613
|
}
|
|
5084
5614
|
async function readFromManifest(workspaceRoot, overrides) {
|
|
5085
|
-
const filePath =
|
|
5615
|
+
const filePath = path34.join(workspaceRoot, REPOS_MANIFEST_FILE);
|
|
5086
5616
|
const result = await readJsonDetailed(filePath);
|
|
5087
5617
|
if (result.status === "missing") return void 0;
|
|
5088
5618
|
if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
|
|
@@ -5124,11 +5654,11 @@ async function readMembers(rootInfo) {
|
|
|
5124
5654
|
}
|
|
5125
5655
|
|
|
5126
5656
|
// src/scanner/scan-project.ts
|
|
5127
|
-
import
|
|
5657
|
+
import path38 from "node:path";
|
|
5128
5658
|
|
|
5129
5659
|
// src/utils/git-root.ts
|
|
5130
5660
|
init_exec();
|
|
5131
|
-
import
|
|
5661
|
+
import path35 from "node:path";
|
|
5132
5662
|
function fallback(start) {
|
|
5133
5663
|
return {
|
|
5134
5664
|
cwd: start,
|
|
@@ -5144,11 +5674,11 @@ function fallback(start) {
|
|
|
5144
5674
|
async function resolveAbsoluteGitPath(flag, cwd, repoRoot) {
|
|
5145
5675
|
const withFormat = await runGit(["rev-parse", "--path-format=absolute", flag], { cwd });
|
|
5146
5676
|
const raw = withFormat.exitCode === 0 ? withFormat.stdout.trim() : "";
|
|
5147
|
-
if (raw &&
|
|
5677
|
+
if (raw && path35.isAbsolute(raw)) return path35.resolve(raw);
|
|
5148
5678
|
const plain = await runGit(["rev-parse", flag], { cwd });
|
|
5149
5679
|
const plainRaw = plain.exitCode === 0 ? plain.stdout.trim() : "";
|
|
5150
5680
|
if (!plainRaw) return "";
|
|
5151
|
-
return
|
|
5681
|
+
return path35.isAbsolute(plainRaw) ? path35.resolve(plainRaw) : path35.resolve(repoRoot, plainRaw);
|
|
5152
5682
|
}
|
|
5153
5683
|
async function resolveRoots(start) {
|
|
5154
5684
|
const cwd = start ?? process.cwd();
|
|
@@ -5157,15 +5687,15 @@ async function resolveRoots(start) {
|
|
|
5157
5687
|
if (toplevel.exitCode !== 0) return fallback(cwd);
|
|
5158
5688
|
const toplevelRaw = toplevel.stdout.trim();
|
|
5159
5689
|
if (!toplevelRaw) return fallback(cwd);
|
|
5160
|
-
const repoRoot =
|
|
5690
|
+
const repoRoot = path35.resolve(toplevelRaw);
|
|
5161
5691
|
const [gitDir, gitCommonDir] = await Promise.all([
|
|
5162
5692
|
resolveAbsoluteGitPath("--git-dir", cwd, repoRoot),
|
|
5163
5693
|
resolveAbsoluteGitPath("--git-common-dir", cwd, repoRoot)
|
|
5164
5694
|
]);
|
|
5165
5695
|
if (!gitDir || !gitCommonDir) return fallback(cwd);
|
|
5166
5696
|
const isLinkedWorktree = gitDir !== gitCommonDir;
|
|
5167
|
-
const mainRoot = isLinkedWorktree &&
|
|
5168
|
-
const worktreeName = isLinkedWorktree ?
|
|
5697
|
+
const mainRoot = isLinkedWorktree && path35.basename(gitCommonDir) === ".git" ? path35.dirname(gitCommonDir) : repoRoot;
|
|
5698
|
+
const worktreeName = isLinkedWorktree ? path35.basename(gitDir) : null;
|
|
5169
5699
|
return {
|
|
5170
5700
|
cwd,
|
|
5171
5701
|
repoRoot,
|
|
@@ -5210,7 +5740,7 @@ function resolveMajorVersion(range) {
|
|
|
5210
5740
|
}
|
|
5211
5741
|
|
|
5212
5742
|
// src/scanner/detect-package-manager.ts
|
|
5213
|
-
import
|
|
5743
|
+
import path36 from "node:path";
|
|
5214
5744
|
import fs28 from "fs-extra";
|
|
5215
5745
|
function detectPackageManager(root, packageManagerField) {
|
|
5216
5746
|
const field = String(packageManagerField ?? "").trim();
|
|
@@ -5229,9 +5759,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
5229
5759
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
5230
5760
|
return "unknown";
|
|
5231
5761
|
}
|
|
5232
|
-
if (fs28.existsSync(
|
|
5233
|
-
if (fs28.existsSync(
|
|
5234
|
-
if (fs28.existsSync(
|
|
5762
|
+
if (fs28.existsSync(path36.join(root, "yarn.lock"))) return "yarn";
|
|
5763
|
+
if (fs28.existsSync(path36.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
5764
|
+
if (fs28.existsSync(path36.join(root, "package-lock.json"))) return "npm";
|
|
5235
5765
|
return "unknown";
|
|
5236
5766
|
}
|
|
5237
5767
|
|
|
@@ -5456,7 +5986,7 @@ function runDetection(ctx, rules = STACK_RULES) {
|
|
|
5456
5986
|
|
|
5457
5987
|
// src/scanner/render.ts
|
|
5458
5988
|
import { readFile } from "node:fs/promises";
|
|
5459
|
-
import
|
|
5989
|
+
import path37 from "node:path";
|
|
5460
5990
|
async function buildContentBlob(root, files) {
|
|
5461
5991
|
const candidates = files.filter(
|
|
5462
5992
|
(f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
|
|
@@ -5464,7 +5994,7 @@ async function buildContentBlob(root, files) {
|
|
|
5464
5994
|
const slice = candidates.slice(0, 300);
|
|
5465
5995
|
const parts = await mapWithConcurrency(slice, async (rel) => {
|
|
5466
5996
|
try {
|
|
5467
|
-
return await readFile(
|
|
5997
|
+
return await readFile(path37.join(root, rel), "utf8");
|
|
5468
5998
|
} catch {
|
|
5469
5999
|
return "";
|
|
5470
6000
|
}
|
|
@@ -5556,8 +6086,8 @@ var SAFE_FILES = [
|
|
|
5556
6086
|
"Gemfile"
|
|
5557
6087
|
];
|
|
5558
6088
|
async function scanProject(root) {
|
|
5559
|
-
const pkg = await readJson(
|
|
5560
|
-
const composer = await readJson(
|
|
6089
|
+
const pkg = await readJson(path38.join(root, "package.json"));
|
|
6090
|
+
const composer = await readJson(path38.join(root, "composer.json"));
|
|
5561
6091
|
const files = await listFiles(root, SAFE_FILES);
|
|
5562
6092
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
5563
6093
|
const depVersions = dependencyVersionMap(pkg, composer);
|
|
@@ -5587,7 +6117,7 @@ async function scanProject(root) {
|
|
|
5587
6117
|
crossRepoHints.push("Monorepo orchestration detected");
|
|
5588
6118
|
const unsupportedSignals = collectUnsupportedSignals(safeFiles);
|
|
5589
6119
|
const detectionStatus = computeDetectionStatus(roles, stacks, unsupportedSignals);
|
|
5590
|
-
const repoNameFallback = pkg?.name ? String(pkg.name) :
|
|
6120
|
+
const repoNameFallback = pkg?.name ? String(pkg.name) : path38.basename((await resolveRoots(root)).mainRoot);
|
|
5591
6121
|
const context = {
|
|
5592
6122
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5593
6123
|
repoName: repoNameFallback,
|
|
@@ -5609,10 +6139,10 @@ async function scanProject(root) {
|
|
|
5609
6139
|
// src/scanner/read-context.ts
|
|
5610
6140
|
async function isCacheFresh(root, cachePath) {
|
|
5611
6141
|
const markerPaths = [
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
6142
|
+
path39.join(root, "package.json"),
|
|
6143
|
+
path39.join(root, "composer.json"),
|
|
6144
|
+
path39.join(root, WORKSPACE_FILE),
|
|
6145
|
+
path39.join(root, REPOS_MANIFEST_FILE)
|
|
5616
6146
|
];
|
|
5617
6147
|
const cacheStat = await fs29.stat(cachePath);
|
|
5618
6148
|
for (const markerPath of markerPaths) {
|
|
@@ -5718,7 +6248,7 @@ async function runDoctor(options) {
|
|
|
5718
6248
|
} else {
|
|
5719
6249
|
ok(`- HOOKS OK: ${hooks.message}`);
|
|
5720
6250
|
}
|
|
5721
|
-
const rootClaudeMdPath =
|
|
6251
|
+
const rootClaudeMdPath = path40.join(root, "CLAUDE.md");
|
|
5722
6252
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
5723
6253
|
if (!rootClaudeMdContent) {
|
|
5724
6254
|
flag(
|
|
@@ -5761,9 +6291,9 @@ async function runDoctor(options) {
|
|
|
5761
6291
|
ok("- CLAUDE.md: import block present");
|
|
5762
6292
|
}
|
|
5763
6293
|
for (const target of actualTargets) {
|
|
5764
|
-
const resolved =
|
|
5765
|
-
const relFromRoot =
|
|
5766
|
-
const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${
|
|
6294
|
+
const resolved = path40.resolve(root, target);
|
|
6295
|
+
const relFromRoot = path40.relative(root, resolved);
|
|
6296
|
+
const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${path40.sep}`) || path40.isAbsolute(relFromRoot);
|
|
5767
6297
|
if (escapesRoot || !await fs30.pathExists(resolved)) {
|
|
5768
6298
|
flag(
|
|
5769
6299
|
`- CLAUDE.md import: @${target} does not resolve (run \`haus apply --write\`)`,
|
|
@@ -5797,8 +6327,8 @@ async function runDoctor(options) {
|
|
|
5797
6327
|
} else {
|
|
5798
6328
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
5799
6329
|
const bodyContent = workflowContent.slice(firstLine.length + 1);
|
|
5800
|
-
const cachePath =
|
|
5801
|
-
const bundledPath =
|
|
6330
|
+
const cachePath = path40.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
6331
|
+
const bundledPath = path40.join(
|
|
5802
6332
|
packageRoot(),
|
|
5803
6333
|
"library",
|
|
5804
6334
|
"global",
|
|
@@ -5847,7 +6377,7 @@ async function runDoctor(options) {
|
|
|
5847
6377
|
}
|
|
5848
6378
|
}
|
|
5849
6379
|
if (workflowExists) {
|
|
5850
|
-
const prettierIgnore = await readText(
|
|
6380
|
+
const prettierIgnore = await readText(path40.join(root, ".prettierignore")) ?? "";
|
|
5851
6381
|
const lines = prettierIgnore.split("\n");
|
|
5852
6382
|
const missing = IGNORED_PATHS.filter((p) => !prettierIgnoreProtects(lines, p));
|
|
5853
6383
|
if (missing.length > 0) {
|
|
@@ -5887,7 +6417,50 @@ async function runDoctor(options) {
|
|
|
5887
6417
|
if (finding.level === "ok") ok(finding.line);
|
|
5888
6418
|
else suggest(finding.line, finding.sentence, finding.fix);
|
|
5889
6419
|
}
|
|
5890
|
-
const
|
|
6420
|
+
const pluginPins = await loadCatalogPluginPins(root);
|
|
6421
|
+
const pluginState = await probePluginEnv();
|
|
6422
|
+
const activePins = pluginPins.filter((pin) => isPluginActive(pluginState, pin));
|
|
6423
|
+
const activeNamespaces = activePins.map((pin) => pin.plugin);
|
|
6424
|
+
if (pluginPins.length === 0) {
|
|
6425
|
+
} else if (!pluginState.available) {
|
|
6426
|
+
suggest(
|
|
6427
|
+
"- GLOBAL PLUGINS: not checked (the `claude` CLI is not on PATH)",
|
|
6428
|
+
"Claude Code's CLI isn't on PATH, so haus can't verify the global plugin(s) it manages",
|
|
6429
|
+
"install Claude Code, then run `haus install`"
|
|
6430
|
+
);
|
|
6431
|
+
} else {
|
|
6432
|
+
const pending = planPluginActions(pluginPins, pluginState).filter(
|
|
6433
|
+
(action) => action.kind !== "up-to-date"
|
|
6434
|
+
);
|
|
6435
|
+
if (pending.length > 0) {
|
|
6436
|
+
suggest(
|
|
6437
|
+
`- GLOBAL PLUGINS: ${pending.length} plugin(s) not at the pinned state (${pending.map((a) => `${a.pin.id}: ${a.reason}`).join("; ")})`,
|
|
6438
|
+
`${pending.length} haus-managed global plugin(s) are missing, disabled, or off the pinned ref`,
|
|
6439
|
+
"haus update (or `haus install` outside a project)"
|
|
6440
|
+
);
|
|
6441
|
+
} else {
|
|
6442
|
+
ok(`- GLOBAL PLUGINS: OK (${pluginPins.map((p) => `${p.id}@${p.ref}`).join(", ")})`);
|
|
6443
|
+
}
|
|
6444
|
+
}
|
|
6445
|
+
if (activePins.length > 0) {
|
|
6446
|
+
const lockEntries = await readJson(hausPath(root, "haus.lock.json"));
|
|
6447
|
+
const superseded = (lockEntries ?? []).map((entry) => entry.id).filter(
|
|
6448
|
+
(id) => typeof id === "string" && isSupersededByPlugin(
|
|
6449
|
+
id,
|
|
6450
|
+
activePins.map((pin) => pin.id)
|
|
6451
|
+
)
|
|
6452
|
+
);
|
|
6453
|
+
if (superseded.length > 0) {
|
|
6454
|
+
suggest(
|
|
6455
|
+
`- GLOBAL PLUGINS: ${superseded.length} installed item(s) are superseded by the global plugin (${superseded.slice(0, 3).join(", ")}${superseded.length > 3 ? ", \u2026" : ""})`,
|
|
6456
|
+
`${superseded.length} project skill(s) duplicate what the global plugin already provides`,
|
|
6457
|
+
"haus update (removes the superseded copies)"
|
|
6458
|
+
);
|
|
6459
|
+
}
|
|
6460
|
+
}
|
|
6461
|
+
const skillRefIssues = await findUnresolvedSkillReferences(root, {
|
|
6462
|
+
pluginNamespaces: activeNamespaces
|
|
6463
|
+
});
|
|
5891
6464
|
if (skillRefIssues.length > 0) {
|
|
5892
6465
|
const prefixBug = skillRefIssues.filter((i) => i.bareInstalled);
|
|
5893
6466
|
const missingDep = skillRefIssues.filter((i) => !i.bareInstalled);
|
|
@@ -5951,7 +6524,7 @@ async function runDoctor(options) {
|
|
|
5951
6524
|
}
|
|
5952
6525
|
}
|
|
5953
6526
|
}
|
|
5954
|
-
const pkgJson = await readJson(
|
|
6527
|
+
const pkgJson = await readJson(path40.join(packageRoot(), "package.json"));
|
|
5955
6528
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
5956
6529
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
5957
6530
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -5984,7 +6557,7 @@ async function runDoctor(options) {
|
|
|
5984
6557
|
}
|
|
5985
6558
|
|
|
5986
6559
|
// src/commands/update.ts
|
|
5987
|
-
import
|
|
6560
|
+
import path43 from "node:path";
|
|
5988
6561
|
|
|
5989
6562
|
// src/claude/refresh-project.ts
|
|
5990
6563
|
import fs31 from "fs-extra";
|
|
@@ -5996,12 +6569,16 @@ async function isHausProject(root) {
|
|
|
5996
6569
|
}
|
|
5997
6570
|
async function refreshProjectApply(root) {
|
|
5998
6571
|
if (!await isHausProject(root)) return [];
|
|
5999
|
-
return writeClaudeFiles(root, false, void 0, {
|
|
6572
|
+
return writeClaudeFiles(root, false, void 0, {
|
|
6573
|
+
refillConfig: false,
|
|
6574
|
+
// Superseded copies are removed in the same pass that re-applies the catalog.
|
|
6575
|
+
activePluginIds: await activeGlobalPluginIds(root)
|
|
6576
|
+
});
|
|
6000
6577
|
}
|
|
6001
6578
|
|
|
6002
6579
|
// src/install/apply.ts
|
|
6003
6580
|
import crypto2 from "node:crypto";
|
|
6004
|
-
import
|
|
6581
|
+
import path41 from "node:path";
|
|
6005
6582
|
import fs32 from "fs-extra";
|
|
6006
6583
|
|
|
6007
6584
|
// src/install/header.ts
|
|
@@ -6095,7 +6672,7 @@ function destFileMissing(destPath) {
|
|
|
6095
6672
|
}
|
|
6096
6673
|
function sourceVersion() {
|
|
6097
6674
|
try {
|
|
6098
|
-
const pkgPath =
|
|
6675
|
+
const pkgPath = path41.join(packageRoot(), "package.json");
|
|
6099
6676
|
const pkg = JSON.parse(fs32.readFileSync(pkgPath, "utf8"));
|
|
6100
6677
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
6101
6678
|
} catch {
|
|
@@ -6103,49 +6680,49 @@ function sourceVersion() {
|
|
|
6103
6680
|
}
|
|
6104
6681
|
}
|
|
6105
6682
|
function globalSrcDir() {
|
|
6106
|
-
return
|
|
6683
|
+
return path41.join(packageRoot(), "library", "global");
|
|
6107
6684
|
}
|
|
6108
6685
|
function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
6109
|
-
const skillDir =
|
|
6686
|
+
const skillDir = path41.join(skillsDir, skillName);
|
|
6110
6687
|
const entries = [];
|
|
6111
6688
|
for (const rel of fs32.readdirSync(skillDir, { recursive: true })) {
|
|
6112
6689
|
if (rel === "SKILL.md") continue;
|
|
6113
|
-
const abs =
|
|
6690
|
+
const abs = path41.join(skillDir, rel);
|
|
6114
6691
|
if (!fs32.statSync(abs).isFile()) continue;
|
|
6115
|
-
const posixRel = rel.split(
|
|
6692
|
+
const posixRel = rel.split(path41.sep).join("/");
|
|
6116
6693
|
entries.push({
|
|
6117
6694
|
stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
|
|
6118
|
-
srcRelPath:
|
|
6119
|
-
destPath:
|
|
6695
|
+
srcRelPath: path41.join("library", "global", "skills", skillName, rel),
|
|
6696
|
+
destPath: path41.join(claudeDir, "skills", skillName, rel)
|
|
6120
6697
|
});
|
|
6121
6698
|
}
|
|
6122
6699
|
return entries;
|
|
6123
6700
|
}
|
|
6124
6701
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
6125
6702
|
const entries = [];
|
|
6126
|
-
const skillsDir =
|
|
6703
|
+
const skillsDir = path41.join(srcDir, "skills");
|
|
6127
6704
|
if (fs32.pathExistsSync(skillsDir)) {
|
|
6128
6705
|
for (const skillName of fs32.readdirSync(skillsDir)) {
|
|
6129
|
-
const skillFile =
|
|
6706
|
+
const skillFile = path41.join(skillsDir, skillName, "SKILL.md");
|
|
6130
6707
|
if (fs32.pathExistsSync(skillFile)) {
|
|
6131
6708
|
entries.push({
|
|
6132
6709
|
stableId: `skill.${skillName}`,
|
|
6133
|
-
srcRelPath:
|
|
6134
|
-
destPath:
|
|
6710
|
+
srcRelPath: path41.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
6711
|
+
destPath: path41.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
6135
6712
|
});
|
|
6136
6713
|
entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
|
|
6137
6714
|
}
|
|
6138
6715
|
}
|
|
6139
6716
|
}
|
|
6140
|
-
const commandsDir =
|
|
6717
|
+
const commandsDir = path41.join(srcDir, "commands");
|
|
6141
6718
|
if (fs32.pathExistsSync(commandsDir)) {
|
|
6142
6719
|
for (const fileName of fs32.readdirSync(commandsDir)) {
|
|
6143
6720
|
if (!fileName.endsWith(".md")) continue;
|
|
6144
6721
|
const commandName = fileName.slice(0, -".md".length);
|
|
6145
6722
|
entries.push({
|
|
6146
6723
|
stableId: `command.${commandName}`,
|
|
6147
|
-
srcRelPath:
|
|
6148
|
-
destPath:
|
|
6724
|
+
srcRelPath: path41.join("library", "global", "commands", fileName),
|
|
6725
|
+
destPath: path41.join(claudeDir, "commands", fileName)
|
|
6149
6726
|
});
|
|
6150
6727
|
}
|
|
6151
6728
|
}
|
|
@@ -6171,7 +6748,7 @@ async function applyInstall(options = {}) {
|
|
|
6171
6748
|
};
|
|
6172
6749
|
const manifestFiles = [];
|
|
6173
6750
|
for (const entry of sourceFiles) {
|
|
6174
|
-
const srcPath =
|
|
6751
|
+
const srcPath = path41.join(packageRoot(), entry.srcRelPath);
|
|
6175
6752
|
const rawContent = await readText(srcPath);
|
|
6176
6753
|
if (rawContent === void 0) {
|
|
6177
6754
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -6232,7 +6809,7 @@ async function applyInstall(options = {}) {
|
|
|
6232
6809
|
}
|
|
6233
6810
|
let mergedSettings;
|
|
6234
6811
|
if (!check) {
|
|
6235
|
-
const fragmentPath =
|
|
6812
|
+
const fragmentPath = path41.join(srcDir, "settings-fragments", "hooks.json");
|
|
6236
6813
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
6237
6814
|
const settings = await readSettings();
|
|
6238
6815
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -6262,9 +6839,12 @@ async function applyInstall(options = {}) {
|
|
|
6262
6839
|
}
|
|
6263
6840
|
if (!dryRun && !check && mergedSettings) {
|
|
6264
6841
|
await writeSettings(mergedSettings);
|
|
6265
|
-
const manifest = buildManifest(
|
|
6266
|
-
|
|
6267
|
-
|
|
6842
|
+
const manifest = buildManifest(
|
|
6843
|
+
source,
|
|
6844
|
+
manifestFiles,
|
|
6845
|
+
[.../* @__PURE__ */ new Set([...existingManifest?.hooks ?? [], ...result.hookIds])],
|
|
6846
|
+
existingManifest?.plugins
|
|
6847
|
+
);
|
|
6268
6848
|
await writeManifest(manifest);
|
|
6269
6849
|
}
|
|
6270
6850
|
return result;
|
|
@@ -6321,7 +6901,7 @@ function summarizeLockDiff(before, after) {
|
|
|
6321
6901
|
|
|
6322
6902
|
// src/update/lockfile.ts
|
|
6323
6903
|
import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
|
|
6324
|
-
import
|
|
6904
|
+
import path42 from "node:path";
|
|
6325
6905
|
async function readLockSummary(root) {
|
|
6326
6906
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
6327
6907
|
const catalogRef = lock.find((item) => item.catalogRef)?.catalogRef ?? null;
|
|
@@ -6359,7 +6939,7 @@ async function applyLock(root) {
|
|
|
6359
6939
|
try {
|
|
6360
6940
|
const backupDir = hausPath(root, "backups");
|
|
6361
6941
|
await mkdir(backupDir, { recursive: true });
|
|
6362
|
-
await copyFile(lockPath,
|
|
6942
|
+
await copyFile(lockPath, path42.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
6363
6943
|
} catch {
|
|
6364
6944
|
}
|
|
6365
6945
|
const enriched = await Promise.all(
|
|
@@ -6381,7 +6961,7 @@ function diffLock(before, after) {
|
|
|
6381
6961
|
}
|
|
6382
6962
|
async function hasLocalOverrides(root) {
|
|
6383
6963
|
try {
|
|
6384
|
-
await readFile2(
|
|
6964
|
+
await readFile2(path42.join(root, ".claude", "settings.json"), "utf8");
|
|
6385
6965
|
return true;
|
|
6386
6966
|
} catch {
|
|
6387
6967
|
return false;
|
|
@@ -6397,15 +6977,26 @@ async function runUpdate(options) {
|
|
|
6397
6977
|
return;
|
|
6398
6978
|
}
|
|
6399
6979
|
if (options.check) {
|
|
6400
|
-
const pkgJson2 = await readJson(
|
|
6980
|
+
const pkgJson2 = await readJson(path43.join(packageRoot(), "package.json"));
|
|
6401
6981
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
6402
|
-
const [
|
|
6982
|
+
const [
|
|
6983
|
+
status,
|
|
6984
|
+
npmVersion,
|
|
6985
|
+
latestCatalogTag,
|
|
6986
|
+
globalInstallDrift,
|
|
6987
|
+
catalogItems,
|
|
6988
|
+
lockItems,
|
|
6989
|
+
globalPluginDrift
|
|
6990
|
+
] = await Promise.all([
|
|
6403
6991
|
options.fast ? fastLockStatus(root) : checkLock(root),
|
|
6404
6992
|
fetchNpmVersionStatus(currentVersion2),
|
|
6405
6993
|
fetchLatestCatalogTag(),
|
|
6406
6994
|
options.fast ? Promise.resolve(null) : detectGlobalInstallDrift(),
|
|
6407
6995
|
loadCatalog(root),
|
|
6408
|
-
readJson(hausPath(root, "haus.lock.json"))
|
|
6996
|
+
readJson(hausPath(root, "haus.lock.json")),
|
|
6997
|
+
// Same tier split as globalInstallDrift: --fast is the cheap probe the
|
|
6998
|
+
// SessionStart hook uses and must not shell out to the `claude` CLI.
|
|
6999
|
+
options.fast ? Promise.resolve(null) : detectGlobalPluginDrift(root)
|
|
6409
7000
|
]);
|
|
6410
7001
|
const formerIdMigrations = findFormerIdMigrations(lockItems ?? [], catalogItems);
|
|
6411
7002
|
const installedRef = options.fast ? status.catalogRef : status.catalogRef ?? "main";
|
|
@@ -6420,6 +7011,7 @@ async function runUpdate(options) {
|
|
|
6420
7011
|
catalogRefBehind,
|
|
6421
7012
|
formerIdMigrations,
|
|
6422
7013
|
globalInstallDrift,
|
|
7014
|
+
globalPluginDrift,
|
|
6423
7015
|
localOverrides: await hasLocalOverrides(root),
|
|
6424
7016
|
summary: diffGeneratedFiles(),
|
|
6425
7017
|
npmVersion
|
|
@@ -6429,12 +7021,12 @@ async function runUpdate(options) {
|
|
|
6429
7021
|
)
|
|
6430
7022
|
);
|
|
6431
7023
|
const hasHashDrift = !options.fast && status.count > 0 && "ok" in status && !status.ok;
|
|
6432
|
-
if (hasHashDrift || formerIdMigrations.length > 0 || globalInstallDrift === true) {
|
|
7024
|
+
if (hasHashDrift || formerIdMigrations.length > 0 || globalInstallDrift === true || globalPluginDrift === true) {
|
|
6433
7025
|
process.exitCode = 1;
|
|
6434
7026
|
}
|
|
6435
7027
|
return;
|
|
6436
7028
|
}
|
|
6437
|
-
const pkgJson = await readJson(
|
|
7029
|
+
const pkgJson = await readJson(path43.join(packageRoot(), "package.json"));
|
|
6438
7030
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
6439
7031
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
6440
7032
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -6467,6 +7059,7 @@ async function runUpdate(options) {
|
|
|
6467
7059
|
warn(formatGithubRateLimitMessage(sync.rateLimit));
|
|
6468
7060
|
}
|
|
6469
7061
|
await refreshGlobalInstall();
|
|
7062
|
+
await ensureCatalogPinnedPlugins(root);
|
|
6470
7063
|
await refreshProjectFiles(root);
|
|
6471
7064
|
const { after } = await applyLock(root);
|
|
6472
7065
|
log(diffLock(lockBefore, after));
|
|
@@ -6477,7 +7070,7 @@ async function runFromHookCheck(root) {
|
|
|
6477
7070
|
try {
|
|
6478
7071
|
const summary = await readLockSummary(root);
|
|
6479
7072
|
if (summary.count === 0) return;
|
|
6480
|
-
const pkgJson = await readJson(
|
|
7073
|
+
const pkgJson = await readJson(path43.join(packageRoot(), "package.json"));
|
|
6481
7074
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
6482
7075
|
const [npmVersion, latestCatalogTag] = await Promise.all([
|
|
6483
7076
|
fetchNpmVersionStatus(currentVersion),
|
|
@@ -6535,6 +7128,26 @@ async function refreshGlobalInstall() {
|
|
|
6535
7128
|
warn(`Could not refresh ~/.claude/: ${err instanceof Error ? err.message : String(err)}`);
|
|
6536
7129
|
}
|
|
6537
7130
|
}
|
|
7131
|
+
async function ensureCatalogPinnedPlugins(root) {
|
|
7132
|
+
try {
|
|
7133
|
+
const pins = await loadCatalogPluginPins(root);
|
|
7134
|
+
const ensured = await ensureGlobalPlugins(pins);
|
|
7135
|
+
await recordManifestPlugins(
|
|
7136
|
+
pins.filter((pin) => ensured.ensured.includes(pin.id)).map((pin) => ({ ...pin, installedAt: (/* @__PURE__ */ new Date()).toISOString() }))
|
|
7137
|
+
);
|
|
7138
|
+
} catch (err) {
|
|
7139
|
+
warn(`Could not update global plugins: ${err instanceof Error ? err.message : String(err)}`);
|
|
7140
|
+
}
|
|
7141
|
+
}
|
|
7142
|
+
async function detectGlobalPluginDrift(root) {
|
|
7143
|
+
try {
|
|
7144
|
+
const pins = await loadCatalogPluginPins(root);
|
|
7145
|
+
const state = await probePluginEnv();
|
|
7146
|
+
return pluginDriftStatus(planPluginActions(pins, state), { matchRef: true });
|
|
7147
|
+
} catch {
|
|
7148
|
+
return null;
|
|
7149
|
+
}
|
|
7150
|
+
}
|
|
6538
7151
|
async function detectGlobalInstallDrift() {
|
|
6539
7152
|
try {
|
|
6540
7153
|
const result = await applyInstall({ check: true });
|
|
@@ -6628,7 +7241,7 @@ async function runCiGate(options = {}) {
|
|
|
6628
7241
|
init_exec();
|
|
6629
7242
|
import { existsSync as existsSync2 } from "node:fs";
|
|
6630
7243
|
import { readdir } from "node:fs/promises";
|
|
6631
|
-
import
|
|
7244
|
+
import path44 from "node:path";
|
|
6632
7245
|
var GIT_LOCATION_VARS = [
|
|
6633
7246
|
"GIT_DIR",
|
|
6634
7247
|
"GIT_WORK_TREE",
|
|
@@ -6688,11 +7301,11 @@ async function runClone(url, opts = {}) {
|
|
|
6688
7301
|
process.exitCode = 1;
|
|
6689
7302
|
return;
|
|
6690
7303
|
}
|
|
6691
|
-
const target =
|
|
7304
|
+
const target = path44.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
6692
7305
|
if (existsSync2(target) && !await isEmptyDirectory(target)) {
|
|
6693
7306
|
const check = await checkExistingTarget(target, url);
|
|
6694
7307
|
if (check.kind === "matches") {
|
|
6695
|
-
log(`\u2022 ${
|
|
7308
|
+
log(`\u2022 ${path44.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
|
|
6696
7309
|
return;
|
|
6697
7310
|
}
|
|
6698
7311
|
const message = check.kind === "not-a-repo" ? `${target} already exists and is not a git repository \u2014 refusing to clone ${url} there. Remove or rename it, or pass a different [dir].` : `${target} already exists as a different repository (origin: ${check.origin ?? "(no origin remote)"}) \u2014 refusing to clone ${url} there. Remove or rename it, or pass a different [dir].`;
|
|
@@ -6801,7 +7414,7 @@ async function runDecisionsGuard(options = {}) {
|
|
|
6801
7414
|
}
|
|
6802
7415
|
|
|
6803
7416
|
// src/commands/decisions.ts
|
|
6804
|
-
import
|
|
7417
|
+
import path45 from "node:path";
|
|
6805
7418
|
|
|
6806
7419
|
// src/decisions/next-number.ts
|
|
6807
7420
|
async function nextDecisionNumber(root) {
|
|
@@ -6911,7 +7524,7 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
6911
7524
|
return;
|
|
6912
7525
|
}
|
|
6913
7526
|
if (sub === "validate") {
|
|
6914
|
-
const target = arg ?
|
|
7527
|
+
const target = arg ? path45.resolve(root, arg) : void 0;
|
|
6915
7528
|
if (!target) {
|
|
6916
7529
|
error("usage: haus decisions validate <path-to-decision.md>");
|
|
6917
7530
|
process.exitCode = 1;
|
|
@@ -7180,7 +7793,7 @@ async function runGuard(kind, _options) {
|
|
|
7180
7793
|
}
|
|
7181
7794
|
|
|
7182
7795
|
// src/commands/init.ts
|
|
7183
|
-
import
|
|
7796
|
+
import path47 from "node:path";
|
|
7184
7797
|
import fs33 from "fs-extra";
|
|
7185
7798
|
|
|
7186
7799
|
// src/recommender/git-signal.ts
|
|
@@ -7223,6 +7836,7 @@ async function readChangedFiles(root) {
|
|
|
7223
7836
|
|
|
7224
7837
|
// src/recommender/recommend.ts
|
|
7225
7838
|
async function recommend(root, context, opts = {}) {
|
|
7839
|
+
const activePluginIds = opts.activePluginIds ?? [];
|
|
7226
7840
|
const items = await loadCatalog(root);
|
|
7227
7841
|
const sources = buildSourcesReport(items);
|
|
7228
7842
|
const deep = await readJson(hausPath(root, "deep-context.json")) ?? {};
|
|
@@ -7280,6 +7894,15 @@ async function recommend(root, context, opts = {}) {
|
|
|
7280
7894
|
"Catalog item is deprecated",
|
|
7281
7895
|
"reviewStatus:deprecated"
|
|
7282
7896
|
);
|
|
7897
|
+
if (isItemSupersededByPlugin(item, activePluginIds)) {
|
|
7898
|
+
add(
|
|
7899
|
+
"superseded-by-global-plugin",
|
|
7900
|
+
false,
|
|
7901
|
+
"superseded-by-global-plugin",
|
|
7902
|
+
"Provided by an installed global Claude Code plugin",
|
|
7903
|
+
`plugin:${activePluginIds.join(",")}`
|
|
7904
|
+
);
|
|
7905
|
+
}
|
|
7283
7906
|
if (normSource === "curated") {
|
|
7284
7907
|
const rs = item.reviewStatus;
|
|
7285
7908
|
add(
|
|
@@ -7619,7 +8242,7 @@ function buildOptInEligible(items, skipped) {
|
|
|
7619
8242
|
}
|
|
7620
8243
|
|
|
7621
8244
|
// src/commands/workspace/discover.ts
|
|
7622
|
-
import
|
|
8245
|
+
import path46 from "node:path";
|
|
7623
8246
|
import fg5 from "fast-glob";
|
|
7624
8247
|
import YAML2 from "yaml";
|
|
7625
8248
|
var DEFAULT_MAX_DEPTH = 3;
|
|
@@ -7660,10 +8283,10 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
7660
8283
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
7661
8284
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
7662
8285
|
for (const match of gitMatches) {
|
|
7663
|
-
gitDirs.add(
|
|
8286
|
+
gitDirs.add(path46.posix.dirname(match));
|
|
7664
8287
|
}
|
|
7665
8288
|
for (const match of manifestMatches) {
|
|
7666
|
-
manifestDirs.add(
|
|
8289
|
+
manifestDirs.add(path46.posix.dirname(match));
|
|
7667
8290
|
}
|
|
7668
8291
|
const repoRoots = [...gitDirs];
|
|
7669
8292
|
const manifestSorted = [...manifestDirs].sort(
|
|
@@ -7680,9 +8303,9 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
7680
8303
|
async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
7681
8304
|
const repoRoots = (await findRepoRoots(workspaceRoot, maxDepth)).filter((dir) => dir !== ".");
|
|
7682
8305
|
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
7683
|
-
const absDir =
|
|
7684
|
-
const pkg = await readJson(
|
|
7685
|
-
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name :
|
|
8306
|
+
const absDir = path46.resolve(workspaceRoot, relDir);
|
|
8307
|
+
const pkg = await readJson(path46.join(absDir, "package.json"));
|
|
8308
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path46.basename(absDir);
|
|
7686
8309
|
let role = "auto";
|
|
7687
8310
|
try {
|
|
7688
8311
|
const scan = await scanProject(absDir);
|
|
@@ -7725,7 +8348,7 @@ function renderWorkspaceYaml(config) {
|
|
|
7725
8348
|
});
|
|
7726
8349
|
}
|
|
7727
8350
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
7728
|
-
const yamlPath =
|
|
8351
|
+
const yamlPath = path46.join(workspaceRoot, "haus.workspace.yaml");
|
|
7729
8352
|
const existingText = await readText(yamlPath);
|
|
7730
8353
|
const existing = parseWorkspaceConfig(existingText);
|
|
7731
8354
|
if (existingText && !existing) {
|
|
@@ -7792,7 +8415,11 @@ async function runSetupCore(root, opts) {
|
|
|
7792
8415
|
const siblingWarning = await hasMultipleSiblingRepos(root) ? [SIBLING_REPO_WARNING] : [];
|
|
7793
8416
|
const context = await readContextOrScan(root);
|
|
7794
8417
|
const priorManualIncludes = await readPriorManualIncludes(root);
|
|
7795
|
-
const
|
|
8418
|
+
const activePluginIds = await activeGlobalPluginIds(root);
|
|
8419
|
+
const recommendation = await recommend(root, context, {
|
|
8420
|
+
include: priorManualIncludes,
|
|
8421
|
+
activePluginIds
|
|
8422
|
+
});
|
|
7796
8423
|
if (recommendation.recommended.length === 0 && !force) {
|
|
7797
8424
|
const warningLines2 = [
|
|
7798
8425
|
.../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? [], ...siblingWarning])
|
|
@@ -7806,7 +8433,10 @@ async function runSetupCore(root, opts) {
|
|
|
7806
8433
|
say(`Repo: ${context.repoName}`);
|
|
7807
8434
|
for (const warning of warningLines2) say(`- WARN: ${warning}`);
|
|
7808
8435
|
}
|
|
7809
|
-
const
|
|
8436
|
+
const supersededCount = recommendation.skipped.filter(
|
|
8437
|
+
(s) => s.skipReasons?.some((r) => r.code === "superseded-by-global-plugin")
|
|
8438
|
+
).length;
|
|
8439
|
+
const message = supersededCount > 0 ? `No catalog items matched this project after ${supersededCount} item(s) were gated as already provided by an installed global plugin \u2014 refusing to write recommendation.json/haus.lock.json. Those skills are available globally; re-run with \`haus setup-project --force\` to write anyway.` : "No catalog items matched this project at all (zero signal) \u2014 refusing to write recommendation.json/haus.lock.json. Re-run with `haus setup-project --force` to write anyway.";
|
|
7810
8440
|
warn(message);
|
|
7811
8441
|
process.exitCode = 1;
|
|
7812
8442
|
return {
|
|
@@ -7872,7 +8502,10 @@ async function runSetupCore(root, opts) {
|
|
|
7872
8502
|
warn(formatGithubRateLimitMessage(sync.rateLimit));
|
|
7873
8503
|
}
|
|
7874
8504
|
}
|
|
7875
|
-
const files = await writeClaudeFiles(root, dryRun ?? false, void 0, {
|
|
8505
|
+
const files = await writeClaudeFiles(root, dryRun ?? false, void 0, {
|
|
8506
|
+
quiet: opts.quiet,
|
|
8507
|
+
activePluginIds
|
|
8508
|
+
});
|
|
7876
8509
|
say("Applied files:");
|
|
7877
8510
|
files.forEach((f) => say(`- ${displayPath(root, f)}`));
|
|
7878
8511
|
const hooksAfter = await verifyProjectSettingsHooksContract(root);
|
|
@@ -7903,7 +8536,7 @@ async function runSetupProject(options) {
|
|
|
7903
8536
|
// src/commands/init.ts
|
|
7904
8537
|
async function runInit(options) {
|
|
7905
8538
|
const root = process.cwd();
|
|
7906
|
-
const hausDir =
|
|
8539
|
+
const hausDir = path47.join(root, ".haus-workflow");
|
|
7907
8540
|
const alreadyInit = await fs33.pathExists(hausDir);
|
|
7908
8541
|
if (alreadyInit) {
|
|
7909
8542
|
log("Haus AI already initialized in this project.");
|
|
@@ -7968,7 +8601,19 @@ function printCompanionToolSuggestions(deps = {}) {
|
|
|
7968
8601
|
}
|
|
7969
8602
|
|
|
7970
8603
|
// src/commands/install.ts
|
|
7971
|
-
|
|
8604
|
+
function toManifestPlugin(pin) {
|
|
8605
|
+
return {
|
|
8606
|
+
id: pin.id,
|
|
8607
|
+
plugin: pin.plugin,
|
|
8608
|
+
marketplace: pin.marketplace,
|
|
8609
|
+
url: pin.url,
|
|
8610
|
+
ref: pin.ref,
|
|
8611
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8612
|
+
};
|
|
8613
|
+
}
|
|
8614
|
+
async function runInstall(options, deps = {}) {
|
|
8615
|
+
const ensure = deps.ensureGlobalPlugins ?? ensureGlobalPlugins;
|
|
8616
|
+
const probe = deps.probePluginEnv ?? probePluginEnv;
|
|
7972
8617
|
try {
|
|
7973
8618
|
const result = await applyInstall({
|
|
7974
8619
|
dryRun: options.dryRun,
|
|
@@ -7976,9 +8621,23 @@ async function runInstall(options) {
|
|
|
7976
8621
|
check: options.check
|
|
7977
8622
|
});
|
|
7978
8623
|
if (!options.postinstall) printApplyResult(result, options.dryRun ?? false);
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
8624
|
+
const pins = [...DEFAULT_GLOBAL_PLUGINS];
|
|
8625
|
+
if (options.check) {
|
|
8626
|
+
const state = await probe();
|
|
8627
|
+
const pluginDrift = pluginDriftStatus(planPluginActions(pins, state), { matchRef: false });
|
|
8628
|
+
if (pluginDrift === true) {
|
|
8629
|
+
log("Global plugin drift: a haus-managed plugin is missing or disabled.");
|
|
8630
|
+
}
|
|
8631
|
+
if (result.drift || pluginDrift === true) process.exitCode = 1;
|
|
8632
|
+
return;
|
|
8633
|
+
}
|
|
8634
|
+
const ensured = await ensure(pins, { dryRun: options.dryRun });
|
|
8635
|
+
if (!options.dryRun) {
|
|
8636
|
+
await recordManifestPlugins(
|
|
8637
|
+
pins.filter((pin) => ensured.ensured.includes(pin.id)).map(toManifestPlugin)
|
|
8638
|
+
);
|
|
8639
|
+
}
|
|
8640
|
+
if (!options.dryRun) {
|
|
7982
8641
|
printCompanionToolSuggestions();
|
|
7983
8642
|
const total = result.created.length + result.updated.length;
|
|
7984
8643
|
if (options.postinstall) {
|
|
@@ -7990,6 +8649,9 @@ async function runInstall(options) {
|
|
|
7990
8649
|
parts.length ? ` \u2022 ${parts.join(", ")} in ~/.claude (skills, slash commands)` : " \u2022 already up to date \u2014 no files changed"
|
|
7991
8650
|
);
|
|
7992
8651
|
log(` \u2022 ensured hooks + security rules are present in ~/.claude/settings.json`);
|
|
8652
|
+
if (ensured.ensured.length) {
|
|
8653
|
+
log(` \u2022 global plugin(s) pinned: ${ensured.ensured.join(", ")}`);
|
|
8654
|
+
}
|
|
7993
8655
|
log("Undo any time with: haus uninstall");
|
|
7994
8656
|
log("Disable this on install: HAUS_NO_POSTINSTALL=1");
|
|
7995
8657
|
} else {
|
|
@@ -8009,7 +8671,8 @@ async function runRecommend(options) {
|
|
|
8009
8671
|
const root = process.cwd();
|
|
8010
8672
|
const context = await readContextOrScan(root);
|
|
8011
8673
|
const include = parseIdList(options.include);
|
|
8012
|
-
const
|
|
8674
|
+
const activePluginIds = await activeGlobalPluginIds(root);
|
|
8675
|
+
const result = await recommend(root, context, { include, activePluginIds });
|
|
8013
8676
|
await writeJson(hausPath(root, "recommendation.json"), result);
|
|
8014
8677
|
if (options.json) {
|
|
8015
8678
|
log(JSON.stringify(result, null, 2));
|
|
@@ -8030,7 +8693,14 @@ async function runRecommend(options) {
|
|
|
8030
8693
|
async function runRefresh() {
|
|
8031
8694
|
const root = process.cwd();
|
|
8032
8695
|
const context = await scanProject(root);
|
|
8033
|
-
|
|
8696
|
+
try {
|
|
8697
|
+
await ensureGlobalPlugins(await loadCatalogPluginPins(root));
|
|
8698
|
+
} catch (err) {
|
|
8699
|
+
warn(`Could not refresh global plugins: ${err instanceof Error ? err.message : String(err)}`);
|
|
8700
|
+
}
|
|
8701
|
+
const recommendation = await recommend(root, context, {
|
|
8702
|
+
activePluginIds: await activeGlobalPluginIds(root)
|
|
8703
|
+
});
|
|
8034
8704
|
await writeJson(hausPath(root, "recommendation.json"), recommendation);
|
|
8035
8705
|
log("Haus refresh complete");
|
|
8036
8706
|
log(`Roles: ${context.repoRoles.join(", ") || "unknown"}`);
|
|
@@ -8039,7 +8709,7 @@ async function runRefresh() {
|
|
|
8039
8709
|
}
|
|
8040
8710
|
|
|
8041
8711
|
// src/install/scaffold.ts
|
|
8042
|
-
import
|
|
8712
|
+
import path48 from "node:path";
|
|
8043
8713
|
import fs34 from "fs-extra";
|
|
8044
8714
|
async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
8045
8715
|
const scaffolded = [];
|
|
@@ -8056,9 +8726,9 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8056
8726
|
const realRoot = await fs34.realpath(catalogRoot).catch(() => null);
|
|
8057
8727
|
for (const item of items) {
|
|
8058
8728
|
if (item.type !== "config") continue;
|
|
8059
|
-
const sourcePath =
|
|
8060
|
-
const rel =
|
|
8061
|
-
const escapes = rel === "" ||
|
|
8729
|
+
const sourcePath = path48.resolve(catalogRoot, item.path);
|
|
8730
|
+
const rel = path48.relative(catalogRoot, sourcePath);
|
|
8731
|
+
const escapes = rel === "" || path48.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
|
|
8062
8732
|
if (escapes) {
|
|
8063
8733
|
warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
|
|
8064
8734
|
continue;
|
|
@@ -8068,8 +8738,8 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8068
8738
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
8069
8739
|
continue;
|
|
8070
8740
|
}
|
|
8071
|
-
const realRel =
|
|
8072
|
-
if (realRel !== "" && (
|
|
8741
|
+
const realRel = path48.relative(realRoot, realSource);
|
|
8742
|
+
if (realRel !== "" && (path48.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
|
|
8073
8743
|
warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
|
|
8074
8744
|
continue;
|
|
8075
8745
|
}
|
|
@@ -8083,14 +8753,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8083
8753
|
continue;
|
|
8084
8754
|
}
|
|
8085
8755
|
if (stat.isFile()) {
|
|
8086
|
-
const filename =
|
|
8087
|
-
const dest =
|
|
8756
|
+
const filename = path48.basename(sourcePath);
|
|
8757
|
+
const dest = path48.join(projectRoot, filename);
|
|
8088
8758
|
record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
|
|
8089
8759
|
} else if (stat.isDirectory()) {
|
|
8090
8760
|
const entries = await fs34.readdir(sourcePath);
|
|
8091
8761
|
for (const entry of entries) {
|
|
8092
|
-
const src =
|
|
8093
|
-
const dest =
|
|
8762
|
+
const src = path48.join(sourcePath, entry);
|
|
8763
|
+
const dest = path48.join(projectRoot, entry);
|
|
8094
8764
|
record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
|
|
8095
8765
|
}
|
|
8096
8766
|
}
|
|
@@ -8098,14 +8768,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8098
8768
|
return { scaffolded, skipped, skippedExisting };
|
|
8099
8769
|
}
|
|
8100
8770
|
async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
8101
|
-
const destDisplay =
|
|
8771
|
+
const destDisplay = path48.relative(projectRoot, dest) || path48.basename(dest);
|
|
8102
8772
|
const srcStat = await fs34.lstat(src).catch(() => null);
|
|
8103
8773
|
if (!srcStat) {
|
|
8104
|
-
warn(`Skipping ${
|
|
8774
|
+
warn(`Skipping ${path48.basename(src)}: source not found`);
|
|
8105
8775
|
return "skipped-refused";
|
|
8106
8776
|
}
|
|
8107
8777
|
if (srcStat.isSymbolicLink()) {
|
|
8108
|
-
warn(`Skipping ${
|
|
8778
|
+
warn(`Skipping ${path48.basename(src)}: source is a symlink`);
|
|
8109
8779
|
return "skipped-refused";
|
|
8110
8780
|
}
|
|
8111
8781
|
const exists = await fs34.pathExists(dest);
|
|
@@ -8114,13 +8784,13 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
|
8114
8784
|
return "skipped-exists";
|
|
8115
8785
|
}
|
|
8116
8786
|
if (opts.dryRun) {
|
|
8117
|
-
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${
|
|
8787
|
+
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path48.basename(dest)} (${itemId})`);
|
|
8118
8788
|
return "scaffolded";
|
|
8119
8789
|
}
|
|
8120
8790
|
if (exists && opts.force) {
|
|
8121
8791
|
await fs34.remove(dest);
|
|
8122
8792
|
}
|
|
8123
|
-
await fs34.ensureDir(
|
|
8793
|
+
await fs34.ensureDir(path48.dirname(dest));
|
|
8124
8794
|
await fs34.copy(src, dest, {
|
|
8125
8795
|
overwrite: true,
|
|
8126
8796
|
dereference: false,
|
|
@@ -8133,7 +8803,7 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
|
8133
8803
|
}
|
|
8134
8804
|
}
|
|
8135
8805
|
});
|
|
8136
|
-
log(`${exists ? "Overwrote" : "Created"} ${
|
|
8806
|
+
log(`${exists ? "Overwrote" : "Created"} ${path48.basename(dest)} (${itemId})`);
|
|
8137
8807
|
return "scaffolded";
|
|
8138
8808
|
}
|
|
8139
8809
|
|
|
@@ -8198,7 +8868,7 @@ async function runScan(options) {
|
|
|
8198
8868
|
}
|
|
8199
8869
|
|
|
8200
8870
|
// src/commands/undo.ts
|
|
8201
|
-
import
|
|
8871
|
+
import path49 from "node:path";
|
|
8202
8872
|
import fs35 from "fs-extra";
|
|
8203
8873
|
|
|
8204
8874
|
// src/claude/managed-paths.ts
|
|
@@ -8226,18 +8896,18 @@ async function collectLockTrackedPaths(root) {
|
|
|
8226
8896
|
const relPaths = row.paths ?? [];
|
|
8227
8897
|
const existingRel = [];
|
|
8228
8898
|
for (const rel of relPaths) {
|
|
8229
|
-
if (await fs35.pathExists(
|
|
8899
|
+
if (await fs35.pathExists(path49.resolve(root, rel))) existingRel.push(rel);
|
|
8230
8900
|
}
|
|
8231
8901
|
if (existingRel.length === 0) continue;
|
|
8232
8902
|
if (row.hash === void 0) {
|
|
8233
|
-
removable.push(...existingRel.map((rel) =>
|
|
8903
|
+
removable.push(...existingRel.map((rel) => path49.resolve(root, rel)));
|
|
8234
8904
|
continue;
|
|
8235
8905
|
}
|
|
8236
8906
|
const currentHash = await hashInstalledPaths(root, existingRel);
|
|
8237
8907
|
if (currentHash === row.hash) {
|
|
8238
|
-
removable.push(...existingRel.map((rel) =>
|
|
8908
|
+
removable.push(...existingRel.map((rel) => path49.resolve(root, rel)));
|
|
8239
8909
|
} else {
|
|
8240
|
-
preserved.push(...existingRel.map((rel) =>
|
|
8910
|
+
preserved.push(...existingRel.map((rel) => path49.resolve(root, rel)));
|
|
8241
8911
|
log(`Preserving locally-modified ${row.id ?? "(unknown item)"}: ${existingRel.join(", ")}`);
|
|
8242
8912
|
}
|
|
8243
8913
|
}
|
|
@@ -8250,7 +8920,7 @@ async function settingsHasHausContent(root) {
|
|
|
8250
8920
|
return settings._haus != null;
|
|
8251
8921
|
}
|
|
8252
8922
|
async function claudeMdHasHausBlock(root) {
|
|
8253
|
-
const filePath =
|
|
8923
|
+
const filePath = path49.join(root, "CLAUDE.md");
|
|
8254
8924
|
if (!await fs35.pathExists(filePath)) return false;
|
|
8255
8925
|
const text = await fs35.readFile(filePath, "utf8");
|
|
8256
8926
|
return text.includes(BLOCK_BEGIN);
|
|
@@ -8263,15 +8933,15 @@ async function stripProjectSettings(root) {
|
|
|
8263
8933
|
const hasContent = Object.keys(settings).length > 0;
|
|
8264
8934
|
if (hasContent) {
|
|
8265
8935
|
await writeProjectSettings(root, settings);
|
|
8266
|
-
log(`Stripped haus rules from ${
|
|
8936
|
+
log(`Stripped haus rules from ${path49.relative(root, settingsPath)} (user settings preserved).`);
|
|
8267
8937
|
return true;
|
|
8268
8938
|
}
|
|
8269
8939
|
await fs35.remove(settingsPath);
|
|
8270
|
-
log(`Removed ${
|
|
8940
|
+
log(`Removed ${path49.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
8271
8941
|
return true;
|
|
8272
8942
|
}
|
|
8273
8943
|
async function stripRootClaudeMd(root) {
|
|
8274
|
-
const filePath =
|
|
8944
|
+
const filePath = path49.join(root, "CLAUDE.md");
|
|
8275
8945
|
if (!await fs35.pathExists(filePath)) return false;
|
|
8276
8946
|
const prev = await fs35.readFile(filePath, "utf8");
|
|
8277
8947
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
@@ -8299,12 +8969,12 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
|
8299
8969
|
const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
|
|
8300
8970
|
for (const abs of managedAbsPaths) {
|
|
8301
8971
|
if (!await fs35.pathExists(abs)) continue;
|
|
8302
|
-
const rel =
|
|
8303
|
-
const backupPath =
|
|
8304
|
-
await fs35.ensureDir(
|
|
8972
|
+
const rel = path49.relative(root, abs);
|
|
8973
|
+
const backupPath = path49.join(backupRoot, rel);
|
|
8974
|
+
await fs35.ensureDir(path49.dirname(backupPath));
|
|
8305
8975
|
await fs35.copy(abs, backupPath);
|
|
8306
8976
|
}
|
|
8307
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
8977
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path49.relative(root, backupRoot)}.`);
|
|
8308
8978
|
}
|
|
8309
8979
|
async function runUndo(options) {
|
|
8310
8980
|
const root = options.root ?? process.cwd();
|
|
@@ -8317,7 +8987,7 @@ async function runUndo(options) {
|
|
|
8317
8987
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
8318
8988
|
return;
|
|
8319
8989
|
}
|
|
8320
|
-
const relTargets = managed.map((p) =>
|
|
8990
|
+
const relTargets = managed.map((p) => path49.relative(root, p));
|
|
8321
8991
|
const summaryParts = [...relTargets];
|
|
8322
8992
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
8323
8993
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -8341,7 +9011,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
8341
9011
|
for (const abs of managed) {
|
|
8342
9012
|
if (!await fs35.pathExists(abs)) continue;
|
|
8343
9013
|
await fs35.remove(abs);
|
|
8344
|
-
log(`Removed ${
|
|
9014
|
+
log(`Removed ${path49.relative(root, abs)}`);
|
|
8345
9015
|
}
|
|
8346
9016
|
if (stripSettings) await stripProjectSettings(root);
|
|
8347
9017
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -8352,16 +9022,43 @@ User-owned .claude/ files will be preserved.`
|
|
|
8352
9022
|
|
|
8353
9023
|
// src/install/uninstall.ts
|
|
8354
9024
|
import crypto3 from "node:crypto";
|
|
8355
|
-
import
|
|
9025
|
+
import path50 from "node:path";
|
|
8356
9026
|
import fs36 from "fs-extra";
|
|
8357
9027
|
async function runUninstall(options = {}) {
|
|
8358
|
-
const { force = false } = options;
|
|
9028
|
+
const { force = false, removePlugins = removeGlobalPlugins } = options;
|
|
8359
9029
|
const manifest = await readManifest();
|
|
8360
|
-
const result = {
|
|
9030
|
+
const result = {
|
|
9031
|
+
deleted: [],
|
|
9032
|
+
skipped: [],
|
|
9033
|
+
hooksStripped: false,
|
|
9034
|
+
pluginsRemoved: []
|
|
9035
|
+
};
|
|
8361
9036
|
if (!manifest) {
|
|
8362
9037
|
warn("No install manifest found \u2014 nothing to uninstall.");
|
|
8363
9038
|
return result;
|
|
8364
9039
|
}
|
|
9040
|
+
const recordedPlugins = Array.isArray(manifest.plugins) ? manifest.plugins : [];
|
|
9041
|
+
const malformed = recordedPlugins.filter((entry) => !isValidPin(entry)).length;
|
|
9042
|
+
if (malformed > 0) {
|
|
9043
|
+
warn(
|
|
9044
|
+
`Ignoring ${malformed} malformed plugin record(s) in the install manifest \u2014 continuing with the rest of the uninstall.`
|
|
9045
|
+
);
|
|
9046
|
+
}
|
|
9047
|
+
const managedPlugins = recordedPlugins.filter(isValidPin).map((p) => ({
|
|
9048
|
+
id: p.id,
|
|
9049
|
+
plugin: p.plugin,
|
|
9050
|
+
marketplace: p.marketplace,
|
|
9051
|
+
url: p.url,
|
|
9052
|
+
ref: p.ref
|
|
9053
|
+
}));
|
|
9054
|
+
try {
|
|
9055
|
+
const pluginRemoval = await removePlugins(managedPlugins);
|
|
9056
|
+
result.pluginsRemoved = pluginRemoval.removed;
|
|
9057
|
+
} catch (err) {
|
|
9058
|
+
warn(
|
|
9059
|
+
`Could not remove global plugin(s): ${err instanceof Error ? err.message : String(err)} \u2014 continuing with the rest of the uninstall.`
|
|
9060
|
+
);
|
|
9061
|
+
}
|
|
8365
9062
|
for (const entry of manifest.files) {
|
|
8366
9063
|
const exists = fs36.pathExistsSync(entry.destPath);
|
|
8367
9064
|
if (!exists) continue;
|
|
@@ -8382,14 +9079,14 @@ async function runUninstall(options = {}) {
|
|
|
8382
9079
|
continue;
|
|
8383
9080
|
}
|
|
8384
9081
|
await fs36.remove(entry.destPath);
|
|
8385
|
-
await pruneEmptyDir(
|
|
9082
|
+
await pruneEmptyDir(path50.dirname(entry.destPath));
|
|
8386
9083
|
result.deleted.push(entry.destPath);
|
|
8387
9084
|
}
|
|
8388
9085
|
const settings = await readSettings();
|
|
8389
9086
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
8390
9087
|
await writeSettings(stripped);
|
|
8391
9088
|
result.hooksStripped = true;
|
|
8392
|
-
const hausDir =
|
|
9089
|
+
const hausDir = path50.join(globalClaudeDir(), "haus");
|
|
8393
9090
|
const manifestPath2 = hausManifestPath();
|
|
8394
9091
|
if (fs36.pathExistsSync(manifestPath2)) {
|
|
8395
9092
|
await fs36.remove(manifestPath2);
|
|
@@ -8415,6 +9112,10 @@ function printUninstallResult(result) {
|
|
|
8415
9112
|
if (result.hooksStripped) {
|
|
8416
9113
|
log("Haus hook entries removed from ~/.claude/settings.json");
|
|
8417
9114
|
}
|
|
9115
|
+
if (result.pluginsRemoved.length) {
|
|
9116
|
+
log(`Global plugin(s) removed: ${result.pluginsRemoved.join(", ")}`);
|
|
9117
|
+
log("Projects relying on those plugin skills get them back by reinstalling haus.");
|
|
9118
|
+
}
|
|
8418
9119
|
}
|
|
8419
9120
|
|
|
8420
9121
|
// src/commands/uninstall.ts
|
|
@@ -8443,11 +9144,11 @@ async function runUninstallCommand(options) {
|
|
|
8443
9144
|
}
|
|
8444
9145
|
|
|
8445
9146
|
// src/commands/validate-catalog.ts
|
|
8446
|
-
import
|
|
9147
|
+
import path52 from "node:path";
|
|
8447
9148
|
|
|
8448
9149
|
// src/catalog/validate-core.ts
|
|
8449
9150
|
import fs37 from "node:fs";
|
|
8450
|
-
import
|
|
9151
|
+
import path51 from "node:path";
|
|
8451
9152
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
8452
9153
|
var ITEM_ID_PATTERN = /^haus\./;
|
|
8453
9154
|
function auditForbiddenStacks(items) {
|
|
@@ -8596,7 +9297,7 @@ function readForAudit(absPath, label, failures) {
|
|
|
8596
9297
|
}
|
|
8597
9298
|
}
|
|
8598
9299
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
8599
|
-
const rel =
|
|
9300
|
+
const rel = path51.relative(manifestDir, absPath);
|
|
8600
9301
|
const failures = [];
|
|
8601
9302
|
const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
|
|
8602
9303
|
if (text === null) return failures;
|
|
@@ -8618,25 +9319,25 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
8618
9319
|
const failures = [];
|
|
8619
9320
|
for (const item of items) {
|
|
8620
9321
|
if (!item.path || !isSafeCatalogPath(item.path)) continue;
|
|
8621
|
-
const absPath =
|
|
9322
|
+
const absPath = path51.join(manifestDir, item.path);
|
|
8622
9323
|
if (item.type === "skill") {
|
|
8623
|
-
const skillMd =
|
|
9324
|
+
const skillMd = path51.join(absPath, "SKILL.md");
|
|
8624
9325
|
if (!fs37.existsSync(skillMd)) {
|
|
8625
|
-
failures.push(`${item.id}: missing ${
|
|
9326
|
+
failures.push(`${item.id}: missing ${path51.relative(manifestDir, skillMd)}`);
|
|
8626
9327
|
continue;
|
|
8627
9328
|
}
|
|
8628
9329
|
const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
|
|
8629
9330
|
if (text === null) continue;
|
|
8630
9331
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
8631
9332
|
failures.push(
|
|
8632
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
9333
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path51.relative(manifestDir, skillMd)}`)
|
|
8633
9334
|
);
|
|
8634
9335
|
} else if (item.type === "agent") {
|
|
8635
9336
|
if (!fs37.existsSync(absPath)) {
|
|
8636
9337
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
8637
9338
|
continue;
|
|
8638
9339
|
}
|
|
8639
|
-
const rel =
|
|
9340
|
+
const rel = path51.relative(manifestDir, absPath);
|
|
8640
9341
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
8641
9342
|
if (text === null) continue;
|
|
8642
9343
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -8652,7 +9353,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
8652
9353
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
8653
9354
|
continue;
|
|
8654
9355
|
}
|
|
8655
|
-
const rel =
|
|
9356
|
+
const rel = path51.relative(manifestDir, absPath);
|
|
8656
9357
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
8657
9358
|
if (text === null) continue;
|
|
8658
9359
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -8684,7 +9385,7 @@ function walkAllFiles(dir, fn, failures) {
|
|
|
8684
9385
|
}
|
|
8685
9386
|
for (const entry of entries) {
|
|
8686
9387
|
if (entry.name === ".DS_Store") continue;
|
|
8687
|
-
const full =
|
|
9388
|
+
const full = path51.join(dir, entry.name);
|
|
8688
9389
|
if (entry.isDirectory()) walkAllFiles(full, fn, failures);
|
|
8689
9390
|
else fn(full);
|
|
8690
9391
|
}
|
|
@@ -8702,12 +9403,12 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
8702
9403
|
const failures = [];
|
|
8703
9404
|
const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
|
|
8704
9405
|
for (const dir of CONTENT_ROOTS) {
|
|
8705
|
-
const abs =
|
|
9406
|
+
const abs = path51.join(manifestDir, dir);
|
|
8706
9407
|
if (!fs37.existsSync(abs)) continue;
|
|
8707
9408
|
walkAllFiles(
|
|
8708
9409
|
abs,
|
|
8709
9410
|
(file) => {
|
|
8710
|
-
const relPath =
|
|
9411
|
+
const relPath = path51.relative(manifestDir, file).replace(/\\/g, "/");
|
|
8711
9412
|
if (!isPathClaimed(relPath, claimedPaths)) {
|
|
8712
9413
|
failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
|
|
8713
9414
|
}
|
|
@@ -8728,7 +9429,7 @@ function walkMd(dir, fn, failures) {
|
|
|
8728
9429
|
return;
|
|
8729
9430
|
}
|
|
8730
9431
|
for (const entry of entries) {
|
|
8731
|
-
const full =
|
|
9432
|
+
const full = path51.join(dir, entry.name);
|
|
8732
9433
|
if (entry.isDirectory()) walkMd(full, fn, failures);
|
|
8733
9434
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
8734
9435
|
}
|
|
@@ -8737,13 +9438,13 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
8737
9438
|
const failures = [];
|
|
8738
9439
|
const pathSourceMap = buildItemPathSourceMap(items);
|
|
8739
9440
|
for (const dir of ["skills", "agents", "templates", "commands"]) {
|
|
8740
|
-
const abs =
|
|
9441
|
+
const abs = path51.join(manifestDir, dir);
|
|
8741
9442
|
if (!fs37.existsSync(abs)) continue;
|
|
8742
9443
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
8743
9444
|
walkMd(
|
|
8744
9445
|
abs,
|
|
8745
9446
|
(file) => {
|
|
8746
|
-
const rel =
|
|
9447
|
+
const rel = path51.relative(manifestDir, file);
|
|
8747
9448
|
const text = readForAudit(file, rel, failures);
|
|
8748
9449
|
if (text === null) return;
|
|
8749
9450
|
const norm = rel.replace(/\\/g, "/");
|
|
@@ -8788,8 +9489,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
8788
9489
|
process.exitCode = 1;
|
|
8789
9490
|
return;
|
|
8790
9491
|
}
|
|
8791
|
-
const abs =
|
|
8792
|
-
const manifestDir =
|
|
9492
|
+
const abs = path52.resolve(process.cwd(), manifestPath2);
|
|
9493
|
+
const manifestDir = path52.dirname(abs);
|
|
8793
9494
|
const data = await readJson(abs);
|
|
8794
9495
|
if (!data?.items) {
|
|
8795
9496
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -8806,7 +9507,7 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
8806
9507
|
}
|
|
8807
9508
|
|
|
8808
9509
|
// src/workspace/worktree/add.ts
|
|
8809
|
-
import
|
|
9510
|
+
import path59 from "node:path";
|
|
8810
9511
|
import fs41 from "fs-extra";
|
|
8811
9512
|
|
|
8812
9513
|
// src/workspace/worktree/git-worktree.ts
|
|
@@ -8926,12 +9627,12 @@ async function hasUnpushedWork(cwd) {
|
|
|
8926
9627
|
}
|
|
8927
9628
|
|
|
8928
9629
|
// src/workspace/worktree/hydrate.ts
|
|
8929
|
-
import
|
|
9630
|
+
import path55 from "node:path";
|
|
8930
9631
|
import fs39 from "fs-extra";
|
|
8931
9632
|
|
|
8932
9633
|
// src/workspace/worktree/cow-copy.ts
|
|
8933
9634
|
init_exec();
|
|
8934
|
-
import
|
|
9635
|
+
import path53 from "node:path";
|
|
8935
9636
|
import fs38 from "fs-extra";
|
|
8936
9637
|
async function getMacFilesystemType(dir) {
|
|
8937
9638
|
const df = await runCommand("df", ["-P", dir]);
|
|
@@ -8966,11 +9667,11 @@ async function cowCopyDir(src, dest) {
|
|
|
8966
9667
|
const srcExists = await fs38.pathExists(src);
|
|
8967
9668
|
if (!srcExists)
|
|
8968
9669
|
return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
|
|
8969
|
-
const strategy = await detectCowStrategy(
|
|
9670
|
+
const strategy = await detectCowStrategy(path53.dirname(dest));
|
|
8970
9671
|
if (strategy === "unsupported" || strategy === "unknown-platform") {
|
|
8971
9672
|
return { strategy, attempted: false, ok: false };
|
|
8972
9673
|
}
|
|
8973
|
-
await fs38.ensureDir(
|
|
9674
|
+
await fs38.ensureDir(path53.dirname(dest));
|
|
8974
9675
|
const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
|
|
8975
9676
|
const result = await runCommand("cp", args);
|
|
8976
9677
|
return {
|
|
@@ -8984,7 +9685,7 @@ async function cowCopyDir(src, dest) {
|
|
|
8984
9685
|
// src/workspace/worktree/install.ts
|
|
8985
9686
|
init_exec();
|
|
8986
9687
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
8987
|
-
import
|
|
9688
|
+
import path54 from "node:path";
|
|
8988
9689
|
import fg6 from "fast-glob";
|
|
8989
9690
|
function detectInstallPlan(signals) {
|
|
8990
9691
|
if (signals.hasYarnLock) {
|
|
@@ -9005,10 +9706,10 @@ function detectInstallPlan(signals) {
|
|
|
9005
9706
|
return { manager: "none" };
|
|
9006
9707
|
}
|
|
9007
9708
|
async function computeLockfileSignals(dir) {
|
|
9008
|
-
const hasYarnLock = existsSync3(
|
|
9009
|
-
const hasPnpmLock = existsSync3(
|
|
9010
|
-
const hasPackageLock = existsSync3(
|
|
9011
|
-
const hasComposerLock = existsSync3(
|
|
9709
|
+
const hasYarnLock = existsSync3(path54.join(dir, "yarn.lock"));
|
|
9710
|
+
const hasPnpmLock = existsSync3(path54.join(dir, "pnpm-lock.yaml"));
|
|
9711
|
+
const hasPackageLock = existsSync3(path54.join(dir, "package-lock.json"));
|
|
9712
|
+
const hasComposerLock = existsSync3(path54.join(dir, "composer.lock"));
|
|
9012
9713
|
const dotnetFiles = await fg6(["**/*.csproj", "**/*.sln"], {
|
|
9013
9714
|
cwd: dir,
|
|
9014
9715
|
onlyFiles: true,
|
|
@@ -9022,7 +9723,7 @@ async function computeLockfileSignals(dir) {
|
|
|
9022
9723
|
});
|
|
9023
9724
|
const hasDotnetProject = dotnetFiles.length > 0;
|
|
9024
9725
|
let hasPackageManagerField = false;
|
|
9025
|
-
const pkgPath =
|
|
9726
|
+
const pkgPath = path54.join(dir, "package.json");
|
|
9026
9727
|
if (existsSync3(pkgPath)) {
|
|
9027
9728
|
try {
|
|
9028
9729
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
@@ -9063,8 +9764,8 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
9063
9764
|
const start = Date.now();
|
|
9064
9765
|
const targets = [];
|
|
9065
9766
|
for (const target of HYDRATION_TARGETS) {
|
|
9066
|
-
const src =
|
|
9067
|
-
const dest =
|
|
9767
|
+
const src = path55.join(mainMemberPath, target);
|
|
9768
|
+
const dest = path55.join(worktreeMemberPath, target);
|
|
9068
9769
|
if (!await fs39.pathExists(src)) continue;
|
|
9069
9770
|
const destExists = await fs39.pathExists(dest);
|
|
9070
9771
|
if (!opts.force && destExists) continue;
|
|
@@ -9082,11 +9783,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
9082
9783
|
|
|
9083
9784
|
// src/workspace/worktree/local-files.ts
|
|
9084
9785
|
init_exec();
|
|
9085
|
-
import
|
|
9786
|
+
import path56 from "node:path";
|
|
9086
9787
|
import fs40 from "fs-extra";
|
|
9087
9788
|
var MACHINE_LOCAL_FILES = [
|
|
9088
9789
|
"docker-compose.local.yml",
|
|
9089
|
-
|
|
9790
|
+
path56.join(".claude", "settings.local.json")
|
|
9090
9791
|
];
|
|
9091
9792
|
async function isTracked(repoRoot, relFile) {
|
|
9092
9793
|
const gitPath = relFile.replace(/\\/g, "/");
|
|
@@ -9096,11 +9797,11 @@ async function isTracked(repoRoot, relFile) {
|
|
|
9096
9797
|
async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
9097
9798
|
const copied = [];
|
|
9098
9799
|
for (const rel of MACHINE_LOCAL_FILES) {
|
|
9099
|
-
const src =
|
|
9800
|
+
const src = path56.join(mainMemberPath, rel);
|
|
9100
9801
|
if (!await fs40.pathExists(src)) continue;
|
|
9101
9802
|
if (await isTracked(mainMemberPath, rel)) continue;
|
|
9102
|
-
const dest =
|
|
9103
|
-
await fs40.ensureDir(
|
|
9803
|
+
const dest = path56.join(worktreeMemberPath, rel);
|
|
9804
|
+
await fs40.ensureDir(path56.dirname(dest));
|
|
9104
9805
|
await fs40.copy(src, dest, { overwrite: true });
|
|
9105
9806
|
copied.push(rel);
|
|
9106
9807
|
}
|
|
@@ -9109,15 +9810,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
|
9109
9810
|
|
|
9110
9811
|
// src/workspace/worktree/root.ts
|
|
9111
9812
|
import { existsSync as existsSync4 } from "node:fs";
|
|
9112
|
-
import
|
|
9813
|
+
import path57 from "node:path";
|
|
9113
9814
|
async function resolveWorkspaceForWorktree(start) {
|
|
9114
9815
|
const rootInfo = await resolveRoots(start);
|
|
9115
9816
|
if (!rootInfo.isGitRepo) {
|
|
9116
9817
|
return { ok: false, reason: "Not inside a git repository." };
|
|
9117
9818
|
}
|
|
9118
9819
|
const workspaceRoot = rootInfo.mainRoot;
|
|
9119
|
-
const hasYaml = existsSync4(
|
|
9120
|
-
const hasManifest = existsSync4(
|
|
9820
|
+
const hasYaml = existsSync4(path57.join(workspaceRoot, WORKSPACE_FILE));
|
|
9821
|
+
const hasManifest = existsSync4(path57.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
9121
9822
|
if (!hasYaml && !hasManifest) {
|
|
9122
9823
|
return {
|
|
9123
9824
|
ok: false,
|
|
@@ -9142,10 +9843,10 @@ function selectMembers(members, only) {
|
|
|
9142
9843
|
}
|
|
9143
9844
|
|
|
9144
9845
|
// src/workspace/worktree/state.ts
|
|
9145
|
-
import
|
|
9846
|
+
import path58 from "node:path";
|
|
9146
9847
|
var WORKTREE_STATE_FILE = ".haus-worktree.json";
|
|
9147
9848
|
function worktreesDir(mainRoot) {
|
|
9148
|
-
return
|
|
9849
|
+
return path58.join(mainRoot, ".claude", "worktrees");
|
|
9149
9850
|
}
|
|
9150
9851
|
function isValidWorktreeSlug(slug) {
|
|
9151
9852
|
if (slug === "" || slug === "." || slug === "..") return false;
|
|
@@ -9158,15 +9859,15 @@ function worktreePath(mainRoot, slug) {
|
|
|
9158
9859
|
`Invalid worktree slug ${JSON.stringify(slug)} \u2014 slugs may only contain letters, digits, ".", "_", "-" (no path separators, and not "." or "..").`
|
|
9159
9860
|
);
|
|
9160
9861
|
}
|
|
9161
|
-
return
|
|
9862
|
+
return path58.join(worktreesDir(mainRoot), slug);
|
|
9162
9863
|
}
|
|
9163
9864
|
async function readWorktreeState(worktreeRoot) {
|
|
9164
|
-
const state = await readJson(
|
|
9865
|
+
const state = await readJson(path58.join(worktreeRoot, WORKTREE_STATE_FILE));
|
|
9165
9866
|
if (state == null || typeof state !== "object" || !Array.isArray(state.members)) return void 0;
|
|
9166
9867
|
return state;
|
|
9167
9868
|
}
|
|
9168
9869
|
async function writeWorktreeState(worktreeRoot, state) {
|
|
9169
|
-
await writeJson(
|
|
9870
|
+
await writeJson(path58.join(worktreeRoot, WORKTREE_STATE_FILE), state);
|
|
9170
9871
|
}
|
|
9171
9872
|
|
|
9172
9873
|
// src/workspace/worktree/add.ts
|
|
@@ -9200,7 +9901,7 @@ async function runAdd(opts) {
|
|
|
9200
9901
|
const memberResults = [];
|
|
9201
9902
|
for (const member of selected) {
|
|
9202
9903
|
const start = Date.now();
|
|
9203
|
-
const memberWorktreePath =
|
|
9904
|
+
const memberWorktreePath = path59.join(wsWorktreePath, member.folder);
|
|
9204
9905
|
const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
|
|
9205
9906
|
preferDefaultBranchFrom: true
|
|
9206
9907
|
});
|
|
@@ -9230,7 +9931,7 @@ async function runAdd(opts) {
|
|
|
9230
9931
|
await mapWithConcurrency(okResults, async (result) => {
|
|
9231
9932
|
const member = selected.find((m) => m.id === result.member);
|
|
9232
9933
|
if (!member) return;
|
|
9233
|
-
const memberWorktreePath =
|
|
9934
|
+
const memberWorktreePath = path59.join(wsWorktreePath, member.folder);
|
|
9234
9935
|
const hydrationStart = Date.now();
|
|
9235
9936
|
if (opts.hydrate !== false) {
|
|
9236
9937
|
result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
|
|
@@ -9297,10 +9998,10 @@ async function runDryRun(args) {
|
|
|
9297
9998
|
|
|
9298
9999
|
// src/workspace/worktree/doctor.ts
|
|
9299
10000
|
import { existsSync as existsSync5 } from "node:fs";
|
|
9300
|
-
import
|
|
10001
|
+
import path60 from "node:path";
|
|
9301
10002
|
var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
|
|
9302
10003
|
function looksHydrated(memberPath) {
|
|
9303
|
-
return HYDRATION_MARKERS.some((marker) => existsSync5(
|
|
10004
|
+
return HYDRATION_MARKERS.some((marker) => existsSync5(path60.join(memberPath, marker)));
|
|
9304
10005
|
}
|
|
9305
10006
|
async function runDoctor2() {
|
|
9306
10007
|
const rootInfo = await resolveRoots();
|
|
@@ -9314,7 +10015,7 @@ async function runDoctor2() {
|
|
|
9314
10015
|
};
|
|
9315
10016
|
}
|
|
9316
10017
|
const workspaceRoot = rootInfo.mainRoot;
|
|
9317
|
-
const isWorkspace = existsSync5(
|
|
10018
|
+
const isWorkspace = existsSync5(path60.join(workspaceRoot, WORKSPACE_FILE)) || existsSync5(path60.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
9318
10019
|
if (!isWorkspace) {
|
|
9319
10020
|
return {
|
|
9320
10021
|
isWorkspace: false,
|
|
@@ -9327,7 +10028,7 @@ async function runDoctor2() {
|
|
|
9327
10028
|
};
|
|
9328
10029
|
}
|
|
9329
10030
|
const wsWorktreesDir = worktreesDir(workspaceRoot);
|
|
9330
|
-
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${
|
|
10031
|
+
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path60.sep}`);
|
|
9331
10032
|
const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
|
|
9332
10033
|
const members = await readMembers(rootInfo);
|
|
9333
10034
|
const problems = [];
|
|
@@ -9335,10 +10036,10 @@ async function runDoctor2() {
|
|
|
9335
10036
|
let slug;
|
|
9336
10037
|
if (inWorkspaceWorktree) {
|
|
9337
10038
|
const wtPath = rootInfo.repoRoot;
|
|
9338
|
-
slug = rootInfo.worktreeName ??
|
|
10039
|
+
slug = rootInfo.worktreeName ?? path60.basename(wtPath);
|
|
9339
10040
|
const state = await readWorktreeState(wtPath);
|
|
9340
10041
|
for (const member of members) {
|
|
9341
|
-
const memberPath =
|
|
10042
|
+
const memberPath = path60.join(wtPath, member.folder);
|
|
9342
10043
|
const materialized = existsSync5(memberPath);
|
|
9343
10044
|
const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
|
|
9344
10045
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
@@ -9369,16 +10070,16 @@ async function runDoctor2() {
|
|
|
9369
10070
|
for (const member of members) {
|
|
9370
10071
|
const entries = await listWorktrees(member.absPath);
|
|
9371
10072
|
for (const entry of entries) {
|
|
9372
|
-
const entryPath =
|
|
9373
|
-
const marker = `${
|
|
10073
|
+
const entryPath = path60.normalize(entry.path);
|
|
10074
|
+
const marker = `${path60.sep}.claude${path60.sep}worktrees${path60.sep}`;
|
|
9374
10075
|
const idx = entryPath.indexOf(marker);
|
|
9375
10076
|
if (idx === -1) continue;
|
|
9376
10077
|
const rest = entryPath.slice(idx + marker.length);
|
|
9377
|
-
const slugName = rest.split(
|
|
10078
|
+
const slugName = rest.split(path60.sep)[0];
|
|
9378
10079
|
if (!slugName) continue;
|
|
9379
|
-
if (!existsSync5(
|
|
10080
|
+
if (!existsSync5(path60.join(wsWorktreesDir, slugName))) {
|
|
9380
10081
|
orphans.push(
|
|
9381
|
-
`${member.id}: worktree registered at ${entry.path} but ${
|
|
10082
|
+
`${member.id}: worktree registered at ${entry.path} but ${path60.join(wsWorktreesDir, slugName)} no longer exists \u2014 run \`git -C "${member.absPath}" worktree prune\`.`
|
|
9382
10083
|
);
|
|
9383
10084
|
}
|
|
9384
10085
|
}
|
|
@@ -9389,7 +10090,7 @@ async function runDoctor2() {
|
|
|
9389
10090
|
|
|
9390
10091
|
// src/workspace/worktree/hydrate-command.ts
|
|
9391
10092
|
import { existsSync as existsSync6 } from "node:fs";
|
|
9392
|
-
import
|
|
10093
|
+
import path61 from "node:path";
|
|
9393
10094
|
async function runHydrateWorktree(opts) {
|
|
9394
10095
|
const rootInfo = await resolveRoots();
|
|
9395
10096
|
if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
|
|
@@ -9400,7 +10101,7 @@ async function runHydrateWorktree(opts) {
|
|
|
9400
10101
|
}
|
|
9401
10102
|
const workspaceRoot = rootInfo.mainRoot;
|
|
9402
10103
|
const wtPath = rootInfo.repoRoot;
|
|
9403
|
-
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${
|
|
10104
|
+
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path61.sep}`)) {
|
|
9404
10105
|
return {
|
|
9405
10106
|
ok: false,
|
|
9406
10107
|
error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
|
|
@@ -9412,13 +10113,13 @@ async function runHydrateWorktree(opts) {
|
|
|
9412
10113
|
return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
|
|
9413
10114
|
}
|
|
9414
10115
|
const state = await readWorktreeState(wtPath);
|
|
9415
|
-
const slug = state?.slug ??
|
|
10116
|
+
const slug = state?.slug ?? path61.basename(wtPath);
|
|
9416
10117
|
const detectedBranch = await currentBranch(wtPath);
|
|
9417
10118
|
const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
|
|
9418
10119
|
if (opts.dryRun) {
|
|
9419
10120
|
const results2 = await Promise.all(
|
|
9420
10121
|
selected.map(async (member) => {
|
|
9421
|
-
const memberPath =
|
|
10122
|
+
const memberPath = path61.join(wtPath, member.folder);
|
|
9422
10123
|
if (existsSync6(memberPath)) {
|
|
9423
10124
|
return { member: member.id, folder: member.folder, status: "planned" };
|
|
9424
10125
|
}
|
|
@@ -9439,7 +10140,7 @@ async function runHydrateWorktree(opts) {
|
|
|
9439
10140
|
const results = await mapWithConcurrency(
|
|
9440
10141
|
selected,
|
|
9441
10142
|
async (member) => {
|
|
9442
|
-
const memberPath =
|
|
10143
|
+
const memberPath = path61.join(wtPath, member.folder);
|
|
9443
10144
|
let branchAction;
|
|
9444
10145
|
if (!existsSync6(memberPath)) {
|
|
9445
10146
|
if (!opts.materialize) {
|
|
@@ -9502,7 +10203,7 @@ async function runHydrateWorktree(opts) {
|
|
|
9502
10203
|
}
|
|
9503
10204
|
|
|
9504
10205
|
// src/workspace/worktree/list.ts
|
|
9505
|
-
import
|
|
10206
|
+
import path62 from "node:path";
|
|
9506
10207
|
import fs42 from "fs-extra";
|
|
9507
10208
|
async function runList2() {
|
|
9508
10209
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -9515,12 +10216,12 @@ async function runList2() {
|
|
|
9515
10216
|
const configuredMembers = await readMembers(rootInfo);
|
|
9516
10217
|
const worktrees = [];
|
|
9517
10218
|
for (const slug of slugs) {
|
|
9518
|
-
const wtPath =
|
|
10219
|
+
const wtPath = path62.join(dir, slug);
|
|
9519
10220
|
const state = await readWorktreeState(wtPath);
|
|
9520
10221
|
const members = [];
|
|
9521
10222
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
9522
10223
|
for (const m of source) {
|
|
9523
|
-
const memberPath =
|
|
10224
|
+
const memberPath = path62.join(wtPath, m.folder);
|
|
9524
10225
|
const materialized = await fs42.pathExists(memberPath);
|
|
9525
10226
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
9526
10227
|
members.push({
|
|
@@ -9542,7 +10243,7 @@ async function runList2() {
|
|
|
9542
10243
|
}
|
|
9543
10244
|
|
|
9544
10245
|
// src/workspace/worktree/remove.ts
|
|
9545
|
-
import
|
|
10246
|
+
import path63 from "node:path";
|
|
9546
10247
|
import fs43 from "fs-extra";
|
|
9547
10248
|
async function runRemove(opts) {
|
|
9548
10249
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -9569,7 +10270,7 @@ async function runRemove(opts) {
|
|
|
9569
10270
|
targets.push({
|
|
9570
10271
|
repo: configMember?.id ?? stateMember.id,
|
|
9571
10272
|
ownerRepo,
|
|
9572
|
-
path:
|
|
10273
|
+
path: path63.join(wsWorktreePath, stateMember.folder)
|
|
9573
10274
|
});
|
|
9574
10275
|
}
|
|
9575
10276
|
const existingTargets = [];
|
|
@@ -9637,12 +10338,6 @@ async function runRemove(opts) {
|
|
|
9637
10338
|
}
|
|
9638
10339
|
|
|
9639
10340
|
// src/commands/workspace/worktree.ts
|
|
9640
|
-
function normalizeOnly(only) {
|
|
9641
|
-
if (!only) return void 0;
|
|
9642
|
-
const list = Array.isArray(only) ? only : only.split(/[\s,]+/);
|
|
9643
|
-
const cleaned = list.map((s) => s.trim()).filter(Boolean);
|
|
9644
|
-
return cleaned.length > 0 ? cleaned : void 0;
|
|
9645
|
-
}
|
|
9646
10341
|
function describeInstall(hydration) {
|
|
9647
10342
|
if (hydration.install.plan.manager === "none") return "no install step (no recognized lockfile)";
|
|
9648
10343
|
if (!hydration.install.ran)
|
|
@@ -9846,7 +10541,7 @@ async function runWorktree(action, opts = {}) {
|
|
|
9846
10541
|
|
|
9847
10542
|
// src/commands/workspace.ts
|
|
9848
10543
|
import { existsSync as existsSync10, statSync as statSync3 } from "node:fs";
|
|
9849
|
-
import
|
|
10544
|
+
import path72 from "node:path";
|
|
9850
10545
|
|
|
9851
10546
|
// src/commands/workspace/aggregate.ts
|
|
9852
10547
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -9901,18 +10596,18 @@ ${summaries.map(
|
|
|
9901
10596
|
|
|
9902
10597
|
// src/commands/workspace/doctor.ts
|
|
9903
10598
|
import { existsSync as existsSync7 } from "node:fs";
|
|
9904
|
-
import
|
|
10599
|
+
import path65 from "node:path";
|
|
9905
10600
|
|
|
9906
10601
|
// src/commands/workspace/manifest.ts
|
|
9907
10602
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
9908
|
-
import
|
|
10603
|
+
import path64 from "node:path";
|
|
9909
10604
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
9910
10605
|
function manifestPath(workspaceRoot) {
|
|
9911
10606
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
9912
10607
|
}
|
|
9913
10608
|
function hausVersion() {
|
|
9914
10609
|
try {
|
|
9915
|
-
const pkg = JSON.parse(readFileSync4(
|
|
10610
|
+
const pkg = JSON.parse(readFileSync4(path64.join(packageRoot(), "package.json"), "utf8"));
|
|
9916
10611
|
return pkg.version ?? "0.0.0";
|
|
9917
10612
|
} catch {
|
|
9918
10613
|
return "0.0.0";
|
|
@@ -10004,7 +10699,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
10004
10699
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
10005
10700
|
const catalogRefByRepo = [];
|
|
10006
10701
|
for (const repo of config.repos) {
|
|
10007
|
-
const repoRoot =
|
|
10702
|
+
const repoRoot = path65.resolve(workspaceRoot, repo.path);
|
|
10008
10703
|
const entry = manifestByName.get(repo.name);
|
|
10009
10704
|
if (!entry) {
|
|
10010
10705
|
flag({
|
|
@@ -10077,7 +10772,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
10077
10772
|
});
|
|
10078
10773
|
continue;
|
|
10079
10774
|
}
|
|
10080
|
-
if (!existsSync7(
|
|
10775
|
+
if (!existsSync7(path65.join(workspaceRoot, entry.path))) {
|
|
10081
10776
|
flag({
|
|
10082
10777
|
repo: entry.repo,
|
|
10083
10778
|
kind: "missing-linked-context-copy",
|
|
@@ -10120,15 +10815,15 @@ function emit(args) {
|
|
|
10120
10815
|
}
|
|
10121
10816
|
|
|
10122
10817
|
// src/workspace/link-context/link.ts
|
|
10123
|
-
import
|
|
10818
|
+
import path68 from "node:path";
|
|
10124
10819
|
import fs46 from "fs-extra";
|
|
10125
10820
|
|
|
10126
10821
|
// src/workspace/link-context/plan.ts
|
|
10127
|
-
import
|
|
10822
|
+
import path67 from "node:path";
|
|
10128
10823
|
import fs45 from "fs-extra";
|
|
10129
10824
|
|
|
10130
10825
|
// src/workspace/link-context/scan-source.ts
|
|
10131
|
-
import
|
|
10826
|
+
import path66 from "node:path";
|
|
10132
10827
|
import fs44 from "fs-extra";
|
|
10133
10828
|
var ASSET_SUBDIR = {
|
|
10134
10829
|
skill: "skills",
|
|
@@ -10137,36 +10832,36 @@ var ASSET_SUBDIR = {
|
|
|
10137
10832
|
};
|
|
10138
10833
|
async function hasLinkableContext(member) {
|
|
10139
10834
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
10140
|
-
if (await fs44.pathExists(
|
|
10835
|
+
if (await fs44.pathExists(path66.join(member.absPath, ".claude", subdir))) return true;
|
|
10141
10836
|
}
|
|
10142
10837
|
return false;
|
|
10143
10838
|
}
|
|
10144
10839
|
async function collectSkills(member) {
|
|
10145
|
-
const dir =
|
|
10840
|
+
const dir = path66.join(member.absPath, ".claude", "skills");
|
|
10146
10841
|
if (!await fs44.pathExists(dir)) return [];
|
|
10147
10842
|
const names = await fs44.readdir(dir);
|
|
10148
10843
|
const out = [];
|
|
10149
10844
|
for (const name of [...names].sort()) {
|
|
10150
|
-
const abs =
|
|
10845
|
+
const abs = path66.join(dir, name);
|
|
10151
10846
|
const stat = await fs44.lstat(abs);
|
|
10152
10847
|
if (!stat.isDirectory()) continue;
|
|
10153
|
-
if (!await fs44.pathExists(
|
|
10154
|
-
out.push({ type: "skill", name, sourceRelPath:
|
|
10848
|
+
if (!await fs44.pathExists(path66.join(abs, "SKILL.md"))) continue;
|
|
10849
|
+
out.push({ type: "skill", name, sourceRelPath: path66.posix.join(".claude", "skills", name) });
|
|
10155
10850
|
}
|
|
10156
10851
|
return out;
|
|
10157
10852
|
}
|
|
10158
10853
|
async function collectFlatMarkdown(member, type) {
|
|
10159
10854
|
const subdir = ASSET_SUBDIR[type];
|
|
10160
|
-
const dir =
|
|
10855
|
+
const dir = path66.join(member.absPath, ".claude", subdir);
|
|
10161
10856
|
if (!await fs44.pathExists(dir)) return [];
|
|
10162
10857
|
const names = await fs44.readdir(dir);
|
|
10163
10858
|
const out = [];
|
|
10164
10859
|
for (const fileName of [...names].sort()) {
|
|
10165
10860
|
if (!fileName.endsWith(".md")) continue;
|
|
10166
|
-
const abs =
|
|
10861
|
+
const abs = path66.join(dir, fileName);
|
|
10167
10862
|
if (!(await fs44.lstat(abs)).isFile()) continue;
|
|
10168
10863
|
const name = fileName.slice(0, -".md".length);
|
|
10169
|
-
out.push({ type, name, sourceRelPath:
|
|
10864
|
+
out.push({ type, name, sourceRelPath: path66.posix.join(".claude", subdir, fileName) });
|
|
10170
10865
|
}
|
|
10171
10866
|
return out;
|
|
10172
10867
|
}
|
|
@@ -10181,9 +10876,9 @@ async function collectSourceAssets(member) {
|
|
|
10181
10876
|
|
|
10182
10877
|
// src/workspace/link-context/plan.ts
|
|
10183
10878
|
function repoFolderPrefix(member) {
|
|
10184
|
-
const base =
|
|
10879
|
+
const base = path67.posix.basename(member.folder);
|
|
10185
10880
|
if (base && base !== "." && base !== "/") return base;
|
|
10186
|
-
const fromAbs =
|
|
10881
|
+
const fromAbs = path67.basename(member.absPath);
|
|
10187
10882
|
return fromAbs || member.id;
|
|
10188
10883
|
}
|
|
10189
10884
|
function assetKey(type, destKey) {
|
|
@@ -10240,7 +10935,7 @@ async function buildLinkPlan(members) {
|
|
|
10240
10935
|
name: asset.name,
|
|
10241
10936
|
destKey,
|
|
10242
10937
|
sourceRelPath: asset.sourceRelPath,
|
|
10243
|
-
sourceAbsPath:
|
|
10938
|
+
sourceAbsPath: path67.join(member.absPath, asset.sourceRelPath),
|
|
10244
10939
|
sourceHash
|
|
10245
10940
|
});
|
|
10246
10941
|
}
|
|
@@ -10251,7 +10946,7 @@ async function buildLinkPlan(members) {
|
|
|
10251
10946
|
function destRelPathFor(type, destKey) {
|
|
10252
10947
|
const subdir = `${type}s`;
|
|
10253
10948
|
const leaf = type === "skill" ? destKey : `${destKey}.md`;
|
|
10254
|
-
return
|
|
10949
|
+
return path68.posix.join(".claude", subdir, leaf);
|
|
10255
10950
|
}
|
|
10256
10951
|
async function runLinkContext(workspaceRoot, opts = {}) {
|
|
10257
10952
|
const write = opts.write ?? false;
|
|
@@ -10277,9 +10972,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
10277
10972
|
const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10278
10973
|
for (const entry of plan.entries) {
|
|
10279
10974
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
10280
|
-
const destAbs =
|
|
10975
|
+
const destAbs = path68.join(workspaceRoot, destRel);
|
|
10281
10976
|
if (write) {
|
|
10282
|
-
await fs46.ensureDir(
|
|
10977
|
+
await fs46.ensureDir(path68.dirname(destAbs));
|
|
10283
10978
|
if (await fs46.pathExists(destAbs)) await fs46.remove(destAbs);
|
|
10284
10979
|
await fs46.copy(entry.sourceAbsPath, destAbs, {
|
|
10285
10980
|
overwrite: true,
|
|
@@ -10368,10 +11063,10 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
|
|
|
10368
11063
|
|
|
10369
11064
|
// src/commands/workspace/setup.ts
|
|
10370
11065
|
import { existsSync as existsSync8, statSync } from "node:fs";
|
|
10371
|
-
import
|
|
11066
|
+
import path70 from "node:path";
|
|
10372
11067
|
|
|
10373
11068
|
// src/claude/write-workspace-claude-md.ts
|
|
10374
|
-
import
|
|
11069
|
+
import path69 from "node:path";
|
|
10375
11070
|
import fs47 from "fs-extra";
|
|
10376
11071
|
var WORKSPACE_SENTINELS = { begin: WORKSPACE_BLOCK_BEGIN, end: WORKSPACE_BLOCK_END };
|
|
10377
11072
|
function buildWorkspaceImportBlock(client, members) {
|
|
@@ -10391,7 +11086,7 @@ ${WORKSPACE_BLOCK_END}`;
|
|
|
10391
11086
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
10392
11087
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
10393
11088
|
const dryRun = opts.dryRun ?? false;
|
|
10394
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
11089
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path69.join(workspaceRoot, "CLAUDE.md");
|
|
10395
11090
|
const say = opts.quiet ? () => {
|
|
10396
11091
|
} : log;
|
|
10397
11092
|
const existed = await fs47.pathExists(filePath);
|
|
@@ -10422,20 +11117,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
10422
11117
|
|
|
10423
11118
|
// src/commands/workspace/setup.ts
|
|
10424
11119
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
10425
|
-
let dir =
|
|
11120
|
+
let dir = path70.resolve(start);
|
|
10426
11121
|
for (; ; ) {
|
|
10427
|
-
if (existsSync8(
|
|
10428
|
-
const parent =
|
|
10429
|
-
if (parent === dir) return
|
|
11122
|
+
if (existsSync8(path70.join(dir, WORKSPACE_FILE))) return dir;
|
|
11123
|
+
const parent = path70.dirname(dir);
|
|
11124
|
+
if (parent === dir) return path70.resolve(start);
|
|
10430
11125
|
dir = parent;
|
|
10431
11126
|
}
|
|
10432
11127
|
}
|
|
10433
11128
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
10434
|
-
return
|
|
11129
|
+
return path70.resolve(workspaceRoot, repoPath) === path70.resolve(workspaceRoot);
|
|
10435
11130
|
}
|
|
10436
11131
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
10437
11132
|
const apply = options.write ?? false;
|
|
10438
|
-
const configText = await readText(
|
|
11133
|
+
const configText = await readText(path70.join(workspaceRoot, WORKSPACE_FILE));
|
|
10439
11134
|
if (!configText) {
|
|
10440
11135
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
10441
11136
|
process.exitCode = 1;
|
|
@@ -10462,7 +11157,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10462
11157
|
const statuses = [];
|
|
10463
11158
|
const aggregateInputs = [];
|
|
10464
11159
|
for (const repo of repos) {
|
|
10465
|
-
const repoRoot =
|
|
11160
|
+
const repoRoot = path70.resolve(workspaceRoot, repo.path);
|
|
10466
11161
|
say(`
|
|
10467
11162
|
\u2192 ${repo.name} (${repo.path})`);
|
|
10468
11163
|
try {
|
|
@@ -10502,7 +11197,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10502
11197
|
process.exitCode = 1;
|
|
10503
11198
|
return result2;
|
|
10504
11199
|
}
|
|
10505
|
-
throw err;
|
|
11200
|
+
throw new Error(`Setup failed for ${repo.name}: ${message}`, { cause: err });
|
|
10506
11201
|
}
|
|
10507
11202
|
error(`Setup failed for ${repo.name}: ${message}`);
|
|
10508
11203
|
}
|
|
@@ -10552,7 +11247,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10552
11247
|
const status = statusByName.get(repo.name);
|
|
10553
11248
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
10554
11249
|
if (status?.status === "ok") {
|
|
10555
|
-
const lock = await checkLock(
|
|
11250
|
+
const lock = await checkLock(path70.resolve(workspaceRoot, repo.path));
|
|
10556
11251
|
manifestRepos.push({
|
|
10557
11252
|
name: repo.name,
|
|
10558
11253
|
path: repo.path,
|
|
@@ -10608,7 +11303,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10608
11303
|
try {
|
|
10609
11304
|
const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
|
|
10610
11305
|
if (linkResult.ok) {
|
|
10611
|
-
written.push(...linkResult.added.map((rel) =>
|
|
11306
|
+
written.push(...linkResult.added.map((rel) => path70.join(workspaceRoot, rel)));
|
|
10612
11307
|
if (!options.json) {
|
|
10613
11308
|
say(
|
|
10614
11309
|
`Linked ${linkResult.linked.length} cross-repo skill/agent/command copy(ies) into .claude/ (${linkResult.added.length} new, ${linkResult.removed.length} removed)${linkResult.skipped.length > 0 ? ` \u2014 ${linkResult.skipped.length} member(s) skipped, see above` : ""}.`
|
|
@@ -10640,7 +11335,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
10640
11335
|
|
|
10641
11336
|
// src/commands/workspace/undo.ts
|
|
10642
11337
|
import { existsSync as existsSync9, statSync as statSync2 } from "node:fs";
|
|
10643
|
-
import
|
|
11338
|
+
import path71 from "node:path";
|
|
10644
11339
|
import fs48 from "fs-extra";
|
|
10645
11340
|
var WORKSPACE_AGGREGATE_FILES = [
|
|
10646
11341
|
"workspace-summary.json",
|
|
@@ -10677,7 +11372,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10677
11372
|
}
|
|
10678
11373
|
let anyRepoFailed = false;
|
|
10679
11374
|
for (const repo of config.repos) {
|
|
10680
|
-
const repoRoot =
|
|
11375
|
+
const repoRoot = path71.resolve(workspaceRoot, repo.path);
|
|
10681
11376
|
log(`
|
|
10682
11377
|
\u2192 ${repo.name} (${repo.path})`);
|
|
10683
11378
|
try {
|
|
@@ -10706,20 +11401,20 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10706
11401
|
const abs = hausPath(workspaceRoot, rel);
|
|
10707
11402
|
if (await fs48.pathExists(abs)) {
|
|
10708
11403
|
await fs48.remove(abs);
|
|
10709
|
-
log(`Removed ${
|
|
11404
|
+
log(`Removed ${path71.relative(workspaceRoot, abs)}`);
|
|
10710
11405
|
}
|
|
10711
11406
|
}
|
|
10712
11407
|
const manifestFile = manifestPath(workspaceRoot);
|
|
10713
11408
|
if (await fs48.pathExists(manifestFile)) {
|
|
10714
11409
|
await fs48.remove(manifestFile);
|
|
10715
|
-
log(`Removed ${
|
|
11410
|
+
log(`Removed ${path71.relative(workspaceRoot, manifestFile)}`);
|
|
10716
11411
|
}
|
|
10717
11412
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
10718
11413
|
if (await fs48.pathExists(workspaceMdPath)) {
|
|
10719
11414
|
await fs48.remove(workspaceMdPath);
|
|
10720
|
-
log(`Removed ${
|
|
11415
|
+
log(`Removed ${path71.relative(workspaceRoot, workspaceMdPath)}`);
|
|
10721
11416
|
} else {
|
|
10722
|
-
const claudeMdPath =
|
|
11417
|
+
const claudeMdPath = path71.join(workspaceRoot, "CLAUDE.md");
|
|
10723
11418
|
if (existsSync9(claudeMdPath)) {
|
|
10724
11419
|
const prev = await readText(claudeMdPath);
|
|
10725
11420
|
if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
|
|
@@ -10758,12 +11453,6 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10758
11453
|
}
|
|
10759
11454
|
|
|
10760
11455
|
// src/commands/workspace.ts
|
|
10761
|
-
function normalizeOnly2(only) {
|
|
10762
|
-
if (!only) return void 0;
|
|
10763
|
-
const list = Array.isArray(only) ? only : only.split(/[\s,]+/);
|
|
10764
|
-
const cleaned = list.map((s) => s.trim()).filter(Boolean);
|
|
10765
|
-
return cleaned.length > 0 ? cleaned : void 0;
|
|
10766
|
-
}
|
|
10767
11456
|
function normalizeMaxDepth(maxDepth) {
|
|
10768
11457
|
if (maxDepth === void 0) return void 0;
|
|
10769
11458
|
const n = typeof maxDepth === "number" ? maxDepth : Number.parseInt(maxDepth, 10);
|
|
@@ -10794,7 +11483,7 @@ relationships: []
|
|
|
10794
11483
|
log("Workspace initialized.");
|
|
10795
11484
|
}
|
|
10796
11485
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
10797
|
-
const configText = await readText(
|
|
11486
|
+
const configText = await readText(path72.join(workspaceRoot, WORKSPACE_FILE));
|
|
10798
11487
|
if (!configText) {
|
|
10799
11488
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
10800
11489
|
process.exitCode = 1;
|
|
@@ -10816,7 +11505,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
10816
11505
|
const inputs = [];
|
|
10817
11506
|
const failures = [];
|
|
10818
11507
|
for (const repo of config.repos) {
|
|
10819
|
-
const repoRoot =
|
|
11508
|
+
const repoRoot = path72.resolve(workspaceRoot, repo.path);
|
|
10820
11509
|
try {
|
|
10821
11510
|
if (!existsSync10(repoRoot) || !statSync3(repoRoot).isDirectory()) {
|
|
10822
11511
|
throw new Error("path is not a directory");
|
|
@@ -10864,7 +11553,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
10864
11553
|
dryRun: options.dryRun,
|
|
10865
11554
|
json: options.json,
|
|
10866
11555
|
continueOnError: options.continueOnError,
|
|
10867
|
-
only:
|
|
11556
|
+
only: normalizeOnly(options.only)
|
|
10868
11557
|
});
|
|
10869
11558
|
return;
|
|
10870
11559
|
case "doctor":
|
|
@@ -10882,7 +11571,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
10882
11571
|
// src/cli.ts
|
|
10883
11572
|
function cliVersion() {
|
|
10884
11573
|
try {
|
|
10885
|
-
const pkgPath =
|
|
11574
|
+
const pkgPath = path73.join(packageRoot(), "package.json");
|
|
10886
11575
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
10887
11576
|
return pkg.version ?? "0.0.0";
|
|
10888
11577
|
} catch {
|
|
@@ -10892,7 +11581,7 @@ function cliVersion() {
|
|
|
10892
11581
|
var program = new Command();
|
|
10893
11582
|
function validateRuntimeNodeVersion() {
|
|
10894
11583
|
try {
|
|
10895
|
-
const pkgPath =
|
|
11584
|
+
const pkgPath = path73.join(packageRoot(), "package.json");
|
|
10896
11585
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
10897
11586
|
const requiredRange = pkg.engines?.node;
|
|
10898
11587
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -10906,18 +11595,22 @@ function validateRuntimeNodeVersion() {
|
|
|
10906
11595
|
}
|
|
10907
11596
|
validateRuntimeNodeVersion();
|
|
10908
11597
|
program.name("haus").description("Haus AI workflow CLI").version(cliVersion());
|
|
10909
|
-
program.command("scan").
|
|
10910
|
-
|
|
11598
|
+
program.command("scan").description(
|
|
11599
|
+
"Scan the repo for roles, dependencies, and package manager; writes context-map.json"
|
|
11600
|
+
).option("--json").action(runScan);
|
|
11601
|
+
program.command("recommend").description("Score catalog items against the scanned context and write recommendation.json").option("--json").option(
|
|
10911
11602
|
"--include <ids...>",
|
|
10912
11603
|
"Force opt-in catalog items into the recommendation as manual selections (space- or comma-separated ids)"
|
|
10913
11604
|
).action(runRecommend);
|
|
10914
|
-
program.command("setup-project").option("--json").option(
|
|
11605
|
+
program.command("setup-project").description("Fast full setup: scan, recommend, and optionally apply Claude files").option("--json").option(
|
|
10915
11606
|
"--force",
|
|
10916
11607
|
"Write recommendation.json/haus.lock.json even when zero catalog items matched at all"
|
|
10917
11608
|
).action(runSetupProject);
|
|
10918
|
-
program.command("doctor").
|
|
11609
|
+
program.command("doctor").description(
|
|
11610
|
+
"Validate project setup: hooks contract, managed files, cache freshness, CLI version"
|
|
11611
|
+
).option("--hooks", "Verify .claude/settings.json matches the hook contract").action(runDoctor);
|
|
10919
11612
|
program.command("fetch-refs").description("Fetch and cache llms.txt content from catalog item references").option("--all", "Fetch for all catalog items (default)").option("--id <id>", "Fetch for a single catalog item by id").option("--json", "Emit machine-readable JSON summary to stdout").action(runFetchRefs);
|
|
10920
|
-
program.command("apply").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option(
|
|
11613
|
+
program.command("apply").description("Write recommended catalog items and core Claude files to .claude/").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option(
|
|
10921
11614
|
"--ids <ids...>",
|
|
10922
11615
|
"Install exactly these recommended item ids non-interactively (skill backend; space- or comma-separated)"
|
|
10923
11616
|
).option(
|
|
@@ -10930,33 +11623,33 @@ program.command("apply").option("--dry-run").option("--write").option("--select"
|
|
|
10930
11623
|
"--prune",
|
|
10931
11624
|
"Remove lock-tracked catalog items no longer in the current recommendation (hash-gated, backed up first)"
|
|
10932
11625
|
).action(runApply);
|
|
10933
|
-
program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
|
|
10934
|
-
var backups = program.command("backups");
|
|
11626
|
+
program.command("undo").description("Remove haus-managed project files; preserves user-owned .claude/ content").option("-y, --yes", "Skip confirmation").action(runUndo);
|
|
11627
|
+
var backups = program.command("backups").description("List, restore, or prune backups under .haus-workflow/backups/");
|
|
10935
11628
|
backups.command("list").description("List backups under .haus-workflow/backups/").action(() => runBackups("list"));
|
|
10936
11629
|
backups.command("restore <id>").description("Restore a specific backup by id (see `haus backups list`)").option("-y, --yes", "Skip confirmation").action((id, opts) => runBackups("restore", { id, yes: opts.yes }));
|
|
10937
11630
|
backups.command("prune").description("Delete old backups by age and/or count (requires --older-than or --keep)").option("--older-than <days>", "Remove backups older than N days").option("--keep <n>", "Keep only the N most recent backups").option("-y, --yes", "Skip confirmation").action(
|
|
10938
11631
|
(opts) => runBackups("prune", { olderThan: opts.olderThan, keep: opts.keep, yes: opts.yes })
|
|
10939
11632
|
);
|
|
10940
|
-
program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
|
|
10941
|
-
program.command("init").option("--json").action(runInit);
|
|
11633
|
+
program.command("explain-recommendation").description("Print a per-item breakdown of the current recommendation, with skip reasons").option("--json").action(runExplainRecommendation);
|
|
11634
|
+
program.command("init").description("First-run setup; delegates to setup-project if .haus-workflow/ does not exist").option("--json").action(runInit);
|
|
10942
11635
|
program.command("clone").description("Clone a single git repository by URL").argument("<url>", "Git URL to clone").argument("[dir]", "Target directory (default: repo name derived from the URL)").option("--dry-run", "Print what would happen without cloning").action(
|
|
10943
11636
|
(url, dir, opts) => runClone(url, { dir, dryRun: opts.dryRun })
|
|
10944
11637
|
);
|
|
10945
|
-
program.command("refresh").action(runRefresh);
|
|
10946
|
-
program.command("catalog-audit").action(runCatalogAudit);
|
|
10947
|
-
program.command("validate-catalog").argument("[manifest]").action(runValidateCatalog);
|
|
10948
|
-
program.command("update").option("--check").option("--fast", "With --check, skip per-item content hashing (cheap count+ref only)").option("--from-hook", "Emit JSON for the Claude Code SessionStart hook (silent if up to date)").action(runUpdate);
|
|
11638
|
+
program.command("refresh").description("Re-scan the project, refresh recommendations, and update scan artifacts").action(runRefresh);
|
|
11639
|
+
program.command("catalog-audit").description("Check the bundled catalog manifest for unsupported or forbidden stacks").action(runCatalogAudit);
|
|
11640
|
+
program.command("validate-catalog").description("Validate a catalog manifest at an explicit path").argument("[manifest]").action(runValidateCatalog);
|
|
11641
|
+
program.command("update").description("Sync the remote catalog cache, refresh the lockfile, and check for CLI updates").option("--check").option("--fast", "With --check, skip per-item content hashing (cheap count+ref only)").option("--from-hook", "Emit JSON for the Claude Code SessionStart hook (silent if up to date)").action(runUpdate);
|
|
10949
11642
|
program.command("ci-gate").description(
|
|
10950
11643
|
"Aggregate `doctor` + `decisions check` + `update --check --fast` into one pass/fail gate for CI. Uses the fast/cheap update-check tier (no per-item content hashing) to keep CI runs quick \u2014 run `haus update --check` directly for the fully-hashed tier. Each constituent command remains independently runnable with its own unchanged exit-code contract."
|
|
10951
11644
|
).option("--json", "Emit machine-readable {doctor, decisions, update, ok}").action((opts) => runCiGate(opts));
|
|
10952
|
-
program.command("install").option("--dry-run").option("--force").option("--check", "Exit non-zero if any HAUS-MANAGED file is out of date").option("--postinstall", "Run by the npm postinstall hook; prints a plain-language change notice").action(runInstall);
|
|
11645
|
+
program.command("install").description("Seed ~/.claude/ with Haus-managed skills, agents, and hook settings").option("--dry-run").option("--force").option("--check", "Exit non-zero if any HAUS-MANAGED file is out of date").option("--postinstall", "Run by the npm postinstall hook; prints a plain-language change notice").action(runInstall);
|
|
10953
11646
|
program.command("scaffold [ids...]").description(
|
|
10954
11647
|
"Copy catalog config items (ESLint, Prettier) into the project root. Pass item IDs to scaffold specific items, or omit to scaffold all available config items."
|
|
10955
11648
|
).option("--force", "Overwrite existing files").option("--dry-run", "Preview what would be written without changing anything").option("--root <path>", "Project root (defaults to cwd)", process.cwd()).action(runScaffold);
|
|
10956
|
-
program.command("uninstall").option("--force").option("-y, --yes", "Skip confirmation").action(runUninstallCommand);
|
|
10957
|
-
var guard = program.command("guard");
|
|
10958
|
-
guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
|
|
10959
|
-
guard.command("bash").option("--from-hook").action((opts) => runGuard("bash", opts));
|
|
11649
|
+
program.command("uninstall").description("Remove Haus-managed skills, agents, and hook entries from ~/.claude/").option("--force").option("-y, --yes", "Skip confirmation").action(runUninstallCommand);
|
|
11650
|
+
var guard = program.command("guard").description("PreToolUse hook guards: block dangerous bash commands and sensitive file access");
|
|
11651
|
+
guard.command("file-access").description("Block reads/edits of sensitive paths (run by the Claude Code hook)").option("--from-hook").action((opts) => runGuard("file-access", opts));
|
|
11652
|
+
guard.command("bash").description("Block dangerous shell commands (run by the Claude Code hook)").option("--from-hook").action((opts) => runGuard("bash", opts));
|
|
10960
11653
|
function registerDecisionsCommand(name) {
|
|
10961
11654
|
const cmd = program.command(name).description("Architecture decision records (ADR) gate and drafts");
|
|
10962
11655
|
cmd.command("check").option("--staged", "Check staged changes only").option("--range <range>", "Git revision range (e.g. origin/main..HEAD)").action((opts) => runDecisions("check", void 0, opts));
|
|
@@ -10969,7 +11662,7 @@ function registerDecisionsCommand(name) {
|
|
|
10969
11662
|
}
|
|
10970
11663
|
registerDecisionsCommand("decisions");
|
|
10971
11664
|
registerDecisionsCommand("adr");
|
|
10972
|
-
var workspace = program.command("workspace");
|
|
11665
|
+
var workspace = program.command("workspace").description("Multi-repo workspace commands: discover, setup, link-context, worktree, doctor");
|
|
10973
11666
|
workspace.command("init").option("-y, --yes", "Skip confirmation when haus.workspace.yaml already exists").action((opts) => runWorkspace("init", opts));
|
|
10974
11667
|
workspace.command("discover").description("Auto-find member repos and write/merge haus.workspace.yaml").option("--write", "Persist haus.workspace.yaml (default previews only)").option("--json", "Output the discovered repos and proposed config as JSON").option("--max-depth <n>", "Max directory depth to traverse (default 3)").option("--client <name>", "Set the workspace client name").action((opts) => runWorkspace("discover", opts));
|
|
10975
11668
|
workspace.command("scan").description("Aggregate a cross-repo summary from a fast scan of each repo").option("--json", "Output the written artifact paths as JSON").action((opts) => runWorkspace("scan", opts));
|