@haus-tech/haus-workflow 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/cli.js +320 -288
- package/library/catalog/manifest.json +1 -1
- package/library/global/commands/haus-clone.md +32 -0
- package/library/global/skills/haus-workflow/SKILL.md +18 -9
- 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
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { readFileSync as readFileSync4 } from "fs";
|
|
5
|
-
import
|
|
5
|
+
import path35 from "path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
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");
|
|
@@ -547,8 +554,8 @@ function buildDenyRules() {
|
|
|
547
554
|
for (const command of DANGEROUS_COMMANDS) {
|
|
548
555
|
rules.push(`Bash(${command}:*)`);
|
|
549
556
|
}
|
|
550
|
-
for (const
|
|
551
|
-
const pattern = SENSITIVE_DIRS.has(
|
|
557
|
+
for (const path36 of SENSITIVE_PATHS) {
|
|
558
|
+
const pattern = SENSITIVE_DIRS.has(path36) ? `${path36}/**` : path36;
|
|
552
559
|
for (const tool of FILE_TOOLS) {
|
|
553
560
|
rules.push(`${tool}(${pattern})`);
|
|
554
561
|
}
|
|
@@ -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
|
}
|
|
@@ -1653,8 +1643,67 @@ async function runCatalogAudit() {
|
|
|
1653
1643
|
log("Catalog audit passed.");
|
|
1654
1644
|
}
|
|
1655
1645
|
|
|
1656
|
-
// src/commands/
|
|
1646
|
+
// src/commands/clone.ts
|
|
1647
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1657
1648
|
import path14 from "path";
|
|
1649
|
+
|
|
1650
|
+
// src/utils/exec.ts
|
|
1651
|
+
import { execa } from "execa";
|
|
1652
|
+
async function runCommand(command, args = [], options = {}) {
|
|
1653
|
+
try {
|
|
1654
|
+
const result = await execa(command, args, {
|
|
1655
|
+
reject: false,
|
|
1656
|
+
// non-zero exits are returned, not thrown
|
|
1657
|
+
...options
|
|
1658
|
+
});
|
|
1659
|
+
return {
|
|
1660
|
+
command,
|
|
1661
|
+
args,
|
|
1662
|
+
stdout: String(result.stdout ?? ""),
|
|
1663
|
+
stderr: String(result.stderr ?? ""),
|
|
1664
|
+
exitCode: result.exitCode ?? 0
|
|
1665
|
+
};
|
|
1666
|
+
} catch (error2) {
|
|
1667
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1668
|
+
throw new Error(`Failed to run command: ${command} ${args.join(" ")} (${message})`);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
async function runGit(args, options = {}) {
|
|
1672
|
+
return runCommand("git", args, options);
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
// src/commands/clone.ts
|
|
1676
|
+
function repoNameFromUrl(url) {
|
|
1677
|
+
const trimmed = url.trim().replace(/\.git$/, "").replace(/\/+$/, "");
|
|
1678
|
+
const tail = trimmed.split(/[/:]/).pop() ?? "";
|
|
1679
|
+
return tail || "repo";
|
|
1680
|
+
}
|
|
1681
|
+
async function runClone(url, opts = {}) {
|
|
1682
|
+
if (!url || !url.trim()) {
|
|
1683
|
+
error("A git URL is required: `haus clone <url> [dir]`.");
|
|
1684
|
+
process.exitCode = 1;
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
const target = path14.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
1688
|
+
if (existsSync2(target)) {
|
|
1689
|
+
log(`\u2022 ${path14.basename(target)} already present at ${target} \u2014 skipped`);
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
if (opts.dryRun) {
|
|
1693
|
+
log(`would clone ${url} \u2192 ${target}`);
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
const res = await runGit(["clone", url, target]);
|
|
1697
|
+
if (res.exitCode !== 0) {
|
|
1698
|
+
error(`clone failed for ${url}: ${(res.stderr || res.stdout).trim()}`);
|
|
1699
|
+
process.exitCode = 1;
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
log(`\u2713 cloned ${url} \u2192 ${target}`);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// src/commands/config.ts
|
|
1706
|
+
import path15 from "path";
|
|
1658
1707
|
var CONFIG_PATH2 = ".haus-workflow/config.json";
|
|
1659
1708
|
var HOOK_ALIASES = {
|
|
1660
1709
|
"hook.context": "context"
|
|
@@ -1667,7 +1716,7 @@ async function runConfig(key, action) {
|
|
|
1667
1716
|
);
|
|
1668
1717
|
}
|
|
1669
1718
|
const root = process.cwd();
|
|
1670
|
-
const configPath =
|
|
1719
|
+
const configPath = path15.join(root, CONFIG_PATH2);
|
|
1671
1720
|
const existing = await readJson(configPath);
|
|
1672
1721
|
const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
|
|
1673
1722
|
cfg.hooks ??= {};
|
|
@@ -2039,7 +2088,7 @@ function selectRules(recommended, task, taskIntents) {
|
|
|
2039
2088
|
|
|
2040
2089
|
// src/scanner/scan-project.ts
|
|
2041
2090
|
import { readFile as readFile2 } from "fs/promises";
|
|
2042
|
-
import
|
|
2091
|
+
import path19 from "path";
|
|
2043
2092
|
|
|
2044
2093
|
// src/utils/audit-checks.ts
|
|
2045
2094
|
function isRecord(v) {
|
|
@@ -2066,8 +2115,8 @@ function compareVersions(a, b) {
|
|
|
2066
2115
|
}
|
|
2067
2116
|
|
|
2068
2117
|
// src/scanner/detect-package-manager.ts
|
|
2069
|
-
import
|
|
2070
|
-
import
|
|
2118
|
+
import path16 from "path";
|
|
2119
|
+
import fs13 from "fs-extra";
|
|
2071
2120
|
function detectPackageManager(root, packageManagerField) {
|
|
2072
2121
|
const field = String(packageManagerField ?? "").trim();
|
|
2073
2122
|
if (field.startsWith("yarn@")) {
|
|
@@ -2085,9 +2134,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
2085
2134
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
2086
2135
|
return "unknown";
|
|
2087
2136
|
}
|
|
2088
|
-
if (
|
|
2089
|
-
if (
|
|
2090
|
-
if (
|
|
2137
|
+
if (fs13.existsSync(path16.join(root, "yarn.lock"))) return "yarn";
|
|
2138
|
+
if (fs13.existsSync(path16.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
2139
|
+
if (fs13.existsSync(path16.join(root, "package-lock.json"))) return "npm";
|
|
2091
2140
|
return "unknown";
|
|
2092
2141
|
}
|
|
2093
2142
|
|
|
@@ -2260,7 +2309,7 @@ function runDetection(ctx, rules = STACK_RULES) {
|
|
|
2260
2309
|
}
|
|
2261
2310
|
|
|
2262
2311
|
// src/scanner/detection.ts
|
|
2263
|
-
import
|
|
2312
|
+
import path17 from "path";
|
|
2264
2313
|
var UNSUPPORTED_MARKERS = {
|
|
2265
2314
|
"requirements.txt": "python",
|
|
2266
2315
|
"pyproject.toml": "python",
|
|
@@ -2314,14 +2363,14 @@ function finalizeRoles(registryRoles, deps, files) {
|
|
|
2314
2363
|
function collectUnsupportedSignals(files) {
|
|
2315
2364
|
return [
|
|
2316
2365
|
...new Set(
|
|
2317
|
-
files.map((f) => UNSUPPORTED_MARKERS[
|
|
2366
|
+
files.map((f) => UNSUPPORTED_MARKERS[path17.basename(f)]).filter((s) => Boolean(s))
|
|
2318
2367
|
)
|
|
2319
2368
|
].sort();
|
|
2320
2369
|
}
|
|
2321
2370
|
|
|
2322
2371
|
// src/scanner/render.ts
|
|
2323
2372
|
import { readFile } from "fs/promises";
|
|
2324
|
-
import
|
|
2373
|
+
import path18 from "path";
|
|
2325
2374
|
|
|
2326
2375
|
// src/scanner/role-labels.ts
|
|
2327
2376
|
var ROLE_LABELS = {
|
|
@@ -2383,7 +2432,7 @@ async function buildContentBlob(root, files) {
|
|
|
2383
2432
|
const slice = candidates.slice(0, 300);
|
|
2384
2433
|
const parts = await mapWithConcurrency(slice, async (rel) => {
|
|
2385
2434
|
try {
|
|
2386
|
-
return await readFile(
|
|
2435
|
+
return await readFile(path18.join(root, rel), "utf8");
|
|
2387
2436
|
} catch {
|
|
2388
2437
|
return "";
|
|
2389
2438
|
}
|
|
@@ -2481,8 +2530,8 @@ var SAFE_FILES = [
|
|
|
2481
2530
|
"Gemfile"
|
|
2482
2531
|
];
|
|
2483
2532
|
async function scanProject(root, mode = "fast") {
|
|
2484
|
-
const pkg = await readJson(
|
|
2485
|
-
const composer = await readJson(
|
|
2533
|
+
const pkg = await readJson(path19.join(root, "package.json"));
|
|
2534
|
+
const composer = await readJson(path19.join(root, "composer.json"));
|
|
2486
2535
|
const files = await listFiles(root, SAFE_FILES);
|
|
2487
2536
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
2488
2537
|
const deps = dependencySet(pkg, composer);
|
|
@@ -2516,7 +2565,7 @@ async function scanProject(root, mode = "fast") {
|
|
|
2516
2565
|
mode,
|
|
2517
2566
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2518
2567
|
root,
|
|
2519
|
-
repoName: String(pkg?.name ??
|
|
2568
|
+
repoName: String(pkg?.name ?? path19.basename(root)),
|
|
2520
2569
|
packageManager,
|
|
2521
2570
|
repoRoles: roles,
|
|
2522
2571
|
detectedStacks: stacks,
|
|
@@ -2534,7 +2583,7 @@ async function scanProject(root, mode = "fast") {
|
|
|
2534
2583
|
const scanHashes = Object.fromEntries(
|
|
2535
2584
|
await mapWithConcurrency(
|
|
2536
2585
|
safeFiles,
|
|
2537
|
-
async (f) => [f, hashText(await readFile2(
|
|
2586
|
+
async (f) => [f, hashText(await readFile2(path19.join(root, f), "utf8"))]
|
|
2538
2587
|
)
|
|
2539
2588
|
);
|
|
2540
2589
|
const repoSummary = renderSummary(context);
|
|
@@ -2619,8 +2668,8 @@ async function runContext(options) {
|
|
|
2619
2668
|
}
|
|
2620
2669
|
|
|
2621
2670
|
// src/commands/doctor.ts
|
|
2622
|
-
import
|
|
2623
|
-
import
|
|
2671
|
+
import path20 from "path";
|
|
2672
|
+
import fs14 from "fs-extra";
|
|
2624
2673
|
|
|
2625
2674
|
// src/update/npm-version.ts
|
|
2626
2675
|
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
@@ -2700,7 +2749,7 @@ async function runDoctor(options) {
|
|
|
2700
2749
|
const enabled = await isHookEnabled(root, key);
|
|
2701
2750
|
ok(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
|
|
2702
2751
|
}
|
|
2703
|
-
const rootClaudeMdPath =
|
|
2752
|
+
const rootClaudeMdPath = path20.join(root, "CLAUDE.md");
|
|
2704
2753
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
2705
2754
|
if (!rootClaudeMdContent) {
|
|
2706
2755
|
flag(
|
|
@@ -2728,7 +2777,7 @@ async function runDoctor(options) {
|
|
|
2728
2777
|
const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
|
|
2729
2778
|
const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
|
|
2730
2779
|
for (const target of importTargets) {
|
|
2731
|
-
if (!await
|
|
2780
|
+
if (!await fs14.pathExists(hausPath(root, target))) {
|
|
2732
2781
|
flag(
|
|
2733
2782
|
`- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
|
|
2734
2783
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -2739,7 +2788,7 @@ async function runDoctor(options) {
|
|
|
2739
2788
|
}
|
|
2740
2789
|
}
|
|
2741
2790
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
2742
|
-
const workflowExists = await
|
|
2791
|
+
const workflowExists = await fs14.pathExists(workflowPath);
|
|
2743
2792
|
if (!workflowExists) {
|
|
2744
2793
|
flag(
|
|
2745
2794
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -2753,15 +2802,15 @@ async function runDoctor(options) {
|
|
|
2753
2802
|
ok("- .haus-workflow/WORKFLOW.md: OK (user-owned)");
|
|
2754
2803
|
} else {
|
|
2755
2804
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
2756
|
-
const cachePath =
|
|
2757
|
-
const bundledPath =
|
|
2805
|
+
const cachePath = path20.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
2806
|
+
const bundledPath = path20.join(
|
|
2758
2807
|
packageRoot(),
|
|
2759
2808
|
"library",
|
|
2760
2809
|
"global",
|
|
2761
2810
|
"templates",
|
|
2762
2811
|
"agentic-workflow-standard.md"
|
|
2763
2812
|
);
|
|
2764
|
-
const templatePath = await
|
|
2813
|
+
const templatePath = await fs14.pathExists(cachePath) ? cachePath : bundledPath;
|
|
2765
2814
|
const templateContent = await readText(templatePath);
|
|
2766
2815
|
if (storedHashMatch && templateContent) {
|
|
2767
2816
|
const currentHash = hashText(normaliseLF(templateContent));
|
|
@@ -2780,7 +2829,7 @@ async function runDoctor(options) {
|
|
|
2780
2829
|
}
|
|
2781
2830
|
}
|
|
2782
2831
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
2783
|
-
const workflowConfigExists = await
|
|
2832
|
+
const workflowConfigExists = await fs14.pathExists(workflowConfigPath);
|
|
2784
2833
|
if (!workflowConfigExists) {
|
|
2785
2834
|
flag(
|
|
2786
2835
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -2788,7 +2837,7 @@ async function runDoctor(options) {
|
|
|
2788
2837
|
"haus apply --write"
|
|
2789
2838
|
);
|
|
2790
2839
|
} else {
|
|
2791
|
-
const cfg = await
|
|
2840
|
+
const cfg = await fs14.readFile(workflowConfigPath, "utf8");
|
|
2792
2841
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
2793
2842
|
if (unfilled > 0) {
|
|
2794
2843
|
flag(
|
|
@@ -2819,7 +2868,7 @@ async function runDoctor(options) {
|
|
|
2819
2868
|
ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
|
|
2820
2869
|
}
|
|
2821
2870
|
}
|
|
2822
|
-
const pkgJson = await readJson(
|
|
2871
|
+
const pkgJson = await readJson(path20.join(packageRoot(), "package.json"));
|
|
2823
2872
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
2824
2873
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
2825
2874
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -2962,8 +3011,8 @@ async function runGuard(kind, _options) {
|
|
|
2962
3011
|
}
|
|
2963
3012
|
|
|
2964
3013
|
// src/commands/init.ts
|
|
2965
|
-
import
|
|
2966
|
-
import
|
|
3014
|
+
import path21 from "path";
|
|
3015
|
+
import fs15 from "fs-extra";
|
|
2967
3016
|
|
|
2968
3017
|
// src/utils/prompts.ts
|
|
2969
3018
|
import { stdin as input, stdout as output } from "process";
|
|
@@ -2983,31 +3032,6 @@ async function confirm(question) {
|
|
|
2983
3032
|
return answer === "y" || answer === "yes";
|
|
2984
3033
|
}
|
|
2985
3034
|
|
|
2986
|
-
// src/utils/exec.ts
|
|
2987
|
-
import { execa } from "execa";
|
|
2988
|
-
async function runCommand(command, args = [], options = {}) {
|
|
2989
|
-
try {
|
|
2990
|
-
const result = await execa(command, args, {
|
|
2991
|
-
reject: false,
|
|
2992
|
-
// non-zero exits are returned, not thrown
|
|
2993
|
-
...options
|
|
2994
|
-
});
|
|
2995
|
-
return {
|
|
2996
|
-
command,
|
|
2997
|
-
args,
|
|
2998
|
-
stdout: String(result.stdout ?? ""),
|
|
2999
|
-
stderr: String(result.stderr ?? ""),
|
|
3000
|
-
exitCode: result.exitCode ?? 0
|
|
3001
|
-
};
|
|
3002
|
-
} catch (error2) {
|
|
3003
|
-
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
3004
|
-
throw new Error(`Failed to run command: ${command} ${args.join(" ")} (${message})`);
|
|
3005
|
-
}
|
|
3006
|
-
}
|
|
3007
|
-
async function runGit(args, options = {}) {
|
|
3008
|
-
return runCommand("git", args, options);
|
|
3009
|
-
}
|
|
3010
|
-
|
|
3011
3035
|
// src/recommender/git-signal.ts
|
|
3012
3036
|
async function readChangedFiles(root) {
|
|
3013
3037
|
if (process.env.HAUS_DISABLE_GIT_SIGNALS === "1") return [];
|
|
@@ -3186,7 +3210,8 @@ async function recommend(root, context) {
|
|
|
3186
3210
|
(t) => context.warnings.join(" ").toLowerCase().includes(t.toLowerCase())
|
|
3187
3211
|
);
|
|
3188
3212
|
if (configSignal) push("config-signal-match", "config signal match", `warning:${configSignal}`);
|
|
3189
|
-
const
|
|
3213
|
+
const idSegment = item.id.split(".").pop() ?? "";
|
|
3214
|
+
const changedMatch = idSegment ? changedFiles.find((f) => f.includes(idSegment)) : void 0;
|
|
3190
3215
|
if (changedMatch)
|
|
3191
3216
|
push("changed-file-match", "changed file match", `changedFile:${changedMatch}`);
|
|
3192
3217
|
const requiresAny = item.requiresAny ?? [];
|
|
@@ -3349,8 +3374,8 @@ async function runSetupProject(options) {
|
|
|
3349
3374
|
// src/commands/init.ts
|
|
3350
3375
|
async function runInit(options) {
|
|
3351
3376
|
const root = process.cwd();
|
|
3352
|
-
const hausDir =
|
|
3353
|
-
const alreadyInit = await
|
|
3377
|
+
const hausDir = path21.join(root, ".haus-workflow");
|
|
3378
|
+
const alreadyInit = await fs15.pathExists(hausDir);
|
|
3354
3379
|
if (alreadyInit) {
|
|
3355
3380
|
log("Haus AI already initialized in this project.");
|
|
3356
3381
|
log("Run `haus setup-project` to reconfigure.");
|
|
@@ -3362,8 +3387,8 @@ async function runInit(options) {
|
|
|
3362
3387
|
|
|
3363
3388
|
// src/install/apply.ts
|
|
3364
3389
|
import crypto2 from "crypto";
|
|
3365
|
-
import
|
|
3366
|
-
import
|
|
3390
|
+
import path22 from "path";
|
|
3391
|
+
import fs16 from "fs-extra";
|
|
3367
3392
|
|
|
3368
3393
|
// src/install/header.ts
|
|
3369
3394
|
var MD_PREFIX = "<!-- HAUS-MANAGED";
|
|
@@ -3451,40 +3476,40 @@ function hashContent(content2) {
|
|
|
3451
3476
|
}
|
|
3452
3477
|
function sourceVersion() {
|
|
3453
3478
|
try {
|
|
3454
|
-
const pkgPath =
|
|
3455
|
-
const pkg = JSON.parse(
|
|
3479
|
+
const pkgPath = path22.join(packageRoot(), "package.json");
|
|
3480
|
+
const pkg = JSON.parse(fs16.readFileSync(pkgPath, "utf8"));
|
|
3456
3481
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
3457
3482
|
} catch {
|
|
3458
3483
|
return "haus@0.0.0";
|
|
3459
3484
|
}
|
|
3460
3485
|
}
|
|
3461
3486
|
function globalSrcDir() {
|
|
3462
|
-
return
|
|
3487
|
+
return path22.join(packageRoot(), "library", "global");
|
|
3463
3488
|
}
|
|
3464
3489
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
3465
3490
|
const entries = [];
|
|
3466
|
-
const skillsDir =
|
|
3467
|
-
if (
|
|
3468
|
-
for (const skillName of
|
|
3469
|
-
const skillFile =
|
|
3470
|
-
if (
|
|
3491
|
+
const skillsDir = path22.join(srcDir, "skills");
|
|
3492
|
+
if (fs16.pathExistsSync(skillsDir)) {
|
|
3493
|
+
for (const skillName of fs16.readdirSync(skillsDir)) {
|
|
3494
|
+
const skillFile = path22.join(skillsDir, skillName, "SKILL.md");
|
|
3495
|
+
if (fs16.pathExistsSync(skillFile)) {
|
|
3471
3496
|
entries.push({
|
|
3472
3497
|
stableId: `skill.${skillName}`,
|
|
3473
|
-
srcRelPath:
|
|
3474
|
-
destPath:
|
|
3498
|
+
srcRelPath: path22.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
3499
|
+
destPath: path22.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
3475
3500
|
});
|
|
3476
3501
|
}
|
|
3477
3502
|
}
|
|
3478
3503
|
}
|
|
3479
|
-
const commandsDir =
|
|
3480
|
-
if (
|
|
3481
|
-
for (const fileName of
|
|
3504
|
+
const commandsDir = path22.join(srcDir, "commands");
|
|
3505
|
+
if (fs16.pathExistsSync(commandsDir)) {
|
|
3506
|
+
for (const fileName of fs16.readdirSync(commandsDir)) {
|
|
3482
3507
|
if (!fileName.endsWith(".md")) continue;
|
|
3483
3508
|
const commandName = fileName.slice(0, -".md".length);
|
|
3484
3509
|
entries.push({
|
|
3485
3510
|
stableId: `command.${commandName}`,
|
|
3486
|
-
srcRelPath:
|
|
3487
|
-
destPath:
|
|
3511
|
+
srcRelPath: path22.join("library", "global", "commands", fileName),
|
|
3512
|
+
destPath: path22.join(claudeDir, "commands", fileName)
|
|
3488
3513
|
});
|
|
3489
3514
|
}
|
|
3490
3515
|
}
|
|
@@ -3508,7 +3533,7 @@ async function applyInstall(options = {}) {
|
|
|
3508
3533
|
};
|
|
3509
3534
|
const manifestFiles = [];
|
|
3510
3535
|
for (const entry of sourceFiles) {
|
|
3511
|
-
const srcPath =
|
|
3536
|
+
const srcPath = path22.join(packageRoot(), entry.srcRelPath);
|
|
3512
3537
|
const rawContent = await readText(srcPath);
|
|
3513
3538
|
if (rawContent === void 0) {
|
|
3514
3539
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -3528,7 +3553,7 @@ async function applyInstall(options = {}) {
|
|
|
3528
3553
|
}
|
|
3529
3554
|
continue;
|
|
3530
3555
|
}
|
|
3531
|
-
const destExists =
|
|
3556
|
+
const destExists = fs16.pathExistsSync(entry.destPath);
|
|
3532
3557
|
if (destExists) {
|
|
3533
3558
|
const currentContent = await readText(entry.destPath);
|
|
3534
3559
|
if (currentContent !== void 0) {
|
|
@@ -3564,7 +3589,7 @@ async function applyInstall(options = {}) {
|
|
|
3564
3589
|
schemaVersion: SCHEMA_VERSION2
|
|
3565
3590
|
});
|
|
3566
3591
|
}
|
|
3567
|
-
const fragmentPath =
|
|
3592
|
+
const fragmentPath = path22.join(srcDir, "settings-fragments", "hooks.json");
|
|
3568
3593
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
3569
3594
|
const settings = await readSettings();
|
|
3570
3595
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -3575,13 +3600,13 @@ async function applyInstall(options = {}) {
|
|
|
3575
3600
|
const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
|
|
3576
3601
|
for (const entry of existingManifest.files) {
|
|
3577
3602
|
if (currentDestPaths.has(entry.destPath)) continue;
|
|
3578
|
-
if (!
|
|
3603
|
+
if (!fs16.pathExistsSync(entry.destPath)) continue;
|
|
3579
3604
|
const content2 = await readText(entry.destPath);
|
|
3580
3605
|
if (!content2) continue;
|
|
3581
3606
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
3582
3607
|
const currentHash = hashContent(content2);
|
|
3583
3608
|
if (hasHeader && currentHash === entry.hash) {
|
|
3584
|
-
if (!dryRun) await
|
|
3609
|
+
if (!dryRun) await fs16.remove(entry.destPath);
|
|
3585
3610
|
result.deleted.push(entry.destPath);
|
|
3586
3611
|
} else {
|
|
3587
3612
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -3708,8 +3733,8 @@ async function runScan(options) {
|
|
|
3708
3733
|
}
|
|
3709
3734
|
|
|
3710
3735
|
// src/commands/undo.ts
|
|
3711
|
-
import
|
|
3712
|
-
import
|
|
3736
|
+
import path23 from "path";
|
|
3737
|
+
import fs17 from "fs-extra";
|
|
3713
3738
|
|
|
3714
3739
|
// src/claude/managed-paths.ts
|
|
3715
3740
|
var PROJECT_MANAGED_CLAUDE_REL = [
|
|
@@ -3735,61 +3760,61 @@ async function collectManagedPaths(root) {
|
|
|
3735
3760
|
const lock = await readJson(hausPath(root, "haus.lock.json"));
|
|
3736
3761
|
for (const row of lock ?? []) {
|
|
3737
3762
|
for (const rel of row.paths ?? []) {
|
|
3738
|
-
paths.add(
|
|
3763
|
+
paths.add(path23.resolve(root, rel));
|
|
3739
3764
|
}
|
|
3740
3765
|
}
|
|
3741
3766
|
const existing = [];
|
|
3742
3767
|
for (const abs of paths) {
|
|
3743
|
-
if (await
|
|
3768
|
+
if (await fs17.pathExists(abs)) existing.push(abs);
|
|
3744
3769
|
}
|
|
3745
3770
|
return existing;
|
|
3746
3771
|
}
|
|
3747
3772
|
async function settingsHasHausContent(root) {
|
|
3748
3773
|
const settingsPath = claudePath(root, "settings.json");
|
|
3749
|
-
if (!await
|
|
3774
|
+
if (!await fs17.pathExists(settingsPath)) return false;
|
|
3750
3775
|
const settings = await readProjectSettings(root);
|
|
3751
3776
|
return settings._haus != null;
|
|
3752
3777
|
}
|
|
3753
3778
|
async function claudeMdHasHausBlock(root) {
|
|
3754
|
-
const filePath =
|
|
3755
|
-
if (!await
|
|
3756
|
-
const text = await
|
|
3779
|
+
const filePath = path23.join(root, "CLAUDE.md");
|
|
3780
|
+
if (!await fs17.pathExists(filePath)) return false;
|
|
3781
|
+
const text = await fs17.readFile(filePath, "utf8");
|
|
3757
3782
|
return text.includes(BLOCK_BEGIN);
|
|
3758
3783
|
}
|
|
3759
3784
|
async function stripProjectSettings(root) {
|
|
3760
3785
|
const settingsPath = claudePath(root, "settings.json");
|
|
3761
|
-
if (!await
|
|
3786
|
+
if (!await fs17.pathExists(settingsPath)) return false;
|
|
3762
3787
|
let settings = await readProjectSettings(root);
|
|
3763
3788
|
settings = stripHausAllow(stripHausDeny(stripHausHooks(settings)));
|
|
3764
3789
|
const hasContent = Object.keys(settings).length > 0;
|
|
3765
3790
|
if (hasContent) {
|
|
3766
3791
|
await writeProjectSettings(root, settings);
|
|
3767
|
-
log(`Stripped haus rules from ${
|
|
3792
|
+
log(`Stripped haus rules from ${path23.relative(root, settingsPath)} (user settings preserved).`);
|
|
3768
3793
|
return true;
|
|
3769
3794
|
}
|
|
3770
|
-
await
|
|
3771
|
-
log(`Removed ${
|
|
3795
|
+
await fs17.remove(settingsPath);
|
|
3796
|
+
log(`Removed ${path23.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
3772
3797
|
return true;
|
|
3773
3798
|
}
|
|
3774
3799
|
async function stripRootClaudeMd(root) {
|
|
3775
|
-
const filePath =
|
|
3776
|
-
if (!await
|
|
3777
|
-
const prev = await
|
|
3800
|
+
const filePath = path23.join(root, "CLAUDE.md");
|
|
3801
|
+
if (!await fs17.pathExists(filePath)) return false;
|
|
3802
|
+
const prev = await fs17.readFile(filePath, "utf8");
|
|
3778
3803
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
3779
3804
|
const next = stripHausBlock(prev);
|
|
3780
3805
|
if (next.length === 0) {
|
|
3781
|
-
await
|
|
3806
|
+
await fs17.remove(filePath);
|
|
3782
3807
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
3783
3808
|
} else {
|
|
3784
|
-
await
|
|
3809
|
+
await fs17.writeFile(filePath, next, "utf8");
|
|
3785
3810
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
3786
3811
|
}
|
|
3787
3812
|
return true;
|
|
3788
3813
|
}
|
|
3789
3814
|
async function pruneDirIfEmpty(dir) {
|
|
3790
|
-
if (!await
|
|
3791
|
-
const entries = await
|
|
3792
|
-
if (entries.length === 0) await
|
|
3815
|
+
if (!await fs17.pathExists(dir)) return;
|
|
3816
|
+
const entries = await fs17.readdir(dir);
|
|
3817
|
+
if (entries.length === 0) await fs17.remove(dir);
|
|
3793
3818
|
}
|
|
3794
3819
|
async function runUndo(options) {
|
|
3795
3820
|
const root = process.cwd();
|
|
@@ -3800,7 +3825,7 @@ async function runUndo(options) {
|
|
|
3800
3825
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
3801
3826
|
return;
|
|
3802
3827
|
}
|
|
3803
|
-
const relTargets = managed.map((p) =>
|
|
3828
|
+
const relTargets = managed.map((p) => path23.relative(root, p));
|
|
3804
3829
|
const summaryParts = [...relTargets];
|
|
3805
3830
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
3806
3831
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -3816,9 +3841,9 @@ User-owned .claude/ files will be preserved.`
|
|
|
3816
3841
|
}
|
|
3817
3842
|
}
|
|
3818
3843
|
for (const abs of managed) {
|
|
3819
|
-
if (!await
|
|
3820
|
-
await
|
|
3821
|
-
log(`Removed ${
|
|
3844
|
+
if (!await fs17.pathExists(abs)) continue;
|
|
3845
|
+
await fs17.remove(abs);
|
|
3846
|
+
log(`Removed ${path23.relative(root, abs)}`);
|
|
3822
3847
|
}
|
|
3823
3848
|
if (stripSettings) await stripProjectSettings(root);
|
|
3824
3849
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -3829,8 +3854,8 @@ User-owned .claude/ files will be preserved.`
|
|
|
3829
3854
|
|
|
3830
3855
|
// src/install/uninstall.ts
|
|
3831
3856
|
import crypto3 from "crypto";
|
|
3832
|
-
import
|
|
3833
|
-
import
|
|
3857
|
+
import path24 from "path";
|
|
3858
|
+
import fs18 from "fs-extra";
|
|
3834
3859
|
async function runUninstall(options = {}) {
|
|
3835
3860
|
const { force = false } = options;
|
|
3836
3861
|
const manifest = await readManifest();
|
|
@@ -3840,7 +3865,7 @@ async function runUninstall(options = {}) {
|
|
|
3840
3865
|
return result;
|
|
3841
3866
|
}
|
|
3842
3867
|
for (const entry of manifest.files) {
|
|
3843
|
-
const exists =
|
|
3868
|
+
const exists = fs18.pathExistsSync(entry.destPath);
|
|
3844
3869
|
if (!exists) continue;
|
|
3845
3870
|
const content2 = await readText(entry.destPath);
|
|
3846
3871
|
if (content2 === void 0) continue;
|
|
@@ -3858,22 +3883,22 @@ async function runUninstall(options = {}) {
|
|
|
3858
3883
|
result.skipped.push(entry.destPath);
|
|
3859
3884
|
continue;
|
|
3860
3885
|
}
|
|
3861
|
-
await
|
|
3862
|
-
await
|
|
3886
|
+
await fs18.remove(entry.destPath);
|
|
3887
|
+
await pruneEmptyDir(path24.dirname(entry.destPath));
|
|
3863
3888
|
result.deleted.push(entry.destPath);
|
|
3864
3889
|
}
|
|
3865
3890
|
const settings = await readSettings();
|
|
3866
3891
|
const stripped = stripHausHooks(stripHausAllow(stripHausDeny(settings)));
|
|
3867
3892
|
await writeSettings(stripped);
|
|
3868
3893
|
result.hooksStripped = true;
|
|
3869
|
-
const hausDir =
|
|
3894
|
+
const hausDir = path24.join(globalClaudeDir(), "haus");
|
|
3870
3895
|
const manifestPath2 = hausManifestPath();
|
|
3871
|
-
if (
|
|
3872
|
-
await
|
|
3896
|
+
if (fs18.pathExistsSync(manifestPath2)) {
|
|
3897
|
+
await fs18.remove(manifestPath2);
|
|
3873
3898
|
}
|
|
3874
|
-
if (
|
|
3875
|
-
const remaining = await
|
|
3876
|
-
if (remaining.length === 0) await
|
|
3899
|
+
if (fs18.pathExistsSync(hausDir)) {
|
|
3900
|
+
const remaining = await fs18.readdir(hausDir);
|
|
3901
|
+
if (remaining.length === 0) await fs18.remove(hausDir);
|
|
3877
3902
|
}
|
|
3878
3903
|
return result;
|
|
3879
3904
|
}
|
|
@@ -3890,13 +3915,6 @@ function printUninstallResult(result) {
|
|
|
3890
3915
|
log("Haus hook entries removed from ~/.claude/settings.json");
|
|
3891
3916
|
}
|
|
3892
3917
|
}
|
|
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
3918
|
|
|
3901
3919
|
// src/commands/uninstall.ts
|
|
3902
3920
|
async function runUninstallCommand(options) {
|
|
@@ -3911,7 +3929,7 @@ async function runUninstallCommand(options) {
|
|
|
3911
3929
|
}
|
|
3912
3930
|
|
|
3913
3931
|
// src/commands/update.ts
|
|
3914
|
-
import
|
|
3932
|
+
import path26 from "path";
|
|
3915
3933
|
|
|
3916
3934
|
// src/update/diff-generated-files.ts
|
|
3917
3935
|
function diffGeneratedFiles() {
|
|
@@ -3938,7 +3956,7 @@ function summarizeLockDiff(before, after) {
|
|
|
3938
3956
|
|
|
3939
3957
|
// src/update/lockfile.ts
|
|
3940
3958
|
import { mkdir, readFile as readFile3, copyFile } from "fs/promises";
|
|
3941
|
-
import
|
|
3959
|
+
import path25 from "path";
|
|
3942
3960
|
async function checkLock(root) {
|
|
3943
3961
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
3944
3962
|
const hasValidVersions = lock.every(
|
|
@@ -3969,7 +3987,7 @@ async function applyLock(root) {
|
|
|
3969
3987
|
try {
|
|
3970
3988
|
const backupDir = hausPath(root, "backups");
|
|
3971
3989
|
await mkdir(backupDir, { recursive: true });
|
|
3972
|
-
await copyFile(lockPath,
|
|
3990
|
+
await copyFile(lockPath, path25.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
3973
3991
|
} catch {
|
|
3974
3992
|
}
|
|
3975
3993
|
const enriched = await Promise.all(
|
|
@@ -3991,7 +4009,7 @@ function diffLock(before, after) {
|
|
|
3991
4009
|
}
|
|
3992
4010
|
async function hasLocalOverrides(root) {
|
|
3993
4011
|
try {
|
|
3994
|
-
await readFile3(
|
|
4012
|
+
await readFile3(path25.join(root, ".claude", "settings.json"), "utf8");
|
|
3995
4013
|
return true;
|
|
3996
4014
|
} catch {
|
|
3997
4015
|
return false;
|
|
@@ -4003,7 +4021,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
|
4003
4021
|
async function runUpdate(options) {
|
|
4004
4022
|
const root = process.cwd();
|
|
4005
4023
|
if (options.check) {
|
|
4006
|
-
const pkgJson2 = await readJson(
|
|
4024
|
+
const pkgJson2 = await readJson(path26.join(packageRoot(), "package.json"));
|
|
4007
4025
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
4008
4026
|
const [status, npmVersion, latestCatalogTag, globalInstallDrift] = await Promise.all([
|
|
4009
4027
|
checkLock(root),
|
|
@@ -4033,7 +4051,7 @@ async function runUpdate(options) {
|
|
|
4033
4051
|
if (status.driftCount > 0) process.exitCode = 1;
|
|
4034
4052
|
return;
|
|
4035
4053
|
}
|
|
4036
|
-
const pkgJson = await readJson(
|
|
4054
|
+
const pkgJson = await readJson(path26.join(packageRoot(), "package.json"));
|
|
4037
4055
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
4038
4056
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
4039
4057
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -4110,8 +4128,8 @@ async function detectGlobalInstallDrift() {
|
|
|
4110
4128
|
}
|
|
4111
4129
|
|
|
4112
4130
|
// src/commands/validate-catalog.ts
|
|
4113
|
-
import
|
|
4114
|
-
import
|
|
4131
|
+
import fs19 from "fs";
|
|
4132
|
+
import path27 from "path";
|
|
4115
4133
|
|
|
4116
4134
|
// src/catalog/forbidden-content.ts
|
|
4117
4135
|
var PROSE_FORBIDDEN_TAGS = FORBIDDEN_TAGS.filter((t) => t.toLowerCase() !== "go");
|
|
@@ -4129,13 +4147,14 @@ function extractUseWhenSection(text) {
|
|
|
4129
4147
|
function isYamlBlockScalarHeader(rest) {
|
|
4130
4148
|
return /^[>|][-+]?(\d+)?(?:\s+#.*)?$/.test(rest);
|
|
4131
4149
|
}
|
|
4132
|
-
function
|
|
4150
|
+
function extractFrontmatterValue(text, key) {
|
|
4133
4151
|
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
4134
4152
|
if (!m) return "";
|
|
4135
4153
|
const lines = m[1].split(/\r?\n/);
|
|
4136
|
-
const
|
|
4154
|
+
const keyRe = new RegExp(`^${escapeRegExp(key)}:[ \\t]*`);
|
|
4155
|
+
const idx = lines.findIndex((l) => keyRe.test(l));
|
|
4137
4156
|
if (idx < 0) return "";
|
|
4138
|
-
const rest = lines[idx].replace(
|
|
4157
|
+
const rest = lines[idx].replace(keyRe, "").trim();
|
|
4139
4158
|
if (!rest) return "";
|
|
4140
4159
|
if (!isYamlBlockScalarHeader(rest)) {
|
|
4141
4160
|
return rest.replace(/^["']|["']$/g, "").trim();
|
|
@@ -4150,6 +4169,9 @@ function extractFrontmatterDescription(text) {
|
|
|
4150
4169
|
}
|
|
4151
4170
|
return body.join(" ").replace(/\s+/g, " ").trim();
|
|
4152
4171
|
}
|
|
4172
|
+
function extractFrontmatterDescription(text) {
|
|
4173
|
+
return extractFrontmatterValue(text, "description");
|
|
4174
|
+
}
|
|
4153
4175
|
function auditForbiddenTagsInText(text, label) {
|
|
4154
4176
|
const body = `${extractFrontmatterDescription(text)}
|
|
4155
4177
|
${extractUseWhenSection(text)}`;
|
|
@@ -4226,33 +4248,37 @@ function auditManifestStructure(items) {
|
|
|
4226
4248
|
}
|
|
4227
4249
|
return failures;
|
|
4228
4250
|
}
|
|
4251
|
+
function checkRequiredFrontmatter(text, label) {
|
|
4252
|
+
const failures = [];
|
|
4253
|
+
for (const key of REQUIRED_SKILL_FRONTMATTER) {
|
|
4254
|
+
if (!extractFrontmatterValue(text, key)) {
|
|
4255
|
+
failures.push(`${label}: missing non-empty frontmatter '${key}:'`);
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
return failures;
|
|
4259
|
+
}
|
|
4229
4260
|
function auditShippedFiles(manifestDir, items) {
|
|
4230
4261
|
const failures = [];
|
|
4231
4262
|
for (const item of items) {
|
|
4232
4263
|
if (!item.path) continue;
|
|
4233
|
-
const absPath =
|
|
4264
|
+
const absPath = path27.join(manifestDir, item.path);
|
|
4234
4265
|
if (item.type === "skill") {
|
|
4235
|
-
const skillMd =
|
|
4236
|
-
if (!
|
|
4237
|
-
failures.push(`${item.id}: missing ${
|
|
4266
|
+
const skillMd = path27.join(absPath, "SKILL.md");
|
|
4267
|
+
if (!fs19.existsSync(skillMd)) {
|
|
4268
|
+
failures.push(`${item.id}: missing ${path27.relative(manifestDir, skillMd)}`);
|
|
4238
4269
|
continue;
|
|
4239
4270
|
}
|
|
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
|
-
}
|
|
4271
|
+
const text = fs19.readFileSync(skillMd, "utf8");
|
|
4272
|
+
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
4247
4273
|
failures.push(
|
|
4248
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
4274
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path27.relative(manifestDir, skillMd)}`)
|
|
4249
4275
|
);
|
|
4250
4276
|
} else if (item.type === "agent") {
|
|
4251
|
-
if (!
|
|
4277
|
+
if (!fs19.existsSync(absPath)) {
|
|
4252
4278
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
4253
4279
|
continue;
|
|
4254
4280
|
}
|
|
4255
|
-
const text =
|
|
4281
|
+
const text = fs19.readFileSync(absPath, "utf8");
|
|
4256
4282
|
if (!text.startsWith("---")) failures.push(`${item.id}: agent file missing YAML frontmatter`);
|
|
4257
4283
|
for (const section of REQUIRED_AGENT_SECTIONS) {
|
|
4258
4284
|
if (!text.includes(section)) failures.push(`${item.id}: agent file missing ${section}`);
|
|
@@ -4263,27 +4289,30 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4263
4289
|
failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
|
|
4264
4290
|
}
|
|
4265
4291
|
failures.push(
|
|
4266
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
4292
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path27.relative(manifestDir, absPath)}`)
|
|
4267
4293
|
);
|
|
4268
4294
|
} else if (item.type === "template") {
|
|
4269
|
-
if (!
|
|
4295
|
+
if (!fs19.existsSync(absPath)) {
|
|
4270
4296
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
4271
4297
|
continue;
|
|
4272
4298
|
}
|
|
4273
4299
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
4274
4300
|
} else if (item.type === "command") {
|
|
4275
|
-
if (!
|
|
4301
|
+
if (!fs19.existsSync(absPath)) {
|
|
4276
4302
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
4277
4303
|
continue;
|
|
4278
4304
|
}
|
|
4305
|
+
const text = fs19.readFileSync(absPath, "utf8");
|
|
4306
|
+
const rel = path27.relative(manifestDir, absPath);
|
|
4307
|
+
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
4279
4308
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
4280
4309
|
}
|
|
4281
4310
|
}
|
|
4282
4311
|
return failures;
|
|
4283
4312
|
}
|
|
4284
4313
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
4285
|
-
const rel =
|
|
4286
|
-
const text =
|
|
4314
|
+
const rel = path27.relative(manifestDir, absPath);
|
|
4315
|
+
const text = fs19.readFileSync(absPath, "utf8");
|
|
4287
4316
|
const failures = [];
|
|
4288
4317
|
const lines = text.split(/\r?\n/);
|
|
4289
4318
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -4305,11 +4334,11 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4305
4334
|
const failures = [];
|
|
4306
4335
|
const dirs = ["skills", "agents", "templates", "commands"];
|
|
4307
4336
|
for (const dir of dirs) {
|
|
4308
|
-
const abs =
|
|
4309
|
-
if (!
|
|
4337
|
+
const abs = path27.join(manifestDir, dir);
|
|
4338
|
+
if (!fs19.existsSync(abs)) continue;
|
|
4310
4339
|
walkMd(abs, (file) => {
|
|
4311
|
-
const text =
|
|
4312
|
-
const rel =
|
|
4340
|
+
const text = fs19.readFileSync(file, "utf8");
|
|
4341
|
+
const rel = path27.relative(manifestDir, file);
|
|
4313
4342
|
const lines = text.split(/\r?\n/);
|
|
4314
4343
|
for (let i = 0; i < lines.length; i++) {
|
|
4315
4344
|
const line2 = lines[i] ?? "";
|
|
@@ -4328,8 +4357,8 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4328
4357
|
return failures;
|
|
4329
4358
|
}
|
|
4330
4359
|
function walkMd(dir, fn) {
|
|
4331
|
-
for (const entry of
|
|
4332
|
-
const full =
|
|
4360
|
+
for (const entry of fs19.readdirSync(dir, { withFileTypes: true })) {
|
|
4361
|
+
const full = path27.join(dir, entry.name);
|
|
4333
4362
|
if (entry.isDirectory()) walkMd(full, fn);
|
|
4334
4363
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
4335
4364
|
}
|
|
@@ -4340,8 +4369,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
4340
4369
|
process.exitCode = 1;
|
|
4341
4370
|
return;
|
|
4342
4371
|
}
|
|
4343
|
-
const abs =
|
|
4344
|
-
const manifestDir =
|
|
4372
|
+
const abs = path27.resolve(process.cwd(), manifestPath2);
|
|
4373
|
+
const manifestDir = path27.dirname(abs);
|
|
4345
4374
|
const data = await readJson(abs);
|
|
4346
4375
|
if (!data?.items) {
|
|
4347
4376
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -4370,8 +4399,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
4370
4399
|
}
|
|
4371
4400
|
|
|
4372
4401
|
// src/commands/workspace.ts
|
|
4373
|
-
import { existsSync as
|
|
4374
|
-
import
|
|
4402
|
+
import { existsSync as existsSync5, statSync as statSync2 } from "fs";
|
|
4403
|
+
import path34 from "path";
|
|
4375
4404
|
|
|
4376
4405
|
// src/commands/workspace/aggregate.ts
|
|
4377
4406
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -4425,7 +4454,7 @@ ${summaries.map(
|
|
|
4425
4454
|
}
|
|
4426
4455
|
|
|
4427
4456
|
// src/commands/workspace/config.ts
|
|
4428
|
-
import
|
|
4457
|
+
import path28 from "path";
|
|
4429
4458
|
import YAML from "yaml";
|
|
4430
4459
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
4431
4460
|
function parseWorkspaceConfig(text) {
|
|
@@ -4448,11 +4477,11 @@ function parseWorkspaceConfig(text) {
|
|
|
4448
4477
|
};
|
|
4449
4478
|
}
|
|
4450
4479
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
4451
|
-
return parseWorkspaceConfig(await readText(
|
|
4480
|
+
return parseWorkspaceConfig(await readText(path28.join(workspaceRoot, WORKSPACE_FILE)));
|
|
4452
4481
|
}
|
|
4453
4482
|
|
|
4454
4483
|
// src/commands/workspace/discover.ts
|
|
4455
|
-
import
|
|
4484
|
+
import path29 from "path";
|
|
4456
4485
|
import fg3 from "fast-glob";
|
|
4457
4486
|
import YAML2 from "yaml";
|
|
4458
4487
|
var DEFAULT_MAX_DEPTH = 3;
|
|
@@ -4480,8 +4509,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
4480
4509
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
4481
4510
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
4482
4511
|
for (const match of matches) {
|
|
4483
|
-
const base =
|
|
4484
|
-
const dir =
|
|
4512
|
+
const base = path29.posix.basename(match);
|
|
4513
|
+
const dir = path29.posix.dirname(match);
|
|
4485
4514
|
const owner = dir === "." ? "." : dir;
|
|
4486
4515
|
if (base === ".git") gitDirs.add(owner);
|
|
4487
4516
|
else manifestDirs.add(owner);
|
|
@@ -4497,9 +4526,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
4497
4526
|
}
|
|
4498
4527
|
repoRoots.sort((a, b) => a.localeCompare(b));
|
|
4499
4528
|
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
4500
|
-
const absDir =
|
|
4501
|
-
const pkg = await readJson(
|
|
4502
|
-
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name :
|
|
4529
|
+
const absDir = path29.resolve(workspaceRoot, relDir);
|
|
4530
|
+
const pkg = await readJson(path29.join(absDir, "package.json"));
|
|
4531
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path29.basename(relDir === "." ? workspaceRoot : absDir);
|
|
4503
4532
|
let role = "auto";
|
|
4504
4533
|
try {
|
|
4505
4534
|
const scan = await scanProject(absDir, "fast");
|
|
@@ -4542,7 +4571,7 @@ function renderWorkspaceYaml(config2) {
|
|
|
4542
4571
|
});
|
|
4543
4572
|
}
|
|
4544
4573
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
4545
|
-
const yamlPath =
|
|
4574
|
+
const yamlPath = path29.join(workspaceRoot, "haus.workspace.yaml");
|
|
4546
4575
|
const existingText = await readText(yamlPath);
|
|
4547
4576
|
const existing = parseWorkspaceConfig(existingText);
|
|
4548
4577
|
if (existingText && !existing) {
|
|
@@ -4575,19 +4604,19 @@ async function runDiscover(workspaceRoot, opts = {}) {
|
|
|
4575
4604
|
}
|
|
4576
4605
|
|
|
4577
4606
|
// src/commands/workspace/doctor.ts
|
|
4578
|
-
import { existsSync as
|
|
4579
|
-
import
|
|
4607
|
+
import { existsSync as existsSync3 } from "fs";
|
|
4608
|
+
import path31 from "path";
|
|
4580
4609
|
|
|
4581
4610
|
// src/commands/workspace/manifest.ts
|
|
4582
4611
|
import { readFileSync as readFileSync3 } from "fs";
|
|
4583
|
-
import
|
|
4612
|
+
import path30 from "path";
|
|
4584
4613
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
4585
4614
|
function manifestPath(workspaceRoot) {
|
|
4586
4615
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
4587
4616
|
}
|
|
4588
4617
|
function hausVersion() {
|
|
4589
4618
|
try {
|
|
4590
|
-
const pkg = JSON.parse(readFileSync3(
|
|
4619
|
+
const pkg = JSON.parse(readFileSync3(path30.join(packageRoot(), "package.json"), "utf8"));
|
|
4591
4620
|
return pkg.version ?? "0.0.0";
|
|
4592
4621
|
} catch {
|
|
4593
4622
|
return "0.0.0";
|
|
@@ -4653,7 +4682,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
4653
4682
|
}
|
|
4654
4683
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
4655
4684
|
for (const repo of config2.repos) {
|
|
4656
|
-
const repoRoot =
|
|
4685
|
+
const repoRoot = path31.resolve(workspaceRoot, repo.path);
|
|
4657
4686
|
const entry = manifestByName.get(repo.name);
|
|
4658
4687
|
if (!entry) {
|
|
4659
4688
|
flag({
|
|
@@ -4678,7 +4707,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
4678
4707
|
detail: `Set up at haus ${entry.hausVersionAtSetup}, current is ${currentVersion} \u2014 re-run setup.`
|
|
4679
4708
|
});
|
|
4680
4709
|
}
|
|
4681
|
-
if (!
|
|
4710
|
+
if (!existsSync3(claudePath(repoRoot))) {
|
|
4682
4711
|
flag({
|
|
4683
4712
|
repo: repo.name,
|
|
4684
4713
|
kind: "missing-claude",
|
|
@@ -4686,7 +4715,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
4686
4715
|
});
|
|
4687
4716
|
}
|
|
4688
4717
|
const lock = await checkLock(repoRoot);
|
|
4689
|
-
if (!
|
|
4718
|
+
if (!existsSync3(hausPath(repoRoot, "haus.lock.json"))) {
|
|
4690
4719
|
flag({
|
|
4691
4720
|
repo: repo.name,
|
|
4692
4721
|
kind: "missing-lock",
|
|
@@ -4726,12 +4755,12 @@ function emit(args) {
|
|
|
4726
4755
|
}
|
|
4727
4756
|
|
|
4728
4757
|
// src/commands/workspace/setup.ts
|
|
4729
|
-
import { existsSync as
|
|
4730
|
-
import
|
|
4758
|
+
import { existsSync as existsSync4, statSync } from "fs";
|
|
4759
|
+
import path33 from "path";
|
|
4731
4760
|
|
|
4732
4761
|
// src/claude/write-workspace-claude-md.ts
|
|
4733
|
-
import
|
|
4734
|
-
import
|
|
4762
|
+
import path32 from "path";
|
|
4763
|
+
import fs20 from "fs-extra";
|
|
4735
4764
|
function buildWorkspaceImportBlock(client, members) {
|
|
4736
4765
|
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
4737
4766
|
const body = [
|
|
@@ -4749,8 +4778,8 @@ ${BLOCK_END}`;
|
|
|
4749
4778
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
4750
4779
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
4751
4780
|
const dryRun = opts.dryRun ?? false;
|
|
4752
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
4753
|
-
const prev = await
|
|
4781
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path32.join(workspaceRoot, "CLAUDE.md");
|
|
4782
|
+
const prev = await fs20.pathExists(filePath) ? await fs20.readFile(filePath, "utf8") : "";
|
|
4754
4783
|
const next = opts.collision ? `${block}
|
|
4755
4784
|
` : injectHausBlock(prev, block);
|
|
4756
4785
|
const printable = displayPath(workspaceRoot, filePath);
|
|
@@ -4775,21 +4804,21 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
4775
4804
|
|
|
4776
4805
|
// src/commands/workspace/setup.ts
|
|
4777
4806
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
4778
|
-
let dir =
|
|
4807
|
+
let dir = path33.resolve(start);
|
|
4779
4808
|
for (; ; ) {
|
|
4780
|
-
if (
|
|
4781
|
-
const parent =
|
|
4782
|
-
if (parent === dir) return
|
|
4809
|
+
if (existsSync4(path33.join(dir, WORKSPACE_FILE))) return dir;
|
|
4810
|
+
const parent = path33.dirname(dir);
|
|
4811
|
+
if (parent === dir) return path33.resolve(start);
|
|
4783
4812
|
dir = parent;
|
|
4784
4813
|
}
|
|
4785
4814
|
}
|
|
4786
4815
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
4787
|
-
return
|
|
4816
|
+
return path33.resolve(workspaceRoot, repoPath) === path33.resolve(workspaceRoot);
|
|
4788
4817
|
}
|
|
4789
4818
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
4790
4819
|
const mode = options.mode ?? "fast";
|
|
4791
4820
|
const apply = options.write ?? false;
|
|
4792
|
-
const configText = await readText(
|
|
4821
|
+
const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
|
|
4793
4822
|
if (!configText) {
|
|
4794
4823
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
4795
4824
|
process.exitCode = 1;
|
|
@@ -4806,11 +4835,11 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
4806
4835
|
const statuses = [];
|
|
4807
4836
|
const aggregateInputs = [];
|
|
4808
4837
|
for (const repo of repos) {
|
|
4809
|
-
const repoRoot =
|
|
4838
|
+
const repoRoot = path33.resolve(workspaceRoot, repo.path);
|
|
4810
4839
|
log(`
|
|
4811
4840
|
\u2192 ${repo.name} (${repo.path})`);
|
|
4812
4841
|
try {
|
|
4813
|
-
if (!
|
|
4842
|
+
if (!existsSync4(repoRoot) || !statSync(repoRoot).isDirectory()) {
|
|
4814
4843
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
4815
4844
|
}
|
|
4816
4845
|
const res = await runSetupCore(repoRoot, {
|
|
@@ -4864,7 +4893,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
4864
4893
|
if (apply && !options.dryRun) {
|
|
4865
4894
|
const statusByName = new Map(statuses.map((s) => [s.name, s]));
|
|
4866
4895
|
const prior = await readManifest2(workspaceRoot);
|
|
4867
|
-
if (!prior &&
|
|
4896
|
+
if (!prior && existsSync4(manifestPath(workspaceRoot))) {
|
|
4868
4897
|
warn(
|
|
4869
4898
|
"Existing workspace.manifest.json is unreadable \u2014 prior per-repo state will not be carried forward."
|
|
4870
4899
|
);
|
|
@@ -4875,7 +4904,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
4875
4904
|
const status = statusByName.get(repo.name);
|
|
4876
4905
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
4877
4906
|
if (status?.status === "ok") {
|
|
4878
|
-
const lock = await checkLock(
|
|
4907
|
+
const lock = await checkLock(path33.resolve(workspaceRoot, repo.path));
|
|
4879
4908
|
manifestRepos.push({
|
|
4880
4909
|
name: repo.name,
|
|
4881
4910
|
path: repo.path,
|
|
@@ -4955,7 +4984,7 @@ relationships: []
|
|
|
4955
4984
|
log("Workspace initialized.");
|
|
4956
4985
|
}
|
|
4957
4986
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
4958
|
-
const configText = await readText(
|
|
4987
|
+
const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
|
|
4959
4988
|
if (!configText) {
|
|
4960
4989
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
4961
4990
|
process.exitCode = 1;
|
|
@@ -4976,8 +5005,8 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
4976
5005
|
}
|
|
4977
5006
|
const inputs = [];
|
|
4978
5007
|
for (const repo of config2.repos) {
|
|
4979
|
-
const repoRoot =
|
|
4980
|
-
if (!
|
|
5008
|
+
const repoRoot = path34.resolve(workspaceRoot, repo.path);
|
|
5009
|
+
if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
|
|
4981
5010
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
4982
5011
|
}
|
|
4983
5012
|
const result = await scanProject(repoRoot, "fast");
|
|
@@ -5027,7 +5056,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
5027
5056
|
// src/cli.ts
|
|
5028
5057
|
function cliVersion() {
|
|
5029
5058
|
try {
|
|
5030
|
-
const pkgPath =
|
|
5059
|
+
const pkgPath = path35.join(packageRoot(), "package.json");
|
|
5031
5060
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5032
5061
|
return pkg.version ?? "0.0.0";
|
|
5033
5062
|
} catch {
|
|
@@ -5037,7 +5066,7 @@ function cliVersion() {
|
|
|
5037
5066
|
var program = new Command();
|
|
5038
5067
|
function validateRuntimeNodeVersion() {
|
|
5039
5068
|
try {
|
|
5040
|
-
const pkgPath =
|
|
5069
|
+
const pkgPath = path35.join(packageRoot(), "package.json");
|
|
5041
5070
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5042
5071
|
const requiredRange = pkg.engines?.node;
|
|
5043
5072
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -5066,6 +5095,9 @@ program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo)
|
|
|
5066
5095
|
program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
|
|
5067
5096
|
program.command("context").option("--task <task>").option("--from-hook").option("--json").option("--verbose").action(runContext);
|
|
5068
5097
|
program.command("init").option("--json").action(runInit);
|
|
5098
|
+
program.command("clone").description("Clone a single git repository by URL").argument("<url>", "Git URL to clone").argument("[dir]", "Target directory (default: repo name derived from the URL)").option("--dry-run", "Print what would happen without cloning").action(
|
|
5099
|
+
(url, dir, opts) => runClone(url, { dir, dryRun: opts.dryRun })
|
|
5100
|
+
);
|
|
5069
5101
|
program.command("refresh").action(runRefresh);
|
|
5070
5102
|
program.command("catalog-audit").action(runCatalogAudit);
|
|
5071
5103
|
program.command("validate-catalog").argument("[manifest]").action(runValidateCatalog);
|