@haus-tech/haus-workflow 0.1.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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);
@@ -1415,7 +1489,7 @@ async function hasNeedle(root, files, needle) {
1415
1489
  );
1416
1490
  for (const rel of candidates.slice(0, 300)) {
1417
1491
  try {
1418
- const content = await readFile(path12.join(root, rel), "utf8");
1492
+ const content = await readFile(path13.join(root, rel), "utf8");
1419
1493
  if (content.includes(needle)) return true;
1420
1494
  } catch {
1421
1495
  continue;
@@ -1508,8 +1582,30 @@ async function runContext(options) {
1508
1582
  }
1509
1583
 
1510
1584
  // src/commands/doctor.ts
1511
- import path13 from "path";
1585
+ import path14 from "path";
1512
1586
  import fs10 from "fs-extra";
1587
+
1588
+ // src/update/npm-version.ts
1589
+ var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
1590
+ async function fetchNpmVersionStatus(currentVersion) {
1591
+ try {
1592
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE_NAME)}/latest`, {
1593
+ signal: AbortSignal.timeout(8e3)
1594
+ });
1595
+ if (!res.ok) return { current: currentVersion, latest: null, updateAvailable: false };
1596
+ const data = await res.json();
1597
+ const latest = data?.version;
1598
+ if (!latest || !normalizeVersion(latest) || !normalizeVersion(currentVersion)) {
1599
+ return { current: currentVersion, latest: null, updateAvailable: false };
1600
+ }
1601
+ const updateAvailable = compareVersions(latest, currentVersion) > 0;
1602
+ return { current: currentVersion, latest, updateAvailable };
1603
+ } catch {
1604
+ return { current: currentVersion, latest: null, updateAvailable: false };
1605
+ }
1606
+ }
1607
+
1608
+ // src/commands/doctor.ts
1513
1609
  async function runDoctor(options) {
1514
1610
  const root = process.cwd();
1515
1611
  if (options?.hooks) {
@@ -1554,7 +1650,7 @@ async function runDoctor(options) {
1554
1650
  const enabled = await isHookEnabled(root, key);
1555
1651
  log(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
1556
1652
  }
1557
- const rootClaudeMdPath = path13.join(root, "CLAUDE.md");
1653
+ const rootClaudeMdPath = path14.join(root, "CLAUDE.md");
1558
1654
  const rootClaudeMdContent = await readText(rootClaudeMdPath);
1559
1655
  if (!rootClaudeMdContent) {
1560
1656
  warn("- CLAUDE.md: missing (run `haus apply --write` to create)");
@@ -1574,7 +1670,7 @@ async function runDoctor(options) {
1574
1670
  warn("- .haus-workflow/haus-way-of-work.md: no HAUS-MANAGED header (user-owned)");
1575
1671
  } else {
1576
1672
  const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
1577
- const templatePath = path13.join(packageRoot(), "library", "global", "templates", "haus-way-of-work.md");
1673
+ const templatePath = path14.join(packageRoot(), "library", "global", "templates", "haus-way-of-work.md");
1578
1674
  const templateContent = await readText(templatePath);
1579
1675
  if (storedHashMatch && templateContent) {
1580
1676
  const currentHash = hashText(templateContent);
@@ -1612,6 +1708,17 @@ async function runDoctor(options) {
1612
1708
  log(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
1613
1709
  }
1614
1710
  }
1711
+ const pkgJson = await readJson(path14.join(packageRoot(), "package.json"));
1712
+ const currentVersion = pkgJson?.version ?? "0.0.0";
1713
+ const npmStatus = await fetchNpmVersionStatus(currentVersion);
1714
+ if (npmStatus.updateAvailable && npmStatus.latest !== null) {
1715
+ warn(`- CLI UPDATE: ${currentVersion} \u2192 ${npmStatus.latest} available (run: npm install -g ${NPM_PACKAGE_NAME})`);
1716
+ process.exitCode = 1;
1717
+ } else if (npmStatus.latest !== null) {
1718
+ log(`- CLI: ${currentVersion} (up to date)`);
1719
+ } else {
1720
+ log(`- CLI: ${currentVersion} (version check unavailable)`);
1721
+ }
1615
1722
  }
1616
1723
 
1617
1724
  // src/recommender/explain-formatters.ts
@@ -1766,7 +1873,7 @@ async function runGuard(kind, _options) {
1766
1873
  }
1767
1874
 
1768
1875
  // src/commands/init.ts
1769
- import path14 from "path";
1876
+ import path15 from "path";
1770
1877
  import fs11 from "fs-extra";
1771
1878
 
1772
1879
  // src/utils/exec.ts
@@ -2288,7 +2395,7 @@ async function runSetupProject(options) {
2288
2395
  // src/commands/init.ts
2289
2396
  async function runInit(options) {
2290
2397
  const root = process.cwd();
2291
- const hausDir = path14.join(root, ".haus-workflow");
2398
+ const hausDir = path15.join(root, ".haus-workflow");
2292
2399
  const alreadyInit = await fs11.pathExists(hausDir);
2293
2400
  if (alreadyInit) {
2294
2401
  log("Haus AI already initialized in this project.");
@@ -2301,7 +2408,7 @@ async function runInit(options) {
2301
2408
 
2302
2409
  // src/install/apply.ts
2303
2410
  import crypto2 from "crypto";
2304
- import path17 from "path";
2411
+ import path18 from "path";
2305
2412
  import fs13 from "fs-extra";
2306
2413
 
2307
2414
  // src/install/header.ts
@@ -2336,13 +2443,13 @@ ${content}`;
2336
2443
 
2337
2444
  // src/install/manifest.ts
2338
2445
  import os5 from "os";
2339
- import path15 from "path";
2446
+ import path16 from "path";
2340
2447
  var MANIFEST_SCHEMA = "haus-install-manifest/1";
2341
2448
  function globalClaudeDir() {
2342
- return path15.join(os5.homedir(), ".claude");
2449
+ return path16.join(os5.homedir(), ".claude");
2343
2450
  }
2344
2451
  function hausManifestPath() {
2345
- return path15.join(globalClaudeDir(), "haus", "install-manifest.json");
2452
+ return path16.join(globalClaudeDir(), "haus", "install-manifest.json");
2346
2453
  }
2347
2454
  async function readManifest() {
2348
2455
  return readJson(hausManifestPath());
@@ -2361,10 +2468,10 @@ function buildManifest(source, files, hooks) {
2361
2468
  }
2362
2469
 
2363
2470
  // src/install/settings-merge.ts
2364
- import path16 from "path";
2471
+ import path17 from "path";
2365
2472
  import fs12 from "fs-extra";
2366
2473
  function settingsJsonPath() {
2367
- return path16.join(globalClaudeDir(), "settings.json");
2474
+ return path17.join(globalClaudeDir(), "settings.json");
2368
2475
  }
2369
2476
  async function readSettings() {
2370
2477
  const parsed = await readJson(settingsJsonPath());
@@ -2435,7 +2542,7 @@ function hashContent(content) {
2435
2542
  }
2436
2543
  function sourceVersion() {
2437
2544
  try {
2438
- const pkgPath = path17.join(packageRoot(), "package.json");
2545
+ const pkgPath = path18.join(packageRoot(), "package.json");
2439
2546
  const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf8"));
2440
2547
  return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
2441
2548
  } catch {
@@ -2443,32 +2550,32 @@ function sourceVersion() {
2443
2550
  }
2444
2551
  }
2445
2552
  function globalSrcDir() {
2446
- return path17.join(packageRoot(), "library", "global");
2553
+ return path18.join(packageRoot(), "library", "global");
2447
2554
  }
2448
2555
  function collectSourceFiles(srcDir, claudeDir) {
2449
2556
  const entries = [];
2450
- const skillsDir = path17.join(srcDir, "skills");
2557
+ const skillsDir = path18.join(srcDir, "skills");
2451
2558
  if (fs13.pathExistsSync(skillsDir)) {
2452
2559
  for (const skillName of fs13.readdirSync(skillsDir)) {
2453
- const skillFile = path17.join(skillsDir, skillName, "SKILL.md");
2560
+ const skillFile = path18.join(skillsDir, skillName, "SKILL.md");
2454
2561
  if (fs13.pathExistsSync(skillFile)) {
2455
2562
  entries.push({
2456
2563
  stableId: `skill.${skillName}`,
2457
- srcRelPath: path17.join("library", "global", "skills", skillName, "SKILL.md"),
2458
- destPath: path17.join(claudeDir, "skills", skillName, "SKILL.md")
2564
+ srcRelPath: path18.join("library", "global", "skills", skillName, "SKILL.md"),
2565
+ destPath: path18.join(claudeDir, "skills", skillName, "SKILL.md")
2459
2566
  });
2460
2567
  }
2461
2568
  }
2462
2569
  }
2463
- const agentsDir = path17.join(srcDir, "agents");
2570
+ const agentsDir = path18.join(srcDir, "agents");
2464
2571
  if (fs13.pathExistsSync(agentsDir)) {
2465
2572
  for (const agentFile of fs13.readdirSync(agentsDir)) {
2466
2573
  if (!agentFile.endsWith(".md")) continue;
2467
2574
  const agentName = agentFile.replace(/\.md$/, "");
2468
2575
  entries.push({
2469
2576
  stableId: `agent.${agentName}`,
2470
- srcRelPath: path17.join("library", "global", "agents", agentFile),
2471
- destPath: path17.join(claudeDir, "agents", agentFile)
2577
+ srcRelPath: path18.join("library", "global", "agents", agentFile),
2578
+ destPath: path18.join(claudeDir, "agents", agentFile)
2472
2579
  });
2473
2580
  }
2474
2581
  }
@@ -2492,7 +2599,7 @@ async function applyInstall(options = {}) {
2492
2599
  };
2493
2600
  const manifestFiles = [];
2494
2601
  for (const entry of sourceFiles) {
2495
- const srcPath = path17.join(packageRoot(), entry.srcRelPath);
2602
+ const srcPath = path18.join(packageRoot(), entry.srcRelPath);
2496
2603
  const rawContent = await readText(srcPath);
2497
2604
  if (rawContent === void 0) {
2498
2605
  warn(`Source file not found: ${entry.srcRelPath}`);
@@ -2548,7 +2655,7 @@ async function applyInstall(options = {}) {
2548
2655
  schemaVersion: SCHEMA_VERSION3
2549
2656
  });
2550
2657
  }
2551
- const fragmentPath = path17.join(srcDir, "settings-fragments", "hooks.json");
2658
+ const fragmentPath = path18.join(srcDir, "settings-fragments", "hooks.json");
2552
2659
  const fragments = await loadHooksFragment(fragmentPath);
2553
2660
  const settings = await readSettings();
2554
2661
  const { settings: mergedSettings, addedIds } = mergeHooks(settings, fragments);
@@ -2733,12 +2840,12 @@ async function runScan(options) {
2733
2840
  }
2734
2841
 
2735
2842
  // src/commands/undo.ts
2736
- import path18 from "path";
2843
+ import path19 from "path";
2737
2844
  import fs14 from "fs-extra";
2738
2845
  var CLAUDE_DIR = ".claude";
2739
2846
  async function runUndo(options) {
2740
2847
  const root = process.cwd();
2741
- const targets = [path18.join(root, CLAUDE_DIR), path18.join(root, HAUS_DIR)];
2848
+ const targets = [path19.join(root, CLAUDE_DIR), path19.join(root, HAUS_DIR)];
2742
2849
  const existing = targets.filter((p) => fs14.existsSync(p));
2743
2850
  if (existing.length === 0) {
2744
2851
  log("Nothing to remove: no .claude/ or .haus-workflow/ in this directory.");
@@ -2746,7 +2853,7 @@ async function runUndo(options) {
2746
2853
  }
2747
2854
  if (!options.yes) {
2748
2855
  const ok = await confirm(
2749
- `Remove ${existing.map((p) => path18.relative(root, p)).join(" and ")}? This cannot be undone.`
2856
+ `Remove ${existing.map((p) => path19.relative(root, p)).join(" and ")}? This cannot be undone.`
2750
2857
  );
2751
2858
  if (!ok) {
2752
2859
  log("Cancelled.");
@@ -2755,13 +2862,13 @@ async function runUndo(options) {
2755
2862
  }
2756
2863
  for (const p of existing) {
2757
2864
  await fs14.remove(p);
2758
- log(`Removed ${path18.relative(root, p)}`);
2865
+ log(`Removed ${path19.relative(root, p)}`);
2759
2866
  }
2760
2867
  }
2761
2868
 
2762
2869
  // src/install/uninstall.ts
2763
2870
  import crypto3 from "crypto";
2764
- import path19 from "path";
2871
+ import path20 from "path";
2765
2872
  import fs15 from "fs-extra";
2766
2873
  async function runUninstall(options = {}) {
2767
2874
  const { force = false } = options;
@@ -2789,14 +2896,14 @@ async function runUninstall(options = {}) {
2789
2896
  continue;
2790
2897
  }
2791
2898
  await fs15.remove(entry.destPath);
2792
- await pruneEmptyDir(path19.dirname(entry.destPath));
2899
+ await pruneEmptyDir(path20.dirname(entry.destPath));
2793
2900
  result.deleted.push(entry.destPath);
2794
2901
  }
2795
2902
  const settings = await readSettings();
2796
2903
  const stripped = stripHausHooks(settings);
2797
2904
  await writeSettings(stripped);
2798
2905
  result.hooksStripped = true;
2799
- const hausDir = path19.join(globalClaudeDir(), "haus");
2906
+ const hausDir = path20.join(globalClaudeDir(), "haus");
2800
2907
  const manifestPath = hausManifestPath();
2801
2908
  if (fs15.pathExistsSync(manifestPath)) {
2802
2909
  await fs15.remove(manifestPath);
@@ -2841,7 +2948,7 @@ async function runUninstallCommand(options) {
2841
2948
  }
2842
2949
 
2843
2950
  // src/commands/update.ts
2844
- import path21 from "path";
2951
+ import path22 from "path";
2845
2952
 
2846
2953
  // src/update/diff-generated-files.ts
2847
2954
  function diffGeneratedFiles() {
@@ -2868,11 +2975,12 @@ function summarizeLockDiff(before, after) {
2868
2975
 
2869
2976
  // src/update/lockfile.ts
2870
2977
  import { mkdir, readFile as readFile2, copyFile } from "fs/promises";
2871
- import path20 from "path";
2978
+ import path21 from "path";
2872
2979
  async function checkLock(root) {
2873
2980
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
2874
2981
  const hasValidVersions = lock.every((item) => !item.version || normalizeVersion(item.version) !== null);
2875
- return { ok: lock.length > 0 && hasValidVersions, count: lock.length };
2982
+ const catalogRef = lock[0]?.catalogRef ?? null;
2983
+ return { ok: lock.length > 0 && hasValidVersions, count: lock.length, catalogRef };
2876
2984
  }
2877
2985
  async function applyLock(root) {
2878
2986
  const lockPath = hausPath(root, "haus.lock.json");
@@ -2886,7 +2994,7 @@ async function applyLock(root) {
2886
2994
  try {
2887
2995
  const backupDir = hausPath(root, "backups");
2888
2996
  await mkdir(backupDir, { recursive: true });
2889
- await copyFile(lockPath, path20.join(backupDir, `haus.lock.${Date.now()}.json`));
2997
+ await copyFile(lockPath, path21.join(backupDir, `haus.lock.${Date.now()}.json`));
2890
2998
  } catch {
2891
2999
  }
2892
3000
  const enriched = await Promise.all(
@@ -2908,7 +3016,7 @@ function diffLock(before, after) {
2908
3016
  }
2909
3017
  async function hasLocalOverrides(root) {
2910
3018
  try {
2911
- await readFile2(path20.join(root, ".claude", "settings.json"), "utf8");
3019
+ await readFile2(path21.join(root, ".claude", "settings.json"), "utf8");
2912
3020
  return true;
2913
3021
  } catch {
2914
3022
  return false;
@@ -2916,35 +3024,29 @@ async function hasLocalOverrides(root) {
2916
3024
  }
2917
3025
 
2918
3026
  // src/commands/update.ts
2919
- var 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
- }
3027
+ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
2938
3028
  async function runUpdate(options) {
2939
3029
  const root = process.cwd();
2940
3030
  if (options.check) {
2941
- const status = await checkLock(root);
3031
+ const pkgJson2 = await readJson(path22.join(packageRoot(), "package.json"));
3032
+ const currentVersion2 = pkgJson2?.version ?? "0.0.0";
3033
+ const [status, npmVersion, latestCatalogTag] = await Promise.all([
3034
+ checkLock(root),
3035
+ fetchNpmVersionStatus(currentVersion2),
3036
+ fetchLatestCatalogTag()
3037
+ ]);
3038
+ const installedRef = status.catalogRef ?? "main";
3039
+ const catalogRefBehind = latestCatalogTag !== null && installedRef !== latestCatalogTag ? `installed from ${installedRef}, latest tag is ${latestCatalogTag}` : false;
2942
3040
  log(
2943
3041
  JSON.stringify(
2944
3042
  {
2945
3043
  ...status,
3044
+ installedCatalogRef: installedRef,
3045
+ latestCatalogTag,
3046
+ catalogRefBehind,
2946
3047
  localOverrides: await hasLocalOverrides(root),
2947
- summary: diffGeneratedFiles()
3048
+ summary: diffGeneratedFiles(),
3049
+ npmVersion
2948
3050
  },
2949
3051
  null,
2950
3052
  2
@@ -2953,9 +3055,15 @@ async function runUpdate(options) {
2953
3055
  if (!status.ok) process.exitCode = 1;
2954
3056
  return;
2955
3057
  }
2956
- const pkgJson = await readJson(path21.join(packageRoot(), "package.json"));
3058
+ const pkgJson = await readJson(path22.join(packageRoot(), "package.json"));
2957
3059
  const currentVersion = pkgJson?.version ?? "0.0.0";
2958
- await checkNpmVersion(currentVersion);
3060
+ const npmStatus = await fetchNpmVersionStatus(currentVersion);
3061
+ if (npmStatus.updateAvailable && npmStatus.latest !== null) {
3062
+ log(`npm update available: ${currentVersion} \u2192 ${npmStatus.latest}`);
3063
+ log(`Run: npm install -g ${NPM_PACKAGE_NAME2}`);
3064
+ } else if (npmStatus.latest !== null) {
3065
+ log(`npm package up to date: ${currentVersion}`);
3066
+ }
2959
3067
  if (await hasLocalOverrides(root)) {
2960
3068
  log("Local .claude overrides detected. Preserving local files; only lockfile updated.");
2961
3069
  }
@@ -2977,17 +3085,18 @@ async function runUpdate(options) {
2977
3085
  }
2978
3086
 
2979
3087
  // src/commands/validate-catalog.ts
2980
- import path23 from "path";
3088
+ import fs16 from "fs";
3089
+ import path24 from "path";
2981
3090
 
2982
3091
  // src/catalog/allowed-stacks.ts
2983
- import path22 from "path";
3092
+ import path23 from "path";
2984
3093
  async function readAllowedStacks(root) {
2985
- const data = await readJson(path22.join(root, "library", "catalog", "allowed-stacks.json"));
3094
+ const data = await readJson(path23.join(root, "library", "catalog", "allowed-stacks.json"));
2986
3095
  return data?.stacks ?? [];
2987
3096
  }
2988
3097
 
2989
- // src/commands/validate-catalog.ts
2990
- var FORBIDDEN2 = [
3098
+ // src/catalog/validation-rules.ts
3099
+ var FORBIDDEN_TAGS = [
2991
3100
  "python",
2992
3101
  "django",
2993
3102
  "go",
@@ -3004,18 +3113,28 @@ var FORBIDDEN2 = [
3004
3113
  "defi",
3005
3114
  "trading"
3006
3115
  ];
3007
- async function auditForbiddenStacks(items) {
3116
+ var BANNED_AGENT_PHRASES = ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"];
3117
+ var REQUIRED_SKILL_SECTIONS = ["## Use when", "## Do not use when"];
3118
+ var REQUIRED_AGENT_SECTIONS = ["## Use when", "## Do not use when", "## Verification"];
3119
+ var RISKY_INSTALL_PATTERNS = [/\bnpx\s+-y\b/i, /\bnpx\s+--yes\b/i, /\byarn\s+dlx\b/i, /\bpnpm\s+dlx\b/i];
3120
+ var ALLOWED_NPX_PATTERN = /\bnpx\s+tsx\b/i;
3121
+ var ANY_NPX_PATTERN = /\bnpx\s+\S+/i;
3122
+ var HTTP_URL_PATTERN = /^http:\/\//i;
3123
+ var PLACEHOLDER_PATTERN = /\bTODO\b|\bPLACEHOLDER\b/i;
3124
+
3125
+ // src/commands/validate-catalog.ts
3126
+ function auditForbiddenStacks(items) {
3008
3127
  const failures = [];
3009
3128
  for (const item of items) {
3010
3129
  const tags = Array.isArray(item.tags) ? item.tags : [];
3011
3130
  const text = `${item.id} ${tags.join(" ")}`.toLowerCase();
3012
- for (const word of FORBIDDEN2) {
3131
+ for (const word of FORBIDDEN_TAGS) {
3013
3132
  if (text.includes(word)) failures.push(`${item.id}: unsupported stack/tag "${word}"`);
3014
3133
  }
3015
3134
  }
3016
3135
  return failures;
3017
3136
  }
3018
- async function auditManifestStructure(items) {
3137
+ function auditManifestStructure(items) {
3019
3138
  const failures = [];
3020
3139
  const seenIds = /* @__PURE__ */ new Map();
3021
3140
  const seenPaths = /* @__PURE__ */ new Map();
@@ -3041,7 +3160,7 @@ async function auditManifestStructure(items) {
3041
3160
  } else {
3042
3161
  seenIds.set(item.id, i);
3043
3162
  }
3044
- if (item.type === "skill" || item.type === "agent") {
3163
+ if (item.type === "skill" || item.type === "agent" || item.type === "template") {
3045
3164
  if (!item.path) {
3046
3165
  failures.push(`${item.id}: missing path`);
3047
3166
  } else {
@@ -3059,7 +3178,7 @@ async function auditManifestStructure(items) {
3059
3178
  failures.push(`${item.id}: source must be "haus" or curated with reviewStatus "approved"`);
3060
3179
  }
3061
3180
  for (const ref of item.references ?? []) {
3062
- if (/^http:\/\//i.test(ref)) {
3181
+ if (HTTP_URL_PATTERN.test(ref)) {
3063
3182
  failures.push(`${item.id}: reference uses insecure http:// URL: ${ref}`);
3064
3183
  }
3065
3184
  }
@@ -3067,13 +3186,84 @@ async function auditManifestStructure(items) {
3067
3186
  }
3068
3187
  return failures;
3069
3188
  }
3189
+ function auditShippedFiles(manifestDir, items) {
3190
+ const failures = [];
3191
+ for (const item of items) {
3192
+ if (!item.path) continue;
3193
+ const absPath = path24.join(manifestDir, item.path);
3194
+ if (item.type === "skill") {
3195
+ const skillMd = path24.join(absPath, "SKILL.md");
3196
+ if (!fs16.existsSync(skillMd)) {
3197
+ failures.push(`${item.id}: missing ${path24.relative(manifestDir, skillMd)}`);
3198
+ continue;
3199
+ }
3200
+ const text = fs16.readFileSync(skillMd, "utf8");
3201
+ for (const section of REQUIRED_SKILL_SECTIONS) {
3202
+ if (!text.includes(section)) failures.push(`${item.id}: SKILL.md missing ${section}`);
3203
+ }
3204
+ } else if (item.type === "agent") {
3205
+ if (!fs16.existsSync(absPath)) {
3206
+ failures.push(`${item.id}: missing agent file ${item.path}`);
3207
+ continue;
3208
+ }
3209
+ const text = fs16.readFileSync(absPath, "utf8");
3210
+ if (!text.startsWith("---")) failures.push(`${item.id}: agent file missing YAML frontmatter`);
3211
+ for (const section of REQUIRED_AGENT_SECTIONS) {
3212
+ if (!text.includes(section)) failures.push(`${item.id}: agent file missing ${section}`);
3213
+ }
3214
+ const lower = text.toLowerCase();
3215
+ for (const phrase of BANNED_AGENT_PHRASES) {
3216
+ if (lower.includes(phrase)) failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
3217
+ }
3218
+ } else if (item.type === "template") {
3219
+ if (!fs16.existsSync(absPath)) {
3220
+ failures.push(`${item.id}: missing template file ${item.path}`);
3221
+ }
3222
+ }
3223
+ }
3224
+ return failures;
3225
+ }
3226
+ function auditMarkdownContent(manifestDir) {
3227
+ const failures = [];
3228
+ const dirs = ["skills", "agents"];
3229
+ for (const dir of dirs) {
3230
+ const abs = path24.join(manifestDir, dir);
3231
+ if (!fs16.existsSync(abs)) continue;
3232
+ walkMd(abs, (file) => {
3233
+ const text = fs16.readFileSync(file, "utf8");
3234
+ const rel = path24.relative(manifestDir, file);
3235
+ const lines = text.split(/\r?\n/);
3236
+ for (let i = 0; i < lines.length; i++) {
3237
+ const line = lines[i] ?? "";
3238
+ if (PLACEHOLDER_PATTERN.test(line)) {
3239
+ failures.push(`${rel}:${i + 1}: TODO or placeholder in shipped content`);
3240
+ }
3241
+ if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line))) {
3242
+ failures.push(`${rel}:${i + 1}: risky install pattern`);
3243
+ }
3244
+ if (ANY_NPX_PATTERN.test(line) && !ALLOWED_NPX_PATTERN.test(line)) {
3245
+ failures.push(`${rel}:${i + 1}: disallowed npx (only npx tsx allowed)`);
3246
+ }
3247
+ }
3248
+ });
3249
+ }
3250
+ return failures;
3251
+ }
3252
+ function walkMd(dir, fn) {
3253
+ for (const entry of fs16.readdirSync(dir, { withFileTypes: true })) {
3254
+ const full = path24.join(dir, entry.name);
3255
+ if (entry.isDirectory()) walkMd(full, fn);
3256
+ else if (entry.name.endsWith(".md")) fn(full);
3257
+ }
3258
+ }
3070
3259
  async function runValidateCatalog(manifestPath) {
3071
3260
  if (!manifestPath) {
3072
3261
  error("Usage: haus validate-catalog <path/to/manifest.json>");
3073
3262
  process.exitCode = 1;
3074
3263
  return;
3075
3264
  }
3076
- const abs = path23.resolve(process.cwd(), manifestPath);
3265
+ const abs = path24.resolve(process.cwd(), manifestPath);
3266
+ const manifestDir = path24.dirname(abs);
3077
3267
  const data = await readJson(abs);
3078
3268
  if (!data?.items) {
3079
3269
  error(`Could not read catalog manifest at ${abs}`);
@@ -3081,22 +3271,22 @@ async function runValidateCatalog(manifestPath) {
3081
3271
  return;
3082
3272
  }
3083
3273
  const items = data.items;
3084
- const [structureFailures, stackFailures] = await Promise.all([
3085
- auditManifestStructure(items),
3086
- auditForbiddenStacks(items)
3087
- ]);
3274
+ const structureFailures = auditManifestStructure(items);
3275
+ const stackFailures = auditForbiddenStacks(items);
3276
+ const fileFailures = auditShippedFiles(manifestDir, items);
3277
+ const contentFailures = auditMarkdownContent(manifestDir);
3088
3278
  const allowed = new Set((await readAllowedStacks(packageRoot())).map((x) => x.toLowerCase()));
3089
3279
  const tagFailures = [];
3090
3280
  if (allowed.size > 0) {
3091
3281
  for (const item of items) {
3092
3282
  for (const tag of Array.isArray(item.tags) ? item.tags : []) {
3093
- if (!allowed.has(tag.toLowerCase()) && !tag.includes("-patterns") && tag !== "haus" && tag !== "security" && tag !== "quality" && tag !== "review" && tag !== "workflow") {
3283
+ if (!allowed.has(tag.toLowerCase()) && !tag.includes("-patterns") && tag !== "haus" && tag !== "security" && tag !== "quality" && tag !== "review" && tag !== "workflow" && tag !== "baseline" && tag !== "project-instructions") {
3094
3284
  tagFailures.push(`${item.id}: tag not in allowlist: "${tag}"`);
3095
3285
  }
3096
3286
  }
3097
3287
  }
3098
3288
  }
3099
- const allFailures = [...structureFailures, ...stackFailures, ...tagFailures];
3289
+ const allFailures = [...structureFailures, ...stackFailures, ...fileFailures, ...contentFailures, ...tagFailures];
3100
3290
  if (allFailures.length) {
3101
3291
  allFailures.forEach((f) => error(f));
3102
3292
  process.exitCode = 1;
@@ -3106,7 +3296,7 @@ async function runValidateCatalog(manifestPath) {
3106
3296
  }
3107
3297
 
3108
3298
  // src/commands/workspace.ts
3109
- import path24 from "path";
3299
+ import path25 from "path";
3110
3300
  import YAML from "yaml";
3111
3301
  async function runWorkspace(action) {
3112
3302
  if (action === "init") {
@@ -3139,7 +3329,7 @@ relationships: []
3139
3329
  const summaries = [];
3140
3330
  const ownership = {};
3141
3331
  for (const repo of repos) {
3142
- const repoRoot = path24.resolve(process.cwd(), repo.path);
3332
+ const repoRoot = path25.resolve(process.cwd(), repo.path);
3143
3333
  const result = await scanProject(repoRoot, "fast");
3144
3334
  summaries.push({
3145
3335
  name: repo.name,
@@ -3175,7 +3365,7 @@ ${summaries.map(
3175
3365
  // src/cli.ts
3176
3366
  function cliVersion() {
3177
3367
  try {
3178
- const pkgPath = path25.join(packageRoot(), "package.json");
3368
+ const pkgPath = path26.join(packageRoot(), "package.json");
3179
3369
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
3180
3370
  return pkg.version ?? "0.0.0";
3181
3371
  } catch {
@@ -3185,7 +3375,7 @@ function cliVersion() {
3185
3375
  var program = new Command();
3186
3376
  function validateRuntimeNodeVersion() {
3187
3377
  try {
3188
- const pkgPath = path25.join(packageRoot(), "package.json");
3378
+ const pkgPath = path26.join(packageRoot(), "package.json");
3189
3379
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
3190
3380
  const requiredRange = pkg.engines?.node;
3191
3381
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
@@ -3203,7 +3393,7 @@ program.command("scan").option("--json").action(runScan);
3203
3393
  program.command("recommend").option("--json").action(runRecommend);
3204
3394
  program.command("setup-project").option("--guided").option("--fast").option("--json").action(runSetupProject);
3205
3395
  program.command("doctor").option("--hooks", "Verify .claude/settings.json matches the hook contract").action(runDoctor);
3206
- program.command("apply").option("--dry-run").option("--write").action(runApply);
3396
+ program.command("apply").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option("--allow-empty-cache", "Apply core files only when catalog cache is empty (skip catalog items without error)").action(runApply);
3207
3397
  program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
3208
3398
  program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
3209
3399
  program.command("context").option("--task <task>").option("--from-hook").option("--json").option("--verbose").action(runContext);