@haus-tech/haus-workflow 0.31.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 +7 -0
- package/dist/cli.js +316 -237
- package/library/catalog/manifest.json +13 -13
- 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 +4 -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,13 +2453,13 @@ 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
|
}
|
|
@@ -2407,7 +2474,7 @@ function parseIdList(value) {
|
|
|
2407
2474
|
|
|
2408
2475
|
// src/commands/apply.ts
|
|
2409
2476
|
async function cacheHasItems() {
|
|
2410
|
-
const data = await readJson(
|
|
2477
|
+
const data = await readJson(path14.join(getCacheDir(), "manifest.json"));
|
|
2411
2478
|
return Array.isArray(data?.items) && data.items.length > 0;
|
|
2412
2479
|
}
|
|
2413
2480
|
function installableRecommendedItems(recommended) {
|
|
@@ -2534,7 +2601,7 @@ async function runCatalogAudit() {
|
|
|
2534
2601
|
|
|
2535
2602
|
// src/commands/clone.ts
|
|
2536
2603
|
import { existsSync as existsSync2 } from "node:fs";
|
|
2537
|
-
import
|
|
2604
|
+
import path15 from "node:path";
|
|
2538
2605
|
|
|
2539
2606
|
// src/utils/exec.ts
|
|
2540
2607
|
import { execa } from "execa";
|
|
@@ -2587,9 +2654,9 @@ async function runClone(url, opts = {}) {
|
|
|
2587
2654
|
process.exitCode = 1;
|
|
2588
2655
|
return;
|
|
2589
2656
|
}
|
|
2590
|
-
const target =
|
|
2657
|
+
const target = path15.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
2591
2658
|
if (existsSync2(target)) {
|
|
2592
|
-
log(`\u2022 ${
|
|
2659
|
+
log(`\u2022 ${path15.basename(target)} already present at ${target} \u2014 skipped`);
|
|
2593
2660
|
return;
|
|
2594
2661
|
}
|
|
2595
2662
|
if (opts.dryRun) {
|
|
@@ -2606,11 +2673,11 @@ async function runClone(url, opts = {}) {
|
|
|
2606
2673
|
}
|
|
2607
2674
|
|
|
2608
2675
|
// src/commands/doctor.ts
|
|
2609
|
-
import
|
|
2610
|
-
import
|
|
2676
|
+
import path20 from "node:path";
|
|
2677
|
+
import fs15 from "fs-extra";
|
|
2611
2678
|
|
|
2612
2679
|
// src/scanner/scan-project.ts
|
|
2613
|
-
import
|
|
2680
|
+
import path19 from "node:path";
|
|
2614
2681
|
|
|
2615
2682
|
// src/utils/audit-checks.ts
|
|
2616
2683
|
function isRecord(v) {
|
|
@@ -2637,8 +2704,8 @@ function compareVersions(a, b) {
|
|
|
2637
2704
|
}
|
|
2638
2705
|
|
|
2639
2706
|
// src/scanner/detect-package-manager.ts
|
|
2640
|
-
import
|
|
2641
|
-
import
|
|
2707
|
+
import path16 from "node:path";
|
|
2708
|
+
import fs14 from "fs-extra";
|
|
2642
2709
|
function detectPackageManager(root, packageManagerField) {
|
|
2643
2710
|
const field = String(packageManagerField ?? "").trim();
|
|
2644
2711
|
if (field.startsWith("yarn@")) {
|
|
@@ -2656,9 +2723,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
2656
2723
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
2657
2724
|
return "unknown";
|
|
2658
2725
|
}
|
|
2659
|
-
if (
|
|
2660
|
-
if (
|
|
2661
|
-
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";
|
|
2662
2729
|
return "unknown";
|
|
2663
2730
|
}
|
|
2664
2731
|
|
|
@@ -2860,7 +2927,7 @@ function runDetection(ctx, rules = STACK_RULES) {
|
|
|
2860
2927
|
}
|
|
2861
2928
|
|
|
2862
2929
|
// src/scanner/detection.ts
|
|
2863
|
-
import
|
|
2930
|
+
import path17 from "node:path";
|
|
2864
2931
|
var UNSUPPORTED_MARKERS = {
|
|
2865
2932
|
"requirements.txt": "python",
|
|
2866
2933
|
"pyproject.toml": "python",
|
|
@@ -2914,14 +2981,14 @@ function finalizeRoles(registryRoles, deps, files) {
|
|
|
2914
2981
|
function collectUnsupportedSignals(files) {
|
|
2915
2982
|
return [
|
|
2916
2983
|
...new Set(
|
|
2917
|
-
files.map((f) => UNSUPPORTED_MARKERS[
|
|
2984
|
+
files.map((f) => UNSUPPORTED_MARKERS[path17.basename(f)]).filter((s) => Boolean(s))
|
|
2918
2985
|
)
|
|
2919
2986
|
].sort();
|
|
2920
2987
|
}
|
|
2921
2988
|
|
|
2922
2989
|
// src/scanner/render.ts
|
|
2923
2990
|
import { readFile } from "node:fs/promises";
|
|
2924
|
-
import
|
|
2991
|
+
import path18 from "node:path";
|
|
2925
2992
|
async function buildContentBlob(root, files) {
|
|
2926
2993
|
const candidates = files.filter(
|
|
2927
2994
|
(f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
|
|
@@ -2929,7 +2996,7 @@ async function buildContentBlob(root, files) {
|
|
|
2929
2996
|
const slice = candidates.slice(0, 300);
|
|
2930
2997
|
const parts = await mapWithConcurrency(slice, async (rel) => {
|
|
2931
2998
|
try {
|
|
2932
|
-
return await readFile(
|
|
2999
|
+
return await readFile(path18.join(root, rel), "utf8");
|
|
2933
3000
|
} catch {
|
|
2934
3001
|
return "";
|
|
2935
3002
|
}
|
|
@@ -3016,8 +3083,8 @@ var SAFE_FILES = [
|
|
|
3016
3083
|
"Gemfile"
|
|
3017
3084
|
];
|
|
3018
3085
|
async function scanProject(root) {
|
|
3019
|
-
const pkg = await readJson(
|
|
3020
|
-
const composer = await readJson(
|
|
3086
|
+
const pkg = await readJson(path19.join(root, "package.json"));
|
|
3087
|
+
const composer = await readJson(path19.join(root, "composer.json"));
|
|
3021
3088
|
const files = await listFiles(root, SAFE_FILES);
|
|
3022
3089
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
3023
3090
|
const deps = dependencySet(pkg, composer);
|
|
@@ -3046,7 +3113,7 @@ async function scanProject(root) {
|
|
|
3046
3113
|
const context = {
|
|
3047
3114
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3048
3115
|
root,
|
|
3049
|
-
repoName: String(pkg?.name ??
|
|
3116
|
+
repoName: String(pkg?.name ?? path19.basename(root)),
|
|
3050
3117
|
packageManager,
|
|
3051
3118
|
repoRoles: roles,
|
|
3052
3119
|
detectedStacks: stacks,
|
|
@@ -3143,7 +3210,7 @@ async function runDoctor(options) {
|
|
|
3143
3210
|
} else {
|
|
3144
3211
|
ok(`- HOOKS OK: ${hooks.message}`);
|
|
3145
3212
|
}
|
|
3146
|
-
const rootClaudeMdPath =
|
|
3213
|
+
const rootClaudeMdPath = path20.join(root, "CLAUDE.md");
|
|
3147
3214
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
3148
3215
|
if (!rootClaudeMdContent) {
|
|
3149
3216
|
flag(
|
|
@@ -3171,7 +3238,7 @@ async function runDoctor(options) {
|
|
|
3171
3238
|
const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
|
|
3172
3239
|
const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
|
|
3173
3240
|
for (const target of importTargets) {
|
|
3174
|
-
if (!await
|
|
3241
|
+
if (!await fs15.pathExists(hausPath(root, target))) {
|
|
3175
3242
|
flag(
|
|
3176
3243
|
`- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
|
|
3177
3244
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -3182,7 +3249,7 @@ async function runDoctor(options) {
|
|
|
3182
3249
|
}
|
|
3183
3250
|
}
|
|
3184
3251
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
3185
|
-
const workflowExists = await
|
|
3252
|
+
const workflowExists = await fs15.pathExists(workflowPath);
|
|
3186
3253
|
if (!workflowExists) {
|
|
3187
3254
|
flag(
|
|
3188
3255
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -3205,15 +3272,15 @@ async function runDoctor(options) {
|
|
|
3205
3272
|
"haus apply --write --force"
|
|
3206
3273
|
);
|
|
3207
3274
|
} else {
|
|
3208
|
-
const cachePath =
|
|
3209
|
-
const bundledPath =
|
|
3275
|
+
const cachePath = path20.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
3276
|
+
const bundledPath = path20.join(
|
|
3210
3277
|
packageRoot(),
|
|
3211
3278
|
"library",
|
|
3212
3279
|
"global",
|
|
3213
3280
|
"templates",
|
|
3214
3281
|
"agentic-workflow-standard.md"
|
|
3215
3282
|
);
|
|
3216
|
-
const templatePath = await
|
|
3283
|
+
const templatePath = await fs15.pathExists(cachePath) ? cachePath : bundledPath;
|
|
3217
3284
|
const templateContent = await readText(templatePath);
|
|
3218
3285
|
if (storedHashMatch && templateContent) {
|
|
3219
3286
|
const currentHash = hashText(normaliseLF(templateContent));
|
|
@@ -3233,7 +3300,7 @@ async function runDoctor(options) {
|
|
|
3233
3300
|
}
|
|
3234
3301
|
}
|
|
3235
3302
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
3236
|
-
const workflowConfigExists = await
|
|
3303
|
+
const workflowConfigExists = await fs15.pathExists(workflowConfigPath);
|
|
3237
3304
|
if (!workflowConfigExists) {
|
|
3238
3305
|
flag(
|
|
3239
3306
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -3241,7 +3308,7 @@ async function runDoctor(options) {
|
|
|
3241
3308
|
"haus apply --write"
|
|
3242
3309
|
);
|
|
3243
3310
|
} else {
|
|
3244
|
-
const cfg = await
|
|
3311
|
+
const cfg = await fs15.readFile(workflowConfigPath, "utf8");
|
|
3245
3312
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
3246
3313
|
if (unfilled > 0) {
|
|
3247
3314
|
flag(
|
|
@@ -3253,6 +3320,18 @@ async function runDoctor(options) {
|
|
|
3253
3320
|
ok("- .haus-workflow/workflow-config.md: OK (project-owned)");
|
|
3254
3321
|
}
|
|
3255
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
|
+
}
|
|
3256
3335
|
const cacheAgeMs = await getCacheManifestAge();
|
|
3257
3336
|
if (cacheAgeMs === null) {
|
|
3258
3337
|
flag(
|
|
@@ -3272,7 +3351,7 @@ async function runDoctor(options) {
|
|
|
3272
3351
|
ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
|
|
3273
3352
|
}
|
|
3274
3353
|
}
|
|
3275
|
-
const pkgJson = await readJson(
|
|
3354
|
+
const pkgJson = await readJson(path20.join(packageRoot(), "package.json"));
|
|
3276
3355
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
3277
3356
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
3278
3357
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -3494,8 +3573,8 @@ async function runGuard(kind, _options) {
|
|
|
3494
3573
|
}
|
|
3495
3574
|
|
|
3496
3575
|
// src/commands/init.ts
|
|
3497
|
-
import
|
|
3498
|
-
import
|
|
3576
|
+
import path21 from "node:path";
|
|
3577
|
+
import fs16 from "fs-extra";
|
|
3499
3578
|
|
|
3500
3579
|
// src/utils/prompts.ts
|
|
3501
3580
|
import { stdin as input, stdout as output } from "node:process";
|
|
@@ -4019,8 +4098,8 @@ async function runSetupProject(options) {
|
|
|
4019
4098
|
// src/commands/init.ts
|
|
4020
4099
|
async function runInit(options) {
|
|
4021
4100
|
const root = process.cwd();
|
|
4022
|
-
const hausDir =
|
|
4023
|
-
const alreadyInit = await
|
|
4101
|
+
const hausDir = path21.join(root, ".haus-workflow");
|
|
4102
|
+
const alreadyInit = await fs16.pathExists(hausDir);
|
|
4024
4103
|
if (alreadyInit) {
|
|
4025
4104
|
log("Haus AI already initialized in this project.");
|
|
4026
4105
|
log("Run `haus setup-project` to reconfigure.");
|
|
@@ -4032,8 +4111,8 @@ async function runInit(options) {
|
|
|
4032
4111
|
|
|
4033
4112
|
// src/install/apply.ts
|
|
4034
4113
|
import crypto2 from "node:crypto";
|
|
4035
|
-
import
|
|
4036
|
-
import
|
|
4114
|
+
import path22 from "node:path";
|
|
4115
|
+
import fs17 from "fs-extra";
|
|
4037
4116
|
|
|
4038
4117
|
// src/install/header.ts
|
|
4039
4118
|
var MD_PREFIX = "<!-- HAUS-MANAGED";
|
|
@@ -4119,40 +4198,40 @@ function hashContent(content2) {
|
|
|
4119
4198
|
}
|
|
4120
4199
|
function sourceVersion() {
|
|
4121
4200
|
try {
|
|
4122
|
-
const pkgPath =
|
|
4123
|
-
const pkg = JSON.parse(
|
|
4201
|
+
const pkgPath = path22.join(packageRoot(), "package.json");
|
|
4202
|
+
const pkg = JSON.parse(fs17.readFileSync(pkgPath, "utf8"));
|
|
4124
4203
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
4125
4204
|
} catch {
|
|
4126
4205
|
return "haus@0.0.0";
|
|
4127
4206
|
}
|
|
4128
4207
|
}
|
|
4129
4208
|
function globalSrcDir() {
|
|
4130
|
-
return
|
|
4209
|
+
return path22.join(packageRoot(), "library", "global");
|
|
4131
4210
|
}
|
|
4132
4211
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
4133
4212
|
const entries = [];
|
|
4134
|
-
const skillsDir =
|
|
4135
|
-
if (
|
|
4136
|
-
for (const skillName of
|
|
4137
|
-
const skillFile =
|
|
4138
|
-
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)) {
|
|
4139
4218
|
entries.push({
|
|
4140
4219
|
stableId: `skill.${skillName}`,
|
|
4141
|
-
srcRelPath:
|
|
4142
|
-
destPath:
|
|
4220
|
+
srcRelPath: path22.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
4221
|
+
destPath: path22.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
4143
4222
|
});
|
|
4144
4223
|
}
|
|
4145
4224
|
}
|
|
4146
4225
|
}
|
|
4147
|
-
const commandsDir =
|
|
4148
|
-
if (
|
|
4149
|
-
for (const fileName of
|
|
4226
|
+
const commandsDir = path22.join(srcDir, "commands");
|
|
4227
|
+
if (fs17.pathExistsSync(commandsDir)) {
|
|
4228
|
+
for (const fileName of fs17.readdirSync(commandsDir)) {
|
|
4150
4229
|
if (!fileName.endsWith(".md")) continue;
|
|
4151
4230
|
const commandName = fileName.slice(0, -".md".length);
|
|
4152
4231
|
entries.push({
|
|
4153
4232
|
stableId: `command.${commandName}`,
|
|
4154
|
-
srcRelPath:
|
|
4155
|
-
destPath:
|
|
4233
|
+
srcRelPath: path22.join("library", "global", "commands", fileName),
|
|
4234
|
+
destPath: path22.join(claudeDir, "commands", fileName)
|
|
4156
4235
|
});
|
|
4157
4236
|
}
|
|
4158
4237
|
}
|
|
@@ -4176,7 +4255,7 @@ async function applyInstall(options = {}) {
|
|
|
4176
4255
|
};
|
|
4177
4256
|
const manifestFiles = [];
|
|
4178
4257
|
for (const entry of sourceFiles) {
|
|
4179
|
-
const srcPath =
|
|
4258
|
+
const srcPath = path22.join(packageRoot(), entry.srcRelPath);
|
|
4180
4259
|
const rawContent = await readText(srcPath);
|
|
4181
4260
|
if (rawContent === void 0) {
|
|
4182
4261
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -4196,7 +4275,7 @@ async function applyInstall(options = {}) {
|
|
|
4196
4275
|
}
|
|
4197
4276
|
continue;
|
|
4198
4277
|
}
|
|
4199
|
-
const destExists =
|
|
4278
|
+
const destExists = fs17.pathExistsSync(entry.destPath);
|
|
4200
4279
|
if (destExists) {
|
|
4201
4280
|
const currentContent = await readText(entry.destPath);
|
|
4202
4281
|
if (currentContent !== void 0) {
|
|
@@ -4232,7 +4311,7 @@ async function applyInstall(options = {}) {
|
|
|
4232
4311
|
schemaVersion: SCHEMA_VERSION2
|
|
4233
4312
|
});
|
|
4234
4313
|
}
|
|
4235
|
-
const fragmentPath =
|
|
4314
|
+
const fragmentPath = path22.join(srcDir, "settings-fragments", "hooks.json");
|
|
4236
4315
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
4237
4316
|
const settings = await readSettings();
|
|
4238
4317
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -4244,13 +4323,13 @@ async function applyInstall(options = {}) {
|
|
|
4244
4323
|
const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
|
|
4245
4324
|
for (const entry of existingManifest.files) {
|
|
4246
4325
|
if (currentDestPaths.has(entry.destPath)) continue;
|
|
4247
|
-
if (!
|
|
4326
|
+
if (!fs17.pathExistsSync(entry.destPath)) continue;
|
|
4248
4327
|
const content2 = await readText(entry.destPath);
|
|
4249
4328
|
if (!content2) continue;
|
|
4250
4329
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
4251
4330
|
const currentHash = hashContent(content2);
|
|
4252
4331
|
if (hasHeader && currentHash === entry.hash) {
|
|
4253
|
-
if (!dryRun) await
|
|
4332
|
+
if (!dryRun) await fs17.remove(entry.destPath);
|
|
4254
4333
|
result.deleted.push(entry.destPath);
|
|
4255
4334
|
} else {
|
|
4256
4335
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -4418,8 +4497,8 @@ async function runRefresh() {
|
|
|
4418
4497
|
}
|
|
4419
4498
|
|
|
4420
4499
|
// src/install/scaffold.ts
|
|
4421
|
-
import
|
|
4422
|
-
import
|
|
4500
|
+
import path23 from "node:path";
|
|
4501
|
+
import fs18 from "fs-extra";
|
|
4423
4502
|
async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
4424
4503
|
const scaffolded = [];
|
|
4425
4504
|
const skipped = [];
|
|
@@ -4432,27 +4511,27 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
4432
4511
|
skipped.push(name);
|
|
4433
4512
|
if (outcome === "skipped-exists") skippedExisting.push(name);
|
|
4434
4513
|
};
|
|
4435
|
-
const realRoot = await
|
|
4514
|
+
const realRoot = await fs18.realpath(catalogRoot).catch(() => null);
|
|
4436
4515
|
for (const item of items) {
|
|
4437
4516
|
if (item.type !== "config") continue;
|
|
4438
|
-
const sourcePath =
|
|
4439
|
-
const rel =
|
|
4440
|
-
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 === "..");
|
|
4441
4520
|
if (escapes) {
|
|
4442
4521
|
warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
|
|
4443
4522
|
continue;
|
|
4444
4523
|
}
|
|
4445
|
-
const realSource = await
|
|
4524
|
+
const realSource = await fs18.realpath(sourcePath).catch(() => null);
|
|
4446
4525
|
if (!realRoot || !realSource) {
|
|
4447
4526
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
4448
4527
|
continue;
|
|
4449
4528
|
}
|
|
4450
|
-
const realRel =
|
|
4451
|
-
if (realRel !== "" && (
|
|
4529
|
+
const realRel = path23.relative(realRoot, realSource);
|
|
4530
|
+
if (realRel !== "" && (path23.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
|
|
4452
4531
|
warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
|
|
4453
4532
|
continue;
|
|
4454
4533
|
}
|
|
4455
|
-
const stat = await
|
|
4534
|
+
const stat = await fs18.lstat(sourcePath).catch(() => null);
|
|
4456
4535
|
if (!stat) {
|
|
4457
4536
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
4458
4537
|
continue;
|
|
@@ -4462,14 +4541,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
4462
4541
|
continue;
|
|
4463
4542
|
}
|
|
4464
4543
|
if (stat.isFile()) {
|
|
4465
|
-
const filename =
|
|
4466
|
-
const dest =
|
|
4544
|
+
const filename = path23.basename(sourcePath);
|
|
4545
|
+
const dest = path23.join(projectRoot, filename);
|
|
4467
4546
|
record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
|
|
4468
4547
|
} else if (stat.isDirectory()) {
|
|
4469
|
-
const entries = await
|
|
4548
|
+
const entries = await fs18.readdir(sourcePath);
|
|
4470
4549
|
for (const entry of entries) {
|
|
4471
|
-
const src =
|
|
4472
|
-
const dest =
|
|
4550
|
+
const src = path23.join(sourcePath, entry);
|
|
4551
|
+
const dest = path23.join(projectRoot, entry);
|
|
4473
4552
|
record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
|
|
4474
4553
|
}
|
|
4475
4554
|
}
|
|
@@ -4477,42 +4556,42 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
4477
4556
|
return { scaffolded, skipped, skippedExisting };
|
|
4478
4557
|
}
|
|
4479
4558
|
async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
4480
|
-
const destDisplay =
|
|
4481
|
-
const srcStat = await
|
|
4559
|
+
const destDisplay = path23.relative(projectRoot, dest) || path23.basename(dest);
|
|
4560
|
+
const srcStat = await fs18.lstat(src).catch(() => null);
|
|
4482
4561
|
if (!srcStat) {
|
|
4483
|
-
warn(`Skipping ${
|
|
4562
|
+
warn(`Skipping ${path23.basename(src)}: source not found`);
|
|
4484
4563
|
return "skipped-refused";
|
|
4485
4564
|
}
|
|
4486
4565
|
if (srcStat.isSymbolicLink()) {
|
|
4487
|
-
warn(`Skipping ${
|
|
4566
|
+
warn(`Skipping ${path23.basename(src)}: source is a symlink`);
|
|
4488
4567
|
return "skipped-refused";
|
|
4489
4568
|
}
|
|
4490
|
-
const exists = await
|
|
4569
|
+
const exists = await fs18.pathExists(dest);
|
|
4491
4570
|
if (exists && !opts.force) {
|
|
4492
4571
|
warn(`Skipping ${destDisplay}: already exists \u2014 re-run with --force to overwrite it`);
|
|
4493
4572
|
return "skipped-exists";
|
|
4494
4573
|
}
|
|
4495
4574
|
if (opts.dryRun) {
|
|
4496
|
-
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${
|
|
4575
|
+
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path23.basename(dest)} (${itemId})`);
|
|
4497
4576
|
return "scaffolded";
|
|
4498
4577
|
}
|
|
4499
4578
|
if (exists && opts.force) {
|
|
4500
|
-
await
|
|
4579
|
+
await fs18.remove(dest);
|
|
4501
4580
|
}
|
|
4502
|
-
await
|
|
4503
|
-
await
|
|
4581
|
+
await fs18.ensureDir(path23.dirname(dest));
|
|
4582
|
+
await fs18.copy(src, dest, {
|
|
4504
4583
|
overwrite: true,
|
|
4505
4584
|
dereference: false,
|
|
4506
4585
|
// A vanished/unreadable entry mid-walk must skip that path, not crash the copy.
|
|
4507
4586
|
filter: (s) => {
|
|
4508
4587
|
try {
|
|
4509
|
-
return !
|
|
4588
|
+
return !fs18.lstatSync(s).isSymbolicLink();
|
|
4510
4589
|
} catch {
|
|
4511
4590
|
return false;
|
|
4512
4591
|
}
|
|
4513
4592
|
}
|
|
4514
4593
|
});
|
|
4515
|
-
log(`${exists ? "Overwrote" : "Created"} ${
|
|
4594
|
+
log(`${exists ? "Overwrote" : "Created"} ${path23.basename(dest)} (${itemId})`);
|
|
4516
4595
|
return "scaffolded";
|
|
4517
4596
|
}
|
|
4518
4597
|
|
|
@@ -4569,8 +4648,8 @@ async function runScan(options) {
|
|
|
4569
4648
|
}
|
|
4570
4649
|
|
|
4571
4650
|
// src/commands/undo.ts
|
|
4572
|
-
import
|
|
4573
|
-
import
|
|
4651
|
+
import path24 from "node:path";
|
|
4652
|
+
import fs19 from "fs-extra";
|
|
4574
4653
|
|
|
4575
4654
|
// src/claude/managed-paths.ts
|
|
4576
4655
|
var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md", "commands/haus-doctor.md"];
|
|
@@ -4587,74 +4666,74 @@ async function collectManagedPaths(root) {
|
|
|
4587
4666
|
const lock = await readJson(hausPath(root, "haus.lock.json"));
|
|
4588
4667
|
for (const row of lock ?? []) {
|
|
4589
4668
|
for (const rel of row.paths ?? []) {
|
|
4590
|
-
paths.add(
|
|
4669
|
+
paths.add(path24.resolve(root, rel));
|
|
4591
4670
|
}
|
|
4592
4671
|
}
|
|
4593
4672
|
const existing = [];
|
|
4594
4673
|
for (const abs of paths) {
|
|
4595
|
-
if (await
|
|
4674
|
+
if (await fs19.pathExists(abs)) existing.push(abs);
|
|
4596
4675
|
}
|
|
4597
4676
|
return existing;
|
|
4598
4677
|
}
|
|
4599
4678
|
async function settingsHasHausContent(root) {
|
|
4600
4679
|
const settingsPath = claudePath(root, "settings.json");
|
|
4601
|
-
if (!await
|
|
4680
|
+
if (!await fs19.pathExists(settingsPath)) return false;
|
|
4602
4681
|
const settings = await readProjectSettings(root);
|
|
4603
4682
|
return settings._haus != null;
|
|
4604
4683
|
}
|
|
4605
4684
|
async function claudeMdHasHausBlock(root) {
|
|
4606
|
-
const filePath =
|
|
4607
|
-
if (!await
|
|
4608
|
-
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");
|
|
4609
4688
|
return text.includes(BLOCK_BEGIN);
|
|
4610
4689
|
}
|
|
4611
4690
|
async function stripProjectSettings(root) {
|
|
4612
4691
|
const settingsPath = claudePath(root, "settings.json");
|
|
4613
|
-
if (!await
|
|
4692
|
+
if (!await fs19.pathExists(settingsPath)) return false;
|
|
4614
4693
|
let settings = await readProjectSettings(root);
|
|
4615
4694
|
settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
4616
4695
|
const hasContent = Object.keys(settings).length > 0;
|
|
4617
4696
|
if (hasContent) {
|
|
4618
4697
|
await writeProjectSettings(root, settings);
|
|
4619
|
-
log(`Stripped haus rules from ${
|
|
4698
|
+
log(`Stripped haus rules from ${path24.relative(root, settingsPath)} (user settings preserved).`);
|
|
4620
4699
|
return true;
|
|
4621
4700
|
}
|
|
4622
|
-
await
|
|
4623
|
-
log(`Removed ${
|
|
4701
|
+
await fs19.remove(settingsPath);
|
|
4702
|
+
log(`Removed ${path24.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
4624
4703
|
return true;
|
|
4625
4704
|
}
|
|
4626
4705
|
async function stripRootClaudeMd(root) {
|
|
4627
|
-
const filePath =
|
|
4628
|
-
if (!await
|
|
4629
|
-
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");
|
|
4630
4709
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
4631
4710
|
const next = stripHausBlock(prev);
|
|
4632
4711
|
if (next.length === 0) {
|
|
4633
|
-
await
|
|
4712
|
+
await fs19.remove(filePath);
|
|
4634
4713
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
4635
4714
|
} else {
|
|
4636
|
-
await
|
|
4715
|
+
await fs19.writeFile(filePath, next, "utf8");
|
|
4637
4716
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
4638
4717
|
}
|
|
4639
4718
|
return true;
|
|
4640
4719
|
}
|
|
4641
4720
|
async function pruneDirIfEmpty(dir) {
|
|
4642
|
-
if (!await
|
|
4643
|
-
const entries = await
|
|
4644
|
-
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);
|
|
4645
4724
|
}
|
|
4646
4725
|
async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
4647
4726
|
if (managedAbsPaths.length === 0) return;
|
|
4648
4727
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4649
4728
|
const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
|
|
4650
4729
|
for (const abs of managedAbsPaths) {
|
|
4651
|
-
if (!await
|
|
4652
|
-
const rel =
|
|
4653
|
-
const backupPath =
|
|
4654
|
-
await
|
|
4655
|
-
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);
|
|
4656
4735
|
}
|
|
4657
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
4736
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path24.relative(root, backupRoot)}.`);
|
|
4658
4737
|
}
|
|
4659
4738
|
async function runUndo(options) {
|
|
4660
4739
|
const root = process.cwd();
|
|
@@ -4665,7 +4744,7 @@ async function runUndo(options) {
|
|
|
4665
4744
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
4666
4745
|
return;
|
|
4667
4746
|
}
|
|
4668
|
-
const relTargets = managed.map((p) =>
|
|
4747
|
+
const relTargets = managed.map((p) => path24.relative(root, p));
|
|
4669
4748
|
const summaryParts = [...relTargets];
|
|
4670
4749
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
4671
4750
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -4682,9 +4761,9 @@ User-owned .claude/ files will be preserved.`
|
|
|
4682
4761
|
}
|
|
4683
4762
|
await backupManagedFilesBeforeUndo(root, managed);
|
|
4684
4763
|
for (const abs of managed) {
|
|
4685
|
-
if (!await
|
|
4686
|
-
await
|
|
4687
|
-
log(`Removed ${
|
|
4764
|
+
if (!await fs19.pathExists(abs)) continue;
|
|
4765
|
+
await fs19.remove(abs);
|
|
4766
|
+
log(`Removed ${path24.relative(root, abs)}`);
|
|
4688
4767
|
}
|
|
4689
4768
|
if (stripSettings) await stripProjectSettings(root);
|
|
4690
4769
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -4695,8 +4774,8 @@ User-owned .claude/ files will be preserved.`
|
|
|
4695
4774
|
|
|
4696
4775
|
// src/install/uninstall.ts
|
|
4697
4776
|
import crypto3 from "node:crypto";
|
|
4698
|
-
import
|
|
4699
|
-
import
|
|
4777
|
+
import path25 from "node:path";
|
|
4778
|
+
import fs20 from "fs-extra";
|
|
4700
4779
|
async function runUninstall(options = {}) {
|
|
4701
4780
|
const { force = false } = options;
|
|
4702
4781
|
const manifest = await readManifest();
|
|
@@ -4706,7 +4785,7 @@ async function runUninstall(options = {}) {
|
|
|
4706
4785
|
return result;
|
|
4707
4786
|
}
|
|
4708
4787
|
for (const entry of manifest.files) {
|
|
4709
|
-
const exists =
|
|
4788
|
+
const exists = fs20.pathExistsSync(entry.destPath);
|
|
4710
4789
|
if (!exists) continue;
|
|
4711
4790
|
const content2 = await readText(entry.destPath);
|
|
4712
4791
|
if (content2 === void 0) continue;
|
|
@@ -4724,22 +4803,22 @@ async function runUninstall(options = {}) {
|
|
|
4724
4803
|
result.skipped.push(entry.destPath);
|
|
4725
4804
|
continue;
|
|
4726
4805
|
}
|
|
4727
|
-
await
|
|
4728
|
-
await pruneEmptyDir(
|
|
4806
|
+
await fs20.remove(entry.destPath);
|
|
4807
|
+
await pruneEmptyDir(path25.dirname(entry.destPath));
|
|
4729
4808
|
result.deleted.push(entry.destPath);
|
|
4730
4809
|
}
|
|
4731
4810
|
const settings = await readSettings();
|
|
4732
4811
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
4733
4812
|
await writeSettings(stripped);
|
|
4734
4813
|
result.hooksStripped = true;
|
|
4735
|
-
const hausDir =
|
|
4814
|
+
const hausDir = path25.join(globalClaudeDir(), "haus");
|
|
4736
4815
|
const manifestPath2 = hausManifestPath();
|
|
4737
|
-
if (
|
|
4738
|
-
await
|
|
4816
|
+
if (fs20.pathExistsSync(manifestPath2)) {
|
|
4817
|
+
await fs20.remove(manifestPath2);
|
|
4739
4818
|
}
|
|
4740
|
-
if (
|
|
4741
|
-
const remaining = await
|
|
4742
|
-
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);
|
|
4743
4822
|
}
|
|
4744
4823
|
return result;
|
|
4745
4824
|
}
|
|
@@ -4770,12 +4849,12 @@ async function runUninstallCommand(options) {
|
|
|
4770
4849
|
}
|
|
4771
4850
|
|
|
4772
4851
|
// src/commands/update.ts
|
|
4773
|
-
import
|
|
4852
|
+
import path27 from "node:path";
|
|
4774
4853
|
|
|
4775
4854
|
// src/claude/refresh-project.ts
|
|
4776
|
-
import
|
|
4855
|
+
import fs21 from "fs-extra";
|
|
4777
4856
|
async function isHausProject(root) {
|
|
4778
|
-
if (await
|
|
4857
|
+
if (await fs21.pathExists(hausPath(root, "recommendation.json"))) return true;
|
|
4779
4858
|
const result = await readJsonDetailed(claudePath(root, "settings.json"));
|
|
4780
4859
|
if (result.status === "ok") return result.value._haus != null;
|
|
4781
4860
|
return false;
|
|
@@ -4810,7 +4889,7 @@ function summarizeLockDiff(before, after) {
|
|
|
4810
4889
|
|
|
4811
4890
|
// src/update/lockfile.ts
|
|
4812
4891
|
import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
|
|
4813
|
-
import
|
|
4892
|
+
import path26 from "node:path";
|
|
4814
4893
|
async function checkLock(root) {
|
|
4815
4894
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
4816
4895
|
const hasValidVersions = lock.every(
|
|
@@ -4841,7 +4920,7 @@ async function applyLock(root) {
|
|
|
4841
4920
|
try {
|
|
4842
4921
|
const backupDir = hausPath(root, "backups");
|
|
4843
4922
|
await mkdir(backupDir, { recursive: true });
|
|
4844
|
-
await copyFile(lockPath,
|
|
4923
|
+
await copyFile(lockPath, path26.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
4845
4924
|
} catch {
|
|
4846
4925
|
}
|
|
4847
4926
|
const enriched = await Promise.all(
|
|
@@ -4863,7 +4942,7 @@ function diffLock(before, after) {
|
|
|
4863
4942
|
}
|
|
4864
4943
|
async function hasLocalOverrides(root) {
|
|
4865
4944
|
try {
|
|
4866
|
-
await readFile2(
|
|
4945
|
+
await readFile2(path26.join(root, ".claude", "settings.json"), "utf8");
|
|
4867
4946
|
return true;
|
|
4868
4947
|
} catch {
|
|
4869
4948
|
return false;
|
|
@@ -4875,7 +4954,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
|
4875
4954
|
async function runUpdate(options) {
|
|
4876
4955
|
const root = process.cwd();
|
|
4877
4956
|
if (options.check) {
|
|
4878
|
-
const pkgJson2 = await readJson(
|
|
4957
|
+
const pkgJson2 = await readJson(path27.join(packageRoot(), "package.json"));
|
|
4879
4958
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
4880
4959
|
const [status, npmVersion, latestCatalogTag, globalInstallDrift] = await Promise.all([
|
|
4881
4960
|
checkLock(root),
|
|
@@ -4905,7 +4984,7 @@ async function runUpdate(options) {
|
|
|
4905
4984
|
if (status.driftCount > 0) process.exitCode = 1;
|
|
4906
4985
|
return;
|
|
4907
4986
|
}
|
|
4908
|
-
const pkgJson = await readJson(
|
|
4987
|
+
const pkgJson = await readJson(path27.join(packageRoot(), "package.json"));
|
|
4909
4988
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
4910
4989
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
4911
4990
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -4982,16 +5061,16 @@ async function detectGlobalInstallDrift() {
|
|
|
4982
5061
|
}
|
|
4983
5062
|
|
|
4984
5063
|
// src/commands/validate-catalog.ts
|
|
4985
|
-
import
|
|
5064
|
+
import path29 from "node:path";
|
|
4986
5065
|
|
|
4987
5066
|
// src/catalog/validate-core.ts
|
|
4988
|
-
import
|
|
4989
|
-
import
|
|
5067
|
+
import fs22 from "node:fs";
|
|
5068
|
+
import path28 from "node:path";
|
|
4990
5069
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
4991
5070
|
var MANIFEST_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
|
|
4992
5071
|
function isSafeCatalogPath2(itemPath) {
|
|
4993
|
-
if (!itemPath ||
|
|
4994
|
-
const normalized =
|
|
5072
|
+
if (!itemPath || path28.isAbsolute(itemPath) || itemPath.includes("\\")) return false;
|
|
5073
|
+
const normalized = path28.normalize(itemPath);
|
|
4995
5074
|
return !normalized.startsWith("..") && !normalized.includes("/..");
|
|
4996
5075
|
}
|
|
4997
5076
|
function auditForbiddenStacks(items) {
|
|
@@ -5103,8 +5182,8 @@ function checkRequiredFrontmatter(text, label) {
|
|
|
5103
5182
|
return failures;
|
|
5104
5183
|
}
|
|
5105
5184
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
5106
|
-
const rel =
|
|
5107
|
-
const text =
|
|
5185
|
+
const rel = path28.relative(manifestDir, absPath);
|
|
5186
|
+
const text = fs22.readFileSync(absPath, "utf8");
|
|
5108
5187
|
const failures = [];
|
|
5109
5188
|
const lines = text.split(/\r?\n/);
|
|
5110
5189
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -5124,44 +5203,44 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
5124
5203
|
const failures = [];
|
|
5125
5204
|
for (const item of items) {
|
|
5126
5205
|
if (!item.path || !isSafeCatalogPath2(item.path)) continue;
|
|
5127
|
-
const absPath =
|
|
5206
|
+
const absPath = path28.join(manifestDir, item.path);
|
|
5128
5207
|
if (item.type === "skill") {
|
|
5129
|
-
const skillMd =
|
|
5130
|
-
if (!
|
|
5131
|
-
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)}`);
|
|
5132
5211
|
continue;
|
|
5133
5212
|
}
|
|
5134
|
-
const text =
|
|
5213
|
+
const text = fs22.readFileSync(skillMd, "utf8");
|
|
5135
5214
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
5136
5215
|
failures.push(
|
|
5137
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
5216
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path28.relative(manifestDir, skillMd)}`)
|
|
5138
5217
|
);
|
|
5139
5218
|
} else if (item.type === "agent") {
|
|
5140
|
-
if (!
|
|
5219
|
+
if (!fs22.existsSync(absPath)) {
|
|
5141
5220
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
5142
5221
|
continue;
|
|
5143
5222
|
}
|
|
5144
|
-
const text =
|
|
5145
|
-
const rel =
|
|
5223
|
+
const text = fs22.readFileSync(absPath, "utf8");
|
|
5224
|
+
const rel = path28.relative(manifestDir, absPath);
|
|
5146
5225
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
5147
5226
|
failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
|
|
5148
5227
|
} else if (item.type === "template") {
|
|
5149
|
-
if (!
|
|
5228
|
+
if (!fs22.existsSync(absPath)) {
|
|
5150
5229
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
5151
5230
|
continue;
|
|
5152
5231
|
}
|
|
5153
5232
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
5154
5233
|
} else if (item.type === "command") {
|
|
5155
|
-
if (!
|
|
5234
|
+
if (!fs22.existsSync(absPath)) {
|
|
5156
5235
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
5157
5236
|
continue;
|
|
5158
5237
|
}
|
|
5159
|
-
const text =
|
|
5160
|
-
const rel =
|
|
5238
|
+
const text = fs22.readFileSync(absPath, "utf8");
|
|
5239
|
+
const rel = path28.relative(manifestDir, absPath);
|
|
5161
5240
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
5162
5241
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
5163
5242
|
} else if (item.type === "config") {
|
|
5164
|
-
if (!
|
|
5243
|
+
if (!fs22.existsSync(absPath)) {
|
|
5165
5244
|
failures.push(`${item.id}: missing config path ${item.path}`);
|
|
5166
5245
|
}
|
|
5167
5246
|
}
|
|
@@ -5175,8 +5254,8 @@ var DIR_ITEM_TYPE = {
|
|
|
5175
5254
|
commands: "command"
|
|
5176
5255
|
};
|
|
5177
5256
|
function walkMd(dir, fn) {
|
|
5178
|
-
for (const entry of
|
|
5179
|
-
const full =
|
|
5257
|
+
for (const entry of fs22.readdirSync(dir, { withFileTypes: true })) {
|
|
5258
|
+
const full = path28.join(dir, entry.name);
|
|
5180
5259
|
if (entry.isDirectory()) walkMd(full, fn);
|
|
5181
5260
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
5182
5261
|
}
|
|
@@ -5185,12 +5264,12 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
5185
5264
|
const failures = [];
|
|
5186
5265
|
const pathSourceMap = buildItemPathSourceMap(items);
|
|
5187
5266
|
for (const dir of ["skills", "agents", "templates", "commands"]) {
|
|
5188
|
-
const abs =
|
|
5189
|
-
if (!
|
|
5267
|
+
const abs = path28.join(manifestDir, dir);
|
|
5268
|
+
if (!fs22.existsSync(abs)) continue;
|
|
5190
5269
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
5191
5270
|
walkMd(abs, (file) => {
|
|
5192
|
-
const text =
|
|
5193
|
-
const rel =
|
|
5271
|
+
const text = fs22.readFileSync(file, "utf8");
|
|
5272
|
+
const rel = path28.relative(manifestDir, file);
|
|
5194
5273
|
const norm = rel.replace(/\\/g, "/");
|
|
5195
5274
|
const source = resolveMarkdownItemSource(norm, pathSourceMap);
|
|
5196
5275
|
const checkNonTsxNpx = !isNpxTsxOnlyExempt(dirType, source);
|
|
@@ -5227,8 +5306,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
5227
5306
|
process.exitCode = 1;
|
|
5228
5307
|
return;
|
|
5229
5308
|
}
|
|
5230
|
-
const abs =
|
|
5231
|
-
const manifestDir =
|
|
5309
|
+
const abs = path29.resolve(process.cwd(), manifestPath2);
|
|
5310
|
+
const manifestDir = path29.dirname(abs);
|
|
5232
5311
|
const data = await readJson(abs);
|
|
5233
5312
|
if (!data?.items) {
|
|
5234
5313
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -5246,7 +5325,7 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
5246
5325
|
|
|
5247
5326
|
// src/commands/workspace.ts
|
|
5248
5327
|
import { existsSync as existsSync5, statSync as statSync2 } from "node:fs";
|
|
5249
|
-
import
|
|
5328
|
+
import path36 from "node:path";
|
|
5250
5329
|
|
|
5251
5330
|
// src/commands/workspace/aggregate.ts
|
|
5252
5331
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -5300,7 +5379,7 @@ ${summaries.map(
|
|
|
5300
5379
|
}
|
|
5301
5380
|
|
|
5302
5381
|
// src/commands/workspace/config.ts
|
|
5303
|
-
import
|
|
5382
|
+
import path30 from "node:path";
|
|
5304
5383
|
import YAML from "yaml";
|
|
5305
5384
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
5306
5385
|
function parseWorkspaceConfig(text) {
|
|
@@ -5323,11 +5402,11 @@ function parseWorkspaceConfig(text) {
|
|
|
5323
5402
|
};
|
|
5324
5403
|
}
|
|
5325
5404
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
5326
|
-
return parseWorkspaceConfig(await readText(
|
|
5405
|
+
return parseWorkspaceConfig(await readText(path30.join(workspaceRoot, WORKSPACE_FILE)));
|
|
5327
5406
|
}
|
|
5328
5407
|
|
|
5329
5408
|
// src/commands/workspace/discover.ts
|
|
5330
|
-
import
|
|
5409
|
+
import path31 from "node:path";
|
|
5331
5410
|
import fg4 from "fast-glob";
|
|
5332
5411
|
import YAML2 from "yaml";
|
|
5333
5412
|
var DEFAULT_MAX_DEPTH = 3;
|
|
@@ -5355,8 +5434,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
5355
5434
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
5356
5435
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
5357
5436
|
for (const match of matches) {
|
|
5358
|
-
const base =
|
|
5359
|
-
const dir =
|
|
5437
|
+
const base = path31.posix.basename(match);
|
|
5438
|
+
const dir = path31.posix.dirname(match);
|
|
5360
5439
|
const owner = dir === "." ? "." : dir;
|
|
5361
5440
|
if (base === ".git") gitDirs.add(owner);
|
|
5362
5441
|
else manifestDirs.add(owner);
|
|
@@ -5372,9 +5451,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
5372
5451
|
}
|
|
5373
5452
|
repoRoots.sort((a, b) => a.localeCompare(b));
|
|
5374
5453
|
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
5375
|
-
const absDir =
|
|
5376
|
-
const pkg = await readJson(
|
|
5377
|
-
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);
|
|
5378
5457
|
let role = "auto";
|
|
5379
5458
|
try {
|
|
5380
5459
|
const scan = await scanProject(absDir);
|
|
@@ -5417,7 +5496,7 @@ function renderWorkspaceYaml(config) {
|
|
|
5417
5496
|
});
|
|
5418
5497
|
}
|
|
5419
5498
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
5420
|
-
const yamlPath =
|
|
5499
|
+
const yamlPath = path31.join(workspaceRoot, "haus.workspace.yaml");
|
|
5421
5500
|
const existingText = await readText(yamlPath);
|
|
5422
5501
|
const existing = parseWorkspaceConfig(existingText);
|
|
5423
5502
|
if (existingText && !existing) {
|
|
@@ -5451,18 +5530,18 @@ async function runDiscover(workspaceRoot, opts = {}) {
|
|
|
5451
5530
|
|
|
5452
5531
|
// src/commands/workspace/doctor.ts
|
|
5453
5532
|
import { existsSync as existsSync3 } from "node:fs";
|
|
5454
|
-
import
|
|
5533
|
+
import path33 from "node:path";
|
|
5455
5534
|
|
|
5456
5535
|
// src/commands/workspace/manifest.ts
|
|
5457
5536
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
5458
|
-
import
|
|
5537
|
+
import path32 from "node:path";
|
|
5459
5538
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
5460
5539
|
function manifestPath(workspaceRoot) {
|
|
5461
5540
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
5462
5541
|
}
|
|
5463
5542
|
function hausVersion() {
|
|
5464
5543
|
try {
|
|
5465
|
-
const pkg = JSON.parse(readFileSync3(
|
|
5544
|
+
const pkg = JSON.parse(readFileSync3(path32.join(packageRoot(), "package.json"), "utf8"));
|
|
5466
5545
|
return pkg.version ?? "0.0.0";
|
|
5467
5546
|
} catch {
|
|
5468
5547
|
return "0.0.0";
|
|
@@ -5528,7 +5607,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
5528
5607
|
}
|
|
5529
5608
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
5530
5609
|
for (const repo of config.repos) {
|
|
5531
|
-
const repoRoot =
|
|
5610
|
+
const repoRoot = path33.resolve(workspaceRoot, repo.path);
|
|
5532
5611
|
const entry = manifestByName.get(repo.name);
|
|
5533
5612
|
if (!entry) {
|
|
5534
5613
|
flag({
|
|
@@ -5602,11 +5681,11 @@ function emit(args) {
|
|
|
5602
5681
|
|
|
5603
5682
|
// src/commands/workspace/setup.ts
|
|
5604
5683
|
import { existsSync as existsSync4, statSync } from "node:fs";
|
|
5605
|
-
import
|
|
5684
|
+
import path35 from "node:path";
|
|
5606
5685
|
|
|
5607
5686
|
// src/claude/write-workspace-claude-md.ts
|
|
5608
|
-
import
|
|
5609
|
-
import
|
|
5687
|
+
import path34 from "node:path";
|
|
5688
|
+
import fs23 from "fs-extra";
|
|
5610
5689
|
function buildWorkspaceImportBlock(client, members) {
|
|
5611
5690
|
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
5612
5691
|
const body = [
|
|
@@ -5624,8 +5703,8 @@ ${BLOCK_END}`;
|
|
|
5624
5703
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
5625
5704
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
5626
5705
|
const dryRun = opts.dryRun ?? false;
|
|
5627
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
5628
|
-
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") : "";
|
|
5629
5708
|
const next = opts.collision ? `${block}
|
|
5630
5709
|
` : injectHausBlock(prev, block);
|
|
5631
5710
|
const printable = displayPath(workspaceRoot, filePath);
|
|
@@ -5650,20 +5729,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
5650
5729
|
|
|
5651
5730
|
// src/commands/workspace/setup.ts
|
|
5652
5731
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
5653
|
-
let dir =
|
|
5732
|
+
let dir = path35.resolve(start);
|
|
5654
5733
|
for (; ; ) {
|
|
5655
|
-
if (existsSync4(
|
|
5656
|
-
const parent =
|
|
5657
|
-
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);
|
|
5658
5737
|
dir = parent;
|
|
5659
5738
|
}
|
|
5660
5739
|
}
|
|
5661
5740
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
5662
|
-
return
|
|
5741
|
+
return path35.resolve(workspaceRoot, repoPath) === path35.resolve(workspaceRoot);
|
|
5663
5742
|
}
|
|
5664
5743
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
5665
5744
|
const apply = options.write ?? false;
|
|
5666
|
-
const configText = await readText(
|
|
5745
|
+
const configText = await readText(path35.join(workspaceRoot, WORKSPACE_FILE));
|
|
5667
5746
|
if (!configText) {
|
|
5668
5747
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
5669
5748
|
process.exitCode = 1;
|
|
@@ -5680,7 +5759,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
5680
5759
|
const statuses = [];
|
|
5681
5760
|
const aggregateInputs = [];
|
|
5682
5761
|
for (const repo of repos) {
|
|
5683
|
-
const repoRoot =
|
|
5762
|
+
const repoRoot = path35.resolve(workspaceRoot, repo.path);
|
|
5684
5763
|
log(`
|
|
5685
5764
|
\u2192 ${repo.name} (${repo.path})`);
|
|
5686
5765
|
try {
|
|
@@ -5748,7 +5827,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
5748
5827
|
const status = statusByName.get(repo.name);
|
|
5749
5828
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
5750
5829
|
if (status?.status === "ok") {
|
|
5751
|
-
const lock = await checkLock(
|
|
5830
|
+
const lock = await checkLock(path35.resolve(workspaceRoot, repo.path));
|
|
5752
5831
|
manifestRepos.push({
|
|
5753
5832
|
name: repo.name,
|
|
5754
5833
|
path: repo.path,
|
|
@@ -5828,7 +5907,7 @@ relationships: []
|
|
|
5828
5907
|
log("Workspace initialized.");
|
|
5829
5908
|
}
|
|
5830
5909
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
5831
|
-
const configText = await readText(
|
|
5910
|
+
const configText = await readText(path36.join(workspaceRoot, WORKSPACE_FILE));
|
|
5832
5911
|
if (!configText) {
|
|
5833
5912
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
5834
5913
|
process.exitCode = 1;
|
|
@@ -5849,7 +5928,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
5849
5928
|
}
|
|
5850
5929
|
const inputs = [];
|
|
5851
5930
|
for (const repo of config.repos) {
|
|
5852
|
-
const repoRoot =
|
|
5931
|
+
const repoRoot = path36.resolve(workspaceRoot, repo.path);
|
|
5853
5932
|
if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
|
|
5854
5933
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
5855
5934
|
}
|
|
@@ -5899,7 +5978,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
5899
5978
|
// src/cli.ts
|
|
5900
5979
|
function cliVersion() {
|
|
5901
5980
|
try {
|
|
5902
|
-
const pkgPath =
|
|
5981
|
+
const pkgPath = path37.join(packageRoot(), "package.json");
|
|
5903
5982
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5904
5983
|
return pkg.version ?? "0.0.0";
|
|
5905
5984
|
} catch {
|
|
@@ -5909,7 +5988,7 @@ function cliVersion() {
|
|
|
5909
5988
|
var program = new Command();
|
|
5910
5989
|
function validateRuntimeNodeVersion() {
|
|
5911
5990
|
try {
|
|
5912
|
-
const pkgPath =
|
|
5991
|
+
const pkgPath = path37.join(packageRoot(), "package.json");
|
|
5913
5992
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5914
5993
|
const requiredRange = pkg.engines?.node;
|
|
5915
5994
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|