@haus-tech/haus-workflow 0.9.0 → 0.10.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 +10 -0
- package/README.md +13 -16
- package/dist/cli.js +97 -64
- package/package.json +1 -2
- package/docs/user-guide.md +0 -176
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.9.0...v0.10.0) (2026-05-29)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* Add config command to manage hooks ([1f2c7b4](https://github.com/WeAreHausTech/haus-workflow/commit/1f2c7b4ced3437ef4468759ec7bfc4d8adbf3efc))
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* address PR review — remove stale module refs, fix plugin-era wording in docs and commands ([6520321](https://github.com/WeAreHausTech/haus-workflow/commit/6520321e1790a304036984c387fdf59d4febe3dd))
|
|
12
|
+
|
|
3
13
|
## [0.9.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.8.0...v0.9.0) (2026-05-28)
|
|
4
14
|
|
|
5
15
|
### Features
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# haus
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CLI that scans a project, recommends AI context assets for the stack, and writes controlled outputs into `.claude/` and `.haus-workflow/`.
|
|
4
4
|
|
|
5
|
-
> **Internal Haus tool.**
|
|
5
|
+
> **Internal Haus tool.**
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -27,7 +27,7 @@ Run once inside each project:
|
|
|
27
27
|
haus init
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Scans the repo, recommends context assets, and writes `.claude/` and `.haus-workflow/`.
|
|
31
31
|
|
|
32
32
|
---
|
|
33
33
|
|
|
@@ -35,35 +35,32 @@ This scans the repo, recommends context assets, and writes `.claude/` and `.haus
|
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
37
|
haus init # first-run setup (scan → recommend → apply)
|
|
38
|
-
haus setup-project # re-run setup on
|
|
38
|
+
haus setup-project # re-run setup on existing project
|
|
39
|
+
haus scan # scan repo and write context-map
|
|
40
|
+
haus recommend # score and recommend catalog items
|
|
39
41
|
haus apply --dry-run # preview what would be written
|
|
40
42
|
haus apply --write # write .claude/ files
|
|
41
43
|
haus update # sync remote catalog + refresh lockfile
|
|
42
44
|
haus update --check # check for updates without applying
|
|
43
45
|
haus doctor # health check: hooks, CLAUDE.md, catalog cache
|
|
46
|
+
haus config # manage hook configuration
|
|
47
|
+
haus memory # view project memory store
|
|
48
|
+
haus guard # test bash/file-access guards
|
|
44
49
|
haus uninstall # remove Haus-managed files from ~/.claude/
|
|
45
50
|
```
|
|
46
51
|
|
|
47
52
|
---
|
|
48
53
|
|
|
49
|
-
##
|
|
54
|
+
## Development
|
|
50
55
|
|
|
51
56
|
```bash
|
|
52
57
|
yarn install
|
|
53
58
|
yarn verify # typecheck + lint + build + test
|
|
59
|
+
yarn dev <cmd> # run CLI without building (tsx)
|
|
54
60
|
```
|
|
55
61
|
|
|
56
|
-
|
|
62
|
+
### Internal docs
|
|
57
63
|
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
## Docs
|
|
61
|
-
|
|
62
|
-
- [User guide](docs/user-guide.md)
|
|
63
64
|
- [Architecture](docs/architecture.md)
|
|
64
|
-
- [
|
|
65
|
-
- [Global install layout](docs/global-install.md)
|
|
66
|
-
- [Generated files](docs/generated-files.md)
|
|
67
|
-
- [Updates and lockfile](docs/updates.md)
|
|
65
|
+
- [CLI reference](docs/cli.md)
|
|
68
66
|
- [Security](docs/security.md)
|
|
69
|
-
- [Memory](docs/memory.md)
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { readFileSync as readFileSync3 } from "fs";
|
|
5
|
-
import
|
|
5
|
+
import path27 from "path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/commands/apply.ts
|
|
@@ -903,6 +903,34 @@ async function runCatalogAudit() {
|
|
|
903
903
|
log("Catalog audit passed.");
|
|
904
904
|
}
|
|
905
905
|
|
|
906
|
+
// src/commands/config.ts
|
|
907
|
+
import path12 from "path";
|
|
908
|
+
var CONFIG_PATH2 = ".haus-workflow/config.json";
|
|
909
|
+
var HOOK_ALIASES = {
|
|
910
|
+
"hook.context": "context",
|
|
911
|
+
"hook.memory": "memoryInject"
|
|
912
|
+
};
|
|
913
|
+
async function runConfig(key, action) {
|
|
914
|
+
const hookKey = HOOK_ALIASES[key];
|
|
915
|
+
if (!hookKey) {
|
|
916
|
+
throw new Error(`Unknown config key "${key}". Valid keys: ${Object.keys(HOOK_ALIASES).join(", ")}`);
|
|
917
|
+
}
|
|
918
|
+
const root = process.cwd();
|
|
919
|
+
const configPath = path12.join(root, CONFIG_PATH2);
|
|
920
|
+
const existing = await readJson(configPath);
|
|
921
|
+
const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
|
|
922
|
+
cfg.hooks ??= {};
|
|
923
|
+
cfg.hooks[hookKey] ??= {};
|
|
924
|
+
if (action === "status") {
|
|
925
|
+
const enabled = cfg.hooks[hookKey]?.enabled === true;
|
|
926
|
+
log(`${key}: ${enabled ? "enabled" : "disabled"}`);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
cfg.hooks[hookKey].enabled = action === "enable";
|
|
930
|
+
await writeJson(configPath, cfg);
|
|
931
|
+
log(`${key} ${action}d`);
|
|
932
|
+
}
|
|
933
|
+
|
|
906
934
|
// src/recommender/explain-recommendation.ts
|
|
907
935
|
function normalizeRecommendation(input2) {
|
|
908
936
|
const recommended = (input2.recommended ?? []).map((item) => {
|
|
@@ -1241,7 +1269,7 @@ function computeRuleIntents(rule) {
|
|
|
1241
1269
|
|
|
1242
1270
|
// src/scanner/scan-project.ts
|
|
1243
1271
|
import { readFile } from "fs/promises";
|
|
1244
|
-
import
|
|
1272
|
+
import path14 from "path";
|
|
1245
1273
|
|
|
1246
1274
|
// src/utils/audit-checks.ts
|
|
1247
1275
|
function isRecord(v) {
|
|
@@ -1268,7 +1296,7 @@ function compareVersions(a, b) {
|
|
|
1268
1296
|
}
|
|
1269
1297
|
|
|
1270
1298
|
// src/scanner/detect-package-manager.ts
|
|
1271
|
-
import
|
|
1299
|
+
import path13 from "path";
|
|
1272
1300
|
import fs9 from "fs-extra";
|
|
1273
1301
|
function detectPackageManager(root, packageManagerField) {
|
|
1274
1302
|
const field = String(packageManagerField ?? "").trim();
|
|
@@ -1287,9 +1315,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
1287
1315
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
1288
1316
|
return "unknown";
|
|
1289
1317
|
}
|
|
1290
|
-
if (fs9.existsSync(
|
|
1291
|
-
if (fs9.existsSync(
|
|
1292
|
-
if (fs9.existsSync(
|
|
1318
|
+
if (fs9.existsSync(path13.join(root, "yarn.lock"))) return "yarn";
|
|
1319
|
+
if (fs9.existsSync(path13.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
1320
|
+
if (fs9.existsSync(path13.join(root, "package-lock.json"))) return "npm";
|
|
1293
1321
|
return "unknown";
|
|
1294
1322
|
}
|
|
1295
1323
|
|
|
@@ -1350,8 +1378,8 @@ function blocked(rel) {
|
|
|
1350
1378
|
return SENSITIVE.some((x) => x.test(rel));
|
|
1351
1379
|
}
|
|
1352
1380
|
async function scanProject(root, mode = "fast") {
|
|
1353
|
-
const pkg = await readJson(
|
|
1354
|
-
const composer = await readJson(
|
|
1381
|
+
const pkg = await readJson(path14.join(root, "package.json"));
|
|
1382
|
+
const composer = await readJson(path14.join(root, "composer.json"));
|
|
1355
1383
|
const files = await listFiles(root, SAFE_FILES);
|
|
1356
1384
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
1357
1385
|
const deps = dependencySet(pkg, composer);
|
|
@@ -1377,7 +1405,7 @@ async function scanProject(root, mode = "fast") {
|
|
|
1377
1405
|
mode,
|
|
1378
1406
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1379
1407
|
root,
|
|
1380
|
-
repoName: String(pkg?.name ??
|
|
1408
|
+
repoName: String(pkg?.name ?? path14.basename(root)),
|
|
1381
1409
|
packageManager,
|
|
1382
1410
|
repoRoles: roles,
|
|
1383
1411
|
confidence: computeConfidence(roles, stacks),
|
|
@@ -1392,7 +1420,7 @@ async function scanProject(root, mode = "fast") {
|
|
|
1392
1420
|
composer: isRecord(composer?.require) ? Object.keys(composer.require) : []
|
|
1393
1421
|
};
|
|
1394
1422
|
const scanHashes = Object.fromEntries(
|
|
1395
|
-
await Promise.all(safeFiles.map(async (f) => [f, hashText(await readFile(
|
|
1423
|
+
await Promise.all(safeFiles.map(async (f) => [f, hashText(await readFile(path14.join(root, f), "utf8"))]))
|
|
1396
1424
|
);
|
|
1397
1425
|
const repoSummary = renderSummary(context);
|
|
1398
1426
|
await writeJson(hausPath(root, "context-map.json"), context);
|
|
@@ -1548,7 +1576,7 @@ async function hasNeedle(root, files, needle) {
|
|
|
1548
1576
|
);
|
|
1549
1577
|
for (const rel of candidates.slice(0, 300)) {
|
|
1550
1578
|
try {
|
|
1551
|
-
const content = await readFile(
|
|
1579
|
+
const content = await readFile(path14.join(root, rel), "utf8");
|
|
1552
1580
|
if (content.includes(needle)) return true;
|
|
1553
1581
|
} catch {
|
|
1554
1582
|
continue;
|
|
@@ -1641,7 +1669,7 @@ async function runContext(options) {
|
|
|
1641
1669
|
}
|
|
1642
1670
|
|
|
1643
1671
|
// src/commands/doctor.ts
|
|
1644
|
-
import
|
|
1672
|
+
import path15 from "path";
|
|
1645
1673
|
import fs10 from "fs-extra";
|
|
1646
1674
|
|
|
1647
1675
|
// src/update/npm-version.ts
|
|
@@ -1709,7 +1737,7 @@ async function runDoctor(options) {
|
|
|
1709
1737
|
const enabled = await isHookEnabled(root, key);
|
|
1710
1738
|
log(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
|
|
1711
1739
|
}
|
|
1712
|
-
const rootClaudeMdPath =
|
|
1740
|
+
const rootClaudeMdPath = path15.join(root, "CLAUDE.md");
|
|
1713
1741
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
1714
1742
|
if (!rootClaudeMdContent) {
|
|
1715
1743
|
warn("- CLAUDE.md: missing (run `haus apply --write` to create)");
|
|
@@ -1729,7 +1757,7 @@ async function runDoctor(options) {
|
|
|
1729
1757
|
warn("- .haus-workflow/haus-way-of-work.md: no HAUS-MANAGED header (user-owned)");
|
|
1730
1758
|
} else {
|
|
1731
1759
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
1732
|
-
const templatePath =
|
|
1760
|
+
const templatePath = path15.join(packageRoot(), "library", "global", "templates", "haus-way-of-work.md");
|
|
1733
1761
|
const templateContent = await readText(templatePath);
|
|
1734
1762
|
if (storedHashMatch && templateContent) {
|
|
1735
1763
|
const currentHash = hashText(templateContent);
|
|
@@ -1767,7 +1795,7 @@ async function runDoctor(options) {
|
|
|
1767
1795
|
log(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
|
|
1768
1796
|
}
|
|
1769
1797
|
}
|
|
1770
|
-
const pkgJson = await readJson(
|
|
1798
|
+
const pkgJson = await readJson(path15.join(packageRoot(), "package.json"));
|
|
1771
1799
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
1772
1800
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
1773
1801
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -1932,7 +1960,7 @@ async function runGuard(kind, _options) {
|
|
|
1932
1960
|
}
|
|
1933
1961
|
|
|
1934
1962
|
// src/commands/init.ts
|
|
1935
|
-
import
|
|
1963
|
+
import path16 from "path";
|
|
1936
1964
|
import fs11 from "fs-extra";
|
|
1937
1965
|
|
|
1938
1966
|
// src/utils/exec.ts
|
|
@@ -1941,6 +1969,7 @@ async function runCommand(command, args = [], options = {}) {
|
|
|
1941
1969
|
try {
|
|
1942
1970
|
const result = await execa(command, args, {
|
|
1943
1971
|
reject: false,
|
|
1972
|
+
// non-zero exits are returned, not thrown
|
|
1944
1973
|
...options
|
|
1945
1974
|
});
|
|
1946
1975
|
return {
|
|
@@ -2454,7 +2483,7 @@ async function runSetupProject(options) {
|
|
|
2454
2483
|
// src/commands/init.ts
|
|
2455
2484
|
async function runInit(options) {
|
|
2456
2485
|
const root = process.cwd();
|
|
2457
|
-
const hausDir =
|
|
2486
|
+
const hausDir = path16.join(root, ".haus-workflow");
|
|
2458
2487
|
const alreadyInit = await fs11.pathExists(hausDir);
|
|
2459
2488
|
if (alreadyInit) {
|
|
2460
2489
|
log("Haus AI already initialized in this project.");
|
|
@@ -2467,7 +2496,7 @@ async function runInit(options) {
|
|
|
2467
2496
|
|
|
2468
2497
|
// src/install/apply.ts
|
|
2469
2498
|
import crypto2 from "crypto";
|
|
2470
|
-
import
|
|
2499
|
+
import path19 from "path";
|
|
2471
2500
|
import fs13 from "fs-extra";
|
|
2472
2501
|
|
|
2473
2502
|
// src/install/header.ts
|
|
@@ -2502,13 +2531,13 @@ ${content}`;
|
|
|
2502
2531
|
|
|
2503
2532
|
// src/install/manifest.ts
|
|
2504
2533
|
import os5 from "os";
|
|
2505
|
-
import
|
|
2534
|
+
import path17 from "path";
|
|
2506
2535
|
var MANIFEST_SCHEMA = "haus-install-manifest/1";
|
|
2507
2536
|
function globalClaudeDir() {
|
|
2508
|
-
return
|
|
2537
|
+
return path17.join(os5.homedir(), ".claude");
|
|
2509
2538
|
}
|
|
2510
2539
|
function hausManifestPath() {
|
|
2511
|
-
return
|
|
2540
|
+
return path17.join(globalClaudeDir(), "haus", "install-manifest.json");
|
|
2512
2541
|
}
|
|
2513
2542
|
async function readManifest() {
|
|
2514
2543
|
return readJson(hausManifestPath());
|
|
@@ -2527,10 +2556,10 @@ function buildManifest(source, files, hooks) {
|
|
|
2527
2556
|
}
|
|
2528
2557
|
|
|
2529
2558
|
// src/install/settings-merge.ts
|
|
2530
|
-
import
|
|
2559
|
+
import path18 from "path";
|
|
2531
2560
|
import fs12 from "fs-extra";
|
|
2532
2561
|
function settingsJsonPath() {
|
|
2533
|
-
return
|
|
2562
|
+
return path18.join(globalClaudeDir(), "settings.json");
|
|
2534
2563
|
}
|
|
2535
2564
|
async function readSettings() {
|
|
2536
2565
|
const parsed = await readJson(settingsJsonPath());
|
|
@@ -2601,7 +2630,7 @@ function hashContent(content) {
|
|
|
2601
2630
|
}
|
|
2602
2631
|
function sourceVersion() {
|
|
2603
2632
|
try {
|
|
2604
|
-
const pkgPath =
|
|
2633
|
+
const pkgPath = path19.join(packageRoot(), "package.json");
|
|
2605
2634
|
const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf8"));
|
|
2606
2635
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
2607
2636
|
} catch {
|
|
@@ -2609,32 +2638,32 @@ function sourceVersion() {
|
|
|
2609
2638
|
}
|
|
2610
2639
|
}
|
|
2611
2640
|
function globalSrcDir() {
|
|
2612
|
-
return
|
|
2641
|
+
return path19.join(packageRoot(), "library", "global");
|
|
2613
2642
|
}
|
|
2614
2643
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
2615
2644
|
const entries = [];
|
|
2616
|
-
const skillsDir =
|
|
2645
|
+
const skillsDir = path19.join(srcDir, "skills");
|
|
2617
2646
|
if (fs13.pathExistsSync(skillsDir)) {
|
|
2618
2647
|
for (const skillName of fs13.readdirSync(skillsDir)) {
|
|
2619
|
-
const skillFile =
|
|
2648
|
+
const skillFile = path19.join(skillsDir, skillName, "SKILL.md");
|
|
2620
2649
|
if (fs13.pathExistsSync(skillFile)) {
|
|
2621
2650
|
entries.push({
|
|
2622
2651
|
stableId: `skill.${skillName}`,
|
|
2623
|
-
srcRelPath:
|
|
2624
|
-
destPath:
|
|
2652
|
+
srcRelPath: path19.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
2653
|
+
destPath: path19.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
2625
2654
|
});
|
|
2626
2655
|
}
|
|
2627
2656
|
}
|
|
2628
2657
|
}
|
|
2629
|
-
const agentsDir =
|
|
2658
|
+
const agentsDir = path19.join(srcDir, "agents");
|
|
2630
2659
|
if (fs13.pathExistsSync(agentsDir)) {
|
|
2631
2660
|
for (const agentFile of fs13.readdirSync(agentsDir)) {
|
|
2632
2661
|
if (!agentFile.endsWith(".md")) continue;
|
|
2633
2662
|
const agentName = agentFile.replace(/\.md$/, "");
|
|
2634
2663
|
entries.push({
|
|
2635
2664
|
stableId: `agent.${agentName}`,
|
|
2636
|
-
srcRelPath:
|
|
2637
|
-
destPath:
|
|
2665
|
+
srcRelPath: path19.join("library", "global", "agents", agentFile),
|
|
2666
|
+
destPath: path19.join(claudeDir, "agents", agentFile)
|
|
2638
2667
|
});
|
|
2639
2668
|
}
|
|
2640
2669
|
}
|
|
@@ -2658,7 +2687,7 @@ async function applyInstall(options = {}) {
|
|
|
2658
2687
|
};
|
|
2659
2688
|
const manifestFiles = [];
|
|
2660
2689
|
for (const entry of sourceFiles) {
|
|
2661
|
-
const srcPath =
|
|
2690
|
+
const srcPath = path19.join(packageRoot(), entry.srcRelPath);
|
|
2662
2691
|
const rawContent = await readText(srcPath);
|
|
2663
2692
|
if (rawContent === void 0) {
|
|
2664
2693
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -2714,7 +2743,7 @@ async function applyInstall(options = {}) {
|
|
|
2714
2743
|
schemaVersion: SCHEMA_VERSION3
|
|
2715
2744
|
});
|
|
2716
2745
|
}
|
|
2717
|
-
const fragmentPath =
|
|
2746
|
+
const fragmentPath = path19.join(srcDir, "settings-fragments", "hooks.json");
|
|
2718
2747
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
2719
2748
|
const settings = await readSettings();
|
|
2720
2749
|
const { settings: mergedSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -2899,12 +2928,12 @@ async function runScan(options) {
|
|
|
2899
2928
|
}
|
|
2900
2929
|
|
|
2901
2930
|
// src/commands/undo.ts
|
|
2902
|
-
import
|
|
2931
|
+
import path20 from "path";
|
|
2903
2932
|
import fs14 from "fs-extra";
|
|
2904
2933
|
var CLAUDE_DIR = ".claude";
|
|
2905
2934
|
async function runUndo(options) {
|
|
2906
2935
|
const root = process.cwd();
|
|
2907
|
-
const targets = [
|
|
2936
|
+
const targets = [path20.join(root, CLAUDE_DIR), path20.join(root, HAUS_DIR)];
|
|
2908
2937
|
const existing = targets.filter((p) => fs14.existsSync(p));
|
|
2909
2938
|
if (existing.length === 0) {
|
|
2910
2939
|
log("Nothing to remove: no .claude/ or .haus-workflow/ in this directory.");
|
|
@@ -2912,7 +2941,7 @@ async function runUndo(options) {
|
|
|
2912
2941
|
}
|
|
2913
2942
|
if (!options.yes) {
|
|
2914
2943
|
const ok = await confirm(
|
|
2915
|
-
`Remove ${existing.map((p) =>
|
|
2944
|
+
`Remove ${existing.map((p) => path20.relative(root, p)).join(" and ")}? This cannot be undone.`
|
|
2916
2945
|
);
|
|
2917
2946
|
if (!ok) {
|
|
2918
2947
|
log("Cancelled.");
|
|
@@ -2921,13 +2950,13 @@ async function runUndo(options) {
|
|
|
2921
2950
|
}
|
|
2922
2951
|
for (const p of existing) {
|
|
2923
2952
|
await fs14.remove(p);
|
|
2924
|
-
log(`Removed ${
|
|
2953
|
+
log(`Removed ${path20.relative(root, p)}`);
|
|
2925
2954
|
}
|
|
2926
2955
|
}
|
|
2927
2956
|
|
|
2928
2957
|
// src/install/uninstall.ts
|
|
2929
2958
|
import crypto3 from "crypto";
|
|
2930
|
-
import
|
|
2959
|
+
import path21 from "path";
|
|
2931
2960
|
import fs15 from "fs-extra";
|
|
2932
2961
|
async function runUninstall(options = {}) {
|
|
2933
2962
|
const { force = false } = options;
|
|
@@ -2955,14 +2984,14 @@ async function runUninstall(options = {}) {
|
|
|
2955
2984
|
continue;
|
|
2956
2985
|
}
|
|
2957
2986
|
await fs15.remove(entry.destPath);
|
|
2958
|
-
await pruneEmptyDir(
|
|
2987
|
+
await pruneEmptyDir(path21.dirname(entry.destPath));
|
|
2959
2988
|
result.deleted.push(entry.destPath);
|
|
2960
2989
|
}
|
|
2961
2990
|
const settings = await readSettings();
|
|
2962
2991
|
const stripped = stripHausHooks(settings);
|
|
2963
2992
|
await writeSettings(stripped);
|
|
2964
2993
|
result.hooksStripped = true;
|
|
2965
|
-
const hausDir =
|
|
2994
|
+
const hausDir = path21.join(globalClaudeDir(), "haus");
|
|
2966
2995
|
const manifestPath = hausManifestPath();
|
|
2967
2996
|
if (fs15.pathExistsSync(manifestPath)) {
|
|
2968
2997
|
await fs15.remove(manifestPath);
|
|
@@ -3007,7 +3036,7 @@ async function runUninstallCommand(options) {
|
|
|
3007
3036
|
}
|
|
3008
3037
|
|
|
3009
3038
|
// src/commands/update.ts
|
|
3010
|
-
import
|
|
3039
|
+
import path23 from "path";
|
|
3011
3040
|
|
|
3012
3041
|
// src/update/diff-generated-files.ts
|
|
3013
3042
|
function diffGeneratedFiles() {
|
|
@@ -3034,7 +3063,7 @@ function summarizeLockDiff(before, after) {
|
|
|
3034
3063
|
|
|
3035
3064
|
// src/update/lockfile.ts
|
|
3036
3065
|
import { mkdir, readFile as readFile2, copyFile } from "fs/promises";
|
|
3037
|
-
import
|
|
3066
|
+
import path22 from "path";
|
|
3038
3067
|
async function checkLock(root) {
|
|
3039
3068
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
3040
3069
|
const hasValidVersions = lock.every((item) => !item.version || normalizeVersion(item.version) !== null);
|
|
@@ -3053,7 +3082,7 @@ async function applyLock(root) {
|
|
|
3053
3082
|
try {
|
|
3054
3083
|
const backupDir = hausPath(root, "backups");
|
|
3055
3084
|
await mkdir(backupDir, { recursive: true });
|
|
3056
|
-
await copyFile(lockPath,
|
|
3085
|
+
await copyFile(lockPath, path22.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
3057
3086
|
} catch {
|
|
3058
3087
|
}
|
|
3059
3088
|
const enriched = await Promise.all(
|
|
@@ -3075,7 +3104,7 @@ function diffLock(before, after) {
|
|
|
3075
3104
|
}
|
|
3076
3105
|
async function hasLocalOverrides(root) {
|
|
3077
3106
|
try {
|
|
3078
|
-
await readFile2(
|
|
3107
|
+
await readFile2(path22.join(root, ".claude", "settings.json"), "utf8");
|
|
3079
3108
|
return true;
|
|
3080
3109
|
} catch {
|
|
3081
3110
|
return false;
|
|
@@ -3087,7 +3116,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
|
3087
3116
|
async function runUpdate(options) {
|
|
3088
3117
|
const root = process.cwd();
|
|
3089
3118
|
if (options.check) {
|
|
3090
|
-
const pkgJson2 = await readJson(
|
|
3119
|
+
const pkgJson2 = await readJson(path23.join(packageRoot(), "package.json"));
|
|
3091
3120
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
3092
3121
|
const [status, npmVersion, latestCatalogTag] = await Promise.all([
|
|
3093
3122
|
checkLock(root),
|
|
@@ -3114,7 +3143,7 @@ async function runUpdate(options) {
|
|
|
3114
3143
|
if (!status.ok) process.exitCode = 1;
|
|
3115
3144
|
return;
|
|
3116
3145
|
}
|
|
3117
|
-
const pkgJson = await readJson(
|
|
3146
|
+
const pkgJson = await readJson(path23.join(packageRoot(), "package.json"));
|
|
3118
3147
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
3119
3148
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
3120
3149
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -3145,12 +3174,12 @@ async function runUpdate(options) {
|
|
|
3145
3174
|
|
|
3146
3175
|
// src/commands/validate-catalog.ts
|
|
3147
3176
|
import fs16 from "fs";
|
|
3148
|
-
import
|
|
3177
|
+
import path25 from "path";
|
|
3149
3178
|
|
|
3150
3179
|
// src/catalog/allowed-stacks.ts
|
|
3151
|
-
import
|
|
3180
|
+
import path24 from "path";
|
|
3152
3181
|
async function readAllowedStacks(root) {
|
|
3153
|
-
const data = await readJson(
|
|
3182
|
+
const data = await readJson(path24.join(root, "library", "catalog", "allowed-stacks.json"));
|
|
3154
3183
|
return data?.stacks ?? [];
|
|
3155
3184
|
}
|
|
3156
3185
|
|
|
@@ -3249,11 +3278,11 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
3249
3278
|
const failures = [];
|
|
3250
3279
|
for (const item of items) {
|
|
3251
3280
|
if (!item.path) continue;
|
|
3252
|
-
const absPath =
|
|
3281
|
+
const absPath = path25.join(manifestDir, item.path);
|
|
3253
3282
|
if (item.type === "skill") {
|
|
3254
|
-
const skillMd =
|
|
3283
|
+
const skillMd = path25.join(absPath, "SKILL.md");
|
|
3255
3284
|
if (!fs16.existsSync(skillMd)) {
|
|
3256
|
-
failures.push(`${item.id}: missing ${
|
|
3285
|
+
failures.push(`${item.id}: missing ${path25.relative(manifestDir, skillMd)}`);
|
|
3257
3286
|
continue;
|
|
3258
3287
|
}
|
|
3259
3288
|
const text = fs16.readFileSync(skillMd, "utf8");
|
|
@@ -3286,11 +3315,11 @@ function auditMarkdownContent(manifestDir) {
|
|
|
3286
3315
|
const failures = [];
|
|
3287
3316
|
const dirs = ["skills", "agents"];
|
|
3288
3317
|
for (const dir of dirs) {
|
|
3289
|
-
const abs =
|
|
3318
|
+
const abs = path25.join(manifestDir, dir);
|
|
3290
3319
|
if (!fs16.existsSync(abs)) continue;
|
|
3291
3320
|
walkMd(abs, (file) => {
|
|
3292
3321
|
const text = fs16.readFileSync(file, "utf8");
|
|
3293
|
-
const rel =
|
|
3322
|
+
const rel = path25.relative(manifestDir, file);
|
|
3294
3323
|
const lines = text.split(/\r?\n/);
|
|
3295
3324
|
for (let i = 0; i < lines.length; i++) {
|
|
3296
3325
|
const line = lines[i] ?? "";
|
|
@@ -3310,7 +3339,7 @@ function auditMarkdownContent(manifestDir) {
|
|
|
3310
3339
|
}
|
|
3311
3340
|
function walkMd(dir, fn) {
|
|
3312
3341
|
for (const entry of fs16.readdirSync(dir, { withFileTypes: true })) {
|
|
3313
|
-
const full =
|
|
3342
|
+
const full = path25.join(dir, entry.name);
|
|
3314
3343
|
if (entry.isDirectory()) walkMd(full, fn);
|
|
3315
3344
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
3316
3345
|
}
|
|
@@ -3321,8 +3350,8 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3321
3350
|
process.exitCode = 1;
|
|
3322
3351
|
return;
|
|
3323
3352
|
}
|
|
3324
|
-
const abs =
|
|
3325
|
-
const manifestDir =
|
|
3353
|
+
const abs = path25.resolve(process.cwd(), manifestPath);
|
|
3354
|
+
const manifestDir = path25.dirname(abs);
|
|
3326
3355
|
const data = await readJson(abs);
|
|
3327
3356
|
if (!data?.items) {
|
|
3328
3357
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -3355,7 +3384,7 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3355
3384
|
}
|
|
3356
3385
|
|
|
3357
3386
|
// src/commands/workspace.ts
|
|
3358
|
-
import
|
|
3387
|
+
import path26 from "path";
|
|
3359
3388
|
import YAML from "yaml";
|
|
3360
3389
|
async function runWorkspace(action) {
|
|
3361
3390
|
if (action === "init") {
|
|
@@ -3378,8 +3407,8 @@ relationships: []
|
|
|
3378
3407
|
process.exitCode = 1;
|
|
3379
3408
|
return;
|
|
3380
3409
|
}
|
|
3381
|
-
const
|
|
3382
|
-
const repos =
|
|
3410
|
+
const config2 = YAML.parse(configText);
|
|
3411
|
+
const repos = config2.repos ?? [];
|
|
3383
3412
|
if (repos.length === 0) {
|
|
3384
3413
|
error("No repos configured in haus.workspace.yaml.");
|
|
3385
3414
|
process.exitCode = 1;
|
|
@@ -3388,7 +3417,7 @@ relationships: []
|
|
|
3388
3417
|
const summaries = [];
|
|
3389
3418
|
const ownership = {};
|
|
3390
3419
|
for (const repo of repos) {
|
|
3391
|
-
const repoRoot =
|
|
3420
|
+
const repoRoot = path26.resolve(process.cwd(), repo.path);
|
|
3392
3421
|
const result = await scanProject(repoRoot, "fast");
|
|
3393
3422
|
summaries.push({
|
|
3394
3423
|
name: repo.name,
|
|
@@ -3424,7 +3453,7 @@ ${summaries.map(
|
|
|
3424
3453
|
// src/cli.ts
|
|
3425
3454
|
function cliVersion() {
|
|
3426
3455
|
try {
|
|
3427
|
-
const pkgPath =
|
|
3456
|
+
const pkgPath = path27.join(packageRoot(), "package.json");
|
|
3428
3457
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
3429
3458
|
return pkg.version ?? "0.0.0";
|
|
3430
3459
|
} catch {
|
|
@@ -3434,7 +3463,7 @@ function cliVersion() {
|
|
|
3434
3463
|
var program = new Command();
|
|
3435
3464
|
function validateRuntimeNodeVersion() {
|
|
3436
3465
|
try {
|
|
3437
|
-
const pkgPath =
|
|
3466
|
+
const pkgPath = path27.join(packageRoot(), "package.json");
|
|
3438
3467
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
3439
3468
|
const requiredRange = pkg.engines?.node;
|
|
3440
3469
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -3471,6 +3500,10 @@ memory.command("promote").action(() => runMemory("promote", {}));
|
|
|
3471
3500
|
var guard = program.command("guard");
|
|
3472
3501
|
guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
|
|
3473
3502
|
guard.command("bash").option("--from-hook").action((opts) => runGuard("bash", opts));
|
|
3503
|
+
var config = program.command("config");
|
|
3504
|
+
config.command("enable <key>").description("Enable a hook (hook.context, hook.memory)").action((key) => runConfig(key, "enable"));
|
|
3505
|
+
config.command("disable <key>").description("Disable a hook (hook.context, hook.memory)").action((key) => runConfig(key, "disable"));
|
|
3506
|
+
config.command("status <key>").description("Show current state of a hook (hook.context, hook.memory)").action((key) => runConfig(key, "status"));
|
|
3474
3507
|
var workspace = program.command("workspace");
|
|
3475
3508
|
workspace.command("init").action(() => runWorkspace("init"));
|
|
3476
3509
|
workspace.command("scan").action(() => runWorkspace("scan"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haus-tech/haus-workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Haus AI workflow CLI for Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
"library/global",
|
|
17
17
|
"library/catalog",
|
|
18
18
|
"tests/fixtures/catalog",
|
|
19
|
-
"docs/user-guide.md",
|
|
20
19
|
"README.md",
|
|
21
20
|
"CHANGELOG.md",
|
|
22
21
|
"LICENSE",
|
package/docs/user-guide.md
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
# Haus AI User Guide
|
|
2
|
-
|
|
3
|
-
This guide shows how to use `haus` in a real project, even if you are not a developer.
|
|
4
|
-
|
|
5
|
-
## What Haus AI does
|
|
6
|
-
|
|
7
|
-
Haus AI scans your project, recommends context files/rules, then writes controlled files so Claude works with safer, stack-aware guidance.
|
|
8
|
-
|
|
9
|
-
Main output folders:
|
|
10
|
-
|
|
11
|
-
- `./.claude` (Claude settings/rules/commands)
|
|
12
|
-
- `./.haus-workflow` (scan/recommendation/lock/memory metadata)
|
|
13
|
-
|
|
14
|
-
## Before you start
|
|
15
|
-
|
|
16
|
-
You need:
|
|
17
|
-
|
|
18
|
-
- a project folder on your machine
|
|
19
|
-
- Node.js 22+ (`node --version`)
|
|
20
|
-
- terminal access
|
|
21
|
-
|
|
22
|
-
Check Node:
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
node --version
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
If version is below 22, install/update Node first.
|
|
29
|
-
|
|
30
|
-
## Install Haus AI
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
npm install -g @haus-tech/haus-workflow
|
|
34
|
-
haus --help
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Seed `~/.claude/` with Haus skills, agents, and hooks (once per machine):
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
haus install
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### If you switch Node versions often (nvm, Herd, Volta…)
|
|
44
|
-
|
|
45
|
-
`npm install -g` binds to the currently active Node version. Switch Node → `haus` disappears. Two options:
|
|
46
|
-
|
|
47
|
-
1. **Re-install per version.** When you change Node, carry globals forward:
|
|
48
|
-
```bash
|
|
49
|
-
nvm install <new-version> --reinstall-packages-from=current
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
2. **Use a shell alias.** No per-version install needed:
|
|
53
|
-
```bash
|
|
54
|
-
echo 'alias haus="node $(npm root -g)/@haus-tech/haus-workflow/dist/cli.js"' >> ~/.zshrc
|
|
55
|
-
source ~/.zshrc
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Use Haus in a project
|
|
59
|
-
|
|
60
|
-
Move terminal to project root (folder that contains your app code), then run:
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
haus setup-project
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Setup modes:
|
|
67
|
-
|
|
68
|
-
- guided: asks simple onboarding questions
|
|
69
|
-
- fast: minimal prompts, default flow
|
|
70
|
-
|
|
71
|
-
## Typical daily workflow
|
|
72
|
-
|
|
73
|
-
### 1) Scan project
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
haus scan --json
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Writes project detection outputs to `./.haus-workflow/*`.
|
|
80
|
-
|
|
81
|
-
### 2) Generate recommendations
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
haus recommend --json
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Creates `./.haus-workflow/recommendation.json` with selected and skipped items, confidence, and reasons.
|
|
88
|
-
|
|
89
|
-
### 3) Preview generated changes
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
haus apply --dry-run
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Shows planned files without writing.
|
|
96
|
-
|
|
97
|
-
### 4) Apply generated files
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
haus apply --write
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
Writes generated files and reports overwrite summaries with concise diff counts.
|
|
104
|
-
|
|
105
|
-
### 5) Verify setup health
|
|
106
|
-
|
|
107
|
-
```bash
|
|
108
|
-
haus doctor
|
|
109
|
-
haus doctor --hooks
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
`--hooks` checks that project hook settings still match the hook contract.
|
|
113
|
-
|
|
114
|
-
## Update flow
|
|
115
|
-
|
|
116
|
-
Check update state:
|
|
117
|
-
|
|
118
|
-
```bash
|
|
119
|
-
haus update --check
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Apply lock refresh:
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
haus update
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
Update behavior:
|
|
129
|
-
|
|
130
|
-
- preserves local `.claude` overrides
|
|
131
|
-
- backs up lockfile under `./.haus-workflow/backups`
|
|
132
|
-
- prints unified lockfile diff summary
|
|
133
|
-
|
|
134
|
-
## Memory commands
|
|
135
|
-
|
|
136
|
-
```bash
|
|
137
|
-
haus memory status
|
|
138
|
-
haus memory add "Use explicit transaction boundaries in checkout service"
|
|
139
|
-
haus memory inject --task "review checkout flow"
|
|
140
|
-
haus memory promote
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
Memory is local-only in `./.haus-workflow/memory`.
|
|
144
|
-
|
|
145
|
-
## Explain/context commands
|
|
146
|
-
|
|
147
|
-
Use when you need to understand why rules were selected:
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
haus explain-recommendation --json
|
|
151
|
-
haus context --task "build shipping plugin" --json
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## Claude slash-command usage
|
|
155
|
-
|
|
156
|
-
After `haus apply --write`, command docs are generated in:
|
|
157
|
-
|
|
158
|
-
- `./.claude/commands/haus-doctor.md`
|
|
159
|
-
- `./.claude/commands/haus-review.md`
|
|
160
|
-
|
|
161
|
-
Some environments expose these as slash commands. If not, run the CLI commands directly.
|
|
162
|
-
|
|
163
|
-
## If something fails
|
|
164
|
-
|
|
165
|
-
- `haus: command not found` -> run `npm install -g @haus-tech/haus-workflow` or check Node version
|
|
166
|
-
- Node engine error -> switch to Node 22+
|
|
167
|
-
- hook mismatch in doctor -> run `haus apply --write` again
|
|
168
|
-
- wrong project scanned -> `cd` into correct project root, rerun
|
|
169
|
-
|
|
170
|
-
## Remove generated setup
|
|
171
|
-
|
|
172
|
-
```bash
|
|
173
|
-
haus undo --yes
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
Removes `./.claude` and `./.haus-workflow` in current project.
|