@haus-tech/haus-workflow 0.25.0 → 0.25.1
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 +2 -0
- package/README.md +0 -2
- package/dist/cli.js +260 -724
- 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
|
|
|
@@ -839,14 +839,7 @@ async function getCacheManifestAge() {
|
|
|
839
839
|
}
|
|
840
840
|
|
|
841
841
|
// src/install/allow-rules.ts
|
|
842
|
-
var ALLOWED_SUBCOMMANDS = [
|
|
843
|
-
"setup-project",
|
|
844
|
-
"apply",
|
|
845
|
-
"doctor",
|
|
846
|
-
"scan",
|
|
847
|
-
"context",
|
|
848
|
-
"recommend"
|
|
849
|
-
];
|
|
842
|
+
var ALLOWED_SUBCOMMANDS = ["setup-project", "apply", "doctor", "scan", "recommend"];
|
|
850
843
|
function buildAllowRules() {
|
|
851
844
|
return [...new Set(ALLOWED_SUBCOMMANDS.map((sub) => `Bash(haus ${sub}:*)`))];
|
|
852
845
|
}
|
|
@@ -901,6 +894,44 @@ function collectEventHookCommands(entries) {
|
|
|
901
894
|
}
|
|
902
895
|
return cmds;
|
|
903
896
|
}
|
|
897
|
+
var RETIRED_HAUS_HOOK_COMMANDS = ["haus context --from-hook"];
|
|
898
|
+
var RETIRED_HAUS_HOOK_IDS = ["hook.context", "haus.context-hook"];
|
|
899
|
+
function reconcileRetiredHausHooks(updated, fragments, priorHaus, addedIds, addedCommands) {
|
|
900
|
+
const canonicalCommands = new Set(
|
|
901
|
+
fragments.filter((f) => f.gate === "keep").map((f) => f.command)
|
|
902
|
+
);
|
|
903
|
+
const canonicalIds = new Set(fragments.filter((f) => f.gate === "keep").map((f) => f.id));
|
|
904
|
+
const retiredCommands = new Set(RETIRED_HAUS_HOOK_COMMANDS);
|
|
905
|
+
for (const cmd of [...priorHaus?.hookCommands ?? [], ...addedCommands]) {
|
|
906
|
+
if (!canonicalCommands.has(cmd)) retiredCommands.add(cmd);
|
|
907
|
+
}
|
|
908
|
+
const prunedHooks = {};
|
|
909
|
+
for (const [event, entries] of Object.entries(updated.hooks ?? {})) {
|
|
910
|
+
const kept = entries.filter((entry) => {
|
|
911
|
+
const cmds = (entry.hooks ?? []).map((h) => h.command).filter(Boolean);
|
|
912
|
+
return cmds.length === 0 || !cmds.some((cmd) => retiredCommands.has(cmd));
|
|
913
|
+
});
|
|
914
|
+
if (kept.length > 0) prunedHooks[event] = kept;
|
|
915
|
+
}
|
|
916
|
+
updated.hooks = prunedHooks;
|
|
917
|
+
const retiredIdSet = new Set(RETIRED_HAUS_HOOK_IDS);
|
|
918
|
+
for (const id of priorHaus?.hooks ?? []) {
|
|
919
|
+
if (!canonicalIds.has(id)) retiredIdSet.add(id);
|
|
920
|
+
}
|
|
921
|
+
const survivingIds = [.../* @__PURE__ */ new Set([...priorHaus?.hooks ?? [], ...addedIds])].filter(
|
|
922
|
+
(id) => canonicalIds.has(id) && !retiredIdSet.has(id)
|
|
923
|
+
);
|
|
924
|
+
const survivingCommands = [
|
|
925
|
+
.../* @__PURE__ */ new Set([...priorHaus?.hookCommands ?? [], ...addedCommands])
|
|
926
|
+
].filter((cmd) => canonicalCommands.has(cmd));
|
|
927
|
+
updated._haus = {
|
|
928
|
+
hooks: survivingIds,
|
|
929
|
+
...survivingCommands.length > 0 ? { hookCommands: survivingCommands } : {},
|
|
930
|
+
...priorHaus?.denyRules ? { denyRules: priorHaus.denyRules } : {},
|
|
931
|
+
...priorHaus?.allowRules ? { allowRules: priorHaus.allowRules } : {},
|
|
932
|
+
...priorHaus?.askRules ? { askRules: priorHaus.askRules } : {}
|
|
933
|
+
};
|
|
934
|
+
}
|
|
904
935
|
function mergeHooks(settings, fragments) {
|
|
905
936
|
const existing = settings._haus?.hooks ?? [];
|
|
906
937
|
const existingCommands = settings._haus?.hookCommands ?? [];
|
|
@@ -926,14 +957,7 @@ function mergeHooks(settings, fragments) {
|
|
|
926
957
|
if (!existing.includes(fragment.id)) addedIds.push(fragment.id);
|
|
927
958
|
if (!existingCommands.includes(fragment.command)) addedCommands.push(fragment.command);
|
|
928
959
|
}
|
|
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
|
-
};
|
|
960
|
+
reconcileRetiredHausHooks(updated, fragments, settings._haus, addedIds, addedCommands);
|
|
937
961
|
return { settings: updated, addedIds };
|
|
938
962
|
}
|
|
939
963
|
function reconcileManagedRules(existing, prevTracked, newRules) {
|
|
@@ -1185,8 +1209,8 @@ function buildDenyRules() {
|
|
|
1185
1209
|
for (const command of DENY_COMMANDS) {
|
|
1186
1210
|
rules.push(`Bash(${command}:*)`);
|
|
1187
1211
|
}
|
|
1188
|
-
for (const
|
|
1189
|
-
const pattern = DENY_DIRS.has(
|
|
1212
|
+
for (const path35 of DENY_PATHS) {
|
|
1213
|
+
const pattern = DENY_DIRS.has(path35) ? `${path35}/**` : path35;
|
|
1190
1214
|
for (const tool of FILE_TOOLS) {
|
|
1191
1215
|
rules.push(`${tool}(${pattern})`);
|
|
1192
1216
|
}
|
|
@@ -1242,12 +1266,6 @@ function packageRoot() {
|
|
|
1242
1266
|
|
|
1243
1267
|
// src/claude/merge-project-settings.ts
|
|
1244
1268
|
var PROJECT_HOOK_FRAGMENTS = [
|
|
1245
|
-
{
|
|
1246
|
-
id: "haus.context-hook",
|
|
1247
|
-
gate: "keep",
|
|
1248
|
-
event: "UserPromptSubmit",
|
|
1249
|
-
command: "haus context --from-hook"
|
|
1250
|
-
},
|
|
1251
1269
|
{
|
|
1252
1270
|
id: "haus.guard-file",
|
|
1253
1271
|
gate: "keep",
|
|
@@ -1285,7 +1303,7 @@ async function applyProjectSettingsMerge(root) {
|
|
|
1285
1303
|
}
|
|
1286
1304
|
|
|
1287
1305
|
// src/claude/write-claude-files.ts
|
|
1288
|
-
import
|
|
1306
|
+
import path12 from "path";
|
|
1289
1307
|
import fs12 from "fs-extra";
|
|
1290
1308
|
|
|
1291
1309
|
// src/catalog/load-catalog.ts
|
|
@@ -1388,19 +1406,6 @@ async function hashInstalledPaths(root, relPaths) {
|
|
|
1388
1406
|
return hashText(fileDigests.map((f) => `${f.rel}=${f.digest}`).join("|"));
|
|
1389
1407
|
}
|
|
1390
1408
|
|
|
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
1409
|
// src/claude/managed-write.ts
|
|
1405
1410
|
import fs5 from "fs-extra";
|
|
1406
1411
|
|
|
@@ -1454,7 +1459,7 @@ async function writeManagedJson(root, filePath, value, dryRun) {
|
|
|
1454
1459
|
}
|
|
1455
1460
|
|
|
1456
1461
|
// src/claude/superpowers-install.ts
|
|
1457
|
-
import
|
|
1462
|
+
import path8 from "path";
|
|
1458
1463
|
import fg3 from "fast-glob";
|
|
1459
1464
|
import fs6 from "fs-extra";
|
|
1460
1465
|
var SUPERPOWERS_ORIGIN_SOURCE_ID = "superpowers-pcvelz";
|
|
@@ -1471,7 +1476,7 @@ function rewriteSuperpowersMarkdown(text) {
|
|
|
1471
1476
|
async function rewriteMarkdownTree(dir) {
|
|
1472
1477
|
const files = await fg3("**/*.md", { cwd: dir, onlyFiles: true, dot: true });
|
|
1473
1478
|
for (const rel of files) {
|
|
1474
|
-
const abs =
|
|
1479
|
+
const abs = path8.join(dir, rel);
|
|
1475
1480
|
const text = await fs6.readFile(abs, "utf8");
|
|
1476
1481
|
const rewritten = rewriteSuperpowersMarkdown(text);
|
|
1477
1482
|
if (rewritten !== text) {
|
|
@@ -1481,7 +1486,7 @@ async function rewriteMarkdownTree(dir) {
|
|
|
1481
1486
|
}
|
|
1482
1487
|
async function installCatalogSkill(sourcePath, destination, opts) {
|
|
1483
1488
|
if (opts.dryRun) return;
|
|
1484
|
-
await fs6.ensureDir(
|
|
1489
|
+
await fs6.ensureDir(path8.dirname(destination));
|
|
1485
1490
|
if (await fs6.pathExists(destination)) {
|
|
1486
1491
|
await fs6.remove(destination);
|
|
1487
1492
|
}
|
|
@@ -1491,17 +1496,17 @@ async function installCatalogSkill(sourcePath, destination, opts) {
|
|
|
1491
1496
|
}
|
|
1492
1497
|
}
|
|
1493
1498
|
async function installSuperpowersShared(contentRoot, projectRoot, dryRun) {
|
|
1494
|
-
const source =
|
|
1499
|
+
const source = path8.join(contentRoot, SUPERPOWERS_SHARED_CATALOG_REL);
|
|
1495
1500
|
if (!await fs6.pathExists(source)) return null;
|
|
1496
1501
|
const destination = claudePath(projectRoot, "skills", "shared");
|
|
1497
|
-
if (dryRun) return
|
|
1498
|
-
await fs6.ensureDir(
|
|
1502
|
+
if (dryRun) return path8.relative(projectRoot, destination);
|
|
1503
|
+
await fs6.ensureDir(path8.dirname(destination));
|
|
1499
1504
|
if (await fs6.pathExists(destination)) {
|
|
1500
1505
|
await fs6.remove(destination);
|
|
1501
1506
|
}
|
|
1502
1507
|
await fs6.copy(source, destination, { overwrite: true, errorOnExist: false });
|
|
1503
1508
|
await rewriteMarkdownTree(destination);
|
|
1504
|
-
return
|
|
1509
|
+
return path8.relative(projectRoot, destination);
|
|
1505
1510
|
}
|
|
1506
1511
|
|
|
1507
1512
|
// src/claude/verify-hooks-contract.ts
|
|
@@ -1510,11 +1515,6 @@ import fs7 from "fs-extra";
|
|
|
1510
1515
|
// src/claude/load-hooks.ts
|
|
1511
1516
|
var CANONICAL_HOOKS = {
|
|
1512
1517
|
hooks: {
|
|
1513
|
-
UserPromptSubmit: [
|
|
1514
|
-
{
|
|
1515
|
-
hooks: [{ type: "command", command: "haus context --from-hook" }]
|
|
1516
|
-
}
|
|
1517
|
-
],
|
|
1518
1518
|
PreToolUse: [
|
|
1519
1519
|
{
|
|
1520
1520
|
matcher: "Read|Edit|Write",
|
|
@@ -1545,11 +1545,6 @@ function collectHookCommands(settings) {
|
|
|
1545
1545
|
}
|
|
1546
1546
|
function hausHookContractSatisfied(project, canonical) {
|
|
1547
1547
|
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
1548
|
for (const block of canonical.hooks.PreToolUse) {
|
|
1554
1549
|
for (const h of block.hooks) {
|
|
1555
1550
|
if (!present.has(h.command)) return false;
|
|
@@ -1604,7 +1599,7 @@ async function verifyProjectSettingsHooksContract(root) {
|
|
|
1604
1599
|
}
|
|
1605
1600
|
|
|
1606
1601
|
// src/claude/write-root-claude-md.ts
|
|
1607
|
-
import
|
|
1602
|
+
import path9 from "path";
|
|
1608
1603
|
import fs8 from "fs-extra";
|
|
1609
1604
|
var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
|
|
1610
1605
|
var BLOCK_END = "<!-- HAUS:END haus-imports -->";
|
|
@@ -1645,7 +1640,7 @@ ${block}
|
|
|
1645
1640
|
`;
|
|
1646
1641
|
}
|
|
1647
1642
|
async function writeRootClaudeMd(root, dryRun) {
|
|
1648
|
-
const filePath =
|
|
1643
|
+
const filePath = path9.join(root, "CLAUDE.md");
|
|
1649
1644
|
const block = buildImportBlock();
|
|
1650
1645
|
const prev = await fs8.pathExists(filePath) ? await fs8.readFile(filePath, "utf8") : "";
|
|
1651
1646
|
const next = injectHausBlock(prev, block);
|
|
@@ -1654,11 +1649,11 @@ async function writeRootClaudeMd(root, dryRun) {
|
|
|
1654
1649
|
}
|
|
1655
1650
|
|
|
1656
1651
|
// src/claude/write-workflow-config.ts
|
|
1657
|
-
import
|
|
1652
|
+
import path11 from "path";
|
|
1658
1653
|
import fs10 from "fs-extra";
|
|
1659
1654
|
|
|
1660
1655
|
// src/claude/derive-workflow-config.ts
|
|
1661
|
-
import
|
|
1656
|
+
import path10 from "path";
|
|
1662
1657
|
import fs9 from "fs-extra";
|
|
1663
1658
|
function binCmd(pm, bin, args) {
|
|
1664
1659
|
const tail = args ? ` ${args}` : "";
|
|
@@ -1668,7 +1663,7 @@ function binCmd(pm, bin, args) {
|
|
|
1668
1663
|
}
|
|
1669
1664
|
async function deriveWorkflowConfig(root, ctx) {
|
|
1670
1665
|
const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
|
|
1671
|
-
const pkg = await readJson(
|
|
1666
|
+
const pkg = await readJson(path10.join(root, "package.json"));
|
|
1672
1667
|
const scripts = pkg?.scripts ?? {};
|
|
1673
1668
|
const deps = new Set(ctx.dependencies);
|
|
1674
1669
|
const stacks = Object.values(ctx.detectedStacks ?? {}).flat();
|
|
@@ -1678,7 +1673,7 @@ async function deriveWorkflowConfig(root, ctx) {
|
|
|
1678
1673
|
return null;
|
|
1679
1674
|
};
|
|
1680
1675
|
const hasDep = (name) => deps.has(name);
|
|
1681
|
-
const exists = (rel) => fs9.pathExistsSync(
|
|
1676
|
+
const exists = (rel) => fs9.pathExistsSync(path10.join(root, rel));
|
|
1682
1677
|
const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
|
|
1683
1678
|
const hasCypress = hasDep("cypress");
|
|
1684
1679
|
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 +1749,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
1754
1749
|
const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
|
|
1755
1750
|
...FALLBACK_CONTEXT,
|
|
1756
1751
|
root,
|
|
1757
|
-
repoName:
|
|
1752
|
+
repoName: path11.basename(root)
|
|
1758
1753
|
};
|
|
1759
1754
|
const values = await deriveWorkflowConfig(root, ctx);
|
|
1760
1755
|
if (exists) {
|
|
@@ -1861,7 +1856,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1861
1856
|
estimatedTokenReductionPct: 0
|
|
1862
1857
|
};
|
|
1863
1858
|
const pkgRoot = packageRoot();
|
|
1864
|
-
const hausVersion2 = (await readJson(
|
|
1859
|
+
const hausVersion2 = (await readJson(path12.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
|
|
1865
1860
|
const coreFiles = [
|
|
1866
1861
|
claudePath(root, "settings.json"),
|
|
1867
1862
|
claudePath(root, "rules", "haus.md"),
|
|
@@ -1885,10 +1880,6 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1885
1880
|
await applyProjectSettingsMerge(root);
|
|
1886
1881
|
await assertPostApplySettingsHausContract(root);
|
|
1887
1882
|
}
|
|
1888
|
-
const configPath = hausPath(root, "config.json");
|
|
1889
|
-
if (!await fs12.pathExists(configPath)) {
|
|
1890
|
-
await writeManagedJson(root, configPath, DEFAULT_HOOKS_CONFIG, dryRun);
|
|
1891
|
-
}
|
|
1892
1883
|
await writeManagedText(
|
|
1893
1884
|
root,
|
|
1894
1885
|
claudePath(root, "commands", "haus-doctor.md"),
|
|
@@ -1947,6 +1938,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1947
1938
|
}
|
|
1948
1939
|
}
|
|
1949
1940
|
const LEGACY_PRUNED_ARTIFACTS = [
|
|
1941
|
+
"config.json",
|
|
1950
1942
|
"selected-context.json",
|
|
1951
1943
|
"dependency-map.json",
|
|
1952
1944
|
"scan-hashes.json",
|
|
@@ -1997,7 +1989,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1997
1989
|
);
|
|
1998
1990
|
continue;
|
|
1999
1991
|
}
|
|
2000
|
-
const destination = claudePath(root, target,
|
|
1992
|
+
const destination = claudePath(root, target, path12.basename(sourcePath));
|
|
2001
1993
|
if (await fs12.pathExists(sourcePath)) {
|
|
2002
1994
|
if (dryRun) {
|
|
2003
1995
|
const exists = await fs12.pathExists(destination);
|
|
@@ -2010,17 +2002,17 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2010
2002
|
dryRun: false
|
|
2011
2003
|
});
|
|
2012
2004
|
} else {
|
|
2013
|
-
await fs12.ensureDir(
|
|
2005
|
+
await fs12.ensureDir(path12.dirname(destination));
|
|
2014
2006
|
await fs12.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
2015
2007
|
}
|
|
2016
2008
|
files.push(destination);
|
|
2017
|
-
const relPaths = [
|
|
2009
|
+
const relPaths = [path12.relative(root, destination)];
|
|
2018
2010
|
if (!superpowersSharedInstalled && manifestItem.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID && item.type === "skill") {
|
|
2019
2011
|
const sharedRel = await installSuperpowersShared(contentRoot, root, dryRun);
|
|
2020
2012
|
if (sharedRel) {
|
|
2021
2013
|
superpowersSharedInstalled = true;
|
|
2022
2014
|
relPaths.push(sharedRel);
|
|
2023
|
-
files.push(
|
|
2015
|
+
files.push(path12.join(root, sharedRel));
|
|
2024
2016
|
}
|
|
2025
2017
|
}
|
|
2026
2018
|
const current = installedPathsByItem.get(item.id) ?? [];
|
|
@@ -2083,7 +2075,7 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
|
|
|
2083
2075
|
if (relPaths.length === 0) continue;
|
|
2084
2076
|
const existing = [];
|
|
2085
2077
|
for (const rel of relPaths) {
|
|
2086
|
-
if (await fs12.pathExists(
|
|
2078
|
+
if (await fs12.pathExists(path12.join(root, rel))) existing.push(rel);
|
|
2087
2079
|
}
|
|
2088
2080
|
if (existing.length === 0) continue;
|
|
2089
2081
|
if (entry.hash === void 0) {
|
|
@@ -2100,13 +2092,13 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
|
|
|
2100
2092
|
continue;
|
|
2101
2093
|
}
|
|
2102
2094
|
for (const rel of existing) {
|
|
2103
|
-
const abs =
|
|
2095
|
+
const abs = path12.join(root, rel);
|
|
2104
2096
|
if (dryRun) {
|
|
2105
2097
|
log(`[dry-run] would remove stale ${displayPath(root, abs)} (${entry.id})`);
|
|
2106
2098
|
continue;
|
|
2107
2099
|
}
|
|
2108
2100
|
await fs12.remove(abs);
|
|
2109
|
-
await pruneEmptyDir(
|
|
2101
|
+
await pruneEmptyDir(path12.dirname(abs));
|
|
2110
2102
|
log(`Removed stale ${displayPath(root, abs)} (${entry.id})`);
|
|
2111
2103
|
}
|
|
2112
2104
|
}
|
|
@@ -2114,7 +2106,7 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
|
|
|
2114
2106
|
|
|
2115
2107
|
// src/commands/apply.ts
|
|
2116
2108
|
async function cacheHasItems() {
|
|
2117
|
-
const data = await readJson(
|
|
2109
|
+
const data = await readJson(path13.join(getCacheDir(), "manifest.json"));
|
|
2118
2110
|
return Array.isArray(data?.items) && data.items.length > 0;
|
|
2119
2111
|
}
|
|
2120
2112
|
async function runApply(options) {
|
|
@@ -2219,7 +2211,7 @@ async function runCatalogAudit() {
|
|
|
2219
2211
|
|
|
2220
2212
|
// src/commands/clone.ts
|
|
2221
2213
|
import { existsSync as existsSync2 } from "fs";
|
|
2222
|
-
import
|
|
2214
|
+
import path14 from "path";
|
|
2223
2215
|
|
|
2224
2216
|
// src/utils/exec.ts
|
|
2225
2217
|
import { execa } from "execa";
|
|
@@ -2272,9 +2264,9 @@ async function runClone(url, opts = {}) {
|
|
|
2272
2264
|
process.exitCode = 1;
|
|
2273
2265
|
return;
|
|
2274
2266
|
}
|
|
2275
|
-
const target =
|
|
2267
|
+
const target = path14.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
2276
2268
|
if (existsSync2(target)) {
|
|
2277
|
-
log(`\u2022 ${
|
|
2269
|
+
log(`\u2022 ${path14.basename(target)} already present at ${target} \u2014 skipped`);
|
|
2278
2270
|
return;
|
|
2279
2271
|
}
|
|
2280
2272
|
if (opts.dryRun) {
|
|
@@ -2290,399 +2282,12 @@ async function runClone(url, opts = {}) {
|
|
|
2290
2282
|
log(`\u2713 cloned ${url} \u2192 ${target}`);
|
|
2291
2283
|
}
|
|
2292
2284
|
|
|
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
|
-
}
|
|
2285
|
+
// src/commands/doctor.ts
|
|
2286
|
+
import path19 from "path";
|
|
2287
|
+
import fs15 from "fs-extra";
|
|
2683
2288
|
|
|
2684
2289
|
// src/scanner/scan-project.ts
|
|
2685
|
-
import
|
|
2290
|
+
import path18 from "path";
|
|
2686
2291
|
|
|
2687
2292
|
// src/utils/audit-checks.ts
|
|
2688
2293
|
function isRecord(v) {
|
|
@@ -2709,7 +2314,7 @@ function compareVersions(a, b) {
|
|
|
2709
2314
|
}
|
|
2710
2315
|
|
|
2711
2316
|
// src/scanner/detect-package-manager.ts
|
|
2712
|
-
import
|
|
2317
|
+
import path15 from "path";
|
|
2713
2318
|
import fs14 from "fs-extra";
|
|
2714
2319
|
function detectPackageManager(root, packageManagerField) {
|
|
2715
2320
|
const field = String(packageManagerField ?? "").trim();
|
|
@@ -2728,9 +2333,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
2728
2333
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
2729
2334
|
return "unknown";
|
|
2730
2335
|
}
|
|
2731
|
-
if (fs14.existsSync(
|
|
2732
|
-
if (fs14.existsSync(
|
|
2733
|
-
if (fs14.existsSync(
|
|
2336
|
+
if (fs14.existsSync(path15.join(root, "yarn.lock"))) return "yarn";
|
|
2337
|
+
if (fs14.existsSync(path15.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
2338
|
+
if (fs14.existsSync(path15.join(root, "package-lock.json"))) return "npm";
|
|
2734
2339
|
return "unknown";
|
|
2735
2340
|
}
|
|
2736
2341
|
|
|
@@ -2927,7 +2532,7 @@ function runDetection(ctx, rules = STACK_RULES) {
|
|
|
2927
2532
|
}
|
|
2928
2533
|
|
|
2929
2534
|
// src/scanner/detection.ts
|
|
2930
|
-
import
|
|
2535
|
+
import path16 from "path";
|
|
2931
2536
|
var UNSUPPORTED_MARKERS = {
|
|
2932
2537
|
"requirements.txt": "python",
|
|
2933
2538
|
"pyproject.toml": "python",
|
|
@@ -2981,68 +2586,14 @@ function finalizeRoles(registryRoles, deps, files) {
|
|
|
2981
2586
|
function collectUnsupportedSignals(files) {
|
|
2982
2587
|
return [
|
|
2983
2588
|
...new Set(
|
|
2984
|
-
files.map((f) => UNSUPPORTED_MARKERS[
|
|
2589
|
+
files.map((f) => UNSUPPORTED_MARKERS[path16.basename(f)]).filter((s) => Boolean(s))
|
|
2985
2590
|
)
|
|
2986
2591
|
].sort();
|
|
2987
2592
|
}
|
|
2988
2593
|
|
|
2989
2594
|
// src/scanner/render.ts
|
|
2990
2595
|
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
|
|
2596
|
+
import path17 from "path";
|
|
3046
2597
|
async function buildContentBlob(root, files) {
|
|
3047
2598
|
const candidates = files.filter(
|
|
3048
2599
|
(f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
|
|
@@ -3050,24 +2601,13 @@ async function buildContentBlob(root, files) {
|
|
|
3050
2601
|
const slice = candidates.slice(0, 300);
|
|
3051
2602
|
const parts = await mapWithConcurrency(slice, async (rel) => {
|
|
3052
2603
|
try {
|
|
3053
|
-
return await readFile(
|
|
2604
|
+
return await readFile(path17.join(root, rel), "utf8");
|
|
3054
2605
|
} catch {
|
|
3055
2606
|
return "";
|
|
3056
2607
|
}
|
|
3057
2608
|
});
|
|
3058
2609
|
return parts.join("\n");
|
|
3059
2610
|
}
|
|
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
2611
|
|
|
3072
2612
|
// src/scanner/write-sources-report.ts
|
|
3073
2613
|
function buildSourcesReport(items) {
|
|
@@ -3148,8 +2688,8 @@ var SAFE_FILES = [
|
|
|
3148
2688
|
"Gemfile"
|
|
3149
2689
|
];
|
|
3150
2690
|
async function scanProject(root) {
|
|
3151
|
-
const pkg = await readJson(
|
|
3152
|
-
const composer = await readJson(
|
|
2691
|
+
const pkg = await readJson(path18.join(root, "package.json"));
|
|
2692
|
+
const composer = await readJson(path18.join(root, "composer.json"));
|
|
3153
2693
|
const files = await listFiles(root, SAFE_FILES);
|
|
3154
2694
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
3155
2695
|
const deps = dependencySet(pkg, composer);
|
|
@@ -3178,7 +2718,7 @@ async function scanProject(root) {
|
|
|
3178
2718
|
const context = {
|
|
3179
2719
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3180
2720
|
root,
|
|
3181
|
-
repoName: String(pkg?.name ??
|
|
2721
|
+
repoName: String(pkg?.name ?? path18.basename(root)),
|
|
3182
2722
|
packageManager,
|
|
3183
2723
|
repoRoles: roles,
|
|
3184
2724
|
detectedStacks: stacks,
|
|
@@ -3202,74 +2742,6 @@ async function readContextOrScan(root) {
|
|
|
3202
2742
|
return scan;
|
|
3203
2743
|
}
|
|
3204
2744
|
|
|
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
2745
|
// src/update/npm-version.ts
|
|
3274
2746
|
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
3275
2747
|
async function fetchNpmVersionStatus(currentVersion) {
|
|
@@ -3343,12 +2815,7 @@ async function runDoctor(options) {
|
|
|
3343
2815
|
} else {
|
|
3344
2816
|
ok(`- HOOKS OK: ${hooks.message}`);
|
|
3345
2817
|
}
|
|
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");
|
|
2818
|
+
const rootClaudeMdPath = path19.join(root, "CLAUDE.md");
|
|
3352
2819
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
3353
2820
|
if (!rootClaudeMdContent) {
|
|
3354
2821
|
flag(
|
|
@@ -3410,8 +2877,8 @@ async function runDoctor(options) {
|
|
|
3410
2877
|
"haus apply --write --force"
|
|
3411
2878
|
);
|
|
3412
2879
|
} else {
|
|
3413
|
-
const cachePath =
|
|
3414
|
-
const bundledPath =
|
|
2880
|
+
const cachePath = path19.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
2881
|
+
const bundledPath = path19.join(
|
|
3415
2882
|
packageRoot(),
|
|
3416
2883
|
"library",
|
|
3417
2884
|
"global",
|
|
@@ -3477,7 +2944,7 @@ async function runDoctor(options) {
|
|
|
3477
2944
|
ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
|
|
3478
2945
|
}
|
|
3479
2946
|
}
|
|
3480
|
-
const pkgJson = await readJson(
|
|
2947
|
+
const pkgJson = await readJson(path19.join(packageRoot(), "package.json"));
|
|
3481
2948
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
3482
2949
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
3483
2950
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -3541,6 +3008,80 @@ function formatRecommendationHuman(rec) {
|
|
|
3541
3008
|
return lines.join("\n");
|
|
3542
3009
|
}
|
|
3543
3010
|
|
|
3011
|
+
// src/recommender/token-estimate.ts
|
|
3012
|
+
var TOKENS_PER_ITEM = 320;
|
|
3013
|
+
function estimateContextTokens(selectedCount) {
|
|
3014
|
+
return selectedCount * TOKENS_PER_ITEM;
|
|
3015
|
+
}
|
|
3016
|
+
function tokenReductionPct(selected, skipped) {
|
|
3017
|
+
const total = selected + skipped;
|
|
3018
|
+
if (total === 0) return 0;
|
|
3019
|
+
return Math.max(0, Math.round(skipped / total * 100));
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
// src/recommender/explain-recommendation.ts
|
|
3023
|
+
function normalizeRecommendation(input2) {
|
|
3024
|
+
const recommended = (input2.recommended ?? []).map((item) => {
|
|
3025
|
+
const normalizedReasons = item.reasons?.map((reason) => ({
|
|
3026
|
+
code: reason.code ?? "legacy-reason",
|
|
3027
|
+
message: reason.message ?? item.reason ?? "legacy recommendation reason",
|
|
3028
|
+
...reason.signal ? { signal: reason.signal } : {}
|
|
3029
|
+
})) ?? [{ code: "legacy-reason", message: item.reason ?? "legacy recommendation reason" }];
|
|
3030
|
+
return {
|
|
3031
|
+
id: item.id,
|
|
3032
|
+
type: item.type ?? "skill",
|
|
3033
|
+
reason: item.reason ?? normalizedReasons.map((reason) => reason.message).join(", "),
|
|
3034
|
+
reasons: normalizedReasons,
|
|
3035
|
+
selectionMode: item.selectionMode ?? "matched",
|
|
3036
|
+
install: item.install ?? true,
|
|
3037
|
+
tags: item.tags,
|
|
3038
|
+
ecosystem: item.ecosystem,
|
|
3039
|
+
tokenEstimate: item.tokenEstimate
|
|
3040
|
+
};
|
|
3041
|
+
});
|
|
3042
|
+
const skipped = (input2.skipped ?? []).map((item) => ({
|
|
3043
|
+
id: item.id,
|
|
3044
|
+
reason: item.reason ?? "legacy skipped reason",
|
|
3045
|
+
skipReasons: item.skipReasons?.map((reason) => ({
|
|
3046
|
+
code: reason.code ?? "legacy-skip-reason",
|
|
3047
|
+
message: reason.message ?? item.reason ?? "legacy skipped reason",
|
|
3048
|
+
...reason.signal ? { signal: reason.signal } : {}
|
|
3049
|
+
})) ?? [{ code: "legacy-skip-reason", message: item.reason ?? "legacy skipped reason" }]
|
|
3050
|
+
}));
|
|
3051
|
+
return {
|
|
3052
|
+
recommended,
|
|
3053
|
+
skipped,
|
|
3054
|
+
warnings: input2.warnings ?? [],
|
|
3055
|
+
estimatedContextTokens: input2.estimatedContextTokens ?? estimateContextTokens(recommended.length),
|
|
3056
|
+
selectedRules: input2.selectedRules ?? recommended.length,
|
|
3057
|
+
skippedRules: input2.skippedRules ?? skipped.length,
|
|
3058
|
+
estimatedTokenReductionPct: input2.estimatedTokenReductionPct ?? tokenReductionPct(recommended.length, skipped.length)
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
3061
|
+
function buildRecommendationExplanation(recommendation) {
|
|
3062
|
+
return {
|
|
3063
|
+
selected: recommendation.recommended.map((item) => ({
|
|
3064
|
+
id: item.id,
|
|
3065
|
+
selectionMode: item.selectionMode,
|
|
3066
|
+
reasons: item.reasons.map((reason) => reason.message)
|
|
3067
|
+
})),
|
|
3068
|
+
skipped: recommendation.skipped.map((item) => ({
|
|
3069
|
+
id: item.id,
|
|
3070
|
+
reasons: item.skipReasons.map((reason) => reason.message),
|
|
3071
|
+
reasonDetails: item.skipReasons.map((reason) => ({
|
|
3072
|
+
code: reason.code,
|
|
3073
|
+
message: reason.message,
|
|
3074
|
+
...reason.signal ? { signal: reason.signal } : {}
|
|
3075
|
+
}))
|
|
3076
|
+
})),
|
|
3077
|
+
stats: {
|
|
3078
|
+
selectedRules: recommendation.selectedRules,
|
|
3079
|
+
skippedRules: recommendation.skippedRules,
|
|
3080
|
+
estimatedTokenReductionPct: recommendation.estimatedTokenReductionPct
|
|
3081
|
+
}
|
|
3082
|
+
};
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3544
3085
|
// src/commands/explain-recommendation.ts
|
|
3545
3086
|
async function runExplainRecommendation(options) {
|
|
3546
3087
|
const root = process.cwd();
|
|
@@ -3620,7 +3161,7 @@ async function runGuard(kind, _options) {
|
|
|
3620
3161
|
}
|
|
3621
3162
|
|
|
3622
3163
|
// src/commands/init.ts
|
|
3623
|
-
import
|
|
3164
|
+
import path20 from "path";
|
|
3624
3165
|
import fs16 from "fs-extra";
|
|
3625
3166
|
|
|
3626
3167
|
// src/utils/prompts.ts
|
|
@@ -3968,7 +3509,7 @@ async function runSetupProject(options) {
|
|
|
3968
3509
|
// src/commands/init.ts
|
|
3969
3510
|
async function runInit(options) {
|
|
3970
3511
|
const root = process.cwd();
|
|
3971
|
-
const hausDir =
|
|
3512
|
+
const hausDir = path20.join(root, ".haus-workflow");
|
|
3972
3513
|
const alreadyInit = await fs16.pathExists(hausDir);
|
|
3973
3514
|
if (alreadyInit) {
|
|
3974
3515
|
log("Haus AI already initialized in this project.");
|
|
@@ -3981,7 +3522,7 @@ async function runInit(options) {
|
|
|
3981
3522
|
|
|
3982
3523
|
// src/install/apply.ts
|
|
3983
3524
|
import crypto2 from "crypto";
|
|
3984
|
-
import
|
|
3525
|
+
import path21 from "path";
|
|
3985
3526
|
import fs17 from "fs-extra";
|
|
3986
3527
|
|
|
3987
3528
|
// src/install/header.ts
|
|
@@ -4070,7 +3611,7 @@ function hashContent(content2) {
|
|
|
4070
3611
|
}
|
|
4071
3612
|
function sourceVersion() {
|
|
4072
3613
|
try {
|
|
4073
|
-
const pkgPath =
|
|
3614
|
+
const pkgPath = path21.join(packageRoot(), "package.json");
|
|
4074
3615
|
const pkg = JSON.parse(fs17.readFileSync(pkgPath, "utf8"));
|
|
4075
3616
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
4076
3617
|
} catch {
|
|
@@ -4078,32 +3619,32 @@ function sourceVersion() {
|
|
|
4078
3619
|
}
|
|
4079
3620
|
}
|
|
4080
3621
|
function globalSrcDir() {
|
|
4081
|
-
return
|
|
3622
|
+
return path21.join(packageRoot(), "library", "global");
|
|
4082
3623
|
}
|
|
4083
3624
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
4084
3625
|
const entries = [];
|
|
4085
|
-
const skillsDir =
|
|
3626
|
+
const skillsDir = path21.join(srcDir, "skills");
|
|
4086
3627
|
if (fs17.pathExistsSync(skillsDir)) {
|
|
4087
3628
|
for (const skillName of fs17.readdirSync(skillsDir)) {
|
|
4088
|
-
const skillFile =
|
|
3629
|
+
const skillFile = path21.join(skillsDir, skillName, "SKILL.md");
|
|
4089
3630
|
if (fs17.pathExistsSync(skillFile)) {
|
|
4090
3631
|
entries.push({
|
|
4091
3632
|
stableId: `skill.${skillName}`,
|
|
4092
|
-
srcRelPath:
|
|
4093
|
-
destPath:
|
|
3633
|
+
srcRelPath: path21.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
3634
|
+
destPath: path21.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
4094
3635
|
});
|
|
4095
3636
|
}
|
|
4096
3637
|
}
|
|
4097
3638
|
}
|
|
4098
|
-
const commandsDir =
|
|
3639
|
+
const commandsDir = path21.join(srcDir, "commands");
|
|
4099
3640
|
if (fs17.pathExistsSync(commandsDir)) {
|
|
4100
3641
|
for (const fileName of fs17.readdirSync(commandsDir)) {
|
|
4101
3642
|
if (!fileName.endsWith(".md")) continue;
|
|
4102
3643
|
const commandName = fileName.slice(0, -".md".length);
|
|
4103
3644
|
entries.push({
|
|
4104
3645
|
stableId: `command.${commandName}`,
|
|
4105
|
-
srcRelPath:
|
|
4106
|
-
destPath:
|
|
3646
|
+
srcRelPath: path21.join("library", "global", "commands", fileName),
|
|
3647
|
+
destPath: path21.join(claudeDir, "commands", fileName)
|
|
4107
3648
|
});
|
|
4108
3649
|
}
|
|
4109
3650
|
}
|
|
@@ -4127,7 +3668,7 @@ async function applyInstall(options = {}) {
|
|
|
4127
3668
|
};
|
|
4128
3669
|
const manifestFiles = [];
|
|
4129
3670
|
for (const entry of sourceFiles) {
|
|
4130
|
-
const srcPath =
|
|
3671
|
+
const srcPath = path21.join(packageRoot(), entry.srcRelPath);
|
|
4131
3672
|
const rawContent = await readText(srcPath);
|
|
4132
3673
|
if (rawContent === void 0) {
|
|
4133
3674
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -4183,7 +3724,7 @@ async function applyInstall(options = {}) {
|
|
|
4183
3724
|
schemaVersion: SCHEMA_VERSION2
|
|
4184
3725
|
});
|
|
4185
3726
|
}
|
|
4186
|
-
const fragmentPath =
|
|
3727
|
+
const fragmentPath = path21.join(srcDir, "settings-fragments", "hooks.json");
|
|
4187
3728
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
4188
3729
|
const settings = await readSettings();
|
|
4189
3730
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -4321,12 +3862,12 @@ async function runScan(options) {
|
|
|
4321
3862
|
}
|
|
4322
3863
|
|
|
4323
3864
|
// src/commands/undo.ts
|
|
4324
|
-
import
|
|
3865
|
+
import path22 from "path";
|
|
4325
3866
|
import fs18 from "fs-extra";
|
|
4326
3867
|
|
|
4327
3868
|
// src/claude/managed-paths.ts
|
|
4328
3869
|
var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md", "commands/haus-doctor.md"];
|
|
4329
|
-
var PROJECT_MANAGED_HAUS_REL = ["haus.lock.json"
|
|
3870
|
+
var PROJECT_MANAGED_HAUS_REL = ["haus.lock.json"];
|
|
4330
3871
|
function coreManagedAbsolutePaths(root) {
|
|
4331
3872
|
const claude = PROJECT_MANAGED_CLAUDE_REL.map((rel) => claudePath(root, rel));
|
|
4332
3873
|
const haus = PROJECT_MANAGED_HAUS_REL.map((rel) => hausPath(root, rel));
|
|
@@ -4339,7 +3880,7 @@ async function collectManagedPaths(root) {
|
|
|
4339
3880
|
const lock = await readJson(hausPath(root, "haus.lock.json"));
|
|
4340
3881
|
for (const row of lock ?? []) {
|
|
4341
3882
|
for (const rel of row.paths ?? []) {
|
|
4342
|
-
paths.add(
|
|
3883
|
+
paths.add(path22.resolve(root, rel));
|
|
4343
3884
|
}
|
|
4344
3885
|
}
|
|
4345
3886
|
const existing = [];
|
|
@@ -4355,7 +3896,7 @@ async function settingsHasHausContent(root) {
|
|
|
4355
3896
|
return settings._haus != null;
|
|
4356
3897
|
}
|
|
4357
3898
|
async function claudeMdHasHausBlock(root) {
|
|
4358
|
-
const filePath =
|
|
3899
|
+
const filePath = path22.join(root, "CLAUDE.md");
|
|
4359
3900
|
if (!await fs18.pathExists(filePath)) return false;
|
|
4360
3901
|
const text = await fs18.readFile(filePath, "utf8");
|
|
4361
3902
|
return text.includes(BLOCK_BEGIN);
|
|
@@ -4368,15 +3909,15 @@ async function stripProjectSettings(root) {
|
|
|
4368
3909
|
const hasContent = Object.keys(settings).length > 0;
|
|
4369
3910
|
if (hasContent) {
|
|
4370
3911
|
await writeProjectSettings(root, settings);
|
|
4371
|
-
log(`Stripped haus rules from ${
|
|
3912
|
+
log(`Stripped haus rules from ${path22.relative(root, settingsPath)} (user settings preserved).`);
|
|
4372
3913
|
return true;
|
|
4373
3914
|
}
|
|
4374
3915
|
await fs18.remove(settingsPath);
|
|
4375
|
-
log(`Removed ${
|
|
3916
|
+
log(`Removed ${path22.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
4376
3917
|
return true;
|
|
4377
3918
|
}
|
|
4378
3919
|
async function stripRootClaudeMd(root) {
|
|
4379
|
-
const filePath =
|
|
3920
|
+
const filePath = path22.join(root, "CLAUDE.md");
|
|
4380
3921
|
if (!await fs18.pathExists(filePath)) return false;
|
|
4381
3922
|
const prev = await fs18.readFile(filePath, "utf8");
|
|
4382
3923
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
@@ -4404,7 +3945,7 @@ async function runUndo(options) {
|
|
|
4404
3945
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
4405
3946
|
return;
|
|
4406
3947
|
}
|
|
4407
|
-
const relTargets = managed.map((p) =>
|
|
3948
|
+
const relTargets = managed.map((p) => path22.relative(root, p));
|
|
4408
3949
|
const summaryParts = [...relTargets];
|
|
4409
3950
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
4410
3951
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -4422,7 +3963,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
4422
3963
|
for (const abs of managed) {
|
|
4423
3964
|
if (!await fs18.pathExists(abs)) continue;
|
|
4424
3965
|
await fs18.remove(abs);
|
|
4425
|
-
log(`Removed ${
|
|
3966
|
+
log(`Removed ${path22.relative(root, abs)}`);
|
|
4426
3967
|
}
|
|
4427
3968
|
if (stripSettings) await stripProjectSettings(root);
|
|
4428
3969
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -4433,7 +3974,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
4433
3974
|
|
|
4434
3975
|
// src/install/uninstall.ts
|
|
4435
3976
|
import crypto3 from "crypto";
|
|
4436
|
-
import
|
|
3977
|
+
import path23 from "path";
|
|
4437
3978
|
import fs19 from "fs-extra";
|
|
4438
3979
|
async function runUninstall(options = {}) {
|
|
4439
3980
|
const { force = false } = options;
|
|
@@ -4463,14 +4004,14 @@ async function runUninstall(options = {}) {
|
|
|
4463
4004
|
continue;
|
|
4464
4005
|
}
|
|
4465
4006
|
await fs19.remove(entry.destPath);
|
|
4466
|
-
await pruneEmptyDir(
|
|
4007
|
+
await pruneEmptyDir(path23.dirname(entry.destPath));
|
|
4467
4008
|
result.deleted.push(entry.destPath);
|
|
4468
4009
|
}
|
|
4469
4010
|
const settings = await readSettings();
|
|
4470
4011
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
4471
4012
|
await writeSettings(stripped);
|
|
4472
4013
|
result.hooksStripped = true;
|
|
4473
|
-
const hausDir =
|
|
4014
|
+
const hausDir = path23.join(globalClaudeDir(), "haus");
|
|
4474
4015
|
const manifestPath2 = hausManifestPath();
|
|
4475
4016
|
if (fs19.pathExistsSync(manifestPath2)) {
|
|
4476
4017
|
await fs19.remove(manifestPath2);
|
|
@@ -4508,7 +4049,7 @@ async function runUninstallCommand(options) {
|
|
|
4508
4049
|
}
|
|
4509
4050
|
|
|
4510
4051
|
// src/commands/update.ts
|
|
4511
|
-
import
|
|
4052
|
+
import path25 from "path";
|
|
4512
4053
|
|
|
4513
4054
|
// src/update/diff-generated-files.ts
|
|
4514
4055
|
function diffGeneratedFiles() {
|
|
@@ -4535,7 +4076,7 @@ function summarizeLockDiff(before, after) {
|
|
|
4535
4076
|
|
|
4536
4077
|
// src/update/lockfile.ts
|
|
4537
4078
|
import { mkdir, readFile as readFile2, copyFile } from "fs/promises";
|
|
4538
|
-
import
|
|
4079
|
+
import path24 from "path";
|
|
4539
4080
|
async function checkLock(root) {
|
|
4540
4081
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
4541
4082
|
const hasValidVersions = lock.every(
|
|
@@ -4566,7 +4107,7 @@ async function applyLock(root) {
|
|
|
4566
4107
|
try {
|
|
4567
4108
|
const backupDir = hausPath(root, "backups");
|
|
4568
4109
|
await mkdir(backupDir, { recursive: true });
|
|
4569
|
-
await copyFile(lockPath,
|
|
4110
|
+
await copyFile(lockPath, path24.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
4570
4111
|
} catch {
|
|
4571
4112
|
}
|
|
4572
4113
|
const enriched = await Promise.all(
|
|
@@ -4588,7 +4129,7 @@ function diffLock(before, after) {
|
|
|
4588
4129
|
}
|
|
4589
4130
|
async function hasLocalOverrides(root) {
|
|
4590
4131
|
try {
|
|
4591
|
-
await readFile2(
|
|
4132
|
+
await readFile2(path24.join(root, ".claude", "settings.json"), "utf8");
|
|
4592
4133
|
return true;
|
|
4593
4134
|
} catch {
|
|
4594
4135
|
return false;
|
|
@@ -4600,7 +4141,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
|
4600
4141
|
async function runUpdate(options) {
|
|
4601
4142
|
const root = process.cwd();
|
|
4602
4143
|
if (options.check) {
|
|
4603
|
-
const pkgJson2 = await readJson(
|
|
4144
|
+
const pkgJson2 = await readJson(path25.join(packageRoot(), "package.json"));
|
|
4604
4145
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
4605
4146
|
const [status, npmVersion, latestCatalogTag, globalInstallDrift] = await Promise.all([
|
|
4606
4147
|
checkLock(root),
|
|
@@ -4630,7 +4171,7 @@ async function runUpdate(options) {
|
|
|
4630
4171
|
if (status.driftCount > 0) process.exitCode = 1;
|
|
4631
4172
|
return;
|
|
4632
4173
|
}
|
|
4633
|
-
const pkgJson = await readJson(
|
|
4174
|
+
const pkgJson = await readJson(path25.join(packageRoot(), "package.json"));
|
|
4634
4175
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
4635
4176
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
4636
4177
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -4708,7 +4249,7 @@ async function detectGlobalInstallDrift() {
|
|
|
4708
4249
|
|
|
4709
4250
|
// src/commands/validate-catalog.ts
|
|
4710
4251
|
import fs20 from "fs";
|
|
4711
|
-
import
|
|
4252
|
+
import path26 from "path";
|
|
4712
4253
|
function auditForbiddenStacks(items) {
|
|
4713
4254
|
const failures = [];
|
|
4714
4255
|
for (const item of items) {
|
|
@@ -4785,17 +4326,17 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4785
4326
|
const failures = [];
|
|
4786
4327
|
for (const item of items) {
|
|
4787
4328
|
if (!item.path) continue;
|
|
4788
|
-
const absPath =
|
|
4329
|
+
const absPath = path26.join(manifestDir, item.path);
|
|
4789
4330
|
if (item.type === "skill") {
|
|
4790
|
-
const skillMd =
|
|
4331
|
+
const skillMd = path26.join(absPath, "SKILL.md");
|
|
4791
4332
|
if (!fs20.existsSync(skillMd)) {
|
|
4792
|
-
failures.push(`${item.id}: missing ${
|
|
4333
|
+
failures.push(`${item.id}: missing ${path26.relative(manifestDir, skillMd)}`);
|
|
4793
4334
|
continue;
|
|
4794
4335
|
}
|
|
4795
4336
|
const text = fs20.readFileSync(skillMd, "utf8");
|
|
4796
4337
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
4797
4338
|
failures.push(
|
|
4798
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
4339
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path26.relative(manifestDir, skillMd)}`)
|
|
4799
4340
|
);
|
|
4800
4341
|
} else if (item.type === "agent") {
|
|
4801
4342
|
if (!fs20.existsSync(absPath)) {
|
|
@@ -4803,7 +4344,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4803
4344
|
continue;
|
|
4804
4345
|
}
|
|
4805
4346
|
const text = fs20.readFileSync(absPath, "utf8");
|
|
4806
|
-
const rel =
|
|
4347
|
+
const rel = path26.relative(manifestDir, absPath);
|
|
4807
4348
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
4808
4349
|
failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
|
|
4809
4350
|
} else if (item.type === "template") {
|
|
@@ -4818,7 +4359,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4818
4359
|
continue;
|
|
4819
4360
|
}
|
|
4820
4361
|
const text = fs20.readFileSync(absPath, "utf8");
|
|
4821
|
-
const rel =
|
|
4362
|
+
const rel = path26.relative(manifestDir, absPath);
|
|
4822
4363
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
4823
4364
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
4824
4365
|
}
|
|
@@ -4826,7 +4367,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4826
4367
|
return failures;
|
|
4827
4368
|
}
|
|
4828
4369
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
4829
|
-
const rel =
|
|
4370
|
+
const rel = path26.relative(manifestDir, absPath);
|
|
4830
4371
|
const text = fs20.readFileSync(absPath, "utf8");
|
|
4831
4372
|
const failures = [];
|
|
4832
4373
|
const lines = text.split(/\r?\n/);
|
|
@@ -4849,11 +4390,11 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4849
4390
|
const failures = [];
|
|
4850
4391
|
const dirs = ["skills", "agents", "templates", "commands"];
|
|
4851
4392
|
for (const dir of dirs) {
|
|
4852
|
-
const abs =
|
|
4393
|
+
const abs = path26.join(manifestDir, dir);
|
|
4853
4394
|
if (!fs20.existsSync(abs)) continue;
|
|
4854
4395
|
walkMd(abs, (file) => {
|
|
4855
4396
|
const text = fs20.readFileSync(file, "utf8");
|
|
4856
|
-
const rel =
|
|
4397
|
+
const rel = path26.relative(manifestDir, file);
|
|
4857
4398
|
const lines = text.split(/\r?\n/);
|
|
4858
4399
|
for (let i = 0; i < lines.length; i++) {
|
|
4859
4400
|
const line2 = lines[i] ?? "";
|
|
@@ -4873,7 +4414,7 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4873
4414
|
}
|
|
4874
4415
|
function walkMd(dir, fn) {
|
|
4875
4416
|
for (const entry of fs20.readdirSync(dir, { withFileTypes: true })) {
|
|
4876
|
-
const full =
|
|
4417
|
+
const full = path26.join(dir, entry.name);
|
|
4877
4418
|
if (entry.isDirectory()) walkMd(full, fn);
|
|
4878
4419
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
4879
4420
|
}
|
|
@@ -4884,8 +4425,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
4884
4425
|
process.exitCode = 1;
|
|
4885
4426
|
return;
|
|
4886
4427
|
}
|
|
4887
|
-
const abs =
|
|
4888
|
-
const manifestDir =
|
|
4428
|
+
const abs = path26.resolve(process.cwd(), manifestPath2);
|
|
4429
|
+
const manifestDir = path26.dirname(abs);
|
|
4889
4430
|
const data = await readJson(abs);
|
|
4890
4431
|
if (!data?.items) {
|
|
4891
4432
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -4915,7 +4456,7 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
4915
4456
|
|
|
4916
4457
|
// src/commands/workspace.ts
|
|
4917
4458
|
import { existsSync as existsSync5, statSync as statSync2 } from "fs";
|
|
4918
|
-
import
|
|
4459
|
+
import path33 from "path";
|
|
4919
4460
|
|
|
4920
4461
|
// src/commands/workspace/aggregate.ts
|
|
4921
4462
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -4969,7 +4510,7 @@ ${summaries.map(
|
|
|
4969
4510
|
}
|
|
4970
4511
|
|
|
4971
4512
|
// src/commands/workspace/config.ts
|
|
4972
|
-
import
|
|
4513
|
+
import path27 from "path";
|
|
4973
4514
|
import YAML from "yaml";
|
|
4974
4515
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
4975
4516
|
function parseWorkspaceConfig(text) {
|
|
@@ -4992,11 +4533,11 @@ function parseWorkspaceConfig(text) {
|
|
|
4992
4533
|
};
|
|
4993
4534
|
}
|
|
4994
4535
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
4995
|
-
return parseWorkspaceConfig(await readText(
|
|
4536
|
+
return parseWorkspaceConfig(await readText(path27.join(workspaceRoot, WORKSPACE_FILE)));
|
|
4996
4537
|
}
|
|
4997
4538
|
|
|
4998
4539
|
// src/commands/workspace/discover.ts
|
|
4999
|
-
import
|
|
4540
|
+
import path28 from "path";
|
|
5000
4541
|
import fg4 from "fast-glob";
|
|
5001
4542
|
import YAML2 from "yaml";
|
|
5002
4543
|
var DEFAULT_MAX_DEPTH = 3;
|
|
@@ -5024,8 +4565,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
5024
4565
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
5025
4566
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
5026
4567
|
for (const match of matches) {
|
|
5027
|
-
const base =
|
|
5028
|
-
const dir =
|
|
4568
|
+
const base = path28.posix.basename(match);
|
|
4569
|
+
const dir = path28.posix.dirname(match);
|
|
5029
4570
|
const owner = dir === "." ? "." : dir;
|
|
5030
4571
|
if (base === ".git") gitDirs.add(owner);
|
|
5031
4572
|
else manifestDirs.add(owner);
|
|
@@ -5041,9 +4582,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
5041
4582
|
}
|
|
5042
4583
|
repoRoots.sort((a, b) => a.localeCompare(b));
|
|
5043
4584
|
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 :
|
|
4585
|
+
const absDir = path28.resolve(workspaceRoot, relDir);
|
|
4586
|
+
const pkg = await readJson(path28.join(absDir, "package.json"));
|
|
4587
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path28.basename(relDir === "." ? workspaceRoot : absDir);
|
|
5047
4588
|
let role = "auto";
|
|
5048
4589
|
try {
|
|
5049
4590
|
const scan = await scanProject(absDir);
|
|
@@ -5078,15 +4619,15 @@ function mergeWorkspaceConfig(existing, discovered, opts = {}) {
|
|
|
5078
4619
|
relationships: existing?.relationships ?? []
|
|
5079
4620
|
};
|
|
5080
4621
|
}
|
|
5081
|
-
function renderWorkspaceYaml(
|
|
4622
|
+
function renderWorkspaceYaml(config) {
|
|
5082
4623
|
return YAML2.stringify({
|
|
5083
|
-
client:
|
|
5084
|
-
repos:
|
|
5085
|
-
relationships:
|
|
4624
|
+
client: config.client,
|
|
4625
|
+
repos: config.repos.map((r) => ({ name: r.name, path: r.path, role: r.role ?? "auto" })),
|
|
4626
|
+
relationships: config.relationships
|
|
5086
4627
|
});
|
|
5087
4628
|
}
|
|
5088
4629
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
5089
|
-
const yamlPath =
|
|
4630
|
+
const yamlPath = path28.join(workspaceRoot, "haus.workspace.yaml");
|
|
5090
4631
|
const existingText = await readText(yamlPath);
|
|
5091
4632
|
const existing = parseWorkspaceConfig(existingText);
|
|
5092
4633
|
if (existingText && !existing) {
|
|
@@ -5120,18 +4661,18 @@ async function runDiscover(workspaceRoot, opts = {}) {
|
|
|
5120
4661
|
|
|
5121
4662
|
// src/commands/workspace/doctor.ts
|
|
5122
4663
|
import { existsSync as existsSync3 } from "fs";
|
|
5123
|
-
import
|
|
4664
|
+
import path30 from "path";
|
|
5124
4665
|
|
|
5125
4666
|
// src/commands/workspace/manifest.ts
|
|
5126
4667
|
import { readFileSync as readFileSync3 } from "fs";
|
|
5127
|
-
import
|
|
4668
|
+
import path29 from "path";
|
|
5128
4669
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
5129
4670
|
function manifestPath(workspaceRoot) {
|
|
5130
4671
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
5131
4672
|
}
|
|
5132
4673
|
function hausVersion() {
|
|
5133
4674
|
try {
|
|
5134
|
-
const pkg = JSON.parse(readFileSync3(
|
|
4675
|
+
const pkg = JSON.parse(readFileSync3(path29.join(packageRoot(), "package.json"), "utf8"));
|
|
5135
4676
|
return pkg.version ?? "0.0.0";
|
|
5136
4677
|
} catch {
|
|
5137
4678
|
return "0.0.0";
|
|
@@ -5169,7 +4710,7 @@ async function writeWorkspaceManifest(workspaceRoot, manifest) {
|
|
|
5169
4710
|
|
|
5170
4711
|
// src/commands/workspace/doctor.ts
|
|
5171
4712
|
async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
5172
|
-
const
|
|
4713
|
+
const config = await readWorkspaceConfig(workspaceRoot);
|
|
5173
4714
|
const manifest = await readManifest2(workspaceRoot);
|
|
5174
4715
|
const currentVersion = hausVersion();
|
|
5175
4716
|
const drift = [];
|
|
@@ -5179,7 +4720,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
5179
4720
|
drift.push(item);
|
|
5180
4721
|
detail.push({ stream: "warn", text: `- ${item.repo}: ${item.detail}` });
|
|
5181
4722
|
};
|
|
5182
|
-
if (!
|
|
4723
|
+
if (!config) {
|
|
5183
4724
|
flag({
|
|
5184
4725
|
repo: "(workspace)",
|
|
5185
4726
|
kind: "no-config",
|
|
@@ -5196,8 +4737,8 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
5196
4737
|
return emit({ workspaceRoot, manifest, drift, detail, json: opts.json });
|
|
5197
4738
|
}
|
|
5198
4739
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
5199
|
-
for (const repo of
|
|
5200
|
-
const repoRoot =
|
|
4740
|
+
for (const repo of config.repos) {
|
|
4741
|
+
const repoRoot = path30.resolve(workspaceRoot, repo.path);
|
|
5201
4742
|
const entry = manifestByName.get(repo.name);
|
|
5202
4743
|
if (!entry) {
|
|
5203
4744
|
flag({
|
|
@@ -5271,10 +4812,10 @@ function emit(args) {
|
|
|
5271
4812
|
|
|
5272
4813
|
// src/commands/workspace/setup.ts
|
|
5273
4814
|
import { existsSync as existsSync4, statSync } from "fs";
|
|
5274
|
-
import
|
|
4815
|
+
import path32 from "path";
|
|
5275
4816
|
|
|
5276
4817
|
// src/claude/write-workspace-claude-md.ts
|
|
5277
|
-
import
|
|
4818
|
+
import path31 from "path";
|
|
5278
4819
|
import fs21 from "fs-extra";
|
|
5279
4820
|
function buildWorkspaceImportBlock(client, members) {
|
|
5280
4821
|
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
@@ -5293,7 +4834,7 @@ ${BLOCK_END}`;
|
|
|
5293
4834
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
5294
4835
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
5295
4836
|
const dryRun = opts.dryRun ?? false;
|
|
5296
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
4837
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path31.join(workspaceRoot, "CLAUDE.md");
|
|
5297
4838
|
const prev = await fs21.pathExists(filePath) ? await fs21.readFile(filePath, "utf8") : "";
|
|
5298
4839
|
const next = opts.collision ? `${block}
|
|
5299
4840
|
` : injectHausBlock(prev, block);
|
|
@@ -5319,37 +4860,37 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
5319
4860
|
|
|
5320
4861
|
// src/commands/workspace/setup.ts
|
|
5321
4862
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
5322
|
-
let dir =
|
|
4863
|
+
let dir = path32.resolve(start);
|
|
5323
4864
|
for (; ; ) {
|
|
5324
|
-
if (existsSync4(
|
|
5325
|
-
const parent =
|
|
5326
|
-
if (parent === dir) return
|
|
4865
|
+
if (existsSync4(path32.join(dir, WORKSPACE_FILE))) return dir;
|
|
4866
|
+
const parent = path32.dirname(dir);
|
|
4867
|
+
if (parent === dir) return path32.resolve(start);
|
|
5327
4868
|
dir = parent;
|
|
5328
4869
|
}
|
|
5329
4870
|
}
|
|
5330
4871
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
5331
|
-
return
|
|
4872
|
+
return path32.resolve(workspaceRoot, repoPath) === path32.resolve(workspaceRoot);
|
|
5332
4873
|
}
|
|
5333
4874
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
5334
4875
|
const apply = options.write ?? false;
|
|
5335
|
-
const configText = await readText(
|
|
4876
|
+
const configText = await readText(path32.join(workspaceRoot, WORKSPACE_FILE));
|
|
5336
4877
|
if (!configText) {
|
|
5337
4878
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
5338
4879
|
process.exitCode = 1;
|
|
5339
4880
|
return { workspaceRoot, statuses: [], written: [] };
|
|
5340
4881
|
}
|
|
5341
|
-
const
|
|
5342
|
-
if (!
|
|
4882
|
+
const config = parseWorkspaceConfig(configText);
|
|
4883
|
+
if (!config || config.repos.length === 0) {
|
|
5343
4884
|
error(`No repos configured in ${WORKSPACE_FILE}.`);
|
|
5344
4885
|
process.exitCode = 1;
|
|
5345
4886
|
return { workspaceRoot, statuses: [], written: [] };
|
|
5346
4887
|
}
|
|
5347
4888
|
const onlySet = options.only && options.only.length > 0 ? new Set(options.only) : void 0;
|
|
5348
|
-
const repos = onlySet ?
|
|
4889
|
+
const repos = onlySet ? config.repos.filter((r) => onlySet.has(r.name)) : config.repos;
|
|
5349
4890
|
const statuses = [];
|
|
5350
4891
|
const aggregateInputs = [];
|
|
5351
4892
|
for (const repo of repos) {
|
|
5352
|
-
const repoRoot =
|
|
4893
|
+
const repoRoot = path32.resolve(workspaceRoot, repo.path);
|
|
5353
4894
|
log(`
|
|
5354
4895
|
\u2192 ${repo.name} (${repo.path})`);
|
|
5355
4896
|
try {
|
|
@@ -5386,18 +4927,18 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
5386
4927
|
}
|
|
5387
4928
|
const written = [];
|
|
5388
4929
|
if (apply && aggregateInputs.length > 0) {
|
|
5389
|
-
const collision =
|
|
4930
|
+
const collision = config.repos.some((r) => isRootRepo(workspaceRoot, r.path));
|
|
5390
4931
|
if (!options.dryRun) {
|
|
5391
4932
|
const artifacts = await writeWorkspaceArtifacts(
|
|
5392
4933
|
workspaceRoot,
|
|
5393
4934
|
aggregateInputs,
|
|
5394
|
-
|
|
4935
|
+
config.relationships
|
|
5395
4936
|
);
|
|
5396
4937
|
written.push(...artifacts);
|
|
5397
4938
|
}
|
|
5398
4939
|
const docPath = await writeWorkspaceClaudeMd(workspaceRoot, {
|
|
5399
|
-
client:
|
|
5400
|
-
members:
|
|
4940
|
+
client: config.client,
|
|
4941
|
+
members: config.repos.map((r) => ({ name: r.name, path: r.path })),
|
|
5401
4942
|
collision,
|
|
5402
4943
|
dryRun: options.dryRun
|
|
5403
4944
|
});
|
|
@@ -5413,11 +4954,11 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
5413
4954
|
}
|
|
5414
4955
|
const priorByName = new Map((prior?.repos ?? []).map((r) => [r.name, r]));
|
|
5415
4956
|
const manifestRepos = [];
|
|
5416
|
-
for (const repo of
|
|
4957
|
+
for (const repo of config.repos) {
|
|
5417
4958
|
const status = statusByName.get(repo.name);
|
|
5418
4959
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
5419
4960
|
if (status?.status === "ok") {
|
|
5420
|
-
const lock = await checkLock(
|
|
4961
|
+
const lock = await checkLock(path32.resolve(workspaceRoot, repo.path));
|
|
5421
4962
|
manifestRepos.push({
|
|
5422
4963
|
name: repo.name,
|
|
5423
4964
|
path: repo.path,
|
|
@@ -5460,7 +5001,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
5460
5001
|
);
|
|
5461
5002
|
}
|
|
5462
5003
|
}
|
|
5463
|
-
const manifest = buildManifest2({ client:
|
|
5004
|
+
const manifest = buildManifest2({ client: config.client, repos: manifestRepos });
|
|
5464
5005
|
const manifestFile = await writeWorkspaceManifest(workspaceRoot, manifest);
|
|
5465
5006
|
written.push(manifestFile);
|
|
5466
5007
|
}
|
|
@@ -5497,35 +5038,35 @@ relationships: []
|
|
|
5497
5038
|
log("Workspace initialized.");
|
|
5498
5039
|
}
|
|
5499
5040
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
5500
|
-
const configText = await readText(
|
|
5041
|
+
const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
|
|
5501
5042
|
if (!configText) {
|
|
5502
5043
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
5503
5044
|
process.exitCode = 1;
|
|
5504
5045
|
return;
|
|
5505
5046
|
}
|
|
5506
|
-
const
|
|
5507
|
-
if (!
|
|
5047
|
+
const config = parseWorkspaceConfig(configText);
|
|
5048
|
+
if (!config) {
|
|
5508
5049
|
error(
|
|
5509
5050
|
`Malformed ${WORKSPACE_FILE}. Fix the YAML or re-run \`haus workspace discover --write\`.`
|
|
5510
5051
|
);
|
|
5511
5052
|
process.exitCode = 1;
|
|
5512
5053
|
return;
|
|
5513
5054
|
}
|
|
5514
|
-
if (
|
|
5055
|
+
if (config.repos.length === 0) {
|
|
5515
5056
|
error(`No repos configured in ${WORKSPACE_FILE}.`);
|
|
5516
5057
|
process.exitCode = 1;
|
|
5517
5058
|
return;
|
|
5518
5059
|
}
|
|
5519
5060
|
const inputs = [];
|
|
5520
|
-
for (const repo of
|
|
5521
|
-
const repoRoot =
|
|
5061
|
+
for (const repo of config.repos) {
|
|
5062
|
+
const repoRoot = path33.resolve(workspaceRoot, repo.path);
|
|
5522
5063
|
if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
|
|
5523
5064
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
5524
5065
|
}
|
|
5525
5066
|
const result = await scanProject(repoRoot);
|
|
5526
5067
|
inputs.push({ name: repo.name, path: repo.path, context: result });
|
|
5527
5068
|
}
|
|
5528
|
-
const written = await writeWorkspaceArtifacts(workspaceRoot, inputs,
|
|
5069
|
+
const written = await writeWorkspaceArtifacts(workspaceRoot, inputs, config.relationships);
|
|
5529
5070
|
if (opts.json) {
|
|
5530
5071
|
log(JSON.stringify({ written }, null, 2));
|
|
5531
5072
|
} else {
|
|
@@ -5568,7 +5109,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
5568
5109
|
// src/cli.ts
|
|
5569
5110
|
function cliVersion() {
|
|
5570
5111
|
try {
|
|
5571
|
-
const pkgPath =
|
|
5112
|
+
const pkgPath = path34.join(packageRoot(), "package.json");
|
|
5572
5113
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5573
5114
|
return pkg.version ?? "0.0.0";
|
|
5574
5115
|
} catch {
|
|
@@ -5578,7 +5119,7 @@ function cliVersion() {
|
|
|
5578
5119
|
var program = new Command();
|
|
5579
5120
|
function validateRuntimeNodeVersion() {
|
|
5580
5121
|
try {
|
|
5581
|
-
const pkgPath =
|
|
5122
|
+
const pkgPath = path34.join(packageRoot(), "package.json");
|
|
5582
5123
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5583
5124
|
const requiredRange = pkg.engines?.node;
|
|
5584
5125
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -5605,7 +5146,6 @@ program.command("apply").option("--dry-run").option("--write").option("--select"
|
|
|
5605
5146
|
).option("--force", "Overwrite user-modified managed workflow files").action(runApply);
|
|
5606
5147
|
program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
|
|
5607
5148
|
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
5149
|
program.command("init").option("--json").action(runInit);
|
|
5610
5150
|
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
5151
|
(url, dir, opts) => runClone(url, { dir, dryRun: opts.dryRun })
|
|
@@ -5619,10 +5159,6 @@ program.command("uninstall").option("--force").action(runUninstallCommand);
|
|
|
5619
5159
|
var guard = program.command("guard");
|
|
5620
5160
|
guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
|
|
5621
5161
|
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
5162
|
var workspace = program.command("workspace");
|
|
5627
5163
|
workspace.command("init").action(() => runWorkspace("init"));
|
|
5628
5164
|
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));
|