@haus-tech/haus-workflow 0.26.1 → 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 +12 -0
- package/README.md +7 -4
- package/dist/cli.js +710 -466
- package/library/catalog/manifest.json +11 -9
- package/package.json +3 -3
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"] },
|
|
@@ -1190,275 +1519,77 @@ var SENSITIVE_ITEM_KEYWORDS = [
|
|
|
1190
1519
|
".pem",
|
|
1191
1520
|
".key"
|
|
1192
1521
|
];
|
|
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
|
-
}
|
|
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);
|
|
1522
|
+
|
|
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
|
}
|
|
@@ -3794,6 +3946,59 @@ function printApplyResult(result, dryRun) {
|
|
|
3794
3946
|
}
|
|
3795
3947
|
}
|
|
3796
3948
|
|
|
3949
|
+
// src/install/companion-tools.ts
|
|
3950
|
+
import { execaSync } from "execa";
|
|
3951
|
+
var COMPANION_TOOLS = [
|
|
3952
|
+
{
|
|
3953
|
+
bin: "caveman",
|
|
3954
|
+
label: "Caveman",
|
|
3955
|
+
blurb: "Ultra-compressed Claude Code responses \u2014 cuts output tokens ~75%.",
|
|
3956
|
+
installCmds: [
|
|
3957
|
+
"curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash"
|
|
3958
|
+
],
|
|
3959
|
+
url: "https://github.com/JuliusBrussee/caveman"
|
|
3960
|
+
},
|
|
3961
|
+
{
|
|
3962
|
+
bin: "rtk",
|
|
3963
|
+
label: "RTK (Rust Token Killer)",
|
|
3964
|
+
blurb: "Token-optimized CLI proxy \u2014 60-90% savings on dev operations.",
|
|
3965
|
+
installCmds: [
|
|
3966
|
+
"brew install rtk",
|
|
3967
|
+
"cargo install --git https://github.com/rtk-ai/rtk # if Homebrew is unavailable"
|
|
3968
|
+
],
|
|
3969
|
+
url: "https://github.com/rtk-ai/rtk",
|
|
3970
|
+
note: 'Another crate named "rtk" (Rust Type Kit) exists on crates.io \u2014 use the commands above, not a bare `cargo install rtk`.'
|
|
3971
|
+
}
|
|
3972
|
+
];
|
|
3973
|
+
function buildCompanionSuggestions(tools, isInstalled) {
|
|
3974
|
+
const missing = tools.filter((t) => !isInstalled(t.bin));
|
|
3975
|
+
if (missing.length === 0) return [];
|
|
3976
|
+
const lines = ["", "Optional token-saving tools you can add:"];
|
|
3977
|
+
for (const tool of missing) {
|
|
3978
|
+
lines.push("", ` ${tool.label} \u2014 ${tool.blurb}`);
|
|
3979
|
+
for (const cmd of tool.installCmds) {
|
|
3980
|
+
lines.push(` ${cmd}`);
|
|
3981
|
+
}
|
|
3982
|
+
if (tool.note) lines.push(` note: ${tool.note}`);
|
|
3983
|
+
lines.push(` ${tool.url}`);
|
|
3984
|
+
}
|
|
3985
|
+
return lines;
|
|
3986
|
+
}
|
|
3987
|
+
function commandExists(bin) {
|
|
3988
|
+
try {
|
|
3989
|
+
const r = execaSync("sh", ["-c", 'command -v -- "$1"', "probe", bin], { reject: false });
|
|
3990
|
+
return r.exitCode === 0;
|
|
3991
|
+
} catch {
|
|
3992
|
+
return false;
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
function printCompanionToolSuggestions(deps = {}) {
|
|
3996
|
+
const isInstalled = deps.isInstalled ?? commandExists;
|
|
3997
|
+
const emit2 = deps.log ?? log;
|
|
3998
|
+
const lines = buildCompanionSuggestions(COMPANION_TOOLS, isInstalled);
|
|
3999
|
+
for (const line2 of lines) emit2(line2);
|
|
4000
|
+
}
|
|
4001
|
+
|
|
3797
4002
|
// src/commands/install.ts
|
|
3798
4003
|
async function runInstall(options) {
|
|
3799
4004
|
try {
|
|
@@ -3806,6 +4011,7 @@ async function runInstall(options) {
|
|
|
3806
4011
|
if (options.check && result.drift) {
|
|
3807
4012
|
process.exitCode = 1;
|
|
3808
4013
|
} else if (!options.check && !options.dryRun) {
|
|
4014
|
+
printCompanionToolSuggestions();
|
|
3809
4015
|
const total = result.created.length + result.updated.length;
|
|
3810
4016
|
if (options.postinstall) {
|
|
3811
4017
|
log("haus configured Claude Code for you:");
|
|
@@ -3871,7 +4077,7 @@ async function runScan(options) {
|
|
|
3871
4077
|
|
|
3872
4078
|
// src/commands/undo.ts
|
|
3873
4079
|
import path22 from "path";
|
|
3874
|
-
import
|
|
4080
|
+
import fs17 from "fs-extra";
|
|
3875
4081
|
|
|
3876
4082
|
// src/claude/managed-paths.ts
|
|
3877
4083
|
var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md", "commands/haus-doctor.md"];
|
|
@@ -3893,25 +4099,25 @@ async function collectManagedPaths(root) {
|
|
|
3893
4099
|
}
|
|
3894
4100
|
const existing = [];
|
|
3895
4101
|
for (const abs of paths) {
|
|
3896
|
-
if (await
|
|
4102
|
+
if (await fs17.pathExists(abs)) existing.push(abs);
|
|
3897
4103
|
}
|
|
3898
4104
|
return existing;
|
|
3899
4105
|
}
|
|
3900
4106
|
async function settingsHasHausContent(root) {
|
|
3901
4107
|
const settingsPath = claudePath(root, "settings.json");
|
|
3902
|
-
if (!await
|
|
4108
|
+
if (!await fs17.pathExists(settingsPath)) return false;
|
|
3903
4109
|
const settings = await readProjectSettings(root);
|
|
3904
4110
|
return settings._haus != null;
|
|
3905
4111
|
}
|
|
3906
4112
|
async function claudeMdHasHausBlock(root) {
|
|
3907
4113
|
const filePath = path22.join(root, "CLAUDE.md");
|
|
3908
|
-
if (!await
|
|
3909
|
-
const text = await
|
|
4114
|
+
if (!await fs17.pathExists(filePath)) return false;
|
|
4115
|
+
const text = await fs17.readFile(filePath, "utf8");
|
|
3910
4116
|
return text.includes(BLOCK_BEGIN);
|
|
3911
4117
|
}
|
|
3912
4118
|
async function stripProjectSettings(root) {
|
|
3913
4119
|
const settingsPath = claudePath(root, "settings.json");
|
|
3914
|
-
if (!await
|
|
4120
|
+
if (!await fs17.pathExists(settingsPath)) return false;
|
|
3915
4121
|
let settings = await readProjectSettings(root);
|
|
3916
4122
|
settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
3917
4123
|
const hasContent = Object.keys(settings).length > 0;
|
|
@@ -3920,29 +4126,42 @@ async function stripProjectSettings(root) {
|
|
|
3920
4126
|
log(`Stripped haus rules from ${path22.relative(root, settingsPath)} (user settings preserved).`);
|
|
3921
4127
|
return true;
|
|
3922
4128
|
}
|
|
3923
|
-
await
|
|
4129
|
+
await fs17.remove(settingsPath);
|
|
3924
4130
|
log(`Removed ${path22.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
3925
4131
|
return true;
|
|
3926
4132
|
}
|
|
3927
4133
|
async function stripRootClaudeMd(root) {
|
|
3928
4134
|
const filePath = path22.join(root, "CLAUDE.md");
|
|
3929
|
-
if (!await
|
|
3930
|
-
const prev = await
|
|
4135
|
+
if (!await fs17.pathExists(filePath)) return false;
|
|
4136
|
+
const prev = await fs17.readFile(filePath, "utf8");
|
|
3931
4137
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
3932
4138
|
const next = stripHausBlock(prev);
|
|
3933
4139
|
if (next.length === 0) {
|
|
3934
|
-
await
|
|
4140
|
+
await fs17.remove(filePath);
|
|
3935
4141
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
3936
4142
|
} else {
|
|
3937
|
-
await
|
|
4143
|
+
await fs17.writeFile(filePath, next, "utf8");
|
|
3938
4144
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
3939
4145
|
}
|
|
3940
4146
|
return true;
|
|
3941
4147
|
}
|
|
3942
4148
|
async function pruneDirIfEmpty(dir) {
|
|
3943
|
-
if (!await
|
|
3944
|
-
const entries = await
|
|
3945
|
-
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)}.`);
|
|
3946
4165
|
}
|
|
3947
4166
|
async function runUndo(options) {
|
|
3948
4167
|
const root = process.cwd();
|
|
@@ -3968,9 +4187,10 @@ User-owned .claude/ files will be preserved.`
|
|
|
3968
4187
|
return;
|
|
3969
4188
|
}
|
|
3970
4189
|
}
|
|
4190
|
+
await backupManagedFilesBeforeUndo(root, managed);
|
|
3971
4191
|
for (const abs of managed) {
|
|
3972
|
-
if (!await
|
|
3973
|
-
await
|
|
4192
|
+
if (!await fs17.pathExists(abs)) continue;
|
|
4193
|
+
await fs17.remove(abs);
|
|
3974
4194
|
log(`Removed ${path22.relative(root, abs)}`);
|
|
3975
4195
|
}
|
|
3976
4196
|
if (stripSettings) await stripProjectSettings(root);
|
|
@@ -3983,7 +4203,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
3983
4203
|
// src/install/uninstall.ts
|
|
3984
4204
|
import crypto3 from "crypto";
|
|
3985
4205
|
import path23 from "path";
|
|
3986
|
-
import
|
|
4206
|
+
import fs18 from "fs-extra";
|
|
3987
4207
|
async function runUninstall(options = {}) {
|
|
3988
4208
|
const { force = false } = options;
|
|
3989
4209
|
const manifest = await readManifest();
|
|
@@ -3993,7 +4213,7 @@ async function runUninstall(options = {}) {
|
|
|
3993
4213
|
return result;
|
|
3994
4214
|
}
|
|
3995
4215
|
for (const entry of manifest.files) {
|
|
3996
|
-
const exists =
|
|
4216
|
+
const exists = fs18.pathExistsSync(entry.destPath);
|
|
3997
4217
|
if (!exists) continue;
|
|
3998
4218
|
const content2 = await readText(entry.destPath);
|
|
3999
4219
|
if (content2 === void 0) continue;
|
|
@@ -4011,7 +4231,7 @@ async function runUninstall(options = {}) {
|
|
|
4011
4231
|
result.skipped.push(entry.destPath);
|
|
4012
4232
|
continue;
|
|
4013
4233
|
}
|
|
4014
|
-
await
|
|
4234
|
+
await fs18.remove(entry.destPath);
|
|
4015
4235
|
await pruneEmptyDir(path23.dirname(entry.destPath));
|
|
4016
4236
|
result.deleted.push(entry.destPath);
|
|
4017
4237
|
}
|
|
@@ -4021,12 +4241,12 @@ async function runUninstall(options = {}) {
|
|
|
4021
4241
|
result.hooksStripped = true;
|
|
4022
4242
|
const hausDir = path23.join(globalClaudeDir(), "haus");
|
|
4023
4243
|
const manifestPath2 = hausManifestPath();
|
|
4024
|
-
if (
|
|
4025
|
-
await
|
|
4244
|
+
if (fs18.pathExistsSync(manifestPath2)) {
|
|
4245
|
+
await fs18.remove(manifestPath2);
|
|
4026
4246
|
}
|
|
4027
|
-
if (
|
|
4028
|
-
const remaining = await
|
|
4029
|
-
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);
|
|
4030
4250
|
}
|
|
4031
4251
|
return result;
|
|
4032
4252
|
}
|
|
@@ -4059,6 +4279,19 @@ async function runUninstallCommand(options) {
|
|
|
4059
4279
|
// src/commands/update.ts
|
|
4060
4280
|
import path25 from "path";
|
|
4061
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
|
+
|
|
4062
4295
|
// src/update/diff-generated-files.ts
|
|
4063
4296
|
function diffGeneratedFiles() {
|
|
4064
4297
|
return "Generated files may change in .claude/* and .haus-workflow/haus.lock.json. Review git diff before apply.";
|
|
@@ -4256,8 +4489,18 @@ async function detectGlobalInstallDrift() {
|
|
|
4256
4489
|
}
|
|
4257
4490
|
|
|
4258
4491
|
// src/commands/validate-catalog.ts
|
|
4492
|
+
import path27 from "path";
|
|
4493
|
+
|
|
4494
|
+
// src/catalog/validate-core.ts
|
|
4259
4495
|
import fs20 from "fs";
|
|
4260
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
|
+
}
|
|
4261
4504
|
function auditForbiddenStacks(items) {
|
|
4262
4505
|
const failures = [];
|
|
4263
4506
|
for (const item of items) {
|
|
@@ -4269,10 +4512,15 @@ function auditForbiddenStacks(items) {
|
|
|
4269
4512
|
}
|
|
4270
4513
|
return failures;
|
|
4271
4514
|
}
|
|
4272
|
-
function auditManifestStructure(items) {
|
|
4515
|
+
function auditManifestStructure(manifestVersion, items) {
|
|
4273
4516
|
const failures = [];
|
|
4274
4517
|
const seenIds = /* @__PURE__ */ new Map();
|
|
4275
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
|
+
}
|
|
4276
4524
|
for (let i = 0; i < items.length; i++) {
|
|
4277
4525
|
const item = items[i];
|
|
4278
4526
|
if (!item.id) {
|
|
@@ -4283,11 +4531,10 @@ function auditManifestStructure(items) {
|
|
|
4283
4531
|
failures.push(`${item.id}: missing type`);
|
|
4284
4532
|
continue;
|
|
4285
4533
|
}
|
|
4286
|
-
if (!item.source) {
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
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)`);
|
|
4291
4538
|
}
|
|
4292
4539
|
const prev = seenIds.get(item.id);
|
|
4293
4540
|
if (prev !== void 0) {
|
|
@@ -4298,14 +4545,13 @@ function auditManifestStructure(items) {
|
|
|
4298
4545
|
if (item.type === "skill" || item.type === "agent" || item.type === "template" || item.type === "command") {
|
|
4299
4546
|
if (!item.path) {
|
|
4300
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`);
|
|
4301
4550
|
} else {
|
|
4302
4551
|
const norm = item.path.replace(/\\/g, "/");
|
|
4303
4552
|
const existing = seenPaths.get(norm);
|
|
4304
|
-
if (existing) {
|
|
4305
|
-
|
|
4306
|
-
} else {
|
|
4307
|
-
seenPaths.set(norm, item.id);
|
|
4308
|
-
}
|
|
4553
|
+
if (existing) failures.push(`${item.id}: path "${norm}" already used by ${existing}`);
|
|
4554
|
+
else seenPaths.set(norm, item.id);
|
|
4309
4555
|
}
|
|
4310
4556
|
const isHaus = item.source === "haus";
|
|
4311
4557
|
const isCuratedApproved = item.source === "curated" && item.reviewStatus === "approved";
|
|
@@ -4313,8 +4559,13 @@ function auditManifestStructure(items) {
|
|
|
4313
4559
|
failures.push(`${item.id}: source must be "haus" or curated with reviewStatus "approved"`);
|
|
4314
4560
|
}
|
|
4315
4561
|
for (const ref of item.references ?? []) {
|
|
4316
|
-
if (
|
|
4317
|
-
|
|
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
|
+
}
|
|
4318
4569
|
}
|
|
4319
4570
|
}
|
|
4320
4571
|
}
|
|
@@ -4324,16 +4575,33 @@ function auditManifestStructure(items) {
|
|
|
4324
4575
|
function checkRequiredFrontmatter(text, label) {
|
|
4325
4576
|
const failures = [];
|
|
4326
4577
|
for (const key of REQUIRED_SKILL_FRONTMATTER) {
|
|
4327
|
-
if (!extractFrontmatterValue(text, key))
|
|
4578
|
+
if (!extractFrontmatterValue(text, key))
|
|
4328
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)`);
|
|
4329
4596
|
}
|
|
4330
4597
|
}
|
|
4598
|
+
failures.push(...auditForbiddenTagsInText(text, `${itemId}: ${rel}`));
|
|
4331
4599
|
return failures;
|
|
4332
4600
|
}
|
|
4333
4601
|
function auditShippedFiles(manifestDir, items) {
|
|
4334
4602
|
const failures = [];
|
|
4335
4603
|
for (const item of items) {
|
|
4336
|
-
if (!item.path) continue;
|
|
4604
|
+
if (!item.path || !isSafeCatalogPath2(item.path)) continue;
|
|
4337
4605
|
const absPath = path26.join(manifestDir, item.path);
|
|
4338
4606
|
if (item.type === "skill") {
|
|
4339
4607
|
const skillMd = path26.join(absPath, "SKILL.md");
|
|
@@ -4374,36 +4642,22 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4374
4642
|
}
|
|
4375
4643
|
return failures;
|
|
4376
4644
|
}
|
|
4377
|
-
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
4378
|
-
const rel = path26.relative(manifestDir, absPath);
|
|
4379
|
-
const text = fs20.readFileSync(absPath, "utf8");
|
|
4380
|
-
const failures = [];
|
|
4381
|
-
const lines = text.split(/\r?\n/);
|
|
4382
|
-
for (let i = 0; i < lines.length; i++) {
|
|
4383
|
-
const line2 = lines[i] ?? "";
|
|
4384
|
-
if (PLACEHOLDER_PATTERN.test(line2)) {
|
|
4385
|
-
failures.push(`${rel}:${i + 1}: TODO or placeholder in shipped content`);
|
|
4386
|
-
}
|
|
4387
|
-
if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2))) {
|
|
4388
|
-
failures.push(`${rel}:${i + 1}: risky install pattern`);
|
|
4389
|
-
}
|
|
4390
|
-
if (ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
|
|
4391
|
-
failures.push(`${rel}:${i + 1}: disallowed npx (only npx tsx allowed)`);
|
|
4392
|
-
}
|
|
4393
|
-
}
|
|
4394
|
-
failures.push(...auditForbiddenTagsInText(text, `${itemId}: ${rel}`));
|
|
4395
|
-
return failures;
|
|
4396
|
-
}
|
|
4397
4645
|
var DIR_ITEM_TYPE = {
|
|
4398
4646
|
skills: "skill",
|
|
4399
4647
|
agents: "agent",
|
|
4400
4648
|
templates: "template",
|
|
4401
4649
|
commands: "command"
|
|
4402
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
|
+
}
|
|
4403
4658
|
function auditMarkdownContent(manifestDir) {
|
|
4404
4659
|
const failures = [];
|
|
4405
|
-
const
|
|
4406
|
-
for (const dir of dirs) {
|
|
4660
|
+
for (const dir of ["skills", "agents", "templates", "commands"]) {
|
|
4407
4661
|
const abs = path26.join(manifestDir, dir);
|
|
4408
4662
|
if (!fs20.existsSync(abs)) continue;
|
|
4409
4663
|
const checkNonTsxNpx = !NPX_TSX_ONLY_EXEMPT_TYPES.includes(DIR_ITEM_TYPE[dir] ?? "");
|
|
@@ -4413,65 +4667,55 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4413
4667
|
const lines = text.split(/\r?\n/);
|
|
4414
4668
|
for (let i = 0; i < lines.length; i++) {
|
|
4415
4669
|
const line2 = lines[i] ?? "";
|
|
4416
|
-
if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2)))
|
|
4670
|
+
if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2)))
|
|
4417
4671
|
failures.push(`${rel}:${i + 1}: risky install pattern`);
|
|
4418
|
-
}
|
|
4419
4672
|
if (checkNonTsxNpx && ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
|
|
4420
4673
|
failures.push(`${rel}:${i + 1}: disallowed npx (only npx tsx allowed)`);
|
|
4421
4674
|
}
|
|
4422
4675
|
}
|
|
4423
|
-
if (!rel.includes("/references/"))
|
|
4424
|
-
failures.push(...auditForbiddenTagsInText(text, rel));
|
|
4425
|
-
}
|
|
4676
|
+
if (!rel.includes("/references/")) failures.push(...auditForbiddenTagsInText(text, rel));
|
|
4426
4677
|
});
|
|
4427
4678
|
}
|
|
4428
4679
|
return failures;
|
|
4429
4680
|
}
|
|
4430
|
-
function
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
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 };
|
|
4436
4690
|
}
|
|
4691
|
+
|
|
4692
|
+
// src/commands/validate-catalog.ts
|
|
4437
4693
|
async function runValidateCatalog(manifestPath2) {
|
|
4438
4694
|
if (!manifestPath2) {
|
|
4439
4695
|
error("Usage: haus validate-catalog <path/to/manifest.json>");
|
|
4440
4696
|
process.exitCode = 1;
|
|
4441
4697
|
return;
|
|
4442
4698
|
}
|
|
4443
|
-
const abs =
|
|
4444
|
-
const manifestDir =
|
|
4699
|
+
const abs = path27.resolve(process.cwd(), manifestPath2);
|
|
4700
|
+
const manifestDir = path27.dirname(abs);
|
|
4445
4701
|
const data = await readJson(abs);
|
|
4446
4702
|
if (!data?.items) {
|
|
4447
4703
|
error(`Could not read catalog manifest at ${abs}`);
|
|
4448
4704
|
process.exitCode = 1;
|
|
4449
4705
|
return;
|
|
4450
4706
|
}
|
|
4451
|
-
const
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
const fileFailures = auditShippedFiles(manifestDir, items);
|
|
4455
|
-
const contentFailures = auditMarkdownContent(manifestDir);
|
|
4456
|
-
const tagFailures = auditDisallowedTags(items);
|
|
4457
|
-
const allFailures = [
|
|
4458
|
-
...structureFailures,
|
|
4459
|
-
...stackFailures,
|
|
4460
|
-
...fileFailures,
|
|
4461
|
-
...contentFailures,
|
|
4462
|
-
...tagFailures
|
|
4463
|
-
];
|
|
4464
|
-
if (allFailures.length) {
|
|
4465
|
-
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));
|
|
4466
4710
|
process.exitCode = 1;
|
|
4467
4711
|
return;
|
|
4468
4712
|
}
|
|
4469
|
-
log(`Catalog valid. ${items.length} items checked.`);
|
|
4713
|
+
log(`Catalog valid. ${result.items.length} items checked.`);
|
|
4470
4714
|
}
|
|
4471
4715
|
|
|
4472
4716
|
// src/commands/workspace.ts
|
|
4473
4717
|
import { existsSync as existsSync5, statSync as statSync2 } from "fs";
|
|
4474
|
-
import
|
|
4718
|
+
import path34 from "path";
|
|
4475
4719
|
|
|
4476
4720
|
// src/commands/workspace/aggregate.ts
|
|
4477
4721
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -4525,7 +4769,7 @@ ${summaries.map(
|
|
|
4525
4769
|
}
|
|
4526
4770
|
|
|
4527
4771
|
// src/commands/workspace/config.ts
|
|
4528
|
-
import
|
|
4772
|
+
import path28 from "path";
|
|
4529
4773
|
import YAML from "yaml";
|
|
4530
4774
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
4531
4775
|
function parseWorkspaceConfig(text) {
|
|
@@ -4548,11 +4792,11 @@ function parseWorkspaceConfig(text) {
|
|
|
4548
4792
|
};
|
|
4549
4793
|
}
|
|
4550
4794
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
4551
|
-
return parseWorkspaceConfig(await readText(
|
|
4795
|
+
return parseWorkspaceConfig(await readText(path28.join(workspaceRoot, WORKSPACE_FILE)));
|
|
4552
4796
|
}
|
|
4553
4797
|
|
|
4554
4798
|
// src/commands/workspace/discover.ts
|
|
4555
|
-
import
|
|
4799
|
+
import path29 from "path";
|
|
4556
4800
|
import fg4 from "fast-glob";
|
|
4557
4801
|
import YAML2 from "yaml";
|
|
4558
4802
|
var DEFAULT_MAX_DEPTH = 3;
|
|
@@ -4580,8 +4824,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
4580
4824
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
4581
4825
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
4582
4826
|
for (const match of matches) {
|
|
4583
|
-
const base =
|
|
4584
|
-
const dir =
|
|
4827
|
+
const base = path29.posix.basename(match);
|
|
4828
|
+
const dir = path29.posix.dirname(match);
|
|
4585
4829
|
const owner = dir === "." ? "." : dir;
|
|
4586
4830
|
if (base === ".git") gitDirs.add(owner);
|
|
4587
4831
|
else manifestDirs.add(owner);
|
|
@@ -4597,9 +4841,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
4597
4841
|
}
|
|
4598
4842
|
repoRoots.sort((a, b) => a.localeCompare(b));
|
|
4599
4843
|
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
4600
|
-
const absDir =
|
|
4601
|
-
const pkg = await readJson(
|
|
4602
|
-
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);
|
|
4603
4847
|
let role = "auto";
|
|
4604
4848
|
try {
|
|
4605
4849
|
const scan = await scanProject(absDir);
|
|
@@ -4642,7 +4886,7 @@ function renderWorkspaceYaml(config) {
|
|
|
4642
4886
|
});
|
|
4643
4887
|
}
|
|
4644
4888
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
4645
|
-
const yamlPath =
|
|
4889
|
+
const yamlPath = path29.join(workspaceRoot, "haus.workspace.yaml");
|
|
4646
4890
|
const existingText = await readText(yamlPath);
|
|
4647
4891
|
const existing = parseWorkspaceConfig(existingText);
|
|
4648
4892
|
if (existingText && !existing) {
|
|
@@ -4676,18 +4920,18 @@ async function runDiscover(workspaceRoot, opts = {}) {
|
|
|
4676
4920
|
|
|
4677
4921
|
// src/commands/workspace/doctor.ts
|
|
4678
4922
|
import { existsSync as existsSync3 } from "fs";
|
|
4679
|
-
import
|
|
4923
|
+
import path31 from "path";
|
|
4680
4924
|
|
|
4681
4925
|
// src/commands/workspace/manifest.ts
|
|
4682
4926
|
import { readFileSync as readFileSync3 } from "fs";
|
|
4683
|
-
import
|
|
4927
|
+
import path30 from "path";
|
|
4684
4928
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
4685
4929
|
function manifestPath(workspaceRoot) {
|
|
4686
4930
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
4687
4931
|
}
|
|
4688
4932
|
function hausVersion() {
|
|
4689
4933
|
try {
|
|
4690
|
-
const pkg = JSON.parse(readFileSync3(
|
|
4934
|
+
const pkg = JSON.parse(readFileSync3(path30.join(packageRoot(), "package.json"), "utf8"));
|
|
4691
4935
|
return pkg.version ?? "0.0.0";
|
|
4692
4936
|
} catch {
|
|
4693
4937
|
return "0.0.0";
|
|
@@ -4753,7 +4997,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
4753
4997
|
}
|
|
4754
4998
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
4755
4999
|
for (const repo of config.repos) {
|
|
4756
|
-
const repoRoot =
|
|
5000
|
+
const repoRoot = path31.resolve(workspaceRoot, repo.path);
|
|
4757
5001
|
const entry = manifestByName.get(repo.name);
|
|
4758
5002
|
if (!entry) {
|
|
4759
5003
|
flag({
|
|
@@ -4827,10 +5071,10 @@ function emit(args) {
|
|
|
4827
5071
|
|
|
4828
5072
|
// src/commands/workspace/setup.ts
|
|
4829
5073
|
import { existsSync as existsSync4, statSync } from "fs";
|
|
4830
|
-
import
|
|
5074
|
+
import path33 from "path";
|
|
4831
5075
|
|
|
4832
5076
|
// src/claude/write-workspace-claude-md.ts
|
|
4833
|
-
import
|
|
5077
|
+
import path32 from "path";
|
|
4834
5078
|
import fs21 from "fs-extra";
|
|
4835
5079
|
function buildWorkspaceImportBlock(client, members) {
|
|
4836
5080
|
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
@@ -4849,7 +5093,7 @@ ${BLOCK_END}`;
|
|
|
4849
5093
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
4850
5094
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
4851
5095
|
const dryRun = opts.dryRun ?? false;
|
|
4852
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
5096
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path32.join(workspaceRoot, "CLAUDE.md");
|
|
4853
5097
|
const prev = await fs21.pathExists(filePath) ? await fs21.readFile(filePath, "utf8") : "";
|
|
4854
5098
|
const next = opts.collision ? `${block}
|
|
4855
5099
|
` : injectHausBlock(prev, block);
|
|
@@ -4875,20 +5119,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
4875
5119
|
|
|
4876
5120
|
// src/commands/workspace/setup.ts
|
|
4877
5121
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
4878
|
-
let dir =
|
|
5122
|
+
let dir = path33.resolve(start);
|
|
4879
5123
|
for (; ; ) {
|
|
4880
|
-
if (existsSync4(
|
|
4881
|
-
const parent =
|
|
4882
|
-
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);
|
|
4883
5127
|
dir = parent;
|
|
4884
5128
|
}
|
|
4885
5129
|
}
|
|
4886
5130
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
4887
|
-
return
|
|
5131
|
+
return path33.resolve(workspaceRoot, repoPath) === path33.resolve(workspaceRoot);
|
|
4888
5132
|
}
|
|
4889
5133
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
4890
5134
|
const apply = options.write ?? false;
|
|
4891
|
-
const configText = await readText(
|
|
5135
|
+
const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
|
|
4892
5136
|
if (!configText) {
|
|
4893
5137
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
4894
5138
|
process.exitCode = 1;
|
|
@@ -4905,7 +5149,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
4905
5149
|
const statuses = [];
|
|
4906
5150
|
const aggregateInputs = [];
|
|
4907
5151
|
for (const repo of repos) {
|
|
4908
|
-
const repoRoot =
|
|
5152
|
+
const repoRoot = path33.resolve(workspaceRoot, repo.path);
|
|
4909
5153
|
log(`
|
|
4910
5154
|
\u2192 ${repo.name} (${repo.path})`);
|
|
4911
5155
|
try {
|
|
@@ -4973,7 +5217,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
4973
5217
|
const status = statusByName.get(repo.name);
|
|
4974
5218
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
4975
5219
|
if (status?.status === "ok") {
|
|
4976
|
-
const lock = await checkLock(
|
|
5220
|
+
const lock = await checkLock(path33.resolve(workspaceRoot, repo.path));
|
|
4977
5221
|
manifestRepos.push({
|
|
4978
5222
|
name: repo.name,
|
|
4979
5223
|
path: repo.path,
|
|
@@ -5053,7 +5297,7 @@ relationships: []
|
|
|
5053
5297
|
log("Workspace initialized.");
|
|
5054
5298
|
}
|
|
5055
5299
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
5056
|
-
const configText = await readText(
|
|
5300
|
+
const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
|
|
5057
5301
|
if (!configText) {
|
|
5058
5302
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
5059
5303
|
process.exitCode = 1;
|
|
@@ -5074,7 +5318,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
5074
5318
|
}
|
|
5075
5319
|
const inputs = [];
|
|
5076
5320
|
for (const repo of config.repos) {
|
|
5077
|
-
const repoRoot =
|
|
5321
|
+
const repoRoot = path34.resolve(workspaceRoot, repo.path);
|
|
5078
5322
|
if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
|
|
5079
5323
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
5080
5324
|
}
|
|
@@ -5124,7 +5368,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
5124
5368
|
// src/cli.ts
|
|
5125
5369
|
function cliVersion() {
|
|
5126
5370
|
try {
|
|
5127
|
-
const pkgPath =
|
|
5371
|
+
const pkgPath = path35.join(packageRoot(), "package.json");
|
|
5128
5372
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5129
5373
|
return pkg.version ?? "0.0.0";
|
|
5130
5374
|
} catch {
|
|
@@ -5134,7 +5378,7 @@ function cliVersion() {
|
|
|
5134
5378
|
var program = new Command();
|
|
5135
5379
|
function validateRuntimeNodeVersion() {
|
|
5136
5380
|
try {
|
|
5137
|
-
const pkgPath =
|
|
5381
|
+
const pkgPath = path35.join(packageRoot(), "package.json");
|
|
5138
5382
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5139
5383
|
const requiredRange = pkg.engines?.node;
|
|
5140
5384
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|