@haus-tech/haus-workflow 1.1.2 → 1.2.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 +24 -0
- package/dist/cli.js +1504 -1139
- package/library/catalog/manifest.json +897 -502
- package/package.json +9 -9
package/dist/cli.js
CHANGED
|
@@ -60,15 +60,15 @@ var init_exec = __esm({
|
|
|
60
60
|
|
|
61
61
|
// src/cli.ts
|
|
62
62
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
63
|
-
import
|
|
63
|
+
import path52 from "node:path";
|
|
64
64
|
import { Command } from "commander";
|
|
65
65
|
|
|
66
66
|
// src/commands/apply.ts
|
|
67
|
-
import
|
|
67
|
+
import path22 from "node:path";
|
|
68
68
|
import checkbox from "@inquirer/checkbox";
|
|
69
69
|
|
|
70
70
|
// src/catalog/load-catalog.ts
|
|
71
|
-
import
|
|
71
|
+
import path9 from "node:path";
|
|
72
72
|
|
|
73
73
|
// src/utils/fs.ts
|
|
74
74
|
import crypto from "node:crypto";
|
|
@@ -211,10 +211,9 @@ function packageRoot() {
|
|
|
211
211
|
return path2.resolve(path2.dirname(file), "../..");
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
// src/catalog/remote-catalog.ts
|
|
215
|
-
import
|
|
216
|
-
import
|
|
217
|
-
import fs2 from "fs-extra";
|
|
214
|
+
// src/catalog/remote-catalog/github-tree.ts
|
|
215
|
+
import path6 from "node:path";
|
|
216
|
+
import fs4 from "fs-extra";
|
|
218
217
|
|
|
219
218
|
// src/utils/logger.ts
|
|
220
219
|
var log = (msg, ...args) => {
|
|
@@ -234,275 +233,287 @@ var SUPERPOWERS_SHARED_CATALOG_REL = "skills/superpowers/shared";
|
|
|
234
233
|
var CATALOG_REF = process.env.HAUS_CATALOG_REF;
|
|
235
234
|
var CATALOG_CACHE_SUBDIR = ".claude/haus/catalog-cache";
|
|
236
235
|
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
"rust",
|
|
244
|
-
"java",
|
|
245
|
-
"spring",
|
|
246
|
-
"kotlin",
|
|
247
|
-
"swift",
|
|
248
|
-
"android",
|
|
249
|
-
"flutter",
|
|
250
|
-
"dart",
|
|
251
|
-
"c++",
|
|
252
|
-
"perl",
|
|
253
|
-
"defi",
|
|
254
|
-
"trading"
|
|
255
|
-
],
|
|
256
|
-
requiredSkillFrontmatter: ["description"],
|
|
257
|
-
riskyInstallPatterns: [
|
|
258
|
-
{ source: "\\bnpx\\s+-y\\b", flags: "i" },
|
|
259
|
-
{ source: "\\bnpx\\s+--yes\\b", flags: "i" },
|
|
260
|
-
{ source: "\\byarn\\s+dlx\\b", flags: "i" },
|
|
261
|
-
{ source: "\\bpnpm\\s+dlx\\b", flags: "i" }
|
|
262
|
-
],
|
|
263
|
-
allowedNpxPattern: { source: "\\bnpx\\s+tsx\\b", flags: "i" },
|
|
264
|
-
anyNpxPattern: { source: "\\bnpx\\s+\\S+", flags: "i" },
|
|
265
|
-
npxTsxOnlyExemptSources: ["curated"],
|
|
266
|
-
httpUrlPattern: { source: "^http:\\/\\/", flags: "i" },
|
|
267
|
-
placeholderPattern: { source: "\\bTODO\\b|\\bPLACEHOLDER\\b", flags: "i" },
|
|
268
|
-
allowedStacks: [
|
|
269
|
-
"haus",
|
|
270
|
-
"security",
|
|
271
|
-
"quality",
|
|
272
|
-
"frontend",
|
|
273
|
-
"backend",
|
|
274
|
-
"testing",
|
|
275
|
-
"review",
|
|
276
|
-
"workflow",
|
|
277
|
-
"reference-pack",
|
|
278
|
-
"core-skill",
|
|
279
|
-
"workflow-skill",
|
|
280
|
-
"stack-skill",
|
|
281
|
-
"review-skill",
|
|
282
|
-
"agent",
|
|
283
|
-
"hook",
|
|
284
|
-
"rule",
|
|
285
|
-
"react",
|
|
286
|
-
"typescript",
|
|
287
|
-
"php",
|
|
288
|
-
"csharp",
|
|
289
|
-
"vendure",
|
|
290
|
-
"vendure3",
|
|
291
|
-
"nestjs",
|
|
292
|
-
"graphql",
|
|
293
|
-
"nx",
|
|
294
|
-
"turbo",
|
|
295
|
-
"nextjs",
|
|
296
|
-
"react19",
|
|
297
|
-
"typescript5",
|
|
298
|
-
"vite8",
|
|
299
|
-
"tanstack-query",
|
|
300
|
-
"tanstack-router",
|
|
301
|
-
"radix",
|
|
302
|
-
"radix-ui",
|
|
303
|
-
"shadcn",
|
|
304
|
-
"shadcn-ui",
|
|
305
|
-
"tailwind",
|
|
306
|
-
"tailwindcss",
|
|
307
|
-
"scss",
|
|
308
|
-
"scss-modules",
|
|
309
|
-
"vue",
|
|
310
|
-
"expressjs",
|
|
311
|
-
"laravel",
|
|
312
|
-
"laravel-nova",
|
|
313
|
-
"wordpress",
|
|
314
|
-
"bedrock",
|
|
315
|
-
"elementor-pro",
|
|
316
|
-
"acf-pro",
|
|
317
|
-
"jetengine",
|
|
318
|
-
"dotnet",
|
|
319
|
-
"oidc",
|
|
320
|
-
"azure-ad",
|
|
321
|
-
"bankid",
|
|
322
|
-
"crypto",
|
|
323
|
-
"postgresql",
|
|
324
|
-
"mariadb",
|
|
325
|
-
"mssql",
|
|
326
|
-
"elasticsearch",
|
|
327
|
-
"yarn4",
|
|
328
|
-
"pnpm89",
|
|
329
|
-
"playwright",
|
|
330
|
-
"testing-library",
|
|
331
|
-
"phpunit",
|
|
332
|
-
"storybook",
|
|
333
|
-
"vitest",
|
|
334
|
-
"jest",
|
|
335
|
-
"redis",
|
|
336
|
-
"sanity",
|
|
337
|
-
"strapi",
|
|
338
|
-
"prisma",
|
|
339
|
-
"cms",
|
|
340
|
-
"database",
|
|
341
|
-
"mysql",
|
|
342
|
-
"saml2",
|
|
343
|
-
"next-auth",
|
|
344
|
-
"auth",
|
|
345
|
-
"expo",
|
|
346
|
-
"react-native",
|
|
347
|
-
"mobile",
|
|
348
|
-
"i18next",
|
|
349
|
-
"i18n",
|
|
350
|
-
"bullmq",
|
|
351
|
-
"queue",
|
|
352
|
-
"sentry",
|
|
353
|
-
"observability",
|
|
354
|
-
"tooling",
|
|
355
|
-
"prettier",
|
|
356
|
-
"eslint",
|
|
357
|
-
"missing-prettier",
|
|
358
|
-
"missing-eslint",
|
|
359
|
-
"docker",
|
|
360
|
-
"pm2",
|
|
361
|
-
"stripe",
|
|
362
|
-
"qliro",
|
|
363
|
-
"supabase",
|
|
364
|
-
"payments",
|
|
365
|
-
"myid",
|
|
366
|
-
"cgi",
|
|
367
|
-
"collection2",
|
|
368
|
-
"deployer-php"
|
|
369
|
-
],
|
|
370
|
-
alwaysAllowedTags: [
|
|
371
|
-
"haus",
|
|
372
|
-
"security",
|
|
373
|
-
"quality",
|
|
374
|
-
"review",
|
|
375
|
-
"workflow",
|
|
376
|
-
"baseline",
|
|
377
|
-
"project-instructions"
|
|
378
|
-
],
|
|
379
|
-
patternTagSuffixes: ["-patterns"]
|
|
380
|
-
};
|
|
381
|
-
|
|
382
|
-
// src/catalog/validation-rules.ts
|
|
383
|
-
var toRegExp = (r) => new RegExp(r.source, r.flags);
|
|
384
|
-
var FORBIDDEN_TAGS = validation_rules_default.forbiddenTags;
|
|
385
|
-
var REQUIRED_SKILL_FRONTMATTER = validation_rules_default.requiredSkillFrontmatter;
|
|
386
|
-
var RISKY_INSTALL_PATTERNS = validation_rules_default.riskyInstallPatterns.map(toRegExp);
|
|
387
|
-
var ALLOWED_NPX_PATTERN = toRegExp(validation_rules_default.allowedNpxPattern);
|
|
388
|
-
var ANY_NPX_PATTERN = toRegExp(validation_rules_default.anyNpxPattern);
|
|
389
|
-
var NPX_TSX_ONLY_EXEMPT_SOURCES = validation_rules_default.npxTsxOnlyExemptSources ?? [];
|
|
390
|
-
function isNpxTsxOnlyExempt(_itemType, itemSource) {
|
|
391
|
-
return Boolean(itemSource && NPX_TSX_ONLY_EXEMPT_SOURCES.includes(itemSource));
|
|
236
|
+
// src/catalog/path-safety.ts
|
|
237
|
+
import path3 from "node:path";
|
|
238
|
+
function isSafeCatalogPath(itemPath) {
|
|
239
|
+
if (!itemPath || path3.isAbsolute(itemPath) || itemPath.includes("\\")) return false;
|
|
240
|
+
const normalized = path3.normalize(itemPath);
|
|
241
|
+
return !normalized.startsWith("..") && !normalized.includes("/..");
|
|
392
242
|
}
|
|
393
|
-
function
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
399
|
-
return map;
|
|
243
|
+
function isSafeRelativeFilePath(rel) {
|
|
244
|
+
if (!rel || rel.startsWith("/") || rel.includes("\\") || rel.includes("//")) return false;
|
|
245
|
+
if (path3.isAbsolute(rel)) return false;
|
|
246
|
+
const normalized = path3.posix.normalize(rel.replace(/\\/g, "/"));
|
|
247
|
+
return normalized !== ".." && !normalized.startsWith("../") && !normalized.includes("/../");
|
|
400
248
|
}
|
|
401
|
-
function
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
249
|
+
function safeJoin(base, itemPath) {
|
|
250
|
+
if (!isSafeCatalogPath(itemPath)) return null;
|
|
251
|
+
const resolved = path3.resolve(base, itemPath);
|
|
252
|
+
return resolved.startsWith(base + path3.sep) || resolved === base ? resolved : null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/catalog/remote-catalog/http.ts
|
|
256
|
+
import path4 from "node:path";
|
|
257
|
+
import fs2 from "fs-extra";
|
|
258
|
+
async function fetchGuarded(url, extract) {
|
|
259
|
+
try {
|
|
260
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
261
|
+
if (!res.ok) {
|
|
262
|
+
warn(`Catalog fetch HTTP ${res.status}: ${url}`);
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
return await extract(res);
|
|
266
|
+
} catch (e) {
|
|
267
|
+
warn(`Catalog fetch failed (${e instanceof Error ? e.constructor.name : String(e)}): ${url}`);
|
|
268
|
+
return null;
|
|
409
269
|
}
|
|
410
|
-
return void 0;
|
|
411
270
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
var ALLOWED_STACKS = validation_rules_default.allowedStacks;
|
|
415
|
-
var ALWAYS_ALLOWED_TAGS = validation_rules_default.alwaysAllowedTags;
|
|
416
|
-
var PATTERN_TAG_SUFFIXES = validation_rules_default.patternTagSuffixes;
|
|
417
|
-
var ALLOWED_SET = new Set([...ALLOWED_STACKS, ...ALWAYS_ALLOWED_TAGS].map((t) => t.toLowerCase()));
|
|
418
|
-
function isTagAllowed(tag) {
|
|
419
|
-
const lower = tag.toLowerCase();
|
|
420
|
-
if (ALLOWED_SET.has(lower)) return true;
|
|
421
|
-
return PATTERN_TAG_SUFFIXES.some((suffix) => lower.endsWith(suffix));
|
|
271
|
+
async function fetchText(url) {
|
|
272
|
+
return fetchGuarded(url, (res) => res.text());
|
|
422
273
|
}
|
|
423
|
-
function
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
274
|
+
async function fetchBytes(url) {
|
|
275
|
+
return fetchGuarded(url, async (res) => Buffer.from(await res.arrayBuffer()));
|
|
276
|
+
}
|
|
277
|
+
async function writeTextIfChanged(dest, text) {
|
|
278
|
+
if (await fs2.pathExists(dest)) {
|
|
279
|
+
const local = await fs2.readFile(dest, "utf8");
|
|
280
|
+
if (local === text) return "unchanged";
|
|
281
|
+
await fs2.writeFile(dest, text, "utf8");
|
|
282
|
+
return "updated";
|
|
430
283
|
}
|
|
431
|
-
|
|
284
|
+
await fs2.ensureDir(path4.dirname(dest));
|
|
285
|
+
await fs2.writeFile(dest, text, "utf8");
|
|
286
|
+
return "created";
|
|
432
287
|
}
|
|
433
288
|
|
|
434
|
-
// src/catalog/
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
289
|
+
// src/catalog/remote-catalog/ref.ts
|
|
290
|
+
import os2 from "node:os";
|
|
291
|
+
import path5 from "node:path";
|
|
292
|
+
import fs3 from "fs-extra";
|
|
293
|
+
function isTestMode() {
|
|
294
|
+
return process.env["HAUS_TEST_MODE"] === "1" || process.env["NODE_ENV"] === "test";
|
|
438
295
|
}
|
|
439
|
-
function
|
|
440
|
-
|
|
441
|
-
const idx = text.toLowerCase().indexOf(marker.toLowerCase());
|
|
442
|
-
if (idx < 0) return "";
|
|
443
|
-
const tail = text.slice(idx + marker.length);
|
|
444
|
-
const next = tail.search(/\n##\s+/);
|
|
445
|
-
return next < 0 ? tail : tail.slice(0, next);
|
|
296
|
+
function getCacheDir() {
|
|
297
|
+
return process.env["HAUS_CATALOG_CACHE_DIR_OVERRIDE"] ?? path5.join(os2.homedir(), CATALOG_CACHE_SUBDIR);
|
|
446
298
|
}
|
|
447
|
-
|
|
448
|
-
|
|
299
|
+
var cachedCatalogRef;
|
|
300
|
+
var inFlightCatalogRef;
|
|
301
|
+
function getBundledCatalogRef() {
|
|
302
|
+
try {
|
|
303
|
+
const manifestPath2 = path5.join(packageRoot(), "library/catalog/manifest.json");
|
|
304
|
+
const raw = fs3.readFileSync(manifestPath2, "utf8");
|
|
305
|
+
const data = JSON.parse(raw);
|
|
306
|
+
if (typeof data.version === "string" && data.version) {
|
|
307
|
+
return data.version.startsWith("v") ? data.version : `v${data.version}`;
|
|
308
|
+
}
|
|
309
|
+
} catch {
|
|
310
|
+
}
|
|
311
|
+
return void 0;
|
|
449
312
|
}
|
|
450
|
-
function
|
|
451
|
-
const
|
|
452
|
-
if (!
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
const rest = lines[idx].replace(keyRe, "").trim();
|
|
458
|
-
if (!rest) return "";
|
|
459
|
-
if (!isYamlBlockScalarHeader(rest)) {
|
|
460
|
-
return rest.replace(/^["']|["']$/g, "").trim();
|
|
313
|
+
function getResolvedCatalogRef() {
|
|
314
|
+
const resolved = cachedCatalogRef ?? process.env["HAUS_CATALOG_REF"] ?? getBundledCatalogRef();
|
|
315
|
+
if (!resolved) {
|
|
316
|
+
warn(
|
|
317
|
+
"Could not determine catalog ref from cache, env, or bundled snapshot \u2014 falling back to main (moving target)."
|
|
318
|
+
);
|
|
319
|
+
return "main";
|
|
461
320
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
321
|
+
return resolved;
|
|
322
|
+
}
|
|
323
|
+
function isCatalogRefResolved() {
|
|
324
|
+
return cachedCatalogRef !== void 0 || process.env["HAUS_CATALOG_REF"] !== void 0;
|
|
325
|
+
}
|
|
326
|
+
var CATALOG_TAGS_API_URL = "https://api.github.com/repos/WeAreHausTech/haus-workflow-catalog/tags";
|
|
327
|
+
function parseSemverTag(tag) {
|
|
328
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(tag);
|
|
329
|
+
if (!match) return null;
|
|
330
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
331
|
+
}
|
|
332
|
+
function compareSemver(a, b) {
|
|
333
|
+
for (let i = 0; i < 3; i += 1) {
|
|
334
|
+
if (a[i] !== b[i]) return a[i] - b[i];
|
|
469
335
|
}
|
|
470
|
-
return
|
|
336
|
+
return 0;
|
|
471
337
|
}
|
|
472
|
-
function
|
|
473
|
-
|
|
338
|
+
function githubApiHeaders() {
|
|
339
|
+
const headers = { Accept: "application/vnd.github+json" };
|
|
340
|
+
const auth = process.env["HAUS_GITHUB_TOKEN"] ?? process.env["GITHUB_TOKEN"];
|
|
341
|
+
if (auth) headers["Authorization"] = `Bearer ${auth}`;
|
|
342
|
+
return headers;
|
|
474
343
|
}
|
|
475
|
-
function
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if (
|
|
344
|
+
async function fetchLatestCatalogTag() {
|
|
345
|
+
if (isTestMode() && process.env["HAUS_CATALOG_REMOTE_BASE"]) return null;
|
|
346
|
+
try {
|
|
347
|
+
const res = await fetch(CATALOG_TAGS_API_URL, {
|
|
348
|
+
signal: AbortSignal.timeout(5e3),
|
|
349
|
+
headers: githubApiHeaders()
|
|
350
|
+
});
|
|
351
|
+
if (!res.ok) return null;
|
|
352
|
+
const tags = await res.json();
|
|
353
|
+
const valid = tags.map((tag) => {
|
|
354
|
+
const name = typeof tag.name === "string" ? tag.name : "";
|
|
355
|
+
const semver2 = parseSemverTag(name);
|
|
356
|
+
return semver2 ? { name, semver: semver2 } : null;
|
|
357
|
+
}).filter(
|
|
358
|
+
(entry) => entry !== null
|
|
359
|
+
);
|
|
360
|
+
if (valid.length === 0) return null;
|
|
361
|
+
valid.sort((a, b) => compareSemver(b.semver, a.semver));
|
|
362
|
+
return valid[0].name;
|
|
363
|
+
} catch {
|
|
364
|
+
return null;
|
|
483
365
|
}
|
|
484
|
-
|
|
366
|
+
}
|
|
367
|
+
async function resolveCatalogRef(opts) {
|
|
368
|
+
const env = opts?.env ?? process.env;
|
|
369
|
+
if (env["HAUS_CATALOG_REF"]) {
|
|
370
|
+
if (env["HAUS_CATALOG_REF"] === "main") {
|
|
371
|
+
warn(
|
|
372
|
+
"HAUS_CATALOG_REF=main is set \u2014 fetching from the moving main branch. Pin to a release tag for reproducible installs."
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return env["HAUS_CATALOG_REF"];
|
|
376
|
+
}
|
|
377
|
+
const fetchLatest = opts?.fetchLatestTag ?? fetchLatestCatalogTag;
|
|
378
|
+
const tag = await fetchLatest();
|
|
379
|
+
if (tag !== null) return tag;
|
|
380
|
+
const fallback = opts?.fallbackRef;
|
|
381
|
+
if (fallback) {
|
|
382
|
+
warn(
|
|
383
|
+
`Tag resolution failed \u2014 using cached ref ${fallback}. To use latest, retry or set HAUS_CATALOG_REF explicitly.`
|
|
384
|
+
);
|
|
385
|
+
return fallback;
|
|
386
|
+
}
|
|
387
|
+
const bundled = getBundledCatalogRef();
|
|
388
|
+
if (bundled) {
|
|
389
|
+
warn(
|
|
390
|
+
`Tag resolution failed \u2014 using bundled snapshot ref ${bundled}. To use latest, retry or set HAUS_CATALOG_REF explicitly.`
|
|
391
|
+
);
|
|
392
|
+
return bundled;
|
|
393
|
+
}
|
|
394
|
+
warn(
|
|
395
|
+
"Tag resolution failed and no fallback ref is available. Set HAUS_CATALOG_REF explicitly to avoid fetching from main."
|
|
396
|
+
);
|
|
397
|
+
return "main";
|
|
398
|
+
}
|
|
399
|
+
async function remoteBase() {
|
|
400
|
+
if (isTestMode() && process.env["HAUS_CATALOG_REMOTE_BASE"]) {
|
|
401
|
+
return process.env["HAUS_CATALOG_REMOTE_BASE"];
|
|
402
|
+
}
|
|
403
|
+
if (cachedCatalogRef === void 0) {
|
|
404
|
+
if (!inFlightCatalogRef) {
|
|
405
|
+
inFlightCatalogRef = resolveCatalogRef({ fallbackRef: getBundledCatalogRef() }).then(
|
|
406
|
+
(ref) => {
|
|
407
|
+
cachedCatalogRef = ref;
|
|
408
|
+
return ref;
|
|
409
|
+
}
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
await inFlightCatalogRef;
|
|
413
|
+
}
|
|
414
|
+
return `${CATALOG_REPO_URL}/${cachedCatalogRef}`;
|
|
485
415
|
}
|
|
486
416
|
|
|
487
|
-
// src/catalog/
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const
|
|
492
|
-
for (
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
return
|
|
417
|
+
// src/catalog/remote-catalog/github-tree.ts
|
|
418
|
+
var cachedBlobPaths;
|
|
419
|
+
var inFlightBlobPaths;
|
|
420
|
+
function sanitizeRelativeFilePaths(files, label) {
|
|
421
|
+
const safe = [];
|
|
422
|
+
for (const rel of files) {
|
|
423
|
+
if (!isSafeRelativeFilePath(rel)) {
|
|
424
|
+
warn(`Rejected unsafe path in ${label}: ${rel}`);
|
|
425
|
+
return null;
|
|
496
426
|
}
|
|
497
|
-
|
|
498
|
-
|
|
427
|
+
safe.push(rel);
|
|
428
|
+
}
|
|
429
|
+
return safe;
|
|
430
|
+
}
|
|
431
|
+
async function listFilesRecursive(dir, base = dir) {
|
|
432
|
+
const out = [];
|
|
433
|
+
let entries;
|
|
434
|
+
try {
|
|
435
|
+
if (!await fs4.pathExists(dir)) return out;
|
|
436
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
437
|
+
} catch {
|
|
438
|
+
return out;
|
|
439
|
+
}
|
|
440
|
+
for (const entry of entries) {
|
|
441
|
+
const full = path6.join(dir, entry.name);
|
|
442
|
+
if (entry.isDirectory()) {
|
|
443
|
+
out.push(...await listFilesRecursive(full, base));
|
|
444
|
+
} else if (entry.isFile()) {
|
|
445
|
+
out.push(path6.relative(base, full).replace(/\\/g, "/"));
|
|
499
446
|
}
|
|
500
447
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
448
|
+
return out.sort();
|
|
449
|
+
}
|
|
450
|
+
async function listMockPrefixFiles(base, prefix) {
|
|
451
|
+
const text = await fetchText(`${base}/__haus_tree__/${encodeURIComponent(prefix)}`);
|
|
452
|
+
if (text === null) return null;
|
|
453
|
+
try {
|
|
454
|
+
const parsed = JSON.parse(text);
|
|
455
|
+
if (!Array.isArray(parsed) || !parsed.every((e) => typeof e === "string")) return null;
|
|
456
|
+
return parsed;
|
|
457
|
+
} catch {
|
|
458
|
+
return null;
|
|
504
459
|
}
|
|
505
|
-
|
|
460
|
+
}
|
|
461
|
+
async function fetchGitHubRecursiveBlobPaths(ref) {
|
|
462
|
+
try {
|
|
463
|
+
const headers = githubApiHeaders();
|
|
464
|
+
const commitRes = await fetch(`${CATALOG_GITHUB_API_URL}/commits/${encodeURIComponent(ref)}`, {
|
|
465
|
+
signal: AbortSignal.timeout(15e3),
|
|
466
|
+
headers
|
|
467
|
+
});
|
|
468
|
+
if (!commitRes.ok) return null;
|
|
469
|
+
const commit = await commitRes.json();
|
|
470
|
+
const treeSha = commit.commit.tree.sha;
|
|
471
|
+
const treeRes = await fetch(`${CATALOG_GITHUB_API_URL}/git/trees/${treeSha}?recursive=1`, {
|
|
472
|
+
signal: AbortSignal.timeout(3e4),
|
|
473
|
+
headers
|
|
474
|
+
});
|
|
475
|
+
if (!treeRes.ok) return null;
|
|
476
|
+
const tree = await treeRes.json();
|
|
477
|
+
if (tree.truncated) {
|
|
478
|
+
warn("Catalog GitHub tree listing was truncated \u2014 refusing partial cache sync");
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
return tree.tree.filter((e) => e.type === "blob").map((e) => e.path);
|
|
482
|
+
} catch {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async function fetchCatalogBlobPaths(_base) {
|
|
487
|
+
if (cachedBlobPaths) return cachedBlobPaths;
|
|
488
|
+
if (isTestMode() && process.env["HAUS_CATALOG_REMOTE_BASE"]) return null;
|
|
489
|
+
if (!inFlightBlobPaths) {
|
|
490
|
+
inFlightBlobPaths = (async () => {
|
|
491
|
+
const ref = getResolvedCatalogRef();
|
|
492
|
+
const paths = await fetchGitHubRecursiveBlobPaths(ref);
|
|
493
|
+
if (paths) cachedBlobPaths = paths;
|
|
494
|
+
inFlightBlobPaths = void 0;
|
|
495
|
+
return paths;
|
|
496
|
+
})();
|
|
497
|
+
}
|
|
498
|
+
return inFlightBlobPaths;
|
|
499
|
+
}
|
|
500
|
+
async function listFilesUnderCatalogPrefix(prefix, base) {
|
|
501
|
+
const normalized = prefix.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
502
|
+
const prefixSlash = `${normalized}/`;
|
|
503
|
+
let relFiles;
|
|
504
|
+
if (isTestMode() && process.env["HAUS_CATALOG_REMOTE_BASE"]) {
|
|
505
|
+
relFiles = await listMockPrefixFiles(base, normalized);
|
|
506
|
+
} else {
|
|
507
|
+
const blobs = await fetchCatalogBlobPaths(base);
|
|
508
|
+
if (!blobs) return null;
|
|
509
|
+
relFiles = blobs.filter((p) => p.startsWith(prefixSlash)).map((p) => p.slice(prefixSlash.length)).sort();
|
|
510
|
+
}
|
|
511
|
+
if (!relFiles) return null;
|
|
512
|
+
return sanitizeRelativeFilePaths(relFiles, normalized);
|
|
513
|
+
}
|
|
514
|
+
function _resetBlobPathCacheForNewSync() {
|
|
515
|
+
cachedBlobPaths = void 0;
|
|
516
|
+
inFlightBlobPaths = void 0;
|
|
506
517
|
}
|
|
507
518
|
|
|
508
519
|
// src/catalog/manifest-item-fields.ts
|
|
@@ -534,6 +545,42 @@ function validateReferences(itemId, references) {
|
|
|
534
545
|
}
|
|
535
546
|
return null;
|
|
536
547
|
}
|
|
548
|
+
function validateFormerIds(items) {
|
|
549
|
+
const failures = [];
|
|
550
|
+
const currentIds = new Set(items.map((item) => item.id).filter(Boolean));
|
|
551
|
+
const claimedBy = /* @__PURE__ */ new Map();
|
|
552
|
+
for (const item of items) {
|
|
553
|
+
const itemId = item.id ?? "unknown";
|
|
554
|
+
const formerIds = item.formerIds;
|
|
555
|
+
if (formerIds === void 0) continue;
|
|
556
|
+
if (Array.isArray(formerIds) && formerIds.length === 0) continue;
|
|
557
|
+
if (!isStringArray(formerIds)) {
|
|
558
|
+
failures.push(`${itemId}: formerIds must be a string array`);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
const seenInItem = /* @__PURE__ */ new Set();
|
|
562
|
+
for (const formerId of formerIds) {
|
|
563
|
+
if (seenInItem.has(formerId)) {
|
|
564
|
+
failures.push(`${itemId}: duplicate formerId "${formerId}"`);
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
seenInItem.add(formerId);
|
|
568
|
+
if (formerId === itemId) {
|
|
569
|
+
failures.push(`${itemId}: formerIds must not include own current id`);
|
|
570
|
+
}
|
|
571
|
+
if (currentIds.has(formerId) && formerId !== itemId) {
|
|
572
|
+
failures.push(`${itemId}: formerId "${formerId}" conflicts with another item's current id`);
|
|
573
|
+
}
|
|
574
|
+
const previousOwner = claimedBy.get(formerId);
|
|
575
|
+
if (previousOwner !== void 0) {
|
|
576
|
+
failures.push(`formerId "${formerId}" claimed by both ${previousOwner} and ${itemId}`);
|
|
577
|
+
} else {
|
|
578
|
+
claimedBy.set(formerId, itemId);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return failures;
|
|
583
|
+
}
|
|
537
584
|
function validateCuratedProvenance(item) {
|
|
538
585
|
if (item.source !== "curated") return null;
|
|
539
586
|
if (!isNonEmptyString(item.reviewStatus)) {
|
|
@@ -648,264 +695,332 @@ function parseManifest(json) {
|
|
|
648
695
|
if (typeof item.tokenEstimate !== "number" || !Number.isFinite(item.tokenEstimate)) {
|
|
649
696
|
return { ok: false, error: `${item.id}: tokenEstimate must be a finite number` };
|
|
650
697
|
}
|
|
698
|
+
if (item.formerIds !== void 0) {
|
|
699
|
+
if (Array.isArray(item.formerIds) && item.formerIds.length === 0) {
|
|
700
|
+
} else if (!isStringArray2(item.formerIds)) {
|
|
701
|
+
return { ok: false, error: `${item.id}: formerIds must be a string array` };
|
|
702
|
+
}
|
|
703
|
+
}
|
|
651
704
|
items.push(raw);
|
|
652
705
|
}
|
|
706
|
+
const formerIdFailures = validateFormerIds(items);
|
|
707
|
+
if (formerIdFailures.length > 0) {
|
|
708
|
+
return { ok: false, error: formerIdFailures[0] };
|
|
709
|
+
}
|
|
653
710
|
return { ok: true, manifest: { version: root.version, items } };
|
|
654
711
|
}
|
|
655
712
|
|
|
656
|
-
// src/catalog/remote-catalog.ts
|
|
657
|
-
function
|
|
658
|
-
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
const raw = fs2.readFileSync(manifestPath2, "utf8");
|
|
669
|
-
const data = JSON.parse(raw);
|
|
670
|
-
if (typeof data.version === "string" && data.version) {
|
|
671
|
-
return data.version.startsWith("v") ? data.version : `v${data.version}`;
|
|
672
|
-
}
|
|
673
|
-
} catch {
|
|
674
|
-
}
|
|
675
|
-
return void 0;
|
|
676
|
-
}
|
|
677
|
-
function getResolvedCatalogRef() {
|
|
678
|
-
const resolved = cachedCatalogRef ?? process.env["HAUS_CATALOG_REF"] ?? getBundledCatalogRef();
|
|
679
|
-
if (!resolved) {
|
|
680
|
-
warn(
|
|
681
|
-
"Could not determine catalog ref from cache, env, or bundled snapshot \u2014 falling back to main (moving target)."
|
|
682
|
-
);
|
|
683
|
-
return "main";
|
|
684
|
-
}
|
|
685
|
-
return resolved;
|
|
686
|
-
}
|
|
687
|
-
function isCatalogRefResolved() {
|
|
688
|
-
return cachedCatalogRef !== void 0 || process.env["HAUS_CATALOG_REF"] !== void 0;
|
|
689
|
-
}
|
|
690
|
-
async function resolveCatalogRef(opts) {
|
|
691
|
-
const env = opts?.env ?? process.env;
|
|
692
|
-
if (env["HAUS_CATALOG_REF"]) {
|
|
693
|
-
if (env["HAUS_CATALOG_REF"] === "main") {
|
|
694
|
-
warn(
|
|
695
|
-
"HAUS_CATALOG_REF=main is set \u2014 fetching from the moving main branch. Pin to a release tag for reproducible installs."
|
|
696
|
-
);
|
|
697
|
-
}
|
|
698
|
-
return env["HAUS_CATALOG_REF"];
|
|
699
|
-
}
|
|
700
|
-
const fetchLatest = opts?.fetchLatestTag ?? fetchLatestCatalogTag;
|
|
701
|
-
const tag = await fetchLatest();
|
|
702
|
-
if (tag !== null) return tag;
|
|
703
|
-
const fallback = opts?.fallbackRef;
|
|
704
|
-
if (fallback) {
|
|
705
|
-
warn(
|
|
706
|
-
`Tag resolution failed \u2014 using cached ref ${fallback}. To use latest, retry or set HAUS_CATALOG_REF explicitly.`
|
|
707
|
-
);
|
|
708
|
-
return fallback;
|
|
709
|
-
}
|
|
710
|
-
const bundled = getBundledCatalogRef();
|
|
711
|
-
if (bundled) {
|
|
712
|
-
warn(
|
|
713
|
-
`Tag resolution failed \u2014 using bundled snapshot ref ${bundled}. To use latest, retry or set HAUS_CATALOG_REF explicitly.`
|
|
714
|
-
);
|
|
715
|
-
return bundled;
|
|
716
|
-
}
|
|
717
|
-
warn(
|
|
718
|
-
"Tag resolution failed and no fallback ref is available. Set HAUS_CATALOG_REF explicitly to avoid fetching from main."
|
|
719
|
-
);
|
|
720
|
-
return "main";
|
|
721
|
-
}
|
|
722
|
-
async function remoteBase() {
|
|
723
|
-
if (isTestMode() && process.env["HAUS_CATALOG_REMOTE_BASE"]) {
|
|
724
|
-
return process.env["HAUS_CATALOG_REMOTE_BASE"];
|
|
725
|
-
}
|
|
726
|
-
if (cachedCatalogRef === void 0) {
|
|
727
|
-
cachedCatalogRef = await resolveCatalogRef({ fallbackRef: getBundledCatalogRef() });
|
|
728
|
-
}
|
|
729
|
-
return `${CATALOG_REPO_URL}/${cachedCatalogRef}`;
|
|
730
|
-
}
|
|
731
|
-
async function fetchText(url) {
|
|
732
|
-
try {
|
|
733
|
-
const res = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
734
|
-
if (!res.ok) {
|
|
735
|
-
warn(`Catalog fetch HTTP ${res.status}: ${url}`);
|
|
736
|
-
return null;
|
|
737
|
-
}
|
|
738
|
-
return await res.text();
|
|
739
|
-
} catch (e) {
|
|
740
|
-
warn(`Catalog fetch failed (${e instanceof Error ? e.constructor.name : String(e)}): ${url}`);
|
|
741
|
-
return null;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
async function fetchBytes(url) {
|
|
745
|
-
try {
|
|
746
|
-
const res = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
747
|
-
if (!res.ok) {
|
|
748
|
-
warn(`Catalog fetch HTTP ${res.status}: ${url}`);
|
|
749
|
-
return null;
|
|
750
|
-
}
|
|
751
|
-
return Buffer.from(await res.arrayBuffer());
|
|
752
|
-
} catch (e) {
|
|
753
|
-
warn(`Catalog fetch failed (${e instanceof Error ? e.constructor.name : String(e)}): ${url}`);
|
|
754
|
-
return null;
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
async function fetchRemoteManifest() {
|
|
758
|
-
const base = await remoteBase();
|
|
759
|
-
const text = await fetchText(`${base}/manifest.json`);
|
|
760
|
-
if (!text) return null;
|
|
761
|
-
const parsed = parseManifest(text);
|
|
762
|
-
if (!parsed.ok) {
|
|
763
|
-
warn(`Remote manifest failed schema validation: ${parsed.error}`);
|
|
764
|
-
return null;
|
|
765
|
-
}
|
|
766
|
-
if (!parsed.manifest.items.length) return null;
|
|
767
|
-
return parsed.manifest;
|
|
768
|
-
}
|
|
769
|
-
async function writeTextIfChanged(dest, text) {
|
|
770
|
-
if (await fs2.pathExists(dest)) {
|
|
771
|
-
const local = await fs2.readFile(dest, "utf8");
|
|
772
|
-
if (local === text) return "unchanged";
|
|
773
|
-
await fs2.writeFile(dest, text, "utf8");
|
|
774
|
-
return "updated";
|
|
775
|
-
}
|
|
776
|
-
await fs2.ensureDir(path3.dirname(dest));
|
|
777
|
-
await fs2.writeFile(dest, text, "utf8");
|
|
778
|
-
return "created";
|
|
713
|
+
// src/catalog/remote-catalog/manifest.ts
|
|
714
|
+
async function fetchRemoteManifest() {
|
|
715
|
+
const base = await remoteBase();
|
|
716
|
+
const text = await fetchText(`${base}/manifest.json`);
|
|
717
|
+
if (!text) return null;
|
|
718
|
+
const parsed = parseManifest(text);
|
|
719
|
+
if (!parsed.ok) {
|
|
720
|
+
warn(`Remote manifest failed schema validation: ${parsed.error}`);
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
if (!parsed.manifest.items.length) return null;
|
|
724
|
+
return parsed.manifest;
|
|
779
725
|
}
|
|
726
|
+
|
|
727
|
+
// src/catalog/remote-catalog/workflow-template.ts
|
|
728
|
+
import path7 from "node:path";
|
|
729
|
+
import fs5 from "fs-extra";
|
|
780
730
|
var WORKFLOW_TEMPLATE_REL = "templates/agentic-workflow-standard.md";
|
|
781
731
|
async function readWorkflowTemplate(opts = {}) {
|
|
782
|
-
const dest =
|
|
732
|
+
const dest = path7.join(getCacheDir(), WORKFLOW_TEMPLATE_REL);
|
|
783
733
|
const base = await remoteBase();
|
|
784
734
|
const text = await fetchText(`${base}/${WORKFLOW_TEMPLATE_REL}`);
|
|
785
735
|
if (text === null) {
|
|
786
|
-
if (await
|
|
736
|
+
if (await fs5.pathExists(dest)) return fs5.readFile(dest, "utf8");
|
|
787
737
|
return null;
|
|
788
738
|
}
|
|
789
739
|
if (!opts.dryRun) {
|
|
790
740
|
await writeTextIfChanged(dest, text);
|
|
791
|
-
} else if (await fs2.pathExists(dest)) {
|
|
792
|
-
return fs2.readFile(dest, "utf8");
|
|
793
741
|
}
|
|
794
742
|
return text;
|
|
795
743
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
744
|
+
|
|
745
|
+
// src/catalog/remote-catalog/sync.ts
|
|
746
|
+
import path8 from "node:path";
|
|
747
|
+
import fs6 from "fs-extra";
|
|
748
|
+
|
|
749
|
+
// library/catalog/validation-rules.json
|
|
750
|
+
var validation_rules_default = {
|
|
751
|
+
forbiddenTags: [
|
|
752
|
+
"python",
|
|
753
|
+
"django",
|
|
754
|
+
"go",
|
|
755
|
+
"rust",
|
|
756
|
+
"java",
|
|
757
|
+
"spring",
|
|
758
|
+
"kotlin",
|
|
759
|
+
"swift",
|
|
760
|
+
"android",
|
|
761
|
+
"flutter",
|
|
762
|
+
"dart",
|
|
763
|
+
"c++",
|
|
764
|
+
"perl",
|
|
765
|
+
"defi",
|
|
766
|
+
"trading"
|
|
767
|
+
],
|
|
768
|
+
requiredSkillFrontmatter: ["description"],
|
|
769
|
+
riskyInstallPatterns: [
|
|
770
|
+
{ source: "\\bnpx\\s+-y\\b", flags: "i" },
|
|
771
|
+
{ source: "\\bnpx\\s+--yes\\b", flags: "i" },
|
|
772
|
+
{ source: "\\byarn\\s+dlx\\b", flags: "i" },
|
|
773
|
+
{ source: "\\bpnpm\\s+dlx\\b", flags: "i" }
|
|
774
|
+
],
|
|
775
|
+
allowedNpxPattern: { source: "\\bnpx\\s+tsx\\b", flags: "i" },
|
|
776
|
+
anyNpxPattern: { source: "\\bnpx\\s+\\S+", flags: "i" },
|
|
777
|
+
npxTsxOnlyExemptSources: ["curated"],
|
|
778
|
+
httpUrlPattern: { source: "^http:\\/\\/", flags: "i" },
|
|
779
|
+
placeholderPattern: { source: "\\bTODO\\b|\\bPLACEHOLDER\\b", flags: "i" },
|
|
780
|
+
allowedStacks: [
|
|
781
|
+
"haus",
|
|
782
|
+
"security",
|
|
783
|
+
"quality",
|
|
784
|
+
"frontend",
|
|
785
|
+
"backend",
|
|
786
|
+
"testing",
|
|
787
|
+
"review",
|
|
788
|
+
"workflow",
|
|
789
|
+
"reference-pack",
|
|
790
|
+
"core-skill",
|
|
791
|
+
"workflow-skill",
|
|
792
|
+
"stack-skill",
|
|
793
|
+
"review-skill",
|
|
794
|
+
"agent",
|
|
795
|
+
"hook",
|
|
796
|
+
"rule",
|
|
797
|
+
"react",
|
|
798
|
+
"typescript",
|
|
799
|
+
"php",
|
|
800
|
+
"csharp",
|
|
801
|
+
"vendure",
|
|
802
|
+
"vendure3",
|
|
803
|
+
"nestjs",
|
|
804
|
+
"graphql",
|
|
805
|
+
"nx",
|
|
806
|
+
"turbo",
|
|
807
|
+
"nextjs",
|
|
808
|
+
"react19",
|
|
809
|
+
"typescript5",
|
|
810
|
+
"vite8",
|
|
811
|
+
"tanstack-query",
|
|
812
|
+
"tanstack-router",
|
|
813
|
+
"radix",
|
|
814
|
+
"radix-ui",
|
|
815
|
+
"shadcn",
|
|
816
|
+
"shadcn-ui",
|
|
817
|
+
"tailwind",
|
|
818
|
+
"tailwindcss",
|
|
819
|
+
"scss",
|
|
820
|
+
"scss-modules",
|
|
821
|
+
"vue",
|
|
822
|
+
"expressjs",
|
|
823
|
+
"laravel",
|
|
824
|
+
"laravel-nova",
|
|
825
|
+
"wordpress",
|
|
826
|
+
"bedrock",
|
|
827
|
+
"elementor-pro",
|
|
828
|
+
"acf-pro",
|
|
829
|
+
"jetengine",
|
|
830
|
+
"dotnet",
|
|
831
|
+
"oidc",
|
|
832
|
+
"azure-ad",
|
|
833
|
+
"bankid",
|
|
834
|
+
"crypto",
|
|
835
|
+
"postgresql",
|
|
836
|
+
"mariadb",
|
|
837
|
+
"mssql",
|
|
838
|
+
"elasticsearch",
|
|
839
|
+
"yarn4",
|
|
840
|
+
"pnpm89",
|
|
841
|
+
"playwright",
|
|
842
|
+
"testing-library",
|
|
843
|
+
"phpunit",
|
|
844
|
+
"storybook",
|
|
845
|
+
"vitest",
|
|
846
|
+
"jest",
|
|
847
|
+
"redis",
|
|
848
|
+
"sanity",
|
|
849
|
+
"strapi",
|
|
850
|
+
"prisma",
|
|
851
|
+
"cms",
|
|
852
|
+
"database",
|
|
853
|
+
"mysql",
|
|
854
|
+
"saml2",
|
|
855
|
+
"next-auth",
|
|
856
|
+
"auth",
|
|
857
|
+
"expo",
|
|
858
|
+
"react-native",
|
|
859
|
+
"mobile",
|
|
860
|
+
"i18next",
|
|
861
|
+
"i18n",
|
|
862
|
+
"bullmq",
|
|
863
|
+
"queue",
|
|
864
|
+
"sentry",
|
|
865
|
+
"observability",
|
|
866
|
+
"tooling",
|
|
867
|
+
"prettier",
|
|
868
|
+
"eslint",
|
|
869
|
+
"missing-prettier",
|
|
870
|
+
"missing-eslint",
|
|
871
|
+
"docker",
|
|
872
|
+
"pm2",
|
|
873
|
+
"stripe",
|
|
874
|
+
"qliro",
|
|
875
|
+
"supabase",
|
|
876
|
+
"payments",
|
|
877
|
+
"myid",
|
|
878
|
+
"cgi",
|
|
879
|
+
"collection2",
|
|
880
|
+
"deployer-php"
|
|
881
|
+
],
|
|
882
|
+
alwaysAllowedTags: [
|
|
883
|
+
"haus",
|
|
884
|
+
"security",
|
|
885
|
+
"quality",
|
|
886
|
+
"review",
|
|
887
|
+
"workflow",
|
|
888
|
+
"baseline",
|
|
889
|
+
"project-instructions"
|
|
890
|
+
],
|
|
891
|
+
patternTagSuffixes: ["-patterns"]
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
// src/catalog/validation-rules.ts
|
|
895
|
+
var toRegExp = (r) => new RegExp(r.source, r.flags);
|
|
896
|
+
var FORBIDDEN_TAGS = validation_rules_default.forbiddenTags;
|
|
897
|
+
var REQUIRED_SKILL_FRONTMATTER = validation_rules_default.requiredSkillFrontmatter;
|
|
898
|
+
var RISKY_INSTALL_PATTERNS = validation_rules_default.riskyInstallPatterns.map(toRegExp);
|
|
899
|
+
var ALLOWED_NPX_PATTERN = toRegExp(validation_rules_default.allowedNpxPattern);
|
|
900
|
+
var ANY_NPX_PATTERN = toRegExp(validation_rules_default.anyNpxPattern);
|
|
901
|
+
var NPX_TSX_ONLY_EXEMPT_SOURCES = validation_rules_default.npxTsxOnlyExemptSources ?? [];
|
|
902
|
+
function isNpxTsxOnlyExempt(_itemType, itemSource) {
|
|
903
|
+
return Boolean(itemSource && NPX_TSX_ONLY_EXEMPT_SOURCES.includes(itemSource));
|
|
800
904
|
}
|
|
801
|
-
function
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
905
|
+
function buildItemPathSourceMap(items) {
|
|
906
|
+
const map = /* @__PURE__ */ new Map();
|
|
907
|
+
for (const item of items) {
|
|
908
|
+
if (!item.path) continue;
|
|
909
|
+
map.set(String(item.path).replace(/\\/g, "/"), item.source);
|
|
910
|
+
}
|
|
911
|
+
return map;
|
|
806
912
|
}
|
|
807
|
-
function
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
913
|
+
function resolveMarkdownItemSource(relPath, pathSourceMap) {
|
|
914
|
+
const norm = relPath.replace(/\\/g, "/");
|
|
915
|
+
let cursor = norm;
|
|
916
|
+
while (true) {
|
|
917
|
+
if (pathSourceMap.has(cursor)) return pathSourceMap.get(cursor);
|
|
918
|
+
const slash = cursor.lastIndexOf("/");
|
|
919
|
+
if (slash === -1) break;
|
|
920
|
+
cursor = cursor.slice(0, slash);
|
|
921
|
+
}
|
|
922
|
+
return void 0;
|
|
812
923
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
924
|
+
var HTTP_URL_PATTERN = toRegExp(validation_rules_default.httpUrlPattern);
|
|
925
|
+
var PLACEHOLDER_PATTERN = toRegExp(validation_rules_default.placeholderPattern);
|
|
926
|
+
var ALLOWED_STACKS = validation_rules_default.allowedStacks;
|
|
927
|
+
var ALWAYS_ALLOWED_TAGS = validation_rules_default.alwaysAllowedTags;
|
|
928
|
+
var PATTERN_TAG_SUFFIXES = validation_rules_default.patternTagSuffixes;
|
|
929
|
+
var ALLOWED_SET = new Set([...ALLOWED_STACKS, ...ALWAYS_ALLOWED_TAGS].map((t) => t.toLowerCase()));
|
|
930
|
+
function isTagAllowed(tag) {
|
|
931
|
+
const lower = tag.toLowerCase();
|
|
932
|
+
if (ALLOWED_SET.has(lower)) return true;
|
|
933
|
+
return PATTERN_TAG_SUFFIXES.some((suffix) => lower.endsWith(suffix));
|
|
934
|
+
}
|
|
935
|
+
function auditDisallowedTags(items) {
|
|
936
|
+
const failures = [];
|
|
937
|
+
for (const item of items) {
|
|
938
|
+
if (!item.id) continue;
|
|
939
|
+
for (const tag of Array.isArray(item.tags) ? item.tags : []) {
|
|
940
|
+
if (!isTagAllowed(tag)) failures.push(`${item.id}: tag not in allowlist: "${tag}"`);
|
|
819
941
|
}
|
|
820
|
-
safe.push(rel);
|
|
821
942
|
}
|
|
822
|
-
return
|
|
943
|
+
return failures;
|
|
823
944
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
945
|
+
|
|
946
|
+
// src/catalog/forbidden-content.ts
|
|
947
|
+
var PROSE_FORBIDDEN_TAGS = FORBIDDEN_TAGS.filter((t) => t.toLowerCase() !== "go");
|
|
948
|
+
function escapeRegExp(value) {
|
|
949
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
828
950
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
951
|
+
function extractUseWhenSection(text) {
|
|
952
|
+
const marker = "## Use when";
|
|
953
|
+
const idx = text.toLowerCase().indexOf(marker.toLowerCase());
|
|
954
|
+
if (idx < 0) return "";
|
|
955
|
+
const tail = text.slice(idx + marker.length);
|
|
956
|
+
const next = tail.search(/\n##\s+/);
|
|
957
|
+
return next < 0 ? tail : tail.slice(0, next);
|
|
832
958
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
959
|
+
function isYamlBlockScalarHeader(rest) {
|
|
960
|
+
return /^[>|][-+]?(\d+)?(?:\s+#.*)?$/.test(rest);
|
|
961
|
+
}
|
|
962
|
+
function extractFrontmatterValue(text, key) {
|
|
963
|
+
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
964
|
+
if (!m) return "";
|
|
965
|
+
const lines = m[1].split(/\r?\n/);
|
|
966
|
+
const keyRe = new RegExp(`^${escapeRegExp(key)}:[ \\t]*`);
|
|
967
|
+
const idx = lines.findIndex((l) => keyRe.test(l));
|
|
968
|
+
if (idx < 0) return "";
|
|
969
|
+
const rest = lines[idx].replace(keyRe, "").trim();
|
|
970
|
+
if (!rest) return "";
|
|
971
|
+
if (!isYamlBlockScalarHeader(rest)) {
|
|
972
|
+
return rest.replace(/^["']|["']$/g, "").trim();
|
|
841
973
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
974
|
+
const body = [];
|
|
975
|
+
for (let j = idx + 1; j < lines.length; j++) {
|
|
976
|
+
const line2 = lines[j];
|
|
977
|
+
if (/^[a-zA-Z_][\w.-]*:[ \t]/.test(line2)) break;
|
|
978
|
+
if (line2.trim() === "") continue;
|
|
979
|
+
if (/^\s+/.test(line2)) body.push(line2.trimStart());
|
|
980
|
+
else break;
|
|
849
981
|
}
|
|
850
|
-
return
|
|
982
|
+
return body.join(" ").replace(/\s+/g, " ").trim();
|
|
851
983
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
984
|
+
function extractFrontmatterDescription(text) {
|
|
985
|
+
return extractFrontmatterValue(text, "description");
|
|
986
|
+
}
|
|
987
|
+
function auditForbiddenTagsInText(text, label) {
|
|
988
|
+
const body = `${extractFrontmatterDescription(text)}
|
|
989
|
+
${extractUseWhenSection(text)}`;
|
|
990
|
+
if (!body.trim()) return [];
|
|
991
|
+
const failures = [];
|
|
992
|
+
for (const word of PROSE_FORBIDDEN_TAGS) {
|
|
993
|
+
const re = new RegExp(`\\b${escapeRegExp(word)}\\b`, "i");
|
|
994
|
+
if (re.test(body)) failures.push(`${label}: forbidden stack/tag "${word}" in content`);
|
|
861
995
|
}
|
|
996
|
+
return failures;
|
|
862
997
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
});
|
|
877
|
-
if (!treeRes.ok) return null;
|
|
878
|
-
const tree = await treeRes.json();
|
|
879
|
-
if (tree.truncated) {
|
|
880
|
-
warn("Catalog GitHub tree listing was truncated \u2014 refusing partial cache sync");
|
|
881
|
-
return null;
|
|
998
|
+
|
|
999
|
+
// src/catalog/ingest-catalog.ts
|
|
1000
|
+
function validateCatalogItem(item, content2) {
|
|
1001
|
+
const label = item.id;
|
|
1002
|
+
const checkNonTsxNpx = !isNpxTsxOnlyExempt(item.type, item.source);
|
|
1003
|
+
const lines = content2.split(/\r?\n/);
|
|
1004
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1005
|
+
const line2 = lines[i] ?? "";
|
|
1006
|
+
if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2))) {
|
|
1007
|
+
return { ok: false, reason: `${label}: risky install pattern at line ${i + 1}` };
|
|
1008
|
+
}
|
|
1009
|
+
if (checkNonTsxNpx && ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
|
|
1010
|
+
return { ok: false, reason: `${label}: disallowed npx at line ${i + 1}` };
|
|
882
1011
|
}
|
|
883
|
-
return tree.tree.filter((e) => e.type === "blob").map((e) => e.path);
|
|
884
|
-
} catch {
|
|
885
|
-
return null;
|
|
886
1012
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
if (isTestMode() && process.env["HAUS_CATALOG_REMOTE_BASE"]) return null;
|
|
891
|
-
const ref = getResolvedCatalogRef();
|
|
892
|
-
const paths = await fetchGitHubRecursiveBlobPaths(ref);
|
|
893
|
-
if (paths) cachedBlobPaths = paths;
|
|
894
|
-
return paths;
|
|
895
|
-
}
|
|
896
|
-
async function listFilesUnderCatalogPrefix(prefix, base) {
|
|
897
|
-
const normalized = prefix.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
898
|
-
const prefixSlash = `${normalized}/`;
|
|
899
|
-
let relFiles;
|
|
900
|
-
if (isTestMode() && process.env["HAUS_CATALOG_REMOTE_BASE"]) {
|
|
901
|
-
relFiles = await listMockPrefixFiles(base, normalized);
|
|
902
|
-
} else {
|
|
903
|
-
const blobs = await fetchCatalogBlobPaths(base);
|
|
904
|
-
if (!blobs) return null;
|
|
905
|
-
relFiles = blobs.filter((p) => p.startsWith(prefixSlash)).map((p) => p.slice(prefixSlash.length)).sort();
|
|
1013
|
+
const tagFailures = auditForbiddenTagsInText(content2, label);
|
|
1014
|
+
if (tagFailures.length > 0) {
|
|
1015
|
+
return { ok: false, reason: tagFailures[0] };
|
|
906
1016
|
}
|
|
907
|
-
|
|
908
|
-
|
|
1017
|
+
return { ok: true };
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/catalog/remote-catalog/sync.ts
|
|
1021
|
+
var KNOWN_ITEM_TYPES = /* @__PURE__ */ new Set(["skill", "agent", "template", "command", "config"]);
|
|
1022
|
+
function isMarkdownPath(rel) {
|
|
1023
|
+
return rel.toLowerCase().endsWith(".md");
|
|
909
1024
|
}
|
|
910
1025
|
async function fetchPrefixFiles(catalogPrefix, relFiles, base, label) {
|
|
911
1026
|
const fetched = [];
|
|
@@ -941,7 +1056,7 @@ function validateMarkdownFiles(item, fetched) {
|
|
|
941
1056
|
return true;
|
|
942
1057
|
}
|
|
943
1058
|
async function directoryMatchesFetched(destDir, fetched) {
|
|
944
|
-
if (!await
|
|
1059
|
+
if (!await fs6.pathExists(destDir)) return false;
|
|
945
1060
|
const existing = await listFilesRecursive(destDir);
|
|
946
1061
|
const relSet = new Set(fetched.map((f) => f.rel));
|
|
947
1062
|
if (existing.length !== fetched.length) return false;
|
|
@@ -949,30 +1064,30 @@ async function directoryMatchesFetched(destDir, fetched) {
|
|
|
949
1064
|
if (!relSet.has(rel)) return false;
|
|
950
1065
|
}
|
|
951
1066
|
for (const file of fetched) {
|
|
952
|
-
const dest =
|
|
953
|
-
if (!await
|
|
1067
|
+
const dest = path8.join(destDir, file.rel);
|
|
1068
|
+
if (!await fs6.pathExists(dest)) return false;
|
|
954
1069
|
if (file.kind === "text") {
|
|
955
|
-
const local = await
|
|
1070
|
+
const local = await fs6.readFile(dest, "utf8");
|
|
956
1071
|
if (local !== file.body) return false;
|
|
957
1072
|
} else {
|
|
958
|
-
const local = await
|
|
1073
|
+
const local = await fs6.readFile(dest);
|
|
959
1074
|
if (!local.equals(file.body)) return false;
|
|
960
1075
|
}
|
|
961
1076
|
}
|
|
962
1077
|
return true;
|
|
963
1078
|
}
|
|
964
1079
|
async function writeFetchedDirectory(destDir, fetched) {
|
|
965
|
-
if (await
|
|
966
|
-
await
|
|
1080
|
+
if (await fs6.pathExists(destDir)) {
|
|
1081
|
+
await fs6.remove(destDir);
|
|
967
1082
|
}
|
|
968
|
-
await
|
|
1083
|
+
await fs6.ensureDir(destDir);
|
|
969
1084
|
for (const file of fetched) {
|
|
970
|
-
const dest =
|
|
971
|
-
await
|
|
1085
|
+
const dest = path8.join(destDir, file.rel);
|
|
1086
|
+
await fs6.ensureDir(path8.dirname(dest));
|
|
972
1087
|
if (file.kind === "text") {
|
|
973
|
-
await
|
|
1088
|
+
await fs6.writeFile(dest, file.body, "utf8");
|
|
974
1089
|
} else {
|
|
975
|
-
await
|
|
1090
|
+
await fs6.writeFile(dest, file.body);
|
|
976
1091
|
}
|
|
977
1092
|
}
|
|
978
1093
|
}
|
|
@@ -1012,7 +1127,7 @@ async function syncDirectoryFromPrefix(item, catalogPrefix, destDir, base, opts)
|
|
|
1012
1127
|
}
|
|
1013
1128
|
}
|
|
1014
1129
|
}
|
|
1015
|
-
const existed = await
|
|
1130
|
+
const existed = await fs6.pathExists(destDir);
|
|
1016
1131
|
if (await directoryMatchesFetched(destDir, fetched)) {
|
|
1017
1132
|
return "unchanged";
|
|
1018
1133
|
}
|
|
@@ -1129,7 +1244,7 @@ async function syncOneItem(item, base) {
|
|
|
1129
1244
|
}
|
|
1130
1245
|
}
|
|
1131
1246
|
async function syncRemoteCatalog() {
|
|
1132
|
-
|
|
1247
|
+
_resetBlobPathCacheForNewSync();
|
|
1133
1248
|
const manifest = await fetchRemoteManifest();
|
|
1134
1249
|
if (!manifest) {
|
|
1135
1250
|
warn("Remote catalog fetch failed \u2014 using bundled catalog");
|
|
@@ -1138,9 +1253,9 @@ async function syncRemoteCatalog() {
|
|
|
1138
1253
|
const { version, items } = manifest;
|
|
1139
1254
|
const cacheDir = getCacheDir();
|
|
1140
1255
|
try {
|
|
1141
|
-
await
|
|
1142
|
-
await
|
|
1143
|
-
|
|
1256
|
+
await fs6.ensureDir(cacheDir);
|
|
1257
|
+
await fs6.writeFile(
|
|
1258
|
+
path8.join(cacheDir, "manifest.json"),
|
|
1144
1259
|
`${JSON.stringify({ version, items }, null, 2)}
|
|
1145
1260
|
`,
|
|
1146
1261
|
"utf8"
|
|
@@ -1171,44 +1286,9 @@ async function syncRemoteCatalog() {
|
|
|
1171
1286
|
}
|
|
1172
1287
|
return { newItems, refreshed, unchanged, failed };
|
|
1173
1288
|
}
|
|
1174
|
-
var CATALOG_TAGS_API_URL = "https://api.github.com/repos/WeAreHausTech/haus-workflow-catalog/tags";
|
|
1175
|
-
function parseSemverTag(tag) {
|
|
1176
|
-
const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(tag);
|
|
1177
|
-
if (!match) return null;
|
|
1178
|
-
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
1179
|
-
}
|
|
1180
|
-
function compareSemver(a, b) {
|
|
1181
|
-
for (let i = 0; i < 3; i += 1) {
|
|
1182
|
-
if (a[i] !== b[i]) return a[i] - b[i];
|
|
1183
|
-
}
|
|
1184
|
-
return 0;
|
|
1185
|
-
}
|
|
1186
|
-
async function fetchLatestCatalogTag() {
|
|
1187
|
-
if (isTestMode() && process.env["HAUS_CATALOG_REMOTE_BASE"]) return null;
|
|
1188
|
-
try {
|
|
1189
|
-
const res = await fetch(CATALOG_TAGS_API_URL, {
|
|
1190
|
-
signal: AbortSignal.timeout(5e3),
|
|
1191
|
-
headers: githubApiHeaders()
|
|
1192
|
-
});
|
|
1193
|
-
if (!res.ok) return null;
|
|
1194
|
-
const tags = await res.json();
|
|
1195
|
-
const valid = tags.map((tag) => {
|
|
1196
|
-
const name = typeof tag.name === "string" ? tag.name : "";
|
|
1197
|
-
const semver2 = parseSemverTag(name);
|
|
1198
|
-
return semver2 ? { name, semver: semver2 } : null;
|
|
1199
|
-
}).filter(
|
|
1200
|
-
(entry) => entry !== null
|
|
1201
|
-
);
|
|
1202
|
-
if (valid.length === 0) return null;
|
|
1203
|
-
valid.sort((a, b) => compareSemver(b.semver, a.semver));
|
|
1204
|
-
return valid[0].name;
|
|
1205
|
-
} catch {
|
|
1206
|
-
return null;
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
1289
|
async function getCacheManifestAge() {
|
|
1210
1290
|
try {
|
|
1211
|
-
const stat = await
|
|
1291
|
+
const stat = await fs6.stat(path8.join(getCacheDir(), "manifest.json"));
|
|
1212
1292
|
return Date.now() - stat.mtimeMs;
|
|
1213
1293
|
} catch {
|
|
1214
1294
|
return null;
|
|
@@ -1222,30 +1302,30 @@ async function loadCatalogContext(root) {
|
|
|
1222
1302
|
const data2 = await readJson(envPath);
|
|
1223
1303
|
return {
|
|
1224
1304
|
items: data2?.items ?? [],
|
|
1225
|
-
contentRoot:
|
|
1305
|
+
contentRoot: path9.dirname(envPath),
|
|
1226
1306
|
source: "fixture"
|
|
1227
1307
|
};
|
|
1228
1308
|
}
|
|
1229
1309
|
const cacheDir = getCacheDir();
|
|
1230
|
-
const cacheManifestPath =
|
|
1310
|
+
const cacheManifestPath = path9.join(cacheDir, "manifest.json");
|
|
1231
1311
|
const cacheData = await readJson(cacheManifestPath);
|
|
1232
1312
|
if (cacheData?.items?.length) {
|
|
1233
1313
|
return { items: cacheData.items, contentRoot: cacheDir, source: "cache" };
|
|
1234
1314
|
}
|
|
1235
|
-
const localManifest =
|
|
1315
|
+
const localManifest = path9.join(root, "library/catalog/manifest.json");
|
|
1236
1316
|
const localData = await readJson(localManifest);
|
|
1237
1317
|
if (localData?.items?.length) {
|
|
1238
1318
|
return {
|
|
1239
1319
|
items: localData.items,
|
|
1240
|
-
contentRoot:
|
|
1320
|
+
contentRoot: path9.dirname(localManifest),
|
|
1241
1321
|
source: "local"
|
|
1242
1322
|
};
|
|
1243
1323
|
}
|
|
1244
|
-
const packageManifest =
|
|
1324
|
+
const packageManifest = path9.join(packageRoot(), "library/catalog/manifest.json");
|
|
1245
1325
|
const data = await readJson(packageManifest);
|
|
1246
1326
|
return {
|
|
1247
1327
|
items: data?.items ?? [],
|
|
1248
|
-
contentRoot:
|
|
1328
|
+
contentRoot: path9.dirname(packageManifest),
|
|
1249
1329
|
source: "bundled"
|
|
1250
1330
|
};
|
|
1251
1331
|
}
|
|
@@ -1254,17 +1334,63 @@ async function loadCatalog(root) {
|
|
|
1254
1334
|
return ctx.items;
|
|
1255
1335
|
}
|
|
1256
1336
|
function catalogItemContentPath(contentRoot, item) {
|
|
1257
|
-
return
|
|
1337
|
+
return path9.join(contentRoot, item.path);
|
|
1258
1338
|
}
|
|
1259
1339
|
|
|
1260
1340
|
// src/claude/write-claude-files.ts
|
|
1261
|
-
import
|
|
1262
|
-
import
|
|
1341
|
+
import path19 from "node:path";
|
|
1342
|
+
import fs18 from "fs-extra";
|
|
1343
|
+
|
|
1344
|
+
// src/catalog/former-ids.ts
|
|
1345
|
+
function normalizeFormerIds(value) {
|
|
1346
|
+
if (value === void 0 || value === null) return [];
|
|
1347
|
+
if (!Array.isArray(value) || value.length === 0) return [];
|
|
1348
|
+
if (!value.every((entry) => typeof entry === "string")) return [];
|
|
1349
|
+
return value.filter((entry) => entry.length > 0);
|
|
1350
|
+
}
|
|
1351
|
+
function assertFormerIdsShape(itemId, formerIds) {
|
|
1352
|
+
if (formerIds === void 0) return;
|
|
1353
|
+
if (Array.isArray(formerIds) && formerIds.length === 0) return;
|
|
1354
|
+
if (!Array.isArray(formerIds) || !formerIds.every((entry) => typeof entry === "string")) {
|
|
1355
|
+
throw new Error(`${itemId}: formerIds must be a string array`);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
function buildFormerIdMap(items) {
|
|
1359
|
+
const map = /* @__PURE__ */ new Map();
|
|
1360
|
+
const currentIds = new Set(items.map((item) => item.id));
|
|
1361
|
+
for (const item of items) {
|
|
1362
|
+
assertFormerIdsShape(item.id, item.formerIds);
|
|
1363
|
+
const formerIds = item.formerIds ?? [];
|
|
1364
|
+
if (!Array.isArray(formerIds) || formerIds.length === 0) continue;
|
|
1365
|
+
for (const formerId of formerIds) {
|
|
1366
|
+
if (currentIds.has(formerId) && formerId !== item.id) {
|
|
1367
|
+
throw new Error(
|
|
1368
|
+
`formerId ${formerId} on ${item.id} conflicts with another item's current id`
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
const existingOwner = map.get(formerId);
|
|
1372
|
+
if (existingOwner !== void 0) {
|
|
1373
|
+
throw new Error(
|
|
1374
|
+
`duplicate formerId ${formerId} claimed by both ${existingOwner} and ${item.id}`
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
map.set(formerId, item.id);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return map;
|
|
1381
|
+
}
|
|
1382
|
+
function findFormerIdMigrations(lockItems, manifestItems) {
|
|
1383
|
+
const formerIdMap = buildFormerIdMap(manifestItems);
|
|
1384
|
+
return lockItems.flatMap(({ id }) => {
|
|
1385
|
+
const newId = formerIdMap.get(id);
|
|
1386
|
+
return newId === void 0 ? [] : [{ oldId: id, newId }];
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1263
1389
|
|
|
1264
1390
|
// src/update/hash-installed.ts
|
|
1265
|
-
import
|
|
1391
|
+
import path10 from "node:path";
|
|
1266
1392
|
import fg2 from "fast-glob";
|
|
1267
|
-
import
|
|
1393
|
+
import fs7 from "fs-extra";
|
|
1268
1394
|
|
|
1269
1395
|
// src/claude/haus-managed-header.ts
|
|
1270
1396
|
function parseHausManagedAttrs(line2) {
|
|
@@ -1303,6 +1429,14 @@ function parseHausManagedHeader(line2) {
|
|
|
1303
1429
|
|
|
1304
1430
|
// src/update/hash-installed.ts
|
|
1305
1431
|
var EMPTY_LOCK_PATHS_TOKEN = "haus-lock:empty-paths";
|
|
1432
|
+
function digestFileContent(buf) {
|
|
1433
|
+
const asText = buf.toString("utf8");
|
|
1434
|
+
const roundTrip = Buffer.from(asText, "utf8");
|
|
1435
|
+
if (roundTrip.equals(buf)) {
|
|
1436
|
+
return hashText(normaliseLF(asText));
|
|
1437
|
+
}
|
|
1438
|
+
return hashText(buf);
|
|
1439
|
+
}
|
|
1306
1440
|
async function hashInstalledPaths(root, relPaths) {
|
|
1307
1441
|
if (relPaths.length === 0) {
|
|
1308
1442
|
return hashText(EMPTY_LOCK_PATHS_TOKEN);
|
|
@@ -1310,21 +1444,21 @@ async function hashInstalledPaths(root, relPaths) {
|
|
|
1310
1444
|
const normalized = [...new Set(relPaths.map((p) => p.replace(/\\/g, "/")))].sort();
|
|
1311
1445
|
const fileDigests = [];
|
|
1312
1446
|
for (const rel of normalized) {
|
|
1313
|
-
const abs =
|
|
1314
|
-
if (!await
|
|
1315
|
-
const stat = await
|
|
1447
|
+
const abs = path10.join(root, rel);
|
|
1448
|
+
if (!await fs7.pathExists(abs)) continue;
|
|
1449
|
+
const stat = await fs7.stat(abs);
|
|
1316
1450
|
if (stat.isFile()) {
|
|
1317
|
-
const body = await
|
|
1318
|
-
fileDigests.push({ rel, digest:
|
|
1451
|
+
const body = await fs7.readFile(abs);
|
|
1452
|
+
fileDigests.push({ rel, digest: digestFileContent(body) });
|
|
1319
1453
|
continue;
|
|
1320
1454
|
}
|
|
1321
1455
|
if (!stat.isDirectory()) continue;
|
|
1322
1456
|
const inner = await fg2("**/*", { cwd: abs, onlyFiles: true, dot: true });
|
|
1323
1457
|
for (const sub of inner.sort()) {
|
|
1324
|
-
const relFile =
|
|
1325
|
-
const absFile =
|
|
1326
|
-
const body = await
|
|
1327
|
-
fileDigests.push({ rel: relFile, digest:
|
|
1458
|
+
const relFile = path10.join(rel, sub).replace(/\\/g, "/");
|
|
1459
|
+
const absFile = path10.join(abs, sub);
|
|
1460
|
+
const body = await fs7.readFile(absFile);
|
|
1461
|
+
fileDigests.push({ rel: relFile, digest: digestFileContent(body) });
|
|
1328
1462
|
}
|
|
1329
1463
|
}
|
|
1330
1464
|
if (fileDigests.length === 0) {
|
|
@@ -1335,7 +1469,7 @@ async function hashInstalledPaths(root, relPaths) {
|
|
|
1335
1469
|
}
|
|
1336
1470
|
|
|
1337
1471
|
// src/claude/managed-write.ts
|
|
1338
|
-
import
|
|
1472
|
+
import fs8 from "fs-extra";
|
|
1339
1473
|
|
|
1340
1474
|
// src/utils/diff.ts
|
|
1341
1475
|
import { createTwoFilesPatch } from "diff";
|
|
@@ -1361,10 +1495,11 @@ function summarizeDiff(diffText) {
|
|
|
1361
1495
|
|
|
1362
1496
|
// src/claude/managed-write.ts
|
|
1363
1497
|
async function writeManagedText(root, filePath, nextText, dryRun) {
|
|
1364
|
-
const
|
|
1498
|
+
const existed = await fs8.pathExists(filePath);
|
|
1499
|
+
const prev = existed ? await fs8.readFile(filePath, "utf8") : "";
|
|
1365
1500
|
const printable = displayPath(root, filePath);
|
|
1366
1501
|
if (dryRun) {
|
|
1367
|
-
if (!
|
|
1502
|
+
if (!existed) {
|
|
1368
1503
|
log(createUnifiedDiff(printable, "", nextText));
|
|
1369
1504
|
} else if (hasTextChanged(prev, nextText)) {
|
|
1370
1505
|
log(createUnifiedDiff(printable, prev, nextText));
|
|
@@ -1373,12 +1508,14 @@ async function writeManagedText(root, filePath, nextText, dryRun) {
|
|
|
1373
1508
|
}
|
|
1374
1509
|
return;
|
|
1375
1510
|
}
|
|
1376
|
-
if (hasTextChanged(prev, nextText)
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1511
|
+
if (!existed || hasTextChanged(prev, nextText)) {
|
|
1512
|
+
if (existed) {
|
|
1513
|
+
const diffText = createUnifiedDiff(printable, prev, nextText);
|
|
1514
|
+
const summary = summarizeDiff(diffText);
|
|
1515
|
+
log(`Overwriting ${printable} (diff +${summary.additions} -${summary.deletions})`);
|
|
1516
|
+
}
|
|
1517
|
+
await writeText(filePath, nextText);
|
|
1380
1518
|
}
|
|
1381
|
-
await writeText(filePath, nextText);
|
|
1382
1519
|
}
|
|
1383
1520
|
async function writeManagedJson(root, filePath, value, dryRun) {
|
|
1384
1521
|
const nextText = `${JSON.stringify(value, null, 2)}
|
|
@@ -1393,18 +1530,18 @@ function buildAllowRules() {
|
|
|
1393
1530
|
}
|
|
1394
1531
|
|
|
1395
1532
|
// src/install/settings-merge.ts
|
|
1396
|
-
import
|
|
1397
|
-
import
|
|
1533
|
+
import path12 from "node:path";
|
|
1534
|
+
import fs9 from "fs-extra";
|
|
1398
1535
|
|
|
1399
1536
|
// src/install/manifest.ts
|
|
1400
1537
|
import os3 from "node:os";
|
|
1401
|
-
import
|
|
1538
|
+
import path11 from "node:path";
|
|
1402
1539
|
var MANIFEST_SCHEMA = "haus-install-manifest/1";
|
|
1403
1540
|
function globalClaudeDir() {
|
|
1404
|
-
return
|
|
1541
|
+
return path11.join(os3.homedir(), ".claude");
|
|
1405
1542
|
}
|
|
1406
1543
|
function hausManifestPath() {
|
|
1407
|
-
return
|
|
1544
|
+
return path11.join(globalClaudeDir(), "haus", "install-manifest.json");
|
|
1408
1545
|
}
|
|
1409
1546
|
async function readManifest() {
|
|
1410
1547
|
return readJson(hausManifestPath());
|
|
@@ -1424,7 +1561,7 @@ function buildManifest(source, files, hooks) {
|
|
|
1424
1561
|
|
|
1425
1562
|
// src/install/settings-merge.ts
|
|
1426
1563
|
function settingsJsonPath() {
|
|
1427
|
-
return
|
|
1564
|
+
return path12.join(globalClaudeDir(), "settings.json");
|
|
1428
1565
|
}
|
|
1429
1566
|
async function readSettings() {
|
|
1430
1567
|
const filePath = settingsJsonPath();
|
|
@@ -1539,77 +1676,62 @@ function reconcileManagedRules(existing, prevTracked, newRules) {
|
|
|
1539
1676
|
const removed = existing.filter((rule) => !newSet.has(rule));
|
|
1540
1677
|
return { rules: [...userRules, ...tracked], tracked, added, removed };
|
|
1541
1678
|
}
|
|
1679
|
+
function createRuleTier(permKey, trackedKey) {
|
|
1680
|
+
function merge(settings, rules) {
|
|
1681
|
+
const existing = settings.permissions?.[permKey] ?? [];
|
|
1682
|
+
const tracked = settings._haus?.[trackedKey] ?? [];
|
|
1683
|
+
const {
|
|
1684
|
+
rules: nextRules,
|
|
1685
|
+
tracked: nextTracked,
|
|
1686
|
+
added
|
|
1687
|
+
} = reconcileManagedRules(existing, tracked, rules);
|
|
1688
|
+
const updated = { ...settings };
|
|
1689
|
+
updated.permissions = { ...settings.permissions ?? {}, [permKey]: nextRules };
|
|
1690
|
+
updated._haus = {
|
|
1691
|
+
hooks: settings._haus?.hooks ?? [],
|
|
1692
|
+
...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
|
|
1693
|
+
...permKey !== "deny" && settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
|
|
1694
|
+
...permKey !== "allow" && settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {},
|
|
1695
|
+
...permKey !== "ask" && settings._haus?.askRules ? { askRules: settings._haus.askRules } : {},
|
|
1696
|
+
[trackedKey]: nextTracked
|
|
1697
|
+
};
|
|
1698
|
+
return { settings: updated, addedRules: added };
|
|
1699
|
+
}
|
|
1700
|
+
function strip(settings) {
|
|
1701
|
+
const prevHaus = settings._haus;
|
|
1702
|
+
const ownedRules = prevHaus?.[trackedKey];
|
|
1703
|
+
if (!ownedRules || ownedRules.length === 0) return settings;
|
|
1704
|
+
const ownedSet = new Set(ownedRules);
|
|
1705
|
+
const updated = { ...settings };
|
|
1706
|
+
const remaining = (settings.permissions?.[permKey] ?? []).filter((rule) => !ownedSet.has(rule));
|
|
1707
|
+
const permissions = { ...settings.permissions ?? {} };
|
|
1708
|
+
if (remaining.length > 0) permissions[permKey] = remaining;
|
|
1709
|
+
else delete permissions[permKey];
|
|
1710
|
+
if (Object.keys(permissions).length > 0) updated.permissions = permissions;
|
|
1711
|
+
else delete updated.permissions;
|
|
1712
|
+
const haus = { ...prevHaus };
|
|
1713
|
+
delete haus[trackedKey];
|
|
1714
|
+
const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.denyRules?.length ?? 0) > 0 || (haus.allowRules?.length ?? 0) > 0 || (haus.askRules?.length ?? 0) > 0;
|
|
1715
|
+
if (stillTracking) updated._haus = haus;
|
|
1716
|
+
else delete updated._haus;
|
|
1717
|
+
return updated;
|
|
1718
|
+
}
|
|
1719
|
+
return { merge, strip };
|
|
1720
|
+
}
|
|
1721
|
+
var denyTier = createRuleTier("deny", "denyRules");
|
|
1722
|
+
var allowTier = createRuleTier("allow", "allowRules");
|
|
1723
|
+
var askTier = createRuleTier("ask", "askRules");
|
|
1542
1724
|
function mergeDenyRules(settings, rules) {
|
|
1543
|
-
|
|
1544
|
-
const trackedDeny = settings._haus?.denyRules ?? [];
|
|
1545
|
-
const { rules: deny, tracked, added } = reconcileManagedRules(existingDeny, trackedDeny, rules);
|
|
1546
|
-
const updated = { ...settings };
|
|
1547
|
-
updated.permissions = {
|
|
1548
|
-
...settings.permissions ?? {},
|
|
1549
|
-
deny
|
|
1550
|
-
};
|
|
1551
|
-
updated._haus = {
|
|
1552
|
-
hooks: settings._haus?.hooks ?? [],
|
|
1553
|
-
...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
|
|
1554
|
-
denyRules: tracked,
|
|
1555
|
-
...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {},
|
|
1556
|
-
...settings._haus?.askRules ? { askRules: settings._haus.askRules } : {}
|
|
1557
|
-
};
|
|
1558
|
-
return { settings: updated, addedRules: added };
|
|
1725
|
+
return denyTier.merge(settings, rules);
|
|
1559
1726
|
}
|
|
1560
1727
|
function mergeAllowRules(settings, rules) {
|
|
1561
|
-
|
|
1562
|
-
const trackedAllow = settings._haus?.allowRules ?? [];
|
|
1563
|
-
const { rules: allow, tracked, added } = reconcileManagedRules(existingAllow, trackedAllow, rules);
|
|
1564
|
-
const updated = { ...settings };
|
|
1565
|
-
updated.permissions = {
|
|
1566
|
-
...settings.permissions ?? {},
|
|
1567
|
-
allow
|
|
1568
|
-
};
|
|
1569
|
-
updated._haus = {
|
|
1570
|
-
hooks: settings._haus?.hooks ?? [],
|
|
1571
|
-
...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
|
|
1572
|
-
...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
|
|
1573
|
-
allowRules: tracked,
|
|
1574
|
-
...settings._haus?.askRules ? { askRules: settings._haus.askRules } : {}
|
|
1575
|
-
};
|
|
1576
|
-
return { settings: updated, addedRules: added };
|
|
1728
|
+
return allowTier.merge(settings, rules);
|
|
1577
1729
|
}
|
|
1578
1730
|
function stripHausAllow(settings) {
|
|
1579
|
-
|
|
1580
|
-
if (!prevHaus?.allowRules || prevHaus.allowRules.length === 0) return settings;
|
|
1581
|
-
const ownedSet = new Set(prevHaus.allowRules);
|
|
1582
|
-
const updated = { ...settings };
|
|
1583
|
-
const remainingAllow = (settings.permissions?.allow ?? []).filter((rule) => !ownedSet.has(rule));
|
|
1584
|
-
const permissions = { ...settings.permissions ?? {} };
|
|
1585
|
-
if (remainingAllow.length > 0) permissions.allow = remainingAllow;
|
|
1586
|
-
else delete permissions.allow;
|
|
1587
|
-
if (Object.keys(permissions).length > 0) updated.permissions = permissions;
|
|
1588
|
-
else delete updated.permissions;
|
|
1589
|
-
const haus = { ...prevHaus };
|
|
1590
|
-
delete haus.allowRules;
|
|
1591
|
-
const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.denyRules?.length ?? 0) > 0 || (haus.askRules?.length ?? 0) > 0;
|
|
1592
|
-
if (stillTracking) updated._haus = haus;
|
|
1593
|
-
else delete updated._haus;
|
|
1594
|
-
return updated;
|
|
1731
|
+
return allowTier.strip(settings);
|
|
1595
1732
|
}
|
|
1596
1733
|
function stripHausDeny(settings) {
|
|
1597
|
-
|
|
1598
|
-
if (!prevHaus?.denyRules || prevHaus.denyRules.length === 0) return settings;
|
|
1599
|
-
const ownedSet = new Set(prevHaus.denyRules);
|
|
1600
|
-
const updated = { ...settings };
|
|
1601
|
-
const remainingDeny = (settings.permissions?.deny ?? []).filter((rule) => !ownedSet.has(rule));
|
|
1602
|
-
const permissions = { ...settings.permissions ?? {} };
|
|
1603
|
-
if (remainingDeny.length > 0) permissions.deny = remainingDeny;
|
|
1604
|
-
else delete permissions.deny;
|
|
1605
|
-
if (Object.keys(permissions).length > 0) updated.permissions = permissions;
|
|
1606
|
-
else delete updated.permissions;
|
|
1607
|
-
const haus = { ...prevHaus };
|
|
1608
|
-
delete haus.denyRules;
|
|
1609
|
-
const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.allowRules?.length ?? 0) > 0 || (haus.askRules?.length ?? 0) > 0;
|
|
1610
|
-
if (stillTracking) updated._haus = haus;
|
|
1611
|
-
else delete updated._haus;
|
|
1612
|
-
return updated;
|
|
1734
|
+
return denyTier.strip(settings);
|
|
1613
1735
|
}
|
|
1614
1736
|
function stripHausHooks(settings) {
|
|
1615
1737
|
if (!settings._haus) return settings;
|
|
@@ -1631,45 +1753,15 @@ function stripHausHooks(settings) {
|
|
|
1631
1753
|
return rest;
|
|
1632
1754
|
}
|
|
1633
1755
|
function mergeAskRules(settings, rules) {
|
|
1634
|
-
|
|
1635
|
-
const trackedAsk = settings._haus?.askRules ?? [];
|
|
1636
|
-
const { rules: ask2, tracked, added } = reconcileManagedRules(existingAsk, trackedAsk, rules);
|
|
1637
|
-
const updated = { ...settings };
|
|
1638
|
-
updated.permissions = {
|
|
1639
|
-
...settings.permissions ?? {},
|
|
1640
|
-
ask: ask2
|
|
1641
|
-
};
|
|
1642
|
-
updated._haus = {
|
|
1643
|
-
hooks: settings._haus?.hooks ?? [],
|
|
1644
|
-
...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
|
|
1645
|
-
...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
|
|
1646
|
-
...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {},
|
|
1647
|
-
askRules: tracked
|
|
1648
|
-
};
|
|
1649
|
-
return { settings: updated, addedRules: added };
|
|
1756
|
+
return askTier.merge(settings, rules);
|
|
1650
1757
|
}
|
|
1651
1758
|
function stripHausAsk(settings) {
|
|
1652
|
-
|
|
1653
|
-
if (!prevHaus?.askRules || prevHaus.askRules.length === 0) return settings;
|
|
1654
|
-
const ownedSet = new Set(prevHaus.askRules);
|
|
1655
|
-
const updated = { ...settings };
|
|
1656
|
-
const remainingAsk = (settings.permissions?.ask ?? []).filter((rule) => !ownedSet.has(rule));
|
|
1657
|
-
const permissions = { ...settings.permissions ?? {} };
|
|
1658
|
-
if (remainingAsk.length > 0) permissions.ask = remainingAsk;
|
|
1659
|
-
else delete permissions.ask;
|
|
1660
|
-
if (Object.keys(permissions).length > 0) updated.permissions = permissions;
|
|
1661
|
-
else delete updated.permissions;
|
|
1662
|
-
const haus = { ...prevHaus };
|
|
1663
|
-
delete haus.askRules;
|
|
1664
|
-
const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.denyRules?.length ?? 0) > 0 || (haus.allowRules?.length ?? 0) > 0;
|
|
1665
|
-
if (stillTracking) updated._haus = haus;
|
|
1666
|
-
else delete updated._haus;
|
|
1667
|
-
return updated;
|
|
1759
|
+
return askTier.strip(settings);
|
|
1668
1760
|
}
|
|
1669
1761
|
async function loadHooksFragment(fragmentPath) {
|
|
1670
1762
|
let raw;
|
|
1671
1763
|
try {
|
|
1672
|
-
raw = await
|
|
1764
|
+
raw = await fs9.readJson(fragmentPath);
|
|
1673
1765
|
} catch {
|
|
1674
1766
|
return [];
|
|
1675
1767
|
}
|
|
@@ -1784,8 +1876,8 @@ function buildDenyRules() {
|
|
|
1784
1876
|
for (const command of DENY_COMMANDS) {
|
|
1785
1877
|
rules.push(`Bash(${command}:*)`);
|
|
1786
1878
|
}
|
|
1787
|
-
for (const
|
|
1788
|
-
const pattern = DENY_DIRS.has(
|
|
1879
|
+
for (const path53 of DENY_PATHS) {
|
|
1880
|
+
const pattern = DENY_DIRS.has(path53) ? `${path53}/**` : path53;
|
|
1789
1881
|
for (const tool of FILE_TOOLS) {
|
|
1790
1882
|
rules.push(`${tool}(${pattern})`);
|
|
1791
1883
|
}
|
|
@@ -1857,9 +1949,9 @@ async function applyProjectSettingsMerge(root) {
|
|
|
1857
1949
|
}
|
|
1858
1950
|
|
|
1859
1951
|
// src/claude/superpowers-install.ts
|
|
1860
|
-
import
|
|
1952
|
+
import path13 from "node:path";
|
|
1861
1953
|
import fg3 from "fast-glob";
|
|
1862
|
-
import
|
|
1954
|
+
import fs10 from "fs-extra";
|
|
1863
1955
|
var SUPERPOWERS_ORIGIN_SOURCE_ID = "superpowers-pcvelz";
|
|
1864
1956
|
var PATH_REWRITES = [
|
|
1865
1957
|
["skills/shared/", ".claude/skills/shared/"]
|
|
@@ -1874,41 +1966,41 @@ function rewriteSuperpowersMarkdown(text) {
|
|
|
1874
1966
|
async function rewriteMarkdownTree(dir) {
|
|
1875
1967
|
const files = await fg3("**/*.md", { cwd: dir, onlyFiles: true, dot: true });
|
|
1876
1968
|
for (const rel of files) {
|
|
1877
|
-
const abs =
|
|
1878
|
-
const text = await
|
|
1969
|
+
const abs = path13.join(dir, rel);
|
|
1970
|
+
const text = await fs10.readFile(abs, "utf8");
|
|
1879
1971
|
const rewritten = rewriteSuperpowersMarkdown(text);
|
|
1880
1972
|
if (rewritten !== text) {
|
|
1881
|
-
await
|
|
1973
|
+
await fs10.writeFile(abs, rewritten, "utf8");
|
|
1882
1974
|
}
|
|
1883
1975
|
}
|
|
1884
1976
|
}
|
|
1885
1977
|
async function installCatalogSkill(sourcePath, destination, opts) {
|
|
1886
1978
|
if (opts.dryRun) return;
|
|
1887
|
-
await
|
|
1888
|
-
if (await
|
|
1889
|
-
await
|
|
1979
|
+
await fs10.ensureDir(path13.dirname(destination));
|
|
1980
|
+
if (await fs10.pathExists(destination)) {
|
|
1981
|
+
await fs10.remove(destination);
|
|
1890
1982
|
}
|
|
1891
|
-
await
|
|
1983
|
+
await fs10.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
1892
1984
|
if (opts.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID) {
|
|
1893
1985
|
await rewriteMarkdownTree(destination);
|
|
1894
1986
|
}
|
|
1895
1987
|
}
|
|
1896
1988
|
async function installSuperpowersShared(contentRoot, projectRoot, dryRun) {
|
|
1897
|
-
const source =
|
|
1898
|
-
if (!await
|
|
1989
|
+
const source = path13.join(contentRoot, SUPERPOWERS_SHARED_CATALOG_REL);
|
|
1990
|
+
if (!await fs10.pathExists(source)) return null;
|
|
1899
1991
|
const destination = claudePath(projectRoot, "skills", "shared");
|
|
1900
|
-
if (dryRun) return
|
|
1901
|
-
await
|
|
1902
|
-
if (await
|
|
1903
|
-
await
|
|
1992
|
+
if (dryRun) return path13.relative(projectRoot, destination);
|
|
1993
|
+
await fs10.ensureDir(path13.dirname(destination));
|
|
1994
|
+
if (await fs10.pathExists(destination)) {
|
|
1995
|
+
await fs10.remove(destination);
|
|
1904
1996
|
}
|
|
1905
|
-
await
|
|
1997
|
+
await fs10.copy(source, destination, { overwrite: true, errorOnExist: false });
|
|
1906
1998
|
await rewriteMarkdownTree(destination);
|
|
1907
|
-
return
|
|
1999
|
+
return path13.relative(projectRoot, destination);
|
|
1908
2000
|
}
|
|
1909
2001
|
|
|
1910
2002
|
// src/claude/verify-hooks-contract.ts
|
|
1911
|
-
import
|
|
2003
|
+
import fs11 from "fs-extra";
|
|
1912
2004
|
|
|
1913
2005
|
// src/claude/load-hooks.ts
|
|
1914
2006
|
var CANONICAL_HOOKS = {
|
|
@@ -1988,7 +2080,7 @@ async function assertPostApplySettingsHausContract(root) {
|
|
|
1988
2080
|
}
|
|
1989
2081
|
async function verifyProjectSettingsHooksContract(root) {
|
|
1990
2082
|
const settingsPath = claudePath(root, "settings.json");
|
|
1991
|
-
if (!await
|
|
2083
|
+
if (!await fs11.pathExists(settingsPath)) {
|
|
1992
2084
|
return {
|
|
1993
2085
|
ok: true,
|
|
1994
2086
|
skipped: true,
|
|
@@ -2015,8 +2107,8 @@ async function verifyProjectSettingsHooksContract(root) {
|
|
|
2015
2107
|
}
|
|
2016
2108
|
|
|
2017
2109
|
// src/claude/write-decisions-seed.ts
|
|
2018
|
-
import
|
|
2019
|
-
import
|
|
2110
|
+
import path14 from "node:path";
|
|
2111
|
+
import fs12 from "fs-extra";
|
|
2020
2112
|
|
|
2021
2113
|
// library/catalog/decisions-triggers.json
|
|
2022
2114
|
var decisions_triggers_default = {
|
|
@@ -2059,17 +2151,17 @@ Why we chose X \u2014 short, dated, write-once decision log for humans and agent
|
|
|
2059
2151
|
| --- | ----- | -------------- | ------ |
|
|
2060
2152
|
`;
|
|
2061
2153
|
async function writeDecisionsSeed(root, dryRun) {
|
|
2062
|
-
const decisionsDir =
|
|
2063
|
-
const readmePath =
|
|
2064
|
-
if (await
|
|
2065
|
-
if (!dryRun) await
|
|
2154
|
+
const decisionsDir = path14.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
2155
|
+
const readmePath = path14.join(decisionsDir, "README.md");
|
|
2156
|
+
if (await fs12.pathExists(readmePath)) return null;
|
|
2157
|
+
if (!dryRun) await fs12.ensureDir(decisionsDir);
|
|
2066
2158
|
await writeManagedText(root, readmePath, SEED_README, dryRun);
|
|
2067
2159
|
return readmePath;
|
|
2068
2160
|
}
|
|
2069
2161
|
|
|
2070
2162
|
// src/claude/write-prettierignore.ts
|
|
2071
|
-
import
|
|
2072
|
-
import
|
|
2163
|
+
import path15 from "node:path";
|
|
2164
|
+
import fs13 from "fs-extra";
|
|
2073
2165
|
var PRETTIERIGNORE_BEGIN = "# HAUS:BEGIN haus-managed v=1";
|
|
2074
2166
|
var PRETTIERIGNORE_END = "# HAUS:END haus-managed";
|
|
2075
2167
|
var IGNORED_PATHS = [".haus-workflow/"];
|
|
@@ -2125,16 +2217,16 @@ ${block}
|
|
|
2125
2217
|
`;
|
|
2126
2218
|
}
|
|
2127
2219
|
async function writePrettierIgnore(root, dryRun) {
|
|
2128
|
-
const filePath =
|
|
2129
|
-
const prev = await
|
|
2220
|
+
const filePath = path15.join(root, ".prettierignore");
|
|
2221
|
+
const prev = await fs13.pathExists(filePath) ? await fs13.readFile(filePath, "utf8") : "";
|
|
2130
2222
|
const next = injectPrettierIgnoreBlock(prev, buildPrettierIgnoreBlock());
|
|
2131
2223
|
await writeManagedText(root, filePath, next, dryRun);
|
|
2132
2224
|
return filePath;
|
|
2133
2225
|
}
|
|
2134
2226
|
|
|
2135
2227
|
// src/claude/write-root-claude-md.ts
|
|
2136
|
-
import
|
|
2137
|
-
import
|
|
2228
|
+
import path16 from "node:path";
|
|
2229
|
+
import fs14 from "fs-extra";
|
|
2138
2230
|
var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
|
|
2139
2231
|
var BLOCK_END = "<!-- HAUS:END haus-imports -->";
|
|
2140
2232
|
var IMPORT_CONTENT = `@.haus-workflow/WORKFLOW.md
|
|
@@ -2165,12 +2257,20 @@ function findImportBlockRange(content2) {
|
|
|
2165
2257
|
}
|
|
2166
2258
|
function stripHausBlock(existing) {
|
|
2167
2259
|
const range = findImportBlockRange(existing);
|
|
2168
|
-
if (
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2260
|
+
if (range) {
|
|
2261
|
+
const before = existing.slice(0, range.beginStart);
|
|
2262
|
+
const after = existing.slice(range.endEnd);
|
|
2263
|
+
const merged = `${before}${after}`.replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
2264
|
+
return merged.length > 0 ? `${merged}
|
|
2173
2265
|
` : "";
|
|
2266
|
+
}
|
|
2267
|
+
const loneBegin = findLineMarker2(existing, BLOCK_BEGIN);
|
|
2268
|
+
if (loneBegin) {
|
|
2269
|
+
const before = existing.slice(0, loneBegin.start).trimEnd();
|
|
2270
|
+
return before.length > 0 ? `${before}
|
|
2271
|
+
` : "";
|
|
2272
|
+
}
|
|
2273
|
+
return existing;
|
|
2174
2274
|
}
|
|
2175
2275
|
function injectHausBlock(existing, block) {
|
|
2176
2276
|
const range = findImportBlockRange(existing);
|
|
@@ -2200,21 +2300,21 @@ ${block}
|
|
|
2200
2300
|
`;
|
|
2201
2301
|
}
|
|
2202
2302
|
async function writeRootClaudeMd(root, dryRun) {
|
|
2203
|
-
const filePath =
|
|
2303
|
+
const filePath = path16.join(root, "CLAUDE.md");
|
|
2204
2304
|
const block = buildImportBlock();
|
|
2205
|
-
const prev = await
|
|
2305
|
+
const prev = await fs14.pathExists(filePath) ? await fs14.readFile(filePath, "utf8") : "";
|
|
2206
2306
|
const next = injectHausBlock(prev, block);
|
|
2207
2307
|
await writeManagedText(root, filePath, next, dryRun);
|
|
2208
2308
|
return filePath;
|
|
2209
2309
|
}
|
|
2210
2310
|
|
|
2211
2311
|
// src/claude/write-workflow-config.ts
|
|
2212
|
-
import
|
|
2213
|
-
import
|
|
2312
|
+
import path18 from "node:path";
|
|
2313
|
+
import fs16 from "fs-extra";
|
|
2214
2314
|
|
|
2215
2315
|
// src/claude/derive-workflow-config.ts
|
|
2216
|
-
import
|
|
2217
|
-
import
|
|
2316
|
+
import path17 from "node:path";
|
|
2317
|
+
import fs15 from "fs-extra";
|
|
2218
2318
|
function binCmd(pm, bin, args) {
|
|
2219
2319
|
const tail = args ? ` ${args}` : "";
|
|
2220
2320
|
if (pm === "yarn") return `yarn ${bin}${tail}`;
|
|
@@ -2223,7 +2323,7 @@ function binCmd(pm, bin, args) {
|
|
|
2223
2323
|
}
|
|
2224
2324
|
async function deriveWorkflowConfig(root, ctx) {
|
|
2225
2325
|
const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
|
|
2226
|
-
const pkg = await readJson(
|
|
2326
|
+
const pkg = await readJson(path17.join(root, "package.json"));
|
|
2227
2327
|
const scripts = pkg?.scripts ?? {};
|
|
2228
2328
|
const deps = new Set(ctx.dependencies);
|
|
2229
2329
|
const stacks = Object.values(ctx.detectedStacks ?? {}).flat();
|
|
@@ -2233,7 +2333,7 @@ async function deriveWorkflowConfig(root, ctx) {
|
|
|
2233
2333
|
return null;
|
|
2234
2334
|
};
|
|
2235
2335
|
const hasDep = (name) => deps.has(name);
|
|
2236
|
-
const exists = (rel) =>
|
|
2336
|
+
const exists = (rel) => fs15.pathExistsSync(path17.join(root, rel));
|
|
2237
2337
|
const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
|
|
2238
2338
|
const hasCypress = hasDep("cypress");
|
|
2239
2339
|
const preCommitTool = exists("lefthook.yml") || exists("lefthook.yaml") ? "lefthook" : exists(".husky") || hasDep("husky") || (scripts.prepare ?? "").includes("husky") ? "husky" : exists(".pre-commit-config.yaml") ? "pre-commit (Python framework)" : null;
|
|
@@ -2301,7 +2401,7 @@ var FALLBACK_CONTEXT = {
|
|
|
2301
2401
|
async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
2302
2402
|
const destPath = hausPath(root, "workflow-config.md");
|
|
2303
2403
|
const printable = displayPath(root, destPath);
|
|
2304
|
-
const exists = await
|
|
2404
|
+
const exists = await fs16.pathExists(destPath);
|
|
2305
2405
|
if (exists && !opts.refill) {
|
|
2306
2406
|
if (dryRun) log(printable + ": exists (project-owned, skipping)");
|
|
2307
2407
|
return null;
|
|
@@ -2309,11 +2409,11 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
2309
2409
|
const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
|
|
2310
2410
|
...FALLBACK_CONTEXT,
|
|
2311
2411
|
root,
|
|
2312
|
-
repoName:
|
|
2412
|
+
repoName: path18.basename(root)
|
|
2313
2413
|
};
|
|
2314
2414
|
const values = await deriveWorkflowConfig(root, ctx);
|
|
2315
2415
|
if (exists) {
|
|
2316
|
-
const current = await
|
|
2416
|
+
const current = await fs16.readFile(destPath, "utf8");
|
|
2317
2417
|
const refilled = refillContent(current, values);
|
|
2318
2418
|
if (refilled === current) {
|
|
2319
2419
|
if (dryRun) log(printable + ": no blank fields to refill");
|
|
@@ -2335,7 +2435,23 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
2335
2435
|
}
|
|
2336
2436
|
|
|
2337
2437
|
// src/claude/write-workflow.ts
|
|
2338
|
-
import
|
|
2438
|
+
import fs17 from "fs-extra";
|
|
2439
|
+
|
|
2440
|
+
// src/claude/managed-tamper.ts
|
|
2441
|
+
function checkManagedTamper(body, storedHash, templateContent) {
|
|
2442
|
+
const bodyHash = hashText(normaliseLF(body));
|
|
2443
|
+
if (storedHash) {
|
|
2444
|
+
if (bodyHash !== storedHash) return { status: "tampered" };
|
|
2445
|
+
if (templateContent === null) return { status: "ok" };
|
|
2446
|
+
const currentTemplateHash2 = hashText(normaliseLF(templateContent));
|
|
2447
|
+
return storedHash !== currentTemplateHash2 ? { status: "stale" } : { status: "ok" };
|
|
2448
|
+
}
|
|
2449
|
+
if (templateContent === null) return { status: "legacy-ok" };
|
|
2450
|
+
const currentTemplateHash = hashText(normaliseLF(templateContent));
|
|
2451
|
+
return bodyHash === currentTemplateHash ? { status: "legacy-ok" } : { status: "legacy-diverged" };
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
// src/claude/write-workflow.ts
|
|
2339
2455
|
var STABLE_ID = "template.workflow";
|
|
2340
2456
|
function makeWorkflowHeader(pkgVersion, contentHash) {
|
|
2341
2457
|
return `<!-- HAUS-MANAGED id=${STABLE_ID} v=${SCHEMA_VERSION} source=@haus-tech/haus-workflow@${pkgVersion} hash=${contentHash} -->`;
|
|
@@ -2354,8 +2470,8 @@ async function writeWorkflow(root, pkgVersion, dryRun, force = false) {
|
|
|
2354
2470
|
${templateContent}`;
|
|
2355
2471
|
const destPath = hausPath(root, "WORKFLOW.md");
|
|
2356
2472
|
const printable = displayPath(root, destPath);
|
|
2357
|
-
if (await
|
|
2358
|
-
const existing = await
|
|
2473
|
+
if (await fs17.pathExists(destPath)) {
|
|
2474
|
+
const existing = await fs17.readFile(destPath, "utf8");
|
|
2359
2475
|
const firstLine = existing.split("\n")[0] ?? "";
|
|
2360
2476
|
const parsed = parseHausManagedHeader(firstLine);
|
|
2361
2477
|
if (!parsed) {
|
|
@@ -2373,32 +2489,25 @@ ${templateContent}`;
|
|
|
2373
2489
|
return null;
|
|
2374
2490
|
}
|
|
2375
2491
|
const existingContent = existing.slice(firstLine.length + 1);
|
|
2376
|
-
const
|
|
2377
|
-
if (
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
if (!bodyMatchesTemplate && !force) {
|
|
2385
|
-
const migratedHeader = makeWorkflowHeader(
|
|
2386
|
-
pkgVersion,
|
|
2387
|
-
hashText(normaliseLF(existingContent))
|
|
2388
|
-
);
|
|
2389
|
-
const migrated = `${migratedHeader}
|
|
2492
|
+
const verdict = checkManagedTamper(existingContent, parsed.hash, templateContent);
|
|
2493
|
+
if (verdict.status === "tampered" && !force) {
|
|
2494
|
+
warn(`${printable}: content modified by user \u2014 skipping. Use --force to overwrite.`);
|
|
2495
|
+
return null;
|
|
2496
|
+
}
|
|
2497
|
+
if (verdict.status === "legacy-diverged" && !force) {
|
|
2498
|
+
const migratedHeader = makeWorkflowHeader(pkgVersion, hashText(normaliseLF(existingContent)));
|
|
2499
|
+
const migrated = `${migratedHeader}
|
|
2390
2500
|
${existingContent}`;
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
}
|
|
2397
|
-
} else if (dryRun) {
|
|
2398
|
-
log(`${printable}: unchanged`);
|
|
2501
|
+
if (hasTextChanged(existing, migrated)) {
|
|
2502
|
+
if (dryRun) {
|
|
2503
|
+
log(`${printable}: migrating legacy hash header (body preserved)`);
|
|
2504
|
+
} else {
|
|
2505
|
+
await writeText(destPath, migrated);
|
|
2399
2506
|
}
|
|
2400
|
-
|
|
2507
|
+
} else if (dryRun) {
|
|
2508
|
+
log(`${printable}: unchanged`);
|
|
2401
2509
|
}
|
|
2510
|
+
return destPath;
|
|
2402
2511
|
}
|
|
2403
2512
|
if (!hasTextChanged(existing, next)) {
|
|
2404
2513
|
if (dryRun) log(`${printable}: unchanged`);
|
|
@@ -2406,7 +2515,7 @@ ${existingContent}`;
|
|
|
2406
2515
|
}
|
|
2407
2516
|
}
|
|
2408
2517
|
if (dryRun) {
|
|
2409
|
-
const prev = await
|
|
2518
|
+
const prev = await fs17.pathExists(destPath) ? await fs17.readFile(destPath, "utf8") : "";
|
|
2410
2519
|
if (!prev) {
|
|
2411
2520
|
log(createUnifiedDiff(printable, "", next));
|
|
2412
2521
|
} else {
|
|
@@ -2428,6 +2537,19 @@ function targetDirForType(type) {
|
|
|
2428
2537
|
if (type === "skill") return "skills";
|
|
2429
2538
|
return null;
|
|
2430
2539
|
}
|
|
2540
|
+
async function removeLegacyManagedStub(root, relPathSegments, stub, dryRun, say) {
|
|
2541
|
+
const target = claudePath(root, ...relPathSegments);
|
|
2542
|
+
if (!await fs18.pathExists(target)) return;
|
|
2543
|
+
const content2 = await fs18.readFile(target, "utf8");
|
|
2544
|
+
if (content2 !== stub && content2 !== `${stub}
|
|
2545
|
+
` && content2 !== `${stub}\r
|
|
2546
|
+
`) return;
|
|
2547
|
+
if (dryRun) {
|
|
2548
|
+
say(`[dry-run] would remove stale ${displayPath(root, target)}`);
|
|
2549
|
+
} else {
|
|
2550
|
+
await fs18.remove(target);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2431
2553
|
async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
2432
2554
|
const say = opts.quiet ? () => {
|
|
2433
2555
|
} : log;
|
|
@@ -2441,7 +2563,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2441
2563
|
estimatedTokenReductionPct: 0
|
|
2442
2564
|
};
|
|
2443
2565
|
const pkgRoot = packageRoot();
|
|
2444
|
-
const hausVersion2 = (await readJson(
|
|
2566
|
+
const hausVersion2 = (await readJson(path19.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
|
|
2445
2567
|
const coreFiles = [claudePath(root, "settings.json"), claudePath(root, "rules", "haus.md")];
|
|
2446
2568
|
const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun);
|
|
2447
2569
|
const decisionsSeedPath = await writeDecisionsSeed(root, dryRun);
|
|
@@ -2465,34 +2587,20 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2465
2587
|
await applyProjectSettingsMerge(root);
|
|
2466
2588
|
await assertPostApplySettingsHausContract(root);
|
|
2467
2589
|
}
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
const legacyDoctorPath = claudePath(root, "commands", "haus-doctor.md");
|
|
2483
|
-
if (await fs14.pathExists(legacyDoctorPath)) {
|
|
2484
|
-
const content2 = await fs14.readFile(legacyDoctorPath, "utf8");
|
|
2485
|
-
const stub = "Run `haus doctor`.";
|
|
2486
|
-
if (content2 === stub || content2 === `${stub}
|
|
2487
|
-
` || content2 === `${stub}\r
|
|
2488
|
-
`) {
|
|
2489
|
-
if (dryRun) {
|
|
2490
|
-
say(`[dry-run] would remove stale ${displayPath(root, legacyDoctorPath)}`);
|
|
2491
|
-
} else {
|
|
2492
|
-
await fs14.remove(legacyDoctorPath);
|
|
2493
|
-
}
|
|
2494
|
-
}
|
|
2495
|
-
}
|
|
2590
|
+
await removeLegacyManagedStub(
|
|
2591
|
+
root,
|
|
2592
|
+
["commands", "haus-review.md"],
|
|
2593
|
+
'Run `haus context --task "code review"` then review diff.',
|
|
2594
|
+
dryRun,
|
|
2595
|
+
say
|
|
2596
|
+
);
|
|
2597
|
+
await removeLegacyManagedStub(
|
|
2598
|
+
root,
|
|
2599
|
+
["commands", "haus-doctor.md"],
|
|
2600
|
+
"Run `haus doctor`.",
|
|
2601
|
+
dryRun,
|
|
2602
|
+
say
|
|
2603
|
+
);
|
|
2496
2604
|
await writeManagedText(
|
|
2497
2605
|
root,
|
|
2498
2606
|
claudePath(root, "rules", "haus.md"),
|
|
@@ -2516,20 +2624,13 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2516
2624
|
].join("\n"),
|
|
2517
2625
|
dryRun
|
|
2518
2626
|
);
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
if (dryRun) {
|
|
2527
|
-
say(`[dry-run] would remove stale ${displayPath(root, legacySecurityPath)}`);
|
|
2528
|
-
} else {
|
|
2529
|
-
await fs14.remove(legacySecurityPath);
|
|
2530
|
-
}
|
|
2531
|
-
}
|
|
2532
|
-
}
|
|
2627
|
+
await removeLegacyManagedStub(
|
|
2628
|
+
root,
|
|
2629
|
+
["rules", "security.md"],
|
|
2630
|
+
"- Never read secrets.\n- Block dangerous shell commands.",
|
|
2631
|
+
dryRun,
|
|
2632
|
+
say
|
|
2633
|
+
);
|
|
2533
2634
|
const LEGACY_PRUNED_ARTIFACTS = [
|
|
2534
2635
|
"config.json",
|
|
2535
2636
|
"selected-context.json",
|
|
@@ -2541,19 +2642,68 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2541
2642
|
];
|
|
2542
2643
|
for (const rel of LEGACY_PRUNED_ARTIFACTS) {
|
|
2543
2644
|
const legacyPath = hausPath(root, rel);
|
|
2544
|
-
if (await
|
|
2645
|
+
if (await fs18.pathExists(legacyPath)) {
|
|
2545
2646
|
if (dryRun) {
|
|
2546
2647
|
say(`[dry-run] would remove stale ${displayPath(root, legacyPath)}`);
|
|
2547
2648
|
} else {
|
|
2548
|
-
await
|
|
2649
|
+
await fs18.remove(legacyPath);
|
|
2549
2650
|
}
|
|
2550
2651
|
}
|
|
2551
2652
|
}
|
|
2552
2653
|
const { items: manifestItems, contentRoot } = await loadCatalogContext(root);
|
|
2553
2654
|
const manifestById = new Map(manifestItems.map((item) => [item.id, item]));
|
|
2655
|
+
const prevLock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
2656
|
+
const allMigrations = findFormerIdMigrations(
|
|
2657
|
+
prevLock.filter((entry) => Boolean(entry.id)),
|
|
2658
|
+
manifestItems
|
|
2659
|
+
);
|
|
2660
|
+
const migrations = allMigrations.filter(
|
|
2661
|
+
(migration) => selectedIds === void 0 || selectedIds.includes(migration.oldId) || selectedIds.includes(migration.newId)
|
|
2662
|
+
);
|
|
2663
|
+
const migrationByOldId = new Map(
|
|
2664
|
+
migrations.map((migration) => [migration.oldId, migration.newId])
|
|
2665
|
+
);
|
|
2666
|
+
const cleanupManifestById = new Map(manifestById);
|
|
2667
|
+
for (const migration of allMigrations) {
|
|
2668
|
+
const currentItem = manifestById.get(migration.newId);
|
|
2669
|
+
if (currentItem) cleanupManifestById.set(migration.oldId, currentItem);
|
|
2670
|
+
}
|
|
2671
|
+
for (const migration of migrations) {
|
|
2672
|
+
if (dryRun) {
|
|
2673
|
+
say(`[dry-run] would migrate ${migration.oldId} \u2192 ${migration.newId} (upstream rename)`);
|
|
2674
|
+
} else {
|
|
2675
|
+
warn(`migrated ${migration.oldId} \u2192 ${migration.newId} (upstream rename)`);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
await cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, opts.quiet);
|
|
2554
2679
|
const installedPathsByItem = /* @__PURE__ */ new Map();
|
|
2555
2680
|
const installedIds = /* @__PURE__ */ new Set();
|
|
2556
|
-
const
|
|
2681
|
+
const selectedCatalogItems = selectedIds !== void 0 ? rec.recommended.filter((r) => selectedIds.includes(r.id)) : rec.recommended;
|
|
2682
|
+
const catalogItemsById = /* @__PURE__ */ new Map();
|
|
2683
|
+
for (const recommended of selectedCatalogItems) {
|
|
2684
|
+
const id = migrationByOldId.get(recommended.id) ?? recommended.id;
|
|
2685
|
+
const manifestItem = manifestById.get(id);
|
|
2686
|
+
catalogItemsById.set(id, {
|
|
2687
|
+
...recommended,
|
|
2688
|
+
id,
|
|
2689
|
+
type: manifestItem?.type ?? recommended.type
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
for (const migration of migrations) {
|
|
2693
|
+
if (catalogItemsById.has(migration.newId)) continue;
|
|
2694
|
+
const lockItem = prevLock.find((entry) => entry.id === migration.oldId);
|
|
2695
|
+
const manifestItem = manifestById.get(migration.newId);
|
|
2696
|
+
if (!lockItem || !manifestItem) continue;
|
|
2697
|
+
catalogItemsById.set(migration.newId, {
|
|
2698
|
+
id: migration.newId,
|
|
2699
|
+
type: manifestItem.type,
|
|
2700
|
+
reason: "migrated from former catalog id",
|
|
2701
|
+
reasons: [{ code: "former-id", message: `Renamed from ${migration.oldId}` }],
|
|
2702
|
+
selectionMode: "manual",
|
|
2703
|
+
install: true
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
const catalogItems = [...catalogItemsById.values()];
|
|
2557
2707
|
let curatedReviewStatusSkips = 0;
|
|
2558
2708
|
let superpowersSharedInstalled = false;
|
|
2559
2709
|
for (const item of catalogItems) {
|
|
@@ -2587,21 +2737,21 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2587
2737
|
);
|
|
2588
2738
|
continue;
|
|
2589
2739
|
}
|
|
2590
|
-
const destination = claudePath(root, target,
|
|
2591
|
-
if (await
|
|
2740
|
+
const destination = claudePath(root, target, path19.basename(sourcePath));
|
|
2741
|
+
if (await fs18.pathExists(sourcePath)) {
|
|
2592
2742
|
if (dryRun) {
|
|
2593
|
-
const exists = await
|
|
2743
|
+
const exists = await fs18.pathExists(destination);
|
|
2594
2744
|
say(
|
|
2595
2745
|
`${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`
|
|
2596
2746
|
);
|
|
2597
2747
|
} else if (item.type === "skill") {
|
|
2598
|
-
const skillFiles = (await
|
|
2748
|
+
const skillFiles = (await fs18.readdir(sourcePath, { recursive: true })).filter(
|
|
2599
2749
|
(f) => typeof f === "string" && f.endsWith(".md")
|
|
2600
2750
|
);
|
|
2601
2751
|
let skillValid = true;
|
|
2602
2752
|
for (const relFile of skillFiles) {
|
|
2603
|
-
const mdPath =
|
|
2604
|
-
const mdContent = await
|
|
2753
|
+
const mdPath = path19.join(sourcePath, relFile);
|
|
2754
|
+
const mdContent = await fs18.readFile(mdPath, "utf8");
|
|
2605
2755
|
const mdValidation = validateCatalogItem(
|
|
2606
2756
|
{
|
|
2607
2757
|
id: manifestItem.id,
|
|
@@ -2625,7 +2775,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2625
2775
|
dryRun: false
|
|
2626
2776
|
});
|
|
2627
2777
|
} else {
|
|
2628
|
-
const fileContent = await
|
|
2778
|
+
const fileContent = await fs18.readFile(sourcePath, "utf8");
|
|
2629
2779
|
const validation = validateCatalogItem(
|
|
2630
2780
|
{
|
|
2631
2781
|
id: manifestItem.id,
|
|
@@ -2639,17 +2789,17 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2639
2789
|
warn(`Skipping ${item.id}: pre-copy validation failed \u2014 ${validation.reason}`);
|
|
2640
2790
|
continue;
|
|
2641
2791
|
}
|
|
2642
|
-
await
|
|
2643
|
-
await
|
|
2792
|
+
await fs18.ensureDir(path19.dirname(destination));
|
|
2793
|
+
await fs18.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
2644
2794
|
}
|
|
2645
2795
|
files.push(destination);
|
|
2646
|
-
const relPaths = [
|
|
2796
|
+
const relPaths = [path19.relative(root, destination)];
|
|
2647
2797
|
if (!superpowersSharedInstalled && manifestItem.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID && item.type === "skill") {
|
|
2648
2798
|
const sharedRel = await installSuperpowersShared(contentRoot, root, dryRun);
|
|
2649
2799
|
if (sharedRel) {
|
|
2650
2800
|
superpowersSharedInstalled = true;
|
|
2651
2801
|
relPaths.push(sharedRel);
|
|
2652
|
-
files.push(
|
|
2802
|
+
files.push(path19.join(root, sharedRel));
|
|
2653
2803
|
}
|
|
2654
2804
|
}
|
|
2655
2805
|
const current = installedPathsByItem.get(item.id) ?? [];
|
|
@@ -2666,12 +2816,11 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2666
2816
|
`${curatedReviewStatusSkips} curated items skipped: reviewStatus is not approved \u2014 possible catalog field rename upstream`
|
|
2667
2817
|
);
|
|
2668
2818
|
}
|
|
2669
|
-
await cleanupStaleCatalogItems(root,
|
|
2819
|
+
await cleanupStaleCatalogItems(root, cleanupManifestById, dryRun, opts.quiet);
|
|
2670
2820
|
if (dryRun) return [...new Set(files)];
|
|
2671
2821
|
const installedItems = catalogItems.filter((r) => installedIds.has(r.id));
|
|
2672
|
-
const prevLock = await readJson(hausPath(root, "haus.lock.json"));
|
|
2673
2822
|
const prevRefById = new Map(
|
|
2674
|
-
|
|
2823
|
+
prevLock.filter((e) => e.id && e.catalogRef).map((e) => [migrationByOldId.get(e.id) ?? e.id, e.catalogRef])
|
|
2675
2824
|
);
|
|
2676
2825
|
const lockCatalogRef = (itemId) => isCatalogRefResolved() ? getResolvedCatalogRef() : prevRefById.get(itemId) ?? getResolvedCatalogRef();
|
|
2677
2826
|
const lock = await Promise.all(
|
|
@@ -2703,6 +2852,44 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2703
2852
|
await writeManagedJson(root, hausPath(root, "haus.lock.json"), lock, false);
|
|
2704
2853
|
return [...new Set(files)];
|
|
2705
2854
|
}
|
|
2855
|
+
async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, quiet) {
|
|
2856
|
+
const say = quiet ? () => {
|
|
2857
|
+
} : log;
|
|
2858
|
+
for (const migration of migrations) {
|
|
2859
|
+
const entry = prevLock.find((candidate) => candidate.id === migration.oldId);
|
|
2860
|
+
if (!entry) continue;
|
|
2861
|
+
const relPaths = entry.paths ?? [];
|
|
2862
|
+
const existing = [];
|
|
2863
|
+
for (const rel of relPaths) {
|
|
2864
|
+
if (await fs18.pathExists(path19.join(root, rel))) existing.push(rel);
|
|
2865
|
+
}
|
|
2866
|
+
if (existing.length === 0) continue;
|
|
2867
|
+
if (entry.hash === void 0) {
|
|
2868
|
+
warn(
|
|
2869
|
+
`Former catalog item ${migration.oldId} has no lock hash \u2014 leaving old paths in place: ${existing.join(", ")}`
|
|
2870
|
+
);
|
|
2871
|
+
continue;
|
|
2872
|
+
}
|
|
2873
|
+
const currentHash = await hashInstalledPaths(root, existing);
|
|
2874
|
+
if (currentHash !== entry.hash) {
|
|
2875
|
+
warn(
|
|
2876
|
+
`Former catalog item ${migration.oldId} was modified locally \u2014 leaving old paths in place: ${existing.join(", ")}`
|
|
2877
|
+
);
|
|
2878
|
+
continue;
|
|
2879
|
+
}
|
|
2880
|
+
for (const rel of existing) {
|
|
2881
|
+
const abs = path19.join(root, rel);
|
|
2882
|
+
if (dryRun) {
|
|
2883
|
+
say(
|
|
2884
|
+
`[dry-run] would remove renamed ${displayPath(root, abs)} (${migration.oldId} \u2192 ${migration.newId})`
|
|
2885
|
+
);
|
|
2886
|
+
continue;
|
|
2887
|
+
}
|
|
2888
|
+
await fs18.remove(abs);
|
|
2889
|
+
await pruneEmptyDir(path19.dirname(abs));
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2706
2893
|
async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet) {
|
|
2707
2894
|
const say = quiet ? () => {
|
|
2708
2895
|
} : log;
|
|
@@ -2718,7 +2905,7 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet) {
|
|
|
2718
2905
|
if (relPaths.length === 0) continue;
|
|
2719
2906
|
const existing = [];
|
|
2720
2907
|
for (const rel of relPaths) {
|
|
2721
|
-
if (await
|
|
2908
|
+
if (await fs18.pathExists(path19.join(root, rel))) existing.push(rel);
|
|
2722
2909
|
}
|
|
2723
2910
|
if (existing.length === 0) continue;
|
|
2724
2911
|
const pruneReason = deprecated ? "deprecated" : "stale";
|
|
@@ -2736,45 +2923,46 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet) {
|
|
|
2736
2923
|
continue;
|
|
2737
2924
|
}
|
|
2738
2925
|
for (const rel of existing) {
|
|
2739
|
-
const abs =
|
|
2926
|
+
const abs = path19.join(root, rel);
|
|
2740
2927
|
if (dryRun) {
|
|
2741
2928
|
say(`[dry-run] would remove ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
2742
2929
|
continue;
|
|
2743
2930
|
}
|
|
2744
|
-
await
|
|
2745
|
-
await pruneEmptyDir(
|
|
2931
|
+
await fs18.remove(abs);
|
|
2932
|
+
await pruneEmptyDir(path19.dirname(abs));
|
|
2746
2933
|
say(`Removed ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
2747
2934
|
}
|
|
2748
2935
|
}
|
|
2749
2936
|
}
|
|
2750
2937
|
|
|
2751
2938
|
// src/refs/fetch-refs.ts
|
|
2752
|
-
import
|
|
2753
|
-
import
|
|
2939
|
+
import path21 from "node:path";
|
|
2940
|
+
import fs20 from "fs-extra";
|
|
2754
2941
|
|
|
2755
2942
|
// src/refs/cache-meta.ts
|
|
2756
|
-
import
|
|
2757
|
-
import
|
|
2943
|
+
import path20 from "node:path";
|
|
2944
|
+
import fs19 from "fs-extra";
|
|
2758
2945
|
var META_FILENAME = "cache-meta.json";
|
|
2759
2946
|
async function readCacheMeta(cacheDir) {
|
|
2760
|
-
const metaPath =
|
|
2947
|
+
const metaPath = path20.join(cacheDir, META_FILENAME);
|
|
2761
2948
|
try {
|
|
2762
|
-
const raw = await
|
|
2949
|
+
const raw = await fs19.readFile(metaPath, "utf8");
|
|
2763
2950
|
return JSON.parse(raw);
|
|
2764
2951
|
} catch {
|
|
2765
2952
|
return {};
|
|
2766
2953
|
}
|
|
2767
2954
|
}
|
|
2768
2955
|
async function writeCacheMeta(cacheDir, meta) {
|
|
2769
|
-
await
|
|
2770
|
-
const metaPath =
|
|
2771
|
-
await
|
|
2956
|
+
await fs19.ensureDir(cacheDir);
|
|
2957
|
+
const metaPath = path20.join(cacheDir, META_FILENAME);
|
|
2958
|
+
await fs19.writeFile(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf8");
|
|
2772
2959
|
}
|
|
2773
2960
|
|
|
2774
2961
|
// src/refs/fetch-refs.ts
|
|
2775
2962
|
function urlToSlug(url) {
|
|
2776
2963
|
const u = new URL(url);
|
|
2777
|
-
|
|
2964
|
+
const protocolPrefix = u.protocol === "https:" ? "" : `${u.protocol.replace(":", "")}-`;
|
|
2965
|
+
return (protocolPrefix + u.host + u.pathname).replace(/[^a-zA-Z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
|
|
2778
2966
|
}
|
|
2779
2967
|
function isLlmsTxtUrl(ref) {
|
|
2780
2968
|
try {
|
|
@@ -2787,8 +2975,8 @@ function isLlmsTxtUrl(ref) {
|
|
|
2787
2975
|
async function fetchSingleRef(url, cacheDir, meta) {
|
|
2788
2976
|
const existing = meta[url];
|
|
2789
2977
|
const headers = {};
|
|
2790
|
-
const safeFile = existing?.file && existing.file ===
|
|
2791
|
-
const cachedFileExists = safeFile && await
|
|
2978
|
+
const safeFile = existing?.file && existing.file === path21.basename(existing.file);
|
|
2979
|
+
const cachedFileExists = safeFile && await fs20.pathExists(path21.join(cacheDir, existing.file));
|
|
2792
2980
|
if (cachedFileExists) {
|
|
2793
2981
|
if (existing?.etag) headers["If-None-Match"] = existing.etag;
|
|
2794
2982
|
if (existing?.lastModified) headers["If-Modified-Since"] = existing.lastModified;
|
|
@@ -2799,8 +2987,8 @@ async function fetchSingleRef(url, cacheDir, meta) {
|
|
|
2799
2987
|
if (!res.ok) return { result: "failed" };
|
|
2800
2988
|
const text = await res.text();
|
|
2801
2989
|
const file = `${urlToSlug(url)}.md`;
|
|
2802
|
-
await
|
|
2803
|
-
await
|
|
2990
|
+
await fs20.ensureDir(cacheDir);
|
|
2991
|
+
await fs20.writeFile(path21.join(cacheDir, file), text, "utf8");
|
|
2804
2992
|
meta[url] = {
|
|
2805
2993
|
url,
|
|
2806
2994
|
etag: res.headers.get("etag") ?? void 0,
|
|
@@ -2823,8 +3011,8 @@ async function pruneOrphanedRefs(cacheDir, keepUrls) {
|
|
|
2823
3011
|
if (keepUrls.has(url)) continue;
|
|
2824
3012
|
delete meta[url];
|
|
2825
3013
|
removed++;
|
|
2826
|
-
const safeFile = typeof entry.file === "string" && entry.file ===
|
|
2827
|
-
if (safeFile) await
|
|
3014
|
+
const safeFile = typeof entry.file === "string" && entry.file === path21.basename(entry.file);
|
|
3015
|
+
if (safeFile) await fs20.remove(path21.join(cacheDir, entry.file)).catch(() => {
|
|
2828
3016
|
});
|
|
2829
3017
|
}
|
|
2830
3018
|
if (removed > 0) await writeCacheMeta(cacheDir, meta);
|
|
@@ -2846,10 +3034,10 @@ async function fetchRefsForItems(items, cacheDir) {
|
|
|
2846
3034
|
const outcome = await fetchSingleRef(url, cacheDir, meta);
|
|
2847
3035
|
if (outcome.result === "fetched") {
|
|
2848
3036
|
summary.fetched++;
|
|
2849
|
-
summary.cachedFiles[url] =
|
|
3037
|
+
summary.cachedFiles[url] = path21.join(cacheDir, outcome.file);
|
|
2850
3038
|
} else if (outcome.result === "unchanged") {
|
|
2851
3039
|
summary.unchanged++;
|
|
2852
|
-
if (meta[url]?.file) summary.cachedFiles[url] =
|
|
3040
|
+
if (meta[url]?.file) summary.cachedFiles[url] = path21.join(cacheDir, meta[url].file);
|
|
2853
3041
|
} else {
|
|
2854
3042
|
summary.failed++;
|
|
2855
3043
|
summary.failedUrls.push(url);
|
|
@@ -2870,8 +3058,9 @@ function parseIdList(value) {
|
|
|
2870
3058
|
}
|
|
2871
3059
|
|
|
2872
3060
|
// src/commands/apply.ts
|
|
3061
|
+
var STALE_CACHE_THRESHOLD_DAYS = 7;
|
|
2873
3062
|
async function cacheHasItems() {
|
|
2874
|
-
const data = await readJson(
|
|
3063
|
+
const data = await readJson(path22.join(getCacheDir(), "manifest.json"));
|
|
2875
3064
|
return Array.isArray(data?.items) && data.items.length > 0;
|
|
2876
3065
|
}
|
|
2877
3066
|
function installableRecommendedItems(recommended) {
|
|
@@ -2944,18 +3133,31 @@ async function runApply(options) {
|
|
|
2944
3133
|
}
|
|
2945
3134
|
}
|
|
2946
3135
|
}
|
|
2947
|
-
if (!
|
|
3136
|
+
if (!process.env["HAUS_FIXTURE_CATALOG"]) {
|
|
2948
3137
|
const rec = await readJson(hausPath(root, "recommendation.json"));
|
|
2949
3138
|
const installableRecommended = installableRecommendedItems(rec?.recommended ?? []);
|
|
2950
3139
|
const catalogItemCount = selectedIds !== void 0 ? selectedIds.length : installableRecommended.length;
|
|
2951
|
-
if (catalogItemCount > 0
|
|
2952
|
-
const
|
|
2953
|
-
if (
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
3140
|
+
if (catalogItemCount > 0) {
|
|
3141
|
+
const hasItems = await cacheHasItems();
|
|
3142
|
+
if (!hasItems && !options.allowEmptyCache) {
|
|
3143
|
+
const message = "No catalog content found. Run `haus update` first.";
|
|
3144
|
+
if (isDryRun) {
|
|
3145
|
+
warn(`Catalog cache is empty \u2014 dry-run will skip catalog items. ${message}`);
|
|
3146
|
+
} else {
|
|
3147
|
+
error(message);
|
|
3148
|
+
process.exitCode = 1;
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
} else if (hasItems) {
|
|
3152
|
+
const cacheAgeMs = await getCacheManifestAge();
|
|
3153
|
+
if (cacheAgeMs !== null) {
|
|
3154
|
+
const cacheAgeDays = Math.floor(cacheAgeMs / (1e3 * 60 * 60 * 24));
|
|
3155
|
+
if (cacheAgeDays >= STALE_CACHE_THRESHOLD_DAYS) {
|
|
3156
|
+
warn(
|
|
3157
|
+
`Catalog cache is ${cacheAgeDays} days old \u2014 run \`haus update\` for the latest content.`
|
|
3158
|
+
);
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
2959
3161
|
}
|
|
2960
3162
|
}
|
|
2961
3163
|
}
|
|
@@ -3024,7 +3226,7 @@ async function runCatalogAudit() {
|
|
|
3024
3226
|
// src/commands/clone.ts
|
|
3025
3227
|
init_exec();
|
|
3026
3228
|
import { existsSync as existsSync2 } from "node:fs";
|
|
3027
|
-
import
|
|
3229
|
+
import path23 from "node:path";
|
|
3028
3230
|
var GIT_LOCATION_VARS = [
|
|
3029
3231
|
"GIT_DIR",
|
|
3030
3232
|
"GIT_WORK_TREE",
|
|
@@ -3050,9 +3252,9 @@ async function runClone(url, opts = {}) {
|
|
|
3050
3252
|
process.exitCode = 1;
|
|
3051
3253
|
return;
|
|
3052
3254
|
}
|
|
3053
|
-
const target =
|
|
3255
|
+
const target = path23.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
3054
3256
|
if (existsSync2(target)) {
|
|
3055
|
-
log(`\u2022 ${
|
|
3257
|
+
log(`\u2022 ${path23.basename(target)} already present at ${target} \u2014 skipped`);
|
|
3056
3258
|
return;
|
|
3057
3259
|
}
|
|
3058
3260
|
if (opts.dryRun) {
|
|
@@ -3123,8 +3325,8 @@ async function resolveBaseRef(root) {
|
|
|
3123
3325
|
}
|
|
3124
3326
|
|
|
3125
3327
|
// src/decisions/check.ts
|
|
3126
|
-
import
|
|
3127
|
-
import
|
|
3328
|
+
import path26 from "node:path";
|
|
3329
|
+
import fs23 from "fs-extra";
|
|
3128
3330
|
|
|
3129
3331
|
// src/decisions/diff.ts
|
|
3130
3332
|
init_exec();
|
|
@@ -3179,22 +3381,22 @@ function matchesAnyRegex(filePath, patterns) {
|
|
|
3179
3381
|
}
|
|
3180
3382
|
|
|
3181
3383
|
// src/decisions/paths.ts
|
|
3182
|
-
import
|
|
3183
|
-
import
|
|
3384
|
+
import path24 from "node:path";
|
|
3385
|
+
import fs21 from "fs-extra";
|
|
3184
3386
|
async function resolveDecisionsDir(root) {
|
|
3185
|
-
const primary =
|
|
3186
|
-
if (await
|
|
3187
|
-
const legacy =
|
|
3188
|
-
if (await
|
|
3387
|
+
const primary = path24.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
3388
|
+
if (await fs21.pathExists(primary)) return primary;
|
|
3389
|
+
const legacy = path24.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
3390
|
+
if (await fs21.pathExists(legacy)) return legacy;
|
|
3189
3391
|
return primary;
|
|
3190
3392
|
}
|
|
3191
3393
|
function relativeDecisionPath(decisionsDir, root) {
|
|
3192
|
-
return
|
|
3394
|
+
return path24.relative(root, decisionsDir).replace(/\\/g, "/");
|
|
3193
3395
|
}
|
|
3194
3396
|
|
|
3195
3397
|
// src/decisions/validate.ts
|
|
3196
|
-
import
|
|
3197
|
-
import
|
|
3398
|
+
import path25 from "node:path";
|
|
3399
|
+
import fs22 from "fs-extra";
|
|
3198
3400
|
function validateDecisionContent(content2) {
|
|
3199
3401
|
const errors = [];
|
|
3200
3402
|
for (const section of DECISIONS_TRIGGERS.requiredSections) {
|
|
@@ -3223,14 +3425,14 @@ function decisionNumberFromFile(name) {
|
|
|
3223
3425
|
return match?.[1] ?? null;
|
|
3224
3426
|
}
|
|
3225
3427
|
async function validateIndexSync(decisionsDir, decisionFiles) {
|
|
3226
|
-
const readmePath =
|
|
3227
|
-
if (!await
|
|
3428
|
+
const readmePath = path25.join(decisionsDir, "README.md");
|
|
3429
|
+
if (!await fs22.pathExists(readmePath)) {
|
|
3228
3430
|
return ["docs/decisions/README.md is missing"];
|
|
3229
3431
|
}
|
|
3230
3432
|
const readme = await readText(readmePath) ?? "";
|
|
3231
3433
|
const errors = [];
|
|
3232
3434
|
for (const file of decisionFiles) {
|
|
3233
|
-
const num = decisionNumberFromFile(
|
|
3435
|
+
const num = decisionNumberFromFile(path25.basename(file));
|
|
3234
3436
|
if (num && !indexReferencesNumber(readme, num)) {
|
|
3235
3437
|
errors.push(`README.md missing index row for ADR-${num}`);
|
|
3236
3438
|
}
|
|
@@ -3284,22 +3486,22 @@ async function checkSatisfaction(input2) {
|
|
|
3284
3486
|
const errors = [];
|
|
3285
3487
|
const decisionsPrefix = `${input2.decisionsRel}/`;
|
|
3286
3488
|
const newDecisionFiles = input2.changedFiles.filter(
|
|
3287
|
-
(f) => f.startsWith(decisionsPrefix) && isDecisionFileName(
|
|
3489
|
+
(f) => f.startsWith(decisionsPrefix) && isDecisionFileName(path26.basename(f))
|
|
3288
3490
|
);
|
|
3289
3491
|
if (newDecisionFiles.length === 0) {
|
|
3290
3492
|
errors.push(`no new or updated decision under ${input2.decisionsRel}/`);
|
|
3291
3493
|
return errors;
|
|
3292
3494
|
}
|
|
3293
|
-
const decisionsDir =
|
|
3495
|
+
const decisionsDir = path26.join(input2.root, input2.decisionsRel);
|
|
3294
3496
|
for (const rel of newDecisionFiles) {
|
|
3295
|
-
const abs =
|
|
3497
|
+
const abs = path26.join(input2.root, rel);
|
|
3296
3498
|
const content2 = await readText(abs) ?? "";
|
|
3297
3499
|
const valid = validateDecisionContent(content2);
|
|
3298
3500
|
if (!valid.ok) errors.push(...valid.errors.map((e) => `${rel}: ${e}`));
|
|
3299
3501
|
}
|
|
3300
3502
|
const indexErrors = await validateIndexSync(
|
|
3301
3503
|
decisionsDir,
|
|
3302
|
-
newDecisionFiles.map((f) =>
|
|
3504
|
+
newDecisionFiles.map((f) => path26.basename(f))
|
|
3303
3505
|
);
|
|
3304
3506
|
errors.push(...indexErrors);
|
|
3305
3507
|
return errors;
|
|
@@ -3343,8 +3545,8 @@ ${opts.commitMessages ?? ""}`;
|
|
|
3343
3545
|
changedFiles: stats.files
|
|
3344
3546
|
});
|
|
3345
3547
|
if (satisfactionErrors.length > 0 && decisionsRel !== legacyRel) {
|
|
3346
|
-
const legacyExists = await
|
|
3347
|
-
if (legacyExists && !await
|
|
3548
|
+
const legacyExists = await fs23.pathExists(path26.join(root, legacyRel));
|
|
3549
|
+
if (legacyExists && !await fs23.pathExists(decisionsDir)) {
|
|
3348
3550
|
satisfactionErrors = await checkSatisfaction({
|
|
3349
3551
|
root,
|
|
3350
3552
|
decisionsRel: legacyRel,
|
|
@@ -3364,8 +3566,8 @@ ${opts.commitMessages ?? ""}`;
|
|
|
3364
3566
|
return { triggered: true, satisfied: false, reasons, securityTriggered };
|
|
3365
3567
|
}
|
|
3366
3568
|
async function listDecisionNumbers(decisionsDir) {
|
|
3367
|
-
if (!await
|
|
3368
|
-
const entries = await
|
|
3569
|
+
if (!await fs23.pathExists(decisionsDir)) return [];
|
|
3570
|
+
const entries = await fs23.readdir(decisionsDir);
|
|
3369
3571
|
return entries.map((name) => decisionNumberFromFile(name)).filter((n) => n != null).map((n) => Number(n));
|
|
3370
3572
|
}
|
|
3371
3573
|
|
|
@@ -3407,7 +3609,7 @@ async function runDecisionsGuard(options = {}) {
|
|
|
3407
3609
|
}
|
|
3408
3610
|
|
|
3409
3611
|
// src/commands/decisions.ts
|
|
3410
|
-
import
|
|
3612
|
+
import path27 from "node:path";
|
|
3411
3613
|
|
|
3412
3614
|
// src/decisions/next-number.ts
|
|
3413
3615
|
async function nextDecisionNumber(root) {
|
|
@@ -3518,7 +3720,7 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
3518
3720
|
return;
|
|
3519
3721
|
}
|
|
3520
3722
|
if (sub === "validate") {
|
|
3521
|
-
const target = arg ?
|
|
3723
|
+
const target = arg ? path27.resolve(root, arg) : void 0;
|
|
3522
3724
|
if (!target) {
|
|
3523
3725
|
error("usage: haus decisions validate <path-to-decision.md>");
|
|
3524
3726
|
process.exitCode = 1;
|
|
@@ -3535,18 +3737,18 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
3535
3737
|
}
|
|
3536
3738
|
|
|
3537
3739
|
// src/commands/doctor.ts
|
|
3538
|
-
import
|
|
3539
|
-
import
|
|
3740
|
+
import path34 from "node:path";
|
|
3741
|
+
import fs27 from "fs-extra";
|
|
3540
3742
|
|
|
3541
3743
|
// src/decisions/doctor.ts
|
|
3542
|
-
import
|
|
3543
|
-
import
|
|
3744
|
+
import path28 from "node:path";
|
|
3745
|
+
import fs24 from "fs-extra";
|
|
3544
3746
|
async function auditDecisionsLayout(root) {
|
|
3545
3747
|
const findings = [];
|
|
3546
|
-
const primary =
|
|
3547
|
-
const legacy =
|
|
3548
|
-
const hasPrimary = await
|
|
3549
|
-
const hasLegacy = await
|
|
3748
|
+
const primary = path28.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
3749
|
+
const legacy = path28.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
3750
|
+
const hasPrimary = await fs24.pathExists(primary);
|
|
3751
|
+
const hasLegacy = await fs24.pathExists(legacy);
|
|
3550
3752
|
if (!hasPrimary && hasLegacy) {
|
|
3551
3753
|
findings.push({
|
|
3552
3754
|
level: "advisory",
|
|
@@ -3559,8 +3761,8 @@ async function auditDecisionsLayout(root) {
|
|
|
3559
3761
|
if (!hasPrimary) return findings;
|
|
3560
3762
|
const decisionsDir = await resolveDecisionsDir(root);
|
|
3561
3763
|
const rel = relativeDecisionPath(decisionsDir, root);
|
|
3562
|
-
const readmePath =
|
|
3563
|
-
if (!await
|
|
3764
|
+
const readmePath = path28.join(decisionsDir, "README.md");
|
|
3765
|
+
if (!await fs24.pathExists(readmePath)) {
|
|
3564
3766
|
findings.push({
|
|
3565
3767
|
level: "advisory",
|
|
3566
3768
|
line: `- DECISIONS: missing ${rel}/README.md index`,
|
|
@@ -3570,7 +3772,7 @@ async function auditDecisionsLayout(root) {
|
|
|
3570
3772
|
return findings;
|
|
3571
3773
|
}
|
|
3572
3774
|
const readme = await readText(readmePath);
|
|
3573
|
-
const entries = (await
|
|
3775
|
+
const entries = (await fs24.readdir(decisionsDir)).filter(
|
|
3574
3776
|
(f) => f.endsWith(".md") && f !== "README.md"
|
|
3575
3777
|
);
|
|
3576
3778
|
for (const file of entries) {
|
|
@@ -3584,7 +3786,7 @@ async function auditDecisionsLayout(root) {
|
|
|
3584
3786
|
});
|
|
3585
3787
|
}
|
|
3586
3788
|
}
|
|
3587
|
-
const claude = await readText(
|
|
3789
|
+
const claude = await readText(path28.join(root, "CLAUDE.md"));
|
|
3588
3790
|
if (claude && !claude.includes(`@${rel}/README.md`)) {
|
|
3589
3791
|
findings.push({
|
|
3590
3792
|
level: "advisory",
|
|
@@ -3614,8 +3816,12 @@ async function auditDecisionsLayout(root) {
|
|
|
3614
3816
|
return findings;
|
|
3615
3817
|
}
|
|
3616
3818
|
|
|
3819
|
+
// src/scanner/read-context.ts
|
|
3820
|
+
import path33 from "node:path";
|
|
3821
|
+
import fs26 from "fs-extra";
|
|
3822
|
+
|
|
3617
3823
|
// src/scanner/scan-project.ts
|
|
3618
|
-
import
|
|
3824
|
+
import path32 from "node:path";
|
|
3619
3825
|
|
|
3620
3826
|
// src/utils/versions.ts
|
|
3621
3827
|
import semver from "semver";
|
|
@@ -3637,8 +3843,8 @@ function compareVersions(a, b) {
|
|
|
3637
3843
|
}
|
|
3638
3844
|
|
|
3639
3845
|
// src/scanner/detect-package-manager.ts
|
|
3640
|
-
import
|
|
3641
|
-
import
|
|
3846
|
+
import path29 from "node:path";
|
|
3847
|
+
import fs25 from "fs-extra";
|
|
3642
3848
|
function detectPackageManager(root, packageManagerField) {
|
|
3643
3849
|
const field = String(packageManagerField ?? "").trim();
|
|
3644
3850
|
if (field.startsWith("yarn@")) {
|
|
@@ -3656,9 +3862,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
3656
3862
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
3657
3863
|
return "unknown";
|
|
3658
3864
|
}
|
|
3659
|
-
if (
|
|
3660
|
-
if (
|
|
3661
|
-
if (
|
|
3865
|
+
if (fs25.existsSync(path29.join(root, "yarn.lock"))) return "yarn";
|
|
3866
|
+
if (fs25.existsSync(path29.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
3867
|
+
if (fs25.existsSync(path29.join(root, "package-lock.json"))) return "npm";
|
|
3662
3868
|
return "unknown";
|
|
3663
3869
|
}
|
|
3664
3870
|
|
|
@@ -3860,7 +4066,7 @@ function runDetection(ctx, rules = STACK_RULES) {
|
|
|
3860
4066
|
}
|
|
3861
4067
|
|
|
3862
4068
|
// src/scanner/detection.ts
|
|
3863
|
-
import
|
|
4069
|
+
import path30 from "node:path";
|
|
3864
4070
|
var UNSUPPORTED_MARKERS = {
|
|
3865
4071
|
"requirements.txt": "python",
|
|
3866
4072
|
"pyproject.toml": "python",
|
|
@@ -3914,14 +4120,14 @@ function finalizeRoles(registryRoles, deps, files) {
|
|
|
3914
4120
|
function collectUnsupportedSignals(files) {
|
|
3915
4121
|
return [
|
|
3916
4122
|
...new Set(
|
|
3917
|
-
files.map((f) => UNSUPPORTED_MARKERS[
|
|
4123
|
+
files.map((f) => UNSUPPORTED_MARKERS[path30.basename(f)]).filter((s) => Boolean(s))
|
|
3918
4124
|
)
|
|
3919
4125
|
].sort();
|
|
3920
4126
|
}
|
|
3921
4127
|
|
|
3922
4128
|
// src/scanner/render.ts
|
|
3923
4129
|
import { readFile } from "node:fs/promises";
|
|
3924
|
-
import
|
|
4130
|
+
import path31 from "node:path";
|
|
3925
4131
|
async function buildContentBlob(root, files) {
|
|
3926
4132
|
const candidates = files.filter(
|
|
3927
4133
|
(f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
|
|
@@ -3929,7 +4135,7 @@ async function buildContentBlob(root, files) {
|
|
|
3929
4135
|
const slice = candidates.slice(0, 300);
|
|
3930
4136
|
const parts = await mapWithConcurrency(slice, async (rel) => {
|
|
3931
4137
|
try {
|
|
3932
|
-
return await readFile(
|
|
4138
|
+
return await readFile(path31.join(root, rel), "utf8");
|
|
3933
4139
|
} catch {
|
|
3934
4140
|
return "";
|
|
3935
4141
|
}
|
|
@@ -4016,8 +4222,8 @@ var SAFE_FILES = [
|
|
|
4016
4222
|
"Gemfile"
|
|
4017
4223
|
];
|
|
4018
4224
|
async function scanProject(root) {
|
|
4019
|
-
const pkg = await readJson(
|
|
4020
|
-
const composer = await readJson(
|
|
4225
|
+
const pkg = await readJson(path32.join(root, "package.json"));
|
|
4226
|
+
const composer = await readJson(path32.join(root, "composer.json"));
|
|
4021
4227
|
const files = await listFiles(root, SAFE_FILES);
|
|
4022
4228
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
4023
4229
|
const deps = dependencySet(pkg, composer);
|
|
@@ -4044,7 +4250,7 @@ async function scanProject(root) {
|
|
|
4044
4250
|
const context = {
|
|
4045
4251
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4046
4252
|
root,
|
|
4047
|
-
repoName: String(pkg?.name ??
|
|
4253
|
+
repoName: String(pkg?.name ?? path32.basename(root)),
|
|
4048
4254
|
packageManager,
|
|
4049
4255
|
repoRoles: roles,
|
|
4050
4256
|
detectedStacks: stacks,
|
|
@@ -4061,9 +4267,16 @@ async function scanProject(root) {
|
|
|
4061
4267
|
}
|
|
4062
4268
|
|
|
4063
4269
|
// src/scanner/read-context.ts
|
|
4270
|
+
async function isCacheFresh(root, cachePath) {
|
|
4271
|
+
const pkgJsonPath = path33.join(root, "package.json");
|
|
4272
|
+
if (!await fs26.pathExists(pkgJsonPath)) return true;
|
|
4273
|
+
const [cacheStat, pkgStat] = await Promise.all([fs26.stat(cachePath), fs26.stat(pkgJsonPath)]);
|
|
4274
|
+
return cacheStat.mtimeMs >= pkgStat.mtimeMs;
|
|
4275
|
+
}
|
|
4064
4276
|
async function readContextOrScan(root) {
|
|
4065
|
-
const
|
|
4066
|
-
|
|
4277
|
+
const cachePath = hausPath(root, "context-map.json");
|
|
4278
|
+
const context = await readJson(cachePath);
|
|
4279
|
+
if (context && await isCacheFresh(root, cachePath)) return context;
|
|
4067
4280
|
const scan = await scanProject(root);
|
|
4068
4281
|
return scan;
|
|
4069
4282
|
}
|
|
@@ -4156,7 +4369,7 @@ async function runDoctor(options) {
|
|
|
4156
4369
|
} else {
|
|
4157
4370
|
ok(`- HOOKS OK: ${hooks.message}`);
|
|
4158
4371
|
}
|
|
4159
|
-
const rootClaudeMdPath =
|
|
4372
|
+
const rootClaudeMdPath = path34.join(root, "CLAUDE.md");
|
|
4160
4373
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
4161
4374
|
if (!rootClaudeMdContent) {
|
|
4162
4375
|
flag(
|
|
@@ -4184,7 +4397,7 @@ async function runDoctor(options) {
|
|
|
4184
4397
|
const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
|
|
4185
4398
|
const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
|
|
4186
4399
|
for (const target of importTargets) {
|
|
4187
|
-
if (!await
|
|
4400
|
+
if (!await fs27.pathExists(hausPath(root, target))) {
|
|
4188
4401
|
flag(
|
|
4189
4402
|
`- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
|
|
4190
4403
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -4195,7 +4408,7 @@ async function runDoctor(options) {
|
|
|
4195
4408
|
}
|
|
4196
4409
|
}
|
|
4197
4410
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
4198
|
-
const workflowExists = await
|
|
4411
|
+
const workflowExists = await fs27.pathExists(workflowPath);
|
|
4199
4412
|
if (!workflowExists) {
|
|
4200
4413
|
flag(
|
|
4201
4414
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -4210,43 +4423,36 @@ async function runDoctor(options) {
|
|
|
4210
4423
|
} else {
|
|
4211
4424
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
4212
4425
|
const bodyContent = workflowContent.slice(firstLine.length + 1);
|
|
4213
|
-
const
|
|
4214
|
-
|
|
4426
|
+
const cachePath = path34.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
4427
|
+
const bundledPath = path34.join(
|
|
4428
|
+
packageRoot(),
|
|
4429
|
+
"library",
|
|
4430
|
+
"global",
|
|
4431
|
+
"templates",
|
|
4432
|
+
"agentic-workflow-standard.md"
|
|
4433
|
+
);
|
|
4434
|
+
const templatePath = await fs27.pathExists(cachePath) ? cachePath : bundledPath;
|
|
4435
|
+
const templateContent = await readText(templatePath);
|
|
4436
|
+
const verdict = checkManagedTamper(bodyContent, storedHashMatch?.[1], templateContent ?? null);
|
|
4437
|
+
if (verdict.status === "tampered" || verdict.status === "legacy-diverged") {
|
|
4215
4438
|
flag(
|
|
4216
4439
|
"- .haus-workflow/WORKFLOW.md: modified locally (run `haus apply --write --force` to restore)",
|
|
4217
4440
|
"The workflow standard file was edited after haus wrote it",
|
|
4218
4441
|
"haus apply --write --force"
|
|
4219
4442
|
);
|
|
4220
|
-
} else {
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
"
|
|
4225
|
-
"global",
|
|
4226
|
-
"templates",
|
|
4227
|
-
"agentic-workflow-standard.md"
|
|
4443
|
+
} else if (verdict.status === "stale") {
|
|
4444
|
+
suggest(
|
|
4445
|
+
"- .haus-workflow/WORKFLOW.md: stale (template updated \u2014 run `haus apply --write`)",
|
|
4446
|
+
"The workflow standard is out of date",
|
|
4447
|
+
"haus apply --write"
|
|
4228
4448
|
);
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
if (storedHashMatch && templateContent) {
|
|
4232
|
-
const currentHash = hashText(normaliseLF(templateContent));
|
|
4233
|
-
if (storedHashMatch[1] !== currentHash) {
|
|
4234
|
-
suggest(
|
|
4235
|
-
"- .haus-workflow/WORKFLOW.md: stale (template updated \u2014 run `haus apply --write`)",
|
|
4236
|
-
"The workflow standard is out of date",
|
|
4237
|
-
"haus apply --write"
|
|
4238
|
-
);
|
|
4239
|
-
} else {
|
|
4240
|
-
ok("- .haus-workflow/WORKFLOW.md: OK");
|
|
4241
|
-
}
|
|
4242
|
-
} else {
|
|
4243
|
-
ok("- .haus-workflow/WORKFLOW.md: OK");
|
|
4244
|
-
}
|
|
4449
|
+
} else {
|
|
4450
|
+
ok("- .haus-workflow/WORKFLOW.md: OK");
|
|
4245
4451
|
}
|
|
4246
4452
|
}
|
|
4247
4453
|
}
|
|
4248
4454
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
4249
|
-
const workflowConfigExists = await
|
|
4455
|
+
const workflowConfigExists = await fs27.pathExists(workflowConfigPath);
|
|
4250
4456
|
if (!workflowConfigExists) {
|
|
4251
4457
|
flag(
|
|
4252
4458
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -4254,7 +4460,7 @@ async function runDoctor(options) {
|
|
|
4254
4460
|
"haus apply --write"
|
|
4255
4461
|
);
|
|
4256
4462
|
} else {
|
|
4257
|
-
const cfg = await
|
|
4463
|
+
const cfg = await fs27.readFile(workflowConfigPath, "utf8");
|
|
4258
4464
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
4259
4465
|
if (unfilled > 0) {
|
|
4260
4466
|
suggest(
|
|
@@ -4267,7 +4473,7 @@ async function runDoctor(options) {
|
|
|
4267
4473
|
}
|
|
4268
4474
|
}
|
|
4269
4475
|
if (workflowExists) {
|
|
4270
|
-
const prettierIgnore = await readText(
|
|
4476
|
+
const prettierIgnore = await readText(path34.join(root, ".prettierignore")) ?? "";
|
|
4271
4477
|
if (!prettierIgnore.split("\n").some((l) => l.trim() === ".haus-workflow/")) {
|
|
4272
4478
|
flag(
|
|
4273
4479
|
"- .prettierignore: not protecting .haus-workflow/ (run `haus apply --write`)",
|
|
@@ -4301,7 +4507,21 @@ async function runDoctor(options) {
|
|
|
4301
4507
|
ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
|
|
4302
4508
|
}
|
|
4303
4509
|
}
|
|
4304
|
-
|
|
4510
|
+
if (recommendation) {
|
|
4511
|
+
const lock = await readJson(hausPath(root, "haus.lock.json"));
|
|
4512
|
+
const recommendedIds = new Set(
|
|
4513
|
+
recommendation.recommended.map((r) => r.id)
|
|
4514
|
+
);
|
|
4515
|
+
const orphaned = (lock ?? []).map((entry) => entry.id).filter((id) => typeof id === "string" && !recommendedIds.has(id));
|
|
4516
|
+
if (orphaned.length > 0) {
|
|
4517
|
+
suggest(
|
|
4518
|
+
`- CATALOG ITEMS: ${orphaned.length} installed item(s) no longer in the current recommendation (${orphaned.join(", ")})`,
|
|
4519
|
+
`${orphaned.length} installed item(s) are no longer recommended for this project`,
|
|
4520
|
+
"review whether they are still needed; haus apply --write leaves them in place until removed from the catalog"
|
|
4521
|
+
);
|
|
4522
|
+
}
|
|
4523
|
+
}
|
|
4524
|
+
const pkgJson = await readJson(path34.join(packageRoot(), "package.json"));
|
|
4305
4525
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
4306
4526
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
4307
4527
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -4517,7 +4737,7 @@ function matchesDenyToken(command, denyPhrase) {
|
|
|
4517
4737
|
if (denyPhrase === "sudo") {
|
|
4518
4738
|
return /(?:^|[|;&]\s*)sudo\b/i.test(command);
|
|
4519
4739
|
}
|
|
4520
|
-
const normalizedCommand = command.toLowerCase().replace(/\s+/g, " ").trim();
|
|
4740
|
+
const normalizedCommand = command.replace(/["'`]/g, "").toLowerCase().replace(/\s+/g, " ").trim();
|
|
4521
4741
|
return normalizedCommand.includes(denyPhrase.toLowerCase());
|
|
4522
4742
|
}
|
|
4523
4743
|
function guardBash(command) {
|
|
@@ -4559,41 +4779,29 @@ async function runGuard(kind, _options) {
|
|
|
4559
4779
|
}
|
|
4560
4780
|
|
|
4561
4781
|
// src/commands/init.ts
|
|
4562
|
-
import
|
|
4563
|
-
import
|
|
4564
|
-
|
|
4565
|
-
// src/utils/prompts.ts
|
|
4566
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
4567
|
-
import readline from "node:readline/promises";
|
|
4568
|
-
async function ask(question) {
|
|
4569
|
-
const rl = readline.createInterface({ input, output });
|
|
4570
|
-
try {
|
|
4571
|
-
const answer = await rl.question(`${question}
|
|
4572
|
-
> `);
|
|
4573
|
-
return answer.trim();
|
|
4574
|
-
} finally {
|
|
4575
|
-
rl.close();
|
|
4576
|
-
}
|
|
4577
|
-
}
|
|
4578
|
-
async function confirm(question) {
|
|
4579
|
-
const answer = (await ask(`${question} [y/N]`)).toLowerCase();
|
|
4580
|
-
return answer === "y" || answer === "yes";
|
|
4581
|
-
}
|
|
4782
|
+
import path35 from "node:path";
|
|
4783
|
+
import fs28 from "fs-extra";
|
|
4582
4784
|
|
|
4583
4785
|
// src/recommender/git-signal.ts
|
|
4584
4786
|
init_exec();
|
|
4585
|
-
async function
|
|
4586
|
-
if (process.env.HAUS_DISABLE_GIT_SIGNALS === "1") return [];
|
|
4787
|
+
async function runGitLines(root, args) {
|
|
4587
4788
|
try {
|
|
4588
|
-
const result = await runGit(
|
|
4589
|
-
if (result.exitCode !== 0)
|
|
4590
|
-
|
|
4591
|
-
}
|
|
4592
|
-
return result.stdout.split("\n").map((x) => x.trim()).filter(Boolean).sort();
|
|
4789
|
+
const result = await runGit(args, { cwd: root, timeout: 3e3 });
|
|
4790
|
+
if (result.exitCode !== 0) return [];
|
|
4791
|
+
return result.stdout.split("\n").map((x) => x.trim()).filter(Boolean);
|
|
4593
4792
|
} catch {
|
|
4594
4793
|
return [];
|
|
4595
4794
|
}
|
|
4596
4795
|
}
|
|
4796
|
+
async function readChangedFiles(root) {
|
|
4797
|
+
if (process.env.HAUS_DISABLE_GIT_SIGNALS === "1") return [];
|
|
4798
|
+
const [unstaged, staged, untracked] = await Promise.all([
|
|
4799
|
+
runGitLines(root, ["diff", "--name-only"]),
|
|
4800
|
+
runGitLines(root, ["diff", "--cached", "--name-only"]),
|
|
4801
|
+
runGitLines(root, ["ls-files", "--others", "--exclude-standard"])
|
|
4802
|
+
]);
|
|
4803
|
+
return [.../* @__PURE__ */ new Set([...unstaged, ...staged, ...untracked])].sort();
|
|
4804
|
+
}
|
|
4597
4805
|
|
|
4598
4806
|
// src/recommender/policies.ts
|
|
4599
4807
|
var UNSUPPORTED = FORBIDDEN_TAGS;
|
|
@@ -4668,6 +4876,7 @@ async function recommend(root, context, opts = {}) {
|
|
|
4668
4876
|
const recommended = [];
|
|
4669
4877
|
const skipped = [];
|
|
4670
4878
|
const sourceTrust = new Map(sources.items.map((x) => [x.source, x.status]));
|
|
4879
|
+
const formerIds = new Set(items.flatMap((item) => normalizeFormerIds(item.formerIds)));
|
|
4671
4880
|
const changedFiles = await readChangedFiles(root);
|
|
4672
4881
|
const skip = (id, code, message, signal) => {
|
|
4673
4882
|
skipped.push({ id, reason: message, skipReasons: [{ code, message, signal }] });
|
|
@@ -4675,6 +4884,10 @@ async function recommend(root, context, opts = {}) {
|
|
|
4675
4884
|
const roleSignal = (name) => scannerRoles.has(name.toLowerCase()) ? `role:${name}` : `deep:role:${name}`;
|
|
4676
4885
|
const stackSignal = (name) => scannerStacks.has(name.toLowerCase()) ? `tag:${name}` : `deep:tag:${name}`;
|
|
4677
4886
|
for (const item of items) {
|
|
4887
|
+
if (formerIds.has(item.id)) {
|
|
4888
|
+
skip(item.id, "former-id", "Catalog id was renamed and cannot be newly recommended");
|
|
4889
|
+
continue;
|
|
4890
|
+
}
|
|
4678
4891
|
const normSource = typeof item.source === "string" ? item.source.trim() : "";
|
|
4679
4892
|
if (!normSource) {
|
|
4680
4893
|
skip(item.id, "invalid-source", "Item source is missing or empty");
|
|
@@ -4916,6 +5129,7 @@ function applyCoInstallSuppression(recommended, skipped) {
|
|
|
4916
5129
|
}
|
|
4917
5130
|
}
|
|
4918
5131
|
var HARD_SKIP_CODES = /* @__PURE__ */ new Set([
|
|
5132
|
+
"former-id",
|
|
4919
5133
|
"unsupported-policy",
|
|
4920
5134
|
"deprecated",
|
|
4921
5135
|
"curated-not-approved",
|
|
@@ -4990,7 +5204,7 @@ function buildOptInEligible(items, skipped) {
|
|
|
4990
5204
|
return eligible;
|
|
4991
5205
|
}
|
|
4992
5206
|
|
|
4993
|
-
// src/
|
|
5207
|
+
// src/claude/setup-core.ts
|
|
4994
5208
|
async function runSetupCore(root, opts) {
|
|
4995
5209
|
const { json, apply, dryRun, quiet, confirm: confirm2 } = opts;
|
|
4996
5210
|
const say = quiet ? () => {
|
|
@@ -5069,6 +5283,24 @@ async function runSetupCore(root, opts) {
|
|
|
5069
5283
|
return { ...baseResult, hooksOk: hooksOkAfter, written: files };
|
|
5070
5284
|
}
|
|
5071
5285
|
|
|
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
|
+
|
|
5072
5304
|
// src/commands/setup-project.ts
|
|
5073
5305
|
async function runSetupProject(options) {
|
|
5074
5306
|
const root = process.cwd();
|
|
@@ -5083,8 +5315,8 @@ async function runSetupProject(options) {
|
|
|
5083
5315
|
// src/commands/init.ts
|
|
5084
5316
|
async function runInit(options) {
|
|
5085
5317
|
const root = process.cwd();
|
|
5086
|
-
const hausDir =
|
|
5087
|
-
const alreadyInit = await
|
|
5318
|
+
const hausDir = path35.join(root, ".haus-workflow");
|
|
5319
|
+
const alreadyInit = await fs28.pathExists(hausDir);
|
|
5088
5320
|
if (alreadyInit) {
|
|
5089
5321
|
log("Haus AI already initialized in this project.");
|
|
5090
5322
|
log("Run `haus setup-project` to reconfigure.");
|
|
@@ -5096,8 +5328,8 @@ async function runInit(options) {
|
|
|
5096
5328
|
|
|
5097
5329
|
// src/install/apply.ts
|
|
5098
5330
|
import crypto2 from "node:crypto";
|
|
5099
|
-
import
|
|
5100
|
-
import
|
|
5331
|
+
import path36 from "node:path";
|
|
5332
|
+
import fs29 from "fs-extra";
|
|
5101
5333
|
|
|
5102
5334
|
// src/install/header.ts
|
|
5103
5335
|
var MD_PREFIX = "<!-- HAUS-MANAGED";
|
|
@@ -5183,57 +5415,57 @@ function hashContent(content2) {
|
|
|
5183
5415
|
}
|
|
5184
5416
|
function sourceVersion() {
|
|
5185
5417
|
try {
|
|
5186
|
-
const pkgPath =
|
|
5187
|
-
const pkg = JSON.parse(
|
|
5418
|
+
const pkgPath = path36.join(packageRoot(), "package.json");
|
|
5419
|
+
const pkg = JSON.parse(fs29.readFileSync(pkgPath, "utf8"));
|
|
5188
5420
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
5189
5421
|
} catch {
|
|
5190
5422
|
return "haus@0.0.0";
|
|
5191
5423
|
}
|
|
5192
5424
|
}
|
|
5193
5425
|
function globalSrcDir() {
|
|
5194
|
-
return
|
|
5426
|
+
return path36.join(packageRoot(), "library", "global");
|
|
5195
5427
|
}
|
|
5196
5428
|
function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
5197
|
-
const skillDir =
|
|
5429
|
+
const skillDir = path36.join(skillsDir, skillName);
|
|
5198
5430
|
const entries = [];
|
|
5199
|
-
for (const rel of
|
|
5431
|
+
for (const rel of fs29.readdirSync(skillDir, { recursive: true })) {
|
|
5200
5432
|
if (rel === "SKILL.md") continue;
|
|
5201
|
-
const abs =
|
|
5202
|
-
if (!
|
|
5203
|
-
const posixRel = rel.split(
|
|
5433
|
+
const abs = path36.join(skillDir, rel);
|
|
5434
|
+
if (!fs29.statSync(abs).isFile()) continue;
|
|
5435
|
+
const posixRel = rel.split(path36.sep).join("/");
|
|
5204
5436
|
entries.push({
|
|
5205
5437
|
stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
|
|
5206
|
-
srcRelPath:
|
|
5207
|
-
destPath:
|
|
5438
|
+
srcRelPath: path36.join("library", "global", "skills", skillName, rel),
|
|
5439
|
+
destPath: path36.join(claudeDir, "skills", skillName, rel)
|
|
5208
5440
|
});
|
|
5209
5441
|
}
|
|
5210
5442
|
return entries;
|
|
5211
5443
|
}
|
|
5212
5444
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
5213
5445
|
const entries = [];
|
|
5214
|
-
const skillsDir =
|
|
5215
|
-
if (
|
|
5216
|
-
for (const skillName of
|
|
5217
|
-
const skillFile =
|
|
5218
|
-
if (
|
|
5446
|
+
const skillsDir = path36.join(srcDir, "skills");
|
|
5447
|
+
if (fs29.pathExistsSync(skillsDir)) {
|
|
5448
|
+
for (const skillName of fs29.readdirSync(skillsDir)) {
|
|
5449
|
+
const skillFile = path36.join(skillsDir, skillName, "SKILL.md");
|
|
5450
|
+
if (fs29.pathExistsSync(skillFile)) {
|
|
5219
5451
|
entries.push({
|
|
5220
5452
|
stableId: `skill.${skillName}`,
|
|
5221
|
-
srcRelPath:
|
|
5222
|
-
destPath:
|
|
5453
|
+
srcRelPath: path36.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
5454
|
+
destPath: path36.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
5223
5455
|
});
|
|
5224
5456
|
entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
|
|
5225
5457
|
}
|
|
5226
5458
|
}
|
|
5227
5459
|
}
|
|
5228
|
-
const commandsDir =
|
|
5229
|
-
if (
|
|
5230
|
-
for (const fileName of
|
|
5460
|
+
const commandsDir = path36.join(srcDir, "commands");
|
|
5461
|
+
if (fs29.pathExistsSync(commandsDir)) {
|
|
5462
|
+
for (const fileName of fs29.readdirSync(commandsDir)) {
|
|
5231
5463
|
if (!fileName.endsWith(".md")) continue;
|
|
5232
5464
|
const commandName = fileName.slice(0, -".md".length);
|
|
5233
5465
|
entries.push({
|
|
5234
5466
|
stableId: `command.${commandName}`,
|
|
5235
|
-
srcRelPath:
|
|
5236
|
-
destPath:
|
|
5467
|
+
srcRelPath: path36.join("library", "global", "commands", fileName),
|
|
5468
|
+
destPath: path36.join(claudeDir, "commands", fileName)
|
|
5237
5469
|
});
|
|
5238
5470
|
}
|
|
5239
5471
|
}
|
|
@@ -5259,7 +5491,7 @@ async function applyInstall(options = {}) {
|
|
|
5259
5491
|
};
|
|
5260
5492
|
const manifestFiles = [];
|
|
5261
5493
|
for (const entry of sourceFiles) {
|
|
5262
|
-
const srcPath =
|
|
5494
|
+
const srcPath = path36.join(packageRoot(), entry.srcRelPath);
|
|
5263
5495
|
const rawContent = await readText(srcPath);
|
|
5264
5496
|
if (rawContent === void 0) {
|
|
5265
5497
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -5279,7 +5511,7 @@ async function applyInstall(options = {}) {
|
|
|
5279
5511
|
}
|
|
5280
5512
|
continue;
|
|
5281
5513
|
}
|
|
5282
|
-
const destExists =
|
|
5514
|
+
const destExists = fs29.pathExistsSync(entry.destPath);
|
|
5283
5515
|
if (destExists) {
|
|
5284
5516
|
const currentContent = await readText(entry.destPath);
|
|
5285
5517
|
if (currentContent !== void 0) {
|
|
@@ -5319,7 +5551,7 @@ async function applyInstall(options = {}) {
|
|
|
5319
5551
|
}
|
|
5320
5552
|
let mergedSettings;
|
|
5321
5553
|
if (!check) {
|
|
5322
|
-
const fragmentPath =
|
|
5554
|
+
const fragmentPath = path36.join(srcDir, "settings-fragments", "hooks.json");
|
|
5323
5555
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
5324
5556
|
const settings = await readSettings();
|
|
5325
5557
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -5333,13 +5565,13 @@ async function applyInstall(options = {}) {
|
|
|
5333
5565
|
const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
|
|
5334
5566
|
for (const entry of existingManifest.files) {
|
|
5335
5567
|
if (currentDestPaths.has(entry.destPath)) continue;
|
|
5336
|
-
if (!
|
|
5568
|
+
if (!fs29.pathExistsSync(entry.destPath)) continue;
|
|
5337
5569
|
const content2 = await readText(entry.destPath);
|
|
5338
5570
|
if (!content2) continue;
|
|
5339
5571
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
5340
5572
|
const currentHash = hashContent(content2);
|
|
5341
5573
|
if (hasHeader && currentHash === entry.hash) {
|
|
5342
|
-
if (!dryRun) await
|
|
5574
|
+
if (!dryRun) await fs29.remove(entry.destPath);
|
|
5343
5575
|
result.deleted.push(entry.destPath);
|
|
5344
5576
|
} else {
|
|
5345
5577
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -5508,8 +5740,8 @@ async function runRefresh() {
|
|
|
5508
5740
|
}
|
|
5509
5741
|
|
|
5510
5742
|
// src/install/scaffold.ts
|
|
5511
|
-
import
|
|
5512
|
-
import
|
|
5743
|
+
import path37 from "node:path";
|
|
5744
|
+
import fs30 from "fs-extra";
|
|
5513
5745
|
async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
5514
5746
|
const scaffolded = [];
|
|
5515
5747
|
const skipped = [];
|
|
@@ -5522,27 +5754,27 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
5522
5754
|
skipped.push(name);
|
|
5523
5755
|
if (outcome === "skipped-exists") skippedExisting.push(name);
|
|
5524
5756
|
};
|
|
5525
|
-
const realRoot = await
|
|
5757
|
+
const realRoot = await fs30.realpath(catalogRoot).catch(() => null);
|
|
5526
5758
|
for (const item of items) {
|
|
5527
5759
|
if (item.type !== "config") continue;
|
|
5528
|
-
const sourcePath =
|
|
5529
|
-
const rel =
|
|
5530
|
-
const escapes = rel === "" ||
|
|
5760
|
+
const sourcePath = path37.resolve(catalogRoot, item.path);
|
|
5761
|
+
const rel = path37.relative(catalogRoot, sourcePath);
|
|
5762
|
+
const escapes = rel === "" || path37.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
|
|
5531
5763
|
if (escapes) {
|
|
5532
5764
|
warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
|
|
5533
5765
|
continue;
|
|
5534
5766
|
}
|
|
5535
|
-
const realSource = await
|
|
5767
|
+
const realSource = await fs30.realpath(sourcePath).catch(() => null);
|
|
5536
5768
|
if (!realRoot || !realSource) {
|
|
5537
5769
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
5538
5770
|
continue;
|
|
5539
5771
|
}
|
|
5540
|
-
const realRel =
|
|
5541
|
-
if (realRel !== "" && (
|
|
5772
|
+
const realRel = path37.relative(realRoot, realSource);
|
|
5773
|
+
if (realRel !== "" && (path37.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
|
|
5542
5774
|
warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
|
|
5543
5775
|
continue;
|
|
5544
5776
|
}
|
|
5545
|
-
const stat = await
|
|
5777
|
+
const stat = await fs30.lstat(sourcePath).catch(() => null);
|
|
5546
5778
|
if (!stat) {
|
|
5547
5779
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
5548
5780
|
continue;
|
|
@@ -5552,14 +5784,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
5552
5784
|
continue;
|
|
5553
5785
|
}
|
|
5554
5786
|
if (stat.isFile()) {
|
|
5555
|
-
const filename =
|
|
5556
|
-
const dest =
|
|
5787
|
+
const filename = path37.basename(sourcePath);
|
|
5788
|
+
const dest = path37.join(projectRoot, filename);
|
|
5557
5789
|
record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
|
|
5558
5790
|
} else if (stat.isDirectory()) {
|
|
5559
|
-
const entries = await
|
|
5791
|
+
const entries = await fs30.readdir(sourcePath);
|
|
5560
5792
|
for (const entry of entries) {
|
|
5561
|
-
const src =
|
|
5562
|
-
const dest =
|
|
5793
|
+
const src = path37.join(sourcePath, entry);
|
|
5794
|
+
const dest = path37.join(projectRoot, entry);
|
|
5563
5795
|
record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
|
|
5564
5796
|
}
|
|
5565
5797
|
}
|
|
@@ -5567,42 +5799,42 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
5567
5799
|
return { scaffolded, skipped, skippedExisting };
|
|
5568
5800
|
}
|
|
5569
5801
|
async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
5570
|
-
const destDisplay =
|
|
5571
|
-
const srcStat = await
|
|
5802
|
+
const destDisplay = path37.relative(projectRoot, dest) || path37.basename(dest);
|
|
5803
|
+
const srcStat = await fs30.lstat(src).catch(() => null);
|
|
5572
5804
|
if (!srcStat) {
|
|
5573
|
-
warn(`Skipping ${
|
|
5805
|
+
warn(`Skipping ${path37.basename(src)}: source not found`);
|
|
5574
5806
|
return "skipped-refused";
|
|
5575
5807
|
}
|
|
5576
5808
|
if (srcStat.isSymbolicLink()) {
|
|
5577
|
-
warn(`Skipping ${
|
|
5809
|
+
warn(`Skipping ${path37.basename(src)}: source is a symlink`);
|
|
5578
5810
|
return "skipped-refused";
|
|
5579
5811
|
}
|
|
5580
|
-
const exists = await
|
|
5812
|
+
const exists = await fs30.pathExists(dest);
|
|
5581
5813
|
if (exists && !opts.force) {
|
|
5582
5814
|
warn(`Skipping ${destDisplay}: already exists \u2014 re-run with --force to overwrite it`);
|
|
5583
5815
|
return "skipped-exists";
|
|
5584
5816
|
}
|
|
5585
5817
|
if (opts.dryRun) {
|
|
5586
|
-
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${
|
|
5818
|
+
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path37.basename(dest)} (${itemId})`);
|
|
5587
5819
|
return "scaffolded";
|
|
5588
5820
|
}
|
|
5589
5821
|
if (exists && opts.force) {
|
|
5590
|
-
await
|
|
5822
|
+
await fs30.remove(dest);
|
|
5591
5823
|
}
|
|
5592
|
-
await
|
|
5593
|
-
await
|
|
5824
|
+
await fs30.ensureDir(path37.dirname(dest));
|
|
5825
|
+
await fs30.copy(src, dest, {
|
|
5594
5826
|
overwrite: true,
|
|
5595
5827
|
dereference: false,
|
|
5596
5828
|
// A vanished/unreadable entry mid-walk must skip that path, not crash the copy.
|
|
5597
5829
|
filter: (s) => {
|
|
5598
5830
|
try {
|
|
5599
|
-
return !
|
|
5831
|
+
return !fs30.lstatSync(s).isSymbolicLink();
|
|
5600
5832
|
} catch {
|
|
5601
5833
|
return false;
|
|
5602
5834
|
}
|
|
5603
5835
|
}
|
|
5604
5836
|
});
|
|
5605
|
-
log(`${exists ? "Overwrote" : "Created"} ${
|
|
5837
|
+
log(`${exists ? "Overwrote" : "Created"} ${path37.basename(dest)} (${itemId})`);
|
|
5606
5838
|
return "scaffolded";
|
|
5607
5839
|
}
|
|
5608
5840
|
|
|
@@ -5659,8 +5891,8 @@ async function runScan(options) {
|
|
|
5659
5891
|
}
|
|
5660
5892
|
|
|
5661
5893
|
// src/commands/undo.ts
|
|
5662
|
-
import
|
|
5663
|
-
import
|
|
5894
|
+
import path38 from "node:path";
|
|
5895
|
+
import fs31 from "fs-extra";
|
|
5664
5896
|
|
|
5665
5897
|
// src/claude/managed-paths.ts
|
|
5666
5898
|
var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md"];
|
|
@@ -5672,67 +5904,85 @@ function coreManagedAbsolutePaths(root) {
|
|
|
5672
5904
|
}
|
|
5673
5905
|
|
|
5674
5906
|
// src/commands/undo.ts
|
|
5675
|
-
async function
|
|
5676
|
-
const
|
|
5907
|
+
async function collectCoreManagedPaths(root) {
|
|
5908
|
+
const existing = [];
|
|
5909
|
+
for (const abs of coreManagedAbsolutePaths(root)) {
|
|
5910
|
+
if (await fs31.pathExists(abs)) existing.push(abs);
|
|
5911
|
+
}
|
|
5912
|
+
return existing;
|
|
5913
|
+
}
|
|
5914
|
+
async function collectLockTrackedPaths(root) {
|
|
5677
5915
|
const lock = await readJson(hausPath(root, "haus.lock.json"));
|
|
5916
|
+
const removable = [];
|
|
5917
|
+
const preserved = [];
|
|
5678
5918
|
for (const row of lock ?? []) {
|
|
5679
|
-
|
|
5680
|
-
|
|
5919
|
+
const relPaths = row.paths ?? [];
|
|
5920
|
+
const existingRel = [];
|
|
5921
|
+
for (const rel of relPaths) {
|
|
5922
|
+
if (await fs31.pathExists(path38.resolve(root, rel))) existingRel.push(rel);
|
|
5923
|
+
}
|
|
5924
|
+
if (existingRel.length === 0) continue;
|
|
5925
|
+
if (row.hash === void 0) {
|
|
5926
|
+
removable.push(...existingRel.map((rel) => path38.resolve(root, rel)));
|
|
5927
|
+
continue;
|
|
5928
|
+
}
|
|
5929
|
+
const currentHash = await hashInstalledPaths(root, existingRel);
|
|
5930
|
+
if (currentHash === row.hash) {
|
|
5931
|
+
removable.push(...existingRel.map((rel) => path38.resolve(root, rel)));
|
|
5932
|
+
} else {
|
|
5933
|
+
preserved.push(...existingRel.map((rel) => path38.resolve(root, rel)));
|
|
5934
|
+
log(`Preserving locally-modified ${row.id ?? "(unknown item)"}: ${existingRel.join(", ")}`);
|
|
5681
5935
|
}
|
|
5682
5936
|
}
|
|
5683
|
-
|
|
5684
|
-
for (const abs of paths) {
|
|
5685
|
-
if (await fs26.pathExists(abs)) existing.push(abs);
|
|
5686
|
-
}
|
|
5687
|
-
return existing;
|
|
5937
|
+
return { removable, preserved };
|
|
5688
5938
|
}
|
|
5689
5939
|
async function settingsHasHausContent(root) {
|
|
5690
5940
|
const settingsPath = claudePath(root, "settings.json");
|
|
5691
|
-
if (!await
|
|
5941
|
+
if (!await fs31.pathExists(settingsPath)) return false;
|
|
5692
5942
|
const settings = await readProjectSettings(root);
|
|
5693
5943
|
return settings._haus != null;
|
|
5694
5944
|
}
|
|
5695
5945
|
async function claudeMdHasHausBlock(root) {
|
|
5696
|
-
const filePath =
|
|
5697
|
-
if (!await
|
|
5698
|
-
const text = await
|
|
5946
|
+
const filePath = path38.join(root, "CLAUDE.md");
|
|
5947
|
+
if (!await fs31.pathExists(filePath)) return false;
|
|
5948
|
+
const text = await fs31.readFile(filePath, "utf8");
|
|
5699
5949
|
return text.includes(BLOCK_BEGIN);
|
|
5700
5950
|
}
|
|
5701
5951
|
async function stripProjectSettings(root) {
|
|
5702
5952
|
const settingsPath = claudePath(root, "settings.json");
|
|
5703
|
-
if (!await
|
|
5953
|
+
if (!await fs31.pathExists(settingsPath)) return false;
|
|
5704
5954
|
let settings = await readProjectSettings(root);
|
|
5705
5955
|
settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
5706
5956
|
const hasContent = Object.keys(settings).length > 0;
|
|
5707
5957
|
if (hasContent) {
|
|
5708
5958
|
await writeProjectSettings(root, settings);
|
|
5709
|
-
log(`Stripped haus rules from ${
|
|
5959
|
+
log(`Stripped haus rules from ${path38.relative(root, settingsPath)} (user settings preserved).`);
|
|
5710
5960
|
return true;
|
|
5711
5961
|
}
|
|
5712
|
-
await
|
|
5713
|
-
log(`Removed ${
|
|
5962
|
+
await fs31.remove(settingsPath);
|
|
5963
|
+
log(`Removed ${path38.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
5714
5964
|
return true;
|
|
5715
5965
|
}
|
|
5716
5966
|
async function stripRootClaudeMd(root) {
|
|
5717
|
-
const filePath =
|
|
5718
|
-
if (!await
|
|
5719
|
-
const prev = await
|
|
5967
|
+
const filePath = path38.join(root, "CLAUDE.md");
|
|
5968
|
+
if (!await fs31.pathExists(filePath)) return false;
|
|
5969
|
+
const prev = await fs31.readFile(filePath, "utf8");
|
|
5720
5970
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
5721
5971
|
const next = stripHausBlock(prev);
|
|
5722
5972
|
if (next.length === 0) {
|
|
5723
|
-
await
|
|
5973
|
+
await fs31.remove(filePath);
|
|
5724
5974
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
5725
5975
|
} else {
|
|
5726
|
-
await
|
|
5976
|
+
await fs31.writeFile(filePath, next, "utf8");
|
|
5727
5977
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
5728
5978
|
}
|
|
5729
5979
|
return true;
|
|
5730
5980
|
}
|
|
5731
5981
|
async function pruneDirIfEmpty(dir) {
|
|
5732
|
-
if (!await
|
|
5982
|
+
if (!await fs31.pathExists(dir)) return;
|
|
5733
5983
|
try {
|
|
5734
|
-
const entries = await
|
|
5735
|
-
if (entries.length === 0) await
|
|
5984
|
+
const entries = await fs31.readdir(dir);
|
|
5985
|
+
if (entries.length === 0) await fs31.remove(dir);
|
|
5736
5986
|
} catch {
|
|
5737
5987
|
}
|
|
5738
5988
|
}
|
|
@@ -5741,27 +5991,34 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
|
5741
5991
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5742
5992
|
const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
|
|
5743
5993
|
for (const abs of managedAbsPaths) {
|
|
5744
|
-
if (!await
|
|
5745
|
-
const rel =
|
|
5746
|
-
const backupPath =
|
|
5747
|
-
await
|
|
5748
|
-
await
|
|
5994
|
+
if (!await fs31.pathExists(abs)) continue;
|
|
5995
|
+
const rel = path38.relative(root, abs);
|
|
5996
|
+
const backupPath = path38.join(backupRoot, rel);
|
|
5997
|
+
await fs31.ensureDir(path38.dirname(backupPath));
|
|
5998
|
+
await fs31.copy(abs, backupPath);
|
|
5749
5999
|
}
|
|
5750
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
6000
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path38.relative(root, backupRoot)}.`);
|
|
5751
6001
|
}
|
|
5752
6002
|
async function runUndo(options) {
|
|
5753
|
-
const root = process.cwd();
|
|
5754
|
-
const
|
|
6003
|
+
const root = options.root ?? process.cwd();
|
|
6004
|
+
const coreManaged = await collectCoreManagedPaths(root);
|
|
6005
|
+
const { removable: lockRemovable, preserved: lockPreserved } = await collectLockTrackedPaths(root);
|
|
6006
|
+
const managed = [.../* @__PURE__ */ new Set([...coreManaged, ...lockRemovable])];
|
|
5755
6007
|
const stripSettings = await settingsHasHausContent(root);
|
|
5756
6008
|
const stripClaudeMd = await claudeMdHasHausBlock(root);
|
|
5757
6009
|
if (managed.length === 0 && !stripSettings && !stripClaudeMd) {
|
|
5758
6010
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
5759
6011
|
return;
|
|
5760
6012
|
}
|
|
5761
|
-
const relTargets = managed.map((p) =>
|
|
6013
|
+
const relTargets = managed.map((p) => path38.relative(root, p));
|
|
5762
6014
|
const summaryParts = [...relTargets];
|
|
5763
6015
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
5764
6016
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
6017
|
+
if (lockPreserved.length > 0) {
|
|
6018
|
+
summaryParts.push(
|
|
6019
|
+
`(preserving ${lockPreserved.length} locally-modified catalog item file(s) \u2014 not shown above)`
|
|
6020
|
+
);
|
|
6021
|
+
}
|
|
5765
6022
|
if (!options.yes) {
|
|
5766
6023
|
const ok = await confirm(
|
|
5767
6024
|
`Remove haus-managed files?
|
|
@@ -5775,9 +6032,9 @@ User-owned .claude/ files will be preserved.`
|
|
|
5775
6032
|
}
|
|
5776
6033
|
await backupManagedFilesBeforeUndo(root, managed);
|
|
5777
6034
|
for (const abs of managed) {
|
|
5778
|
-
if (!await
|
|
5779
|
-
await
|
|
5780
|
-
log(`Removed ${
|
|
6035
|
+
if (!await fs31.pathExists(abs)) continue;
|
|
6036
|
+
await fs31.remove(abs);
|
|
6037
|
+
log(`Removed ${path38.relative(root, abs)}`);
|
|
5781
6038
|
}
|
|
5782
6039
|
if (stripSettings) await stripProjectSettings(root);
|
|
5783
6040
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -5788,8 +6045,8 @@ User-owned .claude/ files will be preserved.`
|
|
|
5788
6045
|
|
|
5789
6046
|
// src/install/uninstall.ts
|
|
5790
6047
|
import crypto3 from "node:crypto";
|
|
5791
|
-
import
|
|
5792
|
-
import
|
|
6048
|
+
import path39 from "node:path";
|
|
6049
|
+
import fs32 from "fs-extra";
|
|
5793
6050
|
async function runUninstall(options = {}) {
|
|
5794
6051
|
const { force = false } = options;
|
|
5795
6052
|
const manifest = await readManifest();
|
|
@@ -5799,7 +6056,7 @@ async function runUninstall(options = {}) {
|
|
|
5799
6056
|
return result;
|
|
5800
6057
|
}
|
|
5801
6058
|
for (const entry of manifest.files) {
|
|
5802
|
-
const exists =
|
|
6059
|
+
const exists = fs32.pathExistsSync(entry.destPath);
|
|
5803
6060
|
if (!exists) continue;
|
|
5804
6061
|
const content2 = await readText(entry.destPath);
|
|
5805
6062
|
if (content2 === void 0) continue;
|
|
@@ -5817,23 +6074,23 @@ async function runUninstall(options = {}) {
|
|
|
5817
6074
|
result.skipped.push(entry.destPath);
|
|
5818
6075
|
continue;
|
|
5819
6076
|
}
|
|
5820
|
-
await
|
|
5821
|
-
await pruneEmptyDir(
|
|
6077
|
+
await fs32.remove(entry.destPath);
|
|
6078
|
+
await pruneEmptyDir(path39.dirname(entry.destPath));
|
|
5822
6079
|
result.deleted.push(entry.destPath);
|
|
5823
6080
|
}
|
|
5824
6081
|
const settings = await readSettings();
|
|
5825
6082
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
5826
6083
|
await writeSettings(stripped);
|
|
5827
6084
|
result.hooksStripped = true;
|
|
5828
|
-
const hausDir =
|
|
6085
|
+
const hausDir = path39.join(globalClaudeDir(), "haus");
|
|
5829
6086
|
const manifestPath2 = hausManifestPath();
|
|
5830
|
-
if (
|
|
5831
|
-
await
|
|
6087
|
+
if (fs32.pathExistsSync(manifestPath2)) {
|
|
6088
|
+
await fs32.remove(manifestPath2);
|
|
5832
6089
|
}
|
|
5833
|
-
if (
|
|
6090
|
+
if (fs32.pathExistsSync(hausDir)) {
|
|
5834
6091
|
try {
|
|
5835
|
-
const remaining = await
|
|
5836
|
-
if (remaining.length === 0) await
|
|
6092
|
+
const remaining = await fs32.readdir(hausDir);
|
|
6093
|
+
if (remaining.length === 0) await fs32.remove(hausDir);
|
|
5837
6094
|
} catch {
|
|
5838
6095
|
}
|
|
5839
6096
|
}
|
|
@@ -5866,12 +6123,12 @@ async function runUninstallCommand(options) {
|
|
|
5866
6123
|
}
|
|
5867
6124
|
|
|
5868
6125
|
// src/commands/update.ts
|
|
5869
|
-
import
|
|
6126
|
+
import path41 from "node:path";
|
|
5870
6127
|
|
|
5871
6128
|
// src/claude/refresh-project.ts
|
|
5872
|
-
import
|
|
6129
|
+
import fs33 from "fs-extra";
|
|
5873
6130
|
async function isHausProject(root) {
|
|
5874
|
-
if (await
|
|
6131
|
+
if (await fs33.pathExists(hausPath(root, "recommendation.json"))) return true;
|
|
5875
6132
|
const result = await readJsonDetailed(claudePath(root, "settings.json"));
|
|
5876
6133
|
if (result.status === "ok") return result.value._haus != null;
|
|
5877
6134
|
return false;
|
|
@@ -5906,7 +6163,7 @@ function summarizeLockDiff(before, after) {
|
|
|
5906
6163
|
|
|
5907
6164
|
// src/update/lockfile.ts
|
|
5908
6165
|
import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
|
|
5909
|
-
import
|
|
6166
|
+
import path40 from "node:path";
|
|
5910
6167
|
async function readLockSummary(root) {
|
|
5911
6168
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
5912
6169
|
const catalogRef = lock.find((item) => item.catalogRef)?.catalogRef ?? null;
|
|
@@ -5917,7 +6174,7 @@ async function checkLock(root) {
|
|
|
5917
6174
|
const hasValidVersions = lock.every(
|
|
5918
6175
|
(item) => !item.version || normalizeVersion(item.version) !== null
|
|
5919
6176
|
);
|
|
5920
|
-
const catalogRef = lock
|
|
6177
|
+
const catalogRef = lock.find((item) => item.catalogRef)?.catalogRef ?? null;
|
|
5921
6178
|
const drift = [];
|
|
5922
6179
|
for (const item of lock) {
|
|
5923
6180
|
if (!item.hash) continue;
|
|
@@ -5942,7 +6199,7 @@ async function applyLock(root) {
|
|
|
5942
6199
|
try {
|
|
5943
6200
|
const backupDir = hausPath(root, "backups");
|
|
5944
6201
|
await mkdir(backupDir, { recursive: true });
|
|
5945
|
-
await copyFile(lockPath,
|
|
6202
|
+
await copyFile(lockPath, path40.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
5946
6203
|
} catch {
|
|
5947
6204
|
}
|
|
5948
6205
|
const enriched = await Promise.all(
|
|
@@ -5964,7 +6221,7 @@ function diffLock(before, after) {
|
|
|
5964
6221
|
}
|
|
5965
6222
|
async function hasLocalOverrides(root) {
|
|
5966
6223
|
try {
|
|
5967
|
-
await readFile2(
|
|
6224
|
+
await readFile2(path40.join(root, ".claude", "settings.json"), "utf8");
|
|
5968
6225
|
return true;
|
|
5969
6226
|
} catch {
|
|
5970
6227
|
return false;
|
|
@@ -5980,23 +6237,28 @@ async function runUpdate(options) {
|
|
|
5980
6237
|
return;
|
|
5981
6238
|
}
|
|
5982
6239
|
if (options.check) {
|
|
5983
|
-
const pkgJson2 = await readJson(
|
|
6240
|
+
const pkgJson2 = await readJson(path41.join(packageRoot(), "package.json"));
|
|
5984
6241
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
5985
|
-
const [status, npmVersion, latestCatalogTag, globalInstallDrift] = await Promise.all([
|
|
5986
|
-
checkLock(root),
|
|
6242
|
+
const [status, npmVersion, latestCatalogTag, globalInstallDrift, catalogItems, lockItems] = await Promise.all([
|
|
6243
|
+
options.fast ? fastLockStatus(root) : checkLock(root),
|
|
5987
6244
|
fetchNpmVersionStatus(currentVersion2),
|
|
5988
6245
|
fetchLatestCatalogTag(),
|
|
5989
|
-
detectGlobalInstallDrift()
|
|
6246
|
+
options.fast ? Promise.resolve(null) : detectGlobalInstallDrift(),
|
|
6247
|
+
loadCatalog(root),
|
|
6248
|
+
readJson(hausPath(root, "haus.lock.json"))
|
|
5990
6249
|
]);
|
|
5991
|
-
const
|
|
5992
|
-
const
|
|
6250
|
+
const formerIdMigrations = findFormerIdMigrations(lockItems ?? [], catalogItems);
|
|
6251
|
+
const installedRef = options.fast ? status.catalogRef : status.catalogRef ?? "main";
|
|
6252
|
+
const catalogRefBehind = installedRef !== null && latestCatalogTag !== null && installedRef !== latestCatalogTag ? `installed from ${installedRef}, latest tag is ${latestCatalogTag}` : false;
|
|
5993
6253
|
log(
|
|
5994
6254
|
JSON.stringify(
|
|
5995
6255
|
{
|
|
5996
6256
|
...status,
|
|
6257
|
+
checkMode: options.fast ? "fast" : "full",
|
|
5997
6258
|
installedCatalogRef: installedRef,
|
|
5998
6259
|
latestCatalogTag,
|
|
5999
6260
|
catalogRefBehind,
|
|
6261
|
+
formerIdMigrations,
|
|
6000
6262
|
globalInstallDrift,
|
|
6001
6263
|
localOverrides: await hasLocalOverrides(root),
|
|
6002
6264
|
summary: diffGeneratedFiles(),
|
|
@@ -6006,10 +6268,11 @@ async function runUpdate(options) {
|
|
|
6006
6268
|
2
|
|
6007
6269
|
)
|
|
6008
6270
|
);
|
|
6009
|
-
|
|
6271
|
+
const hasHashDrift = !options.fast && status.count > 0 && "ok" in status && !status.ok;
|
|
6272
|
+
if (hasHashDrift || formerIdMigrations.length > 0) process.exitCode = 1;
|
|
6010
6273
|
return;
|
|
6011
6274
|
}
|
|
6012
|
-
const pkgJson = await readJson(
|
|
6275
|
+
const pkgJson = await readJson(path41.join(packageRoot(), "package.json"));
|
|
6013
6276
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
6014
6277
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
6015
6278
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -6048,7 +6311,7 @@ async function runFromHookCheck(root) {
|
|
|
6048
6311
|
try {
|
|
6049
6312
|
const summary = await readLockSummary(root);
|
|
6050
6313
|
if (summary.count === 0) return;
|
|
6051
|
-
const pkgJson = await readJson(
|
|
6314
|
+
const pkgJson = await readJson(path41.join(packageRoot(), "package.json"));
|
|
6052
6315
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
6053
6316
|
const [npmVersion, latestCatalogTag] = await Promise.all([
|
|
6054
6317
|
fetchNpmVersionStatus(currentVersion),
|
|
@@ -6114,19 +6377,18 @@ async function detectGlobalInstallDrift() {
|
|
|
6114
6377
|
return null;
|
|
6115
6378
|
}
|
|
6116
6379
|
}
|
|
6380
|
+
async function fastLockStatus(root) {
|
|
6381
|
+
const summary = await readLockSummary(root);
|
|
6382
|
+
return { count: summary.count, catalogRef: summary.catalogRef, drift: [], driftCount: 0 };
|
|
6383
|
+
}
|
|
6117
6384
|
|
|
6118
6385
|
// src/commands/validate-catalog.ts
|
|
6119
|
-
import
|
|
6386
|
+
import path43 from "node:path";
|
|
6120
6387
|
|
|
6121
6388
|
// src/catalog/validate-core.ts
|
|
6122
|
-
import
|
|
6123
|
-
import
|
|
6389
|
+
import fs34 from "node:fs";
|
|
6390
|
+
import path42 from "node:path";
|
|
6124
6391
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
6125
|
-
function isSafeCatalogPath2(itemPath) {
|
|
6126
|
-
if (!itemPath || path36.isAbsolute(itemPath) || itemPath.includes("\\")) return false;
|
|
6127
|
-
const normalized = path36.normalize(itemPath);
|
|
6128
|
-
return !normalized.startsWith("..") && !normalized.includes("/..");
|
|
6129
|
-
}
|
|
6130
6392
|
function auditForbiddenStacks(items) {
|
|
6131
6393
|
const failures = [];
|
|
6132
6394
|
for (const item of items) {
|
|
@@ -6206,7 +6468,7 @@ function auditManifestStructure(manifestVersion, items) {
|
|
|
6206
6468
|
if (item.type === "skill" || item.type === "agent" || item.type === "template" || item.type === "command" || item.type === "config") {
|
|
6207
6469
|
if (!item.path) {
|
|
6208
6470
|
failures.push(`${item.id}: missing path`);
|
|
6209
|
-
} else if (!
|
|
6471
|
+
} else if (!isSafeCatalogPath(item.path)) {
|
|
6210
6472
|
failures.push(`${item.id}: path "${item.path}" is not a safe relative path`);
|
|
6211
6473
|
} else {
|
|
6212
6474
|
const norm = item.path.replace(/\\/g, "/");
|
|
@@ -6235,14 +6497,14 @@ function checkRequiredFrontmatter(text, label) {
|
|
|
6235
6497
|
}
|
|
6236
6498
|
function readForAudit(absPath, label, failures) {
|
|
6237
6499
|
try {
|
|
6238
|
-
return
|
|
6500
|
+
return fs34.readFileSync(absPath, "utf8");
|
|
6239
6501
|
} catch (err) {
|
|
6240
6502
|
failures.push(`${label}: unreadable (${err instanceof Error ? err.message : String(err)})`);
|
|
6241
6503
|
return null;
|
|
6242
6504
|
}
|
|
6243
6505
|
}
|
|
6244
6506
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
6245
|
-
const rel =
|
|
6507
|
+
const rel = path42.relative(manifestDir, absPath);
|
|
6246
6508
|
const failures = [];
|
|
6247
6509
|
const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
|
|
6248
6510
|
if (text === null) return failures;
|
|
@@ -6263,48 +6525,48 @@ function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
|
6263
6525
|
function auditShippedFiles(manifestDir, items) {
|
|
6264
6526
|
const failures = [];
|
|
6265
6527
|
for (const item of items) {
|
|
6266
|
-
if (!item.path || !
|
|
6267
|
-
const absPath =
|
|
6528
|
+
if (!item.path || !isSafeCatalogPath(item.path)) continue;
|
|
6529
|
+
const absPath = path42.join(manifestDir, item.path);
|
|
6268
6530
|
if (item.type === "skill") {
|
|
6269
|
-
const skillMd =
|
|
6270
|
-
if (!
|
|
6271
|
-
failures.push(`${item.id}: missing ${
|
|
6531
|
+
const skillMd = path42.join(absPath, "SKILL.md");
|
|
6532
|
+
if (!fs34.existsSync(skillMd)) {
|
|
6533
|
+
failures.push(`${item.id}: missing ${path42.relative(manifestDir, skillMd)}`);
|
|
6272
6534
|
continue;
|
|
6273
6535
|
}
|
|
6274
6536
|
const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
|
|
6275
6537
|
if (text === null) continue;
|
|
6276
6538
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
6277
6539
|
failures.push(
|
|
6278
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
6540
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path42.relative(manifestDir, skillMd)}`)
|
|
6279
6541
|
);
|
|
6280
6542
|
} else if (item.type === "agent") {
|
|
6281
|
-
if (!
|
|
6543
|
+
if (!fs34.existsSync(absPath)) {
|
|
6282
6544
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
6283
6545
|
continue;
|
|
6284
6546
|
}
|
|
6285
|
-
const rel =
|
|
6547
|
+
const rel = path42.relative(manifestDir, absPath);
|
|
6286
6548
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
6287
6549
|
if (text === null) continue;
|
|
6288
6550
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
6289
6551
|
failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
|
|
6290
6552
|
} else if (item.type === "template") {
|
|
6291
|
-
if (!
|
|
6553
|
+
if (!fs34.existsSync(absPath)) {
|
|
6292
6554
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
6293
6555
|
continue;
|
|
6294
6556
|
}
|
|
6295
6557
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
6296
6558
|
} else if (item.type === "command") {
|
|
6297
|
-
if (!
|
|
6559
|
+
if (!fs34.existsSync(absPath)) {
|
|
6298
6560
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
6299
6561
|
continue;
|
|
6300
6562
|
}
|
|
6301
|
-
const rel =
|
|
6563
|
+
const rel = path42.relative(manifestDir, absPath);
|
|
6302
6564
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
6303
6565
|
if (text === null) continue;
|
|
6304
6566
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
6305
6567
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
6306
6568
|
} else if (item.type === "config") {
|
|
6307
|
-
if (!
|
|
6569
|
+
if (!fs34.existsSync(absPath)) {
|
|
6308
6570
|
failures.push(`${item.id}: missing config path ${item.path}`);
|
|
6309
6571
|
}
|
|
6310
6572
|
}
|
|
@@ -6320,7 +6582,7 @@ var DIR_ITEM_TYPE = {
|
|
|
6320
6582
|
function walkMd(dir, fn, failures) {
|
|
6321
6583
|
let entries;
|
|
6322
6584
|
try {
|
|
6323
|
-
entries =
|
|
6585
|
+
entries = fs34.readdirSync(dir, { withFileTypes: true });
|
|
6324
6586
|
} catch (err) {
|
|
6325
6587
|
failures.push(
|
|
6326
6588
|
`${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
|
|
@@ -6328,7 +6590,7 @@ function walkMd(dir, fn, failures) {
|
|
|
6328
6590
|
return;
|
|
6329
6591
|
}
|
|
6330
6592
|
for (const entry of entries) {
|
|
6331
|
-
const full =
|
|
6593
|
+
const full = path42.join(dir, entry.name);
|
|
6332
6594
|
if (entry.isDirectory()) walkMd(full, fn, failures);
|
|
6333
6595
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
6334
6596
|
}
|
|
@@ -6337,13 +6599,13 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
6337
6599
|
const failures = [];
|
|
6338
6600
|
const pathSourceMap = buildItemPathSourceMap(items);
|
|
6339
6601
|
for (const dir of ["skills", "agents", "templates", "commands"]) {
|
|
6340
|
-
const abs =
|
|
6341
|
-
if (!
|
|
6602
|
+
const abs = path42.join(manifestDir, dir);
|
|
6603
|
+
if (!fs34.existsSync(abs)) continue;
|
|
6342
6604
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
6343
6605
|
walkMd(
|
|
6344
6606
|
abs,
|
|
6345
6607
|
(file) => {
|
|
6346
|
-
const rel =
|
|
6608
|
+
const rel = path42.relative(manifestDir, file);
|
|
6347
6609
|
const text = readForAudit(file, rel, failures);
|
|
6348
6610
|
if (text === null) return;
|
|
6349
6611
|
const norm = rel.replace(/\\/g, "/");
|
|
@@ -6368,6 +6630,7 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
6368
6630
|
function validateCatalogData(manifestDir, manifestVersion, items) {
|
|
6369
6631
|
const failures = [
|
|
6370
6632
|
...auditManifestStructure(manifestVersion, items),
|
|
6633
|
+
...validateFormerIds(items),
|
|
6371
6634
|
...auditOptInMetadata(items),
|
|
6372
6635
|
...auditForbiddenStacks(items),
|
|
6373
6636
|
...auditShippedFiles(manifestDir, items),
|
|
@@ -6384,8 +6647,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
6384
6647
|
process.exitCode = 1;
|
|
6385
6648
|
return;
|
|
6386
6649
|
}
|
|
6387
|
-
const abs =
|
|
6388
|
-
const manifestDir =
|
|
6650
|
+
const abs = path43.resolve(process.cwd(), manifestPath2);
|
|
6651
|
+
const manifestDir = path43.dirname(abs);
|
|
6389
6652
|
const data = await readJson(abs);
|
|
6390
6653
|
if (!data?.items) {
|
|
6391
6654
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -6402,8 +6665,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
6402
6665
|
}
|
|
6403
6666
|
|
|
6404
6667
|
// src/commands/workspace.ts
|
|
6405
|
-
import { existsSync as
|
|
6406
|
-
import
|
|
6668
|
+
import { existsSync as existsSync6, statSync as statSync3 } from "node:fs";
|
|
6669
|
+
import path51 from "node:path";
|
|
6407
6670
|
|
|
6408
6671
|
// src/commands/workspace/aggregate.ts
|
|
6409
6672
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -6457,7 +6720,7 @@ ${summaries.map(
|
|
|
6457
6720
|
}
|
|
6458
6721
|
|
|
6459
6722
|
// src/commands/workspace/config.ts
|
|
6460
|
-
import
|
|
6723
|
+
import path44 from "node:path";
|
|
6461
6724
|
import YAML from "yaml";
|
|
6462
6725
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
6463
6726
|
function parseWorkspaceConfig(text) {
|
|
@@ -6480,11 +6743,11 @@ function parseWorkspaceConfig(text) {
|
|
|
6480
6743
|
};
|
|
6481
6744
|
}
|
|
6482
6745
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
6483
|
-
return parseWorkspaceConfig(await readText(
|
|
6746
|
+
return parseWorkspaceConfig(await readText(path44.join(workspaceRoot, WORKSPACE_FILE)));
|
|
6484
6747
|
}
|
|
6485
6748
|
|
|
6486
6749
|
// src/commands/workspace/discover.ts
|
|
6487
|
-
import
|
|
6750
|
+
import path45 from "node:path";
|
|
6488
6751
|
import fg4 from "fast-glob";
|
|
6489
6752
|
import YAML2 from "yaml";
|
|
6490
6753
|
var DEFAULT_MAX_DEPTH = 3;
|
|
@@ -6515,8 +6778,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
6515
6778
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
6516
6779
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
6517
6780
|
for (const match of matches) {
|
|
6518
|
-
const base =
|
|
6519
|
-
const dir =
|
|
6781
|
+
const base = path45.posix.basename(match);
|
|
6782
|
+
const dir = path45.posix.dirname(match);
|
|
6520
6783
|
const owner = dir === "." ? "." : dir;
|
|
6521
6784
|
if (base === ".git") gitDirs.add(owner);
|
|
6522
6785
|
else manifestDirs.add(owner);
|
|
@@ -6532,9 +6795,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
6532
6795
|
}
|
|
6533
6796
|
repoRoots.sort((a, b) => a.localeCompare(b));
|
|
6534
6797
|
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
6535
|
-
const absDir =
|
|
6536
|
-
const pkg = await readJson(
|
|
6537
|
-
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name :
|
|
6798
|
+
const absDir = path45.resolve(workspaceRoot, relDir);
|
|
6799
|
+
const pkg = await readJson(path45.join(absDir, "package.json"));
|
|
6800
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path45.basename(relDir === "." ? workspaceRoot : absDir);
|
|
6538
6801
|
let role = "auto";
|
|
6539
6802
|
try {
|
|
6540
6803
|
const scan = await scanProject(absDir);
|
|
@@ -6577,7 +6840,7 @@ function renderWorkspaceYaml(config) {
|
|
|
6577
6840
|
});
|
|
6578
6841
|
}
|
|
6579
6842
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
6580
|
-
const yamlPath =
|
|
6843
|
+
const yamlPath = path45.join(workspaceRoot, "haus.workspace.yaml");
|
|
6581
6844
|
const existingText = await readText(yamlPath);
|
|
6582
6845
|
const existing = parseWorkspaceConfig(existingText);
|
|
6583
6846
|
if (existingText && !existing) {
|
|
@@ -6614,18 +6877,18 @@ async function runDiscover(workspaceRoot, opts = {}) {
|
|
|
6614
6877
|
|
|
6615
6878
|
// src/commands/workspace/doctor.ts
|
|
6616
6879
|
import { existsSync as existsSync3 } from "node:fs";
|
|
6617
|
-
import
|
|
6880
|
+
import path47 from "node:path";
|
|
6618
6881
|
|
|
6619
6882
|
// src/commands/workspace/manifest.ts
|
|
6620
6883
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
6621
|
-
import
|
|
6884
|
+
import path46 from "node:path";
|
|
6622
6885
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
6623
6886
|
function manifestPath(workspaceRoot) {
|
|
6624
6887
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
6625
6888
|
}
|
|
6626
6889
|
function hausVersion() {
|
|
6627
6890
|
try {
|
|
6628
|
-
const pkg = JSON.parse(readFileSync3(
|
|
6891
|
+
const pkg = JSON.parse(readFileSync3(path46.join(packageRoot(), "package.json"), "utf8"));
|
|
6629
6892
|
return pkg.version ?? "0.0.0";
|
|
6630
6893
|
} catch {
|
|
6631
6894
|
return "0.0.0";
|
|
@@ -6690,8 +6953,9 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
6690
6953
|
return emit({ workspaceRoot, manifest, drift, detail, json: opts.json });
|
|
6691
6954
|
}
|
|
6692
6955
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
6956
|
+
const catalogRefByRepo = [];
|
|
6693
6957
|
for (const repo of config.repos) {
|
|
6694
|
-
const repoRoot =
|
|
6958
|
+
const repoRoot = path47.resolve(workspaceRoot, repo.path);
|
|
6695
6959
|
const entry = manifestByName.get(repo.name);
|
|
6696
6960
|
if (!entry) {
|
|
6697
6961
|
flag({
|
|
@@ -6724,6 +6988,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
6724
6988
|
});
|
|
6725
6989
|
}
|
|
6726
6990
|
const lock = await checkLock(repoRoot);
|
|
6991
|
+
if (lock.catalogRef) catalogRefByRepo.push({ repo: repo.name, ref: lock.catalogRef });
|
|
6727
6992
|
if (!existsSync3(hausPath(repoRoot, "haus.lock.json"))) {
|
|
6728
6993
|
flag({
|
|
6729
6994
|
repo: repo.name,
|
|
@@ -6741,6 +7006,14 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
6741
7006
|
ok(`- ${repo.name}: OK (${lock.count} lock item(s))`);
|
|
6742
7007
|
}
|
|
6743
7008
|
}
|
|
7009
|
+
const distinctRefs = new Set(catalogRefByRepo.map((r) => r.ref));
|
|
7010
|
+
if (distinctRefs.size > 1) {
|
|
7011
|
+
flag({
|
|
7012
|
+
repo: "(workspace)",
|
|
7013
|
+
kind: "catalog-ref-mismatch",
|
|
7014
|
+
detail: `Repos are on different catalog refs \u2014 ${catalogRefByRepo.map((r) => `${r.repo}: ${r.ref}`).join(", ")}. Run \`haus workspace setup --write\` to bring them onto the same ref.`
|
|
7015
|
+
});
|
|
7016
|
+
}
|
|
6744
7017
|
return emit({ workspaceRoot, manifest, drift, detail, json: opts.json });
|
|
6745
7018
|
}
|
|
6746
7019
|
function emit(args) {
|
|
@@ -6765,11 +7038,11 @@ function emit(args) {
|
|
|
6765
7038
|
|
|
6766
7039
|
// src/commands/workspace/setup.ts
|
|
6767
7040
|
import { existsSync as existsSync4, statSync } from "node:fs";
|
|
6768
|
-
import
|
|
7041
|
+
import path49 from "node:path";
|
|
6769
7042
|
|
|
6770
7043
|
// src/claude/write-workspace-claude-md.ts
|
|
6771
|
-
import
|
|
6772
|
-
import
|
|
7044
|
+
import path48 from "node:path";
|
|
7045
|
+
import fs35 from "fs-extra";
|
|
6773
7046
|
function buildWorkspaceImportBlock(client, members) {
|
|
6774
7047
|
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
6775
7048
|
const body = [
|
|
@@ -6787,15 +7060,16 @@ ${BLOCK_END}`;
|
|
|
6787
7060
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
6788
7061
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
6789
7062
|
const dryRun = opts.dryRun ?? false;
|
|
6790
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
7063
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path48.join(workspaceRoot, "CLAUDE.md");
|
|
6791
7064
|
const say = opts.quiet ? () => {
|
|
6792
7065
|
} : log;
|
|
6793
|
-
const
|
|
7066
|
+
const existed = await fs35.pathExists(filePath);
|
|
7067
|
+
const prev = existed ? await fs35.readFile(filePath, "utf8") : "";
|
|
6794
7068
|
const next = opts.collision ? `${block}
|
|
6795
7069
|
` : injectHausBlock(prev, block);
|
|
6796
7070
|
const printable = displayPath(workspaceRoot, filePath);
|
|
6797
7071
|
if (dryRun) {
|
|
6798
|
-
if (!
|
|
7072
|
+
if (!existed) {
|
|
6799
7073
|
say(createUnifiedDiff(printable, "", next));
|
|
6800
7074
|
} else if (hasTextChanged(prev, next)) {
|
|
6801
7075
|
say(createUnifiedDiff(printable, prev, next));
|
|
@@ -6804,31 +7078,33 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
6804
7078
|
}
|
|
6805
7079
|
return filePath;
|
|
6806
7080
|
}
|
|
6807
|
-
if (hasTextChanged(prev, next)
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
7081
|
+
if (!existed || hasTextChanged(prev, next)) {
|
|
7082
|
+
if (existed) {
|
|
7083
|
+
const diffText = createUnifiedDiff(printable, prev, next);
|
|
7084
|
+
const summary = summarizeDiff(diffText);
|
|
7085
|
+
say(`Overwriting ${printable} (diff +${summary.additions} -${summary.deletions})`);
|
|
7086
|
+
}
|
|
7087
|
+
await writeText(filePath, next);
|
|
6811
7088
|
}
|
|
6812
|
-
await writeText(filePath, next);
|
|
6813
7089
|
return filePath;
|
|
6814
7090
|
}
|
|
6815
7091
|
|
|
6816
7092
|
// src/commands/workspace/setup.ts
|
|
6817
7093
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
6818
|
-
let dir =
|
|
7094
|
+
let dir = path49.resolve(start);
|
|
6819
7095
|
for (; ; ) {
|
|
6820
|
-
if (existsSync4(
|
|
6821
|
-
const parent =
|
|
6822
|
-
if (parent === dir) return
|
|
7096
|
+
if (existsSync4(path49.join(dir, WORKSPACE_FILE))) return dir;
|
|
7097
|
+
const parent = path49.dirname(dir);
|
|
7098
|
+
if (parent === dir) return path49.resolve(start);
|
|
6823
7099
|
dir = parent;
|
|
6824
7100
|
}
|
|
6825
7101
|
}
|
|
6826
7102
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
6827
|
-
return
|
|
7103
|
+
return path49.resolve(workspaceRoot, repoPath) === path49.resolve(workspaceRoot);
|
|
6828
7104
|
}
|
|
6829
7105
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
6830
7106
|
const apply = options.write ?? false;
|
|
6831
|
-
const configText = await readText(
|
|
7107
|
+
const configText = await readText(path49.join(workspaceRoot, WORKSPACE_FILE));
|
|
6832
7108
|
if (!configText) {
|
|
6833
7109
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
6834
7110
|
process.exitCode = 1;
|
|
@@ -6855,7 +7131,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
6855
7131
|
const statuses = [];
|
|
6856
7132
|
const aggregateInputs = [];
|
|
6857
7133
|
for (const repo of repos) {
|
|
6858
|
-
const repoRoot =
|
|
7134
|
+
const repoRoot = path49.resolve(workspaceRoot, repo.path);
|
|
6859
7135
|
say(`
|
|
6860
7136
|
\u2192 ${repo.name} (${repo.path})`);
|
|
6861
7137
|
try {
|
|
@@ -6926,7 +7202,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
6926
7202
|
const status = statusByName.get(repo.name);
|
|
6927
7203
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
6928
7204
|
if (status?.status === "ok") {
|
|
6929
|
-
const lock = await checkLock(
|
|
7205
|
+
const lock = await checkLock(path49.resolve(workspaceRoot, repo.path));
|
|
6930
7206
|
manifestRepos.push({
|
|
6931
7207
|
name: repo.name,
|
|
6932
7208
|
path: repo.path,
|
|
@@ -6988,6 +7264,91 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
6988
7264
|
return result;
|
|
6989
7265
|
}
|
|
6990
7266
|
|
|
7267
|
+
// src/commands/workspace/undo.ts
|
|
7268
|
+
import { existsSync as existsSync5, statSync as statSync2 } from "node:fs";
|
|
7269
|
+
import path50 from "node:path";
|
|
7270
|
+
import fs36 from "fs-extra";
|
|
7271
|
+
var WORKSPACE_AGGREGATE_FILES = [
|
|
7272
|
+
"workspace-summary.json",
|
|
7273
|
+
"dependency-ownership-map.json",
|
|
7274
|
+
"cross-repo-summary.md",
|
|
7275
|
+
"workspace-context-map.json"
|
|
7276
|
+
];
|
|
7277
|
+
async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
7278
|
+
const config = await readWorkspaceConfig(workspaceRoot);
|
|
7279
|
+
if (!config) {
|
|
7280
|
+
error(`Missing or malformed ${WORKSPACE_FILE}. Nothing to undo.`);
|
|
7281
|
+
process.exitCode = 1;
|
|
7282
|
+
return;
|
|
7283
|
+
}
|
|
7284
|
+
if (config.repos.length === 0) {
|
|
7285
|
+
log(`No repos configured in ${WORKSPACE_FILE}.`);
|
|
7286
|
+
}
|
|
7287
|
+
if (!options.yes) {
|
|
7288
|
+
const ok = await confirm(
|
|
7289
|
+
`Undo haus setup for ${config.repos.length} repo(s) in this workspace, plus workspace-root artifacts?`
|
|
7290
|
+
);
|
|
7291
|
+
if (!ok) {
|
|
7292
|
+
log("Cancelled.");
|
|
7293
|
+
return;
|
|
7294
|
+
}
|
|
7295
|
+
}
|
|
7296
|
+
let anyRepoFailed = false;
|
|
7297
|
+
for (const repo of config.repos) {
|
|
7298
|
+
const repoRoot = path50.resolve(workspaceRoot, repo.path);
|
|
7299
|
+
log(`
|
|
7300
|
+
\u2192 ${repo.name} (${repo.path})`);
|
|
7301
|
+
try {
|
|
7302
|
+
if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
|
|
7303
|
+
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
7304
|
+
}
|
|
7305
|
+
await runUndo({ yes: true, root: repoRoot });
|
|
7306
|
+
} catch (err) {
|
|
7307
|
+
anyRepoFailed = true;
|
|
7308
|
+
warn(`Undo failed for ${repo.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7309
|
+
}
|
|
7310
|
+
}
|
|
7311
|
+
for (const rel of WORKSPACE_AGGREGATE_FILES) {
|
|
7312
|
+
const abs = hausPath(workspaceRoot, rel);
|
|
7313
|
+
if (await fs36.pathExists(abs)) {
|
|
7314
|
+
await fs36.remove(abs);
|
|
7315
|
+
log(`Removed ${path50.relative(workspaceRoot, abs)}`);
|
|
7316
|
+
}
|
|
7317
|
+
}
|
|
7318
|
+
const manifestFile = manifestPath(workspaceRoot);
|
|
7319
|
+
if (await fs36.pathExists(manifestFile)) {
|
|
7320
|
+
await fs36.remove(manifestFile);
|
|
7321
|
+
log(`Removed ${path50.relative(workspaceRoot, manifestFile)}`);
|
|
7322
|
+
}
|
|
7323
|
+
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
7324
|
+
if (await fs36.pathExists(workspaceMdPath)) {
|
|
7325
|
+
await fs36.remove(workspaceMdPath);
|
|
7326
|
+
log(`Removed ${path50.relative(workspaceRoot, workspaceMdPath)}`);
|
|
7327
|
+
} else {
|
|
7328
|
+
const claudeMdPath = path50.join(workspaceRoot, "CLAUDE.md");
|
|
7329
|
+
if (existsSync5(claudeMdPath)) {
|
|
7330
|
+
const prev = await readText(claudeMdPath);
|
|
7331
|
+
if (prev?.includes(BLOCK_BEGIN)) {
|
|
7332
|
+
const next = stripHausBlock(prev);
|
|
7333
|
+
if (next.length === 0) {
|
|
7334
|
+
await fs36.remove(claudeMdPath);
|
|
7335
|
+
log("Removed CLAUDE.md (only contained the workspace haus import block).");
|
|
7336
|
+
} else {
|
|
7337
|
+
await fs36.writeFile(claudeMdPath, next, "utf8");
|
|
7338
|
+
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
7339
|
+
}
|
|
7340
|
+
}
|
|
7341
|
+
}
|
|
7342
|
+
}
|
|
7343
|
+
log(`${WORKSPACE_FILE} left in place \u2014 it's your own config, not haus-owned output.`);
|
|
7344
|
+
if (anyRepoFailed) {
|
|
7345
|
+
process.exitCode = 1;
|
|
7346
|
+
warn("Workspace undo finished with at least one repo failure \u2014 see above.");
|
|
7347
|
+
} else {
|
|
7348
|
+
log("Workspace undo complete.");
|
|
7349
|
+
}
|
|
7350
|
+
}
|
|
7351
|
+
|
|
6991
7352
|
// src/commands/workspace.ts
|
|
6992
7353
|
function normalizeOnly(only) {
|
|
6993
7354
|
if (!only) return void 0;
|
|
@@ -7016,7 +7377,7 @@ relationships: []
|
|
|
7016
7377
|
log("Workspace initialized.");
|
|
7017
7378
|
}
|
|
7018
7379
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
7019
|
-
const configText = await readText(
|
|
7380
|
+
const configText = await readText(path51.join(workspaceRoot, WORKSPACE_FILE));
|
|
7020
7381
|
if (!configText) {
|
|
7021
7382
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
7022
7383
|
process.exitCode = 1;
|
|
@@ -7038,9 +7399,9 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
7038
7399
|
const inputs = [];
|
|
7039
7400
|
const failures = [];
|
|
7040
7401
|
for (const repo of config.repos) {
|
|
7041
|
-
const repoRoot =
|
|
7402
|
+
const repoRoot = path51.resolve(workspaceRoot, repo.path);
|
|
7042
7403
|
try {
|
|
7043
|
-
if (!
|
|
7404
|
+
if (!existsSync6(repoRoot) || !statSync3(repoRoot).isDirectory()) {
|
|
7044
7405
|
throw new Error("path is not a directory");
|
|
7045
7406
|
}
|
|
7046
7407
|
const result = await scanProject(repoRoot);
|
|
@@ -7092,13 +7453,16 @@ async function runWorkspace(action, options = {}) {
|
|
|
7092
7453
|
case "doctor":
|
|
7093
7454
|
await runWorkspaceDoctor(workspaceRoot, { json: options.json });
|
|
7094
7455
|
return;
|
|
7456
|
+
case "undo":
|
|
7457
|
+
await runWorkspaceUndo(workspaceRoot, { yes: options.yes });
|
|
7458
|
+
return;
|
|
7095
7459
|
}
|
|
7096
7460
|
}
|
|
7097
7461
|
|
|
7098
7462
|
// src/cli.ts
|
|
7099
7463
|
function cliVersion() {
|
|
7100
7464
|
try {
|
|
7101
|
-
const pkgPath =
|
|
7465
|
+
const pkgPath = path52.join(packageRoot(), "package.json");
|
|
7102
7466
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
7103
7467
|
return pkg.version ?? "0.0.0";
|
|
7104
7468
|
} catch {
|
|
@@ -7108,7 +7472,7 @@ function cliVersion() {
|
|
|
7108
7472
|
var program = new Command();
|
|
7109
7473
|
function validateRuntimeNodeVersion() {
|
|
7110
7474
|
try {
|
|
7111
|
-
const pkgPath =
|
|
7475
|
+
const pkgPath = path52.join(packageRoot(), "package.json");
|
|
7112
7476
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
7113
7477
|
const requiredRange = pkg.engines?.node;
|
|
7114
7478
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -7149,7 +7513,7 @@ program.command("clone").description("Clone a single git repository by URL").arg
|
|
|
7149
7513
|
program.command("refresh").action(runRefresh);
|
|
7150
7514
|
program.command("catalog-audit").action(runCatalogAudit);
|
|
7151
7515
|
program.command("validate-catalog").argument("[manifest]").action(runValidateCatalog);
|
|
7152
|
-
program.command("update").option("--check").option("--from-hook", "Emit JSON for the Claude Code SessionStart hook (silent if up to date)").action(runUpdate);
|
|
7516
|
+
program.command("update").option("--check").option("--fast", "With --check, skip per-item content hashing (cheap count+ref only)").option("--from-hook", "Emit JSON for the Claude Code SessionStart hook (silent if up to date)").action(runUpdate);
|
|
7153
7517
|
program.command("install").option("--dry-run").option("--force").option("--check", "Exit non-zero if any HAUS-MANAGED file is out of date").option("--postinstall", "Run by the npm postinstall hook; prints a plain-language change notice").action(runInstall);
|
|
7154
7518
|
program.command("scaffold [ids...]").description(
|
|
7155
7519
|
"Copy catalog config items (ESLint, Prettier) into the project root. Pass item IDs to scaffold specific items, or omit to scaffold all available config items."
|
|
@@ -7176,6 +7540,7 @@ workspace.command("discover").description("Auto-find member repos and write/merg
|
|
|
7176
7540
|
workspace.command("scan").description("Aggregate a cross-repo summary from a fast scan of each repo").option("--json", "Output the written artifact paths as JSON").action((opts) => runWorkspace("scan", opts));
|
|
7177
7541
|
workspace.command("setup").description("Per-repo setup loop + workspace layer + manifest").option("--write", "Apply changes (default previews only)").option("--dry-run", "Preview changes without writing").option("--json", "Emit machine-readable per-repo output").option("--continue-on-error", "Keep going past a failed repo (default fail-fast)").option("--only <names>", "Restrict to comma-separated repo names").action((opts) => runWorkspace("setup", opts));
|
|
7178
7542
|
workspace.command("doctor").description("Report workspace drift against the manifest").option("--json", "Output the manifest and drift array as JSON").action((opts) => runWorkspace("doctor", opts));
|
|
7543
|
+
workspace.command("undo").description("Revert haus setup for every configured repo, plus workspace-root artifacts").option("-y, --yes", "Skip confirmation").action((opts) => runWorkspace("undo", opts));
|
|
7179
7544
|
program.parseAsync(process.argv).catch((err) => {
|
|
7180
7545
|
const message = err instanceof Error ? err.message : String(err);
|
|
7181
7546
|
error(message);
|