@haus-tech/haus-workflow 0.13.2 → 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 +9 -0
- package/dist/cli.js +749 -132
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.14.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.13.2...v0.14.0) (2026-06-04)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **workspace:** add repo auto-discovery ([#70](https://github.com/WeAreHausTech/haus-workflow/issues/70)) ([4b57571](https://github.com/WeAreHausTech/haus-workflow/commit/4b57571a32ef733dda0faf474d593b7fbb297a8e))
|
|
8
|
+
- **workspace:** extract shared setup core for workspace configuration ([#68](https://github.com/WeAreHausTech/haus-workflow/issues/68)) ([5e9b1ea](https://github.com/WeAreHausTech/haus-workflow/commit/5e9b1eaf72e8c7a32ec28148611f778367e4f1ef))
|
|
9
|
+
- **workspace:** manifest + drift doctor + command wiring (Tasks 4–5) ([#72](https://github.com/WeAreHausTech/haus-workflow/issues/72)) ([4526005](https://github.com/WeAreHausTech/haus-workflow/commit/4526005e693cc2f254ee6c1c5991a92f356506cc))
|
|
10
|
+
- **workspace:** per-repo setup loop + workspace aggregate layer ([#71](https://github.com/WeAreHausTech/haus-workflow/issues/71)) ([5c7ae9d](https://github.com/WeAreHausTech/haus-workflow/commit/5c7ae9d5691b0bd262c3449bad2fe649fd0271bf))
|
|
11
|
+
|
|
3
12
|
## [0.13.2](https://github.com/WeAreHausTech/haus-workflow/compare/v0.13.1...v0.13.2) (2026-06-04)
|
|
4
13
|
|
|
5
14
|
### Bug Fixes
|
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
|
|
@@ -449,8 +449,8 @@ function buildDenyRules() {
|
|
|
449
449
|
for (const command of DANGEROUS_COMMANDS) {
|
|
450
450
|
rules.push(`Bash(${command}:*)`);
|
|
451
451
|
}
|
|
452
|
-
for (const
|
|
453
|
-
const pattern = SENSITIVE_DIRS.has(
|
|
452
|
+
for (const path35 of SENSITIVE_PATHS) {
|
|
453
|
+
const pattern = SENSITIVE_DIRS.has(path35) ? `${path35}/**` : path35;
|
|
454
454
|
for (const tool of FILE_TOOLS) {
|
|
455
455
|
rules.push(`${tool}(${pattern})`);
|
|
456
456
|
}
|
|
@@ -814,7 +814,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
814
814
|
estimatedTokenReductionPct: 0
|
|
815
815
|
};
|
|
816
816
|
const pkgRoot = packageRoot();
|
|
817
|
-
const
|
|
817
|
+
const hausVersion2 = (await readJson(path9.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
|
|
818
818
|
const coreFiles = [
|
|
819
819
|
claudePath(root, "settings.json"),
|
|
820
820
|
claudePath(root, "rules", "haus.md"),
|
|
@@ -823,7 +823,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
823
823
|
claudePath(root, "commands", "haus-review.md")
|
|
824
824
|
];
|
|
825
825
|
const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun);
|
|
826
|
-
const workflowPath = await writeWorkflow(root,
|
|
826
|
+
const workflowPath = await writeWorkflow(root, hausVersion2, dryRun);
|
|
827
827
|
const workflowConfigPath = await writeWorkflowConfig(root, dryRun, {
|
|
828
828
|
refill: opts.refillConfig
|
|
829
829
|
});
|
|
@@ -870,9 +870,9 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
870
870
|
dryRun
|
|
871
871
|
);
|
|
872
872
|
const fixtureManifestPath = process.env["HAUS_FIXTURE_CATALOG"];
|
|
873
|
-
const
|
|
874
|
-
const manifestDir = path9.dirname(
|
|
875
|
-
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: [] };
|
|
876
876
|
const manifestById = new Map((manifest.items ?? []).map((item) => [item.id, item]));
|
|
877
877
|
const cacheManifest = await readJson(
|
|
878
878
|
path9.join(CACHE_DIR, "manifest.json")
|
|
@@ -943,7 +943,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
943
943
|
id: r.id,
|
|
944
944
|
type: r.type,
|
|
945
945
|
source: isCurated ? "curated" : "haus",
|
|
946
|
-
version:
|
|
946
|
+
version: hausVersion2,
|
|
947
947
|
catalogRef: CATALOG_REF,
|
|
948
948
|
hash: await hashInstalledPaths(root, relPaths),
|
|
949
949
|
installMode: "copied",
|
|
@@ -2548,6 +2548,24 @@ async function runGuard(kind, _options) {
|
|
|
2548
2548
|
import path18 from "path";
|
|
2549
2549
|
import fs12 from "fs-extra";
|
|
2550
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
|
+
|
|
2551
2569
|
// src/utils/exec.ts
|
|
2552
2570
|
import { execa } from "execa";
|
|
2553
2571
|
async function runCommand(command, args = [], options = {}) {
|
|
@@ -2832,64 +2850,11 @@ function buildStackSet(context) {
|
|
|
2832
2850
|
);
|
|
2833
2851
|
}
|
|
2834
2852
|
|
|
2835
|
-
// src/
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
async function ask(question) {
|
|
2839
|
-
const rl = readline.createInterface({ input, output });
|
|
2840
|
-
try {
|
|
2841
|
-
const answer = await rl.question(`${question}
|
|
2842
|
-
> `);
|
|
2843
|
-
return answer.trim();
|
|
2844
|
-
} finally {
|
|
2845
|
-
rl.close();
|
|
2846
|
-
}
|
|
2847
|
-
}
|
|
2848
|
-
async function confirm(question) {
|
|
2849
|
-
const answer = (await ask(`${question} [y/N]`)).toLowerCase();
|
|
2850
|
-
return answer === "y" || answer === "yes";
|
|
2851
|
-
}
|
|
2852
|
-
|
|
2853
|
-
// src/commands/setup-project.ts
|
|
2854
|
-
var GUIDED_QUESTIONS = [
|
|
2855
|
-
"What is this project for?",
|
|
2856
|
-
"Is it for a client, internal Haus work, or experimentation?",
|
|
2857
|
-
"What should Claude help with most?",
|
|
2858
|
-
"Is this project connected to other repositories?",
|
|
2859
|
-
"Are there parts of the project Claude should avoid touching?",
|
|
2860
|
-
"Are there client-specific rules or sensitive areas?",
|
|
2861
|
-
"Do you want a minimal, standard, or strict setup?"
|
|
2862
|
-
];
|
|
2863
|
-
async function runSetupProject(options) {
|
|
2864
|
-
const root = process.cwd();
|
|
2865
|
-
let mode = options.guided ? "guided" : "fast";
|
|
2866
|
-
if (!options.guided && !options.fast && !options.json) {
|
|
2867
|
-
log("How do you want to set this project up?");
|
|
2868
|
-
log("1. Guided setup - I'll ask a few simple questions, then scan the project.");
|
|
2869
|
-
log("2. Fast setup - I'll only scan the project and recommend defaults.");
|
|
2870
|
-
const choice = await ask("Choose 1 or 2");
|
|
2871
|
-
mode = choice === "1" ? "guided" : "fast";
|
|
2872
|
-
}
|
|
2873
|
-
if (mode === "guided") {
|
|
2874
|
-
const existing = await readJson(hausPath(root, "setup-answers.json")) ?? {};
|
|
2875
|
-
const merged = {};
|
|
2876
|
-
for (const question of GUIDED_QUESTIONS) {
|
|
2877
|
-
if (options.json) {
|
|
2878
|
-
merged[question] = existing[question] ?? "pending-user-answer";
|
|
2879
|
-
continue;
|
|
2880
|
-
}
|
|
2881
|
-
const prefilled = existing[question];
|
|
2882
|
-
if (prefilled && prefilled !== "pending-user-answer" && prefilled !== "no-answer") {
|
|
2883
|
-
merged[question] = prefilled;
|
|
2884
|
-
continue;
|
|
2885
|
-
}
|
|
2886
|
-
const answer = await ask(question);
|
|
2887
|
-
merged[question] = answer || prefilled || "no-answer";
|
|
2888
|
-
}
|
|
2889
|
-
await writeJson(hausPath(root, "setup-answers.json"), merged);
|
|
2890
|
-
}
|
|
2853
|
+
// src/commands/setup-core.ts
|
|
2854
|
+
async function runSetupCore(root, opts) {
|
|
2855
|
+
const { mode, json, apply, dryRun, confirm: confirm2 } = opts;
|
|
2891
2856
|
const scanResult = await scanProject(root, mode);
|
|
2892
|
-
if (
|
|
2857
|
+
if (json) {
|
|
2893
2858
|
log(JSON.stringify(scanResult, null, 2));
|
|
2894
2859
|
} else {
|
|
2895
2860
|
log("Haus scan complete");
|
|
@@ -2905,7 +2870,7 @@ async function runSetupProject(options) {
|
|
|
2905
2870
|
{ id: "haus.rule.context-minimal", enabled: true },
|
|
2906
2871
|
{ id: "haus.rule.security", enabled: true }
|
|
2907
2872
|
]);
|
|
2908
|
-
if (
|
|
2873
|
+
if (json) {
|
|
2909
2874
|
log(JSON.stringify(recommendation, null, 2));
|
|
2910
2875
|
} else {
|
|
2911
2876
|
log("Haus recommendation ready");
|
|
@@ -2916,6 +2881,7 @@ async function runSetupProject(options) {
|
|
|
2916
2881
|
const warningLines = [.../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? []])];
|
|
2917
2882
|
log(`Repo: ${context.repoName}`);
|
|
2918
2883
|
for (const warning of warningLines) log(`- WARN: ${warning}`);
|
|
2884
|
+
const hooksOk = hooks.ok;
|
|
2919
2885
|
if (hooks.skipped) {
|
|
2920
2886
|
log(`- HOOKS: (skipped) ${hooks.message}`);
|
|
2921
2887
|
} else if (!hooks.ok) {
|
|
@@ -2924,17 +2890,29 @@ async function runSetupProject(options) {
|
|
|
2924
2890
|
} else {
|
|
2925
2891
|
log(`- HOOKS OK: ${hooks.message}`);
|
|
2926
2892
|
}
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
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
|
+
}
|
|
2933
2910
|
}
|
|
2934
|
-
const files = await writeClaudeFiles(root, false);
|
|
2911
|
+
const files = await writeClaudeFiles(root, dryRun ?? false);
|
|
2935
2912
|
log("Applied files:");
|
|
2936
2913
|
files.forEach((f) => log(`- ${displayPath(root, f)}`));
|
|
2937
2914
|
const hooksAfter = await verifyProjectSettingsHooksContract(root);
|
|
2915
|
+
const hooksOkAfter = hooksAfter.ok;
|
|
2938
2916
|
if (hooksAfter.skipped) {
|
|
2939
2917
|
log(`- HOOKS: (skipped) ${hooksAfter.message}`);
|
|
2940
2918
|
} else if (!hooksAfter.ok) {
|
|
@@ -2943,6 +2921,54 @@ async function runSetupProject(options) {
|
|
|
2943
2921
|
} else {
|
|
2944
2922
|
log(`- HOOKS OK: ${hooksAfter.message}`);
|
|
2945
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
|
+
});
|
|
2946
2972
|
}
|
|
2947
2973
|
|
|
2948
2974
|
// src/commands/init.ts
|
|
@@ -3512,9 +3538,9 @@ async function runUninstall(options = {}) {
|
|
|
3512
3538
|
await writeSettings(stripped);
|
|
3513
3539
|
result.hooksStripped = true;
|
|
3514
3540
|
const hausDir = path23.join(globalClaudeDir(), "haus");
|
|
3515
|
-
const
|
|
3516
|
-
if (fs16.pathExistsSync(
|
|
3517
|
-
await fs16.remove(
|
|
3541
|
+
const manifestPath2 = hausManifestPath();
|
|
3542
|
+
if (fs16.pathExistsSync(manifestPath2)) {
|
|
3543
|
+
await fs16.remove(manifestPath2);
|
|
3518
3544
|
}
|
|
3519
3545
|
if (fs16.pathExistsSync(hausDir)) {
|
|
3520
3546
|
const remaining = await fs16.readdir(hausDir);
|
|
@@ -3831,13 +3857,13 @@ function walkMd(dir, fn) {
|
|
|
3831
3857
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
3832
3858
|
}
|
|
3833
3859
|
}
|
|
3834
|
-
async function runValidateCatalog(
|
|
3835
|
-
if (!
|
|
3860
|
+
async function runValidateCatalog(manifestPath2) {
|
|
3861
|
+
if (!manifestPath2) {
|
|
3836
3862
|
error("Usage: haus validate-catalog <path/to/manifest.json>");
|
|
3837
3863
|
process.exitCode = 1;
|
|
3838
3864
|
return;
|
|
3839
3865
|
}
|
|
3840
|
-
const abs = path26.resolve(process.cwd(),
|
|
3866
|
+
const abs = path26.resolve(process.cwd(), manifestPath2);
|
|
3841
3867
|
const manifestDir = path26.dirname(abs);
|
|
3842
3868
|
const data = await readJson(abs);
|
|
3843
3869
|
if (!data?.items) {
|
|
@@ -3867,77 +3893,665 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3867
3893
|
}
|
|
3868
3894
|
|
|
3869
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
|
|
3870
3951
|
import path27 from "path";
|
|
3871
3952
|
import YAML from "yaml";
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
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)."
|
|
3883
4074
|
);
|
|
3884
|
-
|
|
4075
|
+
process.exitCode = 1;
|
|
3885
4076
|
return;
|
|
3886
4077
|
}
|
|
3887
|
-
const
|
|
3888
|
-
if (
|
|
3889
|
-
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.");
|
|
3890
4081
|
process.exitCode = 1;
|
|
3891
4082
|
return;
|
|
3892
4083
|
}
|
|
3893
|
-
const
|
|
3894
|
-
const
|
|
3895
|
-
if (
|
|
3896
|
-
|
|
3897
|
-
|
|
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`);
|
|
3898
4092
|
return;
|
|
3899
4093
|
}
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
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) => ({
|
|
3906
4128
|
name: repo.name,
|
|
3907
4129
|
path: repo.path,
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
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`."
|
|
3911
4166
|
});
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
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))`);
|
|
3915
4227
|
}
|
|
3916
4228
|
}
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
}
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
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
|
+
}
|
|
3925
4250
|
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
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: []
|
|
3929
4476
|
`
|
|
3930
4477
|
);
|
|
3931
|
-
log(
|
|
3932
|
-
|
|
3933
|
-
|
|
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
|
+
}
|
|
3934
4548
|
}
|
|
3935
4549
|
|
|
3936
4550
|
// src/cli.ts
|
|
3937
4551
|
function cliVersion() {
|
|
3938
4552
|
try {
|
|
3939
|
-
const pkgPath =
|
|
3940
|
-
const pkg = JSON.parse(
|
|
4553
|
+
const pkgPath = path34.join(packageRoot(), "package.json");
|
|
4554
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
3941
4555
|
return pkg.version ?? "0.0.0";
|
|
3942
4556
|
} catch {
|
|
3943
4557
|
return "0.0.0";
|
|
@@ -3946,8 +4560,8 @@ function cliVersion() {
|
|
|
3946
4560
|
var program = new Command();
|
|
3947
4561
|
function validateRuntimeNodeVersion() {
|
|
3948
4562
|
try {
|
|
3949
|
-
const pkgPath =
|
|
3950
|
-
const pkg = JSON.parse(
|
|
4563
|
+
const pkgPath = path34.join(packageRoot(), "package.json");
|
|
4564
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
3951
4565
|
const requiredRange = pkg.engines?.node;
|
|
3952
4566
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
3953
4567
|
throw new Error(`Node ${process.version} does not satisfy required range ${requiredRange}`);
|
|
@@ -3990,7 +4604,10 @@ config.command("disable <key>").description("Disable a hook (hook.context)").act
|
|
|
3990
4604
|
config.command("status <key>").description("Show current state of a hook (hook.context)").action((key) => runConfig(key, "status"));
|
|
3991
4605
|
var workspace = program.command("workspace");
|
|
3992
4606
|
workspace.command("init").action(() => runWorkspace("init"));
|
|
3993
|
-
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));
|
|
3994
4611
|
program.parseAsync(process.argv).catch((err) => {
|
|
3995
4612
|
const message = err instanceof Error ? err.message : String(err);
|
|
3996
4613
|
error(message);
|