@haus-tech/haus-workflow 0.28.1 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.29.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.28.1...v0.29.0) (2026-06-17)
4
+
5
+ ### Features
6
+
7
+ - add haus scaffold command for config file distribution ([#124](https://github.com/WeAreHausTech/haus-workflow/issues/124)) ([092a343](https://github.com/WeAreHausTech/haus-workflow/commit/092a3433f9bcab69877dab902b5506d8b01dd5a8))
8
+
3
9
  ## [0.28.1](https://github.com/WeAreHausTech/haus-workflow/compare/v0.28.0...v0.28.1) (2026-06-17)
4
10
 
5
11
  ## [0.28.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.27.1...v0.28.0) (2026-06-17)
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
@@ -637,7 +637,7 @@ function safeJoin(base, itemPath) {
637
637
  const resolved = path2.resolve(base, itemPath);
638
638
  return resolved.startsWith(base + path2.sep) || resolved === base ? resolved : null;
639
639
  }
640
- var KNOWN_ITEM_TYPES = /* @__PURE__ */ new Set(["skill", "agent", "template", "command"]);
640
+ var KNOWN_ITEM_TYPES = /* @__PURE__ */ new Set(["skill", "agent", "template", "command", "config"]);
641
641
  function isMarkdownPath(rel) {
642
642
  return rel.toLowerCase().endsWith(".md");
643
643
  }
@@ -782,12 +782,13 @@ async function writeFetchedDirectory(destDir, fetched) {
782
782
  }
783
783
  }
784
784
  async function syncDirectoryFromPrefix(item, catalogPrefix, destDir, base, opts) {
785
- const relFiles = await listFilesUnderCatalogPrefix(catalogPrefix, base);
785
+ const relFiles = opts.relFiles ?? await listFilesUnderCatalogPrefix(catalogPrefix, base);
786
786
  if (!relFiles) {
787
787
  warn(`Failed to list files for ${item.id}`);
788
788
  return "failed";
789
789
  }
790
- 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) {
791
792
  warn(`Failed to fetch content for ${item.id}: missing SKILL.md`);
792
793
  return "failed";
793
794
  }
@@ -833,6 +834,42 @@ async function syncSkillDirectory(item, base) {
833
834
  return "failed";
834
835
  }
835
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
+ }
836
873
  async function syncSuperpowersShared(base) {
837
874
  const relFiles = await listFilesUnderCatalogPrefix(SUPERPOWERS_SHARED_CATALOG_REL, base);
838
875
  if (!relFiles || relFiles.length === 0) return "skipped";
@@ -866,6 +903,9 @@ async function syncOneItem(item, base) {
866
903
  if (item.type === "skill") {
867
904
  return syncSkillDirectory(item, base);
868
905
  }
906
+ if (item.type === "config") {
907
+ return syncConfigItem(item, base);
908
+ }
869
909
  const dest = safeJoin(getCacheDir(), item.path);
870
910
  if (!dest) {
871
911
  warn(`Skipping ${item.id}: path traversal detected`);
@@ -1593,8 +1633,8 @@ function buildDenyRules() {
1593
1633
  for (const command of DENY_COMMANDS) {
1594
1634
  rules.push(`Bash(${command}:*)`);
1595
1635
  }
1596
- for (const path36 of DENY_PATHS) {
1597
- const pattern = DENY_DIRS.has(path36) ? `${path36}/**` : path36;
1636
+ for (const path37 of DENY_PATHS) {
1637
+ const pattern = DENY_DIRS.has(path37) ? `${path37}/**` : path37;
1598
1638
  for (const tool of FILE_TOOLS) {
1599
1639
  rules.push(`${tool}(${pattern})`);
1600
1640
  }
@@ -2202,6 +2242,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
2202
2242
  continue;
2203
2243
  }
2204
2244
  }
2245
+ if (item.type === "config") continue;
2205
2246
  const sourcePath = catalogItemContentPath(contentRoot, manifestItem);
2206
2247
  const target = targetDirForType(item.type);
2207
2248
  if (!target) {
@@ -3502,6 +3543,15 @@ async function recommend(root, context) {
3502
3543
  skip(item.id, "unsupported-policy", "Unsupported stack policy");
3503
3544
  continue;
3504
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
+ }
3505
3555
  if (item.reviewStatus === "deprecated") {
3506
3556
  skip(item.id, "deprecated", "Catalog item is deprecated", "reviewStatus:deprecated");
3507
3557
  continue;
@@ -4123,6 +4173,144 @@ async function runRefresh() {
4123
4173
  log(`Recommended items: ${recommendation.recommended.length}`);
4124
4174
  }
4125
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
+
4126
4314
  // src/commands/scan.ts
4127
4315
  async function runScan(options) {
4128
4316
  const result = await scanProject(process.cwd());
@@ -4136,8 +4324,8 @@ async function runScan(options) {
4136
4324
  }
4137
4325
 
4138
4326
  // src/commands/undo.ts
4139
- import path22 from "node:path";
4140
- import fs17 from "fs-extra";
4327
+ import path23 from "node:path";
4328
+ import fs18 from "fs-extra";
4141
4329
 
4142
4330
  // src/claude/managed-paths.ts
4143
4331
  var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md", "commands/haus-doctor.md"];
@@ -4154,74 +4342,74 @@ async function collectManagedPaths(root) {
4154
4342
  const lock = await readJson(hausPath(root, "haus.lock.json"));
4155
4343
  for (const row of lock ?? []) {
4156
4344
  for (const rel of row.paths ?? []) {
4157
- paths.add(path22.resolve(root, rel));
4345
+ paths.add(path23.resolve(root, rel));
4158
4346
  }
4159
4347
  }
4160
4348
  const existing = [];
4161
4349
  for (const abs of paths) {
4162
- if (await fs17.pathExists(abs)) existing.push(abs);
4350
+ if (await fs18.pathExists(abs)) existing.push(abs);
4163
4351
  }
4164
4352
  return existing;
4165
4353
  }
4166
4354
  async function settingsHasHausContent(root) {
4167
4355
  const settingsPath = claudePath(root, "settings.json");
4168
- if (!await fs17.pathExists(settingsPath)) return false;
4356
+ if (!await fs18.pathExists(settingsPath)) return false;
4169
4357
  const settings = await readProjectSettings(root);
4170
4358
  return settings._haus != null;
4171
4359
  }
4172
4360
  async function claudeMdHasHausBlock(root) {
4173
- const filePath = path22.join(root, "CLAUDE.md");
4174
- if (!await fs17.pathExists(filePath)) return false;
4175
- 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");
4176
4364
  return text.includes(BLOCK_BEGIN);
4177
4365
  }
4178
4366
  async function stripProjectSettings(root) {
4179
4367
  const settingsPath = claudePath(root, "settings.json");
4180
- if (!await fs17.pathExists(settingsPath)) return false;
4368
+ if (!await fs18.pathExists(settingsPath)) return false;
4181
4369
  let settings = await readProjectSettings(root);
4182
4370
  settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
4183
4371
  const hasContent = Object.keys(settings).length > 0;
4184
4372
  if (hasContent) {
4185
4373
  await writeProjectSettings(root, settings);
4186
- 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).`);
4187
4375
  return true;
4188
4376
  }
4189
- await fs17.remove(settingsPath);
4190
- 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).`);
4191
4379
  return true;
4192
4380
  }
4193
4381
  async function stripRootClaudeMd(root) {
4194
- const filePath = path22.join(root, "CLAUDE.md");
4195
- if (!await fs17.pathExists(filePath)) return false;
4196
- 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");
4197
4385
  if (!prev.includes(BLOCK_BEGIN)) return false;
4198
4386
  const next = stripHausBlock(prev);
4199
4387
  if (next.length === 0) {
4200
- await fs17.remove(filePath);
4388
+ await fs18.remove(filePath);
4201
4389
  log("Removed CLAUDE.md (only contained haus import block).");
4202
4390
  } else {
4203
- await fs17.writeFile(filePath, next, "utf8");
4391
+ await fs18.writeFile(filePath, next, "utf8");
4204
4392
  log("Removed haus import block from CLAUDE.md (user content preserved).");
4205
4393
  }
4206
4394
  return true;
4207
4395
  }
4208
4396
  async function pruneDirIfEmpty(dir) {
4209
- if (!await fs17.pathExists(dir)) return;
4210
- const entries = await fs17.readdir(dir);
4211
- 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);
4212
4400
  }
4213
4401
  async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
4214
4402
  if (managedAbsPaths.length === 0) return;
4215
4403
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4216
4404
  const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
4217
4405
  for (const abs of managedAbsPaths) {
4218
- if (!await fs17.pathExists(abs)) continue;
4219
- const rel = path22.relative(root, abs);
4220
- const backupPath = path22.join(backupRoot, rel);
4221
- await fs17.ensureDir(path22.dirname(backupPath));
4222
- 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);
4223
4411
  }
4224
- 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)}.`);
4225
4413
  }
4226
4414
  async function runUndo(options) {
4227
4415
  const root = process.cwd();
@@ -4232,7 +4420,7 @@ async function runUndo(options) {
4232
4420
  log("Nothing to remove: no haus-managed files found in this directory.");
4233
4421
  return;
4234
4422
  }
4235
- const relTargets = managed.map((p) => path22.relative(root, p));
4423
+ const relTargets = managed.map((p) => path23.relative(root, p));
4236
4424
  const summaryParts = [...relTargets];
4237
4425
  if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
4238
4426
  if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
@@ -4249,9 +4437,9 @@ User-owned .claude/ files will be preserved.`
4249
4437
  }
4250
4438
  await backupManagedFilesBeforeUndo(root, managed);
4251
4439
  for (const abs of managed) {
4252
- if (!await fs17.pathExists(abs)) continue;
4253
- await fs17.remove(abs);
4254
- 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)}`);
4255
4443
  }
4256
4444
  if (stripSettings) await stripProjectSettings(root);
4257
4445
  if (stripClaudeMd) await stripRootClaudeMd(root);
@@ -4262,8 +4450,8 @@ User-owned .claude/ files will be preserved.`
4262
4450
 
4263
4451
  // src/install/uninstall.ts
4264
4452
  import crypto3 from "node:crypto";
4265
- import path23 from "node:path";
4266
- import fs18 from "fs-extra";
4453
+ import path24 from "node:path";
4454
+ import fs19 from "fs-extra";
4267
4455
  async function runUninstall(options = {}) {
4268
4456
  const { force = false } = options;
4269
4457
  const manifest = await readManifest();
@@ -4273,7 +4461,7 @@ async function runUninstall(options = {}) {
4273
4461
  return result;
4274
4462
  }
4275
4463
  for (const entry of manifest.files) {
4276
- const exists = fs18.pathExistsSync(entry.destPath);
4464
+ const exists = fs19.pathExistsSync(entry.destPath);
4277
4465
  if (!exists) continue;
4278
4466
  const content2 = await readText(entry.destPath);
4279
4467
  if (content2 === void 0) continue;
@@ -4291,22 +4479,22 @@ async function runUninstall(options = {}) {
4291
4479
  result.skipped.push(entry.destPath);
4292
4480
  continue;
4293
4481
  }
4294
- await fs18.remove(entry.destPath);
4295
- await pruneEmptyDir(path23.dirname(entry.destPath));
4482
+ await fs19.remove(entry.destPath);
4483
+ await pruneEmptyDir(path24.dirname(entry.destPath));
4296
4484
  result.deleted.push(entry.destPath);
4297
4485
  }
4298
4486
  const settings = await readSettings();
4299
4487
  const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
4300
4488
  await writeSettings(stripped);
4301
4489
  result.hooksStripped = true;
4302
- const hausDir = path23.join(globalClaudeDir(), "haus");
4490
+ const hausDir = path24.join(globalClaudeDir(), "haus");
4303
4491
  const manifestPath2 = hausManifestPath();
4304
- if (fs18.pathExistsSync(manifestPath2)) {
4305
- await fs18.remove(manifestPath2);
4492
+ if (fs19.pathExistsSync(manifestPath2)) {
4493
+ await fs19.remove(manifestPath2);
4306
4494
  }
4307
- if (fs18.pathExistsSync(hausDir)) {
4308
- const remaining = await fs18.readdir(hausDir);
4309
- 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);
4310
4498
  }
4311
4499
  return result;
4312
4500
  }
@@ -4337,12 +4525,12 @@ async function runUninstallCommand(options) {
4337
4525
  }
4338
4526
 
4339
4527
  // src/commands/update.ts
4340
- import path25 from "node:path";
4528
+ import path26 from "node:path";
4341
4529
 
4342
4530
  // src/claude/refresh-project.ts
4343
- import fs19 from "fs-extra";
4531
+ import fs20 from "fs-extra";
4344
4532
  async function isHausProject(root) {
4345
- if (await fs19.pathExists(hausPath(root, "recommendation.json"))) return true;
4533
+ if (await fs20.pathExists(hausPath(root, "recommendation.json"))) return true;
4346
4534
  const result = await readJsonDetailed(claudePath(root, "settings.json"));
4347
4535
  if (result.status === "ok") return result.value._haus != null;
4348
4536
  return false;
@@ -4377,7 +4565,7 @@ function summarizeLockDiff(before, after) {
4377
4565
 
4378
4566
  // src/update/lockfile.ts
4379
4567
  import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
4380
- import path24 from "node:path";
4568
+ import path25 from "node:path";
4381
4569
  async function checkLock(root) {
4382
4570
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
4383
4571
  const hasValidVersions = lock.every(
@@ -4408,7 +4596,7 @@ async function applyLock(root) {
4408
4596
  try {
4409
4597
  const backupDir = hausPath(root, "backups");
4410
4598
  await mkdir(backupDir, { recursive: true });
4411
- await copyFile(lockPath, path24.join(backupDir, `haus.lock.${Date.now()}.json`));
4599
+ await copyFile(lockPath, path25.join(backupDir, `haus.lock.${Date.now()}.json`));
4412
4600
  } catch {
4413
4601
  }
4414
4602
  const enriched = await Promise.all(
@@ -4430,7 +4618,7 @@ function diffLock(before, after) {
4430
4618
  }
4431
4619
  async function hasLocalOverrides(root) {
4432
4620
  try {
4433
- await readFile2(path24.join(root, ".claude", "settings.json"), "utf8");
4621
+ await readFile2(path25.join(root, ".claude", "settings.json"), "utf8");
4434
4622
  return true;
4435
4623
  } catch {
4436
4624
  return false;
@@ -4442,7 +4630,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
4442
4630
  async function runUpdate(options) {
4443
4631
  const root = process.cwd();
4444
4632
  if (options.check) {
4445
- const pkgJson2 = await readJson(path25.join(packageRoot(), "package.json"));
4633
+ const pkgJson2 = await readJson(path26.join(packageRoot(), "package.json"));
4446
4634
  const currentVersion2 = pkgJson2?.version ?? "0.0.0";
4447
4635
  const [status, npmVersion, latestCatalogTag, globalInstallDrift] = await Promise.all([
4448
4636
  checkLock(root),
@@ -4472,7 +4660,7 @@ async function runUpdate(options) {
4472
4660
  if (status.driftCount > 0) process.exitCode = 1;
4473
4661
  return;
4474
4662
  }
4475
- const pkgJson = await readJson(path25.join(packageRoot(), "package.json"));
4663
+ const pkgJson = await readJson(path26.join(packageRoot(), "package.json"));
4476
4664
  const currentVersion = pkgJson?.version ?? "0.0.0";
4477
4665
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
4478
4666
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -4549,16 +4737,16 @@ async function detectGlobalInstallDrift() {
4549
4737
  }
4550
4738
 
4551
4739
  // src/commands/validate-catalog.ts
4552
- import path27 from "node:path";
4740
+ import path28 from "node:path";
4553
4741
 
4554
4742
  // src/catalog/validate-core.ts
4555
- import fs20 from "node:fs";
4556
- import path26 from "node:path";
4743
+ import fs21 from "node:fs";
4744
+ import path27 from "node:path";
4557
4745
  var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
4558
4746
  var MANIFEST_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
4559
4747
  function isSafeCatalogPath2(itemPath) {
4560
- if (!itemPath || path26.isAbsolute(itemPath) || itemPath.includes("\\")) return false;
4561
- const normalized = path26.normalize(itemPath);
4748
+ if (!itemPath || path27.isAbsolute(itemPath) || itemPath.includes("\\")) return false;
4749
+ const normalized = path27.normalize(itemPath);
4562
4750
  return !normalized.startsWith("..") && !normalized.includes("/..");
4563
4751
  }
4564
4752
  function auditForbiddenStacks(items) {
@@ -4606,7 +4794,7 @@ function auditManifestStructure(manifestVersion, items) {
4606
4794
  } else {
4607
4795
  seenIds.set(item.id, i);
4608
4796
  }
4609
- 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") {
4610
4798
  if (!item.path) {
4611
4799
  failures.push(`${item.id}: missing path`);
4612
4800
  } else if (!isSafeCatalogPath2(item.path)) {
@@ -4637,8 +4825,8 @@ function checkRequiredFrontmatter(text, label) {
4637
4825
  return failures;
4638
4826
  }
4639
4827
  function auditTemplateContent(manifestDir, absPath, itemId) {
4640
- const rel = path26.relative(manifestDir, absPath);
4641
- const text = fs20.readFileSync(absPath, "utf8");
4828
+ const rel = path27.relative(manifestDir, absPath);
4829
+ const text = fs21.readFileSync(absPath, "utf8");
4642
4830
  const failures = [];
4643
4831
  const lines = text.split(/\r?\n/);
4644
4832
  for (let i = 0; i < lines.length; i++) {
@@ -4658,42 +4846,46 @@ function auditShippedFiles(manifestDir, items) {
4658
4846
  const failures = [];
4659
4847
  for (const item of items) {
4660
4848
  if (!item.path || !isSafeCatalogPath2(item.path)) continue;
4661
- const absPath = path26.join(manifestDir, item.path);
4849
+ const absPath = path27.join(manifestDir, item.path);
4662
4850
  if (item.type === "skill") {
4663
- const skillMd = path26.join(absPath, "SKILL.md");
4664
- if (!fs20.existsSync(skillMd)) {
4665
- 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)}`);
4666
4854
  continue;
4667
4855
  }
4668
- const text = fs20.readFileSync(skillMd, "utf8");
4856
+ const text = fs21.readFileSync(skillMd, "utf8");
4669
4857
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
4670
4858
  failures.push(
4671
- ...auditForbiddenTagsInText(text, `${item.id}: ${path26.relative(manifestDir, skillMd)}`)
4859
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path27.relative(manifestDir, skillMd)}`)
4672
4860
  );
4673
4861
  } else if (item.type === "agent") {
4674
- if (!fs20.existsSync(absPath)) {
4862
+ if (!fs21.existsSync(absPath)) {
4675
4863
  failures.push(`${item.id}: missing agent file ${item.path}`);
4676
4864
  continue;
4677
4865
  }
4678
- const text = fs20.readFileSync(absPath, "utf8");
4679
- const rel = path26.relative(manifestDir, absPath);
4866
+ const text = fs21.readFileSync(absPath, "utf8");
4867
+ const rel = path27.relative(manifestDir, absPath);
4680
4868
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
4681
4869
  failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
4682
4870
  } else if (item.type === "template") {
4683
- if (!fs20.existsSync(absPath)) {
4871
+ if (!fs21.existsSync(absPath)) {
4684
4872
  failures.push(`${item.id}: missing template file ${item.path}`);
4685
4873
  continue;
4686
4874
  }
4687
4875
  failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
4688
4876
  } else if (item.type === "command") {
4689
- if (!fs20.existsSync(absPath)) {
4877
+ if (!fs21.existsSync(absPath)) {
4690
4878
  failures.push(`${item.id}: missing command file ${item.path}`);
4691
4879
  continue;
4692
4880
  }
4693
- const text = fs20.readFileSync(absPath, "utf8");
4694
- const rel = path26.relative(manifestDir, absPath);
4881
+ const text = fs21.readFileSync(absPath, "utf8");
4882
+ const rel = path27.relative(manifestDir, absPath);
4695
4883
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
4696
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
+ }
4697
4889
  }
4698
4890
  }
4699
4891
  return failures;
@@ -4705,8 +4897,8 @@ var DIR_ITEM_TYPE = {
4705
4897
  commands: "command"
4706
4898
  };
4707
4899
  function walkMd(dir, fn) {
4708
- for (const entry of fs20.readdirSync(dir, { withFileTypes: true })) {
4709
- 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);
4710
4902
  if (entry.isDirectory()) walkMd(full, fn);
4711
4903
  else if (entry.name.endsWith(".md")) fn(full);
4712
4904
  }
@@ -4714,12 +4906,12 @@ function walkMd(dir, fn) {
4714
4906
  function auditMarkdownContent(manifestDir) {
4715
4907
  const failures = [];
4716
4908
  for (const dir of ["skills", "agents", "templates", "commands"]) {
4717
- const abs = path26.join(manifestDir, dir);
4718
- if (!fs20.existsSync(abs)) continue;
4909
+ const abs = path27.join(manifestDir, dir);
4910
+ if (!fs21.existsSync(abs)) continue;
4719
4911
  const checkNonTsxNpx = !NPX_TSX_ONLY_EXEMPT_TYPES.includes(DIR_ITEM_TYPE[dir] ?? "");
4720
4912
  walkMd(abs, (file) => {
4721
- const text = fs20.readFileSync(file, "utf8");
4722
- const rel = path26.relative(manifestDir, file);
4913
+ const text = fs21.readFileSync(file, "utf8");
4914
+ const rel = path27.relative(manifestDir, file);
4723
4915
  const lines = text.split(/\r?\n/);
4724
4916
  for (let i = 0; i < lines.length; i++) {
4725
4917
  const line2 = lines[i] ?? "";
@@ -4752,8 +4944,8 @@ async function runValidateCatalog(manifestPath2) {
4752
4944
  process.exitCode = 1;
4753
4945
  return;
4754
4946
  }
4755
- const abs = path27.resolve(process.cwd(), manifestPath2);
4756
- const manifestDir = path27.dirname(abs);
4947
+ const abs = path28.resolve(process.cwd(), manifestPath2);
4948
+ const manifestDir = path28.dirname(abs);
4757
4949
  const data = await readJson(abs);
4758
4950
  if (!data?.items) {
4759
4951
  error(`Could not read catalog manifest at ${abs}`);
@@ -4771,7 +4963,7 @@ async function runValidateCatalog(manifestPath2) {
4771
4963
 
4772
4964
  // src/commands/workspace.ts
4773
4965
  import { existsSync as existsSync5, statSync as statSync2 } from "node:fs";
4774
- import path34 from "node:path";
4966
+ import path35 from "node:path";
4775
4967
 
4776
4968
  // src/commands/workspace/aggregate.ts
4777
4969
  async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
@@ -4825,7 +5017,7 @@ ${summaries.map(
4825
5017
  }
4826
5018
 
4827
5019
  // src/commands/workspace/config.ts
4828
- import path28 from "node:path";
5020
+ import path29 from "node:path";
4829
5021
  import YAML from "yaml";
4830
5022
  var WORKSPACE_FILE = "haus.workspace.yaml";
4831
5023
  function parseWorkspaceConfig(text) {
@@ -4848,11 +5040,11 @@ function parseWorkspaceConfig(text) {
4848
5040
  };
4849
5041
  }
4850
5042
  async function readWorkspaceConfig(workspaceRoot) {
4851
- return parseWorkspaceConfig(await readText(path28.join(workspaceRoot, WORKSPACE_FILE)));
5043
+ return parseWorkspaceConfig(await readText(path29.join(workspaceRoot, WORKSPACE_FILE)));
4852
5044
  }
4853
5045
 
4854
5046
  // src/commands/workspace/discover.ts
4855
- import path29 from "node:path";
5047
+ import path30 from "node:path";
4856
5048
  import fg4 from "fast-glob";
4857
5049
  import YAML2 from "yaml";
4858
5050
  var DEFAULT_MAX_DEPTH = 3;
@@ -4880,8 +5072,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
4880
5072
  const gitDirs = /* @__PURE__ */ new Set();
4881
5073
  const manifestDirs = /* @__PURE__ */ new Set();
4882
5074
  for (const match of matches) {
4883
- const base = path29.posix.basename(match);
4884
- const dir = path29.posix.dirname(match);
5075
+ const base = path30.posix.basename(match);
5076
+ const dir = path30.posix.dirname(match);
4885
5077
  const owner = dir === "." ? "." : dir;
4886
5078
  if (base === ".git") gitDirs.add(owner);
4887
5079
  else manifestDirs.add(owner);
@@ -4897,9 +5089,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
4897
5089
  }
4898
5090
  repoRoots.sort((a, b) => a.localeCompare(b));
4899
5091
  return mapWithConcurrency(repoRoots, async (relDir) => {
4900
- const absDir = path29.resolve(workspaceRoot, relDir);
4901
- const pkg = await readJson(path29.join(absDir, "package.json"));
4902
- 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);
4903
5095
  let role = "auto";
4904
5096
  try {
4905
5097
  const scan = await scanProject(absDir);
@@ -4942,7 +5134,7 @@ function renderWorkspaceYaml(config) {
4942
5134
  });
4943
5135
  }
4944
5136
  async function runDiscover(workspaceRoot, opts = {}) {
4945
- const yamlPath = path29.join(workspaceRoot, "haus.workspace.yaml");
5137
+ const yamlPath = path30.join(workspaceRoot, "haus.workspace.yaml");
4946
5138
  const existingText = await readText(yamlPath);
4947
5139
  const existing = parseWorkspaceConfig(existingText);
4948
5140
  if (existingText && !existing) {
@@ -4976,18 +5168,18 @@ async function runDiscover(workspaceRoot, opts = {}) {
4976
5168
 
4977
5169
  // src/commands/workspace/doctor.ts
4978
5170
  import { existsSync as existsSync3 } from "node:fs";
4979
- import path31 from "node:path";
5171
+ import path32 from "node:path";
4980
5172
 
4981
5173
  // src/commands/workspace/manifest.ts
4982
5174
  import { readFileSync as readFileSync3 } from "node:fs";
4983
- import path30 from "node:path";
5175
+ import path31 from "node:path";
4984
5176
  var MANIFEST_FILE = "workspace.manifest.json";
4985
5177
  function manifestPath(workspaceRoot) {
4986
5178
  return hausPath(workspaceRoot, MANIFEST_FILE);
4987
5179
  }
4988
5180
  function hausVersion() {
4989
5181
  try {
4990
- const pkg = JSON.parse(readFileSync3(path30.join(packageRoot(), "package.json"), "utf8"));
5182
+ const pkg = JSON.parse(readFileSync3(path31.join(packageRoot(), "package.json"), "utf8"));
4991
5183
  return pkg.version ?? "0.0.0";
4992
5184
  } catch {
4993
5185
  return "0.0.0";
@@ -5053,7 +5245,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
5053
5245
  }
5054
5246
  const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
5055
5247
  for (const repo of config.repos) {
5056
- const repoRoot = path31.resolve(workspaceRoot, repo.path);
5248
+ const repoRoot = path32.resolve(workspaceRoot, repo.path);
5057
5249
  const entry = manifestByName.get(repo.name);
5058
5250
  if (!entry) {
5059
5251
  flag({
@@ -5127,11 +5319,11 @@ function emit(args) {
5127
5319
 
5128
5320
  // src/commands/workspace/setup.ts
5129
5321
  import { existsSync as existsSync4, statSync } from "node:fs";
5130
- import path33 from "node:path";
5322
+ import path34 from "node:path";
5131
5323
 
5132
5324
  // src/claude/write-workspace-claude-md.ts
5133
- import path32 from "node:path";
5134
- import fs21 from "fs-extra";
5325
+ import path33 from "node:path";
5326
+ import fs22 from "fs-extra";
5135
5327
  function buildWorkspaceImportBlock(client, members) {
5136
5328
  const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
5137
5329
  const body = [
@@ -5149,8 +5341,8 @@ ${BLOCK_END}`;
5149
5341
  async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
5150
5342
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
5151
5343
  const dryRun = opts.dryRun ?? false;
5152
- const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path32.join(workspaceRoot, "CLAUDE.md");
5153
- 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") : "";
5154
5346
  const next = opts.collision ? `${block}
5155
5347
  ` : injectHausBlock(prev, block);
5156
5348
  const printable = displayPath(workspaceRoot, filePath);
@@ -5175,20 +5367,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
5175
5367
 
5176
5368
  // src/commands/workspace/setup.ts
5177
5369
  function resolveWorkspaceRoot(start = process.cwd()) {
5178
- let dir = path33.resolve(start);
5370
+ let dir = path34.resolve(start);
5179
5371
  for (; ; ) {
5180
- if (existsSync4(path33.join(dir, WORKSPACE_FILE))) return dir;
5181
- const parent = path33.dirname(dir);
5182
- 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);
5183
5375
  dir = parent;
5184
5376
  }
5185
5377
  }
5186
5378
  function isRootRepo(workspaceRoot, repoPath) {
5187
- return path33.resolve(workspaceRoot, repoPath) === path33.resolve(workspaceRoot);
5379
+ return path34.resolve(workspaceRoot, repoPath) === path34.resolve(workspaceRoot);
5188
5380
  }
5189
5381
  async function runWorkspaceSetup(workspaceRoot, options = {}) {
5190
5382
  const apply = options.write ?? false;
5191
- const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
5383
+ const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
5192
5384
  if (!configText) {
5193
5385
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
5194
5386
  process.exitCode = 1;
@@ -5205,7 +5397,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
5205
5397
  const statuses = [];
5206
5398
  const aggregateInputs = [];
5207
5399
  for (const repo of repos) {
5208
- const repoRoot = path33.resolve(workspaceRoot, repo.path);
5400
+ const repoRoot = path34.resolve(workspaceRoot, repo.path);
5209
5401
  log(`
5210
5402
  \u2192 ${repo.name} (${repo.path})`);
5211
5403
  try {
@@ -5273,7 +5465,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
5273
5465
  const status = statusByName.get(repo.name);
5274
5466
  const role = repo.role ?? status?.roles?.[0] ?? "auto";
5275
5467
  if (status?.status === "ok") {
5276
- const lock = await checkLock(path33.resolve(workspaceRoot, repo.path));
5468
+ const lock = await checkLock(path34.resolve(workspaceRoot, repo.path));
5277
5469
  manifestRepos.push({
5278
5470
  name: repo.name,
5279
5471
  path: repo.path,
@@ -5353,7 +5545,7 @@ relationships: []
5353
5545
  log("Workspace initialized.");
5354
5546
  }
5355
5547
  async function scanWorkspace(workspaceRoot, opts) {
5356
- const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
5548
+ const configText = await readText(path35.join(workspaceRoot, WORKSPACE_FILE));
5357
5549
  if (!configText) {
5358
5550
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
5359
5551
  process.exitCode = 1;
@@ -5374,7 +5566,7 @@ async function scanWorkspace(workspaceRoot, opts) {
5374
5566
  }
5375
5567
  const inputs = [];
5376
5568
  for (const repo of config.repos) {
5377
- const repoRoot = path34.resolve(workspaceRoot, repo.path);
5569
+ const repoRoot = path35.resolve(workspaceRoot, repo.path);
5378
5570
  if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
5379
5571
  throw new Error(`Repo path is not a directory: ${repo.path}`);
5380
5572
  }
@@ -5424,7 +5616,7 @@ async function runWorkspace(action, options = {}) {
5424
5616
  // src/cli.ts
5425
5617
  function cliVersion() {
5426
5618
  try {
5427
- const pkgPath = path35.join(packageRoot(), "package.json");
5619
+ const pkgPath = path36.join(packageRoot(), "package.json");
5428
5620
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5429
5621
  return pkg.version ?? "0.0.0";
5430
5622
  } catch {
@@ -5434,7 +5626,7 @@ function cliVersion() {
5434
5626
  var program = new Command();
5435
5627
  function validateRuntimeNodeVersion() {
5436
5628
  try {
5437
- const pkgPath = path35.join(packageRoot(), "package.json");
5629
+ const pkgPath = path36.join(packageRoot(), "package.json");
5438
5630
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5439
5631
  const requiredRange = pkg.engines?.node;
5440
5632
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
@@ -5470,6 +5662,9 @@ program.command("catalog-audit").action(runCatalogAudit);
5470
5662
  program.command("validate-catalog").argument("[manifest]").action(runValidateCatalog);
5471
5663
  program.command("update").option("--check").action(runUpdate);
5472
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);
5473
5668
  program.command("uninstall").option("--force").action(runUninstallCommand);
5474
5669
  var guard = program.command("guard");
5475
5670
  guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.8.0",
2
+ "version": "2.9.0",
3
3
  "items": [
4
4
  {
5
5
  "id": "haus.nextjs-patterns",
@@ -3133,6 +3133,45 @@
3133
3133
  "packageNamePattern": "@sentry/*"
3134
3134
  }
3135
3135
  ]
3136
+ },
3137
+ {
3138
+ "id": "haus.eslint-config",
3139
+ "version": "1.0.0",
3140
+ "source": "haus",
3141
+ "type": "config",
3142
+ "path": "configs/eslint/eslint.config.mjs",
3143
+ "title": "Haus ESLint config",
3144
+ "purpose": "ESLint 9.x flat config for Haus Tech TypeScript/React projects.",
3145
+ "whenToUse": "Scaffold into a new project to get the standard Haus ESLint setup. Run `haus scaffold haus.eslint-config`.",
3146
+ "whenNotToUse": "Do not scaffold if the project already has a custom ESLint config that extends beyond the Haus baseline.",
3147
+ "tags": ["frontend", "backend", "typescript", "quality"],
3148
+ "repoRoles": ["next-app", "react-app", "nestjs-api", "vendure-plugin", "vendure-app"],
3149
+ "requiresAny": [
3150
+ {
3151
+ "dependency": "typescript"
3152
+ }
3153
+ ],
3154
+ "tokenEstimate": 0,
3155
+ "installMode": "copy-selected",
3156
+ "reviewStatus": "approved",
3157
+ "riskLevel": "low"
3158
+ },
3159
+ {
3160
+ "id": "haus.prettier-config",
3161
+ "version": "1.0.0",
3162
+ "source": "haus",
3163
+ "type": "config",
3164
+ "path": "configs/prettier",
3165
+ "title": "Haus Prettier config",
3166
+ "purpose": "Prettier config and .prettierignore for Haus Tech projects.",
3167
+ "whenToUse": "Scaffold into a new project to get the standard Haus Prettier setup. Run `haus scaffold haus.prettier-config`.",
3168
+ "whenNotToUse": "Do not scaffold if the project already has a custom Prettier config.",
3169
+ "tags": ["frontend", "backend", "typescript", "quality"],
3170
+ "repoRoles": [],
3171
+ "tokenEstimate": 0,
3172
+ "installMode": "copy-selected",
3173
+ "reviewStatus": "approved",
3174
+ "riskLevel": "low"
3136
3175
  }
3137
3176
  ]
3138
3177
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/haus-workflow",
3
- "version": "0.28.1",
3
+ "version": "0.29.0",
4
4
  "description": "Haus AI workflow CLI for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {