@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/CHANGELOG.md +8 -0
- package/dist/cli.js +310 -116
- package/library/catalog/manifest.json +982 -40
- package/library/catalog/validation-rules.json +0 -1
- package/package.json +1 -1
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
|
|
@@ -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
|
-
|
|
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
|
|
1598
|
-
const pattern = DENY_DIRS.has(
|
|
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
|
|
4141
|
-
import
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
4175
|
-
if (!await
|
|
4176
|
-
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");
|
|
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
|
|
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 ${
|
|
4374
|
+
log(`Stripped haus rules from ${path23.relative(root, settingsPath)} (user settings preserved).`);
|
|
4188
4375
|
return true;
|
|
4189
4376
|
}
|
|
4190
|
-
await
|
|
4191
|
-
log(`Removed ${
|
|
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 =
|
|
4196
|
-
if (!await
|
|
4197
|
-
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");
|
|
4198
4385
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
4199
4386
|
const next = stripHausBlock(prev);
|
|
4200
4387
|
if (next.length === 0) {
|
|
4201
|
-
await
|
|
4388
|
+
await fs18.remove(filePath);
|
|
4202
4389
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
4203
4390
|
} else {
|
|
4204
|
-
await
|
|
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
|
|
4211
|
-
const entries = await
|
|
4212
|
-
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);
|
|
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
|
|
4220
|
-
const rel =
|
|
4221
|
-
const backupPath =
|
|
4222
|
-
await
|
|
4223
|
-
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);
|
|
4224
4411
|
}
|
|
4225
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
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) =>
|
|
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
|
|
4254
|
-
await
|
|
4255
|
-
log(`Removed ${
|
|
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
|
|
4267
|
-
import
|
|
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 =
|
|
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
|
|
4296
|
-
await pruneEmptyDir(
|
|
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 =
|
|
4490
|
+
const hausDir = path24.join(globalClaudeDir(), "haus");
|
|
4304
4491
|
const manifestPath2 = hausManifestPath();
|
|
4305
|
-
if (
|
|
4306
|
-
await
|
|
4492
|
+
if (fs19.pathExistsSync(manifestPath2)) {
|
|
4493
|
+
await fs19.remove(manifestPath2);
|
|
4307
4494
|
}
|
|
4308
|
-
if (
|
|
4309
|
-
const remaining = await
|
|
4310
|
-
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);
|
|
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
|
|
4528
|
+
import path26 from "node:path";
|
|
4342
4529
|
|
|
4343
4530
|
// src/claude/refresh-project.ts
|
|
4344
|
-
import
|
|
4531
|
+
import fs20 from "fs-extra";
|
|
4345
4532
|
async function isHausProject(root) {
|
|
4346
|
-
if (await
|
|
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
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
4740
|
+
import path28 from "node:path";
|
|
4554
4741
|
|
|
4555
4742
|
// src/catalog/validate-core.ts
|
|
4556
|
-
import
|
|
4557
|
-
import
|
|
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 ||
|
|
4562
|
-
const normalized =
|
|
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 =
|
|
4642
|
-
const text =
|
|
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 =
|
|
4849
|
+
const absPath = path27.join(manifestDir, item.path);
|
|
4663
4850
|
if (item.type === "skill") {
|
|
4664
|
-
const skillMd =
|
|
4665
|
-
if (!
|
|
4666
|
-
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)}`);
|
|
4667
4854
|
continue;
|
|
4668
4855
|
}
|
|
4669
|
-
const text =
|
|
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}: ${
|
|
4859
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path27.relative(manifestDir, skillMd)}`)
|
|
4673
4860
|
);
|
|
4674
4861
|
} else if (item.type === "agent") {
|
|
4675
|
-
if (!
|
|
4862
|
+
if (!fs21.existsSync(absPath)) {
|
|
4676
4863
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
4677
4864
|
continue;
|
|
4678
4865
|
}
|
|
4679
|
-
const text =
|
|
4680
|
-
const rel =
|
|
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 (!
|
|
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 (!
|
|
4877
|
+
if (!fs21.existsSync(absPath)) {
|
|
4691
4878
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
4692
4879
|
continue;
|
|
4693
4880
|
}
|
|
4694
|
-
const text =
|
|
4695
|
-
const rel =
|
|
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
|
|
4710
|
-
const full =
|
|
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 =
|
|
4719
|
-
if (!
|
|
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 =
|
|
4723
|
-
const rel =
|
|
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 =
|
|
4757
|
-
const manifestDir =
|
|
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
|
|
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
|
|
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(
|
|
5043
|
+
return parseWorkspaceConfig(await readText(path29.join(workspaceRoot, WORKSPACE_FILE)));
|
|
4853
5044
|
}
|
|
4854
5045
|
|
|
4855
5046
|
// src/commands/workspace/discover.ts
|
|
4856
|
-
import
|
|
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 =
|
|
4885
|
-
const dir =
|
|
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 =
|
|
4902
|
-
const pkg = await readJson(
|
|
4903
|
-
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);
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
5322
|
+
import path34 from "node:path";
|
|
5132
5323
|
|
|
5133
5324
|
// src/claude/write-workspace-claude-md.ts
|
|
5134
|
-
import
|
|
5135
|
-
import
|
|
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") :
|
|
5154
|
-
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") : "";
|
|
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 =
|
|
5370
|
+
let dir = path34.resolve(start);
|
|
5180
5371
|
for (; ; ) {
|
|
5181
|
-
if (existsSync4(
|
|
5182
|
-
const parent =
|
|
5183
|
-
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);
|
|
5184
5375
|
dir = parent;
|
|
5185
5376
|
}
|
|
5186
5377
|
}
|
|
5187
5378
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
5188
|
-
return
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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));
|