@haus-tech/haus-workflow 0.28.0 → 0.29.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/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { readFileSync as readFileSync4 } from "node:fs";
5
- import path35 from "node:path";
5
+ import path36 from "node:path";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/commands/apply.ts
@@ -200,7 +200,6 @@ var validation_rules_default = {
200
200
  "scss-modules",
201
201
  "vue",
202
202
  "expressjs",
203
- "soup-base",
204
203
  "laravel",
205
204
  "laravel-nova",
206
205
  "wordpress",
@@ -638,7 +637,7 @@ function safeJoin(base, itemPath) {
638
637
  const resolved = path2.resolve(base, itemPath);
639
638
  return resolved.startsWith(base + path2.sep) || resolved === base ? resolved : null;
640
639
  }
641
- var KNOWN_ITEM_TYPES = /* @__PURE__ */ new Set(["skill", "agent", "template", "command"]);
640
+ var KNOWN_ITEM_TYPES = /* @__PURE__ */ new Set(["skill", "agent", "template", "command", "config"]);
642
641
  function isMarkdownPath(rel) {
643
642
  return rel.toLowerCase().endsWith(".md");
644
643
  }
@@ -783,12 +782,13 @@ async function writeFetchedDirectory(destDir, fetched) {
783
782
  }
784
783
  }
785
784
  async function syncDirectoryFromPrefix(item, catalogPrefix, destDir, base, opts) {
786
- const relFiles = await listFilesUnderCatalogPrefix(catalogPrefix, base);
785
+ const relFiles = opts.relFiles ?? await listFilesUnderCatalogPrefix(catalogPrefix, base);
787
786
  if (!relFiles) {
788
787
  warn(`Failed to list files for ${item.id}`);
789
788
  return "failed";
790
789
  }
791
- if (!relFiles.includes("SKILL.md") && catalogPrefix !== SUPERPOWERS_SHARED_CATALOG_REL) {
790
+ const requireSkillMd = opts.requireSkillMd ?? true;
791
+ if (requireSkillMd && !relFiles.includes("SKILL.md") && catalogPrefix !== SUPERPOWERS_SHARED_CATALOG_REL) {
792
792
  warn(`Failed to fetch content for ${item.id}: missing SKILL.md`);
793
793
  return "failed";
794
794
  }
@@ -834,6 +834,42 @@ async function syncSkillDirectory(item, base) {
834
834
  return "failed";
835
835
  }
836
836
  }
837
+ async function syncConfigItem(item, base) {
838
+ const dest = safeJoin(getCacheDir(), item.path);
839
+ if (!dest) {
840
+ warn(`Skipping ${item.id}: path traversal detected`);
841
+ return "failed";
842
+ }
843
+ const relFiles = await listFilesUnderCatalogPrefix(item.path, base);
844
+ if (relFiles && relFiles.length > 0) {
845
+ try {
846
+ return await syncDirectoryFromPrefix(item, item.path, dest, base, {
847
+ validateMarkdown: false,
848
+ requireSkillMd: false,
849
+ relFiles
850
+ });
851
+ } catch (err) {
852
+ warn(`Failed to cache ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
853
+ return "failed";
854
+ }
855
+ }
856
+ const text = await fetchText(`${base}/${item.path}`);
857
+ if (text === null) {
858
+ warn(`Failed to fetch content for ${item.id}`);
859
+ return "failed";
860
+ }
861
+ const verdict = validateCatalogItem(item, text);
862
+ if (!verdict.ok) {
863
+ warn(`Rejected ${item.id} at ingest: ${verdict.reason}`);
864
+ return "failed";
865
+ }
866
+ try {
867
+ return await writeTextIfChanged(dest, text);
868
+ } catch (err) {
869
+ warn(`Failed to cache ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
870
+ return "failed";
871
+ }
872
+ }
837
873
  async function syncSuperpowersShared(base) {
838
874
  const relFiles = await listFilesUnderCatalogPrefix(SUPERPOWERS_SHARED_CATALOG_REL, base);
839
875
  if (!relFiles || relFiles.length === 0) return "skipped";
@@ -867,6 +903,9 @@ async function syncOneItem(item, base) {
867
903
  if (item.type === "skill") {
868
904
  return syncSkillDirectory(item, base);
869
905
  }
906
+ if (item.type === "config") {
907
+ return syncConfigItem(item, base);
908
+ }
870
909
  const dest = safeJoin(getCacheDir(), item.path);
871
910
  if (!dest) {
872
911
  warn(`Skipping ${item.id}: path traversal detected`);
@@ -1594,8 +1633,8 @@ function buildDenyRules() {
1594
1633
  for (const command of DENY_COMMANDS) {
1595
1634
  rules.push(`Bash(${command}:*)`);
1596
1635
  }
1597
- for (const path36 of DENY_PATHS) {
1598
- const pattern = DENY_DIRS.has(path36) ? `${path36}/**` : path36;
1636
+ for (const path37 of DENY_PATHS) {
1637
+ const pattern = DENY_DIRS.has(path37) ? `${path37}/**` : path37;
1599
1638
  for (const tool of FILE_TOOLS) {
1600
1639
  rules.push(`${tool}(${pattern})`);
1601
1640
  }
@@ -2203,6 +2242,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
2203
2242
  continue;
2204
2243
  }
2205
2244
  }
2245
+ if (item.type === "config") continue;
2206
2246
  const sourcePath = catalogItemContentPath(contentRoot, manifestItem);
2207
2247
  const target = targetDirForType(item.type);
2208
2248
  if (!target) {
@@ -3503,6 +3543,15 @@ async function recommend(root, context) {
3503
3543
  skip(item.id, "unsupported-policy", "Unsupported stack policy");
3504
3544
  continue;
3505
3545
  }
3546
+ if (item.type === "config") {
3547
+ skip(
3548
+ item.id,
3549
+ "config-scaffold-only",
3550
+ "Config item \u2014 install with `haus scaffold`",
3551
+ "type:config"
3552
+ );
3553
+ continue;
3554
+ }
3506
3555
  if (item.reviewStatus === "deprecated") {
3507
3556
  skip(item.id, "deprecated", "Catalog item is deprecated", "reviewStatus:deprecated");
3508
3557
  continue;
@@ -4124,6 +4173,144 @@ async function runRefresh() {
4124
4173
  log(`Recommended items: ${recommendation.recommended.length}`);
4125
4174
  }
4126
4175
 
4176
+ // src/install/scaffold.ts
4177
+ import path22 from "node:path";
4178
+ import fs17 from "fs-extra";
4179
+ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
4180
+ const scaffolded = [];
4181
+ const skipped = [];
4182
+ const skippedExisting = [];
4183
+ const record = (name, outcome) => {
4184
+ if (outcome === "scaffolded") {
4185
+ scaffolded.push(name);
4186
+ return;
4187
+ }
4188
+ skipped.push(name);
4189
+ if (outcome === "skipped-exists") skippedExisting.push(name);
4190
+ };
4191
+ const realRoot = await fs17.realpath(catalogRoot).catch(() => null);
4192
+ for (const item of items) {
4193
+ if (item.type !== "config") continue;
4194
+ const sourcePath = path22.resolve(catalogRoot, item.path);
4195
+ const rel = path22.relative(catalogRoot, sourcePath);
4196
+ const escapes = rel === "" || path22.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
4197
+ if (escapes) {
4198
+ warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
4199
+ continue;
4200
+ }
4201
+ const realSource = await fs17.realpath(sourcePath).catch(() => null);
4202
+ if (!realRoot || !realSource) {
4203
+ warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
4204
+ continue;
4205
+ }
4206
+ const realRel = path22.relative(realRoot, realSource);
4207
+ if (realRel !== "" && (path22.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
4208
+ warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
4209
+ continue;
4210
+ }
4211
+ const stat = await fs17.lstat(sourcePath).catch(() => null);
4212
+ if (!stat) {
4213
+ warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
4214
+ continue;
4215
+ }
4216
+ if (stat.isSymbolicLink()) {
4217
+ warn(`Skipping ${item.id}: source path is a symlink`);
4218
+ continue;
4219
+ }
4220
+ if (stat.isFile()) {
4221
+ const filename = path22.basename(sourcePath);
4222
+ const dest = path22.join(projectRoot, filename);
4223
+ record(filename, await scaffoldFile(sourcePath, dest, item.id, opts));
4224
+ } else if (stat.isDirectory()) {
4225
+ const entries = await fs17.readdir(sourcePath);
4226
+ for (const entry of entries) {
4227
+ const src = path22.join(sourcePath, entry);
4228
+ const dest = path22.join(projectRoot, entry);
4229
+ record(entry, await scaffoldFile(src, dest, item.id, opts));
4230
+ }
4231
+ }
4232
+ }
4233
+ return { scaffolded, skipped, skippedExisting };
4234
+ }
4235
+ async function scaffoldFile(src, dest, itemId, opts) {
4236
+ const srcStat = await fs17.lstat(src).catch(() => null);
4237
+ if (!srcStat) {
4238
+ warn(`Skipping ${path22.basename(src)}: source not found`);
4239
+ return "skipped-refused";
4240
+ }
4241
+ if (srcStat.isSymbolicLink()) {
4242
+ warn(`Skipping ${path22.basename(src)}: source is a symlink`);
4243
+ return "skipped-refused";
4244
+ }
4245
+ const exists = await fs17.pathExists(dest);
4246
+ if (exists && !opts.force) {
4247
+ warn(`Skipping ${path22.basename(dest)}: already exists (use --force to overwrite)`);
4248
+ return "skipped-exists";
4249
+ }
4250
+ if (opts.dryRun) {
4251
+ log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path22.basename(dest)} (${itemId})`);
4252
+ return "scaffolded";
4253
+ }
4254
+ if (exists && opts.force) {
4255
+ await fs17.remove(dest);
4256
+ }
4257
+ await fs17.ensureDir(path22.dirname(dest));
4258
+ await fs17.copy(src, dest, {
4259
+ overwrite: true,
4260
+ dereference: false,
4261
+ // A vanished/unreadable entry mid-walk must skip that path, not crash the copy.
4262
+ filter: (s) => {
4263
+ try {
4264
+ return !fs17.lstatSync(s).isSymbolicLink();
4265
+ } catch {
4266
+ return false;
4267
+ }
4268
+ }
4269
+ });
4270
+ log(`${exists ? "Overwrote" : "Created"} ${path22.basename(dest)} (${itemId})`);
4271
+ return "scaffolded";
4272
+ }
4273
+
4274
+ // src/commands/scaffold.ts
4275
+ async function runScaffold(ids, options) {
4276
+ const { items: allItems, contentRoot } = await loadCatalogContext(options.root);
4277
+ const configItems = allItems.filter((item) => {
4278
+ if (item.type !== "config") return false;
4279
+ if (item.reviewStatus && item.reviewStatus !== "approved") return false;
4280
+ if (item.riskLevel === "blocked") return false;
4281
+ if (ids.length > 0 && !ids.includes(item.id)) return false;
4282
+ return true;
4283
+ });
4284
+ if (configItems.length === 0) {
4285
+ if (ids.length > 0) {
4286
+ log(`No config items found for: ${ids.join(", ")}`);
4287
+ } else {
4288
+ log("No config items available in catalog.");
4289
+ }
4290
+ return;
4291
+ }
4292
+ log(`Scaffolding ${configItems.length} config item(s)\u2026`);
4293
+ const result = await scaffoldConfigItems(options.root, contentRoot, configItems, {
4294
+ force: options.force,
4295
+ dryRun: options.dryRun
4296
+ });
4297
+ if (result.scaffolded.length > 0) {
4298
+ if (options.dryRun) {
4299
+ log(`[dry-run] would scaffold: ${result.scaffolded.join(", ")} (no files written)`);
4300
+ } else {
4301
+ log(`\u2713 Scaffolded: ${result.scaffolded.join(", ")}`);
4302
+ }
4303
+ }
4304
+ if (result.skipped.length > 0) {
4305
+ log(`Skipped: ${result.skipped.join(", ")}`);
4306
+ if (result.skippedExisting.length > 0) {
4307
+ log(
4308
+ `Already present (re-run with --force to overwrite): ${result.skippedExisting.join(", ")}`
4309
+ );
4310
+ }
4311
+ }
4312
+ }
4313
+
4127
4314
  // src/commands/scan.ts
4128
4315
  async function runScan(options) {
4129
4316
  const result = await scanProject(process.cwd());
@@ -4137,8 +4324,8 @@ async function runScan(options) {
4137
4324
  }
4138
4325
 
4139
4326
  // src/commands/undo.ts
4140
- import path22 from "node:path";
4141
- import fs17 from "fs-extra";
4327
+ import path23 from "node:path";
4328
+ import fs18 from "fs-extra";
4142
4329
 
4143
4330
  // src/claude/managed-paths.ts
4144
4331
  var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md", "commands/haus-doctor.md"];
@@ -4155,74 +4342,74 @@ async function collectManagedPaths(root) {
4155
4342
  const lock = await readJson(hausPath(root, "haus.lock.json"));
4156
4343
  for (const row of lock ?? []) {
4157
4344
  for (const rel of row.paths ?? []) {
4158
- paths.add(path22.resolve(root, rel));
4345
+ paths.add(path23.resolve(root, rel));
4159
4346
  }
4160
4347
  }
4161
4348
  const existing = [];
4162
4349
  for (const abs of paths) {
4163
- if (await fs17.pathExists(abs)) existing.push(abs);
4350
+ if (await fs18.pathExists(abs)) existing.push(abs);
4164
4351
  }
4165
4352
  return existing;
4166
4353
  }
4167
4354
  async function settingsHasHausContent(root) {
4168
4355
  const settingsPath = claudePath(root, "settings.json");
4169
- if (!await fs17.pathExists(settingsPath)) return false;
4356
+ if (!await fs18.pathExists(settingsPath)) return false;
4170
4357
  const settings = await readProjectSettings(root);
4171
4358
  return settings._haus != null;
4172
4359
  }
4173
4360
  async function claudeMdHasHausBlock(root) {
4174
- const filePath = path22.join(root, "CLAUDE.md");
4175
- if (!await fs17.pathExists(filePath)) return false;
4176
- const text = await fs17.readFile(filePath, "utf8");
4361
+ const filePath = path23.join(root, "CLAUDE.md");
4362
+ if (!await fs18.pathExists(filePath)) return false;
4363
+ const text = await fs18.readFile(filePath, "utf8");
4177
4364
  return text.includes(BLOCK_BEGIN);
4178
4365
  }
4179
4366
  async function stripProjectSettings(root) {
4180
4367
  const settingsPath = claudePath(root, "settings.json");
4181
- if (!await fs17.pathExists(settingsPath)) return false;
4368
+ if (!await fs18.pathExists(settingsPath)) return false;
4182
4369
  let settings = await readProjectSettings(root);
4183
4370
  settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
4184
4371
  const hasContent = Object.keys(settings).length > 0;
4185
4372
  if (hasContent) {
4186
4373
  await writeProjectSettings(root, settings);
4187
- log(`Stripped haus rules from ${path22.relative(root, settingsPath)} (user settings preserved).`);
4374
+ log(`Stripped haus rules from ${path23.relative(root, settingsPath)} (user settings preserved).`);
4188
4375
  return true;
4189
4376
  }
4190
- await fs17.remove(settingsPath);
4191
- log(`Removed ${path22.relative(root, settingsPath)} (no user-owned settings remained).`);
4377
+ await fs18.remove(settingsPath);
4378
+ log(`Removed ${path23.relative(root, settingsPath)} (no user-owned settings remained).`);
4192
4379
  return true;
4193
4380
  }
4194
4381
  async function stripRootClaudeMd(root) {
4195
- const filePath = path22.join(root, "CLAUDE.md");
4196
- if (!await fs17.pathExists(filePath)) return false;
4197
- const prev = await fs17.readFile(filePath, "utf8");
4382
+ const filePath = path23.join(root, "CLAUDE.md");
4383
+ if (!await fs18.pathExists(filePath)) return false;
4384
+ const prev = await fs18.readFile(filePath, "utf8");
4198
4385
  if (!prev.includes(BLOCK_BEGIN)) return false;
4199
4386
  const next = stripHausBlock(prev);
4200
4387
  if (next.length === 0) {
4201
- await fs17.remove(filePath);
4388
+ await fs18.remove(filePath);
4202
4389
  log("Removed CLAUDE.md (only contained haus import block).");
4203
4390
  } else {
4204
- await fs17.writeFile(filePath, next, "utf8");
4391
+ await fs18.writeFile(filePath, next, "utf8");
4205
4392
  log("Removed haus import block from CLAUDE.md (user content preserved).");
4206
4393
  }
4207
4394
  return true;
4208
4395
  }
4209
4396
  async function pruneDirIfEmpty(dir) {
4210
- if (!await fs17.pathExists(dir)) return;
4211
- const entries = await fs17.readdir(dir);
4212
- if (entries.length === 0) await fs17.remove(dir);
4397
+ if (!await fs18.pathExists(dir)) return;
4398
+ const entries = await fs18.readdir(dir);
4399
+ if (entries.length === 0) await fs18.remove(dir);
4213
4400
  }
4214
4401
  async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
4215
4402
  if (managedAbsPaths.length === 0) return;
4216
4403
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4217
4404
  const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
4218
4405
  for (const abs of managedAbsPaths) {
4219
- if (!await fs17.pathExists(abs)) continue;
4220
- const rel = path22.relative(root, abs);
4221
- const backupPath = path22.join(backupRoot, rel);
4222
- await fs17.ensureDir(path22.dirname(backupPath));
4223
- await fs17.copy(abs, backupPath);
4406
+ if (!await fs18.pathExists(abs)) continue;
4407
+ const rel = path23.relative(root, abs);
4408
+ const backupPath = path23.join(backupRoot, rel);
4409
+ await fs18.ensureDir(path23.dirname(backupPath));
4410
+ await fs18.copy(abs, backupPath);
4224
4411
  }
4225
- log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path22.relative(root, backupRoot)}.`);
4412
+ log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path23.relative(root, backupRoot)}.`);
4226
4413
  }
4227
4414
  async function runUndo(options) {
4228
4415
  const root = process.cwd();
@@ -4233,7 +4420,7 @@ async function runUndo(options) {
4233
4420
  log("Nothing to remove: no haus-managed files found in this directory.");
4234
4421
  return;
4235
4422
  }
4236
- const relTargets = managed.map((p) => path22.relative(root, p));
4423
+ const relTargets = managed.map((p) => path23.relative(root, p));
4237
4424
  const summaryParts = [...relTargets];
4238
4425
  if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
4239
4426
  if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
@@ -4250,9 +4437,9 @@ User-owned .claude/ files will be preserved.`
4250
4437
  }
4251
4438
  await backupManagedFilesBeforeUndo(root, managed);
4252
4439
  for (const abs of managed) {
4253
- if (!await fs17.pathExists(abs)) continue;
4254
- await fs17.remove(abs);
4255
- log(`Removed ${path22.relative(root, abs)}`);
4440
+ if (!await fs18.pathExists(abs)) continue;
4441
+ await fs18.remove(abs);
4442
+ log(`Removed ${path23.relative(root, abs)}`);
4256
4443
  }
4257
4444
  if (stripSettings) await stripProjectSettings(root);
4258
4445
  if (stripClaudeMd) await stripRootClaudeMd(root);
@@ -4263,8 +4450,8 @@ User-owned .claude/ files will be preserved.`
4263
4450
 
4264
4451
  // src/install/uninstall.ts
4265
4452
  import crypto3 from "node:crypto";
4266
- import path23 from "node:path";
4267
- import fs18 from "fs-extra";
4453
+ import path24 from "node:path";
4454
+ import fs19 from "fs-extra";
4268
4455
  async function runUninstall(options = {}) {
4269
4456
  const { force = false } = options;
4270
4457
  const manifest = await readManifest();
@@ -4274,7 +4461,7 @@ async function runUninstall(options = {}) {
4274
4461
  return result;
4275
4462
  }
4276
4463
  for (const entry of manifest.files) {
4277
- const exists = fs18.pathExistsSync(entry.destPath);
4464
+ const exists = fs19.pathExistsSync(entry.destPath);
4278
4465
  if (!exists) continue;
4279
4466
  const content2 = await readText(entry.destPath);
4280
4467
  if (content2 === void 0) continue;
@@ -4292,22 +4479,22 @@ async function runUninstall(options = {}) {
4292
4479
  result.skipped.push(entry.destPath);
4293
4480
  continue;
4294
4481
  }
4295
- await fs18.remove(entry.destPath);
4296
- await pruneEmptyDir(path23.dirname(entry.destPath));
4482
+ await fs19.remove(entry.destPath);
4483
+ await pruneEmptyDir(path24.dirname(entry.destPath));
4297
4484
  result.deleted.push(entry.destPath);
4298
4485
  }
4299
4486
  const settings = await readSettings();
4300
4487
  const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
4301
4488
  await writeSettings(stripped);
4302
4489
  result.hooksStripped = true;
4303
- const hausDir = path23.join(globalClaudeDir(), "haus");
4490
+ const hausDir = path24.join(globalClaudeDir(), "haus");
4304
4491
  const manifestPath2 = hausManifestPath();
4305
- if (fs18.pathExistsSync(manifestPath2)) {
4306
- await fs18.remove(manifestPath2);
4492
+ if (fs19.pathExistsSync(manifestPath2)) {
4493
+ await fs19.remove(manifestPath2);
4307
4494
  }
4308
- if (fs18.pathExistsSync(hausDir)) {
4309
- const remaining = await fs18.readdir(hausDir);
4310
- if (remaining.length === 0) await fs18.remove(hausDir);
4495
+ if (fs19.pathExistsSync(hausDir)) {
4496
+ const remaining = await fs19.readdir(hausDir);
4497
+ if (remaining.length === 0) await fs19.remove(hausDir);
4311
4498
  }
4312
4499
  return result;
4313
4500
  }
@@ -4338,12 +4525,12 @@ async function runUninstallCommand(options) {
4338
4525
  }
4339
4526
 
4340
4527
  // src/commands/update.ts
4341
- import path25 from "node:path";
4528
+ import path26 from "node:path";
4342
4529
 
4343
4530
  // src/claude/refresh-project.ts
4344
- import fs19 from "fs-extra";
4531
+ import fs20 from "fs-extra";
4345
4532
  async function isHausProject(root) {
4346
- if (await fs19.pathExists(hausPath(root, "recommendation.json"))) return true;
4533
+ if (await fs20.pathExists(hausPath(root, "recommendation.json"))) return true;
4347
4534
  const result = await readJsonDetailed(claudePath(root, "settings.json"));
4348
4535
  if (result.status === "ok") return result.value._haus != null;
4349
4536
  return false;
@@ -4378,7 +4565,7 @@ function summarizeLockDiff(before, after) {
4378
4565
 
4379
4566
  // src/update/lockfile.ts
4380
4567
  import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
4381
- import path24 from "node:path";
4568
+ import path25 from "node:path";
4382
4569
  async function checkLock(root) {
4383
4570
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
4384
4571
  const hasValidVersions = lock.every(
@@ -4409,7 +4596,7 @@ async function applyLock(root) {
4409
4596
  try {
4410
4597
  const backupDir = hausPath(root, "backups");
4411
4598
  await mkdir(backupDir, { recursive: true });
4412
- await copyFile(lockPath, path24.join(backupDir, `haus.lock.${Date.now()}.json`));
4599
+ await copyFile(lockPath, path25.join(backupDir, `haus.lock.${Date.now()}.json`));
4413
4600
  } catch {
4414
4601
  }
4415
4602
  const enriched = await Promise.all(
@@ -4431,7 +4618,7 @@ function diffLock(before, after) {
4431
4618
  }
4432
4619
  async function hasLocalOverrides(root) {
4433
4620
  try {
4434
- await readFile2(path24.join(root, ".claude", "settings.json"), "utf8");
4621
+ await readFile2(path25.join(root, ".claude", "settings.json"), "utf8");
4435
4622
  return true;
4436
4623
  } catch {
4437
4624
  return false;
@@ -4443,7 +4630,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
4443
4630
  async function runUpdate(options) {
4444
4631
  const root = process.cwd();
4445
4632
  if (options.check) {
4446
- const pkgJson2 = await readJson(path25.join(packageRoot(), "package.json"));
4633
+ const pkgJson2 = await readJson(path26.join(packageRoot(), "package.json"));
4447
4634
  const currentVersion2 = pkgJson2?.version ?? "0.0.0";
4448
4635
  const [status, npmVersion, latestCatalogTag, globalInstallDrift] = await Promise.all([
4449
4636
  checkLock(root),
@@ -4473,7 +4660,7 @@ async function runUpdate(options) {
4473
4660
  if (status.driftCount > 0) process.exitCode = 1;
4474
4661
  return;
4475
4662
  }
4476
- const pkgJson = await readJson(path25.join(packageRoot(), "package.json"));
4663
+ const pkgJson = await readJson(path26.join(packageRoot(), "package.json"));
4477
4664
  const currentVersion = pkgJson?.version ?? "0.0.0";
4478
4665
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
4479
4666
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -4550,16 +4737,16 @@ async function detectGlobalInstallDrift() {
4550
4737
  }
4551
4738
 
4552
4739
  // src/commands/validate-catalog.ts
4553
- import path27 from "node:path";
4740
+ import path28 from "node:path";
4554
4741
 
4555
4742
  // src/catalog/validate-core.ts
4556
- import fs20 from "node:fs";
4557
- import path26 from "node:path";
4743
+ import fs21 from "node:fs";
4744
+ import path27 from "node:path";
4558
4745
  var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
4559
4746
  var MANIFEST_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
4560
4747
  function isSafeCatalogPath2(itemPath) {
4561
- if (!itemPath || path26.isAbsolute(itemPath) || itemPath.includes("\\")) return false;
4562
- const normalized = path26.normalize(itemPath);
4748
+ if (!itemPath || path27.isAbsolute(itemPath) || itemPath.includes("\\")) return false;
4749
+ const normalized = path27.normalize(itemPath);
4563
4750
  return !normalized.startsWith("..") && !normalized.includes("/..");
4564
4751
  }
4565
4752
  function auditForbiddenStacks(items) {
@@ -4607,7 +4794,7 @@ function auditManifestStructure(manifestVersion, items) {
4607
4794
  } else {
4608
4795
  seenIds.set(item.id, i);
4609
4796
  }
4610
- if (item.type === "skill" || item.type === "agent" || item.type === "template" || item.type === "command") {
4797
+ if (item.type === "skill" || item.type === "agent" || item.type === "template" || item.type === "command" || item.type === "config") {
4611
4798
  if (!item.path) {
4612
4799
  failures.push(`${item.id}: missing path`);
4613
4800
  } else if (!isSafeCatalogPath2(item.path)) {
@@ -4638,8 +4825,8 @@ function checkRequiredFrontmatter(text, label) {
4638
4825
  return failures;
4639
4826
  }
4640
4827
  function auditTemplateContent(manifestDir, absPath, itemId) {
4641
- const rel = path26.relative(manifestDir, absPath);
4642
- const text = fs20.readFileSync(absPath, "utf8");
4828
+ const rel = path27.relative(manifestDir, absPath);
4829
+ const text = fs21.readFileSync(absPath, "utf8");
4643
4830
  const failures = [];
4644
4831
  const lines = text.split(/\r?\n/);
4645
4832
  for (let i = 0; i < lines.length; i++) {
@@ -4659,42 +4846,46 @@ function auditShippedFiles(manifestDir, items) {
4659
4846
  const failures = [];
4660
4847
  for (const item of items) {
4661
4848
  if (!item.path || !isSafeCatalogPath2(item.path)) continue;
4662
- const absPath = path26.join(manifestDir, item.path);
4849
+ const absPath = path27.join(manifestDir, item.path);
4663
4850
  if (item.type === "skill") {
4664
- const skillMd = path26.join(absPath, "SKILL.md");
4665
- if (!fs20.existsSync(skillMd)) {
4666
- failures.push(`${item.id}: missing ${path26.relative(manifestDir, skillMd)}`);
4851
+ const skillMd = path27.join(absPath, "SKILL.md");
4852
+ if (!fs21.existsSync(skillMd)) {
4853
+ failures.push(`${item.id}: missing ${path27.relative(manifestDir, skillMd)}`);
4667
4854
  continue;
4668
4855
  }
4669
- const text = fs20.readFileSync(skillMd, "utf8");
4856
+ const text = fs21.readFileSync(skillMd, "utf8");
4670
4857
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
4671
4858
  failures.push(
4672
- ...auditForbiddenTagsInText(text, `${item.id}: ${path26.relative(manifestDir, skillMd)}`)
4859
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path27.relative(manifestDir, skillMd)}`)
4673
4860
  );
4674
4861
  } else if (item.type === "agent") {
4675
- if (!fs20.existsSync(absPath)) {
4862
+ if (!fs21.existsSync(absPath)) {
4676
4863
  failures.push(`${item.id}: missing agent file ${item.path}`);
4677
4864
  continue;
4678
4865
  }
4679
- const text = fs20.readFileSync(absPath, "utf8");
4680
- const rel = path26.relative(manifestDir, absPath);
4866
+ const text = fs21.readFileSync(absPath, "utf8");
4867
+ const rel = path27.relative(manifestDir, absPath);
4681
4868
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
4682
4869
  failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
4683
4870
  } else if (item.type === "template") {
4684
- if (!fs20.existsSync(absPath)) {
4871
+ if (!fs21.existsSync(absPath)) {
4685
4872
  failures.push(`${item.id}: missing template file ${item.path}`);
4686
4873
  continue;
4687
4874
  }
4688
4875
  failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
4689
4876
  } else if (item.type === "command") {
4690
- if (!fs20.existsSync(absPath)) {
4877
+ if (!fs21.existsSync(absPath)) {
4691
4878
  failures.push(`${item.id}: missing command file ${item.path}`);
4692
4879
  continue;
4693
4880
  }
4694
- const text = fs20.readFileSync(absPath, "utf8");
4695
- const rel = path26.relative(manifestDir, absPath);
4881
+ const text = fs21.readFileSync(absPath, "utf8");
4882
+ const rel = path27.relative(manifestDir, absPath);
4696
4883
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
4697
4884
  failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
4885
+ } else if (item.type === "config") {
4886
+ if (!fs21.existsSync(absPath)) {
4887
+ failures.push(`${item.id}: missing config path ${item.path}`);
4888
+ }
4698
4889
  }
4699
4890
  }
4700
4891
  return failures;
@@ -4706,8 +4897,8 @@ var DIR_ITEM_TYPE = {
4706
4897
  commands: "command"
4707
4898
  };
4708
4899
  function walkMd(dir, fn) {
4709
- for (const entry of fs20.readdirSync(dir, { withFileTypes: true })) {
4710
- const full = path26.join(dir, entry.name);
4900
+ for (const entry of fs21.readdirSync(dir, { withFileTypes: true })) {
4901
+ const full = path27.join(dir, entry.name);
4711
4902
  if (entry.isDirectory()) walkMd(full, fn);
4712
4903
  else if (entry.name.endsWith(".md")) fn(full);
4713
4904
  }
@@ -4715,12 +4906,12 @@ function walkMd(dir, fn) {
4715
4906
  function auditMarkdownContent(manifestDir) {
4716
4907
  const failures = [];
4717
4908
  for (const dir of ["skills", "agents", "templates", "commands"]) {
4718
- const abs = path26.join(manifestDir, dir);
4719
- if (!fs20.existsSync(abs)) continue;
4909
+ const abs = path27.join(manifestDir, dir);
4910
+ if (!fs21.existsSync(abs)) continue;
4720
4911
  const checkNonTsxNpx = !NPX_TSX_ONLY_EXEMPT_TYPES.includes(DIR_ITEM_TYPE[dir] ?? "");
4721
4912
  walkMd(abs, (file) => {
4722
- const text = fs20.readFileSync(file, "utf8");
4723
- const rel = path26.relative(manifestDir, file);
4913
+ const text = fs21.readFileSync(file, "utf8");
4914
+ const rel = path27.relative(manifestDir, file);
4724
4915
  const lines = text.split(/\r?\n/);
4725
4916
  for (let i = 0; i < lines.length; i++) {
4726
4917
  const line2 = lines[i] ?? "";
@@ -4753,8 +4944,8 @@ async function runValidateCatalog(manifestPath2) {
4753
4944
  process.exitCode = 1;
4754
4945
  return;
4755
4946
  }
4756
- const abs = path27.resolve(process.cwd(), manifestPath2);
4757
- const manifestDir = path27.dirname(abs);
4947
+ const abs = path28.resolve(process.cwd(), manifestPath2);
4948
+ const manifestDir = path28.dirname(abs);
4758
4949
  const data = await readJson(abs);
4759
4950
  if (!data?.items) {
4760
4951
  error(`Could not read catalog manifest at ${abs}`);
@@ -4772,7 +4963,7 @@ async function runValidateCatalog(manifestPath2) {
4772
4963
 
4773
4964
  // src/commands/workspace.ts
4774
4965
  import { existsSync as existsSync5, statSync as statSync2 } from "node:fs";
4775
- import path34 from "node:path";
4966
+ import path35 from "node:path";
4776
4967
 
4777
4968
  // src/commands/workspace/aggregate.ts
4778
4969
  async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
@@ -4826,7 +5017,7 @@ ${summaries.map(
4826
5017
  }
4827
5018
 
4828
5019
  // src/commands/workspace/config.ts
4829
- import path28 from "node:path";
5020
+ import path29 from "node:path";
4830
5021
  import YAML from "yaml";
4831
5022
  var WORKSPACE_FILE = "haus.workspace.yaml";
4832
5023
  function parseWorkspaceConfig(text) {
@@ -4849,11 +5040,11 @@ function parseWorkspaceConfig(text) {
4849
5040
  };
4850
5041
  }
4851
5042
  async function readWorkspaceConfig(workspaceRoot) {
4852
- return parseWorkspaceConfig(await readText(path28.join(workspaceRoot, WORKSPACE_FILE)));
5043
+ return parseWorkspaceConfig(await readText(path29.join(workspaceRoot, WORKSPACE_FILE)));
4853
5044
  }
4854
5045
 
4855
5046
  // src/commands/workspace/discover.ts
4856
- import path29 from "node:path";
5047
+ import path30 from "node:path";
4857
5048
  import fg4 from "fast-glob";
4858
5049
  import YAML2 from "yaml";
4859
5050
  var DEFAULT_MAX_DEPTH = 3;
@@ -4881,8 +5072,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
4881
5072
  const gitDirs = /* @__PURE__ */ new Set();
4882
5073
  const manifestDirs = /* @__PURE__ */ new Set();
4883
5074
  for (const match of matches) {
4884
- const base = path29.posix.basename(match);
4885
- const dir = path29.posix.dirname(match);
5075
+ const base = path30.posix.basename(match);
5076
+ const dir = path30.posix.dirname(match);
4886
5077
  const owner = dir === "." ? "." : dir;
4887
5078
  if (base === ".git") gitDirs.add(owner);
4888
5079
  else manifestDirs.add(owner);
@@ -4898,9 +5089,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
4898
5089
  }
4899
5090
  repoRoots.sort((a, b) => a.localeCompare(b));
4900
5091
  return mapWithConcurrency(repoRoots, async (relDir) => {
4901
- const absDir = path29.resolve(workspaceRoot, relDir);
4902
- const pkg = await readJson(path29.join(absDir, "package.json"));
4903
- const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path29.basename(relDir === "." ? workspaceRoot : absDir);
5092
+ const absDir = path30.resolve(workspaceRoot, relDir);
5093
+ const pkg = await readJson(path30.join(absDir, "package.json"));
5094
+ const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path30.basename(relDir === "." ? workspaceRoot : absDir);
4904
5095
  let role = "auto";
4905
5096
  try {
4906
5097
  const scan = await scanProject(absDir);
@@ -4943,7 +5134,7 @@ function renderWorkspaceYaml(config) {
4943
5134
  });
4944
5135
  }
4945
5136
  async function runDiscover(workspaceRoot, opts = {}) {
4946
- const yamlPath = path29.join(workspaceRoot, "haus.workspace.yaml");
5137
+ const yamlPath = path30.join(workspaceRoot, "haus.workspace.yaml");
4947
5138
  const existingText = await readText(yamlPath);
4948
5139
  const existing = parseWorkspaceConfig(existingText);
4949
5140
  if (existingText && !existing) {
@@ -4977,18 +5168,18 @@ async function runDiscover(workspaceRoot, opts = {}) {
4977
5168
 
4978
5169
  // src/commands/workspace/doctor.ts
4979
5170
  import { existsSync as existsSync3 } from "node:fs";
4980
- import path31 from "node:path";
5171
+ import path32 from "node:path";
4981
5172
 
4982
5173
  // src/commands/workspace/manifest.ts
4983
5174
  import { readFileSync as readFileSync3 } from "node:fs";
4984
- import path30 from "node:path";
5175
+ import path31 from "node:path";
4985
5176
  var MANIFEST_FILE = "workspace.manifest.json";
4986
5177
  function manifestPath(workspaceRoot) {
4987
5178
  return hausPath(workspaceRoot, MANIFEST_FILE);
4988
5179
  }
4989
5180
  function hausVersion() {
4990
5181
  try {
4991
- const pkg = JSON.parse(readFileSync3(path30.join(packageRoot(), "package.json"), "utf8"));
5182
+ const pkg = JSON.parse(readFileSync3(path31.join(packageRoot(), "package.json"), "utf8"));
4992
5183
  return pkg.version ?? "0.0.0";
4993
5184
  } catch {
4994
5185
  return "0.0.0";
@@ -5054,7 +5245,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
5054
5245
  }
5055
5246
  const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
5056
5247
  for (const repo of config.repos) {
5057
- const repoRoot = path31.resolve(workspaceRoot, repo.path);
5248
+ const repoRoot = path32.resolve(workspaceRoot, repo.path);
5058
5249
  const entry = manifestByName.get(repo.name);
5059
5250
  if (!entry) {
5060
5251
  flag({
@@ -5128,11 +5319,11 @@ function emit(args) {
5128
5319
 
5129
5320
  // src/commands/workspace/setup.ts
5130
5321
  import { existsSync as existsSync4, statSync } from "node:fs";
5131
- import path33 from "node:path";
5322
+ import path34 from "node:path";
5132
5323
 
5133
5324
  // src/claude/write-workspace-claude-md.ts
5134
- import path32 from "node:path";
5135
- import fs21 from "fs-extra";
5325
+ import path33 from "node:path";
5326
+ import fs22 from "fs-extra";
5136
5327
  function buildWorkspaceImportBlock(client, members) {
5137
5328
  const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
5138
5329
  const body = [
@@ -5150,8 +5341,8 @@ ${BLOCK_END}`;
5150
5341
  async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
5151
5342
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
5152
5343
  const dryRun = opts.dryRun ?? false;
5153
- const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path32.join(workspaceRoot, "CLAUDE.md");
5154
- const prev = await fs21.pathExists(filePath) ? await fs21.readFile(filePath, "utf8") : "";
5344
+ const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path33.join(workspaceRoot, "CLAUDE.md");
5345
+ const prev = await fs22.pathExists(filePath) ? await fs22.readFile(filePath, "utf8") : "";
5155
5346
  const next = opts.collision ? `${block}
5156
5347
  ` : injectHausBlock(prev, block);
5157
5348
  const printable = displayPath(workspaceRoot, filePath);
@@ -5176,20 +5367,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
5176
5367
 
5177
5368
  // src/commands/workspace/setup.ts
5178
5369
  function resolveWorkspaceRoot(start = process.cwd()) {
5179
- let dir = path33.resolve(start);
5370
+ let dir = path34.resolve(start);
5180
5371
  for (; ; ) {
5181
- if (existsSync4(path33.join(dir, WORKSPACE_FILE))) return dir;
5182
- const parent = path33.dirname(dir);
5183
- if (parent === dir) return path33.resolve(start);
5372
+ if (existsSync4(path34.join(dir, WORKSPACE_FILE))) return dir;
5373
+ const parent = path34.dirname(dir);
5374
+ if (parent === dir) return path34.resolve(start);
5184
5375
  dir = parent;
5185
5376
  }
5186
5377
  }
5187
5378
  function isRootRepo(workspaceRoot, repoPath) {
5188
- return path33.resolve(workspaceRoot, repoPath) === path33.resolve(workspaceRoot);
5379
+ return path34.resolve(workspaceRoot, repoPath) === path34.resolve(workspaceRoot);
5189
5380
  }
5190
5381
  async function runWorkspaceSetup(workspaceRoot, options = {}) {
5191
5382
  const apply = options.write ?? false;
5192
- const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
5383
+ const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
5193
5384
  if (!configText) {
5194
5385
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
5195
5386
  process.exitCode = 1;
@@ -5206,7 +5397,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
5206
5397
  const statuses = [];
5207
5398
  const aggregateInputs = [];
5208
5399
  for (const repo of repos) {
5209
- const repoRoot = path33.resolve(workspaceRoot, repo.path);
5400
+ const repoRoot = path34.resolve(workspaceRoot, repo.path);
5210
5401
  log(`
5211
5402
  \u2192 ${repo.name} (${repo.path})`);
5212
5403
  try {
@@ -5274,7 +5465,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
5274
5465
  const status = statusByName.get(repo.name);
5275
5466
  const role = repo.role ?? status?.roles?.[0] ?? "auto";
5276
5467
  if (status?.status === "ok") {
5277
- const lock = await checkLock(path33.resolve(workspaceRoot, repo.path));
5468
+ const lock = await checkLock(path34.resolve(workspaceRoot, repo.path));
5278
5469
  manifestRepos.push({
5279
5470
  name: repo.name,
5280
5471
  path: repo.path,
@@ -5354,7 +5545,7 @@ relationships: []
5354
5545
  log("Workspace initialized.");
5355
5546
  }
5356
5547
  async function scanWorkspace(workspaceRoot, opts) {
5357
- const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
5548
+ const configText = await readText(path35.join(workspaceRoot, WORKSPACE_FILE));
5358
5549
  if (!configText) {
5359
5550
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
5360
5551
  process.exitCode = 1;
@@ -5375,7 +5566,7 @@ async function scanWorkspace(workspaceRoot, opts) {
5375
5566
  }
5376
5567
  const inputs = [];
5377
5568
  for (const repo of config.repos) {
5378
- const repoRoot = path34.resolve(workspaceRoot, repo.path);
5569
+ const repoRoot = path35.resolve(workspaceRoot, repo.path);
5379
5570
  if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
5380
5571
  throw new Error(`Repo path is not a directory: ${repo.path}`);
5381
5572
  }
@@ -5425,7 +5616,7 @@ async function runWorkspace(action, options = {}) {
5425
5616
  // src/cli.ts
5426
5617
  function cliVersion() {
5427
5618
  try {
5428
- const pkgPath = path35.join(packageRoot(), "package.json");
5619
+ const pkgPath = path36.join(packageRoot(), "package.json");
5429
5620
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5430
5621
  return pkg.version ?? "0.0.0";
5431
5622
  } catch {
@@ -5435,7 +5626,7 @@ function cliVersion() {
5435
5626
  var program = new Command();
5436
5627
  function validateRuntimeNodeVersion() {
5437
5628
  try {
5438
- const pkgPath = path35.join(packageRoot(), "package.json");
5629
+ const pkgPath = path36.join(packageRoot(), "package.json");
5439
5630
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5440
5631
  const requiredRange = pkg.engines?.node;
5441
5632
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
@@ -5471,6 +5662,9 @@ program.command("catalog-audit").action(runCatalogAudit);
5471
5662
  program.command("validate-catalog").argument("[manifest]").action(runValidateCatalog);
5472
5663
  program.command("update").option("--check").action(runUpdate);
5473
5664
  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);
5665
+ program.command("scaffold [ids...]").description(
5666
+ "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."
5667
+ ).option("--force", "Overwrite existing files").option("--dry-run", "Preview what would be written without changing anything").option("--root <path>", "Project root (defaults to cwd)", process.cwd()).action(runScaffold);
5474
5668
  program.command("uninstall").option("--force").action(runUninstallCommand);
5475
5669
  var guard = program.command("guard");
5476
5670
  guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));