@haus-tech/haus-workflow 3.1.2 → 4.0.1
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 +21 -0
- package/dist/cli.js +335 -216
- package/library/global/skills/haus-workflow/references/init.md +5 -5
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -1913,7 +1913,7 @@ var init_sync = __esm({
|
|
|
1913
1913
|
|
|
1914
1914
|
// src/cli.ts
|
|
1915
1915
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
1916
|
-
import
|
|
1916
|
+
import path80 from "node:path";
|
|
1917
1917
|
import { Command } from "commander";
|
|
1918
1918
|
|
|
1919
1919
|
// src/commands/apply.ts
|
|
@@ -3050,8 +3050,8 @@ function buildDenyRules() {
|
|
|
3050
3050
|
for (const command of DENY_COMMANDS) {
|
|
3051
3051
|
rules.push(`Bash(${command}:*)`);
|
|
3052
3052
|
}
|
|
3053
|
-
for (const
|
|
3054
|
-
const pattern = DENY_DIRS.has(
|
|
3053
|
+
for (const path81 of DENY_PATHS) {
|
|
3054
|
+
const pattern = DENY_DIRS.has(path81) ? `${path81}/**` : path81;
|
|
3055
3055
|
for (const tool of FILE_TOOLS) {
|
|
3056
3056
|
rules.push(`${tool}(${pattern})`);
|
|
3057
3057
|
}
|
|
@@ -4441,7 +4441,7 @@ async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, q
|
|
|
4441
4441
|
const relPaths = entry.paths ?? [];
|
|
4442
4442
|
const existing = await resolveExistingTrackedPaths(root, migration.oldId, relPaths, warn);
|
|
4443
4443
|
if (existing.length === 0) continue;
|
|
4444
|
-
const gate = await hashGateEntry(root, {
|
|
4444
|
+
const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
|
|
4445
4445
|
if (!gate.ok) {
|
|
4446
4446
|
if (gate.reason === "no-hash") {
|
|
4447
4447
|
warn(
|
|
@@ -4468,7 +4468,7 @@ async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, q
|
|
|
4468
4468
|
return;
|
|
4469
4469
|
}
|
|
4470
4470
|
const allAbsPaths = toRemove.flatMap(({ existing }) => existing.map(({ abs }) => abs));
|
|
4471
|
-
await backupTrackedPaths(root, allAbsPaths, say, "migrate");
|
|
4471
|
+
void await backupTrackedPaths(root, allAbsPaths, say, "migrate");
|
|
4472
4472
|
for (const { existing } of toRemove) {
|
|
4473
4473
|
for (const { abs } of existing) {
|
|
4474
4474
|
await fs22.remove(abs);
|
|
@@ -4487,11 +4487,13 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
|
|
|
4487
4487
|
);
|
|
4488
4488
|
let keepShared = false;
|
|
4489
4489
|
for (const entry of supersededEntries) {
|
|
4490
|
+
const id = entry.id;
|
|
4491
|
+
if (!id) continue;
|
|
4490
4492
|
const relPaths = entry.paths ?? [];
|
|
4491
4493
|
if (relPaths.length === 0) continue;
|
|
4492
|
-
const existing = await resolveExistingTrackedPaths(root,
|
|
4494
|
+
const existing = await resolveExistingTrackedPaths(root, id, relPaths, warn);
|
|
4493
4495
|
if (existing.length === 0) continue;
|
|
4494
|
-
const gate = await hashGateEntry(root, {
|
|
4496
|
+
const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
|
|
4495
4497
|
if (!gate.ok) keepShared = true;
|
|
4496
4498
|
}
|
|
4497
4499
|
for (const entry of prevLock) {
|
|
@@ -4503,12 +4505,15 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
|
|
|
4503
4505
|
if (!removedFromManifest && !deprecated && !superseded) continue;
|
|
4504
4506
|
const relPaths = entry.paths ?? [];
|
|
4505
4507
|
if (relPaths.length === 0) continue;
|
|
4506
|
-
if (await hasProtectedUnsafePath(root, relPaths)) refusedIds.add(entry.id);
|
|
4507
4508
|
const existing = await resolveExistingTrackedPaths(root, entry.id, relPaths, warn);
|
|
4509
|
+
if (await hasProtectedUnsafePath(root, relPaths)) {
|
|
4510
|
+
refusedIds.add(entry.id);
|
|
4511
|
+
continue;
|
|
4512
|
+
}
|
|
4508
4513
|
if (existing.length === 0) continue;
|
|
4509
4514
|
const pruneReason = superseded ? "superseded" : deprecated ? "deprecated" : "stale";
|
|
4510
4515
|
const label = superseded ? "Superseded" : deprecated ? "Deprecated" : "Stale";
|
|
4511
|
-
const gate = await hashGateEntry(root, {
|
|
4516
|
+
const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
|
|
4512
4517
|
if (!gate.ok) {
|
|
4513
4518
|
if (gate.reason === "no-hash") {
|
|
4514
4519
|
warn(
|
|
@@ -4533,7 +4538,7 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
|
|
|
4533
4538
|
return true;
|
|
4534
4539
|
}) : existing;
|
|
4535
4540
|
if (!dryRun && removable.length > 0) {
|
|
4536
|
-
await backupTrackedPaths(
|
|
4541
|
+
void await backupTrackedPaths(
|
|
4537
4542
|
root,
|
|
4538
4543
|
removable.map((e) => e.abs),
|
|
4539
4544
|
say,
|
|
@@ -4563,10 +4568,13 @@ async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun,
|
|
|
4563
4568
|
if (!entry.id) continue;
|
|
4564
4569
|
const relPaths = entry.paths ?? [];
|
|
4565
4570
|
if (relPaths.length === 0) continue;
|
|
4566
|
-
if (await hasProtectedUnsafePath(root, relPaths)) refusedIds.add(entry.id);
|
|
4567
4571
|
const existing = await resolveExistingTrackedPaths(root, entry.id, relPaths, warn);
|
|
4572
|
+
if (await hasProtectedUnsafePath(root, relPaths)) {
|
|
4573
|
+
refusedIds.add(entry.id);
|
|
4574
|
+
continue;
|
|
4575
|
+
}
|
|
4568
4576
|
if (existing.length === 0) continue;
|
|
4569
|
-
const gate = await hashGateEntry(root, {
|
|
4577
|
+
const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
|
|
4570
4578
|
if (!gate.ok) {
|
|
4571
4579
|
const detail = gate.reason === "no-hash" ? "has no lock hash" : "was modified locally";
|
|
4572
4580
|
warn(
|
|
@@ -4587,7 +4595,7 @@ async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun,
|
|
|
4587
4595
|
return refusedIds;
|
|
4588
4596
|
}
|
|
4589
4597
|
const allAbsPaths = toRemove.flatMap(({ existing }) => existing.map(({ abs }) => abs));
|
|
4590
|
-
await backupTrackedPaths(root, allAbsPaths, say, "prune");
|
|
4598
|
+
void await backupTrackedPaths(root, allAbsPaths, say, "prune");
|
|
4591
4599
|
for (const { id, existing } of toRemove) {
|
|
4592
4600
|
await deleteTrackedPaths(root, id, existing, { verb: "Pruned", reason: "orphaned" }, say);
|
|
4593
4601
|
}
|
|
@@ -4604,7 +4612,8 @@ var GITIGNORED_ARTIFACT_PATHS = [
|
|
|
4604
4612
|
".haus-workflow/context-map.json",
|
|
4605
4613
|
".haus-workflow/recommendation.json",
|
|
4606
4614
|
".haus-workflow/sources-report.json",
|
|
4607
|
-
".haus-workflow/deep-context.json"
|
|
4615
|
+
".haus-workflow/deep-context.json",
|
|
4616
|
+
".haus-workflow/backups/"
|
|
4608
4617
|
];
|
|
4609
4618
|
function findLineMarker3(content2, marker, from = 0) {
|
|
4610
4619
|
let idx = content2.indexOf(marker, from);
|
|
@@ -4649,7 +4658,7 @@ function buildBlockFor(existing, beginMarker, endMarker, comment, patterns) {
|
|
|
4649
4658
|
if (needed.length === 0) return null;
|
|
4650
4659
|
return [beginMarker, comment, ...needed, endMarker].join("\n");
|
|
4651
4660
|
}
|
|
4652
|
-
var ARTIFACT_BLOCK_COMMENT = "# haus
|
|
4661
|
+
var ARTIFACT_BLOCK_COMMENT = "# haus machine-local artifacts \u2014 must never be committed (see ADR-0025, ADR-0058)";
|
|
4653
4662
|
function injectBlockFor(existing, beginMarker, endMarker, comment, patterns) {
|
|
4654
4663
|
const block = buildBlockFor(existing, beginMarker, endMarker, comment, patterns);
|
|
4655
4664
|
const range = findBlockRangeFor(existing, beginMarker, endMarker);
|
|
@@ -4742,7 +4751,7 @@ async function untrackMachineLocalArtifacts(root, dryRun, notify) {
|
|
|
4742
4751
|
for (const rel of tracked) {
|
|
4743
4752
|
if (dryRun) {
|
|
4744
4753
|
notify(
|
|
4745
|
-
`Would untrack ${rel} from git \u2014 it
|
|
4754
|
+
`Would untrack ${rel} from git \u2014 it is machine-local haus output and should never have been committed.`
|
|
4746
4755
|
);
|
|
4747
4756
|
continue;
|
|
4748
4757
|
}
|
|
@@ -4750,7 +4759,7 @@ async function untrackMachineLocalArtifacts(root, dryRun, notify) {
|
|
|
4750
4759
|
if (result.exitCode === 0) {
|
|
4751
4760
|
untracked.push(rel);
|
|
4752
4761
|
notify(
|
|
4753
|
-
`Untracked ${rel} from git \u2014 it
|
|
4762
|
+
`Untracked ${rel} from git \u2014 it is machine-local haus output and should never have been committed. Run \`git status\` to review before committing this change.`
|
|
4754
4763
|
);
|
|
4755
4764
|
} else {
|
|
4756
4765
|
notify(`Could not untrack ${rel} from git: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
@@ -7665,12 +7674,12 @@ async function runDoctor(options) {
|
|
|
7665
7674
|
const trackedArtifacts = await listTrackedArtifactPaths(root);
|
|
7666
7675
|
if (trackedArtifacts.length > 0) {
|
|
7667
7676
|
flag(
|
|
7668
|
-
`- GITIGNORE: ${trackedArtifacts.length} machine-local
|
|
7669
|
-
`${trackedArtifacts.length} machine-local
|
|
7677
|
+
`- GITIGNORE: ${trackedArtifacts.length} machine-local artifact(s) still tracked in git (${trackedArtifacts.join(", ")})`,
|
|
7678
|
+
`${trackedArtifacts.length} machine-local artifact(s) are still tracked in git`,
|
|
7670
7679
|
`git rm --cached ${trackedArtifacts.join(" ")} (or run \`haus apply --write\` to do this and update .gitignore for you)`
|
|
7671
7680
|
);
|
|
7672
7681
|
} else {
|
|
7673
|
-
ok("- GITIGNORE: OK (no machine-local
|
|
7682
|
+
ok("- GITIGNORE: OK (no machine-local artifacts tracked in git)");
|
|
7674
7683
|
}
|
|
7675
7684
|
const gitignoredHausPaths = await findGitignoredHausPaths(root);
|
|
7676
7685
|
if (gitignoredHausPaths.length > 0) {
|
|
@@ -8291,7 +8300,8 @@ function summarizeLockDiff(before, after) {
|
|
|
8291
8300
|
}
|
|
8292
8301
|
|
|
8293
8302
|
// src/update/lockfile.ts
|
|
8294
|
-
import {
|
|
8303
|
+
import { constants as fsConstants } from "node:fs";
|
|
8304
|
+
import { copyFile, mkdir, readFile as readFile2 } from "node:fs/promises";
|
|
8295
8305
|
import path47 from "node:path";
|
|
8296
8306
|
init_fs();
|
|
8297
8307
|
init_paths();
|
|
@@ -8325,16 +8335,30 @@ async function readLockRaw(root) {
|
|
|
8325
8335
|
return "[]";
|
|
8326
8336
|
}
|
|
8327
8337
|
}
|
|
8328
|
-
async function
|
|
8338
|
+
async function backupLockfile(root) {
|
|
8329
8339
|
const lockPath = hausPath(root, "haus.lock.json");
|
|
8330
|
-
const before = await readLockRaw(root);
|
|
8331
|
-
const lock = await readJson(lockPath) ?? [];
|
|
8332
8340
|
try {
|
|
8333
8341
|
const backupDir = hausPath(root, "backups");
|
|
8334
8342
|
await mkdir(backupDir, { recursive: true });
|
|
8335
|
-
|
|
8343
|
+
let stamp = Date.now();
|
|
8344
|
+
for (; ; ) {
|
|
8345
|
+
const dest = path47.join(backupDir, `haus.lock.${stamp}.json`);
|
|
8346
|
+
try {
|
|
8347
|
+
await copyFile(lockPath, dest, fsConstants.COPYFILE_EXCL);
|
|
8348
|
+
return;
|
|
8349
|
+
} catch (err) {
|
|
8350
|
+
if (err.code !== "EEXIST") throw err;
|
|
8351
|
+
stamp += 1;
|
|
8352
|
+
}
|
|
8353
|
+
}
|
|
8336
8354
|
} catch {
|
|
8337
8355
|
}
|
|
8356
|
+
}
|
|
8357
|
+
async function applyLock(root) {
|
|
8358
|
+
const lockPath = hausPath(root, "haus.lock.json");
|
|
8359
|
+
const before = await readLockRaw(root);
|
|
8360
|
+
const lock = await readJson(lockPath) ?? [];
|
|
8361
|
+
await backupLockfile(root);
|
|
8338
8362
|
const enriched = await Promise.all(
|
|
8339
8363
|
lock.map(async (x) => {
|
|
8340
8364
|
const paths = Array.isArray(x.paths) ? x.paths.map(String) : [];
|
|
@@ -8357,12 +8381,7 @@ async function removeLockEntries(root, ids) {
|
|
|
8357
8381
|
const removed = [...wanted].filter((id) => present.has(id));
|
|
8358
8382
|
const missing = [...wanted].filter((id) => !present.has(id));
|
|
8359
8383
|
if (removed.length === 0) return { removed, missing, before, after: before };
|
|
8360
|
-
|
|
8361
|
-
const backupDir = hausPath(root, "backups");
|
|
8362
|
-
await mkdir(backupDir, { recursive: true });
|
|
8363
|
-
await copyFile(lockPath, path47.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
8364
|
-
} catch {
|
|
8365
|
-
}
|
|
8384
|
+
await backupLockfile(root);
|
|
8366
8385
|
const survivors = lock.filter((item) => !wanted.has(item.id));
|
|
8367
8386
|
await writeJson(lockPath, survivors);
|
|
8368
8387
|
return { removed, missing, before, after: `${JSON.stringify(survivors, null, 2)}
|
|
@@ -10014,6 +10033,9 @@ async function hasMultipleSiblingRepos(root) {
|
|
|
10014
10033
|
init_fs();
|
|
10015
10034
|
init_logger();
|
|
10016
10035
|
init_paths();
|
|
10036
|
+
function emitJsonEnvelope(json, quiet, envelope) {
|
|
10037
|
+
if (json && !quiet) log(JSON.stringify(envelope, null, 2));
|
|
10038
|
+
}
|
|
10017
10039
|
async function readPriorManualIncludes(root) {
|
|
10018
10040
|
const prior = await readJson(hausPath(root, "recommendation.json"));
|
|
10019
10041
|
if (!prior?.recommended) return [];
|
|
@@ -10021,16 +10043,12 @@ async function readPriorManualIncludes(root) {
|
|
|
10021
10043
|
}
|
|
10022
10044
|
async function runSetupCore(root, opts) {
|
|
10023
10045
|
const { json, apply, dryRun, quiet, confirm: confirm2, force } = opts;
|
|
10024
|
-
const say = quiet ? () => {
|
|
10046
|
+
const say = quiet || json ? () => {
|
|
10025
10047
|
} : log;
|
|
10026
10048
|
const scanResult = await scanProject(root);
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
say("Haus scan complete");
|
|
10031
|
-
say(`Roles: ${scanResult.repoRoles.join(", ") || "unknown"}`);
|
|
10032
|
-
say(`Package manager: ${scanResult.packageManager}`);
|
|
10033
|
-
}
|
|
10049
|
+
say("Haus scan complete");
|
|
10050
|
+
say(`Roles: ${scanResult.repoRoles.join(", ") || "unknown"}`);
|
|
10051
|
+
say(`Package manager: ${scanResult.packageManager}`);
|
|
10034
10052
|
const siblingWarning = await hasMultipleSiblingRepos(root) ? [SIBLING_REPO_WARNING] : [];
|
|
10035
10053
|
const context = await readContextOrScan(root);
|
|
10036
10054
|
const priorManualIncludes = await readPriorManualIncludes(root);
|
|
@@ -10043,39 +10061,39 @@ async function runSetupCore(root, opts) {
|
|
|
10043
10061
|
const warningLines2 = [
|
|
10044
10062
|
.../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? [], ...siblingWarning])
|
|
10045
10063
|
];
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
say(`Skipped: ${recommendation.skipped.length}`);
|
|
10052
|
-
say(`Repo: ${context.repoName}`);
|
|
10053
|
-
for (const warning of warningLines2) say(`- WARN: ${warning}`);
|
|
10054
|
-
}
|
|
10064
|
+
say("Haus recommendation ready");
|
|
10065
|
+
say("Recommended: 0");
|
|
10066
|
+
say(`Skipped: ${recommendation.skipped.length}`);
|
|
10067
|
+
say(`Repo: ${context.repoName}`);
|
|
10068
|
+
for (const warning of warningLines2) say(`- WARN: ${warning}`);
|
|
10055
10069
|
const supersededCount = recommendation.skipped.filter(
|
|
10056
10070
|
(s) => s.skipReasons?.some((r) => r.code === "superseded-by-global-plugin")
|
|
10057
10071
|
).length;
|
|
10058
10072
|
const message2 = supersededCount > 0 ? `No catalog items matched this project after ${supersededCount} item(s) were gated as already provided by an installed global plugin \u2014 refusing to write recommendation.json/haus.lock.json. Those skills are available globally; re-run with \`haus init --force\` to write anyway.` : "No catalog items matched this project at all (zero signal) \u2014 refusing to write recommendation.json/haus.lock.json. Re-run with `haus init --force` to write anyway.";
|
|
10059
10073
|
warn(message2);
|
|
10060
10074
|
process.exitCode = 1;
|
|
10075
|
+
const warnings = [...warningLines2, message2];
|
|
10076
|
+
emitJsonEnvelope(json, quiet, {
|
|
10077
|
+
scan: scanResult,
|
|
10078
|
+
recommendation,
|
|
10079
|
+
hooks: null,
|
|
10080
|
+
repoName: context.repoName,
|
|
10081
|
+
warnings
|
|
10082
|
+
});
|
|
10061
10083
|
return {
|
|
10062
10084
|
root,
|
|
10063
10085
|
repoName: context.repoName,
|
|
10064
10086
|
roles: scanResult.repoRoles,
|
|
10065
10087
|
recommendedCount: 0,
|
|
10066
|
-
warnings
|
|
10088
|
+
warnings,
|
|
10067
10089
|
hooksOk: false,
|
|
10068
10090
|
written: []
|
|
10069
10091
|
};
|
|
10070
10092
|
}
|
|
10071
10093
|
await writeJson(hausPath(root, "recommendation.json"), recommendation);
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
say("Haus recommendation ready");
|
|
10076
|
-
say(`Recommended: ${recommendation.recommended.length}`);
|
|
10077
|
-
say(`Skipped: ${recommendation.skipped.length}`);
|
|
10078
|
-
}
|
|
10094
|
+
say("Haus recommendation ready");
|
|
10095
|
+
say(`Recommended: ${recommendation.recommended.length}`);
|
|
10096
|
+
say(`Skipped: ${recommendation.skipped.length}`);
|
|
10079
10097
|
const hooks = await verifyProjectSettingsHooksContract(root);
|
|
10080
10098
|
const warningLines = [
|
|
10081
10099
|
.../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? [], ...siblingWarning])
|
|
@@ -10100,12 +10118,23 @@ async function runSetupCore(root, opts) {
|
|
|
10100
10118
|
hooksOk,
|
|
10101
10119
|
written: []
|
|
10102
10120
|
};
|
|
10103
|
-
|
|
10121
|
+
const envelope = (hooksResult) => ({
|
|
10122
|
+
scan: scanResult,
|
|
10123
|
+
recommendation,
|
|
10124
|
+
hooks: hooksResult,
|
|
10125
|
+
repoName: context.repoName,
|
|
10126
|
+
warnings: warningLines
|
|
10127
|
+
});
|
|
10128
|
+
if (!apply) {
|
|
10129
|
+
emitJsonEnvelope(json, quiet, envelope(hooks));
|
|
10130
|
+
return baseResult;
|
|
10131
|
+
}
|
|
10104
10132
|
if (confirm2) {
|
|
10105
10133
|
const approved = await confirm2();
|
|
10106
10134
|
if (!approved) {
|
|
10107
10135
|
say("Setup reviewed. No files written.");
|
|
10108
10136
|
say("Next step: run `haus apply --write` when ready.");
|
|
10137
|
+
emitJsonEnvelope(json, quiet, envelope(hooks));
|
|
10109
10138
|
return baseResult;
|
|
10110
10139
|
}
|
|
10111
10140
|
}
|
|
@@ -10115,6 +10144,7 @@ async function runSetupCore(root, opts) {
|
|
|
10115
10144
|
if (sync.formatRefusal) {
|
|
10116
10145
|
error(sync.formatRefusal);
|
|
10117
10146
|
process.exitCode = 1;
|
|
10147
|
+
emitJsonEnvelope(json, quiet, envelope(hooks));
|
|
10118
10148
|
return baseResult;
|
|
10119
10149
|
}
|
|
10120
10150
|
if (sync.newItems.length > 0) {
|
|
@@ -10142,6 +10172,7 @@ async function runSetupCore(root, opts) {
|
|
|
10142
10172
|
} else {
|
|
10143
10173
|
say(`- HOOKS OK: ${hooksAfter.message}`);
|
|
10144
10174
|
}
|
|
10175
|
+
emitJsonEnvelope(json, quiet, envelope(hooksAfter));
|
|
10145
10176
|
return { ...baseResult, hooksOk: hooksOkAfter, written: files };
|
|
10146
10177
|
}
|
|
10147
10178
|
|
|
@@ -10787,6 +10818,7 @@ async function runRecommend(options) {
|
|
|
10787
10818
|
}
|
|
10788
10819
|
|
|
10789
10820
|
// src/commands/remove.ts
|
|
10821
|
+
import path54 from "node:path";
|
|
10790
10822
|
init_fs();
|
|
10791
10823
|
init_logger();
|
|
10792
10824
|
init_paths();
|
|
@@ -10798,7 +10830,7 @@ async function runRemove(ids, options = {}) {
|
|
|
10798
10830
|
const dryRun = Boolean(options.dryRun);
|
|
10799
10831
|
const say = options.json ? () => {
|
|
10800
10832
|
} : log;
|
|
10801
|
-
const wanted = parseIdList(ids);
|
|
10833
|
+
const wanted = [...new Set(parseIdList(ids))];
|
|
10802
10834
|
if (wanted.length === 0) {
|
|
10803
10835
|
error("Specify at least one catalog item id \u2014 see `haus doctor` for what is installed.");
|
|
10804
10836
|
process.exitCode = 1;
|
|
@@ -10844,7 +10876,7 @@ async function runRemove(ids, options = {}) {
|
|
|
10844
10876
|
cleared.push({ id, existing: [] });
|
|
10845
10877
|
continue;
|
|
10846
10878
|
}
|
|
10847
|
-
const gate = await hashGateEntry(root, {
|
|
10879
|
+
const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
|
|
10848
10880
|
if (!gate.ok) {
|
|
10849
10881
|
const detail = gate.reason === "no-hash" ? "has no lock hash" : "was modified locally";
|
|
10850
10882
|
warn(
|
|
@@ -10890,7 +10922,8 @@ async function runRemove(ids, options = {}) {
|
|
|
10890
10922
|
}
|
|
10891
10923
|
}
|
|
10892
10924
|
const allAbs = cleared.flatMap(({ existing }) => existing.map(({ abs }) => abs));
|
|
10893
|
-
|
|
10925
|
+
const backupRoot = await backupTrackedPaths(root, allAbs, say, "remove");
|
|
10926
|
+
report.backupDir = backupRoot === null ? null : path54.basename(backupRoot);
|
|
10894
10927
|
for (const { id, existing } of cleared) {
|
|
10895
10928
|
await deleteTrackedPaths(root, id, existing, { verb: "Removed", reason: "" }, say);
|
|
10896
10929
|
}
|
|
@@ -10975,7 +11008,7 @@ async function runScan(options) {
|
|
|
10975
11008
|
}
|
|
10976
11009
|
|
|
10977
11010
|
// src/commands/undo.ts
|
|
10978
|
-
import
|
|
11011
|
+
import path55 from "node:path";
|
|
10979
11012
|
import fs40 from "fs-extra";
|
|
10980
11013
|
|
|
10981
11014
|
// src/claude/managed-paths.ts
|
|
@@ -11007,18 +11040,18 @@ async function collectLockTrackedPaths(root) {
|
|
|
11007
11040
|
const relPaths = row.paths ?? [];
|
|
11008
11041
|
const existingRel = [];
|
|
11009
11042
|
for (const rel of relPaths) {
|
|
11010
|
-
if (await fs40.pathExists(
|
|
11043
|
+
if (await fs40.pathExists(path55.resolve(root, rel))) existingRel.push(rel);
|
|
11011
11044
|
}
|
|
11012
11045
|
if (existingRel.length === 0) continue;
|
|
11013
11046
|
if (row.hash === void 0) {
|
|
11014
|
-
removable.push(...existingRel.map((rel) =>
|
|
11047
|
+
removable.push(...existingRel.map((rel) => path55.resolve(root, rel)));
|
|
11015
11048
|
continue;
|
|
11016
11049
|
}
|
|
11017
11050
|
const currentHash = await hashInstalledPaths(root, existingRel);
|
|
11018
11051
|
if (currentHash === row.hash) {
|
|
11019
|
-
removable.push(...existingRel.map((rel) =>
|
|
11052
|
+
removable.push(...existingRel.map((rel) => path55.resolve(root, rel)));
|
|
11020
11053
|
} else {
|
|
11021
|
-
preserved.push(...existingRel.map((rel) =>
|
|
11054
|
+
preserved.push(...existingRel.map((rel) => path55.resolve(root, rel)));
|
|
11022
11055
|
log(`Preserving locally-modified ${row.id ?? "(unknown item)"}: ${existingRel.join(", ")}`);
|
|
11023
11056
|
}
|
|
11024
11057
|
}
|
|
@@ -11031,7 +11064,7 @@ async function settingsHasHausContent(root) {
|
|
|
11031
11064
|
return settings._haus != null;
|
|
11032
11065
|
}
|
|
11033
11066
|
async function claudeMdHasHausBlock(root) {
|
|
11034
|
-
const filePath =
|
|
11067
|
+
const filePath = path55.join(root, "CLAUDE.md");
|
|
11035
11068
|
if (!await fs40.pathExists(filePath)) return false;
|
|
11036
11069
|
const text = await fs40.readFile(filePath, "utf8");
|
|
11037
11070
|
return text.includes(BLOCK_BEGIN);
|
|
@@ -11044,15 +11077,15 @@ async function stripProjectSettings(root) {
|
|
|
11044
11077
|
const hasContent = Object.keys(settings).length > 0;
|
|
11045
11078
|
if (hasContent) {
|
|
11046
11079
|
await writeProjectSettings(root, settings);
|
|
11047
|
-
log(`Stripped haus rules from ${
|
|
11080
|
+
log(`Stripped haus rules from ${path55.relative(root, settingsPath)} (user settings preserved).`);
|
|
11048
11081
|
return true;
|
|
11049
11082
|
}
|
|
11050
11083
|
await fs40.remove(settingsPath);
|
|
11051
|
-
log(`Removed ${
|
|
11084
|
+
log(`Removed ${path55.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
11052
11085
|
return true;
|
|
11053
11086
|
}
|
|
11054
11087
|
async function stripRootClaudeMd(root) {
|
|
11055
|
-
const filePath =
|
|
11088
|
+
const filePath = path55.join(root, "CLAUDE.md");
|
|
11056
11089
|
if (!await fs40.pathExists(filePath)) return false;
|
|
11057
11090
|
const prev = await fs40.readFile(filePath, "utf8");
|
|
11058
11091
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
@@ -11080,12 +11113,12 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
|
11080
11113
|
const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
|
|
11081
11114
|
for (const abs of managedAbsPaths) {
|
|
11082
11115
|
if (!await fs40.pathExists(abs)) continue;
|
|
11083
|
-
const rel =
|
|
11084
|
-
const backupPath =
|
|
11085
|
-
await fs40.ensureDir(
|
|
11116
|
+
const rel = path55.relative(root, abs);
|
|
11117
|
+
const backupPath = path55.join(backupRoot, rel);
|
|
11118
|
+
await fs40.ensureDir(path55.dirname(backupPath));
|
|
11086
11119
|
await fs40.copy(abs, backupPath);
|
|
11087
11120
|
}
|
|
11088
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
11121
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path55.relative(root, backupRoot)}.`);
|
|
11089
11122
|
}
|
|
11090
11123
|
async function runUndo(options) {
|
|
11091
11124
|
const root = options.root ?? process.cwd();
|
|
@@ -11098,7 +11131,7 @@ async function runUndo(options) {
|
|
|
11098
11131
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
11099
11132
|
return;
|
|
11100
11133
|
}
|
|
11101
|
-
const relTargets = managed.map((p) =>
|
|
11134
|
+
const relTargets = managed.map((p) => path55.relative(root, p));
|
|
11102
11135
|
const summaryParts = [...relTargets];
|
|
11103
11136
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
11104
11137
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -11128,7 +11161,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
11128
11161
|
for (const abs of managed) {
|
|
11129
11162
|
if (!await fs40.pathExists(abs)) continue;
|
|
11130
11163
|
await fs40.remove(abs);
|
|
11131
|
-
log(`Removed ${
|
|
11164
|
+
log(`Removed ${path55.relative(root, abs)}`);
|
|
11132
11165
|
}
|
|
11133
11166
|
if (stripSettings) await stripProjectSettings(root);
|
|
11134
11167
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -11142,7 +11175,7 @@ init_fs();
|
|
|
11142
11175
|
init_logger();
|
|
11143
11176
|
init_paths();
|
|
11144
11177
|
import crypto3 from "node:crypto";
|
|
11145
|
-
import
|
|
11178
|
+
import path56 from "node:path";
|
|
11146
11179
|
import fs41 from "fs-extra";
|
|
11147
11180
|
async function runUninstall(options = {}) {
|
|
11148
11181
|
const { force = false, removePlugins = removeGlobalPlugins } = options;
|
|
@@ -11199,14 +11232,14 @@ async function runUninstall(options = {}) {
|
|
|
11199
11232
|
continue;
|
|
11200
11233
|
}
|
|
11201
11234
|
await fs41.remove(entry.destPath);
|
|
11202
|
-
await pruneEmptyDir(
|
|
11235
|
+
await pruneEmptyDir(path56.dirname(entry.destPath));
|
|
11203
11236
|
result.deleted.push(entry.destPath);
|
|
11204
11237
|
}
|
|
11205
11238
|
const settings = await readSettings();
|
|
11206
11239
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
11207
11240
|
await writeSettings(stripped);
|
|
11208
11241
|
result.hooksStripped = true;
|
|
11209
|
-
const hausDir =
|
|
11242
|
+
const hausDir = path56.join(globalClaudeDir(), "haus");
|
|
11210
11243
|
const manifestPath2 = hausManifestPath();
|
|
11211
11244
|
if (fs41.pathExistsSync(manifestPath2)) {
|
|
11212
11245
|
await fs41.remove(manifestPath2);
|
|
@@ -11283,7 +11316,7 @@ async function runUninstallCommand(options) {
|
|
|
11283
11316
|
}
|
|
11284
11317
|
|
|
11285
11318
|
// src/commands/validate-catalog.ts
|
|
11286
|
-
import
|
|
11319
|
+
import path58 from "node:path";
|
|
11287
11320
|
|
|
11288
11321
|
// src/catalog/validate-core.ts
|
|
11289
11322
|
init_forbidden_content();
|
|
@@ -11291,7 +11324,7 @@ init_manifest_item_fields();
|
|
|
11291
11324
|
init_path_safety();
|
|
11292
11325
|
init_validation_rules2();
|
|
11293
11326
|
import fs42 from "node:fs";
|
|
11294
|
-
import
|
|
11327
|
+
import path57 from "node:path";
|
|
11295
11328
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
11296
11329
|
var ITEM_ID_PATTERN = /^haus\./;
|
|
11297
11330
|
function auditForbiddenStacks(items) {
|
|
@@ -11442,7 +11475,7 @@ function readForAudit(absPath, label, failures) {
|
|
|
11442
11475
|
}
|
|
11443
11476
|
}
|
|
11444
11477
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
11445
|
-
const rel =
|
|
11478
|
+
const rel = path57.relative(manifestDir, absPath);
|
|
11446
11479
|
const failures = [];
|
|
11447
11480
|
const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
|
|
11448
11481
|
if (text === null) return failures;
|
|
@@ -11464,25 +11497,25 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
11464
11497
|
const failures = [];
|
|
11465
11498
|
for (const item of items) {
|
|
11466
11499
|
if (!item.path || !isSafeCatalogPath(item.path)) continue;
|
|
11467
|
-
const absPath =
|
|
11500
|
+
const absPath = path57.join(manifestDir, item.path);
|
|
11468
11501
|
if (item.type === "skill") {
|
|
11469
|
-
const skillMd =
|
|
11502
|
+
const skillMd = path57.join(absPath, "SKILL.md");
|
|
11470
11503
|
if (!fs42.existsSync(skillMd)) {
|
|
11471
|
-
failures.push(`${item.id}: missing ${
|
|
11504
|
+
failures.push(`${item.id}: missing ${path57.relative(manifestDir, skillMd)}`);
|
|
11472
11505
|
continue;
|
|
11473
11506
|
}
|
|
11474
11507
|
const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
|
|
11475
11508
|
if (text === null) continue;
|
|
11476
11509
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
11477
11510
|
failures.push(
|
|
11478
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
11511
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path57.relative(manifestDir, skillMd)}`)
|
|
11479
11512
|
);
|
|
11480
11513
|
} else if (item.type === "agent") {
|
|
11481
11514
|
if (!fs42.existsSync(absPath)) {
|
|
11482
11515
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
11483
11516
|
continue;
|
|
11484
11517
|
}
|
|
11485
|
-
const rel =
|
|
11518
|
+
const rel = path57.relative(manifestDir, absPath);
|
|
11486
11519
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
11487
11520
|
if (text === null) continue;
|
|
11488
11521
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -11498,7 +11531,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
11498
11531
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
11499
11532
|
continue;
|
|
11500
11533
|
}
|
|
11501
|
-
const rel =
|
|
11534
|
+
const rel = path57.relative(manifestDir, absPath);
|
|
11502
11535
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
11503
11536
|
if (text === null) continue;
|
|
11504
11537
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -11556,7 +11589,7 @@ function walkAllFiles(dir, fn, failures) {
|
|
|
11556
11589
|
}
|
|
11557
11590
|
for (const entry of entries) {
|
|
11558
11591
|
if (entry.name === ".DS_Store") continue;
|
|
11559
|
-
const full =
|
|
11592
|
+
const full = path57.join(dir, entry.name);
|
|
11560
11593
|
if (entry.isDirectory()) walkAllFiles(full, fn, failures);
|
|
11561
11594
|
else fn(full);
|
|
11562
11595
|
}
|
|
@@ -11580,12 +11613,12 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
11580
11613
|
const failures = [];
|
|
11581
11614
|
const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
|
|
11582
11615
|
for (const dir of CONTENT_ROOTS) {
|
|
11583
|
-
const abs =
|
|
11616
|
+
const abs = path57.join(manifestDir, dir);
|
|
11584
11617
|
if (!fs42.existsSync(abs)) continue;
|
|
11585
11618
|
walkAllFiles(
|
|
11586
11619
|
abs,
|
|
11587
11620
|
(file) => {
|
|
11588
|
-
const relPath =
|
|
11621
|
+
const relPath = path57.relative(manifestDir, file).replace(/\\/g, "/");
|
|
11589
11622
|
if (!isPathClaimed(relPath, claimedPaths)) {
|
|
11590
11623
|
failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
|
|
11591
11624
|
}
|
|
@@ -11606,7 +11639,7 @@ function walkMd(dir, fn, failures) {
|
|
|
11606
11639
|
return;
|
|
11607
11640
|
}
|
|
11608
11641
|
for (const entry of entries) {
|
|
11609
|
-
const full =
|
|
11642
|
+
const full = path57.join(dir, entry.name);
|
|
11610
11643
|
if (entry.isDirectory()) walkMd(full, fn, failures);
|
|
11611
11644
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
11612
11645
|
}
|
|
@@ -11616,13 +11649,13 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
11616
11649
|
const pathSourceMap = buildItemPathSourceMap(items);
|
|
11617
11650
|
const pathTypeMap = buildItemPathTypeMap(items);
|
|
11618
11651
|
for (const dir of ["skills", "agents", "templates", "commands", "sources"]) {
|
|
11619
|
-
const abs =
|
|
11652
|
+
const abs = path57.join(manifestDir, dir);
|
|
11620
11653
|
if (!fs42.existsSync(abs)) continue;
|
|
11621
11654
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
11622
11655
|
walkMd(
|
|
11623
11656
|
abs,
|
|
11624
11657
|
(file) => {
|
|
11625
|
-
const rel =
|
|
11658
|
+
const rel = path57.relative(manifestDir, file);
|
|
11626
11659
|
const text = readForAudit(file, rel, failures);
|
|
11627
11660
|
if (text === null) return;
|
|
11628
11661
|
const norm = rel.replace(/\\/g, "/");
|
|
@@ -11670,8 +11703,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
11670
11703
|
process.exitCode = 1;
|
|
11671
11704
|
return;
|
|
11672
11705
|
}
|
|
11673
|
-
const abs =
|
|
11674
|
-
const manifestDir =
|
|
11706
|
+
const abs = path58.resolve(process.cwd(), manifestPath2);
|
|
11707
|
+
const manifestDir = path58.dirname(abs);
|
|
11675
11708
|
const data = await readJson(abs);
|
|
11676
11709
|
if (!data?.items) {
|
|
11677
11710
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -11692,7 +11725,7 @@ init_logger();
|
|
|
11692
11725
|
|
|
11693
11726
|
// src/workspace/worktree/add.ts
|
|
11694
11727
|
init_fs();
|
|
11695
|
-
import
|
|
11728
|
+
import path65 from "node:path";
|
|
11696
11729
|
import fs46 from "fs-extra";
|
|
11697
11730
|
|
|
11698
11731
|
// src/workspace/worktree/git-worktree.ts
|
|
@@ -11813,12 +11846,12 @@ async function hasUnpushedWork(cwd) {
|
|
|
11813
11846
|
}
|
|
11814
11847
|
|
|
11815
11848
|
// src/workspace/worktree/hydrate.ts
|
|
11816
|
-
import
|
|
11849
|
+
import path61 from "node:path";
|
|
11817
11850
|
import fs44 from "fs-extra";
|
|
11818
11851
|
|
|
11819
11852
|
// src/workspace/worktree/cow-copy.ts
|
|
11820
11853
|
init_exec();
|
|
11821
|
-
import
|
|
11854
|
+
import path59 from "node:path";
|
|
11822
11855
|
import fs43 from "fs-extra";
|
|
11823
11856
|
async function getMacFilesystemType(dir) {
|
|
11824
11857
|
const df = await runCommand("df", ["-P", dir]);
|
|
@@ -11853,11 +11886,11 @@ async function cowCopyDir(src, dest) {
|
|
|
11853
11886
|
const srcExists = await fs43.pathExists(src);
|
|
11854
11887
|
if (!srcExists)
|
|
11855
11888
|
return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
|
|
11856
|
-
const strategy = await detectCowStrategy(
|
|
11889
|
+
const strategy = await detectCowStrategy(path59.dirname(dest));
|
|
11857
11890
|
if (strategy === "unsupported" || strategy === "unknown-platform") {
|
|
11858
11891
|
return { strategy, attempted: false, ok: false };
|
|
11859
11892
|
}
|
|
11860
|
-
await fs43.ensureDir(
|
|
11893
|
+
await fs43.ensureDir(path59.dirname(dest));
|
|
11861
11894
|
const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
|
|
11862
11895
|
const result = await runCommand("cp", args);
|
|
11863
11896
|
return {
|
|
@@ -11871,7 +11904,7 @@ async function cowCopyDir(src, dest) {
|
|
|
11871
11904
|
// src/workspace/worktree/install.ts
|
|
11872
11905
|
init_exec();
|
|
11873
11906
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
11874
|
-
import
|
|
11907
|
+
import path60 from "node:path";
|
|
11875
11908
|
import fg6 from "fast-glob";
|
|
11876
11909
|
function detectInstallPlan(signals) {
|
|
11877
11910
|
if (signals.hasYarnLock) {
|
|
@@ -11892,10 +11925,10 @@ function detectInstallPlan(signals) {
|
|
|
11892
11925
|
return { manager: "none" };
|
|
11893
11926
|
}
|
|
11894
11927
|
async function computeLockfileSignals(dir) {
|
|
11895
|
-
const hasYarnLock = existsSync3(
|
|
11896
|
-
const hasPnpmLock = existsSync3(
|
|
11897
|
-
const hasPackageLock = existsSync3(
|
|
11898
|
-
const hasComposerLock = existsSync3(
|
|
11928
|
+
const hasYarnLock = existsSync3(path60.join(dir, "yarn.lock"));
|
|
11929
|
+
const hasPnpmLock = existsSync3(path60.join(dir, "pnpm-lock.yaml"));
|
|
11930
|
+
const hasPackageLock = existsSync3(path60.join(dir, "package-lock.json"));
|
|
11931
|
+
const hasComposerLock = existsSync3(path60.join(dir, "composer.lock"));
|
|
11899
11932
|
const dotnetFiles = await fg6(["**/*.csproj", "**/*.sln"], {
|
|
11900
11933
|
cwd: dir,
|
|
11901
11934
|
onlyFiles: true,
|
|
@@ -11909,7 +11942,7 @@ async function computeLockfileSignals(dir) {
|
|
|
11909
11942
|
});
|
|
11910
11943
|
const hasDotnetProject = dotnetFiles.length > 0;
|
|
11911
11944
|
let hasPackageManagerField = false;
|
|
11912
|
-
const pkgPath =
|
|
11945
|
+
const pkgPath = path60.join(dir, "package.json");
|
|
11913
11946
|
if (existsSync3(pkgPath)) {
|
|
11914
11947
|
try {
|
|
11915
11948
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
@@ -11926,9 +11959,72 @@ async function computeLockfileSignals(dir) {
|
|
|
11926
11959
|
hasDotnetProject
|
|
11927
11960
|
};
|
|
11928
11961
|
}
|
|
11962
|
+
var INSTALL_OUTPUT_CAP = 8192;
|
|
11963
|
+
var INSTALL_TAIL_LINES = 8;
|
|
11964
|
+
function capTail(text, max = INSTALL_OUTPUT_CAP) {
|
|
11965
|
+
return text.length <= max ? text : text.slice(-max);
|
|
11966
|
+
}
|
|
11967
|
+
function nonemptyLines(text) {
|
|
11968
|
+
return text.split(/\r?\n/).filter((line2) => line2.trim().length > 0);
|
|
11969
|
+
}
|
|
11970
|
+
function findRequiredYarnrcEnvVars(content2) {
|
|
11971
|
+
const required = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
11972
|
+
const names = /* @__PURE__ */ new Set();
|
|
11973
|
+
for (const line2 of content2.split(/\r?\n/)) {
|
|
11974
|
+
if (line2.trimStart().startsWith("#")) continue;
|
|
11975
|
+
required.lastIndex = 0;
|
|
11976
|
+
let match;
|
|
11977
|
+
while ((match = required.exec(line2)) !== null) {
|
|
11978
|
+
const name = match[1];
|
|
11979
|
+
if (name !== void 0) names.add(name);
|
|
11980
|
+
}
|
|
11981
|
+
}
|
|
11982
|
+
return [...names];
|
|
11983
|
+
}
|
|
11984
|
+
function missingYarnrcEnvVars(content2, env) {
|
|
11985
|
+
return findRequiredYarnrcEnvVars(content2).filter((name) => env[name] === void 0);
|
|
11986
|
+
}
|
|
11987
|
+
function missingYarnrcEnvVarsInDir(dir, env) {
|
|
11988
|
+
const yarnrcPath = path60.join(dir, ".yarnrc.yml");
|
|
11989
|
+
if (!existsSync3(yarnrcPath)) return [];
|
|
11990
|
+
try {
|
|
11991
|
+
return missingYarnrcEnvVars(readFileSync3(yarnrcPath, "utf8"), env);
|
|
11992
|
+
} catch {
|
|
11993
|
+
return [];
|
|
11994
|
+
}
|
|
11995
|
+
}
|
|
11996
|
+
function tailInstallOutput(stdout2, stderr) {
|
|
11997
|
+
const errLines = nonemptyLines(stderr).slice(-INSTALL_TAIL_LINES);
|
|
11998
|
+
const remaining = INSTALL_TAIL_LINES - errLines.length;
|
|
11999
|
+
const outLines = remaining > 0 ? nonemptyLines(stdout2).slice(-remaining) : [];
|
|
12000
|
+
return [...errLines, ...outLines].join("\n");
|
|
12001
|
+
}
|
|
12002
|
+
function summarizeInstall(install) {
|
|
12003
|
+
if (install.plan.manager === "none") return "no install step (no recognized lockfile)";
|
|
12004
|
+
const envNotes = (install.missingEnvVars ?? []).map(
|
|
12005
|
+
(name) => `${name} not set in environment \u2014 yarn install will fail`
|
|
12006
|
+
);
|
|
12007
|
+
if (!install.ran) {
|
|
12008
|
+
const skip = `install skipped (${install.skippedReason ?? "unavailable"})`;
|
|
12009
|
+
return envNotes.length > 0 ? [skip, ...envNotes].join("\n ") : skip;
|
|
12010
|
+
}
|
|
12011
|
+
if (install.ok) return `hydrated via ${install.plan.manager}`;
|
|
12012
|
+
const head = `install FAILED via ${install.plan.manager}`;
|
|
12013
|
+
const tail = tailInstallOutput(install.stdout ?? "", install.stderr ?? "");
|
|
12014
|
+
const details = [...envNotes, ...tail.length > 0 ? tail.split("\n") : []];
|
|
12015
|
+
return details.length > 0 ? [head, ...details].join("\n ") : head;
|
|
12016
|
+
}
|
|
12017
|
+
function withMissingEnv(result, missingEnvVars) {
|
|
12018
|
+
return missingEnvVars.length > 0 ? { ...result, missingEnvVars } : result;
|
|
12019
|
+
}
|
|
11929
12020
|
async function runInstallPlan(dir, plan, deps = {}) {
|
|
12021
|
+
const env = deps.env ?? process.env;
|
|
12022
|
+
const missingEnvVars = plan.manager === "yarn" ? missingYarnrcEnvVarsInDir(dir, env) : [];
|
|
11930
12023
|
if (plan.manager === "none") {
|
|
11931
|
-
return
|
|
12024
|
+
return withMissingEnv(
|
|
12025
|
+
{ plan, ran: false, ok: true, skippedReason: "no recognized lockfile" },
|
|
12026
|
+
missingEnvVars
|
|
12027
|
+
);
|
|
11932
12028
|
}
|
|
11933
12029
|
const probe = deps.probe ?? probeCommand;
|
|
11934
12030
|
const probed = await probe(plan.command);
|
|
@@ -11936,25 +12032,41 @@ async function runInstallPlan(dir, plan, deps = {}) {
|
|
|
11936
12032
|
case "available":
|
|
11937
12033
|
break;
|
|
11938
12034
|
case "absent":
|
|
11939
|
-
return
|
|
12035
|
+
return withMissingEnv(
|
|
12036
|
+
{ plan, ran: false, ok: true, skippedReason: `${plan.command} not found on PATH` },
|
|
12037
|
+
missingEnvVars
|
|
12038
|
+
);
|
|
11940
12039
|
case "inconclusive":
|
|
11941
|
-
return
|
|
11942
|
-
|
|
11943
|
-
|
|
11944
|
-
|
|
11945
|
-
|
|
11946
|
-
|
|
12040
|
+
return withMissingEnv(
|
|
12041
|
+
{
|
|
12042
|
+
plan,
|
|
12043
|
+
ran: false,
|
|
12044
|
+
ok: true,
|
|
12045
|
+
skippedReason: `could not determine whether ${plan.command} is on PATH (${probed.reason})`
|
|
12046
|
+
},
|
|
12047
|
+
missingEnvVars
|
|
12048
|
+
);
|
|
11947
12049
|
default:
|
|
11948
12050
|
return assertNeverCommandProbe(probed);
|
|
11949
12051
|
}
|
|
11950
|
-
const
|
|
11951
|
-
|
|
11952
|
-
|
|
11953
|
-
ran: true,
|
|
11954
|
-
|
|
11955
|
-
|
|
11956
|
-
|
|
11957
|
-
|
|
12052
|
+
const run = deps.run ?? runCommand;
|
|
12053
|
+
const result = await run(plan.command, plan.args, { cwd: dir });
|
|
12054
|
+
if (result.exitCode === 0) {
|
|
12055
|
+
return withMissingEnv({ plan, ran: true, ok: true, exitCode: 0 }, missingEnvVars);
|
|
12056
|
+
}
|
|
12057
|
+
const stdout2 = capTail(result.stdout);
|
|
12058
|
+
const stderr = capTail(result.stderr);
|
|
12059
|
+
return withMissingEnv(
|
|
12060
|
+
{
|
|
12061
|
+
plan,
|
|
12062
|
+
ran: true,
|
|
12063
|
+
ok: false,
|
|
12064
|
+
exitCode: result.exitCode,
|
|
12065
|
+
stdout: stdout2.length > 0 ? stdout2 : void 0,
|
|
12066
|
+
stderr: stderr.length > 0 ? stderr : void 0
|
|
12067
|
+
},
|
|
12068
|
+
missingEnvVars
|
|
12069
|
+
);
|
|
11958
12070
|
}
|
|
11959
12071
|
|
|
11960
12072
|
// src/workspace/worktree/hydrate.ts
|
|
@@ -11963,8 +12075,8 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
11963
12075
|
const start = Date.now();
|
|
11964
12076
|
const targets = [];
|
|
11965
12077
|
for (const target of HYDRATION_TARGETS) {
|
|
11966
|
-
const src =
|
|
11967
|
-
const dest =
|
|
12078
|
+
const src = path61.join(mainMemberPath, target);
|
|
12079
|
+
const dest = path61.join(worktreeMemberPath, target);
|
|
11968
12080
|
if (!await fs44.pathExists(src)) continue;
|
|
11969
12081
|
const destExists = await fs44.pathExists(dest);
|
|
11970
12082
|
if (!opts.force && destExists) continue;
|
|
@@ -11982,11 +12094,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
11982
12094
|
|
|
11983
12095
|
// src/workspace/worktree/local-files.ts
|
|
11984
12096
|
init_exec();
|
|
11985
|
-
import
|
|
12097
|
+
import path62 from "node:path";
|
|
11986
12098
|
import fs45 from "fs-extra";
|
|
11987
12099
|
var MACHINE_LOCAL_FILES = [
|
|
11988
12100
|
"docker-compose.local.yml",
|
|
11989
|
-
|
|
12101
|
+
path62.join(".claude", "settings.local.json")
|
|
11990
12102
|
];
|
|
11991
12103
|
async function isTracked(repoRoot, relFile) {
|
|
11992
12104
|
const gitPath = relFile.replace(/\\/g, "/");
|
|
@@ -11996,11 +12108,11 @@ async function isTracked(repoRoot, relFile) {
|
|
|
11996
12108
|
async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
11997
12109
|
const copied = [];
|
|
11998
12110
|
for (const rel of MACHINE_LOCAL_FILES) {
|
|
11999
|
-
const src =
|
|
12111
|
+
const src = path62.join(mainMemberPath, rel);
|
|
12000
12112
|
if (!await fs45.pathExists(src)) continue;
|
|
12001
12113
|
if (await isTracked(mainMemberPath, rel)) continue;
|
|
12002
|
-
const dest =
|
|
12003
|
-
await fs45.ensureDir(
|
|
12114
|
+
const dest = path62.join(worktreeMemberPath, rel);
|
|
12115
|
+
await fs45.ensureDir(path62.dirname(dest));
|
|
12004
12116
|
await fs45.copy(src, dest, { overwrite: true });
|
|
12005
12117
|
copied.push(rel);
|
|
12006
12118
|
}
|
|
@@ -12009,15 +12121,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
|
12009
12121
|
|
|
12010
12122
|
// src/workspace/worktree/root.ts
|
|
12011
12123
|
import { existsSync as existsSync4 } from "node:fs";
|
|
12012
|
-
import
|
|
12124
|
+
import path63 from "node:path";
|
|
12013
12125
|
async function resolveWorkspaceForWorktree(start) {
|
|
12014
12126
|
const rootInfo = await resolveRoots(start);
|
|
12015
12127
|
if (!rootInfo.isGitRepo) {
|
|
12016
12128
|
return { ok: false, reason: "Not inside a git repository." };
|
|
12017
12129
|
}
|
|
12018
12130
|
const workspaceRoot = rootInfo.mainRoot;
|
|
12019
|
-
const hasYaml = existsSync4(
|
|
12020
|
-
const hasManifest = existsSync4(
|
|
12131
|
+
const hasYaml = existsSync4(path63.join(workspaceRoot, WORKSPACE_FILE));
|
|
12132
|
+
const hasManifest = existsSync4(path63.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
12021
12133
|
if (!hasYaml && !hasManifest) {
|
|
12022
12134
|
return {
|
|
12023
12135
|
ok: false,
|
|
@@ -12043,10 +12155,10 @@ function selectMembers(members, only) {
|
|
|
12043
12155
|
|
|
12044
12156
|
// src/workspace/worktree/state.ts
|
|
12045
12157
|
init_fs();
|
|
12046
|
-
import
|
|
12158
|
+
import path64 from "node:path";
|
|
12047
12159
|
var WORKTREE_STATE_FILE = ".haus-worktree.json";
|
|
12048
12160
|
function worktreesDir(mainRoot) {
|
|
12049
|
-
return
|
|
12161
|
+
return path64.join(mainRoot, ".claude", "worktrees");
|
|
12050
12162
|
}
|
|
12051
12163
|
function isValidWorktreeSlug(slug) {
|
|
12052
12164
|
if (slug === "" || slug === "." || slug === "..") return false;
|
|
@@ -12059,15 +12171,15 @@ function worktreePath(mainRoot, slug) {
|
|
|
12059
12171
|
`Invalid worktree slug ${JSON.stringify(slug)} \u2014 slugs may only contain letters, digits, ".", "_", "-" (no path separators, and not "." or "..").`
|
|
12060
12172
|
);
|
|
12061
12173
|
}
|
|
12062
|
-
return
|
|
12174
|
+
return path64.join(worktreesDir(mainRoot), slug);
|
|
12063
12175
|
}
|
|
12064
12176
|
async function readWorktreeState(worktreeRoot) {
|
|
12065
|
-
const state = await readJson(
|
|
12177
|
+
const state = await readJson(path64.join(worktreeRoot, WORKTREE_STATE_FILE));
|
|
12066
12178
|
if (state == null || typeof state !== "object" || !Array.isArray(state.members)) return void 0;
|
|
12067
12179
|
return state;
|
|
12068
12180
|
}
|
|
12069
12181
|
async function writeWorktreeState(worktreeRoot, state) {
|
|
12070
|
-
await writeJson(
|
|
12182
|
+
await writeJson(path64.join(worktreeRoot, WORKTREE_STATE_FILE), state);
|
|
12071
12183
|
}
|
|
12072
12184
|
|
|
12073
12185
|
// src/workspace/worktree/add.ts
|
|
@@ -12101,7 +12213,7 @@ async function runAdd(opts) {
|
|
|
12101
12213
|
const memberResults = [];
|
|
12102
12214
|
for (const member of selected) {
|
|
12103
12215
|
const start = Date.now();
|
|
12104
|
-
const memberWorktreePath =
|
|
12216
|
+
const memberWorktreePath = path65.join(wsWorktreePath, member.folder);
|
|
12105
12217
|
const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
|
|
12106
12218
|
preferDefaultBranchFrom: true
|
|
12107
12219
|
});
|
|
@@ -12131,7 +12243,7 @@ async function runAdd(opts) {
|
|
|
12131
12243
|
await mapWithConcurrency(okResults, async (result) => {
|
|
12132
12244
|
const member = selected.find((m) => m.id === result.member);
|
|
12133
12245
|
if (!member) return;
|
|
12134
|
-
const memberWorktreePath =
|
|
12246
|
+
const memberWorktreePath = path65.join(wsWorktreePath, member.folder);
|
|
12135
12247
|
const hydrationStart = Date.now();
|
|
12136
12248
|
if (opts.hydrate !== false) {
|
|
12137
12249
|
result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
|
|
@@ -12198,25 +12310,25 @@ async function runDryRun(args) {
|
|
|
12198
12310
|
|
|
12199
12311
|
// src/workspace/worktree/doctor.ts
|
|
12200
12312
|
import { existsSync as existsSync6 } from "node:fs";
|
|
12201
|
-
import
|
|
12313
|
+
import path67 from "node:path";
|
|
12202
12314
|
|
|
12203
12315
|
// src/workspace/worktree/orphans.ts
|
|
12204
12316
|
import { existsSync as existsSync5 } from "node:fs";
|
|
12205
|
-
import
|
|
12317
|
+
import path66 from "node:path";
|
|
12206
12318
|
var WORKSPACE_REPO_ID = "(workspace)";
|
|
12207
12319
|
async function findOrphanedRegistrations(repos, wsWorktreesDir) {
|
|
12208
12320
|
const orphans = [];
|
|
12209
12321
|
for (const repo of repos) {
|
|
12210
12322
|
const entries = await listWorktrees(repo.absPath);
|
|
12211
12323
|
for (const entry of entries) {
|
|
12212
|
-
const entryPath =
|
|
12213
|
-
const marker = `${
|
|
12324
|
+
const entryPath = path66.normalize(entry.path);
|
|
12325
|
+
const marker = `${path66.sep}.claude${path66.sep}worktrees${path66.sep}`;
|
|
12214
12326
|
const idx = entryPath.indexOf(marker);
|
|
12215
12327
|
if (idx === -1) continue;
|
|
12216
12328
|
const rest = entryPath.slice(idx + marker.length);
|
|
12217
|
-
const slug = rest.split(
|
|
12329
|
+
const slug = rest.split(path66.sep)[0];
|
|
12218
12330
|
if (!slug) continue;
|
|
12219
|
-
if (existsSync5(
|
|
12331
|
+
if (existsSync5(path66.join(wsWorktreesDir, slug))) continue;
|
|
12220
12332
|
orphans.push({
|
|
12221
12333
|
repoId: repo.id,
|
|
12222
12334
|
ownerRepo: repo.absPath,
|
|
@@ -12231,7 +12343,7 @@ async function findOrphanedRegistrations(repos, wsWorktreesDir) {
|
|
|
12231
12343
|
// src/workspace/worktree/doctor.ts
|
|
12232
12344
|
var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
|
|
12233
12345
|
function looksHydrated(memberPath) {
|
|
12234
|
-
return HYDRATION_MARKERS.some((marker) => existsSync6(
|
|
12346
|
+
return HYDRATION_MARKERS.some((marker) => existsSync6(path67.join(memberPath, marker)));
|
|
12235
12347
|
}
|
|
12236
12348
|
async function runDoctor2() {
|
|
12237
12349
|
const rootInfo = await resolveRoots();
|
|
@@ -12247,7 +12359,7 @@ async function runDoctor2() {
|
|
|
12247
12359
|
};
|
|
12248
12360
|
}
|
|
12249
12361
|
const workspaceRoot = rootInfo.mainRoot;
|
|
12250
|
-
const isWorkspace = existsSync6(
|
|
12362
|
+
const isWorkspace = existsSync6(path67.join(workspaceRoot, WORKSPACE_FILE)) || existsSync6(path67.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
12251
12363
|
if (!isWorkspace) {
|
|
12252
12364
|
return {
|
|
12253
12365
|
isWorkspace: false,
|
|
@@ -12262,7 +12374,7 @@ async function runDoctor2() {
|
|
|
12262
12374
|
};
|
|
12263
12375
|
}
|
|
12264
12376
|
const wsWorktreesDir = worktreesDir(workspaceRoot);
|
|
12265
|
-
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${
|
|
12377
|
+
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path67.sep}`);
|
|
12266
12378
|
const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
|
|
12267
12379
|
const workspaceConfig = await readWorkspaceConfig(workspaceRoot);
|
|
12268
12380
|
const autoHydrate = workspaceConfig?.worktree?.autoHydrate === true;
|
|
@@ -12273,10 +12385,10 @@ async function runDoctor2() {
|
|
|
12273
12385
|
let slug;
|
|
12274
12386
|
if (inWorkspaceWorktree) {
|
|
12275
12387
|
const wtPath = rootInfo.repoRoot;
|
|
12276
|
-
slug = rootInfo.worktreeName ??
|
|
12388
|
+
slug = rootInfo.worktreeName ?? path67.basename(wtPath);
|
|
12277
12389
|
const state = await readWorktreeState(wtPath);
|
|
12278
12390
|
for (const member of members) {
|
|
12279
|
-
const memberPath =
|
|
12391
|
+
const memberPath = path67.join(wtPath, member.folder);
|
|
12280
12392
|
const materialized = existsSync6(memberPath);
|
|
12281
12393
|
const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
|
|
12282
12394
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
@@ -12316,7 +12428,7 @@ async function runDoctor2() {
|
|
|
12316
12428
|
wsWorktreesDir
|
|
12317
12429
|
);
|
|
12318
12430
|
const orphans = orphanScan.map(
|
|
12319
|
-
(o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${
|
|
12431
|
+
(o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${path67.join(wsWorktreesDir, o.slug)} no longer exists.`
|
|
12320
12432
|
);
|
|
12321
12433
|
if (orphans.length > 0) {
|
|
12322
12434
|
problems.push(...orphans);
|
|
@@ -12337,7 +12449,7 @@ async function runDoctor2() {
|
|
|
12337
12449
|
// src/workspace/worktree/hydrate-command.ts
|
|
12338
12450
|
init_fs();
|
|
12339
12451
|
import { existsSync as existsSync7 } from "node:fs";
|
|
12340
|
-
import
|
|
12452
|
+
import path68 from "node:path";
|
|
12341
12453
|
async function runHydrateWorktree(opts) {
|
|
12342
12454
|
const rootInfo = await resolveRoots();
|
|
12343
12455
|
if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
|
|
@@ -12348,7 +12460,7 @@ async function runHydrateWorktree(opts) {
|
|
|
12348
12460
|
}
|
|
12349
12461
|
const workspaceRoot = rootInfo.mainRoot;
|
|
12350
12462
|
const wtPath = rootInfo.repoRoot;
|
|
12351
|
-
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${
|
|
12463
|
+
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path68.sep}`)) {
|
|
12352
12464
|
return {
|
|
12353
12465
|
ok: false,
|
|
12354
12466
|
error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
|
|
@@ -12360,13 +12472,13 @@ async function runHydrateWorktree(opts) {
|
|
|
12360
12472
|
return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
|
|
12361
12473
|
}
|
|
12362
12474
|
const state = await readWorktreeState(wtPath);
|
|
12363
|
-
const slug = state?.slug ??
|
|
12475
|
+
const slug = state?.slug ?? path68.basename(wtPath);
|
|
12364
12476
|
const detectedBranch = await currentBranch(wtPath);
|
|
12365
12477
|
const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
|
|
12366
12478
|
if (opts.dryRun) {
|
|
12367
12479
|
const results2 = await Promise.all(
|
|
12368
12480
|
selected.map(async (member) => {
|
|
12369
|
-
const memberPath =
|
|
12481
|
+
const memberPath = path68.join(wtPath, member.folder);
|
|
12370
12482
|
if (existsSync7(memberPath)) {
|
|
12371
12483
|
return { member: member.id, folder: member.folder, status: "planned" };
|
|
12372
12484
|
}
|
|
@@ -12387,7 +12499,7 @@ async function runHydrateWorktree(opts) {
|
|
|
12387
12499
|
const results = await mapWithConcurrency(
|
|
12388
12500
|
selected,
|
|
12389
12501
|
async (member) => {
|
|
12390
|
-
const memberPath =
|
|
12502
|
+
const memberPath = path68.join(wtPath, member.folder);
|
|
12391
12503
|
let branchAction;
|
|
12392
12504
|
if (!existsSync7(memberPath)) {
|
|
12393
12505
|
if (!opts.materialize) {
|
|
@@ -12450,7 +12562,7 @@ async function runHydrateWorktree(opts) {
|
|
|
12450
12562
|
}
|
|
12451
12563
|
|
|
12452
12564
|
// src/workspace/worktree/list.ts
|
|
12453
|
-
import
|
|
12565
|
+
import path69 from "node:path";
|
|
12454
12566
|
import fs47 from "fs-extra";
|
|
12455
12567
|
async function runList2() {
|
|
12456
12568
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -12463,12 +12575,12 @@ async function runList2() {
|
|
|
12463
12575
|
const configuredMembers = await readMembers(rootInfo);
|
|
12464
12576
|
const worktrees = [];
|
|
12465
12577
|
for (const slug of slugs) {
|
|
12466
|
-
const wtPath =
|
|
12578
|
+
const wtPath = path69.join(dir, slug);
|
|
12467
12579
|
const state = await readWorktreeState(wtPath);
|
|
12468
12580
|
const members = [];
|
|
12469
12581
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
12470
12582
|
for (const m of source) {
|
|
12471
|
-
const memberPath =
|
|
12583
|
+
const memberPath = path69.join(wtPath, m.folder);
|
|
12472
12584
|
const materialized = await fs47.pathExists(memberPath);
|
|
12473
12585
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
12474
12586
|
members.push({
|
|
@@ -12530,7 +12642,7 @@ async function runPrune2(opts) {
|
|
|
12530
12642
|
}
|
|
12531
12643
|
|
|
12532
12644
|
// src/workspace/worktree/remove.ts
|
|
12533
|
-
import
|
|
12645
|
+
import path70 from "node:path";
|
|
12534
12646
|
import fs48 from "fs-extra";
|
|
12535
12647
|
async function runRemove2(opts) {
|
|
12536
12648
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -12557,7 +12669,7 @@ async function runRemove2(opts) {
|
|
|
12557
12669
|
targets.push({
|
|
12558
12670
|
repo: configMember?.id ?? stateMember.id,
|
|
12559
12671
|
ownerRepo,
|
|
12560
|
-
path:
|
|
12672
|
+
path: path70.join(wsWorktreePath, stateMember.folder)
|
|
12561
12673
|
});
|
|
12562
12674
|
}
|
|
12563
12675
|
const existingTargets = [];
|
|
@@ -12629,10 +12741,11 @@ function emitJson(result) {
|
|
|
12629
12741
|
log(JSON.stringify(result, null, 2));
|
|
12630
12742
|
}
|
|
12631
12743
|
function describeInstall(hydration) {
|
|
12632
|
-
|
|
12633
|
-
|
|
12634
|
-
|
|
12635
|
-
|
|
12744
|
+
return summarizeInstall(hydration.install);
|
|
12745
|
+
}
|
|
12746
|
+
function emitMemberLine(hydration, line2) {
|
|
12747
|
+
if (hydration?.install.ran && !hydration.install.ok) warn(line2);
|
|
12748
|
+
else log(line2);
|
|
12636
12749
|
}
|
|
12637
12750
|
async function handleAdd(opts) {
|
|
12638
12751
|
if (!opts.slug) {
|
|
@@ -12677,7 +12790,10 @@ async function handleAdd(opts) {
|
|
|
12677
12790
|
for (const m of result.members) {
|
|
12678
12791
|
if (m.status === "ok") {
|
|
12679
12792
|
const hydrationNote = m.hydration ? describeInstall(m.hydration) : "not hydrated (--no-hydrate)";
|
|
12680
|
-
|
|
12793
|
+
emitMemberLine(
|
|
12794
|
+
m.hydration,
|
|
12795
|
+
` ${m.member}: ${m.branchAction} "${m.branch}" \u2014 ${hydrationNote} (${m.durationMs}ms)`
|
|
12796
|
+
);
|
|
12681
12797
|
} else {
|
|
12682
12798
|
warn(` ${m.member}: FAILED \u2014 ${m.error}`);
|
|
12683
12799
|
}
|
|
@@ -12721,7 +12837,10 @@ async function handleHydrate(opts) {
|
|
|
12721
12837
|
for (const r of result.results) {
|
|
12722
12838
|
if (r.status === "ok" && r.hydration) {
|
|
12723
12839
|
const materializedNote = r.branchAction ? `materialized (${r.branchAction}), ` : "";
|
|
12724
|
-
|
|
12840
|
+
emitMemberLine(
|
|
12841
|
+
r.hydration,
|
|
12842
|
+
` ${r.member}: ${materializedNote}${describeInstall(r.hydration)}`
|
|
12843
|
+
);
|
|
12725
12844
|
} else {
|
|
12726
12845
|
warn(` ${r.member}: ${r.status}${r.error ? ` \u2014 ${r.error}` : ""}`);
|
|
12727
12846
|
}
|
|
@@ -12888,7 +13007,7 @@ async function runWorktree(action, opts = {}) {
|
|
|
12888
13007
|
|
|
12889
13008
|
// src/commands/workspace.ts
|
|
12890
13009
|
import { existsSync as existsSync11, statSync as statSync3 } from "node:fs";
|
|
12891
|
-
import
|
|
13010
|
+
import path79 from "node:path";
|
|
12892
13011
|
init_fs();
|
|
12893
13012
|
init_logger();
|
|
12894
13013
|
|
|
@@ -12947,7 +13066,7 @@ ${summaries.map(
|
|
|
12947
13066
|
|
|
12948
13067
|
// src/commands/workspace/doctor.ts
|
|
12949
13068
|
import { existsSync as existsSync8 } from "node:fs";
|
|
12950
|
-
import
|
|
13069
|
+
import path72 from "node:path";
|
|
12951
13070
|
init_logger();
|
|
12952
13071
|
init_paths();
|
|
12953
13072
|
|
|
@@ -12955,14 +13074,14 @@ init_paths();
|
|
|
12955
13074
|
init_fs();
|
|
12956
13075
|
init_paths();
|
|
12957
13076
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
12958
|
-
import
|
|
13077
|
+
import path71 from "node:path";
|
|
12959
13078
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
12960
13079
|
function manifestPath(workspaceRoot) {
|
|
12961
13080
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
12962
13081
|
}
|
|
12963
13082
|
function hausVersion() {
|
|
12964
13083
|
try {
|
|
12965
|
-
const pkg = JSON.parse(readFileSync4(
|
|
13084
|
+
const pkg = JSON.parse(readFileSync4(path71.join(packageRoot(), "package.json"), "utf8"));
|
|
12966
13085
|
return pkg.version ?? "0.0.0";
|
|
12967
13086
|
} catch {
|
|
12968
13087
|
return "0.0.0";
|
|
@@ -13054,7 +13173,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
13054
13173
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
13055
13174
|
const catalogRefByRepo = [];
|
|
13056
13175
|
for (const repo of config.repos) {
|
|
13057
|
-
const repoRoot =
|
|
13176
|
+
const repoRoot = path72.resolve(workspaceRoot, repo.path);
|
|
13058
13177
|
const entry = manifestByName.get(repo.name);
|
|
13059
13178
|
if (!entry) {
|
|
13060
13179
|
flag({
|
|
@@ -13127,7 +13246,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
13127
13246
|
});
|
|
13128
13247
|
continue;
|
|
13129
13248
|
}
|
|
13130
|
-
if (!existsSync8(
|
|
13249
|
+
if (!existsSync8(path72.join(workspaceRoot, entry.path))) {
|
|
13131
13250
|
flag({
|
|
13132
13251
|
repo: entry.repo,
|
|
13133
13252
|
kind: "missing-linked-context-copy",
|
|
@@ -13173,17 +13292,17 @@ function emit(args) {
|
|
|
13173
13292
|
init_logger();
|
|
13174
13293
|
|
|
13175
13294
|
// src/workspace/link-context/link.ts
|
|
13176
|
-
import
|
|
13295
|
+
import path75 from "node:path";
|
|
13177
13296
|
import fs51 from "fs-extra";
|
|
13178
13297
|
init_fs();
|
|
13179
13298
|
init_logger();
|
|
13180
13299
|
|
|
13181
13300
|
// src/workspace/link-context/plan.ts
|
|
13182
|
-
import
|
|
13301
|
+
import path74 from "node:path";
|
|
13183
13302
|
import fs50 from "fs-extra";
|
|
13184
13303
|
|
|
13185
13304
|
// src/workspace/link-context/scan-source.ts
|
|
13186
|
-
import
|
|
13305
|
+
import path73 from "node:path";
|
|
13187
13306
|
import fs49 from "fs-extra";
|
|
13188
13307
|
var ASSET_SUBDIR = {
|
|
13189
13308
|
skill: "skills",
|
|
@@ -13192,36 +13311,36 @@ var ASSET_SUBDIR = {
|
|
|
13192
13311
|
};
|
|
13193
13312
|
async function hasLinkableContext(member) {
|
|
13194
13313
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
13195
|
-
if (await fs49.pathExists(
|
|
13314
|
+
if (await fs49.pathExists(path73.join(member.absPath, ".claude", subdir))) return true;
|
|
13196
13315
|
}
|
|
13197
13316
|
return false;
|
|
13198
13317
|
}
|
|
13199
13318
|
async function collectSkills(member) {
|
|
13200
|
-
const dir =
|
|
13319
|
+
const dir = path73.join(member.absPath, ".claude", "skills");
|
|
13201
13320
|
if (!await fs49.pathExists(dir)) return [];
|
|
13202
13321
|
const names = await fs49.readdir(dir);
|
|
13203
13322
|
const out = [];
|
|
13204
13323
|
for (const name of [...names].sort()) {
|
|
13205
|
-
const abs =
|
|
13324
|
+
const abs = path73.join(dir, name);
|
|
13206
13325
|
const stat = await fs49.lstat(abs);
|
|
13207
13326
|
if (!stat.isDirectory()) continue;
|
|
13208
|
-
if (!await fs49.pathExists(
|
|
13209
|
-
out.push({ type: "skill", name, sourceRelPath:
|
|
13327
|
+
if (!await fs49.pathExists(path73.join(abs, "SKILL.md"))) continue;
|
|
13328
|
+
out.push({ type: "skill", name, sourceRelPath: path73.posix.join(".claude", "skills", name) });
|
|
13210
13329
|
}
|
|
13211
13330
|
return out;
|
|
13212
13331
|
}
|
|
13213
13332
|
async function collectFlatMarkdown(member, type) {
|
|
13214
13333
|
const subdir = ASSET_SUBDIR[type];
|
|
13215
|
-
const dir =
|
|
13334
|
+
const dir = path73.join(member.absPath, ".claude", subdir);
|
|
13216
13335
|
if (!await fs49.pathExists(dir)) return [];
|
|
13217
13336
|
const names = await fs49.readdir(dir);
|
|
13218
13337
|
const out = [];
|
|
13219
13338
|
for (const fileName of [...names].sort()) {
|
|
13220
13339
|
if (!fileName.endsWith(".md")) continue;
|
|
13221
|
-
const abs =
|
|
13340
|
+
const abs = path73.join(dir, fileName);
|
|
13222
13341
|
if (!(await fs49.lstat(abs)).isFile()) continue;
|
|
13223
13342
|
const name = fileName.slice(0, -".md".length);
|
|
13224
|
-
out.push({ type, name, sourceRelPath:
|
|
13343
|
+
out.push({ type, name, sourceRelPath: path73.posix.join(".claude", subdir, fileName) });
|
|
13225
13344
|
}
|
|
13226
13345
|
return out;
|
|
13227
13346
|
}
|
|
@@ -13236,9 +13355,9 @@ async function collectSourceAssets(member) {
|
|
|
13236
13355
|
|
|
13237
13356
|
// src/workspace/link-context/plan.ts
|
|
13238
13357
|
function repoFolderPrefix(member) {
|
|
13239
|
-
const base =
|
|
13358
|
+
const base = path74.posix.basename(member.folder);
|
|
13240
13359
|
if (base && base !== "." && base !== "/") return base;
|
|
13241
|
-
const fromAbs =
|
|
13360
|
+
const fromAbs = path74.basename(member.absPath);
|
|
13242
13361
|
return fromAbs || member.id;
|
|
13243
13362
|
}
|
|
13244
13363
|
function assetKey(type, destKey) {
|
|
@@ -13295,7 +13414,7 @@ async function buildLinkPlan(members) {
|
|
|
13295
13414
|
name: asset.name,
|
|
13296
13415
|
destKey,
|
|
13297
13416
|
sourceRelPath: asset.sourceRelPath,
|
|
13298
|
-
sourceAbsPath:
|
|
13417
|
+
sourceAbsPath: path74.join(member.absPath, asset.sourceRelPath),
|
|
13299
13418
|
sourceHash
|
|
13300
13419
|
});
|
|
13301
13420
|
}
|
|
@@ -13306,7 +13425,7 @@ async function buildLinkPlan(members) {
|
|
|
13306
13425
|
function destRelPathFor(type, destKey) {
|
|
13307
13426
|
const subdir = `${type}s`;
|
|
13308
13427
|
const leaf = type === "skill" ? destKey : `${destKey}.md`;
|
|
13309
|
-
return
|
|
13428
|
+
return path75.posix.join(".claude", subdir, leaf);
|
|
13310
13429
|
}
|
|
13311
13430
|
async function runLinkContext(workspaceRoot, opts = {}) {
|
|
13312
13431
|
const write = resolveWriteMode(opts, "preview-by-default").kind === "write";
|
|
@@ -13332,9 +13451,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
13332
13451
|
const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13333
13452
|
for (const entry of plan.entries) {
|
|
13334
13453
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
13335
|
-
const destAbs =
|
|
13454
|
+
const destAbs = path75.join(workspaceRoot, destRel);
|
|
13336
13455
|
if (write) {
|
|
13337
|
-
await fs51.ensureDir(
|
|
13456
|
+
await fs51.ensureDir(path75.dirname(destAbs));
|
|
13338
13457
|
if (await fs51.pathExists(destAbs)) await fs51.remove(destAbs);
|
|
13339
13458
|
await fs51.copy(entry.sourceAbsPath, destAbs, {
|
|
13340
13459
|
overwrite: true,
|
|
@@ -13423,10 +13542,10 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
|
|
|
13423
13542
|
|
|
13424
13543
|
// src/commands/workspace/setup.ts
|
|
13425
13544
|
import { existsSync as existsSync9, statSync } from "node:fs";
|
|
13426
|
-
import
|
|
13545
|
+
import path77 from "node:path";
|
|
13427
13546
|
|
|
13428
13547
|
// src/claude/write-workspace-claude-md.ts
|
|
13429
|
-
import
|
|
13548
|
+
import path76 from "node:path";
|
|
13430
13549
|
import fs52 from "fs-extra";
|
|
13431
13550
|
init_fs();
|
|
13432
13551
|
init_logger();
|
|
@@ -13449,7 +13568,7 @@ ${WORKSPACE_BLOCK_END}`;
|
|
|
13449
13568
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
13450
13569
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
13451
13570
|
const dryRun = opts.dryRun ?? false;
|
|
13452
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
13571
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path76.join(workspaceRoot, "CLAUDE.md");
|
|
13453
13572
|
const say = opts.quiet ? () => {
|
|
13454
13573
|
} : log;
|
|
13455
13574
|
const existed = await fs52.pathExists(filePath);
|
|
@@ -13482,20 +13601,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
13482
13601
|
init_fs();
|
|
13483
13602
|
init_logger();
|
|
13484
13603
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
13485
|
-
let dir =
|
|
13604
|
+
let dir = path77.resolve(start);
|
|
13486
13605
|
for (; ; ) {
|
|
13487
|
-
if (existsSync9(
|
|
13488
|
-
const parent =
|
|
13489
|
-
if (parent === dir) return
|
|
13606
|
+
if (existsSync9(path77.join(dir, WORKSPACE_FILE))) return dir;
|
|
13607
|
+
const parent = path77.dirname(dir);
|
|
13608
|
+
if (parent === dir) return path77.resolve(start);
|
|
13490
13609
|
dir = parent;
|
|
13491
13610
|
}
|
|
13492
13611
|
}
|
|
13493
13612
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
13494
|
-
return
|
|
13613
|
+
return path77.resolve(workspaceRoot, repoPath) === path77.resolve(workspaceRoot);
|
|
13495
13614
|
}
|
|
13496
13615
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
13497
13616
|
const apply = options.write ?? false;
|
|
13498
|
-
const configText = await readText(
|
|
13617
|
+
const configText = await readText(path77.join(workspaceRoot, WORKSPACE_FILE));
|
|
13499
13618
|
if (!configText) {
|
|
13500
13619
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
13501
13620
|
process.exitCode = 1;
|
|
@@ -13522,7 +13641,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13522
13641
|
const statuses = [];
|
|
13523
13642
|
const aggregateInputs = [];
|
|
13524
13643
|
for (const repo of repos) {
|
|
13525
|
-
const repoRoot =
|
|
13644
|
+
const repoRoot = path77.resolve(workspaceRoot, repo.path);
|
|
13526
13645
|
say(`
|
|
13527
13646
|
\u2192 ${repo.name} (${repo.path})`);
|
|
13528
13647
|
try {
|
|
@@ -13612,7 +13731,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13612
13731
|
const status = statusByName.get(repo.name);
|
|
13613
13732
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
13614
13733
|
if (status?.status === "ok") {
|
|
13615
|
-
const lock = await checkLock(
|
|
13734
|
+
const lock = await checkLock(path77.resolve(workspaceRoot, repo.path));
|
|
13616
13735
|
manifestRepos.push({
|
|
13617
13736
|
name: repo.name,
|
|
13618
13737
|
path: repo.path,
|
|
@@ -13668,7 +13787,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13668
13787
|
try {
|
|
13669
13788
|
const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
|
|
13670
13789
|
if (linkResult.ok) {
|
|
13671
|
-
written.push(...linkResult.added.map((rel) =>
|
|
13790
|
+
written.push(...linkResult.added.map((rel) => path77.join(workspaceRoot, rel)));
|
|
13672
13791
|
if (!options.json) {
|
|
13673
13792
|
say(
|
|
13674
13793
|
`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` : ""}.`
|
|
@@ -13700,7 +13819,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
13700
13819
|
|
|
13701
13820
|
// src/commands/workspace/undo.ts
|
|
13702
13821
|
import { existsSync as existsSync10, statSync as statSync2 } from "node:fs";
|
|
13703
|
-
import
|
|
13822
|
+
import path78 from "node:path";
|
|
13704
13823
|
import fs53 from "fs-extra";
|
|
13705
13824
|
init_fs();
|
|
13706
13825
|
init_logger();
|
|
@@ -13731,7 +13850,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13731
13850
|
}
|
|
13732
13851
|
if (options.dryRun) {
|
|
13733
13852
|
for (const repo of config.repos) {
|
|
13734
|
-
const repoRoot =
|
|
13853
|
+
const repoRoot = path78.resolve(workspaceRoot, repo.path);
|
|
13735
13854
|
log(`
|
|
13736
13855
|
\u2192 ${repo.name} (${repo.path})`);
|
|
13737
13856
|
try {
|
|
@@ -13763,11 +13882,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13763
13882
|
];
|
|
13764
13883
|
for (const abs of workspaceArtifacts) {
|
|
13765
13884
|
if (await fs53.pathExists(abs)) {
|
|
13766
|
-
log(`[dry-run] Would remove ${
|
|
13885
|
+
log(`[dry-run] Would remove ${path78.relative(workspaceRoot, abs)}`);
|
|
13767
13886
|
}
|
|
13768
13887
|
}
|
|
13769
13888
|
if (!await fs53.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
|
|
13770
|
-
const claudeMd = await readText(
|
|
13889
|
+
const claudeMd = await readText(path78.join(workspaceRoot, "CLAUDE.md"));
|
|
13771
13890
|
if (claudeMd?.includes(WORKSPACE_BLOCK_BEGIN) || claudeMd && hasLegacyWorkspaceBlock(claudeMd)) {
|
|
13772
13891
|
log("[dry-run] Would remove the workspace haus import block from CLAUDE.md");
|
|
13773
13892
|
}
|
|
@@ -13786,7 +13905,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13786
13905
|
}
|
|
13787
13906
|
let anyRepoFailed = false;
|
|
13788
13907
|
for (const repo of config.repos) {
|
|
13789
|
-
const repoRoot =
|
|
13908
|
+
const repoRoot = path78.resolve(workspaceRoot, repo.path);
|
|
13790
13909
|
log(`
|
|
13791
13910
|
\u2192 ${repo.name} (${repo.path})`);
|
|
13792
13911
|
try {
|
|
@@ -13815,20 +13934,20 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13815
13934
|
const abs = hausPath(workspaceRoot, rel);
|
|
13816
13935
|
if (await fs53.pathExists(abs)) {
|
|
13817
13936
|
await fs53.remove(abs);
|
|
13818
|
-
log(`Removed ${
|
|
13937
|
+
log(`Removed ${path78.relative(workspaceRoot, abs)}`);
|
|
13819
13938
|
}
|
|
13820
13939
|
}
|
|
13821
13940
|
const manifestFile = manifestPath(workspaceRoot);
|
|
13822
13941
|
if (await fs53.pathExists(manifestFile)) {
|
|
13823
13942
|
await fs53.remove(manifestFile);
|
|
13824
|
-
log(`Removed ${
|
|
13943
|
+
log(`Removed ${path78.relative(workspaceRoot, manifestFile)}`);
|
|
13825
13944
|
}
|
|
13826
13945
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
13827
13946
|
if (await fs53.pathExists(workspaceMdPath)) {
|
|
13828
13947
|
await fs53.remove(workspaceMdPath);
|
|
13829
|
-
log(`Removed ${
|
|
13948
|
+
log(`Removed ${path78.relative(workspaceRoot, workspaceMdPath)}`);
|
|
13830
13949
|
} else {
|
|
13831
|
-
const claudeMdPath =
|
|
13950
|
+
const claudeMdPath = path78.join(workspaceRoot, "CLAUDE.md");
|
|
13832
13951
|
if (existsSync10(claudeMdPath)) {
|
|
13833
13952
|
const prev = await readText(claudeMdPath);
|
|
13834
13953
|
if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
|
|
@@ -13897,7 +14016,7 @@ relationships: []
|
|
|
13897
14016
|
log("Workspace initialized.");
|
|
13898
14017
|
}
|
|
13899
14018
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
13900
|
-
const configText = await readText(
|
|
14019
|
+
const configText = await readText(path79.join(workspaceRoot, WORKSPACE_FILE));
|
|
13901
14020
|
if (!configText) {
|
|
13902
14021
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
13903
14022
|
process.exitCode = 1;
|
|
@@ -13919,7 +14038,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
13919
14038
|
const inputs = [];
|
|
13920
14039
|
const failures = [];
|
|
13921
14040
|
for (const repo of config.repos) {
|
|
13922
|
-
const repoRoot =
|
|
14041
|
+
const repoRoot = path79.resolve(workspaceRoot, repo.path);
|
|
13923
14042
|
try {
|
|
13924
14043
|
if (!existsSync11(repoRoot) || !statSync3(repoRoot).isDirectory()) {
|
|
13925
14044
|
throw new Error("path is not a directory");
|
|
@@ -13987,7 +14106,7 @@ init_logger();
|
|
|
13987
14106
|
init_paths();
|
|
13988
14107
|
function cliVersion() {
|
|
13989
14108
|
try {
|
|
13990
|
-
const pkgPath =
|
|
14109
|
+
const pkgPath = path80.join(packageRoot(), "package.json");
|
|
13991
14110
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
13992
14111
|
return pkg.version ?? "0.0.0";
|
|
13993
14112
|
} catch {
|
|
@@ -13997,7 +14116,7 @@ function cliVersion() {
|
|
|
13997
14116
|
var program = new Command();
|
|
13998
14117
|
function validateRuntimeNodeVersion() {
|
|
13999
14118
|
try {
|
|
14000
|
-
const pkgPath =
|
|
14119
|
+
const pkgPath = path80.join(packageRoot(), "package.json");
|
|
14001
14120
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
14002
14121
|
const requiredRange = pkg.engines?.node;
|
|
14003
14122
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -14058,7 +14177,7 @@ backups.command("prune").description("Delete old backups by age and/or count (re
|
|
|
14058
14177
|
})
|
|
14059
14178
|
);
|
|
14060
14179
|
program.command("explain-recommendation").description("Print a per-item breakdown of the current recommendation, with skip reasons").option("--json").action(runExplainRecommendation);
|
|
14061
|
-
program.command("init").description("Full setup: scan, recommend, and optionally apply Claude files (safe to re-run)").option("--dry-run", "Preview the `.claude/` write; scan artifacts are still written").option("--json", "
|
|
14180
|
+
program.command("init").description("Full setup: scan, recommend, and optionally apply Claude files (safe to re-run)").option("--dry-run", "Preview the `.claude/` write; scan artifacts are still written").option("--json", "Emit one JSON envelope on stdout (requires --dry-run)").option(
|
|
14062
14181
|
"--force",
|
|
14063
14182
|
"Write recommendation.json/haus.lock.json even when zero catalog items matched at all"
|
|
14064
14183
|
).action(runInit);
|