@haus-tech/haus-workflow 1.5.3 → 1.5.4
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 +6 -0
- package/dist/cli.js +99 -21
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.4](https://github.com/WeAreHausTech/haus-workflow/compare/v1.5.3...v1.5.4) (2026-08-11)
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
- **workspace:** stop setup from clobbering CLAUDE.md imports and dropping manual lock includes ([#213](https://github.com/WeAreHausTech/haus-workflow/issues/213)) ([b29ad06](https://github.com/WeAreHausTech/haus-workflow/commit/b29ad06c6e5c58e9381c7da300a02e8b486c6ceb))
|
|
8
|
+
|
|
3
9
|
## [1.5.3](https://github.com/WeAreHausTech/haus-workflow/compare/v1.5.2...v1.5.3) (2026-08-10)
|
|
4
10
|
|
|
5
11
|
### Performance
|
package/dist/cli.js
CHANGED
|
@@ -2479,6 +2479,9 @@ import path16 from "node:path";
|
|
|
2479
2479
|
import fs14 from "fs-extra";
|
|
2480
2480
|
var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
|
|
2481
2481
|
var BLOCK_END = "<!-- HAUS:END haus-imports -->";
|
|
2482
|
+
var WORKSPACE_BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-workspace-imports v=1 -->";
|
|
2483
|
+
var WORKSPACE_BLOCK_END = "<!-- HAUS:END haus-workspace-imports -->";
|
|
2484
|
+
var DEFAULT_SENTINELS = { begin: BLOCK_BEGIN, end: BLOCK_END };
|
|
2482
2485
|
var IMPORT_CONTENT = `@.haus-workflow/WORKFLOW.md
|
|
2483
2486
|
@.haus-workflow/workflow-config.md
|
|
2484
2487
|
@docs/decisions/README.md`;
|
|
@@ -2498,15 +2501,15 @@ function findLineMarker2(content2, marker, from = 0) {
|
|
|
2498
2501
|
}
|
|
2499
2502
|
return null;
|
|
2500
2503
|
}
|
|
2501
|
-
function findImportBlockRange(content2) {
|
|
2502
|
-
const begin = findLineMarker2(content2,
|
|
2504
|
+
function findImportBlockRange(content2, sentinels = DEFAULT_SENTINELS) {
|
|
2505
|
+
const begin = findLineMarker2(content2, sentinels.begin);
|
|
2503
2506
|
if (!begin) return null;
|
|
2504
|
-
const end = findLineMarker2(content2,
|
|
2507
|
+
const end = findLineMarker2(content2, sentinels.end, begin.end);
|
|
2505
2508
|
if (!end) return null;
|
|
2506
2509
|
return { beginStart: begin.start, beginEnd: begin.end, endStart: end.start, endEnd: end.end };
|
|
2507
2510
|
}
|
|
2508
|
-
function stripHausBlock(existing) {
|
|
2509
|
-
const range = findImportBlockRange(existing);
|
|
2511
|
+
function stripHausBlock(existing, sentinels = DEFAULT_SENTINELS) {
|
|
2512
|
+
const range = findImportBlockRange(existing, sentinels);
|
|
2510
2513
|
if (range) {
|
|
2511
2514
|
const before = existing.slice(0, range.beginStart);
|
|
2512
2515
|
const after = existing.slice(range.endEnd);
|
|
@@ -2514,7 +2517,7 @@ function stripHausBlock(existing) {
|
|
|
2514
2517
|
return merged.length > 0 ? `${merged}
|
|
2515
2518
|
` : "";
|
|
2516
2519
|
}
|
|
2517
|
-
const loneBegin = findLineMarker2(existing,
|
|
2520
|
+
const loneBegin = findLineMarker2(existing, sentinels.begin);
|
|
2518
2521
|
if (loneBegin) {
|
|
2519
2522
|
const before = existing.slice(0, loneBegin.start).trimEnd();
|
|
2520
2523
|
return before.length > 0 ? `${before}
|
|
@@ -2522,14 +2525,14 @@ function stripHausBlock(existing) {
|
|
|
2522
2525
|
}
|
|
2523
2526
|
return existing;
|
|
2524
2527
|
}
|
|
2525
|
-
function injectHausBlock(existing, block) {
|
|
2526
|
-
const range = findImportBlockRange(existing);
|
|
2528
|
+
function injectHausBlock(existing, block, sentinels = DEFAULT_SENTINELS) {
|
|
2529
|
+
const range = findImportBlockRange(existing, sentinels);
|
|
2527
2530
|
if (range) {
|
|
2528
2531
|
const before = existing.slice(0, range.beginStart);
|
|
2529
2532
|
const after = existing.slice(range.endEnd);
|
|
2530
2533
|
return `${before}${block}${after}`;
|
|
2531
2534
|
}
|
|
2532
|
-
const loneBegin = findLineMarker2(existing,
|
|
2535
|
+
const loneBegin = findLineMarker2(existing, sentinels.begin);
|
|
2533
2536
|
if (loneBegin) {
|
|
2534
2537
|
const before = existing.slice(0, loneBegin.start).trimEnd();
|
|
2535
2538
|
if (before.length === 0) return `${block}
|
|
@@ -3144,8 +3147,18 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
3144
3147
|
}
|
|
3145
3148
|
await pruneOrphanedCatalogItems(root, prevLock, notOrphaned, dryRun, opts.quiet);
|
|
3146
3149
|
}
|
|
3147
|
-
if (dryRun) return [...new Set(files)];
|
|
3148
3150
|
const installedItems = catalogItems.filter((r) => installedIds.has(r.id));
|
|
3151
|
+
if (dryRun) {
|
|
3152
|
+
const prevIds = new Set(
|
|
3153
|
+
prevLock.map((e) => e.id ? migrationByOldId.get(e.id) ?? e.id : void 0).filter((id) => Boolean(id))
|
|
3154
|
+
);
|
|
3155
|
+
const nextIds = new Set(installedItems.map((r) => r.id));
|
|
3156
|
+
const added = [...nextIds].filter((id) => !prevIds.has(id)).sort();
|
|
3157
|
+
const removed = [...prevIds].filter((id) => !nextIds.has(id)).sort();
|
|
3158
|
+
for (const id of added) say(`[dry-run] haus.lock.json would add: ${id}`);
|
|
3159
|
+
for (const id of removed) say(`[dry-run] haus.lock.json would remove: ${id}`);
|
|
3160
|
+
return [...new Set(files)];
|
|
3161
|
+
}
|
|
3149
3162
|
const prevRefById = new Map(
|
|
3150
3163
|
prevLock.filter((e) => e.id && e.catalogRef).map((e) => [migrationByOldId.get(e.id) ?? e.id, e.catalogRef])
|
|
3151
3164
|
);
|
|
@@ -5479,13 +5492,31 @@ async function runDoctor(options) {
|
|
|
5479
5492
|
"haus apply --write"
|
|
5480
5493
|
);
|
|
5481
5494
|
} else {
|
|
5482
|
-
ok("- CLAUDE.md: import block present");
|
|
5483
5495
|
const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
|
|
5484
|
-
const
|
|
5485
|
-
|
|
5486
|
-
|
|
5496
|
+
const importLinePattern = /^@(\S+)\s*$/gm;
|
|
5497
|
+
const actualTargets = [...block.matchAll(importLinePattern)].map((m) => m[1]);
|
|
5498
|
+
const expectedTargets = [...IMPORT_CONTENT.matchAll(importLinePattern)].map((m) => m[1]);
|
|
5499
|
+
const missingTargets = expectedTargets.filter((t) => !actualTargets.includes(t));
|
|
5500
|
+
const foreignTargets = actualTargets.filter((t) => !expectedTargets.includes(t));
|
|
5501
|
+
if (missingTargets.length > 0 || foreignTargets.length > 0) {
|
|
5502
|
+
const parts = [];
|
|
5503
|
+
if (missingTargets.length > 0) parts.push(`missing @${missingTargets.join(", @")}`);
|
|
5504
|
+
if (foreignTargets.length > 0) parts.push(`unexpected @${foreignTargets.join(", @")}`);
|
|
5505
|
+
flag(
|
|
5506
|
+
`- CLAUDE.md: haus import block content does not match expected imports (${parts.join("; ")}) (run \`haus apply --write\` to repair)`,
|
|
5507
|
+
"The haus import block in CLAUDE.md has been altered and no longer imports the expected files",
|
|
5508
|
+
"haus apply --write"
|
|
5509
|
+
);
|
|
5510
|
+
} else {
|
|
5511
|
+
ok("- CLAUDE.md: import block present");
|
|
5512
|
+
}
|
|
5513
|
+
for (const target of actualTargets) {
|
|
5514
|
+
const resolved = path38.resolve(root, target);
|
|
5515
|
+
const relFromRoot = path38.relative(root, resolved);
|
|
5516
|
+
const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${path38.sep}`) || path38.isAbsolute(relFromRoot);
|
|
5517
|
+
if (escapesRoot || !await fs29.pathExists(resolved)) {
|
|
5487
5518
|
flag(
|
|
5488
|
-
`- CLAUDE.md import:
|
|
5519
|
+
`- CLAUDE.md import: @${target} does not resolve (run \`haus apply --write\`)`,
|
|
5489
5520
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
5490
5521
|
"haus apply --write"
|
|
5491
5522
|
);
|
|
@@ -5493,6 +5524,13 @@ async function runDoctor(options) {
|
|
|
5493
5524
|
}
|
|
5494
5525
|
}
|
|
5495
5526
|
}
|
|
5527
|
+
if (rootClaudeMdContent && rootClaudeMdContent.includes(WORKSPACE_BLOCK_BEGIN) && !rootClaudeMdContent.includes(BLOCK_BEGIN)) {
|
|
5528
|
+
flag(
|
|
5529
|
+
"- CLAUDE.md: haus-workspace-imports block present but the per-repo haus-imports block is missing (run `haus apply --write`)",
|
|
5530
|
+
"The workspace CLAUDE.md has the workspace import block but not the per-repo one alongside it",
|
|
5531
|
+
"haus apply --write"
|
|
5532
|
+
);
|
|
5533
|
+
}
|
|
5496
5534
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
5497
5535
|
const workflowExists = await fs29.pathExists(workflowPath);
|
|
5498
5536
|
if (!workflowExists) {
|
|
@@ -7459,6 +7497,11 @@ async function hasMultipleSiblingRepos(root) {
|
|
|
7459
7497
|
}
|
|
7460
7498
|
|
|
7461
7499
|
// src/claude/setup-core.ts
|
|
7500
|
+
async function readPriorManualIncludes(root) {
|
|
7501
|
+
const prior = await readJson(hausPath(root, "recommendation.json"));
|
|
7502
|
+
if (!prior?.recommended) return [];
|
|
7503
|
+
return prior.recommended.filter((entry) => entry.selectionMode === "manual").map((entry) => entry.id);
|
|
7504
|
+
}
|
|
7462
7505
|
async function runSetupCore(root, opts) {
|
|
7463
7506
|
const { json, apply, dryRun, quiet, confirm: confirm2, force } = opts;
|
|
7464
7507
|
const say = quiet ? () => {
|
|
@@ -7473,7 +7516,8 @@ async function runSetupCore(root, opts) {
|
|
|
7473
7516
|
}
|
|
7474
7517
|
const siblingWarning = await hasMultipleSiblingRepos(root) ? [SIBLING_REPO_WARNING] : [];
|
|
7475
7518
|
const context = await readContextOrScan(root);
|
|
7476
|
-
const
|
|
7519
|
+
const priorManualIncludes = await readPriorManualIncludes(root);
|
|
7520
|
+
const recommendation = await recommend(root, context, { include: priorManualIncludes });
|
|
7477
7521
|
if (recommendation.recommended.length === 0 && !force) {
|
|
7478
7522
|
const warningLines2 = [
|
|
7479
7523
|
.../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? [], ...siblingWarning])
|
|
@@ -10012,6 +10056,7 @@ import path68 from "node:path";
|
|
|
10012
10056
|
// src/claude/write-workspace-claude-md.ts
|
|
10013
10057
|
import path67 from "node:path";
|
|
10014
10058
|
import fs46 from "fs-extra";
|
|
10059
|
+
var WORKSPACE_SENTINELS = { begin: WORKSPACE_BLOCK_BEGIN, end: WORKSPACE_BLOCK_END };
|
|
10015
10060
|
function buildWorkspaceImportBlock(client, members) {
|
|
10016
10061
|
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
10017
10062
|
const body = [
|
|
@@ -10022,9 +10067,9 @@ function buildWorkspaceImportBlock(client, members) {
|
|
|
10022
10067
|
"Member repos:",
|
|
10023
10068
|
...memberLines
|
|
10024
10069
|
].join("\n");
|
|
10025
|
-
return `${
|
|
10070
|
+
return `${WORKSPACE_BLOCK_BEGIN}
|
|
10026
10071
|
${body}
|
|
10027
|
-
${
|
|
10072
|
+
${WORKSPACE_BLOCK_END}`;
|
|
10028
10073
|
}
|
|
10029
10074
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
10030
10075
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
@@ -10035,7 +10080,7 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
10035
10080
|
const existed = await fs46.pathExists(filePath);
|
|
10036
10081
|
const prev = existed ? await fs46.readFile(filePath, "utf8") : "";
|
|
10037
10082
|
const next = opts.collision ? `${block}
|
|
10038
|
-
` : injectHausBlock(prev, block);
|
|
10083
|
+
` : injectHausBlock(prev, block, WORKSPACE_SENTINELS);
|
|
10039
10084
|
const printable = displayPath(workspaceRoot, filePath);
|
|
10040
10085
|
if (dryRun) {
|
|
10041
10086
|
if (!existed) {
|
|
@@ -10147,6 +10192,17 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10147
10192
|
config.relationships
|
|
10148
10193
|
);
|
|
10149
10194
|
written.push(...artifacts);
|
|
10195
|
+
} else {
|
|
10196
|
+
const sayAggregate = options.json ? () => {
|
|
10197
|
+
} : log;
|
|
10198
|
+
for (const rel of [
|
|
10199
|
+
"workspace-summary.json",
|
|
10200
|
+
"dependency-ownership-map.json",
|
|
10201
|
+
"cross-repo-summary.md",
|
|
10202
|
+
"workspace-context-map.json"
|
|
10203
|
+
]) {
|
|
10204
|
+
sayAggregate(`[dry-run] would write .haus-workflow/${rel}`);
|
|
10205
|
+
}
|
|
10150
10206
|
}
|
|
10151
10207
|
const docPath = await writeWorkspaceClaudeMd(workspaceRoot, {
|
|
10152
10208
|
client: config.client,
|
|
@@ -10267,6 +10323,14 @@ var WORKSPACE_AGGREGATE_FILES = [
|
|
|
10267
10323
|
"cross-repo-summary.md",
|
|
10268
10324
|
"workspace-context-map.json"
|
|
10269
10325
|
];
|
|
10326
|
+
function hasLegacyWorkspaceBlock(content2) {
|
|
10327
|
+
const beginIdx = content2.indexOf(BLOCK_BEGIN);
|
|
10328
|
+
if (beginIdx < 0) return false;
|
|
10329
|
+
const endIdx = content2.indexOf(BLOCK_END, beginIdx + BLOCK_BEGIN.length);
|
|
10330
|
+
if (endIdx < 0) return false;
|
|
10331
|
+
const block = content2.slice(beginIdx, endIdx + BLOCK_END.length);
|
|
10332
|
+
return block.includes("@.haus-workflow/cross-repo-summary.md") && block.includes("Member repos:");
|
|
10333
|
+
}
|
|
10270
10334
|
async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
10271
10335
|
const config = await readWorkspaceConfig(workspaceRoot);
|
|
10272
10336
|
if (!config) {
|
|
@@ -10329,8 +10393,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10329
10393
|
const claudeMdPath = path69.join(workspaceRoot, "CLAUDE.md");
|
|
10330
10394
|
if (existsSync9(claudeMdPath)) {
|
|
10331
10395
|
const prev = await readText(claudeMdPath);
|
|
10332
|
-
if (prev?.includes(
|
|
10333
|
-
const next = stripHausBlock(prev
|
|
10396
|
+
if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
|
|
10397
|
+
const next = stripHausBlock(prev, {
|
|
10398
|
+
begin: WORKSPACE_BLOCK_BEGIN,
|
|
10399
|
+
end: WORKSPACE_BLOCK_END
|
|
10400
|
+
});
|
|
10334
10401
|
if (next.length === 0) {
|
|
10335
10402
|
await fs47.remove(claudeMdPath);
|
|
10336
10403
|
log("Removed CLAUDE.md (only contained the workspace haus import block).");
|
|
@@ -10338,6 +10405,17 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10338
10405
|
await fs47.writeFile(claudeMdPath, next, "utf8");
|
|
10339
10406
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
10340
10407
|
}
|
|
10408
|
+
} else if (prev && hasLegacyWorkspaceBlock(prev)) {
|
|
10409
|
+
const next = stripHausBlock(prev, { begin: BLOCK_BEGIN, end: BLOCK_END });
|
|
10410
|
+
if (next.length === 0) {
|
|
10411
|
+
await fs47.remove(claudeMdPath);
|
|
10412
|
+
log("Removed CLAUDE.md (only contained the pre-upgrade workspace haus import block).");
|
|
10413
|
+
} else {
|
|
10414
|
+
await fs47.writeFile(claudeMdPath, next, "utf8");
|
|
10415
|
+
log(
|
|
10416
|
+
"Removed pre-upgrade workspace haus import block from CLAUDE.md (user content preserved)."
|
|
10417
|
+
);
|
|
10418
|
+
}
|
|
10341
10419
|
}
|
|
10342
10420
|
}
|
|
10343
10421
|
}
|