@haus-tech/haus-workflow 3.0.0 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/cli.js +375 -147
- package/library/catalog/manifest.json +90 -50
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -324,7 +324,8 @@ var init_path_safety = __esm({
|
|
|
324
324
|
// src/utils/exec.ts
|
|
325
325
|
var exec_exports = {};
|
|
326
326
|
__export(exec_exports, {
|
|
327
|
-
|
|
327
|
+
assertNeverCommandProbe: () => assertNeverCommandProbe,
|
|
328
|
+
probeCommand: () => probeCommand,
|
|
328
329
|
runCommand: () => runCommand,
|
|
329
330
|
runGit: () => runGit
|
|
330
331
|
});
|
|
@@ -337,14 +338,17 @@ async function runCommand(command, args = [], options = {}) {
|
|
|
337
338
|
...options
|
|
338
339
|
});
|
|
339
340
|
const killed = result.timedOut === true || result.isCanceled === true || Boolean(result.signal);
|
|
341
|
+
const capturedStderr = String(result.stderr ?? "");
|
|
342
|
+
const timeoutReason = result.timedOut === true ? `timed out after ${options.timeout}ms` : void 0;
|
|
343
|
+
const stderr = timeoutReason === void 0 ? capturedStderr : capturedStderr.length > 0 ? `${capturedStderr}
|
|
344
|
+
${timeoutReason}` : timeoutReason;
|
|
340
345
|
return {
|
|
341
346
|
command,
|
|
342
347
|
args,
|
|
343
348
|
stdout: String(result.stdout ?? ""),
|
|
344
|
-
stderr
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
exitCode: result.exitCode ?? (killed || result.failed === true ? 1 : 0)
|
|
349
|
+
stderr,
|
|
350
|
+
exitCode: result.exitCode ?? (killed || result.failed === true ? 1 : 0),
|
|
351
|
+
killed
|
|
348
352
|
};
|
|
349
353
|
} catch (error2) {
|
|
350
354
|
const message2 = error2 instanceof Error ? error2.message : String(error2);
|
|
@@ -354,14 +358,30 @@ async function runCommand(command, args = [], options = {}) {
|
|
|
354
358
|
async function runGit(args, options = {}) {
|
|
355
359
|
return runCommand("git", args, options);
|
|
356
360
|
}
|
|
357
|
-
|
|
361
|
+
function assertNeverCommandProbe(probe) {
|
|
362
|
+
throw new Error(`Unhandled CommandProbe: ${JSON.stringify(probe)}`);
|
|
363
|
+
}
|
|
364
|
+
async function probeOnce(command, timeoutMs, run) {
|
|
358
365
|
try {
|
|
359
|
-
const result = await
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
366
|
+
const result = await run(command, ["--version"], { timeout: timeoutMs });
|
|
367
|
+
if (result.exitCode === 0) return { status: "available" };
|
|
368
|
+
if (result.killed) {
|
|
369
|
+
return {
|
|
370
|
+
status: "inconclusive",
|
|
371
|
+
reason: result.stderr.length > 0 ? result.stderr : `probe for "${command} --version" did not complete within ${timeoutMs}ms`
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
return { status: "absent" };
|
|
375
|
+
} catch (error2) {
|
|
376
|
+
const message2 = error2 instanceof Error ? error2.message : String(error2);
|
|
377
|
+
return { status: "inconclusive", reason: `probing "${command}" threw: ${message2}` };
|
|
363
378
|
}
|
|
364
379
|
}
|
|
380
|
+
async function probeCommand(command, timeoutMs = 1e4, run = runCommand) {
|
|
381
|
+
const first = await probeOnce(command, timeoutMs, run);
|
|
382
|
+
if (first.status !== "inconclusive") return first;
|
|
383
|
+
return probeOnce(command, timeoutMs, run);
|
|
384
|
+
}
|
|
365
385
|
var init_exec = __esm({
|
|
366
386
|
"src/utils/exec.ts"() {
|
|
367
387
|
"use strict";
|
|
@@ -481,9 +501,9 @@ var init_github_rate_limit = __esm({
|
|
|
481
501
|
// src/catalog/remote-catalog/http.ts
|
|
482
502
|
import path4 from "node:path";
|
|
483
503
|
import fs2 from "fs-extra";
|
|
484
|
-
async function fetchGuarded(url, extract) {
|
|
504
|
+
async function fetchGuarded(url, extract, headers = {}) {
|
|
485
505
|
try {
|
|
486
|
-
const res = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
506
|
+
const res = await fetch(url, { headers, signal: AbortSignal.timeout(1e4) });
|
|
487
507
|
if (!res.ok) {
|
|
488
508
|
warn(`Catalog fetch HTTP ${res.status}: ${url}`);
|
|
489
509
|
return null;
|
|
@@ -494,11 +514,11 @@ async function fetchGuarded(url, extract) {
|
|
|
494
514
|
return null;
|
|
495
515
|
}
|
|
496
516
|
}
|
|
497
|
-
async function fetchText(url) {
|
|
498
|
-
return fetchGuarded(url, (res) => res.text());
|
|
517
|
+
async function fetchText(url, headers = {}) {
|
|
518
|
+
return fetchGuarded(url, (res) => res.text(), headers);
|
|
499
519
|
}
|
|
500
|
-
async function fetchBytes(url) {
|
|
501
|
-
return fetchGuarded(url, async (res) => Buffer.from(await res.arrayBuffer()));
|
|
520
|
+
async function fetchBytes(url, headers = {}) {
|
|
521
|
+
return fetchGuarded(url, async (res) => Buffer.from(await res.arrayBuffer()), headers);
|
|
502
522
|
}
|
|
503
523
|
async function writeTextIfChanged(dest, text) {
|
|
504
524
|
if (await fs2.pathExists(dest)) {
|
|
@@ -728,6 +748,18 @@ async function resolveCatalogRef(opts) {
|
|
|
728
748
|
);
|
|
729
749
|
return "main";
|
|
730
750
|
}
|
|
751
|
+
async function catalogContentRequest(base, rel) {
|
|
752
|
+
if (isTestMode() && process.env["HAUS_CATALOG_REMOTE_BASE"]) {
|
|
753
|
+
return { url: `${base}/${rel}`, headers: {} };
|
|
754
|
+
}
|
|
755
|
+
const ref = base.startsWith(`${CATALOG_REPO_URL}/`) ? base.slice(CATALOG_REPO_URL.length + 1) : getResolvedCatalogRef();
|
|
756
|
+
const headers = await getGithubApiHeaders();
|
|
757
|
+
headers["Accept"] = "application/vnd.github.raw";
|
|
758
|
+
return {
|
|
759
|
+
url: `${CATALOG_GITHUB_API_URL}/contents/${rel}?ref=${encodeURIComponent(ref)}`,
|
|
760
|
+
headers
|
|
761
|
+
};
|
|
762
|
+
}
|
|
731
763
|
async function remoteBase(projectRoot) {
|
|
732
764
|
if (isTestMode() && process.env["HAUS_CATALOG_REMOTE_BASE"]) {
|
|
733
765
|
return process.env["HAUS_CATALOG_REMOTE_BASE"];
|
|
@@ -1147,7 +1179,8 @@ var init_manifest_schema = __esm({
|
|
|
1147
1179
|
// src/catalog/remote-catalog/manifest.ts
|
|
1148
1180
|
async function fetchRemoteManifest(projectRoot) {
|
|
1149
1181
|
const base = await remoteBase(projectRoot);
|
|
1150
|
-
const
|
|
1182
|
+
const { url, headers } = await catalogContentRequest(base, "manifest.json");
|
|
1183
|
+
const text = await fetchText(url, headers);
|
|
1151
1184
|
if (!text) return { ok: false, reason: "unavailable" };
|
|
1152
1185
|
const parsed = parseManifest(text);
|
|
1153
1186
|
if (!parsed.ok) {
|
|
@@ -1533,16 +1566,16 @@ function isMarkdownPath(rel) {
|
|
|
1533
1566
|
async function fetchPrefixFiles(catalogPrefix, relFiles, base, label) {
|
|
1534
1567
|
const fetched = [];
|
|
1535
1568
|
for (const rel of relFiles) {
|
|
1536
|
-
const url = `${
|
|
1569
|
+
const { url, headers } = await catalogContentRequest(base, `${catalogPrefix}/${rel}`);
|
|
1537
1570
|
if (isMarkdownPath(rel)) {
|
|
1538
|
-
const text = await fetchText(url);
|
|
1571
|
+
const text = await fetchText(url, headers);
|
|
1539
1572
|
if (text === null) {
|
|
1540
1573
|
warn(`Failed to fetch ${rel} for ${label}`);
|
|
1541
1574
|
return null;
|
|
1542
1575
|
}
|
|
1543
1576
|
fetched.push({ rel, kind: "text", body: text });
|
|
1544
1577
|
} else {
|
|
1545
|
-
const bytes = await fetchBytes(url);
|
|
1578
|
+
const bytes = await fetchBytes(url, headers);
|
|
1546
1579
|
if (bytes === null) {
|
|
1547
1580
|
warn(`Failed to fetch ${rel} for ${label}`);
|
|
1548
1581
|
return null;
|
|
@@ -1681,7 +1714,8 @@ async function syncConfigItem(item, base) {
|
|
|
1681
1714
|
return "failed";
|
|
1682
1715
|
}
|
|
1683
1716
|
}
|
|
1684
|
-
const
|
|
1717
|
+
const configReq = await catalogContentRequest(base, item.path);
|
|
1718
|
+
const text = await fetchText(configReq.url, configReq.headers);
|
|
1685
1719
|
if (text === null) {
|
|
1686
1720
|
warn(`Failed to fetch content for ${item.id}`);
|
|
1687
1721
|
return "failed";
|
|
@@ -1739,7 +1773,8 @@ async function syncOneItem(item, base) {
|
|
|
1739
1773
|
warn(`Skipping ${item.id}: path traversal detected`);
|
|
1740
1774
|
return "failed";
|
|
1741
1775
|
}
|
|
1742
|
-
const
|
|
1776
|
+
const itemReq = await catalogContentRequest(base, item.path);
|
|
1777
|
+
const text = await fetchText(itemReq.url, itemReq.headers);
|
|
1743
1778
|
if (text === null) {
|
|
1744
1779
|
warn(`Failed to fetch content for ${item.id}`);
|
|
1745
1780
|
return "failed";
|
|
@@ -1878,7 +1913,7 @@ var init_sync = __esm({
|
|
|
1878
1913
|
|
|
1879
1914
|
// src/cli.ts
|
|
1880
1915
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
1881
|
-
import
|
|
1916
|
+
import path79 from "node:path";
|
|
1882
1917
|
import { Command } from "commander";
|
|
1883
1918
|
|
|
1884
1919
|
// src/commands/apply.ts
|
|
@@ -1908,7 +1943,8 @@ var WORKFLOW_TEMPLATE_REL = "templates/agentic-workflow-standard.md";
|
|
|
1908
1943
|
async function readWorkflowTemplate(opts = {}) {
|
|
1909
1944
|
const dest = path7.join(getCacheDir(), WORKFLOW_TEMPLATE_REL);
|
|
1910
1945
|
const base = await remoteBase(opts.projectRoot);
|
|
1911
|
-
const
|
|
1946
|
+
const { url, headers } = await catalogContentRequest(base, WORKFLOW_TEMPLATE_REL);
|
|
1947
|
+
const text = await fetchText(url, headers);
|
|
1912
1948
|
if (text === null) {
|
|
1913
1949
|
if (await fs6.pathExists(dest)) return fs6.readFile(dest, "utf8");
|
|
1914
1950
|
return null;
|
|
@@ -3014,8 +3050,8 @@ function buildDenyRules() {
|
|
|
3014
3050
|
for (const command of DENY_COMMANDS) {
|
|
3015
3051
|
rules.push(`Bash(${command}:*)`);
|
|
3016
3052
|
}
|
|
3017
|
-
for (const
|
|
3018
|
-
const pattern = DENY_DIRS.has(
|
|
3053
|
+
for (const path80 of DENY_PATHS) {
|
|
3054
|
+
const pattern = DENY_DIRS.has(path80) ? `${path80}/**` : path80;
|
|
3019
3055
|
for (const tool of FILE_TOOLS) {
|
|
3020
3056
|
rules.push(`${tool}(${pattern})`);
|
|
3021
3057
|
}
|
|
@@ -3228,9 +3264,20 @@ async function installSkillDependencies(opts, deps = {}) {
|
|
|
3228
3264
|
if (!planned.ok) return { status: "skipped", reason: planned.reason };
|
|
3229
3265
|
const { command, args } = planned.command;
|
|
3230
3266
|
if (opts.dryRun) return { status: "would-install", command, args };
|
|
3231
|
-
const exists = deps.exists ??
|
|
3232
|
-
|
|
3233
|
-
|
|
3267
|
+
const exists = deps.exists ?? probeCommand;
|
|
3268
|
+
const probe = await exists(command);
|
|
3269
|
+
switch (probe.status) {
|
|
3270
|
+
case "available":
|
|
3271
|
+
break;
|
|
3272
|
+
case "absent":
|
|
3273
|
+
return { status: "skipped", reason: `${command} not found on PATH` };
|
|
3274
|
+
case "inconclusive":
|
|
3275
|
+
return {
|
|
3276
|
+
status: "skipped",
|
|
3277
|
+
reason: `could not determine whether ${command} is on PATH (${probe.reason}) \u2014 dependency install skipped`
|
|
3278
|
+
};
|
|
3279
|
+
default:
|
|
3280
|
+
return assertNeverCommandProbe(probe);
|
|
3234
3281
|
}
|
|
3235
3282
|
if (!await fs14.pathExists(contained.dir)) {
|
|
3236
3283
|
return { status: "skipped", reason: `install directory disappeared (${contained.dir})` };
|
|
@@ -4962,8 +5009,24 @@ async function parseMarketplaces(stdout2, configDir) {
|
|
|
4962
5009
|
}
|
|
4963
5010
|
async function probePluginEnv(deps = {}) {
|
|
4964
5011
|
const exec = deps.exec ?? realExec;
|
|
4965
|
-
const isAvailable = deps.isAvailable ?? (() => commandExists("claude"));
|
|
4966
5012
|
const warn2 = deps.warn ?? warn;
|
|
5013
|
+
const probeClaude = deps.probeClaude ?? (() => probeCommand("claude"));
|
|
5014
|
+
const isAvailable = deps.isAvailable ?? (async () => {
|
|
5015
|
+
const probe = await probeClaude();
|
|
5016
|
+
switch (probe.status) {
|
|
5017
|
+
case "available":
|
|
5018
|
+
return true;
|
|
5019
|
+
case "absent":
|
|
5020
|
+
return false;
|
|
5021
|
+
case "inconclusive":
|
|
5022
|
+
warn2(
|
|
5023
|
+
`Could not determine whether the \`claude\` CLI is on PATH (${probe.reason}) \u2014 leaving global plugins untouched this run.`
|
|
5024
|
+
);
|
|
5025
|
+
return false;
|
|
5026
|
+
default:
|
|
5027
|
+
return assertNeverCommandProbe(probe);
|
|
5028
|
+
}
|
|
5029
|
+
});
|
|
4967
5030
|
if (globalPluginsDisabled() && deps.exec === void 0) {
|
|
4968
5031
|
return { available: false, installed: [], marketplaces: [] };
|
|
4969
5032
|
}
|
|
@@ -6772,10 +6835,13 @@ function parseWorkspaceConfig(text) {
|
|
|
6772
6835
|
const repos = Array.isArray(obj.repos) ? obj.repos.filter(
|
|
6773
6836
|
(r) => typeof r === "object" && r !== null && typeof r.name === "string" && typeof r.path === "string"
|
|
6774
6837
|
) : [];
|
|
6838
|
+
const worktreeRaw = parsed.worktree;
|
|
6839
|
+
const worktree2 = typeof worktreeRaw === "object" && worktreeRaw !== null && !Array.isArray(worktreeRaw) ? { autoHydrate: worktreeRaw.autoHydrate === true } : void 0;
|
|
6775
6840
|
return {
|
|
6776
6841
|
client: typeof obj.client === "string" ? obj.client : "unknown",
|
|
6777
6842
|
repos,
|
|
6778
|
-
relationships: Array.isArray(obj.relationships) ? obj.relationships : []
|
|
6843
|
+
relationships: Array.isArray(obj.relationships) ? obj.relationships : [],
|
|
6844
|
+
...worktree2 ? { worktree: worktree2 } : {}
|
|
6779
6845
|
};
|
|
6780
6846
|
}
|
|
6781
6847
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
@@ -10139,7 +10205,7 @@ function buildCompanionSuggestions(tools, isInstalled) {
|
|
|
10139
10205
|
}
|
|
10140
10206
|
return lines;
|
|
10141
10207
|
}
|
|
10142
|
-
function
|
|
10208
|
+
function commandExists(bin) {
|
|
10143
10209
|
try {
|
|
10144
10210
|
const r = execaSync("sh", ["-c", 'command -v -- "$1"', "probe", bin], { reject: false });
|
|
10145
10211
|
return r.exitCode === 0;
|
|
@@ -10148,7 +10214,7 @@ function commandExists2(bin) {
|
|
|
10148
10214
|
}
|
|
10149
10215
|
}
|
|
10150
10216
|
function printCompanionToolSuggestions(deps = {}) {
|
|
10151
|
-
const isInstalled = deps.isInstalled ??
|
|
10217
|
+
const isInstalled = deps.isInstalled ?? commandExists;
|
|
10152
10218
|
const emit2 = deps.log ?? log;
|
|
10153
10219
|
const lines = buildCompanionSuggestions(COMPANION_TOOLS, isInstalled);
|
|
10154
10220
|
for (const line2 of lines) emit2(line2);
|
|
@@ -10239,7 +10305,7 @@ async function safeExec(exec, command, args, options) {
|
|
|
10239
10305
|
return await exec(command, args, options);
|
|
10240
10306
|
} catch (error2) {
|
|
10241
10307
|
const message2 = error2 instanceof Error ? error2.message : String(error2);
|
|
10242
|
-
return { command, args, stdout: "", stderr: message2, exitCode: 1 };
|
|
10308
|
+
return { command, args, stdout: "", stderr: message2, exitCode: 1, killed: false };
|
|
10243
10309
|
}
|
|
10244
10310
|
}
|
|
10245
10311
|
async function checkGh(exec = runCommand) {
|
|
@@ -10347,7 +10413,7 @@ var SCAFFOLD_TIMEOUT_MS = 3e5;
|
|
|
10347
10413
|
var defaultDeps = {
|
|
10348
10414
|
exec: runCommand,
|
|
10349
10415
|
git: runGit,
|
|
10350
|
-
commandExists,
|
|
10416
|
+
commandExists: probeCommand,
|
|
10351
10417
|
checkGh: () => checkGh(),
|
|
10352
10418
|
checkOrg: (org) => checkOrg(org),
|
|
10353
10419
|
createRepo,
|
|
@@ -10405,8 +10471,32 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
10405
10471
|
});
|
|
10406
10472
|
return { ok: true, projectPath, installHint, steps };
|
|
10407
10473
|
}
|
|
10408
|
-
|
|
10409
|
-
|
|
10474
|
+
const requiredTools = ["npx", opts.pm];
|
|
10475
|
+
const missingTools = [];
|
|
10476
|
+
const inconclusiveTools = [];
|
|
10477
|
+
for (const tool of requiredTools) {
|
|
10478
|
+
const probe = await deps.commandExists(tool);
|
|
10479
|
+
switch (probe.status) {
|
|
10480
|
+
case "available":
|
|
10481
|
+
break;
|
|
10482
|
+
case "absent":
|
|
10483
|
+
missingTools.push(tool);
|
|
10484
|
+
break;
|
|
10485
|
+
case "inconclusive":
|
|
10486
|
+
inconclusiveTools.push(`${tool} (${probe.reason})`);
|
|
10487
|
+
break;
|
|
10488
|
+
default:
|
|
10489
|
+
return assertNeverCommandProbe(probe);
|
|
10490
|
+
}
|
|
10491
|
+
}
|
|
10492
|
+
if (missingTools.length > 0) {
|
|
10493
|
+
return fail("preflight", `Required tools missing: need ${missingTools.join(" and ")} on PATH.`);
|
|
10494
|
+
}
|
|
10495
|
+
if (inconclusiveTools.length > 0) {
|
|
10496
|
+
return fail(
|
|
10497
|
+
"preflight",
|
|
10498
|
+
`Could not confirm tool availability on PATH: ${inconclusiveTools.join("; ")} \u2014 the probe did not complete (likely transient load). Try again.`
|
|
10499
|
+
);
|
|
10410
10500
|
}
|
|
10411
10501
|
if (opts.github) {
|
|
10412
10502
|
const gh = await deps.checkGh();
|
|
@@ -11657,7 +11747,8 @@ async function removeWorktree(ownerRepo, worktreePath2, force = true) {
|
|
|
11657
11747
|
return { ok: result.exitCode === 0, stderr: result.exitCode === 0 ? void 0 : result.stderr };
|
|
11658
11748
|
}
|
|
11659
11749
|
async function pruneWorktrees(ownerRepo) {
|
|
11660
|
-
await runGit(["worktree", "prune"], { cwd: ownerRepo });
|
|
11750
|
+
const result = await runGit(["worktree", "prune"], { cwd: ownerRepo });
|
|
11751
|
+
return { ok: result.exitCode === 0, stderr: result.exitCode === 0 ? void 0 : result.stderr };
|
|
11661
11752
|
}
|
|
11662
11753
|
async function listWorktrees(ownerRepo) {
|
|
11663
11754
|
const result = await runGit(["worktree", "list", "--porcelain"], { cwd: ownerRepo });
|
|
@@ -11835,13 +11926,26 @@ async function computeLockfileSignals(dir) {
|
|
|
11835
11926
|
hasDotnetProject
|
|
11836
11927
|
};
|
|
11837
11928
|
}
|
|
11838
|
-
async function runInstallPlan(dir, plan) {
|
|
11929
|
+
async function runInstallPlan(dir, plan, deps = {}) {
|
|
11839
11930
|
if (plan.manager === "none") {
|
|
11840
11931
|
return { plan, ran: false, ok: true, skippedReason: "no recognized lockfile" };
|
|
11841
11932
|
}
|
|
11842
|
-
const
|
|
11843
|
-
|
|
11844
|
-
|
|
11933
|
+
const probe = deps.probe ?? probeCommand;
|
|
11934
|
+
const probed = await probe(plan.command);
|
|
11935
|
+
switch (probed.status) {
|
|
11936
|
+
case "available":
|
|
11937
|
+
break;
|
|
11938
|
+
case "absent":
|
|
11939
|
+
return { plan, ran: false, ok: true, skippedReason: `${plan.command} not found on PATH` };
|
|
11940
|
+
case "inconclusive":
|
|
11941
|
+
return {
|
|
11942
|
+
plan,
|
|
11943
|
+
ran: false,
|
|
11944
|
+
ok: true,
|
|
11945
|
+
skippedReason: `could not determine whether ${plan.command} is on PATH (${probed.reason})`
|
|
11946
|
+
};
|
|
11947
|
+
default:
|
|
11948
|
+
return assertNeverCommandProbe(probed);
|
|
11845
11949
|
}
|
|
11846
11950
|
const result = await runCommand(plan.command, plan.args, { cwd: dir });
|
|
11847
11951
|
return {
|
|
@@ -12093,11 +12197,41 @@ async function runDryRun(args) {
|
|
|
12093
12197
|
}
|
|
12094
12198
|
|
|
12095
12199
|
// src/workspace/worktree/doctor.ts
|
|
12200
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
12201
|
+
import path66 from "node:path";
|
|
12202
|
+
|
|
12203
|
+
// src/workspace/worktree/orphans.ts
|
|
12096
12204
|
import { existsSync as existsSync5 } from "node:fs";
|
|
12097
12205
|
import path65 from "node:path";
|
|
12206
|
+
var WORKSPACE_REPO_ID = "(workspace)";
|
|
12207
|
+
async function findOrphanedRegistrations(repos, wsWorktreesDir) {
|
|
12208
|
+
const orphans = [];
|
|
12209
|
+
for (const repo of repos) {
|
|
12210
|
+
const entries = await listWorktrees(repo.absPath);
|
|
12211
|
+
for (const entry of entries) {
|
|
12212
|
+
const entryPath = path65.normalize(entry.path);
|
|
12213
|
+
const marker = `${path65.sep}.claude${path65.sep}worktrees${path65.sep}`;
|
|
12214
|
+
const idx = entryPath.indexOf(marker);
|
|
12215
|
+
if (idx === -1) continue;
|
|
12216
|
+
const rest = entryPath.slice(idx + marker.length);
|
|
12217
|
+
const slug = rest.split(path65.sep)[0];
|
|
12218
|
+
if (!slug) continue;
|
|
12219
|
+
if (existsSync5(path65.join(wsWorktreesDir, slug))) continue;
|
|
12220
|
+
orphans.push({
|
|
12221
|
+
repoId: repo.id,
|
|
12222
|
+
ownerRepo: repo.absPath,
|
|
12223
|
+
registeredPath: entry.path,
|
|
12224
|
+
slug
|
|
12225
|
+
});
|
|
12226
|
+
}
|
|
12227
|
+
}
|
|
12228
|
+
return orphans;
|
|
12229
|
+
}
|
|
12230
|
+
|
|
12231
|
+
// src/workspace/worktree/doctor.ts
|
|
12098
12232
|
var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
|
|
12099
12233
|
function looksHydrated(memberPath) {
|
|
12100
|
-
return HYDRATION_MARKERS.some((marker) =>
|
|
12234
|
+
return HYDRATION_MARKERS.some((marker) => existsSync6(path66.join(memberPath, marker)));
|
|
12101
12235
|
}
|
|
12102
12236
|
async function runDoctor2() {
|
|
12103
12237
|
const rootInfo = await resolveRoots();
|
|
@@ -12107,11 +12241,13 @@ async function runDoctor2() {
|
|
|
12107
12241
|
inWorkspaceWorktree: false,
|
|
12108
12242
|
members: [],
|
|
12109
12243
|
orphans: [],
|
|
12110
|
-
problems: ["Not inside a git repository."]
|
|
12244
|
+
problems: ["Not inside a git repository."],
|
|
12245
|
+
remedies: [],
|
|
12246
|
+
autoHydrate: false
|
|
12111
12247
|
};
|
|
12112
12248
|
}
|
|
12113
12249
|
const workspaceRoot = rootInfo.mainRoot;
|
|
12114
|
-
const isWorkspace =
|
|
12250
|
+
const isWorkspace = existsSync6(path66.join(workspaceRoot, WORKSPACE_FILE)) || existsSync6(path66.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
12115
12251
|
if (!isWorkspace) {
|
|
12116
12252
|
return {
|
|
12117
12253
|
isWorkspace: false,
|
|
@@ -12120,23 +12256,28 @@ async function runDoctor2() {
|
|
|
12120
12256
|
orphans: [],
|
|
12121
12257
|
problems: [
|
|
12122
12258
|
`Not a haus workspace \u2014 no ${WORKSPACE_FILE} or ${REPOS_MANIFEST_FILE} at ${workspaceRoot}.`
|
|
12123
|
-
]
|
|
12259
|
+
],
|
|
12260
|
+
remedies: [],
|
|
12261
|
+
autoHydrate: false
|
|
12124
12262
|
};
|
|
12125
12263
|
}
|
|
12126
12264
|
const wsWorktreesDir = worktreesDir(workspaceRoot);
|
|
12127
|
-
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${
|
|
12265
|
+
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path66.sep}`);
|
|
12128
12266
|
const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
|
|
12267
|
+
const workspaceConfig = await readWorkspaceConfig(workspaceRoot);
|
|
12268
|
+
const autoHydrate = workspaceConfig?.worktree?.autoHydrate === true;
|
|
12129
12269
|
const members = await readMembers(rootInfo);
|
|
12130
12270
|
const problems = [];
|
|
12271
|
+
const remedies = [];
|
|
12131
12272
|
const memberChecks = [];
|
|
12132
12273
|
let slug;
|
|
12133
12274
|
if (inWorkspaceWorktree) {
|
|
12134
12275
|
const wtPath = rootInfo.repoRoot;
|
|
12135
|
-
slug = rootInfo.worktreeName ??
|
|
12276
|
+
slug = rootInfo.worktreeName ?? path66.basename(wtPath);
|
|
12136
12277
|
const state = await readWorktreeState(wtPath);
|
|
12137
12278
|
for (const member of members) {
|
|
12138
|
-
const memberPath =
|
|
12139
|
-
const materialized =
|
|
12279
|
+
const memberPath = path66.join(wtPath, member.folder);
|
|
12280
|
+
const materialized = existsSync6(memberPath);
|
|
12140
12281
|
const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
|
|
12141
12282
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
12142
12283
|
const branchMismatch = Boolean(
|
|
@@ -12161,33 +12302,42 @@ async function runDoctor2() {
|
|
|
12161
12302
|
);
|
|
12162
12303
|
}
|
|
12163
12304
|
}
|
|
12164
|
-
|
|
12165
|
-
|
|
12166
|
-
|
|
12167
|
-
|
|
12168
|
-
for (const entry of entries) {
|
|
12169
|
-
const entryPath = path65.normalize(entry.path);
|
|
12170
|
-
const marker = `${path65.sep}.claude${path65.sep}worktrees${path65.sep}`;
|
|
12171
|
-
const idx = entryPath.indexOf(marker);
|
|
12172
|
-
if (idx === -1) continue;
|
|
12173
|
-
const rest = entryPath.slice(idx + marker.length);
|
|
12174
|
-
const slugName = rest.split(path65.sep)[0];
|
|
12175
|
-
if (!slugName) continue;
|
|
12176
|
-
if (!existsSync5(path65.join(wsWorktreesDir, slugName))) {
|
|
12177
|
-
orphans.push(
|
|
12178
|
-
`${member.id}: worktree registered at ${entry.path} but ${path65.join(wsWorktreesDir, slugName)} no longer exists \u2014 run \`git -C "${member.absPath}" worktree prune\`.`
|
|
12179
|
-
);
|
|
12180
|
-
}
|
|
12305
|
+
if (memberChecks.some((m) => !m.materialized)) {
|
|
12306
|
+
remedies.push(
|
|
12307
|
+
"Run `haus workspace worktree hydrate --materialize` to materialize the missing members."
|
|
12308
|
+
);
|
|
12181
12309
|
}
|
|
12182
12310
|
}
|
|
12183
|
-
|
|
12184
|
-
|
|
12311
|
+
const orphanScan = await findOrphanedRegistrations(
|
|
12312
|
+
[
|
|
12313
|
+
{ id: WORKSPACE_REPO_ID, absPath: workspaceRoot },
|
|
12314
|
+
...members.map((m) => ({ id: m.id, absPath: m.absPath }))
|
|
12315
|
+
],
|
|
12316
|
+
wsWorktreesDir
|
|
12317
|
+
);
|
|
12318
|
+
const orphans = orphanScan.map(
|
|
12319
|
+
(o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${path66.join(wsWorktreesDir, o.slug)} no longer exists.`
|
|
12320
|
+
);
|
|
12321
|
+
if (orphans.length > 0) {
|
|
12322
|
+
problems.push(...orphans);
|
|
12323
|
+
remedies.push("Run `haus workspace worktree prune` to clean up stale worktree registrations.");
|
|
12324
|
+
}
|
|
12325
|
+
return {
|
|
12326
|
+
isWorkspace: true,
|
|
12327
|
+
inWorkspaceWorktree,
|
|
12328
|
+
slug,
|
|
12329
|
+
members: memberChecks,
|
|
12330
|
+
orphans,
|
|
12331
|
+
problems,
|
|
12332
|
+
remedies,
|
|
12333
|
+
autoHydrate
|
|
12334
|
+
};
|
|
12185
12335
|
}
|
|
12186
12336
|
|
|
12187
12337
|
// src/workspace/worktree/hydrate-command.ts
|
|
12188
12338
|
init_fs();
|
|
12189
|
-
import { existsSync as
|
|
12190
|
-
import
|
|
12339
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
12340
|
+
import path67 from "node:path";
|
|
12191
12341
|
async function runHydrateWorktree(opts) {
|
|
12192
12342
|
const rootInfo = await resolveRoots();
|
|
12193
12343
|
if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
|
|
@@ -12198,7 +12348,7 @@ async function runHydrateWorktree(opts) {
|
|
|
12198
12348
|
}
|
|
12199
12349
|
const workspaceRoot = rootInfo.mainRoot;
|
|
12200
12350
|
const wtPath = rootInfo.repoRoot;
|
|
12201
|
-
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${
|
|
12351
|
+
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path67.sep}`)) {
|
|
12202
12352
|
return {
|
|
12203
12353
|
ok: false,
|
|
12204
12354
|
error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
|
|
@@ -12210,14 +12360,14 @@ async function runHydrateWorktree(opts) {
|
|
|
12210
12360
|
return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
|
|
12211
12361
|
}
|
|
12212
12362
|
const state = await readWorktreeState(wtPath);
|
|
12213
|
-
const slug = state?.slug ??
|
|
12363
|
+
const slug = state?.slug ?? path67.basename(wtPath);
|
|
12214
12364
|
const detectedBranch = await currentBranch(wtPath);
|
|
12215
12365
|
const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
|
|
12216
12366
|
if (opts.dryRun) {
|
|
12217
12367
|
const results2 = await Promise.all(
|
|
12218
12368
|
selected.map(async (member) => {
|
|
12219
|
-
const memberPath =
|
|
12220
|
-
if (
|
|
12369
|
+
const memberPath = path67.join(wtPath, member.folder);
|
|
12370
|
+
if (existsSync7(memberPath)) {
|
|
12221
12371
|
return { member: member.id, folder: member.folder, status: "planned" };
|
|
12222
12372
|
}
|
|
12223
12373
|
if (!opts.materialize) {
|
|
@@ -12225,7 +12375,7 @@ async function runHydrateWorktree(opts) {
|
|
|
12225
12375
|
member: member.id,
|
|
12226
12376
|
folder: member.folder,
|
|
12227
12377
|
status: "skipped",
|
|
12228
|
-
error: "not materialized in this worktree"
|
|
12378
|
+
error: "not materialized in this worktree \u2014 re-run with --materialize to create it"
|
|
12229
12379
|
};
|
|
12230
12380
|
}
|
|
12231
12381
|
const branchAction = await branchExists(member.absPath, targetBranch) ? "checkout" : "create";
|
|
@@ -12237,15 +12387,15 @@ async function runHydrateWorktree(opts) {
|
|
|
12237
12387
|
const results = await mapWithConcurrency(
|
|
12238
12388
|
selected,
|
|
12239
12389
|
async (member) => {
|
|
12240
|
-
const memberPath =
|
|
12390
|
+
const memberPath = path67.join(wtPath, member.folder);
|
|
12241
12391
|
let branchAction;
|
|
12242
|
-
if (!
|
|
12392
|
+
if (!existsSync7(memberPath)) {
|
|
12243
12393
|
if (!opts.materialize) {
|
|
12244
12394
|
return {
|
|
12245
12395
|
member: member.id,
|
|
12246
12396
|
folder: member.folder,
|
|
12247
12397
|
status: "skipped",
|
|
12248
|
-
error: "not materialized in this worktree"
|
|
12398
|
+
error: "not materialized in this worktree \u2014 re-run with --materialize to create it"
|
|
12249
12399
|
};
|
|
12250
12400
|
}
|
|
12251
12401
|
const outcome = await addWorktree(member.absPath, memberPath, targetBranch, {
|
|
@@ -12300,7 +12450,7 @@ async function runHydrateWorktree(opts) {
|
|
|
12300
12450
|
}
|
|
12301
12451
|
|
|
12302
12452
|
// src/workspace/worktree/list.ts
|
|
12303
|
-
import
|
|
12453
|
+
import path68 from "node:path";
|
|
12304
12454
|
import fs47 from "fs-extra";
|
|
12305
12455
|
async function runList2() {
|
|
12306
12456
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -12313,12 +12463,12 @@ async function runList2() {
|
|
|
12313
12463
|
const configuredMembers = await readMembers(rootInfo);
|
|
12314
12464
|
const worktrees = [];
|
|
12315
12465
|
for (const slug of slugs) {
|
|
12316
|
-
const wtPath =
|
|
12466
|
+
const wtPath = path68.join(dir, slug);
|
|
12317
12467
|
const state = await readWorktreeState(wtPath);
|
|
12318
12468
|
const members = [];
|
|
12319
12469
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
12320
12470
|
for (const m of source) {
|
|
12321
|
-
const memberPath =
|
|
12471
|
+
const memberPath = path68.join(wtPath, m.folder);
|
|
12322
12472
|
const materialized = await fs47.pathExists(memberPath);
|
|
12323
12473
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
12324
12474
|
members.push({
|
|
@@ -12339,8 +12489,48 @@ async function runList2() {
|
|
|
12339
12489
|
return { ok: true, workspaceRoot, worktrees };
|
|
12340
12490
|
}
|
|
12341
12491
|
|
|
12492
|
+
// src/workspace/worktree/prune.ts
|
|
12493
|
+
async function runPrune2(opts) {
|
|
12494
|
+
const resolved = await resolveWorkspaceForWorktree();
|
|
12495
|
+
if (!resolved.ok) return { ok: false, error: resolved.reason };
|
|
12496
|
+
const { rootInfo, workspaceRoot } = resolved;
|
|
12497
|
+
const members = await readMembers(rootInfo);
|
|
12498
|
+
const orphanScan = await findOrphanedRegistrations(
|
|
12499
|
+
[
|
|
12500
|
+
{ id: WORKSPACE_REPO_ID, absPath: workspaceRoot },
|
|
12501
|
+
...members.map((m) => ({ id: m.id, absPath: m.absPath }))
|
|
12502
|
+
],
|
|
12503
|
+
worktreesDir(workspaceRoot)
|
|
12504
|
+
);
|
|
12505
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
12506
|
+
for (const orphan of orphanScan) {
|
|
12507
|
+
const bucket = byRepo.get(orphan.repoId) ?? { ownerRepo: orphan.ownerRepo, orphans: [] };
|
|
12508
|
+
bucket.orphans.push(orphan);
|
|
12509
|
+
byRepo.set(orphan.repoId, bucket);
|
|
12510
|
+
}
|
|
12511
|
+
if (opts.dryRun) {
|
|
12512
|
+
const results2 = Array.from(byRepo, ([repo, { orphans }]) => ({
|
|
12513
|
+
repo,
|
|
12514
|
+
orphans: orphans.map((o) => o.registeredPath),
|
|
12515
|
+
pruned: false
|
|
12516
|
+
}));
|
|
12517
|
+
return { ok: true, dryRun: true, results: results2 };
|
|
12518
|
+
}
|
|
12519
|
+
const results = [];
|
|
12520
|
+
for (const [repo, { ownerRepo, orphans }] of byRepo) {
|
|
12521
|
+
const outcome = await pruneWorktrees(ownerRepo);
|
|
12522
|
+
results.push({
|
|
12523
|
+
repo,
|
|
12524
|
+
orphans: orphans.map((o) => o.registeredPath),
|
|
12525
|
+
pruned: outcome.ok,
|
|
12526
|
+
error: outcome.ok ? void 0 : outcome.stderr ?? "git worktree prune failed"
|
|
12527
|
+
});
|
|
12528
|
+
}
|
|
12529
|
+
return { ok: results.every((r) => r.pruned), dryRun: false, results };
|
|
12530
|
+
}
|
|
12531
|
+
|
|
12342
12532
|
// src/workspace/worktree/remove.ts
|
|
12343
|
-
import
|
|
12533
|
+
import path69 from "node:path";
|
|
12344
12534
|
import fs48 from "fs-extra";
|
|
12345
12535
|
async function runRemove2(opts) {
|
|
12346
12536
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -12367,7 +12557,7 @@ async function runRemove2(opts) {
|
|
|
12367
12557
|
targets.push({
|
|
12368
12558
|
repo: configMember?.id ?? stateMember.id,
|
|
12369
12559
|
ownerRepo,
|
|
12370
|
-
path:
|
|
12560
|
+
path: path69.join(wsWorktreePath, stateMember.folder)
|
|
12371
12561
|
});
|
|
12372
12562
|
}
|
|
12373
12563
|
const existingTargets = [];
|
|
@@ -12580,6 +12770,32 @@ async function handleRemove(opts) {
|
|
|
12580
12770
|
}
|
|
12581
12771
|
}
|
|
12582
12772
|
}
|
|
12773
|
+
async function handlePrune(opts) {
|
|
12774
|
+
const result = await runPrune2({
|
|
12775
|
+
dryRun: resolveWriteMode(opts, "mutate-by-default").kind === "preview"
|
|
12776
|
+
});
|
|
12777
|
+
if (!result.ok && "error" in result) {
|
|
12778
|
+
if (opts.json) emitJson(result);
|
|
12779
|
+
else error(result.error);
|
|
12780
|
+
process.exitCode = 1;
|
|
12781
|
+
return;
|
|
12782
|
+
}
|
|
12783
|
+
if (opts.json) {
|
|
12784
|
+
emitJson(result);
|
|
12785
|
+
if (!result.dryRun && !result.ok) process.exitCode = 1;
|
|
12786
|
+
return;
|
|
12787
|
+
}
|
|
12788
|
+
if (result.results.length === 0) {
|
|
12789
|
+
log("No stale worktree registrations found.");
|
|
12790
|
+
return;
|
|
12791
|
+
}
|
|
12792
|
+
for (const r of result.results) {
|
|
12793
|
+
const prefix = result.dryRun ? "[dry-run] Would prune" : r.pruned ? "Pruned" : "FAILED to prune";
|
|
12794
|
+
const detail = r.error ? ` \u2014 ${r.error}` : "";
|
|
12795
|
+
log(`${prefix} ${r.repo}: ${r.orphans.join(", ")}${detail}`);
|
|
12796
|
+
}
|
|
12797
|
+
if (!result.dryRun && !result.ok) process.exitCode = 1;
|
|
12798
|
+
}
|
|
12583
12799
|
async function handleList(opts) {
|
|
12584
12800
|
const result = await runList2();
|
|
12585
12801
|
if (opts.json) {
|
|
@@ -12631,12 +12847,17 @@ async function handleDoctor(opts) {
|
|
|
12631
12847
|
}
|
|
12632
12848
|
if (opts.fromHook) {
|
|
12633
12849
|
const scope = report.slug ? `Workspace worktree "${report.slug}"` : "This haus workspace";
|
|
12850
|
+
const needsMaterialize = report.members.some((m) => !m.materialized || !m.hydrated);
|
|
12851
|
+
const directive = report.autoHydrate && needsMaterialize ? "autoHydrate is enabled for this workspace: run `haus workspace worktree hydrate --materialize` now, before any other work, to materialize and hydrate the missing members. " : "";
|
|
12852
|
+
const remedies = directive ? report.remedies.filter((r) => !r.includes("--materialize")) : report.remedies;
|
|
12853
|
+
const remedySuffix = remedies.length > 0 ? ` ${remedies.join(" ")}` : "";
|
|
12634
12854
|
emitFromHookContext(
|
|
12635
|
-
`${scope} has ${report.problems.length} issue(s): ${report.problems.join("; ")}`
|
|
12855
|
+
`${directive}${scope} has ${report.problems.length} issue(s): ${report.problems.join("; ")}${remedySuffix}`
|
|
12636
12856
|
);
|
|
12637
12857
|
return;
|
|
12638
12858
|
}
|
|
12639
12859
|
for (const p of report.problems) warn(p);
|
|
12860
|
+
for (const r of report.remedies) log(r);
|
|
12640
12861
|
process.exitCode = 1;
|
|
12641
12862
|
} catch (err) {
|
|
12642
12863
|
const message2 = err instanceof Error ? err.message : String(err);
|
|
@@ -12660,12 +12881,14 @@ async function runWorktree(action, opts = {}) {
|
|
|
12660
12881
|
return handleList(opts);
|
|
12661
12882
|
case "doctor":
|
|
12662
12883
|
return handleDoctor(opts);
|
|
12884
|
+
case "prune":
|
|
12885
|
+
return handlePrune(opts);
|
|
12663
12886
|
}
|
|
12664
12887
|
}
|
|
12665
12888
|
|
|
12666
12889
|
// src/commands/workspace.ts
|
|
12667
|
-
import { existsSync as
|
|
12668
|
-
import
|
|
12890
|
+
import { existsSync as existsSync11, statSync as statSync3 } from "node:fs";
|
|
12891
|
+
import path78 from "node:path";
|
|
12669
12892
|
init_fs();
|
|
12670
12893
|
init_logger();
|
|
12671
12894
|
|
|
@@ -12723,8 +12946,8 @@ ${summaries.map(
|
|
|
12723
12946
|
}
|
|
12724
12947
|
|
|
12725
12948
|
// src/commands/workspace/doctor.ts
|
|
12726
|
-
import { existsSync as
|
|
12727
|
-
import
|
|
12949
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
12950
|
+
import path71 from "node:path";
|
|
12728
12951
|
init_logger();
|
|
12729
12952
|
init_paths();
|
|
12730
12953
|
|
|
@@ -12732,14 +12955,14 @@ init_paths();
|
|
|
12732
12955
|
init_fs();
|
|
12733
12956
|
init_paths();
|
|
12734
12957
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
12735
|
-
import
|
|
12958
|
+
import path70 from "node:path";
|
|
12736
12959
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
12737
12960
|
function manifestPath(workspaceRoot) {
|
|
12738
12961
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
12739
12962
|
}
|
|
12740
12963
|
function hausVersion() {
|
|
12741
12964
|
try {
|
|
12742
|
-
const pkg = JSON.parse(readFileSync4(
|
|
12965
|
+
const pkg = JSON.parse(readFileSync4(path70.join(packageRoot(), "package.json"), "utf8"));
|
|
12743
12966
|
return pkg.version ?? "0.0.0";
|
|
12744
12967
|
} catch {
|
|
12745
12968
|
return "0.0.0";
|
|
@@ -12831,7 +13054,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
12831
13054
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
12832
13055
|
const catalogRefByRepo = [];
|
|
12833
13056
|
for (const repo of config.repos) {
|
|
12834
|
-
const repoRoot =
|
|
13057
|
+
const repoRoot = path71.resolve(workspaceRoot, repo.path);
|
|
12835
13058
|
const entry = manifestByName.get(repo.name);
|
|
12836
13059
|
if (!entry) {
|
|
12837
13060
|
flag({
|
|
@@ -12856,7 +13079,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
12856
13079
|
detail: `Set up at haus ${entry.hausVersionAtSetup}, current is ${currentVersion} \u2014 re-run setup.`
|
|
12857
13080
|
});
|
|
12858
13081
|
}
|
|
12859
|
-
if (!
|
|
13082
|
+
if (!existsSync8(claudePath(repoRoot))) {
|
|
12860
13083
|
flag({
|
|
12861
13084
|
repo: repo.name,
|
|
12862
13085
|
kind: "missing-claude",
|
|
@@ -12865,7 +13088,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
12865
13088
|
}
|
|
12866
13089
|
const lock = await checkLock(repoRoot);
|
|
12867
13090
|
if (lock.catalogRef) catalogRefByRepo.push({ repo: repo.name, ref: lock.catalogRef });
|
|
12868
|
-
if (!
|
|
13091
|
+
if (!existsSync8(hausPath(repoRoot, "haus.lock.json"))) {
|
|
12869
13092
|
flag({
|
|
12870
13093
|
repo: repo.name,
|
|
12871
13094
|
kind: "missing-lock",
|
|
@@ -12896,7 +13119,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
12896
13119
|
const memberById = new Map(members.map((m) => [m.id, m]));
|
|
12897
13120
|
for (const entry of manifest.linkedContext) {
|
|
12898
13121
|
const member = memberById.get(entry.repo);
|
|
12899
|
-
if (!member || !
|
|
13122
|
+
if (!member || !existsSync8(member.absPath)) {
|
|
12900
13123
|
flag({
|
|
12901
13124
|
repo: entry.repo,
|
|
12902
13125
|
kind: "missing-linked-context-source",
|
|
@@ -12904,7 +13127,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
12904
13127
|
});
|
|
12905
13128
|
continue;
|
|
12906
13129
|
}
|
|
12907
|
-
if (!
|
|
13130
|
+
if (!existsSync8(path71.join(workspaceRoot, entry.path))) {
|
|
12908
13131
|
flag({
|
|
12909
13132
|
repo: entry.repo,
|
|
12910
13133
|
kind: "missing-linked-context-copy",
|
|
@@ -12950,17 +13173,17 @@ function emit(args) {
|
|
|
12950
13173
|
init_logger();
|
|
12951
13174
|
|
|
12952
13175
|
// src/workspace/link-context/link.ts
|
|
12953
|
-
import
|
|
13176
|
+
import path74 from "node:path";
|
|
12954
13177
|
import fs51 from "fs-extra";
|
|
12955
13178
|
init_fs();
|
|
12956
13179
|
init_logger();
|
|
12957
13180
|
|
|
12958
13181
|
// src/workspace/link-context/plan.ts
|
|
12959
|
-
import
|
|
13182
|
+
import path73 from "node:path";
|
|
12960
13183
|
import fs50 from "fs-extra";
|
|
12961
13184
|
|
|
12962
13185
|
// src/workspace/link-context/scan-source.ts
|
|
12963
|
-
import
|
|
13186
|
+
import path72 from "node:path";
|
|
12964
13187
|
import fs49 from "fs-extra";
|
|
12965
13188
|
var ASSET_SUBDIR = {
|
|
12966
13189
|
skill: "skills",
|
|
@@ -12969,36 +13192,36 @@ var ASSET_SUBDIR = {
|
|
|
12969
13192
|
};
|
|
12970
13193
|
async function hasLinkableContext(member) {
|
|
12971
13194
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
12972
|
-
if (await fs49.pathExists(
|
|
13195
|
+
if (await fs49.pathExists(path72.join(member.absPath, ".claude", subdir))) return true;
|
|
12973
13196
|
}
|
|
12974
13197
|
return false;
|
|
12975
13198
|
}
|
|
12976
13199
|
async function collectSkills(member) {
|
|
12977
|
-
const dir =
|
|
13200
|
+
const dir = path72.join(member.absPath, ".claude", "skills");
|
|
12978
13201
|
if (!await fs49.pathExists(dir)) return [];
|
|
12979
13202
|
const names = await fs49.readdir(dir);
|
|
12980
13203
|
const out = [];
|
|
12981
13204
|
for (const name of [...names].sort()) {
|
|
12982
|
-
const abs =
|
|
13205
|
+
const abs = path72.join(dir, name);
|
|
12983
13206
|
const stat = await fs49.lstat(abs);
|
|
12984
13207
|
if (!stat.isDirectory()) continue;
|
|
12985
|
-
if (!await fs49.pathExists(
|
|
12986
|
-
out.push({ type: "skill", name, sourceRelPath:
|
|
13208
|
+
if (!await fs49.pathExists(path72.join(abs, "SKILL.md"))) continue;
|
|
13209
|
+
out.push({ type: "skill", name, sourceRelPath: path72.posix.join(".claude", "skills", name) });
|
|
12987
13210
|
}
|
|
12988
13211
|
return out;
|
|
12989
13212
|
}
|
|
12990
13213
|
async function collectFlatMarkdown(member, type) {
|
|
12991
13214
|
const subdir = ASSET_SUBDIR[type];
|
|
12992
|
-
const dir =
|
|
13215
|
+
const dir = path72.join(member.absPath, ".claude", subdir);
|
|
12993
13216
|
if (!await fs49.pathExists(dir)) return [];
|
|
12994
13217
|
const names = await fs49.readdir(dir);
|
|
12995
13218
|
const out = [];
|
|
12996
13219
|
for (const fileName of [...names].sort()) {
|
|
12997
13220
|
if (!fileName.endsWith(".md")) continue;
|
|
12998
|
-
const abs =
|
|
13221
|
+
const abs = path72.join(dir, fileName);
|
|
12999
13222
|
if (!(await fs49.lstat(abs)).isFile()) continue;
|
|
13000
13223
|
const name = fileName.slice(0, -".md".length);
|
|
13001
|
-
out.push({ type, name, sourceRelPath:
|
|
13224
|
+
out.push({ type, name, sourceRelPath: path72.posix.join(".claude", subdir, fileName) });
|
|
13002
13225
|
}
|
|
13003
13226
|
return out;
|
|
13004
13227
|
}
|
|
@@ -13013,9 +13236,9 @@ async function collectSourceAssets(member) {
|
|
|
13013
13236
|
|
|
13014
13237
|
// src/workspace/link-context/plan.ts
|
|
13015
13238
|
function repoFolderPrefix(member) {
|
|
13016
|
-
const base =
|
|
13239
|
+
const base = path73.posix.basename(member.folder);
|
|
13017
13240
|
if (base && base !== "." && base !== "/") return base;
|
|
13018
|
-
const fromAbs =
|
|
13241
|
+
const fromAbs = path73.basename(member.absPath);
|
|
13019
13242
|
return fromAbs || member.id;
|
|
13020
13243
|
}
|
|
13021
13244
|
function assetKey(type, destKey) {
|
|
@@ -13072,7 +13295,7 @@ async function buildLinkPlan(members) {
|
|
|
13072
13295
|
name: asset.name,
|
|
13073
13296
|
destKey,
|
|
13074
13297
|
sourceRelPath: asset.sourceRelPath,
|
|
13075
|
-
sourceAbsPath:
|
|
13298
|
+
sourceAbsPath: path73.join(member.absPath, asset.sourceRelPath),
|
|
13076
13299
|
sourceHash
|
|
13077
13300
|
});
|
|
13078
13301
|
}
|
|
@@ -13083,7 +13306,7 @@ async function buildLinkPlan(members) {
|
|
|
13083
13306
|
function destRelPathFor(type, destKey) {
|
|
13084
13307
|
const subdir = `${type}s`;
|
|
13085
13308
|
const leaf = type === "skill" ? destKey : `${destKey}.md`;
|
|
13086
|
-
return
|
|
13309
|
+
return path74.posix.join(".claude", subdir, leaf);
|
|
13087
13310
|
}
|
|
13088
13311
|
async function runLinkContext(workspaceRoot, opts = {}) {
|
|
13089
13312
|
const write = resolveWriteMode(opts, "preview-by-default").kind === "write";
|
|
@@ -13109,9 +13332,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
13109
13332
|
const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13110
13333
|
for (const entry of plan.entries) {
|
|
13111
13334
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
13112
|
-
const destAbs =
|
|
13335
|
+
const destAbs = path74.join(workspaceRoot, destRel);
|
|
13113
13336
|
if (write) {
|
|
13114
|
-
await fs51.ensureDir(
|
|
13337
|
+
await fs51.ensureDir(path74.dirname(destAbs));
|
|
13115
13338
|
if (await fs51.pathExists(destAbs)) await fs51.remove(destAbs);
|
|
13116
13339
|
await fs51.copy(entry.sourceAbsPath, destAbs, {
|
|
13117
13340
|
overwrite: true,
|
|
@@ -13199,11 +13422,11 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
|
|
|
13199
13422
|
}
|
|
13200
13423
|
|
|
13201
13424
|
// src/commands/workspace/setup.ts
|
|
13202
|
-
import { existsSync as
|
|
13203
|
-
import
|
|
13425
|
+
import { existsSync as existsSync9, statSync } from "node:fs";
|
|
13426
|
+
import path76 from "node:path";
|
|
13204
13427
|
|
|
13205
13428
|
// src/claude/write-workspace-claude-md.ts
|
|
13206
|
-
import
|
|
13429
|
+
import path75 from "node:path";
|
|
13207
13430
|
import fs52 from "fs-extra";
|
|
13208
13431
|
init_fs();
|
|
13209
13432
|
init_logger();
|
|
@@ -13226,7 +13449,7 @@ ${WORKSPACE_BLOCK_END}`;
|
|
|
13226
13449
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
13227
13450
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
13228
13451
|
const dryRun = opts.dryRun ?? false;
|
|
13229
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
13452
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path75.join(workspaceRoot, "CLAUDE.md");
|
|
13230
13453
|
const say = opts.quiet ? () => {
|
|
13231
13454
|
} : log;
|
|
13232
13455
|
const existed = await fs52.pathExists(filePath);
|
|
@@ -13259,20 +13482,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
13259
13482
|
init_fs();
|
|
13260
13483
|
init_logger();
|
|
13261
13484
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
13262
|
-
let dir =
|
|
13485
|
+
let dir = path76.resolve(start);
|
|
13263
13486
|
for (; ; ) {
|
|
13264
|
-
if (
|
|
13265
|
-
const parent =
|
|
13266
|
-
if (parent === dir) return
|
|
13487
|
+
if (existsSync9(path76.join(dir, WORKSPACE_FILE))) return dir;
|
|
13488
|
+
const parent = path76.dirname(dir);
|
|
13489
|
+
if (parent === dir) return path76.resolve(start);
|
|
13267
13490
|
dir = parent;
|
|
13268
13491
|
}
|
|
13269
13492
|
}
|
|
13270
13493
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
13271
|
-
return
|
|
13494
|
+
return path76.resolve(workspaceRoot, repoPath) === path76.resolve(workspaceRoot);
|
|
13272
13495
|
}
|
|
13273
13496
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
13274
13497
|
const apply = options.write ?? false;
|
|
13275
|
-
const configText = await readText(
|
|
13498
|
+
const configText = await readText(path76.join(workspaceRoot, WORKSPACE_FILE));
|
|
13276
13499
|
if (!configText) {
|
|
13277
13500
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
13278
13501
|
process.exitCode = 1;
|
|
@@ -13299,11 +13522,11 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13299
13522
|
const statuses = [];
|
|
13300
13523
|
const aggregateInputs = [];
|
|
13301
13524
|
for (const repo of repos) {
|
|
13302
|
-
const repoRoot =
|
|
13525
|
+
const repoRoot = path76.resolve(workspaceRoot, repo.path);
|
|
13303
13526
|
say(`
|
|
13304
13527
|
\u2192 ${repo.name} (${repo.path})`);
|
|
13305
13528
|
try {
|
|
13306
|
-
if (!
|
|
13529
|
+
if (!existsSync9(repoRoot) || !statSync(repoRoot).isDirectory()) {
|
|
13307
13530
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
13308
13531
|
}
|
|
13309
13532
|
const res = await runSetupCore(repoRoot, {
|
|
@@ -13378,7 +13601,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13378
13601
|
if (apply && !options.dryRun) {
|
|
13379
13602
|
const statusByName = new Map(statuses.map((s) => [s.name, s]));
|
|
13380
13603
|
const prior = await readManifest2(workspaceRoot);
|
|
13381
|
-
if (!prior &&
|
|
13604
|
+
if (!prior && existsSync9(manifestPath(workspaceRoot))) {
|
|
13382
13605
|
warn(
|
|
13383
13606
|
"Existing workspace.manifest.json is unreadable \u2014 prior per-repo state will not be carried forward."
|
|
13384
13607
|
);
|
|
@@ -13389,7 +13612,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13389
13612
|
const status = statusByName.get(repo.name);
|
|
13390
13613
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
13391
13614
|
if (status?.status === "ok") {
|
|
13392
|
-
const lock = await checkLock(
|
|
13615
|
+
const lock = await checkLock(path76.resolve(workspaceRoot, repo.path));
|
|
13393
13616
|
manifestRepos.push({
|
|
13394
13617
|
name: repo.name,
|
|
13395
13618
|
path: repo.path,
|
|
@@ -13445,7 +13668,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13445
13668
|
try {
|
|
13446
13669
|
const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
|
|
13447
13670
|
if (linkResult.ok) {
|
|
13448
|
-
written.push(...linkResult.added.map((rel) =>
|
|
13671
|
+
written.push(...linkResult.added.map((rel) => path76.join(workspaceRoot, rel)));
|
|
13449
13672
|
if (!options.json) {
|
|
13450
13673
|
say(
|
|
13451
13674
|
`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` : ""}.`
|
|
@@ -13476,8 +13699,8 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
13476
13699
|
}
|
|
13477
13700
|
|
|
13478
13701
|
// src/commands/workspace/undo.ts
|
|
13479
|
-
import { existsSync as
|
|
13480
|
-
import
|
|
13702
|
+
import { existsSync as existsSync10, statSync as statSync2 } from "node:fs";
|
|
13703
|
+
import path77 from "node:path";
|
|
13481
13704
|
import fs53 from "fs-extra";
|
|
13482
13705
|
init_fs();
|
|
13483
13706
|
init_logger();
|
|
@@ -13508,11 +13731,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13508
13731
|
}
|
|
13509
13732
|
if (options.dryRun) {
|
|
13510
13733
|
for (const repo of config.repos) {
|
|
13511
|
-
const repoRoot =
|
|
13734
|
+
const repoRoot = path77.resolve(workspaceRoot, repo.path);
|
|
13512
13735
|
log(`
|
|
13513
13736
|
\u2192 ${repo.name} (${repo.path})`);
|
|
13514
13737
|
try {
|
|
13515
|
-
if (!
|
|
13738
|
+
if (!existsSync10(repoRoot) || !statSync2(repoRoot).isDirectory()) {
|
|
13516
13739
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
13517
13740
|
}
|
|
13518
13741
|
await runUndo({ dryRun: true, root: repoRoot });
|
|
@@ -13540,11 +13763,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13540
13763
|
];
|
|
13541
13764
|
for (const abs of workspaceArtifacts) {
|
|
13542
13765
|
if (await fs53.pathExists(abs)) {
|
|
13543
|
-
log(`[dry-run] Would remove ${
|
|
13766
|
+
log(`[dry-run] Would remove ${path77.relative(workspaceRoot, abs)}`);
|
|
13544
13767
|
}
|
|
13545
13768
|
}
|
|
13546
13769
|
if (!await fs53.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
|
|
13547
|
-
const claudeMd = await readText(
|
|
13770
|
+
const claudeMd = await readText(path77.join(workspaceRoot, "CLAUDE.md"));
|
|
13548
13771
|
if (claudeMd?.includes(WORKSPACE_BLOCK_BEGIN) || claudeMd && hasLegacyWorkspaceBlock(claudeMd)) {
|
|
13549
13772
|
log("[dry-run] Would remove the workspace haus import block from CLAUDE.md");
|
|
13550
13773
|
}
|
|
@@ -13563,11 +13786,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13563
13786
|
}
|
|
13564
13787
|
let anyRepoFailed = false;
|
|
13565
13788
|
for (const repo of config.repos) {
|
|
13566
|
-
const repoRoot =
|
|
13789
|
+
const repoRoot = path77.resolve(workspaceRoot, repo.path);
|
|
13567
13790
|
log(`
|
|
13568
13791
|
\u2192 ${repo.name} (${repo.path})`);
|
|
13569
13792
|
try {
|
|
13570
|
-
if (!
|
|
13793
|
+
if (!existsSync10(repoRoot) || !statSync2(repoRoot).isDirectory()) {
|
|
13571
13794
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
13572
13795
|
}
|
|
13573
13796
|
await runUndo({ yes: true, root: repoRoot });
|
|
@@ -13592,21 +13815,21 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13592
13815
|
const abs = hausPath(workspaceRoot, rel);
|
|
13593
13816
|
if (await fs53.pathExists(abs)) {
|
|
13594
13817
|
await fs53.remove(abs);
|
|
13595
|
-
log(`Removed ${
|
|
13818
|
+
log(`Removed ${path77.relative(workspaceRoot, abs)}`);
|
|
13596
13819
|
}
|
|
13597
13820
|
}
|
|
13598
13821
|
const manifestFile = manifestPath(workspaceRoot);
|
|
13599
13822
|
if (await fs53.pathExists(manifestFile)) {
|
|
13600
13823
|
await fs53.remove(manifestFile);
|
|
13601
|
-
log(`Removed ${
|
|
13824
|
+
log(`Removed ${path77.relative(workspaceRoot, manifestFile)}`);
|
|
13602
13825
|
}
|
|
13603
13826
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
13604
13827
|
if (await fs53.pathExists(workspaceMdPath)) {
|
|
13605
13828
|
await fs53.remove(workspaceMdPath);
|
|
13606
|
-
log(`Removed ${
|
|
13829
|
+
log(`Removed ${path77.relative(workspaceRoot, workspaceMdPath)}`);
|
|
13607
13830
|
} else {
|
|
13608
|
-
const claudeMdPath =
|
|
13609
|
-
if (
|
|
13831
|
+
const claudeMdPath = path77.join(workspaceRoot, "CLAUDE.md");
|
|
13832
|
+
if (existsSync10(claudeMdPath)) {
|
|
13610
13833
|
const prev = await readText(claudeMdPath);
|
|
13611
13834
|
if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
|
|
13612
13835
|
const next = stripHausBlock(prev, {
|
|
@@ -13652,7 +13875,7 @@ function normalizeMaxDepth(maxDepth) {
|
|
|
13652
13875
|
return void 0;
|
|
13653
13876
|
}
|
|
13654
13877
|
async function initWorkspace(options = {}) {
|
|
13655
|
-
if (
|
|
13878
|
+
if (existsSync11(WORKSPACE_FILE) && !options.yes) {
|
|
13656
13879
|
const ok = await confirm(
|
|
13657
13880
|
`${WORKSPACE_FILE} already exists \u2014 overwrite with a fresh placeholder? Existing client/repos/relationships will be lost. Run \`haus workspace discover\` instead to merge in newly found repos.`
|
|
13658
13881
|
);
|
|
@@ -13674,7 +13897,7 @@ relationships: []
|
|
|
13674
13897
|
log("Workspace initialized.");
|
|
13675
13898
|
}
|
|
13676
13899
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
13677
|
-
const configText = await readText(
|
|
13900
|
+
const configText = await readText(path78.join(workspaceRoot, WORKSPACE_FILE));
|
|
13678
13901
|
if (!configText) {
|
|
13679
13902
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
13680
13903
|
process.exitCode = 1;
|
|
@@ -13696,9 +13919,9 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
13696
13919
|
const inputs = [];
|
|
13697
13920
|
const failures = [];
|
|
13698
13921
|
for (const repo of config.repos) {
|
|
13699
|
-
const repoRoot =
|
|
13922
|
+
const repoRoot = path78.resolve(workspaceRoot, repo.path);
|
|
13700
13923
|
try {
|
|
13701
|
-
if (!
|
|
13924
|
+
if (!existsSync11(repoRoot) || !statSync3(repoRoot).isDirectory()) {
|
|
13702
13925
|
throw new Error("path is not a directory");
|
|
13703
13926
|
}
|
|
13704
13927
|
const result = await scanProject(repoRoot);
|
|
@@ -13764,7 +13987,7 @@ init_logger();
|
|
|
13764
13987
|
init_paths();
|
|
13765
13988
|
function cliVersion() {
|
|
13766
13989
|
try {
|
|
13767
|
-
const pkgPath =
|
|
13990
|
+
const pkgPath = path79.join(packageRoot(), "package.json");
|
|
13768
13991
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
13769
13992
|
return pkg.version ?? "0.0.0";
|
|
13770
13993
|
} catch {
|
|
@@ -13774,7 +13997,7 @@ function cliVersion() {
|
|
|
13774
13997
|
var program = new Command();
|
|
13775
13998
|
function validateRuntimeNodeVersion() {
|
|
13776
13999
|
try {
|
|
13777
|
-
const pkgPath =
|
|
14000
|
+
const pkgPath = path79.join(packageRoot(), "package.json");
|
|
13778
14001
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
13779
14002
|
const requiredRange = pkg.engines?.node;
|
|
13780
14003
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -13911,6 +14134,11 @@ worktree.command("list").description("List materialized workspace worktrees and
|
|
|
13911
14134
|
worktree.command("remove <slug>").description("Remove a workspace worktree and its member worktrees").option("--force", "Bypass the uncommitted/unpushed-work refusal").option("--dry-run", "Preview without removing anything").option("--json", "Emit the structured remove result as JSON").action(
|
|
13912
14135
|
(slug, opts) => runWorktree("remove", { slug, force: opts.force, dryRun: opts.dryRun, json: opts.json })
|
|
13913
14136
|
);
|
|
14137
|
+
worktree.command("prune").description(
|
|
14138
|
+
"Remove stale worktree registrations left by a workspace worktree deleted without `haus workspace worktree remove` (e.g. a natively deleted Claude Code worktree)"
|
|
14139
|
+
).option("--dry-run", "Preview which registrations would be pruned").option("--json", "Emit the structured prune result as JSON").action(
|
|
14140
|
+
(opts) => runWorktree("prune", { dryRun: opts.dryRun, json: opts.json })
|
|
14141
|
+
);
|
|
13914
14142
|
worktree.command("doctor").description("Fast, side-effect-free health check for the current workspace worktree").option("--from-hook", "Emit for the SessionStart hook \u2014 always exits 0").action((opts) => runWorktree("doctor", { fromHook: opts.fromHook }));
|
|
13915
14143
|
program.parseAsync(process.argv).catch((err) => {
|
|
13916
14144
|
const message2 = err instanceof Error ? err.message : String(err);
|