@haus-tech/haus-workflow 0.18.0 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/cli.js +159 -164
- package/library/catalog/manifest.json +2 -2
- package/package.json +4 -8
- package/tests/README.md +0 -54
- package/tests/fixtures/catalog/agents/code-reviewer.md +0 -15
- package/tests/fixtures/catalog/agents/docs-researcher.md +0 -15
- package/tests/fixtures/catalog/agents/planner.md +0 -15
- package/tests/fixtures/catalog/agents/security-reviewer.md +0 -15
- package/tests/fixtures/catalog/agents/test-reviewer.md +0 -15
- package/tests/fixtures/catalog/manifest.json +0 -1065
- package/tests/fixtures/catalog/policy-gates-manifest.json +0 -120
- package/tests/fixtures/catalog/skills/auth-oidc-azure-bankid-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/bullmq-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/database-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/dotnet-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/dotnet-service-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/eslint-setup/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/expo-react-native-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/global-engineering-rules/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/i18next-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/jest-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/laravel-nova-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/laravel-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/nestjs-graphql-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/nextauth-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/nextjs-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/nx21-monorepo-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/package-manager-yarn4-pnpm89/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/phpunit-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/playwright-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/prettier-setup/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/prisma-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/production-readiness-review/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/qliro-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/radix-shadcn-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/react-router-v7-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/react19-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/sanity-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/security-review/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/sentry-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/storybook-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/strapi-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/stripe-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/supabase-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/tailwind-scss-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/tanstack-query-router-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/testing-library-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/turbo-monorepo-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/typescript5-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/vendure-app-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/vendure-plugin-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/vite8-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/vitest-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/vue-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/wordpress-acf-elementor-jetengine-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/wordpress-bedrock-patterns/SKILL.md +0 -14
- package/tests/fixtures/catalog/skills/wordpress-patterns/SKILL.md +0 -14
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { Command } from "commander";
|
|
|
8
8
|
// src/commands/apply.ts
|
|
9
9
|
import path13 from "path";
|
|
10
10
|
import checkbox from "@inquirer/checkbox";
|
|
11
|
-
import
|
|
11
|
+
import fs12 from "fs-extra";
|
|
12
12
|
|
|
13
13
|
// src/catalog/remote-catalog.ts
|
|
14
14
|
import os from "os";
|
|
@@ -254,6 +254,13 @@ async function writeJson(file, value) {
|
|
|
254
254
|
await fs2.writeFile(file, `${JSON.stringify(value, null, 2)}
|
|
255
255
|
`, "utf8");
|
|
256
256
|
}
|
|
257
|
+
async function pruneEmptyDir(dir) {
|
|
258
|
+
try {
|
|
259
|
+
const entries = await fs2.readdir(dir);
|
|
260
|
+
if (entries.length === 0) await fs2.remove(dir);
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
}
|
|
257
264
|
async function readText(file) {
|
|
258
265
|
try {
|
|
259
266
|
return await fs2.readFile(file, "utf8");
|
|
@@ -647,7 +654,7 @@ async function applyProjectSettingsMerge(root) {
|
|
|
647
654
|
|
|
648
655
|
// src/claude/write-claude-files.ts
|
|
649
656
|
import path12 from "path";
|
|
650
|
-
import
|
|
657
|
+
import fs11 from "fs-extra";
|
|
651
658
|
|
|
652
659
|
// src/catalog/load-catalog.ts
|
|
653
660
|
import path6 from "path";
|
|
@@ -728,6 +735,22 @@ async function hashInstalledPaths(root, relPaths) {
|
|
|
728
735
|
return hashText(fileDigests.map((f) => `${f.rel}=${f.digest}`).join("|"));
|
|
729
736
|
}
|
|
730
737
|
|
|
738
|
+
// src/claude/load-hooks-config.ts
|
|
739
|
+
import path8 from "path";
|
|
740
|
+
var CONFIG_PATH = ".haus-workflow/config.json";
|
|
741
|
+
var DEFAULT_HOOKS_CONFIG = {
|
|
742
|
+
hooks: {
|
|
743
|
+
context: { enabled: false }
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
async function isHookEnabled(root, key) {
|
|
747
|
+
const cfg = await readJson(path8.join(root, CONFIG_PATH));
|
|
748
|
+
return cfg?.hooks?.[key]?.enabled === true;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/claude/managed-write.ts
|
|
752
|
+
import fs5 from "fs-extra";
|
|
753
|
+
|
|
731
754
|
// src/utils/diff.ts
|
|
732
755
|
import { createTwoFilesPatch } from "diff";
|
|
733
756
|
function hasTextChanged(before, after) {
|
|
@@ -750,21 +773,35 @@ function summarizeDiff(diffText) {
|
|
|
750
773
|
return { additions, deletions };
|
|
751
774
|
}
|
|
752
775
|
|
|
753
|
-
// src/claude/
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
776
|
+
// src/claude/managed-write.ts
|
|
777
|
+
async function writeManagedText(root, filePath, nextText, dryRun) {
|
|
778
|
+
const prev = await fs5.pathExists(filePath) ? await fs5.readFile(filePath, "utf8") : "";
|
|
779
|
+
const printable = displayPath(root, filePath);
|
|
780
|
+
if (dryRun) {
|
|
781
|
+
if (!prev) {
|
|
782
|
+
log(createUnifiedDiff(printable, "", nextText));
|
|
783
|
+
} else if (hasTextChanged(prev, nextText)) {
|
|
784
|
+
log(createUnifiedDiff(printable, prev, nextText));
|
|
785
|
+
} else {
|
|
786
|
+
log(`${printable}: unchanged`);
|
|
787
|
+
}
|
|
788
|
+
return;
|
|
759
789
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
790
|
+
if (hasTextChanged(prev, nextText) && prev.length > 0) {
|
|
791
|
+
const diffText = createUnifiedDiff(printable, prev, nextText);
|
|
792
|
+
const summary = summarizeDiff(diffText);
|
|
793
|
+
log(`Overwriting ${printable} (diff +${summary.additions} -${summary.deletions})`);
|
|
794
|
+
}
|
|
795
|
+
await writeText(filePath, nextText);
|
|
796
|
+
}
|
|
797
|
+
async function writeManagedJson(root, filePath, value, dryRun) {
|
|
798
|
+
const nextText = `${JSON.stringify(value, null, 2)}
|
|
799
|
+
`;
|
|
800
|
+
await writeManagedText(root, filePath, nextText, dryRun);
|
|
764
801
|
}
|
|
765
802
|
|
|
766
803
|
// src/claude/verify-hooks-contract.ts
|
|
767
|
-
import
|
|
804
|
+
import fs6 from "fs-extra";
|
|
768
805
|
|
|
769
806
|
// src/claude/load-hooks.ts
|
|
770
807
|
var CANONICAL_HOOKS = {
|
|
@@ -858,7 +895,7 @@ async function assertPostApplySettingsHausContract(root) {
|
|
|
858
895
|
}
|
|
859
896
|
async function verifyProjectSettingsHooksContract(root) {
|
|
860
897
|
const settingsPath = claudePath(root, "settings.json");
|
|
861
|
-
if (!await
|
|
898
|
+
if (!await fs6.pathExists(settingsPath)) {
|
|
862
899
|
return {
|
|
863
900
|
ok: true,
|
|
864
901
|
skipped: true,
|
|
@@ -886,7 +923,7 @@ async function verifyProjectSettingsHooksContract(root) {
|
|
|
886
923
|
|
|
887
924
|
// src/claude/write-root-claude-md.ts
|
|
888
925
|
import path9 from "path";
|
|
889
|
-
import
|
|
926
|
+
import fs7 from "fs-extra";
|
|
890
927
|
var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
|
|
891
928
|
var BLOCK_END = "<!-- HAUS:END haus-imports -->";
|
|
892
929
|
var IMPORT_CONTENT = `@.haus-workflow/WORKFLOW.md
|
|
@@ -898,8 +935,9 @@ ${BLOCK_END}`;
|
|
|
898
935
|
}
|
|
899
936
|
function stripHausBlock(existing) {
|
|
900
937
|
const beginIdx = existing.indexOf(BLOCK_BEGIN);
|
|
901
|
-
|
|
902
|
-
|
|
938
|
+
if (beginIdx === -1) return existing;
|
|
939
|
+
const endIdx = existing.indexOf(BLOCK_END, beginIdx + BLOCK_BEGIN.length);
|
|
940
|
+
if (endIdx === -1) return existing;
|
|
903
941
|
const before = existing.slice(0, beginIdx);
|
|
904
942
|
const after = existing.slice(endIdx + BLOCK_END.length);
|
|
905
943
|
const merged = `${before}${after}`.replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
@@ -908,8 +946,8 @@ function stripHausBlock(existing) {
|
|
|
908
946
|
}
|
|
909
947
|
function injectHausBlock(existing, block) {
|
|
910
948
|
const beginIdx = existing.indexOf(BLOCK_BEGIN);
|
|
911
|
-
const endIdx = existing.indexOf(BLOCK_END);
|
|
912
|
-
if (beginIdx !== -1 && endIdx !== -1
|
|
949
|
+
const endIdx = beginIdx === -1 ? -1 : existing.indexOf(BLOCK_END, beginIdx + BLOCK_BEGIN.length);
|
|
950
|
+
if (beginIdx !== -1 && endIdx !== -1) {
|
|
913
951
|
const before = existing.slice(0, beginIdx);
|
|
914
952
|
const after = existing.slice(endIdx + BLOCK_END.length);
|
|
915
953
|
return `${before}${block}${after}`;
|
|
@@ -927,35 +965,19 @@ ${block}
|
|
|
927
965
|
async function writeRootClaudeMd(root, dryRun) {
|
|
928
966
|
const filePath = path9.join(root, "CLAUDE.md");
|
|
929
967
|
const block = buildImportBlock();
|
|
930
|
-
const prev = await
|
|
968
|
+
const prev = await fs7.pathExists(filePath) ? await fs7.readFile(filePath, "utf8") : "";
|
|
931
969
|
const next = injectHausBlock(prev, block);
|
|
932
|
-
|
|
933
|
-
if (dryRun) {
|
|
934
|
-
if (!prev) {
|
|
935
|
-
log(createUnifiedDiff(printable, "", next));
|
|
936
|
-
} else if (hasTextChanged(prev, next)) {
|
|
937
|
-
log(createUnifiedDiff(printable, prev, next));
|
|
938
|
-
} else {
|
|
939
|
-
log(`${printable}: unchanged`);
|
|
940
|
-
}
|
|
941
|
-
return filePath;
|
|
942
|
-
}
|
|
943
|
-
if (hasTextChanged(prev, next) && prev.length > 0) {
|
|
944
|
-
const diffText = createUnifiedDiff(printable, prev, next);
|
|
945
|
-
const summary = summarizeDiff(diffText);
|
|
946
|
-
log(`Overwriting ${printable} (diff +${summary.additions} -${summary.deletions})`);
|
|
947
|
-
}
|
|
948
|
-
await writeText(filePath, next);
|
|
970
|
+
await writeManagedText(root, filePath, next, dryRun);
|
|
949
971
|
return filePath;
|
|
950
972
|
}
|
|
951
973
|
|
|
952
974
|
// src/claude/write-workflow-config.ts
|
|
953
975
|
import path11 from "path";
|
|
954
|
-
import
|
|
976
|
+
import fs9 from "fs-extra";
|
|
955
977
|
|
|
956
978
|
// src/claude/derive-workflow-config.ts
|
|
957
979
|
import path10 from "path";
|
|
958
|
-
import
|
|
980
|
+
import fs8 from "fs-extra";
|
|
959
981
|
function binCmd(pm, bin, args) {
|
|
960
982
|
const tail = args ? ` ${args}` : "";
|
|
961
983
|
if (pm === "yarn") return `yarn ${bin}${tail}`;
|
|
@@ -974,7 +996,7 @@ async function deriveWorkflowConfig(root, ctx) {
|
|
|
974
996
|
return null;
|
|
975
997
|
};
|
|
976
998
|
const hasDep = (name) => deps.has(name);
|
|
977
|
-
const exists = (rel) =>
|
|
999
|
+
const exists = (rel) => fs8.pathExistsSync(path10.join(root, rel));
|
|
978
1000
|
const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
|
|
979
1001
|
const hasCypress = hasDep("cypress");
|
|
980
1002
|
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;
|
|
@@ -1043,7 +1065,7 @@ var FALLBACK_CONTEXT = {
|
|
|
1043
1065
|
async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
1044
1066
|
const destPath = hausPath(root, "workflow-config.md");
|
|
1045
1067
|
const printable = displayPath(root, destPath);
|
|
1046
|
-
const exists = await
|
|
1068
|
+
const exists = await fs9.pathExists(destPath);
|
|
1047
1069
|
if (exists && !opts.refill) {
|
|
1048
1070
|
if (dryRun) log(printable + ": exists (project-owned, skipping)");
|
|
1049
1071
|
return null;
|
|
@@ -1055,7 +1077,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
1055
1077
|
};
|
|
1056
1078
|
const values = await deriveWorkflowConfig(root, ctx);
|
|
1057
1079
|
if (exists) {
|
|
1058
|
-
const current = await
|
|
1080
|
+
const current = await fs9.readFile(destPath, "utf8");
|
|
1059
1081
|
const refilled = refillContent(current, values);
|
|
1060
1082
|
if (refilled === current) {
|
|
1061
1083
|
if (dryRun) log(printable + ": no blank fields to refill");
|
|
@@ -1077,7 +1099,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
1077
1099
|
}
|
|
1078
1100
|
|
|
1079
1101
|
// src/claude/write-workflow.ts
|
|
1080
|
-
import
|
|
1102
|
+
import fs10 from "fs-extra";
|
|
1081
1103
|
|
|
1082
1104
|
// src/claude/managed-template.ts
|
|
1083
1105
|
function normaliseLF(content2) {
|
|
@@ -1110,8 +1132,8 @@ async function writeWorkflow(root, pkgVersion, dryRun) {
|
|
|
1110
1132
|
${templateContent}`;
|
|
1111
1133
|
const destPath = hausPath(root, "WORKFLOW.md");
|
|
1112
1134
|
const printable = displayPath(root, destPath);
|
|
1113
|
-
if (await
|
|
1114
|
-
const existing = await
|
|
1135
|
+
if (await fs10.pathExists(destPath)) {
|
|
1136
|
+
const existing = await fs10.readFile(destPath, "utf8");
|
|
1115
1137
|
const firstLine = existing.split("\n")[0] ?? "";
|
|
1116
1138
|
const parsed = parseHausManagedHeader(firstLine);
|
|
1117
1139
|
if (!parsed) {
|
|
@@ -1133,7 +1155,7 @@ ${templateContent}`;
|
|
|
1133
1155
|
}
|
|
1134
1156
|
}
|
|
1135
1157
|
if (dryRun) {
|
|
1136
|
-
const prev = await
|
|
1158
|
+
const prev = await fs10.pathExists(destPath) ? await fs10.readFile(destPath, "utf8") : "";
|
|
1137
1159
|
if (!prev) {
|
|
1138
1160
|
log(createUnifiedDiff(printable, "", next));
|
|
1139
1161
|
} else {
|
|
@@ -1192,7 +1214,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1192
1214
|
await assertPostApplySettingsHausContract(root);
|
|
1193
1215
|
}
|
|
1194
1216
|
const configPath = hausPath(root, "config.json");
|
|
1195
|
-
if (!await
|
|
1217
|
+
if (!await fs11.pathExists(configPath)) {
|
|
1196
1218
|
await writeManagedJson(root, configPath, DEFAULT_HOOKS_CONFIG, dryRun);
|
|
1197
1219
|
}
|
|
1198
1220
|
await writeManagedText(
|
|
@@ -1242,15 +1264,15 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1242
1264
|
const sourcePath = catalogItemContentPath(contentRoot, manifestItem);
|
|
1243
1265
|
const target = item.type === "agent" ? "agents" : item.type === "template" ? "templates" : item.type === "command" ? "commands" : "skills";
|
|
1244
1266
|
const destination = claudePath(root, target, path12.basename(sourcePath));
|
|
1245
|
-
if (await
|
|
1267
|
+
if (await fs11.pathExists(sourcePath)) {
|
|
1246
1268
|
if (dryRun) {
|
|
1247
|
-
const exists = await
|
|
1269
|
+
const exists = await fs11.pathExists(destination);
|
|
1248
1270
|
log(
|
|
1249
1271
|
`${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`
|
|
1250
1272
|
);
|
|
1251
1273
|
} else {
|
|
1252
|
-
await
|
|
1253
|
-
await
|
|
1274
|
+
await fs11.ensureDir(path12.dirname(destination));
|
|
1275
|
+
await fs11.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
1254
1276
|
}
|
|
1255
1277
|
files.push(destination);
|
|
1256
1278
|
const current = installedPathsByItem.get(item.id) ?? [];
|
|
@@ -1314,7 +1336,7 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
|
|
|
1314
1336
|
if (relPaths.length === 0) continue;
|
|
1315
1337
|
const existing = [];
|
|
1316
1338
|
for (const rel of relPaths) {
|
|
1317
|
-
if (await
|
|
1339
|
+
if (await fs11.pathExists(path12.join(root, rel))) existing.push(rel);
|
|
1318
1340
|
}
|
|
1319
1341
|
if (existing.length === 0) continue;
|
|
1320
1342
|
if (entry.hash === void 0) {
|
|
@@ -1336,44 +1358,12 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
|
|
|
1336
1358
|
log(`[dry-run] would remove stale ${displayPath(root, abs)} (${entry.id})`);
|
|
1337
1359
|
continue;
|
|
1338
1360
|
}
|
|
1339
|
-
await
|
|
1361
|
+
await fs11.remove(abs);
|
|
1340
1362
|
await pruneEmptyDir(path12.dirname(abs));
|
|
1341
1363
|
log(`Removed stale ${displayPath(root, abs)} (${entry.id})`);
|
|
1342
1364
|
}
|
|
1343
1365
|
}
|
|
1344
1366
|
}
|
|
1345
|
-
async function pruneEmptyDir(dir) {
|
|
1346
|
-
try {
|
|
1347
|
-
const entries = await fs10.readdir(dir);
|
|
1348
|
-
if (entries.length === 0) await fs10.remove(dir);
|
|
1349
|
-
} catch {
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
async function writeManagedText(root, filePath, nextText, dryRun) {
|
|
1353
|
-
const prev = await fs10.pathExists(filePath) ? await fs10.readFile(filePath, "utf8") : "";
|
|
1354
|
-
const printable = displayPath(root, filePath);
|
|
1355
|
-
if (dryRun) {
|
|
1356
|
-
if (!prev) {
|
|
1357
|
-
log(createUnifiedDiff(printable, "", nextText));
|
|
1358
|
-
} else if (hasTextChanged(prev, nextText)) {
|
|
1359
|
-
log(createUnifiedDiff(printable, prev, nextText));
|
|
1360
|
-
} else {
|
|
1361
|
-
log(`${printable}: unchanged`);
|
|
1362
|
-
}
|
|
1363
|
-
return;
|
|
1364
|
-
}
|
|
1365
|
-
if (hasTextChanged(prev, nextText) && prev.length > 0) {
|
|
1366
|
-
const diffText = createUnifiedDiff(printable, prev, nextText);
|
|
1367
|
-
const summary = summarizeDiff(diffText);
|
|
1368
|
-
log(`Overwriting ${printable} (diff +${summary.additions} -${summary.deletions})`);
|
|
1369
|
-
}
|
|
1370
|
-
await writeText(filePath, nextText);
|
|
1371
|
-
}
|
|
1372
|
-
async function writeManagedJson(root, filePath, value, dryRun) {
|
|
1373
|
-
const nextText = `${JSON.stringify(value, null, 2)}
|
|
1374
|
-
`;
|
|
1375
|
-
await writeManagedText(root, filePath, nextText, dryRun);
|
|
1376
|
-
}
|
|
1377
1367
|
|
|
1378
1368
|
// src/commands/apply.ts
|
|
1379
1369
|
async function cacheHasItems() {
|
|
@@ -1437,8 +1427,8 @@ async function runApply(options) {
|
|
|
1437
1427
|
}
|
|
1438
1428
|
}
|
|
1439
1429
|
async function isHausProject(root) {
|
|
1440
|
-
if (await
|
|
1441
|
-
if (await
|
|
1430
|
+
if (await fs12.pathExists(hausPath(root, "recommendation.json"))) return true;
|
|
1431
|
+
if (await fs12.pathExists(claudePath(root, "settings.json"))) {
|
|
1442
1432
|
const settings = await readProjectSettings(root);
|
|
1443
1433
|
if (settings._haus != null) return true;
|
|
1444
1434
|
}
|
|
@@ -2067,7 +2057,7 @@ function compareVersions(a, b) {
|
|
|
2067
2057
|
|
|
2068
2058
|
// src/scanner/detect-package-manager.ts
|
|
2069
2059
|
import path15 from "path";
|
|
2070
|
-
import
|
|
2060
|
+
import fs13 from "fs-extra";
|
|
2071
2061
|
function detectPackageManager(root, packageManagerField) {
|
|
2072
2062
|
const field = String(packageManagerField ?? "").trim();
|
|
2073
2063
|
if (field.startsWith("yarn@")) {
|
|
@@ -2085,9 +2075,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
2085
2075
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
2086
2076
|
return "unknown";
|
|
2087
2077
|
}
|
|
2088
|
-
if (
|
|
2089
|
-
if (
|
|
2090
|
-
if (
|
|
2078
|
+
if (fs13.existsSync(path15.join(root, "yarn.lock"))) return "yarn";
|
|
2079
|
+
if (fs13.existsSync(path15.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
2080
|
+
if (fs13.existsSync(path15.join(root, "package-lock.json"))) return "npm";
|
|
2091
2081
|
return "unknown";
|
|
2092
2082
|
}
|
|
2093
2083
|
|
|
@@ -2620,7 +2610,7 @@ async function runContext(options) {
|
|
|
2620
2610
|
|
|
2621
2611
|
// src/commands/doctor.ts
|
|
2622
2612
|
import path19 from "path";
|
|
2623
|
-
import
|
|
2613
|
+
import fs14 from "fs-extra";
|
|
2624
2614
|
|
|
2625
2615
|
// src/update/npm-version.ts
|
|
2626
2616
|
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
@@ -2728,7 +2718,7 @@ async function runDoctor(options) {
|
|
|
2728
2718
|
const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
|
|
2729
2719
|
const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
|
|
2730
2720
|
for (const target of importTargets) {
|
|
2731
|
-
if (!await
|
|
2721
|
+
if (!await fs14.pathExists(hausPath(root, target))) {
|
|
2732
2722
|
flag(
|
|
2733
2723
|
`- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
|
|
2734
2724
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -2739,7 +2729,7 @@ async function runDoctor(options) {
|
|
|
2739
2729
|
}
|
|
2740
2730
|
}
|
|
2741
2731
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
2742
|
-
const workflowExists = await
|
|
2732
|
+
const workflowExists = await fs14.pathExists(workflowPath);
|
|
2743
2733
|
if (!workflowExists) {
|
|
2744
2734
|
flag(
|
|
2745
2735
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -2761,7 +2751,7 @@ async function runDoctor(options) {
|
|
|
2761
2751
|
"templates",
|
|
2762
2752
|
"agentic-workflow-standard.md"
|
|
2763
2753
|
);
|
|
2764
|
-
const templatePath = await
|
|
2754
|
+
const templatePath = await fs14.pathExists(cachePath) ? cachePath : bundledPath;
|
|
2765
2755
|
const templateContent = await readText(templatePath);
|
|
2766
2756
|
if (storedHashMatch && templateContent) {
|
|
2767
2757
|
const currentHash = hashText(normaliseLF(templateContent));
|
|
@@ -2780,7 +2770,7 @@ async function runDoctor(options) {
|
|
|
2780
2770
|
}
|
|
2781
2771
|
}
|
|
2782
2772
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
2783
|
-
const workflowConfigExists = await
|
|
2773
|
+
const workflowConfigExists = await fs14.pathExists(workflowConfigPath);
|
|
2784
2774
|
if (!workflowConfigExists) {
|
|
2785
2775
|
flag(
|
|
2786
2776
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -2788,7 +2778,7 @@ async function runDoctor(options) {
|
|
|
2788
2778
|
"haus apply --write"
|
|
2789
2779
|
);
|
|
2790
2780
|
} else {
|
|
2791
|
-
const cfg = await
|
|
2781
|
+
const cfg = await fs14.readFile(workflowConfigPath, "utf8");
|
|
2792
2782
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
2793
2783
|
if (unfilled > 0) {
|
|
2794
2784
|
flag(
|
|
@@ -2963,7 +2953,7 @@ async function runGuard(kind, _options) {
|
|
|
2963
2953
|
|
|
2964
2954
|
// src/commands/init.ts
|
|
2965
2955
|
import path20 from "path";
|
|
2966
|
-
import
|
|
2956
|
+
import fs15 from "fs-extra";
|
|
2967
2957
|
|
|
2968
2958
|
// src/utils/prompts.ts
|
|
2969
2959
|
import { stdin as input, stdout as output } from "process";
|
|
@@ -3186,7 +3176,8 @@ async function recommend(root, context) {
|
|
|
3186
3176
|
(t) => context.warnings.join(" ").toLowerCase().includes(t.toLowerCase())
|
|
3187
3177
|
);
|
|
3188
3178
|
if (configSignal) push("config-signal-match", "config signal match", `warning:${configSignal}`);
|
|
3189
|
-
const
|
|
3179
|
+
const idSegment = item.id.split(".").pop() ?? "";
|
|
3180
|
+
const changedMatch = idSegment ? changedFiles.find((f) => f.includes(idSegment)) : void 0;
|
|
3190
3181
|
if (changedMatch)
|
|
3191
3182
|
push("changed-file-match", "changed file match", `changedFile:${changedMatch}`);
|
|
3192
3183
|
const requiresAny = item.requiresAny ?? [];
|
|
@@ -3350,7 +3341,7 @@ async function runSetupProject(options) {
|
|
|
3350
3341
|
async function runInit(options) {
|
|
3351
3342
|
const root = process.cwd();
|
|
3352
3343
|
const hausDir = path20.join(root, ".haus-workflow");
|
|
3353
|
-
const alreadyInit = await
|
|
3344
|
+
const alreadyInit = await fs15.pathExists(hausDir);
|
|
3354
3345
|
if (alreadyInit) {
|
|
3355
3346
|
log("Haus AI already initialized in this project.");
|
|
3356
3347
|
log("Run `haus setup-project` to reconfigure.");
|
|
@@ -3363,7 +3354,7 @@ async function runInit(options) {
|
|
|
3363
3354
|
// src/install/apply.ts
|
|
3364
3355
|
import crypto2 from "crypto";
|
|
3365
3356
|
import path21 from "path";
|
|
3366
|
-
import
|
|
3357
|
+
import fs16 from "fs-extra";
|
|
3367
3358
|
|
|
3368
3359
|
// src/install/header.ts
|
|
3369
3360
|
var MD_PREFIX = "<!-- HAUS-MANAGED";
|
|
@@ -3452,7 +3443,7 @@ function hashContent(content2) {
|
|
|
3452
3443
|
function sourceVersion() {
|
|
3453
3444
|
try {
|
|
3454
3445
|
const pkgPath = path21.join(packageRoot(), "package.json");
|
|
3455
|
-
const pkg = JSON.parse(
|
|
3446
|
+
const pkg = JSON.parse(fs16.readFileSync(pkgPath, "utf8"));
|
|
3456
3447
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
3457
3448
|
} catch {
|
|
3458
3449
|
return "haus@0.0.0";
|
|
@@ -3464,10 +3455,10 @@ function globalSrcDir() {
|
|
|
3464
3455
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
3465
3456
|
const entries = [];
|
|
3466
3457
|
const skillsDir = path21.join(srcDir, "skills");
|
|
3467
|
-
if (
|
|
3468
|
-
for (const skillName of
|
|
3458
|
+
if (fs16.pathExistsSync(skillsDir)) {
|
|
3459
|
+
for (const skillName of fs16.readdirSync(skillsDir)) {
|
|
3469
3460
|
const skillFile = path21.join(skillsDir, skillName, "SKILL.md");
|
|
3470
|
-
if (
|
|
3461
|
+
if (fs16.pathExistsSync(skillFile)) {
|
|
3471
3462
|
entries.push({
|
|
3472
3463
|
stableId: `skill.${skillName}`,
|
|
3473
3464
|
srcRelPath: path21.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
@@ -3477,8 +3468,8 @@ function collectSourceFiles(srcDir, claudeDir) {
|
|
|
3477
3468
|
}
|
|
3478
3469
|
}
|
|
3479
3470
|
const commandsDir = path21.join(srcDir, "commands");
|
|
3480
|
-
if (
|
|
3481
|
-
for (const fileName of
|
|
3471
|
+
if (fs16.pathExistsSync(commandsDir)) {
|
|
3472
|
+
for (const fileName of fs16.readdirSync(commandsDir)) {
|
|
3482
3473
|
if (!fileName.endsWith(".md")) continue;
|
|
3483
3474
|
const commandName = fileName.slice(0, -".md".length);
|
|
3484
3475
|
entries.push({
|
|
@@ -3528,7 +3519,7 @@ async function applyInstall(options = {}) {
|
|
|
3528
3519
|
}
|
|
3529
3520
|
continue;
|
|
3530
3521
|
}
|
|
3531
|
-
const destExists =
|
|
3522
|
+
const destExists = fs16.pathExistsSync(entry.destPath);
|
|
3532
3523
|
if (destExists) {
|
|
3533
3524
|
const currentContent = await readText(entry.destPath);
|
|
3534
3525
|
if (currentContent !== void 0) {
|
|
@@ -3575,13 +3566,13 @@ async function applyInstall(options = {}) {
|
|
|
3575
3566
|
const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
|
|
3576
3567
|
for (const entry of existingManifest.files) {
|
|
3577
3568
|
if (currentDestPaths.has(entry.destPath)) continue;
|
|
3578
|
-
if (!
|
|
3569
|
+
if (!fs16.pathExistsSync(entry.destPath)) continue;
|
|
3579
3570
|
const content2 = await readText(entry.destPath);
|
|
3580
3571
|
if (!content2) continue;
|
|
3581
3572
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
3582
3573
|
const currentHash = hashContent(content2);
|
|
3583
3574
|
if (hasHeader && currentHash === entry.hash) {
|
|
3584
|
-
if (!dryRun) await
|
|
3575
|
+
if (!dryRun) await fs16.remove(entry.destPath);
|
|
3585
3576
|
result.deleted.push(entry.destPath);
|
|
3586
3577
|
} else {
|
|
3587
3578
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -3709,7 +3700,7 @@ async function runScan(options) {
|
|
|
3709
3700
|
|
|
3710
3701
|
// src/commands/undo.ts
|
|
3711
3702
|
import path22 from "path";
|
|
3712
|
-
import
|
|
3703
|
+
import fs17 from "fs-extra";
|
|
3713
3704
|
|
|
3714
3705
|
// src/claude/managed-paths.ts
|
|
3715
3706
|
var PROJECT_MANAGED_CLAUDE_REL = [
|
|
@@ -3740,25 +3731,25 @@ async function collectManagedPaths(root) {
|
|
|
3740
3731
|
}
|
|
3741
3732
|
const existing = [];
|
|
3742
3733
|
for (const abs of paths) {
|
|
3743
|
-
if (await
|
|
3734
|
+
if (await fs17.pathExists(abs)) existing.push(abs);
|
|
3744
3735
|
}
|
|
3745
3736
|
return existing;
|
|
3746
3737
|
}
|
|
3747
3738
|
async function settingsHasHausContent(root) {
|
|
3748
3739
|
const settingsPath = claudePath(root, "settings.json");
|
|
3749
|
-
if (!await
|
|
3740
|
+
if (!await fs17.pathExists(settingsPath)) return false;
|
|
3750
3741
|
const settings = await readProjectSettings(root);
|
|
3751
3742
|
return settings._haus != null;
|
|
3752
3743
|
}
|
|
3753
3744
|
async function claudeMdHasHausBlock(root) {
|
|
3754
3745
|
const filePath = path22.join(root, "CLAUDE.md");
|
|
3755
|
-
if (!await
|
|
3756
|
-
const text = await
|
|
3746
|
+
if (!await fs17.pathExists(filePath)) return false;
|
|
3747
|
+
const text = await fs17.readFile(filePath, "utf8");
|
|
3757
3748
|
return text.includes(BLOCK_BEGIN);
|
|
3758
3749
|
}
|
|
3759
3750
|
async function stripProjectSettings(root) {
|
|
3760
3751
|
const settingsPath = claudePath(root, "settings.json");
|
|
3761
|
-
if (!await
|
|
3752
|
+
if (!await fs17.pathExists(settingsPath)) return false;
|
|
3762
3753
|
let settings = await readProjectSettings(root);
|
|
3763
3754
|
settings = stripHausAllow(stripHausDeny(stripHausHooks(settings)));
|
|
3764
3755
|
const hasContent = Object.keys(settings).length > 0;
|
|
@@ -3767,29 +3758,29 @@ async function stripProjectSettings(root) {
|
|
|
3767
3758
|
log(`Stripped haus rules from ${path22.relative(root, settingsPath)} (user settings preserved).`);
|
|
3768
3759
|
return true;
|
|
3769
3760
|
}
|
|
3770
|
-
await
|
|
3761
|
+
await fs17.remove(settingsPath);
|
|
3771
3762
|
log(`Removed ${path22.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
3772
3763
|
return true;
|
|
3773
3764
|
}
|
|
3774
3765
|
async function stripRootClaudeMd(root) {
|
|
3775
3766
|
const filePath = path22.join(root, "CLAUDE.md");
|
|
3776
|
-
if (!await
|
|
3777
|
-
const prev = await
|
|
3767
|
+
if (!await fs17.pathExists(filePath)) return false;
|
|
3768
|
+
const prev = await fs17.readFile(filePath, "utf8");
|
|
3778
3769
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
3779
3770
|
const next = stripHausBlock(prev);
|
|
3780
3771
|
if (next.length === 0) {
|
|
3781
|
-
await
|
|
3772
|
+
await fs17.remove(filePath);
|
|
3782
3773
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
3783
3774
|
} else {
|
|
3784
|
-
await
|
|
3775
|
+
await fs17.writeFile(filePath, next, "utf8");
|
|
3785
3776
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
3786
3777
|
}
|
|
3787
3778
|
return true;
|
|
3788
3779
|
}
|
|
3789
3780
|
async function pruneDirIfEmpty(dir) {
|
|
3790
|
-
if (!await
|
|
3791
|
-
const entries = await
|
|
3792
|
-
if (entries.length === 0) await
|
|
3781
|
+
if (!await fs17.pathExists(dir)) return;
|
|
3782
|
+
const entries = await fs17.readdir(dir);
|
|
3783
|
+
if (entries.length === 0) await fs17.remove(dir);
|
|
3793
3784
|
}
|
|
3794
3785
|
async function runUndo(options) {
|
|
3795
3786
|
const root = process.cwd();
|
|
@@ -3816,8 +3807,8 @@ User-owned .claude/ files will be preserved.`
|
|
|
3816
3807
|
}
|
|
3817
3808
|
}
|
|
3818
3809
|
for (const abs of managed) {
|
|
3819
|
-
if (!await
|
|
3820
|
-
await
|
|
3810
|
+
if (!await fs17.pathExists(abs)) continue;
|
|
3811
|
+
await fs17.remove(abs);
|
|
3821
3812
|
log(`Removed ${path22.relative(root, abs)}`);
|
|
3822
3813
|
}
|
|
3823
3814
|
if (stripSettings) await stripProjectSettings(root);
|
|
@@ -3830,7 +3821,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
3830
3821
|
// src/install/uninstall.ts
|
|
3831
3822
|
import crypto3 from "crypto";
|
|
3832
3823
|
import path23 from "path";
|
|
3833
|
-
import
|
|
3824
|
+
import fs18 from "fs-extra";
|
|
3834
3825
|
async function runUninstall(options = {}) {
|
|
3835
3826
|
const { force = false } = options;
|
|
3836
3827
|
const manifest = await readManifest();
|
|
@@ -3840,7 +3831,7 @@ async function runUninstall(options = {}) {
|
|
|
3840
3831
|
return result;
|
|
3841
3832
|
}
|
|
3842
3833
|
for (const entry of manifest.files) {
|
|
3843
|
-
const exists =
|
|
3834
|
+
const exists = fs18.pathExistsSync(entry.destPath);
|
|
3844
3835
|
if (!exists) continue;
|
|
3845
3836
|
const content2 = await readText(entry.destPath);
|
|
3846
3837
|
if (content2 === void 0) continue;
|
|
@@ -3858,8 +3849,8 @@ async function runUninstall(options = {}) {
|
|
|
3858
3849
|
result.skipped.push(entry.destPath);
|
|
3859
3850
|
continue;
|
|
3860
3851
|
}
|
|
3861
|
-
await
|
|
3862
|
-
await
|
|
3852
|
+
await fs18.remove(entry.destPath);
|
|
3853
|
+
await pruneEmptyDir(path23.dirname(entry.destPath));
|
|
3863
3854
|
result.deleted.push(entry.destPath);
|
|
3864
3855
|
}
|
|
3865
3856
|
const settings = await readSettings();
|
|
@@ -3868,12 +3859,12 @@ async function runUninstall(options = {}) {
|
|
|
3868
3859
|
result.hooksStripped = true;
|
|
3869
3860
|
const hausDir = path23.join(globalClaudeDir(), "haus");
|
|
3870
3861
|
const manifestPath2 = hausManifestPath();
|
|
3871
|
-
if (
|
|
3872
|
-
await
|
|
3862
|
+
if (fs18.pathExistsSync(manifestPath2)) {
|
|
3863
|
+
await fs18.remove(manifestPath2);
|
|
3873
3864
|
}
|
|
3874
|
-
if (
|
|
3875
|
-
const remaining = await
|
|
3876
|
-
if (remaining.length === 0) await
|
|
3865
|
+
if (fs18.pathExistsSync(hausDir)) {
|
|
3866
|
+
const remaining = await fs18.readdir(hausDir);
|
|
3867
|
+
if (remaining.length === 0) await fs18.remove(hausDir);
|
|
3877
3868
|
}
|
|
3878
3869
|
return result;
|
|
3879
3870
|
}
|
|
@@ -3890,13 +3881,6 @@ function printUninstallResult(result) {
|
|
|
3890
3881
|
log("Haus hook entries removed from ~/.claude/settings.json");
|
|
3891
3882
|
}
|
|
3892
3883
|
}
|
|
3893
|
-
async function pruneEmptyDir2(dir) {
|
|
3894
|
-
try {
|
|
3895
|
-
const entries = await fs17.readdir(dir);
|
|
3896
|
-
if (entries.length === 0) await fs17.remove(dir);
|
|
3897
|
-
} catch {
|
|
3898
|
-
}
|
|
3899
|
-
}
|
|
3900
3884
|
|
|
3901
3885
|
// src/commands/uninstall.ts
|
|
3902
3886
|
async function runUninstallCommand(options) {
|
|
@@ -4110,7 +4094,7 @@ async function detectGlobalInstallDrift() {
|
|
|
4110
4094
|
}
|
|
4111
4095
|
|
|
4112
4096
|
// src/commands/validate-catalog.ts
|
|
4113
|
-
import
|
|
4097
|
+
import fs19 from "fs";
|
|
4114
4098
|
import path26 from "path";
|
|
4115
4099
|
|
|
4116
4100
|
// src/catalog/forbidden-content.ts
|
|
@@ -4129,13 +4113,14 @@ function extractUseWhenSection(text) {
|
|
|
4129
4113
|
function isYamlBlockScalarHeader(rest) {
|
|
4130
4114
|
return /^[>|][-+]?(\d+)?(?:\s+#.*)?$/.test(rest);
|
|
4131
4115
|
}
|
|
4132
|
-
function
|
|
4116
|
+
function extractFrontmatterValue(text, key) {
|
|
4133
4117
|
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
4134
4118
|
if (!m) return "";
|
|
4135
4119
|
const lines = m[1].split(/\r?\n/);
|
|
4136
|
-
const
|
|
4120
|
+
const keyRe = new RegExp(`^${escapeRegExp(key)}:[ \\t]*`);
|
|
4121
|
+
const idx = lines.findIndex((l) => keyRe.test(l));
|
|
4137
4122
|
if (idx < 0) return "";
|
|
4138
|
-
const rest = lines[idx].replace(
|
|
4123
|
+
const rest = lines[idx].replace(keyRe, "").trim();
|
|
4139
4124
|
if (!rest) return "";
|
|
4140
4125
|
if (!isYamlBlockScalarHeader(rest)) {
|
|
4141
4126
|
return rest.replace(/^["']|["']$/g, "").trim();
|
|
@@ -4150,6 +4135,9 @@ function extractFrontmatterDescription(text) {
|
|
|
4150
4135
|
}
|
|
4151
4136
|
return body.join(" ").replace(/\s+/g, " ").trim();
|
|
4152
4137
|
}
|
|
4138
|
+
function extractFrontmatterDescription(text) {
|
|
4139
|
+
return extractFrontmatterValue(text, "description");
|
|
4140
|
+
}
|
|
4153
4141
|
function auditForbiddenTagsInText(text, label) {
|
|
4154
4142
|
const body = `${extractFrontmatterDescription(text)}
|
|
4155
4143
|
${extractUseWhenSection(text)}`;
|
|
@@ -4226,6 +4214,15 @@ function auditManifestStructure(items) {
|
|
|
4226
4214
|
}
|
|
4227
4215
|
return failures;
|
|
4228
4216
|
}
|
|
4217
|
+
function checkRequiredFrontmatter(text, label) {
|
|
4218
|
+
const failures = [];
|
|
4219
|
+
for (const key of REQUIRED_SKILL_FRONTMATTER) {
|
|
4220
|
+
if (!extractFrontmatterValue(text, key)) {
|
|
4221
|
+
failures.push(`${label}: missing non-empty frontmatter '${key}:'`);
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
return failures;
|
|
4225
|
+
}
|
|
4229
4226
|
function auditShippedFiles(manifestDir, items) {
|
|
4230
4227
|
const failures = [];
|
|
4231
4228
|
for (const item of items) {
|
|
@@ -4233,26 +4230,21 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4233
4230
|
const absPath = path26.join(manifestDir, item.path);
|
|
4234
4231
|
if (item.type === "skill") {
|
|
4235
4232
|
const skillMd = path26.join(absPath, "SKILL.md");
|
|
4236
|
-
if (!
|
|
4233
|
+
if (!fs19.existsSync(skillMd)) {
|
|
4237
4234
|
failures.push(`${item.id}: missing ${path26.relative(manifestDir, skillMd)}`);
|
|
4238
4235
|
continue;
|
|
4239
4236
|
}
|
|
4240
|
-
const text =
|
|
4241
|
-
|
|
4242
|
-
for (const key of REQUIRED_SKILL_FRONTMATTER) {
|
|
4243
|
-
if (key === "description" && !description) {
|
|
4244
|
-
failures.push(`${item.id}: SKILL.md missing non-empty frontmatter 'description:'`);
|
|
4245
|
-
}
|
|
4246
|
-
}
|
|
4237
|
+
const text = fs19.readFileSync(skillMd, "utf8");
|
|
4238
|
+
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
4247
4239
|
failures.push(
|
|
4248
4240
|
...auditForbiddenTagsInText(text, `${item.id}: ${path26.relative(manifestDir, skillMd)}`)
|
|
4249
4241
|
);
|
|
4250
4242
|
} else if (item.type === "agent") {
|
|
4251
|
-
if (!
|
|
4243
|
+
if (!fs19.existsSync(absPath)) {
|
|
4252
4244
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
4253
4245
|
continue;
|
|
4254
4246
|
}
|
|
4255
|
-
const text =
|
|
4247
|
+
const text = fs19.readFileSync(absPath, "utf8");
|
|
4256
4248
|
if (!text.startsWith("---")) failures.push(`${item.id}: agent file missing YAML frontmatter`);
|
|
4257
4249
|
for (const section of REQUIRED_AGENT_SECTIONS) {
|
|
4258
4250
|
if (!text.includes(section)) failures.push(`${item.id}: agent file missing ${section}`);
|
|
@@ -4266,16 +4258,19 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4266
4258
|
...auditForbiddenTagsInText(text, `${item.id}: ${path26.relative(manifestDir, absPath)}`)
|
|
4267
4259
|
);
|
|
4268
4260
|
} else if (item.type === "template") {
|
|
4269
|
-
if (!
|
|
4261
|
+
if (!fs19.existsSync(absPath)) {
|
|
4270
4262
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
4271
4263
|
continue;
|
|
4272
4264
|
}
|
|
4273
4265
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
4274
4266
|
} else if (item.type === "command") {
|
|
4275
|
-
if (!
|
|
4267
|
+
if (!fs19.existsSync(absPath)) {
|
|
4276
4268
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
4277
4269
|
continue;
|
|
4278
4270
|
}
|
|
4271
|
+
const text = fs19.readFileSync(absPath, "utf8");
|
|
4272
|
+
const rel = path26.relative(manifestDir, absPath);
|
|
4273
|
+
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
4279
4274
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
4280
4275
|
}
|
|
4281
4276
|
}
|
|
@@ -4283,7 +4278,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4283
4278
|
}
|
|
4284
4279
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
4285
4280
|
const rel = path26.relative(manifestDir, absPath);
|
|
4286
|
-
const text =
|
|
4281
|
+
const text = fs19.readFileSync(absPath, "utf8");
|
|
4287
4282
|
const failures = [];
|
|
4288
4283
|
const lines = text.split(/\r?\n/);
|
|
4289
4284
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -4306,9 +4301,9 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4306
4301
|
const dirs = ["skills", "agents", "templates", "commands"];
|
|
4307
4302
|
for (const dir of dirs) {
|
|
4308
4303
|
const abs = path26.join(manifestDir, dir);
|
|
4309
|
-
if (!
|
|
4304
|
+
if (!fs19.existsSync(abs)) continue;
|
|
4310
4305
|
walkMd(abs, (file) => {
|
|
4311
|
-
const text =
|
|
4306
|
+
const text = fs19.readFileSync(file, "utf8");
|
|
4312
4307
|
const rel = path26.relative(manifestDir, file);
|
|
4313
4308
|
const lines = text.split(/\r?\n/);
|
|
4314
4309
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -4328,7 +4323,7 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4328
4323
|
return failures;
|
|
4329
4324
|
}
|
|
4330
4325
|
function walkMd(dir, fn) {
|
|
4331
|
-
for (const entry of
|
|
4326
|
+
for (const entry of fs19.readdirSync(dir, { withFileTypes: true })) {
|
|
4332
4327
|
const full = path26.join(dir, entry.name);
|
|
4333
4328
|
if (entry.isDirectory()) walkMd(full, fn);
|
|
4334
4329
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
@@ -4731,7 +4726,7 @@ import path32 from "path";
|
|
|
4731
4726
|
|
|
4732
4727
|
// src/claude/write-workspace-claude-md.ts
|
|
4733
4728
|
import path31 from "path";
|
|
4734
|
-
import
|
|
4729
|
+
import fs20 from "fs-extra";
|
|
4735
4730
|
function buildWorkspaceImportBlock(client, members) {
|
|
4736
4731
|
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
4737
4732
|
const body = [
|
|
@@ -4750,7 +4745,7 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
4750
4745
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
4751
4746
|
const dryRun = opts.dryRun ?? false;
|
|
4752
4747
|
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path31.join(workspaceRoot, "CLAUDE.md");
|
|
4753
|
-
const prev = await
|
|
4748
|
+
const prev = await fs20.pathExists(filePath) ? await fs20.readFile(filePath, "utf8") : "";
|
|
4754
4749
|
const next = opts.collision ? `${block}
|
|
4755
4750
|
` : injectHausBlock(prev, block);
|
|
4756
4751
|
const printable = displayPath(workspaceRoot, filePath);
|