@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 +7 -0
- package/dist/cli.js +141 -84
- package/library/global/commands/haus-cloneandsetup.md +39 -2
- package/package.json +4 -4
- package/dist/cli.d.ts +0 -1
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-
|
|
376
|
-
var
|
|
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 (!
|
|
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 (!
|
|
484
|
+
if (!isNonEmptyString2(item.id)) {
|
|
428
485
|
return { ok: false, error: `item[${i}]: missing id` };
|
|
429
486
|
}
|
|
430
|
-
if (!
|
|
487
|
+
if (!isNonEmptyString2(item.type)) {
|
|
431
488
|
return { ok: false, error: `${item.id}: missing type` };
|
|
432
489
|
}
|
|
433
|
-
if (!
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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 (!
|
|
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
|
|
4558
|
-
if (!isHaus && !
|
|
4559
|
-
failures.push(
|
|
4560
|
-
|
|
4561
|
-
|
|
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
|
-
|
|
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").
|
|
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.
|
|
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": "
|
|
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
|
-
"
|
|
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
|