@haus-tech/haus-workflow 5.2.0 → 5.4.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 +16 -0
- package/dist/cli.js +851 -462
- package/library/catalog/manifest.json +52 -8
- package/library/global/skills/request-sync/SKILL.md +13 -8
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -1226,7 +1226,8 @@ function parseManifest(json) {
|
|
|
1226
1226
|
version: root.version,
|
|
1227
1227
|
manifestFormat: format.format,
|
|
1228
1228
|
items,
|
|
1229
|
-
...root.globalPlugins !== void 0 ? { globalPlugins: root.globalPlugins } : {}
|
|
1229
|
+
...root.globalPlugins !== void 0 ? { globalPlugins: root.globalPlugins } : {},
|
|
1230
|
+
...root.profiles !== void 0 ? { profiles: root.profiles } : {}
|
|
1230
1231
|
},
|
|
1231
1232
|
warnings
|
|
1232
1233
|
};
|
|
@@ -1623,6 +1624,7 @@ var init_ingest_catalog = __esm({
|
|
|
1623
1624
|
// src/catalog/remote-catalog/sync.ts
|
|
1624
1625
|
var sync_exports = {};
|
|
1625
1626
|
__export(sync_exports, {
|
|
1627
|
+
buildCacheManifest: () => buildCacheManifest,
|
|
1626
1628
|
getCacheManifestAge: () => getCacheManifestAge,
|
|
1627
1629
|
syncRemoteCatalog: () => syncRemoteCatalog
|
|
1628
1630
|
});
|
|
@@ -1859,6 +1861,16 @@ async function syncOneItem(item, base) {
|
|
|
1859
1861
|
return "failed";
|
|
1860
1862
|
}
|
|
1861
1863
|
}
|
|
1864
|
+
function buildCacheManifest(input2) {
|
|
1865
|
+
return {
|
|
1866
|
+
version: input2.version,
|
|
1867
|
+
manifestFormat: input2.manifestFormat,
|
|
1868
|
+
...input2.catalogRef !== null ? { catalogRef: input2.catalogRef } : {},
|
|
1869
|
+
items: input2.items,
|
|
1870
|
+
...input2.globalPlugins !== void 0 ? { globalPlugins: input2.globalPlugins } : {},
|
|
1871
|
+
...input2.profiles !== void 0 ? { profiles: input2.profiles } : {}
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1862
1874
|
async function syncRemoteCatalog(projectRoot) {
|
|
1863
1875
|
if (isOffline()) {
|
|
1864
1876
|
warn("Offline \u2014 skipping catalog sync; using cached or bundled content.");
|
|
@@ -1888,7 +1900,7 @@ async function syncRemoteCatalog(projectRoot) {
|
|
|
1888
1900
|
...getPrivateCatalogDenial() ? { accessDenied: getPrivateCatalogDenial() } : {}
|
|
1889
1901
|
};
|
|
1890
1902
|
}
|
|
1891
|
-
const { version, manifestFormat, items, globalPlugins } = fetched.manifest;
|
|
1903
|
+
const { version, manifestFormat, items, globalPlugins, profiles } = fetched.manifest;
|
|
1892
1904
|
const cacheDir = getCacheDir();
|
|
1893
1905
|
const catalogRef = expectedCatalogRef(projectRoot);
|
|
1894
1906
|
try {
|
|
@@ -1911,13 +1923,7 @@ async function syncRemoteCatalog(projectRoot) {
|
|
|
1911
1923
|
// per-project pin (#239) means two projects can want two different refs: without
|
|
1912
1924
|
// this field, whichever synced last silently serves its content to the other.
|
|
1913
1925
|
`${JSON.stringify(
|
|
1914
|
-
{
|
|
1915
|
-
version,
|
|
1916
|
-
manifestFormat,
|
|
1917
|
-
...catalogRef !== null ? { catalogRef } : {},
|
|
1918
|
-
items,
|
|
1919
|
-
...globalPlugins !== void 0 ? { globalPlugins } : {}
|
|
1920
|
-
},
|
|
1926
|
+
buildCacheManifest({ version, manifestFormat, catalogRef, items, globalPlugins, profiles }),
|
|
1921
1927
|
null,
|
|
1922
1928
|
2
|
|
1923
1929
|
)}
|
|
@@ -1998,7 +2004,7 @@ var init_sync = __esm({
|
|
|
1998
2004
|
// src/cli.ts
|
|
1999
2005
|
init_offline();
|
|
2000
2006
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2001
|
-
import
|
|
2007
|
+
import path92 from "node:path";
|
|
2002
2008
|
import { Command, Option } from "commander";
|
|
2003
2009
|
|
|
2004
2010
|
// src/commands/apply.ts
|
|
@@ -2210,13 +2216,14 @@ import path9 from "node:path";
|
|
|
2210
2216
|
import fs7 from "fs-extra";
|
|
2211
2217
|
var WORKFLOW_TEMPLATE_REL = "templates/agentic-workflow-standard.md";
|
|
2212
2218
|
async function readWorkflowTemplate(opts = {}) {
|
|
2213
|
-
const
|
|
2219
|
+
const rel = opts.templateRel ?? WORKFLOW_TEMPLATE_REL;
|
|
2220
|
+
const dest = path9.join(getCacheDir(), rel);
|
|
2214
2221
|
if (isOffline()) {
|
|
2215
2222
|
if (await fs7.pathExists(dest)) return fs7.readFile(dest, "utf8");
|
|
2216
2223
|
return null;
|
|
2217
2224
|
}
|
|
2218
2225
|
const base = await remoteBase(opts.projectRoot);
|
|
2219
|
-
const { url, headers } = await catalogContentRequest(base,
|
|
2226
|
+
const { url, headers } = await catalogContentRequest(base, rel);
|
|
2220
2227
|
const text = await fetchText(url, headers);
|
|
2221
2228
|
if (text === null) {
|
|
2222
2229
|
if (await fs7.pathExists(dest)) return fs7.readFile(dest, "utf8");
|
|
@@ -2251,7 +2258,8 @@ async function loadCatalogContext(root) {
|
|
|
2251
2258
|
source: "fixture",
|
|
2252
2259
|
manifestFormat: assertSupportedManifestFormat(data2?.manifestFormat, "The fixture catalog"),
|
|
2253
2260
|
version: manifestVersion(data2?.version),
|
|
2254
|
-
globalPlugins: data2?.globalPlugins
|
|
2261
|
+
globalPlugins: data2?.globalPlugins,
|
|
2262
|
+
profiles: data2?.profiles
|
|
2255
2263
|
};
|
|
2256
2264
|
}
|
|
2257
2265
|
const cacheDir = getCacheDir();
|
|
@@ -2273,7 +2281,8 @@ async function loadCatalogContext(root) {
|
|
|
2273
2281
|
"The cached catalog"
|
|
2274
2282
|
),
|
|
2275
2283
|
version: manifestVersion(cacheData.version),
|
|
2276
|
-
globalPlugins: cacheData.globalPlugins
|
|
2284
|
+
globalPlugins: cacheData.globalPlugins,
|
|
2285
|
+
profiles: cacheData.profiles
|
|
2277
2286
|
};
|
|
2278
2287
|
}
|
|
2279
2288
|
}
|
|
@@ -2289,7 +2298,8 @@ async function loadCatalogContext(root) {
|
|
|
2289
2298
|
"The project-local catalog"
|
|
2290
2299
|
),
|
|
2291
2300
|
version: manifestVersion(localData.version),
|
|
2292
|
-
globalPlugins: localData.globalPlugins
|
|
2301
|
+
globalPlugins: localData.globalPlugins,
|
|
2302
|
+
profiles: localData.profiles
|
|
2293
2303
|
};
|
|
2294
2304
|
}
|
|
2295
2305
|
const packageManifest = path11.join(packageRoot(), "library/catalog/manifest.json");
|
|
@@ -2300,7 +2310,8 @@ async function loadCatalogContext(root) {
|
|
|
2300
2310
|
source: "bundled",
|
|
2301
2311
|
manifestFormat: assertSupportedManifestFormat(data?.manifestFormat, "The bundled catalog"),
|
|
2302
2312
|
version: manifestVersion(data?.version),
|
|
2303
|
-
globalPlugins: data?.globalPlugins
|
|
2313
|
+
globalPlugins: data?.globalPlugins,
|
|
2314
|
+
profiles: data?.profiles
|
|
2304
2315
|
};
|
|
2305
2316
|
}
|
|
2306
2317
|
async function catalogFormatRefusal(root) {
|
|
@@ -2509,7 +2520,7 @@ async function migrateRequestState(root, dryRun, notify) {
|
|
|
2509
2520
|
|
|
2510
2521
|
// src/claude/write-claude-files.ts
|
|
2511
2522
|
import path27 from "node:path";
|
|
2512
|
-
import
|
|
2523
|
+
import fs26 from "fs-extra";
|
|
2513
2524
|
init_ingest_catalog();
|
|
2514
2525
|
|
|
2515
2526
|
// src/catalog/skill-description.ts
|
|
@@ -2845,6 +2856,206 @@ function mergeRecommendationWarnings(context, isLinkedWorktree = false) {
|
|
|
2845
2856
|
return [.../* @__PURE__ */ new Set([...statusLines, ...context.warnings, ...riskLines])];
|
|
2846
2857
|
}
|
|
2847
2858
|
|
|
2859
|
+
// src/claude/project-profile.ts
|
|
2860
|
+
init_project_pin();
|
|
2861
|
+
init_logger();
|
|
2862
|
+
|
|
2863
|
+
// src/claude/project-intent.ts
|
|
2864
|
+
init_project_pin();
|
|
2865
|
+
init_fs();
|
|
2866
|
+
init_logger();
|
|
2867
|
+
init_paths();
|
|
2868
|
+
import fs10 from "fs-extra";
|
|
2869
|
+
function normalizeIdList(value) {
|
|
2870
|
+
if (!Array.isArray(value)) return [];
|
|
2871
|
+
return [
|
|
2872
|
+
...new Set(
|
|
2873
|
+
value.filter((entry) => typeof entry === "string" && entry.length > 0)
|
|
2874
|
+
)
|
|
2875
|
+
];
|
|
2876
|
+
}
|
|
2877
|
+
function normalizeSignalList(value) {
|
|
2878
|
+
return [...new Set(normalizeIdList(value).map((entry) => entry.toLowerCase()))];
|
|
2879
|
+
}
|
|
2880
|
+
async function readProjectConfigObject(root) {
|
|
2881
|
+
try {
|
|
2882
|
+
const data = await readJson(hausPath(root, PROJECT_CONFIG_FILE));
|
|
2883
|
+
if (data == null || typeof data !== "object" || Array.isArray(data)) return null;
|
|
2884
|
+
return data;
|
|
2885
|
+
} catch (err) {
|
|
2886
|
+
if (err.code === "ENOENT") return null;
|
|
2887
|
+
warn(
|
|
2888
|
+
`${PROJECT_CONFIG_REL} could not be read (${err instanceof Error ? err.message : String(err)}) \u2014 treating project intent as unset`
|
|
2889
|
+
);
|
|
2890
|
+
return null;
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
async function updateProjectConfig(root, mutate) {
|
|
2894
|
+
const file = hausPath(root, PROJECT_CONFIG_FILE);
|
|
2895
|
+
let existing = {};
|
|
2896
|
+
if (await fs10.pathExists(file)) {
|
|
2897
|
+
try {
|
|
2898
|
+
const parsed = JSON.parse(await fs10.readFile(file, "utf8"));
|
|
2899
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
2900
|
+
existing = { ...parsed };
|
|
2901
|
+
} else {
|
|
2902
|
+
warn(`${PROJECT_CONFIG_REL} is not a JSON object \u2014 rewriting with haus-managed keys only`);
|
|
2903
|
+
}
|
|
2904
|
+
} catch (err) {
|
|
2905
|
+
warn(
|
|
2906
|
+
`${PROJECT_CONFIG_REL} could not be parsed (${err instanceof Error ? err.message : String(err)}) \u2014 rewriting with haus-managed keys only`
|
|
2907
|
+
);
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
await writeJson(file, mutate(existing));
|
|
2911
|
+
}
|
|
2912
|
+
async function readProjectIncludedIds(root) {
|
|
2913
|
+
const data = await readProjectConfigObject(root);
|
|
2914
|
+
return data == null ? [] : normalizeIdList(data.includedIds);
|
|
2915
|
+
}
|
|
2916
|
+
async function addProjectIncludedIds(root, ids) {
|
|
2917
|
+
const toAdd = normalizeIdList(ids);
|
|
2918
|
+
if (toAdd.length === 0) return;
|
|
2919
|
+
await updateProjectConfig(root, (existing) => ({
|
|
2920
|
+
...existing,
|
|
2921
|
+
includedIds: [.../* @__PURE__ */ new Set([...normalizeIdList(existing.includedIds), ...toAdd])].sort()
|
|
2922
|
+
}));
|
|
2923
|
+
}
|
|
2924
|
+
function withdrawIncludedIds(existing, ids) {
|
|
2925
|
+
const drop = new Set(ids);
|
|
2926
|
+
return normalizeIdList(existing.includedIds).filter((id) => !drop.has(id));
|
|
2927
|
+
}
|
|
2928
|
+
async function readDeclaredContext(root) {
|
|
2929
|
+
const data = await readProjectConfigObject(root);
|
|
2930
|
+
if (data == null) return { roles: [], stacks: [] };
|
|
2931
|
+
return {
|
|
2932
|
+
roles: normalizeSignalList(data.declaredRoles),
|
|
2933
|
+
stacks: normalizeSignalList(data.declaredStacks)
|
|
2934
|
+
};
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
// src/claude/project-profile.ts
|
|
2938
|
+
var PROFILE_IDS = ["production", "poc"];
|
|
2939
|
+
var DEFAULT_PROFILE = "production";
|
|
2940
|
+
var PROFILES = {
|
|
2941
|
+
production: {
|
|
2942
|
+
workflowTemplateRel: WORKFLOW_TEMPLATE_REL,
|
|
2943
|
+
writesCiGate: true,
|
|
2944
|
+
writesDecisionsSeed: true,
|
|
2945
|
+
droppedHookCommands: [],
|
|
2946
|
+
workflowConfig: "standard"
|
|
2947
|
+
},
|
|
2948
|
+
poc: {
|
|
2949
|
+
// Catalog item `haus.agentic-workflow-poc` (catalog ADR-0066); fetched by path for WORKFLOW.md, exactly like the production standard, and kept out of item recommendations by id (Task 3).
|
|
2950
|
+
workflowTemplateRel: "templates/agentic-workflow-poc.md",
|
|
2951
|
+
writesCiGate: false,
|
|
2952
|
+
writesDecisionsSeed: false,
|
|
2953
|
+
droppedHookCommands: ["haus decisions guard --from-hook", "haus decisions suggest --from-hook"],
|
|
2954
|
+
workflowConfig: "poc"
|
|
2955
|
+
}
|
|
2956
|
+
};
|
|
2957
|
+
function isProfileId(value) {
|
|
2958
|
+
return typeof value === "string" && PROFILE_IDS.includes(value);
|
|
2959
|
+
}
|
|
2960
|
+
function parseProfile(value) {
|
|
2961
|
+
if (value === void 0 || value === null) return { profile: DEFAULT_PROFILE, malformed: false };
|
|
2962
|
+
if (isProfileId(value)) return { profile: value, malformed: false };
|
|
2963
|
+
return { profile: DEFAULT_PROFILE, malformed: true };
|
|
2964
|
+
}
|
|
2965
|
+
async function readProjectProfile(root) {
|
|
2966
|
+
const data = await readProjectConfigObject(root);
|
|
2967
|
+
const { profile, malformed } = parseProfile(data?.profile);
|
|
2968
|
+
if (malformed) {
|
|
2969
|
+
warn(
|
|
2970
|
+
`${PROJECT_CONFIG_REL}: unknown profile ${JSON.stringify(data?.profile)} \u2014 expected one of ${PROFILE_IDS.join(", ")}; treating as ${DEFAULT_PROFILE}`
|
|
2971
|
+
);
|
|
2972
|
+
}
|
|
2973
|
+
return profile;
|
|
2974
|
+
}
|
|
2975
|
+
async function writeProjectProfile(root, profile) {
|
|
2976
|
+
await updateProjectConfig(root, (existing) => ({ ...existing, profile }));
|
|
2977
|
+
}
|
|
2978
|
+
function pocLockedMessage() {
|
|
2979
|
+
return `This repo is a PoC (profile "poc" in ${PROJECT_CONFIG_REL}). Production starts from zero in a new repo, with docs/MANUS.md as input (\`/haus-workflow new\` in production mode). To re-run setup here, pass \`haus init --profile poc\`. To leave the PoC profile on purpose, remove the "profile" line from ${PROJECT_CONFIG_REL} by hand. Nothing was written.`;
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
// src/recommender/profile-gate.ts
|
|
2983
|
+
init_logger();
|
|
2984
|
+
var WORKFLOW_TEMPLATE_ITEM_IDS = /* @__PURE__ */ new Set([
|
|
2985
|
+
"haus.agentic-workflow-poc"
|
|
2986
|
+
]);
|
|
2987
|
+
var KNOWN_CLAUSES = ["tags", "ids"];
|
|
2988
|
+
function stringList(value, label) {
|
|
2989
|
+
if (value === void 0) return [];
|
|
2990
|
+
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
|
|
2991
|
+
warn(`${label} is not a string array \u2014 ignoring it`);
|
|
2992
|
+
return [];
|
|
2993
|
+
}
|
|
2994
|
+
return value;
|
|
2995
|
+
}
|
|
2996
|
+
function parseProfileRules(raw, profile, knownIds) {
|
|
2997
|
+
if (profile === "production") return { tags: /* @__PURE__ */ new Set(), ids: /* @__PURE__ */ new Set(), keepIds: /* @__PURE__ */ new Set() };
|
|
2998
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
2999
|
+
const block = raw[profile];
|
|
3000
|
+
if (!block || typeof block !== "object" || Array.isArray(block)) return null;
|
|
3001
|
+
const exclude = block.exclude;
|
|
3002
|
+
const clauses = exclude && typeof exclude === "object" && !Array.isArray(exclude) ? exclude : {};
|
|
3003
|
+
for (const key of Object.keys(clauses)) {
|
|
3004
|
+
if (!KNOWN_CLAUSES.includes(key)) {
|
|
3005
|
+
warn(
|
|
3006
|
+
`profiles.${profile}.exclude.${key}: unknown clause (catalog schema evolved ahead of this CLI version) \u2014 ignoring it`
|
|
3007
|
+
);
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
const keepIds = stringList(
|
|
3011
|
+
block.keepIds,
|
|
3012
|
+
`profiles.${profile}.keepIds`
|
|
3013
|
+
).filter((id) => {
|
|
3014
|
+
if (knownIds !== void 0 && !knownIds.has(id)) {
|
|
3015
|
+
warn(
|
|
3016
|
+
`profiles.${profile}.keepIds names an id the catalog does not have (${id}) \u2014 ignoring it`
|
|
3017
|
+
);
|
|
3018
|
+
return false;
|
|
3019
|
+
}
|
|
3020
|
+
return true;
|
|
3021
|
+
});
|
|
3022
|
+
return {
|
|
3023
|
+
tags: new Set(
|
|
3024
|
+
stringList(clauses.tags, `profiles.${profile}.exclude.tags`).map((t) => t.toLowerCase())
|
|
3025
|
+
),
|
|
3026
|
+
ids: new Set(stringList(clauses.ids, `profiles.${profile}.exclude.ids`)),
|
|
3027
|
+
keepIds: new Set(keepIds)
|
|
3028
|
+
};
|
|
3029
|
+
}
|
|
3030
|
+
function profileVerdict(item, rules) {
|
|
3031
|
+
if (rules.ids.has(item.id)) return { kind: "excluded", clause: `id:${item.id}` };
|
|
3032
|
+
const tag = item.tags.find((t) => rules.tags.has(t.toLowerCase()));
|
|
3033
|
+
if (!tag) return null;
|
|
3034
|
+
const clause = `tag:${tag.toLowerCase()}`;
|
|
3035
|
+
return rules.keepIds.has(item.id) ? { kind: "kept", clause } : { kind: "excluded", clause };
|
|
3036
|
+
}
|
|
3037
|
+
function describeCatalogTier(ctx) {
|
|
3038
|
+
return `${ctx.source}${ctx.version ? ` (v${ctx.version})` : ""}`;
|
|
3039
|
+
}
|
|
3040
|
+
function profileRefusalMessage(profile, catalogLabel) {
|
|
3041
|
+
return `This project is set to profile "${profile}", but the ${catalogLabel} catalog defines no "${profile}" profile (needs haus-workflow-catalog >= 6.5.0). Run \`haus update\` to fetch a newer catalog, or set "profile" back to "production" in .haus-workflow/haus.config.json.`;
|
|
3042
|
+
}
|
|
3043
|
+
var ProfileUnsupportedError = class extends Error {
|
|
3044
|
+
profile;
|
|
3045
|
+
catalogLabel;
|
|
3046
|
+
constructor(profile, catalogLabel) {
|
|
3047
|
+
super(profileRefusalMessage(profile, catalogLabel));
|
|
3048
|
+
this.name = "ProfileUnsupportedError";
|
|
3049
|
+
this.profile = profile;
|
|
3050
|
+
this.catalogLabel = catalogLabel;
|
|
3051
|
+
}
|
|
3052
|
+
};
|
|
3053
|
+
async function profileRefusal(root, profileOverride) {
|
|
3054
|
+
const profile = profileOverride ?? await readProjectProfile(root);
|
|
3055
|
+
const ctx = await loadCatalogContext(root);
|
|
3056
|
+
return parseProfileRules(ctx.profiles, profile) === null ? profileRefusalMessage(profile, describeCatalogTier(ctx)) : null;
|
|
3057
|
+
}
|
|
3058
|
+
|
|
2848
3059
|
// src/recommender/superseded-by-plugin.ts
|
|
2849
3060
|
var SUPERSESSION_RULES = [
|
|
2850
3061
|
{
|
|
@@ -2875,7 +3086,7 @@ function isSupersededByPlugin(lockId, activePinIds) {
|
|
|
2875
3086
|
|
|
2876
3087
|
// src/scanner/detect-package-manager.ts
|
|
2877
3088
|
import path14 from "node:path";
|
|
2878
|
-
import
|
|
3089
|
+
import fs11 from "fs-extra";
|
|
2879
3090
|
function detectPackageManager(root, packageManagerField) {
|
|
2880
3091
|
const field = String(packageManagerField ?? "").trim();
|
|
2881
3092
|
if (field.startsWith("yarn@")) {
|
|
@@ -2893,16 +3104,16 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
2893
3104
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
2894
3105
|
return "unknown";
|
|
2895
3106
|
}
|
|
2896
|
-
if (
|
|
2897
|
-
if (
|
|
2898
|
-
if (
|
|
3107
|
+
if (fs11.existsSync(path14.join(root, "yarn.lock"))) return "yarn";
|
|
3108
|
+
if (fs11.existsSync(path14.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
3109
|
+
if (fs11.existsSync(path14.join(root, "package-lock.json"))) return "npm";
|
|
2899
3110
|
return "unknown";
|
|
2900
3111
|
}
|
|
2901
3112
|
|
|
2902
3113
|
// src/update/hash-installed.ts
|
|
2903
3114
|
import path16 from "node:path";
|
|
2904
3115
|
import fg3 from "fast-glob";
|
|
2905
|
-
import
|
|
3116
|
+
import fs13 from "fs-extra";
|
|
2906
3117
|
|
|
2907
3118
|
// src/claude/haus-managed-header.ts
|
|
2908
3119
|
function parseHausManagedAttrs(line2) {
|
|
@@ -2942,7 +3153,7 @@ function parseHausManagedHeader(line2) {
|
|
|
2942
3153
|
// src/claude/superpowers-install.ts
|
|
2943
3154
|
import path15 from "node:path";
|
|
2944
3155
|
import fg2 from "fast-glob";
|
|
2945
|
-
import
|
|
3156
|
+
import fs12 from "fs-extra";
|
|
2946
3157
|
init_logger();
|
|
2947
3158
|
var SKILL_DEP_ARTIFACTS = ["node_modules/**"];
|
|
2948
3159
|
var SKILL_DEP_ARTIFACT_DIRS = /* @__PURE__ */ new Set(["node_modules"]);
|
|
@@ -2965,34 +3176,34 @@ async function previewInstalledSkill(srcFile, opts) {
|
|
|
2965
3176
|
return next === text ? void 0 : Buffer.from(next, "utf8");
|
|
2966
3177
|
}
|
|
2967
3178
|
async function readRewritableText(file) {
|
|
2968
|
-
const raw = await
|
|
3179
|
+
const raw = await fs12.readFile(file);
|
|
2969
3180
|
const text = raw.toString("utf8");
|
|
2970
3181
|
return Buffer.from(text, "utf8").equals(raw) ? text : void 0;
|
|
2971
3182
|
}
|
|
2972
3183
|
async function composeInstalledDescription(destination, signal) {
|
|
2973
3184
|
const skillFile = path15.join(destination, SKILL_ENTRY_FILE);
|
|
2974
|
-
if (!await
|
|
3185
|
+
if (!await fs12.pathExists(skillFile)) return;
|
|
2975
3186
|
const text = await readRewritableText(skillFile);
|
|
2976
3187
|
if (text === void 0) return;
|
|
2977
3188
|
const composed = rewriteSkillDescription(text, signal);
|
|
2978
|
-
if (composed !== text) await
|
|
3189
|
+
if (composed !== text) await fs12.writeFile(skillFile, composed, "utf8");
|
|
2979
3190
|
}
|
|
2980
3191
|
async function installCatalogSkill(sourcePath, destination, opts) {
|
|
2981
3192
|
if (opts.dryRun) return;
|
|
2982
|
-
await
|
|
2983
|
-
if (await
|
|
3193
|
+
await fs12.ensureDir(path15.dirname(destination));
|
|
3194
|
+
if (await fs12.pathExists(destination)) {
|
|
2984
3195
|
const extra = await extraDestFiles(sourcePath, destination);
|
|
2985
3196
|
if (extra.length > 0) {
|
|
2986
3197
|
warn(
|
|
2987
3198
|
`${destination}: removing ${extra.length} untracked file(s) not present in the catalog source before reinstall: ${extra.join(", ")}`
|
|
2988
3199
|
);
|
|
2989
3200
|
}
|
|
2990
|
-
for (const entry of await
|
|
3201
|
+
for (const entry of await fs12.readdir(destination)) {
|
|
2991
3202
|
if (SKILL_DEP_ARTIFACT_DIRS.has(entry)) continue;
|
|
2992
|
-
await
|
|
3203
|
+
await fs12.remove(path15.join(destination, entry));
|
|
2993
3204
|
}
|
|
2994
3205
|
}
|
|
2995
|
-
await
|
|
3206
|
+
await fs12.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
2996
3207
|
if (opts.description) await composeInstalledDescription(destination, opts.description);
|
|
2997
3208
|
}
|
|
2998
3209
|
|
|
@@ -3016,11 +3227,11 @@ async function hashInstalledPaths(root, relPaths, opts = {}) {
|
|
|
3016
3227
|
const fileDigests = [];
|
|
3017
3228
|
for (const rel of normalized) {
|
|
3018
3229
|
const abs = path16.join(root, rel);
|
|
3019
|
-
if (!await
|
|
3020
|
-
const stat = followSymlinks ? await
|
|
3230
|
+
if (!await fs13.pathExists(abs)) continue;
|
|
3231
|
+
const stat = followSymlinks ? await fs13.stat(abs) : await fs13.lstat(abs);
|
|
3021
3232
|
if (!followSymlinks && stat.isSymbolicLink()) continue;
|
|
3022
3233
|
if (stat.isFile()) {
|
|
3023
|
-
const body = await
|
|
3234
|
+
const body = await fs13.readFile(abs);
|
|
3024
3235
|
fileDigests.push({ rel, digest: digestFileContent(body) });
|
|
3025
3236
|
continue;
|
|
3026
3237
|
}
|
|
@@ -3041,9 +3252,9 @@ async function hashInstalledPaths(root, relPaths, opts = {}) {
|
|
|
3041
3252
|
});
|
|
3042
3253
|
for (const sub of inner.sort()) {
|
|
3043
3254
|
const absFile = path16.join(abs, sub);
|
|
3044
|
-
if (!followSymlinks && (await
|
|
3255
|
+
if (!followSymlinks && (await fs13.lstat(absFile)).isSymbolicLink()) continue;
|
|
3045
3256
|
const relFile = path16.join(rel, sub).replace(/\\/g, "/");
|
|
3046
|
-
const body = await
|
|
3257
|
+
const body = await fs13.readFile(absFile);
|
|
3047
3258
|
fileDigests.push({ rel: relFile, digest: digestFileContent(body) });
|
|
3048
3259
|
}
|
|
3049
3260
|
}
|
|
@@ -3082,15 +3293,15 @@ init_logger();
|
|
|
3082
3293
|
init_paths();
|
|
3083
3294
|
|
|
3084
3295
|
// src/claude/managed-write.ts
|
|
3085
|
-
import
|
|
3296
|
+
import fs14 from "fs-extra";
|
|
3086
3297
|
init_fs();
|
|
3087
3298
|
init_logger();
|
|
3088
3299
|
init_paths();
|
|
3089
3300
|
async function writeManagedText(root, filePath, nextText, dryRun, quiet = false) {
|
|
3090
3301
|
const say = quiet ? () => {
|
|
3091
3302
|
} : log;
|
|
3092
|
-
const existed = await
|
|
3093
|
-
const prev = existed ? await
|
|
3303
|
+
const existed = await fs14.pathExists(filePath);
|
|
3304
|
+
const prev = existed ? await fs14.readFile(filePath, "utf8") : "";
|
|
3094
3305
|
const printable = displayPath(root, filePath);
|
|
3095
3306
|
if (dryRun) {
|
|
3096
3307
|
if (!existed) {
|
|
@@ -3177,7 +3388,7 @@ function buildAllowRules() {
|
|
|
3177
3388
|
// src/install/settings-merge.ts
|
|
3178
3389
|
init_fs();
|
|
3179
3390
|
import path18 from "node:path";
|
|
3180
|
-
import
|
|
3391
|
+
import fs15 from "fs-extra";
|
|
3181
3392
|
|
|
3182
3393
|
// src/install/manifest.ts
|
|
3183
3394
|
init_fs();
|
|
@@ -3487,7 +3698,7 @@ function stripHausAsk(settings) {
|
|
|
3487
3698
|
async function loadHooksFragment(fragmentPath) {
|
|
3488
3699
|
let raw;
|
|
3489
3700
|
try {
|
|
3490
|
-
raw = await
|
|
3701
|
+
raw = await fs15.readJson(fragmentPath);
|
|
3491
3702
|
} catch {
|
|
3492
3703
|
return [];
|
|
3493
3704
|
}
|
|
@@ -3547,8 +3758,8 @@ function buildDenyRules() {
|
|
|
3547
3758
|
if (RUNTIME_ONLY_DENY_COMMANDS.includes(command)) continue;
|
|
3548
3759
|
rules.push(`Bash(${command}:*)`);
|
|
3549
3760
|
}
|
|
3550
|
-
for (const
|
|
3551
|
-
const pattern = DENY_DIRS.has(
|
|
3761
|
+
for (const path93 of DENY_PATHS) {
|
|
3762
|
+
const pattern = DENY_DIRS.has(path93) ? `${path93}/**` : path93;
|
|
3552
3763
|
for (const tool of FILE_TOOLS) {
|
|
3553
3764
|
rules.push(`${tool}(${pattern})`);
|
|
3554
3765
|
}
|
|
@@ -3611,6 +3822,11 @@ var PROJECT_HOOK_FRAGMENTS = [
|
|
|
3611
3822
|
command: "haus workspace worktree autohydrate --from-hook"
|
|
3612
3823
|
}
|
|
3613
3824
|
];
|
|
3825
|
+
function projectHookFragmentsFor(profile) {
|
|
3826
|
+
const dropped = new Set(PROFILES[profile].droppedHookCommands);
|
|
3827
|
+
if (dropped.size === 0) return PROJECT_HOOK_FRAGMENTS;
|
|
3828
|
+
return PROJECT_HOOK_FRAGMENTS.filter((fragment) => !dropped.has(fragment.command));
|
|
3829
|
+
}
|
|
3614
3830
|
async function readProjectSettings(root) {
|
|
3615
3831
|
const filePath = claudePath(root, "settings.json");
|
|
3616
3832
|
const result = await readJsonDetailed(filePath);
|
|
@@ -3622,16 +3838,17 @@ async function readProjectSettings(root) {
|
|
|
3622
3838
|
async function writeProjectSettings(root, settings) {
|
|
3623
3839
|
await writeJson(claudePath(root, "settings.json"), settings);
|
|
3624
3840
|
}
|
|
3625
|
-
async function mergeProjectSettings(root) {
|
|
3841
|
+
async function mergeProjectSettings(root, profile) {
|
|
3842
|
+
const resolved = profile ?? await readProjectProfile(root);
|
|
3626
3843
|
const base = await readProjectSettings(root);
|
|
3627
|
-
const { settings: withHooks } = mergeHooks(base,
|
|
3844
|
+
const { settings: withHooks } = mergeHooks(base, projectHookFragmentsFor(resolved));
|
|
3628
3845
|
const { settings: withDeny } = mergeDenyRules(withHooks, buildDenyRules());
|
|
3629
3846
|
const { settings: withAllow } = mergeAllowRules(withDeny, buildAllowRules());
|
|
3630
3847
|
const { settings: merged } = mergeAskRules(withAllow, buildAskRules());
|
|
3631
3848
|
return merged;
|
|
3632
3849
|
}
|
|
3633
|
-
async function applyProjectSettingsMerge(root) {
|
|
3634
|
-
const merged = await mergeProjectSettings(root);
|
|
3850
|
+
async function applyProjectSettingsMerge(root, profile) {
|
|
3851
|
+
const merged = await mergeProjectSettings(root, profile);
|
|
3635
3852
|
await writeProjectSettings(root, merged);
|
|
3636
3853
|
return merged;
|
|
3637
3854
|
}
|
|
@@ -3639,7 +3856,7 @@ async function applyProjectSettingsMerge(root) {
|
|
|
3639
3856
|
// src/claude/remove-items.ts
|
|
3640
3857
|
import crypto2 from "node:crypto";
|
|
3641
3858
|
import path19 from "node:path";
|
|
3642
|
-
import
|
|
3859
|
+
import fs16 from "fs-extra";
|
|
3643
3860
|
init_fs();
|
|
3644
3861
|
init_paths();
|
|
3645
3862
|
|
|
@@ -3669,16 +3886,16 @@ function classifyBackupName(name) {
|
|
|
3669
3886
|
async function unsafeTrackedPath(root, rel) {
|
|
3670
3887
|
const abs = resolveContainedPath(root, rel);
|
|
3671
3888
|
if (abs === null) return "escapes-root";
|
|
3672
|
-
if (!await
|
|
3889
|
+
if (!await fs16.pathExists(abs)) {
|
|
3673
3890
|
try {
|
|
3674
|
-
if ((await
|
|
3891
|
+
if ((await fs16.lstat(abs)).isSymbolicLink()) return "symlink";
|
|
3675
3892
|
} catch {
|
|
3676
3893
|
return null;
|
|
3677
3894
|
}
|
|
3678
3895
|
return null;
|
|
3679
3896
|
}
|
|
3680
|
-
if ((await
|
|
3681
|
-
const [realAbs, realRoot] = await Promise.all([
|
|
3897
|
+
if ((await fs16.lstat(abs)).isSymbolicLink()) return "symlink";
|
|
3898
|
+
const [realAbs, realRoot] = await Promise.all([fs16.realpath(abs), fs16.realpath(root)]);
|
|
3682
3899
|
const prefix = realRoot.endsWith(path19.sep) ? realRoot : realRoot + path19.sep;
|
|
3683
3900
|
if (realAbs !== realRoot && !realAbs.startsWith(prefix)) return "symlink";
|
|
3684
3901
|
return null;
|
|
@@ -3704,7 +3921,7 @@ async function resolveExistingTrackedPaths(root, itemId, relPaths, warnFn, unsaf
|
|
|
3704
3921
|
continue;
|
|
3705
3922
|
}
|
|
3706
3923
|
const abs = path19.resolve(root, rel);
|
|
3707
|
-
if (await
|
|
3924
|
+
if (await fs16.pathExists(abs)) existing.push({ rel, abs });
|
|
3708
3925
|
}
|
|
3709
3926
|
return existing;
|
|
3710
3927
|
}
|
|
@@ -3722,11 +3939,11 @@ async function backupTrackedPaths(root, absPaths, say, prefix) {
|
|
|
3722
3939
|
const stamp = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${crypto2.randomBytes(3).toString("hex")}`;
|
|
3723
3940
|
const backupRoot = hausPath(root, "backups", dirBackupDirName(prefix, stamp));
|
|
3724
3941
|
for (const abs of absPaths) {
|
|
3725
|
-
if (!await
|
|
3942
|
+
if (!await fs16.pathExists(abs)) continue;
|
|
3726
3943
|
const rel = path19.relative(root, abs);
|
|
3727
3944
|
const backupPath = path19.join(backupRoot, rel);
|
|
3728
|
-
await
|
|
3729
|
-
await
|
|
3945
|
+
await fs16.ensureDir(path19.dirname(backupPath));
|
|
3946
|
+
await fs16.copy(abs, backupPath);
|
|
3730
3947
|
}
|
|
3731
3948
|
say(
|
|
3732
3949
|
`Backed up ${absPaths.length} path(s) to ${path19.relative(root, backupRoot)} before deleting them.`
|
|
@@ -3735,7 +3952,7 @@ async function backupTrackedPaths(root, absPaths, say, prefix) {
|
|
|
3735
3952
|
}
|
|
3736
3953
|
async function deleteTrackedPaths(root, itemId, existing, { verb, reason }, say) {
|
|
3737
3954
|
for (const { abs } of existing) {
|
|
3738
|
-
await
|
|
3955
|
+
await fs16.remove(abs);
|
|
3739
3956
|
await pruneEmptyDir(path19.dirname(abs));
|
|
3740
3957
|
say([verb, reason, displayPath(root, abs)].filter(Boolean).join(" ") + ` (${itemId})`);
|
|
3741
3958
|
}
|
|
@@ -3744,7 +3961,7 @@ async function deleteTrackedPaths(root, itemId, existing, { verb, reason }, say)
|
|
|
3744
3961
|
// src/claude/skill-dep-install.ts
|
|
3745
3962
|
init_exec();
|
|
3746
3963
|
import path20 from "node:path";
|
|
3747
|
-
import
|
|
3964
|
+
import fs17 from "fs-extra";
|
|
3748
3965
|
function skillDepInstallCommand(manager) {
|
|
3749
3966
|
switch (manager) {
|
|
3750
3967
|
case "npm":
|
|
@@ -3781,7 +3998,7 @@ function skillDepInstallCommand(manager) {
|
|
|
3781
3998
|
}
|
|
3782
3999
|
async function hasDeclaredDependencies(dir) {
|
|
3783
4000
|
try {
|
|
3784
|
-
const raw = await
|
|
4001
|
+
const raw = await fs17.readFile(path20.join(dir, "package.json"), "utf8");
|
|
3785
4002
|
const parsed = JSON.parse(raw);
|
|
3786
4003
|
if (typeof parsed !== "object" || parsed === null) return false;
|
|
3787
4004
|
const deps = parsed.dependencies;
|
|
@@ -3793,7 +4010,7 @@ async function hasDeclaredDependencies(dir) {
|
|
|
3793
4010
|
}
|
|
3794
4011
|
async function realpathOrSelf(target) {
|
|
3795
4012
|
try {
|
|
3796
|
-
return await
|
|
4013
|
+
return await fs17.realpath(target);
|
|
3797
4014
|
} catch {
|
|
3798
4015
|
return path20.resolve(target);
|
|
3799
4016
|
}
|
|
@@ -3838,7 +4055,7 @@ async function installSkillDependencies(opts, deps = {}) {
|
|
|
3838
4055
|
default:
|
|
3839
4056
|
return assertNeverCommandProbe(probe);
|
|
3840
4057
|
}
|
|
3841
|
-
if (!await
|
|
4058
|
+
if (!await fs17.pathExists(contained.dir)) {
|
|
3842
4059
|
return { status: "skipped", reason: `install directory disappeared (${contained.dir})` };
|
|
3843
4060
|
}
|
|
3844
4061
|
const run = deps.run ?? defaultRun;
|
|
@@ -3852,7 +4069,7 @@ async function installSkillDependencies(opts, deps = {}) {
|
|
|
3852
4069
|
// src/claude/verify-hooks-contract.ts
|
|
3853
4070
|
init_fs();
|
|
3854
4071
|
init_paths();
|
|
3855
|
-
import
|
|
4072
|
+
import fs18 from "fs-extra";
|
|
3856
4073
|
|
|
3857
4074
|
// src/claude/load-hooks.ts
|
|
3858
4075
|
var CANONICAL_HOOKS = {
|
|
@@ -3897,8 +4114,14 @@ var CANONICAL_HOOKS = {
|
|
|
3897
4114
|
]
|
|
3898
4115
|
}
|
|
3899
4116
|
};
|
|
3900
|
-
async function loadClaudeHooksSettings() {
|
|
3901
|
-
|
|
4117
|
+
async function loadClaudeHooksSettings(profile = "production") {
|
|
4118
|
+
const dropped = new Set(PROFILES[profile].droppedHookCommands);
|
|
4119
|
+
const hooks = {};
|
|
4120
|
+
for (const [event, blocks] of Object.entries(CANONICAL_HOOKS.hooks)) {
|
|
4121
|
+
const kept = (blocks ?? []).filter((block) => !block.hooks.some((h) => dropped.has(h.command)));
|
|
4122
|
+
if (kept.length > 0) hooks[event] = kept;
|
|
4123
|
+
}
|
|
4124
|
+
return { hooks, permissions: { deny: buildDenyRules(), ask: buildAskRules() } };
|
|
3902
4125
|
}
|
|
3903
4126
|
|
|
3904
4127
|
// src/claude/verify-hooks-contract.ts
|
|
@@ -3934,8 +4157,8 @@ function hausHookContractSatisfied(project, canonical) {
|
|
|
3934
4157
|
if (!coversRules(project.permissions?.ask, canonical.permissions?.ask)) return false;
|
|
3935
4158
|
return true;
|
|
3936
4159
|
}
|
|
3937
|
-
async function assertPostApplySettingsHausContract(root) {
|
|
3938
|
-
const canonical = await loadClaudeHooksSettings();
|
|
4160
|
+
async function assertPostApplySettingsHausContract(root, profile) {
|
|
4161
|
+
const canonical = await loadClaudeHooksSettings(profile ?? await readProjectProfile(root));
|
|
3939
4162
|
const written = await readJson(claudePath(root, "settings.json"));
|
|
3940
4163
|
if (written == null || typeof written !== "object") {
|
|
3941
4164
|
throw new Error(
|
|
@@ -3950,7 +4173,7 @@ async function assertPostApplySettingsHausContract(root) {
|
|
|
3950
4173
|
}
|
|
3951
4174
|
async function verifyProjectSettingsHooksContract(root) {
|
|
3952
4175
|
const settingsPath = claudePath(root, "settings.json");
|
|
3953
|
-
if (!await
|
|
4176
|
+
if (!await fs18.pathExists(settingsPath)) {
|
|
3954
4177
|
return {
|
|
3955
4178
|
ok: true,
|
|
3956
4179
|
skipped: true,
|
|
@@ -3959,7 +4182,7 @@ async function verifyProjectSettingsHooksContract(root) {
|
|
|
3959
4182
|
}
|
|
3960
4183
|
let canonical;
|
|
3961
4184
|
try {
|
|
3962
|
-
canonical = await loadClaudeHooksSettings();
|
|
4185
|
+
canonical = await loadClaudeHooksSettings(await readProjectProfile(root));
|
|
3963
4186
|
} catch (err) {
|
|
3964
4187
|
return { ok: false, message: `Cannot load canonical hooks: ${String(err)}` };
|
|
3965
4188
|
}
|
|
@@ -3979,7 +4202,7 @@ async function verifyProjectSettingsHooksContract(root) {
|
|
|
3979
4202
|
// src/claude/write-ci-gate-workflow.ts
|
|
3980
4203
|
init_logger();
|
|
3981
4204
|
import path21 from "node:path";
|
|
3982
|
-
import
|
|
4205
|
+
import fs19 from "fs-extra";
|
|
3983
4206
|
import YAML from "yaml";
|
|
3984
4207
|
var CI_GATE_WORKFLOW_REL = path21.join(".github", "workflows", "haus-gate.yml");
|
|
3985
4208
|
var WORKFLOW = `# Written by \`haus init\`. Yours to edit \u2014 haus will not rewrite this file.
|
|
@@ -4020,16 +4243,20 @@ jobs:
|
|
|
4020
4243
|
- name: haus ci-gate
|
|
4021
4244
|
run: haus ci-gate
|
|
4022
4245
|
`;
|
|
4246
|
+
var CI_GATE_MARKER_LINE = WORKFLOW.split("\n", 1)[0];
|
|
4247
|
+
function isHausWrittenCiGate(content2) {
|
|
4248
|
+
return content2.startsWith(CI_GATE_MARKER_LINE);
|
|
4249
|
+
}
|
|
4023
4250
|
async function writeCiGateWorkflow(root, dryRun, quiet = false) {
|
|
4024
4251
|
const dest = path21.join(root, CI_GATE_WORKFLOW_REL);
|
|
4025
|
-
if (await
|
|
4252
|
+
if (await fs19.pathExists(dest)) return void 0;
|
|
4026
4253
|
if (await projectRunsCiGate(root)) return void 0;
|
|
4027
4254
|
if (dryRun) {
|
|
4028
4255
|
if (!quiet) log(`Would write ${CI_GATE_WORKFLOW_REL} (CI gate: haus ci-gate on every PR)`);
|
|
4029
4256
|
return dest;
|
|
4030
4257
|
}
|
|
4031
|
-
await
|
|
4032
|
-
await
|
|
4258
|
+
await fs19.ensureDir(path21.dirname(dest));
|
|
4259
|
+
await fs19.writeFile(dest, WORKFLOW, "utf8");
|
|
4033
4260
|
if (!quiet) log(`Wrote ${CI_GATE_WORKFLOW_REL} \u2014 runs \`haus ci-gate\` on every pull request`);
|
|
4034
4261
|
return dest;
|
|
4035
4262
|
}
|
|
@@ -4051,14 +4278,14 @@ async function projectRunsCiGate(root) {
|
|
|
4051
4278
|
const dir = path21.join(root, ".github", "workflows");
|
|
4052
4279
|
let entries;
|
|
4053
4280
|
try {
|
|
4054
|
-
entries = await
|
|
4281
|
+
entries = await fs19.readdir(dir);
|
|
4055
4282
|
} catch {
|
|
4056
4283
|
return false;
|
|
4057
4284
|
}
|
|
4058
4285
|
for (const entry of entries) {
|
|
4059
4286
|
if (!/\.ya?ml$/i.test(entry)) continue;
|
|
4060
4287
|
try {
|
|
4061
|
-
const text = await
|
|
4288
|
+
const text = await fs19.readFile(path21.join(dir, entry), "utf8");
|
|
4062
4289
|
if (runScripts(YAML.parse(text)).some((script) => CI_GATE_COMMAND.test(script))) return true;
|
|
4063
4290
|
} catch {
|
|
4064
4291
|
}
|
|
@@ -4068,7 +4295,7 @@ async function projectRunsCiGate(root) {
|
|
|
4068
4295
|
|
|
4069
4296
|
// src/claude/write-decisions-seed.ts
|
|
4070
4297
|
import path22 from "node:path";
|
|
4071
|
-
import
|
|
4298
|
+
import fs20 from "fs-extra";
|
|
4072
4299
|
|
|
4073
4300
|
// library/catalog/decisions-triggers.json
|
|
4074
4301
|
var decisions_triggers_default = {
|
|
@@ -4113,15 +4340,15 @@ Why we chose X \u2014 short, dated, write-once decision log for humans and agent
|
|
|
4113
4340
|
async function writeDecisionsSeed(root, dryRun, quiet = false) {
|
|
4114
4341
|
const decisionsDir = path22.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
4115
4342
|
const readmePath = path22.join(decisionsDir, "README.md");
|
|
4116
|
-
if (await
|
|
4117
|
-
if (!dryRun) await
|
|
4343
|
+
if (await fs20.pathExists(readmePath)) return null;
|
|
4344
|
+
if (!dryRun) await fs20.ensureDir(decisionsDir);
|
|
4118
4345
|
await writeManagedText(root, readmePath, SEED_README, dryRun, quiet);
|
|
4119
4346
|
return readmePath;
|
|
4120
4347
|
}
|
|
4121
4348
|
|
|
4122
4349
|
// src/claude/write-prettierignore.ts
|
|
4123
4350
|
import path23 from "node:path";
|
|
4124
|
-
import
|
|
4351
|
+
import fs21 from "fs-extra";
|
|
4125
4352
|
var PRETTIERIGNORE_BEGIN = "# HAUS:BEGIN haus-managed v=1";
|
|
4126
4353
|
var PRETTIERIGNORE_END = "# HAUS:END haus-managed";
|
|
4127
4354
|
var IGNORED_PATHS = [".haus-workflow/", ".claude/"];
|
|
@@ -4190,7 +4417,7 @@ ${block}
|
|
|
4190
4417
|
}
|
|
4191
4418
|
async function writePrettierIgnore(root, dryRun, quiet = false) {
|
|
4192
4419
|
const filePath = path23.join(root, ".prettierignore");
|
|
4193
|
-
const prev = await
|
|
4420
|
+
const prev = await fs21.pathExists(filePath) ? await fs21.readFile(filePath, "utf8") : "";
|
|
4194
4421
|
const next = injectPrettierIgnoreBlock(prev, buildPrettierIgnoreBlock());
|
|
4195
4422
|
await writeManagedText(root, filePath, next, dryRun, quiet);
|
|
4196
4423
|
return filePath;
|
|
@@ -4198,19 +4425,21 @@ async function writePrettierIgnore(root, dryRun, quiet = false) {
|
|
|
4198
4425
|
|
|
4199
4426
|
// src/claude/write-root-claude-md.ts
|
|
4200
4427
|
import path24 from "node:path";
|
|
4201
|
-
import
|
|
4428
|
+
import fs22 from "fs-extra";
|
|
4202
4429
|
var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
|
|
4203
4430
|
var BLOCK_END = "<!-- HAUS:END haus-imports -->";
|
|
4204
4431
|
var WORKSPACE_BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-workspace-imports v=1 -->";
|
|
4205
4432
|
var WORKSPACE_BLOCK_END = "<!-- HAUS:END haus-workspace-imports -->";
|
|
4206
4433
|
var DEFAULT_SENTINELS = { begin: BLOCK_BEGIN, end: BLOCK_END };
|
|
4207
|
-
|
|
4208
|
-
@.haus-workflow/workflow-config.md
|
|
4209
|
-
@docs/decisions/README.md
|
|
4210
|
-
|
|
4434
|
+
function importContentFor(profile) {
|
|
4435
|
+
const lines = ["@.haus-workflow/WORKFLOW.md", "@.haus-workflow/workflow-config.md"];
|
|
4436
|
+
if (PROFILES[profile].writesDecisionsSeed) lines.push("@docs/decisions/README.md");
|
|
4437
|
+
return lines.join("\n");
|
|
4438
|
+
}
|
|
4439
|
+
function buildImportBlock(profile) {
|
|
4211
4440
|
return `${BLOCK_BEGIN}
|
|
4212
4441
|
|
|
4213
|
-
${
|
|
4442
|
+
${importContentFor(profile)}
|
|
4214
4443
|
${BLOCK_END}`;
|
|
4215
4444
|
}
|
|
4216
4445
|
function findLineMarker2(content2, marker, from = 0) {
|
|
@@ -4275,10 +4504,10 @@ ${block}
|
|
|
4275
4504
|
${block}
|
|
4276
4505
|
`;
|
|
4277
4506
|
}
|
|
4278
|
-
async function writeRootClaudeMd(root, dryRun, quiet = false) {
|
|
4507
|
+
async function writeRootClaudeMd(root, dryRun, profile, quiet = false) {
|
|
4279
4508
|
const filePath = path24.join(root, "CLAUDE.md");
|
|
4280
|
-
const block = buildImportBlock();
|
|
4281
|
-
const prev = await
|
|
4509
|
+
const block = buildImportBlock(profile);
|
|
4510
|
+
const prev = await fs22.pathExists(filePath) ? await fs22.readFile(filePath, "utf8") : "";
|
|
4282
4511
|
const next = injectHausBlock(prev, block);
|
|
4283
4512
|
await writeManagedText(root, filePath, next, dryRun, quiet);
|
|
4284
4513
|
return filePath;
|
|
@@ -4289,12 +4518,12 @@ init_fs();
|
|
|
4289
4518
|
init_logger();
|
|
4290
4519
|
init_paths();
|
|
4291
4520
|
import path26 from "node:path";
|
|
4292
|
-
import
|
|
4521
|
+
import fs24 from "fs-extra";
|
|
4293
4522
|
|
|
4294
4523
|
// src/claude/derive-workflow-config.ts
|
|
4295
4524
|
init_fs();
|
|
4296
4525
|
import path25 from "node:path";
|
|
4297
|
-
import
|
|
4526
|
+
import fs23 from "fs-extra";
|
|
4298
4527
|
function binCmd(pm, bin, args) {
|
|
4299
4528
|
const tail = args ? ` ${args}` : "";
|
|
4300
4529
|
if (pm === "yarn") return `yarn ${bin}${tail}`;
|
|
@@ -4313,22 +4542,25 @@ async function deriveWorkflowConfig(root, ctx) {
|
|
|
4313
4542
|
return null;
|
|
4314
4543
|
};
|
|
4315
4544
|
const hasDep = (name) => deps.has(name);
|
|
4316
|
-
const exists2 = (rel) =>
|
|
4545
|
+
const exists2 = (rel) => fs23.pathExistsSync(path25.join(root, rel));
|
|
4317
4546
|
const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
|
|
4318
4547
|
const hasCypress = hasDep("cypress");
|
|
4319
4548
|
const preCommitTool = exists2("lefthook.yml") || exists2("lefthook.yaml") ? "lefthook" : exists2(".husky") || hasDep("husky") || (scripts.prepare ?? "").includes("husky") ? "husky" : exists2(".pre-commit-config.yaml") ? "pre-commit (Python framework)" : null;
|
|
4320
4549
|
return {
|
|
4321
4550
|
test: script("test") ?? `${pm} test`,
|
|
4322
4551
|
testE2E: firstScript("test:e2e", "e2e", "test:integration") ?? (hasPlaywright ? binCmd(pm, "playwright", "test") : null) ?? (hasCypress ? binCmd(pm, "cypress", "run") : null),
|
|
4552
|
+
manus: script("manus") ?? binCmd(pm, "playwright", "test demo/manus.spec.ts"),
|
|
4553
|
+
dev: script("dev"),
|
|
4323
4554
|
preCommitTool,
|
|
4324
4555
|
specPath: exists2("docs/SPEC.md") ? "docs/SPEC.md" : null,
|
|
4325
4556
|
designPath: exists2("docs/DESIGN.md") ? "docs/DESIGN.md" : null,
|
|
4326
|
-
uxPath: exists2("docs/UX.md") ? "docs/UX.md" : null
|
|
4557
|
+
uxPath: exists2("docs/UX.md") ? "docs/UX.md" : null,
|
|
4558
|
+
packageManager: pm
|
|
4327
4559
|
};
|
|
4328
4560
|
}
|
|
4329
4561
|
|
|
4330
4562
|
// src/claude/write-workflow-config.ts
|
|
4331
|
-
function
|
|
4563
|
+
function productionFields(v) {
|
|
4332
4564
|
return [
|
|
4333
4565
|
{ prefix: "- Spec: ", value: v.specPath, hint: "path, e.g. docs/SPEC.md" },
|
|
4334
4566
|
{ prefix: "- Design: ", value: v.designPath, hint: "path, e.g. docs/DESIGN.md" },
|
|
@@ -4343,6 +4575,20 @@ function fields(v) {
|
|
|
4343
4575
|
{ prefix: "- Tool: ", value: v.preCommitTool, hint: "e.g. lefthook, husky" }
|
|
4344
4576
|
];
|
|
4345
4577
|
}
|
|
4578
|
+
function pocFields(v) {
|
|
4579
|
+
return [
|
|
4580
|
+
{ prefix: "- Run the manus: ", value: v.manus, hint: "command", code: true },
|
|
4581
|
+
{
|
|
4582
|
+
prefix: "- Dev server: ",
|
|
4583
|
+
value: v.dev,
|
|
4584
|
+
hint: `command, e.g. ${v.packageManager} run dev`,
|
|
4585
|
+
code: true
|
|
4586
|
+
}
|
|
4587
|
+
];
|
|
4588
|
+
}
|
|
4589
|
+
function fieldsFor(profile, v) {
|
|
4590
|
+
return PROFILES[profile].workflowConfig === "poc" ? pocFields(v) : productionFields(v);
|
|
4591
|
+
}
|
|
4346
4592
|
function renderValue(f) {
|
|
4347
4593
|
if (f.value === null) return `<!-- fill in ${f.hint} -->`;
|
|
4348
4594
|
return f.code ? `\`${f.value}\`` : f.value;
|
|
@@ -4351,12 +4597,17 @@ function line(f) {
|
|
|
4351
4597
|
return `${f.prefix}${renderValue(f)}`;
|
|
4352
4598
|
}
|
|
4353
4599
|
function buildWorkflowConfig(v) {
|
|
4354
|
-
const f =
|
|
4600
|
+
const f = productionFields(v);
|
|
4355
4601
|
const byPrefix = (p) => line(f.find((x) => x.prefix === p));
|
|
4356
4602
|
return "# How this project works (workflow methodology bindings)\n\n> The few project-specific values the workflow standard (WORKFLOW.md) binds to:\n> where the source-of-truth docs live, the test commands the TDD/verification gate\n> runs, the highest-stakes logic, and the pre-commit tool. This file is yours to\n> edit and haus will not overwrite it.\n>\n> Everyday commands (dev, build, lint, typecheck, format) and project documentation\n> live in `CLAUDE.md` + `docs/` \u2014 run **`/docs`** to generate/refresh them.\n\n## Source-of-truth documents\n" + byPrefix("- Spec: ") + "\n" + byPrefix("- Design: ") + "\n" + byPrefix("- UX flows: ") + "\n\n## Test commands (TDD / verification gate)\n" + byPrefix("- Test (unit + integration): ") + "\n" + byPrefix("- Test (E2E): ") + "\n\n## Highest-stakes logic\n<!-- fill in domain areas requiring TDD-only treatment, e.g. payment flows, auth, medical data -->\n\n## Pre-commit tool\n" + byPrefix("- Tool: ") + "\n";
|
|
4357
4603
|
}
|
|
4358
|
-
function
|
|
4359
|
-
const f =
|
|
4604
|
+
function buildPocWorkflowConfig(v) {
|
|
4605
|
+
const f = pocFields(v);
|
|
4606
|
+
const byPrefix = (p) => line(f.find((x) => x.prefix === p));
|
|
4607
|
+
return "# How this PoC works (workflow methodology bindings)\n\n> The few project-specific values the PoC standard (WORKFLOW.md) binds to: where the\n> manus lives and the two commands the verification gate runs. This file is yours to\n> edit and haus will not overwrite it. A PoC has no test suite, no CI gate and no\n> pre-commit hooks by design (haus-workflow ADR-0125).\n\n## Source-of-truth documents\n- Manus: docs/MANUS.md\n- Plans: docs/plans/<round>.md\n\n## Demo commands (the verification gate)\n" + byPrefix("- Run the manus: ") + "\n" + byPrefix("- Dev server: ") + "\n\n## Highest-stakes logic\nn/a. A PoC has none by definition; the manus is the only gate.\n\n## Pre-commit tool\nn/a. No hooks in a PoC.\n";
|
|
4608
|
+
}
|
|
4609
|
+
function refillContent(existing, v, profile) {
|
|
4610
|
+
const f = fieldsFor(profile, v);
|
|
4360
4611
|
return existing.split("\n").map((ln) => {
|
|
4361
4612
|
const field = f.find((x) => ln.startsWith(x.prefix));
|
|
4362
4613
|
if (!field || field.value === null) return ln;
|
|
@@ -4380,9 +4631,10 @@ var FALLBACK_CONTEXT = {
|
|
|
4380
4631
|
async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
4381
4632
|
const say = opts.quiet ? () => {
|
|
4382
4633
|
} : log;
|
|
4634
|
+
const profile = opts.profile ?? "production";
|
|
4383
4635
|
const destPath = hausPath(root, "workflow-config.md");
|
|
4384
4636
|
const printable = displayPath(root, destPath);
|
|
4385
|
-
const exists2 = await
|
|
4637
|
+
const exists2 = await fs24.pathExists(destPath);
|
|
4386
4638
|
if (exists2 && !opts.refill) {
|
|
4387
4639
|
if (dryRun) say(printable + ": exists (project-owned, skipping)");
|
|
4388
4640
|
return null;
|
|
@@ -4394,8 +4646,8 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
4394
4646
|
};
|
|
4395
4647
|
const values = await deriveWorkflowConfig(root, ctx);
|
|
4396
4648
|
if (exists2) {
|
|
4397
|
-
const current = await
|
|
4398
|
-
const refilled = refillContent(current, values);
|
|
4649
|
+
const current = await fs24.readFile(destPath, "utf8");
|
|
4650
|
+
const refilled = refillContent(current, values, profile);
|
|
4399
4651
|
if (refilled === current) {
|
|
4400
4652
|
if (dryRun) say(printable + ": no blank fields to refill");
|
|
4401
4653
|
return null;
|
|
@@ -4411,13 +4663,16 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
4411
4663
|
say(printable + ": would create");
|
|
4412
4664
|
return destPath;
|
|
4413
4665
|
}
|
|
4414
|
-
await writeText(
|
|
4666
|
+
await writeText(
|
|
4667
|
+
destPath,
|
|
4668
|
+
PROFILES[profile].workflowConfig === "poc" ? buildPocWorkflowConfig(values) : buildWorkflowConfig(values)
|
|
4669
|
+
);
|
|
4415
4670
|
return destPath;
|
|
4416
4671
|
}
|
|
4417
4672
|
|
|
4418
4673
|
// src/claude/write-workflow.ts
|
|
4419
4674
|
init_offline();
|
|
4420
|
-
import
|
|
4675
|
+
import fs25 from "fs-extra";
|
|
4421
4676
|
init_fs();
|
|
4422
4677
|
init_logger();
|
|
4423
4678
|
init_paths();
|
|
@@ -4443,10 +4698,10 @@ function makeWorkflowHeader(pkgVersion, contentHash) {
|
|
|
4443
4698
|
const hashAttr = contentHash === void 0 ? "" : ` hash=${contentHash}`;
|
|
4444
4699
|
return `<!-- HAUS-MANAGED id=${STABLE_ID} v=${SCHEMA_VERSION} source=@haus-tech/haus-workflow@${pkgVersion}${hashAttr} -->`;
|
|
4445
4700
|
}
|
|
4446
|
-
async function writeWorkflow(root, pkgVersion, dryRun, force = false, quiet = false) {
|
|
4701
|
+
async function writeWorkflow(root, pkgVersion, dryRun, force = false, quiet = false, templateRel = WORKFLOW_TEMPLATE_REL) {
|
|
4447
4702
|
const say = quiet ? () => {
|
|
4448
4703
|
} : log;
|
|
4449
|
-
const templateContent = await readWorkflowTemplate({ dryRun, projectRoot: root });
|
|
4704
|
+
const templateContent = await readWorkflowTemplate({ dryRun, projectRoot: root, templateRel });
|
|
4450
4705
|
if (templateContent === null) {
|
|
4451
4706
|
warn(
|
|
4452
4707
|
isOffline() ? `Workflow template is not in the local catalog cache and this run is offline \u2014 run \`haus catalog bundle import <file>\` (or drop \`--offline\`), then re-run \`haus apply --write\`` : `Workflow template could not be fetched from the catalog \u2014 check your network, then re-run \`haus apply --write\` (or \`haus update\`)`
|
|
@@ -4459,8 +4714,8 @@ async function writeWorkflow(root, pkgVersion, dryRun, force = false, quiet = fa
|
|
|
4459
4714
|
${templateContent}`;
|
|
4460
4715
|
const destPath = hausPath(root, "WORKFLOW.md");
|
|
4461
4716
|
const printable = displayPath(root, destPath);
|
|
4462
|
-
if (await
|
|
4463
|
-
const existing = await
|
|
4717
|
+
if (await fs25.pathExists(destPath)) {
|
|
4718
|
+
const existing = await fs25.readFile(destPath, "utf8");
|
|
4464
4719
|
const firstLine = existing.split("\n")[0] ?? "";
|
|
4465
4720
|
const parsed = parseHausManagedHeader(firstLine);
|
|
4466
4721
|
if (!parsed) {
|
|
@@ -4504,7 +4759,7 @@ ${existingContent}`;
|
|
|
4504
4759
|
}
|
|
4505
4760
|
}
|
|
4506
4761
|
if (dryRun) {
|
|
4507
|
-
const prev = await
|
|
4762
|
+
const prev = await fs25.pathExists(destPath) ? await fs25.readFile(destPath, "utf8") : "";
|
|
4508
4763
|
if (!prev) {
|
|
4509
4764
|
say(createUnifiedDiff(printable, "", next));
|
|
4510
4765
|
} else {
|
|
@@ -4559,14 +4814,14 @@ function decodeIfText(buf) {
|
|
|
4559
4814
|
return Buffer.from(asText, "utf8").equals(buf) ? asText : null;
|
|
4560
4815
|
}
|
|
4561
4816
|
async function listFilesRecursive2(dir) {
|
|
4562
|
-
if (!await
|
|
4817
|
+
if (!await fs26.pathExists(dir)) return [];
|
|
4563
4818
|
const files = [];
|
|
4564
4819
|
const walk = async (current, relPrefix) => {
|
|
4565
|
-
const names = await
|
|
4820
|
+
const names = await fs26.readdir(current);
|
|
4566
4821
|
for (const name of names) {
|
|
4567
4822
|
const abs = path27.join(current, name);
|
|
4568
4823
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
4569
|
-
const stat = await
|
|
4824
|
+
const stat = await fs26.lstat(abs);
|
|
4570
4825
|
if (stat.isSymbolicLink()) {
|
|
4571
4826
|
warn(`Skipping symlink in dry-run diff preview: ${rel}`);
|
|
4572
4827
|
continue;
|
|
@@ -4583,14 +4838,14 @@ async function listFilesRecursive2(dir) {
|
|
|
4583
4838
|
}
|
|
4584
4839
|
async function logFileDryRunDiff(root, srcFile, destFile, itemId, say, transformed) {
|
|
4585
4840
|
const printable = displayPath(root, destFile);
|
|
4586
|
-
const nextBuf = transformed ?? await
|
|
4587
|
-
if (!await
|
|
4841
|
+
const nextBuf = transformed ?? await fs26.readFile(srcFile);
|
|
4842
|
+
if (!await fs26.pathExists(destFile)) {
|
|
4588
4843
|
say(`${printable}: would create (${itemId})`);
|
|
4589
4844
|
const nextText2 = decodeIfText(nextBuf);
|
|
4590
4845
|
if (nextText2 !== null) say(createUnifiedDiff(printable, "", nextText2));
|
|
4591
4846
|
return;
|
|
4592
4847
|
}
|
|
4593
|
-
const prevBuf = await
|
|
4848
|
+
const prevBuf = await fs26.readFile(destFile);
|
|
4594
4849
|
if (prevBuf.equals(nextBuf)) {
|
|
4595
4850
|
say(`${printable}: unchanged (${itemId})`);
|
|
4596
4851
|
return;
|
|
@@ -4619,15 +4874,15 @@ async function logSkillDryRunDiff(root, sourcePath, destination, itemId, say, in
|
|
|
4619
4874
|
}
|
|
4620
4875
|
async function removeLegacyManagedStub(root, relPathSegments, stub, dryRun, say) {
|
|
4621
4876
|
const target = claudePath(root, ...relPathSegments);
|
|
4622
|
-
if (!await
|
|
4623
|
-
const content2 = await
|
|
4877
|
+
if (!await fs26.pathExists(target)) return;
|
|
4878
|
+
const content2 = await fs26.readFile(target, "utf8");
|
|
4624
4879
|
if (content2 !== stub && content2 !== `${stub}
|
|
4625
4880
|
` && content2 !== `${stub}\r
|
|
4626
4881
|
`) return;
|
|
4627
4882
|
if (dryRun) {
|
|
4628
4883
|
say(`[dry-run] would remove stale ${displayPath(root, target)}`);
|
|
4629
4884
|
} else {
|
|
4630
|
-
await
|
|
4885
|
+
await fs26.remove(target);
|
|
4631
4886
|
}
|
|
4632
4887
|
}
|
|
4633
4888
|
async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
@@ -4646,17 +4901,30 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4646
4901
|
};
|
|
4647
4902
|
const pkgRoot = packageRoot();
|
|
4648
4903
|
const hausVersion2 = (await readJson(path27.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
|
|
4649
|
-
const { items: manifestItems, contentRoot } = await loadCatalogContext(root);
|
|
4904
|
+
const { items: manifestItems, contentRoot, ...catalogMeta } = await loadCatalogContext(root);
|
|
4905
|
+
const profile = opts.profile ?? await readProjectProfile(root);
|
|
4906
|
+
const profileSpec = PROFILES[profile];
|
|
4907
|
+
if (parseProfileRules(catalogMeta.profiles, profile) === null) {
|
|
4908
|
+
throw new ProfileUnsupportedError(profile, describeCatalogTier(catalogMeta));
|
|
4909
|
+
}
|
|
4650
4910
|
const coreFiles = [claudePath(root, "settings.json"), claudePath(root, "rules", "haus.md")];
|
|
4651
|
-
const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun, opts.quiet);
|
|
4652
|
-
const decisionsSeedPath = await writeDecisionsSeed(root, dryRun, opts.quiet);
|
|
4653
|
-
const workflowPath = await writeWorkflow(
|
|
4911
|
+
const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun, profile, opts.quiet);
|
|
4912
|
+
const decisionsSeedPath = profileSpec.writesDecisionsSeed ? await writeDecisionsSeed(root, dryRun, opts.quiet) : null;
|
|
4913
|
+
const workflowPath = await writeWorkflow(
|
|
4914
|
+
root,
|
|
4915
|
+
hausVersion2,
|
|
4916
|
+
dryRun,
|
|
4917
|
+
opts.force,
|
|
4918
|
+
opts.quiet,
|
|
4919
|
+
profileSpec.workflowTemplateRel
|
|
4920
|
+
);
|
|
4654
4921
|
const workflowConfigPath = await writeWorkflowConfig(root, dryRun, {
|
|
4655
4922
|
refill: opts.refillConfig,
|
|
4656
|
-
quiet: opts.quiet
|
|
4923
|
+
quiet: opts.quiet,
|
|
4924
|
+
profile
|
|
4657
4925
|
});
|
|
4658
4926
|
const prettierIgnorePath = await writePrettierIgnore(root, dryRun, opts.quiet);
|
|
4659
|
-
const ciGatePath = await writeCiGateWorkflow(root, dryRun, opts.quiet);
|
|
4927
|
+
const ciGatePath = profileSpec.writesCiGate ? await writeCiGateWorkflow(root, dryRun, opts.quiet) : null;
|
|
4660
4928
|
const p6Files = [
|
|
4661
4929
|
rootClaudeMdPath,
|
|
4662
4930
|
...decisionsSeedPath ? [decisionsSeedPath] : [],
|
|
@@ -4667,7 +4935,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4667
4935
|
];
|
|
4668
4936
|
const files = dryRun ? [...coreFiles, ...p6Files] : [...coreFiles, ...p6Files, hausPath(root, "haus.lock.json")];
|
|
4669
4937
|
if (dryRun) {
|
|
4670
|
-
const mergedSettings = await mergeProjectSettings(root);
|
|
4938
|
+
const mergedSettings = await mergeProjectSettings(root, profile);
|
|
4671
4939
|
await writeManagedJson(
|
|
4672
4940
|
root,
|
|
4673
4941
|
claudePath(root, "settings.json"),
|
|
@@ -4676,8 +4944,8 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4676
4944
|
opts.quiet
|
|
4677
4945
|
);
|
|
4678
4946
|
} else {
|
|
4679
|
-
await applyProjectSettingsMerge(root);
|
|
4680
|
-
await assertPostApplySettingsHausContract(root);
|
|
4947
|
+
await applyProjectSettingsMerge(root, profile);
|
|
4948
|
+
await assertPostApplySettingsHausContract(root, profile);
|
|
4681
4949
|
}
|
|
4682
4950
|
await removeLegacyManagedStub(
|
|
4683
4951
|
root,
|
|
@@ -4735,11 +5003,11 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4735
5003
|
];
|
|
4736
5004
|
for (const rel of LEGACY_PRUNED_ARTIFACTS) {
|
|
4737
5005
|
const legacyPath = hausPath(root, rel);
|
|
4738
|
-
if (await
|
|
5006
|
+
if (await fs26.pathExists(legacyPath)) {
|
|
4739
5007
|
if (dryRun) {
|
|
4740
5008
|
say(`[dry-run] would remove stale ${displayPath(root, legacyPath)}`);
|
|
4741
5009
|
} else {
|
|
4742
|
-
await
|
|
5010
|
+
await fs26.remove(legacyPath);
|
|
4743
5011
|
}
|
|
4744
5012
|
}
|
|
4745
5013
|
}
|
|
@@ -4857,7 +5125,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4857
5125
|
}
|
|
4858
5126
|
const sourcePath = catalogItemContentPath(contentRoot, manifestItem);
|
|
4859
5127
|
const destination = placement.destination;
|
|
4860
|
-
if (await
|
|
5128
|
+
if (await fs26.pathExists(sourcePath)) {
|
|
4861
5129
|
if (dryRun) {
|
|
4862
5130
|
if (item.type === "skill") {
|
|
4863
5131
|
await logSkillDryRunDiff(root, sourcePath, destination, item.id, say, {
|
|
@@ -4888,13 +5156,13 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4888
5156
|
)) {
|
|
4889
5157
|
continue;
|
|
4890
5158
|
} else if (item.type === "skill") {
|
|
4891
|
-
const skillFiles = (await
|
|
5159
|
+
const skillFiles = (await fs26.readdir(sourcePath, { recursive: true })).filter(
|
|
4892
5160
|
(f) => typeof f === "string" && f.endsWith(".md")
|
|
4893
5161
|
);
|
|
4894
5162
|
let skillValid = true;
|
|
4895
5163
|
for (const relFile of skillFiles) {
|
|
4896
5164
|
const mdPath = path27.join(sourcePath, relFile);
|
|
4897
|
-
const mdContent = await
|
|
5165
|
+
const mdContent = await fs26.readFile(mdPath, "utf8");
|
|
4898
5166
|
const mdValidation = validateCatalogItem(
|
|
4899
5167
|
{
|
|
4900
5168
|
id: manifestItem.id,
|
|
@@ -4928,7 +5196,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4928
5196
|
say
|
|
4929
5197
|
);
|
|
4930
5198
|
} else {
|
|
4931
|
-
const fileContent = await
|
|
5199
|
+
const fileContent = await fs26.readFile(sourcePath, "utf8");
|
|
4932
5200
|
const validation = validateCatalogItem(
|
|
4933
5201
|
{
|
|
4934
5202
|
id: manifestItem.id,
|
|
@@ -4942,8 +5210,8 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4942
5210
|
warn(`Skipping ${item.id}: pre-copy validation failed \u2014 ${validation.reason}`);
|
|
4943
5211
|
continue;
|
|
4944
5212
|
}
|
|
4945
|
-
await
|
|
4946
|
-
await
|
|
5213
|
+
await fs26.ensureDir(path27.dirname(destination));
|
|
5214
|
+
await fs26.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
4947
5215
|
}
|
|
4948
5216
|
files.push(destination);
|
|
4949
5217
|
const relPaths = [path27.relative(root, destination)];
|
|
@@ -5069,7 +5337,7 @@ async function hasProtectedUnsafePath(root, relPaths, unsafeByRel) {
|
|
|
5069
5337
|
for (const rel of relPaths) {
|
|
5070
5338
|
const unsafe = unsafeByRel.get(rel) ?? null;
|
|
5071
5339
|
if (unsafe === "symlink") return true;
|
|
5072
|
-
if (unsafe === "escapes-root" && await
|
|
5340
|
+
if (unsafe === "escapes-root" && await fs26.pathExists(path27.resolve(root, rel))) return true;
|
|
5073
5341
|
}
|
|
5074
5342
|
return false;
|
|
5075
5343
|
}
|
|
@@ -5134,7 +5402,7 @@ async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, q
|
|
|
5134
5402
|
void await backupTrackedPaths(root, allAbsPaths, say, "migrate");
|
|
5135
5403
|
for (const { existing } of toRemove) {
|
|
5136
5404
|
for (const { abs } of existing) {
|
|
5137
|
-
await
|
|
5405
|
+
await fs26.remove(abs);
|
|
5138
5406
|
await pruneEmptyDir(path27.dirname(abs));
|
|
5139
5407
|
}
|
|
5140
5408
|
}
|
|
@@ -5215,7 +5483,7 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
|
|
|
5215
5483
|
say(`[dry-run] would remove ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
5216
5484
|
continue;
|
|
5217
5485
|
}
|
|
5218
|
-
await
|
|
5486
|
+
await fs26.remove(abs);
|
|
5219
5487
|
await pruneEmptyDir(path27.dirname(abs));
|
|
5220
5488
|
say(`Removed ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
5221
5489
|
}
|
|
@@ -5271,7 +5539,7 @@ async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun,
|
|
|
5271
5539
|
// src/claude/write-gitignore.ts
|
|
5272
5540
|
init_exec();
|
|
5273
5541
|
import path28 from "node:path";
|
|
5274
|
-
import
|
|
5542
|
+
import fs27 from "fs-extra";
|
|
5275
5543
|
var GITIGNORE_BEGIN = "# HAUS:BEGIN haus-managed v=1";
|
|
5276
5544
|
var GITIGNORE_END = "# HAUS:END haus-managed";
|
|
5277
5545
|
var GITIGNORED_ARTIFACT_PATHS = [
|
|
@@ -5400,14 +5668,14 @@ function injectLinkContextGitignoreBlock(existing) {
|
|
|
5400
5668
|
}
|
|
5401
5669
|
async function writeGitignore(root, dryRun, quiet = false) {
|
|
5402
5670
|
const filePath = path28.join(root, ".gitignore");
|
|
5403
|
-
const prev = await
|
|
5671
|
+
const prev = await fs27.pathExists(filePath) ? await fs27.readFile(filePath, "utf8") : "";
|
|
5404
5672
|
const next = injectGitignoreBlock(prev);
|
|
5405
5673
|
await writeManagedText(root, filePath, next, dryRun, quiet);
|
|
5406
5674
|
return filePath;
|
|
5407
5675
|
}
|
|
5408
5676
|
async function writeLinkContextGitignore(root, dryRun, quiet = false) {
|
|
5409
5677
|
const filePath = path28.join(root, ".gitignore");
|
|
5410
|
-
const prev = await
|
|
5678
|
+
const prev = await fs27.pathExists(filePath) ? await fs27.readFile(filePath, "utf8") : "";
|
|
5411
5679
|
const next = injectLinkContextGitignoreBlock(prev);
|
|
5412
5680
|
await writeManagedText(root, filePath, next, dryRun, quiet);
|
|
5413
5681
|
return filePath;
|
|
@@ -5976,25 +6244,25 @@ async function cleanupStaleGlobalPlugins(desired, deps = {}) {
|
|
|
5976
6244
|
|
|
5977
6245
|
// src/refs/fetch-refs.ts
|
|
5978
6246
|
import path31 from "node:path";
|
|
5979
|
-
import
|
|
6247
|
+
import fs29 from "fs-extra";
|
|
5980
6248
|
|
|
5981
6249
|
// src/refs/cache-meta.ts
|
|
5982
6250
|
import path30 from "node:path";
|
|
5983
|
-
import
|
|
6251
|
+
import fs28 from "fs-extra";
|
|
5984
6252
|
var META_FILENAME = "cache-meta.json";
|
|
5985
6253
|
async function readCacheMeta(cacheDir) {
|
|
5986
6254
|
const metaPath = path30.join(cacheDir, META_FILENAME);
|
|
5987
6255
|
try {
|
|
5988
|
-
const raw = await
|
|
6256
|
+
const raw = await fs28.readFile(metaPath, "utf8");
|
|
5989
6257
|
return JSON.parse(raw);
|
|
5990
6258
|
} catch {
|
|
5991
6259
|
return {};
|
|
5992
6260
|
}
|
|
5993
6261
|
}
|
|
5994
6262
|
async function writeCacheMeta(cacheDir, meta) {
|
|
5995
|
-
await
|
|
6263
|
+
await fs28.ensureDir(cacheDir);
|
|
5996
6264
|
const metaPath = path30.join(cacheDir, META_FILENAME);
|
|
5997
|
-
await
|
|
6265
|
+
await fs28.writeFile(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf8");
|
|
5998
6266
|
}
|
|
5999
6267
|
|
|
6000
6268
|
// src/refs/fetch-refs.ts
|
|
@@ -6015,7 +6283,7 @@ async function fetchSingleRef(url, cacheDir, meta) {
|
|
|
6015
6283
|
const existing = meta[url];
|
|
6016
6284
|
const headers = {};
|
|
6017
6285
|
const safeFile = existing?.file && existing.file === path31.basename(existing.file);
|
|
6018
|
-
const cachedFileExists = safeFile && await
|
|
6286
|
+
const cachedFileExists = safeFile && await fs29.pathExists(path31.join(cacheDir, existing.file));
|
|
6019
6287
|
if (cachedFileExists) {
|
|
6020
6288
|
if (existing?.etag) headers["If-None-Match"] = existing.etag;
|
|
6021
6289
|
if (existing?.lastModified) headers["If-Modified-Since"] = existing.lastModified;
|
|
@@ -6026,8 +6294,8 @@ async function fetchSingleRef(url, cacheDir, meta) {
|
|
|
6026
6294
|
if (!res.ok) return { result: "failed" };
|
|
6027
6295
|
const text = await res.text();
|
|
6028
6296
|
const file = `${urlToSlug(url)}.md`;
|
|
6029
|
-
await
|
|
6030
|
-
await
|
|
6297
|
+
await fs29.ensureDir(cacheDir);
|
|
6298
|
+
await fs29.writeFile(path31.join(cacheDir, file), text, "utf8");
|
|
6031
6299
|
meta[url] = {
|
|
6032
6300
|
url,
|
|
6033
6301
|
etag: res.headers.get("etag") ?? void 0,
|
|
@@ -6051,7 +6319,7 @@ async function pruneOrphanedRefs(cacheDir, keepUrls) {
|
|
|
6051
6319
|
delete meta[url];
|
|
6052
6320
|
removed++;
|
|
6053
6321
|
const safeFile = typeof entry.file === "string" && entry.file === path31.basename(entry.file);
|
|
6054
|
-
if (safeFile) await
|
|
6322
|
+
if (safeFile) await fs29.remove(path31.join(cacheDir, entry.file)).catch(() => {
|
|
6055
6323
|
});
|
|
6056
6324
|
}
|
|
6057
6325
|
if (removed > 0) await writeCacheMeta(cacheDir, meta);
|
|
@@ -6199,6 +6467,12 @@ async function runApply(options) {
|
|
|
6199
6467
|
process.exitCode = 1;
|
|
6200
6468
|
return;
|
|
6201
6469
|
}
|
|
6470
|
+
const profileRefused = await profileRefusal(root);
|
|
6471
|
+
if (profileRefused) {
|
|
6472
|
+
error(`${profileRefused} Nothing was written.`);
|
|
6473
|
+
process.exitCode = 1;
|
|
6474
|
+
return;
|
|
6475
|
+
}
|
|
6202
6476
|
const rec = await readJson(hausPath(root, "recommendation.json"));
|
|
6203
6477
|
const installableRecommended = installableRecommendedItems(rec?.recommended ?? []);
|
|
6204
6478
|
const wantedIds = selectedIds ?? installableRecommended.map((item) => item.id);
|
|
@@ -6326,7 +6600,7 @@ async function runApply(options) {
|
|
|
6326
6600
|
|
|
6327
6601
|
// src/commands/backups.ts
|
|
6328
6602
|
import path32 from "node:path";
|
|
6329
|
-
import
|
|
6603
|
+
import fs30 from "fs-extra";
|
|
6330
6604
|
init_logger();
|
|
6331
6605
|
init_paths();
|
|
6332
6606
|
|
|
@@ -6393,14 +6667,14 @@ Enter a number (1-${choices.length})`, io);
|
|
|
6393
6667
|
// src/commands/backups.ts
|
|
6394
6668
|
async function listBackups(root) {
|
|
6395
6669
|
const dir = hausPath(root, "backups");
|
|
6396
|
-
if (!await
|
|
6397
|
-
const names = await
|
|
6670
|
+
if (!await fs30.pathExists(dir)) return [];
|
|
6671
|
+
const names = await fs30.readdir(dir);
|
|
6398
6672
|
const entries = [];
|
|
6399
6673
|
for (const name of names) {
|
|
6400
6674
|
const kind = classifyBackupName(name);
|
|
6401
6675
|
if (!kind) continue;
|
|
6402
6676
|
const absPath = path32.join(dir, name);
|
|
6403
|
-
const stat = await
|
|
6677
|
+
const stat = await fs30.lstat(absPath);
|
|
6404
6678
|
if (stat.isSymbolicLink()) continue;
|
|
6405
6679
|
const expectDirectory = kind !== "lock";
|
|
6406
6680
|
if (stat.isDirectory() !== expectDirectory) continue;
|
|
@@ -6445,7 +6719,7 @@ async function isUnsafeWriteTarget(root, target) {
|
|
|
6445
6719
|
current = path32.join(current, segments[i]);
|
|
6446
6720
|
const isLastSegment = i === segments.length - 1;
|
|
6447
6721
|
try {
|
|
6448
|
-
const stat = await
|
|
6722
|
+
const stat = await fs30.lstat(current);
|
|
6449
6723
|
if (isLastSegment) {
|
|
6450
6724
|
if (!stat.isFile()) return true;
|
|
6451
6725
|
} else if (!stat.isDirectory()) {
|
|
@@ -6460,7 +6734,7 @@ async function isUnsafeWriteTarget(root, target) {
|
|
|
6460
6734
|
}
|
|
6461
6735
|
async function lstatIfRegularFile(abs) {
|
|
6462
6736
|
try {
|
|
6463
|
-
const stat = await
|
|
6737
|
+
const stat = await fs30.lstat(abs);
|
|
6464
6738
|
return stat.isFile() ? stat : null;
|
|
6465
6739
|
} catch (err) {
|
|
6466
6740
|
if (err.code === "ENOENT") return null;
|
|
@@ -6475,8 +6749,8 @@ async function restoreLockBackup(root, entry, yes, json, dryRun) {
|
|
|
6475
6749
|
);
|
|
6476
6750
|
return { ok: false, restored: 0 };
|
|
6477
6751
|
}
|
|
6478
|
-
if (await
|
|
6479
|
-
const targetStat = await
|
|
6752
|
+
if (await fs30.pathExists(target)) {
|
|
6753
|
+
const targetStat = await fs30.stat(target);
|
|
6480
6754
|
if (targetStat.mtimeMs > entry.mtimeMs) {
|
|
6481
6755
|
warn(
|
|
6482
6756
|
`Current haus.lock.json (${new Date(targetStat.mtimeMs).toISOString()}) is newer than backup ${entry.id} (${new Date(entry.mtimeMs).toISOString()}) \u2014 it will be overwritten.`
|
|
@@ -6510,7 +6784,7 @@ async function restoreLockBackup(root, entry, yes, json, dryRun) {
|
|
|
6510
6784
|
error(`Refusing to restore: ${entry.id} or ${target} changed since being checked.`);
|
|
6511
6785
|
return { ok: false, restored: 0 };
|
|
6512
6786
|
}
|
|
6513
|
-
await
|
|
6787
|
+
await fs30.copy(entry.absPath, target, { overwrite: true });
|
|
6514
6788
|
if (!json) log(`Restored .haus-workflow/haus.lock.json from ${entry.id}.`);
|
|
6515
6789
|
return { ok: true, restored: 1 };
|
|
6516
6790
|
}
|
|
@@ -6518,10 +6792,10 @@ async function collectFilesRecursive(dir) {
|
|
|
6518
6792
|
const files = [];
|
|
6519
6793
|
const skippedLinks = [];
|
|
6520
6794
|
const skippedOther = [];
|
|
6521
|
-
const names = await
|
|
6795
|
+
const names = await fs30.readdir(dir);
|
|
6522
6796
|
for (const name of names) {
|
|
6523
6797
|
const abs = path32.join(dir, name);
|
|
6524
|
-
const stat = await
|
|
6798
|
+
const stat = await fs30.lstat(abs);
|
|
6525
6799
|
if (stat.isSymbolicLink()) {
|
|
6526
6800
|
skippedLinks.push(abs);
|
|
6527
6801
|
continue;
|
|
@@ -6584,8 +6858,8 @@ async function restoreDirBackup(root, entry, yes, json, dryRun) {
|
|
|
6584
6858
|
}
|
|
6585
6859
|
toRestore.push(abs);
|
|
6586
6860
|
const target = path32.join(root, rel);
|
|
6587
|
-
if (await
|
|
6588
|
-
const targetStat = await
|
|
6861
|
+
if (await fs30.pathExists(target)) {
|
|
6862
|
+
const targetStat = await fs30.stat(target);
|
|
6589
6863
|
if (targetStat.mtimeMs > backupFileStat.mtimeMs) {
|
|
6590
6864
|
stale.push({
|
|
6591
6865
|
rel,
|
|
@@ -6645,8 +6919,8 @@ async function restoreDirBackup(root, entry, yes, json, dryRun) {
|
|
|
6645
6919
|
skippedAtWrite.push(rel);
|
|
6646
6920
|
continue;
|
|
6647
6921
|
}
|
|
6648
|
-
await
|
|
6649
|
-
await
|
|
6922
|
+
await fs30.ensureDir(path32.dirname(target));
|
|
6923
|
+
await fs30.copy(abs, target, { overwrite: true });
|
|
6650
6924
|
restoredCount++;
|
|
6651
6925
|
}
|
|
6652
6926
|
if (skippedAtWrite.length > 0) {
|
|
@@ -6727,7 +7001,7 @@ Keeping ${keepCount}.`
|
|
|
6727
7001
|
const failed = [];
|
|
6728
7002
|
for (const entry of removeList) {
|
|
6729
7003
|
try {
|
|
6730
|
-
await
|
|
7004
|
+
await fs30.remove(entry.absPath);
|
|
6731
7005
|
removed.push(entry.id);
|
|
6732
7006
|
} catch (err) {
|
|
6733
7007
|
failed.push({ id: entry.id, message: err instanceof Error ? err.message : String(err) });
|
|
@@ -6841,7 +7115,7 @@ init_manifest_format();
|
|
|
6841
7115
|
init_path_safety();
|
|
6842
7116
|
import os2 from "node:os";
|
|
6843
7117
|
import path33 from "node:path";
|
|
6844
|
-
import
|
|
7118
|
+
import fs31 from "fs-extra";
|
|
6845
7119
|
import * as tar from "tar";
|
|
6846
7120
|
init_fs();
|
|
6847
7121
|
init_logger();
|
|
@@ -6879,21 +7153,21 @@ async function runBundleExport(options) {
|
|
|
6879
7153
|
}
|
|
6880
7154
|
async function writeBundleAsTar(cacheDir, meta, dest) {
|
|
6881
7155
|
const bundleMetaPath = path33.join(cacheDir, BUNDLE_META_FILE);
|
|
6882
|
-
const previousBundleMeta = await
|
|
7156
|
+
const previousBundleMeta = await fs31.pathExists(bundleMetaPath) ? await fs31.readFile(bundleMetaPath, "utf8") : null;
|
|
6883
7157
|
await writeJson(bundleMetaPath, meta);
|
|
6884
7158
|
try {
|
|
6885
|
-
await
|
|
7159
|
+
await fs31.ensureDir(path33.dirname(dest));
|
|
6886
7160
|
await tar.create({ gzip: true, cwd: cacheDir, file: dest }, ["."]);
|
|
6887
7161
|
} finally {
|
|
6888
|
-
if (previousBundleMeta === null) await
|
|
6889
|
-
else await
|
|
7162
|
+
if (previousBundleMeta === null) await fs31.remove(bundleMetaPath);
|
|
7163
|
+
else await fs31.writeFile(bundleMetaPath, previousBundleMeta, "utf8");
|
|
6890
7164
|
}
|
|
6891
7165
|
}
|
|
6892
7166
|
function isMarkdownFile(relOrAbsPath) {
|
|
6893
7167
|
return relOrAbsPath.toLowerCase().endsWith(".md");
|
|
6894
7168
|
}
|
|
6895
7169
|
async function listAllFilesUnfiltered(root, current = root) {
|
|
6896
|
-
const entries = await
|
|
7170
|
+
const entries = await fs31.readdir(current, { withFileTypes: true });
|
|
6897
7171
|
const out = [];
|
|
6898
7172
|
for (const entry of entries) {
|
|
6899
7173
|
if (entry.isSymbolicLink()) continue;
|
|
@@ -6907,16 +7181,16 @@ async function listAllFilesUnfiltered(root, current = root) {
|
|
|
6907
7181
|
return out;
|
|
6908
7182
|
}
|
|
6909
7183
|
async function validateStagedItem(item, absPath) {
|
|
6910
|
-
const stat = await
|
|
7184
|
+
const stat = await fs31.stat(absPath).catch(() => null);
|
|
6911
7185
|
if (!stat) return { ok: true };
|
|
6912
7186
|
if (stat.isFile()) {
|
|
6913
|
-
const content2 = await
|
|
7187
|
+
const content2 = await fs31.readFile(absPath, "utf8");
|
|
6914
7188
|
return validateCatalogItem(item, content2);
|
|
6915
7189
|
}
|
|
6916
7190
|
const files = await listAllFilesUnfiltered(absPath);
|
|
6917
7191
|
for (const rel of files) {
|
|
6918
7192
|
if (!isMarkdownFile(rel)) continue;
|
|
6919
|
-
const content2 = await
|
|
7193
|
+
const content2 = await fs31.readFile(path33.join(absPath, rel), "utf8");
|
|
6920
7194
|
const verdict = validateCatalogItem(item, content2);
|
|
6921
7195
|
if (!verdict.ok) return verdict;
|
|
6922
7196
|
}
|
|
@@ -6938,7 +7212,7 @@ async function validateStagedItems(staging, items) {
|
|
|
6938
7212
|
continue;
|
|
6939
7213
|
}
|
|
6940
7214
|
coveredPaths.push(item.path);
|
|
6941
|
-
if (!await
|
|
7215
|
+
if (!await fs31.pathExists(contentPath)) {
|
|
6942
7216
|
if (!isSynthetic) {
|
|
6943
7217
|
accepted.push(item.id);
|
|
6944
7218
|
contentlessIds.add(item.id);
|
|
@@ -6948,7 +7222,7 @@ async function validateStagedItems(staging, items) {
|
|
|
6948
7222
|
const verdict = await validateStagedItem(item, contentPath);
|
|
6949
7223
|
if (!verdict.ok) {
|
|
6950
7224
|
warn(`Rejected ${item.id}: ${verdict.reason}`);
|
|
6951
|
-
await
|
|
7225
|
+
await fs31.remove(contentPath);
|
|
6952
7226
|
if (!isSynthetic) rejected += 1;
|
|
6953
7227
|
continue;
|
|
6954
7228
|
}
|
|
@@ -6960,30 +7234,30 @@ async function validateStagedItems(staging, items) {
|
|
|
6960
7234
|
const covered = coveredPaths.some((p) => rel === p || rel.startsWith(`${p}/`));
|
|
6961
7235
|
if (!covered) {
|
|
6962
7236
|
warn(`Refusing staged file outside any catalog item's path: ${rel}`);
|
|
6963
|
-
await
|
|
7237
|
+
await fs31.remove(path33.join(staging, rel));
|
|
6964
7238
|
}
|
|
6965
7239
|
}
|
|
6966
7240
|
await pruneEmptyDirs(staging);
|
|
6967
7241
|
return { accepted, rejected, contentlessIds };
|
|
6968
7242
|
}
|
|
6969
7243
|
async function pruneEmptyDirs(root, dir = root) {
|
|
6970
|
-
const entries = await
|
|
7244
|
+
const entries = await fs31.readdir(dir, { withFileTypes: true });
|
|
6971
7245
|
for (const entry of entries) {
|
|
6972
7246
|
if (entry.isDirectory()) {
|
|
6973
7247
|
await pruneEmptyDirs(root, path33.join(dir, entry.name));
|
|
6974
7248
|
}
|
|
6975
7249
|
}
|
|
6976
|
-
if (dir !== root && (await
|
|
6977
|
-
await
|
|
7250
|
+
if (dir !== root && (await fs31.readdir(dir)).length === 0) {
|
|
7251
|
+
await fs31.remove(dir);
|
|
6978
7252
|
}
|
|
6979
7253
|
}
|
|
6980
7254
|
async function stripSymlinks(dir) {
|
|
6981
7255
|
const removed = [];
|
|
6982
|
-
const entries = await
|
|
7256
|
+
const entries = await fs31.readdir(dir, { withFileTypes: true });
|
|
6983
7257
|
for (const entry of entries) {
|
|
6984
7258
|
const abs = path33.join(dir, entry.name);
|
|
6985
7259
|
if (entry.isSymbolicLink()) {
|
|
6986
|
-
await
|
|
7260
|
+
await fs31.remove(abs);
|
|
6987
7261
|
removed.push(abs);
|
|
6988
7262
|
continue;
|
|
6989
7263
|
}
|
|
@@ -6996,12 +7270,12 @@ async function stripSymlinks(dir) {
|
|
|
6996
7270
|
async function runBundleImport(file, options = {}) {
|
|
6997
7271
|
const dryRun = Boolean(options.dryRun);
|
|
6998
7272
|
const archive = path33.resolve(file);
|
|
6999
|
-
if (!await
|
|
7273
|
+
if (!await fs31.pathExists(archive)) {
|
|
7000
7274
|
error(`No such bundle: ${file}`);
|
|
7001
7275
|
process.exitCode = 1;
|
|
7002
7276
|
return;
|
|
7003
7277
|
}
|
|
7004
|
-
const staging = await
|
|
7278
|
+
const staging = await fs31.mkdtemp(path33.join(os2.tmpdir(), "haus-catalog-bundle-import-"));
|
|
7005
7279
|
try {
|
|
7006
7280
|
await tar.extract({
|
|
7007
7281
|
file: archive,
|
|
@@ -7057,9 +7331,9 @@ async function runBundleImport(file, options = {}) {
|
|
|
7057
7331
|
}
|
|
7058
7332
|
return;
|
|
7059
7333
|
}
|
|
7060
|
-
await
|
|
7061
|
-
for (const entry of await
|
|
7062
|
-
await
|
|
7334
|
+
await fs31.ensureDir(getCacheDir());
|
|
7335
|
+
for (const entry of await fs31.readdir(staging)) {
|
|
7336
|
+
await fs31.copy(path33.join(staging, entry), path33.join(getCacheDir(), entry), {
|
|
7063
7337
|
overwrite: true,
|
|
7064
7338
|
errorOnExist: false
|
|
7065
7339
|
});
|
|
@@ -7076,7 +7350,7 @@ async function runBundleImport(file, options = {}) {
|
|
|
7076
7350
|
}
|
|
7077
7351
|
throw err;
|
|
7078
7352
|
} finally {
|
|
7079
|
-
await
|
|
7353
|
+
await fs31.remove(staging);
|
|
7080
7354
|
}
|
|
7081
7355
|
}
|
|
7082
7356
|
|
|
@@ -7086,7 +7360,7 @@ import fs41 from "fs-extra";
|
|
|
7086
7360
|
init_manifest_format();
|
|
7087
7361
|
|
|
7088
7362
|
// src/claude/former-install-orphans.ts
|
|
7089
|
-
import
|
|
7363
|
+
import fs32 from "fs-extra";
|
|
7090
7364
|
init_path_safety();
|
|
7091
7365
|
init_paths();
|
|
7092
7366
|
var toPosix = (value) => value.replace(/\\/g, "/");
|
|
@@ -7110,7 +7384,7 @@ async function findOrphanedFormerInstalls(root, items, lockRows) {
|
|
|
7110
7384
|
const projectRel = `.claude/${relPath}`;
|
|
7111
7385
|
if (trackedPaths.has(projectRel) || reported.has(projectRel)) continue;
|
|
7112
7386
|
const abs = safeJoin(claudePath(root), relPath);
|
|
7113
|
-
if (abs === null || !await
|
|
7387
|
+
if (abs === null || !await fs32.pathExists(abs)) continue;
|
|
7114
7388
|
reported.add(projectRel);
|
|
7115
7389
|
orphans.push({ formerId: entry.formerId, currentId: entry.currentId, relPath: projectRel });
|
|
7116
7390
|
}
|
|
@@ -7118,13 +7392,13 @@ async function findOrphanedFormerInstalls(root, items, lockRows) {
|
|
|
7118
7392
|
}
|
|
7119
7393
|
|
|
7120
7394
|
// src/claude/plugin-duplicate-skills.ts
|
|
7121
|
-
import
|
|
7395
|
+
import fs33 from "node:fs";
|
|
7122
7396
|
import path34 from "node:path";
|
|
7123
7397
|
init_paths();
|
|
7124
7398
|
function subdirectoryNames(dir) {
|
|
7125
7399
|
let entries;
|
|
7126
7400
|
try {
|
|
7127
|
-
entries =
|
|
7401
|
+
entries = fs33.readdirSync(dir, { withFileTypes: true });
|
|
7128
7402
|
} catch {
|
|
7129
7403
|
return [];
|
|
7130
7404
|
}
|
|
@@ -7133,7 +7407,7 @@ function subdirectoryNames(dir) {
|
|
|
7133
7407
|
function isDirectorySymlink(dir, entry) {
|
|
7134
7408
|
if (!entry.isSymbolicLink()) return false;
|
|
7135
7409
|
try {
|
|
7136
|
-
return
|
|
7410
|
+
return fs33.statSync(path34.join(dir, entry.name)).isDirectory();
|
|
7137
7411
|
} catch {
|
|
7138
7412
|
return false;
|
|
7139
7413
|
}
|
|
@@ -7172,82 +7446,6 @@ init_project_pin();
|
|
|
7172
7446
|
init_fs();
|
|
7173
7447
|
init_logger();
|
|
7174
7448
|
init_paths();
|
|
7175
|
-
|
|
7176
|
-
// src/claude/project-intent.ts
|
|
7177
|
-
init_project_pin();
|
|
7178
|
-
init_fs();
|
|
7179
|
-
init_logger();
|
|
7180
|
-
init_paths();
|
|
7181
|
-
import fs33 from "fs-extra";
|
|
7182
|
-
function normalizeIdList(value) {
|
|
7183
|
-
if (!Array.isArray(value)) return [];
|
|
7184
|
-
return [
|
|
7185
|
-
...new Set(
|
|
7186
|
-
value.filter((entry) => typeof entry === "string" && entry.length > 0)
|
|
7187
|
-
)
|
|
7188
|
-
];
|
|
7189
|
-
}
|
|
7190
|
-
function normalizeSignalList(value) {
|
|
7191
|
-
return [...new Set(normalizeIdList(value).map((entry) => entry.toLowerCase()))];
|
|
7192
|
-
}
|
|
7193
|
-
async function readProjectConfigObject(root) {
|
|
7194
|
-
try {
|
|
7195
|
-
const data = await readJson(hausPath(root, PROJECT_CONFIG_FILE));
|
|
7196
|
-
if (data == null || typeof data !== "object" || Array.isArray(data)) return null;
|
|
7197
|
-
return data;
|
|
7198
|
-
} catch (err) {
|
|
7199
|
-
if (err.code === "ENOENT") return null;
|
|
7200
|
-
warn(
|
|
7201
|
-
`${PROJECT_CONFIG_REL} could not be read (${err instanceof Error ? err.message : String(err)}) \u2014 treating project intent as unset`
|
|
7202
|
-
);
|
|
7203
|
-
return null;
|
|
7204
|
-
}
|
|
7205
|
-
}
|
|
7206
|
-
async function updateProjectConfig(root, mutate) {
|
|
7207
|
-
const file = hausPath(root, PROJECT_CONFIG_FILE);
|
|
7208
|
-
let existing = {};
|
|
7209
|
-
if (await fs33.pathExists(file)) {
|
|
7210
|
-
try {
|
|
7211
|
-
const parsed = JSON.parse(await fs33.readFile(file, "utf8"));
|
|
7212
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
7213
|
-
existing = { ...parsed };
|
|
7214
|
-
} else {
|
|
7215
|
-
warn(`${PROJECT_CONFIG_REL} is not a JSON object \u2014 rewriting with haus-managed keys only`);
|
|
7216
|
-
}
|
|
7217
|
-
} catch (err) {
|
|
7218
|
-
warn(
|
|
7219
|
-
`${PROJECT_CONFIG_REL} could not be parsed (${err instanceof Error ? err.message : String(err)}) \u2014 rewriting with haus-managed keys only`
|
|
7220
|
-
);
|
|
7221
|
-
}
|
|
7222
|
-
}
|
|
7223
|
-
await writeJson(file, mutate(existing));
|
|
7224
|
-
}
|
|
7225
|
-
async function readProjectIncludedIds(root) {
|
|
7226
|
-
const data = await readProjectConfigObject(root);
|
|
7227
|
-
return data == null ? [] : normalizeIdList(data.includedIds);
|
|
7228
|
-
}
|
|
7229
|
-
async function addProjectIncludedIds(root, ids) {
|
|
7230
|
-
const toAdd = normalizeIdList(ids);
|
|
7231
|
-
if (toAdd.length === 0) return;
|
|
7232
|
-
await updateProjectConfig(root, (existing) => ({
|
|
7233
|
-
...existing,
|
|
7234
|
-
includedIds: [.../* @__PURE__ */ new Set([...normalizeIdList(existing.includedIds), ...toAdd])].sort()
|
|
7235
|
-
}));
|
|
7236
|
-
}
|
|
7237
|
-
function withdrawIncludedIds(existing, ids) {
|
|
7238
|
-
const drop = new Set(ids);
|
|
7239
|
-
return normalizeIdList(existing.includedIds).filter((id) => !drop.has(id));
|
|
7240
|
-
}
|
|
7241
|
-
async function readDeclaredContext(root) {
|
|
7242
|
-
const data = await readProjectConfigObject(root);
|
|
7243
|
-
if (data == null) return { roles: [], stacks: [] };
|
|
7244
|
-
return {
|
|
7245
|
-
roles: normalizeSignalList(data.declaredRoles),
|
|
7246
|
-
stacks: normalizeSignalList(data.declaredStacks)
|
|
7247
|
-
};
|
|
7248
|
-
}
|
|
7249
|
-
|
|
7250
|
-
// src/claude/project-exclusions.ts
|
|
7251
7449
|
function normalizeExcludedIds(value) {
|
|
7252
7450
|
if (!Array.isArray(value)) return [];
|
|
7253
7451
|
return [
|
|
@@ -7568,12 +7766,12 @@ init_fs();
|
|
|
7568
7766
|
import path36 from "node:path";
|
|
7569
7767
|
var NUL = String.fromCharCode(0);
|
|
7570
7768
|
function parseNumstat(output) {
|
|
7571
|
-
const
|
|
7769
|
+
const fields = output.split(NUL);
|
|
7572
7770
|
const linesByFile = {};
|
|
7573
7771
|
let linesAdded = 0;
|
|
7574
7772
|
let linesRemoved = 0;
|
|
7575
|
-
for (let i = 0; i <
|
|
7576
|
-
const field =
|
|
7773
|
+
for (let i = 0; i < fields.length; i++) {
|
|
7774
|
+
const field = fields[i];
|
|
7577
7775
|
if (!field) continue;
|
|
7578
7776
|
const parts = field.split(" ");
|
|
7579
7777
|
if (parts.length < 3) continue;
|
|
@@ -7582,7 +7780,7 @@ function parseNumstat(output) {
|
|
|
7582
7780
|
if (Number.isNaN(added) || Number.isNaN(removed)) continue;
|
|
7583
7781
|
let file = parts[2];
|
|
7584
7782
|
if (file === "") {
|
|
7585
|
-
file =
|
|
7783
|
+
file = fields[i + 2];
|
|
7586
7784
|
i += 2;
|
|
7587
7785
|
}
|
|
7588
7786
|
if (!file) continue;
|
|
@@ -9161,6 +9359,8 @@ async function runDoctor(options) {
|
|
|
9161
9359
|
ok(`- WARN: ${describeUnknownDetection(context.unsupportedSignals ?? [], isLinkedWorktree)}`);
|
|
9162
9360
|
}
|
|
9163
9361
|
ok(`Package manager: ${context.packageManager}`);
|
|
9362
|
+
const profile = await readProjectProfile(root);
|
|
9363
|
+
ok(`Profile: ${profile}`);
|
|
9164
9364
|
if (recommendation) {
|
|
9165
9365
|
ok(`Recommended items: ${recommendation.recommended?.length ?? 0}`);
|
|
9166
9366
|
} else {
|
|
@@ -9219,7 +9419,9 @@ async function runDoctor(options) {
|
|
|
9219
9419
|
const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
|
|
9220
9420
|
const importLinePattern = /^@(\S+)\s*$/gm;
|
|
9221
9421
|
const actualTargets = [...block.matchAll(importLinePattern)].map((m) => m[1]);
|
|
9222
|
-
const expectedTargets = [...
|
|
9422
|
+
const expectedTargets = [...importContentFor(profile).matchAll(importLinePattern)].map(
|
|
9423
|
+
(m) => m[1]
|
|
9424
|
+
);
|
|
9223
9425
|
const missingTargets = expectedTargets.filter((t) => !actualTargets.includes(t));
|
|
9224
9426
|
const foreignTargets = actualTargets.filter((t) => !expectedTargets.includes(t));
|
|
9225
9427
|
if (missingTargets.length > 0 || foreignTargets.length > 0) {
|
|
@@ -9271,13 +9473,13 @@ async function runDoctor(options) {
|
|
|
9271
9473
|
ok("- .haus-workflow/WORKFLOW.md: OK (user-owned)");
|
|
9272
9474
|
} else {
|
|
9273
9475
|
const bodyContent = workflowContent.slice(firstLine.length + 1);
|
|
9274
|
-
const cachePath = path49.join(getCacheDir(),
|
|
9476
|
+
const cachePath = path49.join(getCacheDir(), PROFILES[profile].workflowTemplateRel);
|
|
9275
9477
|
const bundledPath = path49.join(
|
|
9276
9478
|
packageRoot(),
|
|
9277
9479
|
"library",
|
|
9278
9480
|
"global",
|
|
9279
9481
|
"templates",
|
|
9280
|
-
|
|
9482
|
+
path49.basename(PROFILES[profile].workflowTemplateRel)
|
|
9281
9483
|
);
|
|
9282
9484
|
const templatePath = await fs41.pathExists(cachePath) ? cachePath : bundledPath;
|
|
9283
9485
|
const templateContent = await readText(templatePath);
|
|
@@ -9364,6 +9566,16 @@ async function runDoctor(options) {
|
|
|
9364
9566
|
} else {
|
|
9365
9567
|
ok("- CI GATE: OK (a workflow runs `haus ci-gate`)");
|
|
9366
9568
|
}
|
|
9569
|
+
if (!PROFILES[profile].writesCiGate) {
|
|
9570
|
+
const ciGateContent = await readText(path49.join(root, CI_GATE_WORKFLOW_REL));
|
|
9571
|
+
if (ciGateContent !== void 0 && isHausWrittenCiGate(ciGateContent)) {
|
|
9572
|
+
suggest(
|
|
9573
|
+
`- CI GATE: ${CI_GATE_WORKFLOW_REL} is still present under the ${profile} profile, which does not write one`,
|
|
9574
|
+
`This repo is on the ${profile} profile (no CI gate by design), but ${CI_GATE_WORKFLOW_REL} is still here from an earlier setup`,
|
|
9575
|
+
`delete ${CI_GATE_WORKFLOW_REL} by hand (haus will not remove it for you)`
|
|
9576
|
+
);
|
|
9577
|
+
}
|
|
9578
|
+
}
|
|
9367
9579
|
const trackedArtifacts = await listTrackedArtifactPaths(root);
|
|
9368
9580
|
if (trackedArtifacts.length > 0) {
|
|
9369
9581
|
flag(
|
|
@@ -9404,6 +9616,18 @@ async function runDoctor(options) {
|
|
|
9404
9616
|
err.kind === "too-new" ? "npm install -g @haus-tech/haus-workflow@latest" : "fix `manifestFormat` in the catalog manifest \u2014 it must be a positive integer (upgrading haus will not help)"
|
|
9405
9617
|
);
|
|
9406
9618
|
}
|
|
9619
|
+
const profileRules = catalogCtx ? parseProfileRules(
|
|
9620
|
+
catalogCtx.profiles,
|
|
9621
|
+
profile,
|
|
9622
|
+
new Set(catalogCtx.items.map((item) => item.id))
|
|
9623
|
+
) : null;
|
|
9624
|
+
if (catalogCtx && profileRules === null) {
|
|
9625
|
+
flag(
|
|
9626
|
+
`- PROFILE: the ${describeCatalogTier(catalogCtx)} catalog defines no "${profile}" profile \u2014 \`haus apply\` refuses until it does`,
|
|
9627
|
+
`This project is set to the ${profile} profile, but the catalog it uses defines no such profile (needs haus-workflow-catalog >= 6.5.0)`,
|
|
9628
|
+
'haus update (or set "profile" back to "production" in .haus-workflow/haus.config.json)'
|
|
9629
|
+
);
|
|
9630
|
+
}
|
|
9407
9631
|
const pluginPins = catalogItems === null ? [] : await loadCatalogPluginPins(root);
|
|
9408
9632
|
const pluginState = await probePluginEnv();
|
|
9409
9633
|
const activePins = pluginPins.filter((pin) => isPluginActive(pluginState, pin));
|
|
@@ -9571,8 +9795,21 @@ async function runDoctor(options) {
|
|
|
9571
9795
|
const excludedIds = new Set(await readProjectExcludedIds(root));
|
|
9572
9796
|
const activePinIds = activePins.map((pin) => pin.id);
|
|
9573
9797
|
const lockedIds = (await readJson(hausPath(root, "haus.lock.json")) ?? []).map((entry) => entry.id).filter((id) => typeof id === "string");
|
|
9798
|
+
const catalogById = new Map(catalogItems.map((item) => [item.id, item]));
|
|
9799
|
+
const excludedByProfile = profileRules === null ? [] : lockedIds.filter((id) => {
|
|
9800
|
+
const item = catalogById.get(id);
|
|
9801
|
+
return item !== void 0 && profileVerdict(item, profileRules)?.kind === "excluded";
|
|
9802
|
+
});
|
|
9803
|
+
if (excludedByProfile.length > 0) {
|
|
9804
|
+
suggest(
|
|
9805
|
+
`- PROFILE: ${excludedByProfile.length} installed item(s) are excluded by the ${profile} profile (${excludedByProfile.join(", ")})`,
|
|
9806
|
+
`${excludedByProfile.length} installed item(s) are kept out by the ${profile} profile and will not be recommended again`,
|
|
9807
|
+
"haus apply --write --prune"
|
|
9808
|
+
);
|
|
9809
|
+
}
|
|
9810
|
+
const profileExcludedSet = new Set(excludedByProfile);
|
|
9574
9811
|
const drifted = lockedIds.filter(
|
|
9575
|
-
(id) => catalogIds.has(id) && !recommendedIds.has(id) && !excludedIds.has(id) && !isSupersededByPlugin(id, activePinIds)
|
|
9812
|
+
(id) => catalogIds.has(id) && !recommendedIds.has(id) && !excludedIds.has(id) && !profileExcludedSet.has(id) && !isSupersededByPlugin(id, activePinIds)
|
|
9576
9813
|
);
|
|
9577
9814
|
if (drifted.length > 0) {
|
|
9578
9815
|
suggest(
|
|
@@ -9881,14 +10118,14 @@ function stampFrontmatter(content2, h) {
|
|
|
9881
10118
|
}
|
|
9882
10119
|
}
|
|
9883
10120
|
if (close === -1) return content2;
|
|
9884
|
-
const
|
|
10121
|
+
const fields = [];
|
|
9885
10122
|
for (let i = 1; i < close; i++) {
|
|
9886
10123
|
if (/^haus_managed:/.test(lines[i])) continue;
|
|
9887
|
-
|
|
10124
|
+
fields.push(lines[i]);
|
|
9888
10125
|
}
|
|
9889
10126
|
const rebuilt = [
|
|
9890
10127
|
FM_FENCE,
|
|
9891
|
-
...
|
|
10128
|
+
...fields,
|
|
9892
10129
|
`${FM_KEY}: "${buildMarkerValue(h)}"`,
|
|
9893
10130
|
...lines.slice(close)
|
|
9894
10131
|
// closing fence + body
|
|
@@ -11304,6 +11541,10 @@ function normalizeRecommendation(input2) {
|
|
|
11304
11541
|
estimatedTokenReductionPct: input2.estimatedTokenReductionPct ?? tokenReductionPct(recommended.length, skipped.length)
|
|
11305
11542
|
};
|
|
11306
11543
|
}
|
|
11544
|
+
var NEVER_NEAR_MISS_GATES = /* @__PURE__ */ new Set([
|
|
11545
|
+
"profile-excluded",
|
|
11546
|
+
"workflow-template-item"
|
|
11547
|
+
]);
|
|
11307
11548
|
function buildNearMiss(skipped) {
|
|
11308
11549
|
const nearMiss = [];
|
|
11309
11550
|
for (const item of skipped) {
|
|
@@ -11311,6 +11552,7 @@ function buildNearMiss(skipped) {
|
|
|
11311
11552
|
if (!gates) continue;
|
|
11312
11553
|
const failing = gates.filter((g) => !g.passed);
|
|
11313
11554
|
if (failing.length !== 1) continue;
|
|
11555
|
+
if (NEVER_NEAR_MISS_GATES.has(failing[0].name)) continue;
|
|
11314
11556
|
nearMiss.push({
|
|
11315
11557
|
id: item.id,
|
|
11316
11558
|
missingGate: failing[0].name,
|
|
@@ -11721,6 +11963,7 @@ async function runGuard(kind, _options) {
|
|
|
11721
11963
|
}
|
|
11722
11964
|
|
|
11723
11965
|
// src/claude/setup-core.ts
|
|
11966
|
+
import path59 from "node:path";
|
|
11724
11967
|
init_github_rate_limit();
|
|
11725
11968
|
|
|
11726
11969
|
// src/recommender/recommend.ts
|
|
@@ -11770,7 +12013,17 @@ async function readChangedFiles(root) {
|
|
|
11770
12013
|
// src/recommender/recommend.ts
|
|
11771
12014
|
async function recommend(root, context, opts = {}) {
|
|
11772
12015
|
const activePluginIds = opts.activePluginIds ?? [];
|
|
11773
|
-
const
|
|
12016
|
+
const catalogCtx = await loadCatalogContext(root);
|
|
12017
|
+
const items = catalogCtx.items;
|
|
12018
|
+
const profile = opts.profile ?? await readProjectProfile(root);
|
|
12019
|
+
const profileRules = parseProfileRules(
|
|
12020
|
+
catalogCtx.profiles,
|
|
12021
|
+
profile,
|
|
12022
|
+
new Set(items.map((item) => item.id))
|
|
12023
|
+
);
|
|
12024
|
+
if (profileRules === null) {
|
|
12025
|
+
throw new ProfileUnsupportedError(profile, describeCatalogTier(catalogCtx));
|
|
12026
|
+
}
|
|
11774
12027
|
const excludedIds = await readProjectExcludedIds(root);
|
|
11775
12028
|
const sources = buildSourcesReport(items);
|
|
11776
12029
|
const deep = await readJson(hausPath(root, "deep-context.json")) ?? {};
|
|
@@ -11855,6 +12108,33 @@ async function recommend(root, context, opts = {}) {
|
|
|
11855
12108
|
"Catalog item is deprecated",
|
|
11856
12109
|
"reviewStatus:deprecated"
|
|
11857
12110
|
);
|
|
12111
|
+
const verdict = profileVerdict(item, profileRules);
|
|
12112
|
+
if (verdict?.kind === "excluded") {
|
|
12113
|
+
add(
|
|
12114
|
+
"profile-excluded",
|
|
12115
|
+
false,
|
|
12116
|
+
"profile-excluded",
|
|
12117
|
+
`Excluded by profile ${profile} (${verdict.clause})`,
|
|
12118
|
+
verdict.clause
|
|
12119
|
+
);
|
|
12120
|
+
} else if (verdict?.kind === "kept") {
|
|
12121
|
+
add(
|
|
12122
|
+
"profile-excluded",
|
|
12123
|
+
true,
|
|
12124
|
+
"profile-kept",
|
|
12125
|
+
`Kept by profile ${profile} keepIds despite ${verdict.clause}`,
|
|
12126
|
+
`keep:${item.id}`
|
|
12127
|
+
);
|
|
12128
|
+
}
|
|
12129
|
+
if (WORKFLOW_TEMPLATE_ITEM_IDS.has(item.id)) {
|
|
12130
|
+
add(
|
|
12131
|
+
"workflow-template-item",
|
|
12132
|
+
false,
|
|
12133
|
+
"workflow-template-item",
|
|
12134
|
+
"Written as WORKFLOW.md by the profile, never installed as an item",
|
|
12135
|
+
`id:${item.id}`
|
|
12136
|
+
);
|
|
12137
|
+
}
|
|
11858
12138
|
if (isItemSupersededByPlugin(item, activePluginIds)) {
|
|
11859
12139
|
add(
|
|
11860
12140
|
"superseded-by-global-plugin",
|
|
@@ -12157,7 +12437,11 @@ var HARD_SKIP_CODES = /* @__PURE__ */ new Set([
|
|
|
12157
12437
|
"curated-risk-blocked",
|
|
12158
12438
|
"sensitive-policy",
|
|
12159
12439
|
"source-trust",
|
|
12160
|
-
"source-approval"
|
|
12440
|
+
"source-approval",
|
|
12441
|
+
// A `--include` must not force a second copy of the profile's workflow standard into
|
|
12442
|
+
// `.claude/templates/` — it is written as WORKFLOW.md by the profile itself (Task 3
|
|
12443
|
+
// controller ruling, ADR-0125).
|
|
12444
|
+
"workflow-template-item"
|
|
12161
12445
|
]);
|
|
12162
12446
|
function applyManualIncludes(include, items, recommended, skipped) {
|
|
12163
12447
|
const warnings = [];
|
|
@@ -12182,6 +12466,9 @@ function applyManualIncludes(include, items, recommended, skipped) {
|
|
|
12182
12466
|
if (skipCode === "requires-any-unsatisfied") {
|
|
12183
12467
|
warnings.push(`--include: "${id}" added manually though its requiresAny gate is unsatisfied`);
|
|
12184
12468
|
}
|
|
12469
|
+
if (skipCode === "profile-excluded") {
|
|
12470
|
+
warnings.push(`--include: "${id}" added manually though the project profile excludes it`);
|
|
12471
|
+
}
|
|
12185
12472
|
const previouslySkipped = skipIdx >= 0 ? skipped[skipIdx] : void 0;
|
|
12186
12473
|
if (skipIdx >= 0) skipped.splice(skipIdx, 1);
|
|
12187
12474
|
const isConfig = item.type === "config";
|
|
@@ -12402,6 +12689,32 @@ async function runSetupCore(root, opts) {
|
|
|
12402
12689
|
const { json, apply, dryRun, quiet, confirm: confirm2, force } = opts;
|
|
12403
12690
|
const say = quiet || json ? () => {
|
|
12404
12691
|
} : log;
|
|
12692
|
+
const storedProfile = await readProjectProfile(root);
|
|
12693
|
+
if (storedProfile === "poc" && opts.profile !== "poc") {
|
|
12694
|
+
const lockedMessage = pocLockedMessage();
|
|
12695
|
+
error(lockedMessage);
|
|
12696
|
+
process.exitCode = 1;
|
|
12697
|
+
emitJsonEnvelope(json, quiet, {
|
|
12698
|
+
scan: null,
|
|
12699
|
+
recommendation: null,
|
|
12700
|
+
hooks: null,
|
|
12701
|
+
repoName: path59.basename(root),
|
|
12702
|
+
warnings: [lockedMessage]
|
|
12703
|
+
});
|
|
12704
|
+
return {
|
|
12705
|
+
root,
|
|
12706
|
+
repoName: path59.basename(root),
|
|
12707
|
+
roles: [],
|
|
12708
|
+
recommendedCount: 0,
|
|
12709
|
+
warnings: [lockedMessage],
|
|
12710
|
+
hooksOk: false,
|
|
12711
|
+
written: [],
|
|
12712
|
+
catalogInstallOk: true,
|
|
12713
|
+
setupOk: false,
|
|
12714
|
+
failureReason: "profile-locked"
|
|
12715
|
+
};
|
|
12716
|
+
}
|
|
12717
|
+
const profile = opts.profile ?? storedProfile;
|
|
12405
12718
|
const scanResult = await scanProject(root);
|
|
12406
12719
|
say("Haus scan complete");
|
|
12407
12720
|
say(`Roles: ${scanResult.repoRoles.join(", ") || "unknown"}`);
|
|
@@ -12410,10 +12723,37 @@ async function runSetupCore(root, opts) {
|
|
|
12410
12723
|
const context = await readContextOrScan(root);
|
|
12411
12724
|
const priorManualIncludes = await readPriorManualIncludes(root);
|
|
12412
12725
|
const activePluginIds = await activeGlobalPluginIds(root);
|
|
12413
|
-
|
|
12414
|
-
|
|
12415
|
-
|
|
12416
|
-
|
|
12726
|
+
let recommendation;
|
|
12727
|
+
try {
|
|
12728
|
+
recommendation = await recommend(root, context, {
|
|
12729
|
+
include: priorManualIncludes,
|
|
12730
|
+
activePluginIds,
|
|
12731
|
+
profile
|
|
12732
|
+
});
|
|
12733
|
+
} catch (err) {
|
|
12734
|
+
if (!(err instanceof ProfileUnsupportedError)) throw err;
|
|
12735
|
+
error(err.message);
|
|
12736
|
+
process.exitCode = 1;
|
|
12737
|
+
emitJsonEnvelope(json, quiet, {
|
|
12738
|
+
scan: scanResult,
|
|
12739
|
+
recommendation: null,
|
|
12740
|
+
hooks: null,
|
|
12741
|
+
repoName: context.repoName,
|
|
12742
|
+
warnings: [err.message]
|
|
12743
|
+
});
|
|
12744
|
+
return {
|
|
12745
|
+
root,
|
|
12746
|
+
repoName: context.repoName,
|
|
12747
|
+
roles: scanResult.repoRoles,
|
|
12748
|
+
recommendedCount: 0,
|
|
12749
|
+
warnings: [err.message],
|
|
12750
|
+
hooksOk: false,
|
|
12751
|
+
written: [],
|
|
12752
|
+
catalogInstallOk: true,
|
|
12753
|
+
setupOk: false,
|
|
12754
|
+
failureReason: "profile-unsupported"
|
|
12755
|
+
};
|
|
12756
|
+
}
|
|
12417
12757
|
if (recommendation.recommended.length === 0 && !force) {
|
|
12418
12758
|
const warningLines2 = [
|
|
12419
12759
|
.../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? [], ...siblingWarning])
|
|
@@ -12565,9 +12905,13 @@ async function runSetupCore(root, opts) {
|
|
|
12565
12905
|
);
|
|
12566
12906
|
}
|
|
12567
12907
|
}
|
|
12908
|
+
if (opts.profile !== void 0 && opts.profile !== storedProfile && !isPreview) {
|
|
12909
|
+
await writeProjectProfile(root, opts.profile);
|
|
12910
|
+
}
|
|
12568
12911
|
const files = await writeClaudeFiles(root, dryRun ?? false, void 0, {
|
|
12569
12912
|
quiet: opts.quiet,
|
|
12570
|
-
activePluginIds
|
|
12913
|
+
activePluginIds,
|
|
12914
|
+
profile
|
|
12571
12915
|
});
|
|
12572
12916
|
say("Applied files:");
|
|
12573
12917
|
files.forEach((f) => say(`- ${displayPath(root, f)}`));
|
|
@@ -12622,12 +12966,18 @@ async function runInit(options) {
|
|
|
12622
12966
|
process.exitCode = 1;
|
|
12623
12967
|
return;
|
|
12624
12968
|
}
|
|
12969
|
+
if (options.profile !== void 0 && !isProfileId(options.profile)) {
|
|
12970
|
+
error(`--profile must be one of ${PROFILE_IDS.join(", ")} (got "${options.profile}").`);
|
|
12971
|
+
process.exitCode = 1;
|
|
12972
|
+
return;
|
|
12973
|
+
}
|
|
12625
12974
|
await runSetupCore(root, {
|
|
12626
12975
|
json: options.json,
|
|
12627
12976
|
apply: !dryRun,
|
|
12628
12977
|
dryRun: false,
|
|
12629
12978
|
force: options.force,
|
|
12630
12979
|
allowEmptyCache: options.allowEmptyCache,
|
|
12980
|
+
profile: options.profile,
|
|
12631
12981
|
confirm: () => confirm("Approve and write Claude files now?")
|
|
12632
12982
|
});
|
|
12633
12983
|
}
|
|
@@ -12773,10 +13123,10 @@ async function runInstall(options, deps = {}) {
|
|
|
12773
13123
|
}
|
|
12774
13124
|
|
|
12775
13125
|
// src/commands/new.ts
|
|
12776
|
-
import
|
|
13126
|
+
import path61 from "node:path";
|
|
12777
13127
|
|
|
12778
13128
|
// src/new-project/create-project.ts
|
|
12779
|
-
import
|
|
13129
|
+
import path60 from "node:path";
|
|
12780
13130
|
import fs47 from "fs-extra";
|
|
12781
13131
|
init_exec();
|
|
12782
13132
|
init_logger();
|
|
@@ -12962,7 +13312,7 @@ var defaultDeps = {
|
|
|
12962
13312
|
};
|
|
12963
13313
|
var NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
12964
13314
|
async function recordPackageManager(projectPath, pm, deps) {
|
|
12965
|
-
const pkgPath =
|
|
13315
|
+
const pkgPath = path60.join(projectPath, "package.json");
|
|
12966
13316
|
let pkg;
|
|
12967
13317
|
try {
|
|
12968
13318
|
pkg = await fs47.readJson(pkgPath);
|
|
@@ -12993,9 +13343,9 @@ var CONFIG_TOOLING = {
|
|
|
12993
13343
|
}
|
|
12994
13344
|
};
|
|
12995
13345
|
async function ensureConfigTooling(projectPath, scaffolded) {
|
|
12996
|
-
const needed = scaffolded.map((file) => CONFIG_TOOLING[
|
|
13346
|
+
const needed = scaffolded.map((file) => CONFIG_TOOLING[path60.basename(file)]).filter((entry) => entry !== void 0);
|
|
12997
13347
|
if (needed.length === 0) return [];
|
|
12998
|
-
const pkgPath =
|
|
13348
|
+
const pkgPath = path60.join(projectPath, "package.json");
|
|
12999
13349
|
let pkg;
|
|
13000
13350
|
try {
|
|
13001
13351
|
pkg = await fs47.readJson(pkgPath);
|
|
@@ -13027,7 +13377,7 @@ async function ensureConfigTooling(projectPath, scaffolded) {
|
|
|
13027
13377
|
}
|
|
13028
13378
|
async function createProject(opts, deps = defaultDeps) {
|
|
13029
13379
|
const steps = [];
|
|
13030
|
-
const projectPath =
|
|
13380
|
+
const projectPath = path60.resolve(opts.parentDir, opts.name);
|
|
13031
13381
|
const installHint = `cd ${opts.name} && ${opts.pm}${opts.pm === "yarn" ? "" : " install"}`;
|
|
13032
13382
|
const fail = (step, detail) => {
|
|
13033
13383
|
steps.push({ step, status: "failed", detail });
|
|
@@ -13035,14 +13385,14 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
13035
13385
|
};
|
|
13036
13386
|
const stack = getStack(opts.stackId);
|
|
13037
13387
|
if (!stack) return fail("preflight", `Unknown stack "${opts.stackId}". Run \`haus new --list\`.`);
|
|
13038
|
-
const parentStat = await fs47.stat(
|
|
13388
|
+
const parentStat = await fs47.stat(path60.dirname(projectPath)).catch(() => null);
|
|
13039
13389
|
if (!parentStat?.isDirectory()) {
|
|
13040
13390
|
return fail(
|
|
13041
13391
|
"preflight",
|
|
13042
13392
|
`Parent directory ${opts.parentDir} is not an existing directory \u2014 pass an existing one to --dir.`
|
|
13043
13393
|
);
|
|
13044
13394
|
}
|
|
13045
|
-
if (!NAME_RE.test(opts.name) || opts.name.includes("/") || opts.name.includes("\\") ||
|
|
13395
|
+
if (!NAME_RE.test(opts.name) || opts.name.includes("/") || opts.name.includes("\\") || path60.dirname(projectPath) !== path60.resolve(opts.parentDir)) {
|
|
13046
13396
|
return fail(
|
|
13047
13397
|
"preflight",
|
|
13048
13398
|
`Invalid project name "${opts.name}" (lowercase letters, digits, . _ - only).`
|
|
@@ -13064,7 +13414,11 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
13064
13414
|
status: opts.github ? "planned" : "skipped",
|
|
13065
13415
|
detail: opts.github ? `gh repo create ${opts.org ? `${opts.org}/` : ""}${opts.name} --${opts.visibility} --source . --push` : "--no-github"
|
|
13066
13416
|
});
|
|
13067
|
-
steps.push({
|
|
13417
|
+
steps.push({
|
|
13418
|
+
step: "init",
|
|
13419
|
+
status: "planned",
|
|
13420
|
+
detail: `haus init pipeline in the new project${opts.profile ? ` (profile ${opts.profile})` : ""}`
|
|
13421
|
+
});
|
|
13068
13422
|
steps.push({ step: "overlay", status: "planned", detail: stack.overlayConfigIds.join(", ") });
|
|
13069
13423
|
steps.push({
|
|
13070
13424
|
step: "role-check",
|
|
@@ -13126,7 +13480,7 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
13126
13480
|
steps.push({ step: "scaffold", status: "done" });
|
|
13127
13481
|
phase = "git";
|
|
13128
13482
|
if (!opts.json) log("\u25B6 git: init/branch/commit");
|
|
13129
|
-
if (!await fs47.pathExists(
|
|
13483
|
+
if (!await fs47.pathExists(path60.join(projectPath, ".git"))) {
|
|
13130
13484
|
const init = await deps.git(["init"], { cwd: projectPath });
|
|
13131
13485
|
if (init.exitCode !== 0) return fail("git", `git init failed: ${init.stderr}`);
|
|
13132
13486
|
}
|
|
@@ -13167,6 +13521,7 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
13167
13521
|
phase = "init";
|
|
13168
13522
|
if (!opts.json) log("\u25B6 init: haus setup pipeline");
|
|
13169
13523
|
const exitCodeBeforeSetup = process.exitCode;
|
|
13524
|
+
const profileFlag = opts.profile ? ` --profile ${opts.profile}` : "";
|
|
13170
13525
|
let setupResult;
|
|
13171
13526
|
try {
|
|
13172
13527
|
setupResult = await deps.setup(projectPath, {
|
|
@@ -13175,13 +13530,14 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
13175
13530
|
dryRun: false,
|
|
13176
13531
|
quiet: opts.json,
|
|
13177
13532
|
confirm: opts.yes ? async () => true : () => confirm("Approve and write Claude files now?"),
|
|
13178
|
-
allowEmptyCache: opts.allowEmptyCache
|
|
13533
|
+
allowEmptyCache: opts.allowEmptyCache,
|
|
13534
|
+
profile: opts.profile
|
|
13179
13535
|
});
|
|
13180
13536
|
} catch (err) {
|
|
13181
13537
|
steps.push({
|
|
13182
13538
|
step: "init",
|
|
13183
13539
|
status: "failed",
|
|
13184
|
-
detail: `haus init threw (${err instanceof Error ? err.message : String(err)}). Project and repo are intact \u2014 cd ${opts.name} && haus init to retry.`
|
|
13540
|
+
detail: `haus init threw (${err instanceof Error ? err.message : String(err)}). Project and repo are intact \u2014 cd ${opts.name} && haus init${profileFlag} to retry.`
|
|
13185
13541
|
});
|
|
13186
13542
|
return { ok: false, projectPath, repoUrl, installHint, steps };
|
|
13187
13543
|
}
|
|
@@ -13191,13 +13547,15 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
13191
13547
|
const reason = setupResult?.failureReason ?? (catalogInstallRefused ? "catalog-install" : void 0);
|
|
13192
13548
|
switch (reason) {
|
|
13193
13549
|
case "catalog-install":
|
|
13194
|
-
return `${enter} && haus init --allow-empty-cache`;
|
|
13550
|
+
return `${enter} && haus init --allow-empty-cache${profileFlag}`;
|
|
13195
13551
|
case "zero-signal":
|
|
13196
|
-
return `${enter} && haus init --force`;
|
|
13552
|
+
return `${enter} && haus init --force${profileFlag}`;
|
|
13197
13553
|
case "manifest-format":
|
|
13198
|
-
return `npm i -g @haus-tech/haus-workflow@latest, then ${enter} && haus init`;
|
|
13554
|
+
return `npm i -g @haus-tech/haus-workflow@latest, then ${enter} && haus init${profileFlag}`;
|
|
13555
|
+
case "profile-unsupported":
|
|
13556
|
+
return `${enter} && haus update, then haus init${profileFlag}`;
|
|
13199
13557
|
default:
|
|
13200
|
-
return `${enter} && haus init`;
|
|
13558
|
+
return `${enter} && haus init${profileFlag}`;
|
|
13201
13559
|
}
|
|
13202
13560
|
})();
|
|
13203
13561
|
if (Array.isArray(setupResult?.written) && setupResult.written.length === 0 && setupResult.setupOk !== false) {
|
|
@@ -13208,7 +13566,7 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
13208
13566
|
});
|
|
13209
13567
|
return { ok: false, projectPath, repoUrl, installHint, steps };
|
|
13210
13568
|
}
|
|
13211
|
-
if (process.exitCode === 1 && exitCodeBeforeSetup !== 1) {
|
|
13569
|
+
if (process.exitCode === 1 && exitCodeBeforeSetup !== 1 || setupResult?.setupOk === false) {
|
|
13212
13570
|
if (opts.allowEmptyCache && catalogInstallRefused) {
|
|
13213
13571
|
steps.push({
|
|
13214
13572
|
step: "init",
|
|
@@ -13352,11 +13710,14 @@ function resolveNewOptions(stackArg, nameArg, flags, cwd) {
|
|
|
13352
13710
|
if (flags.dir !== void 0 && flags.dir.trim() === "") {
|
|
13353
13711
|
return { ok: false, error: "--dir needs a path (omit it to scaffold into the cwd)." };
|
|
13354
13712
|
}
|
|
13355
|
-
const parentDir = flags.dir === void 0 ? cwd :
|
|
13713
|
+
const parentDir = flags.dir === void 0 ? cwd : path61.resolve(cwd, flags.dir);
|
|
13356
13714
|
const pm = flags.pm ?? "yarn";
|
|
13357
13715
|
if (!PMS.includes(pm)) {
|
|
13358
13716
|
return { ok: false, error: `Unsupported --pm "${pm}" \u2014 use yarn, npm, or pnpm.` };
|
|
13359
13717
|
}
|
|
13718
|
+
if (flags.profile !== void 0 && !isProfileId(flags.profile)) {
|
|
13719
|
+
return { ok: false, error: `Unsupported --profile "${flags.profile}" \u2014 use production or poc.` };
|
|
13720
|
+
}
|
|
13360
13721
|
if (stackArg === void 0 || nameArg === void 0) {
|
|
13361
13722
|
return { ok: false, error: "missing-args" };
|
|
13362
13723
|
}
|
|
@@ -13373,7 +13734,8 @@ function resolveNewOptions(stackArg, nameArg, flags, cwd) {
|
|
|
13373
13734
|
yes: flags.yes === true,
|
|
13374
13735
|
dryRun,
|
|
13375
13736
|
json: flags.json === true,
|
|
13376
|
-
allowEmptyCache: flags.allowEmptyCache === true
|
|
13737
|
+
allowEmptyCache: flags.allowEmptyCache === true,
|
|
13738
|
+
profile: flags.profile
|
|
13377
13739
|
}
|
|
13378
13740
|
};
|
|
13379
13741
|
}
|
|
@@ -13431,7 +13793,15 @@ async function runRecommend(options) {
|
|
|
13431
13793
|
const include = parseIdList(options.include);
|
|
13432
13794
|
if (include.length > 0) await addProjectIncludedIds(root, include);
|
|
13433
13795
|
const activePluginIds = await activeGlobalPluginIds(root);
|
|
13434
|
-
|
|
13796
|
+
let result;
|
|
13797
|
+
try {
|
|
13798
|
+
result = await recommend(root, context, { include, activePluginIds });
|
|
13799
|
+
} catch (err) {
|
|
13800
|
+
if (!(err instanceof ProfileUnsupportedError)) throw err;
|
|
13801
|
+
error(err.message);
|
|
13802
|
+
process.exitCode = 1;
|
|
13803
|
+
return;
|
|
13804
|
+
}
|
|
13435
13805
|
await writeJson(hausPath(root, "recommendation.json"), result);
|
|
13436
13806
|
if (options.json) {
|
|
13437
13807
|
log(JSON.stringify(result, null, 2));
|
|
@@ -13450,7 +13820,7 @@ async function runRecommend(options) {
|
|
|
13450
13820
|
}
|
|
13451
13821
|
|
|
13452
13822
|
// src/commands/remove.ts
|
|
13453
|
-
import
|
|
13823
|
+
import path62 from "node:path";
|
|
13454
13824
|
init_fs();
|
|
13455
13825
|
init_logger();
|
|
13456
13826
|
init_paths();
|
|
@@ -13557,7 +13927,7 @@ async function runRemove(ids, options = {}) {
|
|
|
13557
13927
|
}
|
|
13558
13928
|
const allAbs = cleared.flatMap(({ existing }) => existing.map(({ abs }) => abs));
|
|
13559
13929
|
const backupRoot = await backupTrackedPaths(root, allAbs, say, "remove");
|
|
13560
|
-
report.backupDir = backupRoot === null ? null :
|
|
13930
|
+
report.backupDir = backupRoot === null ? null : path62.basename(backupRoot);
|
|
13561
13931
|
for (const { id, existing } of cleared) {
|
|
13562
13932
|
await deleteTrackedPaths(root, id, existing, { verb: "Removed", reason: "" }, say);
|
|
13563
13933
|
}
|
|
@@ -13606,7 +13976,15 @@ async function runRescan(options = {}) {
|
|
|
13606
13976
|
}
|
|
13607
13977
|
}
|
|
13608
13978
|
const { ids: activePluginIds, probed: pluginProbeConsultedCli } = await probeActiveGlobalPluginIds(root);
|
|
13609
|
-
|
|
13979
|
+
let recommendation;
|
|
13980
|
+
try {
|
|
13981
|
+
recommendation = await recommend(root, context, { activePluginIds });
|
|
13982
|
+
} catch (err) {
|
|
13983
|
+
if (!(err instanceof ProfileUnsupportedError)) throw err;
|
|
13984
|
+
error(err.message);
|
|
13985
|
+
process.exitCode = 1;
|
|
13986
|
+
return;
|
|
13987
|
+
}
|
|
13610
13988
|
if (!dryRun) {
|
|
13611
13989
|
await writeJson(hausPath(root, "recommendation.json"), recommendation);
|
|
13612
13990
|
}
|
|
@@ -13647,7 +14025,7 @@ async function runScan(options) {
|
|
|
13647
14025
|
}
|
|
13648
14026
|
|
|
13649
14027
|
// src/commands/undo.ts
|
|
13650
|
-
import
|
|
14028
|
+
import path63 from "node:path";
|
|
13651
14029
|
import fs48 from "fs-extra";
|
|
13652
14030
|
|
|
13653
14031
|
// src/claude/managed-paths.ts
|
|
@@ -13681,7 +14059,7 @@ async function collectLockTrackedPaths(root) {
|
|
|
13681
14059
|
const unsafeByRel = await unsafeReasonsFor(root, relPaths);
|
|
13682
14060
|
const existing = await resolveExistingTrackedPaths(root, id, relPaths, log, unsafeByRel);
|
|
13683
14061
|
for (const [rel, reason] of unsafeByRel) {
|
|
13684
|
-
if (reason !== null) preserved.push(
|
|
14062
|
+
if (reason !== null) preserved.push(path63.resolve(root, rel));
|
|
13685
14063
|
}
|
|
13686
14064
|
if (existing.length === 0) continue;
|
|
13687
14065
|
const gate = await hashGateEntry(root, { hash: row.hash }, existing);
|
|
@@ -13702,7 +14080,7 @@ async function settingsHasHausContent(root) {
|
|
|
13702
14080
|
return settings._haus != null;
|
|
13703
14081
|
}
|
|
13704
14082
|
async function claudeMdHasHausBlock(root) {
|
|
13705
|
-
const filePath =
|
|
14083
|
+
const filePath = path63.join(root, "CLAUDE.md");
|
|
13706
14084
|
if (!await fs48.pathExists(filePath)) return false;
|
|
13707
14085
|
const text = await fs48.readFile(filePath, "utf8");
|
|
13708
14086
|
return text.includes(BLOCK_BEGIN);
|
|
@@ -13715,15 +14093,15 @@ async function stripProjectSettings(root) {
|
|
|
13715
14093
|
const hasContent = Object.keys(settings).length > 0;
|
|
13716
14094
|
if (hasContent) {
|
|
13717
14095
|
await writeProjectSettings(root, settings);
|
|
13718
|
-
log(`Stripped haus rules from ${
|
|
14096
|
+
log(`Stripped haus rules from ${path63.relative(root, settingsPath)} (user settings preserved).`);
|
|
13719
14097
|
return true;
|
|
13720
14098
|
}
|
|
13721
14099
|
await fs48.remove(settingsPath);
|
|
13722
|
-
log(`Removed ${
|
|
14100
|
+
log(`Removed ${path63.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
13723
14101
|
return true;
|
|
13724
14102
|
}
|
|
13725
14103
|
async function stripRootClaudeMd(root) {
|
|
13726
|
-
const filePath =
|
|
14104
|
+
const filePath = path63.join(root, "CLAUDE.md");
|
|
13727
14105
|
if (!await fs48.pathExists(filePath)) return false;
|
|
13728
14106
|
const prev = await fs48.readFile(filePath, "utf8");
|
|
13729
14107
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
@@ -13751,12 +14129,12 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
|
13751
14129
|
const backupRoot = hausPath(root, "backups", dirBackupDirName("undo", stamp));
|
|
13752
14130
|
for (const abs of managedAbsPaths) {
|
|
13753
14131
|
if (!await fs48.pathExists(abs)) continue;
|
|
13754
|
-
const rel =
|
|
13755
|
-
const backupPath =
|
|
13756
|
-
await fs48.ensureDir(
|
|
14132
|
+
const rel = path63.relative(root, abs);
|
|
14133
|
+
const backupPath = path63.join(backupRoot, rel);
|
|
14134
|
+
await fs48.ensureDir(path63.dirname(backupPath));
|
|
13757
14135
|
await fs48.copy(abs, backupPath);
|
|
13758
14136
|
}
|
|
13759
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
14137
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path63.relative(root, backupRoot)}.`);
|
|
13760
14138
|
}
|
|
13761
14139
|
async function runUndo(options) {
|
|
13762
14140
|
const root = options.root ?? process.cwd();
|
|
@@ -13770,7 +14148,7 @@ async function runUndo(options) {
|
|
|
13770
14148
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
13771
14149
|
return;
|
|
13772
14150
|
}
|
|
13773
|
-
const relTargets = managed.map((p) =>
|
|
14151
|
+
const relTargets = managed.map((p) => path63.relative(root, p));
|
|
13774
14152
|
const summaryParts = [...relTargets];
|
|
13775
14153
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
13776
14154
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -13798,7 +14176,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
13798
14176
|
for (const abs of coreManaged) {
|
|
13799
14177
|
if (!await fs48.pathExists(abs)) continue;
|
|
13800
14178
|
await fs48.remove(abs);
|
|
13801
|
-
log(`Removed ${
|
|
14179
|
+
log(`Removed ${path63.relative(root, abs)}`);
|
|
13802
14180
|
}
|
|
13803
14181
|
for (const { id, existing } of lockRemovable) {
|
|
13804
14182
|
await deleteTrackedPaths(root, id, existing, { verb: "Removed" }, log);
|
|
@@ -13815,7 +14193,7 @@ init_fs();
|
|
|
13815
14193
|
init_logger();
|
|
13816
14194
|
init_paths();
|
|
13817
14195
|
import crypto4 from "node:crypto";
|
|
13818
|
-
import
|
|
14196
|
+
import path64 from "node:path";
|
|
13819
14197
|
import fs49 from "fs-extra";
|
|
13820
14198
|
async function runUninstall(options = {}) {
|
|
13821
14199
|
const { force = false, removePlugins = removeGlobalPlugins } = options;
|
|
@@ -13872,14 +14250,14 @@ async function runUninstall(options = {}) {
|
|
|
13872
14250
|
continue;
|
|
13873
14251
|
}
|
|
13874
14252
|
await fs49.remove(entry.destPath);
|
|
13875
|
-
await pruneEmptyDir(
|
|
14253
|
+
await pruneEmptyDir(path64.dirname(entry.destPath));
|
|
13876
14254
|
result.deleted.push(entry.destPath);
|
|
13877
14255
|
}
|
|
13878
14256
|
const settings = await readSettings();
|
|
13879
14257
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
13880
14258
|
await writeSettings(stripped);
|
|
13881
14259
|
result.hooksStripped = true;
|
|
13882
|
-
const hausDir =
|
|
14260
|
+
const hausDir = path64.join(globalClaudeDir(), "haus");
|
|
13883
14261
|
const manifestPath2 = hausManifestPath();
|
|
13884
14262
|
if (fs49.pathExistsSync(manifestPath2)) {
|
|
13885
14263
|
await fs49.remove(manifestPath2);
|
|
@@ -13957,7 +14335,7 @@ async function runUninstallCommand(options) {
|
|
|
13957
14335
|
}
|
|
13958
14336
|
|
|
13959
14337
|
// src/commands/validate-catalog.ts
|
|
13960
|
-
import
|
|
14338
|
+
import path66 from "node:path";
|
|
13961
14339
|
|
|
13962
14340
|
// src/catalog/validate-core.ts
|
|
13963
14341
|
init_forbidden_content();
|
|
@@ -13965,7 +14343,7 @@ init_manifest_item_fields();
|
|
|
13965
14343
|
init_path_safety();
|
|
13966
14344
|
init_validation_rules2();
|
|
13967
14345
|
import fs50 from "node:fs";
|
|
13968
|
-
import
|
|
14346
|
+
import path65 from "node:path";
|
|
13969
14347
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
13970
14348
|
var ITEM_ID_PATTERN = /^haus\./;
|
|
13971
14349
|
function auditForbiddenStacks(items) {
|
|
@@ -14132,7 +14510,7 @@ function readForAudit(absPath, label, failures) {
|
|
|
14132
14510
|
}
|
|
14133
14511
|
}
|
|
14134
14512
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
14135
|
-
const rel =
|
|
14513
|
+
const rel = path65.relative(manifestDir, absPath);
|
|
14136
14514
|
const failures = [];
|
|
14137
14515
|
const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
|
|
14138
14516
|
if (text === null) return failures;
|
|
@@ -14154,25 +14532,25 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
14154
14532
|
const failures = [];
|
|
14155
14533
|
for (const item of items) {
|
|
14156
14534
|
if (!item.path || !isSafeCatalogPath(item.path)) continue;
|
|
14157
|
-
const absPath =
|
|
14535
|
+
const absPath = path65.join(manifestDir, item.path);
|
|
14158
14536
|
if (item.type === "skill") {
|
|
14159
|
-
const skillMd =
|
|
14537
|
+
const skillMd = path65.join(absPath, "SKILL.md");
|
|
14160
14538
|
if (!fs50.existsSync(skillMd)) {
|
|
14161
|
-
failures.push(`${item.id}: missing ${
|
|
14539
|
+
failures.push(`${item.id}: missing ${path65.relative(manifestDir, skillMd)}`);
|
|
14162
14540
|
continue;
|
|
14163
14541
|
}
|
|
14164
14542
|
const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
|
|
14165
14543
|
if (text === null) continue;
|
|
14166
14544
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
14167
14545
|
failures.push(
|
|
14168
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
14546
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path65.relative(manifestDir, skillMd)}`)
|
|
14169
14547
|
);
|
|
14170
14548
|
} else if (item.type === "agent") {
|
|
14171
14549
|
if (!fs50.existsSync(absPath)) {
|
|
14172
14550
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
14173
14551
|
continue;
|
|
14174
14552
|
}
|
|
14175
|
-
const rel =
|
|
14553
|
+
const rel = path65.relative(manifestDir, absPath);
|
|
14176
14554
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
14177
14555
|
if (text === null) continue;
|
|
14178
14556
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -14188,7 +14566,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
14188
14566
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
14189
14567
|
continue;
|
|
14190
14568
|
}
|
|
14191
|
-
const rel =
|
|
14569
|
+
const rel = path65.relative(manifestDir, absPath);
|
|
14192
14570
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
14193
14571
|
if (text === null) continue;
|
|
14194
14572
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
@@ -14246,7 +14624,7 @@ function walkAllFiles(dir, fn, failures) {
|
|
|
14246
14624
|
}
|
|
14247
14625
|
for (const entry of entries) {
|
|
14248
14626
|
if (entry.name === ".DS_Store") continue;
|
|
14249
|
-
const full =
|
|
14627
|
+
const full = path65.join(dir, entry.name);
|
|
14250
14628
|
if (entry.isDirectory()) walkAllFiles(full, fn, failures);
|
|
14251
14629
|
else fn(full);
|
|
14252
14630
|
}
|
|
@@ -14270,12 +14648,12 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
14270
14648
|
const failures = [];
|
|
14271
14649
|
const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
|
|
14272
14650
|
for (const dir of CONTENT_ROOTS) {
|
|
14273
|
-
const abs =
|
|
14651
|
+
const abs = path65.join(manifestDir, dir);
|
|
14274
14652
|
if (!fs50.existsSync(abs)) continue;
|
|
14275
14653
|
walkAllFiles(
|
|
14276
14654
|
abs,
|
|
14277
14655
|
(file) => {
|
|
14278
|
-
const relPath =
|
|
14656
|
+
const relPath = path65.relative(manifestDir, file).replace(/\\/g, "/");
|
|
14279
14657
|
if (!isPathClaimed(relPath, claimedPaths)) {
|
|
14280
14658
|
failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
|
|
14281
14659
|
}
|
|
@@ -14296,7 +14674,7 @@ function walkMd(dir, fn, failures) {
|
|
|
14296
14674
|
return;
|
|
14297
14675
|
}
|
|
14298
14676
|
for (const entry of entries) {
|
|
14299
|
-
const full =
|
|
14677
|
+
const full = path65.join(dir, entry.name);
|
|
14300
14678
|
if (entry.isDirectory()) walkMd(full, fn, failures);
|
|
14301
14679
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
14302
14680
|
}
|
|
@@ -14306,13 +14684,13 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
14306
14684
|
const pathSourceMap = buildItemPathSourceMap(items);
|
|
14307
14685
|
const pathTypeMap = buildItemPathTypeMap(items);
|
|
14308
14686
|
for (const dir of ["skills", "agents", "templates", "commands", "sources"]) {
|
|
14309
|
-
const abs =
|
|
14687
|
+
const abs = path65.join(manifestDir, dir);
|
|
14310
14688
|
if (!fs50.existsSync(abs)) continue;
|
|
14311
14689
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
14312
14690
|
walkMd(
|
|
14313
14691
|
abs,
|
|
14314
14692
|
(file) => {
|
|
14315
|
-
const rel =
|
|
14693
|
+
const rel = path65.relative(manifestDir, file);
|
|
14316
14694
|
const text = readForAudit(file, rel, failures);
|
|
14317
14695
|
if (text === null) return;
|
|
14318
14696
|
const norm = rel.replace(/\\/g, "/");
|
|
@@ -14360,8 +14738,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
14360
14738
|
process.exitCode = 1;
|
|
14361
14739
|
return;
|
|
14362
14740
|
}
|
|
14363
|
-
const abs =
|
|
14364
|
-
const manifestDir =
|
|
14741
|
+
const abs = path66.resolve(process.cwd(), manifestPath2);
|
|
14742
|
+
const manifestDir = path66.dirname(abs);
|
|
14365
14743
|
const data = await readJson(abs);
|
|
14366
14744
|
if (!data?.items) {
|
|
14367
14745
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -14382,7 +14760,7 @@ init_logger();
|
|
|
14382
14760
|
|
|
14383
14761
|
// src/workspace/worktree/add.ts
|
|
14384
14762
|
init_fs();
|
|
14385
|
-
import
|
|
14763
|
+
import path73 from "node:path";
|
|
14386
14764
|
import fs54 from "fs-extra";
|
|
14387
14765
|
|
|
14388
14766
|
// src/workspace/worktree/git-worktree.ts
|
|
@@ -14503,12 +14881,12 @@ async function hasUnpushedWork(cwd) {
|
|
|
14503
14881
|
}
|
|
14504
14882
|
|
|
14505
14883
|
// src/workspace/worktree/hydrate.ts
|
|
14506
|
-
import
|
|
14884
|
+
import path69 from "node:path";
|
|
14507
14885
|
import fs52 from "fs-extra";
|
|
14508
14886
|
|
|
14509
14887
|
// src/workspace/worktree/cow-copy.ts
|
|
14510
14888
|
init_exec();
|
|
14511
|
-
import
|
|
14889
|
+
import path67 from "node:path";
|
|
14512
14890
|
import fs51 from "fs-extra";
|
|
14513
14891
|
async function getMacFilesystemType(dir) {
|
|
14514
14892
|
const df = await runCommand("df", ["-P", dir]);
|
|
@@ -14543,11 +14921,11 @@ async function cowCopyDir(src, dest) {
|
|
|
14543
14921
|
const srcExists = await fs51.pathExists(src);
|
|
14544
14922
|
if (!srcExists)
|
|
14545
14923
|
return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
|
|
14546
|
-
const strategy = await detectCowStrategy(
|
|
14924
|
+
const strategy = await detectCowStrategy(path67.dirname(dest));
|
|
14547
14925
|
if (strategy === "unsupported" || strategy === "unknown-platform") {
|
|
14548
14926
|
return { strategy, attempted: false, ok: false };
|
|
14549
14927
|
}
|
|
14550
|
-
await fs51.ensureDir(
|
|
14928
|
+
await fs51.ensureDir(path67.dirname(dest));
|
|
14551
14929
|
const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
|
|
14552
14930
|
const result = await runCommand("cp", args);
|
|
14553
14931
|
return {
|
|
@@ -14561,7 +14939,7 @@ async function cowCopyDir(src, dest) {
|
|
|
14561
14939
|
// src/workspace/worktree/install.ts
|
|
14562
14940
|
init_exec();
|
|
14563
14941
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
14564
|
-
import
|
|
14942
|
+
import path68 from "node:path";
|
|
14565
14943
|
import fg7 from "fast-glob";
|
|
14566
14944
|
function detectInstallPlan(signals) {
|
|
14567
14945
|
if (signals.hasYarnLock) {
|
|
@@ -14582,10 +14960,10 @@ function detectInstallPlan(signals) {
|
|
|
14582
14960
|
return { manager: "none" };
|
|
14583
14961
|
}
|
|
14584
14962
|
async function computeLockfileSignals(dir) {
|
|
14585
|
-
const hasYarnLock = existsSync3(
|
|
14586
|
-
const hasPnpmLock = existsSync3(
|
|
14587
|
-
const hasPackageLock = existsSync3(
|
|
14588
|
-
const hasComposerLock = existsSync3(
|
|
14963
|
+
const hasYarnLock = existsSync3(path68.join(dir, "yarn.lock"));
|
|
14964
|
+
const hasPnpmLock = existsSync3(path68.join(dir, "pnpm-lock.yaml"));
|
|
14965
|
+
const hasPackageLock = existsSync3(path68.join(dir, "package-lock.json"));
|
|
14966
|
+
const hasComposerLock = existsSync3(path68.join(dir, "composer.lock"));
|
|
14589
14967
|
const dotnetFiles = await fg7(["**/*.csproj", "**/*.sln"], {
|
|
14590
14968
|
cwd: dir,
|
|
14591
14969
|
onlyFiles: true,
|
|
@@ -14599,7 +14977,7 @@ async function computeLockfileSignals(dir) {
|
|
|
14599
14977
|
});
|
|
14600
14978
|
const hasDotnetProject = dotnetFiles.length > 0;
|
|
14601
14979
|
let hasPackageManagerField = false;
|
|
14602
|
-
const pkgPath =
|
|
14980
|
+
const pkgPath = path68.join(dir, "package.json");
|
|
14603
14981
|
if (existsSync3(pkgPath)) {
|
|
14604
14982
|
try {
|
|
14605
14983
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
@@ -14642,7 +15020,7 @@ function missingYarnrcEnvVars(content2, env) {
|
|
|
14642
15020
|
return findRequiredYarnrcEnvVars(content2).filter((name) => env[name] === void 0);
|
|
14643
15021
|
}
|
|
14644
15022
|
function missingYarnrcEnvVarsInDir(dir, env) {
|
|
14645
|
-
const yarnrcPath =
|
|
15023
|
+
const yarnrcPath = path68.join(dir, ".yarnrc.yml");
|
|
14646
15024
|
if (!existsSync3(yarnrcPath)) return [];
|
|
14647
15025
|
try {
|
|
14648
15026
|
return missingYarnrcEnvVars(readFileSync3(yarnrcPath, "utf8"), env);
|
|
@@ -14738,8 +15116,8 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
14738
15116
|
const start = Date.now();
|
|
14739
15117
|
const targets = [];
|
|
14740
15118
|
for (const target of HYDRATION_TARGETS) {
|
|
14741
|
-
const src =
|
|
14742
|
-
const dest =
|
|
15119
|
+
const src = path69.join(mainMemberPath, target);
|
|
15120
|
+
const dest = path69.join(worktreeMemberPath, target);
|
|
14743
15121
|
if (!await fs52.pathExists(src)) continue;
|
|
14744
15122
|
const destExists = await fs52.pathExists(dest);
|
|
14745
15123
|
if (!opts.force && destExists) continue;
|
|
@@ -14757,11 +15135,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
14757
15135
|
|
|
14758
15136
|
// src/workspace/worktree/local-files.ts
|
|
14759
15137
|
init_exec();
|
|
14760
|
-
import
|
|
15138
|
+
import path70 from "node:path";
|
|
14761
15139
|
import fs53 from "fs-extra";
|
|
14762
15140
|
var MACHINE_LOCAL_FILES = [
|
|
14763
15141
|
"docker-compose.local.yml",
|
|
14764
|
-
|
|
15142
|
+
path70.join(".claude", "settings.local.json")
|
|
14765
15143
|
];
|
|
14766
15144
|
async function isTracked(repoRoot, relFile) {
|
|
14767
15145
|
const gitPath = relFile.replace(/\\/g, "/");
|
|
@@ -14771,11 +15149,11 @@ async function isTracked(repoRoot, relFile) {
|
|
|
14771
15149
|
async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
14772
15150
|
const copied = [];
|
|
14773
15151
|
for (const rel of MACHINE_LOCAL_FILES) {
|
|
14774
|
-
const src =
|
|
15152
|
+
const src = path70.join(mainMemberPath, rel);
|
|
14775
15153
|
if (!await fs53.pathExists(src)) continue;
|
|
14776
15154
|
if (await isTracked(mainMemberPath, rel)) continue;
|
|
14777
|
-
const dest =
|
|
14778
|
-
await fs53.ensureDir(
|
|
15155
|
+
const dest = path70.join(worktreeMemberPath, rel);
|
|
15156
|
+
await fs53.ensureDir(path70.dirname(dest));
|
|
14779
15157
|
await fs53.copy(src, dest, { overwrite: true });
|
|
14780
15158
|
copied.push(rel);
|
|
14781
15159
|
}
|
|
@@ -14784,15 +15162,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
|
14784
15162
|
|
|
14785
15163
|
// src/workspace/worktree/root.ts
|
|
14786
15164
|
import { existsSync as existsSync4 } from "node:fs";
|
|
14787
|
-
import
|
|
15165
|
+
import path71 from "node:path";
|
|
14788
15166
|
async function resolveWorkspaceForWorktree(start) {
|
|
14789
15167
|
const rootInfo = await resolveRoots(start);
|
|
14790
15168
|
if (!rootInfo.isGitRepo) {
|
|
14791
15169
|
return { ok: false, reason: "Not inside a git repository." };
|
|
14792
15170
|
}
|
|
14793
15171
|
const workspaceRoot = rootInfo.mainRoot;
|
|
14794
|
-
const hasYaml = existsSync4(
|
|
14795
|
-
const hasManifest = existsSync4(
|
|
15172
|
+
const hasYaml = existsSync4(path71.join(workspaceRoot, WORKSPACE_FILE));
|
|
15173
|
+
const hasManifest = existsSync4(path71.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
14796
15174
|
if (!hasYaml && !hasManifest) {
|
|
14797
15175
|
return {
|
|
14798
15176
|
ok: false,
|
|
@@ -14818,10 +15196,10 @@ function selectMembers(members, only) {
|
|
|
14818
15196
|
|
|
14819
15197
|
// src/workspace/worktree/state.ts
|
|
14820
15198
|
init_fs();
|
|
14821
|
-
import
|
|
15199
|
+
import path72 from "node:path";
|
|
14822
15200
|
var WORKTREE_STATE_FILE = ".haus-worktree.json";
|
|
14823
15201
|
function worktreesDir(mainRoot) {
|
|
14824
|
-
return
|
|
15202
|
+
return path72.join(mainRoot, ".claude", "worktrees");
|
|
14825
15203
|
}
|
|
14826
15204
|
function isValidWorktreeSlug(slug) {
|
|
14827
15205
|
if (slug === "" || slug === "." || slug === "..") return false;
|
|
@@ -14834,22 +15212,22 @@ function worktreePath(mainRoot, slug) {
|
|
|
14834
15212
|
`Invalid worktree slug ${JSON.stringify(slug)} \u2014 slugs may only contain letters, digits, ".", "_", "-" (no path separators, and not "." or "..").`
|
|
14835
15213
|
);
|
|
14836
15214
|
}
|
|
14837
|
-
return
|
|
15215
|
+
return path72.join(worktreesDir(mainRoot), slug);
|
|
14838
15216
|
}
|
|
14839
15217
|
function slugFromWorktreePath(mainRoot, somePath) {
|
|
14840
15218
|
const dir = worktreesDir(mainRoot);
|
|
14841
|
-
const prefix = `${dir}${
|
|
15219
|
+
const prefix = `${dir}${path72.sep}`;
|
|
14842
15220
|
if (!somePath.startsWith(prefix)) return void 0;
|
|
14843
|
-
const segment = somePath.slice(prefix.length).split(
|
|
15221
|
+
const segment = somePath.slice(prefix.length).split(path72.sep)[0];
|
|
14844
15222
|
return segment !== void 0 && segment !== "" ? segment : void 0;
|
|
14845
15223
|
}
|
|
14846
15224
|
async function readWorktreeState(worktreeRoot) {
|
|
14847
|
-
const state = await readJson(
|
|
15225
|
+
const state = await readJson(path72.join(worktreeRoot, WORKTREE_STATE_FILE));
|
|
14848
15226
|
if (state == null || typeof state !== "object" || !Array.isArray(state.members)) return void 0;
|
|
14849
15227
|
return state;
|
|
14850
15228
|
}
|
|
14851
15229
|
async function writeWorktreeState(worktreeRoot, state) {
|
|
14852
|
-
await writeJson(
|
|
15230
|
+
await writeJson(path72.join(worktreeRoot, WORKTREE_STATE_FILE), state);
|
|
14853
15231
|
}
|
|
14854
15232
|
|
|
14855
15233
|
// src/workspace/worktree/add.ts
|
|
@@ -14883,7 +15261,7 @@ async function runAdd(opts) {
|
|
|
14883
15261
|
const memberResults = [];
|
|
14884
15262
|
for (const member of selected) {
|
|
14885
15263
|
const start = Date.now();
|
|
14886
|
-
const memberWorktreePath =
|
|
15264
|
+
const memberWorktreePath = path73.join(wsWorktreePath, member.folder);
|
|
14887
15265
|
const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
|
|
14888
15266
|
preferDefaultBranchFrom: true
|
|
14889
15267
|
});
|
|
@@ -14913,7 +15291,7 @@ async function runAdd(opts) {
|
|
|
14913
15291
|
await mapWithConcurrency(okResults, async (result) => {
|
|
14914
15292
|
const member = selected.find((m) => m.id === result.member);
|
|
14915
15293
|
if (!member) return;
|
|
14916
|
-
const memberWorktreePath =
|
|
15294
|
+
const memberWorktreePath = path73.join(wsWorktreePath, member.folder);
|
|
14917
15295
|
const hydrationStart = Date.now();
|
|
14918
15296
|
if (opts.hydrate !== false) {
|
|
14919
15297
|
result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
|
|
@@ -14981,16 +15359,16 @@ async function runDryRun(args) {
|
|
|
14981
15359
|
// src/workspace/worktree/autohydrate.ts
|
|
14982
15360
|
import { spawn } from "node:child_process";
|
|
14983
15361
|
import fsSync from "node:fs";
|
|
14984
|
-
import
|
|
15362
|
+
import path78 from "node:path";
|
|
14985
15363
|
import fs56 from "fs-extra";
|
|
14986
15364
|
|
|
14987
15365
|
// src/workspace/worktree/doctor.ts
|
|
14988
15366
|
import { existsSync as existsSync6 } from "node:fs";
|
|
14989
|
-
import
|
|
15367
|
+
import path76 from "node:path";
|
|
14990
15368
|
|
|
14991
15369
|
// src/workspace/worktree/hydrate-lock.ts
|
|
14992
15370
|
init_paths();
|
|
14993
|
-
import
|
|
15371
|
+
import path74 from "node:path";
|
|
14994
15372
|
import fs55 from "fs-extra";
|
|
14995
15373
|
var STALE_LOCK_MS = 30 * 60 * 1e3;
|
|
14996
15374
|
function locksDir(mainRoot) {
|
|
@@ -15008,13 +15386,13 @@ function assertSlug(slug) {
|
|
|
15008
15386
|
return slug;
|
|
15009
15387
|
}
|
|
15010
15388
|
function hydrateLockPath(mainRoot, slug) {
|
|
15011
|
-
return
|
|
15389
|
+
return path74.join(locksDir(mainRoot), `hydrate-${assertSlug(slug)}.lock`);
|
|
15012
15390
|
}
|
|
15013
15391
|
function hydrateLogPath(mainRoot, slug) {
|
|
15014
|
-
return
|
|
15392
|
+
return path74.join(logsDir(mainRoot), `hydrate-${assertSlug(slug)}.log`);
|
|
15015
15393
|
}
|
|
15016
15394
|
function hydrateLastRunPath(mainRoot, slug) {
|
|
15017
|
-
return
|
|
15395
|
+
return path74.join(logsDir(mainRoot), `hydrate-${assertSlug(slug)}.last.json`);
|
|
15018
15396
|
}
|
|
15019
15397
|
function defaultIsAlive(pid) {
|
|
15020
15398
|
try {
|
|
@@ -15026,7 +15404,7 @@ function defaultIsAlive(pid) {
|
|
|
15026
15404
|
}
|
|
15027
15405
|
async function readOwnerPid(lockPath) {
|
|
15028
15406
|
try {
|
|
15029
|
-
const raw = (await fs55.readFile(
|
|
15407
|
+
const raw = (await fs55.readFile(path74.join(lockPath, "pid"), "utf8")).trim();
|
|
15030
15408
|
if (raw === "") return void 0;
|
|
15031
15409
|
const pid = Number.parseInt(raw, 10);
|
|
15032
15410
|
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
@@ -15035,13 +15413,13 @@ async function readOwnerPid(lockPath) {
|
|
|
15035
15413
|
}
|
|
15036
15414
|
}
|
|
15037
15415
|
async function claim(lockPath, pid) {
|
|
15038
|
-
await fs55.ensureDir(
|
|
15416
|
+
await fs55.ensureDir(path74.dirname(lockPath));
|
|
15039
15417
|
try {
|
|
15040
15418
|
await fs55.mkdir(lockPath);
|
|
15041
15419
|
} catch {
|
|
15042
15420
|
return false;
|
|
15043
15421
|
}
|
|
15044
|
-
await fs55.writeFile(
|
|
15422
|
+
await fs55.writeFile(path74.join(lockPath, "pid"), String(pid), "utf8");
|
|
15045
15423
|
return true;
|
|
15046
15424
|
}
|
|
15047
15425
|
async function acquireHydrateLock(mainRoot, slug, deps = {}) {
|
|
@@ -15072,7 +15450,7 @@ async function acquireHydrateLock(mainRoot, slug, deps = {}) {
|
|
|
15072
15450
|
return await claim(lockPath, pid) ? { acquired: true, lockPath } : { acquired: false, reason: "live", ownerPid, lockPath };
|
|
15073
15451
|
}
|
|
15074
15452
|
async function recordHydrateLockOwner(mainRoot, slug, pid) {
|
|
15075
|
-
await fs55.writeFile(
|
|
15453
|
+
await fs55.writeFile(path74.join(hydrateLockPath(mainRoot, slug), "pid"), String(pid), "utf8");
|
|
15076
15454
|
}
|
|
15077
15455
|
async function releaseHydrateLock(mainRoot, slug) {
|
|
15078
15456
|
await fs55.remove(hydrateLockPath(mainRoot, slug));
|
|
@@ -15092,21 +15470,21 @@ async function readHydrateLock(mainRoot, slug) {
|
|
|
15092
15470
|
|
|
15093
15471
|
// src/workspace/worktree/orphans.ts
|
|
15094
15472
|
import { existsSync as existsSync5 } from "node:fs";
|
|
15095
|
-
import
|
|
15473
|
+
import path75 from "node:path";
|
|
15096
15474
|
var WORKSPACE_REPO_ID = "(workspace)";
|
|
15097
15475
|
async function findOrphanedRegistrations(repos, wsWorktreesDir) {
|
|
15098
15476
|
const orphans = [];
|
|
15099
15477
|
for (const repo of repos) {
|
|
15100
15478
|
const entries = await listWorktrees(repo.absPath);
|
|
15101
15479
|
for (const entry of entries) {
|
|
15102
|
-
const entryPath =
|
|
15103
|
-
const marker = `${
|
|
15480
|
+
const entryPath = path75.normalize(entry.path);
|
|
15481
|
+
const marker = `${path75.sep}.claude${path75.sep}worktrees${path75.sep}`;
|
|
15104
15482
|
const idx = entryPath.indexOf(marker);
|
|
15105
15483
|
if (idx === -1) continue;
|
|
15106
15484
|
const rest = entryPath.slice(idx + marker.length);
|
|
15107
|
-
const slug = rest.split(
|
|
15485
|
+
const slug = rest.split(path75.sep)[0];
|
|
15108
15486
|
if (!slug) continue;
|
|
15109
|
-
if (existsSync5(
|
|
15487
|
+
if (existsSync5(path75.join(wsWorktreesDir, slug))) continue;
|
|
15110
15488
|
orphans.push({
|
|
15111
15489
|
repoId: repo.id,
|
|
15112
15490
|
ownerRepo: repo.absPath,
|
|
@@ -15121,7 +15499,7 @@ async function findOrphanedRegistrations(repos, wsWorktreesDir) {
|
|
|
15121
15499
|
// src/workspace/worktree/doctor.ts
|
|
15122
15500
|
var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
|
|
15123
15501
|
function looksHydrated(memberPath) {
|
|
15124
|
-
return HYDRATION_MARKERS.some((marker) => existsSync6(
|
|
15502
|
+
return HYDRATION_MARKERS.some((marker) => existsSync6(path76.join(memberPath, marker)));
|
|
15125
15503
|
}
|
|
15126
15504
|
async function runDoctor2() {
|
|
15127
15505
|
const rootInfo = await resolveRoots();
|
|
@@ -15138,7 +15516,7 @@ async function runDoctor2() {
|
|
|
15138
15516
|
};
|
|
15139
15517
|
}
|
|
15140
15518
|
const workspaceRoot = rootInfo.mainRoot;
|
|
15141
|
-
const isWorkspace = existsSync6(
|
|
15519
|
+
const isWorkspace = existsSync6(path76.join(workspaceRoot, WORKSPACE_FILE)) || existsSync6(path76.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
15142
15520
|
if (!isWorkspace) {
|
|
15143
15521
|
return {
|
|
15144
15522
|
isWorkspace: false,
|
|
@@ -15154,7 +15532,7 @@ async function runDoctor2() {
|
|
|
15154
15532
|
};
|
|
15155
15533
|
}
|
|
15156
15534
|
const wsWorktreesDir = worktreesDir(workspaceRoot);
|
|
15157
|
-
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${
|
|
15535
|
+
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path76.sep}`);
|
|
15158
15536
|
const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
|
|
15159
15537
|
const workspaceConfig = await readWorkspaceConfig(workspaceRoot);
|
|
15160
15538
|
const autoHydrate = workspaceConfig?.worktree?.autoHydrate === true;
|
|
@@ -15167,10 +15545,10 @@ async function runDoctor2() {
|
|
|
15167
15545
|
let hydrationOwnerPid;
|
|
15168
15546
|
if (inWorkspaceWorktree) {
|
|
15169
15547
|
const wtPath = rootInfo.repoRoot;
|
|
15170
|
-
slug = rootInfo.worktreeName ??
|
|
15548
|
+
slug = rootInfo.worktreeName ?? path76.basename(wtPath);
|
|
15171
15549
|
const state = await readWorktreeState(wtPath);
|
|
15172
15550
|
for (const member of members) {
|
|
15173
|
-
const memberPath =
|
|
15551
|
+
const memberPath = path76.join(wtPath, member.folder);
|
|
15174
15552
|
const materialized = existsSync6(memberPath);
|
|
15175
15553
|
const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
|
|
15176
15554
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
@@ -15215,7 +15593,7 @@ async function runDoctor2() {
|
|
|
15215
15593
|
wsWorktreesDir
|
|
15216
15594
|
);
|
|
15217
15595
|
const orphans = orphanScan.map(
|
|
15218
|
-
(o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${
|
|
15596
|
+
(o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${path76.join(wsWorktreesDir, o.slug)} no longer exists.`
|
|
15219
15597
|
);
|
|
15220
15598
|
if (orphans.length > 0) {
|
|
15221
15599
|
problems.push(...orphans);
|
|
@@ -15238,7 +15616,7 @@ async function runDoctor2() {
|
|
|
15238
15616
|
// src/workspace/worktree/hydrate-command.ts
|
|
15239
15617
|
init_fs();
|
|
15240
15618
|
import { existsSync as existsSync7 } from "node:fs";
|
|
15241
|
-
import
|
|
15619
|
+
import path77 from "node:path";
|
|
15242
15620
|
async function runHydrateWorktree(opts) {
|
|
15243
15621
|
const rootInfo = await resolveRoots();
|
|
15244
15622
|
if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
|
|
@@ -15249,7 +15627,7 @@ async function runHydrateWorktree(opts) {
|
|
|
15249
15627
|
}
|
|
15250
15628
|
const workspaceRoot = rootInfo.mainRoot;
|
|
15251
15629
|
const wtPath = rootInfo.repoRoot;
|
|
15252
|
-
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${
|
|
15630
|
+
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path77.sep}`)) {
|
|
15253
15631
|
return {
|
|
15254
15632
|
ok: false,
|
|
15255
15633
|
error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
|
|
@@ -15261,13 +15639,13 @@ async function runHydrateWorktree(opts) {
|
|
|
15261
15639
|
return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
|
|
15262
15640
|
}
|
|
15263
15641
|
const state = await readWorktreeState(wtPath);
|
|
15264
|
-
const slug = state?.slug ??
|
|
15642
|
+
const slug = state?.slug ?? path77.basename(wtPath);
|
|
15265
15643
|
const detectedBranch = await currentBranch(wtPath);
|
|
15266
15644
|
const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
|
|
15267
15645
|
if (opts.dryRun) {
|
|
15268
15646
|
const results2 = await Promise.all(
|
|
15269
15647
|
selected.map(async (member) => {
|
|
15270
|
-
const memberPath =
|
|
15648
|
+
const memberPath = path77.join(wtPath, member.folder);
|
|
15271
15649
|
if (existsSync7(memberPath)) {
|
|
15272
15650
|
return { member: member.id, folder: member.folder, status: "planned" };
|
|
15273
15651
|
}
|
|
@@ -15288,7 +15666,7 @@ async function runHydrateWorktree(opts) {
|
|
|
15288
15666
|
const results = await mapWithConcurrency(
|
|
15289
15667
|
selected,
|
|
15290
15668
|
async (member) => {
|
|
15291
|
-
const memberPath =
|
|
15669
|
+
const memberPath = path77.join(wtPath, member.folder);
|
|
15292
15670
|
let branchAction;
|
|
15293
15671
|
if (!existsSync7(memberPath)) {
|
|
15294
15672
|
if (!opts.materialize) {
|
|
@@ -15409,7 +15787,7 @@ function defaultSpawnDetached(spec) {
|
|
|
15409
15787
|
}
|
|
15410
15788
|
function appendLog(logPath, line2) {
|
|
15411
15789
|
try {
|
|
15412
|
-
fsSync.mkdirSync(
|
|
15790
|
+
fsSync.mkdirSync(path78.dirname(logPath), { recursive: true });
|
|
15413
15791
|
const size = fsSync.statSync(logPath, { throwIfNoEntry: false })?.size ?? 0;
|
|
15414
15792
|
if (size > LOG_SIZE_CAP_BYTES) {
|
|
15415
15793
|
fsSync.writeFileSync(logPath, `--- log rotated (exceeded ${LOG_SIZE_CAP_BYTES} bytes) ---
|
|
@@ -15573,16 +15951,16 @@ async function writeLastRun(mainRoot, slug, ok, failures) {
|
|
|
15573
15951
|
return;
|
|
15574
15952
|
}
|
|
15575
15953
|
const record = { slug, finishedAt: (/* @__PURE__ */ new Date()).toISOString(), ok, failures };
|
|
15576
|
-
await fs56.ensureDir(
|
|
15954
|
+
await fs56.ensureDir(path78.dirname(file));
|
|
15577
15955
|
await fs56.writeJson(file, record, { spaces: 2 }).catch(() => void 0);
|
|
15578
15956
|
}
|
|
15579
15957
|
|
|
15580
15958
|
// src/workspace/worktree/dematerialize.ts
|
|
15581
|
-
import
|
|
15959
|
+
import path80 from "node:path";
|
|
15582
15960
|
import fs58 from "fs-extra";
|
|
15583
15961
|
|
|
15584
15962
|
// src/workspace/worktree/member-targets.ts
|
|
15585
|
-
import
|
|
15963
|
+
import path79 from "node:path";
|
|
15586
15964
|
import fs57 from "fs-extra";
|
|
15587
15965
|
function normalizeStateMembers(state, allMembers) {
|
|
15588
15966
|
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 }));
|
|
@@ -15600,7 +15978,7 @@ function resolveMemberTargets(wsWorktreePath, stateMembers, allMembers) {
|
|
|
15600
15978
|
targets.push({
|
|
15601
15979
|
repo: configMember?.id ?? stateMember.id,
|
|
15602
15980
|
ownerRepo,
|
|
15603
|
-
path:
|
|
15981
|
+
path: path79.join(wsWorktreePath, stateMember.folder)
|
|
15604
15982
|
});
|
|
15605
15983
|
}
|
|
15606
15984
|
return { targets, unresolvedDroppedMembers };
|
|
@@ -15686,7 +16064,7 @@ async function runDematerialize(opts) {
|
|
|
15686
16064
|
const removed = [];
|
|
15687
16065
|
const failed = [];
|
|
15688
16066
|
for (const folder of unresolvedDroppedMembers) {
|
|
15689
|
-
const droppedPath =
|
|
16067
|
+
const droppedPath = path80.join(wsWorktreePath, folder);
|
|
15690
16068
|
if (await fs58.pathExists(droppedPath)) {
|
|
15691
16069
|
await fs58.remove(droppedPath);
|
|
15692
16070
|
failed.push({ repo: folder, error: unresolvedDroppedFailureReason() });
|
|
@@ -15702,7 +16080,7 @@ async function runDematerialize(opts) {
|
|
|
15702
16080
|
}
|
|
15703
16081
|
|
|
15704
16082
|
// src/workspace/worktree/list.ts
|
|
15705
|
-
import
|
|
16083
|
+
import path81 from "node:path";
|
|
15706
16084
|
import fs59 from "fs-extra";
|
|
15707
16085
|
async function runList2() {
|
|
15708
16086
|
const resolved = await resolveWorkspaceForWorktree();
|
|
@@ -15715,12 +16093,12 @@ async function runList2() {
|
|
|
15715
16093
|
const configuredMembers = await readMembers(rootInfo);
|
|
15716
16094
|
const worktrees = [];
|
|
15717
16095
|
for (const slug of slugs) {
|
|
15718
|
-
const wtPath =
|
|
16096
|
+
const wtPath = path81.join(dir, slug);
|
|
15719
16097
|
const state = await readWorktreeState(wtPath);
|
|
15720
16098
|
const members = [];
|
|
15721
16099
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
15722
16100
|
for (const m of source) {
|
|
15723
|
-
const memberPath =
|
|
16101
|
+
const memberPath = path81.join(wtPath, m.folder);
|
|
15724
16102
|
const materialized = await fs59.pathExists(memberPath);
|
|
15725
16103
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
15726
16104
|
members.push({
|
|
@@ -15743,17 +16121,17 @@ async function runList2() {
|
|
|
15743
16121
|
|
|
15744
16122
|
// src/workspace/worktree/reclaim.ts
|
|
15745
16123
|
import { existsSync as existsSync8 } from "node:fs";
|
|
15746
|
-
import
|
|
16124
|
+
import path82 from "node:path";
|
|
15747
16125
|
import fs60 from "fs-extra";
|
|
15748
16126
|
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
15749
16127
|
async function lastTouchedMs(worktreeRoot) {
|
|
15750
|
-
const candidates = [worktreeRoot,
|
|
16128
|
+
const candidates = [worktreeRoot, path82.join(worktreeRoot, ".haus-worktree.json")];
|
|
15751
16129
|
try {
|
|
15752
|
-
const dotGit = await fs60.readFile(
|
|
16130
|
+
const dotGit = await fs60.readFile(path82.join(worktreeRoot, ".git"), "utf8");
|
|
15753
16131
|
const match = /^gitdir:\s*(.+)$/m.exec(dotGit);
|
|
15754
16132
|
if (match?.[1]) {
|
|
15755
16133
|
const gitdir = match[1].trim();
|
|
15756
|
-
candidates.push(
|
|
16134
|
+
candidates.push(path82.join(gitdir, "HEAD"), path82.join(gitdir, "index"));
|
|
15757
16135
|
}
|
|
15758
16136
|
} catch {
|
|
15759
16137
|
}
|
|
@@ -15796,17 +16174,17 @@ async function runReclaim(opts = {}) {
|
|
|
15796
16174
|
for (const name of unaddressable) {
|
|
15797
16175
|
candidates.push({
|
|
15798
16176
|
slug: name,
|
|
15799
|
-
ageDays: (Date.now() - await lastTouchedMs(
|
|
16177
|
+
ageDays: (Date.now() - await lastTouchedMs(path82.join(dir, name))) / MS_PER_DAY,
|
|
15800
16178
|
materializedMembers: 0,
|
|
15801
16179
|
action: "skip",
|
|
15802
16180
|
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"
|
|
15803
16181
|
});
|
|
15804
16182
|
}
|
|
15805
16183
|
for (const slug of entries) {
|
|
15806
|
-
const wtPath =
|
|
16184
|
+
const wtPath = path82.join(dir, slug);
|
|
15807
16185
|
const state = await readWorktreeState(wtPath);
|
|
15808
16186
|
const stateMembers = normalizeStateMembers(state, members);
|
|
15809
|
-
const materialized = stateMembers.filter((m) => existsSync8(
|
|
16187
|
+
const materialized = stateMembers.filter((m) => existsSync8(path82.join(wtPath, m.folder)));
|
|
15810
16188
|
const ageDays = (Date.now() - await lastTouchedMs(wtPath)) / MS_PER_DAY;
|
|
15811
16189
|
const base = { slug, ageDays, materializedMembers: materialized.length };
|
|
15812
16190
|
if (slug === currentSlug) {
|
|
@@ -16403,7 +16781,7 @@ async function runWorktree(action, opts = {}) {
|
|
|
16403
16781
|
|
|
16404
16782
|
// src/commands/workspace.ts
|
|
16405
16783
|
import { existsSync as existsSync12, statSync as statSync3 } from "node:fs";
|
|
16406
|
-
import
|
|
16784
|
+
import path91 from "node:path";
|
|
16407
16785
|
init_fs();
|
|
16408
16786
|
init_logger();
|
|
16409
16787
|
|
|
@@ -16462,7 +16840,7 @@ ${summaries.map(
|
|
|
16462
16840
|
|
|
16463
16841
|
// src/commands/workspace/doctor.ts
|
|
16464
16842
|
import { existsSync as existsSync9 } from "node:fs";
|
|
16465
|
-
import
|
|
16843
|
+
import path84 from "node:path";
|
|
16466
16844
|
init_logger();
|
|
16467
16845
|
init_paths();
|
|
16468
16846
|
|
|
@@ -16470,14 +16848,14 @@ init_paths();
|
|
|
16470
16848
|
init_fs();
|
|
16471
16849
|
init_paths();
|
|
16472
16850
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
16473
|
-
import
|
|
16851
|
+
import path83 from "node:path";
|
|
16474
16852
|
var MANIFEST_FILE2 = "workspace.manifest.json";
|
|
16475
16853
|
function manifestPath(workspaceRoot) {
|
|
16476
16854
|
return hausPath(workspaceRoot, MANIFEST_FILE2);
|
|
16477
16855
|
}
|
|
16478
16856
|
function hausVersion() {
|
|
16479
16857
|
try {
|
|
16480
|
-
const pkg = JSON.parse(readFileSync4(
|
|
16858
|
+
const pkg = JSON.parse(readFileSync4(path83.join(packageRoot(), "package.json"), "utf8"));
|
|
16481
16859
|
return pkg.version ?? "0.0.0";
|
|
16482
16860
|
} catch {
|
|
16483
16861
|
return "0.0.0";
|
|
@@ -16569,7 +16947,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
16569
16947
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
16570
16948
|
const catalogRefByRepo = [];
|
|
16571
16949
|
for (const repo of config.repos) {
|
|
16572
|
-
const repoRoot =
|
|
16950
|
+
const repoRoot = path84.resolve(workspaceRoot, repo.path);
|
|
16573
16951
|
const entry = manifestByName.get(repo.name);
|
|
16574
16952
|
if (!entry) {
|
|
16575
16953
|
flag({
|
|
@@ -16642,7 +17020,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
16642
17020
|
});
|
|
16643
17021
|
continue;
|
|
16644
17022
|
}
|
|
16645
|
-
if (!existsSync9(
|
|
17023
|
+
if (!existsSync9(path84.join(workspaceRoot, entry.path))) {
|
|
16646
17024
|
flag({
|
|
16647
17025
|
repo: entry.repo,
|
|
16648
17026
|
kind: "missing-linked-context-copy",
|
|
@@ -16688,17 +17066,17 @@ function emit(args) {
|
|
|
16688
17066
|
init_logger();
|
|
16689
17067
|
|
|
16690
17068
|
// src/workspace/link-context/link.ts
|
|
16691
|
-
import
|
|
17069
|
+
import path87 from "node:path";
|
|
16692
17070
|
import fs64 from "fs-extra";
|
|
16693
17071
|
init_fs();
|
|
16694
17072
|
init_logger();
|
|
16695
17073
|
|
|
16696
17074
|
// src/workspace/link-context/plan.ts
|
|
16697
|
-
import
|
|
17075
|
+
import path86 from "node:path";
|
|
16698
17076
|
import fs63 from "fs-extra";
|
|
16699
17077
|
|
|
16700
17078
|
// src/workspace/link-context/scan-source.ts
|
|
16701
|
-
import
|
|
17079
|
+
import path85 from "node:path";
|
|
16702
17080
|
import fs62 from "fs-extra";
|
|
16703
17081
|
var ASSET_SUBDIR = {
|
|
16704
17082
|
skill: "skills",
|
|
@@ -16707,36 +17085,36 @@ var ASSET_SUBDIR = {
|
|
|
16707
17085
|
};
|
|
16708
17086
|
async function hasLinkableContext(member) {
|
|
16709
17087
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
16710
|
-
if (await fs62.pathExists(
|
|
17088
|
+
if (await fs62.pathExists(path85.join(member.absPath, ".claude", subdir))) return true;
|
|
16711
17089
|
}
|
|
16712
17090
|
return false;
|
|
16713
17091
|
}
|
|
16714
17092
|
async function collectSkills(member) {
|
|
16715
|
-
const dir =
|
|
17093
|
+
const dir = path85.join(member.absPath, ".claude", "skills");
|
|
16716
17094
|
if (!await fs62.pathExists(dir)) return [];
|
|
16717
17095
|
const names = await fs62.readdir(dir);
|
|
16718
17096
|
const out = [];
|
|
16719
17097
|
for (const name of [...names].sort()) {
|
|
16720
|
-
const abs =
|
|
17098
|
+
const abs = path85.join(dir, name);
|
|
16721
17099
|
const stat = await fs62.lstat(abs);
|
|
16722
17100
|
if (!stat.isDirectory()) continue;
|
|
16723
|
-
if (!await fs62.pathExists(
|
|
16724
|
-
out.push({ type: "skill", name, sourceRelPath:
|
|
17101
|
+
if (!await fs62.pathExists(path85.join(abs, "SKILL.md"))) continue;
|
|
17102
|
+
out.push({ type: "skill", name, sourceRelPath: path85.posix.join(".claude", "skills", name) });
|
|
16725
17103
|
}
|
|
16726
17104
|
return out;
|
|
16727
17105
|
}
|
|
16728
17106
|
async function collectFlatMarkdown(member, type) {
|
|
16729
17107
|
const subdir = ASSET_SUBDIR[type];
|
|
16730
|
-
const dir =
|
|
17108
|
+
const dir = path85.join(member.absPath, ".claude", subdir);
|
|
16731
17109
|
if (!await fs62.pathExists(dir)) return [];
|
|
16732
17110
|
const names = await fs62.readdir(dir);
|
|
16733
17111
|
const out = [];
|
|
16734
17112
|
for (const fileName of [...names].sort()) {
|
|
16735
17113
|
if (!fileName.endsWith(".md")) continue;
|
|
16736
|
-
const abs =
|
|
17114
|
+
const abs = path85.join(dir, fileName);
|
|
16737
17115
|
if (!(await fs62.lstat(abs)).isFile()) continue;
|
|
16738
17116
|
const name = fileName.slice(0, -".md".length);
|
|
16739
|
-
out.push({ type, name, sourceRelPath:
|
|
17117
|
+
out.push({ type, name, sourceRelPath: path85.posix.join(".claude", subdir, fileName) });
|
|
16740
17118
|
}
|
|
16741
17119
|
return out;
|
|
16742
17120
|
}
|
|
@@ -16751,9 +17129,9 @@ async function collectSourceAssets(member) {
|
|
|
16751
17129
|
|
|
16752
17130
|
// src/workspace/link-context/plan.ts
|
|
16753
17131
|
function repoFolderPrefix(member) {
|
|
16754
|
-
const base =
|
|
17132
|
+
const base = path86.posix.basename(member.folder);
|
|
16755
17133
|
if (base && base !== "." && base !== "/") return base;
|
|
16756
|
-
const fromAbs =
|
|
17134
|
+
const fromAbs = path86.basename(member.absPath);
|
|
16757
17135
|
return fromAbs || member.id;
|
|
16758
17136
|
}
|
|
16759
17137
|
function assetKey(type, destKey) {
|
|
@@ -16810,7 +17188,7 @@ async function buildLinkPlan(members) {
|
|
|
16810
17188
|
name: asset.name,
|
|
16811
17189
|
destKey,
|
|
16812
17190
|
sourceRelPath: asset.sourceRelPath,
|
|
16813
|
-
sourceAbsPath:
|
|
17191
|
+
sourceAbsPath: path86.join(member.absPath, asset.sourceRelPath),
|
|
16814
17192
|
sourceHash
|
|
16815
17193
|
});
|
|
16816
17194
|
}
|
|
@@ -16821,7 +17199,7 @@ async function buildLinkPlan(members) {
|
|
|
16821
17199
|
function destRelPathFor(type, destKey) {
|
|
16822
17200
|
const subdir = `${type}s`;
|
|
16823
17201
|
const leaf = type === "skill" ? destKey : `${destKey}.md`;
|
|
16824
|
-
return
|
|
17202
|
+
return path87.posix.join(".claude", subdir, leaf);
|
|
16825
17203
|
}
|
|
16826
17204
|
async function runLinkContext(workspaceRoot, opts = {}) {
|
|
16827
17205
|
const write = resolveWriteMode(opts, "preview-by-default").kind === "write";
|
|
@@ -16847,9 +17225,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
16847
17225
|
const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
16848
17226
|
for (const entry of plan.entries) {
|
|
16849
17227
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
16850
|
-
const destAbs =
|
|
17228
|
+
const destAbs = path87.join(workspaceRoot, destRel);
|
|
16851
17229
|
if (write) {
|
|
16852
|
-
await fs64.ensureDir(
|
|
17230
|
+
await fs64.ensureDir(path87.dirname(destAbs));
|
|
16853
17231
|
if (await fs64.pathExists(destAbs)) await fs64.remove(destAbs);
|
|
16854
17232
|
await fs64.copy(entry.sourceAbsPath, destAbs, {
|
|
16855
17233
|
overwrite: true,
|
|
@@ -16938,10 +17316,10 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
|
|
|
16938
17316
|
|
|
16939
17317
|
// src/commands/workspace/setup.ts
|
|
16940
17318
|
import { existsSync as existsSync10, statSync } from "node:fs";
|
|
16941
|
-
import
|
|
17319
|
+
import path89 from "node:path";
|
|
16942
17320
|
|
|
16943
17321
|
// src/claude/write-workspace-claude-md.ts
|
|
16944
|
-
import
|
|
17322
|
+
import path88 from "node:path";
|
|
16945
17323
|
import fs65 from "fs-extra";
|
|
16946
17324
|
init_fs();
|
|
16947
17325
|
init_logger();
|
|
@@ -16967,7 +17345,7 @@ ${WORKSPACE_BLOCK_END}`;
|
|
|
16967
17345
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
16968
17346
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
16969
17347
|
const dryRun = opts.dryRun ?? false;
|
|
16970
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
17348
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path88.join(workspaceRoot, "CLAUDE.md");
|
|
16971
17349
|
const say = opts.quiet ? () => {
|
|
16972
17350
|
} : log;
|
|
16973
17351
|
const existed = await fs65.pathExists(filePath);
|
|
@@ -17000,20 +17378,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
17000
17378
|
init_fs();
|
|
17001
17379
|
init_logger();
|
|
17002
17380
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
17003
|
-
let dir =
|
|
17381
|
+
let dir = path89.resolve(start);
|
|
17004
17382
|
for (; ; ) {
|
|
17005
|
-
if (existsSync10(
|
|
17006
|
-
const parent =
|
|
17007
|
-
if (parent === dir) return
|
|
17383
|
+
if (existsSync10(path89.join(dir, WORKSPACE_FILE))) return dir;
|
|
17384
|
+
const parent = path89.dirname(dir);
|
|
17385
|
+
if (parent === dir) return path89.resolve(start);
|
|
17008
17386
|
dir = parent;
|
|
17009
17387
|
}
|
|
17010
17388
|
}
|
|
17011
17389
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
17012
|
-
return
|
|
17390
|
+
return path89.resolve(workspaceRoot, repoPath) === path89.resolve(workspaceRoot);
|
|
17013
17391
|
}
|
|
17014
17392
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
17015
17393
|
const apply = options.write ?? false;
|
|
17016
|
-
const configText = await readText(
|
|
17394
|
+
const configText = await readText(path89.join(workspaceRoot, WORKSPACE_FILE));
|
|
17017
17395
|
if (!configText) {
|
|
17018
17396
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
17019
17397
|
process.exitCode = 1;
|
|
@@ -17040,7 +17418,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
17040
17418
|
const statuses = [];
|
|
17041
17419
|
const aggregateInputs = [];
|
|
17042
17420
|
for (const repo of repos) {
|
|
17043
|
-
const repoRoot =
|
|
17421
|
+
const repoRoot = path89.resolve(workspaceRoot, repo.path);
|
|
17044
17422
|
say(`
|
|
17045
17423
|
\u2192 ${repo.name} (${repo.path})`);
|
|
17046
17424
|
let res;
|
|
@@ -17054,7 +17432,12 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
17054
17432
|
// Under --json, mute the inner pipeline entirely; we serialize the
|
|
17055
17433
|
// aggregated result below. Otherwise let it print its normal human output.
|
|
17056
17434
|
quiet: options.json,
|
|
17057
|
-
allowEmptyCache: options.allowEmptyCache
|
|
17435
|
+
allowEmptyCache: options.allowEmptyCache,
|
|
17436
|
+
// A workspace run sets a member up under whatever profile the member already
|
|
17437
|
+
// declares (ADR-0125). Passing it explicitly is what keeps the PoC rail in
|
|
17438
|
+
// runSetupCore from firing here: the rail exists for a deliberate `haus init`,
|
|
17439
|
+
// not for a bulk pass that never meant to switch anyone's profile.
|
|
17440
|
+
profile: await readProjectProfile(repoRoot)
|
|
17058
17441
|
});
|
|
17059
17442
|
} catch (err) {
|
|
17060
17443
|
const message2 = err instanceof Error ? err.message : String(err);
|
|
@@ -17156,7 +17539,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
17156
17539
|
const status = statusByName.get(repo.name);
|
|
17157
17540
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
17158
17541
|
if (status?.status === "ok") {
|
|
17159
|
-
const lock = await checkLock(
|
|
17542
|
+
const lock = await checkLock(path89.resolve(workspaceRoot, repo.path));
|
|
17160
17543
|
manifestRepos.push({
|
|
17161
17544
|
name: repo.name,
|
|
17162
17545
|
path: repo.path,
|
|
@@ -17212,7 +17595,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
17212
17595
|
try {
|
|
17213
17596
|
const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
|
|
17214
17597
|
if (linkResult.ok) {
|
|
17215
|
-
written.push(...linkResult.added.map((rel) =>
|
|
17598
|
+
written.push(...linkResult.added.map((rel) => path89.join(workspaceRoot, rel)));
|
|
17216
17599
|
if (!options.json) {
|
|
17217
17600
|
say(
|
|
17218
17601
|
`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` : ""}.`
|
|
@@ -17245,7 +17628,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
17245
17628
|
|
|
17246
17629
|
// src/commands/workspace/undo.ts
|
|
17247
17630
|
import { existsSync as existsSync11, statSync as statSync2 } from "node:fs";
|
|
17248
|
-
import
|
|
17631
|
+
import path90 from "node:path";
|
|
17249
17632
|
import fs66 from "fs-extra";
|
|
17250
17633
|
init_fs();
|
|
17251
17634
|
init_logger();
|
|
@@ -17276,7 +17659,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
17276
17659
|
}
|
|
17277
17660
|
if (options.dryRun) {
|
|
17278
17661
|
for (const repo of config.repos) {
|
|
17279
|
-
const repoRoot =
|
|
17662
|
+
const repoRoot = path90.resolve(workspaceRoot, repo.path);
|
|
17280
17663
|
log(`
|
|
17281
17664
|
\u2192 ${repo.name} (${repo.path})`);
|
|
17282
17665
|
try {
|
|
@@ -17308,11 +17691,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
17308
17691
|
];
|
|
17309
17692
|
for (const abs of workspaceArtifacts) {
|
|
17310
17693
|
if (await fs66.pathExists(abs)) {
|
|
17311
|
-
log(`[dry-run] Would remove ${
|
|
17694
|
+
log(`[dry-run] Would remove ${path90.relative(workspaceRoot, abs)}`);
|
|
17312
17695
|
}
|
|
17313
17696
|
}
|
|
17314
17697
|
if (!await fs66.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
|
|
17315
|
-
const claudeMd = await readText(
|
|
17698
|
+
const claudeMd = await readText(path90.join(workspaceRoot, "CLAUDE.md"));
|
|
17316
17699
|
if (claudeMd?.includes(WORKSPACE_BLOCK_BEGIN) || claudeMd && hasLegacyWorkspaceBlock(claudeMd)) {
|
|
17317
17700
|
log("[dry-run] Would remove the workspace haus import block from CLAUDE.md");
|
|
17318
17701
|
}
|
|
@@ -17331,7 +17714,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
17331
17714
|
}
|
|
17332
17715
|
let anyRepoFailed = false;
|
|
17333
17716
|
for (const repo of config.repos) {
|
|
17334
|
-
const repoRoot =
|
|
17717
|
+
const repoRoot = path90.resolve(workspaceRoot, repo.path);
|
|
17335
17718
|
log(`
|
|
17336
17719
|
\u2192 ${repo.name} (${repo.path})`);
|
|
17337
17720
|
try {
|
|
@@ -17360,20 +17743,20 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
17360
17743
|
const abs = hausPath(workspaceRoot, rel);
|
|
17361
17744
|
if (await fs66.pathExists(abs)) {
|
|
17362
17745
|
await fs66.remove(abs);
|
|
17363
|
-
log(`Removed ${
|
|
17746
|
+
log(`Removed ${path90.relative(workspaceRoot, abs)}`);
|
|
17364
17747
|
}
|
|
17365
17748
|
}
|
|
17366
17749
|
const manifestFile = manifestPath(workspaceRoot);
|
|
17367
17750
|
if (await fs66.pathExists(manifestFile)) {
|
|
17368
17751
|
await fs66.remove(manifestFile);
|
|
17369
|
-
log(`Removed ${
|
|
17752
|
+
log(`Removed ${path90.relative(workspaceRoot, manifestFile)}`);
|
|
17370
17753
|
}
|
|
17371
17754
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
17372
17755
|
if (await fs66.pathExists(workspaceMdPath)) {
|
|
17373
17756
|
await fs66.remove(workspaceMdPath);
|
|
17374
|
-
log(`Removed ${
|
|
17757
|
+
log(`Removed ${path90.relative(workspaceRoot, workspaceMdPath)}`);
|
|
17375
17758
|
} else {
|
|
17376
|
-
const claudeMdPath =
|
|
17759
|
+
const claudeMdPath = path90.join(workspaceRoot, "CLAUDE.md");
|
|
17377
17760
|
if (existsSync11(claudeMdPath)) {
|
|
17378
17761
|
const prev = await readText(claudeMdPath);
|
|
17379
17762
|
if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
|
|
@@ -17442,7 +17825,7 @@ relationships: []
|
|
|
17442
17825
|
log("Workspace initialized.");
|
|
17443
17826
|
}
|
|
17444
17827
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
17445
|
-
const configText = await readText(
|
|
17828
|
+
const configText = await readText(path91.join(workspaceRoot, WORKSPACE_FILE));
|
|
17446
17829
|
if (!configText) {
|
|
17447
17830
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
17448
17831
|
process.exitCode = 1;
|
|
@@ -17464,7 +17847,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
17464
17847
|
const inputs = [];
|
|
17465
17848
|
const failures = [];
|
|
17466
17849
|
for (const repo of config.repos) {
|
|
17467
|
-
const repoRoot =
|
|
17850
|
+
const repoRoot = path91.resolve(workspaceRoot, repo.path);
|
|
17468
17851
|
try {
|
|
17469
17852
|
if (!existsSync12(repoRoot) || !statSync3(repoRoot).isDirectory()) {
|
|
17470
17853
|
throw new Error("path is not a directory");
|
|
@@ -17533,7 +17916,7 @@ init_logger();
|
|
|
17533
17916
|
init_paths();
|
|
17534
17917
|
function cliVersion() {
|
|
17535
17918
|
try {
|
|
17536
|
-
const pkgPath =
|
|
17919
|
+
const pkgPath = path92.join(packageRoot(), "package.json");
|
|
17537
17920
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
17538
17921
|
return pkg.version ?? "0.0.0";
|
|
17539
17922
|
} catch {
|
|
@@ -17543,7 +17926,7 @@ function cliVersion() {
|
|
|
17543
17926
|
var program = new Command();
|
|
17544
17927
|
function validateRuntimeNodeVersion() {
|
|
17545
17928
|
try {
|
|
17546
|
-
const pkgPath =
|
|
17929
|
+
const pkgPath = path92.join(packageRoot(), "package.json");
|
|
17547
17930
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
17548
17931
|
const requiredRange = pkg.engines?.node;
|
|
17549
17932
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -17616,10 +17999,16 @@ program.command("init").description("Full setup: scan, recommend, and optionally
|
|
|
17616
17999
|
).option(
|
|
17617
18000
|
"--allow-empty-cache",
|
|
17618
18001
|
"Proceed when no catalog item has installable content: write core files and skip the catalog items. Still exits 1 if none installed (ADR-0060)"
|
|
18002
|
+
).option(
|
|
18003
|
+
"--profile <profile>",
|
|
18004
|
+
"Project profile to record when writing: production (default) | poc. Stored in .haus-workflow/haus.config.json (ADR-0125)"
|
|
17619
18005
|
).action(runInit);
|
|
17620
18006
|
program.command("new").description("Create a new project from an allowed Haus stack, push it to GitHub, and run init").argument("[stack]", "Stack id (omit to pick interactively; see --list)").argument("[name]", "Project directory and repo name").option("--list", "Print the allowed stacks and exit").option("--org <org>", "GitHub org to create the repo in (default: personal account)").option("--public", "Create the GitHub repo public").option("--private", "Create the GitHub repo private (default)").option("--no-github", "Skip GitHub repo creation and push").option("--pm <pm>", "Package manager: yarn | npm | pnpm", "yarn").option("--dir <path>", "Directory to scaffold the project into (default: the cwd)").option("-y, --yes", "Skip confirmations (non-interactive)").option("--dry-run", "Print the full plan without executing anything").option("--json", "Emit a machine-readable result document").option(
|
|
17621
18007
|
"--allow-empty-cache",
|
|
17622
18008
|
"Proceed when no catalog item has installable content: write core files and skip the catalog items. Still exits 1 if none installed (ADR-0060)"
|
|
18009
|
+
).option(
|
|
18010
|
+
"--profile <profile>",
|
|
18011
|
+
"Project profile to record when writing: production (default) | poc. Stored in .haus-workflow/haus.config.json (ADR-0125)"
|
|
17623
18012
|
).action(runNew);
|
|
17624
18013
|
program.command("clone").description("Clone a single git repository by URL").argument("<url>", "Git URL to clone").argument("[dir]", "Target directory (default: repo name derived from the URL)").option("--dry-run", "Print what would happen without cloning").action(
|
|
17625
18014
|
(url, dir, opts) => runClone(url, { dir, dryRun: opts.dryRun })
|