@haus-tech/haus-workflow 4.9.0 → 5.0.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 +27 -0
- package/dist/cli.js +847 -499
- package/library/catalog/manifest.json +97 -34
- package/library/global/agents/request-assessor.md +2 -2
- package/library/global/agents/request-scorer.md +13 -8
- package/library/global/agents/request-specifier-eskalerad.md +1 -1
- package/library/global/agents/request-specifier.md +1 -1
- package/library/global/skills/request/SKILL.md +675 -363
- package/library/global/skills/request/scripts/archive.mjs +1376 -0
- package/library/global/skills/request/scripts/check-lasta.mjs +64 -0
- package/library/global/skills/request/scripts/compare-scores.mjs +23 -15
- package/library/global/skills/request/scripts/contract-check.mjs +108 -36
- package/library/global/skills/request/scripts/lib.mjs +66 -0
- package/library/global/skills/request/scripts/precheck.mjs +30 -27
- package/library/global/skills/request/scripts/report.mjs +571 -0
- package/library/global/skills/request/scripts/resolve-anchors.mjs +243 -0
- package/library/global/skills/request-assess-impact/SKILL.md +10 -10
- package/library/global/skills/request-score/SKILL.md +19 -10
- package/library/global/skills/request-sync/SKILL.md +4 -4
- package/library/global/skills/request-write-spec/SKILL.md +6 -6
- package/library/global/skills/request-write-spec/references/specformat.md +2 -2
- package/package.json +1 -1
- package/library/global/agents/request-classifier.md +0 -55
- package/library/global/skills/request/scripts/compare-classifications.mjs +0 -94
- package/library/global/skills/request/scripts/quote-repair.mjs +0 -166
- package/library/global/skills/request-classify/SKILL.md +0 -79
- package/library/global/skills/request-classify/references/klassningsinstruktion.md +0 -152
package/dist/cli.js
CHANGED
|
@@ -1998,7 +1998,7 @@ var init_sync = __esm({
|
|
|
1998
1998
|
// src/cli.ts
|
|
1999
1999
|
init_offline();
|
|
2000
2000
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2001
|
-
import
|
|
2001
|
+
import path90 from "node:path";
|
|
2002
2002
|
import { Command, Option } from "commander";
|
|
2003
2003
|
|
|
2004
2004
|
// src/commands/apply.ts
|
|
@@ -2787,41 +2787,12 @@ function parseHausManagedHeader(line2) {
|
|
|
2787
2787
|
}
|
|
2788
2788
|
|
|
2789
2789
|
// src/claude/superpowers-install.ts
|
|
2790
|
-
init_constants();
|
|
2791
2790
|
import path14 from "node:path";
|
|
2792
2791
|
import fg2 from "fast-glob";
|
|
2793
2792
|
import fs10 from "fs-extra";
|
|
2794
2793
|
init_logger();
|
|
2795
|
-
init_paths();
|
|
2796
|
-
var SUPERPOWERS_ORIGIN_SOURCE_ID = "superpowers-pcvelz";
|
|
2797
|
-
var SUPERPOWERS_SHARED_INSTALL_REL = ".claude/skills/shared";
|
|
2798
|
-
var PATH_REWRITES = [
|
|
2799
|
-
["skills/shared/", ".claude/skills/shared/"]
|
|
2800
|
-
];
|
|
2801
|
-
var PREFIX_REWRITES = ["superpowers-extended-cc:"];
|
|
2802
2794
|
var SKILL_DEP_ARTIFACTS = ["node_modules/**"];
|
|
2803
2795
|
var SKILL_DEP_ARTIFACT_DIRS = /* @__PURE__ */ new Set(["node_modules"]);
|
|
2804
|
-
function rewriteSuperpowersMarkdown(text) {
|
|
2805
|
-
let out = text;
|
|
2806
|
-
for (const [from, to] of PATH_REWRITES) {
|
|
2807
|
-
out = out.split(from).join(to);
|
|
2808
|
-
}
|
|
2809
|
-
for (const prefix of PREFIX_REWRITES) {
|
|
2810
|
-
out = out.split(prefix).join("");
|
|
2811
|
-
}
|
|
2812
|
-
return out;
|
|
2813
|
-
}
|
|
2814
|
-
async function rewriteMarkdownTree(dir) {
|
|
2815
|
-
const files = await fg2("**/*.md", { cwd: dir, onlyFiles: true, dot: true });
|
|
2816
|
-
for (const rel of files) {
|
|
2817
|
-
const abs = path14.join(dir, rel);
|
|
2818
|
-
const text = await fs10.readFile(abs, "utf8");
|
|
2819
|
-
const rewritten = rewriteSuperpowersMarkdown(text);
|
|
2820
|
-
if (rewritten !== text) {
|
|
2821
|
-
await fs10.writeFile(abs, rewritten, "utf8");
|
|
2822
|
-
}
|
|
2823
|
-
}
|
|
2824
|
-
}
|
|
2825
2796
|
async function extraDestFiles(sourcePath, destination) {
|
|
2826
2797
|
const [sourceFiles, destFiles] = await Promise.all([
|
|
2827
2798
|
fg2("**/*", { cwd: sourcePath, onlyFiles: true, dot: true, ignore: [...SKILL_DEP_ARTIFACTS] }),
|
|
@@ -2832,8 +2803,7 @@ async function extraDestFiles(sourcePath, destination) {
|
|
|
2832
2803
|
}
|
|
2833
2804
|
var SKILL_ENTRY_FILE = "SKILL.md";
|
|
2834
2805
|
function installedSkillText(text, opts) {
|
|
2835
|
-
|
|
2836
|
-
return opts.description ? rewriteSkillDescription(rewritten, opts.description) : rewritten;
|
|
2806
|
+
return opts.description ? rewriteSkillDescription(text, opts.description) : text;
|
|
2837
2807
|
}
|
|
2838
2808
|
async function previewInstalledSkill(srcFile, opts) {
|
|
2839
2809
|
const text = await readRewritableText(srcFile);
|
|
@@ -2870,30 +2840,8 @@ async function installCatalogSkill(sourcePath, destination, opts) {
|
|
|
2870
2840
|
}
|
|
2871
2841
|
}
|
|
2872
2842
|
await fs10.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
2873
|
-
if (opts.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID) {
|
|
2874
|
-
await rewriteMarkdownTree(destination);
|
|
2875
|
-
}
|
|
2876
2843
|
if (opts.description) await composeInstalledDescription(destination, opts.description);
|
|
2877
2844
|
}
|
|
2878
|
-
async function installSuperpowersShared(contentRoot, projectRoot, dryRun) {
|
|
2879
|
-
const source = path14.join(contentRoot, SUPERPOWERS_SHARED_CATALOG_REL);
|
|
2880
|
-
if (!await fs10.pathExists(source)) return null;
|
|
2881
|
-
const destination = claudePath(projectRoot, "skills", "shared");
|
|
2882
|
-
if (dryRun) return path14.relative(projectRoot, destination);
|
|
2883
|
-
await fs10.ensureDir(path14.dirname(destination));
|
|
2884
|
-
if (await fs10.pathExists(destination)) {
|
|
2885
|
-
const extra = await extraDestFiles(source, destination);
|
|
2886
|
-
if (extra.length > 0) {
|
|
2887
|
-
warn(
|
|
2888
|
-
`${destination}: removing ${extra.length} untracked file(s) not present in the catalog source before reinstall: ${extra.join(", ")}`
|
|
2889
|
-
);
|
|
2890
|
-
}
|
|
2891
|
-
await fs10.remove(destination);
|
|
2892
|
-
}
|
|
2893
|
-
await fs10.copy(source, destination, { overwrite: true, errorOnExist: false });
|
|
2894
|
-
await rewriteMarkdownTree(destination);
|
|
2895
|
-
return path14.relative(projectRoot, destination);
|
|
2896
|
-
}
|
|
2897
2845
|
|
|
2898
2846
|
// src/update/hash-installed.ts
|
|
2899
2847
|
init_fs();
|
|
@@ -3103,6 +3051,15 @@ async function recordManifestPlugins(plugins) {
|
|
|
3103
3051
|
for (const plugin of plugins) byId.set(plugin.id, plugin);
|
|
3104
3052
|
await writeManifest({ ...manifest, plugins: [...byId.values()] });
|
|
3105
3053
|
}
|
|
3054
|
+
async function pruneManifestPlugins(pluginKeys) {
|
|
3055
|
+
if (pluginKeys.length === 0) return;
|
|
3056
|
+
const manifest = await readManifest();
|
|
3057
|
+
if (!manifest || !manifest.plugins || manifest.plugins.length === 0) return;
|
|
3058
|
+
const drop = new Set(pluginKeys);
|
|
3059
|
+
const kept = manifest.plugins.filter((p) => !drop.has(`${p.plugin}@${p.marketplace}`));
|
|
3060
|
+
if (kept.length === manifest.plugins.length) return;
|
|
3061
|
+
await writeManifest({ ...manifest, plugins: kept });
|
|
3062
|
+
}
|
|
3106
3063
|
function buildManifest(source, files, hooks, plugins) {
|
|
3107
3064
|
return {
|
|
3108
3065
|
_schema: MANIFEST_SCHEMA,
|
|
@@ -3437,8 +3394,8 @@ function buildDenyRules() {
|
|
|
3437
3394
|
if (RUNTIME_ONLY_DENY_COMMANDS.includes(command)) continue;
|
|
3438
3395
|
rules.push(`Bash(${command}:*)`);
|
|
3439
3396
|
}
|
|
3440
|
-
for (const
|
|
3441
|
-
const pattern = DENY_DIRS.has(
|
|
3397
|
+
for (const path91 of DENY_PATHS) {
|
|
3398
|
+
const pattern = DENY_DIRS.has(path91) ? `${path91}/**` : path91;
|
|
3442
3399
|
for (const tool of FILE_TOOLS) {
|
|
3443
3400
|
rules.push(`${tool}(${pattern})`);
|
|
3444
3401
|
}
|
|
@@ -3870,6 +3827,7 @@ async function verifyProjectSettingsHooksContract(root) {
|
|
|
3870
3827
|
init_logger();
|
|
3871
3828
|
import path20 from "node:path";
|
|
3872
3829
|
import fs17 from "fs-extra";
|
|
3830
|
+
import YAML from "yaml";
|
|
3873
3831
|
var CI_GATE_WORKFLOW_REL = path20.join(".github", "workflows", "haus-gate.yml");
|
|
3874
3832
|
var WORKFLOW = `# Written by \`haus init\`. Yours to edit \u2014 haus will not rewrite this file.
|
|
3875
3833
|
#
|
|
@@ -3898,12 +3856,21 @@ jobs:
|
|
|
3898
3856
|
node-version: '22'
|
|
3899
3857
|
- name: Install haus
|
|
3900
3858
|
run: npm install -g @haus-tech/haus-workflow@latest
|
|
3859
|
+
# \`.haus-workflow/recommendation.json\` is machine-local and gitignored
|
|
3860
|
+
# (ADR-0025), so it is absent on every CI clone \u2014 and without it \`doctor\`
|
|
3861
|
+
# cannot tell "the catalog gates this agent out of your stack" from "you are
|
|
3862
|
+
# missing an install", so it keeps the stricter reading and fails the gate over
|
|
3863
|
+
# upstream prose you cannot act on. Computing one first is what makes the
|
|
3864
|
+
# severity rules in ADR-0070/ADR-0095 work here. Writes only gitignored files.
|
|
3865
|
+
- name: Compute a recommendation for this stack
|
|
3866
|
+
run: haus recommend
|
|
3901
3867
|
- name: haus ci-gate
|
|
3902
3868
|
run: haus ci-gate
|
|
3903
3869
|
`;
|
|
3904
3870
|
async function writeCiGateWorkflow(root, dryRun, quiet = false) {
|
|
3905
3871
|
const dest = path20.join(root, CI_GATE_WORKFLOW_REL);
|
|
3906
3872
|
if (await fs17.pathExists(dest)) return void 0;
|
|
3873
|
+
if (await projectRunsCiGate(root)) return void 0;
|
|
3907
3874
|
if (dryRun) {
|
|
3908
3875
|
if (!quiet) log(`Would write ${CI_GATE_WORKFLOW_REL} (CI gate: haus ci-gate on every PR)`);
|
|
3909
3876
|
return dest;
|
|
@@ -3913,6 +3880,20 @@ async function writeCiGateWorkflow(root, dryRun, quiet = false) {
|
|
|
3913
3880
|
if (!quiet) log(`Wrote ${CI_GATE_WORKFLOW_REL} \u2014 runs \`haus ci-gate\` on every pull request`);
|
|
3914
3881
|
return dest;
|
|
3915
3882
|
}
|
|
3883
|
+
var CI_GATE_COMMAND = /\bci-gate\b/;
|
|
3884
|
+
function runScripts(node, found = []) {
|
|
3885
|
+
if (Array.isArray(node)) {
|
|
3886
|
+
for (const item of node) runScripts(item, found);
|
|
3887
|
+
return found;
|
|
3888
|
+
}
|
|
3889
|
+
if (node && typeof node === "object") {
|
|
3890
|
+
for (const [key, value] of Object.entries(node)) {
|
|
3891
|
+
if (key === "run" && typeof value === "string") found.push(value);
|
|
3892
|
+
else runScripts(value, found);
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
return found;
|
|
3896
|
+
}
|
|
3916
3897
|
async function projectRunsCiGate(root) {
|
|
3917
3898
|
const dir = path20.join(root, ".github", "workflows");
|
|
3918
3899
|
let entries;
|
|
@@ -3925,7 +3906,7 @@ async function projectRunsCiGate(root) {
|
|
|
3925
3906
|
if (!/\.ya?ml$/i.test(entry)) continue;
|
|
3926
3907
|
try {
|
|
3927
3908
|
const text = await fs17.readFile(path20.join(dir, entry), "utf8");
|
|
3928
|
-
if (
|
|
3909
|
+
if (runScripts(YAML.parse(text)).some((script) => CI_GATE_COMMAND.test(script))) return true;
|
|
3929
3910
|
} catch {
|
|
3930
3911
|
}
|
|
3931
3912
|
}
|
|
@@ -4667,7 +4648,6 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4667
4648
|
}
|
|
4668
4649
|
const catalogItems = [...catalogItemsById.values()];
|
|
4669
4650
|
let curatedReviewStatusSkips = 0;
|
|
4670
|
-
let superpowersSharedInstalled = false;
|
|
4671
4651
|
const projectPackageManager = detectPackageManager(
|
|
4672
4652
|
root,
|
|
4673
4653
|
(await readJson(path26.join(root, "package.json")))?.packageManager
|
|
@@ -4814,14 +4794,6 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4814
4794
|
}
|
|
4815
4795
|
files.push(destination);
|
|
4816
4796
|
const relPaths = [path26.relative(root, destination)];
|
|
4817
|
-
if (!superpowersSharedInstalled && manifestItem.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID && item.type === "skill") {
|
|
4818
|
-
const sharedRel = await installSuperpowersShared(contentRoot, root, dryRun);
|
|
4819
|
-
if (sharedRel) {
|
|
4820
|
-
superpowersSharedInstalled = true;
|
|
4821
|
-
relPaths.push(sharedRel);
|
|
4822
|
-
files.push(path26.join(root, sharedRel));
|
|
4823
|
-
}
|
|
4824
|
-
}
|
|
4825
4797
|
const current = installedPathsByItem.get(item.id) ?? [];
|
|
4826
4798
|
installedPathsByItem.set(item.id, [...current, ...relPaths]);
|
|
4827
4799
|
installedIds.add(item.id);
|
|
@@ -5014,6 +4986,7 @@ async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, q
|
|
|
5014
4986
|
}
|
|
5015
4987
|
}
|
|
5016
4988
|
}
|
|
4989
|
+
var LEGACY_SUPERPOWERS_SHARED_REL_PATH = ".claude/skills/shared";
|
|
5017
4990
|
async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activePluginIds = []) {
|
|
5018
4991
|
const say = quiet ? () => {
|
|
5019
4992
|
} : log;
|
|
@@ -5067,7 +5040,7 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
|
|
|
5067
5040
|
continue;
|
|
5068
5041
|
}
|
|
5069
5042
|
const removable = superseded ? existing.filter(({ rel }) => {
|
|
5070
|
-
const isShared = rel.split(path26.sep).join("/") ===
|
|
5043
|
+
const isShared = rel.split(path26.sep).join("/") === LEGACY_SUPERPOWERS_SHARED_REL_PATH;
|
|
5071
5044
|
if (isShared && keepShared) {
|
|
5072
5045
|
say(
|
|
5073
5046
|
`Keeping ${displayPath(root, path26.join(root, rel))} \u2014 a locally-edited superpowers skill still uses it`
|
|
@@ -5326,12 +5299,39 @@ init_logger();
|
|
|
5326
5299
|
var DEFAULT_GLOBAL_PLUGINS = [
|
|
5327
5300
|
{
|
|
5328
5301
|
id: "superpowers",
|
|
5302
|
+
plugin: "superpowers",
|
|
5303
|
+
marketplace: "superpowers-dev",
|
|
5304
|
+
url: "https://github.com/obra/superpowers.git",
|
|
5305
|
+
ref: "v6.3.0"
|
|
5306
|
+
}
|
|
5307
|
+
];
|
|
5308
|
+
var FORMER_GLOBAL_PLUGINS = [
|
|
5309
|
+
{
|
|
5310
|
+
id: "superpowers-extended-cc",
|
|
5329
5311
|
plugin: "superpowers-extended-cc",
|
|
5330
5312
|
marketplace: "superpowers-extended-cc-marketplace",
|
|
5331
5313
|
url: "https://github.com/pcvelz/superpowers.git",
|
|
5332
5314
|
ref: "v6.4.0"
|
|
5333
5315
|
}
|
|
5334
5316
|
];
|
|
5317
|
+
function planStalePluginRemovals(desired, candidates, state) {
|
|
5318
|
+
if (!state.available) return [];
|
|
5319
|
+
const desiredIds = new Set(desired.map(pluginId));
|
|
5320
|
+
const desiredMarketplaces = new Set(desired.map((pin) => pin.marketplace));
|
|
5321
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5322
|
+
const stale = [];
|
|
5323
|
+
for (const candidate of candidates) {
|
|
5324
|
+
const id = pluginId(candidate);
|
|
5325
|
+
if (desiredIds.has(id) || seen.has(id)) continue;
|
|
5326
|
+
seen.add(id);
|
|
5327
|
+
const installedAtManagedScope = state.installed.some(
|
|
5328
|
+
(p) => p.id === id && (p.scope ?? MANAGED_SCOPE) === MANAGED_SCOPE
|
|
5329
|
+
);
|
|
5330
|
+
const marketplaceLeftover = !desiredMarketplaces.has(candidate.marketplace) && state.marketplaces.some((m) => m.name === candidate.marketplace);
|
|
5331
|
+
if (installedAtManagedScope || marketplaceLeftover) stale.push(candidate);
|
|
5332
|
+
}
|
|
5333
|
+
return stale;
|
|
5334
|
+
}
|
|
5335
5335
|
var MANAGED_SCOPE = "user";
|
|
5336
5336
|
function pluginId(pin) {
|
|
5337
5337
|
return `${pin.plugin}@${pin.marketplace}`;
|
|
@@ -5712,7 +5712,7 @@ async function removeGlobalPlugins(plugins, deps = {}) {
|
|
|
5712
5712
|
const exec = deps.exec ?? realExec;
|
|
5713
5713
|
const warn2 = deps.warn ?? warn;
|
|
5714
5714
|
const log2 = deps.log ?? log;
|
|
5715
|
-
const result = { removed: [], failed: [], unavailable: false };
|
|
5715
|
+
const result = { removed: [], failed: [], unavailable: false, converged: [] };
|
|
5716
5716
|
if (plugins.length === 0) return result;
|
|
5717
5717
|
if (globalPluginMutationsDisabled() && deps.exec === void 0) {
|
|
5718
5718
|
return { ...result, unavailable: true };
|
|
@@ -5725,15 +5725,28 @@ async function removeGlobalPlugins(plugins, deps = {}) {
|
|
|
5725
5725
|
}
|
|
5726
5726
|
const scopeKey = (id, scope) => `${id}@@${scope ?? "user"}`;
|
|
5727
5727
|
const stillInstalled = new Set(state.installed.map((p) => scopeKey(p.id, p.scope)));
|
|
5728
|
+
const marketplaceNeedsRemoval = (pin) => {
|
|
5729
|
+
const marketplaceStillConfigured = state.marketplaces.some((m) => m.name === pin.marketplace);
|
|
5730
|
+
const othersFromMarketplace = [...stillInstalled].some(
|
|
5731
|
+
(key) => key.split("@@")[0]?.endsWith(`@${pin.marketplace}`)
|
|
5732
|
+
);
|
|
5733
|
+
return marketplaceStillConfigured && !othersFromMarketplace;
|
|
5734
|
+
};
|
|
5728
5735
|
for (const pin of plugins) {
|
|
5729
5736
|
const id = pluginId(pin);
|
|
5730
5737
|
const managedKey = scopeKey(id, "user");
|
|
5738
|
+
const wasInstalledAtManagedScope = stillInstalled.has(managedKey);
|
|
5731
5739
|
if (deps.dryRun) {
|
|
5732
|
-
if (
|
|
5740
|
+
if (wasInstalledAtManagedScope) {
|
|
5733
5741
|
log2(`[dry-run] would remove global plugin ${pin.id} (${id})`);
|
|
5742
|
+
stillInstalled.delete(managedKey);
|
|
5743
|
+
}
|
|
5744
|
+
if (marketplaceNeedsRemoval(pin)) {
|
|
5745
|
+
log2(`[dry-run] would remove marketplace ${pin.marketplace}`);
|
|
5746
|
+
}
|
|
5734
5747
|
continue;
|
|
5735
5748
|
}
|
|
5736
|
-
if (
|
|
5749
|
+
if (wasInstalledAtManagedScope) {
|
|
5737
5750
|
const uninstalled = await exec(["plugin", "uninstall", id, "--scope", "user"]);
|
|
5738
5751
|
if (uninstalled.exitCode !== 0) {
|
|
5739
5752
|
result.failed.push(pin.id);
|
|
@@ -5745,21 +5758,60 @@ async function removeGlobalPlugins(plugins, deps = {}) {
|
|
|
5745
5758
|
stillInstalled.delete(managedKey);
|
|
5746
5759
|
result.removed.push(pin.id);
|
|
5747
5760
|
}
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
(key) => key.split("@@")[0]?.endsWith(`@${pin.marketplace}`)
|
|
5751
|
-
);
|
|
5752
|
-
if (marketplaceStillConfigured && !othersFromMarketplace) {
|
|
5761
|
+
let marketplaceRemovalFailed = false;
|
|
5762
|
+
if (marketplaceNeedsRemoval(pin)) {
|
|
5753
5763
|
const removedMarket = await exec(["plugin", "marketplace", "remove", pin.marketplace]);
|
|
5754
5764
|
if (removedMarket.exitCode !== 0) {
|
|
5765
|
+
marketplaceRemovalFailed = true;
|
|
5755
5766
|
warn2(
|
|
5756
5767
|
`Could not remove marketplace ${pin.marketplace}: ${removedMarket.stderr.trim() || `exit ${removedMarket.exitCode}`}`
|
|
5757
5768
|
);
|
|
5758
5769
|
}
|
|
5759
5770
|
}
|
|
5771
|
+
if (!wasInstalledAtManagedScope && !marketplaceRemovalFailed) {
|
|
5772
|
+
result.converged.push(pin.id);
|
|
5773
|
+
}
|
|
5760
5774
|
}
|
|
5761
5775
|
return result;
|
|
5762
5776
|
}
|
|
5777
|
+
async function cleanupStaleGlobalPlugins(desired, deps = {}) {
|
|
5778
|
+
const none = { planned: [], removed: [], failed: [], unavailable: false };
|
|
5779
|
+
if (globalPluginMutationsDisabled() && deps.exec === void 0) {
|
|
5780
|
+
return { ...none, unavailable: true };
|
|
5781
|
+
}
|
|
5782
|
+
const state = await probePluginEnv(deps);
|
|
5783
|
+
if (!state.available) return { ...none, unavailable: true };
|
|
5784
|
+
const manifest = await readManifest();
|
|
5785
|
+
const recorded = (manifest?.plugins ?? []).filter(isValidPin);
|
|
5786
|
+
const protectedPins = [...desired, ...DEFAULT_GLOBAL_PLUGINS];
|
|
5787
|
+
const planned = planStalePluginRemovals(
|
|
5788
|
+
protectedPins,
|
|
5789
|
+
[...recorded, ...FORMER_GLOBAL_PLUGINS],
|
|
5790
|
+
state
|
|
5791
|
+
);
|
|
5792
|
+
if (planned.length === 0) return none;
|
|
5793
|
+
const removal = await removeGlobalPlugins(planned, deps);
|
|
5794
|
+
const removedPins = planned.filter((pin) => removal.removed.includes(pin.id));
|
|
5795
|
+
const convergedPins = planned.filter((pin) => removal.converged.includes(pin.id));
|
|
5796
|
+
if (!deps.dryRun) {
|
|
5797
|
+
const toPrune = [...removedPins, ...convergedPins];
|
|
5798
|
+
if (toPrune.length > 0) {
|
|
5799
|
+
await pruneManifestPlugins(toPrune.map((pin) => pluginId(pin)));
|
|
5800
|
+
}
|
|
5801
|
+
if (removedPins.length > 0) {
|
|
5802
|
+
const log2 = deps.log ?? log;
|
|
5803
|
+
log2(
|
|
5804
|
+
`Removed stale global plugin(s): ${removedPins.map((pin) => pluginId(pin)).join(", ")}. Restart Claude Code to unload them.`
|
|
5805
|
+
);
|
|
5806
|
+
}
|
|
5807
|
+
}
|
|
5808
|
+
return {
|
|
5809
|
+
planned,
|
|
5810
|
+
removed: removal.removed,
|
|
5811
|
+
failed: removal.failed,
|
|
5812
|
+
unavailable: removal.unavailable
|
|
5813
|
+
};
|
|
5814
|
+
}
|
|
5763
5815
|
|
|
5764
5816
|
// src/refs/fetch-refs.ts
|
|
5765
5817
|
import path30 from "node:path";
|
|
@@ -7080,8 +7132,34 @@ import path34 from "node:path";
|
|
|
7080
7132
|
import fg4 from "fast-glob";
|
|
7081
7133
|
import fs33 from "fs-extra";
|
|
7082
7134
|
var REFERENCE_PATTERN = /\b([a-z][a-z0-9]*(?:-[a-z0-9]+)+):([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\b(?!:\/\/)/g;
|
|
7135
|
+
function escapeRegExp2(value) {
|
|
7136
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7137
|
+
}
|
|
7138
|
+
function buildKnownNamespacePattern(names) {
|
|
7139
|
+
if (names.length === 0) return null;
|
|
7140
|
+
const alternation = names.map(escapeRegExp2).join("|");
|
|
7141
|
+
return new RegExp(`\\b(${alternation}):([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\\b(?!:\\/\\/)`, "g");
|
|
7142
|
+
}
|
|
7143
|
+
function collectReferenceMatches(line2, knownPattern) {
|
|
7144
|
+
const seenSpans = /* @__PURE__ */ new Set();
|
|
7145
|
+
const matches = [];
|
|
7146
|
+
const collectFrom = (regex) => {
|
|
7147
|
+
for (const match of line2.matchAll(regex)) {
|
|
7148
|
+
const [reference, namespace, bareName] = match;
|
|
7149
|
+
if (!reference || !namespace || !bareName || match.index === void 0) continue;
|
|
7150
|
+
const span = `${match.index}:${reference.length}`;
|
|
7151
|
+
if (seenSpans.has(span)) continue;
|
|
7152
|
+
seenSpans.add(span);
|
|
7153
|
+
matches.push({ reference, namespace, bareName, index: match.index });
|
|
7154
|
+
}
|
|
7155
|
+
};
|
|
7156
|
+
collectFrom(REFERENCE_PATTERN);
|
|
7157
|
+
if (knownPattern) collectFrom(knownPattern);
|
|
7158
|
+
return matches.sort((a, b) => a.index - b.index);
|
|
7159
|
+
}
|
|
7160
|
+
var FENCED_BLOCK = /(```|~~~)[\s\S]*?\1/g;
|
|
7083
7161
|
function stripFencedCodeBlocks(text) {
|
|
7084
|
-
return text.replace(
|
|
7162
|
+
return text.replace(FENCED_BLOCK, (block) => block.replace(/[^\n]/g, " "));
|
|
7085
7163
|
}
|
|
7086
7164
|
var INLINE_CODE_SPAN = /(`+)(?!`)[\s\S]*?\1/g;
|
|
7087
7165
|
function inlineCodeSpanRanges(line2) {
|
|
@@ -7093,18 +7171,19 @@ function inlineCodeSpanRanges(line2) {
|
|
|
7093
7171
|
}
|
|
7094
7172
|
async function findUnresolvedSkillReferences(root, options = {}) {
|
|
7095
7173
|
const resolvableNamespaces = new Set(options.pluginNamespaces ?? []);
|
|
7174
|
+
const knownNamespacePattern = buildKnownNamespacePattern(options.knownPluginNamespaces ?? []);
|
|
7096
7175
|
const installedSkillNames = await readInstalledSkillNames(root);
|
|
7097
7176
|
const issues = [];
|
|
7098
7177
|
for (const { file, lines } of await readScannedMarkdown(root)) {
|
|
7099
7178
|
lines.forEach((line2, idx) => {
|
|
7100
7179
|
const codeSpans = inlineCodeSpanRanges(line2);
|
|
7101
|
-
for (const
|
|
7102
|
-
|
|
7180
|
+
for (const { reference, namespace, bareName, index } of collectReferenceMatches(
|
|
7181
|
+
line2,
|
|
7182
|
+
knownNamespacePattern
|
|
7183
|
+
)) {
|
|
7103
7184
|
if (!reference || !bareName) continue;
|
|
7104
7185
|
if (namespace && resolvableNamespaces.has(namespace)) continue;
|
|
7105
|
-
const inCodeSpan = codeSpans.some(
|
|
7106
|
-
([start, end]) => match.index >= start && match.index < end
|
|
7107
|
-
);
|
|
7186
|
+
const inCodeSpan = codeSpans.some(([start, end]) => index >= start && index < end);
|
|
7108
7187
|
if (inCodeSpan && !installedSkillNames.has(bareName)) continue;
|
|
7109
7188
|
issues.push({
|
|
7110
7189
|
file,
|
|
@@ -7118,37 +7197,107 @@ async function findUnresolvedSkillReferences(root, options = {}) {
|
|
|
7118
7197
|
}
|
|
7119
7198
|
return issues;
|
|
7120
7199
|
}
|
|
7121
|
-
var
|
|
7200
|
+
var BOLD_SPAN = /\*\*([^*\n]+)\*\*/g;
|
|
7201
|
+
var BARE_NAME = /^[a-z0-9][a-z0-9-]{2,}$/;
|
|
7202
|
+
var KEBAB_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/;
|
|
7122
7203
|
var AGENT_WORD = /\bagents?\b/i;
|
|
7204
|
+
var ITEMS_HEADING = /\b(?:skills?|agents?)\b/i;
|
|
7205
|
+
var LIST_ITEM = /^(\s*(?:[-*+]|\d+[.)])\s+)(.*)$/;
|
|
7206
|
+
var LIST_LABEL = /^[A-Za-z][A-Za-z /]{0,24}:\s*/;
|
|
7207
|
+
var KEYWORD_BEFORE = /(?:sub)?(?:skills?|agents?)\s*[:=-]*\s*$/i;
|
|
7208
|
+
var KEYWORD_AFTER = /^\s+(?:[A-Za-z]+\s+)?(?:sub)?(?:skills?|agents?)\b/i;
|
|
7209
|
+
var ROUTER_BEFORE = /\b(?:routes?|routing|delegates?|dispatch(?:es)?|hands? off)\s*(?:to|→|->|:)?\s*$/i;
|
|
7210
|
+
var RUN_SEPARATOR = /^\s*(?:\([^)\n]*\))?\s*(?:,|;|\/|\band\b|\bor\b|\bplus\b)\s*(?:the\s+)?$/i;
|
|
7211
|
+
var INLINE_KEYWORD_NAME = /^(?:sub)?(?:skills?|agents?)\s*[:=]\s*(\S+)$/i;
|
|
7212
|
+
var SUBAGENT = /subagent_type\s*=\s*["']([^"']+)["']/g;
|
|
7213
|
+
function isBareNameLike(name) {
|
|
7214
|
+
return BARE_NAME.test(name);
|
|
7215
|
+
}
|
|
7216
|
+
function normalizeName(ref) {
|
|
7217
|
+
return ref.split("#")[0].split("?")[0].trim().replace(/[.,;:!)\]]+$/, "");
|
|
7218
|
+
}
|
|
7219
|
+
function boldNameSpans(line2) {
|
|
7220
|
+
const spans = [];
|
|
7221
|
+
for (const match of line2.matchAll(BOLD_SPAN)) {
|
|
7222
|
+
const text = match[1];
|
|
7223
|
+
if (text && isBareNameLike(text)) {
|
|
7224
|
+
spans.push({ text, start: match.index, end: match.index + match[0].length });
|
|
7225
|
+
}
|
|
7226
|
+
}
|
|
7227
|
+
return spans;
|
|
7228
|
+
}
|
|
7229
|
+
function groupRuns(line2, spans) {
|
|
7230
|
+
const runs = [];
|
|
7231
|
+
for (const span of spans) {
|
|
7232
|
+
const current = runs.at(-1);
|
|
7233
|
+
const previous = current?.at(-1);
|
|
7234
|
+
if (current && previous && RUN_SEPARATOR.test(line2.slice(previous.end, span.start))) {
|
|
7235
|
+
current.push(span);
|
|
7236
|
+
} else {
|
|
7237
|
+
runs.push([span]);
|
|
7238
|
+
}
|
|
7239
|
+
}
|
|
7240
|
+
return runs;
|
|
7241
|
+
}
|
|
7123
7242
|
function isAgentContext(line2, nearestHeading) {
|
|
7124
|
-
|
|
7125
|
-
|
|
7243
|
+
let blanked = "";
|
|
7244
|
+
let cursor = 0;
|
|
7245
|
+
for (const span of boldNameSpans(line2)) {
|
|
7246
|
+
blanked += line2.slice(cursor, span.start) + " ".repeat(span.end - span.start);
|
|
7247
|
+
cursor = span.end;
|
|
7248
|
+
}
|
|
7249
|
+
blanked += line2.slice(cursor);
|
|
7250
|
+
return AGENT_WORD.test(blanked) || AGENT_WORD.test(nearestHeading);
|
|
7251
|
+
}
|
|
7252
|
+
function extractNameReferences(lines, { knownNames = /* @__PURE__ */ new Set() } = {}) {
|
|
7253
|
+
const out = [];
|
|
7254
|
+
let nearestHeading = "";
|
|
7255
|
+
lines.forEach((line2, idx) => {
|
|
7256
|
+
if (/^#{1,6}\s/.test(line2)) nearestHeading = line2.replace(/^#{1,6}\s*/, "");
|
|
7257
|
+
const lineNumber = idx + 1;
|
|
7258
|
+
const codeSpans = inlineCodeSpanRanges(line2);
|
|
7259
|
+
const inCodeSpan = (index) => codeSpans.some(([start, end]) => index >= start && index < end);
|
|
7260
|
+
const listMatch = line2.match(LIST_ITEM);
|
|
7261
|
+
const itemBody = listMatch?.[2]?.replace(LIST_LABEL, "");
|
|
7262
|
+
const itemBodyStart = itemBody === void 0 ? -1 : line2.length - itemBody.length;
|
|
7263
|
+
const underItemsHeading = ITEMS_HEADING.test(nearestHeading);
|
|
7264
|
+
const looseContext = isAgentContext(line2, nearestHeading);
|
|
7265
|
+
for (const run of groupRuns(line2, boldNameSpans(line2))) {
|
|
7266
|
+
const before = line2.slice(0, run[0].start);
|
|
7267
|
+
const after = line2.slice(run.at(-1).end);
|
|
7268
|
+
const positional = KEYWORD_BEFORE.test(before) || ROUTER_BEFORE.test(before) || KEYWORD_AFTER.test(after) || // Two co-listed installed items make the run an item list. One is not enough:
|
|
7269
|
+
// package names share basenames with skills.
|
|
7270
|
+
run.filter((span) => knownNames.has(span.text)).length >= 2 || underItemsHeading && run[0].start === itemBodyStart;
|
|
7271
|
+
for (const span of run) {
|
|
7272
|
+
if (inCodeSpan(span.start)) continue;
|
|
7273
|
+
if (!positional && !(KEBAB_NAME.test(span.text) && looseContext)) continue;
|
|
7274
|
+
out.push({ line: lineNumber, name: span.text, kind: "bold" });
|
|
7275
|
+
}
|
|
7276
|
+
}
|
|
7277
|
+
for (const [start, end] of codeSpans) {
|
|
7278
|
+
const keyword = line2.slice(start, end).replace(/^`+|`+$/g, "").trim().match(INLINE_KEYWORD_NAME);
|
|
7279
|
+
if (!keyword) continue;
|
|
7280
|
+
const name = normalizeName(keyword[1]);
|
|
7281
|
+
if (isBareNameLike(name)) out.push({ line: lineNumber, name, kind: "label" });
|
|
7282
|
+
}
|
|
7283
|
+
for (const match of line2.matchAll(SUBAGENT)) {
|
|
7284
|
+
if (match[1]) out.push({ line: lineNumber, name: match[1], kind: "subagent" });
|
|
7285
|
+
}
|
|
7286
|
+
});
|
|
7287
|
+
return out;
|
|
7126
7288
|
}
|
|
7127
7289
|
async function findUnresolvedAgentReferences(root, options = {}) {
|
|
7128
7290
|
const installedAgentNames = await readInstalledAgentNames(root);
|
|
7129
7291
|
const catalogAgentNames = new Set(options.catalogAgentNames ?? []);
|
|
7130
7292
|
const issues = [];
|
|
7293
|
+
const installedNames = /* @__PURE__ */ new Set([...installedAgentNames, ...await readInstalledSkillNames(root)]);
|
|
7131
7294
|
for (const { file, lines } of await readScannedMarkdown(root)) {
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
if (
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
const agentName = match[1];
|
|
7139
|
-
if (!agentName || installedAgentNames.has(agentName)) continue;
|
|
7140
|
-
const inCodeSpan = codeSpans.some(
|
|
7141
|
-
([start, end]) => match.index >= start && match.index < end
|
|
7142
|
-
);
|
|
7143
|
-
if (inCodeSpan) continue;
|
|
7144
|
-
issues.push({
|
|
7145
|
-
file,
|
|
7146
|
-
line: idx + 1,
|
|
7147
|
-
agentName,
|
|
7148
|
-
inCatalog: catalogAgentNames.has(agentName)
|
|
7149
|
-
});
|
|
7150
|
-
}
|
|
7151
|
-
});
|
|
7295
|
+
for (const { line: line2, name, kind } of extractNameReferences(lines, {
|
|
7296
|
+
knownNames: installedNames
|
|
7297
|
+
})) {
|
|
7298
|
+
if (installedNames.has(name)) continue;
|
|
7299
|
+
issues.push({ file, line: line2, agentName: name, kind, inCatalog: catalogAgentNames.has(name) });
|
|
7300
|
+
}
|
|
7152
7301
|
}
|
|
7153
7302
|
return issues;
|
|
7154
7303
|
}
|
|
@@ -8005,13 +8154,13 @@ import fs39 from "fs-extra";
|
|
|
8005
8154
|
// src/commands/workspace/config.ts
|
|
8006
8155
|
init_fs();
|
|
8007
8156
|
import path41 from "node:path";
|
|
8008
|
-
import
|
|
8157
|
+
import YAML2 from "yaml";
|
|
8009
8158
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
8010
8159
|
function parseWorkspaceConfig(text) {
|
|
8011
8160
|
if (!text) return void 0;
|
|
8012
8161
|
let parsed;
|
|
8013
8162
|
try {
|
|
8014
|
-
parsed =
|
|
8163
|
+
parsed = YAML2.parse(text);
|
|
8015
8164
|
} catch {
|
|
8016
8165
|
return void 0;
|
|
8017
8166
|
}
|
|
@@ -8496,7 +8645,7 @@ async function buildContentBlob(root, files) {
|
|
|
8496
8645
|
// src/scanner/workspace-packages.ts
|
|
8497
8646
|
import path45 from "node:path";
|
|
8498
8647
|
import fg5 from "fast-glob";
|
|
8499
|
-
import
|
|
8648
|
+
import YAML3 from "yaml";
|
|
8500
8649
|
init_fs();
|
|
8501
8650
|
var PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
|
|
8502
8651
|
var MAX_WORKSPACE_MEMBERS = 200;
|
|
@@ -8518,7 +8667,7 @@ function workspaceGlobsFromPackageJson(pkg) {
|
|
|
8518
8667
|
}
|
|
8519
8668
|
function workspaceGlobsFromPnpmYaml(text) {
|
|
8520
8669
|
try {
|
|
8521
|
-
const doc =
|
|
8670
|
+
const doc = YAML3.parse(text);
|
|
8522
8671
|
if (!isRecord(doc)) return [];
|
|
8523
8672
|
return stringEntries(doc.packages);
|
|
8524
8673
|
} catch {
|
|
@@ -8830,9 +8979,7 @@ async function runDoctor(options) {
|
|
|
8830
8979
|
return;
|
|
8831
8980
|
}
|
|
8832
8981
|
const context = await readContextOrScan(root);
|
|
8833
|
-
const recommendation = await readJson(
|
|
8834
|
-
hausPath(root, "recommendation.json")
|
|
8835
|
-
);
|
|
8982
|
+
const recommendation = await readJson(hausPath(root, "recommendation.json"));
|
|
8836
8983
|
const detail = [];
|
|
8837
8984
|
const attention = [];
|
|
8838
8985
|
const suggestions = [];
|
|
@@ -9142,8 +9289,16 @@ async function runDoctor(options) {
|
|
|
9142
9289
|
);
|
|
9143
9290
|
}
|
|
9144
9291
|
}
|
|
9292
|
+
const knownNamespaces = [
|
|
9293
|
+
.../* @__PURE__ */ new Set([
|
|
9294
|
+
...pluginPins.map((pin) => pin.plugin),
|
|
9295
|
+
...DEFAULT_GLOBAL_PLUGINS.map((pin) => pin.plugin),
|
|
9296
|
+
...FORMER_GLOBAL_PLUGINS.map((pin) => pin.plugin)
|
|
9297
|
+
])
|
|
9298
|
+
];
|
|
9145
9299
|
const skillRefIssues = await findUnresolvedSkillReferences(root, {
|
|
9146
|
-
pluginNamespaces: activeNamespaces
|
|
9300
|
+
pluginNamespaces: activeNamespaces,
|
|
9301
|
+
knownPluginNamespaces: knownNamespaces
|
|
9147
9302
|
});
|
|
9148
9303
|
if (skillRefIssues.length > 0) {
|
|
9149
9304
|
const prefixBug = skillRefIssues.filter((i) => i.bareInstalled);
|
|
@@ -9165,27 +9320,83 @@ async function runDoctor(options) {
|
|
|
9165
9320
|
);
|
|
9166
9321
|
}
|
|
9167
9322
|
}
|
|
9168
|
-
const
|
|
9323
|
+
const recommendedEntries = recommendation && Array.isArray(recommendation.recommended) ? recommendation.recommended : null;
|
|
9324
|
+
const recommendedIds = new Set(
|
|
9325
|
+
(recommendedEntries ?? []).map((entry) => entry.id).filter((id) => typeof id === "string")
|
|
9326
|
+
);
|
|
9327
|
+
const eligibilityKnown = recommendedEntries !== null;
|
|
9328
|
+
const catalogAgents = catalogItems === null ? [] : catalogItems.flatMap((item) => {
|
|
9329
|
+
if (item.type !== "agent") return [];
|
|
9330
|
+
const target = installTargetFor(item);
|
|
9331
|
+
return target.ok ? [{ id: item.id, name: path48.basename(target.relPath, ".md") }] : [];
|
|
9332
|
+
});
|
|
9333
|
+
const STACK_GATE_SKIP_CODES = /* @__PURE__ */ new Set([
|
|
9334
|
+
"requires-any-unsatisfied",
|
|
9335
|
+
"no-role-stack-match",
|
|
9336
|
+
"required-role-missing"
|
|
9337
|
+
]);
|
|
9338
|
+
const skippedEntries = Array.isArray(recommendation?.skipped) ? recommendation.skipped : [];
|
|
9339
|
+
const stackGatedIds = /* @__PURE__ */ new Map();
|
|
9340
|
+
for (const entry of skippedEntries) {
|
|
9341
|
+
if (typeof entry.id !== "string" || !Array.isArray(entry.skipReasons)) continue;
|
|
9342
|
+
for (const reason of entry.skipReasons) {
|
|
9343
|
+
if (typeof reason?.code !== "string" || !STACK_GATE_SKIP_CODES.has(reason.code)) continue;
|
|
9344
|
+
if (!stackGatedIds.has(entry.id)) {
|
|
9345
|
+
stackGatedIds.set(entry.id, typeof reason.signal === "string" ? reason.signal : "");
|
|
9346
|
+
}
|
|
9347
|
+
}
|
|
9348
|
+
}
|
|
9349
|
+
const gatedOutAgentSignals = /* @__PURE__ */ new Map();
|
|
9350
|
+
for (const agent of catalogAgents) {
|
|
9351
|
+
if (recommendedIds.has(agent.id)) continue;
|
|
9352
|
+
const signal = stackGatedIds.get(agent.id);
|
|
9353
|
+
if (signal !== void 0) gatedOutAgentSignals.set(agent.name, signal);
|
|
9354
|
+
}
|
|
9169
9355
|
const agentRefIssues = await findUnresolvedAgentReferences(root, {
|
|
9170
|
-
catalogAgentNames:
|
|
9356
|
+
catalogAgentNames: catalogAgents.map((agent) => agent.name)
|
|
9171
9357
|
});
|
|
9172
9358
|
if (agentRefIssues.length > 0) {
|
|
9173
9359
|
const describeAgent = (i) => `${i.file}:${i.line} "${i.agentName}"`;
|
|
9174
|
-
const
|
|
9360
|
+
const referent = (group) => {
|
|
9361
|
+
const kinds = new Set(group.map((i) => i.kind));
|
|
9362
|
+
if (kinds.size === 1 && kinds.has("label")) return "a skill";
|
|
9363
|
+
return kinds.has("label") ? "an agent or skill" : "an agent";
|
|
9364
|
+
};
|
|
9365
|
+
const routedTo = (group, names) => {
|
|
9366
|
+
const list = names.join(", ");
|
|
9367
|
+
const kinds = new Set(group.map((i) => i.kind));
|
|
9368
|
+
if (kinds.size === 1 && kinds.has("label")) return `hand work to the skill ${list}`;
|
|
9369
|
+
return kinds.has("label") ? `point at ${list}` : `send agents after ${list}`;
|
|
9370
|
+
};
|
|
9371
|
+
const stackGatedHere = (i) => eligibilityKnown && gatedOutAgentSignals.has(i.agentName);
|
|
9372
|
+
const installable = agentRefIssues.filter((i) => i.inCatalog && !stackGatedHere(i));
|
|
9373
|
+
const gatedOut = agentRefIssues.filter((i) => i.inCatalog && stackGatedHere(i));
|
|
9175
9374
|
const unknown = agentRefIssues.filter((i) => !i.inCatalog);
|
|
9176
9375
|
if (installable.length > 0) {
|
|
9177
9376
|
const names = [...new Set(installable.map((i) => i.agentName))];
|
|
9178
9377
|
flag(
|
|
9179
|
-
`- AGENT REFS: ${installable.length} reference(s) point at
|
|
9180
|
-
`${installable.length} rule/agent reference(s)
|
|
9378
|
+
`- AGENT REFS: ${installable.length} reference(s) point at ${referent(installable)} the catalog can install but this project has not (${names.join(", ")}) (${installable.slice(0, 3).map(describeAgent).join("; ")}${installable.length > 3 ? "; \u2026" : ""})`,
|
|
9379
|
+
`${installable.length} rule/agent reference(s) ${routedTo(installable, names)}, which is available but not installed`,
|
|
9181
9380
|
"find its catalog id and run `haus recommend --include <id>` then `haus apply --write`"
|
|
9182
9381
|
);
|
|
9183
9382
|
}
|
|
9383
|
+
if (gatedOut.length > 0) {
|
|
9384
|
+
const names = [...new Set(gatedOut.map((i) => i.agentName))];
|
|
9385
|
+
const unmet = names.map((name) => {
|
|
9386
|
+
const signal = gatedOutAgentSignals.get(name);
|
|
9387
|
+
return signal ? `${name} needs ${signal}` : name;
|
|
9388
|
+
}).join("; ");
|
|
9389
|
+
suggest(
|
|
9390
|
+
`- AGENT REFS: ${gatedOut.length} reference(s) name ${referent(gatedOut)} the catalog gates out of this project's stack (${unmet}) (${gatedOut.slice(0, 3).map(describeAgent).join("; ")}${gatedOut.length > 3 ? "; \u2026" : ""})`,
|
|
9391
|
+
`${gatedOut.length} rule/agent reference(s) ${routedTo(gatedOut, names)}, which the catalog does not offer a project with this stack (${unmet}) \u2014 the reference is upstream prose, not a missing install`,
|
|
9392
|
+
`nothing to install here: correct or drop the reference upstream (catalog-owned prose) \u2014 the catalog gates ${names.join(", ")} out of this stack on purpose`
|
|
9393
|
+
);
|
|
9394
|
+
}
|
|
9184
9395
|
if (unknown.length > 0) {
|
|
9185
9396
|
const names = [...new Set(unknown.map((i) => i.agentName))];
|
|
9186
9397
|
suggest(
|
|
9187
|
-
`- AGENT REFS: ${unknown.length} reference(s) name
|
|
9188
|
-
`${unknown.length} rule/agent reference(s)
|
|
9398
|
+
`- AGENT REFS: ${unknown.length} reference(s) name ${referent(unknown)} that exists neither here nor in the catalog (${names.join(", ")}) (${unknown.slice(0, 3).map(describeAgent).join("; ")}${unknown.length > 3 ? "; \u2026" : ""})`,
|
|
9399
|
+
`${unknown.length} rule/agent reference(s) ${routedTo(unknown, names)}, which was never shipped`,
|
|
9189
9400
|
"correct or drop the reference upstream (catalog-owned prose), so agents stop being sent after a tool that does not exist"
|
|
9190
9401
|
);
|
|
9191
9402
|
}
|
|
@@ -9193,9 +9404,6 @@ async function runDoctor(options) {
|
|
|
9193
9404
|
if (skillRefIssues.length === 0 && agentRefIssues.length === 0) {
|
|
9194
9405
|
ok("- SKILL REFS: OK (no unresolved cross-skill or agent references)");
|
|
9195
9406
|
}
|
|
9196
|
-
const recommendedIds = new Set(
|
|
9197
|
-
(recommendation?.recommended ?? []).map((entry) => entry.id).filter((id) => typeof id === "string")
|
|
9198
|
-
);
|
|
9199
9407
|
if (catalogItems !== null && recommendedIds.size > 0) {
|
|
9200
9408
|
const catalogIds = new Set(catalogItems.map((item) => item.id));
|
|
9201
9409
|
const excludedIds = new Set(await readProjectExcludedIds(root));
|
|
@@ -9415,7 +9623,7 @@ async function runDoctor(options) {
|
|
|
9415
9623
|
}
|
|
9416
9624
|
|
|
9417
9625
|
// src/commands/update.ts
|
|
9418
|
-
import
|
|
9626
|
+
import path52 from "node:path";
|
|
9419
9627
|
init_github_rate_limit();
|
|
9420
9628
|
init_private_catalog_access();
|
|
9421
9629
|
|
|
@@ -9442,8 +9650,8 @@ async function refreshProjectApply(root) {
|
|
|
9442
9650
|
|
|
9443
9651
|
// src/install/apply.ts
|
|
9444
9652
|
import crypto3 from "node:crypto";
|
|
9445
|
-
import
|
|
9446
|
-
import
|
|
9653
|
+
import path50 from "node:path";
|
|
9654
|
+
import fs43 from "fs-extra";
|
|
9447
9655
|
init_fs();
|
|
9448
9656
|
init_logger();
|
|
9449
9657
|
init_paths();
|
|
@@ -9550,72 +9758,146 @@ ${rest}`;
|
|
|
9550
9758
|
${content2}`;
|
|
9551
9759
|
}
|
|
9552
9760
|
|
|
9761
|
+
// src/install/internal-channel.ts
|
|
9762
|
+
init_fs();
|
|
9763
|
+
import path49 from "node:path";
|
|
9764
|
+
import fs42 from "fs-extra";
|
|
9765
|
+
var INSTALL_CONFIG_SCHEMA = "haus-install-config/1";
|
|
9766
|
+
var INTERNAL_NAME_PREFIXES = ["request", "slack-cowork"];
|
|
9767
|
+
function internalPrefixMatch(name) {
|
|
9768
|
+
return INTERNAL_NAME_PREFIXES.some(
|
|
9769
|
+
(prefix) => name === prefix || name.startsWith(`${prefix}-`) || name.startsWith(`${prefix}.`)
|
|
9770
|
+
);
|
|
9771
|
+
}
|
|
9772
|
+
function isInternalAsset(stableId) {
|
|
9773
|
+
const separator = stableId.indexOf(".");
|
|
9774
|
+
if (separator === -1) return false;
|
|
9775
|
+
const kind = stableId.slice(0, separator);
|
|
9776
|
+
const name = stableId.slice(separator + 1);
|
|
9777
|
+
if (kind !== "skill" && kind !== "agent") return false;
|
|
9778
|
+
return internalPrefixMatch(name);
|
|
9779
|
+
}
|
|
9780
|
+
function installConfigPath() {
|
|
9781
|
+
return path49.join(globalClaudeDir(), "haus", "install-config.json");
|
|
9782
|
+
}
|
|
9783
|
+
async function readInstallConfig() {
|
|
9784
|
+
const raw = await readJson(installConfigPath());
|
|
9785
|
+
if (!raw || typeof raw.internal !== "boolean") return void 0;
|
|
9786
|
+
return raw;
|
|
9787
|
+
}
|
|
9788
|
+
async function writeInstallConfig(config) {
|
|
9789
|
+
await writeJson(installConfigPath(), {
|
|
9790
|
+
_schema: INSTALL_CONFIG_SCHEMA,
|
|
9791
|
+
internal: config.internal,
|
|
9792
|
+
source: config.source,
|
|
9793
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9794
|
+
});
|
|
9795
|
+
}
|
|
9796
|
+
async function removeInstallConfig() {
|
|
9797
|
+
const p = installConfigPath();
|
|
9798
|
+
if (fs42.pathExistsSync(p)) await fs42.remove(p);
|
|
9799
|
+
}
|
|
9800
|
+
function envOverride() {
|
|
9801
|
+
const raw = process.env.HAUS_INTERNAL_CHANNEL?.trim().toLowerCase();
|
|
9802
|
+
if (raw === void 0 || raw === "") return void 0;
|
|
9803
|
+
if (["1", "true", "yes", "on"].includes(raw)) return true;
|
|
9804
|
+
if (["0", "false", "no", "off"].includes(raw)) return false;
|
|
9805
|
+
return void 0;
|
|
9806
|
+
}
|
|
9807
|
+
function manifestHasInternal(manifest) {
|
|
9808
|
+
return (manifest?.files ?? []).some((file) => isInternalAsset(file.stableId));
|
|
9809
|
+
}
|
|
9810
|
+
async function resolveInternalChannel(manifest, requested) {
|
|
9811
|
+
if (requested !== void 0) return { enabled: requested, source: "flag" };
|
|
9812
|
+
const override = envOverride();
|
|
9813
|
+
if (override !== void 0) return { enabled: override, source: "env" };
|
|
9814
|
+
const config = await readInstallConfig();
|
|
9815
|
+
if (config) return { enabled: config.internal, source: "config" };
|
|
9816
|
+
if (manifestHasInternal(manifest)) return { enabled: true, source: "grandfathered" };
|
|
9817
|
+
return { enabled: false, source: "default" };
|
|
9818
|
+
}
|
|
9819
|
+
|
|
9553
9820
|
// src/install/apply.ts
|
|
9554
9821
|
var SCHEMA_VERSION2 = "1";
|
|
9822
|
+
async function pruneEmptyDirsUpward(dirs, stopAt) {
|
|
9823
|
+
const ordered = [...new Set(dirs)].sort((a, b) => b.length - a.length);
|
|
9824
|
+
for (const start of ordered) {
|
|
9825
|
+
let dir = start;
|
|
9826
|
+
while (dir.startsWith(stopAt) && dir !== stopAt) {
|
|
9827
|
+
try {
|
|
9828
|
+
if ((await fs43.readdir(dir)).length > 0) break;
|
|
9829
|
+
await fs43.remove(dir);
|
|
9830
|
+
} catch {
|
|
9831
|
+
break;
|
|
9832
|
+
}
|
|
9833
|
+
dir = path50.dirname(dir);
|
|
9834
|
+
}
|
|
9835
|
+
}
|
|
9836
|
+
}
|
|
9555
9837
|
function hashContent(content2) {
|
|
9556
9838
|
return `sha256-${crypto3.createHash("sha256").update(normaliseLF(content2)).digest("hex")}`;
|
|
9557
9839
|
}
|
|
9558
9840
|
function destFileMissing(destPath) {
|
|
9559
9841
|
try {
|
|
9560
|
-
return !
|
|
9842
|
+
return !fs43.statSync(destPath).isFile();
|
|
9561
9843
|
} catch {
|
|
9562
9844
|
return true;
|
|
9563
9845
|
}
|
|
9564
9846
|
}
|
|
9565
9847
|
function sourceVersion() {
|
|
9566
9848
|
try {
|
|
9567
|
-
const pkgPath =
|
|
9568
|
-
const pkg = JSON.parse(
|
|
9849
|
+
const pkgPath = path50.join(packageRoot(), "package.json");
|
|
9850
|
+
const pkg = JSON.parse(fs43.readFileSync(pkgPath, "utf8"));
|
|
9569
9851
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
9570
9852
|
} catch {
|
|
9571
9853
|
return "haus@0.0.0";
|
|
9572
9854
|
}
|
|
9573
9855
|
}
|
|
9574
9856
|
function globalSrcDir() {
|
|
9575
|
-
return
|
|
9857
|
+
return path50.join(packageRoot(), "library", "global");
|
|
9576
9858
|
}
|
|
9577
9859
|
function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
9578
|
-
const skillDir =
|
|
9860
|
+
const skillDir = path50.join(skillsDir, skillName);
|
|
9579
9861
|
const entries = [];
|
|
9580
|
-
for (const rel of
|
|
9862
|
+
for (const rel of fs43.readdirSync(skillDir, { recursive: true })) {
|
|
9581
9863
|
if (rel === "SKILL.md") continue;
|
|
9582
|
-
const abs =
|
|
9583
|
-
if (!
|
|
9584
|
-
const posixRel = rel.split(
|
|
9864
|
+
const abs = path50.join(skillDir, rel);
|
|
9865
|
+
if (!fs43.statSync(abs).isFile()) continue;
|
|
9866
|
+
const posixRel = rel.split(path50.sep).join("/");
|
|
9585
9867
|
entries.push({
|
|
9586
9868
|
stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
|
|
9587
|
-
srcRelPath:
|
|
9588
|
-
destPath:
|
|
9869
|
+
srcRelPath: path50.join("library", "global", "skills", skillName, rel),
|
|
9870
|
+
destPath: path50.join(claudeDir, "skills", skillName, rel)
|
|
9589
9871
|
});
|
|
9590
9872
|
}
|
|
9591
9873
|
return entries;
|
|
9592
9874
|
}
|
|
9593
9875
|
function collectFlatChannel(srcDir, claudeDir, dirName, idPrefix) {
|
|
9594
9876
|
const entries = [];
|
|
9595
|
-
const channelDir =
|
|
9596
|
-
if (!
|
|
9597
|
-
for (const fileName of
|
|
9877
|
+
const channelDir = path50.join(srcDir, dirName);
|
|
9878
|
+
if (!fs43.pathExistsSync(channelDir)) return entries;
|
|
9879
|
+
for (const fileName of fs43.readdirSync(channelDir)) {
|
|
9598
9880
|
if (!fileName.endsWith(".md")) continue;
|
|
9599
9881
|
const name = fileName.slice(0, -".md".length);
|
|
9600
9882
|
entries.push({
|
|
9601
9883
|
stableId: `${idPrefix}.${name}`,
|
|
9602
|
-
srcRelPath:
|
|
9603
|
-
destPath:
|
|
9884
|
+
srcRelPath: path50.join("library", "global", dirName, fileName),
|
|
9885
|
+
destPath: path50.join(claudeDir, dirName, fileName)
|
|
9604
9886
|
});
|
|
9605
9887
|
}
|
|
9606
9888
|
return entries;
|
|
9607
9889
|
}
|
|
9608
9890
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
9609
9891
|
const entries = [];
|
|
9610
|
-
const skillsDir =
|
|
9611
|
-
if (
|
|
9612
|
-
for (const skillName of
|
|
9613
|
-
const skillFile =
|
|
9614
|
-
if (
|
|
9892
|
+
const skillsDir = path50.join(srcDir, "skills");
|
|
9893
|
+
if (fs43.pathExistsSync(skillsDir)) {
|
|
9894
|
+
for (const skillName of fs43.readdirSync(skillsDir)) {
|
|
9895
|
+
const skillFile = path50.join(skillsDir, skillName, "SKILL.md");
|
|
9896
|
+
if (fs43.pathExistsSync(skillFile)) {
|
|
9615
9897
|
entries.push({
|
|
9616
9898
|
stableId: `skill.${skillName}`,
|
|
9617
|
-
srcRelPath:
|
|
9618
|
-
destPath:
|
|
9899
|
+
srcRelPath: path50.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
9900
|
+
destPath: path50.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
9619
9901
|
});
|
|
9620
9902
|
entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
|
|
9621
9903
|
}
|
|
@@ -9626,7 +9908,7 @@ function collectSourceFiles(srcDir, claudeDir) {
|
|
|
9626
9908
|
return entries;
|
|
9627
9909
|
}
|
|
9628
9910
|
async function applyInstall(options = {}) {
|
|
9629
|
-
const { dryRun = false, force = false, check = false } = options;
|
|
9911
|
+
const { dryRun = false, force = false, check = false, internal } = options;
|
|
9630
9912
|
const claudeDir = globalClaudeDir();
|
|
9631
9913
|
const srcDir = globalSrcDir();
|
|
9632
9914
|
const source = sourceVersion();
|
|
@@ -9637,7 +9919,10 @@ async function applyInstall(options = {}) {
|
|
|
9637
9919
|
]);
|
|
9638
9920
|
const existingManifest = await readManifest();
|
|
9639
9921
|
const manifestByDest = new Map(existingManifest?.files.map((f) => [f.destPath, f]) ?? []);
|
|
9640
|
-
const
|
|
9922
|
+
const internalChannel = await resolveInternalChannel(existingManifest, internal);
|
|
9923
|
+
const sourceFiles = collectSourceFiles(srcDir, claudeDir).filter(
|
|
9924
|
+
(entry) => internalChannel.enabled || !isInternalAsset(entry.stableId)
|
|
9925
|
+
);
|
|
9641
9926
|
const result = {
|
|
9642
9927
|
created: [],
|
|
9643
9928
|
updated: [],
|
|
@@ -9647,11 +9932,12 @@ async function applyInstall(options = {}) {
|
|
|
9647
9932
|
deleted: [],
|
|
9648
9933
|
hookIds: [],
|
|
9649
9934
|
configDirMigration,
|
|
9935
|
+
internalChannel,
|
|
9650
9936
|
drift: false
|
|
9651
9937
|
};
|
|
9652
9938
|
const manifestFiles = [];
|
|
9653
9939
|
for (const entry of sourceFiles) {
|
|
9654
|
-
const srcPath =
|
|
9940
|
+
const srcPath = path50.join(packageRoot(), entry.srcRelPath);
|
|
9655
9941
|
const rawContent = await readText(srcPath);
|
|
9656
9942
|
if (rawContent === void 0) {
|
|
9657
9943
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -9680,7 +9966,7 @@ async function applyInstall(options = {}) {
|
|
|
9680
9966
|
}
|
|
9681
9967
|
continue;
|
|
9682
9968
|
}
|
|
9683
|
-
const destExists =
|
|
9969
|
+
const destExists = fs43.pathExistsSync(entry.destPath);
|
|
9684
9970
|
if (destExists) {
|
|
9685
9971
|
const currentContent = await readText(entry.destPath);
|
|
9686
9972
|
if (currentContent !== void 0) {
|
|
@@ -9720,7 +10006,7 @@ async function applyInstall(options = {}) {
|
|
|
9720
10006
|
}
|
|
9721
10007
|
let mergedSettings;
|
|
9722
10008
|
if (!check) {
|
|
9723
|
-
const fragmentPath =
|
|
10009
|
+
const fragmentPath = path50.join(srcDir, "settings-fragments", "hooks.json");
|
|
9724
10010
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
9725
10011
|
const settings = await readSettings();
|
|
9726
10012
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -9731,6 +10017,7 @@ async function applyInstall(options = {}) {
|
|
|
9731
10017
|
result.hookIds = addedIds;
|
|
9732
10018
|
}
|
|
9733
10019
|
if (!check && existingManifest) {
|
|
10020
|
+
const emptiedDirs = /* @__PURE__ */ new Set();
|
|
9734
10021
|
const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
|
|
9735
10022
|
for (const entry of existingManifest.files) {
|
|
9736
10023
|
if (currentDestPaths.has(entry.destPath)) continue;
|
|
@@ -9739,19 +10026,21 @@ async function applyInstall(options = {}) {
|
|
|
9739
10026
|
manifestFiles.push(entry);
|
|
9740
10027
|
continue;
|
|
9741
10028
|
}
|
|
9742
|
-
if (!
|
|
10029
|
+
if (!fs43.pathExistsSync(entry.destPath)) continue;
|
|
9743
10030
|
const content2 = await readText(entry.destPath);
|
|
9744
10031
|
if (!content2) continue;
|
|
9745
10032
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
9746
10033
|
const currentHash = hashContent(content2);
|
|
9747
10034
|
if (hasHeader && currentHash === entry.hash) {
|
|
9748
|
-
if (!dryRun) await
|
|
10035
|
+
if (!dryRun) await fs43.remove(entry.destPath);
|
|
10036
|
+
emptiedDirs.add(path50.dirname(entry.destPath));
|
|
9749
10037
|
result.deleted.push(entry.destPath);
|
|
9750
10038
|
} else {
|
|
9751
10039
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
9752
10040
|
result.skipped.push(entry.destPath);
|
|
9753
10041
|
}
|
|
9754
10042
|
}
|
|
10043
|
+
if (!dryRun) await pruneEmptyDirsUpward(emptiedDirs, claudeDir);
|
|
9755
10044
|
}
|
|
9756
10045
|
if (!dryRun && !check && mergedSettings) {
|
|
9757
10046
|
await writeSettings(mergedSettings);
|
|
@@ -9762,9 +10051,27 @@ async function applyInstall(options = {}) {
|
|
|
9762
10051
|
existingManifest?.plugins
|
|
9763
10052
|
);
|
|
9764
10053
|
await writeManifest(manifest);
|
|
10054
|
+
if (internalChannel.source === "grandfathered")
|
|
10055
|
+
await writeInstallConfig({ internal: true, source: "grandfathered" });
|
|
10056
|
+
if (internalChannel.source === "flag")
|
|
10057
|
+
await writeInstallConfig({ internal: internalChannel.enabled, source: "explicit" });
|
|
9765
10058
|
}
|
|
9766
10059
|
return result;
|
|
9767
10060
|
}
|
|
10061
|
+
function printInternalChannel(result, prefix) {
|
|
10062
|
+
const { source, enabled } = result.internalChannel;
|
|
10063
|
+
if (source === "grandfathered") {
|
|
10064
|
+
log(
|
|
10065
|
+
`${prefix}Internal channel kept on: this machine already had the request skills. Turn it off with \`haus install --no-internal\`.`
|
|
10066
|
+
);
|
|
10067
|
+
return;
|
|
10068
|
+
}
|
|
10069
|
+
if (!enabled && result.deleted.some((p) => p.includes("request") || p.includes("slack-cowork"))) {
|
|
10070
|
+
log(
|
|
10071
|
+
`${prefix}Internal channel is off \u2014 the request skills and slack-cowork were removed. Bring them back with \`haus install --internal\`.`
|
|
10072
|
+
);
|
|
10073
|
+
}
|
|
10074
|
+
}
|
|
9768
10075
|
function printApplyResult(result, dryRun) {
|
|
9769
10076
|
const prefix = dryRun ? "[dry-run] " : "";
|
|
9770
10077
|
printConfigDirMigration(result.configDirMigration);
|
|
@@ -9788,6 +10095,7 @@ function printApplyResult(result, dryRun) {
|
|
|
9788
10095
|
if (result.hookIds.length) {
|
|
9789
10096
|
log(`${prefix}Hooks added: ${result.hookIds.join(", ")}`);
|
|
9790
10097
|
}
|
|
10098
|
+
printInternalChannel(result, prefix);
|
|
9791
10099
|
if (result.drift) {
|
|
9792
10100
|
warn("Install drift detected \u2014 run `haus install` to sync.");
|
|
9793
10101
|
}
|
|
@@ -9819,7 +10127,7 @@ function summarizeLockDiff(before, after) {
|
|
|
9819
10127
|
// src/update/lockfile.ts
|
|
9820
10128
|
import { constants as fsConstants } from "node:fs";
|
|
9821
10129
|
import { copyFile, mkdir, readFile as readFile2 } from "node:fs/promises";
|
|
9822
|
-
import
|
|
10130
|
+
import path51 from "node:path";
|
|
9823
10131
|
init_fs();
|
|
9824
10132
|
init_paths();
|
|
9825
10133
|
async function readLockSummary(root) {
|
|
@@ -9859,7 +10167,7 @@ async function backupLockfile(root) {
|
|
|
9859
10167
|
await mkdir(backupDir, { recursive: true });
|
|
9860
10168
|
let stamp = Date.now();
|
|
9861
10169
|
for (; ; ) {
|
|
9862
|
-
const dest =
|
|
10170
|
+
const dest = path51.join(backupDir, lockBackupFileName(stamp));
|
|
9863
10171
|
try {
|
|
9864
10172
|
await copyFile(lockPath, dest, fsConstants.COPYFILE_EXCL);
|
|
9865
10173
|
return;
|
|
@@ -9910,7 +10218,7 @@ function diffLock(before, after) {
|
|
|
9910
10218
|
}
|
|
9911
10219
|
async function hasLocalOverrides(root) {
|
|
9912
10220
|
try {
|
|
9913
|
-
await readFile2(
|
|
10221
|
+
await readFile2(path51.join(root, ".claude", "settings.json"), "utf8");
|
|
9914
10222
|
return true;
|
|
9915
10223
|
} catch {
|
|
9916
10224
|
return false;
|
|
@@ -9955,7 +10263,7 @@ async function runUpdate(options) {
|
|
|
9955
10263
|
return;
|
|
9956
10264
|
}
|
|
9957
10265
|
if (options.check) {
|
|
9958
|
-
const pkgJson2 = await readJson(
|
|
10266
|
+
const pkgJson2 = await readJson(path52.join(packageRoot(), "package.json"));
|
|
9959
10267
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
9960
10268
|
const [
|
|
9961
10269
|
status,
|
|
@@ -10007,7 +10315,7 @@ async function runUpdate(options) {
|
|
|
10007
10315
|
}
|
|
10008
10316
|
return;
|
|
10009
10317
|
}
|
|
10010
|
-
const pkgJson = await readJson(
|
|
10318
|
+
const pkgJson = await readJson(path52.join(packageRoot(), "package.json"));
|
|
10011
10319
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
10012
10320
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
10013
10321
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -10078,7 +10386,7 @@ async function runFromHookCheck(root) {
|
|
|
10078
10386
|
try {
|
|
10079
10387
|
const summary = await readLockSummary(root);
|
|
10080
10388
|
if (summary.count === 0) return;
|
|
10081
|
-
const pkgJson = await readJson(
|
|
10389
|
+
const pkgJson = await readJson(path52.join(packageRoot(), "package.json"));
|
|
10082
10390
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
10083
10391
|
const pin = readProjectCatalogPin(root);
|
|
10084
10392
|
const honoursPin = pin.kind === "pinned" && summary.catalogRef === pin.ref;
|
|
@@ -10145,6 +10453,7 @@ async function refreshGlobalInstall() {
|
|
|
10145
10453
|
async function ensureCatalogPinnedPlugins(root) {
|
|
10146
10454
|
try {
|
|
10147
10455
|
const pins = await loadCatalogPluginPins(root);
|
|
10456
|
+
await cleanupStaleGlobalPlugins(pins);
|
|
10148
10457
|
const ensured = await ensureGlobalPlugins(pins);
|
|
10149
10458
|
await recordManifestPlugins(
|
|
10150
10459
|
pins.filter((pin) => ensured.ensured.includes(pin.id)).map((pin) => ({ ...pin, installedAt: (/* @__PURE__ */ new Date()).toISOString() }))
|
|
@@ -10153,11 +10462,21 @@ async function ensureCatalogPinnedPlugins(root) {
|
|
|
10153
10462
|
warn(`Could not update global plugins: ${err instanceof Error ? err.message : String(err)}`);
|
|
10154
10463
|
}
|
|
10155
10464
|
}
|
|
10156
|
-
async function detectGlobalPluginDrift(root) {
|
|
10465
|
+
async function detectGlobalPluginDrift(root, deps = {}) {
|
|
10157
10466
|
try {
|
|
10158
10467
|
const pins = await loadCatalogPluginPins(root);
|
|
10159
|
-
const state = await probePluginEnv();
|
|
10160
|
-
|
|
10468
|
+
const state = await probePluginEnv(deps);
|
|
10469
|
+
const manifest = await readManifest();
|
|
10470
|
+
const recorded = (manifest?.plugins ?? []).filter(isValidPin);
|
|
10471
|
+
const protectedPins = [...pins, ...DEFAULT_GLOBAL_PLUGINS];
|
|
10472
|
+
const stale = planStalePluginRemovals(
|
|
10473
|
+
protectedPins,
|
|
10474
|
+
[...recorded, ...FORMER_GLOBAL_PLUGINS],
|
|
10475
|
+
state
|
|
10476
|
+
);
|
|
10477
|
+
const pinned = pluginDriftStatus(planPluginActions(pins, state), { matchRef: true });
|
|
10478
|
+
if (pinned === null) return null;
|
|
10479
|
+
return pinned || stale.length > 0;
|
|
10161
10480
|
} catch {
|
|
10162
10481
|
return null;
|
|
10163
10482
|
}
|
|
@@ -10262,7 +10581,7 @@ init_exec();
|
|
|
10262
10581
|
init_logger();
|
|
10263
10582
|
import { existsSync as existsSync2 } from "node:fs";
|
|
10264
10583
|
import { readdir } from "node:fs/promises";
|
|
10265
|
-
import
|
|
10584
|
+
import path53 from "node:path";
|
|
10266
10585
|
var GIT_LOCATION_VARS = [
|
|
10267
10586
|
"GIT_DIR",
|
|
10268
10587
|
"GIT_WORK_TREE",
|
|
@@ -10322,11 +10641,11 @@ async function runClone(url, opts = {}) {
|
|
|
10322
10641
|
process.exitCode = 1;
|
|
10323
10642
|
return;
|
|
10324
10643
|
}
|
|
10325
|
-
const target =
|
|
10644
|
+
const target = path53.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
10326
10645
|
if (existsSync2(target) && !await isEmptyDirectory(target)) {
|
|
10327
10646
|
const check = await checkExistingTarget(target, url);
|
|
10328
10647
|
if (check.kind === "matches") {
|
|
10329
|
-
log(`\u2022 ${
|
|
10648
|
+
log(`\u2022 ${path53.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
|
|
10330
10649
|
return;
|
|
10331
10650
|
}
|
|
10332
10651
|
const message2 = check.kind === "not-a-repo" ? `${target} already exists and is not a git repository \u2014 refusing to clone ${url} there. Remove or rename it, or pass a different [dir].` : `${target} already exists as a different repository (origin: ${check.origin ?? "(no origin remote)"}) \u2014 refusing to clone ${url} there. Remove or rename it, or pass a different [dir].`;
|
|
@@ -10375,8 +10694,8 @@ function isCatalogItemInstallable(item) {
|
|
|
10375
10694
|
|
|
10376
10695
|
// src/install/scaffold.ts
|
|
10377
10696
|
init_logger();
|
|
10378
|
-
import
|
|
10379
|
-
import
|
|
10697
|
+
import path54 from "node:path";
|
|
10698
|
+
import fs44 from "fs-extra";
|
|
10380
10699
|
async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
10381
10700
|
const scaffolded = [];
|
|
10382
10701
|
const skipped = [];
|
|
@@ -10389,27 +10708,27 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
10389
10708
|
skipped.push(name);
|
|
10390
10709
|
if (outcome === "skipped-exists") skippedExisting.push(name);
|
|
10391
10710
|
};
|
|
10392
|
-
const realRoot = await
|
|
10711
|
+
const realRoot = await fs44.realpath(catalogRoot).catch(() => null);
|
|
10393
10712
|
for (const item of items) {
|
|
10394
10713
|
if (item.type !== "config") continue;
|
|
10395
|
-
const sourcePath =
|
|
10396
|
-
const rel =
|
|
10397
|
-
const escapes = rel === "" ||
|
|
10714
|
+
const sourcePath = path54.resolve(catalogRoot, item.path);
|
|
10715
|
+
const rel = path54.relative(catalogRoot, sourcePath);
|
|
10716
|
+
const escapes = rel === "" || path54.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
|
|
10398
10717
|
if (escapes) {
|
|
10399
10718
|
warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
|
|
10400
10719
|
continue;
|
|
10401
10720
|
}
|
|
10402
|
-
const realSource = await
|
|
10721
|
+
const realSource = await fs44.realpath(sourcePath).catch(() => null);
|
|
10403
10722
|
if (!realRoot || !realSource) {
|
|
10404
10723
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
10405
10724
|
continue;
|
|
10406
10725
|
}
|
|
10407
|
-
const realRel =
|
|
10408
|
-
if (realRel !== "" && (
|
|
10726
|
+
const realRel = path54.relative(realRoot, realSource);
|
|
10727
|
+
if (realRel !== "" && (path54.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
|
|
10409
10728
|
warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
|
|
10410
10729
|
continue;
|
|
10411
10730
|
}
|
|
10412
|
-
const stat = await
|
|
10731
|
+
const stat = await fs44.lstat(sourcePath).catch(() => null);
|
|
10413
10732
|
if (!stat) {
|
|
10414
10733
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
10415
10734
|
continue;
|
|
@@ -10419,14 +10738,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
10419
10738
|
continue;
|
|
10420
10739
|
}
|
|
10421
10740
|
if (stat.isFile()) {
|
|
10422
|
-
const filename =
|
|
10423
|
-
const dest =
|
|
10741
|
+
const filename = path54.basename(sourcePath);
|
|
10742
|
+
const dest = path54.join(projectRoot, filename);
|
|
10424
10743
|
record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
|
|
10425
10744
|
} else if (stat.isDirectory()) {
|
|
10426
|
-
const entries = await
|
|
10745
|
+
const entries = await fs44.readdir(sourcePath);
|
|
10427
10746
|
for (const entry of entries) {
|
|
10428
|
-
const src =
|
|
10429
|
-
const dest =
|
|
10747
|
+
const src = path54.join(sourcePath, entry);
|
|
10748
|
+
const dest = path54.join(projectRoot, entry);
|
|
10430
10749
|
record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
|
|
10431
10750
|
}
|
|
10432
10751
|
}
|
|
@@ -10434,42 +10753,42 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
10434
10753
|
return { scaffolded, skipped, skippedExisting };
|
|
10435
10754
|
}
|
|
10436
10755
|
async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
10437
|
-
const destDisplay =
|
|
10438
|
-
const srcStat = await
|
|
10756
|
+
const destDisplay = path54.relative(projectRoot, dest) || path54.basename(dest);
|
|
10757
|
+
const srcStat = await fs44.lstat(src).catch(() => null);
|
|
10439
10758
|
if (!srcStat) {
|
|
10440
|
-
warn(`Skipping ${
|
|
10759
|
+
warn(`Skipping ${path54.basename(src)}: source not found`);
|
|
10441
10760
|
return "skipped-refused";
|
|
10442
10761
|
}
|
|
10443
10762
|
if (srcStat.isSymbolicLink()) {
|
|
10444
|
-
warn(`Skipping ${
|
|
10763
|
+
warn(`Skipping ${path54.basename(src)}: source is a symlink`);
|
|
10445
10764
|
return "skipped-refused";
|
|
10446
10765
|
}
|
|
10447
|
-
const exists = await
|
|
10766
|
+
const exists = await fs44.pathExists(dest);
|
|
10448
10767
|
if (exists && !opts.force) {
|
|
10449
10768
|
warn(`Skipping ${destDisplay}: already exists \u2014 re-run with --force to overwrite it`);
|
|
10450
10769
|
return "skipped-exists";
|
|
10451
10770
|
}
|
|
10452
10771
|
if (opts.dryRun) {
|
|
10453
|
-
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${
|
|
10772
|
+
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path54.basename(dest)} (${itemId})`);
|
|
10454
10773
|
return "scaffolded";
|
|
10455
10774
|
}
|
|
10456
10775
|
if (exists && opts.force) {
|
|
10457
|
-
await
|
|
10776
|
+
await fs44.remove(dest);
|
|
10458
10777
|
}
|
|
10459
|
-
await
|
|
10460
|
-
await
|
|
10778
|
+
await fs44.ensureDir(path54.dirname(dest));
|
|
10779
|
+
await fs44.copy(src, dest, {
|
|
10461
10780
|
overwrite: true,
|
|
10462
10781
|
dereference: false,
|
|
10463
10782
|
// A vanished/unreadable entry mid-walk must skip that path, not crash the copy.
|
|
10464
10783
|
filter: (s) => {
|
|
10465
10784
|
try {
|
|
10466
|
-
return !
|
|
10785
|
+
return !fs44.lstatSync(s).isSymbolicLink();
|
|
10467
10786
|
} catch {
|
|
10468
10787
|
return false;
|
|
10469
10788
|
}
|
|
10470
10789
|
}
|
|
10471
10790
|
});
|
|
10472
|
-
if (!opts.quiet) log(`${exists ? "Overwrote" : "Created"} ${
|
|
10791
|
+
if (!opts.quiet) log(`${exists ? "Overwrote" : "Created"} ${path54.basename(dest)} (${itemId})`);
|
|
10473
10792
|
return "scaffolded";
|
|
10474
10793
|
}
|
|
10475
10794
|
|
|
@@ -10601,7 +10920,7 @@ async function runDecisionsGuard(options = {}) {
|
|
|
10601
10920
|
}
|
|
10602
10921
|
|
|
10603
10922
|
// src/commands/decisions.ts
|
|
10604
|
-
import
|
|
10923
|
+
import path55 from "node:path";
|
|
10605
10924
|
|
|
10606
10925
|
// src/decisions/next-number.ts
|
|
10607
10926
|
async function nextDecisionNumber(root) {
|
|
@@ -10712,7 +11031,7 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
10712
11031
|
return;
|
|
10713
11032
|
}
|
|
10714
11033
|
if (sub === "validate") {
|
|
10715
|
-
const target = arg ?
|
|
11034
|
+
const target = arg ? path55.resolve(root, arg) : void 0;
|
|
10716
11035
|
if (!target) {
|
|
10717
11036
|
error("usage: haus decisions validate <path-to-decision.md>");
|
|
10718
11037
|
process.exitCode = 1;
|
|
@@ -11181,27 +11500,27 @@ function guardBash(command) {
|
|
|
11181
11500
|
}
|
|
11182
11501
|
|
|
11183
11502
|
// src/security/guard-file-access.ts
|
|
11184
|
-
import
|
|
11185
|
-
import
|
|
11503
|
+
import fs45 from "node:fs";
|
|
11504
|
+
import path56 from "node:path";
|
|
11186
11505
|
function matchesDenyTier(candidate) {
|
|
11187
11506
|
const normalized = normalizePathForMatch(candidate);
|
|
11188
11507
|
return DENY_PATH_REGEXES.some((re) => re.test(normalized));
|
|
11189
11508
|
}
|
|
11190
11509
|
function resolveOnDisk(candidate) {
|
|
11191
11510
|
try {
|
|
11192
|
-
const resolved =
|
|
11193
|
-
return resolved ===
|
|
11511
|
+
const resolved = fs45.realpathSync(candidate);
|
|
11512
|
+
return resolved === path56.resolve(candidate) ? {} : { resolved };
|
|
11194
11513
|
} catch {
|
|
11195
11514
|
try {
|
|
11196
|
-
if (
|
|
11515
|
+
if (fs45.lstatSync(candidate).isSymbolicLink()) return { unverifiable: true };
|
|
11197
11516
|
} catch {
|
|
11198
11517
|
}
|
|
11199
|
-
const parent =
|
|
11518
|
+
const parent = path56.dirname(candidate);
|
|
11200
11519
|
if (parent === candidate) return {};
|
|
11201
11520
|
const viaParent = resolveOnDisk(parent);
|
|
11202
11521
|
if (viaParent.unverifiable) return { unverifiable: true };
|
|
11203
11522
|
if (viaParent.resolved)
|
|
11204
|
-
return { resolved:
|
|
11523
|
+
return { resolved: path56.join(viaParent.resolved, path56.basename(candidate)) };
|
|
11205
11524
|
return {};
|
|
11206
11525
|
}
|
|
11207
11526
|
}
|
|
@@ -11751,9 +12070,9 @@ function buildOptInEligible(items, skipped) {
|
|
|
11751
12070
|
}
|
|
11752
12071
|
|
|
11753
12072
|
// src/commands/workspace/discover.ts
|
|
11754
|
-
import
|
|
12073
|
+
import path57 from "node:path";
|
|
11755
12074
|
import fg6 from "fast-glob";
|
|
11756
|
-
import
|
|
12075
|
+
import YAML4 from "yaml";
|
|
11757
12076
|
init_fs();
|
|
11758
12077
|
init_logger();
|
|
11759
12078
|
var DEFAULT_MAX_DEPTH = 3;
|
|
@@ -11794,10 +12113,10 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
11794
12113
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
11795
12114
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
11796
12115
|
for (const match of gitMatches) {
|
|
11797
|
-
gitDirs.add(
|
|
12116
|
+
gitDirs.add(path57.posix.dirname(match));
|
|
11798
12117
|
}
|
|
11799
12118
|
for (const match of manifestMatches) {
|
|
11800
|
-
manifestDirs.add(
|
|
12119
|
+
manifestDirs.add(path57.posix.dirname(match));
|
|
11801
12120
|
}
|
|
11802
12121
|
const repoRoots = [...gitDirs];
|
|
11803
12122
|
const manifestSorted = [...manifestDirs].sort(
|
|
@@ -11814,9 +12133,9 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
11814
12133
|
async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
11815
12134
|
const repoRoots = (await findRepoRoots(workspaceRoot, maxDepth)).filter((dir) => dir !== ".");
|
|
11816
12135
|
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
11817
|
-
const absDir =
|
|
11818
|
-
const pkg = await readJson(
|
|
11819
|
-
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name :
|
|
12136
|
+
const absDir = path57.resolve(workspaceRoot, relDir);
|
|
12137
|
+
const pkg = await readJson(path57.join(absDir, "package.json"));
|
|
12138
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path57.basename(absDir);
|
|
11820
12139
|
let role = "auto";
|
|
11821
12140
|
try {
|
|
11822
12141
|
const scan = await scanProject(absDir);
|
|
@@ -11852,14 +12171,14 @@ function mergeWorkspaceConfig(existing, discovered, opts = {}) {
|
|
|
11852
12171
|
};
|
|
11853
12172
|
}
|
|
11854
12173
|
function renderWorkspaceYaml(config) {
|
|
11855
|
-
return
|
|
12174
|
+
return YAML4.stringify({
|
|
11856
12175
|
client: config.client,
|
|
11857
12176
|
repos: config.repos.map((r) => ({ name: r.name, path: r.path, role: r.role ?? "auto" })),
|
|
11858
12177
|
relationships: config.relationships
|
|
11859
12178
|
});
|
|
11860
12179
|
}
|
|
11861
12180
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
11862
|
-
const yamlPath =
|
|
12181
|
+
const yamlPath = path57.join(workspaceRoot, "haus.workspace.yaml");
|
|
11863
12182
|
const existingText = await readText(yamlPath);
|
|
11864
12183
|
const existing = parseWorkspaceConfig(existingText);
|
|
11865
12184
|
if (existingText && !existing) {
|
|
@@ -12221,23 +12540,39 @@ function toManifestPlugin(pin) {
|
|
|
12221
12540
|
async function runInstall(options, deps = {}) {
|
|
12222
12541
|
const ensure = deps.ensureGlobalPlugins ?? ensureGlobalPlugins;
|
|
12223
12542
|
const probe = deps.probePluginEnv ?? probePluginEnv;
|
|
12543
|
+
const cleanup = deps.cleanupStaleGlobalPlugins ?? cleanupStaleGlobalPlugins;
|
|
12224
12544
|
try {
|
|
12225
12545
|
const result = await applyInstall({
|
|
12226
12546
|
dryRun: options.dryRun,
|
|
12227
12547
|
force: options.force,
|
|
12228
|
-
check: options.check
|
|
12548
|
+
check: options.check,
|
|
12549
|
+
internal: options.internal
|
|
12229
12550
|
});
|
|
12230
12551
|
if (!options.postinstall) printApplyResult(result, options.dryRun ?? false);
|
|
12231
12552
|
const pins = [...DEFAULT_GLOBAL_PLUGINS];
|
|
12232
12553
|
if (options.check) {
|
|
12233
12554
|
const state = await probe();
|
|
12234
12555
|
const pluginDrift = pluginDriftStatus(planPluginActions(pins, state), { matchRef: false });
|
|
12556
|
+
const manifest = await readManifest();
|
|
12557
|
+
const recorded = (manifest?.plugins ?? []).filter(isValidPin);
|
|
12558
|
+
const protectedPins = [...pins, ...DEFAULT_GLOBAL_PLUGINS];
|
|
12559
|
+
const stale = planStalePluginRemovals(
|
|
12560
|
+
protectedPins,
|
|
12561
|
+
[...recorded, ...FORMER_GLOBAL_PLUGINS],
|
|
12562
|
+
state
|
|
12563
|
+
);
|
|
12235
12564
|
if (pluginDrift === true) {
|
|
12236
12565
|
log("Global plugin drift: a haus-managed plugin is missing or disabled.");
|
|
12237
12566
|
}
|
|
12238
|
-
if (
|
|
12567
|
+
if (stale.length > 0) {
|
|
12568
|
+
log(
|
|
12569
|
+
`Global plugin drift: formerly managed plugin(s) still installed: ${stale.map((p) => `${p.plugin}@${p.marketplace}`).join(", ")}.`
|
|
12570
|
+
);
|
|
12571
|
+
}
|
|
12572
|
+
if (result.drift || pluginDrift === true || stale.length > 0) process.exitCode = 1;
|
|
12239
12573
|
return;
|
|
12240
12574
|
}
|
|
12575
|
+
await cleanup(pins, { dryRun: options.dryRun });
|
|
12241
12576
|
const ensured = await ensure(pins, { dryRun: options.dryRun });
|
|
12242
12577
|
if (!options.dryRun) {
|
|
12243
12578
|
await recordManifestPlugins(
|
|
@@ -12276,11 +12611,11 @@ async function runInstall(options, deps = {}) {
|
|
|
12276
12611
|
}
|
|
12277
12612
|
|
|
12278
12613
|
// src/commands/new.ts
|
|
12279
|
-
import
|
|
12614
|
+
import path59 from "node:path";
|
|
12280
12615
|
|
|
12281
12616
|
// src/new-project/create-project.ts
|
|
12282
|
-
import
|
|
12283
|
-
import
|
|
12617
|
+
import path58 from "node:path";
|
|
12618
|
+
import fs46 from "fs-extra";
|
|
12284
12619
|
init_exec();
|
|
12285
12620
|
init_logger();
|
|
12286
12621
|
|
|
@@ -12465,10 +12800,10 @@ var defaultDeps = {
|
|
|
12465
12800
|
};
|
|
12466
12801
|
var NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
12467
12802
|
async function recordPackageManager(projectPath, pm, deps) {
|
|
12468
|
-
const pkgPath =
|
|
12803
|
+
const pkgPath = path58.join(projectPath, "package.json");
|
|
12469
12804
|
let pkg;
|
|
12470
12805
|
try {
|
|
12471
|
-
pkg = await
|
|
12806
|
+
pkg = await fs46.readJson(pkgPath);
|
|
12472
12807
|
} catch {
|
|
12473
12808
|
return {};
|
|
12474
12809
|
}
|
|
@@ -12483,7 +12818,7 @@ async function recordPackageManager(projectPath, pm, deps) {
|
|
|
12483
12818
|
note: `installed ${pm} is ${version}, which haus does not support \u2014 the package manager was not recorded, so the scanner will report it as unknown until you upgrade`
|
|
12484
12819
|
};
|
|
12485
12820
|
}
|
|
12486
|
-
await
|
|
12821
|
+
await fs46.writeJson(pkgPath, { ...pkg, packageManager: field }, { spaces: 2 });
|
|
12487
12822
|
return {};
|
|
12488
12823
|
}
|
|
12489
12824
|
var CONFIG_TOOLING = {
|
|
@@ -12496,12 +12831,12 @@ var CONFIG_TOOLING = {
|
|
|
12496
12831
|
}
|
|
12497
12832
|
};
|
|
12498
12833
|
async function ensureConfigTooling(projectPath, scaffolded) {
|
|
12499
|
-
const needed = scaffolded.map((file) => CONFIG_TOOLING[
|
|
12834
|
+
const needed = scaffolded.map((file) => CONFIG_TOOLING[path58.basename(file)]).filter((entry) => entry !== void 0);
|
|
12500
12835
|
if (needed.length === 0) return [];
|
|
12501
|
-
const pkgPath =
|
|
12836
|
+
const pkgPath = path58.join(projectPath, "package.json");
|
|
12502
12837
|
let pkg;
|
|
12503
12838
|
try {
|
|
12504
|
-
pkg = await
|
|
12839
|
+
pkg = await fs46.readJson(pkgPath);
|
|
12505
12840
|
} catch {
|
|
12506
12841
|
return [];
|
|
12507
12842
|
}
|
|
@@ -12525,12 +12860,12 @@ async function ensureConfigTooling(projectPath, scaffolded) {
|
|
|
12525
12860
|
}
|
|
12526
12861
|
}
|
|
12527
12862
|
if (!changed) return [];
|
|
12528
|
-
await
|
|
12863
|
+
await fs46.writeJson(pkgPath, { ...pkg, devDependencies, scripts }, { spaces: 2 });
|
|
12529
12864
|
return added;
|
|
12530
12865
|
}
|
|
12531
12866
|
async function createProject(opts, deps = defaultDeps) {
|
|
12532
12867
|
const steps = [];
|
|
12533
|
-
const projectPath =
|
|
12868
|
+
const projectPath = path58.resolve(opts.parentDir, opts.name);
|
|
12534
12869
|
const installHint = `cd ${opts.name} && ${opts.pm}${opts.pm === "yarn" ? "" : " install"}`;
|
|
12535
12870
|
const fail = (step, detail) => {
|
|
12536
12871
|
steps.push({ step, status: "failed", detail });
|
|
@@ -12538,20 +12873,20 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
12538
12873
|
};
|
|
12539
12874
|
const stack = getStack(opts.stackId);
|
|
12540
12875
|
if (!stack) return fail("preflight", `Unknown stack "${opts.stackId}". Run \`haus new --list\`.`);
|
|
12541
|
-
const parentStat = await
|
|
12876
|
+
const parentStat = await fs46.stat(path58.dirname(projectPath)).catch(() => null);
|
|
12542
12877
|
if (!parentStat?.isDirectory()) {
|
|
12543
12878
|
return fail(
|
|
12544
12879
|
"preflight",
|
|
12545
12880
|
`Parent directory ${opts.parentDir} is not an existing directory \u2014 pass an existing one to --dir.`
|
|
12546
12881
|
);
|
|
12547
12882
|
}
|
|
12548
|
-
if (!NAME_RE.test(opts.name) || opts.name.includes("/") || opts.name.includes("\\") ||
|
|
12883
|
+
if (!NAME_RE.test(opts.name) || opts.name.includes("/") || opts.name.includes("\\") || path58.dirname(projectPath) !== path58.resolve(opts.parentDir)) {
|
|
12549
12884
|
return fail(
|
|
12550
12885
|
"preflight",
|
|
12551
12886
|
`Invalid project name "${opts.name}" (lowercase letters, digits, . _ - only).`
|
|
12552
12887
|
);
|
|
12553
12888
|
}
|
|
12554
|
-
if (await
|
|
12889
|
+
if (await fs46.pathExists(projectPath)) {
|
|
12555
12890
|
return fail("preflight", `Target ${projectPath} already exists \u2014 refusing to write into it.`);
|
|
12556
12891
|
}
|
|
12557
12892
|
const scaffoldArgs = stack.scaffold.args.map(
|
|
@@ -12623,13 +12958,13 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
12623
12958
|
});
|
|
12624
12959
|
if (scaffold.exitCode !== 0) {
|
|
12625
12960
|
const why = scaffold.killed ? `${pinned} did not complete: ${scaffold.stderr.trim() || `timed out after ${SCAFFOLD_TIMEOUT_MS}ms`}. Scaffolders that stall this way are usually waiting on a prompt despite --yes \u2014 run it yourself to see: npx ${npxArgs.join(" ")}` : `${pinned} exited ${scaffold.exitCode}.`;
|
|
12626
|
-
const leftBehind = await
|
|
12961
|
+
const leftBehind = await fs46.pathExists(projectPath) ? ` The directory was left for inspection \u2014 review it and remove it manually if unwanted (haus never auto-deletes).` : "";
|
|
12627
12962
|
return fail("scaffold", `${why}${leftBehind}`);
|
|
12628
12963
|
}
|
|
12629
12964
|
steps.push({ step: "scaffold", status: "done" });
|
|
12630
12965
|
phase = "git";
|
|
12631
12966
|
if (!opts.json) log("\u25B6 git: init/branch/commit");
|
|
12632
|
-
if (!await
|
|
12967
|
+
if (!await fs46.pathExists(path58.join(projectPath, ".git"))) {
|
|
12633
12968
|
const init = await deps.git(["init"], { cwd: projectPath });
|
|
12634
12969
|
if (init.exitCode !== 0) return fail("git", `git init failed: ${init.stderr}`);
|
|
12635
12970
|
}
|
|
@@ -12855,7 +13190,7 @@ function resolveNewOptions(stackArg, nameArg, flags, cwd) {
|
|
|
12855
13190
|
if (flags.dir !== void 0 && flags.dir.trim() === "") {
|
|
12856
13191
|
return { ok: false, error: "--dir needs a path (omit it to scaffold into the cwd)." };
|
|
12857
13192
|
}
|
|
12858
|
-
const parentDir = flags.dir === void 0 ? cwd :
|
|
13193
|
+
const parentDir = flags.dir === void 0 ? cwd : path59.resolve(cwd, flags.dir);
|
|
12859
13194
|
const pm = flags.pm ?? "yarn";
|
|
12860
13195
|
if (!PMS.includes(pm)) {
|
|
12861
13196
|
return { ok: false, error: `Unsupported --pm "${pm}" \u2014 use yarn, npm, or pnpm.` };
|
|
@@ -12953,7 +13288,7 @@ async function runRecommend(options) {
|
|
|
12953
13288
|
}
|
|
12954
13289
|
|
|
12955
13290
|
// src/commands/remove.ts
|
|
12956
|
-
import
|
|
13291
|
+
import path60 from "node:path";
|
|
12957
13292
|
init_fs();
|
|
12958
13293
|
init_logger();
|
|
12959
13294
|
init_paths();
|
|
@@ -13060,7 +13395,7 @@ async function runRemove(ids, options = {}) {
|
|
|
13060
13395
|
}
|
|
13061
13396
|
const allAbs = cleared.flatMap(({ existing }) => existing.map(({ abs }) => abs));
|
|
13062
13397
|
const backupRoot = await backupTrackedPaths(root, allAbs, say, "remove");
|
|
13063
|
-
report.backupDir = backupRoot === null ? null :
|
|
13398
|
+
report.backupDir = backupRoot === null ? null : path60.basename(backupRoot);
|
|
13064
13399
|
for (const { id, existing } of cleared) {
|
|
13065
13400
|
await deleteTrackedPaths(root, id, existing, { verb: "Removed", reason: "" }, say);
|
|
13066
13401
|
}
|
|
@@ -13101,7 +13436,9 @@ async function runRescan(options = {}) {
|
|
|
13101
13436
|
const context = await scanProject(root, { persist: !dryRun });
|
|
13102
13437
|
if (!dryRun) {
|
|
13103
13438
|
try {
|
|
13104
|
-
|
|
13439
|
+
const pins = await loadCatalogPluginPins(root);
|
|
13440
|
+
await cleanupStaleGlobalPlugins(pins);
|
|
13441
|
+
await ensureGlobalPlugins(pins);
|
|
13105
13442
|
} catch (err) {
|
|
13106
13443
|
warn(`Could not refresh global plugins: ${err instanceof Error ? err.message : String(err)}`);
|
|
13107
13444
|
}
|
|
@@ -13148,8 +13485,8 @@ async function runScan(options) {
|
|
|
13148
13485
|
}
|
|
13149
13486
|
|
|
13150
13487
|
// src/commands/undo.ts
|
|
13151
|
-
import
|
|
13152
|
-
import
|
|
13488
|
+
import path61 from "node:path";
|
|
13489
|
+
import fs47 from "fs-extra";
|
|
13153
13490
|
|
|
13154
13491
|
// src/claude/managed-paths.ts
|
|
13155
13492
|
init_paths();
|
|
@@ -13168,7 +13505,7 @@ init_paths();
|
|
|
13168
13505
|
async function collectCoreManagedPaths(root) {
|
|
13169
13506
|
const existing = [];
|
|
13170
13507
|
for (const abs of coreManagedAbsolutePaths(root)) {
|
|
13171
|
-
if (await
|
|
13508
|
+
if (await fs47.pathExists(abs)) existing.push(abs);
|
|
13172
13509
|
}
|
|
13173
13510
|
return existing;
|
|
13174
13511
|
}
|
|
@@ -13182,7 +13519,7 @@ async function collectLockTrackedPaths(root) {
|
|
|
13182
13519
|
const unsafeByRel = await unsafeReasonsFor(root, relPaths);
|
|
13183
13520
|
const existing = await resolveExistingTrackedPaths(root, id, relPaths, log, unsafeByRel);
|
|
13184
13521
|
for (const [rel, reason] of unsafeByRel) {
|
|
13185
|
-
if (reason !== null) preserved.push(
|
|
13522
|
+
if (reason !== null) preserved.push(path61.resolve(root, rel));
|
|
13186
13523
|
}
|
|
13187
13524
|
if (existing.length === 0) continue;
|
|
13188
13525
|
const gate = await hashGateEntry(root, { hash: row.hash }, existing);
|
|
@@ -13198,51 +13535,51 @@ async function collectLockTrackedPaths(root) {
|
|
|
13198
13535
|
}
|
|
13199
13536
|
async function settingsHasHausContent(root) {
|
|
13200
13537
|
const settingsPath = claudePath(root, "settings.json");
|
|
13201
|
-
if (!await
|
|
13538
|
+
if (!await fs47.pathExists(settingsPath)) return false;
|
|
13202
13539
|
const settings = await readProjectSettings(root);
|
|
13203
13540
|
return settings._haus != null;
|
|
13204
13541
|
}
|
|
13205
13542
|
async function claudeMdHasHausBlock(root) {
|
|
13206
|
-
const filePath =
|
|
13207
|
-
if (!await
|
|
13208
|
-
const text = await
|
|
13543
|
+
const filePath = path61.join(root, "CLAUDE.md");
|
|
13544
|
+
if (!await fs47.pathExists(filePath)) return false;
|
|
13545
|
+
const text = await fs47.readFile(filePath, "utf8");
|
|
13209
13546
|
return text.includes(BLOCK_BEGIN);
|
|
13210
13547
|
}
|
|
13211
13548
|
async function stripProjectSettings(root) {
|
|
13212
13549
|
const settingsPath = claudePath(root, "settings.json");
|
|
13213
|
-
if (!await
|
|
13550
|
+
if (!await fs47.pathExists(settingsPath)) return false;
|
|
13214
13551
|
let settings = await readProjectSettings(root);
|
|
13215
13552
|
settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
13216
13553
|
const hasContent = Object.keys(settings).length > 0;
|
|
13217
13554
|
if (hasContent) {
|
|
13218
13555
|
await writeProjectSettings(root, settings);
|
|
13219
|
-
log(`Stripped haus rules from ${
|
|
13556
|
+
log(`Stripped haus rules from ${path61.relative(root, settingsPath)} (user settings preserved).`);
|
|
13220
13557
|
return true;
|
|
13221
13558
|
}
|
|
13222
|
-
await
|
|
13223
|
-
log(`Removed ${
|
|
13559
|
+
await fs47.remove(settingsPath);
|
|
13560
|
+
log(`Removed ${path61.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
13224
13561
|
return true;
|
|
13225
13562
|
}
|
|
13226
13563
|
async function stripRootClaudeMd(root) {
|
|
13227
|
-
const filePath =
|
|
13228
|
-
if (!await
|
|
13229
|
-
const prev = await
|
|
13564
|
+
const filePath = path61.join(root, "CLAUDE.md");
|
|
13565
|
+
if (!await fs47.pathExists(filePath)) return false;
|
|
13566
|
+
const prev = await fs47.readFile(filePath, "utf8");
|
|
13230
13567
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
13231
13568
|
const next = stripHausBlock(prev);
|
|
13232
13569
|
if (next.length === 0) {
|
|
13233
|
-
await
|
|
13570
|
+
await fs47.remove(filePath);
|
|
13234
13571
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
13235
13572
|
} else {
|
|
13236
|
-
await
|
|
13573
|
+
await fs47.writeFile(filePath, next, "utf8");
|
|
13237
13574
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
13238
13575
|
}
|
|
13239
13576
|
return true;
|
|
13240
13577
|
}
|
|
13241
13578
|
async function pruneDirIfEmpty(dir) {
|
|
13242
|
-
if (!await
|
|
13579
|
+
if (!await fs47.pathExists(dir)) return;
|
|
13243
13580
|
try {
|
|
13244
|
-
const entries = await
|
|
13245
|
-
if (entries.length === 0) await
|
|
13581
|
+
const entries = await fs47.readdir(dir);
|
|
13582
|
+
if (entries.length === 0) await fs47.remove(dir);
|
|
13246
13583
|
} catch {
|
|
13247
13584
|
}
|
|
13248
13585
|
}
|
|
@@ -13251,13 +13588,13 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
|
13251
13588
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
13252
13589
|
const backupRoot = hausPath(root, "backups", dirBackupDirName("undo", stamp));
|
|
13253
13590
|
for (const abs of managedAbsPaths) {
|
|
13254
|
-
if (!await
|
|
13255
|
-
const rel =
|
|
13256
|
-
const backupPath =
|
|
13257
|
-
await
|
|
13258
|
-
await
|
|
13591
|
+
if (!await fs47.pathExists(abs)) continue;
|
|
13592
|
+
const rel = path61.relative(root, abs);
|
|
13593
|
+
const backupPath = path61.join(backupRoot, rel);
|
|
13594
|
+
await fs47.ensureDir(path61.dirname(backupPath));
|
|
13595
|
+
await fs47.copy(abs, backupPath);
|
|
13259
13596
|
}
|
|
13260
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
13597
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path61.relative(root, backupRoot)}.`);
|
|
13261
13598
|
}
|
|
13262
13599
|
async function runUndo(options) {
|
|
13263
13600
|
const root = options.root ?? process.cwd();
|
|
@@ -13271,7 +13608,7 @@ async function runUndo(options) {
|
|
|
13271
13608
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
13272
13609
|
return;
|
|
13273
13610
|
}
|
|
13274
|
-
const relTargets = managed.map((p) =>
|
|
13611
|
+
const relTargets = managed.map((p) => path61.relative(root, p));
|
|
13275
13612
|
const summaryParts = [...relTargets];
|
|
13276
13613
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
13277
13614
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -13297,9 +13634,9 @@ User-owned .claude/ files will be preserved.`
|
|
|
13297
13634
|
}
|
|
13298
13635
|
await backupManagedFilesBeforeUndo(root, managed);
|
|
13299
13636
|
for (const abs of coreManaged) {
|
|
13300
|
-
if (!await
|
|
13301
|
-
await
|
|
13302
|
-
log(`Removed ${
|
|
13637
|
+
if (!await fs47.pathExists(abs)) continue;
|
|
13638
|
+
await fs47.remove(abs);
|
|
13639
|
+
log(`Removed ${path61.relative(root, abs)}`);
|
|
13303
13640
|
}
|
|
13304
13641
|
for (const { id, existing } of lockRemovable) {
|
|
13305
13642
|
await deleteTrackedPaths(root, id, existing, { verb: "Removed" }, log);
|
|
@@ -13316,8 +13653,8 @@ init_fs();
|
|
|
13316
13653
|
init_logger();
|
|
13317
13654
|
init_paths();
|
|
13318
13655
|
import crypto4 from "node:crypto";
|
|
13319
|
-
import
|
|
13320
|
-
import
|
|
13656
|
+
import path62 from "node:path";
|
|
13657
|
+
import fs48 from "fs-extra";
|
|
13321
13658
|
async function runUninstall(options = {}) {
|
|
13322
13659
|
const { force = false, removePlugins = removeGlobalPlugins } = options;
|
|
13323
13660
|
const manifest = await readManifest();
|
|
@@ -13354,7 +13691,7 @@ async function runUninstall(options = {}) {
|
|
|
13354
13691
|
);
|
|
13355
13692
|
}
|
|
13356
13693
|
for (const entry of manifest.files) {
|
|
13357
|
-
const exists =
|
|
13694
|
+
const exists = fs48.pathExistsSync(entry.destPath);
|
|
13358
13695
|
if (!exists) continue;
|
|
13359
13696
|
const content2 = await readText(entry.destPath);
|
|
13360
13697
|
if (content2 === void 0) continue;
|
|
@@ -13372,23 +13709,24 @@ async function runUninstall(options = {}) {
|
|
|
13372
13709
|
result.skipped.push(entry.destPath);
|
|
13373
13710
|
continue;
|
|
13374
13711
|
}
|
|
13375
|
-
await
|
|
13376
|
-
await pruneEmptyDir(
|
|
13712
|
+
await fs48.remove(entry.destPath);
|
|
13713
|
+
await pruneEmptyDir(path62.dirname(entry.destPath));
|
|
13377
13714
|
result.deleted.push(entry.destPath);
|
|
13378
13715
|
}
|
|
13379
13716
|
const settings = await readSettings();
|
|
13380
13717
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
13381
13718
|
await writeSettings(stripped);
|
|
13382
13719
|
result.hooksStripped = true;
|
|
13383
|
-
const hausDir =
|
|
13720
|
+
const hausDir = path62.join(globalClaudeDir(), "haus");
|
|
13384
13721
|
const manifestPath2 = hausManifestPath();
|
|
13385
|
-
if (
|
|
13386
|
-
await
|
|
13722
|
+
if (fs48.pathExistsSync(manifestPath2)) {
|
|
13723
|
+
await fs48.remove(manifestPath2);
|
|
13387
13724
|
}
|
|
13388
|
-
|
|
13725
|
+
await removeInstallConfig();
|
|
13726
|
+
if (fs48.pathExistsSync(hausDir)) {
|
|
13389
13727
|
try {
|
|
13390
|
-
const remaining = await
|
|
13391
|
-
if (remaining.length === 0) await
|
|
13728
|
+
const remaining = await fs48.readdir(hausDir);
|
|
13729
|
+
if (remaining.length === 0) await fs48.remove(hausDir);
|
|
13392
13730
|
} catch {
|
|
13393
13731
|
}
|
|
13394
13732
|
}
|
|
@@ -13457,15 +13795,15 @@ async function runUninstallCommand(options) {
|
|
|
13457
13795
|
}
|
|
13458
13796
|
|
|
13459
13797
|
// src/commands/validate-catalog.ts
|
|
13460
|
-
import
|
|
13798
|
+
import path64 from "node:path";
|
|
13461
13799
|
|
|
13462
13800
|
// src/catalog/validate-core.ts
|
|
13463
13801
|
init_forbidden_content();
|
|
13464
13802
|
init_manifest_item_fields();
|
|
13465
13803
|
init_path_safety();
|
|
13466
13804
|
init_validation_rules2();
|
|
13467
|
-
import
|
|
13468
|
-
import
|
|
13805
|
+
import fs49 from "node:fs";
|
|
13806
|
+
import path63 from "node:path";
|
|
13469
13807
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
13470
13808
|
var ITEM_ID_PATTERN = /^haus\./;
|
|
13471
13809
|
function auditForbiddenStacks(items) {
|
|
@@ -13625,14 +13963,14 @@ function checkRequiredFrontmatter(text, label) {
|
|
|
13625
13963
|
}
|
|
13626
13964
|
function readForAudit(absPath, label, failures) {
|
|
13627
13965
|
try {
|
|
13628
|
-
return
|
|
13966
|
+
return fs49.readFileSync(absPath, "utf8");
|
|
13629
13967
|
} catch (err) {
|
|
13630
13968
|
failures.push(`${label}: unreadable (${err instanceof Error ? err.message : String(err)})`);
|
|
13631
13969
|
return null;
|
|
13632
13970
|
}
|
|
13633
13971
|
}
|
|
13634
13972
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
13635
|
-
const rel =
|
|
13973
|
+
const rel = path63.relative(manifestDir, absPath);
|
|
13636
13974
|
const failures = [];
|
|
13637
13975
|
const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
|
|
13638
13976
|
if (text === null) return failures;
|
|
@@ -13654,55 +13992,55 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
13654
13992
|
const failures = [];
|
|
13655
13993
|
for (const item of items) {
|
|
13656
13994
|
if (!item.path || !isSafeCatalogPath(item.path)) continue;
|
|
13657
|
-
const absPath =
|
|
13995
|
+
const absPath = path63.join(manifestDir, item.path);
|
|
13658
13996
|
if (item.type === "skill") {
|
|
13659
|
-
const skillMd =
|
|
13660
|
-
if (!
|
|
13661
|
-
failures.push(`${item.id}: missing ${
|
|
13997
|
+
const skillMd = path63.join(absPath, "SKILL.md");
|
|
13998
|
+
if (!fs49.existsSync(skillMd)) {
|
|
13999
|
+
failures.push(`${item.id}: missing ${path63.relative(manifestDir, skillMd)}`);
|
|
13662
14000
|
continue;
|
|
13663
14001
|
}
|
|
13664
14002
|
const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
|
|
13665
14003
|
if (text === null) continue;
|
|
13666
14004
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
13667
14005
|
failures.push(
|
|
13668
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
14006
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path63.relative(manifestDir, skillMd)}`)
|
|
13669
14007
|
);
|
|
13670
14008
|
} else if (item.type === "agent") {
|
|
13671
|
-
if (!
|
|
14009
|
+
if (!fs49.existsSync(absPath)) {
|
|
13672
14010
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
13673
14011
|
continue;
|
|
13674
14012
|
}
|
|
13675
|
-
const rel =
|
|
14013
|
+
const rel = path63.relative(manifestDir, absPath);
|
|
13676
14014
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
13677
14015
|
if (text === null) continue;
|
|
13678
14016
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
13679
14017
|
failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
|
|
13680
14018
|
} else if (item.type === "template") {
|
|
13681
|
-
if (!
|
|
14019
|
+
if (!fs49.existsSync(absPath)) {
|
|
13682
14020
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
13683
14021
|
continue;
|
|
13684
14022
|
}
|
|
13685
14023
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
13686
14024
|
} else if (item.type === "command") {
|
|
13687
|
-
if (!
|
|
14025
|
+
if (!fs49.existsSync(absPath)) {
|
|
13688
14026
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
13689
14027
|
continue;
|
|
13690
14028
|
}
|
|
13691
|
-
const rel =
|
|
14029
|
+
const rel = path63.relative(manifestDir, absPath);
|
|
13692
14030
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
13693
14031
|
if (text === null) continue;
|
|
13694
14032
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
13695
14033
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
13696
14034
|
} else if (item.type === "config") {
|
|
13697
|
-
if (!
|
|
14035
|
+
if (!fs49.existsSync(absPath)) {
|
|
13698
14036
|
failures.push(`${item.id}: missing config path ${item.path}`);
|
|
13699
14037
|
}
|
|
13700
14038
|
} else if (item.type === "rule") {
|
|
13701
|
-
if (!
|
|
14039
|
+
if (!fs49.existsSync(absPath)) {
|
|
13702
14040
|
failures.push(`${item.id}: missing rule file ${item.path}`);
|
|
13703
14041
|
}
|
|
13704
14042
|
} else if (item.type === "asset") {
|
|
13705
|
-
if (!
|
|
14043
|
+
if (!fs49.existsSync(absPath)) {
|
|
13706
14044
|
failures.push(`${item.id}: missing asset file ${item.path}`);
|
|
13707
14045
|
}
|
|
13708
14046
|
}
|
|
@@ -13737,7 +14075,7 @@ function resolveItemType(relPath, pathTypeMap) {
|
|
|
13737
14075
|
function walkAllFiles(dir, fn, failures) {
|
|
13738
14076
|
let entries;
|
|
13739
14077
|
try {
|
|
13740
|
-
entries =
|
|
14078
|
+
entries = fs49.readdirSync(dir, { withFileTypes: true });
|
|
13741
14079
|
} catch (err) {
|
|
13742
14080
|
failures.push(
|
|
13743
14081
|
`${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
|
|
@@ -13746,7 +14084,7 @@ function walkAllFiles(dir, fn, failures) {
|
|
|
13746
14084
|
}
|
|
13747
14085
|
for (const entry of entries) {
|
|
13748
14086
|
if (entry.name === ".DS_Store") continue;
|
|
13749
|
-
const full =
|
|
14087
|
+
const full = path63.join(dir, entry.name);
|
|
13750
14088
|
if (entry.isDirectory()) walkAllFiles(full, fn, failures);
|
|
13751
14089
|
else fn(full);
|
|
13752
14090
|
}
|
|
@@ -13770,12 +14108,12 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
13770
14108
|
const failures = [];
|
|
13771
14109
|
const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
|
|
13772
14110
|
for (const dir of CONTENT_ROOTS) {
|
|
13773
|
-
const abs =
|
|
13774
|
-
if (!
|
|
14111
|
+
const abs = path63.join(manifestDir, dir);
|
|
14112
|
+
if (!fs49.existsSync(abs)) continue;
|
|
13775
14113
|
walkAllFiles(
|
|
13776
14114
|
abs,
|
|
13777
14115
|
(file) => {
|
|
13778
|
-
const relPath =
|
|
14116
|
+
const relPath = path63.relative(manifestDir, file).replace(/\\/g, "/");
|
|
13779
14117
|
if (!isPathClaimed(relPath, claimedPaths)) {
|
|
13780
14118
|
failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
|
|
13781
14119
|
}
|
|
@@ -13788,7 +14126,7 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
13788
14126
|
function walkMd(dir, fn, failures) {
|
|
13789
14127
|
let entries;
|
|
13790
14128
|
try {
|
|
13791
|
-
entries =
|
|
14129
|
+
entries = fs49.readdirSync(dir, { withFileTypes: true });
|
|
13792
14130
|
} catch (err) {
|
|
13793
14131
|
failures.push(
|
|
13794
14132
|
`${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
|
|
@@ -13796,7 +14134,7 @@ function walkMd(dir, fn, failures) {
|
|
|
13796
14134
|
return;
|
|
13797
14135
|
}
|
|
13798
14136
|
for (const entry of entries) {
|
|
13799
|
-
const full =
|
|
14137
|
+
const full = path63.join(dir, entry.name);
|
|
13800
14138
|
if (entry.isDirectory()) walkMd(full, fn, failures);
|
|
13801
14139
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
13802
14140
|
}
|
|
@@ -13806,13 +14144,13 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
13806
14144
|
const pathSourceMap = buildItemPathSourceMap(items);
|
|
13807
14145
|
const pathTypeMap = buildItemPathTypeMap(items);
|
|
13808
14146
|
for (const dir of ["skills", "agents", "templates", "commands", "sources"]) {
|
|
13809
|
-
const abs =
|
|
13810
|
-
if (!
|
|
14147
|
+
const abs = path63.join(manifestDir, dir);
|
|
14148
|
+
if (!fs49.existsSync(abs)) continue;
|
|
13811
14149
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
13812
14150
|
walkMd(
|
|
13813
14151
|
abs,
|
|
13814
14152
|
(file) => {
|
|
13815
|
-
const rel =
|
|
14153
|
+
const rel = path63.relative(manifestDir, file);
|
|
13816
14154
|
const text = readForAudit(file, rel, failures);
|
|
13817
14155
|
if (text === null) return;
|
|
13818
14156
|
const norm = rel.replace(/\\/g, "/");
|
|
@@ -13860,8 +14198,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
13860
14198
|
process.exitCode = 1;
|
|
13861
14199
|
return;
|
|
13862
14200
|
}
|
|
13863
|
-
const abs =
|
|
13864
|
-
const manifestDir =
|
|
14201
|
+
const abs = path64.resolve(process.cwd(), manifestPath2);
|
|
14202
|
+
const manifestDir = path64.dirname(abs);
|
|
13865
14203
|
const data = await readJson(abs);
|
|
13866
14204
|
if (!data?.items) {
|
|
13867
14205
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -13882,8 +14220,8 @@ init_logger();
|
|
|
13882
14220
|
|
|
13883
14221
|
// src/workspace/worktree/add.ts
|
|
13884
14222
|
init_fs();
|
|
13885
|
-
import
|
|
13886
|
-
import
|
|
14223
|
+
import path71 from "node:path";
|
|
14224
|
+
import fs53 from "fs-extra";
|
|
13887
14225
|
|
|
13888
14226
|
// src/workspace/worktree/git-worktree.ts
|
|
13889
14227
|
init_exec();
|
|
@@ -14003,13 +14341,13 @@ async function hasUnpushedWork(cwd) {
|
|
|
14003
14341
|
}
|
|
14004
14342
|
|
|
14005
14343
|
// src/workspace/worktree/hydrate.ts
|
|
14006
|
-
import
|
|
14007
|
-
import
|
|
14344
|
+
import path67 from "node:path";
|
|
14345
|
+
import fs51 from "fs-extra";
|
|
14008
14346
|
|
|
14009
14347
|
// src/workspace/worktree/cow-copy.ts
|
|
14010
14348
|
init_exec();
|
|
14011
|
-
import
|
|
14012
|
-
import
|
|
14349
|
+
import path65 from "node:path";
|
|
14350
|
+
import fs50 from "fs-extra";
|
|
14013
14351
|
async function getMacFilesystemType(dir) {
|
|
14014
14352
|
const df = await runCommand("df", ["-P", dir]);
|
|
14015
14353
|
if (df.exitCode !== 0) return void 0;
|
|
@@ -14040,14 +14378,14 @@ async function detectCowStrategy(dir) {
|
|
|
14040
14378
|
return "unknown-platform";
|
|
14041
14379
|
}
|
|
14042
14380
|
async function cowCopyDir(src, dest) {
|
|
14043
|
-
const srcExists = await
|
|
14381
|
+
const srcExists = await fs50.pathExists(src);
|
|
14044
14382
|
if (!srcExists)
|
|
14045
14383
|
return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
|
|
14046
|
-
const strategy = await detectCowStrategy(
|
|
14384
|
+
const strategy = await detectCowStrategy(path65.dirname(dest));
|
|
14047
14385
|
if (strategy === "unsupported" || strategy === "unknown-platform") {
|
|
14048
14386
|
return { strategy, attempted: false, ok: false };
|
|
14049
14387
|
}
|
|
14050
|
-
await
|
|
14388
|
+
await fs50.ensureDir(path65.dirname(dest));
|
|
14051
14389
|
const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
|
|
14052
14390
|
const result = await runCommand("cp", args);
|
|
14053
14391
|
return {
|
|
@@ -14061,7 +14399,7 @@ async function cowCopyDir(src, dest) {
|
|
|
14061
14399
|
// src/workspace/worktree/install.ts
|
|
14062
14400
|
init_exec();
|
|
14063
14401
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
14064
|
-
import
|
|
14402
|
+
import path66 from "node:path";
|
|
14065
14403
|
import fg7 from "fast-glob";
|
|
14066
14404
|
function detectInstallPlan(signals) {
|
|
14067
14405
|
if (signals.hasYarnLock) {
|
|
@@ -14082,10 +14420,10 @@ function detectInstallPlan(signals) {
|
|
|
14082
14420
|
return { manager: "none" };
|
|
14083
14421
|
}
|
|
14084
14422
|
async function computeLockfileSignals(dir) {
|
|
14085
|
-
const hasYarnLock = existsSync3(
|
|
14086
|
-
const hasPnpmLock = existsSync3(
|
|
14087
|
-
const hasPackageLock = existsSync3(
|
|
14088
|
-
const hasComposerLock = existsSync3(
|
|
14423
|
+
const hasYarnLock = existsSync3(path66.join(dir, "yarn.lock"));
|
|
14424
|
+
const hasPnpmLock = existsSync3(path66.join(dir, "pnpm-lock.yaml"));
|
|
14425
|
+
const hasPackageLock = existsSync3(path66.join(dir, "package-lock.json"));
|
|
14426
|
+
const hasComposerLock = existsSync3(path66.join(dir, "composer.lock"));
|
|
14089
14427
|
const dotnetFiles = await fg7(["**/*.csproj", "**/*.sln"], {
|
|
14090
14428
|
cwd: dir,
|
|
14091
14429
|
onlyFiles: true,
|
|
@@ -14099,7 +14437,7 @@ async function computeLockfileSignals(dir) {
|
|
|
14099
14437
|
});
|
|
14100
14438
|
const hasDotnetProject = dotnetFiles.length > 0;
|
|
14101
14439
|
let hasPackageManagerField = false;
|
|
14102
|
-
const pkgPath =
|
|
14440
|
+
const pkgPath = path66.join(dir, "package.json");
|
|
14103
14441
|
if (existsSync3(pkgPath)) {
|
|
14104
14442
|
try {
|
|
14105
14443
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
@@ -14142,7 +14480,7 @@ function missingYarnrcEnvVars(content2, env) {
|
|
|
14142
14480
|
return findRequiredYarnrcEnvVars(content2).filter((name) => env[name] === void 0);
|
|
14143
14481
|
}
|
|
14144
14482
|
function missingYarnrcEnvVarsInDir(dir, env) {
|
|
14145
|
-
const yarnrcPath =
|
|
14483
|
+
const yarnrcPath = path66.join(dir, ".yarnrc.yml");
|
|
14146
14484
|
if (!existsSync3(yarnrcPath)) return [];
|
|
14147
14485
|
try {
|
|
14148
14486
|
return missingYarnrcEnvVars(readFileSync3(yarnrcPath, "utf8"), env);
|
|
@@ -14238,13 +14576,13 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
14238
14576
|
const start = Date.now();
|
|
14239
14577
|
const targets = [];
|
|
14240
14578
|
for (const target of HYDRATION_TARGETS) {
|
|
14241
|
-
const src =
|
|
14242
|
-
const dest =
|
|
14243
|
-
if (!await
|
|
14244
|
-
const destExists = await
|
|
14579
|
+
const src = path67.join(mainMemberPath, target);
|
|
14580
|
+
const dest = path67.join(worktreeMemberPath, target);
|
|
14581
|
+
if (!await fs51.pathExists(src)) continue;
|
|
14582
|
+
const destExists = await fs51.pathExists(dest);
|
|
14245
14583
|
if (!opts.force && destExists) continue;
|
|
14246
14584
|
if (opts.force && destExists) {
|
|
14247
|
-
await
|
|
14585
|
+
await fs51.remove(dest);
|
|
14248
14586
|
}
|
|
14249
14587
|
const copy = await cowCopyDir(src, dest);
|
|
14250
14588
|
targets.push({ target, copy });
|
|
@@ -14257,11 +14595,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
14257
14595
|
|
|
14258
14596
|
// src/workspace/worktree/local-files.ts
|
|
14259
14597
|
init_exec();
|
|
14260
|
-
import
|
|
14261
|
-
import
|
|
14598
|
+
import path68 from "node:path";
|
|
14599
|
+
import fs52 from "fs-extra";
|
|
14262
14600
|
var MACHINE_LOCAL_FILES = [
|
|
14263
14601
|
"docker-compose.local.yml",
|
|
14264
|
-
|
|
14602
|
+
path68.join(".claude", "settings.local.json")
|
|
14265
14603
|
];
|
|
14266
14604
|
async function isTracked(repoRoot, relFile) {
|
|
14267
14605
|
const gitPath = relFile.replace(/\\/g, "/");
|
|
@@ -14271,12 +14609,12 @@ async function isTracked(repoRoot, relFile) {
|
|
|
14271
14609
|
async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
14272
14610
|
const copied = [];
|
|
14273
14611
|
for (const rel of MACHINE_LOCAL_FILES) {
|
|
14274
|
-
const src =
|
|
14275
|
-
if (!await
|
|
14612
|
+
const src = path68.join(mainMemberPath, rel);
|
|
14613
|
+
if (!await fs52.pathExists(src)) continue;
|
|
14276
14614
|
if (await isTracked(mainMemberPath, rel)) continue;
|
|
14277
|
-
const dest =
|
|
14278
|
-
await
|
|
14279
|
-
await
|
|
14615
|
+
const dest = path68.join(worktreeMemberPath, rel);
|
|
14616
|
+
await fs52.ensureDir(path68.dirname(dest));
|
|
14617
|
+
await fs52.copy(src, dest, { overwrite: true });
|
|
14280
14618
|
copied.push(rel);
|
|
14281
14619
|
}
|
|
14282
14620
|
return copied;
|
|
@@ -14284,15 +14622,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
|
14284
14622
|
|
|
14285
14623
|
// src/workspace/worktree/root.ts
|
|
14286
14624
|
import { existsSync as existsSync4 } from "node:fs";
|
|
14287
|
-
import
|
|
14625
|
+
import path69 from "node:path";
|
|
14288
14626
|
async function resolveWorkspaceForWorktree(start) {
|
|
14289
14627
|
const rootInfo = await resolveRoots(start);
|
|
14290
14628
|
if (!rootInfo.isGitRepo) {
|
|
14291
14629
|
return { ok: false, reason: "Not inside a git repository." };
|
|
14292
14630
|
}
|
|
14293
14631
|
const workspaceRoot = rootInfo.mainRoot;
|
|
14294
|
-
const hasYaml = existsSync4(
|
|
14295
|
-
const hasManifest = existsSync4(
|
|
14632
|
+
const hasYaml = existsSync4(path69.join(workspaceRoot, WORKSPACE_FILE));
|
|
14633
|
+
const hasManifest = existsSync4(path69.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
14296
14634
|
if (!hasYaml && !hasManifest) {
|
|
14297
14635
|
return {
|
|
14298
14636
|
ok: false,
|
|
@@ -14318,10 +14656,10 @@ function selectMembers(members, only) {
|
|
|
14318
14656
|
|
|
14319
14657
|
// src/workspace/worktree/state.ts
|
|
14320
14658
|
init_fs();
|
|
14321
|
-
import
|
|
14659
|
+
import path70 from "node:path";
|
|
14322
14660
|
var WORKTREE_STATE_FILE = ".haus-worktree.json";
|
|
14323
14661
|
function worktreesDir(mainRoot) {
|
|
14324
|
-
return
|
|
14662
|
+
return path70.join(mainRoot, ".claude", "worktrees");
|
|
14325
14663
|
}
|
|
14326
14664
|
function isValidWorktreeSlug(slug) {
|
|
14327
14665
|
if (slug === "" || slug === "." || slug === "..") return false;
|
|
@@ -14334,22 +14672,22 @@ function worktreePath(mainRoot, slug) {
|
|
|
14334
14672
|
`Invalid worktree slug ${JSON.stringify(slug)} \u2014 slugs may only contain letters, digits, ".", "_", "-" (no path separators, and not "." or "..").`
|
|
14335
14673
|
);
|
|
14336
14674
|
}
|
|
14337
|
-
return
|
|
14675
|
+
return path70.join(worktreesDir(mainRoot), slug);
|
|
14338
14676
|
}
|
|
14339
14677
|
function slugFromWorktreePath(mainRoot, somePath) {
|
|
14340
14678
|
const dir = worktreesDir(mainRoot);
|
|
14341
|
-
const prefix = `${dir}${
|
|
14679
|
+
const prefix = `${dir}${path70.sep}`;
|
|
14342
14680
|
if (!somePath.startsWith(prefix)) return void 0;
|
|
14343
|
-
const segment = somePath.slice(prefix.length).split(
|
|
14681
|
+
const segment = somePath.slice(prefix.length).split(path70.sep)[0];
|
|
14344
14682
|
return segment !== void 0 && segment !== "" ? segment : void 0;
|
|
14345
14683
|
}
|
|
14346
14684
|
async function readWorktreeState(worktreeRoot) {
|
|
14347
|
-
const state = await readJson(
|
|
14685
|
+
const state = await readJson(path70.join(worktreeRoot, WORKTREE_STATE_FILE));
|
|
14348
14686
|
if (state == null || typeof state !== "object" || !Array.isArray(state.members)) return void 0;
|
|
14349
14687
|
return state;
|
|
14350
14688
|
}
|
|
14351
14689
|
async function writeWorktreeState(worktreeRoot, state) {
|
|
14352
|
-
await writeJson(
|
|
14690
|
+
await writeJson(path70.join(worktreeRoot, WORKTREE_STATE_FILE), state);
|
|
14353
14691
|
}
|
|
14354
14692
|
|
|
14355
14693
|
// src/workspace/worktree/add.ts
|
|
@@ -14364,7 +14702,7 @@ async function runAdd(opts) {
|
|
|
14364
14702
|
}
|
|
14365
14703
|
const branch = opts.branch ?? opts.slug;
|
|
14366
14704
|
const wsWorktreePath = worktreePath(workspaceRoot, opts.slug);
|
|
14367
|
-
if (await
|
|
14705
|
+
if (await fs53.pathExists(wsWorktreePath)) {
|
|
14368
14706
|
return {
|
|
14369
14707
|
ok: false,
|
|
14370
14708
|
error: `Workspace worktree already exists at ${wsWorktreePath} \u2014 run \`haus workspace worktree hydrate --materialize\` from inside it to check out any missing members (e.g. a worktree created by plain \`git worktree add\`), or \`remove\` first to start over.`
|
|
@@ -14383,7 +14721,7 @@ async function runAdd(opts) {
|
|
|
14383
14721
|
const memberResults = [];
|
|
14384
14722
|
for (const member of selected) {
|
|
14385
14723
|
const start = Date.now();
|
|
14386
|
-
const memberWorktreePath =
|
|
14724
|
+
const memberWorktreePath = path71.join(wsWorktreePath, member.folder);
|
|
14387
14725
|
const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
|
|
14388
14726
|
preferDefaultBranchFrom: true
|
|
14389
14727
|
});
|
|
@@ -14413,7 +14751,7 @@ async function runAdd(opts) {
|
|
|
14413
14751
|
await mapWithConcurrency(okResults, async (result) => {
|
|
14414
14752
|
const member = selected.find((m) => m.id === result.member);
|
|
14415
14753
|
if (!member) return;
|
|
14416
|
-
const memberWorktreePath =
|
|
14754
|
+
const memberWorktreePath = path71.join(wsWorktreePath, member.folder);
|
|
14417
14755
|
const hydrationStart = Date.now();
|
|
14418
14756
|
if (opts.hydrate !== false) {
|
|
14419
14757
|
result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
|
|
@@ -14481,17 +14819,17 @@ async function runDryRun(args) {
|
|
|
14481
14819
|
// src/workspace/worktree/autohydrate.ts
|
|
14482
14820
|
import { spawn } from "node:child_process";
|
|
14483
14821
|
import fsSync from "node:fs";
|
|
14484
|
-
import
|
|
14485
|
-
import
|
|
14822
|
+
import path76 from "node:path";
|
|
14823
|
+
import fs55 from "fs-extra";
|
|
14486
14824
|
|
|
14487
14825
|
// src/workspace/worktree/doctor.ts
|
|
14488
14826
|
import { existsSync as existsSync6 } from "node:fs";
|
|
14489
|
-
import
|
|
14827
|
+
import path74 from "node:path";
|
|
14490
14828
|
|
|
14491
14829
|
// src/workspace/worktree/hydrate-lock.ts
|
|
14492
14830
|
init_paths();
|
|
14493
|
-
import
|
|
14494
|
-
import
|
|
14831
|
+
import path72 from "node:path";
|
|
14832
|
+
import fs54 from "fs-extra";
|
|
14495
14833
|
var STALE_LOCK_MS = 30 * 60 * 1e3;
|
|
14496
14834
|
function locksDir(mainRoot) {
|
|
14497
14835
|
return hausPath(mainRoot, "locks");
|
|
@@ -14508,13 +14846,13 @@ function assertSlug(slug) {
|
|
|
14508
14846
|
return slug;
|
|
14509
14847
|
}
|
|
14510
14848
|
function hydrateLockPath(mainRoot, slug) {
|
|
14511
|
-
return
|
|
14849
|
+
return path72.join(locksDir(mainRoot), `hydrate-${assertSlug(slug)}.lock`);
|
|
14512
14850
|
}
|
|
14513
14851
|
function hydrateLogPath(mainRoot, slug) {
|
|
14514
|
-
return
|
|
14852
|
+
return path72.join(logsDir(mainRoot), `hydrate-${assertSlug(slug)}.log`);
|
|
14515
14853
|
}
|
|
14516
14854
|
function hydrateLastRunPath(mainRoot, slug) {
|
|
14517
|
-
return
|
|
14855
|
+
return path72.join(logsDir(mainRoot), `hydrate-${assertSlug(slug)}.last.json`);
|
|
14518
14856
|
}
|
|
14519
14857
|
function defaultIsAlive(pid) {
|
|
14520
14858
|
try {
|
|
@@ -14526,7 +14864,7 @@ function defaultIsAlive(pid) {
|
|
|
14526
14864
|
}
|
|
14527
14865
|
async function readOwnerPid(lockPath) {
|
|
14528
14866
|
try {
|
|
14529
|
-
const raw = (await
|
|
14867
|
+
const raw = (await fs54.readFile(path72.join(lockPath, "pid"), "utf8")).trim();
|
|
14530
14868
|
if (raw === "") return void 0;
|
|
14531
14869
|
const pid = Number.parseInt(raw, 10);
|
|
14532
14870
|
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
@@ -14535,13 +14873,13 @@ async function readOwnerPid(lockPath) {
|
|
|
14535
14873
|
}
|
|
14536
14874
|
}
|
|
14537
14875
|
async function claim(lockPath, pid) {
|
|
14538
|
-
await
|
|
14876
|
+
await fs54.ensureDir(path72.dirname(lockPath));
|
|
14539
14877
|
try {
|
|
14540
|
-
await
|
|
14878
|
+
await fs54.mkdir(lockPath);
|
|
14541
14879
|
} catch {
|
|
14542
14880
|
return false;
|
|
14543
14881
|
}
|
|
14544
|
-
await
|
|
14882
|
+
await fs54.writeFile(path72.join(lockPath, "pid"), String(pid), "utf8");
|
|
14545
14883
|
return true;
|
|
14546
14884
|
}
|
|
14547
14885
|
async function acquireHydrateLock(mainRoot, slug, deps = {}) {
|
|
@@ -14560,7 +14898,7 @@ async function acquireHydrateLock(mainRoot, slug, deps = {}) {
|
|
|
14560
14898
|
}
|
|
14561
14899
|
if (alive) return { acquired: false, reason: "live", ownerPid, lockPath };
|
|
14562
14900
|
} else {
|
|
14563
|
-
const stat = await
|
|
14901
|
+
const stat = await fs54.stat(lockPath).catch(() => void 0);
|
|
14564
14902
|
if (stat === void 0) {
|
|
14565
14903
|
return await claim(lockPath, pid) ? { acquired: true, lockPath } : { acquired: false, reason: "live", lockPath };
|
|
14566
14904
|
}
|
|
@@ -14568,18 +14906,18 @@ async function acquireHydrateLock(mainRoot, slug, deps = {}) {
|
|
|
14568
14906
|
return { acquired: false, reason: "starting", lockPath };
|
|
14569
14907
|
}
|
|
14570
14908
|
}
|
|
14571
|
-
await
|
|
14909
|
+
await fs54.remove(lockPath);
|
|
14572
14910
|
return await claim(lockPath, pid) ? { acquired: true, lockPath } : { acquired: false, reason: "live", ownerPid, lockPath };
|
|
14573
14911
|
}
|
|
14574
14912
|
async function recordHydrateLockOwner(mainRoot, slug, pid) {
|
|
14575
|
-
await
|
|
14913
|
+
await fs54.writeFile(path72.join(hydrateLockPath(mainRoot, slug), "pid"), String(pid), "utf8");
|
|
14576
14914
|
}
|
|
14577
14915
|
async function releaseHydrateLock(mainRoot, slug) {
|
|
14578
|
-
await
|
|
14916
|
+
await fs54.remove(hydrateLockPath(mainRoot, slug));
|
|
14579
14917
|
}
|
|
14580
14918
|
async function readHydrateLock(mainRoot, slug) {
|
|
14581
14919
|
const lockPath = hydrateLockPath(mainRoot, slug);
|
|
14582
|
-
const stat = await
|
|
14920
|
+
const stat = await fs54.stat(lockPath).catch(() => void 0);
|
|
14583
14921
|
if (stat === void 0) return { held: false };
|
|
14584
14922
|
const ownerPid = await readOwnerPid(lockPath);
|
|
14585
14923
|
return {
|
|
@@ -14592,21 +14930,21 @@ async function readHydrateLock(mainRoot, slug) {
|
|
|
14592
14930
|
|
|
14593
14931
|
// src/workspace/worktree/orphans.ts
|
|
14594
14932
|
import { existsSync as existsSync5 } from "node:fs";
|
|
14595
|
-
import
|
|
14933
|
+
import path73 from "node:path";
|
|
14596
14934
|
var WORKSPACE_REPO_ID = "(workspace)";
|
|
14597
14935
|
async function findOrphanedRegistrations(repos, wsWorktreesDir) {
|
|
14598
14936
|
const orphans = [];
|
|
14599
14937
|
for (const repo of repos) {
|
|
14600
14938
|
const entries = await listWorktrees(repo.absPath);
|
|
14601
14939
|
for (const entry of entries) {
|
|
14602
|
-
const entryPath =
|
|
14603
|
-
const marker = `${
|
|
14940
|
+
const entryPath = path73.normalize(entry.path);
|
|
14941
|
+
const marker = `${path73.sep}.claude${path73.sep}worktrees${path73.sep}`;
|
|
14604
14942
|
const idx = entryPath.indexOf(marker);
|
|
14605
14943
|
if (idx === -1) continue;
|
|
14606
14944
|
const rest = entryPath.slice(idx + marker.length);
|
|
14607
|
-
const slug = rest.split(
|
|
14945
|
+
const slug = rest.split(path73.sep)[0];
|
|
14608
14946
|
if (!slug) continue;
|
|
14609
|
-
if (existsSync5(
|
|
14947
|
+
if (existsSync5(path73.join(wsWorktreesDir, slug))) continue;
|
|
14610
14948
|
orphans.push({
|
|
14611
14949
|
repoId: repo.id,
|
|
14612
14950
|
ownerRepo: repo.absPath,
|
|
@@ -14621,7 +14959,7 @@ async function findOrphanedRegistrations(repos, wsWorktreesDir) {
|
|
|
14621
14959
|
// src/workspace/worktree/doctor.ts
|
|
14622
14960
|
var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
|
|
14623
14961
|
function looksHydrated(memberPath) {
|
|
14624
|
-
return HYDRATION_MARKERS.some((marker) => existsSync6(
|
|
14962
|
+
return HYDRATION_MARKERS.some((marker) => existsSync6(path74.join(memberPath, marker)));
|
|
14625
14963
|
}
|
|
14626
14964
|
async function runDoctor2() {
|
|
14627
14965
|
const rootInfo = await resolveRoots();
|
|
@@ -14638,7 +14976,7 @@ async function runDoctor2() {
|
|
|
14638
14976
|
};
|
|
14639
14977
|
}
|
|
14640
14978
|
const workspaceRoot = rootInfo.mainRoot;
|
|
14641
|
-
const isWorkspace = existsSync6(
|
|
14979
|
+
const isWorkspace = existsSync6(path74.join(workspaceRoot, WORKSPACE_FILE)) || existsSync6(path74.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
14642
14980
|
if (!isWorkspace) {
|
|
14643
14981
|
return {
|
|
14644
14982
|
isWorkspace: false,
|
|
@@ -14654,7 +14992,7 @@ async function runDoctor2() {
|
|
|
14654
14992
|
};
|
|
14655
14993
|
}
|
|
14656
14994
|
const wsWorktreesDir = worktreesDir(workspaceRoot);
|
|
14657
|
-
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${
|
|
14995
|
+
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path74.sep}`);
|
|
14658
14996
|
const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
|
|
14659
14997
|
const workspaceConfig = await readWorkspaceConfig(workspaceRoot);
|
|
14660
14998
|
const autoHydrate = workspaceConfig?.worktree?.autoHydrate === true;
|
|
@@ -14667,10 +15005,10 @@ async function runDoctor2() {
|
|
|
14667
15005
|
let hydrationOwnerPid;
|
|
14668
15006
|
if (inWorkspaceWorktree) {
|
|
14669
15007
|
const wtPath = rootInfo.repoRoot;
|
|
14670
|
-
slug = rootInfo.worktreeName ??
|
|
15008
|
+
slug = rootInfo.worktreeName ?? path74.basename(wtPath);
|
|
14671
15009
|
const state = await readWorktreeState(wtPath);
|
|
14672
15010
|
for (const member of members) {
|
|
14673
|
-
const memberPath =
|
|
15011
|
+
const memberPath = path74.join(wtPath, member.folder);
|
|
14674
15012
|
const materialized = existsSync6(memberPath);
|
|
14675
15013
|
const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
|
|
14676
15014
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
@@ -14715,7 +15053,7 @@ async function runDoctor2() {
|
|
|
14715
15053
|
wsWorktreesDir
|
|
14716
15054
|
);
|
|
14717
15055
|
const orphans = orphanScan.map(
|
|
14718
|
-
(o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${
|
|
15056
|
+
(o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${path74.join(wsWorktreesDir, o.slug)} no longer exists.`
|
|
14719
15057
|
);
|
|
14720
15058
|
if (orphans.length > 0) {
|
|
14721
15059
|
problems.push(...orphans);
|
|
@@ -14738,7 +15076,7 @@ async function runDoctor2() {
|
|
|
14738
15076
|
// src/workspace/worktree/hydrate-command.ts
|
|
14739
15077
|
init_fs();
|
|
14740
15078
|
import { existsSync as existsSync7 } from "node:fs";
|
|
14741
|
-
import
|
|
15079
|
+
import path75 from "node:path";
|
|
14742
15080
|
async function runHydrateWorktree(opts) {
|
|
14743
15081
|
const rootInfo = await resolveRoots();
|
|
14744
15082
|
if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
|
|
@@ -14749,7 +15087,7 @@ async function runHydrateWorktree(opts) {
|
|
|
14749
15087
|
}
|
|
14750
15088
|
const workspaceRoot = rootInfo.mainRoot;
|
|
14751
15089
|
const wtPath = rootInfo.repoRoot;
|
|
14752
|
-
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${
|
|
15090
|
+
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path75.sep}`)) {
|
|
14753
15091
|
return {
|
|
14754
15092
|
ok: false,
|
|
14755
15093
|
error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
|
|
@@ -14761,13 +15099,13 @@ async function runHydrateWorktree(opts) {
|
|
|
14761
15099
|
return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
|
|
14762
15100
|
}
|
|
14763
15101
|
const state = await readWorktreeState(wtPath);
|
|
14764
|
-
const slug = state?.slug ??
|
|
15102
|
+
const slug = state?.slug ?? path75.basename(wtPath);
|
|
14765
15103
|
const detectedBranch = await currentBranch(wtPath);
|
|
14766
15104
|
const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
|
|
14767
15105
|
if (opts.dryRun) {
|
|
14768
15106
|
const results2 = await Promise.all(
|
|
14769
15107
|
selected.map(async (member) => {
|
|
14770
|
-
const memberPath =
|
|
15108
|
+
const memberPath = path75.join(wtPath, member.folder);
|
|
14771
15109
|
if (existsSync7(memberPath)) {
|
|
14772
15110
|
return { member: member.id, folder: member.folder, status: "planned" };
|
|
14773
15111
|
}
|
|
@@ -14788,7 +15126,7 @@ async function runHydrateWorktree(opts) {
|
|
|
14788
15126
|
const results = await mapWithConcurrency(
|
|
14789
15127
|
selected,
|
|
14790
15128
|
async (member) => {
|
|
14791
|
-
const memberPath =
|
|
15129
|
+
const memberPath = path75.join(wtPath, member.folder);
|
|
14792
15130
|
let branchAction;
|
|
14793
15131
|
if (!existsSync7(memberPath)) {
|
|
14794
15132
|
if (!opts.materialize) {
|
|
@@ -14909,7 +15247,7 @@ function defaultSpawnDetached(spec) {
|
|
|
14909
15247
|
}
|
|
14910
15248
|
function appendLog(logPath, line2) {
|
|
14911
15249
|
try {
|
|
14912
|
-
fsSync.mkdirSync(
|
|
15250
|
+
fsSync.mkdirSync(path76.dirname(logPath), { recursive: true });
|
|
14913
15251
|
const size = fsSync.statSync(logPath, { throwIfNoEntry: false })?.size ?? 0;
|
|
14914
15252
|
if (size > LOG_SIZE_CAP_BYTES) {
|
|
14915
15253
|
fsSync.writeFileSync(logPath, `--- log rotated (exceeded ${LOG_SIZE_CAP_BYTES} bytes) ---
|
|
@@ -14924,11 +15262,11 @@ async function consumeLastRunFailure(mainRoot, slug) {
|
|
|
14924
15262
|
const file = hydrateLastRunPath(mainRoot, slug);
|
|
14925
15263
|
let parsed;
|
|
14926
15264
|
try {
|
|
14927
|
-
parsed = JSON.parse(await
|
|
15265
|
+
parsed = JSON.parse(await fs55.readFile(file, "utf8"));
|
|
14928
15266
|
} catch {
|
|
14929
15267
|
return void 0;
|
|
14930
15268
|
}
|
|
14931
|
-
await
|
|
15269
|
+
await fs55.remove(file).catch(() => void 0);
|
|
14932
15270
|
if (parsed && parsed.ok === false) return parsed;
|
|
14933
15271
|
return void 0;
|
|
14934
15272
|
}
|
|
@@ -15000,7 +15338,7 @@ async function runAutoHydrate(opts = {}, deps = {}) {
|
|
|
15000
15338
|
reason: "could not determine the haus CLI entry point to re-invoke"
|
|
15001
15339
|
};
|
|
15002
15340
|
}
|
|
15003
|
-
await
|
|
15341
|
+
await fs55.ensureDir(logsDir(mainRoot));
|
|
15004
15342
|
appendLog(
|
|
15005
15343
|
logPath,
|
|
15006
15344
|
`autohydrate: forking hydrate --materialize for "${slug}" (${missingMembers.join(", ")})`
|
|
@@ -15069,21 +15407,21 @@ async function runDetachedHydration() {
|
|
|
15069
15407
|
async function writeLastRun(mainRoot, slug, ok, failures) {
|
|
15070
15408
|
const file = hydrateLastRunPath(mainRoot, slug);
|
|
15071
15409
|
if (ok) {
|
|
15072
|
-
await
|
|
15410
|
+
await fs55.remove(file).catch(() => void 0);
|
|
15073
15411
|
return;
|
|
15074
15412
|
}
|
|
15075
15413
|
const record = { slug, finishedAt: (/* @__PURE__ */ new Date()).toISOString(), ok, failures };
|
|
15076
|
-
await
|
|
15077
|
-
await
|
|
15414
|
+
await fs55.ensureDir(path76.dirname(file));
|
|
15415
|
+
await fs55.writeJson(file, record, { spaces: 2 }).catch(() => void 0);
|
|
15078
15416
|
}
|
|
15079
15417
|
|
|
15080
15418
|
// src/workspace/worktree/dematerialize.ts
|
|
15081
|
-
import
|
|
15082
|
-
import
|
|
15419
|
+
import path78 from "node:path";
|
|
15420
|
+
import fs57 from "fs-extra";
|
|
15083
15421
|
|
|
15084
15422
|
// src/workspace/worktree/member-targets.ts
|
|
15085
|
-
import
|
|
15086
|
-
import
|
|
15423
|
+
import path77 from "node:path";
|
|
15424
|
+
import fs56 from "fs-extra";
|
|
15087
15425
|
function normalizeStateMembers(state, allMembers) {
|
|
15088
15426
|
return state?.members.map((m) => ({ id: m.id, folder: m.folder, absPath: m.absPath })) ?? allMembers.map((m) => ({ id: m.id, folder: m.folder, absPath: m.absPath }));
|
|
15089
15427
|
}
|
|
@@ -15100,7 +15438,7 @@ function resolveMemberTargets(wsWorktreePath, stateMembers, allMembers) {
|
|
|
15100
15438
|
targets.push({
|
|
15101
15439
|
repo: configMember?.id ?? stateMember.id,
|
|
15102
15440
|
ownerRepo,
|
|
15103
|
-
path:
|
|
15441
|
+
path: path77.join(wsWorktreePath, stateMember.folder)
|
|
15104
15442
|
});
|
|
15105
15443
|
}
|
|
15106
15444
|
return { targets, unresolvedDroppedMembers };
|
|
@@ -15108,7 +15446,7 @@ function resolveMemberTargets(wsWorktreePath, stateMembers, allMembers) {
|
|
|
15108
15446
|
async function filterExistingTargets(targets) {
|
|
15109
15447
|
const existing = [];
|
|
15110
15448
|
for (const target of targets) {
|
|
15111
|
-
if (await
|
|
15449
|
+
if (await fs56.pathExists(target.path)) existing.push(target);
|
|
15112
15450
|
}
|
|
15113
15451
|
return existing;
|
|
15114
15452
|
}
|
|
@@ -15153,7 +15491,7 @@ async function runDematerialize(opts) {
|
|
|
15153
15491
|
} catch (err) {
|
|
15154
15492
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
15155
15493
|
}
|
|
15156
|
-
if (!await
|
|
15494
|
+
if (!await fs57.pathExists(wsWorktreePath)) {
|
|
15157
15495
|
return { ok: false, error: `No workspace worktree found at ${wsWorktreePath}.` };
|
|
15158
15496
|
}
|
|
15159
15497
|
const lock = await readHydrateLock(workspaceRoot, slug);
|
|
@@ -15186,9 +15524,9 @@ async function runDematerialize(opts) {
|
|
|
15186
15524
|
const removed = [];
|
|
15187
15525
|
const failed = [];
|
|
15188
15526
|
for (const folder of unresolvedDroppedMembers) {
|
|
15189
|
-
const droppedPath =
|
|
15190
|
-
if (await
|
|
15191
|
-
await
|
|
15527
|
+
const droppedPath = path78.join(wsWorktreePath, folder);
|
|
15528
|
+
if (await fs57.pathExists(droppedPath)) {
|
|
15529
|
+
await fs57.remove(droppedPath);
|
|
15192
15530
|
failed.push({ repo: folder, error: unresolvedDroppedFailureReason() });
|
|
15193
15531
|
}
|
|
15194
15532
|
}
|
|
@@ -15202,26 +15540,26 @@ async function runDematerialize(opts) {
|
|
|
15202
15540
|
}
|
|
15203
15541
|
|
|
15204
15542
|
// src/workspace/worktree/list.ts
|
|
15205
|
-
import
|
|
15206
|
-
import
|
|
15543
|
+
import path79 from "node:path";
|
|
15544
|
+
import fs58 from "fs-extra";
|
|
15207
15545
|
async function runList2() {
|
|
15208
15546
|
const resolved = await resolveWorkspaceForWorktree();
|
|
15209
15547
|
if (!resolved.ok) return { ok: false, error: resolved.reason };
|
|
15210
15548
|
const { rootInfo, workspaceRoot } = resolved;
|
|
15211
15549
|
const dir = worktreesDir(workspaceRoot);
|
|
15212
|
-
if (!await
|
|
15213
|
-
const entries = await
|
|
15550
|
+
if (!await fs58.pathExists(dir)) return { ok: true, workspaceRoot, worktrees: [] };
|
|
15551
|
+
const entries = await fs58.readdir(dir, { withFileTypes: true });
|
|
15214
15552
|
const slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
15215
15553
|
const configuredMembers = await readMembers(rootInfo);
|
|
15216
15554
|
const worktrees = [];
|
|
15217
15555
|
for (const slug of slugs) {
|
|
15218
|
-
const wtPath =
|
|
15556
|
+
const wtPath = path79.join(dir, slug);
|
|
15219
15557
|
const state = await readWorktreeState(wtPath);
|
|
15220
15558
|
const members = [];
|
|
15221
15559
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
15222
15560
|
for (const m of source) {
|
|
15223
|
-
const memberPath =
|
|
15224
|
-
const materialized = await
|
|
15561
|
+
const memberPath = path79.join(wtPath, m.folder);
|
|
15562
|
+
const materialized = await fs58.pathExists(memberPath);
|
|
15225
15563
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
15226
15564
|
members.push({
|
|
15227
15565
|
id: m.id,
|
|
@@ -15243,23 +15581,23 @@ async function runList2() {
|
|
|
15243
15581
|
|
|
15244
15582
|
// src/workspace/worktree/reclaim.ts
|
|
15245
15583
|
import { existsSync as existsSync8 } from "node:fs";
|
|
15246
|
-
import
|
|
15247
|
-
import
|
|
15584
|
+
import path80 from "node:path";
|
|
15585
|
+
import fs59 from "fs-extra";
|
|
15248
15586
|
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
15249
15587
|
async function lastTouchedMs(worktreeRoot) {
|
|
15250
|
-
const candidates = [worktreeRoot,
|
|
15588
|
+
const candidates = [worktreeRoot, path80.join(worktreeRoot, ".haus-worktree.json")];
|
|
15251
15589
|
try {
|
|
15252
|
-
const dotGit = await
|
|
15590
|
+
const dotGit = await fs59.readFile(path80.join(worktreeRoot, ".git"), "utf8");
|
|
15253
15591
|
const match = /^gitdir:\s*(.+)$/m.exec(dotGit);
|
|
15254
15592
|
if (match?.[1]) {
|
|
15255
15593
|
const gitdir = match[1].trim();
|
|
15256
|
-
candidates.push(
|
|
15594
|
+
candidates.push(path80.join(gitdir, "HEAD"), path80.join(gitdir, "index"));
|
|
15257
15595
|
}
|
|
15258
15596
|
} catch {
|
|
15259
15597
|
}
|
|
15260
15598
|
let newest = 0;
|
|
15261
15599
|
for (const candidate of candidates) {
|
|
15262
|
-
const stat = await
|
|
15600
|
+
const stat = await fs59.stat(candidate).catch(() => void 0);
|
|
15263
15601
|
if (stat && stat.mtimeMs > newest) newest = stat.mtimeMs;
|
|
15264
15602
|
}
|
|
15265
15603
|
return newest;
|
|
@@ -15286,7 +15624,7 @@ async function runReclaim(opts = {}) {
|
|
|
15286
15624
|
if (!existsSync8(dir)) {
|
|
15287
15625
|
return { ok: true, dryRun: !opts.write, olderThanDays, candidates: [], reclaimed: 0 };
|
|
15288
15626
|
}
|
|
15289
|
-
const allEntries = (await
|
|
15627
|
+
const allEntries = (await fs59.readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
15290
15628
|
const entries = allEntries.filter((name) => isValidWorktreeSlug(name));
|
|
15291
15629
|
const unaddressable = allEntries.filter((name) => !isValidWorktreeSlug(name));
|
|
15292
15630
|
const members = await readMembers(rootInfo);
|
|
@@ -15296,17 +15634,17 @@ async function runReclaim(opts = {}) {
|
|
|
15296
15634
|
for (const name of unaddressable) {
|
|
15297
15635
|
candidates.push({
|
|
15298
15636
|
slug: name,
|
|
15299
|
-
ageDays: (Date.now() - await lastTouchedMs(
|
|
15637
|
+
ageDays: (Date.now() - await lastTouchedMs(path80.join(dir, name))) / MS_PER_DAY,
|
|
15300
15638
|
materializedMembers: 0,
|
|
15301
15639
|
action: "skip",
|
|
15302
15640
|
reason: "directory name is not a valid worktree slug \u2014 no haus worktree command can address it; remove it by hand if it is debris"
|
|
15303
15641
|
});
|
|
15304
15642
|
}
|
|
15305
15643
|
for (const slug of entries) {
|
|
15306
|
-
const wtPath =
|
|
15644
|
+
const wtPath = path80.join(dir, slug);
|
|
15307
15645
|
const state = await readWorktreeState(wtPath);
|
|
15308
15646
|
const stateMembers = normalizeStateMembers(state, members);
|
|
15309
|
-
const materialized = stateMembers.filter((m) => existsSync8(
|
|
15647
|
+
const materialized = stateMembers.filter((m) => existsSync8(path80.join(wtPath, m.folder)));
|
|
15310
15648
|
const ageDays = (Date.now() - await lastTouchedMs(wtPath)) / MS_PER_DAY;
|
|
15311
15649
|
const base = { slug, ageDays, materializedMembers: materialized.length };
|
|
15312
15650
|
if (slug === currentSlug) {
|
|
@@ -15367,13 +15705,13 @@ async function runReclaim(opts = {}) {
|
|
|
15367
15705
|
}
|
|
15368
15706
|
|
|
15369
15707
|
// src/workspace/worktree/remove.ts
|
|
15370
|
-
import
|
|
15708
|
+
import fs60 from "fs-extra";
|
|
15371
15709
|
async function runRemove2(opts) {
|
|
15372
15710
|
const resolved = await resolveWorkspaceForWorktree();
|
|
15373
15711
|
if (!resolved.ok) return { ok: false, error: resolved.reason };
|
|
15374
15712
|
const { rootInfo, workspaceRoot } = resolved;
|
|
15375
15713
|
const wsWorktreePath = worktreePath(workspaceRoot, opts.slug);
|
|
15376
|
-
if (!await
|
|
15714
|
+
if (!await fs60.pathExists(wsWorktreePath)) {
|
|
15377
15715
|
return { ok: false, error: `No workspace worktree found at ${wsWorktreePath}.` };
|
|
15378
15716
|
}
|
|
15379
15717
|
const state = await readWorktreeState(wsWorktreePath);
|
|
@@ -15441,8 +15779,8 @@ async function runRemove2(opts) {
|
|
|
15441
15779
|
}
|
|
15442
15780
|
await pruneWorktrees(workspaceRoot);
|
|
15443
15781
|
}
|
|
15444
|
-
if (wsRemoveOk && await
|
|
15445
|
-
await
|
|
15782
|
+
if (wsRemoveOk && await fs60.pathExists(wsWorktreePath)) {
|
|
15783
|
+
await fs60.remove(wsWorktreePath);
|
|
15446
15784
|
}
|
|
15447
15785
|
return { ok: failed.length === 0, dryRun: false, slug: opts.slug, removed, failed };
|
|
15448
15786
|
}
|
|
@@ -15903,7 +16241,7 @@ async function runWorktree(action, opts = {}) {
|
|
|
15903
16241
|
|
|
15904
16242
|
// src/commands/workspace.ts
|
|
15905
16243
|
import { existsSync as existsSync12, statSync as statSync3 } from "node:fs";
|
|
15906
|
-
import
|
|
16244
|
+
import path89 from "node:path";
|
|
15907
16245
|
init_fs();
|
|
15908
16246
|
init_logger();
|
|
15909
16247
|
|
|
@@ -15962,7 +16300,7 @@ ${summaries.map(
|
|
|
15962
16300
|
|
|
15963
16301
|
// src/commands/workspace/doctor.ts
|
|
15964
16302
|
import { existsSync as existsSync9 } from "node:fs";
|
|
15965
|
-
import
|
|
16303
|
+
import path82 from "node:path";
|
|
15966
16304
|
init_logger();
|
|
15967
16305
|
init_paths();
|
|
15968
16306
|
|
|
@@ -15970,14 +16308,14 @@ init_paths();
|
|
|
15970
16308
|
init_fs();
|
|
15971
16309
|
init_paths();
|
|
15972
16310
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
15973
|
-
import
|
|
16311
|
+
import path81 from "node:path";
|
|
15974
16312
|
var MANIFEST_FILE2 = "workspace.manifest.json";
|
|
15975
16313
|
function manifestPath(workspaceRoot) {
|
|
15976
16314
|
return hausPath(workspaceRoot, MANIFEST_FILE2);
|
|
15977
16315
|
}
|
|
15978
16316
|
function hausVersion() {
|
|
15979
16317
|
try {
|
|
15980
|
-
const pkg = JSON.parse(readFileSync4(
|
|
16318
|
+
const pkg = JSON.parse(readFileSync4(path81.join(packageRoot(), "package.json"), "utf8"));
|
|
15981
16319
|
return pkg.version ?? "0.0.0";
|
|
15982
16320
|
} catch {
|
|
15983
16321
|
return "0.0.0";
|
|
@@ -16069,7 +16407,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
16069
16407
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
16070
16408
|
const catalogRefByRepo = [];
|
|
16071
16409
|
for (const repo of config.repos) {
|
|
16072
|
-
const repoRoot =
|
|
16410
|
+
const repoRoot = path82.resolve(workspaceRoot, repo.path);
|
|
16073
16411
|
const entry = manifestByName.get(repo.name);
|
|
16074
16412
|
if (!entry) {
|
|
16075
16413
|
flag({
|
|
@@ -16142,7 +16480,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
16142
16480
|
});
|
|
16143
16481
|
continue;
|
|
16144
16482
|
}
|
|
16145
|
-
if (!existsSync9(
|
|
16483
|
+
if (!existsSync9(path82.join(workspaceRoot, entry.path))) {
|
|
16146
16484
|
flag({
|
|
16147
16485
|
repo: entry.repo,
|
|
16148
16486
|
kind: "missing-linked-context-copy",
|
|
@@ -16188,18 +16526,18 @@ function emit(args) {
|
|
|
16188
16526
|
init_logger();
|
|
16189
16527
|
|
|
16190
16528
|
// src/workspace/link-context/link.ts
|
|
16191
|
-
import
|
|
16192
|
-
import
|
|
16529
|
+
import path85 from "node:path";
|
|
16530
|
+
import fs63 from "fs-extra";
|
|
16193
16531
|
init_fs();
|
|
16194
16532
|
init_logger();
|
|
16195
16533
|
|
|
16196
16534
|
// src/workspace/link-context/plan.ts
|
|
16197
|
-
import
|
|
16198
|
-
import
|
|
16535
|
+
import path84 from "node:path";
|
|
16536
|
+
import fs62 from "fs-extra";
|
|
16199
16537
|
|
|
16200
16538
|
// src/workspace/link-context/scan-source.ts
|
|
16201
|
-
import
|
|
16202
|
-
import
|
|
16539
|
+
import path83 from "node:path";
|
|
16540
|
+
import fs61 from "fs-extra";
|
|
16203
16541
|
var ASSET_SUBDIR = {
|
|
16204
16542
|
skill: "skills",
|
|
16205
16543
|
agent: "agents",
|
|
@@ -16207,36 +16545,36 @@ var ASSET_SUBDIR = {
|
|
|
16207
16545
|
};
|
|
16208
16546
|
async function hasLinkableContext(member) {
|
|
16209
16547
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
16210
|
-
if (await
|
|
16548
|
+
if (await fs61.pathExists(path83.join(member.absPath, ".claude", subdir))) return true;
|
|
16211
16549
|
}
|
|
16212
16550
|
return false;
|
|
16213
16551
|
}
|
|
16214
16552
|
async function collectSkills(member) {
|
|
16215
|
-
const dir =
|
|
16216
|
-
if (!await
|
|
16217
|
-
const names = await
|
|
16553
|
+
const dir = path83.join(member.absPath, ".claude", "skills");
|
|
16554
|
+
if (!await fs61.pathExists(dir)) return [];
|
|
16555
|
+
const names = await fs61.readdir(dir);
|
|
16218
16556
|
const out = [];
|
|
16219
16557
|
for (const name of [...names].sort()) {
|
|
16220
|
-
const abs =
|
|
16221
|
-
const stat = await
|
|
16558
|
+
const abs = path83.join(dir, name);
|
|
16559
|
+
const stat = await fs61.lstat(abs);
|
|
16222
16560
|
if (!stat.isDirectory()) continue;
|
|
16223
|
-
if (!await
|
|
16224
|
-
out.push({ type: "skill", name, sourceRelPath:
|
|
16561
|
+
if (!await fs61.pathExists(path83.join(abs, "SKILL.md"))) continue;
|
|
16562
|
+
out.push({ type: "skill", name, sourceRelPath: path83.posix.join(".claude", "skills", name) });
|
|
16225
16563
|
}
|
|
16226
16564
|
return out;
|
|
16227
16565
|
}
|
|
16228
16566
|
async function collectFlatMarkdown(member, type) {
|
|
16229
16567
|
const subdir = ASSET_SUBDIR[type];
|
|
16230
|
-
const dir =
|
|
16231
|
-
if (!await
|
|
16232
|
-
const names = await
|
|
16568
|
+
const dir = path83.join(member.absPath, ".claude", subdir);
|
|
16569
|
+
if (!await fs61.pathExists(dir)) return [];
|
|
16570
|
+
const names = await fs61.readdir(dir);
|
|
16233
16571
|
const out = [];
|
|
16234
16572
|
for (const fileName of [...names].sort()) {
|
|
16235
16573
|
if (!fileName.endsWith(".md")) continue;
|
|
16236
|
-
const abs =
|
|
16237
|
-
if (!(await
|
|
16574
|
+
const abs = path83.join(dir, fileName);
|
|
16575
|
+
if (!(await fs61.lstat(abs)).isFile()) continue;
|
|
16238
16576
|
const name = fileName.slice(0, -".md".length);
|
|
16239
|
-
out.push({ type, name, sourceRelPath:
|
|
16577
|
+
out.push({ type, name, sourceRelPath: path83.posix.join(".claude", subdir, fileName) });
|
|
16240
16578
|
}
|
|
16241
16579
|
return out;
|
|
16242
16580
|
}
|
|
@@ -16251,9 +16589,9 @@ async function collectSourceAssets(member) {
|
|
|
16251
16589
|
|
|
16252
16590
|
// src/workspace/link-context/plan.ts
|
|
16253
16591
|
function repoFolderPrefix(member) {
|
|
16254
|
-
const base =
|
|
16592
|
+
const base = path84.posix.basename(member.folder);
|
|
16255
16593
|
if (base && base !== "." && base !== "/") return base;
|
|
16256
|
-
const fromAbs =
|
|
16594
|
+
const fromAbs = path84.basename(member.absPath);
|
|
16257
16595
|
return fromAbs || member.id;
|
|
16258
16596
|
}
|
|
16259
16597
|
function assetKey(type, destKey) {
|
|
@@ -16268,7 +16606,7 @@ async function buildLinkPlan(members) {
|
|
|
16268
16606
|
const byKey = /* @__PURE__ */ new Map();
|
|
16269
16607
|
const membersById = new Map(members.map((m) => [m.id, m]));
|
|
16270
16608
|
for (const member of members) {
|
|
16271
|
-
if (!await
|
|
16609
|
+
if (!await fs62.pathExists(member.absPath)) {
|
|
16272
16610
|
skipped.push({ repo: member.id, reason: `not cloned locally (${member.absPath}) \u2014 skipped` });
|
|
16273
16611
|
continue;
|
|
16274
16612
|
}
|
|
@@ -16310,7 +16648,7 @@ async function buildLinkPlan(members) {
|
|
|
16310
16648
|
name: asset.name,
|
|
16311
16649
|
destKey,
|
|
16312
16650
|
sourceRelPath: asset.sourceRelPath,
|
|
16313
|
-
sourceAbsPath:
|
|
16651
|
+
sourceAbsPath: path84.join(member.absPath, asset.sourceRelPath),
|
|
16314
16652
|
sourceHash
|
|
16315
16653
|
});
|
|
16316
16654
|
}
|
|
@@ -16321,7 +16659,7 @@ async function buildLinkPlan(members) {
|
|
|
16321
16659
|
function destRelPathFor(type, destKey) {
|
|
16322
16660
|
const subdir = `${type}s`;
|
|
16323
16661
|
const leaf = type === "skill" ? destKey : `${destKey}.md`;
|
|
16324
|
-
return
|
|
16662
|
+
return path85.posix.join(".claude", subdir, leaf);
|
|
16325
16663
|
}
|
|
16326
16664
|
async function runLinkContext(workspaceRoot, opts = {}) {
|
|
16327
16665
|
const write = resolveWriteMode(opts, "preview-by-default").kind === "write";
|
|
@@ -16347,17 +16685,17 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
16347
16685
|
const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
16348
16686
|
for (const entry of plan.entries) {
|
|
16349
16687
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
16350
|
-
const destAbs =
|
|
16688
|
+
const destAbs = path85.join(workspaceRoot, destRel);
|
|
16351
16689
|
if (write) {
|
|
16352
|
-
await
|
|
16353
|
-
if (await
|
|
16354
|
-
await
|
|
16690
|
+
await fs63.ensureDir(path85.dirname(destAbs));
|
|
16691
|
+
if (await fs63.pathExists(destAbs)) await fs63.remove(destAbs);
|
|
16692
|
+
await fs63.copy(entry.sourceAbsPath, destAbs, {
|
|
16355
16693
|
overwrite: true,
|
|
16356
16694
|
errorOnExist: false,
|
|
16357
16695
|
dereference: false,
|
|
16358
16696
|
filter: (s) => {
|
|
16359
16697
|
try {
|
|
16360
|
-
return !
|
|
16698
|
+
return !fs63.lstatSync(s).isSymbolicLink();
|
|
16361
16699
|
} catch {
|
|
16362
16700
|
return false;
|
|
16363
16701
|
}
|
|
@@ -16386,7 +16724,7 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
16386
16724
|
warn(`Refusing to remove linked context path outside the workspace root: ${old.path}`);
|
|
16387
16725
|
continue;
|
|
16388
16726
|
}
|
|
16389
|
-
if (await
|
|
16727
|
+
if (await fs63.pathExists(abs)) await fs63.remove(abs);
|
|
16390
16728
|
}
|
|
16391
16729
|
}
|
|
16392
16730
|
if (write) {
|
|
@@ -16438,11 +16776,11 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
|
|
|
16438
16776
|
|
|
16439
16777
|
// src/commands/workspace/setup.ts
|
|
16440
16778
|
import { existsSync as existsSync10, statSync } from "node:fs";
|
|
16441
|
-
import
|
|
16779
|
+
import path87 from "node:path";
|
|
16442
16780
|
|
|
16443
16781
|
// src/claude/write-workspace-claude-md.ts
|
|
16444
|
-
import
|
|
16445
|
-
import
|
|
16782
|
+
import path86 from "node:path";
|
|
16783
|
+
import fs64 from "fs-extra";
|
|
16446
16784
|
init_fs();
|
|
16447
16785
|
init_logger();
|
|
16448
16786
|
init_paths();
|
|
@@ -16467,11 +16805,11 @@ ${WORKSPACE_BLOCK_END}`;
|
|
|
16467
16805
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
16468
16806
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
16469
16807
|
const dryRun = opts.dryRun ?? false;
|
|
16470
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
16808
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path86.join(workspaceRoot, "CLAUDE.md");
|
|
16471
16809
|
const say = opts.quiet ? () => {
|
|
16472
16810
|
} : log;
|
|
16473
|
-
const existed = await
|
|
16474
|
-
const prev = existed ? await
|
|
16811
|
+
const existed = await fs64.pathExists(filePath);
|
|
16812
|
+
const prev = existed ? await fs64.readFile(filePath, "utf8") : "";
|
|
16475
16813
|
const next = opts.collision ? `${block}
|
|
16476
16814
|
` : injectHausBlock(prev, block, WORKSPACE_SENTINELS);
|
|
16477
16815
|
const printable = displayPath(workspaceRoot, filePath);
|
|
@@ -16500,20 +16838,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
16500
16838
|
init_fs();
|
|
16501
16839
|
init_logger();
|
|
16502
16840
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
16503
|
-
let dir =
|
|
16841
|
+
let dir = path87.resolve(start);
|
|
16504
16842
|
for (; ; ) {
|
|
16505
|
-
if (existsSync10(
|
|
16506
|
-
const parent =
|
|
16507
|
-
if (parent === dir) return
|
|
16843
|
+
if (existsSync10(path87.join(dir, WORKSPACE_FILE))) return dir;
|
|
16844
|
+
const parent = path87.dirname(dir);
|
|
16845
|
+
if (parent === dir) return path87.resolve(start);
|
|
16508
16846
|
dir = parent;
|
|
16509
16847
|
}
|
|
16510
16848
|
}
|
|
16511
16849
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
16512
|
-
return
|
|
16850
|
+
return path87.resolve(workspaceRoot, repoPath) === path87.resolve(workspaceRoot);
|
|
16513
16851
|
}
|
|
16514
16852
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
16515
16853
|
const apply = options.write ?? false;
|
|
16516
|
-
const configText = await readText(
|
|
16854
|
+
const configText = await readText(path87.join(workspaceRoot, WORKSPACE_FILE));
|
|
16517
16855
|
if (!configText) {
|
|
16518
16856
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
16519
16857
|
process.exitCode = 1;
|
|
@@ -16540,7 +16878,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
16540
16878
|
const statuses = [];
|
|
16541
16879
|
const aggregateInputs = [];
|
|
16542
16880
|
for (const repo of repos) {
|
|
16543
|
-
const repoRoot =
|
|
16881
|
+
const repoRoot = path87.resolve(workspaceRoot, repo.path);
|
|
16544
16882
|
say(`
|
|
16545
16883
|
\u2192 ${repo.name} (${repo.path})`);
|
|
16546
16884
|
let res;
|
|
@@ -16656,7 +16994,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
16656
16994
|
const status = statusByName.get(repo.name);
|
|
16657
16995
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
16658
16996
|
if (status?.status === "ok") {
|
|
16659
|
-
const lock = await checkLock(
|
|
16997
|
+
const lock = await checkLock(path87.resolve(workspaceRoot, repo.path));
|
|
16660
16998
|
manifestRepos.push({
|
|
16661
16999
|
name: repo.name,
|
|
16662
17000
|
path: repo.path,
|
|
@@ -16712,7 +17050,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
16712
17050
|
try {
|
|
16713
17051
|
const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
|
|
16714
17052
|
if (linkResult.ok) {
|
|
16715
|
-
written.push(...linkResult.added.map((rel) =>
|
|
17053
|
+
written.push(...linkResult.added.map((rel) => path87.join(workspaceRoot, rel)));
|
|
16716
17054
|
if (!options.json) {
|
|
16717
17055
|
say(
|
|
16718
17056
|
`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` : ""}.`
|
|
@@ -16745,8 +17083,8 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
16745
17083
|
|
|
16746
17084
|
// src/commands/workspace/undo.ts
|
|
16747
17085
|
import { existsSync as existsSync11, statSync as statSync2 } from "node:fs";
|
|
16748
|
-
import
|
|
16749
|
-
import
|
|
17086
|
+
import path88 from "node:path";
|
|
17087
|
+
import fs65 from "fs-extra";
|
|
16750
17088
|
init_fs();
|
|
16751
17089
|
init_logger();
|
|
16752
17090
|
init_paths();
|
|
@@ -16776,7 +17114,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
16776
17114
|
}
|
|
16777
17115
|
if (options.dryRun) {
|
|
16778
17116
|
for (const repo of config.repos) {
|
|
16779
|
-
const repoRoot =
|
|
17117
|
+
const repoRoot = path88.resolve(workspaceRoot, repo.path);
|
|
16780
17118
|
log(`
|
|
16781
17119
|
\u2192 ${repo.name} (${repo.path})`);
|
|
16782
17120
|
try {
|
|
@@ -16797,7 +17135,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
16797
17135
|
warn(`Refusing to remove linked context path outside the workspace root: ${entry.path}`);
|
|
16798
17136
|
continue;
|
|
16799
17137
|
}
|
|
16800
|
-
if (await
|
|
17138
|
+
if (await fs65.pathExists(abs)) {
|
|
16801
17139
|
log(`[dry-run] Would remove ${entry.path} (linked ${entry.type} from ${entry.repo})`);
|
|
16802
17140
|
}
|
|
16803
17141
|
}
|
|
@@ -16807,12 +17145,12 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
16807
17145
|
hausPath(workspaceRoot, "WORKSPACE.md")
|
|
16808
17146
|
];
|
|
16809
17147
|
for (const abs of workspaceArtifacts) {
|
|
16810
|
-
if (await
|
|
16811
|
-
log(`[dry-run] Would remove ${
|
|
17148
|
+
if (await fs65.pathExists(abs)) {
|
|
17149
|
+
log(`[dry-run] Would remove ${path88.relative(workspaceRoot, abs)}`);
|
|
16812
17150
|
}
|
|
16813
17151
|
}
|
|
16814
|
-
if (!await
|
|
16815
|
-
const claudeMd = await readText(
|
|
17152
|
+
if (!await fs65.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
|
|
17153
|
+
const claudeMd = await readText(path88.join(workspaceRoot, "CLAUDE.md"));
|
|
16816
17154
|
if (claudeMd?.includes(WORKSPACE_BLOCK_BEGIN) || claudeMd && hasLegacyWorkspaceBlock(claudeMd)) {
|
|
16817
17155
|
log("[dry-run] Would remove the workspace haus import block from CLAUDE.md");
|
|
16818
17156
|
}
|
|
@@ -16831,7 +17169,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
16831
17169
|
}
|
|
16832
17170
|
let anyRepoFailed = false;
|
|
16833
17171
|
for (const repo of config.repos) {
|
|
16834
|
-
const repoRoot =
|
|
17172
|
+
const repoRoot = path88.resolve(workspaceRoot, repo.path);
|
|
16835
17173
|
log(`
|
|
16836
17174
|
\u2192 ${repo.name} (${repo.path})`);
|
|
16837
17175
|
try {
|
|
@@ -16851,29 +17189,29 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
16851
17189
|
warn(`Refusing to remove linked context path outside the workspace root: ${entry.path}`);
|
|
16852
17190
|
continue;
|
|
16853
17191
|
}
|
|
16854
|
-
if (await
|
|
16855
|
-
await
|
|
17192
|
+
if (await fs65.pathExists(abs)) {
|
|
17193
|
+
await fs65.remove(abs);
|
|
16856
17194
|
log(`Removed ${entry.path} (linked ${entry.type} from ${entry.repo})`);
|
|
16857
17195
|
}
|
|
16858
17196
|
}
|
|
16859
17197
|
for (const rel of WORKSPACE_AGGREGATE_FILES) {
|
|
16860
17198
|
const abs = hausPath(workspaceRoot, rel);
|
|
16861
|
-
if (await
|
|
16862
|
-
await
|
|
16863
|
-
log(`Removed ${
|
|
17199
|
+
if (await fs65.pathExists(abs)) {
|
|
17200
|
+
await fs65.remove(abs);
|
|
17201
|
+
log(`Removed ${path88.relative(workspaceRoot, abs)}`);
|
|
16864
17202
|
}
|
|
16865
17203
|
}
|
|
16866
17204
|
const manifestFile = manifestPath(workspaceRoot);
|
|
16867
|
-
if (await
|
|
16868
|
-
await
|
|
16869
|
-
log(`Removed ${
|
|
17205
|
+
if (await fs65.pathExists(manifestFile)) {
|
|
17206
|
+
await fs65.remove(manifestFile);
|
|
17207
|
+
log(`Removed ${path88.relative(workspaceRoot, manifestFile)}`);
|
|
16870
17208
|
}
|
|
16871
17209
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
16872
|
-
if (await
|
|
16873
|
-
await
|
|
16874
|
-
log(`Removed ${
|
|
17210
|
+
if (await fs65.pathExists(workspaceMdPath)) {
|
|
17211
|
+
await fs65.remove(workspaceMdPath);
|
|
17212
|
+
log(`Removed ${path88.relative(workspaceRoot, workspaceMdPath)}`);
|
|
16875
17213
|
} else {
|
|
16876
|
-
const claudeMdPath =
|
|
17214
|
+
const claudeMdPath = path88.join(workspaceRoot, "CLAUDE.md");
|
|
16877
17215
|
if (existsSync11(claudeMdPath)) {
|
|
16878
17216
|
const prev = await readText(claudeMdPath);
|
|
16879
17217
|
if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
|
|
@@ -16882,19 +17220,19 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
16882
17220
|
end: WORKSPACE_BLOCK_END
|
|
16883
17221
|
});
|
|
16884
17222
|
if (next.length === 0) {
|
|
16885
|
-
await
|
|
17223
|
+
await fs65.remove(claudeMdPath);
|
|
16886
17224
|
log("Removed CLAUDE.md (only contained the workspace haus import block).");
|
|
16887
17225
|
} else {
|
|
16888
|
-
await
|
|
17226
|
+
await fs65.writeFile(claudeMdPath, next, "utf8");
|
|
16889
17227
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
16890
17228
|
}
|
|
16891
17229
|
} else if (prev && hasLegacyWorkspaceBlock(prev)) {
|
|
16892
17230
|
const next = stripHausBlock(prev, { begin: BLOCK_BEGIN, end: BLOCK_END });
|
|
16893
17231
|
if (next.length === 0) {
|
|
16894
|
-
await
|
|
17232
|
+
await fs65.remove(claudeMdPath);
|
|
16895
17233
|
log("Removed CLAUDE.md (only contained the pre-upgrade workspace haus import block).");
|
|
16896
17234
|
} else {
|
|
16897
|
-
await
|
|
17235
|
+
await fs65.writeFile(claudeMdPath, next, "utf8");
|
|
16898
17236
|
log(
|
|
16899
17237
|
"Removed pre-upgrade workspace haus import block from CLAUDE.md (user content preserved)."
|
|
16900
17238
|
);
|
|
@@ -16942,7 +17280,7 @@ relationships: []
|
|
|
16942
17280
|
log("Workspace initialized.");
|
|
16943
17281
|
}
|
|
16944
17282
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
16945
|
-
const configText = await readText(
|
|
17283
|
+
const configText = await readText(path89.join(workspaceRoot, WORKSPACE_FILE));
|
|
16946
17284
|
if (!configText) {
|
|
16947
17285
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
16948
17286
|
process.exitCode = 1;
|
|
@@ -16964,7 +17302,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
16964
17302
|
const inputs = [];
|
|
16965
17303
|
const failures = [];
|
|
16966
17304
|
for (const repo of config.repos) {
|
|
16967
|
-
const repoRoot =
|
|
17305
|
+
const repoRoot = path89.resolve(workspaceRoot, repo.path);
|
|
16968
17306
|
try {
|
|
16969
17307
|
if (!existsSync12(repoRoot) || !statSync3(repoRoot).isDirectory()) {
|
|
16970
17308
|
throw new Error("path is not a directory");
|
|
@@ -17033,7 +17371,7 @@ init_logger();
|
|
|
17033
17371
|
init_paths();
|
|
17034
17372
|
function cliVersion() {
|
|
17035
17373
|
try {
|
|
17036
|
-
const pkgPath =
|
|
17374
|
+
const pkgPath = path90.join(packageRoot(), "package.json");
|
|
17037
17375
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
17038
17376
|
return pkg.version ?? "0.0.0";
|
|
17039
17377
|
} catch {
|
|
@@ -17043,7 +17381,7 @@ function cliVersion() {
|
|
|
17043
17381
|
var program = new Command();
|
|
17044
17382
|
function validateRuntimeNodeVersion() {
|
|
17045
17383
|
try {
|
|
17046
|
-
const pkgPath =
|
|
17384
|
+
const pkgPath = path90.join(packageRoot(), "package.json");
|
|
17047
17385
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
17048
17386
|
const requiredRange = pkg.engines?.node;
|
|
17049
17387
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -17137,7 +17475,17 @@ program.command("update").description("Sync the remote catalog cache, refresh th
|
|
|
17137
17475
|
program.command("ci-gate").description(
|
|
17138
17476
|
"Aggregate `doctor` + `decisions check` + `update --check --fast` into one pass/fail gate for CI. Uses the fast/cheap update-check tier (no per-item content hashing) to keep CI runs quick \u2014 run `haus update --check` directly for the fully-hashed tier. Each constituent command remains independently runnable with its own unchanged exit-code contract."
|
|
17139
17477
|
).option("--json", "Emit machine-readable {doctor, decisions, update, ok}").action((opts) => runCiGate(opts));
|
|
17140
|
-
program.command("install").description("Seed ~/.claude/ with Haus-managed skills, agents, and hook settings").option("--dry-run").option("--force").option("--check", "Exit non-zero if any HAUS-MANAGED file is out of date").option("--postinstall", "Run by the npm postinstall hook; prints a plain-language change notice").
|
|
17478
|
+
program.command("install").description("Seed ~/.claude/ with Haus-managed skills, agents, and hook settings").option("--dry-run").option("--force").option("--check", "Exit non-zero if any HAUS-MANAGED file is out of date").option("--postinstall", "Run by the npm postinstall hook; prints a plain-language change notice").addOption(
|
|
17479
|
+
new Option(
|
|
17480
|
+
"--internal",
|
|
17481
|
+
"Install Haus's internal channel too (request pipeline + slack-cowork); remembered per machine (ADR-0100)"
|
|
17482
|
+
).default(void 0)
|
|
17483
|
+
).addOption(
|
|
17484
|
+
new Option(
|
|
17485
|
+
"--no-internal",
|
|
17486
|
+
"Remove the internal channel from this machine and remember that"
|
|
17487
|
+
).default(void 0)
|
|
17488
|
+
).action(runInstall);
|
|
17141
17489
|
program.command("configs [ids...]").description(
|
|
17142
17490
|
"Copy catalog config items (ESLint, Prettier) into the project root. Pass item IDs to copy specific items, or omit to copy all available config items."
|
|
17143
17491
|
).option("--force", "Overwrite existing files").option("--dry-run", "Preview what would be written without changing anything").option("--root <path>", "Project root (defaults to cwd)", process.cwd()).action(runConfigs);
|