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