@haus-tech/haus-workflow 1.5.8 → 1.7.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 +15 -0
- package/dist/cli.js +1368 -423
- package/library/catalog/manifest.json +56 -403
- 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";
|
|
@@ -4116,23 +4644,33 @@ function formatAge(mtimeMs, now) {
|
|
|
4116
4644
|
if (hours < 48) return `${hours}h ago`;
|
|
4117
4645
|
return `${Math.round(hours / 24)}d ago`;
|
|
4118
4646
|
}
|
|
4119
|
-
async function runList(root) {
|
|
4647
|
+
async function runList(root, json) {
|
|
4120
4648
|
const entries = await listBackups(root);
|
|
4649
|
+
const newestFirst = [...entries].reverse();
|
|
4650
|
+
if (json) {
|
|
4651
|
+
log(
|
|
4652
|
+
JSON.stringify(
|
|
4653
|
+
newestFirst.map(({ id, kind, mtimeMs, absPath }) => ({ id, kind, mtimeMs, absPath })),
|
|
4654
|
+
null,
|
|
4655
|
+
2
|
|
4656
|
+
)
|
|
4657
|
+
);
|
|
4658
|
+
return;
|
|
4659
|
+
}
|
|
4121
4660
|
if (entries.length === 0) {
|
|
4122
4661
|
log("No backups found under .haus-workflow/backups/.");
|
|
4123
4662
|
return;
|
|
4124
4663
|
}
|
|
4125
4664
|
const now = Date.now();
|
|
4126
|
-
const newestFirst = [...entries].reverse();
|
|
4127
4665
|
for (const entry of newestFirst) {
|
|
4128
4666
|
log(`${entry.id} [${entry.kind}] ${formatAge(entry.mtimeMs, now)}`);
|
|
4129
4667
|
}
|
|
4130
4668
|
}
|
|
4131
4669
|
async function isUnsafeWriteTarget(root, target) {
|
|
4132
|
-
const segments =
|
|
4670
|
+
const segments = path26.relative(root, target).split(path26.sep).filter(Boolean);
|
|
4133
4671
|
let current = root;
|
|
4134
4672
|
for (let i = 0; i < segments.length; i++) {
|
|
4135
|
-
current =
|
|
4673
|
+
current = path26.join(current, segments[i]);
|
|
4136
4674
|
const isLastSegment = i === segments.length - 1;
|
|
4137
4675
|
try {
|
|
4138
4676
|
const stat = await fs22.lstat(current);
|
|
@@ -4157,13 +4695,13 @@ async function lstatIfRegularFile(abs) {
|
|
|
4157
4695
|
throw err;
|
|
4158
4696
|
}
|
|
4159
4697
|
}
|
|
4160
|
-
async function restoreLockBackup(root, entry, yes) {
|
|
4698
|
+
async function restoreLockBackup(root, entry, yes, json, dryRun) {
|
|
4161
4699
|
const target = hausPath(root, "haus.lock.json");
|
|
4162
4700
|
if (await isUnsafeWriteTarget(root, target)) {
|
|
4163
4701
|
error(
|
|
4164
4702
|
`Refusing to restore: ${target} (or a parent directory) is a symlink or other non-regular file \u2014 never written through.`
|
|
4165
4703
|
);
|
|
4166
|
-
return false;
|
|
4704
|
+
return { ok: false, restored: 0 };
|
|
4167
4705
|
}
|
|
4168
4706
|
if (await fs22.pathExists(target)) {
|
|
4169
4707
|
const targetStat = await fs22.stat(target);
|
|
@@ -4173,22 +4711,30 @@ async function restoreLockBackup(root, entry, yes) {
|
|
|
4173
4711
|
);
|
|
4174
4712
|
}
|
|
4175
4713
|
}
|
|
4714
|
+
if (dryRun) {
|
|
4715
|
+
if (!json) {
|
|
4716
|
+
log(
|
|
4717
|
+
`[dry-run] Would restore .haus-workflow/haus.lock.json from ${entry.id}. Nothing was changed.`
|
|
4718
|
+
);
|
|
4719
|
+
}
|
|
4720
|
+
return { ok: true, restored: 0, dryRun: true, wouldRestore: [".haus-workflow/haus.lock.json"] };
|
|
4721
|
+
}
|
|
4176
4722
|
if (!yes) {
|
|
4177
4723
|
const ok = await confirm(
|
|
4178
4724
|
`Restore .haus-workflow/haus.lock.json from backup ${entry.id}? This overwrites the current lockfile.`
|
|
4179
4725
|
);
|
|
4180
4726
|
if (!ok) {
|
|
4181
|
-
log("Cancelled.");
|
|
4182
|
-
return false;
|
|
4727
|
+
if (!json) log("Cancelled.");
|
|
4728
|
+
return { ok: false, restored: 0 };
|
|
4183
4729
|
}
|
|
4184
4730
|
}
|
|
4185
4731
|
if (!await lstatIfRegularFile(entry.absPath) || await isUnsafeWriteTarget(root, target)) {
|
|
4186
4732
|
error(`Refusing to restore: ${entry.id} or ${target} changed since being checked.`);
|
|
4187
|
-
return false;
|
|
4733
|
+
return { ok: false, restored: 0 };
|
|
4188
4734
|
}
|
|
4189
4735
|
await fs22.copy(entry.absPath, target, { overwrite: true });
|
|
4190
|
-
log(`Restored .haus-workflow/haus.lock.json from ${entry.id}.`);
|
|
4191
|
-
return true;
|
|
4736
|
+
if (!json) log(`Restored .haus-workflow/haus.lock.json from ${entry.id}.`);
|
|
4737
|
+
return { ok: true, restored: 1 };
|
|
4192
4738
|
}
|
|
4193
4739
|
async function collectFilesRecursive(dir) {
|
|
4194
4740
|
const files = [];
|
|
@@ -4196,7 +4742,7 @@ async function collectFilesRecursive(dir) {
|
|
|
4196
4742
|
const skippedOther = [];
|
|
4197
4743
|
const names = await fs22.readdir(dir);
|
|
4198
4744
|
for (const name of names) {
|
|
4199
|
-
const abs =
|
|
4745
|
+
const abs = path26.join(dir, name);
|
|
4200
4746
|
const stat = await fs22.lstat(abs);
|
|
4201
4747
|
if (stat.isSymbolicLink()) {
|
|
4202
4748
|
skippedLinks.push(abs);
|
|
@@ -4215,27 +4761,27 @@ async function collectFilesRecursive(dir) {
|
|
|
4215
4761
|
}
|
|
4216
4762
|
return { files, skippedLinks, skippedOther };
|
|
4217
4763
|
}
|
|
4218
|
-
async function restoreDirBackup(root, entry, yes) {
|
|
4764
|
+
async function restoreDirBackup(root, entry, yes, json, dryRun) {
|
|
4219
4765
|
const { files, skippedLinks, skippedOther } = await collectFilesRecursive(entry.absPath);
|
|
4220
4766
|
if (skippedLinks.length > 0) {
|
|
4221
4767
|
warn(
|
|
4222
|
-
`Skipped ${skippedLinks.length} symlink(s) inside backup ${entry.id} \u2014 never followed: ${skippedLinks.map((abs) =>
|
|
4768
|
+
`Skipped ${skippedLinks.length} symlink(s) inside backup ${entry.id} \u2014 never followed: ${skippedLinks.map((abs) => path26.relative(entry.absPath, abs)).join(", ")}`
|
|
4223
4769
|
);
|
|
4224
4770
|
}
|
|
4225
4771
|
if (skippedOther.length > 0) {
|
|
4226
4772
|
warn(
|
|
4227
|
-
`Skipped ${skippedOther.length} non-regular file(s) inside backup ${entry.id} (not restorable): ${skippedOther.map((abs) =>
|
|
4773
|
+
`Skipped ${skippedOther.length} non-regular file(s) inside backup ${entry.id} (not restorable): ${skippedOther.map((abs) => path26.relative(entry.absPath, abs)).join(", ")}`
|
|
4228
4774
|
);
|
|
4229
4775
|
}
|
|
4230
4776
|
if (files.length === 0) {
|
|
4231
|
-
log(`Backup ${entry.id} contains no files.`);
|
|
4232
|
-
return false;
|
|
4777
|
+
if (!json) log(`Backup ${entry.id} contains no files.`);
|
|
4778
|
+
return { ok: false, restored: 0 };
|
|
4233
4779
|
}
|
|
4234
4780
|
const safeFiles = [];
|
|
4235
4781
|
const unsafeTargets = [];
|
|
4236
4782
|
for (const abs of files) {
|
|
4237
|
-
const rel =
|
|
4238
|
-
const target =
|
|
4783
|
+
const rel = path26.relative(entry.absPath, abs);
|
|
4784
|
+
const target = path26.join(root, rel);
|
|
4239
4785
|
if (await isUnsafeWriteTarget(root, target)) unsafeTargets.push(rel);
|
|
4240
4786
|
else safeFiles.push(abs);
|
|
4241
4787
|
}
|
|
@@ -4245,21 +4791,21 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
4245
4791
|
);
|
|
4246
4792
|
}
|
|
4247
4793
|
if (safeFiles.length === 0) {
|
|
4248
|
-
log(`Backup ${entry.id} has no restorable targets.`);
|
|
4249
|
-
return false;
|
|
4794
|
+
if (!json) log(`Backup ${entry.id} has no restorable targets.`);
|
|
4795
|
+
return { ok: false, restored: 0 };
|
|
4250
4796
|
}
|
|
4251
4797
|
const stale = [];
|
|
4252
4798
|
const vanished = [];
|
|
4253
4799
|
const toRestore = [];
|
|
4254
4800
|
for (const abs of safeFiles) {
|
|
4255
|
-
const rel =
|
|
4801
|
+
const rel = path26.relative(entry.absPath, abs);
|
|
4256
4802
|
const backupFileStat = await lstatIfRegularFile(abs);
|
|
4257
4803
|
if (!backupFileStat) {
|
|
4258
4804
|
vanished.push(rel);
|
|
4259
4805
|
continue;
|
|
4260
4806
|
}
|
|
4261
4807
|
toRestore.push(abs);
|
|
4262
|
-
const target =
|
|
4808
|
+
const target = path26.join(root, rel);
|
|
4263
4809
|
if (await fs22.pathExists(target)) {
|
|
4264
4810
|
const targetStat = await fs22.stat(target);
|
|
4265
4811
|
if (targetStat.mtimeMs > backupFileStat.mtimeMs) {
|
|
@@ -4277,8 +4823,8 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
4277
4823
|
);
|
|
4278
4824
|
}
|
|
4279
4825
|
if (toRestore.length === 0) {
|
|
4280
|
-
log(`Backup ${entry.id} has no restorable files left after re-validation.`);
|
|
4281
|
-
return false;
|
|
4826
|
+
if (!json) log(`Backup ${entry.id} has no restorable files left after re-validation.`);
|
|
4827
|
+
return { ok: false, restored: 0 };
|
|
4282
4828
|
}
|
|
4283
4829
|
if (stale.length > 0) {
|
|
4284
4830
|
const detail = stale.map(
|
|
@@ -4288,25 +4834,34 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
4288
4834
|
`${stale.length} file(s) at the restore destination are newer than backup ${entry.id} and will be overwritten: ${detail}`
|
|
4289
4835
|
);
|
|
4290
4836
|
}
|
|
4837
|
+
if (dryRun) {
|
|
4838
|
+
const rels = toRestore.map((abs) => path26.relative(entry.absPath, abs));
|
|
4839
|
+
if (!json) {
|
|
4840
|
+
log(`[dry-run] Would restore ${rels.length} file(s) from ${entry.id}:`);
|
|
4841
|
+
for (const rel of rels) log(` ${rel}`);
|
|
4842
|
+
log("[dry-run] Nothing was changed.");
|
|
4843
|
+
}
|
|
4844
|
+
return { ok: true, restored: 0, dryRun: true, wouldRestore: rels };
|
|
4845
|
+
}
|
|
4291
4846
|
if (!yes) {
|
|
4292
4847
|
const ok = await confirm(
|
|
4293
4848
|
`Restore ${toRestore.length} file(s) from backup ${entry.id} to their original paths?`
|
|
4294
4849
|
);
|
|
4295
4850
|
if (!ok) {
|
|
4296
|
-
log("Cancelled.");
|
|
4297
|
-
return false;
|
|
4851
|
+
if (!json) log("Cancelled.");
|
|
4852
|
+
return { ok: false, restored: 0 };
|
|
4298
4853
|
}
|
|
4299
4854
|
}
|
|
4300
4855
|
let restoredCount = 0;
|
|
4301
4856
|
const skippedAtWrite = [];
|
|
4302
4857
|
for (const abs of toRestore) {
|
|
4303
|
-
const rel =
|
|
4304
|
-
const target =
|
|
4858
|
+
const rel = path26.relative(entry.absPath, abs);
|
|
4859
|
+
const target = path26.join(root, rel);
|
|
4305
4860
|
if (!await lstatIfRegularFile(abs) || await isUnsafeWriteTarget(root, target)) {
|
|
4306
4861
|
skippedAtWrite.push(rel);
|
|
4307
4862
|
continue;
|
|
4308
4863
|
}
|
|
4309
|
-
await fs22.ensureDir(
|
|
4864
|
+
await fs22.ensureDir(path26.dirname(target));
|
|
4310
4865
|
await fs22.copy(abs, target, { overwrite: true });
|
|
4311
4866
|
restoredCount++;
|
|
4312
4867
|
}
|
|
@@ -4315,23 +4870,20 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
4315
4870
|
`Skipped ${skippedAtWrite.length} file(s) that changed since being checked, right before restoring: ${skippedAtWrite.join(", ")}`
|
|
4316
4871
|
);
|
|
4317
4872
|
}
|
|
4318
|
-
log(`Restored ${restoredCount} file(s) from ${entry.id}.`);
|
|
4319
|
-
return restoredCount > 0;
|
|
4873
|
+
if (!json) log(`Restored ${restoredCount} file(s) from ${entry.id}.`);
|
|
4874
|
+
return { ok: restoredCount > 0, restored: restoredCount };
|
|
4320
4875
|
}
|
|
4321
|
-
async function runRestore(root, id, yes) {
|
|
4876
|
+
async function runRestore(root, id, yes, json, dryRun) {
|
|
4322
4877
|
const entries = await listBackups(root);
|
|
4323
4878
|
const entry = entries.find((e) => e.id === id);
|
|
4324
4879
|
if (!entry) {
|
|
4325
4880
|
error(`No backup found with id "${id}". Run \`haus backups list\` to see available backups.`);
|
|
4326
|
-
return false;
|
|
4881
|
+
return { ok: false, restored: 0 };
|
|
4327
4882
|
}
|
|
4328
|
-
return entry.kind === "lock" ? restoreLockBackup(root, entry, yes) : restoreDirBackup(root, entry, yes);
|
|
4883
|
+
return entry.kind === "lock" ? restoreLockBackup(root, entry, yes, json, dryRun) : restoreDirBackup(root, entry, yes, json, dryRun);
|
|
4329
4884
|
}
|
|
4330
4885
|
async function runPrune(root, opts) {
|
|
4331
|
-
|
|
4332
|
-
error("Specify --older-than <days> or --keep <n> \u2014 refusing to prune with no bound.");
|
|
4333
|
-
return false;
|
|
4334
|
-
}
|
|
4886
|
+
const json = Boolean(opts.json);
|
|
4335
4887
|
const entries = await listBackups(root);
|
|
4336
4888
|
const now = Date.now();
|
|
4337
4889
|
const toRemove = /* @__PURE__ */ new Set();
|
|
@@ -4344,11 +4896,32 @@ async function runPrune(root, opts) {
|
|
|
4344
4896
|
if (excess > 0) for (const entry of entries.slice(0, excess)) toRemove.add(entry.id);
|
|
4345
4897
|
}
|
|
4346
4898
|
if (toRemove.size === 0) {
|
|
4347
|
-
log("Nothing to prune.");
|
|
4348
|
-
return
|
|
4899
|
+
if (!json) log("Nothing to prune.");
|
|
4900
|
+
return {
|
|
4901
|
+
ok: true,
|
|
4902
|
+
removed: [],
|
|
4903
|
+
failed: [],
|
|
4904
|
+
kept: entries.length,
|
|
4905
|
+
...opts.dryRun ? { dryRun: true, wouldRemove: [] } : {}
|
|
4906
|
+
};
|
|
4349
4907
|
}
|
|
4350
4908
|
const removeList = entries.filter((e) => toRemove.has(e.id));
|
|
4351
4909
|
const keepCount = entries.length - removeList.length;
|
|
4910
|
+
if (opts.dryRun) {
|
|
4911
|
+
if (!json) {
|
|
4912
|
+
log(`[dry-run] Would remove ${removeList.length} backup(s), keeping ${keepCount}:`);
|
|
4913
|
+
for (const e of removeList) log(` ${e.id}`);
|
|
4914
|
+
log("[dry-run] Nothing was changed.");
|
|
4915
|
+
}
|
|
4916
|
+
return {
|
|
4917
|
+
ok: true,
|
|
4918
|
+
removed: [],
|
|
4919
|
+
failed: [],
|
|
4920
|
+
kept: keepCount,
|
|
4921
|
+
dryRun: true,
|
|
4922
|
+
wouldRemove: removeList.map((e) => e.id)
|
|
4923
|
+
};
|
|
4924
|
+
}
|
|
4352
4925
|
if (!opts.yes) {
|
|
4353
4926
|
const ok = await confirm(
|
|
4354
4927
|
`Remove ${removeList.length} backup(s)?
|
|
@@ -4356,8 +4929,8 @@ async function runPrune(root, opts) {
|
|
|
4356
4929
|
Keeping ${keepCount}.`
|
|
4357
4930
|
);
|
|
4358
4931
|
if (!ok) {
|
|
4359
|
-
log("Cancelled.");
|
|
4360
|
-
return false;
|
|
4932
|
+
if (!json) log("Cancelled.");
|
|
4933
|
+
return { ok: false, removed: [], failed: [], kept: entries.length, cancelled: true };
|
|
4361
4934
|
}
|
|
4362
4935
|
}
|
|
4363
4936
|
const removed = [];
|
|
@@ -4370,37 +4943,52 @@ Keeping ${keepCount}.`
|
|
|
4370
4943
|
failed.push({ id: entry.id, message: err instanceof Error ? err.message : String(err) });
|
|
4371
4944
|
}
|
|
4372
4945
|
}
|
|
4373
|
-
log(`Removed ${removed.length} backup(s), kept ${entries.length - removed.length}.`);
|
|
4946
|
+
if (!json) log(`Removed ${removed.length} backup(s), kept ${entries.length - removed.length}.`);
|
|
4374
4947
|
if (failed.length > 0) {
|
|
4375
4948
|
warn(
|
|
4376
4949
|
`Failed to remove ${failed.length} backup(s): ${failed.map((f) => `${f.id} (${f.message})`).join(", ")}`
|
|
4377
4950
|
);
|
|
4378
|
-
return false;
|
|
4379
4951
|
}
|
|
4380
|
-
return
|
|
4952
|
+
return {
|
|
4953
|
+
ok: failed.length === 0,
|
|
4954
|
+
removed,
|
|
4955
|
+
failed,
|
|
4956
|
+
kept: entries.length - removed.length
|
|
4957
|
+
};
|
|
4381
4958
|
}
|
|
4382
4959
|
async function runBackups(action, opts = {}) {
|
|
4383
4960
|
const root = opts.root ?? process.cwd();
|
|
4961
|
+
const json = Boolean(opts.json);
|
|
4384
4962
|
if (action === "list") {
|
|
4385
|
-
await runList(root);
|
|
4963
|
+
await runList(root, json);
|
|
4386
4964
|
return;
|
|
4387
4965
|
}
|
|
4388
4966
|
if (action === "restore") {
|
|
4389
4967
|
if (!opts.id) {
|
|
4968
|
+
if (json) log(JSON.stringify({ ok: false, error: "Usage: haus backups restore <id>" }));
|
|
4390
4969
|
error("Usage: haus backups restore <id>");
|
|
4391
4970
|
process.exitCode = 1;
|
|
4392
4971
|
return;
|
|
4393
4972
|
}
|
|
4394
|
-
|
|
4973
|
+
const result2 = await runRestore(root, opts.id, Boolean(opts.yes), json, Boolean(opts.dryRun));
|
|
4974
|
+
if (json) log(JSON.stringify({ id: opts.id, ...result2 }, null, 2));
|
|
4975
|
+
if (!result2.ok) process.exitCode = 1;
|
|
4976
|
+
return;
|
|
4977
|
+
}
|
|
4978
|
+
if (opts.olderThan === void 0 && opts.keep === void 0) {
|
|
4979
|
+
const message = "Specify --older-than <days> or --keep <n> \u2014 refusing to prune with no bound.";
|
|
4980
|
+
if (json) log(JSON.stringify({ ok: false, error: message }));
|
|
4981
|
+
error(message);
|
|
4982
|
+
process.exitCode = 1;
|
|
4395
4983
|
return;
|
|
4396
4984
|
}
|
|
4397
4985
|
let olderThanDays;
|
|
4398
4986
|
if (opts.olderThan !== void 0) {
|
|
4399
4987
|
const n = Number(opts.olderThan);
|
|
4400
4988
|
if (!Number.isFinite(n) || n < 0) {
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
);
|
|
4989
|
+
const message = `Invalid --older-than value "${opts.olderThan}" \u2014 expected a non-negative number of days.`;
|
|
4990
|
+
if (json) log(JSON.stringify({ ok: false, error: message }));
|
|
4991
|
+
error(message);
|
|
4404
4992
|
process.exitCode = 1;
|
|
4405
4993
|
return;
|
|
4406
4994
|
}
|
|
@@ -4410,13 +4998,23 @@ async function runBackups(action, opts = {}) {
|
|
|
4410
4998
|
if (opts.keep !== void 0) {
|
|
4411
4999
|
const n = Number(opts.keep);
|
|
4412
5000
|
if (!Number.isFinite(n) || n < 0 || !Number.isInteger(n)) {
|
|
4413
|
-
|
|
5001
|
+
const message = `Invalid --keep value "${opts.keep}" \u2014 expected a non-negative integer.`;
|
|
5002
|
+
if (json) log(JSON.stringify({ ok: false, error: message }));
|
|
5003
|
+
error(message);
|
|
4414
5004
|
process.exitCode = 1;
|
|
4415
5005
|
return;
|
|
4416
5006
|
}
|
|
4417
5007
|
keep = n;
|
|
4418
5008
|
}
|
|
4419
|
-
|
|
5009
|
+
const result = await runPrune(root, {
|
|
5010
|
+
olderThanDays,
|
|
5011
|
+
keep,
|
|
5012
|
+
yes: Boolean(opts.yes),
|
|
5013
|
+
json,
|
|
5014
|
+
dryRun: Boolean(opts.dryRun)
|
|
5015
|
+
});
|
|
5016
|
+
if (json) log(JSON.stringify(result, null, 2));
|
|
5017
|
+
if (!result.ok) process.exitCode = 1;
|
|
4420
5018
|
}
|
|
4421
5019
|
|
|
4422
5020
|
// src/commands/catalog-audit.ts
|
|
@@ -4445,18 +5043,19 @@ async function runCatalogAudit() {
|
|
|
4445
5043
|
}
|
|
4446
5044
|
|
|
4447
5045
|
// src/commands/doctor.ts
|
|
4448
|
-
import
|
|
5046
|
+
import path40 from "node:path";
|
|
4449
5047
|
import fs30 from "fs-extra";
|
|
4450
5048
|
|
|
4451
5049
|
// src/claude/skill-reference-check.ts
|
|
4452
|
-
import
|
|
5050
|
+
import path27 from "node:path";
|
|
4453
5051
|
import fg4 from "fast-glob";
|
|
4454
5052
|
import fs23 from "fs-extra";
|
|
4455
5053
|
var REFERENCE_PATTERN = /\b([a-z][a-z0-9]*(?:-[a-z0-9]+)+):([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\b(?!:\/\/)/g;
|
|
4456
5054
|
function stripFencedCodeBlocks(text) {
|
|
4457
5055
|
return text.replace(/```[\s\S]*?```/g, (block) => block.replace(/[^\n]/g, " "));
|
|
4458
5056
|
}
|
|
4459
|
-
async function findUnresolvedSkillReferences(root) {
|
|
5057
|
+
async function findUnresolvedSkillReferences(root, options = {}) {
|
|
5058
|
+
const resolvableNamespaces = new Set(options.pluginNamespaces ?? []);
|
|
4460
5059
|
const skillsRoot = claudePath(root, "skills");
|
|
4461
5060
|
if (!await fs23.pathExists(skillsRoot)) return [];
|
|
4462
5061
|
const skillDirs = (await fs23.readdir(skillsRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory() && entry.name !== "shared").map((entry) => entry.name);
|
|
@@ -4465,15 +5064,16 @@ async function findUnresolvedSkillReferences(root) {
|
|
|
4465
5064
|
const issues = [];
|
|
4466
5065
|
for (const rel of files) {
|
|
4467
5066
|
if (rel.startsWith("shared/")) continue;
|
|
4468
|
-
const abs =
|
|
5067
|
+
const abs = path27.join(skillsRoot, rel);
|
|
4469
5068
|
const raw = await fs23.readFile(abs, "utf8");
|
|
4470
5069
|
const lines = stripFencedCodeBlocks(raw).split("\n");
|
|
4471
5070
|
lines.forEach((line2, idx) => {
|
|
4472
5071
|
for (const match of line2.matchAll(REFERENCE_PATTERN)) {
|
|
4473
|
-
const [reference, , bareName] = match;
|
|
5072
|
+
const [reference, namespace, bareName] = match;
|
|
4474
5073
|
if (!reference || !bareName) continue;
|
|
5074
|
+
if (namespace && resolvableNamespaces.has(namespace)) continue;
|
|
4475
5075
|
issues.push({
|
|
4476
|
-
file:
|
|
5076
|
+
file: path27.join(".claude/skills", rel),
|
|
4477
5077
|
line: idx + 1,
|
|
4478
5078
|
reference,
|
|
4479
5079
|
bareName,
|
|
@@ -4486,11 +5086,11 @@ async function findUnresolvedSkillReferences(root) {
|
|
|
4486
5086
|
}
|
|
4487
5087
|
|
|
4488
5088
|
// src/decisions/doctor.ts
|
|
4489
|
-
import
|
|
5089
|
+
import path32 from "node:path";
|
|
4490
5090
|
import fs27 from "fs-extra";
|
|
4491
5091
|
|
|
4492
5092
|
// src/decisions/check.ts
|
|
4493
|
-
import
|
|
5093
|
+
import path31 from "node:path";
|
|
4494
5094
|
import fs26 from "fs-extra";
|
|
4495
5095
|
|
|
4496
5096
|
// src/decisions/dep-bump.ts
|
|
@@ -4550,7 +5150,7 @@ function isDependencyBumpOnly(beforeText, afterText) {
|
|
|
4550
5150
|
|
|
4551
5151
|
// src/decisions/diff.ts
|
|
4552
5152
|
init_exec();
|
|
4553
|
-
import
|
|
5153
|
+
import path28 from "node:path";
|
|
4554
5154
|
function isPackageJsonPath(file) {
|
|
4555
5155
|
return file === "package.json" || file.endsWith("/package.json");
|
|
4556
5156
|
}
|
|
@@ -4611,7 +5211,7 @@ async function resolvePackageJsonPair(root, file, opts) {
|
|
|
4611
5211
|
return { before: before2, after: after2 };
|
|
4612
5212
|
}
|
|
4613
5213
|
const before = await readBlob(root, `${range}:${file}`) ?? empty;
|
|
4614
|
-
const after = await readText(
|
|
5214
|
+
const after = await readText(path28.join(root, file)) ?? empty;
|
|
4615
5215
|
return { before, after };
|
|
4616
5216
|
}
|
|
4617
5217
|
async function collectDiffStats(root, opts) {
|
|
@@ -4682,21 +5282,21 @@ function firstMatchingRegex(filePath, patterns) {
|
|
|
4682
5282
|
}
|
|
4683
5283
|
|
|
4684
5284
|
// src/decisions/paths.ts
|
|
4685
|
-
import
|
|
5285
|
+
import path29 from "node:path";
|
|
4686
5286
|
import fs24 from "fs-extra";
|
|
4687
5287
|
async function resolveDecisionsDir(root) {
|
|
4688
|
-
const primary =
|
|
5288
|
+
const primary = path29.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
4689
5289
|
if (await fs24.pathExists(primary)) return primary;
|
|
4690
|
-
const legacy =
|
|
5290
|
+
const legacy = path29.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
4691
5291
|
if (await fs24.pathExists(legacy)) return legacy;
|
|
4692
5292
|
return primary;
|
|
4693
5293
|
}
|
|
4694
5294
|
function relativeDecisionPath(decisionsDir, root) {
|
|
4695
|
-
return
|
|
5295
|
+
return path29.relative(root, decisionsDir).replace(/\\/g, "/");
|
|
4696
5296
|
}
|
|
4697
5297
|
|
|
4698
5298
|
// src/decisions/validate.ts
|
|
4699
|
-
import
|
|
5299
|
+
import path30 from "node:path";
|
|
4700
5300
|
import fs25 from "fs-extra";
|
|
4701
5301
|
function validateDecisionContent(content2) {
|
|
4702
5302
|
const errors = [];
|
|
@@ -4726,14 +5326,14 @@ function decisionNumberFromFile(name) {
|
|
|
4726
5326
|
return match?.[1] ?? null;
|
|
4727
5327
|
}
|
|
4728
5328
|
async function validateIndexSync(decisionsDir, decisionFiles) {
|
|
4729
|
-
const readmePath =
|
|
5329
|
+
const readmePath = path30.join(decisionsDir, "README.md");
|
|
4730
5330
|
if (!await fs25.pathExists(readmePath)) {
|
|
4731
5331
|
return ["docs/decisions/README.md is missing"];
|
|
4732
5332
|
}
|
|
4733
5333
|
const readme = await readText(readmePath) ?? "";
|
|
4734
5334
|
const errors = [];
|
|
4735
5335
|
for (const file of decisionFiles) {
|
|
4736
|
-
const num = decisionNumberFromFile(
|
|
5336
|
+
const num = decisionNumberFromFile(path30.basename(file));
|
|
4737
5337
|
if (num && !indexReferencesNumber(readme, num)) {
|
|
4738
5338
|
errors.push(`README.md missing index row for ADR-${num}`);
|
|
4739
5339
|
}
|
|
@@ -4828,22 +5428,22 @@ async function checkSatisfaction(input2) {
|
|
|
4828
5428
|
const errors = [];
|
|
4829
5429
|
const decisionsPrefix = `${input2.decisionsRel}/`;
|
|
4830
5430
|
const newDecisionFiles = input2.changedFiles.filter(
|
|
4831
|
-
(f) => f.startsWith(decisionsPrefix) && isDecisionFileName(
|
|
5431
|
+
(f) => f.startsWith(decisionsPrefix) && isDecisionFileName(path31.basename(f))
|
|
4832
5432
|
);
|
|
4833
5433
|
if (newDecisionFiles.length === 0) {
|
|
4834
5434
|
errors.push(`no new or updated decision under ${input2.decisionsRel}/`);
|
|
4835
5435
|
return errors;
|
|
4836
5436
|
}
|
|
4837
|
-
const decisionsDir =
|
|
5437
|
+
const decisionsDir = path31.join(input2.root, input2.decisionsRel);
|
|
4838
5438
|
for (const rel of newDecisionFiles) {
|
|
4839
|
-
const abs =
|
|
5439
|
+
const abs = path31.join(input2.root, rel);
|
|
4840
5440
|
const content2 = await readText(abs) ?? "";
|
|
4841
5441
|
const valid = validateDecisionContent(content2);
|
|
4842
5442
|
if (!valid.ok) errors.push(...valid.errors.map((e) => `${rel}: ${e}`));
|
|
4843
5443
|
}
|
|
4844
5444
|
const indexErrors = await validateIndexSync(
|
|
4845
5445
|
decisionsDir,
|
|
4846
|
-
newDecisionFiles.map((f) =>
|
|
5446
|
+
newDecisionFiles.map((f) => path31.basename(f))
|
|
4847
5447
|
);
|
|
4848
5448
|
errors.push(...indexErrors);
|
|
4849
5449
|
return errors;
|
|
@@ -4899,7 +5499,7 @@ ${opts.commitMessages ?? ""}`;
|
|
|
4899
5499
|
changedFiles: stats.files
|
|
4900
5500
|
});
|
|
4901
5501
|
if (satisfactionErrors.length > 0 && decisionsRel !== legacyRel) {
|
|
4902
|
-
const legacyExists = await fs26.pathExists(
|
|
5502
|
+
const legacyExists = await fs26.pathExists(path31.join(root, legacyRel));
|
|
4903
5503
|
if (legacyExists && !await fs26.pathExists(decisionsDir)) {
|
|
4904
5504
|
satisfactionErrors = await checkSatisfaction({
|
|
4905
5505
|
root,
|
|
@@ -4930,8 +5530,8 @@ async function listDecisionNumbers(decisionsDir) {
|
|
|
4930
5530
|
// src/decisions/doctor.ts
|
|
4931
5531
|
async function auditDecisionsLayout(root) {
|
|
4932
5532
|
const findings = [];
|
|
4933
|
-
const primary =
|
|
4934
|
-
const legacy =
|
|
5533
|
+
const primary = path32.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
5534
|
+
const legacy = path32.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
4935
5535
|
const hasPrimary = await fs27.pathExists(primary);
|
|
4936
5536
|
const hasLegacy = await fs27.pathExists(legacy);
|
|
4937
5537
|
if (!hasPrimary && hasLegacy) {
|
|
@@ -4946,7 +5546,7 @@ async function auditDecisionsLayout(root) {
|
|
|
4946
5546
|
if (!hasPrimary) return findings;
|
|
4947
5547
|
const decisionsDir = await resolveDecisionsDir(root);
|
|
4948
5548
|
const rel = relativeDecisionPath(decisionsDir, root);
|
|
4949
|
-
const readmePath =
|
|
5549
|
+
const readmePath = path32.join(decisionsDir, "README.md");
|
|
4950
5550
|
if (!await fs27.pathExists(readmePath)) {
|
|
4951
5551
|
findings.push({
|
|
4952
5552
|
level: "advisory",
|
|
@@ -4971,7 +5571,7 @@ async function auditDecisionsLayout(root) {
|
|
|
4971
5571
|
});
|
|
4972
5572
|
}
|
|
4973
5573
|
}
|
|
4974
|
-
const claude = await readText(
|
|
5574
|
+
const claude = await readText(path32.join(root, "CLAUDE.md"));
|
|
4975
5575
|
if (claude && !claude.includes(`@${rel}/README.md`)) {
|
|
4976
5576
|
findings.push({
|
|
4977
5577
|
level: "advisory",
|
|
@@ -5002,11 +5602,11 @@ async function auditDecisionsLayout(root) {
|
|
|
5002
5602
|
}
|
|
5003
5603
|
|
|
5004
5604
|
// src/scanner/read-context.ts
|
|
5005
|
-
import
|
|
5605
|
+
import path39 from "node:path";
|
|
5006
5606
|
import fs29 from "fs-extra";
|
|
5007
5607
|
|
|
5008
5608
|
// src/commands/workspace/config.ts
|
|
5009
|
-
import
|
|
5609
|
+
import path33 from "node:path";
|
|
5010
5610
|
import YAML from "yaml";
|
|
5011
5611
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
5012
5612
|
function parseWorkspaceConfig(text) {
|
|
@@ -5029,11 +5629,11 @@ function parseWorkspaceConfig(text) {
|
|
|
5029
5629
|
};
|
|
5030
5630
|
}
|
|
5031
5631
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
5032
|
-
return parseWorkspaceConfig(await readText(
|
|
5632
|
+
return parseWorkspaceConfig(await readText(path33.join(workspaceRoot, WORKSPACE_FILE)));
|
|
5033
5633
|
}
|
|
5034
5634
|
|
|
5035
5635
|
// src/workspace/members.ts
|
|
5036
|
-
import
|
|
5636
|
+
import path34 from "node:path";
|
|
5037
5637
|
var REPOS_MANIFEST_FILE = "repos.manifest.json";
|
|
5038
5638
|
var REPOS_LOCAL_FILE = "repos.local.json";
|
|
5039
5639
|
var MemberConfigError = class extends Error {
|
|
@@ -5046,14 +5646,14 @@ var MemberConfigError = class extends Error {
|
|
|
5046
5646
|
};
|
|
5047
5647
|
function resolveAbsPath(workspaceRoot, folder, overrides) {
|
|
5048
5648
|
const override = overrides[folder];
|
|
5049
|
-
if (override) return
|
|
5050
|
-
return
|
|
5649
|
+
if (override) return path34.resolve(workspaceRoot, override);
|
|
5650
|
+
return path34.resolve(workspaceRoot, folder);
|
|
5051
5651
|
}
|
|
5052
5652
|
function isSelfMember(workspaceRoot, member) {
|
|
5053
|
-
return
|
|
5653
|
+
return path34.resolve(member.absPath) === path34.resolve(workspaceRoot);
|
|
5054
5654
|
}
|
|
5055
5655
|
async function readPathOverrides(workspaceRoot) {
|
|
5056
|
-
const filePath =
|
|
5656
|
+
const filePath = path34.join(workspaceRoot, REPOS_LOCAL_FILE);
|
|
5057
5657
|
const result = await readJsonDetailed(filePath);
|
|
5058
5658
|
if (result.status === "missing") return {};
|
|
5059
5659
|
if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
|
|
@@ -5069,7 +5669,7 @@ async function readPathOverrides(workspaceRoot) {
|
|
|
5069
5669
|
return overrides;
|
|
5070
5670
|
}
|
|
5071
5671
|
async function readFromWorkspaceYaml(workspaceRoot, overrides) {
|
|
5072
|
-
const filePath =
|
|
5672
|
+
const filePath = path34.join(workspaceRoot, WORKSPACE_FILE);
|
|
5073
5673
|
const text = await readText(filePath);
|
|
5074
5674
|
if (text === void 0) return void 0;
|
|
5075
5675
|
const config = parseWorkspaceConfig(text);
|
|
@@ -5082,7 +5682,7 @@ async function readFromWorkspaceYaml(workspaceRoot, overrides) {
|
|
|
5082
5682
|
}));
|
|
5083
5683
|
}
|
|
5084
5684
|
async function readFromManifest(workspaceRoot, overrides) {
|
|
5085
|
-
const filePath =
|
|
5685
|
+
const filePath = path34.join(workspaceRoot, REPOS_MANIFEST_FILE);
|
|
5086
5686
|
const result = await readJsonDetailed(filePath);
|
|
5087
5687
|
if (result.status === "missing") return void 0;
|
|
5088
5688
|
if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
|
|
@@ -5124,11 +5724,11 @@ async function readMembers(rootInfo) {
|
|
|
5124
5724
|
}
|
|
5125
5725
|
|
|
5126
5726
|
// src/scanner/scan-project.ts
|
|
5127
|
-
import
|
|
5727
|
+
import path38 from "node:path";
|
|
5128
5728
|
|
|
5129
5729
|
// src/utils/git-root.ts
|
|
5130
5730
|
init_exec();
|
|
5131
|
-
import
|
|
5731
|
+
import path35 from "node:path";
|
|
5132
5732
|
function fallback(start) {
|
|
5133
5733
|
return {
|
|
5134
5734
|
cwd: start,
|
|
@@ -5144,11 +5744,11 @@ function fallback(start) {
|
|
|
5144
5744
|
async function resolveAbsoluteGitPath(flag, cwd, repoRoot) {
|
|
5145
5745
|
const withFormat = await runGit(["rev-parse", "--path-format=absolute", flag], { cwd });
|
|
5146
5746
|
const raw = withFormat.exitCode === 0 ? withFormat.stdout.trim() : "";
|
|
5147
|
-
if (raw &&
|
|
5747
|
+
if (raw && path35.isAbsolute(raw)) return path35.resolve(raw);
|
|
5148
5748
|
const plain = await runGit(["rev-parse", flag], { cwd });
|
|
5149
5749
|
const plainRaw = plain.exitCode === 0 ? plain.stdout.trim() : "";
|
|
5150
5750
|
if (!plainRaw) return "";
|
|
5151
|
-
return
|
|
5751
|
+
return path35.isAbsolute(plainRaw) ? path35.resolve(plainRaw) : path35.resolve(repoRoot, plainRaw);
|
|
5152
5752
|
}
|
|
5153
5753
|
async function resolveRoots(start) {
|
|
5154
5754
|
const cwd = start ?? process.cwd();
|
|
@@ -5157,15 +5757,15 @@ async function resolveRoots(start) {
|
|
|
5157
5757
|
if (toplevel.exitCode !== 0) return fallback(cwd);
|
|
5158
5758
|
const toplevelRaw = toplevel.stdout.trim();
|
|
5159
5759
|
if (!toplevelRaw) return fallback(cwd);
|
|
5160
|
-
const repoRoot =
|
|
5760
|
+
const repoRoot = path35.resolve(toplevelRaw);
|
|
5161
5761
|
const [gitDir, gitCommonDir] = await Promise.all([
|
|
5162
5762
|
resolveAbsoluteGitPath("--git-dir", cwd, repoRoot),
|
|
5163
5763
|
resolveAbsoluteGitPath("--git-common-dir", cwd, repoRoot)
|
|
5164
5764
|
]);
|
|
5165
5765
|
if (!gitDir || !gitCommonDir) return fallback(cwd);
|
|
5166
5766
|
const isLinkedWorktree = gitDir !== gitCommonDir;
|
|
5167
|
-
const mainRoot = isLinkedWorktree &&
|
|
5168
|
-
const worktreeName = isLinkedWorktree ?
|
|
5767
|
+
const mainRoot = isLinkedWorktree && path35.basename(gitCommonDir) === ".git" ? path35.dirname(gitCommonDir) : repoRoot;
|
|
5768
|
+
const worktreeName = isLinkedWorktree ? path35.basename(gitDir) : null;
|
|
5169
5769
|
return {
|
|
5170
5770
|
cwd,
|
|
5171
5771
|
repoRoot,
|
|
@@ -5210,7 +5810,7 @@ function resolveMajorVersion(range) {
|
|
|
5210
5810
|
}
|
|
5211
5811
|
|
|
5212
5812
|
// src/scanner/detect-package-manager.ts
|
|
5213
|
-
import
|
|
5813
|
+
import path36 from "node:path";
|
|
5214
5814
|
import fs28 from "fs-extra";
|
|
5215
5815
|
function detectPackageManager(root, packageManagerField) {
|
|
5216
5816
|
const field = String(packageManagerField ?? "").trim();
|
|
@@ -5229,9 +5829,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
5229
5829
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
5230
5830
|
return "unknown";
|
|
5231
5831
|
}
|
|
5232
|
-
if (fs28.existsSync(
|
|
5233
|
-
if (fs28.existsSync(
|
|
5234
|
-
if (fs28.existsSync(
|
|
5832
|
+
if (fs28.existsSync(path36.join(root, "yarn.lock"))) return "yarn";
|
|
5833
|
+
if (fs28.existsSync(path36.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
5834
|
+
if (fs28.existsSync(path36.join(root, "package-lock.json"))) return "npm";
|
|
5235
5835
|
return "unknown";
|
|
5236
5836
|
}
|
|
5237
5837
|
|
|
@@ -5456,7 +6056,7 @@ function runDetection(ctx, rules = STACK_RULES) {
|
|
|
5456
6056
|
|
|
5457
6057
|
// src/scanner/render.ts
|
|
5458
6058
|
import { readFile } from "node:fs/promises";
|
|
5459
|
-
import
|
|
6059
|
+
import path37 from "node:path";
|
|
5460
6060
|
async function buildContentBlob(root, files) {
|
|
5461
6061
|
const candidates = files.filter(
|
|
5462
6062
|
(f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
|
|
@@ -5464,7 +6064,7 @@ async function buildContentBlob(root, files) {
|
|
|
5464
6064
|
const slice = candidates.slice(0, 300);
|
|
5465
6065
|
const parts = await mapWithConcurrency(slice, async (rel) => {
|
|
5466
6066
|
try {
|
|
5467
|
-
return await readFile(
|
|
6067
|
+
return await readFile(path37.join(root, rel), "utf8");
|
|
5468
6068
|
} catch {
|
|
5469
6069
|
return "";
|
|
5470
6070
|
}
|
|
@@ -5556,8 +6156,8 @@ var SAFE_FILES = [
|
|
|
5556
6156
|
"Gemfile"
|
|
5557
6157
|
];
|
|
5558
6158
|
async function scanProject(root) {
|
|
5559
|
-
const pkg = await readJson(
|
|
5560
|
-
const composer = await readJson(
|
|
6159
|
+
const pkg = await readJson(path38.join(root, "package.json"));
|
|
6160
|
+
const composer = await readJson(path38.join(root, "composer.json"));
|
|
5561
6161
|
const files = await listFiles(root, SAFE_FILES);
|
|
5562
6162
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
5563
6163
|
const depVersions = dependencyVersionMap(pkg, composer);
|
|
@@ -5587,7 +6187,7 @@ async function scanProject(root) {
|
|
|
5587
6187
|
crossRepoHints.push("Monorepo orchestration detected");
|
|
5588
6188
|
const unsupportedSignals = collectUnsupportedSignals(safeFiles);
|
|
5589
6189
|
const detectionStatus = computeDetectionStatus(roles, stacks, unsupportedSignals);
|
|
5590
|
-
const repoNameFallback = pkg?.name ? String(pkg.name) :
|
|
6190
|
+
const repoNameFallback = pkg?.name ? String(pkg.name) : path38.basename((await resolveRoots(root)).mainRoot);
|
|
5591
6191
|
const context = {
|
|
5592
6192
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5593
6193
|
repoName: repoNameFallback,
|
|
@@ -5609,10 +6209,10 @@ async function scanProject(root) {
|
|
|
5609
6209
|
// src/scanner/read-context.ts
|
|
5610
6210
|
async function isCacheFresh(root, cachePath) {
|
|
5611
6211
|
const markerPaths = [
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
6212
|
+
path39.join(root, "package.json"),
|
|
6213
|
+
path39.join(root, "composer.json"),
|
|
6214
|
+
path39.join(root, WORKSPACE_FILE),
|
|
6215
|
+
path39.join(root, REPOS_MANIFEST_FILE)
|
|
5616
6216
|
];
|
|
5617
6217
|
const cacheStat = await fs29.stat(cachePath);
|
|
5618
6218
|
for (const markerPath of markerPaths) {
|
|
@@ -5664,12 +6264,19 @@ async function runDoctor(options) {
|
|
|
5664
6264
|
const root = process.cwd();
|
|
5665
6265
|
if (options?.hooks) {
|
|
5666
6266
|
const hooks2 = await verifyProjectSettingsHooksContract(root);
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
6267
|
+
const hooksOk = !hooks2.skipped && hooks2.ok;
|
|
6268
|
+
if (options.json) {
|
|
6269
|
+
log(
|
|
6270
|
+
JSON.stringify(
|
|
6271
|
+
{ ok: hooksOk, skipped: Boolean(hooks2.skipped), message: hooks2.message },
|
|
6272
|
+
null,
|
|
6273
|
+
2
|
|
6274
|
+
)
|
|
6275
|
+
);
|
|
6276
|
+
if (!hooksOk) process.exitCode = 1;
|
|
5670
6277
|
return;
|
|
5671
6278
|
}
|
|
5672
|
-
if (!
|
|
6279
|
+
if (!hooksOk) {
|
|
5673
6280
|
error(`Haus doctor --hooks: ${hooks2.message}`);
|
|
5674
6281
|
process.exitCode = 1;
|
|
5675
6282
|
return;
|
|
@@ -5718,7 +6325,7 @@ async function runDoctor(options) {
|
|
|
5718
6325
|
} else {
|
|
5719
6326
|
ok(`- HOOKS OK: ${hooks.message}`);
|
|
5720
6327
|
}
|
|
5721
|
-
const rootClaudeMdPath =
|
|
6328
|
+
const rootClaudeMdPath = path40.join(root, "CLAUDE.md");
|
|
5722
6329
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
5723
6330
|
if (!rootClaudeMdContent) {
|
|
5724
6331
|
flag(
|
|
@@ -5761,9 +6368,9 @@ async function runDoctor(options) {
|
|
|
5761
6368
|
ok("- CLAUDE.md: import block present");
|
|
5762
6369
|
}
|
|
5763
6370
|
for (const target of actualTargets) {
|
|
5764
|
-
const resolved =
|
|
5765
|
-
const relFromRoot =
|
|
5766
|
-
const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${
|
|
6371
|
+
const resolved = path40.resolve(root, target);
|
|
6372
|
+
const relFromRoot = path40.relative(root, resolved);
|
|
6373
|
+
const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${path40.sep}`) || path40.isAbsolute(relFromRoot);
|
|
5767
6374
|
if (escapesRoot || !await fs30.pathExists(resolved)) {
|
|
5768
6375
|
flag(
|
|
5769
6376
|
`- CLAUDE.md import: @${target} does not resolve (run \`haus apply --write\`)`,
|
|
@@ -5797,8 +6404,8 @@ async function runDoctor(options) {
|
|
|
5797
6404
|
} else {
|
|
5798
6405
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
5799
6406
|
const bodyContent = workflowContent.slice(firstLine.length + 1);
|
|
5800
|
-
const cachePath =
|
|
5801
|
-
const bundledPath =
|
|
6407
|
+
const cachePath = path40.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
6408
|
+
const bundledPath = path40.join(
|
|
5802
6409
|
packageRoot(),
|
|
5803
6410
|
"library",
|
|
5804
6411
|
"global",
|
|
@@ -5809,10 +6416,17 @@ async function runDoctor(options) {
|
|
|
5809
6416
|
const templateContent = await readText(templatePath);
|
|
5810
6417
|
const verdict = checkManagedTamper(bodyContent, storedHashMatch?.[1], templateContent ?? null);
|
|
5811
6418
|
if (verdict.status === "tampered" || verdict.status === "legacy-diverged") {
|
|
6419
|
+
let diffNote = "";
|
|
6420
|
+
if (templateContent != null) {
|
|
6421
|
+
const summary = summarizeDiff(
|
|
6422
|
+
createUnifiedDiff(".haus-workflow/WORKFLOW.md", bodyContent, templateContent)
|
|
6423
|
+
);
|
|
6424
|
+
diffNote = ` \u2014 restoring would discard ${summary.deletions} local line(s) and add ${summary.additions}`;
|
|
6425
|
+
}
|
|
5812
6426
|
flag(
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
"haus apply --write --force"
|
|
6427
|
+
`- .haus-workflow/WORKFLOW.md: modified locally${diffNote} (preview: \`haus apply --dry-run\`; restore: \`haus apply --write --force\`)`,
|
|
6428
|
+
`The workflow standard file was edited after haus wrote it${diffNote}`,
|
|
6429
|
+
"preview with `haus apply --dry-run`, then `haus apply --write --force` to restore"
|
|
5816
6430
|
);
|
|
5817
6431
|
} else if (verdict.status === "stale") {
|
|
5818
6432
|
suggest(
|
|
@@ -5847,7 +6461,7 @@ async function runDoctor(options) {
|
|
|
5847
6461
|
}
|
|
5848
6462
|
}
|
|
5849
6463
|
if (workflowExists) {
|
|
5850
|
-
const prettierIgnore = await readText(
|
|
6464
|
+
const prettierIgnore = await readText(path40.join(root, ".prettierignore")) ?? "";
|
|
5851
6465
|
const lines = prettierIgnore.split("\n");
|
|
5852
6466
|
const missing = IGNORED_PATHS.filter((p) => !prettierIgnoreProtects(lines, p));
|
|
5853
6467
|
if (missing.length > 0) {
|
|
@@ -5887,7 +6501,50 @@ async function runDoctor(options) {
|
|
|
5887
6501
|
if (finding.level === "ok") ok(finding.line);
|
|
5888
6502
|
else suggest(finding.line, finding.sentence, finding.fix);
|
|
5889
6503
|
}
|
|
5890
|
-
const
|
|
6504
|
+
const pluginPins = await loadCatalogPluginPins(root);
|
|
6505
|
+
const pluginState = await probePluginEnv();
|
|
6506
|
+
const activePins = pluginPins.filter((pin) => isPluginActive(pluginState, pin));
|
|
6507
|
+
const activeNamespaces = activePins.map((pin) => pin.plugin);
|
|
6508
|
+
if (pluginPins.length === 0) {
|
|
6509
|
+
} else if (!pluginState.available) {
|
|
6510
|
+
suggest(
|
|
6511
|
+
"- GLOBAL PLUGINS: not checked (the `claude` CLI is not on PATH)",
|
|
6512
|
+
"Claude Code's CLI isn't on PATH, so haus can't verify the global plugin(s) it manages",
|
|
6513
|
+
"install Claude Code, then run `haus install`"
|
|
6514
|
+
);
|
|
6515
|
+
} else {
|
|
6516
|
+
const pending = planPluginActions(pluginPins, pluginState).filter(
|
|
6517
|
+
(action) => action.kind !== "up-to-date"
|
|
6518
|
+
);
|
|
6519
|
+
if (pending.length > 0) {
|
|
6520
|
+
suggest(
|
|
6521
|
+
`- GLOBAL PLUGINS: ${pending.length} plugin(s) not at the pinned state (${pending.map((a) => `${a.pin.id}: ${a.reason}`).join("; ")})`,
|
|
6522
|
+
`${pending.length} haus-managed global plugin(s) are missing, disabled, or off the pinned ref`,
|
|
6523
|
+
"haus update (or `haus install` outside a project)"
|
|
6524
|
+
);
|
|
6525
|
+
} else {
|
|
6526
|
+
ok(`- GLOBAL PLUGINS: OK (${pluginPins.map((p) => `${p.id}@${p.ref}`).join(", ")})`);
|
|
6527
|
+
}
|
|
6528
|
+
}
|
|
6529
|
+
if (activePins.length > 0) {
|
|
6530
|
+
const lockEntries = await readJson(hausPath(root, "haus.lock.json"));
|
|
6531
|
+
const superseded = (lockEntries ?? []).map((entry) => entry.id).filter(
|
|
6532
|
+
(id) => typeof id === "string" && isSupersededByPlugin(
|
|
6533
|
+
id,
|
|
6534
|
+
activePins.map((pin) => pin.id)
|
|
6535
|
+
)
|
|
6536
|
+
);
|
|
6537
|
+
if (superseded.length > 0) {
|
|
6538
|
+
suggest(
|
|
6539
|
+
`- GLOBAL PLUGINS: ${superseded.length} installed item(s) are superseded by the global plugin (${superseded.slice(0, 3).join(", ")}${superseded.length > 3 ? ", \u2026" : ""})`,
|
|
6540
|
+
`${superseded.length} project skill(s) duplicate what the global plugin already provides`,
|
|
6541
|
+
"haus update (removes the superseded copies)"
|
|
6542
|
+
);
|
|
6543
|
+
}
|
|
6544
|
+
}
|
|
6545
|
+
const skillRefIssues = await findUnresolvedSkillReferences(root, {
|
|
6546
|
+
pluginNamespaces: activeNamespaces
|
|
6547
|
+
});
|
|
5891
6548
|
if (skillRefIssues.length > 0) {
|
|
5892
6549
|
const prefixBug = skillRefIssues.filter((i) => i.bareInstalled);
|
|
5893
6550
|
const missingDep = skillRefIssues.filter((i) => !i.bareInstalled);
|
|
@@ -5951,7 +6608,41 @@ async function runDoctor(options) {
|
|
|
5951
6608
|
}
|
|
5952
6609
|
}
|
|
5953
6610
|
}
|
|
5954
|
-
|
|
6611
|
+
{
|
|
6612
|
+
const lockRows = await readJson(
|
|
6613
|
+
hausPath(root, "haus.lock.json")
|
|
6614
|
+
);
|
|
6615
|
+
if (lockRows && lockRows.length > 0) {
|
|
6616
|
+
const catalogItems = await loadCatalog(root);
|
|
6617
|
+
const byId = new Map(catalogItems.map((item) => [item.id, item]));
|
|
6618
|
+
const activePinIds = activePins.map((pin) => pin.id);
|
|
6619
|
+
const lingering = [];
|
|
6620
|
+
for (const row of lockRows) {
|
|
6621
|
+
if (typeof row.id !== "string") continue;
|
|
6622
|
+
if (isSupersededByPlugin(row.id, activePinIds)) continue;
|
|
6623
|
+
const manifestItem = byId.get(row.id);
|
|
6624
|
+
if (manifestItem && manifestItem.reviewStatus !== "deprecated") continue;
|
|
6625
|
+
let onDisk = false;
|
|
6626
|
+
for (const rel of row.paths ?? []) {
|
|
6627
|
+
const abs = resolveContainedPath(root, rel);
|
|
6628
|
+
if (abs !== null && await fs30.pathExists(abs)) {
|
|
6629
|
+
onDisk = true;
|
|
6630
|
+
break;
|
|
6631
|
+
}
|
|
6632
|
+
}
|
|
6633
|
+
if (onDisk) lingering.push(row.id);
|
|
6634
|
+
}
|
|
6635
|
+
if (lingering.length > 0) {
|
|
6636
|
+
const idList = `${lingering.slice(0, 3).join(", ")}${lingering.length > 3 ? ", \u2026" : ""}`;
|
|
6637
|
+
suggest(
|
|
6638
|
+
`- CATALOG ITEMS: ${lingering.length} installed item(s) are removed or deprecated in the current catalog (${idList}) \u2014 pre-0.30.0 CLIs never pruned these`,
|
|
6639
|
+
`${lingering.length} installed item(s) no longer exist in the catalog (removed or deprecated)`,
|
|
6640
|
+
"haus update (prunes them when unmodified; see docs/runbook.md for locally-edited copies)"
|
|
6641
|
+
);
|
|
6642
|
+
}
|
|
6643
|
+
}
|
|
6644
|
+
}
|
|
6645
|
+
const pkgJson = await readJson(path40.join(packageRoot(), "package.json"));
|
|
5955
6646
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
5956
6647
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
5957
6648
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -5966,6 +6657,10 @@ async function runDoctor(options) {
|
|
|
5966
6657
|
ok(`- CLI: ${currentVersion} (version check unavailable)`);
|
|
5967
6658
|
}
|
|
5968
6659
|
if (attention.length > 0) process.exitCode = 1;
|
|
6660
|
+
if (options?.json) {
|
|
6661
|
+
log(JSON.stringify({ ok: attention.length === 0, attention, suggestions, detail }, null, 2));
|
|
6662
|
+
return;
|
|
6663
|
+
}
|
|
5969
6664
|
if (attention.length === 0) {
|
|
5970
6665
|
log("\u2705 Your project is set up and healthy.");
|
|
5971
6666
|
} else {
|
|
@@ -5984,7 +6679,7 @@ async function runDoctor(options) {
|
|
|
5984
6679
|
}
|
|
5985
6680
|
|
|
5986
6681
|
// src/commands/update.ts
|
|
5987
|
-
import
|
|
6682
|
+
import path43 from "node:path";
|
|
5988
6683
|
|
|
5989
6684
|
// src/claude/refresh-project.ts
|
|
5990
6685
|
import fs31 from "fs-extra";
|
|
@@ -5996,12 +6691,16 @@ async function isHausProject(root) {
|
|
|
5996
6691
|
}
|
|
5997
6692
|
async function refreshProjectApply(root) {
|
|
5998
6693
|
if (!await isHausProject(root)) return [];
|
|
5999
|
-
return writeClaudeFiles(root, false, void 0, {
|
|
6694
|
+
return writeClaudeFiles(root, false, void 0, {
|
|
6695
|
+
refillConfig: false,
|
|
6696
|
+
// Superseded copies are removed in the same pass that re-applies the catalog.
|
|
6697
|
+
activePluginIds: await activeGlobalPluginIds(root)
|
|
6698
|
+
});
|
|
6000
6699
|
}
|
|
6001
6700
|
|
|
6002
6701
|
// src/install/apply.ts
|
|
6003
6702
|
import crypto2 from "node:crypto";
|
|
6004
|
-
import
|
|
6703
|
+
import path41 from "node:path";
|
|
6005
6704
|
import fs32 from "fs-extra";
|
|
6006
6705
|
|
|
6007
6706
|
// src/install/header.ts
|
|
@@ -6095,7 +6794,7 @@ function destFileMissing(destPath) {
|
|
|
6095
6794
|
}
|
|
6096
6795
|
function sourceVersion() {
|
|
6097
6796
|
try {
|
|
6098
|
-
const pkgPath =
|
|
6797
|
+
const pkgPath = path41.join(packageRoot(), "package.json");
|
|
6099
6798
|
const pkg = JSON.parse(fs32.readFileSync(pkgPath, "utf8"));
|
|
6100
6799
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
6101
6800
|
} catch {
|
|
@@ -6103,49 +6802,49 @@ function sourceVersion() {
|
|
|
6103
6802
|
}
|
|
6104
6803
|
}
|
|
6105
6804
|
function globalSrcDir() {
|
|
6106
|
-
return
|
|
6805
|
+
return path41.join(packageRoot(), "library", "global");
|
|
6107
6806
|
}
|
|
6108
6807
|
function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
6109
|
-
const skillDir =
|
|
6808
|
+
const skillDir = path41.join(skillsDir, skillName);
|
|
6110
6809
|
const entries = [];
|
|
6111
6810
|
for (const rel of fs32.readdirSync(skillDir, { recursive: true })) {
|
|
6112
6811
|
if (rel === "SKILL.md") continue;
|
|
6113
|
-
const abs =
|
|
6812
|
+
const abs = path41.join(skillDir, rel);
|
|
6114
6813
|
if (!fs32.statSync(abs).isFile()) continue;
|
|
6115
|
-
const posixRel = rel.split(
|
|
6814
|
+
const posixRel = rel.split(path41.sep).join("/");
|
|
6116
6815
|
entries.push({
|
|
6117
6816
|
stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
|
|
6118
|
-
srcRelPath:
|
|
6119
|
-
destPath:
|
|
6817
|
+
srcRelPath: path41.join("library", "global", "skills", skillName, rel),
|
|
6818
|
+
destPath: path41.join(claudeDir, "skills", skillName, rel)
|
|
6120
6819
|
});
|
|
6121
6820
|
}
|
|
6122
6821
|
return entries;
|
|
6123
6822
|
}
|
|
6124
6823
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
6125
6824
|
const entries = [];
|
|
6126
|
-
const skillsDir =
|
|
6825
|
+
const skillsDir = path41.join(srcDir, "skills");
|
|
6127
6826
|
if (fs32.pathExistsSync(skillsDir)) {
|
|
6128
6827
|
for (const skillName of fs32.readdirSync(skillsDir)) {
|
|
6129
|
-
const skillFile =
|
|
6828
|
+
const skillFile = path41.join(skillsDir, skillName, "SKILL.md");
|
|
6130
6829
|
if (fs32.pathExistsSync(skillFile)) {
|
|
6131
6830
|
entries.push({
|
|
6132
6831
|
stableId: `skill.${skillName}`,
|
|
6133
|
-
srcRelPath:
|
|
6134
|
-
destPath:
|
|
6832
|
+
srcRelPath: path41.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
6833
|
+
destPath: path41.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
6135
6834
|
});
|
|
6136
6835
|
entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
|
|
6137
6836
|
}
|
|
6138
6837
|
}
|
|
6139
6838
|
}
|
|
6140
|
-
const commandsDir =
|
|
6839
|
+
const commandsDir = path41.join(srcDir, "commands");
|
|
6141
6840
|
if (fs32.pathExistsSync(commandsDir)) {
|
|
6142
6841
|
for (const fileName of fs32.readdirSync(commandsDir)) {
|
|
6143
6842
|
if (!fileName.endsWith(".md")) continue;
|
|
6144
6843
|
const commandName = fileName.slice(0, -".md".length);
|
|
6145
6844
|
entries.push({
|
|
6146
6845
|
stableId: `command.${commandName}`,
|
|
6147
|
-
srcRelPath:
|
|
6148
|
-
destPath:
|
|
6846
|
+
srcRelPath: path41.join("library", "global", "commands", fileName),
|
|
6847
|
+
destPath: path41.join(claudeDir, "commands", fileName)
|
|
6149
6848
|
});
|
|
6150
6849
|
}
|
|
6151
6850
|
}
|
|
@@ -6171,7 +6870,7 @@ async function applyInstall(options = {}) {
|
|
|
6171
6870
|
};
|
|
6172
6871
|
const manifestFiles = [];
|
|
6173
6872
|
for (const entry of sourceFiles) {
|
|
6174
|
-
const srcPath =
|
|
6873
|
+
const srcPath = path41.join(packageRoot(), entry.srcRelPath);
|
|
6175
6874
|
const rawContent = await readText(srcPath);
|
|
6176
6875
|
if (rawContent === void 0) {
|
|
6177
6876
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -6232,7 +6931,7 @@ async function applyInstall(options = {}) {
|
|
|
6232
6931
|
}
|
|
6233
6932
|
let mergedSettings;
|
|
6234
6933
|
if (!check) {
|
|
6235
|
-
const fragmentPath =
|
|
6934
|
+
const fragmentPath = path41.join(srcDir, "settings-fragments", "hooks.json");
|
|
6236
6935
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
6237
6936
|
const settings = await readSettings();
|
|
6238
6937
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -6262,9 +6961,12 @@ async function applyInstall(options = {}) {
|
|
|
6262
6961
|
}
|
|
6263
6962
|
if (!dryRun && !check && mergedSettings) {
|
|
6264
6963
|
await writeSettings(mergedSettings);
|
|
6265
|
-
const manifest = buildManifest(
|
|
6266
|
-
|
|
6267
|
-
|
|
6964
|
+
const manifest = buildManifest(
|
|
6965
|
+
source,
|
|
6966
|
+
manifestFiles,
|
|
6967
|
+
[.../* @__PURE__ */ new Set([...existingManifest?.hooks ?? [], ...result.hookIds])],
|
|
6968
|
+
existingManifest?.plugins
|
|
6969
|
+
);
|
|
6268
6970
|
await writeManifest(manifest);
|
|
6269
6971
|
}
|
|
6270
6972
|
return result;
|
|
@@ -6321,7 +7023,7 @@ function summarizeLockDiff(before, after) {
|
|
|
6321
7023
|
|
|
6322
7024
|
// src/update/lockfile.ts
|
|
6323
7025
|
import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
|
|
6324
|
-
import
|
|
7026
|
+
import path42 from "node:path";
|
|
6325
7027
|
async function readLockSummary(root) {
|
|
6326
7028
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
6327
7029
|
const catalogRef = lock.find((item) => item.catalogRef)?.catalogRef ?? null;
|
|
@@ -6359,7 +7061,7 @@ async function applyLock(root) {
|
|
|
6359
7061
|
try {
|
|
6360
7062
|
const backupDir = hausPath(root, "backups");
|
|
6361
7063
|
await mkdir(backupDir, { recursive: true });
|
|
6362
|
-
await copyFile(lockPath,
|
|
7064
|
+
await copyFile(lockPath, path42.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
6363
7065
|
} catch {
|
|
6364
7066
|
}
|
|
6365
7067
|
const enriched = await Promise.all(
|
|
@@ -6381,13 +7083,38 @@ function diffLock(before, after) {
|
|
|
6381
7083
|
}
|
|
6382
7084
|
async function hasLocalOverrides(root) {
|
|
6383
7085
|
try {
|
|
6384
|
-
await readFile2(
|
|
7086
|
+
await readFile2(path42.join(root, ".claude", "settings.json"), "utf8");
|
|
6385
7087
|
return true;
|
|
6386
7088
|
} catch {
|
|
6387
7089
|
return false;
|
|
6388
7090
|
}
|
|
6389
7091
|
}
|
|
6390
7092
|
|
|
7093
|
+
// src/update/release-notes.ts
|
|
7094
|
+
var RELEASES_API_BASE = "https://api.github.com/repos/WeAreHausTech/haus-workflow/releases/tags";
|
|
7095
|
+
var MAX_NOTE_LINES = 40;
|
|
7096
|
+
async function fetchReleaseNotes(version) {
|
|
7097
|
+
try {
|
|
7098
|
+
const headers = await getGithubApiHeaders();
|
|
7099
|
+
const res = await fetch(`${RELEASES_API_BASE}/v${version}`, {
|
|
7100
|
+
headers,
|
|
7101
|
+
// Same bound npm-version.ts uses — best-effort must also mean bounded-time.
|
|
7102
|
+
signal: AbortSignal.timeout(8e3)
|
|
7103
|
+
});
|
|
7104
|
+
if (!res.ok) return null;
|
|
7105
|
+
const release = await res.json();
|
|
7106
|
+
if (typeof release.body !== "string" || release.body.trim().length === 0) return null;
|
|
7107
|
+
const lines = release.body.trim().split("\n");
|
|
7108
|
+
if (lines.length <= MAX_NOTE_LINES) return lines.join("\n");
|
|
7109
|
+
return [
|
|
7110
|
+
...lines.slice(0, MAX_NOTE_LINES),
|
|
7111
|
+
`\u2026 (${lines.length - MAX_NOTE_LINES} more lines)`
|
|
7112
|
+
].join("\n");
|
|
7113
|
+
} catch {
|
|
7114
|
+
return null;
|
|
7115
|
+
}
|
|
7116
|
+
}
|
|
7117
|
+
|
|
6391
7118
|
// src/commands/update.ts
|
|
6392
7119
|
var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
6393
7120
|
async function runUpdate(options) {
|
|
@@ -6397,15 +7124,26 @@ async function runUpdate(options) {
|
|
|
6397
7124
|
return;
|
|
6398
7125
|
}
|
|
6399
7126
|
if (options.check) {
|
|
6400
|
-
const pkgJson2 = await readJson(
|
|
7127
|
+
const pkgJson2 = await readJson(path43.join(packageRoot(), "package.json"));
|
|
6401
7128
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
6402
|
-
const [
|
|
7129
|
+
const [
|
|
7130
|
+
status,
|
|
7131
|
+
npmVersion,
|
|
7132
|
+
latestCatalogTag,
|
|
7133
|
+
globalInstallDrift,
|
|
7134
|
+
catalogItems,
|
|
7135
|
+
lockItems,
|
|
7136
|
+
globalPluginDrift
|
|
7137
|
+
] = await Promise.all([
|
|
6403
7138
|
options.fast ? fastLockStatus(root) : checkLock(root),
|
|
6404
7139
|
fetchNpmVersionStatus(currentVersion2),
|
|
6405
7140
|
fetchLatestCatalogTag(),
|
|
6406
7141
|
options.fast ? Promise.resolve(null) : detectGlobalInstallDrift(),
|
|
6407
7142
|
loadCatalog(root),
|
|
6408
|
-
readJson(hausPath(root, "haus.lock.json"))
|
|
7143
|
+
readJson(hausPath(root, "haus.lock.json")),
|
|
7144
|
+
// Same tier split as globalInstallDrift: --fast is the cheap probe the
|
|
7145
|
+
// SessionStart hook uses and must not shell out to the `claude` CLI.
|
|
7146
|
+
options.fast ? Promise.resolve(null) : detectGlobalPluginDrift(root)
|
|
6409
7147
|
]);
|
|
6410
7148
|
const formerIdMigrations = findFormerIdMigrations(lockItems ?? [], catalogItems);
|
|
6411
7149
|
const installedRef = options.fast ? status.catalogRef : status.catalogRef ?? "main";
|
|
@@ -6420,6 +7158,7 @@ async function runUpdate(options) {
|
|
|
6420
7158
|
catalogRefBehind,
|
|
6421
7159
|
formerIdMigrations,
|
|
6422
7160
|
globalInstallDrift,
|
|
7161
|
+
globalPluginDrift,
|
|
6423
7162
|
localOverrides: await hasLocalOverrides(root),
|
|
6424
7163
|
summary: diffGeneratedFiles(),
|
|
6425
7164
|
npmVersion
|
|
@@ -6429,17 +7168,24 @@ async function runUpdate(options) {
|
|
|
6429
7168
|
)
|
|
6430
7169
|
);
|
|
6431
7170
|
const hasHashDrift = !options.fast && status.count > 0 && "ok" in status && !status.ok;
|
|
6432
|
-
if (hasHashDrift || formerIdMigrations.length > 0 || globalInstallDrift === true) {
|
|
7171
|
+
if (hasHashDrift || formerIdMigrations.length > 0 || globalInstallDrift === true || globalPluginDrift === true) {
|
|
6433
7172
|
process.exitCode = 1;
|
|
6434
7173
|
}
|
|
6435
7174
|
return;
|
|
6436
7175
|
}
|
|
6437
|
-
const pkgJson = await readJson(
|
|
7176
|
+
const pkgJson = await readJson(path43.join(packageRoot(), "package.json"));
|
|
6438
7177
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
6439
7178
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
6440
7179
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
6441
7180
|
log(`npm update available: ${currentVersion} \u2192 ${npmStatus.latest}`);
|
|
6442
7181
|
log(`Run: npm install -g ${NPM_PACKAGE_NAME2}`);
|
|
7182
|
+
const notes = await fetchReleaseNotes(npmStatus.latest);
|
|
7183
|
+
if (notes !== null) {
|
|
7184
|
+
log(`
|
|
7185
|
+
What changed in ${npmStatus.latest}:`);
|
|
7186
|
+
log(notes);
|
|
7187
|
+
log("");
|
|
7188
|
+
}
|
|
6443
7189
|
} else if (npmStatus.latest !== null) {
|
|
6444
7190
|
log(`npm package up to date: ${currentVersion}`);
|
|
6445
7191
|
}
|
|
@@ -6467,6 +7213,7 @@ async function runUpdate(options) {
|
|
|
6467
7213
|
warn(formatGithubRateLimitMessage(sync.rateLimit));
|
|
6468
7214
|
}
|
|
6469
7215
|
await refreshGlobalInstall();
|
|
7216
|
+
await ensureCatalogPinnedPlugins(root);
|
|
6470
7217
|
await refreshProjectFiles(root);
|
|
6471
7218
|
const { after } = await applyLock(root);
|
|
6472
7219
|
log(diffLock(lockBefore, after));
|
|
@@ -6477,7 +7224,7 @@ async function runFromHookCheck(root) {
|
|
|
6477
7224
|
try {
|
|
6478
7225
|
const summary = await readLockSummary(root);
|
|
6479
7226
|
if (summary.count === 0) return;
|
|
6480
|
-
const pkgJson = await readJson(
|
|
7227
|
+
const pkgJson = await readJson(path43.join(packageRoot(), "package.json"));
|
|
6481
7228
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
6482
7229
|
const [npmVersion, latestCatalogTag] = await Promise.all([
|
|
6483
7230
|
fetchNpmVersionStatus(currentVersion),
|
|
@@ -6535,6 +7282,26 @@ async function refreshGlobalInstall() {
|
|
|
6535
7282
|
warn(`Could not refresh ~/.claude/: ${err instanceof Error ? err.message : String(err)}`);
|
|
6536
7283
|
}
|
|
6537
7284
|
}
|
|
7285
|
+
async function ensureCatalogPinnedPlugins(root) {
|
|
7286
|
+
try {
|
|
7287
|
+
const pins = await loadCatalogPluginPins(root);
|
|
7288
|
+
const ensured = await ensureGlobalPlugins(pins);
|
|
7289
|
+
await recordManifestPlugins(
|
|
7290
|
+
pins.filter((pin) => ensured.ensured.includes(pin.id)).map((pin) => ({ ...pin, installedAt: (/* @__PURE__ */ new Date()).toISOString() }))
|
|
7291
|
+
);
|
|
7292
|
+
} catch (err) {
|
|
7293
|
+
warn(`Could not update global plugins: ${err instanceof Error ? err.message : String(err)}`);
|
|
7294
|
+
}
|
|
7295
|
+
}
|
|
7296
|
+
async function detectGlobalPluginDrift(root) {
|
|
7297
|
+
try {
|
|
7298
|
+
const pins = await loadCatalogPluginPins(root);
|
|
7299
|
+
const state = await probePluginEnv();
|
|
7300
|
+
return pluginDriftStatus(planPluginActions(pins, state), { matchRef: true });
|
|
7301
|
+
} catch {
|
|
7302
|
+
return null;
|
|
7303
|
+
}
|
|
7304
|
+
}
|
|
6538
7305
|
async function detectGlobalInstallDrift() {
|
|
6539
7306
|
try {
|
|
6540
7307
|
const result = await applyInstall({ check: true });
|
|
@@ -6628,7 +7395,7 @@ async function runCiGate(options = {}) {
|
|
|
6628
7395
|
init_exec();
|
|
6629
7396
|
import { existsSync as existsSync2 } from "node:fs";
|
|
6630
7397
|
import { readdir } from "node:fs/promises";
|
|
6631
|
-
import
|
|
7398
|
+
import path44 from "node:path";
|
|
6632
7399
|
var GIT_LOCATION_VARS = [
|
|
6633
7400
|
"GIT_DIR",
|
|
6634
7401
|
"GIT_WORK_TREE",
|
|
@@ -6688,11 +7455,11 @@ async function runClone(url, opts = {}) {
|
|
|
6688
7455
|
process.exitCode = 1;
|
|
6689
7456
|
return;
|
|
6690
7457
|
}
|
|
6691
|
-
const target =
|
|
7458
|
+
const target = path44.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
6692
7459
|
if (existsSync2(target) && !await isEmptyDirectory(target)) {
|
|
6693
7460
|
const check = await checkExistingTarget(target, url);
|
|
6694
7461
|
if (check.kind === "matches") {
|
|
6695
|
-
log(`\u2022 ${
|
|
7462
|
+
log(`\u2022 ${path44.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
|
|
6696
7463
|
return;
|
|
6697
7464
|
}
|
|
6698
7465
|
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 +7568,7 @@ async function runDecisionsGuard(options = {}) {
|
|
|
6801
7568
|
}
|
|
6802
7569
|
|
|
6803
7570
|
// src/commands/decisions.ts
|
|
6804
|
-
import
|
|
7571
|
+
import path45 from "node:path";
|
|
6805
7572
|
|
|
6806
7573
|
// src/decisions/next-number.ts
|
|
6807
7574
|
async function nextDecisionNumber(root) {
|
|
@@ -6911,7 +7678,7 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
6911
7678
|
return;
|
|
6912
7679
|
}
|
|
6913
7680
|
if (sub === "validate") {
|
|
6914
|
-
const target = arg ?
|
|
7681
|
+
const target = arg ? path45.resolve(root, arg) : void 0;
|
|
6915
7682
|
if (!target) {
|
|
6916
7683
|
error("usage: haus decisions validate <path-to-decision.md>");
|
|
6917
7684
|
process.exitCode = 1;
|
|
@@ -7180,7 +7947,7 @@ async function runGuard(kind, _options) {
|
|
|
7180
7947
|
}
|
|
7181
7948
|
|
|
7182
7949
|
// src/commands/init.ts
|
|
7183
|
-
import
|
|
7950
|
+
import path47 from "node:path";
|
|
7184
7951
|
import fs33 from "fs-extra";
|
|
7185
7952
|
|
|
7186
7953
|
// src/recommender/git-signal.ts
|
|
@@ -7223,6 +7990,7 @@ async function readChangedFiles(root) {
|
|
|
7223
7990
|
|
|
7224
7991
|
// src/recommender/recommend.ts
|
|
7225
7992
|
async function recommend(root, context, opts = {}) {
|
|
7993
|
+
const activePluginIds = opts.activePluginIds ?? [];
|
|
7226
7994
|
const items = await loadCatalog(root);
|
|
7227
7995
|
const sources = buildSourcesReport(items);
|
|
7228
7996
|
const deep = await readJson(hausPath(root, "deep-context.json")) ?? {};
|
|
@@ -7280,6 +8048,15 @@ async function recommend(root, context, opts = {}) {
|
|
|
7280
8048
|
"Catalog item is deprecated",
|
|
7281
8049
|
"reviewStatus:deprecated"
|
|
7282
8050
|
);
|
|
8051
|
+
if (isItemSupersededByPlugin(item, activePluginIds)) {
|
|
8052
|
+
add(
|
|
8053
|
+
"superseded-by-global-plugin",
|
|
8054
|
+
false,
|
|
8055
|
+
"superseded-by-global-plugin",
|
|
8056
|
+
"Provided by an installed global Claude Code plugin",
|
|
8057
|
+
`plugin:${activePluginIds.join(",")}`
|
|
8058
|
+
);
|
|
8059
|
+
}
|
|
7283
8060
|
if (normSource === "curated") {
|
|
7284
8061
|
const rs = item.reviewStatus;
|
|
7285
8062
|
add(
|
|
@@ -7495,13 +8272,6 @@ function applyCoInstallSuppression(recommended, skipped) {
|
|
|
7495
8272
|
message: "Skip TDD superpower when a stack testing skill is selected",
|
|
7496
8273
|
signal: "co-install:stack-testing"
|
|
7497
8274
|
},
|
|
7498
|
-
{
|
|
7499
|
-
suppress: "haus.superpowers-specifying-gates",
|
|
7500
|
-
whenAnyRecommended: ["haus.superpowers-checking-gates"],
|
|
7501
|
-
code: "co-install-gate-workflow",
|
|
7502
|
-
message: "Skip specifying-gates when checking-gates is selected",
|
|
7503
|
-
signal: "co-install:checking-gates"
|
|
7504
|
-
},
|
|
7505
8275
|
{
|
|
7506
8276
|
suppress: "haus.ecc-redis-patterns",
|
|
7507
8277
|
whenAnyRecommended: ["haus.redis-redis-connections"],
|
|
@@ -7619,7 +8389,7 @@ function buildOptInEligible(items, skipped) {
|
|
|
7619
8389
|
}
|
|
7620
8390
|
|
|
7621
8391
|
// src/commands/workspace/discover.ts
|
|
7622
|
-
import
|
|
8392
|
+
import path46 from "node:path";
|
|
7623
8393
|
import fg5 from "fast-glob";
|
|
7624
8394
|
import YAML2 from "yaml";
|
|
7625
8395
|
var DEFAULT_MAX_DEPTH = 3;
|
|
@@ -7660,10 +8430,10 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
7660
8430
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
7661
8431
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
7662
8432
|
for (const match of gitMatches) {
|
|
7663
|
-
gitDirs.add(
|
|
8433
|
+
gitDirs.add(path46.posix.dirname(match));
|
|
7664
8434
|
}
|
|
7665
8435
|
for (const match of manifestMatches) {
|
|
7666
|
-
manifestDirs.add(
|
|
8436
|
+
manifestDirs.add(path46.posix.dirname(match));
|
|
7667
8437
|
}
|
|
7668
8438
|
const repoRoots = [...gitDirs];
|
|
7669
8439
|
const manifestSorted = [...manifestDirs].sort(
|
|
@@ -7680,9 +8450,9 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
7680
8450
|
async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
7681
8451
|
const repoRoots = (await findRepoRoots(workspaceRoot, maxDepth)).filter((dir) => dir !== ".");
|
|
7682
8452
|
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 :
|
|
8453
|
+
const absDir = path46.resolve(workspaceRoot, relDir);
|
|
8454
|
+
const pkg = await readJson(path46.join(absDir, "package.json"));
|
|
8455
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path46.basename(absDir);
|
|
7686
8456
|
let role = "auto";
|
|
7687
8457
|
try {
|
|
7688
8458
|
const scan = await scanProject(absDir);
|
|
@@ -7725,7 +8495,7 @@ function renderWorkspaceYaml(config) {
|
|
|
7725
8495
|
});
|
|
7726
8496
|
}
|
|
7727
8497
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
7728
|
-
const yamlPath =
|
|
8498
|
+
const yamlPath = path46.join(workspaceRoot, "haus.workspace.yaml");
|
|
7729
8499
|
const existingText = await readText(yamlPath);
|
|
7730
8500
|
const existing = parseWorkspaceConfig(existingText);
|
|
7731
8501
|
if (existingText && !existing) {
|
|
@@ -7792,7 +8562,11 @@ async function runSetupCore(root, opts) {
|
|
|
7792
8562
|
const siblingWarning = await hasMultipleSiblingRepos(root) ? [SIBLING_REPO_WARNING] : [];
|
|
7793
8563
|
const context = await readContextOrScan(root);
|
|
7794
8564
|
const priorManualIncludes = await readPriorManualIncludes(root);
|
|
7795
|
-
const
|
|
8565
|
+
const activePluginIds = await activeGlobalPluginIds(root);
|
|
8566
|
+
const recommendation = await recommend(root, context, {
|
|
8567
|
+
include: priorManualIncludes,
|
|
8568
|
+
activePluginIds
|
|
8569
|
+
});
|
|
7796
8570
|
if (recommendation.recommended.length === 0 && !force) {
|
|
7797
8571
|
const warningLines2 = [
|
|
7798
8572
|
.../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? [], ...siblingWarning])
|
|
@@ -7806,7 +8580,10 @@ async function runSetupCore(root, opts) {
|
|
|
7806
8580
|
say(`Repo: ${context.repoName}`);
|
|
7807
8581
|
for (const warning of warningLines2) say(`- WARN: ${warning}`);
|
|
7808
8582
|
}
|
|
7809
|
-
const
|
|
8583
|
+
const supersededCount = recommendation.skipped.filter(
|
|
8584
|
+
(s) => s.skipReasons?.some((r) => r.code === "superseded-by-global-plugin")
|
|
8585
|
+
).length;
|
|
8586
|
+
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
8587
|
warn(message);
|
|
7811
8588
|
process.exitCode = 1;
|
|
7812
8589
|
return {
|
|
@@ -7872,7 +8649,10 @@ async function runSetupCore(root, opts) {
|
|
|
7872
8649
|
warn(formatGithubRateLimitMessage(sync.rateLimit));
|
|
7873
8650
|
}
|
|
7874
8651
|
}
|
|
7875
|
-
const files = await writeClaudeFiles(root, dryRun ?? false, void 0, {
|
|
8652
|
+
const files = await writeClaudeFiles(root, dryRun ?? false, void 0, {
|
|
8653
|
+
quiet: opts.quiet,
|
|
8654
|
+
activePluginIds
|
|
8655
|
+
});
|
|
7876
8656
|
say("Applied files:");
|
|
7877
8657
|
files.forEach((f) => say(`- ${displayPath(root, f)}`));
|
|
7878
8658
|
const hooksAfter = await verifyProjectSettingsHooksContract(root);
|
|
@@ -7903,7 +8683,7 @@ async function runSetupProject(options) {
|
|
|
7903
8683
|
// src/commands/init.ts
|
|
7904
8684
|
async function runInit(options) {
|
|
7905
8685
|
const root = process.cwd();
|
|
7906
|
-
const hausDir =
|
|
8686
|
+
const hausDir = path47.join(root, ".haus-workflow");
|
|
7907
8687
|
const alreadyInit = await fs33.pathExists(hausDir);
|
|
7908
8688
|
if (alreadyInit) {
|
|
7909
8689
|
log("Haus AI already initialized in this project.");
|
|
@@ -7968,7 +8748,19 @@ function printCompanionToolSuggestions(deps = {}) {
|
|
|
7968
8748
|
}
|
|
7969
8749
|
|
|
7970
8750
|
// src/commands/install.ts
|
|
7971
|
-
|
|
8751
|
+
function toManifestPlugin(pin) {
|
|
8752
|
+
return {
|
|
8753
|
+
id: pin.id,
|
|
8754
|
+
plugin: pin.plugin,
|
|
8755
|
+
marketplace: pin.marketplace,
|
|
8756
|
+
url: pin.url,
|
|
8757
|
+
ref: pin.ref,
|
|
8758
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8759
|
+
};
|
|
8760
|
+
}
|
|
8761
|
+
async function runInstall(options, deps = {}) {
|
|
8762
|
+
const ensure = deps.ensureGlobalPlugins ?? ensureGlobalPlugins;
|
|
8763
|
+
const probe = deps.probePluginEnv ?? probePluginEnv;
|
|
7972
8764
|
try {
|
|
7973
8765
|
const result = await applyInstall({
|
|
7974
8766
|
dryRun: options.dryRun,
|
|
@@ -7976,9 +8768,23 @@ async function runInstall(options) {
|
|
|
7976
8768
|
check: options.check
|
|
7977
8769
|
});
|
|
7978
8770
|
if (!options.postinstall) printApplyResult(result, options.dryRun ?? false);
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
8771
|
+
const pins = [...DEFAULT_GLOBAL_PLUGINS];
|
|
8772
|
+
if (options.check) {
|
|
8773
|
+
const state = await probe();
|
|
8774
|
+
const pluginDrift = pluginDriftStatus(planPluginActions(pins, state), { matchRef: false });
|
|
8775
|
+
if (pluginDrift === true) {
|
|
8776
|
+
log("Global plugin drift: a haus-managed plugin is missing or disabled.");
|
|
8777
|
+
}
|
|
8778
|
+
if (result.drift || pluginDrift === true) process.exitCode = 1;
|
|
8779
|
+
return;
|
|
8780
|
+
}
|
|
8781
|
+
const ensured = await ensure(pins, { dryRun: options.dryRun });
|
|
8782
|
+
if (!options.dryRun) {
|
|
8783
|
+
await recordManifestPlugins(
|
|
8784
|
+
pins.filter((pin) => ensured.ensured.includes(pin.id)).map(toManifestPlugin)
|
|
8785
|
+
);
|
|
8786
|
+
}
|
|
8787
|
+
if (!options.dryRun) {
|
|
7982
8788
|
printCompanionToolSuggestions();
|
|
7983
8789
|
const total = result.created.length + result.updated.length;
|
|
7984
8790
|
if (options.postinstall) {
|
|
@@ -7990,6 +8796,9 @@ async function runInstall(options) {
|
|
|
7990
8796
|
parts.length ? ` \u2022 ${parts.join(", ")} in ~/.claude (skills, slash commands)` : " \u2022 already up to date \u2014 no files changed"
|
|
7991
8797
|
);
|
|
7992
8798
|
log(` \u2022 ensured hooks + security rules are present in ~/.claude/settings.json`);
|
|
8799
|
+
if (ensured.ensured.length) {
|
|
8800
|
+
log(` \u2022 global plugin(s) pinned: ${ensured.ensured.join(", ")}`);
|
|
8801
|
+
}
|
|
7993
8802
|
log("Undo any time with: haus uninstall");
|
|
7994
8803
|
log("Disable this on install: HAUS_NO_POSTINSTALL=1");
|
|
7995
8804
|
} else {
|
|
@@ -8009,7 +8818,8 @@ async function runRecommend(options) {
|
|
|
8009
8818
|
const root = process.cwd();
|
|
8010
8819
|
const context = await readContextOrScan(root);
|
|
8011
8820
|
const include = parseIdList(options.include);
|
|
8012
|
-
const
|
|
8821
|
+
const activePluginIds = await activeGlobalPluginIds(root);
|
|
8822
|
+
const result = await recommend(root, context, { include, activePluginIds });
|
|
8013
8823
|
await writeJson(hausPath(root, "recommendation.json"), result);
|
|
8014
8824
|
if (options.json) {
|
|
8015
8825
|
log(JSON.stringify(result, null, 2));
|
|
@@ -8024,13 +8834,21 @@ async function runRecommend(options) {
|
|
|
8024
8834
|
);
|
|
8025
8835
|
}
|
|
8026
8836
|
for (const w of result.warnings) log(`- WARN: ${w}`);
|
|
8837
|
+
log("Run `haus explain-recommendation` for per-item reasons and skip explanations.");
|
|
8027
8838
|
}
|
|
8028
8839
|
|
|
8029
8840
|
// src/commands/refresh.ts
|
|
8030
8841
|
async function runRefresh() {
|
|
8031
8842
|
const root = process.cwd();
|
|
8032
8843
|
const context = await scanProject(root);
|
|
8033
|
-
|
|
8844
|
+
try {
|
|
8845
|
+
await ensureGlobalPlugins(await loadCatalogPluginPins(root));
|
|
8846
|
+
} catch (err) {
|
|
8847
|
+
warn(`Could not refresh global plugins: ${err instanceof Error ? err.message : String(err)}`);
|
|
8848
|
+
}
|
|
8849
|
+
const recommendation = await recommend(root, context, {
|
|
8850
|
+
activePluginIds: await activeGlobalPluginIds(root)
|
|
8851
|
+
});
|
|
8034
8852
|
await writeJson(hausPath(root, "recommendation.json"), recommendation);
|
|
8035
8853
|
log("Haus refresh complete");
|
|
8036
8854
|
log(`Roles: ${context.repoRoles.join(", ") || "unknown"}`);
|
|
@@ -8039,7 +8857,7 @@ async function runRefresh() {
|
|
|
8039
8857
|
}
|
|
8040
8858
|
|
|
8041
8859
|
// src/install/scaffold.ts
|
|
8042
|
-
import
|
|
8860
|
+
import path48 from "node:path";
|
|
8043
8861
|
import fs34 from "fs-extra";
|
|
8044
8862
|
async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
8045
8863
|
const scaffolded = [];
|
|
@@ -8056,9 +8874,9 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8056
8874
|
const realRoot = await fs34.realpath(catalogRoot).catch(() => null);
|
|
8057
8875
|
for (const item of items) {
|
|
8058
8876
|
if (item.type !== "config") continue;
|
|
8059
|
-
const sourcePath =
|
|
8060
|
-
const rel =
|
|
8061
|
-
const escapes = rel === "" ||
|
|
8877
|
+
const sourcePath = path48.resolve(catalogRoot, item.path);
|
|
8878
|
+
const rel = path48.relative(catalogRoot, sourcePath);
|
|
8879
|
+
const escapes = rel === "" || path48.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
|
|
8062
8880
|
if (escapes) {
|
|
8063
8881
|
warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
|
|
8064
8882
|
continue;
|
|
@@ -8068,8 +8886,8 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8068
8886
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
8069
8887
|
continue;
|
|
8070
8888
|
}
|
|
8071
|
-
const realRel =
|
|
8072
|
-
if (realRel !== "" && (
|
|
8889
|
+
const realRel = path48.relative(realRoot, realSource);
|
|
8890
|
+
if (realRel !== "" && (path48.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
|
|
8073
8891
|
warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
|
|
8074
8892
|
continue;
|
|
8075
8893
|
}
|
|
@@ -8083,14 +8901,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8083
8901
|
continue;
|
|
8084
8902
|
}
|
|
8085
8903
|
if (stat.isFile()) {
|
|
8086
|
-
const filename =
|
|
8087
|
-
const dest =
|
|
8904
|
+
const filename = path48.basename(sourcePath);
|
|
8905
|
+
const dest = path48.join(projectRoot, filename);
|
|
8088
8906
|
record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
|
|
8089
8907
|
} else if (stat.isDirectory()) {
|
|
8090
8908
|
const entries = await fs34.readdir(sourcePath);
|
|
8091
8909
|
for (const entry of entries) {
|
|
8092
|
-
const src =
|
|
8093
|
-
const dest =
|
|
8910
|
+
const src = path48.join(sourcePath, entry);
|
|
8911
|
+
const dest = path48.join(projectRoot, entry);
|
|
8094
8912
|
record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
|
|
8095
8913
|
}
|
|
8096
8914
|
}
|
|
@@ -8098,14 +8916,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8098
8916
|
return { scaffolded, skipped, skippedExisting };
|
|
8099
8917
|
}
|
|
8100
8918
|
async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
8101
|
-
const destDisplay =
|
|
8919
|
+
const destDisplay = path48.relative(projectRoot, dest) || path48.basename(dest);
|
|
8102
8920
|
const srcStat = await fs34.lstat(src).catch(() => null);
|
|
8103
8921
|
if (!srcStat) {
|
|
8104
|
-
warn(`Skipping ${
|
|
8922
|
+
warn(`Skipping ${path48.basename(src)}: source not found`);
|
|
8105
8923
|
return "skipped-refused";
|
|
8106
8924
|
}
|
|
8107
8925
|
if (srcStat.isSymbolicLink()) {
|
|
8108
|
-
warn(`Skipping ${
|
|
8926
|
+
warn(`Skipping ${path48.basename(src)}: source is a symlink`);
|
|
8109
8927
|
return "skipped-refused";
|
|
8110
8928
|
}
|
|
8111
8929
|
const exists = await fs34.pathExists(dest);
|
|
@@ -8114,13 +8932,13 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
|
8114
8932
|
return "skipped-exists";
|
|
8115
8933
|
}
|
|
8116
8934
|
if (opts.dryRun) {
|
|
8117
|
-
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${
|
|
8935
|
+
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path48.basename(dest)} (${itemId})`);
|
|
8118
8936
|
return "scaffolded";
|
|
8119
8937
|
}
|
|
8120
8938
|
if (exists && opts.force) {
|
|
8121
8939
|
await fs34.remove(dest);
|
|
8122
8940
|
}
|
|
8123
|
-
await fs34.ensureDir(
|
|
8941
|
+
await fs34.ensureDir(path48.dirname(dest));
|
|
8124
8942
|
await fs34.copy(src, dest, {
|
|
8125
8943
|
overwrite: true,
|
|
8126
8944
|
dereference: false,
|
|
@@ -8133,7 +8951,7 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
|
8133
8951
|
}
|
|
8134
8952
|
}
|
|
8135
8953
|
});
|
|
8136
|
-
log(`${exists ? "Overwrote" : "Created"} ${
|
|
8954
|
+
log(`${exists ? "Overwrote" : "Created"} ${path48.basename(dest)} (${itemId})`);
|
|
8137
8955
|
return "scaffolded";
|
|
8138
8956
|
}
|
|
8139
8957
|
|
|
@@ -8198,7 +9016,7 @@ async function runScan(options) {
|
|
|
8198
9016
|
}
|
|
8199
9017
|
|
|
8200
9018
|
// src/commands/undo.ts
|
|
8201
|
-
import
|
|
9019
|
+
import path49 from "node:path";
|
|
8202
9020
|
import fs35 from "fs-extra";
|
|
8203
9021
|
|
|
8204
9022
|
// src/claude/managed-paths.ts
|
|
@@ -8226,18 +9044,18 @@ async function collectLockTrackedPaths(root) {
|
|
|
8226
9044
|
const relPaths = row.paths ?? [];
|
|
8227
9045
|
const existingRel = [];
|
|
8228
9046
|
for (const rel of relPaths) {
|
|
8229
|
-
if (await fs35.pathExists(
|
|
9047
|
+
if (await fs35.pathExists(path49.resolve(root, rel))) existingRel.push(rel);
|
|
8230
9048
|
}
|
|
8231
9049
|
if (existingRel.length === 0) continue;
|
|
8232
9050
|
if (row.hash === void 0) {
|
|
8233
|
-
removable.push(...existingRel.map((rel) =>
|
|
9051
|
+
removable.push(...existingRel.map((rel) => path49.resolve(root, rel)));
|
|
8234
9052
|
continue;
|
|
8235
9053
|
}
|
|
8236
9054
|
const currentHash = await hashInstalledPaths(root, existingRel);
|
|
8237
9055
|
if (currentHash === row.hash) {
|
|
8238
|
-
removable.push(...existingRel.map((rel) =>
|
|
9056
|
+
removable.push(...existingRel.map((rel) => path49.resolve(root, rel)));
|
|
8239
9057
|
} else {
|
|
8240
|
-
preserved.push(...existingRel.map((rel) =>
|
|
9058
|
+
preserved.push(...existingRel.map((rel) => path49.resolve(root, rel)));
|
|
8241
9059
|
log(`Preserving locally-modified ${row.id ?? "(unknown item)"}: ${existingRel.join(", ")}`);
|
|
8242
9060
|
}
|
|
8243
9061
|
}
|
|
@@ -8250,7 +9068,7 @@ async function settingsHasHausContent(root) {
|
|
|
8250
9068
|
return settings._haus != null;
|
|
8251
9069
|
}
|
|
8252
9070
|
async function claudeMdHasHausBlock(root) {
|
|
8253
|
-
const filePath =
|
|
9071
|
+
const filePath = path49.join(root, "CLAUDE.md");
|
|
8254
9072
|
if (!await fs35.pathExists(filePath)) return false;
|
|
8255
9073
|
const text = await fs35.readFile(filePath, "utf8");
|
|
8256
9074
|
return text.includes(BLOCK_BEGIN);
|
|
@@ -8263,15 +9081,15 @@ async function stripProjectSettings(root) {
|
|
|
8263
9081
|
const hasContent = Object.keys(settings).length > 0;
|
|
8264
9082
|
if (hasContent) {
|
|
8265
9083
|
await writeProjectSettings(root, settings);
|
|
8266
|
-
log(`Stripped haus rules from ${
|
|
9084
|
+
log(`Stripped haus rules from ${path49.relative(root, settingsPath)} (user settings preserved).`);
|
|
8267
9085
|
return true;
|
|
8268
9086
|
}
|
|
8269
9087
|
await fs35.remove(settingsPath);
|
|
8270
|
-
log(`Removed ${
|
|
9088
|
+
log(`Removed ${path49.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
8271
9089
|
return true;
|
|
8272
9090
|
}
|
|
8273
9091
|
async function stripRootClaudeMd(root) {
|
|
8274
|
-
const filePath =
|
|
9092
|
+
const filePath = path49.join(root, "CLAUDE.md");
|
|
8275
9093
|
if (!await fs35.pathExists(filePath)) return false;
|
|
8276
9094
|
const prev = await fs35.readFile(filePath, "utf8");
|
|
8277
9095
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
@@ -8299,12 +9117,12 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
|
8299
9117
|
const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
|
|
8300
9118
|
for (const abs of managedAbsPaths) {
|
|
8301
9119
|
if (!await fs35.pathExists(abs)) continue;
|
|
8302
|
-
const rel =
|
|
8303
|
-
const backupPath =
|
|
8304
|
-
await fs35.ensureDir(
|
|
9120
|
+
const rel = path49.relative(root, abs);
|
|
9121
|
+
const backupPath = path49.join(backupRoot, rel);
|
|
9122
|
+
await fs35.ensureDir(path49.dirname(backupPath));
|
|
8305
9123
|
await fs35.copy(abs, backupPath);
|
|
8306
9124
|
}
|
|
8307
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
9125
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path49.relative(root, backupRoot)}.`);
|
|
8308
9126
|
}
|
|
8309
9127
|
async function runUndo(options) {
|
|
8310
9128
|
const root = options.root ?? process.cwd();
|
|
@@ -8317,7 +9135,7 @@ async function runUndo(options) {
|
|
|
8317
9135
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
8318
9136
|
return;
|
|
8319
9137
|
}
|
|
8320
|
-
const relTargets = managed.map((p) =>
|
|
9138
|
+
const relTargets = managed.map((p) => path49.relative(root, p));
|
|
8321
9139
|
const summaryParts = [...relTargets];
|
|
8322
9140
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
8323
9141
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -8326,6 +9144,12 @@ async function runUndo(options) {
|
|
|
8326
9144
|
`(preserving ${lockPreserved.length} locally-modified catalog item file(s) \u2014 not shown above)`
|
|
8327
9145
|
);
|
|
8328
9146
|
}
|
|
9147
|
+
if (options.dryRun) {
|
|
9148
|
+
log(`[dry-run] Would remove:
|
|
9149
|
+
${summaryParts.join("\n ")}`);
|
|
9150
|
+
log("[dry-run] User-owned .claude/ files would be preserved. Nothing was changed.");
|
|
9151
|
+
return;
|
|
9152
|
+
}
|
|
8329
9153
|
if (!options.yes) {
|
|
8330
9154
|
const ok = await confirm(
|
|
8331
9155
|
`Remove haus-managed files?
|
|
@@ -8341,7 +9165,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
8341
9165
|
for (const abs of managed) {
|
|
8342
9166
|
if (!await fs35.pathExists(abs)) continue;
|
|
8343
9167
|
await fs35.remove(abs);
|
|
8344
|
-
log(`Removed ${
|
|
9168
|
+
log(`Removed ${path49.relative(root, abs)}`);
|
|
8345
9169
|
}
|
|
8346
9170
|
if (stripSettings) await stripProjectSettings(root);
|
|
8347
9171
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -8352,16 +9176,43 @@ User-owned .claude/ files will be preserved.`
|
|
|
8352
9176
|
|
|
8353
9177
|
// src/install/uninstall.ts
|
|
8354
9178
|
import crypto3 from "node:crypto";
|
|
8355
|
-
import
|
|
9179
|
+
import path50 from "node:path";
|
|
8356
9180
|
import fs36 from "fs-extra";
|
|
8357
9181
|
async function runUninstall(options = {}) {
|
|
8358
|
-
const { force = false } = options;
|
|
9182
|
+
const { force = false, removePlugins = removeGlobalPlugins } = options;
|
|
8359
9183
|
const manifest = await readManifest();
|
|
8360
|
-
const result = {
|
|
9184
|
+
const result = {
|
|
9185
|
+
deleted: [],
|
|
9186
|
+
skipped: [],
|
|
9187
|
+
hooksStripped: false,
|
|
9188
|
+
pluginsRemoved: []
|
|
9189
|
+
};
|
|
8361
9190
|
if (!manifest) {
|
|
8362
9191
|
warn("No install manifest found \u2014 nothing to uninstall.");
|
|
8363
9192
|
return result;
|
|
8364
9193
|
}
|
|
9194
|
+
const recordedPlugins = Array.isArray(manifest.plugins) ? manifest.plugins : [];
|
|
9195
|
+
const malformed = recordedPlugins.filter((entry) => !isValidPin(entry)).length;
|
|
9196
|
+
if (malformed > 0) {
|
|
9197
|
+
warn(
|
|
9198
|
+
`Ignoring ${malformed} malformed plugin record(s) in the install manifest \u2014 continuing with the rest of the uninstall.`
|
|
9199
|
+
);
|
|
9200
|
+
}
|
|
9201
|
+
const managedPlugins = recordedPlugins.filter(isValidPin).map((p) => ({
|
|
9202
|
+
id: p.id,
|
|
9203
|
+
plugin: p.plugin,
|
|
9204
|
+
marketplace: p.marketplace,
|
|
9205
|
+
url: p.url,
|
|
9206
|
+
ref: p.ref
|
|
9207
|
+
}));
|
|
9208
|
+
try {
|
|
9209
|
+
const pluginRemoval = await removePlugins(managedPlugins);
|
|
9210
|
+
result.pluginsRemoved = pluginRemoval.removed;
|
|
9211
|
+
} catch (err) {
|
|
9212
|
+
warn(
|
|
9213
|
+
`Could not remove global plugin(s): ${err instanceof Error ? err.message : String(err)} \u2014 continuing with the rest of the uninstall.`
|
|
9214
|
+
);
|
|
9215
|
+
}
|
|
8365
9216
|
for (const entry of manifest.files) {
|
|
8366
9217
|
const exists = fs36.pathExistsSync(entry.destPath);
|
|
8367
9218
|
if (!exists) continue;
|
|
@@ -8382,14 +9233,14 @@ async function runUninstall(options = {}) {
|
|
|
8382
9233
|
continue;
|
|
8383
9234
|
}
|
|
8384
9235
|
await fs36.remove(entry.destPath);
|
|
8385
|
-
await pruneEmptyDir(
|
|
9236
|
+
await pruneEmptyDir(path50.dirname(entry.destPath));
|
|
8386
9237
|
result.deleted.push(entry.destPath);
|
|
8387
9238
|
}
|
|
8388
9239
|
const settings = await readSettings();
|
|
8389
9240
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
8390
9241
|
await writeSettings(stripped);
|
|
8391
9242
|
result.hooksStripped = true;
|
|
8392
|
-
const hausDir =
|
|
9243
|
+
const hausDir = path50.join(globalClaudeDir(), "haus");
|
|
8393
9244
|
const manifestPath2 = hausManifestPath();
|
|
8394
9245
|
if (fs36.pathExistsSync(manifestPath2)) {
|
|
8395
9246
|
await fs36.remove(manifestPath2);
|
|
@@ -8415,11 +9266,30 @@ function printUninstallResult(result) {
|
|
|
8415
9266
|
if (result.hooksStripped) {
|
|
8416
9267
|
log("Haus hook entries removed from ~/.claude/settings.json");
|
|
8417
9268
|
}
|
|
9269
|
+
if (result.pluginsRemoved.length) {
|
|
9270
|
+
log(`Global plugin(s) removed: ${result.pluginsRemoved.join(", ")}`);
|
|
9271
|
+
log("Projects relying on those plugin skills get them back by reinstalling haus.");
|
|
9272
|
+
}
|
|
8418
9273
|
}
|
|
8419
9274
|
|
|
8420
9275
|
// src/commands/uninstall.ts
|
|
8421
9276
|
async function runUninstallCommand(options) {
|
|
8422
9277
|
try {
|
|
9278
|
+
if (options.dryRun) {
|
|
9279
|
+
const manifest = await readManifest();
|
|
9280
|
+
if (!manifest) {
|
|
9281
|
+
log("[dry-run] No install manifest found \u2014 nothing would be removed.");
|
|
9282
|
+
return;
|
|
9283
|
+
}
|
|
9284
|
+
if (manifest.files.length > 0) {
|
|
9285
|
+
log(`[dry-run] Would remove ${manifest.files.length} haus-managed file(s) from ~/.claude/:`);
|
|
9286
|
+
for (const f of manifest.files) log(` ${f.destPath}`);
|
|
9287
|
+
}
|
|
9288
|
+
log(
|
|
9289
|
+
"[dry-run] Would strip haus hook entries from ~/.claude/settings.json and remove the install manifest. Nothing was changed."
|
|
9290
|
+
);
|
|
9291
|
+
return;
|
|
9292
|
+
}
|
|
8423
9293
|
if (!options.yes) {
|
|
8424
9294
|
const manifest = await readManifest();
|
|
8425
9295
|
if (manifest) {
|
|
@@ -8443,11 +9313,11 @@ async function runUninstallCommand(options) {
|
|
|
8443
9313
|
}
|
|
8444
9314
|
|
|
8445
9315
|
// src/commands/validate-catalog.ts
|
|
8446
|
-
import
|
|
9316
|
+
import path52 from "node:path";
|
|
8447
9317
|
|
|
8448
9318
|
// src/catalog/validate-core.ts
|
|
8449
9319
|
import fs37 from "node:fs";
|
|
8450
|
-
import
|
|
9320
|
+
import path51 from "node:path";
|
|
8451
9321
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
8452
9322
|
var ITEM_ID_PATTERN = /^haus\./;
|
|
8453
9323
|
function auditForbiddenStacks(items) {
|
|
@@ -8596,7 +9466,7 @@ function readForAudit(absPath, label, failures) {
|
|
|
8596
9466
|
}
|
|
8597
9467
|
}
|
|
8598
9468
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
8599
|
-
const rel =
|
|
9469
|
+
const rel = path51.relative(manifestDir, absPath);
|
|
8600
9470
|
const failures = [];
|
|
8601
9471
|
const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
|
|
8602
9472
|
if (text === null) return failures;
|
|
@@ -8618,25 +9488,25 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
8618
9488
|
const failures = [];
|
|
8619
9489
|
for (const item of items) {
|
|
8620
9490
|
if (!item.path || !isSafeCatalogPath(item.path)) continue;
|
|
8621
|
-
const absPath =
|
|
9491
|
+
const absPath = path51.join(manifestDir, item.path);
|
|
8622
9492
|
if (item.type === "skill") {
|
|
8623
|
-
const skillMd =
|
|
9493
|
+
const skillMd = path51.join(absPath, "SKILL.md");
|
|
8624
9494
|
if (!fs37.existsSync(skillMd)) {
|
|
8625
|
-
failures.push(`${item.id}: missing ${
|
|
9495
|
+
failures.push(`${item.id}: missing ${path51.relative(manifestDir, skillMd)}`);
|
|
8626
9496
|
continue;
|
|
8627
9497
|
}
|
|
8628
9498
|
const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
|
|
8629
9499
|
if (text === null) continue;
|
|
8630
9500
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
8631
9501
|
failures.push(
|
|
8632
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
9502
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path51.relative(manifestDir, skillMd)}`)
|
|
8633
9503
|
);
|
|
8634
9504
|
} else if (item.type === "agent") {
|
|
8635
9505
|
if (!fs37.existsSync(absPath)) {
|
|
8636
9506
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
8637
9507
|
continue;
|
|
8638
9508
|
}
|
|
8639
|
-
const rel =
|
|
9509
|
+
const rel = path51.relative(manifestDir, absPath);
|
|
8640
9510
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
8641
9511
|
if (text === null) continue;
|
|
8642
9512
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -8652,7 +9522,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
8652
9522
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
8653
9523
|
continue;
|
|
8654
9524
|
}
|
|
8655
|
-
const rel =
|
|
9525
|
+
const rel = path51.relative(manifestDir, absPath);
|
|
8656
9526
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
8657
9527
|
if (text === null) continue;
|
|
8658
9528
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -8684,7 +9554,7 @@ function walkAllFiles(dir, fn, failures) {
|
|
|
8684
9554
|
}
|
|
8685
9555
|
for (const entry of entries) {
|
|
8686
9556
|
if (entry.name === ".DS_Store") continue;
|
|
8687
|
-
const full =
|
|
9557
|
+
const full = path51.join(dir, entry.name);
|
|
8688
9558
|
if (entry.isDirectory()) walkAllFiles(full, fn, failures);
|
|
8689
9559
|
else fn(full);
|
|
8690
9560
|
}
|
|
@@ -8702,12 +9572,12 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
8702
9572
|
const failures = [];
|
|
8703
9573
|
const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
|
|
8704
9574
|
for (const dir of CONTENT_ROOTS) {
|
|
8705
|
-
const abs =
|
|
9575
|
+
const abs = path51.join(manifestDir, dir);
|
|
8706
9576
|
if (!fs37.existsSync(abs)) continue;
|
|
8707
9577
|
walkAllFiles(
|
|
8708
9578
|
abs,
|
|
8709
9579
|
(file) => {
|
|
8710
|
-
const relPath =
|
|
9580
|
+
const relPath = path51.relative(manifestDir, file).replace(/\\/g, "/");
|
|
8711
9581
|
if (!isPathClaimed(relPath, claimedPaths)) {
|
|
8712
9582
|
failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
|
|
8713
9583
|
}
|
|
@@ -8728,7 +9598,7 @@ function walkMd(dir, fn, failures) {
|
|
|
8728
9598
|
return;
|
|
8729
9599
|
}
|
|
8730
9600
|
for (const entry of entries) {
|
|
8731
|
-
const full =
|
|
9601
|
+
const full = path51.join(dir, entry.name);
|
|
8732
9602
|
if (entry.isDirectory()) walkMd(full, fn, failures);
|
|
8733
9603
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
8734
9604
|
}
|
|
@@ -8737,13 +9607,13 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
8737
9607
|
const failures = [];
|
|
8738
9608
|
const pathSourceMap = buildItemPathSourceMap(items);
|
|
8739
9609
|
for (const dir of ["skills", "agents", "templates", "commands"]) {
|
|
8740
|
-
const abs =
|
|
9610
|
+
const abs = path51.join(manifestDir, dir);
|
|
8741
9611
|
if (!fs37.existsSync(abs)) continue;
|
|
8742
9612
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
8743
9613
|
walkMd(
|
|
8744
9614
|
abs,
|
|
8745
9615
|
(file) => {
|
|
8746
|
-
const rel =
|
|
9616
|
+
const rel = path51.relative(manifestDir, file);
|
|
8747
9617
|
const text = readForAudit(file, rel, failures);
|
|
8748
9618
|
if (text === null) return;
|
|
8749
9619
|
const norm = rel.replace(/\\/g, "/");
|
|
@@ -8788,8 +9658,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
8788
9658
|
process.exitCode = 1;
|
|
8789
9659
|
return;
|
|
8790
9660
|
}
|
|
8791
|
-
const abs =
|
|
8792
|
-
const manifestDir =
|
|
9661
|
+
const abs = path52.resolve(process.cwd(), manifestPath2);
|
|
9662
|
+
const manifestDir = path52.dirname(abs);
|
|
8793
9663
|
const data = await readJson(abs);
|
|
8794
9664
|
if (!data?.items) {
|
|
8795
9665
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -8806,7 +9676,7 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
8806
9676
|
}
|
|
8807
9677
|
|
|
8808
9678
|
// src/workspace/worktree/add.ts
|
|
8809
|
-
import
|
|
9679
|
+
import path59 from "node:path";
|
|
8810
9680
|
import fs41 from "fs-extra";
|
|
8811
9681
|
|
|
8812
9682
|
// src/workspace/worktree/git-worktree.ts
|
|
@@ -8926,12 +9796,12 @@ async function hasUnpushedWork(cwd) {
|
|
|
8926
9796
|
}
|
|
8927
9797
|
|
|
8928
9798
|
// src/workspace/worktree/hydrate.ts
|
|
8929
|
-
import
|
|
9799
|
+
import path55 from "node:path";
|
|
8930
9800
|
import fs39 from "fs-extra";
|
|
8931
9801
|
|
|
8932
9802
|
// src/workspace/worktree/cow-copy.ts
|
|
8933
9803
|
init_exec();
|
|
8934
|
-
import
|
|
9804
|
+
import path53 from "node:path";
|
|
8935
9805
|
import fs38 from "fs-extra";
|
|
8936
9806
|
async function getMacFilesystemType(dir) {
|
|
8937
9807
|
const df = await runCommand("df", ["-P", dir]);
|
|
@@ -8966,11 +9836,11 @@ async function cowCopyDir(src, dest) {
|
|
|
8966
9836
|
const srcExists = await fs38.pathExists(src);
|
|
8967
9837
|
if (!srcExists)
|
|
8968
9838
|
return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
|
|
8969
|
-
const strategy = await detectCowStrategy(
|
|
9839
|
+
const strategy = await detectCowStrategy(path53.dirname(dest));
|
|
8970
9840
|
if (strategy === "unsupported" || strategy === "unknown-platform") {
|
|
8971
9841
|
return { strategy, attempted: false, ok: false };
|
|
8972
9842
|
}
|
|
8973
|
-
await fs38.ensureDir(
|
|
9843
|
+
await fs38.ensureDir(path53.dirname(dest));
|
|
8974
9844
|
const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
|
|
8975
9845
|
const result = await runCommand("cp", args);
|
|
8976
9846
|
return {
|
|
@@ -8984,7 +9854,7 @@ async function cowCopyDir(src, dest) {
|
|
|
8984
9854
|
// src/workspace/worktree/install.ts
|
|
8985
9855
|
init_exec();
|
|
8986
9856
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
8987
|
-
import
|
|
9857
|
+
import path54 from "node:path";
|
|
8988
9858
|
import fg6 from "fast-glob";
|
|
8989
9859
|
function detectInstallPlan(signals) {
|
|
8990
9860
|
if (signals.hasYarnLock) {
|
|
@@ -9005,10 +9875,10 @@ function detectInstallPlan(signals) {
|
|
|
9005
9875
|
return { manager: "none" };
|
|
9006
9876
|
}
|
|
9007
9877
|
async function computeLockfileSignals(dir) {
|
|
9008
|
-
const hasYarnLock = existsSync3(
|
|
9009
|
-
const hasPnpmLock = existsSync3(
|
|
9010
|
-
const hasPackageLock = existsSync3(
|
|
9011
|
-
const hasComposerLock = existsSync3(
|
|
9878
|
+
const hasYarnLock = existsSync3(path54.join(dir, "yarn.lock"));
|
|
9879
|
+
const hasPnpmLock = existsSync3(path54.join(dir, "pnpm-lock.yaml"));
|
|
9880
|
+
const hasPackageLock = existsSync3(path54.join(dir, "package-lock.json"));
|
|
9881
|
+
const hasComposerLock = existsSync3(path54.join(dir, "composer.lock"));
|
|
9012
9882
|
const dotnetFiles = await fg6(["**/*.csproj", "**/*.sln"], {
|
|
9013
9883
|
cwd: dir,
|
|
9014
9884
|
onlyFiles: true,
|
|
@@ -9022,7 +9892,7 @@ async function computeLockfileSignals(dir) {
|
|
|
9022
9892
|
});
|
|
9023
9893
|
const hasDotnetProject = dotnetFiles.length > 0;
|
|
9024
9894
|
let hasPackageManagerField = false;
|
|
9025
|
-
const pkgPath =
|
|
9895
|
+
const pkgPath = path54.join(dir, "package.json");
|
|
9026
9896
|
if (existsSync3(pkgPath)) {
|
|
9027
9897
|
try {
|
|
9028
9898
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
@@ -9063,8 +9933,8 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
9063
9933
|
const start = Date.now();
|
|
9064
9934
|
const targets = [];
|
|
9065
9935
|
for (const target of HYDRATION_TARGETS) {
|
|
9066
|
-
const src =
|
|
9067
|
-
const dest =
|
|
9936
|
+
const src = path55.join(mainMemberPath, target);
|
|
9937
|
+
const dest = path55.join(worktreeMemberPath, target);
|
|
9068
9938
|
if (!await fs39.pathExists(src)) continue;
|
|
9069
9939
|
const destExists = await fs39.pathExists(dest);
|
|
9070
9940
|
if (!opts.force && destExists) continue;
|
|
@@ -9082,11 +9952,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
9082
9952
|
|
|
9083
9953
|
// src/workspace/worktree/local-files.ts
|
|
9084
9954
|
init_exec();
|
|
9085
|
-
import
|
|
9955
|
+
import path56 from "node:path";
|
|
9086
9956
|
import fs40 from "fs-extra";
|
|
9087
9957
|
var MACHINE_LOCAL_FILES = [
|
|
9088
9958
|
"docker-compose.local.yml",
|
|
9089
|
-
|
|
9959
|
+
path56.join(".claude", "settings.local.json")
|
|
9090
9960
|
];
|
|
9091
9961
|
async function isTracked(repoRoot, relFile) {
|
|
9092
9962
|
const gitPath = relFile.replace(/\\/g, "/");
|
|
@@ -9096,11 +9966,11 @@ async function isTracked(repoRoot, relFile) {
|
|
|
9096
9966
|
async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
9097
9967
|
const copied = [];
|
|
9098
9968
|
for (const rel of MACHINE_LOCAL_FILES) {
|
|
9099
|
-
const src =
|
|
9969
|
+
const src = path56.join(mainMemberPath, rel);
|
|
9100
9970
|
if (!await fs40.pathExists(src)) continue;
|
|
9101
9971
|
if (await isTracked(mainMemberPath, rel)) continue;
|
|
9102
|
-
const dest =
|
|
9103
|
-
await fs40.ensureDir(
|
|
9972
|
+
const dest = path56.join(worktreeMemberPath, rel);
|
|
9973
|
+
await fs40.ensureDir(path56.dirname(dest));
|
|
9104
9974
|
await fs40.copy(src, dest, { overwrite: true });
|
|
9105
9975
|
copied.push(rel);
|
|
9106
9976
|
}
|
|
@@ -9109,15 +9979,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
|
9109
9979
|
|
|
9110
9980
|
// src/workspace/worktree/root.ts
|
|
9111
9981
|
import { existsSync as existsSync4 } from "node:fs";
|
|
9112
|
-
import
|
|
9982
|
+
import path57 from "node:path";
|
|
9113
9983
|
async function resolveWorkspaceForWorktree(start) {
|
|
9114
9984
|
const rootInfo = await resolveRoots(start);
|
|
9115
9985
|
if (!rootInfo.isGitRepo) {
|
|
9116
9986
|
return { ok: false, reason: "Not inside a git repository." };
|
|
9117
9987
|
}
|
|
9118
9988
|
const workspaceRoot = rootInfo.mainRoot;
|
|
9119
|
-
const hasYaml = existsSync4(
|
|
9120
|
-
const hasManifest = existsSync4(
|
|
9989
|
+
const hasYaml = existsSync4(path57.join(workspaceRoot, WORKSPACE_FILE));
|
|
9990
|
+
const hasManifest = existsSync4(path57.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
9121
9991
|
if (!hasYaml && !hasManifest) {
|
|
9122
9992
|
return {
|
|
9123
9993
|
ok: false,
|
|
@@ -9142,10 +10012,10 @@ function selectMembers(members, only) {
|
|
|
9142
10012
|
}
|
|
9143
10013
|
|
|
9144
10014
|
// src/workspace/worktree/state.ts
|
|
9145
|
-
import
|
|
10015
|
+
import path58 from "node:path";
|
|
9146
10016
|
var WORKTREE_STATE_FILE = ".haus-worktree.json";
|
|
9147
10017
|
function worktreesDir(mainRoot) {
|
|
9148
|
-
return
|
|
10018
|
+
return path58.join(mainRoot, ".claude", "worktrees");
|
|
9149
10019
|
}
|
|
9150
10020
|
function isValidWorktreeSlug(slug) {
|
|
9151
10021
|
if (slug === "" || slug === "." || slug === "..") return false;
|
|
@@ -9158,15 +10028,15 @@ function worktreePath(mainRoot, slug) {
|
|
|
9158
10028
|
`Invalid worktree slug ${JSON.stringify(slug)} \u2014 slugs may only contain letters, digits, ".", "_", "-" (no path separators, and not "." or "..").`
|
|
9159
10029
|
);
|
|
9160
10030
|
}
|
|
9161
|
-
return
|
|
10031
|
+
return path58.join(worktreesDir(mainRoot), slug);
|
|
9162
10032
|
}
|
|
9163
10033
|
async function readWorktreeState(worktreeRoot) {
|
|
9164
|
-
const state = await readJson(
|
|
10034
|
+
const state = await readJson(path58.join(worktreeRoot, WORKTREE_STATE_FILE));
|
|
9165
10035
|
if (state == null || typeof state !== "object" || !Array.isArray(state.members)) return void 0;
|
|
9166
10036
|
return state;
|
|
9167
10037
|
}
|
|
9168
10038
|
async function writeWorktreeState(worktreeRoot, state) {
|
|
9169
|
-
await writeJson(
|
|
10039
|
+
await writeJson(path58.join(worktreeRoot, WORKTREE_STATE_FILE), state);
|
|
9170
10040
|
}
|
|
9171
10041
|
|
|
9172
10042
|
// src/workspace/worktree/add.ts
|
|
@@ -9200,7 +10070,7 @@ async function runAdd(opts) {
|
|
|
9200
10070
|
const memberResults = [];
|
|
9201
10071
|
for (const member of selected) {
|
|
9202
10072
|
const start = Date.now();
|
|
9203
|
-
const memberWorktreePath =
|
|
10073
|
+
const memberWorktreePath = path59.join(wsWorktreePath, member.folder);
|
|
9204
10074
|
const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
|
|
9205
10075
|
preferDefaultBranchFrom: true
|
|
9206
10076
|
});
|
|
@@ -9230,7 +10100,7 @@ async function runAdd(opts) {
|
|
|
9230
10100
|
await mapWithConcurrency(okResults, async (result) => {
|
|
9231
10101
|
const member = selected.find((m) => m.id === result.member);
|
|
9232
10102
|
if (!member) return;
|
|
9233
|
-
const memberWorktreePath =
|
|
10103
|
+
const memberWorktreePath = path59.join(wsWorktreePath, member.folder);
|
|
9234
10104
|
const hydrationStart = Date.now();
|
|
9235
10105
|
if (opts.hydrate !== false) {
|
|
9236
10106
|
result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
|
|
@@ -9297,10 +10167,10 @@ async function runDryRun(args) {
|
|
|
9297
10167
|
|
|
9298
10168
|
// src/workspace/worktree/doctor.ts
|
|
9299
10169
|
import { existsSync as existsSync5 } from "node:fs";
|
|
9300
|
-
import
|
|
10170
|
+
import path60 from "node:path";
|
|
9301
10171
|
var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
|
|
9302
10172
|
function looksHydrated(memberPath) {
|
|
9303
|
-
return HYDRATION_MARKERS.some((marker) => existsSync5(
|
|
10173
|
+
return HYDRATION_MARKERS.some((marker) => existsSync5(path60.join(memberPath, marker)));
|
|
9304
10174
|
}
|
|
9305
10175
|
async function runDoctor2() {
|
|
9306
10176
|
const rootInfo = await resolveRoots();
|
|
@@ -9314,7 +10184,7 @@ async function runDoctor2() {
|
|
|
9314
10184
|
};
|
|
9315
10185
|
}
|
|
9316
10186
|
const workspaceRoot = rootInfo.mainRoot;
|
|
9317
|
-
const isWorkspace = existsSync5(
|
|
10187
|
+
const isWorkspace = existsSync5(path60.join(workspaceRoot, WORKSPACE_FILE)) || existsSync5(path60.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
9318
10188
|
if (!isWorkspace) {
|
|
9319
10189
|
return {
|
|
9320
10190
|
isWorkspace: false,
|
|
@@ -9327,7 +10197,7 @@ async function runDoctor2() {
|
|
|
9327
10197
|
};
|
|
9328
10198
|
}
|
|
9329
10199
|
const wsWorktreesDir = worktreesDir(workspaceRoot);
|
|
9330
|
-
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${
|
|
10200
|
+
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path60.sep}`);
|
|
9331
10201
|
const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
|
|
9332
10202
|
const members = await readMembers(rootInfo);
|
|
9333
10203
|
const problems = [];
|
|
@@ -9335,10 +10205,10 @@ async function runDoctor2() {
|
|
|
9335
10205
|
let slug;
|
|
9336
10206
|
if (inWorkspaceWorktree) {
|
|
9337
10207
|
const wtPath = rootInfo.repoRoot;
|
|
9338
|
-
slug = rootInfo.worktreeName ??
|
|
10208
|
+
slug = rootInfo.worktreeName ?? path60.basename(wtPath);
|
|
9339
10209
|
const state = await readWorktreeState(wtPath);
|
|
9340
10210
|
for (const member of members) {
|
|
9341
|
-
const memberPath =
|
|
10211
|
+
const memberPath = path60.join(wtPath, member.folder);
|
|
9342
10212
|
const materialized = existsSync5(memberPath);
|
|
9343
10213
|
const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
|
|
9344
10214
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
@@ -9369,16 +10239,16 @@ async function runDoctor2() {
|
|
|
9369
10239
|
for (const member of members) {
|
|
9370
10240
|
const entries = await listWorktrees(member.absPath);
|
|
9371
10241
|
for (const entry of entries) {
|
|
9372
|
-
const entryPath =
|
|
9373
|
-
const marker = `${
|
|
10242
|
+
const entryPath = path60.normalize(entry.path);
|
|
10243
|
+
const marker = `${path60.sep}.claude${path60.sep}worktrees${path60.sep}`;
|
|
9374
10244
|
const idx = entryPath.indexOf(marker);
|
|
9375
10245
|
if (idx === -1) continue;
|
|
9376
10246
|
const rest = entryPath.slice(idx + marker.length);
|
|
9377
|
-
const slugName = rest.split(
|
|
10247
|
+
const slugName = rest.split(path60.sep)[0];
|
|
9378
10248
|
if (!slugName) continue;
|
|
9379
|
-
if (!existsSync5(
|
|
10249
|
+
if (!existsSync5(path60.join(wsWorktreesDir, slugName))) {
|
|
9380
10250
|
orphans.push(
|
|
9381
|
-
`${member.id}: worktree registered at ${entry.path} but ${
|
|
10251
|
+
`${member.id}: worktree registered at ${entry.path} but ${path60.join(wsWorktreesDir, slugName)} no longer exists \u2014 run \`git -C "${member.absPath}" worktree prune\`.`
|
|
9382
10252
|
);
|
|
9383
10253
|
}
|
|
9384
10254
|
}
|
|
@@ -9389,7 +10259,7 @@ async function runDoctor2() {
|
|
|
9389
10259
|
|
|
9390
10260
|
// src/workspace/worktree/hydrate-command.ts
|
|
9391
10261
|
import { existsSync as existsSync6 } from "node:fs";
|
|
9392
|
-
import
|
|
10262
|
+
import path61 from "node:path";
|
|
9393
10263
|
async function runHydrateWorktree(opts) {
|
|
9394
10264
|
const rootInfo = await resolveRoots();
|
|
9395
10265
|
if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
|
|
@@ -9400,7 +10270,7 @@ async function runHydrateWorktree(opts) {
|
|
|
9400
10270
|
}
|
|
9401
10271
|
const workspaceRoot = rootInfo.mainRoot;
|
|
9402
10272
|
const wtPath = rootInfo.repoRoot;
|
|
9403
|
-
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${
|
|
10273
|
+
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path61.sep}`)) {
|
|
9404
10274
|
return {
|
|
9405
10275
|
ok: false,
|
|
9406
10276
|
error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
|
|
@@ -9412,13 +10282,13 @@ async function runHydrateWorktree(opts) {
|
|
|
9412
10282
|
return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
|
|
9413
10283
|
}
|
|
9414
10284
|
const state = await readWorktreeState(wtPath);
|
|
9415
|
-
const slug = state?.slug ??
|
|
10285
|
+
const slug = state?.slug ?? path61.basename(wtPath);
|
|
9416
10286
|
const detectedBranch = await currentBranch(wtPath);
|
|
9417
10287
|
const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
|
|
9418
10288
|
if (opts.dryRun) {
|
|
9419
10289
|
const results2 = await Promise.all(
|
|
9420
10290
|
selected.map(async (member) => {
|
|
9421
|
-
const memberPath =
|
|
10291
|
+
const memberPath = path61.join(wtPath, member.folder);
|
|
9422
10292
|
if (existsSync6(memberPath)) {
|
|
9423
10293
|
return { member: member.id, folder: member.folder, status: "planned" };
|
|
9424
10294
|
}
|
|
@@ -9439,7 +10309,7 @@ async function runHydrateWorktree(opts) {
|
|
|
9439
10309
|
const results = await mapWithConcurrency(
|
|
9440
10310
|
selected,
|
|
9441
10311
|
async (member) => {
|
|
9442
|
-
const memberPath =
|
|
10312
|
+
const memberPath = path61.join(wtPath, member.folder);
|
|
9443
10313
|
let branchAction;
|
|
9444
10314
|
if (!existsSync6(memberPath)) {
|
|
9445
10315
|
if (!opts.materialize) {
|
|
@@ -9502,7 +10372,7 @@ async function runHydrateWorktree(opts) {
|
|
|
9502
10372
|
}
|
|
9503
10373
|
|
|
9504
10374
|
// src/workspace/worktree/list.ts
|
|
9505
|
-
import
|
|
10375
|
+
import path62 from "node:path";
|
|
9506
10376
|
import fs42 from "fs-extra";
|
|
9507
10377
|
async function runList2() {
|
|
9508
10378
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -9515,12 +10385,12 @@ async function runList2() {
|
|
|
9515
10385
|
const configuredMembers = await readMembers(rootInfo);
|
|
9516
10386
|
const worktrees = [];
|
|
9517
10387
|
for (const slug of slugs) {
|
|
9518
|
-
const wtPath =
|
|
10388
|
+
const wtPath = path62.join(dir, slug);
|
|
9519
10389
|
const state = await readWorktreeState(wtPath);
|
|
9520
10390
|
const members = [];
|
|
9521
10391
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
9522
10392
|
for (const m of source) {
|
|
9523
|
-
const memberPath =
|
|
10393
|
+
const memberPath = path62.join(wtPath, m.folder);
|
|
9524
10394
|
const materialized = await fs42.pathExists(memberPath);
|
|
9525
10395
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
9526
10396
|
members.push({
|
|
@@ -9542,7 +10412,7 @@ async function runList2() {
|
|
|
9542
10412
|
}
|
|
9543
10413
|
|
|
9544
10414
|
// src/workspace/worktree/remove.ts
|
|
9545
|
-
import
|
|
10415
|
+
import path63 from "node:path";
|
|
9546
10416
|
import fs43 from "fs-extra";
|
|
9547
10417
|
async function runRemove(opts) {
|
|
9548
10418
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -9569,7 +10439,7 @@ async function runRemove(opts) {
|
|
|
9569
10439
|
targets.push({
|
|
9570
10440
|
repo: configMember?.id ?? stateMember.id,
|
|
9571
10441
|
ownerRepo,
|
|
9572
|
-
path:
|
|
10442
|
+
path: path63.join(wsWorktreePath, stateMember.folder)
|
|
9573
10443
|
});
|
|
9574
10444
|
}
|
|
9575
10445
|
const existingTargets = [];
|
|
@@ -9637,11 +10507,8 @@ async function runRemove(opts) {
|
|
|
9637
10507
|
}
|
|
9638
10508
|
|
|
9639
10509
|
// src/commands/workspace/worktree.ts
|
|
9640
|
-
function
|
|
9641
|
-
|
|
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;
|
|
10510
|
+
function emitJson(result) {
|
|
10511
|
+
log(JSON.stringify(result, null, 2));
|
|
9645
10512
|
}
|
|
9646
10513
|
function describeInstall(hydration) {
|
|
9647
10514
|
if (hydration.install.plan.manager === "none") return "no install step (no recognized lockfile)";
|
|
@@ -9651,7 +10518,8 @@ function describeInstall(hydration) {
|
|
|
9651
10518
|
}
|
|
9652
10519
|
async function handleAdd(opts) {
|
|
9653
10520
|
if (!opts.slug) {
|
|
9654
|
-
error
|
|
10521
|
+
if (opts.json) emitJson({ ok: false, error: "Usage: haus workspace worktree add <slug>" });
|
|
10522
|
+
else error("Usage: haus workspace worktree add <slug>");
|
|
9655
10523
|
process.exitCode = 1;
|
|
9656
10524
|
return;
|
|
9657
10525
|
}
|
|
@@ -9664,10 +10532,16 @@ async function handleAdd(opts) {
|
|
|
9664
10532
|
};
|
|
9665
10533
|
const result = await runAdd(addOpts);
|
|
9666
10534
|
if ("error" in result) {
|
|
9667
|
-
|
|
10535
|
+
if (opts.json) emitJson(result);
|
|
10536
|
+
else error(result.error);
|
|
9668
10537
|
process.exitCode = 1;
|
|
9669
10538
|
return;
|
|
9670
10539
|
}
|
|
10540
|
+
if (opts.json) {
|
|
10541
|
+
emitJson(result);
|
|
10542
|
+
if (!result.dryRun && !result.ok) process.exitCode = 1;
|
|
10543
|
+
return;
|
|
10544
|
+
}
|
|
9671
10545
|
if (result.dryRun) {
|
|
9672
10546
|
log(
|
|
9673
10547
|
`[dry-run] Would create the workspace worktree at ${result.worktreePath} on branch "${result.branch}" (${result.workspaceWorktree.branchAction}).`
|
|
@@ -9707,10 +10581,16 @@ async function handleHydrate(opts) {
|
|
|
9707
10581
|
};
|
|
9708
10582
|
const result = await runHydrateWorktree(hydrateOpts);
|
|
9709
10583
|
if (!result.ok && "error" in result) {
|
|
9710
|
-
|
|
10584
|
+
if (opts.json) emitJson(result);
|
|
10585
|
+
else error(result.error);
|
|
9711
10586
|
process.exitCode = 1;
|
|
9712
10587
|
return;
|
|
9713
10588
|
}
|
|
10589
|
+
if (opts.json) {
|
|
10590
|
+
emitJson(result);
|
|
10591
|
+
if (!result.dryRun && !result.ok) process.exitCode = 1;
|
|
10592
|
+
return;
|
|
10593
|
+
}
|
|
9714
10594
|
if (result.dryRun) {
|
|
9715
10595
|
log(`[dry-run] Would hydrate "${result.slug}":`);
|
|
9716
10596
|
for (const r of result.results) {
|
|
@@ -9732,7 +10612,8 @@ async function handleHydrate(opts) {
|
|
|
9732
10612
|
}
|
|
9733
10613
|
async function handleRemove(opts) {
|
|
9734
10614
|
if (!opts.slug) {
|
|
9735
|
-
error
|
|
10615
|
+
if (opts.json) emitJson({ ok: false, error: "Usage: haus workspace worktree remove <slug>" });
|
|
10616
|
+
else error("Usage: haus workspace worktree remove <slug>");
|
|
9736
10617
|
process.exitCode = 1;
|
|
9737
10618
|
return;
|
|
9738
10619
|
}
|
|
@@ -9742,6 +10623,11 @@ async function handleRemove(opts) {
|
|
|
9742
10623
|
dryRun: Boolean(opts.dryRun)
|
|
9743
10624
|
};
|
|
9744
10625
|
const result = await runRemove(removeOpts);
|
|
10626
|
+
if (opts.json) {
|
|
10627
|
+
emitJson(result);
|
|
10628
|
+
if (!result.ok || "removed" in result && result.failed.length > 0) process.exitCode = 1;
|
|
10629
|
+
return;
|
|
10630
|
+
}
|
|
9745
10631
|
if (!result.ok && "error" in result) {
|
|
9746
10632
|
error(result.error);
|
|
9747
10633
|
process.exitCode = 1;
|
|
@@ -9766,8 +10652,13 @@ async function handleRemove(opts) {
|
|
|
9766
10652
|
}
|
|
9767
10653
|
}
|
|
9768
10654
|
}
|
|
9769
|
-
async function handleList() {
|
|
10655
|
+
async function handleList(opts) {
|
|
9770
10656
|
const result = await runList2();
|
|
10657
|
+
if (opts.json) {
|
|
10658
|
+
emitJson(result);
|
|
10659
|
+
if (!result.ok) process.exitCode = 1;
|
|
10660
|
+
return;
|
|
10661
|
+
}
|
|
9771
10662
|
if (!result.ok) {
|
|
9772
10663
|
error(result.error);
|
|
9773
10664
|
process.exitCode = 1;
|
|
@@ -9838,7 +10729,7 @@ async function runWorktree(action, opts = {}) {
|
|
|
9838
10729
|
case "remove":
|
|
9839
10730
|
return handleRemove(opts);
|
|
9840
10731
|
case "list":
|
|
9841
|
-
return handleList();
|
|
10732
|
+
return handleList(opts);
|
|
9842
10733
|
case "doctor":
|
|
9843
10734
|
return handleDoctor(opts);
|
|
9844
10735
|
}
|
|
@@ -9846,7 +10737,7 @@ async function runWorktree(action, opts = {}) {
|
|
|
9846
10737
|
|
|
9847
10738
|
// src/commands/workspace.ts
|
|
9848
10739
|
import { existsSync as existsSync10, statSync as statSync3 } from "node:fs";
|
|
9849
|
-
import
|
|
10740
|
+
import path72 from "node:path";
|
|
9850
10741
|
|
|
9851
10742
|
// src/commands/workspace/aggregate.ts
|
|
9852
10743
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -9901,18 +10792,18 @@ ${summaries.map(
|
|
|
9901
10792
|
|
|
9902
10793
|
// src/commands/workspace/doctor.ts
|
|
9903
10794
|
import { existsSync as existsSync7 } from "node:fs";
|
|
9904
|
-
import
|
|
10795
|
+
import path65 from "node:path";
|
|
9905
10796
|
|
|
9906
10797
|
// src/commands/workspace/manifest.ts
|
|
9907
10798
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
9908
|
-
import
|
|
10799
|
+
import path64 from "node:path";
|
|
9909
10800
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
9910
10801
|
function manifestPath(workspaceRoot) {
|
|
9911
10802
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
9912
10803
|
}
|
|
9913
10804
|
function hausVersion() {
|
|
9914
10805
|
try {
|
|
9915
|
-
const pkg = JSON.parse(readFileSync4(
|
|
10806
|
+
const pkg = JSON.parse(readFileSync4(path64.join(packageRoot(), "package.json"), "utf8"));
|
|
9916
10807
|
return pkg.version ?? "0.0.0";
|
|
9917
10808
|
} catch {
|
|
9918
10809
|
return "0.0.0";
|
|
@@ -10004,7 +10895,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
10004
10895
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
10005
10896
|
const catalogRefByRepo = [];
|
|
10006
10897
|
for (const repo of config.repos) {
|
|
10007
|
-
const repoRoot =
|
|
10898
|
+
const repoRoot = path65.resolve(workspaceRoot, repo.path);
|
|
10008
10899
|
const entry = manifestByName.get(repo.name);
|
|
10009
10900
|
if (!entry) {
|
|
10010
10901
|
flag({
|
|
@@ -10077,7 +10968,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
10077
10968
|
});
|
|
10078
10969
|
continue;
|
|
10079
10970
|
}
|
|
10080
|
-
if (!existsSync7(
|
|
10971
|
+
if (!existsSync7(path65.join(workspaceRoot, entry.path))) {
|
|
10081
10972
|
flag({
|
|
10082
10973
|
repo: entry.repo,
|
|
10083
10974
|
kind: "missing-linked-context-copy",
|
|
@@ -10120,15 +11011,15 @@ function emit(args) {
|
|
|
10120
11011
|
}
|
|
10121
11012
|
|
|
10122
11013
|
// src/workspace/link-context/link.ts
|
|
10123
|
-
import
|
|
11014
|
+
import path68 from "node:path";
|
|
10124
11015
|
import fs46 from "fs-extra";
|
|
10125
11016
|
|
|
10126
11017
|
// src/workspace/link-context/plan.ts
|
|
10127
|
-
import
|
|
11018
|
+
import path67 from "node:path";
|
|
10128
11019
|
import fs45 from "fs-extra";
|
|
10129
11020
|
|
|
10130
11021
|
// src/workspace/link-context/scan-source.ts
|
|
10131
|
-
import
|
|
11022
|
+
import path66 from "node:path";
|
|
10132
11023
|
import fs44 from "fs-extra";
|
|
10133
11024
|
var ASSET_SUBDIR = {
|
|
10134
11025
|
skill: "skills",
|
|
@@ -10137,36 +11028,36 @@ var ASSET_SUBDIR = {
|
|
|
10137
11028
|
};
|
|
10138
11029
|
async function hasLinkableContext(member) {
|
|
10139
11030
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
10140
|
-
if (await fs44.pathExists(
|
|
11031
|
+
if (await fs44.pathExists(path66.join(member.absPath, ".claude", subdir))) return true;
|
|
10141
11032
|
}
|
|
10142
11033
|
return false;
|
|
10143
11034
|
}
|
|
10144
11035
|
async function collectSkills(member) {
|
|
10145
|
-
const dir =
|
|
11036
|
+
const dir = path66.join(member.absPath, ".claude", "skills");
|
|
10146
11037
|
if (!await fs44.pathExists(dir)) return [];
|
|
10147
11038
|
const names = await fs44.readdir(dir);
|
|
10148
11039
|
const out = [];
|
|
10149
11040
|
for (const name of [...names].sort()) {
|
|
10150
|
-
const abs =
|
|
11041
|
+
const abs = path66.join(dir, name);
|
|
10151
11042
|
const stat = await fs44.lstat(abs);
|
|
10152
11043
|
if (!stat.isDirectory()) continue;
|
|
10153
|
-
if (!await fs44.pathExists(
|
|
10154
|
-
out.push({ type: "skill", name, sourceRelPath:
|
|
11044
|
+
if (!await fs44.pathExists(path66.join(abs, "SKILL.md"))) continue;
|
|
11045
|
+
out.push({ type: "skill", name, sourceRelPath: path66.posix.join(".claude", "skills", name) });
|
|
10155
11046
|
}
|
|
10156
11047
|
return out;
|
|
10157
11048
|
}
|
|
10158
11049
|
async function collectFlatMarkdown(member, type) {
|
|
10159
11050
|
const subdir = ASSET_SUBDIR[type];
|
|
10160
|
-
const dir =
|
|
11051
|
+
const dir = path66.join(member.absPath, ".claude", subdir);
|
|
10161
11052
|
if (!await fs44.pathExists(dir)) return [];
|
|
10162
11053
|
const names = await fs44.readdir(dir);
|
|
10163
11054
|
const out = [];
|
|
10164
11055
|
for (const fileName of [...names].sort()) {
|
|
10165
11056
|
if (!fileName.endsWith(".md")) continue;
|
|
10166
|
-
const abs =
|
|
11057
|
+
const abs = path66.join(dir, fileName);
|
|
10167
11058
|
if (!(await fs44.lstat(abs)).isFile()) continue;
|
|
10168
11059
|
const name = fileName.slice(0, -".md".length);
|
|
10169
|
-
out.push({ type, name, sourceRelPath:
|
|
11060
|
+
out.push({ type, name, sourceRelPath: path66.posix.join(".claude", subdir, fileName) });
|
|
10170
11061
|
}
|
|
10171
11062
|
return out;
|
|
10172
11063
|
}
|
|
@@ -10181,9 +11072,9 @@ async function collectSourceAssets(member) {
|
|
|
10181
11072
|
|
|
10182
11073
|
// src/workspace/link-context/plan.ts
|
|
10183
11074
|
function repoFolderPrefix(member) {
|
|
10184
|
-
const base =
|
|
11075
|
+
const base = path67.posix.basename(member.folder);
|
|
10185
11076
|
if (base && base !== "." && base !== "/") return base;
|
|
10186
|
-
const fromAbs =
|
|
11077
|
+
const fromAbs = path67.basename(member.absPath);
|
|
10187
11078
|
return fromAbs || member.id;
|
|
10188
11079
|
}
|
|
10189
11080
|
function assetKey(type, destKey) {
|
|
@@ -10240,7 +11131,7 @@ async function buildLinkPlan(members) {
|
|
|
10240
11131
|
name: asset.name,
|
|
10241
11132
|
destKey,
|
|
10242
11133
|
sourceRelPath: asset.sourceRelPath,
|
|
10243
|
-
sourceAbsPath:
|
|
11134
|
+
sourceAbsPath: path67.join(member.absPath, asset.sourceRelPath),
|
|
10244
11135
|
sourceHash
|
|
10245
11136
|
});
|
|
10246
11137
|
}
|
|
@@ -10251,7 +11142,7 @@ async function buildLinkPlan(members) {
|
|
|
10251
11142
|
function destRelPathFor(type, destKey) {
|
|
10252
11143
|
const subdir = `${type}s`;
|
|
10253
11144
|
const leaf = type === "skill" ? destKey : `${destKey}.md`;
|
|
10254
|
-
return
|
|
11145
|
+
return path68.posix.join(".claude", subdir, leaf);
|
|
10255
11146
|
}
|
|
10256
11147
|
async function runLinkContext(workspaceRoot, opts = {}) {
|
|
10257
11148
|
const write = opts.write ?? false;
|
|
@@ -10277,9 +11168,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
10277
11168
|
const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10278
11169
|
for (const entry of plan.entries) {
|
|
10279
11170
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
10280
|
-
const destAbs =
|
|
11171
|
+
const destAbs = path68.join(workspaceRoot, destRel);
|
|
10281
11172
|
if (write) {
|
|
10282
|
-
await fs46.ensureDir(
|
|
11173
|
+
await fs46.ensureDir(path68.dirname(destAbs));
|
|
10283
11174
|
if (await fs46.pathExists(destAbs)) await fs46.remove(destAbs);
|
|
10284
11175
|
await fs46.copy(entry.sourceAbsPath, destAbs, {
|
|
10285
11176
|
overwrite: true,
|
|
@@ -10368,10 +11259,10 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
|
|
|
10368
11259
|
|
|
10369
11260
|
// src/commands/workspace/setup.ts
|
|
10370
11261
|
import { existsSync as existsSync8, statSync } from "node:fs";
|
|
10371
|
-
import
|
|
11262
|
+
import path70 from "node:path";
|
|
10372
11263
|
|
|
10373
11264
|
// src/claude/write-workspace-claude-md.ts
|
|
10374
|
-
import
|
|
11265
|
+
import path69 from "node:path";
|
|
10375
11266
|
import fs47 from "fs-extra";
|
|
10376
11267
|
var WORKSPACE_SENTINELS = { begin: WORKSPACE_BLOCK_BEGIN, end: WORKSPACE_BLOCK_END };
|
|
10377
11268
|
function buildWorkspaceImportBlock(client, members) {
|
|
@@ -10391,7 +11282,7 @@ ${WORKSPACE_BLOCK_END}`;
|
|
|
10391
11282
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
10392
11283
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
10393
11284
|
const dryRun = opts.dryRun ?? false;
|
|
10394
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
11285
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path69.join(workspaceRoot, "CLAUDE.md");
|
|
10395
11286
|
const say = opts.quiet ? () => {
|
|
10396
11287
|
} : log;
|
|
10397
11288
|
const existed = await fs47.pathExists(filePath);
|
|
@@ -10422,20 +11313,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
10422
11313
|
|
|
10423
11314
|
// src/commands/workspace/setup.ts
|
|
10424
11315
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
10425
|
-
let dir =
|
|
11316
|
+
let dir = path70.resolve(start);
|
|
10426
11317
|
for (; ; ) {
|
|
10427
|
-
if (existsSync8(
|
|
10428
|
-
const parent =
|
|
10429
|
-
if (parent === dir) return
|
|
11318
|
+
if (existsSync8(path70.join(dir, WORKSPACE_FILE))) return dir;
|
|
11319
|
+
const parent = path70.dirname(dir);
|
|
11320
|
+
if (parent === dir) return path70.resolve(start);
|
|
10430
11321
|
dir = parent;
|
|
10431
11322
|
}
|
|
10432
11323
|
}
|
|
10433
11324
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
10434
|
-
return
|
|
11325
|
+
return path70.resolve(workspaceRoot, repoPath) === path70.resolve(workspaceRoot);
|
|
10435
11326
|
}
|
|
10436
11327
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
10437
11328
|
const apply = options.write ?? false;
|
|
10438
|
-
const configText = await readText(
|
|
11329
|
+
const configText = await readText(path70.join(workspaceRoot, WORKSPACE_FILE));
|
|
10439
11330
|
if (!configText) {
|
|
10440
11331
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
10441
11332
|
process.exitCode = 1;
|
|
@@ -10462,7 +11353,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10462
11353
|
const statuses = [];
|
|
10463
11354
|
const aggregateInputs = [];
|
|
10464
11355
|
for (const repo of repos) {
|
|
10465
|
-
const repoRoot =
|
|
11356
|
+
const repoRoot = path70.resolve(workspaceRoot, repo.path);
|
|
10466
11357
|
say(`
|
|
10467
11358
|
\u2192 ${repo.name} (${repo.path})`);
|
|
10468
11359
|
try {
|
|
@@ -10502,7 +11393,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10502
11393
|
process.exitCode = 1;
|
|
10503
11394
|
return result2;
|
|
10504
11395
|
}
|
|
10505
|
-
throw err;
|
|
11396
|
+
throw new Error(`Setup failed for ${repo.name}: ${message}`, { cause: err });
|
|
10506
11397
|
}
|
|
10507
11398
|
error(`Setup failed for ${repo.name}: ${message}`);
|
|
10508
11399
|
}
|
|
@@ -10552,7 +11443,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10552
11443
|
const status = statusByName.get(repo.name);
|
|
10553
11444
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
10554
11445
|
if (status?.status === "ok") {
|
|
10555
|
-
const lock = await checkLock(
|
|
11446
|
+
const lock = await checkLock(path70.resolve(workspaceRoot, repo.path));
|
|
10556
11447
|
manifestRepos.push({
|
|
10557
11448
|
name: repo.name,
|
|
10558
11449
|
path: repo.path,
|
|
@@ -10608,7 +11499,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10608
11499
|
try {
|
|
10609
11500
|
const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
|
|
10610
11501
|
if (linkResult.ok) {
|
|
10611
|
-
written.push(...linkResult.added.map((rel) =>
|
|
11502
|
+
written.push(...linkResult.added.map((rel) => path70.join(workspaceRoot, rel)));
|
|
10612
11503
|
if (!options.json) {
|
|
10613
11504
|
say(
|
|
10614
11505
|
`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 +11531,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
10640
11531
|
|
|
10641
11532
|
// src/commands/workspace/undo.ts
|
|
10642
11533
|
import { existsSync as existsSync9, statSync as statSync2 } from "node:fs";
|
|
10643
|
-
import
|
|
11534
|
+
import path71 from "node:path";
|
|
10644
11535
|
import fs48 from "fs-extra";
|
|
10645
11536
|
var WORKSPACE_AGGREGATE_FILES = [
|
|
10646
11537
|
"workspace-summary.json",
|
|
@@ -10666,6 +11557,52 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10666
11557
|
if (config.repos.length === 0) {
|
|
10667
11558
|
log(`No repos configured in ${WORKSPACE_FILE}.`);
|
|
10668
11559
|
}
|
|
11560
|
+
if (options.dryRun) {
|
|
11561
|
+
for (const repo of config.repos) {
|
|
11562
|
+
const repoRoot = path71.resolve(workspaceRoot, repo.path);
|
|
11563
|
+
log(`
|
|
11564
|
+
\u2192 ${repo.name} (${repo.path})`);
|
|
11565
|
+
try {
|
|
11566
|
+
if (!existsSync9(repoRoot) || !statSync2(repoRoot).isDirectory()) {
|
|
11567
|
+
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
11568
|
+
}
|
|
11569
|
+
await runUndo({ dryRun: true, root: repoRoot });
|
|
11570
|
+
} catch (err) {
|
|
11571
|
+
warn(
|
|
11572
|
+
`[dry-run] Undo preview failed for ${repo.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
11573
|
+
);
|
|
11574
|
+
}
|
|
11575
|
+
}
|
|
11576
|
+
const linkedContext2 = await readLinkedContext(workspaceRoot);
|
|
11577
|
+
for (const entry of linkedContext2) {
|
|
11578
|
+
const abs = resolveContainedPath(workspaceRoot, entry.path);
|
|
11579
|
+
if (abs === null) {
|
|
11580
|
+
warn(`Refusing to remove linked context path outside the workspace root: ${entry.path}`);
|
|
11581
|
+
continue;
|
|
11582
|
+
}
|
|
11583
|
+
if (await fs48.pathExists(abs)) {
|
|
11584
|
+
log(`[dry-run] Would remove ${entry.path} (linked ${entry.type} from ${entry.repo})`);
|
|
11585
|
+
}
|
|
11586
|
+
}
|
|
11587
|
+
const workspaceArtifacts = [
|
|
11588
|
+
...WORKSPACE_AGGREGATE_FILES.map((rel) => hausPath(workspaceRoot, rel)),
|
|
11589
|
+
manifestPath(workspaceRoot),
|
|
11590
|
+
hausPath(workspaceRoot, "WORKSPACE.md")
|
|
11591
|
+
];
|
|
11592
|
+
for (const abs of workspaceArtifacts) {
|
|
11593
|
+
if (await fs48.pathExists(abs)) {
|
|
11594
|
+
log(`[dry-run] Would remove ${path71.relative(workspaceRoot, abs)}`);
|
|
11595
|
+
}
|
|
11596
|
+
}
|
|
11597
|
+
if (!await fs48.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
|
|
11598
|
+
const claudeMd = await readText(path71.join(workspaceRoot, "CLAUDE.md"));
|
|
11599
|
+
if (claudeMd?.includes(WORKSPACE_BLOCK_BEGIN) || claudeMd && hasLegacyWorkspaceBlock(claudeMd)) {
|
|
11600
|
+
log("[dry-run] Would remove the workspace haus import block from CLAUDE.md");
|
|
11601
|
+
}
|
|
11602
|
+
}
|
|
11603
|
+
log(`[dry-run] ${WORKSPACE_FILE} would be left in place. Nothing was changed.`);
|
|
11604
|
+
return;
|
|
11605
|
+
}
|
|
10669
11606
|
if (!options.yes) {
|
|
10670
11607
|
const ok = await confirm(
|
|
10671
11608
|
`Undo haus setup for ${config.repos.length} repo(s) in this workspace, plus workspace-root artifacts?`
|
|
@@ -10677,7 +11614,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10677
11614
|
}
|
|
10678
11615
|
let anyRepoFailed = false;
|
|
10679
11616
|
for (const repo of config.repos) {
|
|
10680
|
-
const repoRoot =
|
|
11617
|
+
const repoRoot = path71.resolve(workspaceRoot, repo.path);
|
|
10681
11618
|
log(`
|
|
10682
11619
|
\u2192 ${repo.name} (${repo.path})`);
|
|
10683
11620
|
try {
|
|
@@ -10706,20 +11643,20 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10706
11643
|
const abs = hausPath(workspaceRoot, rel);
|
|
10707
11644
|
if (await fs48.pathExists(abs)) {
|
|
10708
11645
|
await fs48.remove(abs);
|
|
10709
|
-
log(`Removed ${
|
|
11646
|
+
log(`Removed ${path71.relative(workspaceRoot, abs)}`);
|
|
10710
11647
|
}
|
|
10711
11648
|
}
|
|
10712
11649
|
const manifestFile = manifestPath(workspaceRoot);
|
|
10713
11650
|
if (await fs48.pathExists(manifestFile)) {
|
|
10714
11651
|
await fs48.remove(manifestFile);
|
|
10715
|
-
log(`Removed ${
|
|
11652
|
+
log(`Removed ${path71.relative(workspaceRoot, manifestFile)}`);
|
|
10716
11653
|
}
|
|
10717
11654
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
10718
11655
|
if (await fs48.pathExists(workspaceMdPath)) {
|
|
10719
11656
|
await fs48.remove(workspaceMdPath);
|
|
10720
|
-
log(`Removed ${
|
|
11657
|
+
log(`Removed ${path71.relative(workspaceRoot, workspaceMdPath)}`);
|
|
10721
11658
|
} else {
|
|
10722
|
-
const claudeMdPath =
|
|
11659
|
+
const claudeMdPath = path71.join(workspaceRoot, "CLAUDE.md");
|
|
10723
11660
|
if (existsSync9(claudeMdPath)) {
|
|
10724
11661
|
const prev = await readText(claudeMdPath);
|
|
10725
11662
|
if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
|
|
@@ -10758,12 +11695,6 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10758
11695
|
}
|
|
10759
11696
|
|
|
10760
11697
|
// 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
11698
|
function normalizeMaxDepth(maxDepth) {
|
|
10768
11699
|
if (maxDepth === void 0) return void 0;
|
|
10769
11700
|
const n = typeof maxDepth === "number" ? maxDepth : Number.parseInt(maxDepth, 10);
|
|
@@ -10794,7 +11725,7 @@ relationships: []
|
|
|
10794
11725
|
log("Workspace initialized.");
|
|
10795
11726
|
}
|
|
10796
11727
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
10797
|
-
const configText = await readText(
|
|
11728
|
+
const configText = await readText(path72.join(workspaceRoot, WORKSPACE_FILE));
|
|
10798
11729
|
if (!configText) {
|
|
10799
11730
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
10800
11731
|
process.exitCode = 1;
|
|
@@ -10816,7 +11747,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
10816
11747
|
const inputs = [];
|
|
10817
11748
|
const failures = [];
|
|
10818
11749
|
for (const repo of config.repos) {
|
|
10819
|
-
const repoRoot =
|
|
11750
|
+
const repoRoot = path72.resolve(workspaceRoot, repo.path);
|
|
10820
11751
|
try {
|
|
10821
11752
|
if (!existsSync10(repoRoot) || !statSync3(repoRoot).isDirectory()) {
|
|
10822
11753
|
throw new Error("path is not a directory");
|
|
@@ -10864,14 +11795,14 @@ async function runWorkspace(action, options = {}) {
|
|
|
10864
11795
|
dryRun: options.dryRun,
|
|
10865
11796
|
json: options.json,
|
|
10866
11797
|
continueOnError: options.continueOnError,
|
|
10867
|
-
only:
|
|
11798
|
+
only: normalizeOnly(options.only)
|
|
10868
11799
|
});
|
|
10869
11800
|
return;
|
|
10870
11801
|
case "doctor":
|
|
10871
11802
|
await runWorkspaceDoctor(workspaceRoot, { json: options.json });
|
|
10872
11803
|
return;
|
|
10873
11804
|
case "undo":
|
|
10874
|
-
await runWorkspaceUndo(workspaceRoot, { yes: options.yes });
|
|
11805
|
+
await runWorkspaceUndo(workspaceRoot, { yes: options.yes, dryRun: options.dryRun });
|
|
10875
11806
|
return;
|
|
10876
11807
|
case "link-context":
|
|
10877
11808
|
await runWorkspaceLinkContext(workspaceRoot, { write: options.write, json: options.json });
|
|
@@ -10882,7 +11813,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
10882
11813
|
// src/cli.ts
|
|
10883
11814
|
function cliVersion() {
|
|
10884
11815
|
try {
|
|
10885
|
-
const pkgPath =
|
|
11816
|
+
const pkgPath = path73.join(packageRoot(), "package.json");
|
|
10886
11817
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
10887
11818
|
return pkg.version ?? "0.0.0";
|
|
10888
11819
|
} catch {
|
|
@@ -10892,7 +11823,7 @@ function cliVersion() {
|
|
|
10892
11823
|
var program = new Command();
|
|
10893
11824
|
function validateRuntimeNodeVersion() {
|
|
10894
11825
|
try {
|
|
10895
|
-
const pkgPath =
|
|
11826
|
+
const pkgPath = path73.join(packageRoot(), "package.json");
|
|
10896
11827
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
10897
11828
|
const requiredRange = pkg.engines?.node;
|
|
10898
11829
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -10906,18 +11837,22 @@ function validateRuntimeNodeVersion() {
|
|
|
10906
11837
|
}
|
|
10907
11838
|
validateRuntimeNodeVersion();
|
|
10908
11839
|
program.name("haus").description("Haus AI workflow CLI").version(cliVersion());
|
|
10909
|
-
program.command("scan").
|
|
10910
|
-
|
|
11840
|
+
program.command("scan").description(
|
|
11841
|
+
"Scan the repo for roles, dependencies, and package manager; writes context-map.json"
|
|
11842
|
+
).option("--json").action(runScan);
|
|
11843
|
+
program.command("recommend").description("Score catalog items against the scanned context and write recommendation.json").option("--json").option(
|
|
10911
11844
|
"--include <ids...>",
|
|
10912
11845
|
"Force opt-in catalog items into the recommendation as manual selections (space- or comma-separated ids)"
|
|
10913
11846
|
).action(runRecommend);
|
|
10914
|
-
program.command("setup-project").option("--json").option(
|
|
11847
|
+
program.command("setup-project").description("Fast full setup: scan, recommend, and optionally apply Claude files").option("--json").option(
|
|
10915
11848
|
"--force",
|
|
10916
11849
|
"Write recommendation.json/haus.lock.json even when zero catalog items matched at all"
|
|
10917
11850
|
).action(runSetupProject);
|
|
10918
|
-
program.command("doctor").
|
|
11851
|
+
program.command("doctor").description(
|
|
11852
|
+
"Validate project setup: hooks contract, managed files, cache freshness, CLI version"
|
|
11853
|
+
).option("--hooks", "Verify .claude/settings.json matches the hook contract").option("--json", "Emit all findings as one JSON document on stdout instead of human output").action(runDoctor);
|
|
10919
11854
|
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(
|
|
11855
|
+
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
11856
|
"--ids <ids...>",
|
|
10922
11857
|
"Install exactly these recommended item ids non-interactively (skill backend; space- or comma-separated)"
|
|
10923
11858
|
).option(
|
|
@@ -10930,33 +11865,41 @@ program.command("apply").option("--dry-run").option("--write").option("--select"
|
|
|
10930
11865
|
"--prune",
|
|
10931
11866
|
"Remove lock-tracked catalog items no longer in the current recommendation (hash-gated, backed up first)"
|
|
10932
11867
|
).action(runApply);
|
|
10933
|
-
program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
|
|
10934
|
-
var backups = program.command("backups");
|
|
10935
|
-
backups.command("list").description("List backups under .haus-workflow/backups/").action(() => runBackups("list"));
|
|
10936
|
-
backups.command("restore <id>").description("Restore a specific backup by id (see `haus backups list`)").option("-y, --yes", "Skip confirmation").
|
|
10937
|
-
|
|
10938
|
-
|
|
11868
|
+
program.command("undo").description("Remove haus-managed project files; preserves user-owned .claude/ content").option("-y, --yes", "Skip confirmation").option("--dry-run", "Print what would be removed without deleting anything").action(runUndo);
|
|
11869
|
+
var backups = program.command("backups").description("List, restore, or prune backups under .haus-workflow/backups/");
|
|
11870
|
+
backups.command("list").description("List backups under .haus-workflow/backups/").option("--json", "Emit the backup entries as JSON (newest first)").action((opts) => runBackups("list", { json: opts.json }));
|
|
11871
|
+
backups.command("restore <id>").description("Restore a specific backup by id (see `haus backups list`)").option("-y, --yes", "Skip confirmation").option("--json", "Emit a machine-readable result (JSON-only stdout; warnings on stderr)").option("--dry-run", "Print what would be restored without writing anything").action(
|
|
11872
|
+
(id, opts) => runBackups("restore", { id, yes: opts.yes, json: opts.json, dryRun: opts.dryRun })
|
|
11873
|
+
);
|
|
11874
|
+
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").option("--json", "Emit a machine-readable result (JSON-only stdout; warnings on stderr)").option("--dry-run", "Print what would be pruned without deleting anything").action(
|
|
11875
|
+
(opts) => runBackups("prune", {
|
|
11876
|
+
olderThan: opts.olderThan,
|
|
11877
|
+
dryRun: opts.dryRun,
|
|
11878
|
+
keep: opts.keep,
|
|
11879
|
+
yes: opts.yes,
|
|
11880
|
+
json: opts.json
|
|
11881
|
+
})
|
|
10939
11882
|
);
|
|
10940
|
-
program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
|
|
10941
|
-
program.command("init").option("--json").action(runInit);
|
|
11883
|
+
program.command("explain-recommendation").description("Print a per-item breakdown of the current recommendation, with skip reasons").option("--json").action(runExplainRecommendation);
|
|
11884
|
+
program.command("init").description("First-run setup; delegates to setup-project if .haus-workflow/ does not exist").option("--json").action(runInit);
|
|
10942
11885
|
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
11886
|
(url, dir, opts) => runClone(url, { dir, dryRun: opts.dryRun })
|
|
10944
11887
|
);
|
|
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);
|
|
11888
|
+
program.command("refresh").description("Re-scan the project, refresh recommendations, and update scan artifacts").action(runRefresh);
|
|
11889
|
+
program.command("catalog-audit").description("Check the bundled catalog manifest for unsupported or forbidden stacks").action(runCatalogAudit);
|
|
11890
|
+
program.command("validate-catalog").description("Validate a catalog manifest at an explicit path").argument("[manifest]").action(runValidateCatalog);
|
|
11891
|
+
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
11892
|
program.command("ci-gate").description(
|
|
10950
11893
|
"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
11894
|
).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);
|
|
11895
|
+
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
11896
|
program.command("scaffold [ids...]").description(
|
|
10954
11897
|
"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
11898
|
).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));
|
|
11899
|
+
program.command("uninstall").description("Remove Haus-managed skills, agents, and hook entries from ~/.claude/").option("--force").option("-y, --yes", "Skip confirmation").option("--dry-run", "Print what would be removed without deleting anything").action(runUninstallCommand);
|
|
11900
|
+
var guard = program.command("guard").description("PreToolUse hook guards: block dangerous bash commands and sensitive file access");
|
|
11901
|
+
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));
|
|
11902
|
+
guard.command("bash").description("Block dangerous shell commands (run by the Claude Code hook)").option("--from-hook").action((opts) => runGuard("bash", opts));
|
|
10960
11903
|
function registerDecisionsCommand(name) {
|
|
10961
11904
|
const cmd = program.command(name).description("Architecture decision records (ADR) gate and drafts");
|
|
10962
11905
|
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,13 +11912,13 @@ function registerDecisionsCommand(name) {
|
|
|
10969
11912
|
}
|
|
10970
11913
|
registerDecisionsCommand("decisions");
|
|
10971
11914
|
registerDecisionsCommand("adr");
|
|
10972
|
-
var workspace = program.command("workspace");
|
|
11915
|
+
var workspace = program.command("workspace").description("Multi-repo workspace commands: discover, setup, link-context, worktree, doctor");
|
|
10973
11916
|
workspace.command("init").option("-y, --yes", "Skip confirmation when haus.workspace.yaml already exists").action((opts) => runWorkspace("init", opts));
|
|
10974
11917
|
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
11918
|
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));
|
|
10976
11919
|
workspace.command("setup").description("Per-repo setup loop + workspace layer + manifest").option("--write", "Apply changes (default previews only)").option("--dry-run", "Preview changes without writing").option("--json", "Emit machine-readable per-repo output").option("--continue-on-error", "Keep going past a failed repo (default fail-fast)").option("--only <names>", "Restrict to comma-separated repo names").action((opts) => runWorkspace("setup", opts));
|
|
10977
11920
|
workspace.command("doctor").description("Report workspace drift against the manifest").option("--json", "Output the manifest and drift array as JSON").action((opts) => runWorkspace("doctor", opts));
|
|
10978
|
-
workspace.command("undo").description("Revert haus setup for every configured repo, plus workspace-root artifacts").option("-y, --yes", "Skip confirmation").action((opts) => runWorkspace("undo", opts));
|
|
11921
|
+
workspace.command("undo").description("Revert haus setup for every configured repo, plus workspace-root artifacts").option("-y, --yes", "Skip confirmation").option("--dry-run", "Print what would be removed without deleting anything").action((opts) => runWorkspace("undo", opts));
|
|
10979
11922
|
workspace.command("link-context").description(
|
|
10980
11923
|
"Copy each member repo's skill/agent/command into the workspace root's .claude/, prefixed to avoid collisions"
|
|
10981
11924
|
).option("--write", "Persist copies + manifest + gitignore (default previews only)").option("--json", "Output the link result as JSON").action((opts) => runWorkspace("link-context", opts));
|
|
@@ -10984,29 +11927,31 @@ var worktree = workspace.command("worktree").description(
|
|
|
10984
11927
|
);
|
|
10985
11928
|
worktree.command("add <slug>").description(
|
|
10986
11929
|
"Create a workspace worktree plus one member worktree per repo, on a mirrored branch"
|
|
10987
|
-
).option("--branch <name>", "Branch to create/checkout (default: the slug)").option("--only <repos>", "Restrict to comma- or space-separated member ids/folders").option("--no-hydrate", "Skip hydration (CoW clone + install-reconciliation)").option("--dry-run", "Preview without writing or running anything").action(
|
|
11930
|
+
).option("--branch <name>", "Branch to create/checkout (default: the slug)").option("--only <repos>", "Restrict to comma- or space-separated member ids/folders").option("--no-hydrate", "Skip hydration (CoW clone + install-reconciliation)").option("--dry-run", "Preview without writing or running anything").option("--json", "Emit the structured add result as JSON").action(
|
|
10988
11931
|
(slug, opts) => runWorktree("add", {
|
|
10989
11932
|
slug,
|
|
10990
11933
|
branch: opts.branch,
|
|
10991
11934
|
only: opts.only,
|
|
10992
11935
|
hydrate: opts.hydrate,
|
|
10993
|
-
dryRun: opts.dryRun
|
|
11936
|
+
dryRun: opts.dryRun,
|
|
11937
|
+
json: opts.json
|
|
10994
11938
|
})
|
|
10995
11939
|
);
|
|
10996
11940
|
worktree.command("hydrate").description("Re-run CoW clone + install-reconciliation for the current workspace worktree").option("--only <repos>", "Restrict to comma- or space-separated member ids/folders").option("--force", "Re-clone hydration targets even if already present").option(
|
|
10997
11941
|
"--materialize",
|
|
10998
11942
|
"Also `git worktree add` any member missing entirely (e.g. a worktree created by plain `git worktree add` outside haus) before hydrating it"
|
|
10999
|
-
).option("--dry-run", "Preview without writing or running anything").action(
|
|
11943
|
+
).option("--dry-run", "Preview without writing or running anything").option("--json", "Emit the structured hydrate result as JSON").action(
|
|
11000
11944
|
(opts) => runWorktree("hydrate", {
|
|
11001
11945
|
only: opts.only,
|
|
11002
11946
|
force: opts.force,
|
|
11003
11947
|
materialize: opts.materialize,
|
|
11004
|
-
dryRun: opts.dryRun
|
|
11948
|
+
dryRun: opts.dryRun,
|
|
11949
|
+
json: opts.json
|
|
11005
11950
|
})
|
|
11006
11951
|
);
|
|
11007
|
-
worktree.command("list").description("List materialized workspace worktrees and their member status").action(() => runWorktree("list"));
|
|
11008
|
-
worktree.command("remove <slug>").description("Remove a workspace worktree and its member worktrees").option("--force", "Bypass the uncommitted/unpushed-work refusal").option("--dry-run", "Preview without removing anything").action(
|
|
11009
|
-
(slug, opts) => runWorktree("remove", { slug, force: opts.force, dryRun: opts.dryRun })
|
|
11952
|
+
worktree.command("list").description("List materialized workspace worktrees and their member status").option("--json", "Emit the structured list result as JSON").action((opts) => runWorktree("list", { json: opts.json }));
|
|
11953
|
+
worktree.command("remove <slug>").description("Remove a workspace worktree and its member worktrees").option("--force", "Bypass the uncommitted/unpushed-work refusal").option("--dry-run", "Preview without removing anything").option("--json", "Emit the structured remove result as JSON").action(
|
|
11954
|
+
(slug, opts) => runWorktree("remove", { slug, force: opts.force, dryRun: opts.dryRun, json: opts.json })
|
|
11010
11955
|
);
|
|
11011
11956
|
worktree.command("doctor").description("Fast, side-effect-free health check for the current workspace worktree").option("--from-hook", "Emit for the SessionStart hook \u2014 always exits 0").action((opts) => runWorktree("doctor", { fromHook: opts.fromHook }));
|
|
11012
11957
|
program.parseAsync(process.argv).catch((err) => {
|