@haus-tech/haus-workflow 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/cli.js +613 -310
- package/library/catalog/manifest.json +4 -4
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -60,7 +60,7 @@ var init_exec = __esm({
|
|
|
60
60
|
|
|
61
61
|
// src/cli.ts
|
|
62
62
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
63
|
-
import
|
|
63
|
+
import path71 from "node:path";
|
|
64
64
|
import { Command } from "commander";
|
|
65
65
|
|
|
66
66
|
// src/commands/apply.ts
|
|
@@ -2103,8 +2103,8 @@ function buildDenyRules() {
|
|
|
2103
2103
|
for (const command of DENY_COMMANDS) {
|
|
2104
2104
|
rules.push(`Bash(${command}:*)`);
|
|
2105
2105
|
}
|
|
2106
|
-
for (const
|
|
2107
|
-
const pattern = DENY_DIRS.has(
|
|
2106
|
+
for (const path72 of DENY_PATHS) {
|
|
2107
|
+
const pattern = DENY_DIRS.has(path72) ? `${path72}/**` : path72;
|
|
2108
2108
|
for (const tool of FILE_TOOLS) {
|
|
2109
2109
|
rules.push(`${tool}(${pattern})`);
|
|
2110
2110
|
}
|
|
@@ -4151,19 +4151,133 @@ async function runCatalogAudit() {
|
|
|
4151
4151
|
}
|
|
4152
4152
|
|
|
4153
4153
|
// src/commands/doctor.ts
|
|
4154
|
-
import
|
|
4154
|
+
import path38 from "node:path";
|
|
4155
4155
|
import fs29 from "fs-extra";
|
|
4156
4156
|
|
|
4157
4157
|
// src/decisions/doctor.ts
|
|
4158
|
-
import
|
|
4158
|
+
import path29 from "node:path";
|
|
4159
4159
|
import fs26 from "fs-extra";
|
|
4160
4160
|
|
|
4161
4161
|
// src/decisions/check.ts
|
|
4162
|
-
import
|
|
4162
|
+
import path28 from "node:path";
|
|
4163
4163
|
import fs25 from "fs-extra";
|
|
4164
4164
|
|
|
4165
|
+
// src/decisions/dep-bump.ts
|
|
4166
|
+
var DEP_SECTIONS = [
|
|
4167
|
+
"dependencies",
|
|
4168
|
+
"devDependencies",
|
|
4169
|
+
"peerDependencies",
|
|
4170
|
+
"optionalDependencies"
|
|
4171
|
+
];
|
|
4172
|
+
function isRecord(value) {
|
|
4173
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
4174
|
+
}
|
|
4175
|
+
function extractDepMap(pkg) {
|
|
4176
|
+
const map = /* @__PURE__ */ new Map();
|
|
4177
|
+
if (!isRecord(pkg)) return map;
|
|
4178
|
+
for (const section of DEP_SECTIONS) {
|
|
4179
|
+
const block = pkg[section];
|
|
4180
|
+
if (!isRecord(block)) continue;
|
|
4181
|
+
for (const [name, ver] of Object.entries(block)) {
|
|
4182
|
+
if (typeof ver === "string") map.set(`${section}:${name}`, ver);
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
return map;
|
|
4186
|
+
}
|
|
4187
|
+
function stripDepSections(pkg) {
|
|
4188
|
+
const out = { ...pkg };
|
|
4189
|
+
for (const section of DEP_SECTIONS) delete out[section];
|
|
4190
|
+
return out;
|
|
4191
|
+
}
|
|
4192
|
+
function mapsEqualKeys(a, b) {
|
|
4193
|
+
if (a.size !== b.size) return false;
|
|
4194
|
+
for (const k of a.keys()) if (!b.has(k)) return false;
|
|
4195
|
+
return true;
|
|
4196
|
+
}
|
|
4197
|
+
function isDependencyBumpOnly(beforeText, afterText) {
|
|
4198
|
+
let before;
|
|
4199
|
+
let after;
|
|
4200
|
+
try {
|
|
4201
|
+
before = JSON.parse(beforeText);
|
|
4202
|
+
after = JSON.parse(afterText);
|
|
4203
|
+
} catch {
|
|
4204
|
+
return false;
|
|
4205
|
+
}
|
|
4206
|
+
if (!isRecord(before) || !isRecord(after)) return false;
|
|
4207
|
+
const beforeDeps = extractDepMap(before);
|
|
4208
|
+
const afterDeps = extractDepMap(after);
|
|
4209
|
+
if (beforeDeps.size === 0 && afterDeps.size === 0) return false;
|
|
4210
|
+
if (!mapsEqualKeys(beforeDeps, afterDeps)) return false;
|
|
4211
|
+
if (JSON.stringify(stripDepSections(before)) !== JSON.stringify(stripDepSections(after))) {
|
|
4212
|
+
return false;
|
|
4213
|
+
}
|
|
4214
|
+
for (const [key, ver] of beforeDeps) {
|
|
4215
|
+
if (afterDeps.get(key) !== ver) return true;
|
|
4216
|
+
}
|
|
4217
|
+
return false;
|
|
4218
|
+
}
|
|
4219
|
+
|
|
4165
4220
|
// src/decisions/diff.ts
|
|
4166
4221
|
init_exec();
|
|
4222
|
+
import path25 from "node:path";
|
|
4223
|
+
function isPackageJsonPath(file) {
|
|
4224
|
+
return file === "package.json" || file.endsWith("/package.json");
|
|
4225
|
+
}
|
|
4226
|
+
async function readBlob(root, spec) {
|
|
4227
|
+
const result = await runGit(["show", spec], { cwd: root });
|
|
4228
|
+
return result.exitCode === 0 ? result.stdout : null;
|
|
4229
|
+
}
|
|
4230
|
+
function parseDiffRange(range) {
|
|
4231
|
+
const tripleIdx = range.indexOf("...");
|
|
4232
|
+
if (tripleIdx !== -1) {
|
|
4233
|
+
return {
|
|
4234
|
+
left: range.slice(0, tripleIdx) || "HEAD",
|
|
4235
|
+
right: range.slice(tripleIdx + 3) || "HEAD",
|
|
4236
|
+
triple: true
|
|
4237
|
+
};
|
|
4238
|
+
}
|
|
4239
|
+
const idx = range.indexOf("..");
|
|
4240
|
+
if (idx !== -1) {
|
|
4241
|
+
return {
|
|
4242
|
+
left: range.slice(0, idx) || "HEAD",
|
|
4243
|
+
right: range.slice(idx + 2) || "HEAD",
|
|
4244
|
+
triple: false
|
|
4245
|
+
};
|
|
4246
|
+
}
|
|
4247
|
+
return null;
|
|
4248
|
+
}
|
|
4249
|
+
async function getCurrentBranch(root) {
|
|
4250
|
+
const result = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: root });
|
|
4251
|
+
if (result.exitCode !== 0) return void 0;
|
|
4252
|
+
const name = result.stdout.trim();
|
|
4253
|
+
if (name && name !== "HEAD") return name;
|
|
4254
|
+
const gha = process.env.GITHUB_HEAD_REF?.trim();
|
|
4255
|
+
if (gha) return gha;
|
|
4256
|
+
return void 0;
|
|
4257
|
+
}
|
|
4258
|
+
async function resolvePackageJsonPair(root, file, opts) {
|
|
4259
|
+
const empty = "{}";
|
|
4260
|
+
if (opts.staged) {
|
|
4261
|
+
const before2 = await readBlob(root, `HEAD:${file}`) ?? empty;
|
|
4262
|
+
const after2 = await readBlob(root, `:${file}`) ?? empty;
|
|
4263
|
+
return { before: before2, after: after2 };
|
|
4264
|
+
}
|
|
4265
|
+
const range = opts.range ?? "HEAD";
|
|
4266
|
+
const parsed = parseDiffRange(range);
|
|
4267
|
+
if (parsed) {
|
|
4268
|
+
let leftRev = parsed.left;
|
|
4269
|
+
if (parsed.triple) {
|
|
4270
|
+
const mb = await runGit(["merge-base", parsed.left, parsed.right], { cwd: root });
|
|
4271
|
+
leftRev = mb.exitCode === 0 ? mb.stdout.trim() : parsed.left;
|
|
4272
|
+
}
|
|
4273
|
+
const before2 = await readBlob(root, `${leftRev}:${file}`) ?? empty;
|
|
4274
|
+
const after2 = await readBlob(root, `${parsed.right}:${file}`) ?? empty;
|
|
4275
|
+
return { before: before2, after: after2 };
|
|
4276
|
+
}
|
|
4277
|
+
const before = await readBlob(root, `${range}:${file}`) ?? empty;
|
|
4278
|
+
const after = await readText(path25.join(root, file)) ?? empty;
|
|
4279
|
+
return { before, after };
|
|
4280
|
+
}
|
|
4167
4281
|
async function collectDiffStats(root, opts) {
|
|
4168
4282
|
const gitArgs = opts.staged ? ["diff", "--cached", "--name-only"] : ["diff", "--name-only", opts.range ?? "HEAD"];
|
|
4169
4283
|
const names = await runGit(gitArgs, { cwd: root });
|
|
@@ -4180,9 +4294,23 @@ async function collectDiffStats(root, opts) {
|
|
|
4180
4294
|
if (!Number.isNaN(added)) linesAdded += added;
|
|
4181
4295
|
if (!Number.isNaN(removed)) linesRemoved += removed;
|
|
4182
4296
|
}
|
|
4183
|
-
|
|
4297
|
+
const packageJsonPairs = {};
|
|
4298
|
+
for (const file of files) {
|
|
4299
|
+
if (!isPackageJsonPath(file)) continue;
|
|
4300
|
+
packageJsonPairs[file] = await resolvePackageJsonPair(root, file, opts);
|
|
4301
|
+
}
|
|
4302
|
+
return {
|
|
4303
|
+
files,
|
|
4304
|
+
linesAdded,
|
|
4305
|
+
linesRemoved,
|
|
4306
|
+
...Object.keys(packageJsonPairs).length > 0 ? { packageJsonPairs } : {}
|
|
4307
|
+
};
|
|
4184
4308
|
}
|
|
4185
4309
|
|
|
4310
|
+
// src/decisions/local-exemptions.ts
|
|
4311
|
+
var LOCAL_EXEMPT_GLOBS = ["library/catalog/**"];
|
|
4312
|
+
var LOCAL_EXEMPT_BRANCH_PATTERN = /^chore\/sync-catalog/;
|
|
4313
|
+
|
|
4186
4314
|
// src/decisions/match.ts
|
|
4187
4315
|
function segmentToRegex(segment) {
|
|
4188
4316
|
if (segment === "**") return ".*";
|
|
@@ -4210,26 +4338,29 @@ function matchesAnyGlob(filePath, patterns) {
|
|
|
4210
4338
|
return patterns.some((p) => matchesPathGlob(filePath, p));
|
|
4211
4339
|
}
|
|
4212
4340
|
function matchesAnyRegex(filePath, patterns) {
|
|
4341
|
+
return firstMatchingRegex(filePath, patterns) != null;
|
|
4342
|
+
}
|
|
4343
|
+
function firstMatchingRegex(filePath, patterns) {
|
|
4213
4344
|
const norm = filePath.replace(/\\/g, "/");
|
|
4214
|
-
return patterns.
|
|
4345
|
+
return patterns.find((p) => new RegExp(p).test(norm));
|
|
4215
4346
|
}
|
|
4216
4347
|
|
|
4217
4348
|
// src/decisions/paths.ts
|
|
4218
|
-
import
|
|
4349
|
+
import path26 from "node:path";
|
|
4219
4350
|
import fs23 from "fs-extra";
|
|
4220
4351
|
async function resolveDecisionsDir(root) {
|
|
4221
|
-
const primary =
|
|
4352
|
+
const primary = path26.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
4222
4353
|
if (await fs23.pathExists(primary)) return primary;
|
|
4223
|
-
const legacy =
|
|
4354
|
+
const legacy = path26.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
4224
4355
|
if (await fs23.pathExists(legacy)) return legacy;
|
|
4225
4356
|
return primary;
|
|
4226
4357
|
}
|
|
4227
4358
|
function relativeDecisionPath(decisionsDir, root) {
|
|
4228
|
-
return
|
|
4359
|
+
return path26.relative(root, decisionsDir).replace(/\\/g, "/");
|
|
4229
4360
|
}
|
|
4230
4361
|
|
|
4231
4362
|
// src/decisions/validate.ts
|
|
4232
|
-
import
|
|
4363
|
+
import path27 from "node:path";
|
|
4233
4364
|
import fs24 from "fs-extra";
|
|
4234
4365
|
function validateDecisionContent(content2) {
|
|
4235
4366
|
const errors = [];
|
|
@@ -4259,14 +4390,14 @@ function decisionNumberFromFile(name) {
|
|
|
4259
4390
|
return match?.[1] ?? null;
|
|
4260
4391
|
}
|
|
4261
4392
|
async function validateIndexSync(decisionsDir, decisionFiles) {
|
|
4262
|
-
const readmePath =
|
|
4393
|
+
const readmePath = path27.join(decisionsDir, "README.md");
|
|
4263
4394
|
if (!await fs24.pathExists(readmePath)) {
|
|
4264
4395
|
return ["docs/decisions/README.md is missing"];
|
|
4265
4396
|
}
|
|
4266
4397
|
const readme = await readText(readmePath) ?? "";
|
|
4267
4398
|
const errors = [];
|
|
4268
4399
|
for (const file of decisionFiles) {
|
|
4269
|
-
const num = decisionNumberFromFile(
|
|
4400
|
+
const num = decisionNumberFromFile(path27.basename(file));
|
|
4270
4401
|
if (num && !indexReferencesNumber(readme, num)) {
|
|
4271
4402
|
errors.push(`README.md missing index row for ADR-${num}`);
|
|
4272
4403
|
}
|
|
@@ -4275,8 +4406,35 @@ async function validateIndexSync(decisionsDir, decisionFiles) {
|
|
|
4275
4406
|
}
|
|
4276
4407
|
|
|
4277
4408
|
// src/decisions/check.ts
|
|
4409
|
+
var MANIFEST_LOCK_BASENAMES = /* @__PURE__ */ new Set([
|
|
4410
|
+
"package.json",
|
|
4411
|
+
"yarn.lock",
|
|
4412
|
+
"pnpm-lock.yaml",
|
|
4413
|
+
"package-lock.json"
|
|
4414
|
+
]);
|
|
4278
4415
|
function isExempt(file) {
|
|
4279
|
-
return matchesAnyGlob(file, DECISIONS_TRIGGERS.exemptGlobs);
|
|
4416
|
+
return matchesAnyGlob(file, DECISIONS_TRIGGERS.exemptGlobs) || matchesAnyGlob(file, [...LOCAL_EXEMPT_GLOBS]);
|
|
4417
|
+
}
|
|
4418
|
+
function isManifestOrLockfile(file) {
|
|
4419
|
+
const base = file.split("/").pop() ?? file;
|
|
4420
|
+
return MANIFEST_LOCK_BASENAMES.has(base);
|
|
4421
|
+
}
|
|
4422
|
+
function isPackageJsonPath2(file) {
|
|
4423
|
+
return file === "package.json" || file.endsWith("/package.json");
|
|
4424
|
+
}
|
|
4425
|
+
function isPureDependencyBumpDiff(stats) {
|
|
4426
|
+
const relevant = stats.files.filter((f) => !isExempt(f));
|
|
4427
|
+
if (relevant.length === 0) return false;
|
|
4428
|
+
if (relevant.some((f) => !isManifestOrLockfile(f))) return false;
|
|
4429
|
+
const pkgJsons = relevant.filter(isPackageJsonPath2);
|
|
4430
|
+
if (pkgJsons.length === 0) {
|
|
4431
|
+
return true;
|
|
4432
|
+
}
|
|
4433
|
+
const pairs = stats.packageJsonPairs ?? {};
|
|
4434
|
+
return pkgJsons.every((f) => {
|
|
4435
|
+
const pair = pairs[f];
|
|
4436
|
+
return pair != null && isDependencyBumpOnly(pair.before, pair.after);
|
|
4437
|
+
});
|
|
4280
4438
|
}
|
|
4281
4439
|
function countTopLevelDirs(files) {
|
|
4282
4440
|
const dirs = /* @__PURE__ */ new Set();
|
|
@@ -4286,21 +4444,35 @@ function countTopLevelDirs(files) {
|
|
|
4286
4444
|
}
|
|
4287
4445
|
return dirs.size;
|
|
4288
4446
|
}
|
|
4289
|
-
function
|
|
4290
|
-
|
|
4291
|
-
|
|
4447
|
+
function firstMatchingGlob(file, globs) {
|
|
4448
|
+
return globs.find((g) => matchesPathGlob(file, g));
|
|
4449
|
+
}
|
|
4450
|
+
function explainDecisionTrigger(stats) {
|
|
4451
|
+
let relevant = stats.files.filter((f) => !isExempt(f));
|
|
4452
|
+
if (relevant.length === 0) return [];
|
|
4453
|
+
if (isPureDependencyBumpDiff(stats)) {
|
|
4454
|
+
relevant = relevant.filter((f) => !isManifestOrLockfile(f));
|
|
4455
|
+
if (relevant.length === 0) return [];
|
|
4456
|
+
}
|
|
4292
4457
|
for (const file of relevant) {
|
|
4293
|
-
|
|
4294
|
-
if (
|
|
4458
|
+
const glob = firstMatchingGlob(file, DECISIONS_TRIGGERS.pathGlobs);
|
|
4459
|
+
if (glob) {
|
|
4460
|
+
return [`matched pathGlobs: ${glob}`];
|
|
4461
|
+
}
|
|
4462
|
+
const regex = firstMatchingRegex(file, DECISIONS_TRIGGERS.pathRegex);
|
|
4463
|
+
if (regex) {
|
|
4464
|
+
return [`matched pathRegex: ${regex}`];
|
|
4465
|
+
}
|
|
4295
4466
|
}
|
|
4296
4467
|
const totalLines = stats.linesAdded + stats.linesRemoved;
|
|
4297
|
-
|
|
4298
|
-
|
|
4468
|
+
const dirs = countTopLevelDirs(relevant);
|
|
4469
|
+
if (relevant.length >= DECISIONS_TRIGGERS.minFilesChanged && dirs >= 2) {
|
|
4470
|
+
return [`size threshold: ${relevant.length} files across ${dirs} top-level dirs`];
|
|
4299
4471
|
}
|
|
4300
4472
|
if (totalLines >= DECISIONS_TRIGGERS.minLinesChanged && relevant.some((f) => !f.startsWith("docs/"))) {
|
|
4301
|
-
return
|
|
4473
|
+
return [`size threshold: ${totalLines} lines changed (non-docs)`];
|
|
4302
4474
|
}
|
|
4303
|
-
return
|
|
4475
|
+
return [];
|
|
4304
4476
|
}
|
|
4305
4477
|
function isSecurityTriggered(stats) {
|
|
4306
4478
|
const relevant = stats.files.filter((f) => !isExempt(f));
|
|
@@ -4320,26 +4492,27 @@ async function checkSatisfaction(input2) {
|
|
|
4320
4492
|
const errors = [];
|
|
4321
4493
|
const decisionsPrefix = `${input2.decisionsRel}/`;
|
|
4322
4494
|
const newDecisionFiles = input2.changedFiles.filter(
|
|
4323
|
-
(f) => f.startsWith(decisionsPrefix) && isDecisionFileName(
|
|
4495
|
+
(f) => f.startsWith(decisionsPrefix) && isDecisionFileName(path28.basename(f))
|
|
4324
4496
|
);
|
|
4325
4497
|
if (newDecisionFiles.length === 0) {
|
|
4326
4498
|
errors.push(`no new or updated decision under ${input2.decisionsRel}/`);
|
|
4327
4499
|
return errors;
|
|
4328
4500
|
}
|
|
4329
|
-
const decisionsDir =
|
|
4501
|
+
const decisionsDir = path28.join(input2.root, input2.decisionsRel);
|
|
4330
4502
|
for (const rel of newDecisionFiles) {
|
|
4331
|
-
const abs =
|
|
4503
|
+
const abs = path28.join(input2.root, rel);
|
|
4332
4504
|
const content2 = await readText(abs) ?? "";
|
|
4333
4505
|
const valid = validateDecisionContent(content2);
|
|
4334
4506
|
if (!valid.ok) errors.push(...valid.errors.map((e) => `${rel}: ${e}`));
|
|
4335
4507
|
}
|
|
4336
4508
|
const indexErrors = await validateIndexSync(
|
|
4337
4509
|
decisionsDir,
|
|
4338
|
-
newDecisionFiles.map((f) =>
|
|
4510
|
+
newDecisionFiles.map((f) => path28.basename(f))
|
|
4339
4511
|
);
|
|
4340
4512
|
errors.push(...indexErrors);
|
|
4341
4513
|
return errors;
|
|
4342
4514
|
}
|
|
4515
|
+
var WAYS_OUT = "Write an ADR under docs/decisions/ (haus decisions suggest), or use [adr-skip] for non-security changes.";
|
|
4343
4516
|
async function runDecisionsCheck(root, opts = {}) {
|
|
4344
4517
|
const reasons = [];
|
|
4345
4518
|
if (await readGateMode(root) === "off") {
|
|
@@ -4351,8 +4524,18 @@ async function runDecisionsCheck(root, opts = {}) {
|
|
|
4351
4524
|
};
|
|
4352
4525
|
}
|
|
4353
4526
|
const stats = await collectDiffStats(root, opts);
|
|
4354
|
-
const triggered = isDecisionTriggered(stats);
|
|
4355
4527
|
const securityTriggered = isSecurityTriggered(stats);
|
|
4528
|
+
const branch = await getCurrentBranch(root);
|
|
4529
|
+
if (branch != null && LOCAL_EXEMPT_BRANCH_PATTERN.test(branch) && !securityTriggered) {
|
|
4530
|
+
return {
|
|
4531
|
+
triggered: false,
|
|
4532
|
+
satisfied: true,
|
|
4533
|
+
reasons: ["catalog-sync branch \u2014 exempt"],
|
|
4534
|
+
securityTriggered: false
|
|
4535
|
+
};
|
|
4536
|
+
}
|
|
4537
|
+
const triggerReasons = explainDecisionTrigger(stats);
|
|
4538
|
+
const triggered = triggerReasons.length > 0;
|
|
4356
4539
|
if (!triggered) {
|
|
4357
4540
|
return {
|
|
4358
4541
|
triggered: false,
|
|
@@ -4361,6 +4544,7 @@ async function runDecisionsCheck(root, opts = {}) {
|
|
|
4361
4544
|
securityTriggered
|
|
4362
4545
|
};
|
|
4363
4546
|
}
|
|
4547
|
+
reasons.push(...triggerReasons);
|
|
4364
4548
|
const skipText = `${opts.prBody ?? ""}
|
|
4365
4549
|
${opts.commitMessages ?? ""}`;
|
|
4366
4550
|
if (hasSkipToken(skipText) && !securityTriggered) {
|
|
@@ -4379,7 +4563,7 @@ ${opts.commitMessages ?? ""}`;
|
|
|
4379
4563
|
changedFiles: stats.files
|
|
4380
4564
|
});
|
|
4381
4565
|
if (satisfactionErrors.length > 0 && decisionsRel !== legacyRel) {
|
|
4382
|
-
const legacyExists = await fs25.pathExists(
|
|
4566
|
+
const legacyExists = await fs25.pathExists(path28.join(root, legacyRel));
|
|
4383
4567
|
if (legacyExists && !await fs25.pathExists(decisionsDir)) {
|
|
4384
4568
|
satisfactionErrors = await checkSatisfaction({
|
|
4385
4569
|
root,
|
|
@@ -4389,14 +4573,16 @@ ${opts.commitMessages ?? ""}`;
|
|
|
4389
4573
|
}
|
|
4390
4574
|
}
|
|
4391
4575
|
if (satisfactionErrors.length === 0) {
|
|
4576
|
+
reasons.push("decision record present");
|
|
4392
4577
|
return {
|
|
4393
4578
|
triggered: true,
|
|
4394
4579
|
satisfied: true,
|
|
4395
|
-
reasons
|
|
4580
|
+
reasons,
|
|
4396
4581
|
securityTriggered
|
|
4397
4582
|
};
|
|
4398
4583
|
}
|
|
4399
4584
|
reasons.push(...satisfactionErrors);
|
|
4585
|
+
reasons.push(WAYS_OUT);
|
|
4400
4586
|
return { triggered: true, satisfied: false, reasons, securityTriggered };
|
|
4401
4587
|
}
|
|
4402
4588
|
async function listDecisionNumbers(decisionsDir) {
|
|
@@ -4408,8 +4594,8 @@ async function listDecisionNumbers(decisionsDir) {
|
|
|
4408
4594
|
// src/decisions/doctor.ts
|
|
4409
4595
|
async function auditDecisionsLayout(root) {
|
|
4410
4596
|
const findings = [];
|
|
4411
|
-
const primary =
|
|
4412
|
-
const legacy =
|
|
4597
|
+
const primary = path29.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
4598
|
+
const legacy = path29.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
4413
4599
|
const hasPrimary = await fs26.pathExists(primary);
|
|
4414
4600
|
const hasLegacy = await fs26.pathExists(legacy);
|
|
4415
4601
|
if (!hasPrimary && hasLegacy) {
|
|
@@ -4424,7 +4610,7 @@ async function auditDecisionsLayout(root) {
|
|
|
4424
4610
|
if (!hasPrimary) return findings;
|
|
4425
4611
|
const decisionsDir = await resolveDecisionsDir(root);
|
|
4426
4612
|
const rel = relativeDecisionPath(decisionsDir, root);
|
|
4427
|
-
const readmePath =
|
|
4613
|
+
const readmePath = path29.join(decisionsDir, "README.md");
|
|
4428
4614
|
if (!await fs26.pathExists(readmePath)) {
|
|
4429
4615
|
findings.push({
|
|
4430
4616
|
level: "advisory",
|
|
@@ -4449,7 +4635,7 @@ async function auditDecisionsLayout(root) {
|
|
|
4449
4635
|
});
|
|
4450
4636
|
}
|
|
4451
4637
|
}
|
|
4452
|
-
const claude = await readText(
|
|
4638
|
+
const claude = await readText(path29.join(root, "CLAUDE.md"));
|
|
4453
4639
|
if (claude && !claude.includes(`@${rel}/README.md`)) {
|
|
4454
4640
|
findings.push({
|
|
4455
4641
|
level: "advisory",
|
|
@@ -4480,11 +4666,11 @@ async function auditDecisionsLayout(root) {
|
|
|
4480
4666
|
}
|
|
4481
4667
|
|
|
4482
4668
|
// src/scanner/read-context.ts
|
|
4483
|
-
import
|
|
4669
|
+
import path37 from "node:path";
|
|
4484
4670
|
import fs28 from "fs-extra";
|
|
4485
4671
|
|
|
4486
4672
|
// src/commands/workspace/config.ts
|
|
4487
|
-
import
|
|
4673
|
+
import path30 from "node:path";
|
|
4488
4674
|
import YAML from "yaml";
|
|
4489
4675
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
4490
4676
|
function parseWorkspaceConfig(text) {
|
|
@@ -4507,11 +4693,11 @@ function parseWorkspaceConfig(text) {
|
|
|
4507
4693
|
};
|
|
4508
4694
|
}
|
|
4509
4695
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
4510
|
-
return parseWorkspaceConfig(await readText(
|
|
4696
|
+
return parseWorkspaceConfig(await readText(path30.join(workspaceRoot, WORKSPACE_FILE)));
|
|
4511
4697
|
}
|
|
4512
4698
|
|
|
4513
4699
|
// src/workspace/members.ts
|
|
4514
|
-
import
|
|
4700
|
+
import path31 from "node:path";
|
|
4515
4701
|
var REPOS_MANIFEST_FILE = "repos.manifest.json";
|
|
4516
4702
|
var REPOS_LOCAL_FILE = "repos.local.json";
|
|
4517
4703
|
var MemberConfigError = class extends Error {
|
|
@@ -4524,11 +4710,14 @@ var MemberConfigError = class extends Error {
|
|
|
4524
4710
|
};
|
|
4525
4711
|
function resolveAbsPath(workspaceRoot, folder, overrides) {
|
|
4526
4712
|
const override = overrides[folder];
|
|
4527
|
-
if (override) return
|
|
4528
|
-
return
|
|
4713
|
+
if (override) return path31.resolve(workspaceRoot, override);
|
|
4714
|
+
return path31.resolve(workspaceRoot, folder);
|
|
4715
|
+
}
|
|
4716
|
+
function isSelfMember(workspaceRoot, member) {
|
|
4717
|
+
return path31.resolve(member.absPath) === path31.resolve(workspaceRoot);
|
|
4529
4718
|
}
|
|
4530
4719
|
async function readPathOverrides(workspaceRoot) {
|
|
4531
|
-
const filePath =
|
|
4720
|
+
const filePath = path31.join(workspaceRoot, REPOS_LOCAL_FILE);
|
|
4532
4721
|
const result = await readJsonDetailed(filePath);
|
|
4533
4722
|
if (result.status === "missing") return {};
|
|
4534
4723
|
if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
|
|
@@ -4544,7 +4733,7 @@ async function readPathOverrides(workspaceRoot) {
|
|
|
4544
4733
|
return overrides;
|
|
4545
4734
|
}
|
|
4546
4735
|
async function readFromWorkspaceYaml(workspaceRoot, overrides) {
|
|
4547
|
-
const filePath =
|
|
4736
|
+
const filePath = path31.join(workspaceRoot, WORKSPACE_FILE);
|
|
4548
4737
|
const text = await readText(filePath);
|
|
4549
4738
|
if (text === void 0) return void 0;
|
|
4550
4739
|
const config = parseWorkspaceConfig(text);
|
|
@@ -4557,7 +4746,7 @@ async function readFromWorkspaceYaml(workspaceRoot, overrides) {
|
|
|
4557
4746
|
}));
|
|
4558
4747
|
}
|
|
4559
4748
|
async function readFromManifest(workspaceRoot, overrides) {
|
|
4560
|
-
const filePath =
|
|
4749
|
+
const filePath = path31.join(workspaceRoot, REPOS_MANIFEST_FILE);
|
|
4561
4750
|
const result = await readJsonDetailed(filePath);
|
|
4562
4751
|
if (result.status === "missing") return void 0;
|
|
4563
4752
|
if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
|
|
@@ -4592,23 +4781,23 @@ async function readMembers(rootInfo) {
|
|
|
4592
4781
|
const workspaceRoot = rootInfo.mainRoot;
|
|
4593
4782
|
const overrides = await readPathOverrides(workspaceRoot);
|
|
4594
4783
|
const fromYaml = await readFromWorkspaceYaml(workspaceRoot, overrides);
|
|
4595
|
-
if (fromYaml) return fromYaml;
|
|
4784
|
+
if (fromYaml) return fromYaml.filter((m) => !isSelfMember(workspaceRoot, m));
|
|
4596
4785
|
const fromManifest = await readFromManifest(workspaceRoot, overrides);
|
|
4597
|
-
if (fromManifest) return fromManifest;
|
|
4786
|
+
if (fromManifest) return fromManifest.filter((m) => !isSelfMember(workspaceRoot, m));
|
|
4598
4787
|
return [];
|
|
4599
4788
|
}
|
|
4600
4789
|
|
|
4601
4790
|
// src/scanner/scan-project.ts
|
|
4602
|
-
import
|
|
4791
|
+
import path36 from "node:path";
|
|
4603
4792
|
|
|
4604
4793
|
// src/utils/audit-checks.ts
|
|
4605
|
-
function
|
|
4794
|
+
function isRecord2(v) {
|
|
4606
4795
|
return typeof v === "object" && v !== null;
|
|
4607
4796
|
}
|
|
4608
4797
|
|
|
4609
4798
|
// src/utils/git-root.ts
|
|
4610
4799
|
init_exec();
|
|
4611
|
-
import
|
|
4800
|
+
import path32 from "node:path";
|
|
4612
4801
|
function fallback(start) {
|
|
4613
4802
|
return {
|
|
4614
4803
|
cwd: start,
|
|
@@ -4624,11 +4813,11 @@ function fallback(start) {
|
|
|
4624
4813
|
async function resolveAbsoluteGitPath(flag, cwd, repoRoot) {
|
|
4625
4814
|
const withFormat = await runGit(["rev-parse", "--path-format=absolute", flag], { cwd });
|
|
4626
4815
|
const raw = withFormat.exitCode === 0 ? withFormat.stdout.trim() : "";
|
|
4627
|
-
if (raw &&
|
|
4816
|
+
if (raw && path32.isAbsolute(raw)) return path32.resolve(raw);
|
|
4628
4817
|
const plain = await runGit(["rev-parse", flag], { cwd });
|
|
4629
4818
|
const plainRaw = plain.exitCode === 0 ? plain.stdout.trim() : "";
|
|
4630
4819
|
if (!plainRaw) return "";
|
|
4631
|
-
return
|
|
4820
|
+
return path32.isAbsolute(plainRaw) ? path32.resolve(plainRaw) : path32.resolve(repoRoot, plainRaw);
|
|
4632
4821
|
}
|
|
4633
4822
|
async function resolveRoots(start) {
|
|
4634
4823
|
const cwd = start ?? process.cwd();
|
|
@@ -4637,15 +4826,15 @@ async function resolveRoots(start) {
|
|
|
4637
4826
|
if (toplevel.exitCode !== 0) return fallback(cwd);
|
|
4638
4827
|
const toplevelRaw = toplevel.stdout.trim();
|
|
4639
4828
|
if (!toplevelRaw) return fallback(cwd);
|
|
4640
|
-
const repoRoot =
|
|
4829
|
+
const repoRoot = path32.resolve(toplevelRaw);
|
|
4641
4830
|
const [gitDir, gitCommonDir] = await Promise.all([
|
|
4642
4831
|
resolveAbsoluteGitPath("--git-dir", cwd, repoRoot),
|
|
4643
4832
|
resolveAbsoluteGitPath("--git-common-dir", cwd, repoRoot)
|
|
4644
4833
|
]);
|
|
4645
4834
|
if (!gitDir || !gitCommonDir) return fallback(cwd);
|
|
4646
4835
|
const isLinkedWorktree = gitDir !== gitCommonDir;
|
|
4647
|
-
const mainRoot = isLinkedWorktree &&
|
|
4648
|
-
const worktreeName = isLinkedWorktree ?
|
|
4836
|
+
const mainRoot = isLinkedWorktree && path32.basename(gitCommonDir) === ".git" ? path32.dirname(gitCommonDir) : repoRoot;
|
|
4837
|
+
const worktreeName = isLinkedWorktree ? path32.basename(gitDir) : null;
|
|
4649
4838
|
return {
|
|
4650
4839
|
cwd,
|
|
4651
4840
|
repoRoot,
|
|
@@ -4679,9 +4868,18 @@ function compareVersions(a, b) {
|
|
|
4679
4868
|
}
|
|
4680
4869
|
return semver.compare(normalizedA, normalizedB);
|
|
4681
4870
|
}
|
|
4871
|
+
function resolveMajorVersion(range) {
|
|
4872
|
+
if (!range) return null;
|
|
4873
|
+
try {
|
|
4874
|
+
const min = semver.minVersion(range);
|
|
4875
|
+
return min ? min.major : null;
|
|
4876
|
+
} catch {
|
|
4877
|
+
return null;
|
|
4878
|
+
}
|
|
4879
|
+
}
|
|
4682
4880
|
|
|
4683
4881
|
// src/scanner/detect-package-manager.ts
|
|
4684
|
-
import
|
|
4882
|
+
import path33 from "node:path";
|
|
4685
4883
|
import fs27 from "fs-extra";
|
|
4686
4884
|
function detectPackageManager(root, packageManagerField) {
|
|
4687
4885
|
const field = String(packageManagerField ?? "").trim();
|
|
@@ -4700,9 +4898,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
4700
4898
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
4701
4899
|
return "unknown";
|
|
4702
4900
|
}
|
|
4703
|
-
if (fs27.existsSync(
|
|
4704
|
-
if (fs27.existsSync(
|
|
4705
|
-
if (fs27.existsSync(
|
|
4901
|
+
if (fs27.existsSync(path33.join(root, "yarn.lock"))) return "yarn";
|
|
4902
|
+
if (fs27.existsSync(path33.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
4903
|
+
if (fs27.existsSync(path33.join(root, "package-lock.json"))) return "npm";
|
|
4706
4904
|
return "unknown";
|
|
4707
4905
|
}
|
|
4708
4906
|
|
|
@@ -4710,6 +4908,11 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
4710
4908
|
var dep = (value) => ({ kind: "dep", value });
|
|
4711
4909
|
var depPrefix = (value) => ({ kind: "depPrefix", value });
|
|
4712
4910
|
var depAbsent = (value) => ({ kind: "depAbsent", value });
|
|
4911
|
+
var depMajor = (value, major) => ({
|
|
4912
|
+
kind: "depMajor",
|
|
4913
|
+
value,
|
|
4914
|
+
major
|
|
4915
|
+
});
|
|
4713
4916
|
var fileEndsWith = (value) => ({
|
|
4714
4917
|
kind: "file",
|
|
4715
4918
|
value,
|
|
@@ -4749,6 +4952,8 @@ function matchSignal(sig, ctx) {
|
|
|
4749
4952
|
return false;
|
|
4750
4953
|
case "depAbsent":
|
|
4751
4954
|
return !ctx.deps.has(sig.value);
|
|
4955
|
+
case "depMajor":
|
|
4956
|
+
return ctx.deps.has(sig.value) && resolveMajorVersion(ctx.depVersions.get(sig.value) ?? "") === sig.major;
|
|
4752
4957
|
case "content":
|
|
4753
4958
|
return ctx.contentBlob.includes(sig.value);
|
|
4754
4959
|
case "file":
|
|
@@ -4800,9 +5005,9 @@ var ROLE_RULES = [
|
|
|
4800
5005
|
];
|
|
4801
5006
|
var STACK_RULES = [
|
|
4802
5007
|
{ stack: ["frontend", "nextjs"], any: [dep("next")] },
|
|
4803
|
-
{ stack: ["frontend", "react19"], any: [
|
|
5008
|
+
{ stack: ["frontend", "react19"], any: [depMajor("react", 19)] },
|
|
4804
5009
|
{ stack: ["frontend", "vue"], any: [dep("vue")] },
|
|
4805
|
-
{ stack: ["frontend", "vite8"], any: [
|
|
5010
|
+
{ stack: ["frontend", "vite8"], any: [depMajor("vite", 8)] },
|
|
4806
5011
|
{ stack: ["frontend", "react-router-v7"], all: [dep("react-router"), dep("@react-router/node")] },
|
|
4807
5012
|
{
|
|
4808
5013
|
stack: ["frontend", "tailwindcss"],
|
|
@@ -4812,7 +5017,7 @@ var STACK_RULES = [
|
|
|
4812
5017
|
stack: ["frontend", "shadcn"],
|
|
4813
5018
|
all: [fileEndsWith("components.json"), dep("class-variance-authority")]
|
|
4814
5019
|
},
|
|
4815
|
-
{ stack: ["tooling", "typescript5"], any: [
|
|
5020
|
+
{ stack: ["tooling", "typescript5"], any: [depMajor("typescript", 5)] },
|
|
4816
5021
|
{ stack: ["backend", "sanity"], any: [dep("sanity"), dep("next-sanity"), dep("@sanity/client")] },
|
|
4817
5022
|
{ stack: ["backend", "strapi"], any: [dep("@strapi/strapi"), depPrefix("@strapi/")] },
|
|
4818
5023
|
{ stack: ["backend", "prisma"], any: [dep("prisma"), dep("@prisma/client")] },
|
|
@@ -4837,9 +5042,15 @@ var STACK_RULES = [
|
|
|
4837
5042
|
stack: ["databases", "supabase"],
|
|
4838
5043
|
any: [dep("@supabase/supabase-js"), depPrefix("@supabase/")]
|
|
4839
5044
|
},
|
|
4840
|
-
{ stack: ["backend", "vendure3"], any: [
|
|
5045
|
+
{ stack: ["backend", "vendure3"], any: [depMajor("@vendure/core", 3)] },
|
|
4841
5046
|
{ stack: ["backend", "nestjs"], any: [dep("@nestjs/core")] },
|
|
4842
5047
|
{ stack: ["backend", "nestjs"], any: [content("NestFactory")] },
|
|
5048
|
+
// Deliberately NOT major-gated: this rule catches vendure *plugin* repos that use
|
|
5049
|
+
// the @VendurePlugin decorator without depending on @vendure/core directly (a
|
|
5050
|
+
// peer/transitive dep, common in a plugin package). depMajor requires the name to
|
|
5051
|
+
// be a direct dependency (see matchSignal's depMajor case), which this repo shape
|
|
5052
|
+
// doesn't satisfy — falling back to content detection is the only signal available,
|
|
5053
|
+
// so it stays presence/content-based rather than gated on a major we can't read.
|
|
4843
5054
|
{ stack: ["backend", "vendure3"], any: [content("@VendurePlugin")] },
|
|
4844
5055
|
{ stack: ["backend", "graphql"], any: [dep("graphql"), dep("@nestjs/graphql")] },
|
|
4845
5056
|
{
|
|
@@ -4912,7 +5123,7 @@ function runDetection(ctx, rules = STACK_RULES) {
|
|
|
4912
5123
|
}
|
|
4913
5124
|
|
|
4914
5125
|
// src/scanner/detection.ts
|
|
4915
|
-
import
|
|
5126
|
+
import path34 from "node:path";
|
|
4916
5127
|
var UNSUPPORTED_MARKERS = {
|
|
4917
5128
|
"requirements.txt": "python",
|
|
4918
5129
|
"pyproject.toml": "python",
|
|
@@ -4941,17 +5152,21 @@ function describeUnknownDetection(unsupportedSignals, isLinkedWorktree) {
|
|
|
4941
5152
|
function blocked(rel) {
|
|
4942
5153
|
return SENSITIVE_PATH_REGEXES.some((x) => x.test(rel));
|
|
4943
5154
|
}
|
|
4944
|
-
function
|
|
4945
|
-
const
|
|
5155
|
+
function dependencyVersionMap(pkg, composer) {
|
|
5156
|
+
const versions = /* @__PURE__ */ new Map();
|
|
4946
5157
|
const pushObj = (obj) => {
|
|
4947
|
-
if (!
|
|
4948
|
-
for (const key of Object.keys(obj))
|
|
5158
|
+
if (!isRecord2(obj)) return;
|
|
5159
|
+
for (const key of Object.keys(obj)) {
|
|
5160
|
+
if (versions.has(key)) continue;
|
|
5161
|
+
const value = obj[key];
|
|
5162
|
+
versions.set(key, typeof value === "string" ? value : "");
|
|
5163
|
+
}
|
|
4949
5164
|
};
|
|
4950
5165
|
pushObj(pkg?.dependencies);
|
|
4951
5166
|
pushObj(pkg?.devDependencies);
|
|
4952
5167
|
pushObj(composer?.require);
|
|
4953
5168
|
pushObj(composer?.["require-dev"]);
|
|
4954
|
-
return
|
|
5169
|
+
return versions;
|
|
4955
5170
|
}
|
|
4956
5171
|
function finalizeRoles(registryRoles, deps, files) {
|
|
4957
5172
|
const roles = new Set(registryRoles);
|
|
@@ -4972,14 +5187,14 @@ function finalizeRoles(registryRoles, deps, files) {
|
|
|
4972
5187
|
function collectUnsupportedSignals(files) {
|
|
4973
5188
|
return [
|
|
4974
5189
|
...new Set(
|
|
4975
|
-
files.map((f) => UNSUPPORTED_MARKERS[
|
|
5190
|
+
files.map((f) => UNSUPPORTED_MARKERS[path34.basename(f)]).filter((s) => Boolean(s))
|
|
4976
5191
|
)
|
|
4977
5192
|
].sort();
|
|
4978
5193
|
}
|
|
4979
5194
|
|
|
4980
5195
|
// src/scanner/render.ts
|
|
4981
5196
|
import { readFile } from "node:fs/promises";
|
|
4982
|
-
import
|
|
5197
|
+
import path35 from "node:path";
|
|
4983
5198
|
async function buildContentBlob(root, files) {
|
|
4984
5199
|
const candidates = files.filter(
|
|
4985
5200
|
(f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
|
|
@@ -4987,7 +5202,7 @@ async function buildContentBlob(root, files) {
|
|
|
4987
5202
|
const slice = candidates.slice(0, 300);
|
|
4988
5203
|
const parts = await mapWithConcurrency(slice, async (rel) => {
|
|
4989
5204
|
try {
|
|
4990
|
-
return await readFile(
|
|
5205
|
+
return await readFile(path35.join(root, rel), "utf8");
|
|
4991
5206
|
} catch {
|
|
4992
5207
|
return "";
|
|
4993
5208
|
}
|
|
@@ -5079,14 +5294,20 @@ var SAFE_FILES = [
|
|
|
5079
5294
|
"Gemfile"
|
|
5080
5295
|
];
|
|
5081
5296
|
async function scanProject(root) {
|
|
5082
|
-
const pkg = await readJson(
|
|
5083
|
-
const composer = await readJson(
|
|
5297
|
+
const pkg = await readJson(path36.join(root, "package.json"));
|
|
5298
|
+
const composer = await readJson(path36.join(root, "composer.json"));
|
|
5084
5299
|
const files = await listFiles(root, SAFE_FILES);
|
|
5085
5300
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
5086
|
-
const
|
|
5301
|
+
const depVersions = dependencyVersionMap(pkg, composer);
|
|
5302
|
+
const deps = [...depVersions.keys()].sort();
|
|
5087
5303
|
const packageManager = detectPackageManager(root, String(pkg?.packageManager ?? ""));
|
|
5088
5304
|
const contentBlob = await buildContentBlob(root, safeFiles);
|
|
5089
|
-
const detection = runDetection({
|
|
5305
|
+
const detection = runDetection({
|
|
5306
|
+
deps: new Set(deps),
|
|
5307
|
+
depVersions,
|
|
5308
|
+
files: safeFiles,
|
|
5309
|
+
contentBlob
|
|
5310
|
+
});
|
|
5090
5311
|
const roles = finalizeRoles(detection.roles, deps, safeFiles);
|
|
5091
5312
|
const stacks = detection.stacks;
|
|
5092
5313
|
if (packageManager === "yarn") stacks.packageManagers.push("yarn4");
|
|
@@ -5094,7 +5315,7 @@ async function scanProject(root) {
|
|
|
5094
5315
|
const warnings = [];
|
|
5095
5316
|
const securityRisks = [];
|
|
5096
5317
|
const crossRepoHints = [];
|
|
5097
|
-
const nodeEngine =
|
|
5318
|
+
const nodeEngine = isRecord2(pkg?.engines) ? String(pkg.engines.node ?? "") : "";
|
|
5098
5319
|
if (nodeEngine && !satisfiesVersion(process.version, nodeEngine)) {
|
|
5099
5320
|
warnings.push(`Current Node ${process.version} does not satisfy package engine ${nodeEngine}`);
|
|
5100
5321
|
}
|
|
@@ -5104,7 +5325,7 @@ async function scanProject(root) {
|
|
|
5104
5325
|
crossRepoHints.push("Monorepo orchestration detected");
|
|
5105
5326
|
const unsupportedSignals = collectUnsupportedSignals(safeFiles);
|
|
5106
5327
|
const detectionStatus = computeDetectionStatus(roles, stacks, unsupportedSignals);
|
|
5107
|
-
const repoNameFallback = pkg?.name ? String(pkg.name) :
|
|
5328
|
+
const repoNameFallback = pkg?.name ? String(pkg.name) : path36.basename((await resolveRoots(root)).mainRoot);
|
|
5108
5329
|
const context = {
|
|
5109
5330
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5110
5331
|
repoName: repoNameFallback,
|
|
@@ -5126,9 +5347,9 @@ async function scanProject(root) {
|
|
|
5126
5347
|
// src/scanner/read-context.ts
|
|
5127
5348
|
async function isCacheFresh(root, cachePath) {
|
|
5128
5349
|
const markerPaths = [
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5350
|
+
path37.join(root, "package.json"),
|
|
5351
|
+
path37.join(root, WORKSPACE_FILE),
|
|
5352
|
+
path37.join(root, REPOS_MANIFEST_FILE)
|
|
5132
5353
|
];
|
|
5133
5354
|
const cacheStat = await fs28.stat(cachePath);
|
|
5134
5355
|
for (const markerPath of markerPaths) {
|
|
@@ -5234,7 +5455,7 @@ async function runDoctor(options) {
|
|
|
5234
5455
|
} else {
|
|
5235
5456
|
ok(`- HOOKS OK: ${hooks.message}`);
|
|
5236
5457
|
}
|
|
5237
|
-
const rootClaudeMdPath =
|
|
5458
|
+
const rootClaudeMdPath = path38.join(root, "CLAUDE.md");
|
|
5238
5459
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
5239
5460
|
if (!rootClaudeMdContent) {
|
|
5240
5461
|
flag(
|
|
@@ -5288,8 +5509,8 @@ async function runDoctor(options) {
|
|
|
5288
5509
|
} else {
|
|
5289
5510
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
5290
5511
|
const bodyContent = workflowContent.slice(firstLine.length + 1);
|
|
5291
|
-
const cachePath =
|
|
5292
|
-
const bundledPath =
|
|
5512
|
+
const cachePath = path38.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
5513
|
+
const bundledPath = path38.join(
|
|
5293
5514
|
packageRoot(),
|
|
5294
5515
|
"library",
|
|
5295
5516
|
"global",
|
|
@@ -5338,7 +5559,7 @@ async function runDoctor(options) {
|
|
|
5338
5559
|
}
|
|
5339
5560
|
}
|
|
5340
5561
|
if (workflowExists) {
|
|
5341
|
-
const prettierIgnore = await readText(
|
|
5562
|
+
const prettierIgnore = await readText(path38.join(root, ".prettierignore")) ?? "";
|
|
5342
5563
|
const lines = prettierIgnore.split("\n");
|
|
5343
5564
|
const missing = IGNORED_PATHS.filter((p) => !prettierIgnoreProtects(lines, p));
|
|
5344
5565
|
if (missing.length > 0) {
|
|
@@ -5411,7 +5632,7 @@ async function runDoctor(options) {
|
|
|
5411
5632
|
);
|
|
5412
5633
|
}
|
|
5413
5634
|
}
|
|
5414
|
-
const pkgJson = await readJson(
|
|
5635
|
+
const pkgJson = await readJson(path38.join(packageRoot(), "package.json"));
|
|
5415
5636
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
5416
5637
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
5417
5638
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -5444,7 +5665,7 @@ async function runDoctor(options) {
|
|
|
5444
5665
|
}
|
|
5445
5666
|
|
|
5446
5667
|
// src/commands/update.ts
|
|
5447
|
-
import
|
|
5668
|
+
import path41 from "node:path";
|
|
5448
5669
|
|
|
5449
5670
|
// src/claude/refresh-project.ts
|
|
5450
5671
|
import fs30 from "fs-extra";
|
|
@@ -5461,7 +5682,7 @@ async function refreshProjectApply(root) {
|
|
|
5461
5682
|
|
|
5462
5683
|
// src/install/apply.ts
|
|
5463
5684
|
import crypto2 from "node:crypto";
|
|
5464
|
-
import
|
|
5685
|
+
import path39 from "node:path";
|
|
5465
5686
|
import fs31 from "fs-extra";
|
|
5466
5687
|
|
|
5467
5688
|
// src/install/header.ts
|
|
@@ -5548,7 +5769,7 @@ function hashContent(content2) {
|
|
|
5548
5769
|
}
|
|
5549
5770
|
function sourceVersion() {
|
|
5550
5771
|
try {
|
|
5551
|
-
const pkgPath =
|
|
5772
|
+
const pkgPath = path39.join(packageRoot(), "package.json");
|
|
5552
5773
|
const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf8"));
|
|
5553
5774
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
5554
5775
|
} catch {
|
|
@@ -5556,49 +5777,49 @@ function sourceVersion() {
|
|
|
5556
5777
|
}
|
|
5557
5778
|
}
|
|
5558
5779
|
function globalSrcDir() {
|
|
5559
|
-
return
|
|
5780
|
+
return path39.join(packageRoot(), "library", "global");
|
|
5560
5781
|
}
|
|
5561
5782
|
function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
5562
|
-
const skillDir =
|
|
5783
|
+
const skillDir = path39.join(skillsDir, skillName);
|
|
5563
5784
|
const entries = [];
|
|
5564
5785
|
for (const rel of fs31.readdirSync(skillDir, { recursive: true })) {
|
|
5565
5786
|
if (rel === "SKILL.md") continue;
|
|
5566
|
-
const abs =
|
|
5787
|
+
const abs = path39.join(skillDir, rel);
|
|
5567
5788
|
if (!fs31.statSync(abs).isFile()) continue;
|
|
5568
|
-
const posixRel = rel.split(
|
|
5789
|
+
const posixRel = rel.split(path39.sep).join("/");
|
|
5569
5790
|
entries.push({
|
|
5570
5791
|
stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
|
|
5571
|
-
srcRelPath:
|
|
5572
|
-
destPath:
|
|
5792
|
+
srcRelPath: path39.join("library", "global", "skills", skillName, rel),
|
|
5793
|
+
destPath: path39.join(claudeDir, "skills", skillName, rel)
|
|
5573
5794
|
});
|
|
5574
5795
|
}
|
|
5575
5796
|
return entries;
|
|
5576
5797
|
}
|
|
5577
5798
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
5578
5799
|
const entries = [];
|
|
5579
|
-
const skillsDir =
|
|
5800
|
+
const skillsDir = path39.join(srcDir, "skills");
|
|
5580
5801
|
if (fs31.pathExistsSync(skillsDir)) {
|
|
5581
5802
|
for (const skillName of fs31.readdirSync(skillsDir)) {
|
|
5582
|
-
const skillFile =
|
|
5803
|
+
const skillFile = path39.join(skillsDir, skillName, "SKILL.md");
|
|
5583
5804
|
if (fs31.pathExistsSync(skillFile)) {
|
|
5584
5805
|
entries.push({
|
|
5585
5806
|
stableId: `skill.${skillName}`,
|
|
5586
|
-
srcRelPath:
|
|
5587
|
-
destPath:
|
|
5807
|
+
srcRelPath: path39.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
5808
|
+
destPath: path39.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
5588
5809
|
});
|
|
5589
5810
|
entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
|
|
5590
5811
|
}
|
|
5591
5812
|
}
|
|
5592
5813
|
}
|
|
5593
|
-
const commandsDir =
|
|
5814
|
+
const commandsDir = path39.join(srcDir, "commands");
|
|
5594
5815
|
if (fs31.pathExistsSync(commandsDir)) {
|
|
5595
5816
|
for (const fileName of fs31.readdirSync(commandsDir)) {
|
|
5596
5817
|
if (!fileName.endsWith(".md")) continue;
|
|
5597
5818
|
const commandName = fileName.slice(0, -".md".length);
|
|
5598
5819
|
entries.push({
|
|
5599
5820
|
stableId: `command.${commandName}`,
|
|
5600
|
-
srcRelPath:
|
|
5601
|
-
destPath:
|
|
5821
|
+
srcRelPath: path39.join("library", "global", "commands", fileName),
|
|
5822
|
+
destPath: path39.join(claudeDir, "commands", fileName)
|
|
5602
5823
|
});
|
|
5603
5824
|
}
|
|
5604
5825
|
}
|
|
@@ -5624,7 +5845,7 @@ async function applyInstall(options = {}) {
|
|
|
5624
5845
|
};
|
|
5625
5846
|
const manifestFiles = [];
|
|
5626
5847
|
for (const entry of sourceFiles) {
|
|
5627
|
-
const srcPath =
|
|
5848
|
+
const srcPath = path39.join(packageRoot(), entry.srcRelPath);
|
|
5628
5849
|
const rawContent = await readText(srcPath);
|
|
5629
5850
|
if (rawContent === void 0) {
|
|
5630
5851
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -5684,7 +5905,7 @@ async function applyInstall(options = {}) {
|
|
|
5684
5905
|
}
|
|
5685
5906
|
let mergedSettings;
|
|
5686
5907
|
if (!check) {
|
|
5687
|
-
const fragmentPath =
|
|
5908
|
+
const fragmentPath = path39.join(srcDir, "settings-fragments", "hooks.json");
|
|
5688
5909
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
5689
5910
|
const settings = await readSettings();
|
|
5690
5911
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -5773,7 +5994,7 @@ function summarizeLockDiff(before, after) {
|
|
|
5773
5994
|
|
|
5774
5995
|
// src/update/lockfile.ts
|
|
5775
5996
|
import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
|
|
5776
|
-
import
|
|
5997
|
+
import path40 from "node:path";
|
|
5777
5998
|
async function readLockSummary(root) {
|
|
5778
5999
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
5779
6000
|
const catalogRef = lock.find((item) => item.catalogRef)?.catalogRef ?? null;
|
|
@@ -5809,7 +6030,7 @@ async function applyLock(root) {
|
|
|
5809
6030
|
try {
|
|
5810
6031
|
const backupDir = hausPath(root, "backups");
|
|
5811
6032
|
await mkdir(backupDir, { recursive: true });
|
|
5812
|
-
await copyFile(lockPath,
|
|
6033
|
+
await copyFile(lockPath, path40.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
5813
6034
|
} catch {
|
|
5814
6035
|
}
|
|
5815
6036
|
const enriched = await Promise.all(
|
|
@@ -5831,7 +6052,7 @@ function diffLock(before, after) {
|
|
|
5831
6052
|
}
|
|
5832
6053
|
async function hasLocalOverrides(root) {
|
|
5833
6054
|
try {
|
|
5834
|
-
await readFile2(
|
|
6055
|
+
await readFile2(path40.join(root, ".claude", "settings.json"), "utf8");
|
|
5835
6056
|
return true;
|
|
5836
6057
|
} catch {
|
|
5837
6058
|
return false;
|
|
@@ -5847,7 +6068,7 @@ async function runUpdate(options) {
|
|
|
5847
6068
|
return;
|
|
5848
6069
|
}
|
|
5849
6070
|
if (options.check) {
|
|
5850
|
-
const pkgJson2 = await readJson(
|
|
6071
|
+
const pkgJson2 = await readJson(path41.join(packageRoot(), "package.json"));
|
|
5851
6072
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
5852
6073
|
const [status, npmVersion, latestCatalogTag, globalInstallDrift, catalogItems, lockItems] = await Promise.all([
|
|
5853
6074
|
options.fast ? fastLockStatus(root) : checkLock(root),
|
|
@@ -5882,7 +6103,7 @@ async function runUpdate(options) {
|
|
|
5882
6103
|
if (hasHashDrift || formerIdMigrations.length > 0) process.exitCode = 1;
|
|
5883
6104
|
return;
|
|
5884
6105
|
}
|
|
5885
|
-
const pkgJson = await readJson(
|
|
6106
|
+
const pkgJson = await readJson(path41.join(packageRoot(), "package.json"));
|
|
5886
6107
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
5887
6108
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
5888
6109
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -5924,7 +6145,7 @@ async function runFromHookCheck(root) {
|
|
|
5924
6145
|
try {
|
|
5925
6146
|
const summary = await readLockSummary(root);
|
|
5926
6147
|
if (summary.count === 0) return;
|
|
5927
|
-
const pkgJson = await readJson(
|
|
6148
|
+
const pkgJson = await readJson(path41.join(packageRoot(), "package.json"));
|
|
5928
6149
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
5929
6150
|
const [npmVersion, latestCatalogTag] = await Promise.all([
|
|
5930
6151
|
fetchNpmVersionStatus(currentVersion),
|
|
@@ -6074,7 +6295,7 @@ async function runCiGate(options = {}) {
|
|
|
6074
6295
|
// src/commands/clone.ts
|
|
6075
6296
|
init_exec();
|
|
6076
6297
|
import { existsSync as existsSync2 } from "node:fs";
|
|
6077
|
-
import
|
|
6298
|
+
import path42 from "node:path";
|
|
6078
6299
|
var GIT_LOCATION_VARS = [
|
|
6079
6300
|
"GIT_DIR",
|
|
6080
6301
|
"GIT_WORK_TREE",
|
|
@@ -6124,11 +6345,11 @@ async function runClone(url, opts = {}) {
|
|
|
6124
6345
|
process.exitCode = 1;
|
|
6125
6346
|
return;
|
|
6126
6347
|
}
|
|
6127
|
-
const target =
|
|
6348
|
+
const target = path42.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
6128
6349
|
if (existsSync2(target)) {
|
|
6129
6350
|
const check = await checkExistingTarget(target, url);
|
|
6130
6351
|
if (check.kind === "matches") {
|
|
6131
|
-
log(`\u2022 ${
|
|
6352
|
+
log(`\u2022 ${path42.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
|
|
6132
6353
|
return;
|
|
6133
6354
|
}
|
|
6134
6355
|
const message = check.kind === "not-a-repo" ? `${target} already exists and is not a git repository \u2014 refusing to clone ${url} there. Remove or rename it, or pass a different [dir].` : `${target} already exists as a different repository (origin: ${check.origin ?? "(no origin remote)"}) \u2014 refusing to clone ${url} there. Remove or rename it, or pass a different [dir].`;
|
|
@@ -6168,7 +6389,7 @@ function readHookPayload() {
|
|
|
6168
6389
|
if (!raw) return { payload: {}, malformed: false };
|
|
6169
6390
|
try {
|
|
6170
6391
|
const parsed = JSON.parse(raw);
|
|
6171
|
-
return { payload:
|
|
6392
|
+
return { payload: isRecord2(parsed) ? parsed : {}, malformed: false };
|
|
6172
6393
|
} catch {
|
|
6173
6394
|
return { payload: {}, malformed: true };
|
|
6174
6395
|
}
|
|
@@ -6207,7 +6428,7 @@ function readPayload(options) {
|
|
|
6207
6428
|
if (options.stdinPayload != null) {
|
|
6208
6429
|
try {
|
|
6209
6430
|
const parsed = JSON.parse(options.stdinPayload);
|
|
6210
|
-
return { payload:
|
|
6431
|
+
return { payload: isRecord2(parsed) ? parsed : {}, malformed: false };
|
|
6211
6432
|
} catch {
|
|
6212
6433
|
return { payload: {}, malformed: true };
|
|
6213
6434
|
}
|
|
@@ -6220,7 +6441,7 @@ async function runDecisionsGuard(options = {}) {
|
|
|
6220
6441
|
emitPreToolUseDeny("Malformed hook payload");
|
|
6221
6442
|
return;
|
|
6222
6443
|
}
|
|
6223
|
-
const toolInput =
|
|
6444
|
+
const toolInput = isRecord2(payload.tool_input) ? payload.tool_input : {};
|
|
6224
6445
|
const command = String(toolInput.command ?? "");
|
|
6225
6446
|
if (!PR_CREATE_PATTERN.test(command)) return;
|
|
6226
6447
|
const root = process.cwd();
|
|
@@ -6232,13 +6453,13 @@ async function runDecisionsGuard(options = {}) {
|
|
|
6232
6453
|
const result = await runDecisionsCheck(root, { range, commitMessages });
|
|
6233
6454
|
if (result.triggered && !result.satisfied) {
|
|
6234
6455
|
emitPreToolUseDeny(
|
|
6235
|
-
`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 docs/decisions/, then retry.`
|
|
6456
|
+
`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 docs/decisions/, then retry \u2014 or use [adr-skip] for non-security changes.`
|
|
6236
6457
|
);
|
|
6237
6458
|
}
|
|
6238
6459
|
}
|
|
6239
6460
|
|
|
6240
6461
|
// src/commands/decisions.ts
|
|
6241
|
-
import
|
|
6462
|
+
import path43 from "node:path";
|
|
6242
6463
|
|
|
6243
6464
|
// src/decisions/next-number.ts
|
|
6244
6465
|
async function nextDecisionNumber(root) {
|
|
@@ -6281,10 +6502,9 @@ _(Why this over alternatives \u2014 the line agents read for "why".)_
|
|
|
6281
6502
|
`;
|
|
6282
6503
|
}
|
|
6283
6504
|
async function runDecisionsSuggest(root, opts = {}) {
|
|
6284
|
-
const stats = await collectDiffStats(root, { staged: true });
|
|
6285
6505
|
const stagedCheck = await runDecisionsCheck(root, { staged: true });
|
|
6286
|
-
const
|
|
6287
|
-
const triggered =
|
|
6506
|
+
const unstagedCheck = await runDecisionsCheck(root, {});
|
|
6507
|
+
const triggered = stagedCheck.triggered || unstagedCheck.triggered;
|
|
6288
6508
|
if (!triggered) return;
|
|
6289
6509
|
if (stagedCheck.triggered && stagedCheck.satisfied) return;
|
|
6290
6510
|
const number = await nextDecisionNumber(root);
|
|
@@ -6335,7 +6555,7 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
6335
6555
|
}
|
|
6336
6556
|
for (const reason of result.reasons) warn(reason);
|
|
6337
6557
|
error(
|
|
6338
|
-
|
|
6558
|
+
`decisions gate: decision-worthy change without a valid ADR under docs/decisions/. Why: ${result.reasons.filter((r) => !r.startsWith("no new")).join("; ") || "see above"}. Run \`haus decisions suggest\`, add docs/decisions/NNNN-*.md + README row, or use [adr-skip] with justification (ignored for security paths).`
|
|
6339
6559
|
);
|
|
6340
6560
|
process.exitCode = 1;
|
|
6341
6561
|
return;
|
|
@@ -6349,7 +6569,7 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
6349
6569
|
return;
|
|
6350
6570
|
}
|
|
6351
6571
|
if (sub === "validate") {
|
|
6352
|
-
const target = arg ?
|
|
6572
|
+
const target = arg ? path43.resolve(root, arg) : void 0;
|
|
6353
6573
|
if (!target) {
|
|
6354
6574
|
error("usage: haus decisions validate <path-to-decision.md>");
|
|
6355
6575
|
process.exitCode = 1;
|
|
@@ -6601,7 +6821,7 @@ async function runGuard(kind, _options) {
|
|
|
6601
6821
|
emitPreToolUseDeny("Malformed hook payload");
|
|
6602
6822
|
return;
|
|
6603
6823
|
}
|
|
6604
|
-
const toolInput =
|
|
6824
|
+
const toolInput = isRecord2(payload.tool_input) ? payload.tool_input : {};
|
|
6605
6825
|
if (kind === "file-access") {
|
|
6606
6826
|
const candidate = String(toolInput.path ?? toolInput.file_path ?? "");
|
|
6607
6827
|
const reason2 = guardFileAccess(candidate);
|
|
@@ -6618,7 +6838,7 @@ async function runGuard(kind, _options) {
|
|
|
6618
6838
|
}
|
|
6619
6839
|
|
|
6620
6840
|
// src/commands/init.ts
|
|
6621
|
-
import
|
|
6841
|
+
import path45 from "node:path";
|
|
6622
6842
|
import fs32 from "fs-extra";
|
|
6623
6843
|
|
|
6624
6844
|
// src/recommender/git-signal.ts
|
|
@@ -7086,7 +7306,7 @@ function buildOptInEligible(items, skipped) {
|
|
|
7086
7306
|
}
|
|
7087
7307
|
|
|
7088
7308
|
// src/commands/workspace/discover.ts
|
|
7089
|
-
import
|
|
7309
|
+
import path44 from "node:path";
|
|
7090
7310
|
import fg4 from "fast-glob";
|
|
7091
7311
|
import YAML2 from "yaml";
|
|
7092
7312
|
var DEFAULT_MAX_DEPTH = 3;
|
|
@@ -7127,10 +7347,10 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
7127
7347
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
7128
7348
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
7129
7349
|
for (const match of gitMatches) {
|
|
7130
|
-
gitDirs.add(
|
|
7350
|
+
gitDirs.add(path44.posix.dirname(match));
|
|
7131
7351
|
}
|
|
7132
7352
|
for (const match of manifestMatches) {
|
|
7133
|
-
manifestDirs.add(
|
|
7353
|
+
manifestDirs.add(path44.posix.dirname(match));
|
|
7134
7354
|
}
|
|
7135
7355
|
const repoRoots = [...gitDirs];
|
|
7136
7356
|
const manifestSorted = [...manifestDirs].sort(
|
|
@@ -7145,18 +7365,18 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
7145
7365
|
return repoRoots;
|
|
7146
7366
|
}
|
|
7147
7367
|
async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
7148
|
-
const repoRoots = await findRepoRoots(workspaceRoot, maxDepth);
|
|
7368
|
+
const repoRoots = (await findRepoRoots(workspaceRoot, maxDepth)).filter((dir) => dir !== ".");
|
|
7149
7369
|
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
7150
|
-
const absDir =
|
|
7151
|
-
const pkg = await readJson(
|
|
7152
|
-
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name :
|
|
7370
|
+
const absDir = path44.resolve(workspaceRoot, relDir);
|
|
7371
|
+
const pkg = await readJson(path44.join(absDir, "package.json"));
|
|
7372
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path44.basename(absDir);
|
|
7153
7373
|
let role = "auto";
|
|
7154
7374
|
try {
|
|
7155
7375
|
const scan = await scanProject(absDir);
|
|
7156
7376
|
if (scan.repoRoles.length > 0) role = scan.repoRoles.join("+");
|
|
7157
7377
|
} catch {
|
|
7158
7378
|
}
|
|
7159
|
-
return { name, path: relDir
|
|
7379
|
+
return { name, path: relDir, role };
|
|
7160
7380
|
});
|
|
7161
7381
|
}
|
|
7162
7382
|
function mergeWorkspaceConfig(existing, discovered, opts = {}) {
|
|
@@ -7192,7 +7412,7 @@ function renderWorkspaceYaml(config) {
|
|
|
7192
7412
|
});
|
|
7193
7413
|
}
|
|
7194
7414
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
7195
|
-
const yamlPath =
|
|
7415
|
+
const yamlPath = path44.join(workspaceRoot, "haus.workspace.yaml");
|
|
7196
7416
|
const existingText = await readText(yamlPath);
|
|
7197
7417
|
const existing = parseWorkspaceConfig(existingText);
|
|
7198
7418
|
if (existingText && !existing) {
|
|
@@ -7364,7 +7584,7 @@ async function runSetupProject(options) {
|
|
|
7364
7584
|
// src/commands/init.ts
|
|
7365
7585
|
async function runInit(options) {
|
|
7366
7586
|
const root = process.cwd();
|
|
7367
|
-
const hausDir =
|
|
7587
|
+
const hausDir = path45.join(root, ".haus-workflow");
|
|
7368
7588
|
const alreadyInit = await fs32.pathExists(hausDir);
|
|
7369
7589
|
if (alreadyInit) {
|
|
7370
7590
|
log("Haus AI already initialized in this project.");
|
|
@@ -7500,7 +7720,7 @@ async function runRefresh() {
|
|
|
7500
7720
|
}
|
|
7501
7721
|
|
|
7502
7722
|
// src/install/scaffold.ts
|
|
7503
|
-
import
|
|
7723
|
+
import path46 from "node:path";
|
|
7504
7724
|
import fs33 from "fs-extra";
|
|
7505
7725
|
async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
7506
7726
|
const scaffolded = [];
|
|
@@ -7517,9 +7737,9 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
7517
7737
|
const realRoot = await fs33.realpath(catalogRoot).catch(() => null);
|
|
7518
7738
|
for (const item of items) {
|
|
7519
7739
|
if (item.type !== "config") continue;
|
|
7520
|
-
const sourcePath =
|
|
7521
|
-
const rel =
|
|
7522
|
-
const escapes = rel === "" ||
|
|
7740
|
+
const sourcePath = path46.resolve(catalogRoot, item.path);
|
|
7741
|
+
const rel = path46.relative(catalogRoot, sourcePath);
|
|
7742
|
+
const escapes = rel === "" || path46.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
|
|
7523
7743
|
if (escapes) {
|
|
7524
7744
|
warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
|
|
7525
7745
|
continue;
|
|
@@ -7529,8 +7749,8 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
7529
7749
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
7530
7750
|
continue;
|
|
7531
7751
|
}
|
|
7532
|
-
const realRel =
|
|
7533
|
-
if (realRel !== "" && (
|
|
7752
|
+
const realRel = path46.relative(realRoot, realSource);
|
|
7753
|
+
if (realRel !== "" && (path46.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
|
|
7534
7754
|
warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
|
|
7535
7755
|
continue;
|
|
7536
7756
|
}
|
|
@@ -7544,14 +7764,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
7544
7764
|
continue;
|
|
7545
7765
|
}
|
|
7546
7766
|
if (stat.isFile()) {
|
|
7547
|
-
const filename =
|
|
7548
|
-
const dest =
|
|
7767
|
+
const filename = path46.basename(sourcePath);
|
|
7768
|
+
const dest = path46.join(projectRoot, filename);
|
|
7549
7769
|
record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
|
|
7550
7770
|
} else if (stat.isDirectory()) {
|
|
7551
7771
|
const entries = await fs33.readdir(sourcePath);
|
|
7552
7772
|
for (const entry of entries) {
|
|
7553
|
-
const src =
|
|
7554
|
-
const dest =
|
|
7773
|
+
const src = path46.join(sourcePath, entry);
|
|
7774
|
+
const dest = path46.join(projectRoot, entry);
|
|
7555
7775
|
record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
|
|
7556
7776
|
}
|
|
7557
7777
|
}
|
|
@@ -7559,14 +7779,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
7559
7779
|
return { scaffolded, skipped, skippedExisting };
|
|
7560
7780
|
}
|
|
7561
7781
|
async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
7562
|
-
const destDisplay =
|
|
7782
|
+
const destDisplay = path46.relative(projectRoot, dest) || path46.basename(dest);
|
|
7563
7783
|
const srcStat = await fs33.lstat(src).catch(() => null);
|
|
7564
7784
|
if (!srcStat) {
|
|
7565
|
-
warn(`Skipping ${
|
|
7785
|
+
warn(`Skipping ${path46.basename(src)}: source not found`);
|
|
7566
7786
|
return "skipped-refused";
|
|
7567
7787
|
}
|
|
7568
7788
|
if (srcStat.isSymbolicLink()) {
|
|
7569
|
-
warn(`Skipping ${
|
|
7789
|
+
warn(`Skipping ${path46.basename(src)}: source is a symlink`);
|
|
7570
7790
|
return "skipped-refused";
|
|
7571
7791
|
}
|
|
7572
7792
|
const exists = await fs33.pathExists(dest);
|
|
@@ -7575,13 +7795,13 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
|
7575
7795
|
return "skipped-exists";
|
|
7576
7796
|
}
|
|
7577
7797
|
if (opts.dryRun) {
|
|
7578
|
-
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${
|
|
7798
|
+
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path46.basename(dest)} (${itemId})`);
|
|
7579
7799
|
return "scaffolded";
|
|
7580
7800
|
}
|
|
7581
7801
|
if (exists && opts.force) {
|
|
7582
7802
|
await fs33.remove(dest);
|
|
7583
7803
|
}
|
|
7584
|
-
await fs33.ensureDir(
|
|
7804
|
+
await fs33.ensureDir(path46.dirname(dest));
|
|
7585
7805
|
await fs33.copy(src, dest, {
|
|
7586
7806
|
overwrite: true,
|
|
7587
7807
|
dereference: false,
|
|
@@ -7594,7 +7814,7 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
|
7594
7814
|
}
|
|
7595
7815
|
}
|
|
7596
7816
|
});
|
|
7597
|
-
log(`${exists ? "Overwrote" : "Created"} ${
|
|
7817
|
+
log(`${exists ? "Overwrote" : "Created"} ${path46.basename(dest)} (${itemId})`);
|
|
7598
7818
|
return "scaffolded";
|
|
7599
7819
|
}
|
|
7600
7820
|
|
|
@@ -7659,7 +7879,7 @@ async function runScan(options) {
|
|
|
7659
7879
|
}
|
|
7660
7880
|
|
|
7661
7881
|
// src/commands/undo.ts
|
|
7662
|
-
import
|
|
7882
|
+
import path47 from "node:path";
|
|
7663
7883
|
import fs34 from "fs-extra";
|
|
7664
7884
|
|
|
7665
7885
|
// src/claude/managed-paths.ts
|
|
@@ -7687,18 +7907,18 @@ async function collectLockTrackedPaths(root) {
|
|
|
7687
7907
|
const relPaths = row.paths ?? [];
|
|
7688
7908
|
const existingRel = [];
|
|
7689
7909
|
for (const rel of relPaths) {
|
|
7690
|
-
if (await fs34.pathExists(
|
|
7910
|
+
if (await fs34.pathExists(path47.resolve(root, rel))) existingRel.push(rel);
|
|
7691
7911
|
}
|
|
7692
7912
|
if (existingRel.length === 0) continue;
|
|
7693
7913
|
if (row.hash === void 0) {
|
|
7694
|
-
removable.push(...existingRel.map((rel) =>
|
|
7914
|
+
removable.push(...existingRel.map((rel) => path47.resolve(root, rel)));
|
|
7695
7915
|
continue;
|
|
7696
7916
|
}
|
|
7697
7917
|
const currentHash = await hashInstalledPaths(root, existingRel);
|
|
7698
7918
|
if (currentHash === row.hash) {
|
|
7699
|
-
removable.push(...existingRel.map((rel) =>
|
|
7919
|
+
removable.push(...existingRel.map((rel) => path47.resolve(root, rel)));
|
|
7700
7920
|
} else {
|
|
7701
|
-
preserved.push(...existingRel.map((rel) =>
|
|
7921
|
+
preserved.push(...existingRel.map((rel) => path47.resolve(root, rel)));
|
|
7702
7922
|
log(`Preserving locally-modified ${row.id ?? "(unknown item)"}: ${existingRel.join(", ")}`);
|
|
7703
7923
|
}
|
|
7704
7924
|
}
|
|
@@ -7711,7 +7931,7 @@ async function settingsHasHausContent(root) {
|
|
|
7711
7931
|
return settings._haus != null;
|
|
7712
7932
|
}
|
|
7713
7933
|
async function claudeMdHasHausBlock(root) {
|
|
7714
|
-
const filePath =
|
|
7934
|
+
const filePath = path47.join(root, "CLAUDE.md");
|
|
7715
7935
|
if (!await fs34.pathExists(filePath)) return false;
|
|
7716
7936
|
const text = await fs34.readFile(filePath, "utf8");
|
|
7717
7937
|
return text.includes(BLOCK_BEGIN);
|
|
@@ -7724,15 +7944,15 @@ async function stripProjectSettings(root) {
|
|
|
7724
7944
|
const hasContent = Object.keys(settings).length > 0;
|
|
7725
7945
|
if (hasContent) {
|
|
7726
7946
|
await writeProjectSettings(root, settings);
|
|
7727
|
-
log(`Stripped haus rules from ${
|
|
7947
|
+
log(`Stripped haus rules from ${path47.relative(root, settingsPath)} (user settings preserved).`);
|
|
7728
7948
|
return true;
|
|
7729
7949
|
}
|
|
7730
7950
|
await fs34.remove(settingsPath);
|
|
7731
|
-
log(`Removed ${
|
|
7951
|
+
log(`Removed ${path47.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
7732
7952
|
return true;
|
|
7733
7953
|
}
|
|
7734
7954
|
async function stripRootClaudeMd(root) {
|
|
7735
|
-
const filePath =
|
|
7955
|
+
const filePath = path47.join(root, "CLAUDE.md");
|
|
7736
7956
|
if (!await fs34.pathExists(filePath)) return false;
|
|
7737
7957
|
const prev = await fs34.readFile(filePath, "utf8");
|
|
7738
7958
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
@@ -7760,12 +7980,12 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
|
7760
7980
|
const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
|
|
7761
7981
|
for (const abs of managedAbsPaths) {
|
|
7762
7982
|
if (!await fs34.pathExists(abs)) continue;
|
|
7763
|
-
const rel =
|
|
7764
|
-
const backupPath =
|
|
7765
|
-
await fs34.ensureDir(
|
|
7983
|
+
const rel = path47.relative(root, abs);
|
|
7984
|
+
const backupPath = path47.join(backupRoot, rel);
|
|
7985
|
+
await fs34.ensureDir(path47.dirname(backupPath));
|
|
7766
7986
|
await fs34.copy(abs, backupPath);
|
|
7767
7987
|
}
|
|
7768
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
7988
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path47.relative(root, backupRoot)}.`);
|
|
7769
7989
|
}
|
|
7770
7990
|
async function runUndo(options) {
|
|
7771
7991
|
const root = options.root ?? process.cwd();
|
|
@@ -7778,7 +7998,7 @@ async function runUndo(options) {
|
|
|
7778
7998
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
7779
7999
|
return;
|
|
7780
8000
|
}
|
|
7781
|
-
const relTargets = managed.map((p) =>
|
|
8001
|
+
const relTargets = managed.map((p) => path47.relative(root, p));
|
|
7782
8002
|
const summaryParts = [...relTargets];
|
|
7783
8003
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
7784
8004
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -7802,7 +8022,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
7802
8022
|
for (const abs of managed) {
|
|
7803
8023
|
if (!await fs34.pathExists(abs)) continue;
|
|
7804
8024
|
await fs34.remove(abs);
|
|
7805
|
-
log(`Removed ${
|
|
8025
|
+
log(`Removed ${path47.relative(root, abs)}`);
|
|
7806
8026
|
}
|
|
7807
8027
|
if (stripSettings) await stripProjectSettings(root);
|
|
7808
8028
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -7813,7 +8033,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
7813
8033
|
|
|
7814
8034
|
// src/install/uninstall.ts
|
|
7815
8035
|
import crypto3 from "node:crypto";
|
|
7816
|
-
import
|
|
8036
|
+
import path48 from "node:path";
|
|
7817
8037
|
import fs35 from "fs-extra";
|
|
7818
8038
|
async function runUninstall(options = {}) {
|
|
7819
8039
|
const { force = false } = options;
|
|
@@ -7843,14 +8063,14 @@ async function runUninstall(options = {}) {
|
|
|
7843
8063
|
continue;
|
|
7844
8064
|
}
|
|
7845
8065
|
await fs35.remove(entry.destPath);
|
|
7846
|
-
await pruneEmptyDir(
|
|
8066
|
+
await pruneEmptyDir(path48.dirname(entry.destPath));
|
|
7847
8067
|
result.deleted.push(entry.destPath);
|
|
7848
8068
|
}
|
|
7849
8069
|
const settings = await readSettings();
|
|
7850
8070
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
7851
8071
|
await writeSettings(stripped);
|
|
7852
8072
|
result.hooksStripped = true;
|
|
7853
|
-
const hausDir =
|
|
8073
|
+
const hausDir = path48.join(globalClaudeDir(), "haus");
|
|
7854
8074
|
const manifestPath2 = hausManifestPath();
|
|
7855
8075
|
if (fs35.pathExistsSync(manifestPath2)) {
|
|
7856
8076
|
await fs35.remove(manifestPath2);
|
|
@@ -7891,12 +8111,13 @@ async function runUninstallCommand(options) {
|
|
|
7891
8111
|
}
|
|
7892
8112
|
|
|
7893
8113
|
// src/commands/validate-catalog.ts
|
|
7894
|
-
import
|
|
8114
|
+
import path50 from "node:path";
|
|
7895
8115
|
|
|
7896
8116
|
// src/catalog/validate-core.ts
|
|
7897
8117
|
import fs36 from "node:fs";
|
|
7898
|
-
import
|
|
8118
|
+
import path49 from "node:path";
|
|
7899
8119
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
8120
|
+
var ITEM_ID_PATTERN = /^haus\./;
|
|
7900
8121
|
function auditForbiddenStacks(items) {
|
|
7901
8122
|
const failures = [];
|
|
7902
8123
|
for (const item of items) {
|
|
@@ -7982,6 +8203,9 @@ function auditManifestStructure(manifestVersion, items) {
|
|
|
7982
8203
|
failures.push(`item[${i}]: missing id`);
|
|
7983
8204
|
continue;
|
|
7984
8205
|
}
|
|
8206
|
+
if (!ITEM_ID_PATTERN.test(item.id)) {
|
|
8207
|
+
failures.push(`${item.id}: id must start with "haus."`);
|
|
8208
|
+
}
|
|
7985
8209
|
const provenanceError = validateCuratedProvenance(item);
|
|
7986
8210
|
if (provenanceError) failures.push(provenanceError);
|
|
7987
8211
|
const referencesError = validateReferences(item.id, item.references);
|
|
@@ -8040,7 +8264,7 @@ function readForAudit(absPath, label, failures) {
|
|
|
8040
8264
|
}
|
|
8041
8265
|
}
|
|
8042
8266
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
8043
|
-
const rel =
|
|
8267
|
+
const rel = path49.relative(manifestDir, absPath);
|
|
8044
8268
|
const failures = [];
|
|
8045
8269
|
const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
|
|
8046
8270
|
if (text === null) return failures;
|
|
@@ -8062,25 +8286,25 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
8062
8286
|
const failures = [];
|
|
8063
8287
|
for (const item of items) {
|
|
8064
8288
|
if (!item.path || !isSafeCatalogPath(item.path)) continue;
|
|
8065
|
-
const absPath =
|
|
8289
|
+
const absPath = path49.join(manifestDir, item.path);
|
|
8066
8290
|
if (item.type === "skill") {
|
|
8067
|
-
const skillMd =
|
|
8291
|
+
const skillMd = path49.join(absPath, "SKILL.md");
|
|
8068
8292
|
if (!fs36.existsSync(skillMd)) {
|
|
8069
|
-
failures.push(`${item.id}: missing ${
|
|
8293
|
+
failures.push(`${item.id}: missing ${path49.relative(manifestDir, skillMd)}`);
|
|
8070
8294
|
continue;
|
|
8071
8295
|
}
|
|
8072
8296
|
const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
|
|
8073
8297
|
if (text === null) continue;
|
|
8074
8298
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
8075
8299
|
failures.push(
|
|
8076
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
8300
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path49.relative(manifestDir, skillMd)}`)
|
|
8077
8301
|
);
|
|
8078
8302
|
} else if (item.type === "agent") {
|
|
8079
8303
|
if (!fs36.existsSync(absPath)) {
|
|
8080
8304
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
8081
8305
|
continue;
|
|
8082
8306
|
}
|
|
8083
|
-
const rel =
|
|
8307
|
+
const rel = path49.relative(manifestDir, absPath);
|
|
8084
8308
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
8085
8309
|
if (text === null) continue;
|
|
8086
8310
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -8096,7 +8320,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
8096
8320
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
8097
8321
|
continue;
|
|
8098
8322
|
}
|
|
8099
|
-
const rel =
|
|
8323
|
+
const rel = path49.relative(manifestDir, absPath);
|
|
8100
8324
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
8101
8325
|
if (text === null) continue;
|
|
8102
8326
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -8128,7 +8352,7 @@ function walkAllFiles(dir, fn, failures) {
|
|
|
8128
8352
|
}
|
|
8129
8353
|
for (const entry of entries) {
|
|
8130
8354
|
if (entry.name === ".DS_Store") continue;
|
|
8131
|
-
const full =
|
|
8355
|
+
const full = path49.join(dir, entry.name);
|
|
8132
8356
|
if (entry.isDirectory()) walkAllFiles(full, fn, failures);
|
|
8133
8357
|
else fn(full);
|
|
8134
8358
|
}
|
|
@@ -8146,12 +8370,12 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
8146
8370
|
const failures = [];
|
|
8147
8371
|
const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
|
|
8148
8372
|
for (const dir of CONTENT_ROOTS) {
|
|
8149
|
-
const abs =
|
|
8373
|
+
const abs = path49.join(manifestDir, dir);
|
|
8150
8374
|
if (!fs36.existsSync(abs)) continue;
|
|
8151
8375
|
walkAllFiles(
|
|
8152
8376
|
abs,
|
|
8153
8377
|
(file) => {
|
|
8154
|
-
const relPath =
|
|
8378
|
+
const relPath = path49.relative(manifestDir, file).replace(/\\/g, "/");
|
|
8155
8379
|
if (!isPathClaimed(relPath, claimedPaths)) {
|
|
8156
8380
|
failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
|
|
8157
8381
|
}
|
|
@@ -8172,7 +8396,7 @@ function walkMd(dir, fn, failures) {
|
|
|
8172
8396
|
return;
|
|
8173
8397
|
}
|
|
8174
8398
|
for (const entry of entries) {
|
|
8175
|
-
const full =
|
|
8399
|
+
const full = path49.join(dir, entry.name);
|
|
8176
8400
|
if (entry.isDirectory()) walkMd(full, fn, failures);
|
|
8177
8401
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
8178
8402
|
}
|
|
@@ -8181,13 +8405,13 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
8181
8405
|
const failures = [];
|
|
8182
8406
|
const pathSourceMap = buildItemPathSourceMap(items);
|
|
8183
8407
|
for (const dir of ["skills", "agents", "templates", "commands"]) {
|
|
8184
|
-
const abs =
|
|
8408
|
+
const abs = path49.join(manifestDir, dir);
|
|
8185
8409
|
if (!fs36.existsSync(abs)) continue;
|
|
8186
8410
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
8187
8411
|
walkMd(
|
|
8188
8412
|
abs,
|
|
8189
8413
|
(file) => {
|
|
8190
|
-
const rel =
|
|
8414
|
+
const rel = path49.relative(manifestDir, file);
|
|
8191
8415
|
const text = readForAudit(file, rel, failures);
|
|
8192
8416
|
if (text === null) return;
|
|
8193
8417
|
const norm = rel.replace(/\\/g, "/");
|
|
@@ -8232,8 +8456,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
8232
8456
|
process.exitCode = 1;
|
|
8233
8457
|
return;
|
|
8234
8458
|
}
|
|
8235
|
-
const abs =
|
|
8236
|
-
const manifestDir =
|
|
8459
|
+
const abs = path50.resolve(process.cwd(), manifestPath2);
|
|
8460
|
+
const manifestDir = path50.dirname(abs);
|
|
8237
8461
|
const data = await readJson(abs);
|
|
8238
8462
|
if (!data?.items) {
|
|
8239
8463
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -8250,7 +8474,7 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
8250
8474
|
}
|
|
8251
8475
|
|
|
8252
8476
|
// src/workspace/worktree/add.ts
|
|
8253
|
-
import
|
|
8477
|
+
import path57 from "node:path";
|
|
8254
8478
|
import fs40 from "fs-extra";
|
|
8255
8479
|
|
|
8256
8480
|
// src/workspace/worktree/git-worktree.ts
|
|
@@ -8370,12 +8594,12 @@ async function hasUnpushedWork(cwd) {
|
|
|
8370
8594
|
}
|
|
8371
8595
|
|
|
8372
8596
|
// src/workspace/worktree/hydrate.ts
|
|
8373
|
-
import
|
|
8597
|
+
import path53 from "node:path";
|
|
8374
8598
|
import fs38 from "fs-extra";
|
|
8375
8599
|
|
|
8376
8600
|
// src/workspace/worktree/cow-copy.ts
|
|
8377
8601
|
init_exec();
|
|
8378
|
-
import
|
|
8602
|
+
import path51 from "node:path";
|
|
8379
8603
|
import fs37 from "fs-extra";
|
|
8380
8604
|
async function getMacFilesystemType(dir) {
|
|
8381
8605
|
const df = await runCommand("df", ["-P", dir]);
|
|
@@ -8410,11 +8634,11 @@ async function cowCopyDir(src, dest) {
|
|
|
8410
8634
|
const srcExists = await fs37.pathExists(src);
|
|
8411
8635
|
if (!srcExists)
|
|
8412
8636
|
return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
|
|
8413
|
-
const strategy = await detectCowStrategy(
|
|
8637
|
+
const strategy = await detectCowStrategy(path51.dirname(dest));
|
|
8414
8638
|
if (strategy === "unsupported" || strategy === "unknown-platform") {
|
|
8415
8639
|
return { strategy, attempted: false, ok: false };
|
|
8416
8640
|
}
|
|
8417
|
-
await fs37.ensureDir(
|
|
8641
|
+
await fs37.ensureDir(path51.dirname(dest));
|
|
8418
8642
|
const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
|
|
8419
8643
|
const result = await runCommand("cp", args);
|
|
8420
8644
|
return {
|
|
@@ -8428,7 +8652,7 @@ async function cowCopyDir(src, dest) {
|
|
|
8428
8652
|
// src/workspace/worktree/install.ts
|
|
8429
8653
|
init_exec();
|
|
8430
8654
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
8431
|
-
import
|
|
8655
|
+
import path52 from "node:path";
|
|
8432
8656
|
import fg5 from "fast-glob";
|
|
8433
8657
|
function detectInstallPlan(signals) {
|
|
8434
8658
|
if (signals.hasYarnLock) {
|
|
@@ -8449,10 +8673,10 @@ function detectInstallPlan(signals) {
|
|
|
8449
8673
|
return { manager: "none" };
|
|
8450
8674
|
}
|
|
8451
8675
|
async function computeLockfileSignals(dir) {
|
|
8452
|
-
const hasYarnLock = existsSync3(
|
|
8453
|
-
const hasPnpmLock = existsSync3(
|
|
8454
|
-
const hasPackageLock = existsSync3(
|
|
8455
|
-
const hasComposerLock = existsSync3(
|
|
8676
|
+
const hasYarnLock = existsSync3(path52.join(dir, "yarn.lock"));
|
|
8677
|
+
const hasPnpmLock = existsSync3(path52.join(dir, "pnpm-lock.yaml"));
|
|
8678
|
+
const hasPackageLock = existsSync3(path52.join(dir, "package-lock.json"));
|
|
8679
|
+
const hasComposerLock = existsSync3(path52.join(dir, "composer.lock"));
|
|
8456
8680
|
const dotnetFiles = await fg5(["**/*.csproj", "**/*.sln"], {
|
|
8457
8681
|
cwd: dir,
|
|
8458
8682
|
onlyFiles: true,
|
|
@@ -8466,7 +8690,7 @@ async function computeLockfileSignals(dir) {
|
|
|
8466
8690
|
});
|
|
8467
8691
|
const hasDotnetProject = dotnetFiles.length > 0;
|
|
8468
8692
|
let hasPackageManagerField = false;
|
|
8469
|
-
const pkgPath =
|
|
8693
|
+
const pkgPath = path52.join(dir, "package.json");
|
|
8470
8694
|
if (existsSync3(pkgPath)) {
|
|
8471
8695
|
try {
|
|
8472
8696
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
@@ -8507,8 +8731,8 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
8507
8731
|
const start = Date.now();
|
|
8508
8732
|
const targets = [];
|
|
8509
8733
|
for (const target of HYDRATION_TARGETS) {
|
|
8510
|
-
const src =
|
|
8511
|
-
const dest =
|
|
8734
|
+
const src = path53.join(mainMemberPath, target);
|
|
8735
|
+
const dest = path53.join(worktreeMemberPath, target);
|
|
8512
8736
|
if (!await fs38.pathExists(src)) continue;
|
|
8513
8737
|
const destExists = await fs38.pathExists(dest);
|
|
8514
8738
|
if (!opts.force && destExists) continue;
|
|
@@ -8526,11 +8750,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
8526
8750
|
|
|
8527
8751
|
// src/workspace/worktree/local-files.ts
|
|
8528
8752
|
init_exec();
|
|
8529
|
-
import
|
|
8753
|
+
import path54 from "node:path";
|
|
8530
8754
|
import fs39 from "fs-extra";
|
|
8531
8755
|
var MACHINE_LOCAL_FILES = [
|
|
8532
8756
|
"docker-compose.local.yml",
|
|
8533
|
-
|
|
8757
|
+
path54.join(".claude", "settings.local.json")
|
|
8534
8758
|
];
|
|
8535
8759
|
async function isTracked(repoRoot, relFile) {
|
|
8536
8760
|
const gitPath = relFile.replace(/\\/g, "/");
|
|
@@ -8540,11 +8764,11 @@ async function isTracked(repoRoot, relFile) {
|
|
|
8540
8764
|
async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
8541
8765
|
const copied = [];
|
|
8542
8766
|
for (const rel of MACHINE_LOCAL_FILES) {
|
|
8543
|
-
const src =
|
|
8767
|
+
const src = path54.join(mainMemberPath, rel);
|
|
8544
8768
|
if (!await fs39.pathExists(src)) continue;
|
|
8545
8769
|
if (await isTracked(mainMemberPath, rel)) continue;
|
|
8546
|
-
const dest =
|
|
8547
|
-
await fs39.ensureDir(
|
|
8770
|
+
const dest = path54.join(worktreeMemberPath, rel);
|
|
8771
|
+
await fs39.ensureDir(path54.dirname(dest));
|
|
8548
8772
|
await fs39.copy(src, dest, { overwrite: true });
|
|
8549
8773
|
copied.push(rel);
|
|
8550
8774
|
}
|
|
@@ -8553,15 +8777,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
|
8553
8777
|
|
|
8554
8778
|
// src/workspace/worktree/root.ts
|
|
8555
8779
|
import { existsSync as existsSync4 } from "node:fs";
|
|
8556
|
-
import
|
|
8780
|
+
import path55 from "node:path";
|
|
8557
8781
|
async function resolveWorkspaceForWorktree(start) {
|
|
8558
8782
|
const rootInfo = await resolveRoots(start);
|
|
8559
8783
|
if (!rootInfo.isGitRepo) {
|
|
8560
8784
|
return { ok: false, reason: "Not inside a git repository." };
|
|
8561
8785
|
}
|
|
8562
8786
|
const workspaceRoot = rootInfo.mainRoot;
|
|
8563
|
-
const hasYaml = existsSync4(
|
|
8564
|
-
const hasManifest = existsSync4(
|
|
8787
|
+
const hasYaml = existsSync4(path55.join(workspaceRoot, WORKSPACE_FILE));
|
|
8788
|
+
const hasManifest = existsSync4(path55.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
8565
8789
|
if (!hasYaml && !hasManifest) {
|
|
8566
8790
|
return {
|
|
8567
8791
|
ok: false,
|
|
@@ -8586,19 +8810,19 @@ function selectMembers(members, only) {
|
|
|
8586
8810
|
}
|
|
8587
8811
|
|
|
8588
8812
|
// src/workspace/worktree/state.ts
|
|
8589
|
-
import
|
|
8813
|
+
import path56 from "node:path";
|
|
8590
8814
|
var WORKTREE_STATE_FILE = ".haus-worktree.json";
|
|
8591
8815
|
function worktreesDir(mainRoot) {
|
|
8592
|
-
return
|
|
8816
|
+
return path56.join(mainRoot, ".claude", "worktrees");
|
|
8593
8817
|
}
|
|
8594
8818
|
function worktreePath(mainRoot, slug) {
|
|
8595
|
-
return
|
|
8819
|
+
return path56.join(worktreesDir(mainRoot), slug);
|
|
8596
8820
|
}
|
|
8597
8821
|
async function readWorktreeState(worktreeRoot) {
|
|
8598
|
-
return readJson(
|
|
8822
|
+
return readJson(path56.join(worktreeRoot, WORKTREE_STATE_FILE));
|
|
8599
8823
|
}
|
|
8600
8824
|
async function writeWorktreeState(worktreeRoot, state) {
|
|
8601
|
-
await writeJson(
|
|
8825
|
+
await writeJson(path56.join(worktreeRoot, WORKTREE_STATE_FILE), state);
|
|
8602
8826
|
}
|
|
8603
8827
|
|
|
8604
8828
|
// src/workspace/worktree/add.ts
|
|
@@ -8616,7 +8840,7 @@ async function runAdd(opts) {
|
|
|
8616
8840
|
if (await fs40.pathExists(wsWorktreePath)) {
|
|
8617
8841
|
return {
|
|
8618
8842
|
ok: false,
|
|
8619
|
-
error: `Workspace worktree already exists at ${wsWorktreePath} \u2014 run \`haus workspace worktree hydrate\` or \`remove\` first.`
|
|
8843
|
+
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.`
|
|
8620
8844
|
};
|
|
8621
8845
|
}
|
|
8622
8846
|
if (opts.dryRun) {
|
|
@@ -8632,7 +8856,7 @@ async function runAdd(opts) {
|
|
|
8632
8856
|
const memberResults = [];
|
|
8633
8857
|
for (const member of selected) {
|
|
8634
8858
|
const start = Date.now();
|
|
8635
|
-
const memberWorktreePath =
|
|
8859
|
+
const memberWorktreePath = path57.join(wsWorktreePath, member.folder);
|
|
8636
8860
|
const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
|
|
8637
8861
|
preferDefaultBranchFrom: true
|
|
8638
8862
|
});
|
|
@@ -8662,7 +8886,7 @@ async function runAdd(opts) {
|
|
|
8662
8886
|
await mapWithConcurrency(okResults, async (result) => {
|
|
8663
8887
|
const member = selected.find((m) => m.id === result.member);
|
|
8664
8888
|
if (!member) return;
|
|
8665
|
-
const memberWorktreePath =
|
|
8889
|
+
const memberWorktreePath = path57.join(wsWorktreePath, member.folder);
|
|
8666
8890
|
const hydrationStart = Date.now();
|
|
8667
8891
|
if (opts.hydrate !== false) {
|
|
8668
8892
|
result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
|
|
@@ -8729,10 +8953,10 @@ async function runDryRun(args) {
|
|
|
8729
8953
|
|
|
8730
8954
|
// src/workspace/worktree/doctor.ts
|
|
8731
8955
|
import { existsSync as existsSync5 } from "node:fs";
|
|
8732
|
-
import
|
|
8956
|
+
import path58 from "node:path";
|
|
8733
8957
|
var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
|
|
8734
8958
|
function looksHydrated(memberPath) {
|
|
8735
|
-
return HYDRATION_MARKERS.some((marker) => existsSync5(
|
|
8959
|
+
return HYDRATION_MARKERS.some((marker) => existsSync5(path58.join(memberPath, marker)));
|
|
8736
8960
|
}
|
|
8737
8961
|
async function runDoctor2() {
|
|
8738
8962
|
const rootInfo = await resolveRoots();
|
|
@@ -8746,7 +8970,7 @@ async function runDoctor2() {
|
|
|
8746
8970
|
};
|
|
8747
8971
|
}
|
|
8748
8972
|
const workspaceRoot = rootInfo.mainRoot;
|
|
8749
|
-
const isWorkspace = existsSync5(
|
|
8973
|
+
const isWorkspace = existsSync5(path58.join(workspaceRoot, WORKSPACE_FILE)) || existsSync5(path58.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
8750
8974
|
if (!isWorkspace) {
|
|
8751
8975
|
return {
|
|
8752
8976
|
isWorkspace: false,
|
|
@@ -8759,7 +8983,7 @@ async function runDoctor2() {
|
|
|
8759
8983
|
};
|
|
8760
8984
|
}
|
|
8761
8985
|
const wsWorktreesDir = worktreesDir(workspaceRoot);
|
|
8762
|
-
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${
|
|
8986
|
+
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path58.sep}`);
|
|
8763
8987
|
const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
|
|
8764
8988
|
const members = await readMembers(rootInfo);
|
|
8765
8989
|
const problems = [];
|
|
@@ -8767,10 +8991,10 @@ async function runDoctor2() {
|
|
|
8767
8991
|
let slug;
|
|
8768
8992
|
if (inWorkspaceWorktree) {
|
|
8769
8993
|
const wtPath = rootInfo.repoRoot;
|
|
8770
|
-
slug = rootInfo.worktreeName ??
|
|
8994
|
+
slug = rootInfo.worktreeName ?? path58.basename(wtPath);
|
|
8771
8995
|
const state = await readWorktreeState(wtPath);
|
|
8772
8996
|
for (const member of members) {
|
|
8773
|
-
const memberPath =
|
|
8997
|
+
const memberPath = path58.join(wtPath, member.folder);
|
|
8774
8998
|
const materialized = existsSync5(memberPath);
|
|
8775
8999
|
const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
|
|
8776
9000
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
@@ -8801,16 +9025,16 @@ async function runDoctor2() {
|
|
|
8801
9025
|
for (const member of members) {
|
|
8802
9026
|
const entries = await listWorktrees(member.absPath);
|
|
8803
9027
|
for (const entry of entries) {
|
|
8804
|
-
const entryPath =
|
|
8805
|
-
const marker = `${
|
|
9028
|
+
const entryPath = path58.normalize(entry.path);
|
|
9029
|
+
const marker = `${path58.sep}.claude${path58.sep}worktrees${path58.sep}`;
|
|
8806
9030
|
const idx = entryPath.indexOf(marker);
|
|
8807
9031
|
if (idx === -1) continue;
|
|
8808
9032
|
const rest = entryPath.slice(idx + marker.length);
|
|
8809
|
-
const slugName = rest.split(
|
|
9033
|
+
const slugName = rest.split(path58.sep)[0];
|
|
8810
9034
|
if (!slugName) continue;
|
|
8811
|
-
if (!existsSync5(
|
|
9035
|
+
if (!existsSync5(path58.join(wsWorktreesDir, slugName))) {
|
|
8812
9036
|
orphans.push(
|
|
8813
|
-
`${member.id}: worktree registered at ${entry.path} but ${
|
|
9037
|
+
`${member.id}: worktree registered at ${entry.path} but ${path58.join(wsWorktreesDir, slugName)} no longer exists \u2014 run \`git -C "${member.absPath}" worktree prune\`.`
|
|
8814
9038
|
);
|
|
8815
9039
|
}
|
|
8816
9040
|
}
|
|
@@ -8821,7 +9045,7 @@ async function runDoctor2() {
|
|
|
8821
9045
|
|
|
8822
9046
|
// src/workspace/worktree/hydrate-command.ts
|
|
8823
9047
|
import { existsSync as existsSync6 } from "node:fs";
|
|
8824
|
-
import
|
|
9048
|
+
import path59 from "node:path";
|
|
8825
9049
|
async function runHydrateWorktree(opts) {
|
|
8826
9050
|
const rootInfo = await resolveRoots();
|
|
8827
9051
|
if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
|
|
@@ -8832,7 +9056,7 @@ async function runHydrateWorktree(opts) {
|
|
|
8832
9056
|
}
|
|
8833
9057
|
const workspaceRoot = rootInfo.mainRoot;
|
|
8834
9058
|
const wtPath = rootInfo.repoRoot;
|
|
8835
|
-
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${
|
|
9059
|
+
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path59.sep}`)) {
|
|
8836
9060
|
return {
|
|
8837
9061
|
ok: false,
|
|
8838
9062
|
error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
|
|
@@ -8844,49 +9068,97 @@ async function runHydrateWorktree(opts) {
|
|
|
8844
9068
|
return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
|
|
8845
9069
|
}
|
|
8846
9070
|
const state = await readWorktreeState(wtPath);
|
|
8847
|
-
const slug = state?.slug ??
|
|
9071
|
+
const slug = state?.slug ?? path59.basename(wtPath);
|
|
9072
|
+
const detectedBranch = await currentBranch(wtPath);
|
|
9073
|
+
const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
|
|
8848
9074
|
if (opts.dryRun) {
|
|
8849
|
-
const results2 =
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
9075
|
+
const results2 = await Promise.all(
|
|
9076
|
+
selected.map(async (member) => {
|
|
9077
|
+
const memberPath = path59.join(wtPath, member.folder);
|
|
9078
|
+
if (existsSync6(memberPath)) {
|
|
9079
|
+
return { member: member.id, folder: member.folder, status: "planned" };
|
|
9080
|
+
}
|
|
9081
|
+
if (!opts.materialize) {
|
|
9082
|
+
return {
|
|
9083
|
+
member: member.id,
|
|
9084
|
+
folder: member.folder,
|
|
9085
|
+
status: "skipped",
|
|
9086
|
+
error: "not materialized in this worktree"
|
|
9087
|
+
};
|
|
9088
|
+
}
|
|
9089
|
+
const branchAction = await branchExists(member.absPath, targetBranch) ? "checkout" : "create";
|
|
9090
|
+
return { member: member.id, folder: member.folder, status: "planned", branchAction };
|
|
9091
|
+
})
|
|
9092
|
+
);
|
|
8858
9093
|
return { ok: true, dryRun: true, slug, results: results2 };
|
|
8859
9094
|
}
|
|
8860
9095
|
const results = await mapWithConcurrency(
|
|
8861
9096
|
selected,
|
|
8862
9097
|
async (member) => {
|
|
8863
|
-
const memberPath =
|
|
9098
|
+
const memberPath = path59.join(wtPath, member.folder);
|
|
9099
|
+
let branchAction;
|
|
8864
9100
|
if (!existsSync6(memberPath)) {
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
9101
|
+
if (!opts.materialize) {
|
|
9102
|
+
return {
|
|
9103
|
+
member: member.id,
|
|
9104
|
+
folder: member.folder,
|
|
9105
|
+
status: "skipped",
|
|
9106
|
+
error: "not materialized in this worktree"
|
|
9107
|
+
};
|
|
9108
|
+
}
|
|
9109
|
+
const outcome = await addWorktree(member.absPath, memberPath, targetBranch, {
|
|
9110
|
+
preferDefaultBranchFrom: true
|
|
9111
|
+
});
|
|
9112
|
+
if (!outcome.ok) {
|
|
9113
|
+
return {
|
|
9114
|
+
member: member.id,
|
|
9115
|
+
folder: member.folder,
|
|
9116
|
+
status: "failed",
|
|
9117
|
+
error: outcome.checkedOutElsewhere ? `Branch "${targetBranch}" is already checked out elsewhere in ${member.id} \u2014 remove that worktree or resolve the branch conflict first.` : outcome.stderr ?? "unknown error creating the member worktree"
|
|
9118
|
+
};
|
|
9119
|
+
}
|
|
9120
|
+
branchAction = outcome.branchAction;
|
|
8871
9121
|
}
|
|
8872
9122
|
try {
|
|
8873
9123
|
const hydration = await hydrateMember(member.absPath, memberPath, { force: opts.force });
|
|
8874
|
-
return { member: member.id, folder: member.folder, status: "ok", hydration };
|
|
9124
|
+
return { member: member.id, folder: member.folder, status: "ok", hydration, branchAction };
|
|
8875
9125
|
} catch (err) {
|
|
8876
9126
|
return {
|
|
8877
9127
|
member: member.id,
|
|
8878
9128
|
folder: member.folder,
|
|
8879
9129
|
status: "failed",
|
|
8880
|
-
error: err instanceof Error ? err.message : String(err)
|
|
9130
|
+
error: err instanceof Error ? err.message : String(err),
|
|
9131
|
+
branchAction
|
|
8881
9132
|
};
|
|
8882
9133
|
}
|
|
8883
9134
|
}
|
|
8884
9135
|
);
|
|
9136
|
+
const newlyMaterialized = results.filter((r) => r.branchAction && r.status === "ok");
|
|
9137
|
+
if (newlyMaterialized.length > 0) {
|
|
9138
|
+
const byId = new Map((state?.members ?? []).map((m) => [m.id, m]));
|
|
9139
|
+
for (const r of newlyMaterialized) {
|
|
9140
|
+
const member = selected.find((m) => m.id === r.member);
|
|
9141
|
+
if (!member) continue;
|
|
9142
|
+
const entry = {
|
|
9143
|
+
id: member.id,
|
|
9144
|
+
folder: member.folder,
|
|
9145
|
+
branch: targetBranch,
|
|
9146
|
+
absPath: member.absPath
|
|
9147
|
+
};
|
|
9148
|
+
byId.set(member.id, entry);
|
|
9149
|
+
}
|
|
9150
|
+
await writeWorktreeState(wtPath, {
|
|
9151
|
+
slug,
|
|
9152
|
+
branch: state?.branch ?? targetBranch,
|
|
9153
|
+
createdAt: state?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
9154
|
+
members: Array.from(byId.values())
|
|
9155
|
+
});
|
|
9156
|
+
}
|
|
8885
9157
|
return { ok: results.every((r) => r.status !== "failed"), dryRun: false, slug, results };
|
|
8886
9158
|
}
|
|
8887
9159
|
|
|
8888
9160
|
// src/workspace/worktree/list.ts
|
|
8889
|
-
import
|
|
9161
|
+
import path60 from "node:path";
|
|
8890
9162
|
import fs41 from "fs-extra";
|
|
8891
9163
|
async function runList2() {
|
|
8892
9164
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -8899,12 +9171,12 @@ async function runList2() {
|
|
|
8899
9171
|
const configuredMembers = await readMembers(rootInfo);
|
|
8900
9172
|
const worktrees = [];
|
|
8901
9173
|
for (const slug of slugs) {
|
|
8902
|
-
const wtPath =
|
|
9174
|
+
const wtPath = path60.join(dir, slug);
|
|
8903
9175
|
const state = await readWorktreeState(wtPath);
|
|
8904
9176
|
const members = [];
|
|
8905
9177
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
8906
9178
|
for (const m of source) {
|
|
8907
|
-
const memberPath =
|
|
9179
|
+
const memberPath = path60.join(wtPath, m.folder);
|
|
8908
9180
|
const materialized = await fs41.pathExists(memberPath);
|
|
8909
9181
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
8910
9182
|
members.push({
|
|
@@ -8926,7 +9198,7 @@ async function runList2() {
|
|
|
8926
9198
|
}
|
|
8927
9199
|
|
|
8928
9200
|
// src/workspace/worktree/remove.ts
|
|
8929
|
-
import
|
|
9201
|
+
import path61 from "node:path";
|
|
8930
9202
|
import fs42 from "fs-extra";
|
|
8931
9203
|
async function runRemove(opts) {
|
|
8932
9204
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -8953,7 +9225,7 @@ async function runRemove(opts) {
|
|
|
8953
9225
|
targets.push({
|
|
8954
9226
|
repo: configMember?.id ?? stateMember.id,
|
|
8955
9227
|
ownerRepo,
|
|
8956
|
-
path:
|
|
9228
|
+
path: path61.join(wsWorktreePath, stateMember.folder)
|
|
8957
9229
|
});
|
|
8958
9230
|
}
|
|
8959
9231
|
const existingTargets = [];
|
|
@@ -9081,7 +9353,8 @@ async function handleHydrate(opts) {
|
|
|
9081
9353
|
const hydrateOpts = {
|
|
9082
9354
|
only: normalizeOnly(opts.only),
|
|
9083
9355
|
force: Boolean(opts.force),
|
|
9084
|
-
dryRun: Boolean(opts.dryRun)
|
|
9356
|
+
dryRun: Boolean(opts.dryRun),
|
|
9357
|
+
materialize: Boolean(opts.materialize)
|
|
9085
9358
|
};
|
|
9086
9359
|
const result = await runHydrateWorktree(hydrateOpts);
|
|
9087
9360
|
if (!result.ok && "error" in result) {
|
|
@@ -9091,14 +9364,20 @@ async function handleHydrate(opts) {
|
|
|
9091
9364
|
}
|
|
9092
9365
|
if (result.dryRun) {
|
|
9093
9366
|
log(`[dry-run] Would hydrate "${result.slug}":`);
|
|
9094
|
-
for (const r of result.results)
|
|
9095
|
-
|
|
9367
|
+
for (const r of result.results) {
|
|
9368
|
+
const action = r.branchAction ? ` (${r.branchAction} member worktree first)` : "";
|
|
9369
|
+
log(`[dry-run] ${r.member}: ${r.status}${action}${r.error ? ` (${r.error})` : ""}`);
|
|
9370
|
+
}
|
|
9096
9371
|
return;
|
|
9097
9372
|
}
|
|
9098
9373
|
log(`Hydrated "${result.slug}":`);
|
|
9099
9374
|
for (const r of result.results) {
|
|
9100
|
-
if (r.status === "ok" && r.hydration)
|
|
9101
|
-
|
|
9375
|
+
if (r.status === "ok" && r.hydration) {
|
|
9376
|
+
const materializedNote = r.branchAction ? `materialized (${r.branchAction}), ` : "";
|
|
9377
|
+
log(` ${r.member}: ${materializedNote}${describeInstall(r.hydration)}`);
|
|
9378
|
+
} else {
|
|
9379
|
+
warn(` ${r.member}: ${r.status}${r.error ? ` \u2014 ${r.error}` : ""}`);
|
|
9380
|
+
}
|
|
9102
9381
|
}
|
|
9103
9382
|
if (!result.ok) process.exitCode = 1;
|
|
9104
9383
|
}
|
|
@@ -9157,6 +9436,13 @@ async function handleList() {
|
|
|
9157
9436
|
}
|
|
9158
9437
|
}
|
|
9159
9438
|
}
|
|
9439
|
+
function emitFromHookContext(additionalContext) {
|
|
9440
|
+
log(
|
|
9441
|
+
JSON.stringify({
|
|
9442
|
+
hookSpecificOutput: { hookEventName: "SessionStart", additionalContext }
|
|
9443
|
+
})
|
|
9444
|
+
);
|
|
9445
|
+
}
|
|
9160
9446
|
async function handleDoctor(opts) {
|
|
9161
9447
|
try {
|
|
9162
9448
|
const report = await runDoctor2();
|
|
@@ -9168,17 +9454,26 @@ async function handleDoctor(opts) {
|
|
|
9168
9454
|
return;
|
|
9169
9455
|
}
|
|
9170
9456
|
if (report.problems.length === 0) {
|
|
9171
|
-
|
|
9172
|
-
|
|
9457
|
+
if (!opts.fromHook) {
|
|
9458
|
+
log(
|
|
9459
|
+
report.inWorkspaceWorktree ? `Workspace worktree "${report.slug}" looks healthy.` : "No issues found."
|
|
9460
|
+
);
|
|
9461
|
+
}
|
|
9462
|
+
return;
|
|
9463
|
+
}
|
|
9464
|
+
if (opts.fromHook) {
|
|
9465
|
+
const scope = report.slug ? `Workspace worktree "${report.slug}"` : "This haus workspace";
|
|
9466
|
+
emitFromHookContext(
|
|
9467
|
+
`${scope} has ${report.problems.length} issue(s): ${report.problems.join("; ")}`
|
|
9173
9468
|
);
|
|
9174
9469
|
return;
|
|
9175
9470
|
}
|
|
9176
9471
|
for (const p of report.problems) warn(p);
|
|
9177
|
-
|
|
9472
|
+
process.exitCode = 1;
|
|
9178
9473
|
} catch (err) {
|
|
9179
9474
|
const message = err instanceof Error ? err.message : String(err);
|
|
9180
9475
|
if (opts.fromHook) {
|
|
9181
|
-
|
|
9476
|
+
emitFromHookContext(`\`haus workspace worktree doctor\` failed: ${message}`);
|
|
9182
9477
|
} else {
|
|
9183
9478
|
error(message);
|
|
9184
9479
|
process.exitCode = 1;
|
|
@@ -9202,7 +9497,7 @@ async function runWorktree(action, opts = {}) {
|
|
|
9202
9497
|
|
|
9203
9498
|
// src/commands/workspace.ts
|
|
9204
9499
|
import { existsSync as existsSync10, statSync as statSync3 } from "node:fs";
|
|
9205
|
-
import
|
|
9500
|
+
import path70 from "node:path";
|
|
9206
9501
|
|
|
9207
9502
|
// src/commands/workspace/aggregate.ts
|
|
9208
9503
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -9257,18 +9552,18 @@ ${summaries.map(
|
|
|
9257
9552
|
|
|
9258
9553
|
// src/commands/workspace/doctor.ts
|
|
9259
9554
|
import { existsSync as existsSync7 } from "node:fs";
|
|
9260
|
-
import
|
|
9555
|
+
import path63 from "node:path";
|
|
9261
9556
|
|
|
9262
9557
|
// src/commands/workspace/manifest.ts
|
|
9263
9558
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
9264
|
-
import
|
|
9559
|
+
import path62 from "node:path";
|
|
9265
9560
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
9266
9561
|
function manifestPath(workspaceRoot) {
|
|
9267
9562
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
9268
9563
|
}
|
|
9269
9564
|
function hausVersion() {
|
|
9270
9565
|
try {
|
|
9271
|
-
const pkg = JSON.parse(readFileSync4(
|
|
9566
|
+
const pkg = JSON.parse(readFileSync4(path62.join(packageRoot(), "package.json"), "utf8"));
|
|
9272
9567
|
return pkg.version ?? "0.0.0";
|
|
9273
9568
|
} catch {
|
|
9274
9569
|
return "0.0.0";
|
|
@@ -9352,7 +9647,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
9352
9647
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
9353
9648
|
const catalogRefByRepo = [];
|
|
9354
9649
|
for (const repo of config.repos) {
|
|
9355
|
-
const repoRoot =
|
|
9650
|
+
const repoRoot = path63.resolve(workspaceRoot, repo.path);
|
|
9356
9651
|
const entry = manifestByName.get(repo.name);
|
|
9357
9652
|
if (!entry) {
|
|
9358
9653
|
flag({
|
|
@@ -9425,7 +9720,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
9425
9720
|
});
|
|
9426
9721
|
continue;
|
|
9427
9722
|
}
|
|
9428
|
-
if (!existsSync7(
|
|
9723
|
+
if (!existsSync7(path63.join(workspaceRoot, entry.path))) {
|
|
9429
9724
|
flag({
|
|
9430
9725
|
repo: entry.repo,
|
|
9431
9726
|
kind: "missing-linked-context-copy",
|
|
@@ -9468,15 +9763,15 @@ function emit(args) {
|
|
|
9468
9763
|
}
|
|
9469
9764
|
|
|
9470
9765
|
// src/workspace/link-context/link.ts
|
|
9471
|
-
import
|
|
9766
|
+
import path66 from "node:path";
|
|
9472
9767
|
import fs45 from "fs-extra";
|
|
9473
9768
|
|
|
9474
9769
|
// src/workspace/link-context/plan.ts
|
|
9475
|
-
import
|
|
9770
|
+
import path65 from "node:path";
|
|
9476
9771
|
import fs44 from "fs-extra";
|
|
9477
9772
|
|
|
9478
9773
|
// src/workspace/link-context/scan-source.ts
|
|
9479
|
-
import
|
|
9774
|
+
import path64 from "node:path";
|
|
9480
9775
|
import fs43 from "fs-extra";
|
|
9481
9776
|
var ASSET_SUBDIR = {
|
|
9482
9777
|
skill: "skills",
|
|
@@ -9485,36 +9780,36 @@ var ASSET_SUBDIR = {
|
|
|
9485
9780
|
};
|
|
9486
9781
|
async function hasLinkableContext(member) {
|
|
9487
9782
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
9488
|
-
if (await fs43.pathExists(
|
|
9783
|
+
if (await fs43.pathExists(path64.join(member.absPath, ".claude", subdir))) return true;
|
|
9489
9784
|
}
|
|
9490
9785
|
return false;
|
|
9491
9786
|
}
|
|
9492
9787
|
async function collectSkills(member) {
|
|
9493
|
-
const dir =
|
|
9788
|
+
const dir = path64.join(member.absPath, ".claude", "skills");
|
|
9494
9789
|
if (!await fs43.pathExists(dir)) return [];
|
|
9495
9790
|
const names = await fs43.readdir(dir);
|
|
9496
9791
|
const out = [];
|
|
9497
9792
|
for (const name of [...names].sort()) {
|
|
9498
|
-
const abs =
|
|
9793
|
+
const abs = path64.join(dir, name);
|
|
9499
9794
|
const stat = await fs43.lstat(abs);
|
|
9500
9795
|
if (!stat.isDirectory()) continue;
|
|
9501
|
-
if (!await fs43.pathExists(
|
|
9502
|
-
out.push({ type: "skill", name, sourceRelPath:
|
|
9796
|
+
if (!await fs43.pathExists(path64.join(abs, "SKILL.md"))) continue;
|
|
9797
|
+
out.push({ type: "skill", name, sourceRelPath: path64.posix.join(".claude", "skills", name) });
|
|
9503
9798
|
}
|
|
9504
9799
|
return out;
|
|
9505
9800
|
}
|
|
9506
9801
|
async function collectFlatMarkdown(member, type) {
|
|
9507
9802
|
const subdir = ASSET_SUBDIR[type];
|
|
9508
|
-
const dir =
|
|
9803
|
+
const dir = path64.join(member.absPath, ".claude", subdir);
|
|
9509
9804
|
if (!await fs43.pathExists(dir)) return [];
|
|
9510
9805
|
const names = await fs43.readdir(dir);
|
|
9511
9806
|
const out = [];
|
|
9512
9807
|
for (const fileName of [...names].sort()) {
|
|
9513
9808
|
if (!fileName.endsWith(".md")) continue;
|
|
9514
|
-
const abs =
|
|
9809
|
+
const abs = path64.join(dir, fileName);
|
|
9515
9810
|
if (!(await fs43.lstat(abs)).isFile()) continue;
|
|
9516
9811
|
const name = fileName.slice(0, -".md".length);
|
|
9517
|
-
out.push({ type, name, sourceRelPath:
|
|
9812
|
+
out.push({ type, name, sourceRelPath: path64.posix.join(".claude", subdir, fileName) });
|
|
9518
9813
|
}
|
|
9519
9814
|
return out;
|
|
9520
9815
|
}
|
|
@@ -9529,9 +9824,9 @@ async function collectSourceAssets(member) {
|
|
|
9529
9824
|
|
|
9530
9825
|
// src/workspace/link-context/plan.ts
|
|
9531
9826
|
function repoFolderPrefix(member) {
|
|
9532
|
-
const base =
|
|
9827
|
+
const base = path65.posix.basename(member.folder);
|
|
9533
9828
|
if (base && base !== "." && base !== "/") return base;
|
|
9534
|
-
const fromAbs =
|
|
9829
|
+
const fromAbs = path65.basename(member.absPath);
|
|
9535
9830
|
return fromAbs || member.id;
|
|
9536
9831
|
}
|
|
9537
9832
|
function assetKey(type, destKey) {
|
|
@@ -9588,7 +9883,7 @@ async function buildLinkPlan(members) {
|
|
|
9588
9883
|
name: asset.name,
|
|
9589
9884
|
destKey,
|
|
9590
9885
|
sourceRelPath: asset.sourceRelPath,
|
|
9591
|
-
sourceAbsPath:
|
|
9886
|
+
sourceAbsPath: path65.join(member.absPath, asset.sourceRelPath),
|
|
9592
9887
|
sourceHash
|
|
9593
9888
|
});
|
|
9594
9889
|
}
|
|
@@ -9599,7 +9894,7 @@ async function buildLinkPlan(members) {
|
|
|
9599
9894
|
function destRelPathFor(type, destKey) {
|
|
9600
9895
|
const subdir = `${type}s`;
|
|
9601
9896
|
const leaf = type === "skill" ? destKey : `${destKey}.md`;
|
|
9602
|
-
return
|
|
9897
|
+
return path66.posix.join(".claude", subdir, leaf);
|
|
9603
9898
|
}
|
|
9604
9899
|
async function runLinkContext(workspaceRoot, opts = {}) {
|
|
9605
9900
|
const write = opts.write ?? false;
|
|
@@ -9625,9 +9920,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
9625
9920
|
const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9626
9921
|
for (const entry of plan.entries) {
|
|
9627
9922
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
9628
|
-
const destAbs =
|
|
9923
|
+
const destAbs = path66.join(workspaceRoot, destRel);
|
|
9629
9924
|
if (write) {
|
|
9630
|
-
await fs45.ensureDir(
|
|
9925
|
+
await fs45.ensureDir(path66.dirname(destAbs));
|
|
9631
9926
|
if (await fs45.pathExists(destAbs)) await fs45.remove(destAbs);
|
|
9632
9927
|
await fs45.copy(entry.sourceAbsPath, destAbs, {
|
|
9633
9928
|
overwrite: true,
|
|
@@ -9659,7 +9954,7 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
9659
9954
|
if (nextPaths.has(old.path)) continue;
|
|
9660
9955
|
removed.push(old.path);
|
|
9661
9956
|
if (write) {
|
|
9662
|
-
const abs =
|
|
9957
|
+
const abs = path66.join(workspaceRoot, old.path);
|
|
9663
9958
|
if (await fs45.pathExists(abs)) await fs45.remove(abs);
|
|
9664
9959
|
}
|
|
9665
9960
|
}
|
|
@@ -9712,10 +10007,10 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
|
|
|
9712
10007
|
|
|
9713
10008
|
// src/commands/workspace/setup.ts
|
|
9714
10009
|
import { existsSync as existsSync8, statSync } from "node:fs";
|
|
9715
|
-
import
|
|
10010
|
+
import path68 from "node:path";
|
|
9716
10011
|
|
|
9717
10012
|
// src/claude/write-workspace-claude-md.ts
|
|
9718
|
-
import
|
|
10013
|
+
import path67 from "node:path";
|
|
9719
10014
|
import fs46 from "fs-extra";
|
|
9720
10015
|
function buildWorkspaceImportBlock(client, members) {
|
|
9721
10016
|
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
@@ -9734,7 +10029,7 @@ ${BLOCK_END}`;
|
|
|
9734
10029
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
9735
10030
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
9736
10031
|
const dryRun = opts.dryRun ?? false;
|
|
9737
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
10032
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path67.join(workspaceRoot, "CLAUDE.md");
|
|
9738
10033
|
const say = opts.quiet ? () => {
|
|
9739
10034
|
} : log;
|
|
9740
10035
|
const existed = await fs46.pathExists(filePath);
|
|
@@ -9765,20 +10060,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
9765
10060
|
|
|
9766
10061
|
// src/commands/workspace/setup.ts
|
|
9767
10062
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
9768
|
-
let dir =
|
|
10063
|
+
let dir = path68.resolve(start);
|
|
9769
10064
|
for (; ; ) {
|
|
9770
|
-
if (existsSync8(
|
|
9771
|
-
const parent =
|
|
9772
|
-
if (parent === dir) return
|
|
10065
|
+
if (existsSync8(path68.join(dir, WORKSPACE_FILE))) return dir;
|
|
10066
|
+
const parent = path68.dirname(dir);
|
|
10067
|
+
if (parent === dir) return path68.resolve(start);
|
|
9773
10068
|
dir = parent;
|
|
9774
10069
|
}
|
|
9775
10070
|
}
|
|
9776
10071
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
9777
|
-
return
|
|
10072
|
+
return path68.resolve(workspaceRoot, repoPath) === path68.resolve(workspaceRoot);
|
|
9778
10073
|
}
|
|
9779
10074
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
9780
10075
|
const apply = options.write ?? false;
|
|
9781
|
-
const configText = await readText(
|
|
10076
|
+
const configText = await readText(path68.join(workspaceRoot, WORKSPACE_FILE));
|
|
9782
10077
|
if (!configText) {
|
|
9783
10078
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
9784
10079
|
process.exitCode = 1;
|
|
@@ -9805,7 +10100,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
9805
10100
|
const statuses = [];
|
|
9806
10101
|
const aggregateInputs = [];
|
|
9807
10102
|
for (const repo of repos) {
|
|
9808
|
-
const repoRoot =
|
|
10103
|
+
const repoRoot = path68.resolve(workspaceRoot, repo.path);
|
|
9809
10104
|
say(`
|
|
9810
10105
|
\u2192 ${repo.name} (${repo.path})`);
|
|
9811
10106
|
try {
|
|
@@ -9876,7 +10171,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
9876
10171
|
const status = statusByName.get(repo.name);
|
|
9877
10172
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
9878
10173
|
if (status?.status === "ok") {
|
|
9879
|
-
const lock = await checkLock(
|
|
10174
|
+
const lock = await checkLock(path68.resolve(workspaceRoot, repo.path));
|
|
9880
10175
|
manifestRepos.push({
|
|
9881
10176
|
name: repo.name,
|
|
9882
10177
|
path: repo.path,
|
|
@@ -9932,7 +10227,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
9932
10227
|
try {
|
|
9933
10228
|
const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
|
|
9934
10229
|
if (linkResult.ok) {
|
|
9935
|
-
written.push(...linkResult.added.map((rel) =>
|
|
10230
|
+
written.push(...linkResult.added.map((rel) => path68.join(workspaceRoot, rel)));
|
|
9936
10231
|
if (!options.json) {
|
|
9937
10232
|
say(
|
|
9938
10233
|
`Linked ${linkResult.linked.length} cross-repo skill/agent/command copy(ies) into .claude/ (${linkResult.added.length} new, ${linkResult.removed.length} removed)${linkResult.skipped.length > 0 ? ` \u2014 ${linkResult.skipped.length} member(s) skipped, see above` : ""}.`
|
|
@@ -9964,7 +10259,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
9964
10259
|
|
|
9965
10260
|
// src/commands/workspace/undo.ts
|
|
9966
10261
|
import { existsSync as existsSync9, statSync as statSync2 } from "node:fs";
|
|
9967
|
-
import
|
|
10262
|
+
import path69 from "node:path";
|
|
9968
10263
|
import fs47 from "fs-extra";
|
|
9969
10264
|
var WORKSPACE_AGGREGATE_FILES = [
|
|
9970
10265
|
"workspace-summary.json",
|
|
@@ -9993,7 +10288,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
9993
10288
|
}
|
|
9994
10289
|
let anyRepoFailed = false;
|
|
9995
10290
|
for (const repo of config.repos) {
|
|
9996
|
-
const repoRoot =
|
|
10291
|
+
const repoRoot = path69.resolve(workspaceRoot, repo.path);
|
|
9997
10292
|
log(`
|
|
9998
10293
|
\u2192 ${repo.name} (${repo.path})`);
|
|
9999
10294
|
try {
|
|
@@ -10008,7 +10303,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10008
10303
|
}
|
|
10009
10304
|
const linkedContext = await readLinkedContext(workspaceRoot);
|
|
10010
10305
|
for (const entry of linkedContext) {
|
|
10011
|
-
const abs =
|
|
10306
|
+
const abs = path69.join(workspaceRoot, entry.path);
|
|
10012
10307
|
if (await fs47.pathExists(abs)) {
|
|
10013
10308
|
await fs47.remove(abs);
|
|
10014
10309
|
log(`Removed ${entry.path} (linked ${entry.type} from ${entry.repo})`);
|
|
@@ -10018,20 +10313,20 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10018
10313
|
const abs = hausPath(workspaceRoot, rel);
|
|
10019
10314
|
if (await fs47.pathExists(abs)) {
|
|
10020
10315
|
await fs47.remove(abs);
|
|
10021
|
-
log(`Removed ${
|
|
10316
|
+
log(`Removed ${path69.relative(workspaceRoot, abs)}`);
|
|
10022
10317
|
}
|
|
10023
10318
|
}
|
|
10024
10319
|
const manifestFile = manifestPath(workspaceRoot);
|
|
10025
10320
|
if (await fs47.pathExists(manifestFile)) {
|
|
10026
10321
|
await fs47.remove(manifestFile);
|
|
10027
|
-
log(`Removed ${
|
|
10322
|
+
log(`Removed ${path69.relative(workspaceRoot, manifestFile)}`);
|
|
10028
10323
|
}
|
|
10029
10324
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
10030
10325
|
if (await fs47.pathExists(workspaceMdPath)) {
|
|
10031
10326
|
await fs47.remove(workspaceMdPath);
|
|
10032
|
-
log(`Removed ${
|
|
10327
|
+
log(`Removed ${path69.relative(workspaceRoot, workspaceMdPath)}`);
|
|
10033
10328
|
} else {
|
|
10034
|
-
const claudeMdPath =
|
|
10329
|
+
const claudeMdPath = path69.join(workspaceRoot, "CLAUDE.md");
|
|
10035
10330
|
if (existsSync9(claudeMdPath)) {
|
|
10036
10331
|
const prev = await readText(claudeMdPath);
|
|
10037
10332
|
if (prev?.includes(BLOCK_BEGIN)) {
|
|
@@ -10083,7 +10378,7 @@ relationships: []
|
|
|
10083
10378
|
log("Workspace initialized.");
|
|
10084
10379
|
}
|
|
10085
10380
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
10086
|
-
const configText = await readText(
|
|
10381
|
+
const configText = await readText(path70.join(workspaceRoot, WORKSPACE_FILE));
|
|
10087
10382
|
if (!configText) {
|
|
10088
10383
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
10089
10384
|
process.exitCode = 1;
|
|
@@ -10105,7 +10400,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
10105
10400
|
const inputs = [];
|
|
10106
10401
|
const failures = [];
|
|
10107
10402
|
for (const repo of config.repos) {
|
|
10108
|
-
const repoRoot =
|
|
10403
|
+
const repoRoot = path70.resolve(workspaceRoot, repo.path);
|
|
10109
10404
|
try {
|
|
10110
10405
|
if (!existsSync10(repoRoot) || !statSync3(repoRoot).isDirectory()) {
|
|
10111
10406
|
throw new Error("path is not a directory");
|
|
@@ -10171,7 +10466,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
10171
10466
|
// src/cli.ts
|
|
10172
10467
|
function cliVersion() {
|
|
10173
10468
|
try {
|
|
10174
|
-
const pkgPath =
|
|
10469
|
+
const pkgPath = path71.join(packageRoot(), "package.json");
|
|
10175
10470
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
10176
10471
|
return pkg.version ?? "0.0.0";
|
|
10177
10472
|
} catch {
|
|
@@ -10181,7 +10476,7 @@ function cliVersion() {
|
|
|
10181
10476
|
var program = new Command();
|
|
10182
10477
|
function validateRuntimeNodeVersion() {
|
|
10183
10478
|
try {
|
|
10184
|
-
const pkgPath =
|
|
10479
|
+
const pkgPath = path71.join(packageRoot(), "package.json");
|
|
10185
10480
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
10186
10481
|
const requiredRange = pkg.engines?.node;
|
|
10187
10482
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -10282,8 +10577,16 @@ worktree.command("add <slug>").description(
|
|
|
10282
10577
|
dryRun: opts.dryRun
|
|
10283
10578
|
})
|
|
10284
10579
|
);
|
|
10285
|
-
worktree.command("hydrate").description("Re-run CoW clone + install-reconciliation for the current workspace worktree").option("--only <repos>", "Restrict to comma- or space-separated member ids/folders").option("--force", "Re-clone hydration targets even if already present").option(
|
|
10286
|
-
|
|
10580
|
+
worktree.command("hydrate").description("Re-run CoW clone + install-reconciliation for the current workspace worktree").option("--only <repos>", "Restrict to comma- or space-separated member ids/folders").option("--force", "Re-clone hydration targets even if already present").option(
|
|
10581
|
+
"--materialize",
|
|
10582
|
+
"Also `git worktree add` any member missing entirely (e.g. a worktree created by plain `git worktree add` outside haus) before hydrating it"
|
|
10583
|
+
).option("--dry-run", "Preview without writing or running anything").action(
|
|
10584
|
+
(opts) => runWorktree("hydrate", {
|
|
10585
|
+
only: opts.only,
|
|
10586
|
+
force: opts.force,
|
|
10587
|
+
materialize: opts.materialize,
|
|
10588
|
+
dryRun: opts.dryRun
|
|
10589
|
+
})
|
|
10287
10590
|
);
|
|
10288
10591
|
worktree.command("list").description("List materialized workspace worktrees and their member status").action(() => runWorktree("list"));
|
|
10289
10592
|
worktree.command("remove <slug>").description("Remove a workspace worktree and its member worktrees").option("--force", "Bypass the uncommitted/unpushed-work refusal").option("--dry-run", "Preview without removing anything").action(
|