@haus-tech/haus-workflow 0.29.0 → 0.30.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 +11 -0
- package/README.md +4 -2
- package/dist/cli.js +175 -46
- package/library/catalog/manifest.json +1907 -2242
- package/library/catalog/validation-rules.json +1 -1
- package/library/global/commands/haus-cloneandsetup.md +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.30.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.29.0...v0.30.0) (2026-06-22)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- auto recommend config setups ([#137](https://github.com/WeAreHausTech/haus-workflow/issues/137)) ([56e7941](https://github.com/WeAreHausTech/haus-workflow/commit/56e79413a6cfe00e35f5e48abd03baeeb377214f))
|
|
8
|
+
- **cloneandsetup:** reuse local env values, never commit secrets ([#130](https://github.com/WeAreHausTech/haus-workflow/issues/130)) ([cf74a19](https://github.com/WeAreHausTech/haus-workflow/commit/cf74a196d369fbc59798de97e76bbcd3120155f7))
|
|
9
|
+
- co-install suppression and recommender gates ([#134](https://github.com/WeAreHausTech/haus-workflow/issues/134)) ([4ca1f20](https://github.com/WeAreHausTech/haus-workflow/commit/4ca1f20a6b90645cd7007d195e6cc474558cc971))
|
|
10
|
+
- curated source tools exempt from non-tsx npx ban and update archetype dependencies ([#132](https://github.com/WeAreHausTech/haus-workflow/issues/132)) ([a63d0b2](https://github.com/WeAreHausTech/haus-workflow/commit/a63d0b224ee9d1fd7b686b9aa3979562228f2333))
|
|
11
|
+
- prune deprecated catalog items on apply/update ([#126](https://github.com/WeAreHausTech/haus-workflow/issues/126)) ([fdf6ecc](https://github.com/WeAreHausTech/haus-workflow/commit/fdf6ecc1d36f4eb090cdaa8d9b044381c5836c77))
|
|
12
|
+
- stripe server gate [#139](https://github.com/WeAreHausTech/haus-workflow/issues/139) ([fb2dc35](https://github.com/WeAreHausTech/haus-workflow/commit/fb2dc35b0d332cdd6f3c7dcbaebef94dce06899c))
|
|
13
|
+
|
|
3
14
|
## [0.29.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.28.1...v0.29.0) (2026-06-17)
|
|
4
15
|
|
|
5
16
|
### Features
|
package/README.md
CHANGED
|
@@ -82,8 +82,10 @@ Content lives in [`haus-workflow-catalog`](https://github.com/WeAreHausTech/haus
|
|
|
82
82
|
(version pinned in `library/catalog/manifest.json`). Fetched at runtime from the latest release tag (override with `HAUS_CATALOG_REF`; fallback `main` when no release tag resolves).
|
|
83
83
|
Validation rules sync from catalog → `library/catalog/validation-rules.json` (ADR-0001).
|
|
84
84
|
|
|
85
|
-
On `haus apply` / `haus update`, items **removed from the catalog**
|
|
86
|
-
project when their on-disk copy still matches the
|
|
85
|
+
On `haus apply` / `haus update`, items **removed from the catalog** or marked
|
|
86
|
+
**deprecated** are pruned from the project when their on-disk copy still matches the
|
|
87
|
+
lock hash; user-edited copies are kept. Approved items deselected via `--select` are
|
|
88
|
+
not pruned.
|
|
87
89
|
|
|
88
90
|
## Internal docs
|
|
89
91
|
|
package/dist/cli.js
CHANGED
|
@@ -154,7 +154,7 @@ var validation_rules_default = {
|
|
|
154
154
|
],
|
|
155
155
|
allowedNpxPattern: { source: "\\bnpx\\s+tsx\\b", flags: "i" },
|
|
156
156
|
anyNpxPattern: { source: "\\bnpx\\s+\\S+", flags: "i" },
|
|
157
|
-
|
|
157
|
+
npxTsxOnlyExemptSources: ["curated"],
|
|
158
158
|
httpUrlPattern: { source: "^http:\\/\\/", flags: "i" },
|
|
159
159
|
placeholderPattern: { source: "\\bTODO\\b|\\bPLACEHOLDER\\b", flags: "i" },
|
|
160
160
|
allowedStacks: [
|
|
@@ -274,7 +274,29 @@ var REQUIRED_SKILL_FRONTMATTER = validation_rules_default.requiredSkillFrontmatt
|
|
|
274
274
|
var RISKY_INSTALL_PATTERNS = validation_rules_default.riskyInstallPatterns.map(toRegExp);
|
|
275
275
|
var ALLOWED_NPX_PATTERN = toRegExp(validation_rules_default.allowedNpxPattern);
|
|
276
276
|
var ANY_NPX_PATTERN = toRegExp(validation_rules_default.anyNpxPattern);
|
|
277
|
-
var
|
|
277
|
+
var NPX_TSX_ONLY_EXEMPT_SOURCES = validation_rules_default.npxTsxOnlyExemptSources ?? [];
|
|
278
|
+
function isNpxTsxOnlyExempt(_itemType, itemSource) {
|
|
279
|
+
return Boolean(itemSource && NPX_TSX_ONLY_EXEMPT_SOURCES.includes(itemSource));
|
|
280
|
+
}
|
|
281
|
+
function buildItemPathSourceMap(items) {
|
|
282
|
+
const map = /* @__PURE__ */ new Map();
|
|
283
|
+
for (const item of items) {
|
|
284
|
+
if (!item.path) continue;
|
|
285
|
+
map.set(String(item.path).replace(/\\/g, "/"), item.source);
|
|
286
|
+
}
|
|
287
|
+
return map;
|
|
288
|
+
}
|
|
289
|
+
function resolveMarkdownItemSource(relPath, pathSourceMap) {
|
|
290
|
+
const norm = relPath.replace(/\\/g, "/");
|
|
291
|
+
let cursor = norm;
|
|
292
|
+
while (true) {
|
|
293
|
+
if (pathSourceMap.has(cursor)) return pathSourceMap.get(cursor);
|
|
294
|
+
const slash = cursor.lastIndexOf("/");
|
|
295
|
+
if (slash === -1) break;
|
|
296
|
+
cursor = cursor.slice(0, slash);
|
|
297
|
+
}
|
|
298
|
+
return void 0;
|
|
299
|
+
}
|
|
278
300
|
var HTTP_URL_PATTERN = toRegExp(validation_rules_default.httpUrlPattern);
|
|
279
301
|
var PLACEHOLDER_PATTERN = toRegExp(validation_rules_default.placeholderPattern);
|
|
280
302
|
var ALLOWED_STACKS = validation_rules_default.allowedStacks;
|
|
@@ -353,7 +375,7 @@ ${extractUseWhenSection(text)}`;
|
|
|
353
375
|
// src/catalog/ingest-catalog.ts
|
|
354
376
|
function validateCatalogItem(item, content2) {
|
|
355
377
|
const label = item.id;
|
|
356
|
-
const checkNonTsxNpx = !
|
|
378
|
+
const checkNonTsxNpx = !isNpxTsxOnlyExempt(item.type, item.source);
|
|
357
379
|
const lines = content2.split(/\r?\n/);
|
|
358
380
|
for (let i = 0; i < lines.length; i++) {
|
|
359
381
|
const line2 = lines[i] ?? "";
|
|
@@ -803,7 +825,12 @@ async function syncDirectoryFromPrefix(item, catalogPrefix, destDir, base, opts)
|
|
|
803
825
|
for (const file of fetched) {
|
|
804
826
|
if (file.kind !== "text" || !isMarkdownPath(file.rel)) continue;
|
|
805
827
|
const verdict = validateCatalogItem(
|
|
806
|
-
{
|
|
828
|
+
{
|
|
829
|
+
id: item.id,
|
|
830
|
+
type: "skill",
|
|
831
|
+
path: catalogPrefix,
|
|
832
|
+
..."source" in item && item.source ? { source: item.source } : {}
|
|
833
|
+
},
|
|
807
834
|
file.body
|
|
808
835
|
);
|
|
809
836
|
if (!verdict.ok) {
|
|
@@ -2291,7 +2318,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2291
2318
|
`${curatedReviewStatusSkips} curated items skipped: reviewStatus is not approved \u2014 possible catalog field rename upstream`
|
|
2292
2319
|
);
|
|
2293
2320
|
}
|
|
2294
|
-
await cleanupStaleCatalogItems(root,
|
|
2321
|
+
await cleanupStaleCatalogItems(root, manifestById, dryRun);
|
|
2295
2322
|
if (dryRun) return [...new Set(files)];
|
|
2296
2323
|
const installedItems = catalogItems.filter((r) => installedIds.has(r.id));
|
|
2297
2324
|
const prevLock = await readJson(hausPath(root, "haus.lock.json"));
|
|
@@ -2328,11 +2355,15 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2328
2355
|
await writeManagedJson(root, hausPath(root, "haus.lock.json"), lock, false);
|
|
2329
2356
|
return [...new Set(files)];
|
|
2330
2357
|
}
|
|
2331
|
-
async function cleanupStaleCatalogItems(root,
|
|
2358
|
+
async function cleanupStaleCatalogItems(root, manifestById, dryRun) {
|
|
2332
2359
|
const prevLock = await readJson(hausPath(root, "haus.lock.json"));
|
|
2333
2360
|
if (!prevLock?.length) return;
|
|
2334
2361
|
for (const entry of prevLock) {
|
|
2335
|
-
if (!entry.id
|
|
2362
|
+
if (!entry.id) continue;
|
|
2363
|
+
const manifestItem = manifestById.get(entry.id);
|
|
2364
|
+
const removedFromManifest = manifestItem === void 0;
|
|
2365
|
+
const deprecated = manifestItem?.reviewStatus === "deprecated";
|
|
2366
|
+
if (!removedFromManifest && !deprecated) continue;
|
|
2336
2367
|
const relPaths = entry.paths ?? [];
|
|
2337
2368
|
if (relPaths.length === 0) continue;
|
|
2338
2369
|
const existing = [];
|
|
@@ -2340,28 +2371,29 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
|
|
|
2340
2371
|
if (await fs12.pathExists(path12.join(root, rel))) existing.push(rel);
|
|
2341
2372
|
}
|
|
2342
2373
|
if (existing.length === 0) continue;
|
|
2374
|
+
const pruneReason = deprecated ? "deprecated" : "stale";
|
|
2343
2375
|
if (entry.hash === void 0) {
|
|
2344
2376
|
warn(
|
|
2345
|
-
|
|
2377
|
+
`${deprecated ? "Deprecated" : "Stale"} catalog item ${entry.id} has no lock hash \u2014 leaving in place: ${existing.join(", ")}`
|
|
2346
2378
|
);
|
|
2347
2379
|
continue;
|
|
2348
2380
|
}
|
|
2349
2381
|
const currentHash = await hashInstalledPaths(root, relPaths);
|
|
2350
2382
|
if (currentHash !== entry.hash) {
|
|
2351
2383
|
warn(
|
|
2352
|
-
|
|
2384
|
+
`${deprecated ? "Deprecated" : "Stale"} catalog item ${entry.id} was modified locally \u2014 leaving in place: ${existing.join(", ")}`
|
|
2353
2385
|
);
|
|
2354
2386
|
continue;
|
|
2355
2387
|
}
|
|
2356
2388
|
for (const rel of existing) {
|
|
2357
2389
|
const abs = path12.join(root, rel);
|
|
2358
2390
|
if (dryRun) {
|
|
2359
|
-
log(`[dry-run] would remove
|
|
2391
|
+
log(`[dry-run] would remove ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
2360
2392
|
continue;
|
|
2361
2393
|
}
|
|
2362
2394
|
await fs12.remove(abs);
|
|
2363
2395
|
await pruneEmptyDir(path12.dirname(abs));
|
|
2364
|
-
log(`Removed
|
|
2396
|
+
log(`Removed ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
2365
2397
|
}
|
|
2366
2398
|
}
|
|
2367
2399
|
}
|
|
@@ -2371,6 +2403,9 @@ async function cacheHasItems() {
|
|
|
2371
2403
|
const data = await readJson(path13.join(getCacheDir(), "manifest.json"));
|
|
2372
2404
|
return Array.isArray(data?.items) && data.items.length > 0;
|
|
2373
2405
|
}
|
|
2406
|
+
function installableRecommendedItems(recommended) {
|
|
2407
|
+
return recommended.filter((item) => item.install !== false);
|
|
2408
|
+
}
|
|
2374
2409
|
async function runApply(options) {
|
|
2375
2410
|
if (!options.dryRun && !options.write) {
|
|
2376
2411
|
log("Use --dry-run or --write");
|
|
@@ -2389,28 +2424,31 @@ async function runApply(options) {
|
|
|
2389
2424
|
if (!rec) {
|
|
2390
2425
|
log("No recommendation.json found \u2014 run `haus recommend` first. Writing core files only.");
|
|
2391
2426
|
selectedIds = [];
|
|
2392
|
-
} else if (rec.recommended.length === 0) {
|
|
2393
|
-
log("Recommendation contains no catalog items. Writing core files only.");
|
|
2394
|
-
selectedIds = [];
|
|
2395
2427
|
} else {
|
|
2396
|
-
const items = rec.recommended;
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2428
|
+
const items = installableRecommendedItems(rec.recommended);
|
|
2429
|
+
if (items.length === 0) {
|
|
2430
|
+
log("Recommendation contains no catalog items. Writing core files only.");
|
|
2431
|
+
selectedIds = [];
|
|
2432
|
+
} else {
|
|
2433
|
+
const choices = items.map((item) => ({
|
|
2434
|
+
name: `${item.id} [${item.selectionMode}] \u2014 ${item.reason}`,
|
|
2435
|
+
value: item.id,
|
|
2436
|
+
checked: true
|
|
2437
|
+
}));
|
|
2438
|
+
const chosen = await checkbox({
|
|
2439
|
+
message: "Select catalog items to apply (space to toggle, enter to confirm):",
|
|
2440
|
+
choices,
|
|
2441
|
+
pageSize: Math.min(20, items.length + 2)
|
|
2442
|
+
});
|
|
2443
|
+
selectedIds = chosen;
|
|
2444
|
+
log(`Selected ${selectedIds.length} of ${items.length} catalog items.`);
|
|
2445
|
+
}
|
|
2409
2446
|
}
|
|
2410
2447
|
}
|
|
2411
2448
|
if (!options.allowEmptyCache && !process.env["HAUS_FIXTURE_CATALOG"]) {
|
|
2412
2449
|
const rec = await readJson(hausPath(root, "recommendation.json"));
|
|
2413
|
-
const
|
|
2450
|
+
const installableRecommended = installableRecommendedItems(rec?.recommended ?? []);
|
|
2451
|
+
const catalogItemCount = selectedIds !== void 0 ? selectedIds.length : installableRecommended.length;
|
|
2414
2452
|
if (catalogItemCount > 0 && !await cacheHasItems()) {
|
|
2415
2453
|
const message = "No catalog content found. Run `haus update` first.";
|
|
2416
2454
|
if (isDryRun) {
|
|
@@ -2705,7 +2743,7 @@ var STACK_RULES = [
|
|
|
2705
2743
|
{ stack: ["tooling", "missing-eslint"], any: [depAbsent("eslint")] },
|
|
2706
2744
|
{
|
|
2707
2745
|
stack: ["tooling", "stripe"],
|
|
2708
|
-
any: [dep("@stripe/stripe-js"), dep("@stripe/react-stripe-js")]
|
|
2746
|
+
any: [dep("stripe"), dep("@stripe/stripe-js"), dep("@stripe/react-stripe-js")]
|
|
2709
2747
|
},
|
|
2710
2748
|
{ stack: ["tooling", "qliro"], any: [dep("@haus-tech/qliro-plugin")] },
|
|
2711
2749
|
{
|
|
@@ -3543,15 +3581,6 @@ async function recommend(root, context) {
|
|
|
3543
3581
|
skip(item.id, "unsupported-policy", "Unsupported stack policy");
|
|
3544
3582
|
continue;
|
|
3545
3583
|
}
|
|
3546
|
-
if (item.type === "config") {
|
|
3547
|
-
skip(
|
|
3548
|
-
item.id,
|
|
3549
|
-
"config-scaffold-only",
|
|
3550
|
-
"Config item \u2014 install with `haus scaffold`",
|
|
3551
|
-
"type:config"
|
|
3552
|
-
);
|
|
3553
|
-
continue;
|
|
3554
|
-
}
|
|
3555
3584
|
if (item.reviewStatus === "deprecated") {
|
|
3556
3585
|
skip(item.id, "deprecated", "Catalog item is deprecated", "reviewStatus:deprecated");
|
|
3557
3586
|
continue;
|
|
@@ -3608,6 +3637,19 @@ async function recommend(root, context) {
|
|
|
3608
3637
|
);
|
|
3609
3638
|
continue;
|
|
3610
3639
|
}
|
|
3640
|
+
if (item.id === "haus.ecc-database-reviewer" && !roleSet.has("database")) {
|
|
3641
|
+
skip(item.id, "required-role-missing", "Required role missing: database", "role:database");
|
|
3642
|
+
continue;
|
|
3643
|
+
}
|
|
3644
|
+
if (item.id === "haus.ecc-typescript-reviewer" && (stackSet.has("react") || stackSet.has("react19") || stackSet.has("nextjs") || stackSet.has("next") || roleSet.has("react-app") || roleSet.has("next-app"))) {
|
|
3645
|
+
skip(
|
|
3646
|
+
item.id,
|
|
3647
|
+
"co-install-react-reviewer",
|
|
3648
|
+
"Skip typescript-reviewer on React/Next stacks (use react-reviewer)",
|
|
3649
|
+
"stack:react|nextjs|role:react-app|next-app"
|
|
3650
|
+
);
|
|
3651
|
+
continue;
|
|
3652
|
+
}
|
|
3611
3653
|
const isDefaultBaseline = item.default === true;
|
|
3612
3654
|
const reasons = [];
|
|
3613
3655
|
const push = (code, message, signal) => reasons.push({ code, message, signal });
|
|
@@ -3652,13 +3694,22 @@ async function recommend(root, context) {
|
|
|
3652
3694
|
}
|
|
3653
3695
|
const hasEvidence = reasons.some((r) => r.code !== "default-baseline");
|
|
3654
3696
|
if (isDefaultBaseline || hasEvidence) {
|
|
3697
|
+
const isConfig = item.type === "config";
|
|
3698
|
+
const entryReasons = isConfig ? [
|
|
3699
|
+
...reasons,
|
|
3700
|
+
{
|
|
3701
|
+
code: "config-scaffold",
|
|
3702
|
+
message: "Install with `haus scaffold`",
|
|
3703
|
+
signal: "type:config"
|
|
3704
|
+
}
|
|
3705
|
+
] : reasons;
|
|
3655
3706
|
recommended.push({
|
|
3656
3707
|
id: item.id,
|
|
3657
3708
|
type: item.type,
|
|
3658
|
-
reason:
|
|
3659
|
-
reasons,
|
|
3709
|
+
reason: entryReasons.length ? entryReasons.map((x) => x.message).join(", ") : "eligible",
|
|
3710
|
+
reasons: entryReasons,
|
|
3660
3711
|
selectionMode: isDefaultBaseline && !hasEvidence ? "baseline" : "matched",
|
|
3661
|
-
install:
|
|
3712
|
+
install: !isConfig,
|
|
3662
3713
|
tags: item.tags,
|
|
3663
3714
|
ecosystem: item.ecosystem,
|
|
3664
3715
|
tokenEstimate: item.tokenEstimate
|
|
@@ -3667,9 +3718,11 @@ async function recommend(root, context) {
|
|
|
3667
3718
|
skip(item.id, "no-role-stack-match", "No role/stack match");
|
|
3668
3719
|
}
|
|
3669
3720
|
}
|
|
3721
|
+
applyCoInstallSuppression(recommended, skipped);
|
|
3670
3722
|
recommended.sort((a, b) => a.id.localeCompare(b.id));
|
|
3671
3723
|
skipped.sort((a, b) => a.id.localeCompare(b.id));
|
|
3672
|
-
const
|
|
3724
|
+
const installableRecommended = recommended.filter((item) => item.install);
|
|
3725
|
+
const selectedRules = installableRecommended.length;
|
|
3673
3726
|
const skippedRules = skipped.length;
|
|
3674
3727
|
const estimatedContextTokens = estimateContextTokens(selectedRules);
|
|
3675
3728
|
const estimatedTokenReductionPct = tokenReductionPct(selectedRules, skippedRules);
|
|
@@ -3690,6 +3743,78 @@ function buildStackSet(context) {
|
|
|
3690
3743
|
)
|
|
3691
3744
|
);
|
|
3692
3745
|
}
|
|
3746
|
+
function applyCoInstallSuppression(recommended, skipped) {
|
|
3747
|
+
const recommendedIds = new Set(recommended.map((r) => r.id));
|
|
3748
|
+
const rules = [
|
|
3749
|
+
{
|
|
3750
|
+
suppress: "haus.sentry-sentry-workflow",
|
|
3751
|
+
whenAnyRecommended: [
|
|
3752
|
+
"haus.sentry-sentry-php-sdk",
|
|
3753
|
+
"haus.sentry-sentry-nextjs-sdk",
|
|
3754
|
+
"haus.sentry-sentry-node-sdk",
|
|
3755
|
+
"haus.sentry-sentry-nestjs-sdk"
|
|
3756
|
+
],
|
|
3757
|
+
code: "co-install-sentry-sdk",
|
|
3758
|
+
message: "Skip sentry-workflow when a stack-specific Sentry SDK skill is selected",
|
|
3759
|
+
signal: "co-install:sentry-sdk"
|
|
3760
|
+
},
|
|
3761
|
+
{
|
|
3762
|
+
suppress: "haus.ecc-e2e-runner",
|
|
3763
|
+
whenAnyRecommended: ["haus.ecc-e2e-testing"],
|
|
3764
|
+
code: "co-install-e2e-skill",
|
|
3765
|
+
message: "Skip e2e-runner when e2e-testing skill covers patterns",
|
|
3766
|
+
signal: "co-install:e2e-testing"
|
|
3767
|
+
},
|
|
3768
|
+
{
|
|
3769
|
+
suppress: "haus.oh-my-claudecode-test-engineer",
|
|
3770
|
+
whenAnyRecommended: ["haus.ecc-e2e-testing", "haus.ecc-e2e-runner"],
|
|
3771
|
+
code: "co-install-e2e-coverage",
|
|
3772
|
+
message: "Skip oh-my test-engineer when ECC e2e skill or agent is selected",
|
|
3773
|
+
signal: "co-install:e2e-coverage"
|
|
3774
|
+
},
|
|
3775
|
+
{
|
|
3776
|
+
suppress: "haus.wshobson-js-testing-patterns",
|
|
3777
|
+
whenAnyRecommended: ["haus.ecc-react-testing"],
|
|
3778
|
+
code: "co-install-react-testing",
|
|
3779
|
+
message: "Skip js-testing-patterns when ecc-react-testing covers component tests",
|
|
3780
|
+
signal: "co-install:react-testing"
|
|
3781
|
+
},
|
|
3782
|
+
{
|
|
3783
|
+
suppress: "haus.superpowers-test-driven-development",
|
|
3784
|
+
whenAnyRecommended: ["haus.wshobson-js-testing-patterns", "haus.ecc-react-testing"],
|
|
3785
|
+
code: "co-install-stack-testing",
|
|
3786
|
+
message: "Skip TDD superpower when a stack testing skill is selected",
|
|
3787
|
+
signal: "co-install:stack-testing"
|
|
3788
|
+
},
|
|
3789
|
+
{
|
|
3790
|
+
suppress: "haus.superpowers-specifying-gates",
|
|
3791
|
+
whenAnyRecommended: ["haus.superpowers-checking-gates"],
|
|
3792
|
+
code: "co-install-gate-workflow",
|
|
3793
|
+
message: "Skip specifying-gates when checking-gates is selected",
|
|
3794
|
+
signal: "co-install:checking-gates"
|
|
3795
|
+
},
|
|
3796
|
+
{
|
|
3797
|
+
suppress: "haus.ecc-redis-patterns",
|
|
3798
|
+
whenAnyRecommended: ["haus.redis-redis-connections"],
|
|
3799
|
+
code: "co-install-redis-official",
|
|
3800
|
+
message: "Skip ecc-redis-patterns when official redis-connections skill is selected",
|
|
3801
|
+
signal: "co-install:redis-connections"
|
|
3802
|
+
}
|
|
3803
|
+
];
|
|
3804
|
+
for (const rule of rules) {
|
|
3805
|
+
if (!recommendedIds.has(rule.suppress)) continue;
|
|
3806
|
+
if (!rule.whenAnyRecommended.some((id) => recommendedIds.has(id))) continue;
|
|
3807
|
+
const idx = recommended.findIndex((r) => r.id === rule.suppress);
|
|
3808
|
+
if (idx === -1) continue;
|
|
3809
|
+
const [removed] = recommended.splice(idx, 1);
|
|
3810
|
+
recommendedIds.delete(rule.suppress);
|
|
3811
|
+
skipped.push({
|
|
3812
|
+
id: removed.id,
|
|
3813
|
+
reason: rule.message,
|
|
3814
|
+
skipReasons: [{ code: rule.code, message: rule.message, signal: rule.signal }]
|
|
3815
|
+
});
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3693
3818
|
|
|
3694
3819
|
// src/commands/setup-core.ts
|
|
3695
3820
|
async function runSetupCore(root, opts) {
|
|
@@ -4903,15 +5028,19 @@ function walkMd(dir, fn) {
|
|
|
4903
5028
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
4904
5029
|
}
|
|
4905
5030
|
}
|
|
4906
|
-
function auditMarkdownContent(manifestDir) {
|
|
5031
|
+
function auditMarkdownContent(manifestDir, items) {
|
|
4907
5032
|
const failures = [];
|
|
5033
|
+
const pathSourceMap = buildItemPathSourceMap(items);
|
|
4908
5034
|
for (const dir of ["skills", "agents", "templates", "commands"]) {
|
|
4909
5035
|
const abs = path27.join(manifestDir, dir);
|
|
4910
5036
|
if (!fs21.existsSync(abs)) continue;
|
|
4911
|
-
const
|
|
5037
|
+
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
4912
5038
|
walkMd(abs, (file) => {
|
|
4913
5039
|
const text = fs21.readFileSync(file, "utf8");
|
|
4914
5040
|
const rel = path27.relative(manifestDir, file);
|
|
5041
|
+
const norm = rel.replace(/\\/g, "/");
|
|
5042
|
+
const source = resolveMarkdownItemSource(norm, pathSourceMap);
|
|
5043
|
+
const checkNonTsxNpx = !isNpxTsxOnlyExempt(dirType, source);
|
|
4915
5044
|
const lines = text.split(/\r?\n/);
|
|
4916
5045
|
for (let i = 0; i < lines.length; i++) {
|
|
4917
5046
|
const line2 = lines[i] ?? "";
|
|
@@ -4921,7 +5050,7 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4921
5050
|
failures.push(`${rel}:${i + 1}: disallowed npx (only npx tsx allowed)`);
|
|
4922
5051
|
}
|
|
4923
5052
|
}
|
|
4924
|
-
if (!
|
|
5053
|
+
if (!norm.includes("/references/")) failures.push(...auditForbiddenTagsInText(text, rel));
|
|
4925
5054
|
});
|
|
4926
5055
|
}
|
|
4927
5056
|
return failures;
|
|
@@ -4931,7 +5060,7 @@ function validateCatalogData(manifestDir, manifestVersion, items) {
|
|
|
4931
5060
|
...auditManifestStructure(manifestVersion, items),
|
|
4932
5061
|
...auditForbiddenStacks(items),
|
|
4933
5062
|
...auditShippedFiles(manifestDir, items),
|
|
4934
|
-
...auditMarkdownContent(manifestDir),
|
|
5063
|
+
...auditMarkdownContent(manifestDir, items),
|
|
4935
5064
|
...auditDisallowedTags(items)
|
|
4936
5065
|
];
|
|
4937
5066
|
return { ok: failures.length === 0, failures, items };
|