@haus-tech/haus-workflow 0.1.0 → 0.2.2
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 +135 -0
- package/README.md +35 -26
- package/dist/cli.js +297 -107
- package/library/catalog/haus-lock.schema.json +2 -41
- package/library/catalog/manifest.json +742 -199
- package/package.json +5 -4
- package/tests/fixtures/catalog/manifest.json +35 -35
package/dist/cli.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { readFileSync as readFileSync3 } from "fs";
|
|
5
|
-
import
|
|
5
|
+
import path26 from "path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
|
-
// src/
|
|
9
|
-
import
|
|
10
|
-
import
|
|
8
|
+
// src/commands/apply.ts
|
|
9
|
+
import path10 from "path";
|
|
10
|
+
import checkbox from "@inquirer/checkbox";
|
|
11
11
|
|
|
12
12
|
// src/catalog/remote-catalog.ts
|
|
13
13
|
import os from "os";
|
|
@@ -27,7 +27,7 @@ var error = (msg, ...args) => {
|
|
|
27
27
|
|
|
28
28
|
// src/catalog/constants.ts
|
|
29
29
|
var CATALOG_REPO_URL = "https://raw.githubusercontent.com/wearehaustech/haus-workflow-catalog";
|
|
30
|
-
var CATALOG_REF = "main";
|
|
30
|
+
var CATALOG_REF = process.env.HAUS_CATALOG_REF ?? "main";
|
|
31
31
|
var CATALOG_CACHE_SUBDIR = ".claude/haus/catalog-cache";
|
|
32
32
|
|
|
33
33
|
// src/catalog/remote-catalog.ts
|
|
@@ -129,6 +129,21 @@ async function syncRemoteCatalog() {
|
|
|
129
129
|
}
|
|
130
130
|
return { newItems, unchanged, failed };
|
|
131
131
|
}
|
|
132
|
+
var CATALOG_TAGS_API_URL = "https://api.github.com/repos/WeAreHausTech/haus-workflow-catalog/tags";
|
|
133
|
+
async function fetchLatestCatalogTag() {
|
|
134
|
+
if (process.env["HAUS_CATALOG_REMOTE_BASE"]) return null;
|
|
135
|
+
try {
|
|
136
|
+
const res = await fetch(CATALOG_TAGS_API_URL, {
|
|
137
|
+
signal: AbortSignal.timeout(5e3),
|
|
138
|
+
headers: { Accept: "application/vnd.github+json" }
|
|
139
|
+
});
|
|
140
|
+
if (!res.ok) return null;
|
|
141
|
+
const tags = await res.json();
|
|
142
|
+
return tags[0]?.name ?? null;
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
132
147
|
async function getCacheManifestAge() {
|
|
133
148
|
try {
|
|
134
149
|
const stat = await fs.stat(path.join(CACHE_DIR, "manifest.json"));
|
|
@@ -138,6 +153,10 @@ async function getCacheManifestAge() {
|
|
|
138
153
|
}
|
|
139
154
|
}
|
|
140
155
|
|
|
156
|
+
// src/claude/write-claude-files.ts
|
|
157
|
+
import path9 from "path";
|
|
158
|
+
import fs8 from "fs-extra";
|
|
159
|
+
|
|
141
160
|
// src/update/hash-installed.ts
|
|
142
161
|
import path3 from "path";
|
|
143
162
|
import fg2 from "fast-glob";
|
|
@@ -605,7 +624,7 @@ ${templateContent}`;
|
|
|
605
624
|
}
|
|
606
625
|
|
|
607
626
|
// src/claude/write-claude-files.ts
|
|
608
|
-
async function writeClaudeFiles(root, dryRun) {
|
|
627
|
+
async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
609
628
|
const rec = await readJson(hausPath(root, "recommendation.json")) ?? {
|
|
610
629
|
mode: "fast",
|
|
611
630
|
recommended: [],
|
|
@@ -656,15 +675,17 @@ async function writeClaudeFiles(root, dryRun) {
|
|
|
656
675
|
"- Never read secrets.\n- Block dangerous shell commands.\n",
|
|
657
676
|
dryRun
|
|
658
677
|
);
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
678
|
+
const fixtureManifestPath = process.env["HAUS_FIXTURE_CATALOG"];
|
|
679
|
+
const manifestPath = fixtureManifestPath ?? path9.join(pkgRoot, "library", "catalog", "manifest.json");
|
|
680
|
+
const manifestDir = path9.dirname(manifestPath);
|
|
681
|
+
const manifest = await readJson(manifestPath) ?? { items: [] };
|
|
662
682
|
const manifestById = new Map((manifest.items ?? []).map((item) => [item.id, item]));
|
|
663
683
|
const cacheManifest = await readJson(path9.join(CACHE_DIR, "manifest.json"));
|
|
664
684
|
const cacheManifestById = new Map((cacheManifest?.items ?? []).map((item) => [item.id, item]));
|
|
665
685
|
const installedPathsByItem = /* @__PURE__ */ new Map();
|
|
666
686
|
const installedIds = /* @__PURE__ */ new Set();
|
|
667
|
-
|
|
687
|
+
const catalogItems = selectedIds !== void 0 ? rec.recommended.filter((r) => selectedIds.includes(r.id)) : rec.recommended;
|
|
688
|
+
for (const item of catalogItems) {
|
|
668
689
|
const manifestItem = manifestById.get(item.id);
|
|
669
690
|
if (!manifestItem?.path) continue;
|
|
670
691
|
if (manifestItem.source === "curated") {
|
|
@@ -681,8 +702,8 @@ async function writeClaudeFiles(root, dryRun) {
|
|
|
681
702
|
}
|
|
682
703
|
const cachedItem = cacheManifestById.get(item.id);
|
|
683
704
|
const cachePath = cachedItem?.path ? path9.join(CACHE_DIR, cachedItem.path) : null;
|
|
684
|
-
const sourcePath = cachePath && await fs8.pathExists(cachePath) ? cachePath : path9.join(
|
|
685
|
-
const target = item.type === "agent" ? "agents" : "skills";
|
|
705
|
+
const sourcePath = cachePath && await fs8.pathExists(cachePath) ? cachePath : path9.join(manifestDir, manifestItem.path);
|
|
706
|
+
const target = item.type === "agent" ? "agents" : item.type === "template" ? "templates" : "skills";
|
|
686
707
|
const destination = claudePath(root, target, path9.basename(sourcePath));
|
|
687
708
|
if (await fs8.pathExists(sourcePath)) {
|
|
688
709
|
if (dryRun) {
|
|
@@ -701,7 +722,7 @@ async function writeClaudeFiles(root, dryRun) {
|
|
|
701
722
|
}
|
|
702
723
|
}
|
|
703
724
|
if (dryRun) return [...new Set(files)];
|
|
704
|
-
const installedItems =
|
|
725
|
+
const installedItems = catalogItems.filter((r) => installedIds.has(r.id));
|
|
705
726
|
await writeManagedJson(
|
|
706
727
|
root,
|
|
707
728
|
hausPath(root, "selected-context.json"),
|
|
@@ -718,6 +739,7 @@ async function writeClaudeFiles(root, dryRun) {
|
|
|
718
739
|
type: r.type,
|
|
719
740
|
source: isCurated ? "curated" : "haus",
|
|
720
741
|
version: hausVersion,
|
|
742
|
+
catalogRef: CATALOG_REF,
|
|
721
743
|
hash: await hashInstalledPaths(root, relPaths),
|
|
722
744
|
installMode: "copied",
|
|
723
745
|
paths: relPaths
|
|
@@ -763,6 +785,10 @@ async function writeManagedJson(root, filePath, value, dryRun) {
|
|
|
763
785
|
}
|
|
764
786
|
|
|
765
787
|
// src/commands/apply.ts
|
|
788
|
+
async function cacheHasItems() {
|
|
789
|
+
const data = await readJson(path10.join(CACHE_DIR, "manifest.json"));
|
|
790
|
+
return Array.isArray(data?.items) && data.items.length > 0;
|
|
791
|
+
}
|
|
766
792
|
async function runApply(options) {
|
|
767
793
|
if (!options.dryRun && !options.write) {
|
|
768
794
|
log("Use --dry-run or --write");
|
|
@@ -770,7 +796,52 @@ async function runApply(options) {
|
|
|
770
796
|
}
|
|
771
797
|
const root = process.cwd();
|
|
772
798
|
const isDryRun = Boolean(options.dryRun) && !options.write;
|
|
773
|
-
|
|
799
|
+
let selectedIds;
|
|
800
|
+
if (options.select) {
|
|
801
|
+
if (!process.stdin.isTTY) {
|
|
802
|
+
error("--select requires an interactive terminal (stdin is not a TTY)");
|
|
803
|
+
process.exitCode = 1;
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const rec = await readJson(hausPath(root, "recommendation.json"));
|
|
807
|
+
if (!rec) {
|
|
808
|
+
log("No recommendation.json found \u2014 run `haus recommend` first. Writing core files only.");
|
|
809
|
+
selectedIds = [];
|
|
810
|
+
} else if (rec.recommended.length === 0) {
|
|
811
|
+
log("Recommendation contains no catalog items. Writing core files only.");
|
|
812
|
+
selectedIds = [];
|
|
813
|
+
} else {
|
|
814
|
+
const items = rec.recommended;
|
|
815
|
+
const choices = items.map((item) => ({
|
|
816
|
+
name: `${item.id} [${item.confidenceLevel}] \u2014 ${item.reason}`,
|
|
817
|
+
value: item.id,
|
|
818
|
+
checked: true
|
|
819
|
+
}));
|
|
820
|
+
const chosen = await checkbox({
|
|
821
|
+
message: "Select catalog items to apply (space to toggle, enter to confirm):",
|
|
822
|
+
choices,
|
|
823
|
+
pageSize: Math.min(20, items.length + 2)
|
|
824
|
+
});
|
|
825
|
+
selectedIds = chosen;
|
|
826
|
+
log(`Selected ${selectedIds.length} of ${items.length} catalog items.`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (!options.allowEmptyCache && !process.env["HAUS_FIXTURE_CATALOG"]) {
|
|
830
|
+
const rec = await readJson(hausPath(root, "recommendation.json"));
|
|
831
|
+
const catalogItemCount = selectedIds !== void 0 ? selectedIds.length : rec?.recommended.length ?? 0;
|
|
832
|
+
if (catalogItemCount > 0 && !await cacheHasItems()) {
|
|
833
|
+
if (isDryRun) {
|
|
834
|
+
warn("Catalog cache is empty \u2014 `haus apply --write` will skip catalog items. Run `haus update` first.");
|
|
835
|
+
} else {
|
|
836
|
+
error(
|
|
837
|
+
"Catalog cache is empty \u2014 cannot install catalog items. Run `haus update` first, or pass --allow-empty-cache to apply core files only."
|
|
838
|
+
);
|
|
839
|
+
process.exitCode = 1;
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
const files = await writeClaudeFiles(root, isDryRun, selectedIds);
|
|
774
845
|
if (isDryRun) {
|
|
775
846
|
log(`Dry-run complete \u2014 ${files.length} file(s) planned, none written. Run --write to apply.`);
|
|
776
847
|
} else {
|
|
@@ -781,8 +852,8 @@ async function runApply(options) {
|
|
|
781
852
|
|
|
782
853
|
// src/catalog/load-catalog.ts
|
|
783
854
|
import os4 from "os";
|
|
784
|
-
import
|
|
785
|
-
var CACHE_MANIFEST =
|
|
855
|
+
import path11 from "path";
|
|
856
|
+
var CACHE_MANIFEST = path11.join(os4.homedir(), CATALOG_CACHE_SUBDIR, "manifest.json");
|
|
786
857
|
async function loadCatalog(root) {
|
|
787
858
|
const envPath = process.env["HAUS_FIXTURE_CATALOG"];
|
|
788
859
|
if (envPath) {
|
|
@@ -791,10 +862,10 @@ async function loadCatalog(root) {
|
|
|
791
862
|
}
|
|
792
863
|
const cacheData = await readJson(CACHE_MANIFEST);
|
|
793
864
|
if (cacheData?.items?.length) return cacheData.items;
|
|
794
|
-
const localManifest =
|
|
865
|
+
const localManifest = path11.join(root, "library/catalog/manifest.json");
|
|
795
866
|
const localData = await readJson(localManifest);
|
|
796
867
|
if (localData?.items?.length) return localData.items;
|
|
797
|
-
const packageManifest =
|
|
868
|
+
const packageManifest = path11.join(packageRoot(), "library/catalog/manifest.json");
|
|
798
869
|
const data = await readJson(packageManifest);
|
|
799
870
|
return data?.items ?? [];
|
|
800
871
|
}
|
|
@@ -1148,6 +1219,9 @@ function computeRuleIntents(rule) {
|
|
|
1148
1219
|
intents.add("admin-ui");
|
|
1149
1220
|
intents.add("storefront");
|
|
1150
1221
|
}
|
|
1222
|
+
if (eco === "tailwind" || eco === "vite") {
|
|
1223
|
+
intents.add("frontend");
|
|
1224
|
+
}
|
|
1151
1225
|
if (eco === "nx" || eco === "turbo") {
|
|
1152
1226
|
intents.add("monorepo");
|
|
1153
1227
|
}
|
|
@@ -1167,7 +1241,7 @@ function computeRuleIntents(rule) {
|
|
|
1167
1241
|
|
|
1168
1242
|
// src/scanner/scan-project.ts
|
|
1169
1243
|
import { readFile } from "fs/promises";
|
|
1170
|
-
import
|
|
1244
|
+
import path13 from "path";
|
|
1171
1245
|
|
|
1172
1246
|
// src/utils/audit-checks.ts
|
|
1173
1247
|
function isRecord(v) {
|
|
@@ -1194,7 +1268,7 @@ function compareVersions(a, b) {
|
|
|
1194
1268
|
}
|
|
1195
1269
|
|
|
1196
1270
|
// src/scanner/detect-package-manager.ts
|
|
1197
|
-
import
|
|
1271
|
+
import path12 from "path";
|
|
1198
1272
|
import fs9 from "fs-extra";
|
|
1199
1273
|
function detectPackageManager(root, packageManagerField) {
|
|
1200
1274
|
const field = String(packageManagerField ?? "").trim();
|
|
@@ -1213,9 +1287,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
1213
1287
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
1214
1288
|
return "unknown";
|
|
1215
1289
|
}
|
|
1216
|
-
if (fs9.existsSync(
|
|
1217
|
-
if (fs9.existsSync(
|
|
1218
|
-
if (fs9.existsSync(
|
|
1290
|
+
if (fs9.existsSync(path12.join(root, "yarn.lock"))) return "yarn";
|
|
1291
|
+
if (fs9.existsSync(path12.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
1292
|
+
if (fs9.existsSync(path12.join(root, "package-lock.json"))) return "npm";
|
|
1219
1293
|
return "unknown";
|
|
1220
1294
|
}
|
|
1221
1295
|
|
|
@@ -1276,8 +1350,8 @@ function blocked(rel) {
|
|
|
1276
1350
|
return SENSITIVE.some((x) => x.test(rel));
|
|
1277
1351
|
}
|
|
1278
1352
|
async function scanProject(root, mode = "fast") {
|
|
1279
|
-
const pkg = await readJson(
|
|
1280
|
-
const composer = await readJson(
|
|
1353
|
+
const pkg = await readJson(path13.join(root, "package.json"));
|
|
1354
|
+
const composer = await readJson(path13.join(root, "composer.json"));
|
|
1281
1355
|
const files = await listFiles(root, SAFE_FILES);
|
|
1282
1356
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
1283
1357
|
const deps = dependencySet(pkg, composer);
|
|
@@ -1303,7 +1377,7 @@ async function scanProject(root, mode = "fast") {
|
|
|
1303
1377
|
mode,
|
|
1304
1378
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1305
1379
|
root,
|
|
1306
|
-
repoName: String(pkg?.name ??
|
|
1380
|
+
repoName: String(pkg?.name ?? path13.basename(root)),
|
|
1307
1381
|
packageManager,
|
|
1308
1382
|
repoRoles: roles,
|
|
1309
1383
|
confidence: computeConfidence(roles, stacks),
|
|
@@ -1318,7 +1392,7 @@ async function scanProject(root, mode = "fast") {
|
|
|
1318
1392
|
composer: isRecord(composer?.require) ? Object.keys(composer.require) : []
|
|
1319
1393
|
};
|
|
1320
1394
|
const scanHashes = Object.fromEntries(
|
|
1321
|
-
await Promise.all(safeFiles.map(async (f) => [f, hashText(await readFile(
|
|
1395
|
+
await Promise.all(safeFiles.map(async (f) => [f, hashText(await readFile(path13.join(root, f), "utf8"))]))
|
|
1322
1396
|
);
|
|
1323
1397
|
const repoSummary = renderSummary(context);
|
|
1324
1398
|
await writeJson(hausPath(root, "context-map.json"), context);
|
|
@@ -1415,7 +1489,7 @@ async function hasNeedle(root, files, needle) {
|
|
|
1415
1489
|
);
|
|
1416
1490
|
for (const rel of candidates.slice(0, 300)) {
|
|
1417
1491
|
try {
|
|
1418
|
-
const content = await readFile(
|
|
1492
|
+
const content = await readFile(path13.join(root, rel), "utf8");
|
|
1419
1493
|
if (content.includes(needle)) return true;
|
|
1420
1494
|
} catch {
|
|
1421
1495
|
continue;
|
|
@@ -1508,8 +1582,30 @@ async function runContext(options) {
|
|
|
1508
1582
|
}
|
|
1509
1583
|
|
|
1510
1584
|
// src/commands/doctor.ts
|
|
1511
|
-
import
|
|
1585
|
+
import path14 from "path";
|
|
1512
1586
|
import fs10 from "fs-extra";
|
|
1587
|
+
|
|
1588
|
+
// src/update/npm-version.ts
|
|
1589
|
+
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
1590
|
+
async function fetchNpmVersionStatus(currentVersion) {
|
|
1591
|
+
try {
|
|
1592
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE_NAME)}/latest`, {
|
|
1593
|
+
signal: AbortSignal.timeout(8e3)
|
|
1594
|
+
});
|
|
1595
|
+
if (!res.ok) return { current: currentVersion, latest: null, updateAvailable: false };
|
|
1596
|
+
const data = await res.json();
|
|
1597
|
+
const latest = data?.version;
|
|
1598
|
+
if (!latest || !normalizeVersion(latest) || !normalizeVersion(currentVersion)) {
|
|
1599
|
+
return { current: currentVersion, latest: null, updateAvailable: false };
|
|
1600
|
+
}
|
|
1601
|
+
const updateAvailable = compareVersions(latest, currentVersion) > 0;
|
|
1602
|
+
return { current: currentVersion, latest, updateAvailable };
|
|
1603
|
+
} catch {
|
|
1604
|
+
return { current: currentVersion, latest: null, updateAvailable: false };
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// src/commands/doctor.ts
|
|
1513
1609
|
async function runDoctor(options) {
|
|
1514
1610
|
const root = process.cwd();
|
|
1515
1611
|
if (options?.hooks) {
|
|
@@ -1554,7 +1650,7 @@ async function runDoctor(options) {
|
|
|
1554
1650
|
const enabled = await isHookEnabled(root, key);
|
|
1555
1651
|
log(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
|
|
1556
1652
|
}
|
|
1557
|
-
const rootClaudeMdPath =
|
|
1653
|
+
const rootClaudeMdPath = path14.join(root, "CLAUDE.md");
|
|
1558
1654
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
1559
1655
|
if (!rootClaudeMdContent) {
|
|
1560
1656
|
warn("- CLAUDE.md: missing (run `haus apply --write` to create)");
|
|
@@ -1574,7 +1670,7 @@ async function runDoctor(options) {
|
|
|
1574
1670
|
warn("- .haus-workflow/haus-way-of-work.md: no HAUS-MANAGED header (user-owned)");
|
|
1575
1671
|
} else {
|
|
1576
1672
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
1577
|
-
const templatePath =
|
|
1673
|
+
const templatePath = path14.join(packageRoot(), "library", "global", "templates", "haus-way-of-work.md");
|
|
1578
1674
|
const templateContent = await readText(templatePath);
|
|
1579
1675
|
if (storedHashMatch && templateContent) {
|
|
1580
1676
|
const currentHash = hashText(templateContent);
|
|
@@ -1612,6 +1708,17 @@ async function runDoctor(options) {
|
|
|
1612
1708
|
log(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
|
|
1613
1709
|
}
|
|
1614
1710
|
}
|
|
1711
|
+
const pkgJson = await readJson(path14.join(packageRoot(), "package.json"));
|
|
1712
|
+
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
1713
|
+
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
1714
|
+
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
1715
|
+
warn(`- CLI UPDATE: ${currentVersion} \u2192 ${npmStatus.latest} available (run: npm install -g ${NPM_PACKAGE_NAME})`);
|
|
1716
|
+
process.exitCode = 1;
|
|
1717
|
+
} else if (npmStatus.latest !== null) {
|
|
1718
|
+
log(`- CLI: ${currentVersion} (up to date)`);
|
|
1719
|
+
} else {
|
|
1720
|
+
log(`- CLI: ${currentVersion} (version check unavailable)`);
|
|
1721
|
+
}
|
|
1615
1722
|
}
|
|
1616
1723
|
|
|
1617
1724
|
// src/recommender/explain-formatters.ts
|
|
@@ -1766,7 +1873,7 @@ async function runGuard(kind, _options) {
|
|
|
1766
1873
|
}
|
|
1767
1874
|
|
|
1768
1875
|
// src/commands/init.ts
|
|
1769
|
-
import
|
|
1876
|
+
import path15 from "path";
|
|
1770
1877
|
import fs11 from "fs-extra";
|
|
1771
1878
|
|
|
1772
1879
|
// src/utils/exec.ts
|
|
@@ -2288,7 +2395,7 @@ async function runSetupProject(options) {
|
|
|
2288
2395
|
// src/commands/init.ts
|
|
2289
2396
|
async function runInit(options) {
|
|
2290
2397
|
const root = process.cwd();
|
|
2291
|
-
const hausDir =
|
|
2398
|
+
const hausDir = path15.join(root, ".haus-workflow");
|
|
2292
2399
|
const alreadyInit = await fs11.pathExists(hausDir);
|
|
2293
2400
|
if (alreadyInit) {
|
|
2294
2401
|
log("Haus AI already initialized in this project.");
|
|
@@ -2301,7 +2408,7 @@ async function runInit(options) {
|
|
|
2301
2408
|
|
|
2302
2409
|
// src/install/apply.ts
|
|
2303
2410
|
import crypto2 from "crypto";
|
|
2304
|
-
import
|
|
2411
|
+
import path18 from "path";
|
|
2305
2412
|
import fs13 from "fs-extra";
|
|
2306
2413
|
|
|
2307
2414
|
// src/install/header.ts
|
|
@@ -2336,13 +2443,13 @@ ${content}`;
|
|
|
2336
2443
|
|
|
2337
2444
|
// src/install/manifest.ts
|
|
2338
2445
|
import os5 from "os";
|
|
2339
|
-
import
|
|
2446
|
+
import path16 from "path";
|
|
2340
2447
|
var MANIFEST_SCHEMA = "haus-install-manifest/1";
|
|
2341
2448
|
function globalClaudeDir() {
|
|
2342
|
-
return
|
|
2449
|
+
return path16.join(os5.homedir(), ".claude");
|
|
2343
2450
|
}
|
|
2344
2451
|
function hausManifestPath() {
|
|
2345
|
-
return
|
|
2452
|
+
return path16.join(globalClaudeDir(), "haus", "install-manifest.json");
|
|
2346
2453
|
}
|
|
2347
2454
|
async function readManifest() {
|
|
2348
2455
|
return readJson(hausManifestPath());
|
|
@@ -2361,10 +2468,10 @@ function buildManifest(source, files, hooks) {
|
|
|
2361
2468
|
}
|
|
2362
2469
|
|
|
2363
2470
|
// src/install/settings-merge.ts
|
|
2364
|
-
import
|
|
2471
|
+
import path17 from "path";
|
|
2365
2472
|
import fs12 from "fs-extra";
|
|
2366
2473
|
function settingsJsonPath() {
|
|
2367
|
-
return
|
|
2474
|
+
return path17.join(globalClaudeDir(), "settings.json");
|
|
2368
2475
|
}
|
|
2369
2476
|
async function readSettings() {
|
|
2370
2477
|
const parsed = await readJson(settingsJsonPath());
|
|
@@ -2435,7 +2542,7 @@ function hashContent(content) {
|
|
|
2435
2542
|
}
|
|
2436
2543
|
function sourceVersion() {
|
|
2437
2544
|
try {
|
|
2438
|
-
const pkgPath =
|
|
2545
|
+
const pkgPath = path18.join(packageRoot(), "package.json");
|
|
2439
2546
|
const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf8"));
|
|
2440
2547
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
2441
2548
|
} catch {
|
|
@@ -2443,32 +2550,32 @@ function sourceVersion() {
|
|
|
2443
2550
|
}
|
|
2444
2551
|
}
|
|
2445
2552
|
function globalSrcDir() {
|
|
2446
|
-
return
|
|
2553
|
+
return path18.join(packageRoot(), "library", "global");
|
|
2447
2554
|
}
|
|
2448
2555
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
2449
2556
|
const entries = [];
|
|
2450
|
-
const skillsDir =
|
|
2557
|
+
const skillsDir = path18.join(srcDir, "skills");
|
|
2451
2558
|
if (fs13.pathExistsSync(skillsDir)) {
|
|
2452
2559
|
for (const skillName of fs13.readdirSync(skillsDir)) {
|
|
2453
|
-
const skillFile =
|
|
2560
|
+
const skillFile = path18.join(skillsDir, skillName, "SKILL.md");
|
|
2454
2561
|
if (fs13.pathExistsSync(skillFile)) {
|
|
2455
2562
|
entries.push({
|
|
2456
2563
|
stableId: `skill.${skillName}`,
|
|
2457
|
-
srcRelPath:
|
|
2458
|
-
destPath:
|
|
2564
|
+
srcRelPath: path18.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
2565
|
+
destPath: path18.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
2459
2566
|
});
|
|
2460
2567
|
}
|
|
2461
2568
|
}
|
|
2462
2569
|
}
|
|
2463
|
-
const agentsDir =
|
|
2570
|
+
const agentsDir = path18.join(srcDir, "agents");
|
|
2464
2571
|
if (fs13.pathExistsSync(agentsDir)) {
|
|
2465
2572
|
for (const agentFile of fs13.readdirSync(agentsDir)) {
|
|
2466
2573
|
if (!agentFile.endsWith(".md")) continue;
|
|
2467
2574
|
const agentName = agentFile.replace(/\.md$/, "");
|
|
2468
2575
|
entries.push({
|
|
2469
2576
|
stableId: `agent.${agentName}`,
|
|
2470
|
-
srcRelPath:
|
|
2471
|
-
destPath:
|
|
2577
|
+
srcRelPath: path18.join("library", "global", "agents", agentFile),
|
|
2578
|
+
destPath: path18.join(claudeDir, "agents", agentFile)
|
|
2472
2579
|
});
|
|
2473
2580
|
}
|
|
2474
2581
|
}
|
|
@@ -2492,7 +2599,7 @@ async function applyInstall(options = {}) {
|
|
|
2492
2599
|
};
|
|
2493
2600
|
const manifestFiles = [];
|
|
2494
2601
|
for (const entry of sourceFiles) {
|
|
2495
|
-
const srcPath =
|
|
2602
|
+
const srcPath = path18.join(packageRoot(), entry.srcRelPath);
|
|
2496
2603
|
const rawContent = await readText(srcPath);
|
|
2497
2604
|
if (rawContent === void 0) {
|
|
2498
2605
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -2548,7 +2655,7 @@ async function applyInstall(options = {}) {
|
|
|
2548
2655
|
schemaVersion: SCHEMA_VERSION3
|
|
2549
2656
|
});
|
|
2550
2657
|
}
|
|
2551
|
-
const fragmentPath =
|
|
2658
|
+
const fragmentPath = path18.join(srcDir, "settings-fragments", "hooks.json");
|
|
2552
2659
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
2553
2660
|
const settings = await readSettings();
|
|
2554
2661
|
const { settings: mergedSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -2733,12 +2840,12 @@ async function runScan(options) {
|
|
|
2733
2840
|
}
|
|
2734
2841
|
|
|
2735
2842
|
// src/commands/undo.ts
|
|
2736
|
-
import
|
|
2843
|
+
import path19 from "path";
|
|
2737
2844
|
import fs14 from "fs-extra";
|
|
2738
2845
|
var CLAUDE_DIR = ".claude";
|
|
2739
2846
|
async function runUndo(options) {
|
|
2740
2847
|
const root = process.cwd();
|
|
2741
|
-
const targets = [
|
|
2848
|
+
const targets = [path19.join(root, CLAUDE_DIR), path19.join(root, HAUS_DIR)];
|
|
2742
2849
|
const existing = targets.filter((p) => fs14.existsSync(p));
|
|
2743
2850
|
if (existing.length === 0) {
|
|
2744
2851
|
log("Nothing to remove: no .claude/ or .haus-workflow/ in this directory.");
|
|
@@ -2746,7 +2853,7 @@ async function runUndo(options) {
|
|
|
2746
2853
|
}
|
|
2747
2854
|
if (!options.yes) {
|
|
2748
2855
|
const ok = await confirm(
|
|
2749
|
-
`Remove ${existing.map((p) =>
|
|
2856
|
+
`Remove ${existing.map((p) => path19.relative(root, p)).join(" and ")}? This cannot be undone.`
|
|
2750
2857
|
);
|
|
2751
2858
|
if (!ok) {
|
|
2752
2859
|
log("Cancelled.");
|
|
@@ -2755,13 +2862,13 @@ async function runUndo(options) {
|
|
|
2755
2862
|
}
|
|
2756
2863
|
for (const p of existing) {
|
|
2757
2864
|
await fs14.remove(p);
|
|
2758
|
-
log(`Removed ${
|
|
2865
|
+
log(`Removed ${path19.relative(root, p)}`);
|
|
2759
2866
|
}
|
|
2760
2867
|
}
|
|
2761
2868
|
|
|
2762
2869
|
// src/install/uninstall.ts
|
|
2763
2870
|
import crypto3 from "crypto";
|
|
2764
|
-
import
|
|
2871
|
+
import path20 from "path";
|
|
2765
2872
|
import fs15 from "fs-extra";
|
|
2766
2873
|
async function runUninstall(options = {}) {
|
|
2767
2874
|
const { force = false } = options;
|
|
@@ -2789,14 +2896,14 @@ async function runUninstall(options = {}) {
|
|
|
2789
2896
|
continue;
|
|
2790
2897
|
}
|
|
2791
2898
|
await fs15.remove(entry.destPath);
|
|
2792
|
-
await pruneEmptyDir(
|
|
2899
|
+
await pruneEmptyDir(path20.dirname(entry.destPath));
|
|
2793
2900
|
result.deleted.push(entry.destPath);
|
|
2794
2901
|
}
|
|
2795
2902
|
const settings = await readSettings();
|
|
2796
2903
|
const stripped = stripHausHooks(settings);
|
|
2797
2904
|
await writeSettings(stripped);
|
|
2798
2905
|
result.hooksStripped = true;
|
|
2799
|
-
const hausDir =
|
|
2906
|
+
const hausDir = path20.join(globalClaudeDir(), "haus");
|
|
2800
2907
|
const manifestPath = hausManifestPath();
|
|
2801
2908
|
if (fs15.pathExistsSync(manifestPath)) {
|
|
2802
2909
|
await fs15.remove(manifestPath);
|
|
@@ -2841,7 +2948,7 @@ async function runUninstallCommand(options) {
|
|
|
2841
2948
|
}
|
|
2842
2949
|
|
|
2843
2950
|
// src/commands/update.ts
|
|
2844
|
-
import
|
|
2951
|
+
import path22 from "path";
|
|
2845
2952
|
|
|
2846
2953
|
// src/update/diff-generated-files.ts
|
|
2847
2954
|
function diffGeneratedFiles() {
|
|
@@ -2868,11 +2975,12 @@ function summarizeLockDiff(before, after) {
|
|
|
2868
2975
|
|
|
2869
2976
|
// src/update/lockfile.ts
|
|
2870
2977
|
import { mkdir, readFile as readFile2, copyFile } from "fs/promises";
|
|
2871
|
-
import
|
|
2978
|
+
import path21 from "path";
|
|
2872
2979
|
async function checkLock(root) {
|
|
2873
2980
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
2874
2981
|
const hasValidVersions = lock.every((item) => !item.version || normalizeVersion(item.version) !== null);
|
|
2875
|
-
|
|
2982
|
+
const catalogRef = lock[0]?.catalogRef ?? null;
|
|
2983
|
+
return { ok: lock.length > 0 && hasValidVersions, count: lock.length, catalogRef };
|
|
2876
2984
|
}
|
|
2877
2985
|
async function applyLock(root) {
|
|
2878
2986
|
const lockPath = hausPath(root, "haus.lock.json");
|
|
@@ -2886,7 +2994,7 @@ async function applyLock(root) {
|
|
|
2886
2994
|
try {
|
|
2887
2995
|
const backupDir = hausPath(root, "backups");
|
|
2888
2996
|
await mkdir(backupDir, { recursive: true });
|
|
2889
|
-
await copyFile(lockPath,
|
|
2997
|
+
await copyFile(lockPath, path21.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
2890
2998
|
} catch {
|
|
2891
2999
|
}
|
|
2892
3000
|
const enriched = await Promise.all(
|
|
@@ -2908,7 +3016,7 @@ function diffLock(before, after) {
|
|
|
2908
3016
|
}
|
|
2909
3017
|
async function hasLocalOverrides(root) {
|
|
2910
3018
|
try {
|
|
2911
|
-
await readFile2(
|
|
3019
|
+
await readFile2(path21.join(root, ".claude", "settings.json"), "utf8");
|
|
2912
3020
|
return true;
|
|
2913
3021
|
} catch {
|
|
2914
3022
|
return false;
|
|
@@ -2916,35 +3024,29 @@ async function hasLocalOverrides(root) {
|
|
|
2916
3024
|
}
|
|
2917
3025
|
|
|
2918
3026
|
// src/commands/update.ts
|
|
2919
|
-
var
|
|
2920
|
-
async function checkNpmVersion(currentVersion) {
|
|
2921
|
-
try {
|
|
2922
|
-
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE_NAME)}/latest`, {
|
|
2923
|
-
signal: AbortSignal.timeout(8e3)
|
|
2924
|
-
});
|
|
2925
|
-
if (!res.ok) return;
|
|
2926
|
-
const data = await res.json();
|
|
2927
|
-
const latest = data?.version;
|
|
2928
|
-
if (!latest || !normalizeVersion(latest) || !normalizeVersion(currentVersion)) return;
|
|
2929
|
-
if (compareVersions(latest, currentVersion) > 0) {
|
|
2930
|
-
log(`npm update available: ${currentVersion} \u2192 ${latest}`);
|
|
2931
|
-
log(`Run: npm install -g ${NPM_PACKAGE_NAME}`);
|
|
2932
|
-
} else {
|
|
2933
|
-
log(`npm package up to date: ${currentVersion}`);
|
|
2934
|
-
}
|
|
2935
|
-
} catch {
|
|
2936
|
-
}
|
|
2937
|
-
}
|
|
3027
|
+
var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
2938
3028
|
async function runUpdate(options) {
|
|
2939
3029
|
const root = process.cwd();
|
|
2940
3030
|
if (options.check) {
|
|
2941
|
-
const
|
|
3031
|
+
const pkgJson2 = await readJson(path22.join(packageRoot(), "package.json"));
|
|
3032
|
+
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
3033
|
+
const [status, npmVersion, latestCatalogTag] = await Promise.all([
|
|
3034
|
+
checkLock(root),
|
|
3035
|
+
fetchNpmVersionStatus(currentVersion2),
|
|
3036
|
+
fetchLatestCatalogTag()
|
|
3037
|
+
]);
|
|
3038
|
+
const installedRef = status.catalogRef ?? "main";
|
|
3039
|
+
const catalogRefBehind = latestCatalogTag !== null && installedRef !== latestCatalogTag ? `installed from ${installedRef}, latest tag is ${latestCatalogTag}` : false;
|
|
2942
3040
|
log(
|
|
2943
3041
|
JSON.stringify(
|
|
2944
3042
|
{
|
|
2945
3043
|
...status,
|
|
3044
|
+
installedCatalogRef: installedRef,
|
|
3045
|
+
latestCatalogTag,
|
|
3046
|
+
catalogRefBehind,
|
|
2946
3047
|
localOverrides: await hasLocalOverrides(root),
|
|
2947
|
-
summary: diffGeneratedFiles()
|
|
3048
|
+
summary: diffGeneratedFiles(),
|
|
3049
|
+
npmVersion
|
|
2948
3050
|
},
|
|
2949
3051
|
null,
|
|
2950
3052
|
2
|
|
@@ -2953,9 +3055,15 @@ async function runUpdate(options) {
|
|
|
2953
3055
|
if (!status.ok) process.exitCode = 1;
|
|
2954
3056
|
return;
|
|
2955
3057
|
}
|
|
2956
|
-
const pkgJson = await readJson(
|
|
3058
|
+
const pkgJson = await readJson(path22.join(packageRoot(), "package.json"));
|
|
2957
3059
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
2958
|
-
await
|
|
3060
|
+
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
3061
|
+
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
3062
|
+
log(`npm update available: ${currentVersion} \u2192 ${npmStatus.latest}`);
|
|
3063
|
+
log(`Run: npm install -g ${NPM_PACKAGE_NAME2}`);
|
|
3064
|
+
} else if (npmStatus.latest !== null) {
|
|
3065
|
+
log(`npm package up to date: ${currentVersion}`);
|
|
3066
|
+
}
|
|
2959
3067
|
if (await hasLocalOverrides(root)) {
|
|
2960
3068
|
log("Local .claude overrides detected. Preserving local files; only lockfile updated.");
|
|
2961
3069
|
}
|
|
@@ -2977,17 +3085,18 @@ async function runUpdate(options) {
|
|
|
2977
3085
|
}
|
|
2978
3086
|
|
|
2979
3087
|
// src/commands/validate-catalog.ts
|
|
2980
|
-
import
|
|
3088
|
+
import fs16 from "fs";
|
|
3089
|
+
import path24 from "path";
|
|
2981
3090
|
|
|
2982
3091
|
// src/catalog/allowed-stacks.ts
|
|
2983
|
-
import
|
|
3092
|
+
import path23 from "path";
|
|
2984
3093
|
async function readAllowedStacks(root) {
|
|
2985
|
-
const data = await readJson(
|
|
3094
|
+
const data = await readJson(path23.join(root, "library", "catalog", "allowed-stacks.json"));
|
|
2986
3095
|
return data?.stacks ?? [];
|
|
2987
3096
|
}
|
|
2988
3097
|
|
|
2989
|
-
// src/
|
|
2990
|
-
var
|
|
3098
|
+
// src/catalog/validation-rules.ts
|
|
3099
|
+
var FORBIDDEN_TAGS = [
|
|
2991
3100
|
"python",
|
|
2992
3101
|
"django",
|
|
2993
3102
|
"go",
|
|
@@ -3004,18 +3113,28 @@ var FORBIDDEN2 = [
|
|
|
3004
3113
|
"defi",
|
|
3005
3114
|
"trading"
|
|
3006
3115
|
];
|
|
3007
|
-
|
|
3116
|
+
var BANNED_AGENT_PHRASES = ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"];
|
|
3117
|
+
var REQUIRED_SKILL_SECTIONS = ["## Use when", "## Do not use when"];
|
|
3118
|
+
var REQUIRED_AGENT_SECTIONS = ["## Use when", "## Do not use when", "## Verification"];
|
|
3119
|
+
var RISKY_INSTALL_PATTERNS = [/\bnpx\s+-y\b/i, /\bnpx\s+--yes\b/i, /\byarn\s+dlx\b/i, /\bpnpm\s+dlx\b/i];
|
|
3120
|
+
var ALLOWED_NPX_PATTERN = /\bnpx\s+tsx\b/i;
|
|
3121
|
+
var ANY_NPX_PATTERN = /\bnpx\s+\S+/i;
|
|
3122
|
+
var HTTP_URL_PATTERN = /^http:\/\//i;
|
|
3123
|
+
var PLACEHOLDER_PATTERN = /\bTODO\b|\bPLACEHOLDER\b/i;
|
|
3124
|
+
|
|
3125
|
+
// src/commands/validate-catalog.ts
|
|
3126
|
+
function auditForbiddenStacks(items) {
|
|
3008
3127
|
const failures = [];
|
|
3009
3128
|
for (const item of items) {
|
|
3010
3129
|
const tags = Array.isArray(item.tags) ? item.tags : [];
|
|
3011
3130
|
const text = `${item.id} ${tags.join(" ")}`.toLowerCase();
|
|
3012
|
-
for (const word of
|
|
3131
|
+
for (const word of FORBIDDEN_TAGS) {
|
|
3013
3132
|
if (text.includes(word)) failures.push(`${item.id}: unsupported stack/tag "${word}"`);
|
|
3014
3133
|
}
|
|
3015
3134
|
}
|
|
3016
3135
|
return failures;
|
|
3017
3136
|
}
|
|
3018
|
-
|
|
3137
|
+
function auditManifestStructure(items) {
|
|
3019
3138
|
const failures = [];
|
|
3020
3139
|
const seenIds = /* @__PURE__ */ new Map();
|
|
3021
3140
|
const seenPaths = /* @__PURE__ */ new Map();
|
|
@@ -3041,7 +3160,7 @@ async function auditManifestStructure(items) {
|
|
|
3041
3160
|
} else {
|
|
3042
3161
|
seenIds.set(item.id, i);
|
|
3043
3162
|
}
|
|
3044
|
-
if (item.type === "skill" || item.type === "agent") {
|
|
3163
|
+
if (item.type === "skill" || item.type === "agent" || item.type === "template") {
|
|
3045
3164
|
if (!item.path) {
|
|
3046
3165
|
failures.push(`${item.id}: missing path`);
|
|
3047
3166
|
} else {
|
|
@@ -3059,7 +3178,7 @@ async function auditManifestStructure(items) {
|
|
|
3059
3178
|
failures.push(`${item.id}: source must be "haus" or curated with reviewStatus "approved"`);
|
|
3060
3179
|
}
|
|
3061
3180
|
for (const ref of item.references ?? []) {
|
|
3062
|
-
if (
|
|
3181
|
+
if (HTTP_URL_PATTERN.test(ref)) {
|
|
3063
3182
|
failures.push(`${item.id}: reference uses insecure http:// URL: ${ref}`);
|
|
3064
3183
|
}
|
|
3065
3184
|
}
|
|
@@ -3067,13 +3186,84 @@ async function auditManifestStructure(items) {
|
|
|
3067
3186
|
}
|
|
3068
3187
|
return failures;
|
|
3069
3188
|
}
|
|
3189
|
+
function auditShippedFiles(manifestDir, items) {
|
|
3190
|
+
const failures = [];
|
|
3191
|
+
for (const item of items) {
|
|
3192
|
+
if (!item.path) continue;
|
|
3193
|
+
const absPath = path24.join(manifestDir, item.path);
|
|
3194
|
+
if (item.type === "skill") {
|
|
3195
|
+
const skillMd = path24.join(absPath, "SKILL.md");
|
|
3196
|
+
if (!fs16.existsSync(skillMd)) {
|
|
3197
|
+
failures.push(`${item.id}: missing ${path24.relative(manifestDir, skillMd)}`);
|
|
3198
|
+
continue;
|
|
3199
|
+
}
|
|
3200
|
+
const text = fs16.readFileSync(skillMd, "utf8");
|
|
3201
|
+
for (const section of REQUIRED_SKILL_SECTIONS) {
|
|
3202
|
+
if (!text.includes(section)) failures.push(`${item.id}: SKILL.md missing ${section}`);
|
|
3203
|
+
}
|
|
3204
|
+
} else if (item.type === "agent") {
|
|
3205
|
+
if (!fs16.existsSync(absPath)) {
|
|
3206
|
+
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
3207
|
+
continue;
|
|
3208
|
+
}
|
|
3209
|
+
const text = fs16.readFileSync(absPath, "utf8");
|
|
3210
|
+
if (!text.startsWith("---")) failures.push(`${item.id}: agent file missing YAML frontmatter`);
|
|
3211
|
+
for (const section of REQUIRED_AGENT_SECTIONS) {
|
|
3212
|
+
if (!text.includes(section)) failures.push(`${item.id}: agent file missing ${section}`);
|
|
3213
|
+
}
|
|
3214
|
+
const lower = text.toLowerCase();
|
|
3215
|
+
for (const phrase of BANNED_AGENT_PHRASES) {
|
|
3216
|
+
if (lower.includes(phrase)) failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
|
|
3217
|
+
}
|
|
3218
|
+
} else if (item.type === "template") {
|
|
3219
|
+
if (!fs16.existsSync(absPath)) {
|
|
3220
|
+
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
return failures;
|
|
3225
|
+
}
|
|
3226
|
+
function auditMarkdownContent(manifestDir) {
|
|
3227
|
+
const failures = [];
|
|
3228
|
+
const dirs = ["skills", "agents"];
|
|
3229
|
+
for (const dir of dirs) {
|
|
3230
|
+
const abs = path24.join(manifestDir, dir);
|
|
3231
|
+
if (!fs16.existsSync(abs)) continue;
|
|
3232
|
+
walkMd(abs, (file) => {
|
|
3233
|
+
const text = fs16.readFileSync(file, "utf8");
|
|
3234
|
+
const rel = path24.relative(manifestDir, file);
|
|
3235
|
+
const lines = text.split(/\r?\n/);
|
|
3236
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3237
|
+
const line = lines[i] ?? "";
|
|
3238
|
+
if (PLACEHOLDER_PATTERN.test(line)) {
|
|
3239
|
+
failures.push(`${rel}:${i + 1}: TODO or placeholder in shipped content`);
|
|
3240
|
+
}
|
|
3241
|
+
if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line))) {
|
|
3242
|
+
failures.push(`${rel}:${i + 1}: risky install pattern`);
|
|
3243
|
+
}
|
|
3244
|
+
if (ANY_NPX_PATTERN.test(line) && !ALLOWED_NPX_PATTERN.test(line)) {
|
|
3245
|
+
failures.push(`${rel}:${i + 1}: disallowed npx (only npx tsx allowed)`);
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
return failures;
|
|
3251
|
+
}
|
|
3252
|
+
function walkMd(dir, fn) {
|
|
3253
|
+
for (const entry of fs16.readdirSync(dir, { withFileTypes: true })) {
|
|
3254
|
+
const full = path24.join(dir, entry.name);
|
|
3255
|
+
if (entry.isDirectory()) walkMd(full, fn);
|
|
3256
|
+
else if (entry.name.endsWith(".md")) fn(full);
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3070
3259
|
async function runValidateCatalog(manifestPath) {
|
|
3071
3260
|
if (!manifestPath) {
|
|
3072
3261
|
error("Usage: haus validate-catalog <path/to/manifest.json>");
|
|
3073
3262
|
process.exitCode = 1;
|
|
3074
3263
|
return;
|
|
3075
3264
|
}
|
|
3076
|
-
const abs =
|
|
3265
|
+
const abs = path24.resolve(process.cwd(), manifestPath);
|
|
3266
|
+
const manifestDir = path24.dirname(abs);
|
|
3077
3267
|
const data = await readJson(abs);
|
|
3078
3268
|
if (!data?.items) {
|
|
3079
3269
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -3081,22 +3271,22 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3081
3271
|
return;
|
|
3082
3272
|
}
|
|
3083
3273
|
const items = data.items;
|
|
3084
|
-
const
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3274
|
+
const structureFailures = auditManifestStructure(items);
|
|
3275
|
+
const stackFailures = auditForbiddenStacks(items);
|
|
3276
|
+
const fileFailures = auditShippedFiles(manifestDir, items);
|
|
3277
|
+
const contentFailures = auditMarkdownContent(manifestDir);
|
|
3088
3278
|
const allowed = new Set((await readAllowedStacks(packageRoot())).map((x) => x.toLowerCase()));
|
|
3089
3279
|
const tagFailures = [];
|
|
3090
3280
|
if (allowed.size > 0) {
|
|
3091
3281
|
for (const item of items) {
|
|
3092
3282
|
for (const tag of Array.isArray(item.tags) ? item.tags : []) {
|
|
3093
|
-
if (!allowed.has(tag.toLowerCase()) && !tag.includes("-patterns") && tag !== "haus" && tag !== "security" && tag !== "quality" && tag !== "review" && tag !== "workflow") {
|
|
3283
|
+
if (!allowed.has(tag.toLowerCase()) && !tag.includes("-patterns") && tag !== "haus" && tag !== "security" && tag !== "quality" && tag !== "review" && tag !== "workflow" && tag !== "baseline" && tag !== "project-instructions") {
|
|
3094
3284
|
tagFailures.push(`${item.id}: tag not in allowlist: "${tag}"`);
|
|
3095
3285
|
}
|
|
3096
3286
|
}
|
|
3097
3287
|
}
|
|
3098
3288
|
}
|
|
3099
|
-
const allFailures = [...structureFailures, ...stackFailures, ...tagFailures];
|
|
3289
|
+
const allFailures = [...structureFailures, ...stackFailures, ...fileFailures, ...contentFailures, ...tagFailures];
|
|
3100
3290
|
if (allFailures.length) {
|
|
3101
3291
|
allFailures.forEach((f) => error(f));
|
|
3102
3292
|
process.exitCode = 1;
|
|
@@ -3106,7 +3296,7 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3106
3296
|
}
|
|
3107
3297
|
|
|
3108
3298
|
// src/commands/workspace.ts
|
|
3109
|
-
import
|
|
3299
|
+
import path25 from "path";
|
|
3110
3300
|
import YAML from "yaml";
|
|
3111
3301
|
async function runWorkspace(action) {
|
|
3112
3302
|
if (action === "init") {
|
|
@@ -3139,7 +3329,7 @@ relationships: []
|
|
|
3139
3329
|
const summaries = [];
|
|
3140
3330
|
const ownership = {};
|
|
3141
3331
|
for (const repo of repos) {
|
|
3142
|
-
const repoRoot =
|
|
3332
|
+
const repoRoot = path25.resolve(process.cwd(), repo.path);
|
|
3143
3333
|
const result = await scanProject(repoRoot, "fast");
|
|
3144
3334
|
summaries.push({
|
|
3145
3335
|
name: repo.name,
|
|
@@ -3175,7 +3365,7 @@ ${summaries.map(
|
|
|
3175
3365
|
// src/cli.ts
|
|
3176
3366
|
function cliVersion() {
|
|
3177
3367
|
try {
|
|
3178
|
-
const pkgPath =
|
|
3368
|
+
const pkgPath = path26.join(packageRoot(), "package.json");
|
|
3179
3369
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
3180
3370
|
return pkg.version ?? "0.0.0";
|
|
3181
3371
|
} catch {
|
|
@@ -3185,7 +3375,7 @@ function cliVersion() {
|
|
|
3185
3375
|
var program = new Command();
|
|
3186
3376
|
function validateRuntimeNodeVersion() {
|
|
3187
3377
|
try {
|
|
3188
|
-
const pkgPath =
|
|
3378
|
+
const pkgPath = path26.join(packageRoot(), "package.json");
|
|
3189
3379
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
3190
3380
|
const requiredRange = pkg.engines?.node;
|
|
3191
3381
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -3203,7 +3393,7 @@ program.command("scan").option("--json").action(runScan);
|
|
|
3203
3393
|
program.command("recommend").option("--json").action(runRecommend);
|
|
3204
3394
|
program.command("setup-project").option("--guided").option("--fast").option("--json").action(runSetupProject);
|
|
3205
3395
|
program.command("doctor").option("--hooks", "Verify .claude/settings.json matches the hook contract").action(runDoctor);
|
|
3206
|
-
program.command("apply").option("--dry-run").option("--write").action(runApply);
|
|
3396
|
+
program.command("apply").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option("--allow-empty-cache", "Apply core files only when catalog cache is empty (skip catalog items without error)").action(runApply);
|
|
3207
3397
|
program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
|
|
3208
3398
|
program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
|
|
3209
3399
|
program.command("context").option("--task <task>").option("--from-hook").option("--json").option("--verbose").action(runContext);
|