@haus-tech/haus-workflow 5.0.0 → 5.1.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 +13 -0
- package/dist/cli.js +913 -751
- package/library/catalog/manifest.json +67 -61
- package/library/global/agents/request-assessor.md +10 -2
- package/library/global/agents/request-specifier-eskalerad.md +8 -0
- package/library/global/agents/request-specifier.md +8 -3
- package/library/global/skills/request/SKILL.md +590 -227
- package/library/global/skills/request/scripts/archive.mjs +736 -59
- package/library/global/skills/request/scripts/compare-scores.mjs +119 -13
- package/library/global/skills/request/scripts/contract-check.mjs +268 -40
- package/library/global/skills/request/scripts/kontroll.mjs +622 -0
- package/library/global/skills/request/scripts/korning.mjs +1502 -0
- package/library/global/skills/request/scripts/lib.mjs +477 -5
- package/library/global/skills/request/scripts/precheck.mjs +132 -5
- package/library/global/skills/request/scripts/report.mjs +85 -14
- package/library/global/skills/request/scripts/svep.mjs +640 -0
- package/library/global/skills/request-assess-impact/SKILL.md +45 -4
- package/library/global/skills/request-score/SKILL.md +51 -11
- package/library/global/skills/request-score/references/poangskala.md +3 -0
- package/library/global/skills/request-sync/SKILL.md +3 -3
- package/library/global/skills/request-write-spec/SKILL.md +9 -4
- package/library/global/skills/request-write-spec/references/specformat.md +48 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1998,7 +1998,7 @@ var init_sync = __esm({
|
|
|
1998
1998
|
// src/cli.ts
|
|
1999
1999
|
init_offline();
|
|
2000
2000
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2001
|
-
import
|
|
2001
|
+
import path91 from "node:path";
|
|
2002
2002
|
import { Command, Option } from "commander";
|
|
2003
2003
|
|
|
2004
2004
|
// src/commands/apply.ts
|
|
@@ -2354,9 +2354,162 @@ function summarizeGitignoredHausDirs(paths) {
|
|
|
2354
2354
|
return [...dirs];
|
|
2355
2355
|
}
|
|
2356
2356
|
|
|
2357
|
+
// src/claude/migrate-request-state.ts
|
|
2358
|
+
init_exec();
|
|
2359
|
+
init_paths();
|
|
2360
|
+
import path12 from "node:path";
|
|
2361
|
+
import fs9 from "fs-extra";
|
|
2362
|
+
var LEGACY_REQUEST_DIR = ".haus";
|
|
2363
|
+
var LEGACY_REQUEST_STATE_MOVES = [
|
|
2364
|
+
{ from: `${LEGACY_REQUEST_DIR}/requests`, to: `${HAUS_DIR}/requests`, keepTracked: false },
|
|
2365
|
+
{ from: `${LEGACY_REQUEST_DIR}/request.json`, to: `${HAUS_DIR}/request.json`, keepTracked: true }
|
|
2366
|
+
];
|
|
2367
|
+
async function isSymlink(abs) {
|
|
2368
|
+
try {
|
|
2369
|
+
return (await fs9.lstat(abs)).isSymbolicLink();
|
|
2370
|
+
} catch {
|
|
2371
|
+
return false;
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
async function exists(abs) {
|
|
2375
|
+
if (await fs9.pathExists(abs)) return true;
|
|
2376
|
+
return isSymlink(abs);
|
|
2377
|
+
}
|
|
2378
|
+
async function trackedUnder(root, rel) {
|
|
2379
|
+
try {
|
|
2380
|
+
const result = await runGit(["ls-files", "--", rel], { cwd: root, timeout: 5e3 });
|
|
2381
|
+
if (result.exitCode !== 0) return [];
|
|
2382
|
+
return result.stdout.split("\n").map((line2) => line2.trim()).filter(Boolean);
|
|
2383
|
+
} catch {
|
|
2384
|
+
return [];
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
async function runGitStep(root, args, notify) {
|
|
2388
|
+
try {
|
|
2389
|
+
const result = await runGit([...args], { cwd: root, timeout: 5e3 });
|
|
2390
|
+
if (result.exitCode === 0) return true;
|
|
2391
|
+
notify(
|
|
2392
|
+
`Could not run \`git ${args.join(" ")}\`: ${result.stderr.trim() || result.stdout.trim()}`
|
|
2393
|
+
);
|
|
2394
|
+
return false;
|
|
2395
|
+
} catch (error2) {
|
|
2396
|
+
notify(
|
|
2397
|
+
`Could not run \`git ${args.join(" ")}\`: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2398
|
+
);
|
|
2399
|
+
return false;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
async function reconcileIndex(root, move, dryRun, notify) {
|
|
2403
|
+
const tracked = await trackedUnder(root, move.from);
|
|
2404
|
+
if (tracked.length === 0) return;
|
|
2405
|
+
if (dryRun) {
|
|
2406
|
+
notify(
|
|
2407
|
+
move.keepTracked ? `Would stage the move of ${move.from} to ${move.to} in git (${tracked.length} tracked file(s)).` : `Would untrack ${tracked.length} committed file(s) under ${move.from} from git \u2014 request pricing material should never have been committed.`
|
|
2408
|
+
);
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
const removed = await runGitStep(
|
|
2412
|
+
root,
|
|
2413
|
+
["rm", "--cached", "-r", "--quiet", "--", move.from],
|
|
2414
|
+
notify
|
|
2415
|
+
);
|
|
2416
|
+
if (!removed) return;
|
|
2417
|
+
if (move.keepTracked) {
|
|
2418
|
+
await runGitStep(root, ["add", "--", move.to], notify);
|
|
2419
|
+
notify(
|
|
2420
|
+
`Staged the move of ${move.from} to ${move.to} in git \u2014 it is board configuration and stays tracked. Run \`git status\` to review before committing this change.`
|
|
2421
|
+
);
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
notify(
|
|
2425
|
+
`Untracked ${tracked.length} file(s) under ${move.from} from git \u2014 request pricing material should never have been committed, and \`${move.to}\` is gitignored from here on. Run \`git status\` to review before committing this change.`
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
async function removeEmptyLegacyDir(root, previewedMoves, dryRun, notify) {
|
|
2429
|
+
const legacyAbs = path12.join(root, LEGACY_REQUEST_DIR);
|
|
2430
|
+
if (!await fs9.pathExists(legacyAbs)) return;
|
|
2431
|
+
if (await isSymlink(legacyAbs)) return;
|
|
2432
|
+
try {
|
|
2433
|
+
const remaining = await fs9.readdir(legacyAbs);
|
|
2434
|
+
if (dryRun) {
|
|
2435
|
+
const wouldLeave = new Set(previewedMoves.map((m) => path12.basename(m.from)));
|
|
2436
|
+
if (remaining.some((entry) => !wouldLeave.has(entry))) return;
|
|
2437
|
+
notify(`Would remove the ${LEGACY_REQUEST_DIR}/ directory once the moves above empty it.`);
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
2440
|
+
if (remaining.length > 0) return;
|
|
2441
|
+
await fs9.remove(legacyAbs);
|
|
2442
|
+
notify(`Removed the now-empty ${LEGACY_REQUEST_DIR}/ directory.`);
|
|
2443
|
+
} catch (error2) {
|
|
2444
|
+
notify(
|
|
2445
|
+
`Could not remove the empty ${LEGACY_REQUEST_DIR}/ directory: ${error2 instanceof Error ? error2.message : String(error2)}. The request state was migrated successfully \u2014 delete ${LEGACY_REQUEST_DIR}/ by hand.`
|
|
2446
|
+
);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
async function migrateRequestState(root, dryRun, notify) {
|
|
2450
|
+
const moved = [];
|
|
2451
|
+
const conflicts = [];
|
|
2452
|
+
const failures = [];
|
|
2453
|
+
const legacyAbs = path12.join(root, LEGACY_REQUEST_DIR);
|
|
2454
|
+
if (!await exists(legacyAbs)) return { moved, conflicts, failures };
|
|
2455
|
+
if (await isSymlink(legacyAbs)) {
|
|
2456
|
+
for (const move of LEGACY_REQUEST_STATE_MOVES) {
|
|
2457
|
+
conflicts.push({ ...move, reason: "legacy-dir-is-symlink" });
|
|
2458
|
+
}
|
|
2459
|
+
notify(
|
|
2460
|
+
`Refused to migrate request state: ${LEGACY_REQUEST_DIR}/ is a symlink. Moving through it would relocate the link target's real archive into this repo. Migrate the link's target yourself, replace the link with a real directory, then re-run.`
|
|
2461
|
+
);
|
|
2462
|
+
return { moved, conflicts, failures };
|
|
2463
|
+
}
|
|
2464
|
+
for (const move of LEGACY_REQUEST_STATE_MOVES) {
|
|
2465
|
+
const fromAbs = path12.join(root, move.from);
|
|
2466
|
+
const toAbs = path12.join(root, move.to);
|
|
2467
|
+
if (!await exists(fromAbs)) continue;
|
|
2468
|
+
if (await isSymlink(fromAbs)) {
|
|
2469
|
+
conflicts.push({ ...move, reason: "source-is-symlink" });
|
|
2470
|
+
notify(
|
|
2471
|
+
`Refused to migrate ${move.from} to ${move.to}: ${move.from} is a symlink. Moving the link would leave the real request state outside \`${HAUS_DIR}/\`. Move the link's target yourself, then re-run.`
|
|
2472
|
+
);
|
|
2473
|
+
continue;
|
|
2474
|
+
}
|
|
2475
|
+
if (await exists(toAbs)) {
|
|
2476
|
+
conflicts.push({ ...move, reason: "destination-exists" });
|
|
2477
|
+
notify(
|
|
2478
|
+
`Refused to migrate ${move.from} to ${move.to}: ${move.to} already exists. Two locations hold request state \u2014 reconcile them by hand, then remove ${move.from} so this migration can finish.`
|
|
2479
|
+
);
|
|
2480
|
+
continue;
|
|
2481
|
+
}
|
|
2482
|
+
if (dryRun) {
|
|
2483
|
+
moved.push(move);
|
|
2484
|
+
notify(`Would move ${move.from} to ${move.to} (request state now lives under ${HAUS_DIR}/).`);
|
|
2485
|
+
await reconcileIndex(root, move, true, notify);
|
|
2486
|
+
continue;
|
|
2487
|
+
}
|
|
2488
|
+
const trackedBefore = await trackedUnder(root, move.from);
|
|
2489
|
+
try {
|
|
2490
|
+
await fs9.ensureDir(path12.dirname(toAbs));
|
|
2491
|
+
await fs9.move(fromAbs, toAbs);
|
|
2492
|
+
} catch (error2) {
|
|
2493
|
+
const message2 = error2 instanceof Error ? error2.message : String(error2);
|
|
2494
|
+
failures.push({ ...move, message: message2 });
|
|
2495
|
+
notify(
|
|
2496
|
+
`Could not migrate ${move.from} to ${move.to}: ${message2}. ${move.from} was left in place \u2014 fix the cause and re-run \`haus apply --write\`.`
|
|
2497
|
+
);
|
|
2498
|
+
continue;
|
|
2499
|
+
}
|
|
2500
|
+
moved.push(move);
|
|
2501
|
+
notify(
|
|
2502
|
+
`Moved ${move.from} to ${move.to} \u2014 request state now lives under \`${HAUS_DIR}/\`, where haus's gitignore rules cover it (ADR-0103).`
|
|
2503
|
+
);
|
|
2504
|
+
if (trackedBefore.length > 0) await reconcileIndex(root, move, false, notify);
|
|
2505
|
+
}
|
|
2506
|
+
if (moved.length > 0) await removeEmptyLegacyDir(root, moved, dryRun, notify);
|
|
2507
|
+
return { moved, conflicts, failures };
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2357
2510
|
// src/claude/write-claude-files.ts
|
|
2358
|
-
import
|
|
2359
|
-
import
|
|
2511
|
+
import path27 from "node:path";
|
|
2512
|
+
import fs25 from "fs-extra";
|
|
2360
2513
|
init_ingest_catalog();
|
|
2361
2514
|
|
|
2362
2515
|
// src/catalog/skill-description.ts
|
|
@@ -2441,7 +2594,7 @@ function findOrphanedLockEntries(lock, recommendedIds) {
|
|
|
2441
2594
|
init_validation_rules2();
|
|
2442
2595
|
|
|
2443
2596
|
// src/scanner/detection.ts
|
|
2444
|
-
import
|
|
2597
|
+
import path13 from "node:path";
|
|
2445
2598
|
|
|
2446
2599
|
// src/security/sensitive-paths.ts
|
|
2447
2600
|
var DENY_PATHS = [
|
|
@@ -2638,7 +2791,7 @@ function finalizeRoles(registryRoles, deps, files) {
|
|
|
2638
2791
|
function collectUnsupportedSignals(files) {
|
|
2639
2792
|
return [
|
|
2640
2793
|
...new Set(
|
|
2641
|
-
files.map((f) => UNSUPPORTED_MARKERS[
|
|
2794
|
+
files.map((f) => UNSUPPORTED_MARKERS[path13.basename(f)]).filter((s) => Boolean(s))
|
|
2642
2795
|
)
|
|
2643
2796
|
].sort();
|
|
2644
2797
|
}
|
|
@@ -2721,8 +2874,8 @@ function isSupersededByPlugin(lockId, activePinIds) {
|
|
|
2721
2874
|
}
|
|
2722
2875
|
|
|
2723
2876
|
// src/scanner/detect-package-manager.ts
|
|
2724
|
-
import
|
|
2725
|
-
import
|
|
2877
|
+
import path14 from "node:path";
|
|
2878
|
+
import fs10 from "fs-extra";
|
|
2726
2879
|
function detectPackageManager(root, packageManagerField) {
|
|
2727
2880
|
const field = String(packageManagerField ?? "").trim();
|
|
2728
2881
|
if (field.startsWith("yarn@")) {
|
|
@@ -2740,16 +2893,16 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
2740
2893
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
2741
2894
|
return "unknown";
|
|
2742
2895
|
}
|
|
2743
|
-
if (
|
|
2744
|
-
if (
|
|
2745
|
-
if (
|
|
2896
|
+
if (fs10.existsSync(path14.join(root, "yarn.lock"))) return "yarn";
|
|
2897
|
+
if (fs10.existsSync(path14.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
2898
|
+
if (fs10.existsSync(path14.join(root, "package-lock.json"))) return "npm";
|
|
2746
2899
|
return "unknown";
|
|
2747
2900
|
}
|
|
2748
2901
|
|
|
2749
2902
|
// src/update/hash-installed.ts
|
|
2750
|
-
import
|
|
2903
|
+
import path16 from "node:path";
|
|
2751
2904
|
import fg3 from "fast-glob";
|
|
2752
|
-
import
|
|
2905
|
+
import fs12 from "fs-extra";
|
|
2753
2906
|
|
|
2754
2907
|
// src/claude/haus-managed-header.ts
|
|
2755
2908
|
function parseHausManagedAttrs(line2) {
|
|
@@ -2787,9 +2940,9 @@ function parseHausManagedHeader(line2) {
|
|
|
2787
2940
|
}
|
|
2788
2941
|
|
|
2789
2942
|
// src/claude/superpowers-install.ts
|
|
2790
|
-
import
|
|
2943
|
+
import path15 from "node:path";
|
|
2791
2944
|
import fg2 from "fast-glob";
|
|
2792
|
-
import
|
|
2945
|
+
import fs11 from "fs-extra";
|
|
2793
2946
|
init_logger();
|
|
2794
2947
|
var SKILL_DEP_ARTIFACTS = ["node_modules/**"];
|
|
2795
2948
|
var SKILL_DEP_ARTIFACT_DIRS = /* @__PURE__ */ new Set(["node_modules"]);
|
|
@@ -2812,34 +2965,34 @@ async function previewInstalledSkill(srcFile, opts) {
|
|
|
2812
2965
|
return next === text ? void 0 : Buffer.from(next, "utf8");
|
|
2813
2966
|
}
|
|
2814
2967
|
async function readRewritableText(file) {
|
|
2815
|
-
const raw = await
|
|
2968
|
+
const raw = await fs11.readFile(file);
|
|
2816
2969
|
const text = raw.toString("utf8");
|
|
2817
2970
|
return Buffer.from(text, "utf8").equals(raw) ? text : void 0;
|
|
2818
2971
|
}
|
|
2819
2972
|
async function composeInstalledDescription(destination, signal) {
|
|
2820
|
-
const skillFile =
|
|
2821
|
-
if (!await
|
|
2973
|
+
const skillFile = path15.join(destination, SKILL_ENTRY_FILE);
|
|
2974
|
+
if (!await fs11.pathExists(skillFile)) return;
|
|
2822
2975
|
const text = await readRewritableText(skillFile);
|
|
2823
2976
|
if (text === void 0) return;
|
|
2824
2977
|
const composed = rewriteSkillDescription(text, signal);
|
|
2825
|
-
if (composed !== text) await
|
|
2978
|
+
if (composed !== text) await fs11.writeFile(skillFile, composed, "utf8");
|
|
2826
2979
|
}
|
|
2827
2980
|
async function installCatalogSkill(sourcePath, destination, opts) {
|
|
2828
2981
|
if (opts.dryRun) return;
|
|
2829
|
-
await
|
|
2830
|
-
if (await
|
|
2982
|
+
await fs11.ensureDir(path15.dirname(destination));
|
|
2983
|
+
if (await fs11.pathExists(destination)) {
|
|
2831
2984
|
const extra = await extraDestFiles(sourcePath, destination);
|
|
2832
2985
|
if (extra.length > 0) {
|
|
2833
2986
|
warn(
|
|
2834
2987
|
`${destination}: removing ${extra.length} untracked file(s) not present in the catalog source before reinstall: ${extra.join(", ")}`
|
|
2835
2988
|
);
|
|
2836
2989
|
}
|
|
2837
|
-
for (const entry of await
|
|
2990
|
+
for (const entry of await fs11.readdir(destination)) {
|
|
2838
2991
|
if (SKILL_DEP_ARTIFACT_DIRS.has(entry)) continue;
|
|
2839
|
-
await
|
|
2992
|
+
await fs11.remove(path15.join(destination, entry));
|
|
2840
2993
|
}
|
|
2841
2994
|
}
|
|
2842
|
-
await
|
|
2995
|
+
await fs11.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
2843
2996
|
if (opts.description) await composeInstalledDescription(destination, opts.description);
|
|
2844
2997
|
}
|
|
2845
2998
|
|
|
@@ -2862,12 +3015,12 @@ async function hashInstalledPaths(root, relPaths, opts = {}) {
|
|
|
2862
3015
|
const normalized = [...new Set(relPaths.map((p) => p.replace(/\\/g, "/")))].sort();
|
|
2863
3016
|
const fileDigests = [];
|
|
2864
3017
|
for (const rel of normalized) {
|
|
2865
|
-
const abs =
|
|
2866
|
-
if (!await
|
|
2867
|
-
const stat = followSymlinks ? await
|
|
3018
|
+
const abs = path16.join(root, rel);
|
|
3019
|
+
if (!await fs12.pathExists(abs)) continue;
|
|
3020
|
+
const stat = followSymlinks ? await fs12.stat(abs) : await fs12.lstat(abs);
|
|
2868
3021
|
if (!followSymlinks && stat.isSymbolicLink()) continue;
|
|
2869
3022
|
if (stat.isFile()) {
|
|
2870
|
-
const body = await
|
|
3023
|
+
const body = await fs12.readFile(abs);
|
|
2871
3024
|
fileDigests.push({ rel, digest: digestFileContent(body) });
|
|
2872
3025
|
continue;
|
|
2873
3026
|
}
|
|
@@ -2887,10 +3040,10 @@ async function hashInstalledPaths(root, relPaths, opts = {}) {
|
|
|
2887
3040
|
ignore: [...SKILL_DEP_ARTIFACTS]
|
|
2888
3041
|
});
|
|
2889
3042
|
for (const sub of inner.sort()) {
|
|
2890
|
-
const absFile =
|
|
2891
|
-
if (!followSymlinks && (await
|
|
2892
|
-
const relFile =
|
|
2893
|
-
const body = await
|
|
3043
|
+
const absFile = path16.join(abs, sub);
|
|
3044
|
+
if (!followSymlinks && (await fs12.lstat(absFile)).isSymbolicLink()) continue;
|
|
3045
|
+
const relFile = path16.join(rel, sub).replace(/\\/g, "/");
|
|
3046
|
+
const body = await fs12.readFile(absFile);
|
|
2894
3047
|
fileDigests.push({ rel: relFile, digest: digestFileContent(body) });
|
|
2895
3048
|
}
|
|
2896
3049
|
}
|
|
@@ -2929,15 +3082,15 @@ init_logger();
|
|
|
2929
3082
|
init_paths();
|
|
2930
3083
|
|
|
2931
3084
|
// src/claude/managed-write.ts
|
|
2932
|
-
import
|
|
3085
|
+
import fs13 from "fs-extra";
|
|
2933
3086
|
init_fs();
|
|
2934
3087
|
init_logger();
|
|
2935
3088
|
init_paths();
|
|
2936
3089
|
async function writeManagedText(root, filePath, nextText, dryRun, quiet = false) {
|
|
2937
3090
|
const say = quiet ? () => {
|
|
2938
3091
|
} : log;
|
|
2939
|
-
const existed = await
|
|
2940
|
-
const prev = existed ? await
|
|
3092
|
+
const existed = await fs13.pathExists(filePath);
|
|
3093
|
+
const prev = existed ? await fs13.readFile(filePath, "utf8") : "";
|
|
2941
3094
|
const printable = displayPath(root, filePath);
|
|
2942
3095
|
if (dryRun) {
|
|
2943
3096
|
if (!existed) {
|
|
@@ -3023,19 +3176,19 @@ function buildAllowRules() {
|
|
|
3023
3176
|
|
|
3024
3177
|
// src/install/settings-merge.ts
|
|
3025
3178
|
init_fs();
|
|
3026
|
-
import
|
|
3027
|
-
import
|
|
3179
|
+
import path18 from "node:path";
|
|
3180
|
+
import fs14 from "fs-extra";
|
|
3028
3181
|
|
|
3029
3182
|
// src/install/manifest.ts
|
|
3030
3183
|
init_fs();
|
|
3031
3184
|
init_paths();
|
|
3032
|
-
import
|
|
3185
|
+
import path17 from "node:path";
|
|
3033
3186
|
var MANIFEST_SCHEMA = "haus-install-manifest/1";
|
|
3034
3187
|
function globalClaudeDir() {
|
|
3035
3188
|
return claudeConfigDir();
|
|
3036
3189
|
}
|
|
3037
3190
|
function hausManifestPath() {
|
|
3038
|
-
return
|
|
3191
|
+
return path17.join(globalClaudeDir(), "haus", "install-manifest.json");
|
|
3039
3192
|
}
|
|
3040
3193
|
async function readManifest() {
|
|
3041
3194
|
return readJson(hausManifestPath());
|
|
@@ -3074,7 +3227,7 @@ function buildManifest(source, files, hooks, plugins) {
|
|
|
3074
3227
|
// src/install/settings-merge.ts
|
|
3075
3228
|
init_fs();
|
|
3076
3229
|
function settingsJsonPath() {
|
|
3077
|
-
return
|
|
3230
|
+
return path18.join(globalClaudeDir(), "settings.json");
|
|
3078
3231
|
}
|
|
3079
3232
|
async function readSettings() {
|
|
3080
3233
|
const filePath = settingsJsonPath();
|
|
@@ -3334,7 +3487,7 @@ function stripHausAsk(settings) {
|
|
|
3334
3487
|
async function loadHooksFragment(fragmentPath) {
|
|
3335
3488
|
let raw;
|
|
3336
3489
|
try {
|
|
3337
|
-
raw = await
|
|
3490
|
+
raw = await fs14.readJson(fragmentPath);
|
|
3338
3491
|
} catch {
|
|
3339
3492
|
return [];
|
|
3340
3493
|
}
|
|
@@ -3394,8 +3547,8 @@ function buildDenyRules() {
|
|
|
3394
3547
|
if (RUNTIME_ONLY_DENY_COMMANDS.includes(command)) continue;
|
|
3395
3548
|
rules.push(`Bash(${command}:*)`);
|
|
3396
3549
|
}
|
|
3397
|
-
for (const
|
|
3398
|
-
const pattern = DENY_DIRS.has(
|
|
3550
|
+
for (const path92 of DENY_PATHS) {
|
|
3551
|
+
const pattern = DENY_DIRS.has(path92) ? `${path92}/**` : path92;
|
|
3399
3552
|
for (const tool of FILE_TOOLS) {
|
|
3400
3553
|
rules.push(`${tool}(${pattern})`);
|
|
3401
3554
|
}
|
|
@@ -3485,8 +3638,8 @@ async function applyProjectSettingsMerge(root) {
|
|
|
3485
3638
|
|
|
3486
3639
|
// src/claude/remove-items.ts
|
|
3487
3640
|
import crypto2 from "node:crypto";
|
|
3488
|
-
import
|
|
3489
|
-
import
|
|
3641
|
+
import path19 from "node:path";
|
|
3642
|
+
import fs15 from "fs-extra";
|
|
3490
3643
|
init_fs();
|
|
3491
3644
|
init_paths();
|
|
3492
3645
|
|
|
@@ -3516,17 +3669,17 @@ function classifyBackupName(name) {
|
|
|
3516
3669
|
async function unsafeTrackedPath(root, rel) {
|
|
3517
3670
|
const abs = resolveContainedPath(root, rel);
|
|
3518
3671
|
if (abs === null) return "escapes-root";
|
|
3519
|
-
if (!await
|
|
3672
|
+
if (!await fs15.pathExists(abs)) {
|
|
3520
3673
|
try {
|
|
3521
|
-
if ((await
|
|
3674
|
+
if ((await fs15.lstat(abs)).isSymbolicLink()) return "symlink";
|
|
3522
3675
|
} catch {
|
|
3523
3676
|
return null;
|
|
3524
3677
|
}
|
|
3525
3678
|
return null;
|
|
3526
3679
|
}
|
|
3527
|
-
if ((await
|
|
3528
|
-
const [realAbs, realRoot] = await Promise.all([
|
|
3529
|
-
const prefix = realRoot.endsWith(
|
|
3680
|
+
if ((await fs15.lstat(abs)).isSymbolicLink()) return "symlink";
|
|
3681
|
+
const [realAbs, realRoot] = await Promise.all([fs15.realpath(abs), fs15.realpath(root)]);
|
|
3682
|
+
const prefix = realRoot.endsWith(path19.sep) ? realRoot : realRoot + path19.sep;
|
|
3530
3683
|
if (realAbs !== realRoot && !realAbs.startsWith(prefix)) return "symlink";
|
|
3531
3684
|
return null;
|
|
3532
3685
|
}
|
|
@@ -3550,8 +3703,8 @@ async function resolveExistingTrackedPaths(root, itemId, relPaths, warnFn, unsaf
|
|
|
3550
3703
|
warnFn(`Refusing to touch ${itemId}'s recorded path through a symlink: ${rel}`);
|
|
3551
3704
|
continue;
|
|
3552
3705
|
}
|
|
3553
|
-
const abs =
|
|
3554
|
-
if (await
|
|
3706
|
+
const abs = path19.resolve(root, rel);
|
|
3707
|
+
if (await fs15.pathExists(abs)) existing.push({ rel, abs });
|
|
3555
3708
|
}
|
|
3556
3709
|
return existing;
|
|
3557
3710
|
}
|
|
@@ -3569,29 +3722,29 @@ async function backupTrackedPaths(root, absPaths, say, prefix) {
|
|
|
3569
3722
|
const stamp = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${crypto2.randomBytes(3).toString("hex")}`;
|
|
3570
3723
|
const backupRoot = hausPath(root, "backups", dirBackupDirName(prefix, stamp));
|
|
3571
3724
|
for (const abs of absPaths) {
|
|
3572
|
-
if (!await
|
|
3573
|
-
const rel =
|
|
3574
|
-
const backupPath =
|
|
3575
|
-
await
|
|
3576
|
-
await
|
|
3725
|
+
if (!await fs15.pathExists(abs)) continue;
|
|
3726
|
+
const rel = path19.relative(root, abs);
|
|
3727
|
+
const backupPath = path19.join(backupRoot, rel);
|
|
3728
|
+
await fs15.ensureDir(path19.dirname(backupPath));
|
|
3729
|
+
await fs15.copy(abs, backupPath);
|
|
3577
3730
|
}
|
|
3578
3731
|
say(
|
|
3579
|
-
`Backed up ${absPaths.length} path(s) to ${
|
|
3732
|
+
`Backed up ${absPaths.length} path(s) to ${path19.relative(root, backupRoot)} before deleting them.`
|
|
3580
3733
|
);
|
|
3581
3734
|
return backupRoot;
|
|
3582
3735
|
}
|
|
3583
3736
|
async function deleteTrackedPaths(root, itemId, existing, { verb, reason }, say) {
|
|
3584
3737
|
for (const { abs } of existing) {
|
|
3585
|
-
await
|
|
3586
|
-
await pruneEmptyDir(
|
|
3738
|
+
await fs15.remove(abs);
|
|
3739
|
+
await pruneEmptyDir(path19.dirname(abs));
|
|
3587
3740
|
say([verb, reason, displayPath(root, abs)].filter(Boolean).join(" ") + ` (${itemId})`);
|
|
3588
3741
|
}
|
|
3589
3742
|
}
|
|
3590
3743
|
|
|
3591
3744
|
// src/claude/skill-dep-install.ts
|
|
3592
3745
|
init_exec();
|
|
3593
|
-
import
|
|
3594
|
-
import
|
|
3746
|
+
import path20 from "node:path";
|
|
3747
|
+
import fs16 from "fs-extra";
|
|
3595
3748
|
function skillDepInstallCommand(manager) {
|
|
3596
3749
|
switch (manager) {
|
|
3597
3750
|
case "npm":
|
|
@@ -3628,7 +3781,7 @@ function skillDepInstallCommand(manager) {
|
|
|
3628
3781
|
}
|
|
3629
3782
|
async function hasDeclaredDependencies(dir) {
|
|
3630
3783
|
try {
|
|
3631
|
-
const raw = await
|
|
3784
|
+
const raw = await fs16.readFile(path20.join(dir, "package.json"), "utf8");
|
|
3632
3785
|
const parsed = JSON.parse(raw);
|
|
3633
3786
|
if (typeof parsed !== "object" || parsed === null) return false;
|
|
3634
3787
|
const deps = parsed.dependencies;
|
|
@@ -3640,16 +3793,16 @@ async function hasDeclaredDependencies(dir) {
|
|
|
3640
3793
|
}
|
|
3641
3794
|
async function realpathOrSelf(target) {
|
|
3642
3795
|
try {
|
|
3643
|
-
return await
|
|
3796
|
+
return await fs16.realpath(target);
|
|
3644
3797
|
} catch {
|
|
3645
|
-
return
|
|
3798
|
+
return path20.resolve(target);
|
|
3646
3799
|
}
|
|
3647
3800
|
}
|
|
3648
3801
|
async function resolveSkillInstallDir(projectRoot, skillDir) {
|
|
3649
|
-
const skillsRoot = await realpathOrSelf(
|
|
3802
|
+
const skillsRoot = await realpathOrSelf(path20.join(projectRoot, ".claude", "skills"));
|
|
3650
3803
|
const resolved = await realpathOrSelf(skillDir);
|
|
3651
|
-
const rel =
|
|
3652
|
-
const contained = rel !== "" && !
|
|
3804
|
+
const rel = path20.relative(skillsRoot, resolved);
|
|
3805
|
+
const contained = rel !== "" && !path20.isAbsolute(rel) && !rel.split(path20.sep).includes("..") && // A directory named exactly `.claude/skills` is the container, not a skill.
|
|
3653
3806
|
rel.length > 0;
|
|
3654
3807
|
if (!contained) {
|
|
3655
3808
|
return { ok: false, reason: `refusing to install outside .claude/skills (${resolved})` };
|
|
@@ -3670,8 +3823,8 @@ async function installSkillDependencies(opts, deps = {}) {
|
|
|
3670
3823
|
if (!planned.ok) return { status: "skipped", reason: planned.reason };
|
|
3671
3824
|
const { command, args } = planned.command;
|
|
3672
3825
|
if (opts.dryRun) return { status: "would-install", command, args };
|
|
3673
|
-
const
|
|
3674
|
-
const probe = await
|
|
3826
|
+
const exists2 = deps.exists ?? probeCommand;
|
|
3827
|
+
const probe = await exists2(command);
|
|
3675
3828
|
switch (probe.status) {
|
|
3676
3829
|
case "available":
|
|
3677
3830
|
break;
|
|
@@ -3685,7 +3838,7 @@ async function installSkillDependencies(opts, deps = {}) {
|
|
|
3685
3838
|
default:
|
|
3686
3839
|
return assertNeverCommandProbe(probe);
|
|
3687
3840
|
}
|
|
3688
|
-
if (!await
|
|
3841
|
+
if (!await fs16.pathExists(contained.dir)) {
|
|
3689
3842
|
return { status: "skipped", reason: `install directory disappeared (${contained.dir})` };
|
|
3690
3843
|
}
|
|
3691
3844
|
const run = deps.run ?? defaultRun;
|
|
@@ -3699,7 +3852,7 @@ async function installSkillDependencies(opts, deps = {}) {
|
|
|
3699
3852
|
// src/claude/verify-hooks-contract.ts
|
|
3700
3853
|
init_fs();
|
|
3701
3854
|
init_paths();
|
|
3702
|
-
import
|
|
3855
|
+
import fs17 from "fs-extra";
|
|
3703
3856
|
|
|
3704
3857
|
// src/claude/load-hooks.ts
|
|
3705
3858
|
var CANONICAL_HOOKS = {
|
|
@@ -3797,7 +3950,7 @@ async function assertPostApplySettingsHausContract(root) {
|
|
|
3797
3950
|
}
|
|
3798
3951
|
async function verifyProjectSettingsHooksContract(root) {
|
|
3799
3952
|
const settingsPath = claudePath(root, "settings.json");
|
|
3800
|
-
if (!await
|
|
3953
|
+
if (!await fs17.pathExists(settingsPath)) {
|
|
3801
3954
|
return {
|
|
3802
3955
|
ok: true,
|
|
3803
3956
|
skipped: true,
|
|
@@ -3825,10 +3978,10 @@ async function verifyProjectSettingsHooksContract(root) {
|
|
|
3825
3978
|
|
|
3826
3979
|
// src/claude/write-ci-gate-workflow.ts
|
|
3827
3980
|
init_logger();
|
|
3828
|
-
import
|
|
3829
|
-
import
|
|
3981
|
+
import path21 from "node:path";
|
|
3982
|
+
import fs18 from "fs-extra";
|
|
3830
3983
|
import YAML from "yaml";
|
|
3831
|
-
var CI_GATE_WORKFLOW_REL =
|
|
3984
|
+
var CI_GATE_WORKFLOW_REL = path21.join(".github", "workflows", "haus-gate.yml");
|
|
3832
3985
|
var WORKFLOW = `# Written by \`haus init\`. Yours to edit \u2014 haus will not rewrite this file.
|
|
3833
3986
|
#
|
|
3834
3987
|
# Runs the checks haus already knows how to run, on every pull request:
|
|
@@ -3868,15 +4021,15 @@ jobs:
|
|
|
3868
4021
|
run: haus ci-gate
|
|
3869
4022
|
`;
|
|
3870
4023
|
async function writeCiGateWorkflow(root, dryRun, quiet = false) {
|
|
3871
|
-
const dest =
|
|
3872
|
-
if (await
|
|
4024
|
+
const dest = path21.join(root, CI_GATE_WORKFLOW_REL);
|
|
4025
|
+
if (await fs18.pathExists(dest)) return void 0;
|
|
3873
4026
|
if (await projectRunsCiGate(root)) return void 0;
|
|
3874
4027
|
if (dryRun) {
|
|
3875
4028
|
if (!quiet) log(`Would write ${CI_GATE_WORKFLOW_REL} (CI gate: haus ci-gate on every PR)`);
|
|
3876
4029
|
return dest;
|
|
3877
4030
|
}
|
|
3878
|
-
await
|
|
3879
|
-
await
|
|
4031
|
+
await fs18.ensureDir(path21.dirname(dest));
|
|
4032
|
+
await fs18.writeFile(dest, WORKFLOW, "utf8");
|
|
3880
4033
|
if (!quiet) log(`Wrote ${CI_GATE_WORKFLOW_REL} \u2014 runs \`haus ci-gate\` on every pull request`);
|
|
3881
4034
|
return dest;
|
|
3882
4035
|
}
|
|
@@ -3895,17 +4048,17 @@ function runScripts(node, found = []) {
|
|
|
3895
4048
|
return found;
|
|
3896
4049
|
}
|
|
3897
4050
|
async function projectRunsCiGate(root) {
|
|
3898
|
-
const dir =
|
|
4051
|
+
const dir = path21.join(root, ".github", "workflows");
|
|
3899
4052
|
let entries;
|
|
3900
4053
|
try {
|
|
3901
|
-
entries = await
|
|
4054
|
+
entries = await fs18.readdir(dir);
|
|
3902
4055
|
} catch {
|
|
3903
4056
|
return false;
|
|
3904
4057
|
}
|
|
3905
4058
|
for (const entry of entries) {
|
|
3906
4059
|
if (!/\.ya?ml$/i.test(entry)) continue;
|
|
3907
4060
|
try {
|
|
3908
|
-
const text = await
|
|
4061
|
+
const text = await fs18.readFile(path21.join(dir, entry), "utf8");
|
|
3909
4062
|
if (runScripts(YAML.parse(text)).some((script) => CI_GATE_COMMAND.test(script))) return true;
|
|
3910
4063
|
} catch {
|
|
3911
4064
|
}
|
|
@@ -3914,8 +4067,8 @@ async function projectRunsCiGate(root) {
|
|
|
3914
4067
|
}
|
|
3915
4068
|
|
|
3916
4069
|
// src/claude/write-decisions-seed.ts
|
|
3917
|
-
import
|
|
3918
|
-
import
|
|
4070
|
+
import path22 from "node:path";
|
|
4071
|
+
import fs19 from "fs-extra";
|
|
3919
4072
|
|
|
3920
4073
|
// library/catalog/decisions-triggers.json
|
|
3921
4074
|
var decisions_triggers_default = {
|
|
@@ -3958,17 +4111,17 @@ Why we chose X \u2014 short, dated, write-once decision log for humans and agent
|
|
|
3958
4111
|
| --- | ----- | -------------- | ------ |
|
|
3959
4112
|
`;
|
|
3960
4113
|
async function writeDecisionsSeed(root, dryRun, quiet = false) {
|
|
3961
|
-
const decisionsDir =
|
|
3962
|
-
const readmePath =
|
|
3963
|
-
if (await
|
|
3964
|
-
if (!dryRun) await
|
|
4114
|
+
const decisionsDir = path22.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
4115
|
+
const readmePath = path22.join(decisionsDir, "README.md");
|
|
4116
|
+
if (await fs19.pathExists(readmePath)) return null;
|
|
4117
|
+
if (!dryRun) await fs19.ensureDir(decisionsDir);
|
|
3965
4118
|
await writeManagedText(root, readmePath, SEED_README, dryRun, quiet);
|
|
3966
4119
|
return readmePath;
|
|
3967
4120
|
}
|
|
3968
4121
|
|
|
3969
4122
|
// src/claude/write-prettierignore.ts
|
|
3970
|
-
import
|
|
3971
|
-
import
|
|
4123
|
+
import path23 from "node:path";
|
|
4124
|
+
import fs20 from "fs-extra";
|
|
3972
4125
|
var PRETTIERIGNORE_BEGIN = "# HAUS:BEGIN haus-managed v=1";
|
|
3973
4126
|
var PRETTIERIGNORE_END = "# HAUS:END haus-managed";
|
|
3974
4127
|
var IGNORED_PATHS = [".haus-workflow/", ".claude/"];
|
|
@@ -4036,16 +4189,16 @@ ${block}
|
|
|
4036
4189
|
`;
|
|
4037
4190
|
}
|
|
4038
4191
|
async function writePrettierIgnore(root, dryRun, quiet = false) {
|
|
4039
|
-
const filePath =
|
|
4040
|
-
const prev = await
|
|
4192
|
+
const filePath = path23.join(root, ".prettierignore");
|
|
4193
|
+
const prev = await fs20.pathExists(filePath) ? await fs20.readFile(filePath, "utf8") : "";
|
|
4041
4194
|
const next = injectPrettierIgnoreBlock(prev, buildPrettierIgnoreBlock());
|
|
4042
4195
|
await writeManagedText(root, filePath, next, dryRun, quiet);
|
|
4043
4196
|
return filePath;
|
|
4044
4197
|
}
|
|
4045
4198
|
|
|
4046
4199
|
// src/claude/write-root-claude-md.ts
|
|
4047
|
-
import
|
|
4048
|
-
import
|
|
4200
|
+
import path24 from "node:path";
|
|
4201
|
+
import fs21 from "fs-extra";
|
|
4049
4202
|
var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
|
|
4050
4203
|
var BLOCK_END = "<!-- HAUS:END haus-imports -->";
|
|
4051
4204
|
var WORKSPACE_BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-workspace-imports v=1 -->";
|
|
@@ -4123,9 +4276,9 @@ ${block}
|
|
|
4123
4276
|
`;
|
|
4124
4277
|
}
|
|
4125
4278
|
async function writeRootClaudeMd(root, dryRun, quiet = false) {
|
|
4126
|
-
const filePath =
|
|
4279
|
+
const filePath = path24.join(root, "CLAUDE.md");
|
|
4127
4280
|
const block = buildImportBlock();
|
|
4128
|
-
const prev = await
|
|
4281
|
+
const prev = await fs21.pathExists(filePath) ? await fs21.readFile(filePath, "utf8") : "";
|
|
4129
4282
|
const next = injectHausBlock(prev, block);
|
|
4130
4283
|
await writeManagedText(root, filePath, next, dryRun, quiet);
|
|
4131
4284
|
return filePath;
|
|
@@ -4135,13 +4288,13 @@ async function writeRootClaudeMd(root, dryRun, quiet = false) {
|
|
|
4135
4288
|
init_fs();
|
|
4136
4289
|
init_logger();
|
|
4137
4290
|
init_paths();
|
|
4138
|
-
import
|
|
4139
|
-
import
|
|
4291
|
+
import path26 from "node:path";
|
|
4292
|
+
import fs23 from "fs-extra";
|
|
4140
4293
|
|
|
4141
4294
|
// src/claude/derive-workflow-config.ts
|
|
4142
4295
|
init_fs();
|
|
4143
|
-
import
|
|
4144
|
-
import
|
|
4296
|
+
import path25 from "node:path";
|
|
4297
|
+
import fs22 from "fs-extra";
|
|
4145
4298
|
function binCmd(pm, bin, args) {
|
|
4146
4299
|
const tail = args ? ` ${args}` : "";
|
|
4147
4300
|
if (pm === "yarn") return `yarn ${bin}${tail}`;
|
|
@@ -4150,7 +4303,7 @@ function binCmd(pm, bin, args) {
|
|
|
4150
4303
|
}
|
|
4151
4304
|
async function deriveWorkflowConfig(root, ctx) {
|
|
4152
4305
|
const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
|
|
4153
|
-
const pkg = await readJson(
|
|
4306
|
+
const pkg = await readJson(path25.join(root, "package.json"));
|
|
4154
4307
|
const scripts = pkg?.scripts ?? {};
|
|
4155
4308
|
const deps = new Set(ctx.dependencies);
|
|
4156
4309
|
const stacks = Object.values(ctx.detectedStacks ?? {}).flat();
|
|
@@ -4160,17 +4313,17 @@ async function deriveWorkflowConfig(root, ctx) {
|
|
|
4160
4313
|
return null;
|
|
4161
4314
|
};
|
|
4162
4315
|
const hasDep = (name) => deps.has(name);
|
|
4163
|
-
const
|
|
4316
|
+
const exists2 = (rel) => fs22.pathExistsSync(path25.join(root, rel));
|
|
4164
4317
|
const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
|
|
4165
4318
|
const hasCypress = hasDep("cypress");
|
|
4166
|
-
const preCommitTool =
|
|
4319
|
+
const preCommitTool = exists2("lefthook.yml") || exists2("lefthook.yaml") ? "lefthook" : exists2(".husky") || hasDep("husky") || (scripts.prepare ?? "").includes("husky") ? "husky" : exists2(".pre-commit-config.yaml") ? "pre-commit (Python framework)" : null;
|
|
4167
4320
|
return {
|
|
4168
4321
|
test: script("test") ?? `${pm} test`,
|
|
4169
4322
|
testE2E: firstScript("test:e2e", "e2e", "test:integration") ?? (hasPlaywright ? binCmd(pm, "playwright", "test") : null) ?? (hasCypress ? binCmd(pm, "cypress", "run") : null),
|
|
4170
4323
|
preCommitTool,
|
|
4171
|
-
specPath:
|
|
4172
|
-
designPath:
|
|
4173
|
-
uxPath:
|
|
4324
|
+
specPath: exists2("docs/SPEC.md") ? "docs/SPEC.md" : null,
|
|
4325
|
+
designPath: exists2("docs/DESIGN.md") ? "docs/DESIGN.md" : null,
|
|
4326
|
+
uxPath: exists2("docs/UX.md") ? "docs/UX.md" : null
|
|
4174
4327
|
};
|
|
4175
4328
|
}
|
|
4176
4329
|
|
|
@@ -4229,19 +4382,19 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
4229
4382
|
} : log;
|
|
4230
4383
|
const destPath = hausPath(root, "workflow-config.md");
|
|
4231
4384
|
const printable = displayPath(root, destPath);
|
|
4232
|
-
const
|
|
4233
|
-
if (
|
|
4385
|
+
const exists2 = await fs23.pathExists(destPath);
|
|
4386
|
+
if (exists2 && !opts.refill) {
|
|
4234
4387
|
if (dryRun) say(printable + ": exists (project-owned, skipping)");
|
|
4235
4388
|
return null;
|
|
4236
4389
|
}
|
|
4237
4390
|
const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
|
|
4238
4391
|
...FALLBACK_CONTEXT,
|
|
4239
4392
|
root,
|
|
4240
|
-
repoName:
|
|
4393
|
+
repoName: path26.basename(root)
|
|
4241
4394
|
};
|
|
4242
4395
|
const values = await deriveWorkflowConfig(root, ctx);
|
|
4243
|
-
if (
|
|
4244
|
-
const current = await
|
|
4396
|
+
if (exists2) {
|
|
4397
|
+
const current = await fs23.readFile(destPath, "utf8");
|
|
4245
4398
|
const refilled = refillContent(current, values);
|
|
4246
4399
|
if (refilled === current) {
|
|
4247
4400
|
if (dryRun) say(printable + ": no blank fields to refill");
|
|
@@ -4264,7 +4417,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
4264
4417
|
|
|
4265
4418
|
// src/claude/write-workflow.ts
|
|
4266
4419
|
init_offline();
|
|
4267
|
-
import
|
|
4420
|
+
import fs24 from "fs-extra";
|
|
4268
4421
|
init_fs();
|
|
4269
4422
|
init_logger();
|
|
4270
4423
|
init_paths();
|
|
@@ -4306,8 +4459,8 @@ async function writeWorkflow(root, pkgVersion, dryRun, force = false, quiet = fa
|
|
|
4306
4459
|
${templateContent}`;
|
|
4307
4460
|
const destPath = hausPath(root, "WORKFLOW.md");
|
|
4308
4461
|
const printable = displayPath(root, destPath);
|
|
4309
|
-
if (await
|
|
4310
|
-
const existing = await
|
|
4462
|
+
if (await fs24.pathExists(destPath)) {
|
|
4463
|
+
const existing = await fs24.readFile(destPath, "utf8");
|
|
4311
4464
|
const firstLine = existing.split("\n")[0] ?? "";
|
|
4312
4465
|
const parsed = parseHausManagedHeader(firstLine);
|
|
4313
4466
|
if (!parsed) {
|
|
@@ -4351,7 +4504,7 @@ ${existingContent}`;
|
|
|
4351
4504
|
}
|
|
4352
4505
|
}
|
|
4353
4506
|
if (dryRun) {
|
|
4354
|
-
const prev = await
|
|
4507
|
+
const prev = await fs24.pathExists(destPath) ? await fs24.readFile(destPath, "utf8") : "";
|
|
4355
4508
|
if (!prev) {
|
|
4356
4509
|
say(createUnifiedDiff(printable, "", next));
|
|
4357
4510
|
} else {
|
|
@@ -4406,14 +4559,14 @@ function decodeIfText(buf) {
|
|
|
4406
4559
|
return Buffer.from(asText, "utf8").equals(buf) ? asText : null;
|
|
4407
4560
|
}
|
|
4408
4561
|
async function listFilesRecursive2(dir) {
|
|
4409
|
-
if (!await
|
|
4562
|
+
if (!await fs25.pathExists(dir)) return [];
|
|
4410
4563
|
const files = [];
|
|
4411
4564
|
const walk = async (current, relPrefix) => {
|
|
4412
|
-
const names = await
|
|
4565
|
+
const names = await fs25.readdir(current);
|
|
4413
4566
|
for (const name of names) {
|
|
4414
|
-
const abs =
|
|
4567
|
+
const abs = path27.join(current, name);
|
|
4415
4568
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
4416
|
-
const stat = await
|
|
4569
|
+
const stat = await fs25.lstat(abs);
|
|
4417
4570
|
if (stat.isSymbolicLink()) {
|
|
4418
4571
|
warn(`Skipping symlink in dry-run diff preview: ${rel}`);
|
|
4419
4572
|
continue;
|
|
@@ -4430,14 +4583,14 @@ async function listFilesRecursive2(dir) {
|
|
|
4430
4583
|
}
|
|
4431
4584
|
async function logFileDryRunDiff(root, srcFile, destFile, itemId, say, transformed) {
|
|
4432
4585
|
const printable = displayPath(root, destFile);
|
|
4433
|
-
const nextBuf = transformed ?? await
|
|
4434
|
-
if (!await
|
|
4586
|
+
const nextBuf = transformed ?? await fs25.readFile(srcFile);
|
|
4587
|
+
if (!await fs25.pathExists(destFile)) {
|
|
4435
4588
|
say(`${printable}: would create (${itemId})`);
|
|
4436
4589
|
const nextText2 = decodeIfText(nextBuf);
|
|
4437
4590
|
if (nextText2 !== null) say(createUnifiedDiff(printable, "", nextText2));
|
|
4438
4591
|
return;
|
|
4439
4592
|
}
|
|
4440
|
-
const prevBuf = await
|
|
4593
|
+
const prevBuf = await fs25.readFile(destFile);
|
|
4441
4594
|
if (prevBuf.equals(nextBuf)) {
|
|
4442
4595
|
say(`${printable}: unchanged (${itemId})`);
|
|
4443
4596
|
return;
|
|
@@ -4453,28 +4606,28 @@ async function logSkillDryRunDiff(root, sourcePath, destination, itemId, say, in
|
|
|
4453
4606
|
const sourceFiles = await listFilesRecursive2(sourcePath);
|
|
4454
4607
|
const destFiles = await listFilesRecursive2(destination);
|
|
4455
4608
|
for (const rel of sourceFiles) {
|
|
4456
|
-
const src =
|
|
4609
|
+
const src = path27.join(sourcePath, rel);
|
|
4457
4610
|
const transformed = rel === "SKILL.md" ? await previewInstalledSkill(src, install) : void 0;
|
|
4458
|
-
await logFileDryRunDiff(root, src,
|
|
4611
|
+
await logFileDryRunDiff(root, src, path27.join(destination, rel), itemId, say, transformed);
|
|
4459
4612
|
}
|
|
4460
4613
|
const sourceFileSet = new Set(sourceFiles);
|
|
4461
4614
|
for (const rel of destFiles) {
|
|
4462
4615
|
if (!sourceFileSet.has(rel)) {
|
|
4463
|
-
say(`${displayPath(root,
|
|
4616
|
+
say(`${displayPath(root, path27.join(destination, rel))}: would remove (${itemId})`);
|
|
4464
4617
|
}
|
|
4465
4618
|
}
|
|
4466
4619
|
}
|
|
4467
4620
|
async function removeLegacyManagedStub(root, relPathSegments, stub, dryRun, say) {
|
|
4468
4621
|
const target = claudePath(root, ...relPathSegments);
|
|
4469
|
-
if (!await
|
|
4470
|
-
const content2 = await
|
|
4622
|
+
if (!await fs25.pathExists(target)) return;
|
|
4623
|
+
const content2 = await fs25.readFile(target, "utf8");
|
|
4471
4624
|
if (content2 !== stub && content2 !== `${stub}
|
|
4472
4625
|
` && content2 !== `${stub}\r
|
|
4473
4626
|
`) return;
|
|
4474
4627
|
if (dryRun) {
|
|
4475
4628
|
say(`[dry-run] would remove stale ${displayPath(root, target)}`);
|
|
4476
4629
|
} else {
|
|
4477
|
-
await
|
|
4630
|
+
await fs25.remove(target);
|
|
4478
4631
|
}
|
|
4479
4632
|
}
|
|
4480
4633
|
async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
@@ -4492,7 +4645,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4492
4645
|
estimatedTokenReductionPct: 0
|
|
4493
4646
|
};
|
|
4494
4647
|
const pkgRoot = packageRoot();
|
|
4495
|
-
const hausVersion2 = (await readJson(
|
|
4648
|
+
const hausVersion2 = (await readJson(path27.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
|
|
4496
4649
|
const { items: manifestItems, contentRoot } = await loadCatalogContext(root);
|
|
4497
4650
|
const coreFiles = [claudePath(root, "settings.json"), claudePath(root, "rules", "haus.md")];
|
|
4498
4651
|
const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun, opts.quiet);
|
|
@@ -4582,11 +4735,11 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4582
4735
|
];
|
|
4583
4736
|
for (const rel of LEGACY_PRUNED_ARTIFACTS) {
|
|
4584
4737
|
const legacyPath = hausPath(root, rel);
|
|
4585
|
-
if (await
|
|
4738
|
+
if (await fs25.pathExists(legacyPath)) {
|
|
4586
4739
|
if (dryRun) {
|
|
4587
4740
|
say(`[dry-run] would remove stale ${displayPath(root, legacyPath)}`);
|
|
4588
4741
|
} else {
|
|
4589
|
-
await
|
|
4742
|
+
await fs25.remove(legacyPath);
|
|
4590
4743
|
}
|
|
4591
4744
|
}
|
|
4592
4745
|
}
|
|
@@ -4650,7 +4803,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4650
4803
|
let curatedReviewStatusSkips = 0;
|
|
4651
4804
|
const projectPackageManager = detectPackageManager(
|
|
4652
4805
|
root,
|
|
4653
|
-
(await readJson(
|
|
4806
|
+
(await readJson(path27.join(root, "package.json")))?.packageManager
|
|
4654
4807
|
);
|
|
4655
4808
|
for (const item of catalogItems) {
|
|
4656
4809
|
const manifestItem = manifestById.get(item.id);
|
|
@@ -4704,7 +4857,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4704
4857
|
}
|
|
4705
4858
|
const sourcePath = catalogItemContentPath(contentRoot, manifestItem);
|
|
4706
4859
|
const destination = placement.destination;
|
|
4707
|
-
if (await
|
|
4860
|
+
if (await fs25.pathExists(sourcePath)) {
|
|
4708
4861
|
if (dryRun) {
|
|
4709
4862
|
if (item.type === "skill") {
|
|
4710
4863
|
await logSkillDryRunDiff(root, sourcePath, destination, item.id, say, {
|
|
@@ -4735,13 +4888,13 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4735
4888
|
)) {
|
|
4736
4889
|
continue;
|
|
4737
4890
|
} else if (item.type === "skill") {
|
|
4738
|
-
const skillFiles = (await
|
|
4891
|
+
const skillFiles = (await fs25.readdir(sourcePath, { recursive: true })).filter(
|
|
4739
4892
|
(f) => typeof f === "string" && f.endsWith(".md")
|
|
4740
4893
|
);
|
|
4741
4894
|
let skillValid = true;
|
|
4742
4895
|
for (const relFile of skillFiles) {
|
|
4743
|
-
const mdPath =
|
|
4744
|
-
const mdContent = await
|
|
4896
|
+
const mdPath = path27.join(sourcePath, relFile);
|
|
4897
|
+
const mdContent = await fs25.readFile(mdPath, "utf8");
|
|
4745
4898
|
const mdValidation = validateCatalogItem(
|
|
4746
4899
|
{
|
|
4747
4900
|
id: manifestItem.id,
|
|
@@ -4775,7 +4928,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4775
4928
|
say
|
|
4776
4929
|
);
|
|
4777
4930
|
} else {
|
|
4778
|
-
const fileContent = await
|
|
4931
|
+
const fileContent = await fs25.readFile(sourcePath, "utf8");
|
|
4779
4932
|
const validation = validateCatalogItem(
|
|
4780
4933
|
{
|
|
4781
4934
|
id: manifestItem.id,
|
|
@@ -4789,11 +4942,11 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4789
4942
|
warn(`Skipping ${item.id}: pre-copy validation failed \u2014 ${validation.reason}`);
|
|
4790
4943
|
continue;
|
|
4791
4944
|
}
|
|
4792
|
-
await
|
|
4793
|
-
await
|
|
4945
|
+
await fs25.ensureDir(path27.dirname(destination));
|
|
4946
|
+
await fs25.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
4794
4947
|
}
|
|
4795
4948
|
files.push(destination);
|
|
4796
|
-
const relPaths = [
|
|
4949
|
+
const relPaths = [path27.relative(root, destination)];
|
|
4797
4950
|
const current = installedPathsByItem.get(item.id) ?? [];
|
|
4798
4951
|
installedPathsByItem.set(item.id, [...current, ...relPaths]);
|
|
4799
4952
|
installedIds.add(item.id);
|
|
@@ -4916,7 +5069,7 @@ async function hasProtectedUnsafePath(root, relPaths, unsafeByRel) {
|
|
|
4916
5069
|
for (const rel of relPaths) {
|
|
4917
5070
|
const unsafe = unsafeByRel.get(rel) ?? null;
|
|
4918
5071
|
if (unsafe === "symlink") return true;
|
|
4919
|
-
if (unsafe === "escapes-root" && await
|
|
5072
|
+
if (unsafe === "escapes-root" && await fs25.pathExists(path27.resolve(root, rel))) return true;
|
|
4920
5073
|
}
|
|
4921
5074
|
return false;
|
|
4922
5075
|
}
|
|
@@ -4981,8 +5134,8 @@ async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, q
|
|
|
4981
5134
|
void await backupTrackedPaths(root, allAbsPaths, say, "migrate");
|
|
4982
5135
|
for (const { existing } of toRemove) {
|
|
4983
5136
|
for (const { abs } of existing) {
|
|
4984
|
-
await
|
|
4985
|
-
await pruneEmptyDir(
|
|
5137
|
+
await fs25.remove(abs);
|
|
5138
|
+
await pruneEmptyDir(path27.dirname(abs));
|
|
4986
5139
|
}
|
|
4987
5140
|
}
|
|
4988
5141
|
}
|
|
@@ -5040,10 +5193,10 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
|
|
|
5040
5193
|
continue;
|
|
5041
5194
|
}
|
|
5042
5195
|
const removable = superseded ? existing.filter(({ rel }) => {
|
|
5043
|
-
const isShared = rel.split(
|
|
5196
|
+
const isShared = rel.split(path27.sep).join("/") === LEGACY_SUPERPOWERS_SHARED_REL_PATH;
|
|
5044
5197
|
if (isShared && keepShared) {
|
|
5045
5198
|
say(
|
|
5046
|
-
`Keeping ${displayPath(root,
|
|
5199
|
+
`Keeping ${displayPath(root, path27.join(root, rel))} \u2014 a locally-edited superpowers skill still uses it`
|
|
5047
5200
|
);
|
|
5048
5201
|
return false;
|
|
5049
5202
|
}
|
|
@@ -5062,8 +5215,8 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
|
|
|
5062
5215
|
say(`[dry-run] would remove ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
5063
5216
|
continue;
|
|
5064
5217
|
}
|
|
5065
|
-
await
|
|
5066
|
-
await pruneEmptyDir(
|
|
5218
|
+
await fs25.remove(abs);
|
|
5219
|
+
await pruneEmptyDir(path27.dirname(abs));
|
|
5067
5220
|
say(`Removed ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
5068
5221
|
}
|
|
5069
5222
|
}
|
|
@@ -5117,8 +5270,8 @@ async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun,
|
|
|
5117
5270
|
|
|
5118
5271
|
// src/claude/write-gitignore.ts
|
|
5119
5272
|
init_exec();
|
|
5120
|
-
import
|
|
5121
|
-
import
|
|
5273
|
+
import path28 from "node:path";
|
|
5274
|
+
import fs26 from "fs-extra";
|
|
5122
5275
|
var GITIGNORE_BEGIN = "# HAUS:BEGIN haus-managed v=1";
|
|
5123
5276
|
var GITIGNORE_END = "# HAUS:END haus-managed";
|
|
5124
5277
|
var GITIGNORED_ARTIFACT_PATHS = [
|
|
@@ -5131,7 +5284,15 @@ var GITIGNORED_ARTIFACT_PATHS = [
|
|
|
5131
5284
|
// Hydration locks and the detached hydration log/last-run record (#419): live
|
|
5132
5285
|
// process state and machine-local diagnostics, meaningless in another checkout.
|
|
5133
5286
|
".haus-workflow/locks/",
|
|
5134
|
-
".haus-workflow/logs/"
|
|
5287
|
+
".haus-workflow/logs/",
|
|
5288
|
+
// The request pipeline's archive (#472, ADR-0103): per-request pricing material —
|
|
5289
|
+
// specs, impact analyses, scores — written by the request skill, not this CLI. It
|
|
5290
|
+
// lived at `.haus/requests/` behind a hand-added ignore line OUTSIDE this block,
|
|
5291
|
+
// so no client repo ever received the rule; `migrate-request-state.ts` moves it
|
|
5292
|
+
// here precisely so this block can own it. Note `.haus-workflow/request.json`
|
|
5293
|
+
// (the Asana board binding) is deliberately absent: it is shared config, not
|
|
5294
|
+
// machine-local state, and stays tracked like `workflow-config.md`.
|
|
5295
|
+
".haus-workflow/requests/"
|
|
5135
5296
|
];
|
|
5136
5297
|
function findLineMarker3(content2, marker, from = 0) {
|
|
5137
5298
|
let idx = content2.indexOf(marker, from);
|
|
@@ -5238,15 +5399,15 @@ function injectLinkContextGitignoreBlock(existing) {
|
|
|
5238
5399
|
);
|
|
5239
5400
|
}
|
|
5240
5401
|
async function writeGitignore(root, dryRun, quiet = false) {
|
|
5241
|
-
const filePath =
|
|
5242
|
-
const prev = await
|
|
5402
|
+
const filePath = path28.join(root, ".gitignore");
|
|
5403
|
+
const prev = await fs26.pathExists(filePath) ? await fs26.readFile(filePath, "utf8") : "";
|
|
5243
5404
|
const next = injectGitignoreBlock(prev);
|
|
5244
5405
|
await writeManagedText(root, filePath, next, dryRun, quiet);
|
|
5245
5406
|
return filePath;
|
|
5246
5407
|
}
|
|
5247
5408
|
async function writeLinkContextGitignore(root, dryRun, quiet = false) {
|
|
5248
|
-
const filePath =
|
|
5249
|
-
const prev = await
|
|
5409
|
+
const filePath = path28.join(root, ".gitignore");
|
|
5410
|
+
const prev = await fs26.pathExists(filePath) ? await fs26.readFile(filePath, "utf8") : "";
|
|
5250
5411
|
const next = injectLinkContextGitignoreBlock(prev);
|
|
5251
5412
|
await writeManagedText(root, filePath, next, dryRun, quiet);
|
|
5252
5413
|
return filePath;
|
|
@@ -5287,7 +5448,7 @@ async function untrackMachineLocalArtifacts(root, dryRun, notify) {
|
|
|
5287
5448
|
}
|
|
5288
5449
|
|
|
5289
5450
|
// src/install/ensure-global-plugins.ts
|
|
5290
|
-
import
|
|
5451
|
+
import path29 from "node:path";
|
|
5291
5452
|
init_offline();
|
|
5292
5453
|
init_exec();
|
|
5293
5454
|
init_fs();
|
|
@@ -5526,7 +5687,7 @@ function parseInstalled(stdout2) {
|
|
|
5526
5687
|
}
|
|
5527
5688
|
}
|
|
5528
5689
|
async function readMarketplaceRefs(configDir) {
|
|
5529
|
-
const settings = await readJson(
|
|
5690
|
+
const settings = await readJson(path29.join(configDir, "settings.json"));
|
|
5530
5691
|
const out = {};
|
|
5531
5692
|
for (const [name, entry] of Object.entries(settings?.extraKnownMarketplaces ?? {})) {
|
|
5532
5693
|
const ref = entry?.source?.ref;
|
|
@@ -5536,7 +5697,7 @@ async function readMarketplaceRefs(configDir) {
|
|
|
5536
5697
|
}
|
|
5537
5698
|
async function readClonedPluginVersion(installLocation) {
|
|
5538
5699
|
const manifest = await readJson(
|
|
5539
|
-
|
|
5700
|
+
path29.join(installLocation, ".claude-plugin", "plugin.json")
|
|
5540
5701
|
);
|
|
5541
5702
|
return typeof manifest?.version === "string" ? manifest.version : void 0;
|
|
5542
5703
|
}
|
|
@@ -5814,26 +5975,26 @@ async function cleanupStaleGlobalPlugins(desired, deps = {}) {
|
|
|
5814
5975
|
}
|
|
5815
5976
|
|
|
5816
5977
|
// src/refs/fetch-refs.ts
|
|
5817
|
-
import
|
|
5818
|
-
import
|
|
5978
|
+
import path31 from "node:path";
|
|
5979
|
+
import fs28 from "fs-extra";
|
|
5819
5980
|
|
|
5820
5981
|
// src/refs/cache-meta.ts
|
|
5821
|
-
import
|
|
5822
|
-
import
|
|
5982
|
+
import path30 from "node:path";
|
|
5983
|
+
import fs27 from "fs-extra";
|
|
5823
5984
|
var META_FILENAME = "cache-meta.json";
|
|
5824
5985
|
async function readCacheMeta(cacheDir) {
|
|
5825
|
-
const metaPath =
|
|
5986
|
+
const metaPath = path30.join(cacheDir, META_FILENAME);
|
|
5826
5987
|
try {
|
|
5827
|
-
const raw = await
|
|
5988
|
+
const raw = await fs27.readFile(metaPath, "utf8");
|
|
5828
5989
|
return JSON.parse(raw);
|
|
5829
5990
|
} catch {
|
|
5830
5991
|
return {};
|
|
5831
5992
|
}
|
|
5832
5993
|
}
|
|
5833
5994
|
async function writeCacheMeta(cacheDir, meta) {
|
|
5834
|
-
await
|
|
5835
|
-
const metaPath =
|
|
5836
|
-
await
|
|
5995
|
+
await fs27.ensureDir(cacheDir);
|
|
5996
|
+
const metaPath = path30.join(cacheDir, META_FILENAME);
|
|
5997
|
+
await fs27.writeFile(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf8");
|
|
5837
5998
|
}
|
|
5838
5999
|
|
|
5839
6000
|
// src/refs/fetch-refs.ts
|
|
@@ -5853,8 +6014,8 @@ function isLlmsTxtUrl(ref) {
|
|
|
5853
6014
|
async function fetchSingleRef(url, cacheDir, meta) {
|
|
5854
6015
|
const existing = meta[url];
|
|
5855
6016
|
const headers = {};
|
|
5856
|
-
const safeFile = existing?.file && existing.file ===
|
|
5857
|
-
const cachedFileExists = safeFile && await
|
|
6017
|
+
const safeFile = existing?.file && existing.file === path31.basename(existing.file);
|
|
6018
|
+
const cachedFileExists = safeFile && await fs28.pathExists(path31.join(cacheDir, existing.file));
|
|
5858
6019
|
if (cachedFileExists) {
|
|
5859
6020
|
if (existing?.etag) headers["If-None-Match"] = existing.etag;
|
|
5860
6021
|
if (existing?.lastModified) headers["If-Modified-Since"] = existing.lastModified;
|
|
@@ -5865,8 +6026,8 @@ async function fetchSingleRef(url, cacheDir, meta) {
|
|
|
5865
6026
|
if (!res.ok) return { result: "failed" };
|
|
5866
6027
|
const text = await res.text();
|
|
5867
6028
|
const file = `${urlToSlug(url)}.md`;
|
|
5868
|
-
await
|
|
5869
|
-
await
|
|
6029
|
+
await fs28.ensureDir(cacheDir);
|
|
6030
|
+
await fs28.writeFile(path31.join(cacheDir, file), text, "utf8");
|
|
5870
6031
|
meta[url] = {
|
|
5871
6032
|
url,
|
|
5872
6033
|
etag: res.headers.get("etag") ?? void 0,
|
|
@@ -5889,8 +6050,8 @@ async function pruneOrphanedRefs(cacheDir, keepUrls) {
|
|
|
5889
6050
|
if (keepUrls.has(url)) continue;
|
|
5890
6051
|
delete meta[url];
|
|
5891
6052
|
removed++;
|
|
5892
|
-
const safeFile = typeof entry.file === "string" && entry.file ===
|
|
5893
|
-
if (safeFile) await
|
|
6053
|
+
const safeFile = typeof entry.file === "string" && entry.file === path31.basename(entry.file);
|
|
6054
|
+
if (safeFile) await fs28.remove(path31.join(cacheDir, entry.file)).catch(() => {
|
|
5894
6055
|
});
|
|
5895
6056
|
}
|
|
5896
6057
|
if (removed > 0) await writeCacheMeta(cacheDir, meta);
|
|
@@ -5912,10 +6073,10 @@ async function fetchRefsForItems(items, cacheDir) {
|
|
|
5912
6073
|
const outcome = await fetchSingleRef(url, cacheDir, meta);
|
|
5913
6074
|
if (outcome.result === "fetched") {
|
|
5914
6075
|
summary.fetched++;
|
|
5915
|
-
summary.cachedFiles[url] =
|
|
6076
|
+
summary.cachedFiles[url] = path31.join(cacheDir, outcome.file);
|
|
5916
6077
|
} else if (outcome.result === "unchanged") {
|
|
5917
6078
|
summary.unchanged++;
|
|
5918
|
-
if (meta[url]?.file) summary.cachedFiles[url] =
|
|
6079
|
+
if (meta[url]?.file) summary.cachedFiles[url] = path31.join(cacheDir, meta[url].file);
|
|
5919
6080
|
} else {
|
|
5920
6081
|
summary.failed++;
|
|
5921
6082
|
summary.failedUrls.push(url);
|
|
@@ -6097,6 +6258,7 @@ async function runApply(options) {
|
|
|
6097
6258
|
// Copies a global plugin already provides are removed here (issue #272).
|
|
6098
6259
|
activePluginIds: await activeGlobalPluginIds(root)
|
|
6099
6260
|
});
|
|
6261
|
+
await migrateRequestState(root, isDryRun, warn);
|
|
6100
6262
|
await writeGitignore(root, isDryRun);
|
|
6101
6263
|
await untrackMachineLocalArtifacts(root, isDryRun, warn);
|
|
6102
6264
|
if (!isDryRun) {
|
|
@@ -6163,8 +6325,8 @@ async function runApply(options) {
|
|
|
6163
6325
|
}
|
|
6164
6326
|
|
|
6165
6327
|
// src/commands/backups.ts
|
|
6166
|
-
import
|
|
6167
|
-
import
|
|
6328
|
+
import path32 from "node:path";
|
|
6329
|
+
import fs29 from "fs-extra";
|
|
6168
6330
|
init_logger();
|
|
6169
6331
|
init_paths();
|
|
6170
6332
|
|
|
@@ -6231,14 +6393,14 @@ Enter a number (1-${choices.length})`, io);
|
|
|
6231
6393
|
// src/commands/backups.ts
|
|
6232
6394
|
async function listBackups(root) {
|
|
6233
6395
|
const dir = hausPath(root, "backups");
|
|
6234
|
-
if (!await
|
|
6235
|
-
const names = await
|
|
6396
|
+
if (!await fs29.pathExists(dir)) return [];
|
|
6397
|
+
const names = await fs29.readdir(dir);
|
|
6236
6398
|
const entries = [];
|
|
6237
6399
|
for (const name of names) {
|
|
6238
6400
|
const kind = classifyBackupName(name);
|
|
6239
6401
|
if (!kind) continue;
|
|
6240
|
-
const absPath =
|
|
6241
|
-
const stat = await
|
|
6402
|
+
const absPath = path32.join(dir, name);
|
|
6403
|
+
const stat = await fs29.lstat(absPath);
|
|
6242
6404
|
if (stat.isSymbolicLink()) continue;
|
|
6243
6405
|
const expectDirectory = kind !== "lock";
|
|
6244
6406
|
if (stat.isDirectory() !== expectDirectory) continue;
|
|
@@ -6277,13 +6439,13 @@ async function runList(root, json) {
|
|
|
6277
6439
|
}
|
|
6278
6440
|
}
|
|
6279
6441
|
async function isUnsafeWriteTarget(root, target) {
|
|
6280
|
-
const segments =
|
|
6442
|
+
const segments = path32.relative(root, target).split(path32.sep).filter(Boolean);
|
|
6281
6443
|
let current = root;
|
|
6282
6444
|
for (let i = 0; i < segments.length; i++) {
|
|
6283
|
-
current =
|
|
6445
|
+
current = path32.join(current, segments[i]);
|
|
6284
6446
|
const isLastSegment = i === segments.length - 1;
|
|
6285
6447
|
try {
|
|
6286
|
-
const stat = await
|
|
6448
|
+
const stat = await fs29.lstat(current);
|
|
6287
6449
|
if (isLastSegment) {
|
|
6288
6450
|
if (!stat.isFile()) return true;
|
|
6289
6451
|
} else if (!stat.isDirectory()) {
|
|
@@ -6298,7 +6460,7 @@ async function isUnsafeWriteTarget(root, target) {
|
|
|
6298
6460
|
}
|
|
6299
6461
|
async function lstatIfRegularFile(abs) {
|
|
6300
6462
|
try {
|
|
6301
|
-
const stat = await
|
|
6463
|
+
const stat = await fs29.lstat(abs);
|
|
6302
6464
|
return stat.isFile() ? stat : null;
|
|
6303
6465
|
} catch (err) {
|
|
6304
6466
|
if (err.code === "ENOENT") return null;
|
|
@@ -6313,8 +6475,8 @@ async function restoreLockBackup(root, entry, yes, json, dryRun) {
|
|
|
6313
6475
|
);
|
|
6314
6476
|
return { ok: false, restored: 0 };
|
|
6315
6477
|
}
|
|
6316
|
-
if (await
|
|
6317
|
-
const targetStat = await
|
|
6478
|
+
if (await fs29.pathExists(target)) {
|
|
6479
|
+
const targetStat = await fs29.stat(target);
|
|
6318
6480
|
if (targetStat.mtimeMs > entry.mtimeMs) {
|
|
6319
6481
|
warn(
|
|
6320
6482
|
`Current haus.lock.json (${new Date(targetStat.mtimeMs).toISOString()}) is newer than backup ${entry.id} (${new Date(entry.mtimeMs).toISOString()}) \u2014 it will be overwritten.`
|
|
@@ -6348,7 +6510,7 @@ async function restoreLockBackup(root, entry, yes, json, dryRun) {
|
|
|
6348
6510
|
error(`Refusing to restore: ${entry.id} or ${target} changed since being checked.`);
|
|
6349
6511
|
return { ok: false, restored: 0 };
|
|
6350
6512
|
}
|
|
6351
|
-
await
|
|
6513
|
+
await fs29.copy(entry.absPath, target, { overwrite: true });
|
|
6352
6514
|
if (!json) log(`Restored .haus-workflow/haus.lock.json from ${entry.id}.`);
|
|
6353
6515
|
return { ok: true, restored: 1 };
|
|
6354
6516
|
}
|
|
@@ -6356,10 +6518,10 @@ async function collectFilesRecursive(dir) {
|
|
|
6356
6518
|
const files = [];
|
|
6357
6519
|
const skippedLinks = [];
|
|
6358
6520
|
const skippedOther = [];
|
|
6359
|
-
const names = await
|
|
6521
|
+
const names = await fs29.readdir(dir);
|
|
6360
6522
|
for (const name of names) {
|
|
6361
|
-
const abs =
|
|
6362
|
-
const stat = await
|
|
6523
|
+
const abs = path32.join(dir, name);
|
|
6524
|
+
const stat = await fs29.lstat(abs);
|
|
6363
6525
|
if (stat.isSymbolicLink()) {
|
|
6364
6526
|
skippedLinks.push(abs);
|
|
6365
6527
|
continue;
|
|
@@ -6381,12 +6543,12 @@ async function restoreDirBackup(root, entry, yes, json, dryRun) {
|
|
|
6381
6543
|
const { files, skippedLinks, skippedOther } = await collectFilesRecursive(entry.absPath);
|
|
6382
6544
|
if (skippedLinks.length > 0) {
|
|
6383
6545
|
warn(
|
|
6384
|
-
`Skipped ${skippedLinks.length} symlink(s) inside backup ${entry.id} \u2014 never followed: ${skippedLinks.map((abs) =>
|
|
6546
|
+
`Skipped ${skippedLinks.length} symlink(s) inside backup ${entry.id} \u2014 never followed: ${skippedLinks.map((abs) => path32.relative(entry.absPath, abs)).join(", ")}`
|
|
6385
6547
|
);
|
|
6386
6548
|
}
|
|
6387
6549
|
if (skippedOther.length > 0) {
|
|
6388
6550
|
warn(
|
|
6389
|
-
`Skipped ${skippedOther.length} non-regular file(s) inside backup ${entry.id} (not restorable): ${skippedOther.map((abs) =>
|
|
6551
|
+
`Skipped ${skippedOther.length} non-regular file(s) inside backup ${entry.id} (not restorable): ${skippedOther.map((abs) => path32.relative(entry.absPath, abs)).join(", ")}`
|
|
6390
6552
|
);
|
|
6391
6553
|
}
|
|
6392
6554
|
if (files.length === 0) {
|
|
@@ -6396,8 +6558,8 @@ async function restoreDirBackup(root, entry, yes, json, dryRun) {
|
|
|
6396
6558
|
const safeFiles = [];
|
|
6397
6559
|
const unsafeTargets = [];
|
|
6398
6560
|
for (const abs of files) {
|
|
6399
|
-
const rel =
|
|
6400
|
-
const target =
|
|
6561
|
+
const rel = path32.relative(entry.absPath, abs);
|
|
6562
|
+
const target = path32.join(root, rel);
|
|
6401
6563
|
if (await isUnsafeWriteTarget(root, target)) unsafeTargets.push(rel);
|
|
6402
6564
|
else safeFiles.push(abs);
|
|
6403
6565
|
}
|
|
@@ -6414,16 +6576,16 @@ async function restoreDirBackup(root, entry, yes, json, dryRun) {
|
|
|
6414
6576
|
const vanished = [];
|
|
6415
6577
|
const toRestore = [];
|
|
6416
6578
|
for (const abs of safeFiles) {
|
|
6417
|
-
const rel =
|
|
6579
|
+
const rel = path32.relative(entry.absPath, abs);
|
|
6418
6580
|
const backupFileStat = await lstatIfRegularFile(abs);
|
|
6419
6581
|
if (!backupFileStat) {
|
|
6420
6582
|
vanished.push(rel);
|
|
6421
6583
|
continue;
|
|
6422
6584
|
}
|
|
6423
6585
|
toRestore.push(abs);
|
|
6424
|
-
const target =
|
|
6425
|
-
if (await
|
|
6426
|
-
const targetStat = await
|
|
6586
|
+
const target = path32.join(root, rel);
|
|
6587
|
+
if (await fs29.pathExists(target)) {
|
|
6588
|
+
const targetStat = await fs29.stat(target);
|
|
6427
6589
|
if (targetStat.mtimeMs > backupFileStat.mtimeMs) {
|
|
6428
6590
|
stale.push({
|
|
6429
6591
|
rel,
|
|
@@ -6451,7 +6613,7 @@ async function restoreDirBackup(root, entry, yes, json, dryRun) {
|
|
|
6451
6613
|
);
|
|
6452
6614
|
}
|
|
6453
6615
|
if (dryRun) {
|
|
6454
|
-
const rels = toRestore.map((abs) =>
|
|
6616
|
+
const rels = toRestore.map((abs) => path32.relative(entry.absPath, abs));
|
|
6455
6617
|
if (!json) {
|
|
6456
6618
|
log(`[dry-run] Would restore ${rels.length} file(s) from ${entry.id}:`);
|
|
6457
6619
|
for (const rel of rels) log(` ${rel}`);
|
|
@@ -6477,14 +6639,14 @@ async function restoreDirBackup(root, entry, yes, json, dryRun) {
|
|
|
6477
6639
|
let restoredCount = 0;
|
|
6478
6640
|
const skippedAtWrite = [];
|
|
6479
6641
|
for (const abs of toRestore) {
|
|
6480
|
-
const rel =
|
|
6481
|
-
const target =
|
|
6642
|
+
const rel = path32.relative(entry.absPath, abs);
|
|
6643
|
+
const target = path32.join(root, rel);
|
|
6482
6644
|
if (!await lstatIfRegularFile(abs) || await isUnsafeWriteTarget(root, target)) {
|
|
6483
6645
|
skippedAtWrite.push(rel);
|
|
6484
6646
|
continue;
|
|
6485
6647
|
}
|
|
6486
|
-
await
|
|
6487
|
-
await
|
|
6648
|
+
await fs29.ensureDir(path32.dirname(target));
|
|
6649
|
+
await fs29.copy(abs, target, { overwrite: true });
|
|
6488
6650
|
restoredCount++;
|
|
6489
6651
|
}
|
|
6490
6652
|
if (skippedAtWrite.length > 0) {
|
|
@@ -6565,7 +6727,7 @@ Keeping ${keepCount}.`
|
|
|
6565
6727
|
const failed = [];
|
|
6566
6728
|
for (const entry of removeList) {
|
|
6567
6729
|
try {
|
|
6568
|
-
await
|
|
6730
|
+
await fs29.remove(entry.absPath);
|
|
6569
6731
|
removed.push(entry.id);
|
|
6570
6732
|
} catch (err) {
|
|
6571
6733
|
failed.push({ id: entry.id, message: err instanceof Error ? err.message : String(err) });
|
|
@@ -6678,8 +6840,8 @@ init_ingest_catalog();
|
|
|
6678
6840
|
init_manifest_format();
|
|
6679
6841
|
init_path_safety();
|
|
6680
6842
|
import os2 from "node:os";
|
|
6681
|
-
import
|
|
6682
|
-
import
|
|
6843
|
+
import path33 from "node:path";
|
|
6844
|
+
import fs30 from "fs-extra";
|
|
6683
6845
|
import * as tar from "tar";
|
|
6684
6846
|
init_fs();
|
|
6685
6847
|
init_logger();
|
|
@@ -6698,7 +6860,7 @@ async function runBundleExport(options) {
|
|
|
6698
6860
|
return;
|
|
6699
6861
|
}
|
|
6700
6862
|
const cacheDir = getCacheDir();
|
|
6701
|
-
const manifest = await readJson(
|
|
6863
|
+
const manifest = await readJson(path33.join(cacheDir, MANIFEST_FILE));
|
|
6702
6864
|
if (!manifest?.items?.length) {
|
|
6703
6865
|
error("Catalog cache is empty \u2014 run `haus update` on a connected machine first.");
|
|
6704
6866
|
process.exitCode = 1;
|
|
@@ -6709,52 +6871,52 @@ async function runBundleExport(options) {
|
|
|
6709
6871
|
catalogRef: typeof manifest.catalogRef === "string" ? manifest.catalogRef : null,
|
|
6710
6872
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6711
6873
|
};
|
|
6712
|
-
await writeBundleAsTar(cacheDir, meta,
|
|
6874
|
+
await writeBundleAsTar(cacheDir, meta, path33.resolve(out));
|
|
6713
6875
|
log(
|
|
6714
6876
|
`Exported ${manifest.items.length} catalog item(s) to ${out}${meta.catalogRef ? ` (${meta.catalogRef})` : ""}.`
|
|
6715
6877
|
);
|
|
6716
6878
|
log("Move it to the disconnected machine and run `haus catalog bundle import <file>`.");
|
|
6717
6879
|
}
|
|
6718
6880
|
async function writeBundleAsTar(cacheDir, meta, dest) {
|
|
6719
|
-
const bundleMetaPath =
|
|
6720
|
-
const previousBundleMeta = await
|
|
6881
|
+
const bundleMetaPath = path33.join(cacheDir, BUNDLE_META_FILE);
|
|
6882
|
+
const previousBundleMeta = await fs30.pathExists(bundleMetaPath) ? await fs30.readFile(bundleMetaPath, "utf8") : null;
|
|
6721
6883
|
await writeJson(bundleMetaPath, meta);
|
|
6722
6884
|
try {
|
|
6723
|
-
await
|
|
6885
|
+
await fs30.ensureDir(path33.dirname(dest));
|
|
6724
6886
|
await tar.create({ gzip: true, cwd: cacheDir, file: dest }, ["."]);
|
|
6725
6887
|
} finally {
|
|
6726
|
-
if (previousBundleMeta === null) await
|
|
6727
|
-
else await
|
|
6888
|
+
if (previousBundleMeta === null) await fs30.remove(bundleMetaPath);
|
|
6889
|
+
else await fs30.writeFile(bundleMetaPath, previousBundleMeta, "utf8");
|
|
6728
6890
|
}
|
|
6729
6891
|
}
|
|
6730
6892
|
function isMarkdownFile(relOrAbsPath) {
|
|
6731
6893
|
return relOrAbsPath.toLowerCase().endsWith(".md");
|
|
6732
6894
|
}
|
|
6733
6895
|
async function listAllFilesUnfiltered(root, current = root) {
|
|
6734
|
-
const entries = await
|
|
6896
|
+
const entries = await fs30.readdir(current, { withFileTypes: true });
|
|
6735
6897
|
const out = [];
|
|
6736
6898
|
for (const entry of entries) {
|
|
6737
6899
|
if (entry.isSymbolicLink()) continue;
|
|
6738
|
-
const abs =
|
|
6900
|
+
const abs = path33.join(current, entry.name);
|
|
6739
6901
|
if (entry.isDirectory()) {
|
|
6740
6902
|
out.push(...await listAllFilesUnfiltered(root, abs));
|
|
6741
6903
|
} else if (entry.isFile()) {
|
|
6742
|
-
out.push(
|
|
6904
|
+
out.push(path33.relative(root, abs).split(path33.sep).join("/"));
|
|
6743
6905
|
}
|
|
6744
6906
|
}
|
|
6745
6907
|
return out;
|
|
6746
6908
|
}
|
|
6747
6909
|
async function validateStagedItem(item, absPath) {
|
|
6748
|
-
const stat = await
|
|
6910
|
+
const stat = await fs30.stat(absPath).catch(() => null);
|
|
6749
6911
|
if (!stat) return { ok: true };
|
|
6750
6912
|
if (stat.isFile()) {
|
|
6751
|
-
const content2 = await
|
|
6913
|
+
const content2 = await fs30.readFile(absPath, "utf8");
|
|
6752
6914
|
return validateCatalogItem(item, content2);
|
|
6753
6915
|
}
|
|
6754
6916
|
const files = await listAllFilesUnfiltered(absPath);
|
|
6755
6917
|
for (const rel of files) {
|
|
6756
6918
|
if (!isMarkdownFile(rel)) continue;
|
|
6757
|
-
const content2 = await
|
|
6919
|
+
const content2 = await fs30.readFile(path33.join(absPath, rel), "utf8");
|
|
6758
6920
|
const verdict = validateCatalogItem(item, content2);
|
|
6759
6921
|
if (!verdict.ok) return verdict;
|
|
6760
6922
|
}
|
|
@@ -6776,7 +6938,7 @@ async function validateStagedItems(staging, items) {
|
|
|
6776
6938
|
continue;
|
|
6777
6939
|
}
|
|
6778
6940
|
coveredPaths.push(item.path);
|
|
6779
|
-
if (!await
|
|
6941
|
+
if (!await fs30.pathExists(contentPath)) {
|
|
6780
6942
|
if (!isSynthetic) {
|
|
6781
6943
|
accepted.push(item.id);
|
|
6782
6944
|
contentlessIds.add(item.id);
|
|
@@ -6786,7 +6948,7 @@ async function validateStagedItems(staging, items) {
|
|
|
6786
6948
|
const verdict = await validateStagedItem(item, contentPath);
|
|
6787
6949
|
if (!verdict.ok) {
|
|
6788
6950
|
warn(`Rejected ${item.id}: ${verdict.reason}`);
|
|
6789
|
-
await
|
|
6951
|
+
await fs30.remove(contentPath);
|
|
6790
6952
|
if (!isSynthetic) rejected += 1;
|
|
6791
6953
|
continue;
|
|
6792
6954
|
}
|
|
@@ -6798,30 +6960,30 @@ async function validateStagedItems(staging, items) {
|
|
|
6798
6960
|
const covered = coveredPaths.some((p) => rel === p || rel.startsWith(`${p}/`));
|
|
6799
6961
|
if (!covered) {
|
|
6800
6962
|
warn(`Refusing staged file outside any catalog item's path: ${rel}`);
|
|
6801
|
-
await
|
|
6963
|
+
await fs30.remove(path33.join(staging, rel));
|
|
6802
6964
|
}
|
|
6803
6965
|
}
|
|
6804
6966
|
await pruneEmptyDirs(staging);
|
|
6805
6967
|
return { accepted, rejected, contentlessIds };
|
|
6806
6968
|
}
|
|
6807
6969
|
async function pruneEmptyDirs(root, dir = root) {
|
|
6808
|
-
const entries = await
|
|
6970
|
+
const entries = await fs30.readdir(dir, { withFileTypes: true });
|
|
6809
6971
|
for (const entry of entries) {
|
|
6810
6972
|
if (entry.isDirectory()) {
|
|
6811
|
-
await pruneEmptyDirs(root,
|
|
6973
|
+
await pruneEmptyDirs(root, path33.join(dir, entry.name));
|
|
6812
6974
|
}
|
|
6813
6975
|
}
|
|
6814
|
-
if (dir !== root && (await
|
|
6815
|
-
await
|
|
6976
|
+
if (dir !== root && (await fs30.readdir(dir)).length === 0) {
|
|
6977
|
+
await fs30.remove(dir);
|
|
6816
6978
|
}
|
|
6817
6979
|
}
|
|
6818
6980
|
async function stripSymlinks(dir) {
|
|
6819
6981
|
const removed = [];
|
|
6820
|
-
const entries = await
|
|
6982
|
+
const entries = await fs30.readdir(dir, { withFileTypes: true });
|
|
6821
6983
|
for (const entry of entries) {
|
|
6822
|
-
const abs =
|
|
6984
|
+
const abs = path33.join(dir, entry.name);
|
|
6823
6985
|
if (entry.isSymbolicLink()) {
|
|
6824
|
-
await
|
|
6986
|
+
await fs30.remove(abs);
|
|
6825
6987
|
removed.push(abs);
|
|
6826
6988
|
continue;
|
|
6827
6989
|
}
|
|
@@ -6833,13 +6995,13 @@ async function stripSymlinks(dir) {
|
|
|
6833
6995
|
}
|
|
6834
6996
|
async function runBundleImport(file, options = {}) {
|
|
6835
6997
|
const dryRun = Boolean(options.dryRun);
|
|
6836
|
-
const archive =
|
|
6837
|
-
if (!await
|
|
6998
|
+
const archive = path33.resolve(file);
|
|
6999
|
+
if (!await fs30.pathExists(archive)) {
|
|
6838
7000
|
error(`No such bundle: ${file}`);
|
|
6839
7001
|
process.exitCode = 1;
|
|
6840
7002
|
return;
|
|
6841
7003
|
}
|
|
6842
|
-
const staging = await
|
|
7004
|
+
const staging = await fs30.mkdtemp(path33.join(os2.tmpdir(), "haus-catalog-bundle-import-"));
|
|
6843
7005
|
try {
|
|
6844
7006
|
await tar.extract({
|
|
6845
7007
|
file: archive,
|
|
@@ -6859,9 +7021,9 @@ async function runBundleImport(file, options = {}) {
|
|
|
6859
7021
|
});
|
|
6860
7022
|
const strippedSymlinks = await stripSymlinks(staging);
|
|
6861
7023
|
for (const abs of strippedSymlinks) {
|
|
6862
|
-
warn(`Refusing symlink in bundle: ${
|
|
7024
|
+
warn(`Refusing symlink in bundle: ${path33.relative(staging, abs)}`);
|
|
6863
7025
|
}
|
|
6864
|
-
const manifest = await readJson(
|
|
7026
|
+
const manifest = await readJson(path33.join(staging, MANIFEST_FILE));
|
|
6865
7027
|
if (!manifest?.items?.length) {
|
|
6866
7028
|
error("Bundle contains no catalog manifest \u2014 refusing to import.");
|
|
6867
7029
|
process.exitCode = 1;
|
|
@@ -6872,7 +7034,7 @@ async function runBundleImport(file, options = {}) {
|
|
|
6872
7034
|
staging,
|
|
6873
7035
|
manifest.items
|
|
6874
7036
|
);
|
|
6875
|
-
const meta = await readJson(
|
|
7037
|
+
const meta = await readJson(path33.join(staging, BUNDLE_META_FILE));
|
|
6876
7038
|
const label = meta?.catalogRef ?? (meta?.version ? `v${meta.version}` : "unknown ref");
|
|
6877
7039
|
const installedCount = accepted.length;
|
|
6878
7040
|
if (dryRun) {
|
|
@@ -6895,9 +7057,9 @@ async function runBundleImport(file, options = {}) {
|
|
|
6895
7057
|
}
|
|
6896
7058
|
return;
|
|
6897
7059
|
}
|
|
6898
|
-
await
|
|
6899
|
-
for (const entry of await
|
|
6900
|
-
await
|
|
7060
|
+
await fs30.ensureDir(getCacheDir());
|
|
7061
|
+
for (const entry of await fs30.readdir(staging)) {
|
|
7062
|
+
await fs30.copy(path33.join(staging, entry), path33.join(getCacheDir(), entry), {
|
|
6901
7063
|
overwrite: true,
|
|
6902
7064
|
errorOnExist: false
|
|
6903
7065
|
});
|
|
@@ -6914,17 +7076,17 @@ async function runBundleImport(file, options = {}) {
|
|
|
6914
7076
|
}
|
|
6915
7077
|
throw err;
|
|
6916
7078
|
} finally {
|
|
6917
|
-
await
|
|
7079
|
+
await fs30.remove(staging);
|
|
6918
7080
|
}
|
|
6919
7081
|
}
|
|
6920
7082
|
|
|
6921
7083
|
// src/commands/doctor.ts
|
|
6922
|
-
import
|
|
6923
|
-
import
|
|
7084
|
+
import path49 from "node:path";
|
|
7085
|
+
import fs41 from "fs-extra";
|
|
6924
7086
|
init_manifest_format();
|
|
6925
7087
|
|
|
6926
7088
|
// src/claude/former-install-orphans.ts
|
|
6927
|
-
import
|
|
7089
|
+
import fs31 from "fs-extra";
|
|
6928
7090
|
init_path_safety();
|
|
6929
7091
|
init_paths();
|
|
6930
7092
|
var toPosix = (value) => value.replace(/\\/g, "/");
|
|
@@ -6948,7 +7110,7 @@ async function findOrphanedFormerInstalls(root, items, lockRows) {
|
|
|
6948
7110
|
const projectRel = `.claude/${relPath}`;
|
|
6949
7111
|
if (trackedPaths.has(projectRel) || reported.has(projectRel)) continue;
|
|
6950
7112
|
const abs = safeJoin(claudePath(root), relPath);
|
|
6951
|
-
if (abs === null || !await
|
|
7113
|
+
if (abs === null || !await fs31.pathExists(abs)) continue;
|
|
6952
7114
|
reported.add(projectRel);
|
|
6953
7115
|
orphans.push({ formerId: entry.formerId, currentId: entry.currentId, relPath: projectRel });
|
|
6954
7116
|
}
|
|
@@ -6956,13 +7118,13 @@ async function findOrphanedFormerInstalls(root, items, lockRows) {
|
|
|
6956
7118
|
}
|
|
6957
7119
|
|
|
6958
7120
|
// src/claude/plugin-duplicate-skills.ts
|
|
6959
|
-
import
|
|
6960
|
-
import
|
|
7121
|
+
import fs32 from "node:fs";
|
|
7122
|
+
import path34 from "node:path";
|
|
6961
7123
|
init_paths();
|
|
6962
7124
|
function subdirectoryNames(dir) {
|
|
6963
7125
|
let entries;
|
|
6964
7126
|
try {
|
|
6965
|
-
entries =
|
|
7127
|
+
entries = fs32.readdirSync(dir, { withFileTypes: true });
|
|
6966
7128
|
} catch {
|
|
6967
7129
|
return [];
|
|
6968
7130
|
}
|
|
@@ -6971,7 +7133,7 @@ function subdirectoryNames(dir) {
|
|
|
6971
7133
|
function isDirectorySymlink(dir, entry) {
|
|
6972
7134
|
if (!entry.isSymbolicLink()) return false;
|
|
6973
7135
|
try {
|
|
6974
|
-
return
|
|
7136
|
+
return fs32.statSync(path34.join(dir, entry.name)).isDirectory();
|
|
6975
7137
|
} catch {
|
|
6976
7138
|
return false;
|
|
6977
7139
|
}
|
|
@@ -6989,7 +7151,7 @@ function findPluginDuplicateSkills(root, pluginSkillDirs) {
|
|
|
6989
7151
|
return [...duplicates].sort();
|
|
6990
7152
|
}
|
|
6991
7153
|
function pluginSkillsDir(installLocation) {
|
|
6992
|
-
return
|
|
7154
|
+
return path34.join(installLocation, "skills");
|
|
6993
7155
|
}
|
|
6994
7156
|
function reportPluginDuplicates(root, { activePins, marketplaces, lockIds }) {
|
|
6995
7157
|
if (activePins.length === 0) return { supersededIds: [], untrackedNames: [] };
|
|
@@ -7016,7 +7178,7 @@ init_project_pin();
|
|
|
7016
7178
|
init_fs();
|
|
7017
7179
|
init_logger();
|
|
7018
7180
|
init_paths();
|
|
7019
|
-
import
|
|
7181
|
+
import fs33 from "fs-extra";
|
|
7020
7182
|
function normalizeIdList(value) {
|
|
7021
7183
|
if (!Array.isArray(value)) return [];
|
|
7022
7184
|
return [
|
|
@@ -7044,9 +7206,9 @@ async function readProjectConfigObject(root) {
|
|
|
7044
7206
|
async function updateProjectConfig(root, mutate) {
|
|
7045
7207
|
const file = hausPath(root, PROJECT_CONFIG_FILE);
|
|
7046
7208
|
let existing = {};
|
|
7047
|
-
if (await
|
|
7209
|
+
if (await fs33.pathExists(file)) {
|
|
7048
7210
|
try {
|
|
7049
|
-
const parsed = JSON.parse(await
|
|
7211
|
+
const parsed = JSON.parse(await fs33.readFile(file, "utf8"));
|
|
7050
7212
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
7051
7213
|
existing = { ...parsed };
|
|
7052
7214
|
} else {
|
|
@@ -7128,9 +7290,9 @@ async function addProjectExcludedIds(root, ids) {
|
|
|
7128
7290
|
|
|
7129
7291
|
// src/claude/skill-reference-check.ts
|
|
7130
7292
|
init_paths();
|
|
7131
|
-
import
|
|
7293
|
+
import path35 from "node:path";
|
|
7132
7294
|
import fg4 from "fast-glob";
|
|
7133
|
-
import
|
|
7295
|
+
import fs34 from "fs-extra";
|
|
7134
7296
|
var REFERENCE_PATTERN = /\b([a-z][a-z0-9]*(?:-[a-z0-9]+)+):([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\b(?!:\/\/)/g;
|
|
7135
7297
|
function escapeRegExp2(value) {
|
|
7136
7298
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -7303,14 +7465,14 @@ async function findUnresolvedAgentReferences(root, options = {}) {
|
|
|
7303
7465
|
}
|
|
7304
7466
|
async function readInstalledSkillNames(root) {
|
|
7305
7467
|
const skillsRoot = claudePath(root, "skills");
|
|
7306
|
-
if (!await
|
|
7307
|
-
const entries = await
|
|
7468
|
+
if (!await fs34.pathExists(skillsRoot)) return /* @__PURE__ */ new Set();
|
|
7469
|
+
const entries = await fs34.readdir(skillsRoot, { withFileTypes: true });
|
|
7308
7470
|
return new Set(entries.filter((e) => e.isDirectory() && e.name !== "shared").map((e) => e.name));
|
|
7309
7471
|
}
|
|
7310
7472
|
async function readInstalledAgentNames(root) {
|
|
7311
7473
|
const agentsRoot = claudePath(root, "agents");
|
|
7312
|
-
if (!await
|
|
7313
|
-
const entries = await
|
|
7474
|
+
if (!await fs34.pathExists(agentsRoot)) return /* @__PURE__ */ new Set();
|
|
7475
|
+
const entries = await fs34.readdir(agentsRoot, { withFileTypes: true });
|
|
7314
7476
|
return new Set(
|
|
7315
7477
|
entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name.replace(/\.md$/, ""))
|
|
7316
7478
|
);
|
|
@@ -7319,14 +7481,14 @@ async function readScannedMarkdown(root) {
|
|
|
7319
7481
|
const scanned = [];
|
|
7320
7482
|
for (const dir of ["skills", "agents", "rules"]) {
|
|
7321
7483
|
const dirRoot = claudePath(root, dir);
|
|
7322
|
-
if (!await
|
|
7484
|
+
if (!await fs34.pathExists(dirRoot)) continue;
|
|
7323
7485
|
const pattern = dir === "skills" ? "*/**/*.md" : "**/*.md";
|
|
7324
7486
|
const files = await fg4(pattern, { cwd: dirRoot, onlyFiles: true, dot: true });
|
|
7325
7487
|
for (const rel of files) {
|
|
7326
7488
|
if (dir === "skills" && rel.startsWith("shared/")) continue;
|
|
7327
|
-
const raw = await
|
|
7489
|
+
const raw = await fs34.readFile(path35.join(dirRoot, rel), "utf8");
|
|
7328
7490
|
scanned.push({
|
|
7329
|
-
file:
|
|
7491
|
+
file: path35.posix.join(".claude", dir, rel.split(path35.sep).join("/")),
|
|
7330
7492
|
lines: stripFencedCodeBlocks(raw).split("\n")
|
|
7331
7493
|
});
|
|
7332
7494
|
}
|
|
@@ -7336,14 +7498,14 @@ async function readScannedMarkdown(root) {
|
|
|
7336
7498
|
|
|
7337
7499
|
// src/decisions/doctor.ts
|
|
7338
7500
|
init_fs();
|
|
7339
|
-
import
|
|
7340
|
-
import
|
|
7501
|
+
import path40 from "node:path";
|
|
7502
|
+
import fs38 from "fs-extra";
|
|
7341
7503
|
|
|
7342
7504
|
// src/decisions/check.ts
|
|
7343
7505
|
init_fs();
|
|
7344
7506
|
init_paths();
|
|
7345
|
-
import
|
|
7346
|
-
import
|
|
7507
|
+
import path39 from "node:path";
|
|
7508
|
+
import fs37 from "fs-extra";
|
|
7347
7509
|
|
|
7348
7510
|
// src/decisions/dep-bump.ts
|
|
7349
7511
|
var DEP_SECTIONS = [
|
|
@@ -7403,7 +7565,7 @@ function isDependencyBumpOnly(beforeText, afterText) {
|
|
|
7403
7565
|
// src/decisions/diff.ts
|
|
7404
7566
|
init_exec();
|
|
7405
7567
|
init_fs();
|
|
7406
|
-
import
|
|
7568
|
+
import path36 from "node:path";
|
|
7407
7569
|
var NUL = String.fromCharCode(0);
|
|
7408
7570
|
function parseNumstat(output) {
|
|
7409
7571
|
const fields2 = output.split(NUL);
|
|
@@ -7490,7 +7652,7 @@ async function resolvePackageJsonPair(root, file, opts) {
|
|
|
7490
7652
|
return { before: before2, after: after2 };
|
|
7491
7653
|
}
|
|
7492
7654
|
const before = await readBlob(root, `${range}:${file}`) ?? empty;
|
|
7493
|
-
const after = await readText(
|
|
7655
|
+
const after = await readText(path36.join(root, file)) ?? empty;
|
|
7494
7656
|
return { before, after };
|
|
7495
7657
|
}
|
|
7496
7658
|
async function collectDiffStats(root, opts) {
|
|
@@ -7553,23 +7715,23 @@ function firstMatchingRegex(filePath, patterns) {
|
|
|
7553
7715
|
}
|
|
7554
7716
|
|
|
7555
7717
|
// src/decisions/paths.ts
|
|
7556
|
-
import
|
|
7557
|
-
import
|
|
7718
|
+
import path37 from "node:path";
|
|
7719
|
+
import fs35 from "fs-extra";
|
|
7558
7720
|
async function resolveDecisionsDir(root) {
|
|
7559
|
-
const primary =
|
|
7560
|
-
if (await
|
|
7561
|
-
const legacy =
|
|
7562
|
-
if (await
|
|
7721
|
+
const primary = path37.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
7722
|
+
if (await fs35.pathExists(primary)) return primary;
|
|
7723
|
+
const legacy = path37.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
7724
|
+
if (await fs35.pathExists(legacy)) return legacy;
|
|
7563
7725
|
return primary;
|
|
7564
7726
|
}
|
|
7565
7727
|
function relativeDecisionPath(decisionsDir, root) {
|
|
7566
|
-
return
|
|
7728
|
+
return path37.relative(root, decisionsDir).replace(/\\/g, "/");
|
|
7567
7729
|
}
|
|
7568
7730
|
|
|
7569
7731
|
// src/decisions/validate.ts
|
|
7570
7732
|
init_fs();
|
|
7571
|
-
import
|
|
7572
|
-
import
|
|
7733
|
+
import path38 from "node:path";
|
|
7734
|
+
import fs36 from "fs-extra";
|
|
7573
7735
|
function validateDecisionContent(content2) {
|
|
7574
7736
|
const errors = [];
|
|
7575
7737
|
for (const section of DECISIONS_TRIGGERS.requiredSections) {
|
|
@@ -7598,14 +7760,14 @@ function decisionNumberFromFile(name) {
|
|
|
7598
7760
|
return match?.[1] ?? null;
|
|
7599
7761
|
}
|
|
7600
7762
|
async function validateIndexSync(decisionsDir, decisionFiles) {
|
|
7601
|
-
const readmePath =
|
|
7602
|
-
if (!await
|
|
7763
|
+
const readmePath = path38.join(decisionsDir, "README.md");
|
|
7764
|
+
if (!await fs36.pathExists(readmePath)) {
|
|
7603
7765
|
return ["docs/decisions/README.md is missing"];
|
|
7604
7766
|
}
|
|
7605
7767
|
const readme = await readText(readmePath) ?? "";
|
|
7606
7768
|
const errors = [];
|
|
7607
7769
|
for (const file of decisionFiles) {
|
|
7608
|
-
const num = decisionNumberFromFile(
|
|
7770
|
+
const num = decisionNumberFromFile(path38.basename(file));
|
|
7609
7771
|
if (num && !indexReferencesNumber(readme, num)) {
|
|
7610
7772
|
errors.push(`README.md missing index row for ADR-${num}`);
|
|
7611
7773
|
}
|
|
@@ -7705,22 +7867,22 @@ async function checkSatisfaction(input2) {
|
|
|
7705
7867
|
const errors = [];
|
|
7706
7868
|
const decisionsPrefix = `${input2.decisionsRel}/`;
|
|
7707
7869
|
const newDecisionFiles = input2.changedFiles.filter(
|
|
7708
|
-
(f) => f.startsWith(decisionsPrefix) && isDecisionFileName(
|
|
7870
|
+
(f) => f.startsWith(decisionsPrefix) && isDecisionFileName(path39.basename(f))
|
|
7709
7871
|
);
|
|
7710
7872
|
if (newDecisionFiles.length === 0) {
|
|
7711
7873
|
errors.push(`no new or updated decision under ${input2.decisionsRel}/`);
|
|
7712
7874
|
return errors;
|
|
7713
7875
|
}
|
|
7714
|
-
const decisionsDir =
|
|
7876
|
+
const decisionsDir = path39.join(input2.root, input2.decisionsRel);
|
|
7715
7877
|
for (const rel of newDecisionFiles) {
|
|
7716
|
-
const abs =
|
|
7878
|
+
const abs = path39.join(input2.root, rel);
|
|
7717
7879
|
const content2 = await readText(abs) ?? "";
|
|
7718
7880
|
const valid = validateDecisionContent(content2);
|
|
7719
7881
|
if (!valid.ok) errors.push(...valid.errors.map((e) => `${rel}: ${e}`));
|
|
7720
7882
|
}
|
|
7721
7883
|
const indexErrors = await validateIndexSync(
|
|
7722
7884
|
decisionsDir,
|
|
7723
|
-
newDecisionFiles.map((f) =>
|
|
7885
|
+
newDecisionFiles.map((f) => path39.basename(f))
|
|
7724
7886
|
);
|
|
7725
7887
|
errors.push(...indexErrors);
|
|
7726
7888
|
return errors;
|
|
@@ -7776,8 +7938,8 @@ ${opts.commitMessages ?? ""}`;
|
|
|
7776
7938
|
changedFiles: stats.files
|
|
7777
7939
|
});
|
|
7778
7940
|
if (satisfactionErrors.length > 0 && decisionsRel !== legacyRel) {
|
|
7779
|
-
const legacyExists = await
|
|
7780
|
-
if (legacyExists && !await
|
|
7941
|
+
const legacyExists = await fs37.pathExists(path39.join(root, legacyRel));
|
|
7942
|
+
if (legacyExists && !await fs37.pathExists(decisionsDir)) {
|
|
7781
7943
|
satisfactionErrors = await checkSatisfaction({
|
|
7782
7944
|
root,
|
|
7783
7945
|
decisionsRel: legacyRel,
|
|
@@ -7799,18 +7961,18 @@ ${opts.commitMessages ?? ""}`;
|
|
|
7799
7961
|
return { triggered: true, satisfied: false, reasons, securityTriggered };
|
|
7800
7962
|
}
|
|
7801
7963
|
async function listDecisionNumbers(decisionsDir) {
|
|
7802
|
-
if (!await
|
|
7803
|
-
const entries = await
|
|
7964
|
+
if (!await fs37.pathExists(decisionsDir)) return [];
|
|
7965
|
+
const entries = await fs37.readdir(decisionsDir);
|
|
7804
7966
|
return entries.map((name) => decisionNumberFromFile(name)).filter((n) => n != null).map((n) => Number(n));
|
|
7805
7967
|
}
|
|
7806
7968
|
|
|
7807
7969
|
// src/decisions/doctor.ts
|
|
7808
7970
|
async function auditDecisionsLayout(root) {
|
|
7809
7971
|
const findings = [];
|
|
7810
|
-
const primary =
|
|
7811
|
-
const legacy =
|
|
7812
|
-
const hasPrimary = await
|
|
7813
|
-
const hasLegacy = await
|
|
7972
|
+
const primary = path40.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
7973
|
+
const legacy = path40.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
7974
|
+
const hasPrimary = await fs38.pathExists(primary);
|
|
7975
|
+
const hasLegacy = await fs38.pathExists(legacy);
|
|
7814
7976
|
if (!hasPrimary && hasLegacy) {
|
|
7815
7977
|
findings.push({
|
|
7816
7978
|
level: "advisory",
|
|
@@ -7823,8 +7985,8 @@ async function auditDecisionsLayout(root) {
|
|
|
7823
7985
|
if (!hasPrimary) return findings;
|
|
7824
7986
|
const decisionsDir = await resolveDecisionsDir(root);
|
|
7825
7987
|
const rel = relativeDecisionPath(decisionsDir, root);
|
|
7826
|
-
const readmePath =
|
|
7827
|
-
if (!await
|
|
7988
|
+
const readmePath = path40.join(decisionsDir, "README.md");
|
|
7989
|
+
if (!await fs38.pathExists(readmePath)) {
|
|
7828
7990
|
findings.push({
|
|
7829
7991
|
level: "advisory",
|
|
7830
7992
|
line: `- DECISIONS: missing ${rel}/README.md index`,
|
|
@@ -7834,7 +7996,7 @@ async function auditDecisionsLayout(root) {
|
|
|
7834
7996
|
return findings;
|
|
7835
7997
|
}
|
|
7836
7998
|
const readme = await readText(readmePath);
|
|
7837
|
-
const entries = (await
|
|
7999
|
+
const entries = (await fs38.readdir(decisionsDir)).filter(
|
|
7838
8000
|
(f) => f.endsWith(".md") && f !== "README.md"
|
|
7839
8001
|
);
|
|
7840
8002
|
for (const file of entries) {
|
|
@@ -7848,7 +8010,7 @@ async function auditDecisionsLayout(root) {
|
|
|
7848
8010
|
});
|
|
7849
8011
|
}
|
|
7850
8012
|
}
|
|
7851
|
-
const claude = await readText(
|
|
8013
|
+
const claude = await readText(path40.join(root, "CLAUDE.md"));
|
|
7852
8014
|
if (claude && !claude.includes(`@${rel}/README.md`)) {
|
|
7853
8015
|
findings.push({
|
|
7854
8016
|
level: "advisory",
|
|
@@ -7882,14 +8044,14 @@ async function auditDecisionsLayout(root) {
|
|
|
7882
8044
|
init_fs();
|
|
7883
8045
|
init_logger();
|
|
7884
8046
|
init_paths();
|
|
7885
|
-
import
|
|
7886
|
-
import
|
|
8047
|
+
import path41 from "node:path";
|
|
8048
|
+
import fs39 from "fs-extra";
|
|
7887
8049
|
function manifestPathIn(configDir) {
|
|
7888
|
-
return
|
|
8050
|
+
return path41.join(configDir, "haus", "install-manifest.json");
|
|
7889
8051
|
}
|
|
7890
8052
|
function relativeInside(base, target) {
|
|
7891
|
-
const rel =
|
|
7892
|
-
if (!rel || rel === ".." || rel.startsWith(`..${
|
|
8053
|
+
const rel = path41.relative(base, target);
|
|
8054
|
+
if (!rel || rel === ".." || rel.startsWith(`..${path41.sep}`) || path41.isAbsolute(rel)) {
|
|
7893
8055
|
return void 0;
|
|
7894
8056
|
}
|
|
7895
8057
|
return rel;
|
|
@@ -7901,27 +8063,27 @@ async function sameContent(a, b) {
|
|
|
7901
8063
|
async function pruneVacatedDirs(root, vacatedFiles) {
|
|
7902
8064
|
const dirs = /* @__PURE__ */ new Set();
|
|
7903
8065
|
for (const file of vacatedFiles) {
|
|
7904
|
-
let dir =
|
|
8066
|
+
let dir = path41.dirname(file);
|
|
7905
8067
|
while (relativeInside(root, dir) !== void 0) {
|
|
7906
8068
|
dirs.add(dir);
|
|
7907
|
-
dir =
|
|
8069
|
+
dir = path41.dirname(dir);
|
|
7908
8070
|
}
|
|
7909
8071
|
}
|
|
7910
|
-
const deepestFirst = [...dirs].sort((a, b) => b.split(
|
|
8072
|
+
const deepestFirst = [...dirs].sort((a, b) => b.split(path41.sep).length - a.split(path41.sep).length);
|
|
7911
8073
|
for (const dir of deepestFirst) await pruneEmptyDir(dir);
|
|
7912
8074
|
}
|
|
7913
8075
|
async function destinationWritable(dir) {
|
|
7914
|
-
let candidate =
|
|
8076
|
+
let candidate = path41.resolve(dir);
|
|
7915
8077
|
for (; ; ) {
|
|
7916
|
-
if (await
|
|
8078
|
+
if (await fs39.pathExists(candidate)) {
|
|
7917
8079
|
try {
|
|
7918
|
-
await
|
|
8080
|
+
await fs39.access(candidate, fs39.constants.W_OK);
|
|
7919
8081
|
return true;
|
|
7920
8082
|
} catch (err) {
|
|
7921
8083
|
return err instanceof Error ? err.message : String(err);
|
|
7922
8084
|
}
|
|
7923
8085
|
}
|
|
7924
|
-
const parent =
|
|
8086
|
+
const parent = path41.dirname(candidate);
|
|
7925
8087
|
if (parent === candidate) return `no existing ancestor directory for ${dir}`;
|
|
7926
8088
|
candidate = parent;
|
|
7927
8089
|
}
|
|
@@ -7947,7 +8109,7 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
7947
8109
|
if (!legacy || !Array.isArray(legacy.files)) {
|
|
7948
8110
|
return { ...base, reason: "no install manifest under ~/.claude" };
|
|
7949
8111
|
}
|
|
7950
|
-
if (await
|
|
8112
|
+
if (await fs39.pathExists(manifestPathIn(to))) {
|
|
7951
8113
|
return { ...base, reason: "an install manifest already exists in the resolved config dir" };
|
|
7952
8114
|
}
|
|
7953
8115
|
const writable = await destinationWritable(to);
|
|
@@ -7969,15 +8131,15 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
7969
8131
|
rewritten.push(entry);
|
|
7970
8132
|
continue;
|
|
7971
8133
|
}
|
|
7972
|
-
const newDest =
|
|
7973
|
-
if (!await
|
|
8134
|
+
const newDest = path41.join(to, rel);
|
|
8135
|
+
if (!await fs39.pathExists(entry.destPath)) {
|
|
7974
8136
|
rewritten.push({ ...entry, destPath: newDest });
|
|
7975
8137
|
continue;
|
|
7976
8138
|
}
|
|
7977
|
-
if (await
|
|
8139
|
+
if (await fs39.pathExists(newDest)) {
|
|
7978
8140
|
if (await sameContent(entry.destPath, newDest)) {
|
|
7979
8141
|
if (!dryRun) {
|
|
7980
|
-
await
|
|
8142
|
+
await fs39.remove(entry.destPath);
|
|
7981
8143
|
vacated.push(entry.destPath);
|
|
7982
8144
|
}
|
|
7983
8145
|
moved.push(newDest);
|
|
@@ -7994,7 +8156,7 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
7994
8156
|
continue;
|
|
7995
8157
|
}
|
|
7996
8158
|
try {
|
|
7997
|
-
await
|
|
8159
|
+
await fs39.move(entry.destPath, newDest);
|
|
7998
8160
|
vacated.push(entry.destPath);
|
|
7999
8161
|
moved.push(newDest);
|
|
8000
8162
|
rewritten.push({ ...entry, destPath: newDest });
|
|
@@ -8019,7 +8181,7 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
8019
8181
|
}
|
|
8020
8182
|
try {
|
|
8021
8183
|
await writeJson(manifestPathIn(to), { ...legacy, files: rewritten });
|
|
8022
|
-
await
|
|
8184
|
+
await fs39.remove(legacyManifestPath);
|
|
8023
8185
|
vacated.push(legacyManifestPath);
|
|
8024
8186
|
} catch (err) {
|
|
8025
8187
|
return {
|
|
@@ -8148,12 +8310,12 @@ function formatPrereqReport(report) {
|
|
|
8148
8310
|
}
|
|
8149
8311
|
|
|
8150
8312
|
// src/scanner/read-context.ts
|
|
8151
|
-
import
|
|
8152
|
-
import
|
|
8313
|
+
import path48 from "node:path";
|
|
8314
|
+
import fs40 from "fs-extra";
|
|
8153
8315
|
|
|
8154
8316
|
// src/commands/workspace/config.ts
|
|
8155
8317
|
init_fs();
|
|
8156
|
-
import
|
|
8318
|
+
import path42 from "node:path";
|
|
8157
8319
|
import YAML2 from "yaml";
|
|
8158
8320
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
8159
8321
|
function parseWorkspaceConfig(text) {
|
|
@@ -8184,7 +8346,7 @@ function parseWorkspaceConfig(text) {
|
|
|
8184
8346
|
};
|
|
8185
8347
|
}
|
|
8186
8348
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
8187
|
-
return parseWorkspaceConfig(await readText(
|
|
8349
|
+
return parseWorkspaceConfig(await readText(path42.join(workspaceRoot, WORKSPACE_FILE)));
|
|
8188
8350
|
}
|
|
8189
8351
|
|
|
8190
8352
|
// src/scanner/read-context.ts
|
|
@@ -8192,7 +8354,7 @@ init_fs();
|
|
|
8192
8354
|
init_paths();
|
|
8193
8355
|
|
|
8194
8356
|
// src/workspace/members.ts
|
|
8195
|
-
import
|
|
8357
|
+
import path43 from "node:path";
|
|
8196
8358
|
init_fs();
|
|
8197
8359
|
var REPOS_MANIFEST_FILE = "repos.manifest.json";
|
|
8198
8360
|
var REPOS_LOCAL_FILE = "repos.local.json";
|
|
@@ -8206,14 +8368,14 @@ var MemberConfigError = class extends Error {
|
|
|
8206
8368
|
};
|
|
8207
8369
|
function resolveAbsPath(workspaceRoot, folder, overrides) {
|
|
8208
8370
|
const override = overrides[folder];
|
|
8209
|
-
if (override) return
|
|
8210
|
-
return
|
|
8371
|
+
if (override) return path43.resolve(workspaceRoot, override);
|
|
8372
|
+
return path43.resolve(workspaceRoot, folder);
|
|
8211
8373
|
}
|
|
8212
8374
|
function isSelfMember(workspaceRoot, member) {
|
|
8213
|
-
return
|
|
8375
|
+
return path43.resolve(member.absPath) === path43.resolve(workspaceRoot);
|
|
8214
8376
|
}
|
|
8215
8377
|
async function readPathOverrides(workspaceRoot) {
|
|
8216
|
-
const filePath =
|
|
8378
|
+
const filePath = path43.join(workspaceRoot, REPOS_LOCAL_FILE);
|
|
8217
8379
|
const result = await readJsonDetailed(filePath);
|
|
8218
8380
|
if (result.status === "missing") return {};
|
|
8219
8381
|
if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
|
|
@@ -8229,7 +8391,7 @@ async function readPathOverrides(workspaceRoot) {
|
|
|
8229
8391
|
return overrides;
|
|
8230
8392
|
}
|
|
8231
8393
|
async function readFromWorkspaceYaml(workspaceRoot, overrides) {
|
|
8232
|
-
const filePath =
|
|
8394
|
+
const filePath = path43.join(workspaceRoot, WORKSPACE_FILE);
|
|
8233
8395
|
const text = await readText(filePath);
|
|
8234
8396
|
if (text === void 0) return void 0;
|
|
8235
8397
|
const config = parseWorkspaceConfig(text);
|
|
@@ -8242,7 +8404,7 @@ async function readFromWorkspaceYaml(workspaceRoot, overrides) {
|
|
|
8242
8404
|
}));
|
|
8243
8405
|
}
|
|
8244
8406
|
async function readFromManifest(workspaceRoot, overrides) {
|
|
8245
|
-
const filePath =
|
|
8407
|
+
const filePath = path43.join(workspaceRoot, REPOS_MANIFEST_FILE);
|
|
8246
8408
|
const result = await readJsonDetailed(filePath);
|
|
8247
8409
|
if (result.status === "missing") return void 0;
|
|
8248
8410
|
if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
|
|
@@ -8284,12 +8446,12 @@ async function readMembers(rootInfo) {
|
|
|
8284
8446
|
}
|
|
8285
8447
|
|
|
8286
8448
|
// src/scanner/scan-project.ts
|
|
8287
|
-
import
|
|
8449
|
+
import path47 from "node:path";
|
|
8288
8450
|
init_fs();
|
|
8289
8451
|
|
|
8290
8452
|
// src/utils/git-root.ts
|
|
8291
8453
|
init_exec();
|
|
8292
|
-
import
|
|
8454
|
+
import path44 from "node:path";
|
|
8293
8455
|
function fallback(start) {
|
|
8294
8456
|
return {
|
|
8295
8457
|
cwd: start,
|
|
@@ -8305,11 +8467,11 @@ function fallback(start) {
|
|
|
8305
8467
|
async function resolveAbsoluteGitPath(flag, cwd, repoRoot) {
|
|
8306
8468
|
const withFormat = await runGit(["rev-parse", "--path-format=absolute", flag], { cwd });
|
|
8307
8469
|
const raw = withFormat.exitCode === 0 ? withFormat.stdout.trim() : "";
|
|
8308
|
-
if (raw &&
|
|
8470
|
+
if (raw && path44.isAbsolute(raw)) return path44.resolve(raw);
|
|
8309
8471
|
const plain = await runGit(["rev-parse", flag], { cwd });
|
|
8310
8472
|
const plainRaw = plain.exitCode === 0 ? plain.stdout.trim() : "";
|
|
8311
8473
|
if (!plainRaw) return "";
|
|
8312
|
-
return
|
|
8474
|
+
return path44.isAbsolute(plainRaw) ? path44.resolve(plainRaw) : path44.resolve(repoRoot, plainRaw);
|
|
8313
8475
|
}
|
|
8314
8476
|
async function resolveRoots(start) {
|
|
8315
8477
|
const cwd = start ?? process.cwd();
|
|
@@ -8318,15 +8480,15 @@ async function resolveRoots(start) {
|
|
|
8318
8480
|
if (toplevel.exitCode !== 0) return fallback(cwd);
|
|
8319
8481
|
const toplevelRaw = toplevel.stdout.trim();
|
|
8320
8482
|
if (!toplevelRaw) return fallback(cwd);
|
|
8321
|
-
const repoRoot =
|
|
8483
|
+
const repoRoot = path44.resolve(toplevelRaw);
|
|
8322
8484
|
const [gitDir, gitCommonDir] = await Promise.all([
|
|
8323
8485
|
resolveAbsoluteGitPath("--git-dir", cwd, repoRoot),
|
|
8324
8486
|
resolveAbsoluteGitPath("--git-common-dir", cwd, repoRoot)
|
|
8325
8487
|
]);
|
|
8326
8488
|
if (!gitDir || !gitCommonDir) return fallback(cwd);
|
|
8327
8489
|
const isLinkedWorktree = gitDir !== gitCommonDir;
|
|
8328
|
-
const mainRoot = isLinkedWorktree &&
|
|
8329
|
-
const worktreeName = isLinkedWorktree ?
|
|
8490
|
+
const mainRoot = isLinkedWorktree && path44.basename(gitCommonDir) === ".git" ? path44.dirname(gitCommonDir) : repoRoot;
|
|
8491
|
+
const worktreeName = isLinkedWorktree ? path44.basename(gitDir) : null;
|
|
8330
8492
|
return {
|
|
8331
8493
|
cwd,
|
|
8332
8494
|
repoRoot,
|
|
@@ -8626,7 +8788,7 @@ function runDetection(ctx, rules = STACK_RULES) {
|
|
|
8626
8788
|
// src/scanner/render.ts
|
|
8627
8789
|
init_fs();
|
|
8628
8790
|
import { readFile } from "node:fs/promises";
|
|
8629
|
-
import
|
|
8791
|
+
import path45 from "node:path";
|
|
8630
8792
|
async function buildContentBlob(root, files) {
|
|
8631
8793
|
const candidates = files.filter(
|
|
8632
8794
|
(f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
|
|
@@ -8634,7 +8796,7 @@ async function buildContentBlob(root, files) {
|
|
|
8634
8796
|
const slice = candidates.slice(0, 300);
|
|
8635
8797
|
const parts = await mapWithConcurrency(slice, async (rel) => {
|
|
8636
8798
|
try {
|
|
8637
|
-
return await readFile(
|
|
8799
|
+
return await readFile(path45.join(root, rel), "utf8");
|
|
8638
8800
|
} catch {
|
|
8639
8801
|
return "";
|
|
8640
8802
|
}
|
|
@@ -8643,7 +8805,7 @@ async function buildContentBlob(root, files) {
|
|
|
8643
8805
|
}
|
|
8644
8806
|
|
|
8645
8807
|
// src/scanner/workspace-packages.ts
|
|
8646
|
-
import
|
|
8808
|
+
import path46 from "node:path";
|
|
8647
8809
|
import fg5 from "fast-glob";
|
|
8648
8810
|
import YAML3 from "yaml";
|
|
8649
8811
|
init_fs();
|
|
@@ -8651,7 +8813,7 @@ var PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
|
|
|
8651
8813
|
var MAX_WORKSPACE_MEMBERS = 200;
|
|
8652
8814
|
function isRepoRelativeGlob(glob) {
|
|
8653
8815
|
if (glob.length === 0) return false;
|
|
8654
|
-
if (
|
|
8816
|
+
if (path46.isAbsolute(glob) || glob.startsWith("/")) return false;
|
|
8655
8817
|
return !glob.split("/").includes("..");
|
|
8656
8818
|
}
|
|
8657
8819
|
function stringEntries(value) {
|
|
@@ -8676,7 +8838,7 @@ function workspaceGlobsFromPnpmYaml(text) {
|
|
|
8676
8838
|
}
|
|
8677
8839
|
async function readWorkspaceGlobs(root, pkg) {
|
|
8678
8840
|
const fromPkg = workspaceGlobsFromPackageJson(pkg);
|
|
8679
|
-
const pnpmText = await readText(
|
|
8841
|
+
const pnpmText = await readText(path46.join(root, PNPM_WORKSPACE_FILE));
|
|
8680
8842
|
const fromPnpm = pnpmText == null ? [] : workspaceGlobsFromPnpmYaml(pnpmText);
|
|
8681
8843
|
return [.../* @__PURE__ */ new Set([...fromPkg, ...fromPnpm])];
|
|
8682
8844
|
}
|
|
@@ -8697,7 +8859,7 @@ async function collectWorkspaceMemberPackages(root, globs) {
|
|
|
8697
8859
|
} catch {
|
|
8698
8860
|
return [];
|
|
8699
8861
|
}
|
|
8700
|
-
const rootManifest =
|
|
8862
|
+
const rootManifest = path46.resolve(root, "package.json");
|
|
8701
8863
|
const contained = [
|
|
8702
8864
|
...new Set(
|
|
8703
8865
|
matches.map((rel) => resolveContainedPath(root, rel)).filter((abs) => abs != null && abs !== rootManifest)
|
|
@@ -8706,8 +8868,8 @@ async function collectWorkspaceMemberPackages(root, globs) {
|
|
|
8706
8868
|
const read = await mapWithConcurrency(contained, async (abs) => {
|
|
8707
8869
|
const pkg = await readJson(abs);
|
|
8708
8870
|
if (!isRecord(pkg)) return null;
|
|
8709
|
-
const rel =
|
|
8710
|
-
return { dir: rel.split(
|
|
8871
|
+
const rel = path46.relative(root, path46.dirname(abs));
|
|
8872
|
+
return { dir: rel.split(path46.sep).join("/"), pkg };
|
|
8711
8873
|
});
|
|
8712
8874
|
return read.filter((entry) => entry != null);
|
|
8713
8875
|
}
|
|
@@ -8827,8 +8989,8 @@ var SAFE_FILES = [
|
|
|
8827
8989
|
"Gemfile"
|
|
8828
8990
|
];
|
|
8829
8991
|
async function scanProject(root, options = {}) {
|
|
8830
|
-
const pkg = await readJson(
|
|
8831
|
-
const composer = await readJson(
|
|
8992
|
+
const pkg = await readJson(path47.join(root, "package.json"));
|
|
8993
|
+
const composer = await readJson(path47.join(root, "composer.json"));
|
|
8832
8994
|
const files = await listFiles(root, SAFE_FILES);
|
|
8833
8995
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
8834
8996
|
const workspaceGlobs = await readWorkspaceGlobs(root, pkg ?? void 0);
|
|
@@ -8865,7 +9027,7 @@ async function scanProject(root, options = {}) {
|
|
|
8865
9027
|
crossRepoHints.push("Monorepo orchestration detected");
|
|
8866
9028
|
const unsupportedSignals = collectUnsupportedSignals(safeFiles);
|
|
8867
9029
|
const detectionStatus = computeDetectionStatus(roles, stacks, unsupportedSignals);
|
|
8868
|
-
const repoNameFallback = pkg?.name ? String(pkg.name) :
|
|
9030
|
+
const repoNameFallback = pkg?.name ? String(pkg.name) : path47.basename((await resolveRoots(root)).mainRoot);
|
|
8869
9031
|
const context = {
|
|
8870
9032
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8871
9033
|
repoName: repoNameFallback,
|
|
@@ -8889,15 +9051,15 @@ async function scanProject(root, options = {}) {
|
|
|
8889
9051
|
// src/scanner/read-context.ts
|
|
8890
9052
|
async function isCacheFresh(root, cachePath) {
|
|
8891
9053
|
const markerPaths = [
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
9054
|
+
path48.join(root, "package.json"),
|
|
9055
|
+
path48.join(root, "composer.json"),
|
|
9056
|
+
path48.join(root, WORKSPACE_FILE),
|
|
9057
|
+
path48.join(root, REPOS_MANIFEST_FILE)
|
|
8896
9058
|
];
|
|
8897
|
-
const cacheStat = await
|
|
9059
|
+
const cacheStat = await fs40.stat(cachePath);
|
|
8898
9060
|
for (const markerPath of markerPaths) {
|
|
8899
|
-
if (!await
|
|
8900
|
-
const markerStat = await
|
|
9061
|
+
if (!await fs40.pathExists(markerPath)) continue;
|
|
9062
|
+
const markerStat = await fs40.stat(markerPath);
|
|
8901
9063
|
if (markerStat.mtimeMs > cacheStat.mtimeMs) return false;
|
|
8902
9064
|
}
|
|
8903
9065
|
return true;
|
|
@@ -9030,7 +9192,7 @@ async function runDoctor(options) {
|
|
|
9030
9192
|
} else {
|
|
9031
9193
|
ok(`- HOOKS OK: ${hooks.message}`);
|
|
9032
9194
|
}
|
|
9033
|
-
const rootClaudeMdPath =
|
|
9195
|
+
const rootClaudeMdPath = path49.join(root, "CLAUDE.md");
|
|
9034
9196
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
9035
9197
|
if (!rootClaudeMdContent) {
|
|
9036
9198
|
flag(
|
|
@@ -9073,10 +9235,10 @@ async function runDoctor(options) {
|
|
|
9073
9235
|
ok("- CLAUDE.md: import block present");
|
|
9074
9236
|
}
|
|
9075
9237
|
for (const target of actualTargets) {
|
|
9076
|
-
const resolved =
|
|
9077
|
-
const relFromRoot =
|
|
9078
|
-
const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${
|
|
9079
|
-
if (escapesRoot || !await
|
|
9238
|
+
const resolved = path49.resolve(root, target);
|
|
9239
|
+
const relFromRoot = path49.relative(root, resolved);
|
|
9240
|
+
const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${path49.sep}`) || path49.isAbsolute(relFromRoot);
|
|
9241
|
+
if (escapesRoot || !await fs41.pathExists(resolved)) {
|
|
9080
9242
|
flag(
|
|
9081
9243
|
`- CLAUDE.md import: @${target} does not resolve (run \`haus apply --write\`)`,
|
|
9082
9244
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -9094,7 +9256,7 @@ async function runDoctor(options) {
|
|
|
9094
9256
|
);
|
|
9095
9257
|
}
|
|
9096
9258
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
9097
|
-
const workflowExists = await
|
|
9259
|
+
const workflowExists = await fs41.pathExists(workflowPath);
|
|
9098
9260
|
if (!workflowExists) {
|
|
9099
9261
|
flag(
|
|
9100
9262
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -9109,15 +9271,15 @@ async function runDoctor(options) {
|
|
|
9109
9271
|
ok("- .haus-workflow/WORKFLOW.md: OK (user-owned)");
|
|
9110
9272
|
} else {
|
|
9111
9273
|
const bodyContent = workflowContent.slice(firstLine.length + 1);
|
|
9112
|
-
const cachePath =
|
|
9113
|
-
const bundledPath =
|
|
9274
|
+
const cachePath = path49.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
9275
|
+
const bundledPath = path49.join(
|
|
9114
9276
|
packageRoot(),
|
|
9115
9277
|
"library",
|
|
9116
9278
|
"global",
|
|
9117
9279
|
"templates",
|
|
9118
9280
|
"agentic-workflow-standard.md"
|
|
9119
9281
|
);
|
|
9120
|
-
const templatePath = await
|
|
9282
|
+
const templatePath = await fs41.pathExists(cachePath) ? cachePath : bundledPath;
|
|
9121
9283
|
const templateContent = await readText(templatePath);
|
|
9122
9284
|
const verdict = checkManagedTamper(bodyContent, managedHeader.hash, templateContent ?? null);
|
|
9123
9285
|
if (verdict.status === "tampered" || verdict.status === "legacy-diverged") {
|
|
@@ -9145,7 +9307,7 @@ async function runDoctor(options) {
|
|
|
9145
9307
|
}
|
|
9146
9308
|
}
|
|
9147
9309
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
9148
|
-
const workflowConfigExists = await
|
|
9310
|
+
const workflowConfigExists = await fs41.pathExists(workflowConfigPath);
|
|
9149
9311
|
if (!workflowConfigExists) {
|
|
9150
9312
|
flag(
|
|
9151
9313
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -9153,7 +9315,7 @@ async function runDoctor(options) {
|
|
|
9153
9315
|
"haus apply --write"
|
|
9154
9316
|
);
|
|
9155
9317
|
} else {
|
|
9156
|
-
const cfg = await
|
|
9318
|
+
const cfg = await fs41.readFile(workflowConfigPath, "utf8");
|
|
9157
9319
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
9158
9320
|
if (unfilled > 0) {
|
|
9159
9321
|
suggest(
|
|
@@ -9165,11 +9327,11 @@ async function runDoctor(options) {
|
|
|
9165
9327
|
ok("- .haus-workflow/workflow-config.md: OK (project-owned)");
|
|
9166
9328
|
}
|
|
9167
9329
|
}
|
|
9168
|
-
const docsSkillInstalled = await
|
|
9169
|
-
|
|
9330
|
+
const docsSkillInstalled = await fs41.pathExists(
|
|
9331
|
+
path49.join(root, ".claude", "skills", "writing-documentation", "SKILL.md")
|
|
9170
9332
|
);
|
|
9171
9333
|
if (docsSkillInstalled) {
|
|
9172
|
-
if (await
|
|
9334
|
+
if (await fs41.pathExists(path49.join(root, "docs", "SUMMARY.md"))) {
|
|
9173
9335
|
ok("- PROJECT DOCS: OK (docs/SUMMARY.md present)");
|
|
9174
9336
|
} else {
|
|
9175
9337
|
suggest(
|
|
@@ -9180,7 +9342,7 @@ async function runDoctor(options) {
|
|
|
9180
9342
|
}
|
|
9181
9343
|
}
|
|
9182
9344
|
if (workflowExists) {
|
|
9183
|
-
const prettierIgnore = await readText(
|
|
9345
|
+
const prettierIgnore = await readText(path49.join(root, ".prettierignore")) ?? "";
|
|
9184
9346
|
const lines = prettierIgnore.split("\n");
|
|
9185
9347
|
const missing = IGNORED_PATHS.filter((p) => !prettierIgnoreProtects(lines, p));
|
|
9186
9348
|
if (missing.length > 0) {
|
|
@@ -9328,7 +9490,7 @@ async function runDoctor(options) {
|
|
|
9328
9490
|
const catalogAgents = catalogItems === null ? [] : catalogItems.flatMap((item) => {
|
|
9329
9491
|
if (item.type !== "agent") return [];
|
|
9330
9492
|
const target = installTargetFor(item);
|
|
9331
|
-
return target.ok ? [{ id: item.id, name:
|
|
9493
|
+
return target.ok ? [{ id: item.id, name: path49.basename(target.relPath, ".md") }] : [];
|
|
9332
9494
|
});
|
|
9333
9495
|
const STACK_GATE_SKIP_CODES = /* @__PURE__ */ new Set([
|
|
9334
9496
|
"requires-any-unsatisfied",
|
|
@@ -9556,7 +9718,7 @@ async function runDoctor(options) {
|
|
|
9556
9718
|
let onDisk = false;
|
|
9557
9719
|
for (const rel of row.paths ?? []) {
|
|
9558
9720
|
const abs = resolveContainedPath(root, rel);
|
|
9559
|
-
if (abs !== null && await
|
|
9721
|
+
if (abs !== null && await fs41.pathExists(abs)) {
|
|
9560
9722
|
onDisk = true;
|
|
9561
9723
|
break;
|
|
9562
9724
|
}
|
|
@@ -9572,7 +9734,7 @@ async function runDoctor(options) {
|
|
|
9572
9734
|
);
|
|
9573
9735
|
}
|
|
9574
9736
|
}
|
|
9575
|
-
const pkgJson = await readJson(
|
|
9737
|
+
const pkgJson = await readJson(path49.join(packageRoot(), "package.json"));
|
|
9576
9738
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
9577
9739
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
9578
9740
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -9623,16 +9785,16 @@ async function runDoctor(options) {
|
|
|
9623
9785
|
}
|
|
9624
9786
|
|
|
9625
9787
|
// src/commands/update.ts
|
|
9626
|
-
import
|
|
9788
|
+
import path53 from "node:path";
|
|
9627
9789
|
init_github_rate_limit();
|
|
9628
9790
|
init_private_catalog_access();
|
|
9629
9791
|
|
|
9630
9792
|
// src/claude/refresh-project.ts
|
|
9631
|
-
import
|
|
9793
|
+
import fs42 from "fs-extra";
|
|
9632
9794
|
init_fs();
|
|
9633
9795
|
init_paths();
|
|
9634
9796
|
async function isHausProject(root) {
|
|
9635
|
-
if (await
|
|
9797
|
+
if (await fs42.pathExists(hausPath(root, "recommendation.json"))) return true;
|
|
9636
9798
|
const settings = await readJsonDetailed(claudePath(root, "settings.json"));
|
|
9637
9799
|
if (settings.status === "ok" && settings.value._haus != null) return true;
|
|
9638
9800
|
const lock = await readJsonDetailed(hausPath(root, "haus.lock.json"));
|
|
@@ -9650,8 +9812,8 @@ async function refreshProjectApply(root) {
|
|
|
9650
9812
|
|
|
9651
9813
|
// src/install/apply.ts
|
|
9652
9814
|
import crypto3 from "node:crypto";
|
|
9653
|
-
import
|
|
9654
|
-
import
|
|
9815
|
+
import path51 from "node:path";
|
|
9816
|
+
import fs44 from "fs-extra";
|
|
9655
9817
|
init_fs();
|
|
9656
9818
|
init_logger();
|
|
9657
9819
|
init_paths();
|
|
@@ -9760,8 +9922,8 @@ ${content2}`;
|
|
|
9760
9922
|
|
|
9761
9923
|
// src/install/internal-channel.ts
|
|
9762
9924
|
init_fs();
|
|
9763
|
-
import
|
|
9764
|
-
import
|
|
9925
|
+
import path50 from "node:path";
|
|
9926
|
+
import fs43 from "fs-extra";
|
|
9765
9927
|
var INSTALL_CONFIG_SCHEMA = "haus-install-config/1";
|
|
9766
9928
|
var INTERNAL_NAME_PREFIXES = ["request", "slack-cowork"];
|
|
9767
9929
|
function internalPrefixMatch(name) {
|
|
@@ -9778,7 +9940,7 @@ function isInternalAsset(stableId) {
|
|
|
9778
9940
|
return internalPrefixMatch(name);
|
|
9779
9941
|
}
|
|
9780
9942
|
function installConfigPath() {
|
|
9781
|
-
return
|
|
9943
|
+
return path50.join(globalClaudeDir(), "haus", "install-config.json");
|
|
9782
9944
|
}
|
|
9783
9945
|
async function readInstallConfig() {
|
|
9784
9946
|
const raw = await readJson(installConfigPath());
|
|
@@ -9795,7 +9957,7 @@ async function writeInstallConfig(config) {
|
|
|
9795
9957
|
}
|
|
9796
9958
|
async function removeInstallConfig() {
|
|
9797
9959
|
const p = installConfigPath();
|
|
9798
|
-
if (
|
|
9960
|
+
if (fs43.pathExistsSync(p)) await fs43.remove(p);
|
|
9799
9961
|
}
|
|
9800
9962
|
function envOverride() {
|
|
9801
9963
|
const raw = process.env.HAUS_INTERNAL_CHANNEL?.trim().toLowerCase();
|
|
@@ -9825,12 +9987,12 @@ async function pruneEmptyDirsUpward(dirs, stopAt) {
|
|
|
9825
9987
|
let dir = start;
|
|
9826
9988
|
while (dir.startsWith(stopAt) && dir !== stopAt) {
|
|
9827
9989
|
try {
|
|
9828
|
-
if ((await
|
|
9829
|
-
await
|
|
9990
|
+
if ((await fs44.readdir(dir)).length > 0) break;
|
|
9991
|
+
await fs44.remove(dir);
|
|
9830
9992
|
} catch {
|
|
9831
9993
|
break;
|
|
9832
9994
|
}
|
|
9833
|
-
dir =
|
|
9995
|
+
dir = path51.dirname(dir);
|
|
9834
9996
|
}
|
|
9835
9997
|
}
|
|
9836
9998
|
}
|
|
@@ -9839,65 +10001,65 @@ function hashContent(content2) {
|
|
|
9839
10001
|
}
|
|
9840
10002
|
function destFileMissing(destPath) {
|
|
9841
10003
|
try {
|
|
9842
|
-
return !
|
|
10004
|
+
return !fs44.statSync(destPath).isFile();
|
|
9843
10005
|
} catch {
|
|
9844
10006
|
return true;
|
|
9845
10007
|
}
|
|
9846
10008
|
}
|
|
9847
10009
|
function sourceVersion() {
|
|
9848
10010
|
try {
|
|
9849
|
-
const pkgPath =
|
|
9850
|
-
const pkg = JSON.parse(
|
|
10011
|
+
const pkgPath = path51.join(packageRoot(), "package.json");
|
|
10012
|
+
const pkg = JSON.parse(fs44.readFileSync(pkgPath, "utf8"));
|
|
9851
10013
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
9852
10014
|
} catch {
|
|
9853
10015
|
return "haus@0.0.0";
|
|
9854
10016
|
}
|
|
9855
10017
|
}
|
|
9856
10018
|
function globalSrcDir() {
|
|
9857
|
-
return
|
|
10019
|
+
return path51.join(packageRoot(), "library", "global");
|
|
9858
10020
|
}
|
|
9859
10021
|
function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
9860
|
-
const skillDir =
|
|
10022
|
+
const skillDir = path51.join(skillsDir, skillName);
|
|
9861
10023
|
const entries = [];
|
|
9862
|
-
for (const rel of
|
|
10024
|
+
for (const rel of fs44.readdirSync(skillDir, { recursive: true })) {
|
|
9863
10025
|
if (rel === "SKILL.md") continue;
|
|
9864
|
-
const abs =
|
|
9865
|
-
if (!
|
|
9866
|
-
const posixRel = rel.split(
|
|
10026
|
+
const abs = path51.join(skillDir, rel);
|
|
10027
|
+
if (!fs44.statSync(abs).isFile()) continue;
|
|
10028
|
+
const posixRel = rel.split(path51.sep).join("/");
|
|
9867
10029
|
entries.push({
|
|
9868
10030
|
stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
|
|
9869
|
-
srcRelPath:
|
|
9870
|
-
destPath:
|
|
10031
|
+
srcRelPath: path51.join("library", "global", "skills", skillName, rel),
|
|
10032
|
+
destPath: path51.join(claudeDir, "skills", skillName, rel)
|
|
9871
10033
|
});
|
|
9872
10034
|
}
|
|
9873
10035
|
return entries;
|
|
9874
10036
|
}
|
|
9875
10037
|
function collectFlatChannel(srcDir, claudeDir, dirName, idPrefix) {
|
|
9876
10038
|
const entries = [];
|
|
9877
|
-
const channelDir =
|
|
9878
|
-
if (!
|
|
9879
|
-
for (const fileName of
|
|
10039
|
+
const channelDir = path51.join(srcDir, dirName);
|
|
10040
|
+
if (!fs44.pathExistsSync(channelDir)) return entries;
|
|
10041
|
+
for (const fileName of fs44.readdirSync(channelDir)) {
|
|
9880
10042
|
if (!fileName.endsWith(".md")) continue;
|
|
9881
10043
|
const name = fileName.slice(0, -".md".length);
|
|
9882
10044
|
entries.push({
|
|
9883
10045
|
stableId: `${idPrefix}.${name}`,
|
|
9884
|
-
srcRelPath:
|
|
9885
|
-
destPath:
|
|
10046
|
+
srcRelPath: path51.join("library", "global", dirName, fileName),
|
|
10047
|
+
destPath: path51.join(claudeDir, dirName, fileName)
|
|
9886
10048
|
});
|
|
9887
10049
|
}
|
|
9888
10050
|
return entries;
|
|
9889
10051
|
}
|
|
9890
10052
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
9891
10053
|
const entries = [];
|
|
9892
|
-
const skillsDir =
|
|
9893
|
-
if (
|
|
9894
|
-
for (const skillName of
|
|
9895
|
-
const skillFile =
|
|
9896
|
-
if (
|
|
10054
|
+
const skillsDir = path51.join(srcDir, "skills");
|
|
10055
|
+
if (fs44.pathExistsSync(skillsDir)) {
|
|
10056
|
+
for (const skillName of fs44.readdirSync(skillsDir)) {
|
|
10057
|
+
const skillFile = path51.join(skillsDir, skillName, "SKILL.md");
|
|
10058
|
+
if (fs44.pathExistsSync(skillFile)) {
|
|
9897
10059
|
entries.push({
|
|
9898
10060
|
stableId: `skill.${skillName}`,
|
|
9899
|
-
srcRelPath:
|
|
9900
|
-
destPath:
|
|
10061
|
+
srcRelPath: path51.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
10062
|
+
destPath: path51.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
9901
10063
|
});
|
|
9902
10064
|
entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
|
|
9903
10065
|
}
|
|
@@ -9937,7 +10099,7 @@ async function applyInstall(options = {}) {
|
|
|
9937
10099
|
};
|
|
9938
10100
|
const manifestFiles = [];
|
|
9939
10101
|
for (const entry of sourceFiles) {
|
|
9940
|
-
const srcPath =
|
|
10102
|
+
const srcPath = path51.join(packageRoot(), entry.srcRelPath);
|
|
9941
10103
|
const rawContent = await readText(srcPath);
|
|
9942
10104
|
if (rawContent === void 0) {
|
|
9943
10105
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -9966,7 +10128,7 @@ async function applyInstall(options = {}) {
|
|
|
9966
10128
|
}
|
|
9967
10129
|
continue;
|
|
9968
10130
|
}
|
|
9969
|
-
const destExists =
|
|
10131
|
+
const destExists = fs44.pathExistsSync(entry.destPath);
|
|
9970
10132
|
if (destExists) {
|
|
9971
10133
|
const currentContent = await readText(entry.destPath);
|
|
9972
10134
|
if (currentContent !== void 0) {
|
|
@@ -10006,7 +10168,7 @@ async function applyInstall(options = {}) {
|
|
|
10006
10168
|
}
|
|
10007
10169
|
let mergedSettings;
|
|
10008
10170
|
if (!check) {
|
|
10009
|
-
const fragmentPath =
|
|
10171
|
+
const fragmentPath = path51.join(srcDir, "settings-fragments", "hooks.json");
|
|
10010
10172
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
10011
10173
|
const settings = await readSettings();
|
|
10012
10174
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -10026,14 +10188,14 @@ async function applyInstall(options = {}) {
|
|
|
10026
10188
|
manifestFiles.push(entry);
|
|
10027
10189
|
continue;
|
|
10028
10190
|
}
|
|
10029
|
-
if (!
|
|
10191
|
+
if (!fs44.pathExistsSync(entry.destPath)) continue;
|
|
10030
10192
|
const content2 = await readText(entry.destPath);
|
|
10031
10193
|
if (!content2) continue;
|
|
10032
10194
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
10033
10195
|
const currentHash = hashContent(content2);
|
|
10034
10196
|
if (hasHeader && currentHash === entry.hash) {
|
|
10035
|
-
if (!dryRun) await
|
|
10036
|
-
emptiedDirs.add(
|
|
10197
|
+
if (!dryRun) await fs44.remove(entry.destPath);
|
|
10198
|
+
emptiedDirs.add(path51.dirname(entry.destPath));
|
|
10037
10199
|
result.deleted.push(entry.destPath);
|
|
10038
10200
|
} else {
|
|
10039
10201
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -10127,7 +10289,7 @@ function summarizeLockDiff(before, after) {
|
|
|
10127
10289
|
// src/update/lockfile.ts
|
|
10128
10290
|
import { constants as fsConstants } from "node:fs";
|
|
10129
10291
|
import { copyFile, mkdir, readFile as readFile2 } from "node:fs/promises";
|
|
10130
|
-
import
|
|
10292
|
+
import path52 from "node:path";
|
|
10131
10293
|
init_fs();
|
|
10132
10294
|
init_paths();
|
|
10133
10295
|
async function readLockSummary(root) {
|
|
@@ -10167,7 +10329,7 @@ async function backupLockfile(root) {
|
|
|
10167
10329
|
await mkdir(backupDir, { recursive: true });
|
|
10168
10330
|
let stamp = Date.now();
|
|
10169
10331
|
for (; ; ) {
|
|
10170
|
-
const dest =
|
|
10332
|
+
const dest = path52.join(backupDir, lockBackupFileName(stamp));
|
|
10171
10333
|
try {
|
|
10172
10334
|
await copyFile(lockPath, dest, fsConstants.COPYFILE_EXCL);
|
|
10173
10335
|
return;
|
|
@@ -10218,7 +10380,7 @@ function diffLock(before, after) {
|
|
|
10218
10380
|
}
|
|
10219
10381
|
async function hasLocalOverrides(root) {
|
|
10220
10382
|
try {
|
|
10221
|
-
await readFile2(
|
|
10383
|
+
await readFile2(path52.join(root, ".claude", "settings.json"), "utf8");
|
|
10222
10384
|
return true;
|
|
10223
10385
|
} catch {
|
|
10224
10386
|
return false;
|
|
@@ -10263,7 +10425,7 @@ async function runUpdate(options) {
|
|
|
10263
10425
|
return;
|
|
10264
10426
|
}
|
|
10265
10427
|
if (options.check) {
|
|
10266
|
-
const pkgJson2 = await readJson(
|
|
10428
|
+
const pkgJson2 = await readJson(path53.join(packageRoot(), "package.json"));
|
|
10267
10429
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
10268
10430
|
const [
|
|
10269
10431
|
status,
|
|
@@ -10315,7 +10477,7 @@ async function runUpdate(options) {
|
|
|
10315
10477
|
}
|
|
10316
10478
|
return;
|
|
10317
10479
|
}
|
|
10318
|
-
const pkgJson = await readJson(
|
|
10480
|
+
const pkgJson = await readJson(path53.join(packageRoot(), "package.json"));
|
|
10319
10481
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
10320
10482
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
10321
10483
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -10386,7 +10548,7 @@ async function runFromHookCheck(root) {
|
|
|
10386
10548
|
try {
|
|
10387
10549
|
const summary = await readLockSummary(root);
|
|
10388
10550
|
if (summary.count === 0) return;
|
|
10389
|
-
const pkgJson = await readJson(
|
|
10551
|
+
const pkgJson = await readJson(path53.join(packageRoot(), "package.json"));
|
|
10390
10552
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
10391
10553
|
const pin = readProjectCatalogPin(root);
|
|
10392
10554
|
const honoursPin = pin.kind === "pinned" && summary.catalogRef === pin.ref;
|
|
@@ -10581,7 +10743,7 @@ init_exec();
|
|
|
10581
10743
|
init_logger();
|
|
10582
10744
|
import { existsSync as existsSync2 } from "node:fs";
|
|
10583
10745
|
import { readdir } from "node:fs/promises";
|
|
10584
|
-
import
|
|
10746
|
+
import path54 from "node:path";
|
|
10585
10747
|
var GIT_LOCATION_VARS = [
|
|
10586
10748
|
"GIT_DIR",
|
|
10587
10749
|
"GIT_WORK_TREE",
|
|
@@ -10641,11 +10803,11 @@ async function runClone(url, opts = {}) {
|
|
|
10641
10803
|
process.exitCode = 1;
|
|
10642
10804
|
return;
|
|
10643
10805
|
}
|
|
10644
|
-
const target =
|
|
10806
|
+
const target = path54.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
10645
10807
|
if (existsSync2(target) && !await isEmptyDirectory(target)) {
|
|
10646
10808
|
const check = await checkExistingTarget(target, url);
|
|
10647
10809
|
if (check.kind === "matches") {
|
|
10648
|
-
log(`\u2022 ${
|
|
10810
|
+
log(`\u2022 ${path54.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
|
|
10649
10811
|
return;
|
|
10650
10812
|
}
|
|
10651
10813
|
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].`;
|
|
@@ -10694,8 +10856,8 @@ function isCatalogItemInstallable(item) {
|
|
|
10694
10856
|
|
|
10695
10857
|
// src/install/scaffold.ts
|
|
10696
10858
|
init_logger();
|
|
10697
|
-
import
|
|
10698
|
-
import
|
|
10859
|
+
import path55 from "node:path";
|
|
10860
|
+
import fs45 from "fs-extra";
|
|
10699
10861
|
async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
10700
10862
|
const scaffolded = [];
|
|
10701
10863
|
const skipped = [];
|
|
@@ -10708,27 +10870,27 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
10708
10870
|
skipped.push(name);
|
|
10709
10871
|
if (outcome === "skipped-exists") skippedExisting.push(name);
|
|
10710
10872
|
};
|
|
10711
|
-
const realRoot = await
|
|
10873
|
+
const realRoot = await fs45.realpath(catalogRoot).catch(() => null);
|
|
10712
10874
|
for (const item of items) {
|
|
10713
10875
|
if (item.type !== "config") continue;
|
|
10714
|
-
const sourcePath =
|
|
10715
|
-
const rel =
|
|
10716
|
-
const escapes = rel === "" ||
|
|
10876
|
+
const sourcePath = path55.resolve(catalogRoot, item.path);
|
|
10877
|
+
const rel = path55.relative(catalogRoot, sourcePath);
|
|
10878
|
+
const escapes = rel === "" || path55.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
|
|
10717
10879
|
if (escapes) {
|
|
10718
10880
|
warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
|
|
10719
10881
|
continue;
|
|
10720
10882
|
}
|
|
10721
|
-
const realSource = await
|
|
10883
|
+
const realSource = await fs45.realpath(sourcePath).catch(() => null);
|
|
10722
10884
|
if (!realRoot || !realSource) {
|
|
10723
10885
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
10724
10886
|
continue;
|
|
10725
10887
|
}
|
|
10726
|
-
const realRel =
|
|
10727
|
-
if (realRel !== "" && (
|
|
10888
|
+
const realRel = path55.relative(realRoot, realSource);
|
|
10889
|
+
if (realRel !== "" && (path55.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
|
|
10728
10890
|
warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
|
|
10729
10891
|
continue;
|
|
10730
10892
|
}
|
|
10731
|
-
const stat = await
|
|
10893
|
+
const stat = await fs45.lstat(sourcePath).catch(() => null);
|
|
10732
10894
|
if (!stat) {
|
|
10733
10895
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
10734
10896
|
continue;
|
|
@@ -10738,14 +10900,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
10738
10900
|
continue;
|
|
10739
10901
|
}
|
|
10740
10902
|
if (stat.isFile()) {
|
|
10741
|
-
const filename =
|
|
10742
|
-
const dest =
|
|
10903
|
+
const filename = path55.basename(sourcePath);
|
|
10904
|
+
const dest = path55.join(projectRoot, filename);
|
|
10743
10905
|
record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
|
|
10744
10906
|
} else if (stat.isDirectory()) {
|
|
10745
|
-
const entries = await
|
|
10907
|
+
const entries = await fs45.readdir(sourcePath);
|
|
10746
10908
|
for (const entry of entries) {
|
|
10747
|
-
const src =
|
|
10748
|
-
const dest =
|
|
10909
|
+
const src = path55.join(sourcePath, entry);
|
|
10910
|
+
const dest = path55.join(projectRoot, entry);
|
|
10749
10911
|
record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
|
|
10750
10912
|
}
|
|
10751
10913
|
}
|
|
@@ -10753,42 +10915,42 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
10753
10915
|
return { scaffolded, skipped, skippedExisting };
|
|
10754
10916
|
}
|
|
10755
10917
|
async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
10756
|
-
const destDisplay =
|
|
10757
|
-
const srcStat = await
|
|
10918
|
+
const destDisplay = path55.relative(projectRoot, dest) || path55.basename(dest);
|
|
10919
|
+
const srcStat = await fs45.lstat(src).catch(() => null);
|
|
10758
10920
|
if (!srcStat) {
|
|
10759
|
-
warn(`Skipping ${
|
|
10921
|
+
warn(`Skipping ${path55.basename(src)}: source not found`);
|
|
10760
10922
|
return "skipped-refused";
|
|
10761
10923
|
}
|
|
10762
10924
|
if (srcStat.isSymbolicLink()) {
|
|
10763
|
-
warn(`Skipping ${
|
|
10925
|
+
warn(`Skipping ${path55.basename(src)}: source is a symlink`);
|
|
10764
10926
|
return "skipped-refused";
|
|
10765
10927
|
}
|
|
10766
|
-
const
|
|
10767
|
-
if (
|
|
10928
|
+
const exists2 = await fs45.pathExists(dest);
|
|
10929
|
+
if (exists2 && !opts.force) {
|
|
10768
10930
|
warn(`Skipping ${destDisplay}: already exists \u2014 re-run with --force to overwrite it`);
|
|
10769
10931
|
return "skipped-exists";
|
|
10770
10932
|
}
|
|
10771
10933
|
if (opts.dryRun) {
|
|
10772
|
-
log(`[dry-run] would ${
|
|
10934
|
+
log(`[dry-run] would ${exists2 ? "overwrite" : "create"} ${path55.basename(dest)} (${itemId})`);
|
|
10773
10935
|
return "scaffolded";
|
|
10774
10936
|
}
|
|
10775
|
-
if (
|
|
10776
|
-
await
|
|
10937
|
+
if (exists2 && opts.force) {
|
|
10938
|
+
await fs45.remove(dest);
|
|
10777
10939
|
}
|
|
10778
|
-
await
|
|
10779
|
-
await
|
|
10940
|
+
await fs45.ensureDir(path55.dirname(dest));
|
|
10941
|
+
await fs45.copy(src, dest, {
|
|
10780
10942
|
overwrite: true,
|
|
10781
10943
|
dereference: false,
|
|
10782
10944
|
// A vanished/unreadable entry mid-walk must skip that path, not crash the copy.
|
|
10783
10945
|
filter: (s) => {
|
|
10784
10946
|
try {
|
|
10785
|
-
return !
|
|
10947
|
+
return !fs45.lstatSync(s).isSymbolicLink();
|
|
10786
10948
|
} catch {
|
|
10787
10949
|
return false;
|
|
10788
10950
|
}
|
|
10789
10951
|
}
|
|
10790
10952
|
});
|
|
10791
|
-
if (!opts.quiet) log(`${
|
|
10953
|
+
if (!opts.quiet) log(`${exists2 ? "Overwrote" : "Created"} ${path55.basename(dest)} (${itemId})`);
|
|
10792
10954
|
return "scaffolded";
|
|
10793
10955
|
}
|
|
10794
10956
|
|
|
@@ -10920,7 +11082,7 @@ async function runDecisionsGuard(options = {}) {
|
|
|
10920
11082
|
}
|
|
10921
11083
|
|
|
10922
11084
|
// src/commands/decisions.ts
|
|
10923
|
-
import
|
|
11085
|
+
import path56 from "node:path";
|
|
10924
11086
|
|
|
10925
11087
|
// src/decisions/next-number.ts
|
|
10926
11088
|
async function nextDecisionNumber(root) {
|
|
@@ -11031,7 +11193,7 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
11031
11193
|
return;
|
|
11032
11194
|
}
|
|
11033
11195
|
if (sub === "validate") {
|
|
11034
|
-
const target = arg ?
|
|
11196
|
+
const target = arg ? path56.resolve(root, arg) : void 0;
|
|
11035
11197
|
if (!target) {
|
|
11036
11198
|
error("usage: haus decisions validate <path-to-decision.md>");
|
|
11037
11199
|
process.exitCode = 1;
|
|
@@ -11500,27 +11662,27 @@ function guardBash(command) {
|
|
|
11500
11662
|
}
|
|
11501
11663
|
|
|
11502
11664
|
// src/security/guard-file-access.ts
|
|
11503
|
-
import
|
|
11504
|
-
import
|
|
11665
|
+
import fs46 from "node:fs";
|
|
11666
|
+
import path57 from "node:path";
|
|
11505
11667
|
function matchesDenyTier(candidate) {
|
|
11506
11668
|
const normalized = normalizePathForMatch(candidate);
|
|
11507
11669
|
return DENY_PATH_REGEXES.some((re) => re.test(normalized));
|
|
11508
11670
|
}
|
|
11509
11671
|
function resolveOnDisk(candidate) {
|
|
11510
11672
|
try {
|
|
11511
|
-
const resolved =
|
|
11512
|
-
return resolved ===
|
|
11673
|
+
const resolved = fs46.realpathSync(candidate);
|
|
11674
|
+
return resolved === path57.resolve(candidate) ? {} : { resolved };
|
|
11513
11675
|
} catch {
|
|
11514
11676
|
try {
|
|
11515
|
-
if (
|
|
11677
|
+
if (fs46.lstatSync(candidate).isSymbolicLink()) return { unverifiable: true };
|
|
11516
11678
|
} catch {
|
|
11517
11679
|
}
|
|
11518
|
-
const parent =
|
|
11680
|
+
const parent = path57.dirname(candidate);
|
|
11519
11681
|
if (parent === candidate) return {};
|
|
11520
11682
|
const viaParent = resolveOnDisk(parent);
|
|
11521
11683
|
if (viaParent.unverifiable) return { unverifiable: true };
|
|
11522
11684
|
if (viaParent.resolved)
|
|
11523
|
-
return { resolved:
|
|
11685
|
+
return { resolved: path57.join(viaParent.resolved, path57.basename(candidate)) };
|
|
11524
11686
|
return {};
|
|
11525
11687
|
}
|
|
11526
11688
|
}
|
|
@@ -12070,7 +12232,7 @@ function buildOptInEligible(items, skipped) {
|
|
|
12070
12232
|
}
|
|
12071
12233
|
|
|
12072
12234
|
// src/commands/workspace/discover.ts
|
|
12073
|
-
import
|
|
12235
|
+
import path58 from "node:path";
|
|
12074
12236
|
import fg6 from "fast-glob";
|
|
12075
12237
|
import YAML4 from "yaml";
|
|
12076
12238
|
init_fs();
|
|
@@ -12113,10 +12275,10 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
12113
12275
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
12114
12276
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
12115
12277
|
for (const match of gitMatches) {
|
|
12116
|
-
gitDirs.add(
|
|
12278
|
+
gitDirs.add(path58.posix.dirname(match));
|
|
12117
12279
|
}
|
|
12118
12280
|
for (const match of manifestMatches) {
|
|
12119
|
-
manifestDirs.add(
|
|
12281
|
+
manifestDirs.add(path58.posix.dirname(match));
|
|
12120
12282
|
}
|
|
12121
12283
|
const repoRoots = [...gitDirs];
|
|
12122
12284
|
const manifestSorted = [...manifestDirs].sort(
|
|
@@ -12133,9 +12295,9 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
12133
12295
|
async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
12134
12296
|
const repoRoots = (await findRepoRoots(workspaceRoot, maxDepth)).filter((dir) => dir !== ".");
|
|
12135
12297
|
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
12136
|
-
const absDir =
|
|
12137
|
-
const pkg = await readJson(
|
|
12138
|
-
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name :
|
|
12298
|
+
const absDir = path58.resolve(workspaceRoot, relDir);
|
|
12299
|
+
const pkg = await readJson(path58.join(absDir, "package.json"));
|
|
12300
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path58.basename(absDir);
|
|
12139
12301
|
let role = "auto";
|
|
12140
12302
|
try {
|
|
12141
12303
|
const scan = await scanProject(absDir);
|
|
@@ -12178,7 +12340,7 @@ function renderWorkspaceYaml(config) {
|
|
|
12178
12340
|
});
|
|
12179
12341
|
}
|
|
12180
12342
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
12181
|
-
const yamlPath =
|
|
12343
|
+
const yamlPath = path58.join(workspaceRoot, "haus.workspace.yaml");
|
|
12182
12344
|
const existingText = await readText(yamlPath);
|
|
12183
12345
|
const existing = parseWorkspaceConfig(existingText);
|
|
12184
12346
|
if (existingText && !existing) {
|
|
@@ -12611,11 +12773,11 @@ async function runInstall(options, deps = {}) {
|
|
|
12611
12773
|
}
|
|
12612
12774
|
|
|
12613
12775
|
// src/commands/new.ts
|
|
12614
|
-
import
|
|
12776
|
+
import path60 from "node:path";
|
|
12615
12777
|
|
|
12616
12778
|
// src/new-project/create-project.ts
|
|
12617
|
-
import
|
|
12618
|
-
import
|
|
12779
|
+
import path59 from "node:path";
|
|
12780
|
+
import fs47 from "fs-extra";
|
|
12619
12781
|
init_exec();
|
|
12620
12782
|
init_logger();
|
|
12621
12783
|
|
|
@@ -12800,10 +12962,10 @@ var defaultDeps = {
|
|
|
12800
12962
|
};
|
|
12801
12963
|
var NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
12802
12964
|
async function recordPackageManager(projectPath, pm, deps) {
|
|
12803
|
-
const pkgPath =
|
|
12965
|
+
const pkgPath = path59.join(projectPath, "package.json");
|
|
12804
12966
|
let pkg;
|
|
12805
12967
|
try {
|
|
12806
|
-
pkg = await
|
|
12968
|
+
pkg = await fs47.readJson(pkgPath);
|
|
12807
12969
|
} catch {
|
|
12808
12970
|
return {};
|
|
12809
12971
|
}
|
|
@@ -12818,7 +12980,7 @@ async function recordPackageManager(projectPath, pm, deps) {
|
|
|
12818
12980
|
note: `installed ${pm} is ${version}, which haus does not support \u2014 the package manager was not recorded, so the scanner will report it as unknown until you upgrade`
|
|
12819
12981
|
};
|
|
12820
12982
|
}
|
|
12821
|
-
await
|
|
12983
|
+
await fs47.writeJson(pkgPath, { ...pkg, packageManager: field }, { spaces: 2 });
|
|
12822
12984
|
return {};
|
|
12823
12985
|
}
|
|
12824
12986
|
var CONFIG_TOOLING = {
|
|
@@ -12831,12 +12993,12 @@ var CONFIG_TOOLING = {
|
|
|
12831
12993
|
}
|
|
12832
12994
|
};
|
|
12833
12995
|
async function ensureConfigTooling(projectPath, scaffolded) {
|
|
12834
|
-
const needed = scaffolded.map((file) => CONFIG_TOOLING[
|
|
12996
|
+
const needed = scaffolded.map((file) => CONFIG_TOOLING[path59.basename(file)]).filter((entry) => entry !== void 0);
|
|
12835
12997
|
if (needed.length === 0) return [];
|
|
12836
|
-
const pkgPath =
|
|
12998
|
+
const pkgPath = path59.join(projectPath, "package.json");
|
|
12837
12999
|
let pkg;
|
|
12838
13000
|
try {
|
|
12839
|
-
pkg = await
|
|
13001
|
+
pkg = await fs47.readJson(pkgPath);
|
|
12840
13002
|
} catch {
|
|
12841
13003
|
return [];
|
|
12842
13004
|
}
|
|
@@ -12860,12 +13022,12 @@ async function ensureConfigTooling(projectPath, scaffolded) {
|
|
|
12860
13022
|
}
|
|
12861
13023
|
}
|
|
12862
13024
|
if (!changed) return [];
|
|
12863
|
-
await
|
|
13025
|
+
await fs47.writeJson(pkgPath, { ...pkg, devDependencies, scripts }, { spaces: 2 });
|
|
12864
13026
|
return added;
|
|
12865
13027
|
}
|
|
12866
13028
|
async function createProject(opts, deps = defaultDeps) {
|
|
12867
13029
|
const steps = [];
|
|
12868
|
-
const projectPath =
|
|
13030
|
+
const projectPath = path59.resolve(opts.parentDir, opts.name);
|
|
12869
13031
|
const installHint = `cd ${opts.name} && ${opts.pm}${opts.pm === "yarn" ? "" : " install"}`;
|
|
12870
13032
|
const fail = (step, detail) => {
|
|
12871
13033
|
steps.push({ step, status: "failed", detail });
|
|
@@ -12873,20 +13035,20 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
12873
13035
|
};
|
|
12874
13036
|
const stack = getStack(opts.stackId);
|
|
12875
13037
|
if (!stack) return fail("preflight", `Unknown stack "${opts.stackId}". Run \`haus new --list\`.`);
|
|
12876
|
-
const parentStat = await
|
|
13038
|
+
const parentStat = await fs47.stat(path59.dirname(projectPath)).catch(() => null);
|
|
12877
13039
|
if (!parentStat?.isDirectory()) {
|
|
12878
13040
|
return fail(
|
|
12879
13041
|
"preflight",
|
|
12880
13042
|
`Parent directory ${opts.parentDir} is not an existing directory \u2014 pass an existing one to --dir.`
|
|
12881
13043
|
);
|
|
12882
13044
|
}
|
|
12883
|
-
if (!NAME_RE.test(opts.name) || opts.name.includes("/") || opts.name.includes("\\") ||
|
|
13045
|
+
if (!NAME_RE.test(opts.name) || opts.name.includes("/") || opts.name.includes("\\") || path59.dirname(projectPath) !== path59.resolve(opts.parentDir)) {
|
|
12884
13046
|
return fail(
|
|
12885
13047
|
"preflight",
|
|
12886
13048
|
`Invalid project name "${opts.name}" (lowercase letters, digits, . _ - only).`
|
|
12887
13049
|
);
|
|
12888
13050
|
}
|
|
12889
|
-
if (await
|
|
13051
|
+
if (await fs47.pathExists(projectPath)) {
|
|
12890
13052
|
return fail("preflight", `Target ${projectPath} already exists \u2014 refusing to write into it.`);
|
|
12891
13053
|
}
|
|
12892
13054
|
const scaffoldArgs = stack.scaffold.args.map(
|
|
@@ -12958,13 +13120,13 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
12958
13120
|
});
|
|
12959
13121
|
if (scaffold.exitCode !== 0) {
|
|
12960
13122
|
const why = scaffold.killed ? `${pinned} did not complete: ${scaffold.stderr.trim() || `timed out after ${SCAFFOLD_TIMEOUT_MS}ms`}. Scaffolders that stall this way are usually waiting on a prompt despite --yes \u2014 run it yourself to see: npx ${npxArgs.join(" ")}` : `${pinned} exited ${scaffold.exitCode}.`;
|
|
12961
|
-
const leftBehind = await
|
|
13123
|
+
const leftBehind = await fs47.pathExists(projectPath) ? ` The directory was left for inspection \u2014 review it and remove it manually if unwanted (haus never auto-deletes).` : "";
|
|
12962
13124
|
return fail("scaffold", `${why}${leftBehind}`);
|
|
12963
13125
|
}
|
|
12964
13126
|
steps.push({ step: "scaffold", status: "done" });
|
|
12965
13127
|
phase = "git";
|
|
12966
13128
|
if (!opts.json) log("\u25B6 git: init/branch/commit");
|
|
12967
|
-
if (!await
|
|
13129
|
+
if (!await fs47.pathExists(path59.join(projectPath, ".git"))) {
|
|
12968
13130
|
const init = await deps.git(["init"], { cwd: projectPath });
|
|
12969
13131
|
if (init.exitCode !== 0) return fail("git", `git init failed: ${init.stderr}`);
|
|
12970
13132
|
}
|
|
@@ -13190,7 +13352,7 @@ function resolveNewOptions(stackArg, nameArg, flags, cwd) {
|
|
|
13190
13352
|
if (flags.dir !== void 0 && flags.dir.trim() === "") {
|
|
13191
13353
|
return { ok: false, error: "--dir needs a path (omit it to scaffold into the cwd)." };
|
|
13192
13354
|
}
|
|
13193
|
-
const parentDir = flags.dir === void 0 ? cwd :
|
|
13355
|
+
const parentDir = flags.dir === void 0 ? cwd : path60.resolve(cwd, flags.dir);
|
|
13194
13356
|
const pm = flags.pm ?? "yarn";
|
|
13195
13357
|
if (!PMS.includes(pm)) {
|
|
13196
13358
|
return { ok: false, error: `Unsupported --pm "${pm}" \u2014 use yarn, npm, or pnpm.` };
|
|
@@ -13288,7 +13450,7 @@ async function runRecommend(options) {
|
|
|
13288
13450
|
}
|
|
13289
13451
|
|
|
13290
13452
|
// src/commands/remove.ts
|
|
13291
|
-
import
|
|
13453
|
+
import path61 from "node:path";
|
|
13292
13454
|
init_fs();
|
|
13293
13455
|
init_logger();
|
|
13294
13456
|
init_paths();
|
|
@@ -13395,7 +13557,7 @@ async function runRemove(ids, options = {}) {
|
|
|
13395
13557
|
}
|
|
13396
13558
|
const allAbs = cleared.flatMap(({ existing }) => existing.map(({ abs }) => abs));
|
|
13397
13559
|
const backupRoot = await backupTrackedPaths(root, allAbs, say, "remove");
|
|
13398
|
-
report.backupDir = backupRoot === null ? null :
|
|
13560
|
+
report.backupDir = backupRoot === null ? null : path61.basename(backupRoot);
|
|
13399
13561
|
for (const { id, existing } of cleared) {
|
|
13400
13562
|
await deleteTrackedPaths(root, id, existing, { verb: "Removed", reason: "" }, say);
|
|
13401
13563
|
}
|
|
@@ -13485,8 +13647,8 @@ async function runScan(options) {
|
|
|
13485
13647
|
}
|
|
13486
13648
|
|
|
13487
13649
|
// src/commands/undo.ts
|
|
13488
|
-
import
|
|
13489
|
-
import
|
|
13650
|
+
import path62 from "node:path";
|
|
13651
|
+
import fs48 from "fs-extra";
|
|
13490
13652
|
|
|
13491
13653
|
// src/claude/managed-paths.ts
|
|
13492
13654
|
init_paths();
|
|
@@ -13505,7 +13667,7 @@ init_paths();
|
|
|
13505
13667
|
async function collectCoreManagedPaths(root) {
|
|
13506
13668
|
const existing = [];
|
|
13507
13669
|
for (const abs of coreManagedAbsolutePaths(root)) {
|
|
13508
|
-
if (await
|
|
13670
|
+
if (await fs48.pathExists(abs)) existing.push(abs);
|
|
13509
13671
|
}
|
|
13510
13672
|
return existing;
|
|
13511
13673
|
}
|
|
@@ -13519,7 +13681,7 @@ async function collectLockTrackedPaths(root) {
|
|
|
13519
13681
|
const unsafeByRel = await unsafeReasonsFor(root, relPaths);
|
|
13520
13682
|
const existing = await resolveExistingTrackedPaths(root, id, relPaths, log, unsafeByRel);
|
|
13521
13683
|
for (const [rel, reason] of unsafeByRel) {
|
|
13522
|
-
if (reason !== null) preserved.push(
|
|
13684
|
+
if (reason !== null) preserved.push(path62.resolve(root, rel));
|
|
13523
13685
|
}
|
|
13524
13686
|
if (existing.length === 0) continue;
|
|
13525
13687
|
const gate = await hashGateEntry(root, { hash: row.hash }, existing);
|
|
@@ -13535,51 +13697,51 @@ async function collectLockTrackedPaths(root) {
|
|
|
13535
13697
|
}
|
|
13536
13698
|
async function settingsHasHausContent(root) {
|
|
13537
13699
|
const settingsPath = claudePath(root, "settings.json");
|
|
13538
|
-
if (!await
|
|
13700
|
+
if (!await fs48.pathExists(settingsPath)) return false;
|
|
13539
13701
|
const settings = await readProjectSettings(root);
|
|
13540
13702
|
return settings._haus != null;
|
|
13541
13703
|
}
|
|
13542
13704
|
async function claudeMdHasHausBlock(root) {
|
|
13543
|
-
const filePath =
|
|
13544
|
-
if (!await
|
|
13545
|
-
const text = await
|
|
13705
|
+
const filePath = path62.join(root, "CLAUDE.md");
|
|
13706
|
+
if (!await fs48.pathExists(filePath)) return false;
|
|
13707
|
+
const text = await fs48.readFile(filePath, "utf8");
|
|
13546
13708
|
return text.includes(BLOCK_BEGIN);
|
|
13547
13709
|
}
|
|
13548
13710
|
async function stripProjectSettings(root) {
|
|
13549
13711
|
const settingsPath = claudePath(root, "settings.json");
|
|
13550
|
-
if (!await
|
|
13712
|
+
if (!await fs48.pathExists(settingsPath)) return false;
|
|
13551
13713
|
let settings = await readProjectSettings(root);
|
|
13552
13714
|
settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
13553
13715
|
const hasContent = Object.keys(settings).length > 0;
|
|
13554
13716
|
if (hasContent) {
|
|
13555
13717
|
await writeProjectSettings(root, settings);
|
|
13556
|
-
log(`Stripped haus rules from ${
|
|
13718
|
+
log(`Stripped haus rules from ${path62.relative(root, settingsPath)} (user settings preserved).`);
|
|
13557
13719
|
return true;
|
|
13558
13720
|
}
|
|
13559
|
-
await
|
|
13560
|
-
log(`Removed ${
|
|
13721
|
+
await fs48.remove(settingsPath);
|
|
13722
|
+
log(`Removed ${path62.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
13561
13723
|
return true;
|
|
13562
13724
|
}
|
|
13563
13725
|
async function stripRootClaudeMd(root) {
|
|
13564
|
-
const filePath =
|
|
13565
|
-
if (!await
|
|
13566
|
-
const prev = await
|
|
13726
|
+
const filePath = path62.join(root, "CLAUDE.md");
|
|
13727
|
+
if (!await fs48.pathExists(filePath)) return false;
|
|
13728
|
+
const prev = await fs48.readFile(filePath, "utf8");
|
|
13567
13729
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
13568
13730
|
const next = stripHausBlock(prev);
|
|
13569
13731
|
if (next.length === 0) {
|
|
13570
|
-
await
|
|
13732
|
+
await fs48.remove(filePath);
|
|
13571
13733
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
13572
13734
|
} else {
|
|
13573
|
-
await
|
|
13735
|
+
await fs48.writeFile(filePath, next, "utf8");
|
|
13574
13736
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
13575
13737
|
}
|
|
13576
13738
|
return true;
|
|
13577
13739
|
}
|
|
13578
13740
|
async function pruneDirIfEmpty(dir) {
|
|
13579
|
-
if (!await
|
|
13741
|
+
if (!await fs48.pathExists(dir)) return;
|
|
13580
13742
|
try {
|
|
13581
|
-
const entries = await
|
|
13582
|
-
if (entries.length === 0) await
|
|
13743
|
+
const entries = await fs48.readdir(dir);
|
|
13744
|
+
if (entries.length === 0) await fs48.remove(dir);
|
|
13583
13745
|
} catch {
|
|
13584
13746
|
}
|
|
13585
13747
|
}
|
|
@@ -13588,13 +13750,13 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
|
13588
13750
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
13589
13751
|
const backupRoot = hausPath(root, "backups", dirBackupDirName("undo", stamp));
|
|
13590
13752
|
for (const abs of managedAbsPaths) {
|
|
13591
|
-
if (!await
|
|
13592
|
-
const rel =
|
|
13593
|
-
const backupPath =
|
|
13594
|
-
await
|
|
13595
|
-
await
|
|
13753
|
+
if (!await fs48.pathExists(abs)) continue;
|
|
13754
|
+
const rel = path62.relative(root, abs);
|
|
13755
|
+
const backupPath = path62.join(backupRoot, rel);
|
|
13756
|
+
await fs48.ensureDir(path62.dirname(backupPath));
|
|
13757
|
+
await fs48.copy(abs, backupPath);
|
|
13596
13758
|
}
|
|
13597
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
13759
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path62.relative(root, backupRoot)}.`);
|
|
13598
13760
|
}
|
|
13599
13761
|
async function runUndo(options) {
|
|
13600
13762
|
const root = options.root ?? process.cwd();
|
|
@@ -13608,7 +13770,7 @@ async function runUndo(options) {
|
|
|
13608
13770
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
13609
13771
|
return;
|
|
13610
13772
|
}
|
|
13611
|
-
const relTargets = managed.map((p) =>
|
|
13773
|
+
const relTargets = managed.map((p) => path62.relative(root, p));
|
|
13612
13774
|
const summaryParts = [...relTargets];
|
|
13613
13775
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
13614
13776
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -13634,9 +13796,9 @@ User-owned .claude/ files will be preserved.`
|
|
|
13634
13796
|
}
|
|
13635
13797
|
await backupManagedFilesBeforeUndo(root, managed);
|
|
13636
13798
|
for (const abs of coreManaged) {
|
|
13637
|
-
if (!await
|
|
13638
|
-
await
|
|
13639
|
-
log(`Removed ${
|
|
13799
|
+
if (!await fs48.pathExists(abs)) continue;
|
|
13800
|
+
await fs48.remove(abs);
|
|
13801
|
+
log(`Removed ${path62.relative(root, abs)}`);
|
|
13640
13802
|
}
|
|
13641
13803
|
for (const { id, existing } of lockRemovable) {
|
|
13642
13804
|
await deleteTrackedPaths(root, id, existing, { verb: "Removed" }, log);
|
|
@@ -13653,8 +13815,8 @@ init_fs();
|
|
|
13653
13815
|
init_logger();
|
|
13654
13816
|
init_paths();
|
|
13655
13817
|
import crypto4 from "node:crypto";
|
|
13656
|
-
import
|
|
13657
|
-
import
|
|
13818
|
+
import path63 from "node:path";
|
|
13819
|
+
import fs49 from "fs-extra";
|
|
13658
13820
|
async function runUninstall(options = {}) {
|
|
13659
13821
|
const { force = false, removePlugins = removeGlobalPlugins } = options;
|
|
13660
13822
|
const manifest = await readManifest();
|
|
@@ -13691,8 +13853,8 @@ async function runUninstall(options = {}) {
|
|
|
13691
13853
|
);
|
|
13692
13854
|
}
|
|
13693
13855
|
for (const entry of manifest.files) {
|
|
13694
|
-
const
|
|
13695
|
-
if (!
|
|
13856
|
+
const exists2 = fs49.pathExistsSync(entry.destPath);
|
|
13857
|
+
if (!exists2) continue;
|
|
13696
13858
|
const content2 = await readText(entry.destPath);
|
|
13697
13859
|
if (content2 === void 0) continue;
|
|
13698
13860
|
const header = parseMarkdownHeader(content2);
|
|
@@ -13709,24 +13871,24 @@ async function runUninstall(options = {}) {
|
|
|
13709
13871
|
result.skipped.push(entry.destPath);
|
|
13710
13872
|
continue;
|
|
13711
13873
|
}
|
|
13712
|
-
await
|
|
13713
|
-
await pruneEmptyDir(
|
|
13874
|
+
await fs49.remove(entry.destPath);
|
|
13875
|
+
await pruneEmptyDir(path63.dirname(entry.destPath));
|
|
13714
13876
|
result.deleted.push(entry.destPath);
|
|
13715
13877
|
}
|
|
13716
13878
|
const settings = await readSettings();
|
|
13717
13879
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
13718
13880
|
await writeSettings(stripped);
|
|
13719
13881
|
result.hooksStripped = true;
|
|
13720
|
-
const hausDir =
|
|
13882
|
+
const hausDir = path63.join(globalClaudeDir(), "haus");
|
|
13721
13883
|
const manifestPath2 = hausManifestPath();
|
|
13722
|
-
if (
|
|
13723
|
-
await
|
|
13884
|
+
if (fs49.pathExistsSync(manifestPath2)) {
|
|
13885
|
+
await fs49.remove(manifestPath2);
|
|
13724
13886
|
}
|
|
13725
13887
|
await removeInstallConfig();
|
|
13726
|
-
if (
|
|
13888
|
+
if (fs49.pathExistsSync(hausDir)) {
|
|
13727
13889
|
try {
|
|
13728
|
-
const remaining = await
|
|
13729
|
-
if (remaining.length === 0) await
|
|
13890
|
+
const remaining = await fs49.readdir(hausDir);
|
|
13891
|
+
if (remaining.length === 0) await fs49.remove(hausDir);
|
|
13730
13892
|
} catch {
|
|
13731
13893
|
}
|
|
13732
13894
|
}
|
|
@@ -13795,15 +13957,15 @@ async function runUninstallCommand(options) {
|
|
|
13795
13957
|
}
|
|
13796
13958
|
|
|
13797
13959
|
// src/commands/validate-catalog.ts
|
|
13798
|
-
import
|
|
13960
|
+
import path65 from "node:path";
|
|
13799
13961
|
|
|
13800
13962
|
// src/catalog/validate-core.ts
|
|
13801
13963
|
init_forbidden_content();
|
|
13802
13964
|
init_manifest_item_fields();
|
|
13803
13965
|
init_path_safety();
|
|
13804
13966
|
init_validation_rules2();
|
|
13805
|
-
import
|
|
13806
|
-
import
|
|
13967
|
+
import fs50 from "node:fs";
|
|
13968
|
+
import path64 from "node:path";
|
|
13807
13969
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
13808
13970
|
var ITEM_ID_PATTERN = /^haus\./;
|
|
13809
13971
|
function auditForbiddenStacks(items) {
|
|
@@ -13963,14 +14125,14 @@ function checkRequiredFrontmatter(text, label) {
|
|
|
13963
14125
|
}
|
|
13964
14126
|
function readForAudit(absPath, label, failures) {
|
|
13965
14127
|
try {
|
|
13966
|
-
return
|
|
14128
|
+
return fs50.readFileSync(absPath, "utf8");
|
|
13967
14129
|
} catch (err) {
|
|
13968
14130
|
failures.push(`${label}: unreadable (${err instanceof Error ? err.message : String(err)})`);
|
|
13969
14131
|
return null;
|
|
13970
14132
|
}
|
|
13971
14133
|
}
|
|
13972
14134
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
13973
|
-
const rel =
|
|
14135
|
+
const rel = path64.relative(manifestDir, absPath);
|
|
13974
14136
|
const failures = [];
|
|
13975
14137
|
const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
|
|
13976
14138
|
if (text === null) return failures;
|
|
@@ -13992,55 +14154,55 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
13992
14154
|
const failures = [];
|
|
13993
14155
|
for (const item of items) {
|
|
13994
14156
|
if (!item.path || !isSafeCatalogPath(item.path)) continue;
|
|
13995
|
-
const absPath =
|
|
14157
|
+
const absPath = path64.join(manifestDir, item.path);
|
|
13996
14158
|
if (item.type === "skill") {
|
|
13997
|
-
const skillMd =
|
|
13998
|
-
if (!
|
|
13999
|
-
failures.push(`${item.id}: missing ${
|
|
14159
|
+
const skillMd = path64.join(absPath, "SKILL.md");
|
|
14160
|
+
if (!fs50.existsSync(skillMd)) {
|
|
14161
|
+
failures.push(`${item.id}: missing ${path64.relative(manifestDir, skillMd)}`);
|
|
14000
14162
|
continue;
|
|
14001
14163
|
}
|
|
14002
14164
|
const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
|
|
14003
14165
|
if (text === null) continue;
|
|
14004
14166
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
14005
14167
|
failures.push(
|
|
14006
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
14168
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path64.relative(manifestDir, skillMd)}`)
|
|
14007
14169
|
);
|
|
14008
14170
|
} else if (item.type === "agent") {
|
|
14009
|
-
if (!
|
|
14171
|
+
if (!fs50.existsSync(absPath)) {
|
|
14010
14172
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
14011
14173
|
continue;
|
|
14012
14174
|
}
|
|
14013
|
-
const rel =
|
|
14175
|
+
const rel = path64.relative(manifestDir, absPath);
|
|
14014
14176
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
14015
14177
|
if (text === null) continue;
|
|
14016
14178
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
14017
14179
|
failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
|
|
14018
14180
|
} else if (item.type === "template") {
|
|
14019
|
-
if (!
|
|
14181
|
+
if (!fs50.existsSync(absPath)) {
|
|
14020
14182
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
14021
14183
|
continue;
|
|
14022
14184
|
}
|
|
14023
14185
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
14024
14186
|
} else if (item.type === "command") {
|
|
14025
|
-
if (!
|
|
14187
|
+
if (!fs50.existsSync(absPath)) {
|
|
14026
14188
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
14027
14189
|
continue;
|
|
14028
14190
|
}
|
|
14029
|
-
const rel =
|
|
14191
|
+
const rel = path64.relative(manifestDir, absPath);
|
|
14030
14192
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
14031
14193
|
if (text === null) continue;
|
|
14032
14194
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
14033
14195
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
14034
14196
|
} else if (item.type === "config") {
|
|
14035
|
-
if (!
|
|
14197
|
+
if (!fs50.existsSync(absPath)) {
|
|
14036
14198
|
failures.push(`${item.id}: missing config path ${item.path}`);
|
|
14037
14199
|
}
|
|
14038
14200
|
} else if (item.type === "rule") {
|
|
14039
|
-
if (!
|
|
14201
|
+
if (!fs50.existsSync(absPath)) {
|
|
14040
14202
|
failures.push(`${item.id}: missing rule file ${item.path}`);
|
|
14041
14203
|
}
|
|
14042
14204
|
} else if (item.type === "asset") {
|
|
14043
|
-
if (!
|
|
14205
|
+
if (!fs50.existsSync(absPath)) {
|
|
14044
14206
|
failures.push(`${item.id}: missing asset file ${item.path}`);
|
|
14045
14207
|
}
|
|
14046
14208
|
}
|
|
@@ -14075,7 +14237,7 @@ function resolveItemType(relPath, pathTypeMap) {
|
|
|
14075
14237
|
function walkAllFiles(dir, fn, failures) {
|
|
14076
14238
|
let entries;
|
|
14077
14239
|
try {
|
|
14078
|
-
entries =
|
|
14240
|
+
entries = fs50.readdirSync(dir, { withFileTypes: true });
|
|
14079
14241
|
} catch (err) {
|
|
14080
14242
|
failures.push(
|
|
14081
14243
|
`${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
|
|
@@ -14084,7 +14246,7 @@ function walkAllFiles(dir, fn, failures) {
|
|
|
14084
14246
|
}
|
|
14085
14247
|
for (const entry of entries) {
|
|
14086
14248
|
if (entry.name === ".DS_Store") continue;
|
|
14087
|
-
const full =
|
|
14249
|
+
const full = path64.join(dir, entry.name);
|
|
14088
14250
|
if (entry.isDirectory()) walkAllFiles(full, fn, failures);
|
|
14089
14251
|
else fn(full);
|
|
14090
14252
|
}
|
|
@@ -14108,12 +14270,12 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
14108
14270
|
const failures = [];
|
|
14109
14271
|
const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
|
|
14110
14272
|
for (const dir of CONTENT_ROOTS) {
|
|
14111
|
-
const abs =
|
|
14112
|
-
if (!
|
|
14273
|
+
const abs = path64.join(manifestDir, dir);
|
|
14274
|
+
if (!fs50.existsSync(abs)) continue;
|
|
14113
14275
|
walkAllFiles(
|
|
14114
14276
|
abs,
|
|
14115
14277
|
(file) => {
|
|
14116
|
-
const relPath =
|
|
14278
|
+
const relPath = path64.relative(manifestDir, file).replace(/\\/g, "/");
|
|
14117
14279
|
if (!isPathClaimed(relPath, claimedPaths)) {
|
|
14118
14280
|
failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
|
|
14119
14281
|
}
|
|
@@ -14126,7 +14288,7 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
14126
14288
|
function walkMd(dir, fn, failures) {
|
|
14127
14289
|
let entries;
|
|
14128
14290
|
try {
|
|
14129
|
-
entries =
|
|
14291
|
+
entries = fs50.readdirSync(dir, { withFileTypes: true });
|
|
14130
14292
|
} catch (err) {
|
|
14131
14293
|
failures.push(
|
|
14132
14294
|
`${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
|
|
@@ -14134,7 +14296,7 @@ function walkMd(dir, fn, failures) {
|
|
|
14134
14296
|
return;
|
|
14135
14297
|
}
|
|
14136
14298
|
for (const entry of entries) {
|
|
14137
|
-
const full =
|
|
14299
|
+
const full = path64.join(dir, entry.name);
|
|
14138
14300
|
if (entry.isDirectory()) walkMd(full, fn, failures);
|
|
14139
14301
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
14140
14302
|
}
|
|
@@ -14144,13 +14306,13 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
14144
14306
|
const pathSourceMap = buildItemPathSourceMap(items);
|
|
14145
14307
|
const pathTypeMap = buildItemPathTypeMap(items);
|
|
14146
14308
|
for (const dir of ["skills", "agents", "templates", "commands", "sources"]) {
|
|
14147
|
-
const abs =
|
|
14148
|
-
if (!
|
|
14309
|
+
const abs = path64.join(manifestDir, dir);
|
|
14310
|
+
if (!fs50.existsSync(abs)) continue;
|
|
14149
14311
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
14150
14312
|
walkMd(
|
|
14151
14313
|
abs,
|
|
14152
14314
|
(file) => {
|
|
14153
|
-
const rel =
|
|
14315
|
+
const rel = path64.relative(manifestDir, file);
|
|
14154
14316
|
const text = readForAudit(file, rel, failures);
|
|
14155
14317
|
if (text === null) return;
|
|
14156
14318
|
const norm = rel.replace(/\\/g, "/");
|
|
@@ -14198,8 +14360,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
14198
14360
|
process.exitCode = 1;
|
|
14199
14361
|
return;
|
|
14200
14362
|
}
|
|
14201
|
-
const abs =
|
|
14202
|
-
const manifestDir =
|
|
14363
|
+
const abs = path65.resolve(process.cwd(), manifestPath2);
|
|
14364
|
+
const manifestDir = path65.dirname(abs);
|
|
14203
14365
|
const data = await readJson(abs);
|
|
14204
14366
|
if (!data?.items) {
|
|
14205
14367
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -14220,8 +14382,8 @@ init_logger();
|
|
|
14220
14382
|
|
|
14221
14383
|
// src/workspace/worktree/add.ts
|
|
14222
14384
|
init_fs();
|
|
14223
|
-
import
|
|
14224
|
-
import
|
|
14385
|
+
import path72 from "node:path";
|
|
14386
|
+
import fs54 from "fs-extra";
|
|
14225
14387
|
|
|
14226
14388
|
// src/workspace/worktree/git-worktree.ts
|
|
14227
14389
|
init_exec();
|
|
@@ -14243,8 +14405,8 @@ function isBranchCheckedOutElsewhereError(stderr) {
|
|
|
14243
14405
|
return CHECKED_OUT_ELSEWHERE_PATTERNS.some((re) => re.test(stderr));
|
|
14244
14406
|
}
|
|
14245
14407
|
async function addWorktree(cwd, worktreePath2, branch, opts = {}) {
|
|
14246
|
-
const
|
|
14247
|
-
if (
|
|
14408
|
+
const exists2 = await branchExists(cwd, branch);
|
|
14409
|
+
if (exists2) {
|
|
14248
14410
|
const result2 = await runGit(["worktree", "add", worktreePath2, branch], { cwd });
|
|
14249
14411
|
return {
|
|
14250
14412
|
ok: result2.exitCode === 0,
|
|
@@ -14341,13 +14503,13 @@ async function hasUnpushedWork(cwd) {
|
|
|
14341
14503
|
}
|
|
14342
14504
|
|
|
14343
14505
|
// src/workspace/worktree/hydrate.ts
|
|
14344
|
-
import
|
|
14345
|
-
import
|
|
14506
|
+
import path68 from "node:path";
|
|
14507
|
+
import fs52 from "fs-extra";
|
|
14346
14508
|
|
|
14347
14509
|
// src/workspace/worktree/cow-copy.ts
|
|
14348
14510
|
init_exec();
|
|
14349
|
-
import
|
|
14350
|
-
import
|
|
14511
|
+
import path66 from "node:path";
|
|
14512
|
+
import fs51 from "fs-extra";
|
|
14351
14513
|
async function getMacFilesystemType(dir) {
|
|
14352
14514
|
const df = await runCommand("df", ["-P", dir]);
|
|
14353
14515
|
if (df.exitCode !== 0) return void 0;
|
|
@@ -14378,14 +14540,14 @@ async function detectCowStrategy(dir) {
|
|
|
14378
14540
|
return "unknown-platform";
|
|
14379
14541
|
}
|
|
14380
14542
|
async function cowCopyDir(src, dest) {
|
|
14381
|
-
const srcExists = await
|
|
14543
|
+
const srcExists = await fs51.pathExists(src);
|
|
14382
14544
|
if (!srcExists)
|
|
14383
14545
|
return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
|
|
14384
|
-
const strategy = await detectCowStrategy(
|
|
14546
|
+
const strategy = await detectCowStrategy(path66.dirname(dest));
|
|
14385
14547
|
if (strategy === "unsupported" || strategy === "unknown-platform") {
|
|
14386
14548
|
return { strategy, attempted: false, ok: false };
|
|
14387
14549
|
}
|
|
14388
|
-
await
|
|
14550
|
+
await fs51.ensureDir(path66.dirname(dest));
|
|
14389
14551
|
const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
|
|
14390
14552
|
const result = await runCommand("cp", args);
|
|
14391
14553
|
return {
|
|
@@ -14399,7 +14561,7 @@ async function cowCopyDir(src, dest) {
|
|
|
14399
14561
|
// src/workspace/worktree/install.ts
|
|
14400
14562
|
init_exec();
|
|
14401
14563
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
14402
|
-
import
|
|
14564
|
+
import path67 from "node:path";
|
|
14403
14565
|
import fg7 from "fast-glob";
|
|
14404
14566
|
function detectInstallPlan(signals) {
|
|
14405
14567
|
if (signals.hasYarnLock) {
|
|
@@ -14420,10 +14582,10 @@ function detectInstallPlan(signals) {
|
|
|
14420
14582
|
return { manager: "none" };
|
|
14421
14583
|
}
|
|
14422
14584
|
async function computeLockfileSignals(dir) {
|
|
14423
|
-
const hasYarnLock = existsSync3(
|
|
14424
|
-
const hasPnpmLock = existsSync3(
|
|
14425
|
-
const hasPackageLock = existsSync3(
|
|
14426
|
-
const hasComposerLock = existsSync3(
|
|
14585
|
+
const hasYarnLock = existsSync3(path67.join(dir, "yarn.lock"));
|
|
14586
|
+
const hasPnpmLock = existsSync3(path67.join(dir, "pnpm-lock.yaml"));
|
|
14587
|
+
const hasPackageLock = existsSync3(path67.join(dir, "package-lock.json"));
|
|
14588
|
+
const hasComposerLock = existsSync3(path67.join(dir, "composer.lock"));
|
|
14427
14589
|
const dotnetFiles = await fg7(["**/*.csproj", "**/*.sln"], {
|
|
14428
14590
|
cwd: dir,
|
|
14429
14591
|
onlyFiles: true,
|
|
@@ -14437,7 +14599,7 @@ async function computeLockfileSignals(dir) {
|
|
|
14437
14599
|
});
|
|
14438
14600
|
const hasDotnetProject = dotnetFiles.length > 0;
|
|
14439
14601
|
let hasPackageManagerField = false;
|
|
14440
|
-
const pkgPath =
|
|
14602
|
+
const pkgPath = path67.join(dir, "package.json");
|
|
14441
14603
|
if (existsSync3(pkgPath)) {
|
|
14442
14604
|
try {
|
|
14443
14605
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
@@ -14480,7 +14642,7 @@ function missingYarnrcEnvVars(content2, env) {
|
|
|
14480
14642
|
return findRequiredYarnrcEnvVars(content2).filter((name) => env[name] === void 0);
|
|
14481
14643
|
}
|
|
14482
14644
|
function missingYarnrcEnvVarsInDir(dir, env) {
|
|
14483
|
-
const yarnrcPath =
|
|
14645
|
+
const yarnrcPath = path67.join(dir, ".yarnrc.yml");
|
|
14484
14646
|
if (!existsSync3(yarnrcPath)) return [];
|
|
14485
14647
|
try {
|
|
14486
14648
|
return missingYarnrcEnvVars(readFileSync3(yarnrcPath, "utf8"), env);
|
|
@@ -14576,13 +14738,13 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
14576
14738
|
const start = Date.now();
|
|
14577
14739
|
const targets = [];
|
|
14578
14740
|
for (const target of HYDRATION_TARGETS) {
|
|
14579
|
-
const src =
|
|
14580
|
-
const dest =
|
|
14581
|
-
if (!await
|
|
14582
|
-
const destExists = await
|
|
14741
|
+
const src = path68.join(mainMemberPath, target);
|
|
14742
|
+
const dest = path68.join(worktreeMemberPath, target);
|
|
14743
|
+
if (!await fs52.pathExists(src)) continue;
|
|
14744
|
+
const destExists = await fs52.pathExists(dest);
|
|
14583
14745
|
if (!opts.force && destExists) continue;
|
|
14584
14746
|
if (opts.force && destExists) {
|
|
14585
|
-
await
|
|
14747
|
+
await fs52.remove(dest);
|
|
14586
14748
|
}
|
|
14587
14749
|
const copy = await cowCopyDir(src, dest);
|
|
14588
14750
|
targets.push({ target, copy });
|
|
@@ -14595,11 +14757,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
14595
14757
|
|
|
14596
14758
|
// src/workspace/worktree/local-files.ts
|
|
14597
14759
|
init_exec();
|
|
14598
|
-
import
|
|
14599
|
-
import
|
|
14760
|
+
import path69 from "node:path";
|
|
14761
|
+
import fs53 from "fs-extra";
|
|
14600
14762
|
var MACHINE_LOCAL_FILES = [
|
|
14601
14763
|
"docker-compose.local.yml",
|
|
14602
|
-
|
|
14764
|
+
path69.join(".claude", "settings.local.json")
|
|
14603
14765
|
];
|
|
14604
14766
|
async function isTracked(repoRoot, relFile) {
|
|
14605
14767
|
const gitPath = relFile.replace(/\\/g, "/");
|
|
@@ -14609,12 +14771,12 @@ async function isTracked(repoRoot, relFile) {
|
|
|
14609
14771
|
async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
14610
14772
|
const copied = [];
|
|
14611
14773
|
for (const rel of MACHINE_LOCAL_FILES) {
|
|
14612
|
-
const src =
|
|
14613
|
-
if (!await
|
|
14774
|
+
const src = path69.join(mainMemberPath, rel);
|
|
14775
|
+
if (!await fs53.pathExists(src)) continue;
|
|
14614
14776
|
if (await isTracked(mainMemberPath, rel)) continue;
|
|
14615
|
-
const dest =
|
|
14616
|
-
await
|
|
14617
|
-
await
|
|
14777
|
+
const dest = path69.join(worktreeMemberPath, rel);
|
|
14778
|
+
await fs53.ensureDir(path69.dirname(dest));
|
|
14779
|
+
await fs53.copy(src, dest, { overwrite: true });
|
|
14618
14780
|
copied.push(rel);
|
|
14619
14781
|
}
|
|
14620
14782
|
return copied;
|
|
@@ -14622,15 +14784,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
|
14622
14784
|
|
|
14623
14785
|
// src/workspace/worktree/root.ts
|
|
14624
14786
|
import { existsSync as existsSync4 } from "node:fs";
|
|
14625
|
-
import
|
|
14787
|
+
import path70 from "node:path";
|
|
14626
14788
|
async function resolveWorkspaceForWorktree(start) {
|
|
14627
14789
|
const rootInfo = await resolveRoots(start);
|
|
14628
14790
|
if (!rootInfo.isGitRepo) {
|
|
14629
14791
|
return { ok: false, reason: "Not inside a git repository." };
|
|
14630
14792
|
}
|
|
14631
14793
|
const workspaceRoot = rootInfo.mainRoot;
|
|
14632
|
-
const hasYaml = existsSync4(
|
|
14633
|
-
const hasManifest = existsSync4(
|
|
14794
|
+
const hasYaml = existsSync4(path70.join(workspaceRoot, WORKSPACE_FILE));
|
|
14795
|
+
const hasManifest = existsSync4(path70.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
14634
14796
|
if (!hasYaml && !hasManifest) {
|
|
14635
14797
|
return {
|
|
14636
14798
|
ok: false,
|
|
@@ -14656,10 +14818,10 @@ function selectMembers(members, only) {
|
|
|
14656
14818
|
|
|
14657
14819
|
// src/workspace/worktree/state.ts
|
|
14658
14820
|
init_fs();
|
|
14659
|
-
import
|
|
14821
|
+
import path71 from "node:path";
|
|
14660
14822
|
var WORKTREE_STATE_FILE = ".haus-worktree.json";
|
|
14661
14823
|
function worktreesDir(mainRoot) {
|
|
14662
|
-
return
|
|
14824
|
+
return path71.join(mainRoot, ".claude", "worktrees");
|
|
14663
14825
|
}
|
|
14664
14826
|
function isValidWorktreeSlug(slug) {
|
|
14665
14827
|
if (slug === "" || slug === "." || slug === "..") return false;
|
|
@@ -14672,22 +14834,22 @@ function worktreePath(mainRoot, slug) {
|
|
|
14672
14834
|
`Invalid worktree slug ${JSON.stringify(slug)} \u2014 slugs may only contain letters, digits, ".", "_", "-" (no path separators, and not "." or "..").`
|
|
14673
14835
|
);
|
|
14674
14836
|
}
|
|
14675
|
-
return
|
|
14837
|
+
return path71.join(worktreesDir(mainRoot), slug);
|
|
14676
14838
|
}
|
|
14677
14839
|
function slugFromWorktreePath(mainRoot, somePath) {
|
|
14678
14840
|
const dir = worktreesDir(mainRoot);
|
|
14679
|
-
const prefix = `${dir}${
|
|
14841
|
+
const prefix = `${dir}${path71.sep}`;
|
|
14680
14842
|
if (!somePath.startsWith(prefix)) return void 0;
|
|
14681
|
-
const segment = somePath.slice(prefix.length).split(
|
|
14843
|
+
const segment = somePath.slice(prefix.length).split(path71.sep)[0];
|
|
14682
14844
|
return segment !== void 0 && segment !== "" ? segment : void 0;
|
|
14683
14845
|
}
|
|
14684
14846
|
async function readWorktreeState(worktreeRoot) {
|
|
14685
|
-
const state = await readJson(
|
|
14847
|
+
const state = await readJson(path71.join(worktreeRoot, WORKTREE_STATE_FILE));
|
|
14686
14848
|
if (state == null || typeof state !== "object" || !Array.isArray(state.members)) return void 0;
|
|
14687
14849
|
return state;
|
|
14688
14850
|
}
|
|
14689
14851
|
async function writeWorktreeState(worktreeRoot, state) {
|
|
14690
|
-
await writeJson(
|
|
14852
|
+
await writeJson(path71.join(worktreeRoot, WORKTREE_STATE_FILE), state);
|
|
14691
14853
|
}
|
|
14692
14854
|
|
|
14693
14855
|
// src/workspace/worktree/add.ts
|
|
@@ -14702,7 +14864,7 @@ async function runAdd(opts) {
|
|
|
14702
14864
|
}
|
|
14703
14865
|
const branch = opts.branch ?? opts.slug;
|
|
14704
14866
|
const wsWorktreePath = worktreePath(workspaceRoot, opts.slug);
|
|
14705
|
-
if (await
|
|
14867
|
+
if (await fs54.pathExists(wsWorktreePath)) {
|
|
14706
14868
|
return {
|
|
14707
14869
|
ok: false,
|
|
14708
14870
|
error: `Workspace worktree already exists at ${wsWorktreePath} \u2014 run \`haus workspace worktree hydrate --materialize\` from inside it to check out any missing members (e.g. a worktree created by plain \`git worktree add\`), or \`remove\` first to start over.`
|
|
@@ -14721,7 +14883,7 @@ async function runAdd(opts) {
|
|
|
14721
14883
|
const memberResults = [];
|
|
14722
14884
|
for (const member of selected) {
|
|
14723
14885
|
const start = Date.now();
|
|
14724
|
-
const memberWorktreePath =
|
|
14886
|
+
const memberWorktreePath = path72.join(wsWorktreePath, member.folder);
|
|
14725
14887
|
const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
|
|
14726
14888
|
preferDefaultBranchFrom: true
|
|
14727
14889
|
});
|
|
@@ -14751,7 +14913,7 @@ async function runAdd(opts) {
|
|
|
14751
14913
|
await mapWithConcurrency(okResults, async (result) => {
|
|
14752
14914
|
const member = selected.find((m) => m.id === result.member);
|
|
14753
14915
|
if (!member) return;
|
|
14754
|
-
const memberWorktreePath =
|
|
14916
|
+
const memberWorktreePath = path72.join(wsWorktreePath, member.folder);
|
|
14755
14917
|
const hydrationStart = Date.now();
|
|
14756
14918
|
if (opts.hydrate !== false) {
|
|
14757
14919
|
result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
|
|
@@ -14788,9 +14950,9 @@ async function runDryRun(args) {
|
|
|
14788
14950
|
const wsBranchExists = await branchExists(workspaceRoot, branch);
|
|
14789
14951
|
const members = [];
|
|
14790
14952
|
for (const member of selected) {
|
|
14791
|
-
const
|
|
14953
|
+
const exists2 = await branchExists(member.absPath, branch);
|
|
14792
14954
|
let startPoint;
|
|
14793
|
-
if (!
|
|
14955
|
+
if (!exists2) {
|
|
14794
14956
|
const def = await resolveDefaultBranch(member.absPath);
|
|
14795
14957
|
startPoint = def?.ref;
|
|
14796
14958
|
}
|
|
@@ -14799,7 +14961,7 @@ async function runDryRun(args) {
|
|
|
14799
14961
|
folder: member.folder,
|
|
14800
14962
|
status: "planned",
|
|
14801
14963
|
branch,
|
|
14802
|
-
branchAction:
|
|
14964
|
+
branchAction: exists2 ? "checkout" : "create",
|
|
14803
14965
|
startPoint,
|
|
14804
14966
|
durationMs: 0
|
|
14805
14967
|
});
|
|
@@ -14819,17 +14981,17 @@ async function runDryRun(args) {
|
|
|
14819
14981
|
// src/workspace/worktree/autohydrate.ts
|
|
14820
14982
|
import { spawn } from "node:child_process";
|
|
14821
14983
|
import fsSync from "node:fs";
|
|
14822
|
-
import
|
|
14823
|
-
import
|
|
14984
|
+
import path77 from "node:path";
|
|
14985
|
+
import fs56 from "fs-extra";
|
|
14824
14986
|
|
|
14825
14987
|
// src/workspace/worktree/doctor.ts
|
|
14826
14988
|
import { existsSync as existsSync6 } from "node:fs";
|
|
14827
|
-
import
|
|
14989
|
+
import path75 from "node:path";
|
|
14828
14990
|
|
|
14829
14991
|
// src/workspace/worktree/hydrate-lock.ts
|
|
14830
14992
|
init_paths();
|
|
14831
|
-
import
|
|
14832
|
-
import
|
|
14993
|
+
import path73 from "node:path";
|
|
14994
|
+
import fs55 from "fs-extra";
|
|
14833
14995
|
var STALE_LOCK_MS = 30 * 60 * 1e3;
|
|
14834
14996
|
function locksDir(mainRoot) {
|
|
14835
14997
|
return hausPath(mainRoot, "locks");
|
|
@@ -14846,13 +15008,13 @@ function assertSlug(slug) {
|
|
|
14846
15008
|
return slug;
|
|
14847
15009
|
}
|
|
14848
15010
|
function hydrateLockPath(mainRoot, slug) {
|
|
14849
|
-
return
|
|
15011
|
+
return path73.join(locksDir(mainRoot), `hydrate-${assertSlug(slug)}.lock`);
|
|
14850
15012
|
}
|
|
14851
15013
|
function hydrateLogPath(mainRoot, slug) {
|
|
14852
|
-
return
|
|
15014
|
+
return path73.join(logsDir(mainRoot), `hydrate-${assertSlug(slug)}.log`);
|
|
14853
15015
|
}
|
|
14854
15016
|
function hydrateLastRunPath(mainRoot, slug) {
|
|
14855
|
-
return
|
|
15017
|
+
return path73.join(logsDir(mainRoot), `hydrate-${assertSlug(slug)}.last.json`);
|
|
14856
15018
|
}
|
|
14857
15019
|
function defaultIsAlive(pid) {
|
|
14858
15020
|
try {
|
|
@@ -14864,7 +15026,7 @@ function defaultIsAlive(pid) {
|
|
|
14864
15026
|
}
|
|
14865
15027
|
async function readOwnerPid(lockPath) {
|
|
14866
15028
|
try {
|
|
14867
|
-
const raw = (await
|
|
15029
|
+
const raw = (await fs55.readFile(path73.join(lockPath, "pid"), "utf8")).trim();
|
|
14868
15030
|
if (raw === "") return void 0;
|
|
14869
15031
|
const pid = Number.parseInt(raw, 10);
|
|
14870
15032
|
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
@@ -14873,13 +15035,13 @@ async function readOwnerPid(lockPath) {
|
|
|
14873
15035
|
}
|
|
14874
15036
|
}
|
|
14875
15037
|
async function claim(lockPath, pid) {
|
|
14876
|
-
await
|
|
15038
|
+
await fs55.ensureDir(path73.dirname(lockPath));
|
|
14877
15039
|
try {
|
|
14878
|
-
await
|
|
15040
|
+
await fs55.mkdir(lockPath);
|
|
14879
15041
|
} catch {
|
|
14880
15042
|
return false;
|
|
14881
15043
|
}
|
|
14882
|
-
await
|
|
15044
|
+
await fs55.writeFile(path73.join(lockPath, "pid"), String(pid), "utf8");
|
|
14883
15045
|
return true;
|
|
14884
15046
|
}
|
|
14885
15047
|
async function acquireHydrateLock(mainRoot, slug, deps = {}) {
|
|
@@ -14898,7 +15060,7 @@ async function acquireHydrateLock(mainRoot, slug, deps = {}) {
|
|
|
14898
15060
|
}
|
|
14899
15061
|
if (alive) return { acquired: false, reason: "live", ownerPid, lockPath };
|
|
14900
15062
|
} else {
|
|
14901
|
-
const stat = await
|
|
15063
|
+
const stat = await fs55.stat(lockPath).catch(() => void 0);
|
|
14902
15064
|
if (stat === void 0) {
|
|
14903
15065
|
return await claim(lockPath, pid) ? { acquired: true, lockPath } : { acquired: false, reason: "live", lockPath };
|
|
14904
15066
|
}
|
|
@@ -14906,18 +15068,18 @@ async function acquireHydrateLock(mainRoot, slug, deps = {}) {
|
|
|
14906
15068
|
return { acquired: false, reason: "starting", lockPath };
|
|
14907
15069
|
}
|
|
14908
15070
|
}
|
|
14909
|
-
await
|
|
15071
|
+
await fs55.remove(lockPath);
|
|
14910
15072
|
return await claim(lockPath, pid) ? { acquired: true, lockPath } : { acquired: false, reason: "live", ownerPid, lockPath };
|
|
14911
15073
|
}
|
|
14912
15074
|
async function recordHydrateLockOwner(mainRoot, slug, pid) {
|
|
14913
|
-
await
|
|
15075
|
+
await fs55.writeFile(path73.join(hydrateLockPath(mainRoot, slug), "pid"), String(pid), "utf8");
|
|
14914
15076
|
}
|
|
14915
15077
|
async function releaseHydrateLock(mainRoot, slug) {
|
|
14916
|
-
await
|
|
15078
|
+
await fs55.remove(hydrateLockPath(mainRoot, slug));
|
|
14917
15079
|
}
|
|
14918
15080
|
async function readHydrateLock(mainRoot, slug) {
|
|
14919
15081
|
const lockPath = hydrateLockPath(mainRoot, slug);
|
|
14920
|
-
const stat = await
|
|
15082
|
+
const stat = await fs55.stat(lockPath).catch(() => void 0);
|
|
14921
15083
|
if (stat === void 0) return { held: false };
|
|
14922
15084
|
const ownerPid = await readOwnerPid(lockPath);
|
|
14923
15085
|
return {
|
|
@@ -14930,21 +15092,21 @@ async function readHydrateLock(mainRoot, slug) {
|
|
|
14930
15092
|
|
|
14931
15093
|
// src/workspace/worktree/orphans.ts
|
|
14932
15094
|
import { existsSync as existsSync5 } from "node:fs";
|
|
14933
|
-
import
|
|
15095
|
+
import path74 from "node:path";
|
|
14934
15096
|
var WORKSPACE_REPO_ID = "(workspace)";
|
|
14935
15097
|
async function findOrphanedRegistrations(repos, wsWorktreesDir) {
|
|
14936
15098
|
const orphans = [];
|
|
14937
15099
|
for (const repo of repos) {
|
|
14938
15100
|
const entries = await listWorktrees(repo.absPath);
|
|
14939
15101
|
for (const entry of entries) {
|
|
14940
|
-
const entryPath =
|
|
14941
|
-
const marker = `${
|
|
15102
|
+
const entryPath = path74.normalize(entry.path);
|
|
15103
|
+
const marker = `${path74.sep}.claude${path74.sep}worktrees${path74.sep}`;
|
|
14942
15104
|
const idx = entryPath.indexOf(marker);
|
|
14943
15105
|
if (idx === -1) continue;
|
|
14944
15106
|
const rest = entryPath.slice(idx + marker.length);
|
|
14945
|
-
const slug = rest.split(
|
|
15107
|
+
const slug = rest.split(path74.sep)[0];
|
|
14946
15108
|
if (!slug) continue;
|
|
14947
|
-
if (existsSync5(
|
|
15109
|
+
if (existsSync5(path74.join(wsWorktreesDir, slug))) continue;
|
|
14948
15110
|
orphans.push({
|
|
14949
15111
|
repoId: repo.id,
|
|
14950
15112
|
ownerRepo: repo.absPath,
|
|
@@ -14959,7 +15121,7 @@ async function findOrphanedRegistrations(repos, wsWorktreesDir) {
|
|
|
14959
15121
|
// src/workspace/worktree/doctor.ts
|
|
14960
15122
|
var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
|
|
14961
15123
|
function looksHydrated(memberPath) {
|
|
14962
|
-
return HYDRATION_MARKERS.some((marker) => existsSync6(
|
|
15124
|
+
return HYDRATION_MARKERS.some((marker) => existsSync6(path75.join(memberPath, marker)));
|
|
14963
15125
|
}
|
|
14964
15126
|
async function runDoctor2() {
|
|
14965
15127
|
const rootInfo = await resolveRoots();
|
|
@@ -14976,7 +15138,7 @@ async function runDoctor2() {
|
|
|
14976
15138
|
};
|
|
14977
15139
|
}
|
|
14978
15140
|
const workspaceRoot = rootInfo.mainRoot;
|
|
14979
|
-
const isWorkspace = existsSync6(
|
|
15141
|
+
const isWorkspace = existsSync6(path75.join(workspaceRoot, WORKSPACE_FILE)) || existsSync6(path75.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
14980
15142
|
if (!isWorkspace) {
|
|
14981
15143
|
return {
|
|
14982
15144
|
isWorkspace: false,
|
|
@@ -14992,7 +15154,7 @@ async function runDoctor2() {
|
|
|
14992
15154
|
};
|
|
14993
15155
|
}
|
|
14994
15156
|
const wsWorktreesDir = worktreesDir(workspaceRoot);
|
|
14995
|
-
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${
|
|
15157
|
+
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path75.sep}`);
|
|
14996
15158
|
const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
|
|
14997
15159
|
const workspaceConfig = await readWorkspaceConfig(workspaceRoot);
|
|
14998
15160
|
const autoHydrate = workspaceConfig?.worktree?.autoHydrate === true;
|
|
@@ -15005,10 +15167,10 @@ async function runDoctor2() {
|
|
|
15005
15167
|
let hydrationOwnerPid;
|
|
15006
15168
|
if (inWorkspaceWorktree) {
|
|
15007
15169
|
const wtPath = rootInfo.repoRoot;
|
|
15008
|
-
slug = rootInfo.worktreeName ??
|
|
15170
|
+
slug = rootInfo.worktreeName ?? path75.basename(wtPath);
|
|
15009
15171
|
const state = await readWorktreeState(wtPath);
|
|
15010
15172
|
for (const member of members) {
|
|
15011
|
-
const memberPath =
|
|
15173
|
+
const memberPath = path75.join(wtPath, member.folder);
|
|
15012
15174
|
const materialized = existsSync6(memberPath);
|
|
15013
15175
|
const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
|
|
15014
15176
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
@@ -15053,7 +15215,7 @@ async function runDoctor2() {
|
|
|
15053
15215
|
wsWorktreesDir
|
|
15054
15216
|
);
|
|
15055
15217
|
const orphans = orphanScan.map(
|
|
15056
|
-
(o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${
|
|
15218
|
+
(o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${path75.join(wsWorktreesDir, o.slug)} no longer exists.`
|
|
15057
15219
|
);
|
|
15058
15220
|
if (orphans.length > 0) {
|
|
15059
15221
|
problems.push(...orphans);
|
|
@@ -15076,7 +15238,7 @@ async function runDoctor2() {
|
|
|
15076
15238
|
// src/workspace/worktree/hydrate-command.ts
|
|
15077
15239
|
init_fs();
|
|
15078
15240
|
import { existsSync as existsSync7 } from "node:fs";
|
|
15079
|
-
import
|
|
15241
|
+
import path76 from "node:path";
|
|
15080
15242
|
async function runHydrateWorktree(opts) {
|
|
15081
15243
|
const rootInfo = await resolveRoots();
|
|
15082
15244
|
if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
|
|
@@ -15087,7 +15249,7 @@ async function runHydrateWorktree(opts) {
|
|
|
15087
15249
|
}
|
|
15088
15250
|
const workspaceRoot = rootInfo.mainRoot;
|
|
15089
15251
|
const wtPath = rootInfo.repoRoot;
|
|
15090
|
-
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${
|
|
15252
|
+
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path76.sep}`)) {
|
|
15091
15253
|
return {
|
|
15092
15254
|
ok: false,
|
|
15093
15255
|
error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
|
|
@@ -15099,13 +15261,13 @@ async function runHydrateWorktree(opts) {
|
|
|
15099
15261
|
return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
|
|
15100
15262
|
}
|
|
15101
15263
|
const state = await readWorktreeState(wtPath);
|
|
15102
|
-
const slug = state?.slug ??
|
|
15264
|
+
const slug = state?.slug ?? path76.basename(wtPath);
|
|
15103
15265
|
const detectedBranch = await currentBranch(wtPath);
|
|
15104
15266
|
const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
|
|
15105
15267
|
if (opts.dryRun) {
|
|
15106
15268
|
const results2 = await Promise.all(
|
|
15107
15269
|
selected.map(async (member) => {
|
|
15108
|
-
const memberPath =
|
|
15270
|
+
const memberPath = path76.join(wtPath, member.folder);
|
|
15109
15271
|
if (existsSync7(memberPath)) {
|
|
15110
15272
|
return { member: member.id, folder: member.folder, status: "planned" };
|
|
15111
15273
|
}
|
|
@@ -15126,7 +15288,7 @@ async function runHydrateWorktree(opts) {
|
|
|
15126
15288
|
const results = await mapWithConcurrency(
|
|
15127
15289
|
selected,
|
|
15128
15290
|
async (member) => {
|
|
15129
|
-
const memberPath =
|
|
15291
|
+
const memberPath = path76.join(wtPath, member.folder);
|
|
15130
15292
|
let branchAction;
|
|
15131
15293
|
if (!existsSync7(memberPath)) {
|
|
15132
15294
|
if (!opts.materialize) {
|
|
@@ -15247,7 +15409,7 @@ function defaultSpawnDetached(spec) {
|
|
|
15247
15409
|
}
|
|
15248
15410
|
function appendLog(logPath, line2) {
|
|
15249
15411
|
try {
|
|
15250
|
-
fsSync.mkdirSync(
|
|
15412
|
+
fsSync.mkdirSync(path77.dirname(logPath), { recursive: true });
|
|
15251
15413
|
const size = fsSync.statSync(logPath, { throwIfNoEntry: false })?.size ?? 0;
|
|
15252
15414
|
if (size > LOG_SIZE_CAP_BYTES) {
|
|
15253
15415
|
fsSync.writeFileSync(logPath, `--- log rotated (exceeded ${LOG_SIZE_CAP_BYTES} bytes) ---
|
|
@@ -15262,11 +15424,11 @@ async function consumeLastRunFailure(mainRoot, slug) {
|
|
|
15262
15424
|
const file = hydrateLastRunPath(mainRoot, slug);
|
|
15263
15425
|
let parsed;
|
|
15264
15426
|
try {
|
|
15265
|
-
parsed = JSON.parse(await
|
|
15427
|
+
parsed = JSON.parse(await fs56.readFile(file, "utf8"));
|
|
15266
15428
|
} catch {
|
|
15267
15429
|
return void 0;
|
|
15268
15430
|
}
|
|
15269
|
-
await
|
|
15431
|
+
await fs56.remove(file).catch(() => void 0);
|
|
15270
15432
|
if (parsed && parsed.ok === false) return parsed;
|
|
15271
15433
|
return void 0;
|
|
15272
15434
|
}
|
|
@@ -15338,7 +15500,7 @@ async function runAutoHydrate(opts = {}, deps = {}) {
|
|
|
15338
15500
|
reason: "could not determine the haus CLI entry point to re-invoke"
|
|
15339
15501
|
};
|
|
15340
15502
|
}
|
|
15341
|
-
await
|
|
15503
|
+
await fs56.ensureDir(logsDir(mainRoot));
|
|
15342
15504
|
appendLog(
|
|
15343
15505
|
logPath,
|
|
15344
15506
|
`autohydrate: forking hydrate --materialize for "${slug}" (${missingMembers.join(", ")})`
|
|
@@ -15407,21 +15569,21 @@ async function runDetachedHydration() {
|
|
|
15407
15569
|
async function writeLastRun(mainRoot, slug, ok, failures) {
|
|
15408
15570
|
const file = hydrateLastRunPath(mainRoot, slug);
|
|
15409
15571
|
if (ok) {
|
|
15410
|
-
await
|
|
15572
|
+
await fs56.remove(file).catch(() => void 0);
|
|
15411
15573
|
return;
|
|
15412
15574
|
}
|
|
15413
15575
|
const record = { slug, finishedAt: (/* @__PURE__ */ new Date()).toISOString(), ok, failures };
|
|
15414
|
-
await
|
|
15415
|
-
await
|
|
15576
|
+
await fs56.ensureDir(path77.dirname(file));
|
|
15577
|
+
await fs56.writeJson(file, record, { spaces: 2 }).catch(() => void 0);
|
|
15416
15578
|
}
|
|
15417
15579
|
|
|
15418
15580
|
// src/workspace/worktree/dematerialize.ts
|
|
15419
|
-
import
|
|
15420
|
-
import
|
|
15581
|
+
import path79 from "node:path";
|
|
15582
|
+
import fs58 from "fs-extra";
|
|
15421
15583
|
|
|
15422
15584
|
// src/workspace/worktree/member-targets.ts
|
|
15423
|
-
import
|
|
15424
|
-
import
|
|
15585
|
+
import path78 from "node:path";
|
|
15586
|
+
import fs57 from "fs-extra";
|
|
15425
15587
|
function normalizeStateMembers(state, allMembers) {
|
|
15426
15588
|
return state?.members.map((m) => ({ id: m.id, folder: m.folder, absPath: m.absPath })) ?? allMembers.map((m) => ({ id: m.id, folder: m.folder, absPath: m.absPath }));
|
|
15427
15589
|
}
|
|
@@ -15438,7 +15600,7 @@ function resolveMemberTargets(wsWorktreePath, stateMembers, allMembers) {
|
|
|
15438
15600
|
targets.push({
|
|
15439
15601
|
repo: configMember?.id ?? stateMember.id,
|
|
15440
15602
|
ownerRepo,
|
|
15441
|
-
path:
|
|
15603
|
+
path: path78.join(wsWorktreePath, stateMember.folder)
|
|
15442
15604
|
});
|
|
15443
15605
|
}
|
|
15444
15606
|
return { targets, unresolvedDroppedMembers };
|
|
@@ -15446,7 +15608,7 @@ function resolveMemberTargets(wsWorktreePath, stateMembers, allMembers) {
|
|
|
15446
15608
|
async function filterExistingTargets(targets) {
|
|
15447
15609
|
const existing = [];
|
|
15448
15610
|
for (const target of targets) {
|
|
15449
|
-
if (await
|
|
15611
|
+
if (await fs57.pathExists(target.path)) existing.push(target);
|
|
15450
15612
|
}
|
|
15451
15613
|
return existing;
|
|
15452
15614
|
}
|
|
@@ -15491,7 +15653,7 @@ async function runDematerialize(opts) {
|
|
|
15491
15653
|
} catch (err) {
|
|
15492
15654
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
15493
15655
|
}
|
|
15494
|
-
if (!await
|
|
15656
|
+
if (!await fs58.pathExists(wsWorktreePath)) {
|
|
15495
15657
|
return { ok: false, error: `No workspace worktree found at ${wsWorktreePath}.` };
|
|
15496
15658
|
}
|
|
15497
15659
|
const lock = await readHydrateLock(workspaceRoot, slug);
|
|
@@ -15524,9 +15686,9 @@ async function runDematerialize(opts) {
|
|
|
15524
15686
|
const removed = [];
|
|
15525
15687
|
const failed = [];
|
|
15526
15688
|
for (const folder of unresolvedDroppedMembers) {
|
|
15527
|
-
const droppedPath =
|
|
15528
|
-
if (await
|
|
15529
|
-
await
|
|
15689
|
+
const droppedPath = path79.join(wsWorktreePath, folder);
|
|
15690
|
+
if (await fs58.pathExists(droppedPath)) {
|
|
15691
|
+
await fs58.remove(droppedPath);
|
|
15530
15692
|
failed.push({ repo: folder, error: unresolvedDroppedFailureReason() });
|
|
15531
15693
|
}
|
|
15532
15694
|
}
|
|
@@ -15540,26 +15702,26 @@ async function runDematerialize(opts) {
|
|
|
15540
15702
|
}
|
|
15541
15703
|
|
|
15542
15704
|
// src/workspace/worktree/list.ts
|
|
15543
|
-
import
|
|
15544
|
-
import
|
|
15705
|
+
import path80 from "node:path";
|
|
15706
|
+
import fs59 from "fs-extra";
|
|
15545
15707
|
async function runList2() {
|
|
15546
15708
|
const resolved = await resolveWorkspaceForWorktree();
|
|
15547
15709
|
if (!resolved.ok) return { ok: false, error: resolved.reason };
|
|
15548
15710
|
const { rootInfo, workspaceRoot } = resolved;
|
|
15549
15711
|
const dir = worktreesDir(workspaceRoot);
|
|
15550
|
-
if (!await
|
|
15551
|
-
const entries = await
|
|
15712
|
+
if (!await fs59.pathExists(dir)) return { ok: true, workspaceRoot, worktrees: [] };
|
|
15713
|
+
const entries = await fs59.readdir(dir, { withFileTypes: true });
|
|
15552
15714
|
const slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
15553
15715
|
const configuredMembers = await readMembers(rootInfo);
|
|
15554
15716
|
const worktrees = [];
|
|
15555
15717
|
for (const slug of slugs) {
|
|
15556
|
-
const wtPath =
|
|
15718
|
+
const wtPath = path80.join(dir, slug);
|
|
15557
15719
|
const state = await readWorktreeState(wtPath);
|
|
15558
15720
|
const members = [];
|
|
15559
15721
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
15560
15722
|
for (const m of source) {
|
|
15561
|
-
const memberPath =
|
|
15562
|
-
const materialized = await
|
|
15723
|
+
const memberPath = path80.join(wtPath, m.folder);
|
|
15724
|
+
const materialized = await fs59.pathExists(memberPath);
|
|
15563
15725
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
15564
15726
|
members.push({
|
|
15565
15727
|
id: m.id,
|
|
@@ -15581,23 +15743,23 @@ async function runList2() {
|
|
|
15581
15743
|
|
|
15582
15744
|
// src/workspace/worktree/reclaim.ts
|
|
15583
15745
|
import { existsSync as existsSync8 } from "node:fs";
|
|
15584
|
-
import
|
|
15585
|
-
import
|
|
15746
|
+
import path81 from "node:path";
|
|
15747
|
+
import fs60 from "fs-extra";
|
|
15586
15748
|
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
15587
15749
|
async function lastTouchedMs(worktreeRoot) {
|
|
15588
|
-
const candidates = [worktreeRoot,
|
|
15750
|
+
const candidates = [worktreeRoot, path81.join(worktreeRoot, ".haus-worktree.json")];
|
|
15589
15751
|
try {
|
|
15590
|
-
const dotGit = await
|
|
15752
|
+
const dotGit = await fs60.readFile(path81.join(worktreeRoot, ".git"), "utf8");
|
|
15591
15753
|
const match = /^gitdir:\s*(.+)$/m.exec(dotGit);
|
|
15592
15754
|
if (match?.[1]) {
|
|
15593
15755
|
const gitdir = match[1].trim();
|
|
15594
|
-
candidates.push(
|
|
15756
|
+
candidates.push(path81.join(gitdir, "HEAD"), path81.join(gitdir, "index"));
|
|
15595
15757
|
}
|
|
15596
15758
|
} catch {
|
|
15597
15759
|
}
|
|
15598
15760
|
let newest = 0;
|
|
15599
15761
|
for (const candidate of candidates) {
|
|
15600
|
-
const stat = await
|
|
15762
|
+
const stat = await fs60.stat(candidate).catch(() => void 0);
|
|
15601
15763
|
if (stat && stat.mtimeMs > newest) newest = stat.mtimeMs;
|
|
15602
15764
|
}
|
|
15603
15765
|
return newest;
|
|
@@ -15624,7 +15786,7 @@ async function runReclaim(opts = {}) {
|
|
|
15624
15786
|
if (!existsSync8(dir)) {
|
|
15625
15787
|
return { ok: true, dryRun: !opts.write, olderThanDays, candidates: [], reclaimed: 0 };
|
|
15626
15788
|
}
|
|
15627
|
-
const allEntries = (await
|
|
15789
|
+
const allEntries = (await fs60.readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
15628
15790
|
const entries = allEntries.filter((name) => isValidWorktreeSlug(name));
|
|
15629
15791
|
const unaddressable = allEntries.filter((name) => !isValidWorktreeSlug(name));
|
|
15630
15792
|
const members = await readMembers(rootInfo);
|
|
@@ -15634,17 +15796,17 @@ async function runReclaim(opts = {}) {
|
|
|
15634
15796
|
for (const name of unaddressable) {
|
|
15635
15797
|
candidates.push({
|
|
15636
15798
|
slug: name,
|
|
15637
|
-
ageDays: (Date.now() - await lastTouchedMs(
|
|
15799
|
+
ageDays: (Date.now() - await lastTouchedMs(path81.join(dir, name))) / MS_PER_DAY,
|
|
15638
15800
|
materializedMembers: 0,
|
|
15639
15801
|
action: "skip",
|
|
15640
15802
|
reason: "directory name is not a valid worktree slug \u2014 no haus worktree command can address it; remove it by hand if it is debris"
|
|
15641
15803
|
});
|
|
15642
15804
|
}
|
|
15643
15805
|
for (const slug of entries) {
|
|
15644
|
-
const wtPath =
|
|
15806
|
+
const wtPath = path81.join(dir, slug);
|
|
15645
15807
|
const state = await readWorktreeState(wtPath);
|
|
15646
15808
|
const stateMembers = normalizeStateMembers(state, members);
|
|
15647
|
-
const materialized = stateMembers.filter((m) => existsSync8(
|
|
15809
|
+
const materialized = stateMembers.filter((m) => existsSync8(path81.join(wtPath, m.folder)));
|
|
15648
15810
|
const ageDays = (Date.now() - await lastTouchedMs(wtPath)) / MS_PER_DAY;
|
|
15649
15811
|
const base = { slug, ageDays, materializedMembers: materialized.length };
|
|
15650
15812
|
if (slug === currentSlug) {
|
|
@@ -15705,13 +15867,13 @@ async function runReclaim(opts = {}) {
|
|
|
15705
15867
|
}
|
|
15706
15868
|
|
|
15707
15869
|
// src/workspace/worktree/remove.ts
|
|
15708
|
-
import
|
|
15870
|
+
import fs61 from "fs-extra";
|
|
15709
15871
|
async function runRemove2(opts) {
|
|
15710
15872
|
const resolved = await resolveWorkspaceForWorktree();
|
|
15711
15873
|
if (!resolved.ok) return { ok: false, error: resolved.reason };
|
|
15712
15874
|
const { rootInfo, workspaceRoot } = resolved;
|
|
15713
15875
|
const wsWorktreePath = worktreePath(workspaceRoot, opts.slug);
|
|
15714
|
-
if (!await
|
|
15876
|
+
if (!await fs61.pathExists(wsWorktreePath)) {
|
|
15715
15877
|
return { ok: false, error: `No workspace worktree found at ${wsWorktreePath}.` };
|
|
15716
15878
|
}
|
|
15717
15879
|
const state = await readWorktreeState(wsWorktreePath);
|
|
@@ -15779,8 +15941,8 @@ async function runRemove2(opts) {
|
|
|
15779
15941
|
}
|
|
15780
15942
|
await pruneWorktrees(workspaceRoot);
|
|
15781
15943
|
}
|
|
15782
|
-
if (wsRemoveOk && await
|
|
15783
|
-
await
|
|
15944
|
+
if (wsRemoveOk && await fs61.pathExists(wsWorktreePath)) {
|
|
15945
|
+
await fs61.remove(wsWorktreePath);
|
|
15784
15946
|
}
|
|
15785
15947
|
return { ok: failed.length === 0, dryRun: false, slug: opts.slug, removed, failed };
|
|
15786
15948
|
}
|
|
@@ -16241,7 +16403,7 @@ async function runWorktree(action, opts = {}) {
|
|
|
16241
16403
|
|
|
16242
16404
|
// src/commands/workspace.ts
|
|
16243
16405
|
import { existsSync as existsSync12, statSync as statSync3 } from "node:fs";
|
|
16244
|
-
import
|
|
16406
|
+
import path90 from "node:path";
|
|
16245
16407
|
init_fs();
|
|
16246
16408
|
init_logger();
|
|
16247
16409
|
|
|
@@ -16300,7 +16462,7 @@ ${summaries.map(
|
|
|
16300
16462
|
|
|
16301
16463
|
// src/commands/workspace/doctor.ts
|
|
16302
16464
|
import { existsSync as existsSync9 } from "node:fs";
|
|
16303
|
-
import
|
|
16465
|
+
import path83 from "node:path";
|
|
16304
16466
|
init_logger();
|
|
16305
16467
|
init_paths();
|
|
16306
16468
|
|
|
@@ -16308,14 +16470,14 @@ init_paths();
|
|
|
16308
16470
|
init_fs();
|
|
16309
16471
|
init_paths();
|
|
16310
16472
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
16311
|
-
import
|
|
16473
|
+
import path82 from "node:path";
|
|
16312
16474
|
var MANIFEST_FILE2 = "workspace.manifest.json";
|
|
16313
16475
|
function manifestPath(workspaceRoot) {
|
|
16314
16476
|
return hausPath(workspaceRoot, MANIFEST_FILE2);
|
|
16315
16477
|
}
|
|
16316
16478
|
function hausVersion() {
|
|
16317
16479
|
try {
|
|
16318
|
-
const pkg = JSON.parse(readFileSync4(
|
|
16480
|
+
const pkg = JSON.parse(readFileSync4(path82.join(packageRoot(), "package.json"), "utf8"));
|
|
16319
16481
|
return pkg.version ?? "0.0.0";
|
|
16320
16482
|
} catch {
|
|
16321
16483
|
return "0.0.0";
|
|
@@ -16407,7 +16569,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
16407
16569
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
16408
16570
|
const catalogRefByRepo = [];
|
|
16409
16571
|
for (const repo of config.repos) {
|
|
16410
|
-
const repoRoot =
|
|
16572
|
+
const repoRoot = path83.resolve(workspaceRoot, repo.path);
|
|
16411
16573
|
const entry = manifestByName.get(repo.name);
|
|
16412
16574
|
if (!entry) {
|
|
16413
16575
|
flag({
|
|
@@ -16480,7 +16642,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
16480
16642
|
});
|
|
16481
16643
|
continue;
|
|
16482
16644
|
}
|
|
16483
|
-
if (!existsSync9(
|
|
16645
|
+
if (!existsSync9(path83.join(workspaceRoot, entry.path))) {
|
|
16484
16646
|
flag({
|
|
16485
16647
|
repo: entry.repo,
|
|
16486
16648
|
kind: "missing-linked-context-copy",
|
|
@@ -16526,18 +16688,18 @@ function emit(args) {
|
|
|
16526
16688
|
init_logger();
|
|
16527
16689
|
|
|
16528
16690
|
// src/workspace/link-context/link.ts
|
|
16529
|
-
import
|
|
16530
|
-
import
|
|
16691
|
+
import path86 from "node:path";
|
|
16692
|
+
import fs64 from "fs-extra";
|
|
16531
16693
|
init_fs();
|
|
16532
16694
|
init_logger();
|
|
16533
16695
|
|
|
16534
16696
|
// src/workspace/link-context/plan.ts
|
|
16535
|
-
import
|
|
16536
|
-
import
|
|
16697
|
+
import path85 from "node:path";
|
|
16698
|
+
import fs63 from "fs-extra";
|
|
16537
16699
|
|
|
16538
16700
|
// src/workspace/link-context/scan-source.ts
|
|
16539
|
-
import
|
|
16540
|
-
import
|
|
16701
|
+
import path84 from "node:path";
|
|
16702
|
+
import fs62 from "fs-extra";
|
|
16541
16703
|
var ASSET_SUBDIR = {
|
|
16542
16704
|
skill: "skills",
|
|
16543
16705
|
agent: "agents",
|
|
@@ -16545,36 +16707,36 @@ var ASSET_SUBDIR = {
|
|
|
16545
16707
|
};
|
|
16546
16708
|
async function hasLinkableContext(member) {
|
|
16547
16709
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
16548
|
-
if (await
|
|
16710
|
+
if (await fs62.pathExists(path84.join(member.absPath, ".claude", subdir))) return true;
|
|
16549
16711
|
}
|
|
16550
16712
|
return false;
|
|
16551
16713
|
}
|
|
16552
16714
|
async function collectSkills(member) {
|
|
16553
|
-
const dir =
|
|
16554
|
-
if (!await
|
|
16555
|
-
const names = await
|
|
16715
|
+
const dir = path84.join(member.absPath, ".claude", "skills");
|
|
16716
|
+
if (!await fs62.pathExists(dir)) return [];
|
|
16717
|
+
const names = await fs62.readdir(dir);
|
|
16556
16718
|
const out = [];
|
|
16557
16719
|
for (const name of [...names].sort()) {
|
|
16558
|
-
const abs =
|
|
16559
|
-
const stat = await
|
|
16720
|
+
const abs = path84.join(dir, name);
|
|
16721
|
+
const stat = await fs62.lstat(abs);
|
|
16560
16722
|
if (!stat.isDirectory()) continue;
|
|
16561
|
-
if (!await
|
|
16562
|
-
out.push({ type: "skill", name, sourceRelPath:
|
|
16723
|
+
if (!await fs62.pathExists(path84.join(abs, "SKILL.md"))) continue;
|
|
16724
|
+
out.push({ type: "skill", name, sourceRelPath: path84.posix.join(".claude", "skills", name) });
|
|
16563
16725
|
}
|
|
16564
16726
|
return out;
|
|
16565
16727
|
}
|
|
16566
16728
|
async function collectFlatMarkdown(member, type) {
|
|
16567
16729
|
const subdir = ASSET_SUBDIR[type];
|
|
16568
|
-
const dir =
|
|
16569
|
-
if (!await
|
|
16570
|
-
const names = await
|
|
16730
|
+
const dir = path84.join(member.absPath, ".claude", subdir);
|
|
16731
|
+
if (!await fs62.pathExists(dir)) return [];
|
|
16732
|
+
const names = await fs62.readdir(dir);
|
|
16571
16733
|
const out = [];
|
|
16572
16734
|
for (const fileName of [...names].sort()) {
|
|
16573
16735
|
if (!fileName.endsWith(".md")) continue;
|
|
16574
|
-
const abs =
|
|
16575
|
-
if (!(await
|
|
16736
|
+
const abs = path84.join(dir, fileName);
|
|
16737
|
+
if (!(await fs62.lstat(abs)).isFile()) continue;
|
|
16576
16738
|
const name = fileName.slice(0, -".md".length);
|
|
16577
|
-
out.push({ type, name, sourceRelPath:
|
|
16739
|
+
out.push({ type, name, sourceRelPath: path84.posix.join(".claude", subdir, fileName) });
|
|
16578
16740
|
}
|
|
16579
16741
|
return out;
|
|
16580
16742
|
}
|
|
@@ -16589,9 +16751,9 @@ async function collectSourceAssets(member) {
|
|
|
16589
16751
|
|
|
16590
16752
|
// src/workspace/link-context/plan.ts
|
|
16591
16753
|
function repoFolderPrefix(member) {
|
|
16592
|
-
const base =
|
|
16754
|
+
const base = path85.posix.basename(member.folder);
|
|
16593
16755
|
if (base && base !== "." && base !== "/") return base;
|
|
16594
|
-
const fromAbs =
|
|
16756
|
+
const fromAbs = path85.basename(member.absPath);
|
|
16595
16757
|
return fromAbs || member.id;
|
|
16596
16758
|
}
|
|
16597
16759
|
function assetKey(type, destKey) {
|
|
@@ -16606,7 +16768,7 @@ async function buildLinkPlan(members) {
|
|
|
16606
16768
|
const byKey = /* @__PURE__ */ new Map();
|
|
16607
16769
|
const membersById = new Map(members.map((m) => [m.id, m]));
|
|
16608
16770
|
for (const member of members) {
|
|
16609
|
-
if (!await
|
|
16771
|
+
if (!await fs63.pathExists(member.absPath)) {
|
|
16610
16772
|
skipped.push({ repo: member.id, reason: `not cloned locally (${member.absPath}) \u2014 skipped` });
|
|
16611
16773
|
continue;
|
|
16612
16774
|
}
|
|
@@ -16648,7 +16810,7 @@ async function buildLinkPlan(members) {
|
|
|
16648
16810
|
name: asset.name,
|
|
16649
16811
|
destKey,
|
|
16650
16812
|
sourceRelPath: asset.sourceRelPath,
|
|
16651
|
-
sourceAbsPath:
|
|
16813
|
+
sourceAbsPath: path85.join(member.absPath, asset.sourceRelPath),
|
|
16652
16814
|
sourceHash
|
|
16653
16815
|
});
|
|
16654
16816
|
}
|
|
@@ -16659,7 +16821,7 @@ async function buildLinkPlan(members) {
|
|
|
16659
16821
|
function destRelPathFor(type, destKey) {
|
|
16660
16822
|
const subdir = `${type}s`;
|
|
16661
16823
|
const leaf = type === "skill" ? destKey : `${destKey}.md`;
|
|
16662
|
-
return
|
|
16824
|
+
return path86.posix.join(".claude", subdir, leaf);
|
|
16663
16825
|
}
|
|
16664
16826
|
async function runLinkContext(workspaceRoot, opts = {}) {
|
|
16665
16827
|
const write = resolveWriteMode(opts, "preview-by-default").kind === "write";
|
|
@@ -16685,17 +16847,17 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
16685
16847
|
const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
16686
16848
|
for (const entry of plan.entries) {
|
|
16687
16849
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
16688
|
-
const destAbs =
|
|
16850
|
+
const destAbs = path86.join(workspaceRoot, destRel);
|
|
16689
16851
|
if (write) {
|
|
16690
|
-
await
|
|
16691
|
-
if (await
|
|
16692
|
-
await
|
|
16852
|
+
await fs64.ensureDir(path86.dirname(destAbs));
|
|
16853
|
+
if (await fs64.pathExists(destAbs)) await fs64.remove(destAbs);
|
|
16854
|
+
await fs64.copy(entry.sourceAbsPath, destAbs, {
|
|
16693
16855
|
overwrite: true,
|
|
16694
16856
|
errorOnExist: false,
|
|
16695
16857
|
dereference: false,
|
|
16696
16858
|
filter: (s) => {
|
|
16697
16859
|
try {
|
|
16698
|
-
return !
|
|
16860
|
+
return !fs64.lstatSync(s).isSymbolicLink();
|
|
16699
16861
|
} catch {
|
|
16700
16862
|
return false;
|
|
16701
16863
|
}
|
|
@@ -16724,7 +16886,7 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
16724
16886
|
warn(`Refusing to remove linked context path outside the workspace root: ${old.path}`);
|
|
16725
16887
|
continue;
|
|
16726
16888
|
}
|
|
16727
|
-
if (await
|
|
16889
|
+
if (await fs64.pathExists(abs)) await fs64.remove(abs);
|
|
16728
16890
|
}
|
|
16729
16891
|
}
|
|
16730
16892
|
if (write) {
|
|
@@ -16776,11 +16938,11 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
|
|
|
16776
16938
|
|
|
16777
16939
|
// src/commands/workspace/setup.ts
|
|
16778
16940
|
import { existsSync as existsSync10, statSync } from "node:fs";
|
|
16779
|
-
import
|
|
16941
|
+
import path88 from "node:path";
|
|
16780
16942
|
|
|
16781
16943
|
// src/claude/write-workspace-claude-md.ts
|
|
16782
|
-
import
|
|
16783
|
-
import
|
|
16944
|
+
import path87 from "node:path";
|
|
16945
|
+
import fs65 from "fs-extra";
|
|
16784
16946
|
init_fs();
|
|
16785
16947
|
init_logger();
|
|
16786
16948
|
init_paths();
|
|
@@ -16805,11 +16967,11 @@ ${WORKSPACE_BLOCK_END}`;
|
|
|
16805
16967
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
16806
16968
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
16807
16969
|
const dryRun = opts.dryRun ?? false;
|
|
16808
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
16970
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path87.join(workspaceRoot, "CLAUDE.md");
|
|
16809
16971
|
const say = opts.quiet ? () => {
|
|
16810
16972
|
} : log;
|
|
16811
|
-
const existed = await
|
|
16812
|
-
const prev = existed ? await
|
|
16973
|
+
const existed = await fs65.pathExists(filePath);
|
|
16974
|
+
const prev = existed ? await fs65.readFile(filePath, "utf8") : "";
|
|
16813
16975
|
const next = opts.collision ? `${block}
|
|
16814
16976
|
` : injectHausBlock(prev, block, WORKSPACE_SENTINELS);
|
|
16815
16977
|
const printable = displayPath(workspaceRoot, filePath);
|
|
@@ -16838,20 +17000,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
16838
17000
|
init_fs();
|
|
16839
17001
|
init_logger();
|
|
16840
17002
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
16841
|
-
let dir =
|
|
17003
|
+
let dir = path88.resolve(start);
|
|
16842
17004
|
for (; ; ) {
|
|
16843
|
-
if (existsSync10(
|
|
16844
|
-
const parent =
|
|
16845
|
-
if (parent === dir) return
|
|
17005
|
+
if (existsSync10(path88.join(dir, WORKSPACE_FILE))) return dir;
|
|
17006
|
+
const parent = path88.dirname(dir);
|
|
17007
|
+
if (parent === dir) return path88.resolve(start);
|
|
16846
17008
|
dir = parent;
|
|
16847
17009
|
}
|
|
16848
17010
|
}
|
|
16849
17011
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
16850
|
-
return
|
|
17012
|
+
return path88.resolve(workspaceRoot, repoPath) === path88.resolve(workspaceRoot);
|
|
16851
17013
|
}
|
|
16852
17014
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
16853
17015
|
const apply = options.write ?? false;
|
|
16854
|
-
const configText = await readText(
|
|
17016
|
+
const configText = await readText(path88.join(workspaceRoot, WORKSPACE_FILE));
|
|
16855
17017
|
if (!configText) {
|
|
16856
17018
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
16857
17019
|
process.exitCode = 1;
|
|
@@ -16878,7 +17040,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
16878
17040
|
const statuses = [];
|
|
16879
17041
|
const aggregateInputs = [];
|
|
16880
17042
|
for (const repo of repos) {
|
|
16881
|
-
const repoRoot =
|
|
17043
|
+
const repoRoot = path88.resolve(workspaceRoot, repo.path);
|
|
16882
17044
|
say(`
|
|
16883
17045
|
\u2192 ${repo.name} (${repo.path})`);
|
|
16884
17046
|
let res;
|
|
@@ -16994,7 +17156,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
16994
17156
|
const status = statusByName.get(repo.name);
|
|
16995
17157
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
16996
17158
|
if (status?.status === "ok") {
|
|
16997
|
-
const lock = await checkLock(
|
|
17159
|
+
const lock = await checkLock(path88.resolve(workspaceRoot, repo.path));
|
|
16998
17160
|
manifestRepos.push({
|
|
16999
17161
|
name: repo.name,
|
|
17000
17162
|
path: repo.path,
|
|
@@ -17050,7 +17212,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
17050
17212
|
try {
|
|
17051
17213
|
const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
|
|
17052
17214
|
if (linkResult.ok) {
|
|
17053
|
-
written.push(...linkResult.added.map((rel) =>
|
|
17215
|
+
written.push(...linkResult.added.map((rel) => path88.join(workspaceRoot, rel)));
|
|
17054
17216
|
if (!options.json) {
|
|
17055
17217
|
say(
|
|
17056
17218
|
`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` : ""}.`
|
|
@@ -17083,8 +17245,8 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
17083
17245
|
|
|
17084
17246
|
// src/commands/workspace/undo.ts
|
|
17085
17247
|
import { existsSync as existsSync11, statSync as statSync2 } from "node:fs";
|
|
17086
|
-
import
|
|
17087
|
-
import
|
|
17248
|
+
import path89 from "node:path";
|
|
17249
|
+
import fs66 from "fs-extra";
|
|
17088
17250
|
init_fs();
|
|
17089
17251
|
init_logger();
|
|
17090
17252
|
init_paths();
|
|
@@ -17114,7 +17276,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
17114
17276
|
}
|
|
17115
17277
|
if (options.dryRun) {
|
|
17116
17278
|
for (const repo of config.repos) {
|
|
17117
|
-
const repoRoot =
|
|
17279
|
+
const repoRoot = path89.resolve(workspaceRoot, repo.path);
|
|
17118
17280
|
log(`
|
|
17119
17281
|
\u2192 ${repo.name} (${repo.path})`);
|
|
17120
17282
|
try {
|
|
@@ -17135,7 +17297,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
17135
17297
|
warn(`Refusing to remove linked context path outside the workspace root: ${entry.path}`);
|
|
17136
17298
|
continue;
|
|
17137
17299
|
}
|
|
17138
|
-
if (await
|
|
17300
|
+
if (await fs66.pathExists(abs)) {
|
|
17139
17301
|
log(`[dry-run] Would remove ${entry.path} (linked ${entry.type} from ${entry.repo})`);
|
|
17140
17302
|
}
|
|
17141
17303
|
}
|
|
@@ -17145,12 +17307,12 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
17145
17307
|
hausPath(workspaceRoot, "WORKSPACE.md")
|
|
17146
17308
|
];
|
|
17147
17309
|
for (const abs of workspaceArtifacts) {
|
|
17148
|
-
if (await
|
|
17149
|
-
log(`[dry-run] Would remove ${
|
|
17310
|
+
if (await fs66.pathExists(abs)) {
|
|
17311
|
+
log(`[dry-run] Would remove ${path89.relative(workspaceRoot, abs)}`);
|
|
17150
17312
|
}
|
|
17151
17313
|
}
|
|
17152
|
-
if (!await
|
|
17153
|
-
const claudeMd = await readText(
|
|
17314
|
+
if (!await fs66.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
|
|
17315
|
+
const claudeMd = await readText(path89.join(workspaceRoot, "CLAUDE.md"));
|
|
17154
17316
|
if (claudeMd?.includes(WORKSPACE_BLOCK_BEGIN) || claudeMd && hasLegacyWorkspaceBlock(claudeMd)) {
|
|
17155
17317
|
log("[dry-run] Would remove the workspace haus import block from CLAUDE.md");
|
|
17156
17318
|
}
|
|
@@ -17169,7 +17331,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
17169
17331
|
}
|
|
17170
17332
|
let anyRepoFailed = false;
|
|
17171
17333
|
for (const repo of config.repos) {
|
|
17172
|
-
const repoRoot =
|
|
17334
|
+
const repoRoot = path89.resolve(workspaceRoot, repo.path);
|
|
17173
17335
|
log(`
|
|
17174
17336
|
\u2192 ${repo.name} (${repo.path})`);
|
|
17175
17337
|
try {
|
|
@@ -17189,29 +17351,29 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
17189
17351
|
warn(`Refusing to remove linked context path outside the workspace root: ${entry.path}`);
|
|
17190
17352
|
continue;
|
|
17191
17353
|
}
|
|
17192
|
-
if (await
|
|
17193
|
-
await
|
|
17354
|
+
if (await fs66.pathExists(abs)) {
|
|
17355
|
+
await fs66.remove(abs);
|
|
17194
17356
|
log(`Removed ${entry.path} (linked ${entry.type} from ${entry.repo})`);
|
|
17195
17357
|
}
|
|
17196
17358
|
}
|
|
17197
17359
|
for (const rel of WORKSPACE_AGGREGATE_FILES) {
|
|
17198
17360
|
const abs = hausPath(workspaceRoot, rel);
|
|
17199
|
-
if (await
|
|
17200
|
-
await
|
|
17201
|
-
log(`Removed ${
|
|
17361
|
+
if (await fs66.pathExists(abs)) {
|
|
17362
|
+
await fs66.remove(abs);
|
|
17363
|
+
log(`Removed ${path89.relative(workspaceRoot, abs)}`);
|
|
17202
17364
|
}
|
|
17203
17365
|
}
|
|
17204
17366
|
const manifestFile = manifestPath(workspaceRoot);
|
|
17205
|
-
if (await
|
|
17206
|
-
await
|
|
17207
|
-
log(`Removed ${
|
|
17367
|
+
if (await fs66.pathExists(manifestFile)) {
|
|
17368
|
+
await fs66.remove(manifestFile);
|
|
17369
|
+
log(`Removed ${path89.relative(workspaceRoot, manifestFile)}`);
|
|
17208
17370
|
}
|
|
17209
17371
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
17210
|
-
if (await
|
|
17211
|
-
await
|
|
17212
|
-
log(`Removed ${
|
|
17372
|
+
if (await fs66.pathExists(workspaceMdPath)) {
|
|
17373
|
+
await fs66.remove(workspaceMdPath);
|
|
17374
|
+
log(`Removed ${path89.relative(workspaceRoot, workspaceMdPath)}`);
|
|
17213
17375
|
} else {
|
|
17214
|
-
const claudeMdPath =
|
|
17376
|
+
const claudeMdPath = path89.join(workspaceRoot, "CLAUDE.md");
|
|
17215
17377
|
if (existsSync11(claudeMdPath)) {
|
|
17216
17378
|
const prev = await readText(claudeMdPath);
|
|
17217
17379
|
if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
|
|
@@ -17220,19 +17382,19 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
17220
17382
|
end: WORKSPACE_BLOCK_END
|
|
17221
17383
|
});
|
|
17222
17384
|
if (next.length === 0) {
|
|
17223
|
-
await
|
|
17385
|
+
await fs66.remove(claudeMdPath);
|
|
17224
17386
|
log("Removed CLAUDE.md (only contained the workspace haus import block).");
|
|
17225
17387
|
} else {
|
|
17226
|
-
await
|
|
17388
|
+
await fs66.writeFile(claudeMdPath, next, "utf8");
|
|
17227
17389
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
17228
17390
|
}
|
|
17229
17391
|
} else if (prev && hasLegacyWorkspaceBlock(prev)) {
|
|
17230
17392
|
const next = stripHausBlock(prev, { begin: BLOCK_BEGIN, end: BLOCK_END });
|
|
17231
17393
|
if (next.length === 0) {
|
|
17232
|
-
await
|
|
17394
|
+
await fs66.remove(claudeMdPath);
|
|
17233
17395
|
log("Removed CLAUDE.md (only contained the pre-upgrade workspace haus import block).");
|
|
17234
17396
|
} else {
|
|
17235
|
-
await
|
|
17397
|
+
await fs66.writeFile(claudeMdPath, next, "utf8");
|
|
17236
17398
|
log(
|
|
17237
17399
|
"Removed pre-upgrade workspace haus import block from CLAUDE.md (user content preserved)."
|
|
17238
17400
|
);
|
|
@@ -17280,7 +17442,7 @@ relationships: []
|
|
|
17280
17442
|
log("Workspace initialized.");
|
|
17281
17443
|
}
|
|
17282
17444
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
17283
|
-
const configText = await readText(
|
|
17445
|
+
const configText = await readText(path90.join(workspaceRoot, WORKSPACE_FILE));
|
|
17284
17446
|
if (!configText) {
|
|
17285
17447
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
17286
17448
|
process.exitCode = 1;
|
|
@@ -17302,7 +17464,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
17302
17464
|
const inputs = [];
|
|
17303
17465
|
const failures = [];
|
|
17304
17466
|
for (const repo of config.repos) {
|
|
17305
|
-
const repoRoot =
|
|
17467
|
+
const repoRoot = path90.resolve(workspaceRoot, repo.path);
|
|
17306
17468
|
try {
|
|
17307
17469
|
if (!existsSync12(repoRoot) || !statSync3(repoRoot).isDirectory()) {
|
|
17308
17470
|
throw new Error("path is not a directory");
|
|
@@ -17371,7 +17533,7 @@ init_logger();
|
|
|
17371
17533
|
init_paths();
|
|
17372
17534
|
function cliVersion() {
|
|
17373
17535
|
try {
|
|
17374
|
-
const pkgPath =
|
|
17536
|
+
const pkgPath = path91.join(packageRoot(), "package.json");
|
|
17375
17537
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
17376
17538
|
return pkg.version ?? "0.0.0";
|
|
17377
17539
|
} catch {
|
|
@@ -17381,7 +17543,7 @@ function cliVersion() {
|
|
|
17381
17543
|
var program = new Command();
|
|
17382
17544
|
function validateRuntimeNodeVersion() {
|
|
17383
17545
|
try {
|
|
17384
|
-
const pkgPath =
|
|
17546
|
+
const pkgPath = path91.join(packageRoot(), "package.json");
|
|
17385
17547
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
17386
17548
|
const requiredRange = pkg.engines?.node;
|
|
17387
17549
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|