@haus-tech/haus-workflow 0.27.1 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.28.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.27.1...v0.28.0) (2026-06-17)
4
+
5
+ ### Features
6
+
7
+ - catalog compatibility for curated provenance and deprecations ([#121](https://github.com/WeAreHausTech/haus-workflow/issues/121)) ([1a8a790](https://github.com/WeAreHausTech/haus-workflow/commit/1a8a7902bf7c27462e905d01dfa4d0baac319b57))
8
+ - **cloneandsetup:** add up-front data-access preflight + seed access/empty-ok ([#120](https://github.com/WeAreHausTech/haus-workflow/issues/120)) ([70e32ad](https://github.com/WeAreHausTech/haus-workflow/commit/70e32adba2f5ed25ade170a09d1f0cfb2bd198d7))
9
+
3
10
  ## [0.27.1](https://github.com/WeAreHausTech/haus-workflow/compare/v0.27.0...v0.27.1) (2026-06-16)
4
11
 
5
12
  ### Bug Fixes
package/dist/cli.js CHANGED
@@ -1,22 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { readFileSync as readFileSync4 } from "fs";
5
- import path35 from "path";
4
+ import { readFileSync as readFileSync4 } from "node:fs";
5
+ import path35 from "node:path";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/commands/apply.ts
9
- import path13 from "path";
9
+ import path13 from "node:path";
10
10
  import checkbox from "@inquirer/checkbox";
11
11
 
12
12
  // src/catalog/remote-catalog.ts
13
- import os from "os";
14
- import path2 from "path";
13
+ import os from "node:os";
14
+ import path2 from "node:path";
15
15
  import fs2 from "fs-extra";
16
16
 
17
17
  // src/utils/fs.ts
18
- import crypto from "crypto";
19
- import path from "path";
18
+ import crypto from "node:crypto";
19
+ import path from "node:path";
20
20
  import fg from "fast-glob";
21
21
  import fs from "fs-extra";
22
22
  var MalformedJsonFileError = class extends Error {
@@ -372,14 +372,71 @@ function validateCatalogItem(item, content2) {
372
372
  return { ok: true };
373
373
  }
374
374
 
375
- // src/catalog/manifest-schema.ts
376
- var POLLUTION_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
375
+ // src/catalog/manifest-item-fields.ts
376
+ var USE_MODES = /* @__PURE__ */ new Set(["copy", "adapted", "wrapped", "rewritten", "reference-only"]);
377
+ var LICENSE_CONFIDENCES = /* @__PURE__ */ new Set(["high", "medium", "low", "unknown"]);
378
+ var REVIEW_STATUSES = /* @__PURE__ */ new Set([
379
+ "approved",
380
+ "candidate",
381
+ "needs-review",
382
+ "rejected",
383
+ "deprecated"
384
+ ]);
385
+ var RISK_LEVELS = /* @__PURE__ */ new Set(["low", "medium", "high", "blocked"]);
377
386
  function isNonEmptyString(value) {
378
387
  return typeof value === "string" && value.length > 0;
379
388
  }
380
389
  function isStringArray(value) {
381
390
  return Array.isArray(value) && value.every((entry) => typeof entry === "string");
382
391
  }
392
+ function validateReferences(itemId, references) {
393
+ if (references === void 0) return null;
394
+ if (!isStringArray(references)) return `${itemId}: references must be a string array`;
395
+ for (const ref of references) {
396
+ if (/^https?:\/\//i.test(ref)) {
397
+ if (!/^https:\/\//i.test(ref)) {
398
+ return `${itemId}: reference must be https:// URL: ${ref}`;
399
+ }
400
+ }
401
+ }
402
+ return null;
403
+ }
404
+ function validateCuratedProvenance(item) {
405
+ if (item.source !== "curated") return null;
406
+ if (!isNonEmptyString(item.reviewStatus)) {
407
+ return `${item.id}: curated item missing reviewStatus`;
408
+ }
409
+ if (!REVIEW_STATUSES.has(item.reviewStatus)) {
410
+ return `${item.id}: invalid reviewStatus "${item.reviewStatus}"`;
411
+ }
412
+ if (!isNonEmptyString(item.riskLevel)) {
413
+ return `${item.id}: curated item missing riskLevel`;
414
+ }
415
+ if (!RISK_LEVELS.has(item.riskLevel)) {
416
+ return `${item.id}: invalid riskLevel "${item.riskLevel}"`;
417
+ }
418
+ if (item.useMode !== void 0 && !USE_MODES.has(String(item.useMode))) {
419
+ return `${item.id}: invalid useMode "${item.useMode}"`;
420
+ }
421
+ if (item.licenseConfidence !== void 0 && !LICENSE_CONFIDENCES.has(String(item.licenseConfidence))) {
422
+ return `${item.id}: invalid licenseConfidence "${item.licenseConfidence}"`;
423
+ }
424
+ if (item.originUrl !== void 0) {
425
+ if (typeof item.originUrl !== "string" || !item.originUrl.startsWith("https://")) {
426
+ return `${item.id}: originUrl must be an https:// URL`;
427
+ }
428
+ }
429
+ return null;
430
+ }
431
+
432
+ // src/catalog/manifest-schema.ts
433
+ var POLLUTION_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
434
+ function isNonEmptyString2(value) {
435
+ return typeof value === "string" && value.length > 0;
436
+ }
437
+ function isStringArray2(value) {
438
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
439
+ }
383
440
  var REQUIRES_ANY_KEYS = ["stack", "dependency", "packageNamePattern", "role"];
384
441
  function isRequiresAnyClause(value) {
385
442
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
@@ -409,7 +466,7 @@ function parseManifest(json) {
409
466
  return { ok: false, error: "manifest must be an object" };
410
467
  }
411
468
  const root = data;
412
- if (!isNonEmptyString(root.version)) {
469
+ if (!isNonEmptyString2(root.version)) {
413
470
  return { ok: false, error: "missing version" };
414
471
  }
415
472
  if (!Array.isArray(root.items)) {
@@ -424,13 +481,13 @@ function parseManifest(json) {
424
481
  return { ok: false, error: `item[${i}]: invalid entry` };
425
482
  }
426
483
  const item = raw;
427
- if (!isNonEmptyString(item.id)) {
484
+ if (!isNonEmptyString2(item.id)) {
428
485
  return { ok: false, error: `item[${i}]: missing id` };
429
486
  }
430
- if (!isNonEmptyString(item.type)) {
487
+ if (!isNonEmptyString2(item.type)) {
431
488
  return { ok: false, error: `${item.id}: missing type` };
432
489
  }
433
- if (!isNonEmptyString(item.path)) {
490
+ if (!isNonEmptyString2(item.path)) {
434
491
  return { ok: false, error: `${item.id}: missing path` };
435
492
  }
436
493
  if (seenIds.has(item.id)) {
@@ -442,18 +499,14 @@ function parseManifest(json) {
442
499
  return { ok: false, error: `${item.id}: duplicate path "${normPath}"` };
443
500
  }
444
501
  seenPaths.add(normPath);
445
- if (item.source === "curated") {
446
- if (!isNonEmptyString(item.reviewStatus)) {
447
- return { ok: false, error: `${item.id}: curated item missing reviewStatus` };
448
- }
449
- if (!isNonEmptyString(item.riskLevel)) {
450
- return { ok: false, error: `${item.id}: curated item missing riskLevel` };
451
- }
452
- }
453
- if (!isStringArray(item.tags)) {
502
+ const provenanceError = validateCuratedProvenance(item);
503
+ if (provenanceError) return { ok: false, error: provenanceError };
504
+ const referencesError = validateReferences(String(item.id), item.references);
505
+ if (referencesError) return { ok: false, error: referencesError };
506
+ if (!isStringArray2(item.tags)) {
454
507
  return { ok: false, error: `${item.id}: tags must be a string array` };
455
508
  }
456
- if (!isStringArray(item.repoRoles)) {
509
+ if (!isStringArray2(item.repoRoles)) {
457
510
  return { ok: false, error: `${item.id}: repoRoles must be a string array` };
458
511
  }
459
512
  if (item.requiresAny !== void 0 && !isRequiresAnyArray(item.requiresAny)) {
@@ -924,17 +977,17 @@ async function getCacheManifestAge() {
924
977
  }
925
978
 
926
979
  // src/claude/write-claude-files.ts
927
- import path12 from "path";
980
+ import path12 from "node:path";
928
981
  import fs12 from "fs-extra";
929
982
 
930
983
  // src/catalog/load-catalog.ts
931
- import path4 from "path";
984
+ import path4 from "node:path";
932
985
 
933
986
  // src/utils/paths.ts
934
- import { existsSync, readFileSync } from "fs";
935
- import os2 from "os";
936
- import path3 from "path";
937
- import { fileURLToPath } from "url";
987
+ import { existsSync, readFileSync } from "node:fs";
988
+ import os2 from "node:os";
989
+ import path3 from "node:path";
990
+ import { fileURLToPath } from "node:url";
938
991
  var HAUS_DIR = ".haus-workflow";
939
992
  function hausPath(root, ...parts) {
940
993
  return path3.join(root, HAUS_DIR, ...parts);
@@ -1019,7 +1072,7 @@ function catalogItemContentPath(contentRoot, item) {
1019
1072
  }
1020
1073
 
1021
1074
  // src/update/hash-installed.ts
1022
- import path5 from "path";
1075
+ import path5 from "node:path";
1023
1076
  import fg2 from "fast-glob";
1024
1077
  import fs3 from "fs-extra";
1025
1078
 
@@ -1150,12 +1203,12 @@ function buildAllowRules() {
1150
1203
  }
1151
1204
 
1152
1205
  // src/install/settings-merge.ts
1153
- import path7 from "path";
1206
+ import path7 from "node:path";
1154
1207
  import fs5 from "fs-extra";
1155
1208
 
1156
1209
  // src/install/manifest.ts
1157
- import os3 from "os";
1158
- import path6 from "path";
1210
+ import os3 from "node:os";
1211
+ import path6 from "node:path";
1159
1212
  var MANIFEST_SCHEMA = "haus-install-manifest/1";
1160
1213
  function globalClaudeDir() {
1161
1214
  return path6.join(os3.homedir(), ".claude");
@@ -1593,7 +1646,7 @@ async function applyProjectSettingsMerge(root) {
1593
1646
  }
1594
1647
 
1595
1648
  // src/claude/superpowers-install.ts
1596
- import path8 from "path";
1649
+ import path8 from "node:path";
1597
1650
  import fg3 from "fast-glob";
1598
1651
  import fs6 from "fs-extra";
1599
1652
  var SUPERPOWERS_ORIGIN_SOURCE_ID = "superpowers-pcvelz";
@@ -1733,7 +1786,7 @@ async function verifyProjectSettingsHooksContract(root) {
1733
1786
  }
1734
1787
 
1735
1788
  // src/claude/write-root-claude-md.ts
1736
- import path9 from "path";
1789
+ import path9 from "node:path";
1737
1790
  import fs8 from "fs-extra";
1738
1791
  var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
1739
1792
  var BLOCK_END = "<!-- HAUS:END haus-imports -->";
@@ -1808,11 +1861,11 @@ async function writeRootClaudeMd(root, dryRun) {
1808
1861
  }
1809
1862
 
1810
1863
  // src/claude/write-workflow-config.ts
1811
- import path11 from "path";
1864
+ import path11 from "node:path";
1812
1865
  import fs10 from "fs-extra";
1813
1866
 
1814
1867
  // src/claude/derive-workflow-config.ts
1815
- import path10 from "path";
1868
+ import path10 from "node:path";
1816
1869
  import fs9 from "fs-extra";
1817
1870
  function binCmd(pm, bin, args) {
1818
1871
  const tail = args ? ` ${args}` : "";
@@ -2131,6 +2184,10 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
2131
2184
  for (const item of catalogItems) {
2132
2185
  const manifestItem = manifestById.get(item.id);
2133
2186
  if (!manifestItem?.path) continue;
2187
+ if (manifestItem.reviewStatus === "deprecated") {
2188
+ warn(`Skipping ${item.id}: reviewStatus is deprecated`);
2189
+ continue;
2190
+ }
2134
2191
  if (manifestItem.source === "curated") {
2135
2192
  if (manifestItem.reviewStatus !== "approved") {
2136
2193
  curatedReviewStatusSkips++;
@@ -2363,8 +2420,8 @@ async function runCatalogAudit() {
2363
2420
  }
2364
2421
 
2365
2422
  // src/commands/clone.ts
2366
- import { existsSync as existsSync2 } from "fs";
2367
- import path14 from "path";
2423
+ import { existsSync as existsSync2 } from "node:fs";
2424
+ import path14 from "node:path";
2368
2425
 
2369
2426
  // src/utils/exec.ts
2370
2427
  import { execa } from "execa";
@@ -2436,11 +2493,11 @@ async function runClone(url, opts = {}) {
2436
2493
  }
2437
2494
 
2438
2495
  // src/commands/doctor.ts
2439
- import path19 from "path";
2496
+ import path19 from "node:path";
2440
2497
  import fs14 from "fs-extra";
2441
2498
 
2442
2499
  // src/scanner/scan-project.ts
2443
- import path18 from "path";
2500
+ import path18 from "node:path";
2444
2501
 
2445
2502
  // src/utils/audit-checks.ts
2446
2503
  function isRecord(v) {
@@ -2467,7 +2524,7 @@ function compareVersions(a, b) {
2467
2524
  }
2468
2525
 
2469
2526
  // src/scanner/detect-package-manager.ts
2470
- import path15 from "path";
2527
+ import path15 from "node:path";
2471
2528
  import fs13 from "fs-extra";
2472
2529
  function detectPackageManager(root, packageManagerField) {
2473
2530
  const field = String(packageManagerField ?? "").trim();
@@ -2690,7 +2747,7 @@ function runDetection(ctx, rules = STACK_RULES) {
2690
2747
  }
2691
2748
 
2692
2749
  // src/scanner/detection.ts
2693
- import path16 from "path";
2750
+ import path16 from "node:path";
2694
2751
  var UNSUPPORTED_MARKERS = {
2695
2752
  "requirements.txt": "python",
2696
2753
  "pyproject.toml": "python",
@@ -2750,8 +2807,8 @@ function collectUnsupportedSignals(files) {
2750
2807
  }
2751
2808
 
2752
2809
  // src/scanner/render.ts
2753
- import { readFile } from "fs/promises";
2754
- import path17 from "path";
2810
+ import { readFile } from "node:fs/promises";
2811
+ import path17 from "node:path";
2755
2812
  async function buildContentBlob(root, files) {
2756
2813
  const candidates = files.filter(
2757
2814
  (f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
@@ -3254,7 +3311,7 @@ async function runExplainRecommendation(options) {
3254
3311
  }
3255
3312
 
3256
3313
  // src/commands/guard.ts
3257
- import { readFileSync as readFileSync2 } from "fs";
3314
+ import { readFileSync as readFileSync2 } from "node:fs";
3258
3315
 
3259
3316
  // src/security/guard-bash.ts
3260
3317
  function matchesDenyToken(command, denyPhrase) {
@@ -3324,12 +3381,12 @@ async function runGuard(kind, _options) {
3324
3381
  }
3325
3382
 
3326
3383
  // src/commands/init.ts
3327
- import path20 from "path";
3384
+ import path20 from "node:path";
3328
3385
  import fs15 from "fs-extra";
3329
3386
 
3330
3387
  // src/utils/prompts.ts
3331
- import { stdin as input, stdout as output } from "process";
3332
- import readline from "readline/promises";
3388
+ import { stdin as input, stdout as output } from "node:process";
3389
+ import readline from "node:readline/promises";
3333
3390
  async function ask(question) {
3334
3391
  const rl = readline.createInterface({ input, output });
3335
3392
  try {
@@ -3446,6 +3503,10 @@ async function recommend(root, context) {
3446
3503
  skip(item.id, "unsupported-policy", "Unsupported stack policy");
3447
3504
  continue;
3448
3505
  }
3506
+ if (item.reviewStatus === "deprecated") {
3507
+ skip(item.id, "deprecated", "Catalog item is deprecated", "reviewStatus:deprecated");
3508
+ continue;
3509
+ }
3449
3510
  if (item.source === "curated") {
3450
3511
  const rs = item.reviewStatus;
3451
3512
  if (!rs || rs !== "approved") {
@@ -3684,8 +3745,8 @@ async function runInit(options) {
3684
3745
  }
3685
3746
 
3686
3747
  // src/install/apply.ts
3687
- import crypto2 from "crypto";
3688
- import path21 from "path";
3748
+ import crypto2 from "node:crypto";
3749
+ import path21 from "node:path";
3689
3750
  import fs16 from "fs-extra";
3690
3751
 
3691
3752
  // src/install/header.ts
@@ -4076,7 +4137,7 @@ async function runScan(options) {
4076
4137
  }
4077
4138
 
4078
4139
  // src/commands/undo.ts
4079
- import path22 from "path";
4140
+ import path22 from "node:path";
4080
4141
  import fs17 from "fs-extra";
4081
4142
 
4082
4143
  // src/claude/managed-paths.ts
@@ -4201,8 +4262,8 @@ User-owned .claude/ files will be preserved.`
4201
4262
  }
4202
4263
 
4203
4264
  // src/install/uninstall.ts
4204
- import crypto3 from "crypto";
4205
- import path23 from "path";
4265
+ import crypto3 from "node:crypto";
4266
+ import path23 from "node:path";
4206
4267
  import fs18 from "fs-extra";
4207
4268
  async function runUninstall(options = {}) {
4208
4269
  const { force = false } = options;
@@ -4277,7 +4338,7 @@ async function runUninstallCommand(options) {
4277
4338
  }
4278
4339
 
4279
4340
  // src/commands/update.ts
4280
- import path25 from "path";
4341
+ import path25 from "node:path";
4281
4342
 
4282
4343
  // src/claude/refresh-project.ts
4283
4344
  import fs19 from "fs-extra";
@@ -4316,8 +4377,8 @@ function summarizeLockDiff(before, after) {
4316
4377
  }
4317
4378
 
4318
4379
  // src/update/lockfile.ts
4319
- import { mkdir, readFile as readFile2, copyFile } from "fs/promises";
4320
- import path24 from "path";
4380
+ import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
4381
+ import path24 from "node:path";
4321
4382
  async function checkLock(root) {
4322
4383
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
4323
4384
  const hasValidVersions = lock.every(
@@ -4489,11 +4550,11 @@ async function detectGlobalInstallDrift() {
4489
4550
  }
4490
4551
 
4491
4552
  // src/commands/validate-catalog.ts
4492
- import path27 from "path";
4553
+ import path27 from "node:path";
4493
4554
 
4494
4555
  // src/catalog/validate-core.ts
4495
- import fs20 from "fs";
4496
- import path26 from "path";
4556
+ import fs20 from "node:fs";
4557
+ import path26 from "node:path";
4497
4558
  var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
4498
4559
  var MANIFEST_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
4499
4560
  function isSafeCatalogPath2(itemPath) {
@@ -4527,6 +4588,10 @@ function auditManifestStructure(manifestVersion, items) {
4527
4588
  failures.push(`item[${i}]: missing id`);
4528
4589
  continue;
4529
4590
  }
4591
+ const provenanceError = validateCuratedProvenance(item);
4592
+ if (provenanceError) failures.push(provenanceError);
4593
+ const referencesError = validateReferences(item.id, item.references);
4594
+ if (referencesError) failures.push(referencesError);
4530
4595
  if (!item.type) {
4531
4596
  failures.push(`${item.id}: missing type`);
4532
4597
  continue;
@@ -4554,19 +4619,11 @@ function auditManifestStructure(manifestVersion, items) {
4554
4619
  else seenPaths.set(norm, item.id);
4555
4620
  }
4556
4621
  const isHaus = item.source === "haus";
4557
- const isCuratedApproved = item.source === "curated" && item.reviewStatus === "approved";
4558
- if (!isHaus && !isCuratedApproved) {
4559
- failures.push(`${item.id}: source must be "haus" or curated with reviewStatus "approved"`);
4560
- }
4561
- for (const ref of item.references ?? []) {
4562
- if (/^https?:\/\//i.test(ref)) {
4563
- if (!ref.startsWith("https://")) {
4564
- failures.push(`${item.id}: reference must be https:// URL: ${ref}`);
4565
- }
4566
- if (HTTP_URL_PATTERN.test(ref)) {
4567
- failures.push(`${item.id}: reference uses insecure http:// URL: ${ref}`);
4568
- }
4569
- }
4622
+ const isCuratedUsableState = item.source === "curated" && (item.reviewStatus === "approved" || item.reviewStatus === "deprecated");
4623
+ if (!isHaus && !isCuratedUsableState) {
4624
+ failures.push(
4625
+ `${item.id}: source must be "haus" or curated with reviewStatus "approved"/"deprecated"`
4626
+ );
4570
4627
  }
4571
4628
  }
4572
4629
  }
@@ -4714,8 +4771,8 @@ async function runValidateCatalog(manifestPath2) {
4714
4771
  }
4715
4772
 
4716
4773
  // src/commands/workspace.ts
4717
- import { existsSync as existsSync5, statSync as statSync2 } from "fs";
4718
- import path34 from "path";
4774
+ import { existsSync as existsSync5, statSync as statSync2 } from "node:fs";
4775
+ import path34 from "node:path";
4719
4776
 
4720
4777
  // src/commands/workspace/aggregate.ts
4721
4778
  async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
@@ -4769,7 +4826,7 @@ ${summaries.map(
4769
4826
  }
4770
4827
 
4771
4828
  // src/commands/workspace/config.ts
4772
- import path28 from "path";
4829
+ import path28 from "node:path";
4773
4830
  import YAML from "yaml";
4774
4831
  var WORKSPACE_FILE = "haus.workspace.yaml";
4775
4832
  function parseWorkspaceConfig(text) {
@@ -4796,7 +4853,7 @@ async function readWorkspaceConfig(workspaceRoot) {
4796
4853
  }
4797
4854
 
4798
4855
  // src/commands/workspace/discover.ts
4799
- import path29 from "path";
4856
+ import path29 from "node:path";
4800
4857
  import fg4 from "fast-glob";
4801
4858
  import YAML2 from "yaml";
4802
4859
  var DEFAULT_MAX_DEPTH = 3;
@@ -4919,12 +4976,12 @@ async function runDiscover(workspaceRoot, opts = {}) {
4919
4976
  }
4920
4977
 
4921
4978
  // src/commands/workspace/doctor.ts
4922
- import { existsSync as existsSync3 } from "fs";
4923
- import path31 from "path";
4979
+ import { existsSync as existsSync3 } from "node:fs";
4980
+ import path31 from "node:path";
4924
4981
 
4925
4982
  // src/commands/workspace/manifest.ts
4926
- import { readFileSync as readFileSync3 } from "fs";
4927
- import path30 from "path";
4983
+ import { readFileSync as readFileSync3 } from "node:fs";
4984
+ import path30 from "node:path";
4928
4985
  var MANIFEST_FILE = "workspace.manifest.json";
4929
4986
  function manifestPath(workspaceRoot) {
4930
4987
  return hausPath(workspaceRoot, MANIFEST_FILE);
@@ -5070,11 +5127,11 @@ function emit(args) {
5070
5127
  }
5071
5128
 
5072
5129
  // src/commands/workspace/setup.ts
5073
- import { existsSync as existsSync4, statSync } from "fs";
5074
- import path33 from "path";
5130
+ import { existsSync as existsSync4, statSync } from "node:fs";
5131
+ import path33 from "node:path";
5075
5132
 
5076
5133
  // src/claude/write-workspace-claude-md.ts
5077
- import path32 from "path";
5134
+ import path32 from "node:path";
5078
5135
  import fs21 from "fs-extra";
5079
5136
  function buildWorkspaceImportBlock(client, members) {
5080
5137
  const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
@@ -15,8 +15,25 @@ Before any setup work, probe **everything** the workspace will need and surface
15
15
  - **PHP environment** — `valet`, `herd`, `ddev`, or `php` on PATH, if any repo is a PHP/WordPress site.
16
16
  - **WP-CLI** — `wp --version`, if any repo's `seed:` pulls a WordPress DB.
17
17
  - **Node versions** — the versions named in each repo's `.nvmrc` / `engines.node` are installable via `nvm`.
18
+ - **Data-access preflight** — for every data load the workspace will run (each repo's `seed` **and** the workspace `seeds`), check up front that the user can actually reach the source, so nobody ends up with a half-set-up environment that only fails at the seed step. See below.
18
19
 
19
- Then list the repos and what each will run (node, deps, localdev steps), **report every gap at once**, and get a single go-ahead. For **reused** local clones, ask whether to re-run setup. For each gap, name what it blocks; the user decides whether to fix it now (**ask before any global/system install**) or proceed and skip the affected steps. Don't begin per-repo work until this gate is acknowledged.
20
+ ### Data-access preflight
21
+
22
+ The single most common way cloneAndSetup leaves a **half-completed environment** is discovering at the `seed:` step — after cloning, installing, and bringing up services — that the user lacks the AWS/SSH/etc. access to pull the data. Catch it **here**, before any heavy work, and make the data outcome a deliberate choice.
23
+
24
+ 1. **Collect every seed:** each repo's `seed` plus the workspace `seeds`. For each, determine its required access:
25
+ - If `seed` is an **object with `access`**, run each entry's `check` (a read-only probe; exit 0 = reachable).
26
+ - If `seed` is a **bare string**, infer the channel and probe generically: `aws …`/SSM → `aws sts get-caller-identity`; `dep db:pull …` / `ssh`/`scp`/`rsync <user@host>` → `ssh -o BatchMode=yes -o ConnectTimeout=5 <host> true`; `northflank …` → `northflank whoami`. A purely local seed (no remote) needs nothing.
27
+ 2. **Report one table** alongside the rest of the gate — per data source: its `label`, the access it needs, and ✓ reachable / ✗ + the `login` remedy (verbatim).
28
+ 3. **Resolve every ✗ with one consolidated choice**, then remember it for the run:
29
+ - **Set up access now** → print the `login` text, let the user do it, **re-check**, then seed normally.
30
+ - **Create that DB EMPTY for now** (only when `empty-ok` is true — the default) → the datastore comes up empty (schema via migrations/`needs:`), the app still boots, and the user can run that one seed later. Name the exact command to fill it.
31
+ - **Abort.**
32
+ 4. **Only a gap on a seed with `empty-ok: false` is a HARD STOP** (the repo is genuinely broken without data — log in or abort). Every other gap offers the empty path; one source's missing access never blocks another's.
33
+
34
+ This is the one prompt the "no half-completed environment" goal justifies. The two valid end-states are **seeded** or **deliberately empty** — never a third "did the expensive work then died at seed."
35
+
36
+ Then list the repos and what each will run (node, deps, localdev steps, **data sources + the chosen seed/empty plan**), **report every gap at once**, and get a single go-ahead. For **reused** local clones, ask whether to re-run setup. For each gap, name what it blocks; the user decides whether to fix it now (**ask before any global/system install**) or proceed and skip the affected steps. Don't begin per-repo work until this gate is acknowledged.
20
37
 
21
38
  ## Step 3 — Per-repo dependency pass
22
39
 
@@ -50,6 +67,16 @@ needs: [mysql] # services this repo requires; provisioned in Docker, env wired t
50
67
  needsEnv: [WP_HOME, DB_NAME] # env keys that must be present to run/serve
51
68
  build: 'yarn build' # optional: the build command (run after deps)
52
69
  seed: 'dep db:pull staging-oderland' # optional: how to load data; remote/destructive → confirm first
70
+ # `seed` may also be an OBJECT when the load needs external access (AWS, SSH, …) so the
71
+ # Step-2 data-access preflight can check it up front and offer an empty-DB fallback:
72
+ seed:
73
+ run: './scripts/pull-aws-db.sh --yes' # the seed command
74
+ empty-ok: true # default true: skipping yields a coherent EMPTY DB (schema via migrations).
75
+ # # false = the repo is broken without data → a missing-access gap HARD-STOPS.
76
+ access: # what the user must have to run `run`; each entry probed in the Step-2 preflight
77
+ - label: 'AWS CLI credentials — eu-north-1 / 842675986884'
78
+ check: 'aws sts get-caller-identity --region eu-north-1' # read-only probe; exit 0 = OK
79
+ login: 'Set up the AWS CLI (brew install awscli; aws configure …) — or pick empty DB.'
53
80
  serve: # optional: how it runs (printed as a next-step, never auto-started)
54
81
  via: herd # herd | valet | docker | command — for PHP envs, bring-your-own (don't install)
55
82
  url: 'https://example.test'
@@ -77,6 +104,16 @@ env:
77
104
  # source: { repo: <repo>, provides: '<value>' } # … a value produced by another repo
78
105
  sinks:
79
106
  - { repo: <repo>, key: DB_NAME } # written under `key` into each sink's .env
107
+ seeds: # optional: data sources NOT owned by a manifest repo (e.g. a shared WordPress host
108
+ # pulled by a workspace script). Same shape as a per-repo `seed` object; included in
109
+ # the Step-2 data-access preflight.
110
+ - label: 'WordPress host (example.se)'
111
+ run: './scripts/pull-wp-example.sh'
112
+ empty-ok: true
113
+ access:
114
+ - label: 'SSH user@host'
115
+ check: 'ssh -o BatchMode=yes -o ConnectTimeout=5 user@host true'
116
+ login: 'Add your SSH key to the host (ssh-add …) / request access — or skip the pull.'
80
117
  ```
81
118
 
82
119
  ### Run order
@@ -87,7 +124,7 @@ env:
87
124
  - **`needs`** → add the service to the **workspace compose project** and `docker compose up -d` if not already running (namespaced by `COMPOSE_PROJECT_NAME`, free host ports per instance; not the repo's own bind-mounting compose). Comes up **empty**; record host ports in the workspace `localdev.yml` `env` map — data lands in `seed:`.
88
125
  - **`build`** → run it (honor any node version the repo's docs note).
89
126
  - **`serve`** → for `via: herd` / PHP envs, install nothing — verify the dev's environment serves `web/`, and report the URL.
90
- - **`seed`** → populate the empty datastore. **Always a distinct, confirm-gated step, separate from `needs:` bring-up** (don't conflate "DB up" with "DB has data"). Before running, **check its prerequisites and report any gap instead of running blind** — e.g. WP-CLI present (for `wp` / `db:pull` seeds), the target repo's **`.env` written** (the seed reads connection values from it — done in the Env step above), the **SSH alias resolves** (for remote pulls like `dep db:pull staging-oderland`). **Confirm first** for every remote (SSH) or destructive (overwrites data) seed — every run, even on re-run. Missing prerequisite → skip with a clear message, don't guess.
127
+ - **`seed`** → populate the empty datastore (`seed.run`, or the bare-string form). **Always a distinct, confirm-gated step, separate from `needs:` bring-up** (don't conflate "DB up" with "DB has data"). **Honor the data plan chosen in the Step-2 data-access preflight** — don't re-discover access here: if the user chose _empty_ for this source, skip it (the DB stays empty, schema via migrations) and print the command to fill it later; if they chose _seed_, run it. Still verify the run-time prerequisites the preflight couldn't (the target repo's **`.env` written** — done in the Env step above; WP-CLI present for `wp`/`db:pull` seeds) and **confirm first** for every remote (SSH/AWS) or destructive (overwrites data) seed — every run, even on re-run. Missing prerequisite → skip with a clear message, don't guess.
91
128
  - **`steps`** (escape hatch) → run in order, selecting `node:` per step, `optional:` failures continue, **confirming before any `remote:`/`destructive:` step** — every run, even on re-run.
92
129
  4. **Links** (workspace-owned, performed generically — do NOT call a repo's own `setup-dev-mode.sh`, which is deprecated):
93
130
  - `symlink` → `ln -s <from> <to>`; replace an existing symlink, but never clobber a real directory without confirmation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/haus-workflow",
3
- "version": "0.27.1",
3
+ "version": "0.28.0",
4
4
  "description": "Haus AI workflow CLI for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "CHANGELOG.md"
21
21
  ],
22
22
  "scripts": {
23
- "build": "tsup src/cli.ts --format esm --dts --clean --out-dir dist --external @inquirer/checkbox",
23
+ "build": "rm -rf dist && esbuild src/cli.ts --bundle --packages=external --platform=node --format=esm --outdir=dist --external:@inquirer/checkbox",
24
24
  "dev": "tsx src/cli.ts",
25
25
  "test": "node scripts/run-tests.mjs tests/**/*.test.js",
26
26
  "test:fast": "node scripts/test-fast.mjs",
@@ -58,13 +58,13 @@
58
58
  "@typescript-eslint/eslint-plugin": "8.59.3",
59
59
  "@typescript-eslint/parser": "8.59.3",
60
60
  "c8": "11.0.0",
61
+ "esbuild": "0.28.1",
61
62
  "eslint": "9.39.4",
62
63
  "eslint-plugin-import": "2.32.0",
63
64
  "lefthook": "1.13.6",
64
65
  "prettier": "3.8.3",
65
66
  "release-it": "20.0.1",
66
- "tsup": "8.5.1",
67
- "tsx": "4.21.0",
67
+ "tsx": "4.22.4",
68
68
  "typescript": "5.9.3"
69
69
  },
70
70
  "engines": {
package/dist/cli.d.ts DELETED
@@ -1 +0,0 @@
1
- #!/usr/bin/env node