@haus-tech/haus-workflow 4.5.0 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/cli.js +871 -429
- package/library/catalog/manifest.json +6 -15
- package/library/global/agents/request-assessor.md +44 -0
- package/library/global/agents/request-classifier.md +55 -0
- package/library/global/agents/request-scorer.md +54 -0
- package/library/global/agents/request-specifier-eskalerad.md +43 -0
- package/library/global/agents/request-specifier.md +44 -0
- package/library/global/skills/request/SKILL.md +446 -0
- package/library/global/skills/request/scripts/compare-classifications.mjs +94 -0
- package/library/global/skills/request/scripts/compare-scores.mjs +50 -0
- package/library/global/skills/request/scripts/contract-check.mjs +334 -0
- package/library/global/skills/request/scripts/lib.mjs +113 -0
- package/library/global/skills/request/scripts/precheck.mjs +217 -0
- package/library/global/skills/request/scripts/ref-hash.mjs +22 -0
- package/package.json +2 -1
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,11 +6525,132 @@ 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, " "));
|
|
@@ -6509,17 +6665,9 @@ function inlineCodeSpanRanges(line2) {
|
|
|
6509
6665
|
}
|
|
6510
6666
|
async function findUnresolvedSkillReferences(root, options = {}) {
|
|
6511
6667
|
const resolvableNamespaces = new Set(options.pluginNamespaces ?? []);
|
|
6512
|
-
const
|
|
6513
|
-
if (!await fs30.pathExists(skillsRoot)) return [];
|
|
6514
|
-
const skillDirs = (await fs30.readdir(skillsRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory() && entry.name !== "shared").map((entry) => entry.name);
|
|
6515
|
-
const installedSkillNames = new Set(skillDirs);
|
|
6516
|
-
const files = await fg4("*/**/*.md", { cwd: skillsRoot, onlyFiles: true, dot: true });
|
|
6668
|
+
const installedSkillNames = await readInstalledSkillNames(root);
|
|
6517
6669
|
const issues = [];
|
|
6518
|
-
for (const
|
|
6519
|
-
if (rel.startsWith("shared/")) continue;
|
|
6520
|
-
const abs = path32.join(skillsRoot, rel);
|
|
6521
|
-
const raw = await fs30.readFile(abs, "utf8");
|
|
6522
|
-
const lines = stripFencedCodeBlocks(raw).split("\n");
|
|
6670
|
+
for (const { file, lines } of await readScannedMarkdown(root)) {
|
|
6523
6671
|
lines.forEach((line2, idx) => {
|
|
6524
6672
|
const codeSpans = inlineCodeSpanRanges(line2);
|
|
6525
6673
|
for (const match of line2.matchAll(REFERENCE_PATTERN)) {
|
|
@@ -6531,7 +6679,7 @@ async function findUnresolvedSkillReferences(root, options = {}) {
|
|
|
6531
6679
|
);
|
|
6532
6680
|
if (inCodeSpan && !installedSkillNames.has(bareName)) continue;
|
|
6533
6681
|
issues.push({
|
|
6534
|
-
file
|
|
6682
|
+
file,
|
|
6535
6683
|
line: idx + 1,
|
|
6536
6684
|
reference,
|
|
6537
6685
|
bareName,
|
|
@@ -6542,17 +6690,83 @@ async function findUnresolvedSkillReferences(root, options = {}) {
|
|
|
6542
6690
|
}
|
|
6543
6691
|
return issues;
|
|
6544
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
|
+
}
|
|
6545
6759
|
|
|
6546
6760
|
// src/decisions/doctor.ts
|
|
6547
6761
|
init_fs();
|
|
6548
6762
|
import path37 from "node:path";
|
|
6549
|
-
import
|
|
6763
|
+
import fs35 from "fs-extra";
|
|
6550
6764
|
|
|
6551
6765
|
// src/decisions/check.ts
|
|
6552
6766
|
init_fs();
|
|
6553
6767
|
init_paths();
|
|
6554
6768
|
import path36 from "node:path";
|
|
6555
|
-
import
|
|
6769
|
+
import fs34 from "fs-extra";
|
|
6556
6770
|
|
|
6557
6771
|
// src/decisions/dep-bump.ts
|
|
6558
6772
|
var DEP_SECTIONS = [
|
|
@@ -6613,6 +6827,32 @@ function isDependencyBumpOnly(beforeText, afterText) {
|
|
|
6613
6827
|
init_exec();
|
|
6614
6828
|
init_fs();
|
|
6615
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
|
+
}
|
|
6616
6856
|
function isPackageJsonPath(file) {
|
|
6617
6857
|
return file === "package.json" || file.endsWith("/package.json");
|
|
6618
6858
|
}
|
|
@@ -6680,18 +6920,9 @@ async function collectDiffStats(root, opts) {
|
|
|
6680
6920
|
const gitArgs = opts.staged ? ["diff", "--cached", "--name-only"] : ["diff", "--name-only", opts.range ?? "HEAD"];
|
|
6681
6921
|
const names = await runGit(gitArgs, { cwd: root });
|
|
6682
6922
|
const files = names.stdout.split("\n").map((f) => f.trim()).filter(Boolean);
|
|
6683
|
-
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"];
|
|
6684
6924
|
const numstat = await runGit(numstatArgs, { cwd: root });
|
|
6685
|
-
|
|
6686
|
-
let linesRemoved = 0;
|
|
6687
|
-
for (const line2 of numstat.stdout.split("\n")) {
|
|
6688
|
-
const parts = line2.trim().split(/\s+/);
|
|
6689
|
-
if (parts.length < 3) continue;
|
|
6690
|
-
const added = parts[0] === "-" ? 0 : Number(parts[0]);
|
|
6691
|
-
const removed = parts[1] === "-" ? 0 : Number(parts[1]);
|
|
6692
|
-
if (!Number.isNaN(added)) linesAdded += added;
|
|
6693
|
-
if (!Number.isNaN(removed)) linesRemoved += removed;
|
|
6694
|
-
}
|
|
6925
|
+
const { linesByFile, linesAdded, linesRemoved } = parseNumstat(numstat.stdout);
|
|
6695
6926
|
const packageJsonPairs = {};
|
|
6696
6927
|
for (const file of files) {
|
|
6697
6928
|
if (!isPackageJsonPath(file)) continue;
|
|
@@ -6701,6 +6932,7 @@ async function collectDiffStats(root, opts) {
|
|
|
6701
6932
|
files,
|
|
6702
6933
|
linesAdded,
|
|
6703
6934
|
linesRemoved,
|
|
6935
|
+
linesByFile,
|
|
6704
6936
|
...Object.keys(packageJsonPairs).length > 0 ? { packageJsonPairs } : {}
|
|
6705
6937
|
};
|
|
6706
6938
|
}
|
|
@@ -6745,12 +6977,12 @@ function firstMatchingRegex(filePath, patterns) {
|
|
|
6745
6977
|
|
|
6746
6978
|
// src/decisions/paths.ts
|
|
6747
6979
|
import path34 from "node:path";
|
|
6748
|
-
import
|
|
6980
|
+
import fs32 from "fs-extra";
|
|
6749
6981
|
async function resolveDecisionsDir(root) {
|
|
6750
6982
|
const primary = path34.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
6751
|
-
if (await
|
|
6983
|
+
if (await fs32.pathExists(primary)) return primary;
|
|
6752
6984
|
const legacy = path34.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
6753
|
-
if (await
|
|
6985
|
+
if (await fs32.pathExists(legacy)) return legacy;
|
|
6754
6986
|
return primary;
|
|
6755
6987
|
}
|
|
6756
6988
|
function relativeDecisionPath(decisionsDir, root) {
|
|
@@ -6760,7 +6992,7 @@ function relativeDecisionPath(decisionsDir, root) {
|
|
|
6760
6992
|
// src/decisions/validate.ts
|
|
6761
6993
|
init_fs();
|
|
6762
6994
|
import path35 from "node:path";
|
|
6763
|
-
import
|
|
6995
|
+
import fs33 from "fs-extra";
|
|
6764
6996
|
function validateDecisionContent(content2) {
|
|
6765
6997
|
const errors = [];
|
|
6766
6998
|
for (const section of DECISIONS_TRIGGERS.requiredSections) {
|
|
@@ -6790,7 +7022,7 @@ function decisionNumberFromFile(name) {
|
|
|
6790
7022
|
}
|
|
6791
7023
|
async function validateIndexSync(decisionsDir, decisionFiles) {
|
|
6792
7024
|
const readmePath = path35.join(decisionsDir, "README.md");
|
|
6793
|
-
if (!await
|
|
7025
|
+
if (!await fs33.pathExists(readmePath)) {
|
|
6794
7026
|
return ["docs/decisions/README.md is missing"];
|
|
6795
7027
|
}
|
|
6796
7028
|
const readme = await readText(readmePath) ?? "";
|
|
@@ -6846,6 +7078,11 @@ function countTopLevelDirs(files) {
|
|
|
6846
7078
|
function firstMatchingGlob(file, globs) {
|
|
6847
7079
|
return globs.find((g) => matchesPathGlob(file, g));
|
|
6848
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
|
+
}
|
|
6849
7086
|
function explainDecisionTrigger(stats) {
|
|
6850
7087
|
let relevant = stats.files.filter((f) => !isExempt(f));
|
|
6851
7088
|
if (relevant.length === 0) return [];
|
|
@@ -6863,7 +7100,7 @@ function explainDecisionTrigger(stats) {
|
|
|
6863
7100
|
return [`matched pathRegex: ${regex}`];
|
|
6864
7101
|
}
|
|
6865
7102
|
}
|
|
6866
|
-
const totalLines = stats
|
|
7103
|
+
const totalLines = relevantLineCount(stats, relevant);
|
|
6867
7104
|
const dirs = countTopLevelDirs(relevant);
|
|
6868
7105
|
if (relevant.length >= DECISIONS_TRIGGERS.minFilesChanged && dirs >= 2) {
|
|
6869
7106
|
return [`size threshold: ${relevant.length} files across ${dirs} top-level dirs`];
|
|
@@ -6962,8 +7199,8 @@ ${opts.commitMessages ?? ""}`;
|
|
|
6962
7199
|
changedFiles: stats.files
|
|
6963
7200
|
});
|
|
6964
7201
|
if (satisfactionErrors.length > 0 && decisionsRel !== legacyRel) {
|
|
6965
|
-
const legacyExists = await
|
|
6966
|
-
if (legacyExists && !await
|
|
7202
|
+
const legacyExists = await fs34.pathExists(path36.join(root, legacyRel));
|
|
7203
|
+
if (legacyExists && !await fs34.pathExists(decisionsDir)) {
|
|
6967
7204
|
satisfactionErrors = await checkSatisfaction({
|
|
6968
7205
|
root,
|
|
6969
7206
|
decisionsRel: legacyRel,
|
|
@@ -6985,8 +7222,8 @@ ${opts.commitMessages ?? ""}`;
|
|
|
6985
7222
|
return { triggered: true, satisfied: false, reasons, securityTriggered };
|
|
6986
7223
|
}
|
|
6987
7224
|
async function listDecisionNumbers(decisionsDir) {
|
|
6988
|
-
if (!await
|
|
6989
|
-
const entries = await
|
|
7225
|
+
if (!await fs34.pathExists(decisionsDir)) return [];
|
|
7226
|
+
const entries = await fs34.readdir(decisionsDir);
|
|
6990
7227
|
return entries.map((name) => decisionNumberFromFile(name)).filter((n) => n != null).map((n) => Number(n));
|
|
6991
7228
|
}
|
|
6992
7229
|
|
|
@@ -6995,8 +7232,8 @@ async function auditDecisionsLayout(root) {
|
|
|
6995
7232
|
const findings = [];
|
|
6996
7233
|
const primary = path37.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
6997
7234
|
const legacy = path37.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
6998
|
-
const hasPrimary = await
|
|
6999
|
-
const hasLegacy = await
|
|
7235
|
+
const hasPrimary = await fs35.pathExists(primary);
|
|
7236
|
+
const hasLegacy = await fs35.pathExists(legacy);
|
|
7000
7237
|
if (!hasPrimary && hasLegacy) {
|
|
7001
7238
|
findings.push({
|
|
7002
7239
|
level: "advisory",
|
|
@@ -7010,7 +7247,7 @@ async function auditDecisionsLayout(root) {
|
|
|
7010
7247
|
const decisionsDir = await resolveDecisionsDir(root);
|
|
7011
7248
|
const rel = relativeDecisionPath(decisionsDir, root);
|
|
7012
7249
|
const readmePath = path37.join(decisionsDir, "README.md");
|
|
7013
|
-
if (!await
|
|
7250
|
+
if (!await fs35.pathExists(readmePath)) {
|
|
7014
7251
|
findings.push({
|
|
7015
7252
|
level: "advisory",
|
|
7016
7253
|
line: `- DECISIONS: missing ${rel}/README.md index`,
|
|
@@ -7020,7 +7257,7 @@ async function auditDecisionsLayout(root) {
|
|
|
7020
7257
|
return findings;
|
|
7021
7258
|
}
|
|
7022
7259
|
const readme = await readText(readmePath);
|
|
7023
|
-
const entries = (await
|
|
7260
|
+
const entries = (await fs35.readdir(decisionsDir)).filter(
|
|
7024
7261
|
(f) => f.endsWith(".md") && f !== "README.md"
|
|
7025
7262
|
);
|
|
7026
7263
|
for (const file of entries) {
|
|
@@ -7069,7 +7306,7 @@ init_fs();
|
|
|
7069
7306
|
init_logger();
|
|
7070
7307
|
init_paths();
|
|
7071
7308
|
import path38 from "node:path";
|
|
7072
|
-
import
|
|
7309
|
+
import fs36 from "fs-extra";
|
|
7073
7310
|
function manifestPathIn(configDir) {
|
|
7074
7311
|
return path38.join(configDir, "haus", "install-manifest.json");
|
|
7075
7312
|
}
|
|
@@ -7099,9 +7336,9 @@ async function pruneVacatedDirs(root, vacatedFiles) {
|
|
|
7099
7336
|
async function destinationWritable(dir) {
|
|
7100
7337
|
let candidate = path38.resolve(dir);
|
|
7101
7338
|
for (; ; ) {
|
|
7102
|
-
if (await
|
|
7339
|
+
if (await fs36.pathExists(candidate)) {
|
|
7103
7340
|
try {
|
|
7104
|
-
await
|
|
7341
|
+
await fs36.access(candidate, fs36.constants.W_OK);
|
|
7105
7342
|
return true;
|
|
7106
7343
|
} catch (err) {
|
|
7107
7344
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -7133,7 +7370,7 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
7133
7370
|
if (!legacy || !Array.isArray(legacy.files)) {
|
|
7134
7371
|
return { ...base, reason: "no install manifest under ~/.claude" };
|
|
7135
7372
|
}
|
|
7136
|
-
if (await
|
|
7373
|
+
if (await fs36.pathExists(manifestPathIn(to))) {
|
|
7137
7374
|
return { ...base, reason: "an install manifest already exists in the resolved config dir" };
|
|
7138
7375
|
}
|
|
7139
7376
|
const writable = await destinationWritable(to);
|
|
@@ -7156,14 +7393,14 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
7156
7393
|
continue;
|
|
7157
7394
|
}
|
|
7158
7395
|
const newDest = path38.join(to, rel);
|
|
7159
|
-
if (!await
|
|
7396
|
+
if (!await fs36.pathExists(entry.destPath)) {
|
|
7160
7397
|
rewritten.push({ ...entry, destPath: newDest });
|
|
7161
7398
|
continue;
|
|
7162
7399
|
}
|
|
7163
|
-
if (await
|
|
7400
|
+
if (await fs36.pathExists(newDest)) {
|
|
7164
7401
|
if (await sameContent(entry.destPath, newDest)) {
|
|
7165
7402
|
if (!dryRun) {
|
|
7166
|
-
await
|
|
7403
|
+
await fs36.remove(entry.destPath);
|
|
7167
7404
|
vacated.push(entry.destPath);
|
|
7168
7405
|
}
|
|
7169
7406
|
moved.push(newDest);
|
|
@@ -7180,7 +7417,7 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
7180
7417
|
continue;
|
|
7181
7418
|
}
|
|
7182
7419
|
try {
|
|
7183
|
-
await
|
|
7420
|
+
await fs36.move(entry.destPath, newDest);
|
|
7184
7421
|
vacated.push(entry.destPath);
|
|
7185
7422
|
moved.push(newDest);
|
|
7186
7423
|
rewritten.push({ ...entry, destPath: newDest });
|
|
@@ -7205,7 +7442,7 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
7205
7442
|
}
|
|
7206
7443
|
try {
|
|
7207
7444
|
await writeJson(manifestPathIn(to), { ...legacy, files: rewritten });
|
|
7208
|
-
await
|
|
7445
|
+
await fs36.remove(legacyManifestPath);
|
|
7209
7446
|
vacated.push(legacyManifestPath);
|
|
7210
7447
|
} catch (err) {
|
|
7211
7448
|
return {
|
|
@@ -7250,8 +7487,8 @@ function printConfigDirMigration(result) {
|
|
|
7250
7487
|
}
|
|
7251
7488
|
|
|
7252
7489
|
// src/scanner/read-context.ts
|
|
7253
|
-
import
|
|
7254
|
-
import
|
|
7490
|
+
import path45 from "node:path";
|
|
7491
|
+
import fs37 from "fs-extra";
|
|
7255
7492
|
|
|
7256
7493
|
// src/commands/workspace/config.ts
|
|
7257
7494
|
init_fs();
|
|
@@ -7381,7 +7618,7 @@ async function readMembers(rootInfo) {
|
|
|
7381
7618
|
}
|
|
7382
7619
|
|
|
7383
7620
|
// src/scanner/scan-project.ts
|
|
7384
|
-
import
|
|
7621
|
+
import path44 from "node:path";
|
|
7385
7622
|
init_fs();
|
|
7386
7623
|
|
|
7387
7624
|
// src/utils/git-root.ts
|
|
@@ -7445,7 +7682,7 @@ init_paths();
|
|
|
7445
7682
|
// src/scanner/detection-registry.ts
|
|
7446
7683
|
var dep = (value) => ({ kind: "dep", value });
|
|
7447
7684
|
var depPrefix = (value) => ({ kind: "depPrefix", value });
|
|
7448
|
-
var
|
|
7685
|
+
var depSuffix = (value) => ({ kind: "depSuffix", value });
|
|
7449
7686
|
var depMajor = (value, major) => ({
|
|
7450
7687
|
kind: "depMajor",
|
|
7451
7688
|
value,
|
|
@@ -7472,6 +7709,7 @@ var fileStartsWith = (value) => ({
|
|
|
7472
7709
|
mode: "startsWith"
|
|
7473
7710
|
});
|
|
7474
7711
|
var content = (value) => ({ kind: "content", value });
|
|
7712
|
+
var composerType = (value) => ({ kind: "composerType", value });
|
|
7475
7713
|
var STACK_BUCKETS = [
|
|
7476
7714
|
"backend",
|
|
7477
7715
|
"frontend",
|
|
@@ -7488,12 +7726,17 @@ function matchSignal(sig, ctx) {
|
|
|
7488
7726
|
case "depPrefix":
|
|
7489
7727
|
for (const d of ctx.deps) if (d.startsWith(sig.value)) return true;
|
|
7490
7728
|
return false;
|
|
7729
|
+
case "depSuffix":
|
|
7730
|
+
for (const d of ctx.deps) if (d.endsWith(sig.value)) return true;
|
|
7731
|
+
return false;
|
|
7491
7732
|
case "depAbsent":
|
|
7492
7733
|
return !ctx.deps.has(sig.value);
|
|
7493
7734
|
case "depMajor":
|
|
7494
7735
|
return ctx.deps.has(sig.value) && resolveMajorVersion(ctx.depVersions.get(sig.value) ?? "") === sig.major;
|
|
7495
7736
|
case "content":
|
|
7496
7737
|
return ctx.contentBlob.includes(sig.value);
|
|
7738
|
+
case "composerType":
|
|
7739
|
+
return sig.value.length > 0 && ctx.composerType === sig.value;
|
|
7497
7740
|
case "file":
|
|
7498
7741
|
return ctx.files.some((f) => {
|
|
7499
7742
|
switch (sig.mode) {
|
|
@@ -7510,9 +7753,12 @@ function matchSignal(sig, ctx) {
|
|
|
7510
7753
|
}
|
|
7511
7754
|
}
|
|
7512
7755
|
function matchRule(rule, ctx) {
|
|
7513
|
-
|
|
7514
|
-
if (rule.
|
|
7515
|
-
|
|
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);
|
|
7516
7762
|
}
|
|
7517
7763
|
var ROLE_RULES = [
|
|
7518
7764
|
// Workspace/meta-repo root — same bridge signal `readMembers()`/
|
|
@@ -7570,8 +7816,33 @@ var STACK_RULES = [
|
|
|
7570
7816
|
},
|
|
7571
7817
|
{ stack: ["tooling", "pm2"], any: [dep("pm2"), fileIncludes("ecosystem.config")] },
|
|
7572
7818
|
{ stack: ["tooling", "sentry"], any: [depPrefix("@sentry/")] },
|
|
7573
|
-
|
|
7574
|
-
|
|
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
|
+
},
|
|
7575
7846
|
{
|
|
7576
7847
|
stack: ["tooling", "stripe"],
|
|
7577
7848
|
any: [dep("stripe"), dep("@stripe/stripe-js"), dep("@stripe/react-stripe-js")]
|
|
@@ -7603,7 +7874,16 @@ var STACK_RULES = [
|
|
|
7603
7874
|
{ stack: ["backend", "php"], any: [fileEndsWith("composer.json")] },
|
|
7604
7875
|
{ stack: ["backend", "laravel"], any: [dep("laravel/framework")] },
|
|
7605
7876
|
{ stack: ["backend", "laravel"], any: [fileIncludes("app/Providers/"), fileIncludes("routes/")] },
|
|
7606
|
-
|
|
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
|
+
},
|
|
7607
7887
|
{
|
|
7608
7888
|
stack: ["backend", "elementor"],
|
|
7609
7889
|
any: [
|
|
@@ -7696,6 +7976,76 @@ async function buildContentBlob(root, files) {
|
|
|
7696
7976
|
return parts.join("\n");
|
|
7697
7977
|
}
|
|
7698
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
|
+
|
|
7699
8049
|
// src/scanner/write-sources-report.ts
|
|
7700
8050
|
init_manifest_format();
|
|
7701
8051
|
init_fs();
|
|
@@ -7758,6 +8108,20 @@ var SAFE_FILES = [
|
|
|
7758
8108
|
"next.config.*",
|
|
7759
8109
|
"tailwind.config.*",
|
|
7760
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*",
|
|
7761
8125
|
"playwright.config.*",
|
|
7762
8126
|
"phpunit.xml",
|
|
7763
8127
|
"artisan",
|
|
@@ -7797,11 +8161,17 @@ var SAFE_FILES = [
|
|
|
7797
8161
|
"Gemfile"
|
|
7798
8162
|
];
|
|
7799
8163
|
async function scanProject(root, options = {}) {
|
|
7800
|
-
const pkg = await readJson(
|
|
7801
|
-
const composer = await readJson(
|
|
8164
|
+
const pkg = await readJson(path44.join(root, "package.json"));
|
|
8165
|
+
const composer = await readJson(path44.join(root, "composer.json"));
|
|
7802
8166
|
const files = await listFiles(root, SAFE_FILES);
|
|
7803
8167
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
7804
|
-
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
|
+
);
|
|
7805
8175
|
const deps = [...depVersions.keys()].sort();
|
|
7806
8176
|
const packageManager = detectPackageManager(root, String(pkg?.packageManager ?? ""));
|
|
7807
8177
|
const contentBlob = await buildContentBlob(root, safeFiles);
|
|
@@ -7809,7 +8179,8 @@ async function scanProject(root, options = {}) {
|
|
|
7809
8179
|
deps: new Set(deps),
|
|
7810
8180
|
depVersions,
|
|
7811
8181
|
files: safeFiles,
|
|
7812
|
-
contentBlob
|
|
8182
|
+
contentBlob,
|
|
8183
|
+
composerType: typeof composer?.type === "string" ? composer.type : ""
|
|
7813
8184
|
});
|
|
7814
8185
|
const roles = finalizeRoles(detection.roles, deps, safeFiles);
|
|
7815
8186
|
const stacks = detection.stacks;
|
|
@@ -7828,7 +8199,7 @@ async function scanProject(root, options = {}) {
|
|
|
7828
8199
|
crossRepoHints.push("Monorepo orchestration detected");
|
|
7829
8200
|
const unsupportedSignals = collectUnsupportedSignals(safeFiles);
|
|
7830
8201
|
const detectionStatus = computeDetectionStatus(roles, stacks, unsupportedSignals);
|
|
7831
|
-
const repoNameFallback = pkg?.name ? String(pkg.name) :
|
|
8202
|
+
const repoNameFallback = pkg?.name ? String(pkg.name) : path44.basename((await resolveRoots(root)).mainRoot);
|
|
7832
8203
|
const context = {
|
|
7833
8204
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7834
8205
|
repoName: repoNameFallback,
|
|
@@ -7852,15 +8223,15 @@ async function scanProject(root, options = {}) {
|
|
|
7852
8223
|
// src/scanner/read-context.ts
|
|
7853
8224
|
async function isCacheFresh(root, cachePath) {
|
|
7854
8225
|
const markerPaths = [
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
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)
|
|
7859
8230
|
];
|
|
7860
|
-
const cacheStat = await
|
|
8231
|
+
const cacheStat = await fs37.stat(cachePath);
|
|
7861
8232
|
for (const markerPath of markerPaths) {
|
|
7862
|
-
if (!await
|
|
7863
|
-
const markerStat = await
|
|
8233
|
+
if (!await fs37.pathExists(markerPath)) continue;
|
|
8234
|
+
const markerStat = await fs37.stat(markerPath);
|
|
7864
8235
|
if (markerStat.mtimeMs > cacheStat.mtimeMs) return false;
|
|
7865
8236
|
}
|
|
7866
8237
|
return true;
|
|
@@ -7975,7 +8346,7 @@ async function runDoctor(options) {
|
|
|
7975
8346
|
} else {
|
|
7976
8347
|
ok(`- HOOKS OK: ${hooks.message}`);
|
|
7977
8348
|
}
|
|
7978
|
-
const rootClaudeMdPath =
|
|
8349
|
+
const rootClaudeMdPath = path46.join(root, "CLAUDE.md");
|
|
7979
8350
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
7980
8351
|
if (!rootClaudeMdContent) {
|
|
7981
8352
|
flag(
|
|
@@ -8018,10 +8389,10 @@ async function runDoctor(options) {
|
|
|
8018
8389
|
ok("- CLAUDE.md: import block present");
|
|
8019
8390
|
}
|
|
8020
8391
|
for (const target of actualTargets) {
|
|
8021
|
-
const resolved =
|
|
8022
|
-
const relFromRoot =
|
|
8023
|
-
const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${
|
|
8024
|
-
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)) {
|
|
8025
8396
|
flag(
|
|
8026
8397
|
`- CLAUDE.md import: @${target} does not resolve (run \`haus apply --write\`)`,
|
|
8027
8398
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -8039,7 +8410,7 @@ async function runDoctor(options) {
|
|
|
8039
8410
|
);
|
|
8040
8411
|
}
|
|
8041
8412
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
8042
|
-
const workflowExists = await
|
|
8413
|
+
const workflowExists = await fs38.pathExists(workflowPath);
|
|
8043
8414
|
if (!workflowExists) {
|
|
8044
8415
|
flag(
|
|
8045
8416
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -8054,15 +8425,15 @@ async function runDoctor(options) {
|
|
|
8054
8425
|
} else {
|
|
8055
8426
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
8056
8427
|
const bodyContent = workflowContent.slice(firstLine.length + 1);
|
|
8057
|
-
const cachePath =
|
|
8058
|
-
const bundledPath =
|
|
8428
|
+
const cachePath = path46.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
8429
|
+
const bundledPath = path46.join(
|
|
8059
8430
|
packageRoot(),
|
|
8060
8431
|
"library",
|
|
8061
8432
|
"global",
|
|
8062
8433
|
"templates",
|
|
8063
8434
|
"agentic-workflow-standard.md"
|
|
8064
8435
|
);
|
|
8065
|
-
const templatePath = await
|
|
8436
|
+
const templatePath = await fs38.pathExists(cachePath) ? cachePath : bundledPath;
|
|
8066
8437
|
const templateContent = await readText(templatePath);
|
|
8067
8438
|
const verdict = checkManagedTamper(bodyContent, storedHashMatch?.[1], templateContent ?? null);
|
|
8068
8439
|
if (verdict.status === "tampered" || verdict.status === "legacy-diverged") {
|
|
@@ -8090,7 +8461,7 @@ async function runDoctor(options) {
|
|
|
8090
8461
|
}
|
|
8091
8462
|
}
|
|
8092
8463
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
8093
|
-
const workflowConfigExists = await
|
|
8464
|
+
const workflowConfigExists = await fs38.pathExists(workflowConfigPath);
|
|
8094
8465
|
if (!workflowConfigExists) {
|
|
8095
8466
|
flag(
|
|
8096
8467
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -8098,7 +8469,7 @@ async function runDoctor(options) {
|
|
|
8098
8469
|
"haus apply --write"
|
|
8099
8470
|
);
|
|
8100
8471
|
} else {
|
|
8101
|
-
const cfg = await
|
|
8472
|
+
const cfg = await fs38.readFile(workflowConfigPath, "utf8");
|
|
8102
8473
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
8103
8474
|
if (unfilled > 0) {
|
|
8104
8475
|
suggest(
|
|
@@ -8110,11 +8481,11 @@ async function runDoctor(options) {
|
|
|
8110
8481
|
ok("- .haus-workflow/workflow-config.md: OK (project-owned)");
|
|
8111
8482
|
}
|
|
8112
8483
|
}
|
|
8113
|
-
const docsSkillInstalled = await
|
|
8114
|
-
|
|
8484
|
+
const docsSkillInstalled = await fs38.pathExists(
|
|
8485
|
+
path46.join(root, ".claude", "skills", "writing-documentation", "SKILL.md")
|
|
8115
8486
|
);
|
|
8116
8487
|
if (docsSkillInstalled) {
|
|
8117
|
-
if (await
|
|
8488
|
+
if (await fs38.pathExists(path46.join(root, "docs", "SUMMARY.md"))) {
|
|
8118
8489
|
ok("- PROJECT DOCS: OK (docs/SUMMARY.md present)");
|
|
8119
8490
|
} else {
|
|
8120
8491
|
suggest(
|
|
@@ -8125,7 +8496,7 @@ async function runDoctor(options) {
|
|
|
8125
8496
|
}
|
|
8126
8497
|
}
|
|
8127
8498
|
if (workflowExists) {
|
|
8128
|
-
const prettierIgnore = await readText(
|
|
8499
|
+
const prettierIgnore = await readText(path46.join(root, ".prettierignore")) ?? "";
|
|
8129
8500
|
const lines = prettierIgnore.split("\n");
|
|
8130
8501
|
const missing = IGNORED_PATHS.filter((p) => !prettierIgnoreProtects(lines, p));
|
|
8131
8502
|
if (missing.length > 0) {
|
|
@@ -8241,8 +8612,53 @@ async function runDoctor(options) {
|
|
|
8241
8612
|
"find its catalog id and run `haus recommend --include <id>` then `haus apply --write`, or remove the reference"
|
|
8242
8613
|
);
|
|
8243
8614
|
}
|
|
8244
|
-
}
|
|
8245
|
-
|
|
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
|
+
}
|
|
8246
8662
|
}
|
|
8247
8663
|
if (claudeConfigDirIsRelocated()) {
|
|
8248
8664
|
const configMigration = await probeGlobalConfigDirMigration();
|
|
@@ -8321,10 +8737,10 @@ async function runDoctor(options) {
|
|
|
8321
8737
|
);
|
|
8322
8738
|
} else {
|
|
8323
8739
|
const lock = await readJson(hausPath(root, "haus.lock.json"));
|
|
8324
|
-
const
|
|
8740
|
+
const recommendedIds2 = new Set(
|
|
8325
8741
|
recommendation.recommended.map((r) => r.id).filter((id) => typeof id === "string")
|
|
8326
8742
|
);
|
|
8327
|
-
const orphaned = findOrphanedLockEntries(lock ?? [],
|
|
8743
|
+
const orphaned = findOrphanedLockEntries(lock ?? [], recommendedIds2).map((entry) => entry.id);
|
|
8328
8744
|
if (orphaned.length > 0) {
|
|
8329
8745
|
suggest(
|
|
8330
8746
|
`- CATALOG ITEMS: ${orphaned.length} installed item(s) no longer in the current recommendation (${orphaned.join(", ")})`,
|
|
@@ -8380,7 +8796,7 @@ async function runDoctor(options) {
|
|
|
8380
8796
|
let onDisk = false;
|
|
8381
8797
|
for (const rel of row.paths ?? []) {
|
|
8382
8798
|
const abs = resolveContainedPath(root, rel);
|
|
8383
|
-
if (abs !== null && await
|
|
8799
|
+
if (abs !== null && await fs38.pathExists(abs)) {
|
|
8384
8800
|
onDisk = true;
|
|
8385
8801
|
break;
|
|
8386
8802
|
}
|
|
@@ -8396,7 +8812,7 @@ async function runDoctor(options) {
|
|
|
8396
8812
|
);
|
|
8397
8813
|
}
|
|
8398
8814
|
}
|
|
8399
|
-
const pkgJson = await readJson(
|
|
8815
|
+
const pkgJson = await readJson(path46.join(packageRoot(), "package.json"));
|
|
8400
8816
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
8401
8817
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
8402
8818
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -8447,15 +8863,15 @@ async function runDoctor(options) {
|
|
|
8447
8863
|
}
|
|
8448
8864
|
|
|
8449
8865
|
// src/commands/update.ts
|
|
8450
|
-
import
|
|
8866
|
+
import path49 from "node:path";
|
|
8451
8867
|
init_github_rate_limit();
|
|
8452
8868
|
|
|
8453
8869
|
// src/claude/refresh-project.ts
|
|
8454
|
-
import
|
|
8870
|
+
import fs39 from "fs-extra";
|
|
8455
8871
|
init_fs();
|
|
8456
8872
|
init_paths();
|
|
8457
8873
|
async function isHausProject(root) {
|
|
8458
|
-
if (await
|
|
8874
|
+
if (await fs39.pathExists(hausPath(root, "recommendation.json"))) return true;
|
|
8459
8875
|
const result = await readJsonDetailed(claudePath(root, "settings.json"));
|
|
8460
8876
|
if (result.status === "ok") return result.value._haus != null;
|
|
8461
8877
|
return false;
|
|
@@ -8471,8 +8887,8 @@ async function refreshProjectApply(root) {
|
|
|
8471
8887
|
|
|
8472
8888
|
// src/install/apply.ts
|
|
8473
8889
|
import crypto2 from "node:crypto";
|
|
8474
|
-
import
|
|
8475
|
-
import
|
|
8890
|
+
import path47 from "node:path";
|
|
8891
|
+
import fs40 from "fs-extra";
|
|
8476
8892
|
init_fs();
|
|
8477
8893
|
init_logger();
|
|
8478
8894
|
init_paths();
|
|
@@ -8482,6 +8898,17 @@ var MD_PREFIX = "<!-- HAUS-MANAGED";
|
|
|
8482
8898
|
var MD_SUFFIX = " -->";
|
|
8483
8899
|
var FM_FENCE = "---";
|
|
8484
8900
|
var FM_KEY = "haus_managed";
|
|
8901
|
+
var JS_PREFIX = "// HAUS-MANAGED";
|
|
8902
|
+
var JS_EXT_RE = /\.[mc]?js$/i;
|
|
8903
|
+
var MD_EXT_RE = /\.(md|markdown)$/i;
|
|
8904
|
+
function stampableMarkerForm(destPath) {
|
|
8905
|
+
if (JS_EXT_RE.test(destPath)) return "js";
|
|
8906
|
+
if (MD_EXT_RE.test(destPath)) return "markdown";
|
|
8907
|
+
return void 0;
|
|
8908
|
+
}
|
|
8909
|
+
function isShebang(line2) {
|
|
8910
|
+
return (line2 ?? "").startsWith("#!");
|
|
8911
|
+
}
|
|
8485
8912
|
function parseAttrs(raw) {
|
|
8486
8913
|
const attrs = parseHausManagedAttrs(raw) ?? parseHausManagedAttrs(`<!-- HAUS-MANAGED ${raw.trim()} -->`);
|
|
8487
8914
|
if (!attrs?.id || !attrs.v || !attrs.source) return void 0;
|
|
@@ -8504,8 +8931,11 @@ function frontmatterMarkerValue(content2) {
|
|
|
8504
8931
|
return void 0;
|
|
8505
8932
|
}
|
|
8506
8933
|
function parseMarkdownHeader(content2) {
|
|
8507
|
-
const
|
|
8934
|
+
const lines = content2.split("\n");
|
|
8935
|
+
const firstLine = lines[0] ?? "";
|
|
8508
8936
|
if (firstLine.startsWith(MD_PREFIX)) return parseAttrs(firstLine);
|
|
8937
|
+
const jsLine = isShebang(firstLine) ? lines[1] ?? "" : firstLine;
|
|
8938
|
+
if (jsLine.startsWith(JS_PREFIX)) return parseAttrs(jsLine.slice(JS_PREFIX.length));
|
|
8509
8939
|
const fmValue = frontmatterMarkerValue(content2);
|
|
8510
8940
|
if (fmValue !== void 0) return parseAttrs(fmValue);
|
|
8511
8941
|
return void 0;
|
|
@@ -8540,7 +8970,18 @@ function stampFrontmatter(content2, h) {
|
|
|
8540
8970
|
];
|
|
8541
8971
|
return rebuilt.join("\n");
|
|
8542
8972
|
}
|
|
8543
|
-
function
|
|
8973
|
+
function buildJsHeader(h) {
|
|
8974
|
+
return `${JS_PREFIX} ${buildMarkerValue(h)}`;
|
|
8975
|
+
}
|
|
8976
|
+
function stampJs(content2, h) {
|
|
8977
|
+
const lines = content2.split("\n");
|
|
8978
|
+
const at = isShebang(lines[0]) ? 1 : 0;
|
|
8979
|
+
if ((lines[at] ?? "").startsWith(JS_PREFIX)) lines[at] = buildJsHeader(h);
|
|
8980
|
+
else lines.splice(at, 0, buildJsHeader(h));
|
|
8981
|
+
return lines.join("\n");
|
|
8982
|
+
}
|
|
8983
|
+
function stampMarkdown(content2, h, destPath) {
|
|
8984
|
+
if (destPath !== void 0 && stampableMarkerForm(destPath) === "js") return stampJs(content2, h);
|
|
8544
8985
|
if (hasFrontmatter(content2)) return stampFrontmatter(content2, h);
|
|
8545
8986
|
const header = buildMarkdownHeader(h);
|
|
8546
8987
|
const firstLine = content2.split("\n")[0] ?? "";
|
|
@@ -8561,67 +9002,72 @@ function hashContent(content2) {
|
|
|
8561
9002
|
}
|
|
8562
9003
|
function destFileMissing(destPath) {
|
|
8563
9004
|
try {
|
|
8564
|
-
return !
|
|
9005
|
+
return !fs40.statSync(destPath).isFile();
|
|
8565
9006
|
} catch {
|
|
8566
9007
|
return true;
|
|
8567
9008
|
}
|
|
8568
9009
|
}
|
|
8569
9010
|
function sourceVersion() {
|
|
8570
9011
|
try {
|
|
8571
|
-
const pkgPath =
|
|
8572
|
-
const pkg = JSON.parse(
|
|
9012
|
+
const pkgPath = path47.join(packageRoot(), "package.json");
|
|
9013
|
+
const pkg = JSON.parse(fs40.readFileSync(pkgPath, "utf8"));
|
|
8573
9014
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
8574
9015
|
} catch {
|
|
8575
9016
|
return "haus@0.0.0";
|
|
8576
9017
|
}
|
|
8577
9018
|
}
|
|
8578
9019
|
function globalSrcDir() {
|
|
8579
|
-
return
|
|
9020
|
+
return path47.join(packageRoot(), "library", "global");
|
|
8580
9021
|
}
|
|
8581
9022
|
function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
8582
|
-
const skillDir =
|
|
9023
|
+
const skillDir = path47.join(skillsDir, skillName);
|
|
8583
9024
|
const entries = [];
|
|
8584
|
-
for (const rel of
|
|
9025
|
+
for (const rel of fs40.readdirSync(skillDir, { recursive: true })) {
|
|
8585
9026
|
if (rel === "SKILL.md") continue;
|
|
8586
|
-
const abs =
|
|
8587
|
-
if (!
|
|
8588
|
-
const posixRel = rel.split(
|
|
9027
|
+
const abs = path47.join(skillDir, rel);
|
|
9028
|
+
if (!fs40.statSync(abs).isFile()) continue;
|
|
9029
|
+
const posixRel = rel.split(path47.sep).join("/");
|
|
8589
9030
|
entries.push({
|
|
8590
9031
|
stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
|
|
8591
|
-
srcRelPath:
|
|
8592
|
-
destPath:
|
|
9032
|
+
srcRelPath: path47.join("library", "global", "skills", skillName, rel),
|
|
9033
|
+
destPath: path47.join(claudeDir, "skills", skillName, rel)
|
|
9034
|
+
});
|
|
9035
|
+
}
|
|
9036
|
+
return entries;
|
|
9037
|
+
}
|
|
9038
|
+
function collectFlatChannel(srcDir, claudeDir, dirName, idPrefix) {
|
|
9039
|
+
const entries = [];
|
|
9040
|
+
const channelDir = path47.join(srcDir, dirName);
|
|
9041
|
+
if (!fs40.pathExistsSync(channelDir)) return entries;
|
|
9042
|
+
for (const fileName of fs40.readdirSync(channelDir)) {
|
|
9043
|
+
if (!fileName.endsWith(".md")) continue;
|
|
9044
|
+
const name = fileName.slice(0, -".md".length);
|
|
9045
|
+
entries.push({
|
|
9046
|
+
stableId: `${idPrefix}.${name}`,
|
|
9047
|
+
srcRelPath: path47.join("library", "global", dirName, fileName),
|
|
9048
|
+
destPath: path47.join(claudeDir, dirName, fileName)
|
|
8593
9049
|
});
|
|
8594
9050
|
}
|
|
8595
9051
|
return entries;
|
|
8596
9052
|
}
|
|
8597
9053
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
8598
9054
|
const entries = [];
|
|
8599
|
-
const skillsDir =
|
|
8600
|
-
if (
|
|
8601
|
-
for (const skillName of
|
|
8602
|
-
const skillFile =
|
|
8603
|
-
if (
|
|
9055
|
+
const skillsDir = path47.join(srcDir, "skills");
|
|
9056
|
+
if (fs40.pathExistsSync(skillsDir)) {
|
|
9057
|
+
for (const skillName of fs40.readdirSync(skillsDir)) {
|
|
9058
|
+
const skillFile = path47.join(skillsDir, skillName, "SKILL.md");
|
|
9059
|
+
if (fs40.pathExistsSync(skillFile)) {
|
|
8604
9060
|
entries.push({
|
|
8605
9061
|
stableId: `skill.${skillName}`,
|
|
8606
|
-
srcRelPath:
|
|
8607
|
-
destPath:
|
|
9062
|
+
srcRelPath: path47.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
9063
|
+
destPath: path47.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
8608
9064
|
});
|
|
8609
9065
|
entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
|
|
8610
9066
|
}
|
|
8611
9067
|
}
|
|
8612
9068
|
}
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
for (const fileName of fs39.readdirSync(commandsDir)) {
|
|
8616
|
-
if (!fileName.endsWith(".md")) continue;
|
|
8617
|
-
const commandName = fileName.slice(0, -".md".length);
|
|
8618
|
-
entries.push({
|
|
8619
|
-
stableId: `command.${commandName}`,
|
|
8620
|
-
srcRelPath: path46.join("library", "global", "commands", fileName),
|
|
8621
|
-
destPath: path46.join(claudeDir, "commands", fileName)
|
|
8622
|
-
});
|
|
8623
|
-
}
|
|
8624
|
-
}
|
|
9069
|
+
entries.push(...collectFlatChannel(srcDir, claudeDir, "commands", "command"));
|
|
9070
|
+
entries.push(...collectFlatChannel(srcDir, claudeDir, "agents", "agent"));
|
|
8625
9071
|
return entries;
|
|
8626
9072
|
}
|
|
8627
9073
|
async function applyInstall(options = {}) {
|
|
@@ -8650,17 +9096,25 @@ async function applyInstall(options = {}) {
|
|
|
8650
9096
|
};
|
|
8651
9097
|
const manifestFiles = [];
|
|
8652
9098
|
for (const entry of sourceFiles) {
|
|
8653
|
-
const srcPath =
|
|
9099
|
+
const srcPath = path47.join(packageRoot(), entry.srcRelPath);
|
|
8654
9100
|
const rawContent = await readText(srcPath);
|
|
8655
9101
|
if (rawContent === void 0) {
|
|
8656
9102
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
8657
9103
|
continue;
|
|
8658
9104
|
}
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
9105
|
+
if (stampableMarkerForm(entry.destPath) === void 0)
|
|
9106
|
+
throw new Error(
|
|
9107
|
+
`No safe ownership-marker form for ${entry.srcRelPath} \u2014 only .md and .js/.mjs/.cjs aux files can ship in library/global (see ADR-0072)`
|
|
9108
|
+
);
|
|
9109
|
+
const stamped = stampMarkdown(
|
|
9110
|
+
rawContent,
|
|
9111
|
+
{
|
|
9112
|
+
stableId: entry.stableId,
|
|
9113
|
+
schemaVersion: SCHEMA_VERSION2,
|
|
9114
|
+
source
|
|
9115
|
+
},
|
|
9116
|
+
entry.destPath
|
|
9117
|
+
);
|
|
8664
9118
|
const newHash = hashContent(stamped);
|
|
8665
9119
|
const existing = manifestByDest.get(entry.destPath);
|
|
8666
9120
|
if (check) {
|
|
@@ -8671,7 +9125,7 @@ async function applyInstall(options = {}) {
|
|
|
8671
9125
|
}
|
|
8672
9126
|
continue;
|
|
8673
9127
|
}
|
|
8674
|
-
const destExists =
|
|
9128
|
+
const destExists = fs40.pathExistsSync(entry.destPath);
|
|
8675
9129
|
if (destExists) {
|
|
8676
9130
|
const currentContent = await readText(entry.destPath);
|
|
8677
9131
|
if (currentContent !== void 0) {
|
|
@@ -8711,7 +9165,7 @@ async function applyInstall(options = {}) {
|
|
|
8711
9165
|
}
|
|
8712
9166
|
let mergedSettings;
|
|
8713
9167
|
if (!check) {
|
|
8714
|
-
const fragmentPath =
|
|
9168
|
+
const fragmentPath = path47.join(srcDir, "settings-fragments", "hooks.json");
|
|
8715
9169
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
8716
9170
|
const settings = await readSettings();
|
|
8717
9171
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -8730,13 +9184,13 @@ async function applyInstall(options = {}) {
|
|
|
8730
9184
|
manifestFiles.push(entry);
|
|
8731
9185
|
continue;
|
|
8732
9186
|
}
|
|
8733
|
-
if (!
|
|
9187
|
+
if (!fs40.pathExistsSync(entry.destPath)) continue;
|
|
8734
9188
|
const content2 = await readText(entry.destPath);
|
|
8735
9189
|
if (!content2) continue;
|
|
8736
9190
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
8737
9191
|
const currentHash = hashContent(content2);
|
|
8738
9192
|
if (hasHeader && currentHash === entry.hash) {
|
|
8739
|
-
if (!dryRun) await
|
|
9193
|
+
if (!dryRun) await fs40.remove(entry.destPath);
|
|
8740
9194
|
result.deleted.push(entry.destPath);
|
|
8741
9195
|
} else {
|
|
8742
9196
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -8810,7 +9264,7 @@ function summarizeLockDiff(before, after) {
|
|
|
8810
9264
|
// src/update/lockfile.ts
|
|
8811
9265
|
import { constants as fsConstants } from "node:fs";
|
|
8812
9266
|
import { copyFile, mkdir, readFile as readFile2 } from "node:fs/promises";
|
|
8813
|
-
import
|
|
9267
|
+
import path48 from "node:path";
|
|
8814
9268
|
init_fs();
|
|
8815
9269
|
init_paths();
|
|
8816
9270
|
async function readLockSummary(root) {
|
|
@@ -8850,7 +9304,7 @@ async function backupLockfile(root) {
|
|
|
8850
9304
|
await mkdir(backupDir, { recursive: true });
|
|
8851
9305
|
let stamp = Date.now();
|
|
8852
9306
|
for (; ; ) {
|
|
8853
|
-
const dest =
|
|
9307
|
+
const dest = path48.join(backupDir, lockBackupFileName(stamp));
|
|
8854
9308
|
try {
|
|
8855
9309
|
await copyFile(lockPath, dest, fsConstants.COPYFILE_EXCL);
|
|
8856
9310
|
return;
|
|
@@ -8901,7 +9355,7 @@ function diffLock(before, after) {
|
|
|
8901
9355
|
}
|
|
8902
9356
|
async function hasLocalOverrides(root) {
|
|
8903
9357
|
try {
|
|
8904
|
-
await readFile2(
|
|
9358
|
+
await readFile2(path48.join(root, ".claude", "settings.json"), "utf8");
|
|
8905
9359
|
return true;
|
|
8906
9360
|
} catch {
|
|
8907
9361
|
return false;
|
|
@@ -8946,7 +9400,7 @@ async function runUpdate(options) {
|
|
|
8946
9400
|
return;
|
|
8947
9401
|
}
|
|
8948
9402
|
if (options.check) {
|
|
8949
|
-
const pkgJson2 = await readJson(
|
|
9403
|
+
const pkgJson2 = await readJson(path49.join(packageRoot(), "package.json"));
|
|
8950
9404
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
8951
9405
|
const [
|
|
8952
9406
|
status,
|
|
@@ -8998,7 +9452,7 @@ async function runUpdate(options) {
|
|
|
8998
9452
|
}
|
|
8999
9453
|
return;
|
|
9000
9454
|
}
|
|
9001
|
-
const pkgJson = await readJson(
|
|
9455
|
+
const pkgJson = await readJson(path49.join(packageRoot(), "package.json"));
|
|
9002
9456
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
9003
9457
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
9004
9458
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -9055,7 +9509,7 @@ async function runFromHookCheck(root) {
|
|
|
9055
9509
|
try {
|
|
9056
9510
|
const summary = await readLockSummary(root);
|
|
9057
9511
|
if (summary.count === 0) return;
|
|
9058
|
-
const pkgJson = await readJson(
|
|
9512
|
+
const pkgJson = await readJson(path49.join(packageRoot(), "package.json"));
|
|
9059
9513
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
9060
9514
|
const pin = readProjectCatalogPin(root);
|
|
9061
9515
|
const honoursPin = pin.kind === "pinned" && summary.catalogRef === pin.ref;
|
|
@@ -9239,7 +9693,7 @@ init_exec();
|
|
|
9239
9693
|
init_logger();
|
|
9240
9694
|
import { existsSync as existsSync2 } from "node:fs";
|
|
9241
9695
|
import { readdir } from "node:fs/promises";
|
|
9242
|
-
import
|
|
9696
|
+
import path50 from "node:path";
|
|
9243
9697
|
var GIT_LOCATION_VARS = [
|
|
9244
9698
|
"GIT_DIR",
|
|
9245
9699
|
"GIT_WORK_TREE",
|
|
@@ -9299,11 +9753,11 @@ async function runClone(url, opts = {}) {
|
|
|
9299
9753
|
process.exitCode = 1;
|
|
9300
9754
|
return;
|
|
9301
9755
|
}
|
|
9302
|
-
const target =
|
|
9756
|
+
const target = path50.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
9303
9757
|
if (existsSync2(target) && !await isEmptyDirectory(target)) {
|
|
9304
9758
|
const check = await checkExistingTarget(target, url);
|
|
9305
9759
|
if (check.kind === "matches") {
|
|
9306
|
-
log(`\u2022 ${
|
|
9760
|
+
log(`\u2022 ${path50.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
|
|
9307
9761
|
return;
|
|
9308
9762
|
}
|
|
9309
9763
|
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].`;
|
|
@@ -9330,8 +9784,8 @@ async function runClone(url, opts = {}) {
|
|
|
9330
9784
|
|
|
9331
9785
|
// src/install/scaffold.ts
|
|
9332
9786
|
init_logger();
|
|
9333
|
-
import
|
|
9334
|
-
import
|
|
9787
|
+
import path51 from "node:path";
|
|
9788
|
+
import fs41 from "fs-extra";
|
|
9335
9789
|
async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
9336
9790
|
const scaffolded = [];
|
|
9337
9791
|
const skipped = [];
|
|
@@ -9344,27 +9798,27 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
9344
9798
|
skipped.push(name);
|
|
9345
9799
|
if (outcome === "skipped-exists") skippedExisting.push(name);
|
|
9346
9800
|
};
|
|
9347
|
-
const realRoot = await
|
|
9801
|
+
const realRoot = await fs41.realpath(catalogRoot).catch(() => null);
|
|
9348
9802
|
for (const item of items) {
|
|
9349
9803
|
if (item.type !== "config") continue;
|
|
9350
|
-
const sourcePath =
|
|
9351
|
-
const rel =
|
|
9352
|
-
const escapes = rel === "" ||
|
|
9804
|
+
const sourcePath = path51.resolve(catalogRoot, item.path);
|
|
9805
|
+
const rel = path51.relative(catalogRoot, sourcePath);
|
|
9806
|
+
const escapes = rel === "" || path51.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
|
|
9353
9807
|
if (escapes) {
|
|
9354
9808
|
warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
|
|
9355
9809
|
continue;
|
|
9356
9810
|
}
|
|
9357
|
-
const realSource = await
|
|
9811
|
+
const realSource = await fs41.realpath(sourcePath).catch(() => null);
|
|
9358
9812
|
if (!realRoot || !realSource) {
|
|
9359
9813
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
9360
9814
|
continue;
|
|
9361
9815
|
}
|
|
9362
|
-
const realRel =
|
|
9363
|
-
if (realRel !== "" && (
|
|
9816
|
+
const realRel = path51.relative(realRoot, realSource);
|
|
9817
|
+
if (realRel !== "" && (path51.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
|
|
9364
9818
|
warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
|
|
9365
9819
|
continue;
|
|
9366
9820
|
}
|
|
9367
|
-
const stat = await
|
|
9821
|
+
const stat = await fs41.lstat(sourcePath).catch(() => null);
|
|
9368
9822
|
if (!stat) {
|
|
9369
9823
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
9370
9824
|
continue;
|
|
@@ -9374,14 +9828,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
9374
9828
|
continue;
|
|
9375
9829
|
}
|
|
9376
9830
|
if (stat.isFile()) {
|
|
9377
|
-
const filename =
|
|
9378
|
-
const dest =
|
|
9831
|
+
const filename = path51.basename(sourcePath);
|
|
9832
|
+
const dest = path51.join(projectRoot, filename);
|
|
9379
9833
|
record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
|
|
9380
9834
|
} else if (stat.isDirectory()) {
|
|
9381
|
-
const entries = await
|
|
9835
|
+
const entries = await fs41.readdir(sourcePath);
|
|
9382
9836
|
for (const entry of entries) {
|
|
9383
|
-
const src =
|
|
9384
|
-
const dest =
|
|
9837
|
+
const src = path51.join(sourcePath, entry);
|
|
9838
|
+
const dest = path51.join(projectRoot, entry);
|
|
9385
9839
|
record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
|
|
9386
9840
|
}
|
|
9387
9841
|
}
|
|
@@ -9389,42 +9843,42 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
9389
9843
|
return { scaffolded, skipped, skippedExisting };
|
|
9390
9844
|
}
|
|
9391
9845
|
async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
9392
|
-
const destDisplay =
|
|
9393
|
-
const srcStat = await
|
|
9846
|
+
const destDisplay = path51.relative(projectRoot, dest) || path51.basename(dest);
|
|
9847
|
+
const srcStat = await fs41.lstat(src).catch(() => null);
|
|
9394
9848
|
if (!srcStat) {
|
|
9395
|
-
warn(`Skipping ${
|
|
9849
|
+
warn(`Skipping ${path51.basename(src)}: source not found`);
|
|
9396
9850
|
return "skipped-refused";
|
|
9397
9851
|
}
|
|
9398
9852
|
if (srcStat.isSymbolicLink()) {
|
|
9399
|
-
warn(`Skipping ${
|
|
9853
|
+
warn(`Skipping ${path51.basename(src)}: source is a symlink`);
|
|
9400
9854
|
return "skipped-refused";
|
|
9401
9855
|
}
|
|
9402
|
-
const exists = await
|
|
9856
|
+
const exists = await fs41.pathExists(dest);
|
|
9403
9857
|
if (exists && !opts.force) {
|
|
9404
9858
|
warn(`Skipping ${destDisplay}: already exists \u2014 re-run with --force to overwrite it`);
|
|
9405
9859
|
return "skipped-exists";
|
|
9406
9860
|
}
|
|
9407
9861
|
if (opts.dryRun) {
|
|
9408
|
-
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${
|
|
9862
|
+
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path51.basename(dest)} (${itemId})`);
|
|
9409
9863
|
return "scaffolded";
|
|
9410
9864
|
}
|
|
9411
9865
|
if (exists && opts.force) {
|
|
9412
|
-
await
|
|
9866
|
+
await fs41.remove(dest);
|
|
9413
9867
|
}
|
|
9414
|
-
await
|
|
9415
|
-
await
|
|
9868
|
+
await fs41.ensureDir(path51.dirname(dest));
|
|
9869
|
+
await fs41.copy(src, dest, {
|
|
9416
9870
|
overwrite: true,
|
|
9417
9871
|
dereference: false,
|
|
9418
9872
|
// A vanished/unreadable entry mid-walk must skip that path, not crash the copy.
|
|
9419
9873
|
filter: (s) => {
|
|
9420
9874
|
try {
|
|
9421
|
-
return !
|
|
9875
|
+
return !fs41.lstatSync(s).isSymbolicLink();
|
|
9422
9876
|
} catch {
|
|
9423
9877
|
return false;
|
|
9424
9878
|
}
|
|
9425
9879
|
}
|
|
9426
9880
|
});
|
|
9427
|
-
if (!opts.quiet) log(`${exists ? "Overwrote" : "Created"} ${
|
|
9881
|
+
if (!opts.quiet) log(`${exists ? "Overwrote" : "Created"} ${path51.basename(dest)} (${itemId})`);
|
|
9428
9882
|
return "scaffolded";
|
|
9429
9883
|
}
|
|
9430
9884
|
|
|
@@ -9553,7 +10007,7 @@ async function runDecisionsGuard(options = {}) {
|
|
|
9553
10007
|
}
|
|
9554
10008
|
|
|
9555
10009
|
// src/commands/decisions.ts
|
|
9556
|
-
import
|
|
10010
|
+
import path52 from "node:path";
|
|
9557
10011
|
|
|
9558
10012
|
// src/decisions/next-number.ts
|
|
9559
10013
|
async function nextDecisionNumber(root) {
|
|
@@ -9664,7 +10118,7 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
9664
10118
|
return;
|
|
9665
10119
|
}
|
|
9666
10120
|
if (sub === "validate") {
|
|
9667
|
-
const target = arg ?
|
|
10121
|
+
const target = arg ? path52.resolve(root, arg) : void 0;
|
|
9668
10122
|
if (!target) {
|
|
9669
10123
|
error("usage: haus decisions validate <path-to-decision.md>");
|
|
9670
10124
|
process.exitCode = 1;
|
|
@@ -9940,61 +10394,6 @@ async function runGuard(kind, _options) {
|
|
|
9940
10394
|
// src/claude/setup-core.ts
|
|
9941
10395
|
init_github_rate_limit();
|
|
9942
10396
|
|
|
9943
|
-
// src/claude/project-exclusions.ts
|
|
9944
|
-
import fs41 from "fs-extra";
|
|
9945
|
-
init_project_pin();
|
|
9946
|
-
init_fs();
|
|
9947
|
-
init_logger();
|
|
9948
|
-
init_paths();
|
|
9949
|
-
function normalizeExcludedIds(value) {
|
|
9950
|
-
if (!Array.isArray(value)) return [];
|
|
9951
|
-
return [
|
|
9952
|
-
...new Set(
|
|
9953
|
-
value.filter((entry) => typeof entry === "string" && entry.length > 0)
|
|
9954
|
-
)
|
|
9955
|
-
];
|
|
9956
|
-
}
|
|
9957
|
-
async function readProjectExcludedIds(root) {
|
|
9958
|
-
try {
|
|
9959
|
-
const data = await readJson(hausPath(root, PROJECT_CONFIG_FILE));
|
|
9960
|
-
if (!data) return [];
|
|
9961
|
-
return normalizeExcludedIds(data.excludedIds);
|
|
9962
|
-
} catch (err) {
|
|
9963
|
-
if (err.code === "ENOENT") return [];
|
|
9964
|
-
warn(
|
|
9965
|
-
`${PROJECT_CONFIG_REL} could not be read for excludedIds (${err instanceof Error ? err.message : String(err)}) \u2014 treating as none`
|
|
9966
|
-
);
|
|
9967
|
-
return [];
|
|
9968
|
-
}
|
|
9969
|
-
}
|
|
9970
|
-
function isItemExcluded(item, excluded) {
|
|
9971
|
-
if (excluded.has(item.id)) return true;
|
|
9972
|
-
return normalizeFormerIds(item.formerIds).some((formerId) => excluded.has(formerId));
|
|
9973
|
-
}
|
|
9974
|
-
async function addProjectExcludedIds(root, ids) {
|
|
9975
|
-
const toAdd = [...new Set(ids.filter((id) => id.length > 0))];
|
|
9976
|
-
if (toAdd.length === 0) return;
|
|
9977
|
-
const file = hausPath(root, PROJECT_CONFIG_FILE);
|
|
9978
|
-
let existing = {};
|
|
9979
|
-
if (await fs41.pathExists(file)) {
|
|
9980
|
-
try {
|
|
9981
|
-
const parsed = JSON.parse(await fs41.readFile(file, "utf8"));
|
|
9982
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
9983
|
-
existing = { ...parsed };
|
|
9984
|
-
} else {
|
|
9985
|
-
warn(`${PROJECT_CONFIG_REL} is not a JSON object \u2014 rewriting with excludedIds only`);
|
|
9986
|
-
}
|
|
9987
|
-
} catch (err) {
|
|
9988
|
-
warn(
|
|
9989
|
-
`${PROJECT_CONFIG_REL} could not be parsed (${err instanceof Error ? err.message : String(err)}) \u2014 rewriting with excludedIds only`
|
|
9990
|
-
);
|
|
9991
|
-
}
|
|
9992
|
-
}
|
|
9993
|
-
const merged = normalizeExcludedIds(existing.excludedIds);
|
|
9994
|
-
const next = [.../* @__PURE__ */ new Set([...merged, ...toAdd])].sort();
|
|
9995
|
-
await writeJson(file, { ...existing, excludedIds: next });
|
|
9996
|
-
}
|
|
9997
|
-
|
|
9998
10397
|
// src/recommender/recommend.ts
|
|
9999
10398
|
init_fs();
|
|
10000
10399
|
init_paths();
|
|
@@ -10055,8 +10454,11 @@ async function recommend(root, context, opts = {}) {
|
|
|
10055
10454
|
(x) => x.toLowerCase()
|
|
10056
10455
|
)
|
|
10057
10456
|
);
|
|
10058
|
-
const
|
|
10059
|
-
const
|
|
10457
|
+
const declared = await readDeclaredContext(root);
|
|
10458
|
+
const declaredRoles = new Set(declared.roles);
|
|
10459
|
+
const declaredStacks = new Set(declared.stacks);
|
|
10460
|
+
const roleSet = /* @__PURE__ */ new Set([...scannerRoles, ...deepRoles, ...declaredRoles]);
|
|
10461
|
+
const stackSet = /* @__PURE__ */ new Set([...scannerStacks, ...deepStacks, ...declaredStacks]);
|
|
10060
10462
|
const depSet = scannerDeps;
|
|
10061
10463
|
const recommended = [];
|
|
10062
10464
|
const skipped = [];
|
|
@@ -10071,8 +10473,32 @@ async function recommend(root, context, opts = {}) {
|
|
|
10071
10473
|
...gates ? { gates: gates.map(({ name, passed }) => ({ name, passed })) } : {}
|
|
10072
10474
|
});
|
|
10073
10475
|
};
|
|
10074
|
-
const roleSignal = (name) =>
|
|
10075
|
-
|
|
10476
|
+
const roleSignal = (name) => {
|
|
10477
|
+
const key = name.toLowerCase();
|
|
10478
|
+
if (scannerRoles.has(key)) return `role:${name}`;
|
|
10479
|
+
if (declaredRoles.has(key)) return `declared:role:${name}`;
|
|
10480
|
+
return `deep:role:${name}`;
|
|
10481
|
+
};
|
|
10482
|
+
const stackSignal = (name) => {
|
|
10483
|
+
const key = name.toLowerCase();
|
|
10484
|
+
if (scannerStacks.has(key)) return `tag:${name}`;
|
|
10485
|
+
if (declaredStacks.has(key)) return `declared:tag:${name}`;
|
|
10486
|
+
return `deep:tag:${name}`;
|
|
10487
|
+
};
|
|
10488
|
+
const markDeclaredProvenance = (signal) => {
|
|
10489
|
+
if (signal == null) return signal;
|
|
10490
|
+
const separator = signal.indexOf(":");
|
|
10491
|
+
if (separator === -1) return signal;
|
|
10492
|
+
const kind = signal.slice(0, separator);
|
|
10493
|
+
const value = signal.slice(separator + 1).toLowerCase();
|
|
10494
|
+
if (kind === "role" && !scannerRoles.has(value) && declaredRoles.has(value)) {
|
|
10495
|
+
return `declared:${signal}`;
|
|
10496
|
+
}
|
|
10497
|
+
if (kind === "stack" && !scannerStacks.has(value) && declaredStacks.has(value)) {
|
|
10498
|
+
return `declared:${signal}`;
|
|
10499
|
+
}
|
|
10500
|
+
return signal;
|
|
10501
|
+
};
|
|
10076
10502
|
const buildGateChecks = (item, itemTags, normSource) => {
|
|
10077
10503
|
const gates = [];
|
|
10078
10504
|
const add = (name, passed, code, message2, signal) => {
|
|
@@ -10220,7 +10646,11 @@ async function recommend(root, context, opts = {}) {
|
|
|
10220
10646
|
if (changedMatch)
|
|
10221
10647
|
push("changed-file-match", "changed file match", `changedFile:${changedMatch}`);
|
|
10222
10648
|
if (requiresAnySatisfied && !reasons.some((r) => r.code === "stack-match")) {
|
|
10223
|
-
push(
|
|
10649
|
+
push(
|
|
10650
|
+
"requires-any-match",
|
|
10651
|
+
"requires-any signal match",
|
|
10652
|
+
markDeclaredProvenance(requiresAnySatisfied.signal)
|
|
10653
|
+
);
|
|
10224
10654
|
}
|
|
10225
10655
|
const gateSummary = gates.map(({ name, passed }) => ({ name, passed }));
|
|
10226
10656
|
const hasEvidence = reasons.some((r) => r.code !== "default-baseline");
|
|
@@ -10252,7 +10682,15 @@ async function recommend(root, context, opts = {}) {
|
|
|
10252
10682
|
}
|
|
10253
10683
|
applyCoInstallSuppression(recommended, skipped);
|
|
10254
10684
|
applyManualExcludes(excludedIds, items, recommended, skipped);
|
|
10255
|
-
const
|
|
10685
|
+
const persistedIncludes = (await readProjectIncludedIds(root)).filter(
|
|
10686
|
+
(id) => !excludedIds.includes(id)
|
|
10687
|
+
);
|
|
10688
|
+
const includeWarnings = applyManualIncludes(
|
|
10689
|
+
[.../* @__PURE__ */ new Set([...opts.include ?? [], ...persistedIncludes])],
|
|
10690
|
+
items,
|
|
10691
|
+
recommended,
|
|
10692
|
+
skipped
|
|
10693
|
+
);
|
|
10256
10694
|
const optInEligible = buildOptInEligible(items, skipped);
|
|
10257
10695
|
recommended.sort((a, b) => a.id.localeCompare(b.id));
|
|
10258
10696
|
skipped.sort((a, b) => a.id.localeCompare(b.id));
|
|
@@ -10463,9 +10901,9 @@ function buildOptInEligible(items, skipped) {
|
|
|
10463
10901
|
}
|
|
10464
10902
|
|
|
10465
10903
|
// src/commands/workspace/discover.ts
|
|
10466
|
-
import
|
|
10467
|
-
import
|
|
10468
|
-
import
|
|
10904
|
+
import path53 from "node:path";
|
|
10905
|
+
import fg6 from "fast-glob";
|
|
10906
|
+
import YAML3 from "yaml";
|
|
10469
10907
|
init_fs();
|
|
10470
10908
|
init_logger();
|
|
10471
10909
|
var DEFAULT_MAX_DEPTH = 3;
|
|
@@ -10500,16 +10938,16 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
10500
10938
|
suppressErrors: true
|
|
10501
10939
|
};
|
|
10502
10940
|
const [gitMatches, manifestMatches] = await Promise.all([
|
|
10503
|
-
|
|
10504
|
-
|
|
10941
|
+
fg6(GIT_MARKER, { ...commonOpts, ignore: IGNORE }),
|
|
10942
|
+
fg6(MANIFEST_MARKERS, { ...commonOpts, ignore: [...IGNORE, "**/.git/**"] })
|
|
10505
10943
|
]);
|
|
10506
10944
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
10507
10945
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
10508
10946
|
for (const match of gitMatches) {
|
|
10509
|
-
gitDirs.add(
|
|
10947
|
+
gitDirs.add(path53.posix.dirname(match));
|
|
10510
10948
|
}
|
|
10511
10949
|
for (const match of manifestMatches) {
|
|
10512
|
-
manifestDirs.add(
|
|
10950
|
+
manifestDirs.add(path53.posix.dirname(match));
|
|
10513
10951
|
}
|
|
10514
10952
|
const repoRoots = [...gitDirs];
|
|
10515
10953
|
const manifestSorted = [...manifestDirs].sort(
|
|
@@ -10526,9 +10964,9 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
10526
10964
|
async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
10527
10965
|
const repoRoots = (await findRepoRoots(workspaceRoot, maxDepth)).filter((dir) => dir !== ".");
|
|
10528
10966
|
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
10529
|
-
const absDir =
|
|
10530
|
-
const pkg = await readJson(
|
|
10531
|
-
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name :
|
|
10967
|
+
const absDir = path53.resolve(workspaceRoot, relDir);
|
|
10968
|
+
const pkg = await readJson(path53.join(absDir, "package.json"));
|
|
10969
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path53.basename(absDir);
|
|
10532
10970
|
let role = "auto";
|
|
10533
10971
|
try {
|
|
10534
10972
|
const scan = await scanProject(absDir);
|
|
@@ -10564,14 +11002,14 @@ function mergeWorkspaceConfig(existing, discovered, opts = {}) {
|
|
|
10564
11002
|
};
|
|
10565
11003
|
}
|
|
10566
11004
|
function renderWorkspaceYaml(config) {
|
|
10567
|
-
return
|
|
11005
|
+
return YAML3.stringify({
|
|
10568
11006
|
client: config.client,
|
|
10569
11007
|
repos: config.repos.map((r) => ({ name: r.name, path: r.path, role: r.role ?? "auto" })),
|
|
10570
11008
|
relationships: config.relationships
|
|
10571
11009
|
});
|
|
10572
11010
|
}
|
|
10573
11011
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
10574
|
-
const yamlPath =
|
|
11012
|
+
const yamlPath = path53.join(workspaceRoot, "haus.workspace.yaml");
|
|
10575
11013
|
const existingText = await readText(yamlPath);
|
|
10576
11014
|
const existing = parseWorkspaceConfig(existingText);
|
|
10577
11015
|
if (existingText && !existing) {
|
|
@@ -10955,7 +11393,7 @@ async function runInstall(options, deps = {}) {
|
|
|
10955
11393
|
if (result.created.length) parts.push(`${result.created.length} file(s) added`);
|
|
10956
11394
|
if (result.updated.length) parts.push(`${result.updated.length} file(s) updated`);
|
|
10957
11395
|
log(
|
|
10958
|
-
parts.length ? ` \u2022 ${parts.join(", ")} in ${claudeConfigDirLabel()} (skills, slash commands)` : " \u2022 already up to date \u2014 no files changed"
|
|
11396
|
+
parts.length ? ` \u2022 ${parts.join(", ")} in ${claudeConfigDirLabel()} (skills, agents, slash commands)` : " \u2022 already up to date \u2014 no files changed"
|
|
10959
11397
|
);
|
|
10960
11398
|
log(
|
|
10961
11399
|
` \u2022 ensured hooks + security rules are present in ${claudeConfigDirLabel()}/settings.json`
|
|
@@ -10978,7 +11416,7 @@ async function runInstall(options, deps = {}) {
|
|
|
10978
11416
|
}
|
|
10979
11417
|
|
|
10980
11418
|
// src/new-project/create-project.ts
|
|
10981
|
-
import
|
|
11419
|
+
import path54 from "node:path";
|
|
10982
11420
|
import fs42 from "fs-extra";
|
|
10983
11421
|
init_exec();
|
|
10984
11422
|
init_logger();
|
|
@@ -11142,7 +11580,7 @@ var defaultDeps = {
|
|
|
11142
11580
|
var NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
11143
11581
|
async function createProject(opts, deps = defaultDeps) {
|
|
11144
11582
|
const steps = [];
|
|
11145
|
-
const projectPath =
|
|
11583
|
+
const projectPath = path54.resolve(opts.parentDir, opts.name);
|
|
11146
11584
|
const installHint = `cd ${opts.name} && ${opts.pm}${opts.pm === "yarn" ? "" : " install"}`;
|
|
11147
11585
|
const fail = (step, detail) => {
|
|
11148
11586
|
steps.push({ step, status: "failed", detail });
|
|
@@ -11150,7 +11588,7 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
11150
11588
|
};
|
|
11151
11589
|
const stack = getStack(opts.stackId);
|
|
11152
11590
|
if (!stack) return fail("preflight", `Unknown stack "${opts.stackId}". Run \`haus new --list\`.`);
|
|
11153
|
-
if (!NAME_RE.test(opts.name) || opts.name.includes("/") || opts.name.includes("\\") ||
|
|
11591
|
+
if (!NAME_RE.test(opts.name) || opts.name.includes("/") || opts.name.includes("\\") || path54.dirname(projectPath) !== path54.resolve(opts.parentDir)) {
|
|
11154
11592
|
return fail(
|
|
11155
11593
|
"preflight",
|
|
11156
11594
|
`Invalid project name "${opts.name}" (lowercase letters, digits, . _ - only).`
|
|
@@ -11234,15 +11672,15 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
11234
11672
|
steps.push({ step: "scaffold", status: "done" });
|
|
11235
11673
|
phase = "git";
|
|
11236
11674
|
if (!opts.json) log("\u25B6 git: init/branch/commit");
|
|
11237
|
-
if (!await fs42.pathExists(
|
|
11675
|
+
if (!await fs42.pathExists(path54.join(projectPath, ".git"))) {
|
|
11238
11676
|
const init = await deps.git(["init"], { cwd: projectPath });
|
|
11239
11677
|
if (init.exitCode !== 0) return fail("git", `git init failed: ${init.stderr}`);
|
|
11240
11678
|
}
|
|
11241
11679
|
const branch = await deps.git(["branch", "-M", "main"], { cwd: projectPath });
|
|
11242
11680
|
if (branch.exitCode !== 0) return fail("git", `git branch -M main failed: ${branch.stderr}`);
|
|
11243
11681
|
const lockfile = opts.pm === "yarn" ? "yarn.lock" : opts.pm === "pnpm" ? "pnpm-lock.yaml" : "package-lock.json";
|
|
11244
|
-
if (!await fs42.pathExists(
|
|
11245
|
-
await fs42.writeFile(
|
|
11682
|
+
if (!await fs42.pathExists(path54.join(projectPath, lockfile))) {
|
|
11683
|
+
await fs42.writeFile(path54.join(projectPath, lockfile), "");
|
|
11246
11684
|
}
|
|
11247
11685
|
const add = await deps.git(["add", "-A"], { cwd: projectPath });
|
|
11248
11686
|
if (add.exitCode !== 0) return fail("git", `git add failed: ${add.stderr}`);
|
|
@@ -11491,6 +11929,7 @@ async function runRecommend(options) {
|
|
|
11491
11929
|
const root = process.cwd();
|
|
11492
11930
|
const context = await readContextOrScan(root);
|
|
11493
11931
|
const include = parseIdList(options.include);
|
|
11932
|
+
if (include.length > 0) await addProjectIncludedIds(root, include);
|
|
11494
11933
|
const activePluginIds = await activeGlobalPluginIds(root);
|
|
11495
11934
|
const result = await recommend(root, context, { include, activePluginIds });
|
|
11496
11935
|
await writeJson(hausPath(root, "recommendation.json"), result);
|
|
@@ -11511,7 +11950,7 @@ async function runRecommend(options) {
|
|
|
11511
11950
|
}
|
|
11512
11951
|
|
|
11513
11952
|
// src/commands/remove.ts
|
|
11514
|
-
import
|
|
11953
|
+
import path55 from "node:path";
|
|
11515
11954
|
init_fs();
|
|
11516
11955
|
init_logger();
|
|
11517
11956
|
init_paths();
|
|
@@ -11616,7 +12055,7 @@ async function runRemove(ids, options = {}) {
|
|
|
11616
12055
|
}
|
|
11617
12056
|
const allAbs = cleared.flatMap(({ existing }) => existing.map(({ abs }) => abs));
|
|
11618
12057
|
const backupRoot = await backupTrackedPaths(root, allAbs, say, "remove");
|
|
11619
|
-
report.backupDir = backupRoot === null ? null :
|
|
12058
|
+
report.backupDir = backupRoot === null ? null : path55.basename(backupRoot);
|
|
11620
12059
|
for (const { id, existing } of cleared) {
|
|
11621
12060
|
await deleteTrackedPaths(root, id, existing, { verb: "Removed", reason: "" }, say);
|
|
11622
12061
|
}
|
|
@@ -11704,7 +12143,7 @@ async function runScan(options) {
|
|
|
11704
12143
|
}
|
|
11705
12144
|
|
|
11706
12145
|
// src/commands/undo.ts
|
|
11707
|
-
import
|
|
12146
|
+
import path56 from "node:path";
|
|
11708
12147
|
import fs43 from "fs-extra";
|
|
11709
12148
|
|
|
11710
12149
|
// src/claude/managed-paths.ts
|
|
@@ -11754,7 +12193,7 @@ async function settingsHasHausContent(root) {
|
|
|
11754
12193
|
return settings._haus != null;
|
|
11755
12194
|
}
|
|
11756
12195
|
async function claudeMdHasHausBlock(root) {
|
|
11757
|
-
const filePath =
|
|
12196
|
+
const filePath = path56.join(root, "CLAUDE.md");
|
|
11758
12197
|
if (!await fs43.pathExists(filePath)) return false;
|
|
11759
12198
|
const text = await fs43.readFile(filePath, "utf8");
|
|
11760
12199
|
return text.includes(BLOCK_BEGIN);
|
|
@@ -11767,15 +12206,15 @@ async function stripProjectSettings(root) {
|
|
|
11767
12206
|
const hasContent = Object.keys(settings).length > 0;
|
|
11768
12207
|
if (hasContent) {
|
|
11769
12208
|
await writeProjectSettings(root, settings);
|
|
11770
|
-
log(`Stripped haus rules from ${
|
|
12209
|
+
log(`Stripped haus rules from ${path56.relative(root, settingsPath)} (user settings preserved).`);
|
|
11771
12210
|
return true;
|
|
11772
12211
|
}
|
|
11773
12212
|
await fs43.remove(settingsPath);
|
|
11774
|
-
log(`Removed ${
|
|
12213
|
+
log(`Removed ${path56.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
11775
12214
|
return true;
|
|
11776
12215
|
}
|
|
11777
12216
|
async function stripRootClaudeMd(root) {
|
|
11778
|
-
const filePath =
|
|
12217
|
+
const filePath = path56.join(root, "CLAUDE.md");
|
|
11779
12218
|
if (!await fs43.pathExists(filePath)) return false;
|
|
11780
12219
|
const prev = await fs43.readFile(filePath, "utf8");
|
|
11781
12220
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
@@ -11803,12 +12242,12 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
|
11803
12242
|
const backupRoot = hausPath(root, "backups", dirBackupDirName("undo", stamp));
|
|
11804
12243
|
for (const abs of managedAbsPaths) {
|
|
11805
12244
|
if (!await fs43.pathExists(abs)) continue;
|
|
11806
|
-
const rel =
|
|
11807
|
-
const backupPath =
|
|
11808
|
-
await fs43.ensureDir(
|
|
12245
|
+
const rel = path56.relative(root, abs);
|
|
12246
|
+
const backupPath = path56.join(backupRoot, rel);
|
|
12247
|
+
await fs43.ensureDir(path56.dirname(backupPath));
|
|
11809
12248
|
await fs43.copy(abs, backupPath);
|
|
11810
12249
|
}
|
|
11811
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
12250
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path56.relative(root, backupRoot)}.`);
|
|
11812
12251
|
}
|
|
11813
12252
|
async function runUndo(options) {
|
|
11814
12253
|
const root = options.root ?? process.cwd();
|
|
@@ -11822,7 +12261,7 @@ async function runUndo(options) {
|
|
|
11822
12261
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
11823
12262
|
return;
|
|
11824
12263
|
}
|
|
11825
|
-
const relTargets = managed.map((p) =>
|
|
12264
|
+
const relTargets = managed.map((p) => path56.relative(root, p));
|
|
11826
12265
|
const summaryParts = [...relTargets];
|
|
11827
12266
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
11828
12267
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -11850,7 +12289,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
11850
12289
|
for (const abs of coreManaged) {
|
|
11851
12290
|
if (!await fs43.pathExists(abs)) continue;
|
|
11852
12291
|
await fs43.remove(abs);
|
|
11853
|
-
log(`Removed ${
|
|
12292
|
+
log(`Removed ${path56.relative(root, abs)}`);
|
|
11854
12293
|
}
|
|
11855
12294
|
for (const { id, existing } of lockRemovable) {
|
|
11856
12295
|
await deleteTrackedPaths(root, id, existing, { verb: "Removed" }, log);
|
|
@@ -11867,7 +12306,7 @@ init_fs();
|
|
|
11867
12306
|
init_logger();
|
|
11868
12307
|
init_paths();
|
|
11869
12308
|
import crypto3 from "node:crypto";
|
|
11870
|
-
import
|
|
12309
|
+
import path57 from "node:path";
|
|
11871
12310
|
import fs44 from "fs-extra";
|
|
11872
12311
|
async function runUninstall(options = {}) {
|
|
11873
12312
|
const { force = false, removePlugins = removeGlobalPlugins } = options;
|
|
@@ -11924,14 +12363,14 @@ async function runUninstall(options = {}) {
|
|
|
11924
12363
|
continue;
|
|
11925
12364
|
}
|
|
11926
12365
|
await fs44.remove(entry.destPath);
|
|
11927
|
-
await pruneEmptyDir(
|
|
12366
|
+
await pruneEmptyDir(path57.dirname(entry.destPath));
|
|
11928
12367
|
result.deleted.push(entry.destPath);
|
|
11929
12368
|
}
|
|
11930
12369
|
const settings = await readSettings();
|
|
11931
12370
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
11932
12371
|
await writeSettings(stripped);
|
|
11933
12372
|
result.hooksStripped = true;
|
|
11934
|
-
const hausDir =
|
|
12373
|
+
const hausDir = path57.join(globalClaudeDir(), "haus");
|
|
11935
12374
|
const manifestPath2 = hausManifestPath();
|
|
11936
12375
|
if (fs44.pathExistsSync(manifestPath2)) {
|
|
11937
12376
|
await fs44.remove(manifestPath2);
|
|
@@ -12008,7 +12447,7 @@ async function runUninstallCommand(options) {
|
|
|
12008
12447
|
}
|
|
12009
12448
|
|
|
12010
12449
|
// src/commands/validate-catalog.ts
|
|
12011
|
-
import
|
|
12450
|
+
import path59 from "node:path";
|
|
12012
12451
|
|
|
12013
12452
|
// src/catalog/validate-core.ts
|
|
12014
12453
|
init_forbidden_content();
|
|
@@ -12016,7 +12455,7 @@ init_manifest_item_fields();
|
|
|
12016
12455
|
init_path_safety();
|
|
12017
12456
|
init_validation_rules2();
|
|
12018
12457
|
import fs45 from "node:fs";
|
|
12019
|
-
import
|
|
12458
|
+
import path58 from "node:path";
|
|
12020
12459
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
12021
12460
|
var ITEM_ID_PATTERN = /^haus\./;
|
|
12022
12461
|
function auditForbiddenStacks(items) {
|
|
@@ -12167,7 +12606,7 @@ function readForAudit(absPath, label, failures) {
|
|
|
12167
12606
|
}
|
|
12168
12607
|
}
|
|
12169
12608
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
12170
|
-
const rel =
|
|
12609
|
+
const rel = path58.relative(manifestDir, absPath);
|
|
12171
12610
|
const failures = [];
|
|
12172
12611
|
const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
|
|
12173
12612
|
if (text === null) return failures;
|
|
@@ -12189,25 +12628,25 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
12189
12628
|
const failures = [];
|
|
12190
12629
|
for (const item of items) {
|
|
12191
12630
|
if (!item.path || !isSafeCatalogPath(item.path)) continue;
|
|
12192
|
-
const absPath =
|
|
12631
|
+
const absPath = path58.join(manifestDir, item.path);
|
|
12193
12632
|
if (item.type === "skill") {
|
|
12194
|
-
const skillMd =
|
|
12633
|
+
const skillMd = path58.join(absPath, "SKILL.md");
|
|
12195
12634
|
if (!fs45.existsSync(skillMd)) {
|
|
12196
|
-
failures.push(`${item.id}: missing ${
|
|
12635
|
+
failures.push(`${item.id}: missing ${path58.relative(manifestDir, skillMd)}`);
|
|
12197
12636
|
continue;
|
|
12198
12637
|
}
|
|
12199
12638
|
const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
|
|
12200
12639
|
if (text === null) continue;
|
|
12201
12640
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
12202
12641
|
failures.push(
|
|
12203
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
12642
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path58.relative(manifestDir, skillMd)}`)
|
|
12204
12643
|
);
|
|
12205
12644
|
} else if (item.type === "agent") {
|
|
12206
12645
|
if (!fs45.existsSync(absPath)) {
|
|
12207
12646
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
12208
12647
|
continue;
|
|
12209
12648
|
}
|
|
12210
|
-
const rel =
|
|
12649
|
+
const rel = path58.relative(manifestDir, absPath);
|
|
12211
12650
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
12212
12651
|
if (text === null) continue;
|
|
12213
12652
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -12223,7 +12662,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
12223
12662
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
12224
12663
|
continue;
|
|
12225
12664
|
}
|
|
12226
|
-
const rel =
|
|
12665
|
+
const rel = path58.relative(manifestDir, absPath);
|
|
12227
12666
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
12228
12667
|
if (text === null) continue;
|
|
12229
12668
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -12281,7 +12720,7 @@ function walkAllFiles(dir, fn, failures) {
|
|
|
12281
12720
|
}
|
|
12282
12721
|
for (const entry of entries) {
|
|
12283
12722
|
if (entry.name === ".DS_Store") continue;
|
|
12284
|
-
const full =
|
|
12723
|
+
const full = path58.join(dir, entry.name);
|
|
12285
12724
|
if (entry.isDirectory()) walkAllFiles(full, fn, failures);
|
|
12286
12725
|
else fn(full);
|
|
12287
12726
|
}
|
|
@@ -12305,12 +12744,12 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
12305
12744
|
const failures = [];
|
|
12306
12745
|
const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
|
|
12307
12746
|
for (const dir of CONTENT_ROOTS) {
|
|
12308
|
-
const abs =
|
|
12747
|
+
const abs = path58.join(manifestDir, dir);
|
|
12309
12748
|
if (!fs45.existsSync(abs)) continue;
|
|
12310
12749
|
walkAllFiles(
|
|
12311
12750
|
abs,
|
|
12312
12751
|
(file) => {
|
|
12313
|
-
const relPath =
|
|
12752
|
+
const relPath = path58.relative(manifestDir, file).replace(/\\/g, "/");
|
|
12314
12753
|
if (!isPathClaimed(relPath, claimedPaths)) {
|
|
12315
12754
|
failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
|
|
12316
12755
|
}
|
|
@@ -12331,7 +12770,7 @@ function walkMd(dir, fn, failures) {
|
|
|
12331
12770
|
return;
|
|
12332
12771
|
}
|
|
12333
12772
|
for (const entry of entries) {
|
|
12334
|
-
const full =
|
|
12773
|
+
const full = path58.join(dir, entry.name);
|
|
12335
12774
|
if (entry.isDirectory()) walkMd(full, fn, failures);
|
|
12336
12775
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
12337
12776
|
}
|
|
@@ -12341,13 +12780,13 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
12341
12780
|
const pathSourceMap = buildItemPathSourceMap(items);
|
|
12342
12781
|
const pathTypeMap = buildItemPathTypeMap(items);
|
|
12343
12782
|
for (const dir of ["skills", "agents", "templates", "commands", "sources"]) {
|
|
12344
|
-
const abs =
|
|
12783
|
+
const abs = path58.join(manifestDir, dir);
|
|
12345
12784
|
if (!fs45.existsSync(abs)) continue;
|
|
12346
12785
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
12347
12786
|
walkMd(
|
|
12348
12787
|
abs,
|
|
12349
12788
|
(file) => {
|
|
12350
|
-
const rel =
|
|
12789
|
+
const rel = path58.relative(manifestDir, file);
|
|
12351
12790
|
const text = readForAudit(file, rel, failures);
|
|
12352
12791
|
if (text === null) return;
|
|
12353
12792
|
const norm = rel.replace(/\\/g, "/");
|
|
@@ -12395,8 +12834,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
12395
12834
|
process.exitCode = 1;
|
|
12396
12835
|
return;
|
|
12397
12836
|
}
|
|
12398
|
-
const abs =
|
|
12399
|
-
const manifestDir =
|
|
12837
|
+
const abs = path59.resolve(process.cwd(), manifestPath2);
|
|
12838
|
+
const manifestDir = path59.dirname(abs);
|
|
12400
12839
|
const data = await readJson(abs);
|
|
12401
12840
|
if (!data?.items) {
|
|
12402
12841
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -12417,7 +12856,7 @@ init_logger();
|
|
|
12417
12856
|
|
|
12418
12857
|
// src/workspace/worktree/add.ts
|
|
12419
12858
|
init_fs();
|
|
12420
|
-
import
|
|
12859
|
+
import path66 from "node:path";
|
|
12421
12860
|
import fs49 from "fs-extra";
|
|
12422
12861
|
|
|
12423
12862
|
// src/workspace/worktree/git-worktree.ts
|
|
@@ -12538,12 +12977,12 @@ async function hasUnpushedWork(cwd) {
|
|
|
12538
12977
|
}
|
|
12539
12978
|
|
|
12540
12979
|
// src/workspace/worktree/hydrate.ts
|
|
12541
|
-
import
|
|
12980
|
+
import path62 from "node:path";
|
|
12542
12981
|
import fs47 from "fs-extra";
|
|
12543
12982
|
|
|
12544
12983
|
// src/workspace/worktree/cow-copy.ts
|
|
12545
12984
|
init_exec();
|
|
12546
|
-
import
|
|
12985
|
+
import path60 from "node:path";
|
|
12547
12986
|
import fs46 from "fs-extra";
|
|
12548
12987
|
async function getMacFilesystemType(dir) {
|
|
12549
12988
|
const df = await runCommand("df", ["-P", dir]);
|
|
@@ -12578,11 +13017,11 @@ async function cowCopyDir(src, dest) {
|
|
|
12578
13017
|
const srcExists = await fs46.pathExists(src);
|
|
12579
13018
|
if (!srcExists)
|
|
12580
13019
|
return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
|
|
12581
|
-
const strategy = await detectCowStrategy(
|
|
13020
|
+
const strategy = await detectCowStrategy(path60.dirname(dest));
|
|
12582
13021
|
if (strategy === "unsupported" || strategy === "unknown-platform") {
|
|
12583
13022
|
return { strategy, attempted: false, ok: false };
|
|
12584
13023
|
}
|
|
12585
|
-
await fs46.ensureDir(
|
|
13024
|
+
await fs46.ensureDir(path60.dirname(dest));
|
|
12586
13025
|
const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
|
|
12587
13026
|
const result = await runCommand("cp", args);
|
|
12588
13027
|
return {
|
|
@@ -12596,8 +13035,8 @@ async function cowCopyDir(src, dest) {
|
|
|
12596
13035
|
// src/workspace/worktree/install.ts
|
|
12597
13036
|
init_exec();
|
|
12598
13037
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
12599
|
-
import
|
|
12600
|
-
import
|
|
13038
|
+
import path61 from "node:path";
|
|
13039
|
+
import fg7 from "fast-glob";
|
|
12601
13040
|
function detectInstallPlan(signals) {
|
|
12602
13041
|
if (signals.hasYarnLock) {
|
|
12603
13042
|
return signals.hasPackageManagerField ? { manager: "yarn", command: "corepack", args: ["yarn", "install"] } : { manager: "yarn", command: "yarn", args: ["install"] };
|
|
@@ -12617,11 +13056,11 @@ function detectInstallPlan(signals) {
|
|
|
12617
13056
|
return { manager: "none" };
|
|
12618
13057
|
}
|
|
12619
13058
|
async function computeLockfileSignals(dir) {
|
|
12620
|
-
const hasYarnLock = existsSync3(
|
|
12621
|
-
const hasPnpmLock = existsSync3(
|
|
12622
|
-
const hasPackageLock = existsSync3(
|
|
12623
|
-
const hasComposerLock = existsSync3(
|
|
12624
|
-
const dotnetFiles = await
|
|
13059
|
+
const hasYarnLock = existsSync3(path61.join(dir, "yarn.lock"));
|
|
13060
|
+
const hasPnpmLock = existsSync3(path61.join(dir, "pnpm-lock.yaml"));
|
|
13061
|
+
const hasPackageLock = existsSync3(path61.join(dir, "package-lock.json"));
|
|
13062
|
+
const hasComposerLock = existsSync3(path61.join(dir, "composer.lock"));
|
|
13063
|
+
const dotnetFiles = await fg7(["**/*.csproj", "**/*.sln"], {
|
|
12625
13064
|
cwd: dir,
|
|
12626
13065
|
onlyFiles: true,
|
|
12627
13066
|
suppressErrors: true,
|
|
@@ -12634,7 +13073,7 @@ async function computeLockfileSignals(dir) {
|
|
|
12634
13073
|
});
|
|
12635
13074
|
const hasDotnetProject = dotnetFiles.length > 0;
|
|
12636
13075
|
let hasPackageManagerField = false;
|
|
12637
|
-
const pkgPath =
|
|
13076
|
+
const pkgPath = path61.join(dir, "package.json");
|
|
12638
13077
|
if (existsSync3(pkgPath)) {
|
|
12639
13078
|
try {
|
|
12640
13079
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
@@ -12677,7 +13116,7 @@ function missingYarnrcEnvVars(content2, env) {
|
|
|
12677
13116
|
return findRequiredYarnrcEnvVars(content2).filter((name) => env[name] === void 0);
|
|
12678
13117
|
}
|
|
12679
13118
|
function missingYarnrcEnvVarsInDir(dir, env) {
|
|
12680
|
-
const yarnrcPath =
|
|
13119
|
+
const yarnrcPath = path61.join(dir, ".yarnrc.yml");
|
|
12681
13120
|
if (!existsSync3(yarnrcPath)) return [];
|
|
12682
13121
|
try {
|
|
12683
13122
|
return missingYarnrcEnvVars(readFileSync3(yarnrcPath, "utf8"), env);
|
|
@@ -12767,8 +13206,8 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
12767
13206
|
const start = Date.now();
|
|
12768
13207
|
const targets = [];
|
|
12769
13208
|
for (const target of HYDRATION_TARGETS) {
|
|
12770
|
-
const src =
|
|
12771
|
-
const dest =
|
|
13209
|
+
const src = path62.join(mainMemberPath, target);
|
|
13210
|
+
const dest = path62.join(worktreeMemberPath, target);
|
|
12772
13211
|
if (!await fs47.pathExists(src)) continue;
|
|
12773
13212
|
const destExists = await fs47.pathExists(dest);
|
|
12774
13213
|
if (!opts.force && destExists) continue;
|
|
@@ -12786,11 +13225,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
12786
13225
|
|
|
12787
13226
|
// src/workspace/worktree/local-files.ts
|
|
12788
13227
|
init_exec();
|
|
12789
|
-
import
|
|
13228
|
+
import path63 from "node:path";
|
|
12790
13229
|
import fs48 from "fs-extra";
|
|
12791
13230
|
var MACHINE_LOCAL_FILES = [
|
|
12792
13231
|
"docker-compose.local.yml",
|
|
12793
|
-
|
|
13232
|
+
path63.join(".claude", "settings.local.json")
|
|
12794
13233
|
];
|
|
12795
13234
|
async function isTracked(repoRoot, relFile) {
|
|
12796
13235
|
const gitPath = relFile.replace(/\\/g, "/");
|
|
@@ -12800,11 +13239,11 @@ async function isTracked(repoRoot, relFile) {
|
|
|
12800
13239
|
async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
12801
13240
|
const copied = [];
|
|
12802
13241
|
for (const rel of MACHINE_LOCAL_FILES) {
|
|
12803
|
-
const src =
|
|
13242
|
+
const src = path63.join(mainMemberPath, rel);
|
|
12804
13243
|
if (!await fs48.pathExists(src)) continue;
|
|
12805
13244
|
if (await isTracked(mainMemberPath, rel)) continue;
|
|
12806
|
-
const dest =
|
|
12807
|
-
await fs48.ensureDir(
|
|
13245
|
+
const dest = path63.join(worktreeMemberPath, rel);
|
|
13246
|
+
await fs48.ensureDir(path63.dirname(dest));
|
|
12808
13247
|
await fs48.copy(src, dest, { overwrite: true });
|
|
12809
13248
|
copied.push(rel);
|
|
12810
13249
|
}
|
|
@@ -12813,15 +13252,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
|
12813
13252
|
|
|
12814
13253
|
// src/workspace/worktree/root.ts
|
|
12815
13254
|
import { existsSync as existsSync4 } from "node:fs";
|
|
12816
|
-
import
|
|
13255
|
+
import path64 from "node:path";
|
|
12817
13256
|
async function resolveWorkspaceForWorktree(start) {
|
|
12818
13257
|
const rootInfo = await resolveRoots(start);
|
|
12819
13258
|
if (!rootInfo.isGitRepo) {
|
|
12820
13259
|
return { ok: false, reason: "Not inside a git repository." };
|
|
12821
13260
|
}
|
|
12822
13261
|
const workspaceRoot = rootInfo.mainRoot;
|
|
12823
|
-
const hasYaml = existsSync4(
|
|
12824
|
-
const hasManifest = existsSync4(
|
|
13262
|
+
const hasYaml = existsSync4(path64.join(workspaceRoot, WORKSPACE_FILE));
|
|
13263
|
+
const hasManifest = existsSync4(path64.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
12825
13264
|
if (!hasYaml && !hasManifest) {
|
|
12826
13265
|
return {
|
|
12827
13266
|
ok: false,
|
|
@@ -12847,10 +13286,10 @@ function selectMembers(members, only) {
|
|
|
12847
13286
|
|
|
12848
13287
|
// src/workspace/worktree/state.ts
|
|
12849
13288
|
init_fs();
|
|
12850
|
-
import
|
|
13289
|
+
import path65 from "node:path";
|
|
12851
13290
|
var WORKTREE_STATE_FILE = ".haus-worktree.json";
|
|
12852
13291
|
function worktreesDir(mainRoot) {
|
|
12853
|
-
return
|
|
13292
|
+
return path65.join(mainRoot, ".claude", "worktrees");
|
|
12854
13293
|
}
|
|
12855
13294
|
function isValidWorktreeSlug(slug) {
|
|
12856
13295
|
if (slug === "" || slug === "." || slug === "..") return false;
|
|
@@ -12863,15 +13302,15 @@ function worktreePath(mainRoot, slug) {
|
|
|
12863
13302
|
`Invalid worktree slug ${JSON.stringify(slug)} \u2014 slugs may only contain letters, digits, ".", "_", "-" (no path separators, and not "." or "..").`
|
|
12864
13303
|
);
|
|
12865
13304
|
}
|
|
12866
|
-
return
|
|
13305
|
+
return path65.join(worktreesDir(mainRoot), slug);
|
|
12867
13306
|
}
|
|
12868
13307
|
async function readWorktreeState(worktreeRoot) {
|
|
12869
|
-
const state = await readJson(
|
|
13308
|
+
const state = await readJson(path65.join(worktreeRoot, WORKTREE_STATE_FILE));
|
|
12870
13309
|
if (state == null || typeof state !== "object" || !Array.isArray(state.members)) return void 0;
|
|
12871
13310
|
return state;
|
|
12872
13311
|
}
|
|
12873
13312
|
async function writeWorktreeState(worktreeRoot, state) {
|
|
12874
|
-
await writeJson(
|
|
13313
|
+
await writeJson(path65.join(worktreeRoot, WORKTREE_STATE_FILE), state);
|
|
12875
13314
|
}
|
|
12876
13315
|
|
|
12877
13316
|
// src/workspace/worktree/add.ts
|
|
@@ -12905,7 +13344,7 @@ async function runAdd(opts) {
|
|
|
12905
13344
|
const memberResults = [];
|
|
12906
13345
|
for (const member of selected) {
|
|
12907
13346
|
const start = Date.now();
|
|
12908
|
-
const memberWorktreePath =
|
|
13347
|
+
const memberWorktreePath = path66.join(wsWorktreePath, member.folder);
|
|
12909
13348
|
const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
|
|
12910
13349
|
preferDefaultBranchFrom: true
|
|
12911
13350
|
});
|
|
@@ -12935,7 +13374,7 @@ async function runAdd(opts) {
|
|
|
12935
13374
|
await mapWithConcurrency(okResults, async (result) => {
|
|
12936
13375
|
const member = selected.find((m) => m.id === result.member);
|
|
12937
13376
|
if (!member) return;
|
|
12938
|
-
const memberWorktreePath =
|
|
13377
|
+
const memberWorktreePath = path66.join(wsWorktreePath, member.folder);
|
|
12939
13378
|
const hydrationStart = Date.now();
|
|
12940
13379
|
if (opts.hydrate !== false) {
|
|
12941
13380
|
result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
|
|
@@ -13002,25 +13441,25 @@ async function runDryRun(args) {
|
|
|
13002
13441
|
|
|
13003
13442
|
// src/workspace/worktree/doctor.ts
|
|
13004
13443
|
import { existsSync as existsSync6 } from "node:fs";
|
|
13005
|
-
import
|
|
13444
|
+
import path68 from "node:path";
|
|
13006
13445
|
|
|
13007
13446
|
// src/workspace/worktree/orphans.ts
|
|
13008
13447
|
import { existsSync as existsSync5 } from "node:fs";
|
|
13009
|
-
import
|
|
13448
|
+
import path67 from "node:path";
|
|
13010
13449
|
var WORKSPACE_REPO_ID = "(workspace)";
|
|
13011
13450
|
async function findOrphanedRegistrations(repos, wsWorktreesDir) {
|
|
13012
13451
|
const orphans = [];
|
|
13013
13452
|
for (const repo of repos) {
|
|
13014
13453
|
const entries = await listWorktrees(repo.absPath);
|
|
13015
13454
|
for (const entry of entries) {
|
|
13016
|
-
const entryPath =
|
|
13017
|
-
const marker = `${
|
|
13455
|
+
const entryPath = path67.normalize(entry.path);
|
|
13456
|
+
const marker = `${path67.sep}.claude${path67.sep}worktrees${path67.sep}`;
|
|
13018
13457
|
const idx = entryPath.indexOf(marker);
|
|
13019
13458
|
if (idx === -1) continue;
|
|
13020
13459
|
const rest = entryPath.slice(idx + marker.length);
|
|
13021
|
-
const slug = rest.split(
|
|
13460
|
+
const slug = rest.split(path67.sep)[0];
|
|
13022
13461
|
if (!slug) continue;
|
|
13023
|
-
if (existsSync5(
|
|
13462
|
+
if (existsSync5(path67.join(wsWorktreesDir, slug))) continue;
|
|
13024
13463
|
orphans.push({
|
|
13025
13464
|
repoId: repo.id,
|
|
13026
13465
|
ownerRepo: repo.absPath,
|
|
@@ -13035,7 +13474,7 @@ async function findOrphanedRegistrations(repos, wsWorktreesDir) {
|
|
|
13035
13474
|
// src/workspace/worktree/doctor.ts
|
|
13036
13475
|
var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
|
|
13037
13476
|
function looksHydrated(memberPath) {
|
|
13038
|
-
return HYDRATION_MARKERS.some((marker) => existsSync6(
|
|
13477
|
+
return HYDRATION_MARKERS.some((marker) => existsSync6(path68.join(memberPath, marker)));
|
|
13039
13478
|
}
|
|
13040
13479
|
async function runDoctor2() {
|
|
13041
13480
|
const rootInfo = await resolveRoots();
|
|
@@ -13051,7 +13490,7 @@ async function runDoctor2() {
|
|
|
13051
13490
|
};
|
|
13052
13491
|
}
|
|
13053
13492
|
const workspaceRoot = rootInfo.mainRoot;
|
|
13054
|
-
const isWorkspace = existsSync6(
|
|
13493
|
+
const isWorkspace = existsSync6(path68.join(workspaceRoot, WORKSPACE_FILE)) || existsSync6(path68.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
13055
13494
|
if (!isWorkspace) {
|
|
13056
13495
|
return {
|
|
13057
13496
|
isWorkspace: false,
|
|
@@ -13066,7 +13505,7 @@ async function runDoctor2() {
|
|
|
13066
13505
|
};
|
|
13067
13506
|
}
|
|
13068
13507
|
const wsWorktreesDir = worktreesDir(workspaceRoot);
|
|
13069
|
-
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${
|
|
13508
|
+
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path68.sep}`);
|
|
13070
13509
|
const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
|
|
13071
13510
|
const workspaceConfig = await readWorkspaceConfig(workspaceRoot);
|
|
13072
13511
|
const autoHydrate = workspaceConfig?.worktree?.autoHydrate === true;
|
|
@@ -13077,10 +13516,10 @@ async function runDoctor2() {
|
|
|
13077
13516
|
let slug;
|
|
13078
13517
|
if (inWorkspaceWorktree) {
|
|
13079
13518
|
const wtPath = rootInfo.repoRoot;
|
|
13080
|
-
slug = rootInfo.worktreeName ??
|
|
13519
|
+
slug = rootInfo.worktreeName ?? path68.basename(wtPath);
|
|
13081
13520
|
const state = await readWorktreeState(wtPath);
|
|
13082
13521
|
for (const member of members) {
|
|
13083
|
-
const memberPath =
|
|
13522
|
+
const memberPath = path68.join(wtPath, member.folder);
|
|
13084
13523
|
const materialized = existsSync6(memberPath);
|
|
13085
13524
|
const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
|
|
13086
13525
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
@@ -13120,7 +13559,7 @@ async function runDoctor2() {
|
|
|
13120
13559
|
wsWorktreesDir
|
|
13121
13560
|
);
|
|
13122
13561
|
const orphans = orphanScan.map(
|
|
13123
|
-
(o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${
|
|
13562
|
+
(o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${path68.join(wsWorktreesDir, o.slug)} no longer exists.`
|
|
13124
13563
|
);
|
|
13125
13564
|
if (orphans.length > 0) {
|
|
13126
13565
|
problems.push(...orphans);
|
|
@@ -13141,7 +13580,7 @@ async function runDoctor2() {
|
|
|
13141
13580
|
// src/workspace/worktree/hydrate-command.ts
|
|
13142
13581
|
init_fs();
|
|
13143
13582
|
import { existsSync as existsSync7 } from "node:fs";
|
|
13144
|
-
import
|
|
13583
|
+
import path69 from "node:path";
|
|
13145
13584
|
async function runHydrateWorktree(opts) {
|
|
13146
13585
|
const rootInfo = await resolveRoots();
|
|
13147
13586
|
if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
|
|
@@ -13152,7 +13591,7 @@ async function runHydrateWorktree(opts) {
|
|
|
13152
13591
|
}
|
|
13153
13592
|
const workspaceRoot = rootInfo.mainRoot;
|
|
13154
13593
|
const wtPath = rootInfo.repoRoot;
|
|
13155
|
-
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${
|
|
13594
|
+
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path69.sep}`)) {
|
|
13156
13595
|
return {
|
|
13157
13596
|
ok: false,
|
|
13158
13597
|
error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
|
|
@@ -13164,13 +13603,13 @@ async function runHydrateWorktree(opts) {
|
|
|
13164
13603
|
return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
|
|
13165
13604
|
}
|
|
13166
13605
|
const state = await readWorktreeState(wtPath);
|
|
13167
|
-
const slug = state?.slug ??
|
|
13606
|
+
const slug = state?.slug ?? path69.basename(wtPath);
|
|
13168
13607
|
const detectedBranch = await currentBranch(wtPath);
|
|
13169
13608
|
const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
|
|
13170
13609
|
if (opts.dryRun) {
|
|
13171
13610
|
const results2 = await Promise.all(
|
|
13172
13611
|
selected.map(async (member) => {
|
|
13173
|
-
const memberPath =
|
|
13612
|
+
const memberPath = path69.join(wtPath, member.folder);
|
|
13174
13613
|
if (existsSync7(memberPath)) {
|
|
13175
13614
|
return { member: member.id, folder: member.folder, status: "planned" };
|
|
13176
13615
|
}
|
|
@@ -13191,7 +13630,7 @@ async function runHydrateWorktree(opts) {
|
|
|
13191
13630
|
const results = await mapWithConcurrency(
|
|
13192
13631
|
selected,
|
|
13193
13632
|
async (member) => {
|
|
13194
|
-
const memberPath =
|
|
13633
|
+
const memberPath = path69.join(wtPath, member.folder);
|
|
13195
13634
|
let branchAction;
|
|
13196
13635
|
if (!existsSync7(memberPath)) {
|
|
13197
13636
|
if (!opts.materialize) {
|
|
@@ -13254,7 +13693,7 @@ async function runHydrateWorktree(opts) {
|
|
|
13254
13693
|
}
|
|
13255
13694
|
|
|
13256
13695
|
// src/workspace/worktree/list.ts
|
|
13257
|
-
import
|
|
13696
|
+
import path70 from "node:path";
|
|
13258
13697
|
import fs50 from "fs-extra";
|
|
13259
13698
|
async function runList2() {
|
|
13260
13699
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -13267,12 +13706,12 @@ async function runList2() {
|
|
|
13267
13706
|
const configuredMembers = await readMembers(rootInfo);
|
|
13268
13707
|
const worktrees = [];
|
|
13269
13708
|
for (const slug of slugs) {
|
|
13270
|
-
const wtPath =
|
|
13709
|
+
const wtPath = path70.join(dir, slug);
|
|
13271
13710
|
const state = await readWorktreeState(wtPath);
|
|
13272
13711
|
const members = [];
|
|
13273
13712
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
13274
13713
|
for (const m of source) {
|
|
13275
|
-
const memberPath =
|
|
13714
|
+
const memberPath = path70.join(wtPath, m.folder);
|
|
13276
13715
|
const materialized = await fs50.pathExists(memberPath);
|
|
13277
13716
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
13278
13717
|
members.push({
|
|
@@ -13334,7 +13773,7 @@ async function runPrune2(opts) {
|
|
|
13334
13773
|
}
|
|
13335
13774
|
|
|
13336
13775
|
// src/workspace/worktree/remove.ts
|
|
13337
|
-
import
|
|
13776
|
+
import path71 from "node:path";
|
|
13338
13777
|
import fs51 from "fs-extra";
|
|
13339
13778
|
async function runRemove2(opts) {
|
|
13340
13779
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -13361,7 +13800,7 @@ async function runRemove2(opts) {
|
|
|
13361
13800
|
targets.push({
|
|
13362
13801
|
repo: configMember?.id ?? stateMember.id,
|
|
13363
13802
|
ownerRepo,
|
|
13364
|
-
path:
|
|
13803
|
+
path: path71.join(wsWorktreePath, stateMember.folder)
|
|
13365
13804
|
});
|
|
13366
13805
|
}
|
|
13367
13806
|
const existingTargets = [];
|
|
@@ -13699,7 +14138,7 @@ async function runWorktree(action, opts = {}) {
|
|
|
13699
14138
|
|
|
13700
14139
|
// src/commands/workspace.ts
|
|
13701
14140
|
import { existsSync as existsSync11, statSync as statSync3 } from "node:fs";
|
|
13702
|
-
import
|
|
14141
|
+
import path80 from "node:path";
|
|
13703
14142
|
init_fs();
|
|
13704
14143
|
init_logger();
|
|
13705
14144
|
|
|
@@ -13758,7 +14197,7 @@ ${summaries.map(
|
|
|
13758
14197
|
|
|
13759
14198
|
// src/commands/workspace/doctor.ts
|
|
13760
14199
|
import { existsSync as existsSync8 } from "node:fs";
|
|
13761
|
-
import
|
|
14200
|
+
import path73 from "node:path";
|
|
13762
14201
|
init_logger();
|
|
13763
14202
|
init_paths();
|
|
13764
14203
|
|
|
@@ -13766,14 +14205,14 @@ init_paths();
|
|
|
13766
14205
|
init_fs();
|
|
13767
14206
|
init_paths();
|
|
13768
14207
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
13769
|
-
import
|
|
14208
|
+
import path72 from "node:path";
|
|
13770
14209
|
var MANIFEST_FILE2 = "workspace.manifest.json";
|
|
13771
14210
|
function manifestPath(workspaceRoot) {
|
|
13772
14211
|
return hausPath(workspaceRoot, MANIFEST_FILE2);
|
|
13773
14212
|
}
|
|
13774
14213
|
function hausVersion() {
|
|
13775
14214
|
try {
|
|
13776
|
-
const pkg = JSON.parse(readFileSync4(
|
|
14215
|
+
const pkg = JSON.parse(readFileSync4(path72.join(packageRoot(), "package.json"), "utf8"));
|
|
13777
14216
|
return pkg.version ?? "0.0.0";
|
|
13778
14217
|
} catch {
|
|
13779
14218
|
return "0.0.0";
|
|
@@ -13865,7 +14304,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
13865
14304
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
13866
14305
|
const catalogRefByRepo = [];
|
|
13867
14306
|
for (const repo of config.repos) {
|
|
13868
|
-
const repoRoot =
|
|
14307
|
+
const repoRoot = path73.resolve(workspaceRoot, repo.path);
|
|
13869
14308
|
const entry = manifestByName.get(repo.name);
|
|
13870
14309
|
if (!entry) {
|
|
13871
14310
|
flag({
|
|
@@ -13938,7 +14377,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
13938
14377
|
});
|
|
13939
14378
|
continue;
|
|
13940
14379
|
}
|
|
13941
|
-
if (!existsSync8(
|
|
14380
|
+
if (!existsSync8(path73.join(workspaceRoot, entry.path))) {
|
|
13942
14381
|
flag({
|
|
13943
14382
|
repo: entry.repo,
|
|
13944
14383
|
kind: "missing-linked-context-copy",
|
|
@@ -13984,17 +14423,17 @@ function emit(args) {
|
|
|
13984
14423
|
init_logger();
|
|
13985
14424
|
|
|
13986
14425
|
// src/workspace/link-context/link.ts
|
|
13987
|
-
import
|
|
14426
|
+
import path76 from "node:path";
|
|
13988
14427
|
import fs54 from "fs-extra";
|
|
13989
14428
|
init_fs();
|
|
13990
14429
|
init_logger();
|
|
13991
14430
|
|
|
13992
14431
|
// src/workspace/link-context/plan.ts
|
|
13993
|
-
import
|
|
14432
|
+
import path75 from "node:path";
|
|
13994
14433
|
import fs53 from "fs-extra";
|
|
13995
14434
|
|
|
13996
14435
|
// src/workspace/link-context/scan-source.ts
|
|
13997
|
-
import
|
|
14436
|
+
import path74 from "node:path";
|
|
13998
14437
|
import fs52 from "fs-extra";
|
|
13999
14438
|
var ASSET_SUBDIR = {
|
|
14000
14439
|
skill: "skills",
|
|
@@ -14003,36 +14442,36 @@ var ASSET_SUBDIR = {
|
|
|
14003
14442
|
};
|
|
14004
14443
|
async function hasLinkableContext(member) {
|
|
14005
14444
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
14006
|
-
if (await fs52.pathExists(
|
|
14445
|
+
if (await fs52.pathExists(path74.join(member.absPath, ".claude", subdir))) return true;
|
|
14007
14446
|
}
|
|
14008
14447
|
return false;
|
|
14009
14448
|
}
|
|
14010
14449
|
async function collectSkills(member) {
|
|
14011
|
-
const dir =
|
|
14450
|
+
const dir = path74.join(member.absPath, ".claude", "skills");
|
|
14012
14451
|
if (!await fs52.pathExists(dir)) return [];
|
|
14013
14452
|
const names = await fs52.readdir(dir);
|
|
14014
14453
|
const out = [];
|
|
14015
14454
|
for (const name of [...names].sort()) {
|
|
14016
|
-
const abs =
|
|
14455
|
+
const abs = path74.join(dir, name);
|
|
14017
14456
|
const stat = await fs52.lstat(abs);
|
|
14018
14457
|
if (!stat.isDirectory()) continue;
|
|
14019
|
-
if (!await fs52.pathExists(
|
|
14020
|
-
out.push({ type: "skill", name, sourceRelPath:
|
|
14458
|
+
if (!await fs52.pathExists(path74.join(abs, "SKILL.md"))) continue;
|
|
14459
|
+
out.push({ type: "skill", name, sourceRelPath: path74.posix.join(".claude", "skills", name) });
|
|
14021
14460
|
}
|
|
14022
14461
|
return out;
|
|
14023
14462
|
}
|
|
14024
14463
|
async function collectFlatMarkdown(member, type) {
|
|
14025
14464
|
const subdir = ASSET_SUBDIR[type];
|
|
14026
|
-
const dir =
|
|
14465
|
+
const dir = path74.join(member.absPath, ".claude", subdir);
|
|
14027
14466
|
if (!await fs52.pathExists(dir)) return [];
|
|
14028
14467
|
const names = await fs52.readdir(dir);
|
|
14029
14468
|
const out = [];
|
|
14030
14469
|
for (const fileName of [...names].sort()) {
|
|
14031
14470
|
if (!fileName.endsWith(".md")) continue;
|
|
14032
|
-
const abs =
|
|
14471
|
+
const abs = path74.join(dir, fileName);
|
|
14033
14472
|
if (!(await fs52.lstat(abs)).isFile()) continue;
|
|
14034
14473
|
const name = fileName.slice(0, -".md".length);
|
|
14035
|
-
out.push({ type, name, sourceRelPath:
|
|
14474
|
+
out.push({ type, name, sourceRelPath: path74.posix.join(".claude", subdir, fileName) });
|
|
14036
14475
|
}
|
|
14037
14476
|
return out;
|
|
14038
14477
|
}
|
|
@@ -14047,9 +14486,9 @@ async function collectSourceAssets(member) {
|
|
|
14047
14486
|
|
|
14048
14487
|
// src/workspace/link-context/plan.ts
|
|
14049
14488
|
function repoFolderPrefix(member) {
|
|
14050
|
-
const base =
|
|
14489
|
+
const base = path75.posix.basename(member.folder);
|
|
14051
14490
|
if (base && base !== "." && base !== "/") return base;
|
|
14052
|
-
const fromAbs =
|
|
14491
|
+
const fromAbs = path75.basename(member.absPath);
|
|
14053
14492
|
return fromAbs || member.id;
|
|
14054
14493
|
}
|
|
14055
14494
|
function assetKey(type, destKey) {
|
|
@@ -14106,7 +14545,7 @@ async function buildLinkPlan(members) {
|
|
|
14106
14545
|
name: asset.name,
|
|
14107
14546
|
destKey,
|
|
14108
14547
|
sourceRelPath: asset.sourceRelPath,
|
|
14109
|
-
sourceAbsPath:
|
|
14548
|
+
sourceAbsPath: path75.join(member.absPath, asset.sourceRelPath),
|
|
14110
14549
|
sourceHash
|
|
14111
14550
|
});
|
|
14112
14551
|
}
|
|
@@ -14117,7 +14556,7 @@ async function buildLinkPlan(members) {
|
|
|
14117
14556
|
function destRelPathFor(type, destKey) {
|
|
14118
14557
|
const subdir = `${type}s`;
|
|
14119
14558
|
const leaf = type === "skill" ? destKey : `${destKey}.md`;
|
|
14120
|
-
return
|
|
14559
|
+
return path76.posix.join(".claude", subdir, leaf);
|
|
14121
14560
|
}
|
|
14122
14561
|
async function runLinkContext(workspaceRoot, opts = {}) {
|
|
14123
14562
|
const write = resolveWriteMode(opts, "preview-by-default").kind === "write";
|
|
@@ -14143,9 +14582,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
14143
14582
|
const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
14144
14583
|
for (const entry of plan.entries) {
|
|
14145
14584
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
14146
|
-
const destAbs =
|
|
14585
|
+
const destAbs = path76.join(workspaceRoot, destRel);
|
|
14147
14586
|
if (write) {
|
|
14148
|
-
await fs54.ensureDir(
|
|
14587
|
+
await fs54.ensureDir(path76.dirname(destAbs));
|
|
14149
14588
|
if (await fs54.pathExists(destAbs)) await fs54.remove(destAbs);
|
|
14150
14589
|
await fs54.copy(entry.sourceAbsPath, destAbs, {
|
|
14151
14590
|
overwrite: true,
|
|
@@ -14234,10 +14673,10 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
|
|
|
14234
14673
|
|
|
14235
14674
|
// src/commands/workspace/setup.ts
|
|
14236
14675
|
import { existsSync as existsSync9, statSync } from "node:fs";
|
|
14237
|
-
import
|
|
14676
|
+
import path78 from "node:path";
|
|
14238
14677
|
|
|
14239
14678
|
// src/claude/write-workspace-claude-md.ts
|
|
14240
|
-
import
|
|
14679
|
+
import path77 from "node:path";
|
|
14241
14680
|
import fs55 from "fs-extra";
|
|
14242
14681
|
init_fs();
|
|
14243
14682
|
init_logger();
|
|
@@ -14251,16 +14690,19 @@ function buildWorkspaceImportBlock(client, members) {
|
|
|
14251
14690
|
`# Workspace: ${client}`,
|
|
14252
14691
|
"",
|
|
14253
14692
|
"Member repos:",
|
|
14693
|
+
"",
|
|
14254
14694
|
...memberLines
|
|
14255
14695
|
].join("\n");
|
|
14256
14696
|
return `${WORKSPACE_BLOCK_BEGIN}
|
|
14697
|
+
|
|
14257
14698
|
${body}
|
|
14699
|
+
|
|
14258
14700
|
${WORKSPACE_BLOCK_END}`;
|
|
14259
14701
|
}
|
|
14260
14702
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
14261
14703
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
14262
14704
|
const dryRun = opts.dryRun ?? false;
|
|
14263
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
14705
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path77.join(workspaceRoot, "CLAUDE.md");
|
|
14264
14706
|
const say = opts.quiet ? () => {
|
|
14265
14707
|
} : log;
|
|
14266
14708
|
const existed = await fs55.pathExists(filePath);
|
|
@@ -14293,20 +14735,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
14293
14735
|
init_fs();
|
|
14294
14736
|
init_logger();
|
|
14295
14737
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
14296
|
-
let dir =
|
|
14738
|
+
let dir = path78.resolve(start);
|
|
14297
14739
|
for (; ; ) {
|
|
14298
|
-
if (existsSync9(
|
|
14299
|
-
const parent =
|
|
14300
|
-
if (parent === dir) return
|
|
14740
|
+
if (existsSync9(path78.join(dir, WORKSPACE_FILE))) return dir;
|
|
14741
|
+
const parent = path78.dirname(dir);
|
|
14742
|
+
if (parent === dir) return path78.resolve(start);
|
|
14301
14743
|
dir = parent;
|
|
14302
14744
|
}
|
|
14303
14745
|
}
|
|
14304
14746
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
14305
|
-
return
|
|
14747
|
+
return path78.resolve(workspaceRoot, repoPath) === path78.resolve(workspaceRoot);
|
|
14306
14748
|
}
|
|
14307
14749
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
14308
14750
|
const apply = options.write ?? false;
|
|
14309
|
-
const configText = await readText(
|
|
14751
|
+
const configText = await readText(path78.join(workspaceRoot, WORKSPACE_FILE));
|
|
14310
14752
|
if (!configText) {
|
|
14311
14753
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
14312
14754
|
process.exitCode = 1;
|
|
@@ -14333,7 +14775,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
14333
14775
|
const statuses = [];
|
|
14334
14776
|
const aggregateInputs = [];
|
|
14335
14777
|
for (const repo of repos) {
|
|
14336
|
-
const repoRoot =
|
|
14778
|
+
const repoRoot = path78.resolve(workspaceRoot, repo.path);
|
|
14337
14779
|
say(`
|
|
14338
14780
|
\u2192 ${repo.name} (${repo.path})`);
|
|
14339
14781
|
let res;
|
|
@@ -14449,7 +14891,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
14449
14891
|
const status = statusByName.get(repo.name);
|
|
14450
14892
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
14451
14893
|
if (status?.status === "ok") {
|
|
14452
|
-
const lock = await checkLock(
|
|
14894
|
+
const lock = await checkLock(path78.resolve(workspaceRoot, repo.path));
|
|
14453
14895
|
manifestRepos.push({
|
|
14454
14896
|
name: repo.name,
|
|
14455
14897
|
path: repo.path,
|
|
@@ -14505,7 +14947,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
14505
14947
|
try {
|
|
14506
14948
|
const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
|
|
14507
14949
|
if (linkResult.ok) {
|
|
14508
|
-
written.push(...linkResult.added.map((rel) =>
|
|
14950
|
+
written.push(...linkResult.added.map((rel) => path78.join(workspaceRoot, rel)));
|
|
14509
14951
|
if (!options.json) {
|
|
14510
14952
|
say(
|
|
14511
14953
|
`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` : ""}.`
|
|
@@ -14538,7 +14980,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
14538
14980
|
|
|
14539
14981
|
// src/commands/workspace/undo.ts
|
|
14540
14982
|
import { existsSync as existsSync10, statSync as statSync2 } from "node:fs";
|
|
14541
|
-
import
|
|
14983
|
+
import path79 from "node:path";
|
|
14542
14984
|
import fs56 from "fs-extra";
|
|
14543
14985
|
init_fs();
|
|
14544
14986
|
init_logger();
|
|
@@ -14569,7 +15011,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
14569
15011
|
}
|
|
14570
15012
|
if (options.dryRun) {
|
|
14571
15013
|
for (const repo of config.repos) {
|
|
14572
|
-
const repoRoot =
|
|
15014
|
+
const repoRoot = path79.resolve(workspaceRoot, repo.path);
|
|
14573
15015
|
log(`
|
|
14574
15016
|
\u2192 ${repo.name} (${repo.path})`);
|
|
14575
15017
|
try {
|
|
@@ -14601,11 +15043,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
14601
15043
|
];
|
|
14602
15044
|
for (const abs of workspaceArtifacts) {
|
|
14603
15045
|
if (await fs56.pathExists(abs)) {
|
|
14604
|
-
log(`[dry-run] Would remove ${
|
|
15046
|
+
log(`[dry-run] Would remove ${path79.relative(workspaceRoot, abs)}`);
|
|
14605
15047
|
}
|
|
14606
15048
|
}
|
|
14607
15049
|
if (!await fs56.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
|
|
14608
|
-
const claudeMd = await readText(
|
|
15050
|
+
const claudeMd = await readText(path79.join(workspaceRoot, "CLAUDE.md"));
|
|
14609
15051
|
if (claudeMd?.includes(WORKSPACE_BLOCK_BEGIN) || claudeMd && hasLegacyWorkspaceBlock(claudeMd)) {
|
|
14610
15052
|
log("[dry-run] Would remove the workspace haus import block from CLAUDE.md");
|
|
14611
15053
|
}
|
|
@@ -14624,7 +15066,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
14624
15066
|
}
|
|
14625
15067
|
let anyRepoFailed = false;
|
|
14626
15068
|
for (const repo of config.repos) {
|
|
14627
|
-
const repoRoot =
|
|
15069
|
+
const repoRoot = path79.resolve(workspaceRoot, repo.path);
|
|
14628
15070
|
log(`
|
|
14629
15071
|
\u2192 ${repo.name} (${repo.path})`);
|
|
14630
15072
|
try {
|
|
@@ -14653,20 +15095,20 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
14653
15095
|
const abs = hausPath(workspaceRoot, rel);
|
|
14654
15096
|
if (await fs56.pathExists(abs)) {
|
|
14655
15097
|
await fs56.remove(abs);
|
|
14656
|
-
log(`Removed ${
|
|
15098
|
+
log(`Removed ${path79.relative(workspaceRoot, abs)}`);
|
|
14657
15099
|
}
|
|
14658
15100
|
}
|
|
14659
15101
|
const manifestFile = manifestPath(workspaceRoot);
|
|
14660
15102
|
if (await fs56.pathExists(manifestFile)) {
|
|
14661
15103
|
await fs56.remove(manifestFile);
|
|
14662
|
-
log(`Removed ${
|
|
15104
|
+
log(`Removed ${path79.relative(workspaceRoot, manifestFile)}`);
|
|
14663
15105
|
}
|
|
14664
15106
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
14665
15107
|
if (await fs56.pathExists(workspaceMdPath)) {
|
|
14666
15108
|
await fs56.remove(workspaceMdPath);
|
|
14667
|
-
log(`Removed ${
|
|
15109
|
+
log(`Removed ${path79.relative(workspaceRoot, workspaceMdPath)}`);
|
|
14668
15110
|
} else {
|
|
14669
|
-
const claudeMdPath =
|
|
15111
|
+
const claudeMdPath = path79.join(workspaceRoot, "CLAUDE.md");
|
|
14670
15112
|
if (existsSync10(claudeMdPath)) {
|
|
14671
15113
|
const prev = await readText(claudeMdPath);
|
|
14672
15114
|
if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
|
|
@@ -14735,7 +15177,7 @@ relationships: []
|
|
|
14735
15177
|
log("Workspace initialized.");
|
|
14736
15178
|
}
|
|
14737
15179
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
14738
|
-
const configText = await readText(
|
|
15180
|
+
const configText = await readText(path80.join(workspaceRoot, WORKSPACE_FILE));
|
|
14739
15181
|
if (!configText) {
|
|
14740
15182
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
14741
15183
|
process.exitCode = 1;
|
|
@@ -14757,7 +15199,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
14757
15199
|
const inputs = [];
|
|
14758
15200
|
const failures = [];
|
|
14759
15201
|
for (const repo of config.repos) {
|
|
14760
|
-
const repoRoot =
|
|
15202
|
+
const repoRoot = path80.resolve(workspaceRoot, repo.path);
|
|
14761
15203
|
try {
|
|
14762
15204
|
if (!existsSync11(repoRoot) || !statSync3(repoRoot).isDirectory()) {
|
|
14763
15205
|
throw new Error("path is not a directory");
|
|
@@ -14826,7 +15268,7 @@ init_logger();
|
|
|
14826
15268
|
init_paths();
|
|
14827
15269
|
function cliVersion() {
|
|
14828
15270
|
try {
|
|
14829
|
-
const pkgPath =
|
|
15271
|
+
const pkgPath = path81.join(packageRoot(), "package.json");
|
|
14830
15272
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
14831
15273
|
return pkg.version ?? "0.0.0";
|
|
14832
15274
|
} catch {
|
|
@@ -14836,7 +15278,7 @@ function cliVersion() {
|
|
|
14836
15278
|
var program = new Command();
|
|
14837
15279
|
function validateRuntimeNodeVersion() {
|
|
14838
15280
|
try {
|
|
14839
|
-
const pkgPath =
|
|
15281
|
+
const pkgPath = path81.join(packageRoot(), "package.json");
|
|
14840
15282
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
14841
15283
|
const requiredRange = pkg.engines?.node;
|
|
14842
15284
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|