@haus-tech/haus-workflow 0.27.0 → 0.27.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 +6 -0
- package/README.md +3 -3
- package/dist/cli.js +666 -476
- package/library/catalog/manifest.json +2 -1
- package/package.json +1 -2
package/dist/cli.js
CHANGED
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { readFileSync as readFileSync4 } from "fs";
|
|
5
|
-
import
|
|
5
|
+
import path35 from "path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/commands/apply.ts
|
|
9
9
|
import path13 from "path";
|
|
10
10
|
import checkbox from "@inquirer/checkbox";
|
|
11
|
-
import fs13 from "fs-extra";
|
|
12
11
|
|
|
13
12
|
// src/catalog/remote-catalog.ts
|
|
14
13
|
import os from "os";
|
|
@@ -20,17 +19,44 @@ import crypto from "crypto";
|
|
|
20
19
|
import path from "path";
|
|
21
20
|
import fg from "fast-glob";
|
|
22
21
|
import fs from "fs-extra";
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
32
|
-
|
|
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
|
|
51
|
-
|
|
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/
|
|
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;
|
|
@@ -342,6 +377,21 @@ var POLLUTION_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constru
|
|
|
342
377
|
function isNonEmptyString(value) {
|
|
343
378
|
return typeof value === "string" && value.length > 0;
|
|
344
379
|
}
|
|
380
|
+
function isStringArray(value) {
|
|
381
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
382
|
+
}
|
|
383
|
+
var REQUIRES_ANY_KEYS = ["stack", "dependency", "packageNamePattern", "role"];
|
|
384
|
+
function isRequiresAnyClause(value) {
|
|
385
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
386
|
+
const keys = Object.keys(value);
|
|
387
|
+
if (keys.length !== 1) return false;
|
|
388
|
+
const key = keys[0];
|
|
389
|
+
if (!REQUIRES_ANY_KEYS.includes(key)) return false;
|
|
390
|
+
return typeof value[key] === "string";
|
|
391
|
+
}
|
|
392
|
+
function isRequiresAnyArray(value) {
|
|
393
|
+
return Array.isArray(value) && value.every(isRequiresAnyClause);
|
|
394
|
+
}
|
|
345
395
|
function safeParse(json) {
|
|
346
396
|
return JSON.parse(json, (key, value) => {
|
|
347
397
|
if (POLLUTION_KEYS.has(key)) return void 0;
|
|
@@ -400,6 +450,18 @@ function parseManifest(json) {
|
|
|
400
450
|
return { ok: false, error: `${item.id}: curated item missing riskLevel` };
|
|
401
451
|
}
|
|
402
452
|
}
|
|
453
|
+
if (!isStringArray(item.tags)) {
|
|
454
|
+
return { ok: false, error: `${item.id}: tags must be a string array` };
|
|
455
|
+
}
|
|
456
|
+
if (!isStringArray(item.repoRoles)) {
|
|
457
|
+
return { ok: false, error: `${item.id}: repoRoles must be a string array` };
|
|
458
|
+
}
|
|
459
|
+
if (item.requiresAny !== void 0 && !isRequiresAnyArray(item.requiresAny)) {
|
|
460
|
+
return { ok: false, error: `${item.id}: requiresAny must be an array of clause objects` };
|
|
461
|
+
}
|
|
462
|
+
if (typeof item.tokenEstimate !== "number" || !Number.isFinite(item.tokenEstimate)) {
|
|
463
|
+
return { ok: false, error: `${item.id}: tokenEstimate must be a finite number` };
|
|
464
|
+
}
|
|
403
465
|
items.push(raw);
|
|
404
466
|
}
|
|
405
467
|
return { ok: true, manifest: { version: root.version, items } };
|
|
@@ -818,6 +880,17 @@ async function syncRemoteCatalog() {
|
|
|
818
880
|
return { newItems, refreshed, unchanged, failed };
|
|
819
881
|
}
|
|
820
882
|
var CATALOG_TAGS_API_URL = "https://api.github.com/repos/WeAreHausTech/haus-workflow-catalog/tags";
|
|
883
|
+
function parseSemverTag(tag) {
|
|
884
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(tag);
|
|
885
|
+
if (!match) return null;
|
|
886
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
887
|
+
}
|
|
888
|
+
function compareSemver(a, b) {
|
|
889
|
+
for (let i = 0; i < 3; i += 1) {
|
|
890
|
+
if (a[i] !== b[i]) return a[i] - b[i];
|
|
891
|
+
}
|
|
892
|
+
return 0;
|
|
893
|
+
}
|
|
821
894
|
async function fetchLatestCatalogTag() {
|
|
822
895
|
if (process.env["HAUS_CATALOG_REMOTE_BASE"]) return null;
|
|
823
896
|
try {
|
|
@@ -827,7 +900,16 @@ async function fetchLatestCatalogTag() {
|
|
|
827
900
|
});
|
|
828
901
|
if (!res.ok) return null;
|
|
829
902
|
const tags = await res.json();
|
|
830
|
-
|
|
903
|
+
const valid = tags.map((tag) => {
|
|
904
|
+
const name = typeof tag.name === "string" ? tag.name : "";
|
|
905
|
+
const semver2 = parseSemverTag(name);
|
|
906
|
+
return semver2 ? { name, semver: semver2 } : null;
|
|
907
|
+
}).filter(
|
|
908
|
+
(entry) => entry !== null
|
|
909
|
+
);
|
|
910
|
+
if (valid.length === 0) return null;
|
|
911
|
+
valid.sort((a, b) => compareSemver(b.semver, a.semver));
|
|
912
|
+
return valid[0].name;
|
|
831
913
|
} catch {
|
|
832
914
|
return null;
|
|
833
915
|
}
|
|
@@ -841,6 +923,226 @@ async function getCacheManifestAge() {
|
|
|
841
923
|
}
|
|
842
924
|
}
|
|
843
925
|
|
|
926
|
+
// src/claude/write-claude-files.ts
|
|
927
|
+
import path12 from "path";
|
|
928
|
+
import fs12 from "fs-extra";
|
|
929
|
+
|
|
930
|
+
// src/catalog/load-catalog.ts
|
|
931
|
+
import path4 from "path";
|
|
932
|
+
|
|
933
|
+
// src/utils/paths.ts
|
|
934
|
+
import { existsSync, readFileSync } from "fs";
|
|
935
|
+
import os2 from "os";
|
|
936
|
+
import path3 from "path";
|
|
937
|
+
import { fileURLToPath } from "url";
|
|
938
|
+
var HAUS_DIR = ".haus-workflow";
|
|
939
|
+
function hausPath(root, ...parts) {
|
|
940
|
+
return path3.join(root, HAUS_DIR, ...parts);
|
|
941
|
+
}
|
|
942
|
+
function claudePath(root, ...parts) {
|
|
943
|
+
return path3.join(root, ".claude", ...parts);
|
|
944
|
+
}
|
|
945
|
+
function displayPath(root, targetPath) {
|
|
946
|
+
const rel = path3.relative(root, targetPath).replace(/\\/g, "/");
|
|
947
|
+
if (rel && !rel.startsWith("../") && rel !== "..") {
|
|
948
|
+
return rel.startsWith("./") ? rel : `./${rel}`;
|
|
949
|
+
}
|
|
950
|
+
const home = os2.homedir();
|
|
951
|
+
const normalized = targetPath.replace(/\\/g, "/");
|
|
952
|
+
if (home && home.trim().length > 0) {
|
|
953
|
+
const homeRel = path3.relative(home, targetPath).replace(/\\/g, "/");
|
|
954
|
+
if (homeRel && !homeRel.startsWith("../") && homeRel !== "..") {
|
|
955
|
+
return `~/${homeRel}`;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return normalized;
|
|
959
|
+
}
|
|
960
|
+
function packageRoot() {
|
|
961
|
+
let dir = path3.dirname(fileURLToPath(import.meta.url));
|
|
962
|
+
for (let i = 0; i < 12; i++) {
|
|
963
|
+
const pkgPath = path3.join(dir, "package.json");
|
|
964
|
+
if (existsSync(pkgPath)) {
|
|
965
|
+
try {
|
|
966
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
967
|
+
if (pkg.name === "haus" || pkg.name === "@haus-tech/haus-workflow") return dir;
|
|
968
|
+
} catch {
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
const parent = path3.dirname(dir);
|
|
972
|
+
if (parent === dir) break;
|
|
973
|
+
dir = parent;
|
|
974
|
+
}
|
|
975
|
+
const file = fileURLToPath(import.meta.url);
|
|
976
|
+
return path3.resolve(path3.dirname(file), "../..");
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// src/catalog/load-catalog.ts
|
|
980
|
+
async function loadCatalogContext(root) {
|
|
981
|
+
const envPath = process.env["HAUS_FIXTURE_CATALOG"];
|
|
982
|
+
if (envPath) {
|
|
983
|
+
const data2 = await readJson(envPath);
|
|
984
|
+
return {
|
|
985
|
+
items: data2?.items ?? [],
|
|
986
|
+
contentRoot: path4.dirname(envPath),
|
|
987
|
+
source: "fixture"
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
const cacheDir = getCacheDir();
|
|
991
|
+
const cacheManifestPath = path4.join(cacheDir, "manifest.json");
|
|
992
|
+
const cacheData = await readJson(cacheManifestPath);
|
|
993
|
+
if (cacheData?.items?.length) {
|
|
994
|
+
return { items: cacheData.items, contentRoot: cacheDir, source: "cache" };
|
|
995
|
+
}
|
|
996
|
+
const localManifest = path4.join(root, "library/catalog/manifest.json");
|
|
997
|
+
const localData = await readJson(localManifest);
|
|
998
|
+
if (localData?.items?.length) {
|
|
999
|
+
return {
|
|
1000
|
+
items: localData.items,
|
|
1001
|
+
contentRoot: path4.dirname(localManifest),
|
|
1002
|
+
source: "local"
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
const packageManifest = path4.join(packageRoot(), "library/catalog/manifest.json");
|
|
1006
|
+
const data = await readJson(packageManifest);
|
|
1007
|
+
return {
|
|
1008
|
+
items: data?.items ?? [],
|
|
1009
|
+
contentRoot: path4.dirname(packageManifest),
|
|
1010
|
+
source: "bundled"
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
async function loadCatalog(root) {
|
|
1014
|
+
const ctx = await loadCatalogContext(root);
|
|
1015
|
+
return ctx.items;
|
|
1016
|
+
}
|
|
1017
|
+
function catalogItemContentPath(contentRoot, item) {
|
|
1018
|
+
return path4.join(contentRoot, item.path);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// src/update/hash-installed.ts
|
|
1022
|
+
import path5 from "path";
|
|
1023
|
+
import fg2 from "fast-glob";
|
|
1024
|
+
import fs3 from "fs-extra";
|
|
1025
|
+
|
|
1026
|
+
// src/claude/haus-managed-header.ts
|
|
1027
|
+
function parseHausManagedAttrs(line2) {
|
|
1028
|
+
const match = /^<!--\s+HAUS-MANAGED\b(.*?)-->$/.exec(line2.trim());
|
|
1029
|
+
if (!match) return null;
|
|
1030
|
+
const raw = match[1] ?? "";
|
|
1031
|
+
const pairs = [...raw.matchAll(/\b([a-z]+)=([^\s>]+)/g)];
|
|
1032
|
+
const attrs = {};
|
|
1033
|
+
for (const [, key, value] of pairs) attrs[key] = value;
|
|
1034
|
+
if (!attrs.id) return null;
|
|
1035
|
+
return {
|
|
1036
|
+
id: attrs.id,
|
|
1037
|
+
v: attrs.v,
|
|
1038
|
+
source: attrs.source,
|
|
1039
|
+
hash: attrs.hash
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// src/claude/managed-template.ts
|
|
1044
|
+
function normaliseLF(content2) {
|
|
1045
|
+
return content2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1046
|
+
}
|
|
1047
|
+
var SCHEMA_VERSION = 1;
|
|
1048
|
+
function parseHausManagedHeader(line2) {
|
|
1049
|
+
const attrs = parseHausManagedAttrs(line2);
|
|
1050
|
+
if (!attrs?.id) return null;
|
|
1051
|
+
const parsedV = attrs.v && /^\d+$/.test(attrs.v) ? Number(attrs.v) : void 0;
|
|
1052
|
+
const hash = attrs.hash && /^sha256-[a-f0-9]+$/.test(attrs.hash) ? attrs.hash : void 0;
|
|
1053
|
+
return {
|
|
1054
|
+
id: attrs.id,
|
|
1055
|
+
v: parsedV,
|
|
1056
|
+
source: attrs.source,
|
|
1057
|
+
hash
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// src/update/hash-installed.ts
|
|
1062
|
+
var EMPTY_LOCK_PATHS_TOKEN = "haus-lock:empty-paths";
|
|
1063
|
+
async function hashInstalledPaths(root, relPaths) {
|
|
1064
|
+
if (relPaths.length === 0) {
|
|
1065
|
+
return hashText(EMPTY_LOCK_PATHS_TOKEN);
|
|
1066
|
+
}
|
|
1067
|
+
const normalized = [...new Set(relPaths.map((p) => p.replace(/\\/g, "/")))].sort();
|
|
1068
|
+
const fileDigests = [];
|
|
1069
|
+
for (const rel of normalized) {
|
|
1070
|
+
const abs = path5.join(root, rel);
|
|
1071
|
+
if (!await fs3.pathExists(abs)) continue;
|
|
1072
|
+
const stat = await fs3.stat(abs);
|
|
1073
|
+
if (stat.isFile()) {
|
|
1074
|
+
const body = await fs3.readFile(abs, "utf8");
|
|
1075
|
+
fileDigests.push({ rel, digest: hashText(normaliseLF(body)) });
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
if (!stat.isDirectory()) continue;
|
|
1079
|
+
const inner = await fg2("**/*", { cwd: abs, onlyFiles: true, dot: true });
|
|
1080
|
+
for (const sub of inner.sort()) {
|
|
1081
|
+
const relFile = path5.join(rel, sub).replace(/\\/g, "/");
|
|
1082
|
+
const absFile = path5.join(abs, sub);
|
|
1083
|
+
const body = await fs3.readFile(absFile, "utf8");
|
|
1084
|
+
fileDigests.push({ rel: relFile, digest: hashText(normaliseLF(body)) });
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
if (fileDigests.length === 0) {
|
|
1088
|
+
return hashText(`${EMPTY_LOCK_PATHS_TOKEN}|${normalized.join("|")}`);
|
|
1089
|
+
}
|
|
1090
|
+
fileDigests.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
1091
|
+
return hashText(fileDigests.map((f) => `${f.rel}=${f.digest}`).join("|"));
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// src/claude/managed-write.ts
|
|
1095
|
+
import fs4 from "fs-extra";
|
|
1096
|
+
|
|
1097
|
+
// src/utils/diff.ts
|
|
1098
|
+
import { createTwoFilesPatch } from "diff";
|
|
1099
|
+
function hasTextChanged(before, after) {
|
|
1100
|
+
return before !== after;
|
|
1101
|
+
}
|
|
1102
|
+
function createUnifiedDiff(filePath, before, after) {
|
|
1103
|
+
return createTwoFilesPatch(filePath, filePath, before, after, "before", "after", {
|
|
1104
|
+
context: 3
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
function summarizeDiff(diffText) {
|
|
1108
|
+
const lines = diffText.split("\n");
|
|
1109
|
+
let additions = 0;
|
|
1110
|
+
let deletions = 0;
|
|
1111
|
+
for (const line2 of lines) {
|
|
1112
|
+
if (line2.startsWith("+++ ") || line2.startsWith("--- ")) continue;
|
|
1113
|
+
if (line2.startsWith("+")) additions += 1;
|
|
1114
|
+
if (line2.startsWith("-")) deletions += 1;
|
|
1115
|
+
}
|
|
1116
|
+
return { additions, deletions };
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/claude/managed-write.ts
|
|
1120
|
+
async function writeManagedText(root, filePath, nextText, dryRun) {
|
|
1121
|
+
const prev = await fs4.pathExists(filePath) ? await fs4.readFile(filePath, "utf8") : "";
|
|
1122
|
+
const printable = displayPath(root, filePath);
|
|
1123
|
+
if (dryRun) {
|
|
1124
|
+
if (!prev) {
|
|
1125
|
+
log(createUnifiedDiff(printable, "", nextText));
|
|
1126
|
+
} else if (hasTextChanged(prev, nextText)) {
|
|
1127
|
+
log(createUnifiedDiff(printable, prev, nextText));
|
|
1128
|
+
} else {
|
|
1129
|
+
log(`${printable}: unchanged`);
|
|
1130
|
+
}
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
if (hasTextChanged(prev, nextText) && prev.length > 0) {
|
|
1134
|
+
const diffText = createUnifiedDiff(printable, prev, nextText);
|
|
1135
|
+
const summary = summarizeDiff(diffText);
|
|
1136
|
+
log(`Overwriting ${printable} (diff +${summary.additions} -${summary.deletions})`);
|
|
1137
|
+
}
|
|
1138
|
+
await writeText(filePath, nextText);
|
|
1139
|
+
}
|
|
1140
|
+
async function writeManagedJson(root, filePath, value, dryRun) {
|
|
1141
|
+
const nextText = `${JSON.stringify(value, null, 2)}
|
|
1142
|
+
`;
|
|
1143
|
+
await writeManagedText(root, filePath, nextText, dryRun);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
844
1146
|
// src/install/allow-rules.ts
|
|
845
1147
|
var ALLOWED_SUBCOMMANDS = ["setup-project", "apply", "doctor", "scan", "recommend"];
|
|
846
1148
|
function buildAllowRules() {
|
|
@@ -848,18 +1150,18 @@ function buildAllowRules() {
|
|
|
848
1150
|
}
|
|
849
1151
|
|
|
850
1152
|
// src/install/settings-merge.ts
|
|
851
|
-
import
|
|
852
|
-
import
|
|
1153
|
+
import path7 from "path";
|
|
1154
|
+
import fs5 from "fs-extra";
|
|
853
1155
|
|
|
854
1156
|
// src/install/manifest.ts
|
|
855
|
-
import
|
|
856
|
-
import
|
|
1157
|
+
import os3 from "os";
|
|
1158
|
+
import path6 from "path";
|
|
857
1159
|
var MANIFEST_SCHEMA = "haus-install-manifest/1";
|
|
858
1160
|
function globalClaudeDir() {
|
|
859
|
-
return
|
|
1161
|
+
return path6.join(os3.homedir(), ".claude");
|
|
860
1162
|
}
|
|
861
1163
|
function hausManifestPath() {
|
|
862
|
-
return
|
|
1164
|
+
return path6.join(globalClaudeDir(), "haus", "install-manifest.json");
|
|
863
1165
|
}
|
|
864
1166
|
async function readManifest() {
|
|
865
1167
|
return readJson(hausManifestPath());
|
|
@@ -879,11 +1181,15 @@ function buildManifest(source, files, hooks) {
|
|
|
879
1181
|
|
|
880
1182
|
// src/install/settings-merge.ts
|
|
881
1183
|
function settingsJsonPath() {
|
|
882
|
-
return
|
|
1184
|
+
return path7.join(globalClaudeDir(), "settings.json");
|
|
883
1185
|
}
|
|
884
1186
|
async function readSettings() {
|
|
885
|
-
const
|
|
886
|
-
|
|
1187
|
+
const filePath = settingsJsonPath();
|
|
1188
|
+
const result = await readJsonDetailed(filePath);
|
|
1189
|
+
if (result.status === "ok") return result.value;
|
|
1190
|
+
if (result.status === "missing") return {};
|
|
1191
|
+
const backupPath = await backupMalformedJsonFile(filePath);
|
|
1192
|
+
throw new MalformedJsonFileError(filePath, backupPath);
|
|
887
1193
|
}
|
|
888
1194
|
async function writeSettings(settings) {
|
|
889
1195
|
await writeJson(settingsJsonPath(), settings);
|
|
@@ -897,6 +1203,14 @@ function collectEventHookCommands(entries) {
|
|
|
897
1203
|
}
|
|
898
1204
|
return cmds;
|
|
899
1205
|
}
|
|
1206
|
+
function pruneEntryCommands(entry, shouldDrop) {
|
|
1207
|
+
const keptHooks = (entry.hooks ?? []).filter((hook) => {
|
|
1208
|
+
const cmd = hook.command ?? "";
|
|
1209
|
+
return !cmd || !shouldDrop(cmd);
|
|
1210
|
+
});
|
|
1211
|
+
if (keptHooks.length === 0) return null;
|
|
1212
|
+
return { ...entry, hooks: keptHooks };
|
|
1213
|
+
}
|
|
900
1214
|
var RETIRED_HAUS_HOOK_COMMANDS = ["haus context --from-hook"];
|
|
901
1215
|
var RETIRED_HAUS_HOOK_IDS = ["hook.context", "haus.context-hook"];
|
|
902
1216
|
function reconcileRetiredHausHooks(updated, fragments, priorHaus, addedIds, addedCommands) {
|
|
@@ -910,10 +1224,7 @@ function reconcileRetiredHausHooks(updated, fragments, priorHaus, addedIds, adde
|
|
|
910
1224
|
}
|
|
911
1225
|
const prunedHooks = {};
|
|
912
1226
|
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
|
-
});
|
|
1227
|
+
const kept = entries.map((entry) => pruneEntryCommands(entry, (cmd) => retiredCommands.has(cmd))).filter((entry) => entry !== null);
|
|
917
1228
|
if (kept.length > 0) prunedHooks[event] = kept;
|
|
918
1229
|
}
|
|
919
1230
|
updated.hooks = prunedHooks;
|
|
@@ -1064,10 +1375,12 @@ function stripHausHooks(settings) {
|
|
|
1064
1375
|
const updated = { ...settings };
|
|
1065
1376
|
updated.hooks = {};
|
|
1066
1377
|
for (const [event, entries] of Object.entries(settings.hooks ?? {})) {
|
|
1067
|
-
const kept = entries.
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1378
|
+
const kept = entries.map(
|
|
1379
|
+
(entry) => pruneEntryCommands(
|
|
1380
|
+
entry,
|
|
1381
|
+
(cmd) => usePrefix ? cmd.startsWith("haus ") : ownedCommands.has(cmd)
|
|
1382
|
+
)
|
|
1383
|
+
).filter((entry) => entry !== null);
|
|
1071
1384
|
if (kept.length > 0) updated.hooks[event] = kept;
|
|
1072
1385
|
}
|
|
1073
1386
|
const { _haus: _, ...rest } = updated;
|
|
@@ -1113,7 +1426,7 @@ function stripHausAsk(settings) {
|
|
|
1113
1426
|
async function loadHooksFragment(fragmentPath) {
|
|
1114
1427
|
let raw;
|
|
1115
1428
|
try {
|
|
1116
|
-
raw = await
|
|
1429
|
+
raw = await fs5.readJson(fragmentPath);
|
|
1117
1430
|
} catch {
|
|
1118
1431
|
return [];
|
|
1119
1432
|
}
|
|
@@ -1126,6 +1439,8 @@ var DENY_COMMANDS = [
|
|
|
1126
1439
|
"sudo",
|
|
1127
1440
|
"chmod -R 777",
|
|
1128
1441
|
"git push --force",
|
|
1442
|
+
"git push --force-with-lease",
|
|
1443
|
+
"git push -f",
|
|
1129
1444
|
"drop database",
|
|
1130
1445
|
"truncate table",
|
|
1131
1446
|
"npm publish",
|
|
@@ -1153,6 +1468,20 @@ var DENY_PATHS = [
|
|
|
1153
1468
|
"certs"
|
|
1154
1469
|
];
|
|
1155
1470
|
var DENY_DIRS = /* @__PURE__ */ new Set(["customer-data", "secrets", "certs"]);
|
|
1471
|
+
var DENY_PATH_REGEXES = [
|
|
1472
|
+
/(^|\/)[^/]+\.pem$/i,
|
|
1473
|
+
/(^|\/)[^/]+\.key$/i,
|
|
1474
|
+
/(^|\/)[^/]+\.p12$/i,
|
|
1475
|
+
/(^|\/)[^/]+\.pfx$/i,
|
|
1476
|
+
/(^|\/)id_rsa$/i,
|
|
1477
|
+
/(^|\/)id_ed25519$/i,
|
|
1478
|
+
/(^|\/)customer-data(\/|$)/i,
|
|
1479
|
+
/(^|\/)secrets(\/|$)/i,
|
|
1480
|
+
/(^|\/)certs(\/|$)/i
|
|
1481
|
+
];
|
|
1482
|
+
function normalizePathForMatch(candidate) {
|
|
1483
|
+
return candidate.replace(/\\/g, "/").replace(/\/+/g, "/").trim();
|
|
1484
|
+
}
|
|
1156
1485
|
var ASK_PATHS = [
|
|
1157
1486
|
{ pattern: ".env", tools: ["Edit", "Write"] },
|
|
1158
1487
|
{ pattern: ".env.*", tools: ["Edit", "Write"] },
|
|
@@ -1179,286 +1508,88 @@ var SENSITIVE_PATH_REGEXES = [
|
|
|
1179
1508
|
/secrets/,
|
|
1180
1509
|
/(^|\/)storage\/logs(\/|$)/,
|
|
1181
1510
|
/(^|\/)wp-content\/uploads(\/|$)/,
|
|
1182
|
-
/(^|\/)uploads(\/|$)/
|
|
1183
|
-
];
|
|
1184
|
-
var SENSITIVE_ITEM_KEYWORDS = [
|
|
1185
|
-
".env",
|
|
1186
|
-
"secrets",
|
|
1187
|
-
"certs",
|
|
1188
|
-
"customer-data",
|
|
1189
|
-
"exports",
|
|
1190
|
-
".pem",
|
|
1191
|
-
".key"
|
|
1192
|
-
];
|
|
1193
|
-
|
|
1194
|
-
// src/security/ask-rules.ts
|
|
1195
|
-
function buildAskRules() {
|
|
1196
|
-
const rules = [];
|
|
1197
|
-
for (const command of ASK_COMMANDS) {
|
|
1198
|
-
rules.push(`Bash(${command}:*)`);
|
|
1199
|
-
}
|
|
1200
|
-
for (const { pattern, tools } of ASK_PATHS) {
|
|
1201
|
-
for (const tool of tools) {
|
|
1202
|
-
rules.push(`${tool}(${pattern})`);
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
return [...new Set(rules)];
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
// src/security/deny-rules.ts
|
|
1209
|
-
var FILE_TOOLS = ["Read", "Edit", "Write"];
|
|
1210
|
-
function buildDenyRules() {
|
|
1211
|
-
const rules = [];
|
|
1212
|
-
for (const command of DENY_COMMANDS) {
|
|
1213
|
-
rules.push(`Bash(${command}:*)`);
|
|
1214
|
-
}
|
|
1215
|
-
for (const path35 of DENY_PATHS) {
|
|
1216
|
-
const pattern = DENY_DIRS.has(path35) ? `${path35}/**` : path35;
|
|
1217
|
-
for (const tool of FILE_TOOLS) {
|
|
1218
|
-
rules.push(`${tool}(${pattern})`);
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
return [...new Set(rules)];
|
|
1222
|
-
}
|
|
1223
|
-
|
|
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
|
-
// src/claude/merge-project-settings.ts
|
|
1271
|
-
var PROJECT_HOOK_FRAGMENTS = [
|
|
1272
|
-
{
|
|
1273
|
-
id: "haus.guard-file",
|
|
1274
|
-
gate: "keep",
|
|
1275
|
-
event: "PreToolUse",
|
|
1276
|
-
matcher: "Read|Edit|Write",
|
|
1277
|
-
command: "haus guard file-access --from-hook"
|
|
1278
|
-
},
|
|
1279
|
-
{
|
|
1280
|
-
id: "haus.guard-bash",
|
|
1281
|
-
gate: "keep",
|
|
1282
|
-
event: "PreToolUse",
|
|
1283
|
-
matcher: "Bash",
|
|
1284
|
-
command: "haus guard bash --from-hook"
|
|
1285
|
-
}
|
|
1286
|
-
];
|
|
1287
|
-
async function readProjectSettings(root) {
|
|
1288
|
-
const parsed = await readJson(claudePath(root, "settings.json"));
|
|
1289
|
-
return parsed ?? {};
|
|
1290
|
-
}
|
|
1291
|
-
async function writeProjectSettings(root, settings) {
|
|
1292
|
-
await writeJson(claudePath(root, "settings.json"), settings);
|
|
1293
|
-
}
|
|
1294
|
-
async function mergeProjectSettings(root) {
|
|
1295
|
-
const base = await readProjectSettings(root);
|
|
1296
|
-
const { settings: withHooks } = mergeHooks(base, PROJECT_HOOK_FRAGMENTS);
|
|
1297
|
-
const { settings: withDeny } = mergeDenyRules(withHooks, buildDenyRules());
|
|
1298
|
-
const { settings: withAllow } = mergeAllowRules(withDeny, buildAllowRules());
|
|
1299
|
-
const { settings: merged } = mergeAskRules(withAllow, buildAskRules());
|
|
1300
|
-
return merged;
|
|
1301
|
-
}
|
|
1302
|
-
async function applyProjectSettingsMerge(root) {
|
|
1303
|
-
const merged = await mergeProjectSettings(root);
|
|
1304
|
-
await writeProjectSettings(root, merged);
|
|
1305
|
-
return merged;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
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
|
-
}
|
|
1511
|
+
/(^|\/)uploads(\/|$)/
|
|
1512
|
+
];
|
|
1513
|
+
var SENSITIVE_ITEM_KEYWORDS = [
|
|
1514
|
+
".env",
|
|
1515
|
+
"secrets",
|
|
1516
|
+
"certs",
|
|
1517
|
+
"customer-data",
|
|
1518
|
+
"exports",
|
|
1519
|
+
".pem",
|
|
1520
|
+
".key"
|
|
1521
|
+
];
|
|
1378
1522
|
|
|
1379
|
-
// src/
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1523
|
+
// src/security/ask-rules.ts
|
|
1524
|
+
function buildAskRules() {
|
|
1525
|
+
const rules = [];
|
|
1526
|
+
for (const command of ASK_COMMANDS) {
|
|
1527
|
+
rules.push(`Bash(${command}:*)`);
|
|
1384
1528
|
}
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1387
|
-
|
|
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)) });
|
|
1529
|
+
for (const { pattern, tools } of ASK_PATHS) {
|
|
1530
|
+
for (const tool of tools) {
|
|
1531
|
+
rules.push(`${tool}(${pattern})`);
|
|
1403
1532
|
}
|
|
1404
1533
|
}
|
|
1405
|
-
|
|
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("|"));
|
|
1534
|
+
return [...new Set(rules)];
|
|
1410
1535
|
}
|
|
1411
1536
|
|
|
1412
|
-
// src/
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
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;
|
|
1537
|
+
// src/security/deny-rules.ts
|
|
1538
|
+
var FILE_TOOLS = ["Read", "Edit", "Write"];
|
|
1539
|
+
function buildDenyRules() {
|
|
1540
|
+
const rules = [];
|
|
1541
|
+
for (const command of DENY_COMMANDS) {
|
|
1542
|
+
rules.push(`Bash(${command}:*)`);
|
|
1433
1543
|
}
|
|
1434
|
-
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
|
|
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`);
|
|
1544
|
+
for (const path36 of DENY_PATHS) {
|
|
1545
|
+
const pattern = DENY_DIRS.has(path36) ? `${path36}/**` : path36;
|
|
1546
|
+
for (const tool of FILE_TOOLS) {
|
|
1547
|
+
rules.push(`${tool}(${pattern})`);
|
|
1448
1548
|
}
|
|
1449
|
-
return;
|
|
1450
1549
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1550
|
+
return [...new Set(rules)];
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// src/claude/merge-project-settings.ts
|
|
1554
|
+
var PROJECT_HOOK_FRAGMENTS = [
|
|
1555
|
+
{
|
|
1556
|
+
id: "haus.guard-file",
|
|
1557
|
+
gate: "keep",
|
|
1558
|
+
event: "PreToolUse",
|
|
1559
|
+
matcher: "Read|Edit|Write",
|
|
1560
|
+
command: "haus guard file-access --from-hook"
|
|
1561
|
+
},
|
|
1562
|
+
{
|
|
1563
|
+
id: "haus.guard-bash",
|
|
1564
|
+
gate: "keep",
|
|
1565
|
+
event: "PreToolUse",
|
|
1566
|
+
matcher: "Bash",
|
|
1567
|
+
command: "haus guard bash --from-hook"
|
|
1455
1568
|
}
|
|
1456
|
-
|
|
1569
|
+
];
|
|
1570
|
+
async function readProjectSettings(root) {
|
|
1571
|
+
const filePath = claudePath(root, "settings.json");
|
|
1572
|
+
const result = await readJsonDetailed(filePath);
|
|
1573
|
+
if (result.status === "ok") return result.value;
|
|
1574
|
+
if (result.status === "missing") return {};
|
|
1575
|
+
const backupPath = await backupMalformedJsonFile(filePath);
|
|
1576
|
+
throw new MalformedJsonFileError(filePath, backupPath);
|
|
1457
1577
|
}
|
|
1458
|
-
async function
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1578
|
+
async function writeProjectSettings(root, settings) {
|
|
1579
|
+
await writeJson(claudePath(root, "settings.json"), settings);
|
|
1580
|
+
}
|
|
1581
|
+
async function mergeProjectSettings(root) {
|
|
1582
|
+
const base = await readProjectSettings(root);
|
|
1583
|
+
const { settings: withHooks } = mergeHooks(base, PROJECT_HOOK_FRAGMENTS);
|
|
1584
|
+
const { settings: withDeny } = mergeDenyRules(withHooks, buildDenyRules());
|
|
1585
|
+
const { settings: withAllow } = mergeAllowRules(withDeny, buildAllowRules());
|
|
1586
|
+
const { settings: merged } = mergeAskRules(withAllow, buildAskRules());
|
|
1587
|
+
return merged;
|
|
1588
|
+
}
|
|
1589
|
+
async function applyProjectSettingsMerge(root) {
|
|
1590
|
+
const merged = await mergeProjectSettings(root);
|
|
1591
|
+
await writeProjectSettings(root, merged);
|
|
1592
|
+
return merged;
|
|
1462
1593
|
}
|
|
1463
1594
|
|
|
1464
1595
|
// src/claude/superpowers-install.ts
|
|
@@ -1613,25 +1744,50 @@ function buildImportBlock() {
|
|
|
1613
1744
|
${IMPORT_CONTENT}
|
|
1614
1745
|
${BLOCK_END}`;
|
|
1615
1746
|
}
|
|
1747
|
+
function findLineMarker(content2, marker, from = 0) {
|
|
1748
|
+
let idx = content2.indexOf(marker, from);
|
|
1749
|
+
while (idx !== -1) {
|
|
1750
|
+
const lineStart = idx === 0 || content2[idx - 1] === "\n";
|
|
1751
|
+
const after = idx + marker.length;
|
|
1752
|
+
const lineEnd = after === content2.length || content2[after] === "\n" || content2[after] === "\r";
|
|
1753
|
+
if (lineStart && lineEnd) return { start: idx, end: after };
|
|
1754
|
+
idx = content2.indexOf(marker, idx + marker.length);
|
|
1755
|
+
}
|
|
1756
|
+
return null;
|
|
1757
|
+
}
|
|
1758
|
+
function findImportBlockRange(content2) {
|
|
1759
|
+
const begin = findLineMarker(content2, BLOCK_BEGIN);
|
|
1760
|
+
if (!begin) return null;
|
|
1761
|
+
const end = findLineMarker(content2, BLOCK_END, begin.end);
|
|
1762
|
+
if (!end) return null;
|
|
1763
|
+
return { beginStart: begin.start, beginEnd: begin.end, endStart: end.start, endEnd: end.end };
|
|
1764
|
+
}
|
|
1616
1765
|
function stripHausBlock(existing) {
|
|
1617
|
-
const
|
|
1618
|
-
if (
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1621
|
-
const before = existing.slice(0, beginIdx);
|
|
1622
|
-
const after = existing.slice(endIdx + BLOCK_END.length);
|
|
1766
|
+
const range = findImportBlockRange(existing);
|
|
1767
|
+
if (!range) return existing;
|
|
1768
|
+
const before = existing.slice(0, range.beginStart);
|
|
1769
|
+
const after = existing.slice(range.endEnd);
|
|
1623
1770
|
const merged = `${before}${after}`.replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
1624
1771
|
return merged.length > 0 ? `${merged}
|
|
1625
1772
|
` : "";
|
|
1626
1773
|
}
|
|
1627
1774
|
function injectHausBlock(existing, block) {
|
|
1628
|
-
const
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
const
|
|
1632
|
-
const after = existing.slice(endIdx + BLOCK_END.length);
|
|
1775
|
+
const range = findImportBlockRange(existing);
|
|
1776
|
+
if (range) {
|
|
1777
|
+
const before = existing.slice(0, range.beginStart);
|
|
1778
|
+
const after = existing.slice(range.endEnd);
|
|
1633
1779
|
return `${before}${block}${after}`;
|
|
1634
1780
|
}
|
|
1781
|
+
const loneBegin = findLineMarker(existing, BLOCK_BEGIN);
|
|
1782
|
+
if (loneBegin) {
|
|
1783
|
+
const before = existing.slice(0, loneBegin.start).trimEnd();
|
|
1784
|
+
if (before.length === 0) return `${block}
|
|
1785
|
+
`;
|
|
1786
|
+
return `${before}
|
|
1787
|
+
|
|
1788
|
+
${block}
|
|
1789
|
+
`;
|
|
1790
|
+
}
|
|
1635
1791
|
const trimmed = existing.trimEnd();
|
|
1636
1792
|
if (trimmed.length === 0) {
|
|
1637
1793
|
return `${block}
|
|
@@ -1816,6 +1972,12 @@ ${templateContent}`;
|
|
|
1816
1972
|
return null;
|
|
1817
1973
|
}
|
|
1818
1974
|
const existingContent = existing.slice(firstLine.length + 1);
|
|
1975
|
+
if (!parsed.hash && !force) {
|
|
1976
|
+
warn(
|
|
1977
|
+
`${printable}: managed header missing hash \u2014 cannot verify tamper status, skipping. Use --force to overwrite.`
|
|
1978
|
+
);
|
|
1979
|
+
return null;
|
|
1980
|
+
}
|
|
1819
1981
|
if (parsed.hash && hashText(normaliseLF(existingContent)) !== parsed.hash && !force) {
|
|
1820
1982
|
warn(`${printable}: content modified by user \u2014 skipping. Use --force to overwrite.`);
|
|
1821
1983
|
return null;
|
|
@@ -2174,18 +2336,6 @@ async function runApply(options) {
|
|
|
2174
2336
|
files.forEach((f) => log(`- ${displayPath(root, f)}`));
|
|
2175
2337
|
}
|
|
2176
2338
|
}
|
|
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
2339
|
|
|
2190
2340
|
// src/commands/catalog-audit.ts
|
|
2191
2341
|
function auditForbiddenTags(items) {
|
|
@@ -2287,7 +2437,7 @@ async function runClone(url, opts = {}) {
|
|
|
2287
2437
|
|
|
2288
2438
|
// src/commands/doctor.ts
|
|
2289
2439
|
import path19 from "path";
|
|
2290
|
-
import
|
|
2440
|
+
import fs14 from "fs-extra";
|
|
2291
2441
|
|
|
2292
2442
|
// src/scanner/scan-project.ts
|
|
2293
2443
|
import path18 from "path";
|
|
@@ -2318,7 +2468,7 @@ function compareVersions(a, b) {
|
|
|
2318
2468
|
|
|
2319
2469
|
// src/scanner/detect-package-manager.ts
|
|
2320
2470
|
import path15 from "path";
|
|
2321
|
-
import
|
|
2471
|
+
import fs13 from "fs-extra";
|
|
2322
2472
|
function detectPackageManager(root, packageManagerField) {
|
|
2323
2473
|
const field = String(packageManagerField ?? "").trim();
|
|
2324
2474
|
if (field.startsWith("yarn@")) {
|
|
@@ -2336,9 +2486,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
2336
2486
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
2337
2487
|
return "unknown";
|
|
2338
2488
|
}
|
|
2339
|
-
if (
|
|
2340
|
-
if (
|
|
2341
|
-
if (
|
|
2489
|
+
if (fs13.existsSync(path15.join(root, "yarn.lock"))) return "yarn";
|
|
2490
|
+
if (fs13.existsSync(path15.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
2491
|
+
if (fs13.existsSync(path15.join(root, "package-lock.json"))) return "npm";
|
|
2342
2492
|
return "unknown";
|
|
2343
2493
|
}
|
|
2344
2494
|
|
|
@@ -2851,7 +3001,7 @@ async function runDoctor(options) {
|
|
|
2851
3001
|
const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
|
|
2852
3002
|
const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
|
|
2853
3003
|
for (const target of importTargets) {
|
|
2854
|
-
if (!await
|
|
3004
|
+
if (!await fs14.pathExists(hausPath(root, target))) {
|
|
2855
3005
|
flag(
|
|
2856
3006
|
`- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
|
|
2857
3007
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -2862,7 +3012,7 @@ async function runDoctor(options) {
|
|
|
2862
3012
|
}
|
|
2863
3013
|
}
|
|
2864
3014
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
2865
|
-
const workflowExists = await
|
|
3015
|
+
const workflowExists = await fs14.pathExists(workflowPath);
|
|
2866
3016
|
if (!workflowExists) {
|
|
2867
3017
|
flag(
|
|
2868
3018
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -2893,7 +3043,7 @@ async function runDoctor(options) {
|
|
|
2893
3043
|
"templates",
|
|
2894
3044
|
"agentic-workflow-standard.md"
|
|
2895
3045
|
);
|
|
2896
|
-
const templatePath = await
|
|
3046
|
+
const templatePath = await fs14.pathExists(cachePath) ? cachePath : bundledPath;
|
|
2897
3047
|
const templateContent = await readText(templatePath);
|
|
2898
3048
|
if (storedHashMatch && templateContent) {
|
|
2899
3049
|
const currentHash = hashText(normaliseLF(templateContent));
|
|
@@ -2913,7 +3063,7 @@ async function runDoctor(options) {
|
|
|
2913
3063
|
}
|
|
2914
3064
|
}
|
|
2915
3065
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
2916
|
-
const workflowConfigExists = await
|
|
3066
|
+
const workflowConfigExists = await fs14.pathExists(workflowConfigPath);
|
|
2917
3067
|
if (!workflowConfigExists) {
|
|
2918
3068
|
flag(
|
|
2919
3069
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -2921,7 +3071,7 @@ async function runDoctor(options) {
|
|
|
2921
3071
|
"haus apply --write"
|
|
2922
3072
|
);
|
|
2923
3073
|
} else {
|
|
2924
|
-
const cfg = await
|
|
3074
|
+
const cfg = await fs14.readFile(workflowConfigPath, "utf8");
|
|
2925
3075
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
2926
3076
|
if (unfilled > 0) {
|
|
2927
3077
|
flag(
|
|
@@ -2961,9 +3111,6 @@ async function runDoctor(options) {
|
|
|
2961
3111
|
`A newer haus (${npmStatus.latest}) is available`,
|
|
2962
3112
|
`npm install -g ${NPM_PACKAGE_NAME}`
|
|
2963
3113
|
);
|
|
2964
|
-
if (!process.env["HAUS_FIXTURE_CATALOG"]) {
|
|
2965
|
-
process.exitCode = 1;
|
|
2966
|
-
}
|
|
2967
3114
|
} else if (npmStatus.latest !== null) {
|
|
2968
3115
|
ok(`- CLI: ${currentVersion} (up to date)`);
|
|
2969
3116
|
} else {
|
|
@@ -3110,15 +3257,23 @@ async function runExplainRecommendation(options) {
|
|
|
3110
3257
|
import { readFileSync as readFileSync2 } from "fs";
|
|
3111
3258
|
|
|
3112
3259
|
// src/security/guard-bash.ts
|
|
3260
|
+
function matchesDenyToken(command, denyPhrase) {
|
|
3261
|
+
if (denyPhrase === "sudo") {
|
|
3262
|
+
return /(?:^|[|;&]\s*)sudo\b/i.test(command);
|
|
3263
|
+
}
|
|
3264
|
+
const normalizedCommand = command.toLowerCase().replace(/\s+/g, " ").trim();
|
|
3265
|
+
return normalizedCommand.includes(denyPhrase.toLowerCase());
|
|
3266
|
+
}
|
|
3113
3267
|
function guardBash(command) {
|
|
3114
|
-
const matched = DENY_COMMANDS.find((
|
|
3268
|
+
const matched = DENY_COMMANDS.find((denyPhrase) => matchesDenyToken(command, denyPhrase));
|
|
3115
3269
|
if (matched) return `I didn't run that \u2014 it can permanently change or delete things: ${command}`;
|
|
3116
3270
|
return void 0;
|
|
3117
3271
|
}
|
|
3118
3272
|
|
|
3119
3273
|
// src/security/guard-file-access.ts
|
|
3120
3274
|
function guardFileAccess(candidate) {
|
|
3121
|
-
const
|
|
3275
|
+
const normalized = normalizePathForMatch(candidate);
|
|
3276
|
+
const matched = DENY_PATH_REGEXES.find((re) => re.test(normalized));
|
|
3122
3277
|
if (matched)
|
|
3123
3278
|
return `I didn't open ${candidate} \u2014 it looks like it holds secrets or sensitive data`;
|
|
3124
3279
|
return void 0;
|
|
@@ -3170,7 +3325,7 @@ async function runGuard(kind, _options) {
|
|
|
3170
3325
|
|
|
3171
3326
|
// src/commands/init.ts
|
|
3172
3327
|
import path20 from "path";
|
|
3173
|
-
import
|
|
3328
|
+
import fs15 from "fs-extra";
|
|
3174
3329
|
|
|
3175
3330
|
// src/utils/prompts.ts
|
|
3176
3331
|
import { stdin as input, stdout as output } from "process";
|
|
@@ -3518,7 +3673,7 @@ async function runSetupProject(options) {
|
|
|
3518
3673
|
async function runInit(options) {
|
|
3519
3674
|
const root = process.cwd();
|
|
3520
3675
|
const hausDir = path20.join(root, ".haus-workflow");
|
|
3521
|
-
const alreadyInit = await
|
|
3676
|
+
const alreadyInit = await fs15.pathExists(hausDir);
|
|
3522
3677
|
if (alreadyInit) {
|
|
3523
3678
|
log("Haus AI already initialized in this project.");
|
|
3524
3679
|
log("Run `haus setup-project` to reconfigure.");
|
|
@@ -3531,7 +3686,7 @@ async function runInit(options) {
|
|
|
3531
3686
|
// src/install/apply.ts
|
|
3532
3687
|
import crypto2 from "crypto";
|
|
3533
3688
|
import path21 from "path";
|
|
3534
|
-
import
|
|
3689
|
+
import fs16 from "fs-extra";
|
|
3535
3690
|
|
|
3536
3691
|
// src/install/header.ts
|
|
3537
3692
|
var MD_PREFIX = "<!-- HAUS-MANAGED";
|
|
@@ -3539,11 +3694,9 @@ var MD_SUFFIX = " -->";
|
|
|
3539
3694
|
var FM_FENCE = "---";
|
|
3540
3695
|
var FM_KEY = "haus_managed";
|
|
3541
3696
|
function parseAttrs(raw) {
|
|
3542
|
-
const
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
if (!idMatch || !vMatch || !srcMatch) return void 0;
|
|
3546
|
-
return { stableId: idMatch[1], schemaVersion: vMatch[1], source: srcMatch[1] };
|
|
3697
|
+
const attrs = parseHausManagedAttrs(raw) ?? parseHausManagedAttrs(`<!-- HAUS-MANAGED ${raw.trim()} -->`);
|
|
3698
|
+
if (!attrs?.id || !attrs.v || !attrs.source) return void 0;
|
|
3699
|
+
return { stableId: attrs.id, schemaVersion: attrs.v, source: attrs.source };
|
|
3547
3700
|
}
|
|
3548
3701
|
function isFence(line2) {
|
|
3549
3702
|
return (line2 ?? "").replace(/\r$/, "") === FM_FENCE;
|
|
@@ -3620,7 +3773,7 @@ function hashContent(content2) {
|
|
|
3620
3773
|
function sourceVersion() {
|
|
3621
3774
|
try {
|
|
3622
3775
|
const pkgPath = path21.join(packageRoot(), "package.json");
|
|
3623
|
-
const pkg = JSON.parse(
|
|
3776
|
+
const pkg = JSON.parse(fs16.readFileSync(pkgPath, "utf8"));
|
|
3624
3777
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
3625
3778
|
} catch {
|
|
3626
3779
|
return "haus@0.0.0";
|
|
@@ -3632,10 +3785,10 @@ function globalSrcDir() {
|
|
|
3632
3785
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
3633
3786
|
const entries = [];
|
|
3634
3787
|
const skillsDir = path21.join(srcDir, "skills");
|
|
3635
|
-
if (
|
|
3636
|
-
for (const skillName of
|
|
3788
|
+
if (fs16.pathExistsSync(skillsDir)) {
|
|
3789
|
+
for (const skillName of fs16.readdirSync(skillsDir)) {
|
|
3637
3790
|
const skillFile = path21.join(skillsDir, skillName, "SKILL.md");
|
|
3638
|
-
if (
|
|
3791
|
+
if (fs16.pathExistsSync(skillFile)) {
|
|
3639
3792
|
entries.push({
|
|
3640
3793
|
stableId: `skill.${skillName}`,
|
|
3641
3794
|
srcRelPath: path21.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
@@ -3645,8 +3798,8 @@ function collectSourceFiles(srcDir, claudeDir) {
|
|
|
3645
3798
|
}
|
|
3646
3799
|
}
|
|
3647
3800
|
const commandsDir = path21.join(srcDir, "commands");
|
|
3648
|
-
if (
|
|
3649
|
-
for (const fileName of
|
|
3801
|
+
if (fs16.pathExistsSync(commandsDir)) {
|
|
3802
|
+
for (const fileName of fs16.readdirSync(commandsDir)) {
|
|
3650
3803
|
if (!fileName.endsWith(".md")) continue;
|
|
3651
3804
|
const commandName = fileName.slice(0, -".md".length);
|
|
3652
3805
|
entries.push({
|
|
@@ -3696,7 +3849,7 @@ async function applyInstall(options = {}) {
|
|
|
3696
3849
|
}
|
|
3697
3850
|
continue;
|
|
3698
3851
|
}
|
|
3699
|
-
const destExists =
|
|
3852
|
+
const destExists = fs16.pathExistsSync(entry.destPath);
|
|
3700
3853
|
if (destExists) {
|
|
3701
3854
|
const currentContent = await readText(entry.destPath);
|
|
3702
3855
|
if (currentContent !== void 0) {
|
|
@@ -3744,13 +3897,13 @@ async function applyInstall(options = {}) {
|
|
|
3744
3897
|
const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
|
|
3745
3898
|
for (const entry of existingManifest.files) {
|
|
3746
3899
|
if (currentDestPaths.has(entry.destPath)) continue;
|
|
3747
|
-
if (!
|
|
3900
|
+
if (!fs16.pathExistsSync(entry.destPath)) continue;
|
|
3748
3901
|
const content2 = await readText(entry.destPath);
|
|
3749
3902
|
if (!content2) continue;
|
|
3750
3903
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
3751
3904
|
const currentHash = hashContent(content2);
|
|
3752
3905
|
if (hasHeader && currentHash === entry.hash) {
|
|
3753
|
-
if (!dryRun) await
|
|
3906
|
+
if (!dryRun) await fs16.remove(entry.destPath);
|
|
3754
3907
|
result.deleted.push(entry.destPath);
|
|
3755
3908
|
} else {
|
|
3756
3909
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -3761,8 +3914,7 @@ async function applyInstall(options = {}) {
|
|
|
3761
3914
|
if (!dryRun && !check) {
|
|
3762
3915
|
await writeSettings(mergedSettings);
|
|
3763
3916
|
const manifest = buildManifest(source, manifestFiles, [
|
|
3764
|
-
...existingManifest?.hooks ?? [],
|
|
3765
|
-
...addedIds
|
|
3917
|
+
.../* @__PURE__ */ new Set([...existingManifest?.hooks ?? [], ...addedIds])
|
|
3766
3918
|
]);
|
|
3767
3919
|
await writeManifest(manifest);
|
|
3768
3920
|
}
|
|
@@ -3925,7 +4077,7 @@ async function runScan(options) {
|
|
|
3925
4077
|
|
|
3926
4078
|
// src/commands/undo.ts
|
|
3927
4079
|
import path22 from "path";
|
|
3928
|
-
import
|
|
4080
|
+
import fs17 from "fs-extra";
|
|
3929
4081
|
|
|
3930
4082
|
// src/claude/managed-paths.ts
|
|
3931
4083
|
var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md", "commands/haus-doctor.md"];
|
|
@@ -3947,25 +4099,25 @@ async function collectManagedPaths(root) {
|
|
|
3947
4099
|
}
|
|
3948
4100
|
const existing = [];
|
|
3949
4101
|
for (const abs of paths) {
|
|
3950
|
-
if (await
|
|
4102
|
+
if (await fs17.pathExists(abs)) existing.push(abs);
|
|
3951
4103
|
}
|
|
3952
4104
|
return existing;
|
|
3953
4105
|
}
|
|
3954
4106
|
async function settingsHasHausContent(root) {
|
|
3955
4107
|
const settingsPath = claudePath(root, "settings.json");
|
|
3956
|
-
if (!await
|
|
4108
|
+
if (!await fs17.pathExists(settingsPath)) return false;
|
|
3957
4109
|
const settings = await readProjectSettings(root);
|
|
3958
4110
|
return settings._haus != null;
|
|
3959
4111
|
}
|
|
3960
4112
|
async function claudeMdHasHausBlock(root) {
|
|
3961
4113
|
const filePath = path22.join(root, "CLAUDE.md");
|
|
3962
|
-
if (!await
|
|
3963
|
-
const text = await
|
|
4114
|
+
if (!await fs17.pathExists(filePath)) return false;
|
|
4115
|
+
const text = await fs17.readFile(filePath, "utf8");
|
|
3964
4116
|
return text.includes(BLOCK_BEGIN);
|
|
3965
4117
|
}
|
|
3966
4118
|
async function stripProjectSettings(root) {
|
|
3967
4119
|
const settingsPath = claudePath(root, "settings.json");
|
|
3968
|
-
if (!await
|
|
4120
|
+
if (!await fs17.pathExists(settingsPath)) return false;
|
|
3969
4121
|
let settings = await readProjectSettings(root);
|
|
3970
4122
|
settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
3971
4123
|
const hasContent = Object.keys(settings).length > 0;
|
|
@@ -3974,29 +4126,42 @@ async function stripProjectSettings(root) {
|
|
|
3974
4126
|
log(`Stripped haus rules from ${path22.relative(root, settingsPath)} (user settings preserved).`);
|
|
3975
4127
|
return true;
|
|
3976
4128
|
}
|
|
3977
|
-
await
|
|
4129
|
+
await fs17.remove(settingsPath);
|
|
3978
4130
|
log(`Removed ${path22.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
3979
4131
|
return true;
|
|
3980
4132
|
}
|
|
3981
4133
|
async function stripRootClaudeMd(root) {
|
|
3982
4134
|
const filePath = path22.join(root, "CLAUDE.md");
|
|
3983
|
-
if (!await
|
|
3984
|
-
const prev = await
|
|
4135
|
+
if (!await fs17.pathExists(filePath)) return false;
|
|
4136
|
+
const prev = await fs17.readFile(filePath, "utf8");
|
|
3985
4137
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
3986
4138
|
const next = stripHausBlock(prev);
|
|
3987
4139
|
if (next.length === 0) {
|
|
3988
|
-
await
|
|
4140
|
+
await fs17.remove(filePath);
|
|
3989
4141
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
3990
4142
|
} else {
|
|
3991
|
-
await
|
|
4143
|
+
await fs17.writeFile(filePath, next, "utf8");
|
|
3992
4144
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
3993
4145
|
}
|
|
3994
4146
|
return true;
|
|
3995
4147
|
}
|
|
3996
4148
|
async function pruneDirIfEmpty(dir) {
|
|
3997
|
-
if (!await
|
|
3998
|
-
const entries = await
|
|
3999
|
-
if (entries.length === 0) await
|
|
4149
|
+
if (!await fs17.pathExists(dir)) return;
|
|
4150
|
+
const entries = await fs17.readdir(dir);
|
|
4151
|
+
if (entries.length === 0) await fs17.remove(dir);
|
|
4152
|
+
}
|
|
4153
|
+
async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
4154
|
+
if (managedAbsPaths.length === 0) return;
|
|
4155
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4156
|
+
const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
|
|
4157
|
+
for (const abs of managedAbsPaths) {
|
|
4158
|
+
if (!await fs17.pathExists(abs)) continue;
|
|
4159
|
+
const rel = path22.relative(root, abs);
|
|
4160
|
+
const backupPath = path22.join(backupRoot, rel);
|
|
4161
|
+
await fs17.ensureDir(path22.dirname(backupPath));
|
|
4162
|
+
await fs17.copy(abs, backupPath);
|
|
4163
|
+
}
|
|
4164
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path22.relative(root, backupRoot)}.`);
|
|
4000
4165
|
}
|
|
4001
4166
|
async function runUndo(options) {
|
|
4002
4167
|
const root = process.cwd();
|
|
@@ -4022,9 +4187,10 @@ User-owned .claude/ files will be preserved.`
|
|
|
4022
4187
|
return;
|
|
4023
4188
|
}
|
|
4024
4189
|
}
|
|
4190
|
+
await backupManagedFilesBeforeUndo(root, managed);
|
|
4025
4191
|
for (const abs of managed) {
|
|
4026
|
-
if (!await
|
|
4027
|
-
await
|
|
4192
|
+
if (!await fs17.pathExists(abs)) continue;
|
|
4193
|
+
await fs17.remove(abs);
|
|
4028
4194
|
log(`Removed ${path22.relative(root, abs)}`);
|
|
4029
4195
|
}
|
|
4030
4196
|
if (stripSettings) await stripProjectSettings(root);
|
|
@@ -4037,7 +4203,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
4037
4203
|
// src/install/uninstall.ts
|
|
4038
4204
|
import crypto3 from "crypto";
|
|
4039
4205
|
import path23 from "path";
|
|
4040
|
-
import
|
|
4206
|
+
import fs18 from "fs-extra";
|
|
4041
4207
|
async function runUninstall(options = {}) {
|
|
4042
4208
|
const { force = false } = options;
|
|
4043
4209
|
const manifest = await readManifest();
|
|
@@ -4047,7 +4213,7 @@ async function runUninstall(options = {}) {
|
|
|
4047
4213
|
return result;
|
|
4048
4214
|
}
|
|
4049
4215
|
for (const entry of manifest.files) {
|
|
4050
|
-
const exists =
|
|
4216
|
+
const exists = fs18.pathExistsSync(entry.destPath);
|
|
4051
4217
|
if (!exists) continue;
|
|
4052
4218
|
const content2 = await readText(entry.destPath);
|
|
4053
4219
|
if (content2 === void 0) continue;
|
|
@@ -4065,7 +4231,7 @@ async function runUninstall(options = {}) {
|
|
|
4065
4231
|
result.skipped.push(entry.destPath);
|
|
4066
4232
|
continue;
|
|
4067
4233
|
}
|
|
4068
|
-
await
|
|
4234
|
+
await fs18.remove(entry.destPath);
|
|
4069
4235
|
await pruneEmptyDir(path23.dirname(entry.destPath));
|
|
4070
4236
|
result.deleted.push(entry.destPath);
|
|
4071
4237
|
}
|
|
@@ -4075,12 +4241,12 @@ async function runUninstall(options = {}) {
|
|
|
4075
4241
|
result.hooksStripped = true;
|
|
4076
4242
|
const hausDir = path23.join(globalClaudeDir(), "haus");
|
|
4077
4243
|
const manifestPath2 = hausManifestPath();
|
|
4078
|
-
if (
|
|
4079
|
-
await
|
|
4244
|
+
if (fs18.pathExistsSync(manifestPath2)) {
|
|
4245
|
+
await fs18.remove(manifestPath2);
|
|
4080
4246
|
}
|
|
4081
|
-
if (
|
|
4082
|
-
const remaining = await
|
|
4083
|
-
if (remaining.length === 0) await
|
|
4247
|
+
if (fs18.pathExistsSync(hausDir)) {
|
|
4248
|
+
const remaining = await fs18.readdir(hausDir);
|
|
4249
|
+
if (remaining.length === 0) await fs18.remove(hausDir);
|
|
4084
4250
|
}
|
|
4085
4251
|
return result;
|
|
4086
4252
|
}
|
|
@@ -4113,6 +4279,19 @@ async function runUninstallCommand(options) {
|
|
|
4113
4279
|
// src/commands/update.ts
|
|
4114
4280
|
import path25 from "path";
|
|
4115
4281
|
|
|
4282
|
+
// src/claude/refresh-project.ts
|
|
4283
|
+
import fs19 from "fs-extra";
|
|
4284
|
+
async function isHausProject(root) {
|
|
4285
|
+
if (await fs19.pathExists(hausPath(root, "recommendation.json"))) return true;
|
|
4286
|
+
const result = await readJsonDetailed(claudePath(root, "settings.json"));
|
|
4287
|
+
if (result.status === "ok") return result.value._haus != null;
|
|
4288
|
+
return false;
|
|
4289
|
+
}
|
|
4290
|
+
async function refreshProjectApply(root) {
|
|
4291
|
+
if (!await isHausProject(root)) return [];
|
|
4292
|
+
return writeClaudeFiles(root, false, void 0, { refillConfig: false });
|
|
4293
|
+
}
|
|
4294
|
+
|
|
4116
4295
|
// src/update/diff-generated-files.ts
|
|
4117
4296
|
function diffGeneratedFiles() {
|
|
4118
4297
|
return "Generated files may change in .claude/* and .haus-workflow/haus.lock.json. Review git diff before apply.";
|
|
@@ -4310,8 +4489,18 @@ async function detectGlobalInstallDrift() {
|
|
|
4310
4489
|
}
|
|
4311
4490
|
|
|
4312
4491
|
// src/commands/validate-catalog.ts
|
|
4492
|
+
import path27 from "path";
|
|
4493
|
+
|
|
4494
|
+
// src/catalog/validate-core.ts
|
|
4313
4495
|
import fs20 from "fs";
|
|
4314
4496
|
import path26 from "path";
|
|
4497
|
+
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
4498
|
+
var MANIFEST_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
|
|
4499
|
+
function isSafeCatalogPath2(itemPath) {
|
|
4500
|
+
if (!itemPath || path26.isAbsolute(itemPath) || itemPath.includes("\\")) return false;
|
|
4501
|
+
const normalized = path26.normalize(itemPath);
|
|
4502
|
+
return !normalized.startsWith("..") && !normalized.includes("/..");
|
|
4503
|
+
}
|
|
4315
4504
|
function auditForbiddenStacks(items) {
|
|
4316
4505
|
const failures = [];
|
|
4317
4506
|
for (const item of items) {
|
|
@@ -4323,10 +4512,15 @@ function auditForbiddenStacks(items) {
|
|
|
4323
4512
|
}
|
|
4324
4513
|
return failures;
|
|
4325
4514
|
}
|
|
4326
|
-
function auditManifestStructure(items) {
|
|
4515
|
+
function auditManifestStructure(manifestVersion, items) {
|
|
4327
4516
|
const failures = [];
|
|
4328
4517
|
const seenIds = /* @__PURE__ */ new Map();
|
|
4329
4518
|
const seenPaths = /* @__PURE__ */ new Map();
|
|
4519
|
+
if (typeof manifestVersion !== "string" || !MANIFEST_SEMVER_RE.test(manifestVersion)) {
|
|
4520
|
+
failures.push(
|
|
4521
|
+
'manifest.json: top-level "version" is not valid semver (expected X.Y.Z or X.Y.Z-pre)'
|
|
4522
|
+
);
|
|
4523
|
+
}
|
|
4330
4524
|
for (let i = 0; i < items.length; i++) {
|
|
4331
4525
|
const item = items[i];
|
|
4332
4526
|
if (!item.id) {
|
|
@@ -4337,11 +4531,10 @@ function auditManifestStructure(items) {
|
|
|
4337
4531
|
failures.push(`${item.id}: missing type`);
|
|
4338
4532
|
continue;
|
|
4339
4533
|
}
|
|
4340
|
-
if (!item.source) {
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
failures.push(`${item.id}: missing title`);
|
|
4534
|
+
if (!item.source) failures.push(`${item.id}: missing source`);
|
|
4535
|
+
if (!item.title) failures.push(`${item.id}: missing title`);
|
|
4536
|
+
if (item.version && !ITEM_SEMVER_RE.test(item.version)) {
|
|
4537
|
+
failures.push(`${item.id}: version "${item.version}" is not valid semver (expected X.Y.Z)`);
|
|
4345
4538
|
}
|
|
4346
4539
|
const prev = seenIds.get(item.id);
|
|
4347
4540
|
if (prev !== void 0) {
|
|
@@ -4352,14 +4545,13 @@ function auditManifestStructure(items) {
|
|
|
4352
4545
|
if (item.type === "skill" || item.type === "agent" || item.type === "template" || item.type === "command") {
|
|
4353
4546
|
if (!item.path) {
|
|
4354
4547
|
failures.push(`${item.id}: missing path`);
|
|
4548
|
+
} else if (!isSafeCatalogPath2(item.path)) {
|
|
4549
|
+
failures.push(`${item.id}: path "${item.path}" is not a safe relative path`);
|
|
4355
4550
|
} else {
|
|
4356
4551
|
const norm = item.path.replace(/\\/g, "/");
|
|
4357
4552
|
const existing = seenPaths.get(norm);
|
|
4358
|
-
if (existing) {
|
|
4359
|
-
|
|
4360
|
-
} else {
|
|
4361
|
-
seenPaths.set(norm, item.id);
|
|
4362
|
-
}
|
|
4553
|
+
if (existing) failures.push(`${item.id}: path "${norm}" already used by ${existing}`);
|
|
4554
|
+
else seenPaths.set(norm, item.id);
|
|
4363
4555
|
}
|
|
4364
4556
|
const isHaus = item.source === "haus";
|
|
4365
4557
|
const isCuratedApproved = item.source === "curated" && item.reviewStatus === "approved";
|
|
@@ -4367,8 +4559,13 @@ function auditManifestStructure(items) {
|
|
|
4367
4559
|
failures.push(`${item.id}: source must be "haus" or curated with reviewStatus "approved"`);
|
|
4368
4560
|
}
|
|
4369
4561
|
for (const ref of item.references ?? []) {
|
|
4370
|
-
if (
|
|
4371
|
-
|
|
4562
|
+
if (/^https?:\/\//i.test(ref)) {
|
|
4563
|
+
if (!ref.startsWith("https://")) {
|
|
4564
|
+
failures.push(`${item.id}: reference must be https:// URL: ${ref}`);
|
|
4565
|
+
}
|
|
4566
|
+
if (HTTP_URL_PATTERN.test(ref)) {
|
|
4567
|
+
failures.push(`${item.id}: reference uses insecure http:// URL: ${ref}`);
|
|
4568
|
+
}
|
|
4372
4569
|
}
|
|
4373
4570
|
}
|
|
4374
4571
|
}
|
|
@@ -4378,16 +4575,33 @@ function auditManifestStructure(items) {
|
|
|
4378
4575
|
function checkRequiredFrontmatter(text, label) {
|
|
4379
4576
|
const failures = [];
|
|
4380
4577
|
for (const key of REQUIRED_SKILL_FRONTMATTER) {
|
|
4381
|
-
if (!extractFrontmatterValue(text, key))
|
|
4578
|
+
if (!extractFrontmatterValue(text, key))
|
|
4382
4579
|
failures.push(`${label}: missing non-empty frontmatter '${key}:'`);
|
|
4580
|
+
}
|
|
4581
|
+
return failures;
|
|
4582
|
+
}
|
|
4583
|
+
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
4584
|
+
const rel = path26.relative(manifestDir, absPath);
|
|
4585
|
+
const text = fs20.readFileSync(absPath, "utf8");
|
|
4586
|
+
const failures = [];
|
|
4587
|
+
const lines = text.split(/\r?\n/);
|
|
4588
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4589
|
+
const line2 = lines[i] ?? "";
|
|
4590
|
+
if (PLACEHOLDER_PATTERN.test(line2))
|
|
4591
|
+
failures.push(`${rel}:${i + 1}: TODO or placeholder in shipped content`);
|
|
4592
|
+
if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2)))
|
|
4593
|
+
failures.push(`${rel}:${i + 1}: risky install pattern`);
|
|
4594
|
+
if (ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
|
|
4595
|
+
failures.push(`${rel}:${i + 1}: disallowed npx (only npx tsx allowed)`);
|
|
4383
4596
|
}
|
|
4384
4597
|
}
|
|
4598
|
+
failures.push(...auditForbiddenTagsInText(text, `${itemId}: ${rel}`));
|
|
4385
4599
|
return failures;
|
|
4386
4600
|
}
|
|
4387
4601
|
function auditShippedFiles(manifestDir, items) {
|
|
4388
4602
|
const failures = [];
|
|
4389
4603
|
for (const item of items) {
|
|
4390
|
-
if (!item.path) continue;
|
|
4604
|
+
if (!item.path || !isSafeCatalogPath2(item.path)) continue;
|
|
4391
4605
|
const absPath = path26.join(manifestDir, item.path);
|
|
4392
4606
|
if (item.type === "skill") {
|
|
4393
4607
|
const skillMd = path26.join(absPath, "SKILL.md");
|
|
@@ -4428,36 +4642,22 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4428
4642
|
}
|
|
4429
4643
|
return failures;
|
|
4430
4644
|
}
|
|
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
4645
|
var DIR_ITEM_TYPE = {
|
|
4452
4646
|
skills: "skill",
|
|
4453
4647
|
agents: "agent",
|
|
4454
4648
|
templates: "template",
|
|
4455
4649
|
commands: "command"
|
|
4456
4650
|
};
|
|
4651
|
+
function walkMd(dir, fn) {
|
|
4652
|
+
for (const entry of fs20.readdirSync(dir, { withFileTypes: true })) {
|
|
4653
|
+
const full = path26.join(dir, entry.name);
|
|
4654
|
+
if (entry.isDirectory()) walkMd(full, fn);
|
|
4655
|
+
else if (entry.name.endsWith(".md")) fn(full);
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
4457
4658
|
function auditMarkdownContent(manifestDir) {
|
|
4458
4659
|
const failures = [];
|
|
4459
|
-
const
|
|
4460
|
-
for (const dir of dirs) {
|
|
4660
|
+
for (const dir of ["skills", "agents", "templates", "commands"]) {
|
|
4461
4661
|
const abs = path26.join(manifestDir, dir);
|
|
4462
4662
|
if (!fs20.existsSync(abs)) continue;
|
|
4463
4663
|
const checkNonTsxNpx = !NPX_TSX_ONLY_EXEMPT_TYPES.includes(DIR_ITEM_TYPE[dir] ?? "");
|
|
@@ -4467,65 +4667,55 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4467
4667
|
const lines = text.split(/\r?\n/);
|
|
4468
4668
|
for (let i = 0; i < lines.length; i++) {
|
|
4469
4669
|
const line2 = lines[i] ?? "";
|
|
4470
|
-
if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2)))
|
|
4670
|
+
if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2)))
|
|
4471
4671
|
failures.push(`${rel}:${i + 1}: risky install pattern`);
|
|
4472
|
-
}
|
|
4473
4672
|
if (checkNonTsxNpx && ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
|
|
4474
4673
|
failures.push(`${rel}:${i + 1}: disallowed npx (only npx tsx allowed)`);
|
|
4475
4674
|
}
|
|
4476
4675
|
}
|
|
4477
|
-
if (!rel.includes("/references/"))
|
|
4478
|
-
failures.push(...auditForbiddenTagsInText(text, rel));
|
|
4479
|
-
}
|
|
4676
|
+
if (!rel.includes("/references/")) failures.push(...auditForbiddenTagsInText(text, rel));
|
|
4480
4677
|
});
|
|
4481
4678
|
}
|
|
4482
4679
|
return failures;
|
|
4483
4680
|
}
|
|
4484
|
-
function
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4681
|
+
function validateCatalogData(manifestDir, manifestVersion, items) {
|
|
4682
|
+
const failures = [
|
|
4683
|
+
...auditManifestStructure(manifestVersion, items),
|
|
4684
|
+
...auditForbiddenStacks(items),
|
|
4685
|
+
...auditShippedFiles(manifestDir, items),
|
|
4686
|
+
...auditMarkdownContent(manifestDir),
|
|
4687
|
+
...auditDisallowedTags(items)
|
|
4688
|
+
];
|
|
4689
|
+
return { ok: failures.length === 0, failures, items };
|
|
4490
4690
|
}
|
|
4691
|
+
|
|
4692
|
+
// src/commands/validate-catalog.ts
|
|
4491
4693
|
async function runValidateCatalog(manifestPath2) {
|
|
4492
4694
|
if (!manifestPath2) {
|
|
4493
4695
|
error("Usage: haus validate-catalog <path/to/manifest.json>");
|
|
4494
4696
|
process.exitCode = 1;
|
|
4495
4697
|
return;
|
|
4496
4698
|
}
|
|
4497
|
-
const abs =
|
|
4498
|
-
const manifestDir =
|
|
4699
|
+
const abs = path27.resolve(process.cwd(), manifestPath2);
|
|
4700
|
+
const manifestDir = path27.dirname(abs);
|
|
4499
4701
|
const data = await readJson(abs);
|
|
4500
4702
|
if (!data?.items) {
|
|
4501
4703
|
error(`Could not read catalog manifest at ${abs}`);
|
|
4502
4704
|
process.exitCode = 1;
|
|
4503
4705
|
return;
|
|
4504
4706
|
}
|
|
4505
|
-
const
|
|
4506
|
-
|
|
4507
|
-
|
|
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));
|
|
4707
|
+
const result = validateCatalogData(manifestDir, data.version, data.items);
|
|
4708
|
+
if (!result.ok) {
|
|
4709
|
+
result.failures.forEach((f) => error(f));
|
|
4520
4710
|
process.exitCode = 1;
|
|
4521
4711
|
return;
|
|
4522
4712
|
}
|
|
4523
|
-
log(`Catalog valid. ${items.length} items checked.`);
|
|
4713
|
+
log(`Catalog valid. ${result.items.length} items checked.`);
|
|
4524
4714
|
}
|
|
4525
4715
|
|
|
4526
4716
|
// src/commands/workspace.ts
|
|
4527
4717
|
import { existsSync as existsSync5, statSync as statSync2 } from "fs";
|
|
4528
|
-
import
|
|
4718
|
+
import path34 from "path";
|
|
4529
4719
|
|
|
4530
4720
|
// src/commands/workspace/aggregate.ts
|
|
4531
4721
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -4579,7 +4769,7 @@ ${summaries.map(
|
|
|
4579
4769
|
}
|
|
4580
4770
|
|
|
4581
4771
|
// src/commands/workspace/config.ts
|
|
4582
|
-
import
|
|
4772
|
+
import path28 from "path";
|
|
4583
4773
|
import YAML from "yaml";
|
|
4584
4774
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
4585
4775
|
function parseWorkspaceConfig(text) {
|
|
@@ -4602,11 +4792,11 @@ function parseWorkspaceConfig(text) {
|
|
|
4602
4792
|
};
|
|
4603
4793
|
}
|
|
4604
4794
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
4605
|
-
return parseWorkspaceConfig(await readText(
|
|
4795
|
+
return parseWorkspaceConfig(await readText(path28.join(workspaceRoot, WORKSPACE_FILE)));
|
|
4606
4796
|
}
|
|
4607
4797
|
|
|
4608
4798
|
// src/commands/workspace/discover.ts
|
|
4609
|
-
import
|
|
4799
|
+
import path29 from "path";
|
|
4610
4800
|
import fg4 from "fast-glob";
|
|
4611
4801
|
import YAML2 from "yaml";
|
|
4612
4802
|
var DEFAULT_MAX_DEPTH = 3;
|
|
@@ -4634,8 +4824,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
4634
4824
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
4635
4825
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
4636
4826
|
for (const match of matches) {
|
|
4637
|
-
const base =
|
|
4638
|
-
const dir =
|
|
4827
|
+
const base = path29.posix.basename(match);
|
|
4828
|
+
const dir = path29.posix.dirname(match);
|
|
4639
4829
|
const owner = dir === "." ? "." : dir;
|
|
4640
4830
|
if (base === ".git") gitDirs.add(owner);
|
|
4641
4831
|
else manifestDirs.add(owner);
|
|
@@ -4651,9 +4841,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
4651
4841
|
}
|
|
4652
4842
|
repoRoots.sort((a, b) => a.localeCompare(b));
|
|
4653
4843
|
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
4654
|
-
const absDir =
|
|
4655
|
-
const pkg = await readJson(
|
|
4656
|
-
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name :
|
|
4844
|
+
const absDir = path29.resolve(workspaceRoot, relDir);
|
|
4845
|
+
const pkg = await readJson(path29.join(absDir, "package.json"));
|
|
4846
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path29.basename(relDir === "." ? workspaceRoot : absDir);
|
|
4657
4847
|
let role = "auto";
|
|
4658
4848
|
try {
|
|
4659
4849
|
const scan = await scanProject(absDir);
|
|
@@ -4696,7 +4886,7 @@ function renderWorkspaceYaml(config) {
|
|
|
4696
4886
|
});
|
|
4697
4887
|
}
|
|
4698
4888
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
4699
|
-
const yamlPath =
|
|
4889
|
+
const yamlPath = path29.join(workspaceRoot, "haus.workspace.yaml");
|
|
4700
4890
|
const existingText = await readText(yamlPath);
|
|
4701
4891
|
const existing = parseWorkspaceConfig(existingText);
|
|
4702
4892
|
if (existingText && !existing) {
|
|
@@ -4730,18 +4920,18 @@ async function runDiscover(workspaceRoot, opts = {}) {
|
|
|
4730
4920
|
|
|
4731
4921
|
// src/commands/workspace/doctor.ts
|
|
4732
4922
|
import { existsSync as existsSync3 } from "fs";
|
|
4733
|
-
import
|
|
4923
|
+
import path31 from "path";
|
|
4734
4924
|
|
|
4735
4925
|
// src/commands/workspace/manifest.ts
|
|
4736
4926
|
import { readFileSync as readFileSync3 } from "fs";
|
|
4737
|
-
import
|
|
4927
|
+
import path30 from "path";
|
|
4738
4928
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
4739
4929
|
function manifestPath(workspaceRoot) {
|
|
4740
4930
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
4741
4931
|
}
|
|
4742
4932
|
function hausVersion() {
|
|
4743
4933
|
try {
|
|
4744
|
-
const pkg = JSON.parse(readFileSync3(
|
|
4934
|
+
const pkg = JSON.parse(readFileSync3(path30.join(packageRoot(), "package.json"), "utf8"));
|
|
4745
4935
|
return pkg.version ?? "0.0.0";
|
|
4746
4936
|
} catch {
|
|
4747
4937
|
return "0.0.0";
|
|
@@ -4807,7 +4997,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
4807
4997
|
}
|
|
4808
4998
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
4809
4999
|
for (const repo of config.repos) {
|
|
4810
|
-
const repoRoot =
|
|
5000
|
+
const repoRoot = path31.resolve(workspaceRoot, repo.path);
|
|
4811
5001
|
const entry = manifestByName.get(repo.name);
|
|
4812
5002
|
if (!entry) {
|
|
4813
5003
|
flag({
|
|
@@ -4881,10 +5071,10 @@ function emit(args) {
|
|
|
4881
5071
|
|
|
4882
5072
|
// src/commands/workspace/setup.ts
|
|
4883
5073
|
import { existsSync as existsSync4, statSync } from "fs";
|
|
4884
|
-
import
|
|
5074
|
+
import path33 from "path";
|
|
4885
5075
|
|
|
4886
5076
|
// src/claude/write-workspace-claude-md.ts
|
|
4887
|
-
import
|
|
5077
|
+
import path32 from "path";
|
|
4888
5078
|
import fs21 from "fs-extra";
|
|
4889
5079
|
function buildWorkspaceImportBlock(client, members) {
|
|
4890
5080
|
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
@@ -4903,7 +5093,7 @@ ${BLOCK_END}`;
|
|
|
4903
5093
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
4904
5094
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
4905
5095
|
const dryRun = opts.dryRun ?? false;
|
|
4906
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
5096
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path32.join(workspaceRoot, "CLAUDE.md");
|
|
4907
5097
|
const prev = await fs21.pathExists(filePath) ? await fs21.readFile(filePath, "utf8") : "";
|
|
4908
5098
|
const next = opts.collision ? `${block}
|
|
4909
5099
|
` : injectHausBlock(prev, block);
|
|
@@ -4929,20 +5119,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
4929
5119
|
|
|
4930
5120
|
// src/commands/workspace/setup.ts
|
|
4931
5121
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
4932
|
-
let dir =
|
|
5122
|
+
let dir = path33.resolve(start);
|
|
4933
5123
|
for (; ; ) {
|
|
4934
|
-
if (existsSync4(
|
|
4935
|
-
const parent =
|
|
4936
|
-
if (parent === dir) return
|
|
5124
|
+
if (existsSync4(path33.join(dir, WORKSPACE_FILE))) return dir;
|
|
5125
|
+
const parent = path33.dirname(dir);
|
|
5126
|
+
if (parent === dir) return path33.resolve(start);
|
|
4937
5127
|
dir = parent;
|
|
4938
5128
|
}
|
|
4939
5129
|
}
|
|
4940
5130
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
4941
|
-
return
|
|
5131
|
+
return path33.resolve(workspaceRoot, repoPath) === path33.resolve(workspaceRoot);
|
|
4942
5132
|
}
|
|
4943
5133
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
4944
5134
|
const apply = options.write ?? false;
|
|
4945
|
-
const configText = await readText(
|
|
5135
|
+
const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
|
|
4946
5136
|
if (!configText) {
|
|
4947
5137
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
4948
5138
|
process.exitCode = 1;
|
|
@@ -4959,7 +5149,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
4959
5149
|
const statuses = [];
|
|
4960
5150
|
const aggregateInputs = [];
|
|
4961
5151
|
for (const repo of repos) {
|
|
4962
|
-
const repoRoot =
|
|
5152
|
+
const repoRoot = path33.resolve(workspaceRoot, repo.path);
|
|
4963
5153
|
log(`
|
|
4964
5154
|
\u2192 ${repo.name} (${repo.path})`);
|
|
4965
5155
|
try {
|
|
@@ -5027,7 +5217,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
5027
5217
|
const status = statusByName.get(repo.name);
|
|
5028
5218
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
5029
5219
|
if (status?.status === "ok") {
|
|
5030
|
-
const lock = await checkLock(
|
|
5220
|
+
const lock = await checkLock(path33.resolve(workspaceRoot, repo.path));
|
|
5031
5221
|
manifestRepos.push({
|
|
5032
5222
|
name: repo.name,
|
|
5033
5223
|
path: repo.path,
|
|
@@ -5107,7 +5297,7 @@ relationships: []
|
|
|
5107
5297
|
log("Workspace initialized.");
|
|
5108
5298
|
}
|
|
5109
5299
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
5110
|
-
const configText = await readText(
|
|
5300
|
+
const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
|
|
5111
5301
|
if (!configText) {
|
|
5112
5302
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
5113
5303
|
process.exitCode = 1;
|
|
@@ -5128,7 +5318,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
5128
5318
|
}
|
|
5129
5319
|
const inputs = [];
|
|
5130
5320
|
for (const repo of config.repos) {
|
|
5131
|
-
const repoRoot =
|
|
5321
|
+
const repoRoot = path34.resolve(workspaceRoot, repo.path);
|
|
5132
5322
|
if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
|
|
5133
5323
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
5134
5324
|
}
|
|
@@ -5178,7 +5368,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
5178
5368
|
// src/cli.ts
|
|
5179
5369
|
function cliVersion() {
|
|
5180
5370
|
try {
|
|
5181
|
-
const pkgPath =
|
|
5371
|
+
const pkgPath = path35.join(packageRoot(), "package.json");
|
|
5182
5372
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5183
5373
|
return pkg.version ?? "0.0.0";
|
|
5184
5374
|
} catch {
|
|
@@ -5188,7 +5378,7 @@ function cliVersion() {
|
|
|
5188
5378
|
var program = new Command();
|
|
5189
5379
|
function validateRuntimeNodeVersion() {
|
|
5190
5380
|
try {
|
|
5191
|
-
const pkgPath =
|
|
5381
|
+
const pkgPath = path35.join(packageRoot(), "package.json");
|
|
5192
5382
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5193
5383
|
const requiredRange = pkg.engines?.node;
|
|
5194
5384
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|