@haus-tech/haus-workflow 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,36 +1,62 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { readFileSync as readFileSync4 } from "fs";
5
- import path34 from "path";
4
+ import { readFileSync as readFileSync4 } from "node:fs";
5
+ import path35 from "node:path";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/commands/apply.ts
9
- import path13 from "path";
9
+ import path13 from "node:path";
10
10
  import checkbox from "@inquirer/checkbox";
11
- import fs13 from "fs-extra";
12
11
 
13
12
  // src/catalog/remote-catalog.ts
14
- import os from "os";
15
- import path2 from "path";
13
+ import os from "node:os";
14
+ import path2 from "node:path";
16
15
  import fs2 from "fs-extra";
17
16
 
18
17
  // src/utils/fs.ts
19
- import crypto from "crypto";
20
- import path from "path";
18
+ import crypto from "node:crypto";
19
+ import path from "node:path";
21
20
  import fg from "fast-glob";
22
21
  import fs from "fs-extra";
23
- async function readJson(file) {
22
+ var MalformedJsonFileError = class extends Error {
23
+ constructor(filePath, backupPath) {
24
+ super(`Refusing to modify ${filePath}: invalid JSON. Backup written to ${backupPath}`);
25
+ this.filePath = filePath;
26
+ this.backupPath = backupPath;
27
+ }
28
+ filePath;
29
+ backupPath;
30
+ name = "MalformedJsonFileError";
31
+ };
32
+ async function readJsonDetailed(file) {
24
33
  try {
25
- return await fs.readJson(file);
26
- } catch {
27
- return void 0;
34
+ if (!await fs.pathExists(file)) return { status: "missing" };
35
+ const raw = await fs.readFile(file, "utf8");
36
+ try {
37
+ return { status: "ok", value: JSON.parse(raw) };
38
+ } catch (error2) {
39
+ return { status: "invalid", error: error2 };
40
+ }
41
+ } catch (error2) {
42
+ if (error2.code === "ENOENT") return { status: "missing" };
43
+ throw error2;
28
44
  }
29
45
  }
46
+ async function backupMalformedJsonFile(file) {
47
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
48
+ const backupPath = `${file}.haus-malformed-${stamp}.bak`;
49
+ await fs.copy(file, backupPath);
50
+ return backupPath;
51
+ }
52
+ async function readJson(file) {
53
+ const result = await readJsonDetailed(file);
54
+ if (result.status === "ok") return result.value;
55
+ return void 0;
56
+ }
30
57
  async function writeJson(file, value) {
31
- await fs.ensureDir(path.dirname(file));
32
- await fs.writeFile(file, `${JSON.stringify(value, null, 2)}
33
- `, "utf8");
58
+ await atomicWriteText(file, `${JSON.stringify(value, null, 2)}
59
+ `);
34
60
  }
35
61
  async function pruneEmptyDir(dir) {
36
62
  try {
@@ -47,8 +73,17 @@ async function readText(file) {
47
73
  }
48
74
  }
49
75
  async function writeText(file, value) {
50
- await fs.ensureDir(path.dirname(file));
51
- await fs.writeFile(file, value, "utf8");
76
+ await atomicWriteText(file, value);
77
+ }
78
+ async function atomicWriteText(file, value) {
79
+ const dir = path.dirname(file);
80
+ await fs.ensureDir(dir);
81
+ const tempPath = path.join(
82
+ dir,
83
+ `.tmp-haus-write-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`
84
+ );
85
+ await fs.writeFile(tempPath, value, "utf8");
86
+ await fs.move(tempPath, file, { overwrite: true });
52
87
  }
53
88
  async function listFiles(root, patterns) {
54
89
  const files = await fg(patterns, {
@@ -85,7 +120,7 @@ var error = (msg, ...args) => {
85
120
  };
86
121
 
87
122
  // src/catalog/constants.ts
88
- var CATALOG_REPO_URL = "https://raw.githubusercontent.com/wearehaustech/haus-workflow-catalog";
123
+ var CATALOG_REPO_URL = "https://raw.githubusercontent.com/WeAreHausTech/haus-workflow-catalog";
89
124
  var CATALOG_GITHUB_API_URL = "https://api.github.com/repos/WeAreHausTech/haus-workflow-catalog";
90
125
  var SUPERPOWERS_SHARED_CATALOG_REL = "skills/superpowers/shared";
91
126
  var CATALOG_REF = process.env.HAUS_CATALOG_REF;
@@ -337,11 +372,83 @@ function validateCatalogItem(item, content2) {
337
372
  return { ok: true };
338
373
  }
339
374
 
375
+ // src/catalog/manifest-item-fields.ts
376
+ var USE_MODES = /* @__PURE__ */ new Set(["copy", "adapted", "wrapped", "rewritten", "reference-only"]);
377
+ var LICENSE_CONFIDENCES = /* @__PURE__ */ new Set(["high", "medium", "low", "unknown"]);
378
+ var REVIEW_STATUSES = /* @__PURE__ */ new Set([
379
+ "approved",
380
+ "candidate",
381
+ "needs-review",
382
+ "rejected",
383
+ "deprecated"
384
+ ]);
385
+ var RISK_LEVELS = /* @__PURE__ */ new Set(["low", "medium", "high", "blocked"]);
386
+ function isNonEmptyString(value) {
387
+ return typeof value === "string" && value.length > 0;
388
+ }
389
+ function isStringArray(value) {
390
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
391
+ }
392
+ function validateReferences(itemId, references) {
393
+ if (references === void 0) return null;
394
+ if (!isStringArray(references)) return `${itemId}: references must be a string array`;
395
+ for (const ref of references) {
396
+ if (/^https?:\/\//i.test(ref)) {
397
+ if (!/^https:\/\//i.test(ref)) {
398
+ return `${itemId}: reference must be https:// URL: ${ref}`;
399
+ }
400
+ }
401
+ }
402
+ return null;
403
+ }
404
+ function validateCuratedProvenance(item) {
405
+ if (item.source !== "curated") return null;
406
+ if (!isNonEmptyString(item.reviewStatus)) {
407
+ return `${item.id}: curated item missing reviewStatus`;
408
+ }
409
+ if (!REVIEW_STATUSES.has(item.reviewStatus)) {
410
+ return `${item.id}: invalid reviewStatus "${item.reviewStatus}"`;
411
+ }
412
+ if (!isNonEmptyString(item.riskLevel)) {
413
+ return `${item.id}: curated item missing riskLevel`;
414
+ }
415
+ if (!RISK_LEVELS.has(item.riskLevel)) {
416
+ return `${item.id}: invalid riskLevel "${item.riskLevel}"`;
417
+ }
418
+ if (item.useMode !== void 0 && !USE_MODES.has(String(item.useMode))) {
419
+ return `${item.id}: invalid useMode "${item.useMode}"`;
420
+ }
421
+ if (item.licenseConfidence !== void 0 && !LICENSE_CONFIDENCES.has(String(item.licenseConfidence))) {
422
+ return `${item.id}: invalid licenseConfidence "${item.licenseConfidence}"`;
423
+ }
424
+ if (item.originUrl !== void 0) {
425
+ if (typeof item.originUrl !== "string" || !item.originUrl.startsWith("https://")) {
426
+ return `${item.id}: originUrl must be an https:// URL`;
427
+ }
428
+ }
429
+ return null;
430
+ }
431
+
340
432
  // src/catalog/manifest-schema.ts
341
433
  var POLLUTION_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
342
- function isNonEmptyString(value) {
434
+ function isNonEmptyString2(value) {
343
435
  return typeof value === "string" && value.length > 0;
344
436
  }
437
+ function isStringArray2(value) {
438
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
439
+ }
440
+ var REQUIRES_ANY_KEYS = ["stack", "dependency", "packageNamePattern", "role"];
441
+ function isRequiresAnyClause(value) {
442
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
443
+ const keys = Object.keys(value);
444
+ if (keys.length !== 1) return false;
445
+ const key = keys[0];
446
+ if (!REQUIRES_ANY_KEYS.includes(key)) return false;
447
+ return typeof value[key] === "string";
448
+ }
449
+ function isRequiresAnyArray(value) {
450
+ return Array.isArray(value) && value.every(isRequiresAnyClause);
451
+ }
345
452
  function safeParse(json) {
346
453
  return JSON.parse(json, (key, value) => {
347
454
  if (POLLUTION_KEYS.has(key)) return void 0;
@@ -359,7 +466,7 @@ function parseManifest(json) {
359
466
  return { ok: false, error: "manifest must be an object" };
360
467
  }
361
468
  const root = data;
362
- if (!isNonEmptyString(root.version)) {
469
+ if (!isNonEmptyString2(root.version)) {
363
470
  return { ok: false, error: "missing version" };
364
471
  }
365
472
  if (!Array.isArray(root.items)) {
@@ -374,13 +481,13 @@ function parseManifest(json) {
374
481
  return { ok: false, error: `item[${i}]: invalid entry` };
375
482
  }
376
483
  const item = raw;
377
- if (!isNonEmptyString(item.id)) {
484
+ if (!isNonEmptyString2(item.id)) {
378
485
  return { ok: false, error: `item[${i}]: missing id` };
379
486
  }
380
- if (!isNonEmptyString(item.type)) {
487
+ if (!isNonEmptyString2(item.type)) {
381
488
  return { ok: false, error: `${item.id}: missing type` };
382
489
  }
383
- if (!isNonEmptyString(item.path)) {
490
+ if (!isNonEmptyString2(item.path)) {
384
491
  return { ok: false, error: `${item.id}: missing path` };
385
492
  }
386
493
  if (seenIds.has(item.id)) {
@@ -392,13 +499,21 @@ function parseManifest(json) {
392
499
  return { ok: false, error: `${item.id}: duplicate path "${normPath}"` };
393
500
  }
394
501
  seenPaths.add(normPath);
395
- if (item.source === "curated") {
396
- if (!isNonEmptyString(item.reviewStatus)) {
397
- return { ok: false, error: `${item.id}: curated item missing reviewStatus` };
398
- }
399
- if (!isNonEmptyString(item.riskLevel)) {
400
- return { ok: false, error: `${item.id}: curated item missing riskLevel` };
401
- }
502
+ const provenanceError = validateCuratedProvenance(item);
503
+ if (provenanceError) return { ok: false, error: provenanceError };
504
+ const referencesError = validateReferences(String(item.id), item.references);
505
+ if (referencesError) return { ok: false, error: referencesError };
506
+ if (!isStringArray2(item.tags)) {
507
+ return { ok: false, error: `${item.id}: tags must be a string array` };
508
+ }
509
+ if (!isStringArray2(item.repoRoles)) {
510
+ return { ok: false, error: `${item.id}: repoRoles must be a string array` };
511
+ }
512
+ if (item.requiresAny !== void 0 && !isRequiresAnyArray(item.requiresAny)) {
513
+ return { ok: false, error: `${item.id}: requiresAny must be an array of clause objects` };
514
+ }
515
+ if (typeof item.tokenEstimate !== "number" || !Number.isFinite(item.tokenEstimate)) {
516
+ return { ok: false, error: `${item.id}: tokenEstimate must be a finite number` };
402
517
  }
403
518
  items.push(raw);
404
519
  }
@@ -818,6 +933,17 @@ async function syncRemoteCatalog() {
818
933
  return { newItems, refreshed, unchanged, failed };
819
934
  }
820
935
  var CATALOG_TAGS_API_URL = "https://api.github.com/repos/WeAreHausTech/haus-workflow-catalog/tags";
936
+ function parseSemverTag(tag) {
937
+ const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(tag);
938
+ if (!match) return null;
939
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
940
+ }
941
+ function compareSemver(a, b) {
942
+ for (let i = 0; i < 3; i += 1) {
943
+ if (a[i] !== b[i]) return a[i] - b[i];
944
+ }
945
+ return 0;
946
+ }
821
947
  async function fetchLatestCatalogTag() {
822
948
  if (process.env["HAUS_CATALOG_REMOTE_BASE"]) return null;
823
949
  try {
@@ -827,7 +953,16 @@ async function fetchLatestCatalogTag() {
827
953
  });
828
954
  if (!res.ok) return null;
829
955
  const tags = await res.json();
830
- return tags[0]?.name ?? null;
956
+ const valid = tags.map((tag) => {
957
+ const name = typeof tag.name === "string" ? tag.name : "";
958
+ const semver2 = parseSemverTag(name);
959
+ return semver2 ? { name, semver: semver2 } : null;
960
+ }).filter(
961
+ (entry) => entry !== null
962
+ );
963
+ if (valid.length === 0) return null;
964
+ valid.sort((a, b) => compareSemver(b.semver, a.semver));
965
+ return valid[0].name;
831
966
  } catch {
832
967
  return null;
833
968
  }
@@ -841,6 +976,226 @@ async function getCacheManifestAge() {
841
976
  }
842
977
  }
843
978
 
979
+ // src/claude/write-claude-files.ts
980
+ import path12 from "node:path";
981
+ import fs12 from "fs-extra";
982
+
983
+ // src/catalog/load-catalog.ts
984
+ import path4 from "node:path";
985
+
986
+ // src/utils/paths.ts
987
+ import { existsSync, readFileSync } from "node:fs";
988
+ import os2 from "node:os";
989
+ import path3 from "node:path";
990
+ import { fileURLToPath } from "node:url";
991
+ var HAUS_DIR = ".haus-workflow";
992
+ function hausPath(root, ...parts) {
993
+ return path3.join(root, HAUS_DIR, ...parts);
994
+ }
995
+ function claudePath(root, ...parts) {
996
+ return path3.join(root, ".claude", ...parts);
997
+ }
998
+ function displayPath(root, targetPath) {
999
+ const rel = path3.relative(root, targetPath).replace(/\\/g, "/");
1000
+ if (rel && !rel.startsWith("../") && rel !== "..") {
1001
+ return rel.startsWith("./") ? rel : `./${rel}`;
1002
+ }
1003
+ const home = os2.homedir();
1004
+ const normalized = targetPath.replace(/\\/g, "/");
1005
+ if (home && home.trim().length > 0) {
1006
+ const homeRel = path3.relative(home, targetPath).replace(/\\/g, "/");
1007
+ if (homeRel && !homeRel.startsWith("../") && homeRel !== "..") {
1008
+ return `~/${homeRel}`;
1009
+ }
1010
+ }
1011
+ return normalized;
1012
+ }
1013
+ function packageRoot() {
1014
+ let dir = path3.dirname(fileURLToPath(import.meta.url));
1015
+ for (let i = 0; i < 12; i++) {
1016
+ const pkgPath = path3.join(dir, "package.json");
1017
+ if (existsSync(pkgPath)) {
1018
+ try {
1019
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
1020
+ if (pkg.name === "haus" || pkg.name === "@haus-tech/haus-workflow") return dir;
1021
+ } catch {
1022
+ }
1023
+ }
1024
+ const parent = path3.dirname(dir);
1025
+ if (parent === dir) break;
1026
+ dir = parent;
1027
+ }
1028
+ const file = fileURLToPath(import.meta.url);
1029
+ return path3.resolve(path3.dirname(file), "../..");
1030
+ }
1031
+
1032
+ // src/catalog/load-catalog.ts
1033
+ async function loadCatalogContext(root) {
1034
+ const envPath = process.env["HAUS_FIXTURE_CATALOG"];
1035
+ if (envPath) {
1036
+ const data2 = await readJson(envPath);
1037
+ return {
1038
+ items: data2?.items ?? [],
1039
+ contentRoot: path4.dirname(envPath),
1040
+ source: "fixture"
1041
+ };
1042
+ }
1043
+ const cacheDir = getCacheDir();
1044
+ const cacheManifestPath = path4.join(cacheDir, "manifest.json");
1045
+ const cacheData = await readJson(cacheManifestPath);
1046
+ if (cacheData?.items?.length) {
1047
+ return { items: cacheData.items, contentRoot: cacheDir, source: "cache" };
1048
+ }
1049
+ const localManifest = path4.join(root, "library/catalog/manifest.json");
1050
+ const localData = await readJson(localManifest);
1051
+ if (localData?.items?.length) {
1052
+ return {
1053
+ items: localData.items,
1054
+ contentRoot: path4.dirname(localManifest),
1055
+ source: "local"
1056
+ };
1057
+ }
1058
+ const packageManifest = path4.join(packageRoot(), "library/catalog/manifest.json");
1059
+ const data = await readJson(packageManifest);
1060
+ return {
1061
+ items: data?.items ?? [],
1062
+ contentRoot: path4.dirname(packageManifest),
1063
+ source: "bundled"
1064
+ };
1065
+ }
1066
+ async function loadCatalog(root) {
1067
+ const ctx = await loadCatalogContext(root);
1068
+ return ctx.items;
1069
+ }
1070
+ function catalogItemContentPath(contentRoot, item) {
1071
+ return path4.join(contentRoot, item.path);
1072
+ }
1073
+
1074
+ // src/update/hash-installed.ts
1075
+ import path5 from "node:path";
1076
+ import fg2 from "fast-glob";
1077
+ import fs3 from "fs-extra";
1078
+
1079
+ // src/claude/haus-managed-header.ts
1080
+ function parseHausManagedAttrs(line2) {
1081
+ const match = /^<!--\s+HAUS-MANAGED\b(.*?)-->$/.exec(line2.trim());
1082
+ if (!match) return null;
1083
+ const raw = match[1] ?? "";
1084
+ const pairs = [...raw.matchAll(/\b([a-z]+)=([^\s>]+)/g)];
1085
+ const attrs = {};
1086
+ for (const [, key, value] of pairs) attrs[key] = value;
1087
+ if (!attrs.id) return null;
1088
+ return {
1089
+ id: attrs.id,
1090
+ v: attrs.v,
1091
+ source: attrs.source,
1092
+ hash: attrs.hash
1093
+ };
1094
+ }
1095
+
1096
+ // src/claude/managed-template.ts
1097
+ function normaliseLF(content2) {
1098
+ return content2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1099
+ }
1100
+ var SCHEMA_VERSION = 1;
1101
+ function parseHausManagedHeader(line2) {
1102
+ const attrs = parseHausManagedAttrs(line2);
1103
+ if (!attrs?.id) return null;
1104
+ const parsedV = attrs.v && /^\d+$/.test(attrs.v) ? Number(attrs.v) : void 0;
1105
+ const hash = attrs.hash && /^sha256-[a-f0-9]+$/.test(attrs.hash) ? attrs.hash : void 0;
1106
+ return {
1107
+ id: attrs.id,
1108
+ v: parsedV,
1109
+ source: attrs.source,
1110
+ hash
1111
+ };
1112
+ }
1113
+
1114
+ // src/update/hash-installed.ts
1115
+ var EMPTY_LOCK_PATHS_TOKEN = "haus-lock:empty-paths";
1116
+ async function hashInstalledPaths(root, relPaths) {
1117
+ if (relPaths.length === 0) {
1118
+ return hashText(EMPTY_LOCK_PATHS_TOKEN);
1119
+ }
1120
+ const normalized = [...new Set(relPaths.map((p) => p.replace(/\\/g, "/")))].sort();
1121
+ const fileDigests = [];
1122
+ for (const rel of normalized) {
1123
+ const abs = path5.join(root, rel);
1124
+ if (!await fs3.pathExists(abs)) continue;
1125
+ const stat = await fs3.stat(abs);
1126
+ if (stat.isFile()) {
1127
+ const body = await fs3.readFile(abs, "utf8");
1128
+ fileDigests.push({ rel, digest: hashText(normaliseLF(body)) });
1129
+ continue;
1130
+ }
1131
+ if (!stat.isDirectory()) continue;
1132
+ const inner = await fg2("**/*", { cwd: abs, onlyFiles: true, dot: true });
1133
+ for (const sub of inner.sort()) {
1134
+ const relFile = path5.join(rel, sub).replace(/\\/g, "/");
1135
+ const absFile = path5.join(abs, sub);
1136
+ const body = await fs3.readFile(absFile, "utf8");
1137
+ fileDigests.push({ rel: relFile, digest: hashText(normaliseLF(body)) });
1138
+ }
1139
+ }
1140
+ if (fileDigests.length === 0) {
1141
+ return hashText(`${EMPTY_LOCK_PATHS_TOKEN}|${normalized.join("|")}`);
1142
+ }
1143
+ fileDigests.sort((a, b) => a.rel.localeCompare(b.rel));
1144
+ return hashText(fileDigests.map((f) => `${f.rel}=${f.digest}`).join("|"));
1145
+ }
1146
+
1147
+ // src/claude/managed-write.ts
1148
+ import fs4 from "fs-extra";
1149
+
1150
+ // src/utils/diff.ts
1151
+ import { createTwoFilesPatch } from "diff";
1152
+ function hasTextChanged(before, after) {
1153
+ return before !== after;
1154
+ }
1155
+ function createUnifiedDiff(filePath, before, after) {
1156
+ return createTwoFilesPatch(filePath, filePath, before, after, "before", "after", {
1157
+ context: 3
1158
+ });
1159
+ }
1160
+ function summarizeDiff(diffText) {
1161
+ const lines = diffText.split("\n");
1162
+ let additions = 0;
1163
+ let deletions = 0;
1164
+ for (const line2 of lines) {
1165
+ if (line2.startsWith("+++ ") || line2.startsWith("--- ")) continue;
1166
+ if (line2.startsWith("+")) additions += 1;
1167
+ if (line2.startsWith("-")) deletions += 1;
1168
+ }
1169
+ return { additions, deletions };
1170
+ }
1171
+
1172
+ // src/claude/managed-write.ts
1173
+ async function writeManagedText(root, filePath, nextText, dryRun) {
1174
+ const prev = await fs4.pathExists(filePath) ? await fs4.readFile(filePath, "utf8") : "";
1175
+ const printable = displayPath(root, filePath);
1176
+ if (dryRun) {
1177
+ if (!prev) {
1178
+ log(createUnifiedDiff(printable, "", nextText));
1179
+ } else if (hasTextChanged(prev, nextText)) {
1180
+ log(createUnifiedDiff(printable, prev, nextText));
1181
+ } else {
1182
+ log(`${printable}: unchanged`);
1183
+ }
1184
+ return;
1185
+ }
1186
+ if (hasTextChanged(prev, nextText) && prev.length > 0) {
1187
+ const diffText = createUnifiedDiff(printable, prev, nextText);
1188
+ const summary = summarizeDiff(diffText);
1189
+ log(`Overwriting ${printable} (diff +${summary.additions} -${summary.deletions})`);
1190
+ }
1191
+ await writeText(filePath, nextText);
1192
+ }
1193
+ async function writeManagedJson(root, filePath, value, dryRun) {
1194
+ const nextText = `${JSON.stringify(value, null, 2)}
1195
+ `;
1196
+ await writeManagedText(root, filePath, nextText, dryRun);
1197
+ }
1198
+
844
1199
  // src/install/allow-rules.ts
845
1200
  var ALLOWED_SUBCOMMANDS = ["setup-project", "apply", "doctor", "scan", "recommend"];
846
1201
  function buildAllowRules() {
@@ -848,18 +1203,18 @@ function buildAllowRules() {
848
1203
  }
849
1204
 
850
1205
  // src/install/settings-merge.ts
851
- import path4 from "path";
852
- import fs3 from "fs-extra";
1206
+ import path7 from "node:path";
1207
+ import fs5 from "fs-extra";
853
1208
 
854
1209
  // src/install/manifest.ts
855
- import os2 from "os";
856
- import path3 from "path";
1210
+ import os3 from "node:os";
1211
+ import path6 from "node:path";
857
1212
  var MANIFEST_SCHEMA = "haus-install-manifest/1";
858
1213
  function globalClaudeDir() {
859
- return path3.join(os2.homedir(), ".claude");
1214
+ return path6.join(os3.homedir(), ".claude");
860
1215
  }
861
1216
  function hausManifestPath() {
862
- return path3.join(globalClaudeDir(), "haus", "install-manifest.json");
1217
+ return path6.join(globalClaudeDir(), "haus", "install-manifest.json");
863
1218
  }
864
1219
  async function readManifest() {
865
1220
  return readJson(hausManifestPath());
@@ -879,11 +1234,15 @@ function buildManifest(source, files, hooks) {
879
1234
 
880
1235
  // src/install/settings-merge.ts
881
1236
  function settingsJsonPath() {
882
- return path4.join(globalClaudeDir(), "settings.json");
1237
+ return path7.join(globalClaudeDir(), "settings.json");
883
1238
  }
884
1239
  async function readSettings() {
885
- const parsed = await readJson(settingsJsonPath());
886
- return parsed ?? {};
1240
+ const filePath = settingsJsonPath();
1241
+ const result = await readJsonDetailed(filePath);
1242
+ if (result.status === "ok") return result.value;
1243
+ if (result.status === "missing") return {};
1244
+ const backupPath = await backupMalformedJsonFile(filePath);
1245
+ throw new MalformedJsonFileError(filePath, backupPath);
887
1246
  }
888
1247
  async function writeSettings(settings) {
889
1248
  await writeJson(settingsJsonPath(), settings);
@@ -897,6 +1256,14 @@ function collectEventHookCommands(entries) {
897
1256
  }
898
1257
  return cmds;
899
1258
  }
1259
+ function pruneEntryCommands(entry, shouldDrop) {
1260
+ const keptHooks = (entry.hooks ?? []).filter((hook) => {
1261
+ const cmd = hook.command ?? "";
1262
+ return !cmd || !shouldDrop(cmd);
1263
+ });
1264
+ if (keptHooks.length === 0) return null;
1265
+ return { ...entry, hooks: keptHooks };
1266
+ }
900
1267
  var RETIRED_HAUS_HOOK_COMMANDS = ["haus context --from-hook"];
901
1268
  var RETIRED_HAUS_HOOK_IDS = ["hook.context", "haus.context-hook"];
902
1269
  function reconcileRetiredHausHooks(updated, fragments, priorHaus, addedIds, addedCommands) {
@@ -910,10 +1277,7 @@ function reconcileRetiredHausHooks(updated, fragments, priorHaus, addedIds, adde
910
1277
  }
911
1278
  const prunedHooks = {};
912
1279
  for (const [event, entries] of Object.entries(updated.hooks ?? {})) {
913
- const kept = entries.filter((entry) => {
914
- const cmds = (entry.hooks ?? []).map((h) => h.command).filter(Boolean);
915
- return cmds.length === 0 || !cmds.some((cmd) => retiredCommands.has(cmd));
916
- });
1280
+ const kept = entries.map((entry) => pruneEntryCommands(entry, (cmd) => retiredCommands.has(cmd))).filter((entry) => entry !== null);
917
1281
  if (kept.length > 0) prunedHooks[event] = kept;
918
1282
  }
919
1283
  updated.hooks = prunedHooks;
@@ -1064,10 +1428,12 @@ function stripHausHooks(settings) {
1064
1428
  const updated = { ...settings };
1065
1429
  updated.hooks = {};
1066
1430
  for (const [event, entries] of Object.entries(settings.hooks ?? {})) {
1067
- const kept = entries.filter((entry) => {
1068
- const cmd = entry.hooks[0]?.command ?? "";
1069
- return usePrefix ? !cmd.startsWith("haus ") : !ownedCommands.has(cmd);
1070
- });
1431
+ const kept = entries.map(
1432
+ (entry) => pruneEntryCommands(
1433
+ entry,
1434
+ (cmd) => usePrefix ? cmd.startsWith("haus ") : ownedCommands.has(cmd)
1435
+ )
1436
+ ).filter((entry) => entry !== null);
1071
1437
  if (kept.length > 0) updated.hooks[event] = kept;
1072
1438
  }
1073
1439
  const { _haus: _, ...rest } = updated;
@@ -1113,7 +1479,7 @@ function stripHausAsk(settings) {
1113
1479
  async function loadHooksFragment(fragmentPath) {
1114
1480
  let raw;
1115
1481
  try {
1116
- raw = await fs3.readJson(fragmentPath);
1482
+ raw = await fs5.readJson(fragmentPath);
1117
1483
  } catch {
1118
1484
  return [];
1119
1485
  }
@@ -1126,6 +1492,8 @@ var DENY_COMMANDS = [
1126
1492
  "sudo",
1127
1493
  "chmod -R 777",
1128
1494
  "git push --force",
1495
+ "git push --force-with-lease",
1496
+ "git push -f",
1129
1497
  "drop database",
1130
1498
  "truncate table",
1131
1499
  "npm publish",
@@ -1153,6 +1521,20 @@ var DENY_PATHS = [
1153
1521
  "certs"
1154
1522
  ];
1155
1523
  var DENY_DIRS = /* @__PURE__ */ new Set(["customer-data", "secrets", "certs"]);
1524
+ var DENY_PATH_REGEXES = [
1525
+ /(^|\/)[^/]+\.pem$/i,
1526
+ /(^|\/)[^/]+\.key$/i,
1527
+ /(^|\/)[^/]+\.p12$/i,
1528
+ /(^|\/)[^/]+\.pfx$/i,
1529
+ /(^|\/)id_rsa$/i,
1530
+ /(^|\/)id_ed25519$/i,
1531
+ /(^|\/)customer-data(\/|$)/i,
1532
+ /(^|\/)secrets(\/|$)/i,
1533
+ /(^|\/)certs(\/|$)/i
1534
+ ];
1535
+ function normalizePathForMatch(candidate) {
1536
+ return candidate.replace(/\\/g, "/").replace(/\/+/g, "/").trim();
1537
+ }
1156
1538
  var ASK_PATHS = [
1157
1539
  { pattern: ".env", tools: ["Edit", "Write"] },
1158
1540
  { pattern: ".env.*", tools: ["Edit", "Write"] },
@@ -1212,8 +1594,8 @@ function buildDenyRules() {
1212
1594
  for (const command of DENY_COMMANDS) {
1213
1595
  rules.push(`Bash(${command}:*)`);
1214
1596
  }
1215
- for (const path35 of DENY_PATHS) {
1216
- const pattern = DENY_DIRS.has(path35) ? `${path35}/**` : path35;
1597
+ for (const path36 of DENY_PATHS) {
1598
+ const pattern = DENY_DIRS.has(path36) ? `${path36}/**` : path36;
1217
1599
  for (const tool of FILE_TOOLS) {
1218
1600
  rules.push(`${tool}(${pattern})`);
1219
1601
  }
@@ -1221,52 +1603,6 @@ function buildDenyRules() {
1221
1603
  return [...new Set(rules)];
1222
1604
  }
1223
1605
 
1224
- // src/utils/paths.ts
1225
- import { existsSync, readFileSync } from "fs";
1226
- import os3 from "os";
1227
- import path5 from "path";
1228
- import { fileURLToPath } from "url";
1229
- var HAUS_DIR = ".haus-workflow";
1230
- function hausPath(root, ...parts) {
1231
- return path5.join(root, HAUS_DIR, ...parts);
1232
- }
1233
- function claudePath(root, ...parts) {
1234
- return path5.join(root, ".claude", ...parts);
1235
- }
1236
- function displayPath(root, targetPath) {
1237
- const rel = path5.relative(root, targetPath).replace(/\\/g, "/");
1238
- if (rel && !rel.startsWith("../") && rel !== "..") {
1239
- return rel.startsWith("./") ? rel : `./${rel}`;
1240
- }
1241
- const home = os3.homedir();
1242
- const normalized = targetPath.replace(/\\/g, "/");
1243
- if (home && home.trim().length > 0) {
1244
- const homeRel = path5.relative(home, targetPath).replace(/\\/g, "/");
1245
- if (homeRel && !homeRel.startsWith("../") && homeRel !== "..") {
1246
- return `~/${homeRel}`;
1247
- }
1248
- }
1249
- return normalized;
1250
- }
1251
- function packageRoot() {
1252
- let dir = path5.dirname(fileURLToPath(import.meta.url));
1253
- for (let i = 0; i < 12; i++) {
1254
- const pkgPath = path5.join(dir, "package.json");
1255
- if (existsSync(pkgPath)) {
1256
- try {
1257
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
1258
- if (pkg.name === "haus" || pkg.name === "@haus-tech/haus-workflow") return dir;
1259
- } catch {
1260
- }
1261
- }
1262
- const parent = path5.dirname(dir);
1263
- if (parent === dir) break;
1264
- dir = parent;
1265
- }
1266
- const file = fileURLToPath(import.meta.url);
1267
- return path5.resolve(path5.dirname(file), "../..");
1268
- }
1269
-
1270
1606
  // src/claude/merge-project-settings.ts
1271
1607
  var PROJECT_HOOK_FRAGMENTS = [
1272
1608
  {
@@ -1285,8 +1621,12 @@ var PROJECT_HOOK_FRAGMENTS = [
1285
1621
  }
1286
1622
  ];
1287
1623
  async function readProjectSettings(root) {
1288
- const parsed = await readJson(claudePath(root, "settings.json"));
1289
- return parsed ?? {};
1624
+ const filePath = claudePath(root, "settings.json");
1625
+ const result = await readJsonDetailed(filePath);
1626
+ if (result.status === "ok") return result.value;
1627
+ if (result.status === "missing") return {};
1628
+ const backupPath = await backupMalformedJsonFile(filePath);
1629
+ throw new MalformedJsonFileError(filePath, backupPath);
1290
1630
  }
1291
1631
  async function writeProjectSettings(root, settings) {
1292
1632
  await writeJson(claudePath(root, "settings.json"), settings);
@@ -1305,164 +1645,8 @@ async function applyProjectSettingsMerge(root) {
1305
1645
  return merged;
1306
1646
  }
1307
1647
 
1308
- // src/claude/write-claude-files.ts
1309
- import path12 from "path";
1310
- import fs12 from "fs-extra";
1311
-
1312
- // src/catalog/load-catalog.ts
1313
- import path6 from "path";
1314
- async function loadCatalogContext(root) {
1315
- const envPath = process.env["HAUS_FIXTURE_CATALOG"];
1316
- if (envPath) {
1317
- const data2 = await readJson(envPath);
1318
- return {
1319
- items: data2?.items ?? [],
1320
- contentRoot: path6.dirname(envPath),
1321
- source: "fixture"
1322
- };
1323
- }
1324
- const cacheDir = getCacheDir();
1325
- const cacheManifestPath = path6.join(cacheDir, "manifest.json");
1326
- const cacheData = await readJson(cacheManifestPath);
1327
- if (cacheData?.items?.length) {
1328
- return { items: cacheData.items, contentRoot: cacheDir, source: "cache" };
1329
- }
1330
- const localManifest = path6.join(root, "library/catalog/manifest.json");
1331
- const localData = await readJson(localManifest);
1332
- if (localData?.items?.length) {
1333
- return {
1334
- items: localData.items,
1335
- contentRoot: path6.dirname(localManifest),
1336
- source: "local"
1337
- };
1338
- }
1339
- const packageManifest = path6.join(packageRoot(), "library/catalog/manifest.json");
1340
- const data = await readJson(packageManifest);
1341
- return {
1342
- items: data?.items ?? [],
1343
- contentRoot: path6.dirname(packageManifest),
1344
- source: "bundled"
1345
- };
1346
- }
1347
- async function loadCatalog(root) {
1348
- const ctx = await loadCatalogContext(root);
1349
- return ctx.items;
1350
- }
1351
- function catalogItemContentPath(contentRoot, item) {
1352
- return path6.join(contentRoot, item.path);
1353
- }
1354
-
1355
- // src/update/hash-installed.ts
1356
- import path7 from "path";
1357
- import fg2 from "fast-glob";
1358
- import fs4 from "fs-extra";
1359
-
1360
- // src/claude/managed-template.ts
1361
- function normaliseLF(content2) {
1362
- return content2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1363
- }
1364
- var SCHEMA_VERSION = 1;
1365
- function parseHausManagedHeader(line2) {
1366
- const match = line2.match(/<!-- HAUS-MANAGED id=([\w.:-]+)/);
1367
- if (!match) return null;
1368
- const vMatch = line2.match(/\bv=(\d+)/);
1369
- const sourceMatch = line2.match(/\bsource=([^\s]+)/);
1370
- const hashMatch = line2.match(/hash=(sha256-[a-f0-9]+)/);
1371
- return {
1372
- id: match[1],
1373
- v: vMatch ? Number(vMatch[1]) : void 0,
1374
- source: sourceMatch?.[1],
1375
- hash: hashMatch?.[1]
1376
- };
1377
- }
1378
-
1379
- // src/update/hash-installed.ts
1380
- var EMPTY_LOCK_PATHS_TOKEN = "haus-lock:empty-paths";
1381
- async function hashInstalledPaths(root, relPaths) {
1382
- if (relPaths.length === 0) {
1383
- return hashText(EMPTY_LOCK_PATHS_TOKEN);
1384
- }
1385
- const normalized = [...new Set(relPaths.map((p) => p.replace(/\\/g, "/")))].sort();
1386
- const fileDigests = [];
1387
- for (const rel of normalized) {
1388
- const abs = path7.join(root, rel);
1389
- if (!await fs4.pathExists(abs)) continue;
1390
- const stat = await fs4.stat(abs);
1391
- if (stat.isFile()) {
1392
- const body = await fs4.readFile(abs, "utf8");
1393
- fileDigests.push({ rel, digest: hashText(normaliseLF(body)) });
1394
- continue;
1395
- }
1396
- if (!stat.isDirectory()) continue;
1397
- const inner = await fg2("**/*", { cwd: abs, onlyFiles: true, dot: true });
1398
- for (const sub of inner.sort()) {
1399
- const relFile = path7.join(rel, sub).replace(/\\/g, "/");
1400
- const absFile = path7.join(abs, sub);
1401
- const body = await fs4.readFile(absFile, "utf8");
1402
- fileDigests.push({ rel: relFile, digest: hashText(normaliseLF(body)) });
1403
- }
1404
- }
1405
- if (fileDigests.length === 0) {
1406
- return hashText(`${EMPTY_LOCK_PATHS_TOKEN}|${normalized.join("|")}`);
1407
- }
1408
- fileDigests.sort((a, b) => a.rel.localeCompare(b.rel));
1409
- return hashText(fileDigests.map((f) => `${f.rel}=${f.digest}`).join("|"));
1410
- }
1411
-
1412
- // src/claude/managed-write.ts
1413
- import fs5 from "fs-extra";
1414
-
1415
- // src/utils/diff.ts
1416
- import { createTwoFilesPatch } from "diff";
1417
- function hasTextChanged(before, after) {
1418
- return before !== after;
1419
- }
1420
- function createUnifiedDiff(filePath, before, after) {
1421
- return createTwoFilesPatch(filePath, filePath, before, after, "before", "after", {
1422
- context: 3
1423
- });
1424
- }
1425
- function summarizeDiff(diffText) {
1426
- const lines = diffText.split("\n");
1427
- let additions = 0;
1428
- let deletions = 0;
1429
- for (const line2 of lines) {
1430
- if (line2.startsWith("+++ ") || line2.startsWith("--- ")) continue;
1431
- if (line2.startsWith("+")) additions += 1;
1432
- if (line2.startsWith("-")) deletions += 1;
1433
- }
1434
- return { additions, deletions };
1435
- }
1436
-
1437
- // src/claude/managed-write.ts
1438
- async function writeManagedText(root, filePath, nextText, dryRun) {
1439
- const prev = await fs5.pathExists(filePath) ? await fs5.readFile(filePath, "utf8") : "";
1440
- const printable = displayPath(root, filePath);
1441
- if (dryRun) {
1442
- if (!prev) {
1443
- log(createUnifiedDiff(printable, "", nextText));
1444
- } else if (hasTextChanged(prev, nextText)) {
1445
- log(createUnifiedDiff(printable, prev, nextText));
1446
- } else {
1447
- log(`${printable}: unchanged`);
1448
- }
1449
- return;
1450
- }
1451
- if (hasTextChanged(prev, nextText) && prev.length > 0) {
1452
- const diffText = createUnifiedDiff(printable, prev, nextText);
1453
- const summary = summarizeDiff(diffText);
1454
- log(`Overwriting ${printable} (diff +${summary.additions} -${summary.deletions})`);
1455
- }
1456
- await writeText(filePath, nextText);
1457
- }
1458
- async function writeManagedJson(root, filePath, value, dryRun) {
1459
- const nextText = `${JSON.stringify(value, null, 2)}
1460
- `;
1461
- await writeManagedText(root, filePath, nextText, dryRun);
1462
- }
1463
-
1464
1648
  // src/claude/superpowers-install.ts
1465
- import path8 from "path";
1649
+ import path8 from "node:path";
1466
1650
  import fg3 from "fast-glob";
1467
1651
  import fs6 from "fs-extra";
1468
1652
  var SUPERPOWERS_ORIGIN_SOURCE_ID = "superpowers-pcvelz";
@@ -1602,7 +1786,7 @@ async function verifyProjectSettingsHooksContract(root) {
1602
1786
  }
1603
1787
 
1604
1788
  // src/claude/write-root-claude-md.ts
1605
- import path9 from "path";
1789
+ import path9 from "node:path";
1606
1790
  import fs8 from "fs-extra";
1607
1791
  var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
1608
1792
  var BLOCK_END = "<!-- HAUS:END haus-imports -->";
@@ -1613,25 +1797,50 @@ function buildImportBlock() {
1613
1797
  ${IMPORT_CONTENT}
1614
1798
  ${BLOCK_END}`;
1615
1799
  }
1800
+ function findLineMarker(content2, marker, from = 0) {
1801
+ let idx = content2.indexOf(marker, from);
1802
+ while (idx !== -1) {
1803
+ const lineStart = idx === 0 || content2[idx - 1] === "\n";
1804
+ const after = idx + marker.length;
1805
+ const lineEnd = after === content2.length || content2[after] === "\n" || content2[after] === "\r";
1806
+ if (lineStart && lineEnd) return { start: idx, end: after };
1807
+ idx = content2.indexOf(marker, idx + marker.length);
1808
+ }
1809
+ return null;
1810
+ }
1811
+ function findImportBlockRange(content2) {
1812
+ const begin = findLineMarker(content2, BLOCK_BEGIN);
1813
+ if (!begin) return null;
1814
+ const end = findLineMarker(content2, BLOCK_END, begin.end);
1815
+ if (!end) return null;
1816
+ return { beginStart: begin.start, beginEnd: begin.end, endStart: end.start, endEnd: end.end };
1817
+ }
1616
1818
  function stripHausBlock(existing) {
1617
- const beginIdx = existing.indexOf(BLOCK_BEGIN);
1618
- if (beginIdx === -1) return existing;
1619
- const endIdx = existing.indexOf(BLOCK_END, beginIdx + BLOCK_BEGIN.length);
1620
- if (endIdx === -1) return existing;
1621
- const before = existing.slice(0, beginIdx);
1622
- const after = existing.slice(endIdx + BLOCK_END.length);
1819
+ const range = findImportBlockRange(existing);
1820
+ if (!range) return existing;
1821
+ const before = existing.slice(0, range.beginStart);
1822
+ const after = existing.slice(range.endEnd);
1623
1823
  const merged = `${before}${after}`.replace(/\n{3,}/g, "\n\n").trimEnd();
1624
1824
  return merged.length > 0 ? `${merged}
1625
1825
  ` : "";
1626
1826
  }
1627
1827
  function injectHausBlock(existing, block) {
1628
- const beginIdx = existing.indexOf(BLOCK_BEGIN);
1629
- const endIdx = beginIdx === -1 ? -1 : existing.indexOf(BLOCK_END, beginIdx + BLOCK_BEGIN.length);
1630
- if (beginIdx !== -1 && endIdx !== -1) {
1631
- const before = existing.slice(0, beginIdx);
1632
- const after = existing.slice(endIdx + BLOCK_END.length);
1828
+ const range = findImportBlockRange(existing);
1829
+ if (range) {
1830
+ const before = existing.slice(0, range.beginStart);
1831
+ const after = existing.slice(range.endEnd);
1633
1832
  return `${before}${block}${after}`;
1634
1833
  }
1834
+ const loneBegin = findLineMarker(existing, BLOCK_BEGIN);
1835
+ if (loneBegin) {
1836
+ const before = existing.slice(0, loneBegin.start).trimEnd();
1837
+ if (before.length === 0) return `${block}
1838
+ `;
1839
+ return `${before}
1840
+
1841
+ ${block}
1842
+ `;
1843
+ }
1635
1844
  const trimmed = existing.trimEnd();
1636
1845
  if (trimmed.length === 0) {
1637
1846
  return `${block}
@@ -1652,11 +1861,11 @@ async function writeRootClaudeMd(root, dryRun) {
1652
1861
  }
1653
1862
 
1654
1863
  // src/claude/write-workflow-config.ts
1655
- import path11 from "path";
1864
+ import path11 from "node:path";
1656
1865
  import fs10 from "fs-extra";
1657
1866
 
1658
1867
  // src/claude/derive-workflow-config.ts
1659
- import path10 from "path";
1868
+ import path10 from "node:path";
1660
1869
  import fs9 from "fs-extra";
1661
1870
  function binCmd(pm, bin, args) {
1662
1871
  const tail = args ? ` ${args}` : "";
@@ -1816,6 +2025,12 @@ ${templateContent}`;
1816
2025
  return null;
1817
2026
  }
1818
2027
  const existingContent = existing.slice(firstLine.length + 1);
2028
+ if (!parsed.hash && !force) {
2029
+ warn(
2030
+ `${printable}: managed header missing hash \u2014 cannot verify tamper status, skipping. Use --force to overwrite.`
2031
+ );
2032
+ return null;
2033
+ }
1819
2034
  if (parsed.hash && hashText(normaliseLF(existingContent)) !== parsed.hash && !force) {
1820
2035
  warn(`${printable}: content modified by user \u2014 skipping. Use --force to overwrite.`);
1821
2036
  return null;
@@ -1969,6 +2184,10 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1969
2184
  for (const item of catalogItems) {
1970
2185
  const manifestItem = manifestById.get(item.id);
1971
2186
  if (!manifestItem?.path) continue;
2187
+ if (manifestItem.reviewStatus === "deprecated") {
2188
+ warn(`Skipping ${item.id}: reviewStatus is deprecated`);
2189
+ continue;
2190
+ }
1972
2191
  if (manifestItem.source === "curated") {
1973
2192
  if (manifestItem.reviewStatus !== "approved") {
1974
2193
  curatedReviewStatusSkips++;
@@ -2174,18 +2393,6 @@ async function runApply(options) {
2174
2393
  files.forEach((f) => log(`- ${displayPath(root, f)}`));
2175
2394
  }
2176
2395
  }
2177
- async function isHausProject(root) {
2178
- if (await fs13.pathExists(hausPath(root, "recommendation.json"))) return true;
2179
- if (await fs13.pathExists(claudePath(root, "settings.json"))) {
2180
- const settings = await readProjectSettings(root);
2181
- if (settings._haus != null) return true;
2182
- }
2183
- return false;
2184
- }
2185
- async function refreshProjectApply(root) {
2186
- if (!await isHausProject(root)) return [];
2187
- return writeClaudeFiles(root, false, void 0, { refillConfig: false });
2188
- }
2189
2396
 
2190
2397
  // src/commands/catalog-audit.ts
2191
2398
  function auditForbiddenTags(items) {
@@ -2213,8 +2420,8 @@ async function runCatalogAudit() {
2213
2420
  }
2214
2421
 
2215
2422
  // src/commands/clone.ts
2216
- import { existsSync as existsSync2 } from "fs";
2217
- import path14 from "path";
2423
+ import { existsSync as existsSync2 } from "node:fs";
2424
+ import path14 from "node:path";
2218
2425
 
2219
2426
  // src/utils/exec.ts
2220
2427
  import { execa } from "execa";
@@ -2286,11 +2493,11 @@ async function runClone(url, opts = {}) {
2286
2493
  }
2287
2494
 
2288
2495
  // src/commands/doctor.ts
2289
- import path19 from "path";
2290
- import fs15 from "fs-extra";
2496
+ import path19 from "node:path";
2497
+ import fs14 from "fs-extra";
2291
2498
 
2292
2499
  // src/scanner/scan-project.ts
2293
- import path18 from "path";
2500
+ import path18 from "node:path";
2294
2501
 
2295
2502
  // src/utils/audit-checks.ts
2296
2503
  function isRecord(v) {
@@ -2317,8 +2524,8 @@ function compareVersions(a, b) {
2317
2524
  }
2318
2525
 
2319
2526
  // src/scanner/detect-package-manager.ts
2320
- import path15 from "path";
2321
- import fs14 from "fs-extra";
2527
+ import path15 from "node:path";
2528
+ import fs13 from "fs-extra";
2322
2529
  function detectPackageManager(root, packageManagerField) {
2323
2530
  const field = String(packageManagerField ?? "").trim();
2324
2531
  if (field.startsWith("yarn@")) {
@@ -2336,9 +2543,9 @@ function detectPackageManager(root, packageManagerField) {
2336
2543
  if (satisfiesVersion(version, ">=9")) return "npm";
2337
2544
  return "unknown";
2338
2545
  }
2339
- if (fs14.existsSync(path15.join(root, "yarn.lock"))) return "yarn";
2340
- if (fs14.existsSync(path15.join(root, "pnpm-lock.yaml"))) return "pnpm";
2341
- if (fs14.existsSync(path15.join(root, "package-lock.json"))) return "npm";
2546
+ if (fs13.existsSync(path15.join(root, "yarn.lock"))) return "yarn";
2547
+ if (fs13.existsSync(path15.join(root, "pnpm-lock.yaml"))) return "pnpm";
2548
+ if (fs13.existsSync(path15.join(root, "package-lock.json"))) return "npm";
2342
2549
  return "unknown";
2343
2550
  }
2344
2551
 
@@ -2540,7 +2747,7 @@ function runDetection(ctx, rules = STACK_RULES) {
2540
2747
  }
2541
2748
 
2542
2749
  // src/scanner/detection.ts
2543
- import path16 from "path";
2750
+ import path16 from "node:path";
2544
2751
  var UNSUPPORTED_MARKERS = {
2545
2752
  "requirements.txt": "python",
2546
2753
  "pyproject.toml": "python",
@@ -2600,8 +2807,8 @@ function collectUnsupportedSignals(files) {
2600
2807
  }
2601
2808
 
2602
2809
  // src/scanner/render.ts
2603
- import { readFile } from "fs/promises";
2604
- import path17 from "path";
2810
+ import { readFile } from "node:fs/promises";
2811
+ import path17 from "node:path";
2605
2812
  async function buildContentBlob(root, files) {
2606
2813
  const candidates = files.filter(
2607
2814
  (f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
@@ -2851,7 +3058,7 @@ async function runDoctor(options) {
2851
3058
  const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
2852
3059
  const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
2853
3060
  for (const target of importTargets) {
2854
- if (!await fs15.pathExists(hausPath(root, target))) {
3061
+ if (!await fs14.pathExists(hausPath(root, target))) {
2855
3062
  flag(
2856
3063
  `- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
2857
3064
  `A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
@@ -2862,7 +3069,7 @@ async function runDoctor(options) {
2862
3069
  }
2863
3070
  }
2864
3071
  const workflowPath = hausPath(root, "WORKFLOW.md");
2865
- const workflowExists = await fs15.pathExists(workflowPath);
3072
+ const workflowExists = await fs14.pathExists(workflowPath);
2866
3073
  if (!workflowExists) {
2867
3074
  flag(
2868
3075
  "- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
@@ -2893,7 +3100,7 @@ async function runDoctor(options) {
2893
3100
  "templates",
2894
3101
  "agentic-workflow-standard.md"
2895
3102
  );
2896
- const templatePath = await fs15.pathExists(cachePath) ? cachePath : bundledPath;
3103
+ const templatePath = await fs14.pathExists(cachePath) ? cachePath : bundledPath;
2897
3104
  const templateContent = await readText(templatePath);
2898
3105
  if (storedHashMatch && templateContent) {
2899
3106
  const currentHash = hashText(normaliseLF(templateContent));
@@ -2913,7 +3120,7 @@ async function runDoctor(options) {
2913
3120
  }
2914
3121
  }
2915
3122
  const workflowConfigPath = hausPath(root, "workflow-config.md");
2916
- const workflowConfigExists = await fs15.pathExists(workflowConfigPath);
3123
+ const workflowConfigExists = await fs14.pathExists(workflowConfigPath);
2917
3124
  if (!workflowConfigExists) {
2918
3125
  flag(
2919
3126
  "- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
@@ -2921,7 +3128,7 @@ async function runDoctor(options) {
2921
3128
  "haus apply --write"
2922
3129
  );
2923
3130
  } else {
2924
- const cfg = await fs15.readFile(workflowConfigPath, "utf8");
3131
+ const cfg = await fs14.readFile(workflowConfigPath, "utf8");
2925
3132
  const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
2926
3133
  if (unfilled > 0) {
2927
3134
  flag(
@@ -2961,9 +3168,6 @@ async function runDoctor(options) {
2961
3168
  `A newer haus (${npmStatus.latest}) is available`,
2962
3169
  `npm install -g ${NPM_PACKAGE_NAME}`
2963
3170
  );
2964
- if (!process.env["HAUS_FIXTURE_CATALOG"]) {
2965
- process.exitCode = 1;
2966
- }
2967
3171
  } else if (npmStatus.latest !== null) {
2968
3172
  ok(`- CLI: ${currentVersion} (up to date)`);
2969
3173
  } else {
@@ -3107,18 +3311,26 @@ async function runExplainRecommendation(options) {
3107
3311
  }
3108
3312
 
3109
3313
  // src/commands/guard.ts
3110
- import { readFileSync as readFileSync2 } from "fs";
3314
+ import { readFileSync as readFileSync2 } from "node:fs";
3111
3315
 
3112
3316
  // src/security/guard-bash.ts
3317
+ function matchesDenyToken(command, denyPhrase) {
3318
+ if (denyPhrase === "sudo") {
3319
+ return /(?:^|[|;&]\s*)sudo\b/i.test(command);
3320
+ }
3321
+ const normalizedCommand = command.toLowerCase().replace(/\s+/g, " ").trim();
3322
+ return normalizedCommand.includes(denyPhrase.toLowerCase());
3323
+ }
3113
3324
  function guardBash(command) {
3114
- const matched = DENY_COMMANDS.find((token) => command.includes(token));
3325
+ const matched = DENY_COMMANDS.find((denyPhrase) => matchesDenyToken(command, denyPhrase));
3115
3326
  if (matched) return `I didn't run that \u2014 it can permanently change or delete things: ${command}`;
3116
3327
  return void 0;
3117
3328
  }
3118
3329
 
3119
3330
  // src/security/guard-file-access.ts
3120
3331
  function guardFileAccess(candidate) {
3121
- const matched = DENY_PATHS.find((token) => candidate.includes(token.replace(/\*/g, "")));
3332
+ const normalized = normalizePathForMatch(candidate);
3333
+ const matched = DENY_PATH_REGEXES.find((re) => re.test(normalized));
3122
3334
  if (matched)
3123
3335
  return `I didn't open ${candidate} \u2014 it looks like it holds secrets or sensitive data`;
3124
3336
  return void 0;
@@ -3169,12 +3381,12 @@ async function runGuard(kind, _options) {
3169
3381
  }
3170
3382
 
3171
3383
  // src/commands/init.ts
3172
- import path20 from "path";
3173
- import fs16 from "fs-extra";
3384
+ import path20 from "node:path";
3385
+ import fs15 from "fs-extra";
3174
3386
 
3175
3387
  // src/utils/prompts.ts
3176
- import { stdin as input, stdout as output } from "process";
3177
- import readline from "readline/promises";
3388
+ import { stdin as input, stdout as output } from "node:process";
3389
+ import readline from "node:readline/promises";
3178
3390
  async function ask(question) {
3179
3391
  const rl = readline.createInterface({ input, output });
3180
3392
  try {
@@ -3291,6 +3503,10 @@ async function recommend(root, context) {
3291
3503
  skip(item.id, "unsupported-policy", "Unsupported stack policy");
3292
3504
  continue;
3293
3505
  }
3506
+ if (item.reviewStatus === "deprecated") {
3507
+ skip(item.id, "deprecated", "Catalog item is deprecated", "reviewStatus:deprecated");
3508
+ continue;
3509
+ }
3294
3510
  if (item.source === "curated") {
3295
3511
  const rs = item.reviewStatus;
3296
3512
  if (!rs || rs !== "approved") {
@@ -3518,7 +3734,7 @@ async function runSetupProject(options) {
3518
3734
  async function runInit(options) {
3519
3735
  const root = process.cwd();
3520
3736
  const hausDir = path20.join(root, ".haus-workflow");
3521
- const alreadyInit = await fs16.pathExists(hausDir);
3737
+ const alreadyInit = await fs15.pathExists(hausDir);
3522
3738
  if (alreadyInit) {
3523
3739
  log("Haus AI already initialized in this project.");
3524
3740
  log("Run `haus setup-project` to reconfigure.");
@@ -3529,9 +3745,9 @@ async function runInit(options) {
3529
3745
  }
3530
3746
 
3531
3747
  // src/install/apply.ts
3532
- import crypto2 from "crypto";
3533
- import path21 from "path";
3534
- import fs17 from "fs-extra";
3748
+ import crypto2 from "node:crypto";
3749
+ import path21 from "node:path";
3750
+ import fs16 from "fs-extra";
3535
3751
 
3536
3752
  // src/install/header.ts
3537
3753
  var MD_PREFIX = "<!-- HAUS-MANAGED";
@@ -3539,11 +3755,9 @@ var MD_SUFFIX = " -->";
3539
3755
  var FM_FENCE = "---";
3540
3756
  var FM_KEY = "haus_managed";
3541
3757
  function parseAttrs(raw) {
3542
- const idMatch = /\bid=(\S+)/.exec(raw);
3543
- const vMatch = /\bv=(\S+)/.exec(raw);
3544
- const srcMatch = /\bsource=(\S+)/.exec(raw);
3545
- if (!idMatch || !vMatch || !srcMatch) return void 0;
3546
- return { stableId: idMatch[1], schemaVersion: vMatch[1], source: srcMatch[1] };
3758
+ const attrs = parseHausManagedAttrs(raw) ?? parseHausManagedAttrs(`<!-- HAUS-MANAGED ${raw.trim()} -->`);
3759
+ if (!attrs?.id || !attrs.v || !attrs.source) return void 0;
3760
+ return { stableId: attrs.id, schemaVersion: attrs.v, source: attrs.source };
3547
3761
  }
3548
3762
  function isFence(line2) {
3549
3763
  return (line2 ?? "").replace(/\r$/, "") === FM_FENCE;
@@ -3620,7 +3834,7 @@ function hashContent(content2) {
3620
3834
  function sourceVersion() {
3621
3835
  try {
3622
3836
  const pkgPath = path21.join(packageRoot(), "package.json");
3623
- const pkg = JSON.parse(fs17.readFileSync(pkgPath, "utf8"));
3837
+ const pkg = JSON.parse(fs16.readFileSync(pkgPath, "utf8"));
3624
3838
  return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
3625
3839
  } catch {
3626
3840
  return "haus@0.0.0";
@@ -3632,10 +3846,10 @@ function globalSrcDir() {
3632
3846
  function collectSourceFiles(srcDir, claudeDir) {
3633
3847
  const entries = [];
3634
3848
  const skillsDir = path21.join(srcDir, "skills");
3635
- if (fs17.pathExistsSync(skillsDir)) {
3636
- for (const skillName of fs17.readdirSync(skillsDir)) {
3849
+ if (fs16.pathExistsSync(skillsDir)) {
3850
+ for (const skillName of fs16.readdirSync(skillsDir)) {
3637
3851
  const skillFile = path21.join(skillsDir, skillName, "SKILL.md");
3638
- if (fs17.pathExistsSync(skillFile)) {
3852
+ if (fs16.pathExistsSync(skillFile)) {
3639
3853
  entries.push({
3640
3854
  stableId: `skill.${skillName}`,
3641
3855
  srcRelPath: path21.join("library", "global", "skills", skillName, "SKILL.md"),
@@ -3645,8 +3859,8 @@ function collectSourceFiles(srcDir, claudeDir) {
3645
3859
  }
3646
3860
  }
3647
3861
  const commandsDir = path21.join(srcDir, "commands");
3648
- if (fs17.pathExistsSync(commandsDir)) {
3649
- for (const fileName of fs17.readdirSync(commandsDir)) {
3862
+ if (fs16.pathExistsSync(commandsDir)) {
3863
+ for (const fileName of fs16.readdirSync(commandsDir)) {
3650
3864
  if (!fileName.endsWith(".md")) continue;
3651
3865
  const commandName = fileName.slice(0, -".md".length);
3652
3866
  entries.push({
@@ -3696,7 +3910,7 @@ async function applyInstall(options = {}) {
3696
3910
  }
3697
3911
  continue;
3698
3912
  }
3699
- const destExists = fs17.pathExistsSync(entry.destPath);
3913
+ const destExists = fs16.pathExistsSync(entry.destPath);
3700
3914
  if (destExists) {
3701
3915
  const currentContent = await readText(entry.destPath);
3702
3916
  if (currentContent !== void 0) {
@@ -3744,13 +3958,13 @@ async function applyInstall(options = {}) {
3744
3958
  const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
3745
3959
  for (const entry of existingManifest.files) {
3746
3960
  if (currentDestPaths.has(entry.destPath)) continue;
3747
- if (!fs17.pathExistsSync(entry.destPath)) continue;
3961
+ if (!fs16.pathExistsSync(entry.destPath)) continue;
3748
3962
  const content2 = await readText(entry.destPath);
3749
3963
  if (!content2) continue;
3750
3964
  const hasHeader = parseMarkdownHeader(content2) !== void 0;
3751
3965
  const currentHash = hashContent(content2);
3752
3966
  if (hasHeader && currentHash === entry.hash) {
3753
- if (!dryRun) await fs17.remove(entry.destPath);
3967
+ if (!dryRun) await fs16.remove(entry.destPath);
3754
3968
  result.deleted.push(entry.destPath);
3755
3969
  } else {
3756
3970
  warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
@@ -3761,8 +3975,7 @@ async function applyInstall(options = {}) {
3761
3975
  if (!dryRun && !check) {
3762
3976
  await writeSettings(mergedSettings);
3763
3977
  const manifest = buildManifest(source, manifestFiles, [
3764
- ...existingManifest?.hooks ?? [],
3765
- ...addedIds
3978
+ .../* @__PURE__ */ new Set([...existingManifest?.hooks ?? [], ...addedIds])
3766
3979
  ]);
3767
3980
  await writeManifest(manifest);
3768
3981
  }
@@ -3924,8 +4137,8 @@ async function runScan(options) {
3924
4137
  }
3925
4138
 
3926
4139
  // src/commands/undo.ts
3927
- import path22 from "path";
3928
- import fs18 from "fs-extra";
4140
+ import path22 from "node:path";
4141
+ import fs17 from "fs-extra";
3929
4142
 
3930
4143
  // src/claude/managed-paths.ts
3931
4144
  var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md", "commands/haus-doctor.md"];
@@ -3947,25 +4160,25 @@ async function collectManagedPaths(root) {
3947
4160
  }
3948
4161
  const existing = [];
3949
4162
  for (const abs of paths) {
3950
- if (await fs18.pathExists(abs)) existing.push(abs);
4163
+ if (await fs17.pathExists(abs)) existing.push(abs);
3951
4164
  }
3952
4165
  return existing;
3953
4166
  }
3954
4167
  async function settingsHasHausContent(root) {
3955
4168
  const settingsPath = claudePath(root, "settings.json");
3956
- if (!await fs18.pathExists(settingsPath)) return false;
4169
+ if (!await fs17.pathExists(settingsPath)) return false;
3957
4170
  const settings = await readProjectSettings(root);
3958
4171
  return settings._haus != null;
3959
4172
  }
3960
4173
  async function claudeMdHasHausBlock(root) {
3961
4174
  const filePath = path22.join(root, "CLAUDE.md");
3962
- if (!await fs18.pathExists(filePath)) return false;
3963
- const text = await fs18.readFile(filePath, "utf8");
4175
+ if (!await fs17.pathExists(filePath)) return false;
4176
+ const text = await fs17.readFile(filePath, "utf8");
3964
4177
  return text.includes(BLOCK_BEGIN);
3965
4178
  }
3966
4179
  async function stripProjectSettings(root) {
3967
4180
  const settingsPath = claudePath(root, "settings.json");
3968
- if (!await fs18.pathExists(settingsPath)) return false;
4181
+ if (!await fs17.pathExists(settingsPath)) return false;
3969
4182
  let settings = await readProjectSettings(root);
3970
4183
  settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
3971
4184
  const hasContent = Object.keys(settings).length > 0;
@@ -3974,29 +4187,42 @@ async function stripProjectSettings(root) {
3974
4187
  log(`Stripped haus rules from ${path22.relative(root, settingsPath)} (user settings preserved).`);
3975
4188
  return true;
3976
4189
  }
3977
- await fs18.remove(settingsPath);
4190
+ await fs17.remove(settingsPath);
3978
4191
  log(`Removed ${path22.relative(root, settingsPath)} (no user-owned settings remained).`);
3979
4192
  return true;
3980
4193
  }
3981
4194
  async function stripRootClaudeMd(root) {
3982
4195
  const filePath = path22.join(root, "CLAUDE.md");
3983
- if (!await fs18.pathExists(filePath)) return false;
3984
- const prev = await fs18.readFile(filePath, "utf8");
4196
+ if (!await fs17.pathExists(filePath)) return false;
4197
+ const prev = await fs17.readFile(filePath, "utf8");
3985
4198
  if (!prev.includes(BLOCK_BEGIN)) return false;
3986
4199
  const next = stripHausBlock(prev);
3987
4200
  if (next.length === 0) {
3988
- await fs18.remove(filePath);
4201
+ await fs17.remove(filePath);
3989
4202
  log("Removed CLAUDE.md (only contained haus import block).");
3990
4203
  } else {
3991
- await fs18.writeFile(filePath, next, "utf8");
4204
+ await fs17.writeFile(filePath, next, "utf8");
3992
4205
  log("Removed haus import block from CLAUDE.md (user content preserved).");
3993
4206
  }
3994
4207
  return true;
3995
4208
  }
3996
4209
  async function pruneDirIfEmpty(dir) {
3997
- if (!await fs18.pathExists(dir)) return;
3998
- const entries = await fs18.readdir(dir);
3999
- if (entries.length === 0) await fs18.remove(dir);
4210
+ if (!await fs17.pathExists(dir)) return;
4211
+ const entries = await fs17.readdir(dir);
4212
+ if (entries.length === 0) await fs17.remove(dir);
4213
+ }
4214
+ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
4215
+ if (managedAbsPaths.length === 0) return;
4216
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4217
+ const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
4218
+ for (const abs of managedAbsPaths) {
4219
+ if (!await fs17.pathExists(abs)) continue;
4220
+ const rel = path22.relative(root, abs);
4221
+ const backupPath = path22.join(backupRoot, rel);
4222
+ await fs17.ensureDir(path22.dirname(backupPath));
4223
+ await fs17.copy(abs, backupPath);
4224
+ }
4225
+ log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path22.relative(root, backupRoot)}.`);
4000
4226
  }
4001
4227
  async function runUndo(options) {
4002
4228
  const root = process.cwd();
@@ -4022,9 +4248,10 @@ User-owned .claude/ files will be preserved.`
4022
4248
  return;
4023
4249
  }
4024
4250
  }
4251
+ await backupManagedFilesBeforeUndo(root, managed);
4025
4252
  for (const abs of managed) {
4026
- if (!await fs18.pathExists(abs)) continue;
4027
- await fs18.remove(abs);
4253
+ if (!await fs17.pathExists(abs)) continue;
4254
+ await fs17.remove(abs);
4028
4255
  log(`Removed ${path22.relative(root, abs)}`);
4029
4256
  }
4030
4257
  if (stripSettings) await stripProjectSettings(root);
@@ -4035,9 +4262,9 @@ User-owned .claude/ files will be preserved.`
4035
4262
  }
4036
4263
 
4037
4264
  // src/install/uninstall.ts
4038
- import crypto3 from "crypto";
4039
- import path23 from "path";
4040
- import fs19 from "fs-extra";
4265
+ import crypto3 from "node:crypto";
4266
+ import path23 from "node:path";
4267
+ import fs18 from "fs-extra";
4041
4268
  async function runUninstall(options = {}) {
4042
4269
  const { force = false } = options;
4043
4270
  const manifest = await readManifest();
@@ -4047,7 +4274,7 @@ async function runUninstall(options = {}) {
4047
4274
  return result;
4048
4275
  }
4049
4276
  for (const entry of manifest.files) {
4050
- const exists = fs19.pathExistsSync(entry.destPath);
4277
+ const exists = fs18.pathExistsSync(entry.destPath);
4051
4278
  if (!exists) continue;
4052
4279
  const content2 = await readText(entry.destPath);
4053
4280
  if (content2 === void 0) continue;
@@ -4065,7 +4292,7 @@ async function runUninstall(options = {}) {
4065
4292
  result.skipped.push(entry.destPath);
4066
4293
  continue;
4067
4294
  }
4068
- await fs19.remove(entry.destPath);
4295
+ await fs18.remove(entry.destPath);
4069
4296
  await pruneEmptyDir(path23.dirname(entry.destPath));
4070
4297
  result.deleted.push(entry.destPath);
4071
4298
  }
@@ -4075,12 +4302,12 @@ async function runUninstall(options = {}) {
4075
4302
  result.hooksStripped = true;
4076
4303
  const hausDir = path23.join(globalClaudeDir(), "haus");
4077
4304
  const manifestPath2 = hausManifestPath();
4078
- if (fs19.pathExistsSync(manifestPath2)) {
4079
- await fs19.remove(manifestPath2);
4305
+ if (fs18.pathExistsSync(manifestPath2)) {
4306
+ await fs18.remove(manifestPath2);
4080
4307
  }
4081
- if (fs19.pathExistsSync(hausDir)) {
4082
- const remaining = await fs19.readdir(hausDir);
4083
- if (remaining.length === 0) await fs19.remove(hausDir);
4308
+ if (fs18.pathExistsSync(hausDir)) {
4309
+ const remaining = await fs18.readdir(hausDir);
4310
+ if (remaining.length === 0) await fs18.remove(hausDir);
4084
4311
  }
4085
4312
  return result;
4086
4313
  }
@@ -4111,7 +4338,20 @@ async function runUninstallCommand(options) {
4111
4338
  }
4112
4339
 
4113
4340
  // src/commands/update.ts
4114
- import path25 from "path";
4341
+ import path25 from "node:path";
4342
+
4343
+ // src/claude/refresh-project.ts
4344
+ import fs19 from "fs-extra";
4345
+ async function isHausProject(root) {
4346
+ if (await fs19.pathExists(hausPath(root, "recommendation.json"))) return true;
4347
+ const result = await readJsonDetailed(claudePath(root, "settings.json"));
4348
+ if (result.status === "ok") return result.value._haus != null;
4349
+ return false;
4350
+ }
4351
+ async function refreshProjectApply(root) {
4352
+ if (!await isHausProject(root)) return [];
4353
+ return writeClaudeFiles(root, false, void 0, { refillConfig: false });
4354
+ }
4115
4355
 
4116
4356
  // src/update/diff-generated-files.ts
4117
4357
  function diffGeneratedFiles() {
@@ -4137,8 +4377,8 @@ function summarizeLockDiff(before, after) {
4137
4377
  }
4138
4378
 
4139
4379
  // src/update/lockfile.ts
4140
- import { mkdir, readFile as readFile2, copyFile } from "fs/promises";
4141
- import path24 from "path";
4380
+ import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
4381
+ import path24 from "node:path";
4142
4382
  async function checkLock(root) {
4143
4383
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
4144
4384
  const hasValidVersions = lock.every(
@@ -4310,8 +4550,18 @@ async function detectGlobalInstallDrift() {
4310
4550
  }
4311
4551
 
4312
4552
  // src/commands/validate-catalog.ts
4313
- import fs20 from "fs";
4314
- import path26 from "path";
4553
+ import path27 from "node:path";
4554
+
4555
+ // src/catalog/validate-core.ts
4556
+ import fs20 from "node:fs";
4557
+ import path26 from "node:path";
4558
+ var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
4559
+ var MANIFEST_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
4560
+ function isSafeCatalogPath2(itemPath) {
4561
+ if (!itemPath || path26.isAbsolute(itemPath) || itemPath.includes("\\")) return false;
4562
+ const normalized = path26.normalize(itemPath);
4563
+ return !normalized.startsWith("..") && !normalized.includes("/..");
4564
+ }
4315
4565
  function auditForbiddenStacks(items) {
4316
4566
  const failures = [];
4317
4567
  for (const item of items) {
@@ -4323,25 +4573,33 @@ function auditForbiddenStacks(items) {
4323
4573
  }
4324
4574
  return failures;
4325
4575
  }
4326
- function auditManifestStructure(items) {
4576
+ function auditManifestStructure(manifestVersion, items) {
4327
4577
  const failures = [];
4328
4578
  const seenIds = /* @__PURE__ */ new Map();
4329
4579
  const seenPaths = /* @__PURE__ */ new Map();
4580
+ if (typeof manifestVersion !== "string" || !MANIFEST_SEMVER_RE.test(manifestVersion)) {
4581
+ failures.push(
4582
+ 'manifest.json: top-level "version" is not valid semver (expected X.Y.Z or X.Y.Z-pre)'
4583
+ );
4584
+ }
4330
4585
  for (let i = 0; i < items.length; i++) {
4331
4586
  const item = items[i];
4332
4587
  if (!item.id) {
4333
4588
  failures.push(`item[${i}]: missing id`);
4334
4589
  continue;
4335
4590
  }
4591
+ const provenanceError = validateCuratedProvenance(item);
4592
+ if (provenanceError) failures.push(provenanceError);
4593
+ const referencesError = validateReferences(item.id, item.references);
4594
+ if (referencesError) failures.push(referencesError);
4336
4595
  if (!item.type) {
4337
4596
  failures.push(`${item.id}: missing type`);
4338
4597
  continue;
4339
4598
  }
4340
- if (!item.source) {
4341
- failures.push(`${item.id}: missing source`);
4342
- }
4343
- if (!item.title) {
4344
- failures.push(`${item.id}: missing title`);
4599
+ if (!item.source) failures.push(`${item.id}: missing source`);
4600
+ if (!item.title) failures.push(`${item.id}: missing title`);
4601
+ if (item.version && !ITEM_SEMVER_RE.test(item.version)) {
4602
+ failures.push(`${item.id}: version "${item.version}" is not valid semver (expected X.Y.Z)`);
4345
4603
  }
4346
4604
  const prev = seenIds.get(item.id);
4347
4605
  if (prev !== void 0) {
@@ -4352,24 +4610,20 @@ function auditManifestStructure(items) {
4352
4610
  if (item.type === "skill" || item.type === "agent" || item.type === "template" || item.type === "command") {
4353
4611
  if (!item.path) {
4354
4612
  failures.push(`${item.id}: missing path`);
4613
+ } else if (!isSafeCatalogPath2(item.path)) {
4614
+ failures.push(`${item.id}: path "${item.path}" is not a safe relative path`);
4355
4615
  } else {
4356
4616
  const norm = item.path.replace(/\\/g, "/");
4357
4617
  const existing = seenPaths.get(norm);
4358
- if (existing) {
4359
- failures.push(`${item.id}: path "${norm}" already used by ${existing}`);
4360
- } else {
4361
- seenPaths.set(norm, item.id);
4362
- }
4618
+ if (existing) failures.push(`${item.id}: path "${norm}" already used by ${existing}`);
4619
+ else seenPaths.set(norm, item.id);
4363
4620
  }
4364
4621
  const isHaus = item.source === "haus";
4365
- const isCuratedApproved = item.source === "curated" && item.reviewStatus === "approved";
4366
- if (!isHaus && !isCuratedApproved) {
4367
- failures.push(`${item.id}: source must be "haus" or curated with reviewStatus "approved"`);
4368
- }
4369
- for (const ref of item.references ?? []) {
4370
- if (HTTP_URL_PATTERN.test(ref)) {
4371
- failures.push(`${item.id}: reference uses insecure http:// URL: ${ref}`);
4372
- }
4622
+ const isCuratedUsableState = item.source === "curated" && (item.reviewStatus === "approved" || item.reviewStatus === "deprecated");
4623
+ if (!isHaus && !isCuratedUsableState) {
4624
+ failures.push(
4625
+ `${item.id}: source must be "haus" or curated with reviewStatus "approved"/"deprecated"`
4626
+ );
4373
4627
  }
4374
4628
  }
4375
4629
  }
@@ -4378,16 +4632,33 @@ function auditManifestStructure(items) {
4378
4632
  function checkRequiredFrontmatter(text, label) {
4379
4633
  const failures = [];
4380
4634
  for (const key of REQUIRED_SKILL_FRONTMATTER) {
4381
- if (!extractFrontmatterValue(text, key)) {
4635
+ if (!extractFrontmatterValue(text, key))
4382
4636
  failures.push(`${label}: missing non-empty frontmatter '${key}:'`);
4637
+ }
4638
+ return failures;
4639
+ }
4640
+ function auditTemplateContent(manifestDir, absPath, itemId) {
4641
+ const rel = path26.relative(manifestDir, absPath);
4642
+ const text = fs20.readFileSync(absPath, "utf8");
4643
+ const failures = [];
4644
+ const lines = text.split(/\r?\n/);
4645
+ for (let i = 0; i < lines.length; i++) {
4646
+ const line2 = lines[i] ?? "";
4647
+ if (PLACEHOLDER_PATTERN.test(line2))
4648
+ failures.push(`${rel}:${i + 1}: TODO or placeholder in shipped content`);
4649
+ if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2)))
4650
+ failures.push(`${rel}:${i + 1}: risky install pattern`);
4651
+ if (ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
4652
+ failures.push(`${rel}:${i + 1}: disallowed npx (only npx tsx allowed)`);
4383
4653
  }
4384
4654
  }
4655
+ failures.push(...auditForbiddenTagsInText(text, `${itemId}: ${rel}`));
4385
4656
  return failures;
4386
4657
  }
4387
4658
  function auditShippedFiles(manifestDir, items) {
4388
4659
  const failures = [];
4389
4660
  for (const item of items) {
4390
- if (!item.path) continue;
4661
+ if (!item.path || !isSafeCatalogPath2(item.path)) continue;
4391
4662
  const absPath = path26.join(manifestDir, item.path);
4392
4663
  if (item.type === "skill") {
4393
4664
  const skillMd = path26.join(absPath, "SKILL.md");
@@ -4428,36 +4699,22 @@ function auditShippedFiles(manifestDir, items) {
4428
4699
  }
4429
4700
  return failures;
4430
4701
  }
4431
- function auditTemplateContent(manifestDir, absPath, itemId) {
4432
- const rel = path26.relative(manifestDir, absPath);
4433
- const text = fs20.readFileSync(absPath, "utf8");
4434
- const failures = [];
4435
- const lines = text.split(/\r?\n/);
4436
- for (let i = 0; i < lines.length; i++) {
4437
- const line2 = lines[i] ?? "";
4438
- if (PLACEHOLDER_PATTERN.test(line2)) {
4439
- failures.push(`${rel}:${i + 1}: TODO or placeholder in shipped content`);
4440
- }
4441
- if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2))) {
4442
- failures.push(`${rel}:${i + 1}: risky install pattern`);
4443
- }
4444
- if (ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
4445
- failures.push(`${rel}:${i + 1}: disallowed npx (only npx tsx allowed)`);
4446
- }
4447
- }
4448
- failures.push(...auditForbiddenTagsInText(text, `${itemId}: ${rel}`));
4449
- return failures;
4450
- }
4451
4702
  var DIR_ITEM_TYPE = {
4452
4703
  skills: "skill",
4453
4704
  agents: "agent",
4454
4705
  templates: "template",
4455
4706
  commands: "command"
4456
4707
  };
4708
+ function walkMd(dir, fn) {
4709
+ for (const entry of fs20.readdirSync(dir, { withFileTypes: true })) {
4710
+ const full = path26.join(dir, entry.name);
4711
+ if (entry.isDirectory()) walkMd(full, fn);
4712
+ else if (entry.name.endsWith(".md")) fn(full);
4713
+ }
4714
+ }
4457
4715
  function auditMarkdownContent(manifestDir) {
4458
4716
  const failures = [];
4459
- const dirs = ["skills", "agents", "templates", "commands"];
4460
- for (const dir of dirs) {
4717
+ for (const dir of ["skills", "agents", "templates", "commands"]) {
4461
4718
  const abs = path26.join(manifestDir, dir);
4462
4719
  if (!fs20.existsSync(abs)) continue;
4463
4720
  const checkNonTsxNpx = !NPX_TSX_ONLY_EXEMPT_TYPES.includes(DIR_ITEM_TYPE[dir] ?? "");
@@ -4467,65 +4724,55 @@ function auditMarkdownContent(manifestDir) {
4467
4724
  const lines = text.split(/\r?\n/);
4468
4725
  for (let i = 0; i < lines.length; i++) {
4469
4726
  const line2 = lines[i] ?? "";
4470
- if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2))) {
4727
+ if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2)))
4471
4728
  failures.push(`${rel}:${i + 1}: risky install pattern`);
4472
- }
4473
4729
  if (checkNonTsxNpx && ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
4474
4730
  failures.push(`${rel}:${i + 1}: disallowed npx (only npx tsx allowed)`);
4475
4731
  }
4476
4732
  }
4477
- if (!rel.includes("/references/")) {
4478
- failures.push(...auditForbiddenTagsInText(text, rel));
4479
- }
4733
+ if (!rel.includes("/references/")) failures.push(...auditForbiddenTagsInText(text, rel));
4480
4734
  });
4481
4735
  }
4482
4736
  return failures;
4483
4737
  }
4484
- function walkMd(dir, fn) {
4485
- for (const entry of fs20.readdirSync(dir, { withFileTypes: true })) {
4486
- const full = path26.join(dir, entry.name);
4487
- if (entry.isDirectory()) walkMd(full, fn);
4488
- else if (entry.name.endsWith(".md")) fn(full);
4489
- }
4738
+ function validateCatalogData(manifestDir, manifestVersion, items) {
4739
+ const failures = [
4740
+ ...auditManifestStructure(manifestVersion, items),
4741
+ ...auditForbiddenStacks(items),
4742
+ ...auditShippedFiles(manifestDir, items),
4743
+ ...auditMarkdownContent(manifestDir),
4744
+ ...auditDisallowedTags(items)
4745
+ ];
4746
+ return { ok: failures.length === 0, failures, items };
4490
4747
  }
4748
+
4749
+ // src/commands/validate-catalog.ts
4491
4750
  async function runValidateCatalog(manifestPath2) {
4492
4751
  if (!manifestPath2) {
4493
4752
  error("Usage: haus validate-catalog <path/to/manifest.json>");
4494
4753
  process.exitCode = 1;
4495
4754
  return;
4496
4755
  }
4497
- const abs = path26.resolve(process.cwd(), manifestPath2);
4498
- const manifestDir = path26.dirname(abs);
4756
+ const abs = path27.resolve(process.cwd(), manifestPath2);
4757
+ const manifestDir = path27.dirname(abs);
4499
4758
  const data = await readJson(abs);
4500
4759
  if (!data?.items) {
4501
4760
  error(`Could not read catalog manifest at ${abs}`);
4502
4761
  process.exitCode = 1;
4503
4762
  return;
4504
4763
  }
4505
- const items = data.items;
4506
- const structureFailures = auditManifestStructure(items);
4507
- const stackFailures = auditForbiddenStacks(items);
4508
- const fileFailures = auditShippedFiles(manifestDir, items);
4509
- const contentFailures = auditMarkdownContent(manifestDir);
4510
- const tagFailures = auditDisallowedTags(items);
4511
- const allFailures = [
4512
- ...structureFailures,
4513
- ...stackFailures,
4514
- ...fileFailures,
4515
- ...contentFailures,
4516
- ...tagFailures
4517
- ];
4518
- if (allFailures.length) {
4519
- allFailures.forEach((f) => error(f));
4764
+ const result = validateCatalogData(manifestDir, data.version, data.items);
4765
+ if (!result.ok) {
4766
+ result.failures.forEach((f) => error(f));
4520
4767
  process.exitCode = 1;
4521
4768
  return;
4522
4769
  }
4523
- log(`Catalog valid. ${items.length} items checked.`);
4770
+ log(`Catalog valid. ${result.items.length} items checked.`);
4524
4771
  }
4525
4772
 
4526
4773
  // src/commands/workspace.ts
4527
- import { existsSync as existsSync5, statSync as statSync2 } from "fs";
4528
- import path33 from "path";
4774
+ import { existsSync as existsSync5, statSync as statSync2 } from "node:fs";
4775
+ import path34 from "node:path";
4529
4776
 
4530
4777
  // src/commands/workspace/aggregate.ts
4531
4778
  async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
@@ -4579,7 +4826,7 @@ ${summaries.map(
4579
4826
  }
4580
4827
 
4581
4828
  // src/commands/workspace/config.ts
4582
- import path27 from "path";
4829
+ import path28 from "node:path";
4583
4830
  import YAML from "yaml";
4584
4831
  var WORKSPACE_FILE = "haus.workspace.yaml";
4585
4832
  function parseWorkspaceConfig(text) {
@@ -4602,11 +4849,11 @@ function parseWorkspaceConfig(text) {
4602
4849
  };
4603
4850
  }
4604
4851
  async function readWorkspaceConfig(workspaceRoot) {
4605
- return parseWorkspaceConfig(await readText(path27.join(workspaceRoot, WORKSPACE_FILE)));
4852
+ return parseWorkspaceConfig(await readText(path28.join(workspaceRoot, WORKSPACE_FILE)));
4606
4853
  }
4607
4854
 
4608
4855
  // src/commands/workspace/discover.ts
4609
- import path28 from "path";
4856
+ import path29 from "node:path";
4610
4857
  import fg4 from "fast-glob";
4611
4858
  import YAML2 from "yaml";
4612
4859
  var DEFAULT_MAX_DEPTH = 3;
@@ -4634,8 +4881,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
4634
4881
  const gitDirs = /* @__PURE__ */ new Set();
4635
4882
  const manifestDirs = /* @__PURE__ */ new Set();
4636
4883
  for (const match of matches) {
4637
- const base = path28.posix.basename(match);
4638
- const dir = path28.posix.dirname(match);
4884
+ const base = path29.posix.basename(match);
4885
+ const dir = path29.posix.dirname(match);
4639
4886
  const owner = dir === "." ? "." : dir;
4640
4887
  if (base === ".git") gitDirs.add(owner);
4641
4888
  else manifestDirs.add(owner);
@@ -4651,9 +4898,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
4651
4898
  }
4652
4899
  repoRoots.sort((a, b) => a.localeCompare(b));
4653
4900
  return mapWithConcurrency(repoRoots, async (relDir) => {
4654
- const absDir = path28.resolve(workspaceRoot, relDir);
4655
- const pkg = await readJson(path28.join(absDir, "package.json"));
4656
- const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path28.basename(relDir === "." ? workspaceRoot : absDir);
4901
+ const absDir = path29.resolve(workspaceRoot, relDir);
4902
+ const pkg = await readJson(path29.join(absDir, "package.json"));
4903
+ const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path29.basename(relDir === "." ? workspaceRoot : absDir);
4657
4904
  let role = "auto";
4658
4905
  try {
4659
4906
  const scan = await scanProject(absDir);
@@ -4696,7 +4943,7 @@ function renderWorkspaceYaml(config) {
4696
4943
  });
4697
4944
  }
4698
4945
  async function runDiscover(workspaceRoot, opts = {}) {
4699
- const yamlPath = path28.join(workspaceRoot, "haus.workspace.yaml");
4946
+ const yamlPath = path29.join(workspaceRoot, "haus.workspace.yaml");
4700
4947
  const existingText = await readText(yamlPath);
4701
4948
  const existing = parseWorkspaceConfig(existingText);
4702
4949
  if (existingText && !existing) {
@@ -4729,19 +4976,19 @@ async function runDiscover(workspaceRoot, opts = {}) {
4729
4976
  }
4730
4977
 
4731
4978
  // src/commands/workspace/doctor.ts
4732
- import { existsSync as existsSync3 } from "fs";
4733
- import path30 from "path";
4979
+ import { existsSync as existsSync3 } from "node:fs";
4980
+ import path31 from "node:path";
4734
4981
 
4735
4982
  // src/commands/workspace/manifest.ts
4736
- import { readFileSync as readFileSync3 } from "fs";
4737
- import path29 from "path";
4983
+ import { readFileSync as readFileSync3 } from "node:fs";
4984
+ import path30 from "node:path";
4738
4985
  var MANIFEST_FILE = "workspace.manifest.json";
4739
4986
  function manifestPath(workspaceRoot) {
4740
4987
  return hausPath(workspaceRoot, MANIFEST_FILE);
4741
4988
  }
4742
4989
  function hausVersion() {
4743
4990
  try {
4744
- const pkg = JSON.parse(readFileSync3(path29.join(packageRoot(), "package.json"), "utf8"));
4991
+ const pkg = JSON.parse(readFileSync3(path30.join(packageRoot(), "package.json"), "utf8"));
4745
4992
  return pkg.version ?? "0.0.0";
4746
4993
  } catch {
4747
4994
  return "0.0.0";
@@ -4807,7 +5054,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
4807
5054
  }
4808
5055
  const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
4809
5056
  for (const repo of config.repos) {
4810
- const repoRoot = path30.resolve(workspaceRoot, repo.path);
5057
+ const repoRoot = path31.resolve(workspaceRoot, repo.path);
4811
5058
  const entry = manifestByName.get(repo.name);
4812
5059
  if (!entry) {
4813
5060
  flag({
@@ -4880,11 +5127,11 @@ function emit(args) {
4880
5127
  }
4881
5128
 
4882
5129
  // src/commands/workspace/setup.ts
4883
- import { existsSync as existsSync4, statSync } from "fs";
4884
- import path32 from "path";
5130
+ import { existsSync as existsSync4, statSync } from "node:fs";
5131
+ import path33 from "node:path";
4885
5132
 
4886
5133
  // src/claude/write-workspace-claude-md.ts
4887
- import path31 from "path";
5134
+ import path32 from "node:path";
4888
5135
  import fs21 from "fs-extra";
4889
5136
  function buildWorkspaceImportBlock(client, members) {
4890
5137
  const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
@@ -4903,7 +5150,7 @@ ${BLOCK_END}`;
4903
5150
  async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
4904
5151
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
4905
5152
  const dryRun = opts.dryRun ?? false;
4906
- const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path31.join(workspaceRoot, "CLAUDE.md");
5153
+ const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path32.join(workspaceRoot, "CLAUDE.md");
4907
5154
  const prev = await fs21.pathExists(filePath) ? await fs21.readFile(filePath, "utf8") : "";
4908
5155
  const next = opts.collision ? `${block}
4909
5156
  ` : injectHausBlock(prev, block);
@@ -4929,20 +5176,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
4929
5176
 
4930
5177
  // src/commands/workspace/setup.ts
4931
5178
  function resolveWorkspaceRoot(start = process.cwd()) {
4932
- let dir = path32.resolve(start);
5179
+ let dir = path33.resolve(start);
4933
5180
  for (; ; ) {
4934
- if (existsSync4(path32.join(dir, WORKSPACE_FILE))) return dir;
4935
- const parent = path32.dirname(dir);
4936
- if (parent === dir) return path32.resolve(start);
5181
+ if (existsSync4(path33.join(dir, WORKSPACE_FILE))) return dir;
5182
+ const parent = path33.dirname(dir);
5183
+ if (parent === dir) return path33.resolve(start);
4937
5184
  dir = parent;
4938
5185
  }
4939
5186
  }
4940
5187
  function isRootRepo(workspaceRoot, repoPath) {
4941
- return path32.resolve(workspaceRoot, repoPath) === path32.resolve(workspaceRoot);
5188
+ return path33.resolve(workspaceRoot, repoPath) === path33.resolve(workspaceRoot);
4942
5189
  }
4943
5190
  async function runWorkspaceSetup(workspaceRoot, options = {}) {
4944
5191
  const apply = options.write ?? false;
4945
- const configText = await readText(path32.join(workspaceRoot, WORKSPACE_FILE));
5192
+ const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
4946
5193
  if (!configText) {
4947
5194
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
4948
5195
  process.exitCode = 1;
@@ -4959,7 +5206,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
4959
5206
  const statuses = [];
4960
5207
  const aggregateInputs = [];
4961
5208
  for (const repo of repos) {
4962
- const repoRoot = path32.resolve(workspaceRoot, repo.path);
5209
+ const repoRoot = path33.resolve(workspaceRoot, repo.path);
4963
5210
  log(`
4964
5211
  \u2192 ${repo.name} (${repo.path})`);
4965
5212
  try {
@@ -5027,7 +5274,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
5027
5274
  const status = statusByName.get(repo.name);
5028
5275
  const role = repo.role ?? status?.roles?.[0] ?? "auto";
5029
5276
  if (status?.status === "ok") {
5030
- const lock = await checkLock(path32.resolve(workspaceRoot, repo.path));
5277
+ const lock = await checkLock(path33.resolve(workspaceRoot, repo.path));
5031
5278
  manifestRepos.push({
5032
5279
  name: repo.name,
5033
5280
  path: repo.path,
@@ -5107,7 +5354,7 @@ relationships: []
5107
5354
  log("Workspace initialized.");
5108
5355
  }
5109
5356
  async function scanWorkspace(workspaceRoot, opts) {
5110
- const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
5357
+ const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
5111
5358
  if (!configText) {
5112
5359
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
5113
5360
  process.exitCode = 1;
@@ -5128,7 +5375,7 @@ async function scanWorkspace(workspaceRoot, opts) {
5128
5375
  }
5129
5376
  const inputs = [];
5130
5377
  for (const repo of config.repos) {
5131
- const repoRoot = path33.resolve(workspaceRoot, repo.path);
5378
+ const repoRoot = path34.resolve(workspaceRoot, repo.path);
5132
5379
  if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
5133
5380
  throw new Error(`Repo path is not a directory: ${repo.path}`);
5134
5381
  }
@@ -5178,7 +5425,7 @@ async function runWorkspace(action, options = {}) {
5178
5425
  // src/cli.ts
5179
5426
  function cliVersion() {
5180
5427
  try {
5181
- const pkgPath = path34.join(packageRoot(), "package.json");
5428
+ const pkgPath = path35.join(packageRoot(), "package.json");
5182
5429
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5183
5430
  return pkg.version ?? "0.0.0";
5184
5431
  } catch {
@@ -5188,7 +5435,7 @@ function cliVersion() {
5188
5435
  var program = new Command();
5189
5436
  function validateRuntimeNodeVersion() {
5190
5437
  try {
5191
- const pkgPath = path34.join(packageRoot(), "package.json");
5438
+ const pkgPath = path35.join(packageRoot(), "package.json");
5192
5439
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5193
5440
  const requiredRange = pkg.engines?.node;
5194
5441
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {