@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 +6 -0
- package/dist/cli.js +310 -115
- package/library/catalog/manifest.json +40 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
|
1597
|
-
const pattern = DENY_DIRS.has(
|
|
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
|
|
4140
|
-
import
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
4174
|
-
if (!await
|
|
4175
|
-
const text = await
|
|
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
|
|
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 ${
|
|
4374
|
+
log(`Stripped haus rules from ${path23.relative(root, settingsPath)} (user settings preserved).`);
|
|
4187
4375
|
return true;
|
|
4188
4376
|
}
|
|
4189
|
-
await
|
|
4190
|
-
log(`Removed ${
|
|
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 =
|
|
4195
|
-
if (!await
|
|
4196
|
-
const prev = await
|
|
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
|
|
4388
|
+
await fs18.remove(filePath);
|
|
4201
4389
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
4202
4390
|
} else {
|
|
4203
|
-
await
|
|
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
|
|
4210
|
-
const entries = await
|
|
4211
|
-
if (entries.length === 0) await
|
|
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
|
|
4219
|
-
const rel =
|
|
4220
|
-
const backupPath =
|
|
4221
|
-
await
|
|
4222
|
-
await
|
|
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 ${
|
|
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) =>
|
|
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
|
|
4253
|
-
await
|
|
4254
|
-
log(`Removed ${
|
|
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
|
|
4266
|
-
import
|
|
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 =
|
|
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
|
|
4295
|
-
await pruneEmptyDir(
|
|
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 =
|
|
4490
|
+
const hausDir = path24.join(globalClaudeDir(), "haus");
|
|
4303
4491
|
const manifestPath2 = hausManifestPath();
|
|
4304
|
-
if (
|
|
4305
|
-
await
|
|
4492
|
+
if (fs19.pathExistsSync(manifestPath2)) {
|
|
4493
|
+
await fs19.remove(manifestPath2);
|
|
4306
4494
|
}
|
|
4307
|
-
if (
|
|
4308
|
-
const remaining = await
|
|
4309
|
-
if (remaining.length === 0) await
|
|
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
|
|
4528
|
+
import path26 from "node:path";
|
|
4341
4529
|
|
|
4342
4530
|
// src/claude/refresh-project.ts
|
|
4343
|
-
import
|
|
4531
|
+
import fs20 from "fs-extra";
|
|
4344
4532
|
async function isHausProject(root) {
|
|
4345
|
-
if (await
|
|
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
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
4740
|
+
import path28 from "node:path";
|
|
4553
4741
|
|
|
4554
4742
|
// src/catalog/validate-core.ts
|
|
4555
|
-
import
|
|
4556
|
-
import
|
|
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 ||
|
|
4561
|
-
const normalized =
|
|
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 =
|
|
4641
|
-
const text =
|
|
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 =
|
|
4849
|
+
const absPath = path27.join(manifestDir, item.path);
|
|
4662
4850
|
if (item.type === "skill") {
|
|
4663
|
-
const skillMd =
|
|
4664
|
-
if (!
|
|
4665
|
-
failures.push(`${item.id}: missing ${
|
|
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 =
|
|
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}: ${
|
|
4859
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path27.relative(manifestDir, skillMd)}`)
|
|
4672
4860
|
);
|
|
4673
4861
|
} else if (item.type === "agent") {
|
|
4674
|
-
if (!
|
|
4862
|
+
if (!fs21.existsSync(absPath)) {
|
|
4675
4863
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
4676
4864
|
continue;
|
|
4677
4865
|
}
|
|
4678
|
-
const text =
|
|
4679
|
-
const rel =
|
|
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 (!
|
|
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 (!
|
|
4877
|
+
if (!fs21.existsSync(absPath)) {
|
|
4690
4878
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
4691
4879
|
continue;
|
|
4692
4880
|
}
|
|
4693
|
-
const text =
|
|
4694
|
-
const rel =
|
|
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
|
|
4709
|
-
const full =
|
|
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 =
|
|
4718
|
-
if (!
|
|
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 =
|
|
4722
|
-
const rel =
|
|
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 =
|
|
4756
|
-
const manifestDir =
|
|
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
|
|
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
|
|
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(
|
|
5043
|
+
return parseWorkspaceConfig(await readText(path29.join(workspaceRoot, WORKSPACE_FILE)));
|
|
4852
5044
|
}
|
|
4853
5045
|
|
|
4854
5046
|
// src/commands/workspace/discover.ts
|
|
4855
|
-
import
|
|
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 =
|
|
4884
|
-
const dir =
|
|
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 =
|
|
4901
|
-
const pkg = await readJson(
|
|
4902
|
-
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name :
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
5322
|
+
import path34 from "node:path";
|
|
5131
5323
|
|
|
5132
5324
|
// src/claude/write-workspace-claude-md.ts
|
|
5133
|
-
import
|
|
5134
|
-
import
|
|
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") :
|
|
5153
|
-
const prev = await
|
|
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 =
|
|
5370
|
+
let dir = path34.resolve(start);
|
|
5179
5371
|
for (; ; ) {
|
|
5180
|
-
if (existsSync4(
|
|
5181
|
-
const parent =
|
|
5182
|
-
if (parent === dir) return
|
|
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
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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
|
}
|