@haus-tech/haus-workflow 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/cli.js +515 -189
- package/library/catalog/manifest.json +90 -50
- package/library/global/skills/haus-workflow/references/init.md +5 -5
- 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) {
|
|
@@ -9948,6 +10014,9 @@ async function hasMultipleSiblingRepos(root) {
|
|
|
9948
10014
|
init_fs();
|
|
9949
10015
|
init_logger();
|
|
9950
10016
|
init_paths();
|
|
10017
|
+
function emitJsonEnvelope(json, quiet, envelope) {
|
|
10018
|
+
if (json && !quiet) log(JSON.stringify(envelope, null, 2));
|
|
10019
|
+
}
|
|
9951
10020
|
async function readPriorManualIncludes(root) {
|
|
9952
10021
|
const prior = await readJson(hausPath(root, "recommendation.json"));
|
|
9953
10022
|
if (!prior?.recommended) return [];
|
|
@@ -9955,16 +10024,12 @@ async function readPriorManualIncludes(root) {
|
|
|
9955
10024
|
}
|
|
9956
10025
|
async function runSetupCore(root, opts) {
|
|
9957
10026
|
const { json, apply, dryRun, quiet, confirm: confirm2, force } = opts;
|
|
9958
|
-
const say = quiet ? () => {
|
|
10027
|
+
const say = quiet || json ? () => {
|
|
9959
10028
|
} : log;
|
|
9960
10029
|
const scanResult = await scanProject(root);
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
say("Haus scan complete");
|
|
9965
|
-
say(`Roles: ${scanResult.repoRoles.join(", ") || "unknown"}`);
|
|
9966
|
-
say(`Package manager: ${scanResult.packageManager}`);
|
|
9967
|
-
}
|
|
10030
|
+
say("Haus scan complete");
|
|
10031
|
+
say(`Roles: ${scanResult.repoRoles.join(", ") || "unknown"}`);
|
|
10032
|
+
say(`Package manager: ${scanResult.packageManager}`);
|
|
9968
10033
|
const siblingWarning = await hasMultipleSiblingRepos(root) ? [SIBLING_REPO_WARNING] : [];
|
|
9969
10034
|
const context = await readContextOrScan(root);
|
|
9970
10035
|
const priorManualIncludes = await readPriorManualIncludes(root);
|
|
@@ -9977,39 +10042,39 @@ async function runSetupCore(root, opts) {
|
|
|
9977
10042
|
const warningLines2 = [
|
|
9978
10043
|
.../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? [], ...siblingWarning])
|
|
9979
10044
|
];
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
say(`Skipped: ${recommendation.skipped.length}`);
|
|
9986
|
-
say(`Repo: ${context.repoName}`);
|
|
9987
|
-
for (const warning of warningLines2) say(`- WARN: ${warning}`);
|
|
9988
|
-
}
|
|
10045
|
+
say("Haus recommendation ready");
|
|
10046
|
+
say("Recommended: 0");
|
|
10047
|
+
say(`Skipped: ${recommendation.skipped.length}`);
|
|
10048
|
+
say(`Repo: ${context.repoName}`);
|
|
10049
|
+
for (const warning of warningLines2) say(`- WARN: ${warning}`);
|
|
9989
10050
|
const supersededCount = recommendation.skipped.filter(
|
|
9990
10051
|
(s) => s.skipReasons?.some((r) => r.code === "superseded-by-global-plugin")
|
|
9991
10052
|
).length;
|
|
9992
10053
|
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.";
|
|
9993
10054
|
warn(message2);
|
|
9994
10055
|
process.exitCode = 1;
|
|
10056
|
+
const warnings = [...warningLines2, message2];
|
|
10057
|
+
emitJsonEnvelope(json, quiet, {
|
|
10058
|
+
scan: scanResult,
|
|
10059
|
+
recommendation,
|
|
10060
|
+
hooks: null,
|
|
10061
|
+
repoName: context.repoName,
|
|
10062
|
+
warnings
|
|
10063
|
+
});
|
|
9995
10064
|
return {
|
|
9996
10065
|
root,
|
|
9997
10066
|
repoName: context.repoName,
|
|
9998
10067
|
roles: scanResult.repoRoles,
|
|
9999
10068
|
recommendedCount: 0,
|
|
10000
|
-
warnings
|
|
10069
|
+
warnings,
|
|
10001
10070
|
hooksOk: false,
|
|
10002
10071
|
written: []
|
|
10003
10072
|
};
|
|
10004
10073
|
}
|
|
10005
10074
|
await writeJson(hausPath(root, "recommendation.json"), recommendation);
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
say("Haus recommendation ready");
|
|
10010
|
-
say(`Recommended: ${recommendation.recommended.length}`);
|
|
10011
|
-
say(`Skipped: ${recommendation.skipped.length}`);
|
|
10012
|
-
}
|
|
10075
|
+
say("Haus recommendation ready");
|
|
10076
|
+
say(`Recommended: ${recommendation.recommended.length}`);
|
|
10077
|
+
say(`Skipped: ${recommendation.skipped.length}`);
|
|
10013
10078
|
const hooks = await verifyProjectSettingsHooksContract(root);
|
|
10014
10079
|
const warningLines = [
|
|
10015
10080
|
.../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? [], ...siblingWarning])
|
|
@@ -10034,12 +10099,23 @@ async function runSetupCore(root, opts) {
|
|
|
10034
10099
|
hooksOk,
|
|
10035
10100
|
written: []
|
|
10036
10101
|
};
|
|
10037
|
-
|
|
10102
|
+
const envelope = (hooksResult) => ({
|
|
10103
|
+
scan: scanResult,
|
|
10104
|
+
recommendation,
|
|
10105
|
+
hooks: hooksResult,
|
|
10106
|
+
repoName: context.repoName,
|
|
10107
|
+
warnings: warningLines
|
|
10108
|
+
});
|
|
10109
|
+
if (!apply) {
|
|
10110
|
+
emitJsonEnvelope(json, quiet, envelope(hooks));
|
|
10111
|
+
return baseResult;
|
|
10112
|
+
}
|
|
10038
10113
|
if (confirm2) {
|
|
10039
10114
|
const approved = await confirm2();
|
|
10040
10115
|
if (!approved) {
|
|
10041
10116
|
say("Setup reviewed. No files written.");
|
|
10042
10117
|
say("Next step: run `haus apply --write` when ready.");
|
|
10118
|
+
emitJsonEnvelope(json, quiet, envelope(hooks));
|
|
10043
10119
|
return baseResult;
|
|
10044
10120
|
}
|
|
10045
10121
|
}
|
|
@@ -10049,6 +10125,7 @@ async function runSetupCore(root, opts) {
|
|
|
10049
10125
|
if (sync.formatRefusal) {
|
|
10050
10126
|
error(sync.formatRefusal);
|
|
10051
10127
|
process.exitCode = 1;
|
|
10128
|
+
emitJsonEnvelope(json, quiet, envelope(hooks));
|
|
10052
10129
|
return baseResult;
|
|
10053
10130
|
}
|
|
10054
10131
|
if (sync.newItems.length > 0) {
|
|
@@ -10076,6 +10153,7 @@ async function runSetupCore(root, opts) {
|
|
|
10076
10153
|
} else {
|
|
10077
10154
|
say(`- HOOKS OK: ${hooksAfter.message}`);
|
|
10078
10155
|
}
|
|
10156
|
+
emitJsonEnvelope(json, quiet, envelope(hooksAfter));
|
|
10079
10157
|
return { ...baseResult, hooksOk: hooksOkAfter, written: files };
|
|
10080
10158
|
}
|
|
10081
10159
|
|
|
@@ -10139,7 +10217,7 @@ function buildCompanionSuggestions(tools, isInstalled) {
|
|
|
10139
10217
|
}
|
|
10140
10218
|
return lines;
|
|
10141
10219
|
}
|
|
10142
|
-
function
|
|
10220
|
+
function commandExists(bin) {
|
|
10143
10221
|
try {
|
|
10144
10222
|
const r = execaSync("sh", ["-c", 'command -v -- "$1"', "probe", bin], { reject: false });
|
|
10145
10223
|
return r.exitCode === 0;
|
|
@@ -10148,7 +10226,7 @@ function commandExists2(bin) {
|
|
|
10148
10226
|
}
|
|
10149
10227
|
}
|
|
10150
10228
|
function printCompanionToolSuggestions(deps = {}) {
|
|
10151
|
-
const isInstalled = deps.isInstalled ??
|
|
10229
|
+
const isInstalled = deps.isInstalled ?? commandExists;
|
|
10152
10230
|
const emit2 = deps.log ?? log;
|
|
10153
10231
|
const lines = buildCompanionSuggestions(COMPANION_TOOLS, isInstalled);
|
|
10154
10232
|
for (const line2 of lines) emit2(line2);
|
|
@@ -10239,7 +10317,7 @@ async function safeExec(exec, command, args, options) {
|
|
|
10239
10317
|
return await exec(command, args, options);
|
|
10240
10318
|
} catch (error2) {
|
|
10241
10319
|
const message2 = error2 instanceof Error ? error2.message : String(error2);
|
|
10242
|
-
return { command, args, stdout: "", stderr: message2, exitCode: 1 };
|
|
10320
|
+
return { command, args, stdout: "", stderr: message2, exitCode: 1, killed: false };
|
|
10243
10321
|
}
|
|
10244
10322
|
}
|
|
10245
10323
|
async function checkGh(exec = runCommand) {
|
|
@@ -10347,7 +10425,7 @@ var SCAFFOLD_TIMEOUT_MS = 3e5;
|
|
|
10347
10425
|
var defaultDeps = {
|
|
10348
10426
|
exec: runCommand,
|
|
10349
10427
|
git: runGit,
|
|
10350
|
-
commandExists,
|
|
10428
|
+
commandExists: probeCommand,
|
|
10351
10429
|
checkGh: () => checkGh(),
|
|
10352
10430
|
checkOrg: (org) => checkOrg(org),
|
|
10353
10431
|
createRepo,
|
|
@@ -10405,8 +10483,32 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
10405
10483
|
});
|
|
10406
10484
|
return { ok: true, projectPath, installHint, steps };
|
|
10407
10485
|
}
|
|
10408
|
-
|
|
10409
|
-
|
|
10486
|
+
const requiredTools = ["npx", opts.pm];
|
|
10487
|
+
const missingTools = [];
|
|
10488
|
+
const inconclusiveTools = [];
|
|
10489
|
+
for (const tool of requiredTools) {
|
|
10490
|
+
const probe = await deps.commandExists(tool);
|
|
10491
|
+
switch (probe.status) {
|
|
10492
|
+
case "available":
|
|
10493
|
+
break;
|
|
10494
|
+
case "absent":
|
|
10495
|
+
missingTools.push(tool);
|
|
10496
|
+
break;
|
|
10497
|
+
case "inconclusive":
|
|
10498
|
+
inconclusiveTools.push(`${tool} (${probe.reason})`);
|
|
10499
|
+
break;
|
|
10500
|
+
default:
|
|
10501
|
+
return assertNeverCommandProbe(probe);
|
|
10502
|
+
}
|
|
10503
|
+
}
|
|
10504
|
+
if (missingTools.length > 0) {
|
|
10505
|
+
return fail("preflight", `Required tools missing: need ${missingTools.join(" and ")} on PATH.`);
|
|
10506
|
+
}
|
|
10507
|
+
if (inconclusiveTools.length > 0) {
|
|
10508
|
+
return fail(
|
|
10509
|
+
"preflight",
|
|
10510
|
+
`Could not confirm tool availability on PATH: ${inconclusiveTools.join("; ")} \u2014 the probe did not complete (likely transient load). Try again.`
|
|
10511
|
+
);
|
|
10410
10512
|
}
|
|
10411
10513
|
if (opts.github) {
|
|
10412
10514
|
const gh = await deps.checkGh();
|
|
@@ -11657,7 +11759,8 @@ async function removeWorktree(ownerRepo, worktreePath2, force = true) {
|
|
|
11657
11759
|
return { ok: result.exitCode === 0, stderr: result.exitCode === 0 ? void 0 : result.stderr };
|
|
11658
11760
|
}
|
|
11659
11761
|
async function pruneWorktrees(ownerRepo) {
|
|
11660
|
-
await runGit(["worktree", "prune"], { cwd: ownerRepo });
|
|
11762
|
+
const result = await runGit(["worktree", "prune"], { cwd: ownerRepo });
|
|
11763
|
+
return { ok: result.exitCode === 0, stderr: result.exitCode === 0 ? void 0 : result.stderr };
|
|
11661
11764
|
}
|
|
11662
11765
|
async function listWorktrees(ownerRepo) {
|
|
11663
11766
|
const result = await runGit(["worktree", "list", "--porcelain"], { cwd: ownerRepo });
|
|
@@ -11835,22 +11938,114 @@ async function computeLockfileSignals(dir) {
|
|
|
11835
11938
|
hasDotnetProject
|
|
11836
11939
|
};
|
|
11837
11940
|
}
|
|
11838
|
-
|
|
11941
|
+
var INSTALL_OUTPUT_CAP = 8192;
|
|
11942
|
+
var INSTALL_TAIL_LINES = 8;
|
|
11943
|
+
function capTail(text, max = INSTALL_OUTPUT_CAP) {
|
|
11944
|
+
return text.length <= max ? text : text.slice(-max);
|
|
11945
|
+
}
|
|
11946
|
+
function nonemptyLines(text) {
|
|
11947
|
+
return text.split(/\r?\n/).filter((line2) => line2.trim().length > 0);
|
|
11948
|
+
}
|
|
11949
|
+
function findRequiredYarnrcEnvVars(content2) {
|
|
11950
|
+
const required = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
11951
|
+
const names = /* @__PURE__ */ new Set();
|
|
11952
|
+
for (const line2 of content2.split(/\r?\n/)) {
|
|
11953
|
+
if (line2.trimStart().startsWith("#")) continue;
|
|
11954
|
+
required.lastIndex = 0;
|
|
11955
|
+
let match;
|
|
11956
|
+
while ((match = required.exec(line2)) !== null) {
|
|
11957
|
+
const name = match[1];
|
|
11958
|
+
if (name !== void 0) names.add(name);
|
|
11959
|
+
}
|
|
11960
|
+
}
|
|
11961
|
+
return [...names];
|
|
11962
|
+
}
|
|
11963
|
+
function missingYarnrcEnvVars(content2, env) {
|
|
11964
|
+
return findRequiredYarnrcEnvVars(content2).filter((name) => env[name] === void 0);
|
|
11965
|
+
}
|
|
11966
|
+
function missingYarnrcEnvVarsInDir(dir, env) {
|
|
11967
|
+
const yarnrcPath = path59.join(dir, ".yarnrc.yml");
|
|
11968
|
+
if (!existsSync3(yarnrcPath)) return [];
|
|
11969
|
+
try {
|
|
11970
|
+
return missingYarnrcEnvVars(readFileSync3(yarnrcPath, "utf8"), env);
|
|
11971
|
+
} catch {
|
|
11972
|
+
return [];
|
|
11973
|
+
}
|
|
11974
|
+
}
|
|
11975
|
+
function tailInstallOutput(stdout2, stderr) {
|
|
11976
|
+
const errLines = nonemptyLines(stderr).slice(-INSTALL_TAIL_LINES);
|
|
11977
|
+
const remaining = INSTALL_TAIL_LINES - errLines.length;
|
|
11978
|
+
const outLines = remaining > 0 ? nonemptyLines(stdout2).slice(-remaining) : [];
|
|
11979
|
+
return [...errLines, ...outLines].join("\n");
|
|
11980
|
+
}
|
|
11981
|
+
function summarizeInstall(install) {
|
|
11982
|
+
if (install.plan.manager === "none") return "no install step (no recognized lockfile)";
|
|
11983
|
+
const envNotes = (install.missingEnvVars ?? []).map(
|
|
11984
|
+
(name) => `${name} not set in environment \u2014 yarn install will fail`
|
|
11985
|
+
);
|
|
11986
|
+
if (!install.ran) {
|
|
11987
|
+
const skip = `install skipped (${install.skippedReason ?? "unavailable"})`;
|
|
11988
|
+
return envNotes.length > 0 ? [skip, ...envNotes].join("\n ") : skip;
|
|
11989
|
+
}
|
|
11990
|
+
if (install.ok) return `hydrated via ${install.plan.manager}`;
|
|
11991
|
+
const head = `install FAILED via ${install.plan.manager}`;
|
|
11992
|
+
const tail = tailInstallOutput(install.stdout ?? "", install.stderr ?? "");
|
|
11993
|
+
const details = [...envNotes, ...tail.length > 0 ? tail.split("\n") : []];
|
|
11994
|
+
return details.length > 0 ? [head, ...details].join("\n ") : head;
|
|
11995
|
+
}
|
|
11996
|
+
function withMissingEnv(result, missingEnvVars) {
|
|
11997
|
+
return missingEnvVars.length > 0 ? { ...result, missingEnvVars } : result;
|
|
11998
|
+
}
|
|
11999
|
+
async function runInstallPlan(dir, plan, deps = {}) {
|
|
12000
|
+
const env = deps.env ?? process.env;
|
|
12001
|
+
const missingEnvVars = plan.manager === "yarn" ? missingYarnrcEnvVarsInDir(dir, env) : [];
|
|
11839
12002
|
if (plan.manager === "none") {
|
|
11840
|
-
return
|
|
12003
|
+
return withMissingEnv(
|
|
12004
|
+
{ plan, ran: false, ok: true, skippedReason: "no recognized lockfile" },
|
|
12005
|
+
missingEnvVars
|
|
12006
|
+
);
|
|
11841
12007
|
}
|
|
11842
|
-
const
|
|
11843
|
-
|
|
11844
|
-
|
|
12008
|
+
const probe = deps.probe ?? probeCommand;
|
|
12009
|
+
const probed = await probe(plan.command);
|
|
12010
|
+
switch (probed.status) {
|
|
12011
|
+
case "available":
|
|
12012
|
+
break;
|
|
12013
|
+
case "absent":
|
|
12014
|
+
return withMissingEnv(
|
|
12015
|
+
{ plan, ran: false, ok: true, skippedReason: `${plan.command} not found on PATH` },
|
|
12016
|
+
missingEnvVars
|
|
12017
|
+
);
|
|
12018
|
+
case "inconclusive":
|
|
12019
|
+
return withMissingEnv(
|
|
12020
|
+
{
|
|
12021
|
+
plan,
|
|
12022
|
+
ran: false,
|
|
12023
|
+
ok: true,
|
|
12024
|
+
skippedReason: `could not determine whether ${plan.command} is on PATH (${probed.reason})`
|
|
12025
|
+
},
|
|
12026
|
+
missingEnvVars
|
|
12027
|
+
);
|
|
12028
|
+
default:
|
|
12029
|
+
return assertNeverCommandProbe(probed);
|
|
11845
12030
|
}
|
|
11846
|
-
const
|
|
11847
|
-
|
|
11848
|
-
|
|
11849
|
-
ran: true,
|
|
11850
|
-
|
|
11851
|
-
|
|
11852
|
-
|
|
11853
|
-
|
|
12031
|
+
const run = deps.run ?? runCommand;
|
|
12032
|
+
const result = await run(plan.command, plan.args, { cwd: dir });
|
|
12033
|
+
if (result.exitCode === 0) {
|
|
12034
|
+
return withMissingEnv({ plan, ran: true, ok: true, exitCode: 0 }, missingEnvVars);
|
|
12035
|
+
}
|
|
12036
|
+
const stdout2 = capTail(result.stdout);
|
|
12037
|
+
const stderr = capTail(result.stderr);
|
|
12038
|
+
return withMissingEnv(
|
|
12039
|
+
{
|
|
12040
|
+
plan,
|
|
12041
|
+
ran: true,
|
|
12042
|
+
ok: false,
|
|
12043
|
+
exitCode: result.exitCode,
|
|
12044
|
+
stdout: stdout2.length > 0 ? stdout2 : void 0,
|
|
12045
|
+
stderr: stderr.length > 0 ? stderr : void 0
|
|
12046
|
+
},
|
|
12047
|
+
missingEnvVars
|
|
12048
|
+
);
|
|
11854
12049
|
}
|
|
11855
12050
|
|
|
11856
12051
|
// src/workspace/worktree/hydrate.ts
|
|
@@ -12093,11 +12288,41 @@ async function runDryRun(args) {
|
|
|
12093
12288
|
}
|
|
12094
12289
|
|
|
12095
12290
|
// src/workspace/worktree/doctor.ts
|
|
12291
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
12292
|
+
import path66 from "node:path";
|
|
12293
|
+
|
|
12294
|
+
// src/workspace/worktree/orphans.ts
|
|
12096
12295
|
import { existsSync as existsSync5 } from "node:fs";
|
|
12097
12296
|
import path65 from "node:path";
|
|
12297
|
+
var WORKSPACE_REPO_ID = "(workspace)";
|
|
12298
|
+
async function findOrphanedRegistrations(repos, wsWorktreesDir) {
|
|
12299
|
+
const orphans = [];
|
|
12300
|
+
for (const repo of repos) {
|
|
12301
|
+
const entries = await listWorktrees(repo.absPath);
|
|
12302
|
+
for (const entry of entries) {
|
|
12303
|
+
const entryPath = path65.normalize(entry.path);
|
|
12304
|
+
const marker = `${path65.sep}.claude${path65.sep}worktrees${path65.sep}`;
|
|
12305
|
+
const idx = entryPath.indexOf(marker);
|
|
12306
|
+
if (idx === -1) continue;
|
|
12307
|
+
const rest = entryPath.slice(idx + marker.length);
|
|
12308
|
+
const slug = rest.split(path65.sep)[0];
|
|
12309
|
+
if (!slug) continue;
|
|
12310
|
+
if (existsSync5(path65.join(wsWorktreesDir, slug))) continue;
|
|
12311
|
+
orphans.push({
|
|
12312
|
+
repoId: repo.id,
|
|
12313
|
+
ownerRepo: repo.absPath,
|
|
12314
|
+
registeredPath: entry.path,
|
|
12315
|
+
slug
|
|
12316
|
+
});
|
|
12317
|
+
}
|
|
12318
|
+
}
|
|
12319
|
+
return orphans;
|
|
12320
|
+
}
|
|
12321
|
+
|
|
12322
|
+
// src/workspace/worktree/doctor.ts
|
|
12098
12323
|
var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
|
|
12099
12324
|
function looksHydrated(memberPath) {
|
|
12100
|
-
return HYDRATION_MARKERS.some((marker) =>
|
|
12325
|
+
return HYDRATION_MARKERS.some((marker) => existsSync6(path66.join(memberPath, marker)));
|
|
12101
12326
|
}
|
|
12102
12327
|
async function runDoctor2() {
|
|
12103
12328
|
const rootInfo = await resolveRoots();
|
|
@@ -12107,11 +12332,13 @@ async function runDoctor2() {
|
|
|
12107
12332
|
inWorkspaceWorktree: false,
|
|
12108
12333
|
members: [],
|
|
12109
12334
|
orphans: [],
|
|
12110
|
-
problems: ["Not inside a git repository."]
|
|
12335
|
+
problems: ["Not inside a git repository."],
|
|
12336
|
+
remedies: [],
|
|
12337
|
+
autoHydrate: false
|
|
12111
12338
|
};
|
|
12112
12339
|
}
|
|
12113
12340
|
const workspaceRoot = rootInfo.mainRoot;
|
|
12114
|
-
const isWorkspace =
|
|
12341
|
+
const isWorkspace = existsSync6(path66.join(workspaceRoot, WORKSPACE_FILE)) || existsSync6(path66.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
12115
12342
|
if (!isWorkspace) {
|
|
12116
12343
|
return {
|
|
12117
12344
|
isWorkspace: false,
|
|
@@ -12120,23 +12347,28 @@ async function runDoctor2() {
|
|
|
12120
12347
|
orphans: [],
|
|
12121
12348
|
problems: [
|
|
12122
12349
|
`Not a haus workspace \u2014 no ${WORKSPACE_FILE} or ${REPOS_MANIFEST_FILE} at ${workspaceRoot}.`
|
|
12123
|
-
]
|
|
12350
|
+
],
|
|
12351
|
+
remedies: [],
|
|
12352
|
+
autoHydrate: false
|
|
12124
12353
|
};
|
|
12125
12354
|
}
|
|
12126
12355
|
const wsWorktreesDir = worktreesDir(workspaceRoot);
|
|
12127
|
-
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${
|
|
12356
|
+
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path66.sep}`);
|
|
12128
12357
|
const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
|
|
12358
|
+
const workspaceConfig = await readWorkspaceConfig(workspaceRoot);
|
|
12359
|
+
const autoHydrate = workspaceConfig?.worktree?.autoHydrate === true;
|
|
12129
12360
|
const members = await readMembers(rootInfo);
|
|
12130
12361
|
const problems = [];
|
|
12362
|
+
const remedies = [];
|
|
12131
12363
|
const memberChecks = [];
|
|
12132
12364
|
let slug;
|
|
12133
12365
|
if (inWorkspaceWorktree) {
|
|
12134
12366
|
const wtPath = rootInfo.repoRoot;
|
|
12135
|
-
slug = rootInfo.worktreeName ??
|
|
12367
|
+
slug = rootInfo.worktreeName ?? path66.basename(wtPath);
|
|
12136
12368
|
const state = await readWorktreeState(wtPath);
|
|
12137
12369
|
for (const member of members) {
|
|
12138
|
-
const memberPath =
|
|
12139
|
-
const materialized =
|
|
12370
|
+
const memberPath = path66.join(wtPath, member.folder);
|
|
12371
|
+
const materialized = existsSync6(memberPath);
|
|
12140
12372
|
const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
|
|
12141
12373
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
12142
12374
|
const branchMismatch = Boolean(
|
|
@@ -12161,33 +12393,42 @@ async function runDoctor2() {
|
|
|
12161
12393
|
);
|
|
12162
12394
|
}
|
|
12163
12395
|
}
|
|
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
|
-
}
|
|
12396
|
+
if (memberChecks.some((m) => !m.materialized)) {
|
|
12397
|
+
remedies.push(
|
|
12398
|
+
"Run `haus workspace worktree hydrate --materialize` to materialize the missing members."
|
|
12399
|
+
);
|
|
12181
12400
|
}
|
|
12182
12401
|
}
|
|
12183
|
-
|
|
12184
|
-
|
|
12402
|
+
const orphanScan = await findOrphanedRegistrations(
|
|
12403
|
+
[
|
|
12404
|
+
{ id: WORKSPACE_REPO_ID, absPath: workspaceRoot },
|
|
12405
|
+
...members.map((m) => ({ id: m.id, absPath: m.absPath }))
|
|
12406
|
+
],
|
|
12407
|
+
wsWorktreesDir
|
|
12408
|
+
);
|
|
12409
|
+
const orphans = orphanScan.map(
|
|
12410
|
+
(o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${path66.join(wsWorktreesDir, o.slug)} no longer exists.`
|
|
12411
|
+
);
|
|
12412
|
+
if (orphans.length > 0) {
|
|
12413
|
+
problems.push(...orphans);
|
|
12414
|
+
remedies.push("Run `haus workspace worktree prune` to clean up stale worktree registrations.");
|
|
12415
|
+
}
|
|
12416
|
+
return {
|
|
12417
|
+
isWorkspace: true,
|
|
12418
|
+
inWorkspaceWorktree,
|
|
12419
|
+
slug,
|
|
12420
|
+
members: memberChecks,
|
|
12421
|
+
orphans,
|
|
12422
|
+
problems,
|
|
12423
|
+
remedies,
|
|
12424
|
+
autoHydrate
|
|
12425
|
+
};
|
|
12185
12426
|
}
|
|
12186
12427
|
|
|
12187
12428
|
// src/workspace/worktree/hydrate-command.ts
|
|
12188
12429
|
init_fs();
|
|
12189
|
-
import { existsSync as
|
|
12190
|
-
import
|
|
12430
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
12431
|
+
import path67 from "node:path";
|
|
12191
12432
|
async function runHydrateWorktree(opts) {
|
|
12192
12433
|
const rootInfo = await resolveRoots();
|
|
12193
12434
|
if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
|
|
@@ -12198,7 +12439,7 @@ async function runHydrateWorktree(opts) {
|
|
|
12198
12439
|
}
|
|
12199
12440
|
const workspaceRoot = rootInfo.mainRoot;
|
|
12200
12441
|
const wtPath = rootInfo.repoRoot;
|
|
12201
|
-
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${
|
|
12442
|
+
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path67.sep}`)) {
|
|
12202
12443
|
return {
|
|
12203
12444
|
ok: false,
|
|
12204
12445
|
error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
|
|
@@ -12210,14 +12451,14 @@ async function runHydrateWorktree(opts) {
|
|
|
12210
12451
|
return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
|
|
12211
12452
|
}
|
|
12212
12453
|
const state = await readWorktreeState(wtPath);
|
|
12213
|
-
const slug = state?.slug ??
|
|
12454
|
+
const slug = state?.slug ?? path67.basename(wtPath);
|
|
12214
12455
|
const detectedBranch = await currentBranch(wtPath);
|
|
12215
12456
|
const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
|
|
12216
12457
|
if (opts.dryRun) {
|
|
12217
12458
|
const results2 = await Promise.all(
|
|
12218
12459
|
selected.map(async (member) => {
|
|
12219
|
-
const memberPath =
|
|
12220
|
-
if (
|
|
12460
|
+
const memberPath = path67.join(wtPath, member.folder);
|
|
12461
|
+
if (existsSync7(memberPath)) {
|
|
12221
12462
|
return { member: member.id, folder: member.folder, status: "planned" };
|
|
12222
12463
|
}
|
|
12223
12464
|
if (!opts.materialize) {
|
|
@@ -12225,7 +12466,7 @@ async function runHydrateWorktree(opts) {
|
|
|
12225
12466
|
member: member.id,
|
|
12226
12467
|
folder: member.folder,
|
|
12227
12468
|
status: "skipped",
|
|
12228
|
-
error: "not materialized in this worktree"
|
|
12469
|
+
error: "not materialized in this worktree \u2014 re-run with --materialize to create it"
|
|
12229
12470
|
};
|
|
12230
12471
|
}
|
|
12231
12472
|
const branchAction = await branchExists(member.absPath, targetBranch) ? "checkout" : "create";
|
|
@@ -12237,15 +12478,15 @@ async function runHydrateWorktree(opts) {
|
|
|
12237
12478
|
const results = await mapWithConcurrency(
|
|
12238
12479
|
selected,
|
|
12239
12480
|
async (member) => {
|
|
12240
|
-
const memberPath =
|
|
12481
|
+
const memberPath = path67.join(wtPath, member.folder);
|
|
12241
12482
|
let branchAction;
|
|
12242
|
-
if (!
|
|
12483
|
+
if (!existsSync7(memberPath)) {
|
|
12243
12484
|
if (!opts.materialize) {
|
|
12244
12485
|
return {
|
|
12245
12486
|
member: member.id,
|
|
12246
12487
|
folder: member.folder,
|
|
12247
12488
|
status: "skipped",
|
|
12248
|
-
error: "not materialized in this worktree"
|
|
12489
|
+
error: "not materialized in this worktree \u2014 re-run with --materialize to create it"
|
|
12249
12490
|
};
|
|
12250
12491
|
}
|
|
12251
12492
|
const outcome = await addWorktree(member.absPath, memberPath, targetBranch, {
|
|
@@ -12300,7 +12541,7 @@ async function runHydrateWorktree(opts) {
|
|
|
12300
12541
|
}
|
|
12301
12542
|
|
|
12302
12543
|
// src/workspace/worktree/list.ts
|
|
12303
|
-
import
|
|
12544
|
+
import path68 from "node:path";
|
|
12304
12545
|
import fs47 from "fs-extra";
|
|
12305
12546
|
async function runList2() {
|
|
12306
12547
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -12313,12 +12554,12 @@ async function runList2() {
|
|
|
12313
12554
|
const configuredMembers = await readMembers(rootInfo);
|
|
12314
12555
|
const worktrees = [];
|
|
12315
12556
|
for (const slug of slugs) {
|
|
12316
|
-
const wtPath =
|
|
12557
|
+
const wtPath = path68.join(dir, slug);
|
|
12317
12558
|
const state = await readWorktreeState(wtPath);
|
|
12318
12559
|
const members = [];
|
|
12319
12560
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
12320
12561
|
for (const m of source) {
|
|
12321
|
-
const memberPath =
|
|
12562
|
+
const memberPath = path68.join(wtPath, m.folder);
|
|
12322
12563
|
const materialized = await fs47.pathExists(memberPath);
|
|
12323
12564
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
12324
12565
|
members.push({
|
|
@@ -12339,8 +12580,48 @@ async function runList2() {
|
|
|
12339
12580
|
return { ok: true, workspaceRoot, worktrees };
|
|
12340
12581
|
}
|
|
12341
12582
|
|
|
12583
|
+
// src/workspace/worktree/prune.ts
|
|
12584
|
+
async function runPrune2(opts) {
|
|
12585
|
+
const resolved = await resolveWorkspaceForWorktree();
|
|
12586
|
+
if (!resolved.ok) return { ok: false, error: resolved.reason };
|
|
12587
|
+
const { rootInfo, workspaceRoot } = resolved;
|
|
12588
|
+
const members = await readMembers(rootInfo);
|
|
12589
|
+
const orphanScan = await findOrphanedRegistrations(
|
|
12590
|
+
[
|
|
12591
|
+
{ id: WORKSPACE_REPO_ID, absPath: workspaceRoot },
|
|
12592
|
+
...members.map((m) => ({ id: m.id, absPath: m.absPath }))
|
|
12593
|
+
],
|
|
12594
|
+
worktreesDir(workspaceRoot)
|
|
12595
|
+
);
|
|
12596
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
12597
|
+
for (const orphan of orphanScan) {
|
|
12598
|
+
const bucket = byRepo.get(orphan.repoId) ?? { ownerRepo: orphan.ownerRepo, orphans: [] };
|
|
12599
|
+
bucket.orphans.push(orphan);
|
|
12600
|
+
byRepo.set(orphan.repoId, bucket);
|
|
12601
|
+
}
|
|
12602
|
+
if (opts.dryRun) {
|
|
12603
|
+
const results2 = Array.from(byRepo, ([repo, { orphans }]) => ({
|
|
12604
|
+
repo,
|
|
12605
|
+
orphans: orphans.map((o) => o.registeredPath),
|
|
12606
|
+
pruned: false
|
|
12607
|
+
}));
|
|
12608
|
+
return { ok: true, dryRun: true, results: results2 };
|
|
12609
|
+
}
|
|
12610
|
+
const results = [];
|
|
12611
|
+
for (const [repo, { ownerRepo, orphans }] of byRepo) {
|
|
12612
|
+
const outcome = await pruneWorktrees(ownerRepo);
|
|
12613
|
+
results.push({
|
|
12614
|
+
repo,
|
|
12615
|
+
orphans: orphans.map((o) => o.registeredPath),
|
|
12616
|
+
pruned: outcome.ok,
|
|
12617
|
+
error: outcome.ok ? void 0 : outcome.stderr ?? "git worktree prune failed"
|
|
12618
|
+
});
|
|
12619
|
+
}
|
|
12620
|
+
return { ok: results.every((r) => r.pruned), dryRun: false, results };
|
|
12621
|
+
}
|
|
12622
|
+
|
|
12342
12623
|
// src/workspace/worktree/remove.ts
|
|
12343
|
-
import
|
|
12624
|
+
import path69 from "node:path";
|
|
12344
12625
|
import fs48 from "fs-extra";
|
|
12345
12626
|
async function runRemove2(opts) {
|
|
12346
12627
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -12367,7 +12648,7 @@ async function runRemove2(opts) {
|
|
|
12367
12648
|
targets.push({
|
|
12368
12649
|
repo: configMember?.id ?? stateMember.id,
|
|
12369
12650
|
ownerRepo,
|
|
12370
|
-
path:
|
|
12651
|
+
path: path69.join(wsWorktreePath, stateMember.folder)
|
|
12371
12652
|
});
|
|
12372
12653
|
}
|
|
12373
12654
|
const existingTargets = [];
|
|
@@ -12439,10 +12720,11 @@ function emitJson(result) {
|
|
|
12439
12720
|
log(JSON.stringify(result, null, 2));
|
|
12440
12721
|
}
|
|
12441
12722
|
function describeInstall(hydration) {
|
|
12442
|
-
|
|
12443
|
-
|
|
12444
|
-
|
|
12445
|
-
|
|
12723
|
+
return summarizeInstall(hydration.install);
|
|
12724
|
+
}
|
|
12725
|
+
function emitMemberLine(hydration, line2) {
|
|
12726
|
+
if (hydration?.install.ran && !hydration.install.ok) warn(line2);
|
|
12727
|
+
else log(line2);
|
|
12446
12728
|
}
|
|
12447
12729
|
async function handleAdd(opts) {
|
|
12448
12730
|
if (!opts.slug) {
|
|
@@ -12487,7 +12769,10 @@ async function handleAdd(opts) {
|
|
|
12487
12769
|
for (const m of result.members) {
|
|
12488
12770
|
if (m.status === "ok") {
|
|
12489
12771
|
const hydrationNote = m.hydration ? describeInstall(m.hydration) : "not hydrated (--no-hydrate)";
|
|
12490
|
-
|
|
12772
|
+
emitMemberLine(
|
|
12773
|
+
m.hydration,
|
|
12774
|
+
` ${m.member}: ${m.branchAction} "${m.branch}" \u2014 ${hydrationNote} (${m.durationMs}ms)`
|
|
12775
|
+
);
|
|
12491
12776
|
} else {
|
|
12492
12777
|
warn(` ${m.member}: FAILED \u2014 ${m.error}`);
|
|
12493
12778
|
}
|
|
@@ -12531,7 +12816,10 @@ async function handleHydrate(opts) {
|
|
|
12531
12816
|
for (const r of result.results) {
|
|
12532
12817
|
if (r.status === "ok" && r.hydration) {
|
|
12533
12818
|
const materializedNote = r.branchAction ? `materialized (${r.branchAction}), ` : "";
|
|
12534
|
-
|
|
12819
|
+
emitMemberLine(
|
|
12820
|
+
r.hydration,
|
|
12821
|
+
` ${r.member}: ${materializedNote}${describeInstall(r.hydration)}`
|
|
12822
|
+
);
|
|
12535
12823
|
} else {
|
|
12536
12824
|
warn(` ${r.member}: ${r.status}${r.error ? ` \u2014 ${r.error}` : ""}`);
|
|
12537
12825
|
}
|
|
@@ -12580,6 +12868,32 @@ async function handleRemove(opts) {
|
|
|
12580
12868
|
}
|
|
12581
12869
|
}
|
|
12582
12870
|
}
|
|
12871
|
+
async function handlePrune(opts) {
|
|
12872
|
+
const result = await runPrune2({
|
|
12873
|
+
dryRun: resolveWriteMode(opts, "mutate-by-default").kind === "preview"
|
|
12874
|
+
});
|
|
12875
|
+
if (!result.ok && "error" in result) {
|
|
12876
|
+
if (opts.json) emitJson(result);
|
|
12877
|
+
else error(result.error);
|
|
12878
|
+
process.exitCode = 1;
|
|
12879
|
+
return;
|
|
12880
|
+
}
|
|
12881
|
+
if (opts.json) {
|
|
12882
|
+
emitJson(result);
|
|
12883
|
+
if (!result.dryRun && !result.ok) process.exitCode = 1;
|
|
12884
|
+
return;
|
|
12885
|
+
}
|
|
12886
|
+
if (result.results.length === 0) {
|
|
12887
|
+
log("No stale worktree registrations found.");
|
|
12888
|
+
return;
|
|
12889
|
+
}
|
|
12890
|
+
for (const r of result.results) {
|
|
12891
|
+
const prefix = result.dryRun ? "[dry-run] Would prune" : r.pruned ? "Pruned" : "FAILED to prune";
|
|
12892
|
+
const detail = r.error ? ` \u2014 ${r.error}` : "";
|
|
12893
|
+
log(`${prefix} ${r.repo}: ${r.orphans.join(", ")}${detail}`);
|
|
12894
|
+
}
|
|
12895
|
+
if (!result.dryRun && !result.ok) process.exitCode = 1;
|
|
12896
|
+
}
|
|
12583
12897
|
async function handleList(opts) {
|
|
12584
12898
|
const result = await runList2();
|
|
12585
12899
|
if (opts.json) {
|
|
@@ -12631,12 +12945,17 @@ async function handleDoctor(opts) {
|
|
|
12631
12945
|
}
|
|
12632
12946
|
if (opts.fromHook) {
|
|
12633
12947
|
const scope = report.slug ? `Workspace worktree "${report.slug}"` : "This haus workspace";
|
|
12948
|
+
const needsMaterialize = report.members.some((m) => !m.materialized || !m.hydrated);
|
|
12949
|
+
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. " : "";
|
|
12950
|
+
const remedies = directive ? report.remedies.filter((r) => !r.includes("--materialize")) : report.remedies;
|
|
12951
|
+
const remedySuffix = remedies.length > 0 ? ` ${remedies.join(" ")}` : "";
|
|
12634
12952
|
emitFromHookContext(
|
|
12635
|
-
`${scope} has ${report.problems.length} issue(s): ${report.problems.join("; ")}`
|
|
12953
|
+
`${directive}${scope} has ${report.problems.length} issue(s): ${report.problems.join("; ")}${remedySuffix}`
|
|
12636
12954
|
);
|
|
12637
12955
|
return;
|
|
12638
12956
|
}
|
|
12639
12957
|
for (const p of report.problems) warn(p);
|
|
12958
|
+
for (const r of report.remedies) log(r);
|
|
12640
12959
|
process.exitCode = 1;
|
|
12641
12960
|
} catch (err) {
|
|
12642
12961
|
const message2 = err instanceof Error ? err.message : String(err);
|
|
@@ -12660,12 +12979,14 @@ async function runWorktree(action, opts = {}) {
|
|
|
12660
12979
|
return handleList(opts);
|
|
12661
12980
|
case "doctor":
|
|
12662
12981
|
return handleDoctor(opts);
|
|
12982
|
+
case "prune":
|
|
12983
|
+
return handlePrune(opts);
|
|
12663
12984
|
}
|
|
12664
12985
|
}
|
|
12665
12986
|
|
|
12666
12987
|
// src/commands/workspace.ts
|
|
12667
|
-
import { existsSync as
|
|
12668
|
-
import
|
|
12988
|
+
import { existsSync as existsSync11, statSync as statSync3 } from "node:fs";
|
|
12989
|
+
import path78 from "node:path";
|
|
12669
12990
|
init_fs();
|
|
12670
12991
|
init_logger();
|
|
12671
12992
|
|
|
@@ -12723,8 +13044,8 @@ ${summaries.map(
|
|
|
12723
13044
|
}
|
|
12724
13045
|
|
|
12725
13046
|
// src/commands/workspace/doctor.ts
|
|
12726
|
-
import { existsSync as
|
|
12727
|
-
import
|
|
13047
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
13048
|
+
import path71 from "node:path";
|
|
12728
13049
|
init_logger();
|
|
12729
13050
|
init_paths();
|
|
12730
13051
|
|
|
@@ -12732,14 +13053,14 @@ init_paths();
|
|
|
12732
13053
|
init_fs();
|
|
12733
13054
|
init_paths();
|
|
12734
13055
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
12735
|
-
import
|
|
13056
|
+
import path70 from "node:path";
|
|
12736
13057
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
12737
13058
|
function manifestPath(workspaceRoot) {
|
|
12738
13059
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
12739
13060
|
}
|
|
12740
13061
|
function hausVersion() {
|
|
12741
13062
|
try {
|
|
12742
|
-
const pkg = JSON.parse(readFileSync4(
|
|
13063
|
+
const pkg = JSON.parse(readFileSync4(path70.join(packageRoot(), "package.json"), "utf8"));
|
|
12743
13064
|
return pkg.version ?? "0.0.0";
|
|
12744
13065
|
} catch {
|
|
12745
13066
|
return "0.0.0";
|
|
@@ -12831,7 +13152,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
12831
13152
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
12832
13153
|
const catalogRefByRepo = [];
|
|
12833
13154
|
for (const repo of config.repos) {
|
|
12834
|
-
const repoRoot =
|
|
13155
|
+
const repoRoot = path71.resolve(workspaceRoot, repo.path);
|
|
12835
13156
|
const entry = manifestByName.get(repo.name);
|
|
12836
13157
|
if (!entry) {
|
|
12837
13158
|
flag({
|
|
@@ -12856,7 +13177,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
12856
13177
|
detail: `Set up at haus ${entry.hausVersionAtSetup}, current is ${currentVersion} \u2014 re-run setup.`
|
|
12857
13178
|
});
|
|
12858
13179
|
}
|
|
12859
|
-
if (!
|
|
13180
|
+
if (!existsSync8(claudePath(repoRoot))) {
|
|
12860
13181
|
flag({
|
|
12861
13182
|
repo: repo.name,
|
|
12862
13183
|
kind: "missing-claude",
|
|
@@ -12865,7 +13186,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
12865
13186
|
}
|
|
12866
13187
|
const lock = await checkLock(repoRoot);
|
|
12867
13188
|
if (lock.catalogRef) catalogRefByRepo.push({ repo: repo.name, ref: lock.catalogRef });
|
|
12868
|
-
if (!
|
|
13189
|
+
if (!existsSync8(hausPath(repoRoot, "haus.lock.json"))) {
|
|
12869
13190
|
flag({
|
|
12870
13191
|
repo: repo.name,
|
|
12871
13192
|
kind: "missing-lock",
|
|
@@ -12896,7 +13217,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
12896
13217
|
const memberById = new Map(members.map((m) => [m.id, m]));
|
|
12897
13218
|
for (const entry of manifest.linkedContext) {
|
|
12898
13219
|
const member = memberById.get(entry.repo);
|
|
12899
|
-
if (!member || !
|
|
13220
|
+
if (!member || !existsSync8(member.absPath)) {
|
|
12900
13221
|
flag({
|
|
12901
13222
|
repo: entry.repo,
|
|
12902
13223
|
kind: "missing-linked-context-source",
|
|
@@ -12904,7 +13225,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
12904
13225
|
});
|
|
12905
13226
|
continue;
|
|
12906
13227
|
}
|
|
12907
|
-
if (!
|
|
13228
|
+
if (!existsSync8(path71.join(workspaceRoot, entry.path))) {
|
|
12908
13229
|
flag({
|
|
12909
13230
|
repo: entry.repo,
|
|
12910
13231
|
kind: "missing-linked-context-copy",
|
|
@@ -12950,17 +13271,17 @@ function emit(args) {
|
|
|
12950
13271
|
init_logger();
|
|
12951
13272
|
|
|
12952
13273
|
// src/workspace/link-context/link.ts
|
|
12953
|
-
import
|
|
13274
|
+
import path74 from "node:path";
|
|
12954
13275
|
import fs51 from "fs-extra";
|
|
12955
13276
|
init_fs();
|
|
12956
13277
|
init_logger();
|
|
12957
13278
|
|
|
12958
13279
|
// src/workspace/link-context/plan.ts
|
|
12959
|
-
import
|
|
13280
|
+
import path73 from "node:path";
|
|
12960
13281
|
import fs50 from "fs-extra";
|
|
12961
13282
|
|
|
12962
13283
|
// src/workspace/link-context/scan-source.ts
|
|
12963
|
-
import
|
|
13284
|
+
import path72 from "node:path";
|
|
12964
13285
|
import fs49 from "fs-extra";
|
|
12965
13286
|
var ASSET_SUBDIR = {
|
|
12966
13287
|
skill: "skills",
|
|
@@ -12969,36 +13290,36 @@ var ASSET_SUBDIR = {
|
|
|
12969
13290
|
};
|
|
12970
13291
|
async function hasLinkableContext(member) {
|
|
12971
13292
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
12972
|
-
if (await fs49.pathExists(
|
|
13293
|
+
if (await fs49.pathExists(path72.join(member.absPath, ".claude", subdir))) return true;
|
|
12973
13294
|
}
|
|
12974
13295
|
return false;
|
|
12975
13296
|
}
|
|
12976
13297
|
async function collectSkills(member) {
|
|
12977
|
-
const dir =
|
|
13298
|
+
const dir = path72.join(member.absPath, ".claude", "skills");
|
|
12978
13299
|
if (!await fs49.pathExists(dir)) return [];
|
|
12979
13300
|
const names = await fs49.readdir(dir);
|
|
12980
13301
|
const out = [];
|
|
12981
13302
|
for (const name of [...names].sort()) {
|
|
12982
|
-
const abs =
|
|
13303
|
+
const abs = path72.join(dir, name);
|
|
12983
13304
|
const stat = await fs49.lstat(abs);
|
|
12984
13305
|
if (!stat.isDirectory()) continue;
|
|
12985
|
-
if (!await fs49.pathExists(
|
|
12986
|
-
out.push({ type: "skill", name, sourceRelPath:
|
|
13306
|
+
if (!await fs49.pathExists(path72.join(abs, "SKILL.md"))) continue;
|
|
13307
|
+
out.push({ type: "skill", name, sourceRelPath: path72.posix.join(".claude", "skills", name) });
|
|
12987
13308
|
}
|
|
12988
13309
|
return out;
|
|
12989
13310
|
}
|
|
12990
13311
|
async function collectFlatMarkdown(member, type) {
|
|
12991
13312
|
const subdir = ASSET_SUBDIR[type];
|
|
12992
|
-
const dir =
|
|
13313
|
+
const dir = path72.join(member.absPath, ".claude", subdir);
|
|
12993
13314
|
if (!await fs49.pathExists(dir)) return [];
|
|
12994
13315
|
const names = await fs49.readdir(dir);
|
|
12995
13316
|
const out = [];
|
|
12996
13317
|
for (const fileName of [...names].sort()) {
|
|
12997
13318
|
if (!fileName.endsWith(".md")) continue;
|
|
12998
|
-
const abs =
|
|
13319
|
+
const abs = path72.join(dir, fileName);
|
|
12999
13320
|
if (!(await fs49.lstat(abs)).isFile()) continue;
|
|
13000
13321
|
const name = fileName.slice(0, -".md".length);
|
|
13001
|
-
out.push({ type, name, sourceRelPath:
|
|
13322
|
+
out.push({ type, name, sourceRelPath: path72.posix.join(".claude", subdir, fileName) });
|
|
13002
13323
|
}
|
|
13003
13324
|
return out;
|
|
13004
13325
|
}
|
|
@@ -13013,9 +13334,9 @@ async function collectSourceAssets(member) {
|
|
|
13013
13334
|
|
|
13014
13335
|
// src/workspace/link-context/plan.ts
|
|
13015
13336
|
function repoFolderPrefix(member) {
|
|
13016
|
-
const base =
|
|
13337
|
+
const base = path73.posix.basename(member.folder);
|
|
13017
13338
|
if (base && base !== "." && base !== "/") return base;
|
|
13018
|
-
const fromAbs =
|
|
13339
|
+
const fromAbs = path73.basename(member.absPath);
|
|
13019
13340
|
return fromAbs || member.id;
|
|
13020
13341
|
}
|
|
13021
13342
|
function assetKey(type, destKey) {
|
|
@@ -13072,7 +13393,7 @@ async function buildLinkPlan(members) {
|
|
|
13072
13393
|
name: asset.name,
|
|
13073
13394
|
destKey,
|
|
13074
13395
|
sourceRelPath: asset.sourceRelPath,
|
|
13075
|
-
sourceAbsPath:
|
|
13396
|
+
sourceAbsPath: path73.join(member.absPath, asset.sourceRelPath),
|
|
13076
13397
|
sourceHash
|
|
13077
13398
|
});
|
|
13078
13399
|
}
|
|
@@ -13083,7 +13404,7 @@ async function buildLinkPlan(members) {
|
|
|
13083
13404
|
function destRelPathFor(type, destKey) {
|
|
13084
13405
|
const subdir = `${type}s`;
|
|
13085
13406
|
const leaf = type === "skill" ? destKey : `${destKey}.md`;
|
|
13086
|
-
return
|
|
13407
|
+
return path74.posix.join(".claude", subdir, leaf);
|
|
13087
13408
|
}
|
|
13088
13409
|
async function runLinkContext(workspaceRoot, opts = {}) {
|
|
13089
13410
|
const write = resolveWriteMode(opts, "preview-by-default").kind === "write";
|
|
@@ -13109,9 +13430,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
13109
13430
|
const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13110
13431
|
for (const entry of plan.entries) {
|
|
13111
13432
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
13112
|
-
const destAbs =
|
|
13433
|
+
const destAbs = path74.join(workspaceRoot, destRel);
|
|
13113
13434
|
if (write) {
|
|
13114
|
-
await fs51.ensureDir(
|
|
13435
|
+
await fs51.ensureDir(path74.dirname(destAbs));
|
|
13115
13436
|
if (await fs51.pathExists(destAbs)) await fs51.remove(destAbs);
|
|
13116
13437
|
await fs51.copy(entry.sourceAbsPath, destAbs, {
|
|
13117
13438
|
overwrite: true,
|
|
@@ -13199,11 +13520,11 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
|
|
|
13199
13520
|
}
|
|
13200
13521
|
|
|
13201
13522
|
// src/commands/workspace/setup.ts
|
|
13202
|
-
import { existsSync as
|
|
13203
|
-
import
|
|
13523
|
+
import { existsSync as existsSync9, statSync } from "node:fs";
|
|
13524
|
+
import path76 from "node:path";
|
|
13204
13525
|
|
|
13205
13526
|
// src/claude/write-workspace-claude-md.ts
|
|
13206
|
-
import
|
|
13527
|
+
import path75 from "node:path";
|
|
13207
13528
|
import fs52 from "fs-extra";
|
|
13208
13529
|
init_fs();
|
|
13209
13530
|
init_logger();
|
|
@@ -13226,7 +13547,7 @@ ${WORKSPACE_BLOCK_END}`;
|
|
|
13226
13547
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
13227
13548
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
13228
13549
|
const dryRun = opts.dryRun ?? false;
|
|
13229
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
13550
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path75.join(workspaceRoot, "CLAUDE.md");
|
|
13230
13551
|
const say = opts.quiet ? () => {
|
|
13231
13552
|
} : log;
|
|
13232
13553
|
const existed = await fs52.pathExists(filePath);
|
|
@@ -13259,20 +13580,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
13259
13580
|
init_fs();
|
|
13260
13581
|
init_logger();
|
|
13261
13582
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
13262
|
-
let dir =
|
|
13583
|
+
let dir = path76.resolve(start);
|
|
13263
13584
|
for (; ; ) {
|
|
13264
|
-
if (
|
|
13265
|
-
const parent =
|
|
13266
|
-
if (parent === dir) return
|
|
13585
|
+
if (existsSync9(path76.join(dir, WORKSPACE_FILE))) return dir;
|
|
13586
|
+
const parent = path76.dirname(dir);
|
|
13587
|
+
if (parent === dir) return path76.resolve(start);
|
|
13267
13588
|
dir = parent;
|
|
13268
13589
|
}
|
|
13269
13590
|
}
|
|
13270
13591
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
13271
|
-
return
|
|
13592
|
+
return path76.resolve(workspaceRoot, repoPath) === path76.resolve(workspaceRoot);
|
|
13272
13593
|
}
|
|
13273
13594
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
13274
13595
|
const apply = options.write ?? false;
|
|
13275
|
-
const configText = await readText(
|
|
13596
|
+
const configText = await readText(path76.join(workspaceRoot, WORKSPACE_FILE));
|
|
13276
13597
|
if (!configText) {
|
|
13277
13598
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
13278
13599
|
process.exitCode = 1;
|
|
@@ -13299,11 +13620,11 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13299
13620
|
const statuses = [];
|
|
13300
13621
|
const aggregateInputs = [];
|
|
13301
13622
|
for (const repo of repos) {
|
|
13302
|
-
const repoRoot =
|
|
13623
|
+
const repoRoot = path76.resolve(workspaceRoot, repo.path);
|
|
13303
13624
|
say(`
|
|
13304
13625
|
\u2192 ${repo.name} (${repo.path})`);
|
|
13305
13626
|
try {
|
|
13306
|
-
if (!
|
|
13627
|
+
if (!existsSync9(repoRoot) || !statSync(repoRoot).isDirectory()) {
|
|
13307
13628
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
13308
13629
|
}
|
|
13309
13630
|
const res = await runSetupCore(repoRoot, {
|
|
@@ -13378,7 +13699,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13378
13699
|
if (apply && !options.dryRun) {
|
|
13379
13700
|
const statusByName = new Map(statuses.map((s) => [s.name, s]));
|
|
13380
13701
|
const prior = await readManifest2(workspaceRoot);
|
|
13381
|
-
if (!prior &&
|
|
13702
|
+
if (!prior && existsSync9(manifestPath(workspaceRoot))) {
|
|
13382
13703
|
warn(
|
|
13383
13704
|
"Existing workspace.manifest.json is unreadable \u2014 prior per-repo state will not be carried forward."
|
|
13384
13705
|
);
|
|
@@ -13389,7 +13710,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13389
13710
|
const status = statusByName.get(repo.name);
|
|
13390
13711
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
13391
13712
|
if (status?.status === "ok") {
|
|
13392
|
-
const lock = await checkLock(
|
|
13713
|
+
const lock = await checkLock(path76.resolve(workspaceRoot, repo.path));
|
|
13393
13714
|
manifestRepos.push({
|
|
13394
13715
|
name: repo.name,
|
|
13395
13716
|
path: repo.path,
|
|
@@ -13445,7 +13766,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13445
13766
|
try {
|
|
13446
13767
|
const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
|
|
13447
13768
|
if (linkResult.ok) {
|
|
13448
|
-
written.push(...linkResult.added.map((rel) =>
|
|
13769
|
+
written.push(...linkResult.added.map((rel) => path76.join(workspaceRoot, rel)));
|
|
13449
13770
|
if (!options.json) {
|
|
13450
13771
|
say(
|
|
13451
13772
|
`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 +13797,8 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
13476
13797
|
}
|
|
13477
13798
|
|
|
13478
13799
|
// src/commands/workspace/undo.ts
|
|
13479
|
-
import { existsSync as
|
|
13480
|
-
import
|
|
13800
|
+
import { existsSync as existsSync10, statSync as statSync2 } from "node:fs";
|
|
13801
|
+
import path77 from "node:path";
|
|
13481
13802
|
import fs53 from "fs-extra";
|
|
13482
13803
|
init_fs();
|
|
13483
13804
|
init_logger();
|
|
@@ -13508,11 +13829,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13508
13829
|
}
|
|
13509
13830
|
if (options.dryRun) {
|
|
13510
13831
|
for (const repo of config.repos) {
|
|
13511
|
-
const repoRoot =
|
|
13832
|
+
const repoRoot = path77.resolve(workspaceRoot, repo.path);
|
|
13512
13833
|
log(`
|
|
13513
13834
|
\u2192 ${repo.name} (${repo.path})`);
|
|
13514
13835
|
try {
|
|
13515
|
-
if (!
|
|
13836
|
+
if (!existsSync10(repoRoot) || !statSync2(repoRoot).isDirectory()) {
|
|
13516
13837
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
13517
13838
|
}
|
|
13518
13839
|
await runUndo({ dryRun: true, root: repoRoot });
|
|
@@ -13540,11 +13861,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13540
13861
|
];
|
|
13541
13862
|
for (const abs of workspaceArtifacts) {
|
|
13542
13863
|
if (await fs53.pathExists(abs)) {
|
|
13543
|
-
log(`[dry-run] Would remove ${
|
|
13864
|
+
log(`[dry-run] Would remove ${path77.relative(workspaceRoot, abs)}`);
|
|
13544
13865
|
}
|
|
13545
13866
|
}
|
|
13546
13867
|
if (!await fs53.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
|
|
13547
|
-
const claudeMd = await readText(
|
|
13868
|
+
const claudeMd = await readText(path77.join(workspaceRoot, "CLAUDE.md"));
|
|
13548
13869
|
if (claudeMd?.includes(WORKSPACE_BLOCK_BEGIN) || claudeMd && hasLegacyWorkspaceBlock(claudeMd)) {
|
|
13549
13870
|
log("[dry-run] Would remove the workspace haus import block from CLAUDE.md");
|
|
13550
13871
|
}
|
|
@@ -13563,11 +13884,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13563
13884
|
}
|
|
13564
13885
|
let anyRepoFailed = false;
|
|
13565
13886
|
for (const repo of config.repos) {
|
|
13566
|
-
const repoRoot =
|
|
13887
|
+
const repoRoot = path77.resolve(workspaceRoot, repo.path);
|
|
13567
13888
|
log(`
|
|
13568
13889
|
\u2192 ${repo.name} (${repo.path})`);
|
|
13569
13890
|
try {
|
|
13570
|
-
if (!
|
|
13891
|
+
if (!existsSync10(repoRoot) || !statSync2(repoRoot).isDirectory()) {
|
|
13571
13892
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
13572
13893
|
}
|
|
13573
13894
|
await runUndo({ yes: true, root: repoRoot });
|
|
@@ -13592,21 +13913,21 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13592
13913
|
const abs = hausPath(workspaceRoot, rel);
|
|
13593
13914
|
if (await fs53.pathExists(abs)) {
|
|
13594
13915
|
await fs53.remove(abs);
|
|
13595
|
-
log(`Removed ${
|
|
13916
|
+
log(`Removed ${path77.relative(workspaceRoot, abs)}`);
|
|
13596
13917
|
}
|
|
13597
13918
|
}
|
|
13598
13919
|
const manifestFile = manifestPath(workspaceRoot);
|
|
13599
13920
|
if (await fs53.pathExists(manifestFile)) {
|
|
13600
13921
|
await fs53.remove(manifestFile);
|
|
13601
|
-
log(`Removed ${
|
|
13922
|
+
log(`Removed ${path77.relative(workspaceRoot, manifestFile)}`);
|
|
13602
13923
|
}
|
|
13603
13924
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
13604
13925
|
if (await fs53.pathExists(workspaceMdPath)) {
|
|
13605
13926
|
await fs53.remove(workspaceMdPath);
|
|
13606
|
-
log(`Removed ${
|
|
13927
|
+
log(`Removed ${path77.relative(workspaceRoot, workspaceMdPath)}`);
|
|
13607
13928
|
} else {
|
|
13608
|
-
const claudeMdPath =
|
|
13609
|
-
if (
|
|
13929
|
+
const claudeMdPath = path77.join(workspaceRoot, "CLAUDE.md");
|
|
13930
|
+
if (existsSync10(claudeMdPath)) {
|
|
13610
13931
|
const prev = await readText(claudeMdPath);
|
|
13611
13932
|
if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
|
|
13612
13933
|
const next = stripHausBlock(prev, {
|
|
@@ -13652,7 +13973,7 @@ function normalizeMaxDepth(maxDepth) {
|
|
|
13652
13973
|
return void 0;
|
|
13653
13974
|
}
|
|
13654
13975
|
async function initWorkspace(options = {}) {
|
|
13655
|
-
if (
|
|
13976
|
+
if (existsSync11(WORKSPACE_FILE) && !options.yes) {
|
|
13656
13977
|
const ok = await confirm(
|
|
13657
13978
|
`${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
13979
|
);
|
|
@@ -13674,7 +13995,7 @@ relationships: []
|
|
|
13674
13995
|
log("Workspace initialized.");
|
|
13675
13996
|
}
|
|
13676
13997
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
13677
|
-
const configText = await readText(
|
|
13998
|
+
const configText = await readText(path78.join(workspaceRoot, WORKSPACE_FILE));
|
|
13678
13999
|
if (!configText) {
|
|
13679
14000
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
13680
14001
|
process.exitCode = 1;
|
|
@@ -13696,9 +14017,9 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
13696
14017
|
const inputs = [];
|
|
13697
14018
|
const failures = [];
|
|
13698
14019
|
for (const repo of config.repos) {
|
|
13699
|
-
const repoRoot =
|
|
14020
|
+
const repoRoot = path78.resolve(workspaceRoot, repo.path);
|
|
13700
14021
|
try {
|
|
13701
|
-
if (!
|
|
14022
|
+
if (!existsSync11(repoRoot) || !statSync3(repoRoot).isDirectory()) {
|
|
13702
14023
|
throw new Error("path is not a directory");
|
|
13703
14024
|
}
|
|
13704
14025
|
const result = await scanProject(repoRoot);
|
|
@@ -13764,7 +14085,7 @@ init_logger();
|
|
|
13764
14085
|
init_paths();
|
|
13765
14086
|
function cliVersion() {
|
|
13766
14087
|
try {
|
|
13767
|
-
const pkgPath =
|
|
14088
|
+
const pkgPath = path79.join(packageRoot(), "package.json");
|
|
13768
14089
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
13769
14090
|
return pkg.version ?? "0.0.0";
|
|
13770
14091
|
} catch {
|
|
@@ -13774,7 +14095,7 @@ function cliVersion() {
|
|
|
13774
14095
|
var program = new Command();
|
|
13775
14096
|
function validateRuntimeNodeVersion() {
|
|
13776
14097
|
try {
|
|
13777
|
-
const pkgPath =
|
|
14098
|
+
const pkgPath = path79.join(packageRoot(), "package.json");
|
|
13778
14099
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
13779
14100
|
const requiredRange = pkg.engines?.node;
|
|
13780
14101
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -13835,7 +14156,7 @@ backups.command("prune").description("Delete old backups by age and/or count (re
|
|
|
13835
14156
|
})
|
|
13836
14157
|
);
|
|
13837
14158
|
program.command("explain-recommendation").description("Print a per-item breakdown of the current recommendation, with skip reasons").option("--json").action(runExplainRecommendation);
|
|
13838
|
-
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", "
|
|
14159
|
+
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(
|
|
13839
14160
|
"--force",
|
|
13840
14161
|
"Write recommendation.json/haus.lock.json even when zero catalog items matched at all"
|
|
13841
14162
|
).action(runInit);
|
|
@@ -13911,6 +14232,11 @@ worktree.command("list").description("List materialized workspace worktrees and
|
|
|
13911
14232
|
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
14233
|
(slug, opts) => runWorktree("remove", { slug, force: opts.force, dryRun: opts.dryRun, json: opts.json })
|
|
13913
14234
|
);
|
|
14235
|
+
worktree.command("prune").description(
|
|
14236
|
+
"Remove stale worktree registrations left by a workspace worktree deleted without `haus workspace worktree remove` (e.g. a natively deleted Claude Code worktree)"
|
|
14237
|
+
).option("--dry-run", "Preview which registrations would be pruned").option("--json", "Emit the structured prune result as JSON").action(
|
|
14238
|
+
(opts) => runWorktree("prune", { dryRun: opts.dryRun, json: opts.json })
|
|
14239
|
+
);
|
|
13914
14240
|
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
14241
|
program.parseAsync(process.argv).catch((err) => {
|
|
13916
14242
|
const message2 = err instanceof Error ? err.message : String(err);
|