@haus-tech/haus-workflow 0.25.0 → 0.26.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 +12 -0
- package/LICENSE +21 -0
- package/README.md +0 -2
- package/dist/cli.js +277 -726
- package/library/catalog/manifest.json +318 -1
- package/library/catalog/validation-rules.json +1 -0
- package/library/global/settings-fragments/hooks.json +0 -6
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { readFileSync as readFileSync4 } from "fs";
|
|
5
|
-
import
|
|
5
|
+
import path34 from "path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/commands/apply.ts
|
|
9
|
-
import
|
|
9
|
+
import path13 from "path";
|
|
10
10
|
import checkbox from "@inquirer/checkbox";
|
|
11
11
|
import fs13 from "fs-extra";
|
|
12
12
|
|
|
@@ -119,6 +119,7 @@ var validation_rules_default = {
|
|
|
119
119
|
],
|
|
120
120
|
allowedNpxPattern: { source: "\\bnpx\\s+tsx\\b", flags: "i" },
|
|
121
121
|
anyNpxPattern: { source: "\\bnpx\\s+\\S+", flags: "i" },
|
|
122
|
+
npxTsxOnlyExemptTypes: ["agent"],
|
|
122
123
|
httpUrlPattern: { source: "^http:\\/\\/", flags: "i" },
|
|
123
124
|
placeholderPattern: { source: "\\bTODO\\b|\\bPLACEHOLDER\\b", flags: "i" },
|
|
124
125
|
allowedStacks: [
|
|
@@ -239,6 +240,7 @@ var REQUIRED_SKILL_FRONTMATTER = validation_rules_default.requiredSkillFrontmatt
|
|
|
239
240
|
var RISKY_INSTALL_PATTERNS = validation_rules_default.riskyInstallPatterns.map(toRegExp);
|
|
240
241
|
var ALLOWED_NPX_PATTERN = toRegExp(validation_rules_default.allowedNpxPattern);
|
|
241
242
|
var ANY_NPX_PATTERN = toRegExp(validation_rules_default.anyNpxPattern);
|
|
243
|
+
var NPX_TSX_ONLY_EXEMPT_TYPES = validation_rules_default.npxTsxOnlyExemptTypes ?? [];
|
|
242
244
|
var HTTP_URL_PATTERN = toRegExp(validation_rules_default.httpUrlPattern);
|
|
243
245
|
var PLACEHOLDER_PATTERN = toRegExp(validation_rules_default.placeholderPattern);
|
|
244
246
|
var ALLOWED_STACKS = validation_rules_default.allowedStacks;
|
|
@@ -317,13 +319,14 @@ ${extractUseWhenSection(text)}`;
|
|
|
317
319
|
// src/catalog/ingest-catalog.ts
|
|
318
320
|
function validateCatalogItem(item, content2) {
|
|
319
321
|
const label = item.id;
|
|
322
|
+
const checkNonTsxNpx = !NPX_TSX_ONLY_EXEMPT_TYPES.includes(item.type);
|
|
320
323
|
const lines = content2.split(/\r?\n/);
|
|
321
324
|
for (let i = 0; i < lines.length; i++) {
|
|
322
325
|
const line2 = lines[i] ?? "";
|
|
323
326
|
if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2))) {
|
|
324
327
|
return { ok: false, reason: `${label}: risky install pattern at line ${i + 1}` };
|
|
325
328
|
}
|
|
326
|
-
if (ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
|
|
329
|
+
if (checkNonTsxNpx && ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
|
|
327
330
|
return { ok: false, reason: `${label}: disallowed npx at line ${i + 1}` };
|
|
328
331
|
}
|
|
329
332
|
}
|
|
@@ -839,14 +842,7 @@ async function getCacheManifestAge() {
|
|
|
839
842
|
}
|
|
840
843
|
|
|
841
844
|
// src/install/allow-rules.ts
|
|
842
|
-
var ALLOWED_SUBCOMMANDS = [
|
|
843
|
-
"setup-project",
|
|
844
|
-
"apply",
|
|
845
|
-
"doctor",
|
|
846
|
-
"scan",
|
|
847
|
-
"context",
|
|
848
|
-
"recommend"
|
|
849
|
-
];
|
|
845
|
+
var ALLOWED_SUBCOMMANDS = ["setup-project", "apply", "doctor", "scan", "recommend"];
|
|
850
846
|
function buildAllowRules() {
|
|
851
847
|
return [...new Set(ALLOWED_SUBCOMMANDS.map((sub) => `Bash(haus ${sub}:*)`))];
|
|
852
848
|
}
|
|
@@ -901,6 +897,44 @@ function collectEventHookCommands(entries) {
|
|
|
901
897
|
}
|
|
902
898
|
return cmds;
|
|
903
899
|
}
|
|
900
|
+
var RETIRED_HAUS_HOOK_COMMANDS = ["haus context --from-hook"];
|
|
901
|
+
var RETIRED_HAUS_HOOK_IDS = ["hook.context", "haus.context-hook"];
|
|
902
|
+
function reconcileRetiredHausHooks(updated, fragments, priorHaus, addedIds, addedCommands) {
|
|
903
|
+
const canonicalCommands = new Set(
|
|
904
|
+
fragments.filter((f) => f.gate === "keep").map((f) => f.command)
|
|
905
|
+
);
|
|
906
|
+
const canonicalIds = new Set(fragments.filter((f) => f.gate === "keep").map((f) => f.id));
|
|
907
|
+
const retiredCommands = new Set(RETIRED_HAUS_HOOK_COMMANDS);
|
|
908
|
+
for (const cmd of [...priorHaus?.hookCommands ?? [], ...addedCommands]) {
|
|
909
|
+
if (!canonicalCommands.has(cmd)) retiredCommands.add(cmd);
|
|
910
|
+
}
|
|
911
|
+
const prunedHooks = {};
|
|
912
|
+
for (const [event, entries] of Object.entries(updated.hooks ?? {})) {
|
|
913
|
+
const kept = entries.filter((entry) => {
|
|
914
|
+
const cmds = (entry.hooks ?? []).map((h) => h.command).filter(Boolean);
|
|
915
|
+
return cmds.length === 0 || !cmds.some((cmd) => retiredCommands.has(cmd));
|
|
916
|
+
});
|
|
917
|
+
if (kept.length > 0) prunedHooks[event] = kept;
|
|
918
|
+
}
|
|
919
|
+
updated.hooks = prunedHooks;
|
|
920
|
+
const retiredIdSet = new Set(RETIRED_HAUS_HOOK_IDS);
|
|
921
|
+
for (const id of priorHaus?.hooks ?? []) {
|
|
922
|
+
if (!canonicalIds.has(id)) retiredIdSet.add(id);
|
|
923
|
+
}
|
|
924
|
+
const survivingIds = [.../* @__PURE__ */ new Set([...priorHaus?.hooks ?? [], ...addedIds])].filter(
|
|
925
|
+
(id) => canonicalIds.has(id) && !retiredIdSet.has(id)
|
|
926
|
+
);
|
|
927
|
+
const survivingCommands = [
|
|
928
|
+
.../* @__PURE__ */ new Set([...priorHaus?.hookCommands ?? [], ...addedCommands])
|
|
929
|
+
].filter((cmd) => canonicalCommands.has(cmd));
|
|
930
|
+
updated._haus = {
|
|
931
|
+
hooks: survivingIds,
|
|
932
|
+
...survivingCommands.length > 0 ? { hookCommands: survivingCommands } : {},
|
|
933
|
+
...priorHaus?.denyRules ? { denyRules: priorHaus.denyRules } : {},
|
|
934
|
+
...priorHaus?.allowRules ? { allowRules: priorHaus.allowRules } : {},
|
|
935
|
+
...priorHaus?.askRules ? { askRules: priorHaus.askRules } : {}
|
|
936
|
+
};
|
|
937
|
+
}
|
|
904
938
|
function mergeHooks(settings, fragments) {
|
|
905
939
|
const existing = settings._haus?.hooks ?? [];
|
|
906
940
|
const existingCommands = settings._haus?.hookCommands ?? [];
|
|
@@ -926,14 +960,7 @@ function mergeHooks(settings, fragments) {
|
|
|
926
960
|
if (!existing.includes(fragment.id)) addedIds.push(fragment.id);
|
|
927
961
|
if (!existingCommands.includes(fragment.command)) addedCommands.push(fragment.command);
|
|
928
962
|
}
|
|
929
|
-
updated._haus
|
|
930
|
-
hooks: [...existing, ...addedIds],
|
|
931
|
-
hookCommands: [...existingCommands, ...addedCommands],
|
|
932
|
-
// Preserve deny/allow/ask tracking so hook, deny, allow, and ask merges are order-independent.
|
|
933
|
-
...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
|
|
934
|
-
...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {},
|
|
935
|
-
...settings._haus?.askRules ? { askRules: settings._haus.askRules } : {}
|
|
936
|
-
};
|
|
963
|
+
reconcileRetiredHausHooks(updated, fragments, settings._haus, addedIds, addedCommands);
|
|
937
964
|
return { settings: updated, addedIds };
|
|
938
965
|
}
|
|
939
966
|
function reconcileManagedRules(existing, prevTracked, newRules) {
|
|
@@ -1185,8 +1212,8 @@ function buildDenyRules() {
|
|
|
1185
1212
|
for (const command of DENY_COMMANDS) {
|
|
1186
1213
|
rules.push(`Bash(${command}:*)`);
|
|
1187
1214
|
}
|
|
1188
|
-
for (const
|
|
1189
|
-
const pattern = DENY_DIRS.has(
|
|
1215
|
+
for (const path35 of DENY_PATHS) {
|
|
1216
|
+
const pattern = DENY_DIRS.has(path35) ? `${path35}/**` : path35;
|
|
1190
1217
|
for (const tool of FILE_TOOLS) {
|
|
1191
1218
|
rules.push(`${tool}(${pattern})`);
|
|
1192
1219
|
}
|
|
@@ -1242,12 +1269,6 @@ function packageRoot() {
|
|
|
1242
1269
|
|
|
1243
1270
|
// src/claude/merge-project-settings.ts
|
|
1244
1271
|
var PROJECT_HOOK_FRAGMENTS = [
|
|
1245
|
-
{
|
|
1246
|
-
id: "haus.context-hook",
|
|
1247
|
-
gate: "keep",
|
|
1248
|
-
event: "UserPromptSubmit",
|
|
1249
|
-
command: "haus context --from-hook"
|
|
1250
|
-
},
|
|
1251
1272
|
{
|
|
1252
1273
|
id: "haus.guard-file",
|
|
1253
1274
|
gate: "keep",
|
|
@@ -1285,7 +1306,7 @@ async function applyProjectSettingsMerge(root) {
|
|
|
1285
1306
|
}
|
|
1286
1307
|
|
|
1287
1308
|
// src/claude/write-claude-files.ts
|
|
1288
|
-
import
|
|
1309
|
+
import path12 from "path";
|
|
1289
1310
|
import fs12 from "fs-extra";
|
|
1290
1311
|
|
|
1291
1312
|
// src/catalog/load-catalog.ts
|
|
@@ -1388,19 +1409,6 @@ async function hashInstalledPaths(root, relPaths) {
|
|
|
1388
1409
|
return hashText(fileDigests.map((f) => `${f.rel}=${f.digest}`).join("|"));
|
|
1389
1410
|
}
|
|
1390
1411
|
|
|
1391
|
-
// src/claude/load-hooks-config.ts
|
|
1392
|
-
import path8 from "path";
|
|
1393
|
-
var CONFIG_PATH = ".haus-workflow/config.json";
|
|
1394
|
-
var DEFAULT_HOOKS_CONFIG = {
|
|
1395
|
-
hooks: {
|
|
1396
|
-
context: { enabled: false }
|
|
1397
|
-
}
|
|
1398
|
-
};
|
|
1399
|
-
async function isHookEnabled(root, key) {
|
|
1400
|
-
const cfg = await readJson(path8.join(root, CONFIG_PATH));
|
|
1401
|
-
return cfg?.hooks?.[key]?.enabled === true;
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
1412
|
// src/claude/managed-write.ts
|
|
1405
1413
|
import fs5 from "fs-extra";
|
|
1406
1414
|
|
|
@@ -1454,7 +1462,7 @@ async function writeManagedJson(root, filePath, value, dryRun) {
|
|
|
1454
1462
|
}
|
|
1455
1463
|
|
|
1456
1464
|
// src/claude/superpowers-install.ts
|
|
1457
|
-
import
|
|
1465
|
+
import path8 from "path";
|
|
1458
1466
|
import fg3 from "fast-glob";
|
|
1459
1467
|
import fs6 from "fs-extra";
|
|
1460
1468
|
var SUPERPOWERS_ORIGIN_SOURCE_ID = "superpowers-pcvelz";
|
|
@@ -1471,7 +1479,7 @@ function rewriteSuperpowersMarkdown(text) {
|
|
|
1471
1479
|
async function rewriteMarkdownTree(dir) {
|
|
1472
1480
|
const files = await fg3("**/*.md", { cwd: dir, onlyFiles: true, dot: true });
|
|
1473
1481
|
for (const rel of files) {
|
|
1474
|
-
const abs =
|
|
1482
|
+
const abs = path8.join(dir, rel);
|
|
1475
1483
|
const text = await fs6.readFile(abs, "utf8");
|
|
1476
1484
|
const rewritten = rewriteSuperpowersMarkdown(text);
|
|
1477
1485
|
if (rewritten !== text) {
|
|
@@ -1481,7 +1489,7 @@ async function rewriteMarkdownTree(dir) {
|
|
|
1481
1489
|
}
|
|
1482
1490
|
async function installCatalogSkill(sourcePath, destination, opts) {
|
|
1483
1491
|
if (opts.dryRun) return;
|
|
1484
|
-
await fs6.ensureDir(
|
|
1492
|
+
await fs6.ensureDir(path8.dirname(destination));
|
|
1485
1493
|
if (await fs6.pathExists(destination)) {
|
|
1486
1494
|
await fs6.remove(destination);
|
|
1487
1495
|
}
|
|
@@ -1491,17 +1499,17 @@ async function installCatalogSkill(sourcePath, destination, opts) {
|
|
|
1491
1499
|
}
|
|
1492
1500
|
}
|
|
1493
1501
|
async function installSuperpowersShared(contentRoot, projectRoot, dryRun) {
|
|
1494
|
-
const source =
|
|
1502
|
+
const source = path8.join(contentRoot, SUPERPOWERS_SHARED_CATALOG_REL);
|
|
1495
1503
|
if (!await fs6.pathExists(source)) return null;
|
|
1496
1504
|
const destination = claudePath(projectRoot, "skills", "shared");
|
|
1497
|
-
if (dryRun) return
|
|
1498
|
-
await fs6.ensureDir(
|
|
1505
|
+
if (dryRun) return path8.relative(projectRoot, destination);
|
|
1506
|
+
await fs6.ensureDir(path8.dirname(destination));
|
|
1499
1507
|
if (await fs6.pathExists(destination)) {
|
|
1500
1508
|
await fs6.remove(destination);
|
|
1501
1509
|
}
|
|
1502
1510
|
await fs6.copy(source, destination, { overwrite: true, errorOnExist: false });
|
|
1503
1511
|
await rewriteMarkdownTree(destination);
|
|
1504
|
-
return
|
|
1512
|
+
return path8.relative(projectRoot, destination);
|
|
1505
1513
|
}
|
|
1506
1514
|
|
|
1507
1515
|
// src/claude/verify-hooks-contract.ts
|
|
@@ -1510,11 +1518,6 @@ import fs7 from "fs-extra";
|
|
|
1510
1518
|
// src/claude/load-hooks.ts
|
|
1511
1519
|
var CANONICAL_HOOKS = {
|
|
1512
1520
|
hooks: {
|
|
1513
|
-
UserPromptSubmit: [
|
|
1514
|
-
{
|
|
1515
|
-
hooks: [{ type: "command", command: "haus context --from-hook" }]
|
|
1516
|
-
}
|
|
1517
|
-
],
|
|
1518
1521
|
PreToolUse: [
|
|
1519
1522
|
{
|
|
1520
1523
|
matcher: "Read|Edit|Write",
|
|
@@ -1545,11 +1548,6 @@ function collectHookCommands(settings) {
|
|
|
1545
1548
|
}
|
|
1546
1549
|
function hausHookContractSatisfied(project, canonical) {
|
|
1547
1550
|
const present = new Set(collectHookCommands(project));
|
|
1548
|
-
for (const block of canonical.hooks.UserPromptSubmit) {
|
|
1549
|
-
for (const h of block.hooks) {
|
|
1550
|
-
if (!present.has(h.command)) return false;
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
1551
|
for (const block of canonical.hooks.PreToolUse) {
|
|
1554
1552
|
for (const h of block.hooks) {
|
|
1555
1553
|
if (!present.has(h.command)) return false;
|
|
@@ -1604,7 +1602,7 @@ async function verifyProjectSettingsHooksContract(root) {
|
|
|
1604
1602
|
}
|
|
1605
1603
|
|
|
1606
1604
|
// src/claude/write-root-claude-md.ts
|
|
1607
|
-
import
|
|
1605
|
+
import path9 from "path";
|
|
1608
1606
|
import fs8 from "fs-extra";
|
|
1609
1607
|
var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
|
|
1610
1608
|
var BLOCK_END = "<!-- HAUS:END haus-imports -->";
|
|
@@ -1645,7 +1643,7 @@ ${block}
|
|
|
1645
1643
|
`;
|
|
1646
1644
|
}
|
|
1647
1645
|
async function writeRootClaudeMd(root, dryRun) {
|
|
1648
|
-
const filePath =
|
|
1646
|
+
const filePath = path9.join(root, "CLAUDE.md");
|
|
1649
1647
|
const block = buildImportBlock();
|
|
1650
1648
|
const prev = await fs8.pathExists(filePath) ? await fs8.readFile(filePath, "utf8") : "";
|
|
1651
1649
|
const next = injectHausBlock(prev, block);
|
|
@@ -1654,11 +1652,11 @@ async function writeRootClaudeMd(root, dryRun) {
|
|
|
1654
1652
|
}
|
|
1655
1653
|
|
|
1656
1654
|
// src/claude/write-workflow-config.ts
|
|
1657
|
-
import
|
|
1655
|
+
import path11 from "path";
|
|
1658
1656
|
import fs10 from "fs-extra";
|
|
1659
1657
|
|
|
1660
1658
|
// src/claude/derive-workflow-config.ts
|
|
1661
|
-
import
|
|
1659
|
+
import path10 from "path";
|
|
1662
1660
|
import fs9 from "fs-extra";
|
|
1663
1661
|
function binCmd(pm, bin, args) {
|
|
1664
1662
|
const tail = args ? ` ${args}` : "";
|
|
@@ -1668,7 +1666,7 @@ function binCmd(pm, bin, args) {
|
|
|
1668
1666
|
}
|
|
1669
1667
|
async function deriveWorkflowConfig(root, ctx) {
|
|
1670
1668
|
const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
|
|
1671
|
-
const pkg = await readJson(
|
|
1669
|
+
const pkg = await readJson(path10.join(root, "package.json"));
|
|
1672
1670
|
const scripts = pkg?.scripts ?? {};
|
|
1673
1671
|
const deps = new Set(ctx.dependencies);
|
|
1674
1672
|
const stacks = Object.values(ctx.detectedStacks ?? {}).flat();
|
|
@@ -1678,7 +1676,7 @@ async function deriveWorkflowConfig(root, ctx) {
|
|
|
1678
1676
|
return null;
|
|
1679
1677
|
};
|
|
1680
1678
|
const hasDep = (name) => deps.has(name);
|
|
1681
|
-
const exists = (rel) => fs9.pathExistsSync(
|
|
1679
|
+
const exists = (rel) => fs9.pathExistsSync(path10.join(root, rel));
|
|
1682
1680
|
const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
|
|
1683
1681
|
const hasCypress = hasDep("cypress");
|
|
1684
1682
|
const preCommitTool = exists("lefthook.yml") || exists("lefthook.yaml") ? "lefthook" : exists(".husky") || hasDep("husky") || (scripts.prepare ?? "").includes("husky") ? "husky" : exists(".pre-commit-config.yaml") ? "pre-commit (Python framework)" : null;
|
|
@@ -1754,7 +1752,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
1754
1752
|
const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
|
|
1755
1753
|
...FALLBACK_CONTEXT,
|
|
1756
1754
|
root,
|
|
1757
|
-
repoName:
|
|
1755
|
+
repoName: path11.basename(root)
|
|
1758
1756
|
};
|
|
1759
1757
|
const values = await deriveWorkflowConfig(root, ctx);
|
|
1760
1758
|
if (exists) {
|
|
@@ -1861,7 +1859,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1861
1859
|
estimatedTokenReductionPct: 0
|
|
1862
1860
|
};
|
|
1863
1861
|
const pkgRoot = packageRoot();
|
|
1864
|
-
const hausVersion2 = (await readJson(
|
|
1862
|
+
const hausVersion2 = (await readJson(path12.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
|
|
1865
1863
|
const coreFiles = [
|
|
1866
1864
|
claudePath(root, "settings.json"),
|
|
1867
1865
|
claudePath(root, "rules", "haus.md"),
|
|
@@ -1885,10 +1883,6 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1885
1883
|
await applyProjectSettingsMerge(root);
|
|
1886
1884
|
await assertPostApplySettingsHausContract(root);
|
|
1887
1885
|
}
|
|
1888
|
-
const configPath = hausPath(root, "config.json");
|
|
1889
|
-
if (!await fs12.pathExists(configPath)) {
|
|
1890
|
-
await writeManagedJson(root, configPath, DEFAULT_HOOKS_CONFIG, dryRun);
|
|
1891
|
-
}
|
|
1892
1886
|
await writeManagedText(
|
|
1893
1887
|
root,
|
|
1894
1888
|
claudePath(root, "commands", "haus-doctor.md"),
|
|
@@ -1947,6 +1941,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1947
1941
|
}
|
|
1948
1942
|
}
|
|
1949
1943
|
const LEGACY_PRUNED_ARTIFACTS = [
|
|
1944
|
+
"config.json",
|
|
1950
1945
|
"selected-context.json",
|
|
1951
1946
|
"dependency-map.json",
|
|
1952
1947
|
"scan-hashes.json",
|
|
@@ -1997,7 +1992,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1997
1992
|
);
|
|
1998
1993
|
continue;
|
|
1999
1994
|
}
|
|
2000
|
-
const destination = claudePath(root, target,
|
|
1995
|
+
const destination = claudePath(root, target, path12.basename(sourcePath));
|
|
2001
1996
|
if (await fs12.pathExists(sourcePath)) {
|
|
2002
1997
|
if (dryRun) {
|
|
2003
1998
|
const exists = await fs12.pathExists(destination);
|
|
@@ -2010,17 +2005,17 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2010
2005
|
dryRun: false
|
|
2011
2006
|
});
|
|
2012
2007
|
} else {
|
|
2013
|
-
await fs12.ensureDir(
|
|
2008
|
+
await fs12.ensureDir(path12.dirname(destination));
|
|
2014
2009
|
await fs12.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
2015
2010
|
}
|
|
2016
2011
|
files.push(destination);
|
|
2017
|
-
const relPaths = [
|
|
2012
|
+
const relPaths = [path12.relative(root, destination)];
|
|
2018
2013
|
if (!superpowersSharedInstalled && manifestItem.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID && item.type === "skill") {
|
|
2019
2014
|
const sharedRel = await installSuperpowersShared(contentRoot, root, dryRun);
|
|
2020
2015
|
if (sharedRel) {
|
|
2021
2016
|
superpowersSharedInstalled = true;
|
|
2022
2017
|
relPaths.push(sharedRel);
|
|
2023
|
-
files.push(
|
|
2018
|
+
files.push(path12.join(root, sharedRel));
|
|
2024
2019
|
}
|
|
2025
2020
|
}
|
|
2026
2021
|
const current = installedPathsByItem.get(item.id) ?? [];
|
|
@@ -2083,7 +2078,7 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
|
|
|
2083
2078
|
if (relPaths.length === 0) continue;
|
|
2084
2079
|
const existing = [];
|
|
2085
2080
|
for (const rel of relPaths) {
|
|
2086
|
-
if (await fs12.pathExists(
|
|
2081
|
+
if (await fs12.pathExists(path12.join(root, rel))) existing.push(rel);
|
|
2087
2082
|
}
|
|
2088
2083
|
if (existing.length === 0) continue;
|
|
2089
2084
|
if (entry.hash === void 0) {
|
|
@@ -2100,13 +2095,13 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
|
|
|
2100
2095
|
continue;
|
|
2101
2096
|
}
|
|
2102
2097
|
for (const rel of existing) {
|
|
2103
|
-
const abs =
|
|
2098
|
+
const abs = path12.join(root, rel);
|
|
2104
2099
|
if (dryRun) {
|
|
2105
2100
|
log(`[dry-run] would remove stale ${displayPath(root, abs)} (${entry.id})`);
|
|
2106
2101
|
continue;
|
|
2107
2102
|
}
|
|
2108
2103
|
await fs12.remove(abs);
|
|
2109
|
-
await pruneEmptyDir(
|
|
2104
|
+
await pruneEmptyDir(path12.dirname(abs));
|
|
2110
2105
|
log(`Removed stale ${displayPath(root, abs)} (${entry.id})`);
|
|
2111
2106
|
}
|
|
2112
2107
|
}
|
|
@@ -2114,7 +2109,7 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
|
|
|
2114
2109
|
|
|
2115
2110
|
// src/commands/apply.ts
|
|
2116
2111
|
async function cacheHasItems() {
|
|
2117
|
-
const data = await readJson(
|
|
2112
|
+
const data = await readJson(path13.join(getCacheDir(), "manifest.json"));
|
|
2118
2113
|
return Array.isArray(data?.items) && data.items.length > 0;
|
|
2119
2114
|
}
|
|
2120
2115
|
async function runApply(options) {
|
|
@@ -2219,7 +2214,7 @@ async function runCatalogAudit() {
|
|
|
2219
2214
|
|
|
2220
2215
|
// src/commands/clone.ts
|
|
2221
2216
|
import { existsSync as existsSync2 } from "fs";
|
|
2222
|
-
import
|
|
2217
|
+
import path14 from "path";
|
|
2223
2218
|
|
|
2224
2219
|
// src/utils/exec.ts
|
|
2225
2220
|
import { execa } from "execa";
|
|
@@ -2272,9 +2267,9 @@ async function runClone(url, opts = {}) {
|
|
|
2272
2267
|
process.exitCode = 1;
|
|
2273
2268
|
return;
|
|
2274
2269
|
}
|
|
2275
|
-
const target =
|
|
2270
|
+
const target = path14.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
2276
2271
|
if (existsSync2(target)) {
|
|
2277
|
-
log(`\u2022 ${
|
|
2272
|
+
log(`\u2022 ${path14.basename(target)} already present at ${target} \u2014 skipped`);
|
|
2278
2273
|
return;
|
|
2279
2274
|
}
|
|
2280
2275
|
if (opts.dryRun) {
|
|
@@ -2290,399 +2285,12 @@ async function runClone(url, opts = {}) {
|
|
|
2290
2285
|
log(`\u2713 cloned ${url} \u2192 ${target}`);
|
|
2291
2286
|
}
|
|
2292
2287
|
|
|
2293
|
-
// src/commands/
|
|
2294
|
-
import
|
|
2295
|
-
|
|
2296
|
-
var HOOK_ALIASES = {
|
|
2297
|
-
"hook.context": "context"
|
|
2298
|
-
};
|
|
2299
|
-
async function runConfig(key, action) {
|
|
2300
|
-
const hookKey = HOOK_ALIASES[key];
|
|
2301
|
-
if (!hookKey) {
|
|
2302
|
-
throw new Error(
|
|
2303
|
-
`Unknown config key "${key}". Valid keys: ${Object.keys(HOOK_ALIASES).join(", ")}`
|
|
2304
|
-
);
|
|
2305
|
-
}
|
|
2306
|
-
const root = process.cwd();
|
|
2307
|
-
const configPath = path16.join(root, CONFIG_PATH2);
|
|
2308
|
-
const existing = await readJson(configPath);
|
|
2309
|
-
const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
|
|
2310
|
-
cfg.hooks ??= {};
|
|
2311
|
-
cfg.hooks[hookKey] ??= {};
|
|
2312
|
-
if (action === "status") {
|
|
2313
|
-
const enabled = cfg.hooks[hookKey]?.enabled === true;
|
|
2314
|
-
log(`${key}: ${enabled ? "enabled" : "disabled"}`);
|
|
2315
|
-
return;
|
|
2316
|
-
}
|
|
2317
|
-
cfg.hooks[hookKey].enabled = action === "enable";
|
|
2318
|
-
await writeJson(configPath, cfg);
|
|
2319
|
-
log(`${key} ${action}d`);
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
// src/recommender/token-estimate.ts
|
|
2323
|
-
var TOKENS_PER_ITEM = 320;
|
|
2324
|
-
function estimateContextTokens(selectedCount) {
|
|
2325
|
-
return selectedCount * TOKENS_PER_ITEM;
|
|
2326
|
-
}
|
|
2327
|
-
function tokenReductionPct(selected, skipped) {
|
|
2328
|
-
const total = selected + skipped;
|
|
2329
|
-
if (total === 0) return 0;
|
|
2330
|
-
return Math.max(0, Math.round(skipped / total * 100));
|
|
2331
|
-
}
|
|
2332
|
-
|
|
2333
|
-
// src/recommender/explain-recommendation.ts
|
|
2334
|
-
function normalizeRecommendation(input2) {
|
|
2335
|
-
const recommended = (input2.recommended ?? []).map((item) => {
|
|
2336
|
-
const normalizedReasons = item.reasons?.map((reason) => ({
|
|
2337
|
-
code: reason.code ?? "legacy-reason",
|
|
2338
|
-
message: reason.message ?? item.reason ?? "legacy recommendation reason",
|
|
2339
|
-
...reason.signal ? { signal: reason.signal } : {}
|
|
2340
|
-
})) ?? [{ code: "legacy-reason", message: item.reason ?? "legacy recommendation reason" }];
|
|
2341
|
-
return {
|
|
2342
|
-
id: item.id,
|
|
2343
|
-
type: item.type ?? "skill",
|
|
2344
|
-
reason: item.reason ?? normalizedReasons.map((reason) => reason.message).join(", "),
|
|
2345
|
-
reasons: normalizedReasons,
|
|
2346
|
-
selectionMode: item.selectionMode ?? "matched",
|
|
2347
|
-
install: item.install ?? true,
|
|
2348
|
-
tags: item.tags,
|
|
2349
|
-
ecosystem: item.ecosystem,
|
|
2350
|
-
tokenEstimate: item.tokenEstimate
|
|
2351
|
-
};
|
|
2352
|
-
});
|
|
2353
|
-
const skipped = (input2.skipped ?? []).map((item) => ({
|
|
2354
|
-
id: item.id,
|
|
2355
|
-
reason: item.reason ?? "legacy skipped reason",
|
|
2356
|
-
skipReasons: item.skipReasons?.map((reason) => ({
|
|
2357
|
-
code: reason.code ?? "legacy-skip-reason",
|
|
2358
|
-
message: reason.message ?? item.reason ?? "legacy skipped reason",
|
|
2359
|
-
...reason.signal ? { signal: reason.signal } : {}
|
|
2360
|
-
})) ?? [{ code: "legacy-skip-reason", message: item.reason ?? "legacy skipped reason" }]
|
|
2361
|
-
}));
|
|
2362
|
-
return {
|
|
2363
|
-
recommended,
|
|
2364
|
-
skipped,
|
|
2365
|
-
warnings: input2.warnings ?? [],
|
|
2366
|
-
estimatedContextTokens: input2.estimatedContextTokens ?? estimateContextTokens(recommended.length),
|
|
2367
|
-
selectedRules: input2.selectedRules ?? recommended.length,
|
|
2368
|
-
skippedRules: input2.skippedRules ?? skipped.length,
|
|
2369
|
-
estimatedTokenReductionPct: input2.estimatedTokenReductionPct ?? tokenReductionPct(recommended.length, skipped.length)
|
|
2370
|
-
};
|
|
2371
|
-
}
|
|
2372
|
-
function buildRecommendationExplanation(recommendation) {
|
|
2373
|
-
return {
|
|
2374
|
-
selected: recommendation.recommended.map((item) => ({
|
|
2375
|
-
id: item.id,
|
|
2376
|
-
selectionMode: item.selectionMode,
|
|
2377
|
-
reasons: item.reasons.map((reason) => reason.message)
|
|
2378
|
-
})),
|
|
2379
|
-
skipped: recommendation.skipped.map((item) => ({
|
|
2380
|
-
id: item.id,
|
|
2381
|
-
reasons: item.skipReasons.map((reason) => reason.message),
|
|
2382
|
-
reasonDetails: item.skipReasons.map((reason) => ({
|
|
2383
|
-
code: reason.code,
|
|
2384
|
-
message: reason.message,
|
|
2385
|
-
...reason.signal ? { signal: reason.signal } : {}
|
|
2386
|
-
}))
|
|
2387
|
-
})),
|
|
2388
|
-
stats: {
|
|
2389
|
-
selectedRules: recommendation.selectedRules,
|
|
2390
|
-
skippedRules: recommendation.skippedRules,
|
|
2391
|
-
estimatedTokenReductionPct: recommendation.estimatedTokenReductionPct
|
|
2392
|
-
}
|
|
2393
|
-
};
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
|
-
// src/recommender/task-classification.ts
|
|
2397
|
-
var ALL_INTENTS = [
|
|
2398
|
-
"backend",
|
|
2399
|
-
"frontend",
|
|
2400
|
-
"admin-ui",
|
|
2401
|
-
"storefront",
|
|
2402
|
-
"graphql",
|
|
2403
|
-
"database",
|
|
2404
|
-
"auth",
|
|
2405
|
-
"testing",
|
|
2406
|
-
"docs",
|
|
2407
|
-
"monorepo"
|
|
2408
|
-
];
|
|
2409
|
-
var TASK_INTENT_KEYWORDS = {
|
|
2410
|
-
backend: [
|
|
2411
|
-
"api",
|
|
2412
|
-
"endpoint",
|
|
2413
|
-
"controller",
|
|
2414
|
-
"service",
|
|
2415
|
-
"queue",
|
|
2416
|
-
"job",
|
|
2417
|
-
"worker",
|
|
2418
|
-
"cron",
|
|
2419
|
-
"middleware",
|
|
2420
|
-
"resolver",
|
|
2421
|
-
"migration",
|
|
2422
|
-
"seeder",
|
|
2423
|
-
"model",
|
|
2424
|
-
"repository",
|
|
2425
|
-
"handler",
|
|
2426
|
-
"plugin",
|
|
2427
|
-
"webhook",
|
|
2428
|
-
"schedule",
|
|
2429
|
-
"background",
|
|
2430
|
-
"consumer",
|
|
2431
|
-
"producer",
|
|
2432
|
-
"command",
|
|
2433
|
-
"nova resource",
|
|
2434
|
-
"api mutation",
|
|
2435
|
-
"api subscription"
|
|
2436
|
-
],
|
|
2437
|
-
frontend: [
|
|
2438
|
-
"component",
|
|
2439
|
-
"page",
|
|
2440
|
-
"route",
|
|
2441
|
-
"view",
|
|
2442
|
-
"layout",
|
|
2443
|
-
"form",
|
|
2444
|
-
"dashboard",
|
|
2445
|
-
"modal",
|
|
2446
|
-
"navbar",
|
|
2447
|
-
"navigation",
|
|
2448
|
-
"sidebar",
|
|
2449
|
-
"menu",
|
|
2450
|
-
"tailwind",
|
|
2451
|
-
"scss",
|
|
2452
|
-
"style",
|
|
2453
|
-
"theme",
|
|
2454
|
-
"tanstack",
|
|
2455
|
-
"shadcn",
|
|
2456
|
-
"radix",
|
|
2457
|
-
"block",
|
|
2458
|
-
"client component",
|
|
2459
|
-
"server component"
|
|
2460
|
-
],
|
|
2461
|
-
"admin-ui": [
|
|
2462
|
-
"admin",
|
|
2463
|
-
"admin-ui",
|
|
2464
|
-
"admin ui",
|
|
2465
|
-
"backoffice",
|
|
2466
|
-
"back-office",
|
|
2467
|
-
"back office",
|
|
2468
|
-
"nova",
|
|
2469
|
-
"control panel",
|
|
2470
|
-
"wp-admin",
|
|
2471
|
-
"vendure admin"
|
|
2472
|
-
],
|
|
2473
|
-
storefront: [
|
|
2474
|
-
"storefront",
|
|
2475
|
-
"checkout",
|
|
2476
|
-
"cart",
|
|
2477
|
-
"product page",
|
|
2478
|
-
"product listing",
|
|
2479
|
-
"category page",
|
|
2480
|
-
"shop",
|
|
2481
|
-
"ecommerce",
|
|
2482
|
-
"e-commerce",
|
|
2483
|
-
"order page"
|
|
2484
|
-
],
|
|
2485
|
-
graphql: ["graphql", "resolver", "graphql mutation", "graphql subscription", "schema", "codegen"],
|
|
2486
|
-
database: [
|
|
2487
|
-
"database",
|
|
2488
|
-
"migration",
|
|
2489
|
-
"seed",
|
|
2490
|
-
"table",
|
|
2491
|
-
"index",
|
|
2492
|
-
"elasticsearch",
|
|
2493
|
-
"postgres",
|
|
2494
|
-
"mariadb",
|
|
2495
|
-
"mssql",
|
|
2496
|
-
"sql query",
|
|
2497
|
-
"db query"
|
|
2498
|
-
],
|
|
2499
|
-
auth: [
|
|
2500
|
-
"auth",
|
|
2501
|
-
"login",
|
|
2502
|
-
"logout",
|
|
2503
|
-
"oauth",
|
|
2504
|
-
"oidc",
|
|
2505
|
-
"bankid",
|
|
2506
|
-
"azure ad",
|
|
2507
|
-
"session",
|
|
2508
|
-
"jwt",
|
|
2509
|
-
"permission",
|
|
2510
|
-
"rbac",
|
|
2511
|
-
"acl",
|
|
2512
|
-
"guard",
|
|
2513
|
-
"saml"
|
|
2514
|
-
],
|
|
2515
|
-
testing: [
|
|
2516
|
-
"test",
|
|
2517
|
-
"tests",
|
|
2518
|
-
"testing",
|
|
2519
|
-
"spec",
|
|
2520
|
-
"e2e",
|
|
2521
|
-
"unit",
|
|
2522
|
-
"story",
|
|
2523
|
-
"stories",
|
|
2524
|
-
"snapshot",
|
|
2525
|
-
"fixture",
|
|
2526
|
-
"playwright",
|
|
2527
|
-
"cypress",
|
|
2528
|
-
"phpunit",
|
|
2529
|
-
"vitest"
|
|
2530
|
-
],
|
|
2531
|
-
docs: ["doc", "docs", "documentation", "readme", "guide", "tutorial", "changelog"],
|
|
2532
|
-
monorepo: [
|
|
2533
|
-
"lib",
|
|
2534
|
-
"library",
|
|
2535
|
-
"package",
|
|
2536
|
-
"workspace",
|
|
2537
|
-
"shared",
|
|
2538
|
-
"monorepo",
|
|
2539
|
-
"nx",
|
|
2540
|
-
"turbo",
|
|
2541
|
-
"pnpm workspace",
|
|
2542
|
-
"yarn workspace"
|
|
2543
|
-
]
|
|
2544
|
-
};
|
|
2545
|
-
function normalizeTaskForMatching(task) {
|
|
2546
|
-
return ` ${task.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim()} `;
|
|
2547
|
-
}
|
|
2548
|
-
function classifyTaskIntents(task) {
|
|
2549
|
-
const t = normalizeTaskForMatching(task);
|
|
2550
|
-
const intents = /* @__PURE__ */ new Set();
|
|
2551
|
-
for (const intent of ALL_INTENTS) {
|
|
2552
|
-
const keywords = TASK_INTENT_KEYWORDS[intent];
|
|
2553
|
-
for (const kw of keywords) {
|
|
2554
|
-
const needle = ` ${kw} `;
|
|
2555
|
-
if (t.includes(needle)) {
|
|
2556
|
-
intents.add(intent);
|
|
2557
|
-
break;
|
|
2558
|
-
}
|
|
2559
|
-
}
|
|
2560
|
-
}
|
|
2561
|
-
return intents;
|
|
2562
|
-
}
|
|
2563
|
-
function computeRuleIntents(rule) {
|
|
2564
|
-
const intents = /* @__PURE__ */ new Set();
|
|
2565
|
-
const tags = new Set((rule.tags ?? []).map((t) => t.toLowerCase()));
|
|
2566
|
-
const eco = rule.ecosystem;
|
|
2567
|
-
if (!eco && tags.size === 0) return intents;
|
|
2568
|
-
const isTestingRule = tags.has("playwright") || tags.has("phpunit") || tags.has("testing-library") || tags.has("storybook") || tags.has("testing");
|
|
2569
|
-
if (isTestingRule) {
|
|
2570
|
-
intents.add("testing");
|
|
2571
|
-
return intents;
|
|
2572
|
-
}
|
|
2573
|
-
if (eco === "laravel" || eco === "nestjs" || eco === "dotnet") {
|
|
2574
|
-
intents.add("backend");
|
|
2575
|
-
}
|
|
2576
|
-
if (eco === "vendure") {
|
|
2577
|
-
intents.add("backend");
|
|
2578
|
-
intents.add("admin-ui");
|
|
2579
|
-
}
|
|
2580
|
-
if (eco === "wordpress") {
|
|
2581
|
-
intents.add("backend");
|
|
2582
|
-
intents.add("frontend");
|
|
2583
|
-
intents.add("admin-ui");
|
|
2584
|
-
}
|
|
2585
|
-
if (eco === "nextjs" || eco === "react" || eco === "vue") {
|
|
2586
|
-
intents.add("frontend");
|
|
2587
|
-
intents.add("admin-ui");
|
|
2588
|
-
intents.add("storefront");
|
|
2589
|
-
}
|
|
2590
|
-
if (eco === "tailwind" || eco === "vite") {
|
|
2591
|
-
intents.add("frontend");
|
|
2592
|
-
}
|
|
2593
|
-
if (eco === "nx" || eco === "turbo") {
|
|
2594
|
-
intents.add("monorepo");
|
|
2595
|
-
}
|
|
2596
|
-
if (tags.has("backend")) intents.add("backend");
|
|
2597
|
-
if (tags.has("frontend")) intents.add("frontend");
|
|
2598
|
-
if (tags.has("graphql")) intents.add("graphql");
|
|
2599
|
-
if (tags.has("laravel-nova")) intents.add("admin-ui");
|
|
2600
|
-
if (tags.has("oidc") || tags.has("azure-ad") || tags.has("bankid")) intents.add("auth");
|
|
2601
|
-
if (tags.has("postgresql") || tags.has("mariadb") || tags.has("mssql") || tags.has("elasticsearch")) {
|
|
2602
|
-
intents.add("database");
|
|
2603
|
-
}
|
|
2604
|
-
if (tags.has("nx21") || tags.has("turbo") || tags.has("yarn4") || tags.has("pnpm89")) {
|
|
2605
|
-
intents.add("monorepo");
|
|
2606
|
-
}
|
|
2607
|
-
return intents;
|
|
2608
|
-
}
|
|
2609
|
-
|
|
2610
|
-
// src/recommender/rule-selection.ts
|
|
2611
|
-
function evidenceCount(rule) {
|
|
2612
|
-
return rule.reasons.filter((r) => r.code !== "default-baseline").length;
|
|
2613
|
-
}
|
|
2614
|
-
function isRoleOnly(rule) {
|
|
2615
|
-
const codes = rule.reasons.map((r) => r.code).filter((c) => c !== "default-baseline");
|
|
2616
|
-
return codes.length > 0 && codes.every((c) => c === "repo-role-match");
|
|
2617
|
-
}
|
|
2618
|
-
var DEFAULT_CONTEXT_TOKEN_BUDGET = 12e3;
|
|
2619
|
-
function pickTaskRelevantRules(recommendation, task, taskIntents = /* @__PURE__ */ new Set(), opts = {}) {
|
|
2620
|
-
const recommended = recommendation?.recommended ?? [];
|
|
2621
|
-
return applyTokenBudget(selectRules(recommended, task, taskIntents), opts.tokenBudget);
|
|
2622
|
-
}
|
|
2623
|
-
function applyTokenBudget(rules, budget) {
|
|
2624
|
-
if (!budget || budget <= 0) return rules;
|
|
2625
|
-
const total = rules.reduce((sum, r) => sum + (r.tokenEstimate ?? 0), 0);
|
|
2626
|
-
if (total <= budget) return rules;
|
|
2627
|
-
const keep = /* @__PURE__ */ new Set();
|
|
2628
|
-
let used = 0;
|
|
2629
|
-
for (const r of rules) {
|
|
2630
|
-
if (r.selectionMode === "baseline") {
|
|
2631
|
-
keep.add(r.id);
|
|
2632
|
-
used += r.tokenEstimate ?? 0;
|
|
2633
|
-
}
|
|
2634
|
-
}
|
|
2635
|
-
const matched = rules.filter((r) => r.selectionMode !== "baseline").sort((a, b) => evidenceCount(b) - evidenceCount(a) || a.id.localeCompare(b.id));
|
|
2636
|
-
for (const r of matched) {
|
|
2637
|
-
const est = r.tokenEstimate ?? 0;
|
|
2638
|
-
if (used + est <= budget) {
|
|
2639
|
-
keep.add(r.id);
|
|
2640
|
-
used += est;
|
|
2641
|
-
}
|
|
2642
|
-
}
|
|
2643
|
-
return rules.filter((r) => keep.has(r.id));
|
|
2644
|
-
}
|
|
2645
|
-
function selectRules(recommended, task, taskIntents) {
|
|
2646
|
-
if (!task) return recommended;
|
|
2647
|
-
if (taskIntents.size > 0) {
|
|
2648
|
-
const intentMatches = recommended.filter((rule) => {
|
|
2649
|
-
if (rule.selectionMode === "baseline") return false;
|
|
2650
|
-
const ruleIntents = computeRuleIntents(rule);
|
|
2651
|
-
if (ruleIntents.size === 0) return false;
|
|
2652
|
-
for (const ti of taskIntents) {
|
|
2653
|
-
if (ruleIntents.has(ti)) return true;
|
|
2654
|
-
}
|
|
2655
|
-
return false;
|
|
2656
|
-
});
|
|
2657
|
-
if (intentMatches.length > 0) return intentMatches;
|
|
2658
|
-
}
|
|
2659
|
-
const tokens = task.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
|
|
2660
|
-
const tokenMatches = recommended.filter((rule) => {
|
|
2661
|
-
if (rule.selectionMode === "baseline") return false;
|
|
2662
|
-
const corpus = [
|
|
2663
|
-
rule.id,
|
|
2664
|
-
rule.ecosystem ?? "",
|
|
2665
|
-
...rule.tags ?? [],
|
|
2666
|
-
rule.reason ?? "",
|
|
2667
|
-
...rule.reasons.map((r) => r.message)
|
|
2668
|
-
].join(" ").toLowerCase();
|
|
2669
|
-
return tokens.some((token) => corpus.includes(token));
|
|
2670
|
-
});
|
|
2671
|
-
if (tokenMatches.length > 0) return tokenMatches;
|
|
2672
|
-
const taskWantsTesting = taskIntents.has("testing");
|
|
2673
|
-
const capped = recommended.filter((rule) => {
|
|
2674
|
-
if (rule.selectionMode === "baseline") return false;
|
|
2675
|
-
if (isRoleOnly(rule)) return false;
|
|
2676
|
-
if (taskWantsTesting) return true;
|
|
2677
|
-
const ruleIntents = computeRuleIntents(rule);
|
|
2678
|
-
const isTestingOnly = ruleIntents.size > 0 && [...ruleIntents].every((i) => i === "testing");
|
|
2679
|
-
return !isTestingOnly;
|
|
2680
|
-
});
|
|
2681
|
-
return capped.slice(0, 8);
|
|
2682
|
-
}
|
|
2288
|
+
// src/commands/doctor.ts
|
|
2289
|
+
import path19 from "path";
|
|
2290
|
+
import fs15 from "fs-extra";
|
|
2683
2291
|
|
|
2684
2292
|
// src/scanner/scan-project.ts
|
|
2685
|
-
import
|
|
2293
|
+
import path18 from "path";
|
|
2686
2294
|
|
|
2687
2295
|
// src/utils/audit-checks.ts
|
|
2688
2296
|
function isRecord(v) {
|
|
@@ -2709,7 +2317,7 @@ function compareVersions(a, b) {
|
|
|
2709
2317
|
}
|
|
2710
2318
|
|
|
2711
2319
|
// src/scanner/detect-package-manager.ts
|
|
2712
|
-
import
|
|
2320
|
+
import path15 from "path";
|
|
2713
2321
|
import fs14 from "fs-extra";
|
|
2714
2322
|
function detectPackageManager(root, packageManagerField) {
|
|
2715
2323
|
const field = String(packageManagerField ?? "").trim();
|
|
@@ -2728,9 +2336,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
2728
2336
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
2729
2337
|
return "unknown";
|
|
2730
2338
|
}
|
|
2731
|
-
if (fs14.existsSync(
|
|
2732
|
-
if (fs14.existsSync(
|
|
2733
|
-
if (fs14.existsSync(
|
|
2339
|
+
if (fs14.existsSync(path15.join(root, "yarn.lock"))) return "yarn";
|
|
2340
|
+
if (fs14.existsSync(path15.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
2341
|
+
if (fs14.existsSync(path15.join(root, "package-lock.json"))) return "npm";
|
|
2734
2342
|
return "unknown";
|
|
2735
2343
|
}
|
|
2736
2344
|
|
|
@@ -2866,6 +2474,11 @@ var STACK_RULES = [
|
|
|
2866
2474
|
stack: ["backend", "graphql"],
|
|
2867
2475
|
any: [fileEndsWith(".graphql"), fileEndsWith("schema.graphql")]
|
|
2868
2476
|
},
|
|
2477
|
+
// PHP (Composer-managed) — broad language signal so php-tooling catalog items
|
|
2478
|
+
// (e.g. the php-reviewer agent gated on { stack: 'php' }) are recommendable on any
|
|
2479
|
+
// Composer project. Placed before laravel/wordpress so 'php' precedes the
|
|
2480
|
+
// framework-specific stacks in the backend bucket.
|
|
2481
|
+
{ stack: ["backend", "php"], any: [fileEndsWith("composer.json")] },
|
|
2869
2482
|
{ stack: ["backend", "laravel"], any: [dep("laravel/framework")] },
|
|
2870
2483
|
{ stack: ["backend", "laravel"], any: [fileIncludes("app/Providers/"), fileIncludes("routes/")] },
|
|
2871
2484
|
{ stack: ["backend", "wordpress"], any: [fileEndsWith("wp-config.php"), dep("roots/wordpress")] },
|
|
@@ -2927,7 +2540,7 @@ function runDetection(ctx, rules = STACK_RULES) {
|
|
|
2927
2540
|
}
|
|
2928
2541
|
|
|
2929
2542
|
// src/scanner/detection.ts
|
|
2930
|
-
import
|
|
2543
|
+
import path16 from "path";
|
|
2931
2544
|
var UNSUPPORTED_MARKERS = {
|
|
2932
2545
|
"requirements.txt": "python",
|
|
2933
2546
|
"pyproject.toml": "python",
|
|
@@ -2981,68 +2594,14 @@ function finalizeRoles(registryRoles, deps, files) {
|
|
|
2981
2594
|
function collectUnsupportedSignals(files) {
|
|
2982
2595
|
return [
|
|
2983
2596
|
...new Set(
|
|
2984
|
-
files.map((f) => UNSUPPORTED_MARKERS[
|
|
2597
|
+
files.map((f) => UNSUPPORTED_MARKERS[path16.basename(f)]).filter((s) => Boolean(s))
|
|
2985
2598
|
)
|
|
2986
2599
|
].sort();
|
|
2987
2600
|
}
|
|
2988
2601
|
|
|
2989
2602
|
// src/scanner/render.ts
|
|
2990
2603
|
import { readFile } from "fs/promises";
|
|
2991
|
-
import
|
|
2992
|
-
|
|
2993
|
-
// src/scanner/role-labels.ts
|
|
2994
|
-
var ROLE_LABELS = {
|
|
2995
|
-
"next-app": "a Next.js app",
|
|
2996
|
-
"react-app": "a React app",
|
|
2997
|
-
"vite-app": "a Vite app",
|
|
2998
|
-
"react-router-app": "a React Router app",
|
|
2999
|
-
"sanity-studio": "a Sanity Studio",
|
|
3000
|
-
"strapi-app": "a Strapi app",
|
|
3001
|
-
"expo-app": "an Expo app",
|
|
3002
|
-
"vendure-app": "a Vendure server",
|
|
3003
|
-
"vendure-plugin": "a Vendure plugin",
|
|
3004
|
-
"nestjs-api": "a NestJS API",
|
|
3005
|
-
"graphql-api": "a GraphQL API",
|
|
3006
|
-
"nx-monorepo": "an Nx monorepo",
|
|
3007
|
-
"turbo-monorepo": "a Turborepo monorepo",
|
|
3008
|
-
"laravel-app": "a Laravel app",
|
|
3009
|
-
"laravel-nova-app": "a Laravel Nova app",
|
|
3010
|
-
"dotnet-service": "a .NET service",
|
|
3011
|
-
"express-service": "an Express service",
|
|
3012
|
-
"wordpress-bedrock-site": "a WordPress (Bedrock) site",
|
|
3013
|
-
"wordpress-vanilla-site": "a WordPress site",
|
|
3014
|
-
"wordpress-site": "a WordPress site"
|
|
3015
|
-
};
|
|
3016
|
-
function article(word) {
|
|
3017
|
-
return /^[aeiou]/i.test(word) ? "an" : "a";
|
|
3018
|
-
}
|
|
3019
|
-
function friendlyRole(role) {
|
|
3020
|
-
const known = ROLE_LABELS[role];
|
|
3021
|
-
if (known) return known;
|
|
3022
|
-
const words = role.replace(/[-_]+/g, " ").trim();
|
|
3023
|
-
return words ? `${article(words)} ${words}` : "a project";
|
|
3024
|
-
}
|
|
3025
|
-
function joinRoles(labels) {
|
|
3026
|
-
if (labels.length === 0) return "";
|
|
3027
|
-
if (labels.length === 1) return labels[0];
|
|
3028
|
-
return `${labels.slice(0, -1).join(", ")} and ${labels[labels.length - 1]}`;
|
|
3029
|
-
}
|
|
3030
|
-
function describeRepo(context) {
|
|
3031
|
-
const labels = context.repoRoles.map(friendlyRole);
|
|
3032
|
-
const roleText = joinRoles(labels);
|
|
3033
|
-
if (context.detectionStatus === "unknown") {
|
|
3034
|
-
const markers = context.unsupportedSignals.join(", ");
|
|
3035
|
-
const detail = markers ? ` (I see ${markers})` : "";
|
|
3036
|
-
return `I couldn't fully recognise this stack${detail}, so I'll apply the general workflow and security guidance rather than framework-specific help.`;
|
|
3037
|
-
}
|
|
3038
|
-
const base = roleText ? `This looks like ${roleText}, using ${context.packageManager}.` : `I recognised this project's tooling (${context.packageManager}) but not a specific framework.`;
|
|
3039
|
-
if (context.detectionStatus === "partial" && context.unsupportedSignals.length > 0) {
|
|
3040
|
-
return `${base} I also see ${context.unsupportedSignals.join(", ")}, which haus doesn't fully support \u2014 guidance covers the recognised parts.`;
|
|
3041
|
-
}
|
|
3042
|
-
return base;
|
|
3043
|
-
}
|
|
3044
|
-
|
|
3045
|
-
// src/scanner/render.ts
|
|
2604
|
+
import path17 from "path";
|
|
3046
2605
|
async function buildContentBlob(root, files) {
|
|
3047
2606
|
const candidates = files.filter(
|
|
3048
2607
|
(f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
|
|
@@ -3050,24 +2609,13 @@ async function buildContentBlob(root, files) {
|
|
|
3050
2609
|
const slice = candidates.slice(0, 300);
|
|
3051
2610
|
const parts = await mapWithConcurrency(slice, async (rel) => {
|
|
3052
2611
|
try {
|
|
3053
|
-
return await readFile(
|
|
2612
|
+
return await readFile(path17.join(root, rel), "utf8");
|
|
3054
2613
|
} catch {
|
|
3055
2614
|
return "";
|
|
3056
2615
|
}
|
|
3057
2616
|
});
|
|
3058
2617
|
return parts.join("\n");
|
|
3059
2618
|
}
|
|
3060
|
-
function renderSummary(context) {
|
|
3061
|
-
return `# Repo summary
|
|
3062
|
-
|
|
3063
|
-
${describeRepo(context)}
|
|
3064
|
-
|
|
3065
|
-
- Repo: ${context.repoName}
|
|
3066
|
-
- Package manager: ${context.packageManager}
|
|
3067
|
-
- Roles: ${context.repoRoles.join(", ") || "unknown"}
|
|
3068
|
-
- Generated: ${context.generatedAt}
|
|
3069
|
-
`;
|
|
3070
|
-
}
|
|
3071
2619
|
|
|
3072
2620
|
// src/scanner/write-sources-report.ts
|
|
3073
2621
|
function buildSourcesReport(items) {
|
|
@@ -3148,8 +2696,8 @@ var SAFE_FILES = [
|
|
|
3148
2696
|
"Gemfile"
|
|
3149
2697
|
];
|
|
3150
2698
|
async function scanProject(root) {
|
|
3151
|
-
const pkg = await readJson(
|
|
3152
|
-
const composer = await readJson(
|
|
2699
|
+
const pkg = await readJson(path18.join(root, "package.json"));
|
|
2700
|
+
const composer = await readJson(path18.join(root, "composer.json"));
|
|
3153
2701
|
const files = await listFiles(root, SAFE_FILES);
|
|
3154
2702
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
3155
2703
|
const deps = dependencySet(pkg, composer);
|
|
@@ -3178,7 +2726,7 @@ async function scanProject(root) {
|
|
|
3178
2726
|
const context = {
|
|
3179
2727
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3180
2728
|
root,
|
|
3181
|
-
repoName: String(pkg?.name ??
|
|
2729
|
+
repoName: String(pkg?.name ?? path18.basename(root)),
|
|
3182
2730
|
packageManager,
|
|
3183
2731
|
repoRoles: roles,
|
|
3184
2732
|
detectedStacks: stacks,
|
|
@@ -3202,74 +2750,6 @@ async function readContextOrScan(root) {
|
|
|
3202
2750
|
return scan;
|
|
3203
2751
|
}
|
|
3204
2752
|
|
|
3205
|
-
// src/security/secret-patterns.ts
|
|
3206
|
-
var SECRET_PATTERNS = [
|
|
3207
|
-
/api[_-]?key\s*[:=]\s*\S+/i,
|
|
3208
|
-
/token\s*[:=]\s*\S+/i,
|
|
3209
|
-
/password\s*[:=]\s*\S+/i
|
|
3210
|
-
];
|
|
3211
|
-
|
|
3212
|
-
// src/security/redact-sensitive.ts
|
|
3213
|
-
function redactSensitive(input2) {
|
|
3214
|
-
return SECRET_PATTERNS.reduce((acc, pattern) => acc.replace(pattern, "[REDACTED]"), input2);
|
|
3215
|
-
}
|
|
3216
|
-
|
|
3217
|
-
// src/commands/context.ts
|
|
3218
|
-
async function runContext(options) {
|
|
3219
|
-
const root = process.cwd();
|
|
3220
|
-
if (options.fromHook && !await isHookEnabled(root, "context")) {
|
|
3221
|
-
return;
|
|
3222
|
-
}
|
|
3223
|
-
const context = await readContextOrScan(root);
|
|
3224
|
-
const summary = renderSummary(context);
|
|
3225
|
-
const recommendationRaw = await readJson(hausPath(root, "recommendation.json"));
|
|
3226
|
-
const recommendation = recommendationRaw ? normalizeRecommendation(recommendationRaw) : void 0;
|
|
3227
|
-
const taskIntents = options.task ? classifyTaskIntents(options.task) : /* @__PURE__ */ new Set();
|
|
3228
|
-
const selected = pickTaskRelevantRules(recommendation, options.task, taskIntents, {
|
|
3229
|
-
tokenBudget: DEFAULT_CONTEXT_TOKEN_BUDGET
|
|
3230
|
-
});
|
|
3231
|
-
const payload = {
|
|
3232
|
-
task: options.task ?? "not provided",
|
|
3233
|
-
taskIntents: [...taskIntents].sort(),
|
|
3234
|
-
roles: context.repoRoles,
|
|
3235
|
-
selectedRules: selected.map((x) => ({
|
|
3236
|
-
id: x.id,
|
|
3237
|
-
selectionMode: x.selectionMode,
|
|
3238
|
-
reasons: x.reasons.map((reason) => reason.message),
|
|
3239
|
-
...options.verbose ? { signals: x.reasons.map((r) => r.signal).filter(Boolean) } : {}
|
|
3240
|
-
})),
|
|
3241
|
-
skippedCount: recommendation?.skippedRules ?? 0,
|
|
3242
|
-
estimatedTokenReductionPct: recommendation?.estimatedTokenReductionPct ?? 0
|
|
3243
|
-
};
|
|
3244
|
-
if (options.json) {
|
|
3245
|
-
log(JSON.stringify(payload, null, 2));
|
|
3246
|
-
return;
|
|
3247
|
-
}
|
|
3248
|
-
const lines = [
|
|
3249
|
-
"# Haus Context",
|
|
3250
|
-
`Task: ${payload.task}`,
|
|
3251
|
-
`Task intents: ${payload.taskIntents.join(", ") || "(none classified)"}`,
|
|
3252
|
-
`Roles: ${payload.roles.join(", ") || "unknown"}`,
|
|
3253
|
-
`Selected rules: ${payload.selectedRules.length}`,
|
|
3254
|
-
`Skipped rules: ${payload.skippedCount}`,
|
|
3255
|
-
`Estimated token reduction: ${payload.estimatedTokenReductionPct}%`,
|
|
3256
|
-
"Use minimal context.",
|
|
3257
|
-
...payload.selectedRules.flatMap((rule) => {
|
|
3258
|
-
const reasonLine = `- ${rule.id}: ${rule.reasons.join(", ")}`;
|
|
3259
|
-
if (!options.verbose) return [reasonLine];
|
|
3260
|
-
const signals = (rule.signals ?? []).map((s) => ` \u2022 ${s}`);
|
|
3261
|
-
return [reasonLine, ...signals];
|
|
3262
|
-
}),
|
|
3263
|
-
summary
|
|
3264
|
-
];
|
|
3265
|
-
const text = redactSensitive(lines.join("\n"));
|
|
3266
|
-
log(options.fromHook ? text.slice(0, 3e3) : text);
|
|
3267
|
-
}
|
|
3268
|
-
|
|
3269
|
-
// src/commands/doctor.ts
|
|
3270
|
-
import path21 from "path";
|
|
3271
|
-
import fs15 from "fs-extra";
|
|
3272
|
-
|
|
3273
2753
|
// src/update/npm-version.ts
|
|
3274
2754
|
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
3275
2755
|
async function fetchNpmVersionStatus(currentVersion) {
|
|
@@ -3343,12 +2823,7 @@ async function runDoctor(options) {
|
|
|
3343
2823
|
} else {
|
|
3344
2824
|
ok(`- HOOKS OK: ${hooks.message}`);
|
|
3345
2825
|
}
|
|
3346
|
-
const
|
|
3347
|
-
for (const key of gatedHooks) {
|
|
3348
|
-
const enabled = await isHookEnabled(root, key);
|
|
3349
|
-
ok(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
|
|
3350
|
-
}
|
|
3351
|
-
const rootClaudeMdPath = path21.join(root, "CLAUDE.md");
|
|
2826
|
+
const rootClaudeMdPath = path19.join(root, "CLAUDE.md");
|
|
3352
2827
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
3353
2828
|
if (!rootClaudeMdContent) {
|
|
3354
2829
|
flag(
|
|
@@ -3410,8 +2885,8 @@ async function runDoctor(options) {
|
|
|
3410
2885
|
"haus apply --write --force"
|
|
3411
2886
|
);
|
|
3412
2887
|
} else {
|
|
3413
|
-
const cachePath =
|
|
3414
|
-
const bundledPath =
|
|
2888
|
+
const cachePath = path19.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
2889
|
+
const bundledPath = path19.join(
|
|
3415
2890
|
packageRoot(),
|
|
3416
2891
|
"library",
|
|
3417
2892
|
"global",
|
|
@@ -3477,7 +2952,7 @@ async function runDoctor(options) {
|
|
|
3477
2952
|
ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
|
|
3478
2953
|
}
|
|
3479
2954
|
}
|
|
3480
|
-
const pkgJson = await readJson(
|
|
2955
|
+
const pkgJson = await readJson(path19.join(packageRoot(), "package.json"));
|
|
3481
2956
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
3482
2957
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
3483
2958
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -3541,6 +3016,80 @@ function formatRecommendationHuman(rec) {
|
|
|
3541
3016
|
return lines.join("\n");
|
|
3542
3017
|
}
|
|
3543
3018
|
|
|
3019
|
+
// src/recommender/token-estimate.ts
|
|
3020
|
+
var TOKENS_PER_ITEM = 320;
|
|
3021
|
+
function estimateContextTokens(selectedCount) {
|
|
3022
|
+
return selectedCount * TOKENS_PER_ITEM;
|
|
3023
|
+
}
|
|
3024
|
+
function tokenReductionPct(selected, skipped) {
|
|
3025
|
+
const total = selected + skipped;
|
|
3026
|
+
if (total === 0) return 0;
|
|
3027
|
+
return Math.max(0, Math.round(skipped / total * 100));
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
// src/recommender/explain-recommendation.ts
|
|
3031
|
+
function normalizeRecommendation(input2) {
|
|
3032
|
+
const recommended = (input2.recommended ?? []).map((item) => {
|
|
3033
|
+
const normalizedReasons = item.reasons?.map((reason) => ({
|
|
3034
|
+
code: reason.code ?? "legacy-reason",
|
|
3035
|
+
message: reason.message ?? item.reason ?? "legacy recommendation reason",
|
|
3036
|
+
...reason.signal ? { signal: reason.signal } : {}
|
|
3037
|
+
})) ?? [{ code: "legacy-reason", message: item.reason ?? "legacy recommendation reason" }];
|
|
3038
|
+
return {
|
|
3039
|
+
id: item.id,
|
|
3040
|
+
type: item.type ?? "skill",
|
|
3041
|
+
reason: item.reason ?? normalizedReasons.map((reason) => reason.message).join(", "),
|
|
3042
|
+
reasons: normalizedReasons,
|
|
3043
|
+
selectionMode: item.selectionMode ?? "matched",
|
|
3044
|
+
install: item.install ?? true,
|
|
3045
|
+
tags: item.tags,
|
|
3046
|
+
ecosystem: item.ecosystem,
|
|
3047
|
+
tokenEstimate: item.tokenEstimate
|
|
3048
|
+
};
|
|
3049
|
+
});
|
|
3050
|
+
const skipped = (input2.skipped ?? []).map((item) => ({
|
|
3051
|
+
id: item.id,
|
|
3052
|
+
reason: item.reason ?? "legacy skipped reason",
|
|
3053
|
+
skipReasons: item.skipReasons?.map((reason) => ({
|
|
3054
|
+
code: reason.code ?? "legacy-skip-reason",
|
|
3055
|
+
message: reason.message ?? item.reason ?? "legacy skipped reason",
|
|
3056
|
+
...reason.signal ? { signal: reason.signal } : {}
|
|
3057
|
+
})) ?? [{ code: "legacy-skip-reason", message: item.reason ?? "legacy skipped reason" }]
|
|
3058
|
+
}));
|
|
3059
|
+
return {
|
|
3060
|
+
recommended,
|
|
3061
|
+
skipped,
|
|
3062
|
+
warnings: input2.warnings ?? [],
|
|
3063
|
+
estimatedContextTokens: input2.estimatedContextTokens ?? estimateContextTokens(recommended.length),
|
|
3064
|
+
selectedRules: input2.selectedRules ?? recommended.length,
|
|
3065
|
+
skippedRules: input2.skippedRules ?? skipped.length,
|
|
3066
|
+
estimatedTokenReductionPct: input2.estimatedTokenReductionPct ?? tokenReductionPct(recommended.length, skipped.length)
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
function buildRecommendationExplanation(recommendation) {
|
|
3070
|
+
return {
|
|
3071
|
+
selected: recommendation.recommended.map((item) => ({
|
|
3072
|
+
id: item.id,
|
|
3073
|
+
selectionMode: item.selectionMode,
|
|
3074
|
+
reasons: item.reasons.map((reason) => reason.message)
|
|
3075
|
+
})),
|
|
3076
|
+
skipped: recommendation.skipped.map((item) => ({
|
|
3077
|
+
id: item.id,
|
|
3078
|
+
reasons: item.skipReasons.map((reason) => reason.message),
|
|
3079
|
+
reasonDetails: item.skipReasons.map((reason) => ({
|
|
3080
|
+
code: reason.code,
|
|
3081
|
+
message: reason.message,
|
|
3082
|
+
...reason.signal ? { signal: reason.signal } : {}
|
|
3083
|
+
}))
|
|
3084
|
+
})),
|
|
3085
|
+
stats: {
|
|
3086
|
+
selectedRules: recommendation.selectedRules,
|
|
3087
|
+
skippedRules: recommendation.skippedRules,
|
|
3088
|
+
estimatedTokenReductionPct: recommendation.estimatedTokenReductionPct
|
|
3089
|
+
}
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3544
3093
|
// src/commands/explain-recommendation.ts
|
|
3545
3094
|
async function runExplainRecommendation(options) {
|
|
3546
3095
|
const root = process.cwd();
|
|
@@ -3620,7 +3169,7 @@ async function runGuard(kind, _options) {
|
|
|
3620
3169
|
}
|
|
3621
3170
|
|
|
3622
3171
|
// src/commands/init.ts
|
|
3623
|
-
import
|
|
3172
|
+
import path20 from "path";
|
|
3624
3173
|
import fs16 from "fs-extra";
|
|
3625
3174
|
|
|
3626
3175
|
// src/utils/prompts.ts
|
|
@@ -3968,7 +3517,7 @@ async function runSetupProject(options) {
|
|
|
3968
3517
|
// src/commands/init.ts
|
|
3969
3518
|
async function runInit(options) {
|
|
3970
3519
|
const root = process.cwd();
|
|
3971
|
-
const hausDir =
|
|
3520
|
+
const hausDir = path20.join(root, ".haus-workflow");
|
|
3972
3521
|
const alreadyInit = await fs16.pathExists(hausDir);
|
|
3973
3522
|
if (alreadyInit) {
|
|
3974
3523
|
log("Haus AI already initialized in this project.");
|
|
@@ -3981,7 +3530,7 @@ async function runInit(options) {
|
|
|
3981
3530
|
|
|
3982
3531
|
// src/install/apply.ts
|
|
3983
3532
|
import crypto2 from "crypto";
|
|
3984
|
-
import
|
|
3533
|
+
import path21 from "path";
|
|
3985
3534
|
import fs17 from "fs-extra";
|
|
3986
3535
|
|
|
3987
3536
|
// src/install/header.ts
|
|
@@ -4070,7 +3619,7 @@ function hashContent(content2) {
|
|
|
4070
3619
|
}
|
|
4071
3620
|
function sourceVersion() {
|
|
4072
3621
|
try {
|
|
4073
|
-
const pkgPath =
|
|
3622
|
+
const pkgPath = path21.join(packageRoot(), "package.json");
|
|
4074
3623
|
const pkg = JSON.parse(fs17.readFileSync(pkgPath, "utf8"));
|
|
4075
3624
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
4076
3625
|
} catch {
|
|
@@ -4078,32 +3627,32 @@ function sourceVersion() {
|
|
|
4078
3627
|
}
|
|
4079
3628
|
}
|
|
4080
3629
|
function globalSrcDir() {
|
|
4081
|
-
return
|
|
3630
|
+
return path21.join(packageRoot(), "library", "global");
|
|
4082
3631
|
}
|
|
4083
3632
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
4084
3633
|
const entries = [];
|
|
4085
|
-
const skillsDir =
|
|
3634
|
+
const skillsDir = path21.join(srcDir, "skills");
|
|
4086
3635
|
if (fs17.pathExistsSync(skillsDir)) {
|
|
4087
3636
|
for (const skillName of fs17.readdirSync(skillsDir)) {
|
|
4088
|
-
const skillFile =
|
|
3637
|
+
const skillFile = path21.join(skillsDir, skillName, "SKILL.md");
|
|
4089
3638
|
if (fs17.pathExistsSync(skillFile)) {
|
|
4090
3639
|
entries.push({
|
|
4091
3640
|
stableId: `skill.${skillName}`,
|
|
4092
|
-
srcRelPath:
|
|
4093
|
-
destPath:
|
|
3641
|
+
srcRelPath: path21.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
3642
|
+
destPath: path21.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
4094
3643
|
});
|
|
4095
3644
|
}
|
|
4096
3645
|
}
|
|
4097
3646
|
}
|
|
4098
|
-
const commandsDir =
|
|
3647
|
+
const commandsDir = path21.join(srcDir, "commands");
|
|
4099
3648
|
if (fs17.pathExistsSync(commandsDir)) {
|
|
4100
3649
|
for (const fileName of fs17.readdirSync(commandsDir)) {
|
|
4101
3650
|
if (!fileName.endsWith(".md")) continue;
|
|
4102
3651
|
const commandName = fileName.slice(0, -".md".length);
|
|
4103
3652
|
entries.push({
|
|
4104
3653
|
stableId: `command.${commandName}`,
|
|
4105
|
-
srcRelPath:
|
|
4106
|
-
destPath:
|
|
3654
|
+
srcRelPath: path21.join("library", "global", "commands", fileName),
|
|
3655
|
+
destPath: path21.join(claudeDir, "commands", fileName)
|
|
4107
3656
|
});
|
|
4108
3657
|
}
|
|
4109
3658
|
}
|
|
@@ -4127,7 +3676,7 @@ async function applyInstall(options = {}) {
|
|
|
4127
3676
|
};
|
|
4128
3677
|
const manifestFiles = [];
|
|
4129
3678
|
for (const entry of sourceFiles) {
|
|
4130
|
-
const srcPath =
|
|
3679
|
+
const srcPath = path21.join(packageRoot(), entry.srcRelPath);
|
|
4131
3680
|
const rawContent = await readText(srcPath);
|
|
4132
3681
|
if (rawContent === void 0) {
|
|
4133
3682
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -4183,7 +3732,7 @@ async function applyInstall(options = {}) {
|
|
|
4183
3732
|
schemaVersion: SCHEMA_VERSION2
|
|
4184
3733
|
});
|
|
4185
3734
|
}
|
|
4186
|
-
const fragmentPath =
|
|
3735
|
+
const fragmentPath = path21.join(srcDir, "settings-fragments", "hooks.json");
|
|
4187
3736
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
4188
3737
|
const settings = await readSettings();
|
|
4189
3738
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -4321,12 +3870,12 @@ async function runScan(options) {
|
|
|
4321
3870
|
}
|
|
4322
3871
|
|
|
4323
3872
|
// src/commands/undo.ts
|
|
4324
|
-
import
|
|
3873
|
+
import path22 from "path";
|
|
4325
3874
|
import fs18 from "fs-extra";
|
|
4326
3875
|
|
|
4327
3876
|
// src/claude/managed-paths.ts
|
|
4328
3877
|
var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md", "commands/haus-doctor.md"];
|
|
4329
|
-
var PROJECT_MANAGED_HAUS_REL = ["haus.lock.json"
|
|
3878
|
+
var PROJECT_MANAGED_HAUS_REL = ["haus.lock.json"];
|
|
4330
3879
|
function coreManagedAbsolutePaths(root) {
|
|
4331
3880
|
const claude = PROJECT_MANAGED_CLAUDE_REL.map((rel) => claudePath(root, rel));
|
|
4332
3881
|
const haus = PROJECT_MANAGED_HAUS_REL.map((rel) => hausPath(root, rel));
|
|
@@ -4339,7 +3888,7 @@ async function collectManagedPaths(root) {
|
|
|
4339
3888
|
const lock = await readJson(hausPath(root, "haus.lock.json"));
|
|
4340
3889
|
for (const row of lock ?? []) {
|
|
4341
3890
|
for (const rel of row.paths ?? []) {
|
|
4342
|
-
paths.add(
|
|
3891
|
+
paths.add(path22.resolve(root, rel));
|
|
4343
3892
|
}
|
|
4344
3893
|
}
|
|
4345
3894
|
const existing = [];
|
|
@@ -4355,7 +3904,7 @@ async function settingsHasHausContent(root) {
|
|
|
4355
3904
|
return settings._haus != null;
|
|
4356
3905
|
}
|
|
4357
3906
|
async function claudeMdHasHausBlock(root) {
|
|
4358
|
-
const filePath =
|
|
3907
|
+
const filePath = path22.join(root, "CLAUDE.md");
|
|
4359
3908
|
if (!await fs18.pathExists(filePath)) return false;
|
|
4360
3909
|
const text = await fs18.readFile(filePath, "utf8");
|
|
4361
3910
|
return text.includes(BLOCK_BEGIN);
|
|
@@ -4368,15 +3917,15 @@ async function stripProjectSettings(root) {
|
|
|
4368
3917
|
const hasContent = Object.keys(settings).length > 0;
|
|
4369
3918
|
if (hasContent) {
|
|
4370
3919
|
await writeProjectSettings(root, settings);
|
|
4371
|
-
log(`Stripped haus rules from ${
|
|
3920
|
+
log(`Stripped haus rules from ${path22.relative(root, settingsPath)} (user settings preserved).`);
|
|
4372
3921
|
return true;
|
|
4373
3922
|
}
|
|
4374
3923
|
await fs18.remove(settingsPath);
|
|
4375
|
-
log(`Removed ${
|
|
3924
|
+
log(`Removed ${path22.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
4376
3925
|
return true;
|
|
4377
3926
|
}
|
|
4378
3927
|
async function stripRootClaudeMd(root) {
|
|
4379
|
-
const filePath =
|
|
3928
|
+
const filePath = path22.join(root, "CLAUDE.md");
|
|
4380
3929
|
if (!await fs18.pathExists(filePath)) return false;
|
|
4381
3930
|
const prev = await fs18.readFile(filePath, "utf8");
|
|
4382
3931
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
@@ -4404,7 +3953,7 @@ async function runUndo(options) {
|
|
|
4404
3953
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
4405
3954
|
return;
|
|
4406
3955
|
}
|
|
4407
|
-
const relTargets = managed.map((p) =>
|
|
3956
|
+
const relTargets = managed.map((p) => path22.relative(root, p));
|
|
4408
3957
|
const summaryParts = [...relTargets];
|
|
4409
3958
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
4410
3959
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -4422,7 +3971,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
4422
3971
|
for (const abs of managed) {
|
|
4423
3972
|
if (!await fs18.pathExists(abs)) continue;
|
|
4424
3973
|
await fs18.remove(abs);
|
|
4425
|
-
log(`Removed ${
|
|
3974
|
+
log(`Removed ${path22.relative(root, abs)}`);
|
|
4426
3975
|
}
|
|
4427
3976
|
if (stripSettings) await stripProjectSettings(root);
|
|
4428
3977
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -4433,7 +3982,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
4433
3982
|
|
|
4434
3983
|
// src/install/uninstall.ts
|
|
4435
3984
|
import crypto3 from "crypto";
|
|
4436
|
-
import
|
|
3985
|
+
import path23 from "path";
|
|
4437
3986
|
import fs19 from "fs-extra";
|
|
4438
3987
|
async function runUninstall(options = {}) {
|
|
4439
3988
|
const { force = false } = options;
|
|
@@ -4463,14 +4012,14 @@ async function runUninstall(options = {}) {
|
|
|
4463
4012
|
continue;
|
|
4464
4013
|
}
|
|
4465
4014
|
await fs19.remove(entry.destPath);
|
|
4466
|
-
await pruneEmptyDir(
|
|
4015
|
+
await pruneEmptyDir(path23.dirname(entry.destPath));
|
|
4467
4016
|
result.deleted.push(entry.destPath);
|
|
4468
4017
|
}
|
|
4469
4018
|
const settings = await readSettings();
|
|
4470
4019
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
4471
4020
|
await writeSettings(stripped);
|
|
4472
4021
|
result.hooksStripped = true;
|
|
4473
|
-
const hausDir =
|
|
4022
|
+
const hausDir = path23.join(globalClaudeDir(), "haus");
|
|
4474
4023
|
const manifestPath2 = hausManifestPath();
|
|
4475
4024
|
if (fs19.pathExistsSync(manifestPath2)) {
|
|
4476
4025
|
await fs19.remove(manifestPath2);
|
|
@@ -4508,7 +4057,7 @@ async function runUninstallCommand(options) {
|
|
|
4508
4057
|
}
|
|
4509
4058
|
|
|
4510
4059
|
// src/commands/update.ts
|
|
4511
|
-
import
|
|
4060
|
+
import path25 from "path";
|
|
4512
4061
|
|
|
4513
4062
|
// src/update/diff-generated-files.ts
|
|
4514
4063
|
function diffGeneratedFiles() {
|
|
@@ -4535,7 +4084,7 @@ function summarizeLockDiff(before, after) {
|
|
|
4535
4084
|
|
|
4536
4085
|
// src/update/lockfile.ts
|
|
4537
4086
|
import { mkdir, readFile as readFile2, copyFile } from "fs/promises";
|
|
4538
|
-
import
|
|
4087
|
+
import path24 from "path";
|
|
4539
4088
|
async function checkLock(root) {
|
|
4540
4089
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
4541
4090
|
const hasValidVersions = lock.every(
|
|
@@ -4566,7 +4115,7 @@ async function applyLock(root) {
|
|
|
4566
4115
|
try {
|
|
4567
4116
|
const backupDir = hausPath(root, "backups");
|
|
4568
4117
|
await mkdir(backupDir, { recursive: true });
|
|
4569
|
-
await copyFile(lockPath,
|
|
4118
|
+
await copyFile(lockPath, path24.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
4570
4119
|
} catch {
|
|
4571
4120
|
}
|
|
4572
4121
|
const enriched = await Promise.all(
|
|
@@ -4588,7 +4137,7 @@ function diffLock(before, after) {
|
|
|
4588
4137
|
}
|
|
4589
4138
|
async function hasLocalOverrides(root) {
|
|
4590
4139
|
try {
|
|
4591
|
-
await readFile2(
|
|
4140
|
+
await readFile2(path24.join(root, ".claude", "settings.json"), "utf8");
|
|
4592
4141
|
return true;
|
|
4593
4142
|
} catch {
|
|
4594
4143
|
return false;
|
|
@@ -4600,7 +4149,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
|
4600
4149
|
async function runUpdate(options) {
|
|
4601
4150
|
const root = process.cwd();
|
|
4602
4151
|
if (options.check) {
|
|
4603
|
-
const pkgJson2 = await readJson(
|
|
4152
|
+
const pkgJson2 = await readJson(path25.join(packageRoot(), "package.json"));
|
|
4604
4153
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
4605
4154
|
const [status, npmVersion, latestCatalogTag, globalInstallDrift] = await Promise.all([
|
|
4606
4155
|
checkLock(root),
|
|
@@ -4630,7 +4179,7 @@ async function runUpdate(options) {
|
|
|
4630
4179
|
if (status.driftCount > 0) process.exitCode = 1;
|
|
4631
4180
|
return;
|
|
4632
4181
|
}
|
|
4633
|
-
const pkgJson = await readJson(
|
|
4182
|
+
const pkgJson = await readJson(path25.join(packageRoot(), "package.json"));
|
|
4634
4183
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
4635
4184
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
4636
4185
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -4708,7 +4257,7 @@ async function detectGlobalInstallDrift() {
|
|
|
4708
4257
|
|
|
4709
4258
|
// src/commands/validate-catalog.ts
|
|
4710
4259
|
import fs20 from "fs";
|
|
4711
|
-
import
|
|
4260
|
+
import path26 from "path";
|
|
4712
4261
|
function auditForbiddenStacks(items) {
|
|
4713
4262
|
const failures = [];
|
|
4714
4263
|
for (const item of items) {
|
|
@@ -4785,17 +4334,17 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4785
4334
|
const failures = [];
|
|
4786
4335
|
for (const item of items) {
|
|
4787
4336
|
if (!item.path) continue;
|
|
4788
|
-
const absPath =
|
|
4337
|
+
const absPath = path26.join(manifestDir, item.path);
|
|
4789
4338
|
if (item.type === "skill") {
|
|
4790
|
-
const skillMd =
|
|
4339
|
+
const skillMd = path26.join(absPath, "SKILL.md");
|
|
4791
4340
|
if (!fs20.existsSync(skillMd)) {
|
|
4792
|
-
failures.push(`${item.id}: missing ${
|
|
4341
|
+
failures.push(`${item.id}: missing ${path26.relative(manifestDir, skillMd)}`);
|
|
4793
4342
|
continue;
|
|
4794
4343
|
}
|
|
4795
4344
|
const text = fs20.readFileSync(skillMd, "utf8");
|
|
4796
4345
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
4797
4346
|
failures.push(
|
|
4798
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
4347
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path26.relative(manifestDir, skillMd)}`)
|
|
4799
4348
|
);
|
|
4800
4349
|
} else if (item.type === "agent") {
|
|
4801
4350
|
if (!fs20.existsSync(absPath)) {
|
|
@@ -4803,7 +4352,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4803
4352
|
continue;
|
|
4804
4353
|
}
|
|
4805
4354
|
const text = fs20.readFileSync(absPath, "utf8");
|
|
4806
|
-
const rel =
|
|
4355
|
+
const rel = path26.relative(manifestDir, absPath);
|
|
4807
4356
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
4808
4357
|
failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
|
|
4809
4358
|
} else if (item.type === "template") {
|
|
@@ -4818,7 +4367,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4818
4367
|
continue;
|
|
4819
4368
|
}
|
|
4820
4369
|
const text = fs20.readFileSync(absPath, "utf8");
|
|
4821
|
-
const rel =
|
|
4370
|
+
const rel = path26.relative(manifestDir, absPath);
|
|
4822
4371
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
4823
4372
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
4824
4373
|
}
|
|
@@ -4826,7 +4375,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4826
4375
|
return failures;
|
|
4827
4376
|
}
|
|
4828
4377
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
4829
|
-
const rel =
|
|
4378
|
+
const rel = path26.relative(manifestDir, absPath);
|
|
4830
4379
|
const text = fs20.readFileSync(absPath, "utf8");
|
|
4831
4380
|
const failures = [];
|
|
4832
4381
|
const lines = text.split(/\r?\n/);
|
|
@@ -4845,22 +4394,29 @@ function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
|
4845
4394
|
failures.push(...auditForbiddenTagsInText(text, `${itemId}: ${rel}`));
|
|
4846
4395
|
return failures;
|
|
4847
4396
|
}
|
|
4397
|
+
var DIR_ITEM_TYPE = {
|
|
4398
|
+
skills: "skill",
|
|
4399
|
+
agents: "agent",
|
|
4400
|
+
templates: "template",
|
|
4401
|
+
commands: "command"
|
|
4402
|
+
};
|
|
4848
4403
|
function auditMarkdownContent(manifestDir) {
|
|
4849
4404
|
const failures = [];
|
|
4850
4405
|
const dirs = ["skills", "agents", "templates", "commands"];
|
|
4851
4406
|
for (const dir of dirs) {
|
|
4852
|
-
const abs =
|
|
4407
|
+
const abs = path26.join(manifestDir, dir);
|
|
4853
4408
|
if (!fs20.existsSync(abs)) continue;
|
|
4409
|
+
const checkNonTsxNpx = !NPX_TSX_ONLY_EXEMPT_TYPES.includes(DIR_ITEM_TYPE[dir] ?? "");
|
|
4854
4410
|
walkMd(abs, (file) => {
|
|
4855
4411
|
const text = fs20.readFileSync(file, "utf8");
|
|
4856
|
-
const rel =
|
|
4412
|
+
const rel = path26.relative(manifestDir, file);
|
|
4857
4413
|
const lines = text.split(/\r?\n/);
|
|
4858
4414
|
for (let i = 0; i < lines.length; i++) {
|
|
4859
4415
|
const line2 = lines[i] ?? "";
|
|
4860
4416
|
if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2))) {
|
|
4861
4417
|
failures.push(`${rel}:${i + 1}: risky install pattern`);
|
|
4862
4418
|
}
|
|
4863
|
-
if (ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
|
|
4419
|
+
if (checkNonTsxNpx && ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
|
|
4864
4420
|
failures.push(`${rel}:${i + 1}: disallowed npx (only npx tsx allowed)`);
|
|
4865
4421
|
}
|
|
4866
4422
|
}
|
|
@@ -4873,7 +4429,7 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4873
4429
|
}
|
|
4874
4430
|
function walkMd(dir, fn) {
|
|
4875
4431
|
for (const entry of fs20.readdirSync(dir, { withFileTypes: true })) {
|
|
4876
|
-
const full =
|
|
4432
|
+
const full = path26.join(dir, entry.name);
|
|
4877
4433
|
if (entry.isDirectory()) walkMd(full, fn);
|
|
4878
4434
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
4879
4435
|
}
|
|
@@ -4884,8 +4440,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
4884
4440
|
process.exitCode = 1;
|
|
4885
4441
|
return;
|
|
4886
4442
|
}
|
|
4887
|
-
const abs =
|
|
4888
|
-
const manifestDir =
|
|
4443
|
+
const abs = path26.resolve(process.cwd(), manifestPath2);
|
|
4444
|
+
const manifestDir = path26.dirname(abs);
|
|
4889
4445
|
const data = await readJson(abs);
|
|
4890
4446
|
if (!data?.items) {
|
|
4891
4447
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -4915,7 +4471,7 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
4915
4471
|
|
|
4916
4472
|
// src/commands/workspace.ts
|
|
4917
4473
|
import { existsSync as existsSync5, statSync as statSync2 } from "fs";
|
|
4918
|
-
import
|
|
4474
|
+
import path33 from "path";
|
|
4919
4475
|
|
|
4920
4476
|
// src/commands/workspace/aggregate.ts
|
|
4921
4477
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -4969,7 +4525,7 @@ ${summaries.map(
|
|
|
4969
4525
|
}
|
|
4970
4526
|
|
|
4971
4527
|
// src/commands/workspace/config.ts
|
|
4972
|
-
import
|
|
4528
|
+
import path27 from "path";
|
|
4973
4529
|
import YAML from "yaml";
|
|
4974
4530
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
4975
4531
|
function parseWorkspaceConfig(text) {
|
|
@@ -4992,11 +4548,11 @@ function parseWorkspaceConfig(text) {
|
|
|
4992
4548
|
};
|
|
4993
4549
|
}
|
|
4994
4550
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
4995
|
-
return parseWorkspaceConfig(await readText(
|
|
4551
|
+
return parseWorkspaceConfig(await readText(path27.join(workspaceRoot, WORKSPACE_FILE)));
|
|
4996
4552
|
}
|
|
4997
4553
|
|
|
4998
4554
|
// src/commands/workspace/discover.ts
|
|
4999
|
-
import
|
|
4555
|
+
import path28 from "path";
|
|
5000
4556
|
import fg4 from "fast-glob";
|
|
5001
4557
|
import YAML2 from "yaml";
|
|
5002
4558
|
var DEFAULT_MAX_DEPTH = 3;
|
|
@@ -5024,8 +4580,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
5024
4580
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
5025
4581
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
5026
4582
|
for (const match of matches) {
|
|
5027
|
-
const base =
|
|
5028
|
-
const dir =
|
|
4583
|
+
const base = path28.posix.basename(match);
|
|
4584
|
+
const dir = path28.posix.dirname(match);
|
|
5029
4585
|
const owner = dir === "." ? "." : dir;
|
|
5030
4586
|
if (base === ".git") gitDirs.add(owner);
|
|
5031
4587
|
else manifestDirs.add(owner);
|
|
@@ -5041,9 +4597,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
5041
4597
|
}
|
|
5042
4598
|
repoRoots.sort((a, b) => a.localeCompare(b));
|
|
5043
4599
|
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
5044
|
-
const absDir =
|
|
5045
|
-
const pkg = await readJson(
|
|
5046
|
-
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name :
|
|
4600
|
+
const absDir = path28.resolve(workspaceRoot, relDir);
|
|
4601
|
+
const pkg = await readJson(path28.join(absDir, "package.json"));
|
|
4602
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path28.basename(relDir === "." ? workspaceRoot : absDir);
|
|
5047
4603
|
let role = "auto";
|
|
5048
4604
|
try {
|
|
5049
4605
|
const scan = await scanProject(absDir);
|
|
@@ -5078,15 +4634,15 @@ function mergeWorkspaceConfig(existing, discovered, opts = {}) {
|
|
|
5078
4634
|
relationships: existing?.relationships ?? []
|
|
5079
4635
|
};
|
|
5080
4636
|
}
|
|
5081
|
-
function renderWorkspaceYaml(
|
|
4637
|
+
function renderWorkspaceYaml(config) {
|
|
5082
4638
|
return YAML2.stringify({
|
|
5083
|
-
client:
|
|
5084
|
-
repos:
|
|
5085
|
-
relationships:
|
|
4639
|
+
client: config.client,
|
|
4640
|
+
repos: config.repos.map((r) => ({ name: r.name, path: r.path, role: r.role ?? "auto" })),
|
|
4641
|
+
relationships: config.relationships
|
|
5086
4642
|
});
|
|
5087
4643
|
}
|
|
5088
4644
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
5089
|
-
const yamlPath =
|
|
4645
|
+
const yamlPath = path28.join(workspaceRoot, "haus.workspace.yaml");
|
|
5090
4646
|
const existingText = await readText(yamlPath);
|
|
5091
4647
|
const existing = parseWorkspaceConfig(existingText);
|
|
5092
4648
|
if (existingText && !existing) {
|
|
@@ -5120,18 +4676,18 @@ async function runDiscover(workspaceRoot, opts = {}) {
|
|
|
5120
4676
|
|
|
5121
4677
|
// src/commands/workspace/doctor.ts
|
|
5122
4678
|
import { existsSync as existsSync3 } from "fs";
|
|
5123
|
-
import
|
|
4679
|
+
import path30 from "path";
|
|
5124
4680
|
|
|
5125
4681
|
// src/commands/workspace/manifest.ts
|
|
5126
4682
|
import { readFileSync as readFileSync3 } from "fs";
|
|
5127
|
-
import
|
|
4683
|
+
import path29 from "path";
|
|
5128
4684
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
5129
4685
|
function manifestPath(workspaceRoot) {
|
|
5130
4686
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
5131
4687
|
}
|
|
5132
4688
|
function hausVersion() {
|
|
5133
4689
|
try {
|
|
5134
|
-
const pkg = JSON.parse(readFileSync3(
|
|
4690
|
+
const pkg = JSON.parse(readFileSync3(path29.join(packageRoot(), "package.json"), "utf8"));
|
|
5135
4691
|
return pkg.version ?? "0.0.0";
|
|
5136
4692
|
} catch {
|
|
5137
4693
|
return "0.0.0";
|
|
@@ -5169,7 +4725,7 @@ async function writeWorkspaceManifest(workspaceRoot, manifest) {
|
|
|
5169
4725
|
|
|
5170
4726
|
// src/commands/workspace/doctor.ts
|
|
5171
4727
|
async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
5172
|
-
const
|
|
4728
|
+
const config = await readWorkspaceConfig(workspaceRoot);
|
|
5173
4729
|
const manifest = await readManifest2(workspaceRoot);
|
|
5174
4730
|
const currentVersion = hausVersion();
|
|
5175
4731
|
const drift = [];
|
|
@@ -5179,7 +4735,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
5179
4735
|
drift.push(item);
|
|
5180
4736
|
detail.push({ stream: "warn", text: `- ${item.repo}: ${item.detail}` });
|
|
5181
4737
|
};
|
|
5182
|
-
if (!
|
|
4738
|
+
if (!config) {
|
|
5183
4739
|
flag({
|
|
5184
4740
|
repo: "(workspace)",
|
|
5185
4741
|
kind: "no-config",
|
|
@@ -5196,8 +4752,8 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
5196
4752
|
return emit({ workspaceRoot, manifest, drift, detail, json: opts.json });
|
|
5197
4753
|
}
|
|
5198
4754
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
5199
|
-
for (const repo of
|
|
5200
|
-
const repoRoot =
|
|
4755
|
+
for (const repo of config.repos) {
|
|
4756
|
+
const repoRoot = path30.resolve(workspaceRoot, repo.path);
|
|
5201
4757
|
const entry = manifestByName.get(repo.name);
|
|
5202
4758
|
if (!entry) {
|
|
5203
4759
|
flag({
|
|
@@ -5271,10 +4827,10 @@ function emit(args) {
|
|
|
5271
4827
|
|
|
5272
4828
|
// src/commands/workspace/setup.ts
|
|
5273
4829
|
import { existsSync as existsSync4, statSync } from "fs";
|
|
5274
|
-
import
|
|
4830
|
+
import path32 from "path";
|
|
5275
4831
|
|
|
5276
4832
|
// src/claude/write-workspace-claude-md.ts
|
|
5277
|
-
import
|
|
4833
|
+
import path31 from "path";
|
|
5278
4834
|
import fs21 from "fs-extra";
|
|
5279
4835
|
function buildWorkspaceImportBlock(client, members) {
|
|
5280
4836
|
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
@@ -5293,7 +4849,7 @@ ${BLOCK_END}`;
|
|
|
5293
4849
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
5294
4850
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
5295
4851
|
const dryRun = opts.dryRun ?? false;
|
|
5296
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
4852
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path31.join(workspaceRoot, "CLAUDE.md");
|
|
5297
4853
|
const prev = await fs21.pathExists(filePath) ? await fs21.readFile(filePath, "utf8") : "";
|
|
5298
4854
|
const next = opts.collision ? `${block}
|
|
5299
4855
|
` : injectHausBlock(prev, block);
|
|
@@ -5319,37 +4875,37 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
5319
4875
|
|
|
5320
4876
|
// src/commands/workspace/setup.ts
|
|
5321
4877
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
5322
|
-
let dir =
|
|
4878
|
+
let dir = path32.resolve(start);
|
|
5323
4879
|
for (; ; ) {
|
|
5324
|
-
if (existsSync4(
|
|
5325
|
-
const parent =
|
|
5326
|
-
if (parent === dir) return
|
|
4880
|
+
if (existsSync4(path32.join(dir, WORKSPACE_FILE))) return dir;
|
|
4881
|
+
const parent = path32.dirname(dir);
|
|
4882
|
+
if (parent === dir) return path32.resolve(start);
|
|
5327
4883
|
dir = parent;
|
|
5328
4884
|
}
|
|
5329
4885
|
}
|
|
5330
4886
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
5331
|
-
return
|
|
4887
|
+
return path32.resolve(workspaceRoot, repoPath) === path32.resolve(workspaceRoot);
|
|
5332
4888
|
}
|
|
5333
4889
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
5334
4890
|
const apply = options.write ?? false;
|
|
5335
|
-
const configText = await readText(
|
|
4891
|
+
const configText = await readText(path32.join(workspaceRoot, WORKSPACE_FILE));
|
|
5336
4892
|
if (!configText) {
|
|
5337
4893
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
5338
4894
|
process.exitCode = 1;
|
|
5339
4895
|
return { workspaceRoot, statuses: [], written: [] };
|
|
5340
4896
|
}
|
|
5341
|
-
const
|
|
5342
|
-
if (!
|
|
4897
|
+
const config = parseWorkspaceConfig(configText);
|
|
4898
|
+
if (!config || config.repos.length === 0) {
|
|
5343
4899
|
error(`No repos configured in ${WORKSPACE_FILE}.`);
|
|
5344
4900
|
process.exitCode = 1;
|
|
5345
4901
|
return { workspaceRoot, statuses: [], written: [] };
|
|
5346
4902
|
}
|
|
5347
4903
|
const onlySet = options.only && options.only.length > 0 ? new Set(options.only) : void 0;
|
|
5348
|
-
const repos = onlySet ?
|
|
4904
|
+
const repos = onlySet ? config.repos.filter((r) => onlySet.has(r.name)) : config.repos;
|
|
5349
4905
|
const statuses = [];
|
|
5350
4906
|
const aggregateInputs = [];
|
|
5351
4907
|
for (const repo of repos) {
|
|
5352
|
-
const repoRoot =
|
|
4908
|
+
const repoRoot = path32.resolve(workspaceRoot, repo.path);
|
|
5353
4909
|
log(`
|
|
5354
4910
|
\u2192 ${repo.name} (${repo.path})`);
|
|
5355
4911
|
try {
|
|
@@ -5386,18 +4942,18 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
5386
4942
|
}
|
|
5387
4943
|
const written = [];
|
|
5388
4944
|
if (apply && aggregateInputs.length > 0) {
|
|
5389
|
-
const collision =
|
|
4945
|
+
const collision = config.repos.some((r) => isRootRepo(workspaceRoot, r.path));
|
|
5390
4946
|
if (!options.dryRun) {
|
|
5391
4947
|
const artifacts = await writeWorkspaceArtifacts(
|
|
5392
4948
|
workspaceRoot,
|
|
5393
4949
|
aggregateInputs,
|
|
5394
|
-
|
|
4950
|
+
config.relationships
|
|
5395
4951
|
);
|
|
5396
4952
|
written.push(...artifacts);
|
|
5397
4953
|
}
|
|
5398
4954
|
const docPath = await writeWorkspaceClaudeMd(workspaceRoot, {
|
|
5399
|
-
client:
|
|
5400
|
-
members:
|
|
4955
|
+
client: config.client,
|
|
4956
|
+
members: config.repos.map((r) => ({ name: r.name, path: r.path })),
|
|
5401
4957
|
collision,
|
|
5402
4958
|
dryRun: options.dryRun
|
|
5403
4959
|
});
|
|
@@ -5413,11 +4969,11 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
5413
4969
|
}
|
|
5414
4970
|
const priorByName = new Map((prior?.repos ?? []).map((r) => [r.name, r]));
|
|
5415
4971
|
const manifestRepos = [];
|
|
5416
|
-
for (const repo of
|
|
4972
|
+
for (const repo of config.repos) {
|
|
5417
4973
|
const status = statusByName.get(repo.name);
|
|
5418
4974
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
5419
4975
|
if (status?.status === "ok") {
|
|
5420
|
-
const lock = await checkLock(
|
|
4976
|
+
const lock = await checkLock(path32.resolve(workspaceRoot, repo.path));
|
|
5421
4977
|
manifestRepos.push({
|
|
5422
4978
|
name: repo.name,
|
|
5423
4979
|
path: repo.path,
|
|
@@ -5460,7 +5016,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
5460
5016
|
);
|
|
5461
5017
|
}
|
|
5462
5018
|
}
|
|
5463
|
-
const manifest = buildManifest2({ client:
|
|
5019
|
+
const manifest = buildManifest2({ client: config.client, repos: manifestRepos });
|
|
5464
5020
|
const manifestFile = await writeWorkspaceManifest(workspaceRoot, manifest);
|
|
5465
5021
|
written.push(manifestFile);
|
|
5466
5022
|
}
|
|
@@ -5497,35 +5053,35 @@ relationships: []
|
|
|
5497
5053
|
log("Workspace initialized.");
|
|
5498
5054
|
}
|
|
5499
5055
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
5500
|
-
const configText = await readText(
|
|
5056
|
+
const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
|
|
5501
5057
|
if (!configText) {
|
|
5502
5058
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
5503
5059
|
process.exitCode = 1;
|
|
5504
5060
|
return;
|
|
5505
5061
|
}
|
|
5506
|
-
const
|
|
5507
|
-
if (!
|
|
5062
|
+
const config = parseWorkspaceConfig(configText);
|
|
5063
|
+
if (!config) {
|
|
5508
5064
|
error(
|
|
5509
5065
|
`Malformed ${WORKSPACE_FILE}. Fix the YAML or re-run \`haus workspace discover --write\`.`
|
|
5510
5066
|
);
|
|
5511
5067
|
process.exitCode = 1;
|
|
5512
5068
|
return;
|
|
5513
5069
|
}
|
|
5514
|
-
if (
|
|
5070
|
+
if (config.repos.length === 0) {
|
|
5515
5071
|
error(`No repos configured in ${WORKSPACE_FILE}.`);
|
|
5516
5072
|
process.exitCode = 1;
|
|
5517
5073
|
return;
|
|
5518
5074
|
}
|
|
5519
5075
|
const inputs = [];
|
|
5520
|
-
for (const repo of
|
|
5521
|
-
const repoRoot =
|
|
5076
|
+
for (const repo of config.repos) {
|
|
5077
|
+
const repoRoot = path33.resolve(workspaceRoot, repo.path);
|
|
5522
5078
|
if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
|
|
5523
5079
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
5524
5080
|
}
|
|
5525
5081
|
const result = await scanProject(repoRoot);
|
|
5526
5082
|
inputs.push({ name: repo.name, path: repo.path, context: result });
|
|
5527
5083
|
}
|
|
5528
|
-
const written = await writeWorkspaceArtifacts(workspaceRoot, inputs,
|
|
5084
|
+
const written = await writeWorkspaceArtifacts(workspaceRoot, inputs, config.relationships);
|
|
5529
5085
|
if (opts.json) {
|
|
5530
5086
|
log(JSON.stringify({ written }, null, 2));
|
|
5531
5087
|
} else {
|
|
@@ -5568,7 +5124,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
5568
5124
|
// src/cli.ts
|
|
5569
5125
|
function cliVersion() {
|
|
5570
5126
|
try {
|
|
5571
|
-
const pkgPath =
|
|
5127
|
+
const pkgPath = path34.join(packageRoot(), "package.json");
|
|
5572
5128
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5573
5129
|
return pkg.version ?? "0.0.0";
|
|
5574
5130
|
} catch {
|
|
@@ -5578,7 +5134,7 @@ function cliVersion() {
|
|
|
5578
5134
|
var program = new Command();
|
|
5579
5135
|
function validateRuntimeNodeVersion() {
|
|
5580
5136
|
try {
|
|
5581
|
-
const pkgPath =
|
|
5137
|
+
const pkgPath = path34.join(packageRoot(), "package.json");
|
|
5582
5138
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5583
5139
|
const requiredRange = pkg.engines?.node;
|
|
5584
5140
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -5605,7 +5161,6 @@ program.command("apply").option("--dry-run").option("--write").option("--select"
|
|
|
5605
5161
|
).option("--force", "Overwrite user-modified managed workflow files").action(runApply);
|
|
5606
5162
|
program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
|
|
5607
5163
|
program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
|
|
5608
|
-
program.command("context").option("--task <task>").option("--from-hook").option("--json").option("--verbose").action(runContext);
|
|
5609
5164
|
program.command("init").option("--json").action(runInit);
|
|
5610
5165
|
program.command("clone").description("Clone a single git repository by URL").argument("<url>", "Git URL to clone").argument("[dir]", "Target directory (default: repo name derived from the URL)").option("--dry-run", "Print what would happen without cloning").action(
|
|
5611
5166
|
(url, dir, opts) => runClone(url, { dir, dryRun: opts.dryRun })
|
|
@@ -5619,10 +5174,6 @@ program.command("uninstall").option("--force").action(runUninstallCommand);
|
|
|
5619
5174
|
var guard = program.command("guard");
|
|
5620
5175
|
guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
|
|
5621
5176
|
guard.command("bash").option("--from-hook").action((opts) => runGuard("bash", opts));
|
|
5622
|
-
var config = program.command("config");
|
|
5623
|
-
config.command("enable <key>").description("Enable a hook (hook.context)").action((key) => runConfig(key, "enable"));
|
|
5624
|
-
config.command("disable <key>").description("Disable a hook (hook.context)").action((key) => runConfig(key, "disable"));
|
|
5625
|
-
config.command("status <key>").description("Show current state of a hook (hook.context)").action((key) => runConfig(key, "status"));
|
|
5626
5177
|
var workspace = program.command("workspace");
|
|
5627
5178
|
workspace.command("init").action(() => runWorkspace("init"));
|
|
5628
5179
|
workspace.command("discover").description("Auto-find member repos and write/merge haus.workspace.yaml").option("--write", "Persist haus.workspace.yaml (default previews only)").option("--json", "Output the discovered repos and proposed config as JSON").option("--max-depth <n>", "Max directory depth to traverse (default 3)").option("--client <name>", "Set the workspace client name").action((opts) => runWorkspace("discover", opts));
|