@haus-tech/haus-workflow 0.13.1 → 0.14.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 +15 -0
- package/dist/cli.js +967 -359
- package/library/catalog/manifest.json +1 -1
- package/package.json +5 -1
- package/tests/fixtures/catalog/policy-gates-manifest.json +120 -0
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import
|
|
4
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
5
|
+
import path34 from "path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/commands/apply.ts
|
|
@@ -242,6 +242,16 @@ async function listFiles(root, patterns) {
|
|
|
242
242
|
function hashText(value) {
|
|
243
243
|
return `sha256-${crypto.createHash("sha256").update(value).digest("hex")}`;
|
|
244
244
|
}
|
|
245
|
+
async function mapWithConcurrency(items, fn, concurrency = 24) {
|
|
246
|
+
const size = Number.isFinite(concurrency) ? Math.max(1, Math.floor(concurrency)) : 24;
|
|
247
|
+
const results = new Array(items.length);
|
|
248
|
+
for (let i = 0; i < items.length; i += size) {
|
|
249
|
+
const batch = items.slice(i, i + size);
|
|
250
|
+
const settled = await Promise.all(batch.map((item, j) => fn(item, i + j)));
|
|
251
|
+
for (let j = 0; j < settled.length; j += 1) results[i + j] = settled[j];
|
|
252
|
+
}
|
|
253
|
+
return results;
|
|
254
|
+
}
|
|
245
255
|
|
|
246
256
|
// src/update/hash-installed.ts
|
|
247
257
|
var EMPTY_LOCK_PATHS_TOKEN = "haus-lock:empty-paths";
|
|
@@ -439,8 +449,8 @@ function buildDenyRules() {
|
|
|
439
449
|
for (const command of DANGEROUS_COMMANDS) {
|
|
440
450
|
rules.push(`Bash(${command}:*)`);
|
|
441
451
|
}
|
|
442
|
-
for (const
|
|
443
|
-
const pattern = SENSITIVE_DIRS.has(
|
|
452
|
+
for (const path35 of SENSITIVE_PATHS) {
|
|
453
|
+
const pattern = SENSITIVE_DIRS.has(path35) ? `${path35}/**` : path35;
|
|
444
454
|
for (const tool of FILE_TOOLS) {
|
|
445
455
|
rules.push(`${tool}(${pattern})`);
|
|
446
456
|
}
|
|
@@ -804,7 +814,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
804
814
|
estimatedTokenReductionPct: 0
|
|
805
815
|
};
|
|
806
816
|
const pkgRoot = packageRoot();
|
|
807
|
-
const
|
|
817
|
+
const hausVersion2 = (await readJson(path9.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
|
|
808
818
|
const coreFiles = [
|
|
809
819
|
claudePath(root, "settings.json"),
|
|
810
820
|
claudePath(root, "rules", "haus.md"),
|
|
@@ -813,7 +823,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
813
823
|
claudePath(root, "commands", "haus-review.md")
|
|
814
824
|
];
|
|
815
825
|
const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun);
|
|
816
|
-
const workflowPath = await writeWorkflow(root,
|
|
826
|
+
const workflowPath = await writeWorkflow(root, hausVersion2, dryRun);
|
|
817
827
|
const workflowConfigPath = await writeWorkflowConfig(root, dryRun, {
|
|
818
828
|
refill: opts.refillConfig
|
|
819
829
|
});
|
|
@@ -860,9 +870,9 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
860
870
|
dryRun
|
|
861
871
|
);
|
|
862
872
|
const fixtureManifestPath = process.env["HAUS_FIXTURE_CATALOG"];
|
|
863
|
-
const
|
|
864
|
-
const manifestDir = path9.dirname(
|
|
865
|
-
const manifest = await readJson(
|
|
873
|
+
const manifestPath2 = fixtureManifestPath ?? path9.join(pkgRoot, "library", "catalog", "manifest.json");
|
|
874
|
+
const manifestDir = path9.dirname(manifestPath2);
|
|
875
|
+
const manifest = await readJson(manifestPath2) ?? { items: [] };
|
|
866
876
|
const manifestById = new Map((manifest.items ?? []).map((item) => [item.id, item]));
|
|
867
877
|
const cacheManifest = await readJson(
|
|
868
878
|
path9.join(CACHE_DIR, "manifest.json")
|
|
@@ -933,7 +943,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
933
943
|
id: r.id,
|
|
934
944
|
type: r.type,
|
|
935
945
|
source: isCurated ? "curated" : "haus",
|
|
936
|
-
version:
|
|
946
|
+
version: hausVersion2,
|
|
937
947
|
catalogRef: CATALOG_REF,
|
|
938
948
|
hash: await hashInstalledPaths(root, relPaths),
|
|
939
949
|
installMode: "copied",
|
|
@@ -1069,32 +1079,202 @@ async function loadCatalog(root) {
|
|
|
1069
1079
|
return data?.items ?? [];
|
|
1070
1080
|
}
|
|
1071
1081
|
|
|
1082
|
+
// library/catalog/validation-rules.json
|
|
1083
|
+
var validation_rules_default = {
|
|
1084
|
+
forbiddenTags: [
|
|
1085
|
+
"python",
|
|
1086
|
+
"django",
|
|
1087
|
+
"go",
|
|
1088
|
+
"rust",
|
|
1089
|
+
"java",
|
|
1090
|
+
"spring",
|
|
1091
|
+
"kotlin",
|
|
1092
|
+
"swift",
|
|
1093
|
+
"android",
|
|
1094
|
+
"flutter",
|
|
1095
|
+
"dart",
|
|
1096
|
+
"c++",
|
|
1097
|
+
"perl",
|
|
1098
|
+
"defi",
|
|
1099
|
+
"trading"
|
|
1100
|
+
],
|
|
1101
|
+
bannedAgentPhrases: ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"],
|
|
1102
|
+
requiredSkillSections: ["## Use when", "## Do not use when"],
|
|
1103
|
+
requiredAgentSections: ["## Use when", "## Do not use when", "## Verification"],
|
|
1104
|
+
riskyInstallPatterns: [
|
|
1105
|
+
{ source: "\\bnpx\\s+-y\\b", flags: "i" },
|
|
1106
|
+
{ source: "\\bnpx\\s+--yes\\b", flags: "i" },
|
|
1107
|
+
{ source: "\\byarn\\s+dlx\\b", flags: "i" },
|
|
1108
|
+
{ source: "\\bpnpm\\s+dlx\\b", flags: "i" }
|
|
1109
|
+
],
|
|
1110
|
+
allowedNpxPattern: { source: "\\bnpx\\s+tsx\\b", flags: "i" },
|
|
1111
|
+
anyNpxPattern: { source: "\\bnpx\\s+\\S+", flags: "i" },
|
|
1112
|
+
httpUrlPattern: { source: "^http:\\/\\/", flags: "i" },
|
|
1113
|
+
placeholderPattern: { source: "\\bTODO\\b|\\bPLACEHOLDER\\b", flags: "i" },
|
|
1114
|
+
allowedStacks: [
|
|
1115
|
+
"haus",
|
|
1116
|
+
"security",
|
|
1117
|
+
"quality",
|
|
1118
|
+
"frontend",
|
|
1119
|
+
"backend",
|
|
1120
|
+
"testing",
|
|
1121
|
+
"review",
|
|
1122
|
+
"workflow",
|
|
1123
|
+
"reference-pack",
|
|
1124
|
+
"core-skill",
|
|
1125
|
+
"workflow-skill",
|
|
1126
|
+
"stack-skill",
|
|
1127
|
+
"review-skill",
|
|
1128
|
+
"agent",
|
|
1129
|
+
"hook",
|
|
1130
|
+
"rule",
|
|
1131
|
+
"react",
|
|
1132
|
+
"typescript",
|
|
1133
|
+
"php",
|
|
1134
|
+
"csharp",
|
|
1135
|
+
"vendure",
|
|
1136
|
+
"vendure3",
|
|
1137
|
+
"nestjs",
|
|
1138
|
+
"graphql",
|
|
1139
|
+
"nx21",
|
|
1140
|
+
"turbo",
|
|
1141
|
+
"nextjs",
|
|
1142
|
+
"react19",
|
|
1143
|
+
"typescript5",
|
|
1144
|
+
"vite8",
|
|
1145
|
+
"tanstack-query",
|
|
1146
|
+
"tanstack-router",
|
|
1147
|
+
"radix",
|
|
1148
|
+
"radix-ui",
|
|
1149
|
+
"shadcn",
|
|
1150
|
+
"shadcn-ui",
|
|
1151
|
+
"tailwind",
|
|
1152
|
+
"tailwindcss",
|
|
1153
|
+
"scss",
|
|
1154
|
+
"scss-modules",
|
|
1155
|
+
"vue",
|
|
1156
|
+
"expressjs",
|
|
1157
|
+
"soup-base",
|
|
1158
|
+
"laravel",
|
|
1159
|
+
"laravel-nova",
|
|
1160
|
+
"wordpress",
|
|
1161
|
+
"bedrock",
|
|
1162
|
+
"elementor-pro",
|
|
1163
|
+
"acf-pro",
|
|
1164
|
+
"jetengine",
|
|
1165
|
+
"dotnet",
|
|
1166
|
+
"oidc",
|
|
1167
|
+
"azure-ad",
|
|
1168
|
+
"bankid",
|
|
1169
|
+
"myid",
|
|
1170
|
+
"cgi",
|
|
1171
|
+
"crypto",
|
|
1172
|
+
"collection2",
|
|
1173
|
+
"postgresql",
|
|
1174
|
+
"mariadb",
|
|
1175
|
+
"mssql",
|
|
1176
|
+
"elasticsearch",
|
|
1177
|
+
"yarn4",
|
|
1178
|
+
"pnpm89",
|
|
1179
|
+
"playwright",
|
|
1180
|
+
"testing-library",
|
|
1181
|
+
"phpunit",
|
|
1182
|
+
"storybook",
|
|
1183
|
+
"wisest",
|
|
1184
|
+
"vitest",
|
|
1185
|
+
"jest",
|
|
1186
|
+
"redis",
|
|
1187
|
+
"sanity",
|
|
1188
|
+
"strapi",
|
|
1189
|
+
"prisma",
|
|
1190
|
+
"cms",
|
|
1191
|
+
"database",
|
|
1192
|
+
"mysql",
|
|
1193
|
+
"saml2",
|
|
1194
|
+
"next-auth",
|
|
1195
|
+
"auth",
|
|
1196
|
+
"expo",
|
|
1197
|
+
"react-native",
|
|
1198
|
+
"mobile",
|
|
1199
|
+
"i18next",
|
|
1200
|
+
"i18n",
|
|
1201
|
+
"bullmq",
|
|
1202
|
+
"queue",
|
|
1203
|
+
"sentry",
|
|
1204
|
+
"observability",
|
|
1205
|
+
"tooling",
|
|
1206
|
+
"prettier",
|
|
1207
|
+
"eslint",
|
|
1208
|
+
"missing-prettier",
|
|
1209
|
+
"missing-eslint",
|
|
1210
|
+
"docker",
|
|
1211
|
+
"pm2",
|
|
1212
|
+
"deployer-php",
|
|
1213
|
+
"stripe",
|
|
1214
|
+
"qliro",
|
|
1215
|
+
"supabase",
|
|
1216
|
+
"payments"
|
|
1217
|
+
],
|
|
1218
|
+
alwaysAllowedTags: [
|
|
1219
|
+
"haus",
|
|
1220
|
+
"security",
|
|
1221
|
+
"quality",
|
|
1222
|
+
"review",
|
|
1223
|
+
"workflow",
|
|
1224
|
+
"baseline",
|
|
1225
|
+
"project-instructions"
|
|
1226
|
+
],
|
|
1227
|
+
patternTagSuffixes: ["-patterns"]
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1230
|
+
// src/catalog/validation-rules.ts
|
|
1231
|
+
var toRegExp = (r) => new RegExp(r.source, r.flags);
|
|
1232
|
+
var FORBIDDEN_TAGS = validation_rules_default.forbiddenTags;
|
|
1233
|
+
var BANNED_AGENT_PHRASES = validation_rules_default.bannedAgentPhrases;
|
|
1234
|
+
var REQUIRED_SKILL_SECTIONS = validation_rules_default.requiredSkillSections;
|
|
1235
|
+
var REQUIRED_AGENT_SECTIONS = validation_rules_default.requiredAgentSections;
|
|
1236
|
+
var RISKY_INSTALL_PATTERNS = validation_rules_default.riskyInstallPatterns.map(toRegExp);
|
|
1237
|
+
var ALLOWED_NPX_PATTERN = toRegExp(validation_rules_default.allowedNpxPattern);
|
|
1238
|
+
var ANY_NPX_PATTERN = toRegExp(validation_rules_default.anyNpxPattern);
|
|
1239
|
+
var HTTP_URL_PATTERN = toRegExp(validation_rules_default.httpUrlPattern);
|
|
1240
|
+
var PLACEHOLDER_PATTERN = toRegExp(validation_rules_default.placeholderPattern);
|
|
1241
|
+
var ALLOWED_STACKS = validation_rules_default.allowedStacks;
|
|
1242
|
+
var ALWAYS_ALLOWED_TAGS = validation_rules_default.alwaysAllowedTags;
|
|
1243
|
+
var PATTERN_TAG_SUFFIXES = validation_rules_default.patternTagSuffixes;
|
|
1244
|
+
var ALLOWED_SET = new Set([...ALLOWED_STACKS, ...ALWAYS_ALLOWED_TAGS].map((t) => t.toLowerCase()));
|
|
1245
|
+
function isTagAllowed(tag) {
|
|
1246
|
+
const lower = tag.toLowerCase();
|
|
1247
|
+
if (ALLOWED_SET.has(lower)) return true;
|
|
1248
|
+
return PATTERN_TAG_SUFFIXES.some((suffix) => lower.endsWith(suffix));
|
|
1249
|
+
}
|
|
1250
|
+
function auditDisallowedTags(items) {
|
|
1251
|
+
const failures = [];
|
|
1252
|
+
for (const item of items) {
|
|
1253
|
+
if (!item.id) continue;
|
|
1254
|
+
for (const tag of Array.isArray(item.tags) ? item.tags : []) {
|
|
1255
|
+
if (!isTagAllowed(tag)) failures.push(`${item.id}: tag not in allowlist: "${tag}"`);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return failures;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1072
1261
|
// src/commands/catalog-audit.ts
|
|
1073
|
-
|
|
1074
|
-
"python",
|
|
1075
|
-
"django",
|
|
1076
|
-
"go",
|
|
1077
|
-
"rust",
|
|
1078
|
-
"java",
|
|
1079
|
-
"spring",
|
|
1080
|
-
"kotlin",
|
|
1081
|
-
"swift",
|
|
1082
|
-
"android",
|
|
1083
|
-
"flutter",
|
|
1084
|
-
"dart",
|
|
1085
|
-
"c++",
|
|
1086
|
-
"perl",
|
|
1087
|
-
"defi",
|
|
1088
|
-
"trading"
|
|
1089
|
-
];
|
|
1090
|
-
async function runCatalogAudit() {
|
|
1091
|
-
const items = await loadCatalog(process.cwd());
|
|
1262
|
+
function auditForbiddenTags(items) {
|
|
1092
1263
|
const failures = [];
|
|
1264
|
+
const forbidden = new Set(FORBIDDEN_TAGS.map((w) => w.toLowerCase()));
|
|
1093
1265
|
for (const item of items) {
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1266
|
+
for (const tag of item.tags) {
|
|
1267
|
+
if (forbidden.has(tag.toLowerCase())) failures.push(`${item.id} has unsupported tag ${tag}`);
|
|
1268
|
+
}
|
|
1269
|
+
for (const token of item.id.toLowerCase().split(/[^a-z0-9+]+/)) {
|
|
1270
|
+
if (forbidden.has(token)) failures.push(`${item.id} has unsupported tag ${token}`);
|
|
1271
|
+
}
|
|
1097
1272
|
}
|
|
1273
|
+
return failures;
|
|
1274
|
+
}
|
|
1275
|
+
async function runCatalogAudit() {
|
|
1276
|
+
const items = await loadCatalog(process.cwd());
|
|
1277
|
+
const failures = auditForbiddenTags(items);
|
|
1098
1278
|
if (failures.length) {
|
|
1099
1279
|
failures.forEach((f) => error(f));
|
|
1100
1280
|
process.exitCode = 1;
|
|
@@ -1831,20 +2011,13 @@ async function buildContentBlob(root, files) {
|
|
|
1831
2011
|
(f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
|
|
1832
2012
|
);
|
|
1833
2013
|
const slice = candidates.slice(0, 300);
|
|
1834
|
-
const
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
} catch {
|
|
1842
|
-
return "";
|
|
1843
|
-
}
|
|
1844
|
-
})
|
|
1845
|
-
);
|
|
1846
|
-
parts.push(...batch);
|
|
1847
|
-
}
|
|
2014
|
+
const parts = await mapWithConcurrency(slice, async (rel) => {
|
|
2015
|
+
try {
|
|
2016
|
+
return await readFile(path15.join(root, rel), "utf8");
|
|
2017
|
+
} catch {
|
|
2018
|
+
return "";
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
1848
2021
|
return parts.join("\n");
|
|
1849
2022
|
}
|
|
1850
2023
|
function renderSummary(context) {
|
|
@@ -1957,10 +2130,9 @@ async function scanProject(root, mode = "fast") {
|
|
|
1957
2130
|
composer: isRecord(composer?.require) ? Object.keys(composer.require) : []
|
|
1958
2131
|
};
|
|
1959
2132
|
const scanHashes = Object.fromEntries(
|
|
1960
|
-
await
|
|
1961
|
-
safeFiles
|
|
1962
|
-
|
|
1963
|
-
)
|
|
2133
|
+
await mapWithConcurrency(
|
|
2134
|
+
safeFiles,
|
|
2135
|
+
async (f) => [f, hashText(await readFile2(path16.join(root, f), "utf8"))]
|
|
1964
2136
|
)
|
|
1965
2137
|
);
|
|
1966
2138
|
const repoSummary = renderSummary(context);
|
|
@@ -2376,6 +2548,24 @@ async function runGuard(kind, _options) {
|
|
|
2376
2548
|
import path18 from "path";
|
|
2377
2549
|
import fs12 from "fs-extra";
|
|
2378
2550
|
|
|
2551
|
+
// src/utils/prompts.ts
|
|
2552
|
+
import { stdin as input, stdout as output } from "process";
|
|
2553
|
+
import readline from "readline/promises";
|
|
2554
|
+
async function ask(question) {
|
|
2555
|
+
const rl = readline.createInterface({ input, output });
|
|
2556
|
+
try {
|
|
2557
|
+
const answer = await rl.question(`${question}
|
|
2558
|
+
> `);
|
|
2559
|
+
return answer.trim();
|
|
2560
|
+
} finally {
|
|
2561
|
+
rl.close();
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
async function confirm(question) {
|
|
2565
|
+
const answer = (await ask(`${question} [y/N]`)).toLowerCase();
|
|
2566
|
+
return answer === "y" || answer === "yes";
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2379
2569
|
// src/utils/exec.ts
|
|
2380
2570
|
import { execa } from "execa";
|
|
2381
2571
|
async function runCommand(command, args = [], options = {}) {
|
|
@@ -2660,69 +2850,16 @@ function buildStackSet(context) {
|
|
|
2660
2850
|
);
|
|
2661
2851
|
}
|
|
2662
2852
|
|
|
2663
|
-
// src/
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
rl.close();
|
|
2674
|
-
}
|
|
2675
|
-
}
|
|
2676
|
-
async function confirm(question) {
|
|
2677
|
-
const answer = (await ask(`${question} [y/N]`)).toLowerCase();
|
|
2678
|
-
return answer === "y" || answer === "yes";
|
|
2679
|
-
}
|
|
2680
|
-
|
|
2681
|
-
// src/commands/setup-project.ts
|
|
2682
|
-
var GUIDED_QUESTIONS = [
|
|
2683
|
-
"What is this project for?",
|
|
2684
|
-
"Is it for a client, internal Haus work, or experimentation?",
|
|
2685
|
-
"What should Claude help with most?",
|
|
2686
|
-
"Is this project connected to other repositories?",
|
|
2687
|
-
"Are there parts of the project Claude should avoid touching?",
|
|
2688
|
-
"Are there client-specific rules or sensitive areas?",
|
|
2689
|
-
"Do you want a minimal, standard, or strict setup?"
|
|
2690
|
-
];
|
|
2691
|
-
async function runSetupProject(options) {
|
|
2692
|
-
const root = process.cwd();
|
|
2693
|
-
let mode = options.guided ? "guided" : "fast";
|
|
2694
|
-
if (!options.guided && !options.fast && !options.json) {
|
|
2695
|
-
log("How do you want to set this project up?");
|
|
2696
|
-
log("1. Guided setup - I'll ask a few simple questions, then scan the project.");
|
|
2697
|
-
log("2. Fast setup - I'll only scan the project and recommend defaults.");
|
|
2698
|
-
const choice = await ask("Choose 1 or 2");
|
|
2699
|
-
mode = choice === "1" ? "guided" : "fast";
|
|
2700
|
-
}
|
|
2701
|
-
if (mode === "guided") {
|
|
2702
|
-
const existing = await readJson(hausPath(root, "setup-answers.json")) ?? {};
|
|
2703
|
-
const merged = {};
|
|
2704
|
-
for (const question of GUIDED_QUESTIONS) {
|
|
2705
|
-
if (options.json) {
|
|
2706
|
-
merged[question] = existing[question] ?? "pending-user-answer";
|
|
2707
|
-
continue;
|
|
2708
|
-
}
|
|
2709
|
-
const prefilled = existing[question];
|
|
2710
|
-
if (prefilled && prefilled !== "pending-user-answer" && prefilled !== "no-answer") {
|
|
2711
|
-
merged[question] = prefilled;
|
|
2712
|
-
continue;
|
|
2713
|
-
}
|
|
2714
|
-
const answer = await ask(question);
|
|
2715
|
-
merged[question] = answer || prefilled || "no-answer";
|
|
2716
|
-
}
|
|
2717
|
-
await writeJson(hausPath(root, "setup-answers.json"), merged);
|
|
2718
|
-
}
|
|
2719
|
-
const scanResult = await scanProject(root, mode);
|
|
2720
|
-
if (options.json) {
|
|
2721
|
-
log(JSON.stringify(scanResult, null, 2));
|
|
2722
|
-
} else {
|
|
2723
|
-
log("Haus scan complete");
|
|
2724
|
-
log(`Roles: ${scanResult.repoRoles.join(", ") || "unknown"}`);
|
|
2725
|
-
log(`Package manager: ${scanResult.packageManager}`);
|
|
2853
|
+
// src/commands/setup-core.ts
|
|
2854
|
+
async function runSetupCore(root, opts) {
|
|
2855
|
+
const { mode, json, apply, dryRun, confirm: confirm2 } = opts;
|
|
2856
|
+
const scanResult = await scanProject(root, mode);
|
|
2857
|
+
if (json) {
|
|
2858
|
+
log(JSON.stringify(scanResult, null, 2));
|
|
2859
|
+
} else {
|
|
2860
|
+
log("Haus scan complete");
|
|
2861
|
+
log(`Roles: ${scanResult.repoRoles.join(", ") || "unknown"}`);
|
|
2862
|
+
log(`Package manager: ${scanResult.packageManager}`);
|
|
2726
2863
|
}
|
|
2727
2864
|
const context = await readContextOrScan(root);
|
|
2728
2865
|
const recommendation = await recommend(root, context);
|
|
@@ -2733,7 +2870,7 @@ async function runSetupProject(options) {
|
|
|
2733
2870
|
{ id: "haus.rule.context-minimal", enabled: true },
|
|
2734
2871
|
{ id: "haus.rule.security", enabled: true }
|
|
2735
2872
|
]);
|
|
2736
|
-
if (
|
|
2873
|
+
if (json) {
|
|
2737
2874
|
log(JSON.stringify(recommendation, null, 2));
|
|
2738
2875
|
} else {
|
|
2739
2876
|
log("Haus recommendation ready");
|
|
@@ -2744,6 +2881,7 @@ async function runSetupProject(options) {
|
|
|
2744
2881
|
const warningLines = [.../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? []])];
|
|
2745
2882
|
log(`Repo: ${context.repoName}`);
|
|
2746
2883
|
for (const warning of warningLines) log(`- WARN: ${warning}`);
|
|
2884
|
+
const hooksOk = hooks.ok;
|
|
2747
2885
|
if (hooks.skipped) {
|
|
2748
2886
|
log(`- HOOKS: (skipped) ${hooks.message}`);
|
|
2749
2887
|
} else if (!hooks.ok) {
|
|
@@ -2752,17 +2890,29 @@ async function runSetupProject(options) {
|
|
|
2752
2890
|
} else {
|
|
2753
2891
|
log(`- HOOKS OK: ${hooks.message}`);
|
|
2754
2892
|
}
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2893
|
+
const baseResult = {
|
|
2894
|
+
root,
|
|
2895
|
+
repoName: context.repoName,
|
|
2896
|
+
roles: scanResult.repoRoles,
|
|
2897
|
+
recommendedCount: recommendation.recommended.length,
|
|
2898
|
+
warnings: warningLines,
|
|
2899
|
+
hooksOk,
|
|
2900
|
+
written: []
|
|
2901
|
+
};
|
|
2902
|
+
if (!apply) return baseResult;
|
|
2903
|
+
if (confirm2) {
|
|
2904
|
+
const approved = await confirm2();
|
|
2905
|
+
if (!approved) {
|
|
2906
|
+
log("Setup reviewed. No files written.");
|
|
2907
|
+
log("Next step: run `haus apply --write` when ready.");
|
|
2908
|
+
return baseResult;
|
|
2909
|
+
}
|
|
2761
2910
|
}
|
|
2762
|
-
const files = await writeClaudeFiles(root, false);
|
|
2911
|
+
const files = await writeClaudeFiles(root, dryRun ?? false);
|
|
2763
2912
|
log("Applied files:");
|
|
2764
2913
|
files.forEach((f) => log(`- ${displayPath(root, f)}`));
|
|
2765
2914
|
const hooksAfter = await verifyProjectSettingsHooksContract(root);
|
|
2915
|
+
const hooksOkAfter = hooksAfter.ok;
|
|
2766
2916
|
if (hooksAfter.skipped) {
|
|
2767
2917
|
log(`- HOOKS: (skipped) ${hooksAfter.message}`);
|
|
2768
2918
|
} else if (!hooksAfter.ok) {
|
|
@@ -2771,6 +2921,54 @@ async function runSetupProject(options) {
|
|
|
2771
2921
|
} else {
|
|
2772
2922
|
log(`- HOOKS OK: ${hooksAfter.message}`);
|
|
2773
2923
|
}
|
|
2924
|
+
return { ...baseResult, hooksOk: hooksOkAfter, written: files };
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
// src/commands/setup-project.ts
|
|
2928
|
+
var GUIDED_QUESTIONS = [
|
|
2929
|
+
"What is this project for?",
|
|
2930
|
+
"Is it for a client, internal Haus work, or experimentation?",
|
|
2931
|
+
"What should Claude help with most?",
|
|
2932
|
+
"Is this project connected to other repositories?",
|
|
2933
|
+
"Are there parts of the project Claude should avoid touching?",
|
|
2934
|
+
"Are there client-specific rules or sensitive areas?",
|
|
2935
|
+
"Do you want a minimal, standard, or strict setup?"
|
|
2936
|
+
];
|
|
2937
|
+
async function runSetupProject(options) {
|
|
2938
|
+
const root = process.cwd();
|
|
2939
|
+
let mode = options.guided ? "guided" : "fast";
|
|
2940
|
+
if (!options.guided && !options.fast && !options.json) {
|
|
2941
|
+
log("How do you want to set this project up?");
|
|
2942
|
+
log("1. Guided setup - I'll ask a few simple questions, then scan the project.");
|
|
2943
|
+
log("2. Fast setup - I'll only scan the project and recommend defaults.");
|
|
2944
|
+
const choice = await ask("Choose 1 or 2");
|
|
2945
|
+
mode = choice === "1" ? "guided" : "fast";
|
|
2946
|
+
}
|
|
2947
|
+
if (mode === "guided") {
|
|
2948
|
+
const existing = await readJson(hausPath(root, "setup-answers.json")) ?? {};
|
|
2949
|
+
const merged = {};
|
|
2950
|
+
for (const question of GUIDED_QUESTIONS) {
|
|
2951
|
+
if (options.json) {
|
|
2952
|
+
merged[question] = existing[question] ?? "pending-user-answer";
|
|
2953
|
+
continue;
|
|
2954
|
+
}
|
|
2955
|
+
const prefilled = existing[question];
|
|
2956
|
+
if (prefilled && prefilled !== "pending-user-answer" && prefilled !== "no-answer") {
|
|
2957
|
+
merged[question] = prefilled;
|
|
2958
|
+
continue;
|
|
2959
|
+
}
|
|
2960
|
+
const answer = await ask(question);
|
|
2961
|
+
merged[question] = answer || prefilled || "no-answer";
|
|
2962
|
+
}
|
|
2963
|
+
await writeJson(hausPath(root, "setup-answers.json"), merged);
|
|
2964
|
+
}
|
|
2965
|
+
await runSetupCore(root, {
|
|
2966
|
+
mode,
|
|
2967
|
+
json: options.json,
|
|
2968
|
+
apply: !options.json,
|
|
2969
|
+
dryRun: false,
|
|
2970
|
+
confirm: () => confirm("Approve and write Claude files now?")
|
|
2971
|
+
});
|
|
2774
2972
|
}
|
|
2775
2973
|
|
|
2776
2974
|
// src/commands/init.ts
|
|
@@ -3340,9 +3538,9 @@ async function runUninstall(options = {}) {
|
|
|
3340
3538
|
await writeSettings(stripped);
|
|
3341
3539
|
result.hooksStripped = true;
|
|
3342
3540
|
const hausDir = path23.join(globalClaudeDir(), "haus");
|
|
3343
|
-
const
|
|
3344
|
-
if (fs16.pathExistsSync(
|
|
3345
|
-
await fs16.remove(
|
|
3541
|
+
const manifestPath2 = hausManifestPath();
|
|
3542
|
+
if (fs16.pathExistsSync(manifestPath2)) {
|
|
3543
|
+
await fs16.remove(manifestPath2);
|
|
3346
3544
|
}
|
|
3347
3545
|
if (fs16.pathExistsSync(hausDir)) {
|
|
3348
3546
|
const remaining = await fs16.readdir(hausDir);
|
|
@@ -3525,187 +3723,6 @@ async function runUpdate(options) {
|
|
|
3525
3723
|
// src/commands/validate-catalog.ts
|
|
3526
3724
|
import fs17 from "fs";
|
|
3527
3725
|
import path26 from "path";
|
|
3528
|
-
|
|
3529
|
-
// library/catalog/validation-rules.json
|
|
3530
|
-
var validation_rules_default = {
|
|
3531
|
-
forbiddenTags: [
|
|
3532
|
-
"python",
|
|
3533
|
-
"django",
|
|
3534
|
-
"go",
|
|
3535
|
-
"rust",
|
|
3536
|
-
"java",
|
|
3537
|
-
"spring",
|
|
3538
|
-
"kotlin",
|
|
3539
|
-
"swift",
|
|
3540
|
-
"android",
|
|
3541
|
-
"flutter",
|
|
3542
|
-
"dart",
|
|
3543
|
-
"c++",
|
|
3544
|
-
"perl",
|
|
3545
|
-
"defi",
|
|
3546
|
-
"trading"
|
|
3547
|
-
],
|
|
3548
|
-
bannedAgentPhrases: ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"],
|
|
3549
|
-
requiredSkillSections: ["## Use when", "## Do not use when"],
|
|
3550
|
-
requiredAgentSections: ["## Use when", "## Do not use when", "## Verification"],
|
|
3551
|
-
riskyInstallPatterns: [
|
|
3552
|
-
{ source: "\\bnpx\\s+-y\\b", flags: "i" },
|
|
3553
|
-
{ source: "\\bnpx\\s+--yes\\b", flags: "i" },
|
|
3554
|
-
{ source: "\\byarn\\s+dlx\\b", flags: "i" },
|
|
3555
|
-
{ source: "\\bpnpm\\s+dlx\\b", flags: "i" }
|
|
3556
|
-
],
|
|
3557
|
-
allowedNpxPattern: { source: "\\bnpx\\s+tsx\\b", flags: "i" },
|
|
3558
|
-
anyNpxPattern: { source: "\\bnpx\\s+\\S+", flags: "i" },
|
|
3559
|
-
httpUrlPattern: { source: "^http:\\/\\/", flags: "i" },
|
|
3560
|
-
placeholderPattern: { source: "\\bTODO\\b|\\bPLACEHOLDER\\b", flags: "i" },
|
|
3561
|
-
allowedStacks: [
|
|
3562
|
-
"haus",
|
|
3563
|
-
"security",
|
|
3564
|
-
"quality",
|
|
3565
|
-
"frontend",
|
|
3566
|
-
"backend",
|
|
3567
|
-
"testing",
|
|
3568
|
-
"review",
|
|
3569
|
-
"workflow",
|
|
3570
|
-
"reference-pack",
|
|
3571
|
-
"core-skill",
|
|
3572
|
-
"workflow-skill",
|
|
3573
|
-
"stack-skill",
|
|
3574
|
-
"review-skill",
|
|
3575
|
-
"agent",
|
|
3576
|
-
"hook",
|
|
3577
|
-
"rule",
|
|
3578
|
-
"react",
|
|
3579
|
-
"typescript",
|
|
3580
|
-
"php",
|
|
3581
|
-
"csharp",
|
|
3582
|
-
"vendure",
|
|
3583
|
-
"vendure3",
|
|
3584
|
-
"nestjs",
|
|
3585
|
-
"graphql",
|
|
3586
|
-
"nx21",
|
|
3587
|
-
"turbo",
|
|
3588
|
-
"nextjs",
|
|
3589
|
-
"react19",
|
|
3590
|
-
"typescript5",
|
|
3591
|
-
"vite8",
|
|
3592
|
-
"tanstack-query",
|
|
3593
|
-
"tanstack-router",
|
|
3594
|
-
"radix",
|
|
3595
|
-
"radix-ui",
|
|
3596
|
-
"shadcn",
|
|
3597
|
-
"shadcn-ui",
|
|
3598
|
-
"tailwind",
|
|
3599
|
-
"tailwindcss",
|
|
3600
|
-
"scss",
|
|
3601
|
-
"scss-modules",
|
|
3602
|
-
"vue",
|
|
3603
|
-
"expressjs",
|
|
3604
|
-
"soup-base",
|
|
3605
|
-
"laravel",
|
|
3606
|
-
"laravel-nova",
|
|
3607
|
-
"wordpress",
|
|
3608
|
-
"bedrock",
|
|
3609
|
-
"elementor-pro",
|
|
3610
|
-
"acf-pro",
|
|
3611
|
-
"jetengine",
|
|
3612
|
-
"dotnet",
|
|
3613
|
-
"oidc",
|
|
3614
|
-
"azure-ad",
|
|
3615
|
-
"bankid",
|
|
3616
|
-
"myid",
|
|
3617
|
-
"cgi",
|
|
3618
|
-
"crypto",
|
|
3619
|
-
"collection2",
|
|
3620
|
-
"postgresql",
|
|
3621
|
-
"mariadb",
|
|
3622
|
-
"mssql",
|
|
3623
|
-
"elasticsearch",
|
|
3624
|
-
"yarn4",
|
|
3625
|
-
"pnpm89",
|
|
3626
|
-
"playwright",
|
|
3627
|
-
"testing-library",
|
|
3628
|
-
"phpunit",
|
|
3629
|
-
"storybook",
|
|
3630
|
-
"wisest",
|
|
3631
|
-
"vitest",
|
|
3632
|
-
"jest",
|
|
3633
|
-
"redis",
|
|
3634
|
-
"sanity",
|
|
3635
|
-
"strapi",
|
|
3636
|
-
"prisma",
|
|
3637
|
-
"cms",
|
|
3638
|
-
"database",
|
|
3639
|
-
"mysql",
|
|
3640
|
-
"saml2",
|
|
3641
|
-
"next-auth",
|
|
3642
|
-
"auth",
|
|
3643
|
-
"expo",
|
|
3644
|
-
"react-native",
|
|
3645
|
-
"mobile",
|
|
3646
|
-
"i18next",
|
|
3647
|
-
"i18n",
|
|
3648
|
-
"bullmq",
|
|
3649
|
-
"queue",
|
|
3650
|
-
"sentry",
|
|
3651
|
-
"observability",
|
|
3652
|
-
"tooling",
|
|
3653
|
-
"prettier",
|
|
3654
|
-
"eslint",
|
|
3655
|
-
"missing-prettier",
|
|
3656
|
-
"missing-eslint",
|
|
3657
|
-
"docker",
|
|
3658
|
-
"pm2",
|
|
3659
|
-
"deployer-php",
|
|
3660
|
-
"stripe",
|
|
3661
|
-
"qliro",
|
|
3662
|
-
"supabase",
|
|
3663
|
-
"payments"
|
|
3664
|
-
],
|
|
3665
|
-
alwaysAllowedTags: [
|
|
3666
|
-
"haus",
|
|
3667
|
-
"security",
|
|
3668
|
-
"quality",
|
|
3669
|
-
"review",
|
|
3670
|
-
"workflow",
|
|
3671
|
-
"baseline",
|
|
3672
|
-
"project-instructions"
|
|
3673
|
-
],
|
|
3674
|
-
patternTagSuffixes: ["-patterns"]
|
|
3675
|
-
};
|
|
3676
|
-
|
|
3677
|
-
// src/catalog/validation-rules.ts
|
|
3678
|
-
var toRegExp = (r) => new RegExp(r.source, r.flags);
|
|
3679
|
-
var FORBIDDEN_TAGS = validation_rules_default.forbiddenTags;
|
|
3680
|
-
var BANNED_AGENT_PHRASES = validation_rules_default.bannedAgentPhrases;
|
|
3681
|
-
var REQUIRED_SKILL_SECTIONS = validation_rules_default.requiredSkillSections;
|
|
3682
|
-
var REQUIRED_AGENT_SECTIONS = validation_rules_default.requiredAgentSections;
|
|
3683
|
-
var RISKY_INSTALL_PATTERNS = validation_rules_default.riskyInstallPatterns.map(toRegExp);
|
|
3684
|
-
var ALLOWED_NPX_PATTERN = toRegExp(validation_rules_default.allowedNpxPattern);
|
|
3685
|
-
var ANY_NPX_PATTERN = toRegExp(validation_rules_default.anyNpxPattern);
|
|
3686
|
-
var HTTP_URL_PATTERN = toRegExp(validation_rules_default.httpUrlPattern);
|
|
3687
|
-
var PLACEHOLDER_PATTERN = toRegExp(validation_rules_default.placeholderPattern);
|
|
3688
|
-
var ALLOWED_STACKS = validation_rules_default.allowedStacks;
|
|
3689
|
-
var ALWAYS_ALLOWED_TAGS = validation_rules_default.alwaysAllowedTags;
|
|
3690
|
-
var PATTERN_TAG_SUFFIXES = validation_rules_default.patternTagSuffixes;
|
|
3691
|
-
var ALLOWED_SET = new Set([...ALLOWED_STACKS, ...ALWAYS_ALLOWED_TAGS].map((t) => t.toLowerCase()));
|
|
3692
|
-
function isTagAllowed(tag) {
|
|
3693
|
-
const lower = tag.toLowerCase();
|
|
3694
|
-
if (ALLOWED_SET.has(lower)) return true;
|
|
3695
|
-
return PATTERN_TAG_SUFFIXES.some((suffix) => lower.endsWith(suffix));
|
|
3696
|
-
}
|
|
3697
|
-
function auditDisallowedTags(items) {
|
|
3698
|
-
const failures = [];
|
|
3699
|
-
for (const item of items) {
|
|
3700
|
-
if (!item.id) continue;
|
|
3701
|
-
for (const tag of Array.isArray(item.tags) ? item.tags : []) {
|
|
3702
|
-
if (!isTagAllowed(tag)) failures.push(`${item.id}: tag not in allowlist: "${tag}"`);
|
|
3703
|
-
}
|
|
3704
|
-
}
|
|
3705
|
-
return failures;
|
|
3706
|
-
}
|
|
3707
|
-
|
|
3708
|
-
// src/commands/validate-catalog.ts
|
|
3709
3726
|
function auditForbiddenStacks(items) {
|
|
3710
3727
|
const failures = [];
|
|
3711
3728
|
for (const item of items) {
|
|
@@ -3840,13 +3857,13 @@ function walkMd(dir, fn) {
|
|
|
3840
3857
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
3841
3858
|
}
|
|
3842
3859
|
}
|
|
3843
|
-
async function runValidateCatalog(
|
|
3844
|
-
if (!
|
|
3860
|
+
async function runValidateCatalog(manifestPath2) {
|
|
3861
|
+
if (!manifestPath2) {
|
|
3845
3862
|
error("Usage: haus validate-catalog <path/to/manifest.json>");
|
|
3846
3863
|
process.exitCode = 1;
|
|
3847
3864
|
return;
|
|
3848
3865
|
}
|
|
3849
|
-
const abs = path26.resolve(process.cwd(),
|
|
3866
|
+
const abs = path26.resolve(process.cwd(), manifestPath2);
|
|
3850
3867
|
const manifestDir = path26.dirname(abs);
|
|
3851
3868
|
const data = await readJson(abs);
|
|
3852
3869
|
if (!data?.items) {
|
|
@@ -3876,77 +3893,665 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3876
3893
|
}
|
|
3877
3894
|
|
|
3878
3895
|
// src/commands/workspace.ts
|
|
3896
|
+
import { existsSync as existsSync4, statSync as statSync2 } from "fs";
|
|
3897
|
+
import path33 from "path";
|
|
3898
|
+
|
|
3899
|
+
// src/commands/workspace/aggregate.ts
|
|
3900
|
+
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
3901
|
+
const summaries = repos.map((repo) => ({
|
|
3902
|
+
name: repo.name,
|
|
3903
|
+
path: repo.path,
|
|
3904
|
+
roles: repo.context.repoRoles ?? [],
|
|
3905
|
+
packageManager: repo.context.packageManager,
|
|
3906
|
+
deps: repo.context.dependencies ?? []
|
|
3907
|
+
}));
|
|
3908
|
+
const ownership = {};
|
|
3909
|
+
for (const repo of summaries) {
|
|
3910
|
+
for (const dep2 of repo.deps) {
|
|
3911
|
+
ownership[dep2] ??= [];
|
|
3912
|
+
ownership[dep2].push(repo.name);
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
const roles = [...new Set(summaries.flatMap((r) => r.roles))].sort();
|
|
3916
|
+
const crossRepoHints = [...new Set(repos.flatMap((r) => r.context.crossRepoHints ?? []))].sort();
|
|
3917
|
+
const summaryPath = hausPath(workspaceRoot, "workspace-summary.json");
|
|
3918
|
+
const ownershipPath = hausPath(workspaceRoot, "dependency-ownership-map.json");
|
|
3919
|
+
const crossRepoPath = hausPath(workspaceRoot, "cross-repo-summary.md");
|
|
3920
|
+
const contextMapPath = hausPath(workspaceRoot, "workspace-context-map.json");
|
|
3921
|
+
await writeJson(summaryPath, {
|
|
3922
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3923
|
+
repos: summaries
|
|
3924
|
+
});
|
|
3925
|
+
await writeJson(ownershipPath, ownership);
|
|
3926
|
+
await writeText(
|
|
3927
|
+
crossRepoPath,
|
|
3928
|
+
`# Cross Repo Summary
|
|
3929
|
+
|
|
3930
|
+
${summaries.map(
|
|
3931
|
+
(repo) => `- ${repo.name} (${repo.path}) roles: ${repo.roles.join(", ") || "unknown"}; package manager: ${repo.packageManager}`
|
|
3932
|
+
).join("\n")}
|
|
3933
|
+
`
|
|
3934
|
+
);
|
|
3935
|
+
await writeJson(contextMapPath, {
|
|
3936
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3937
|
+
roles,
|
|
3938
|
+
crossRepoHints,
|
|
3939
|
+
repos: summaries.map((r) => ({
|
|
3940
|
+
name: r.name,
|
|
3941
|
+
path: r.path,
|
|
3942
|
+
roles: r.roles,
|
|
3943
|
+
packageManager: r.packageManager
|
|
3944
|
+
})),
|
|
3945
|
+
relationships
|
|
3946
|
+
});
|
|
3947
|
+
return [summaryPath, ownershipPath, crossRepoPath, contextMapPath];
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
// src/commands/workspace/config.ts
|
|
3879
3951
|
import path27 from "path";
|
|
3880
3952
|
import YAML from "yaml";
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3953
|
+
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
3954
|
+
function parseWorkspaceConfig(text) {
|
|
3955
|
+
if (!text) return void 0;
|
|
3956
|
+
let parsed;
|
|
3957
|
+
try {
|
|
3958
|
+
parsed = YAML.parse(text);
|
|
3959
|
+
} catch {
|
|
3960
|
+
return void 0;
|
|
3961
|
+
}
|
|
3962
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
3963
|
+
const obj = parsed;
|
|
3964
|
+
const repos = Array.isArray(obj.repos) ? obj.repos.filter(
|
|
3965
|
+
(r) => typeof r === "object" && r !== null && typeof r.name === "string" && typeof r.path === "string"
|
|
3966
|
+
) : [];
|
|
3967
|
+
return {
|
|
3968
|
+
client: typeof obj.client === "string" ? obj.client : "unknown",
|
|
3969
|
+
repos,
|
|
3970
|
+
relationships: Array.isArray(obj.relationships) ? obj.relationships : []
|
|
3971
|
+
};
|
|
3972
|
+
}
|
|
3973
|
+
async function readWorkspaceConfig(workspaceRoot) {
|
|
3974
|
+
return parseWorkspaceConfig(await readText(path27.join(workspaceRoot, WORKSPACE_FILE)));
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
// src/commands/workspace/discover.ts
|
|
3978
|
+
import path28 from "path";
|
|
3979
|
+
import fg3 from "fast-glob";
|
|
3980
|
+
import YAML2 from "yaml";
|
|
3981
|
+
var DEFAULT_MAX_DEPTH = 3;
|
|
3982
|
+
var REPO_MARKERS = ["**/.git", "**/package.json", "**/composer.json"];
|
|
3983
|
+
var IGNORE = [
|
|
3984
|
+
"**/node_modules/**",
|
|
3985
|
+
"**/.git/**",
|
|
3986
|
+
"**/vendor/**",
|
|
3987
|
+
"**/dist/**",
|
|
3988
|
+
"**/.haus-workflow/**"
|
|
3989
|
+
];
|
|
3990
|
+
function isDescendant(child, ancestor) {
|
|
3991
|
+
if (ancestor === ".") return child !== ".";
|
|
3992
|
+
return child === ancestor ? false : child.startsWith(`${ancestor}/`);
|
|
3993
|
+
}
|
|
3994
|
+
async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
3995
|
+
const matches = await fg3(REPO_MARKERS, {
|
|
3996
|
+
cwd: workspaceRoot,
|
|
3997
|
+
dot: true,
|
|
3998
|
+
onlyFiles: false,
|
|
3999
|
+
deep: maxDepth,
|
|
4000
|
+
followSymbolicLinks: false,
|
|
4001
|
+
ignore: IGNORE
|
|
4002
|
+
});
|
|
4003
|
+
const gitDirs = /* @__PURE__ */ new Set();
|
|
4004
|
+
const manifestDirs = /* @__PURE__ */ new Set();
|
|
4005
|
+
for (const match of matches) {
|
|
4006
|
+
const base = path28.posix.basename(match);
|
|
4007
|
+
const dir = path28.posix.dirname(match);
|
|
4008
|
+
const owner = dir === "." ? "." : dir;
|
|
4009
|
+
if (base === ".git") gitDirs.add(owner);
|
|
4010
|
+
else manifestDirs.add(owner);
|
|
4011
|
+
}
|
|
4012
|
+
const repoRoots = [...gitDirs];
|
|
4013
|
+
const manifestSorted = [...manifestDirs].sort(
|
|
4014
|
+
(a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b)
|
|
4015
|
+
);
|
|
4016
|
+
for (const dir of manifestSorted) {
|
|
4017
|
+
if (gitDirs.has(dir)) continue;
|
|
4018
|
+
if (repoRoots.some((root) => isDescendant(dir, root))) continue;
|
|
4019
|
+
repoRoots.push(dir);
|
|
4020
|
+
}
|
|
4021
|
+
repoRoots.sort((a, b) => a.localeCompare(b));
|
|
4022
|
+
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
4023
|
+
const absDir = path28.resolve(workspaceRoot, relDir);
|
|
4024
|
+
const pkg = await readJson(path28.join(absDir, "package.json"));
|
|
4025
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path28.basename(relDir === "." ? workspaceRoot : absDir);
|
|
4026
|
+
let role = "auto";
|
|
4027
|
+
try {
|
|
4028
|
+
const scan = await scanProject(absDir, "fast");
|
|
4029
|
+
if (scan.repoRoles[0]) role = scan.repoRoles[0];
|
|
4030
|
+
} catch {
|
|
4031
|
+
}
|
|
4032
|
+
return { name, path: relDir === "." ? "." : relDir, role };
|
|
4033
|
+
});
|
|
4034
|
+
}
|
|
4035
|
+
function mergeWorkspaceConfig(existing, discovered, opts = {}) {
|
|
4036
|
+
const existingRepos = existing?.repos ?? [];
|
|
4037
|
+
const byPath = new Map(existingRepos.map((r) => [r.path, r]));
|
|
4038
|
+
for (const repo of discovered) {
|
|
4039
|
+
if (!byPath.has(repo.path)) {
|
|
4040
|
+
byPath.set(repo.path, { name: repo.name, path: repo.path, role: repo.role });
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
const ordered = [];
|
|
4044
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4045
|
+
for (const repo of existingRepos) {
|
|
4046
|
+
ordered.push(byPath.get(repo.path));
|
|
4047
|
+
seen.add(repo.path);
|
|
4048
|
+
}
|
|
4049
|
+
for (const repo of discovered) {
|
|
4050
|
+
if (seen.has(repo.path)) continue;
|
|
4051
|
+
ordered.push(byPath.get(repo.path));
|
|
4052
|
+
seen.add(repo.path);
|
|
4053
|
+
}
|
|
4054
|
+
return {
|
|
4055
|
+
client: opts.client ?? existing?.client ?? "unknown",
|
|
4056
|
+
repos: ordered,
|
|
4057
|
+
relationships: existing?.relationships ?? []
|
|
4058
|
+
};
|
|
4059
|
+
}
|
|
4060
|
+
function renderWorkspaceYaml(config2) {
|
|
4061
|
+
return YAML2.stringify({
|
|
4062
|
+
client: config2.client,
|
|
4063
|
+
repos: config2.repos.map((r) => ({ name: r.name, path: r.path, role: r.role ?? "auto" })),
|
|
4064
|
+
relationships: config2.relationships
|
|
4065
|
+
});
|
|
4066
|
+
}
|
|
4067
|
+
async function runDiscover(workspaceRoot, opts = {}) {
|
|
4068
|
+
const yamlPath = path28.join(workspaceRoot, "haus.workspace.yaml");
|
|
4069
|
+
const existingText = await readText(yamlPath);
|
|
4070
|
+
const existing = parseWorkspaceConfig(existingText);
|
|
4071
|
+
if (existingText && !existing) {
|
|
4072
|
+
error(
|
|
4073
|
+
"Existing haus.workspace.yaml is malformed \u2014 fix or remove it before running discover (refusing to overwrite)."
|
|
3892
4074
|
);
|
|
3893
|
-
|
|
4075
|
+
process.exitCode = 1;
|
|
3894
4076
|
return;
|
|
3895
4077
|
}
|
|
3896
|
-
const
|
|
3897
|
-
if (
|
|
3898
|
-
error("
|
|
4078
|
+
const discovered = await discoverRepos(workspaceRoot, opts.maxDepth ?? DEFAULT_MAX_DEPTH);
|
|
4079
|
+
if (discovered.length === 0) {
|
|
4080
|
+
error("No repos discovered under the workspace root.");
|
|
3899
4081
|
process.exitCode = 1;
|
|
3900
4082
|
return;
|
|
3901
4083
|
}
|
|
3902
|
-
const
|
|
3903
|
-
const
|
|
3904
|
-
if (
|
|
3905
|
-
|
|
3906
|
-
|
|
4084
|
+
const merged = mergeWorkspaceConfig(existing, discovered, { client: opts.client });
|
|
4085
|
+
const yamlText = renderWorkspaceYaml(merged);
|
|
4086
|
+
if (opts.json) {
|
|
4087
|
+
log(JSON.stringify({ discovered, config: merged }, null, 2));
|
|
4088
|
+
}
|
|
4089
|
+
if (opts.write) {
|
|
4090
|
+
await writeText(yamlPath, yamlText);
|
|
4091
|
+
log(`Wrote ${merged.repos.length} repo(s) to haus.workspace.yaml`);
|
|
3907
4092
|
return;
|
|
3908
4093
|
}
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
4094
|
+
if (!opts.json) {
|
|
4095
|
+
log("Proposed haus.workspace.yaml (run with --write to persist):\n");
|
|
4096
|
+
log(yamlText);
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
|
|
4100
|
+
// src/commands/workspace/doctor.ts
|
|
4101
|
+
import { existsSync as existsSync2 } from "fs";
|
|
4102
|
+
import path30 from "path";
|
|
4103
|
+
|
|
4104
|
+
// src/commands/workspace/manifest.ts
|
|
4105
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
4106
|
+
import path29 from "path";
|
|
4107
|
+
var MANIFEST_FILE = "workspace.manifest.json";
|
|
4108
|
+
function manifestPath(workspaceRoot) {
|
|
4109
|
+
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
4110
|
+
}
|
|
4111
|
+
function hausVersion() {
|
|
4112
|
+
try {
|
|
4113
|
+
const pkg = JSON.parse(readFileSync3(path29.join(packageRoot(), "package.json"), "utf8"));
|
|
4114
|
+
return pkg.version ?? "0.0.0";
|
|
4115
|
+
} catch {
|
|
4116
|
+
return "0.0.0";
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
function buildManifest2(opts) {
|
|
4120
|
+
const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4121
|
+
const version = opts.version ?? hausVersion();
|
|
4122
|
+
return {
|
|
4123
|
+
version: 1,
|
|
4124
|
+
generatedAt: now,
|
|
4125
|
+
hausVersion: version,
|
|
4126
|
+
client: opts.client,
|
|
4127
|
+
repos: opts.repos.map((repo) => ({
|
|
3915
4128
|
name: repo.name,
|
|
3916
4129
|
path: repo.path,
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
4130
|
+
role: repo.role,
|
|
4131
|
+
lastSetupAt: repo.lastSetupAt !== void 0 ? repo.lastSetupAt : repo.status === "ok" ? now : null,
|
|
4132
|
+
hausVersionAtSetup: repo.hausVersionAtSetup !== void 0 ? repo.hausVersionAtSetup : repo.status === "ok" ? version : null,
|
|
4133
|
+
lockItemCount: repo.lockItemCount,
|
|
4134
|
+
catalogRef: repo.catalogRef,
|
|
4135
|
+
status: repo.status,
|
|
4136
|
+
...repo.error ? { error: repo.error } : {}
|
|
4137
|
+
}))
|
|
4138
|
+
};
|
|
4139
|
+
}
|
|
4140
|
+
async function readManifest2(workspaceRoot) {
|
|
4141
|
+
return readJson(manifestPath(workspaceRoot));
|
|
4142
|
+
}
|
|
4143
|
+
async function writeWorkspaceManifest(workspaceRoot, manifest) {
|
|
4144
|
+
const target = manifestPath(workspaceRoot);
|
|
4145
|
+
await writeJson(target, manifest);
|
|
4146
|
+
return target;
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4149
|
+
// src/commands/workspace/doctor.ts
|
|
4150
|
+
async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
4151
|
+
const config2 = await readWorkspaceConfig(workspaceRoot);
|
|
4152
|
+
const manifest = await readManifest2(workspaceRoot);
|
|
4153
|
+
const currentVersion = hausVersion();
|
|
4154
|
+
const drift = [];
|
|
4155
|
+
const detail = [];
|
|
4156
|
+
const ok = (text) => detail.push({ stream: "log", text });
|
|
4157
|
+
const flag = (item) => {
|
|
4158
|
+
drift.push(item);
|
|
4159
|
+
detail.push({ stream: "warn", text: `- ${item.repo}: ${item.detail}` });
|
|
4160
|
+
};
|
|
4161
|
+
if (!config2) {
|
|
4162
|
+
flag({
|
|
4163
|
+
repo: "(workspace)",
|
|
4164
|
+
kind: "no-config",
|
|
4165
|
+
detail: "Missing or malformed haus.workspace.yaml \u2014 run `haus workspace discover --write` or `init`."
|
|
3920
4166
|
});
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
4167
|
+
return emit({ workspaceRoot, manifest, drift, detail, json: opts.json });
|
|
4168
|
+
}
|
|
4169
|
+
if (!manifest) {
|
|
4170
|
+
flag({
|
|
4171
|
+
repo: "(workspace)",
|
|
4172
|
+
kind: "no-manifest",
|
|
4173
|
+
detail: "No workspace.manifest.json \u2014 run `haus workspace setup --write` first."
|
|
4174
|
+
});
|
|
4175
|
+
return emit({ workspaceRoot, manifest, drift, detail, json: opts.json });
|
|
4176
|
+
}
|
|
4177
|
+
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
4178
|
+
for (const repo of config2.repos) {
|
|
4179
|
+
const repoRoot = path30.resolve(workspaceRoot, repo.path);
|
|
4180
|
+
const entry = manifestByName.get(repo.name);
|
|
4181
|
+
if (!entry) {
|
|
4182
|
+
flag({
|
|
4183
|
+
repo: repo.name,
|
|
4184
|
+
kind: "missing-from-manifest",
|
|
4185
|
+
detail: "Configured in yaml but absent from the manifest \u2014 run `haus workspace setup --write`."
|
|
4186
|
+
});
|
|
4187
|
+
continue;
|
|
4188
|
+
}
|
|
4189
|
+
const driftBefore = drift.length;
|
|
4190
|
+
if (entry.status === "failed") {
|
|
4191
|
+
flag({
|
|
4192
|
+
repo: repo.name,
|
|
4193
|
+
kind: "failed",
|
|
4194
|
+
detail: `Last setup failed${entry.error ? `: ${entry.error}` : ""}.`
|
|
4195
|
+
});
|
|
4196
|
+
}
|
|
4197
|
+
if (entry.hausVersionAtSetup && entry.hausVersionAtSetup !== currentVersion) {
|
|
4198
|
+
flag({
|
|
4199
|
+
repo: repo.name,
|
|
4200
|
+
kind: "version-mismatch",
|
|
4201
|
+
detail: `Set up at haus ${entry.hausVersionAtSetup}, current is ${currentVersion} \u2014 re-run setup.`
|
|
4202
|
+
});
|
|
4203
|
+
}
|
|
4204
|
+
if (!existsSync2(claudePath(repoRoot))) {
|
|
4205
|
+
flag({
|
|
4206
|
+
repo: repo.name,
|
|
4207
|
+
kind: "missing-claude",
|
|
4208
|
+
detail: "Missing .claude/ \u2014 run `haus workspace setup --write`."
|
|
4209
|
+
});
|
|
4210
|
+
}
|
|
4211
|
+
const lock = await checkLock(repoRoot);
|
|
4212
|
+
if (!existsSync2(hausPath(repoRoot, "haus.lock.json"))) {
|
|
4213
|
+
flag({
|
|
4214
|
+
repo: repo.name,
|
|
4215
|
+
kind: "missing-lock",
|
|
4216
|
+
detail: "Missing .haus-workflow/haus.lock.json \u2014 run `haus workspace setup --write`."
|
|
4217
|
+
});
|
|
4218
|
+
} else if (lock.count > 0 && !lock.ok) {
|
|
4219
|
+
flag({
|
|
4220
|
+
repo: repo.name,
|
|
4221
|
+
kind: "invalid-lock",
|
|
4222
|
+
detail: "haus.lock.json present but invalid \u2014 re-run `haus workspace setup --write`."
|
|
4223
|
+
});
|
|
4224
|
+
}
|
|
4225
|
+
if (drift.length === driftBefore) {
|
|
4226
|
+
ok(`- ${repo.name}: OK (${lock.count} lock item(s))`);
|
|
3924
4227
|
}
|
|
3925
4228
|
}
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
}
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
4229
|
+
return emit({ workspaceRoot, manifest, drift, detail, json: opts.json });
|
|
4230
|
+
}
|
|
4231
|
+
function emit(args) {
|
|
4232
|
+
const { workspaceRoot, manifest, drift, detail } = args;
|
|
4233
|
+
if (args.json) {
|
|
4234
|
+
log(JSON.stringify({ manifest: manifest ?? null, drift }, null, 2));
|
|
4235
|
+
} else {
|
|
4236
|
+
if (drift.length === 0) {
|
|
4237
|
+
log("\u2705 Workspace is set up and healthy.");
|
|
4238
|
+
} else {
|
|
4239
|
+
log(`\u26A0\uFE0F ${drift.length} workspace drift item(s) need attention:`);
|
|
4240
|
+
}
|
|
4241
|
+
log("Haus Workspace Doctor");
|
|
4242
|
+
for (const line2 of detail) {
|
|
4243
|
+
if (line2.stream === "warn") warn(line2.text);
|
|
4244
|
+
else log(line2.text);
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
if (drift.length > 0) process.exitCode = 1;
|
|
4248
|
+
return { workspaceRoot, manifest, drift };
|
|
4249
|
+
}
|
|
3934
4250
|
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
4251
|
+
// src/commands/workspace/setup.ts
|
|
4252
|
+
import { existsSync as existsSync3, statSync } from "fs";
|
|
4253
|
+
import path32 from "path";
|
|
4254
|
+
|
|
4255
|
+
// src/claude/write-workspace-claude-md.ts
|
|
4256
|
+
import path31 from "path";
|
|
4257
|
+
import fs18 from "fs-extra";
|
|
4258
|
+
function buildWorkspaceImportBlock(client, members) {
|
|
4259
|
+
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
4260
|
+
const body = [
|
|
4261
|
+
"@.haus-workflow/cross-repo-summary.md",
|
|
4262
|
+
"",
|
|
4263
|
+
`# Workspace: ${client}`,
|
|
4264
|
+
"",
|
|
4265
|
+
"Member repos:",
|
|
4266
|
+
...memberLines
|
|
4267
|
+
].join("\n");
|
|
4268
|
+
return `${BLOCK_BEGIN}
|
|
4269
|
+
${body}
|
|
4270
|
+
${BLOCK_END}`;
|
|
4271
|
+
}
|
|
4272
|
+
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
4273
|
+
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
4274
|
+
const dryRun = opts.dryRun ?? false;
|
|
4275
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path31.join(workspaceRoot, "CLAUDE.md");
|
|
4276
|
+
const prev = await fs18.pathExists(filePath) ? await fs18.readFile(filePath, "utf8") : "";
|
|
4277
|
+
const next = opts.collision ? `${block}
|
|
4278
|
+
` : injectHausBlock(prev, block);
|
|
4279
|
+
const printable = displayPath(workspaceRoot, filePath);
|
|
4280
|
+
if (dryRun) {
|
|
4281
|
+
if (!prev) {
|
|
4282
|
+
log(createUnifiedDiff(printable, "", next));
|
|
4283
|
+
} else if (hasTextChanged(prev, next)) {
|
|
4284
|
+
log(createUnifiedDiff(printable, prev, next));
|
|
4285
|
+
} else {
|
|
4286
|
+
log(`${printable}: unchanged`);
|
|
4287
|
+
}
|
|
4288
|
+
return filePath;
|
|
4289
|
+
}
|
|
4290
|
+
if (hasTextChanged(prev, next) && prev.length > 0) {
|
|
4291
|
+
const diffText = createUnifiedDiff(printable, prev, next);
|
|
4292
|
+
const summary = summarizeDiff(diffText);
|
|
4293
|
+
log(`Overwriting ${printable} (diff +${summary.additions} -${summary.deletions})`);
|
|
4294
|
+
}
|
|
4295
|
+
await writeText(filePath, next);
|
|
4296
|
+
return filePath;
|
|
4297
|
+
}
|
|
4298
|
+
|
|
4299
|
+
// src/commands/workspace/setup.ts
|
|
4300
|
+
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
4301
|
+
let dir = path32.resolve(start);
|
|
4302
|
+
for (; ; ) {
|
|
4303
|
+
if (existsSync3(path32.join(dir, WORKSPACE_FILE))) return dir;
|
|
4304
|
+
const parent = path32.dirname(dir);
|
|
4305
|
+
if (parent === dir) return path32.resolve(start);
|
|
4306
|
+
dir = parent;
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
function isRootRepo(workspaceRoot, repoPath) {
|
|
4310
|
+
return path32.resolve(workspaceRoot, repoPath) === path32.resolve(workspaceRoot);
|
|
4311
|
+
}
|
|
4312
|
+
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
4313
|
+
const mode = options.mode ?? "fast";
|
|
4314
|
+
const apply = options.write ?? false;
|
|
4315
|
+
const configText = await readText(path32.join(workspaceRoot, WORKSPACE_FILE));
|
|
4316
|
+
if (!configText) {
|
|
4317
|
+
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
4318
|
+
process.exitCode = 1;
|
|
4319
|
+
return { workspaceRoot, statuses: [], written: [] };
|
|
4320
|
+
}
|
|
4321
|
+
const config2 = parseWorkspaceConfig(configText);
|
|
4322
|
+
if (!config2 || config2.repos.length === 0) {
|
|
4323
|
+
error(`No repos configured in ${WORKSPACE_FILE}.`);
|
|
4324
|
+
process.exitCode = 1;
|
|
4325
|
+
return { workspaceRoot, statuses: [], written: [] };
|
|
4326
|
+
}
|
|
4327
|
+
const onlySet = options.only && options.only.length > 0 ? new Set(options.only) : void 0;
|
|
4328
|
+
const repos = onlySet ? config2.repos.filter((r) => onlySet.has(r.name)) : config2.repos;
|
|
4329
|
+
const statuses = [];
|
|
4330
|
+
const aggregateInputs = [];
|
|
4331
|
+
for (const repo of repos) {
|
|
4332
|
+
const repoRoot = path32.resolve(workspaceRoot, repo.path);
|
|
4333
|
+
log(`
|
|
4334
|
+
\u2192 ${repo.name} (${repo.path})`);
|
|
4335
|
+
try {
|
|
4336
|
+
if (!existsSync3(repoRoot) || !statSync(repoRoot).isDirectory()) {
|
|
4337
|
+
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
4338
|
+
}
|
|
4339
|
+
const res = await runSetupCore(repoRoot, {
|
|
4340
|
+
mode,
|
|
4341
|
+
json: options.json,
|
|
4342
|
+
apply,
|
|
4343
|
+
dryRun: options.dryRun
|
|
4344
|
+
});
|
|
4345
|
+
statuses.push({
|
|
4346
|
+
name: repo.name,
|
|
4347
|
+
path: repo.path,
|
|
4348
|
+
root: repoRoot,
|
|
4349
|
+
status: "ok",
|
|
4350
|
+
roles: res.roles,
|
|
4351
|
+
recommendedCount: res.recommendedCount
|
|
4352
|
+
});
|
|
4353
|
+
const context = await readContextOrScan(repoRoot);
|
|
4354
|
+
aggregateInputs.push({ name: repo.name, path: repo.path, context });
|
|
4355
|
+
} catch (err) {
|
|
4356
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4357
|
+
statuses.push({
|
|
4358
|
+
name: repo.name,
|
|
4359
|
+
path: repo.path,
|
|
4360
|
+
root: repoRoot,
|
|
4361
|
+
status: "failed",
|
|
4362
|
+
error: message
|
|
4363
|
+
});
|
|
4364
|
+
if (!options.continueOnError) throw err;
|
|
4365
|
+
error(`Setup failed for ${repo.name}: ${message}`);
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
const written = [];
|
|
4369
|
+
if (apply && aggregateInputs.length > 0) {
|
|
4370
|
+
const collision = config2.repos.some((r) => isRootRepo(workspaceRoot, r.path));
|
|
4371
|
+
if (!options.dryRun) {
|
|
4372
|
+
const artifacts = await writeWorkspaceArtifacts(
|
|
4373
|
+
workspaceRoot,
|
|
4374
|
+
aggregateInputs,
|
|
4375
|
+
config2.relationships
|
|
4376
|
+
);
|
|
4377
|
+
written.push(...artifacts);
|
|
4378
|
+
}
|
|
4379
|
+
const docPath = await writeWorkspaceClaudeMd(workspaceRoot, {
|
|
4380
|
+
client: config2.client,
|
|
4381
|
+
members: config2.repos.map((r) => ({ name: r.name, path: r.path })),
|
|
4382
|
+
collision,
|
|
4383
|
+
dryRun: options.dryRun
|
|
4384
|
+
});
|
|
4385
|
+
written.push(docPath);
|
|
4386
|
+
}
|
|
4387
|
+
if (apply && !options.dryRun) {
|
|
4388
|
+
const statusByName = new Map(statuses.map((s) => [s.name, s]));
|
|
4389
|
+
const prior = await readManifest2(workspaceRoot);
|
|
4390
|
+
if (!prior && existsSync3(manifestPath(workspaceRoot))) {
|
|
4391
|
+
warn(
|
|
4392
|
+
"Existing workspace.manifest.json is unreadable \u2014 prior per-repo state will not be carried forward."
|
|
4393
|
+
);
|
|
4394
|
+
}
|
|
4395
|
+
const priorByName = new Map((prior?.repos ?? []).map((r) => [r.name, r]));
|
|
4396
|
+
const manifestRepos = [];
|
|
4397
|
+
for (const repo of config2.repos) {
|
|
4398
|
+
const status = statusByName.get(repo.name);
|
|
4399
|
+
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
4400
|
+
if (status?.status === "ok") {
|
|
4401
|
+
const lock = await checkLock(path32.resolve(workspaceRoot, repo.path));
|
|
4402
|
+
manifestRepos.push({
|
|
4403
|
+
name: repo.name,
|
|
4404
|
+
path: repo.path,
|
|
4405
|
+
role,
|
|
4406
|
+
status: "ok",
|
|
4407
|
+
lockItemCount: lock.count,
|
|
4408
|
+
catalogRef: lock.catalogRef
|
|
4409
|
+
});
|
|
4410
|
+
} else if (status?.status === "failed") {
|
|
4411
|
+
manifestRepos.push({
|
|
4412
|
+
name: repo.name,
|
|
4413
|
+
path: repo.path,
|
|
4414
|
+
role,
|
|
4415
|
+
status: "failed",
|
|
4416
|
+
lockItemCount: 0,
|
|
4417
|
+
catalogRef: null,
|
|
4418
|
+
error: status.error
|
|
4419
|
+
});
|
|
4420
|
+
} else {
|
|
4421
|
+
const carried = priorByName.get(repo.name);
|
|
4422
|
+
manifestRepos.push(
|
|
4423
|
+
carried ? {
|
|
4424
|
+
name: carried.name,
|
|
4425
|
+
path: carried.path,
|
|
4426
|
+
role: carried.role,
|
|
4427
|
+
status: carried.status,
|
|
4428
|
+
lockItemCount: carried.lockItemCount,
|
|
4429
|
+
catalogRef: carried.catalogRef,
|
|
4430
|
+
lastSetupAt: carried.lastSetupAt,
|
|
4431
|
+
hausVersionAtSetup: carried.hausVersionAtSetup,
|
|
4432
|
+
...carried.error ? { error: carried.error } : {}
|
|
4433
|
+
} : {
|
|
4434
|
+
name: repo.name,
|
|
4435
|
+
path: repo.path,
|
|
4436
|
+
role,
|
|
4437
|
+
status: "pending",
|
|
4438
|
+
lockItemCount: 0,
|
|
4439
|
+
catalogRef: null
|
|
4440
|
+
}
|
|
4441
|
+
);
|
|
4442
|
+
}
|
|
4443
|
+
}
|
|
4444
|
+
const manifest = buildManifest2({ client: config2.client, repos: manifestRepos });
|
|
4445
|
+
const manifestFile = await writeWorkspaceManifest(workspaceRoot, manifest);
|
|
4446
|
+
written.push(manifestFile);
|
|
4447
|
+
}
|
|
4448
|
+
const ok = statuses.filter((s) => s.status === "ok").length;
|
|
4449
|
+
const failed = statuses.length - ok;
|
|
4450
|
+
log(`
|
|
4451
|
+
Workspace setup complete: ${ok} ok, ${failed} failed.`);
|
|
4452
|
+
return { workspaceRoot, statuses, written };
|
|
4453
|
+
}
|
|
4454
|
+
|
|
4455
|
+
// src/commands/workspace.ts
|
|
4456
|
+
function normalizeOnly(only) {
|
|
4457
|
+
if (!only) return void 0;
|
|
4458
|
+
const list = Array.isArray(only) ? only : only.split(/[\s,]+/);
|
|
4459
|
+
const cleaned = list.map((s) => s.trim()).filter(Boolean);
|
|
4460
|
+
return cleaned.length > 0 ? cleaned : void 0;
|
|
4461
|
+
}
|
|
4462
|
+
function normalizeMaxDepth(maxDepth) {
|
|
4463
|
+
if (maxDepth === void 0) return void 0;
|
|
4464
|
+
const n = typeof maxDepth === "number" ? maxDepth : Number.parseInt(maxDepth, 10);
|
|
4465
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : void 0;
|
|
4466
|
+
}
|
|
4467
|
+
async function initWorkspace() {
|
|
4468
|
+
await writeText(
|
|
4469
|
+
WORKSPACE_FILE,
|
|
4470
|
+
`client: unknown
|
|
4471
|
+
repos:
|
|
4472
|
+
- name: current
|
|
4473
|
+
path: .
|
|
4474
|
+
role: auto
|
|
4475
|
+
relationships: []
|
|
3938
4476
|
`
|
|
3939
4477
|
);
|
|
3940
|
-
log(
|
|
3941
|
-
|
|
3942
|
-
|
|
4478
|
+
log("Workspace initialized.");
|
|
4479
|
+
}
|
|
4480
|
+
async function scanWorkspace(workspaceRoot, opts) {
|
|
4481
|
+
const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
|
|
4482
|
+
if (!configText) {
|
|
4483
|
+
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
4484
|
+
process.exitCode = 1;
|
|
4485
|
+
return;
|
|
4486
|
+
}
|
|
4487
|
+
const config2 = parseWorkspaceConfig(configText);
|
|
4488
|
+
if (!config2) {
|
|
4489
|
+
error(
|
|
4490
|
+
`Malformed ${WORKSPACE_FILE}. Fix the YAML or re-run \`haus workspace discover --write\`.`
|
|
4491
|
+
);
|
|
4492
|
+
process.exitCode = 1;
|
|
4493
|
+
return;
|
|
4494
|
+
}
|
|
4495
|
+
if (config2.repos.length === 0) {
|
|
4496
|
+
error(`No repos configured in ${WORKSPACE_FILE}.`);
|
|
4497
|
+
process.exitCode = 1;
|
|
4498
|
+
return;
|
|
4499
|
+
}
|
|
4500
|
+
const inputs = [];
|
|
4501
|
+
for (const repo of config2.repos) {
|
|
4502
|
+
const repoRoot = path33.resolve(workspaceRoot, repo.path);
|
|
4503
|
+
if (!existsSync4(repoRoot) || !statSync2(repoRoot).isDirectory()) {
|
|
4504
|
+
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
4505
|
+
}
|
|
4506
|
+
const result = await scanProject(repoRoot, "fast");
|
|
4507
|
+
inputs.push({ name: repo.name, path: repo.path, context: result });
|
|
4508
|
+
}
|
|
4509
|
+
const written = await writeWorkspaceArtifacts(workspaceRoot, inputs, config2.relationships);
|
|
4510
|
+
if (opts.json) {
|
|
4511
|
+
log(JSON.stringify({ written }, null, 2));
|
|
4512
|
+
} else {
|
|
4513
|
+
log(`Workspace scan complete. Wrote ${written.length} artifact(s) under .haus-workflow/.`);
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
async function runWorkspace(action, options = {}) {
|
|
4517
|
+
if (action === "init") {
|
|
4518
|
+
await initWorkspace();
|
|
4519
|
+
return;
|
|
4520
|
+
}
|
|
4521
|
+
const workspaceRoot = resolveWorkspaceRoot();
|
|
4522
|
+
switch (action) {
|
|
4523
|
+
case "discover":
|
|
4524
|
+
await runDiscover(workspaceRoot, {
|
|
4525
|
+
write: options.write,
|
|
4526
|
+
json: options.json,
|
|
4527
|
+
maxDepth: normalizeMaxDepth(options.maxDepth),
|
|
4528
|
+
client: options.client
|
|
4529
|
+
});
|
|
4530
|
+
return;
|
|
4531
|
+
case "scan":
|
|
4532
|
+
await scanWorkspace(workspaceRoot, { json: options.json });
|
|
4533
|
+
return;
|
|
4534
|
+
case "setup":
|
|
4535
|
+
await runWorkspaceSetup(workspaceRoot, {
|
|
4536
|
+
mode: options.guided ? "guided" : "fast",
|
|
4537
|
+
write: options.write,
|
|
4538
|
+
dryRun: options.dryRun,
|
|
4539
|
+
json: options.json,
|
|
4540
|
+
continueOnError: options.continueOnError,
|
|
4541
|
+
only: normalizeOnly(options.only)
|
|
4542
|
+
});
|
|
4543
|
+
return;
|
|
4544
|
+
case "doctor":
|
|
4545
|
+
await runWorkspaceDoctor(workspaceRoot, { json: options.json });
|
|
4546
|
+
return;
|
|
4547
|
+
}
|
|
3943
4548
|
}
|
|
3944
4549
|
|
|
3945
4550
|
// src/cli.ts
|
|
3946
4551
|
function cliVersion() {
|
|
3947
4552
|
try {
|
|
3948
|
-
const pkgPath =
|
|
3949
|
-
const pkg = JSON.parse(
|
|
4553
|
+
const pkgPath = path34.join(packageRoot(), "package.json");
|
|
4554
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
3950
4555
|
return pkg.version ?? "0.0.0";
|
|
3951
4556
|
} catch {
|
|
3952
4557
|
return "0.0.0";
|
|
@@ -3955,8 +4560,8 @@ function cliVersion() {
|
|
|
3955
4560
|
var program = new Command();
|
|
3956
4561
|
function validateRuntimeNodeVersion() {
|
|
3957
4562
|
try {
|
|
3958
|
-
const pkgPath =
|
|
3959
|
-
const pkg = JSON.parse(
|
|
4563
|
+
const pkgPath = path34.join(packageRoot(), "package.json");
|
|
4564
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
3960
4565
|
const requiredRange = pkg.engines?.node;
|
|
3961
4566
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
3962
4567
|
throw new Error(`Node ${process.version} does not satisfy required range ${requiredRange}`);
|
|
@@ -3999,7 +4604,10 @@ config.command("disable <key>").description("Disable a hook (hook.context)").act
|
|
|
3999
4604
|
config.command("status <key>").description("Show current state of a hook (hook.context)").action((key) => runConfig(key, "status"));
|
|
4000
4605
|
var workspace = program.command("workspace");
|
|
4001
4606
|
workspace.command("init").action(() => runWorkspace("init"));
|
|
4002
|
-
workspace.command("
|
|
4607
|
+
workspace.command("discover").description("Auto-find member repos and write/merge haus.workspace.yaml").option("--write", "Persist haus.workspace.yaml (default previews only)").option("--json", "Output the discovered repos and proposed config as JSON").option("--max-depth <n>", "Max directory depth to traverse (default 3)").option("--client <name>", "Set the workspace client name").action((opts) => runWorkspace("discover", opts));
|
|
4608
|
+
workspace.command("scan").description("Aggregate a cross-repo summary from a fast scan of each repo").option("--json", "Output the written artifact paths as JSON").action((opts) => runWorkspace("scan", opts));
|
|
4609
|
+
workspace.command("setup").description("Per-repo setup loop + workspace layer + manifest").option("--write", "Apply changes (default previews only)").option("--dry-run", "Preview changes without writing").option("--json", "Emit machine-readable per-repo output").option("--fast", "Skip interactive prompts (default)").option("--guided", "Enable guided Q&A per repo").option("--continue-on-error", "Keep going past a failed repo (default fail-fast)").option("--only <names>", "Restrict to comma-separated repo names").action((opts) => runWorkspace("setup", opts));
|
|
4610
|
+
workspace.command("doctor").description("Report workspace drift against the manifest").option("--json", "Output the manifest and drift array as JSON").action((opts) => runWorkspace("doctor", opts));
|
|
4003
4611
|
program.parseAsync(process.argv).catch((err) => {
|
|
4004
4612
|
const message = err instanceof Error ? err.message : String(err);
|
|
4005
4613
|
error(message);
|