@haus-tech/haus-workflow 0.28.1 → 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 +17 -0
- package/README.md +4 -2
- package/dist/cli.js +476 -152
- package/library/catalog/manifest.json +1903 -2199
- package/library/catalog/validation-rules.json +1 -1
- package/library/global/commands/haus-cloneandsetup.md +2 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
5
|
-
import
|
|
5
|
+
import path36 from "node:path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/commands/apply.ts
|
|
@@ -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] ?? "";
|
|
@@ -637,7 +659,7 @@ function safeJoin(base, itemPath) {
|
|
|
637
659
|
const resolved = path2.resolve(base, itemPath);
|
|
638
660
|
return resolved.startsWith(base + path2.sep) || resolved === base ? resolved : null;
|
|
639
661
|
}
|
|
640
|
-
var KNOWN_ITEM_TYPES = /* @__PURE__ */ new Set(["skill", "agent", "template", "command"]);
|
|
662
|
+
var KNOWN_ITEM_TYPES = /* @__PURE__ */ new Set(["skill", "agent", "template", "command", "config"]);
|
|
641
663
|
function isMarkdownPath(rel) {
|
|
642
664
|
return rel.toLowerCase().endsWith(".md");
|
|
643
665
|
}
|
|
@@ -782,12 +804,13 @@ async function writeFetchedDirectory(destDir, fetched) {
|
|
|
782
804
|
}
|
|
783
805
|
}
|
|
784
806
|
async function syncDirectoryFromPrefix(item, catalogPrefix, destDir, base, opts) {
|
|
785
|
-
const relFiles = await listFilesUnderCatalogPrefix(catalogPrefix, base);
|
|
807
|
+
const relFiles = opts.relFiles ?? await listFilesUnderCatalogPrefix(catalogPrefix, base);
|
|
786
808
|
if (!relFiles) {
|
|
787
809
|
warn(`Failed to list files for ${item.id}`);
|
|
788
810
|
return "failed";
|
|
789
811
|
}
|
|
790
|
-
|
|
812
|
+
const requireSkillMd = opts.requireSkillMd ?? true;
|
|
813
|
+
if (requireSkillMd && !relFiles.includes("SKILL.md") && catalogPrefix !== SUPERPOWERS_SHARED_CATALOG_REL) {
|
|
791
814
|
warn(`Failed to fetch content for ${item.id}: missing SKILL.md`);
|
|
792
815
|
return "failed";
|
|
793
816
|
}
|
|
@@ -802,7 +825,12 @@ async function syncDirectoryFromPrefix(item, catalogPrefix, destDir, base, opts)
|
|
|
802
825
|
for (const file of fetched) {
|
|
803
826
|
if (file.kind !== "text" || !isMarkdownPath(file.rel)) continue;
|
|
804
827
|
const verdict = validateCatalogItem(
|
|
805
|
-
{
|
|
828
|
+
{
|
|
829
|
+
id: item.id,
|
|
830
|
+
type: "skill",
|
|
831
|
+
path: catalogPrefix,
|
|
832
|
+
..."source" in item && item.source ? { source: item.source } : {}
|
|
833
|
+
},
|
|
806
834
|
file.body
|
|
807
835
|
);
|
|
808
836
|
if (!verdict.ok) {
|
|
@@ -833,6 +861,42 @@ async function syncSkillDirectory(item, base) {
|
|
|
833
861
|
return "failed";
|
|
834
862
|
}
|
|
835
863
|
}
|
|
864
|
+
async function syncConfigItem(item, base) {
|
|
865
|
+
const dest = safeJoin(getCacheDir(), item.path);
|
|
866
|
+
if (!dest) {
|
|
867
|
+
warn(`Skipping ${item.id}: path traversal detected`);
|
|
868
|
+
return "failed";
|
|
869
|
+
}
|
|
870
|
+
const relFiles = await listFilesUnderCatalogPrefix(item.path, base);
|
|
871
|
+
if (relFiles && relFiles.length > 0) {
|
|
872
|
+
try {
|
|
873
|
+
return await syncDirectoryFromPrefix(item, item.path, dest, base, {
|
|
874
|
+
validateMarkdown: false,
|
|
875
|
+
requireSkillMd: false,
|
|
876
|
+
relFiles
|
|
877
|
+
});
|
|
878
|
+
} catch (err) {
|
|
879
|
+
warn(`Failed to cache ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
880
|
+
return "failed";
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
const text = await fetchText(`${base}/${item.path}`);
|
|
884
|
+
if (text === null) {
|
|
885
|
+
warn(`Failed to fetch content for ${item.id}`);
|
|
886
|
+
return "failed";
|
|
887
|
+
}
|
|
888
|
+
const verdict = validateCatalogItem(item, text);
|
|
889
|
+
if (!verdict.ok) {
|
|
890
|
+
warn(`Rejected ${item.id} at ingest: ${verdict.reason}`);
|
|
891
|
+
return "failed";
|
|
892
|
+
}
|
|
893
|
+
try {
|
|
894
|
+
return await writeTextIfChanged(dest, text);
|
|
895
|
+
} catch (err) {
|
|
896
|
+
warn(`Failed to cache ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
897
|
+
return "failed";
|
|
898
|
+
}
|
|
899
|
+
}
|
|
836
900
|
async function syncSuperpowersShared(base) {
|
|
837
901
|
const relFiles = await listFilesUnderCatalogPrefix(SUPERPOWERS_SHARED_CATALOG_REL, base);
|
|
838
902
|
if (!relFiles || relFiles.length === 0) return "skipped";
|
|
@@ -866,6 +930,9 @@ async function syncOneItem(item, base) {
|
|
|
866
930
|
if (item.type === "skill") {
|
|
867
931
|
return syncSkillDirectory(item, base);
|
|
868
932
|
}
|
|
933
|
+
if (item.type === "config") {
|
|
934
|
+
return syncConfigItem(item, base);
|
|
935
|
+
}
|
|
869
936
|
const dest = safeJoin(getCacheDir(), item.path);
|
|
870
937
|
if (!dest) {
|
|
871
938
|
warn(`Skipping ${item.id}: path traversal detected`);
|
|
@@ -1593,8 +1660,8 @@ function buildDenyRules() {
|
|
|
1593
1660
|
for (const command of DENY_COMMANDS) {
|
|
1594
1661
|
rules.push(`Bash(${command}:*)`);
|
|
1595
1662
|
}
|
|
1596
|
-
for (const
|
|
1597
|
-
const pattern = DENY_DIRS.has(
|
|
1663
|
+
for (const path37 of DENY_PATHS) {
|
|
1664
|
+
const pattern = DENY_DIRS.has(path37) ? `${path37}/**` : path37;
|
|
1598
1665
|
for (const tool of FILE_TOOLS) {
|
|
1599
1666
|
rules.push(`${tool}(${pattern})`);
|
|
1600
1667
|
}
|
|
@@ -2202,6 +2269,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2202
2269
|
continue;
|
|
2203
2270
|
}
|
|
2204
2271
|
}
|
|
2272
|
+
if (item.type === "config") continue;
|
|
2205
2273
|
const sourcePath = catalogItemContentPath(contentRoot, manifestItem);
|
|
2206
2274
|
const target = targetDirForType(item.type);
|
|
2207
2275
|
if (!target) {
|
|
@@ -2250,7 +2318,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2250
2318
|
`${curatedReviewStatusSkips} curated items skipped: reviewStatus is not approved \u2014 possible catalog field rename upstream`
|
|
2251
2319
|
);
|
|
2252
2320
|
}
|
|
2253
|
-
await cleanupStaleCatalogItems(root,
|
|
2321
|
+
await cleanupStaleCatalogItems(root, manifestById, dryRun);
|
|
2254
2322
|
if (dryRun) return [...new Set(files)];
|
|
2255
2323
|
const installedItems = catalogItems.filter((r) => installedIds.has(r.id));
|
|
2256
2324
|
const prevLock = await readJson(hausPath(root, "haus.lock.json"));
|
|
@@ -2287,11 +2355,15 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2287
2355
|
await writeManagedJson(root, hausPath(root, "haus.lock.json"), lock, false);
|
|
2288
2356
|
return [...new Set(files)];
|
|
2289
2357
|
}
|
|
2290
|
-
async function cleanupStaleCatalogItems(root,
|
|
2358
|
+
async function cleanupStaleCatalogItems(root, manifestById, dryRun) {
|
|
2291
2359
|
const prevLock = await readJson(hausPath(root, "haus.lock.json"));
|
|
2292
2360
|
if (!prevLock?.length) return;
|
|
2293
2361
|
for (const entry of prevLock) {
|
|
2294
|
-
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;
|
|
2295
2367
|
const relPaths = entry.paths ?? [];
|
|
2296
2368
|
if (relPaths.length === 0) continue;
|
|
2297
2369
|
const existing = [];
|
|
@@ -2299,28 +2371,29 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
|
|
|
2299
2371
|
if (await fs12.pathExists(path12.join(root, rel))) existing.push(rel);
|
|
2300
2372
|
}
|
|
2301
2373
|
if (existing.length === 0) continue;
|
|
2374
|
+
const pruneReason = deprecated ? "deprecated" : "stale";
|
|
2302
2375
|
if (entry.hash === void 0) {
|
|
2303
2376
|
warn(
|
|
2304
|
-
|
|
2377
|
+
`${deprecated ? "Deprecated" : "Stale"} catalog item ${entry.id} has no lock hash \u2014 leaving in place: ${existing.join(", ")}`
|
|
2305
2378
|
);
|
|
2306
2379
|
continue;
|
|
2307
2380
|
}
|
|
2308
2381
|
const currentHash = await hashInstalledPaths(root, relPaths);
|
|
2309
2382
|
if (currentHash !== entry.hash) {
|
|
2310
2383
|
warn(
|
|
2311
|
-
|
|
2384
|
+
`${deprecated ? "Deprecated" : "Stale"} catalog item ${entry.id} was modified locally \u2014 leaving in place: ${existing.join(", ")}`
|
|
2312
2385
|
);
|
|
2313
2386
|
continue;
|
|
2314
2387
|
}
|
|
2315
2388
|
for (const rel of existing) {
|
|
2316
2389
|
const abs = path12.join(root, rel);
|
|
2317
2390
|
if (dryRun) {
|
|
2318
|
-
log(`[dry-run] would remove
|
|
2391
|
+
log(`[dry-run] would remove ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
2319
2392
|
continue;
|
|
2320
2393
|
}
|
|
2321
2394
|
await fs12.remove(abs);
|
|
2322
2395
|
await pruneEmptyDir(path12.dirname(abs));
|
|
2323
|
-
log(`Removed
|
|
2396
|
+
log(`Removed ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
2324
2397
|
}
|
|
2325
2398
|
}
|
|
2326
2399
|
}
|
|
@@ -2330,6 +2403,9 @@ async function cacheHasItems() {
|
|
|
2330
2403
|
const data = await readJson(path13.join(getCacheDir(), "manifest.json"));
|
|
2331
2404
|
return Array.isArray(data?.items) && data.items.length > 0;
|
|
2332
2405
|
}
|
|
2406
|
+
function installableRecommendedItems(recommended) {
|
|
2407
|
+
return recommended.filter((item) => item.install !== false);
|
|
2408
|
+
}
|
|
2333
2409
|
async function runApply(options) {
|
|
2334
2410
|
if (!options.dryRun && !options.write) {
|
|
2335
2411
|
log("Use --dry-run or --write");
|
|
@@ -2348,28 +2424,31 @@ async function runApply(options) {
|
|
|
2348
2424
|
if (!rec) {
|
|
2349
2425
|
log("No recommendation.json found \u2014 run `haus recommend` first. Writing core files only.");
|
|
2350
2426
|
selectedIds = [];
|
|
2351
|
-
} else if (rec.recommended.length === 0) {
|
|
2352
|
-
log("Recommendation contains no catalog items. Writing core files only.");
|
|
2353
|
-
selectedIds = [];
|
|
2354
2427
|
} else {
|
|
2355
|
-
const items = rec.recommended;
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
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
|
+
}
|
|
2368
2446
|
}
|
|
2369
2447
|
}
|
|
2370
2448
|
if (!options.allowEmptyCache && !process.env["HAUS_FIXTURE_CATALOG"]) {
|
|
2371
2449
|
const rec = await readJson(hausPath(root, "recommendation.json"));
|
|
2372
|
-
const
|
|
2450
|
+
const installableRecommended = installableRecommendedItems(rec?.recommended ?? []);
|
|
2451
|
+
const catalogItemCount = selectedIds !== void 0 ? selectedIds.length : installableRecommended.length;
|
|
2373
2452
|
if (catalogItemCount > 0 && !await cacheHasItems()) {
|
|
2374
2453
|
const message = "No catalog content found. Run `haus update` first.";
|
|
2375
2454
|
if (isDryRun) {
|
|
@@ -2664,7 +2743,7 @@ var STACK_RULES = [
|
|
|
2664
2743
|
{ stack: ["tooling", "missing-eslint"], any: [depAbsent("eslint")] },
|
|
2665
2744
|
{
|
|
2666
2745
|
stack: ["tooling", "stripe"],
|
|
2667
|
-
any: [dep("@stripe/stripe-js"), dep("@stripe/react-stripe-js")]
|
|
2746
|
+
any: [dep("stripe"), dep("@stripe/stripe-js"), dep("@stripe/react-stripe-js")]
|
|
2668
2747
|
},
|
|
2669
2748
|
{ stack: ["tooling", "qliro"], any: [dep("@haus-tech/qliro-plugin")] },
|
|
2670
2749
|
{
|
|
@@ -3558,6 +3637,19 @@ async function recommend(root, context) {
|
|
|
3558
3637
|
);
|
|
3559
3638
|
continue;
|
|
3560
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
|
+
}
|
|
3561
3653
|
const isDefaultBaseline = item.default === true;
|
|
3562
3654
|
const reasons = [];
|
|
3563
3655
|
const push = (code, message, signal) => reasons.push({ code, message, signal });
|
|
@@ -3602,13 +3694,22 @@ async function recommend(root, context) {
|
|
|
3602
3694
|
}
|
|
3603
3695
|
const hasEvidence = reasons.some((r) => r.code !== "default-baseline");
|
|
3604
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;
|
|
3605
3706
|
recommended.push({
|
|
3606
3707
|
id: item.id,
|
|
3607
3708
|
type: item.type,
|
|
3608
|
-
reason:
|
|
3609
|
-
reasons,
|
|
3709
|
+
reason: entryReasons.length ? entryReasons.map((x) => x.message).join(", ") : "eligible",
|
|
3710
|
+
reasons: entryReasons,
|
|
3610
3711
|
selectionMode: isDefaultBaseline && !hasEvidence ? "baseline" : "matched",
|
|
3611
|
-
install:
|
|
3712
|
+
install: !isConfig,
|
|
3612
3713
|
tags: item.tags,
|
|
3613
3714
|
ecosystem: item.ecosystem,
|
|
3614
3715
|
tokenEstimate: item.tokenEstimate
|
|
@@ -3617,9 +3718,11 @@ async function recommend(root, context) {
|
|
|
3617
3718
|
skip(item.id, "no-role-stack-match", "No role/stack match");
|
|
3618
3719
|
}
|
|
3619
3720
|
}
|
|
3721
|
+
applyCoInstallSuppression(recommended, skipped);
|
|
3620
3722
|
recommended.sort((a, b) => a.id.localeCompare(b.id));
|
|
3621
3723
|
skipped.sort((a, b) => a.id.localeCompare(b.id));
|
|
3622
|
-
const
|
|
3724
|
+
const installableRecommended = recommended.filter((item) => item.install);
|
|
3725
|
+
const selectedRules = installableRecommended.length;
|
|
3623
3726
|
const skippedRules = skipped.length;
|
|
3624
3727
|
const estimatedContextTokens = estimateContextTokens(selectedRules);
|
|
3625
3728
|
const estimatedTokenReductionPct = tokenReductionPct(selectedRules, skippedRules);
|
|
@@ -3640,6 +3743,78 @@ function buildStackSet(context) {
|
|
|
3640
3743
|
)
|
|
3641
3744
|
);
|
|
3642
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
|
+
}
|
|
3643
3818
|
|
|
3644
3819
|
// src/commands/setup-core.ts
|
|
3645
3820
|
async function runSetupCore(root, opts) {
|
|
@@ -4123,6 +4298,144 @@ async function runRefresh() {
|
|
|
4123
4298
|
log(`Recommended items: ${recommendation.recommended.length}`);
|
|
4124
4299
|
}
|
|
4125
4300
|
|
|
4301
|
+
// src/install/scaffold.ts
|
|
4302
|
+
import path22 from "node:path";
|
|
4303
|
+
import fs17 from "fs-extra";
|
|
4304
|
+
async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
4305
|
+
const scaffolded = [];
|
|
4306
|
+
const skipped = [];
|
|
4307
|
+
const skippedExisting = [];
|
|
4308
|
+
const record = (name, outcome) => {
|
|
4309
|
+
if (outcome === "scaffolded") {
|
|
4310
|
+
scaffolded.push(name);
|
|
4311
|
+
return;
|
|
4312
|
+
}
|
|
4313
|
+
skipped.push(name);
|
|
4314
|
+
if (outcome === "skipped-exists") skippedExisting.push(name);
|
|
4315
|
+
};
|
|
4316
|
+
const realRoot = await fs17.realpath(catalogRoot).catch(() => null);
|
|
4317
|
+
for (const item of items) {
|
|
4318
|
+
if (item.type !== "config") continue;
|
|
4319
|
+
const sourcePath = path22.resolve(catalogRoot, item.path);
|
|
4320
|
+
const rel = path22.relative(catalogRoot, sourcePath);
|
|
4321
|
+
const escapes = rel === "" || path22.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
|
|
4322
|
+
if (escapes) {
|
|
4323
|
+
warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
|
|
4324
|
+
continue;
|
|
4325
|
+
}
|
|
4326
|
+
const realSource = await fs17.realpath(sourcePath).catch(() => null);
|
|
4327
|
+
if (!realRoot || !realSource) {
|
|
4328
|
+
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
4329
|
+
continue;
|
|
4330
|
+
}
|
|
4331
|
+
const realRel = path22.relative(realRoot, realSource);
|
|
4332
|
+
if (realRel !== "" && (path22.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
|
|
4333
|
+
warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
|
|
4334
|
+
continue;
|
|
4335
|
+
}
|
|
4336
|
+
const stat = await fs17.lstat(sourcePath).catch(() => null);
|
|
4337
|
+
if (!stat) {
|
|
4338
|
+
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
4339
|
+
continue;
|
|
4340
|
+
}
|
|
4341
|
+
if (stat.isSymbolicLink()) {
|
|
4342
|
+
warn(`Skipping ${item.id}: source path is a symlink`);
|
|
4343
|
+
continue;
|
|
4344
|
+
}
|
|
4345
|
+
if (stat.isFile()) {
|
|
4346
|
+
const filename = path22.basename(sourcePath);
|
|
4347
|
+
const dest = path22.join(projectRoot, filename);
|
|
4348
|
+
record(filename, await scaffoldFile(sourcePath, dest, item.id, opts));
|
|
4349
|
+
} else if (stat.isDirectory()) {
|
|
4350
|
+
const entries = await fs17.readdir(sourcePath);
|
|
4351
|
+
for (const entry of entries) {
|
|
4352
|
+
const src = path22.join(sourcePath, entry);
|
|
4353
|
+
const dest = path22.join(projectRoot, entry);
|
|
4354
|
+
record(entry, await scaffoldFile(src, dest, item.id, opts));
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
return { scaffolded, skipped, skippedExisting };
|
|
4359
|
+
}
|
|
4360
|
+
async function scaffoldFile(src, dest, itemId, opts) {
|
|
4361
|
+
const srcStat = await fs17.lstat(src).catch(() => null);
|
|
4362
|
+
if (!srcStat) {
|
|
4363
|
+
warn(`Skipping ${path22.basename(src)}: source not found`);
|
|
4364
|
+
return "skipped-refused";
|
|
4365
|
+
}
|
|
4366
|
+
if (srcStat.isSymbolicLink()) {
|
|
4367
|
+
warn(`Skipping ${path22.basename(src)}: source is a symlink`);
|
|
4368
|
+
return "skipped-refused";
|
|
4369
|
+
}
|
|
4370
|
+
const exists = await fs17.pathExists(dest);
|
|
4371
|
+
if (exists && !opts.force) {
|
|
4372
|
+
warn(`Skipping ${path22.basename(dest)}: already exists (use --force to overwrite)`);
|
|
4373
|
+
return "skipped-exists";
|
|
4374
|
+
}
|
|
4375
|
+
if (opts.dryRun) {
|
|
4376
|
+
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path22.basename(dest)} (${itemId})`);
|
|
4377
|
+
return "scaffolded";
|
|
4378
|
+
}
|
|
4379
|
+
if (exists && opts.force) {
|
|
4380
|
+
await fs17.remove(dest);
|
|
4381
|
+
}
|
|
4382
|
+
await fs17.ensureDir(path22.dirname(dest));
|
|
4383
|
+
await fs17.copy(src, dest, {
|
|
4384
|
+
overwrite: true,
|
|
4385
|
+
dereference: false,
|
|
4386
|
+
// A vanished/unreadable entry mid-walk must skip that path, not crash the copy.
|
|
4387
|
+
filter: (s) => {
|
|
4388
|
+
try {
|
|
4389
|
+
return !fs17.lstatSync(s).isSymbolicLink();
|
|
4390
|
+
} catch {
|
|
4391
|
+
return false;
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
});
|
|
4395
|
+
log(`${exists ? "Overwrote" : "Created"} ${path22.basename(dest)} (${itemId})`);
|
|
4396
|
+
return "scaffolded";
|
|
4397
|
+
}
|
|
4398
|
+
|
|
4399
|
+
// src/commands/scaffold.ts
|
|
4400
|
+
async function runScaffold(ids, options) {
|
|
4401
|
+
const { items: allItems, contentRoot } = await loadCatalogContext(options.root);
|
|
4402
|
+
const configItems = allItems.filter((item) => {
|
|
4403
|
+
if (item.type !== "config") return false;
|
|
4404
|
+
if (item.reviewStatus && item.reviewStatus !== "approved") return false;
|
|
4405
|
+
if (item.riskLevel === "blocked") return false;
|
|
4406
|
+
if (ids.length > 0 && !ids.includes(item.id)) return false;
|
|
4407
|
+
return true;
|
|
4408
|
+
});
|
|
4409
|
+
if (configItems.length === 0) {
|
|
4410
|
+
if (ids.length > 0) {
|
|
4411
|
+
log(`No config items found for: ${ids.join(", ")}`);
|
|
4412
|
+
} else {
|
|
4413
|
+
log("No config items available in catalog.");
|
|
4414
|
+
}
|
|
4415
|
+
return;
|
|
4416
|
+
}
|
|
4417
|
+
log(`Scaffolding ${configItems.length} config item(s)\u2026`);
|
|
4418
|
+
const result = await scaffoldConfigItems(options.root, contentRoot, configItems, {
|
|
4419
|
+
force: options.force,
|
|
4420
|
+
dryRun: options.dryRun
|
|
4421
|
+
});
|
|
4422
|
+
if (result.scaffolded.length > 0) {
|
|
4423
|
+
if (options.dryRun) {
|
|
4424
|
+
log(`[dry-run] would scaffold: ${result.scaffolded.join(", ")} (no files written)`);
|
|
4425
|
+
} else {
|
|
4426
|
+
log(`\u2713 Scaffolded: ${result.scaffolded.join(", ")}`);
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
if (result.skipped.length > 0) {
|
|
4430
|
+
log(`Skipped: ${result.skipped.join(", ")}`);
|
|
4431
|
+
if (result.skippedExisting.length > 0) {
|
|
4432
|
+
log(
|
|
4433
|
+
`Already present (re-run with --force to overwrite): ${result.skippedExisting.join(", ")}`
|
|
4434
|
+
);
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
}
|
|
4438
|
+
|
|
4126
4439
|
// src/commands/scan.ts
|
|
4127
4440
|
async function runScan(options) {
|
|
4128
4441
|
const result = await scanProject(process.cwd());
|
|
@@ -4136,8 +4449,8 @@ async function runScan(options) {
|
|
|
4136
4449
|
}
|
|
4137
4450
|
|
|
4138
4451
|
// src/commands/undo.ts
|
|
4139
|
-
import
|
|
4140
|
-
import
|
|
4452
|
+
import path23 from "node:path";
|
|
4453
|
+
import fs18 from "fs-extra";
|
|
4141
4454
|
|
|
4142
4455
|
// src/claude/managed-paths.ts
|
|
4143
4456
|
var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md", "commands/haus-doctor.md"];
|
|
@@ -4154,74 +4467,74 @@ async function collectManagedPaths(root) {
|
|
|
4154
4467
|
const lock = await readJson(hausPath(root, "haus.lock.json"));
|
|
4155
4468
|
for (const row of lock ?? []) {
|
|
4156
4469
|
for (const rel of row.paths ?? []) {
|
|
4157
|
-
paths.add(
|
|
4470
|
+
paths.add(path23.resolve(root, rel));
|
|
4158
4471
|
}
|
|
4159
4472
|
}
|
|
4160
4473
|
const existing = [];
|
|
4161
4474
|
for (const abs of paths) {
|
|
4162
|
-
if (await
|
|
4475
|
+
if (await fs18.pathExists(abs)) existing.push(abs);
|
|
4163
4476
|
}
|
|
4164
4477
|
return existing;
|
|
4165
4478
|
}
|
|
4166
4479
|
async function settingsHasHausContent(root) {
|
|
4167
4480
|
const settingsPath = claudePath(root, "settings.json");
|
|
4168
|
-
if (!await
|
|
4481
|
+
if (!await fs18.pathExists(settingsPath)) return false;
|
|
4169
4482
|
const settings = await readProjectSettings(root);
|
|
4170
4483
|
return settings._haus != null;
|
|
4171
4484
|
}
|
|
4172
4485
|
async function claudeMdHasHausBlock(root) {
|
|
4173
|
-
const filePath =
|
|
4174
|
-
if (!await
|
|
4175
|
-
const text = await
|
|
4486
|
+
const filePath = path23.join(root, "CLAUDE.md");
|
|
4487
|
+
if (!await fs18.pathExists(filePath)) return false;
|
|
4488
|
+
const text = await fs18.readFile(filePath, "utf8");
|
|
4176
4489
|
return text.includes(BLOCK_BEGIN);
|
|
4177
4490
|
}
|
|
4178
4491
|
async function stripProjectSettings(root) {
|
|
4179
4492
|
const settingsPath = claudePath(root, "settings.json");
|
|
4180
|
-
if (!await
|
|
4493
|
+
if (!await fs18.pathExists(settingsPath)) return false;
|
|
4181
4494
|
let settings = await readProjectSettings(root);
|
|
4182
4495
|
settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
4183
4496
|
const hasContent = Object.keys(settings).length > 0;
|
|
4184
4497
|
if (hasContent) {
|
|
4185
4498
|
await writeProjectSettings(root, settings);
|
|
4186
|
-
log(`Stripped haus rules from ${
|
|
4499
|
+
log(`Stripped haus rules from ${path23.relative(root, settingsPath)} (user settings preserved).`);
|
|
4187
4500
|
return true;
|
|
4188
4501
|
}
|
|
4189
|
-
await
|
|
4190
|
-
log(`Removed ${
|
|
4502
|
+
await fs18.remove(settingsPath);
|
|
4503
|
+
log(`Removed ${path23.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
4191
4504
|
return true;
|
|
4192
4505
|
}
|
|
4193
4506
|
async function stripRootClaudeMd(root) {
|
|
4194
|
-
const filePath =
|
|
4195
|
-
if (!await
|
|
4196
|
-
const prev = await
|
|
4507
|
+
const filePath = path23.join(root, "CLAUDE.md");
|
|
4508
|
+
if (!await fs18.pathExists(filePath)) return false;
|
|
4509
|
+
const prev = await fs18.readFile(filePath, "utf8");
|
|
4197
4510
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
4198
4511
|
const next = stripHausBlock(prev);
|
|
4199
4512
|
if (next.length === 0) {
|
|
4200
|
-
await
|
|
4513
|
+
await fs18.remove(filePath);
|
|
4201
4514
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
4202
4515
|
} else {
|
|
4203
|
-
await
|
|
4516
|
+
await fs18.writeFile(filePath, next, "utf8");
|
|
4204
4517
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
4205
4518
|
}
|
|
4206
4519
|
return true;
|
|
4207
4520
|
}
|
|
4208
4521
|
async function pruneDirIfEmpty(dir) {
|
|
4209
|
-
if (!await
|
|
4210
|
-
const entries = await
|
|
4211
|
-
if (entries.length === 0) await
|
|
4522
|
+
if (!await fs18.pathExists(dir)) return;
|
|
4523
|
+
const entries = await fs18.readdir(dir);
|
|
4524
|
+
if (entries.length === 0) await fs18.remove(dir);
|
|
4212
4525
|
}
|
|
4213
4526
|
async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
4214
4527
|
if (managedAbsPaths.length === 0) return;
|
|
4215
4528
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4216
4529
|
const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
|
|
4217
4530
|
for (const abs of managedAbsPaths) {
|
|
4218
|
-
if (!await
|
|
4219
|
-
const rel =
|
|
4220
|
-
const backupPath =
|
|
4221
|
-
await
|
|
4222
|
-
await
|
|
4531
|
+
if (!await fs18.pathExists(abs)) continue;
|
|
4532
|
+
const rel = path23.relative(root, abs);
|
|
4533
|
+
const backupPath = path23.join(backupRoot, rel);
|
|
4534
|
+
await fs18.ensureDir(path23.dirname(backupPath));
|
|
4535
|
+
await fs18.copy(abs, backupPath);
|
|
4223
4536
|
}
|
|
4224
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
4537
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path23.relative(root, backupRoot)}.`);
|
|
4225
4538
|
}
|
|
4226
4539
|
async function runUndo(options) {
|
|
4227
4540
|
const root = process.cwd();
|
|
@@ -4232,7 +4545,7 @@ async function runUndo(options) {
|
|
|
4232
4545
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
4233
4546
|
return;
|
|
4234
4547
|
}
|
|
4235
|
-
const relTargets = managed.map((p) =>
|
|
4548
|
+
const relTargets = managed.map((p) => path23.relative(root, p));
|
|
4236
4549
|
const summaryParts = [...relTargets];
|
|
4237
4550
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
4238
4551
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -4249,9 +4562,9 @@ User-owned .claude/ files will be preserved.`
|
|
|
4249
4562
|
}
|
|
4250
4563
|
await backupManagedFilesBeforeUndo(root, managed);
|
|
4251
4564
|
for (const abs of managed) {
|
|
4252
|
-
if (!await
|
|
4253
|
-
await
|
|
4254
|
-
log(`Removed ${
|
|
4565
|
+
if (!await fs18.pathExists(abs)) continue;
|
|
4566
|
+
await fs18.remove(abs);
|
|
4567
|
+
log(`Removed ${path23.relative(root, abs)}`);
|
|
4255
4568
|
}
|
|
4256
4569
|
if (stripSettings) await stripProjectSettings(root);
|
|
4257
4570
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -4262,8 +4575,8 @@ User-owned .claude/ files will be preserved.`
|
|
|
4262
4575
|
|
|
4263
4576
|
// src/install/uninstall.ts
|
|
4264
4577
|
import crypto3 from "node:crypto";
|
|
4265
|
-
import
|
|
4266
|
-
import
|
|
4578
|
+
import path24 from "node:path";
|
|
4579
|
+
import fs19 from "fs-extra";
|
|
4267
4580
|
async function runUninstall(options = {}) {
|
|
4268
4581
|
const { force = false } = options;
|
|
4269
4582
|
const manifest = await readManifest();
|
|
@@ -4273,7 +4586,7 @@ async function runUninstall(options = {}) {
|
|
|
4273
4586
|
return result;
|
|
4274
4587
|
}
|
|
4275
4588
|
for (const entry of manifest.files) {
|
|
4276
|
-
const exists =
|
|
4589
|
+
const exists = fs19.pathExistsSync(entry.destPath);
|
|
4277
4590
|
if (!exists) continue;
|
|
4278
4591
|
const content2 = await readText(entry.destPath);
|
|
4279
4592
|
if (content2 === void 0) continue;
|
|
@@ -4291,22 +4604,22 @@ async function runUninstall(options = {}) {
|
|
|
4291
4604
|
result.skipped.push(entry.destPath);
|
|
4292
4605
|
continue;
|
|
4293
4606
|
}
|
|
4294
|
-
await
|
|
4295
|
-
await pruneEmptyDir(
|
|
4607
|
+
await fs19.remove(entry.destPath);
|
|
4608
|
+
await pruneEmptyDir(path24.dirname(entry.destPath));
|
|
4296
4609
|
result.deleted.push(entry.destPath);
|
|
4297
4610
|
}
|
|
4298
4611
|
const settings = await readSettings();
|
|
4299
4612
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
4300
4613
|
await writeSettings(stripped);
|
|
4301
4614
|
result.hooksStripped = true;
|
|
4302
|
-
const hausDir =
|
|
4615
|
+
const hausDir = path24.join(globalClaudeDir(), "haus");
|
|
4303
4616
|
const manifestPath2 = hausManifestPath();
|
|
4304
|
-
if (
|
|
4305
|
-
await
|
|
4617
|
+
if (fs19.pathExistsSync(manifestPath2)) {
|
|
4618
|
+
await fs19.remove(manifestPath2);
|
|
4306
4619
|
}
|
|
4307
|
-
if (
|
|
4308
|
-
const remaining = await
|
|
4309
|
-
if (remaining.length === 0) await
|
|
4620
|
+
if (fs19.pathExistsSync(hausDir)) {
|
|
4621
|
+
const remaining = await fs19.readdir(hausDir);
|
|
4622
|
+
if (remaining.length === 0) await fs19.remove(hausDir);
|
|
4310
4623
|
}
|
|
4311
4624
|
return result;
|
|
4312
4625
|
}
|
|
@@ -4337,12 +4650,12 @@ async function runUninstallCommand(options) {
|
|
|
4337
4650
|
}
|
|
4338
4651
|
|
|
4339
4652
|
// src/commands/update.ts
|
|
4340
|
-
import
|
|
4653
|
+
import path26 from "node:path";
|
|
4341
4654
|
|
|
4342
4655
|
// src/claude/refresh-project.ts
|
|
4343
|
-
import
|
|
4656
|
+
import fs20 from "fs-extra";
|
|
4344
4657
|
async function isHausProject(root) {
|
|
4345
|
-
if (await
|
|
4658
|
+
if (await fs20.pathExists(hausPath(root, "recommendation.json"))) return true;
|
|
4346
4659
|
const result = await readJsonDetailed(claudePath(root, "settings.json"));
|
|
4347
4660
|
if (result.status === "ok") return result.value._haus != null;
|
|
4348
4661
|
return false;
|
|
@@ -4377,7 +4690,7 @@ function summarizeLockDiff(before, after) {
|
|
|
4377
4690
|
|
|
4378
4691
|
// src/update/lockfile.ts
|
|
4379
4692
|
import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
|
|
4380
|
-
import
|
|
4693
|
+
import path25 from "node:path";
|
|
4381
4694
|
async function checkLock(root) {
|
|
4382
4695
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
4383
4696
|
const hasValidVersions = lock.every(
|
|
@@ -4408,7 +4721,7 @@ async function applyLock(root) {
|
|
|
4408
4721
|
try {
|
|
4409
4722
|
const backupDir = hausPath(root, "backups");
|
|
4410
4723
|
await mkdir(backupDir, { recursive: true });
|
|
4411
|
-
await copyFile(lockPath,
|
|
4724
|
+
await copyFile(lockPath, path25.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
4412
4725
|
} catch {
|
|
4413
4726
|
}
|
|
4414
4727
|
const enriched = await Promise.all(
|
|
@@ -4430,7 +4743,7 @@ function diffLock(before, after) {
|
|
|
4430
4743
|
}
|
|
4431
4744
|
async function hasLocalOverrides(root) {
|
|
4432
4745
|
try {
|
|
4433
|
-
await readFile2(
|
|
4746
|
+
await readFile2(path25.join(root, ".claude", "settings.json"), "utf8");
|
|
4434
4747
|
return true;
|
|
4435
4748
|
} catch {
|
|
4436
4749
|
return false;
|
|
@@ -4442,7 +4755,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
|
4442
4755
|
async function runUpdate(options) {
|
|
4443
4756
|
const root = process.cwd();
|
|
4444
4757
|
if (options.check) {
|
|
4445
|
-
const pkgJson2 = await readJson(
|
|
4758
|
+
const pkgJson2 = await readJson(path26.join(packageRoot(), "package.json"));
|
|
4446
4759
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
4447
4760
|
const [status, npmVersion, latestCatalogTag, globalInstallDrift] = await Promise.all([
|
|
4448
4761
|
checkLock(root),
|
|
@@ -4472,7 +4785,7 @@ async function runUpdate(options) {
|
|
|
4472
4785
|
if (status.driftCount > 0) process.exitCode = 1;
|
|
4473
4786
|
return;
|
|
4474
4787
|
}
|
|
4475
|
-
const pkgJson = await readJson(
|
|
4788
|
+
const pkgJson = await readJson(path26.join(packageRoot(), "package.json"));
|
|
4476
4789
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
4477
4790
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
4478
4791
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -4549,16 +4862,16 @@ async function detectGlobalInstallDrift() {
|
|
|
4549
4862
|
}
|
|
4550
4863
|
|
|
4551
4864
|
// src/commands/validate-catalog.ts
|
|
4552
|
-
import
|
|
4865
|
+
import path28 from "node:path";
|
|
4553
4866
|
|
|
4554
4867
|
// src/catalog/validate-core.ts
|
|
4555
|
-
import
|
|
4556
|
-
import
|
|
4868
|
+
import fs21 from "node:fs";
|
|
4869
|
+
import path27 from "node:path";
|
|
4557
4870
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
4558
4871
|
var MANIFEST_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
|
|
4559
4872
|
function isSafeCatalogPath2(itemPath) {
|
|
4560
|
-
if (!itemPath ||
|
|
4561
|
-
const normalized =
|
|
4873
|
+
if (!itemPath || path27.isAbsolute(itemPath) || itemPath.includes("\\")) return false;
|
|
4874
|
+
const normalized = path27.normalize(itemPath);
|
|
4562
4875
|
return !normalized.startsWith("..") && !normalized.includes("/..");
|
|
4563
4876
|
}
|
|
4564
4877
|
function auditForbiddenStacks(items) {
|
|
@@ -4606,7 +4919,7 @@ function auditManifestStructure(manifestVersion, items) {
|
|
|
4606
4919
|
} else {
|
|
4607
4920
|
seenIds.set(item.id, i);
|
|
4608
4921
|
}
|
|
4609
|
-
if (item.type === "skill" || item.type === "agent" || item.type === "template" || item.type === "command") {
|
|
4922
|
+
if (item.type === "skill" || item.type === "agent" || item.type === "template" || item.type === "command" || item.type === "config") {
|
|
4610
4923
|
if (!item.path) {
|
|
4611
4924
|
failures.push(`${item.id}: missing path`);
|
|
4612
4925
|
} else if (!isSafeCatalogPath2(item.path)) {
|
|
@@ -4637,8 +4950,8 @@ function checkRequiredFrontmatter(text, label) {
|
|
|
4637
4950
|
return failures;
|
|
4638
4951
|
}
|
|
4639
4952
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
4640
|
-
const rel =
|
|
4641
|
-
const text =
|
|
4953
|
+
const rel = path27.relative(manifestDir, absPath);
|
|
4954
|
+
const text = fs21.readFileSync(absPath, "utf8");
|
|
4642
4955
|
const failures = [];
|
|
4643
4956
|
const lines = text.split(/\r?\n/);
|
|
4644
4957
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -4658,42 +4971,46 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4658
4971
|
const failures = [];
|
|
4659
4972
|
for (const item of items) {
|
|
4660
4973
|
if (!item.path || !isSafeCatalogPath2(item.path)) continue;
|
|
4661
|
-
const absPath =
|
|
4974
|
+
const absPath = path27.join(manifestDir, item.path);
|
|
4662
4975
|
if (item.type === "skill") {
|
|
4663
|
-
const skillMd =
|
|
4664
|
-
if (!
|
|
4665
|
-
failures.push(`${item.id}: missing ${
|
|
4976
|
+
const skillMd = path27.join(absPath, "SKILL.md");
|
|
4977
|
+
if (!fs21.existsSync(skillMd)) {
|
|
4978
|
+
failures.push(`${item.id}: missing ${path27.relative(manifestDir, skillMd)}`);
|
|
4666
4979
|
continue;
|
|
4667
4980
|
}
|
|
4668
|
-
const text =
|
|
4981
|
+
const text = fs21.readFileSync(skillMd, "utf8");
|
|
4669
4982
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
4670
4983
|
failures.push(
|
|
4671
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
4984
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path27.relative(manifestDir, skillMd)}`)
|
|
4672
4985
|
);
|
|
4673
4986
|
} else if (item.type === "agent") {
|
|
4674
|
-
if (!
|
|
4987
|
+
if (!fs21.existsSync(absPath)) {
|
|
4675
4988
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
4676
4989
|
continue;
|
|
4677
4990
|
}
|
|
4678
|
-
const text =
|
|
4679
|
-
const rel =
|
|
4991
|
+
const text = fs21.readFileSync(absPath, "utf8");
|
|
4992
|
+
const rel = path27.relative(manifestDir, absPath);
|
|
4680
4993
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
4681
4994
|
failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
|
|
4682
4995
|
} else if (item.type === "template") {
|
|
4683
|
-
if (!
|
|
4996
|
+
if (!fs21.existsSync(absPath)) {
|
|
4684
4997
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
4685
4998
|
continue;
|
|
4686
4999
|
}
|
|
4687
5000
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
4688
5001
|
} else if (item.type === "command") {
|
|
4689
|
-
if (!
|
|
5002
|
+
if (!fs21.existsSync(absPath)) {
|
|
4690
5003
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
4691
5004
|
continue;
|
|
4692
5005
|
}
|
|
4693
|
-
const text =
|
|
4694
|
-
const rel =
|
|
5006
|
+
const text = fs21.readFileSync(absPath, "utf8");
|
|
5007
|
+
const rel = path27.relative(manifestDir, absPath);
|
|
4695
5008
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
4696
5009
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
5010
|
+
} else if (item.type === "config") {
|
|
5011
|
+
if (!fs21.existsSync(absPath)) {
|
|
5012
|
+
failures.push(`${item.id}: missing config path ${item.path}`);
|
|
5013
|
+
}
|
|
4697
5014
|
}
|
|
4698
5015
|
}
|
|
4699
5016
|
return failures;
|
|
@@ -4705,21 +5022,25 @@ var DIR_ITEM_TYPE = {
|
|
|
4705
5022
|
commands: "command"
|
|
4706
5023
|
};
|
|
4707
5024
|
function walkMd(dir, fn) {
|
|
4708
|
-
for (const entry of
|
|
4709
|
-
const full =
|
|
5025
|
+
for (const entry of fs21.readdirSync(dir, { withFileTypes: true })) {
|
|
5026
|
+
const full = path27.join(dir, entry.name);
|
|
4710
5027
|
if (entry.isDirectory()) walkMd(full, fn);
|
|
4711
5028
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
4712
5029
|
}
|
|
4713
5030
|
}
|
|
4714
|
-
function auditMarkdownContent(manifestDir) {
|
|
5031
|
+
function auditMarkdownContent(manifestDir, items) {
|
|
4715
5032
|
const failures = [];
|
|
5033
|
+
const pathSourceMap = buildItemPathSourceMap(items);
|
|
4716
5034
|
for (const dir of ["skills", "agents", "templates", "commands"]) {
|
|
4717
|
-
const abs =
|
|
4718
|
-
if (!
|
|
4719
|
-
const
|
|
5035
|
+
const abs = path27.join(manifestDir, dir);
|
|
5036
|
+
if (!fs21.existsSync(abs)) continue;
|
|
5037
|
+
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
4720
5038
|
walkMd(abs, (file) => {
|
|
4721
|
-
const text =
|
|
4722
|
-
const rel =
|
|
5039
|
+
const text = fs21.readFileSync(file, "utf8");
|
|
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);
|
|
4723
5044
|
const lines = text.split(/\r?\n/);
|
|
4724
5045
|
for (let i = 0; i < lines.length; i++) {
|
|
4725
5046
|
const line2 = lines[i] ?? "";
|
|
@@ -4729,7 +5050,7 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4729
5050
|
failures.push(`${rel}:${i + 1}: disallowed npx (only npx tsx allowed)`);
|
|
4730
5051
|
}
|
|
4731
5052
|
}
|
|
4732
|
-
if (!
|
|
5053
|
+
if (!norm.includes("/references/")) failures.push(...auditForbiddenTagsInText(text, rel));
|
|
4733
5054
|
});
|
|
4734
5055
|
}
|
|
4735
5056
|
return failures;
|
|
@@ -4739,7 +5060,7 @@ function validateCatalogData(manifestDir, manifestVersion, items) {
|
|
|
4739
5060
|
...auditManifestStructure(manifestVersion, items),
|
|
4740
5061
|
...auditForbiddenStacks(items),
|
|
4741
5062
|
...auditShippedFiles(manifestDir, items),
|
|
4742
|
-
...auditMarkdownContent(manifestDir),
|
|
5063
|
+
...auditMarkdownContent(manifestDir, items),
|
|
4743
5064
|
...auditDisallowedTags(items)
|
|
4744
5065
|
];
|
|
4745
5066
|
return { ok: failures.length === 0, failures, items };
|
|
@@ -4752,8 +5073,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
4752
5073
|
process.exitCode = 1;
|
|
4753
5074
|
return;
|
|
4754
5075
|
}
|
|
4755
|
-
const abs =
|
|
4756
|
-
const manifestDir =
|
|
5076
|
+
const abs = path28.resolve(process.cwd(), manifestPath2);
|
|
5077
|
+
const manifestDir = path28.dirname(abs);
|
|
4757
5078
|
const data = await readJson(abs);
|
|
4758
5079
|
if (!data?.items) {
|
|
4759
5080
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -4771,7 +5092,7 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
4771
5092
|
|
|
4772
5093
|
// src/commands/workspace.ts
|
|
4773
5094
|
import { existsSync as existsSync5, statSync as statSync2 } from "node:fs";
|
|
4774
|
-
import
|
|
5095
|
+
import path35 from "node:path";
|
|
4775
5096
|
|
|
4776
5097
|
// src/commands/workspace/aggregate.ts
|
|
4777
5098
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -4825,7 +5146,7 @@ ${summaries.map(
|
|
|
4825
5146
|
}
|
|
4826
5147
|
|
|
4827
5148
|
// src/commands/workspace/config.ts
|
|
4828
|
-
import
|
|
5149
|
+
import path29 from "node:path";
|
|
4829
5150
|
import YAML from "yaml";
|
|
4830
5151
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
4831
5152
|
function parseWorkspaceConfig(text) {
|
|
@@ -4848,11 +5169,11 @@ function parseWorkspaceConfig(text) {
|
|
|
4848
5169
|
};
|
|
4849
5170
|
}
|
|
4850
5171
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
4851
|
-
return parseWorkspaceConfig(await readText(
|
|
5172
|
+
return parseWorkspaceConfig(await readText(path29.join(workspaceRoot, WORKSPACE_FILE)));
|
|
4852
5173
|
}
|
|
4853
5174
|
|
|
4854
5175
|
// src/commands/workspace/discover.ts
|
|
4855
|
-
import
|
|
5176
|
+
import path30 from "node:path";
|
|
4856
5177
|
import fg4 from "fast-glob";
|
|
4857
5178
|
import YAML2 from "yaml";
|
|
4858
5179
|
var DEFAULT_MAX_DEPTH = 3;
|
|
@@ -4880,8 +5201,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
4880
5201
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
4881
5202
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
4882
5203
|
for (const match of matches) {
|
|
4883
|
-
const base =
|
|
4884
|
-
const dir =
|
|
5204
|
+
const base = path30.posix.basename(match);
|
|
5205
|
+
const dir = path30.posix.dirname(match);
|
|
4885
5206
|
const owner = dir === "." ? "." : dir;
|
|
4886
5207
|
if (base === ".git") gitDirs.add(owner);
|
|
4887
5208
|
else manifestDirs.add(owner);
|
|
@@ -4897,9 +5218,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
4897
5218
|
}
|
|
4898
5219
|
repoRoots.sort((a, b) => a.localeCompare(b));
|
|
4899
5220
|
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
4900
|
-
const absDir =
|
|
4901
|
-
const pkg = await readJson(
|
|
4902
|
-
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name :
|
|
5221
|
+
const absDir = path30.resolve(workspaceRoot, relDir);
|
|
5222
|
+
const pkg = await readJson(path30.join(absDir, "package.json"));
|
|
5223
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path30.basename(relDir === "." ? workspaceRoot : absDir);
|
|
4903
5224
|
let role = "auto";
|
|
4904
5225
|
try {
|
|
4905
5226
|
const scan = await scanProject(absDir);
|
|
@@ -4942,7 +5263,7 @@ function renderWorkspaceYaml(config) {
|
|
|
4942
5263
|
});
|
|
4943
5264
|
}
|
|
4944
5265
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
4945
|
-
const yamlPath =
|
|
5266
|
+
const yamlPath = path30.join(workspaceRoot, "haus.workspace.yaml");
|
|
4946
5267
|
const existingText = await readText(yamlPath);
|
|
4947
5268
|
const existing = parseWorkspaceConfig(existingText);
|
|
4948
5269
|
if (existingText && !existing) {
|
|
@@ -4976,18 +5297,18 @@ async function runDiscover(workspaceRoot, opts = {}) {
|
|
|
4976
5297
|
|
|
4977
5298
|
// src/commands/workspace/doctor.ts
|
|
4978
5299
|
import { existsSync as existsSync3 } from "node:fs";
|
|
4979
|
-
import
|
|
5300
|
+
import path32 from "node:path";
|
|
4980
5301
|
|
|
4981
5302
|
// src/commands/workspace/manifest.ts
|
|
4982
5303
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
4983
|
-
import
|
|
5304
|
+
import path31 from "node:path";
|
|
4984
5305
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
4985
5306
|
function manifestPath(workspaceRoot) {
|
|
4986
5307
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
4987
5308
|
}
|
|
4988
5309
|
function hausVersion() {
|
|
4989
5310
|
try {
|
|
4990
|
-
const pkg = JSON.parse(readFileSync3(
|
|
5311
|
+
const pkg = JSON.parse(readFileSync3(path31.join(packageRoot(), "package.json"), "utf8"));
|
|
4991
5312
|
return pkg.version ?? "0.0.0";
|
|
4992
5313
|
} catch {
|
|
4993
5314
|
return "0.0.0";
|
|
@@ -5053,7 +5374,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
5053
5374
|
}
|
|
5054
5375
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
5055
5376
|
for (const repo of config.repos) {
|
|
5056
|
-
const repoRoot =
|
|
5377
|
+
const repoRoot = path32.resolve(workspaceRoot, repo.path);
|
|
5057
5378
|
const entry = manifestByName.get(repo.name);
|
|
5058
5379
|
if (!entry) {
|
|
5059
5380
|
flag({
|
|
@@ -5127,11 +5448,11 @@ function emit(args) {
|
|
|
5127
5448
|
|
|
5128
5449
|
// src/commands/workspace/setup.ts
|
|
5129
5450
|
import { existsSync as existsSync4, statSync } from "node:fs";
|
|
5130
|
-
import
|
|
5451
|
+
import path34 from "node:path";
|
|
5131
5452
|
|
|
5132
5453
|
// src/claude/write-workspace-claude-md.ts
|
|
5133
|
-
import
|
|
5134
|
-
import
|
|
5454
|
+
import path33 from "node:path";
|
|
5455
|
+
import fs22 from "fs-extra";
|
|
5135
5456
|
function buildWorkspaceImportBlock(client, members) {
|
|
5136
5457
|
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
5137
5458
|
const body = [
|
|
@@ -5149,8 +5470,8 @@ ${BLOCK_END}`;
|
|
|
5149
5470
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
5150
5471
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
5151
5472
|
const dryRun = opts.dryRun ?? false;
|
|
5152
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
5153
|
-
const prev = await
|
|
5473
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path33.join(workspaceRoot, "CLAUDE.md");
|
|
5474
|
+
const prev = await fs22.pathExists(filePath) ? await fs22.readFile(filePath, "utf8") : "";
|
|
5154
5475
|
const next = opts.collision ? `${block}
|
|
5155
5476
|
` : injectHausBlock(prev, block);
|
|
5156
5477
|
const printable = displayPath(workspaceRoot, filePath);
|
|
@@ -5175,20 +5496,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
5175
5496
|
|
|
5176
5497
|
// src/commands/workspace/setup.ts
|
|
5177
5498
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
5178
|
-
let dir =
|
|
5499
|
+
let dir = path34.resolve(start);
|
|
5179
5500
|
for (; ; ) {
|
|
5180
|
-
if (existsSync4(
|
|
5181
|
-
const parent =
|
|
5182
|
-
if (parent === dir) return
|
|
5501
|
+
if (existsSync4(path34.join(dir, WORKSPACE_FILE))) return dir;
|
|
5502
|
+
const parent = path34.dirname(dir);
|
|
5503
|
+
if (parent === dir) return path34.resolve(start);
|
|
5183
5504
|
dir = parent;
|
|
5184
5505
|
}
|
|
5185
5506
|
}
|
|
5186
5507
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
5187
|
-
return
|
|
5508
|
+
return path34.resolve(workspaceRoot, repoPath) === path34.resolve(workspaceRoot);
|
|
5188
5509
|
}
|
|
5189
5510
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
5190
5511
|
const apply = options.write ?? false;
|
|
5191
|
-
const configText = await readText(
|
|
5512
|
+
const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
|
|
5192
5513
|
if (!configText) {
|
|
5193
5514
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
5194
5515
|
process.exitCode = 1;
|
|
@@ -5205,7 +5526,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
5205
5526
|
const statuses = [];
|
|
5206
5527
|
const aggregateInputs = [];
|
|
5207
5528
|
for (const repo of repos) {
|
|
5208
|
-
const repoRoot =
|
|
5529
|
+
const repoRoot = path34.resolve(workspaceRoot, repo.path);
|
|
5209
5530
|
log(`
|
|
5210
5531
|
\u2192 ${repo.name} (${repo.path})`);
|
|
5211
5532
|
try {
|
|
@@ -5273,7 +5594,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
5273
5594
|
const status = statusByName.get(repo.name);
|
|
5274
5595
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
5275
5596
|
if (status?.status === "ok") {
|
|
5276
|
-
const lock = await checkLock(
|
|
5597
|
+
const lock = await checkLock(path34.resolve(workspaceRoot, repo.path));
|
|
5277
5598
|
manifestRepos.push({
|
|
5278
5599
|
name: repo.name,
|
|
5279
5600
|
path: repo.path,
|
|
@@ -5353,7 +5674,7 @@ relationships: []
|
|
|
5353
5674
|
log("Workspace initialized.");
|
|
5354
5675
|
}
|
|
5355
5676
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
5356
|
-
const configText = await readText(
|
|
5677
|
+
const configText = await readText(path35.join(workspaceRoot, WORKSPACE_FILE));
|
|
5357
5678
|
if (!configText) {
|
|
5358
5679
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
5359
5680
|
process.exitCode = 1;
|
|
@@ -5374,7 +5695,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
5374
5695
|
}
|
|
5375
5696
|
const inputs = [];
|
|
5376
5697
|
for (const repo of config.repos) {
|
|
5377
|
-
const repoRoot =
|
|
5698
|
+
const repoRoot = path35.resolve(workspaceRoot, repo.path);
|
|
5378
5699
|
if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
|
|
5379
5700
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
5380
5701
|
}
|
|
@@ -5424,7 +5745,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
5424
5745
|
// src/cli.ts
|
|
5425
5746
|
function cliVersion() {
|
|
5426
5747
|
try {
|
|
5427
|
-
const pkgPath =
|
|
5748
|
+
const pkgPath = path36.join(packageRoot(), "package.json");
|
|
5428
5749
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5429
5750
|
return pkg.version ?? "0.0.0";
|
|
5430
5751
|
} catch {
|
|
@@ -5434,7 +5755,7 @@ function cliVersion() {
|
|
|
5434
5755
|
var program = new Command();
|
|
5435
5756
|
function validateRuntimeNodeVersion() {
|
|
5436
5757
|
try {
|
|
5437
|
-
const pkgPath =
|
|
5758
|
+
const pkgPath = path36.join(packageRoot(), "package.json");
|
|
5438
5759
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5439
5760
|
const requiredRange = pkg.engines?.node;
|
|
5440
5761
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -5470,6 +5791,9 @@ program.command("catalog-audit").action(runCatalogAudit);
|
|
|
5470
5791
|
program.command("validate-catalog").argument("[manifest]").action(runValidateCatalog);
|
|
5471
5792
|
program.command("update").option("--check").action(runUpdate);
|
|
5472
5793
|
program.command("install").option("--dry-run").option("--force").option("--check", "Exit non-zero if any HAUS-MANAGED file is out of date").option("--postinstall", "Run by the npm postinstall hook; prints a plain-language change notice").action(runInstall);
|
|
5794
|
+
program.command("scaffold [ids...]").description(
|
|
5795
|
+
"Copy catalog config items (ESLint, Prettier) into the project root. Pass item IDs to scaffold specific items, or omit to scaffold all available config items."
|
|
5796
|
+
).option("--force", "Overwrite existing files").option("--dry-run", "Preview what would be written without changing anything").option("--root <path>", "Project root (defaults to cwd)", process.cwd()).action(runScaffold);
|
|
5473
5797
|
program.command("uninstall").option("--force").action(runUninstallCommand);
|
|
5474
5798
|
var guard = program.command("guard");
|
|
5475
5799
|
guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
|