@haus-tech/haus-workflow 0.18.2 → 0.20.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 +12 -0
- package/dist/cli.js +166 -129
- package/library/global/commands/haus-clone.md +36 -0
- package/library/global/skills/haus-workflow/SKILL.md +18 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.20.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.19.0...v0.20.0) (2026-06-11)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **clone:** enhance user confirmation prompts before cloning repositories ([#93](https://github.com/WeAreHausTech/haus-workflow/issues/93)) ([1f3e6b4](https://github.com/WeAreHausTech/haus-workflow/commit/1f3e6b4c4e97b2ab7b979ab4fbcab8ce6cda3f32))
|
|
8
|
+
|
|
9
|
+
## [0.19.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.18.2...v0.19.0) (2026-06-11)
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- **clone:** implement single repository cloning command and related … ([#89](https://github.com/WeAreHausTech/haus-workflow/issues/89)) ([3ac9883](https://github.com/WeAreHausTech/haus-workflow/commit/3ac98838db79bf69a602f5c3c17cfd6a1a7f924a))
|
|
14
|
+
|
|
3
15
|
## [0.18.2](https://github.com/WeAreHausTech/haus-workflow/compare/v0.18.1...v0.18.2) (2026-06-11)
|
|
4
16
|
|
|
5
17
|
### Bug Fixes
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { readFileSync as readFileSync4 } from "fs";
|
|
5
|
-
import
|
|
5
|
+
import path35 from "path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/commands/apply.ts
|
|
@@ -554,8 +554,8 @@ function buildDenyRules() {
|
|
|
554
554
|
for (const command of DANGEROUS_COMMANDS) {
|
|
555
555
|
rules.push(`Bash(${command}:*)`);
|
|
556
556
|
}
|
|
557
|
-
for (const
|
|
558
|
-
const pattern = SENSITIVE_DIRS.has(
|
|
557
|
+
for (const path36 of SENSITIVE_PATHS) {
|
|
558
|
+
const pattern = SENSITIVE_DIRS.has(path36) ? `${path36}/**` : path36;
|
|
559
559
|
for (const tool of FILE_TOOLS) {
|
|
560
560
|
rules.push(`${tool}(${pattern})`);
|
|
561
561
|
}
|
|
@@ -1643,8 +1643,67 @@ async function runCatalogAudit() {
|
|
|
1643
1643
|
log("Catalog audit passed.");
|
|
1644
1644
|
}
|
|
1645
1645
|
|
|
1646
|
-
// src/commands/
|
|
1646
|
+
// src/commands/clone.ts
|
|
1647
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1647
1648
|
import path14 from "path";
|
|
1649
|
+
|
|
1650
|
+
// src/utils/exec.ts
|
|
1651
|
+
import { execa } from "execa";
|
|
1652
|
+
async function runCommand(command, args = [], options = {}) {
|
|
1653
|
+
try {
|
|
1654
|
+
const result = await execa(command, args, {
|
|
1655
|
+
reject: false,
|
|
1656
|
+
// non-zero exits are returned, not thrown
|
|
1657
|
+
...options
|
|
1658
|
+
});
|
|
1659
|
+
return {
|
|
1660
|
+
command,
|
|
1661
|
+
args,
|
|
1662
|
+
stdout: String(result.stdout ?? ""),
|
|
1663
|
+
stderr: String(result.stderr ?? ""),
|
|
1664
|
+
exitCode: result.exitCode ?? 0
|
|
1665
|
+
};
|
|
1666
|
+
} catch (error2) {
|
|
1667
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1668
|
+
throw new Error(`Failed to run command: ${command} ${args.join(" ")} (${message})`);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
async function runGit(args, options = {}) {
|
|
1672
|
+
return runCommand("git", args, options);
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
// src/commands/clone.ts
|
|
1676
|
+
function repoNameFromUrl(url) {
|
|
1677
|
+
const trimmed = url.trim().replace(/\.git$/, "").replace(/\/+$/, "");
|
|
1678
|
+
const tail = trimmed.split(/[/:]/).pop() ?? "";
|
|
1679
|
+
return tail || "repo";
|
|
1680
|
+
}
|
|
1681
|
+
async function runClone(url, opts = {}) {
|
|
1682
|
+
if (!url || !url.trim()) {
|
|
1683
|
+
error("A git URL is required: `haus clone <url> [dir]`.");
|
|
1684
|
+
process.exitCode = 1;
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
const target = path14.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
1688
|
+
if (existsSync2(target)) {
|
|
1689
|
+
log(`\u2022 ${path14.basename(target)} already present at ${target} \u2014 skipped`);
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
if (opts.dryRun) {
|
|
1693
|
+
log(`would clone ${url} \u2192 ${target}`);
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
const res = await runGit(["clone", url, target]);
|
|
1697
|
+
if (res.exitCode !== 0) {
|
|
1698
|
+
error(`clone failed for ${url}: ${(res.stderr || res.stdout).trim()}`);
|
|
1699
|
+
process.exitCode = 1;
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
log(`\u2713 cloned ${url} \u2192 ${target}`);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// src/commands/config.ts
|
|
1706
|
+
import path15 from "path";
|
|
1648
1707
|
var CONFIG_PATH2 = ".haus-workflow/config.json";
|
|
1649
1708
|
var HOOK_ALIASES = {
|
|
1650
1709
|
"hook.context": "context"
|
|
@@ -1657,7 +1716,7 @@ async function runConfig(key, action) {
|
|
|
1657
1716
|
);
|
|
1658
1717
|
}
|
|
1659
1718
|
const root = process.cwd();
|
|
1660
|
-
const configPath =
|
|
1719
|
+
const configPath = path15.join(root, CONFIG_PATH2);
|
|
1661
1720
|
const existing = await readJson(configPath);
|
|
1662
1721
|
const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
|
|
1663
1722
|
cfg.hooks ??= {};
|
|
@@ -2029,7 +2088,7 @@ function selectRules(recommended, task, taskIntents) {
|
|
|
2029
2088
|
|
|
2030
2089
|
// src/scanner/scan-project.ts
|
|
2031
2090
|
import { readFile as readFile2 } from "fs/promises";
|
|
2032
|
-
import
|
|
2091
|
+
import path19 from "path";
|
|
2033
2092
|
|
|
2034
2093
|
// src/utils/audit-checks.ts
|
|
2035
2094
|
function isRecord(v) {
|
|
@@ -2056,7 +2115,7 @@ function compareVersions(a, b) {
|
|
|
2056
2115
|
}
|
|
2057
2116
|
|
|
2058
2117
|
// src/scanner/detect-package-manager.ts
|
|
2059
|
-
import
|
|
2118
|
+
import path16 from "path";
|
|
2060
2119
|
import fs13 from "fs-extra";
|
|
2061
2120
|
function detectPackageManager(root, packageManagerField) {
|
|
2062
2121
|
const field = String(packageManagerField ?? "").trim();
|
|
@@ -2075,9 +2134,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
2075
2134
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
2076
2135
|
return "unknown";
|
|
2077
2136
|
}
|
|
2078
|
-
if (fs13.existsSync(
|
|
2079
|
-
if (fs13.existsSync(
|
|
2080
|
-
if (fs13.existsSync(
|
|
2137
|
+
if (fs13.existsSync(path16.join(root, "yarn.lock"))) return "yarn";
|
|
2138
|
+
if (fs13.existsSync(path16.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
2139
|
+
if (fs13.existsSync(path16.join(root, "package-lock.json"))) return "npm";
|
|
2081
2140
|
return "unknown";
|
|
2082
2141
|
}
|
|
2083
2142
|
|
|
@@ -2250,7 +2309,7 @@ function runDetection(ctx, rules = STACK_RULES) {
|
|
|
2250
2309
|
}
|
|
2251
2310
|
|
|
2252
2311
|
// src/scanner/detection.ts
|
|
2253
|
-
import
|
|
2312
|
+
import path17 from "path";
|
|
2254
2313
|
var UNSUPPORTED_MARKERS = {
|
|
2255
2314
|
"requirements.txt": "python",
|
|
2256
2315
|
"pyproject.toml": "python",
|
|
@@ -2304,14 +2363,14 @@ function finalizeRoles(registryRoles, deps, files) {
|
|
|
2304
2363
|
function collectUnsupportedSignals(files) {
|
|
2305
2364
|
return [
|
|
2306
2365
|
...new Set(
|
|
2307
|
-
files.map((f) => UNSUPPORTED_MARKERS[
|
|
2366
|
+
files.map((f) => UNSUPPORTED_MARKERS[path17.basename(f)]).filter((s) => Boolean(s))
|
|
2308
2367
|
)
|
|
2309
2368
|
].sort();
|
|
2310
2369
|
}
|
|
2311
2370
|
|
|
2312
2371
|
// src/scanner/render.ts
|
|
2313
2372
|
import { readFile } from "fs/promises";
|
|
2314
|
-
import
|
|
2373
|
+
import path18 from "path";
|
|
2315
2374
|
|
|
2316
2375
|
// src/scanner/role-labels.ts
|
|
2317
2376
|
var ROLE_LABELS = {
|
|
@@ -2373,7 +2432,7 @@ async function buildContentBlob(root, files) {
|
|
|
2373
2432
|
const slice = candidates.slice(0, 300);
|
|
2374
2433
|
const parts = await mapWithConcurrency(slice, async (rel) => {
|
|
2375
2434
|
try {
|
|
2376
|
-
return await readFile(
|
|
2435
|
+
return await readFile(path18.join(root, rel), "utf8");
|
|
2377
2436
|
} catch {
|
|
2378
2437
|
return "";
|
|
2379
2438
|
}
|
|
@@ -2471,8 +2530,8 @@ var SAFE_FILES = [
|
|
|
2471
2530
|
"Gemfile"
|
|
2472
2531
|
];
|
|
2473
2532
|
async function scanProject(root, mode = "fast") {
|
|
2474
|
-
const pkg = await readJson(
|
|
2475
|
-
const composer = await readJson(
|
|
2533
|
+
const pkg = await readJson(path19.join(root, "package.json"));
|
|
2534
|
+
const composer = await readJson(path19.join(root, "composer.json"));
|
|
2476
2535
|
const files = await listFiles(root, SAFE_FILES);
|
|
2477
2536
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
2478
2537
|
const deps = dependencySet(pkg, composer);
|
|
@@ -2506,7 +2565,7 @@ async function scanProject(root, mode = "fast") {
|
|
|
2506
2565
|
mode,
|
|
2507
2566
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2508
2567
|
root,
|
|
2509
|
-
repoName: String(pkg?.name ??
|
|
2568
|
+
repoName: String(pkg?.name ?? path19.basename(root)),
|
|
2510
2569
|
packageManager,
|
|
2511
2570
|
repoRoles: roles,
|
|
2512
2571
|
detectedStacks: stacks,
|
|
@@ -2524,7 +2583,7 @@ async function scanProject(root, mode = "fast") {
|
|
|
2524
2583
|
const scanHashes = Object.fromEntries(
|
|
2525
2584
|
await mapWithConcurrency(
|
|
2526
2585
|
safeFiles,
|
|
2527
|
-
async (f) => [f, hashText(await readFile2(
|
|
2586
|
+
async (f) => [f, hashText(await readFile2(path19.join(root, f), "utf8"))]
|
|
2528
2587
|
)
|
|
2529
2588
|
);
|
|
2530
2589
|
const repoSummary = renderSummary(context);
|
|
@@ -2609,7 +2668,7 @@ async function runContext(options) {
|
|
|
2609
2668
|
}
|
|
2610
2669
|
|
|
2611
2670
|
// src/commands/doctor.ts
|
|
2612
|
-
import
|
|
2671
|
+
import path20 from "path";
|
|
2613
2672
|
import fs14 from "fs-extra";
|
|
2614
2673
|
|
|
2615
2674
|
// src/update/npm-version.ts
|
|
@@ -2690,7 +2749,7 @@ async function runDoctor(options) {
|
|
|
2690
2749
|
const enabled = await isHookEnabled(root, key);
|
|
2691
2750
|
ok(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
|
|
2692
2751
|
}
|
|
2693
|
-
const rootClaudeMdPath =
|
|
2752
|
+
const rootClaudeMdPath = path20.join(root, "CLAUDE.md");
|
|
2694
2753
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
2695
2754
|
if (!rootClaudeMdContent) {
|
|
2696
2755
|
flag(
|
|
@@ -2743,8 +2802,8 @@ async function runDoctor(options) {
|
|
|
2743
2802
|
ok("- .haus-workflow/WORKFLOW.md: OK (user-owned)");
|
|
2744
2803
|
} else {
|
|
2745
2804
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
2746
|
-
const cachePath =
|
|
2747
|
-
const bundledPath =
|
|
2805
|
+
const cachePath = path20.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
2806
|
+
const bundledPath = path20.join(
|
|
2748
2807
|
packageRoot(),
|
|
2749
2808
|
"library",
|
|
2750
2809
|
"global",
|
|
@@ -2809,7 +2868,7 @@ async function runDoctor(options) {
|
|
|
2809
2868
|
ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
|
|
2810
2869
|
}
|
|
2811
2870
|
}
|
|
2812
|
-
const pkgJson = await readJson(
|
|
2871
|
+
const pkgJson = await readJson(path20.join(packageRoot(), "package.json"));
|
|
2813
2872
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
2814
2873
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
2815
2874
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -2952,7 +3011,7 @@ async function runGuard(kind, _options) {
|
|
|
2952
3011
|
}
|
|
2953
3012
|
|
|
2954
3013
|
// src/commands/init.ts
|
|
2955
|
-
import
|
|
3014
|
+
import path21 from "path";
|
|
2956
3015
|
import fs15 from "fs-extra";
|
|
2957
3016
|
|
|
2958
3017
|
// src/utils/prompts.ts
|
|
@@ -2973,31 +3032,6 @@ async function confirm(question) {
|
|
|
2973
3032
|
return answer === "y" || answer === "yes";
|
|
2974
3033
|
}
|
|
2975
3034
|
|
|
2976
|
-
// src/utils/exec.ts
|
|
2977
|
-
import { execa } from "execa";
|
|
2978
|
-
async function runCommand(command, args = [], options = {}) {
|
|
2979
|
-
try {
|
|
2980
|
-
const result = await execa(command, args, {
|
|
2981
|
-
reject: false,
|
|
2982
|
-
// non-zero exits are returned, not thrown
|
|
2983
|
-
...options
|
|
2984
|
-
});
|
|
2985
|
-
return {
|
|
2986
|
-
command,
|
|
2987
|
-
args,
|
|
2988
|
-
stdout: String(result.stdout ?? ""),
|
|
2989
|
-
stderr: String(result.stderr ?? ""),
|
|
2990
|
-
exitCode: result.exitCode ?? 0
|
|
2991
|
-
};
|
|
2992
|
-
} catch (error2) {
|
|
2993
|
-
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2994
|
-
throw new Error(`Failed to run command: ${command} ${args.join(" ")} (${message})`);
|
|
2995
|
-
}
|
|
2996
|
-
}
|
|
2997
|
-
async function runGit(args, options = {}) {
|
|
2998
|
-
return runCommand("git", args, options);
|
|
2999
|
-
}
|
|
3000
|
-
|
|
3001
3035
|
// src/recommender/git-signal.ts
|
|
3002
3036
|
async function readChangedFiles(root) {
|
|
3003
3037
|
if (process.env.HAUS_DISABLE_GIT_SIGNALS === "1") return [];
|
|
@@ -3340,7 +3374,7 @@ async function runSetupProject(options) {
|
|
|
3340
3374
|
// src/commands/init.ts
|
|
3341
3375
|
async function runInit(options) {
|
|
3342
3376
|
const root = process.cwd();
|
|
3343
|
-
const hausDir =
|
|
3377
|
+
const hausDir = path21.join(root, ".haus-workflow");
|
|
3344
3378
|
const alreadyInit = await fs15.pathExists(hausDir);
|
|
3345
3379
|
if (alreadyInit) {
|
|
3346
3380
|
log("Haus AI already initialized in this project.");
|
|
@@ -3353,7 +3387,7 @@ async function runInit(options) {
|
|
|
3353
3387
|
|
|
3354
3388
|
// src/install/apply.ts
|
|
3355
3389
|
import crypto2 from "crypto";
|
|
3356
|
-
import
|
|
3390
|
+
import path22 from "path";
|
|
3357
3391
|
import fs16 from "fs-extra";
|
|
3358
3392
|
|
|
3359
3393
|
// src/install/header.ts
|
|
@@ -3442,7 +3476,7 @@ function hashContent(content2) {
|
|
|
3442
3476
|
}
|
|
3443
3477
|
function sourceVersion() {
|
|
3444
3478
|
try {
|
|
3445
|
-
const pkgPath =
|
|
3479
|
+
const pkgPath = path22.join(packageRoot(), "package.json");
|
|
3446
3480
|
const pkg = JSON.parse(fs16.readFileSync(pkgPath, "utf8"));
|
|
3447
3481
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
3448
3482
|
} catch {
|
|
@@ -3450,32 +3484,32 @@ function sourceVersion() {
|
|
|
3450
3484
|
}
|
|
3451
3485
|
}
|
|
3452
3486
|
function globalSrcDir() {
|
|
3453
|
-
return
|
|
3487
|
+
return path22.join(packageRoot(), "library", "global");
|
|
3454
3488
|
}
|
|
3455
3489
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
3456
3490
|
const entries = [];
|
|
3457
|
-
const skillsDir =
|
|
3491
|
+
const skillsDir = path22.join(srcDir, "skills");
|
|
3458
3492
|
if (fs16.pathExistsSync(skillsDir)) {
|
|
3459
3493
|
for (const skillName of fs16.readdirSync(skillsDir)) {
|
|
3460
|
-
const skillFile =
|
|
3494
|
+
const skillFile = path22.join(skillsDir, skillName, "SKILL.md");
|
|
3461
3495
|
if (fs16.pathExistsSync(skillFile)) {
|
|
3462
3496
|
entries.push({
|
|
3463
3497
|
stableId: `skill.${skillName}`,
|
|
3464
|
-
srcRelPath:
|
|
3465
|
-
destPath:
|
|
3498
|
+
srcRelPath: path22.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
3499
|
+
destPath: path22.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
3466
3500
|
});
|
|
3467
3501
|
}
|
|
3468
3502
|
}
|
|
3469
3503
|
}
|
|
3470
|
-
const commandsDir =
|
|
3504
|
+
const commandsDir = path22.join(srcDir, "commands");
|
|
3471
3505
|
if (fs16.pathExistsSync(commandsDir)) {
|
|
3472
3506
|
for (const fileName of fs16.readdirSync(commandsDir)) {
|
|
3473
3507
|
if (!fileName.endsWith(".md")) continue;
|
|
3474
3508
|
const commandName = fileName.slice(0, -".md".length);
|
|
3475
3509
|
entries.push({
|
|
3476
3510
|
stableId: `command.${commandName}`,
|
|
3477
|
-
srcRelPath:
|
|
3478
|
-
destPath:
|
|
3511
|
+
srcRelPath: path22.join("library", "global", "commands", fileName),
|
|
3512
|
+
destPath: path22.join(claudeDir, "commands", fileName)
|
|
3479
3513
|
});
|
|
3480
3514
|
}
|
|
3481
3515
|
}
|
|
@@ -3499,7 +3533,7 @@ async function applyInstall(options = {}) {
|
|
|
3499
3533
|
};
|
|
3500
3534
|
const manifestFiles = [];
|
|
3501
3535
|
for (const entry of sourceFiles) {
|
|
3502
|
-
const srcPath =
|
|
3536
|
+
const srcPath = path22.join(packageRoot(), entry.srcRelPath);
|
|
3503
3537
|
const rawContent = await readText(srcPath);
|
|
3504
3538
|
if (rawContent === void 0) {
|
|
3505
3539
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -3555,7 +3589,7 @@ async function applyInstall(options = {}) {
|
|
|
3555
3589
|
schemaVersion: SCHEMA_VERSION2
|
|
3556
3590
|
});
|
|
3557
3591
|
}
|
|
3558
|
-
const fragmentPath =
|
|
3592
|
+
const fragmentPath = path22.join(srcDir, "settings-fragments", "hooks.json");
|
|
3559
3593
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
3560
3594
|
const settings = await readSettings();
|
|
3561
3595
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -3699,7 +3733,7 @@ async function runScan(options) {
|
|
|
3699
3733
|
}
|
|
3700
3734
|
|
|
3701
3735
|
// src/commands/undo.ts
|
|
3702
|
-
import
|
|
3736
|
+
import path23 from "path";
|
|
3703
3737
|
import fs17 from "fs-extra";
|
|
3704
3738
|
|
|
3705
3739
|
// src/claude/managed-paths.ts
|
|
@@ -3726,7 +3760,7 @@ async function collectManagedPaths(root) {
|
|
|
3726
3760
|
const lock = await readJson(hausPath(root, "haus.lock.json"));
|
|
3727
3761
|
for (const row of lock ?? []) {
|
|
3728
3762
|
for (const rel of row.paths ?? []) {
|
|
3729
|
-
paths.add(
|
|
3763
|
+
paths.add(path23.resolve(root, rel));
|
|
3730
3764
|
}
|
|
3731
3765
|
}
|
|
3732
3766
|
const existing = [];
|
|
@@ -3742,7 +3776,7 @@ async function settingsHasHausContent(root) {
|
|
|
3742
3776
|
return settings._haus != null;
|
|
3743
3777
|
}
|
|
3744
3778
|
async function claudeMdHasHausBlock(root) {
|
|
3745
|
-
const filePath =
|
|
3779
|
+
const filePath = path23.join(root, "CLAUDE.md");
|
|
3746
3780
|
if (!await fs17.pathExists(filePath)) return false;
|
|
3747
3781
|
const text = await fs17.readFile(filePath, "utf8");
|
|
3748
3782
|
return text.includes(BLOCK_BEGIN);
|
|
@@ -3755,15 +3789,15 @@ async function stripProjectSettings(root) {
|
|
|
3755
3789
|
const hasContent = Object.keys(settings).length > 0;
|
|
3756
3790
|
if (hasContent) {
|
|
3757
3791
|
await writeProjectSettings(root, settings);
|
|
3758
|
-
log(`Stripped haus rules from ${
|
|
3792
|
+
log(`Stripped haus rules from ${path23.relative(root, settingsPath)} (user settings preserved).`);
|
|
3759
3793
|
return true;
|
|
3760
3794
|
}
|
|
3761
3795
|
await fs17.remove(settingsPath);
|
|
3762
|
-
log(`Removed ${
|
|
3796
|
+
log(`Removed ${path23.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
3763
3797
|
return true;
|
|
3764
3798
|
}
|
|
3765
3799
|
async function stripRootClaudeMd(root) {
|
|
3766
|
-
const filePath =
|
|
3800
|
+
const filePath = path23.join(root, "CLAUDE.md");
|
|
3767
3801
|
if (!await fs17.pathExists(filePath)) return false;
|
|
3768
3802
|
const prev = await fs17.readFile(filePath, "utf8");
|
|
3769
3803
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
@@ -3791,7 +3825,7 @@ async function runUndo(options) {
|
|
|
3791
3825
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
3792
3826
|
return;
|
|
3793
3827
|
}
|
|
3794
|
-
const relTargets = managed.map((p) =>
|
|
3828
|
+
const relTargets = managed.map((p) => path23.relative(root, p));
|
|
3795
3829
|
const summaryParts = [...relTargets];
|
|
3796
3830
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
3797
3831
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -3809,7 +3843,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
3809
3843
|
for (const abs of managed) {
|
|
3810
3844
|
if (!await fs17.pathExists(abs)) continue;
|
|
3811
3845
|
await fs17.remove(abs);
|
|
3812
|
-
log(`Removed ${
|
|
3846
|
+
log(`Removed ${path23.relative(root, abs)}`);
|
|
3813
3847
|
}
|
|
3814
3848
|
if (stripSettings) await stripProjectSettings(root);
|
|
3815
3849
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -3820,7 +3854,7 @@ User-owned .claude/ files will be preserved.`
|
|
|
3820
3854
|
|
|
3821
3855
|
// src/install/uninstall.ts
|
|
3822
3856
|
import crypto3 from "crypto";
|
|
3823
|
-
import
|
|
3857
|
+
import path24 from "path";
|
|
3824
3858
|
import fs18 from "fs-extra";
|
|
3825
3859
|
async function runUninstall(options = {}) {
|
|
3826
3860
|
const { force = false } = options;
|
|
@@ -3850,14 +3884,14 @@ async function runUninstall(options = {}) {
|
|
|
3850
3884
|
continue;
|
|
3851
3885
|
}
|
|
3852
3886
|
await fs18.remove(entry.destPath);
|
|
3853
|
-
await pruneEmptyDir(
|
|
3887
|
+
await pruneEmptyDir(path24.dirname(entry.destPath));
|
|
3854
3888
|
result.deleted.push(entry.destPath);
|
|
3855
3889
|
}
|
|
3856
3890
|
const settings = await readSettings();
|
|
3857
3891
|
const stripped = stripHausHooks(stripHausAllow(stripHausDeny(settings)));
|
|
3858
3892
|
await writeSettings(stripped);
|
|
3859
3893
|
result.hooksStripped = true;
|
|
3860
|
-
const hausDir =
|
|
3894
|
+
const hausDir = path24.join(globalClaudeDir(), "haus");
|
|
3861
3895
|
const manifestPath2 = hausManifestPath();
|
|
3862
3896
|
if (fs18.pathExistsSync(manifestPath2)) {
|
|
3863
3897
|
await fs18.remove(manifestPath2);
|
|
@@ -3895,7 +3929,7 @@ async function runUninstallCommand(options) {
|
|
|
3895
3929
|
}
|
|
3896
3930
|
|
|
3897
3931
|
// src/commands/update.ts
|
|
3898
|
-
import
|
|
3932
|
+
import path26 from "path";
|
|
3899
3933
|
|
|
3900
3934
|
// src/update/diff-generated-files.ts
|
|
3901
3935
|
function diffGeneratedFiles() {
|
|
@@ -3922,7 +3956,7 @@ function summarizeLockDiff(before, after) {
|
|
|
3922
3956
|
|
|
3923
3957
|
// src/update/lockfile.ts
|
|
3924
3958
|
import { mkdir, readFile as readFile3, copyFile } from "fs/promises";
|
|
3925
|
-
import
|
|
3959
|
+
import path25 from "path";
|
|
3926
3960
|
async function checkLock(root) {
|
|
3927
3961
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
3928
3962
|
const hasValidVersions = lock.every(
|
|
@@ -3953,7 +3987,7 @@ async function applyLock(root) {
|
|
|
3953
3987
|
try {
|
|
3954
3988
|
const backupDir = hausPath(root, "backups");
|
|
3955
3989
|
await mkdir(backupDir, { recursive: true });
|
|
3956
|
-
await copyFile(lockPath,
|
|
3990
|
+
await copyFile(lockPath, path25.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
3957
3991
|
} catch {
|
|
3958
3992
|
}
|
|
3959
3993
|
const enriched = await Promise.all(
|
|
@@ -3975,7 +4009,7 @@ function diffLock(before, after) {
|
|
|
3975
4009
|
}
|
|
3976
4010
|
async function hasLocalOverrides(root) {
|
|
3977
4011
|
try {
|
|
3978
|
-
await readFile3(
|
|
4012
|
+
await readFile3(path25.join(root, ".claude", "settings.json"), "utf8");
|
|
3979
4013
|
return true;
|
|
3980
4014
|
} catch {
|
|
3981
4015
|
return false;
|
|
@@ -3987,7 +4021,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
|
3987
4021
|
async function runUpdate(options) {
|
|
3988
4022
|
const root = process.cwd();
|
|
3989
4023
|
if (options.check) {
|
|
3990
|
-
const pkgJson2 = await readJson(
|
|
4024
|
+
const pkgJson2 = await readJson(path26.join(packageRoot(), "package.json"));
|
|
3991
4025
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
3992
4026
|
const [status, npmVersion, latestCatalogTag, globalInstallDrift] = await Promise.all([
|
|
3993
4027
|
checkLock(root),
|
|
@@ -4017,7 +4051,7 @@ async function runUpdate(options) {
|
|
|
4017
4051
|
if (status.driftCount > 0) process.exitCode = 1;
|
|
4018
4052
|
return;
|
|
4019
4053
|
}
|
|
4020
|
-
const pkgJson = await readJson(
|
|
4054
|
+
const pkgJson = await readJson(path26.join(packageRoot(), "package.json"));
|
|
4021
4055
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
4022
4056
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
4023
4057
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -4095,7 +4129,7 @@ async function detectGlobalInstallDrift() {
|
|
|
4095
4129
|
|
|
4096
4130
|
// src/commands/validate-catalog.ts
|
|
4097
4131
|
import fs19 from "fs";
|
|
4098
|
-
import
|
|
4132
|
+
import path27 from "path";
|
|
4099
4133
|
|
|
4100
4134
|
// src/catalog/forbidden-content.ts
|
|
4101
4135
|
var PROSE_FORBIDDEN_TAGS = FORBIDDEN_TAGS.filter((t) => t.toLowerCase() !== "go");
|
|
@@ -4227,17 +4261,17 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4227
4261
|
const failures = [];
|
|
4228
4262
|
for (const item of items) {
|
|
4229
4263
|
if (!item.path) continue;
|
|
4230
|
-
const absPath =
|
|
4264
|
+
const absPath = path27.join(manifestDir, item.path);
|
|
4231
4265
|
if (item.type === "skill") {
|
|
4232
|
-
const skillMd =
|
|
4266
|
+
const skillMd = path27.join(absPath, "SKILL.md");
|
|
4233
4267
|
if (!fs19.existsSync(skillMd)) {
|
|
4234
|
-
failures.push(`${item.id}: missing ${
|
|
4268
|
+
failures.push(`${item.id}: missing ${path27.relative(manifestDir, skillMd)}`);
|
|
4235
4269
|
continue;
|
|
4236
4270
|
}
|
|
4237
4271
|
const text = fs19.readFileSync(skillMd, "utf8");
|
|
4238
4272
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
4239
4273
|
failures.push(
|
|
4240
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
4274
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path27.relative(manifestDir, skillMd)}`)
|
|
4241
4275
|
);
|
|
4242
4276
|
} else if (item.type === "agent") {
|
|
4243
4277
|
if (!fs19.existsSync(absPath)) {
|
|
@@ -4255,7 +4289,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4255
4289
|
failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
|
|
4256
4290
|
}
|
|
4257
4291
|
failures.push(
|
|
4258
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
4292
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path27.relative(manifestDir, absPath)}`)
|
|
4259
4293
|
);
|
|
4260
4294
|
} else if (item.type === "template") {
|
|
4261
4295
|
if (!fs19.existsSync(absPath)) {
|
|
@@ -4269,7 +4303,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4269
4303
|
continue;
|
|
4270
4304
|
}
|
|
4271
4305
|
const text = fs19.readFileSync(absPath, "utf8");
|
|
4272
|
-
const rel =
|
|
4306
|
+
const rel = path27.relative(manifestDir, absPath);
|
|
4273
4307
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
4274
4308
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
4275
4309
|
}
|
|
@@ -4277,7 +4311,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4277
4311
|
return failures;
|
|
4278
4312
|
}
|
|
4279
4313
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
4280
|
-
const rel =
|
|
4314
|
+
const rel = path27.relative(manifestDir, absPath);
|
|
4281
4315
|
const text = fs19.readFileSync(absPath, "utf8");
|
|
4282
4316
|
const failures = [];
|
|
4283
4317
|
const lines = text.split(/\r?\n/);
|
|
@@ -4300,11 +4334,11 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4300
4334
|
const failures = [];
|
|
4301
4335
|
const dirs = ["skills", "agents", "templates", "commands"];
|
|
4302
4336
|
for (const dir of dirs) {
|
|
4303
|
-
const abs =
|
|
4337
|
+
const abs = path27.join(manifestDir, dir);
|
|
4304
4338
|
if (!fs19.existsSync(abs)) continue;
|
|
4305
4339
|
walkMd(abs, (file) => {
|
|
4306
4340
|
const text = fs19.readFileSync(file, "utf8");
|
|
4307
|
-
const rel =
|
|
4341
|
+
const rel = path27.relative(manifestDir, file);
|
|
4308
4342
|
const lines = text.split(/\r?\n/);
|
|
4309
4343
|
for (let i = 0; i < lines.length; i++) {
|
|
4310
4344
|
const line2 = lines[i] ?? "";
|
|
@@ -4324,7 +4358,7 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4324
4358
|
}
|
|
4325
4359
|
function walkMd(dir, fn) {
|
|
4326
4360
|
for (const entry of fs19.readdirSync(dir, { withFileTypes: true })) {
|
|
4327
|
-
const full =
|
|
4361
|
+
const full = path27.join(dir, entry.name);
|
|
4328
4362
|
if (entry.isDirectory()) walkMd(full, fn);
|
|
4329
4363
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
4330
4364
|
}
|
|
@@ -4335,8 +4369,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
4335
4369
|
process.exitCode = 1;
|
|
4336
4370
|
return;
|
|
4337
4371
|
}
|
|
4338
|
-
const abs =
|
|
4339
|
-
const manifestDir =
|
|
4372
|
+
const abs = path27.resolve(process.cwd(), manifestPath2);
|
|
4373
|
+
const manifestDir = path27.dirname(abs);
|
|
4340
4374
|
const data = await readJson(abs);
|
|
4341
4375
|
if (!data?.items) {
|
|
4342
4376
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -4365,8 +4399,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
4365
4399
|
}
|
|
4366
4400
|
|
|
4367
4401
|
// src/commands/workspace.ts
|
|
4368
|
-
import { existsSync as
|
|
4369
|
-
import
|
|
4402
|
+
import { existsSync as existsSync5, statSync as statSync2 } from "fs";
|
|
4403
|
+
import path34 from "path";
|
|
4370
4404
|
|
|
4371
4405
|
// src/commands/workspace/aggregate.ts
|
|
4372
4406
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -4420,7 +4454,7 @@ ${summaries.map(
|
|
|
4420
4454
|
}
|
|
4421
4455
|
|
|
4422
4456
|
// src/commands/workspace/config.ts
|
|
4423
|
-
import
|
|
4457
|
+
import path28 from "path";
|
|
4424
4458
|
import YAML from "yaml";
|
|
4425
4459
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
4426
4460
|
function parseWorkspaceConfig(text) {
|
|
@@ -4443,11 +4477,11 @@ function parseWorkspaceConfig(text) {
|
|
|
4443
4477
|
};
|
|
4444
4478
|
}
|
|
4445
4479
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
4446
|
-
return parseWorkspaceConfig(await readText(
|
|
4480
|
+
return parseWorkspaceConfig(await readText(path28.join(workspaceRoot, WORKSPACE_FILE)));
|
|
4447
4481
|
}
|
|
4448
4482
|
|
|
4449
4483
|
// src/commands/workspace/discover.ts
|
|
4450
|
-
import
|
|
4484
|
+
import path29 from "path";
|
|
4451
4485
|
import fg3 from "fast-glob";
|
|
4452
4486
|
import YAML2 from "yaml";
|
|
4453
4487
|
var DEFAULT_MAX_DEPTH = 3;
|
|
@@ -4475,8 +4509,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
4475
4509
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
4476
4510
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
4477
4511
|
for (const match of matches) {
|
|
4478
|
-
const base =
|
|
4479
|
-
const dir =
|
|
4512
|
+
const base = path29.posix.basename(match);
|
|
4513
|
+
const dir = path29.posix.dirname(match);
|
|
4480
4514
|
const owner = dir === "." ? "." : dir;
|
|
4481
4515
|
if (base === ".git") gitDirs.add(owner);
|
|
4482
4516
|
else manifestDirs.add(owner);
|
|
@@ -4492,9 +4526,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
4492
4526
|
}
|
|
4493
4527
|
repoRoots.sort((a, b) => a.localeCompare(b));
|
|
4494
4528
|
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
4495
|
-
const absDir =
|
|
4496
|
-
const pkg = await readJson(
|
|
4497
|
-
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name :
|
|
4529
|
+
const absDir = path29.resolve(workspaceRoot, relDir);
|
|
4530
|
+
const pkg = await readJson(path29.join(absDir, "package.json"));
|
|
4531
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path29.basename(relDir === "." ? workspaceRoot : absDir);
|
|
4498
4532
|
let role = "auto";
|
|
4499
4533
|
try {
|
|
4500
4534
|
const scan = await scanProject(absDir, "fast");
|
|
@@ -4537,7 +4571,7 @@ function renderWorkspaceYaml(config2) {
|
|
|
4537
4571
|
});
|
|
4538
4572
|
}
|
|
4539
4573
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
4540
|
-
const yamlPath =
|
|
4574
|
+
const yamlPath = path29.join(workspaceRoot, "haus.workspace.yaml");
|
|
4541
4575
|
const existingText = await readText(yamlPath);
|
|
4542
4576
|
const existing = parseWorkspaceConfig(existingText);
|
|
4543
4577
|
if (existingText && !existing) {
|
|
@@ -4570,19 +4604,19 @@ async function runDiscover(workspaceRoot, opts = {}) {
|
|
|
4570
4604
|
}
|
|
4571
4605
|
|
|
4572
4606
|
// src/commands/workspace/doctor.ts
|
|
4573
|
-
import { existsSync as
|
|
4574
|
-
import
|
|
4607
|
+
import { existsSync as existsSync3 } from "fs";
|
|
4608
|
+
import path31 from "path";
|
|
4575
4609
|
|
|
4576
4610
|
// src/commands/workspace/manifest.ts
|
|
4577
4611
|
import { readFileSync as readFileSync3 } from "fs";
|
|
4578
|
-
import
|
|
4612
|
+
import path30 from "path";
|
|
4579
4613
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
4580
4614
|
function manifestPath(workspaceRoot) {
|
|
4581
4615
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
4582
4616
|
}
|
|
4583
4617
|
function hausVersion() {
|
|
4584
4618
|
try {
|
|
4585
|
-
const pkg = JSON.parse(readFileSync3(
|
|
4619
|
+
const pkg = JSON.parse(readFileSync3(path30.join(packageRoot(), "package.json"), "utf8"));
|
|
4586
4620
|
return pkg.version ?? "0.0.0";
|
|
4587
4621
|
} catch {
|
|
4588
4622
|
return "0.0.0";
|
|
@@ -4648,7 +4682,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
4648
4682
|
}
|
|
4649
4683
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
4650
4684
|
for (const repo of config2.repos) {
|
|
4651
|
-
const repoRoot =
|
|
4685
|
+
const repoRoot = path31.resolve(workspaceRoot, repo.path);
|
|
4652
4686
|
const entry = manifestByName.get(repo.name);
|
|
4653
4687
|
if (!entry) {
|
|
4654
4688
|
flag({
|
|
@@ -4673,7 +4707,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
4673
4707
|
detail: `Set up at haus ${entry.hausVersionAtSetup}, current is ${currentVersion} \u2014 re-run setup.`
|
|
4674
4708
|
});
|
|
4675
4709
|
}
|
|
4676
|
-
if (!
|
|
4710
|
+
if (!existsSync3(claudePath(repoRoot))) {
|
|
4677
4711
|
flag({
|
|
4678
4712
|
repo: repo.name,
|
|
4679
4713
|
kind: "missing-claude",
|
|
@@ -4681,7 +4715,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
4681
4715
|
});
|
|
4682
4716
|
}
|
|
4683
4717
|
const lock = await checkLock(repoRoot);
|
|
4684
|
-
if (!
|
|
4718
|
+
if (!existsSync3(hausPath(repoRoot, "haus.lock.json"))) {
|
|
4685
4719
|
flag({
|
|
4686
4720
|
repo: repo.name,
|
|
4687
4721
|
kind: "missing-lock",
|
|
@@ -4721,11 +4755,11 @@ function emit(args) {
|
|
|
4721
4755
|
}
|
|
4722
4756
|
|
|
4723
4757
|
// src/commands/workspace/setup.ts
|
|
4724
|
-
import { existsSync as
|
|
4725
|
-
import
|
|
4758
|
+
import { existsSync as existsSync4, statSync } from "fs";
|
|
4759
|
+
import path33 from "path";
|
|
4726
4760
|
|
|
4727
4761
|
// src/claude/write-workspace-claude-md.ts
|
|
4728
|
-
import
|
|
4762
|
+
import path32 from "path";
|
|
4729
4763
|
import fs20 from "fs-extra";
|
|
4730
4764
|
function buildWorkspaceImportBlock(client, members) {
|
|
4731
4765
|
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
@@ -4744,7 +4778,7 @@ ${BLOCK_END}`;
|
|
|
4744
4778
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
4745
4779
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
4746
4780
|
const dryRun = opts.dryRun ?? false;
|
|
4747
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
4781
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path32.join(workspaceRoot, "CLAUDE.md");
|
|
4748
4782
|
const prev = await fs20.pathExists(filePath) ? await fs20.readFile(filePath, "utf8") : "";
|
|
4749
4783
|
const next = opts.collision ? `${block}
|
|
4750
4784
|
` : injectHausBlock(prev, block);
|
|
@@ -4770,21 +4804,21 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
4770
4804
|
|
|
4771
4805
|
// src/commands/workspace/setup.ts
|
|
4772
4806
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
4773
|
-
let dir =
|
|
4807
|
+
let dir = path33.resolve(start);
|
|
4774
4808
|
for (; ; ) {
|
|
4775
|
-
if (
|
|
4776
|
-
const parent =
|
|
4777
|
-
if (parent === dir) return
|
|
4809
|
+
if (existsSync4(path33.join(dir, WORKSPACE_FILE))) return dir;
|
|
4810
|
+
const parent = path33.dirname(dir);
|
|
4811
|
+
if (parent === dir) return path33.resolve(start);
|
|
4778
4812
|
dir = parent;
|
|
4779
4813
|
}
|
|
4780
4814
|
}
|
|
4781
4815
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
4782
|
-
return
|
|
4816
|
+
return path33.resolve(workspaceRoot, repoPath) === path33.resolve(workspaceRoot);
|
|
4783
4817
|
}
|
|
4784
4818
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
4785
4819
|
const mode = options.mode ?? "fast";
|
|
4786
4820
|
const apply = options.write ?? false;
|
|
4787
|
-
const configText = await readText(
|
|
4821
|
+
const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
|
|
4788
4822
|
if (!configText) {
|
|
4789
4823
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
4790
4824
|
process.exitCode = 1;
|
|
@@ -4801,11 +4835,11 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
4801
4835
|
const statuses = [];
|
|
4802
4836
|
const aggregateInputs = [];
|
|
4803
4837
|
for (const repo of repos) {
|
|
4804
|
-
const repoRoot =
|
|
4838
|
+
const repoRoot = path33.resolve(workspaceRoot, repo.path);
|
|
4805
4839
|
log(`
|
|
4806
4840
|
\u2192 ${repo.name} (${repo.path})`);
|
|
4807
4841
|
try {
|
|
4808
|
-
if (!
|
|
4842
|
+
if (!existsSync4(repoRoot) || !statSync(repoRoot).isDirectory()) {
|
|
4809
4843
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
4810
4844
|
}
|
|
4811
4845
|
const res = await runSetupCore(repoRoot, {
|
|
@@ -4859,7 +4893,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
4859
4893
|
if (apply && !options.dryRun) {
|
|
4860
4894
|
const statusByName = new Map(statuses.map((s) => [s.name, s]));
|
|
4861
4895
|
const prior = await readManifest2(workspaceRoot);
|
|
4862
|
-
if (!prior &&
|
|
4896
|
+
if (!prior && existsSync4(manifestPath(workspaceRoot))) {
|
|
4863
4897
|
warn(
|
|
4864
4898
|
"Existing workspace.manifest.json is unreadable \u2014 prior per-repo state will not be carried forward."
|
|
4865
4899
|
);
|
|
@@ -4870,7 +4904,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
4870
4904
|
const status = statusByName.get(repo.name);
|
|
4871
4905
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
4872
4906
|
if (status?.status === "ok") {
|
|
4873
|
-
const lock = await checkLock(
|
|
4907
|
+
const lock = await checkLock(path33.resolve(workspaceRoot, repo.path));
|
|
4874
4908
|
manifestRepos.push({
|
|
4875
4909
|
name: repo.name,
|
|
4876
4910
|
path: repo.path,
|
|
@@ -4950,7 +4984,7 @@ relationships: []
|
|
|
4950
4984
|
log("Workspace initialized.");
|
|
4951
4985
|
}
|
|
4952
4986
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
4953
|
-
const configText = await readText(
|
|
4987
|
+
const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
|
|
4954
4988
|
if (!configText) {
|
|
4955
4989
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
4956
4990
|
process.exitCode = 1;
|
|
@@ -4971,8 +5005,8 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
4971
5005
|
}
|
|
4972
5006
|
const inputs = [];
|
|
4973
5007
|
for (const repo of config2.repos) {
|
|
4974
|
-
const repoRoot =
|
|
4975
|
-
if (!
|
|
5008
|
+
const repoRoot = path34.resolve(workspaceRoot, repo.path);
|
|
5009
|
+
if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
|
|
4976
5010
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
4977
5011
|
}
|
|
4978
5012
|
const result = await scanProject(repoRoot, "fast");
|
|
@@ -5022,7 +5056,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
5022
5056
|
// src/cli.ts
|
|
5023
5057
|
function cliVersion() {
|
|
5024
5058
|
try {
|
|
5025
|
-
const pkgPath =
|
|
5059
|
+
const pkgPath = path35.join(packageRoot(), "package.json");
|
|
5026
5060
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5027
5061
|
return pkg.version ?? "0.0.0";
|
|
5028
5062
|
} catch {
|
|
@@ -5032,7 +5066,7 @@ function cliVersion() {
|
|
|
5032
5066
|
var program = new Command();
|
|
5033
5067
|
function validateRuntimeNodeVersion() {
|
|
5034
5068
|
try {
|
|
5035
|
-
const pkgPath =
|
|
5069
|
+
const pkgPath = path35.join(packageRoot(), "package.json");
|
|
5036
5070
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5037
5071
|
const requiredRange = pkg.engines?.node;
|
|
5038
5072
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -5061,6 +5095,9 @@ program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo)
|
|
|
5061
5095
|
program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
|
|
5062
5096
|
program.command("context").option("--task <task>").option("--from-hook").option("--json").option("--verbose").action(runContext);
|
|
5063
5097
|
program.command("init").option("--json").action(runInit);
|
|
5098
|
+
program.command("clone").description("Clone a single git repository by URL").argument("<url>", "Git URL to clone").argument("[dir]", "Target directory (default: repo name derived from the URL)").option("--dry-run", "Print what would happen without cloning").action(
|
|
5099
|
+
(url, dir, opts) => runClone(url, { dir, dryRun: opts.dryRun })
|
|
5100
|
+
);
|
|
5064
5101
|
program.command("refresh").action(runRefresh);
|
|
5065
5102
|
program.command("catalog-audit").action(runCatalogAudit);
|
|
5066
5103
|
program.command("validate-catalog").argument("[manifest]").action(runValidateCatalog);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Clone repositories for this project. Per-repo setup (install, Docker, `.env`) is a separate step that isn't wired yet — this command only gets repos onto disk.
|
|
2
|
+
|
|
3
|
+
Cloning a single repo is always `haus clone <url> [dir]`. This command picks _which_ repos to clone and runs that primitive for each. There are two modes, chosen by whether a name was given.
|
|
4
|
+
|
|
5
|
+
**Always ask before cloning — never assume.** The user may already have the repos on disk. Do not start cloning until they have confirmed. A missing `repos.local.json` does **not** mean they want a fresh clone; it just means nothing is recorded yet — you must still ask.
|
|
6
|
+
|
|
7
|
+
## Mode A — a project name was given (`project:clone <name>`)
|
|
8
|
+
|
|
9
|
+
Find one repo by name on GitHub and clone it. Does **not** require a workspace or `repos.manifest.json`.
|
|
10
|
+
|
|
11
|
+
1. Make sure GitHub CLI is ready: run `gh auth status`. If not authenticated, tell the user to run `gh auth login` and stop.
|
|
12
|
+
2. Scope the search to repos the user owns or belongs to: get their login with `gh api user -q .login` and their orgs with `gh api user/orgs -q '.[].login'`.
|
|
13
|
+
3. Search by name, passing one `--owner` per login/org from step 2:
|
|
14
|
+
`gh search repos "<name>" --match name --limit 10 --json fullName,description,url,isPrivate,pushedAt --owner <login> [--owner <org> …]`
|
|
15
|
+
If that returns nothing, retry **without** `--owner` (a broader, all-of-GitHub search) and tell the user you widened it.
|
|
16
|
+
4. Decide from the results:
|
|
17
|
+
- **0 matches** — tell the user nothing matched `<name>`; offer to try a different name or broaden. Stop.
|
|
18
|
+
- **1 match** — show `fullName` + description and ask the user to confirm before cloning.
|
|
19
|
+
- **2+ matches** — use `AskUserQuestion` to let the user pick which repo (list each `fullName` with its description; private repos noted). Include a final option like "None of these — search again / broaden" so they can refine.
|
|
20
|
+
5. Once the user has confirmed both the repo and where it should land, clone it with `haus clone <url> [dir]` using the `url` from the search result (default target is a folder named after the repo under the current directory). Quote the exact command first.
|
|
21
|
+
6. Report the result (cloned / skipped if already present / failed). Remind the user that installing dependencies and configuring the repo is still a manual step for now.
|
|
22
|
+
|
|
23
|
+
## Mode B — no name was given (`project:clone`)
|
|
24
|
+
|
|
25
|
+
Clone a whole **workspace** from its manifest. Workspace-only (a `repos.manifest.json` at the repo root); for a lone repo without a manifest, use Mode A with a name instead.
|
|
26
|
+
|
|
27
|
+
1. Confirm `repos.manifest.json` exists at the workspace root. If not, tell the user this mode is for multi-repo workspaces (or they can pass a `<name>` to clone a single repo) and stop.
|
|
28
|
+
2. Read `repos.manifest.json`. Each entry has an `id`, a `folder`, and a git URL (`repo`). If entries have no `repo` URL, ask the user to add them (or supply the URLs) — `haus clone` needs a URL per repo.
|
|
29
|
+
3. Read `repos.local.json` if present — its `pathOverrides` map (`folder` → absolute path) marks repos the user already has locally and does not want re-cloned.
|
|
30
|
+
4. **Always ask first**, via `AskUserQuestion` — never skip this, even when `repos.local.json` is absent or every repo is missing locally:
|
|
31
|
+
- **Clean clone** — clone every manifest repo fresh into its `folder` under the workspace.
|
|
32
|
+
- **I already have some or all of them** — the user has clones elsewhere on disk. Ask where they live, then for each repo found there, record it in `repos.local.json` `pathOverrides` (`folder` → absolute path) so it's reused instead of cloned; clone only the repos that aren't found. (You can match by folder name under the directory they give, confirming each.)
|
|
33
|
+
- **Cancel** — do nothing.
|
|
34
|
+
5. Show the concrete plan before touching anything: list which repos will be cloned (and into which `folder`) and which will be reused/skipped. Get a final go-ahead.
|
|
35
|
+
6. For each repo to clone, run (quoting it first): `haus clone <repo-url> <folder>` from the workspace root. Offer `--dry-run` first if the user wants a preview. If one repo fails, report it and continue to the next.
|
|
36
|
+
7. After the loop, report which repos were cloned, reused (local), skipped (already present), and failed. Remind the user that installing dependencies and configuring each repo (`.env`, services) is still a manual step for now.
|
|
@@ -21,15 +21,16 @@ The unprefixed verbs (`update`, `catalog`, `install`, `uninstall`) act on **this
|
|
|
21
21
|
haus install** (`~/.claude`, npm) — they manage the haus tool itself, like `npm install -g`.
|
|
22
22
|
The short legacy aliases still work but the names below are canonical.
|
|
23
23
|
|
|
24
|
-
| Task name (legacy aliases) | Command | Scope | What it does
|
|
25
|
-
| ----------------------------------------------------------------- | ----------------------- | ------- |
|
|
26
|
-
| `project:init` (`setup`, `init`) | _Setup procedure below_ | project | First-time setup of an **existing** repo: adds AI skills, commands, workflow + project docs
|
|
27
|
-
| `project:
|
|
28
|
-
| `project:
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
24
|
+
| Task name (legacy aliases) | Command | Scope | What it does |
|
|
25
|
+
| ----------------------------------------------------------------- | ----------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
26
|
+
| `project:init` (`setup`, `init`) | _Setup procedure below_ | project | First-time setup of an **existing** repo: adds AI skills, commands, workflow + project docs |
|
|
27
|
+
| `project:clone [name]` (`clone`) | _Clone procedure below_ | project | No name: clone a **workspace**'s repos from `repos.manifest.json`. With a `name`: find & clone one repo by name from GitHub |
|
|
28
|
+
| `project:refresh` (`apply`, `refresh`, `claude-md`, `regenerate`) | `haus apply --write` | project | Re-run setup / refresh `.claude/` context + regenerate root `CLAUDE.md` import block |
|
|
29
|
+
| `project:doctor` (`doctor`, `check`) | `haus doctor` | project | Check for install drift |
|
|
30
|
+
| `update` (`upgrade`) | `haus update` | global | Update npm package + catalog + `~/.claude/` (also refreshes this project) |
|
|
31
|
+
| `catalog` | `haus update` | global | Fetch latest catalog (same command as update) |
|
|
32
|
+
| `install` (`global`) | `haus install` | global | Seed `~/.claude/` with haus-owned files |
|
|
33
|
+
| `uninstall` | `haus uninstall` | global | Remove all haus global files from `~/.claude/` |
|
|
33
34
|
|
|
34
35
|
## Step 1 — Determine the task
|
|
35
36
|
|
|
@@ -48,6 +49,8 @@ Options:
|
|
|
48
49
|
(haus update — checks npm for new version, fetches catalog, refreshes ~/.claude/)
|
|
49
50
|
4. [global] catalog — fetch catalog updates only
|
|
50
51
|
(haus update — same command; pulls latest workflow templates and lockfile)
|
|
52
|
+
5. [project] project:clone [name] — clone repos
|
|
53
|
+
(no name: clone a workspace from repos.manifest.json; with a name: find & clone one repo by name from GitHub)
|
|
51
54
|
```
|
|
52
55
|
|
|
53
56
|
Map the user's selection to the command from the alias table, then continue to Step 2.
|
|
@@ -58,6 +61,8 @@ Run the mapped command via Bash. Quote the exact command you are running before
|
|
|
58
61
|
|
|
59
62
|
**Exception — `project:init` (`setup` / `init`):** this maps to a multi-step procedure, not a single command. Do not run a bare `haus init`. Skip to **Setup (`project:init`)** under Step 3 and follow it.
|
|
60
63
|
|
|
64
|
+
**Exception — `project:clone` (`clone`):** this asks the user a question before running, so it is a short procedure too. Skip to **Clone (`project:clone`)** under Step 3 and follow it.
|
|
65
|
+
|
|
61
66
|
## Step 3 — Post-run steps
|
|
62
67
|
|
|
63
68
|
After the command completes, follow the relevant post-run steps below.
|
|
@@ -67,6 +72,10 @@ After the command completes, follow the relevant post-run steps below.
|
|
|
67
72
|
1. Open and follow `~/.claude/commands/haus-setup.md` — the installed `haus-setup` command (in some projects also `.claude/commands/haus-setup.md`). Run every step in order. It detects the stack, asks the guided questions, runs `haus apply --write` (scaffolding, skills, commands, rules, docs skill), writes the **project docs** (`CLAUDE.md` body + `docs/`) and `.haus-workflow/deep-context.json`, runs `haus recommend`, applies the newly-matched helpers, and confirms.
|
|
68
73
|
2. Then fill `.haus-workflow/workflow-config.md` — replace every placeholder (`TODO`, `n/a`, empty): test/lint/typecheck/build commands (check `package.json`), docs paths, validation library, pre-commit tool, highest-stakes logic (ask if unclear). Leave none.
|
|
69
74
|
|
|
75
|
+
### Clone (`project:clone`)
|
|
76
|
+
|
|
77
|
+
1. Open and follow `~/.claude/commands/haus-clone.md` — the installed `haus-clone` command. With a `name` argument it finds and clones one matching repo from GitHub; with no argument it clones a workspace's repos from `repos.manifest.json`. Per-repo setup (install, Docker, env) is a separate step that isn't wired yet, so just get the repos in place for now.
|
|
78
|
+
|
|
70
79
|
### After `haus apply --write`
|
|
71
80
|
|
|
72
81
|
Verify that the root `CLAUDE.md` imports all three haus files:
|