@haus-tech/haus-workflow 1.2.0 → 1.3.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 +12 -0
- package/dist/cli.js +780 -279
- package/library/catalog/manifest.json +209 -706
- package/library/catalog/validation-rules.json +20 -0
- package/library/global/skills/haus-workflow/SKILL.md +35 -14
- package/library/global/skills/haus-workflow/references/clone.md +24 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -60,7 +60,7 @@ var init_exec = __esm({
|
|
|
60
60
|
|
|
61
61
|
// src/cli.ts
|
|
62
62
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
63
|
-
import
|
|
63
|
+
import path53 from "node:path";
|
|
64
64
|
import { Command } from "commander";
|
|
65
65
|
|
|
66
66
|
// src/commands/apply.ts
|
|
@@ -879,6 +879,26 @@ var validation_rules_default = {
|
|
|
879
879
|
"collection2",
|
|
880
880
|
"deployer-php"
|
|
881
881
|
],
|
|
882
|
+
safetyNotesRequiredTags: [
|
|
883
|
+
"stripe",
|
|
884
|
+
"qliro",
|
|
885
|
+
"payments",
|
|
886
|
+
"bankid",
|
|
887
|
+
"myid",
|
|
888
|
+
"oidc",
|
|
889
|
+
"azure-ad",
|
|
890
|
+
"saml2"
|
|
891
|
+
],
|
|
892
|
+
intentsRequiredTags: [
|
|
893
|
+
"stripe",
|
|
894
|
+
"qliro",
|
|
895
|
+
"payments",
|
|
896
|
+
"bankid",
|
|
897
|
+
"myid",
|
|
898
|
+
"oidc",
|
|
899
|
+
"azure-ad",
|
|
900
|
+
"saml2"
|
|
901
|
+
],
|
|
882
902
|
alwaysAllowedTags: [
|
|
883
903
|
"haus",
|
|
884
904
|
"security",
|
|
@@ -1387,6 +1407,13 @@ function findFormerIdMigrations(lockItems, manifestItems) {
|
|
|
1387
1407
|
});
|
|
1388
1408
|
}
|
|
1389
1409
|
|
|
1410
|
+
// src/recommender/orphaned-items.ts
|
|
1411
|
+
function findOrphanedLockEntries(lock, recommendedIds) {
|
|
1412
|
+
return lock.filter(
|
|
1413
|
+
(entry) => typeof entry.id === "string" && !recommendedIds.has(entry.id)
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1390
1417
|
// src/update/hash-installed.ts
|
|
1391
1418
|
import path10 from "node:path";
|
|
1392
1419
|
import fg2 from "fast-glob";
|
|
@@ -1876,8 +1903,8 @@ function buildDenyRules() {
|
|
|
1876
1903
|
for (const command of DENY_COMMANDS) {
|
|
1877
1904
|
rules.push(`Bash(${command}:*)`);
|
|
1878
1905
|
}
|
|
1879
|
-
for (const
|
|
1880
|
-
const pattern = DENY_DIRS.has(
|
|
1906
|
+
for (const path54 of DENY_PATHS) {
|
|
1907
|
+
const pattern = DENY_DIRS.has(path54) ? `${path54}/**` : path54;
|
|
1881
1908
|
for (const tool of FILE_TOOLS) {
|
|
1882
1909
|
rules.push(`${tool}(${pattern})`);
|
|
1883
1910
|
}
|
|
@@ -2164,11 +2191,23 @@ import path15 from "node:path";
|
|
|
2164
2191
|
import fs13 from "fs-extra";
|
|
2165
2192
|
var PRETTIERIGNORE_BEGIN = "# HAUS:BEGIN haus-managed v=1";
|
|
2166
2193
|
var PRETTIERIGNORE_END = "# HAUS:END haus-managed";
|
|
2167
|
-
var IGNORED_PATHS = [".haus-workflow/"];
|
|
2194
|
+
var IGNORED_PATHS = [".haus-workflow/", ".claude/"];
|
|
2195
|
+
function prettierIgnoreProtects(lines, dir) {
|
|
2196
|
+
const bare = dir.replace(/\/+$/, "");
|
|
2197
|
+
const equivalents = /* @__PURE__ */ new Set([
|
|
2198
|
+
bare,
|
|
2199
|
+
`${bare}/`,
|
|
2200
|
+
`/${bare}`,
|
|
2201
|
+
`/${bare}/`,
|
|
2202
|
+
`${bare}/**`,
|
|
2203
|
+
`/${bare}/**`
|
|
2204
|
+
]);
|
|
2205
|
+
return lines.some((line2) => equivalents.has(line2.trim()));
|
|
2206
|
+
}
|
|
2168
2207
|
function buildPrettierIgnoreBlock() {
|
|
2169
2208
|
return [
|
|
2170
2209
|
PRETTIERIGNORE_BEGIN,
|
|
2171
|
-
"# haus-owned files \u2014 do not reformat (keeps tamper
|
|
2210
|
+
"# haus-owned files \u2014 do not reformat (keeps tamper/lock hashes stable)",
|
|
2172
2211
|
...IGNORED_PATHS,
|
|
2173
2212
|
PRETTIERIGNORE_END
|
|
2174
2213
|
].join("\n");
|
|
@@ -2817,6 +2856,16 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2817
2856
|
);
|
|
2818
2857
|
}
|
|
2819
2858
|
await cleanupStaleCatalogItems(root, cleanupManifestById, dryRun, opts.quiet);
|
|
2859
|
+
if (opts.prune) {
|
|
2860
|
+
const notOrphaned = new Set(rec.recommended.map((r) => r.id));
|
|
2861
|
+
for (const migration of allMigrations) notOrphaned.add(migration.oldId);
|
|
2862
|
+
for (const entry of prevLock) {
|
|
2863
|
+
if (!entry.id) continue;
|
|
2864
|
+
const manifestItem = manifestById.get(entry.id);
|
|
2865
|
+
if (!manifestItem || manifestItem.reviewStatus === "deprecated") notOrphaned.add(entry.id);
|
|
2866
|
+
}
|
|
2867
|
+
await pruneOrphanedCatalogItems(root, prevLock, notOrphaned, dryRun, opts.quiet);
|
|
2868
|
+
}
|
|
2820
2869
|
if (dryRun) return [...new Set(files)];
|
|
2821
2870
|
const installedItems = catalogItems.filter((r) => installedIds.has(r.id));
|
|
2822
2871
|
const prevRefById = new Map(
|
|
@@ -2934,6 +2983,71 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet) {
|
|
|
2934
2983
|
}
|
|
2935
2984
|
}
|
|
2936
2985
|
}
|
|
2986
|
+
async function backupBeforePrune(root, absPaths, say) {
|
|
2987
|
+
if (absPaths.length === 0) return;
|
|
2988
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2989
|
+
const backupRoot = hausPath(root, "backups", `prune-${stamp}`);
|
|
2990
|
+
for (const abs of absPaths) {
|
|
2991
|
+
if (!await fs18.pathExists(abs)) continue;
|
|
2992
|
+
const rel = path19.relative(root, abs);
|
|
2993
|
+
const backupPath = path19.join(backupRoot, rel);
|
|
2994
|
+
await fs18.ensureDir(path19.dirname(backupPath));
|
|
2995
|
+
await fs18.copy(abs, backupPath);
|
|
2996
|
+
}
|
|
2997
|
+
say(`Backed up ${absPaths.length} path(s) to ${path19.relative(root, backupRoot)} before pruning.`);
|
|
2998
|
+
}
|
|
2999
|
+
async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun, quiet) {
|
|
3000
|
+
const say = quiet ? () => {
|
|
3001
|
+
} : log;
|
|
3002
|
+
const orphaned = findOrphanedLockEntries(prevLock, notOrphanedIds);
|
|
3003
|
+
if (orphaned.length === 0) return;
|
|
3004
|
+
const toRemove = [];
|
|
3005
|
+
for (const entry of orphaned) {
|
|
3006
|
+
if (!entry.id) continue;
|
|
3007
|
+
const relPaths = entry.paths ?? [];
|
|
3008
|
+
if (relPaths.length === 0) continue;
|
|
3009
|
+
const existing = [];
|
|
3010
|
+
for (const rel of relPaths) {
|
|
3011
|
+
if (await fs18.pathExists(path19.join(root, rel))) existing.push(rel);
|
|
3012
|
+
}
|
|
3013
|
+
if (existing.length === 0) continue;
|
|
3014
|
+
if (entry.hash === void 0) {
|
|
3015
|
+
warn(
|
|
3016
|
+
`Orphaned catalog item ${entry.id} has no lock hash \u2014 leaving in place: ${existing.join(", ")}`
|
|
3017
|
+
);
|
|
3018
|
+
continue;
|
|
3019
|
+
}
|
|
3020
|
+
const currentHash = await hashInstalledPaths(root, relPaths);
|
|
3021
|
+
if (currentHash !== entry.hash) {
|
|
3022
|
+
warn(
|
|
3023
|
+
`Orphaned catalog item ${entry.id} was modified locally \u2014 leaving in place: ${existing.join(", ")}`
|
|
3024
|
+
);
|
|
3025
|
+
continue;
|
|
3026
|
+
}
|
|
3027
|
+
toRemove.push({ id: entry.id, existing });
|
|
3028
|
+
}
|
|
3029
|
+
if (toRemove.length === 0) return;
|
|
3030
|
+
if (dryRun) {
|
|
3031
|
+
for (const { id, existing } of toRemove) {
|
|
3032
|
+
for (const rel of existing) {
|
|
3033
|
+
say(`[dry-run] would prune orphaned ${displayPath(root, path19.join(root, rel))} (${id})`);
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
const allAbsPaths = toRemove.flatMap(
|
|
3039
|
+
({ existing }) => existing.map((rel) => path19.join(root, rel))
|
|
3040
|
+
);
|
|
3041
|
+
await backupBeforePrune(root, allAbsPaths, say);
|
|
3042
|
+
for (const { id, existing } of toRemove) {
|
|
3043
|
+
for (const rel of existing) {
|
|
3044
|
+
const abs = path19.join(root, rel);
|
|
3045
|
+
await fs18.remove(abs);
|
|
3046
|
+
await pruneEmptyDir(path19.dirname(abs));
|
|
3047
|
+
say(`Pruned orphaned ${displayPath(root, abs)} (${id})`);
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
2937
3051
|
|
|
2938
3052
|
// src/refs/fetch-refs.ts
|
|
2939
3053
|
import path21 from "node:path";
|
|
@@ -3163,7 +3277,8 @@ async function runApply(options) {
|
|
|
3163
3277
|
}
|
|
3164
3278
|
const files = await writeClaudeFiles(root, isDryRun, selectedIds, {
|
|
3165
3279
|
refillConfig: options.refillConfig,
|
|
3166
|
-
force: options.force
|
|
3280
|
+
force: options.force,
|
|
3281
|
+
prune: options.prune
|
|
3167
3282
|
});
|
|
3168
3283
|
if (!isDryRun) {
|
|
3169
3284
|
try {
|
|
@@ -3198,6 +3313,364 @@ async function runApply(options) {
|
|
|
3198
3313
|
}
|
|
3199
3314
|
}
|
|
3200
3315
|
|
|
3316
|
+
// src/commands/backups.ts
|
|
3317
|
+
import path23 from "node:path";
|
|
3318
|
+
import fs21 from "fs-extra";
|
|
3319
|
+
|
|
3320
|
+
// src/utils/prompts.ts
|
|
3321
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3322
|
+
import readline from "node:readline/promises";
|
|
3323
|
+
async function ask(question) {
|
|
3324
|
+
const rl = readline.createInterface({ input, output });
|
|
3325
|
+
try {
|
|
3326
|
+
const answer = await rl.question(`${question}
|
|
3327
|
+
> `);
|
|
3328
|
+
return answer.trim();
|
|
3329
|
+
} finally {
|
|
3330
|
+
rl.close();
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
async function confirm(question) {
|
|
3334
|
+
const answer = (await ask(`${question} [y/N]`)).toLowerCase();
|
|
3335
|
+
return answer === "y" || answer === "yes";
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
// src/commands/backups.ts
|
|
3339
|
+
var LOCK_BACKUP_RE = /^haus\.lock\.\d+\.json$/;
|
|
3340
|
+
function classify(name) {
|
|
3341
|
+
if (LOCK_BACKUP_RE.test(name)) return "lock";
|
|
3342
|
+
if (name.startsWith("undo-")) return "undo";
|
|
3343
|
+
if (name.startsWith("prune-")) return "prune";
|
|
3344
|
+
return void 0;
|
|
3345
|
+
}
|
|
3346
|
+
async function listBackups(root) {
|
|
3347
|
+
const dir = hausPath(root, "backups");
|
|
3348
|
+
if (!await fs21.pathExists(dir)) return [];
|
|
3349
|
+
const names = await fs21.readdir(dir);
|
|
3350
|
+
const entries = [];
|
|
3351
|
+
for (const name of names) {
|
|
3352
|
+
const kind = classify(name);
|
|
3353
|
+
if (!kind) continue;
|
|
3354
|
+
const absPath = path23.join(dir, name);
|
|
3355
|
+
const stat = await fs21.lstat(absPath);
|
|
3356
|
+
if (stat.isSymbolicLink()) continue;
|
|
3357
|
+
const expectDirectory = kind !== "lock";
|
|
3358
|
+
if (stat.isDirectory() !== expectDirectory) continue;
|
|
3359
|
+
entries.push({ id: name, kind, absPath, mtimeMs: stat.mtimeMs });
|
|
3360
|
+
}
|
|
3361
|
+
entries.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
3362
|
+
return entries;
|
|
3363
|
+
}
|
|
3364
|
+
function formatAge(mtimeMs, now) {
|
|
3365
|
+
const minutes = Math.round(Math.max(0, now - mtimeMs) / 6e4);
|
|
3366
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
3367
|
+
const hours = Math.round(minutes / 60);
|
|
3368
|
+
if (hours < 48) return `${hours}h ago`;
|
|
3369
|
+
return `${Math.round(hours / 24)}d ago`;
|
|
3370
|
+
}
|
|
3371
|
+
async function runList(root) {
|
|
3372
|
+
const entries = await listBackups(root);
|
|
3373
|
+
if (entries.length === 0) {
|
|
3374
|
+
log("No backups found under .haus-workflow/backups/.");
|
|
3375
|
+
return;
|
|
3376
|
+
}
|
|
3377
|
+
const now = Date.now();
|
|
3378
|
+
const newestFirst = [...entries].reverse();
|
|
3379
|
+
for (const entry of newestFirst) {
|
|
3380
|
+
log(`${entry.id} [${entry.kind}] ${formatAge(entry.mtimeMs, now)}`);
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
async function isUnsafeWriteTarget(root, target) {
|
|
3384
|
+
const segments = path23.relative(root, target).split(path23.sep).filter(Boolean);
|
|
3385
|
+
let current = root;
|
|
3386
|
+
for (let i = 0; i < segments.length; i++) {
|
|
3387
|
+
current = path23.join(current, segments[i]);
|
|
3388
|
+
const isLastSegment = i === segments.length - 1;
|
|
3389
|
+
try {
|
|
3390
|
+
const stat = await fs21.lstat(current);
|
|
3391
|
+
if (isLastSegment) {
|
|
3392
|
+
if (!stat.isFile()) return true;
|
|
3393
|
+
} else if (!stat.isDirectory()) {
|
|
3394
|
+
return true;
|
|
3395
|
+
}
|
|
3396
|
+
} catch (err) {
|
|
3397
|
+
if (err.code === "ENOENT") return false;
|
|
3398
|
+
throw err;
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
return false;
|
|
3402
|
+
}
|
|
3403
|
+
async function lstatIfRegularFile(abs) {
|
|
3404
|
+
try {
|
|
3405
|
+
const stat = await fs21.lstat(abs);
|
|
3406
|
+
return stat.isFile() ? stat : null;
|
|
3407
|
+
} catch (err) {
|
|
3408
|
+
if (err.code === "ENOENT") return null;
|
|
3409
|
+
throw err;
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
async function restoreLockBackup(root, entry, yes) {
|
|
3413
|
+
const target = hausPath(root, "haus.lock.json");
|
|
3414
|
+
if (await isUnsafeWriteTarget(root, target)) {
|
|
3415
|
+
error(
|
|
3416
|
+
`Refusing to restore: ${target} (or a parent directory) is a symlink or other non-regular file \u2014 never written through.`
|
|
3417
|
+
);
|
|
3418
|
+
return false;
|
|
3419
|
+
}
|
|
3420
|
+
if (await fs21.pathExists(target)) {
|
|
3421
|
+
const targetStat = await fs21.stat(target);
|
|
3422
|
+
if (targetStat.mtimeMs > entry.mtimeMs) {
|
|
3423
|
+
warn(
|
|
3424
|
+
`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.`
|
|
3425
|
+
);
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
if (!yes) {
|
|
3429
|
+
const ok = await confirm(
|
|
3430
|
+
`Restore .haus-workflow/haus.lock.json from backup ${entry.id}? This overwrites the current lockfile.`
|
|
3431
|
+
);
|
|
3432
|
+
if (!ok) {
|
|
3433
|
+
log("Cancelled.");
|
|
3434
|
+
return false;
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
if (!await lstatIfRegularFile(entry.absPath) || await isUnsafeWriteTarget(root, target)) {
|
|
3438
|
+
error(`Refusing to restore: ${entry.id} or ${target} changed since being checked.`);
|
|
3439
|
+
return false;
|
|
3440
|
+
}
|
|
3441
|
+
await fs21.copy(entry.absPath, target, { overwrite: true });
|
|
3442
|
+
log(`Restored .haus-workflow/haus.lock.json from ${entry.id}.`);
|
|
3443
|
+
return true;
|
|
3444
|
+
}
|
|
3445
|
+
async function collectFilesRecursive(dir) {
|
|
3446
|
+
const files = [];
|
|
3447
|
+
const skippedLinks = [];
|
|
3448
|
+
const skippedOther = [];
|
|
3449
|
+
const names = await fs21.readdir(dir);
|
|
3450
|
+
for (const name of names) {
|
|
3451
|
+
const abs = path23.join(dir, name);
|
|
3452
|
+
const stat = await fs21.lstat(abs);
|
|
3453
|
+
if (stat.isSymbolicLink()) {
|
|
3454
|
+
skippedLinks.push(abs);
|
|
3455
|
+
continue;
|
|
3456
|
+
}
|
|
3457
|
+
if (stat.isDirectory()) {
|
|
3458
|
+
const nested = await collectFilesRecursive(abs);
|
|
3459
|
+
files.push(...nested.files);
|
|
3460
|
+
skippedLinks.push(...nested.skippedLinks);
|
|
3461
|
+
skippedOther.push(...nested.skippedOther);
|
|
3462
|
+
} else if (stat.isFile()) {
|
|
3463
|
+
files.push(abs);
|
|
3464
|
+
} else {
|
|
3465
|
+
skippedOther.push(abs);
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
return { files, skippedLinks, skippedOther };
|
|
3469
|
+
}
|
|
3470
|
+
async function restoreDirBackup(root, entry, yes) {
|
|
3471
|
+
const { files, skippedLinks, skippedOther } = await collectFilesRecursive(entry.absPath);
|
|
3472
|
+
if (skippedLinks.length > 0) {
|
|
3473
|
+
warn(
|
|
3474
|
+
`Skipped ${skippedLinks.length} symlink(s) inside backup ${entry.id} \u2014 never followed: ${skippedLinks.map((abs) => path23.relative(entry.absPath, abs)).join(", ")}`
|
|
3475
|
+
);
|
|
3476
|
+
}
|
|
3477
|
+
if (skippedOther.length > 0) {
|
|
3478
|
+
warn(
|
|
3479
|
+
`Skipped ${skippedOther.length} non-regular file(s) inside backup ${entry.id} (not restorable): ${skippedOther.map((abs) => path23.relative(entry.absPath, abs)).join(", ")}`
|
|
3480
|
+
);
|
|
3481
|
+
}
|
|
3482
|
+
if (files.length === 0) {
|
|
3483
|
+
log(`Backup ${entry.id} contains no files.`);
|
|
3484
|
+
return false;
|
|
3485
|
+
}
|
|
3486
|
+
const safeFiles = [];
|
|
3487
|
+
const unsafeTargets = [];
|
|
3488
|
+
for (const abs of files) {
|
|
3489
|
+
const rel = path23.relative(entry.absPath, abs);
|
|
3490
|
+
const target = path23.join(root, rel);
|
|
3491
|
+
if (await isUnsafeWriteTarget(root, target)) unsafeTargets.push(rel);
|
|
3492
|
+
else safeFiles.push(abs);
|
|
3493
|
+
}
|
|
3494
|
+
if (unsafeTargets.length > 0) {
|
|
3495
|
+
warn(
|
|
3496
|
+
`Skipped ${unsafeTargets.length} restore target(s) that are a symlink (or under one) rather than a plain path \u2014 never written through: ${unsafeTargets.join(", ")}`
|
|
3497
|
+
);
|
|
3498
|
+
}
|
|
3499
|
+
if (safeFiles.length === 0) {
|
|
3500
|
+
log(`Backup ${entry.id} has no restorable targets.`);
|
|
3501
|
+
return false;
|
|
3502
|
+
}
|
|
3503
|
+
const stale = [];
|
|
3504
|
+
const vanished = [];
|
|
3505
|
+
const toRestore = [];
|
|
3506
|
+
for (const abs of safeFiles) {
|
|
3507
|
+
const rel = path23.relative(entry.absPath, abs);
|
|
3508
|
+
const backupFileStat = await lstatIfRegularFile(abs);
|
|
3509
|
+
if (!backupFileStat) {
|
|
3510
|
+
vanished.push(rel);
|
|
3511
|
+
continue;
|
|
3512
|
+
}
|
|
3513
|
+
toRestore.push(abs);
|
|
3514
|
+
const target = path23.join(root, rel);
|
|
3515
|
+
if (await fs21.pathExists(target)) {
|
|
3516
|
+
const targetStat = await fs21.stat(target);
|
|
3517
|
+
if (targetStat.mtimeMs > backupFileStat.mtimeMs) {
|
|
3518
|
+
stale.push({
|
|
3519
|
+
rel,
|
|
3520
|
+
targetMtimeMs: targetStat.mtimeMs,
|
|
3521
|
+
backupMtimeMs: backupFileStat.mtimeMs
|
|
3522
|
+
});
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
if (vanished.length > 0) {
|
|
3527
|
+
warn(
|
|
3528
|
+
`Skipped ${vanished.length} backup file(s) that changed (no longer a plain file) since being listed: ${vanished.join(", ")}`
|
|
3529
|
+
);
|
|
3530
|
+
}
|
|
3531
|
+
if (toRestore.length === 0) {
|
|
3532
|
+
log(`Backup ${entry.id} has no restorable files left after re-validation.`);
|
|
3533
|
+
return false;
|
|
3534
|
+
}
|
|
3535
|
+
if (stale.length > 0) {
|
|
3536
|
+
const detail = stale.map(
|
|
3537
|
+
(s) => `${s.rel} (destination: ${new Date(s.targetMtimeMs).toISOString()}, backup: ${new Date(s.backupMtimeMs).toISOString()})`
|
|
3538
|
+
).join("; ");
|
|
3539
|
+
warn(
|
|
3540
|
+
`${stale.length} file(s) at the restore destination are newer than backup ${entry.id} and will be overwritten: ${detail}`
|
|
3541
|
+
);
|
|
3542
|
+
}
|
|
3543
|
+
if (!yes) {
|
|
3544
|
+
const ok = await confirm(
|
|
3545
|
+
`Restore ${toRestore.length} file(s) from backup ${entry.id} to their original paths?`
|
|
3546
|
+
);
|
|
3547
|
+
if (!ok) {
|
|
3548
|
+
log("Cancelled.");
|
|
3549
|
+
return false;
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
let restoredCount = 0;
|
|
3553
|
+
const skippedAtWrite = [];
|
|
3554
|
+
for (const abs of toRestore) {
|
|
3555
|
+
const rel = path23.relative(entry.absPath, abs);
|
|
3556
|
+
const target = path23.join(root, rel);
|
|
3557
|
+
if (!await lstatIfRegularFile(abs) || await isUnsafeWriteTarget(root, target)) {
|
|
3558
|
+
skippedAtWrite.push(rel);
|
|
3559
|
+
continue;
|
|
3560
|
+
}
|
|
3561
|
+
await fs21.ensureDir(path23.dirname(target));
|
|
3562
|
+
await fs21.copy(abs, target, { overwrite: true });
|
|
3563
|
+
restoredCount++;
|
|
3564
|
+
}
|
|
3565
|
+
if (skippedAtWrite.length > 0) {
|
|
3566
|
+
warn(
|
|
3567
|
+
`Skipped ${skippedAtWrite.length} file(s) that changed since being checked, right before restoring: ${skippedAtWrite.join(", ")}`
|
|
3568
|
+
);
|
|
3569
|
+
}
|
|
3570
|
+
log(`Restored ${restoredCount} file(s) from ${entry.id}.`);
|
|
3571
|
+
return restoredCount > 0;
|
|
3572
|
+
}
|
|
3573
|
+
async function runRestore(root, id, yes) {
|
|
3574
|
+
const entries = await listBackups(root);
|
|
3575
|
+
const entry = entries.find((e) => e.id === id);
|
|
3576
|
+
if (!entry) {
|
|
3577
|
+
error(`No backup found with id "${id}". Run \`haus backups list\` to see available backups.`);
|
|
3578
|
+
return false;
|
|
3579
|
+
}
|
|
3580
|
+
return entry.kind === "lock" ? restoreLockBackup(root, entry, yes) : restoreDirBackup(root, entry, yes);
|
|
3581
|
+
}
|
|
3582
|
+
async function runPrune(root, opts) {
|
|
3583
|
+
if (opts.olderThanDays === void 0 && opts.keep === void 0) {
|
|
3584
|
+
error("Specify --older-than <days> or --keep <n> \u2014 refusing to prune with no bound.");
|
|
3585
|
+
return false;
|
|
3586
|
+
}
|
|
3587
|
+
const entries = await listBackups(root);
|
|
3588
|
+
const now = Date.now();
|
|
3589
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
3590
|
+
if (opts.olderThanDays !== void 0) {
|
|
3591
|
+
const cutoff = now - opts.olderThanDays * 24 * 60 * 60 * 1e3;
|
|
3592
|
+
for (const entry of entries) if (entry.mtimeMs < cutoff) toRemove.add(entry.id);
|
|
3593
|
+
}
|
|
3594
|
+
if (opts.keep !== void 0) {
|
|
3595
|
+
const excess = entries.length - opts.keep;
|
|
3596
|
+
if (excess > 0) for (const entry of entries.slice(0, excess)) toRemove.add(entry.id);
|
|
3597
|
+
}
|
|
3598
|
+
if (toRemove.size === 0) {
|
|
3599
|
+
log("Nothing to prune.");
|
|
3600
|
+
return true;
|
|
3601
|
+
}
|
|
3602
|
+
const removeList = entries.filter((e) => toRemove.has(e.id));
|
|
3603
|
+
const keepCount = entries.length - removeList.length;
|
|
3604
|
+
if (!opts.yes) {
|
|
3605
|
+
const ok = await confirm(
|
|
3606
|
+
`Remove ${removeList.length} backup(s)?
|
|
3607
|
+
${removeList.map((e) => e.id).join("\n ")}
|
|
3608
|
+
Keeping ${keepCount}.`
|
|
3609
|
+
);
|
|
3610
|
+
if (!ok) {
|
|
3611
|
+
log("Cancelled.");
|
|
3612
|
+
return false;
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
const removed = [];
|
|
3616
|
+
const failed = [];
|
|
3617
|
+
for (const entry of removeList) {
|
|
3618
|
+
try {
|
|
3619
|
+
await fs21.remove(entry.absPath);
|
|
3620
|
+
removed.push(entry.id);
|
|
3621
|
+
} catch (err) {
|
|
3622
|
+
failed.push({ id: entry.id, message: err instanceof Error ? err.message : String(err) });
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
log(`Removed ${removed.length} backup(s), kept ${entries.length - removed.length}.`);
|
|
3626
|
+
if (failed.length > 0) {
|
|
3627
|
+
warn(
|
|
3628
|
+
`Failed to remove ${failed.length} backup(s): ${failed.map((f) => `${f.id} (${f.message})`).join(", ")}`
|
|
3629
|
+
);
|
|
3630
|
+
return false;
|
|
3631
|
+
}
|
|
3632
|
+
return true;
|
|
3633
|
+
}
|
|
3634
|
+
async function runBackups(action, opts = {}) {
|
|
3635
|
+
const root = opts.root ?? process.cwd();
|
|
3636
|
+
if (action === "list") {
|
|
3637
|
+
await runList(root);
|
|
3638
|
+
return;
|
|
3639
|
+
}
|
|
3640
|
+
if (action === "restore") {
|
|
3641
|
+
if (!opts.id) {
|
|
3642
|
+
error("Usage: haus backups restore <id>");
|
|
3643
|
+
process.exitCode = 1;
|
|
3644
|
+
return;
|
|
3645
|
+
}
|
|
3646
|
+
if (!await runRestore(root, opts.id, Boolean(opts.yes))) process.exitCode = 1;
|
|
3647
|
+
return;
|
|
3648
|
+
}
|
|
3649
|
+
let olderThanDays;
|
|
3650
|
+
if (opts.olderThan !== void 0) {
|
|
3651
|
+
const n = Number(opts.olderThan);
|
|
3652
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
3653
|
+
error(
|
|
3654
|
+
`Invalid --older-than value "${opts.olderThan}" \u2014 expected a non-negative number of days.`
|
|
3655
|
+
);
|
|
3656
|
+
process.exitCode = 1;
|
|
3657
|
+
return;
|
|
3658
|
+
}
|
|
3659
|
+
olderThanDays = n;
|
|
3660
|
+
}
|
|
3661
|
+
let keep;
|
|
3662
|
+
if (opts.keep !== void 0) {
|
|
3663
|
+
const n = Number(opts.keep);
|
|
3664
|
+
if (!Number.isFinite(n) || n < 0 || !Number.isInteger(n)) {
|
|
3665
|
+
error(`Invalid --keep value "${opts.keep}" \u2014 expected a non-negative integer.`);
|
|
3666
|
+
process.exitCode = 1;
|
|
3667
|
+
return;
|
|
3668
|
+
}
|
|
3669
|
+
keep = n;
|
|
3670
|
+
}
|
|
3671
|
+
if (!await runPrune(root, { olderThanDays, keep, yes: Boolean(opts.yes) })) process.exitCode = 1;
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3201
3674
|
// src/commands/catalog-audit.ts
|
|
3202
3675
|
function auditForbiddenTags(items) {
|
|
3203
3676
|
const failures = [];
|
|
@@ -3226,7 +3699,7 @@ async function runCatalogAudit() {
|
|
|
3226
3699
|
// src/commands/clone.ts
|
|
3227
3700
|
init_exec();
|
|
3228
3701
|
import { existsSync as existsSync2 } from "node:fs";
|
|
3229
|
-
import
|
|
3702
|
+
import path24 from "node:path";
|
|
3230
3703
|
var GIT_LOCATION_VARS = [
|
|
3231
3704
|
"GIT_DIR",
|
|
3232
3705
|
"GIT_WORK_TREE",
|
|
@@ -3246,15 +3719,50 @@ function repoNameFromUrl(url) {
|
|
|
3246
3719
|
const tail = trimmed.split(/[/:]/).pop() ?? "";
|
|
3247
3720
|
return tail || "repo";
|
|
3248
3721
|
}
|
|
3722
|
+
function normalizeGitUrl(url) {
|
|
3723
|
+
let s = url.trim();
|
|
3724
|
+
s = s.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "");
|
|
3725
|
+
s = s.replace(/^[^/@]+@/, "");
|
|
3726
|
+
s = s.replace(/:(?=[^/])/, "/");
|
|
3727
|
+
s = s.replace(/\/+$/, "");
|
|
3728
|
+
s = s.replace(/\.git$/i, "");
|
|
3729
|
+
return s.toLowerCase();
|
|
3730
|
+
}
|
|
3731
|
+
async function checkExistingTarget(target, url) {
|
|
3732
|
+
const isRepo = await runGit(["-C", target, "rev-parse", "--is-inside-work-tree"], {
|
|
3733
|
+
env: cloneEnv(),
|
|
3734
|
+
extendEnv: false
|
|
3735
|
+
});
|
|
3736
|
+
if (isRepo.exitCode !== 0) return { kind: "not-a-repo" };
|
|
3737
|
+
const origin = await runGit(["-C", target, "remote", "get-url", "origin"], {
|
|
3738
|
+
env: cloneEnv(),
|
|
3739
|
+
extendEnv: false
|
|
3740
|
+
});
|
|
3741
|
+
if (origin.exitCode !== 0) return { kind: "different-repo", origin: null };
|
|
3742
|
+
const originUrl = origin.stdout.trim();
|
|
3743
|
+
if (normalizeGitUrl(originUrl) === normalizeGitUrl(url)) return { kind: "matches" };
|
|
3744
|
+
return { kind: "different-repo", origin: originUrl };
|
|
3745
|
+
}
|
|
3249
3746
|
async function runClone(url, opts = {}) {
|
|
3250
3747
|
if (!url || !url.trim()) {
|
|
3251
3748
|
error("A git URL is required: `haus clone <url> [dir]`.");
|
|
3252
3749
|
process.exitCode = 1;
|
|
3253
3750
|
return;
|
|
3254
3751
|
}
|
|
3255
|
-
const target =
|
|
3752
|
+
const target = path24.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
3256
3753
|
if (existsSync2(target)) {
|
|
3257
|
-
|
|
3754
|
+
const check = await checkExistingTarget(target, url);
|
|
3755
|
+
if (check.kind === "matches") {
|
|
3756
|
+
log(`\u2022 ${path24.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
|
|
3757
|
+
return;
|
|
3758
|
+
}
|
|
3759
|
+
const message = check.kind === "not-a-repo" ? `${target} already exists and is not a git repository \u2014 refusing to clone ${url} there. Remove or rename it, or pass a different [dir].` : `${target} already exists as a different repository (origin: ${check.origin ?? "(no origin remote)"}) \u2014 refusing to clone ${url} there. Remove or rename it, or pass a different [dir].`;
|
|
3760
|
+
if (opts.dryRun) {
|
|
3761
|
+
warn(`[dry-run] ${message}`);
|
|
3762
|
+
return;
|
|
3763
|
+
}
|
|
3764
|
+
error(message);
|
|
3765
|
+
process.exitCode = 1;
|
|
3258
3766
|
return;
|
|
3259
3767
|
}
|
|
3260
3768
|
if (opts.dryRun) {
|
|
@@ -3325,8 +3833,8 @@ async function resolveBaseRef(root) {
|
|
|
3325
3833
|
}
|
|
3326
3834
|
|
|
3327
3835
|
// src/decisions/check.ts
|
|
3328
|
-
import
|
|
3329
|
-
import
|
|
3836
|
+
import path27 from "node:path";
|
|
3837
|
+
import fs24 from "fs-extra";
|
|
3330
3838
|
|
|
3331
3839
|
// src/decisions/diff.ts
|
|
3332
3840
|
init_exec();
|
|
@@ -3381,22 +3889,22 @@ function matchesAnyRegex(filePath, patterns) {
|
|
|
3381
3889
|
}
|
|
3382
3890
|
|
|
3383
3891
|
// src/decisions/paths.ts
|
|
3384
|
-
import
|
|
3385
|
-
import
|
|
3892
|
+
import path25 from "node:path";
|
|
3893
|
+
import fs22 from "fs-extra";
|
|
3386
3894
|
async function resolveDecisionsDir(root) {
|
|
3387
|
-
const primary =
|
|
3388
|
-
if (await
|
|
3389
|
-
const legacy =
|
|
3390
|
-
if (await
|
|
3895
|
+
const primary = path25.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
3896
|
+
if (await fs22.pathExists(primary)) return primary;
|
|
3897
|
+
const legacy = path25.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
3898
|
+
if (await fs22.pathExists(legacy)) return legacy;
|
|
3391
3899
|
return primary;
|
|
3392
3900
|
}
|
|
3393
3901
|
function relativeDecisionPath(decisionsDir, root) {
|
|
3394
|
-
return
|
|
3902
|
+
return path25.relative(root, decisionsDir).replace(/\\/g, "/");
|
|
3395
3903
|
}
|
|
3396
3904
|
|
|
3397
3905
|
// src/decisions/validate.ts
|
|
3398
|
-
import
|
|
3399
|
-
import
|
|
3906
|
+
import path26 from "node:path";
|
|
3907
|
+
import fs23 from "fs-extra";
|
|
3400
3908
|
function validateDecisionContent(content2) {
|
|
3401
3909
|
const errors = [];
|
|
3402
3910
|
for (const section of DECISIONS_TRIGGERS.requiredSections) {
|
|
@@ -3425,14 +3933,14 @@ function decisionNumberFromFile(name) {
|
|
|
3425
3933
|
return match?.[1] ?? null;
|
|
3426
3934
|
}
|
|
3427
3935
|
async function validateIndexSync(decisionsDir, decisionFiles) {
|
|
3428
|
-
const readmePath =
|
|
3429
|
-
if (!await
|
|
3936
|
+
const readmePath = path26.join(decisionsDir, "README.md");
|
|
3937
|
+
if (!await fs23.pathExists(readmePath)) {
|
|
3430
3938
|
return ["docs/decisions/README.md is missing"];
|
|
3431
3939
|
}
|
|
3432
3940
|
const readme = await readText(readmePath) ?? "";
|
|
3433
3941
|
const errors = [];
|
|
3434
3942
|
for (const file of decisionFiles) {
|
|
3435
|
-
const num = decisionNumberFromFile(
|
|
3943
|
+
const num = decisionNumberFromFile(path26.basename(file));
|
|
3436
3944
|
if (num && !indexReferencesNumber(readme, num)) {
|
|
3437
3945
|
errors.push(`README.md missing index row for ADR-${num}`);
|
|
3438
3946
|
}
|
|
@@ -3486,22 +3994,22 @@ async function checkSatisfaction(input2) {
|
|
|
3486
3994
|
const errors = [];
|
|
3487
3995
|
const decisionsPrefix = `${input2.decisionsRel}/`;
|
|
3488
3996
|
const newDecisionFiles = input2.changedFiles.filter(
|
|
3489
|
-
(f) => f.startsWith(decisionsPrefix) && isDecisionFileName(
|
|
3997
|
+
(f) => f.startsWith(decisionsPrefix) && isDecisionFileName(path27.basename(f))
|
|
3490
3998
|
);
|
|
3491
3999
|
if (newDecisionFiles.length === 0) {
|
|
3492
4000
|
errors.push(`no new or updated decision under ${input2.decisionsRel}/`);
|
|
3493
4001
|
return errors;
|
|
3494
4002
|
}
|
|
3495
|
-
const decisionsDir =
|
|
4003
|
+
const decisionsDir = path27.join(input2.root, input2.decisionsRel);
|
|
3496
4004
|
for (const rel of newDecisionFiles) {
|
|
3497
|
-
const abs =
|
|
4005
|
+
const abs = path27.join(input2.root, rel);
|
|
3498
4006
|
const content2 = await readText(abs) ?? "";
|
|
3499
4007
|
const valid = validateDecisionContent(content2);
|
|
3500
4008
|
if (!valid.ok) errors.push(...valid.errors.map((e) => `${rel}: ${e}`));
|
|
3501
4009
|
}
|
|
3502
4010
|
const indexErrors = await validateIndexSync(
|
|
3503
4011
|
decisionsDir,
|
|
3504
|
-
newDecisionFiles.map((f) =>
|
|
4012
|
+
newDecisionFiles.map((f) => path27.basename(f))
|
|
3505
4013
|
);
|
|
3506
4014
|
errors.push(...indexErrors);
|
|
3507
4015
|
return errors;
|
|
@@ -3545,8 +4053,8 @@ ${opts.commitMessages ?? ""}`;
|
|
|
3545
4053
|
changedFiles: stats.files
|
|
3546
4054
|
});
|
|
3547
4055
|
if (satisfactionErrors.length > 0 && decisionsRel !== legacyRel) {
|
|
3548
|
-
const legacyExists = await
|
|
3549
|
-
if (legacyExists && !await
|
|
4056
|
+
const legacyExists = await fs24.pathExists(path27.join(root, legacyRel));
|
|
4057
|
+
if (legacyExists && !await fs24.pathExists(decisionsDir)) {
|
|
3550
4058
|
satisfactionErrors = await checkSatisfaction({
|
|
3551
4059
|
root,
|
|
3552
4060
|
decisionsRel: legacyRel,
|
|
@@ -3566,8 +4074,8 @@ ${opts.commitMessages ?? ""}`;
|
|
|
3566
4074
|
return { triggered: true, satisfied: false, reasons, securityTriggered };
|
|
3567
4075
|
}
|
|
3568
4076
|
async function listDecisionNumbers(decisionsDir) {
|
|
3569
|
-
if (!await
|
|
3570
|
-
const entries = await
|
|
4077
|
+
if (!await fs24.pathExists(decisionsDir)) return [];
|
|
4078
|
+
const entries = await fs24.readdir(decisionsDir);
|
|
3571
4079
|
return entries.map((name) => decisionNumberFromFile(name)).filter((n) => n != null).map((n) => Number(n));
|
|
3572
4080
|
}
|
|
3573
4081
|
|
|
@@ -3609,7 +4117,7 @@ async function runDecisionsGuard(options = {}) {
|
|
|
3609
4117
|
}
|
|
3610
4118
|
|
|
3611
4119
|
// src/commands/decisions.ts
|
|
3612
|
-
import
|
|
4120
|
+
import path28 from "node:path";
|
|
3613
4121
|
|
|
3614
4122
|
// src/decisions/next-number.ts
|
|
3615
4123
|
async function nextDecisionNumber(root) {
|
|
@@ -3720,7 +4228,7 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
3720
4228
|
return;
|
|
3721
4229
|
}
|
|
3722
4230
|
if (sub === "validate") {
|
|
3723
|
-
const target = arg ?
|
|
4231
|
+
const target = arg ? path28.resolve(root, arg) : void 0;
|
|
3724
4232
|
if (!target) {
|
|
3725
4233
|
error("usage: haus decisions validate <path-to-decision.md>");
|
|
3726
4234
|
process.exitCode = 1;
|
|
@@ -3737,18 +4245,18 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
3737
4245
|
}
|
|
3738
4246
|
|
|
3739
4247
|
// src/commands/doctor.ts
|
|
3740
|
-
import
|
|
3741
|
-
import
|
|
4248
|
+
import path35 from "node:path";
|
|
4249
|
+
import fs28 from "fs-extra";
|
|
3742
4250
|
|
|
3743
4251
|
// src/decisions/doctor.ts
|
|
3744
|
-
import
|
|
3745
|
-
import
|
|
4252
|
+
import path29 from "node:path";
|
|
4253
|
+
import fs25 from "fs-extra";
|
|
3746
4254
|
async function auditDecisionsLayout(root) {
|
|
3747
4255
|
const findings = [];
|
|
3748
|
-
const primary =
|
|
3749
|
-
const legacy =
|
|
3750
|
-
const hasPrimary = await
|
|
3751
|
-
const hasLegacy = await
|
|
4256
|
+
const primary = path29.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
4257
|
+
const legacy = path29.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
4258
|
+
const hasPrimary = await fs25.pathExists(primary);
|
|
4259
|
+
const hasLegacy = await fs25.pathExists(legacy);
|
|
3752
4260
|
if (!hasPrimary && hasLegacy) {
|
|
3753
4261
|
findings.push({
|
|
3754
4262
|
level: "advisory",
|
|
@@ -3761,8 +4269,8 @@ async function auditDecisionsLayout(root) {
|
|
|
3761
4269
|
if (!hasPrimary) return findings;
|
|
3762
4270
|
const decisionsDir = await resolveDecisionsDir(root);
|
|
3763
4271
|
const rel = relativeDecisionPath(decisionsDir, root);
|
|
3764
|
-
const readmePath =
|
|
3765
|
-
if (!await
|
|
4272
|
+
const readmePath = path29.join(decisionsDir, "README.md");
|
|
4273
|
+
if (!await fs25.pathExists(readmePath)) {
|
|
3766
4274
|
findings.push({
|
|
3767
4275
|
level: "advisory",
|
|
3768
4276
|
line: `- DECISIONS: missing ${rel}/README.md index`,
|
|
@@ -3772,7 +4280,7 @@ async function auditDecisionsLayout(root) {
|
|
|
3772
4280
|
return findings;
|
|
3773
4281
|
}
|
|
3774
4282
|
const readme = await readText(readmePath);
|
|
3775
|
-
const entries = (await
|
|
4283
|
+
const entries = (await fs25.readdir(decisionsDir)).filter(
|
|
3776
4284
|
(f) => f.endsWith(".md") && f !== "README.md"
|
|
3777
4285
|
);
|
|
3778
4286
|
for (const file of entries) {
|
|
@@ -3786,7 +4294,7 @@ async function auditDecisionsLayout(root) {
|
|
|
3786
4294
|
});
|
|
3787
4295
|
}
|
|
3788
4296
|
}
|
|
3789
|
-
const claude = await readText(
|
|
4297
|
+
const claude = await readText(path29.join(root, "CLAUDE.md"));
|
|
3790
4298
|
if (claude && !claude.includes(`@${rel}/README.md`)) {
|
|
3791
4299
|
findings.push({
|
|
3792
4300
|
level: "advisory",
|
|
@@ -3817,11 +4325,11 @@ async function auditDecisionsLayout(root) {
|
|
|
3817
4325
|
}
|
|
3818
4326
|
|
|
3819
4327
|
// src/scanner/read-context.ts
|
|
3820
|
-
import
|
|
3821
|
-
import
|
|
4328
|
+
import path34 from "node:path";
|
|
4329
|
+
import fs27 from "fs-extra";
|
|
3822
4330
|
|
|
3823
4331
|
// src/scanner/scan-project.ts
|
|
3824
|
-
import
|
|
4332
|
+
import path33 from "node:path";
|
|
3825
4333
|
|
|
3826
4334
|
// src/utils/versions.ts
|
|
3827
4335
|
import semver from "semver";
|
|
@@ -3843,8 +4351,8 @@ function compareVersions(a, b) {
|
|
|
3843
4351
|
}
|
|
3844
4352
|
|
|
3845
4353
|
// src/scanner/detect-package-manager.ts
|
|
3846
|
-
import
|
|
3847
|
-
import
|
|
4354
|
+
import path30 from "node:path";
|
|
4355
|
+
import fs26 from "fs-extra";
|
|
3848
4356
|
function detectPackageManager(root, packageManagerField) {
|
|
3849
4357
|
const field = String(packageManagerField ?? "").trim();
|
|
3850
4358
|
if (field.startsWith("yarn@")) {
|
|
@@ -3862,9 +4370,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
3862
4370
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
3863
4371
|
return "unknown";
|
|
3864
4372
|
}
|
|
3865
|
-
if (
|
|
3866
|
-
if (
|
|
3867
|
-
if (
|
|
4373
|
+
if (fs26.existsSync(path30.join(root, "yarn.lock"))) return "yarn";
|
|
4374
|
+
if (fs26.existsSync(path30.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
4375
|
+
if (fs26.existsSync(path30.join(root, "package-lock.json"))) return "npm";
|
|
3868
4376
|
return "unknown";
|
|
3869
4377
|
}
|
|
3870
4378
|
|
|
@@ -4066,7 +4574,7 @@ function runDetection(ctx, rules = STACK_RULES) {
|
|
|
4066
4574
|
}
|
|
4067
4575
|
|
|
4068
4576
|
// src/scanner/detection.ts
|
|
4069
|
-
import
|
|
4577
|
+
import path31 from "node:path";
|
|
4070
4578
|
var UNSUPPORTED_MARKERS = {
|
|
4071
4579
|
"requirements.txt": "python",
|
|
4072
4580
|
"pyproject.toml": "python",
|
|
@@ -4120,14 +4628,14 @@ function finalizeRoles(registryRoles, deps, files) {
|
|
|
4120
4628
|
function collectUnsupportedSignals(files) {
|
|
4121
4629
|
return [
|
|
4122
4630
|
...new Set(
|
|
4123
|
-
files.map((f) => UNSUPPORTED_MARKERS[
|
|
4631
|
+
files.map((f) => UNSUPPORTED_MARKERS[path31.basename(f)]).filter((s) => Boolean(s))
|
|
4124
4632
|
)
|
|
4125
4633
|
].sort();
|
|
4126
4634
|
}
|
|
4127
4635
|
|
|
4128
4636
|
// src/scanner/render.ts
|
|
4129
4637
|
import { readFile } from "node:fs/promises";
|
|
4130
|
-
import
|
|
4638
|
+
import path32 from "node:path";
|
|
4131
4639
|
async function buildContentBlob(root, files) {
|
|
4132
4640
|
const candidates = files.filter(
|
|
4133
4641
|
(f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
|
|
@@ -4135,7 +4643,7 @@ async function buildContentBlob(root, files) {
|
|
|
4135
4643
|
const slice = candidates.slice(0, 300);
|
|
4136
4644
|
const parts = await mapWithConcurrency(slice, async (rel) => {
|
|
4137
4645
|
try {
|
|
4138
|
-
return await readFile(
|
|
4646
|
+
return await readFile(path32.join(root, rel), "utf8");
|
|
4139
4647
|
} catch {
|
|
4140
4648
|
return "";
|
|
4141
4649
|
}
|
|
@@ -4222,8 +4730,8 @@ var SAFE_FILES = [
|
|
|
4222
4730
|
"Gemfile"
|
|
4223
4731
|
];
|
|
4224
4732
|
async function scanProject(root) {
|
|
4225
|
-
const pkg = await readJson(
|
|
4226
|
-
const composer = await readJson(
|
|
4733
|
+
const pkg = await readJson(path33.join(root, "package.json"));
|
|
4734
|
+
const composer = await readJson(path33.join(root, "composer.json"));
|
|
4227
4735
|
const files = await listFiles(root, SAFE_FILES);
|
|
4228
4736
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
4229
4737
|
const deps = dependencySet(pkg, composer);
|
|
@@ -4250,7 +4758,7 @@ async function scanProject(root) {
|
|
|
4250
4758
|
const context = {
|
|
4251
4759
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4252
4760
|
root,
|
|
4253
|
-
repoName: String(pkg?.name ??
|
|
4761
|
+
repoName: String(pkg?.name ?? path33.basename(root)),
|
|
4254
4762
|
packageManager,
|
|
4255
4763
|
repoRoles: roles,
|
|
4256
4764
|
detectedStacks: stacks,
|
|
@@ -4268,9 +4776,9 @@ async function scanProject(root) {
|
|
|
4268
4776
|
|
|
4269
4777
|
// src/scanner/read-context.ts
|
|
4270
4778
|
async function isCacheFresh(root, cachePath) {
|
|
4271
|
-
const pkgJsonPath =
|
|
4272
|
-
if (!await
|
|
4273
|
-
const [cacheStat, pkgStat] = await Promise.all([
|
|
4779
|
+
const pkgJsonPath = path34.join(root, "package.json");
|
|
4780
|
+
if (!await fs27.pathExists(pkgJsonPath)) return true;
|
|
4781
|
+
const [cacheStat, pkgStat] = await Promise.all([fs27.stat(cachePath), fs27.stat(pkgJsonPath)]);
|
|
4274
4782
|
return cacheStat.mtimeMs >= pkgStat.mtimeMs;
|
|
4275
4783
|
}
|
|
4276
4784
|
async function readContextOrScan(root) {
|
|
@@ -4369,7 +4877,7 @@ async function runDoctor(options) {
|
|
|
4369
4877
|
} else {
|
|
4370
4878
|
ok(`- HOOKS OK: ${hooks.message}`);
|
|
4371
4879
|
}
|
|
4372
|
-
const rootClaudeMdPath =
|
|
4880
|
+
const rootClaudeMdPath = path35.join(root, "CLAUDE.md");
|
|
4373
4881
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
4374
4882
|
if (!rootClaudeMdContent) {
|
|
4375
4883
|
flag(
|
|
@@ -4397,7 +4905,7 @@ async function runDoctor(options) {
|
|
|
4397
4905
|
const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
|
|
4398
4906
|
const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
|
|
4399
4907
|
for (const target of importTargets) {
|
|
4400
|
-
if (!await
|
|
4908
|
+
if (!await fs28.pathExists(hausPath(root, target))) {
|
|
4401
4909
|
flag(
|
|
4402
4910
|
`- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
|
|
4403
4911
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -4408,7 +4916,7 @@ async function runDoctor(options) {
|
|
|
4408
4916
|
}
|
|
4409
4917
|
}
|
|
4410
4918
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
4411
|
-
const workflowExists = await
|
|
4919
|
+
const workflowExists = await fs28.pathExists(workflowPath);
|
|
4412
4920
|
if (!workflowExists) {
|
|
4413
4921
|
flag(
|
|
4414
4922
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -4423,15 +4931,15 @@ async function runDoctor(options) {
|
|
|
4423
4931
|
} else {
|
|
4424
4932
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
4425
4933
|
const bodyContent = workflowContent.slice(firstLine.length + 1);
|
|
4426
|
-
const cachePath =
|
|
4427
|
-
const bundledPath =
|
|
4934
|
+
const cachePath = path35.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
4935
|
+
const bundledPath = path35.join(
|
|
4428
4936
|
packageRoot(),
|
|
4429
4937
|
"library",
|
|
4430
4938
|
"global",
|
|
4431
4939
|
"templates",
|
|
4432
4940
|
"agentic-workflow-standard.md"
|
|
4433
4941
|
);
|
|
4434
|
-
const templatePath = await
|
|
4942
|
+
const templatePath = await fs28.pathExists(cachePath) ? cachePath : bundledPath;
|
|
4435
4943
|
const templateContent = await readText(templatePath);
|
|
4436
4944
|
const verdict = checkManagedTamper(bodyContent, storedHashMatch?.[1], templateContent ?? null);
|
|
4437
4945
|
if (verdict.status === "tampered" || verdict.status === "legacy-diverged") {
|
|
@@ -4452,7 +4960,7 @@ async function runDoctor(options) {
|
|
|
4452
4960
|
}
|
|
4453
4961
|
}
|
|
4454
4962
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
4455
|
-
const workflowConfigExists = await
|
|
4963
|
+
const workflowConfigExists = await fs28.pathExists(workflowConfigPath);
|
|
4456
4964
|
if (!workflowConfigExists) {
|
|
4457
4965
|
flag(
|
|
4458
4966
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -4460,7 +4968,7 @@ async function runDoctor(options) {
|
|
|
4460
4968
|
"haus apply --write"
|
|
4461
4969
|
);
|
|
4462
4970
|
} else {
|
|
4463
|
-
const cfg = await
|
|
4971
|
+
const cfg = await fs28.readFile(workflowConfigPath, "utf8");
|
|
4464
4972
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
4465
4973
|
if (unfilled > 0) {
|
|
4466
4974
|
suggest(
|
|
@@ -4473,15 +4981,17 @@ async function runDoctor(options) {
|
|
|
4473
4981
|
}
|
|
4474
4982
|
}
|
|
4475
4983
|
if (workflowExists) {
|
|
4476
|
-
const prettierIgnore = await readText(
|
|
4477
|
-
|
|
4984
|
+
const prettierIgnore = await readText(path35.join(root, ".prettierignore")) ?? "";
|
|
4985
|
+
const lines = prettierIgnore.split("\n");
|
|
4986
|
+
const missing = IGNORED_PATHS.filter((p) => !prettierIgnoreProtects(lines, p));
|
|
4987
|
+
if (missing.length > 0) {
|
|
4478
4988
|
flag(
|
|
4479
|
-
|
|
4989
|
+
`- .prettierignore: not protecting ${missing.join(", ")} (run \`haus apply --write\`)`,
|
|
4480
4990
|
'The formatter may reformat managed files and trigger false "modified locally" reports',
|
|
4481
4991
|
"haus apply --write"
|
|
4482
4992
|
);
|
|
4483
4993
|
} else {
|
|
4484
|
-
ok(
|
|
4994
|
+
ok(`- .prettierignore: protects ${IGNORED_PATHS.join(" and ")}`);
|
|
4485
4995
|
}
|
|
4486
4996
|
}
|
|
4487
4997
|
for (const finding of await auditDecisionsLayout(root)) {
|
|
@@ -4510,9 +5020,9 @@ async function runDoctor(options) {
|
|
|
4510
5020
|
if (recommendation) {
|
|
4511
5021
|
const lock = await readJson(hausPath(root, "haus.lock.json"));
|
|
4512
5022
|
const recommendedIds = new Set(
|
|
4513
|
-
recommendation.recommended.map((r) => r.id)
|
|
5023
|
+
recommendation.recommended.map((r) => r.id).filter((id) => typeof id === "string")
|
|
4514
5024
|
);
|
|
4515
|
-
const orphaned = (lock ?? []).map((entry) => entry.id)
|
|
5025
|
+
const orphaned = findOrphanedLockEntries(lock ?? [], recommendedIds).map((entry) => entry.id);
|
|
4516
5026
|
if (orphaned.length > 0) {
|
|
4517
5027
|
suggest(
|
|
4518
5028
|
`- CATALOG ITEMS: ${orphaned.length} installed item(s) no longer in the current recommendation (${orphaned.join(", ")})`,
|
|
@@ -4521,7 +5031,7 @@ async function runDoctor(options) {
|
|
|
4521
5031
|
);
|
|
4522
5032
|
}
|
|
4523
5033
|
}
|
|
4524
|
-
const pkgJson = await readJson(
|
|
5034
|
+
const pkgJson = await readJson(path35.join(packageRoot(), "package.json"));
|
|
4525
5035
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
4526
5036
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
4527
5037
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -4779,8 +5289,8 @@ async function runGuard(kind, _options) {
|
|
|
4779
5289
|
}
|
|
4780
5290
|
|
|
4781
5291
|
// src/commands/init.ts
|
|
4782
|
-
import
|
|
4783
|
-
import
|
|
5292
|
+
import path36 from "node:path";
|
|
5293
|
+
import fs29 from "fs-extra";
|
|
4784
5294
|
|
|
4785
5295
|
// src/recommender/git-signal.ts
|
|
4786
5296
|
init_exec();
|
|
@@ -5283,24 +5793,6 @@ async function runSetupCore(root, opts) {
|
|
|
5283
5793
|
return { ...baseResult, hooksOk: hooksOkAfter, written: files };
|
|
5284
5794
|
}
|
|
5285
5795
|
|
|
5286
|
-
// src/utils/prompts.ts
|
|
5287
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
5288
|
-
import readline from "node:readline/promises";
|
|
5289
|
-
async function ask(question) {
|
|
5290
|
-
const rl = readline.createInterface({ input, output });
|
|
5291
|
-
try {
|
|
5292
|
-
const answer = await rl.question(`${question}
|
|
5293
|
-
> `);
|
|
5294
|
-
return answer.trim();
|
|
5295
|
-
} finally {
|
|
5296
|
-
rl.close();
|
|
5297
|
-
}
|
|
5298
|
-
}
|
|
5299
|
-
async function confirm(question) {
|
|
5300
|
-
const answer = (await ask(`${question} [y/N]`)).toLowerCase();
|
|
5301
|
-
return answer === "y" || answer === "yes";
|
|
5302
|
-
}
|
|
5303
|
-
|
|
5304
5796
|
// src/commands/setup-project.ts
|
|
5305
5797
|
async function runSetupProject(options) {
|
|
5306
5798
|
const root = process.cwd();
|
|
@@ -5315,8 +5807,8 @@ async function runSetupProject(options) {
|
|
|
5315
5807
|
// src/commands/init.ts
|
|
5316
5808
|
async function runInit(options) {
|
|
5317
5809
|
const root = process.cwd();
|
|
5318
|
-
const hausDir =
|
|
5319
|
-
const alreadyInit = await
|
|
5810
|
+
const hausDir = path36.join(root, ".haus-workflow");
|
|
5811
|
+
const alreadyInit = await fs29.pathExists(hausDir);
|
|
5320
5812
|
if (alreadyInit) {
|
|
5321
5813
|
log("Haus AI already initialized in this project.");
|
|
5322
5814
|
log("Run `haus setup-project` to reconfigure.");
|
|
@@ -5328,8 +5820,8 @@ async function runInit(options) {
|
|
|
5328
5820
|
|
|
5329
5821
|
// src/install/apply.ts
|
|
5330
5822
|
import crypto2 from "node:crypto";
|
|
5331
|
-
import
|
|
5332
|
-
import
|
|
5823
|
+
import path37 from "node:path";
|
|
5824
|
+
import fs30 from "fs-extra";
|
|
5333
5825
|
|
|
5334
5826
|
// src/install/header.ts
|
|
5335
5827
|
var MD_PREFIX = "<!-- HAUS-MANAGED";
|
|
@@ -5415,57 +5907,57 @@ function hashContent(content2) {
|
|
|
5415
5907
|
}
|
|
5416
5908
|
function sourceVersion() {
|
|
5417
5909
|
try {
|
|
5418
|
-
const pkgPath =
|
|
5419
|
-
const pkg = JSON.parse(
|
|
5910
|
+
const pkgPath = path37.join(packageRoot(), "package.json");
|
|
5911
|
+
const pkg = JSON.parse(fs30.readFileSync(pkgPath, "utf8"));
|
|
5420
5912
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
5421
5913
|
} catch {
|
|
5422
5914
|
return "haus@0.0.0";
|
|
5423
5915
|
}
|
|
5424
5916
|
}
|
|
5425
5917
|
function globalSrcDir() {
|
|
5426
|
-
return
|
|
5918
|
+
return path37.join(packageRoot(), "library", "global");
|
|
5427
5919
|
}
|
|
5428
5920
|
function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
5429
|
-
const skillDir =
|
|
5921
|
+
const skillDir = path37.join(skillsDir, skillName);
|
|
5430
5922
|
const entries = [];
|
|
5431
|
-
for (const rel of
|
|
5923
|
+
for (const rel of fs30.readdirSync(skillDir, { recursive: true })) {
|
|
5432
5924
|
if (rel === "SKILL.md") continue;
|
|
5433
|
-
const abs =
|
|
5434
|
-
if (!
|
|
5435
|
-
const posixRel = rel.split(
|
|
5925
|
+
const abs = path37.join(skillDir, rel);
|
|
5926
|
+
if (!fs30.statSync(abs).isFile()) continue;
|
|
5927
|
+
const posixRel = rel.split(path37.sep).join("/");
|
|
5436
5928
|
entries.push({
|
|
5437
5929
|
stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
|
|
5438
|
-
srcRelPath:
|
|
5439
|
-
destPath:
|
|
5930
|
+
srcRelPath: path37.join("library", "global", "skills", skillName, rel),
|
|
5931
|
+
destPath: path37.join(claudeDir, "skills", skillName, rel)
|
|
5440
5932
|
});
|
|
5441
5933
|
}
|
|
5442
5934
|
return entries;
|
|
5443
5935
|
}
|
|
5444
5936
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
5445
5937
|
const entries = [];
|
|
5446
|
-
const skillsDir =
|
|
5447
|
-
if (
|
|
5448
|
-
for (const skillName of
|
|
5449
|
-
const skillFile =
|
|
5450
|
-
if (
|
|
5938
|
+
const skillsDir = path37.join(srcDir, "skills");
|
|
5939
|
+
if (fs30.pathExistsSync(skillsDir)) {
|
|
5940
|
+
for (const skillName of fs30.readdirSync(skillsDir)) {
|
|
5941
|
+
const skillFile = path37.join(skillsDir, skillName, "SKILL.md");
|
|
5942
|
+
if (fs30.pathExistsSync(skillFile)) {
|
|
5451
5943
|
entries.push({
|
|
5452
5944
|
stableId: `skill.${skillName}`,
|
|
5453
|
-
srcRelPath:
|
|
5454
|
-
destPath:
|
|
5945
|
+
srcRelPath: path37.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
5946
|
+
destPath: path37.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
5455
5947
|
});
|
|
5456
5948
|
entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
|
|
5457
5949
|
}
|
|
5458
5950
|
}
|
|
5459
5951
|
}
|
|
5460
|
-
const commandsDir =
|
|
5461
|
-
if (
|
|
5462
|
-
for (const fileName of
|
|
5952
|
+
const commandsDir = path37.join(srcDir, "commands");
|
|
5953
|
+
if (fs30.pathExistsSync(commandsDir)) {
|
|
5954
|
+
for (const fileName of fs30.readdirSync(commandsDir)) {
|
|
5463
5955
|
if (!fileName.endsWith(".md")) continue;
|
|
5464
5956
|
const commandName = fileName.slice(0, -".md".length);
|
|
5465
5957
|
entries.push({
|
|
5466
5958
|
stableId: `command.${commandName}`,
|
|
5467
|
-
srcRelPath:
|
|
5468
|
-
destPath:
|
|
5959
|
+
srcRelPath: path37.join("library", "global", "commands", fileName),
|
|
5960
|
+
destPath: path37.join(claudeDir, "commands", fileName)
|
|
5469
5961
|
});
|
|
5470
5962
|
}
|
|
5471
5963
|
}
|
|
@@ -5491,7 +5983,7 @@ async function applyInstall(options = {}) {
|
|
|
5491
5983
|
};
|
|
5492
5984
|
const manifestFiles = [];
|
|
5493
5985
|
for (const entry of sourceFiles) {
|
|
5494
|
-
const srcPath =
|
|
5986
|
+
const srcPath = path37.join(packageRoot(), entry.srcRelPath);
|
|
5495
5987
|
const rawContent = await readText(srcPath);
|
|
5496
5988
|
if (rawContent === void 0) {
|
|
5497
5989
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -5511,7 +6003,7 @@ async function applyInstall(options = {}) {
|
|
|
5511
6003
|
}
|
|
5512
6004
|
continue;
|
|
5513
6005
|
}
|
|
5514
|
-
const destExists =
|
|
6006
|
+
const destExists = fs30.pathExistsSync(entry.destPath);
|
|
5515
6007
|
if (destExists) {
|
|
5516
6008
|
const currentContent = await readText(entry.destPath);
|
|
5517
6009
|
if (currentContent !== void 0) {
|
|
@@ -5551,7 +6043,7 @@ async function applyInstall(options = {}) {
|
|
|
5551
6043
|
}
|
|
5552
6044
|
let mergedSettings;
|
|
5553
6045
|
if (!check) {
|
|
5554
|
-
const fragmentPath =
|
|
6046
|
+
const fragmentPath = path37.join(srcDir, "settings-fragments", "hooks.json");
|
|
5555
6047
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
5556
6048
|
const settings = await readSettings();
|
|
5557
6049
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -5565,13 +6057,13 @@ async function applyInstall(options = {}) {
|
|
|
5565
6057
|
const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
|
|
5566
6058
|
for (const entry of existingManifest.files) {
|
|
5567
6059
|
if (currentDestPaths.has(entry.destPath)) continue;
|
|
5568
|
-
if (!
|
|
6060
|
+
if (!fs30.pathExistsSync(entry.destPath)) continue;
|
|
5569
6061
|
const content2 = await readText(entry.destPath);
|
|
5570
6062
|
if (!content2) continue;
|
|
5571
6063
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
5572
6064
|
const currentHash = hashContent(content2);
|
|
5573
6065
|
if (hasHeader && currentHash === entry.hash) {
|
|
5574
|
-
if (!dryRun) await
|
|
6066
|
+
if (!dryRun) await fs30.remove(entry.destPath);
|
|
5575
6067
|
result.deleted.push(entry.destPath);
|
|
5576
6068
|
} else {
|
|
5577
6069
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -5740,8 +6232,8 @@ async function runRefresh() {
|
|
|
5740
6232
|
}
|
|
5741
6233
|
|
|
5742
6234
|
// src/install/scaffold.ts
|
|
5743
|
-
import
|
|
5744
|
-
import
|
|
6235
|
+
import path38 from "node:path";
|
|
6236
|
+
import fs31 from "fs-extra";
|
|
5745
6237
|
async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
5746
6238
|
const scaffolded = [];
|
|
5747
6239
|
const skipped = [];
|
|
@@ -5754,27 +6246,27 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
5754
6246
|
skipped.push(name);
|
|
5755
6247
|
if (outcome === "skipped-exists") skippedExisting.push(name);
|
|
5756
6248
|
};
|
|
5757
|
-
const realRoot = await
|
|
6249
|
+
const realRoot = await fs31.realpath(catalogRoot).catch(() => null);
|
|
5758
6250
|
for (const item of items) {
|
|
5759
6251
|
if (item.type !== "config") continue;
|
|
5760
|
-
const sourcePath =
|
|
5761
|
-
const rel =
|
|
5762
|
-
const escapes = rel === "" ||
|
|
6252
|
+
const sourcePath = path38.resolve(catalogRoot, item.path);
|
|
6253
|
+
const rel = path38.relative(catalogRoot, sourcePath);
|
|
6254
|
+
const escapes = rel === "" || path38.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
|
|
5763
6255
|
if (escapes) {
|
|
5764
6256
|
warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
|
|
5765
6257
|
continue;
|
|
5766
6258
|
}
|
|
5767
|
-
const realSource = await
|
|
6259
|
+
const realSource = await fs31.realpath(sourcePath).catch(() => null);
|
|
5768
6260
|
if (!realRoot || !realSource) {
|
|
5769
6261
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
5770
6262
|
continue;
|
|
5771
6263
|
}
|
|
5772
|
-
const realRel =
|
|
5773
|
-
if (realRel !== "" && (
|
|
6264
|
+
const realRel = path38.relative(realRoot, realSource);
|
|
6265
|
+
if (realRel !== "" && (path38.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
|
|
5774
6266
|
warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
|
|
5775
6267
|
continue;
|
|
5776
6268
|
}
|
|
5777
|
-
const stat = await
|
|
6269
|
+
const stat = await fs31.lstat(sourcePath).catch(() => null);
|
|
5778
6270
|
if (!stat) {
|
|
5779
6271
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
5780
6272
|
continue;
|
|
@@ -5784,14 +6276,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
5784
6276
|
continue;
|
|
5785
6277
|
}
|
|
5786
6278
|
if (stat.isFile()) {
|
|
5787
|
-
const filename =
|
|
5788
|
-
const dest =
|
|
6279
|
+
const filename = path38.basename(sourcePath);
|
|
6280
|
+
const dest = path38.join(projectRoot, filename);
|
|
5789
6281
|
record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
|
|
5790
6282
|
} else if (stat.isDirectory()) {
|
|
5791
|
-
const entries = await
|
|
6283
|
+
const entries = await fs31.readdir(sourcePath);
|
|
5792
6284
|
for (const entry of entries) {
|
|
5793
|
-
const src =
|
|
5794
|
-
const dest =
|
|
6285
|
+
const src = path38.join(sourcePath, entry);
|
|
6286
|
+
const dest = path38.join(projectRoot, entry);
|
|
5795
6287
|
record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
|
|
5796
6288
|
}
|
|
5797
6289
|
}
|
|
@@ -5799,42 +6291,42 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
5799
6291
|
return { scaffolded, skipped, skippedExisting };
|
|
5800
6292
|
}
|
|
5801
6293
|
async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
5802
|
-
const destDisplay =
|
|
5803
|
-
const srcStat = await
|
|
6294
|
+
const destDisplay = path38.relative(projectRoot, dest) || path38.basename(dest);
|
|
6295
|
+
const srcStat = await fs31.lstat(src).catch(() => null);
|
|
5804
6296
|
if (!srcStat) {
|
|
5805
|
-
warn(`Skipping ${
|
|
6297
|
+
warn(`Skipping ${path38.basename(src)}: source not found`);
|
|
5806
6298
|
return "skipped-refused";
|
|
5807
6299
|
}
|
|
5808
6300
|
if (srcStat.isSymbolicLink()) {
|
|
5809
|
-
warn(`Skipping ${
|
|
6301
|
+
warn(`Skipping ${path38.basename(src)}: source is a symlink`);
|
|
5810
6302
|
return "skipped-refused";
|
|
5811
6303
|
}
|
|
5812
|
-
const exists = await
|
|
6304
|
+
const exists = await fs31.pathExists(dest);
|
|
5813
6305
|
if (exists && !opts.force) {
|
|
5814
6306
|
warn(`Skipping ${destDisplay}: already exists \u2014 re-run with --force to overwrite it`);
|
|
5815
6307
|
return "skipped-exists";
|
|
5816
6308
|
}
|
|
5817
6309
|
if (opts.dryRun) {
|
|
5818
|
-
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${
|
|
6310
|
+
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path38.basename(dest)} (${itemId})`);
|
|
5819
6311
|
return "scaffolded";
|
|
5820
6312
|
}
|
|
5821
6313
|
if (exists && opts.force) {
|
|
5822
|
-
await
|
|
6314
|
+
await fs31.remove(dest);
|
|
5823
6315
|
}
|
|
5824
|
-
await
|
|
5825
|
-
await
|
|
6316
|
+
await fs31.ensureDir(path38.dirname(dest));
|
|
6317
|
+
await fs31.copy(src, dest, {
|
|
5826
6318
|
overwrite: true,
|
|
5827
6319
|
dereference: false,
|
|
5828
6320
|
// A vanished/unreadable entry mid-walk must skip that path, not crash the copy.
|
|
5829
6321
|
filter: (s) => {
|
|
5830
6322
|
try {
|
|
5831
|
-
return !
|
|
6323
|
+
return !fs31.lstatSync(s).isSymbolicLink();
|
|
5832
6324
|
} catch {
|
|
5833
6325
|
return false;
|
|
5834
6326
|
}
|
|
5835
6327
|
}
|
|
5836
6328
|
});
|
|
5837
|
-
log(`${exists ? "Overwrote" : "Created"} ${
|
|
6329
|
+
log(`${exists ? "Overwrote" : "Created"} ${path38.basename(dest)} (${itemId})`);
|
|
5838
6330
|
return "scaffolded";
|
|
5839
6331
|
}
|
|
5840
6332
|
|
|
@@ -5891,8 +6383,8 @@ async function runScan(options) {
|
|
|
5891
6383
|
}
|
|
5892
6384
|
|
|
5893
6385
|
// src/commands/undo.ts
|
|
5894
|
-
import
|
|
5895
|
-
import
|
|
6386
|
+
import path39 from "node:path";
|
|
6387
|
+
import fs32 from "fs-extra";
|
|
5896
6388
|
|
|
5897
6389
|
// src/claude/managed-paths.ts
|
|
5898
6390
|
var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md"];
|
|
@@ -5907,7 +6399,7 @@ function coreManagedAbsolutePaths(root) {
|
|
|
5907
6399
|
async function collectCoreManagedPaths(root) {
|
|
5908
6400
|
const existing = [];
|
|
5909
6401
|
for (const abs of coreManagedAbsolutePaths(root)) {
|
|
5910
|
-
if (await
|
|
6402
|
+
if (await fs32.pathExists(abs)) existing.push(abs);
|
|
5911
6403
|
}
|
|
5912
6404
|
return existing;
|
|
5913
6405
|
}
|
|
@@ -5919,18 +6411,18 @@ async function collectLockTrackedPaths(root) {
|
|
|
5919
6411
|
const relPaths = row.paths ?? [];
|
|
5920
6412
|
const existingRel = [];
|
|
5921
6413
|
for (const rel of relPaths) {
|
|
5922
|
-
if (await
|
|
6414
|
+
if (await fs32.pathExists(path39.resolve(root, rel))) existingRel.push(rel);
|
|
5923
6415
|
}
|
|
5924
6416
|
if (existingRel.length === 0) continue;
|
|
5925
6417
|
if (row.hash === void 0) {
|
|
5926
|
-
removable.push(...existingRel.map((rel) =>
|
|
6418
|
+
removable.push(...existingRel.map((rel) => path39.resolve(root, rel)));
|
|
5927
6419
|
continue;
|
|
5928
6420
|
}
|
|
5929
6421
|
const currentHash = await hashInstalledPaths(root, existingRel);
|
|
5930
6422
|
if (currentHash === row.hash) {
|
|
5931
|
-
removable.push(...existingRel.map((rel) =>
|
|
6423
|
+
removable.push(...existingRel.map((rel) => path39.resolve(root, rel)));
|
|
5932
6424
|
} else {
|
|
5933
|
-
preserved.push(...existingRel.map((rel) =>
|
|
6425
|
+
preserved.push(...existingRel.map((rel) => path39.resolve(root, rel)));
|
|
5934
6426
|
log(`Preserving locally-modified ${row.id ?? "(unknown item)"}: ${existingRel.join(", ")}`);
|
|
5935
6427
|
}
|
|
5936
6428
|
}
|
|
@@ -5938,51 +6430,51 @@ async function collectLockTrackedPaths(root) {
|
|
|
5938
6430
|
}
|
|
5939
6431
|
async function settingsHasHausContent(root) {
|
|
5940
6432
|
const settingsPath = claudePath(root, "settings.json");
|
|
5941
|
-
if (!await
|
|
6433
|
+
if (!await fs32.pathExists(settingsPath)) return false;
|
|
5942
6434
|
const settings = await readProjectSettings(root);
|
|
5943
6435
|
return settings._haus != null;
|
|
5944
6436
|
}
|
|
5945
6437
|
async function claudeMdHasHausBlock(root) {
|
|
5946
|
-
const filePath =
|
|
5947
|
-
if (!await
|
|
5948
|
-
const text = await
|
|
6438
|
+
const filePath = path39.join(root, "CLAUDE.md");
|
|
6439
|
+
if (!await fs32.pathExists(filePath)) return false;
|
|
6440
|
+
const text = await fs32.readFile(filePath, "utf8");
|
|
5949
6441
|
return text.includes(BLOCK_BEGIN);
|
|
5950
6442
|
}
|
|
5951
6443
|
async function stripProjectSettings(root) {
|
|
5952
6444
|
const settingsPath = claudePath(root, "settings.json");
|
|
5953
|
-
if (!await
|
|
6445
|
+
if (!await fs32.pathExists(settingsPath)) return false;
|
|
5954
6446
|
let settings = await readProjectSettings(root);
|
|
5955
6447
|
settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
5956
6448
|
const hasContent = Object.keys(settings).length > 0;
|
|
5957
6449
|
if (hasContent) {
|
|
5958
6450
|
await writeProjectSettings(root, settings);
|
|
5959
|
-
log(`Stripped haus rules from ${
|
|
6451
|
+
log(`Stripped haus rules from ${path39.relative(root, settingsPath)} (user settings preserved).`);
|
|
5960
6452
|
return true;
|
|
5961
6453
|
}
|
|
5962
|
-
await
|
|
5963
|
-
log(`Removed ${
|
|
6454
|
+
await fs32.remove(settingsPath);
|
|
6455
|
+
log(`Removed ${path39.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
5964
6456
|
return true;
|
|
5965
6457
|
}
|
|
5966
6458
|
async function stripRootClaudeMd(root) {
|
|
5967
|
-
const filePath =
|
|
5968
|
-
if (!await
|
|
5969
|
-
const prev = await
|
|
6459
|
+
const filePath = path39.join(root, "CLAUDE.md");
|
|
6460
|
+
if (!await fs32.pathExists(filePath)) return false;
|
|
6461
|
+
const prev = await fs32.readFile(filePath, "utf8");
|
|
5970
6462
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
5971
6463
|
const next = stripHausBlock(prev);
|
|
5972
6464
|
if (next.length === 0) {
|
|
5973
|
-
await
|
|
6465
|
+
await fs32.remove(filePath);
|
|
5974
6466
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
5975
6467
|
} else {
|
|
5976
|
-
await
|
|
6468
|
+
await fs32.writeFile(filePath, next, "utf8");
|
|
5977
6469
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
5978
6470
|
}
|
|
5979
6471
|
return true;
|
|
5980
6472
|
}
|
|
5981
6473
|
async function pruneDirIfEmpty(dir) {
|
|
5982
|
-
if (!await
|
|
6474
|
+
if (!await fs32.pathExists(dir)) return;
|
|
5983
6475
|
try {
|
|
5984
|
-
const entries = await
|
|
5985
|
-
if (entries.length === 0) await
|
|
6476
|
+
const entries = await fs32.readdir(dir);
|
|
6477
|
+
if (entries.length === 0) await fs32.remove(dir);
|
|
5986
6478
|
} catch {
|
|
5987
6479
|
}
|
|
5988
6480
|
}
|
|
@@ -5991,13 +6483,13 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
|
5991
6483
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5992
6484
|
const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
|
|
5993
6485
|
for (const abs of managedAbsPaths) {
|
|
5994
|
-
if (!await
|
|
5995
|
-
const rel =
|
|
5996
|
-
const backupPath =
|
|
5997
|
-
await
|
|
5998
|
-
await
|
|
6486
|
+
if (!await fs32.pathExists(abs)) continue;
|
|
6487
|
+
const rel = path39.relative(root, abs);
|
|
6488
|
+
const backupPath = path39.join(backupRoot, rel);
|
|
6489
|
+
await fs32.ensureDir(path39.dirname(backupPath));
|
|
6490
|
+
await fs32.copy(abs, backupPath);
|
|
5999
6491
|
}
|
|
6000
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
6492
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path39.relative(root, backupRoot)}.`);
|
|
6001
6493
|
}
|
|
6002
6494
|
async function runUndo(options) {
|
|
6003
6495
|
const root = options.root ?? process.cwd();
|
|
@@ -6010,7 +6502,7 @@ async function runUndo(options) {
|
|
|
6010
6502
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
6011
6503
|
return;
|
|
6012
6504
|
}
|
|
6013
|
-
const relTargets = managed.map((p) =>
|
|
6505
|
+
const relTargets = managed.map((p) => path39.relative(root, p));
|
|
6014
6506
|
const summaryParts = [...relTargets];
|
|
6015
6507
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
6016
6508
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -6032,9 +6524,9 @@ User-owned .claude/ files will be preserved.`
|
|
|
6032
6524
|
}
|
|
6033
6525
|
await backupManagedFilesBeforeUndo(root, managed);
|
|
6034
6526
|
for (const abs of managed) {
|
|
6035
|
-
if (!await
|
|
6036
|
-
await
|
|
6037
|
-
log(`Removed ${
|
|
6527
|
+
if (!await fs32.pathExists(abs)) continue;
|
|
6528
|
+
await fs32.remove(abs);
|
|
6529
|
+
log(`Removed ${path39.relative(root, abs)}`);
|
|
6038
6530
|
}
|
|
6039
6531
|
if (stripSettings) await stripProjectSettings(root);
|
|
6040
6532
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -6045,8 +6537,8 @@ User-owned .claude/ files will be preserved.`
|
|
|
6045
6537
|
|
|
6046
6538
|
// src/install/uninstall.ts
|
|
6047
6539
|
import crypto3 from "node:crypto";
|
|
6048
|
-
import
|
|
6049
|
-
import
|
|
6540
|
+
import path40 from "node:path";
|
|
6541
|
+
import fs33 from "fs-extra";
|
|
6050
6542
|
async function runUninstall(options = {}) {
|
|
6051
6543
|
const { force = false } = options;
|
|
6052
6544
|
const manifest = await readManifest();
|
|
@@ -6056,7 +6548,7 @@ async function runUninstall(options = {}) {
|
|
|
6056
6548
|
return result;
|
|
6057
6549
|
}
|
|
6058
6550
|
for (const entry of manifest.files) {
|
|
6059
|
-
const exists =
|
|
6551
|
+
const exists = fs33.pathExistsSync(entry.destPath);
|
|
6060
6552
|
if (!exists) continue;
|
|
6061
6553
|
const content2 = await readText(entry.destPath);
|
|
6062
6554
|
if (content2 === void 0) continue;
|
|
@@ -6074,23 +6566,23 @@ async function runUninstall(options = {}) {
|
|
|
6074
6566
|
result.skipped.push(entry.destPath);
|
|
6075
6567
|
continue;
|
|
6076
6568
|
}
|
|
6077
|
-
await
|
|
6078
|
-
await pruneEmptyDir(
|
|
6569
|
+
await fs33.remove(entry.destPath);
|
|
6570
|
+
await pruneEmptyDir(path40.dirname(entry.destPath));
|
|
6079
6571
|
result.deleted.push(entry.destPath);
|
|
6080
6572
|
}
|
|
6081
6573
|
const settings = await readSettings();
|
|
6082
6574
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
6083
6575
|
await writeSettings(stripped);
|
|
6084
6576
|
result.hooksStripped = true;
|
|
6085
|
-
const hausDir =
|
|
6577
|
+
const hausDir = path40.join(globalClaudeDir(), "haus");
|
|
6086
6578
|
const manifestPath2 = hausManifestPath();
|
|
6087
|
-
if (
|
|
6088
|
-
await
|
|
6579
|
+
if (fs33.pathExistsSync(manifestPath2)) {
|
|
6580
|
+
await fs33.remove(manifestPath2);
|
|
6089
6581
|
}
|
|
6090
|
-
if (
|
|
6582
|
+
if (fs33.pathExistsSync(hausDir)) {
|
|
6091
6583
|
try {
|
|
6092
|
-
const remaining = await
|
|
6093
|
-
if (remaining.length === 0) await
|
|
6584
|
+
const remaining = await fs33.readdir(hausDir);
|
|
6585
|
+
if (remaining.length === 0) await fs33.remove(hausDir);
|
|
6094
6586
|
} catch {
|
|
6095
6587
|
}
|
|
6096
6588
|
}
|
|
@@ -6123,12 +6615,12 @@ async function runUninstallCommand(options) {
|
|
|
6123
6615
|
}
|
|
6124
6616
|
|
|
6125
6617
|
// src/commands/update.ts
|
|
6126
|
-
import
|
|
6618
|
+
import path42 from "node:path";
|
|
6127
6619
|
|
|
6128
6620
|
// src/claude/refresh-project.ts
|
|
6129
|
-
import
|
|
6621
|
+
import fs34 from "fs-extra";
|
|
6130
6622
|
async function isHausProject(root) {
|
|
6131
|
-
if (await
|
|
6623
|
+
if (await fs34.pathExists(hausPath(root, "recommendation.json"))) return true;
|
|
6132
6624
|
const result = await readJsonDetailed(claudePath(root, "settings.json"));
|
|
6133
6625
|
if (result.status === "ok") return result.value._haus != null;
|
|
6134
6626
|
return false;
|
|
@@ -6163,7 +6655,7 @@ function summarizeLockDiff(before, after) {
|
|
|
6163
6655
|
|
|
6164
6656
|
// src/update/lockfile.ts
|
|
6165
6657
|
import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
|
|
6166
|
-
import
|
|
6658
|
+
import path41 from "node:path";
|
|
6167
6659
|
async function readLockSummary(root) {
|
|
6168
6660
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
6169
6661
|
const catalogRef = lock.find((item) => item.catalogRef)?.catalogRef ?? null;
|
|
@@ -6199,7 +6691,7 @@ async function applyLock(root) {
|
|
|
6199
6691
|
try {
|
|
6200
6692
|
const backupDir = hausPath(root, "backups");
|
|
6201
6693
|
await mkdir(backupDir, { recursive: true });
|
|
6202
|
-
await copyFile(lockPath,
|
|
6694
|
+
await copyFile(lockPath, path41.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
6203
6695
|
} catch {
|
|
6204
6696
|
}
|
|
6205
6697
|
const enriched = await Promise.all(
|
|
@@ -6221,7 +6713,7 @@ function diffLock(before, after) {
|
|
|
6221
6713
|
}
|
|
6222
6714
|
async function hasLocalOverrides(root) {
|
|
6223
6715
|
try {
|
|
6224
|
-
await readFile2(
|
|
6716
|
+
await readFile2(path41.join(root, ".claude", "settings.json"), "utf8");
|
|
6225
6717
|
return true;
|
|
6226
6718
|
} catch {
|
|
6227
6719
|
return false;
|
|
@@ -6237,7 +6729,7 @@ async function runUpdate(options) {
|
|
|
6237
6729
|
return;
|
|
6238
6730
|
}
|
|
6239
6731
|
if (options.check) {
|
|
6240
|
-
const pkgJson2 = await readJson(
|
|
6732
|
+
const pkgJson2 = await readJson(path42.join(packageRoot(), "package.json"));
|
|
6241
6733
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
6242
6734
|
const [status, npmVersion, latestCatalogTag, globalInstallDrift, catalogItems, lockItems] = await Promise.all([
|
|
6243
6735
|
options.fast ? fastLockStatus(root) : checkLock(root),
|
|
@@ -6272,7 +6764,7 @@ async function runUpdate(options) {
|
|
|
6272
6764
|
if (hasHashDrift || formerIdMigrations.length > 0) process.exitCode = 1;
|
|
6273
6765
|
return;
|
|
6274
6766
|
}
|
|
6275
|
-
const pkgJson = await readJson(
|
|
6767
|
+
const pkgJson = await readJson(path42.join(packageRoot(), "package.json"));
|
|
6276
6768
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
6277
6769
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
6278
6770
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -6311,7 +6803,7 @@ async function runFromHookCheck(root) {
|
|
|
6311
6803
|
try {
|
|
6312
6804
|
const summary = await readLockSummary(root);
|
|
6313
6805
|
if (summary.count === 0) return;
|
|
6314
|
-
const pkgJson = await readJson(
|
|
6806
|
+
const pkgJson = await readJson(path42.join(packageRoot(), "package.json"));
|
|
6315
6807
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
6316
6808
|
const [npmVersion, latestCatalogTag] = await Promise.all([
|
|
6317
6809
|
fetchNpmVersionStatus(currentVersion),
|
|
@@ -6383,11 +6875,11 @@ async function fastLockStatus(root) {
|
|
|
6383
6875
|
}
|
|
6384
6876
|
|
|
6385
6877
|
// src/commands/validate-catalog.ts
|
|
6386
|
-
import
|
|
6878
|
+
import path44 from "node:path";
|
|
6387
6879
|
|
|
6388
6880
|
// src/catalog/validate-core.ts
|
|
6389
|
-
import
|
|
6390
|
-
import
|
|
6881
|
+
import fs35 from "node:fs";
|
|
6882
|
+
import path43 from "node:path";
|
|
6391
6883
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
6392
6884
|
function auditForbiddenStacks(items) {
|
|
6393
6885
|
const failures = [];
|
|
@@ -6497,14 +6989,14 @@ function checkRequiredFrontmatter(text, label) {
|
|
|
6497
6989
|
}
|
|
6498
6990
|
function readForAudit(absPath, label, failures) {
|
|
6499
6991
|
try {
|
|
6500
|
-
return
|
|
6992
|
+
return fs35.readFileSync(absPath, "utf8");
|
|
6501
6993
|
} catch (err) {
|
|
6502
6994
|
failures.push(`${label}: unreadable (${err instanceof Error ? err.message : String(err)})`);
|
|
6503
6995
|
return null;
|
|
6504
6996
|
}
|
|
6505
6997
|
}
|
|
6506
6998
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
6507
|
-
const rel =
|
|
6999
|
+
const rel = path43.relative(manifestDir, absPath);
|
|
6508
7000
|
const failures = [];
|
|
6509
7001
|
const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
|
|
6510
7002
|
if (text === null) return failures;
|
|
@@ -6526,47 +7018,47 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
6526
7018
|
const failures = [];
|
|
6527
7019
|
for (const item of items) {
|
|
6528
7020
|
if (!item.path || !isSafeCatalogPath(item.path)) continue;
|
|
6529
|
-
const absPath =
|
|
7021
|
+
const absPath = path43.join(manifestDir, item.path);
|
|
6530
7022
|
if (item.type === "skill") {
|
|
6531
|
-
const skillMd =
|
|
6532
|
-
if (!
|
|
6533
|
-
failures.push(`${item.id}: missing ${
|
|
7023
|
+
const skillMd = path43.join(absPath, "SKILL.md");
|
|
7024
|
+
if (!fs35.existsSync(skillMd)) {
|
|
7025
|
+
failures.push(`${item.id}: missing ${path43.relative(manifestDir, skillMd)}`);
|
|
6534
7026
|
continue;
|
|
6535
7027
|
}
|
|
6536
7028
|
const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
|
|
6537
7029
|
if (text === null) continue;
|
|
6538
7030
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
6539
7031
|
failures.push(
|
|
6540
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
7032
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path43.relative(manifestDir, skillMd)}`)
|
|
6541
7033
|
);
|
|
6542
7034
|
} else if (item.type === "agent") {
|
|
6543
|
-
if (!
|
|
7035
|
+
if (!fs35.existsSync(absPath)) {
|
|
6544
7036
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
6545
7037
|
continue;
|
|
6546
7038
|
}
|
|
6547
|
-
const rel =
|
|
7039
|
+
const rel = path43.relative(manifestDir, absPath);
|
|
6548
7040
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
6549
7041
|
if (text === null) continue;
|
|
6550
7042
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
6551
7043
|
failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
|
|
6552
7044
|
} else if (item.type === "template") {
|
|
6553
|
-
if (!
|
|
7045
|
+
if (!fs35.existsSync(absPath)) {
|
|
6554
7046
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
6555
7047
|
continue;
|
|
6556
7048
|
}
|
|
6557
7049
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
6558
7050
|
} else if (item.type === "command") {
|
|
6559
|
-
if (!
|
|
7051
|
+
if (!fs35.existsSync(absPath)) {
|
|
6560
7052
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
6561
7053
|
continue;
|
|
6562
7054
|
}
|
|
6563
|
-
const rel =
|
|
7055
|
+
const rel = path43.relative(manifestDir, absPath);
|
|
6564
7056
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
6565
7057
|
if (text === null) continue;
|
|
6566
7058
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
6567
7059
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
6568
7060
|
} else if (item.type === "config") {
|
|
6569
|
-
if (!
|
|
7061
|
+
if (!fs35.existsSync(absPath)) {
|
|
6570
7062
|
failures.push(`${item.id}: missing config path ${item.path}`);
|
|
6571
7063
|
}
|
|
6572
7064
|
}
|
|
@@ -6582,7 +7074,7 @@ var DIR_ITEM_TYPE = {
|
|
|
6582
7074
|
function walkMd(dir, fn, failures) {
|
|
6583
7075
|
let entries;
|
|
6584
7076
|
try {
|
|
6585
|
-
entries =
|
|
7077
|
+
entries = fs35.readdirSync(dir, { withFileTypes: true });
|
|
6586
7078
|
} catch (err) {
|
|
6587
7079
|
failures.push(
|
|
6588
7080
|
`${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
|
|
@@ -6590,7 +7082,7 @@ function walkMd(dir, fn, failures) {
|
|
|
6590
7082
|
return;
|
|
6591
7083
|
}
|
|
6592
7084
|
for (const entry of entries) {
|
|
6593
|
-
const full =
|
|
7085
|
+
const full = path43.join(dir, entry.name);
|
|
6594
7086
|
if (entry.isDirectory()) walkMd(full, fn, failures);
|
|
6595
7087
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
6596
7088
|
}
|
|
@@ -6599,13 +7091,13 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
6599
7091
|
const failures = [];
|
|
6600
7092
|
const pathSourceMap = buildItemPathSourceMap(items);
|
|
6601
7093
|
for (const dir of ["skills", "agents", "templates", "commands"]) {
|
|
6602
|
-
const abs =
|
|
6603
|
-
if (!
|
|
7094
|
+
const abs = path43.join(manifestDir, dir);
|
|
7095
|
+
if (!fs35.existsSync(abs)) continue;
|
|
6604
7096
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
6605
7097
|
walkMd(
|
|
6606
7098
|
abs,
|
|
6607
7099
|
(file) => {
|
|
6608
|
-
const rel =
|
|
7100
|
+
const rel = path43.relative(manifestDir, file);
|
|
6609
7101
|
const text = readForAudit(file, rel, failures);
|
|
6610
7102
|
if (text === null) return;
|
|
6611
7103
|
const norm = rel.replace(/\\/g, "/");
|
|
@@ -6647,8 +7139,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
6647
7139
|
process.exitCode = 1;
|
|
6648
7140
|
return;
|
|
6649
7141
|
}
|
|
6650
|
-
const abs =
|
|
6651
|
-
const manifestDir =
|
|
7142
|
+
const abs = path44.resolve(process.cwd(), manifestPath2);
|
|
7143
|
+
const manifestDir = path44.dirname(abs);
|
|
6652
7144
|
const data = await readJson(abs);
|
|
6653
7145
|
if (!data?.items) {
|
|
6654
7146
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -6666,7 +7158,7 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
6666
7158
|
|
|
6667
7159
|
// src/commands/workspace.ts
|
|
6668
7160
|
import { existsSync as existsSync6, statSync as statSync3 } from "node:fs";
|
|
6669
|
-
import
|
|
7161
|
+
import path52 from "node:path";
|
|
6670
7162
|
|
|
6671
7163
|
// src/commands/workspace/aggregate.ts
|
|
6672
7164
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -6720,7 +7212,7 @@ ${summaries.map(
|
|
|
6720
7212
|
}
|
|
6721
7213
|
|
|
6722
7214
|
// src/commands/workspace/config.ts
|
|
6723
|
-
import
|
|
7215
|
+
import path45 from "node:path";
|
|
6724
7216
|
import YAML from "yaml";
|
|
6725
7217
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
6726
7218
|
function parseWorkspaceConfig(text) {
|
|
@@ -6743,11 +7235,11 @@ function parseWorkspaceConfig(text) {
|
|
|
6743
7235
|
};
|
|
6744
7236
|
}
|
|
6745
7237
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
6746
|
-
return parseWorkspaceConfig(await readText(
|
|
7238
|
+
return parseWorkspaceConfig(await readText(path45.join(workspaceRoot, WORKSPACE_FILE)));
|
|
6747
7239
|
}
|
|
6748
7240
|
|
|
6749
7241
|
// src/commands/workspace/discover.ts
|
|
6750
|
-
import
|
|
7242
|
+
import path46 from "node:path";
|
|
6751
7243
|
import fg4 from "fast-glob";
|
|
6752
7244
|
import YAML2 from "yaml";
|
|
6753
7245
|
var DEFAULT_MAX_DEPTH = 3;
|
|
@@ -6778,8 +7270,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
6778
7270
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
6779
7271
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
6780
7272
|
for (const match of matches) {
|
|
6781
|
-
const base =
|
|
6782
|
-
const dir =
|
|
7273
|
+
const base = path46.posix.basename(match);
|
|
7274
|
+
const dir = path46.posix.dirname(match);
|
|
6783
7275
|
const owner = dir === "." ? "." : dir;
|
|
6784
7276
|
if (base === ".git") gitDirs.add(owner);
|
|
6785
7277
|
else manifestDirs.add(owner);
|
|
@@ -6795,9 +7287,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
6795
7287
|
}
|
|
6796
7288
|
repoRoots.sort((a, b) => a.localeCompare(b));
|
|
6797
7289
|
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
6798
|
-
const absDir =
|
|
6799
|
-
const pkg = await readJson(
|
|
6800
|
-
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name :
|
|
7290
|
+
const absDir = path46.resolve(workspaceRoot, relDir);
|
|
7291
|
+
const pkg = await readJson(path46.join(absDir, "package.json"));
|
|
7292
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path46.basename(relDir === "." ? workspaceRoot : absDir);
|
|
6801
7293
|
let role = "auto";
|
|
6802
7294
|
try {
|
|
6803
7295
|
const scan = await scanProject(absDir);
|
|
@@ -6840,7 +7332,7 @@ function renderWorkspaceYaml(config) {
|
|
|
6840
7332
|
});
|
|
6841
7333
|
}
|
|
6842
7334
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
6843
|
-
const yamlPath =
|
|
7335
|
+
const yamlPath = path46.join(workspaceRoot, "haus.workspace.yaml");
|
|
6844
7336
|
const existingText = await readText(yamlPath);
|
|
6845
7337
|
const existing = parseWorkspaceConfig(existingText);
|
|
6846
7338
|
if (existingText && !existing) {
|
|
@@ -6877,18 +7369,18 @@ async function runDiscover(workspaceRoot, opts = {}) {
|
|
|
6877
7369
|
|
|
6878
7370
|
// src/commands/workspace/doctor.ts
|
|
6879
7371
|
import { existsSync as existsSync3 } from "node:fs";
|
|
6880
|
-
import
|
|
7372
|
+
import path48 from "node:path";
|
|
6881
7373
|
|
|
6882
7374
|
// src/commands/workspace/manifest.ts
|
|
6883
7375
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
6884
|
-
import
|
|
7376
|
+
import path47 from "node:path";
|
|
6885
7377
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
6886
7378
|
function manifestPath(workspaceRoot) {
|
|
6887
7379
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
6888
7380
|
}
|
|
6889
7381
|
function hausVersion() {
|
|
6890
7382
|
try {
|
|
6891
|
-
const pkg = JSON.parse(readFileSync3(
|
|
7383
|
+
const pkg = JSON.parse(readFileSync3(path47.join(packageRoot(), "package.json"), "utf8"));
|
|
6892
7384
|
return pkg.version ?? "0.0.0";
|
|
6893
7385
|
} catch {
|
|
6894
7386
|
return "0.0.0";
|
|
@@ -6955,7 +7447,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
6955
7447
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
6956
7448
|
const catalogRefByRepo = [];
|
|
6957
7449
|
for (const repo of config.repos) {
|
|
6958
|
-
const repoRoot =
|
|
7450
|
+
const repoRoot = path48.resolve(workspaceRoot, repo.path);
|
|
6959
7451
|
const entry = manifestByName.get(repo.name);
|
|
6960
7452
|
if (!entry) {
|
|
6961
7453
|
flag({
|
|
@@ -7038,11 +7530,11 @@ function emit(args) {
|
|
|
7038
7530
|
|
|
7039
7531
|
// src/commands/workspace/setup.ts
|
|
7040
7532
|
import { existsSync as existsSync4, statSync } from "node:fs";
|
|
7041
|
-
import
|
|
7533
|
+
import path50 from "node:path";
|
|
7042
7534
|
|
|
7043
7535
|
// src/claude/write-workspace-claude-md.ts
|
|
7044
|
-
import
|
|
7045
|
-
import
|
|
7536
|
+
import path49 from "node:path";
|
|
7537
|
+
import fs36 from "fs-extra";
|
|
7046
7538
|
function buildWorkspaceImportBlock(client, members) {
|
|
7047
7539
|
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
7048
7540
|
const body = [
|
|
@@ -7060,11 +7552,11 @@ ${BLOCK_END}`;
|
|
|
7060
7552
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
7061
7553
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
7062
7554
|
const dryRun = opts.dryRun ?? false;
|
|
7063
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
7555
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path49.join(workspaceRoot, "CLAUDE.md");
|
|
7064
7556
|
const say = opts.quiet ? () => {
|
|
7065
7557
|
} : log;
|
|
7066
|
-
const existed = await
|
|
7067
|
-
const prev = existed ? await
|
|
7558
|
+
const existed = await fs36.pathExists(filePath);
|
|
7559
|
+
const prev = existed ? await fs36.readFile(filePath, "utf8") : "";
|
|
7068
7560
|
const next = opts.collision ? `${block}
|
|
7069
7561
|
` : injectHausBlock(prev, block);
|
|
7070
7562
|
const printable = displayPath(workspaceRoot, filePath);
|
|
@@ -7091,20 +7583,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
7091
7583
|
|
|
7092
7584
|
// src/commands/workspace/setup.ts
|
|
7093
7585
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
7094
|
-
let dir =
|
|
7586
|
+
let dir = path50.resolve(start);
|
|
7095
7587
|
for (; ; ) {
|
|
7096
|
-
if (existsSync4(
|
|
7097
|
-
const parent =
|
|
7098
|
-
if (parent === dir) return
|
|
7588
|
+
if (existsSync4(path50.join(dir, WORKSPACE_FILE))) return dir;
|
|
7589
|
+
const parent = path50.dirname(dir);
|
|
7590
|
+
if (parent === dir) return path50.resolve(start);
|
|
7099
7591
|
dir = parent;
|
|
7100
7592
|
}
|
|
7101
7593
|
}
|
|
7102
7594
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
7103
|
-
return
|
|
7595
|
+
return path50.resolve(workspaceRoot, repoPath) === path50.resolve(workspaceRoot);
|
|
7104
7596
|
}
|
|
7105
7597
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
7106
7598
|
const apply = options.write ?? false;
|
|
7107
|
-
const configText = await readText(
|
|
7599
|
+
const configText = await readText(path50.join(workspaceRoot, WORKSPACE_FILE));
|
|
7108
7600
|
if (!configText) {
|
|
7109
7601
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
7110
7602
|
process.exitCode = 1;
|
|
@@ -7131,7 +7623,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
7131
7623
|
const statuses = [];
|
|
7132
7624
|
const aggregateInputs = [];
|
|
7133
7625
|
for (const repo of repos) {
|
|
7134
|
-
const repoRoot =
|
|
7626
|
+
const repoRoot = path50.resolve(workspaceRoot, repo.path);
|
|
7135
7627
|
say(`
|
|
7136
7628
|
\u2192 ${repo.name} (${repo.path})`);
|
|
7137
7629
|
try {
|
|
@@ -7202,7 +7694,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
7202
7694
|
const status = statusByName.get(repo.name);
|
|
7203
7695
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
7204
7696
|
if (status?.status === "ok") {
|
|
7205
|
-
const lock = await checkLock(
|
|
7697
|
+
const lock = await checkLock(path50.resolve(workspaceRoot, repo.path));
|
|
7206
7698
|
manifestRepos.push({
|
|
7207
7699
|
name: repo.name,
|
|
7208
7700
|
path: repo.path,
|
|
@@ -7266,8 +7758,8 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
7266
7758
|
|
|
7267
7759
|
// src/commands/workspace/undo.ts
|
|
7268
7760
|
import { existsSync as existsSync5, statSync as statSync2 } from "node:fs";
|
|
7269
|
-
import
|
|
7270
|
-
import
|
|
7761
|
+
import path51 from "node:path";
|
|
7762
|
+
import fs37 from "fs-extra";
|
|
7271
7763
|
var WORKSPACE_AGGREGATE_FILES = [
|
|
7272
7764
|
"workspace-summary.json",
|
|
7273
7765
|
"dependency-ownership-map.json",
|
|
@@ -7295,7 +7787,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
7295
7787
|
}
|
|
7296
7788
|
let anyRepoFailed = false;
|
|
7297
7789
|
for (const repo of config.repos) {
|
|
7298
|
-
const repoRoot =
|
|
7790
|
+
const repoRoot = path51.resolve(workspaceRoot, repo.path);
|
|
7299
7791
|
log(`
|
|
7300
7792
|
\u2192 ${repo.name} (${repo.path})`);
|
|
7301
7793
|
try {
|
|
@@ -7310,31 +7802,31 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
7310
7802
|
}
|
|
7311
7803
|
for (const rel of WORKSPACE_AGGREGATE_FILES) {
|
|
7312
7804
|
const abs = hausPath(workspaceRoot, rel);
|
|
7313
|
-
if (await
|
|
7314
|
-
await
|
|
7315
|
-
log(`Removed ${
|
|
7805
|
+
if (await fs37.pathExists(abs)) {
|
|
7806
|
+
await fs37.remove(abs);
|
|
7807
|
+
log(`Removed ${path51.relative(workspaceRoot, abs)}`);
|
|
7316
7808
|
}
|
|
7317
7809
|
}
|
|
7318
7810
|
const manifestFile = manifestPath(workspaceRoot);
|
|
7319
|
-
if (await
|
|
7320
|
-
await
|
|
7321
|
-
log(`Removed ${
|
|
7811
|
+
if (await fs37.pathExists(manifestFile)) {
|
|
7812
|
+
await fs37.remove(manifestFile);
|
|
7813
|
+
log(`Removed ${path51.relative(workspaceRoot, manifestFile)}`);
|
|
7322
7814
|
}
|
|
7323
7815
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
7324
|
-
if (await
|
|
7325
|
-
await
|
|
7326
|
-
log(`Removed ${
|
|
7816
|
+
if (await fs37.pathExists(workspaceMdPath)) {
|
|
7817
|
+
await fs37.remove(workspaceMdPath);
|
|
7818
|
+
log(`Removed ${path51.relative(workspaceRoot, workspaceMdPath)}`);
|
|
7327
7819
|
} else {
|
|
7328
|
-
const claudeMdPath =
|
|
7820
|
+
const claudeMdPath = path51.join(workspaceRoot, "CLAUDE.md");
|
|
7329
7821
|
if (existsSync5(claudeMdPath)) {
|
|
7330
7822
|
const prev = await readText(claudeMdPath);
|
|
7331
7823
|
if (prev?.includes(BLOCK_BEGIN)) {
|
|
7332
7824
|
const next = stripHausBlock(prev);
|
|
7333
7825
|
if (next.length === 0) {
|
|
7334
|
-
await
|
|
7826
|
+
await fs37.remove(claudeMdPath);
|
|
7335
7827
|
log("Removed CLAUDE.md (only contained the workspace haus import block).");
|
|
7336
7828
|
} else {
|
|
7337
|
-
await
|
|
7829
|
+
await fs37.writeFile(claudeMdPath, next, "utf8");
|
|
7338
7830
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
7339
7831
|
}
|
|
7340
7832
|
}
|
|
@@ -7377,7 +7869,7 @@ relationships: []
|
|
|
7377
7869
|
log("Workspace initialized.");
|
|
7378
7870
|
}
|
|
7379
7871
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
7380
|
-
const configText = await readText(
|
|
7872
|
+
const configText = await readText(path52.join(workspaceRoot, WORKSPACE_FILE));
|
|
7381
7873
|
if (!configText) {
|
|
7382
7874
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
7383
7875
|
process.exitCode = 1;
|
|
@@ -7399,7 +7891,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
7399
7891
|
const inputs = [];
|
|
7400
7892
|
const failures = [];
|
|
7401
7893
|
for (const repo of config.repos) {
|
|
7402
|
-
const repoRoot =
|
|
7894
|
+
const repoRoot = path52.resolve(workspaceRoot, repo.path);
|
|
7403
7895
|
try {
|
|
7404
7896
|
if (!existsSync6(repoRoot) || !statSync3(repoRoot).isDirectory()) {
|
|
7405
7897
|
throw new Error("path is not a directory");
|
|
@@ -7462,7 +7954,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
7462
7954
|
// src/cli.ts
|
|
7463
7955
|
function cliVersion() {
|
|
7464
7956
|
try {
|
|
7465
|
-
const pkgPath =
|
|
7957
|
+
const pkgPath = path53.join(packageRoot(), "package.json");
|
|
7466
7958
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
7467
7959
|
return pkg.version ?? "0.0.0";
|
|
7468
7960
|
} catch {
|
|
@@ -7472,7 +7964,7 @@ function cliVersion() {
|
|
|
7472
7964
|
var program = new Command();
|
|
7473
7965
|
function validateRuntimeNodeVersion() {
|
|
7474
7966
|
try {
|
|
7475
|
-
const pkgPath =
|
|
7967
|
+
const pkgPath = path53.join(packageRoot(), "package.json");
|
|
7476
7968
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
7477
7969
|
const requiredRange = pkg.engines?.node;
|
|
7478
7970
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -7503,8 +7995,17 @@ program.command("apply").option("--dry-run").option("--write").option("--select"
|
|
|
7503
7995
|
).option(
|
|
7504
7996
|
"--refill-config",
|
|
7505
7997
|
"Fill still-blank fields in an existing workflow-config.md without touching edited ones"
|
|
7506
|
-
).option("--force", "Overwrite user-modified managed workflow files").
|
|
7998
|
+
).option("--force", "Overwrite user-modified managed workflow files").option(
|
|
7999
|
+
"--prune",
|
|
8000
|
+
"Remove lock-tracked catalog items no longer in the current recommendation (hash-gated, backed up first)"
|
|
8001
|
+
).action(runApply);
|
|
7507
8002
|
program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
|
|
8003
|
+
var backups = program.command("backups");
|
|
8004
|
+
backups.command("list").description("List backups under .haus-workflow/backups/").action(() => runBackups("list"));
|
|
8005
|
+
backups.command("restore <id>").description("Restore a specific backup by id (see `haus backups list`)").option("-y, --yes", "Skip confirmation").action((id, opts) => runBackups("restore", { id, yes: opts.yes }));
|
|
8006
|
+
backups.command("prune").description("Delete old backups by age and/or count (requires --older-than or --keep)").option("--older-than <days>", "Remove backups older than N days").option("--keep <n>", "Keep only the N most recent backups").option("-y, --yes", "Skip confirmation").action(
|
|
8007
|
+
(opts) => runBackups("prune", { olderThan: opts.olderThan, keep: opts.keep, yes: opts.yes })
|
|
8008
|
+
);
|
|
7508
8009
|
program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
|
|
7509
8010
|
program.command("init").option("--json").action(runInit);
|
|
7510
8011
|
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(
|