@haus-tech/haus-workflow 4.0.0 → 4.0.1
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 +7 -0
- package/dist/cli.js +189 -168
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.0.1](https://github.com/WeAreHausTech/haus-workflow/compare/v4.0.0...v4.0.1) (2026-08-20)
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
- land remaining [#299](https://github.com/WeAreHausTech/haus-workflow/issues/299) cleanup backlog ([#322](https://github.com/WeAreHausTech/haus-workflow/issues/322)) ([0b9a747](https://github.com/WeAreHausTech/haus-workflow/commit/0b9a747be0ad4d61c52113a62d126f8133f6e175))
|
|
8
|
+
- treat mixed escaping-path entries as whole-entry refusals ([#321](https://github.com/WeAreHausTech/haus-workflow/issues/321)) ([59593e8](https://github.com/WeAreHausTech/haus-workflow/commit/59593e8495f88422943132d8a9a62badee887431))
|
|
9
|
+
|
|
3
10
|
## [4.0.0](https://github.com/WeAreHausTech/haus-workflow/compare/v3.1.2...v4.0.0) (2026-08-20)
|
|
4
11
|
|
|
5
12
|
### ⚠ BREAKING CHANGES
|
package/dist/cli.js
CHANGED
|
@@ -1913,7 +1913,7 @@ var init_sync = __esm({
|
|
|
1913
1913
|
|
|
1914
1914
|
// src/cli.ts
|
|
1915
1915
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
1916
|
-
import
|
|
1916
|
+
import path80 from "node:path";
|
|
1917
1917
|
import { Command } from "commander";
|
|
1918
1918
|
|
|
1919
1919
|
// src/commands/apply.ts
|
|
@@ -3050,8 +3050,8 @@ function buildDenyRules() {
|
|
|
3050
3050
|
for (const command of DENY_COMMANDS) {
|
|
3051
3051
|
rules.push(`Bash(${command}:*)`);
|
|
3052
3052
|
}
|
|
3053
|
-
for (const
|
|
3054
|
-
const pattern = DENY_DIRS.has(
|
|
3053
|
+
for (const path81 of DENY_PATHS) {
|
|
3054
|
+
const pattern = DENY_DIRS.has(path81) ? `${path81}/**` : path81;
|
|
3055
3055
|
for (const tool of FILE_TOOLS) {
|
|
3056
3056
|
rules.push(`${tool}(${pattern})`);
|
|
3057
3057
|
}
|
|
@@ -4441,7 +4441,7 @@ async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, q
|
|
|
4441
4441
|
const relPaths = entry.paths ?? [];
|
|
4442
4442
|
const existing = await resolveExistingTrackedPaths(root, migration.oldId, relPaths, warn);
|
|
4443
4443
|
if (existing.length === 0) continue;
|
|
4444
|
-
const gate = await hashGateEntry(root, {
|
|
4444
|
+
const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
|
|
4445
4445
|
if (!gate.ok) {
|
|
4446
4446
|
if (gate.reason === "no-hash") {
|
|
4447
4447
|
warn(
|
|
@@ -4468,7 +4468,7 @@ async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, q
|
|
|
4468
4468
|
return;
|
|
4469
4469
|
}
|
|
4470
4470
|
const allAbsPaths = toRemove.flatMap(({ existing }) => existing.map(({ abs }) => abs));
|
|
4471
|
-
await backupTrackedPaths(root, allAbsPaths, say, "migrate");
|
|
4471
|
+
void await backupTrackedPaths(root, allAbsPaths, say, "migrate");
|
|
4472
4472
|
for (const { existing } of toRemove) {
|
|
4473
4473
|
for (const { abs } of existing) {
|
|
4474
4474
|
await fs22.remove(abs);
|
|
@@ -4487,11 +4487,13 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
|
|
|
4487
4487
|
);
|
|
4488
4488
|
let keepShared = false;
|
|
4489
4489
|
for (const entry of supersededEntries) {
|
|
4490
|
+
const id = entry.id;
|
|
4491
|
+
if (!id) continue;
|
|
4490
4492
|
const relPaths = entry.paths ?? [];
|
|
4491
4493
|
if (relPaths.length === 0) continue;
|
|
4492
|
-
const existing = await resolveExistingTrackedPaths(root,
|
|
4494
|
+
const existing = await resolveExistingTrackedPaths(root, id, relPaths, warn);
|
|
4493
4495
|
if (existing.length === 0) continue;
|
|
4494
|
-
const gate = await hashGateEntry(root, {
|
|
4496
|
+
const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
|
|
4495
4497
|
if (!gate.ok) keepShared = true;
|
|
4496
4498
|
}
|
|
4497
4499
|
for (const entry of prevLock) {
|
|
@@ -4503,12 +4505,15 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
|
|
|
4503
4505
|
if (!removedFromManifest && !deprecated && !superseded) continue;
|
|
4504
4506
|
const relPaths = entry.paths ?? [];
|
|
4505
4507
|
if (relPaths.length === 0) continue;
|
|
4506
|
-
if (await hasProtectedUnsafePath(root, relPaths)) refusedIds.add(entry.id);
|
|
4507
4508
|
const existing = await resolveExistingTrackedPaths(root, entry.id, relPaths, warn);
|
|
4509
|
+
if (await hasProtectedUnsafePath(root, relPaths)) {
|
|
4510
|
+
refusedIds.add(entry.id);
|
|
4511
|
+
continue;
|
|
4512
|
+
}
|
|
4508
4513
|
if (existing.length === 0) continue;
|
|
4509
4514
|
const pruneReason = superseded ? "superseded" : deprecated ? "deprecated" : "stale";
|
|
4510
4515
|
const label = superseded ? "Superseded" : deprecated ? "Deprecated" : "Stale";
|
|
4511
|
-
const gate = await hashGateEntry(root, {
|
|
4516
|
+
const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
|
|
4512
4517
|
if (!gate.ok) {
|
|
4513
4518
|
if (gate.reason === "no-hash") {
|
|
4514
4519
|
warn(
|
|
@@ -4533,7 +4538,7 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
|
|
|
4533
4538
|
return true;
|
|
4534
4539
|
}) : existing;
|
|
4535
4540
|
if (!dryRun && removable.length > 0) {
|
|
4536
|
-
await backupTrackedPaths(
|
|
4541
|
+
void await backupTrackedPaths(
|
|
4537
4542
|
root,
|
|
4538
4543
|
removable.map((e) => e.abs),
|
|
4539
4544
|
say,
|
|
@@ -4563,10 +4568,13 @@ async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun,
|
|
|
4563
4568
|
if (!entry.id) continue;
|
|
4564
4569
|
const relPaths = entry.paths ?? [];
|
|
4565
4570
|
if (relPaths.length === 0) continue;
|
|
4566
|
-
if (await hasProtectedUnsafePath(root, relPaths)) refusedIds.add(entry.id);
|
|
4567
4571
|
const existing = await resolveExistingTrackedPaths(root, entry.id, relPaths, warn);
|
|
4572
|
+
if (await hasProtectedUnsafePath(root, relPaths)) {
|
|
4573
|
+
refusedIds.add(entry.id);
|
|
4574
|
+
continue;
|
|
4575
|
+
}
|
|
4568
4576
|
if (existing.length === 0) continue;
|
|
4569
|
-
const gate = await hashGateEntry(root, {
|
|
4577
|
+
const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
|
|
4570
4578
|
if (!gate.ok) {
|
|
4571
4579
|
const detail = gate.reason === "no-hash" ? "has no lock hash" : "was modified locally";
|
|
4572
4580
|
warn(
|
|
@@ -4587,7 +4595,7 @@ async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun,
|
|
|
4587
4595
|
return refusedIds;
|
|
4588
4596
|
}
|
|
4589
4597
|
const allAbsPaths = toRemove.flatMap(({ existing }) => existing.map(({ abs }) => abs));
|
|
4590
|
-
await backupTrackedPaths(root, allAbsPaths, say, "prune");
|
|
4598
|
+
void await backupTrackedPaths(root, allAbsPaths, say, "prune");
|
|
4591
4599
|
for (const { id, existing } of toRemove) {
|
|
4592
4600
|
await deleteTrackedPaths(root, id, existing, { verb: "Pruned", reason: "orphaned" }, say);
|
|
4593
4601
|
}
|
|
@@ -4604,7 +4612,8 @@ var GITIGNORED_ARTIFACT_PATHS = [
|
|
|
4604
4612
|
".haus-workflow/context-map.json",
|
|
4605
4613
|
".haus-workflow/recommendation.json",
|
|
4606
4614
|
".haus-workflow/sources-report.json",
|
|
4607
|
-
".haus-workflow/deep-context.json"
|
|
4615
|
+
".haus-workflow/deep-context.json",
|
|
4616
|
+
".haus-workflow/backups/"
|
|
4608
4617
|
];
|
|
4609
4618
|
function findLineMarker3(content2, marker, from = 0) {
|
|
4610
4619
|
let idx = content2.indexOf(marker, from);
|
|
@@ -4649,7 +4658,7 @@ function buildBlockFor(existing, beginMarker, endMarker, comment, patterns) {
|
|
|
4649
4658
|
if (needed.length === 0) return null;
|
|
4650
4659
|
return [beginMarker, comment, ...needed, endMarker].join("\n");
|
|
4651
4660
|
}
|
|
4652
|
-
var ARTIFACT_BLOCK_COMMENT = "# haus
|
|
4661
|
+
var ARTIFACT_BLOCK_COMMENT = "# haus machine-local artifacts \u2014 must never be committed (see ADR-0025, ADR-0058)";
|
|
4653
4662
|
function injectBlockFor(existing, beginMarker, endMarker, comment, patterns) {
|
|
4654
4663
|
const block = buildBlockFor(existing, beginMarker, endMarker, comment, patterns);
|
|
4655
4664
|
const range = findBlockRangeFor(existing, beginMarker, endMarker);
|
|
@@ -4742,7 +4751,7 @@ async function untrackMachineLocalArtifacts(root, dryRun, notify) {
|
|
|
4742
4751
|
for (const rel of tracked) {
|
|
4743
4752
|
if (dryRun) {
|
|
4744
4753
|
notify(
|
|
4745
|
-
`Would untrack ${rel} from git \u2014 it
|
|
4754
|
+
`Would untrack ${rel} from git \u2014 it is machine-local haus output and should never have been committed.`
|
|
4746
4755
|
);
|
|
4747
4756
|
continue;
|
|
4748
4757
|
}
|
|
@@ -4750,7 +4759,7 @@ async function untrackMachineLocalArtifacts(root, dryRun, notify) {
|
|
|
4750
4759
|
if (result.exitCode === 0) {
|
|
4751
4760
|
untracked.push(rel);
|
|
4752
4761
|
notify(
|
|
4753
|
-
`Untracked ${rel} from git \u2014 it
|
|
4762
|
+
`Untracked ${rel} from git \u2014 it is machine-local haus output and should never have been committed. Run \`git status\` to review before committing this change.`
|
|
4754
4763
|
);
|
|
4755
4764
|
} else {
|
|
4756
4765
|
notify(`Could not untrack ${rel} from git: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
@@ -7665,12 +7674,12 @@ async function runDoctor(options) {
|
|
|
7665
7674
|
const trackedArtifacts = await listTrackedArtifactPaths(root);
|
|
7666
7675
|
if (trackedArtifacts.length > 0) {
|
|
7667
7676
|
flag(
|
|
7668
|
-
`- GITIGNORE: ${trackedArtifacts.length} machine-local
|
|
7669
|
-
`${trackedArtifacts.length} machine-local
|
|
7677
|
+
`- GITIGNORE: ${trackedArtifacts.length} machine-local artifact(s) still tracked in git (${trackedArtifacts.join(", ")})`,
|
|
7678
|
+
`${trackedArtifacts.length} machine-local artifact(s) are still tracked in git`,
|
|
7670
7679
|
`git rm --cached ${trackedArtifacts.join(" ")} (or run \`haus apply --write\` to do this and update .gitignore for you)`
|
|
7671
7680
|
);
|
|
7672
7681
|
} else {
|
|
7673
|
-
ok("- GITIGNORE: OK (no machine-local
|
|
7682
|
+
ok("- GITIGNORE: OK (no machine-local artifacts tracked in git)");
|
|
7674
7683
|
}
|
|
7675
7684
|
const gitignoredHausPaths = await findGitignoredHausPaths(root);
|
|
7676
7685
|
if (gitignoredHausPaths.length > 0) {
|
|
@@ -8291,7 +8300,8 @@ function summarizeLockDiff(before, after) {
|
|
|
8291
8300
|
}
|
|
8292
8301
|
|
|
8293
8302
|
// src/update/lockfile.ts
|
|
8294
|
-
import {
|
|
8303
|
+
import { constants as fsConstants } from "node:fs";
|
|
8304
|
+
import { copyFile, mkdir, readFile as readFile2 } from "node:fs/promises";
|
|
8295
8305
|
import path47 from "node:path";
|
|
8296
8306
|
init_fs();
|
|
8297
8307
|
init_paths();
|
|
@@ -8325,16 +8335,30 @@ async function readLockRaw(root) {
|
|
|
8325
8335
|
return "[]";
|
|
8326
8336
|
}
|
|
8327
8337
|
}
|
|
8328
|
-
async function
|
|
8338
|
+
async function backupLockfile(root) {
|
|
8329
8339
|
const lockPath = hausPath(root, "haus.lock.json");
|
|
8330
|
-
const before = await readLockRaw(root);
|
|
8331
|
-
const lock = await readJson(lockPath) ?? [];
|
|
8332
8340
|
try {
|
|
8333
8341
|
const backupDir = hausPath(root, "backups");
|
|
8334
8342
|
await mkdir(backupDir, { recursive: true });
|
|
8335
|
-
|
|
8343
|
+
let stamp = Date.now();
|
|
8344
|
+
for (; ; ) {
|
|
8345
|
+
const dest = path47.join(backupDir, `haus.lock.${stamp}.json`);
|
|
8346
|
+
try {
|
|
8347
|
+
await copyFile(lockPath, dest, fsConstants.COPYFILE_EXCL);
|
|
8348
|
+
return;
|
|
8349
|
+
} catch (err) {
|
|
8350
|
+
if (err.code !== "EEXIST") throw err;
|
|
8351
|
+
stamp += 1;
|
|
8352
|
+
}
|
|
8353
|
+
}
|
|
8336
8354
|
} catch {
|
|
8337
8355
|
}
|
|
8356
|
+
}
|
|
8357
|
+
async function applyLock(root) {
|
|
8358
|
+
const lockPath = hausPath(root, "haus.lock.json");
|
|
8359
|
+
const before = await readLockRaw(root);
|
|
8360
|
+
const lock = await readJson(lockPath) ?? [];
|
|
8361
|
+
await backupLockfile(root);
|
|
8338
8362
|
const enriched = await Promise.all(
|
|
8339
8363
|
lock.map(async (x) => {
|
|
8340
8364
|
const paths = Array.isArray(x.paths) ? x.paths.map(String) : [];
|
|
@@ -8357,12 +8381,7 @@ async function removeLockEntries(root, ids) {
|
|
|
8357
8381
|
const removed = [...wanted].filter((id) => present.has(id));
|
|
8358
8382
|
const missing = [...wanted].filter((id) => !present.has(id));
|
|
8359
8383
|
if (removed.length === 0) return { removed, missing, before, after: before };
|
|
8360
|
-
|
|
8361
|
-
const backupDir = hausPath(root, "backups");
|
|
8362
|
-
await mkdir(backupDir, { recursive: true });
|
|
8363
|
-
await copyFile(lockPath, path47.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
8364
|
-
} catch {
|
|
8365
|
-
}
|
|
8384
|
+
await backupLockfile(root);
|
|
8366
8385
|
const survivors = lock.filter((item) => !wanted.has(item.id));
|
|
8367
8386
|
await writeJson(lockPath, survivors);
|
|
8368
8387
|
return { removed, missing, before, after: `${JSON.stringify(survivors, null, 2)}
|
|
@@ -10799,6 +10818,7 @@ async function runRecommend(options) {
|
|
|
10799
10818
|
}
|
|
10800
10819
|
|
|
10801
10820
|
// src/commands/remove.ts
|
|
10821
|
+
import path54 from "node:path";
|
|
10802
10822
|
init_fs();
|
|
10803
10823
|
init_logger();
|
|
10804
10824
|
init_paths();
|
|
@@ -10810,7 +10830,7 @@ async function runRemove(ids, options = {}) {
|
|
|
10810
10830
|
const dryRun = Boolean(options.dryRun);
|
|
10811
10831
|
const say = options.json ? () => {
|
|
10812
10832
|
} : log;
|
|
10813
|
-
const wanted = parseIdList(ids);
|
|
10833
|
+
const wanted = [...new Set(parseIdList(ids))];
|
|
10814
10834
|
if (wanted.length === 0) {
|
|
10815
10835
|
error("Specify at least one catalog item id \u2014 see `haus doctor` for what is installed.");
|
|
10816
10836
|
process.exitCode = 1;
|
|
@@ -10856,7 +10876,7 @@ async function runRemove(ids, options = {}) {
|
|
|
10856
10876
|
cleared.push({ id, existing: [] });
|
|
10857
10877
|
continue;
|
|
10858
10878
|
}
|
|
10859
|
-
const gate = await hashGateEntry(root, {
|
|
10879
|
+
const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
|
|
10860
10880
|
if (!gate.ok) {
|
|
10861
10881
|
const detail = gate.reason === "no-hash" ? "has no lock hash" : "was modified locally";
|
|
10862
10882
|
warn(
|
|
@@ -10902,7 +10922,8 @@ async function runRemove(ids, options = {}) {
|
|
|
10902
10922
|
}
|
|
10903
10923
|
}
|
|
10904
10924
|
const allAbs = cleared.flatMap(({ existing }) => existing.map(({ abs }) => abs));
|
|
10905
|
-
|
|
10925
|
+
const backupRoot = await backupTrackedPaths(root, allAbs, say, "remove");
|
|
10926
|
+
report.backupDir = backupRoot === null ? null : path54.basename(backupRoot);
|
|
10906
10927
|
for (const { id, existing } of cleared) {
|
|
10907
10928
|
await deleteTrackedPaths(root, id, existing, { verb: "Removed", reason: "" }, say);
|
|
10908
10929
|
}
|
|
@@ -10987,7 +11008,7 @@ async function runScan(options) {
|
|
|
10987
11008
|
}
|
|
10988
11009
|
|
|
10989
11010
|
// src/commands/undo.ts
|
|
10990
|
-
import
|
|
11011
|
+
import path55 from "node:path";
|
|
10991
11012
|
import fs40 from "fs-extra";
|
|
10992
11013
|
|
|
10993
11014
|
// src/claude/managed-paths.ts
|
|
@@ -11019,18 +11040,18 @@ async function collectLockTrackedPaths(root) {
|
|
|
11019
11040
|
const relPaths = row.paths ?? [];
|
|
11020
11041
|
const existingRel = [];
|
|
11021
11042
|
for (const rel of relPaths) {
|
|
11022
|
-
if (await fs40.pathExists(
|
|
11043
|
+
if (await fs40.pathExists(path55.resolve(root, rel))) existingRel.push(rel);
|
|
11023
11044
|
}
|
|
11024
11045
|
if (existingRel.length === 0) continue;
|
|
11025
11046
|
if (row.hash === void 0) {
|
|
11026
|
-
removable.push(...existingRel.map((rel) =>
|
|
11047
|
+
removable.push(...existingRel.map((rel) => path55.resolve(root, rel)));
|
|
11027
11048
|
continue;
|
|
11028
11049
|
}
|
|
11029
11050
|
const currentHash = await hashInstalledPaths(root, existingRel);
|
|
11030
11051
|
if (currentHash === row.hash) {
|
|
11031
|
-
removable.push(...existingRel.map((rel) =>
|
|
11052
|
+
removable.push(...existingRel.map((rel) => path55.resolve(root, rel)));
|
|
11032
11053
|
} else {
|
|
11033
|
-
preserved.push(...existingRel.map((rel) =>
|
|
11054
|
+
preserved.push(...existingRel.map((rel) => path55.resolve(root, rel)));
|
|
11034
11055
|
log(`Preserving locally-modified ${row.id ?? "(unknown item)"}: ${existingRel.join(", ")}`);
|
|
11035
11056
|
}
|
|
11036
11057
|
}
|
|
@@ -11043,7 +11064,7 @@ async function settingsHasHausContent(root) {
|
|
|
11043
11064
|
return settings._haus != null;
|
|
11044
11065
|
}
|
|
11045
11066
|
async function claudeMdHasHausBlock(root) {
|
|
11046
|
-
const filePath =
|
|
11067
|
+
const filePath = path55.join(root, "CLAUDE.md");
|
|
11047
11068
|
if (!await fs40.pathExists(filePath)) return false;
|
|
11048
11069
|
const text = await fs40.readFile(filePath, "utf8");
|
|
11049
11070
|
return text.includes(BLOCK_BEGIN);
|
|
@@ -11056,15 +11077,15 @@ async function stripProjectSettings(root) {
|
|
|
11056
11077
|
const hasContent = Object.keys(settings).length > 0;
|
|
11057
11078
|
if (hasContent) {
|
|
11058
11079
|
await writeProjectSettings(root, settings);
|
|
11059
|
-
log(`Stripped haus rules from ${
|
|
11080
|
+
log(`Stripped haus rules from ${path55.relative(root, settingsPath)} (user settings preserved).`);
|
|
11060
11081
|
return true;
|
|
11061
11082
|
}
|
|
11062
11083
|
await fs40.remove(settingsPath);
|
|
11063
|
-
log(`Removed ${
|
|
11084
|
+
log(`Removed ${path55.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
11064
11085
|
return true;
|
|
11065
11086
|
}
|
|
11066
11087
|
async function stripRootClaudeMd(root) {
|
|
11067
|
-
const filePath =
|
|
11088
|
+
const filePath = path55.join(root, "CLAUDE.md");
|
|
11068
11089
|
if (!await fs40.pathExists(filePath)) return false;
|
|
11069
11090
|
const prev = await fs40.readFile(filePath, "utf8");
|
|
11070
11091
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
@@ -11092,12 +11113,12 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
|
11092
11113
|
const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
|
|
11093
11114
|
for (const abs of managedAbsPaths) {
|
|
11094
11115
|
if (!await fs40.pathExists(abs)) continue;
|
|
11095
|
-
const rel =
|
|
11096
|
-
const backupPath =
|
|
11097
|
-
await fs40.ensureDir(
|
|
11116
|
+
const rel = path55.relative(root, abs);
|
|
11117
|
+
const backupPath = path55.join(backupRoot, rel);
|
|
11118
|
+
await fs40.ensureDir(path55.dirname(backupPath));
|
|
11098
11119
|
await fs40.copy(abs, backupPath);
|
|
11099
11120
|
}
|
|
11100
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
11121
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path55.relative(root, backupRoot)}.`);
|
|
11101
11122
|
}
|
|
11102
11123
|
async function runUndo(options) {
|
|
11103
11124
|
const root = options.root ?? process.cwd();
|
|
@@ -11110,7 +11131,7 @@ async function runUndo(options) {
|
|
|
11110
11131
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
11111
11132
|
return;
|
|
11112
11133
|
}
|
|
11113
|
-
const relTargets = managed.map((p) =>
|
|
11134
|
+
const relTargets = managed.map((p) => path55.relative(root, p));
|
|
11114
11135
|
const summaryParts = [...relTargets];
|
|
11115
11136
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
11116
11137
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -11140,7 +11161,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
11140
11161
|
for (const abs of managed) {
|
|
11141
11162
|
if (!await fs40.pathExists(abs)) continue;
|
|
11142
11163
|
await fs40.remove(abs);
|
|
11143
|
-
log(`Removed ${
|
|
11164
|
+
log(`Removed ${path55.relative(root, abs)}`);
|
|
11144
11165
|
}
|
|
11145
11166
|
if (stripSettings) await stripProjectSettings(root);
|
|
11146
11167
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -11154,7 +11175,7 @@ init_fs();
|
|
|
11154
11175
|
init_logger();
|
|
11155
11176
|
init_paths();
|
|
11156
11177
|
import crypto3 from "node:crypto";
|
|
11157
|
-
import
|
|
11178
|
+
import path56 from "node:path";
|
|
11158
11179
|
import fs41 from "fs-extra";
|
|
11159
11180
|
async function runUninstall(options = {}) {
|
|
11160
11181
|
const { force = false, removePlugins = removeGlobalPlugins } = options;
|
|
@@ -11211,14 +11232,14 @@ async function runUninstall(options = {}) {
|
|
|
11211
11232
|
continue;
|
|
11212
11233
|
}
|
|
11213
11234
|
await fs41.remove(entry.destPath);
|
|
11214
|
-
await pruneEmptyDir(
|
|
11235
|
+
await pruneEmptyDir(path56.dirname(entry.destPath));
|
|
11215
11236
|
result.deleted.push(entry.destPath);
|
|
11216
11237
|
}
|
|
11217
11238
|
const settings = await readSettings();
|
|
11218
11239
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
11219
11240
|
await writeSettings(stripped);
|
|
11220
11241
|
result.hooksStripped = true;
|
|
11221
|
-
const hausDir =
|
|
11242
|
+
const hausDir = path56.join(globalClaudeDir(), "haus");
|
|
11222
11243
|
const manifestPath2 = hausManifestPath();
|
|
11223
11244
|
if (fs41.pathExistsSync(manifestPath2)) {
|
|
11224
11245
|
await fs41.remove(manifestPath2);
|
|
@@ -11295,7 +11316,7 @@ async function runUninstallCommand(options) {
|
|
|
11295
11316
|
}
|
|
11296
11317
|
|
|
11297
11318
|
// src/commands/validate-catalog.ts
|
|
11298
|
-
import
|
|
11319
|
+
import path58 from "node:path";
|
|
11299
11320
|
|
|
11300
11321
|
// src/catalog/validate-core.ts
|
|
11301
11322
|
init_forbidden_content();
|
|
@@ -11303,7 +11324,7 @@ init_manifest_item_fields();
|
|
|
11303
11324
|
init_path_safety();
|
|
11304
11325
|
init_validation_rules2();
|
|
11305
11326
|
import fs42 from "node:fs";
|
|
11306
|
-
import
|
|
11327
|
+
import path57 from "node:path";
|
|
11307
11328
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
11308
11329
|
var ITEM_ID_PATTERN = /^haus\./;
|
|
11309
11330
|
function auditForbiddenStacks(items) {
|
|
@@ -11454,7 +11475,7 @@ function readForAudit(absPath, label, failures) {
|
|
|
11454
11475
|
}
|
|
11455
11476
|
}
|
|
11456
11477
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
11457
|
-
const rel =
|
|
11478
|
+
const rel = path57.relative(manifestDir, absPath);
|
|
11458
11479
|
const failures = [];
|
|
11459
11480
|
const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
|
|
11460
11481
|
if (text === null) return failures;
|
|
@@ -11476,25 +11497,25 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
11476
11497
|
const failures = [];
|
|
11477
11498
|
for (const item of items) {
|
|
11478
11499
|
if (!item.path || !isSafeCatalogPath(item.path)) continue;
|
|
11479
|
-
const absPath =
|
|
11500
|
+
const absPath = path57.join(manifestDir, item.path);
|
|
11480
11501
|
if (item.type === "skill") {
|
|
11481
|
-
const skillMd =
|
|
11502
|
+
const skillMd = path57.join(absPath, "SKILL.md");
|
|
11482
11503
|
if (!fs42.existsSync(skillMd)) {
|
|
11483
|
-
failures.push(`${item.id}: missing ${
|
|
11504
|
+
failures.push(`${item.id}: missing ${path57.relative(manifestDir, skillMd)}`);
|
|
11484
11505
|
continue;
|
|
11485
11506
|
}
|
|
11486
11507
|
const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
|
|
11487
11508
|
if (text === null) continue;
|
|
11488
11509
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
11489
11510
|
failures.push(
|
|
11490
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
11511
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path57.relative(manifestDir, skillMd)}`)
|
|
11491
11512
|
);
|
|
11492
11513
|
} else if (item.type === "agent") {
|
|
11493
11514
|
if (!fs42.existsSync(absPath)) {
|
|
11494
11515
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
11495
11516
|
continue;
|
|
11496
11517
|
}
|
|
11497
|
-
const rel =
|
|
11518
|
+
const rel = path57.relative(manifestDir, absPath);
|
|
11498
11519
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
11499
11520
|
if (text === null) continue;
|
|
11500
11521
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -11510,7 +11531,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
11510
11531
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
11511
11532
|
continue;
|
|
11512
11533
|
}
|
|
11513
|
-
const rel =
|
|
11534
|
+
const rel = path57.relative(manifestDir, absPath);
|
|
11514
11535
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
11515
11536
|
if (text === null) continue;
|
|
11516
11537
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -11568,7 +11589,7 @@ function walkAllFiles(dir, fn, failures) {
|
|
|
11568
11589
|
}
|
|
11569
11590
|
for (const entry of entries) {
|
|
11570
11591
|
if (entry.name === ".DS_Store") continue;
|
|
11571
|
-
const full =
|
|
11592
|
+
const full = path57.join(dir, entry.name);
|
|
11572
11593
|
if (entry.isDirectory()) walkAllFiles(full, fn, failures);
|
|
11573
11594
|
else fn(full);
|
|
11574
11595
|
}
|
|
@@ -11592,12 +11613,12 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
11592
11613
|
const failures = [];
|
|
11593
11614
|
const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
|
|
11594
11615
|
for (const dir of CONTENT_ROOTS) {
|
|
11595
|
-
const abs =
|
|
11616
|
+
const abs = path57.join(manifestDir, dir);
|
|
11596
11617
|
if (!fs42.existsSync(abs)) continue;
|
|
11597
11618
|
walkAllFiles(
|
|
11598
11619
|
abs,
|
|
11599
11620
|
(file) => {
|
|
11600
|
-
const relPath =
|
|
11621
|
+
const relPath = path57.relative(manifestDir, file).replace(/\\/g, "/");
|
|
11601
11622
|
if (!isPathClaimed(relPath, claimedPaths)) {
|
|
11602
11623
|
failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
|
|
11603
11624
|
}
|
|
@@ -11618,7 +11639,7 @@ function walkMd(dir, fn, failures) {
|
|
|
11618
11639
|
return;
|
|
11619
11640
|
}
|
|
11620
11641
|
for (const entry of entries) {
|
|
11621
|
-
const full =
|
|
11642
|
+
const full = path57.join(dir, entry.name);
|
|
11622
11643
|
if (entry.isDirectory()) walkMd(full, fn, failures);
|
|
11623
11644
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
11624
11645
|
}
|
|
@@ -11628,13 +11649,13 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
11628
11649
|
const pathSourceMap = buildItemPathSourceMap(items);
|
|
11629
11650
|
const pathTypeMap = buildItemPathTypeMap(items);
|
|
11630
11651
|
for (const dir of ["skills", "agents", "templates", "commands", "sources"]) {
|
|
11631
|
-
const abs =
|
|
11652
|
+
const abs = path57.join(manifestDir, dir);
|
|
11632
11653
|
if (!fs42.existsSync(abs)) continue;
|
|
11633
11654
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
11634
11655
|
walkMd(
|
|
11635
11656
|
abs,
|
|
11636
11657
|
(file) => {
|
|
11637
|
-
const rel =
|
|
11658
|
+
const rel = path57.relative(manifestDir, file);
|
|
11638
11659
|
const text = readForAudit(file, rel, failures);
|
|
11639
11660
|
if (text === null) return;
|
|
11640
11661
|
const norm = rel.replace(/\\/g, "/");
|
|
@@ -11682,8 +11703,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
11682
11703
|
process.exitCode = 1;
|
|
11683
11704
|
return;
|
|
11684
11705
|
}
|
|
11685
|
-
const abs =
|
|
11686
|
-
const manifestDir =
|
|
11706
|
+
const abs = path58.resolve(process.cwd(), manifestPath2);
|
|
11707
|
+
const manifestDir = path58.dirname(abs);
|
|
11687
11708
|
const data = await readJson(abs);
|
|
11688
11709
|
if (!data?.items) {
|
|
11689
11710
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -11704,7 +11725,7 @@ init_logger();
|
|
|
11704
11725
|
|
|
11705
11726
|
// src/workspace/worktree/add.ts
|
|
11706
11727
|
init_fs();
|
|
11707
|
-
import
|
|
11728
|
+
import path65 from "node:path";
|
|
11708
11729
|
import fs46 from "fs-extra";
|
|
11709
11730
|
|
|
11710
11731
|
// src/workspace/worktree/git-worktree.ts
|
|
@@ -11825,12 +11846,12 @@ async function hasUnpushedWork(cwd) {
|
|
|
11825
11846
|
}
|
|
11826
11847
|
|
|
11827
11848
|
// src/workspace/worktree/hydrate.ts
|
|
11828
|
-
import
|
|
11849
|
+
import path61 from "node:path";
|
|
11829
11850
|
import fs44 from "fs-extra";
|
|
11830
11851
|
|
|
11831
11852
|
// src/workspace/worktree/cow-copy.ts
|
|
11832
11853
|
init_exec();
|
|
11833
|
-
import
|
|
11854
|
+
import path59 from "node:path";
|
|
11834
11855
|
import fs43 from "fs-extra";
|
|
11835
11856
|
async function getMacFilesystemType(dir) {
|
|
11836
11857
|
const df = await runCommand("df", ["-P", dir]);
|
|
@@ -11865,11 +11886,11 @@ async function cowCopyDir(src, dest) {
|
|
|
11865
11886
|
const srcExists = await fs43.pathExists(src);
|
|
11866
11887
|
if (!srcExists)
|
|
11867
11888
|
return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
|
|
11868
|
-
const strategy = await detectCowStrategy(
|
|
11889
|
+
const strategy = await detectCowStrategy(path59.dirname(dest));
|
|
11869
11890
|
if (strategy === "unsupported" || strategy === "unknown-platform") {
|
|
11870
11891
|
return { strategy, attempted: false, ok: false };
|
|
11871
11892
|
}
|
|
11872
|
-
await fs43.ensureDir(
|
|
11893
|
+
await fs43.ensureDir(path59.dirname(dest));
|
|
11873
11894
|
const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
|
|
11874
11895
|
const result = await runCommand("cp", args);
|
|
11875
11896
|
return {
|
|
@@ -11883,7 +11904,7 @@ async function cowCopyDir(src, dest) {
|
|
|
11883
11904
|
// src/workspace/worktree/install.ts
|
|
11884
11905
|
init_exec();
|
|
11885
11906
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
11886
|
-
import
|
|
11907
|
+
import path60 from "node:path";
|
|
11887
11908
|
import fg6 from "fast-glob";
|
|
11888
11909
|
function detectInstallPlan(signals) {
|
|
11889
11910
|
if (signals.hasYarnLock) {
|
|
@@ -11904,10 +11925,10 @@ function detectInstallPlan(signals) {
|
|
|
11904
11925
|
return { manager: "none" };
|
|
11905
11926
|
}
|
|
11906
11927
|
async function computeLockfileSignals(dir) {
|
|
11907
|
-
const hasYarnLock = existsSync3(
|
|
11908
|
-
const hasPnpmLock = existsSync3(
|
|
11909
|
-
const hasPackageLock = existsSync3(
|
|
11910
|
-
const hasComposerLock = existsSync3(
|
|
11928
|
+
const hasYarnLock = existsSync3(path60.join(dir, "yarn.lock"));
|
|
11929
|
+
const hasPnpmLock = existsSync3(path60.join(dir, "pnpm-lock.yaml"));
|
|
11930
|
+
const hasPackageLock = existsSync3(path60.join(dir, "package-lock.json"));
|
|
11931
|
+
const hasComposerLock = existsSync3(path60.join(dir, "composer.lock"));
|
|
11911
11932
|
const dotnetFiles = await fg6(["**/*.csproj", "**/*.sln"], {
|
|
11912
11933
|
cwd: dir,
|
|
11913
11934
|
onlyFiles: true,
|
|
@@ -11921,7 +11942,7 @@ async function computeLockfileSignals(dir) {
|
|
|
11921
11942
|
});
|
|
11922
11943
|
const hasDotnetProject = dotnetFiles.length > 0;
|
|
11923
11944
|
let hasPackageManagerField = false;
|
|
11924
|
-
const pkgPath =
|
|
11945
|
+
const pkgPath = path60.join(dir, "package.json");
|
|
11925
11946
|
if (existsSync3(pkgPath)) {
|
|
11926
11947
|
try {
|
|
11927
11948
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
@@ -11964,7 +11985,7 @@ function missingYarnrcEnvVars(content2, env) {
|
|
|
11964
11985
|
return findRequiredYarnrcEnvVars(content2).filter((name) => env[name] === void 0);
|
|
11965
11986
|
}
|
|
11966
11987
|
function missingYarnrcEnvVarsInDir(dir, env) {
|
|
11967
|
-
const yarnrcPath =
|
|
11988
|
+
const yarnrcPath = path60.join(dir, ".yarnrc.yml");
|
|
11968
11989
|
if (!existsSync3(yarnrcPath)) return [];
|
|
11969
11990
|
try {
|
|
11970
11991
|
return missingYarnrcEnvVars(readFileSync3(yarnrcPath, "utf8"), env);
|
|
@@ -12054,8 +12075,8 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
12054
12075
|
const start = Date.now();
|
|
12055
12076
|
const targets = [];
|
|
12056
12077
|
for (const target of HYDRATION_TARGETS) {
|
|
12057
|
-
const src =
|
|
12058
|
-
const dest =
|
|
12078
|
+
const src = path61.join(mainMemberPath, target);
|
|
12079
|
+
const dest = path61.join(worktreeMemberPath, target);
|
|
12059
12080
|
if (!await fs44.pathExists(src)) continue;
|
|
12060
12081
|
const destExists = await fs44.pathExists(dest);
|
|
12061
12082
|
if (!opts.force && destExists) continue;
|
|
@@ -12073,11 +12094,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
12073
12094
|
|
|
12074
12095
|
// src/workspace/worktree/local-files.ts
|
|
12075
12096
|
init_exec();
|
|
12076
|
-
import
|
|
12097
|
+
import path62 from "node:path";
|
|
12077
12098
|
import fs45 from "fs-extra";
|
|
12078
12099
|
var MACHINE_LOCAL_FILES = [
|
|
12079
12100
|
"docker-compose.local.yml",
|
|
12080
|
-
|
|
12101
|
+
path62.join(".claude", "settings.local.json")
|
|
12081
12102
|
];
|
|
12082
12103
|
async function isTracked(repoRoot, relFile) {
|
|
12083
12104
|
const gitPath = relFile.replace(/\\/g, "/");
|
|
@@ -12087,11 +12108,11 @@ async function isTracked(repoRoot, relFile) {
|
|
|
12087
12108
|
async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
12088
12109
|
const copied = [];
|
|
12089
12110
|
for (const rel of MACHINE_LOCAL_FILES) {
|
|
12090
|
-
const src =
|
|
12111
|
+
const src = path62.join(mainMemberPath, rel);
|
|
12091
12112
|
if (!await fs45.pathExists(src)) continue;
|
|
12092
12113
|
if (await isTracked(mainMemberPath, rel)) continue;
|
|
12093
|
-
const dest =
|
|
12094
|
-
await fs45.ensureDir(
|
|
12114
|
+
const dest = path62.join(worktreeMemberPath, rel);
|
|
12115
|
+
await fs45.ensureDir(path62.dirname(dest));
|
|
12095
12116
|
await fs45.copy(src, dest, { overwrite: true });
|
|
12096
12117
|
copied.push(rel);
|
|
12097
12118
|
}
|
|
@@ -12100,15 +12121,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
|
12100
12121
|
|
|
12101
12122
|
// src/workspace/worktree/root.ts
|
|
12102
12123
|
import { existsSync as existsSync4 } from "node:fs";
|
|
12103
|
-
import
|
|
12124
|
+
import path63 from "node:path";
|
|
12104
12125
|
async function resolveWorkspaceForWorktree(start) {
|
|
12105
12126
|
const rootInfo = await resolveRoots(start);
|
|
12106
12127
|
if (!rootInfo.isGitRepo) {
|
|
12107
12128
|
return { ok: false, reason: "Not inside a git repository." };
|
|
12108
12129
|
}
|
|
12109
12130
|
const workspaceRoot = rootInfo.mainRoot;
|
|
12110
|
-
const hasYaml = existsSync4(
|
|
12111
|
-
const hasManifest = existsSync4(
|
|
12131
|
+
const hasYaml = existsSync4(path63.join(workspaceRoot, WORKSPACE_FILE));
|
|
12132
|
+
const hasManifest = existsSync4(path63.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
12112
12133
|
if (!hasYaml && !hasManifest) {
|
|
12113
12134
|
return {
|
|
12114
12135
|
ok: false,
|
|
@@ -12134,10 +12155,10 @@ function selectMembers(members, only) {
|
|
|
12134
12155
|
|
|
12135
12156
|
// src/workspace/worktree/state.ts
|
|
12136
12157
|
init_fs();
|
|
12137
|
-
import
|
|
12158
|
+
import path64 from "node:path";
|
|
12138
12159
|
var WORKTREE_STATE_FILE = ".haus-worktree.json";
|
|
12139
12160
|
function worktreesDir(mainRoot) {
|
|
12140
|
-
return
|
|
12161
|
+
return path64.join(mainRoot, ".claude", "worktrees");
|
|
12141
12162
|
}
|
|
12142
12163
|
function isValidWorktreeSlug(slug) {
|
|
12143
12164
|
if (slug === "" || slug === "." || slug === "..") return false;
|
|
@@ -12150,15 +12171,15 @@ function worktreePath(mainRoot, slug) {
|
|
|
12150
12171
|
`Invalid worktree slug ${JSON.stringify(slug)} \u2014 slugs may only contain letters, digits, ".", "_", "-" (no path separators, and not "." or "..").`
|
|
12151
12172
|
);
|
|
12152
12173
|
}
|
|
12153
|
-
return
|
|
12174
|
+
return path64.join(worktreesDir(mainRoot), slug);
|
|
12154
12175
|
}
|
|
12155
12176
|
async function readWorktreeState(worktreeRoot) {
|
|
12156
|
-
const state = await readJson(
|
|
12177
|
+
const state = await readJson(path64.join(worktreeRoot, WORKTREE_STATE_FILE));
|
|
12157
12178
|
if (state == null || typeof state !== "object" || !Array.isArray(state.members)) return void 0;
|
|
12158
12179
|
return state;
|
|
12159
12180
|
}
|
|
12160
12181
|
async function writeWorktreeState(worktreeRoot, state) {
|
|
12161
|
-
await writeJson(
|
|
12182
|
+
await writeJson(path64.join(worktreeRoot, WORKTREE_STATE_FILE), state);
|
|
12162
12183
|
}
|
|
12163
12184
|
|
|
12164
12185
|
// src/workspace/worktree/add.ts
|
|
@@ -12192,7 +12213,7 @@ async function runAdd(opts) {
|
|
|
12192
12213
|
const memberResults = [];
|
|
12193
12214
|
for (const member of selected) {
|
|
12194
12215
|
const start = Date.now();
|
|
12195
|
-
const memberWorktreePath =
|
|
12216
|
+
const memberWorktreePath = path65.join(wsWorktreePath, member.folder);
|
|
12196
12217
|
const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
|
|
12197
12218
|
preferDefaultBranchFrom: true
|
|
12198
12219
|
});
|
|
@@ -12222,7 +12243,7 @@ async function runAdd(opts) {
|
|
|
12222
12243
|
await mapWithConcurrency(okResults, async (result) => {
|
|
12223
12244
|
const member = selected.find((m) => m.id === result.member);
|
|
12224
12245
|
if (!member) return;
|
|
12225
|
-
const memberWorktreePath =
|
|
12246
|
+
const memberWorktreePath = path65.join(wsWorktreePath, member.folder);
|
|
12226
12247
|
const hydrationStart = Date.now();
|
|
12227
12248
|
if (opts.hydrate !== false) {
|
|
12228
12249
|
result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
|
|
@@ -12289,25 +12310,25 @@ async function runDryRun(args) {
|
|
|
12289
12310
|
|
|
12290
12311
|
// src/workspace/worktree/doctor.ts
|
|
12291
12312
|
import { existsSync as existsSync6 } from "node:fs";
|
|
12292
|
-
import
|
|
12313
|
+
import path67 from "node:path";
|
|
12293
12314
|
|
|
12294
12315
|
// src/workspace/worktree/orphans.ts
|
|
12295
12316
|
import { existsSync as existsSync5 } from "node:fs";
|
|
12296
|
-
import
|
|
12317
|
+
import path66 from "node:path";
|
|
12297
12318
|
var WORKSPACE_REPO_ID = "(workspace)";
|
|
12298
12319
|
async function findOrphanedRegistrations(repos, wsWorktreesDir) {
|
|
12299
12320
|
const orphans = [];
|
|
12300
12321
|
for (const repo of repos) {
|
|
12301
12322
|
const entries = await listWorktrees(repo.absPath);
|
|
12302
12323
|
for (const entry of entries) {
|
|
12303
|
-
const entryPath =
|
|
12304
|
-
const marker = `${
|
|
12324
|
+
const entryPath = path66.normalize(entry.path);
|
|
12325
|
+
const marker = `${path66.sep}.claude${path66.sep}worktrees${path66.sep}`;
|
|
12305
12326
|
const idx = entryPath.indexOf(marker);
|
|
12306
12327
|
if (idx === -1) continue;
|
|
12307
12328
|
const rest = entryPath.slice(idx + marker.length);
|
|
12308
|
-
const slug = rest.split(
|
|
12329
|
+
const slug = rest.split(path66.sep)[0];
|
|
12309
12330
|
if (!slug) continue;
|
|
12310
|
-
if (existsSync5(
|
|
12331
|
+
if (existsSync5(path66.join(wsWorktreesDir, slug))) continue;
|
|
12311
12332
|
orphans.push({
|
|
12312
12333
|
repoId: repo.id,
|
|
12313
12334
|
ownerRepo: repo.absPath,
|
|
@@ -12322,7 +12343,7 @@ async function findOrphanedRegistrations(repos, wsWorktreesDir) {
|
|
|
12322
12343
|
// src/workspace/worktree/doctor.ts
|
|
12323
12344
|
var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
|
|
12324
12345
|
function looksHydrated(memberPath) {
|
|
12325
|
-
return HYDRATION_MARKERS.some((marker) => existsSync6(
|
|
12346
|
+
return HYDRATION_MARKERS.some((marker) => existsSync6(path67.join(memberPath, marker)));
|
|
12326
12347
|
}
|
|
12327
12348
|
async function runDoctor2() {
|
|
12328
12349
|
const rootInfo = await resolveRoots();
|
|
@@ -12338,7 +12359,7 @@ async function runDoctor2() {
|
|
|
12338
12359
|
};
|
|
12339
12360
|
}
|
|
12340
12361
|
const workspaceRoot = rootInfo.mainRoot;
|
|
12341
|
-
const isWorkspace = existsSync6(
|
|
12362
|
+
const isWorkspace = existsSync6(path67.join(workspaceRoot, WORKSPACE_FILE)) || existsSync6(path67.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
12342
12363
|
if (!isWorkspace) {
|
|
12343
12364
|
return {
|
|
12344
12365
|
isWorkspace: false,
|
|
@@ -12353,7 +12374,7 @@ async function runDoctor2() {
|
|
|
12353
12374
|
};
|
|
12354
12375
|
}
|
|
12355
12376
|
const wsWorktreesDir = worktreesDir(workspaceRoot);
|
|
12356
|
-
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${
|
|
12377
|
+
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path67.sep}`);
|
|
12357
12378
|
const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
|
|
12358
12379
|
const workspaceConfig = await readWorkspaceConfig(workspaceRoot);
|
|
12359
12380
|
const autoHydrate = workspaceConfig?.worktree?.autoHydrate === true;
|
|
@@ -12364,10 +12385,10 @@ async function runDoctor2() {
|
|
|
12364
12385
|
let slug;
|
|
12365
12386
|
if (inWorkspaceWorktree) {
|
|
12366
12387
|
const wtPath = rootInfo.repoRoot;
|
|
12367
|
-
slug = rootInfo.worktreeName ??
|
|
12388
|
+
slug = rootInfo.worktreeName ?? path67.basename(wtPath);
|
|
12368
12389
|
const state = await readWorktreeState(wtPath);
|
|
12369
12390
|
for (const member of members) {
|
|
12370
|
-
const memberPath =
|
|
12391
|
+
const memberPath = path67.join(wtPath, member.folder);
|
|
12371
12392
|
const materialized = existsSync6(memberPath);
|
|
12372
12393
|
const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
|
|
12373
12394
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
@@ -12407,7 +12428,7 @@ async function runDoctor2() {
|
|
|
12407
12428
|
wsWorktreesDir
|
|
12408
12429
|
);
|
|
12409
12430
|
const orphans = orphanScan.map(
|
|
12410
|
-
(o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${
|
|
12431
|
+
(o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${path67.join(wsWorktreesDir, o.slug)} no longer exists.`
|
|
12411
12432
|
);
|
|
12412
12433
|
if (orphans.length > 0) {
|
|
12413
12434
|
problems.push(...orphans);
|
|
@@ -12428,7 +12449,7 @@ async function runDoctor2() {
|
|
|
12428
12449
|
// src/workspace/worktree/hydrate-command.ts
|
|
12429
12450
|
init_fs();
|
|
12430
12451
|
import { existsSync as existsSync7 } from "node:fs";
|
|
12431
|
-
import
|
|
12452
|
+
import path68 from "node:path";
|
|
12432
12453
|
async function runHydrateWorktree(opts) {
|
|
12433
12454
|
const rootInfo = await resolveRoots();
|
|
12434
12455
|
if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
|
|
@@ -12439,7 +12460,7 @@ async function runHydrateWorktree(opts) {
|
|
|
12439
12460
|
}
|
|
12440
12461
|
const workspaceRoot = rootInfo.mainRoot;
|
|
12441
12462
|
const wtPath = rootInfo.repoRoot;
|
|
12442
|
-
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${
|
|
12463
|
+
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path68.sep}`)) {
|
|
12443
12464
|
return {
|
|
12444
12465
|
ok: false,
|
|
12445
12466
|
error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
|
|
@@ -12451,13 +12472,13 @@ async function runHydrateWorktree(opts) {
|
|
|
12451
12472
|
return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
|
|
12452
12473
|
}
|
|
12453
12474
|
const state = await readWorktreeState(wtPath);
|
|
12454
|
-
const slug = state?.slug ??
|
|
12475
|
+
const slug = state?.slug ?? path68.basename(wtPath);
|
|
12455
12476
|
const detectedBranch = await currentBranch(wtPath);
|
|
12456
12477
|
const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
|
|
12457
12478
|
if (opts.dryRun) {
|
|
12458
12479
|
const results2 = await Promise.all(
|
|
12459
12480
|
selected.map(async (member) => {
|
|
12460
|
-
const memberPath =
|
|
12481
|
+
const memberPath = path68.join(wtPath, member.folder);
|
|
12461
12482
|
if (existsSync7(memberPath)) {
|
|
12462
12483
|
return { member: member.id, folder: member.folder, status: "planned" };
|
|
12463
12484
|
}
|
|
@@ -12478,7 +12499,7 @@ async function runHydrateWorktree(opts) {
|
|
|
12478
12499
|
const results = await mapWithConcurrency(
|
|
12479
12500
|
selected,
|
|
12480
12501
|
async (member) => {
|
|
12481
|
-
const memberPath =
|
|
12502
|
+
const memberPath = path68.join(wtPath, member.folder);
|
|
12482
12503
|
let branchAction;
|
|
12483
12504
|
if (!existsSync7(memberPath)) {
|
|
12484
12505
|
if (!opts.materialize) {
|
|
@@ -12541,7 +12562,7 @@ async function runHydrateWorktree(opts) {
|
|
|
12541
12562
|
}
|
|
12542
12563
|
|
|
12543
12564
|
// src/workspace/worktree/list.ts
|
|
12544
|
-
import
|
|
12565
|
+
import path69 from "node:path";
|
|
12545
12566
|
import fs47 from "fs-extra";
|
|
12546
12567
|
async function runList2() {
|
|
12547
12568
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -12554,12 +12575,12 @@ async function runList2() {
|
|
|
12554
12575
|
const configuredMembers = await readMembers(rootInfo);
|
|
12555
12576
|
const worktrees = [];
|
|
12556
12577
|
for (const slug of slugs) {
|
|
12557
|
-
const wtPath =
|
|
12578
|
+
const wtPath = path69.join(dir, slug);
|
|
12558
12579
|
const state = await readWorktreeState(wtPath);
|
|
12559
12580
|
const members = [];
|
|
12560
12581
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
12561
12582
|
for (const m of source) {
|
|
12562
|
-
const memberPath =
|
|
12583
|
+
const memberPath = path69.join(wtPath, m.folder);
|
|
12563
12584
|
const materialized = await fs47.pathExists(memberPath);
|
|
12564
12585
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
12565
12586
|
members.push({
|
|
@@ -12621,7 +12642,7 @@ async function runPrune2(opts) {
|
|
|
12621
12642
|
}
|
|
12622
12643
|
|
|
12623
12644
|
// src/workspace/worktree/remove.ts
|
|
12624
|
-
import
|
|
12645
|
+
import path70 from "node:path";
|
|
12625
12646
|
import fs48 from "fs-extra";
|
|
12626
12647
|
async function runRemove2(opts) {
|
|
12627
12648
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -12648,7 +12669,7 @@ async function runRemove2(opts) {
|
|
|
12648
12669
|
targets.push({
|
|
12649
12670
|
repo: configMember?.id ?? stateMember.id,
|
|
12650
12671
|
ownerRepo,
|
|
12651
|
-
path:
|
|
12672
|
+
path: path70.join(wsWorktreePath, stateMember.folder)
|
|
12652
12673
|
});
|
|
12653
12674
|
}
|
|
12654
12675
|
const existingTargets = [];
|
|
@@ -12986,7 +13007,7 @@ async function runWorktree(action, opts = {}) {
|
|
|
12986
13007
|
|
|
12987
13008
|
// src/commands/workspace.ts
|
|
12988
13009
|
import { existsSync as existsSync11, statSync as statSync3 } from "node:fs";
|
|
12989
|
-
import
|
|
13010
|
+
import path79 from "node:path";
|
|
12990
13011
|
init_fs();
|
|
12991
13012
|
init_logger();
|
|
12992
13013
|
|
|
@@ -13045,7 +13066,7 @@ ${summaries.map(
|
|
|
13045
13066
|
|
|
13046
13067
|
// src/commands/workspace/doctor.ts
|
|
13047
13068
|
import { existsSync as existsSync8 } from "node:fs";
|
|
13048
|
-
import
|
|
13069
|
+
import path72 from "node:path";
|
|
13049
13070
|
init_logger();
|
|
13050
13071
|
init_paths();
|
|
13051
13072
|
|
|
@@ -13053,14 +13074,14 @@ init_paths();
|
|
|
13053
13074
|
init_fs();
|
|
13054
13075
|
init_paths();
|
|
13055
13076
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
13056
|
-
import
|
|
13077
|
+
import path71 from "node:path";
|
|
13057
13078
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
13058
13079
|
function manifestPath(workspaceRoot) {
|
|
13059
13080
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
13060
13081
|
}
|
|
13061
13082
|
function hausVersion() {
|
|
13062
13083
|
try {
|
|
13063
|
-
const pkg = JSON.parse(readFileSync4(
|
|
13084
|
+
const pkg = JSON.parse(readFileSync4(path71.join(packageRoot(), "package.json"), "utf8"));
|
|
13064
13085
|
return pkg.version ?? "0.0.0";
|
|
13065
13086
|
} catch {
|
|
13066
13087
|
return "0.0.0";
|
|
@@ -13152,7 +13173,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
13152
13173
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
13153
13174
|
const catalogRefByRepo = [];
|
|
13154
13175
|
for (const repo of config.repos) {
|
|
13155
|
-
const repoRoot =
|
|
13176
|
+
const repoRoot = path72.resolve(workspaceRoot, repo.path);
|
|
13156
13177
|
const entry = manifestByName.get(repo.name);
|
|
13157
13178
|
if (!entry) {
|
|
13158
13179
|
flag({
|
|
@@ -13225,7 +13246,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
13225
13246
|
});
|
|
13226
13247
|
continue;
|
|
13227
13248
|
}
|
|
13228
|
-
if (!existsSync8(
|
|
13249
|
+
if (!existsSync8(path72.join(workspaceRoot, entry.path))) {
|
|
13229
13250
|
flag({
|
|
13230
13251
|
repo: entry.repo,
|
|
13231
13252
|
kind: "missing-linked-context-copy",
|
|
@@ -13271,17 +13292,17 @@ function emit(args) {
|
|
|
13271
13292
|
init_logger();
|
|
13272
13293
|
|
|
13273
13294
|
// src/workspace/link-context/link.ts
|
|
13274
|
-
import
|
|
13295
|
+
import path75 from "node:path";
|
|
13275
13296
|
import fs51 from "fs-extra";
|
|
13276
13297
|
init_fs();
|
|
13277
13298
|
init_logger();
|
|
13278
13299
|
|
|
13279
13300
|
// src/workspace/link-context/plan.ts
|
|
13280
|
-
import
|
|
13301
|
+
import path74 from "node:path";
|
|
13281
13302
|
import fs50 from "fs-extra";
|
|
13282
13303
|
|
|
13283
13304
|
// src/workspace/link-context/scan-source.ts
|
|
13284
|
-
import
|
|
13305
|
+
import path73 from "node:path";
|
|
13285
13306
|
import fs49 from "fs-extra";
|
|
13286
13307
|
var ASSET_SUBDIR = {
|
|
13287
13308
|
skill: "skills",
|
|
@@ -13290,36 +13311,36 @@ var ASSET_SUBDIR = {
|
|
|
13290
13311
|
};
|
|
13291
13312
|
async function hasLinkableContext(member) {
|
|
13292
13313
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
13293
|
-
if (await fs49.pathExists(
|
|
13314
|
+
if (await fs49.pathExists(path73.join(member.absPath, ".claude", subdir))) return true;
|
|
13294
13315
|
}
|
|
13295
13316
|
return false;
|
|
13296
13317
|
}
|
|
13297
13318
|
async function collectSkills(member) {
|
|
13298
|
-
const dir =
|
|
13319
|
+
const dir = path73.join(member.absPath, ".claude", "skills");
|
|
13299
13320
|
if (!await fs49.pathExists(dir)) return [];
|
|
13300
13321
|
const names = await fs49.readdir(dir);
|
|
13301
13322
|
const out = [];
|
|
13302
13323
|
for (const name of [...names].sort()) {
|
|
13303
|
-
const abs =
|
|
13324
|
+
const abs = path73.join(dir, name);
|
|
13304
13325
|
const stat = await fs49.lstat(abs);
|
|
13305
13326
|
if (!stat.isDirectory()) continue;
|
|
13306
|
-
if (!await fs49.pathExists(
|
|
13307
|
-
out.push({ type: "skill", name, sourceRelPath:
|
|
13327
|
+
if (!await fs49.pathExists(path73.join(abs, "SKILL.md"))) continue;
|
|
13328
|
+
out.push({ type: "skill", name, sourceRelPath: path73.posix.join(".claude", "skills", name) });
|
|
13308
13329
|
}
|
|
13309
13330
|
return out;
|
|
13310
13331
|
}
|
|
13311
13332
|
async function collectFlatMarkdown(member, type) {
|
|
13312
13333
|
const subdir = ASSET_SUBDIR[type];
|
|
13313
|
-
const dir =
|
|
13334
|
+
const dir = path73.join(member.absPath, ".claude", subdir);
|
|
13314
13335
|
if (!await fs49.pathExists(dir)) return [];
|
|
13315
13336
|
const names = await fs49.readdir(dir);
|
|
13316
13337
|
const out = [];
|
|
13317
13338
|
for (const fileName of [...names].sort()) {
|
|
13318
13339
|
if (!fileName.endsWith(".md")) continue;
|
|
13319
|
-
const abs =
|
|
13340
|
+
const abs = path73.join(dir, fileName);
|
|
13320
13341
|
if (!(await fs49.lstat(abs)).isFile()) continue;
|
|
13321
13342
|
const name = fileName.slice(0, -".md".length);
|
|
13322
|
-
out.push({ type, name, sourceRelPath:
|
|
13343
|
+
out.push({ type, name, sourceRelPath: path73.posix.join(".claude", subdir, fileName) });
|
|
13323
13344
|
}
|
|
13324
13345
|
return out;
|
|
13325
13346
|
}
|
|
@@ -13334,9 +13355,9 @@ async function collectSourceAssets(member) {
|
|
|
13334
13355
|
|
|
13335
13356
|
// src/workspace/link-context/plan.ts
|
|
13336
13357
|
function repoFolderPrefix(member) {
|
|
13337
|
-
const base =
|
|
13358
|
+
const base = path74.posix.basename(member.folder);
|
|
13338
13359
|
if (base && base !== "." && base !== "/") return base;
|
|
13339
|
-
const fromAbs =
|
|
13360
|
+
const fromAbs = path74.basename(member.absPath);
|
|
13340
13361
|
return fromAbs || member.id;
|
|
13341
13362
|
}
|
|
13342
13363
|
function assetKey(type, destKey) {
|
|
@@ -13393,7 +13414,7 @@ async function buildLinkPlan(members) {
|
|
|
13393
13414
|
name: asset.name,
|
|
13394
13415
|
destKey,
|
|
13395
13416
|
sourceRelPath: asset.sourceRelPath,
|
|
13396
|
-
sourceAbsPath:
|
|
13417
|
+
sourceAbsPath: path74.join(member.absPath, asset.sourceRelPath),
|
|
13397
13418
|
sourceHash
|
|
13398
13419
|
});
|
|
13399
13420
|
}
|
|
@@ -13404,7 +13425,7 @@ async function buildLinkPlan(members) {
|
|
|
13404
13425
|
function destRelPathFor(type, destKey) {
|
|
13405
13426
|
const subdir = `${type}s`;
|
|
13406
13427
|
const leaf = type === "skill" ? destKey : `${destKey}.md`;
|
|
13407
|
-
return
|
|
13428
|
+
return path75.posix.join(".claude", subdir, leaf);
|
|
13408
13429
|
}
|
|
13409
13430
|
async function runLinkContext(workspaceRoot, opts = {}) {
|
|
13410
13431
|
const write = resolveWriteMode(opts, "preview-by-default").kind === "write";
|
|
@@ -13430,9 +13451,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
13430
13451
|
const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13431
13452
|
for (const entry of plan.entries) {
|
|
13432
13453
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
13433
|
-
const destAbs =
|
|
13454
|
+
const destAbs = path75.join(workspaceRoot, destRel);
|
|
13434
13455
|
if (write) {
|
|
13435
|
-
await fs51.ensureDir(
|
|
13456
|
+
await fs51.ensureDir(path75.dirname(destAbs));
|
|
13436
13457
|
if (await fs51.pathExists(destAbs)) await fs51.remove(destAbs);
|
|
13437
13458
|
await fs51.copy(entry.sourceAbsPath, destAbs, {
|
|
13438
13459
|
overwrite: true,
|
|
@@ -13521,10 +13542,10 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
|
|
|
13521
13542
|
|
|
13522
13543
|
// src/commands/workspace/setup.ts
|
|
13523
13544
|
import { existsSync as existsSync9, statSync } from "node:fs";
|
|
13524
|
-
import
|
|
13545
|
+
import path77 from "node:path";
|
|
13525
13546
|
|
|
13526
13547
|
// src/claude/write-workspace-claude-md.ts
|
|
13527
|
-
import
|
|
13548
|
+
import path76 from "node:path";
|
|
13528
13549
|
import fs52 from "fs-extra";
|
|
13529
13550
|
init_fs();
|
|
13530
13551
|
init_logger();
|
|
@@ -13547,7 +13568,7 @@ ${WORKSPACE_BLOCK_END}`;
|
|
|
13547
13568
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
13548
13569
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
13549
13570
|
const dryRun = opts.dryRun ?? false;
|
|
13550
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
13571
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path76.join(workspaceRoot, "CLAUDE.md");
|
|
13551
13572
|
const say = opts.quiet ? () => {
|
|
13552
13573
|
} : log;
|
|
13553
13574
|
const existed = await fs52.pathExists(filePath);
|
|
@@ -13580,20 +13601,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
13580
13601
|
init_fs();
|
|
13581
13602
|
init_logger();
|
|
13582
13603
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
13583
|
-
let dir =
|
|
13604
|
+
let dir = path77.resolve(start);
|
|
13584
13605
|
for (; ; ) {
|
|
13585
|
-
if (existsSync9(
|
|
13586
|
-
const parent =
|
|
13587
|
-
if (parent === dir) return
|
|
13606
|
+
if (existsSync9(path77.join(dir, WORKSPACE_FILE))) return dir;
|
|
13607
|
+
const parent = path77.dirname(dir);
|
|
13608
|
+
if (parent === dir) return path77.resolve(start);
|
|
13588
13609
|
dir = parent;
|
|
13589
13610
|
}
|
|
13590
13611
|
}
|
|
13591
13612
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
13592
|
-
return
|
|
13613
|
+
return path77.resolve(workspaceRoot, repoPath) === path77.resolve(workspaceRoot);
|
|
13593
13614
|
}
|
|
13594
13615
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
13595
13616
|
const apply = options.write ?? false;
|
|
13596
|
-
const configText = await readText(
|
|
13617
|
+
const configText = await readText(path77.join(workspaceRoot, WORKSPACE_FILE));
|
|
13597
13618
|
if (!configText) {
|
|
13598
13619
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
13599
13620
|
process.exitCode = 1;
|
|
@@ -13620,7 +13641,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13620
13641
|
const statuses = [];
|
|
13621
13642
|
const aggregateInputs = [];
|
|
13622
13643
|
for (const repo of repos) {
|
|
13623
|
-
const repoRoot =
|
|
13644
|
+
const repoRoot = path77.resolve(workspaceRoot, repo.path);
|
|
13624
13645
|
say(`
|
|
13625
13646
|
\u2192 ${repo.name} (${repo.path})`);
|
|
13626
13647
|
try {
|
|
@@ -13710,7 +13731,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13710
13731
|
const status = statusByName.get(repo.name);
|
|
13711
13732
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
13712
13733
|
if (status?.status === "ok") {
|
|
13713
|
-
const lock = await checkLock(
|
|
13734
|
+
const lock = await checkLock(path77.resolve(workspaceRoot, repo.path));
|
|
13714
13735
|
manifestRepos.push({
|
|
13715
13736
|
name: repo.name,
|
|
13716
13737
|
path: repo.path,
|
|
@@ -13766,7 +13787,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13766
13787
|
try {
|
|
13767
13788
|
const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
|
|
13768
13789
|
if (linkResult.ok) {
|
|
13769
|
-
written.push(...linkResult.added.map((rel) =>
|
|
13790
|
+
written.push(...linkResult.added.map((rel) => path77.join(workspaceRoot, rel)));
|
|
13770
13791
|
if (!options.json) {
|
|
13771
13792
|
say(
|
|
13772
13793
|
`Linked ${linkResult.linked.length} cross-repo skill/agent/command copy(ies) into .claude/ (${linkResult.added.length} new, ${linkResult.removed.length} removed)${linkResult.skipped.length > 0 ? ` \u2014 ${linkResult.skipped.length} member(s) skipped, see above` : ""}.`
|
|
@@ -13798,7 +13819,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
13798
13819
|
|
|
13799
13820
|
// src/commands/workspace/undo.ts
|
|
13800
13821
|
import { existsSync as existsSync10, statSync as statSync2 } from "node:fs";
|
|
13801
|
-
import
|
|
13822
|
+
import path78 from "node:path";
|
|
13802
13823
|
import fs53 from "fs-extra";
|
|
13803
13824
|
init_fs();
|
|
13804
13825
|
init_logger();
|
|
@@ -13829,7 +13850,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13829
13850
|
}
|
|
13830
13851
|
if (options.dryRun) {
|
|
13831
13852
|
for (const repo of config.repos) {
|
|
13832
|
-
const repoRoot =
|
|
13853
|
+
const repoRoot = path78.resolve(workspaceRoot, repo.path);
|
|
13833
13854
|
log(`
|
|
13834
13855
|
\u2192 ${repo.name} (${repo.path})`);
|
|
13835
13856
|
try {
|
|
@@ -13861,11 +13882,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13861
13882
|
];
|
|
13862
13883
|
for (const abs of workspaceArtifacts) {
|
|
13863
13884
|
if (await fs53.pathExists(abs)) {
|
|
13864
|
-
log(`[dry-run] Would remove ${
|
|
13885
|
+
log(`[dry-run] Would remove ${path78.relative(workspaceRoot, abs)}`);
|
|
13865
13886
|
}
|
|
13866
13887
|
}
|
|
13867
13888
|
if (!await fs53.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
|
|
13868
|
-
const claudeMd = await readText(
|
|
13889
|
+
const claudeMd = await readText(path78.join(workspaceRoot, "CLAUDE.md"));
|
|
13869
13890
|
if (claudeMd?.includes(WORKSPACE_BLOCK_BEGIN) || claudeMd && hasLegacyWorkspaceBlock(claudeMd)) {
|
|
13870
13891
|
log("[dry-run] Would remove the workspace haus import block from CLAUDE.md");
|
|
13871
13892
|
}
|
|
@@ -13884,7 +13905,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13884
13905
|
}
|
|
13885
13906
|
let anyRepoFailed = false;
|
|
13886
13907
|
for (const repo of config.repos) {
|
|
13887
|
-
const repoRoot =
|
|
13908
|
+
const repoRoot = path78.resolve(workspaceRoot, repo.path);
|
|
13888
13909
|
log(`
|
|
13889
13910
|
\u2192 ${repo.name} (${repo.path})`);
|
|
13890
13911
|
try {
|
|
@@ -13913,20 +13934,20 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13913
13934
|
const abs = hausPath(workspaceRoot, rel);
|
|
13914
13935
|
if (await fs53.pathExists(abs)) {
|
|
13915
13936
|
await fs53.remove(abs);
|
|
13916
|
-
log(`Removed ${
|
|
13937
|
+
log(`Removed ${path78.relative(workspaceRoot, abs)}`);
|
|
13917
13938
|
}
|
|
13918
13939
|
}
|
|
13919
13940
|
const manifestFile = manifestPath(workspaceRoot);
|
|
13920
13941
|
if (await fs53.pathExists(manifestFile)) {
|
|
13921
13942
|
await fs53.remove(manifestFile);
|
|
13922
|
-
log(`Removed ${
|
|
13943
|
+
log(`Removed ${path78.relative(workspaceRoot, manifestFile)}`);
|
|
13923
13944
|
}
|
|
13924
13945
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
13925
13946
|
if (await fs53.pathExists(workspaceMdPath)) {
|
|
13926
13947
|
await fs53.remove(workspaceMdPath);
|
|
13927
|
-
log(`Removed ${
|
|
13948
|
+
log(`Removed ${path78.relative(workspaceRoot, workspaceMdPath)}`);
|
|
13928
13949
|
} else {
|
|
13929
|
-
const claudeMdPath =
|
|
13950
|
+
const claudeMdPath = path78.join(workspaceRoot, "CLAUDE.md");
|
|
13930
13951
|
if (existsSync10(claudeMdPath)) {
|
|
13931
13952
|
const prev = await readText(claudeMdPath);
|
|
13932
13953
|
if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
|
|
@@ -13995,7 +14016,7 @@ relationships: []
|
|
|
13995
14016
|
log("Workspace initialized.");
|
|
13996
14017
|
}
|
|
13997
14018
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
13998
|
-
const configText = await readText(
|
|
14019
|
+
const configText = await readText(path79.join(workspaceRoot, WORKSPACE_FILE));
|
|
13999
14020
|
if (!configText) {
|
|
14000
14021
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
14001
14022
|
process.exitCode = 1;
|
|
@@ -14017,7 +14038,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
14017
14038
|
const inputs = [];
|
|
14018
14039
|
const failures = [];
|
|
14019
14040
|
for (const repo of config.repos) {
|
|
14020
|
-
const repoRoot =
|
|
14041
|
+
const repoRoot = path79.resolve(workspaceRoot, repo.path);
|
|
14021
14042
|
try {
|
|
14022
14043
|
if (!existsSync11(repoRoot) || !statSync3(repoRoot).isDirectory()) {
|
|
14023
14044
|
throw new Error("path is not a directory");
|
|
@@ -14085,7 +14106,7 @@ init_logger();
|
|
|
14085
14106
|
init_paths();
|
|
14086
14107
|
function cliVersion() {
|
|
14087
14108
|
try {
|
|
14088
|
-
const pkgPath =
|
|
14109
|
+
const pkgPath = path80.join(packageRoot(), "package.json");
|
|
14089
14110
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
14090
14111
|
return pkg.version ?? "0.0.0";
|
|
14091
14112
|
} catch {
|
|
@@ -14095,7 +14116,7 @@ function cliVersion() {
|
|
|
14095
14116
|
var program = new Command();
|
|
14096
14117
|
function validateRuntimeNodeVersion() {
|
|
14097
14118
|
try {
|
|
14098
|
-
const pkgPath =
|
|
14119
|
+
const pkgPath = path80.join(packageRoot(), "package.json");
|
|
14099
14120
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
14100
14121
|
const requiredRange = pkg.engines?.node;
|
|
14101
14122
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haus-tech/haus-workflow",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "Haus AI workflow CLI for Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"verify": "node scripts/verify.mjs",
|
|
38
38
|
"verify:full": "node scripts/verify.mjs --coverage",
|
|
39
39
|
"check:contract-behavior": "node --import tsx scripts/contract-behavior-check.mjs",
|
|
40
|
+
"check:mutation-table": "node scripts/check-mutation-table.mjs",
|
|
40
41
|
"postinstall": "node ./scripts/postinstall.mjs || true",
|
|
41
42
|
"prepare": "lefthook install || true",
|
|
42
43
|
"security:audit": "yarn npm audit -A -R --severity high --environment production"
|