@haus-tech/haus-workflow 4.4.0 → 4.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 +36 -0
- package/README.md +13 -15
- package/dist/cli.js +832 -415
- package/library/catalog/manifest.json +6 -15
- package/library/global/skills/haus-workflow/SKILL.md +162 -185
- package/library/global/skills/haus-workflow/references/clone.md +191 -0
- package/library/global/skills/haus-workflow/references/new.md +15 -0
- package/library/global/skills/request-assess-impact/SKILL.md +24 -0
- package/library/global/skills/request-score/SKILL.md +80 -34
- package/library/global/skills/request-score/references/poangskala.md +195 -42
- package/library/global/skills/request-sync/SKILL.md +169 -57
- package/package.json +2 -1
- package/library/global/skills/haus-workflow/references/cloneandsetup.md +0 -172
package/dist/cli.js
CHANGED
|
@@ -404,6 +404,9 @@ var init_exec = __esm({
|
|
|
404
404
|
});
|
|
405
405
|
|
|
406
406
|
// src/catalog/remote-catalog/github-auth.ts
|
|
407
|
+
function ghProbeDisabled() {
|
|
408
|
+
return process.env["HAUS_NO_GH_AUTH_PROBE"] === "1";
|
|
409
|
+
}
|
|
407
410
|
async function defaultGhTokenResolver() {
|
|
408
411
|
try {
|
|
409
412
|
const result = await runCommand("gh", ["auth", "token"], {
|
|
@@ -425,8 +428,9 @@ async function resolveGithubAuthToken(env = process.env) {
|
|
|
425
428
|
if (haus) return haus;
|
|
426
429
|
const github = env["GITHUB_TOKEN"]?.trim();
|
|
427
430
|
if (github) return github;
|
|
428
|
-
|
|
429
|
-
return
|
|
431
|
+
if (ghResolverOverride) return ghResolverOverride();
|
|
432
|
+
if (ghProbeDisabled()) return null;
|
|
433
|
+
return defaultGhTokenResolver();
|
|
430
434
|
})().then((token) => {
|
|
431
435
|
cachedToken = token;
|
|
432
436
|
inFlight = void 0;
|
|
@@ -1932,7 +1936,7 @@ var init_sync = __esm({
|
|
|
1932
1936
|
// src/cli.ts
|
|
1933
1937
|
init_offline();
|
|
1934
1938
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
1935
|
-
import
|
|
1939
|
+
import path81 from "node:path";
|
|
1936
1940
|
import { Command } from "commander";
|
|
1937
1941
|
|
|
1938
1942
|
// src/commands/apply.ts
|
|
@@ -2338,6 +2342,11 @@ var ASK_PATHS = [
|
|
|
2338
2342
|
{ pattern: ".env.*", tools: ["Edit", "Write"] },
|
|
2339
2343
|
{ pattern: "storage/logs/**", tools: ["Edit", "Write"] },
|
|
2340
2344
|
{ pattern: "exports/**", tools: ["Edit", "Write"] },
|
|
2345
|
+
// Dump-shaped formats: reading one is the leak, so Read is in the tool set. `*.sql`
|
|
2346
|
+
// belongs with them — a dump can carry an entire customer table — and was the one
|
|
2347
|
+
// missing from every permission tier (it appeared only in SENSITIVE_PATH_REGEXES,
|
|
2348
|
+
// which gates the bulk scanner, not on-demand file access). See ADR-0071.
|
|
2349
|
+
{ pattern: "*.sql", tools: ["Read", "Edit", "Write"] },
|
|
2341
2350
|
{ pattern: "*.dump", tools: ["Read", "Edit", "Write"] },
|
|
2342
2351
|
{ pattern: "*.backup", tools: ["Read", "Edit", "Write"] },
|
|
2343
2352
|
{ pattern: "*.bak", tools: ["Read", "Edit", "Write"] },
|
|
@@ -2376,6 +2385,34 @@ function isRecord(v) {
|
|
|
2376
2385
|
return typeof v === "object" && v !== null;
|
|
2377
2386
|
}
|
|
2378
2387
|
|
|
2388
|
+
// src/utils/versions.ts
|
|
2389
|
+
import semver from "semver";
|
|
2390
|
+
function normalizeVersion(version) {
|
|
2391
|
+
return semver.valid(semver.coerce(version));
|
|
2392
|
+
}
|
|
2393
|
+
function satisfiesVersion(version, range) {
|
|
2394
|
+
const normalized = normalizeVersion(version);
|
|
2395
|
+
if (!normalized) return false;
|
|
2396
|
+
return semver.satisfies(normalized, range, { includePrerelease: true });
|
|
2397
|
+
}
|
|
2398
|
+
function compareVersions(a, b) {
|
|
2399
|
+
const normalizedA = normalizeVersion(a);
|
|
2400
|
+
const normalizedB = normalizeVersion(b);
|
|
2401
|
+
if (!normalizedA || !normalizedB) {
|
|
2402
|
+
throw new Error(`Cannot compare invalid versions: ${a} vs ${b}`);
|
|
2403
|
+
}
|
|
2404
|
+
return semver.compare(normalizedA, normalizedB);
|
|
2405
|
+
}
|
|
2406
|
+
function resolveMajorVersion(range) {
|
|
2407
|
+
if (!range) return null;
|
|
2408
|
+
try {
|
|
2409
|
+
const min = semver.minVersion(range);
|
|
2410
|
+
return min ? min.major : null;
|
|
2411
|
+
} catch {
|
|
2412
|
+
return null;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2379
2416
|
// src/scanner/detection.ts
|
|
2380
2417
|
var UNSUPPORTED_MARKERS = {
|
|
2381
2418
|
"Cargo.toml": "rust",
|
|
@@ -2402,7 +2439,7 @@ function describeUnknownDetection(unsupportedSignals, isLinkedWorktree) {
|
|
|
2402
2439
|
function blocked(rel) {
|
|
2403
2440
|
return SENSITIVE_PATH_REGEXES.some((x) => x.test(rel));
|
|
2404
2441
|
}
|
|
2405
|
-
function dependencyVersionMap(pkg, composer) {
|
|
2442
|
+
function dependencyVersionMap(pkg, composer, memberPkgs = []) {
|
|
2406
2443
|
const versions = /* @__PURE__ */ new Map();
|
|
2407
2444
|
const pushObj = (obj) => {
|
|
2408
2445
|
if (!isRecord(obj)) return;
|
|
@@ -2416,8 +2453,34 @@ function dependencyVersionMap(pkg, composer) {
|
|
|
2416
2453
|
pushObj(pkg?.devDependencies);
|
|
2417
2454
|
pushObj(composer?.require);
|
|
2418
2455
|
pushObj(composer?.["require-dev"]);
|
|
2456
|
+
const rootDeclared = new Set(versions.keys());
|
|
2457
|
+
for (const member of memberPkgs) {
|
|
2458
|
+
pushObj(member?.dependencies);
|
|
2459
|
+
pushObj(member?.devDependencies);
|
|
2460
|
+
}
|
|
2461
|
+
for (const [name, majors] of memberMajorsByName(memberPkgs)) {
|
|
2462
|
+
if (rootDeclared.has(name)) continue;
|
|
2463
|
+
if (majors.size > 1) versions.set(name, "");
|
|
2464
|
+
}
|
|
2419
2465
|
return versions;
|
|
2420
2466
|
}
|
|
2467
|
+
function memberMajorsByName(memberPkgs) {
|
|
2468
|
+
const majors = /* @__PURE__ */ new Map();
|
|
2469
|
+
for (const member of memberPkgs) {
|
|
2470
|
+
for (const section of [member?.dependencies, member?.devDependencies]) {
|
|
2471
|
+
if (!isRecord(section)) continue;
|
|
2472
|
+
for (const [name, raw] of Object.entries(section)) {
|
|
2473
|
+
if (typeof raw !== "string") continue;
|
|
2474
|
+
const major = resolveMajorVersion(raw);
|
|
2475
|
+
if (major == null) continue;
|
|
2476
|
+
const seen = majors.get(name) ?? /* @__PURE__ */ new Set();
|
|
2477
|
+
seen.add(major);
|
|
2478
|
+
majors.set(name, seen);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
return majors;
|
|
2483
|
+
}
|
|
2421
2484
|
function finalizeRoles(registryRoles, deps, files) {
|
|
2422
2485
|
const roles = new Set(registryRoles);
|
|
2423
2486
|
const hasWpConfig = files.some((f) => f.endsWith("wp-config.php"));
|
|
@@ -2514,36 +2577,6 @@ function isSupersededByPlugin(lockId, activePinIds) {
|
|
|
2514
2577
|
// src/scanner/detect-package-manager.ts
|
|
2515
2578
|
import path13 from "node:path";
|
|
2516
2579
|
import fs9 from "fs-extra";
|
|
2517
|
-
|
|
2518
|
-
// src/utils/versions.ts
|
|
2519
|
-
import semver from "semver";
|
|
2520
|
-
function normalizeVersion(version) {
|
|
2521
|
-
return semver.valid(semver.coerce(version));
|
|
2522
|
-
}
|
|
2523
|
-
function satisfiesVersion(version, range) {
|
|
2524
|
-
const normalized = normalizeVersion(version);
|
|
2525
|
-
if (!normalized) return false;
|
|
2526
|
-
return semver.satisfies(normalized, range, { includePrerelease: true });
|
|
2527
|
-
}
|
|
2528
|
-
function compareVersions(a, b) {
|
|
2529
|
-
const normalizedA = normalizeVersion(a);
|
|
2530
|
-
const normalizedB = normalizeVersion(b);
|
|
2531
|
-
if (!normalizedA || !normalizedB) {
|
|
2532
|
-
throw new Error(`Cannot compare invalid versions: ${a} vs ${b}`);
|
|
2533
|
-
}
|
|
2534
|
-
return semver.compare(normalizedA, normalizedB);
|
|
2535
|
-
}
|
|
2536
|
-
function resolveMajorVersion(range) {
|
|
2537
|
-
if (!range) return null;
|
|
2538
|
-
try {
|
|
2539
|
-
const min = semver.minVersion(range);
|
|
2540
|
-
return min ? min.major : null;
|
|
2541
|
-
} catch {
|
|
2542
|
-
return null;
|
|
2543
|
-
}
|
|
2544
|
-
}
|
|
2545
|
-
|
|
2546
|
-
// src/scanner/detect-package-manager.ts
|
|
2547
2580
|
function detectPackageManager(root, packageManagerField) {
|
|
2548
2581
|
const field = String(packageManagerField ?? "").trim();
|
|
2549
2582
|
if (field.startsWith("yarn@")) {
|
|
@@ -3160,8 +3193,8 @@ function buildDenyRules() {
|
|
|
3160
3193
|
for (const command of DENY_COMMANDS) {
|
|
3161
3194
|
rules.push(`Bash(${command}:*)`);
|
|
3162
3195
|
}
|
|
3163
|
-
for (const
|
|
3164
|
-
const pattern = DENY_DIRS.has(
|
|
3196
|
+
for (const path82 of DENY_PATHS) {
|
|
3197
|
+
const pattern = DENY_DIRS.has(path82) ? `${path82}/**` : path82;
|
|
3165
3198
|
for (const tool of FILE_TOOLS) {
|
|
3166
3199
|
rules.push(`${tool}(${pattern})`);
|
|
3167
3200
|
}
|
|
@@ -3680,6 +3713,7 @@ var IMPORT_CONTENT = `@.haus-workflow/WORKFLOW.md
|
|
|
3680
3713
|
@docs/decisions/README.md`;
|
|
3681
3714
|
function buildImportBlock() {
|
|
3682
3715
|
return `${BLOCK_BEGIN}
|
|
3716
|
+
|
|
3683
3717
|
${IMPORT_CONTENT}
|
|
3684
3718
|
${BLOCK_END}`;
|
|
3685
3719
|
}
|
|
@@ -4748,7 +4782,8 @@ var GITIGNORED_ARTIFACT_PATHS = [
|
|
|
4748
4782
|
".haus-workflow/recommendation.json",
|
|
4749
4783
|
".haus-workflow/sources-report.json",
|
|
4750
4784
|
".haus-workflow/deep-context.json",
|
|
4751
|
-
".haus-workflow/backups/"
|
|
4785
|
+
".haus-workflow/backups/",
|
|
4786
|
+
".haus-workflow/llms-cache/"
|
|
4752
4787
|
];
|
|
4753
4788
|
function findLineMarker3(content2, marker, from = 0) {
|
|
4754
4789
|
let idx = content2.indexOf(marker, from);
|
|
@@ -4793,7 +4828,7 @@ function buildBlockFor(existing, beginMarker, endMarker, comment, patterns) {
|
|
|
4793
4828
|
if (needed.length === 0) return null;
|
|
4794
4829
|
return [beginMarker, comment, ...needed, endMarker].join("\n");
|
|
4795
4830
|
}
|
|
4796
|
-
var ARTIFACT_BLOCK_COMMENT = "# haus machine-local artifacts \u2014 must never be committed (see ADR-0025, ADR-0058)";
|
|
4831
|
+
var ARTIFACT_BLOCK_COMMENT = "# haus machine-local artifacts \u2014 must never be committed (see ADR-0025, ADR-0058, ADR-0009)";
|
|
4797
4832
|
function injectBlockFor(existing, beginMarker, endMarker, comment, patterns) {
|
|
4798
4833
|
const block = buildBlockFor(existing, beginMarker, endMarker, comment, patterns);
|
|
4799
4834
|
const range = findBlockRangeFor(existing, beginMarker, endMarker);
|
|
@@ -6454,8 +6489,8 @@ async function runBundleImport(file, options = {}) {
|
|
|
6454
6489
|
}
|
|
6455
6490
|
|
|
6456
6491
|
// src/commands/doctor.ts
|
|
6457
|
-
import
|
|
6458
|
-
import
|
|
6492
|
+
import path46 from "node:path";
|
|
6493
|
+
import fs38 from "fs-extra";
|
|
6459
6494
|
init_manifest_format();
|
|
6460
6495
|
|
|
6461
6496
|
// src/claude/former-install-orphans.ts
|
|
@@ -6490,35 +6525,161 @@ async function findOrphanedFormerInstalls(root, items, lockRows) {
|
|
|
6490
6525
|
return orphans;
|
|
6491
6526
|
}
|
|
6492
6527
|
|
|
6528
|
+
// src/claude/project-exclusions.ts
|
|
6529
|
+
init_project_pin();
|
|
6530
|
+
init_fs();
|
|
6531
|
+
init_logger();
|
|
6532
|
+
init_paths();
|
|
6533
|
+
|
|
6534
|
+
// src/claude/project-intent.ts
|
|
6535
|
+
init_project_pin();
|
|
6536
|
+
init_fs();
|
|
6537
|
+
init_logger();
|
|
6538
|
+
init_paths();
|
|
6539
|
+
import fs30 from "fs-extra";
|
|
6540
|
+
function normalizeIdList(value) {
|
|
6541
|
+
if (!Array.isArray(value)) return [];
|
|
6542
|
+
return [
|
|
6543
|
+
...new Set(
|
|
6544
|
+
value.filter((entry) => typeof entry === "string" && entry.length > 0)
|
|
6545
|
+
)
|
|
6546
|
+
];
|
|
6547
|
+
}
|
|
6548
|
+
function normalizeSignalList(value) {
|
|
6549
|
+
return [...new Set(normalizeIdList(value).map((entry) => entry.toLowerCase()))];
|
|
6550
|
+
}
|
|
6551
|
+
async function readProjectConfigObject(root) {
|
|
6552
|
+
try {
|
|
6553
|
+
const data = await readJson(hausPath(root, PROJECT_CONFIG_FILE));
|
|
6554
|
+
if (data == null || typeof data !== "object" || Array.isArray(data)) return null;
|
|
6555
|
+
return data;
|
|
6556
|
+
} catch (err) {
|
|
6557
|
+
if (err.code === "ENOENT") return null;
|
|
6558
|
+
warn(
|
|
6559
|
+
`${PROJECT_CONFIG_REL} could not be read (${err instanceof Error ? err.message : String(err)}) \u2014 treating project intent as unset`
|
|
6560
|
+
);
|
|
6561
|
+
return null;
|
|
6562
|
+
}
|
|
6563
|
+
}
|
|
6564
|
+
async function updateProjectConfig(root, mutate) {
|
|
6565
|
+
const file = hausPath(root, PROJECT_CONFIG_FILE);
|
|
6566
|
+
let existing = {};
|
|
6567
|
+
if (await fs30.pathExists(file)) {
|
|
6568
|
+
try {
|
|
6569
|
+
const parsed = JSON.parse(await fs30.readFile(file, "utf8"));
|
|
6570
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
6571
|
+
existing = { ...parsed };
|
|
6572
|
+
} else {
|
|
6573
|
+
warn(`${PROJECT_CONFIG_REL} is not a JSON object \u2014 rewriting with haus-managed keys only`);
|
|
6574
|
+
}
|
|
6575
|
+
} catch (err) {
|
|
6576
|
+
warn(
|
|
6577
|
+
`${PROJECT_CONFIG_REL} could not be parsed (${err instanceof Error ? err.message : String(err)}) \u2014 rewriting with haus-managed keys only`
|
|
6578
|
+
);
|
|
6579
|
+
}
|
|
6580
|
+
}
|
|
6581
|
+
await writeJson(file, mutate(existing));
|
|
6582
|
+
}
|
|
6583
|
+
async function readProjectIncludedIds(root) {
|
|
6584
|
+
const data = await readProjectConfigObject(root);
|
|
6585
|
+
return data == null ? [] : normalizeIdList(data.includedIds);
|
|
6586
|
+
}
|
|
6587
|
+
async function addProjectIncludedIds(root, ids) {
|
|
6588
|
+
const toAdd = normalizeIdList(ids);
|
|
6589
|
+
if (toAdd.length === 0) return;
|
|
6590
|
+
await updateProjectConfig(root, (existing) => ({
|
|
6591
|
+
...existing,
|
|
6592
|
+
includedIds: [.../* @__PURE__ */ new Set([...normalizeIdList(existing.includedIds), ...toAdd])].sort()
|
|
6593
|
+
}));
|
|
6594
|
+
}
|
|
6595
|
+
function withdrawIncludedIds(existing, ids) {
|
|
6596
|
+
const drop = new Set(ids);
|
|
6597
|
+
return normalizeIdList(existing.includedIds).filter((id) => !drop.has(id));
|
|
6598
|
+
}
|
|
6599
|
+
async function readDeclaredContext(root) {
|
|
6600
|
+
const data = await readProjectConfigObject(root);
|
|
6601
|
+
if (data == null) return { roles: [], stacks: [] };
|
|
6602
|
+
return {
|
|
6603
|
+
roles: normalizeSignalList(data.declaredRoles),
|
|
6604
|
+
stacks: normalizeSignalList(data.declaredStacks)
|
|
6605
|
+
};
|
|
6606
|
+
}
|
|
6607
|
+
|
|
6608
|
+
// src/claude/project-exclusions.ts
|
|
6609
|
+
function normalizeExcludedIds(value) {
|
|
6610
|
+
if (!Array.isArray(value)) return [];
|
|
6611
|
+
return [
|
|
6612
|
+
...new Set(
|
|
6613
|
+
value.filter((entry) => typeof entry === "string" && entry.length > 0)
|
|
6614
|
+
)
|
|
6615
|
+
];
|
|
6616
|
+
}
|
|
6617
|
+
async function readProjectExcludedIds(root) {
|
|
6618
|
+
try {
|
|
6619
|
+
const data = await readJson(hausPath(root, PROJECT_CONFIG_FILE));
|
|
6620
|
+
if (!data) return [];
|
|
6621
|
+
return normalizeExcludedIds(data.excludedIds);
|
|
6622
|
+
} catch (err) {
|
|
6623
|
+
if (err.code === "ENOENT") return [];
|
|
6624
|
+
warn(
|
|
6625
|
+
`${PROJECT_CONFIG_REL} could not be read for excludedIds (${err instanceof Error ? err.message : String(err)}) \u2014 treating as none`
|
|
6626
|
+
);
|
|
6627
|
+
return [];
|
|
6628
|
+
}
|
|
6629
|
+
}
|
|
6630
|
+
function isItemExcluded(item, excluded) {
|
|
6631
|
+
if (excluded.has(item.id)) return true;
|
|
6632
|
+
return normalizeFormerIds(item.formerIds).some((formerId) => excluded.has(formerId));
|
|
6633
|
+
}
|
|
6634
|
+
async function addProjectExcludedIds(root, ids) {
|
|
6635
|
+
const toAdd = [...new Set(ids.filter((id) => id.length > 0))];
|
|
6636
|
+
if (toAdd.length === 0) return;
|
|
6637
|
+
await updateProjectConfig(root, (existing) => {
|
|
6638
|
+
const next = { ...existing };
|
|
6639
|
+
next.excludedIds = [
|
|
6640
|
+
.../* @__PURE__ */ new Set([...normalizeExcludedIds(existing.excludedIds), ...toAdd])
|
|
6641
|
+
].sort();
|
|
6642
|
+
const remainingIncludes = withdrawIncludedIds(existing, toAdd);
|
|
6643
|
+
if (remainingIncludes.length > 0) next.includedIds = remainingIncludes;
|
|
6644
|
+
else delete next.includedIds;
|
|
6645
|
+
return next;
|
|
6646
|
+
});
|
|
6647
|
+
}
|
|
6648
|
+
|
|
6493
6649
|
// src/claude/skill-reference-check.ts
|
|
6494
6650
|
init_paths();
|
|
6495
6651
|
import path32 from "node:path";
|
|
6496
6652
|
import fg4 from "fast-glob";
|
|
6497
|
-
import
|
|
6653
|
+
import fs31 from "fs-extra";
|
|
6498
6654
|
var REFERENCE_PATTERN = /\b([a-z][a-z0-9]*(?:-[a-z0-9]+)+):([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\b(?!:\/\/)/g;
|
|
6499
6655
|
function stripFencedCodeBlocks(text) {
|
|
6500
6656
|
return text.replace(/```[\s\S]*?```/g, (block) => block.replace(/[^\n]/g, " "));
|
|
6501
6657
|
}
|
|
6658
|
+
var INLINE_CODE_SPAN = /(`+)(?!`)[\s\S]*?\1/g;
|
|
6659
|
+
function inlineCodeSpanRanges(line2) {
|
|
6660
|
+
const ranges = [];
|
|
6661
|
+
for (const match of line2.matchAll(INLINE_CODE_SPAN)) {
|
|
6662
|
+
ranges.push([match.index, match.index + match[0].length]);
|
|
6663
|
+
}
|
|
6664
|
+
return ranges;
|
|
6665
|
+
}
|
|
6502
6666
|
async function findUnresolvedSkillReferences(root, options = {}) {
|
|
6503
6667
|
const resolvableNamespaces = new Set(options.pluginNamespaces ?? []);
|
|
6504
|
-
const
|
|
6505
|
-
if (!await fs30.pathExists(skillsRoot)) return [];
|
|
6506
|
-
const skillDirs = (await fs30.readdir(skillsRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory() && entry.name !== "shared").map((entry) => entry.name);
|
|
6507
|
-
const installedSkillNames = new Set(skillDirs);
|
|
6508
|
-
const files = await fg4("*/**/*.md", { cwd: skillsRoot, onlyFiles: true, dot: true });
|
|
6668
|
+
const installedSkillNames = await readInstalledSkillNames(root);
|
|
6509
6669
|
const issues = [];
|
|
6510
|
-
for (const
|
|
6511
|
-
if (rel.startsWith("shared/")) continue;
|
|
6512
|
-
const abs = path32.join(skillsRoot, rel);
|
|
6513
|
-
const raw = await fs30.readFile(abs, "utf8");
|
|
6514
|
-
const lines = stripFencedCodeBlocks(raw).split("\n");
|
|
6670
|
+
for (const { file, lines } of await readScannedMarkdown(root)) {
|
|
6515
6671
|
lines.forEach((line2, idx) => {
|
|
6672
|
+
const codeSpans = inlineCodeSpanRanges(line2);
|
|
6516
6673
|
for (const match of line2.matchAll(REFERENCE_PATTERN)) {
|
|
6517
6674
|
const [reference, namespace, bareName] = match;
|
|
6518
6675
|
if (!reference || !bareName) continue;
|
|
6519
6676
|
if (namespace && resolvableNamespaces.has(namespace)) continue;
|
|
6677
|
+
const inCodeSpan = codeSpans.some(
|
|
6678
|
+
([start, end]) => match.index >= start && match.index < end
|
|
6679
|
+
);
|
|
6680
|
+
if (inCodeSpan && !installedSkillNames.has(bareName)) continue;
|
|
6520
6681
|
issues.push({
|
|
6521
|
-
file
|
|
6682
|
+
file,
|
|
6522
6683
|
line: idx + 1,
|
|
6523
6684
|
reference,
|
|
6524
6685
|
bareName,
|
|
@@ -6529,17 +6690,83 @@ async function findUnresolvedSkillReferences(root, options = {}) {
|
|
|
6529
6690
|
}
|
|
6530
6691
|
return issues;
|
|
6531
6692
|
}
|
|
6693
|
+
var BOLD_AGENT_PATTERN = /\*\*([a-z][a-z0-9]*(?:-[a-z0-9]+)+)\*\*/g;
|
|
6694
|
+
var AGENT_WORD = /\bagents?\b/i;
|
|
6695
|
+
function isAgentContext(line2, nearestHeading) {
|
|
6696
|
+
const outsideBold = line2.replace(BOLD_AGENT_PATTERN, " ");
|
|
6697
|
+
return AGENT_WORD.test(outsideBold) || AGENT_WORD.test(nearestHeading);
|
|
6698
|
+
}
|
|
6699
|
+
async function findUnresolvedAgentReferences(root, options = {}) {
|
|
6700
|
+
const installedAgentNames = await readInstalledAgentNames(root);
|
|
6701
|
+
const catalogAgentNames = new Set(options.catalogAgentNames ?? []);
|
|
6702
|
+
const issues = [];
|
|
6703
|
+
for (const { file, lines } of await readScannedMarkdown(root)) {
|
|
6704
|
+
let nearestHeading = "";
|
|
6705
|
+
lines.forEach((line2, idx) => {
|
|
6706
|
+
if (/^#{1,6}\s/.test(line2)) nearestHeading = line2.replace(/^#{1,6}\s*/, "");
|
|
6707
|
+
if (!isAgentContext(line2, nearestHeading)) return;
|
|
6708
|
+
const codeSpans = inlineCodeSpanRanges(line2);
|
|
6709
|
+
for (const match of line2.matchAll(BOLD_AGENT_PATTERN)) {
|
|
6710
|
+
const agentName = match[1];
|
|
6711
|
+
if (!agentName || installedAgentNames.has(agentName)) continue;
|
|
6712
|
+
const inCodeSpan = codeSpans.some(
|
|
6713
|
+
([start, end]) => match.index >= start && match.index < end
|
|
6714
|
+
);
|
|
6715
|
+
if (inCodeSpan) continue;
|
|
6716
|
+
issues.push({
|
|
6717
|
+
file,
|
|
6718
|
+
line: idx + 1,
|
|
6719
|
+
agentName,
|
|
6720
|
+
inCatalog: catalogAgentNames.has(agentName)
|
|
6721
|
+
});
|
|
6722
|
+
}
|
|
6723
|
+
});
|
|
6724
|
+
}
|
|
6725
|
+
return issues;
|
|
6726
|
+
}
|
|
6727
|
+
async function readInstalledSkillNames(root) {
|
|
6728
|
+
const skillsRoot = claudePath(root, "skills");
|
|
6729
|
+
if (!await fs31.pathExists(skillsRoot)) return /* @__PURE__ */ new Set();
|
|
6730
|
+
const entries = await fs31.readdir(skillsRoot, { withFileTypes: true });
|
|
6731
|
+
return new Set(entries.filter((e) => e.isDirectory() && e.name !== "shared").map((e) => e.name));
|
|
6732
|
+
}
|
|
6733
|
+
async function readInstalledAgentNames(root) {
|
|
6734
|
+
const agentsRoot = claudePath(root, "agents");
|
|
6735
|
+
if (!await fs31.pathExists(agentsRoot)) return /* @__PURE__ */ new Set();
|
|
6736
|
+
const entries = await fs31.readdir(agentsRoot, { withFileTypes: true });
|
|
6737
|
+
return new Set(
|
|
6738
|
+
entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name.replace(/\.md$/, ""))
|
|
6739
|
+
);
|
|
6740
|
+
}
|
|
6741
|
+
async function readScannedMarkdown(root) {
|
|
6742
|
+
const scanned = [];
|
|
6743
|
+
for (const dir of ["skills", "agents", "rules"]) {
|
|
6744
|
+
const dirRoot = claudePath(root, dir);
|
|
6745
|
+
if (!await fs31.pathExists(dirRoot)) continue;
|
|
6746
|
+
const pattern = dir === "skills" ? "*/**/*.md" : "**/*.md";
|
|
6747
|
+
const files = await fg4(pattern, { cwd: dirRoot, onlyFiles: true, dot: true });
|
|
6748
|
+
for (const rel of files) {
|
|
6749
|
+
if (dir === "skills" && rel.startsWith("shared/")) continue;
|
|
6750
|
+
const raw = await fs31.readFile(path32.join(dirRoot, rel), "utf8");
|
|
6751
|
+
scanned.push({
|
|
6752
|
+
file: path32.posix.join(".claude", dir, rel.split(path32.sep).join("/")),
|
|
6753
|
+
lines: stripFencedCodeBlocks(raw).split("\n")
|
|
6754
|
+
});
|
|
6755
|
+
}
|
|
6756
|
+
}
|
|
6757
|
+
return scanned;
|
|
6758
|
+
}
|
|
6532
6759
|
|
|
6533
6760
|
// src/decisions/doctor.ts
|
|
6534
6761
|
init_fs();
|
|
6535
6762
|
import path37 from "node:path";
|
|
6536
|
-
import
|
|
6763
|
+
import fs35 from "fs-extra";
|
|
6537
6764
|
|
|
6538
6765
|
// src/decisions/check.ts
|
|
6539
6766
|
init_fs();
|
|
6540
6767
|
init_paths();
|
|
6541
6768
|
import path36 from "node:path";
|
|
6542
|
-
import
|
|
6769
|
+
import fs34 from "fs-extra";
|
|
6543
6770
|
|
|
6544
6771
|
// src/decisions/dep-bump.ts
|
|
6545
6772
|
var DEP_SECTIONS = [
|
|
@@ -6600,6 +6827,32 @@ function isDependencyBumpOnly(beforeText, afterText) {
|
|
|
6600
6827
|
init_exec();
|
|
6601
6828
|
init_fs();
|
|
6602
6829
|
import path33 from "node:path";
|
|
6830
|
+
var NUL = String.fromCharCode(0);
|
|
6831
|
+
function parseNumstat(output) {
|
|
6832
|
+
const fields2 = output.split(NUL);
|
|
6833
|
+
const linesByFile = {};
|
|
6834
|
+
let linesAdded = 0;
|
|
6835
|
+
let linesRemoved = 0;
|
|
6836
|
+
for (let i = 0; i < fields2.length; i++) {
|
|
6837
|
+
const field = fields2[i];
|
|
6838
|
+
if (!field) continue;
|
|
6839
|
+
const parts = field.split(" ");
|
|
6840
|
+
if (parts.length < 3) continue;
|
|
6841
|
+
const added = parts[0] === "-" ? 0 : Number(parts[0]);
|
|
6842
|
+
const removed = parts[1] === "-" ? 0 : Number(parts[1]);
|
|
6843
|
+
if (Number.isNaN(added) || Number.isNaN(removed)) continue;
|
|
6844
|
+
let file = parts[2];
|
|
6845
|
+
if (file === "") {
|
|
6846
|
+
file = fields2[i + 2];
|
|
6847
|
+
i += 2;
|
|
6848
|
+
}
|
|
6849
|
+
if (!file) continue;
|
|
6850
|
+
linesAdded += added;
|
|
6851
|
+
linesRemoved += removed;
|
|
6852
|
+
linesByFile[file] = (linesByFile[file] ?? 0) + added + removed;
|
|
6853
|
+
}
|
|
6854
|
+
return { linesByFile, linesAdded, linesRemoved };
|
|
6855
|
+
}
|
|
6603
6856
|
function isPackageJsonPath(file) {
|
|
6604
6857
|
return file === "package.json" || file.endsWith("/package.json");
|
|
6605
6858
|
}
|
|
@@ -6667,18 +6920,9 @@ async function collectDiffStats(root, opts) {
|
|
|
6667
6920
|
const gitArgs = opts.staged ? ["diff", "--cached", "--name-only"] : ["diff", "--name-only", opts.range ?? "HEAD"];
|
|
6668
6921
|
const names = await runGit(gitArgs, { cwd: root });
|
|
6669
6922
|
const files = names.stdout.split("\n").map((f) => f.trim()).filter(Boolean);
|
|
6670
|
-
const numstatArgs = opts.staged ? ["diff", "--cached", "--numstat"] : ["diff", "--numstat", opts.range ?? "HEAD"];
|
|
6923
|
+
const numstatArgs = opts.staged ? ["diff", "--cached", "--numstat", "-z"] : ["diff", "--numstat", "-z", opts.range ?? "HEAD"];
|
|
6671
6924
|
const numstat = await runGit(numstatArgs, { cwd: root });
|
|
6672
|
-
|
|
6673
|
-
let linesRemoved = 0;
|
|
6674
|
-
for (const line2 of numstat.stdout.split("\n")) {
|
|
6675
|
-
const parts = line2.trim().split(/\s+/);
|
|
6676
|
-
if (parts.length < 3) continue;
|
|
6677
|
-
const added = parts[0] === "-" ? 0 : Number(parts[0]);
|
|
6678
|
-
const removed = parts[1] === "-" ? 0 : Number(parts[1]);
|
|
6679
|
-
if (!Number.isNaN(added)) linesAdded += added;
|
|
6680
|
-
if (!Number.isNaN(removed)) linesRemoved += removed;
|
|
6681
|
-
}
|
|
6925
|
+
const { linesByFile, linesAdded, linesRemoved } = parseNumstat(numstat.stdout);
|
|
6682
6926
|
const packageJsonPairs = {};
|
|
6683
6927
|
for (const file of files) {
|
|
6684
6928
|
if (!isPackageJsonPath(file)) continue;
|
|
@@ -6688,6 +6932,7 @@ async function collectDiffStats(root, opts) {
|
|
|
6688
6932
|
files,
|
|
6689
6933
|
linesAdded,
|
|
6690
6934
|
linesRemoved,
|
|
6935
|
+
linesByFile,
|
|
6691
6936
|
...Object.keys(packageJsonPairs).length > 0 ? { packageJsonPairs } : {}
|
|
6692
6937
|
};
|
|
6693
6938
|
}
|
|
@@ -6732,12 +6977,12 @@ function firstMatchingRegex(filePath, patterns) {
|
|
|
6732
6977
|
|
|
6733
6978
|
// src/decisions/paths.ts
|
|
6734
6979
|
import path34 from "node:path";
|
|
6735
|
-
import
|
|
6980
|
+
import fs32 from "fs-extra";
|
|
6736
6981
|
async function resolveDecisionsDir(root) {
|
|
6737
6982
|
const primary = path34.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
6738
|
-
if (await
|
|
6983
|
+
if (await fs32.pathExists(primary)) return primary;
|
|
6739
6984
|
const legacy = path34.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
6740
|
-
if (await
|
|
6985
|
+
if (await fs32.pathExists(legacy)) return legacy;
|
|
6741
6986
|
return primary;
|
|
6742
6987
|
}
|
|
6743
6988
|
function relativeDecisionPath(decisionsDir, root) {
|
|
@@ -6747,7 +6992,7 @@ function relativeDecisionPath(decisionsDir, root) {
|
|
|
6747
6992
|
// src/decisions/validate.ts
|
|
6748
6993
|
init_fs();
|
|
6749
6994
|
import path35 from "node:path";
|
|
6750
|
-
import
|
|
6995
|
+
import fs33 from "fs-extra";
|
|
6751
6996
|
function validateDecisionContent(content2) {
|
|
6752
6997
|
const errors = [];
|
|
6753
6998
|
for (const section of DECISIONS_TRIGGERS.requiredSections) {
|
|
@@ -6777,7 +7022,7 @@ function decisionNumberFromFile(name) {
|
|
|
6777
7022
|
}
|
|
6778
7023
|
async function validateIndexSync(decisionsDir, decisionFiles) {
|
|
6779
7024
|
const readmePath = path35.join(decisionsDir, "README.md");
|
|
6780
|
-
if (!await
|
|
7025
|
+
if (!await fs33.pathExists(readmePath)) {
|
|
6781
7026
|
return ["docs/decisions/README.md is missing"];
|
|
6782
7027
|
}
|
|
6783
7028
|
const readme = await readText(readmePath) ?? "";
|
|
@@ -6833,6 +7078,11 @@ function countTopLevelDirs(files) {
|
|
|
6833
7078
|
function firstMatchingGlob(file, globs) {
|
|
6834
7079
|
return globs.find((g) => matchesPathGlob(file, g));
|
|
6835
7080
|
}
|
|
7081
|
+
function relevantLineCount(stats, relevant) {
|
|
7082
|
+
const byFile = stats.linesByFile;
|
|
7083
|
+
if (byFile == null) return stats.linesAdded + stats.linesRemoved;
|
|
7084
|
+
return relevant.reduce((sum, file) => sum + (byFile[file] ?? 0), 0);
|
|
7085
|
+
}
|
|
6836
7086
|
function explainDecisionTrigger(stats) {
|
|
6837
7087
|
let relevant = stats.files.filter((f) => !isExempt(f));
|
|
6838
7088
|
if (relevant.length === 0) return [];
|
|
@@ -6850,7 +7100,7 @@ function explainDecisionTrigger(stats) {
|
|
|
6850
7100
|
return [`matched pathRegex: ${regex}`];
|
|
6851
7101
|
}
|
|
6852
7102
|
}
|
|
6853
|
-
const totalLines = stats
|
|
7103
|
+
const totalLines = relevantLineCount(stats, relevant);
|
|
6854
7104
|
const dirs = countTopLevelDirs(relevant);
|
|
6855
7105
|
if (relevant.length >= DECISIONS_TRIGGERS.minFilesChanged && dirs >= 2) {
|
|
6856
7106
|
return [`size threshold: ${relevant.length} files across ${dirs} top-level dirs`];
|
|
@@ -6949,8 +7199,8 @@ ${opts.commitMessages ?? ""}`;
|
|
|
6949
7199
|
changedFiles: stats.files
|
|
6950
7200
|
});
|
|
6951
7201
|
if (satisfactionErrors.length > 0 && decisionsRel !== legacyRel) {
|
|
6952
|
-
const legacyExists = await
|
|
6953
|
-
if (legacyExists && !await
|
|
7202
|
+
const legacyExists = await fs34.pathExists(path36.join(root, legacyRel));
|
|
7203
|
+
if (legacyExists && !await fs34.pathExists(decisionsDir)) {
|
|
6954
7204
|
satisfactionErrors = await checkSatisfaction({
|
|
6955
7205
|
root,
|
|
6956
7206
|
decisionsRel: legacyRel,
|
|
@@ -6972,8 +7222,8 @@ ${opts.commitMessages ?? ""}`;
|
|
|
6972
7222
|
return { triggered: true, satisfied: false, reasons, securityTriggered };
|
|
6973
7223
|
}
|
|
6974
7224
|
async function listDecisionNumbers(decisionsDir) {
|
|
6975
|
-
if (!await
|
|
6976
|
-
const entries = await
|
|
7225
|
+
if (!await fs34.pathExists(decisionsDir)) return [];
|
|
7226
|
+
const entries = await fs34.readdir(decisionsDir);
|
|
6977
7227
|
return entries.map((name) => decisionNumberFromFile(name)).filter((n) => n != null).map((n) => Number(n));
|
|
6978
7228
|
}
|
|
6979
7229
|
|
|
@@ -6982,8 +7232,8 @@ async function auditDecisionsLayout(root) {
|
|
|
6982
7232
|
const findings = [];
|
|
6983
7233
|
const primary = path37.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
6984
7234
|
const legacy = path37.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
6985
|
-
const hasPrimary = await
|
|
6986
|
-
const hasLegacy = await
|
|
7235
|
+
const hasPrimary = await fs35.pathExists(primary);
|
|
7236
|
+
const hasLegacy = await fs35.pathExists(legacy);
|
|
6987
7237
|
if (!hasPrimary && hasLegacy) {
|
|
6988
7238
|
findings.push({
|
|
6989
7239
|
level: "advisory",
|
|
@@ -6997,7 +7247,7 @@ async function auditDecisionsLayout(root) {
|
|
|
6997
7247
|
const decisionsDir = await resolveDecisionsDir(root);
|
|
6998
7248
|
const rel = relativeDecisionPath(decisionsDir, root);
|
|
6999
7249
|
const readmePath = path37.join(decisionsDir, "README.md");
|
|
7000
|
-
if (!await
|
|
7250
|
+
if (!await fs35.pathExists(readmePath)) {
|
|
7001
7251
|
findings.push({
|
|
7002
7252
|
level: "advisory",
|
|
7003
7253
|
line: `- DECISIONS: missing ${rel}/README.md index`,
|
|
@@ -7007,7 +7257,7 @@ async function auditDecisionsLayout(root) {
|
|
|
7007
7257
|
return findings;
|
|
7008
7258
|
}
|
|
7009
7259
|
const readme = await readText(readmePath);
|
|
7010
|
-
const entries = (await
|
|
7260
|
+
const entries = (await fs35.readdir(decisionsDir)).filter(
|
|
7011
7261
|
(f) => f.endsWith(".md") && f !== "README.md"
|
|
7012
7262
|
);
|
|
7013
7263
|
for (const file of entries) {
|
|
@@ -7056,7 +7306,7 @@ init_fs();
|
|
|
7056
7306
|
init_logger();
|
|
7057
7307
|
init_paths();
|
|
7058
7308
|
import path38 from "node:path";
|
|
7059
|
-
import
|
|
7309
|
+
import fs36 from "fs-extra";
|
|
7060
7310
|
function manifestPathIn(configDir) {
|
|
7061
7311
|
return path38.join(configDir, "haus", "install-manifest.json");
|
|
7062
7312
|
}
|
|
@@ -7086,9 +7336,9 @@ async function pruneVacatedDirs(root, vacatedFiles) {
|
|
|
7086
7336
|
async function destinationWritable(dir) {
|
|
7087
7337
|
let candidate = path38.resolve(dir);
|
|
7088
7338
|
for (; ; ) {
|
|
7089
|
-
if (await
|
|
7339
|
+
if (await fs36.pathExists(candidate)) {
|
|
7090
7340
|
try {
|
|
7091
|
-
await
|
|
7341
|
+
await fs36.access(candidate, fs36.constants.W_OK);
|
|
7092
7342
|
return true;
|
|
7093
7343
|
} catch (err) {
|
|
7094
7344
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -7120,7 +7370,7 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
7120
7370
|
if (!legacy || !Array.isArray(legacy.files)) {
|
|
7121
7371
|
return { ...base, reason: "no install manifest under ~/.claude" };
|
|
7122
7372
|
}
|
|
7123
|
-
if (await
|
|
7373
|
+
if (await fs36.pathExists(manifestPathIn(to))) {
|
|
7124
7374
|
return { ...base, reason: "an install manifest already exists in the resolved config dir" };
|
|
7125
7375
|
}
|
|
7126
7376
|
const writable = await destinationWritable(to);
|
|
@@ -7143,14 +7393,14 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
7143
7393
|
continue;
|
|
7144
7394
|
}
|
|
7145
7395
|
const newDest = path38.join(to, rel);
|
|
7146
|
-
if (!await
|
|
7396
|
+
if (!await fs36.pathExists(entry.destPath)) {
|
|
7147
7397
|
rewritten.push({ ...entry, destPath: newDest });
|
|
7148
7398
|
continue;
|
|
7149
7399
|
}
|
|
7150
|
-
if (await
|
|
7400
|
+
if (await fs36.pathExists(newDest)) {
|
|
7151
7401
|
if (await sameContent(entry.destPath, newDest)) {
|
|
7152
7402
|
if (!dryRun) {
|
|
7153
|
-
await
|
|
7403
|
+
await fs36.remove(entry.destPath);
|
|
7154
7404
|
vacated.push(entry.destPath);
|
|
7155
7405
|
}
|
|
7156
7406
|
moved.push(newDest);
|
|
@@ -7167,7 +7417,7 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
7167
7417
|
continue;
|
|
7168
7418
|
}
|
|
7169
7419
|
try {
|
|
7170
|
-
await
|
|
7420
|
+
await fs36.move(entry.destPath, newDest);
|
|
7171
7421
|
vacated.push(entry.destPath);
|
|
7172
7422
|
moved.push(newDest);
|
|
7173
7423
|
rewritten.push({ ...entry, destPath: newDest });
|
|
@@ -7192,7 +7442,7 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
7192
7442
|
}
|
|
7193
7443
|
try {
|
|
7194
7444
|
await writeJson(manifestPathIn(to), { ...legacy, files: rewritten });
|
|
7195
|
-
await
|
|
7445
|
+
await fs36.remove(legacyManifestPath);
|
|
7196
7446
|
vacated.push(legacyManifestPath);
|
|
7197
7447
|
} catch (err) {
|
|
7198
7448
|
return {
|
|
@@ -7237,8 +7487,8 @@ function printConfigDirMigration(result) {
|
|
|
7237
7487
|
}
|
|
7238
7488
|
|
|
7239
7489
|
// src/scanner/read-context.ts
|
|
7240
|
-
import
|
|
7241
|
-
import
|
|
7490
|
+
import path45 from "node:path";
|
|
7491
|
+
import fs37 from "fs-extra";
|
|
7242
7492
|
|
|
7243
7493
|
// src/commands/workspace/config.ts
|
|
7244
7494
|
init_fs();
|
|
@@ -7368,7 +7618,7 @@ async function readMembers(rootInfo) {
|
|
|
7368
7618
|
}
|
|
7369
7619
|
|
|
7370
7620
|
// src/scanner/scan-project.ts
|
|
7371
|
-
import
|
|
7621
|
+
import path44 from "node:path";
|
|
7372
7622
|
init_fs();
|
|
7373
7623
|
|
|
7374
7624
|
// src/utils/git-root.ts
|
|
@@ -7432,7 +7682,7 @@ init_paths();
|
|
|
7432
7682
|
// src/scanner/detection-registry.ts
|
|
7433
7683
|
var dep = (value) => ({ kind: "dep", value });
|
|
7434
7684
|
var depPrefix = (value) => ({ kind: "depPrefix", value });
|
|
7435
|
-
var
|
|
7685
|
+
var depSuffix = (value) => ({ kind: "depSuffix", value });
|
|
7436
7686
|
var depMajor = (value, major) => ({
|
|
7437
7687
|
kind: "depMajor",
|
|
7438
7688
|
value,
|
|
@@ -7459,6 +7709,7 @@ var fileStartsWith = (value) => ({
|
|
|
7459
7709
|
mode: "startsWith"
|
|
7460
7710
|
});
|
|
7461
7711
|
var content = (value) => ({ kind: "content", value });
|
|
7712
|
+
var composerType = (value) => ({ kind: "composerType", value });
|
|
7462
7713
|
var STACK_BUCKETS = [
|
|
7463
7714
|
"backend",
|
|
7464
7715
|
"frontend",
|
|
@@ -7475,12 +7726,17 @@ function matchSignal(sig, ctx) {
|
|
|
7475
7726
|
case "depPrefix":
|
|
7476
7727
|
for (const d of ctx.deps) if (d.startsWith(sig.value)) return true;
|
|
7477
7728
|
return false;
|
|
7729
|
+
case "depSuffix":
|
|
7730
|
+
for (const d of ctx.deps) if (d.endsWith(sig.value)) return true;
|
|
7731
|
+
return false;
|
|
7478
7732
|
case "depAbsent":
|
|
7479
7733
|
return !ctx.deps.has(sig.value);
|
|
7480
7734
|
case "depMajor":
|
|
7481
7735
|
return ctx.deps.has(sig.value) && resolveMajorVersion(ctx.depVersions.get(sig.value) ?? "") === sig.major;
|
|
7482
7736
|
case "content":
|
|
7483
7737
|
return ctx.contentBlob.includes(sig.value);
|
|
7738
|
+
case "composerType":
|
|
7739
|
+
return sig.value.length > 0 && ctx.composerType === sig.value;
|
|
7484
7740
|
case "file":
|
|
7485
7741
|
return ctx.files.some((f) => {
|
|
7486
7742
|
switch (sig.mode) {
|
|
@@ -7497,9 +7753,12 @@ function matchSignal(sig, ctx) {
|
|
|
7497
7753
|
}
|
|
7498
7754
|
}
|
|
7499
7755
|
function matchRule(rule, ctx) {
|
|
7500
|
-
|
|
7501
|
-
if (rule.
|
|
7502
|
-
|
|
7756
|
+
const groups = [];
|
|
7757
|
+
if (rule.all) groups.push(rule.all.every((s) => matchSignal(s, ctx)));
|
|
7758
|
+
if (rule.any) groups.push(rule.any.some((s) => matchSignal(s, ctx)));
|
|
7759
|
+
if (rule.none) groups.push(!rule.none.some((s) => matchSignal(s, ctx)));
|
|
7760
|
+
if (groups.length === 0) return false;
|
|
7761
|
+
return groups.every(Boolean);
|
|
7503
7762
|
}
|
|
7504
7763
|
var ROLE_RULES = [
|
|
7505
7764
|
// Workspace/meta-repo root — same bridge signal `readMembers()`/
|
|
@@ -7557,8 +7816,33 @@ var STACK_RULES = [
|
|
|
7557
7816
|
},
|
|
7558
7817
|
{ stack: ["tooling", "pm2"], any: [dep("pm2"), fileIncludes("ecosystem.config")] },
|
|
7559
7818
|
{ stack: ["tooling", "sentry"], any: [depPrefix("@sentry/")] },
|
|
7560
|
-
|
|
7561
|
-
|
|
7819
|
+
// "Missing" means no evidence of Prettier at all — not merely no direct dependency.
|
|
7820
|
+
// A repo configuring Prettier through a shared config package (`@scope/prettier-config`)
|
|
7821
|
+
// has no `prettier` key of its own, so `depAbsent('prettier')` reported it as missing
|
|
7822
|
+
// even with a `.prettierrc` sitting right there.
|
|
7823
|
+
{
|
|
7824
|
+
stack: ["tooling", "missing-prettier"],
|
|
7825
|
+
none: [
|
|
7826
|
+
dep("prettier"),
|
|
7827
|
+
depSuffix("prettier-config"),
|
|
7828
|
+
fileIncludes(".prettierrc"),
|
|
7829
|
+
fileIncludes("prettier.config.")
|
|
7830
|
+
]
|
|
7831
|
+
},
|
|
7832
|
+
// Same shape as missing-prettier above (#374). ESLint has TWO shared-config naming
|
|
7833
|
+
// conventions rather than one — `@scope/eslint-config` and the older
|
|
7834
|
+
// `eslint-config-<name>` — so both are evidence, as is a flat (`eslint.config.*`) or
|
|
7835
|
+
// legacy (`.eslintrc*`) config file.
|
|
7836
|
+
{
|
|
7837
|
+
stack: ["tooling", "missing-eslint"],
|
|
7838
|
+
none: [
|
|
7839
|
+
dep("eslint"),
|
|
7840
|
+
depSuffix("eslint-config"),
|
|
7841
|
+
depPrefix("eslint-config-"),
|
|
7842
|
+
fileIncludes("eslint.config."),
|
|
7843
|
+
fileIncludes(".eslintrc")
|
|
7844
|
+
]
|
|
7845
|
+
},
|
|
7562
7846
|
{
|
|
7563
7847
|
stack: ["tooling", "stripe"],
|
|
7564
7848
|
any: [dep("stripe"), dep("@stripe/stripe-js"), dep("@stripe/react-stripe-js")]
|
|
@@ -7590,7 +7874,16 @@ var STACK_RULES = [
|
|
|
7590
7874
|
{ stack: ["backend", "php"], any: [fileEndsWith("composer.json")] },
|
|
7591
7875
|
{ stack: ["backend", "laravel"], any: [dep("laravel/framework")] },
|
|
7592
7876
|
{ stack: ["backend", "laravel"], any: [fileIncludes("app/Providers/"), fileIncludes("routes/")] },
|
|
7593
|
-
|
|
7877
|
+
// `wp-config.php` and `roots/wordpress` both describe a WordPress *site* — the thing
|
|
7878
|
+
// that runs the install. A WordPress *plugin* repo has neither; it ships source that
|
|
7879
|
+
// gets dropped into someone else's `wp-content/plugins/`, and declares that with
|
|
7880
|
+
// composer's `"type": "wordpress-plugin"`. Both shapes need WordPress context, so
|
|
7881
|
+
// both earn the stack. Site-only roles (`wordpress-*-site`) are unaffected — those
|
|
7882
|
+
// still come from the site signals alone (`finalizeRoles`, src/scanner/detection.ts).
|
|
7883
|
+
{
|
|
7884
|
+
stack: ["backend", "wordpress"],
|
|
7885
|
+
any: [fileEndsWith("wp-config.php"), dep("roots/wordpress"), composerType("wordpress-plugin")]
|
|
7886
|
+
},
|
|
7594
7887
|
{
|
|
7595
7888
|
stack: ["backend", "elementor"],
|
|
7596
7889
|
any: [
|
|
@@ -7683,6 +7976,76 @@ async function buildContentBlob(root, files) {
|
|
|
7683
7976
|
return parts.join("\n");
|
|
7684
7977
|
}
|
|
7685
7978
|
|
|
7979
|
+
// src/scanner/workspace-packages.ts
|
|
7980
|
+
import path43 from "node:path";
|
|
7981
|
+
import fg5 from "fast-glob";
|
|
7982
|
+
import YAML2 from "yaml";
|
|
7983
|
+
init_fs();
|
|
7984
|
+
var PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
|
|
7985
|
+
var MAX_WORKSPACE_MEMBERS = 200;
|
|
7986
|
+
function isRepoRelativeGlob(glob) {
|
|
7987
|
+
if (glob.length === 0) return false;
|
|
7988
|
+
if (path43.isAbsolute(glob) || glob.startsWith("/")) return false;
|
|
7989
|
+
return !glob.split("/").includes("..");
|
|
7990
|
+
}
|
|
7991
|
+
function stringEntries(value) {
|
|
7992
|
+
if (!Array.isArray(value)) return [];
|
|
7993
|
+
return value.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
7994
|
+
}
|
|
7995
|
+
function workspaceGlobsFromPackageJson(pkg) {
|
|
7996
|
+
if (!isRecord(pkg)) return [];
|
|
7997
|
+
const field = pkg.workspaces;
|
|
7998
|
+
if (Array.isArray(field)) return stringEntries(field);
|
|
7999
|
+
if (isRecord(field)) return stringEntries(field.packages);
|
|
8000
|
+
return [];
|
|
8001
|
+
}
|
|
8002
|
+
function workspaceGlobsFromPnpmYaml(text) {
|
|
8003
|
+
try {
|
|
8004
|
+
const doc = YAML2.parse(text);
|
|
8005
|
+
if (!isRecord(doc)) return [];
|
|
8006
|
+
return stringEntries(doc.packages);
|
|
8007
|
+
} catch {
|
|
8008
|
+
return [];
|
|
8009
|
+
}
|
|
8010
|
+
}
|
|
8011
|
+
async function readWorkspaceGlobs(root, pkg) {
|
|
8012
|
+
const fromPkg = workspaceGlobsFromPackageJson(pkg);
|
|
8013
|
+
const pnpmText = await readText(path43.join(root, PNPM_WORKSPACE_FILE));
|
|
8014
|
+
const fromPnpm = pnpmText == null ? [] : workspaceGlobsFromPnpmYaml(pnpmText);
|
|
8015
|
+
return [.../* @__PURE__ */ new Set([...fromPkg, ...fromPnpm])];
|
|
8016
|
+
}
|
|
8017
|
+
async function collectWorkspaceMemberPackages(root, globs) {
|
|
8018
|
+
const safeGlobs = globs.filter(isRepoRelativeGlob);
|
|
8019
|
+
if (safeGlobs.length === 0) return [];
|
|
8020
|
+
const patterns = safeGlobs.map((g) => `${g.replace(/\/+$/, "")}/package.json`);
|
|
8021
|
+
let matches;
|
|
8022
|
+
try {
|
|
8023
|
+
matches = await fg5(patterns, {
|
|
8024
|
+
cwd: root,
|
|
8025
|
+
dot: false,
|
|
8026
|
+
onlyFiles: true,
|
|
8027
|
+
followSymbolicLinks: false,
|
|
8028
|
+
suppressErrors: true,
|
|
8029
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"]
|
|
8030
|
+
});
|
|
8031
|
+
} catch {
|
|
8032
|
+
return [];
|
|
8033
|
+
}
|
|
8034
|
+
const rootManifest = path43.resolve(root, "package.json");
|
|
8035
|
+
const contained = [
|
|
8036
|
+
...new Set(
|
|
8037
|
+
matches.map((rel) => resolveContainedPath(root, rel)).filter((abs) => abs != null && abs !== rootManifest)
|
|
8038
|
+
)
|
|
8039
|
+
].sort().slice(0, MAX_WORKSPACE_MEMBERS);
|
|
8040
|
+
const read = await mapWithConcurrency(contained, async (abs) => {
|
|
8041
|
+
const pkg = await readJson(abs);
|
|
8042
|
+
if (!isRecord(pkg)) return null;
|
|
8043
|
+
const rel = path43.relative(root, path43.dirname(abs));
|
|
8044
|
+
return { dir: rel.split(path43.sep).join("/"), pkg };
|
|
8045
|
+
});
|
|
8046
|
+
return read.filter((entry) => entry != null);
|
|
8047
|
+
}
|
|
8048
|
+
|
|
7686
8049
|
// src/scanner/write-sources-report.ts
|
|
7687
8050
|
init_manifest_format();
|
|
7688
8051
|
init_fs();
|
|
@@ -7745,6 +8108,20 @@ var SAFE_FILES = [
|
|
|
7745
8108
|
"next.config.*",
|
|
7746
8109
|
"tailwind.config.*",
|
|
7747
8110
|
"components.json",
|
|
8111
|
+
// Prettier config, in every shape Prettier itself accepts. Presence is what the
|
|
8112
|
+
// `missing-prettier` rule needs: a repo can configure Prettier entirely through a
|
|
8113
|
+
// shared config package, with no `prettier` dependency of its own to detect.
|
|
8114
|
+
".prettierrc*",
|
|
8115
|
+
"prettier.config.*",
|
|
8116
|
+
"**/.prettierrc*",
|
|
8117
|
+
"**/prettier.config.*",
|
|
8118
|
+
// ESLint config, flat and legacy, for the same reason (#374): a repo can configure
|
|
8119
|
+
// ESLint entirely through a shared config package with no `eslint` dependency of
|
|
8120
|
+
// its own, so presence of the config file is the only local evidence.
|
|
8121
|
+
"eslint.config.*",
|
|
8122
|
+
".eslintrc*",
|
|
8123
|
+
"**/eslint.config.*",
|
|
8124
|
+
"**/.eslintrc*",
|
|
7748
8125
|
"playwright.config.*",
|
|
7749
8126
|
"phpunit.xml",
|
|
7750
8127
|
"artisan",
|
|
@@ -7784,11 +8161,17 @@ var SAFE_FILES = [
|
|
|
7784
8161
|
"Gemfile"
|
|
7785
8162
|
];
|
|
7786
8163
|
async function scanProject(root, options = {}) {
|
|
7787
|
-
const pkg = await readJson(
|
|
7788
|
-
const composer = await readJson(
|
|
8164
|
+
const pkg = await readJson(path44.join(root, "package.json"));
|
|
8165
|
+
const composer = await readJson(path44.join(root, "composer.json"));
|
|
7789
8166
|
const files = await listFiles(root, SAFE_FILES);
|
|
7790
8167
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
7791
|
-
const
|
|
8168
|
+
const workspaceGlobs = await readWorkspaceGlobs(root, pkg ?? void 0);
|
|
8169
|
+
const memberPackages = await collectWorkspaceMemberPackages(root, workspaceGlobs);
|
|
8170
|
+
const depVersions = dependencyVersionMap(
|
|
8171
|
+
pkg,
|
|
8172
|
+
composer,
|
|
8173
|
+
memberPackages.map((m) => m.pkg)
|
|
8174
|
+
);
|
|
7792
8175
|
const deps = [...depVersions.keys()].sort();
|
|
7793
8176
|
const packageManager = detectPackageManager(root, String(pkg?.packageManager ?? ""));
|
|
7794
8177
|
const contentBlob = await buildContentBlob(root, safeFiles);
|
|
@@ -7796,7 +8179,8 @@ async function scanProject(root, options = {}) {
|
|
|
7796
8179
|
deps: new Set(deps),
|
|
7797
8180
|
depVersions,
|
|
7798
8181
|
files: safeFiles,
|
|
7799
|
-
contentBlob
|
|
8182
|
+
contentBlob,
|
|
8183
|
+
composerType: typeof composer?.type === "string" ? composer.type : ""
|
|
7800
8184
|
});
|
|
7801
8185
|
const roles = finalizeRoles(detection.roles, deps, safeFiles);
|
|
7802
8186
|
const stacks = detection.stacks;
|
|
@@ -7815,7 +8199,7 @@ async function scanProject(root, options = {}) {
|
|
|
7815
8199
|
crossRepoHints.push("Monorepo orchestration detected");
|
|
7816
8200
|
const unsupportedSignals = collectUnsupportedSignals(safeFiles);
|
|
7817
8201
|
const detectionStatus = computeDetectionStatus(roles, stacks, unsupportedSignals);
|
|
7818
|
-
const repoNameFallback = pkg?.name ? String(pkg.name) :
|
|
8202
|
+
const repoNameFallback = pkg?.name ? String(pkg.name) : path44.basename((await resolveRoots(root)).mainRoot);
|
|
7819
8203
|
const context = {
|
|
7820
8204
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7821
8205
|
repoName: repoNameFallback,
|
|
@@ -7839,15 +8223,15 @@ async function scanProject(root, options = {}) {
|
|
|
7839
8223
|
// src/scanner/read-context.ts
|
|
7840
8224
|
async function isCacheFresh(root, cachePath) {
|
|
7841
8225
|
const markerPaths = [
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
8226
|
+
path45.join(root, "package.json"),
|
|
8227
|
+
path45.join(root, "composer.json"),
|
|
8228
|
+
path45.join(root, WORKSPACE_FILE),
|
|
8229
|
+
path45.join(root, REPOS_MANIFEST_FILE)
|
|
7846
8230
|
];
|
|
7847
|
-
const cacheStat = await
|
|
8231
|
+
const cacheStat = await fs37.stat(cachePath);
|
|
7848
8232
|
for (const markerPath of markerPaths) {
|
|
7849
|
-
if (!await
|
|
7850
|
-
const markerStat = await
|
|
8233
|
+
if (!await fs37.pathExists(markerPath)) continue;
|
|
8234
|
+
const markerStat = await fs37.stat(markerPath);
|
|
7851
8235
|
if (markerStat.mtimeMs > cacheStat.mtimeMs) return false;
|
|
7852
8236
|
}
|
|
7853
8237
|
return true;
|
|
@@ -7962,7 +8346,7 @@ async function runDoctor(options) {
|
|
|
7962
8346
|
} else {
|
|
7963
8347
|
ok(`- HOOKS OK: ${hooks.message}`);
|
|
7964
8348
|
}
|
|
7965
|
-
const rootClaudeMdPath =
|
|
8349
|
+
const rootClaudeMdPath = path46.join(root, "CLAUDE.md");
|
|
7966
8350
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
7967
8351
|
if (!rootClaudeMdContent) {
|
|
7968
8352
|
flag(
|
|
@@ -8005,10 +8389,10 @@ async function runDoctor(options) {
|
|
|
8005
8389
|
ok("- CLAUDE.md: import block present");
|
|
8006
8390
|
}
|
|
8007
8391
|
for (const target of actualTargets) {
|
|
8008
|
-
const resolved =
|
|
8009
|
-
const relFromRoot =
|
|
8010
|
-
const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${
|
|
8011
|
-
if (escapesRoot || !await
|
|
8392
|
+
const resolved = path46.resolve(root, target);
|
|
8393
|
+
const relFromRoot = path46.relative(root, resolved);
|
|
8394
|
+
const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${path46.sep}`) || path46.isAbsolute(relFromRoot);
|
|
8395
|
+
if (escapesRoot || !await fs38.pathExists(resolved)) {
|
|
8012
8396
|
flag(
|
|
8013
8397
|
`- CLAUDE.md import: @${target} does not resolve (run \`haus apply --write\`)`,
|
|
8014
8398
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -8026,7 +8410,7 @@ async function runDoctor(options) {
|
|
|
8026
8410
|
);
|
|
8027
8411
|
}
|
|
8028
8412
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
8029
|
-
const workflowExists = await
|
|
8413
|
+
const workflowExists = await fs38.pathExists(workflowPath);
|
|
8030
8414
|
if (!workflowExists) {
|
|
8031
8415
|
flag(
|
|
8032
8416
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -8041,15 +8425,15 @@ async function runDoctor(options) {
|
|
|
8041
8425
|
} else {
|
|
8042
8426
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
8043
8427
|
const bodyContent = workflowContent.slice(firstLine.length + 1);
|
|
8044
|
-
const cachePath =
|
|
8045
|
-
const bundledPath =
|
|
8428
|
+
const cachePath = path46.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
8429
|
+
const bundledPath = path46.join(
|
|
8046
8430
|
packageRoot(),
|
|
8047
8431
|
"library",
|
|
8048
8432
|
"global",
|
|
8049
8433
|
"templates",
|
|
8050
8434
|
"agentic-workflow-standard.md"
|
|
8051
8435
|
);
|
|
8052
|
-
const templatePath = await
|
|
8436
|
+
const templatePath = await fs38.pathExists(cachePath) ? cachePath : bundledPath;
|
|
8053
8437
|
const templateContent = await readText(templatePath);
|
|
8054
8438
|
const verdict = checkManagedTamper(bodyContent, storedHashMatch?.[1], templateContent ?? null);
|
|
8055
8439
|
if (verdict.status === "tampered" || verdict.status === "legacy-diverged") {
|
|
@@ -8077,7 +8461,7 @@ async function runDoctor(options) {
|
|
|
8077
8461
|
}
|
|
8078
8462
|
}
|
|
8079
8463
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
8080
|
-
const workflowConfigExists = await
|
|
8464
|
+
const workflowConfigExists = await fs38.pathExists(workflowConfigPath);
|
|
8081
8465
|
if (!workflowConfigExists) {
|
|
8082
8466
|
flag(
|
|
8083
8467
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -8085,7 +8469,7 @@ async function runDoctor(options) {
|
|
|
8085
8469
|
"haus apply --write"
|
|
8086
8470
|
);
|
|
8087
8471
|
} else {
|
|
8088
|
-
const cfg = await
|
|
8472
|
+
const cfg = await fs38.readFile(workflowConfigPath, "utf8");
|
|
8089
8473
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
8090
8474
|
if (unfilled > 0) {
|
|
8091
8475
|
suggest(
|
|
@@ -8097,11 +8481,11 @@ async function runDoctor(options) {
|
|
|
8097
8481
|
ok("- .haus-workflow/workflow-config.md: OK (project-owned)");
|
|
8098
8482
|
}
|
|
8099
8483
|
}
|
|
8100
|
-
const docsSkillInstalled = await
|
|
8101
|
-
|
|
8484
|
+
const docsSkillInstalled = await fs38.pathExists(
|
|
8485
|
+
path46.join(root, ".claude", "skills", "writing-documentation", "SKILL.md")
|
|
8102
8486
|
);
|
|
8103
8487
|
if (docsSkillInstalled) {
|
|
8104
|
-
if (await
|
|
8488
|
+
if (await fs38.pathExists(path46.join(root, "docs", "SUMMARY.md"))) {
|
|
8105
8489
|
ok("- PROJECT DOCS: OK (docs/SUMMARY.md present)");
|
|
8106
8490
|
} else {
|
|
8107
8491
|
suggest(
|
|
@@ -8112,7 +8496,7 @@ async function runDoctor(options) {
|
|
|
8112
8496
|
}
|
|
8113
8497
|
}
|
|
8114
8498
|
if (workflowExists) {
|
|
8115
|
-
const prettierIgnore = await readText(
|
|
8499
|
+
const prettierIgnore = await readText(path46.join(root, ".prettierignore")) ?? "";
|
|
8116
8500
|
const lines = prettierIgnore.split("\n");
|
|
8117
8501
|
const missing = IGNORED_PATHS.filter((p) => !prettierIgnoreProtects(lines, p));
|
|
8118
8502
|
if (missing.length > 0) {
|
|
@@ -8228,8 +8612,53 @@ async function runDoctor(options) {
|
|
|
8228
8612
|
"find its catalog id and run `haus recommend --include <id>` then `haus apply --write`, or remove the reference"
|
|
8229
8613
|
);
|
|
8230
8614
|
}
|
|
8231
|
-
}
|
|
8232
|
-
|
|
8615
|
+
}
|
|
8616
|
+
const agentCatalogNames = catalogItems === null ? [] : catalogItems.filter((item) => item.type === "agent").map((item) => installTargetFor(item)).flatMap((target) => target.ok ? [path46.basename(target.relPath, ".md")] : []);
|
|
8617
|
+
const agentRefIssues = await findUnresolvedAgentReferences(root, {
|
|
8618
|
+
catalogAgentNames: agentCatalogNames
|
|
8619
|
+
});
|
|
8620
|
+
if (agentRefIssues.length > 0) {
|
|
8621
|
+
const describeAgent = (i) => `${i.file}:${i.line} "${i.agentName}"`;
|
|
8622
|
+
const installable = agentRefIssues.filter((i) => i.inCatalog);
|
|
8623
|
+
const unknown = agentRefIssues.filter((i) => !i.inCatalog);
|
|
8624
|
+
if (installable.length > 0) {
|
|
8625
|
+
const names = [...new Set(installable.map((i) => i.agentName))];
|
|
8626
|
+
flag(
|
|
8627
|
+
`- AGENT REFS: ${installable.length} reference(s) point at an agent the catalog can install but this project has not (${names.join(", ")}) (${installable.slice(0, 3).map(describeAgent).join("; ")}${installable.length > 3 ? "; \u2026" : ""})`,
|
|
8628
|
+
`${installable.length} rule/agent reference(s) send agents after ${names.join(", ")}, which is available but not installed`,
|
|
8629
|
+
"find its catalog id and run `haus recommend --include <id>` then `haus apply --write`"
|
|
8630
|
+
);
|
|
8631
|
+
}
|
|
8632
|
+
if (unknown.length > 0) {
|
|
8633
|
+
const names = [...new Set(unknown.map((i) => i.agentName))];
|
|
8634
|
+
suggest(
|
|
8635
|
+
`- AGENT REFS: ${unknown.length} reference(s) name an agent that exists neither here nor in the catalog (${names.join(", ")}) (${unknown.slice(0, 3).map(describeAgent).join("; ")}${unknown.length > 3 ? "; \u2026" : ""})`,
|
|
8636
|
+
`${unknown.length} rule/agent reference(s) send agents after ${names.join(", ")}, which was never shipped`,
|
|
8637
|
+
"correct or drop the reference upstream (catalog-owned prose), so agents stop being sent after a tool that does not exist"
|
|
8638
|
+
);
|
|
8639
|
+
}
|
|
8640
|
+
}
|
|
8641
|
+
if (skillRefIssues.length === 0 && agentRefIssues.length === 0) {
|
|
8642
|
+
ok("- SKILL REFS: OK (no unresolved cross-skill or agent references)");
|
|
8643
|
+
}
|
|
8644
|
+
const recommendedIds = new Set(
|
|
8645
|
+
(recommendation?.recommended ?? []).map((entry) => entry.id).filter((id) => typeof id === "string")
|
|
8646
|
+
);
|
|
8647
|
+
if (catalogItems !== null && recommendedIds.size > 0) {
|
|
8648
|
+
const catalogIds = new Set(catalogItems.map((item) => item.id));
|
|
8649
|
+
const excludedIds = new Set(await readProjectExcludedIds(root));
|
|
8650
|
+
const activePinIds = activePins.map((pin) => pin.id);
|
|
8651
|
+
const lockedIds = (await readJson(hausPath(root, "haus.lock.json")) ?? []).map((entry) => entry.id).filter((id) => typeof id === "string");
|
|
8652
|
+
const drifted = lockedIds.filter(
|
|
8653
|
+
(id) => catalogIds.has(id) && !recommendedIds.has(id) && !excludedIds.has(id) && !isSupersededByPlugin(id, activePinIds)
|
|
8654
|
+
);
|
|
8655
|
+
if (drifted.length > 0) {
|
|
8656
|
+
suggest(
|
|
8657
|
+
`- ELIGIBILITY DRIFT: ${drifted.length} installed item(s) are no longer recommended by the current signals (${drifted.slice(0, 3).join(", ")}${drifted.length > 3 ? ", \u2026" : ""})`,
|
|
8658
|
+
`${drifted.length} installed item(s) no longer match anything haus can detect in this repo \u2014 often a role that only \`deep-context.json\` knew about, which is not tracked in git`,
|
|
8659
|
+
"declare the role or stack in .haus-workflow/haus.config.json (declaredRoles / declaredStacks), re-run the docs pass to regenerate deep-context.json, or `haus remove <id>` if the item is genuinely no longer wanted"
|
|
8660
|
+
);
|
|
8661
|
+
}
|
|
8233
8662
|
}
|
|
8234
8663
|
if (claudeConfigDirIsRelocated()) {
|
|
8235
8664
|
const configMigration = await probeGlobalConfigDirMigration();
|
|
@@ -8308,10 +8737,10 @@ async function runDoctor(options) {
|
|
|
8308
8737
|
);
|
|
8309
8738
|
} else {
|
|
8310
8739
|
const lock = await readJson(hausPath(root, "haus.lock.json"));
|
|
8311
|
-
const
|
|
8740
|
+
const recommendedIds2 = new Set(
|
|
8312
8741
|
recommendation.recommended.map((r) => r.id).filter((id) => typeof id === "string")
|
|
8313
8742
|
);
|
|
8314
|
-
const orphaned = findOrphanedLockEntries(lock ?? [],
|
|
8743
|
+
const orphaned = findOrphanedLockEntries(lock ?? [], recommendedIds2).map((entry) => entry.id);
|
|
8315
8744
|
if (orphaned.length > 0) {
|
|
8316
8745
|
suggest(
|
|
8317
8746
|
`- CATALOG ITEMS: ${orphaned.length} installed item(s) no longer in the current recommendation (${orphaned.join(", ")})`,
|
|
@@ -8367,7 +8796,7 @@ async function runDoctor(options) {
|
|
|
8367
8796
|
let onDisk = false;
|
|
8368
8797
|
for (const rel of row.paths ?? []) {
|
|
8369
8798
|
const abs = resolveContainedPath(root, rel);
|
|
8370
|
-
if (abs !== null && await
|
|
8799
|
+
if (abs !== null && await fs38.pathExists(abs)) {
|
|
8371
8800
|
onDisk = true;
|
|
8372
8801
|
break;
|
|
8373
8802
|
}
|
|
@@ -8383,7 +8812,7 @@ async function runDoctor(options) {
|
|
|
8383
8812
|
);
|
|
8384
8813
|
}
|
|
8385
8814
|
}
|
|
8386
|
-
const pkgJson = await readJson(
|
|
8815
|
+
const pkgJson = await readJson(path46.join(packageRoot(), "package.json"));
|
|
8387
8816
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
8388
8817
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
8389
8818
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -8434,15 +8863,15 @@ async function runDoctor(options) {
|
|
|
8434
8863
|
}
|
|
8435
8864
|
|
|
8436
8865
|
// src/commands/update.ts
|
|
8437
|
-
import
|
|
8866
|
+
import path49 from "node:path";
|
|
8438
8867
|
init_github_rate_limit();
|
|
8439
8868
|
|
|
8440
8869
|
// src/claude/refresh-project.ts
|
|
8441
|
-
import
|
|
8870
|
+
import fs39 from "fs-extra";
|
|
8442
8871
|
init_fs();
|
|
8443
8872
|
init_paths();
|
|
8444
8873
|
async function isHausProject(root) {
|
|
8445
|
-
if (await
|
|
8874
|
+
if (await fs39.pathExists(hausPath(root, "recommendation.json"))) return true;
|
|
8446
8875
|
const result = await readJsonDetailed(claudePath(root, "settings.json"));
|
|
8447
8876
|
if (result.status === "ok") return result.value._haus != null;
|
|
8448
8877
|
return false;
|
|
@@ -8458,8 +8887,8 @@ async function refreshProjectApply(root) {
|
|
|
8458
8887
|
|
|
8459
8888
|
// src/install/apply.ts
|
|
8460
8889
|
import crypto2 from "node:crypto";
|
|
8461
|
-
import
|
|
8462
|
-
import
|
|
8890
|
+
import path47 from "node:path";
|
|
8891
|
+
import fs40 from "fs-extra";
|
|
8463
8892
|
init_fs();
|
|
8464
8893
|
init_logger();
|
|
8465
8894
|
init_paths();
|
|
@@ -8548,64 +8977,64 @@ function hashContent(content2) {
|
|
|
8548
8977
|
}
|
|
8549
8978
|
function destFileMissing(destPath) {
|
|
8550
8979
|
try {
|
|
8551
|
-
return !
|
|
8980
|
+
return !fs40.statSync(destPath).isFile();
|
|
8552
8981
|
} catch {
|
|
8553
8982
|
return true;
|
|
8554
8983
|
}
|
|
8555
8984
|
}
|
|
8556
8985
|
function sourceVersion() {
|
|
8557
8986
|
try {
|
|
8558
|
-
const pkgPath =
|
|
8559
|
-
const pkg = JSON.parse(
|
|
8987
|
+
const pkgPath = path47.join(packageRoot(), "package.json");
|
|
8988
|
+
const pkg = JSON.parse(fs40.readFileSync(pkgPath, "utf8"));
|
|
8560
8989
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
8561
8990
|
} catch {
|
|
8562
8991
|
return "haus@0.0.0";
|
|
8563
8992
|
}
|
|
8564
8993
|
}
|
|
8565
8994
|
function globalSrcDir() {
|
|
8566
|
-
return
|
|
8995
|
+
return path47.join(packageRoot(), "library", "global");
|
|
8567
8996
|
}
|
|
8568
8997
|
function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
8569
|
-
const skillDir =
|
|
8998
|
+
const skillDir = path47.join(skillsDir, skillName);
|
|
8570
8999
|
const entries = [];
|
|
8571
|
-
for (const rel of
|
|
9000
|
+
for (const rel of fs40.readdirSync(skillDir, { recursive: true })) {
|
|
8572
9001
|
if (rel === "SKILL.md") continue;
|
|
8573
|
-
const abs =
|
|
8574
|
-
if (!
|
|
8575
|
-
const posixRel = rel.split(
|
|
9002
|
+
const abs = path47.join(skillDir, rel);
|
|
9003
|
+
if (!fs40.statSync(abs).isFile()) continue;
|
|
9004
|
+
const posixRel = rel.split(path47.sep).join("/");
|
|
8576
9005
|
entries.push({
|
|
8577
9006
|
stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
|
|
8578
|
-
srcRelPath:
|
|
8579
|
-
destPath:
|
|
9007
|
+
srcRelPath: path47.join("library", "global", "skills", skillName, rel),
|
|
9008
|
+
destPath: path47.join(claudeDir, "skills", skillName, rel)
|
|
8580
9009
|
});
|
|
8581
9010
|
}
|
|
8582
9011
|
return entries;
|
|
8583
9012
|
}
|
|
8584
9013
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
8585
9014
|
const entries = [];
|
|
8586
|
-
const skillsDir =
|
|
8587
|
-
if (
|
|
8588
|
-
for (const skillName of
|
|
8589
|
-
const skillFile =
|
|
8590
|
-
if (
|
|
9015
|
+
const skillsDir = path47.join(srcDir, "skills");
|
|
9016
|
+
if (fs40.pathExistsSync(skillsDir)) {
|
|
9017
|
+
for (const skillName of fs40.readdirSync(skillsDir)) {
|
|
9018
|
+
const skillFile = path47.join(skillsDir, skillName, "SKILL.md");
|
|
9019
|
+
if (fs40.pathExistsSync(skillFile)) {
|
|
8591
9020
|
entries.push({
|
|
8592
9021
|
stableId: `skill.${skillName}`,
|
|
8593
|
-
srcRelPath:
|
|
8594
|
-
destPath:
|
|
9022
|
+
srcRelPath: path47.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
9023
|
+
destPath: path47.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
8595
9024
|
});
|
|
8596
9025
|
entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
|
|
8597
9026
|
}
|
|
8598
9027
|
}
|
|
8599
9028
|
}
|
|
8600
|
-
const commandsDir =
|
|
8601
|
-
if (
|
|
8602
|
-
for (const fileName of
|
|
9029
|
+
const commandsDir = path47.join(srcDir, "commands");
|
|
9030
|
+
if (fs40.pathExistsSync(commandsDir)) {
|
|
9031
|
+
for (const fileName of fs40.readdirSync(commandsDir)) {
|
|
8603
9032
|
if (!fileName.endsWith(".md")) continue;
|
|
8604
9033
|
const commandName = fileName.slice(0, -".md".length);
|
|
8605
9034
|
entries.push({
|
|
8606
9035
|
stableId: `command.${commandName}`,
|
|
8607
|
-
srcRelPath:
|
|
8608
|
-
destPath:
|
|
9036
|
+
srcRelPath: path47.join("library", "global", "commands", fileName),
|
|
9037
|
+
destPath: path47.join(claudeDir, "commands", fileName)
|
|
8609
9038
|
});
|
|
8610
9039
|
}
|
|
8611
9040
|
}
|
|
@@ -8637,7 +9066,7 @@ async function applyInstall(options = {}) {
|
|
|
8637
9066
|
};
|
|
8638
9067
|
const manifestFiles = [];
|
|
8639
9068
|
for (const entry of sourceFiles) {
|
|
8640
|
-
const srcPath =
|
|
9069
|
+
const srcPath = path47.join(packageRoot(), entry.srcRelPath);
|
|
8641
9070
|
const rawContent = await readText(srcPath);
|
|
8642
9071
|
if (rawContent === void 0) {
|
|
8643
9072
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -8658,7 +9087,7 @@ async function applyInstall(options = {}) {
|
|
|
8658
9087
|
}
|
|
8659
9088
|
continue;
|
|
8660
9089
|
}
|
|
8661
|
-
const destExists =
|
|
9090
|
+
const destExists = fs40.pathExistsSync(entry.destPath);
|
|
8662
9091
|
if (destExists) {
|
|
8663
9092
|
const currentContent = await readText(entry.destPath);
|
|
8664
9093
|
if (currentContent !== void 0) {
|
|
@@ -8698,7 +9127,7 @@ async function applyInstall(options = {}) {
|
|
|
8698
9127
|
}
|
|
8699
9128
|
let mergedSettings;
|
|
8700
9129
|
if (!check) {
|
|
8701
|
-
const fragmentPath =
|
|
9130
|
+
const fragmentPath = path47.join(srcDir, "settings-fragments", "hooks.json");
|
|
8702
9131
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
8703
9132
|
const settings = await readSettings();
|
|
8704
9133
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -8717,13 +9146,13 @@ async function applyInstall(options = {}) {
|
|
|
8717
9146
|
manifestFiles.push(entry);
|
|
8718
9147
|
continue;
|
|
8719
9148
|
}
|
|
8720
|
-
if (!
|
|
9149
|
+
if (!fs40.pathExistsSync(entry.destPath)) continue;
|
|
8721
9150
|
const content2 = await readText(entry.destPath);
|
|
8722
9151
|
if (!content2) continue;
|
|
8723
9152
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
8724
9153
|
const currentHash = hashContent(content2);
|
|
8725
9154
|
if (hasHeader && currentHash === entry.hash) {
|
|
8726
|
-
if (!dryRun) await
|
|
9155
|
+
if (!dryRun) await fs40.remove(entry.destPath);
|
|
8727
9156
|
result.deleted.push(entry.destPath);
|
|
8728
9157
|
} else {
|
|
8729
9158
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -8797,7 +9226,7 @@ function summarizeLockDiff(before, after) {
|
|
|
8797
9226
|
// src/update/lockfile.ts
|
|
8798
9227
|
import { constants as fsConstants } from "node:fs";
|
|
8799
9228
|
import { copyFile, mkdir, readFile as readFile2 } from "node:fs/promises";
|
|
8800
|
-
import
|
|
9229
|
+
import path48 from "node:path";
|
|
8801
9230
|
init_fs();
|
|
8802
9231
|
init_paths();
|
|
8803
9232
|
async function readLockSummary(root) {
|
|
@@ -8837,7 +9266,7 @@ async function backupLockfile(root) {
|
|
|
8837
9266
|
await mkdir(backupDir, { recursive: true });
|
|
8838
9267
|
let stamp = Date.now();
|
|
8839
9268
|
for (; ; ) {
|
|
8840
|
-
const dest =
|
|
9269
|
+
const dest = path48.join(backupDir, lockBackupFileName(stamp));
|
|
8841
9270
|
try {
|
|
8842
9271
|
await copyFile(lockPath, dest, fsConstants.COPYFILE_EXCL);
|
|
8843
9272
|
return;
|
|
@@ -8888,7 +9317,7 @@ function diffLock(before, after) {
|
|
|
8888
9317
|
}
|
|
8889
9318
|
async function hasLocalOverrides(root) {
|
|
8890
9319
|
try {
|
|
8891
|
-
await readFile2(
|
|
9320
|
+
await readFile2(path48.join(root, ".claude", "settings.json"), "utf8");
|
|
8892
9321
|
return true;
|
|
8893
9322
|
} catch {
|
|
8894
9323
|
return false;
|
|
@@ -8933,7 +9362,7 @@ async function runUpdate(options) {
|
|
|
8933
9362
|
return;
|
|
8934
9363
|
}
|
|
8935
9364
|
if (options.check) {
|
|
8936
|
-
const pkgJson2 = await readJson(
|
|
9365
|
+
const pkgJson2 = await readJson(path49.join(packageRoot(), "package.json"));
|
|
8937
9366
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
8938
9367
|
const [
|
|
8939
9368
|
status,
|
|
@@ -8985,7 +9414,7 @@ async function runUpdate(options) {
|
|
|
8985
9414
|
}
|
|
8986
9415
|
return;
|
|
8987
9416
|
}
|
|
8988
|
-
const pkgJson = await readJson(
|
|
9417
|
+
const pkgJson = await readJson(path49.join(packageRoot(), "package.json"));
|
|
8989
9418
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
8990
9419
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
8991
9420
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -9042,7 +9471,7 @@ async function runFromHookCheck(root) {
|
|
|
9042
9471
|
try {
|
|
9043
9472
|
const summary = await readLockSummary(root);
|
|
9044
9473
|
if (summary.count === 0) return;
|
|
9045
|
-
const pkgJson = await readJson(
|
|
9474
|
+
const pkgJson = await readJson(path49.join(packageRoot(), "package.json"));
|
|
9046
9475
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
9047
9476
|
const pin = readProjectCatalogPin(root);
|
|
9048
9477
|
const honoursPin = pin.kind === "pinned" && summary.catalogRef === pin.ref;
|
|
@@ -9063,7 +9492,7 @@ async function runFromHookCheck(root) {
|
|
|
9063
9492
|
JSON.stringify({
|
|
9064
9493
|
hookSpecificOutput: {
|
|
9065
9494
|
hookEventName: "SessionStart",
|
|
9066
|
-
additionalContext: `This project's haus setup is behind: ${reasons.join("; ")}. Suggest running \`/haus-workflow
|
|
9495
|
+
additionalContext: `This project's haus setup is behind: ${reasons.join("; ")}. Suggest running \`/haus-workflow refresh\` to bring it up to date.`
|
|
9067
9496
|
}
|
|
9068
9497
|
})
|
|
9069
9498
|
);
|
|
@@ -9226,7 +9655,7 @@ init_exec();
|
|
|
9226
9655
|
init_logger();
|
|
9227
9656
|
import { existsSync as existsSync2 } from "node:fs";
|
|
9228
9657
|
import { readdir } from "node:fs/promises";
|
|
9229
|
-
import
|
|
9658
|
+
import path50 from "node:path";
|
|
9230
9659
|
var GIT_LOCATION_VARS = [
|
|
9231
9660
|
"GIT_DIR",
|
|
9232
9661
|
"GIT_WORK_TREE",
|
|
@@ -9286,11 +9715,11 @@ async function runClone(url, opts = {}) {
|
|
|
9286
9715
|
process.exitCode = 1;
|
|
9287
9716
|
return;
|
|
9288
9717
|
}
|
|
9289
|
-
const target =
|
|
9718
|
+
const target = path50.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
9290
9719
|
if (existsSync2(target) && !await isEmptyDirectory(target)) {
|
|
9291
9720
|
const check = await checkExistingTarget(target, url);
|
|
9292
9721
|
if (check.kind === "matches") {
|
|
9293
|
-
log(`\u2022 ${
|
|
9722
|
+
log(`\u2022 ${path50.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
|
|
9294
9723
|
return;
|
|
9295
9724
|
}
|
|
9296
9725
|
const message2 = 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].`;
|
|
@@ -9317,8 +9746,8 @@ async function runClone(url, opts = {}) {
|
|
|
9317
9746
|
|
|
9318
9747
|
// src/install/scaffold.ts
|
|
9319
9748
|
init_logger();
|
|
9320
|
-
import
|
|
9321
|
-
import
|
|
9749
|
+
import path51 from "node:path";
|
|
9750
|
+
import fs41 from "fs-extra";
|
|
9322
9751
|
async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
9323
9752
|
const scaffolded = [];
|
|
9324
9753
|
const skipped = [];
|
|
@@ -9331,27 +9760,27 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
9331
9760
|
skipped.push(name);
|
|
9332
9761
|
if (outcome === "skipped-exists") skippedExisting.push(name);
|
|
9333
9762
|
};
|
|
9334
|
-
const realRoot = await
|
|
9763
|
+
const realRoot = await fs41.realpath(catalogRoot).catch(() => null);
|
|
9335
9764
|
for (const item of items) {
|
|
9336
9765
|
if (item.type !== "config") continue;
|
|
9337
|
-
const sourcePath =
|
|
9338
|
-
const rel =
|
|
9339
|
-
const escapes = rel === "" ||
|
|
9766
|
+
const sourcePath = path51.resolve(catalogRoot, item.path);
|
|
9767
|
+
const rel = path51.relative(catalogRoot, sourcePath);
|
|
9768
|
+
const escapes = rel === "" || path51.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
|
|
9340
9769
|
if (escapes) {
|
|
9341
9770
|
warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
|
|
9342
9771
|
continue;
|
|
9343
9772
|
}
|
|
9344
|
-
const realSource = await
|
|
9773
|
+
const realSource = await fs41.realpath(sourcePath).catch(() => null);
|
|
9345
9774
|
if (!realRoot || !realSource) {
|
|
9346
9775
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
9347
9776
|
continue;
|
|
9348
9777
|
}
|
|
9349
|
-
const realRel =
|
|
9350
|
-
if (realRel !== "" && (
|
|
9778
|
+
const realRel = path51.relative(realRoot, realSource);
|
|
9779
|
+
if (realRel !== "" && (path51.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
|
|
9351
9780
|
warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
|
|
9352
9781
|
continue;
|
|
9353
9782
|
}
|
|
9354
|
-
const stat = await
|
|
9783
|
+
const stat = await fs41.lstat(sourcePath).catch(() => null);
|
|
9355
9784
|
if (!stat) {
|
|
9356
9785
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
9357
9786
|
continue;
|
|
@@ -9361,14 +9790,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
9361
9790
|
continue;
|
|
9362
9791
|
}
|
|
9363
9792
|
if (stat.isFile()) {
|
|
9364
|
-
const filename =
|
|
9365
|
-
const dest =
|
|
9793
|
+
const filename = path51.basename(sourcePath);
|
|
9794
|
+
const dest = path51.join(projectRoot, filename);
|
|
9366
9795
|
record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
|
|
9367
9796
|
} else if (stat.isDirectory()) {
|
|
9368
|
-
const entries = await
|
|
9797
|
+
const entries = await fs41.readdir(sourcePath);
|
|
9369
9798
|
for (const entry of entries) {
|
|
9370
|
-
const src =
|
|
9371
|
-
const dest =
|
|
9799
|
+
const src = path51.join(sourcePath, entry);
|
|
9800
|
+
const dest = path51.join(projectRoot, entry);
|
|
9372
9801
|
record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
|
|
9373
9802
|
}
|
|
9374
9803
|
}
|
|
@@ -9376,42 +9805,42 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
9376
9805
|
return { scaffolded, skipped, skippedExisting };
|
|
9377
9806
|
}
|
|
9378
9807
|
async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
9379
|
-
const destDisplay =
|
|
9380
|
-
const srcStat = await
|
|
9808
|
+
const destDisplay = path51.relative(projectRoot, dest) || path51.basename(dest);
|
|
9809
|
+
const srcStat = await fs41.lstat(src).catch(() => null);
|
|
9381
9810
|
if (!srcStat) {
|
|
9382
|
-
warn(`Skipping ${
|
|
9811
|
+
warn(`Skipping ${path51.basename(src)}: source not found`);
|
|
9383
9812
|
return "skipped-refused";
|
|
9384
9813
|
}
|
|
9385
9814
|
if (srcStat.isSymbolicLink()) {
|
|
9386
|
-
warn(`Skipping ${
|
|
9815
|
+
warn(`Skipping ${path51.basename(src)}: source is a symlink`);
|
|
9387
9816
|
return "skipped-refused";
|
|
9388
9817
|
}
|
|
9389
|
-
const exists = await
|
|
9818
|
+
const exists = await fs41.pathExists(dest);
|
|
9390
9819
|
if (exists && !opts.force) {
|
|
9391
9820
|
warn(`Skipping ${destDisplay}: already exists \u2014 re-run with --force to overwrite it`);
|
|
9392
9821
|
return "skipped-exists";
|
|
9393
9822
|
}
|
|
9394
9823
|
if (opts.dryRun) {
|
|
9395
|
-
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${
|
|
9824
|
+
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path51.basename(dest)} (${itemId})`);
|
|
9396
9825
|
return "scaffolded";
|
|
9397
9826
|
}
|
|
9398
9827
|
if (exists && opts.force) {
|
|
9399
|
-
await
|
|
9828
|
+
await fs41.remove(dest);
|
|
9400
9829
|
}
|
|
9401
|
-
await
|
|
9402
|
-
await
|
|
9830
|
+
await fs41.ensureDir(path51.dirname(dest));
|
|
9831
|
+
await fs41.copy(src, dest, {
|
|
9403
9832
|
overwrite: true,
|
|
9404
9833
|
dereference: false,
|
|
9405
9834
|
// A vanished/unreadable entry mid-walk must skip that path, not crash the copy.
|
|
9406
9835
|
filter: (s) => {
|
|
9407
9836
|
try {
|
|
9408
|
-
return !
|
|
9837
|
+
return !fs41.lstatSync(s).isSymbolicLink();
|
|
9409
9838
|
} catch {
|
|
9410
9839
|
return false;
|
|
9411
9840
|
}
|
|
9412
9841
|
}
|
|
9413
9842
|
});
|
|
9414
|
-
if (!opts.quiet) log(`${exists ? "Overwrote" : "Created"} ${
|
|
9843
|
+
if (!opts.quiet) log(`${exists ? "Overwrote" : "Created"} ${path51.basename(dest)} (${itemId})`);
|
|
9415
9844
|
return "scaffolded";
|
|
9416
9845
|
}
|
|
9417
9846
|
|
|
@@ -9540,7 +9969,7 @@ async function runDecisionsGuard(options = {}) {
|
|
|
9540
9969
|
}
|
|
9541
9970
|
|
|
9542
9971
|
// src/commands/decisions.ts
|
|
9543
|
-
import
|
|
9972
|
+
import path52 from "node:path";
|
|
9544
9973
|
|
|
9545
9974
|
// src/decisions/next-number.ts
|
|
9546
9975
|
async function nextDecisionNumber(root) {
|
|
@@ -9651,7 +10080,7 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
9651
10080
|
return;
|
|
9652
10081
|
}
|
|
9653
10082
|
if (sub === "validate") {
|
|
9654
|
-
const target = arg ?
|
|
10083
|
+
const target = arg ? path52.resolve(root, arg) : void 0;
|
|
9655
10084
|
if (!target) {
|
|
9656
10085
|
error("usage: haus decisions validate <path-to-decision.md>");
|
|
9657
10086
|
process.exitCode = 1;
|
|
@@ -9927,61 +10356,6 @@ async function runGuard(kind, _options) {
|
|
|
9927
10356
|
// src/claude/setup-core.ts
|
|
9928
10357
|
init_github_rate_limit();
|
|
9929
10358
|
|
|
9930
|
-
// src/claude/project-exclusions.ts
|
|
9931
|
-
import fs41 from "fs-extra";
|
|
9932
|
-
init_project_pin();
|
|
9933
|
-
init_fs();
|
|
9934
|
-
init_logger();
|
|
9935
|
-
init_paths();
|
|
9936
|
-
function normalizeExcludedIds(value) {
|
|
9937
|
-
if (!Array.isArray(value)) return [];
|
|
9938
|
-
return [
|
|
9939
|
-
...new Set(
|
|
9940
|
-
value.filter((entry) => typeof entry === "string" && entry.length > 0)
|
|
9941
|
-
)
|
|
9942
|
-
];
|
|
9943
|
-
}
|
|
9944
|
-
async function readProjectExcludedIds(root) {
|
|
9945
|
-
try {
|
|
9946
|
-
const data = await readJson(hausPath(root, PROJECT_CONFIG_FILE));
|
|
9947
|
-
if (!data) return [];
|
|
9948
|
-
return normalizeExcludedIds(data.excludedIds);
|
|
9949
|
-
} catch (err) {
|
|
9950
|
-
if (err.code === "ENOENT") return [];
|
|
9951
|
-
warn(
|
|
9952
|
-
`${PROJECT_CONFIG_REL} could not be read for excludedIds (${err instanceof Error ? err.message : String(err)}) \u2014 treating as none`
|
|
9953
|
-
);
|
|
9954
|
-
return [];
|
|
9955
|
-
}
|
|
9956
|
-
}
|
|
9957
|
-
function isItemExcluded(item, excluded) {
|
|
9958
|
-
if (excluded.has(item.id)) return true;
|
|
9959
|
-
return normalizeFormerIds(item.formerIds).some((formerId) => excluded.has(formerId));
|
|
9960
|
-
}
|
|
9961
|
-
async function addProjectExcludedIds(root, ids) {
|
|
9962
|
-
const toAdd = [...new Set(ids.filter((id) => id.length > 0))];
|
|
9963
|
-
if (toAdd.length === 0) return;
|
|
9964
|
-
const file = hausPath(root, PROJECT_CONFIG_FILE);
|
|
9965
|
-
let existing = {};
|
|
9966
|
-
if (await fs41.pathExists(file)) {
|
|
9967
|
-
try {
|
|
9968
|
-
const parsed = JSON.parse(await fs41.readFile(file, "utf8"));
|
|
9969
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
9970
|
-
existing = { ...parsed };
|
|
9971
|
-
} else {
|
|
9972
|
-
warn(`${PROJECT_CONFIG_REL} is not a JSON object \u2014 rewriting with excludedIds only`);
|
|
9973
|
-
}
|
|
9974
|
-
} catch (err) {
|
|
9975
|
-
warn(
|
|
9976
|
-
`${PROJECT_CONFIG_REL} could not be parsed (${err instanceof Error ? err.message : String(err)}) \u2014 rewriting with excludedIds only`
|
|
9977
|
-
);
|
|
9978
|
-
}
|
|
9979
|
-
}
|
|
9980
|
-
const merged = normalizeExcludedIds(existing.excludedIds);
|
|
9981
|
-
const next = [.../* @__PURE__ */ new Set([...merged, ...toAdd])].sort();
|
|
9982
|
-
await writeJson(file, { ...existing, excludedIds: next });
|
|
9983
|
-
}
|
|
9984
|
-
|
|
9985
10359
|
// src/recommender/recommend.ts
|
|
9986
10360
|
init_fs();
|
|
9987
10361
|
init_paths();
|
|
@@ -10042,8 +10416,11 @@ async function recommend(root, context, opts = {}) {
|
|
|
10042
10416
|
(x) => x.toLowerCase()
|
|
10043
10417
|
)
|
|
10044
10418
|
);
|
|
10045
|
-
const
|
|
10046
|
-
const
|
|
10419
|
+
const declared = await readDeclaredContext(root);
|
|
10420
|
+
const declaredRoles = new Set(declared.roles);
|
|
10421
|
+
const declaredStacks = new Set(declared.stacks);
|
|
10422
|
+
const roleSet = /* @__PURE__ */ new Set([...scannerRoles, ...deepRoles, ...declaredRoles]);
|
|
10423
|
+
const stackSet = /* @__PURE__ */ new Set([...scannerStacks, ...deepStacks, ...declaredStacks]);
|
|
10047
10424
|
const depSet = scannerDeps;
|
|
10048
10425
|
const recommended = [];
|
|
10049
10426
|
const skipped = [];
|
|
@@ -10058,8 +10435,32 @@ async function recommend(root, context, opts = {}) {
|
|
|
10058
10435
|
...gates ? { gates: gates.map(({ name, passed }) => ({ name, passed })) } : {}
|
|
10059
10436
|
});
|
|
10060
10437
|
};
|
|
10061
|
-
const roleSignal = (name) =>
|
|
10062
|
-
|
|
10438
|
+
const roleSignal = (name) => {
|
|
10439
|
+
const key = name.toLowerCase();
|
|
10440
|
+
if (scannerRoles.has(key)) return `role:${name}`;
|
|
10441
|
+
if (declaredRoles.has(key)) return `declared:role:${name}`;
|
|
10442
|
+
return `deep:role:${name}`;
|
|
10443
|
+
};
|
|
10444
|
+
const stackSignal = (name) => {
|
|
10445
|
+
const key = name.toLowerCase();
|
|
10446
|
+
if (scannerStacks.has(key)) return `tag:${name}`;
|
|
10447
|
+
if (declaredStacks.has(key)) return `declared:tag:${name}`;
|
|
10448
|
+
return `deep:tag:${name}`;
|
|
10449
|
+
};
|
|
10450
|
+
const markDeclaredProvenance = (signal) => {
|
|
10451
|
+
if (signal == null) return signal;
|
|
10452
|
+
const separator = signal.indexOf(":");
|
|
10453
|
+
if (separator === -1) return signal;
|
|
10454
|
+
const kind = signal.slice(0, separator);
|
|
10455
|
+
const value = signal.slice(separator + 1).toLowerCase();
|
|
10456
|
+
if (kind === "role" && !scannerRoles.has(value) && declaredRoles.has(value)) {
|
|
10457
|
+
return `declared:${signal}`;
|
|
10458
|
+
}
|
|
10459
|
+
if (kind === "stack" && !scannerStacks.has(value) && declaredStacks.has(value)) {
|
|
10460
|
+
return `declared:${signal}`;
|
|
10461
|
+
}
|
|
10462
|
+
return signal;
|
|
10463
|
+
};
|
|
10063
10464
|
const buildGateChecks = (item, itemTags, normSource) => {
|
|
10064
10465
|
const gates = [];
|
|
10065
10466
|
const add = (name, passed, code, message2, signal) => {
|
|
@@ -10207,7 +10608,11 @@ async function recommend(root, context, opts = {}) {
|
|
|
10207
10608
|
if (changedMatch)
|
|
10208
10609
|
push("changed-file-match", "changed file match", `changedFile:${changedMatch}`);
|
|
10209
10610
|
if (requiresAnySatisfied && !reasons.some((r) => r.code === "stack-match")) {
|
|
10210
|
-
push(
|
|
10611
|
+
push(
|
|
10612
|
+
"requires-any-match",
|
|
10613
|
+
"requires-any signal match",
|
|
10614
|
+
markDeclaredProvenance(requiresAnySatisfied.signal)
|
|
10615
|
+
);
|
|
10211
10616
|
}
|
|
10212
10617
|
const gateSummary = gates.map(({ name, passed }) => ({ name, passed }));
|
|
10213
10618
|
const hasEvidence = reasons.some((r) => r.code !== "default-baseline");
|
|
@@ -10239,7 +10644,15 @@ async function recommend(root, context, opts = {}) {
|
|
|
10239
10644
|
}
|
|
10240
10645
|
applyCoInstallSuppression(recommended, skipped);
|
|
10241
10646
|
applyManualExcludes(excludedIds, items, recommended, skipped);
|
|
10242
|
-
const
|
|
10647
|
+
const persistedIncludes = (await readProjectIncludedIds(root)).filter(
|
|
10648
|
+
(id) => !excludedIds.includes(id)
|
|
10649
|
+
);
|
|
10650
|
+
const includeWarnings = applyManualIncludes(
|
|
10651
|
+
[.../* @__PURE__ */ new Set([...opts.include ?? [], ...persistedIncludes])],
|
|
10652
|
+
items,
|
|
10653
|
+
recommended,
|
|
10654
|
+
skipped
|
|
10655
|
+
);
|
|
10243
10656
|
const optInEligible = buildOptInEligible(items, skipped);
|
|
10244
10657
|
recommended.sort((a, b) => a.id.localeCompare(b.id));
|
|
10245
10658
|
skipped.sort((a, b) => a.id.localeCompare(b.id));
|
|
@@ -10450,9 +10863,9 @@ function buildOptInEligible(items, skipped) {
|
|
|
10450
10863
|
}
|
|
10451
10864
|
|
|
10452
10865
|
// src/commands/workspace/discover.ts
|
|
10453
|
-
import
|
|
10454
|
-
import
|
|
10455
|
-
import
|
|
10866
|
+
import path53 from "node:path";
|
|
10867
|
+
import fg6 from "fast-glob";
|
|
10868
|
+
import YAML3 from "yaml";
|
|
10456
10869
|
init_fs();
|
|
10457
10870
|
init_logger();
|
|
10458
10871
|
var DEFAULT_MAX_DEPTH = 3;
|
|
@@ -10487,16 +10900,16 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
10487
10900
|
suppressErrors: true
|
|
10488
10901
|
};
|
|
10489
10902
|
const [gitMatches, manifestMatches] = await Promise.all([
|
|
10490
|
-
|
|
10491
|
-
|
|
10903
|
+
fg6(GIT_MARKER, { ...commonOpts, ignore: IGNORE }),
|
|
10904
|
+
fg6(MANIFEST_MARKERS, { ...commonOpts, ignore: [...IGNORE, "**/.git/**"] })
|
|
10492
10905
|
]);
|
|
10493
10906
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
10494
10907
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
10495
10908
|
for (const match of gitMatches) {
|
|
10496
|
-
gitDirs.add(
|
|
10909
|
+
gitDirs.add(path53.posix.dirname(match));
|
|
10497
10910
|
}
|
|
10498
10911
|
for (const match of manifestMatches) {
|
|
10499
|
-
manifestDirs.add(
|
|
10912
|
+
manifestDirs.add(path53.posix.dirname(match));
|
|
10500
10913
|
}
|
|
10501
10914
|
const repoRoots = [...gitDirs];
|
|
10502
10915
|
const manifestSorted = [...manifestDirs].sort(
|
|
@@ -10513,9 +10926,9 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
10513
10926
|
async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
10514
10927
|
const repoRoots = (await findRepoRoots(workspaceRoot, maxDepth)).filter((dir) => dir !== ".");
|
|
10515
10928
|
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
10516
|
-
const absDir =
|
|
10517
|
-
const pkg = await readJson(
|
|
10518
|
-
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name :
|
|
10929
|
+
const absDir = path53.resolve(workspaceRoot, relDir);
|
|
10930
|
+
const pkg = await readJson(path53.join(absDir, "package.json"));
|
|
10931
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path53.basename(absDir);
|
|
10519
10932
|
let role = "auto";
|
|
10520
10933
|
try {
|
|
10521
10934
|
const scan = await scanProject(absDir);
|
|
@@ -10551,14 +10964,14 @@ function mergeWorkspaceConfig(existing, discovered, opts = {}) {
|
|
|
10551
10964
|
};
|
|
10552
10965
|
}
|
|
10553
10966
|
function renderWorkspaceYaml(config) {
|
|
10554
|
-
return
|
|
10967
|
+
return YAML3.stringify({
|
|
10555
10968
|
client: config.client,
|
|
10556
10969
|
repos: config.repos.map((r) => ({ name: r.name, path: r.path, role: r.role ?? "auto" })),
|
|
10557
10970
|
relationships: config.relationships
|
|
10558
10971
|
});
|
|
10559
10972
|
}
|
|
10560
10973
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
10561
|
-
const yamlPath =
|
|
10974
|
+
const yamlPath = path53.join(workspaceRoot, "haus.workspace.yaml");
|
|
10562
10975
|
const existingText = await readText(yamlPath);
|
|
10563
10976
|
const existing = parseWorkspaceConfig(existingText);
|
|
10564
10977
|
if (existingText && !existing) {
|
|
@@ -10965,7 +11378,7 @@ async function runInstall(options, deps = {}) {
|
|
|
10965
11378
|
}
|
|
10966
11379
|
|
|
10967
11380
|
// src/new-project/create-project.ts
|
|
10968
|
-
import
|
|
11381
|
+
import path54 from "node:path";
|
|
10969
11382
|
import fs42 from "fs-extra";
|
|
10970
11383
|
init_exec();
|
|
10971
11384
|
init_logger();
|
|
@@ -11129,7 +11542,7 @@ var defaultDeps = {
|
|
|
11129
11542
|
var NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
11130
11543
|
async function createProject(opts, deps = defaultDeps) {
|
|
11131
11544
|
const steps = [];
|
|
11132
|
-
const projectPath =
|
|
11545
|
+
const projectPath = path54.resolve(opts.parentDir, opts.name);
|
|
11133
11546
|
const installHint = `cd ${opts.name} && ${opts.pm}${opts.pm === "yarn" ? "" : " install"}`;
|
|
11134
11547
|
const fail = (step, detail) => {
|
|
11135
11548
|
steps.push({ step, status: "failed", detail });
|
|
@@ -11137,7 +11550,7 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
11137
11550
|
};
|
|
11138
11551
|
const stack = getStack(opts.stackId);
|
|
11139
11552
|
if (!stack) return fail("preflight", `Unknown stack "${opts.stackId}". Run \`haus new --list\`.`);
|
|
11140
|
-
if (!NAME_RE.test(opts.name) || opts.name.includes("/") || opts.name.includes("\\") ||
|
|
11553
|
+
if (!NAME_RE.test(opts.name) || opts.name.includes("/") || opts.name.includes("\\") || path54.dirname(projectPath) !== path54.resolve(opts.parentDir)) {
|
|
11141
11554
|
return fail(
|
|
11142
11555
|
"preflight",
|
|
11143
11556
|
`Invalid project name "${opts.name}" (lowercase letters, digits, . _ - only).`
|
|
@@ -11221,15 +11634,15 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
11221
11634
|
steps.push({ step: "scaffold", status: "done" });
|
|
11222
11635
|
phase = "git";
|
|
11223
11636
|
if (!opts.json) log("\u25B6 git: init/branch/commit");
|
|
11224
|
-
if (!await fs42.pathExists(
|
|
11637
|
+
if (!await fs42.pathExists(path54.join(projectPath, ".git"))) {
|
|
11225
11638
|
const init = await deps.git(["init"], { cwd: projectPath });
|
|
11226
11639
|
if (init.exitCode !== 0) return fail("git", `git init failed: ${init.stderr}`);
|
|
11227
11640
|
}
|
|
11228
11641
|
const branch = await deps.git(["branch", "-M", "main"], { cwd: projectPath });
|
|
11229
11642
|
if (branch.exitCode !== 0) return fail("git", `git branch -M main failed: ${branch.stderr}`);
|
|
11230
11643
|
const lockfile = opts.pm === "yarn" ? "yarn.lock" : opts.pm === "pnpm" ? "pnpm-lock.yaml" : "package-lock.json";
|
|
11231
|
-
if (!await fs42.pathExists(
|
|
11232
|
-
await fs42.writeFile(
|
|
11644
|
+
if (!await fs42.pathExists(path54.join(projectPath, lockfile))) {
|
|
11645
|
+
await fs42.writeFile(path54.join(projectPath, lockfile), "");
|
|
11233
11646
|
}
|
|
11234
11647
|
const add = await deps.git(["add", "-A"], { cwd: projectPath });
|
|
11235
11648
|
if (add.exitCode !== 0) return fail("git", `git add failed: ${add.stderr}`);
|
|
@@ -11478,6 +11891,7 @@ async function runRecommend(options) {
|
|
|
11478
11891
|
const root = process.cwd();
|
|
11479
11892
|
const context = await readContextOrScan(root);
|
|
11480
11893
|
const include = parseIdList(options.include);
|
|
11894
|
+
if (include.length > 0) await addProjectIncludedIds(root, include);
|
|
11481
11895
|
const activePluginIds = await activeGlobalPluginIds(root);
|
|
11482
11896
|
const result = await recommend(root, context, { include, activePluginIds });
|
|
11483
11897
|
await writeJson(hausPath(root, "recommendation.json"), result);
|
|
@@ -11498,7 +11912,7 @@ async function runRecommend(options) {
|
|
|
11498
11912
|
}
|
|
11499
11913
|
|
|
11500
11914
|
// src/commands/remove.ts
|
|
11501
|
-
import
|
|
11915
|
+
import path55 from "node:path";
|
|
11502
11916
|
init_fs();
|
|
11503
11917
|
init_logger();
|
|
11504
11918
|
init_paths();
|
|
@@ -11603,7 +12017,7 @@ async function runRemove(ids, options = {}) {
|
|
|
11603
12017
|
}
|
|
11604
12018
|
const allAbs = cleared.flatMap(({ existing }) => existing.map(({ abs }) => abs));
|
|
11605
12019
|
const backupRoot = await backupTrackedPaths(root, allAbs, say, "remove");
|
|
11606
|
-
report.backupDir = backupRoot === null ? null :
|
|
12020
|
+
report.backupDir = backupRoot === null ? null : path55.basename(backupRoot);
|
|
11607
12021
|
for (const { id, existing } of cleared) {
|
|
11608
12022
|
await deleteTrackedPaths(root, id, existing, { verb: "Removed", reason: "" }, say);
|
|
11609
12023
|
}
|
|
@@ -11691,7 +12105,7 @@ async function runScan(options) {
|
|
|
11691
12105
|
}
|
|
11692
12106
|
|
|
11693
12107
|
// src/commands/undo.ts
|
|
11694
|
-
import
|
|
12108
|
+
import path56 from "node:path";
|
|
11695
12109
|
import fs43 from "fs-extra";
|
|
11696
12110
|
|
|
11697
12111
|
// src/claude/managed-paths.ts
|
|
@@ -11741,7 +12155,7 @@ async function settingsHasHausContent(root) {
|
|
|
11741
12155
|
return settings._haus != null;
|
|
11742
12156
|
}
|
|
11743
12157
|
async function claudeMdHasHausBlock(root) {
|
|
11744
|
-
const filePath =
|
|
12158
|
+
const filePath = path56.join(root, "CLAUDE.md");
|
|
11745
12159
|
if (!await fs43.pathExists(filePath)) return false;
|
|
11746
12160
|
const text = await fs43.readFile(filePath, "utf8");
|
|
11747
12161
|
return text.includes(BLOCK_BEGIN);
|
|
@@ -11754,15 +12168,15 @@ async function stripProjectSettings(root) {
|
|
|
11754
12168
|
const hasContent = Object.keys(settings).length > 0;
|
|
11755
12169
|
if (hasContent) {
|
|
11756
12170
|
await writeProjectSettings(root, settings);
|
|
11757
|
-
log(`Stripped haus rules from ${
|
|
12171
|
+
log(`Stripped haus rules from ${path56.relative(root, settingsPath)} (user settings preserved).`);
|
|
11758
12172
|
return true;
|
|
11759
12173
|
}
|
|
11760
12174
|
await fs43.remove(settingsPath);
|
|
11761
|
-
log(`Removed ${
|
|
12175
|
+
log(`Removed ${path56.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
11762
12176
|
return true;
|
|
11763
12177
|
}
|
|
11764
12178
|
async function stripRootClaudeMd(root) {
|
|
11765
|
-
const filePath =
|
|
12179
|
+
const filePath = path56.join(root, "CLAUDE.md");
|
|
11766
12180
|
if (!await fs43.pathExists(filePath)) return false;
|
|
11767
12181
|
const prev = await fs43.readFile(filePath, "utf8");
|
|
11768
12182
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
@@ -11790,12 +12204,12 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
|
11790
12204
|
const backupRoot = hausPath(root, "backups", dirBackupDirName("undo", stamp));
|
|
11791
12205
|
for (const abs of managedAbsPaths) {
|
|
11792
12206
|
if (!await fs43.pathExists(abs)) continue;
|
|
11793
|
-
const rel =
|
|
11794
|
-
const backupPath =
|
|
11795
|
-
await fs43.ensureDir(
|
|
12207
|
+
const rel = path56.relative(root, abs);
|
|
12208
|
+
const backupPath = path56.join(backupRoot, rel);
|
|
12209
|
+
await fs43.ensureDir(path56.dirname(backupPath));
|
|
11796
12210
|
await fs43.copy(abs, backupPath);
|
|
11797
12211
|
}
|
|
11798
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
12212
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path56.relative(root, backupRoot)}.`);
|
|
11799
12213
|
}
|
|
11800
12214
|
async function runUndo(options) {
|
|
11801
12215
|
const root = options.root ?? process.cwd();
|
|
@@ -11809,7 +12223,7 @@ async function runUndo(options) {
|
|
|
11809
12223
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
11810
12224
|
return;
|
|
11811
12225
|
}
|
|
11812
|
-
const relTargets = managed.map((p) =>
|
|
12226
|
+
const relTargets = managed.map((p) => path56.relative(root, p));
|
|
11813
12227
|
const summaryParts = [...relTargets];
|
|
11814
12228
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
11815
12229
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -11837,7 +12251,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
11837
12251
|
for (const abs of coreManaged) {
|
|
11838
12252
|
if (!await fs43.pathExists(abs)) continue;
|
|
11839
12253
|
await fs43.remove(abs);
|
|
11840
|
-
log(`Removed ${
|
|
12254
|
+
log(`Removed ${path56.relative(root, abs)}`);
|
|
11841
12255
|
}
|
|
11842
12256
|
for (const { id, existing } of lockRemovable) {
|
|
11843
12257
|
await deleteTrackedPaths(root, id, existing, { verb: "Removed" }, log);
|
|
@@ -11854,7 +12268,7 @@ init_fs();
|
|
|
11854
12268
|
init_logger();
|
|
11855
12269
|
init_paths();
|
|
11856
12270
|
import crypto3 from "node:crypto";
|
|
11857
|
-
import
|
|
12271
|
+
import path57 from "node:path";
|
|
11858
12272
|
import fs44 from "fs-extra";
|
|
11859
12273
|
async function runUninstall(options = {}) {
|
|
11860
12274
|
const { force = false, removePlugins = removeGlobalPlugins } = options;
|
|
@@ -11911,14 +12325,14 @@ async function runUninstall(options = {}) {
|
|
|
11911
12325
|
continue;
|
|
11912
12326
|
}
|
|
11913
12327
|
await fs44.remove(entry.destPath);
|
|
11914
|
-
await pruneEmptyDir(
|
|
12328
|
+
await pruneEmptyDir(path57.dirname(entry.destPath));
|
|
11915
12329
|
result.deleted.push(entry.destPath);
|
|
11916
12330
|
}
|
|
11917
12331
|
const settings = await readSettings();
|
|
11918
12332
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
11919
12333
|
await writeSettings(stripped);
|
|
11920
12334
|
result.hooksStripped = true;
|
|
11921
|
-
const hausDir =
|
|
12335
|
+
const hausDir = path57.join(globalClaudeDir(), "haus");
|
|
11922
12336
|
const manifestPath2 = hausManifestPath();
|
|
11923
12337
|
if (fs44.pathExistsSync(manifestPath2)) {
|
|
11924
12338
|
await fs44.remove(manifestPath2);
|
|
@@ -11995,7 +12409,7 @@ async function runUninstallCommand(options) {
|
|
|
11995
12409
|
}
|
|
11996
12410
|
|
|
11997
12411
|
// src/commands/validate-catalog.ts
|
|
11998
|
-
import
|
|
12412
|
+
import path59 from "node:path";
|
|
11999
12413
|
|
|
12000
12414
|
// src/catalog/validate-core.ts
|
|
12001
12415
|
init_forbidden_content();
|
|
@@ -12003,7 +12417,7 @@ init_manifest_item_fields();
|
|
|
12003
12417
|
init_path_safety();
|
|
12004
12418
|
init_validation_rules2();
|
|
12005
12419
|
import fs45 from "node:fs";
|
|
12006
|
-
import
|
|
12420
|
+
import path58 from "node:path";
|
|
12007
12421
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
12008
12422
|
var ITEM_ID_PATTERN = /^haus\./;
|
|
12009
12423
|
function auditForbiddenStacks(items) {
|
|
@@ -12154,7 +12568,7 @@ function readForAudit(absPath, label, failures) {
|
|
|
12154
12568
|
}
|
|
12155
12569
|
}
|
|
12156
12570
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
12157
|
-
const rel =
|
|
12571
|
+
const rel = path58.relative(manifestDir, absPath);
|
|
12158
12572
|
const failures = [];
|
|
12159
12573
|
const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
|
|
12160
12574
|
if (text === null) return failures;
|
|
@@ -12176,25 +12590,25 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
12176
12590
|
const failures = [];
|
|
12177
12591
|
for (const item of items) {
|
|
12178
12592
|
if (!item.path || !isSafeCatalogPath(item.path)) continue;
|
|
12179
|
-
const absPath =
|
|
12593
|
+
const absPath = path58.join(manifestDir, item.path);
|
|
12180
12594
|
if (item.type === "skill") {
|
|
12181
|
-
const skillMd =
|
|
12595
|
+
const skillMd = path58.join(absPath, "SKILL.md");
|
|
12182
12596
|
if (!fs45.existsSync(skillMd)) {
|
|
12183
|
-
failures.push(`${item.id}: missing ${
|
|
12597
|
+
failures.push(`${item.id}: missing ${path58.relative(manifestDir, skillMd)}`);
|
|
12184
12598
|
continue;
|
|
12185
12599
|
}
|
|
12186
12600
|
const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
|
|
12187
12601
|
if (text === null) continue;
|
|
12188
12602
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
12189
12603
|
failures.push(
|
|
12190
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
12604
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path58.relative(manifestDir, skillMd)}`)
|
|
12191
12605
|
);
|
|
12192
12606
|
} else if (item.type === "agent") {
|
|
12193
12607
|
if (!fs45.existsSync(absPath)) {
|
|
12194
12608
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
12195
12609
|
continue;
|
|
12196
12610
|
}
|
|
12197
|
-
const rel =
|
|
12611
|
+
const rel = path58.relative(manifestDir, absPath);
|
|
12198
12612
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
12199
12613
|
if (text === null) continue;
|
|
12200
12614
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -12210,7 +12624,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
12210
12624
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
12211
12625
|
continue;
|
|
12212
12626
|
}
|
|
12213
|
-
const rel =
|
|
12627
|
+
const rel = path58.relative(manifestDir, absPath);
|
|
12214
12628
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
12215
12629
|
if (text === null) continue;
|
|
12216
12630
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -12268,7 +12682,7 @@ function walkAllFiles(dir, fn, failures) {
|
|
|
12268
12682
|
}
|
|
12269
12683
|
for (const entry of entries) {
|
|
12270
12684
|
if (entry.name === ".DS_Store") continue;
|
|
12271
|
-
const full =
|
|
12685
|
+
const full = path58.join(dir, entry.name);
|
|
12272
12686
|
if (entry.isDirectory()) walkAllFiles(full, fn, failures);
|
|
12273
12687
|
else fn(full);
|
|
12274
12688
|
}
|
|
@@ -12292,12 +12706,12 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
12292
12706
|
const failures = [];
|
|
12293
12707
|
const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
|
|
12294
12708
|
for (const dir of CONTENT_ROOTS) {
|
|
12295
|
-
const abs =
|
|
12709
|
+
const abs = path58.join(manifestDir, dir);
|
|
12296
12710
|
if (!fs45.existsSync(abs)) continue;
|
|
12297
12711
|
walkAllFiles(
|
|
12298
12712
|
abs,
|
|
12299
12713
|
(file) => {
|
|
12300
|
-
const relPath =
|
|
12714
|
+
const relPath = path58.relative(manifestDir, file).replace(/\\/g, "/");
|
|
12301
12715
|
if (!isPathClaimed(relPath, claimedPaths)) {
|
|
12302
12716
|
failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
|
|
12303
12717
|
}
|
|
@@ -12318,7 +12732,7 @@ function walkMd(dir, fn, failures) {
|
|
|
12318
12732
|
return;
|
|
12319
12733
|
}
|
|
12320
12734
|
for (const entry of entries) {
|
|
12321
|
-
const full =
|
|
12735
|
+
const full = path58.join(dir, entry.name);
|
|
12322
12736
|
if (entry.isDirectory()) walkMd(full, fn, failures);
|
|
12323
12737
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
12324
12738
|
}
|
|
@@ -12328,13 +12742,13 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
12328
12742
|
const pathSourceMap = buildItemPathSourceMap(items);
|
|
12329
12743
|
const pathTypeMap = buildItemPathTypeMap(items);
|
|
12330
12744
|
for (const dir of ["skills", "agents", "templates", "commands", "sources"]) {
|
|
12331
|
-
const abs =
|
|
12745
|
+
const abs = path58.join(manifestDir, dir);
|
|
12332
12746
|
if (!fs45.existsSync(abs)) continue;
|
|
12333
12747
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
12334
12748
|
walkMd(
|
|
12335
12749
|
abs,
|
|
12336
12750
|
(file) => {
|
|
12337
|
-
const rel =
|
|
12751
|
+
const rel = path58.relative(manifestDir, file);
|
|
12338
12752
|
const text = readForAudit(file, rel, failures);
|
|
12339
12753
|
if (text === null) return;
|
|
12340
12754
|
const norm = rel.replace(/\\/g, "/");
|
|
@@ -12382,8 +12796,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
12382
12796
|
process.exitCode = 1;
|
|
12383
12797
|
return;
|
|
12384
12798
|
}
|
|
12385
|
-
const abs =
|
|
12386
|
-
const manifestDir =
|
|
12799
|
+
const abs = path59.resolve(process.cwd(), manifestPath2);
|
|
12800
|
+
const manifestDir = path59.dirname(abs);
|
|
12387
12801
|
const data = await readJson(abs);
|
|
12388
12802
|
if (!data?.items) {
|
|
12389
12803
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -12404,7 +12818,7 @@ init_logger();
|
|
|
12404
12818
|
|
|
12405
12819
|
// src/workspace/worktree/add.ts
|
|
12406
12820
|
init_fs();
|
|
12407
|
-
import
|
|
12821
|
+
import path66 from "node:path";
|
|
12408
12822
|
import fs49 from "fs-extra";
|
|
12409
12823
|
|
|
12410
12824
|
// src/workspace/worktree/git-worktree.ts
|
|
@@ -12525,12 +12939,12 @@ async function hasUnpushedWork(cwd) {
|
|
|
12525
12939
|
}
|
|
12526
12940
|
|
|
12527
12941
|
// src/workspace/worktree/hydrate.ts
|
|
12528
|
-
import
|
|
12942
|
+
import path62 from "node:path";
|
|
12529
12943
|
import fs47 from "fs-extra";
|
|
12530
12944
|
|
|
12531
12945
|
// src/workspace/worktree/cow-copy.ts
|
|
12532
12946
|
init_exec();
|
|
12533
|
-
import
|
|
12947
|
+
import path60 from "node:path";
|
|
12534
12948
|
import fs46 from "fs-extra";
|
|
12535
12949
|
async function getMacFilesystemType(dir) {
|
|
12536
12950
|
const df = await runCommand("df", ["-P", dir]);
|
|
@@ -12565,11 +12979,11 @@ async function cowCopyDir(src, dest) {
|
|
|
12565
12979
|
const srcExists = await fs46.pathExists(src);
|
|
12566
12980
|
if (!srcExists)
|
|
12567
12981
|
return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
|
|
12568
|
-
const strategy = await detectCowStrategy(
|
|
12982
|
+
const strategy = await detectCowStrategy(path60.dirname(dest));
|
|
12569
12983
|
if (strategy === "unsupported" || strategy === "unknown-platform") {
|
|
12570
12984
|
return { strategy, attempted: false, ok: false };
|
|
12571
12985
|
}
|
|
12572
|
-
await fs46.ensureDir(
|
|
12986
|
+
await fs46.ensureDir(path60.dirname(dest));
|
|
12573
12987
|
const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
|
|
12574
12988
|
const result = await runCommand("cp", args);
|
|
12575
12989
|
return {
|
|
@@ -12583,8 +12997,8 @@ async function cowCopyDir(src, dest) {
|
|
|
12583
12997
|
// src/workspace/worktree/install.ts
|
|
12584
12998
|
init_exec();
|
|
12585
12999
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
12586
|
-
import
|
|
12587
|
-
import
|
|
13000
|
+
import path61 from "node:path";
|
|
13001
|
+
import fg7 from "fast-glob";
|
|
12588
13002
|
function detectInstallPlan(signals) {
|
|
12589
13003
|
if (signals.hasYarnLock) {
|
|
12590
13004
|
return signals.hasPackageManagerField ? { manager: "yarn", command: "corepack", args: ["yarn", "install"] } : { manager: "yarn", command: "yarn", args: ["install"] };
|
|
@@ -12604,11 +13018,11 @@ function detectInstallPlan(signals) {
|
|
|
12604
13018
|
return { manager: "none" };
|
|
12605
13019
|
}
|
|
12606
13020
|
async function computeLockfileSignals(dir) {
|
|
12607
|
-
const hasYarnLock = existsSync3(
|
|
12608
|
-
const hasPnpmLock = existsSync3(
|
|
12609
|
-
const hasPackageLock = existsSync3(
|
|
12610
|
-
const hasComposerLock = existsSync3(
|
|
12611
|
-
const dotnetFiles = await
|
|
13021
|
+
const hasYarnLock = existsSync3(path61.join(dir, "yarn.lock"));
|
|
13022
|
+
const hasPnpmLock = existsSync3(path61.join(dir, "pnpm-lock.yaml"));
|
|
13023
|
+
const hasPackageLock = existsSync3(path61.join(dir, "package-lock.json"));
|
|
13024
|
+
const hasComposerLock = existsSync3(path61.join(dir, "composer.lock"));
|
|
13025
|
+
const dotnetFiles = await fg7(["**/*.csproj", "**/*.sln"], {
|
|
12612
13026
|
cwd: dir,
|
|
12613
13027
|
onlyFiles: true,
|
|
12614
13028
|
suppressErrors: true,
|
|
@@ -12621,7 +13035,7 @@ async function computeLockfileSignals(dir) {
|
|
|
12621
13035
|
});
|
|
12622
13036
|
const hasDotnetProject = dotnetFiles.length > 0;
|
|
12623
13037
|
let hasPackageManagerField = false;
|
|
12624
|
-
const pkgPath =
|
|
13038
|
+
const pkgPath = path61.join(dir, "package.json");
|
|
12625
13039
|
if (existsSync3(pkgPath)) {
|
|
12626
13040
|
try {
|
|
12627
13041
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
@@ -12664,7 +13078,7 @@ function missingYarnrcEnvVars(content2, env) {
|
|
|
12664
13078
|
return findRequiredYarnrcEnvVars(content2).filter((name) => env[name] === void 0);
|
|
12665
13079
|
}
|
|
12666
13080
|
function missingYarnrcEnvVarsInDir(dir, env) {
|
|
12667
|
-
const yarnrcPath =
|
|
13081
|
+
const yarnrcPath = path61.join(dir, ".yarnrc.yml");
|
|
12668
13082
|
if (!existsSync3(yarnrcPath)) return [];
|
|
12669
13083
|
try {
|
|
12670
13084
|
return missingYarnrcEnvVars(readFileSync3(yarnrcPath, "utf8"), env);
|
|
@@ -12754,8 +13168,8 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
12754
13168
|
const start = Date.now();
|
|
12755
13169
|
const targets = [];
|
|
12756
13170
|
for (const target of HYDRATION_TARGETS) {
|
|
12757
|
-
const src =
|
|
12758
|
-
const dest =
|
|
13171
|
+
const src = path62.join(mainMemberPath, target);
|
|
13172
|
+
const dest = path62.join(worktreeMemberPath, target);
|
|
12759
13173
|
if (!await fs47.pathExists(src)) continue;
|
|
12760
13174
|
const destExists = await fs47.pathExists(dest);
|
|
12761
13175
|
if (!opts.force && destExists) continue;
|
|
@@ -12773,11 +13187,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
12773
13187
|
|
|
12774
13188
|
// src/workspace/worktree/local-files.ts
|
|
12775
13189
|
init_exec();
|
|
12776
|
-
import
|
|
13190
|
+
import path63 from "node:path";
|
|
12777
13191
|
import fs48 from "fs-extra";
|
|
12778
13192
|
var MACHINE_LOCAL_FILES = [
|
|
12779
13193
|
"docker-compose.local.yml",
|
|
12780
|
-
|
|
13194
|
+
path63.join(".claude", "settings.local.json")
|
|
12781
13195
|
];
|
|
12782
13196
|
async function isTracked(repoRoot, relFile) {
|
|
12783
13197
|
const gitPath = relFile.replace(/\\/g, "/");
|
|
@@ -12787,11 +13201,11 @@ async function isTracked(repoRoot, relFile) {
|
|
|
12787
13201
|
async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
12788
13202
|
const copied = [];
|
|
12789
13203
|
for (const rel of MACHINE_LOCAL_FILES) {
|
|
12790
|
-
const src =
|
|
13204
|
+
const src = path63.join(mainMemberPath, rel);
|
|
12791
13205
|
if (!await fs48.pathExists(src)) continue;
|
|
12792
13206
|
if (await isTracked(mainMemberPath, rel)) continue;
|
|
12793
|
-
const dest =
|
|
12794
|
-
await fs48.ensureDir(
|
|
13207
|
+
const dest = path63.join(worktreeMemberPath, rel);
|
|
13208
|
+
await fs48.ensureDir(path63.dirname(dest));
|
|
12795
13209
|
await fs48.copy(src, dest, { overwrite: true });
|
|
12796
13210
|
copied.push(rel);
|
|
12797
13211
|
}
|
|
@@ -12800,15 +13214,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
|
12800
13214
|
|
|
12801
13215
|
// src/workspace/worktree/root.ts
|
|
12802
13216
|
import { existsSync as existsSync4 } from "node:fs";
|
|
12803
|
-
import
|
|
13217
|
+
import path64 from "node:path";
|
|
12804
13218
|
async function resolveWorkspaceForWorktree(start) {
|
|
12805
13219
|
const rootInfo = await resolveRoots(start);
|
|
12806
13220
|
if (!rootInfo.isGitRepo) {
|
|
12807
13221
|
return { ok: false, reason: "Not inside a git repository." };
|
|
12808
13222
|
}
|
|
12809
13223
|
const workspaceRoot = rootInfo.mainRoot;
|
|
12810
|
-
const hasYaml = existsSync4(
|
|
12811
|
-
const hasManifest = existsSync4(
|
|
13224
|
+
const hasYaml = existsSync4(path64.join(workspaceRoot, WORKSPACE_FILE));
|
|
13225
|
+
const hasManifest = existsSync4(path64.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
12812
13226
|
if (!hasYaml && !hasManifest) {
|
|
12813
13227
|
return {
|
|
12814
13228
|
ok: false,
|
|
@@ -12834,10 +13248,10 @@ function selectMembers(members, only) {
|
|
|
12834
13248
|
|
|
12835
13249
|
// src/workspace/worktree/state.ts
|
|
12836
13250
|
init_fs();
|
|
12837
|
-
import
|
|
13251
|
+
import path65 from "node:path";
|
|
12838
13252
|
var WORKTREE_STATE_FILE = ".haus-worktree.json";
|
|
12839
13253
|
function worktreesDir(mainRoot) {
|
|
12840
|
-
return
|
|
13254
|
+
return path65.join(mainRoot, ".claude", "worktrees");
|
|
12841
13255
|
}
|
|
12842
13256
|
function isValidWorktreeSlug(slug) {
|
|
12843
13257
|
if (slug === "" || slug === "." || slug === "..") return false;
|
|
@@ -12850,15 +13264,15 @@ function worktreePath(mainRoot, slug) {
|
|
|
12850
13264
|
`Invalid worktree slug ${JSON.stringify(slug)} \u2014 slugs may only contain letters, digits, ".", "_", "-" (no path separators, and not "." or "..").`
|
|
12851
13265
|
);
|
|
12852
13266
|
}
|
|
12853
|
-
return
|
|
13267
|
+
return path65.join(worktreesDir(mainRoot), slug);
|
|
12854
13268
|
}
|
|
12855
13269
|
async function readWorktreeState(worktreeRoot) {
|
|
12856
|
-
const state = await readJson(
|
|
13270
|
+
const state = await readJson(path65.join(worktreeRoot, WORKTREE_STATE_FILE));
|
|
12857
13271
|
if (state == null || typeof state !== "object" || !Array.isArray(state.members)) return void 0;
|
|
12858
13272
|
return state;
|
|
12859
13273
|
}
|
|
12860
13274
|
async function writeWorktreeState(worktreeRoot, state) {
|
|
12861
|
-
await writeJson(
|
|
13275
|
+
await writeJson(path65.join(worktreeRoot, WORKTREE_STATE_FILE), state);
|
|
12862
13276
|
}
|
|
12863
13277
|
|
|
12864
13278
|
// src/workspace/worktree/add.ts
|
|
@@ -12892,7 +13306,7 @@ async function runAdd(opts) {
|
|
|
12892
13306
|
const memberResults = [];
|
|
12893
13307
|
for (const member of selected) {
|
|
12894
13308
|
const start = Date.now();
|
|
12895
|
-
const memberWorktreePath =
|
|
13309
|
+
const memberWorktreePath = path66.join(wsWorktreePath, member.folder);
|
|
12896
13310
|
const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
|
|
12897
13311
|
preferDefaultBranchFrom: true
|
|
12898
13312
|
});
|
|
@@ -12922,7 +13336,7 @@ async function runAdd(opts) {
|
|
|
12922
13336
|
await mapWithConcurrency(okResults, async (result) => {
|
|
12923
13337
|
const member = selected.find((m) => m.id === result.member);
|
|
12924
13338
|
if (!member) return;
|
|
12925
|
-
const memberWorktreePath =
|
|
13339
|
+
const memberWorktreePath = path66.join(wsWorktreePath, member.folder);
|
|
12926
13340
|
const hydrationStart = Date.now();
|
|
12927
13341
|
if (opts.hydrate !== false) {
|
|
12928
13342
|
result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
|
|
@@ -12989,25 +13403,25 @@ async function runDryRun(args) {
|
|
|
12989
13403
|
|
|
12990
13404
|
// src/workspace/worktree/doctor.ts
|
|
12991
13405
|
import { existsSync as existsSync6 } from "node:fs";
|
|
12992
|
-
import
|
|
13406
|
+
import path68 from "node:path";
|
|
12993
13407
|
|
|
12994
13408
|
// src/workspace/worktree/orphans.ts
|
|
12995
13409
|
import { existsSync as existsSync5 } from "node:fs";
|
|
12996
|
-
import
|
|
13410
|
+
import path67 from "node:path";
|
|
12997
13411
|
var WORKSPACE_REPO_ID = "(workspace)";
|
|
12998
13412
|
async function findOrphanedRegistrations(repos, wsWorktreesDir) {
|
|
12999
13413
|
const orphans = [];
|
|
13000
13414
|
for (const repo of repos) {
|
|
13001
13415
|
const entries = await listWorktrees(repo.absPath);
|
|
13002
13416
|
for (const entry of entries) {
|
|
13003
|
-
const entryPath =
|
|
13004
|
-
const marker = `${
|
|
13417
|
+
const entryPath = path67.normalize(entry.path);
|
|
13418
|
+
const marker = `${path67.sep}.claude${path67.sep}worktrees${path67.sep}`;
|
|
13005
13419
|
const idx = entryPath.indexOf(marker);
|
|
13006
13420
|
if (idx === -1) continue;
|
|
13007
13421
|
const rest = entryPath.slice(idx + marker.length);
|
|
13008
|
-
const slug = rest.split(
|
|
13422
|
+
const slug = rest.split(path67.sep)[0];
|
|
13009
13423
|
if (!slug) continue;
|
|
13010
|
-
if (existsSync5(
|
|
13424
|
+
if (existsSync5(path67.join(wsWorktreesDir, slug))) continue;
|
|
13011
13425
|
orphans.push({
|
|
13012
13426
|
repoId: repo.id,
|
|
13013
13427
|
ownerRepo: repo.absPath,
|
|
@@ -13022,7 +13436,7 @@ async function findOrphanedRegistrations(repos, wsWorktreesDir) {
|
|
|
13022
13436
|
// src/workspace/worktree/doctor.ts
|
|
13023
13437
|
var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
|
|
13024
13438
|
function looksHydrated(memberPath) {
|
|
13025
|
-
return HYDRATION_MARKERS.some((marker) => existsSync6(
|
|
13439
|
+
return HYDRATION_MARKERS.some((marker) => existsSync6(path68.join(memberPath, marker)));
|
|
13026
13440
|
}
|
|
13027
13441
|
async function runDoctor2() {
|
|
13028
13442
|
const rootInfo = await resolveRoots();
|
|
@@ -13038,7 +13452,7 @@ async function runDoctor2() {
|
|
|
13038
13452
|
};
|
|
13039
13453
|
}
|
|
13040
13454
|
const workspaceRoot = rootInfo.mainRoot;
|
|
13041
|
-
const isWorkspace = existsSync6(
|
|
13455
|
+
const isWorkspace = existsSync6(path68.join(workspaceRoot, WORKSPACE_FILE)) || existsSync6(path68.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
13042
13456
|
if (!isWorkspace) {
|
|
13043
13457
|
return {
|
|
13044
13458
|
isWorkspace: false,
|
|
@@ -13053,7 +13467,7 @@ async function runDoctor2() {
|
|
|
13053
13467
|
};
|
|
13054
13468
|
}
|
|
13055
13469
|
const wsWorktreesDir = worktreesDir(workspaceRoot);
|
|
13056
|
-
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${
|
|
13470
|
+
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path68.sep}`);
|
|
13057
13471
|
const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
|
|
13058
13472
|
const workspaceConfig = await readWorkspaceConfig(workspaceRoot);
|
|
13059
13473
|
const autoHydrate = workspaceConfig?.worktree?.autoHydrate === true;
|
|
@@ -13064,10 +13478,10 @@ async function runDoctor2() {
|
|
|
13064
13478
|
let slug;
|
|
13065
13479
|
if (inWorkspaceWorktree) {
|
|
13066
13480
|
const wtPath = rootInfo.repoRoot;
|
|
13067
|
-
slug = rootInfo.worktreeName ??
|
|
13481
|
+
slug = rootInfo.worktreeName ?? path68.basename(wtPath);
|
|
13068
13482
|
const state = await readWorktreeState(wtPath);
|
|
13069
13483
|
for (const member of members) {
|
|
13070
|
-
const memberPath =
|
|
13484
|
+
const memberPath = path68.join(wtPath, member.folder);
|
|
13071
13485
|
const materialized = existsSync6(memberPath);
|
|
13072
13486
|
const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
|
|
13073
13487
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
@@ -13107,7 +13521,7 @@ async function runDoctor2() {
|
|
|
13107
13521
|
wsWorktreesDir
|
|
13108
13522
|
);
|
|
13109
13523
|
const orphans = orphanScan.map(
|
|
13110
|
-
(o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${
|
|
13524
|
+
(o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${path68.join(wsWorktreesDir, o.slug)} no longer exists.`
|
|
13111
13525
|
);
|
|
13112
13526
|
if (orphans.length > 0) {
|
|
13113
13527
|
problems.push(...orphans);
|
|
@@ -13128,7 +13542,7 @@ async function runDoctor2() {
|
|
|
13128
13542
|
// src/workspace/worktree/hydrate-command.ts
|
|
13129
13543
|
init_fs();
|
|
13130
13544
|
import { existsSync as existsSync7 } from "node:fs";
|
|
13131
|
-
import
|
|
13545
|
+
import path69 from "node:path";
|
|
13132
13546
|
async function runHydrateWorktree(opts) {
|
|
13133
13547
|
const rootInfo = await resolveRoots();
|
|
13134
13548
|
if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
|
|
@@ -13139,7 +13553,7 @@ async function runHydrateWorktree(opts) {
|
|
|
13139
13553
|
}
|
|
13140
13554
|
const workspaceRoot = rootInfo.mainRoot;
|
|
13141
13555
|
const wtPath = rootInfo.repoRoot;
|
|
13142
|
-
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${
|
|
13556
|
+
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path69.sep}`)) {
|
|
13143
13557
|
return {
|
|
13144
13558
|
ok: false,
|
|
13145
13559
|
error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
|
|
@@ -13151,13 +13565,13 @@ async function runHydrateWorktree(opts) {
|
|
|
13151
13565
|
return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
|
|
13152
13566
|
}
|
|
13153
13567
|
const state = await readWorktreeState(wtPath);
|
|
13154
|
-
const slug = state?.slug ??
|
|
13568
|
+
const slug = state?.slug ?? path69.basename(wtPath);
|
|
13155
13569
|
const detectedBranch = await currentBranch(wtPath);
|
|
13156
13570
|
const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
|
|
13157
13571
|
if (opts.dryRun) {
|
|
13158
13572
|
const results2 = await Promise.all(
|
|
13159
13573
|
selected.map(async (member) => {
|
|
13160
|
-
const memberPath =
|
|
13574
|
+
const memberPath = path69.join(wtPath, member.folder);
|
|
13161
13575
|
if (existsSync7(memberPath)) {
|
|
13162
13576
|
return { member: member.id, folder: member.folder, status: "planned" };
|
|
13163
13577
|
}
|
|
@@ -13178,7 +13592,7 @@ async function runHydrateWorktree(opts) {
|
|
|
13178
13592
|
const results = await mapWithConcurrency(
|
|
13179
13593
|
selected,
|
|
13180
13594
|
async (member) => {
|
|
13181
|
-
const memberPath =
|
|
13595
|
+
const memberPath = path69.join(wtPath, member.folder);
|
|
13182
13596
|
let branchAction;
|
|
13183
13597
|
if (!existsSync7(memberPath)) {
|
|
13184
13598
|
if (!opts.materialize) {
|
|
@@ -13241,7 +13655,7 @@ async function runHydrateWorktree(opts) {
|
|
|
13241
13655
|
}
|
|
13242
13656
|
|
|
13243
13657
|
// src/workspace/worktree/list.ts
|
|
13244
|
-
import
|
|
13658
|
+
import path70 from "node:path";
|
|
13245
13659
|
import fs50 from "fs-extra";
|
|
13246
13660
|
async function runList2() {
|
|
13247
13661
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -13254,12 +13668,12 @@ async function runList2() {
|
|
|
13254
13668
|
const configuredMembers = await readMembers(rootInfo);
|
|
13255
13669
|
const worktrees = [];
|
|
13256
13670
|
for (const slug of slugs) {
|
|
13257
|
-
const wtPath =
|
|
13671
|
+
const wtPath = path70.join(dir, slug);
|
|
13258
13672
|
const state = await readWorktreeState(wtPath);
|
|
13259
13673
|
const members = [];
|
|
13260
13674
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
13261
13675
|
for (const m of source) {
|
|
13262
|
-
const memberPath =
|
|
13676
|
+
const memberPath = path70.join(wtPath, m.folder);
|
|
13263
13677
|
const materialized = await fs50.pathExists(memberPath);
|
|
13264
13678
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
13265
13679
|
members.push({
|
|
@@ -13321,7 +13735,7 @@ async function runPrune2(opts) {
|
|
|
13321
13735
|
}
|
|
13322
13736
|
|
|
13323
13737
|
// src/workspace/worktree/remove.ts
|
|
13324
|
-
import
|
|
13738
|
+
import path71 from "node:path";
|
|
13325
13739
|
import fs51 from "fs-extra";
|
|
13326
13740
|
async function runRemove2(opts) {
|
|
13327
13741
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -13348,7 +13762,7 @@ async function runRemove2(opts) {
|
|
|
13348
13762
|
targets.push({
|
|
13349
13763
|
repo: configMember?.id ?? stateMember.id,
|
|
13350
13764
|
ownerRepo,
|
|
13351
|
-
path:
|
|
13765
|
+
path: path71.join(wsWorktreePath, stateMember.folder)
|
|
13352
13766
|
});
|
|
13353
13767
|
}
|
|
13354
13768
|
const existingTargets = [];
|
|
@@ -13686,7 +14100,7 @@ async function runWorktree(action, opts = {}) {
|
|
|
13686
14100
|
|
|
13687
14101
|
// src/commands/workspace.ts
|
|
13688
14102
|
import { existsSync as existsSync11, statSync as statSync3 } from "node:fs";
|
|
13689
|
-
import
|
|
14103
|
+
import path80 from "node:path";
|
|
13690
14104
|
init_fs();
|
|
13691
14105
|
init_logger();
|
|
13692
14106
|
|
|
@@ -13745,7 +14159,7 @@ ${summaries.map(
|
|
|
13745
14159
|
|
|
13746
14160
|
// src/commands/workspace/doctor.ts
|
|
13747
14161
|
import { existsSync as existsSync8 } from "node:fs";
|
|
13748
|
-
import
|
|
14162
|
+
import path73 from "node:path";
|
|
13749
14163
|
init_logger();
|
|
13750
14164
|
init_paths();
|
|
13751
14165
|
|
|
@@ -13753,14 +14167,14 @@ init_paths();
|
|
|
13753
14167
|
init_fs();
|
|
13754
14168
|
init_paths();
|
|
13755
14169
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
13756
|
-
import
|
|
14170
|
+
import path72 from "node:path";
|
|
13757
14171
|
var MANIFEST_FILE2 = "workspace.manifest.json";
|
|
13758
14172
|
function manifestPath(workspaceRoot) {
|
|
13759
14173
|
return hausPath(workspaceRoot, MANIFEST_FILE2);
|
|
13760
14174
|
}
|
|
13761
14175
|
function hausVersion() {
|
|
13762
14176
|
try {
|
|
13763
|
-
const pkg = JSON.parse(readFileSync4(
|
|
14177
|
+
const pkg = JSON.parse(readFileSync4(path72.join(packageRoot(), "package.json"), "utf8"));
|
|
13764
14178
|
return pkg.version ?? "0.0.0";
|
|
13765
14179
|
} catch {
|
|
13766
14180
|
return "0.0.0";
|
|
@@ -13852,7 +14266,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
13852
14266
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
13853
14267
|
const catalogRefByRepo = [];
|
|
13854
14268
|
for (const repo of config.repos) {
|
|
13855
|
-
const repoRoot =
|
|
14269
|
+
const repoRoot = path73.resolve(workspaceRoot, repo.path);
|
|
13856
14270
|
const entry = manifestByName.get(repo.name);
|
|
13857
14271
|
if (!entry) {
|
|
13858
14272
|
flag({
|
|
@@ -13925,7 +14339,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
13925
14339
|
});
|
|
13926
14340
|
continue;
|
|
13927
14341
|
}
|
|
13928
|
-
if (!existsSync8(
|
|
14342
|
+
if (!existsSync8(path73.join(workspaceRoot, entry.path))) {
|
|
13929
14343
|
flag({
|
|
13930
14344
|
repo: entry.repo,
|
|
13931
14345
|
kind: "missing-linked-context-copy",
|
|
@@ -13971,17 +14385,17 @@ function emit(args) {
|
|
|
13971
14385
|
init_logger();
|
|
13972
14386
|
|
|
13973
14387
|
// src/workspace/link-context/link.ts
|
|
13974
|
-
import
|
|
14388
|
+
import path76 from "node:path";
|
|
13975
14389
|
import fs54 from "fs-extra";
|
|
13976
14390
|
init_fs();
|
|
13977
14391
|
init_logger();
|
|
13978
14392
|
|
|
13979
14393
|
// src/workspace/link-context/plan.ts
|
|
13980
|
-
import
|
|
14394
|
+
import path75 from "node:path";
|
|
13981
14395
|
import fs53 from "fs-extra";
|
|
13982
14396
|
|
|
13983
14397
|
// src/workspace/link-context/scan-source.ts
|
|
13984
|
-
import
|
|
14398
|
+
import path74 from "node:path";
|
|
13985
14399
|
import fs52 from "fs-extra";
|
|
13986
14400
|
var ASSET_SUBDIR = {
|
|
13987
14401
|
skill: "skills",
|
|
@@ -13990,36 +14404,36 @@ var ASSET_SUBDIR = {
|
|
|
13990
14404
|
};
|
|
13991
14405
|
async function hasLinkableContext(member) {
|
|
13992
14406
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
13993
|
-
if (await fs52.pathExists(
|
|
14407
|
+
if (await fs52.pathExists(path74.join(member.absPath, ".claude", subdir))) return true;
|
|
13994
14408
|
}
|
|
13995
14409
|
return false;
|
|
13996
14410
|
}
|
|
13997
14411
|
async function collectSkills(member) {
|
|
13998
|
-
const dir =
|
|
14412
|
+
const dir = path74.join(member.absPath, ".claude", "skills");
|
|
13999
14413
|
if (!await fs52.pathExists(dir)) return [];
|
|
14000
14414
|
const names = await fs52.readdir(dir);
|
|
14001
14415
|
const out = [];
|
|
14002
14416
|
for (const name of [...names].sort()) {
|
|
14003
|
-
const abs =
|
|
14417
|
+
const abs = path74.join(dir, name);
|
|
14004
14418
|
const stat = await fs52.lstat(abs);
|
|
14005
14419
|
if (!stat.isDirectory()) continue;
|
|
14006
|
-
if (!await fs52.pathExists(
|
|
14007
|
-
out.push({ type: "skill", name, sourceRelPath:
|
|
14420
|
+
if (!await fs52.pathExists(path74.join(abs, "SKILL.md"))) continue;
|
|
14421
|
+
out.push({ type: "skill", name, sourceRelPath: path74.posix.join(".claude", "skills", name) });
|
|
14008
14422
|
}
|
|
14009
14423
|
return out;
|
|
14010
14424
|
}
|
|
14011
14425
|
async function collectFlatMarkdown(member, type) {
|
|
14012
14426
|
const subdir = ASSET_SUBDIR[type];
|
|
14013
|
-
const dir =
|
|
14427
|
+
const dir = path74.join(member.absPath, ".claude", subdir);
|
|
14014
14428
|
if (!await fs52.pathExists(dir)) return [];
|
|
14015
14429
|
const names = await fs52.readdir(dir);
|
|
14016
14430
|
const out = [];
|
|
14017
14431
|
for (const fileName of [...names].sort()) {
|
|
14018
14432
|
if (!fileName.endsWith(".md")) continue;
|
|
14019
|
-
const abs =
|
|
14433
|
+
const abs = path74.join(dir, fileName);
|
|
14020
14434
|
if (!(await fs52.lstat(abs)).isFile()) continue;
|
|
14021
14435
|
const name = fileName.slice(0, -".md".length);
|
|
14022
|
-
out.push({ type, name, sourceRelPath:
|
|
14436
|
+
out.push({ type, name, sourceRelPath: path74.posix.join(".claude", subdir, fileName) });
|
|
14023
14437
|
}
|
|
14024
14438
|
return out;
|
|
14025
14439
|
}
|
|
@@ -14034,9 +14448,9 @@ async function collectSourceAssets(member) {
|
|
|
14034
14448
|
|
|
14035
14449
|
// src/workspace/link-context/plan.ts
|
|
14036
14450
|
function repoFolderPrefix(member) {
|
|
14037
|
-
const base =
|
|
14451
|
+
const base = path75.posix.basename(member.folder);
|
|
14038
14452
|
if (base && base !== "." && base !== "/") return base;
|
|
14039
|
-
const fromAbs =
|
|
14453
|
+
const fromAbs = path75.basename(member.absPath);
|
|
14040
14454
|
return fromAbs || member.id;
|
|
14041
14455
|
}
|
|
14042
14456
|
function assetKey(type, destKey) {
|
|
@@ -14093,7 +14507,7 @@ async function buildLinkPlan(members) {
|
|
|
14093
14507
|
name: asset.name,
|
|
14094
14508
|
destKey,
|
|
14095
14509
|
sourceRelPath: asset.sourceRelPath,
|
|
14096
|
-
sourceAbsPath:
|
|
14510
|
+
sourceAbsPath: path75.join(member.absPath, asset.sourceRelPath),
|
|
14097
14511
|
sourceHash
|
|
14098
14512
|
});
|
|
14099
14513
|
}
|
|
@@ -14104,7 +14518,7 @@ async function buildLinkPlan(members) {
|
|
|
14104
14518
|
function destRelPathFor(type, destKey) {
|
|
14105
14519
|
const subdir = `${type}s`;
|
|
14106
14520
|
const leaf = type === "skill" ? destKey : `${destKey}.md`;
|
|
14107
|
-
return
|
|
14521
|
+
return path76.posix.join(".claude", subdir, leaf);
|
|
14108
14522
|
}
|
|
14109
14523
|
async function runLinkContext(workspaceRoot, opts = {}) {
|
|
14110
14524
|
const write = resolveWriteMode(opts, "preview-by-default").kind === "write";
|
|
@@ -14130,9 +14544,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
14130
14544
|
const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
14131
14545
|
for (const entry of plan.entries) {
|
|
14132
14546
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
14133
|
-
const destAbs =
|
|
14547
|
+
const destAbs = path76.join(workspaceRoot, destRel);
|
|
14134
14548
|
if (write) {
|
|
14135
|
-
await fs54.ensureDir(
|
|
14549
|
+
await fs54.ensureDir(path76.dirname(destAbs));
|
|
14136
14550
|
if (await fs54.pathExists(destAbs)) await fs54.remove(destAbs);
|
|
14137
14551
|
await fs54.copy(entry.sourceAbsPath, destAbs, {
|
|
14138
14552
|
overwrite: true,
|
|
@@ -14221,10 +14635,10 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
|
|
|
14221
14635
|
|
|
14222
14636
|
// src/commands/workspace/setup.ts
|
|
14223
14637
|
import { existsSync as existsSync9, statSync } from "node:fs";
|
|
14224
|
-
import
|
|
14638
|
+
import path78 from "node:path";
|
|
14225
14639
|
|
|
14226
14640
|
// src/claude/write-workspace-claude-md.ts
|
|
14227
|
-
import
|
|
14641
|
+
import path77 from "node:path";
|
|
14228
14642
|
import fs55 from "fs-extra";
|
|
14229
14643
|
init_fs();
|
|
14230
14644
|
init_logger();
|
|
@@ -14238,16 +14652,19 @@ function buildWorkspaceImportBlock(client, members) {
|
|
|
14238
14652
|
`# Workspace: ${client}`,
|
|
14239
14653
|
"",
|
|
14240
14654
|
"Member repos:",
|
|
14655
|
+
"",
|
|
14241
14656
|
...memberLines
|
|
14242
14657
|
].join("\n");
|
|
14243
14658
|
return `${WORKSPACE_BLOCK_BEGIN}
|
|
14659
|
+
|
|
14244
14660
|
${body}
|
|
14661
|
+
|
|
14245
14662
|
${WORKSPACE_BLOCK_END}`;
|
|
14246
14663
|
}
|
|
14247
14664
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
14248
14665
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
14249
14666
|
const dryRun = opts.dryRun ?? false;
|
|
14250
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
14667
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path77.join(workspaceRoot, "CLAUDE.md");
|
|
14251
14668
|
const say = opts.quiet ? () => {
|
|
14252
14669
|
} : log;
|
|
14253
14670
|
const existed = await fs55.pathExists(filePath);
|
|
@@ -14280,20 +14697,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
14280
14697
|
init_fs();
|
|
14281
14698
|
init_logger();
|
|
14282
14699
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
14283
|
-
let dir =
|
|
14700
|
+
let dir = path78.resolve(start);
|
|
14284
14701
|
for (; ; ) {
|
|
14285
|
-
if (existsSync9(
|
|
14286
|
-
const parent =
|
|
14287
|
-
if (parent === dir) return
|
|
14702
|
+
if (existsSync9(path78.join(dir, WORKSPACE_FILE))) return dir;
|
|
14703
|
+
const parent = path78.dirname(dir);
|
|
14704
|
+
if (parent === dir) return path78.resolve(start);
|
|
14288
14705
|
dir = parent;
|
|
14289
14706
|
}
|
|
14290
14707
|
}
|
|
14291
14708
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
14292
|
-
return
|
|
14709
|
+
return path78.resolve(workspaceRoot, repoPath) === path78.resolve(workspaceRoot);
|
|
14293
14710
|
}
|
|
14294
14711
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
14295
14712
|
const apply = options.write ?? false;
|
|
14296
|
-
const configText = await readText(
|
|
14713
|
+
const configText = await readText(path78.join(workspaceRoot, WORKSPACE_FILE));
|
|
14297
14714
|
if (!configText) {
|
|
14298
14715
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
14299
14716
|
process.exitCode = 1;
|
|
@@ -14320,7 +14737,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
14320
14737
|
const statuses = [];
|
|
14321
14738
|
const aggregateInputs = [];
|
|
14322
14739
|
for (const repo of repos) {
|
|
14323
|
-
const repoRoot =
|
|
14740
|
+
const repoRoot = path78.resolve(workspaceRoot, repo.path);
|
|
14324
14741
|
say(`
|
|
14325
14742
|
\u2192 ${repo.name} (${repo.path})`);
|
|
14326
14743
|
let res;
|
|
@@ -14436,7 +14853,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
14436
14853
|
const status = statusByName.get(repo.name);
|
|
14437
14854
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
14438
14855
|
if (status?.status === "ok") {
|
|
14439
|
-
const lock = await checkLock(
|
|
14856
|
+
const lock = await checkLock(path78.resolve(workspaceRoot, repo.path));
|
|
14440
14857
|
manifestRepos.push({
|
|
14441
14858
|
name: repo.name,
|
|
14442
14859
|
path: repo.path,
|
|
@@ -14492,7 +14909,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
14492
14909
|
try {
|
|
14493
14910
|
const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
|
|
14494
14911
|
if (linkResult.ok) {
|
|
14495
|
-
written.push(...linkResult.added.map((rel) =>
|
|
14912
|
+
written.push(...linkResult.added.map((rel) => path78.join(workspaceRoot, rel)));
|
|
14496
14913
|
if (!options.json) {
|
|
14497
14914
|
say(
|
|
14498
14915
|
`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` : ""}.`
|
|
@@ -14525,7 +14942,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
14525
14942
|
|
|
14526
14943
|
// src/commands/workspace/undo.ts
|
|
14527
14944
|
import { existsSync as existsSync10, statSync as statSync2 } from "node:fs";
|
|
14528
|
-
import
|
|
14945
|
+
import path79 from "node:path";
|
|
14529
14946
|
import fs56 from "fs-extra";
|
|
14530
14947
|
init_fs();
|
|
14531
14948
|
init_logger();
|
|
@@ -14556,7 +14973,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
14556
14973
|
}
|
|
14557
14974
|
if (options.dryRun) {
|
|
14558
14975
|
for (const repo of config.repos) {
|
|
14559
|
-
const repoRoot =
|
|
14976
|
+
const repoRoot = path79.resolve(workspaceRoot, repo.path);
|
|
14560
14977
|
log(`
|
|
14561
14978
|
\u2192 ${repo.name} (${repo.path})`);
|
|
14562
14979
|
try {
|
|
@@ -14588,11 +15005,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
14588
15005
|
];
|
|
14589
15006
|
for (const abs of workspaceArtifacts) {
|
|
14590
15007
|
if (await fs56.pathExists(abs)) {
|
|
14591
|
-
log(`[dry-run] Would remove ${
|
|
15008
|
+
log(`[dry-run] Would remove ${path79.relative(workspaceRoot, abs)}`);
|
|
14592
15009
|
}
|
|
14593
15010
|
}
|
|
14594
15011
|
if (!await fs56.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
|
|
14595
|
-
const claudeMd = await readText(
|
|
15012
|
+
const claudeMd = await readText(path79.join(workspaceRoot, "CLAUDE.md"));
|
|
14596
15013
|
if (claudeMd?.includes(WORKSPACE_BLOCK_BEGIN) || claudeMd && hasLegacyWorkspaceBlock(claudeMd)) {
|
|
14597
15014
|
log("[dry-run] Would remove the workspace haus import block from CLAUDE.md");
|
|
14598
15015
|
}
|
|
@@ -14611,7 +15028,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
14611
15028
|
}
|
|
14612
15029
|
let anyRepoFailed = false;
|
|
14613
15030
|
for (const repo of config.repos) {
|
|
14614
|
-
const repoRoot =
|
|
15031
|
+
const repoRoot = path79.resolve(workspaceRoot, repo.path);
|
|
14615
15032
|
log(`
|
|
14616
15033
|
\u2192 ${repo.name} (${repo.path})`);
|
|
14617
15034
|
try {
|
|
@@ -14640,20 +15057,20 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
14640
15057
|
const abs = hausPath(workspaceRoot, rel);
|
|
14641
15058
|
if (await fs56.pathExists(abs)) {
|
|
14642
15059
|
await fs56.remove(abs);
|
|
14643
|
-
log(`Removed ${
|
|
15060
|
+
log(`Removed ${path79.relative(workspaceRoot, abs)}`);
|
|
14644
15061
|
}
|
|
14645
15062
|
}
|
|
14646
15063
|
const manifestFile = manifestPath(workspaceRoot);
|
|
14647
15064
|
if (await fs56.pathExists(manifestFile)) {
|
|
14648
15065
|
await fs56.remove(manifestFile);
|
|
14649
|
-
log(`Removed ${
|
|
15066
|
+
log(`Removed ${path79.relative(workspaceRoot, manifestFile)}`);
|
|
14650
15067
|
}
|
|
14651
15068
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
14652
15069
|
if (await fs56.pathExists(workspaceMdPath)) {
|
|
14653
15070
|
await fs56.remove(workspaceMdPath);
|
|
14654
|
-
log(`Removed ${
|
|
15071
|
+
log(`Removed ${path79.relative(workspaceRoot, workspaceMdPath)}`);
|
|
14655
15072
|
} else {
|
|
14656
|
-
const claudeMdPath =
|
|
15073
|
+
const claudeMdPath = path79.join(workspaceRoot, "CLAUDE.md");
|
|
14657
15074
|
if (existsSync10(claudeMdPath)) {
|
|
14658
15075
|
const prev = await readText(claudeMdPath);
|
|
14659
15076
|
if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
|
|
@@ -14722,7 +15139,7 @@ relationships: []
|
|
|
14722
15139
|
log("Workspace initialized.");
|
|
14723
15140
|
}
|
|
14724
15141
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
14725
|
-
const configText = await readText(
|
|
15142
|
+
const configText = await readText(path80.join(workspaceRoot, WORKSPACE_FILE));
|
|
14726
15143
|
if (!configText) {
|
|
14727
15144
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
14728
15145
|
process.exitCode = 1;
|
|
@@ -14744,7 +15161,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
14744
15161
|
const inputs = [];
|
|
14745
15162
|
const failures = [];
|
|
14746
15163
|
for (const repo of config.repos) {
|
|
14747
|
-
const repoRoot =
|
|
15164
|
+
const repoRoot = path80.resolve(workspaceRoot, repo.path);
|
|
14748
15165
|
try {
|
|
14749
15166
|
if (!existsSync11(repoRoot) || !statSync3(repoRoot).isDirectory()) {
|
|
14750
15167
|
throw new Error("path is not a directory");
|
|
@@ -14813,7 +15230,7 @@ init_logger();
|
|
|
14813
15230
|
init_paths();
|
|
14814
15231
|
function cliVersion() {
|
|
14815
15232
|
try {
|
|
14816
|
-
const pkgPath =
|
|
15233
|
+
const pkgPath = path81.join(packageRoot(), "package.json");
|
|
14817
15234
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
14818
15235
|
return pkg.version ?? "0.0.0";
|
|
14819
15236
|
} catch {
|
|
@@ -14823,7 +15240,7 @@ function cliVersion() {
|
|
|
14823
15240
|
var program = new Command();
|
|
14824
15241
|
function validateRuntimeNodeVersion() {
|
|
14825
15242
|
try {
|
|
14826
|
-
const pkgPath =
|
|
15243
|
+
const pkgPath = path81.join(packageRoot(), "package.json");
|
|
14827
15244
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
14828
15245
|
const requiredRange = pkg.engines?.node;
|
|
14829
15246
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|