@haus-tech/haus-workflow 5.10.1 → 5.11.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/dist/cli.js
CHANGED
|
@@ -337,13 +337,6 @@ var init_constants = __esm({
|
|
|
337
337
|
});
|
|
338
338
|
|
|
339
339
|
// src/utils/exec.ts
|
|
340
|
-
var exec_exports = {};
|
|
341
|
-
__export(exec_exports, {
|
|
342
|
-
assertNeverCommandProbe: () => assertNeverCommandProbe,
|
|
343
|
-
probeCommand: () => probeCommand,
|
|
344
|
-
runCommand: () => runCommand,
|
|
345
|
-
runGit: () => runGit
|
|
346
|
-
});
|
|
347
340
|
import { execa } from "execa";
|
|
348
341
|
async function runCommand(command, args = [], options = {}) {
|
|
349
342
|
try {
|
|
@@ -2965,6 +2958,14 @@ async function addProjectIncludedIds(root, ids) {
|
|
|
2965
2958
|
includedIds: [.../* @__PURE__ */ new Set([...normalizeIdList(existing.includedIds), ...toAdd])].sort()
|
|
2966
2959
|
}));
|
|
2967
2960
|
}
|
|
2961
|
+
async function addProjectDeclaredRoles(root, roles) {
|
|
2962
|
+
const toAdd = normalizeSignalList(roles.map((role) => role.replace(/^role=/i, "")));
|
|
2963
|
+
if (toAdd.length === 0) return;
|
|
2964
|
+
await updateProjectConfig(root, (existing) => ({
|
|
2965
|
+
...existing,
|
|
2966
|
+
declaredRoles: [.../* @__PURE__ */ new Set([...normalizeSignalList(existing.declaredRoles), ...toAdd])].sort()
|
|
2967
|
+
}));
|
|
2968
|
+
}
|
|
2968
2969
|
function withdrawIncludedIds(existing, ids) {
|
|
2969
2970
|
const drop = new Set(ids);
|
|
2970
2971
|
return normalizeIdList(existing.includedIds).filter((id) => !drop.has(id));
|
|
@@ -8704,7 +8705,7 @@ async function runBundleImport(file, options = {}) {
|
|
|
8704
8705
|
|
|
8705
8706
|
// src/commands/doctor.ts
|
|
8706
8707
|
import path50 from "node:path";
|
|
8707
|
-
import
|
|
8708
|
+
import fs43 from "fs-extra";
|
|
8708
8709
|
init_manifest_format();
|
|
8709
8710
|
|
|
8710
8711
|
// src/claude/former-install-orphans.ts
|
|
@@ -9044,14 +9045,14 @@ async function readScannedMarkdown(root) {
|
|
|
9044
9045
|
|
|
9045
9046
|
// src/decisions/doctor.ts
|
|
9046
9047
|
import path48 from "node:path";
|
|
9047
|
-
import
|
|
9048
|
+
import fs41 from "fs-extra";
|
|
9048
9049
|
init_fs();
|
|
9049
9050
|
|
|
9050
9051
|
// src/decisions/check.ts
|
|
9051
9052
|
init_fs();
|
|
9052
9053
|
init_paths();
|
|
9053
9054
|
import path47 from "node:path";
|
|
9054
|
-
import
|
|
9055
|
+
import fs40 from "fs-extra";
|
|
9055
9056
|
|
|
9056
9057
|
// src/decisions/dep-bump.ts
|
|
9057
9058
|
var DEP_SECTIONS = [
|
|
@@ -9112,6 +9113,7 @@ function isDependencyBumpOnly(beforeText, afterText) {
|
|
|
9112
9113
|
init_exec();
|
|
9113
9114
|
init_fs();
|
|
9114
9115
|
import path44 from "node:path";
|
|
9116
|
+
import fs37 from "fs-extra";
|
|
9115
9117
|
var NUL = String.fromCharCode(0);
|
|
9116
9118
|
function parseNumstat(output) {
|
|
9117
9119
|
const fields = output.split(NUL);
|
|
@@ -9138,6 +9140,27 @@ function parseNumstat(output) {
|
|
|
9138
9140
|
}
|
|
9139
9141
|
return { linesByFile, linesAdded, linesRemoved };
|
|
9140
9142
|
}
|
|
9143
|
+
function parseNameStatus(output) {
|
|
9144
|
+
const fields = output.split(NUL);
|
|
9145
|
+
const files = [];
|
|
9146
|
+
const presentFiles = [];
|
|
9147
|
+
const renames = [];
|
|
9148
|
+
for (let i = 0; i < fields.length; i++) {
|
|
9149
|
+
const status = fields[i];
|
|
9150
|
+
if (!status) continue;
|
|
9151
|
+
const step = /^[RC]/.test(status) ? 2 : 1;
|
|
9152
|
+
const from = fields[i + 1];
|
|
9153
|
+
const file = fields[i + step];
|
|
9154
|
+
i += step;
|
|
9155
|
+
if (!file) continue;
|
|
9156
|
+
files.push(file);
|
|
9157
|
+
if (status !== "D") presentFiles.push(file);
|
|
9158
|
+
if (status.startsWith("R") && from) {
|
|
9159
|
+
renames.push({ from, to: file, similarity: Number(status.slice(1)) });
|
|
9160
|
+
}
|
|
9161
|
+
}
|
|
9162
|
+
return { files, presentFiles, renames };
|
|
9163
|
+
}
|
|
9141
9164
|
function isPackageJsonPath(file) {
|
|
9142
9165
|
return file === "package.json" || file.endsWith("/package.json");
|
|
9143
9166
|
}
|
|
@@ -9169,6 +9192,10 @@ function toAncestryLogRange(range) {
|
|
|
9169
9192
|
if (!parsed) return `${range}..HEAD`;
|
|
9170
9193
|
return `${parsed.left}..${parsed.right}`;
|
|
9171
9194
|
}
|
|
9195
|
+
async function readCommitMessages(root, range) {
|
|
9196
|
+
const log2 = await runGit(["log", "-z", "--format=%B", toAncestryLogRange(range)], { cwd: root });
|
|
9197
|
+
return log2.stdout.split(NUL).filter((message2) => message2.trim().length > 0);
|
|
9198
|
+
}
|
|
9172
9199
|
async function getCurrentBranch(root) {
|
|
9173
9200
|
const result = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: root });
|
|
9174
9201
|
if (result.exitCode !== 0) return void 0;
|
|
@@ -9178,33 +9205,45 @@ async function getCurrentBranch(root) {
|
|
|
9178
9205
|
if (gha) return gha;
|
|
9179
9206
|
return void 0;
|
|
9180
9207
|
}
|
|
9181
|
-
async function
|
|
9182
|
-
|
|
9183
|
-
if (opts.staged) {
|
|
9184
|
-
const before2 = await readBlob(root, `HEAD:${file}`) ?? empty;
|
|
9185
|
-
const after2 = await readBlob(root, `:${file}`) ?? empty;
|
|
9186
|
-
return { before: before2, after: after2 };
|
|
9187
|
-
}
|
|
9208
|
+
async function diffBaseRev(root, opts) {
|
|
9209
|
+
if (opts.staged) return "HEAD";
|
|
9188
9210
|
const range = opts.range ?? "HEAD";
|
|
9189
9211
|
const parsed = parseDiffRange(range);
|
|
9190
|
-
if (parsed)
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
}
|
|
9200
|
-
const before = await readBlob(root, `${range}:${file}`) ?? empty;
|
|
9201
|
-
const after = await readText(path44.join(root, file)) ?? empty;
|
|
9212
|
+
if (!parsed) return range;
|
|
9213
|
+
if (!parsed.triple) return parsed.left;
|
|
9214
|
+
const mb = await runGit(["merge-base", parsed.left, parsed.right], { cwd: root });
|
|
9215
|
+
return mb.exitCode === 0 ? mb.stdout.trim() : parsed.left;
|
|
9216
|
+
}
|
|
9217
|
+
async function resolvePackageJsonPair(root, file, opts) {
|
|
9218
|
+
const empty = "{}";
|
|
9219
|
+
const before = await readBlob(root, `${await diffBaseRev(root, opts)}:${file}`) ?? empty;
|
|
9220
|
+
const after = await readDiffContent(root, file, opts) ?? empty;
|
|
9202
9221
|
return { before, after };
|
|
9203
9222
|
}
|
|
9223
|
+
async function existsAtDiffBase(root, relPath, opts) {
|
|
9224
|
+
const base = await diffBaseRev(root, opts);
|
|
9225
|
+
return (await runGit(["cat-file", "-e", `${base}:${relPath}`], { cwd: root })).exitCode === 0;
|
|
9226
|
+
}
|
|
9227
|
+
async function readDiffContent(root, file, opts) {
|
|
9228
|
+
if (opts.staged) return await readBlob(root, `:${file}`) ?? void 0;
|
|
9229
|
+
const parsed = parseDiffRange(opts.range ?? "HEAD");
|
|
9230
|
+
if (parsed) return await readBlob(root, `${parsed.right}:${file}`) ?? void 0;
|
|
9231
|
+
return readText(path44.join(root, file));
|
|
9232
|
+
}
|
|
9233
|
+
async function listDiffDirectory(root, dirRel, opts) {
|
|
9234
|
+
const prefix = `${dirRel}/`;
|
|
9235
|
+
const listDirect = async (args) => (await runGit(args, { cwd: root })).stdout.split(NUL).filter((rel) => rel.startsWith(prefix) && !rel.slice(prefix.length).includes("/"));
|
|
9236
|
+
if (opts.staged) return listDirect(["ls-files", "-z", "--", prefix]);
|
|
9237
|
+
const parsed = parseDiffRange(opts.range ?? "HEAD");
|
|
9238
|
+
if (parsed) return listDirect(["ls-tree", "-z", "--name-only", parsed.right, "--", prefix]);
|
|
9239
|
+
const dir = path44.join(root, dirRel);
|
|
9240
|
+
if (!await fs37.pathExists(dir)) return [];
|
|
9241
|
+
return (await fs37.readdir(dir)).map((name) => `${prefix}${name}`);
|
|
9242
|
+
}
|
|
9204
9243
|
async function collectDiffStats(root, opts) {
|
|
9205
|
-
const gitArgs = opts.staged ? ["diff", "--cached", "--name-
|
|
9244
|
+
const gitArgs = opts.staged ? ["diff", "--cached", "--name-status", "-z"] : ["diff", "--name-status", "-z", opts.range ?? "HEAD"];
|
|
9206
9245
|
const names = await runGit(gitArgs, { cwd: root });
|
|
9207
|
-
const files = names.stdout
|
|
9246
|
+
const { files, presentFiles, renames } = parseNameStatus(names.stdout);
|
|
9208
9247
|
const numstatArgs = opts.staged ? ["diff", "--cached", "--numstat", "-z"] : ["diff", "--numstat", "-z", opts.range ?? "HEAD"];
|
|
9209
9248
|
const numstat = await runGit(numstatArgs, { cwd: root });
|
|
9210
9249
|
const { linesByFile, linesAdded, linesRemoved } = parseNumstat(numstat.stdout);
|
|
@@ -9215,6 +9254,8 @@ async function collectDiffStats(root, opts) {
|
|
|
9215
9254
|
}
|
|
9216
9255
|
return {
|
|
9217
9256
|
files,
|
|
9257
|
+
presentFiles,
|
|
9258
|
+
renames,
|
|
9218
9259
|
linesAdded,
|
|
9219
9260
|
linesRemoved,
|
|
9220
9261
|
linesByFile,
|
|
@@ -9262,12 +9303,12 @@ function firstMatchingRegex(filePath, patterns) {
|
|
|
9262
9303
|
|
|
9263
9304
|
// src/decisions/paths.ts
|
|
9264
9305
|
import path45 from "node:path";
|
|
9265
|
-
import
|
|
9306
|
+
import fs38 from "fs-extra";
|
|
9266
9307
|
async function resolveDecisionsDir(root) {
|
|
9267
9308
|
const primary = path45.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
9268
|
-
if (await
|
|
9309
|
+
if (await fs38.pathExists(primary)) return primary;
|
|
9269
9310
|
const legacy = path45.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
9270
|
-
if (await
|
|
9311
|
+
if (await fs38.pathExists(legacy)) return legacy;
|
|
9271
9312
|
return primary;
|
|
9272
9313
|
}
|
|
9273
9314
|
function relativeDecisionPath(decisionsDir, root) {
|
|
@@ -9277,7 +9318,7 @@ function relativeDecisionPath(decisionsDir, root) {
|
|
|
9277
9318
|
// src/decisions/validate.ts
|
|
9278
9319
|
init_fs();
|
|
9279
9320
|
import path46 from "node:path";
|
|
9280
|
-
import
|
|
9321
|
+
import fs39 from "fs-extra";
|
|
9281
9322
|
function validateDecisionContent(content2) {
|
|
9282
9323
|
const errors = [];
|
|
9283
9324
|
for (const section of DECISIONS_TRIGGERS.requiredSections) {
|
|
@@ -9305,10 +9346,38 @@ function decisionNumberFromFile(name) {
|
|
|
9305
9346
|
const match = /^(\d{4})-/.exec(name);
|
|
9306
9347
|
return match?.[1] ?? null;
|
|
9307
9348
|
}
|
|
9308
|
-
|
|
9349
|
+
var ADR_HEADING_RE = /^#\s+ADR-\d{4}\b/;
|
|
9350
|
+
function firstHeading(content2) {
|
|
9351
|
+
const lines = content2.split(/\r?\n/);
|
|
9352
|
+
let start = 0;
|
|
9353
|
+
if (lines[0]?.trim() === "---") {
|
|
9354
|
+
const end = lines.findIndex((line2, i) => i > 0 && line2.trim() === "---");
|
|
9355
|
+
if (end > 0) start = end + 1;
|
|
9356
|
+
}
|
|
9357
|
+
let fence;
|
|
9358
|
+
for (const line2 of lines.slice(start)) {
|
|
9359
|
+
const marker = /^ {0,3}(`{3,}|~{3,})/.exec(line2)?.[1];
|
|
9360
|
+
if (marker != null) {
|
|
9361
|
+
if (fence == null) fence = marker;
|
|
9362
|
+
else if (marker[0] === fence[0] && marker.length >= fence.length) fence = void 0;
|
|
9363
|
+
continue;
|
|
9364
|
+
}
|
|
9365
|
+
if (fence == null && /^#{1,6}(\s|$)/.test(line2)) return line2;
|
|
9366
|
+
}
|
|
9367
|
+
return void 0;
|
|
9368
|
+
}
|
|
9369
|
+
function isAdrShaped(relPath, content2) {
|
|
9370
|
+
const segments = relPath.replace(/\\/g, "/").toLowerCase().split("/");
|
|
9371
|
+
const dirs = segments.slice(0, -1);
|
|
9372
|
+
if (!/\.md$/i.test(relPath) || dirs.includes("templates")) return false;
|
|
9373
|
+
if (dirs.includes("adr")) return true;
|
|
9374
|
+
const heading = firstHeading(content2);
|
|
9375
|
+
return heading != null && ADR_HEADING_RE.test(heading);
|
|
9376
|
+
}
|
|
9377
|
+
async function validateIndexSync(decisionsDir, decisionFiles, decisionsRel) {
|
|
9309
9378
|
const readmePath = path46.join(decisionsDir, "README.md");
|
|
9310
|
-
if (!await
|
|
9311
|
-
return [
|
|
9379
|
+
if (!await fs39.pathExists(readmePath)) {
|
|
9380
|
+
return [`${decisionsRel}/README.md is missing`];
|
|
9312
9381
|
}
|
|
9313
9382
|
const readme = await readText(readmePath) ?? "";
|
|
9314
9383
|
const errors = [];
|
|
@@ -9322,6 +9391,9 @@ async function validateIndexSync(decisionsDir, decisionFiles) {
|
|
|
9322
9391
|
}
|
|
9323
9392
|
|
|
9324
9393
|
// src/decisions/check.ts
|
|
9394
|
+
function noRecordFailure() {
|
|
9395
|
+
return { missingDecision: false, recordErrors: [], recordWayOut: "" };
|
|
9396
|
+
}
|
|
9325
9397
|
var MANIFEST_LOCK_BASENAMES = /* @__PURE__ */ new Set([
|
|
9326
9398
|
"package.json",
|
|
9327
9399
|
"yarn.lock",
|
|
@@ -9406,34 +9478,92 @@ async function readGateMode(root) {
|
|
|
9406
9478
|
const cfg = await readJson(hausPath(root, "adr-gate.json"));
|
|
9407
9479
|
return cfg?.mode === "off" ? "off" : "on";
|
|
9408
9480
|
}
|
|
9481
|
+
var SKIP_DIRECTIVE = "[adr-skip]";
|
|
9482
|
+
function textLines(text) {
|
|
9483
|
+
return text.split(/\r\n|\r|\n/);
|
|
9484
|
+
}
|
|
9409
9485
|
function hasSkipToken(text) {
|
|
9410
|
-
return text.
|
|
9486
|
+
return textLines(text).some((line2) => line2.trimStart().startsWith(SKIP_DIRECTIVE));
|
|
9411
9487
|
}
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
9488
|
+
function commitHasSkipDirective(message2) {
|
|
9489
|
+
return hasSkipToken(textLines(message2).slice(1).join("\n"));
|
|
9490
|
+
}
|
|
9491
|
+
async function migratedRecords(root, decisionsRel, renames, diff) {
|
|
9492
|
+
const legacyRel = DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr";
|
|
9493
|
+
if (decisionsRel === legacyRel) return /* @__PURE__ */ new Set();
|
|
9494
|
+
const moved = renames.filter(
|
|
9495
|
+
(r) => r.similarity === 100 && r.from.startsWith(`${legacyRel}/`) && r.to.startsWith(`${decisionsRel}/`)
|
|
9496
|
+
);
|
|
9497
|
+
if (moved.length === 0 || await existsAtDiffBase(root, decisionsRel, diff)) return /* @__PURE__ */ new Set();
|
|
9498
|
+
return new Set(moved.map((r) => r.to));
|
|
9499
|
+
}
|
|
9500
|
+
async function duplicateNumbers(input2, records) {
|
|
9501
|
+
if (records.length === 0) return [];
|
|
9502
|
+
const numberOf = (rel) => decisionNumberFromFile(path47.basename(rel));
|
|
9503
|
+
const listed = (await listDiffDirectory(input2.root, input2.decisionsRel, input2.diff)).filter(
|
|
9504
|
+
(rel) => isDecisionFileName(path47.basename(rel))
|
|
9417
9505
|
);
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9506
|
+
const messages = [];
|
|
9507
|
+
for (const num of new Set(records.map(numberOf))) {
|
|
9508
|
+
if (num == null) continue;
|
|
9509
|
+
const changed = records.filter((rel) => numberOf(rel) === num).sort();
|
|
9510
|
+
const others = listed.filter((rel) => numberOf(rel) === num && !changed.includes(rel)).sort();
|
|
9511
|
+
if (changed.length + others.length < 2) continue;
|
|
9512
|
+
messages.push(
|
|
9513
|
+
`duplicate ADR number ${num}: ${[...changed, ...others].join(", ")}; take the next free number (haus decisions next-number)`
|
|
9514
|
+
);
|
|
9421
9515
|
}
|
|
9516
|
+
return messages;
|
|
9517
|
+
}
|
|
9518
|
+
async function auditChangedRecords(input2) {
|
|
9519
|
+
const decisionsPrefix = `${input2.decisionsRel}/`;
|
|
9422
9520
|
const decisionsDir = path47.join(input2.root, input2.decisionsRel);
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
|
|
9521
|
+
const audit = {
|
|
9522
|
+
records: [],
|
|
9523
|
+
misplaced: [],
|
|
9524
|
+
failing: [],
|
|
9525
|
+
duplicates: [],
|
|
9526
|
+
errors: []
|
|
9527
|
+
};
|
|
9528
|
+
const indexErrors = /* @__PURE__ */ new Set();
|
|
9529
|
+
for (const rel of input2.changedFiles) {
|
|
9530
|
+
if (!/\.md$/i.test(rel)) continue;
|
|
9531
|
+
const inside = rel.startsWith(decisionsPrefix);
|
|
9532
|
+
if (inside && !isDecisionFileName(path47.basename(rel))) continue;
|
|
9533
|
+
const content2 = await readDiffContent(input2.root, rel, input2.diff);
|
|
9534
|
+
if (content2 === void 0) continue;
|
|
9535
|
+
if (!inside) {
|
|
9536
|
+
if (!isAdrShaped(rel, content2)) continue;
|
|
9537
|
+
audit.misplaced.push(rel);
|
|
9538
|
+
audit.errors.push(`no ADR outside ${decisionsPrefix}: ${rel}`);
|
|
9539
|
+
continue;
|
|
9540
|
+
}
|
|
9541
|
+
audit.records.push(rel);
|
|
9426
9542
|
const valid = validateDecisionContent(content2);
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
);
|
|
9433
|
-
errors.push(...indexErrors);
|
|
9434
|
-
return
|
|
9543
|
+
const index = await validateIndexSync(decisionsDir, [path47.basename(rel)], input2.decisionsRel);
|
|
9544
|
+
if (!valid.ok) audit.errors.push(...valid.errors.map((e) => `${rel}: ${e}`));
|
|
9545
|
+
for (const e of index) indexErrors.add(e);
|
|
9546
|
+
if (!valid.ok || index.length > 0) audit.failing.push(rel);
|
|
9547
|
+
}
|
|
9548
|
+
audit.duplicates = await duplicateNumbers(input2, audit.records);
|
|
9549
|
+
audit.errors.push(...indexErrors, ...audit.duplicates);
|
|
9550
|
+
return audit;
|
|
9551
|
+
}
|
|
9552
|
+
function waysOut(decisionsRel) {
|
|
9553
|
+
return `Write an ADR under ${decisionsRel}/ (haus decisions suggest), or put [adr-skip] on its own line in the PR body or a commit body for non-security changes.`;
|
|
9554
|
+
}
|
|
9555
|
+
function recordWayOut(decisionsRel, audit, answersTrigger) {
|
|
9556
|
+
if (audit.errors.length === 0) return "";
|
|
9557
|
+
const answers = answersTrigger ? "; a record moved there also answers the trigger" : "";
|
|
9558
|
+
const steps = [
|
|
9559
|
+
...audit.misplaced.length > 0 ? [
|
|
9560
|
+
`Move ${audit.misplaced.join(", ")} into ${decisionsRel}/ under the next free number (haus decisions next-number) with the house heading${answers}.`
|
|
9561
|
+
] : [],
|
|
9562
|
+
...audit.duplicates.map((duplicate) => `${duplicate}.`),
|
|
9563
|
+
...audit.failing.length > 0 ? [`Fix ${audit.failing.join(", ")} in place.`] : []
|
|
9564
|
+
];
|
|
9565
|
+
return `${steps.join(" ")} A record takes the house format: a \`# ADR-NNNN: Title\` heading, the sections ## Context, ## Decision and ## Consequences, and a row in ${decisionsRel}/README.md. Even [adr-skip] on its own line does not waive this.`;
|
|
9435
9566
|
}
|
|
9436
|
-
var WAYS_OUT = "Write an ADR under docs/decisions/ (haus decisions suggest), or use [adr-skip] for non-security changes.";
|
|
9437
9567
|
async function runDecisionsCheck(root, opts = {}) {
|
|
9438
9568
|
const reasons = [];
|
|
9439
9569
|
if (await readGateMode(root) === "off") {
|
|
@@ -9441,7 +9571,8 @@ async function runDecisionsCheck(root, opts = {}) {
|
|
|
9441
9571
|
triggered: false,
|
|
9442
9572
|
satisfied: true,
|
|
9443
9573
|
reasons: ["gate disabled via .haus-workflow/adr-gate.json"],
|
|
9444
|
-
securityTriggered: false
|
|
9574
|
+
securityTriggered: false,
|
|
9575
|
+
...noRecordFailure()
|
|
9445
9576
|
};
|
|
9446
9577
|
}
|
|
9447
9578
|
const stats = await collectDiffStats(root, opts);
|
|
@@ -9452,73 +9583,77 @@ async function runDecisionsCheck(root, opts = {}) {
|
|
|
9452
9583
|
triggered: false,
|
|
9453
9584
|
satisfied: true,
|
|
9454
9585
|
reasons: ["catalog-sync branch \u2014 exempt"],
|
|
9455
|
-
securityTriggered: false
|
|
9586
|
+
securityTriggered: false,
|
|
9587
|
+
...noRecordFailure()
|
|
9456
9588
|
};
|
|
9457
9589
|
}
|
|
9590
|
+
const decisionsDir = await resolveDecisionsDir(root);
|
|
9591
|
+
const decisionsRel = relativeDecisionPath(decisionsDir, root);
|
|
9592
|
+
const diff = { staged: opts.staged, range: opts.range };
|
|
9593
|
+
const migrated = await migratedRecords(root, decisionsRel, stats.renames ?? [], diff);
|
|
9594
|
+
const changed = await auditChangedRecords({
|
|
9595
|
+
root,
|
|
9596
|
+
decisionsRel,
|
|
9597
|
+
changedFiles: (stats.presentFiles ?? stats.files).filter((rel) => !migrated.has(rel)),
|
|
9598
|
+
diff
|
|
9599
|
+
});
|
|
9458
9600
|
const triggerReasons = explainDecisionTrigger(stats);
|
|
9459
|
-
const triggered = triggerReasons.length > 0;
|
|
9601
|
+
const triggered = triggerReasons.length > 0 || changed.errors.length > 0;
|
|
9460
9602
|
if (!triggered) {
|
|
9461
9603
|
return {
|
|
9462
9604
|
triggered: false,
|
|
9463
9605
|
satisfied: true,
|
|
9464
9606
|
reasons: ["no decision-worthy changes"],
|
|
9465
|
-
securityTriggered
|
|
9607
|
+
securityTriggered,
|
|
9608
|
+
...noRecordFailure()
|
|
9466
9609
|
};
|
|
9467
9610
|
}
|
|
9468
9611
|
reasons.push(...triggerReasons);
|
|
9469
|
-
const
|
|
9470
|
-
|
|
9471
|
-
if (
|
|
9472
|
-
reasons.push("[adr-skip]
|
|
9473
|
-
return { triggered: true, satisfied: true, reasons, securityTriggered };
|
|
9474
|
-
}
|
|
9475
|
-
if (
|
|
9476
|
-
reasons.push(
|
|
9612
|
+
const skip = hasSkipToken(opts.prBody ?? "") || (opts.commitMessages ?? []).some(commitHasSkipDirective);
|
|
9613
|
+
const waived = skip && !securityTriggered;
|
|
9614
|
+
if (waived && changed.errors.length === 0) {
|
|
9615
|
+
reasons.push("gate waived by [adr-skip] on its own line (non-security change)");
|
|
9616
|
+
return { triggered: true, satisfied: true, reasons, securityTriggered, ...noRecordFailure() };
|
|
9617
|
+
}
|
|
9618
|
+
if (waived) {
|
|
9619
|
+
reasons.push(
|
|
9620
|
+
"the trigger rules are waived by [adr-skip] on its own line, but a failing record is not"
|
|
9621
|
+
);
|
|
9477
9622
|
}
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
const legacyRel = DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr";
|
|
9481
|
-
let satisfactionErrors = await checkSatisfaction({
|
|
9482
|
-
root,
|
|
9483
|
-
decisionsRel,
|
|
9484
|
-
changedFiles: stats.files
|
|
9485
|
-
});
|
|
9486
|
-
if (satisfactionErrors.length > 0 && decisionsRel !== legacyRel) {
|
|
9487
|
-
const legacyExists = await fs39.pathExists(path47.join(root, legacyRel));
|
|
9488
|
-
if (legacyExists && !await fs39.pathExists(decisionsDir)) {
|
|
9489
|
-
satisfactionErrors = await checkSatisfaction({
|
|
9490
|
-
root,
|
|
9491
|
-
decisionsRel: legacyRel,
|
|
9492
|
-
changedFiles: stats.files
|
|
9493
|
-
});
|
|
9494
|
-
}
|
|
9623
|
+
if (skip && securityTriggered) {
|
|
9624
|
+
reasons.push("security/auth path changes ignore [adr-skip], even on its own line");
|
|
9495
9625
|
}
|
|
9496
|
-
|
|
9626
|
+
const missingDecision = !waived && triggerReasons.length > 0 && changed.records.length === 0;
|
|
9627
|
+
const wayOut = recordWayOut(decisionsRel, changed, missingDecision);
|
|
9628
|
+
const recordFields = { missingDecision, recordErrors: changed.errors, recordWayOut: wayOut };
|
|
9629
|
+
if (!missingDecision && changed.errors.length === 0) {
|
|
9497
9630
|
reasons.push("decision record present");
|
|
9498
|
-
return {
|
|
9499
|
-
triggered: true,
|
|
9500
|
-
satisfied: true,
|
|
9501
|
-
reasons,
|
|
9502
|
-
securityTriggered
|
|
9503
|
-
};
|
|
9631
|
+
return { triggered: true, satisfied: true, reasons, securityTriggered, ...recordFields };
|
|
9504
9632
|
}
|
|
9505
|
-
reasons.push(
|
|
9506
|
-
reasons.push(
|
|
9507
|
-
|
|
9633
|
+
if (missingDecision) reasons.push(`no new or updated decision under ${decisionsRel}/`);
|
|
9634
|
+
reasons.push(...changed.errors);
|
|
9635
|
+
if (missingDecision && changed.errors.length === 0) reasons.push(waysOut(decisionsRel));
|
|
9636
|
+
if (wayOut) reasons.push(wayOut);
|
|
9637
|
+
return { triggered: true, satisfied: false, reasons, securityTriggered, ...recordFields };
|
|
9508
9638
|
}
|
|
9509
9639
|
async function listDecisionNumbers(decisionsDir) {
|
|
9510
|
-
if (!await
|
|
9511
|
-
const entries = await
|
|
9640
|
+
if (!await fs40.pathExists(decisionsDir)) return [];
|
|
9641
|
+
const entries = await fs40.readdir(decisionsDir);
|
|
9512
9642
|
return entries.map((name) => decisionNumberFromFile(name)).filter((n) => n != null).map((n) => Number(n));
|
|
9513
9643
|
}
|
|
9514
9644
|
|
|
9515
9645
|
// src/decisions/doctor.ts
|
|
9646
|
+
async function countMarkdownFiles(dir) {
|
|
9647
|
+
if (!(await fs41.stat(dir)).isDirectory()) return 0;
|
|
9648
|
+
const entries = await fs41.readdir(dir, { recursive: true, withFileTypes: true });
|
|
9649
|
+
return entries.filter((entry) => entry.isFile() && /\.md$/i.test(entry.name)).length;
|
|
9650
|
+
}
|
|
9516
9651
|
async function auditDecisionsLayout(root, profile) {
|
|
9517
9652
|
const findings = [];
|
|
9518
9653
|
const primary = path48.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
9519
9654
|
const legacy = path48.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
9520
|
-
const hasPrimary = await
|
|
9521
|
-
const hasLegacy = await
|
|
9655
|
+
const hasPrimary = await fs41.pathExists(primary);
|
|
9656
|
+
const hasLegacy = await fs41.pathExists(legacy);
|
|
9522
9657
|
if (!hasPrimary && hasLegacy) {
|
|
9523
9658
|
if (PROFILES[profile].writesDecisionsSeed) {
|
|
9524
9659
|
findings.push({
|
|
@@ -9531,10 +9666,20 @@ async function auditDecisionsLayout(root, profile) {
|
|
|
9531
9666
|
return findings;
|
|
9532
9667
|
}
|
|
9533
9668
|
if (!hasPrimary) return findings;
|
|
9669
|
+
if (hasLegacy) {
|
|
9670
|
+
const legacyRel = DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr";
|
|
9671
|
+
const primaryRel = DECISIONS_TRIGGERS.decisionsDir;
|
|
9672
|
+
findings.push({
|
|
9673
|
+
level: "advisory",
|
|
9674
|
+
line: `- DECISIONS: a second ADR home exists at ${legacyRel}/ (${await countMarkdownFiles(legacy)} file(s)); records there are invisible to the gate`,
|
|
9675
|
+
sentence: `Decision records in ${legacyRel}/ never count: the decisions gate accepts records only in ${primaryRel}/, and a record moved there must pass haus decisions validate and take a free number`,
|
|
9676
|
+
fix: `move each file to ${primaryRel}/ under the next free number (haus decisions next-number), with the # ADR-NNNN: Title heading and a README row`
|
|
9677
|
+
});
|
|
9678
|
+
}
|
|
9534
9679
|
const decisionsDir = await resolveDecisionsDir(root);
|
|
9535
9680
|
const rel = relativeDecisionPath(decisionsDir, root);
|
|
9536
9681
|
const readmePath = path48.join(decisionsDir, "README.md");
|
|
9537
|
-
if (!await
|
|
9682
|
+
if (!await fs41.pathExists(readmePath)) {
|
|
9538
9683
|
findings.push({
|
|
9539
9684
|
level: "advisory",
|
|
9540
9685
|
line: `- DECISIONS: missing ${rel}/README.md index`,
|
|
@@ -9544,7 +9689,7 @@ async function auditDecisionsLayout(root, profile) {
|
|
|
9544
9689
|
return findings;
|
|
9545
9690
|
}
|
|
9546
9691
|
const readme = await readText(readmePath);
|
|
9547
|
-
const entries = (await
|
|
9692
|
+
const entries = (await fs41.readdir(decisionsDir)).filter(
|
|
9548
9693
|
(f) => f.endsWith(".md") && f !== "README.md"
|
|
9549
9694
|
);
|
|
9550
9695
|
for (const file of entries) {
|
|
@@ -9593,7 +9738,7 @@ init_fs();
|
|
|
9593
9738
|
init_logger();
|
|
9594
9739
|
init_paths();
|
|
9595
9740
|
import path49 from "node:path";
|
|
9596
|
-
import
|
|
9741
|
+
import fs42 from "fs-extra";
|
|
9597
9742
|
function manifestPathIn(configDir) {
|
|
9598
9743
|
return path49.join(configDir, "haus", "install-manifest.json");
|
|
9599
9744
|
}
|
|
@@ -9623,9 +9768,9 @@ async function pruneVacatedDirs(root, vacatedFiles) {
|
|
|
9623
9768
|
async function destinationWritable(dir) {
|
|
9624
9769
|
let candidate = path49.resolve(dir);
|
|
9625
9770
|
for (; ; ) {
|
|
9626
|
-
if (await
|
|
9771
|
+
if (await fs42.pathExists(candidate)) {
|
|
9627
9772
|
try {
|
|
9628
|
-
await
|
|
9773
|
+
await fs42.access(candidate, fs42.constants.W_OK);
|
|
9629
9774
|
return true;
|
|
9630
9775
|
} catch (err) {
|
|
9631
9776
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -9657,7 +9802,7 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
9657
9802
|
if (!legacy || !Array.isArray(legacy.files)) {
|
|
9658
9803
|
return { ...base, reason: "no install manifest under ~/.claude" };
|
|
9659
9804
|
}
|
|
9660
|
-
if (await
|
|
9805
|
+
if (await fs42.pathExists(manifestPathIn(to))) {
|
|
9661
9806
|
return { ...base, reason: "an install manifest already exists in the resolved config dir" };
|
|
9662
9807
|
}
|
|
9663
9808
|
const writable = await destinationWritable(to);
|
|
@@ -9680,14 +9825,14 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
9680
9825
|
continue;
|
|
9681
9826
|
}
|
|
9682
9827
|
const newDest = path49.join(to, rel);
|
|
9683
|
-
if (!await
|
|
9828
|
+
if (!await fs42.pathExists(entry.destPath)) {
|
|
9684
9829
|
rewritten.push({ ...entry, destPath: newDest });
|
|
9685
9830
|
continue;
|
|
9686
9831
|
}
|
|
9687
|
-
if (await
|
|
9832
|
+
if (await fs42.pathExists(newDest)) {
|
|
9688
9833
|
if (await sameContent(entry.destPath, newDest)) {
|
|
9689
9834
|
if (!dryRun) {
|
|
9690
|
-
await
|
|
9835
|
+
await fs42.remove(entry.destPath);
|
|
9691
9836
|
vacated.push(entry.destPath);
|
|
9692
9837
|
}
|
|
9693
9838
|
moved.push(newDest);
|
|
@@ -9704,7 +9849,7 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
9704
9849
|
continue;
|
|
9705
9850
|
}
|
|
9706
9851
|
try {
|
|
9707
|
-
await
|
|
9852
|
+
await fs42.move(entry.destPath, newDest);
|
|
9708
9853
|
vacated.push(entry.destPath);
|
|
9709
9854
|
moved.push(newDest);
|
|
9710
9855
|
rewritten.push({ ...entry, destPath: newDest });
|
|
@@ -9729,7 +9874,7 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
9729
9874
|
}
|
|
9730
9875
|
try {
|
|
9731
9876
|
await writeJson(manifestPathIn(to), { ...legacy, files: rewritten });
|
|
9732
|
-
await
|
|
9877
|
+
await fs42.remove(legacyManifestPath);
|
|
9733
9878
|
vacated.push(legacyManifestPath);
|
|
9734
9879
|
} catch (err) {
|
|
9735
9880
|
return {
|
|
@@ -10027,7 +10172,7 @@ async function runDoctor(options) {
|
|
|
10027
10172
|
const resolved = path50.resolve(root, target);
|
|
10028
10173
|
const relFromRoot = path50.relative(root, resolved);
|
|
10029
10174
|
const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${path50.sep}`) || path50.isAbsolute(relFromRoot);
|
|
10030
|
-
if (escapesRoot || !await
|
|
10175
|
+
if (escapesRoot || !await fs43.pathExists(resolved)) {
|
|
10031
10176
|
flag(
|
|
10032
10177
|
`- CLAUDE.md import: @${target} does not resolve (run \`haus apply --write\`)`,
|
|
10033
10178
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -10045,7 +10190,7 @@ async function runDoctor(options) {
|
|
|
10045
10190
|
);
|
|
10046
10191
|
}
|
|
10047
10192
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
10048
|
-
const workflowExists = await
|
|
10193
|
+
const workflowExists = await fs43.pathExists(workflowPath);
|
|
10049
10194
|
if (!workflowExists) {
|
|
10050
10195
|
flag(
|
|
10051
10196
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -10068,7 +10213,7 @@ async function runDoctor(options) {
|
|
|
10068
10213
|
"templates",
|
|
10069
10214
|
path50.basename(PROFILES[profile].workflowTemplateRel)
|
|
10070
10215
|
);
|
|
10071
|
-
const templatePath = await
|
|
10216
|
+
const templatePath = await fs43.pathExists(cachePath) ? cachePath : bundledPath;
|
|
10072
10217
|
const templateContent = await readText(templatePath);
|
|
10073
10218
|
const verdict = checkManagedTamper(bodyContent, managedHeader.hash, templateContent ?? null);
|
|
10074
10219
|
if (verdict.status === "tampered" || verdict.status === "legacy-diverged") {
|
|
@@ -10096,7 +10241,7 @@ async function runDoctor(options) {
|
|
|
10096
10241
|
}
|
|
10097
10242
|
}
|
|
10098
10243
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
10099
|
-
const workflowConfigExists = await
|
|
10244
|
+
const workflowConfigExists = await fs43.pathExists(workflowConfigPath);
|
|
10100
10245
|
if (!workflowConfigExists) {
|
|
10101
10246
|
flag(
|
|
10102
10247
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -10104,7 +10249,7 @@ async function runDoctor(options) {
|
|
|
10104
10249
|
"haus apply --write"
|
|
10105
10250
|
);
|
|
10106
10251
|
} else {
|
|
10107
|
-
const cfg = await
|
|
10252
|
+
const cfg = await fs43.readFile(workflowConfigPath, "utf8");
|
|
10108
10253
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
10109
10254
|
if (unfilled > 0) {
|
|
10110
10255
|
suggest(
|
|
@@ -10116,11 +10261,11 @@ async function runDoctor(options) {
|
|
|
10116
10261
|
ok("- .haus-workflow/workflow-config.md: OK (project-owned)");
|
|
10117
10262
|
}
|
|
10118
10263
|
}
|
|
10119
|
-
const docsSkillInstalled = await
|
|
10264
|
+
const docsSkillInstalled = await fs43.pathExists(
|
|
10120
10265
|
path50.join(root, ".claude", "skills", "writing-documentation", "SKILL.md")
|
|
10121
10266
|
);
|
|
10122
10267
|
if (docsSkillInstalled) {
|
|
10123
|
-
if (await
|
|
10268
|
+
if (await fs43.pathExists(path50.join(root, "docs", "SUMMARY.md"))) {
|
|
10124
10269
|
ok("- PROJECT DOCS: OK (docs/SUMMARY.md present)");
|
|
10125
10270
|
} else if (PROFILES[profile].runsDocsPass) {
|
|
10126
10271
|
suggest(
|
|
@@ -10449,7 +10594,7 @@ async function runDoctor(options) {
|
|
|
10449
10594
|
suggest(
|
|
10450
10595
|
`- ELIGIBILITY DRIFT: ${drifted.length} installed item(s) are no longer recommended by the current signals (${drifted.slice(0, 3).join(", ")}${drifted.length > 3 ? ", \u2026" : ""})`,
|
|
10451
10596
|
`${drifted.length} installed item(s) no longer match anything haus can detect in this repo \u2014 often a role that only \`deep-context.json\` knew about, which is not tracked in git`,
|
|
10452
|
-
"declare the role or stack in .haus-workflow/haus.config.json (
|
|
10597
|
+
"declare the role with `haus recommend --declare-role <role>` or the stack in .haus-workflow/haus.config.json (declaredStacks), re-run the docs pass to regenerate deep-context.json, or `haus remove <id>` if the item is genuinely no longer wanted"
|
|
10453
10598
|
);
|
|
10454
10599
|
}
|
|
10455
10600
|
}
|
|
@@ -10684,7 +10829,7 @@ async function runDoctor(options) {
|
|
|
10684
10829
|
let onDisk = false;
|
|
10685
10830
|
for (const rel of row.paths ?? []) {
|
|
10686
10831
|
const abs = resolveContainedPath(root, rel);
|
|
10687
|
-
if (abs !== null && await
|
|
10832
|
+
if (abs !== null && await fs43.pathExists(abs)) {
|
|
10688
10833
|
onDisk = true;
|
|
10689
10834
|
break;
|
|
10690
10835
|
}
|
|
@@ -10766,11 +10911,11 @@ init_github_rate_limit();
|
|
|
10766
10911
|
init_private_catalog_access();
|
|
10767
10912
|
|
|
10768
10913
|
// src/claude/refresh-project.ts
|
|
10769
|
-
import
|
|
10914
|
+
import fs44 from "fs-extra";
|
|
10770
10915
|
init_fs();
|
|
10771
10916
|
init_paths();
|
|
10772
10917
|
async function isHausProject(root) {
|
|
10773
|
-
if (await
|
|
10918
|
+
if (await fs44.pathExists(hausPath(root, "recommendation.json"))) return true;
|
|
10774
10919
|
const settings = await readJsonDetailed(claudePath(root, "settings.json"));
|
|
10775
10920
|
if (settings.status === "ok" && settings.value._haus != null) return true;
|
|
10776
10921
|
const lock = await readJsonDetailed(hausPath(root, "haus.lock.json"));
|
|
@@ -10792,7 +10937,7 @@ async function refreshProjectApply(root) {
|
|
|
10792
10937
|
|
|
10793
10938
|
// src/install/apply.ts
|
|
10794
10939
|
import path52 from "node:path";
|
|
10795
|
-
import
|
|
10940
|
+
import fs46 from "fs-extra";
|
|
10796
10941
|
init_fs();
|
|
10797
10942
|
init_logger();
|
|
10798
10943
|
init_paths();
|
|
@@ -10902,7 +11047,7 @@ ${content2}`;
|
|
|
10902
11047
|
// src/install/internal-channel.ts
|
|
10903
11048
|
init_fs();
|
|
10904
11049
|
import path51 from "node:path";
|
|
10905
|
-
import
|
|
11050
|
+
import fs45 from "fs-extra";
|
|
10906
11051
|
var INSTALL_CONFIG_SCHEMA = "haus-install-config/1";
|
|
10907
11052
|
var INTERNAL_NAME_PREFIXES = ["request", "slack-cowork"];
|
|
10908
11053
|
function internalPrefixMatch(name) {
|
|
@@ -10936,7 +11081,7 @@ async function writeInstallConfig(config) {
|
|
|
10936
11081
|
}
|
|
10937
11082
|
async function removeInstallConfig() {
|
|
10938
11083
|
const p = installConfigPath();
|
|
10939
|
-
if (
|
|
11084
|
+
if (fs45.pathExistsSync(p)) await fs45.remove(p);
|
|
10940
11085
|
}
|
|
10941
11086
|
function envOverride() {
|
|
10942
11087
|
const raw = process.env.HAUS_INTERNAL_CHANNEL?.trim().toLowerCase();
|
|
@@ -10966,8 +11111,8 @@ async function pruneEmptyDirsUpward(dirs, stopAt) {
|
|
|
10966
11111
|
let dir = start;
|
|
10967
11112
|
while (dir.startsWith(stopAt) && dir !== stopAt) {
|
|
10968
11113
|
try {
|
|
10969
|
-
if ((await
|
|
10970
|
-
await
|
|
11114
|
+
if ((await fs46.readdir(dir)).length > 0) break;
|
|
11115
|
+
await fs46.remove(dir);
|
|
10971
11116
|
} catch {
|
|
10972
11117
|
break;
|
|
10973
11118
|
}
|
|
@@ -10980,7 +11125,7 @@ function hashContent(content2) {
|
|
|
10980
11125
|
}
|
|
10981
11126
|
function destFileMissing(destPath) {
|
|
10982
11127
|
try {
|
|
10983
|
-
return !
|
|
11128
|
+
return !fs46.statSync(destPath).isFile();
|
|
10984
11129
|
} catch {
|
|
10985
11130
|
return true;
|
|
10986
11131
|
}
|
|
@@ -10988,7 +11133,7 @@ function destFileMissing(destPath) {
|
|
|
10988
11133
|
function sourceVersion() {
|
|
10989
11134
|
try {
|
|
10990
11135
|
const pkgPath = path52.join(packageRoot(), "package.json");
|
|
10991
|
-
const pkg = JSON.parse(
|
|
11136
|
+
const pkg = JSON.parse(fs46.readFileSync(pkgPath, "utf8"));
|
|
10992
11137
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
10993
11138
|
} catch {
|
|
10994
11139
|
return "haus@0.0.0";
|
|
@@ -11000,10 +11145,10 @@ function globalSrcDir() {
|
|
|
11000
11145
|
function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
11001
11146
|
const skillDir = path52.join(skillsDir, skillName);
|
|
11002
11147
|
const entries = [];
|
|
11003
|
-
for (const rel of
|
|
11148
|
+
for (const rel of fs46.readdirSync(skillDir, { recursive: true })) {
|
|
11004
11149
|
if (rel === "SKILL.md") continue;
|
|
11005
11150
|
const abs = path52.join(skillDir, rel);
|
|
11006
|
-
if (!
|
|
11151
|
+
if (!fs46.statSync(abs).isFile()) continue;
|
|
11007
11152
|
const posixRel = rel.split(path52.sep).join("/");
|
|
11008
11153
|
entries.push({
|
|
11009
11154
|
stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
|
|
@@ -11016,8 +11161,8 @@ function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
|
11016
11161
|
function collectFlatChannel(srcDir, claudeDir, dirName, idPrefix) {
|
|
11017
11162
|
const entries = [];
|
|
11018
11163
|
const channelDir = path52.join(srcDir, dirName);
|
|
11019
|
-
if (!
|
|
11020
|
-
for (const fileName of
|
|
11164
|
+
if (!fs46.pathExistsSync(channelDir)) return entries;
|
|
11165
|
+
for (const fileName of fs46.readdirSync(channelDir)) {
|
|
11021
11166
|
if (!fileName.endsWith(".md")) continue;
|
|
11022
11167
|
const name = fileName.slice(0, -".md".length);
|
|
11023
11168
|
entries.push({
|
|
@@ -11031,10 +11176,10 @@ function collectFlatChannel(srcDir, claudeDir, dirName, idPrefix) {
|
|
|
11031
11176
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
11032
11177
|
const entries = [];
|
|
11033
11178
|
const skillsDir = path52.join(srcDir, "skills");
|
|
11034
|
-
if (
|
|
11035
|
-
for (const skillName of
|
|
11179
|
+
if (fs46.pathExistsSync(skillsDir)) {
|
|
11180
|
+
for (const skillName of fs46.readdirSync(skillsDir)) {
|
|
11036
11181
|
const skillFile = path52.join(skillsDir, skillName, "SKILL.md");
|
|
11037
|
-
if (
|
|
11182
|
+
if (fs46.pathExistsSync(skillFile)) {
|
|
11038
11183
|
entries.push({
|
|
11039
11184
|
stableId: `skill.${skillName}`,
|
|
11040
11185
|
srcRelPath: path52.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
@@ -11107,7 +11252,7 @@ async function applyInstall(options = {}) {
|
|
|
11107
11252
|
}
|
|
11108
11253
|
continue;
|
|
11109
11254
|
}
|
|
11110
|
-
const destExists =
|
|
11255
|
+
const destExists = fs46.pathExistsSync(entry.destPath);
|
|
11111
11256
|
if (destExists) {
|
|
11112
11257
|
const currentContent = await readText(entry.destPath);
|
|
11113
11258
|
if (currentContent !== void 0) {
|
|
@@ -11167,13 +11312,13 @@ async function applyInstall(options = {}) {
|
|
|
11167
11312
|
manifestFiles.push(entry);
|
|
11168
11313
|
continue;
|
|
11169
11314
|
}
|
|
11170
|
-
if (!
|
|
11315
|
+
if (!fs46.pathExistsSync(entry.destPath)) continue;
|
|
11171
11316
|
const content2 = await readText(entry.destPath);
|
|
11172
11317
|
if (!content2) continue;
|
|
11173
11318
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
11174
11319
|
const currentHash = hashContent(content2);
|
|
11175
11320
|
if (hasHeader && currentHash === entry.hash) {
|
|
11176
|
-
if (!dryRun) await
|
|
11321
|
+
if (!dryRun) await fs46.remove(entry.destPath);
|
|
11177
11322
|
emptiedDirs.add(path52.dirname(entry.destPath));
|
|
11178
11323
|
result.deleted.push(entry.destPath);
|
|
11179
11324
|
} else {
|
|
@@ -11857,7 +12002,7 @@ function isCatalogItemInstallable(item) {
|
|
|
11857
12002
|
// src/install/scaffold.ts
|
|
11858
12003
|
init_logger();
|
|
11859
12004
|
import path56 from "node:path";
|
|
11860
|
-
import
|
|
12005
|
+
import fs47 from "fs-extra";
|
|
11861
12006
|
async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
11862
12007
|
const scaffolded = [];
|
|
11863
12008
|
const skipped = [];
|
|
@@ -11870,7 +12015,7 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
11870
12015
|
skipped.push(name);
|
|
11871
12016
|
if (outcome === "skipped-exists") skippedExisting.push(name);
|
|
11872
12017
|
};
|
|
11873
|
-
const realRoot = await
|
|
12018
|
+
const realRoot = await fs47.realpath(catalogRoot).catch(() => null);
|
|
11874
12019
|
for (const item of items) {
|
|
11875
12020
|
if (item.type !== "config") continue;
|
|
11876
12021
|
const sourcePath = path56.resolve(catalogRoot, item.path);
|
|
@@ -11880,7 +12025,7 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
11880
12025
|
warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
|
|
11881
12026
|
continue;
|
|
11882
12027
|
}
|
|
11883
|
-
const realSource = await
|
|
12028
|
+
const realSource = await fs47.realpath(sourcePath).catch(() => null);
|
|
11884
12029
|
if (!realRoot || !realSource) {
|
|
11885
12030
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
11886
12031
|
continue;
|
|
@@ -11890,7 +12035,7 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
11890
12035
|
warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
|
|
11891
12036
|
continue;
|
|
11892
12037
|
}
|
|
11893
|
-
const stat = await
|
|
12038
|
+
const stat = await fs47.lstat(sourcePath).catch(() => null);
|
|
11894
12039
|
if (!stat) {
|
|
11895
12040
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
11896
12041
|
continue;
|
|
@@ -11904,7 +12049,7 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
11904
12049
|
const dest = path56.join(projectRoot, filename);
|
|
11905
12050
|
record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
|
|
11906
12051
|
} else if (stat.isDirectory()) {
|
|
11907
|
-
const entries = await
|
|
12052
|
+
const entries = await fs47.readdir(sourcePath);
|
|
11908
12053
|
for (const entry of entries) {
|
|
11909
12054
|
const src = path56.join(sourcePath, entry);
|
|
11910
12055
|
const dest = path56.join(projectRoot, entry);
|
|
@@ -11916,7 +12061,7 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
11916
12061
|
}
|
|
11917
12062
|
async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
11918
12063
|
const destDisplay = path56.relative(projectRoot, dest) || path56.basename(dest);
|
|
11919
|
-
const srcStat = await
|
|
12064
|
+
const srcStat = await fs47.lstat(src).catch(() => null);
|
|
11920
12065
|
if (!srcStat) {
|
|
11921
12066
|
warn(`Skipping ${path56.basename(src)}: source not found`);
|
|
11922
12067
|
return "skipped-refused";
|
|
@@ -11925,7 +12070,7 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
|
11925
12070
|
warn(`Skipping ${path56.basename(src)}: source is a symlink`);
|
|
11926
12071
|
return "skipped-refused";
|
|
11927
12072
|
}
|
|
11928
|
-
const exists2 = await
|
|
12073
|
+
const exists2 = await fs47.pathExists(dest);
|
|
11929
12074
|
if (exists2 && !opts.force) {
|
|
11930
12075
|
warn(`Skipping ${destDisplay}: already exists \u2014 re-run with --force to overwrite it`);
|
|
11931
12076
|
return "skipped-exists";
|
|
@@ -11935,16 +12080,16 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
|
11935
12080
|
return "scaffolded";
|
|
11936
12081
|
}
|
|
11937
12082
|
if (exists2 && opts.force) {
|
|
11938
|
-
await
|
|
12083
|
+
await fs47.remove(dest);
|
|
11939
12084
|
}
|
|
11940
|
-
await
|
|
11941
|
-
await
|
|
12085
|
+
await fs47.ensureDir(path56.dirname(dest));
|
|
12086
|
+
await fs47.copy(src, dest, {
|
|
11942
12087
|
overwrite: true,
|
|
11943
12088
|
dereference: false,
|
|
11944
12089
|
// A vanished/unreadable entry mid-walk must skip that path, not crash the copy.
|
|
11945
12090
|
filter: (s) => {
|
|
11946
12091
|
try {
|
|
11947
|
-
return !
|
|
12092
|
+
return !fs47.lstatSync(s).isSymbolicLink();
|
|
11948
12093
|
} catch {
|
|
11949
12094
|
return false;
|
|
11950
12095
|
}
|
|
@@ -12057,7 +12202,6 @@ async function resolveBaseRef(root) {
|
|
|
12057
12202
|
}
|
|
12058
12203
|
|
|
12059
12204
|
// src/commands/decisions-guard.ts
|
|
12060
|
-
init_exec();
|
|
12061
12205
|
var PR_CREATE_PATTERN = /\bgh\s+pr\s+create\b/;
|
|
12062
12206
|
function readPayload(options) {
|
|
12063
12207
|
if (options.stdinPayload != null) {
|
|
@@ -12083,11 +12227,20 @@ async function runDecisionsGuard(options = {}) {
|
|
|
12083
12227
|
const baseRef = await resolveBaseRef(root);
|
|
12084
12228
|
if (!baseRef) return;
|
|
12085
12229
|
const range = `${baseRef}...HEAD`;
|
|
12086
|
-
const commitMessages =
|
|
12230
|
+
const commitMessages = await readCommitMessages(root, range);
|
|
12087
12231
|
const result = await runDecisionsCheck(root, { range, commitMessages });
|
|
12232
|
+
if (result.triggered && !result.satisfied && result.recordErrors.length > 0) {
|
|
12233
|
+
const what = result.missingDecision ? "this change needs an ADR, and a decision record in it is misplaced or invalid" : "a decision record in this change is misplaced or invalid";
|
|
12234
|
+
const why = result.missingDecision ? result.reasons.filter((r) => !r.startsWith("no new") && r !== result.recordWayOut) : result.recordErrors;
|
|
12235
|
+
emitPreToolUseDeny(
|
|
12236
|
+
`I didn't run that: ${what} (${why.join("; ")}). ${result.recordWayOut} Then retry.`
|
|
12237
|
+
);
|
|
12238
|
+
return;
|
|
12239
|
+
}
|
|
12088
12240
|
if (result.triggered && !result.satisfied) {
|
|
12241
|
+
const rel = relativeDecisionPath(await resolveDecisionsDir(root), root);
|
|
12089
12242
|
emitPreToolUseDeny(
|
|
12090
|
-
`I didn't run that \u2014 this change needs an ADR before opening a PR (${result.reasons.join("; ")}). Run haus decisions suggest, add the decision under
|
|
12243
|
+
`I didn't run that \u2014 this change needs an ADR before opening a PR (${result.reasons.join("; ")}). Run haus decisions suggest, add the decision under ${rel}/, then retry, or put [adr-skip] on its own line in a commit body for non-security changes.`
|
|
12091
12244
|
);
|
|
12092
12245
|
}
|
|
12093
12246
|
}
|
|
@@ -12104,6 +12257,14 @@ async function nextDecisionNumber(root) {
|
|
|
12104
12257
|
}
|
|
12105
12258
|
|
|
12106
12259
|
// src/decisions/suggest.ts
|
|
12260
|
+
function printRecordFailures(checks) {
|
|
12261
|
+
const lines = /* @__PURE__ */ new Set([
|
|
12262
|
+
...checks.flatMap((check) => check.recordErrors),
|
|
12263
|
+
...checks.map((check) => check.recordWayOut).filter((wayOut) => wayOut.length > 0)
|
|
12264
|
+
]);
|
|
12265
|
+
for (const line2 of lines) process.stdout.write(`${line2}
|
|
12266
|
+
`);
|
|
12267
|
+
}
|
|
12107
12268
|
function draftTemplate(number, title) {
|
|
12108
12269
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
12109
12270
|
return `# ADR-${number}: ${title}
|
|
@@ -12136,11 +12297,18 @@ _(Why this over alternatives \u2014 the line agents read for "why".)_
|
|
|
12136
12297
|
`;
|
|
12137
12298
|
}
|
|
12138
12299
|
async function runDecisionsSuggest(root, opts = {}) {
|
|
12139
|
-
|
|
12140
|
-
|
|
12141
|
-
|
|
12142
|
-
|
|
12143
|
-
|
|
12300
|
+
if (!opts.title) {
|
|
12301
|
+
const stagedCheck = await runDecisionsCheck(root, { staged: true });
|
|
12302
|
+
const unstagedCheck = await runDecisionsCheck(root, {});
|
|
12303
|
+
const triggered = stagedCheck.triggered || unstagedCheck.triggered;
|
|
12304
|
+
if (!triggered) return;
|
|
12305
|
+
if (stagedCheck.triggered && stagedCheck.satisfied) return;
|
|
12306
|
+
const wantsDraft = (check) => check.missingDecision && check.recordErrors.length === 0;
|
|
12307
|
+
if (!wantsDraft(stagedCheck) && !wantsDraft(unstagedCheck)) {
|
|
12308
|
+
if (!opts.fromHook) printRecordFailures([stagedCheck, unstagedCheck]);
|
|
12309
|
+
return;
|
|
12310
|
+
}
|
|
12311
|
+
}
|
|
12144
12312
|
const number = await nextDecisionNumber(root);
|
|
12145
12313
|
const title = opts.title ?? "Decision from current changes";
|
|
12146
12314
|
const draft = draftTemplate(number, title);
|
|
@@ -12168,9 +12336,7 @@ init_logger();
|
|
|
12168
12336
|
async function runDecisions(sub, arg, options = {}) {
|
|
12169
12337
|
const root = process.cwd();
|
|
12170
12338
|
if (sub === "check") {
|
|
12171
|
-
const commitMessages = options.range != null ?
|
|
12172
|
-
(m) => m.runGit(["log", "--format=%B", toAncestryLogRange(options.range)], { cwd: root })
|
|
12173
|
-
)).stdout : "";
|
|
12339
|
+
const commitMessages = options.range != null ? await readCommitMessages(root, options.range) : [];
|
|
12174
12340
|
const result = await runDecisionsCheck(root, {
|
|
12175
12341
|
staged: options.staged,
|
|
12176
12342
|
range: options.range,
|
|
@@ -12188,9 +12354,17 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
12188
12354
|
log(`decisions gate: satisfied (${result.reasons.join("; ")})`);
|
|
12189
12355
|
return;
|
|
12190
12356
|
}
|
|
12191
|
-
for (const reason of result.reasons) warn(reason);
|
|
12357
|
+
for (const reason of result.reasons) if (reason !== result.recordWayOut) warn(reason);
|
|
12358
|
+
if (result.recordErrors.length > 0) {
|
|
12359
|
+
const what = result.missingDecision ? "this change needs an ADR, and a decision record in it is misplaced or invalid" : "a decision record in this change is misplaced or invalid";
|
|
12360
|
+
const why = result.missingDecision ? result.reasons.filter((r) => !r.startsWith("no new") && r !== result.recordWayOut) : result.recordErrors;
|
|
12361
|
+
error(`decisions gate: ${what}. Why: ${why.join("; ")}. ${result.recordWayOut}`);
|
|
12362
|
+
process.exitCode = 1;
|
|
12363
|
+
return;
|
|
12364
|
+
}
|
|
12365
|
+
const rel = relativeDecisionPath(await resolveDecisionsDir(root), root);
|
|
12192
12366
|
error(
|
|
12193
|
-
`decisions gate: decision-worthy change without a valid ADR under
|
|
12367
|
+
`decisions gate: decision-worthy change without a valid ADR under ${rel}/. Why: ${result.reasons.filter((r) => !r.startsWith("no new")).join("; ") || "see above"}. Run \`haus decisions suggest\`, add ${rel}/NNNN-*.md + README row, or put [adr-skip] on its own line with a justification in the PR body or a commit body (ignored for security paths).`
|
|
12194
12368
|
);
|
|
12195
12369
|
process.exitCode = 1;
|
|
12196
12370
|
return;
|
|
@@ -12720,7 +12894,7 @@ function matchDangerousCommandShape(command) {
|
|
|
12720
12894
|
}
|
|
12721
12895
|
|
|
12722
12896
|
// src/security/guard-file-access.ts
|
|
12723
|
-
import
|
|
12897
|
+
import fs48 from "node:fs";
|
|
12724
12898
|
import path58 from "node:path";
|
|
12725
12899
|
function matchesDenyTier(candidate) {
|
|
12726
12900
|
const normalized = normalizePathForMatch(candidate);
|
|
@@ -12728,11 +12902,11 @@ function matchesDenyTier(candidate) {
|
|
|
12728
12902
|
}
|
|
12729
12903
|
function resolveOnDisk(candidate) {
|
|
12730
12904
|
try {
|
|
12731
|
-
const resolved =
|
|
12905
|
+
const resolved = fs48.realpathSync(candidate);
|
|
12732
12906
|
return resolved === path58.resolve(candidate) ? {} : { resolved };
|
|
12733
12907
|
} catch {
|
|
12734
12908
|
try {
|
|
12735
|
-
if (
|
|
12909
|
+
if (fs48.lstatSync(candidate).isSymbolicLink()) return { unverifiable: true };
|
|
12736
12910
|
} catch {
|
|
12737
12911
|
}
|
|
12738
12912
|
const parent = path58.dirname(candidate);
|
|
@@ -14861,7 +15035,7 @@ import path63 from "node:path";
|
|
|
14861
15035
|
|
|
14862
15036
|
// src/new-project/create-project.ts
|
|
14863
15037
|
import path62 from "node:path";
|
|
14864
|
-
import
|
|
15038
|
+
import fs49 from "fs-extra";
|
|
14865
15039
|
init_exec();
|
|
14866
15040
|
init_logger();
|
|
14867
15041
|
|
|
@@ -15049,7 +15223,7 @@ async function recordPackageManager(projectPath, pm, deps) {
|
|
|
15049
15223
|
const pkgPath = path62.join(projectPath, "package.json");
|
|
15050
15224
|
let pkg;
|
|
15051
15225
|
try {
|
|
15052
|
-
pkg = await
|
|
15226
|
+
pkg = await fs49.readJson(pkgPath);
|
|
15053
15227
|
} catch {
|
|
15054
15228
|
return {};
|
|
15055
15229
|
}
|
|
@@ -15064,7 +15238,7 @@ async function recordPackageManager(projectPath, pm, deps) {
|
|
|
15064
15238
|
note: `installed ${pm} is ${version}, which haus does not support \u2014 the package manager was not recorded, so the scanner will report it as unknown until you upgrade`
|
|
15065
15239
|
};
|
|
15066
15240
|
}
|
|
15067
|
-
await
|
|
15241
|
+
await fs49.writeJson(pkgPath, { ...pkg, packageManager: field }, { spaces: 2 });
|
|
15068
15242
|
return {};
|
|
15069
15243
|
}
|
|
15070
15244
|
var CONFIG_TOOLING = {
|
|
@@ -15082,7 +15256,7 @@ async function ensureConfigTooling(projectPath, scaffolded) {
|
|
|
15082
15256
|
const pkgPath = path62.join(projectPath, "package.json");
|
|
15083
15257
|
let pkg;
|
|
15084
15258
|
try {
|
|
15085
|
-
pkg = await
|
|
15259
|
+
pkg = await fs49.readJson(pkgPath);
|
|
15086
15260
|
} catch {
|
|
15087
15261
|
return [];
|
|
15088
15262
|
}
|
|
@@ -15106,7 +15280,7 @@ async function ensureConfigTooling(projectPath, scaffolded) {
|
|
|
15106
15280
|
}
|
|
15107
15281
|
}
|
|
15108
15282
|
if (!changed) return [];
|
|
15109
|
-
await
|
|
15283
|
+
await fs49.writeJson(pkgPath, { ...pkg, devDependencies, scripts }, { spaces: 2 });
|
|
15110
15284
|
return added;
|
|
15111
15285
|
}
|
|
15112
15286
|
async function createProject(opts, deps = defaultDeps) {
|
|
@@ -15119,7 +15293,7 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
15119
15293
|
};
|
|
15120
15294
|
const stack = getStack(opts.stackId);
|
|
15121
15295
|
if (!stack) return fail("preflight", `Unknown stack "${opts.stackId}". Run \`haus new --list\`.`);
|
|
15122
|
-
const parentStat = await
|
|
15296
|
+
const parentStat = await fs49.stat(path62.dirname(projectPath)).catch(() => null);
|
|
15123
15297
|
if (!parentStat?.isDirectory()) {
|
|
15124
15298
|
return fail(
|
|
15125
15299
|
"preflight",
|
|
@@ -15132,7 +15306,7 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
15132
15306
|
`Invalid project name "${opts.name}" (lowercase letters, digits, . _ - only).`
|
|
15133
15307
|
);
|
|
15134
15308
|
}
|
|
15135
|
-
if (await
|
|
15309
|
+
if (await fs49.pathExists(projectPath)) {
|
|
15136
15310
|
return fail("preflight", `Target ${projectPath} already exists \u2014 refusing to write into it.`);
|
|
15137
15311
|
}
|
|
15138
15312
|
const scaffoldArgs = stack.scaffold.args.map(
|
|
@@ -15208,13 +15382,13 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
15208
15382
|
});
|
|
15209
15383
|
if (scaffold.exitCode !== 0) {
|
|
15210
15384
|
const why = scaffold.killed ? `${pinned} did not complete: ${scaffold.stderr.trim() || `timed out after ${SCAFFOLD_TIMEOUT_MS}ms`}. Scaffolders that stall this way are usually waiting on a prompt despite --yes \u2014 run it yourself to see: npx ${npxArgs.join(" ")}` : `${pinned} exited ${scaffold.exitCode}.`;
|
|
15211
|
-
const leftBehind = await
|
|
15385
|
+
const leftBehind = await fs49.pathExists(projectPath) ? ` The directory was left for inspection \u2014 review it and remove it manually if unwanted (haus never auto-deletes).` : "";
|
|
15212
15386
|
return fail("scaffold", `${why}${leftBehind}`);
|
|
15213
15387
|
}
|
|
15214
15388
|
steps.push({ step: "scaffold", status: "done" });
|
|
15215
15389
|
phase = "git";
|
|
15216
15390
|
if (!opts.json) log("\u25B6 git: init/branch/commit");
|
|
15217
|
-
if (!await
|
|
15391
|
+
if (!await fs49.pathExists(path62.join(projectPath, ".git"))) {
|
|
15218
15392
|
const init = await deps.git(["init"], { cwd: projectPath });
|
|
15219
15393
|
if (init.exitCode !== 0) return fail("git", `git init failed: ${init.stderr}`);
|
|
15220
15394
|
}
|
|
@@ -15525,7 +15699,9 @@ async function runRecommend(options) {
|
|
|
15525
15699
|
const root = process.cwd();
|
|
15526
15700
|
const context = await readContextOrScan(root);
|
|
15527
15701
|
const include = parseIdList(options.include);
|
|
15702
|
+
const declaredRoles = parseIdList(options.declareRole);
|
|
15528
15703
|
if (include.length > 0) await addProjectIncludedIds(root, include);
|
|
15704
|
+
if (declaredRoles.length > 0) await addProjectDeclaredRoles(root, declaredRoles);
|
|
15529
15705
|
const activePluginIds = pluginIdsForInstall(await activeGlobalPluginIds(root));
|
|
15530
15706
|
let result;
|
|
15531
15707
|
try {
|
|
@@ -15546,7 +15722,7 @@ async function runRecommend(options) {
|
|
|
15546
15722
|
log(`Skipped: ${result.skipped.length}`);
|
|
15547
15723
|
if (result.optInEligible && result.optInEligible.length > 0) {
|
|
15548
15724
|
log(
|
|
15549
|
-
`Opt-in available: ${result.optInEligible.length} (add with \`haus recommend --include <id>\`)`
|
|
15725
|
+
`Opt-in available: ${result.optInEligible.length} (add with \`haus recommend --include <id>\`, or with \`haus recommend --declare-role <role>\` for role-gated items)`
|
|
15550
15726
|
);
|
|
15551
15727
|
}
|
|
15552
15728
|
for (const w of result.warnings) log(`- WARN: ${w}`);
|
|
@@ -15760,7 +15936,7 @@ async function runScan(options) {
|
|
|
15760
15936
|
|
|
15761
15937
|
// src/commands/undo.ts
|
|
15762
15938
|
import path65 from "node:path";
|
|
15763
|
-
import
|
|
15939
|
+
import fs50 from "fs-extra";
|
|
15764
15940
|
|
|
15765
15941
|
// src/claude/managed-paths.ts
|
|
15766
15942
|
init_paths();
|
|
@@ -15779,7 +15955,7 @@ init_paths();
|
|
|
15779
15955
|
async function collectCoreManagedPaths(root) {
|
|
15780
15956
|
const existing = [];
|
|
15781
15957
|
for (const abs of coreManagedAbsolutePaths(root)) {
|
|
15782
|
-
if (await
|
|
15958
|
+
if (await fs50.pathExists(abs)) existing.push(abs);
|
|
15783
15959
|
}
|
|
15784
15960
|
return existing;
|
|
15785
15961
|
}
|
|
@@ -15809,19 +15985,19 @@ async function collectLockTrackedPaths(root) {
|
|
|
15809
15985
|
}
|
|
15810
15986
|
async function settingsHasHausContent(root) {
|
|
15811
15987
|
const settingsPath = claudePath(root, "settings.json");
|
|
15812
|
-
if (!await
|
|
15988
|
+
if (!await fs50.pathExists(settingsPath)) return false;
|
|
15813
15989
|
const settings = await readProjectSettings(root);
|
|
15814
15990
|
return settings._haus != null;
|
|
15815
15991
|
}
|
|
15816
15992
|
async function claudeMdHasHausBlock(root) {
|
|
15817
15993
|
const filePath = path65.join(root, "CLAUDE.md");
|
|
15818
|
-
if (!await
|
|
15819
|
-
const text = await
|
|
15994
|
+
if (!await fs50.pathExists(filePath)) return false;
|
|
15995
|
+
const text = await fs50.readFile(filePath, "utf8");
|
|
15820
15996
|
return text.includes(BLOCK_BEGIN);
|
|
15821
15997
|
}
|
|
15822
15998
|
async function stripProjectSettings(root) {
|
|
15823
15999
|
const settingsPath = claudePath(root, "settings.json");
|
|
15824
|
-
if (!await
|
|
16000
|
+
if (!await fs50.pathExists(settingsPath)) return false;
|
|
15825
16001
|
let settings = await readProjectSettings(root);
|
|
15826
16002
|
settings = stripHausHooks(
|
|
15827
16003
|
stripHausAsk(stripHausAllow(stripHausDeny(stripHausEnabledPlugins(settings))))
|
|
@@ -15832,30 +16008,30 @@ async function stripProjectSettings(root) {
|
|
|
15832
16008
|
log(`Stripped haus rules from ${path65.relative(root, settingsPath)} (user settings preserved).`);
|
|
15833
16009
|
return true;
|
|
15834
16010
|
}
|
|
15835
|
-
await
|
|
16011
|
+
await fs50.remove(settingsPath);
|
|
15836
16012
|
log(`Removed ${path65.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
15837
16013
|
return true;
|
|
15838
16014
|
}
|
|
15839
16015
|
async function stripRootClaudeMd(root) {
|
|
15840
16016
|
const filePath = path65.join(root, "CLAUDE.md");
|
|
15841
|
-
if (!await
|
|
15842
|
-
const prev = await
|
|
16017
|
+
if (!await fs50.pathExists(filePath)) return false;
|
|
16018
|
+
const prev = await fs50.readFile(filePath, "utf8");
|
|
15843
16019
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
15844
16020
|
const next = stripHausBlock(prev);
|
|
15845
16021
|
if (next.length === 0) {
|
|
15846
|
-
await
|
|
16022
|
+
await fs50.remove(filePath);
|
|
15847
16023
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
15848
16024
|
} else {
|
|
15849
|
-
await
|
|
16025
|
+
await fs50.writeFile(filePath, next, "utf8");
|
|
15850
16026
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
15851
16027
|
}
|
|
15852
16028
|
return true;
|
|
15853
16029
|
}
|
|
15854
16030
|
async function pruneDirIfEmpty(dir) {
|
|
15855
|
-
if (!await
|
|
16031
|
+
if (!await fs50.pathExists(dir)) return;
|
|
15856
16032
|
try {
|
|
15857
|
-
const entries = await
|
|
15858
|
-
if (entries.length === 0) await
|
|
16033
|
+
const entries = await fs50.readdir(dir);
|
|
16034
|
+
if (entries.length === 0) await fs50.remove(dir);
|
|
15859
16035
|
} catch {
|
|
15860
16036
|
}
|
|
15861
16037
|
}
|
|
@@ -15864,11 +16040,11 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
|
15864
16040
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
15865
16041
|
const backupRoot = hausPath(root, "backups", dirBackupDirName("undo", stamp));
|
|
15866
16042
|
for (const abs of managedAbsPaths) {
|
|
15867
|
-
if (!await
|
|
16043
|
+
if (!await fs50.pathExists(abs)) continue;
|
|
15868
16044
|
const rel = path65.relative(root, abs);
|
|
15869
16045
|
const backupPath = path65.join(backupRoot, rel);
|
|
15870
|
-
await
|
|
15871
|
-
await
|
|
16046
|
+
await fs50.ensureDir(path65.dirname(backupPath));
|
|
16047
|
+
await fs50.copy(abs, backupPath);
|
|
15872
16048
|
}
|
|
15873
16049
|
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path65.relative(root, backupRoot)}.`);
|
|
15874
16050
|
}
|
|
@@ -15910,8 +16086,8 @@ User-owned .claude/ files will be preserved.`
|
|
|
15910
16086
|
}
|
|
15911
16087
|
await backupManagedFilesBeforeUndo(root, managed);
|
|
15912
16088
|
for (const abs of coreManaged) {
|
|
15913
|
-
if (!await
|
|
15914
|
-
await
|
|
16089
|
+
if (!await fs50.pathExists(abs)) continue;
|
|
16090
|
+
await fs50.remove(abs);
|
|
15915
16091
|
log(`Removed ${path65.relative(root, abs)}`);
|
|
15916
16092
|
}
|
|
15917
16093
|
for (const { id, existing } of lockRemovable) {
|
|
@@ -15926,7 +16102,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
15926
16102
|
|
|
15927
16103
|
// src/install/uninstall.ts
|
|
15928
16104
|
import path66 from "node:path";
|
|
15929
|
-
import
|
|
16105
|
+
import fs51 from "fs-extra";
|
|
15930
16106
|
init_fs();
|
|
15931
16107
|
init_logger();
|
|
15932
16108
|
init_paths();
|
|
@@ -15960,7 +16136,7 @@ async function runUninstall(options = {}) {
|
|
|
15960
16136
|
);
|
|
15961
16137
|
}
|
|
15962
16138
|
for (const entry of manifest.files) {
|
|
15963
|
-
const exists2 =
|
|
16139
|
+
const exists2 = fs51.pathExistsSync(entry.destPath);
|
|
15964
16140
|
if (!exists2) continue;
|
|
15965
16141
|
const content2 = await readText(entry.destPath);
|
|
15966
16142
|
if (content2 === void 0) continue;
|
|
@@ -15978,7 +16154,7 @@ async function runUninstall(options = {}) {
|
|
|
15978
16154
|
result.skipped.push(entry.destPath);
|
|
15979
16155
|
continue;
|
|
15980
16156
|
}
|
|
15981
|
-
await
|
|
16157
|
+
await fs51.remove(entry.destPath);
|
|
15982
16158
|
await pruneEmptyDir(path66.dirname(entry.destPath));
|
|
15983
16159
|
result.deleted.push(entry.destPath);
|
|
15984
16160
|
}
|
|
@@ -15990,14 +16166,14 @@ async function runUninstall(options = {}) {
|
|
|
15990
16166
|
result.hooksStripped = true;
|
|
15991
16167
|
const hausDir = path66.join(globalClaudeDir(), "haus");
|
|
15992
16168
|
const manifestPath2 = hausManifestPath();
|
|
15993
|
-
if (
|
|
15994
|
-
await
|
|
16169
|
+
if (fs51.pathExistsSync(manifestPath2)) {
|
|
16170
|
+
await fs51.remove(manifestPath2);
|
|
15995
16171
|
}
|
|
15996
16172
|
await removeInstallConfig();
|
|
15997
|
-
if (
|
|
16173
|
+
if (fs51.pathExistsSync(hausDir)) {
|
|
15998
16174
|
try {
|
|
15999
|
-
const remaining = await
|
|
16000
|
-
if (remaining.length === 0) await
|
|
16175
|
+
const remaining = await fs51.readdir(hausDir);
|
|
16176
|
+
if (remaining.length === 0) await fs51.remove(hausDir);
|
|
16001
16177
|
} catch {
|
|
16002
16178
|
}
|
|
16003
16179
|
}
|
|
@@ -16073,7 +16249,7 @@ init_forbidden_content();
|
|
|
16073
16249
|
init_manifest_item_fields();
|
|
16074
16250
|
init_path_safety();
|
|
16075
16251
|
init_validation_rules2();
|
|
16076
|
-
import
|
|
16252
|
+
import fs52 from "node:fs";
|
|
16077
16253
|
import path67 from "node:path";
|
|
16078
16254
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
16079
16255
|
var ITEM_ID_PATTERN = /^haus\./;
|
|
@@ -16234,7 +16410,7 @@ function checkRequiredFrontmatter(text, label) {
|
|
|
16234
16410
|
}
|
|
16235
16411
|
function readForAudit(absPath, label, failures) {
|
|
16236
16412
|
try {
|
|
16237
|
-
return
|
|
16413
|
+
return fs52.readFileSync(absPath, "utf8");
|
|
16238
16414
|
} catch (err) {
|
|
16239
16415
|
failures.push(`${label}: unreadable (${err instanceof Error ? err.message : String(err)})`);
|
|
16240
16416
|
return null;
|
|
@@ -16266,7 +16442,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
16266
16442
|
const absPath = path67.join(manifestDir, item.path);
|
|
16267
16443
|
if (item.type === "skill") {
|
|
16268
16444
|
const skillMd = path67.join(absPath, "SKILL.md");
|
|
16269
|
-
if (!
|
|
16445
|
+
if (!fs52.existsSync(skillMd)) {
|
|
16270
16446
|
failures.push(`${item.id}: missing ${path67.relative(manifestDir, skillMd)}`);
|
|
16271
16447
|
continue;
|
|
16272
16448
|
}
|
|
@@ -16277,7 +16453,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
16277
16453
|
...auditForbiddenTagsInText(text, `${item.id}: ${path67.relative(manifestDir, skillMd)}`)
|
|
16278
16454
|
);
|
|
16279
16455
|
} else if (item.type === "agent") {
|
|
16280
|
-
if (!
|
|
16456
|
+
if (!fs52.existsSync(absPath)) {
|
|
16281
16457
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
16282
16458
|
continue;
|
|
16283
16459
|
}
|
|
@@ -16287,13 +16463,13 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
16287
16463
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
16288
16464
|
failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
|
|
16289
16465
|
} else if (item.type === "template") {
|
|
16290
|
-
if (!
|
|
16466
|
+
if (!fs52.existsSync(absPath)) {
|
|
16291
16467
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
16292
16468
|
continue;
|
|
16293
16469
|
}
|
|
16294
16470
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
16295
16471
|
} else if (item.type === "command") {
|
|
16296
|
-
if (!
|
|
16472
|
+
if (!fs52.existsSync(absPath)) {
|
|
16297
16473
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
16298
16474
|
continue;
|
|
16299
16475
|
}
|
|
@@ -16303,15 +16479,15 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
16303
16479
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
16304
16480
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
16305
16481
|
} else if (item.type === "config") {
|
|
16306
|
-
if (!
|
|
16482
|
+
if (!fs52.existsSync(absPath)) {
|
|
16307
16483
|
failures.push(`${item.id}: missing config path ${item.path}`);
|
|
16308
16484
|
}
|
|
16309
16485
|
} else if (item.type === "rule") {
|
|
16310
|
-
if (!
|
|
16486
|
+
if (!fs52.existsSync(absPath)) {
|
|
16311
16487
|
failures.push(`${item.id}: missing rule file ${item.path}`);
|
|
16312
16488
|
}
|
|
16313
16489
|
} else if (item.type === "asset") {
|
|
16314
|
-
if (!
|
|
16490
|
+
if (!fs52.existsSync(absPath)) {
|
|
16315
16491
|
failures.push(`${item.id}: missing asset file ${item.path}`);
|
|
16316
16492
|
}
|
|
16317
16493
|
}
|
|
@@ -16346,7 +16522,7 @@ function resolveItemType(relPath, pathTypeMap) {
|
|
|
16346
16522
|
function walkAllFiles(dir, fn, failures) {
|
|
16347
16523
|
let entries;
|
|
16348
16524
|
try {
|
|
16349
|
-
entries =
|
|
16525
|
+
entries = fs52.readdirSync(dir, { withFileTypes: true });
|
|
16350
16526
|
} catch (err) {
|
|
16351
16527
|
failures.push(
|
|
16352
16528
|
`${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
|
|
@@ -16380,7 +16556,7 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
16380
16556
|
const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
|
|
16381
16557
|
for (const dir of CONTENT_ROOTS) {
|
|
16382
16558
|
const abs = path67.join(manifestDir, dir);
|
|
16383
|
-
if (!
|
|
16559
|
+
if (!fs52.existsSync(abs)) continue;
|
|
16384
16560
|
walkAllFiles(
|
|
16385
16561
|
abs,
|
|
16386
16562
|
(file) => {
|
|
@@ -16397,7 +16573,7 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
16397
16573
|
function walkMd(dir, fn, failures) {
|
|
16398
16574
|
let entries;
|
|
16399
16575
|
try {
|
|
16400
|
-
entries =
|
|
16576
|
+
entries = fs52.readdirSync(dir, { withFileTypes: true });
|
|
16401
16577
|
} catch (err) {
|
|
16402
16578
|
failures.push(
|
|
16403
16579
|
`${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
|
|
@@ -16416,7 +16592,7 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
16416
16592
|
const pathTypeMap = buildItemPathTypeMap(items);
|
|
16417
16593
|
for (const dir of ["skills", "agents", "templates", "commands", "sources"]) {
|
|
16418
16594
|
const abs = path67.join(manifestDir, dir);
|
|
16419
|
-
if (!
|
|
16595
|
+
if (!fs52.existsSync(abs)) continue;
|
|
16420
16596
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
16421
16597
|
walkMd(
|
|
16422
16598
|
abs,
|
|
@@ -16492,7 +16668,7 @@ init_logger();
|
|
|
16492
16668
|
// src/workspace/worktree/add.ts
|
|
16493
16669
|
init_fs();
|
|
16494
16670
|
import path75 from "node:path";
|
|
16495
|
-
import
|
|
16671
|
+
import fs56 from "fs-extra";
|
|
16496
16672
|
|
|
16497
16673
|
// src/workspace/worktree/git-worktree.ts
|
|
16498
16674
|
init_exec();
|
|
@@ -16613,12 +16789,12 @@ async function hasUnpushedWork(cwd) {
|
|
|
16613
16789
|
|
|
16614
16790
|
// src/workspace/worktree/hydrate.ts
|
|
16615
16791
|
import path71 from "node:path";
|
|
16616
|
-
import
|
|
16792
|
+
import fs54 from "fs-extra";
|
|
16617
16793
|
|
|
16618
16794
|
// src/workspace/worktree/cow-copy.ts
|
|
16619
16795
|
init_exec();
|
|
16620
16796
|
import path69 from "node:path";
|
|
16621
|
-
import
|
|
16797
|
+
import fs53 from "fs-extra";
|
|
16622
16798
|
async function getMacFilesystemType(dir) {
|
|
16623
16799
|
const df = await runCommand("df", ["-P", dir]);
|
|
16624
16800
|
if (df.exitCode !== 0) return void 0;
|
|
@@ -16649,14 +16825,14 @@ async function detectCowStrategy(dir) {
|
|
|
16649
16825
|
return "unknown-platform";
|
|
16650
16826
|
}
|
|
16651
16827
|
async function cowCopyDir(src, dest) {
|
|
16652
|
-
const srcExists = await
|
|
16828
|
+
const srcExists = await fs53.pathExists(src);
|
|
16653
16829
|
if (!srcExists)
|
|
16654
16830
|
return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
|
|
16655
16831
|
const strategy = await detectCowStrategy(path69.dirname(dest));
|
|
16656
16832
|
if (strategy === "unsupported" || strategy === "unknown-platform") {
|
|
16657
16833
|
return { strategy, attempted: false, ok: false };
|
|
16658
16834
|
}
|
|
16659
|
-
await
|
|
16835
|
+
await fs53.ensureDir(path69.dirname(dest));
|
|
16660
16836
|
const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
|
|
16661
16837
|
const result = await runCommand("cp", args);
|
|
16662
16838
|
return {
|
|
@@ -16849,11 +17025,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
16849
17025
|
for (const target of HYDRATION_TARGETS) {
|
|
16850
17026
|
const src = path71.join(mainMemberPath, target);
|
|
16851
17027
|
const dest = path71.join(worktreeMemberPath, target);
|
|
16852
|
-
if (!await
|
|
16853
|
-
const destExists = await
|
|
17028
|
+
if (!await fs54.pathExists(src)) continue;
|
|
17029
|
+
const destExists = await fs54.pathExists(dest);
|
|
16854
17030
|
if (!opts.force && destExists) continue;
|
|
16855
17031
|
if (opts.force && destExists) {
|
|
16856
|
-
await
|
|
17032
|
+
await fs54.remove(dest);
|
|
16857
17033
|
}
|
|
16858
17034
|
const copy = await cowCopyDir(src, dest);
|
|
16859
17035
|
targets.push({ target, copy });
|
|
@@ -16867,7 +17043,7 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
16867
17043
|
// src/workspace/worktree/local-files.ts
|
|
16868
17044
|
init_exec();
|
|
16869
17045
|
import path72 from "node:path";
|
|
16870
|
-
import
|
|
17046
|
+
import fs55 from "fs-extra";
|
|
16871
17047
|
var MACHINE_LOCAL_FILES = [
|
|
16872
17048
|
"docker-compose.local.yml",
|
|
16873
17049
|
path72.join(".claude", "settings.local.json")
|
|
@@ -16881,11 +17057,11 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
|
16881
17057
|
const copied = [];
|
|
16882
17058
|
for (const rel of MACHINE_LOCAL_FILES) {
|
|
16883
17059
|
const src = path72.join(mainMemberPath, rel);
|
|
16884
|
-
if (!await
|
|
17060
|
+
if (!await fs55.pathExists(src)) continue;
|
|
16885
17061
|
if (await isTracked(mainMemberPath, rel)) continue;
|
|
16886
17062
|
const dest = path72.join(worktreeMemberPath, rel);
|
|
16887
|
-
await
|
|
16888
|
-
await
|
|
17063
|
+
await fs55.ensureDir(path72.dirname(dest));
|
|
17064
|
+
await fs55.copy(src, dest, { overwrite: true });
|
|
16889
17065
|
copied.push(rel);
|
|
16890
17066
|
}
|
|
16891
17067
|
return copied;
|
|
@@ -16973,7 +17149,7 @@ async function runAdd(opts) {
|
|
|
16973
17149
|
}
|
|
16974
17150
|
const branch = opts.branch ?? opts.slug;
|
|
16975
17151
|
const wsWorktreePath = worktreePath(workspaceRoot, opts.slug);
|
|
16976
|
-
if (await
|
|
17152
|
+
if (await fs56.pathExists(wsWorktreePath)) {
|
|
16977
17153
|
return {
|
|
16978
17154
|
ok: false,
|
|
16979
17155
|
error: `Workspace worktree already exists at ${wsWorktreePath} \u2014 run \`haus workspace worktree hydrate --materialize\` from inside it to check out any missing members (e.g. a worktree created by plain \`git worktree add\`), or \`remove\` first to start over.`
|
|
@@ -17091,7 +17267,7 @@ async function runDryRun(args) {
|
|
|
17091
17267
|
import { spawn } from "node:child_process";
|
|
17092
17268
|
import fsSync from "node:fs";
|
|
17093
17269
|
import path81 from "node:path";
|
|
17094
|
-
import
|
|
17270
|
+
import fs58 from "fs-extra";
|
|
17095
17271
|
|
|
17096
17272
|
// src/workspace/worktree/doctor.ts
|
|
17097
17273
|
import { existsSync as existsSync7 } from "node:fs";
|
|
@@ -17100,7 +17276,7 @@ import path78 from "node:path";
|
|
|
17100
17276
|
// src/workspace/worktree/hydrate-lock.ts
|
|
17101
17277
|
init_paths();
|
|
17102
17278
|
import path76 from "node:path";
|
|
17103
|
-
import
|
|
17279
|
+
import fs57 from "fs-extra";
|
|
17104
17280
|
var STALE_LOCK_MS = 30 * 60 * 1e3;
|
|
17105
17281
|
function locksDir(mainRoot) {
|
|
17106
17282
|
return hausPath(mainRoot, "locks");
|
|
@@ -17135,7 +17311,7 @@ function defaultIsAlive(pid) {
|
|
|
17135
17311
|
}
|
|
17136
17312
|
async function readOwnerPid(lockPath) {
|
|
17137
17313
|
try {
|
|
17138
|
-
const raw = (await
|
|
17314
|
+
const raw = (await fs57.readFile(path76.join(lockPath, "pid"), "utf8")).trim();
|
|
17139
17315
|
if (raw === "") return void 0;
|
|
17140
17316
|
const pid = Number.parseInt(raw, 10);
|
|
17141
17317
|
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
@@ -17144,13 +17320,13 @@ async function readOwnerPid(lockPath) {
|
|
|
17144
17320
|
}
|
|
17145
17321
|
}
|
|
17146
17322
|
async function claim(lockPath, pid) {
|
|
17147
|
-
await
|
|
17323
|
+
await fs57.ensureDir(path76.dirname(lockPath));
|
|
17148
17324
|
try {
|
|
17149
|
-
await
|
|
17325
|
+
await fs57.mkdir(lockPath);
|
|
17150
17326
|
} catch {
|
|
17151
17327
|
return false;
|
|
17152
17328
|
}
|
|
17153
|
-
await
|
|
17329
|
+
await fs57.writeFile(path76.join(lockPath, "pid"), String(pid), "utf8");
|
|
17154
17330
|
return true;
|
|
17155
17331
|
}
|
|
17156
17332
|
async function acquireHydrateLock(mainRoot, slug, deps = {}) {
|
|
@@ -17169,7 +17345,7 @@ async function acquireHydrateLock(mainRoot, slug, deps = {}) {
|
|
|
17169
17345
|
}
|
|
17170
17346
|
if (alive) return { acquired: false, reason: "live", ownerPid, lockPath };
|
|
17171
17347
|
} else {
|
|
17172
|
-
const stat = await
|
|
17348
|
+
const stat = await fs57.stat(lockPath).catch(() => void 0);
|
|
17173
17349
|
if (stat === void 0) {
|
|
17174
17350
|
return await claim(lockPath, pid) ? { acquired: true, lockPath } : { acquired: false, reason: "live", lockPath };
|
|
17175
17351
|
}
|
|
@@ -17177,18 +17353,18 @@ async function acquireHydrateLock(mainRoot, slug, deps = {}) {
|
|
|
17177
17353
|
return { acquired: false, reason: "starting", lockPath };
|
|
17178
17354
|
}
|
|
17179
17355
|
}
|
|
17180
|
-
await
|
|
17356
|
+
await fs57.remove(lockPath);
|
|
17181
17357
|
return await claim(lockPath, pid) ? { acquired: true, lockPath } : { acquired: false, reason: "live", ownerPid, lockPath };
|
|
17182
17358
|
}
|
|
17183
17359
|
async function recordHydrateLockOwner(mainRoot, slug, pid) {
|
|
17184
|
-
await
|
|
17360
|
+
await fs57.writeFile(path76.join(hydrateLockPath(mainRoot, slug), "pid"), String(pid), "utf8");
|
|
17185
17361
|
}
|
|
17186
17362
|
async function releaseHydrateLock(mainRoot, slug) {
|
|
17187
|
-
await
|
|
17363
|
+
await fs57.remove(hydrateLockPath(mainRoot, slug));
|
|
17188
17364
|
}
|
|
17189
17365
|
async function readHydrateLock(mainRoot, slug) {
|
|
17190
17366
|
const lockPath = hydrateLockPath(mainRoot, slug);
|
|
17191
|
-
const stat = await
|
|
17367
|
+
const stat = await fs57.stat(lockPath).catch(() => void 0);
|
|
17192
17368
|
if (stat === void 0) return { held: false };
|
|
17193
17369
|
const ownerPid = await readOwnerPid(lockPath);
|
|
17194
17370
|
return {
|
|
@@ -17568,11 +17744,11 @@ async function consumeLastRunFailure(mainRoot, slug) {
|
|
|
17568
17744
|
const file = hydrateLastRunPath(mainRoot, slug);
|
|
17569
17745
|
let parsed;
|
|
17570
17746
|
try {
|
|
17571
|
-
parsed = JSON.parse(await
|
|
17747
|
+
parsed = JSON.parse(await fs58.readFile(file, "utf8"));
|
|
17572
17748
|
} catch {
|
|
17573
17749
|
return void 0;
|
|
17574
17750
|
}
|
|
17575
|
-
await
|
|
17751
|
+
await fs58.remove(file).catch(() => void 0);
|
|
17576
17752
|
if (parsed && parsed.ok === false) return parsed;
|
|
17577
17753
|
return void 0;
|
|
17578
17754
|
}
|
|
@@ -17644,7 +17820,7 @@ async function runAutoHydrate(opts = {}, deps = {}) {
|
|
|
17644
17820
|
reason: "could not determine the haus CLI entry point to re-invoke"
|
|
17645
17821
|
};
|
|
17646
17822
|
}
|
|
17647
|
-
await
|
|
17823
|
+
await fs58.ensureDir(logsDir(mainRoot));
|
|
17648
17824
|
appendLog(
|
|
17649
17825
|
logPath,
|
|
17650
17826
|
`autohydrate: forking hydrate --materialize for "${slug}" (${missingMembers.join(", ")})`
|
|
@@ -17713,21 +17889,21 @@ async function runDetachedHydration() {
|
|
|
17713
17889
|
async function writeLastRun(mainRoot, slug, ok, failures) {
|
|
17714
17890
|
const file = hydrateLastRunPath(mainRoot, slug);
|
|
17715
17891
|
if (ok) {
|
|
17716
|
-
await
|
|
17892
|
+
await fs58.remove(file).catch(() => void 0);
|
|
17717
17893
|
return;
|
|
17718
17894
|
}
|
|
17719
17895
|
const record = { slug, finishedAt: (/* @__PURE__ */ new Date()).toISOString(), ok, failures };
|
|
17720
|
-
await
|
|
17721
|
-
await
|
|
17896
|
+
await fs58.ensureDir(path81.dirname(file));
|
|
17897
|
+
await fs58.writeJson(file, record, { spaces: 2 }).catch(() => void 0);
|
|
17722
17898
|
}
|
|
17723
17899
|
|
|
17724
17900
|
// src/workspace/worktree/dematerialize.ts
|
|
17725
17901
|
import path83 from "node:path";
|
|
17726
|
-
import
|
|
17902
|
+
import fs60 from "fs-extra";
|
|
17727
17903
|
|
|
17728
17904
|
// src/workspace/worktree/member-targets.ts
|
|
17729
17905
|
import path82 from "node:path";
|
|
17730
|
-
import
|
|
17906
|
+
import fs59 from "fs-extra";
|
|
17731
17907
|
function normalizeStateMembers(state, allMembers) {
|
|
17732
17908
|
return state?.members.map((m) => ({ id: m.id, folder: m.folder, absPath: m.absPath })) ?? allMembers.map((m) => ({ id: m.id, folder: m.folder, absPath: m.absPath }));
|
|
17733
17909
|
}
|
|
@@ -17752,7 +17928,7 @@ function resolveMemberTargets(wsWorktreePath, stateMembers, allMembers) {
|
|
|
17752
17928
|
async function filterExistingTargets(targets) {
|
|
17753
17929
|
const existing = [];
|
|
17754
17930
|
for (const target of targets) {
|
|
17755
|
-
if (await
|
|
17931
|
+
if (await fs59.pathExists(target.path)) existing.push(target);
|
|
17756
17932
|
}
|
|
17757
17933
|
return existing;
|
|
17758
17934
|
}
|
|
@@ -17797,7 +17973,7 @@ async function runDematerialize(opts) {
|
|
|
17797
17973
|
} catch (err) {
|
|
17798
17974
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
17799
17975
|
}
|
|
17800
|
-
if (!await
|
|
17976
|
+
if (!await fs60.pathExists(wsWorktreePath)) {
|
|
17801
17977
|
return { ok: false, error: `No workspace worktree found at ${wsWorktreePath}.` };
|
|
17802
17978
|
}
|
|
17803
17979
|
const lock = await readHydrateLock(workspaceRoot, slug);
|
|
@@ -17831,8 +18007,8 @@ async function runDematerialize(opts) {
|
|
|
17831
18007
|
const failed = [];
|
|
17832
18008
|
for (const folder of unresolvedDroppedMembers) {
|
|
17833
18009
|
const droppedPath = path83.join(wsWorktreePath, folder);
|
|
17834
|
-
if (await
|
|
17835
|
-
await
|
|
18010
|
+
if (await fs60.pathExists(droppedPath)) {
|
|
18011
|
+
await fs60.remove(droppedPath);
|
|
17836
18012
|
failed.push({ repo: folder, error: unresolvedDroppedFailureReason() });
|
|
17837
18013
|
}
|
|
17838
18014
|
}
|
|
@@ -17847,14 +18023,14 @@ async function runDematerialize(opts) {
|
|
|
17847
18023
|
|
|
17848
18024
|
// src/workspace/worktree/list.ts
|
|
17849
18025
|
import path84 from "node:path";
|
|
17850
|
-
import
|
|
18026
|
+
import fs61 from "fs-extra";
|
|
17851
18027
|
async function runList2() {
|
|
17852
18028
|
const resolved = await resolveWorkspaceForWorktree();
|
|
17853
18029
|
if (!resolved.ok) return { ok: false, error: resolved.reason };
|
|
17854
18030
|
const { rootInfo, workspaceRoot } = resolved;
|
|
17855
18031
|
const dir = worktreesDir(workspaceRoot);
|
|
17856
|
-
if (!await
|
|
17857
|
-
const entries = await
|
|
18032
|
+
if (!await fs61.pathExists(dir)) return { ok: true, workspaceRoot, worktrees: [] };
|
|
18033
|
+
const entries = await fs61.readdir(dir, { withFileTypes: true });
|
|
17858
18034
|
const slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
17859
18035
|
const configuredMembers = await readMembers(rootInfo);
|
|
17860
18036
|
const worktrees = [];
|
|
@@ -17865,7 +18041,7 @@ async function runList2() {
|
|
|
17865
18041
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
17866
18042
|
for (const m of source) {
|
|
17867
18043
|
const memberPath = path84.join(wtPath, m.folder);
|
|
17868
|
-
const materialized = await
|
|
18044
|
+
const materialized = await fs61.pathExists(memberPath);
|
|
17869
18045
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
17870
18046
|
members.push({
|
|
17871
18047
|
id: m.id,
|
|
@@ -17888,12 +18064,12 @@ async function runList2() {
|
|
|
17888
18064
|
// src/workspace/worktree/reclaim.ts
|
|
17889
18065
|
import { existsSync as existsSync9 } from "node:fs";
|
|
17890
18066
|
import path85 from "node:path";
|
|
17891
|
-
import
|
|
18067
|
+
import fs62 from "fs-extra";
|
|
17892
18068
|
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
17893
18069
|
async function lastTouchedMs(worktreeRoot) {
|
|
17894
18070
|
const candidates = [worktreeRoot, path85.join(worktreeRoot, ".haus-worktree.json")];
|
|
17895
18071
|
try {
|
|
17896
|
-
const dotGit = await
|
|
18072
|
+
const dotGit = await fs62.readFile(path85.join(worktreeRoot, ".git"), "utf8");
|
|
17897
18073
|
const match = /^gitdir:\s*(.+)$/m.exec(dotGit);
|
|
17898
18074
|
if (match?.[1]) {
|
|
17899
18075
|
const gitdir = match[1].trim();
|
|
@@ -17903,7 +18079,7 @@ async function lastTouchedMs(worktreeRoot) {
|
|
|
17903
18079
|
}
|
|
17904
18080
|
let newest = 0;
|
|
17905
18081
|
for (const candidate of candidates) {
|
|
17906
|
-
const stat = await
|
|
18082
|
+
const stat = await fs62.stat(candidate).catch(() => void 0);
|
|
17907
18083
|
if (stat && stat.mtimeMs > newest) newest = stat.mtimeMs;
|
|
17908
18084
|
}
|
|
17909
18085
|
return newest;
|
|
@@ -17930,7 +18106,7 @@ async function runReclaim(opts = {}) {
|
|
|
17930
18106
|
if (!existsSync9(dir)) {
|
|
17931
18107
|
return { ok: true, dryRun: !opts.write, olderThanDays, candidates: [], reclaimed: 0 };
|
|
17932
18108
|
}
|
|
17933
|
-
const allEntries = (await
|
|
18109
|
+
const allEntries = (await fs62.readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
17934
18110
|
const entries = allEntries.filter((name) => isValidWorktreeSlug(name));
|
|
17935
18111
|
const unaddressable = allEntries.filter((name) => !isValidWorktreeSlug(name));
|
|
17936
18112
|
const members = await readMembers(rootInfo);
|
|
@@ -18011,13 +18187,13 @@ async function runReclaim(opts = {}) {
|
|
|
18011
18187
|
}
|
|
18012
18188
|
|
|
18013
18189
|
// src/workspace/worktree/remove.ts
|
|
18014
|
-
import
|
|
18190
|
+
import fs63 from "fs-extra";
|
|
18015
18191
|
async function runRemove2(opts) {
|
|
18016
18192
|
const resolved = await resolveWorkspaceForWorktree();
|
|
18017
18193
|
if (!resolved.ok) return { ok: false, error: resolved.reason };
|
|
18018
18194
|
const { rootInfo, workspaceRoot } = resolved;
|
|
18019
18195
|
const wsWorktreePath = worktreePath(workspaceRoot, opts.slug);
|
|
18020
|
-
if (!await
|
|
18196
|
+
if (!await fs63.pathExists(wsWorktreePath)) {
|
|
18021
18197
|
return { ok: false, error: `No workspace worktree found at ${wsWorktreePath}.` };
|
|
18022
18198
|
}
|
|
18023
18199
|
const state = await readWorktreeState(wsWorktreePath);
|
|
@@ -18085,8 +18261,8 @@ async function runRemove2(opts) {
|
|
|
18085
18261
|
}
|
|
18086
18262
|
await pruneWorktrees(workspaceRoot);
|
|
18087
18263
|
}
|
|
18088
|
-
if (wsRemoveOk && await
|
|
18089
|
-
await
|
|
18264
|
+
if (wsRemoveOk && await fs63.pathExists(wsWorktreePath)) {
|
|
18265
|
+
await fs63.remove(wsWorktreePath);
|
|
18090
18266
|
}
|
|
18091
18267
|
return { ok: failed.length === 0, dryRun: false, slug: opts.slug, removed, failed };
|
|
18092
18268
|
}
|
|
@@ -18859,17 +19035,17 @@ init_logger();
|
|
|
18859
19035
|
|
|
18860
19036
|
// src/workspace/link-context/link.ts
|
|
18861
19037
|
import path90 from "node:path";
|
|
18862
|
-
import
|
|
19038
|
+
import fs66 from "fs-extra";
|
|
18863
19039
|
init_fs();
|
|
18864
19040
|
init_logger();
|
|
18865
19041
|
|
|
18866
19042
|
// src/workspace/link-context/plan.ts
|
|
18867
19043
|
import path89 from "node:path";
|
|
18868
|
-
import
|
|
19044
|
+
import fs65 from "fs-extra";
|
|
18869
19045
|
|
|
18870
19046
|
// src/workspace/link-context/scan-source.ts
|
|
18871
19047
|
import path88 from "node:path";
|
|
18872
|
-
import
|
|
19048
|
+
import fs64 from "fs-extra";
|
|
18873
19049
|
var ASSET_SUBDIR = {
|
|
18874
19050
|
skill: "skills",
|
|
18875
19051
|
agent: "agents",
|
|
@@ -18877,20 +19053,20 @@ var ASSET_SUBDIR = {
|
|
|
18877
19053
|
};
|
|
18878
19054
|
async function hasLinkableContext(member) {
|
|
18879
19055
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
18880
|
-
if (await
|
|
19056
|
+
if (await fs64.pathExists(path88.join(member.absPath, ".claude", subdir))) return true;
|
|
18881
19057
|
}
|
|
18882
19058
|
return false;
|
|
18883
19059
|
}
|
|
18884
19060
|
async function collectSkills(member) {
|
|
18885
19061
|
const dir = path88.join(member.absPath, ".claude", "skills");
|
|
18886
|
-
if (!await
|
|
18887
|
-
const names = await
|
|
19062
|
+
if (!await fs64.pathExists(dir)) return [];
|
|
19063
|
+
const names = await fs64.readdir(dir);
|
|
18888
19064
|
const out = [];
|
|
18889
19065
|
for (const name of [...names].sort()) {
|
|
18890
19066
|
const abs = path88.join(dir, name);
|
|
18891
|
-
const stat = await
|
|
19067
|
+
const stat = await fs64.lstat(abs);
|
|
18892
19068
|
if (!stat.isDirectory()) continue;
|
|
18893
|
-
if (!await
|
|
19069
|
+
if (!await fs64.pathExists(path88.join(abs, "SKILL.md"))) continue;
|
|
18894
19070
|
out.push({ type: "skill", name, sourceRelPath: path88.posix.join(".claude", "skills", name) });
|
|
18895
19071
|
}
|
|
18896
19072
|
return out;
|
|
@@ -18898,13 +19074,13 @@ async function collectSkills(member) {
|
|
|
18898
19074
|
async function collectFlatMarkdown(member, type) {
|
|
18899
19075
|
const subdir = ASSET_SUBDIR[type];
|
|
18900
19076
|
const dir = path88.join(member.absPath, ".claude", subdir);
|
|
18901
|
-
if (!await
|
|
18902
|
-
const names = await
|
|
19077
|
+
if (!await fs64.pathExists(dir)) return [];
|
|
19078
|
+
const names = await fs64.readdir(dir);
|
|
18903
19079
|
const out = [];
|
|
18904
19080
|
for (const fileName of [...names].sort()) {
|
|
18905
19081
|
if (!fileName.endsWith(".md")) continue;
|
|
18906
19082
|
const abs = path88.join(dir, fileName);
|
|
18907
|
-
if (!(await
|
|
19083
|
+
if (!(await fs64.lstat(abs)).isFile()) continue;
|
|
18908
19084
|
const name = fileName.slice(0, -".md".length);
|
|
18909
19085
|
out.push({ type, name, sourceRelPath: path88.posix.join(".claude", subdir, fileName) });
|
|
18910
19086
|
}
|
|
@@ -18938,7 +19114,7 @@ async function buildLinkPlan(members) {
|
|
|
18938
19114
|
const byKey = /* @__PURE__ */ new Map();
|
|
18939
19115
|
const membersById = new Map(members.map((m) => [m.id, m]));
|
|
18940
19116
|
for (const member of members) {
|
|
18941
|
-
if (!await
|
|
19117
|
+
if (!await fs65.pathExists(member.absPath)) {
|
|
18942
19118
|
skipped.push({ repo: member.id, reason: `not cloned locally (${member.absPath}) \u2014 skipped` });
|
|
18943
19119
|
continue;
|
|
18944
19120
|
}
|
|
@@ -19019,15 +19195,15 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
19019
19195
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
19020
19196
|
const destAbs = path90.join(workspaceRoot, destRel);
|
|
19021
19197
|
if (write) {
|
|
19022
|
-
await
|
|
19023
|
-
if (await
|
|
19024
|
-
await
|
|
19198
|
+
await fs66.ensureDir(path90.dirname(destAbs));
|
|
19199
|
+
if (await fs66.pathExists(destAbs)) await fs66.remove(destAbs);
|
|
19200
|
+
await fs66.copy(entry.sourceAbsPath, destAbs, {
|
|
19025
19201
|
overwrite: true,
|
|
19026
19202
|
errorOnExist: false,
|
|
19027
19203
|
dereference: false,
|
|
19028
19204
|
filter: (s) => {
|
|
19029
19205
|
try {
|
|
19030
|
-
return !
|
|
19206
|
+
return !fs66.lstatSync(s).isSymbolicLink();
|
|
19031
19207
|
} catch {
|
|
19032
19208
|
return false;
|
|
19033
19209
|
}
|
|
@@ -19056,7 +19232,7 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
19056
19232
|
warn(`Refusing to remove linked context path outside the workspace root: ${old.path}`);
|
|
19057
19233
|
continue;
|
|
19058
19234
|
}
|
|
19059
|
-
if (await
|
|
19235
|
+
if (await fs66.pathExists(abs)) await fs66.remove(abs);
|
|
19060
19236
|
}
|
|
19061
19237
|
}
|
|
19062
19238
|
if (write) {
|
|
@@ -19112,7 +19288,7 @@ import path92 from "node:path";
|
|
|
19112
19288
|
|
|
19113
19289
|
// src/claude/write-workspace-claude-md.ts
|
|
19114
19290
|
import path91 from "node:path";
|
|
19115
|
-
import
|
|
19291
|
+
import fs67 from "fs-extra";
|
|
19116
19292
|
init_fs();
|
|
19117
19293
|
init_logger();
|
|
19118
19294
|
init_paths();
|
|
@@ -19140,8 +19316,8 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
19140
19316
|
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path91.join(workspaceRoot, "CLAUDE.md");
|
|
19141
19317
|
const say = opts.quiet ? () => {
|
|
19142
19318
|
} : log;
|
|
19143
|
-
const existed = await
|
|
19144
|
-
const prev = existed ? await
|
|
19319
|
+
const existed = await fs67.pathExists(filePath);
|
|
19320
|
+
const prev = existed ? await fs67.readFile(filePath, "utf8") : "";
|
|
19145
19321
|
const next = opts.collision ? `${block}
|
|
19146
19322
|
` : injectHausBlock(prev, block, WORKSPACE_SENTINELS);
|
|
19147
19323
|
const printable = displayPath(workspaceRoot, filePath);
|
|
@@ -19423,7 +19599,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
19423
19599
|
// src/commands/workspace/undo.ts
|
|
19424
19600
|
import { existsSync as existsSync12, statSync as statSync2 } from "node:fs";
|
|
19425
19601
|
import path93 from "node:path";
|
|
19426
|
-
import
|
|
19602
|
+
import fs68 from "fs-extra";
|
|
19427
19603
|
init_fs();
|
|
19428
19604
|
init_logger();
|
|
19429
19605
|
init_paths();
|
|
@@ -19474,7 +19650,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
19474
19650
|
warn(`Refusing to remove linked context path outside the workspace root: ${entry.path}`);
|
|
19475
19651
|
continue;
|
|
19476
19652
|
}
|
|
19477
|
-
if (await
|
|
19653
|
+
if (await fs68.pathExists(abs)) {
|
|
19478
19654
|
log(`[dry-run] Would remove ${entry.path} (linked ${entry.type} from ${entry.repo})`);
|
|
19479
19655
|
}
|
|
19480
19656
|
}
|
|
@@ -19484,11 +19660,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
19484
19660
|
hausPath(workspaceRoot, "WORKSPACE.md")
|
|
19485
19661
|
];
|
|
19486
19662
|
for (const abs of workspaceArtifacts) {
|
|
19487
|
-
if (await
|
|
19663
|
+
if (await fs68.pathExists(abs)) {
|
|
19488
19664
|
log(`[dry-run] Would remove ${path93.relative(workspaceRoot, abs)}`);
|
|
19489
19665
|
}
|
|
19490
19666
|
}
|
|
19491
|
-
if (!await
|
|
19667
|
+
if (!await fs68.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
|
|
19492
19668
|
const claudeMd = await readText(path93.join(workspaceRoot, "CLAUDE.md"));
|
|
19493
19669
|
if (claudeMd?.includes(WORKSPACE_BLOCK_BEGIN) || claudeMd && hasLegacyWorkspaceBlock(claudeMd)) {
|
|
19494
19670
|
log("[dry-run] Would remove the workspace haus import block from CLAUDE.md");
|
|
@@ -19528,26 +19704,26 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
19528
19704
|
warn(`Refusing to remove linked context path outside the workspace root: ${entry.path}`);
|
|
19529
19705
|
continue;
|
|
19530
19706
|
}
|
|
19531
|
-
if (await
|
|
19532
|
-
await
|
|
19707
|
+
if (await fs68.pathExists(abs)) {
|
|
19708
|
+
await fs68.remove(abs);
|
|
19533
19709
|
log(`Removed ${entry.path} (linked ${entry.type} from ${entry.repo})`);
|
|
19534
19710
|
}
|
|
19535
19711
|
}
|
|
19536
19712
|
for (const rel of WORKSPACE_AGGREGATE_FILES) {
|
|
19537
19713
|
const abs = hausPath(workspaceRoot, rel);
|
|
19538
|
-
if (await
|
|
19539
|
-
await
|
|
19714
|
+
if (await fs68.pathExists(abs)) {
|
|
19715
|
+
await fs68.remove(abs);
|
|
19540
19716
|
log(`Removed ${path93.relative(workspaceRoot, abs)}`);
|
|
19541
19717
|
}
|
|
19542
19718
|
}
|
|
19543
19719
|
const manifestFile = manifestPath(workspaceRoot);
|
|
19544
|
-
if (await
|
|
19545
|
-
await
|
|
19720
|
+
if (await fs68.pathExists(manifestFile)) {
|
|
19721
|
+
await fs68.remove(manifestFile);
|
|
19546
19722
|
log(`Removed ${path93.relative(workspaceRoot, manifestFile)}`);
|
|
19547
19723
|
}
|
|
19548
19724
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
19549
|
-
if (await
|
|
19550
|
-
await
|
|
19725
|
+
if (await fs68.pathExists(workspaceMdPath)) {
|
|
19726
|
+
await fs68.remove(workspaceMdPath);
|
|
19551
19727
|
log(`Removed ${path93.relative(workspaceRoot, workspaceMdPath)}`);
|
|
19552
19728
|
} else {
|
|
19553
19729
|
const claudeMdPath = path93.join(workspaceRoot, "CLAUDE.md");
|
|
@@ -19559,19 +19735,19 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
19559
19735
|
end: WORKSPACE_BLOCK_END
|
|
19560
19736
|
});
|
|
19561
19737
|
if (next.length === 0) {
|
|
19562
|
-
await
|
|
19738
|
+
await fs68.remove(claudeMdPath);
|
|
19563
19739
|
log("Removed CLAUDE.md (only contained the workspace haus import block).");
|
|
19564
19740
|
} else {
|
|
19565
|
-
await
|
|
19741
|
+
await fs68.writeFile(claudeMdPath, next, "utf8");
|
|
19566
19742
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
19567
19743
|
}
|
|
19568
19744
|
} else if (prev && hasLegacyWorkspaceBlock(prev)) {
|
|
19569
19745
|
const next = stripHausBlock(prev, { begin: BLOCK_BEGIN, end: BLOCK_END });
|
|
19570
19746
|
if (next.length === 0) {
|
|
19571
|
-
await
|
|
19747
|
+
await fs68.remove(claudeMdPath);
|
|
19572
19748
|
log("Removed CLAUDE.md (only contained the pre-upgrade workspace haus import block).");
|
|
19573
19749
|
} else {
|
|
19574
|
-
await
|
|
19750
|
+
await fs68.writeFile(claudeMdPath, next, "utf8");
|
|
19575
19751
|
log(
|
|
19576
19752
|
"Removed pre-upgrade workspace haus import block from CLAUDE.md (user content preserved)."
|
|
19577
19753
|
);
|
|
@@ -19743,6 +19919,9 @@ program.command("scan").description(
|
|
|
19743
19919
|
program.command("recommend").description("Score catalog items against the scanned context and write recommendation.json").option("--json").option(
|
|
19744
19920
|
"--include <ids...>",
|
|
19745
19921
|
"Force opt-in catalog items into the recommendation as manual selections (space- or comma-separated ids)"
|
|
19922
|
+
).option(
|
|
19923
|
+
"--declare-role <roles...>",
|
|
19924
|
+
"Declare a repo role (persisted in .haus-workflow/haus.config.json declaredRoles) so role-gated opt-in items are recommended"
|
|
19746
19925
|
).action(runRecommend);
|
|
19747
19926
|
program.command("doctor").description(
|
|
19748
19927
|
"Validate project setup: hooks contract, managed files, cache freshness, CLI version"
|
|
@@ -19841,7 +20020,10 @@ guard.command("bash").description("Block dangerous shell commands (run by the Cl
|
|
|
19841
20020
|
function registerDecisionsCommand(name) {
|
|
19842
20021
|
const cmd = program.command(name).description("Architecture decision records (ADR) gate and drafts");
|
|
19843
20022
|
cmd.command("check").option("--staged", "Check staged changes only").option("--range <range>", "Git revision range (e.g. origin/main..HEAD)").action((opts) => runDecisions("check", void 0, opts));
|
|
19844
|
-
cmd.command("suggest").option("--from-hook", "Emit JSON for Claude Code Stop hook").option(
|
|
20023
|
+
cmd.command("suggest").option("--from-hook", "Emit JSON for Claude Code Stop hook").option(
|
|
20024
|
+
"--title <title>",
|
|
20025
|
+
"Draft title; with a title the draft is printed even when the diff is not decision-worthy"
|
|
20026
|
+
).action(
|
|
19845
20027
|
(opts) => runDecisions("suggest", void 0, opts)
|
|
19846
20028
|
);
|
|
19847
20029
|
cmd.command("next-number").action(() => runDecisions("next-number"));
|