@haus-tech/haus-workflow 0.1.0 → 0.3.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 +143 -0
- package/README.md +35 -26
- package/dist/cli.js +319 -111
- package/library/catalog/allowed-stacks.json +4 -1
- package/library/catalog/haus-lock.schema.json +2 -41
- package/library/catalog/manifest.json +836 -198
- package/package.json +5 -4
- package/tests/fixtures/catalog/manifest.json +83 -38
- package/tests/fixtures/catalog/skills/jest-patterns/SKILL.md +9 -0
- package/tests/fixtures/catalog/skills/vitest-patterns/SKILL.md +9 -0
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);
|
|
@@ -1353,13 +1427,17 @@ function detectRoles(deps, files) {
|
|
|
1353
1427
|
if (files.some((f) => f.endsWith("turbo.json"))) roles.add("turbo-monorepo");
|
|
1354
1428
|
if (files.some((f) => f.endsWith("artisan")) || deps.includes("laravel/framework")) roles.add("laravel-app");
|
|
1355
1429
|
if (deps.includes("laravel/nova")) roles.add("laravel-nova-app");
|
|
1356
|
-
|
|
1430
|
+
const hasWpConfig = files.some((f) => f.endsWith("wp-config.php"));
|
|
1431
|
+
const hasBedrockLayout = files.some((f) => f.includes("web/app")) || deps.includes("roots/wordpress");
|
|
1432
|
+
if (hasWpConfig && hasBedrockLayout) {
|
|
1357
1433
|
roles.add("wordpress-bedrock-site");
|
|
1358
1434
|
roles.add("wordpress-site");
|
|
1359
|
-
}
|
|
1360
|
-
if (files.some((f) => f.endsWith("wp-config.php")) && !files.some((f) => f.includes("web/app"))) {
|
|
1435
|
+
} else if (hasWpConfig) {
|
|
1361
1436
|
roles.add("wordpress-vanilla-site");
|
|
1362
1437
|
roles.add("wordpress-site");
|
|
1438
|
+
} else if (deps.includes("roots/wordpress")) {
|
|
1439
|
+
roles.add("wordpress-bedrock-site");
|
|
1440
|
+
roles.add("wordpress-site");
|
|
1363
1441
|
}
|
|
1364
1442
|
if (files.some((f) => f.endsWith(".csproj") || f.endsWith(".sln"))) roles.add("dotnet-service");
|
|
1365
1443
|
if (deps.includes("express")) roles.add("express-service");
|
|
@@ -1391,17 +1469,31 @@ async function detectStacks(root, deps, files, packageManager) {
|
|
|
1391
1469
|
if (files.some((f) => f.endsWith(".graphql") || f.endsWith("schema.graphql"))) add("backend", "graphql");
|
|
1392
1470
|
if (deps.includes("laravel/framework")) add("backend", "laravel");
|
|
1393
1471
|
if (files.some((f) => f.includes("app/Providers/") || f.includes("routes/"))) add("backend", "laravel");
|
|
1394
|
-
if (files.some((f) => f.endsWith("wp-config.php"))) add("backend", "wordpress");
|
|
1472
|
+
if (files.some((f) => f.endsWith("wp-config.php")) || deps.includes("roots/wordpress")) add("backend", "wordpress");
|
|
1473
|
+
if (deps.includes("wpackagist-plugin/elementor") || deps.includes("wearehaus/elementor-pro") || deps.includes("wpackagist-theme/hello-elementor")) {
|
|
1474
|
+
add("backend", "elementor");
|
|
1475
|
+
}
|
|
1476
|
+
if (deps.includes("wearehaus/advanced-custom-fields-pro") || deps.includes("wpackagist-plugin/advanced-custom-fields")) {
|
|
1477
|
+
add("backend", "acf-pro");
|
|
1478
|
+
}
|
|
1479
|
+
if (deps.includes("wearehaus/jet-engine")) add("backend", "jetengine");
|
|
1480
|
+
if (deps.includes("wearehaus/jet-smart-filters")) add("backend", "jetsmartfilters");
|
|
1481
|
+
if (deps.includes("wearehaus/gravityforms")) add("backend", "gravityforms");
|
|
1395
1482
|
if (files.some((f) => f.endsWith(".csproj") || f.endsWith(".sln"))) add("backend", "dotnet");
|
|
1396
1483
|
if (deps.includes("@playwright/test")) add("testing", "playwright");
|
|
1397
1484
|
if (files.some((f) => f.includes(".storybook"))) add("testing", "storybook");
|
|
1398
1485
|
if (deps.some((d) => d.startsWith("@testing-library/"))) add("testing", "testing-library");
|
|
1399
1486
|
if (files.some((f) => f.endsWith("phpunit.xml"))) add("testing", "phpunit");
|
|
1400
1487
|
if (deps.some((d) => d.startsWith("@storybook/"))) add("testing", "storybook");
|
|
1488
|
+
if (deps.includes("vitest")) add("testing", "vitest");
|
|
1489
|
+
if (deps.includes("jest") || deps.includes("jest-environment-jsdom")) add("testing", "jest");
|
|
1401
1490
|
if (deps.includes("pg")) add("databases", "postgresql");
|
|
1402
1491
|
if (deps.includes("mariadb") || deps.includes("mysql2")) add("databases", "mariadb");
|
|
1403
1492
|
if (deps.includes("mssql")) add("databases", "mssql");
|
|
1404
1493
|
if (deps.includes("@elastic/elasticsearch")) add("databases", "elasticsearch");
|
|
1494
|
+
if (deps.includes("predis/predis") || deps.includes("ioredis") || deps.includes("redis")) {
|
|
1495
|
+
add("databases", "redis");
|
|
1496
|
+
}
|
|
1405
1497
|
if (await hasNeedle(root, files, "openid")) add("auth", "oidc");
|
|
1406
1498
|
if (await hasNeedle(root, files, "AZURE_AD")) add("auth", "azure-ad");
|
|
1407
1499
|
if (await hasNeedle(root, files, "BANKID")) add("auth", "bankid");
|
|
@@ -1415,7 +1507,7 @@ async function hasNeedle(root, files, needle) {
|
|
|
1415
1507
|
);
|
|
1416
1508
|
for (const rel of candidates.slice(0, 300)) {
|
|
1417
1509
|
try {
|
|
1418
|
-
const content = await readFile(
|
|
1510
|
+
const content = await readFile(path13.join(root, rel), "utf8");
|
|
1419
1511
|
if (content.includes(needle)) return true;
|
|
1420
1512
|
} catch {
|
|
1421
1513
|
continue;
|
|
@@ -1508,8 +1600,30 @@ async function runContext(options) {
|
|
|
1508
1600
|
}
|
|
1509
1601
|
|
|
1510
1602
|
// src/commands/doctor.ts
|
|
1511
|
-
import
|
|
1603
|
+
import path14 from "path";
|
|
1512
1604
|
import fs10 from "fs-extra";
|
|
1605
|
+
|
|
1606
|
+
// src/update/npm-version.ts
|
|
1607
|
+
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
1608
|
+
async function fetchNpmVersionStatus(currentVersion) {
|
|
1609
|
+
try {
|
|
1610
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE_NAME)}/latest`, {
|
|
1611
|
+
signal: AbortSignal.timeout(8e3)
|
|
1612
|
+
});
|
|
1613
|
+
if (!res.ok) return { current: currentVersion, latest: null, updateAvailable: false };
|
|
1614
|
+
const data = await res.json();
|
|
1615
|
+
const latest = data?.version;
|
|
1616
|
+
if (!latest || !normalizeVersion(latest) || !normalizeVersion(currentVersion)) {
|
|
1617
|
+
return { current: currentVersion, latest: null, updateAvailable: false };
|
|
1618
|
+
}
|
|
1619
|
+
const updateAvailable = compareVersions(latest, currentVersion) > 0;
|
|
1620
|
+
return { current: currentVersion, latest, updateAvailable };
|
|
1621
|
+
} catch {
|
|
1622
|
+
return { current: currentVersion, latest: null, updateAvailable: false };
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// src/commands/doctor.ts
|
|
1513
1627
|
async function runDoctor(options) {
|
|
1514
1628
|
const root = process.cwd();
|
|
1515
1629
|
if (options?.hooks) {
|
|
@@ -1554,7 +1668,7 @@ async function runDoctor(options) {
|
|
|
1554
1668
|
const enabled = await isHookEnabled(root, key);
|
|
1555
1669
|
log(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
|
|
1556
1670
|
}
|
|
1557
|
-
const rootClaudeMdPath =
|
|
1671
|
+
const rootClaudeMdPath = path14.join(root, "CLAUDE.md");
|
|
1558
1672
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
1559
1673
|
if (!rootClaudeMdContent) {
|
|
1560
1674
|
warn("- CLAUDE.md: missing (run `haus apply --write` to create)");
|
|
@@ -1574,7 +1688,7 @@ async function runDoctor(options) {
|
|
|
1574
1688
|
warn("- .haus-workflow/haus-way-of-work.md: no HAUS-MANAGED header (user-owned)");
|
|
1575
1689
|
} else {
|
|
1576
1690
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
1577
|
-
const templatePath =
|
|
1691
|
+
const templatePath = path14.join(packageRoot(), "library", "global", "templates", "haus-way-of-work.md");
|
|
1578
1692
|
const templateContent = await readText(templatePath);
|
|
1579
1693
|
if (storedHashMatch && templateContent) {
|
|
1580
1694
|
const currentHash = hashText(templateContent);
|
|
@@ -1612,6 +1726,17 @@ async function runDoctor(options) {
|
|
|
1612
1726
|
log(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
|
|
1613
1727
|
}
|
|
1614
1728
|
}
|
|
1729
|
+
const pkgJson = await readJson(path14.join(packageRoot(), "package.json"));
|
|
1730
|
+
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
1731
|
+
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
1732
|
+
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
1733
|
+
warn(`- CLI UPDATE: ${currentVersion} \u2192 ${npmStatus.latest} available (run: npm install -g ${NPM_PACKAGE_NAME})`);
|
|
1734
|
+
process.exitCode = 1;
|
|
1735
|
+
} else if (npmStatus.latest !== null) {
|
|
1736
|
+
log(`- CLI: ${currentVersion} (up to date)`);
|
|
1737
|
+
} else {
|
|
1738
|
+
log(`- CLI: ${currentVersion} (version check unavailable)`);
|
|
1739
|
+
}
|
|
1615
1740
|
}
|
|
1616
1741
|
|
|
1617
1742
|
// src/recommender/explain-formatters.ts
|
|
@@ -1766,7 +1891,7 @@ async function runGuard(kind, _options) {
|
|
|
1766
1891
|
}
|
|
1767
1892
|
|
|
1768
1893
|
// src/commands/init.ts
|
|
1769
|
-
import
|
|
1894
|
+
import path15 from "path";
|
|
1770
1895
|
import fs11 from "fs-extra";
|
|
1771
1896
|
|
|
1772
1897
|
// src/utils/exec.ts
|
|
@@ -2288,7 +2413,7 @@ async function runSetupProject(options) {
|
|
|
2288
2413
|
// src/commands/init.ts
|
|
2289
2414
|
async function runInit(options) {
|
|
2290
2415
|
const root = process.cwd();
|
|
2291
|
-
const hausDir =
|
|
2416
|
+
const hausDir = path15.join(root, ".haus-workflow");
|
|
2292
2417
|
const alreadyInit = await fs11.pathExists(hausDir);
|
|
2293
2418
|
if (alreadyInit) {
|
|
2294
2419
|
log("Haus AI already initialized in this project.");
|
|
@@ -2301,7 +2426,7 @@ async function runInit(options) {
|
|
|
2301
2426
|
|
|
2302
2427
|
// src/install/apply.ts
|
|
2303
2428
|
import crypto2 from "crypto";
|
|
2304
|
-
import
|
|
2429
|
+
import path18 from "path";
|
|
2305
2430
|
import fs13 from "fs-extra";
|
|
2306
2431
|
|
|
2307
2432
|
// src/install/header.ts
|
|
@@ -2336,13 +2461,13 @@ ${content}`;
|
|
|
2336
2461
|
|
|
2337
2462
|
// src/install/manifest.ts
|
|
2338
2463
|
import os5 from "os";
|
|
2339
|
-
import
|
|
2464
|
+
import path16 from "path";
|
|
2340
2465
|
var MANIFEST_SCHEMA = "haus-install-manifest/1";
|
|
2341
2466
|
function globalClaudeDir() {
|
|
2342
|
-
return
|
|
2467
|
+
return path16.join(os5.homedir(), ".claude");
|
|
2343
2468
|
}
|
|
2344
2469
|
function hausManifestPath() {
|
|
2345
|
-
return
|
|
2470
|
+
return path16.join(globalClaudeDir(), "haus", "install-manifest.json");
|
|
2346
2471
|
}
|
|
2347
2472
|
async function readManifest() {
|
|
2348
2473
|
return readJson(hausManifestPath());
|
|
@@ -2361,10 +2486,10 @@ function buildManifest(source, files, hooks) {
|
|
|
2361
2486
|
}
|
|
2362
2487
|
|
|
2363
2488
|
// src/install/settings-merge.ts
|
|
2364
|
-
import
|
|
2489
|
+
import path17 from "path";
|
|
2365
2490
|
import fs12 from "fs-extra";
|
|
2366
2491
|
function settingsJsonPath() {
|
|
2367
|
-
return
|
|
2492
|
+
return path17.join(globalClaudeDir(), "settings.json");
|
|
2368
2493
|
}
|
|
2369
2494
|
async function readSettings() {
|
|
2370
2495
|
const parsed = await readJson(settingsJsonPath());
|
|
@@ -2435,7 +2560,7 @@ function hashContent(content) {
|
|
|
2435
2560
|
}
|
|
2436
2561
|
function sourceVersion() {
|
|
2437
2562
|
try {
|
|
2438
|
-
const pkgPath =
|
|
2563
|
+
const pkgPath = path18.join(packageRoot(), "package.json");
|
|
2439
2564
|
const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf8"));
|
|
2440
2565
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
2441
2566
|
} catch {
|
|
@@ -2443,32 +2568,32 @@ function sourceVersion() {
|
|
|
2443
2568
|
}
|
|
2444
2569
|
}
|
|
2445
2570
|
function globalSrcDir() {
|
|
2446
|
-
return
|
|
2571
|
+
return path18.join(packageRoot(), "library", "global");
|
|
2447
2572
|
}
|
|
2448
2573
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
2449
2574
|
const entries = [];
|
|
2450
|
-
const skillsDir =
|
|
2575
|
+
const skillsDir = path18.join(srcDir, "skills");
|
|
2451
2576
|
if (fs13.pathExistsSync(skillsDir)) {
|
|
2452
2577
|
for (const skillName of fs13.readdirSync(skillsDir)) {
|
|
2453
|
-
const skillFile =
|
|
2578
|
+
const skillFile = path18.join(skillsDir, skillName, "SKILL.md");
|
|
2454
2579
|
if (fs13.pathExistsSync(skillFile)) {
|
|
2455
2580
|
entries.push({
|
|
2456
2581
|
stableId: `skill.${skillName}`,
|
|
2457
|
-
srcRelPath:
|
|
2458
|
-
destPath:
|
|
2582
|
+
srcRelPath: path18.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
2583
|
+
destPath: path18.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
2459
2584
|
});
|
|
2460
2585
|
}
|
|
2461
2586
|
}
|
|
2462
2587
|
}
|
|
2463
|
-
const agentsDir =
|
|
2588
|
+
const agentsDir = path18.join(srcDir, "agents");
|
|
2464
2589
|
if (fs13.pathExistsSync(agentsDir)) {
|
|
2465
2590
|
for (const agentFile of fs13.readdirSync(agentsDir)) {
|
|
2466
2591
|
if (!agentFile.endsWith(".md")) continue;
|
|
2467
2592
|
const agentName = agentFile.replace(/\.md$/, "");
|
|
2468
2593
|
entries.push({
|
|
2469
2594
|
stableId: `agent.${agentName}`,
|
|
2470
|
-
srcRelPath:
|
|
2471
|
-
destPath:
|
|
2595
|
+
srcRelPath: path18.join("library", "global", "agents", agentFile),
|
|
2596
|
+
destPath: path18.join(claudeDir, "agents", agentFile)
|
|
2472
2597
|
});
|
|
2473
2598
|
}
|
|
2474
2599
|
}
|
|
@@ -2492,7 +2617,7 @@ async function applyInstall(options = {}) {
|
|
|
2492
2617
|
};
|
|
2493
2618
|
const manifestFiles = [];
|
|
2494
2619
|
for (const entry of sourceFiles) {
|
|
2495
|
-
const srcPath =
|
|
2620
|
+
const srcPath = path18.join(packageRoot(), entry.srcRelPath);
|
|
2496
2621
|
const rawContent = await readText(srcPath);
|
|
2497
2622
|
if (rawContent === void 0) {
|
|
2498
2623
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -2548,7 +2673,7 @@ async function applyInstall(options = {}) {
|
|
|
2548
2673
|
schemaVersion: SCHEMA_VERSION3
|
|
2549
2674
|
});
|
|
2550
2675
|
}
|
|
2551
|
-
const fragmentPath =
|
|
2676
|
+
const fragmentPath = path18.join(srcDir, "settings-fragments", "hooks.json");
|
|
2552
2677
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
2553
2678
|
const settings = await readSettings();
|
|
2554
2679
|
const { settings: mergedSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -2733,12 +2858,12 @@ async function runScan(options) {
|
|
|
2733
2858
|
}
|
|
2734
2859
|
|
|
2735
2860
|
// src/commands/undo.ts
|
|
2736
|
-
import
|
|
2861
|
+
import path19 from "path";
|
|
2737
2862
|
import fs14 from "fs-extra";
|
|
2738
2863
|
var CLAUDE_DIR = ".claude";
|
|
2739
2864
|
async function runUndo(options) {
|
|
2740
2865
|
const root = process.cwd();
|
|
2741
|
-
const targets = [
|
|
2866
|
+
const targets = [path19.join(root, CLAUDE_DIR), path19.join(root, HAUS_DIR)];
|
|
2742
2867
|
const existing = targets.filter((p) => fs14.existsSync(p));
|
|
2743
2868
|
if (existing.length === 0) {
|
|
2744
2869
|
log("Nothing to remove: no .claude/ or .haus-workflow/ in this directory.");
|
|
@@ -2746,7 +2871,7 @@ async function runUndo(options) {
|
|
|
2746
2871
|
}
|
|
2747
2872
|
if (!options.yes) {
|
|
2748
2873
|
const ok = await confirm(
|
|
2749
|
-
`Remove ${existing.map((p) =>
|
|
2874
|
+
`Remove ${existing.map((p) => path19.relative(root, p)).join(" and ")}? This cannot be undone.`
|
|
2750
2875
|
);
|
|
2751
2876
|
if (!ok) {
|
|
2752
2877
|
log("Cancelled.");
|
|
@@ -2755,13 +2880,13 @@ async function runUndo(options) {
|
|
|
2755
2880
|
}
|
|
2756
2881
|
for (const p of existing) {
|
|
2757
2882
|
await fs14.remove(p);
|
|
2758
|
-
log(`Removed ${
|
|
2883
|
+
log(`Removed ${path19.relative(root, p)}`);
|
|
2759
2884
|
}
|
|
2760
2885
|
}
|
|
2761
2886
|
|
|
2762
2887
|
// src/install/uninstall.ts
|
|
2763
2888
|
import crypto3 from "crypto";
|
|
2764
|
-
import
|
|
2889
|
+
import path20 from "path";
|
|
2765
2890
|
import fs15 from "fs-extra";
|
|
2766
2891
|
async function runUninstall(options = {}) {
|
|
2767
2892
|
const { force = false } = options;
|
|
@@ -2789,14 +2914,14 @@ async function runUninstall(options = {}) {
|
|
|
2789
2914
|
continue;
|
|
2790
2915
|
}
|
|
2791
2916
|
await fs15.remove(entry.destPath);
|
|
2792
|
-
await pruneEmptyDir(
|
|
2917
|
+
await pruneEmptyDir(path20.dirname(entry.destPath));
|
|
2793
2918
|
result.deleted.push(entry.destPath);
|
|
2794
2919
|
}
|
|
2795
2920
|
const settings = await readSettings();
|
|
2796
2921
|
const stripped = stripHausHooks(settings);
|
|
2797
2922
|
await writeSettings(stripped);
|
|
2798
2923
|
result.hooksStripped = true;
|
|
2799
|
-
const hausDir =
|
|
2924
|
+
const hausDir = path20.join(globalClaudeDir(), "haus");
|
|
2800
2925
|
const manifestPath = hausManifestPath();
|
|
2801
2926
|
if (fs15.pathExistsSync(manifestPath)) {
|
|
2802
2927
|
await fs15.remove(manifestPath);
|
|
@@ -2841,7 +2966,7 @@ async function runUninstallCommand(options) {
|
|
|
2841
2966
|
}
|
|
2842
2967
|
|
|
2843
2968
|
// src/commands/update.ts
|
|
2844
|
-
import
|
|
2969
|
+
import path22 from "path";
|
|
2845
2970
|
|
|
2846
2971
|
// src/update/diff-generated-files.ts
|
|
2847
2972
|
function diffGeneratedFiles() {
|
|
@@ -2868,11 +2993,12 @@ function summarizeLockDiff(before, after) {
|
|
|
2868
2993
|
|
|
2869
2994
|
// src/update/lockfile.ts
|
|
2870
2995
|
import { mkdir, readFile as readFile2, copyFile } from "fs/promises";
|
|
2871
|
-
import
|
|
2996
|
+
import path21 from "path";
|
|
2872
2997
|
async function checkLock(root) {
|
|
2873
2998
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
2874
2999
|
const hasValidVersions = lock.every((item) => !item.version || normalizeVersion(item.version) !== null);
|
|
2875
|
-
|
|
3000
|
+
const catalogRef = lock[0]?.catalogRef ?? null;
|
|
3001
|
+
return { ok: lock.length > 0 && hasValidVersions, count: lock.length, catalogRef };
|
|
2876
3002
|
}
|
|
2877
3003
|
async function applyLock(root) {
|
|
2878
3004
|
const lockPath = hausPath(root, "haus.lock.json");
|
|
@@ -2886,7 +3012,7 @@ async function applyLock(root) {
|
|
|
2886
3012
|
try {
|
|
2887
3013
|
const backupDir = hausPath(root, "backups");
|
|
2888
3014
|
await mkdir(backupDir, { recursive: true });
|
|
2889
|
-
await copyFile(lockPath,
|
|
3015
|
+
await copyFile(lockPath, path21.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
2890
3016
|
} catch {
|
|
2891
3017
|
}
|
|
2892
3018
|
const enriched = await Promise.all(
|
|
@@ -2908,7 +3034,7 @@ function diffLock(before, after) {
|
|
|
2908
3034
|
}
|
|
2909
3035
|
async function hasLocalOverrides(root) {
|
|
2910
3036
|
try {
|
|
2911
|
-
await readFile2(
|
|
3037
|
+
await readFile2(path21.join(root, ".claude", "settings.json"), "utf8");
|
|
2912
3038
|
return true;
|
|
2913
3039
|
} catch {
|
|
2914
3040
|
return false;
|
|
@@ -2916,35 +3042,29 @@ async function hasLocalOverrides(root) {
|
|
|
2916
3042
|
}
|
|
2917
3043
|
|
|
2918
3044
|
// 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
|
-
}
|
|
3045
|
+
var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
2938
3046
|
async function runUpdate(options) {
|
|
2939
3047
|
const root = process.cwd();
|
|
2940
3048
|
if (options.check) {
|
|
2941
|
-
const
|
|
3049
|
+
const pkgJson2 = await readJson(path22.join(packageRoot(), "package.json"));
|
|
3050
|
+
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
3051
|
+
const [status, npmVersion, latestCatalogTag] = await Promise.all([
|
|
3052
|
+
checkLock(root),
|
|
3053
|
+
fetchNpmVersionStatus(currentVersion2),
|
|
3054
|
+
fetchLatestCatalogTag()
|
|
3055
|
+
]);
|
|
3056
|
+
const installedRef = status.catalogRef ?? "main";
|
|
3057
|
+
const catalogRefBehind = latestCatalogTag !== null && installedRef !== latestCatalogTag ? `installed from ${installedRef}, latest tag is ${latestCatalogTag}` : false;
|
|
2942
3058
|
log(
|
|
2943
3059
|
JSON.stringify(
|
|
2944
3060
|
{
|
|
2945
3061
|
...status,
|
|
3062
|
+
installedCatalogRef: installedRef,
|
|
3063
|
+
latestCatalogTag,
|
|
3064
|
+
catalogRefBehind,
|
|
2946
3065
|
localOverrides: await hasLocalOverrides(root),
|
|
2947
|
-
summary: diffGeneratedFiles()
|
|
3066
|
+
summary: diffGeneratedFiles(),
|
|
3067
|
+
npmVersion
|
|
2948
3068
|
},
|
|
2949
3069
|
null,
|
|
2950
3070
|
2
|
|
@@ -2953,9 +3073,15 @@ async function runUpdate(options) {
|
|
|
2953
3073
|
if (!status.ok) process.exitCode = 1;
|
|
2954
3074
|
return;
|
|
2955
3075
|
}
|
|
2956
|
-
const pkgJson = await readJson(
|
|
3076
|
+
const pkgJson = await readJson(path22.join(packageRoot(), "package.json"));
|
|
2957
3077
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
2958
|
-
await
|
|
3078
|
+
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
3079
|
+
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
3080
|
+
log(`npm update available: ${currentVersion} \u2192 ${npmStatus.latest}`);
|
|
3081
|
+
log(`Run: npm install -g ${NPM_PACKAGE_NAME2}`);
|
|
3082
|
+
} else if (npmStatus.latest !== null) {
|
|
3083
|
+
log(`npm package up to date: ${currentVersion}`);
|
|
3084
|
+
}
|
|
2959
3085
|
if (await hasLocalOverrides(root)) {
|
|
2960
3086
|
log("Local .claude overrides detected. Preserving local files; only lockfile updated.");
|
|
2961
3087
|
}
|
|
@@ -2977,17 +3103,18 @@ async function runUpdate(options) {
|
|
|
2977
3103
|
}
|
|
2978
3104
|
|
|
2979
3105
|
// src/commands/validate-catalog.ts
|
|
2980
|
-
import
|
|
3106
|
+
import fs16 from "fs";
|
|
3107
|
+
import path24 from "path";
|
|
2981
3108
|
|
|
2982
3109
|
// src/catalog/allowed-stacks.ts
|
|
2983
|
-
import
|
|
3110
|
+
import path23 from "path";
|
|
2984
3111
|
async function readAllowedStacks(root) {
|
|
2985
|
-
const data = await readJson(
|
|
3112
|
+
const data = await readJson(path23.join(root, "library", "catalog", "allowed-stacks.json"));
|
|
2986
3113
|
return data?.stacks ?? [];
|
|
2987
3114
|
}
|
|
2988
3115
|
|
|
2989
|
-
// src/
|
|
2990
|
-
var
|
|
3116
|
+
// src/catalog/validation-rules.ts
|
|
3117
|
+
var FORBIDDEN_TAGS = [
|
|
2991
3118
|
"python",
|
|
2992
3119
|
"django",
|
|
2993
3120
|
"go",
|
|
@@ -3004,18 +3131,28 @@ var FORBIDDEN2 = [
|
|
|
3004
3131
|
"defi",
|
|
3005
3132
|
"trading"
|
|
3006
3133
|
];
|
|
3007
|
-
|
|
3134
|
+
var BANNED_AGENT_PHRASES = ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"];
|
|
3135
|
+
var REQUIRED_SKILL_SECTIONS = ["## Use when", "## Do not use when"];
|
|
3136
|
+
var REQUIRED_AGENT_SECTIONS = ["## Use when", "## Do not use when", "## Verification"];
|
|
3137
|
+
var RISKY_INSTALL_PATTERNS = [/\bnpx\s+-y\b/i, /\bnpx\s+--yes\b/i, /\byarn\s+dlx\b/i, /\bpnpm\s+dlx\b/i];
|
|
3138
|
+
var ALLOWED_NPX_PATTERN = /\bnpx\s+tsx\b/i;
|
|
3139
|
+
var ANY_NPX_PATTERN = /\bnpx\s+\S+/i;
|
|
3140
|
+
var HTTP_URL_PATTERN = /^http:\/\//i;
|
|
3141
|
+
var PLACEHOLDER_PATTERN = /\bTODO\b|\bPLACEHOLDER\b/i;
|
|
3142
|
+
|
|
3143
|
+
// src/commands/validate-catalog.ts
|
|
3144
|
+
function auditForbiddenStacks(items) {
|
|
3008
3145
|
const failures = [];
|
|
3009
3146
|
for (const item of items) {
|
|
3010
3147
|
const tags = Array.isArray(item.tags) ? item.tags : [];
|
|
3011
3148
|
const text = `${item.id} ${tags.join(" ")}`.toLowerCase();
|
|
3012
|
-
for (const word of
|
|
3149
|
+
for (const word of FORBIDDEN_TAGS) {
|
|
3013
3150
|
if (text.includes(word)) failures.push(`${item.id}: unsupported stack/tag "${word}"`);
|
|
3014
3151
|
}
|
|
3015
3152
|
}
|
|
3016
3153
|
return failures;
|
|
3017
3154
|
}
|
|
3018
|
-
|
|
3155
|
+
function auditManifestStructure(items) {
|
|
3019
3156
|
const failures = [];
|
|
3020
3157
|
const seenIds = /* @__PURE__ */ new Map();
|
|
3021
3158
|
const seenPaths = /* @__PURE__ */ new Map();
|
|
@@ -3041,7 +3178,7 @@ async function auditManifestStructure(items) {
|
|
|
3041
3178
|
} else {
|
|
3042
3179
|
seenIds.set(item.id, i);
|
|
3043
3180
|
}
|
|
3044
|
-
if (item.type === "skill" || item.type === "agent") {
|
|
3181
|
+
if (item.type === "skill" || item.type === "agent" || item.type === "template") {
|
|
3045
3182
|
if (!item.path) {
|
|
3046
3183
|
failures.push(`${item.id}: missing path`);
|
|
3047
3184
|
} else {
|
|
@@ -3059,7 +3196,7 @@ async function auditManifestStructure(items) {
|
|
|
3059
3196
|
failures.push(`${item.id}: source must be "haus" or curated with reviewStatus "approved"`);
|
|
3060
3197
|
}
|
|
3061
3198
|
for (const ref of item.references ?? []) {
|
|
3062
|
-
if (
|
|
3199
|
+
if (HTTP_URL_PATTERN.test(ref)) {
|
|
3063
3200
|
failures.push(`${item.id}: reference uses insecure http:// URL: ${ref}`);
|
|
3064
3201
|
}
|
|
3065
3202
|
}
|
|
@@ -3067,13 +3204,84 @@ async function auditManifestStructure(items) {
|
|
|
3067
3204
|
}
|
|
3068
3205
|
return failures;
|
|
3069
3206
|
}
|
|
3207
|
+
function auditShippedFiles(manifestDir, items) {
|
|
3208
|
+
const failures = [];
|
|
3209
|
+
for (const item of items) {
|
|
3210
|
+
if (!item.path) continue;
|
|
3211
|
+
const absPath = path24.join(manifestDir, item.path);
|
|
3212
|
+
if (item.type === "skill") {
|
|
3213
|
+
const skillMd = path24.join(absPath, "SKILL.md");
|
|
3214
|
+
if (!fs16.existsSync(skillMd)) {
|
|
3215
|
+
failures.push(`${item.id}: missing ${path24.relative(manifestDir, skillMd)}`);
|
|
3216
|
+
continue;
|
|
3217
|
+
}
|
|
3218
|
+
const text = fs16.readFileSync(skillMd, "utf8");
|
|
3219
|
+
for (const section of REQUIRED_SKILL_SECTIONS) {
|
|
3220
|
+
if (!text.includes(section)) failures.push(`${item.id}: SKILL.md missing ${section}`);
|
|
3221
|
+
}
|
|
3222
|
+
} else if (item.type === "agent") {
|
|
3223
|
+
if (!fs16.existsSync(absPath)) {
|
|
3224
|
+
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
3225
|
+
continue;
|
|
3226
|
+
}
|
|
3227
|
+
const text = fs16.readFileSync(absPath, "utf8");
|
|
3228
|
+
if (!text.startsWith("---")) failures.push(`${item.id}: agent file missing YAML frontmatter`);
|
|
3229
|
+
for (const section of REQUIRED_AGENT_SECTIONS) {
|
|
3230
|
+
if (!text.includes(section)) failures.push(`${item.id}: agent file missing ${section}`);
|
|
3231
|
+
}
|
|
3232
|
+
const lower = text.toLowerCase();
|
|
3233
|
+
for (const phrase of BANNED_AGENT_PHRASES) {
|
|
3234
|
+
if (lower.includes(phrase)) failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
|
|
3235
|
+
}
|
|
3236
|
+
} else if (item.type === "template") {
|
|
3237
|
+
if (!fs16.existsSync(absPath)) {
|
|
3238
|
+
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
return failures;
|
|
3243
|
+
}
|
|
3244
|
+
function auditMarkdownContent(manifestDir) {
|
|
3245
|
+
const failures = [];
|
|
3246
|
+
const dirs = ["skills", "agents"];
|
|
3247
|
+
for (const dir of dirs) {
|
|
3248
|
+
const abs = path24.join(manifestDir, dir);
|
|
3249
|
+
if (!fs16.existsSync(abs)) continue;
|
|
3250
|
+
walkMd(abs, (file) => {
|
|
3251
|
+
const text = fs16.readFileSync(file, "utf8");
|
|
3252
|
+
const rel = path24.relative(manifestDir, file);
|
|
3253
|
+
const lines = text.split(/\r?\n/);
|
|
3254
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3255
|
+
const line = lines[i] ?? "";
|
|
3256
|
+
if (PLACEHOLDER_PATTERN.test(line)) {
|
|
3257
|
+
failures.push(`${rel}:${i + 1}: TODO or placeholder in shipped content`);
|
|
3258
|
+
}
|
|
3259
|
+
if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line))) {
|
|
3260
|
+
failures.push(`${rel}:${i + 1}: risky install pattern`);
|
|
3261
|
+
}
|
|
3262
|
+
if (ANY_NPX_PATTERN.test(line) && !ALLOWED_NPX_PATTERN.test(line)) {
|
|
3263
|
+
failures.push(`${rel}:${i + 1}: disallowed npx (only npx tsx allowed)`);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
return failures;
|
|
3269
|
+
}
|
|
3270
|
+
function walkMd(dir, fn) {
|
|
3271
|
+
for (const entry of fs16.readdirSync(dir, { withFileTypes: true })) {
|
|
3272
|
+
const full = path24.join(dir, entry.name);
|
|
3273
|
+
if (entry.isDirectory()) walkMd(full, fn);
|
|
3274
|
+
else if (entry.name.endsWith(".md")) fn(full);
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3070
3277
|
async function runValidateCatalog(manifestPath) {
|
|
3071
3278
|
if (!manifestPath) {
|
|
3072
3279
|
error("Usage: haus validate-catalog <path/to/manifest.json>");
|
|
3073
3280
|
process.exitCode = 1;
|
|
3074
3281
|
return;
|
|
3075
3282
|
}
|
|
3076
|
-
const abs =
|
|
3283
|
+
const abs = path24.resolve(process.cwd(), manifestPath);
|
|
3284
|
+
const manifestDir = path24.dirname(abs);
|
|
3077
3285
|
const data = await readJson(abs);
|
|
3078
3286
|
if (!data?.items) {
|
|
3079
3287
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -3081,22 +3289,22 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3081
3289
|
return;
|
|
3082
3290
|
}
|
|
3083
3291
|
const items = data.items;
|
|
3084
|
-
const
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3292
|
+
const structureFailures = auditManifestStructure(items);
|
|
3293
|
+
const stackFailures = auditForbiddenStacks(items);
|
|
3294
|
+
const fileFailures = auditShippedFiles(manifestDir, items);
|
|
3295
|
+
const contentFailures = auditMarkdownContent(manifestDir);
|
|
3088
3296
|
const allowed = new Set((await readAllowedStacks(packageRoot())).map((x) => x.toLowerCase()));
|
|
3089
3297
|
const tagFailures = [];
|
|
3090
3298
|
if (allowed.size > 0) {
|
|
3091
3299
|
for (const item of items) {
|
|
3092
3300
|
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") {
|
|
3301
|
+
if (!allowed.has(tag.toLowerCase()) && !tag.includes("-patterns") && tag !== "haus" && tag !== "security" && tag !== "quality" && tag !== "review" && tag !== "workflow" && tag !== "baseline" && tag !== "project-instructions") {
|
|
3094
3302
|
tagFailures.push(`${item.id}: tag not in allowlist: "${tag}"`);
|
|
3095
3303
|
}
|
|
3096
3304
|
}
|
|
3097
3305
|
}
|
|
3098
3306
|
}
|
|
3099
|
-
const allFailures = [...structureFailures, ...stackFailures, ...tagFailures];
|
|
3307
|
+
const allFailures = [...structureFailures, ...stackFailures, ...fileFailures, ...contentFailures, ...tagFailures];
|
|
3100
3308
|
if (allFailures.length) {
|
|
3101
3309
|
allFailures.forEach((f) => error(f));
|
|
3102
3310
|
process.exitCode = 1;
|
|
@@ -3106,7 +3314,7 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3106
3314
|
}
|
|
3107
3315
|
|
|
3108
3316
|
// src/commands/workspace.ts
|
|
3109
|
-
import
|
|
3317
|
+
import path25 from "path";
|
|
3110
3318
|
import YAML from "yaml";
|
|
3111
3319
|
async function runWorkspace(action) {
|
|
3112
3320
|
if (action === "init") {
|
|
@@ -3139,7 +3347,7 @@ relationships: []
|
|
|
3139
3347
|
const summaries = [];
|
|
3140
3348
|
const ownership = {};
|
|
3141
3349
|
for (const repo of repos) {
|
|
3142
|
-
const repoRoot =
|
|
3350
|
+
const repoRoot = path25.resolve(process.cwd(), repo.path);
|
|
3143
3351
|
const result = await scanProject(repoRoot, "fast");
|
|
3144
3352
|
summaries.push({
|
|
3145
3353
|
name: repo.name,
|
|
@@ -3175,7 +3383,7 @@ ${summaries.map(
|
|
|
3175
3383
|
// src/cli.ts
|
|
3176
3384
|
function cliVersion() {
|
|
3177
3385
|
try {
|
|
3178
|
-
const pkgPath =
|
|
3386
|
+
const pkgPath = path26.join(packageRoot(), "package.json");
|
|
3179
3387
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
3180
3388
|
return pkg.version ?? "0.0.0";
|
|
3181
3389
|
} catch {
|
|
@@ -3185,7 +3393,7 @@ function cliVersion() {
|
|
|
3185
3393
|
var program = new Command();
|
|
3186
3394
|
function validateRuntimeNodeVersion() {
|
|
3187
3395
|
try {
|
|
3188
|
-
const pkgPath =
|
|
3396
|
+
const pkgPath = path26.join(packageRoot(), "package.json");
|
|
3189
3397
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
3190
3398
|
const requiredRange = pkg.engines?.node;
|
|
3191
3399
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -3203,7 +3411,7 @@ program.command("scan").option("--json").action(runScan);
|
|
|
3203
3411
|
program.command("recommend").option("--json").action(runRecommend);
|
|
3204
3412
|
program.command("setup-project").option("--guided").option("--fast").option("--json").action(runSetupProject);
|
|
3205
3413
|
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);
|
|
3414
|
+
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
3415
|
program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
|
|
3208
3416
|
program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
|
|
3209
3417
|
program.command("context").option("--task <task>").option("--from-hook").option("--json").option("--verbose").action(runContext);
|