@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/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 path25 from "path";
5
+ import path26 from "path";
6
6
  import { Command } from "commander";
7
7
 
8
- // src/claude/write-claude-files.ts
9
- import path9 from "path";
10
- import fs8 from "fs-extra";
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 manifest = await readJson(
660
- path9.join(pkgRoot, "library", "catalog", "manifest.json")
661
- ) ?? { items: [] };
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
- for (const item of rec.recommended) {
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(pkgRoot, manifestItem.path);
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 = rec.recommended.filter((r) => installedIds.has(r.id));
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
- const files = await writeClaudeFiles(root, isDryRun);
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 path10 from "path";
785
- var CACHE_MANIFEST = path10.join(os4.homedir(), CATALOG_CACHE_SUBDIR, "manifest.json");
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 = path10.join(root, "library/catalog/manifest.json");
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 = path10.join(packageRoot(), "library/catalog/manifest.json");
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 path12 from "path";
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 path11 from "path";
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(path11.join(root, "yarn.lock"))) return "yarn";
1217
- if (fs9.existsSync(path11.join(root, "pnpm-lock.yaml"))) return "pnpm";
1218
- if (fs9.existsSync(path11.join(root, "package-lock.json"))) return "npm";
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(path12.join(root, "package.json"));
1280
- const composer = await readJson(path12.join(root, "composer.json"));
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 ?? path12.basename(root)),
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(path12.join(root, f), "utf8"))]))
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
- if (files.some((f) => f.endsWith("wp-config.php")) && files.some((f) => f.includes("web/app"))) {
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(path12.join(root, rel), "utf8");
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 path13 from "path";
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 = path13.join(root, "CLAUDE.md");
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 = path13.join(packageRoot(), "library", "global", "templates", "haus-way-of-work.md");
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 path14 from "path";
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 = path14.join(root, ".haus-workflow");
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 path17 from "path";
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 path15 from "path";
2464
+ import path16 from "path";
2340
2465
  var MANIFEST_SCHEMA = "haus-install-manifest/1";
2341
2466
  function globalClaudeDir() {
2342
- return path15.join(os5.homedir(), ".claude");
2467
+ return path16.join(os5.homedir(), ".claude");
2343
2468
  }
2344
2469
  function hausManifestPath() {
2345
- return path15.join(globalClaudeDir(), "haus", "install-manifest.json");
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 path16 from "path";
2489
+ import path17 from "path";
2365
2490
  import fs12 from "fs-extra";
2366
2491
  function settingsJsonPath() {
2367
- return path16.join(globalClaudeDir(), "settings.json");
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 = path17.join(packageRoot(), "package.json");
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 path17.join(packageRoot(), "library", "global");
2571
+ return path18.join(packageRoot(), "library", "global");
2447
2572
  }
2448
2573
  function collectSourceFiles(srcDir, claudeDir) {
2449
2574
  const entries = [];
2450
- const skillsDir = path17.join(srcDir, "skills");
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 = path17.join(skillsDir, skillName, "SKILL.md");
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: path17.join("library", "global", "skills", skillName, "SKILL.md"),
2458
- destPath: path17.join(claudeDir, "skills", skillName, "SKILL.md")
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 = path17.join(srcDir, "agents");
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: path17.join("library", "global", "agents", agentFile),
2471
- destPath: path17.join(claudeDir, "agents", agentFile)
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 = path17.join(packageRoot(), entry.srcRelPath);
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 = path17.join(srcDir, "settings-fragments", "hooks.json");
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 path18 from "path";
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 = [path18.join(root, CLAUDE_DIR), path18.join(root, HAUS_DIR)];
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) => path18.relative(root, p)).join(" and ")}? This cannot be undone.`
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 ${path18.relative(root, p)}`);
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 path19 from "path";
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(path19.dirname(entry.destPath));
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 = path19.join(globalClaudeDir(), "haus");
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 path21 from "path";
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 path20 from "path";
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
- return { ok: lock.length > 0 && hasValidVersions, count: lock.length };
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, path20.join(backupDir, `haus.lock.${Date.now()}.json`));
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(path20.join(root, ".claude", "settings.json"), "utf8");
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 NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
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 status = await checkLock(root);
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(path21.join(packageRoot(), "package.json"));
3076
+ const pkgJson = await readJson(path22.join(packageRoot(), "package.json"));
2957
3077
  const currentVersion = pkgJson?.version ?? "0.0.0";
2958
- await checkNpmVersion(currentVersion);
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 path23 from "path";
3106
+ import fs16 from "fs";
3107
+ import path24 from "path";
2981
3108
 
2982
3109
  // src/catalog/allowed-stacks.ts
2983
- import path22 from "path";
3110
+ import path23 from "path";
2984
3111
  async function readAllowedStacks(root) {
2985
- const data = await readJson(path22.join(root, "library", "catalog", "allowed-stacks.json"));
3112
+ const data = await readJson(path23.join(root, "library", "catalog", "allowed-stacks.json"));
2986
3113
  return data?.stacks ?? [];
2987
3114
  }
2988
3115
 
2989
- // src/commands/validate-catalog.ts
2990
- var FORBIDDEN2 = [
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
- async function auditForbiddenStacks(items) {
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 FORBIDDEN2) {
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
- async function auditManifestStructure(items) {
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 (/^http:\/\//i.test(ref)) {
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 = path23.resolve(process.cwd(), manifestPath);
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 [structureFailures, stackFailures] = await Promise.all([
3085
- auditManifestStructure(items),
3086
- auditForbiddenStacks(items)
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 path24 from "path";
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 = path24.resolve(process.cwd(), repo.path);
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 = path25.join(packageRoot(), "package.json");
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 = path25.join(packageRoot(), "package.json");
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);