@haus-tech/haus-workflow 0.30.0 → 0.31.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 +13 -0
- package/README.md +4 -0
- package/dist/cli.js +483 -244
- package/library/catalog/manifest.json +53 -21
- package/library/catalog/validation-rules.json +1 -1
- package/library/global/commands/haus-clone.md +4 -0
- package/library/global/commands/haus-cloneandsetup.md +4 -0
- package/library/global/commands/haus-doctor.md +4 -0
- package/library/global/commands/haus-fix.md +4 -0
- package/library/global/commands/haus-setup.md +33 -5
- package/library/global/skills/haus-workflow/SKILL.md +30 -0
- 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 "node:fs";
|
|
5
|
-
import
|
|
5
|
+
import path37 from "node:path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/commands/apply.ts
|
|
9
|
-
import
|
|
9
|
+
import path14 from "node:path";
|
|
10
10
|
import checkbox from "@inquirer/checkbox";
|
|
11
11
|
|
|
12
12
|
// src/catalog/remote-catalog.ts
|
|
@@ -182,7 +182,7 @@ var validation_rules_default = {
|
|
|
182
182
|
"vendure3",
|
|
183
183
|
"nestjs",
|
|
184
184
|
"graphql",
|
|
185
|
-
"
|
|
185
|
+
"nx",
|
|
186
186
|
"turbo",
|
|
187
187
|
"nextjs",
|
|
188
188
|
"react19",
|
|
@@ -1043,8 +1043,8 @@ async function getCacheManifestAge() {
|
|
|
1043
1043
|
}
|
|
1044
1044
|
|
|
1045
1045
|
// src/claude/write-claude-files.ts
|
|
1046
|
-
import
|
|
1047
|
-
import
|
|
1046
|
+
import path13 from "node:path";
|
|
1047
|
+
import fs13 from "fs-extra";
|
|
1048
1048
|
|
|
1049
1049
|
// src/catalog/load-catalog.ts
|
|
1050
1050
|
import path4 from "node:path";
|
|
@@ -1660,8 +1660,8 @@ function buildDenyRules() {
|
|
|
1660
1660
|
for (const command of DENY_COMMANDS) {
|
|
1661
1661
|
rules.push(`Bash(${command}:*)`);
|
|
1662
1662
|
}
|
|
1663
|
-
for (const
|
|
1664
|
-
const pattern = DENY_DIRS.has(
|
|
1663
|
+
for (const path38 of DENY_PATHS) {
|
|
1664
|
+
const pattern = DENY_DIRS.has(path38) ? `${path38}/**` : path38;
|
|
1665
1665
|
for (const tool of FILE_TOOLS) {
|
|
1666
1666
|
rules.push(`${tool}(${pattern})`);
|
|
1667
1667
|
}
|
|
@@ -1851,9 +1851,74 @@ async function verifyProjectSettingsHooksContract(root) {
|
|
|
1851
1851
|
return { ok: true, message: "settings.json carries required haus hook contract." };
|
|
1852
1852
|
}
|
|
1853
1853
|
|
|
1854
|
-
// src/claude/write-
|
|
1854
|
+
// src/claude/write-prettierignore.ts
|
|
1855
1855
|
import path9 from "node:path";
|
|
1856
1856
|
import fs8 from "fs-extra";
|
|
1857
|
+
var PRETTIERIGNORE_BEGIN = "# HAUS:BEGIN haus-managed v=1";
|
|
1858
|
+
var PRETTIERIGNORE_END = "# HAUS:END haus-managed";
|
|
1859
|
+
var IGNORED_PATHS = [".haus-workflow/"];
|
|
1860
|
+
function buildPrettierIgnoreBlock() {
|
|
1861
|
+
return [
|
|
1862
|
+
PRETTIERIGNORE_BEGIN,
|
|
1863
|
+
"# haus-owned files \u2014 do not reformat (keeps tamper-detection hashes stable)",
|
|
1864
|
+
...IGNORED_PATHS,
|
|
1865
|
+
PRETTIERIGNORE_END
|
|
1866
|
+
].join("\n");
|
|
1867
|
+
}
|
|
1868
|
+
function findLineMarker(content2, marker, from = 0) {
|
|
1869
|
+
let idx = content2.indexOf(marker, from);
|
|
1870
|
+
while (idx !== -1) {
|
|
1871
|
+
const lineStart = idx === 0 || content2[idx - 1] === "\n";
|
|
1872
|
+
const after = idx + marker.length;
|
|
1873
|
+
const lineEnd = after === content2.length || content2[after] === "\n" || content2[after] === "\r";
|
|
1874
|
+
if (lineStart && lineEnd) return { start: idx, end: after };
|
|
1875
|
+
idx = content2.indexOf(marker, idx + marker.length);
|
|
1876
|
+
}
|
|
1877
|
+
return null;
|
|
1878
|
+
}
|
|
1879
|
+
function findBlockRange(content2) {
|
|
1880
|
+
const begin = findLineMarker(content2, PRETTIERIGNORE_BEGIN);
|
|
1881
|
+
if (!begin) return null;
|
|
1882
|
+
const end = findLineMarker(content2, PRETTIERIGNORE_END, begin.end);
|
|
1883
|
+
if (!end) return null;
|
|
1884
|
+
return { start: begin.start, end: end.end };
|
|
1885
|
+
}
|
|
1886
|
+
function injectPrettierIgnoreBlock(existing, block) {
|
|
1887
|
+
const range = findBlockRange(existing);
|
|
1888
|
+
if (range) {
|
|
1889
|
+
const before = existing.slice(0, range.start);
|
|
1890
|
+
const after = existing.slice(range.end);
|
|
1891
|
+
return `${before}${block}${after}`.replace(/\n*$/, "\n");
|
|
1892
|
+
}
|
|
1893
|
+
const loneBegin = findLineMarker(existing, PRETTIERIGNORE_BEGIN);
|
|
1894
|
+
if (loneBegin) {
|
|
1895
|
+
const before = existing.slice(0, loneBegin.start).trimEnd();
|
|
1896
|
+
if (before.length === 0) return `${block}
|
|
1897
|
+
`;
|
|
1898
|
+
return `${before}
|
|
1899
|
+
|
|
1900
|
+
${block}
|
|
1901
|
+
`;
|
|
1902
|
+
}
|
|
1903
|
+
const trimmed = existing.trimEnd();
|
|
1904
|
+
if (trimmed.length === 0) return `${block}
|
|
1905
|
+
`;
|
|
1906
|
+
return `${trimmed}
|
|
1907
|
+
|
|
1908
|
+
${block}
|
|
1909
|
+
`;
|
|
1910
|
+
}
|
|
1911
|
+
async function writePrettierIgnore(root, dryRun) {
|
|
1912
|
+
const filePath = path9.join(root, ".prettierignore");
|
|
1913
|
+
const prev = await fs8.pathExists(filePath) ? await fs8.readFile(filePath, "utf8") : "";
|
|
1914
|
+
const next = injectPrettierIgnoreBlock(prev, buildPrettierIgnoreBlock());
|
|
1915
|
+
await writeManagedText(root, filePath, next, dryRun);
|
|
1916
|
+
return filePath;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// src/claude/write-root-claude-md.ts
|
|
1920
|
+
import path10 from "node:path";
|
|
1921
|
+
import fs9 from "fs-extra";
|
|
1857
1922
|
var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
|
|
1858
1923
|
var BLOCK_END = "<!-- HAUS:END haus-imports -->";
|
|
1859
1924
|
var IMPORT_CONTENT = `@.haus-workflow/WORKFLOW.md
|
|
@@ -1863,7 +1928,7 @@ function buildImportBlock() {
|
|
|
1863
1928
|
${IMPORT_CONTENT}
|
|
1864
1929
|
${BLOCK_END}`;
|
|
1865
1930
|
}
|
|
1866
|
-
function
|
|
1931
|
+
function findLineMarker2(content2, marker, from = 0) {
|
|
1867
1932
|
let idx = content2.indexOf(marker, from);
|
|
1868
1933
|
while (idx !== -1) {
|
|
1869
1934
|
const lineStart = idx === 0 || content2[idx - 1] === "\n";
|
|
@@ -1875,9 +1940,9 @@ function findLineMarker(content2, marker, from = 0) {
|
|
|
1875
1940
|
return null;
|
|
1876
1941
|
}
|
|
1877
1942
|
function findImportBlockRange(content2) {
|
|
1878
|
-
const begin =
|
|
1943
|
+
const begin = findLineMarker2(content2, BLOCK_BEGIN);
|
|
1879
1944
|
if (!begin) return null;
|
|
1880
|
-
const end =
|
|
1945
|
+
const end = findLineMarker2(content2, BLOCK_END, begin.end);
|
|
1881
1946
|
if (!end) return null;
|
|
1882
1947
|
return { beginStart: begin.start, beginEnd: begin.end, endStart: end.start, endEnd: end.end };
|
|
1883
1948
|
}
|
|
@@ -1897,7 +1962,7 @@ function injectHausBlock(existing, block) {
|
|
|
1897
1962
|
const after = existing.slice(range.endEnd);
|
|
1898
1963
|
return `${before}${block}${after}`;
|
|
1899
1964
|
}
|
|
1900
|
-
const loneBegin =
|
|
1965
|
+
const loneBegin = findLineMarker2(existing, BLOCK_BEGIN);
|
|
1901
1966
|
if (loneBegin) {
|
|
1902
1967
|
const before = existing.slice(0, loneBegin.start).trimEnd();
|
|
1903
1968
|
if (before.length === 0) return `${block}
|
|
@@ -1918,21 +1983,21 @@ ${block}
|
|
|
1918
1983
|
`;
|
|
1919
1984
|
}
|
|
1920
1985
|
async function writeRootClaudeMd(root, dryRun) {
|
|
1921
|
-
const filePath =
|
|
1986
|
+
const filePath = path10.join(root, "CLAUDE.md");
|
|
1922
1987
|
const block = buildImportBlock();
|
|
1923
|
-
const prev = await
|
|
1988
|
+
const prev = await fs9.pathExists(filePath) ? await fs9.readFile(filePath, "utf8") : "";
|
|
1924
1989
|
const next = injectHausBlock(prev, block);
|
|
1925
1990
|
await writeManagedText(root, filePath, next, dryRun);
|
|
1926
1991
|
return filePath;
|
|
1927
1992
|
}
|
|
1928
1993
|
|
|
1929
1994
|
// src/claude/write-workflow-config.ts
|
|
1930
|
-
import
|
|
1931
|
-
import
|
|
1995
|
+
import path12 from "node:path";
|
|
1996
|
+
import fs11 from "fs-extra";
|
|
1932
1997
|
|
|
1933
1998
|
// src/claude/derive-workflow-config.ts
|
|
1934
|
-
import
|
|
1935
|
-
import
|
|
1999
|
+
import path11 from "node:path";
|
|
2000
|
+
import fs10 from "fs-extra";
|
|
1936
2001
|
function binCmd(pm, bin, args) {
|
|
1937
2002
|
const tail = args ? ` ${args}` : "";
|
|
1938
2003
|
if (pm === "yarn") return `yarn ${bin}${tail}`;
|
|
@@ -1941,7 +2006,7 @@ function binCmd(pm, bin, args) {
|
|
|
1941
2006
|
}
|
|
1942
2007
|
async function deriveWorkflowConfig(root, ctx) {
|
|
1943
2008
|
const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
|
|
1944
|
-
const pkg = await readJson(
|
|
2009
|
+
const pkg = await readJson(path11.join(root, "package.json"));
|
|
1945
2010
|
const scripts = pkg?.scripts ?? {};
|
|
1946
2011
|
const deps = new Set(ctx.dependencies);
|
|
1947
2012
|
const stacks = Object.values(ctx.detectedStacks ?? {}).flat();
|
|
@@ -1951,7 +2016,7 @@ async function deriveWorkflowConfig(root, ctx) {
|
|
|
1951
2016
|
return null;
|
|
1952
2017
|
};
|
|
1953
2018
|
const hasDep = (name) => deps.has(name);
|
|
1954
|
-
const exists = (rel) =>
|
|
2019
|
+
const exists = (rel) => fs10.pathExistsSync(path11.join(root, rel));
|
|
1955
2020
|
const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
|
|
1956
2021
|
const hasCypress = hasDep("cypress");
|
|
1957
2022
|
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;
|
|
@@ -2019,7 +2084,7 @@ var FALLBACK_CONTEXT = {
|
|
|
2019
2084
|
async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
2020
2085
|
const destPath = hausPath(root, "workflow-config.md");
|
|
2021
2086
|
const printable = displayPath(root, destPath);
|
|
2022
|
-
const exists = await
|
|
2087
|
+
const exists = await fs11.pathExists(destPath);
|
|
2023
2088
|
if (exists && !opts.refill) {
|
|
2024
2089
|
if (dryRun) log(printable + ": exists (project-owned, skipping)");
|
|
2025
2090
|
return null;
|
|
@@ -2027,11 +2092,11 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
2027
2092
|
const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
|
|
2028
2093
|
...FALLBACK_CONTEXT,
|
|
2029
2094
|
root,
|
|
2030
|
-
repoName:
|
|
2095
|
+
repoName: path12.basename(root)
|
|
2031
2096
|
};
|
|
2032
2097
|
const values = await deriveWorkflowConfig(root, ctx);
|
|
2033
2098
|
if (exists) {
|
|
2034
|
-
const current = await
|
|
2099
|
+
const current = await fs11.readFile(destPath, "utf8");
|
|
2035
2100
|
const refilled = refillContent(current, values);
|
|
2036
2101
|
if (refilled === current) {
|
|
2037
2102
|
if (dryRun) log(printable + ": no blank fields to refill");
|
|
@@ -2053,7 +2118,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
2053
2118
|
}
|
|
2054
2119
|
|
|
2055
2120
|
// src/claude/write-workflow.ts
|
|
2056
|
-
import
|
|
2121
|
+
import fs12 from "fs-extra";
|
|
2057
2122
|
var STABLE_ID = "template.workflow";
|
|
2058
2123
|
function makeWorkflowHeader(pkgVersion, contentHash) {
|
|
2059
2124
|
return `<!-- HAUS-MANAGED id=${STABLE_ID} v=${SCHEMA_VERSION} source=@haus-tech/haus-workflow@${pkgVersion} hash=${contentHash} -->`;
|
|
@@ -2072,8 +2137,8 @@ async function writeWorkflow(root, pkgVersion, dryRun, force = false) {
|
|
|
2072
2137
|
${templateContent}`;
|
|
2073
2138
|
const destPath = hausPath(root, "WORKFLOW.md");
|
|
2074
2139
|
const printable = displayPath(root, destPath);
|
|
2075
|
-
if (await
|
|
2076
|
-
const existing = await
|
|
2140
|
+
if (await fs12.pathExists(destPath)) {
|
|
2141
|
+
const existing = await fs12.readFile(destPath, "utf8");
|
|
2077
2142
|
const firstLine = existing.split("\n")[0] ?? "";
|
|
2078
2143
|
const parsed = parseHausManagedHeader(firstLine);
|
|
2079
2144
|
if (!parsed) {
|
|
@@ -2107,7 +2172,7 @@ ${templateContent}`;
|
|
|
2107
2172
|
}
|
|
2108
2173
|
}
|
|
2109
2174
|
if (dryRun) {
|
|
2110
|
-
const prev = await
|
|
2175
|
+
const prev = await fs12.pathExists(destPath) ? await fs12.readFile(destPath, "utf8") : "";
|
|
2111
2176
|
if (!prev) {
|
|
2112
2177
|
log(createUnifiedDiff(printable, "", next));
|
|
2113
2178
|
} else {
|
|
@@ -2140,7 +2205,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2140
2205
|
estimatedTokenReductionPct: 0
|
|
2141
2206
|
};
|
|
2142
2207
|
const pkgRoot = packageRoot();
|
|
2143
|
-
const hausVersion2 = (await readJson(
|
|
2208
|
+
const hausVersion2 = (await readJson(path13.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
|
|
2144
2209
|
const coreFiles = [
|
|
2145
2210
|
claudePath(root, "settings.json"),
|
|
2146
2211
|
claudePath(root, "rules", "haus.md"),
|
|
@@ -2151,10 +2216,12 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2151
2216
|
const workflowConfigPath = await writeWorkflowConfig(root, dryRun, {
|
|
2152
2217
|
refill: opts.refillConfig
|
|
2153
2218
|
});
|
|
2219
|
+
const prettierIgnorePath = await writePrettierIgnore(root, dryRun);
|
|
2154
2220
|
const p6Files = [
|
|
2155
2221
|
rootClaudeMdPath,
|
|
2156
2222
|
...workflowPath ? [workflowPath] : [],
|
|
2157
|
-
...workflowConfigPath ? [workflowConfigPath] : []
|
|
2223
|
+
...workflowConfigPath ? [workflowConfigPath] : [],
|
|
2224
|
+
prettierIgnorePath
|
|
2158
2225
|
];
|
|
2159
2226
|
const files = dryRun ? [...coreFiles, ...p6Files] : [...coreFiles, ...p6Files, hausPath(root, "haus.lock.json")];
|
|
2160
2227
|
if (dryRun) {
|
|
@@ -2171,8 +2238,8 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2171
2238
|
dryRun
|
|
2172
2239
|
);
|
|
2173
2240
|
const legacyReviewPath = claudePath(root, "commands", "haus-review.md");
|
|
2174
|
-
if (await
|
|
2175
|
-
const content2 = await
|
|
2241
|
+
if (await fs13.pathExists(legacyReviewPath)) {
|
|
2242
|
+
const content2 = await fs13.readFile(legacyReviewPath, "utf8");
|
|
2176
2243
|
const stub = 'Run `haus context --task "code review"` then review diff.';
|
|
2177
2244
|
if (content2 === stub || content2 === `${stub}
|
|
2178
2245
|
` || content2 === `${stub}\r
|
|
@@ -2180,7 +2247,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2180
2247
|
if (dryRun) {
|
|
2181
2248
|
log(`[dry-run] would remove stale ${displayPath(root, legacyReviewPath)}`);
|
|
2182
2249
|
} else {
|
|
2183
|
-
await
|
|
2250
|
+
await fs13.remove(legacyReviewPath);
|
|
2184
2251
|
}
|
|
2185
2252
|
}
|
|
2186
2253
|
}
|
|
@@ -2208,8 +2275,8 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2208
2275
|
dryRun
|
|
2209
2276
|
);
|
|
2210
2277
|
const legacySecurityPath = claudePath(root, "rules", "security.md");
|
|
2211
|
-
if (await
|
|
2212
|
-
const content2 = await
|
|
2278
|
+
if (await fs13.pathExists(legacySecurityPath)) {
|
|
2279
|
+
const content2 = await fs13.readFile(legacySecurityPath, "utf8");
|
|
2213
2280
|
const stub = "- Never read secrets.\n- Block dangerous shell commands.";
|
|
2214
2281
|
if (content2 === stub || content2 === `${stub}
|
|
2215
2282
|
` || content2 === `${stub}\r
|
|
@@ -2217,7 +2284,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2217
2284
|
if (dryRun) {
|
|
2218
2285
|
log(`[dry-run] would remove stale ${displayPath(root, legacySecurityPath)}`);
|
|
2219
2286
|
} else {
|
|
2220
|
-
await
|
|
2287
|
+
await fs13.remove(legacySecurityPath);
|
|
2221
2288
|
}
|
|
2222
2289
|
}
|
|
2223
2290
|
}
|
|
@@ -2232,11 +2299,11 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2232
2299
|
];
|
|
2233
2300
|
for (const rel of LEGACY_PRUNED_ARTIFACTS) {
|
|
2234
2301
|
const legacyPath = hausPath(root, rel);
|
|
2235
|
-
if (await
|
|
2302
|
+
if (await fs13.pathExists(legacyPath)) {
|
|
2236
2303
|
if (dryRun) {
|
|
2237
2304
|
log(`[dry-run] would remove stale ${displayPath(root, legacyPath)}`);
|
|
2238
2305
|
} else {
|
|
2239
|
-
await
|
|
2306
|
+
await fs13.remove(legacyPath);
|
|
2240
2307
|
}
|
|
2241
2308
|
}
|
|
2242
2309
|
}
|
|
@@ -2278,10 +2345,10 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2278
2345
|
);
|
|
2279
2346
|
continue;
|
|
2280
2347
|
}
|
|
2281
|
-
const destination = claudePath(root, target,
|
|
2282
|
-
if (await
|
|
2348
|
+
const destination = claudePath(root, target, path13.basename(sourcePath));
|
|
2349
|
+
if (await fs13.pathExists(sourcePath)) {
|
|
2283
2350
|
if (dryRun) {
|
|
2284
|
-
const exists = await
|
|
2351
|
+
const exists = await fs13.pathExists(destination);
|
|
2285
2352
|
log(
|
|
2286
2353
|
`${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`
|
|
2287
2354
|
);
|
|
@@ -2291,17 +2358,17 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2291
2358
|
dryRun: false
|
|
2292
2359
|
});
|
|
2293
2360
|
} else {
|
|
2294
|
-
await
|
|
2295
|
-
await
|
|
2361
|
+
await fs13.ensureDir(path13.dirname(destination));
|
|
2362
|
+
await fs13.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
2296
2363
|
}
|
|
2297
2364
|
files.push(destination);
|
|
2298
|
-
const relPaths = [
|
|
2365
|
+
const relPaths = [path13.relative(root, destination)];
|
|
2299
2366
|
if (!superpowersSharedInstalled && manifestItem.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID && item.type === "skill") {
|
|
2300
2367
|
const sharedRel = await installSuperpowersShared(contentRoot, root, dryRun);
|
|
2301
2368
|
if (sharedRel) {
|
|
2302
2369
|
superpowersSharedInstalled = true;
|
|
2303
2370
|
relPaths.push(sharedRel);
|
|
2304
|
-
files.push(
|
|
2371
|
+
files.push(path13.join(root, sharedRel));
|
|
2305
2372
|
}
|
|
2306
2373
|
}
|
|
2307
2374
|
const current = installedPathsByItem.get(item.id) ?? [];
|
|
@@ -2368,7 +2435,7 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun) {
|
|
|
2368
2435
|
if (relPaths.length === 0) continue;
|
|
2369
2436
|
const existing = [];
|
|
2370
2437
|
for (const rel of relPaths) {
|
|
2371
|
-
if (await
|
|
2438
|
+
if (await fs13.pathExists(path13.join(root, rel))) existing.push(rel);
|
|
2372
2439
|
}
|
|
2373
2440
|
if (existing.length === 0) continue;
|
|
2374
2441
|
const pruneReason = deprecated ? "deprecated" : "stale";
|
|
@@ -2386,21 +2453,28 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun) {
|
|
|
2386
2453
|
continue;
|
|
2387
2454
|
}
|
|
2388
2455
|
for (const rel of existing) {
|
|
2389
|
-
const abs =
|
|
2456
|
+
const abs = path13.join(root, rel);
|
|
2390
2457
|
if (dryRun) {
|
|
2391
2458
|
log(`[dry-run] would remove ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
2392
2459
|
continue;
|
|
2393
2460
|
}
|
|
2394
|
-
await
|
|
2395
|
-
await pruneEmptyDir(
|
|
2461
|
+
await fs13.remove(abs);
|
|
2462
|
+
await pruneEmptyDir(path13.dirname(abs));
|
|
2396
2463
|
log(`Removed ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
2397
2464
|
}
|
|
2398
2465
|
}
|
|
2399
2466
|
}
|
|
2400
2467
|
|
|
2468
|
+
// src/utils/args.ts
|
|
2469
|
+
function parseIdList(value) {
|
|
2470
|
+
if (!value) return [];
|
|
2471
|
+
const raw = Array.isArray(value) ? value : [value];
|
|
2472
|
+
return raw.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v.length > 0);
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2401
2475
|
// src/commands/apply.ts
|
|
2402
2476
|
async function cacheHasItems() {
|
|
2403
|
-
const data = await readJson(
|
|
2477
|
+
const data = await readJson(path14.join(getCacheDir(), "manifest.json"));
|
|
2404
2478
|
return Array.isArray(data?.items) && data.items.length > 0;
|
|
2405
2479
|
}
|
|
2406
2480
|
function installableRecommendedItems(recommended) {
|
|
@@ -2414,6 +2488,34 @@ async function runApply(options) {
|
|
|
2414
2488
|
const root = process.cwd();
|
|
2415
2489
|
const isDryRun = Boolean(options.dryRun) && !options.write;
|
|
2416
2490
|
let selectedIds;
|
|
2491
|
+
const explicitIds = parseIdList(options.ids);
|
|
2492
|
+
if (explicitIds.length > 0) {
|
|
2493
|
+
if (options.select) {
|
|
2494
|
+
error("Use either --select (interactive) or --ids (explicit), not both");
|
|
2495
|
+
process.exitCode = 1;
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2498
|
+
const rec = await readJson(hausPath(root, "recommendation.json"));
|
|
2499
|
+
const recommended = rec?.recommended ?? [];
|
|
2500
|
+
const installableIds = new Set(installableRecommendedItems(recommended).map((i) => i.id));
|
|
2501
|
+
const configIds = new Set(recommended.filter((i) => i.install === false).map((i) => i.id));
|
|
2502
|
+
const skippedIds = new Set((rec?.skipped ?? []).map((s) => s.id));
|
|
2503
|
+
for (const id of explicitIds.filter((id2) => !installableIds.has(id2))) {
|
|
2504
|
+
if (configIds.has(id)) {
|
|
2505
|
+
warn(`--ids: "${id}" is a config item \u2014 install it with \`haus scaffold ${id}\`. Ignoring.`);
|
|
2506
|
+
} else if (skippedIds.has(id)) {
|
|
2507
|
+
warn(
|
|
2508
|
+
`--ids: "${id}" is not currently recommended (skipped) \u2014 add it with \`haus recommend --include ${id}\` first. Ignoring.`
|
|
2509
|
+
);
|
|
2510
|
+
} else {
|
|
2511
|
+
warn(
|
|
2512
|
+
`--ids: "${id}" is not in recommendation.json \u2014 run \`haus recommend\` (or \`--include ${id}\`) first. Ignoring.`
|
|
2513
|
+
);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
selectedIds = explicitIds.filter((id) => installableIds.has(id));
|
|
2517
|
+
log(`Applying ${selectedIds.length} explicitly selected catalog item(s).`);
|
|
2518
|
+
}
|
|
2417
2519
|
if (options.select) {
|
|
2418
2520
|
if (!process.stdin.isTTY) {
|
|
2419
2521
|
error("--select requires an interactive terminal (stdin is not a TTY)");
|
|
@@ -2499,7 +2601,7 @@ async function runCatalogAudit() {
|
|
|
2499
2601
|
|
|
2500
2602
|
// src/commands/clone.ts
|
|
2501
2603
|
import { existsSync as existsSync2 } from "node:fs";
|
|
2502
|
-
import
|
|
2604
|
+
import path15 from "node:path";
|
|
2503
2605
|
|
|
2504
2606
|
// src/utils/exec.ts
|
|
2505
2607
|
import { execa } from "execa";
|
|
@@ -2552,9 +2654,9 @@ async function runClone(url, opts = {}) {
|
|
|
2552
2654
|
process.exitCode = 1;
|
|
2553
2655
|
return;
|
|
2554
2656
|
}
|
|
2555
|
-
const target =
|
|
2657
|
+
const target = path15.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
2556
2658
|
if (existsSync2(target)) {
|
|
2557
|
-
log(`\u2022 ${
|
|
2659
|
+
log(`\u2022 ${path15.basename(target)} already present at ${target} \u2014 skipped`);
|
|
2558
2660
|
return;
|
|
2559
2661
|
}
|
|
2560
2662
|
if (opts.dryRun) {
|
|
@@ -2571,11 +2673,11 @@ async function runClone(url, opts = {}) {
|
|
|
2571
2673
|
}
|
|
2572
2674
|
|
|
2573
2675
|
// src/commands/doctor.ts
|
|
2574
|
-
import
|
|
2575
|
-
import
|
|
2676
|
+
import path20 from "node:path";
|
|
2677
|
+
import fs15 from "fs-extra";
|
|
2576
2678
|
|
|
2577
2679
|
// src/scanner/scan-project.ts
|
|
2578
|
-
import
|
|
2680
|
+
import path19 from "node:path";
|
|
2579
2681
|
|
|
2580
2682
|
// src/utils/audit-checks.ts
|
|
2581
2683
|
function isRecord(v) {
|
|
@@ -2602,8 +2704,8 @@ function compareVersions(a, b) {
|
|
|
2602
2704
|
}
|
|
2603
2705
|
|
|
2604
2706
|
// src/scanner/detect-package-manager.ts
|
|
2605
|
-
import
|
|
2606
|
-
import
|
|
2707
|
+
import path16 from "node:path";
|
|
2708
|
+
import fs14 from "fs-extra";
|
|
2607
2709
|
function detectPackageManager(root, packageManagerField) {
|
|
2608
2710
|
const field = String(packageManagerField ?? "").trim();
|
|
2609
2711
|
if (field.startsWith("yarn@")) {
|
|
@@ -2621,9 +2723,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
2621
2723
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
2622
2724
|
return "unknown";
|
|
2623
2725
|
}
|
|
2624
|
-
if (
|
|
2625
|
-
if (
|
|
2626
|
-
if (
|
|
2726
|
+
if (fs14.existsSync(path16.join(root, "yarn.lock"))) return "yarn";
|
|
2727
|
+
if (fs14.existsSync(path16.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
2728
|
+
if (fs14.existsSync(path16.join(root, "package-lock.json"))) return "npm";
|
|
2627
2729
|
return "unknown";
|
|
2628
2730
|
}
|
|
2629
2731
|
|
|
@@ -2825,7 +2927,7 @@ function runDetection(ctx, rules = STACK_RULES) {
|
|
|
2825
2927
|
}
|
|
2826
2928
|
|
|
2827
2929
|
// src/scanner/detection.ts
|
|
2828
|
-
import
|
|
2930
|
+
import path17 from "node:path";
|
|
2829
2931
|
var UNSUPPORTED_MARKERS = {
|
|
2830
2932
|
"requirements.txt": "python",
|
|
2831
2933
|
"pyproject.toml": "python",
|
|
@@ -2879,14 +2981,14 @@ function finalizeRoles(registryRoles, deps, files) {
|
|
|
2879
2981
|
function collectUnsupportedSignals(files) {
|
|
2880
2982
|
return [
|
|
2881
2983
|
...new Set(
|
|
2882
|
-
files.map((f) => UNSUPPORTED_MARKERS[
|
|
2984
|
+
files.map((f) => UNSUPPORTED_MARKERS[path17.basename(f)]).filter((s) => Boolean(s))
|
|
2883
2985
|
)
|
|
2884
2986
|
].sort();
|
|
2885
2987
|
}
|
|
2886
2988
|
|
|
2887
2989
|
// src/scanner/render.ts
|
|
2888
2990
|
import { readFile } from "node:fs/promises";
|
|
2889
|
-
import
|
|
2991
|
+
import path18 from "node:path";
|
|
2890
2992
|
async function buildContentBlob(root, files) {
|
|
2891
2993
|
const candidates = files.filter(
|
|
2892
2994
|
(f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
|
|
@@ -2894,7 +2996,7 @@ async function buildContentBlob(root, files) {
|
|
|
2894
2996
|
const slice = candidates.slice(0, 300);
|
|
2895
2997
|
const parts = await mapWithConcurrency(slice, async (rel) => {
|
|
2896
2998
|
try {
|
|
2897
|
-
return await readFile(
|
|
2999
|
+
return await readFile(path18.join(root, rel), "utf8");
|
|
2898
3000
|
} catch {
|
|
2899
3001
|
return "";
|
|
2900
3002
|
}
|
|
@@ -2981,8 +3083,8 @@ var SAFE_FILES = [
|
|
|
2981
3083
|
"Gemfile"
|
|
2982
3084
|
];
|
|
2983
3085
|
async function scanProject(root) {
|
|
2984
|
-
const pkg = await readJson(
|
|
2985
|
-
const composer = await readJson(
|
|
3086
|
+
const pkg = await readJson(path19.join(root, "package.json"));
|
|
3087
|
+
const composer = await readJson(path19.join(root, "composer.json"));
|
|
2986
3088
|
const files = await listFiles(root, SAFE_FILES);
|
|
2987
3089
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
2988
3090
|
const deps = dependencySet(pkg, composer);
|
|
@@ -3011,7 +3113,7 @@ async function scanProject(root) {
|
|
|
3011
3113
|
const context = {
|
|
3012
3114
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3013
3115
|
root,
|
|
3014
|
-
repoName: String(pkg?.name ??
|
|
3116
|
+
repoName: String(pkg?.name ?? path19.basename(root)),
|
|
3015
3117
|
packageManager,
|
|
3016
3118
|
repoRoles: roles,
|
|
3017
3119
|
detectedStacks: stacks,
|
|
@@ -3108,7 +3210,7 @@ async function runDoctor(options) {
|
|
|
3108
3210
|
} else {
|
|
3109
3211
|
ok(`- HOOKS OK: ${hooks.message}`);
|
|
3110
3212
|
}
|
|
3111
|
-
const rootClaudeMdPath =
|
|
3213
|
+
const rootClaudeMdPath = path20.join(root, "CLAUDE.md");
|
|
3112
3214
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
3113
3215
|
if (!rootClaudeMdContent) {
|
|
3114
3216
|
flag(
|
|
@@ -3136,7 +3238,7 @@ async function runDoctor(options) {
|
|
|
3136
3238
|
const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
|
|
3137
3239
|
const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
|
|
3138
3240
|
for (const target of importTargets) {
|
|
3139
|
-
if (!await
|
|
3241
|
+
if (!await fs15.pathExists(hausPath(root, target))) {
|
|
3140
3242
|
flag(
|
|
3141
3243
|
`- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
|
|
3142
3244
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -3147,7 +3249,7 @@ async function runDoctor(options) {
|
|
|
3147
3249
|
}
|
|
3148
3250
|
}
|
|
3149
3251
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
3150
|
-
const workflowExists = await
|
|
3252
|
+
const workflowExists = await fs15.pathExists(workflowPath);
|
|
3151
3253
|
if (!workflowExists) {
|
|
3152
3254
|
flag(
|
|
3153
3255
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -3170,15 +3272,15 @@ async function runDoctor(options) {
|
|
|
3170
3272
|
"haus apply --write --force"
|
|
3171
3273
|
);
|
|
3172
3274
|
} else {
|
|
3173
|
-
const cachePath =
|
|
3174
|
-
const bundledPath =
|
|
3275
|
+
const cachePath = path20.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
3276
|
+
const bundledPath = path20.join(
|
|
3175
3277
|
packageRoot(),
|
|
3176
3278
|
"library",
|
|
3177
3279
|
"global",
|
|
3178
3280
|
"templates",
|
|
3179
3281
|
"agentic-workflow-standard.md"
|
|
3180
3282
|
);
|
|
3181
|
-
const templatePath = await
|
|
3283
|
+
const templatePath = await fs15.pathExists(cachePath) ? cachePath : bundledPath;
|
|
3182
3284
|
const templateContent = await readText(templatePath);
|
|
3183
3285
|
if (storedHashMatch && templateContent) {
|
|
3184
3286
|
const currentHash = hashText(normaliseLF(templateContent));
|
|
@@ -3198,7 +3300,7 @@ async function runDoctor(options) {
|
|
|
3198
3300
|
}
|
|
3199
3301
|
}
|
|
3200
3302
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
3201
|
-
const workflowConfigExists = await
|
|
3303
|
+
const workflowConfigExists = await fs15.pathExists(workflowConfigPath);
|
|
3202
3304
|
if (!workflowConfigExists) {
|
|
3203
3305
|
flag(
|
|
3204
3306
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -3206,7 +3308,7 @@ async function runDoctor(options) {
|
|
|
3206
3308
|
"haus apply --write"
|
|
3207
3309
|
);
|
|
3208
3310
|
} else {
|
|
3209
|
-
const cfg = await
|
|
3311
|
+
const cfg = await fs15.readFile(workflowConfigPath, "utf8");
|
|
3210
3312
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
3211
3313
|
if (unfilled > 0) {
|
|
3212
3314
|
flag(
|
|
@@ -3218,6 +3320,18 @@ async function runDoctor(options) {
|
|
|
3218
3320
|
ok("- .haus-workflow/workflow-config.md: OK (project-owned)");
|
|
3219
3321
|
}
|
|
3220
3322
|
}
|
|
3323
|
+
if (workflowExists) {
|
|
3324
|
+
const prettierIgnore = await readText(path20.join(root, ".prettierignore")) ?? "";
|
|
3325
|
+
if (!prettierIgnore.split("\n").some((l) => l.trim() === ".haus-workflow/")) {
|
|
3326
|
+
flag(
|
|
3327
|
+
"- .prettierignore: not protecting .haus-workflow/ (run `haus apply --write`)",
|
|
3328
|
+
'The formatter may reformat managed files and trigger false "modified locally" reports',
|
|
3329
|
+
"haus apply --write"
|
|
3330
|
+
);
|
|
3331
|
+
} else {
|
|
3332
|
+
ok("- .prettierignore: protects .haus-workflow/");
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3221
3335
|
const cacheAgeMs = await getCacheManifestAge();
|
|
3222
3336
|
if (cacheAgeMs === null) {
|
|
3223
3337
|
flag(
|
|
@@ -3237,7 +3351,7 @@ async function runDoctor(options) {
|
|
|
3237
3351
|
ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
|
|
3238
3352
|
}
|
|
3239
3353
|
}
|
|
3240
|
-
const pkgJson = await readJson(
|
|
3354
|
+
const pkgJson = await readJson(path20.join(packageRoot(), "package.json"));
|
|
3241
3355
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
3242
3356
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
3243
3357
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -3459,8 +3573,8 @@ async function runGuard(kind, _options) {
|
|
|
3459
3573
|
}
|
|
3460
3574
|
|
|
3461
3575
|
// src/commands/init.ts
|
|
3462
|
-
import
|
|
3463
|
-
import
|
|
3576
|
+
import path21 from "node:path";
|
|
3577
|
+
import fs16 from "fs-extra";
|
|
3464
3578
|
|
|
3465
3579
|
// src/utils/prompts.ts
|
|
3466
3580
|
import { stdin as input, stdout as output } from "node:process";
|
|
@@ -3546,7 +3660,7 @@ function mergeRecommendationWarnings(context) {
|
|
|
3546
3660
|
}
|
|
3547
3661
|
|
|
3548
3662
|
// src/recommender/recommend.ts
|
|
3549
|
-
async function recommend(root, context) {
|
|
3663
|
+
async function recommend(root, context, opts = {}) {
|
|
3550
3664
|
const items = await loadCatalog(root);
|
|
3551
3665
|
const sources = await readJson(
|
|
3552
3666
|
hausPath(root, "sources-report.json")
|
|
@@ -3719,6 +3833,8 @@ async function recommend(root, context) {
|
|
|
3719
3833
|
}
|
|
3720
3834
|
}
|
|
3721
3835
|
applyCoInstallSuppression(recommended, skipped);
|
|
3836
|
+
const includeWarnings = applyManualIncludes(opts.include ?? [], items, recommended, skipped);
|
|
3837
|
+
const optInEligible = buildOptInEligible(items, skipped);
|
|
3722
3838
|
recommended.sort((a, b) => a.id.localeCompare(b.id));
|
|
3723
3839
|
skipped.sort((a, b) => a.id.localeCompare(b.id));
|
|
3724
3840
|
const installableRecommended = recommended.filter((item) => item.install);
|
|
@@ -3729,7 +3845,8 @@ async function recommend(root, context) {
|
|
|
3729
3845
|
return {
|
|
3730
3846
|
recommended,
|
|
3731
3847
|
skipped,
|
|
3732
|
-
|
|
3848
|
+
optInEligible,
|
|
3849
|
+
warnings: [...mergeRecommendationWarnings(context), ...includeWarnings],
|
|
3733
3850
|
estimatedContextTokens,
|
|
3734
3851
|
selectedRules,
|
|
3735
3852
|
skippedRules,
|
|
@@ -3815,6 +3932,80 @@ function applyCoInstallSuppression(recommended, skipped) {
|
|
|
3815
3932
|
});
|
|
3816
3933
|
}
|
|
3817
3934
|
}
|
|
3935
|
+
var HARD_SKIP_CODES = /* @__PURE__ */ new Set([
|
|
3936
|
+
"unsupported-policy",
|
|
3937
|
+
"deprecated",
|
|
3938
|
+
"curated-not-approved",
|
|
3939
|
+
"curated-risk-blocked",
|
|
3940
|
+
"sensitive-policy",
|
|
3941
|
+
"source-trust",
|
|
3942
|
+
"source-approval"
|
|
3943
|
+
]);
|
|
3944
|
+
function applyManualIncludes(include, items, recommended, skipped) {
|
|
3945
|
+
const warnings = [];
|
|
3946
|
+
if (include.length === 0) return warnings;
|
|
3947
|
+
const byId = new Map(items.map((i) => [i.id, i]));
|
|
3948
|
+
const recommendedIds = new Set(recommended.map((r) => r.id));
|
|
3949
|
+
for (const id of include) {
|
|
3950
|
+
const item = byId.get(id);
|
|
3951
|
+
if (!item) {
|
|
3952
|
+
warnings.push(`--include: unknown catalog id "${id}" (not in manifest)`);
|
|
3953
|
+
continue;
|
|
3954
|
+
}
|
|
3955
|
+
if (recommendedIds.has(id)) continue;
|
|
3956
|
+
const skipIdx = skipped.findIndex((s) => s.id === id);
|
|
3957
|
+
const skipCode = skipIdx >= 0 ? skipped[skipIdx].skipReasons[0]?.code : void 0;
|
|
3958
|
+
if (skipCode && HARD_SKIP_CODES.has(skipCode)) {
|
|
3959
|
+
warnings.push(`--include: "${id}" cannot be force-installed (blocked by ${skipCode})`);
|
|
3960
|
+
continue;
|
|
3961
|
+
}
|
|
3962
|
+
if (skipCode === "requires-any-unsatisfied") {
|
|
3963
|
+
warnings.push(`--include: "${id}" added manually though its requiresAny gate is unsatisfied`);
|
|
3964
|
+
}
|
|
3965
|
+
if (skipIdx >= 0) skipped.splice(skipIdx, 1);
|
|
3966
|
+
const isConfig = item.type === "config";
|
|
3967
|
+
recommended.push({
|
|
3968
|
+
id: item.id,
|
|
3969
|
+
type: item.type,
|
|
3970
|
+
reason: "manually included",
|
|
3971
|
+
reasons: [
|
|
3972
|
+
{
|
|
3973
|
+
code: "manual-include",
|
|
3974
|
+
message: "manually included via --include",
|
|
3975
|
+
signal: "selection:manual"
|
|
3976
|
+
}
|
|
3977
|
+
],
|
|
3978
|
+
selectionMode: "manual",
|
|
3979
|
+
install: !isConfig,
|
|
3980
|
+
tags: item.tags,
|
|
3981
|
+
ecosystem: item.ecosystem,
|
|
3982
|
+
tokenEstimate: item.tokenEstimate
|
|
3983
|
+
});
|
|
3984
|
+
recommendedIds.add(id);
|
|
3985
|
+
}
|
|
3986
|
+
return warnings;
|
|
3987
|
+
}
|
|
3988
|
+
function buildOptInEligible(items, skipped) {
|
|
3989
|
+
const byId = new Map(items.map((i) => [i.id, i]));
|
|
3990
|
+
const eligible = [];
|
|
3991
|
+
for (const entry of skipped) {
|
|
3992
|
+
if (entry.skipReasons[0]?.code !== "requires-any-unsatisfied") continue;
|
|
3993
|
+
const item = byId.get(entry.id);
|
|
3994
|
+
if (!item?.optInTier || !item.optInGroup) continue;
|
|
3995
|
+
eligible.push({
|
|
3996
|
+
id: item.id,
|
|
3997
|
+
type: item.type,
|
|
3998
|
+
title: item.title,
|
|
3999
|
+
optInTier: item.optInTier,
|
|
4000
|
+
optInGroup: item.optInGroup,
|
|
4001
|
+
purpose: item.purpose,
|
|
4002
|
+
tokenEstimate: item.tokenEstimate,
|
|
4003
|
+
requires: entry.skipReasons[0]?.signal ?? describeRequiresAny(item.requiresAny ?? [])
|
|
4004
|
+
});
|
|
4005
|
+
}
|
|
4006
|
+
eligible.sort((a, b) => a.optInGroup.localeCompare(b.optInGroup) || a.id.localeCompare(b.id));
|
|
4007
|
+
return eligible;
|
|
4008
|
+
}
|
|
3818
4009
|
|
|
3819
4010
|
// src/commands/setup-core.ts
|
|
3820
4011
|
async function runSetupCore(root, opts) {
|
|
@@ -3907,8 +4098,8 @@ async function runSetupProject(options) {
|
|
|
3907
4098
|
// src/commands/init.ts
|
|
3908
4099
|
async function runInit(options) {
|
|
3909
4100
|
const root = process.cwd();
|
|
3910
|
-
const hausDir =
|
|
3911
|
-
const alreadyInit = await
|
|
4101
|
+
const hausDir = path21.join(root, ".haus-workflow");
|
|
4102
|
+
const alreadyInit = await fs16.pathExists(hausDir);
|
|
3912
4103
|
if (alreadyInit) {
|
|
3913
4104
|
log("Haus AI already initialized in this project.");
|
|
3914
4105
|
log("Run `haus setup-project` to reconfigure.");
|
|
@@ -3920,8 +4111,8 @@ async function runInit(options) {
|
|
|
3920
4111
|
|
|
3921
4112
|
// src/install/apply.ts
|
|
3922
4113
|
import crypto2 from "node:crypto";
|
|
3923
|
-
import
|
|
3924
|
-
import
|
|
4114
|
+
import path22 from "node:path";
|
|
4115
|
+
import fs17 from "fs-extra";
|
|
3925
4116
|
|
|
3926
4117
|
// src/install/header.ts
|
|
3927
4118
|
var MD_PREFIX = "<!-- HAUS-MANAGED";
|
|
@@ -4007,40 +4198,40 @@ function hashContent(content2) {
|
|
|
4007
4198
|
}
|
|
4008
4199
|
function sourceVersion() {
|
|
4009
4200
|
try {
|
|
4010
|
-
const pkgPath =
|
|
4011
|
-
const pkg = JSON.parse(
|
|
4201
|
+
const pkgPath = path22.join(packageRoot(), "package.json");
|
|
4202
|
+
const pkg = JSON.parse(fs17.readFileSync(pkgPath, "utf8"));
|
|
4012
4203
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
4013
4204
|
} catch {
|
|
4014
4205
|
return "haus@0.0.0";
|
|
4015
4206
|
}
|
|
4016
4207
|
}
|
|
4017
4208
|
function globalSrcDir() {
|
|
4018
|
-
return
|
|
4209
|
+
return path22.join(packageRoot(), "library", "global");
|
|
4019
4210
|
}
|
|
4020
4211
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
4021
4212
|
const entries = [];
|
|
4022
|
-
const skillsDir =
|
|
4023
|
-
if (
|
|
4024
|
-
for (const skillName of
|
|
4025
|
-
const skillFile =
|
|
4026
|
-
if (
|
|
4213
|
+
const skillsDir = path22.join(srcDir, "skills");
|
|
4214
|
+
if (fs17.pathExistsSync(skillsDir)) {
|
|
4215
|
+
for (const skillName of fs17.readdirSync(skillsDir)) {
|
|
4216
|
+
const skillFile = path22.join(skillsDir, skillName, "SKILL.md");
|
|
4217
|
+
if (fs17.pathExistsSync(skillFile)) {
|
|
4027
4218
|
entries.push({
|
|
4028
4219
|
stableId: `skill.${skillName}`,
|
|
4029
|
-
srcRelPath:
|
|
4030
|
-
destPath:
|
|
4220
|
+
srcRelPath: path22.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
4221
|
+
destPath: path22.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
4031
4222
|
});
|
|
4032
4223
|
}
|
|
4033
4224
|
}
|
|
4034
4225
|
}
|
|
4035
|
-
const commandsDir =
|
|
4036
|
-
if (
|
|
4037
|
-
for (const fileName of
|
|
4226
|
+
const commandsDir = path22.join(srcDir, "commands");
|
|
4227
|
+
if (fs17.pathExistsSync(commandsDir)) {
|
|
4228
|
+
for (const fileName of fs17.readdirSync(commandsDir)) {
|
|
4038
4229
|
if (!fileName.endsWith(".md")) continue;
|
|
4039
4230
|
const commandName = fileName.slice(0, -".md".length);
|
|
4040
4231
|
entries.push({
|
|
4041
4232
|
stableId: `command.${commandName}`,
|
|
4042
|
-
srcRelPath:
|
|
4043
|
-
destPath:
|
|
4233
|
+
srcRelPath: path22.join("library", "global", "commands", fileName),
|
|
4234
|
+
destPath: path22.join(claudeDir, "commands", fileName)
|
|
4044
4235
|
});
|
|
4045
4236
|
}
|
|
4046
4237
|
}
|
|
@@ -4064,7 +4255,7 @@ async function applyInstall(options = {}) {
|
|
|
4064
4255
|
};
|
|
4065
4256
|
const manifestFiles = [];
|
|
4066
4257
|
for (const entry of sourceFiles) {
|
|
4067
|
-
const srcPath =
|
|
4258
|
+
const srcPath = path22.join(packageRoot(), entry.srcRelPath);
|
|
4068
4259
|
const rawContent = await readText(srcPath);
|
|
4069
4260
|
if (rawContent === void 0) {
|
|
4070
4261
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -4084,7 +4275,7 @@ async function applyInstall(options = {}) {
|
|
|
4084
4275
|
}
|
|
4085
4276
|
continue;
|
|
4086
4277
|
}
|
|
4087
|
-
const destExists =
|
|
4278
|
+
const destExists = fs17.pathExistsSync(entry.destPath);
|
|
4088
4279
|
if (destExists) {
|
|
4089
4280
|
const currentContent = await readText(entry.destPath);
|
|
4090
4281
|
if (currentContent !== void 0) {
|
|
@@ -4120,7 +4311,7 @@ async function applyInstall(options = {}) {
|
|
|
4120
4311
|
schemaVersion: SCHEMA_VERSION2
|
|
4121
4312
|
});
|
|
4122
4313
|
}
|
|
4123
|
-
const fragmentPath =
|
|
4314
|
+
const fragmentPath = path22.join(srcDir, "settings-fragments", "hooks.json");
|
|
4124
4315
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
4125
4316
|
const settings = await readSettings();
|
|
4126
4317
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -4132,13 +4323,13 @@ async function applyInstall(options = {}) {
|
|
|
4132
4323
|
const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
|
|
4133
4324
|
for (const entry of existingManifest.files) {
|
|
4134
4325
|
if (currentDestPaths.has(entry.destPath)) continue;
|
|
4135
|
-
if (!
|
|
4326
|
+
if (!fs17.pathExistsSync(entry.destPath)) continue;
|
|
4136
4327
|
const content2 = await readText(entry.destPath);
|
|
4137
4328
|
if (!content2) continue;
|
|
4138
4329
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
4139
4330
|
const currentHash = hashContent(content2);
|
|
4140
4331
|
if (hasHeader && currentHash === entry.hash) {
|
|
4141
|
-
if (!dryRun) await
|
|
4332
|
+
if (!dryRun) await fs17.remove(entry.destPath);
|
|
4142
4333
|
result.deleted.push(entry.destPath);
|
|
4143
4334
|
} else {
|
|
4144
4335
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -4275,7 +4466,8 @@ async function runInstall(options) {
|
|
|
4275
4466
|
async function runRecommend(options) {
|
|
4276
4467
|
const root = process.cwd();
|
|
4277
4468
|
const context = await readContextOrScan(root);
|
|
4278
|
-
const
|
|
4469
|
+
const include = parseIdList(options.include);
|
|
4470
|
+
const result = await recommend(root, context, { include });
|
|
4279
4471
|
await writeJson(hausPath(root, "recommendation.json"), result);
|
|
4280
4472
|
if (options.json) {
|
|
4281
4473
|
log(JSON.stringify(result, null, 2));
|
|
@@ -4284,6 +4476,12 @@ async function runRecommend(options) {
|
|
|
4284
4476
|
log("Haus recommendation ready");
|
|
4285
4477
|
log(`Recommended: ${result.recommended.length}`);
|
|
4286
4478
|
log(`Skipped: ${result.skipped.length}`);
|
|
4479
|
+
if (result.optInEligible && result.optInEligible.length > 0) {
|
|
4480
|
+
log(
|
|
4481
|
+
`Opt-in available: ${result.optInEligible.length} (add with \`haus recommend --include <id>\`)`
|
|
4482
|
+
);
|
|
4483
|
+
}
|
|
4484
|
+
for (const w of result.warnings.filter((w2) => w2.startsWith("--include:"))) log(w);
|
|
4287
4485
|
}
|
|
4288
4486
|
|
|
4289
4487
|
// src/commands/refresh.ts
|
|
@@ -4299,8 +4497,8 @@ async function runRefresh() {
|
|
|
4299
4497
|
}
|
|
4300
4498
|
|
|
4301
4499
|
// src/install/scaffold.ts
|
|
4302
|
-
import
|
|
4303
|
-
import
|
|
4500
|
+
import path23 from "node:path";
|
|
4501
|
+
import fs18 from "fs-extra";
|
|
4304
4502
|
async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
4305
4503
|
const scaffolded = [];
|
|
4306
4504
|
const skipped = [];
|
|
@@ -4313,27 +4511,27 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
4313
4511
|
skipped.push(name);
|
|
4314
4512
|
if (outcome === "skipped-exists") skippedExisting.push(name);
|
|
4315
4513
|
};
|
|
4316
|
-
const realRoot = await
|
|
4514
|
+
const realRoot = await fs18.realpath(catalogRoot).catch(() => null);
|
|
4317
4515
|
for (const item of items) {
|
|
4318
4516
|
if (item.type !== "config") continue;
|
|
4319
|
-
const sourcePath =
|
|
4320
|
-
const rel =
|
|
4321
|
-
const escapes = rel === "" ||
|
|
4517
|
+
const sourcePath = path23.resolve(catalogRoot, item.path);
|
|
4518
|
+
const rel = path23.relative(catalogRoot, sourcePath);
|
|
4519
|
+
const escapes = rel === "" || path23.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
|
|
4322
4520
|
if (escapes) {
|
|
4323
4521
|
warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
|
|
4324
4522
|
continue;
|
|
4325
4523
|
}
|
|
4326
|
-
const realSource = await
|
|
4524
|
+
const realSource = await fs18.realpath(sourcePath).catch(() => null);
|
|
4327
4525
|
if (!realRoot || !realSource) {
|
|
4328
4526
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
4329
4527
|
continue;
|
|
4330
4528
|
}
|
|
4331
|
-
const realRel =
|
|
4332
|
-
if (realRel !== "" && (
|
|
4529
|
+
const realRel = path23.relative(realRoot, realSource);
|
|
4530
|
+
if (realRel !== "" && (path23.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
|
|
4333
4531
|
warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
|
|
4334
4532
|
continue;
|
|
4335
4533
|
}
|
|
4336
|
-
const stat = await
|
|
4534
|
+
const stat = await fs18.lstat(sourcePath).catch(() => null);
|
|
4337
4535
|
if (!stat) {
|
|
4338
4536
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
4339
4537
|
continue;
|
|
@@ -4343,56 +4541,57 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
4343
4541
|
continue;
|
|
4344
4542
|
}
|
|
4345
4543
|
if (stat.isFile()) {
|
|
4346
|
-
const filename =
|
|
4347
|
-
const dest =
|
|
4348
|
-
record(filename, await scaffoldFile(sourcePath, dest, item.id, opts));
|
|
4544
|
+
const filename = path23.basename(sourcePath);
|
|
4545
|
+
const dest = path23.join(projectRoot, filename);
|
|
4546
|
+
record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
|
|
4349
4547
|
} else if (stat.isDirectory()) {
|
|
4350
|
-
const entries = await
|
|
4548
|
+
const entries = await fs18.readdir(sourcePath);
|
|
4351
4549
|
for (const entry of entries) {
|
|
4352
|
-
const src =
|
|
4353
|
-
const dest =
|
|
4354
|
-
record(entry, await scaffoldFile(src, dest, item.id, opts));
|
|
4550
|
+
const src = path23.join(sourcePath, entry);
|
|
4551
|
+
const dest = path23.join(projectRoot, entry);
|
|
4552
|
+
record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
|
|
4355
4553
|
}
|
|
4356
4554
|
}
|
|
4357
4555
|
}
|
|
4358
4556
|
return { scaffolded, skipped, skippedExisting };
|
|
4359
4557
|
}
|
|
4360
|
-
async function scaffoldFile(src, dest, itemId, opts) {
|
|
4361
|
-
const
|
|
4558
|
+
async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
4559
|
+
const destDisplay = path23.relative(projectRoot, dest) || path23.basename(dest);
|
|
4560
|
+
const srcStat = await fs18.lstat(src).catch(() => null);
|
|
4362
4561
|
if (!srcStat) {
|
|
4363
|
-
warn(`Skipping ${
|
|
4562
|
+
warn(`Skipping ${path23.basename(src)}: source not found`);
|
|
4364
4563
|
return "skipped-refused";
|
|
4365
4564
|
}
|
|
4366
4565
|
if (srcStat.isSymbolicLink()) {
|
|
4367
|
-
warn(`Skipping ${
|
|
4566
|
+
warn(`Skipping ${path23.basename(src)}: source is a symlink`);
|
|
4368
4567
|
return "skipped-refused";
|
|
4369
4568
|
}
|
|
4370
|
-
const exists = await
|
|
4569
|
+
const exists = await fs18.pathExists(dest);
|
|
4371
4570
|
if (exists && !opts.force) {
|
|
4372
|
-
warn(`Skipping ${
|
|
4571
|
+
warn(`Skipping ${destDisplay}: already exists \u2014 re-run with --force to overwrite it`);
|
|
4373
4572
|
return "skipped-exists";
|
|
4374
4573
|
}
|
|
4375
4574
|
if (opts.dryRun) {
|
|
4376
|
-
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${
|
|
4575
|
+
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path23.basename(dest)} (${itemId})`);
|
|
4377
4576
|
return "scaffolded";
|
|
4378
4577
|
}
|
|
4379
4578
|
if (exists && opts.force) {
|
|
4380
|
-
await
|
|
4579
|
+
await fs18.remove(dest);
|
|
4381
4580
|
}
|
|
4382
|
-
await
|
|
4383
|
-
await
|
|
4581
|
+
await fs18.ensureDir(path23.dirname(dest));
|
|
4582
|
+
await fs18.copy(src, dest, {
|
|
4384
4583
|
overwrite: true,
|
|
4385
4584
|
dereference: false,
|
|
4386
4585
|
// A vanished/unreadable entry mid-walk must skip that path, not crash the copy.
|
|
4387
4586
|
filter: (s) => {
|
|
4388
4587
|
try {
|
|
4389
|
-
return !
|
|
4588
|
+
return !fs18.lstatSync(s).isSymbolicLink();
|
|
4390
4589
|
} catch {
|
|
4391
4590
|
return false;
|
|
4392
4591
|
}
|
|
4393
4592
|
}
|
|
4394
4593
|
});
|
|
4395
|
-
log(`${exists ? "Overwrote" : "Created"} ${
|
|
4594
|
+
log(`${exists ? "Overwrote" : "Created"} ${path23.basename(dest)} (${itemId})`);
|
|
4396
4595
|
return "scaffolded";
|
|
4397
4596
|
}
|
|
4398
4597
|
|
|
@@ -4449,8 +4648,8 @@ async function runScan(options) {
|
|
|
4449
4648
|
}
|
|
4450
4649
|
|
|
4451
4650
|
// src/commands/undo.ts
|
|
4452
|
-
import
|
|
4453
|
-
import
|
|
4651
|
+
import path24 from "node:path";
|
|
4652
|
+
import fs19 from "fs-extra";
|
|
4454
4653
|
|
|
4455
4654
|
// src/claude/managed-paths.ts
|
|
4456
4655
|
var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md", "commands/haus-doctor.md"];
|
|
@@ -4467,74 +4666,74 @@ async function collectManagedPaths(root) {
|
|
|
4467
4666
|
const lock = await readJson(hausPath(root, "haus.lock.json"));
|
|
4468
4667
|
for (const row of lock ?? []) {
|
|
4469
4668
|
for (const rel of row.paths ?? []) {
|
|
4470
|
-
paths.add(
|
|
4669
|
+
paths.add(path24.resolve(root, rel));
|
|
4471
4670
|
}
|
|
4472
4671
|
}
|
|
4473
4672
|
const existing = [];
|
|
4474
4673
|
for (const abs of paths) {
|
|
4475
|
-
if (await
|
|
4674
|
+
if (await fs19.pathExists(abs)) existing.push(abs);
|
|
4476
4675
|
}
|
|
4477
4676
|
return existing;
|
|
4478
4677
|
}
|
|
4479
4678
|
async function settingsHasHausContent(root) {
|
|
4480
4679
|
const settingsPath = claudePath(root, "settings.json");
|
|
4481
|
-
if (!await
|
|
4680
|
+
if (!await fs19.pathExists(settingsPath)) return false;
|
|
4482
4681
|
const settings = await readProjectSettings(root);
|
|
4483
4682
|
return settings._haus != null;
|
|
4484
4683
|
}
|
|
4485
4684
|
async function claudeMdHasHausBlock(root) {
|
|
4486
|
-
const filePath =
|
|
4487
|
-
if (!await
|
|
4488
|
-
const text = await
|
|
4685
|
+
const filePath = path24.join(root, "CLAUDE.md");
|
|
4686
|
+
if (!await fs19.pathExists(filePath)) return false;
|
|
4687
|
+
const text = await fs19.readFile(filePath, "utf8");
|
|
4489
4688
|
return text.includes(BLOCK_BEGIN);
|
|
4490
4689
|
}
|
|
4491
4690
|
async function stripProjectSettings(root) {
|
|
4492
4691
|
const settingsPath = claudePath(root, "settings.json");
|
|
4493
|
-
if (!await
|
|
4692
|
+
if (!await fs19.pathExists(settingsPath)) return false;
|
|
4494
4693
|
let settings = await readProjectSettings(root);
|
|
4495
4694
|
settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
4496
4695
|
const hasContent = Object.keys(settings).length > 0;
|
|
4497
4696
|
if (hasContent) {
|
|
4498
4697
|
await writeProjectSettings(root, settings);
|
|
4499
|
-
log(`Stripped haus rules from ${
|
|
4698
|
+
log(`Stripped haus rules from ${path24.relative(root, settingsPath)} (user settings preserved).`);
|
|
4500
4699
|
return true;
|
|
4501
4700
|
}
|
|
4502
|
-
await
|
|
4503
|
-
log(`Removed ${
|
|
4701
|
+
await fs19.remove(settingsPath);
|
|
4702
|
+
log(`Removed ${path24.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
4504
4703
|
return true;
|
|
4505
4704
|
}
|
|
4506
4705
|
async function stripRootClaudeMd(root) {
|
|
4507
|
-
const filePath =
|
|
4508
|
-
if (!await
|
|
4509
|
-
const prev = await
|
|
4706
|
+
const filePath = path24.join(root, "CLAUDE.md");
|
|
4707
|
+
if (!await fs19.pathExists(filePath)) return false;
|
|
4708
|
+
const prev = await fs19.readFile(filePath, "utf8");
|
|
4510
4709
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
4511
4710
|
const next = stripHausBlock(prev);
|
|
4512
4711
|
if (next.length === 0) {
|
|
4513
|
-
await
|
|
4712
|
+
await fs19.remove(filePath);
|
|
4514
4713
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
4515
4714
|
} else {
|
|
4516
|
-
await
|
|
4715
|
+
await fs19.writeFile(filePath, next, "utf8");
|
|
4517
4716
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
4518
4717
|
}
|
|
4519
4718
|
return true;
|
|
4520
4719
|
}
|
|
4521
4720
|
async function pruneDirIfEmpty(dir) {
|
|
4522
|
-
if (!await
|
|
4523
|
-
const entries = await
|
|
4524
|
-
if (entries.length === 0) await
|
|
4721
|
+
if (!await fs19.pathExists(dir)) return;
|
|
4722
|
+
const entries = await fs19.readdir(dir);
|
|
4723
|
+
if (entries.length === 0) await fs19.remove(dir);
|
|
4525
4724
|
}
|
|
4526
4725
|
async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
4527
4726
|
if (managedAbsPaths.length === 0) return;
|
|
4528
4727
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4529
4728
|
const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
|
|
4530
4729
|
for (const abs of managedAbsPaths) {
|
|
4531
|
-
if (!await
|
|
4532
|
-
const rel =
|
|
4533
|
-
const backupPath =
|
|
4534
|
-
await
|
|
4535
|
-
await
|
|
4730
|
+
if (!await fs19.pathExists(abs)) continue;
|
|
4731
|
+
const rel = path24.relative(root, abs);
|
|
4732
|
+
const backupPath = path24.join(backupRoot, rel);
|
|
4733
|
+
await fs19.ensureDir(path24.dirname(backupPath));
|
|
4734
|
+
await fs19.copy(abs, backupPath);
|
|
4536
4735
|
}
|
|
4537
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
4736
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path24.relative(root, backupRoot)}.`);
|
|
4538
4737
|
}
|
|
4539
4738
|
async function runUndo(options) {
|
|
4540
4739
|
const root = process.cwd();
|
|
@@ -4545,7 +4744,7 @@ async function runUndo(options) {
|
|
|
4545
4744
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
4546
4745
|
return;
|
|
4547
4746
|
}
|
|
4548
|
-
const relTargets = managed.map((p) =>
|
|
4747
|
+
const relTargets = managed.map((p) => path24.relative(root, p));
|
|
4549
4748
|
const summaryParts = [...relTargets];
|
|
4550
4749
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
4551
4750
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -4562,9 +4761,9 @@ User-owned .claude/ files will be preserved.`
|
|
|
4562
4761
|
}
|
|
4563
4762
|
await backupManagedFilesBeforeUndo(root, managed);
|
|
4564
4763
|
for (const abs of managed) {
|
|
4565
|
-
if (!await
|
|
4566
|
-
await
|
|
4567
|
-
log(`Removed ${
|
|
4764
|
+
if (!await fs19.pathExists(abs)) continue;
|
|
4765
|
+
await fs19.remove(abs);
|
|
4766
|
+
log(`Removed ${path24.relative(root, abs)}`);
|
|
4568
4767
|
}
|
|
4569
4768
|
if (stripSettings) await stripProjectSettings(root);
|
|
4570
4769
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -4575,8 +4774,8 @@ User-owned .claude/ files will be preserved.`
|
|
|
4575
4774
|
|
|
4576
4775
|
// src/install/uninstall.ts
|
|
4577
4776
|
import crypto3 from "node:crypto";
|
|
4578
|
-
import
|
|
4579
|
-
import
|
|
4777
|
+
import path25 from "node:path";
|
|
4778
|
+
import fs20 from "fs-extra";
|
|
4580
4779
|
async function runUninstall(options = {}) {
|
|
4581
4780
|
const { force = false } = options;
|
|
4582
4781
|
const manifest = await readManifest();
|
|
@@ -4586,7 +4785,7 @@ async function runUninstall(options = {}) {
|
|
|
4586
4785
|
return result;
|
|
4587
4786
|
}
|
|
4588
4787
|
for (const entry of manifest.files) {
|
|
4589
|
-
const exists =
|
|
4788
|
+
const exists = fs20.pathExistsSync(entry.destPath);
|
|
4590
4789
|
if (!exists) continue;
|
|
4591
4790
|
const content2 = await readText(entry.destPath);
|
|
4592
4791
|
if (content2 === void 0) continue;
|
|
@@ -4604,22 +4803,22 @@ async function runUninstall(options = {}) {
|
|
|
4604
4803
|
result.skipped.push(entry.destPath);
|
|
4605
4804
|
continue;
|
|
4606
4805
|
}
|
|
4607
|
-
await
|
|
4608
|
-
await pruneEmptyDir(
|
|
4806
|
+
await fs20.remove(entry.destPath);
|
|
4807
|
+
await pruneEmptyDir(path25.dirname(entry.destPath));
|
|
4609
4808
|
result.deleted.push(entry.destPath);
|
|
4610
4809
|
}
|
|
4611
4810
|
const settings = await readSettings();
|
|
4612
4811
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
4613
4812
|
await writeSettings(stripped);
|
|
4614
4813
|
result.hooksStripped = true;
|
|
4615
|
-
const hausDir =
|
|
4814
|
+
const hausDir = path25.join(globalClaudeDir(), "haus");
|
|
4616
4815
|
const manifestPath2 = hausManifestPath();
|
|
4617
|
-
if (
|
|
4618
|
-
await
|
|
4816
|
+
if (fs20.pathExistsSync(manifestPath2)) {
|
|
4817
|
+
await fs20.remove(manifestPath2);
|
|
4619
4818
|
}
|
|
4620
|
-
if (
|
|
4621
|
-
const remaining = await
|
|
4622
|
-
if (remaining.length === 0) await
|
|
4819
|
+
if (fs20.pathExistsSync(hausDir)) {
|
|
4820
|
+
const remaining = await fs20.readdir(hausDir);
|
|
4821
|
+
if (remaining.length === 0) await fs20.remove(hausDir);
|
|
4623
4822
|
}
|
|
4624
4823
|
return result;
|
|
4625
4824
|
}
|
|
@@ -4650,12 +4849,12 @@ async function runUninstallCommand(options) {
|
|
|
4650
4849
|
}
|
|
4651
4850
|
|
|
4652
4851
|
// src/commands/update.ts
|
|
4653
|
-
import
|
|
4852
|
+
import path27 from "node:path";
|
|
4654
4853
|
|
|
4655
4854
|
// src/claude/refresh-project.ts
|
|
4656
|
-
import
|
|
4855
|
+
import fs21 from "fs-extra";
|
|
4657
4856
|
async function isHausProject(root) {
|
|
4658
|
-
if (await
|
|
4857
|
+
if (await fs21.pathExists(hausPath(root, "recommendation.json"))) return true;
|
|
4659
4858
|
const result = await readJsonDetailed(claudePath(root, "settings.json"));
|
|
4660
4859
|
if (result.status === "ok") return result.value._haus != null;
|
|
4661
4860
|
return false;
|
|
@@ -4690,7 +4889,7 @@ function summarizeLockDiff(before, after) {
|
|
|
4690
4889
|
|
|
4691
4890
|
// src/update/lockfile.ts
|
|
4692
4891
|
import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
|
|
4693
|
-
import
|
|
4892
|
+
import path26 from "node:path";
|
|
4694
4893
|
async function checkLock(root) {
|
|
4695
4894
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
4696
4895
|
const hasValidVersions = lock.every(
|
|
@@ -4721,7 +4920,7 @@ async function applyLock(root) {
|
|
|
4721
4920
|
try {
|
|
4722
4921
|
const backupDir = hausPath(root, "backups");
|
|
4723
4922
|
await mkdir(backupDir, { recursive: true });
|
|
4724
|
-
await copyFile(lockPath,
|
|
4923
|
+
await copyFile(lockPath, path26.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
4725
4924
|
} catch {
|
|
4726
4925
|
}
|
|
4727
4926
|
const enriched = await Promise.all(
|
|
@@ -4743,7 +4942,7 @@ function diffLock(before, after) {
|
|
|
4743
4942
|
}
|
|
4744
4943
|
async function hasLocalOverrides(root) {
|
|
4745
4944
|
try {
|
|
4746
|
-
await readFile2(
|
|
4945
|
+
await readFile2(path26.join(root, ".claude", "settings.json"), "utf8");
|
|
4747
4946
|
return true;
|
|
4748
4947
|
} catch {
|
|
4749
4948
|
return false;
|
|
@@ -4755,7 +4954,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
|
4755
4954
|
async function runUpdate(options) {
|
|
4756
4955
|
const root = process.cwd();
|
|
4757
4956
|
if (options.check) {
|
|
4758
|
-
const pkgJson2 = await readJson(
|
|
4957
|
+
const pkgJson2 = await readJson(path27.join(packageRoot(), "package.json"));
|
|
4759
4958
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
4760
4959
|
const [status, npmVersion, latestCatalogTag, globalInstallDrift] = await Promise.all([
|
|
4761
4960
|
checkLock(root),
|
|
@@ -4785,7 +4984,7 @@ async function runUpdate(options) {
|
|
|
4785
4984
|
if (status.driftCount > 0) process.exitCode = 1;
|
|
4786
4985
|
return;
|
|
4787
4986
|
}
|
|
4788
|
-
const pkgJson = await readJson(
|
|
4987
|
+
const pkgJson = await readJson(path27.join(packageRoot(), "package.json"));
|
|
4789
4988
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
4790
4989
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
4791
4990
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -4862,16 +5061,16 @@ async function detectGlobalInstallDrift() {
|
|
|
4862
5061
|
}
|
|
4863
5062
|
|
|
4864
5063
|
// src/commands/validate-catalog.ts
|
|
4865
|
-
import
|
|
5064
|
+
import path29 from "node:path";
|
|
4866
5065
|
|
|
4867
5066
|
// src/catalog/validate-core.ts
|
|
4868
|
-
import
|
|
4869
|
-
import
|
|
5067
|
+
import fs22 from "node:fs";
|
|
5068
|
+
import path28 from "node:path";
|
|
4870
5069
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
4871
5070
|
var MANIFEST_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
|
|
4872
5071
|
function isSafeCatalogPath2(itemPath) {
|
|
4873
|
-
if (!itemPath ||
|
|
4874
|
-
const normalized =
|
|
5072
|
+
if (!itemPath || path28.isAbsolute(itemPath) || itemPath.includes("\\")) return false;
|
|
5073
|
+
const normalized = path28.normalize(itemPath);
|
|
4875
5074
|
return !normalized.startsWith("..") && !normalized.includes("/..");
|
|
4876
5075
|
}
|
|
4877
5076
|
function auditForbiddenStacks(items) {
|
|
@@ -4885,6 +5084,39 @@ function auditForbiddenStacks(items) {
|
|
|
4885
5084
|
}
|
|
4886
5085
|
return failures;
|
|
4887
5086
|
}
|
|
5087
|
+
var OPT_IN_TIERS = /* @__PURE__ */ new Set(["workflow", "ops", "review", "design"]);
|
|
5088
|
+
function auditOptInMetadata(items) {
|
|
5089
|
+
const failures = [];
|
|
5090
|
+
for (const item of items) {
|
|
5091
|
+
const hasTier = item.optInTier !== void 0;
|
|
5092
|
+
const hasGroup = item.optInGroup !== void 0;
|
|
5093
|
+
if (hasTier !== hasGroup) {
|
|
5094
|
+
failures.push(`${item.id}: optInTier and optInGroup must be set together`);
|
|
5095
|
+
}
|
|
5096
|
+
if (hasTier) {
|
|
5097
|
+
if (!OPT_IN_TIERS.has(item.optInTier)) {
|
|
5098
|
+
failures.push(
|
|
5099
|
+
`${item.id}: invalid optInTier "${item.optInTier}" (expected one of ${[...OPT_IN_TIERS].join(", ")})`
|
|
5100
|
+
);
|
|
5101
|
+
}
|
|
5102
|
+
if (typeof item.optInGroup !== "string" || item.optInGroup.length === 0) {
|
|
5103
|
+
failures.push(`${item.id}: optInGroup must be a non-empty string`);
|
|
5104
|
+
}
|
|
5105
|
+
if (item.default === true) {
|
|
5106
|
+
failures.push(`${item.id}: opt-in items (optInTier set) must not be default:true`);
|
|
5107
|
+
}
|
|
5108
|
+
if (typeof item.purpose !== "string" || item.purpose.length === 0) {
|
|
5109
|
+
failures.push(`${item.id}: opt-in items (optInTier set) must have a non-empty purpose`);
|
|
5110
|
+
}
|
|
5111
|
+
}
|
|
5112
|
+
const requiresAny = Array.isArray(item.requiresAny) ? item.requiresAny : [];
|
|
5113
|
+
const roleOnly = requiresAny.length > 0 && requiresAny.every((clause) => clause && clause.role);
|
|
5114
|
+
if (item.default !== true && roleOnly && !hasTier) {
|
|
5115
|
+
failures.push(`${item.id}: role-gated opt-in item missing optInTier/optInGroup`);
|
|
5116
|
+
}
|
|
5117
|
+
}
|
|
5118
|
+
return failures;
|
|
5119
|
+
}
|
|
4888
5120
|
function auditManifestStructure(manifestVersion, items) {
|
|
4889
5121
|
const failures = [];
|
|
4890
5122
|
const seenIds = /* @__PURE__ */ new Map();
|
|
@@ -4950,8 +5182,8 @@ function checkRequiredFrontmatter(text, label) {
|
|
|
4950
5182
|
return failures;
|
|
4951
5183
|
}
|
|
4952
5184
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
4953
|
-
const rel =
|
|
4954
|
-
const text =
|
|
5185
|
+
const rel = path28.relative(manifestDir, absPath);
|
|
5186
|
+
const text = fs22.readFileSync(absPath, "utf8");
|
|
4955
5187
|
const failures = [];
|
|
4956
5188
|
const lines = text.split(/\r?\n/);
|
|
4957
5189
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -4971,44 +5203,44 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4971
5203
|
const failures = [];
|
|
4972
5204
|
for (const item of items) {
|
|
4973
5205
|
if (!item.path || !isSafeCatalogPath2(item.path)) continue;
|
|
4974
|
-
const absPath =
|
|
5206
|
+
const absPath = path28.join(manifestDir, item.path);
|
|
4975
5207
|
if (item.type === "skill") {
|
|
4976
|
-
const skillMd =
|
|
4977
|
-
if (!
|
|
4978
|
-
failures.push(`${item.id}: missing ${
|
|
5208
|
+
const skillMd = path28.join(absPath, "SKILL.md");
|
|
5209
|
+
if (!fs22.existsSync(skillMd)) {
|
|
5210
|
+
failures.push(`${item.id}: missing ${path28.relative(manifestDir, skillMd)}`);
|
|
4979
5211
|
continue;
|
|
4980
5212
|
}
|
|
4981
|
-
const text =
|
|
5213
|
+
const text = fs22.readFileSync(skillMd, "utf8");
|
|
4982
5214
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
4983
5215
|
failures.push(
|
|
4984
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
5216
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path28.relative(manifestDir, skillMd)}`)
|
|
4985
5217
|
);
|
|
4986
5218
|
} else if (item.type === "agent") {
|
|
4987
|
-
if (!
|
|
5219
|
+
if (!fs22.existsSync(absPath)) {
|
|
4988
5220
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
4989
5221
|
continue;
|
|
4990
5222
|
}
|
|
4991
|
-
const text =
|
|
4992
|
-
const rel =
|
|
5223
|
+
const text = fs22.readFileSync(absPath, "utf8");
|
|
5224
|
+
const rel = path28.relative(manifestDir, absPath);
|
|
4993
5225
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
4994
5226
|
failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
|
|
4995
5227
|
} else if (item.type === "template") {
|
|
4996
|
-
if (!
|
|
5228
|
+
if (!fs22.existsSync(absPath)) {
|
|
4997
5229
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
4998
5230
|
continue;
|
|
4999
5231
|
}
|
|
5000
5232
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
5001
5233
|
} else if (item.type === "command") {
|
|
5002
|
-
if (!
|
|
5234
|
+
if (!fs22.existsSync(absPath)) {
|
|
5003
5235
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
5004
5236
|
continue;
|
|
5005
5237
|
}
|
|
5006
|
-
const text =
|
|
5007
|
-
const rel =
|
|
5238
|
+
const text = fs22.readFileSync(absPath, "utf8");
|
|
5239
|
+
const rel = path28.relative(manifestDir, absPath);
|
|
5008
5240
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
5009
5241
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
5010
5242
|
} else if (item.type === "config") {
|
|
5011
|
-
if (!
|
|
5243
|
+
if (!fs22.existsSync(absPath)) {
|
|
5012
5244
|
failures.push(`${item.id}: missing config path ${item.path}`);
|
|
5013
5245
|
}
|
|
5014
5246
|
}
|
|
@@ -5022,8 +5254,8 @@ var DIR_ITEM_TYPE = {
|
|
|
5022
5254
|
commands: "command"
|
|
5023
5255
|
};
|
|
5024
5256
|
function walkMd(dir, fn) {
|
|
5025
|
-
for (const entry of
|
|
5026
|
-
const full =
|
|
5257
|
+
for (const entry of fs22.readdirSync(dir, { withFileTypes: true })) {
|
|
5258
|
+
const full = path28.join(dir, entry.name);
|
|
5027
5259
|
if (entry.isDirectory()) walkMd(full, fn);
|
|
5028
5260
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
5029
5261
|
}
|
|
@@ -5032,12 +5264,12 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
5032
5264
|
const failures = [];
|
|
5033
5265
|
const pathSourceMap = buildItemPathSourceMap(items);
|
|
5034
5266
|
for (const dir of ["skills", "agents", "templates", "commands"]) {
|
|
5035
|
-
const abs =
|
|
5036
|
-
if (!
|
|
5267
|
+
const abs = path28.join(manifestDir, dir);
|
|
5268
|
+
if (!fs22.existsSync(abs)) continue;
|
|
5037
5269
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
5038
5270
|
walkMd(abs, (file) => {
|
|
5039
|
-
const text =
|
|
5040
|
-
const rel =
|
|
5271
|
+
const text = fs22.readFileSync(file, "utf8");
|
|
5272
|
+
const rel = path28.relative(manifestDir, file);
|
|
5041
5273
|
const norm = rel.replace(/\\/g, "/");
|
|
5042
5274
|
const source = resolveMarkdownItemSource(norm, pathSourceMap);
|
|
5043
5275
|
const checkNonTsxNpx = !isNpxTsxOnlyExempt(dirType, source);
|
|
@@ -5058,6 +5290,7 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
5058
5290
|
function validateCatalogData(manifestDir, manifestVersion, items) {
|
|
5059
5291
|
const failures = [
|
|
5060
5292
|
...auditManifestStructure(manifestVersion, items),
|
|
5293
|
+
...auditOptInMetadata(items),
|
|
5061
5294
|
...auditForbiddenStacks(items),
|
|
5062
5295
|
...auditShippedFiles(manifestDir, items),
|
|
5063
5296
|
...auditMarkdownContent(manifestDir, items),
|
|
@@ -5073,8 +5306,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
5073
5306
|
process.exitCode = 1;
|
|
5074
5307
|
return;
|
|
5075
5308
|
}
|
|
5076
|
-
const abs =
|
|
5077
|
-
const manifestDir =
|
|
5309
|
+
const abs = path29.resolve(process.cwd(), manifestPath2);
|
|
5310
|
+
const manifestDir = path29.dirname(abs);
|
|
5078
5311
|
const data = await readJson(abs);
|
|
5079
5312
|
if (!data?.items) {
|
|
5080
5313
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -5092,7 +5325,7 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
5092
5325
|
|
|
5093
5326
|
// src/commands/workspace.ts
|
|
5094
5327
|
import { existsSync as existsSync5, statSync as statSync2 } from "node:fs";
|
|
5095
|
-
import
|
|
5328
|
+
import path36 from "node:path";
|
|
5096
5329
|
|
|
5097
5330
|
// src/commands/workspace/aggregate.ts
|
|
5098
5331
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -5146,7 +5379,7 @@ ${summaries.map(
|
|
|
5146
5379
|
}
|
|
5147
5380
|
|
|
5148
5381
|
// src/commands/workspace/config.ts
|
|
5149
|
-
import
|
|
5382
|
+
import path30 from "node:path";
|
|
5150
5383
|
import YAML from "yaml";
|
|
5151
5384
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
5152
5385
|
function parseWorkspaceConfig(text) {
|
|
@@ -5169,11 +5402,11 @@ function parseWorkspaceConfig(text) {
|
|
|
5169
5402
|
};
|
|
5170
5403
|
}
|
|
5171
5404
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
5172
|
-
return parseWorkspaceConfig(await readText(
|
|
5405
|
+
return parseWorkspaceConfig(await readText(path30.join(workspaceRoot, WORKSPACE_FILE)));
|
|
5173
5406
|
}
|
|
5174
5407
|
|
|
5175
5408
|
// src/commands/workspace/discover.ts
|
|
5176
|
-
import
|
|
5409
|
+
import path31 from "node:path";
|
|
5177
5410
|
import fg4 from "fast-glob";
|
|
5178
5411
|
import YAML2 from "yaml";
|
|
5179
5412
|
var DEFAULT_MAX_DEPTH = 3;
|
|
@@ -5201,8 +5434,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
5201
5434
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
5202
5435
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
5203
5436
|
for (const match of matches) {
|
|
5204
|
-
const base =
|
|
5205
|
-
const dir =
|
|
5437
|
+
const base = path31.posix.basename(match);
|
|
5438
|
+
const dir = path31.posix.dirname(match);
|
|
5206
5439
|
const owner = dir === "." ? "." : dir;
|
|
5207
5440
|
if (base === ".git") gitDirs.add(owner);
|
|
5208
5441
|
else manifestDirs.add(owner);
|
|
@@ -5218,9 +5451,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
5218
5451
|
}
|
|
5219
5452
|
repoRoots.sort((a, b) => a.localeCompare(b));
|
|
5220
5453
|
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
5221
|
-
const absDir =
|
|
5222
|
-
const pkg = await readJson(
|
|
5223
|
-
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name :
|
|
5454
|
+
const absDir = path31.resolve(workspaceRoot, relDir);
|
|
5455
|
+
const pkg = await readJson(path31.join(absDir, "package.json"));
|
|
5456
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path31.basename(relDir === "." ? workspaceRoot : absDir);
|
|
5224
5457
|
let role = "auto";
|
|
5225
5458
|
try {
|
|
5226
5459
|
const scan = await scanProject(absDir);
|
|
@@ -5263,7 +5496,7 @@ function renderWorkspaceYaml(config) {
|
|
|
5263
5496
|
});
|
|
5264
5497
|
}
|
|
5265
5498
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
5266
|
-
const yamlPath =
|
|
5499
|
+
const yamlPath = path31.join(workspaceRoot, "haus.workspace.yaml");
|
|
5267
5500
|
const existingText = await readText(yamlPath);
|
|
5268
5501
|
const existing = parseWorkspaceConfig(existingText);
|
|
5269
5502
|
if (existingText && !existing) {
|
|
@@ -5297,18 +5530,18 @@ async function runDiscover(workspaceRoot, opts = {}) {
|
|
|
5297
5530
|
|
|
5298
5531
|
// src/commands/workspace/doctor.ts
|
|
5299
5532
|
import { existsSync as existsSync3 } from "node:fs";
|
|
5300
|
-
import
|
|
5533
|
+
import path33 from "node:path";
|
|
5301
5534
|
|
|
5302
5535
|
// src/commands/workspace/manifest.ts
|
|
5303
5536
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
5304
|
-
import
|
|
5537
|
+
import path32 from "node:path";
|
|
5305
5538
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
5306
5539
|
function manifestPath(workspaceRoot) {
|
|
5307
5540
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
5308
5541
|
}
|
|
5309
5542
|
function hausVersion() {
|
|
5310
5543
|
try {
|
|
5311
|
-
const pkg = JSON.parse(readFileSync3(
|
|
5544
|
+
const pkg = JSON.parse(readFileSync3(path32.join(packageRoot(), "package.json"), "utf8"));
|
|
5312
5545
|
return pkg.version ?? "0.0.0";
|
|
5313
5546
|
} catch {
|
|
5314
5547
|
return "0.0.0";
|
|
@@ -5374,7 +5607,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
5374
5607
|
}
|
|
5375
5608
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
5376
5609
|
for (const repo of config.repos) {
|
|
5377
|
-
const repoRoot =
|
|
5610
|
+
const repoRoot = path33.resolve(workspaceRoot, repo.path);
|
|
5378
5611
|
const entry = manifestByName.get(repo.name);
|
|
5379
5612
|
if (!entry) {
|
|
5380
5613
|
flag({
|
|
@@ -5448,11 +5681,11 @@ function emit(args) {
|
|
|
5448
5681
|
|
|
5449
5682
|
// src/commands/workspace/setup.ts
|
|
5450
5683
|
import { existsSync as existsSync4, statSync } from "node:fs";
|
|
5451
|
-
import
|
|
5684
|
+
import path35 from "node:path";
|
|
5452
5685
|
|
|
5453
5686
|
// src/claude/write-workspace-claude-md.ts
|
|
5454
|
-
import
|
|
5455
|
-
import
|
|
5687
|
+
import path34 from "node:path";
|
|
5688
|
+
import fs23 from "fs-extra";
|
|
5456
5689
|
function buildWorkspaceImportBlock(client, members) {
|
|
5457
5690
|
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
5458
5691
|
const body = [
|
|
@@ -5470,8 +5703,8 @@ ${BLOCK_END}`;
|
|
|
5470
5703
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
5471
5704
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
5472
5705
|
const dryRun = opts.dryRun ?? false;
|
|
5473
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
5474
|
-
const prev = await
|
|
5706
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path34.join(workspaceRoot, "CLAUDE.md");
|
|
5707
|
+
const prev = await fs23.pathExists(filePath) ? await fs23.readFile(filePath, "utf8") : "";
|
|
5475
5708
|
const next = opts.collision ? `${block}
|
|
5476
5709
|
` : injectHausBlock(prev, block);
|
|
5477
5710
|
const printable = displayPath(workspaceRoot, filePath);
|
|
@@ -5496,20 +5729,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
5496
5729
|
|
|
5497
5730
|
// src/commands/workspace/setup.ts
|
|
5498
5731
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
5499
|
-
let dir =
|
|
5732
|
+
let dir = path35.resolve(start);
|
|
5500
5733
|
for (; ; ) {
|
|
5501
|
-
if (existsSync4(
|
|
5502
|
-
const parent =
|
|
5503
|
-
if (parent === dir) return
|
|
5734
|
+
if (existsSync4(path35.join(dir, WORKSPACE_FILE))) return dir;
|
|
5735
|
+
const parent = path35.dirname(dir);
|
|
5736
|
+
if (parent === dir) return path35.resolve(start);
|
|
5504
5737
|
dir = parent;
|
|
5505
5738
|
}
|
|
5506
5739
|
}
|
|
5507
5740
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
5508
|
-
return
|
|
5741
|
+
return path35.resolve(workspaceRoot, repoPath) === path35.resolve(workspaceRoot);
|
|
5509
5742
|
}
|
|
5510
5743
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
5511
5744
|
const apply = options.write ?? false;
|
|
5512
|
-
const configText = await readText(
|
|
5745
|
+
const configText = await readText(path35.join(workspaceRoot, WORKSPACE_FILE));
|
|
5513
5746
|
if (!configText) {
|
|
5514
5747
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
5515
5748
|
process.exitCode = 1;
|
|
@@ -5526,7 +5759,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
5526
5759
|
const statuses = [];
|
|
5527
5760
|
const aggregateInputs = [];
|
|
5528
5761
|
for (const repo of repos) {
|
|
5529
|
-
const repoRoot =
|
|
5762
|
+
const repoRoot = path35.resolve(workspaceRoot, repo.path);
|
|
5530
5763
|
log(`
|
|
5531
5764
|
\u2192 ${repo.name} (${repo.path})`);
|
|
5532
5765
|
try {
|
|
@@ -5594,7 +5827,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
5594
5827
|
const status = statusByName.get(repo.name);
|
|
5595
5828
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
5596
5829
|
if (status?.status === "ok") {
|
|
5597
|
-
const lock = await checkLock(
|
|
5830
|
+
const lock = await checkLock(path35.resolve(workspaceRoot, repo.path));
|
|
5598
5831
|
manifestRepos.push({
|
|
5599
5832
|
name: repo.name,
|
|
5600
5833
|
path: repo.path,
|
|
@@ -5674,7 +5907,7 @@ relationships: []
|
|
|
5674
5907
|
log("Workspace initialized.");
|
|
5675
5908
|
}
|
|
5676
5909
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
5677
|
-
const configText = await readText(
|
|
5910
|
+
const configText = await readText(path36.join(workspaceRoot, WORKSPACE_FILE));
|
|
5678
5911
|
if (!configText) {
|
|
5679
5912
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
5680
5913
|
process.exitCode = 1;
|
|
@@ -5695,7 +5928,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
5695
5928
|
}
|
|
5696
5929
|
const inputs = [];
|
|
5697
5930
|
for (const repo of config.repos) {
|
|
5698
|
-
const repoRoot =
|
|
5931
|
+
const repoRoot = path36.resolve(workspaceRoot, repo.path);
|
|
5699
5932
|
if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
|
|
5700
5933
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
5701
5934
|
}
|
|
@@ -5745,7 +5978,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
5745
5978
|
// src/cli.ts
|
|
5746
5979
|
function cliVersion() {
|
|
5747
5980
|
try {
|
|
5748
|
-
const pkgPath =
|
|
5981
|
+
const pkgPath = path37.join(packageRoot(), "package.json");
|
|
5749
5982
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5750
5983
|
return pkg.version ?? "0.0.0";
|
|
5751
5984
|
} catch {
|
|
@@ -5755,7 +5988,7 @@ function cliVersion() {
|
|
|
5755
5988
|
var program = new Command();
|
|
5756
5989
|
function validateRuntimeNodeVersion() {
|
|
5757
5990
|
try {
|
|
5758
|
-
const pkgPath =
|
|
5991
|
+
const pkgPath = path37.join(packageRoot(), "package.json");
|
|
5759
5992
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5760
5993
|
const requiredRange = pkg.engines?.node;
|
|
5761
5994
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -5770,10 +6003,16 @@ function validateRuntimeNodeVersion() {
|
|
|
5770
6003
|
validateRuntimeNodeVersion();
|
|
5771
6004
|
program.name("haus").description("Haus AI workflow CLI").version(cliVersion());
|
|
5772
6005
|
program.command("scan").option("--json").action(runScan);
|
|
5773
|
-
program.command("recommend").option("--json").
|
|
6006
|
+
program.command("recommend").option("--json").option(
|
|
6007
|
+
"--include <ids...>",
|
|
6008
|
+
"Force opt-in catalog items into the recommendation as manual selections (space- or comma-separated ids)"
|
|
6009
|
+
).action(runRecommend);
|
|
5774
6010
|
program.command("setup-project").option("--json").action(runSetupProject);
|
|
5775
6011
|
program.command("doctor").option("--hooks", "Verify .claude/settings.json matches the hook contract").action(runDoctor);
|
|
5776
6012
|
program.command("apply").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option(
|
|
6013
|
+
"--ids <ids...>",
|
|
6014
|
+
"Install exactly these recommended item ids non-interactively (skill backend; space- or comma-separated)"
|
|
6015
|
+
).option(
|
|
5777
6016
|
"--allow-empty-cache",
|
|
5778
6017
|
"Apply core files only when catalog cache is empty (skip catalog items without error)"
|
|
5779
6018
|
).option(
|