@haus-tech/haus-workflow 1.5.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/cli.js +564 -296
- 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,11 @@ 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);
|
|
4529
4715
|
}
|
|
4530
4716
|
async function readPathOverrides(workspaceRoot) {
|
|
4531
|
-
const filePath =
|
|
4717
|
+
const filePath = path31.join(workspaceRoot, REPOS_LOCAL_FILE);
|
|
4532
4718
|
const result = await readJsonDetailed(filePath);
|
|
4533
4719
|
if (result.status === "missing") return {};
|
|
4534
4720
|
if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
|
|
@@ -4544,7 +4730,7 @@ async function readPathOverrides(workspaceRoot) {
|
|
|
4544
4730
|
return overrides;
|
|
4545
4731
|
}
|
|
4546
4732
|
async function readFromWorkspaceYaml(workspaceRoot, overrides) {
|
|
4547
|
-
const filePath =
|
|
4733
|
+
const filePath = path31.join(workspaceRoot, WORKSPACE_FILE);
|
|
4548
4734
|
const text = await readText(filePath);
|
|
4549
4735
|
if (text === void 0) return void 0;
|
|
4550
4736
|
const config = parseWorkspaceConfig(text);
|
|
@@ -4557,7 +4743,7 @@ async function readFromWorkspaceYaml(workspaceRoot, overrides) {
|
|
|
4557
4743
|
}));
|
|
4558
4744
|
}
|
|
4559
4745
|
async function readFromManifest(workspaceRoot, overrides) {
|
|
4560
|
-
const filePath =
|
|
4746
|
+
const filePath = path31.join(workspaceRoot, REPOS_MANIFEST_FILE);
|
|
4561
4747
|
const result = await readJsonDetailed(filePath);
|
|
4562
4748
|
if (result.status === "missing") return void 0;
|
|
4563
4749
|
if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
|
|
@@ -4599,16 +4785,16 @@ async function readMembers(rootInfo) {
|
|
|
4599
4785
|
}
|
|
4600
4786
|
|
|
4601
4787
|
// src/scanner/scan-project.ts
|
|
4602
|
-
import
|
|
4788
|
+
import path36 from "node:path";
|
|
4603
4789
|
|
|
4604
4790
|
// src/utils/audit-checks.ts
|
|
4605
|
-
function
|
|
4791
|
+
function isRecord2(v) {
|
|
4606
4792
|
return typeof v === "object" && v !== null;
|
|
4607
4793
|
}
|
|
4608
4794
|
|
|
4609
4795
|
// src/utils/git-root.ts
|
|
4610
4796
|
init_exec();
|
|
4611
|
-
import
|
|
4797
|
+
import path32 from "node:path";
|
|
4612
4798
|
function fallback(start) {
|
|
4613
4799
|
return {
|
|
4614
4800
|
cwd: start,
|
|
@@ -4624,11 +4810,11 @@ function fallback(start) {
|
|
|
4624
4810
|
async function resolveAbsoluteGitPath(flag, cwd, repoRoot) {
|
|
4625
4811
|
const withFormat = await runGit(["rev-parse", "--path-format=absolute", flag], { cwd });
|
|
4626
4812
|
const raw = withFormat.exitCode === 0 ? withFormat.stdout.trim() : "";
|
|
4627
|
-
if (raw &&
|
|
4813
|
+
if (raw && path32.isAbsolute(raw)) return path32.resolve(raw);
|
|
4628
4814
|
const plain = await runGit(["rev-parse", flag], { cwd });
|
|
4629
4815
|
const plainRaw = plain.exitCode === 0 ? plain.stdout.trim() : "";
|
|
4630
4816
|
if (!plainRaw) return "";
|
|
4631
|
-
return
|
|
4817
|
+
return path32.isAbsolute(plainRaw) ? path32.resolve(plainRaw) : path32.resolve(repoRoot, plainRaw);
|
|
4632
4818
|
}
|
|
4633
4819
|
async function resolveRoots(start) {
|
|
4634
4820
|
const cwd = start ?? process.cwd();
|
|
@@ -4637,15 +4823,15 @@ async function resolveRoots(start) {
|
|
|
4637
4823
|
if (toplevel.exitCode !== 0) return fallback(cwd);
|
|
4638
4824
|
const toplevelRaw = toplevel.stdout.trim();
|
|
4639
4825
|
if (!toplevelRaw) return fallback(cwd);
|
|
4640
|
-
const repoRoot =
|
|
4826
|
+
const repoRoot = path32.resolve(toplevelRaw);
|
|
4641
4827
|
const [gitDir, gitCommonDir] = await Promise.all([
|
|
4642
4828
|
resolveAbsoluteGitPath("--git-dir", cwd, repoRoot),
|
|
4643
4829
|
resolveAbsoluteGitPath("--git-common-dir", cwd, repoRoot)
|
|
4644
4830
|
]);
|
|
4645
4831
|
if (!gitDir || !gitCommonDir) return fallback(cwd);
|
|
4646
4832
|
const isLinkedWorktree = gitDir !== gitCommonDir;
|
|
4647
|
-
const mainRoot = isLinkedWorktree &&
|
|
4648
|
-
const worktreeName = isLinkedWorktree ?
|
|
4833
|
+
const mainRoot = isLinkedWorktree && path32.basename(gitCommonDir) === ".git" ? path32.dirname(gitCommonDir) : repoRoot;
|
|
4834
|
+
const worktreeName = isLinkedWorktree ? path32.basename(gitDir) : null;
|
|
4649
4835
|
return {
|
|
4650
4836
|
cwd,
|
|
4651
4837
|
repoRoot,
|
|
@@ -4681,7 +4867,7 @@ function compareVersions(a, b) {
|
|
|
4681
4867
|
}
|
|
4682
4868
|
|
|
4683
4869
|
// src/scanner/detect-package-manager.ts
|
|
4684
|
-
import
|
|
4870
|
+
import path33 from "node:path";
|
|
4685
4871
|
import fs27 from "fs-extra";
|
|
4686
4872
|
function detectPackageManager(root, packageManagerField) {
|
|
4687
4873
|
const field = String(packageManagerField ?? "").trim();
|
|
@@ -4700,9 +4886,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
4700
4886
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
4701
4887
|
return "unknown";
|
|
4702
4888
|
}
|
|
4703
|
-
if (fs27.existsSync(
|
|
4704
|
-
if (fs27.existsSync(
|
|
4705
|
-
if (fs27.existsSync(
|
|
4889
|
+
if (fs27.existsSync(path33.join(root, "yarn.lock"))) return "yarn";
|
|
4890
|
+
if (fs27.existsSync(path33.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
4891
|
+
if (fs27.existsSync(path33.join(root, "package-lock.json"))) return "npm";
|
|
4706
4892
|
return "unknown";
|
|
4707
4893
|
}
|
|
4708
4894
|
|
|
@@ -4912,7 +5098,7 @@ function runDetection(ctx, rules = STACK_RULES) {
|
|
|
4912
5098
|
}
|
|
4913
5099
|
|
|
4914
5100
|
// src/scanner/detection.ts
|
|
4915
|
-
import
|
|
5101
|
+
import path34 from "node:path";
|
|
4916
5102
|
var UNSUPPORTED_MARKERS = {
|
|
4917
5103
|
"requirements.txt": "python",
|
|
4918
5104
|
"pyproject.toml": "python",
|
|
@@ -4944,7 +5130,7 @@ function blocked(rel) {
|
|
|
4944
5130
|
function dependencySet(pkg, composer) {
|
|
4945
5131
|
const depNames = /* @__PURE__ */ new Set();
|
|
4946
5132
|
const pushObj = (obj) => {
|
|
4947
|
-
if (!
|
|
5133
|
+
if (!isRecord2(obj)) return;
|
|
4948
5134
|
for (const key of Object.keys(obj)) depNames.add(key);
|
|
4949
5135
|
};
|
|
4950
5136
|
pushObj(pkg?.dependencies);
|
|
@@ -4972,14 +5158,14 @@ function finalizeRoles(registryRoles, deps, files) {
|
|
|
4972
5158
|
function collectUnsupportedSignals(files) {
|
|
4973
5159
|
return [
|
|
4974
5160
|
...new Set(
|
|
4975
|
-
files.map((f) => UNSUPPORTED_MARKERS[
|
|
5161
|
+
files.map((f) => UNSUPPORTED_MARKERS[path34.basename(f)]).filter((s) => Boolean(s))
|
|
4976
5162
|
)
|
|
4977
5163
|
].sort();
|
|
4978
5164
|
}
|
|
4979
5165
|
|
|
4980
5166
|
// src/scanner/render.ts
|
|
4981
5167
|
import { readFile } from "node:fs/promises";
|
|
4982
|
-
import
|
|
5168
|
+
import path35 from "node:path";
|
|
4983
5169
|
async function buildContentBlob(root, files) {
|
|
4984
5170
|
const candidates = files.filter(
|
|
4985
5171
|
(f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
|
|
@@ -4987,7 +5173,7 @@ async function buildContentBlob(root, files) {
|
|
|
4987
5173
|
const slice = candidates.slice(0, 300);
|
|
4988
5174
|
const parts = await mapWithConcurrency(slice, async (rel) => {
|
|
4989
5175
|
try {
|
|
4990
|
-
return await readFile(
|
|
5176
|
+
return await readFile(path35.join(root, rel), "utf8");
|
|
4991
5177
|
} catch {
|
|
4992
5178
|
return "";
|
|
4993
5179
|
}
|
|
@@ -5079,8 +5265,8 @@ var SAFE_FILES = [
|
|
|
5079
5265
|
"Gemfile"
|
|
5080
5266
|
];
|
|
5081
5267
|
async function scanProject(root) {
|
|
5082
|
-
const pkg = await readJson(
|
|
5083
|
-
const composer = await readJson(
|
|
5268
|
+
const pkg = await readJson(path36.join(root, "package.json"));
|
|
5269
|
+
const composer = await readJson(path36.join(root, "composer.json"));
|
|
5084
5270
|
const files = await listFiles(root, SAFE_FILES);
|
|
5085
5271
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
5086
5272
|
const deps = dependencySet(pkg, composer);
|
|
@@ -5094,7 +5280,7 @@ async function scanProject(root) {
|
|
|
5094
5280
|
const warnings = [];
|
|
5095
5281
|
const securityRisks = [];
|
|
5096
5282
|
const crossRepoHints = [];
|
|
5097
|
-
const nodeEngine =
|
|
5283
|
+
const nodeEngine = isRecord2(pkg?.engines) ? String(pkg.engines.node ?? "") : "";
|
|
5098
5284
|
if (nodeEngine && !satisfiesVersion(process.version, nodeEngine)) {
|
|
5099
5285
|
warnings.push(`Current Node ${process.version} does not satisfy package engine ${nodeEngine}`);
|
|
5100
5286
|
}
|
|
@@ -5104,7 +5290,7 @@ async function scanProject(root) {
|
|
|
5104
5290
|
crossRepoHints.push("Monorepo orchestration detected");
|
|
5105
5291
|
const unsupportedSignals = collectUnsupportedSignals(safeFiles);
|
|
5106
5292
|
const detectionStatus = computeDetectionStatus(roles, stacks, unsupportedSignals);
|
|
5107
|
-
const repoNameFallback = pkg?.name ? String(pkg.name) :
|
|
5293
|
+
const repoNameFallback = pkg?.name ? String(pkg.name) : path36.basename((await resolveRoots(root)).mainRoot);
|
|
5108
5294
|
const context = {
|
|
5109
5295
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5110
5296
|
repoName: repoNameFallback,
|
|
@@ -5126,9 +5312,9 @@ async function scanProject(root) {
|
|
|
5126
5312
|
// src/scanner/read-context.ts
|
|
5127
5313
|
async function isCacheFresh(root, cachePath) {
|
|
5128
5314
|
const markerPaths = [
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5315
|
+
path37.join(root, "package.json"),
|
|
5316
|
+
path37.join(root, WORKSPACE_FILE),
|
|
5317
|
+
path37.join(root, REPOS_MANIFEST_FILE)
|
|
5132
5318
|
];
|
|
5133
5319
|
const cacheStat = await fs28.stat(cachePath);
|
|
5134
5320
|
for (const markerPath of markerPaths) {
|
|
@@ -5234,7 +5420,7 @@ async function runDoctor(options) {
|
|
|
5234
5420
|
} else {
|
|
5235
5421
|
ok(`- HOOKS OK: ${hooks.message}`);
|
|
5236
5422
|
}
|
|
5237
|
-
const rootClaudeMdPath =
|
|
5423
|
+
const rootClaudeMdPath = path38.join(root, "CLAUDE.md");
|
|
5238
5424
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
5239
5425
|
if (!rootClaudeMdContent) {
|
|
5240
5426
|
flag(
|
|
@@ -5288,8 +5474,8 @@ async function runDoctor(options) {
|
|
|
5288
5474
|
} else {
|
|
5289
5475
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
5290
5476
|
const bodyContent = workflowContent.slice(firstLine.length + 1);
|
|
5291
|
-
const cachePath =
|
|
5292
|
-
const bundledPath =
|
|
5477
|
+
const cachePath = path38.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
5478
|
+
const bundledPath = path38.join(
|
|
5293
5479
|
packageRoot(),
|
|
5294
5480
|
"library",
|
|
5295
5481
|
"global",
|
|
@@ -5338,7 +5524,7 @@ async function runDoctor(options) {
|
|
|
5338
5524
|
}
|
|
5339
5525
|
}
|
|
5340
5526
|
if (workflowExists) {
|
|
5341
|
-
const prettierIgnore = await readText(
|
|
5527
|
+
const prettierIgnore = await readText(path38.join(root, ".prettierignore")) ?? "";
|
|
5342
5528
|
const lines = prettierIgnore.split("\n");
|
|
5343
5529
|
const missing = IGNORED_PATHS.filter((p) => !prettierIgnoreProtects(lines, p));
|
|
5344
5530
|
if (missing.length > 0) {
|
|
@@ -5411,7 +5597,7 @@ async function runDoctor(options) {
|
|
|
5411
5597
|
);
|
|
5412
5598
|
}
|
|
5413
5599
|
}
|
|
5414
|
-
const pkgJson = await readJson(
|
|
5600
|
+
const pkgJson = await readJson(path38.join(packageRoot(), "package.json"));
|
|
5415
5601
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
5416
5602
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
5417
5603
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -5444,7 +5630,7 @@ async function runDoctor(options) {
|
|
|
5444
5630
|
}
|
|
5445
5631
|
|
|
5446
5632
|
// src/commands/update.ts
|
|
5447
|
-
import
|
|
5633
|
+
import path41 from "node:path";
|
|
5448
5634
|
|
|
5449
5635
|
// src/claude/refresh-project.ts
|
|
5450
5636
|
import fs30 from "fs-extra";
|
|
@@ -5461,7 +5647,7 @@ async function refreshProjectApply(root) {
|
|
|
5461
5647
|
|
|
5462
5648
|
// src/install/apply.ts
|
|
5463
5649
|
import crypto2 from "node:crypto";
|
|
5464
|
-
import
|
|
5650
|
+
import path39 from "node:path";
|
|
5465
5651
|
import fs31 from "fs-extra";
|
|
5466
5652
|
|
|
5467
5653
|
// src/install/header.ts
|
|
@@ -5548,7 +5734,7 @@ function hashContent(content2) {
|
|
|
5548
5734
|
}
|
|
5549
5735
|
function sourceVersion() {
|
|
5550
5736
|
try {
|
|
5551
|
-
const pkgPath =
|
|
5737
|
+
const pkgPath = path39.join(packageRoot(), "package.json");
|
|
5552
5738
|
const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf8"));
|
|
5553
5739
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
5554
5740
|
} catch {
|
|
@@ -5556,49 +5742,49 @@ function sourceVersion() {
|
|
|
5556
5742
|
}
|
|
5557
5743
|
}
|
|
5558
5744
|
function globalSrcDir() {
|
|
5559
|
-
return
|
|
5745
|
+
return path39.join(packageRoot(), "library", "global");
|
|
5560
5746
|
}
|
|
5561
5747
|
function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
5562
|
-
const skillDir =
|
|
5748
|
+
const skillDir = path39.join(skillsDir, skillName);
|
|
5563
5749
|
const entries = [];
|
|
5564
5750
|
for (const rel of fs31.readdirSync(skillDir, { recursive: true })) {
|
|
5565
5751
|
if (rel === "SKILL.md") continue;
|
|
5566
|
-
const abs =
|
|
5752
|
+
const abs = path39.join(skillDir, rel);
|
|
5567
5753
|
if (!fs31.statSync(abs).isFile()) continue;
|
|
5568
|
-
const posixRel = rel.split(
|
|
5754
|
+
const posixRel = rel.split(path39.sep).join("/");
|
|
5569
5755
|
entries.push({
|
|
5570
5756
|
stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
|
|
5571
|
-
srcRelPath:
|
|
5572
|
-
destPath:
|
|
5757
|
+
srcRelPath: path39.join("library", "global", "skills", skillName, rel),
|
|
5758
|
+
destPath: path39.join(claudeDir, "skills", skillName, rel)
|
|
5573
5759
|
});
|
|
5574
5760
|
}
|
|
5575
5761
|
return entries;
|
|
5576
5762
|
}
|
|
5577
5763
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
5578
5764
|
const entries = [];
|
|
5579
|
-
const skillsDir =
|
|
5765
|
+
const skillsDir = path39.join(srcDir, "skills");
|
|
5580
5766
|
if (fs31.pathExistsSync(skillsDir)) {
|
|
5581
5767
|
for (const skillName of fs31.readdirSync(skillsDir)) {
|
|
5582
|
-
const skillFile =
|
|
5768
|
+
const skillFile = path39.join(skillsDir, skillName, "SKILL.md");
|
|
5583
5769
|
if (fs31.pathExistsSync(skillFile)) {
|
|
5584
5770
|
entries.push({
|
|
5585
5771
|
stableId: `skill.${skillName}`,
|
|
5586
|
-
srcRelPath:
|
|
5587
|
-
destPath:
|
|
5772
|
+
srcRelPath: path39.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
5773
|
+
destPath: path39.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
5588
5774
|
});
|
|
5589
5775
|
entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
|
|
5590
5776
|
}
|
|
5591
5777
|
}
|
|
5592
5778
|
}
|
|
5593
|
-
const commandsDir =
|
|
5779
|
+
const commandsDir = path39.join(srcDir, "commands");
|
|
5594
5780
|
if (fs31.pathExistsSync(commandsDir)) {
|
|
5595
5781
|
for (const fileName of fs31.readdirSync(commandsDir)) {
|
|
5596
5782
|
if (!fileName.endsWith(".md")) continue;
|
|
5597
5783
|
const commandName = fileName.slice(0, -".md".length);
|
|
5598
5784
|
entries.push({
|
|
5599
5785
|
stableId: `command.${commandName}`,
|
|
5600
|
-
srcRelPath:
|
|
5601
|
-
destPath:
|
|
5786
|
+
srcRelPath: path39.join("library", "global", "commands", fileName),
|
|
5787
|
+
destPath: path39.join(claudeDir, "commands", fileName)
|
|
5602
5788
|
});
|
|
5603
5789
|
}
|
|
5604
5790
|
}
|
|
@@ -5624,7 +5810,7 @@ async function applyInstall(options = {}) {
|
|
|
5624
5810
|
};
|
|
5625
5811
|
const manifestFiles = [];
|
|
5626
5812
|
for (const entry of sourceFiles) {
|
|
5627
|
-
const srcPath =
|
|
5813
|
+
const srcPath = path39.join(packageRoot(), entry.srcRelPath);
|
|
5628
5814
|
const rawContent = await readText(srcPath);
|
|
5629
5815
|
if (rawContent === void 0) {
|
|
5630
5816
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -5684,7 +5870,7 @@ async function applyInstall(options = {}) {
|
|
|
5684
5870
|
}
|
|
5685
5871
|
let mergedSettings;
|
|
5686
5872
|
if (!check) {
|
|
5687
|
-
const fragmentPath =
|
|
5873
|
+
const fragmentPath = path39.join(srcDir, "settings-fragments", "hooks.json");
|
|
5688
5874
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
5689
5875
|
const settings = await readSettings();
|
|
5690
5876
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -5773,7 +5959,7 @@ function summarizeLockDiff(before, after) {
|
|
|
5773
5959
|
|
|
5774
5960
|
// src/update/lockfile.ts
|
|
5775
5961
|
import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
|
|
5776
|
-
import
|
|
5962
|
+
import path40 from "node:path";
|
|
5777
5963
|
async function readLockSummary(root) {
|
|
5778
5964
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
5779
5965
|
const catalogRef = lock.find((item) => item.catalogRef)?.catalogRef ?? null;
|
|
@@ -5809,7 +5995,7 @@ async function applyLock(root) {
|
|
|
5809
5995
|
try {
|
|
5810
5996
|
const backupDir = hausPath(root, "backups");
|
|
5811
5997
|
await mkdir(backupDir, { recursive: true });
|
|
5812
|
-
await copyFile(lockPath,
|
|
5998
|
+
await copyFile(lockPath, path40.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
5813
5999
|
} catch {
|
|
5814
6000
|
}
|
|
5815
6001
|
const enriched = await Promise.all(
|
|
@@ -5831,7 +6017,7 @@ function diffLock(before, after) {
|
|
|
5831
6017
|
}
|
|
5832
6018
|
async function hasLocalOverrides(root) {
|
|
5833
6019
|
try {
|
|
5834
|
-
await readFile2(
|
|
6020
|
+
await readFile2(path40.join(root, ".claude", "settings.json"), "utf8");
|
|
5835
6021
|
return true;
|
|
5836
6022
|
} catch {
|
|
5837
6023
|
return false;
|
|
@@ -5847,7 +6033,7 @@ async function runUpdate(options) {
|
|
|
5847
6033
|
return;
|
|
5848
6034
|
}
|
|
5849
6035
|
if (options.check) {
|
|
5850
|
-
const pkgJson2 = await readJson(
|
|
6036
|
+
const pkgJson2 = await readJson(path41.join(packageRoot(), "package.json"));
|
|
5851
6037
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
5852
6038
|
const [status, npmVersion, latestCatalogTag, globalInstallDrift, catalogItems, lockItems] = await Promise.all([
|
|
5853
6039
|
options.fast ? fastLockStatus(root) : checkLock(root),
|
|
@@ -5882,7 +6068,7 @@ async function runUpdate(options) {
|
|
|
5882
6068
|
if (hasHashDrift || formerIdMigrations.length > 0) process.exitCode = 1;
|
|
5883
6069
|
return;
|
|
5884
6070
|
}
|
|
5885
|
-
const pkgJson = await readJson(
|
|
6071
|
+
const pkgJson = await readJson(path41.join(packageRoot(), "package.json"));
|
|
5886
6072
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
5887
6073
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
5888
6074
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -5924,7 +6110,7 @@ async function runFromHookCheck(root) {
|
|
|
5924
6110
|
try {
|
|
5925
6111
|
const summary = await readLockSummary(root);
|
|
5926
6112
|
if (summary.count === 0) return;
|
|
5927
|
-
const pkgJson = await readJson(
|
|
6113
|
+
const pkgJson = await readJson(path41.join(packageRoot(), "package.json"));
|
|
5928
6114
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
5929
6115
|
const [npmVersion, latestCatalogTag] = await Promise.all([
|
|
5930
6116
|
fetchNpmVersionStatus(currentVersion),
|
|
@@ -6074,7 +6260,7 @@ async function runCiGate(options = {}) {
|
|
|
6074
6260
|
// src/commands/clone.ts
|
|
6075
6261
|
init_exec();
|
|
6076
6262
|
import { existsSync as existsSync2 } from "node:fs";
|
|
6077
|
-
import
|
|
6263
|
+
import path42 from "node:path";
|
|
6078
6264
|
var GIT_LOCATION_VARS = [
|
|
6079
6265
|
"GIT_DIR",
|
|
6080
6266
|
"GIT_WORK_TREE",
|
|
@@ -6124,11 +6310,11 @@ async function runClone(url, opts = {}) {
|
|
|
6124
6310
|
process.exitCode = 1;
|
|
6125
6311
|
return;
|
|
6126
6312
|
}
|
|
6127
|
-
const target =
|
|
6313
|
+
const target = path42.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
6128
6314
|
if (existsSync2(target)) {
|
|
6129
6315
|
const check = await checkExistingTarget(target, url);
|
|
6130
6316
|
if (check.kind === "matches") {
|
|
6131
|
-
log(`\u2022 ${
|
|
6317
|
+
log(`\u2022 ${path42.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
|
|
6132
6318
|
return;
|
|
6133
6319
|
}
|
|
6134
6320
|
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 +6354,7 @@ function readHookPayload() {
|
|
|
6168
6354
|
if (!raw) return { payload: {}, malformed: false };
|
|
6169
6355
|
try {
|
|
6170
6356
|
const parsed = JSON.parse(raw);
|
|
6171
|
-
return { payload:
|
|
6357
|
+
return { payload: isRecord2(parsed) ? parsed : {}, malformed: false };
|
|
6172
6358
|
} catch {
|
|
6173
6359
|
return { payload: {}, malformed: true };
|
|
6174
6360
|
}
|
|
@@ -6207,7 +6393,7 @@ function readPayload(options) {
|
|
|
6207
6393
|
if (options.stdinPayload != null) {
|
|
6208
6394
|
try {
|
|
6209
6395
|
const parsed = JSON.parse(options.stdinPayload);
|
|
6210
|
-
return { payload:
|
|
6396
|
+
return { payload: isRecord2(parsed) ? parsed : {}, malformed: false };
|
|
6211
6397
|
} catch {
|
|
6212
6398
|
return { payload: {}, malformed: true };
|
|
6213
6399
|
}
|
|
@@ -6220,7 +6406,7 @@ async function runDecisionsGuard(options = {}) {
|
|
|
6220
6406
|
emitPreToolUseDeny("Malformed hook payload");
|
|
6221
6407
|
return;
|
|
6222
6408
|
}
|
|
6223
|
-
const toolInput =
|
|
6409
|
+
const toolInput = isRecord2(payload.tool_input) ? payload.tool_input : {};
|
|
6224
6410
|
const command = String(toolInput.command ?? "");
|
|
6225
6411
|
if (!PR_CREATE_PATTERN.test(command)) return;
|
|
6226
6412
|
const root = process.cwd();
|
|
@@ -6232,13 +6418,13 @@ async function runDecisionsGuard(options = {}) {
|
|
|
6232
6418
|
const result = await runDecisionsCheck(root, { range, commitMessages });
|
|
6233
6419
|
if (result.triggered && !result.satisfied) {
|
|
6234
6420
|
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.`
|
|
6421
|
+
`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
6422
|
);
|
|
6237
6423
|
}
|
|
6238
6424
|
}
|
|
6239
6425
|
|
|
6240
6426
|
// src/commands/decisions.ts
|
|
6241
|
-
import
|
|
6427
|
+
import path43 from "node:path";
|
|
6242
6428
|
|
|
6243
6429
|
// src/decisions/next-number.ts
|
|
6244
6430
|
async function nextDecisionNumber(root) {
|
|
@@ -6281,10 +6467,9 @@ _(Why this over alternatives \u2014 the line agents read for "why".)_
|
|
|
6281
6467
|
`;
|
|
6282
6468
|
}
|
|
6283
6469
|
async function runDecisionsSuggest(root, opts = {}) {
|
|
6284
|
-
const stats = await collectDiffStats(root, { staged: true });
|
|
6285
6470
|
const stagedCheck = await runDecisionsCheck(root, { staged: true });
|
|
6286
|
-
const
|
|
6287
|
-
const triggered =
|
|
6471
|
+
const unstagedCheck = await runDecisionsCheck(root, {});
|
|
6472
|
+
const triggered = stagedCheck.triggered || unstagedCheck.triggered;
|
|
6288
6473
|
if (!triggered) return;
|
|
6289
6474
|
if (stagedCheck.triggered && stagedCheck.satisfied) return;
|
|
6290
6475
|
const number = await nextDecisionNumber(root);
|
|
@@ -6335,7 +6520,7 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
6335
6520
|
}
|
|
6336
6521
|
for (const reason of result.reasons) warn(reason);
|
|
6337
6522
|
error(
|
|
6338
|
-
|
|
6523
|
+
`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
6524
|
);
|
|
6340
6525
|
process.exitCode = 1;
|
|
6341
6526
|
return;
|
|
@@ -6349,7 +6534,7 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
6349
6534
|
return;
|
|
6350
6535
|
}
|
|
6351
6536
|
if (sub === "validate") {
|
|
6352
|
-
const target = arg ?
|
|
6537
|
+
const target = arg ? path43.resolve(root, arg) : void 0;
|
|
6353
6538
|
if (!target) {
|
|
6354
6539
|
error("usage: haus decisions validate <path-to-decision.md>");
|
|
6355
6540
|
process.exitCode = 1;
|
|
@@ -6601,7 +6786,7 @@ async function runGuard(kind, _options) {
|
|
|
6601
6786
|
emitPreToolUseDeny("Malformed hook payload");
|
|
6602
6787
|
return;
|
|
6603
6788
|
}
|
|
6604
|
-
const toolInput =
|
|
6789
|
+
const toolInput = isRecord2(payload.tool_input) ? payload.tool_input : {};
|
|
6605
6790
|
if (kind === "file-access") {
|
|
6606
6791
|
const candidate = String(toolInput.path ?? toolInput.file_path ?? "");
|
|
6607
6792
|
const reason2 = guardFileAccess(candidate);
|
|
@@ -6618,7 +6803,7 @@ async function runGuard(kind, _options) {
|
|
|
6618
6803
|
}
|
|
6619
6804
|
|
|
6620
6805
|
// src/commands/init.ts
|
|
6621
|
-
import
|
|
6806
|
+
import path45 from "node:path";
|
|
6622
6807
|
import fs32 from "fs-extra";
|
|
6623
6808
|
|
|
6624
6809
|
// src/recommender/git-signal.ts
|
|
@@ -7086,7 +7271,7 @@ function buildOptInEligible(items, skipped) {
|
|
|
7086
7271
|
}
|
|
7087
7272
|
|
|
7088
7273
|
// src/commands/workspace/discover.ts
|
|
7089
|
-
import
|
|
7274
|
+
import path44 from "node:path";
|
|
7090
7275
|
import fg4 from "fast-glob";
|
|
7091
7276
|
import YAML2 from "yaml";
|
|
7092
7277
|
var DEFAULT_MAX_DEPTH = 3;
|
|
@@ -7127,10 +7312,10 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
7127
7312
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
7128
7313
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
7129
7314
|
for (const match of gitMatches) {
|
|
7130
|
-
gitDirs.add(
|
|
7315
|
+
gitDirs.add(path44.posix.dirname(match));
|
|
7131
7316
|
}
|
|
7132
7317
|
for (const match of manifestMatches) {
|
|
7133
|
-
manifestDirs.add(
|
|
7318
|
+
manifestDirs.add(path44.posix.dirname(match));
|
|
7134
7319
|
}
|
|
7135
7320
|
const repoRoots = [...gitDirs];
|
|
7136
7321
|
const manifestSorted = [...manifestDirs].sort(
|
|
@@ -7147,9 +7332,9 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
7147
7332
|
async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
7148
7333
|
const repoRoots = await findRepoRoots(workspaceRoot, maxDepth);
|
|
7149
7334
|
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 :
|
|
7335
|
+
const absDir = path44.resolve(workspaceRoot, relDir);
|
|
7336
|
+
const pkg = await readJson(path44.join(absDir, "package.json"));
|
|
7337
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path44.basename(relDir === "." ? workspaceRoot : absDir);
|
|
7153
7338
|
let role = "auto";
|
|
7154
7339
|
try {
|
|
7155
7340
|
const scan = await scanProject(absDir);
|
|
@@ -7192,7 +7377,7 @@ function renderWorkspaceYaml(config) {
|
|
|
7192
7377
|
});
|
|
7193
7378
|
}
|
|
7194
7379
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
7195
|
-
const yamlPath =
|
|
7380
|
+
const yamlPath = path44.join(workspaceRoot, "haus.workspace.yaml");
|
|
7196
7381
|
const existingText = await readText(yamlPath);
|
|
7197
7382
|
const existing = parseWorkspaceConfig(existingText);
|
|
7198
7383
|
if (existingText && !existing) {
|
|
@@ -7364,7 +7549,7 @@ async function runSetupProject(options) {
|
|
|
7364
7549
|
// src/commands/init.ts
|
|
7365
7550
|
async function runInit(options) {
|
|
7366
7551
|
const root = process.cwd();
|
|
7367
|
-
const hausDir =
|
|
7552
|
+
const hausDir = path45.join(root, ".haus-workflow");
|
|
7368
7553
|
const alreadyInit = await fs32.pathExists(hausDir);
|
|
7369
7554
|
if (alreadyInit) {
|
|
7370
7555
|
log("Haus AI already initialized in this project.");
|
|
@@ -7500,7 +7685,7 @@ async function runRefresh() {
|
|
|
7500
7685
|
}
|
|
7501
7686
|
|
|
7502
7687
|
// src/install/scaffold.ts
|
|
7503
|
-
import
|
|
7688
|
+
import path46 from "node:path";
|
|
7504
7689
|
import fs33 from "fs-extra";
|
|
7505
7690
|
async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
7506
7691
|
const scaffolded = [];
|
|
@@ -7517,9 +7702,9 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
7517
7702
|
const realRoot = await fs33.realpath(catalogRoot).catch(() => null);
|
|
7518
7703
|
for (const item of items) {
|
|
7519
7704
|
if (item.type !== "config") continue;
|
|
7520
|
-
const sourcePath =
|
|
7521
|
-
const rel =
|
|
7522
|
-
const escapes = rel === "" ||
|
|
7705
|
+
const sourcePath = path46.resolve(catalogRoot, item.path);
|
|
7706
|
+
const rel = path46.relative(catalogRoot, sourcePath);
|
|
7707
|
+
const escapes = rel === "" || path46.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
|
|
7523
7708
|
if (escapes) {
|
|
7524
7709
|
warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
|
|
7525
7710
|
continue;
|
|
@@ -7529,8 +7714,8 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
7529
7714
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
7530
7715
|
continue;
|
|
7531
7716
|
}
|
|
7532
|
-
const realRel =
|
|
7533
|
-
if (realRel !== "" && (
|
|
7717
|
+
const realRel = path46.relative(realRoot, realSource);
|
|
7718
|
+
if (realRel !== "" && (path46.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
|
|
7534
7719
|
warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
|
|
7535
7720
|
continue;
|
|
7536
7721
|
}
|
|
@@ -7544,14 +7729,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
7544
7729
|
continue;
|
|
7545
7730
|
}
|
|
7546
7731
|
if (stat.isFile()) {
|
|
7547
|
-
const filename =
|
|
7548
|
-
const dest =
|
|
7732
|
+
const filename = path46.basename(sourcePath);
|
|
7733
|
+
const dest = path46.join(projectRoot, filename);
|
|
7549
7734
|
record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
|
|
7550
7735
|
} else if (stat.isDirectory()) {
|
|
7551
7736
|
const entries = await fs33.readdir(sourcePath);
|
|
7552
7737
|
for (const entry of entries) {
|
|
7553
|
-
const src =
|
|
7554
|
-
const dest =
|
|
7738
|
+
const src = path46.join(sourcePath, entry);
|
|
7739
|
+
const dest = path46.join(projectRoot, entry);
|
|
7555
7740
|
record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
|
|
7556
7741
|
}
|
|
7557
7742
|
}
|
|
@@ -7559,14 +7744,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
7559
7744
|
return { scaffolded, skipped, skippedExisting };
|
|
7560
7745
|
}
|
|
7561
7746
|
async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
7562
|
-
const destDisplay =
|
|
7747
|
+
const destDisplay = path46.relative(projectRoot, dest) || path46.basename(dest);
|
|
7563
7748
|
const srcStat = await fs33.lstat(src).catch(() => null);
|
|
7564
7749
|
if (!srcStat) {
|
|
7565
|
-
warn(`Skipping ${
|
|
7750
|
+
warn(`Skipping ${path46.basename(src)}: source not found`);
|
|
7566
7751
|
return "skipped-refused";
|
|
7567
7752
|
}
|
|
7568
7753
|
if (srcStat.isSymbolicLink()) {
|
|
7569
|
-
warn(`Skipping ${
|
|
7754
|
+
warn(`Skipping ${path46.basename(src)}: source is a symlink`);
|
|
7570
7755
|
return "skipped-refused";
|
|
7571
7756
|
}
|
|
7572
7757
|
const exists = await fs33.pathExists(dest);
|
|
@@ -7575,13 +7760,13 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
|
7575
7760
|
return "skipped-exists";
|
|
7576
7761
|
}
|
|
7577
7762
|
if (opts.dryRun) {
|
|
7578
|
-
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${
|
|
7763
|
+
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path46.basename(dest)} (${itemId})`);
|
|
7579
7764
|
return "scaffolded";
|
|
7580
7765
|
}
|
|
7581
7766
|
if (exists && opts.force) {
|
|
7582
7767
|
await fs33.remove(dest);
|
|
7583
7768
|
}
|
|
7584
|
-
await fs33.ensureDir(
|
|
7769
|
+
await fs33.ensureDir(path46.dirname(dest));
|
|
7585
7770
|
await fs33.copy(src, dest, {
|
|
7586
7771
|
overwrite: true,
|
|
7587
7772
|
dereference: false,
|
|
@@ -7594,7 +7779,7 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
|
7594
7779
|
}
|
|
7595
7780
|
}
|
|
7596
7781
|
});
|
|
7597
|
-
log(`${exists ? "Overwrote" : "Created"} ${
|
|
7782
|
+
log(`${exists ? "Overwrote" : "Created"} ${path46.basename(dest)} (${itemId})`);
|
|
7598
7783
|
return "scaffolded";
|
|
7599
7784
|
}
|
|
7600
7785
|
|
|
@@ -7659,7 +7844,7 @@ async function runScan(options) {
|
|
|
7659
7844
|
}
|
|
7660
7845
|
|
|
7661
7846
|
// src/commands/undo.ts
|
|
7662
|
-
import
|
|
7847
|
+
import path47 from "node:path";
|
|
7663
7848
|
import fs34 from "fs-extra";
|
|
7664
7849
|
|
|
7665
7850
|
// src/claude/managed-paths.ts
|
|
@@ -7687,18 +7872,18 @@ async function collectLockTrackedPaths(root) {
|
|
|
7687
7872
|
const relPaths = row.paths ?? [];
|
|
7688
7873
|
const existingRel = [];
|
|
7689
7874
|
for (const rel of relPaths) {
|
|
7690
|
-
if (await fs34.pathExists(
|
|
7875
|
+
if (await fs34.pathExists(path47.resolve(root, rel))) existingRel.push(rel);
|
|
7691
7876
|
}
|
|
7692
7877
|
if (existingRel.length === 0) continue;
|
|
7693
7878
|
if (row.hash === void 0) {
|
|
7694
|
-
removable.push(...existingRel.map((rel) =>
|
|
7879
|
+
removable.push(...existingRel.map((rel) => path47.resolve(root, rel)));
|
|
7695
7880
|
continue;
|
|
7696
7881
|
}
|
|
7697
7882
|
const currentHash = await hashInstalledPaths(root, existingRel);
|
|
7698
7883
|
if (currentHash === row.hash) {
|
|
7699
|
-
removable.push(...existingRel.map((rel) =>
|
|
7884
|
+
removable.push(...existingRel.map((rel) => path47.resolve(root, rel)));
|
|
7700
7885
|
} else {
|
|
7701
|
-
preserved.push(...existingRel.map((rel) =>
|
|
7886
|
+
preserved.push(...existingRel.map((rel) => path47.resolve(root, rel)));
|
|
7702
7887
|
log(`Preserving locally-modified ${row.id ?? "(unknown item)"}: ${existingRel.join(", ")}`);
|
|
7703
7888
|
}
|
|
7704
7889
|
}
|
|
@@ -7711,7 +7896,7 @@ async function settingsHasHausContent(root) {
|
|
|
7711
7896
|
return settings._haus != null;
|
|
7712
7897
|
}
|
|
7713
7898
|
async function claudeMdHasHausBlock(root) {
|
|
7714
|
-
const filePath =
|
|
7899
|
+
const filePath = path47.join(root, "CLAUDE.md");
|
|
7715
7900
|
if (!await fs34.pathExists(filePath)) return false;
|
|
7716
7901
|
const text = await fs34.readFile(filePath, "utf8");
|
|
7717
7902
|
return text.includes(BLOCK_BEGIN);
|
|
@@ -7724,15 +7909,15 @@ async function stripProjectSettings(root) {
|
|
|
7724
7909
|
const hasContent = Object.keys(settings).length > 0;
|
|
7725
7910
|
if (hasContent) {
|
|
7726
7911
|
await writeProjectSettings(root, settings);
|
|
7727
|
-
log(`Stripped haus rules from ${
|
|
7912
|
+
log(`Stripped haus rules from ${path47.relative(root, settingsPath)} (user settings preserved).`);
|
|
7728
7913
|
return true;
|
|
7729
7914
|
}
|
|
7730
7915
|
await fs34.remove(settingsPath);
|
|
7731
|
-
log(`Removed ${
|
|
7916
|
+
log(`Removed ${path47.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
7732
7917
|
return true;
|
|
7733
7918
|
}
|
|
7734
7919
|
async function stripRootClaudeMd(root) {
|
|
7735
|
-
const filePath =
|
|
7920
|
+
const filePath = path47.join(root, "CLAUDE.md");
|
|
7736
7921
|
if (!await fs34.pathExists(filePath)) return false;
|
|
7737
7922
|
const prev = await fs34.readFile(filePath, "utf8");
|
|
7738
7923
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
@@ -7760,12 +7945,12 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
|
7760
7945
|
const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
|
|
7761
7946
|
for (const abs of managedAbsPaths) {
|
|
7762
7947
|
if (!await fs34.pathExists(abs)) continue;
|
|
7763
|
-
const rel =
|
|
7764
|
-
const backupPath =
|
|
7765
|
-
await fs34.ensureDir(
|
|
7948
|
+
const rel = path47.relative(root, abs);
|
|
7949
|
+
const backupPath = path47.join(backupRoot, rel);
|
|
7950
|
+
await fs34.ensureDir(path47.dirname(backupPath));
|
|
7766
7951
|
await fs34.copy(abs, backupPath);
|
|
7767
7952
|
}
|
|
7768
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
7953
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path47.relative(root, backupRoot)}.`);
|
|
7769
7954
|
}
|
|
7770
7955
|
async function runUndo(options) {
|
|
7771
7956
|
const root = options.root ?? process.cwd();
|
|
@@ -7778,7 +7963,7 @@ async function runUndo(options) {
|
|
|
7778
7963
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
7779
7964
|
return;
|
|
7780
7965
|
}
|
|
7781
|
-
const relTargets = managed.map((p) =>
|
|
7966
|
+
const relTargets = managed.map((p) => path47.relative(root, p));
|
|
7782
7967
|
const summaryParts = [...relTargets];
|
|
7783
7968
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
7784
7969
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -7802,7 +7987,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
7802
7987
|
for (const abs of managed) {
|
|
7803
7988
|
if (!await fs34.pathExists(abs)) continue;
|
|
7804
7989
|
await fs34.remove(abs);
|
|
7805
|
-
log(`Removed ${
|
|
7990
|
+
log(`Removed ${path47.relative(root, abs)}`);
|
|
7806
7991
|
}
|
|
7807
7992
|
if (stripSettings) await stripProjectSettings(root);
|
|
7808
7993
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -7813,7 +7998,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
7813
7998
|
|
|
7814
7999
|
// src/install/uninstall.ts
|
|
7815
8000
|
import crypto3 from "node:crypto";
|
|
7816
|
-
import
|
|
8001
|
+
import path48 from "node:path";
|
|
7817
8002
|
import fs35 from "fs-extra";
|
|
7818
8003
|
async function runUninstall(options = {}) {
|
|
7819
8004
|
const { force = false } = options;
|
|
@@ -7843,14 +8028,14 @@ async function runUninstall(options = {}) {
|
|
|
7843
8028
|
continue;
|
|
7844
8029
|
}
|
|
7845
8030
|
await fs35.remove(entry.destPath);
|
|
7846
|
-
await pruneEmptyDir(
|
|
8031
|
+
await pruneEmptyDir(path48.dirname(entry.destPath));
|
|
7847
8032
|
result.deleted.push(entry.destPath);
|
|
7848
8033
|
}
|
|
7849
8034
|
const settings = await readSettings();
|
|
7850
8035
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
7851
8036
|
await writeSettings(stripped);
|
|
7852
8037
|
result.hooksStripped = true;
|
|
7853
|
-
const hausDir =
|
|
8038
|
+
const hausDir = path48.join(globalClaudeDir(), "haus");
|
|
7854
8039
|
const manifestPath2 = hausManifestPath();
|
|
7855
8040
|
if (fs35.pathExistsSync(manifestPath2)) {
|
|
7856
8041
|
await fs35.remove(manifestPath2);
|
|
@@ -7891,12 +8076,13 @@ async function runUninstallCommand(options) {
|
|
|
7891
8076
|
}
|
|
7892
8077
|
|
|
7893
8078
|
// src/commands/validate-catalog.ts
|
|
7894
|
-
import
|
|
8079
|
+
import path50 from "node:path";
|
|
7895
8080
|
|
|
7896
8081
|
// src/catalog/validate-core.ts
|
|
7897
8082
|
import fs36 from "node:fs";
|
|
7898
|
-
import
|
|
8083
|
+
import path49 from "node:path";
|
|
7899
8084
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
8085
|
+
var ITEM_ID_PATTERN = /^haus\./;
|
|
7900
8086
|
function auditForbiddenStacks(items) {
|
|
7901
8087
|
const failures = [];
|
|
7902
8088
|
for (const item of items) {
|
|
@@ -7982,6 +8168,9 @@ function auditManifestStructure(manifestVersion, items) {
|
|
|
7982
8168
|
failures.push(`item[${i}]: missing id`);
|
|
7983
8169
|
continue;
|
|
7984
8170
|
}
|
|
8171
|
+
if (!ITEM_ID_PATTERN.test(item.id)) {
|
|
8172
|
+
failures.push(`${item.id}: id must start with "haus."`);
|
|
8173
|
+
}
|
|
7985
8174
|
const provenanceError = validateCuratedProvenance(item);
|
|
7986
8175
|
if (provenanceError) failures.push(provenanceError);
|
|
7987
8176
|
const referencesError = validateReferences(item.id, item.references);
|
|
@@ -8040,7 +8229,7 @@ function readForAudit(absPath, label, failures) {
|
|
|
8040
8229
|
}
|
|
8041
8230
|
}
|
|
8042
8231
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
8043
|
-
const rel =
|
|
8232
|
+
const rel = path49.relative(manifestDir, absPath);
|
|
8044
8233
|
const failures = [];
|
|
8045
8234
|
const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
|
|
8046
8235
|
if (text === null) return failures;
|
|
@@ -8062,25 +8251,25 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
8062
8251
|
const failures = [];
|
|
8063
8252
|
for (const item of items) {
|
|
8064
8253
|
if (!item.path || !isSafeCatalogPath(item.path)) continue;
|
|
8065
|
-
const absPath =
|
|
8254
|
+
const absPath = path49.join(manifestDir, item.path);
|
|
8066
8255
|
if (item.type === "skill") {
|
|
8067
|
-
const skillMd =
|
|
8256
|
+
const skillMd = path49.join(absPath, "SKILL.md");
|
|
8068
8257
|
if (!fs36.existsSync(skillMd)) {
|
|
8069
|
-
failures.push(`${item.id}: missing ${
|
|
8258
|
+
failures.push(`${item.id}: missing ${path49.relative(manifestDir, skillMd)}`);
|
|
8070
8259
|
continue;
|
|
8071
8260
|
}
|
|
8072
8261
|
const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
|
|
8073
8262
|
if (text === null) continue;
|
|
8074
8263
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
8075
8264
|
failures.push(
|
|
8076
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
8265
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path49.relative(manifestDir, skillMd)}`)
|
|
8077
8266
|
);
|
|
8078
8267
|
} else if (item.type === "agent") {
|
|
8079
8268
|
if (!fs36.existsSync(absPath)) {
|
|
8080
8269
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
8081
8270
|
continue;
|
|
8082
8271
|
}
|
|
8083
|
-
const rel =
|
|
8272
|
+
const rel = path49.relative(manifestDir, absPath);
|
|
8084
8273
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
8085
8274
|
if (text === null) continue;
|
|
8086
8275
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -8096,7 +8285,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
8096
8285
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
8097
8286
|
continue;
|
|
8098
8287
|
}
|
|
8099
|
-
const rel =
|
|
8288
|
+
const rel = path49.relative(manifestDir, absPath);
|
|
8100
8289
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
8101
8290
|
if (text === null) continue;
|
|
8102
8291
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -8128,7 +8317,7 @@ function walkAllFiles(dir, fn, failures) {
|
|
|
8128
8317
|
}
|
|
8129
8318
|
for (const entry of entries) {
|
|
8130
8319
|
if (entry.name === ".DS_Store") continue;
|
|
8131
|
-
const full =
|
|
8320
|
+
const full = path49.join(dir, entry.name);
|
|
8132
8321
|
if (entry.isDirectory()) walkAllFiles(full, fn, failures);
|
|
8133
8322
|
else fn(full);
|
|
8134
8323
|
}
|
|
@@ -8146,12 +8335,12 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
8146
8335
|
const failures = [];
|
|
8147
8336
|
const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
|
|
8148
8337
|
for (const dir of CONTENT_ROOTS) {
|
|
8149
|
-
const abs =
|
|
8338
|
+
const abs = path49.join(manifestDir, dir);
|
|
8150
8339
|
if (!fs36.existsSync(abs)) continue;
|
|
8151
8340
|
walkAllFiles(
|
|
8152
8341
|
abs,
|
|
8153
8342
|
(file) => {
|
|
8154
|
-
const relPath =
|
|
8343
|
+
const relPath = path49.relative(manifestDir, file).replace(/\\/g, "/");
|
|
8155
8344
|
if (!isPathClaimed(relPath, claimedPaths)) {
|
|
8156
8345
|
failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
|
|
8157
8346
|
}
|
|
@@ -8172,7 +8361,7 @@ function walkMd(dir, fn, failures) {
|
|
|
8172
8361
|
return;
|
|
8173
8362
|
}
|
|
8174
8363
|
for (const entry of entries) {
|
|
8175
|
-
const full =
|
|
8364
|
+
const full = path49.join(dir, entry.name);
|
|
8176
8365
|
if (entry.isDirectory()) walkMd(full, fn, failures);
|
|
8177
8366
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
8178
8367
|
}
|
|
@@ -8181,13 +8370,13 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
8181
8370
|
const failures = [];
|
|
8182
8371
|
const pathSourceMap = buildItemPathSourceMap(items);
|
|
8183
8372
|
for (const dir of ["skills", "agents", "templates", "commands"]) {
|
|
8184
|
-
const abs =
|
|
8373
|
+
const abs = path49.join(manifestDir, dir);
|
|
8185
8374
|
if (!fs36.existsSync(abs)) continue;
|
|
8186
8375
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
8187
8376
|
walkMd(
|
|
8188
8377
|
abs,
|
|
8189
8378
|
(file) => {
|
|
8190
|
-
const rel =
|
|
8379
|
+
const rel = path49.relative(manifestDir, file);
|
|
8191
8380
|
const text = readForAudit(file, rel, failures);
|
|
8192
8381
|
if (text === null) return;
|
|
8193
8382
|
const norm = rel.replace(/\\/g, "/");
|
|
@@ -8232,8 +8421,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
8232
8421
|
process.exitCode = 1;
|
|
8233
8422
|
return;
|
|
8234
8423
|
}
|
|
8235
|
-
const abs =
|
|
8236
|
-
const manifestDir =
|
|
8424
|
+
const abs = path50.resolve(process.cwd(), manifestPath2);
|
|
8425
|
+
const manifestDir = path50.dirname(abs);
|
|
8237
8426
|
const data = await readJson(abs);
|
|
8238
8427
|
if (!data?.items) {
|
|
8239
8428
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -8250,7 +8439,7 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
8250
8439
|
}
|
|
8251
8440
|
|
|
8252
8441
|
// src/workspace/worktree/add.ts
|
|
8253
|
-
import
|
|
8442
|
+
import path57 from "node:path";
|
|
8254
8443
|
import fs40 from "fs-extra";
|
|
8255
8444
|
|
|
8256
8445
|
// src/workspace/worktree/git-worktree.ts
|
|
@@ -8370,12 +8559,12 @@ async function hasUnpushedWork(cwd) {
|
|
|
8370
8559
|
}
|
|
8371
8560
|
|
|
8372
8561
|
// src/workspace/worktree/hydrate.ts
|
|
8373
|
-
import
|
|
8562
|
+
import path53 from "node:path";
|
|
8374
8563
|
import fs38 from "fs-extra";
|
|
8375
8564
|
|
|
8376
8565
|
// src/workspace/worktree/cow-copy.ts
|
|
8377
8566
|
init_exec();
|
|
8378
|
-
import
|
|
8567
|
+
import path51 from "node:path";
|
|
8379
8568
|
import fs37 from "fs-extra";
|
|
8380
8569
|
async function getMacFilesystemType(dir) {
|
|
8381
8570
|
const df = await runCommand("df", ["-P", dir]);
|
|
@@ -8410,11 +8599,11 @@ async function cowCopyDir(src, dest) {
|
|
|
8410
8599
|
const srcExists = await fs37.pathExists(src);
|
|
8411
8600
|
if (!srcExists)
|
|
8412
8601
|
return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
|
|
8413
|
-
const strategy = await detectCowStrategy(
|
|
8602
|
+
const strategy = await detectCowStrategy(path51.dirname(dest));
|
|
8414
8603
|
if (strategy === "unsupported" || strategy === "unknown-platform") {
|
|
8415
8604
|
return { strategy, attempted: false, ok: false };
|
|
8416
8605
|
}
|
|
8417
|
-
await fs37.ensureDir(
|
|
8606
|
+
await fs37.ensureDir(path51.dirname(dest));
|
|
8418
8607
|
const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
|
|
8419
8608
|
const result = await runCommand("cp", args);
|
|
8420
8609
|
return {
|
|
@@ -8428,7 +8617,7 @@ async function cowCopyDir(src, dest) {
|
|
|
8428
8617
|
// src/workspace/worktree/install.ts
|
|
8429
8618
|
init_exec();
|
|
8430
8619
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
8431
|
-
import
|
|
8620
|
+
import path52 from "node:path";
|
|
8432
8621
|
import fg5 from "fast-glob";
|
|
8433
8622
|
function detectInstallPlan(signals) {
|
|
8434
8623
|
if (signals.hasYarnLock) {
|
|
@@ -8449,10 +8638,10 @@ function detectInstallPlan(signals) {
|
|
|
8449
8638
|
return { manager: "none" };
|
|
8450
8639
|
}
|
|
8451
8640
|
async function computeLockfileSignals(dir) {
|
|
8452
|
-
const hasYarnLock = existsSync3(
|
|
8453
|
-
const hasPnpmLock = existsSync3(
|
|
8454
|
-
const hasPackageLock = existsSync3(
|
|
8455
|
-
const hasComposerLock = existsSync3(
|
|
8641
|
+
const hasYarnLock = existsSync3(path52.join(dir, "yarn.lock"));
|
|
8642
|
+
const hasPnpmLock = existsSync3(path52.join(dir, "pnpm-lock.yaml"));
|
|
8643
|
+
const hasPackageLock = existsSync3(path52.join(dir, "package-lock.json"));
|
|
8644
|
+
const hasComposerLock = existsSync3(path52.join(dir, "composer.lock"));
|
|
8456
8645
|
const dotnetFiles = await fg5(["**/*.csproj", "**/*.sln"], {
|
|
8457
8646
|
cwd: dir,
|
|
8458
8647
|
onlyFiles: true,
|
|
@@ -8466,7 +8655,7 @@ async function computeLockfileSignals(dir) {
|
|
|
8466
8655
|
});
|
|
8467
8656
|
const hasDotnetProject = dotnetFiles.length > 0;
|
|
8468
8657
|
let hasPackageManagerField = false;
|
|
8469
|
-
const pkgPath =
|
|
8658
|
+
const pkgPath = path52.join(dir, "package.json");
|
|
8470
8659
|
if (existsSync3(pkgPath)) {
|
|
8471
8660
|
try {
|
|
8472
8661
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
@@ -8507,8 +8696,8 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
8507
8696
|
const start = Date.now();
|
|
8508
8697
|
const targets = [];
|
|
8509
8698
|
for (const target of HYDRATION_TARGETS) {
|
|
8510
|
-
const src =
|
|
8511
|
-
const dest =
|
|
8699
|
+
const src = path53.join(mainMemberPath, target);
|
|
8700
|
+
const dest = path53.join(worktreeMemberPath, target);
|
|
8512
8701
|
if (!await fs38.pathExists(src)) continue;
|
|
8513
8702
|
const destExists = await fs38.pathExists(dest);
|
|
8514
8703
|
if (!opts.force && destExists) continue;
|
|
@@ -8526,11 +8715,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
8526
8715
|
|
|
8527
8716
|
// src/workspace/worktree/local-files.ts
|
|
8528
8717
|
init_exec();
|
|
8529
|
-
import
|
|
8718
|
+
import path54 from "node:path";
|
|
8530
8719
|
import fs39 from "fs-extra";
|
|
8531
8720
|
var MACHINE_LOCAL_FILES = [
|
|
8532
8721
|
"docker-compose.local.yml",
|
|
8533
|
-
|
|
8722
|
+
path54.join(".claude", "settings.local.json")
|
|
8534
8723
|
];
|
|
8535
8724
|
async function isTracked(repoRoot, relFile) {
|
|
8536
8725
|
const gitPath = relFile.replace(/\\/g, "/");
|
|
@@ -8540,11 +8729,11 @@ async function isTracked(repoRoot, relFile) {
|
|
|
8540
8729
|
async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
8541
8730
|
const copied = [];
|
|
8542
8731
|
for (const rel of MACHINE_LOCAL_FILES) {
|
|
8543
|
-
const src =
|
|
8732
|
+
const src = path54.join(mainMemberPath, rel);
|
|
8544
8733
|
if (!await fs39.pathExists(src)) continue;
|
|
8545
8734
|
if (await isTracked(mainMemberPath, rel)) continue;
|
|
8546
|
-
const dest =
|
|
8547
|
-
await fs39.ensureDir(
|
|
8735
|
+
const dest = path54.join(worktreeMemberPath, rel);
|
|
8736
|
+
await fs39.ensureDir(path54.dirname(dest));
|
|
8548
8737
|
await fs39.copy(src, dest, { overwrite: true });
|
|
8549
8738
|
copied.push(rel);
|
|
8550
8739
|
}
|
|
@@ -8553,15 +8742,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
|
8553
8742
|
|
|
8554
8743
|
// src/workspace/worktree/root.ts
|
|
8555
8744
|
import { existsSync as existsSync4 } from "node:fs";
|
|
8556
|
-
import
|
|
8745
|
+
import path55 from "node:path";
|
|
8557
8746
|
async function resolveWorkspaceForWorktree(start) {
|
|
8558
8747
|
const rootInfo = await resolveRoots(start);
|
|
8559
8748
|
if (!rootInfo.isGitRepo) {
|
|
8560
8749
|
return { ok: false, reason: "Not inside a git repository." };
|
|
8561
8750
|
}
|
|
8562
8751
|
const workspaceRoot = rootInfo.mainRoot;
|
|
8563
|
-
const hasYaml = existsSync4(
|
|
8564
|
-
const hasManifest = existsSync4(
|
|
8752
|
+
const hasYaml = existsSync4(path55.join(workspaceRoot, WORKSPACE_FILE));
|
|
8753
|
+
const hasManifest = existsSync4(path55.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
8565
8754
|
if (!hasYaml && !hasManifest) {
|
|
8566
8755
|
return {
|
|
8567
8756
|
ok: false,
|
|
@@ -8586,19 +8775,19 @@ function selectMembers(members, only) {
|
|
|
8586
8775
|
}
|
|
8587
8776
|
|
|
8588
8777
|
// src/workspace/worktree/state.ts
|
|
8589
|
-
import
|
|
8778
|
+
import path56 from "node:path";
|
|
8590
8779
|
var WORKTREE_STATE_FILE = ".haus-worktree.json";
|
|
8591
8780
|
function worktreesDir(mainRoot) {
|
|
8592
|
-
return
|
|
8781
|
+
return path56.join(mainRoot, ".claude", "worktrees");
|
|
8593
8782
|
}
|
|
8594
8783
|
function worktreePath(mainRoot, slug) {
|
|
8595
|
-
return
|
|
8784
|
+
return path56.join(worktreesDir(mainRoot), slug);
|
|
8596
8785
|
}
|
|
8597
8786
|
async function readWorktreeState(worktreeRoot) {
|
|
8598
|
-
return readJson(
|
|
8787
|
+
return readJson(path56.join(worktreeRoot, WORKTREE_STATE_FILE));
|
|
8599
8788
|
}
|
|
8600
8789
|
async function writeWorktreeState(worktreeRoot, state) {
|
|
8601
|
-
await writeJson(
|
|
8790
|
+
await writeJson(path56.join(worktreeRoot, WORKTREE_STATE_FILE), state);
|
|
8602
8791
|
}
|
|
8603
8792
|
|
|
8604
8793
|
// src/workspace/worktree/add.ts
|
|
@@ -8616,7 +8805,7 @@ async function runAdd(opts) {
|
|
|
8616
8805
|
if (await fs40.pathExists(wsWorktreePath)) {
|
|
8617
8806
|
return {
|
|
8618
8807
|
ok: false,
|
|
8619
|
-
error: `Workspace worktree already exists at ${wsWorktreePath} \u2014 run \`haus workspace worktree hydrate\` or \`remove\` first.`
|
|
8808
|
+
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
8809
|
};
|
|
8621
8810
|
}
|
|
8622
8811
|
if (opts.dryRun) {
|
|
@@ -8632,7 +8821,7 @@ async function runAdd(opts) {
|
|
|
8632
8821
|
const memberResults = [];
|
|
8633
8822
|
for (const member of selected) {
|
|
8634
8823
|
const start = Date.now();
|
|
8635
|
-
const memberWorktreePath =
|
|
8824
|
+
const memberWorktreePath = path57.join(wsWorktreePath, member.folder);
|
|
8636
8825
|
const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
|
|
8637
8826
|
preferDefaultBranchFrom: true
|
|
8638
8827
|
});
|
|
@@ -8662,7 +8851,7 @@ async function runAdd(opts) {
|
|
|
8662
8851
|
await mapWithConcurrency(okResults, async (result) => {
|
|
8663
8852
|
const member = selected.find((m) => m.id === result.member);
|
|
8664
8853
|
if (!member) return;
|
|
8665
|
-
const memberWorktreePath =
|
|
8854
|
+
const memberWorktreePath = path57.join(wsWorktreePath, member.folder);
|
|
8666
8855
|
const hydrationStart = Date.now();
|
|
8667
8856
|
if (opts.hydrate !== false) {
|
|
8668
8857
|
result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
|
|
@@ -8729,10 +8918,10 @@ async function runDryRun(args) {
|
|
|
8729
8918
|
|
|
8730
8919
|
// src/workspace/worktree/doctor.ts
|
|
8731
8920
|
import { existsSync as existsSync5 } from "node:fs";
|
|
8732
|
-
import
|
|
8921
|
+
import path58 from "node:path";
|
|
8733
8922
|
var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
|
|
8734
8923
|
function looksHydrated(memberPath) {
|
|
8735
|
-
return HYDRATION_MARKERS.some((marker) => existsSync5(
|
|
8924
|
+
return HYDRATION_MARKERS.some((marker) => existsSync5(path58.join(memberPath, marker)));
|
|
8736
8925
|
}
|
|
8737
8926
|
async function runDoctor2() {
|
|
8738
8927
|
const rootInfo = await resolveRoots();
|
|
@@ -8746,7 +8935,7 @@ async function runDoctor2() {
|
|
|
8746
8935
|
};
|
|
8747
8936
|
}
|
|
8748
8937
|
const workspaceRoot = rootInfo.mainRoot;
|
|
8749
|
-
const isWorkspace = existsSync5(
|
|
8938
|
+
const isWorkspace = existsSync5(path58.join(workspaceRoot, WORKSPACE_FILE)) || existsSync5(path58.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
8750
8939
|
if (!isWorkspace) {
|
|
8751
8940
|
return {
|
|
8752
8941
|
isWorkspace: false,
|
|
@@ -8759,7 +8948,7 @@ async function runDoctor2() {
|
|
|
8759
8948
|
};
|
|
8760
8949
|
}
|
|
8761
8950
|
const wsWorktreesDir = worktreesDir(workspaceRoot);
|
|
8762
|
-
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${
|
|
8951
|
+
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path58.sep}`);
|
|
8763
8952
|
const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
|
|
8764
8953
|
const members = await readMembers(rootInfo);
|
|
8765
8954
|
const problems = [];
|
|
@@ -8767,10 +8956,10 @@ async function runDoctor2() {
|
|
|
8767
8956
|
let slug;
|
|
8768
8957
|
if (inWorkspaceWorktree) {
|
|
8769
8958
|
const wtPath = rootInfo.repoRoot;
|
|
8770
|
-
slug = rootInfo.worktreeName ??
|
|
8959
|
+
slug = rootInfo.worktreeName ?? path58.basename(wtPath);
|
|
8771
8960
|
const state = await readWorktreeState(wtPath);
|
|
8772
8961
|
for (const member of members) {
|
|
8773
|
-
const memberPath =
|
|
8962
|
+
const memberPath = path58.join(wtPath, member.folder);
|
|
8774
8963
|
const materialized = existsSync5(memberPath);
|
|
8775
8964
|
const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
|
|
8776
8965
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
@@ -8801,16 +8990,16 @@ async function runDoctor2() {
|
|
|
8801
8990
|
for (const member of members) {
|
|
8802
8991
|
const entries = await listWorktrees(member.absPath);
|
|
8803
8992
|
for (const entry of entries) {
|
|
8804
|
-
const entryPath =
|
|
8805
|
-
const marker = `${
|
|
8993
|
+
const entryPath = path58.normalize(entry.path);
|
|
8994
|
+
const marker = `${path58.sep}.claude${path58.sep}worktrees${path58.sep}`;
|
|
8806
8995
|
const idx = entryPath.indexOf(marker);
|
|
8807
8996
|
if (idx === -1) continue;
|
|
8808
8997
|
const rest = entryPath.slice(idx + marker.length);
|
|
8809
|
-
const slugName = rest.split(
|
|
8998
|
+
const slugName = rest.split(path58.sep)[0];
|
|
8810
8999
|
if (!slugName) continue;
|
|
8811
|
-
if (!existsSync5(
|
|
9000
|
+
if (!existsSync5(path58.join(wsWorktreesDir, slugName))) {
|
|
8812
9001
|
orphans.push(
|
|
8813
|
-
`${member.id}: worktree registered at ${entry.path} but ${
|
|
9002
|
+
`${member.id}: worktree registered at ${entry.path} but ${path58.join(wsWorktreesDir, slugName)} no longer exists \u2014 run \`git -C "${member.absPath}" worktree prune\`.`
|
|
8814
9003
|
);
|
|
8815
9004
|
}
|
|
8816
9005
|
}
|
|
@@ -8821,7 +9010,7 @@ async function runDoctor2() {
|
|
|
8821
9010
|
|
|
8822
9011
|
// src/workspace/worktree/hydrate-command.ts
|
|
8823
9012
|
import { existsSync as existsSync6 } from "node:fs";
|
|
8824
|
-
import
|
|
9013
|
+
import path59 from "node:path";
|
|
8825
9014
|
async function runHydrateWorktree(opts) {
|
|
8826
9015
|
const rootInfo = await resolveRoots();
|
|
8827
9016
|
if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
|
|
@@ -8832,7 +9021,7 @@ async function runHydrateWorktree(opts) {
|
|
|
8832
9021
|
}
|
|
8833
9022
|
const workspaceRoot = rootInfo.mainRoot;
|
|
8834
9023
|
const wtPath = rootInfo.repoRoot;
|
|
8835
|
-
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${
|
|
9024
|
+
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path59.sep}`)) {
|
|
8836
9025
|
return {
|
|
8837
9026
|
ok: false,
|
|
8838
9027
|
error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
|
|
@@ -8844,49 +9033,97 @@ async function runHydrateWorktree(opts) {
|
|
|
8844
9033
|
return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
|
|
8845
9034
|
}
|
|
8846
9035
|
const state = await readWorktreeState(wtPath);
|
|
8847
|
-
const slug = state?.slug ??
|
|
9036
|
+
const slug = state?.slug ?? path59.basename(wtPath);
|
|
9037
|
+
const detectedBranch = await currentBranch(wtPath);
|
|
9038
|
+
const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
|
|
8848
9039
|
if (opts.dryRun) {
|
|
8849
|
-
const results2 =
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
9040
|
+
const results2 = await Promise.all(
|
|
9041
|
+
selected.map(async (member) => {
|
|
9042
|
+
const memberPath = path59.join(wtPath, member.folder);
|
|
9043
|
+
if (existsSync6(memberPath)) {
|
|
9044
|
+
return { member: member.id, folder: member.folder, status: "planned" };
|
|
9045
|
+
}
|
|
9046
|
+
if (!opts.materialize) {
|
|
9047
|
+
return {
|
|
9048
|
+
member: member.id,
|
|
9049
|
+
folder: member.folder,
|
|
9050
|
+
status: "skipped",
|
|
9051
|
+
error: "not materialized in this worktree"
|
|
9052
|
+
};
|
|
9053
|
+
}
|
|
9054
|
+
const branchAction = await branchExists(member.absPath, targetBranch) ? "checkout" : "create";
|
|
9055
|
+
return { member: member.id, folder: member.folder, status: "planned", branchAction };
|
|
9056
|
+
})
|
|
9057
|
+
);
|
|
8858
9058
|
return { ok: true, dryRun: true, slug, results: results2 };
|
|
8859
9059
|
}
|
|
8860
9060
|
const results = await mapWithConcurrency(
|
|
8861
9061
|
selected,
|
|
8862
9062
|
async (member) => {
|
|
8863
|
-
const memberPath =
|
|
9063
|
+
const memberPath = path59.join(wtPath, member.folder);
|
|
9064
|
+
let branchAction;
|
|
8864
9065
|
if (!existsSync6(memberPath)) {
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
9066
|
+
if (!opts.materialize) {
|
|
9067
|
+
return {
|
|
9068
|
+
member: member.id,
|
|
9069
|
+
folder: member.folder,
|
|
9070
|
+
status: "skipped",
|
|
9071
|
+
error: "not materialized in this worktree"
|
|
9072
|
+
};
|
|
9073
|
+
}
|
|
9074
|
+
const outcome = await addWorktree(member.absPath, memberPath, targetBranch, {
|
|
9075
|
+
preferDefaultBranchFrom: true
|
|
9076
|
+
});
|
|
9077
|
+
if (!outcome.ok) {
|
|
9078
|
+
return {
|
|
9079
|
+
member: member.id,
|
|
9080
|
+
folder: member.folder,
|
|
9081
|
+
status: "failed",
|
|
9082
|
+
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"
|
|
9083
|
+
};
|
|
9084
|
+
}
|
|
9085
|
+
branchAction = outcome.branchAction;
|
|
8871
9086
|
}
|
|
8872
9087
|
try {
|
|
8873
9088
|
const hydration = await hydrateMember(member.absPath, memberPath, { force: opts.force });
|
|
8874
|
-
return { member: member.id, folder: member.folder, status: "ok", hydration };
|
|
9089
|
+
return { member: member.id, folder: member.folder, status: "ok", hydration, branchAction };
|
|
8875
9090
|
} catch (err) {
|
|
8876
9091
|
return {
|
|
8877
9092
|
member: member.id,
|
|
8878
9093
|
folder: member.folder,
|
|
8879
9094
|
status: "failed",
|
|
8880
|
-
error: err instanceof Error ? err.message : String(err)
|
|
9095
|
+
error: err instanceof Error ? err.message : String(err),
|
|
9096
|
+
branchAction
|
|
8881
9097
|
};
|
|
8882
9098
|
}
|
|
8883
9099
|
}
|
|
8884
9100
|
);
|
|
9101
|
+
const newlyMaterialized = results.filter((r) => r.branchAction && r.status === "ok");
|
|
9102
|
+
if (newlyMaterialized.length > 0) {
|
|
9103
|
+
const byId = new Map((state?.members ?? []).map((m) => [m.id, m]));
|
|
9104
|
+
for (const r of newlyMaterialized) {
|
|
9105
|
+
const member = selected.find((m) => m.id === r.member);
|
|
9106
|
+
if (!member) continue;
|
|
9107
|
+
const entry = {
|
|
9108
|
+
id: member.id,
|
|
9109
|
+
folder: member.folder,
|
|
9110
|
+
branch: targetBranch,
|
|
9111
|
+
absPath: member.absPath
|
|
9112
|
+
};
|
|
9113
|
+
byId.set(member.id, entry);
|
|
9114
|
+
}
|
|
9115
|
+
await writeWorktreeState(wtPath, {
|
|
9116
|
+
slug,
|
|
9117
|
+
branch: state?.branch ?? targetBranch,
|
|
9118
|
+
createdAt: state?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
9119
|
+
members: Array.from(byId.values())
|
|
9120
|
+
});
|
|
9121
|
+
}
|
|
8885
9122
|
return { ok: results.every((r) => r.status !== "failed"), dryRun: false, slug, results };
|
|
8886
9123
|
}
|
|
8887
9124
|
|
|
8888
9125
|
// src/workspace/worktree/list.ts
|
|
8889
|
-
import
|
|
9126
|
+
import path60 from "node:path";
|
|
8890
9127
|
import fs41 from "fs-extra";
|
|
8891
9128
|
async function runList2() {
|
|
8892
9129
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -8899,12 +9136,12 @@ async function runList2() {
|
|
|
8899
9136
|
const configuredMembers = await readMembers(rootInfo);
|
|
8900
9137
|
const worktrees = [];
|
|
8901
9138
|
for (const slug of slugs) {
|
|
8902
|
-
const wtPath =
|
|
9139
|
+
const wtPath = path60.join(dir, slug);
|
|
8903
9140
|
const state = await readWorktreeState(wtPath);
|
|
8904
9141
|
const members = [];
|
|
8905
9142
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
8906
9143
|
for (const m of source) {
|
|
8907
|
-
const memberPath =
|
|
9144
|
+
const memberPath = path60.join(wtPath, m.folder);
|
|
8908
9145
|
const materialized = await fs41.pathExists(memberPath);
|
|
8909
9146
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
8910
9147
|
members.push({
|
|
@@ -8926,7 +9163,7 @@ async function runList2() {
|
|
|
8926
9163
|
}
|
|
8927
9164
|
|
|
8928
9165
|
// src/workspace/worktree/remove.ts
|
|
8929
|
-
import
|
|
9166
|
+
import path61 from "node:path";
|
|
8930
9167
|
import fs42 from "fs-extra";
|
|
8931
9168
|
async function runRemove(opts) {
|
|
8932
9169
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -8953,7 +9190,7 @@ async function runRemove(opts) {
|
|
|
8953
9190
|
targets.push({
|
|
8954
9191
|
repo: configMember?.id ?? stateMember.id,
|
|
8955
9192
|
ownerRepo,
|
|
8956
|
-
path:
|
|
9193
|
+
path: path61.join(wsWorktreePath, stateMember.folder)
|
|
8957
9194
|
});
|
|
8958
9195
|
}
|
|
8959
9196
|
const existingTargets = [];
|
|
@@ -9081,7 +9318,8 @@ async function handleHydrate(opts) {
|
|
|
9081
9318
|
const hydrateOpts = {
|
|
9082
9319
|
only: normalizeOnly(opts.only),
|
|
9083
9320
|
force: Boolean(opts.force),
|
|
9084
|
-
dryRun: Boolean(opts.dryRun)
|
|
9321
|
+
dryRun: Boolean(opts.dryRun),
|
|
9322
|
+
materialize: Boolean(opts.materialize)
|
|
9085
9323
|
};
|
|
9086
9324
|
const result = await runHydrateWorktree(hydrateOpts);
|
|
9087
9325
|
if (!result.ok && "error" in result) {
|
|
@@ -9091,14 +9329,20 @@ async function handleHydrate(opts) {
|
|
|
9091
9329
|
}
|
|
9092
9330
|
if (result.dryRun) {
|
|
9093
9331
|
log(`[dry-run] Would hydrate "${result.slug}":`);
|
|
9094
|
-
for (const r of result.results)
|
|
9095
|
-
|
|
9332
|
+
for (const r of result.results) {
|
|
9333
|
+
const action = r.branchAction ? ` (${r.branchAction} member worktree first)` : "";
|
|
9334
|
+
log(`[dry-run] ${r.member}: ${r.status}${action}${r.error ? ` (${r.error})` : ""}`);
|
|
9335
|
+
}
|
|
9096
9336
|
return;
|
|
9097
9337
|
}
|
|
9098
9338
|
log(`Hydrated "${result.slug}":`);
|
|
9099
9339
|
for (const r of result.results) {
|
|
9100
|
-
if (r.status === "ok" && r.hydration)
|
|
9101
|
-
|
|
9340
|
+
if (r.status === "ok" && r.hydration) {
|
|
9341
|
+
const materializedNote = r.branchAction ? `materialized (${r.branchAction}), ` : "";
|
|
9342
|
+
log(` ${r.member}: ${materializedNote}${describeInstall(r.hydration)}`);
|
|
9343
|
+
} else {
|
|
9344
|
+
warn(` ${r.member}: ${r.status}${r.error ? ` \u2014 ${r.error}` : ""}`);
|
|
9345
|
+
}
|
|
9102
9346
|
}
|
|
9103
9347
|
if (!result.ok) process.exitCode = 1;
|
|
9104
9348
|
}
|
|
@@ -9157,6 +9401,13 @@ async function handleList() {
|
|
|
9157
9401
|
}
|
|
9158
9402
|
}
|
|
9159
9403
|
}
|
|
9404
|
+
function emitFromHookContext(additionalContext) {
|
|
9405
|
+
log(
|
|
9406
|
+
JSON.stringify({
|
|
9407
|
+
hookSpecificOutput: { hookEventName: "SessionStart", additionalContext }
|
|
9408
|
+
})
|
|
9409
|
+
);
|
|
9410
|
+
}
|
|
9160
9411
|
async function handleDoctor(opts) {
|
|
9161
9412
|
try {
|
|
9162
9413
|
const report = await runDoctor2();
|
|
@@ -9168,17 +9419,26 @@ async function handleDoctor(opts) {
|
|
|
9168
9419
|
return;
|
|
9169
9420
|
}
|
|
9170
9421
|
if (report.problems.length === 0) {
|
|
9171
|
-
|
|
9172
|
-
|
|
9422
|
+
if (!opts.fromHook) {
|
|
9423
|
+
log(
|
|
9424
|
+
report.inWorkspaceWorktree ? `Workspace worktree "${report.slug}" looks healthy.` : "No issues found."
|
|
9425
|
+
);
|
|
9426
|
+
}
|
|
9427
|
+
return;
|
|
9428
|
+
}
|
|
9429
|
+
if (opts.fromHook) {
|
|
9430
|
+
const scope = report.slug ? `Workspace worktree "${report.slug}"` : "This haus workspace";
|
|
9431
|
+
emitFromHookContext(
|
|
9432
|
+
`${scope} has ${report.problems.length} issue(s): ${report.problems.join("; ")}`
|
|
9173
9433
|
);
|
|
9174
9434
|
return;
|
|
9175
9435
|
}
|
|
9176
9436
|
for (const p of report.problems) warn(p);
|
|
9177
|
-
|
|
9437
|
+
process.exitCode = 1;
|
|
9178
9438
|
} catch (err) {
|
|
9179
9439
|
const message = err instanceof Error ? err.message : String(err);
|
|
9180
9440
|
if (opts.fromHook) {
|
|
9181
|
-
|
|
9441
|
+
emitFromHookContext(`\`haus workspace worktree doctor\` failed: ${message}`);
|
|
9182
9442
|
} else {
|
|
9183
9443
|
error(message);
|
|
9184
9444
|
process.exitCode = 1;
|
|
@@ -9202,7 +9462,7 @@ async function runWorktree(action, opts = {}) {
|
|
|
9202
9462
|
|
|
9203
9463
|
// src/commands/workspace.ts
|
|
9204
9464
|
import { existsSync as existsSync10, statSync as statSync3 } from "node:fs";
|
|
9205
|
-
import
|
|
9465
|
+
import path70 from "node:path";
|
|
9206
9466
|
|
|
9207
9467
|
// src/commands/workspace/aggregate.ts
|
|
9208
9468
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -9257,18 +9517,18 @@ ${summaries.map(
|
|
|
9257
9517
|
|
|
9258
9518
|
// src/commands/workspace/doctor.ts
|
|
9259
9519
|
import { existsSync as existsSync7 } from "node:fs";
|
|
9260
|
-
import
|
|
9520
|
+
import path63 from "node:path";
|
|
9261
9521
|
|
|
9262
9522
|
// src/commands/workspace/manifest.ts
|
|
9263
9523
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
9264
|
-
import
|
|
9524
|
+
import path62 from "node:path";
|
|
9265
9525
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
9266
9526
|
function manifestPath(workspaceRoot) {
|
|
9267
9527
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
9268
9528
|
}
|
|
9269
9529
|
function hausVersion() {
|
|
9270
9530
|
try {
|
|
9271
|
-
const pkg = JSON.parse(readFileSync4(
|
|
9531
|
+
const pkg = JSON.parse(readFileSync4(path62.join(packageRoot(), "package.json"), "utf8"));
|
|
9272
9532
|
return pkg.version ?? "0.0.0";
|
|
9273
9533
|
} catch {
|
|
9274
9534
|
return "0.0.0";
|
|
@@ -9352,7 +9612,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
9352
9612
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
9353
9613
|
const catalogRefByRepo = [];
|
|
9354
9614
|
for (const repo of config.repos) {
|
|
9355
|
-
const repoRoot =
|
|
9615
|
+
const repoRoot = path63.resolve(workspaceRoot, repo.path);
|
|
9356
9616
|
const entry = manifestByName.get(repo.name);
|
|
9357
9617
|
if (!entry) {
|
|
9358
9618
|
flag({
|
|
@@ -9425,7 +9685,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
9425
9685
|
});
|
|
9426
9686
|
continue;
|
|
9427
9687
|
}
|
|
9428
|
-
if (!existsSync7(
|
|
9688
|
+
if (!existsSync7(path63.join(workspaceRoot, entry.path))) {
|
|
9429
9689
|
flag({
|
|
9430
9690
|
repo: entry.repo,
|
|
9431
9691
|
kind: "missing-linked-context-copy",
|
|
@@ -9468,15 +9728,15 @@ function emit(args) {
|
|
|
9468
9728
|
}
|
|
9469
9729
|
|
|
9470
9730
|
// src/workspace/link-context/link.ts
|
|
9471
|
-
import
|
|
9731
|
+
import path66 from "node:path";
|
|
9472
9732
|
import fs45 from "fs-extra";
|
|
9473
9733
|
|
|
9474
9734
|
// src/workspace/link-context/plan.ts
|
|
9475
|
-
import
|
|
9735
|
+
import path65 from "node:path";
|
|
9476
9736
|
import fs44 from "fs-extra";
|
|
9477
9737
|
|
|
9478
9738
|
// src/workspace/link-context/scan-source.ts
|
|
9479
|
-
import
|
|
9739
|
+
import path64 from "node:path";
|
|
9480
9740
|
import fs43 from "fs-extra";
|
|
9481
9741
|
var ASSET_SUBDIR = {
|
|
9482
9742
|
skill: "skills",
|
|
@@ -9485,36 +9745,36 @@ var ASSET_SUBDIR = {
|
|
|
9485
9745
|
};
|
|
9486
9746
|
async function hasLinkableContext(member) {
|
|
9487
9747
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
9488
|
-
if (await fs43.pathExists(
|
|
9748
|
+
if (await fs43.pathExists(path64.join(member.absPath, ".claude", subdir))) return true;
|
|
9489
9749
|
}
|
|
9490
9750
|
return false;
|
|
9491
9751
|
}
|
|
9492
9752
|
async function collectSkills(member) {
|
|
9493
|
-
const dir =
|
|
9753
|
+
const dir = path64.join(member.absPath, ".claude", "skills");
|
|
9494
9754
|
if (!await fs43.pathExists(dir)) return [];
|
|
9495
9755
|
const names = await fs43.readdir(dir);
|
|
9496
9756
|
const out = [];
|
|
9497
9757
|
for (const name of [...names].sort()) {
|
|
9498
|
-
const abs =
|
|
9758
|
+
const abs = path64.join(dir, name);
|
|
9499
9759
|
const stat = await fs43.lstat(abs);
|
|
9500
9760
|
if (!stat.isDirectory()) continue;
|
|
9501
|
-
if (!await fs43.pathExists(
|
|
9502
|
-
out.push({ type: "skill", name, sourceRelPath:
|
|
9761
|
+
if (!await fs43.pathExists(path64.join(abs, "SKILL.md"))) continue;
|
|
9762
|
+
out.push({ type: "skill", name, sourceRelPath: path64.posix.join(".claude", "skills", name) });
|
|
9503
9763
|
}
|
|
9504
9764
|
return out;
|
|
9505
9765
|
}
|
|
9506
9766
|
async function collectFlatMarkdown(member, type) {
|
|
9507
9767
|
const subdir = ASSET_SUBDIR[type];
|
|
9508
|
-
const dir =
|
|
9768
|
+
const dir = path64.join(member.absPath, ".claude", subdir);
|
|
9509
9769
|
if (!await fs43.pathExists(dir)) return [];
|
|
9510
9770
|
const names = await fs43.readdir(dir);
|
|
9511
9771
|
const out = [];
|
|
9512
9772
|
for (const fileName of [...names].sort()) {
|
|
9513
9773
|
if (!fileName.endsWith(".md")) continue;
|
|
9514
|
-
const abs =
|
|
9774
|
+
const abs = path64.join(dir, fileName);
|
|
9515
9775
|
if (!(await fs43.lstat(abs)).isFile()) continue;
|
|
9516
9776
|
const name = fileName.slice(0, -".md".length);
|
|
9517
|
-
out.push({ type, name, sourceRelPath:
|
|
9777
|
+
out.push({ type, name, sourceRelPath: path64.posix.join(".claude", subdir, fileName) });
|
|
9518
9778
|
}
|
|
9519
9779
|
return out;
|
|
9520
9780
|
}
|
|
@@ -9529,9 +9789,9 @@ async function collectSourceAssets(member) {
|
|
|
9529
9789
|
|
|
9530
9790
|
// src/workspace/link-context/plan.ts
|
|
9531
9791
|
function repoFolderPrefix(member) {
|
|
9532
|
-
const base =
|
|
9792
|
+
const base = path65.posix.basename(member.folder);
|
|
9533
9793
|
if (base && base !== "." && base !== "/") return base;
|
|
9534
|
-
const fromAbs =
|
|
9794
|
+
const fromAbs = path65.basename(member.absPath);
|
|
9535
9795
|
return fromAbs || member.id;
|
|
9536
9796
|
}
|
|
9537
9797
|
function assetKey(type, destKey) {
|
|
@@ -9588,7 +9848,7 @@ async function buildLinkPlan(members) {
|
|
|
9588
9848
|
name: asset.name,
|
|
9589
9849
|
destKey,
|
|
9590
9850
|
sourceRelPath: asset.sourceRelPath,
|
|
9591
|
-
sourceAbsPath:
|
|
9851
|
+
sourceAbsPath: path65.join(member.absPath, asset.sourceRelPath),
|
|
9592
9852
|
sourceHash
|
|
9593
9853
|
});
|
|
9594
9854
|
}
|
|
@@ -9599,7 +9859,7 @@ async function buildLinkPlan(members) {
|
|
|
9599
9859
|
function destRelPathFor(type, destKey) {
|
|
9600
9860
|
const subdir = `${type}s`;
|
|
9601
9861
|
const leaf = type === "skill" ? destKey : `${destKey}.md`;
|
|
9602
|
-
return
|
|
9862
|
+
return path66.posix.join(".claude", subdir, leaf);
|
|
9603
9863
|
}
|
|
9604
9864
|
async function runLinkContext(workspaceRoot, opts = {}) {
|
|
9605
9865
|
const write = opts.write ?? false;
|
|
@@ -9625,9 +9885,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
9625
9885
|
const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9626
9886
|
for (const entry of plan.entries) {
|
|
9627
9887
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
9628
|
-
const destAbs =
|
|
9888
|
+
const destAbs = path66.join(workspaceRoot, destRel);
|
|
9629
9889
|
if (write) {
|
|
9630
|
-
await fs45.ensureDir(
|
|
9890
|
+
await fs45.ensureDir(path66.dirname(destAbs));
|
|
9631
9891
|
if (await fs45.pathExists(destAbs)) await fs45.remove(destAbs);
|
|
9632
9892
|
await fs45.copy(entry.sourceAbsPath, destAbs, {
|
|
9633
9893
|
overwrite: true,
|
|
@@ -9659,7 +9919,7 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
9659
9919
|
if (nextPaths.has(old.path)) continue;
|
|
9660
9920
|
removed.push(old.path);
|
|
9661
9921
|
if (write) {
|
|
9662
|
-
const abs =
|
|
9922
|
+
const abs = path66.join(workspaceRoot, old.path);
|
|
9663
9923
|
if (await fs45.pathExists(abs)) await fs45.remove(abs);
|
|
9664
9924
|
}
|
|
9665
9925
|
}
|
|
@@ -9712,10 +9972,10 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
|
|
|
9712
9972
|
|
|
9713
9973
|
// src/commands/workspace/setup.ts
|
|
9714
9974
|
import { existsSync as existsSync8, statSync } from "node:fs";
|
|
9715
|
-
import
|
|
9975
|
+
import path68 from "node:path";
|
|
9716
9976
|
|
|
9717
9977
|
// src/claude/write-workspace-claude-md.ts
|
|
9718
|
-
import
|
|
9978
|
+
import path67 from "node:path";
|
|
9719
9979
|
import fs46 from "fs-extra";
|
|
9720
9980
|
function buildWorkspaceImportBlock(client, members) {
|
|
9721
9981
|
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
@@ -9734,7 +9994,7 @@ ${BLOCK_END}`;
|
|
|
9734
9994
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
9735
9995
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
9736
9996
|
const dryRun = opts.dryRun ?? false;
|
|
9737
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
9997
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path67.join(workspaceRoot, "CLAUDE.md");
|
|
9738
9998
|
const say = opts.quiet ? () => {
|
|
9739
9999
|
} : log;
|
|
9740
10000
|
const existed = await fs46.pathExists(filePath);
|
|
@@ -9765,20 +10025,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
9765
10025
|
|
|
9766
10026
|
// src/commands/workspace/setup.ts
|
|
9767
10027
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
9768
|
-
let dir =
|
|
10028
|
+
let dir = path68.resolve(start);
|
|
9769
10029
|
for (; ; ) {
|
|
9770
|
-
if (existsSync8(
|
|
9771
|
-
const parent =
|
|
9772
|
-
if (parent === dir) return
|
|
10030
|
+
if (existsSync8(path68.join(dir, WORKSPACE_FILE))) return dir;
|
|
10031
|
+
const parent = path68.dirname(dir);
|
|
10032
|
+
if (parent === dir) return path68.resolve(start);
|
|
9773
10033
|
dir = parent;
|
|
9774
10034
|
}
|
|
9775
10035
|
}
|
|
9776
10036
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
9777
|
-
return
|
|
10037
|
+
return path68.resolve(workspaceRoot, repoPath) === path68.resolve(workspaceRoot);
|
|
9778
10038
|
}
|
|
9779
10039
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
9780
10040
|
const apply = options.write ?? false;
|
|
9781
|
-
const configText = await readText(
|
|
10041
|
+
const configText = await readText(path68.join(workspaceRoot, WORKSPACE_FILE));
|
|
9782
10042
|
if (!configText) {
|
|
9783
10043
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
9784
10044
|
process.exitCode = 1;
|
|
@@ -9805,7 +10065,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
9805
10065
|
const statuses = [];
|
|
9806
10066
|
const aggregateInputs = [];
|
|
9807
10067
|
for (const repo of repos) {
|
|
9808
|
-
const repoRoot =
|
|
10068
|
+
const repoRoot = path68.resolve(workspaceRoot, repo.path);
|
|
9809
10069
|
say(`
|
|
9810
10070
|
\u2192 ${repo.name} (${repo.path})`);
|
|
9811
10071
|
try {
|
|
@@ -9876,7 +10136,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
9876
10136
|
const status = statusByName.get(repo.name);
|
|
9877
10137
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
9878
10138
|
if (status?.status === "ok") {
|
|
9879
|
-
const lock = await checkLock(
|
|
10139
|
+
const lock = await checkLock(path68.resolve(workspaceRoot, repo.path));
|
|
9880
10140
|
manifestRepos.push({
|
|
9881
10141
|
name: repo.name,
|
|
9882
10142
|
path: repo.path,
|
|
@@ -9932,7 +10192,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
9932
10192
|
try {
|
|
9933
10193
|
const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
|
|
9934
10194
|
if (linkResult.ok) {
|
|
9935
|
-
written.push(...linkResult.added.map((rel) =>
|
|
10195
|
+
written.push(...linkResult.added.map((rel) => path68.join(workspaceRoot, rel)));
|
|
9936
10196
|
if (!options.json) {
|
|
9937
10197
|
say(
|
|
9938
10198
|
`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 +10224,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
9964
10224
|
|
|
9965
10225
|
// src/commands/workspace/undo.ts
|
|
9966
10226
|
import { existsSync as existsSync9, statSync as statSync2 } from "node:fs";
|
|
9967
|
-
import
|
|
10227
|
+
import path69 from "node:path";
|
|
9968
10228
|
import fs47 from "fs-extra";
|
|
9969
10229
|
var WORKSPACE_AGGREGATE_FILES = [
|
|
9970
10230
|
"workspace-summary.json",
|
|
@@ -9993,7 +10253,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
9993
10253
|
}
|
|
9994
10254
|
let anyRepoFailed = false;
|
|
9995
10255
|
for (const repo of config.repos) {
|
|
9996
|
-
const repoRoot =
|
|
10256
|
+
const repoRoot = path69.resolve(workspaceRoot, repo.path);
|
|
9997
10257
|
log(`
|
|
9998
10258
|
\u2192 ${repo.name} (${repo.path})`);
|
|
9999
10259
|
try {
|
|
@@ -10008,7 +10268,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10008
10268
|
}
|
|
10009
10269
|
const linkedContext = await readLinkedContext(workspaceRoot);
|
|
10010
10270
|
for (const entry of linkedContext) {
|
|
10011
|
-
const abs =
|
|
10271
|
+
const abs = path69.join(workspaceRoot, entry.path);
|
|
10012
10272
|
if (await fs47.pathExists(abs)) {
|
|
10013
10273
|
await fs47.remove(abs);
|
|
10014
10274
|
log(`Removed ${entry.path} (linked ${entry.type} from ${entry.repo})`);
|
|
@@ -10018,20 +10278,20 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10018
10278
|
const abs = hausPath(workspaceRoot, rel);
|
|
10019
10279
|
if (await fs47.pathExists(abs)) {
|
|
10020
10280
|
await fs47.remove(abs);
|
|
10021
|
-
log(`Removed ${
|
|
10281
|
+
log(`Removed ${path69.relative(workspaceRoot, abs)}`);
|
|
10022
10282
|
}
|
|
10023
10283
|
}
|
|
10024
10284
|
const manifestFile = manifestPath(workspaceRoot);
|
|
10025
10285
|
if (await fs47.pathExists(manifestFile)) {
|
|
10026
10286
|
await fs47.remove(manifestFile);
|
|
10027
|
-
log(`Removed ${
|
|
10287
|
+
log(`Removed ${path69.relative(workspaceRoot, manifestFile)}`);
|
|
10028
10288
|
}
|
|
10029
10289
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
10030
10290
|
if (await fs47.pathExists(workspaceMdPath)) {
|
|
10031
10291
|
await fs47.remove(workspaceMdPath);
|
|
10032
|
-
log(`Removed ${
|
|
10292
|
+
log(`Removed ${path69.relative(workspaceRoot, workspaceMdPath)}`);
|
|
10033
10293
|
} else {
|
|
10034
|
-
const claudeMdPath =
|
|
10294
|
+
const claudeMdPath = path69.join(workspaceRoot, "CLAUDE.md");
|
|
10035
10295
|
if (existsSync9(claudeMdPath)) {
|
|
10036
10296
|
const prev = await readText(claudeMdPath);
|
|
10037
10297
|
if (prev?.includes(BLOCK_BEGIN)) {
|
|
@@ -10083,7 +10343,7 @@ relationships: []
|
|
|
10083
10343
|
log("Workspace initialized.");
|
|
10084
10344
|
}
|
|
10085
10345
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
10086
|
-
const configText = await readText(
|
|
10346
|
+
const configText = await readText(path70.join(workspaceRoot, WORKSPACE_FILE));
|
|
10087
10347
|
if (!configText) {
|
|
10088
10348
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
10089
10349
|
process.exitCode = 1;
|
|
@@ -10105,7 +10365,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
10105
10365
|
const inputs = [];
|
|
10106
10366
|
const failures = [];
|
|
10107
10367
|
for (const repo of config.repos) {
|
|
10108
|
-
const repoRoot =
|
|
10368
|
+
const repoRoot = path70.resolve(workspaceRoot, repo.path);
|
|
10109
10369
|
try {
|
|
10110
10370
|
if (!existsSync10(repoRoot) || !statSync3(repoRoot).isDirectory()) {
|
|
10111
10371
|
throw new Error("path is not a directory");
|
|
@@ -10171,7 +10431,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
10171
10431
|
// src/cli.ts
|
|
10172
10432
|
function cliVersion() {
|
|
10173
10433
|
try {
|
|
10174
|
-
const pkgPath =
|
|
10434
|
+
const pkgPath = path71.join(packageRoot(), "package.json");
|
|
10175
10435
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
10176
10436
|
return pkg.version ?? "0.0.0";
|
|
10177
10437
|
} catch {
|
|
@@ -10181,7 +10441,7 @@ function cliVersion() {
|
|
|
10181
10441
|
var program = new Command();
|
|
10182
10442
|
function validateRuntimeNodeVersion() {
|
|
10183
10443
|
try {
|
|
10184
|
-
const pkgPath =
|
|
10444
|
+
const pkgPath = path71.join(packageRoot(), "package.json");
|
|
10185
10445
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
10186
10446
|
const requiredRange = pkg.engines?.node;
|
|
10187
10447
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -10282,8 +10542,16 @@ worktree.command("add <slug>").description(
|
|
|
10282
10542
|
dryRun: opts.dryRun
|
|
10283
10543
|
})
|
|
10284
10544
|
);
|
|
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
|
-
|
|
10545
|
+
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(
|
|
10546
|
+
"--materialize",
|
|
10547
|
+
"Also `git worktree add` any member missing entirely (e.g. a worktree created by plain `git worktree add` outside haus) before hydrating it"
|
|
10548
|
+
).option("--dry-run", "Preview without writing or running anything").action(
|
|
10549
|
+
(opts) => runWorktree("hydrate", {
|
|
10550
|
+
only: opts.only,
|
|
10551
|
+
force: opts.force,
|
|
10552
|
+
materialize: opts.materialize,
|
|
10553
|
+
dryRun: opts.dryRun
|
|
10554
|
+
})
|
|
10287
10555
|
);
|
|
10288
10556
|
worktree.command("list").description("List materialized workspace worktrees and their member status").action(() => runWorktree("list"));
|
|
10289
10557
|
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(
|