@haus-tech/haus-workflow 1.5.6 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/cli.js +1084 -357
- package/library/catalog/manifest.json +45 -1
- package/library/catalog/validation-rules.json +0 -2
- 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
|
|
@@ -944,8 +960,6 @@ var validation_rules_default = {
|
|
|
944
960
|
"java",
|
|
945
961
|
"spring",
|
|
946
962
|
"kotlin",
|
|
947
|
-
"swift",
|
|
948
|
-
"android",
|
|
949
963
|
"flutter",
|
|
950
964
|
"dart",
|
|
951
965
|
"c++",
|
|
@@ -1167,6 +1181,16 @@ function extractUseWhenSection(text) {
|
|
|
1167
1181
|
const next = tail.search(/\n##\s+/);
|
|
1168
1182
|
return next < 0 ? tail : tail.slice(0, next);
|
|
1169
1183
|
}
|
|
1184
|
+
function indentedContinuationLines(lines, idx) {
|
|
1185
|
+
const out = [];
|
|
1186
|
+
for (let j = idx + 1; j < lines.length; j++) {
|
|
1187
|
+
const line2 = lines[j];
|
|
1188
|
+
if (line2.trim() === "") continue;
|
|
1189
|
+
if (!/^\s+\S/.test(line2)) break;
|
|
1190
|
+
out.push(line2.trim());
|
|
1191
|
+
}
|
|
1192
|
+
return out;
|
|
1193
|
+
}
|
|
1170
1194
|
function isYamlBlockScalarHeader(rest) {
|
|
1171
1195
|
return /^[>|][-+]?(\d+)?(?:\s+#.*)?$/.test(rest);
|
|
1172
1196
|
}
|
|
@@ -1180,7 +1204,7 @@ function extractFrontmatterValue(text, key) {
|
|
|
1180
1204
|
const rest = lines[idx].replace(keyRe, "").trim();
|
|
1181
1205
|
if (!rest) return "";
|
|
1182
1206
|
if (!isYamlBlockScalarHeader(rest)) {
|
|
1183
|
-
return rest.replace(/^["']|["']$/g, "").trim();
|
|
1207
|
+
return [rest, ...indentedContinuationLines(lines, idx)].join(" ").replace(/\s+/g, " ").replace(/^["']|["']$/g, "").trim();
|
|
1184
1208
|
}
|
|
1185
1209
|
const body = [];
|
|
1186
1210
|
for (let j = idx + 1; j < lines.length; j++) {
|
|
@@ -1192,11 +1216,20 @@ function extractFrontmatterValue(text, key) {
|
|
|
1192
1216
|
}
|
|
1193
1217
|
return body.join(" ").replace(/\s+/g, " ").trim();
|
|
1194
1218
|
}
|
|
1195
|
-
function
|
|
1196
|
-
|
|
1219
|
+
function frontmatterDescriptionRegion(text) {
|
|
1220
|
+
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1221
|
+
if (!m) return "";
|
|
1222
|
+
const lines = m[1].split(/\r?\n/);
|
|
1223
|
+
const idx = lines.findIndex((l) => /^description:[ \t]*/.test(l));
|
|
1224
|
+
if (idx < 0) return "";
|
|
1225
|
+
const region = [
|
|
1226
|
+
lines[idx].replace(/^description:[ \t]*/, ""),
|
|
1227
|
+
...indentedContinuationLines(lines, idx)
|
|
1228
|
+
];
|
|
1229
|
+
return region.join(" ").replace(/\s+/g, " ").trim();
|
|
1197
1230
|
}
|
|
1198
1231
|
function auditForbiddenTagsInText(text, label) {
|
|
1199
|
-
const body = `${
|
|
1232
|
+
const body = `${frontmatterDescriptionRegion(text)}
|
|
1200
1233
|
${extractUseWhenSection(text)}`;
|
|
1201
1234
|
if (!body.trim()) return [];
|
|
1202
1235
|
const failures = [];
|
|
@@ -1467,13 +1500,16 @@ async function syncRemoteCatalog() {
|
|
|
1467
1500
|
warn("Remote catalog fetch failed \u2014 using bundled catalog");
|
|
1468
1501
|
return { newItems: [], refreshed: [], unchanged: 0, failed: [] };
|
|
1469
1502
|
}
|
|
1470
|
-
const { version, items } = manifest;
|
|
1503
|
+
const { version, items, globalPlugins } = manifest;
|
|
1471
1504
|
const cacheDir = getCacheDir();
|
|
1472
1505
|
try {
|
|
1473
1506
|
await fs6.ensureDir(cacheDir);
|
|
1474
1507
|
await fs6.writeFile(
|
|
1475
1508
|
path8.join(cacheDir, "manifest.json"),
|
|
1476
|
-
|
|
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)}
|
|
1477
1513
|
`,
|
|
1478
1514
|
"utf8"
|
|
1479
1515
|
);
|
|
@@ -1527,14 +1563,20 @@ async function loadCatalogContext(root) {
|
|
|
1527
1563
|
return {
|
|
1528
1564
|
items: data2?.items ?? [],
|
|
1529
1565
|
contentRoot: path9.dirname(envPath),
|
|
1530
|
-
source: "fixture"
|
|
1566
|
+
source: "fixture",
|
|
1567
|
+
globalPlugins: data2?.globalPlugins
|
|
1531
1568
|
};
|
|
1532
1569
|
}
|
|
1533
1570
|
const cacheDir = getCacheDir();
|
|
1534
1571
|
const cacheManifestPath = path9.join(cacheDir, "manifest.json");
|
|
1535
1572
|
const cacheData = await readJson(cacheManifestPath);
|
|
1536
1573
|
if (cacheData?.items?.length) {
|
|
1537
|
-
return {
|
|
1574
|
+
return {
|
|
1575
|
+
items: cacheData.items,
|
|
1576
|
+
contentRoot: cacheDir,
|
|
1577
|
+
source: "cache",
|
|
1578
|
+
globalPlugins: cacheData.globalPlugins
|
|
1579
|
+
};
|
|
1538
1580
|
}
|
|
1539
1581
|
const localManifest = path9.join(root, "library/catalog/manifest.json");
|
|
1540
1582
|
const localData = await readJson(localManifest);
|
|
@@ -1542,7 +1584,8 @@ async function loadCatalogContext(root) {
|
|
|
1542
1584
|
return {
|
|
1543
1585
|
items: localData.items,
|
|
1544
1586
|
contentRoot: path9.dirname(localManifest),
|
|
1545
|
-
source: "local"
|
|
1587
|
+
source: "local",
|
|
1588
|
+
globalPlugins: localData.globalPlugins
|
|
1546
1589
|
};
|
|
1547
1590
|
}
|
|
1548
1591
|
const packageManifest = path9.join(packageRoot(), "library/catalog/manifest.json");
|
|
@@ -1550,7 +1593,8 @@ async function loadCatalogContext(root) {
|
|
|
1550
1593
|
return {
|
|
1551
1594
|
items: data?.items ?? [],
|
|
1552
1595
|
contentRoot: path9.dirname(packageManifest),
|
|
1553
|
-
source: "bundled"
|
|
1596
|
+
source: "bundled",
|
|
1597
|
+
globalPlugins: data?.globalPlugins
|
|
1554
1598
|
};
|
|
1555
1599
|
}
|
|
1556
1600
|
async function loadCatalog(root) {
|
|
@@ -1840,6 +1884,26 @@ function mergeRecommendationWarnings(context, isLinkedWorktree = false) {
|
|
|
1840
1884
|
return [.../* @__PURE__ */ new Set([...statusLines, ...context.warnings, ...riskLines])];
|
|
1841
1885
|
}
|
|
1842
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
|
+
|
|
1843
1907
|
// src/update/hash-installed.ts
|
|
1844
1908
|
import path11 from "node:path";
|
|
1845
1909
|
import fg2 from "fast-glob";
|
|
@@ -2008,13 +2072,22 @@ async function readManifest() {
|
|
|
2008
2072
|
async function writeManifest(manifest) {
|
|
2009
2073
|
await writeJson(hausManifestPath(), manifest);
|
|
2010
2074
|
}
|
|
2011
|
-
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) {
|
|
2012
2084
|
return {
|
|
2013
2085
|
_schema: MANIFEST_SCHEMA,
|
|
2014
2086
|
source,
|
|
2015
2087
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2016
2088
|
files,
|
|
2017
|
-
hooks
|
|
2089
|
+
hooks,
|
|
2090
|
+
...plugins && plugins.length > 0 ? { plugins } : {}
|
|
2018
2091
|
};
|
|
2019
2092
|
}
|
|
2020
2093
|
|
|
@@ -2270,8 +2343,8 @@ function buildDenyRules() {
|
|
|
2270
2343
|
for (const command of DENY_COMMANDS) {
|
|
2271
2344
|
rules.push(`Bash(${command}:*)`);
|
|
2272
2345
|
}
|
|
2273
|
-
for (const
|
|
2274
|
-
const pattern = DENY_DIRS.has(
|
|
2346
|
+
for (const path74 of DENY_PATHS) {
|
|
2347
|
+
const pattern = DENY_DIRS.has(path74) ? `${path74}/**` : path74;
|
|
2275
2348
|
for (const tool of FILE_TOOLS) {
|
|
2276
2349
|
rules.push(`${tool}(${pattern})`);
|
|
2277
2350
|
}
|
|
@@ -2354,6 +2427,7 @@ import path14 from "node:path";
|
|
|
2354
2427
|
import fg3 from "fast-glob";
|
|
2355
2428
|
import fs10 from "fs-extra";
|
|
2356
2429
|
var SUPERPOWERS_ORIGIN_SOURCE_ID = "superpowers-pcvelz";
|
|
2430
|
+
var SUPERPOWERS_SHARED_INSTALL_REL = ".claude/skills/shared";
|
|
2357
2431
|
var PATH_REWRITES = [
|
|
2358
2432
|
["skills/shared/", ".claude/skills/shared/"]
|
|
2359
2433
|
];
|
|
@@ -3229,6 +3303,10 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
3229
3303
|
warn(`Skipping ${item.id}: reviewStatus is deprecated`);
|
|
3230
3304
|
continue;
|
|
3231
3305
|
}
|
|
3306
|
+
if (isItemSupersededByPlugin(manifestItem, opts.activePluginIds ?? [])) {
|
|
3307
|
+
warn(`Skipping ${item.id}: provided by an installed global plugin`);
|
|
3308
|
+
continue;
|
|
3309
|
+
}
|
|
3232
3310
|
const manifestItemTags = (manifestItem.tags ?? []).map((t) => t.toLowerCase());
|
|
3233
3311
|
if (UNSUPPORTED.some((x) => manifestItemTags.includes(x))) {
|
|
3234
3312
|
warn(`Skipping ${item.id}: unsupported stack policy`);
|
|
@@ -3354,7 +3432,13 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
3354
3432
|
`${curatedReviewStatusSkips} curated items skipped: reviewStatus is not approved \u2014 possible catalog field rename upstream`
|
|
3355
3433
|
);
|
|
3356
3434
|
}
|
|
3357
|
-
await cleanupStaleCatalogItems(
|
|
3435
|
+
await cleanupStaleCatalogItems(
|
|
3436
|
+
root,
|
|
3437
|
+
cleanupManifestById,
|
|
3438
|
+
dryRun,
|
|
3439
|
+
opts.quiet,
|
|
3440
|
+
opts.activePluginIds ?? []
|
|
3441
|
+
);
|
|
3358
3442
|
const refuseDestructiveLockChange = recommendationUnusable && prevLock.length > 0 && !opts.force;
|
|
3359
3443
|
if (opts.prune) {
|
|
3360
3444
|
if (refuseDestructiveLockChange) {
|
|
@@ -3498,25 +3582,46 @@ async function resolveExistingTrackedPaths(root, itemId, relPaths, warnFn) {
|
|
|
3498
3582
|
}
|
|
3499
3583
|
return existing;
|
|
3500
3584
|
}
|
|
3501
|
-
async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet) {
|
|
3585
|
+
async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activePluginIds = []) {
|
|
3502
3586
|
const say = quiet ? () => {
|
|
3503
3587
|
} : log;
|
|
3504
3588
|
const prevLock = await readJson(hausPath(root, "haus.lock.json"));
|
|
3505
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
|
+
}
|
|
3506
3609
|
for (const entry of prevLock) {
|
|
3507
3610
|
if (!entry.id) continue;
|
|
3508
3611
|
const manifestItem = manifestById.get(entry.id);
|
|
3509
3612
|
const removedFromManifest = manifestItem === void 0;
|
|
3510
3613
|
const deprecated = manifestItem?.reviewStatus === "deprecated";
|
|
3511
|
-
|
|
3614
|
+
const superseded = isSupersededByPlugin(entry.id, activePluginIds);
|
|
3615
|
+
if (!removedFromManifest && !deprecated && !superseded) continue;
|
|
3512
3616
|
const relPaths = entry.paths ?? [];
|
|
3513
3617
|
if (relPaths.length === 0) continue;
|
|
3514
3618
|
const existing = await resolveExistingTrackedPaths(root, entry.id, relPaths, warn);
|
|
3515
3619
|
if (existing.length === 0) continue;
|
|
3516
|
-
const pruneReason = deprecated ? "deprecated" : "stale";
|
|
3620
|
+
const pruneReason = superseded ? "superseded" : deprecated ? "deprecated" : "stale";
|
|
3621
|
+
const label = superseded ? "Superseded" : deprecated ? "Deprecated" : "Stale";
|
|
3517
3622
|
if (entry.hash === void 0) {
|
|
3518
3623
|
warn(
|
|
3519
|
-
`${
|
|
3624
|
+
`${label} catalog item ${entry.id} has no lock hash \u2014 leaving in place: ${existing.map((e) => e.rel).join(", ")}`
|
|
3520
3625
|
);
|
|
3521
3626
|
continue;
|
|
3522
3627
|
}
|
|
@@ -3526,11 +3631,28 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet) {
|
|
|
3526
3631
|
);
|
|
3527
3632
|
if (currentHash !== entry.hash) {
|
|
3528
3633
|
warn(
|
|
3529
|
-
`${
|
|
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(", ")}`
|
|
3530
3635
|
);
|
|
3531
3636
|
continue;
|
|
3532
3637
|
}
|
|
3533
|
-
|
|
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) {
|
|
3534
3656
|
if (dryRun) {
|
|
3535
3657
|
say(`[dry-run] would remove ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
3536
3658
|
continue;
|
|
@@ -3769,16 +3891,431 @@ async function untrackMachineLocalArtifacts(root, dryRun, notify) {
|
|
|
3769
3891
|
return untracked;
|
|
3770
3892
|
}
|
|
3771
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
|
+
|
|
3772
4309
|
// src/refs/fetch-refs.ts
|
|
3773
|
-
import
|
|
4310
|
+
import path24 from "node:path";
|
|
3774
4311
|
import fs21 from "fs-extra";
|
|
3775
4312
|
|
|
3776
4313
|
// src/refs/cache-meta.ts
|
|
3777
|
-
import
|
|
4314
|
+
import path23 from "node:path";
|
|
3778
4315
|
import fs20 from "fs-extra";
|
|
3779
4316
|
var META_FILENAME = "cache-meta.json";
|
|
3780
4317
|
async function readCacheMeta(cacheDir) {
|
|
3781
|
-
const metaPath =
|
|
4318
|
+
const metaPath = path23.join(cacheDir, META_FILENAME);
|
|
3782
4319
|
try {
|
|
3783
4320
|
const raw = await fs20.readFile(metaPath, "utf8");
|
|
3784
4321
|
return JSON.parse(raw);
|
|
@@ -3788,7 +4325,7 @@ async function readCacheMeta(cacheDir) {
|
|
|
3788
4325
|
}
|
|
3789
4326
|
async function writeCacheMeta(cacheDir, meta) {
|
|
3790
4327
|
await fs20.ensureDir(cacheDir);
|
|
3791
|
-
const metaPath =
|
|
4328
|
+
const metaPath = path23.join(cacheDir, META_FILENAME);
|
|
3792
4329
|
await fs20.writeFile(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf8");
|
|
3793
4330
|
}
|
|
3794
4331
|
|
|
@@ -3809,8 +4346,8 @@ function isLlmsTxtUrl(ref) {
|
|
|
3809
4346
|
async function fetchSingleRef(url, cacheDir, meta) {
|
|
3810
4347
|
const existing = meta[url];
|
|
3811
4348
|
const headers = {};
|
|
3812
|
-
const safeFile = existing?.file && existing.file ===
|
|
3813
|
-
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));
|
|
3814
4351
|
if (cachedFileExists) {
|
|
3815
4352
|
if (existing?.etag) headers["If-None-Match"] = existing.etag;
|
|
3816
4353
|
if (existing?.lastModified) headers["If-Modified-Since"] = existing.lastModified;
|
|
@@ -3822,7 +4359,7 @@ async function fetchSingleRef(url, cacheDir, meta) {
|
|
|
3822
4359
|
const text = await res.text();
|
|
3823
4360
|
const file = `${urlToSlug(url)}.md`;
|
|
3824
4361
|
await fs21.ensureDir(cacheDir);
|
|
3825
|
-
await fs21.writeFile(
|
|
4362
|
+
await fs21.writeFile(path24.join(cacheDir, file), text, "utf8");
|
|
3826
4363
|
meta[url] = {
|
|
3827
4364
|
url,
|
|
3828
4365
|
etag: res.headers.get("etag") ?? void 0,
|
|
@@ -3845,8 +4382,8 @@ async function pruneOrphanedRefs(cacheDir, keepUrls) {
|
|
|
3845
4382
|
if (keepUrls.has(url)) continue;
|
|
3846
4383
|
delete meta[url];
|
|
3847
4384
|
removed++;
|
|
3848
|
-
const safeFile = typeof entry.file === "string" && entry.file ===
|
|
3849
|
-
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(() => {
|
|
3850
4387
|
});
|
|
3851
4388
|
}
|
|
3852
4389
|
if (removed > 0) await writeCacheMeta(cacheDir, meta);
|
|
@@ -3868,10 +4405,10 @@ async function fetchRefsForItems(items, cacheDir) {
|
|
|
3868
4405
|
const outcome = await fetchSingleRef(url, cacheDir, meta);
|
|
3869
4406
|
if (outcome.result === "fetched") {
|
|
3870
4407
|
summary.fetched++;
|
|
3871
|
-
summary.cachedFiles[url] =
|
|
4408
|
+
summary.cachedFiles[url] = path24.join(cacheDir, outcome.file);
|
|
3872
4409
|
} else if (outcome.result === "unchanged") {
|
|
3873
4410
|
summary.unchanged++;
|
|
3874
|
-
if (meta[url]?.file) summary.cachedFiles[url] =
|
|
4411
|
+
if (meta[url]?.file) summary.cachedFiles[url] = path24.join(cacheDir, meta[url].file);
|
|
3875
4412
|
} else {
|
|
3876
4413
|
summary.failed++;
|
|
3877
4414
|
summary.failedUrls.push(url);
|
|
@@ -3890,11 +4427,17 @@ function parseIdList(value) {
|
|
|
3890
4427
|
const raw = Array.isArray(value) ? value : [value];
|
|
3891
4428
|
return raw.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v.length > 0);
|
|
3892
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
|
+
}
|
|
3893
4436
|
|
|
3894
4437
|
// src/commands/apply.ts
|
|
3895
4438
|
var STALE_CACHE_THRESHOLD_DAYS = 7;
|
|
3896
4439
|
async function cacheHasItems() {
|
|
3897
|
-
const data = await readJson(
|
|
4440
|
+
const data = await readJson(path25.join(getCacheDir(), "manifest.json"));
|
|
3898
4441
|
return Array.isArray(data?.items) && data.items.length > 0;
|
|
3899
4442
|
}
|
|
3900
4443
|
function installableRecommendedItems(recommended) {
|
|
@@ -3998,7 +4541,9 @@ async function runApply(options) {
|
|
|
3998
4541
|
const files = await writeClaudeFiles(root, isDryRun, selectedIds, {
|
|
3999
4542
|
refillConfig: options.refillConfig,
|
|
4000
4543
|
force: options.force,
|
|
4001
|
-
prune: options.prune
|
|
4544
|
+
prune: options.prune,
|
|
4545
|
+
// Copies a global plugin already provides are removed here (issue #272).
|
|
4546
|
+
activePluginIds: await activeGlobalPluginIds(root)
|
|
4002
4547
|
});
|
|
4003
4548
|
await writeGitignore(root, isDryRun);
|
|
4004
4549
|
await untrackMachineLocalArtifacts(root, isDryRun, warn);
|
|
@@ -4045,7 +4590,7 @@ async function runApply(options) {
|
|
|
4045
4590
|
}
|
|
4046
4591
|
|
|
4047
4592
|
// src/commands/backups.ts
|
|
4048
|
-
import
|
|
4593
|
+
import path26 from "node:path";
|
|
4049
4594
|
import fs22 from "fs-extra";
|
|
4050
4595
|
|
|
4051
4596
|
// src/utils/prompts.ts
|
|
@@ -4082,7 +4627,7 @@ async function listBackups(root) {
|
|
|
4082
4627
|
for (const name of names) {
|
|
4083
4628
|
const kind = classify(name);
|
|
4084
4629
|
if (!kind) continue;
|
|
4085
|
-
const absPath =
|
|
4630
|
+
const absPath = path26.join(dir, name);
|
|
4086
4631
|
const stat = await fs22.lstat(absPath);
|
|
4087
4632
|
if (stat.isSymbolicLink()) continue;
|
|
4088
4633
|
const expectDirectory = kind !== "lock";
|
|
@@ -4112,10 +4657,10 @@ async function runList(root) {
|
|
|
4112
4657
|
}
|
|
4113
4658
|
}
|
|
4114
4659
|
async function isUnsafeWriteTarget(root, target) {
|
|
4115
|
-
const segments =
|
|
4660
|
+
const segments = path26.relative(root, target).split(path26.sep).filter(Boolean);
|
|
4116
4661
|
let current = root;
|
|
4117
4662
|
for (let i = 0; i < segments.length; i++) {
|
|
4118
|
-
current =
|
|
4663
|
+
current = path26.join(current, segments[i]);
|
|
4119
4664
|
const isLastSegment = i === segments.length - 1;
|
|
4120
4665
|
try {
|
|
4121
4666
|
const stat = await fs22.lstat(current);
|
|
@@ -4179,7 +4724,7 @@ async function collectFilesRecursive(dir) {
|
|
|
4179
4724
|
const skippedOther = [];
|
|
4180
4725
|
const names = await fs22.readdir(dir);
|
|
4181
4726
|
for (const name of names) {
|
|
4182
|
-
const abs =
|
|
4727
|
+
const abs = path26.join(dir, name);
|
|
4183
4728
|
const stat = await fs22.lstat(abs);
|
|
4184
4729
|
if (stat.isSymbolicLink()) {
|
|
4185
4730
|
skippedLinks.push(abs);
|
|
@@ -4202,12 +4747,12 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
4202
4747
|
const { files, skippedLinks, skippedOther } = await collectFilesRecursive(entry.absPath);
|
|
4203
4748
|
if (skippedLinks.length > 0) {
|
|
4204
4749
|
warn(
|
|
4205
|
-
`Skipped ${skippedLinks.length} symlink(s) inside backup ${entry.id} \u2014 never followed: ${skippedLinks.map((abs) =>
|
|
4750
|
+
`Skipped ${skippedLinks.length} symlink(s) inside backup ${entry.id} \u2014 never followed: ${skippedLinks.map((abs) => path26.relative(entry.absPath, abs)).join(", ")}`
|
|
4206
4751
|
);
|
|
4207
4752
|
}
|
|
4208
4753
|
if (skippedOther.length > 0) {
|
|
4209
4754
|
warn(
|
|
4210
|
-
`Skipped ${skippedOther.length} non-regular file(s) inside backup ${entry.id} (not restorable): ${skippedOther.map((abs) =>
|
|
4755
|
+
`Skipped ${skippedOther.length} non-regular file(s) inside backup ${entry.id} (not restorable): ${skippedOther.map((abs) => path26.relative(entry.absPath, abs)).join(", ")}`
|
|
4211
4756
|
);
|
|
4212
4757
|
}
|
|
4213
4758
|
if (files.length === 0) {
|
|
@@ -4217,8 +4762,8 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
4217
4762
|
const safeFiles = [];
|
|
4218
4763
|
const unsafeTargets = [];
|
|
4219
4764
|
for (const abs of files) {
|
|
4220
|
-
const rel =
|
|
4221
|
-
const target =
|
|
4765
|
+
const rel = path26.relative(entry.absPath, abs);
|
|
4766
|
+
const target = path26.join(root, rel);
|
|
4222
4767
|
if (await isUnsafeWriteTarget(root, target)) unsafeTargets.push(rel);
|
|
4223
4768
|
else safeFiles.push(abs);
|
|
4224
4769
|
}
|
|
@@ -4235,14 +4780,14 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
4235
4780
|
const vanished = [];
|
|
4236
4781
|
const toRestore = [];
|
|
4237
4782
|
for (const abs of safeFiles) {
|
|
4238
|
-
const rel =
|
|
4783
|
+
const rel = path26.relative(entry.absPath, abs);
|
|
4239
4784
|
const backupFileStat = await lstatIfRegularFile(abs);
|
|
4240
4785
|
if (!backupFileStat) {
|
|
4241
4786
|
vanished.push(rel);
|
|
4242
4787
|
continue;
|
|
4243
4788
|
}
|
|
4244
4789
|
toRestore.push(abs);
|
|
4245
|
-
const target =
|
|
4790
|
+
const target = path26.join(root, rel);
|
|
4246
4791
|
if (await fs22.pathExists(target)) {
|
|
4247
4792
|
const targetStat = await fs22.stat(target);
|
|
4248
4793
|
if (targetStat.mtimeMs > backupFileStat.mtimeMs) {
|
|
@@ -4283,13 +4828,13 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
4283
4828
|
let restoredCount = 0;
|
|
4284
4829
|
const skippedAtWrite = [];
|
|
4285
4830
|
for (const abs of toRestore) {
|
|
4286
|
-
const rel =
|
|
4287
|
-
const target =
|
|
4831
|
+
const rel = path26.relative(entry.absPath, abs);
|
|
4832
|
+
const target = path26.join(root, rel);
|
|
4288
4833
|
if (!await lstatIfRegularFile(abs) || await isUnsafeWriteTarget(root, target)) {
|
|
4289
4834
|
skippedAtWrite.push(rel);
|
|
4290
4835
|
continue;
|
|
4291
4836
|
}
|
|
4292
|
-
await fs22.ensureDir(
|
|
4837
|
+
await fs22.ensureDir(path26.dirname(target));
|
|
4293
4838
|
await fs22.copy(abs, target, { overwrite: true });
|
|
4294
4839
|
restoredCount++;
|
|
4295
4840
|
}
|
|
@@ -4428,18 +4973,19 @@ async function runCatalogAudit() {
|
|
|
4428
4973
|
}
|
|
4429
4974
|
|
|
4430
4975
|
// src/commands/doctor.ts
|
|
4431
|
-
import
|
|
4976
|
+
import path40 from "node:path";
|
|
4432
4977
|
import fs30 from "fs-extra";
|
|
4433
4978
|
|
|
4434
4979
|
// src/claude/skill-reference-check.ts
|
|
4435
|
-
import
|
|
4980
|
+
import path27 from "node:path";
|
|
4436
4981
|
import fg4 from "fast-glob";
|
|
4437
4982
|
import fs23 from "fs-extra";
|
|
4438
4983
|
var REFERENCE_PATTERN = /\b([a-z][a-z0-9]*(?:-[a-z0-9]+)+):([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\b(?!:\/\/)/g;
|
|
4439
4984
|
function stripFencedCodeBlocks(text) {
|
|
4440
4985
|
return text.replace(/```[\s\S]*?```/g, (block) => block.replace(/[^\n]/g, " "));
|
|
4441
4986
|
}
|
|
4442
|
-
async function findUnresolvedSkillReferences(root) {
|
|
4987
|
+
async function findUnresolvedSkillReferences(root, options = {}) {
|
|
4988
|
+
const resolvableNamespaces = new Set(options.pluginNamespaces ?? []);
|
|
4443
4989
|
const skillsRoot = claudePath(root, "skills");
|
|
4444
4990
|
if (!await fs23.pathExists(skillsRoot)) return [];
|
|
4445
4991
|
const skillDirs = (await fs23.readdir(skillsRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory() && entry.name !== "shared").map((entry) => entry.name);
|
|
@@ -4448,15 +4994,16 @@ async function findUnresolvedSkillReferences(root) {
|
|
|
4448
4994
|
const issues = [];
|
|
4449
4995
|
for (const rel of files) {
|
|
4450
4996
|
if (rel.startsWith("shared/")) continue;
|
|
4451
|
-
const abs =
|
|
4997
|
+
const abs = path27.join(skillsRoot, rel);
|
|
4452
4998
|
const raw = await fs23.readFile(abs, "utf8");
|
|
4453
4999
|
const lines = stripFencedCodeBlocks(raw).split("\n");
|
|
4454
5000
|
lines.forEach((line2, idx) => {
|
|
4455
5001
|
for (const match of line2.matchAll(REFERENCE_PATTERN)) {
|
|
4456
|
-
const [reference, , bareName] = match;
|
|
5002
|
+
const [reference, namespace, bareName] = match;
|
|
4457
5003
|
if (!reference || !bareName) continue;
|
|
5004
|
+
if (namespace && resolvableNamespaces.has(namespace)) continue;
|
|
4458
5005
|
issues.push({
|
|
4459
|
-
file:
|
|
5006
|
+
file: path27.join(".claude/skills", rel),
|
|
4460
5007
|
line: idx + 1,
|
|
4461
5008
|
reference,
|
|
4462
5009
|
bareName,
|
|
@@ -4469,11 +5016,11 @@ async function findUnresolvedSkillReferences(root) {
|
|
|
4469
5016
|
}
|
|
4470
5017
|
|
|
4471
5018
|
// src/decisions/doctor.ts
|
|
4472
|
-
import
|
|
5019
|
+
import path32 from "node:path";
|
|
4473
5020
|
import fs27 from "fs-extra";
|
|
4474
5021
|
|
|
4475
5022
|
// src/decisions/check.ts
|
|
4476
|
-
import
|
|
5023
|
+
import path31 from "node:path";
|
|
4477
5024
|
import fs26 from "fs-extra";
|
|
4478
5025
|
|
|
4479
5026
|
// src/decisions/dep-bump.ts
|
|
@@ -4533,7 +5080,7 @@ function isDependencyBumpOnly(beforeText, afterText) {
|
|
|
4533
5080
|
|
|
4534
5081
|
// src/decisions/diff.ts
|
|
4535
5082
|
init_exec();
|
|
4536
|
-
import
|
|
5083
|
+
import path28 from "node:path";
|
|
4537
5084
|
function isPackageJsonPath(file) {
|
|
4538
5085
|
return file === "package.json" || file.endsWith("/package.json");
|
|
4539
5086
|
}
|
|
@@ -4594,7 +5141,7 @@ async function resolvePackageJsonPair(root, file, opts) {
|
|
|
4594
5141
|
return { before: before2, after: after2 };
|
|
4595
5142
|
}
|
|
4596
5143
|
const before = await readBlob(root, `${range}:${file}`) ?? empty;
|
|
4597
|
-
const after = await readText(
|
|
5144
|
+
const after = await readText(path28.join(root, file)) ?? empty;
|
|
4598
5145
|
return { before, after };
|
|
4599
5146
|
}
|
|
4600
5147
|
async function collectDiffStats(root, opts) {
|
|
@@ -4665,21 +5212,21 @@ function firstMatchingRegex(filePath, patterns) {
|
|
|
4665
5212
|
}
|
|
4666
5213
|
|
|
4667
5214
|
// src/decisions/paths.ts
|
|
4668
|
-
import
|
|
5215
|
+
import path29 from "node:path";
|
|
4669
5216
|
import fs24 from "fs-extra";
|
|
4670
5217
|
async function resolveDecisionsDir(root) {
|
|
4671
|
-
const primary =
|
|
5218
|
+
const primary = path29.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
4672
5219
|
if (await fs24.pathExists(primary)) return primary;
|
|
4673
|
-
const legacy =
|
|
5220
|
+
const legacy = path29.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
4674
5221
|
if (await fs24.pathExists(legacy)) return legacy;
|
|
4675
5222
|
return primary;
|
|
4676
5223
|
}
|
|
4677
5224
|
function relativeDecisionPath(decisionsDir, root) {
|
|
4678
|
-
return
|
|
5225
|
+
return path29.relative(root, decisionsDir).replace(/\\/g, "/");
|
|
4679
5226
|
}
|
|
4680
5227
|
|
|
4681
5228
|
// src/decisions/validate.ts
|
|
4682
|
-
import
|
|
5229
|
+
import path30 from "node:path";
|
|
4683
5230
|
import fs25 from "fs-extra";
|
|
4684
5231
|
function validateDecisionContent(content2) {
|
|
4685
5232
|
const errors = [];
|
|
@@ -4709,14 +5256,14 @@ function decisionNumberFromFile(name) {
|
|
|
4709
5256
|
return match?.[1] ?? null;
|
|
4710
5257
|
}
|
|
4711
5258
|
async function validateIndexSync(decisionsDir, decisionFiles) {
|
|
4712
|
-
const readmePath =
|
|
5259
|
+
const readmePath = path30.join(decisionsDir, "README.md");
|
|
4713
5260
|
if (!await fs25.pathExists(readmePath)) {
|
|
4714
5261
|
return ["docs/decisions/README.md is missing"];
|
|
4715
5262
|
}
|
|
4716
5263
|
const readme = await readText(readmePath) ?? "";
|
|
4717
5264
|
const errors = [];
|
|
4718
5265
|
for (const file of decisionFiles) {
|
|
4719
|
-
const num = decisionNumberFromFile(
|
|
5266
|
+
const num = decisionNumberFromFile(path30.basename(file));
|
|
4720
5267
|
if (num && !indexReferencesNumber(readme, num)) {
|
|
4721
5268
|
errors.push(`README.md missing index row for ADR-${num}`);
|
|
4722
5269
|
}
|
|
@@ -4811,22 +5358,22 @@ async function checkSatisfaction(input2) {
|
|
|
4811
5358
|
const errors = [];
|
|
4812
5359
|
const decisionsPrefix = `${input2.decisionsRel}/`;
|
|
4813
5360
|
const newDecisionFiles = input2.changedFiles.filter(
|
|
4814
|
-
(f) => f.startsWith(decisionsPrefix) && isDecisionFileName(
|
|
5361
|
+
(f) => f.startsWith(decisionsPrefix) && isDecisionFileName(path31.basename(f))
|
|
4815
5362
|
);
|
|
4816
5363
|
if (newDecisionFiles.length === 0) {
|
|
4817
5364
|
errors.push(`no new or updated decision under ${input2.decisionsRel}/`);
|
|
4818
5365
|
return errors;
|
|
4819
5366
|
}
|
|
4820
|
-
const decisionsDir =
|
|
5367
|
+
const decisionsDir = path31.join(input2.root, input2.decisionsRel);
|
|
4821
5368
|
for (const rel of newDecisionFiles) {
|
|
4822
|
-
const abs =
|
|
5369
|
+
const abs = path31.join(input2.root, rel);
|
|
4823
5370
|
const content2 = await readText(abs) ?? "";
|
|
4824
5371
|
const valid = validateDecisionContent(content2);
|
|
4825
5372
|
if (!valid.ok) errors.push(...valid.errors.map((e) => `${rel}: ${e}`));
|
|
4826
5373
|
}
|
|
4827
5374
|
const indexErrors = await validateIndexSync(
|
|
4828
5375
|
decisionsDir,
|
|
4829
|
-
newDecisionFiles.map((f) =>
|
|
5376
|
+
newDecisionFiles.map((f) => path31.basename(f))
|
|
4830
5377
|
);
|
|
4831
5378
|
errors.push(...indexErrors);
|
|
4832
5379
|
return errors;
|
|
@@ -4882,7 +5429,7 @@ ${opts.commitMessages ?? ""}`;
|
|
|
4882
5429
|
changedFiles: stats.files
|
|
4883
5430
|
});
|
|
4884
5431
|
if (satisfactionErrors.length > 0 && decisionsRel !== legacyRel) {
|
|
4885
|
-
const legacyExists = await fs26.pathExists(
|
|
5432
|
+
const legacyExists = await fs26.pathExists(path31.join(root, legacyRel));
|
|
4886
5433
|
if (legacyExists && !await fs26.pathExists(decisionsDir)) {
|
|
4887
5434
|
satisfactionErrors = await checkSatisfaction({
|
|
4888
5435
|
root,
|
|
@@ -4913,8 +5460,8 @@ async function listDecisionNumbers(decisionsDir) {
|
|
|
4913
5460
|
// src/decisions/doctor.ts
|
|
4914
5461
|
async function auditDecisionsLayout(root) {
|
|
4915
5462
|
const findings = [];
|
|
4916
|
-
const primary =
|
|
4917
|
-
const legacy =
|
|
5463
|
+
const primary = path32.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
5464
|
+
const legacy = path32.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
4918
5465
|
const hasPrimary = await fs27.pathExists(primary);
|
|
4919
5466
|
const hasLegacy = await fs27.pathExists(legacy);
|
|
4920
5467
|
if (!hasPrimary && hasLegacy) {
|
|
@@ -4929,7 +5476,7 @@ async function auditDecisionsLayout(root) {
|
|
|
4929
5476
|
if (!hasPrimary) return findings;
|
|
4930
5477
|
const decisionsDir = await resolveDecisionsDir(root);
|
|
4931
5478
|
const rel = relativeDecisionPath(decisionsDir, root);
|
|
4932
|
-
const readmePath =
|
|
5479
|
+
const readmePath = path32.join(decisionsDir, "README.md");
|
|
4933
5480
|
if (!await fs27.pathExists(readmePath)) {
|
|
4934
5481
|
findings.push({
|
|
4935
5482
|
level: "advisory",
|
|
@@ -4954,7 +5501,7 @@ async function auditDecisionsLayout(root) {
|
|
|
4954
5501
|
});
|
|
4955
5502
|
}
|
|
4956
5503
|
}
|
|
4957
|
-
const claude = await readText(
|
|
5504
|
+
const claude = await readText(path32.join(root, "CLAUDE.md"));
|
|
4958
5505
|
if (claude && !claude.includes(`@${rel}/README.md`)) {
|
|
4959
5506
|
findings.push({
|
|
4960
5507
|
level: "advisory",
|
|
@@ -4985,11 +5532,11 @@ async function auditDecisionsLayout(root) {
|
|
|
4985
5532
|
}
|
|
4986
5533
|
|
|
4987
5534
|
// src/scanner/read-context.ts
|
|
4988
|
-
import
|
|
5535
|
+
import path39 from "node:path";
|
|
4989
5536
|
import fs29 from "fs-extra";
|
|
4990
5537
|
|
|
4991
5538
|
// src/commands/workspace/config.ts
|
|
4992
|
-
import
|
|
5539
|
+
import path33 from "node:path";
|
|
4993
5540
|
import YAML from "yaml";
|
|
4994
5541
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
4995
5542
|
function parseWorkspaceConfig(text) {
|
|
@@ -5012,11 +5559,11 @@ function parseWorkspaceConfig(text) {
|
|
|
5012
5559
|
};
|
|
5013
5560
|
}
|
|
5014
5561
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
5015
|
-
return parseWorkspaceConfig(await readText(
|
|
5562
|
+
return parseWorkspaceConfig(await readText(path33.join(workspaceRoot, WORKSPACE_FILE)));
|
|
5016
5563
|
}
|
|
5017
5564
|
|
|
5018
5565
|
// src/workspace/members.ts
|
|
5019
|
-
import
|
|
5566
|
+
import path34 from "node:path";
|
|
5020
5567
|
var REPOS_MANIFEST_FILE = "repos.manifest.json";
|
|
5021
5568
|
var REPOS_LOCAL_FILE = "repos.local.json";
|
|
5022
5569
|
var MemberConfigError = class extends Error {
|
|
@@ -5029,14 +5576,14 @@ var MemberConfigError = class extends Error {
|
|
|
5029
5576
|
};
|
|
5030
5577
|
function resolveAbsPath(workspaceRoot, folder, overrides) {
|
|
5031
5578
|
const override = overrides[folder];
|
|
5032
|
-
if (override) return
|
|
5033
|
-
return
|
|
5579
|
+
if (override) return path34.resolve(workspaceRoot, override);
|
|
5580
|
+
return path34.resolve(workspaceRoot, folder);
|
|
5034
5581
|
}
|
|
5035
5582
|
function isSelfMember(workspaceRoot, member) {
|
|
5036
|
-
return
|
|
5583
|
+
return path34.resolve(member.absPath) === path34.resolve(workspaceRoot);
|
|
5037
5584
|
}
|
|
5038
5585
|
async function readPathOverrides(workspaceRoot) {
|
|
5039
|
-
const filePath =
|
|
5586
|
+
const filePath = path34.join(workspaceRoot, REPOS_LOCAL_FILE);
|
|
5040
5587
|
const result = await readJsonDetailed(filePath);
|
|
5041
5588
|
if (result.status === "missing") return {};
|
|
5042
5589
|
if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
|
|
@@ -5052,7 +5599,7 @@ async function readPathOverrides(workspaceRoot) {
|
|
|
5052
5599
|
return overrides;
|
|
5053
5600
|
}
|
|
5054
5601
|
async function readFromWorkspaceYaml(workspaceRoot, overrides) {
|
|
5055
|
-
const filePath =
|
|
5602
|
+
const filePath = path34.join(workspaceRoot, WORKSPACE_FILE);
|
|
5056
5603
|
const text = await readText(filePath);
|
|
5057
5604
|
if (text === void 0) return void 0;
|
|
5058
5605
|
const config = parseWorkspaceConfig(text);
|
|
@@ -5065,7 +5612,7 @@ async function readFromWorkspaceYaml(workspaceRoot, overrides) {
|
|
|
5065
5612
|
}));
|
|
5066
5613
|
}
|
|
5067
5614
|
async function readFromManifest(workspaceRoot, overrides) {
|
|
5068
|
-
const filePath =
|
|
5615
|
+
const filePath = path34.join(workspaceRoot, REPOS_MANIFEST_FILE);
|
|
5069
5616
|
const result = await readJsonDetailed(filePath);
|
|
5070
5617
|
if (result.status === "missing") return void 0;
|
|
5071
5618
|
if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
|
|
@@ -5107,11 +5654,11 @@ async function readMembers(rootInfo) {
|
|
|
5107
5654
|
}
|
|
5108
5655
|
|
|
5109
5656
|
// src/scanner/scan-project.ts
|
|
5110
|
-
import
|
|
5657
|
+
import path38 from "node:path";
|
|
5111
5658
|
|
|
5112
5659
|
// src/utils/git-root.ts
|
|
5113
5660
|
init_exec();
|
|
5114
|
-
import
|
|
5661
|
+
import path35 from "node:path";
|
|
5115
5662
|
function fallback(start) {
|
|
5116
5663
|
return {
|
|
5117
5664
|
cwd: start,
|
|
@@ -5127,11 +5674,11 @@ function fallback(start) {
|
|
|
5127
5674
|
async function resolveAbsoluteGitPath(flag, cwd, repoRoot) {
|
|
5128
5675
|
const withFormat = await runGit(["rev-parse", "--path-format=absolute", flag], { cwd });
|
|
5129
5676
|
const raw = withFormat.exitCode === 0 ? withFormat.stdout.trim() : "";
|
|
5130
|
-
if (raw &&
|
|
5677
|
+
if (raw && path35.isAbsolute(raw)) return path35.resolve(raw);
|
|
5131
5678
|
const plain = await runGit(["rev-parse", flag], { cwd });
|
|
5132
5679
|
const plainRaw = plain.exitCode === 0 ? plain.stdout.trim() : "";
|
|
5133
5680
|
if (!plainRaw) return "";
|
|
5134
|
-
return
|
|
5681
|
+
return path35.isAbsolute(plainRaw) ? path35.resolve(plainRaw) : path35.resolve(repoRoot, plainRaw);
|
|
5135
5682
|
}
|
|
5136
5683
|
async function resolveRoots(start) {
|
|
5137
5684
|
const cwd = start ?? process.cwd();
|
|
@@ -5140,15 +5687,15 @@ async function resolveRoots(start) {
|
|
|
5140
5687
|
if (toplevel.exitCode !== 0) return fallback(cwd);
|
|
5141
5688
|
const toplevelRaw = toplevel.stdout.trim();
|
|
5142
5689
|
if (!toplevelRaw) return fallback(cwd);
|
|
5143
|
-
const repoRoot =
|
|
5690
|
+
const repoRoot = path35.resolve(toplevelRaw);
|
|
5144
5691
|
const [gitDir, gitCommonDir] = await Promise.all([
|
|
5145
5692
|
resolveAbsoluteGitPath("--git-dir", cwd, repoRoot),
|
|
5146
5693
|
resolveAbsoluteGitPath("--git-common-dir", cwd, repoRoot)
|
|
5147
5694
|
]);
|
|
5148
5695
|
if (!gitDir || !gitCommonDir) return fallback(cwd);
|
|
5149
5696
|
const isLinkedWorktree = gitDir !== gitCommonDir;
|
|
5150
|
-
const mainRoot = isLinkedWorktree &&
|
|
5151
|
-
const worktreeName = isLinkedWorktree ?
|
|
5697
|
+
const mainRoot = isLinkedWorktree && path35.basename(gitCommonDir) === ".git" ? path35.dirname(gitCommonDir) : repoRoot;
|
|
5698
|
+
const worktreeName = isLinkedWorktree ? path35.basename(gitDir) : null;
|
|
5152
5699
|
return {
|
|
5153
5700
|
cwd,
|
|
5154
5701
|
repoRoot,
|
|
@@ -5193,7 +5740,7 @@ function resolveMajorVersion(range) {
|
|
|
5193
5740
|
}
|
|
5194
5741
|
|
|
5195
5742
|
// src/scanner/detect-package-manager.ts
|
|
5196
|
-
import
|
|
5743
|
+
import path36 from "node:path";
|
|
5197
5744
|
import fs28 from "fs-extra";
|
|
5198
5745
|
function detectPackageManager(root, packageManagerField) {
|
|
5199
5746
|
const field = String(packageManagerField ?? "").trim();
|
|
@@ -5212,9 +5759,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
5212
5759
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
5213
5760
|
return "unknown";
|
|
5214
5761
|
}
|
|
5215
|
-
if (fs28.existsSync(
|
|
5216
|
-
if (fs28.existsSync(
|
|
5217
|
-
if (fs28.existsSync(
|
|
5762
|
+
if (fs28.existsSync(path36.join(root, "yarn.lock"))) return "yarn";
|
|
5763
|
+
if (fs28.existsSync(path36.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
5764
|
+
if (fs28.existsSync(path36.join(root, "package-lock.json"))) return "npm";
|
|
5218
5765
|
return "unknown";
|
|
5219
5766
|
}
|
|
5220
5767
|
|
|
@@ -5439,7 +5986,7 @@ function runDetection(ctx, rules = STACK_RULES) {
|
|
|
5439
5986
|
|
|
5440
5987
|
// src/scanner/render.ts
|
|
5441
5988
|
import { readFile } from "node:fs/promises";
|
|
5442
|
-
import
|
|
5989
|
+
import path37 from "node:path";
|
|
5443
5990
|
async function buildContentBlob(root, files) {
|
|
5444
5991
|
const candidates = files.filter(
|
|
5445
5992
|
(f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
|
|
@@ -5447,7 +5994,7 @@ async function buildContentBlob(root, files) {
|
|
|
5447
5994
|
const slice = candidates.slice(0, 300);
|
|
5448
5995
|
const parts = await mapWithConcurrency(slice, async (rel) => {
|
|
5449
5996
|
try {
|
|
5450
|
-
return await readFile(
|
|
5997
|
+
return await readFile(path37.join(root, rel), "utf8");
|
|
5451
5998
|
} catch {
|
|
5452
5999
|
return "";
|
|
5453
6000
|
}
|
|
@@ -5539,8 +6086,8 @@ var SAFE_FILES = [
|
|
|
5539
6086
|
"Gemfile"
|
|
5540
6087
|
];
|
|
5541
6088
|
async function scanProject(root) {
|
|
5542
|
-
const pkg = await readJson(
|
|
5543
|
-
const composer = await readJson(
|
|
6089
|
+
const pkg = await readJson(path38.join(root, "package.json"));
|
|
6090
|
+
const composer = await readJson(path38.join(root, "composer.json"));
|
|
5544
6091
|
const files = await listFiles(root, SAFE_FILES);
|
|
5545
6092
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
5546
6093
|
const depVersions = dependencyVersionMap(pkg, composer);
|
|
@@ -5570,7 +6117,7 @@ async function scanProject(root) {
|
|
|
5570
6117
|
crossRepoHints.push("Monorepo orchestration detected");
|
|
5571
6118
|
const unsupportedSignals = collectUnsupportedSignals(safeFiles);
|
|
5572
6119
|
const detectionStatus = computeDetectionStatus(roles, stacks, unsupportedSignals);
|
|
5573
|
-
const repoNameFallback = pkg?.name ? String(pkg.name) :
|
|
6120
|
+
const repoNameFallback = pkg?.name ? String(pkg.name) : path38.basename((await resolveRoots(root)).mainRoot);
|
|
5574
6121
|
const context = {
|
|
5575
6122
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5576
6123
|
repoName: repoNameFallback,
|
|
@@ -5592,10 +6139,10 @@ async function scanProject(root) {
|
|
|
5592
6139
|
// src/scanner/read-context.ts
|
|
5593
6140
|
async function isCacheFresh(root, cachePath) {
|
|
5594
6141
|
const markerPaths = [
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
6142
|
+
path39.join(root, "package.json"),
|
|
6143
|
+
path39.join(root, "composer.json"),
|
|
6144
|
+
path39.join(root, WORKSPACE_FILE),
|
|
6145
|
+
path39.join(root, REPOS_MANIFEST_FILE)
|
|
5599
6146
|
];
|
|
5600
6147
|
const cacheStat = await fs29.stat(cachePath);
|
|
5601
6148
|
for (const markerPath of markerPaths) {
|
|
@@ -5701,7 +6248,7 @@ async function runDoctor(options) {
|
|
|
5701
6248
|
} else {
|
|
5702
6249
|
ok(`- HOOKS OK: ${hooks.message}`);
|
|
5703
6250
|
}
|
|
5704
|
-
const rootClaudeMdPath =
|
|
6251
|
+
const rootClaudeMdPath = path40.join(root, "CLAUDE.md");
|
|
5705
6252
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
5706
6253
|
if (!rootClaudeMdContent) {
|
|
5707
6254
|
flag(
|
|
@@ -5744,9 +6291,9 @@ async function runDoctor(options) {
|
|
|
5744
6291
|
ok("- CLAUDE.md: import block present");
|
|
5745
6292
|
}
|
|
5746
6293
|
for (const target of actualTargets) {
|
|
5747
|
-
const resolved =
|
|
5748
|
-
const relFromRoot =
|
|
5749
|
-
const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${
|
|
6294
|
+
const resolved = path40.resolve(root, target);
|
|
6295
|
+
const relFromRoot = path40.relative(root, resolved);
|
|
6296
|
+
const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${path40.sep}`) || path40.isAbsolute(relFromRoot);
|
|
5750
6297
|
if (escapesRoot || !await fs30.pathExists(resolved)) {
|
|
5751
6298
|
flag(
|
|
5752
6299
|
`- CLAUDE.md import: @${target} does not resolve (run \`haus apply --write\`)`,
|
|
@@ -5780,8 +6327,8 @@ async function runDoctor(options) {
|
|
|
5780
6327
|
} else {
|
|
5781
6328
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
5782
6329
|
const bodyContent = workflowContent.slice(firstLine.length + 1);
|
|
5783
|
-
const cachePath =
|
|
5784
|
-
const bundledPath =
|
|
6330
|
+
const cachePath = path40.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
6331
|
+
const bundledPath = path40.join(
|
|
5785
6332
|
packageRoot(),
|
|
5786
6333
|
"library",
|
|
5787
6334
|
"global",
|
|
@@ -5830,7 +6377,7 @@ async function runDoctor(options) {
|
|
|
5830
6377
|
}
|
|
5831
6378
|
}
|
|
5832
6379
|
if (workflowExists) {
|
|
5833
|
-
const prettierIgnore = await readText(
|
|
6380
|
+
const prettierIgnore = await readText(path40.join(root, ".prettierignore")) ?? "";
|
|
5834
6381
|
const lines = prettierIgnore.split("\n");
|
|
5835
6382
|
const missing = IGNORED_PATHS.filter((p) => !prettierIgnoreProtects(lines, p));
|
|
5836
6383
|
if (missing.length > 0) {
|
|
@@ -5870,7 +6417,50 @@ async function runDoctor(options) {
|
|
|
5870
6417
|
if (finding.level === "ok") ok(finding.line);
|
|
5871
6418
|
else suggest(finding.line, finding.sentence, finding.fix);
|
|
5872
6419
|
}
|
|
5873
|
-
const
|
|
6420
|
+
const pluginPins = await loadCatalogPluginPins(root);
|
|
6421
|
+
const pluginState = await probePluginEnv();
|
|
6422
|
+
const activePins = pluginPins.filter((pin) => isPluginActive(pluginState, pin));
|
|
6423
|
+
const activeNamespaces = activePins.map((pin) => pin.plugin);
|
|
6424
|
+
if (pluginPins.length === 0) {
|
|
6425
|
+
} else if (!pluginState.available) {
|
|
6426
|
+
suggest(
|
|
6427
|
+
"- GLOBAL PLUGINS: not checked (the `claude` CLI is not on PATH)",
|
|
6428
|
+
"Claude Code's CLI isn't on PATH, so haus can't verify the global plugin(s) it manages",
|
|
6429
|
+
"install Claude Code, then run `haus install`"
|
|
6430
|
+
);
|
|
6431
|
+
} else {
|
|
6432
|
+
const pending = planPluginActions(pluginPins, pluginState).filter(
|
|
6433
|
+
(action) => action.kind !== "up-to-date"
|
|
6434
|
+
);
|
|
6435
|
+
if (pending.length > 0) {
|
|
6436
|
+
suggest(
|
|
6437
|
+
`- GLOBAL PLUGINS: ${pending.length} plugin(s) not at the pinned state (${pending.map((a) => `${a.pin.id}: ${a.reason}`).join("; ")})`,
|
|
6438
|
+
`${pending.length} haus-managed global plugin(s) are missing, disabled, or off the pinned ref`,
|
|
6439
|
+
"haus update (or `haus install` outside a project)"
|
|
6440
|
+
);
|
|
6441
|
+
} else {
|
|
6442
|
+
ok(`- GLOBAL PLUGINS: OK (${pluginPins.map((p) => `${p.id}@${p.ref}`).join(", ")})`);
|
|
6443
|
+
}
|
|
6444
|
+
}
|
|
6445
|
+
if (activePins.length > 0) {
|
|
6446
|
+
const lockEntries = await readJson(hausPath(root, "haus.lock.json"));
|
|
6447
|
+
const superseded = (lockEntries ?? []).map((entry) => entry.id).filter(
|
|
6448
|
+
(id) => typeof id === "string" && isSupersededByPlugin(
|
|
6449
|
+
id,
|
|
6450
|
+
activePins.map((pin) => pin.id)
|
|
6451
|
+
)
|
|
6452
|
+
);
|
|
6453
|
+
if (superseded.length > 0) {
|
|
6454
|
+
suggest(
|
|
6455
|
+
`- GLOBAL PLUGINS: ${superseded.length} installed item(s) are superseded by the global plugin (${superseded.slice(0, 3).join(", ")}${superseded.length > 3 ? ", \u2026" : ""})`,
|
|
6456
|
+
`${superseded.length} project skill(s) duplicate what the global plugin already provides`,
|
|
6457
|
+
"haus update (removes the superseded copies)"
|
|
6458
|
+
);
|
|
6459
|
+
}
|
|
6460
|
+
}
|
|
6461
|
+
const skillRefIssues = await findUnresolvedSkillReferences(root, {
|
|
6462
|
+
pluginNamespaces: activeNamespaces
|
|
6463
|
+
});
|
|
5874
6464
|
if (skillRefIssues.length > 0) {
|
|
5875
6465
|
const prefixBug = skillRefIssues.filter((i) => i.bareInstalled);
|
|
5876
6466
|
const missingDep = skillRefIssues.filter((i) => !i.bareInstalled);
|
|
@@ -5934,7 +6524,7 @@ async function runDoctor(options) {
|
|
|
5934
6524
|
}
|
|
5935
6525
|
}
|
|
5936
6526
|
}
|
|
5937
|
-
const pkgJson = await readJson(
|
|
6527
|
+
const pkgJson = await readJson(path40.join(packageRoot(), "package.json"));
|
|
5938
6528
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
5939
6529
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
5940
6530
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -5967,7 +6557,7 @@ async function runDoctor(options) {
|
|
|
5967
6557
|
}
|
|
5968
6558
|
|
|
5969
6559
|
// src/commands/update.ts
|
|
5970
|
-
import
|
|
6560
|
+
import path43 from "node:path";
|
|
5971
6561
|
|
|
5972
6562
|
// src/claude/refresh-project.ts
|
|
5973
6563
|
import fs31 from "fs-extra";
|
|
@@ -5979,12 +6569,16 @@ async function isHausProject(root) {
|
|
|
5979
6569
|
}
|
|
5980
6570
|
async function refreshProjectApply(root) {
|
|
5981
6571
|
if (!await isHausProject(root)) return [];
|
|
5982
|
-
return writeClaudeFiles(root, false, void 0, {
|
|
6572
|
+
return writeClaudeFiles(root, false, void 0, {
|
|
6573
|
+
refillConfig: false,
|
|
6574
|
+
// Superseded copies are removed in the same pass that re-applies the catalog.
|
|
6575
|
+
activePluginIds: await activeGlobalPluginIds(root)
|
|
6576
|
+
});
|
|
5983
6577
|
}
|
|
5984
6578
|
|
|
5985
6579
|
// src/install/apply.ts
|
|
5986
6580
|
import crypto2 from "node:crypto";
|
|
5987
|
-
import
|
|
6581
|
+
import path41 from "node:path";
|
|
5988
6582
|
import fs32 from "fs-extra";
|
|
5989
6583
|
|
|
5990
6584
|
// src/install/header.ts
|
|
@@ -6078,7 +6672,7 @@ function destFileMissing(destPath) {
|
|
|
6078
6672
|
}
|
|
6079
6673
|
function sourceVersion() {
|
|
6080
6674
|
try {
|
|
6081
|
-
const pkgPath =
|
|
6675
|
+
const pkgPath = path41.join(packageRoot(), "package.json");
|
|
6082
6676
|
const pkg = JSON.parse(fs32.readFileSync(pkgPath, "utf8"));
|
|
6083
6677
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
6084
6678
|
} catch {
|
|
@@ -6086,49 +6680,49 @@ function sourceVersion() {
|
|
|
6086
6680
|
}
|
|
6087
6681
|
}
|
|
6088
6682
|
function globalSrcDir() {
|
|
6089
|
-
return
|
|
6683
|
+
return path41.join(packageRoot(), "library", "global");
|
|
6090
6684
|
}
|
|
6091
6685
|
function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
6092
|
-
const skillDir =
|
|
6686
|
+
const skillDir = path41.join(skillsDir, skillName);
|
|
6093
6687
|
const entries = [];
|
|
6094
6688
|
for (const rel of fs32.readdirSync(skillDir, { recursive: true })) {
|
|
6095
6689
|
if (rel === "SKILL.md") continue;
|
|
6096
|
-
const abs =
|
|
6690
|
+
const abs = path41.join(skillDir, rel);
|
|
6097
6691
|
if (!fs32.statSync(abs).isFile()) continue;
|
|
6098
|
-
const posixRel = rel.split(
|
|
6692
|
+
const posixRel = rel.split(path41.sep).join("/");
|
|
6099
6693
|
entries.push({
|
|
6100
6694
|
stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
|
|
6101
|
-
srcRelPath:
|
|
6102
|
-
destPath:
|
|
6695
|
+
srcRelPath: path41.join("library", "global", "skills", skillName, rel),
|
|
6696
|
+
destPath: path41.join(claudeDir, "skills", skillName, rel)
|
|
6103
6697
|
});
|
|
6104
6698
|
}
|
|
6105
6699
|
return entries;
|
|
6106
6700
|
}
|
|
6107
6701
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
6108
6702
|
const entries = [];
|
|
6109
|
-
const skillsDir =
|
|
6703
|
+
const skillsDir = path41.join(srcDir, "skills");
|
|
6110
6704
|
if (fs32.pathExistsSync(skillsDir)) {
|
|
6111
6705
|
for (const skillName of fs32.readdirSync(skillsDir)) {
|
|
6112
|
-
const skillFile =
|
|
6706
|
+
const skillFile = path41.join(skillsDir, skillName, "SKILL.md");
|
|
6113
6707
|
if (fs32.pathExistsSync(skillFile)) {
|
|
6114
6708
|
entries.push({
|
|
6115
6709
|
stableId: `skill.${skillName}`,
|
|
6116
|
-
srcRelPath:
|
|
6117
|
-
destPath:
|
|
6710
|
+
srcRelPath: path41.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
6711
|
+
destPath: path41.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
6118
6712
|
});
|
|
6119
6713
|
entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
|
|
6120
6714
|
}
|
|
6121
6715
|
}
|
|
6122
6716
|
}
|
|
6123
|
-
const commandsDir =
|
|
6717
|
+
const commandsDir = path41.join(srcDir, "commands");
|
|
6124
6718
|
if (fs32.pathExistsSync(commandsDir)) {
|
|
6125
6719
|
for (const fileName of fs32.readdirSync(commandsDir)) {
|
|
6126
6720
|
if (!fileName.endsWith(".md")) continue;
|
|
6127
6721
|
const commandName = fileName.slice(0, -".md".length);
|
|
6128
6722
|
entries.push({
|
|
6129
6723
|
stableId: `command.${commandName}`,
|
|
6130
|
-
srcRelPath:
|
|
6131
|
-
destPath:
|
|
6724
|
+
srcRelPath: path41.join("library", "global", "commands", fileName),
|
|
6725
|
+
destPath: path41.join(claudeDir, "commands", fileName)
|
|
6132
6726
|
});
|
|
6133
6727
|
}
|
|
6134
6728
|
}
|
|
@@ -6154,7 +6748,7 @@ async function applyInstall(options = {}) {
|
|
|
6154
6748
|
};
|
|
6155
6749
|
const manifestFiles = [];
|
|
6156
6750
|
for (const entry of sourceFiles) {
|
|
6157
|
-
const srcPath =
|
|
6751
|
+
const srcPath = path41.join(packageRoot(), entry.srcRelPath);
|
|
6158
6752
|
const rawContent = await readText(srcPath);
|
|
6159
6753
|
if (rawContent === void 0) {
|
|
6160
6754
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -6215,7 +6809,7 @@ async function applyInstall(options = {}) {
|
|
|
6215
6809
|
}
|
|
6216
6810
|
let mergedSettings;
|
|
6217
6811
|
if (!check) {
|
|
6218
|
-
const fragmentPath =
|
|
6812
|
+
const fragmentPath = path41.join(srcDir, "settings-fragments", "hooks.json");
|
|
6219
6813
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
6220
6814
|
const settings = await readSettings();
|
|
6221
6815
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -6245,9 +6839,12 @@ async function applyInstall(options = {}) {
|
|
|
6245
6839
|
}
|
|
6246
6840
|
if (!dryRun && !check && mergedSettings) {
|
|
6247
6841
|
await writeSettings(mergedSettings);
|
|
6248
|
-
const manifest = buildManifest(
|
|
6249
|
-
|
|
6250
|
-
|
|
6842
|
+
const manifest = buildManifest(
|
|
6843
|
+
source,
|
|
6844
|
+
manifestFiles,
|
|
6845
|
+
[.../* @__PURE__ */ new Set([...existingManifest?.hooks ?? [], ...result.hookIds])],
|
|
6846
|
+
existingManifest?.plugins
|
|
6847
|
+
);
|
|
6251
6848
|
await writeManifest(manifest);
|
|
6252
6849
|
}
|
|
6253
6850
|
return result;
|
|
@@ -6304,7 +6901,7 @@ function summarizeLockDiff(before, after) {
|
|
|
6304
6901
|
|
|
6305
6902
|
// src/update/lockfile.ts
|
|
6306
6903
|
import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
|
|
6307
|
-
import
|
|
6904
|
+
import path42 from "node:path";
|
|
6308
6905
|
async function readLockSummary(root) {
|
|
6309
6906
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
6310
6907
|
const catalogRef = lock.find((item) => item.catalogRef)?.catalogRef ?? null;
|
|
@@ -6342,7 +6939,7 @@ async function applyLock(root) {
|
|
|
6342
6939
|
try {
|
|
6343
6940
|
const backupDir = hausPath(root, "backups");
|
|
6344
6941
|
await mkdir(backupDir, { recursive: true });
|
|
6345
|
-
await copyFile(lockPath,
|
|
6942
|
+
await copyFile(lockPath, path42.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
6346
6943
|
} catch {
|
|
6347
6944
|
}
|
|
6348
6945
|
const enriched = await Promise.all(
|
|
@@ -6364,7 +6961,7 @@ function diffLock(before, after) {
|
|
|
6364
6961
|
}
|
|
6365
6962
|
async function hasLocalOverrides(root) {
|
|
6366
6963
|
try {
|
|
6367
|
-
await readFile2(
|
|
6964
|
+
await readFile2(path42.join(root, ".claude", "settings.json"), "utf8");
|
|
6368
6965
|
return true;
|
|
6369
6966
|
} catch {
|
|
6370
6967
|
return false;
|
|
@@ -6380,15 +6977,26 @@ async function runUpdate(options) {
|
|
|
6380
6977
|
return;
|
|
6381
6978
|
}
|
|
6382
6979
|
if (options.check) {
|
|
6383
|
-
const pkgJson2 = await readJson(
|
|
6980
|
+
const pkgJson2 = await readJson(path43.join(packageRoot(), "package.json"));
|
|
6384
6981
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
6385
|
-
const [
|
|
6982
|
+
const [
|
|
6983
|
+
status,
|
|
6984
|
+
npmVersion,
|
|
6985
|
+
latestCatalogTag,
|
|
6986
|
+
globalInstallDrift,
|
|
6987
|
+
catalogItems,
|
|
6988
|
+
lockItems,
|
|
6989
|
+
globalPluginDrift
|
|
6990
|
+
] = await Promise.all([
|
|
6386
6991
|
options.fast ? fastLockStatus(root) : checkLock(root),
|
|
6387
6992
|
fetchNpmVersionStatus(currentVersion2),
|
|
6388
6993
|
fetchLatestCatalogTag(),
|
|
6389
6994
|
options.fast ? Promise.resolve(null) : detectGlobalInstallDrift(),
|
|
6390
6995
|
loadCatalog(root),
|
|
6391
|
-
readJson(hausPath(root, "haus.lock.json"))
|
|
6996
|
+
readJson(hausPath(root, "haus.lock.json")),
|
|
6997
|
+
// Same tier split as globalInstallDrift: --fast is the cheap probe the
|
|
6998
|
+
// SessionStart hook uses and must not shell out to the `claude` CLI.
|
|
6999
|
+
options.fast ? Promise.resolve(null) : detectGlobalPluginDrift(root)
|
|
6392
7000
|
]);
|
|
6393
7001
|
const formerIdMigrations = findFormerIdMigrations(lockItems ?? [], catalogItems);
|
|
6394
7002
|
const installedRef = options.fast ? status.catalogRef : status.catalogRef ?? "main";
|
|
@@ -6403,6 +7011,7 @@ async function runUpdate(options) {
|
|
|
6403
7011
|
catalogRefBehind,
|
|
6404
7012
|
formerIdMigrations,
|
|
6405
7013
|
globalInstallDrift,
|
|
7014
|
+
globalPluginDrift,
|
|
6406
7015
|
localOverrides: await hasLocalOverrides(root),
|
|
6407
7016
|
summary: diffGeneratedFiles(),
|
|
6408
7017
|
npmVersion
|
|
@@ -6412,12 +7021,12 @@ async function runUpdate(options) {
|
|
|
6412
7021
|
)
|
|
6413
7022
|
);
|
|
6414
7023
|
const hasHashDrift = !options.fast && status.count > 0 && "ok" in status && !status.ok;
|
|
6415
|
-
if (hasHashDrift || formerIdMigrations.length > 0 || globalInstallDrift === true) {
|
|
7024
|
+
if (hasHashDrift || formerIdMigrations.length > 0 || globalInstallDrift === true || globalPluginDrift === true) {
|
|
6416
7025
|
process.exitCode = 1;
|
|
6417
7026
|
}
|
|
6418
7027
|
return;
|
|
6419
7028
|
}
|
|
6420
|
-
const pkgJson = await readJson(
|
|
7029
|
+
const pkgJson = await readJson(path43.join(packageRoot(), "package.json"));
|
|
6421
7030
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
6422
7031
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
6423
7032
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -6450,6 +7059,7 @@ async function runUpdate(options) {
|
|
|
6450
7059
|
warn(formatGithubRateLimitMessage(sync.rateLimit));
|
|
6451
7060
|
}
|
|
6452
7061
|
await refreshGlobalInstall();
|
|
7062
|
+
await ensureCatalogPinnedPlugins(root);
|
|
6453
7063
|
await refreshProjectFiles(root);
|
|
6454
7064
|
const { after } = await applyLock(root);
|
|
6455
7065
|
log(diffLock(lockBefore, after));
|
|
@@ -6460,7 +7070,7 @@ async function runFromHookCheck(root) {
|
|
|
6460
7070
|
try {
|
|
6461
7071
|
const summary = await readLockSummary(root);
|
|
6462
7072
|
if (summary.count === 0) return;
|
|
6463
|
-
const pkgJson = await readJson(
|
|
7073
|
+
const pkgJson = await readJson(path43.join(packageRoot(), "package.json"));
|
|
6464
7074
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
6465
7075
|
const [npmVersion, latestCatalogTag] = await Promise.all([
|
|
6466
7076
|
fetchNpmVersionStatus(currentVersion),
|
|
@@ -6518,6 +7128,26 @@ async function refreshGlobalInstall() {
|
|
|
6518
7128
|
warn(`Could not refresh ~/.claude/: ${err instanceof Error ? err.message : String(err)}`);
|
|
6519
7129
|
}
|
|
6520
7130
|
}
|
|
7131
|
+
async function ensureCatalogPinnedPlugins(root) {
|
|
7132
|
+
try {
|
|
7133
|
+
const pins = await loadCatalogPluginPins(root);
|
|
7134
|
+
const ensured = await ensureGlobalPlugins(pins);
|
|
7135
|
+
await recordManifestPlugins(
|
|
7136
|
+
pins.filter((pin) => ensured.ensured.includes(pin.id)).map((pin) => ({ ...pin, installedAt: (/* @__PURE__ */ new Date()).toISOString() }))
|
|
7137
|
+
);
|
|
7138
|
+
} catch (err) {
|
|
7139
|
+
warn(`Could not update global plugins: ${err instanceof Error ? err.message : String(err)}`);
|
|
7140
|
+
}
|
|
7141
|
+
}
|
|
7142
|
+
async function detectGlobalPluginDrift(root) {
|
|
7143
|
+
try {
|
|
7144
|
+
const pins = await loadCatalogPluginPins(root);
|
|
7145
|
+
const state = await probePluginEnv();
|
|
7146
|
+
return pluginDriftStatus(planPluginActions(pins, state), { matchRef: true });
|
|
7147
|
+
} catch {
|
|
7148
|
+
return null;
|
|
7149
|
+
}
|
|
7150
|
+
}
|
|
6521
7151
|
async function detectGlobalInstallDrift() {
|
|
6522
7152
|
try {
|
|
6523
7153
|
const result = await applyInstall({ check: true });
|
|
@@ -6611,7 +7241,7 @@ async function runCiGate(options = {}) {
|
|
|
6611
7241
|
init_exec();
|
|
6612
7242
|
import { existsSync as existsSync2 } from "node:fs";
|
|
6613
7243
|
import { readdir } from "node:fs/promises";
|
|
6614
|
-
import
|
|
7244
|
+
import path44 from "node:path";
|
|
6615
7245
|
var GIT_LOCATION_VARS = [
|
|
6616
7246
|
"GIT_DIR",
|
|
6617
7247
|
"GIT_WORK_TREE",
|
|
@@ -6671,11 +7301,11 @@ async function runClone(url, opts = {}) {
|
|
|
6671
7301
|
process.exitCode = 1;
|
|
6672
7302
|
return;
|
|
6673
7303
|
}
|
|
6674
|
-
const target =
|
|
7304
|
+
const target = path44.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
6675
7305
|
if (existsSync2(target) && !await isEmptyDirectory(target)) {
|
|
6676
7306
|
const check = await checkExistingTarget(target, url);
|
|
6677
7307
|
if (check.kind === "matches") {
|
|
6678
|
-
log(`\u2022 ${
|
|
7308
|
+
log(`\u2022 ${path44.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
|
|
6679
7309
|
return;
|
|
6680
7310
|
}
|
|
6681
7311
|
const message = check.kind === "not-a-repo" ? `${target} already exists and is not a git repository \u2014 refusing to clone ${url} there. Remove or rename it, or pass a different [dir].` : `${target} already exists as a different repository (origin: ${check.origin ?? "(no origin remote)"}) \u2014 refusing to clone ${url} there. Remove or rename it, or pass a different [dir].`;
|
|
@@ -6784,7 +7414,7 @@ async function runDecisionsGuard(options = {}) {
|
|
|
6784
7414
|
}
|
|
6785
7415
|
|
|
6786
7416
|
// src/commands/decisions.ts
|
|
6787
|
-
import
|
|
7417
|
+
import path45 from "node:path";
|
|
6788
7418
|
|
|
6789
7419
|
// src/decisions/next-number.ts
|
|
6790
7420
|
async function nextDecisionNumber(root) {
|
|
@@ -6894,7 +7524,7 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
6894
7524
|
return;
|
|
6895
7525
|
}
|
|
6896
7526
|
if (sub === "validate") {
|
|
6897
|
-
const target = arg ?
|
|
7527
|
+
const target = arg ? path45.resolve(root, arg) : void 0;
|
|
6898
7528
|
if (!target) {
|
|
6899
7529
|
error("usage: haus decisions validate <path-to-decision.md>");
|
|
6900
7530
|
process.exitCode = 1;
|
|
@@ -7163,17 +7793,34 @@ async function runGuard(kind, _options) {
|
|
|
7163
7793
|
}
|
|
7164
7794
|
|
|
7165
7795
|
// src/commands/init.ts
|
|
7166
|
-
import
|
|
7796
|
+
import path47 from "node:path";
|
|
7167
7797
|
import fs33 from "fs-extra";
|
|
7168
7798
|
|
|
7169
7799
|
// src/recommender/git-signal.ts
|
|
7170
7800
|
init_exec();
|
|
7171
7801
|
async function runGitLines(root, args) {
|
|
7802
|
+
const started = Date.now();
|
|
7172
7803
|
try {
|
|
7173
7804
|
const result = await runGit(args, { cwd: root, timeout: 3e3 });
|
|
7805
|
+
if (process.env.HAUS_DEBUG_GITSIG) {
|
|
7806
|
+
const { appendFileSync } = await import("node:fs");
|
|
7807
|
+
appendFileSync(
|
|
7808
|
+
process.env.HAUS_DEBUG_GITSIG,
|
|
7809
|
+
`GITSIG ok git ${args.join(" ")} cwd=${root} ms=${Date.now() - started} exit=${result.exitCode} stdout=${JSON.stringify(result.stdout.slice(0, 200))} stderr=${JSON.stringify(result.stderr.slice(0, 200))}
|
|
7810
|
+
`
|
|
7811
|
+
);
|
|
7812
|
+
}
|
|
7174
7813
|
if (result.exitCode !== 0) return [];
|
|
7175
7814
|
return result.stdout.split("\n").map((x) => x.trim()).filter(Boolean);
|
|
7176
|
-
} catch {
|
|
7815
|
+
} catch (err) {
|
|
7816
|
+
if (process.env.HAUS_DEBUG_GITSIG) {
|
|
7817
|
+
const { appendFileSync } = await import("node:fs");
|
|
7818
|
+
appendFileSync(
|
|
7819
|
+
process.env.HAUS_DEBUG_GITSIG,
|
|
7820
|
+
`GITSIG threw git ${args.join(" ")} cwd=${root} ms=${Date.now() - started} err=${JSON.stringify(String(err).slice(0, 300))}
|
|
7821
|
+
`
|
|
7822
|
+
);
|
|
7823
|
+
}
|
|
7177
7824
|
return [];
|
|
7178
7825
|
}
|
|
7179
7826
|
}
|
|
@@ -7189,6 +7836,7 @@ async function readChangedFiles(root) {
|
|
|
7189
7836
|
|
|
7190
7837
|
// src/recommender/recommend.ts
|
|
7191
7838
|
async function recommend(root, context, opts = {}) {
|
|
7839
|
+
const activePluginIds = opts.activePluginIds ?? [];
|
|
7192
7840
|
const items = await loadCatalog(root);
|
|
7193
7841
|
const sources = buildSourcesReport(items);
|
|
7194
7842
|
const deep = await readJson(hausPath(root, "deep-context.json")) ?? {};
|
|
@@ -7246,6 +7894,15 @@ async function recommend(root, context, opts = {}) {
|
|
|
7246
7894
|
"Catalog item is deprecated",
|
|
7247
7895
|
"reviewStatus:deprecated"
|
|
7248
7896
|
);
|
|
7897
|
+
if (isItemSupersededByPlugin(item, activePluginIds)) {
|
|
7898
|
+
add(
|
|
7899
|
+
"superseded-by-global-plugin",
|
|
7900
|
+
false,
|
|
7901
|
+
"superseded-by-global-plugin",
|
|
7902
|
+
"Provided by an installed global Claude Code plugin",
|
|
7903
|
+
`plugin:${activePluginIds.join(",")}`
|
|
7904
|
+
);
|
|
7905
|
+
}
|
|
7249
7906
|
if (normSource === "curated") {
|
|
7250
7907
|
const rs = item.reviewStatus;
|
|
7251
7908
|
add(
|
|
@@ -7585,7 +8242,7 @@ function buildOptInEligible(items, skipped) {
|
|
|
7585
8242
|
}
|
|
7586
8243
|
|
|
7587
8244
|
// src/commands/workspace/discover.ts
|
|
7588
|
-
import
|
|
8245
|
+
import path46 from "node:path";
|
|
7589
8246
|
import fg5 from "fast-glob";
|
|
7590
8247
|
import YAML2 from "yaml";
|
|
7591
8248
|
var DEFAULT_MAX_DEPTH = 3;
|
|
@@ -7626,10 +8283,10 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
7626
8283
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
7627
8284
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
7628
8285
|
for (const match of gitMatches) {
|
|
7629
|
-
gitDirs.add(
|
|
8286
|
+
gitDirs.add(path46.posix.dirname(match));
|
|
7630
8287
|
}
|
|
7631
8288
|
for (const match of manifestMatches) {
|
|
7632
|
-
manifestDirs.add(
|
|
8289
|
+
manifestDirs.add(path46.posix.dirname(match));
|
|
7633
8290
|
}
|
|
7634
8291
|
const repoRoots = [...gitDirs];
|
|
7635
8292
|
const manifestSorted = [...manifestDirs].sort(
|
|
@@ -7646,9 +8303,9 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
7646
8303
|
async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
7647
8304
|
const repoRoots = (await findRepoRoots(workspaceRoot, maxDepth)).filter((dir) => dir !== ".");
|
|
7648
8305
|
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
7649
|
-
const absDir =
|
|
7650
|
-
const pkg = await readJson(
|
|
7651
|
-
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name :
|
|
8306
|
+
const absDir = path46.resolve(workspaceRoot, relDir);
|
|
8307
|
+
const pkg = await readJson(path46.join(absDir, "package.json"));
|
|
8308
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path46.basename(absDir);
|
|
7652
8309
|
let role = "auto";
|
|
7653
8310
|
try {
|
|
7654
8311
|
const scan = await scanProject(absDir);
|
|
@@ -7691,7 +8348,7 @@ function renderWorkspaceYaml(config) {
|
|
|
7691
8348
|
});
|
|
7692
8349
|
}
|
|
7693
8350
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
7694
|
-
const yamlPath =
|
|
8351
|
+
const yamlPath = path46.join(workspaceRoot, "haus.workspace.yaml");
|
|
7695
8352
|
const existingText = await readText(yamlPath);
|
|
7696
8353
|
const existing = parseWorkspaceConfig(existingText);
|
|
7697
8354
|
if (existingText && !existing) {
|
|
@@ -7758,7 +8415,11 @@ async function runSetupCore(root, opts) {
|
|
|
7758
8415
|
const siblingWarning = await hasMultipleSiblingRepos(root) ? [SIBLING_REPO_WARNING] : [];
|
|
7759
8416
|
const context = await readContextOrScan(root);
|
|
7760
8417
|
const priorManualIncludes = await readPriorManualIncludes(root);
|
|
7761
|
-
const
|
|
8418
|
+
const activePluginIds = await activeGlobalPluginIds(root);
|
|
8419
|
+
const recommendation = await recommend(root, context, {
|
|
8420
|
+
include: priorManualIncludes,
|
|
8421
|
+
activePluginIds
|
|
8422
|
+
});
|
|
7762
8423
|
if (recommendation.recommended.length === 0 && !force) {
|
|
7763
8424
|
const warningLines2 = [
|
|
7764
8425
|
.../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? [], ...siblingWarning])
|
|
@@ -7772,7 +8433,10 @@ async function runSetupCore(root, opts) {
|
|
|
7772
8433
|
say(`Repo: ${context.repoName}`);
|
|
7773
8434
|
for (const warning of warningLines2) say(`- WARN: ${warning}`);
|
|
7774
8435
|
}
|
|
7775
|
-
const
|
|
8436
|
+
const supersededCount = recommendation.skipped.filter(
|
|
8437
|
+
(s) => s.skipReasons?.some((r) => r.code === "superseded-by-global-plugin")
|
|
8438
|
+
).length;
|
|
8439
|
+
const message = supersededCount > 0 ? `No catalog items matched this project after ${supersededCount} item(s) were gated as already provided by an installed global plugin \u2014 refusing to write recommendation.json/haus.lock.json. Those skills are available globally; re-run with \`haus setup-project --force\` to write anyway.` : "No catalog items matched this project at all (zero signal) \u2014 refusing to write recommendation.json/haus.lock.json. Re-run with `haus setup-project --force` to write anyway.";
|
|
7776
8440
|
warn(message);
|
|
7777
8441
|
process.exitCode = 1;
|
|
7778
8442
|
return {
|
|
@@ -7838,7 +8502,10 @@ async function runSetupCore(root, opts) {
|
|
|
7838
8502
|
warn(formatGithubRateLimitMessage(sync.rateLimit));
|
|
7839
8503
|
}
|
|
7840
8504
|
}
|
|
7841
|
-
const files = await writeClaudeFiles(root, dryRun ?? false, void 0, {
|
|
8505
|
+
const files = await writeClaudeFiles(root, dryRun ?? false, void 0, {
|
|
8506
|
+
quiet: opts.quiet,
|
|
8507
|
+
activePluginIds
|
|
8508
|
+
});
|
|
7842
8509
|
say("Applied files:");
|
|
7843
8510
|
files.forEach((f) => say(`- ${displayPath(root, f)}`));
|
|
7844
8511
|
const hooksAfter = await verifyProjectSettingsHooksContract(root);
|
|
@@ -7869,7 +8536,7 @@ async function runSetupProject(options) {
|
|
|
7869
8536
|
// src/commands/init.ts
|
|
7870
8537
|
async function runInit(options) {
|
|
7871
8538
|
const root = process.cwd();
|
|
7872
|
-
const hausDir =
|
|
8539
|
+
const hausDir = path47.join(root, ".haus-workflow");
|
|
7873
8540
|
const alreadyInit = await fs33.pathExists(hausDir);
|
|
7874
8541
|
if (alreadyInit) {
|
|
7875
8542
|
log("Haus AI already initialized in this project.");
|
|
@@ -7934,7 +8601,19 @@ function printCompanionToolSuggestions(deps = {}) {
|
|
|
7934
8601
|
}
|
|
7935
8602
|
|
|
7936
8603
|
// src/commands/install.ts
|
|
7937
|
-
|
|
8604
|
+
function toManifestPlugin(pin) {
|
|
8605
|
+
return {
|
|
8606
|
+
id: pin.id,
|
|
8607
|
+
plugin: pin.plugin,
|
|
8608
|
+
marketplace: pin.marketplace,
|
|
8609
|
+
url: pin.url,
|
|
8610
|
+
ref: pin.ref,
|
|
8611
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8612
|
+
};
|
|
8613
|
+
}
|
|
8614
|
+
async function runInstall(options, deps = {}) {
|
|
8615
|
+
const ensure = deps.ensureGlobalPlugins ?? ensureGlobalPlugins;
|
|
8616
|
+
const probe = deps.probePluginEnv ?? probePluginEnv;
|
|
7938
8617
|
try {
|
|
7939
8618
|
const result = await applyInstall({
|
|
7940
8619
|
dryRun: options.dryRun,
|
|
@@ -7942,9 +8621,23 @@ async function runInstall(options) {
|
|
|
7942
8621
|
check: options.check
|
|
7943
8622
|
});
|
|
7944
8623
|
if (!options.postinstall) printApplyResult(result, options.dryRun ?? false);
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
|
|
8624
|
+
const pins = [...DEFAULT_GLOBAL_PLUGINS];
|
|
8625
|
+
if (options.check) {
|
|
8626
|
+
const state = await probe();
|
|
8627
|
+
const pluginDrift = pluginDriftStatus(planPluginActions(pins, state), { matchRef: false });
|
|
8628
|
+
if (pluginDrift === true) {
|
|
8629
|
+
log("Global plugin drift: a haus-managed plugin is missing or disabled.");
|
|
8630
|
+
}
|
|
8631
|
+
if (result.drift || pluginDrift === true) process.exitCode = 1;
|
|
8632
|
+
return;
|
|
8633
|
+
}
|
|
8634
|
+
const ensured = await ensure(pins, { dryRun: options.dryRun });
|
|
8635
|
+
if (!options.dryRun) {
|
|
8636
|
+
await recordManifestPlugins(
|
|
8637
|
+
pins.filter((pin) => ensured.ensured.includes(pin.id)).map(toManifestPlugin)
|
|
8638
|
+
);
|
|
8639
|
+
}
|
|
8640
|
+
if (!options.dryRun) {
|
|
7948
8641
|
printCompanionToolSuggestions();
|
|
7949
8642
|
const total = result.created.length + result.updated.length;
|
|
7950
8643
|
if (options.postinstall) {
|
|
@@ -7956,6 +8649,9 @@ async function runInstall(options) {
|
|
|
7956
8649
|
parts.length ? ` \u2022 ${parts.join(", ")} in ~/.claude (skills, slash commands)` : " \u2022 already up to date \u2014 no files changed"
|
|
7957
8650
|
);
|
|
7958
8651
|
log(` \u2022 ensured hooks + security rules are present in ~/.claude/settings.json`);
|
|
8652
|
+
if (ensured.ensured.length) {
|
|
8653
|
+
log(` \u2022 global plugin(s) pinned: ${ensured.ensured.join(", ")}`);
|
|
8654
|
+
}
|
|
7959
8655
|
log("Undo any time with: haus uninstall");
|
|
7960
8656
|
log("Disable this on install: HAUS_NO_POSTINSTALL=1");
|
|
7961
8657
|
} else {
|
|
@@ -7975,7 +8671,8 @@ async function runRecommend(options) {
|
|
|
7975
8671
|
const root = process.cwd();
|
|
7976
8672
|
const context = await readContextOrScan(root);
|
|
7977
8673
|
const include = parseIdList(options.include);
|
|
7978
|
-
const
|
|
8674
|
+
const activePluginIds = await activeGlobalPluginIds(root);
|
|
8675
|
+
const result = await recommend(root, context, { include, activePluginIds });
|
|
7979
8676
|
await writeJson(hausPath(root, "recommendation.json"), result);
|
|
7980
8677
|
if (options.json) {
|
|
7981
8678
|
log(JSON.stringify(result, null, 2));
|
|
@@ -7996,7 +8693,14 @@ async function runRecommend(options) {
|
|
|
7996
8693
|
async function runRefresh() {
|
|
7997
8694
|
const root = process.cwd();
|
|
7998
8695
|
const context = await scanProject(root);
|
|
7999
|
-
|
|
8696
|
+
try {
|
|
8697
|
+
await ensureGlobalPlugins(await loadCatalogPluginPins(root));
|
|
8698
|
+
} catch (err) {
|
|
8699
|
+
warn(`Could not refresh global plugins: ${err instanceof Error ? err.message : String(err)}`);
|
|
8700
|
+
}
|
|
8701
|
+
const recommendation = await recommend(root, context, {
|
|
8702
|
+
activePluginIds: await activeGlobalPluginIds(root)
|
|
8703
|
+
});
|
|
8000
8704
|
await writeJson(hausPath(root, "recommendation.json"), recommendation);
|
|
8001
8705
|
log("Haus refresh complete");
|
|
8002
8706
|
log(`Roles: ${context.repoRoles.join(", ") || "unknown"}`);
|
|
@@ -8005,7 +8709,7 @@ async function runRefresh() {
|
|
|
8005
8709
|
}
|
|
8006
8710
|
|
|
8007
8711
|
// src/install/scaffold.ts
|
|
8008
|
-
import
|
|
8712
|
+
import path48 from "node:path";
|
|
8009
8713
|
import fs34 from "fs-extra";
|
|
8010
8714
|
async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
8011
8715
|
const scaffolded = [];
|
|
@@ -8022,9 +8726,9 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8022
8726
|
const realRoot = await fs34.realpath(catalogRoot).catch(() => null);
|
|
8023
8727
|
for (const item of items) {
|
|
8024
8728
|
if (item.type !== "config") continue;
|
|
8025
|
-
const sourcePath =
|
|
8026
|
-
const rel =
|
|
8027
|
-
const escapes = rel === "" ||
|
|
8729
|
+
const sourcePath = path48.resolve(catalogRoot, item.path);
|
|
8730
|
+
const rel = path48.relative(catalogRoot, sourcePath);
|
|
8731
|
+
const escapes = rel === "" || path48.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
|
|
8028
8732
|
if (escapes) {
|
|
8029
8733
|
warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
|
|
8030
8734
|
continue;
|
|
@@ -8034,8 +8738,8 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8034
8738
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
8035
8739
|
continue;
|
|
8036
8740
|
}
|
|
8037
|
-
const realRel =
|
|
8038
|
-
if (realRel !== "" && (
|
|
8741
|
+
const realRel = path48.relative(realRoot, realSource);
|
|
8742
|
+
if (realRel !== "" && (path48.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
|
|
8039
8743
|
warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
|
|
8040
8744
|
continue;
|
|
8041
8745
|
}
|
|
@@ -8049,14 +8753,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8049
8753
|
continue;
|
|
8050
8754
|
}
|
|
8051
8755
|
if (stat.isFile()) {
|
|
8052
|
-
const filename =
|
|
8053
|
-
const dest =
|
|
8756
|
+
const filename = path48.basename(sourcePath);
|
|
8757
|
+
const dest = path48.join(projectRoot, filename);
|
|
8054
8758
|
record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
|
|
8055
8759
|
} else if (stat.isDirectory()) {
|
|
8056
8760
|
const entries = await fs34.readdir(sourcePath);
|
|
8057
8761
|
for (const entry of entries) {
|
|
8058
|
-
const src =
|
|
8059
|
-
const dest =
|
|
8762
|
+
const src = path48.join(sourcePath, entry);
|
|
8763
|
+
const dest = path48.join(projectRoot, entry);
|
|
8060
8764
|
record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
|
|
8061
8765
|
}
|
|
8062
8766
|
}
|
|
@@ -8064,14 +8768,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8064
8768
|
return { scaffolded, skipped, skippedExisting };
|
|
8065
8769
|
}
|
|
8066
8770
|
async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
8067
|
-
const destDisplay =
|
|
8771
|
+
const destDisplay = path48.relative(projectRoot, dest) || path48.basename(dest);
|
|
8068
8772
|
const srcStat = await fs34.lstat(src).catch(() => null);
|
|
8069
8773
|
if (!srcStat) {
|
|
8070
|
-
warn(`Skipping ${
|
|
8774
|
+
warn(`Skipping ${path48.basename(src)}: source not found`);
|
|
8071
8775
|
return "skipped-refused";
|
|
8072
8776
|
}
|
|
8073
8777
|
if (srcStat.isSymbolicLink()) {
|
|
8074
|
-
warn(`Skipping ${
|
|
8778
|
+
warn(`Skipping ${path48.basename(src)}: source is a symlink`);
|
|
8075
8779
|
return "skipped-refused";
|
|
8076
8780
|
}
|
|
8077
8781
|
const exists = await fs34.pathExists(dest);
|
|
@@ -8080,13 +8784,13 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
|
8080
8784
|
return "skipped-exists";
|
|
8081
8785
|
}
|
|
8082
8786
|
if (opts.dryRun) {
|
|
8083
|
-
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${
|
|
8787
|
+
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path48.basename(dest)} (${itemId})`);
|
|
8084
8788
|
return "scaffolded";
|
|
8085
8789
|
}
|
|
8086
8790
|
if (exists && opts.force) {
|
|
8087
8791
|
await fs34.remove(dest);
|
|
8088
8792
|
}
|
|
8089
|
-
await fs34.ensureDir(
|
|
8793
|
+
await fs34.ensureDir(path48.dirname(dest));
|
|
8090
8794
|
await fs34.copy(src, dest, {
|
|
8091
8795
|
overwrite: true,
|
|
8092
8796
|
dereference: false,
|
|
@@ -8099,7 +8803,7 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
|
8099
8803
|
}
|
|
8100
8804
|
}
|
|
8101
8805
|
});
|
|
8102
|
-
log(`${exists ? "Overwrote" : "Created"} ${
|
|
8806
|
+
log(`${exists ? "Overwrote" : "Created"} ${path48.basename(dest)} (${itemId})`);
|
|
8103
8807
|
return "scaffolded";
|
|
8104
8808
|
}
|
|
8105
8809
|
|
|
@@ -8164,7 +8868,7 @@ async function runScan(options) {
|
|
|
8164
8868
|
}
|
|
8165
8869
|
|
|
8166
8870
|
// src/commands/undo.ts
|
|
8167
|
-
import
|
|
8871
|
+
import path49 from "node:path";
|
|
8168
8872
|
import fs35 from "fs-extra";
|
|
8169
8873
|
|
|
8170
8874
|
// src/claude/managed-paths.ts
|
|
@@ -8192,18 +8896,18 @@ async function collectLockTrackedPaths(root) {
|
|
|
8192
8896
|
const relPaths = row.paths ?? [];
|
|
8193
8897
|
const existingRel = [];
|
|
8194
8898
|
for (const rel of relPaths) {
|
|
8195
|
-
if (await fs35.pathExists(
|
|
8899
|
+
if (await fs35.pathExists(path49.resolve(root, rel))) existingRel.push(rel);
|
|
8196
8900
|
}
|
|
8197
8901
|
if (existingRel.length === 0) continue;
|
|
8198
8902
|
if (row.hash === void 0) {
|
|
8199
|
-
removable.push(...existingRel.map((rel) =>
|
|
8903
|
+
removable.push(...existingRel.map((rel) => path49.resolve(root, rel)));
|
|
8200
8904
|
continue;
|
|
8201
8905
|
}
|
|
8202
8906
|
const currentHash = await hashInstalledPaths(root, existingRel);
|
|
8203
8907
|
if (currentHash === row.hash) {
|
|
8204
|
-
removable.push(...existingRel.map((rel) =>
|
|
8908
|
+
removable.push(...existingRel.map((rel) => path49.resolve(root, rel)));
|
|
8205
8909
|
} else {
|
|
8206
|
-
preserved.push(...existingRel.map((rel) =>
|
|
8910
|
+
preserved.push(...existingRel.map((rel) => path49.resolve(root, rel)));
|
|
8207
8911
|
log(`Preserving locally-modified ${row.id ?? "(unknown item)"}: ${existingRel.join(", ")}`);
|
|
8208
8912
|
}
|
|
8209
8913
|
}
|
|
@@ -8216,7 +8920,7 @@ async function settingsHasHausContent(root) {
|
|
|
8216
8920
|
return settings._haus != null;
|
|
8217
8921
|
}
|
|
8218
8922
|
async function claudeMdHasHausBlock(root) {
|
|
8219
|
-
const filePath =
|
|
8923
|
+
const filePath = path49.join(root, "CLAUDE.md");
|
|
8220
8924
|
if (!await fs35.pathExists(filePath)) return false;
|
|
8221
8925
|
const text = await fs35.readFile(filePath, "utf8");
|
|
8222
8926
|
return text.includes(BLOCK_BEGIN);
|
|
@@ -8229,15 +8933,15 @@ async function stripProjectSettings(root) {
|
|
|
8229
8933
|
const hasContent = Object.keys(settings).length > 0;
|
|
8230
8934
|
if (hasContent) {
|
|
8231
8935
|
await writeProjectSettings(root, settings);
|
|
8232
|
-
log(`Stripped haus rules from ${
|
|
8936
|
+
log(`Stripped haus rules from ${path49.relative(root, settingsPath)} (user settings preserved).`);
|
|
8233
8937
|
return true;
|
|
8234
8938
|
}
|
|
8235
8939
|
await fs35.remove(settingsPath);
|
|
8236
|
-
log(`Removed ${
|
|
8940
|
+
log(`Removed ${path49.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
8237
8941
|
return true;
|
|
8238
8942
|
}
|
|
8239
8943
|
async function stripRootClaudeMd(root) {
|
|
8240
|
-
const filePath =
|
|
8944
|
+
const filePath = path49.join(root, "CLAUDE.md");
|
|
8241
8945
|
if (!await fs35.pathExists(filePath)) return false;
|
|
8242
8946
|
const prev = await fs35.readFile(filePath, "utf8");
|
|
8243
8947
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
@@ -8265,12 +8969,12 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
|
8265
8969
|
const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
|
|
8266
8970
|
for (const abs of managedAbsPaths) {
|
|
8267
8971
|
if (!await fs35.pathExists(abs)) continue;
|
|
8268
|
-
const rel =
|
|
8269
|
-
const backupPath =
|
|
8270
|
-
await fs35.ensureDir(
|
|
8972
|
+
const rel = path49.relative(root, abs);
|
|
8973
|
+
const backupPath = path49.join(backupRoot, rel);
|
|
8974
|
+
await fs35.ensureDir(path49.dirname(backupPath));
|
|
8271
8975
|
await fs35.copy(abs, backupPath);
|
|
8272
8976
|
}
|
|
8273
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
8977
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path49.relative(root, backupRoot)}.`);
|
|
8274
8978
|
}
|
|
8275
8979
|
async function runUndo(options) {
|
|
8276
8980
|
const root = options.root ?? process.cwd();
|
|
@@ -8283,7 +8987,7 @@ async function runUndo(options) {
|
|
|
8283
8987
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
8284
8988
|
return;
|
|
8285
8989
|
}
|
|
8286
|
-
const relTargets = managed.map((p) =>
|
|
8990
|
+
const relTargets = managed.map((p) => path49.relative(root, p));
|
|
8287
8991
|
const summaryParts = [...relTargets];
|
|
8288
8992
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
8289
8993
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -8307,7 +9011,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
8307
9011
|
for (const abs of managed) {
|
|
8308
9012
|
if (!await fs35.pathExists(abs)) continue;
|
|
8309
9013
|
await fs35.remove(abs);
|
|
8310
|
-
log(`Removed ${
|
|
9014
|
+
log(`Removed ${path49.relative(root, abs)}`);
|
|
8311
9015
|
}
|
|
8312
9016
|
if (stripSettings) await stripProjectSettings(root);
|
|
8313
9017
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -8318,16 +9022,43 @@ User-owned .claude/ files will be preserved.`
|
|
|
8318
9022
|
|
|
8319
9023
|
// src/install/uninstall.ts
|
|
8320
9024
|
import crypto3 from "node:crypto";
|
|
8321
|
-
import
|
|
9025
|
+
import path50 from "node:path";
|
|
8322
9026
|
import fs36 from "fs-extra";
|
|
8323
9027
|
async function runUninstall(options = {}) {
|
|
8324
|
-
const { force = false } = options;
|
|
9028
|
+
const { force = false, removePlugins = removeGlobalPlugins } = options;
|
|
8325
9029
|
const manifest = await readManifest();
|
|
8326
|
-
const result = {
|
|
9030
|
+
const result = {
|
|
9031
|
+
deleted: [],
|
|
9032
|
+
skipped: [],
|
|
9033
|
+
hooksStripped: false,
|
|
9034
|
+
pluginsRemoved: []
|
|
9035
|
+
};
|
|
8327
9036
|
if (!manifest) {
|
|
8328
9037
|
warn("No install manifest found \u2014 nothing to uninstall.");
|
|
8329
9038
|
return result;
|
|
8330
9039
|
}
|
|
9040
|
+
const recordedPlugins = Array.isArray(manifest.plugins) ? manifest.plugins : [];
|
|
9041
|
+
const malformed = recordedPlugins.filter((entry) => !isValidPin(entry)).length;
|
|
9042
|
+
if (malformed > 0) {
|
|
9043
|
+
warn(
|
|
9044
|
+
`Ignoring ${malformed} malformed plugin record(s) in the install manifest \u2014 continuing with the rest of the uninstall.`
|
|
9045
|
+
);
|
|
9046
|
+
}
|
|
9047
|
+
const managedPlugins = recordedPlugins.filter(isValidPin).map((p) => ({
|
|
9048
|
+
id: p.id,
|
|
9049
|
+
plugin: p.plugin,
|
|
9050
|
+
marketplace: p.marketplace,
|
|
9051
|
+
url: p.url,
|
|
9052
|
+
ref: p.ref
|
|
9053
|
+
}));
|
|
9054
|
+
try {
|
|
9055
|
+
const pluginRemoval = await removePlugins(managedPlugins);
|
|
9056
|
+
result.pluginsRemoved = pluginRemoval.removed;
|
|
9057
|
+
} catch (err) {
|
|
9058
|
+
warn(
|
|
9059
|
+
`Could not remove global plugin(s): ${err instanceof Error ? err.message : String(err)} \u2014 continuing with the rest of the uninstall.`
|
|
9060
|
+
);
|
|
9061
|
+
}
|
|
8331
9062
|
for (const entry of manifest.files) {
|
|
8332
9063
|
const exists = fs36.pathExistsSync(entry.destPath);
|
|
8333
9064
|
if (!exists) continue;
|
|
@@ -8348,14 +9079,14 @@ async function runUninstall(options = {}) {
|
|
|
8348
9079
|
continue;
|
|
8349
9080
|
}
|
|
8350
9081
|
await fs36.remove(entry.destPath);
|
|
8351
|
-
await pruneEmptyDir(
|
|
9082
|
+
await pruneEmptyDir(path50.dirname(entry.destPath));
|
|
8352
9083
|
result.deleted.push(entry.destPath);
|
|
8353
9084
|
}
|
|
8354
9085
|
const settings = await readSettings();
|
|
8355
9086
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
8356
9087
|
await writeSettings(stripped);
|
|
8357
9088
|
result.hooksStripped = true;
|
|
8358
|
-
const hausDir =
|
|
9089
|
+
const hausDir = path50.join(globalClaudeDir(), "haus");
|
|
8359
9090
|
const manifestPath2 = hausManifestPath();
|
|
8360
9091
|
if (fs36.pathExistsSync(manifestPath2)) {
|
|
8361
9092
|
await fs36.remove(manifestPath2);
|
|
@@ -8381,6 +9112,10 @@ function printUninstallResult(result) {
|
|
|
8381
9112
|
if (result.hooksStripped) {
|
|
8382
9113
|
log("Haus hook entries removed from ~/.claude/settings.json");
|
|
8383
9114
|
}
|
|
9115
|
+
if (result.pluginsRemoved.length) {
|
|
9116
|
+
log(`Global plugin(s) removed: ${result.pluginsRemoved.join(", ")}`);
|
|
9117
|
+
log("Projects relying on those plugin skills get them back by reinstalling haus.");
|
|
9118
|
+
}
|
|
8384
9119
|
}
|
|
8385
9120
|
|
|
8386
9121
|
// src/commands/uninstall.ts
|
|
@@ -8409,11 +9144,11 @@ async function runUninstallCommand(options) {
|
|
|
8409
9144
|
}
|
|
8410
9145
|
|
|
8411
9146
|
// src/commands/validate-catalog.ts
|
|
8412
|
-
import
|
|
9147
|
+
import path52 from "node:path";
|
|
8413
9148
|
|
|
8414
9149
|
// src/catalog/validate-core.ts
|
|
8415
9150
|
import fs37 from "node:fs";
|
|
8416
|
-
import
|
|
9151
|
+
import path51 from "node:path";
|
|
8417
9152
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
8418
9153
|
var ITEM_ID_PATTERN = /^haus\./;
|
|
8419
9154
|
function auditForbiddenStacks(items) {
|
|
@@ -8562,7 +9297,7 @@ function readForAudit(absPath, label, failures) {
|
|
|
8562
9297
|
}
|
|
8563
9298
|
}
|
|
8564
9299
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
8565
|
-
const rel =
|
|
9300
|
+
const rel = path51.relative(manifestDir, absPath);
|
|
8566
9301
|
const failures = [];
|
|
8567
9302
|
const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
|
|
8568
9303
|
if (text === null) return failures;
|
|
@@ -8584,25 +9319,25 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
8584
9319
|
const failures = [];
|
|
8585
9320
|
for (const item of items) {
|
|
8586
9321
|
if (!item.path || !isSafeCatalogPath(item.path)) continue;
|
|
8587
|
-
const absPath =
|
|
9322
|
+
const absPath = path51.join(manifestDir, item.path);
|
|
8588
9323
|
if (item.type === "skill") {
|
|
8589
|
-
const skillMd =
|
|
9324
|
+
const skillMd = path51.join(absPath, "SKILL.md");
|
|
8590
9325
|
if (!fs37.existsSync(skillMd)) {
|
|
8591
|
-
failures.push(`${item.id}: missing ${
|
|
9326
|
+
failures.push(`${item.id}: missing ${path51.relative(manifestDir, skillMd)}`);
|
|
8592
9327
|
continue;
|
|
8593
9328
|
}
|
|
8594
9329
|
const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
|
|
8595
9330
|
if (text === null) continue;
|
|
8596
9331
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
8597
9332
|
failures.push(
|
|
8598
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
9333
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path51.relative(manifestDir, skillMd)}`)
|
|
8599
9334
|
);
|
|
8600
9335
|
} else if (item.type === "agent") {
|
|
8601
9336
|
if (!fs37.existsSync(absPath)) {
|
|
8602
9337
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
8603
9338
|
continue;
|
|
8604
9339
|
}
|
|
8605
|
-
const rel =
|
|
9340
|
+
const rel = path51.relative(manifestDir, absPath);
|
|
8606
9341
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
8607
9342
|
if (text === null) continue;
|
|
8608
9343
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -8618,7 +9353,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
8618
9353
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
8619
9354
|
continue;
|
|
8620
9355
|
}
|
|
8621
|
-
const rel =
|
|
9356
|
+
const rel = path51.relative(manifestDir, absPath);
|
|
8622
9357
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
8623
9358
|
if (text === null) continue;
|
|
8624
9359
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -8650,7 +9385,7 @@ function walkAllFiles(dir, fn, failures) {
|
|
|
8650
9385
|
}
|
|
8651
9386
|
for (const entry of entries) {
|
|
8652
9387
|
if (entry.name === ".DS_Store") continue;
|
|
8653
|
-
const full =
|
|
9388
|
+
const full = path51.join(dir, entry.name);
|
|
8654
9389
|
if (entry.isDirectory()) walkAllFiles(full, fn, failures);
|
|
8655
9390
|
else fn(full);
|
|
8656
9391
|
}
|
|
@@ -8668,12 +9403,12 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
8668
9403
|
const failures = [];
|
|
8669
9404
|
const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
|
|
8670
9405
|
for (const dir of CONTENT_ROOTS) {
|
|
8671
|
-
const abs =
|
|
9406
|
+
const abs = path51.join(manifestDir, dir);
|
|
8672
9407
|
if (!fs37.existsSync(abs)) continue;
|
|
8673
9408
|
walkAllFiles(
|
|
8674
9409
|
abs,
|
|
8675
9410
|
(file) => {
|
|
8676
|
-
const relPath =
|
|
9411
|
+
const relPath = path51.relative(manifestDir, file).replace(/\\/g, "/");
|
|
8677
9412
|
if (!isPathClaimed(relPath, claimedPaths)) {
|
|
8678
9413
|
failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
|
|
8679
9414
|
}
|
|
@@ -8694,7 +9429,7 @@ function walkMd(dir, fn, failures) {
|
|
|
8694
9429
|
return;
|
|
8695
9430
|
}
|
|
8696
9431
|
for (const entry of entries) {
|
|
8697
|
-
const full =
|
|
9432
|
+
const full = path51.join(dir, entry.name);
|
|
8698
9433
|
if (entry.isDirectory()) walkMd(full, fn, failures);
|
|
8699
9434
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
8700
9435
|
}
|
|
@@ -8703,13 +9438,13 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
8703
9438
|
const failures = [];
|
|
8704
9439
|
const pathSourceMap = buildItemPathSourceMap(items);
|
|
8705
9440
|
for (const dir of ["skills", "agents", "templates", "commands"]) {
|
|
8706
|
-
const abs =
|
|
9441
|
+
const abs = path51.join(manifestDir, dir);
|
|
8707
9442
|
if (!fs37.existsSync(abs)) continue;
|
|
8708
9443
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
8709
9444
|
walkMd(
|
|
8710
9445
|
abs,
|
|
8711
9446
|
(file) => {
|
|
8712
|
-
const rel =
|
|
9447
|
+
const rel = path51.relative(manifestDir, file);
|
|
8713
9448
|
const text = readForAudit(file, rel, failures);
|
|
8714
9449
|
if (text === null) return;
|
|
8715
9450
|
const norm = rel.replace(/\\/g, "/");
|
|
@@ -8754,8 +9489,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
8754
9489
|
process.exitCode = 1;
|
|
8755
9490
|
return;
|
|
8756
9491
|
}
|
|
8757
|
-
const abs =
|
|
8758
|
-
const manifestDir =
|
|
9492
|
+
const abs = path52.resolve(process.cwd(), manifestPath2);
|
|
9493
|
+
const manifestDir = path52.dirname(abs);
|
|
8759
9494
|
const data = await readJson(abs);
|
|
8760
9495
|
if (!data?.items) {
|
|
8761
9496
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -8772,7 +9507,7 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
8772
9507
|
}
|
|
8773
9508
|
|
|
8774
9509
|
// src/workspace/worktree/add.ts
|
|
8775
|
-
import
|
|
9510
|
+
import path59 from "node:path";
|
|
8776
9511
|
import fs41 from "fs-extra";
|
|
8777
9512
|
|
|
8778
9513
|
// src/workspace/worktree/git-worktree.ts
|
|
@@ -8892,12 +9627,12 @@ async function hasUnpushedWork(cwd) {
|
|
|
8892
9627
|
}
|
|
8893
9628
|
|
|
8894
9629
|
// src/workspace/worktree/hydrate.ts
|
|
8895
|
-
import
|
|
9630
|
+
import path55 from "node:path";
|
|
8896
9631
|
import fs39 from "fs-extra";
|
|
8897
9632
|
|
|
8898
9633
|
// src/workspace/worktree/cow-copy.ts
|
|
8899
9634
|
init_exec();
|
|
8900
|
-
import
|
|
9635
|
+
import path53 from "node:path";
|
|
8901
9636
|
import fs38 from "fs-extra";
|
|
8902
9637
|
async function getMacFilesystemType(dir) {
|
|
8903
9638
|
const df = await runCommand("df", ["-P", dir]);
|
|
@@ -8932,11 +9667,11 @@ async function cowCopyDir(src, dest) {
|
|
|
8932
9667
|
const srcExists = await fs38.pathExists(src);
|
|
8933
9668
|
if (!srcExists)
|
|
8934
9669
|
return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
|
|
8935
|
-
const strategy = await detectCowStrategy(
|
|
9670
|
+
const strategy = await detectCowStrategy(path53.dirname(dest));
|
|
8936
9671
|
if (strategy === "unsupported" || strategy === "unknown-platform") {
|
|
8937
9672
|
return { strategy, attempted: false, ok: false };
|
|
8938
9673
|
}
|
|
8939
|
-
await fs38.ensureDir(
|
|
9674
|
+
await fs38.ensureDir(path53.dirname(dest));
|
|
8940
9675
|
const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
|
|
8941
9676
|
const result = await runCommand("cp", args);
|
|
8942
9677
|
return {
|
|
@@ -8950,7 +9685,7 @@ async function cowCopyDir(src, dest) {
|
|
|
8950
9685
|
// src/workspace/worktree/install.ts
|
|
8951
9686
|
init_exec();
|
|
8952
9687
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
8953
|
-
import
|
|
9688
|
+
import path54 from "node:path";
|
|
8954
9689
|
import fg6 from "fast-glob";
|
|
8955
9690
|
function detectInstallPlan(signals) {
|
|
8956
9691
|
if (signals.hasYarnLock) {
|
|
@@ -8971,10 +9706,10 @@ function detectInstallPlan(signals) {
|
|
|
8971
9706
|
return { manager: "none" };
|
|
8972
9707
|
}
|
|
8973
9708
|
async function computeLockfileSignals(dir) {
|
|
8974
|
-
const hasYarnLock = existsSync3(
|
|
8975
|
-
const hasPnpmLock = existsSync3(
|
|
8976
|
-
const hasPackageLock = existsSync3(
|
|
8977
|
-
const hasComposerLock = existsSync3(
|
|
9709
|
+
const hasYarnLock = existsSync3(path54.join(dir, "yarn.lock"));
|
|
9710
|
+
const hasPnpmLock = existsSync3(path54.join(dir, "pnpm-lock.yaml"));
|
|
9711
|
+
const hasPackageLock = existsSync3(path54.join(dir, "package-lock.json"));
|
|
9712
|
+
const hasComposerLock = existsSync3(path54.join(dir, "composer.lock"));
|
|
8978
9713
|
const dotnetFiles = await fg6(["**/*.csproj", "**/*.sln"], {
|
|
8979
9714
|
cwd: dir,
|
|
8980
9715
|
onlyFiles: true,
|
|
@@ -8988,7 +9723,7 @@ async function computeLockfileSignals(dir) {
|
|
|
8988
9723
|
});
|
|
8989
9724
|
const hasDotnetProject = dotnetFiles.length > 0;
|
|
8990
9725
|
let hasPackageManagerField = false;
|
|
8991
|
-
const pkgPath =
|
|
9726
|
+
const pkgPath = path54.join(dir, "package.json");
|
|
8992
9727
|
if (existsSync3(pkgPath)) {
|
|
8993
9728
|
try {
|
|
8994
9729
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
@@ -9029,8 +9764,8 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
9029
9764
|
const start = Date.now();
|
|
9030
9765
|
const targets = [];
|
|
9031
9766
|
for (const target of HYDRATION_TARGETS) {
|
|
9032
|
-
const src =
|
|
9033
|
-
const dest =
|
|
9767
|
+
const src = path55.join(mainMemberPath, target);
|
|
9768
|
+
const dest = path55.join(worktreeMemberPath, target);
|
|
9034
9769
|
if (!await fs39.pathExists(src)) continue;
|
|
9035
9770
|
const destExists = await fs39.pathExists(dest);
|
|
9036
9771
|
if (!opts.force && destExists) continue;
|
|
@@ -9048,11 +9783,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
9048
9783
|
|
|
9049
9784
|
// src/workspace/worktree/local-files.ts
|
|
9050
9785
|
init_exec();
|
|
9051
|
-
import
|
|
9786
|
+
import path56 from "node:path";
|
|
9052
9787
|
import fs40 from "fs-extra";
|
|
9053
9788
|
var MACHINE_LOCAL_FILES = [
|
|
9054
9789
|
"docker-compose.local.yml",
|
|
9055
|
-
|
|
9790
|
+
path56.join(".claude", "settings.local.json")
|
|
9056
9791
|
];
|
|
9057
9792
|
async function isTracked(repoRoot, relFile) {
|
|
9058
9793
|
const gitPath = relFile.replace(/\\/g, "/");
|
|
@@ -9062,11 +9797,11 @@ async function isTracked(repoRoot, relFile) {
|
|
|
9062
9797
|
async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
9063
9798
|
const copied = [];
|
|
9064
9799
|
for (const rel of MACHINE_LOCAL_FILES) {
|
|
9065
|
-
const src =
|
|
9800
|
+
const src = path56.join(mainMemberPath, rel);
|
|
9066
9801
|
if (!await fs40.pathExists(src)) continue;
|
|
9067
9802
|
if (await isTracked(mainMemberPath, rel)) continue;
|
|
9068
|
-
const dest =
|
|
9069
|
-
await fs40.ensureDir(
|
|
9803
|
+
const dest = path56.join(worktreeMemberPath, rel);
|
|
9804
|
+
await fs40.ensureDir(path56.dirname(dest));
|
|
9070
9805
|
await fs40.copy(src, dest, { overwrite: true });
|
|
9071
9806
|
copied.push(rel);
|
|
9072
9807
|
}
|
|
@@ -9075,15 +9810,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
|
9075
9810
|
|
|
9076
9811
|
// src/workspace/worktree/root.ts
|
|
9077
9812
|
import { existsSync as existsSync4 } from "node:fs";
|
|
9078
|
-
import
|
|
9813
|
+
import path57 from "node:path";
|
|
9079
9814
|
async function resolveWorkspaceForWorktree(start) {
|
|
9080
9815
|
const rootInfo = await resolveRoots(start);
|
|
9081
9816
|
if (!rootInfo.isGitRepo) {
|
|
9082
9817
|
return { ok: false, reason: "Not inside a git repository." };
|
|
9083
9818
|
}
|
|
9084
9819
|
const workspaceRoot = rootInfo.mainRoot;
|
|
9085
|
-
const hasYaml = existsSync4(
|
|
9086
|
-
const hasManifest = existsSync4(
|
|
9820
|
+
const hasYaml = existsSync4(path57.join(workspaceRoot, WORKSPACE_FILE));
|
|
9821
|
+
const hasManifest = existsSync4(path57.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
9087
9822
|
if (!hasYaml && !hasManifest) {
|
|
9088
9823
|
return {
|
|
9089
9824
|
ok: false,
|
|
@@ -9108,10 +9843,10 @@ function selectMembers(members, only) {
|
|
|
9108
9843
|
}
|
|
9109
9844
|
|
|
9110
9845
|
// src/workspace/worktree/state.ts
|
|
9111
|
-
import
|
|
9846
|
+
import path58 from "node:path";
|
|
9112
9847
|
var WORKTREE_STATE_FILE = ".haus-worktree.json";
|
|
9113
9848
|
function worktreesDir(mainRoot) {
|
|
9114
|
-
return
|
|
9849
|
+
return path58.join(mainRoot, ".claude", "worktrees");
|
|
9115
9850
|
}
|
|
9116
9851
|
function isValidWorktreeSlug(slug) {
|
|
9117
9852
|
if (slug === "" || slug === "." || slug === "..") return false;
|
|
@@ -9124,15 +9859,15 @@ function worktreePath(mainRoot, slug) {
|
|
|
9124
9859
|
`Invalid worktree slug ${JSON.stringify(slug)} \u2014 slugs may only contain letters, digits, ".", "_", "-" (no path separators, and not "." or "..").`
|
|
9125
9860
|
);
|
|
9126
9861
|
}
|
|
9127
|
-
return
|
|
9862
|
+
return path58.join(worktreesDir(mainRoot), slug);
|
|
9128
9863
|
}
|
|
9129
9864
|
async function readWorktreeState(worktreeRoot) {
|
|
9130
|
-
const state = await readJson(
|
|
9865
|
+
const state = await readJson(path58.join(worktreeRoot, WORKTREE_STATE_FILE));
|
|
9131
9866
|
if (state == null || typeof state !== "object" || !Array.isArray(state.members)) return void 0;
|
|
9132
9867
|
return state;
|
|
9133
9868
|
}
|
|
9134
9869
|
async function writeWorktreeState(worktreeRoot, state) {
|
|
9135
|
-
await writeJson(
|
|
9870
|
+
await writeJson(path58.join(worktreeRoot, WORKTREE_STATE_FILE), state);
|
|
9136
9871
|
}
|
|
9137
9872
|
|
|
9138
9873
|
// src/workspace/worktree/add.ts
|
|
@@ -9166,7 +9901,7 @@ async function runAdd(opts) {
|
|
|
9166
9901
|
const memberResults = [];
|
|
9167
9902
|
for (const member of selected) {
|
|
9168
9903
|
const start = Date.now();
|
|
9169
|
-
const memberWorktreePath =
|
|
9904
|
+
const memberWorktreePath = path59.join(wsWorktreePath, member.folder);
|
|
9170
9905
|
const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
|
|
9171
9906
|
preferDefaultBranchFrom: true
|
|
9172
9907
|
});
|
|
@@ -9196,7 +9931,7 @@ async function runAdd(opts) {
|
|
|
9196
9931
|
await mapWithConcurrency(okResults, async (result) => {
|
|
9197
9932
|
const member = selected.find((m) => m.id === result.member);
|
|
9198
9933
|
if (!member) return;
|
|
9199
|
-
const memberWorktreePath =
|
|
9934
|
+
const memberWorktreePath = path59.join(wsWorktreePath, member.folder);
|
|
9200
9935
|
const hydrationStart = Date.now();
|
|
9201
9936
|
if (opts.hydrate !== false) {
|
|
9202
9937
|
result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
|
|
@@ -9263,10 +9998,10 @@ async function runDryRun(args) {
|
|
|
9263
9998
|
|
|
9264
9999
|
// src/workspace/worktree/doctor.ts
|
|
9265
10000
|
import { existsSync as existsSync5 } from "node:fs";
|
|
9266
|
-
import
|
|
10001
|
+
import path60 from "node:path";
|
|
9267
10002
|
var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
|
|
9268
10003
|
function looksHydrated(memberPath) {
|
|
9269
|
-
return HYDRATION_MARKERS.some((marker) => existsSync5(
|
|
10004
|
+
return HYDRATION_MARKERS.some((marker) => existsSync5(path60.join(memberPath, marker)));
|
|
9270
10005
|
}
|
|
9271
10006
|
async function runDoctor2() {
|
|
9272
10007
|
const rootInfo = await resolveRoots();
|
|
@@ -9280,7 +10015,7 @@ async function runDoctor2() {
|
|
|
9280
10015
|
};
|
|
9281
10016
|
}
|
|
9282
10017
|
const workspaceRoot = rootInfo.mainRoot;
|
|
9283
|
-
const isWorkspace = existsSync5(
|
|
10018
|
+
const isWorkspace = existsSync5(path60.join(workspaceRoot, WORKSPACE_FILE)) || existsSync5(path60.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
9284
10019
|
if (!isWorkspace) {
|
|
9285
10020
|
return {
|
|
9286
10021
|
isWorkspace: false,
|
|
@@ -9293,7 +10028,7 @@ async function runDoctor2() {
|
|
|
9293
10028
|
};
|
|
9294
10029
|
}
|
|
9295
10030
|
const wsWorktreesDir = worktreesDir(workspaceRoot);
|
|
9296
|
-
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${
|
|
10031
|
+
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path60.sep}`);
|
|
9297
10032
|
const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
|
|
9298
10033
|
const members = await readMembers(rootInfo);
|
|
9299
10034
|
const problems = [];
|
|
@@ -9301,10 +10036,10 @@ async function runDoctor2() {
|
|
|
9301
10036
|
let slug;
|
|
9302
10037
|
if (inWorkspaceWorktree) {
|
|
9303
10038
|
const wtPath = rootInfo.repoRoot;
|
|
9304
|
-
slug = rootInfo.worktreeName ??
|
|
10039
|
+
slug = rootInfo.worktreeName ?? path60.basename(wtPath);
|
|
9305
10040
|
const state = await readWorktreeState(wtPath);
|
|
9306
10041
|
for (const member of members) {
|
|
9307
|
-
const memberPath =
|
|
10042
|
+
const memberPath = path60.join(wtPath, member.folder);
|
|
9308
10043
|
const materialized = existsSync5(memberPath);
|
|
9309
10044
|
const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
|
|
9310
10045
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
@@ -9335,16 +10070,16 @@ async function runDoctor2() {
|
|
|
9335
10070
|
for (const member of members) {
|
|
9336
10071
|
const entries = await listWorktrees(member.absPath);
|
|
9337
10072
|
for (const entry of entries) {
|
|
9338
|
-
const entryPath =
|
|
9339
|
-
const marker = `${
|
|
10073
|
+
const entryPath = path60.normalize(entry.path);
|
|
10074
|
+
const marker = `${path60.sep}.claude${path60.sep}worktrees${path60.sep}`;
|
|
9340
10075
|
const idx = entryPath.indexOf(marker);
|
|
9341
10076
|
if (idx === -1) continue;
|
|
9342
10077
|
const rest = entryPath.slice(idx + marker.length);
|
|
9343
|
-
const slugName = rest.split(
|
|
10078
|
+
const slugName = rest.split(path60.sep)[0];
|
|
9344
10079
|
if (!slugName) continue;
|
|
9345
|
-
if (!existsSync5(
|
|
10080
|
+
if (!existsSync5(path60.join(wsWorktreesDir, slugName))) {
|
|
9346
10081
|
orphans.push(
|
|
9347
|
-
`${member.id}: worktree registered at ${entry.path} but ${
|
|
10082
|
+
`${member.id}: worktree registered at ${entry.path} but ${path60.join(wsWorktreesDir, slugName)} no longer exists \u2014 run \`git -C "${member.absPath}" worktree prune\`.`
|
|
9348
10083
|
);
|
|
9349
10084
|
}
|
|
9350
10085
|
}
|
|
@@ -9355,7 +10090,7 @@ async function runDoctor2() {
|
|
|
9355
10090
|
|
|
9356
10091
|
// src/workspace/worktree/hydrate-command.ts
|
|
9357
10092
|
import { existsSync as existsSync6 } from "node:fs";
|
|
9358
|
-
import
|
|
10093
|
+
import path61 from "node:path";
|
|
9359
10094
|
async function runHydrateWorktree(opts) {
|
|
9360
10095
|
const rootInfo = await resolveRoots();
|
|
9361
10096
|
if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
|
|
@@ -9366,7 +10101,7 @@ async function runHydrateWorktree(opts) {
|
|
|
9366
10101
|
}
|
|
9367
10102
|
const workspaceRoot = rootInfo.mainRoot;
|
|
9368
10103
|
const wtPath = rootInfo.repoRoot;
|
|
9369
|
-
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${
|
|
10104
|
+
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path61.sep}`)) {
|
|
9370
10105
|
return {
|
|
9371
10106
|
ok: false,
|
|
9372
10107
|
error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
|
|
@@ -9378,13 +10113,13 @@ async function runHydrateWorktree(opts) {
|
|
|
9378
10113
|
return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
|
|
9379
10114
|
}
|
|
9380
10115
|
const state = await readWorktreeState(wtPath);
|
|
9381
|
-
const slug = state?.slug ??
|
|
10116
|
+
const slug = state?.slug ?? path61.basename(wtPath);
|
|
9382
10117
|
const detectedBranch = await currentBranch(wtPath);
|
|
9383
10118
|
const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
|
|
9384
10119
|
if (opts.dryRun) {
|
|
9385
10120
|
const results2 = await Promise.all(
|
|
9386
10121
|
selected.map(async (member) => {
|
|
9387
|
-
const memberPath =
|
|
10122
|
+
const memberPath = path61.join(wtPath, member.folder);
|
|
9388
10123
|
if (existsSync6(memberPath)) {
|
|
9389
10124
|
return { member: member.id, folder: member.folder, status: "planned" };
|
|
9390
10125
|
}
|
|
@@ -9405,7 +10140,7 @@ async function runHydrateWorktree(opts) {
|
|
|
9405
10140
|
const results = await mapWithConcurrency(
|
|
9406
10141
|
selected,
|
|
9407
10142
|
async (member) => {
|
|
9408
|
-
const memberPath =
|
|
10143
|
+
const memberPath = path61.join(wtPath, member.folder);
|
|
9409
10144
|
let branchAction;
|
|
9410
10145
|
if (!existsSync6(memberPath)) {
|
|
9411
10146
|
if (!opts.materialize) {
|
|
@@ -9468,7 +10203,7 @@ async function runHydrateWorktree(opts) {
|
|
|
9468
10203
|
}
|
|
9469
10204
|
|
|
9470
10205
|
// src/workspace/worktree/list.ts
|
|
9471
|
-
import
|
|
10206
|
+
import path62 from "node:path";
|
|
9472
10207
|
import fs42 from "fs-extra";
|
|
9473
10208
|
async function runList2() {
|
|
9474
10209
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -9481,12 +10216,12 @@ async function runList2() {
|
|
|
9481
10216
|
const configuredMembers = await readMembers(rootInfo);
|
|
9482
10217
|
const worktrees = [];
|
|
9483
10218
|
for (const slug of slugs) {
|
|
9484
|
-
const wtPath =
|
|
10219
|
+
const wtPath = path62.join(dir, slug);
|
|
9485
10220
|
const state = await readWorktreeState(wtPath);
|
|
9486
10221
|
const members = [];
|
|
9487
10222
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
9488
10223
|
for (const m of source) {
|
|
9489
|
-
const memberPath =
|
|
10224
|
+
const memberPath = path62.join(wtPath, m.folder);
|
|
9490
10225
|
const materialized = await fs42.pathExists(memberPath);
|
|
9491
10226
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
9492
10227
|
members.push({
|
|
@@ -9508,7 +10243,7 @@ async function runList2() {
|
|
|
9508
10243
|
}
|
|
9509
10244
|
|
|
9510
10245
|
// src/workspace/worktree/remove.ts
|
|
9511
|
-
import
|
|
10246
|
+
import path63 from "node:path";
|
|
9512
10247
|
import fs43 from "fs-extra";
|
|
9513
10248
|
async function runRemove(opts) {
|
|
9514
10249
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -9535,7 +10270,7 @@ async function runRemove(opts) {
|
|
|
9535
10270
|
targets.push({
|
|
9536
10271
|
repo: configMember?.id ?? stateMember.id,
|
|
9537
10272
|
ownerRepo,
|
|
9538
|
-
path:
|
|
10273
|
+
path: path63.join(wsWorktreePath, stateMember.folder)
|
|
9539
10274
|
});
|
|
9540
10275
|
}
|
|
9541
10276
|
const existingTargets = [];
|
|
@@ -9603,12 +10338,6 @@ async function runRemove(opts) {
|
|
|
9603
10338
|
}
|
|
9604
10339
|
|
|
9605
10340
|
// src/commands/workspace/worktree.ts
|
|
9606
|
-
function normalizeOnly(only) {
|
|
9607
|
-
if (!only) return void 0;
|
|
9608
|
-
const list = Array.isArray(only) ? only : only.split(/[\s,]+/);
|
|
9609
|
-
const cleaned = list.map((s) => s.trim()).filter(Boolean);
|
|
9610
|
-
return cleaned.length > 0 ? cleaned : void 0;
|
|
9611
|
-
}
|
|
9612
10341
|
function describeInstall(hydration) {
|
|
9613
10342
|
if (hydration.install.plan.manager === "none") return "no install step (no recognized lockfile)";
|
|
9614
10343
|
if (!hydration.install.ran)
|
|
@@ -9812,7 +10541,7 @@ async function runWorktree(action, opts = {}) {
|
|
|
9812
10541
|
|
|
9813
10542
|
// src/commands/workspace.ts
|
|
9814
10543
|
import { existsSync as existsSync10, statSync as statSync3 } from "node:fs";
|
|
9815
|
-
import
|
|
10544
|
+
import path72 from "node:path";
|
|
9816
10545
|
|
|
9817
10546
|
// src/commands/workspace/aggregate.ts
|
|
9818
10547
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -9867,18 +10596,18 @@ ${summaries.map(
|
|
|
9867
10596
|
|
|
9868
10597
|
// src/commands/workspace/doctor.ts
|
|
9869
10598
|
import { existsSync as existsSync7 } from "node:fs";
|
|
9870
|
-
import
|
|
10599
|
+
import path65 from "node:path";
|
|
9871
10600
|
|
|
9872
10601
|
// src/commands/workspace/manifest.ts
|
|
9873
10602
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
9874
|
-
import
|
|
10603
|
+
import path64 from "node:path";
|
|
9875
10604
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
9876
10605
|
function manifestPath(workspaceRoot) {
|
|
9877
10606
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
9878
10607
|
}
|
|
9879
10608
|
function hausVersion() {
|
|
9880
10609
|
try {
|
|
9881
|
-
const pkg = JSON.parse(readFileSync4(
|
|
10610
|
+
const pkg = JSON.parse(readFileSync4(path64.join(packageRoot(), "package.json"), "utf8"));
|
|
9882
10611
|
return pkg.version ?? "0.0.0";
|
|
9883
10612
|
} catch {
|
|
9884
10613
|
return "0.0.0";
|
|
@@ -9970,7 +10699,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
9970
10699
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
9971
10700
|
const catalogRefByRepo = [];
|
|
9972
10701
|
for (const repo of config.repos) {
|
|
9973
|
-
const repoRoot =
|
|
10702
|
+
const repoRoot = path65.resolve(workspaceRoot, repo.path);
|
|
9974
10703
|
const entry = manifestByName.get(repo.name);
|
|
9975
10704
|
if (!entry) {
|
|
9976
10705
|
flag({
|
|
@@ -10043,7 +10772,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
10043
10772
|
});
|
|
10044
10773
|
continue;
|
|
10045
10774
|
}
|
|
10046
|
-
if (!existsSync7(
|
|
10775
|
+
if (!existsSync7(path65.join(workspaceRoot, entry.path))) {
|
|
10047
10776
|
flag({
|
|
10048
10777
|
repo: entry.repo,
|
|
10049
10778
|
kind: "missing-linked-context-copy",
|
|
@@ -10086,15 +10815,15 @@ function emit(args) {
|
|
|
10086
10815
|
}
|
|
10087
10816
|
|
|
10088
10817
|
// src/workspace/link-context/link.ts
|
|
10089
|
-
import
|
|
10818
|
+
import path68 from "node:path";
|
|
10090
10819
|
import fs46 from "fs-extra";
|
|
10091
10820
|
|
|
10092
10821
|
// src/workspace/link-context/plan.ts
|
|
10093
|
-
import
|
|
10822
|
+
import path67 from "node:path";
|
|
10094
10823
|
import fs45 from "fs-extra";
|
|
10095
10824
|
|
|
10096
10825
|
// src/workspace/link-context/scan-source.ts
|
|
10097
|
-
import
|
|
10826
|
+
import path66 from "node:path";
|
|
10098
10827
|
import fs44 from "fs-extra";
|
|
10099
10828
|
var ASSET_SUBDIR = {
|
|
10100
10829
|
skill: "skills",
|
|
@@ -10103,36 +10832,36 @@ var ASSET_SUBDIR = {
|
|
|
10103
10832
|
};
|
|
10104
10833
|
async function hasLinkableContext(member) {
|
|
10105
10834
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
10106
|
-
if (await fs44.pathExists(
|
|
10835
|
+
if (await fs44.pathExists(path66.join(member.absPath, ".claude", subdir))) return true;
|
|
10107
10836
|
}
|
|
10108
10837
|
return false;
|
|
10109
10838
|
}
|
|
10110
10839
|
async function collectSkills(member) {
|
|
10111
|
-
const dir =
|
|
10840
|
+
const dir = path66.join(member.absPath, ".claude", "skills");
|
|
10112
10841
|
if (!await fs44.pathExists(dir)) return [];
|
|
10113
10842
|
const names = await fs44.readdir(dir);
|
|
10114
10843
|
const out = [];
|
|
10115
10844
|
for (const name of [...names].sort()) {
|
|
10116
|
-
const abs =
|
|
10845
|
+
const abs = path66.join(dir, name);
|
|
10117
10846
|
const stat = await fs44.lstat(abs);
|
|
10118
10847
|
if (!stat.isDirectory()) continue;
|
|
10119
|
-
if (!await fs44.pathExists(
|
|
10120
|
-
out.push({ type: "skill", name, sourceRelPath:
|
|
10848
|
+
if (!await fs44.pathExists(path66.join(abs, "SKILL.md"))) continue;
|
|
10849
|
+
out.push({ type: "skill", name, sourceRelPath: path66.posix.join(".claude", "skills", name) });
|
|
10121
10850
|
}
|
|
10122
10851
|
return out;
|
|
10123
10852
|
}
|
|
10124
10853
|
async function collectFlatMarkdown(member, type) {
|
|
10125
10854
|
const subdir = ASSET_SUBDIR[type];
|
|
10126
|
-
const dir =
|
|
10855
|
+
const dir = path66.join(member.absPath, ".claude", subdir);
|
|
10127
10856
|
if (!await fs44.pathExists(dir)) return [];
|
|
10128
10857
|
const names = await fs44.readdir(dir);
|
|
10129
10858
|
const out = [];
|
|
10130
10859
|
for (const fileName of [...names].sort()) {
|
|
10131
10860
|
if (!fileName.endsWith(".md")) continue;
|
|
10132
|
-
const abs =
|
|
10861
|
+
const abs = path66.join(dir, fileName);
|
|
10133
10862
|
if (!(await fs44.lstat(abs)).isFile()) continue;
|
|
10134
10863
|
const name = fileName.slice(0, -".md".length);
|
|
10135
|
-
out.push({ type, name, sourceRelPath:
|
|
10864
|
+
out.push({ type, name, sourceRelPath: path66.posix.join(".claude", subdir, fileName) });
|
|
10136
10865
|
}
|
|
10137
10866
|
return out;
|
|
10138
10867
|
}
|
|
@@ -10147,9 +10876,9 @@ async function collectSourceAssets(member) {
|
|
|
10147
10876
|
|
|
10148
10877
|
// src/workspace/link-context/plan.ts
|
|
10149
10878
|
function repoFolderPrefix(member) {
|
|
10150
|
-
const base =
|
|
10879
|
+
const base = path67.posix.basename(member.folder);
|
|
10151
10880
|
if (base && base !== "." && base !== "/") return base;
|
|
10152
|
-
const fromAbs =
|
|
10881
|
+
const fromAbs = path67.basename(member.absPath);
|
|
10153
10882
|
return fromAbs || member.id;
|
|
10154
10883
|
}
|
|
10155
10884
|
function assetKey(type, destKey) {
|
|
@@ -10206,7 +10935,7 @@ async function buildLinkPlan(members) {
|
|
|
10206
10935
|
name: asset.name,
|
|
10207
10936
|
destKey,
|
|
10208
10937
|
sourceRelPath: asset.sourceRelPath,
|
|
10209
|
-
sourceAbsPath:
|
|
10938
|
+
sourceAbsPath: path67.join(member.absPath, asset.sourceRelPath),
|
|
10210
10939
|
sourceHash
|
|
10211
10940
|
});
|
|
10212
10941
|
}
|
|
@@ -10217,7 +10946,7 @@ async function buildLinkPlan(members) {
|
|
|
10217
10946
|
function destRelPathFor(type, destKey) {
|
|
10218
10947
|
const subdir = `${type}s`;
|
|
10219
10948
|
const leaf = type === "skill" ? destKey : `${destKey}.md`;
|
|
10220
|
-
return
|
|
10949
|
+
return path68.posix.join(".claude", subdir, leaf);
|
|
10221
10950
|
}
|
|
10222
10951
|
async function runLinkContext(workspaceRoot, opts = {}) {
|
|
10223
10952
|
const write = opts.write ?? false;
|
|
@@ -10243,9 +10972,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
10243
10972
|
const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10244
10973
|
for (const entry of plan.entries) {
|
|
10245
10974
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
10246
|
-
const destAbs =
|
|
10975
|
+
const destAbs = path68.join(workspaceRoot, destRel);
|
|
10247
10976
|
if (write) {
|
|
10248
|
-
await fs46.ensureDir(
|
|
10977
|
+
await fs46.ensureDir(path68.dirname(destAbs));
|
|
10249
10978
|
if (await fs46.pathExists(destAbs)) await fs46.remove(destAbs);
|
|
10250
10979
|
await fs46.copy(entry.sourceAbsPath, destAbs, {
|
|
10251
10980
|
overwrite: true,
|
|
@@ -10334,10 +11063,10 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
|
|
|
10334
11063
|
|
|
10335
11064
|
// src/commands/workspace/setup.ts
|
|
10336
11065
|
import { existsSync as existsSync8, statSync } from "node:fs";
|
|
10337
|
-
import
|
|
11066
|
+
import path70 from "node:path";
|
|
10338
11067
|
|
|
10339
11068
|
// src/claude/write-workspace-claude-md.ts
|
|
10340
|
-
import
|
|
11069
|
+
import path69 from "node:path";
|
|
10341
11070
|
import fs47 from "fs-extra";
|
|
10342
11071
|
var WORKSPACE_SENTINELS = { begin: WORKSPACE_BLOCK_BEGIN, end: WORKSPACE_BLOCK_END };
|
|
10343
11072
|
function buildWorkspaceImportBlock(client, members) {
|
|
@@ -10357,7 +11086,7 @@ ${WORKSPACE_BLOCK_END}`;
|
|
|
10357
11086
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
10358
11087
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
10359
11088
|
const dryRun = opts.dryRun ?? false;
|
|
10360
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
11089
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path69.join(workspaceRoot, "CLAUDE.md");
|
|
10361
11090
|
const say = opts.quiet ? () => {
|
|
10362
11091
|
} : log;
|
|
10363
11092
|
const existed = await fs47.pathExists(filePath);
|
|
@@ -10388,20 +11117,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
10388
11117
|
|
|
10389
11118
|
// src/commands/workspace/setup.ts
|
|
10390
11119
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
10391
|
-
let dir =
|
|
11120
|
+
let dir = path70.resolve(start);
|
|
10392
11121
|
for (; ; ) {
|
|
10393
|
-
if (existsSync8(
|
|
10394
|
-
const parent =
|
|
10395
|
-
if (parent === dir) return
|
|
11122
|
+
if (existsSync8(path70.join(dir, WORKSPACE_FILE))) return dir;
|
|
11123
|
+
const parent = path70.dirname(dir);
|
|
11124
|
+
if (parent === dir) return path70.resolve(start);
|
|
10396
11125
|
dir = parent;
|
|
10397
11126
|
}
|
|
10398
11127
|
}
|
|
10399
11128
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
10400
|
-
return
|
|
11129
|
+
return path70.resolve(workspaceRoot, repoPath) === path70.resolve(workspaceRoot);
|
|
10401
11130
|
}
|
|
10402
11131
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
10403
11132
|
const apply = options.write ?? false;
|
|
10404
|
-
const configText = await readText(
|
|
11133
|
+
const configText = await readText(path70.join(workspaceRoot, WORKSPACE_FILE));
|
|
10405
11134
|
if (!configText) {
|
|
10406
11135
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
10407
11136
|
process.exitCode = 1;
|
|
@@ -10428,7 +11157,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10428
11157
|
const statuses = [];
|
|
10429
11158
|
const aggregateInputs = [];
|
|
10430
11159
|
for (const repo of repos) {
|
|
10431
|
-
const repoRoot =
|
|
11160
|
+
const repoRoot = path70.resolve(workspaceRoot, repo.path);
|
|
10432
11161
|
say(`
|
|
10433
11162
|
\u2192 ${repo.name} (${repo.path})`);
|
|
10434
11163
|
try {
|
|
@@ -10468,7 +11197,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10468
11197
|
process.exitCode = 1;
|
|
10469
11198
|
return result2;
|
|
10470
11199
|
}
|
|
10471
|
-
throw err;
|
|
11200
|
+
throw new Error(`Setup failed for ${repo.name}: ${message}`, { cause: err });
|
|
10472
11201
|
}
|
|
10473
11202
|
error(`Setup failed for ${repo.name}: ${message}`);
|
|
10474
11203
|
}
|
|
@@ -10518,7 +11247,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10518
11247
|
const status = statusByName.get(repo.name);
|
|
10519
11248
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
10520
11249
|
if (status?.status === "ok") {
|
|
10521
|
-
const lock = await checkLock(
|
|
11250
|
+
const lock = await checkLock(path70.resolve(workspaceRoot, repo.path));
|
|
10522
11251
|
manifestRepos.push({
|
|
10523
11252
|
name: repo.name,
|
|
10524
11253
|
path: repo.path,
|
|
@@ -10574,7 +11303,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10574
11303
|
try {
|
|
10575
11304
|
const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
|
|
10576
11305
|
if (linkResult.ok) {
|
|
10577
|
-
written.push(...linkResult.added.map((rel) =>
|
|
11306
|
+
written.push(...linkResult.added.map((rel) => path70.join(workspaceRoot, rel)));
|
|
10578
11307
|
if (!options.json) {
|
|
10579
11308
|
say(
|
|
10580
11309
|
`Linked ${linkResult.linked.length} cross-repo skill/agent/command copy(ies) into .claude/ (${linkResult.added.length} new, ${linkResult.removed.length} removed)${linkResult.skipped.length > 0 ? ` \u2014 ${linkResult.skipped.length} member(s) skipped, see above` : ""}.`
|
|
@@ -10606,7 +11335,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
10606
11335
|
|
|
10607
11336
|
// src/commands/workspace/undo.ts
|
|
10608
11337
|
import { existsSync as existsSync9, statSync as statSync2 } from "node:fs";
|
|
10609
|
-
import
|
|
11338
|
+
import path71 from "node:path";
|
|
10610
11339
|
import fs48 from "fs-extra";
|
|
10611
11340
|
var WORKSPACE_AGGREGATE_FILES = [
|
|
10612
11341
|
"workspace-summary.json",
|
|
@@ -10643,7 +11372,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10643
11372
|
}
|
|
10644
11373
|
let anyRepoFailed = false;
|
|
10645
11374
|
for (const repo of config.repos) {
|
|
10646
|
-
const repoRoot =
|
|
11375
|
+
const repoRoot = path71.resolve(workspaceRoot, repo.path);
|
|
10647
11376
|
log(`
|
|
10648
11377
|
\u2192 ${repo.name} (${repo.path})`);
|
|
10649
11378
|
try {
|
|
@@ -10672,20 +11401,20 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10672
11401
|
const abs = hausPath(workspaceRoot, rel);
|
|
10673
11402
|
if (await fs48.pathExists(abs)) {
|
|
10674
11403
|
await fs48.remove(abs);
|
|
10675
|
-
log(`Removed ${
|
|
11404
|
+
log(`Removed ${path71.relative(workspaceRoot, abs)}`);
|
|
10676
11405
|
}
|
|
10677
11406
|
}
|
|
10678
11407
|
const manifestFile = manifestPath(workspaceRoot);
|
|
10679
11408
|
if (await fs48.pathExists(manifestFile)) {
|
|
10680
11409
|
await fs48.remove(manifestFile);
|
|
10681
|
-
log(`Removed ${
|
|
11410
|
+
log(`Removed ${path71.relative(workspaceRoot, manifestFile)}`);
|
|
10682
11411
|
}
|
|
10683
11412
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
10684
11413
|
if (await fs48.pathExists(workspaceMdPath)) {
|
|
10685
11414
|
await fs48.remove(workspaceMdPath);
|
|
10686
|
-
log(`Removed ${
|
|
11415
|
+
log(`Removed ${path71.relative(workspaceRoot, workspaceMdPath)}`);
|
|
10687
11416
|
} else {
|
|
10688
|
-
const claudeMdPath =
|
|
11417
|
+
const claudeMdPath = path71.join(workspaceRoot, "CLAUDE.md");
|
|
10689
11418
|
if (existsSync9(claudeMdPath)) {
|
|
10690
11419
|
const prev = await readText(claudeMdPath);
|
|
10691
11420
|
if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
|
|
@@ -10724,12 +11453,6 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10724
11453
|
}
|
|
10725
11454
|
|
|
10726
11455
|
// src/commands/workspace.ts
|
|
10727
|
-
function normalizeOnly2(only) {
|
|
10728
|
-
if (!only) return void 0;
|
|
10729
|
-
const list = Array.isArray(only) ? only : only.split(/[\s,]+/);
|
|
10730
|
-
const cleaned = list.map((s) => s.trim()).filter(Boolean);
|
|
10731
|
-
return cleaned.length > 0 ? cleaned : void 0;
|
|
10732
|
-
}
|
|
10733
11456
|
function normalizeMaxDepth(maxDepth) {
|
|
10734
11457
|
if (maxDepth === void 0) return void 0;
|
|
10735
11458
|
const n = typeof maxDepth === "number" ? maxDepth : Number.parseInt(maxDepth, 10);
|
|
@@ -10760,7 +11483,7 @@ relationships: []
|
|
|
10760
11483
|
log("Workspace initialized.");
|
|
10761
11484
|
}
|
|
10762
11485
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
10763
|
-
const configText = await readText(
|
|
11486
|
+
const configText = await readText(path72.join(workspaceRoot, WORKSPACE_FILE));
|
|
10764
11487
|
if (!configText) {
|
|
10765
11488
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
10766
11489
|
process.exitCode = 1;
|
|
@@ -10782,7 +11505,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
10782
11505
|
const inputs = [];
|
|
10783
11506
|
const failures = [];
|
|
10784
11507
|
for (const repo of config.repos) {
|
|
10785
|
-
const repoRoot =
|
|
11508
|
+
const repoRoot = path72.resolve(workspaceRoot, repo.path);
|
|
10786
11509
|
try {
|
|
10787
11510
|
if (!existsSync10(repoRoot) || !statSync3(repoRoot).isDirectory()) {
|
|
10788
11511
|
throw new Error("path is not a directory");
|
|
@@ -10830,7 +11553,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
10830
11553
|
dryRun: options.dryRun,
|
|
10831
11554
|
json: options.json,
|
|
10832
11555
|
continueOnError: options.continueOnError,
|
|
10833
|
-
only:
|
|
11556
|
+
only: normalizeOnly(options.only)
|
|
10834
11557
|
});
|
|
10835
11558
|
return;
|
|
10836
11559
|
case "doctor":
|
|
@@ -10848,7 +11571,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
10848
11571
|
// src/cli.ts
|
|
10849
11572
|
function cliVersion() {
|
|
10850
11573
|
try {
|
|
10851
|
-
const pkgPath =
|
|
11574
|
+
const pkgPath = path73.join(packageRoot(), "package.json");
|
|
10852
11575
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
10853
11576
|
return pkg.version ?? "0.0.0";
|
|
10854
11577
|
} catch {
|
|
@@ -10858,7 +11581,7 @@ function cliVersion() {
|
|
|
10858
11581
|
var program = new Command();
|
|
10859
11582
|
function validateRuntimeNodeVersion() {
|
|
10860
11583
|
try {
|
|
10861
|
-
const pkgPath =
|
|
11584
|
+
const pkgPath = path73.join(packageRoot(), "package.json");
|
|
10862
11585
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
10863
11586
|
const requiredRange = pkg.engines?.node;
|
|
10864
11587
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -10872,18 +11595,22 @@ function validateRuntimeNodeVersion() {
|
|
|
10872
11595
|
}
|
|
10873
11596
|
validateRuntimeNodeVersion();
|
|
10874
11597
|
program.name("haus").description("Haus AI workflow CLI").version(cliVersion());
|
|
10875
|
-
program.command("scan").
|
|
10876
|
-
|
|
11598
|
+
program.command("scan").description(
|
|
11599
|
+
"Scan the repo for roles, dependencies, and package manager; writes context-map.json"
|
|
11600
|
+
).option("--json").action(runScan);
|
|
11601
|
+
program.command("recommend").description("Score catalog items against the scanned context and write recommendation.json").option("--json").option(
|
|
10877
11602
|
"--include <ids...>",
|
|
10878
11603
|
"Force opt-in catalog items into the recommendation as manual selections (space- or comma-separated ids)"
|
|
10879
11604
|
).action(runRecommend);
|
|
10880
|
-
program.command("setup-project").option("--json").option(
|
|
11605
|
+
program.command("setup-project").description("Fast full setup: scan, recommend, and optionally apply Claude files").option("--json").option(
|
|
10881
11606
|
"--force",
|
|
10882
11607
|
"Write recommendation.json/haus.lock.json even when zero catalog items matched at all"
|
|
10883
11608
|
).action(runSetupProject);
|
|
10884
|
-
program.command("doctor").
|
|
11609
|
+
program.command("doctor").description(
|
|
11610
|
+
"Validate project setup: hooks contract, managed files, cache freshness, CLI version"
|
|
11611
|
+
).option("--hooks", "Verify .claude/settings.json matches the hook contract").action(runDoctor);
|
|
10885
11612
|
program.command("fetch-refs").description("Fetch and cache llms.txt content from catalog item references").option("--all", "Fetch for all catalog items (default)").option("--id <id>", "Fetch for a single catalog item by id").option("--json", "Emit machine-readable JSON summary to stdout").action(runFetchRefs);
|
|
10886
|
-
program.command("apply").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option(
|
|
11613
|
+
program.command("apply").description("Write recommended catalog items and core Claude files to .claude/").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option(
|
|
10887
11614
|
"--ids <ids...>",
|
|
10888
11615
|
"Install exactly these recommended item ids non-interactively (skill backend; space- or comma-separated)"
|
|
10889
11616
|
).option(
|
|
@@ -10896,33 +11623,33 @@ program.command("apply").option("--dry-run").option("--write").option("--select"
|
|
|
10896
11623
|
"--prune",
|
|
10897
11624
|
"Remove lock-tracked catalog items no longer in the current recommendation (hash-gated, backed up first)"
|
|
10898
11625
|
).action(runApply);
|
|
10899
|
-
program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
|
|
10900
|
-
var backups = program.command("backups");
|
|
11626
|
+
program.command("undo").description("Remove haus-managed project files; preserves user-owned .claude/ content").option("-y, --yes", "Skip confirmation").action(runUndo);
|
|
11627
|
+
var backups = program.command("backups").description("List, restore, or prune backups under .haus-workflow/backups/");
|
|
10901
11628
|
backups.command("list").description("List backups under .haus-workflow/backups/").action(() => runBackups("list"));
|
|
10902
11629
|
backups.command("restore <id>").description("Restore a specific backup by id (see `haus backups list`)").option("-y, --yes", "Skip confirmation").action((id, opts) => runBackups("restore", { id, yes: opts.yes }));
|
|
10903
11630
|
backups.command("prune").description("Delete old backups by age and/or count (requires --older-than or --keep)").option("--older-than <days>", "Remove backups older than N days").option("--keep <n>", "Keep only the N most recent backups").option("-y, --yes", "Skip confirmation").action(
|
|
10904
11631
|
(opts) => runBackups("prune", { olderThan: opts.olderThan, keep: opts.keep, yes: opts.yes })
|
|
10905
11632
|
);
|
|
10906
|
-
program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
|
|
10907
|
-
program.command("init").option("--json").action(runInit);
|
|
11633
|
+
program.command("explain-recommendation").description("Print a per-item breakdown of the current recommendation, with skip reasons").option("--json").action(runExplainRecommendation);
|
|
11634
|
+
program.command("init").description("First-run setup; delegates to setup-project if .haus-workflow/ does not exist").option("--json").action(runInit);
|
|
10908
11635
|
program.command("clone").description("Clone a single git repository by URL").argument("<url>", "Git URL to clone").argument("[dir]", "Target directory (default: repo name derived from the URL)").option("--dry-run", "Print what would happen without cloning").action(
|
|
10909
11636
|
(url, dir, opts) => runClone(url, { dir, dryRun: opts.dryRun })
|
|
10910
11637
|
);
|
|
10911
|
-
program.command("refresh").action(runRefresh);
|
|
10912
|
-
program.command("catalog-audit").action(runCatalogAudit);
|
|
10913
|
-
program.command("validate-catalog").argument("[manifest]").action(runValidateCatalog);
|
|
10914
|
-
program.command("update").option("--check").option("--fast", "With --check, skip per-item content hashing (cheap count+ref only)").option("--from-hook", "Emit JSON for the Claude Code SessionStart hook (silent if up to date)").action(runUpdate);
|
|
11638
|
+
program.command("refresh").description("Re-scan the project, refresh recommendations, and update scan artifacts").action(runRefresh);
|
|
11639
|
+
program.command("catalog-audit").description("Check the bundled catalog manifest for unsupported or forbidden stacks").action(runCatalogAudit);
|
|
11640
|
+
program.command("validate-catalog").description("Validate a catalog manifest at an explicit path").argument("[manifest]").action(runValidateCatalog);
|
|
11641
|
+
program.command("update").description("Sync the remote catalog cache, refresh the lockfile, and check for CLI updates").option("--check").option("--fast", "With --check, skip per-item content hashing (cheap count+ref only)").option("--from-hook", "Emit JSON for the Claude Code SessionStart hook (silent if up to date)").action(runUpdate);
|
|
10915
11642
|
program.command("ci-gate").description(
|
|
10916
11643
|
"Aggregate `doctor` + `decisions check` + `update --check --fast` into one pass/fail gate for CI. Uses the fast/cheap update-check tier (no per-item content hashing) to keep CI runs quick \u2014 run `haus update --check` directly for the fully-hashed tier. Each constituent command remains independently runnable with its own unchanged exit-code contract."
|
|
10917
11644
|
).option("--json", "Emit machine-readable {doctor, decisions, update, ok}").action((opts) => runCiGate(opts));
|
|
10918
|
-
program.command("install").option("--dry-run").option("--force").option("--check", "Exit non-zero if any HAUS-MANAGED file is out of date").option("--postinstall", "Run by the npm postinstall hook; prints a plain-language change notice").action(runInstall);
|
|
11645
|
+
program.command("install").description("Seed ~/.claude/ with Haus-managed skills, agents, and hook settings").option("--dry-run").option("--force").option("--check", "Exit non-zero if any HAUS-MANAGED file is out of date").option("--postinstall", "Run by the npm postinstall hook; prints a plain-language change notice").action(runInstall);
|
|
10919
11646
|
program.command("scaffold [ids...]").description(
|
|
10920
11647
|
"Copy catalog config items (ESLint, Prettier) into the project root. Pass item IDs to scaffold specific items, or omit to scaffold all available config items."
|
|
10921
11648
|
).option("--force", "Overwrite existing files").option("--dry-run", "Preview what would be written without changing anything").option("--root <path>", "Project root (defaults to cwd)", process.cwd()).action(runScaffold);
|
|
10922
|
-
program.command("uninstall").option("--force").option("-y, --yes", "Skip confirmation").action(runUninstallCommand);
|
|
10923
|
-
var guard = program.command("guard");
|
|
10924
|
-
guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
|
|
10925
|
-
guard.command("bash").option("--from-hook").action((opts) => runGuard("bash", opts));
|
|
11649
|
+
program.command("uninstall").description("Remove Haus-managed skills, agents, and hook entries from ~/.claude/").option("--force").option("-y, --yes", "Skip confirmation").action(runUninstallCommand);
|
|
11650
|
+
var guard = program.command("guard").description("PreToolUse hook guards: block dangerous bash commands and sensitive file access");
|
|
11651
|
+
guard.command("file-access").description("Block reads/edits of sensitive paths (run by the Claude Code hook)").option("--from-hook").action((opts) => runGuard("file-access", opts));
|
|
11652
|
+
guard.command("bash").description("Block dangerous shell commands (run by the Claude Code hook)").option("--from-hook").action((opts) => runGuard("bash", opts));
|
|
10926
11653
|
function registerDecisionsCommand(name) {
|
|
10927
11654
|
const cmd = program.command(name).description("Architecture decision records (ADR) gate and drafts");
|
|
10928
11655
|
cmd.command("check").option("--staged", "Check staged changes only").option("--range <range>", "Git revision range (e.g. origin/main..HEAD)").action((opts) => runDecisions("check", void 0, opts));
|
|
@@ -10935,7 +11662,7 @@ function registerDecisionsCommand(name) {
|
|
|
10935
11662
|
}
|
|
10936
11663
|
registerDecisionsCommand("decisions");
|
|
10937
11664
|
registerDecisionsCommand("adr");
|
|
10938
|
-
var workspace = program.command("workspace");
|
|
11665
|
+
var workspace = program.command("workspace").description("Multi-repo workspace commands: discover, setup, link-context, worktree, doctor");
|
|
10939
11666
|
workspace.command("init").option("-y, --yes", "Skip confirmation when haus.workspace.yaml already exists").action((opts) => runWorkspace("init", opts));
|
|
10940
11667
|
workspace.command("discover").description("Auto-find member repos and write/merge haus.workspace.yaml").option("--write", "Persist haus.workspace.yaml (default previews only)").option("--json", "Output the discovered repos and proposed config as JSON").option("--max-depth <n>", "Max directory depth to traverse (default 3)").option("--client <name>", "Set the workspace client name").action((opts) => runWorkspace("discover", opts));
|
|
10941
11668
|
workspace.command("scan").description("Aggregate a cross-repo summary from a fast scan of each repo").option("--json", "Output the written artifact paths as JSON").action((opts) => runWorkspace("scan", opts));
|