@haus-tech/haus-workflow 0.11.1 → 0.12.1
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 +36 -8
- package/README.md +40 -9
- package/dist/cli.js +1444 -822
- package/library/catalog/manifest.json +90 -74
- package/library/catalog/{allowed-stacks.json → validation-rules.json} +42 -2
- package/library/global/commands/haus-doctor.md +10 -0
- package/library/global/commands/haus-fix.md +12 -0
- package/library/global/commands/haus-setup.md +28 -0
- package/library/global/settings-fragments/hooks.json +0 -6
- package/library/global/skills/haus-workflow/SKILL.md +81 -41
- package/package.json +10 -10
- package/scripts/postinstall.mjs +62 -0
- package/tests/README.md +5 -21
- package/tests/fixtures/catalog/manifest.json +0 -19
- package/library/catalog/sources.yaml +0 -411
- package/library/global/agents/haus-code-reviewer.md +0 -28
- package/library/global/agents/haus-docs-researcher.md +0 -27
- package/library/global/agents/haus-planner.md +0 -27
- package/library/global/agents/haus-security-reviewer.md +0 -27
- package/library/global/agents/haus-test-reviewer.md +0 -27
- package/library/global/templates/agentic-workflow-standard.md +0 -279
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 path29 from "path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/commands/apply.ts
|
|
@@ -53,6 +53,18 @@ async function fetchRemoteManifest() {
|
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
var WORKFLOW_TEMPLATE_REL = "templates/agentic-workflow-standard.md";
|
|
57
|
+
async function readWorkflowTemplate(opts = {}) {
|
|
58
|
+
const dest = path.join(CACHE_DIR, WORKFLOW_TEMPLATE_REL);
|
|
59
|
+
if (await fs.pathExists(dest)) return fs.readFile(dest, "utf8");
|
|
60
|
+
const text = await fetchText(`${REMOTE_BASE}/${WORKFLOW_TEMPLATE_REL}`);
|
|
61
|
+
if (text === null) return null;
|
|
62
|
+
if (!opts.dryRun) {
|
|
63
|
+
await fs.ensureDir(path.dirname(dest));
|
|
64
|
+
await fs.writeFile(dest, text, "utf8");
|
|
65
|
+
}
|
|
66
|
+
return text;
|
|
67
|
+
}
|
|
56
68
|
function isSafeCatalogPath(itemPath) {
|
|
57
69
|
if (!itemPath || path.isAbsolute(itemPath) || itemPath.includes("\\")) return false;
|
|
58
70
|
const normalized = path.normalize(itemPath);
|
|
@@ -80,7 +92,8 @@ async function syncRemoteCatalog() {
|
|
|
80
92
|
let unchanged = 0;
|
|
81
93
|
const failed = [];
|
|
82
94
|
for (const item of items) {
|
|
83
|
-
if (item.type !== "skill" && item.type !== "agent" || !item.path)
|
|
95
|
+
if (item.type !== "skill" && item.type !== "agent" && item.type !== "template" || !item.path)
|
|
96
|
+
continue;
|
|
84
97
|
if (!isSafeCatalogPath(item.path)) {
|
|
85
98
|
warn(`Skipping ${item.id}: invalid path "${item.path}"`);
|
|
86
99
|
failed.push(item.id);
|
|
@@ -159,7 +172,7 @@ async function getCacheManifestAge() {
|
|
|
159
172
|
|
|
160
173
|
// src/claude/write-claude-files.ts
|
|
161
174
|
import path10 from "path";
|
|
162
|
-
import
|
|
175
|
+
import fs10 from "fs-extra";
|
|
163
176
|
|
|
164
177
|
// src/update/hash-installed.ts
|
|
165
178
|
import path3 from "path";
|
|
@@ -254,10 +267,10 @@ function summarizeDiff(diffText) {
|
|
|
254
267
|
const lines = diffText.split("\n");
|
|
255
268
|
let additions = 0;
|
|
256
269
|
let deletions = 0;
|
|
257
|
-
for (const
|
|
258
|
-
if (
|
|
259
|
-
if (
|
|
260
|
-
if (
|
|
270
|
+
for (const line2 of lines) {
|
|
271
|
+
if (line2.startsWith("+++ ") || line2.startsWith("--- ")) continue;
|
|
272
|
+
if (line2.startsWith("+")) additions += 1;
|
|
273
|
+
if (line2.startsWith("-")) deletions += 1;
|
|
261
274
|
}
|
|
262
275
|
return { additions, deletions };
|
|
263
276
|
}
|
|
@@ -313,8 +326,7 @@ import path5 from "path";
|
|
|
313
326
|
var CONFIG_PATH = ".haus-workflow/config.json";
|
|
314
327
|
var DEFAULT_HOOKS_CONFIG = {
|
|
315
328
|
hooks: {
|
|
316
|
-
context: { enabled: false }
|
|
317
|
-
memoryInject: { enabled: false }
|
|
329
|
+
context: { enabled: false }
|
|
318
330
|
}
|
|
319
331
|
};
|
|
320
332
|
async function isHookEnabled(root, key) {
|
|
@@ -322,15 +334,103 @@ async function isHookEnabled(root, key) {
|
|
|
322
334
|
return cfg?.hooks?.[key]?.enabled === true;
|
|
323
335
|
}
|
|
324
336
|
|
|
337
|
+
// src/security/dangerous-commands.ts
|
|
338
|
+
var DANGEROUS_COMMANDS = [
|
|
339
|
+
"rm -rf",
|
|
340
|
+
"sudo",
|
|
341
|
+
"chmod -R 777",
|
|
342
|
+
"chown -R",
|
|
343
|
+
"git push --force",
|
|
344
|
+
"git reset --hard",
|
|
345
|
+
"docker system prune",
|
|
346
|
+
"drop database",
|
|
347
|
+
"truncate table",
|
|
348
|
+
"php artisan migrate --force",
|
|
349
|
+
"npm publish",
|
|
350
|
+
"yarn npm publish",
|
|
351
|
+
"pnpm publish"
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
// src/security/sensitive-paths.ts
|
|
355
|
+
var SENSITIVE_PATHS = [
|
|
356
|
+
".env",
|
|
357
|
+
".env.*",
|
|
358
|
+
"*.pem",
|
|
359
|
+
"*.key",
|
|
360
|
+
"*.p12",
|
|
361
|
+
"*.pfx",
|
|
362
|
+
"id_rsa",
|
|
363
|
+
"id_ed25519",
|
|
364
|
+
"*.sql",
|
|
365
|
+
"*.dump",
|
|
366
|
+
"*.backup",
|
|
367
|
+
"*.bak",
|
|
368
|
+
"storage/logs",
|
|
369
|
+
"wp-content/uploads",
|
|
370
|
+
"uploads",
|
|
371
|
+
"customer-data",
|
|
372
|
+
"exports",
|
|
373
|
+
"secrets",
|
|
374
|
+
"certs"
|
|
375
|
+
];
|
|
376
|
+
var SENSITIVE_PATH_REGEXES = [
|
|
377
|
+
/^\.env(\.|$)/,
|
|
378
|
+
/(^|\/)\.env(\.|$)/,
|
|
379
|
+
/\.pem$/,
|
|
380
|
+
/\.key$/,
|
|
381
|
+
/\.p12$/,
|
|
382
|
+
/\.pfx$/,
|
|
383
|
+
/\.sql$/,
|
|
384
|
+
/\.dump$/,
|
|
385
|
+
/customer-data/,
|
|
386
|
+
/exports/,
|
|
387
|
+
/certs/,
|
|
388
|
+
/secrets/,
|
|
389
|
+
/(^|\/)storage\/logs(\/|$)/,
|
|
390
|
+
/(^|\/)wp-content\/uploads(\/|$)/,
|
|
391
|
+
/(^|\/)uploads(\/|$)/
|
|
392
|
+
];
|
|
393
|
+
var SENSITIVE_ITEM_KEYWORDS = [
|
|
394
|
+
".env",
|
|
395
|
+
"secrets",
|
|
396
|
+
"certs",
|
|
397
|
+
"customer-data",
|
|
398
|
+
"exports",
|
|
399
|
+
".pem",
|
|
400
|
+
".key"
|
|
401
|
+
];
|
|
402
|
+
|
|
403
|
+
// src/security/deny-rules.ts
|
|
404
|
+
var SENSITIVE_DIRS = /* @__PURE__ */ new Set([
|
|
405
|
+
"storage/logs",
|
|
406
|
+
"wp-content/uploads",
|
|
407
|
+
"uploads",
|
|
408
|
+
"customer-data",
|
|
409
|
+
"exports",
|
|
410
|
+
"secrets",
|
|
411
|
+
"certs"
|
|
412
|
+
]);
|
|
413
|
+
var FILE_TOOLS = ["Read", "Edit", "Write"];
|
|
414
|
+
function buildDenyRules() {
|
|
415
|
+
const rules = [];
|
|
416
|
+
for (const command of DANGEROUS_COMMANDS) {
|
|
417
|
+
rules.push(`Bash(${command}:*)`);
|
|
418
|
+
}
|
|
419
|
+
for (const path30 of SENSITIVE_PATHS) {
|
|
420
|
+
const pattern = SENSITIVE_DIRS.has(path30) ? `${path30}/**` : path30;
|
|
421
|
+
for (const tool of FILE_TOOLS) {
|
|
422
|
+
rules.push(`${tool}(${pattern})`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return [...new Set(rules)];
|
|
426
|
+
}
|
|
427
|
+
|
|
325
428
|
// src/claude/load-hooks.ts
|
|
326
429
|
var CANONICAL_HOOKS = {
|
|
327
430
|
hooks: {
|
|
328
431
|
UserPromptSubmit: [
|
|
329
432
|
{
|
|
330
|
-
hooks: [
|
|
331
|
-
{ type: "command", command: "haus context --from-hook || true" },
|
|
332
|
-
{ type: "command", command: "haus memory inject --from-hook || true" }
|
|
333
|
-
]
|
|
433
|
+
hooks: [{ type: "command", command: "haus context --from-hook || true" }]
|
|
334
434
|
}
|
|
335
435
|
],
|
|
336
436
|
PreToolUse: [
|
|
@@ -347,12 +447,11 @@ var CANONICAL_HOOKS = {
|
|
|
347
447
|
};
|
|
348
448
|
var STABLE_HOOK_IDS = {
|
|
349
449
|
"haus context --from-hook || true": "haus.context-hook",
|
|
350
|
-
"haus memory inject --from-hook || true": "haus.memory-hook",
|
|
351
450
|
"haus guard file-access --from-hook || true": "haus.guard-file",
|
|
352
451
|
"haus guard bash --from-hook || true": "haus.guard-bash"
|
|
353
452
|
};
|
|
354
453
|
async function loadClaudeHooksSettings() {
|
|
355
|
-
return CANONICAL_HOOKS;
|
|
454
|
+
return { ...CANONICAL_HOOKS, permissions: { deny: buildDenyRules() } };
|
|
356
455
|
}
|
|
357
456
|
function flattenRecommendedHooks(settings) {
|
|
358
457
|
const out = [];
|
|
@@ -438,9 +537,11 @@ function renderProjectFacts(ctx, rec, pkgVersion) {
|
|
|
438
537
|
const repoName = ctx.repoName ?? path6.basename(ctx.root ?? "unknown");
|
|
439
538
|
return `${header}
|
|
440
539
|
|
|
441
|
-
#
|
|
540
|
+
# What haus found in this project
|
|
442
541
|
|
|
443
|
-
>
|
|
542
|
+
> This is a plain summary of your project that haus wrote automatically, so Claude
|
|
543
|
+
> always has the basics to hand. haus rewrites it on every \`haus apply\`, so don't
|
|
544
|
+
> edit it by hand \u2014 your changes would be replaced next time.
|
|
444
545
|
|
|
445
546
|
**Repo:** ${repoName}
|
|
446
547
|
**Package manager:** ${ctx.packageManager ?? "unknown"}
|
|
@@ -472,7 +573,9 @@ async function writeProjectFacts(root, pkgVersion, dryRun) {
|
|
|
472
573
|
dependencies: [],
|
|
473
574
|
securityRisks: [],
|
|
474
575
|
crossRepoHints: [],
|
|
475
|
-
warnings: []
|
|
576
|
+
warnings: [],
|
|
577
|
+
detectionStatus: "unknown",
|
|
578
|
+
unsupportedSignals: []
|
|
476
579
|
};
|
|
477
580
|
const rec = await readJson(hausPath(root, "recommendation.json")) ?? {
|
|
478
581
|
mode: "fast",
|
|
@@ -564,84 +667,189 @@ async function writeRootClaudeMd(root, dryRun) {
|
|
|
564
667
|
}
|
|
565
668
|
|
|
566
669
|
// src/claude/write-workflow-config.ts
|
|
670
|
+
import path9 from "path";
|
|
671
|
+
import fs8 from "fs-extra";
|
|
672
|
+
|
|
673
|
+
// src/claude/derive-workflow-config.ts
|
|
567
674
|
import path8 from "path";
|
|
568
675
|
import fs7 from "fs-extra";
|
|
569
|
-
|
|
676
|
+
var VALIDATION_LIBS = [
|
|
677
|
+
"zod",
|
|
678
|
+
"valibot",
|
|
679
|
+
"yup",
|
|
680
|
+
"joi",
|
|
681
|
+
"@hapi/joi",
|
|
682
|
+
"class-validator",
|
|
683
|
+
"superstruct",
|
|
684
|
+
"ajv"
|
|
685
|
+
];
|
|
686
|
+
function binCmd(pm, bin, args) {
|
|
687
|
+
const tail = args ? ` ${args}` : "";
|
|
688
|
+
if (pm === "yarn") return `yarn ${bin}${tail}`;
|
|
689
|
+
if (pm === "pnpm") return `pnpm exec ${bin}${tail}`;
|
|
690
|
+
return `npx --no-install ${bin}${tail}`;
|
|
691
|
+
}
|
|
692
|
+
async function deriveWorkflowConfig(root, ctx) {
|
|
570
693
|
const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
|
|
571
|
-
const
|
|
572
|
-
const
|
|
573
|
-
|
|
694
|
+
const pkg = await readJson(path8.join(root, "package.json"));
|
|
695
|
+
const scripts = pkg?.scripts ?? {};
|
|
696
|
+
const deps = new Set(ctx.dependencies);
|
|
697
|
+
const stacks = Object.values(ctx.detectedStacks ?? {}).flat();
|
|
698
|
+
const script = (name) => scripts[name] ? `${pm} run ${name}` : null;
|
|
699
|
+
const firstScript = (...names) => {
|
|
700
|
+
for (const n of names) if (scripts[n]) return `${pm} run ${n}`;
|
|
701
|
+
return null;
|
|
702
|
+
};
|
|
703
|
+
const hasDep = (name) => deps.has(name);
|
|
704
|
+
const exists = (rel) => fs7.pathExistsSync(path8.join(root, rel));
|
|
705
|
+
const hasTypeScript = hasDep("typescript") || exists("tsconfig.json");
|
|
706
|
+
const hasEslint = hasDep("eslint");
|
|
707
|
+
const hasPrettier = hasDep("prettier");
|
|
708
|
+
const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
|
|
709
|
+
const hasCypress = hasDep("cypress");
|
|
710
|
+
const preCommitTool = exists("lefthook.yml") || exists("lefthook.yaml") ? "lefthook" : exists(".husky") || hasDep("husky") || (scripts.prepare ?? "").includes("husky") ? "husky" : exists(".pre-commit-config.yaml") ? "pre-commit (Python framework)" : null;
|
|
711
|
+
return {
|
|
712
|
+
test: script("test") ?? `${pm} test`,
|
|
713
|
+
testE2E: firstScript("test:e2e", "e2e", "test:integration") ?? (hasPlaywright ? binCmd(pm, "playwright", "test") : null) ?? (hasCypress ? binCmd(pm, "cypress", "run") : null),
|
|
714
|
+
typecheck: firstScript("typecheck", "type-check", "tsc") ?? (hasTypeScript ? binCmd(pm, "tsc", "--noEmit") : null),
|
|
715
|
+
lint: script("lint") ?? (hasEslint ? binCmd(pm, "eslint", ".") : null),
|
|
716
|
+
lintFix: firstScript("lint:fix", "lint-fix") ?? (scripts.lint ? `${pm} run lint -- --fix` : hasEslint ? binCmd(pm, "eslint", ". --fix") : null),
|
|
717
|
+
formatCheck: firstScript("format:check", "format-check", "prettier:check") ?? (hasPrettier ? binCmd(pm, "prettier", "--check .") : null),
|
|
718
|
+
securityAudit: `${pm} audit`,
|
|
719
|
+
validationLibrary: VALIDATION_LIBS.find((lib) => deps.has(lib)) ?? null,
|
|
720
|
+
preCommitTool,
|
|
721
|
+
specPath: exists("docs/SPEC.md") ? "docs/SPEC.md" : null,
|
|
722
|
+
designPath: exists("docs/DESIGN.md") ? "docs/DESIGN.md" : null,
|
|
723
|
+
uxPath: exists("docs/UX.md") ? "docs/UX.md" : null
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/claude/write-workflow-config.ts
|
|
728
|
+
function fields(v) {
|
|
729
|
+
return [
|
|
730
|
+
{ prefix: "- Spec: ", value: v.specPath, hint: "path, e.g. docs/SPEC.md" },
|
|
731
|
+
{ prefix: "- Design: ", value: v.designPath, hint: "path, e.g. docs/DESIGN.md" },
|
|
732
|
+
{ prefix: "- UX flows: ", value: v.uxPath, hint: "path, e.g. docs/UX.md" },
|
|
733
|
+
{ prefix: "- Test (unit + integration): ", value: v.test, hint: "command", code: true },
|
|
734
|
+
{ prefix: "- Test (E2E): ", value: v.testE2E, hint: "command, e.g. playwright test", code: true },
|
|
735
|
+
{ prefix: "- Type check: ", value: v.typecheck, hint: "command, e.g. tsc --noEmit", code: true },
|
|
736
|
+
{ prefix: "- Lint: ", value: v.lint, hint: "command, e.g. eslint .", code: true },
|
|
737
|
+
{ prefix: "- Lint fix: ", value: v.lintFix, hint: "command, e.g. eslint . --fix", code: true },
|
|
738
|
+
{ prefix: "- Format check: ", value: v.formatCheck, hint: "command, e.g. prettier --check .", code: true },
|
|
739
|
+
{ prefix: "- Security audit: ", value: v.securityAudit, hint: "command", code: true },
|
|
740
|
+
{ prefix: "- Library: ", value: v.validationLibrary, hint: "e.g. zod, yup, joi" },
|
|
741
|
+
{ prefix: "- Tool: ", value: v.preCommitTool, hint: "e.g. lefthook, husky" }
|
|
742
|
+
];
|
|
574
743
|
}
|
|
575
|
-
|
|
744
|
+
function renderValue(f) {
|
|
745
|
+
if (f.value === null) return `<!-- fill in ${f.hint} -->`;
|
|
746
|
+
return f.code ? `\`${f.value}\`` : f.value;
|
|
747
|
+
}
|
|
748
|
+
function line(f) {
|
|
749
|
+
return `${f.prefix}${renderValue(f)}`;
|
|
750
|
+
}
|
|
751
|
+
function buildWorkflowConfig(v) {
|
|
752
|
+
const f = fields(v);
|
|
753
|
+
const byPrefix = (p) => line(f.find((x) => x.prefix === p));
|
|
754
|
+
return "# How this project works (commands & conventions)\n\n> The everyday commands and conventions for this project \u2014 the build, test, and\n> lint commands, where docs live, and so on. This file is yours to edit and haus\n> will not overwrite it. haus fills in what it can detect on first setup;\n> `haus apply --refill-config` fills any still-blank fields without touching\n> anything you've edited.\n\n## Source-of-truth documents\n" + byPrefix("- Spec: ") + "\n" + byPrefix("- Design: ") + "\n" + byPrefix("- UX flows: ") + "\n\n## Commands\n" + byPrefix("- Test (unit + integration): ") + "\n" + byPrefix("- Test (E2E): ") + "\n" + byPrefix("- Type check: ") + "\n" + byPrefix("- Lint: ") + "\n" + byPrefix("- Lint fix: ") + "\n" + byPrefix("- Format check: ") + "\n" + byPrefix("- Security audit: ") + "\n\n## Validation library\n" + byPrefix("- Library: ") + "\n\n## Highest-stakes logic\n<!-- fill in domain areas requiring TDD-only treatment, e.g. payment flows, auth, medical data -->\n\n## Pre-commit tool\n" + byPrefix("- Tool: ") + "\n";
|
|
755
|
+
}
|
|
756
|
+
function refillContent(existing, v) {
|
|
757
|
+
const f = fields(v);
|
|
758
|
+
return existing.split("\n").map((ln) => {
|
|
759
|
+
const field = f.find((x) => ln.startsWith(x.prefix));
|
|
760
|
+
if (!field || field.value === null) return ln;
|
|
761
|
+
const rest = ln.slice(field.prefix.length).trim();
|
|
762
|
+
return rest.startsWith("<!-- fill in") ? line(field) : ln;
|
|
763
|
+
}).join("\n");
|
|
764
|
+
}
|
|
765
|
+
var FALLBACK_CONTEXT = {
|
|
766
|
+
mode: "fast",
|
|
767
|
+
generatedAt: "",
|
|
768
|
+
root: "",
|
|
769
|
+
repoName: "",
|
|
770
|
+
packageManager: "unknown",
|
|
771
|
+
repoRoles: [],
|
|
772
|
+
confidence: 0,
|
|
773
|
+
detectedStacks: {},
|
|
774
|
+
dependencies: [],
|
|
775
|
+
securityRisks: [],
|
|
776
|
+
crossRepoHints: [],
|
|
777
|
+
warnings: [],
|
|
778
|
+
detectionStatus: "unknown",
|
|
779
|
+
unsupportedSignals: []
|
|
780
|
+
};
|
|
781
|
+
async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
576
782
|
const destPath = hausPath(root, "workflow-config.md");
|
|
577
783
|
const printable = displayPath(root, destPath);
|
|
578
|
-
|
|
784
|
+
const exists = await fs8.pathExists(destPath);
|
|
785
|
+
if (exists && !opts.refill) {
|
|
579
786
|
if (dryRun) log(printable + ": exists (project-owned, skipping)");
|
|
580
787
|
return null;
|
|
581
788
|
}
|
|
582
789
|
const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
|
|
583
|
-
|
|
584
|
-
generatedAt: "",
|
|
790
|
+
...FALLBACK_CONTEXT,
|
|
585
791
|
root,
|
|
586
|
-
repoName:
|
|
587
|
-
packageManager: "unknown",
|
|
588
|
-
repoRoles: [],
|
|
589
|
-
confidence: 0,
|
|
590
|
-
detectedStacks: {},
|
|
591
|
-
dependencies: [],
|
|
592
|
-
securityRisks: [],
|
|
593
|
-
crossRepoHints: [],
|
|
594
|
-
warnings: []
|
|
792
|
+
repoName: path9.basename(root)
|
|
595
793
|
};
|
|
596
|
-
const
|
|
794
|
+
const values = await deriveWorkflowConfig(root, ctx);
|
|
795
|
+
if (exists) {
|
|
796
|
+
const current = await fs8.readFile(destPath, "utf8");
|
|
797
|
+
const refilled = refillContent(current, values);
|
|
798
|
+
if (refilled === current) {
|
|
799
|
+
if (dryRun) log(printable + ": no blank fields to refill");
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
if (dryRun) {
|
|
803
|
+
log(printable + ": would refill blank fields");
|
|
804
|
+
return destPath;
|
|
805
|
+
}
|
|
806
|
+
await writeText(destPath, refilled);
|
|
807
|
+
return destPath;
|
|
808
|
+
}
|
|
597
809
|
if (dryRun) {
|
|
598
810
|
log(printable + ": would create");
|
|
599
811
|
return destPath;
|
|
600
812
|
}
|
|
601
|
-
await writeText(destPath,
|
|
813
|
+
await writeText(destPath, buildWorkflowConfig(values));
|
|
602
814
|
return destPath;
|
|
603
815
|
}
|
|
604
816
|
|
|
605
817
|
// src/claude/write-workflow.ts
|
|
606
|
-
import
|
|
607
|
-
import fs8 from "fs-extra";
|
|
818
|
+
import fs9 from "fs-extra";
|
|
608
819
|
|
|
609
820
|
// src/claude/managed-template.ts
|
|
610
|
-
function normaliseLF(
|
|
611
|
-
return
|
|
821
|
+
function normaliseLF(content2) {
|
|
822
|
+
return content2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
612
823
|
}
|
|
613
|
-
function parseHausManagedHeader(
|
|
614
|
-
const match =
|
|
824
|
+
function parseHausManagedHeader(line2) {
|
|
825
|
+
const match = line2.match(/<!-- HAUS-MANAGED id=([\w.:-]+)/);
|
|
615
826
|
if (!match) return null;
|
|
616
|
-
const hashMatch =
|
|
827
|
+
const hashMatch = line2.match(/hash=(sha256-[a-f0-9]+)/);
|
|
617
828
|
return { id: match[1], hash: hashMatch?.[1] };
|
|
618
829
|
}
|
|
619
830
|
|
|
620
831
|
// src/claude/write-workflow.ts
|
|
621
832
|
var STABLE_ID2 = "template.workflow";
|
|
622
833
|
var SCHEMA_VERSION2 = "1";
|
|
623
|
-
var TEMPLATE_REL = "library/global/templates/agentic-workflow-standard.md";
|
|
624
|
-
var CATALOG_CACHE_TEMPLATE = path9.join(CACHE_DIR, "templates/agentic-workflow-standard.md");
|
|
625
834
|
function makeWorkflowHeader(pkgVersion, contentHash) {
|
|
626
835
|
return `<!-- HAUS-MANAGED id=${STABLE_ID2} v=${SCHEMA_VERSION2} source=@haus-tech/haus-workflow@${pkgVersion} hash=${contentHash} -->`;
|
|
627
836
|
}
|
|
628
837
|
async function writeWorkflow(root, pkgVersion, dryRun) {
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
838
|
+
const templateContent = await readWorkflowTemplate({ dryRun });
|
|
839
|
+
if (templateContent === null) {
|
|
840
|
+
warn(
|
|
841
|
+
`Workflow template could not be fetched from the catalog \u2014 check your network, then re-run \`haus apply --write\` (or \`haus update\`)`
|
|
842
|
+
);
|
|
634
843
|
return null;
|
|
635
844
|
}
|
|
636
|
-
const templateContent = await fs8.readFile(templatePath, "utf8");
|
|
637
845
|
const contentHash = hashText(normaliseLF(templateContent));
|
|
638
846
|
const header = makeWorkflowHeader(pkgVersion, contentHash);
|
|
639
847
|
const next = `${header}
|
|
640
848
|
${templateContent}`;
|
|
641
849
|
const destPath = hausPath(root, "WORKFLOW.md");
|
|
642
850
|
const printable = displayPath(root, destPath);
|
|
643
|
-
if (await
|
|
644
|
-
const existing = await
|
|
851
|
+
if (await fs9.pathExists(destPath)) {
|
|
852
|
+
const existing = await fs9.readFile(destPath, "utf8");
|
|
645
853
|
const firstLine = existing.split("\n")[0] ?? "";
|
|
646
854
|
const parsed = parseHausManagedHeader(firstLine);
|
|
647
855
|
if (!parsed) {
|
|
@@ -663,7 +871,7 @@ ${templateContent}`;
|
|
|
663
871
|
}
|
|
664
872
|
}
|
|
665
873
|
if (dryRun) {
|
|
666
|
-
const prev = await
|
|
874
|
+
const prev = await fs9.pathExists(destPath) ? await fs9.readFile(destPath, "utf8") : "";
|
|
667
875
|
if (!prev) {
|
|
668
876
|
log(createUnifiedDiff(printable, "", next));
|
|
669
877
|
} else {
|
|
@@ -678,7 +886,7 @@ ${templateContent}`;
|
|
|
678
886
|
}
|
|
679
887
|
|
|
680
888
|
// src/claude/write-claude-files.ts
|
|
681
|
-
async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
889
|
+
async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
682
890
|
const rec = await readJson(hausPath(root, "recommendation.json")) ?? {
|
|
683
891
|
mode: "fast",
|
|
684
892
|
recommended: [],
|
|
@@ -700,7 +908,9 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
700
908
|
];
|
|
701
909
|
const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun);
|
|
702
910
|
const workflowPath = await writeWorkflow(root, hausVersion, dryRun);
|
|
703
|
-
const workflowConfigPath = await writeWorkflowConfig(root, dryRun
|
|
911
|
+
const workflowConfigPath = await writeWorkflowConfig(root, dryRun, {
|
|
912
|
+
refill: opts.refillConfig
|
|
913
|
+
});
|
|
704
914
|
const projectFactsPath = await writeProjectFacts(root, hausVersion, dryRun);
|
|
705
915
|
const p6Files = [
|
|
706
916
|
rootClaudeMdPath,
|
|
@@ -718,7 +928,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
718
928
|
await writeManagedJson(root, claudePath(root, "settings.json"), hookSettings, dryRun);
|
|
719
929
|
if (!dryRun) await assertPostApplySettingsMatchCanonical(root, hookSettings);
|
|
720
930
|
const configPath = hausPath(root, "config.json");
|
|
721
|
-
if (!await
|
|
931
|
+
if (!await fs10.pathExists(configPath)) {
|
|
722
932
|
await writeManagedJson(root, configPath, DEFAULT_HOOKS_CONFIG, dryRun);
|
|
723
933
|
}
|
|
724
934
|
await writeManagedText(
|
|
@@ -736,7 +946,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
736
946
|
await writeManagedText(
|
|
737
947
|
root,
|
|
738
948
|
claudePath(root, "rules", "haus.md"),
|
|
739
|
-
"- Keep context minimal.\n- Follow project conventions.\n",
|
|
949
|
+
"- Keep context minimal.\n- Follow project conventions.\n\n## Driving haus\nWhen the user asks to set up, configure, check, or fix the project, run `haus setup-project` or `haus doctor` and narrate results in plain language \u2014 never make them use a terminal or read JSON. The `/haus-setup`, `/haus-doctor`, and `/haus-fix` commands do the same.\n",
|
|
740
950
|
dryRun
|
|
741
951
|
);
|
|
742
952
|
await writeManagedText(
|
|
@@ -774,18 +984,18 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
774
984
|
}
|
|
775
985
|
const cachedItem = cacheManifestById.get(item.id);
|
|
776
986
|
const cachePath = cachedItem?.path ? path10.join(CACHE_DIR, cachedItem.path) : null;
|
|
777
|
-
const sourcePath = cachePath && await
|
|
987
|
+
const sourcePath = cachePath && await fs10.pathExists(cachePath) ? cachePath : path10.join(manifestDir, manifestItem.path);
|
|
778
988
|
const target = item.type === "agent" ? "agents" : item.type === "template" ? "templates" : "skills";
|
|
779
989
|
const destination = claudePath(root, target, path10.basename(sourcePath));
|
|
780
|
-
if (await
|
|
990
|
+
if (await fs10.pathExists(sourcePath)) {
|
|
781
991
|
if (dryRun) {
|
|
782
|
-
const exists = await
|
|
992
|
+
const exists = await fs10.pathExists(destination);
|
|
783
993
|
log(
|
|
784
994
|
`${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`
|
|
785
995
|
);
|
|
786
996
|
} else {
|
|
787
|
-
await
|
|
788
|
-
await
|
|
997
|
+
await fs10.ensureDir(path10.dirname(destination));
|
|
998
|
+
await fs10.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
789
999
|
}
|
|
790
1000
|
files.push(destination);
|
|
791
1001
|
const current = installedPathsByItem.get(item.id) ?? [];
|
|
@@ -840,7 +1050,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
840
1050
|
return [...new Set(files)];
|
|
841
1051
|
}
|
|
842
1052
|
async function writeManagedText(root, filePath, nextText, dryRun) {
|
|
843
|
-
const prev = await
|
|
1053
|
+
const prev = await fs10.pathExists(filePath) ? await fs10.readFile(filePath, "utf8") : "";
|
|
844
1054
|
const printable = displayPath(root, filePath);
|
|
845
1055
|
if (dryRun) {
|
|
846
1056
|
if (!prev) {
|
|
@@ -924,7 +1134,9 @@ async function runApply(options) {
|
|
|
924
1134
|
}
|
|
925
1135
|
}
|
|
926
1136
|
}
|
|
927
|
-
const files = await writeClaudeFiles(root, isDryRun, selectedIds
|
|
1137
|
+
const files = await writeClaudeFiles(root, isDryRun, selectedIds, {
|
|
1138
|
+
refillConfig: options.refillConfig
|
|
1139
|
+
});
|
|
928
1140
|
if (isDryRun) {
|
|
929
1141
|
log(`Dry-run complete \u2014 ${files.length} file(s) planned, none written. Run --write to apply.`);
|
|
930
1142
|
} else {
|
|
@@ -991,8 +1203,7 @@ async function runCatalogAudit() {
|
|
|
991
1203
|
import path13 from "path";
|
|
992
1204
|
var CONFIG_PATH2 = ".haus-workflow/config.json";
|
|
993
1205
|
var HOOK_ALIASES = {
|
|
994
|
-
"hook.context": "context"
|
|
995
|
-
"hook.memory": "memoryInject"
|
|
1206
|
+
"hook.context": "context"
|
|
996
1207
|
};
|
|
997
1208
|
async function runConfig(key, action) {
|
|
998
1209
|
const hookKey = HOOK_ALIASES[key];
|
|
@@ -1045,7 +1256,8 @@ function normalizeRecommendation(input2) {
|
|
|
1045
1256
|
finalScore: item.score ?? 0
|
|
1046
1257
|
},
|
|
1047
1258
|
tags: item.tags,
|
|
1048
|
-
ecosystem: item.ecosystem
|
|
1259
|
+
ecosystem: item.ecosystem,
|
|
1260
|
+
tokenEstimate: item.tokenEstimate
|
|
1049
1261
|
};
|
|
1050
1262
|
});
|
|
1051
1263
|
const skipped = (input2.skipped ?? []).map((item) => ({
|
|
@@ -1105,46 +1317,7 @@ function buildRecommendationExplanation(recommendation) {
|
|
|
1105
1317
|
};
|
|
1106
1318
|
}
|
|
1107
1319
|
|
|
1108
|
-
// src/recommender/task-
|
|
1109
|
-
function pickTaskRelevantRules(recommendation, task, taskIntents = /* @__PURE__ */ new Set()) {
|
|
1110
|
-
const recommended = recommendation?.recommended ?? [];
|
|
1111
|
-
if (!task) return recommended;
|
|
1112
|
-
if (taskIntents.size > 0) {
|
|
1113
|
-
const intentMatches = recommended.filter((rule) => {
|
|
1114
|
-
if (rule.selectionMode === "baseline") return false;
|
|
1115
|
-
const ruleIntents = computeRuleIntents(rule);
|
|
1116
|
-
if (ruleIntents.size === 0) return false;
|
|
1117
|
-
for (const ti of taskIntents) {
|
|
1118
|
-
if (ruleIntents.has(ti)) return true;
|
|
1119
|
-
}
|
|
1120
|
-
return false;
|
|
1121
|
-
});
|
|
1122
|
-
if (intentMatches.length > 0) return intentMatches;
|
|
1123
|
-
}
|
|
1124
|
-
const tokens = task.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
|
|
1125
|
-
const tokenMatches = recommended.filter((rule) => {
|
|
1126
|
-
if (rule.selectionMode === "baseline") return false;
|
|
1127
|
-
const corpus = [
|
|
1128
|
-
rule.id,
|
|
1129
|
-
rule.ecosystem ?? "",
|
|
1130
|
-
...rule.tags ?? [],
|
|
1131
|
-
rule.reason ?? "",
|
|
1132
|
-
...rule.reasons.map((r) => r.message)
|
|
1133
|
-
].join(" ").toLowerCase();
|
|
1134
|
-
return tokens.some((token) => corpus.includes(token));
|
|
1135
|
-
});
|
|
1136
|
-
if (tokenMatches.length > 0) return tokenMatches;
|
|
1137
|
-
const taskWantsTesting = taskIntents.has("testing");
|
|
1138
|
-
const cappedMediumOrHigh = recommended.filter((rule) => {
|
|
1139
|
-
if (rule.selectionMode === "baseline") return false;
|
|
1140
|
-
if (rule.confidenceLevel === "low") return false;
|
|
1141
|
-
if (taskWantsTesting) return true;
|
|
1142
|
-
const ruleIntents = computeRuleIntents(rule);
|
|
1143
|
-
const isTestingOnly = ruleIntents.size > 0 && [...ruleIntents].every((i) => i === "testing");
|
|
1144
|
-
return !isTestingOnly;
|
|
1145
|
-
});
|
|
1146
|
-
return cappedMediumOrHigh.slice(0, 8);
|
|
1147
|
-
}
|
|
1320
|
+
// src/recommender/task-classification.ts
|
|
1148
1321
|
var ALL_INTENTS = [
|
|
1149
1322
|
"backend",
|
|
1150
1323
|
"frontend",
|
|
@@ -1358,9 +1531,76 @@ function computeRuleIntents(rule) {
|
|
|
1358
1531
|
return intents;
|
|
1359
1532
|
}
|
|
1360
1533
|
|
|
1534
|
+
// src/recommender/rule-selection.ts
|
|
1535
|
+
var DEFAULT_CONTEXT_TOKEN_BUDGET = 12e3;
|
|
1536
|
+
function pickTaskRelevantRules(recommendation, task, taskIntents = /* @__PURE__ */ new Set(), opts = {}) {
|
|
1537
|
+
const recommended = recommendation?.recommended ?? [];
|
|
1538
|
+
return applyTokenBudget(selectRules(recommended, task, taskIntents), opts.tokenBudget);
|
|
1539
|
+
}
|
|
1540
|
+
function applyTokenBudget(rules, budget) {
|
|
1541
|
+
if (!budget || budget <= 0) return rules;
|
|
1542
|
+
const total = rules.reduce((sum, r) => sum + (r.tokenEstimate ?? 0), 0);
|
|
1543
|
+
if (total <= budget) return rules;
|
|
1544
|
+
const keep = /* @__PURE__ */ new Set();
|
|
1545
|
+
let used = 0;
|
|
1546
|
+
for (const r of rules) {
|
|
1547
|
+
if (r.selectionMode === "baseline") {
|
|
1548
|
+
keep.add(r.id);
|
|
1549
|
+
used += r.tokenEstimate ?? 0;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
const matched = rules.filter((r) => r.selectionMode !== "baseline").sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
1553
|
+
for (const r of matched) {
|
|
1554
|
+
const est = r.tokenEstimate ?? 0;
|
|
1555
|
+
if (used + est <= budget) {
|
|
1556
|
+
keep.add(r.id);
|
|
1557
|
+
used += est;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
return rules.filter((r) => keep.has(r.id));
|
|
1561
|
+
}
|
|
1562
|
+
function selectRules(recommended, task, taskIntents) {
|
|
1563
|
+
if (!task) return recommended;
|
|
1564
|
+
if (taskIntents.size > 0) {
|
|
1565
|
+
const intentMatches = recommended.filter((rule) => {
|
|
1566
|
+
if (rule.selectionMode === "baseline") return false;
|
|
1567
|
+
const ruleIntents = computeRuleIntents(rule);
|
|
1568
|
+
if (ruleIntents.size === 0) return false;
|
|
1569
|
+
for (const ti of taskIntents) {
|
|
1570
|
+
if (ruleIntents.has(ti)) return true;
|
|
1571
|
+
}
|
|
1572
|
+
return false;
|
|
1573
|
+
});
|
|
1574
|
+
if (intentMatches.length > 0) return intentMatches;
|
|
1575
|
+
}
|
|
1576
|
+
const tokens = task.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
|
|
1577
|
+
const tokenMatches = recommended.filter((rule) => {
|
|
1578
|
+
if (rule.selectionMode === "baseline") return false;
|
|
1579
|
+
const corpus = [
|
|
1580
|
+
rule.id,
|
|
1581
|
+
rule.ecosystem ?? "",
|
|
1582
|
+
...rule.tags ?? [],
|
|
1583
|
+
rule.reason ?? "",
|
|
1584
|
+
...rule.reasons.map((r) => r.message)
|
|
1585
|
+
].join(" ").toLowerCase();
|
|
1586
|
+
return tokens.some((token) => corpus.includes(token));
|
|
1587
|
+
});
|
|
1588
|
+
if (tokenMatches.length > 0) return tokenMatches;
|
|
1589
|
+
const taskWantsTesting = taskIntents.has("testing");
|
|
1590
|
+
const cappedMediumOrHigh = recommended.filter((rule) => {
|
|
1591
|
+
if (rule.selectionMode === "baseline") return false;
|
|
1592
|
+
if (rule.confidenceLevel === "low") return false;
|
|
1593
|
+
if (taskWantsTesting) return true;
|
|
1594
|
+
const ruleIntents = computeRuleIntents(rule);
|
|
1595
|
+
const isTestingOnly = ruleIntents.size > 0 && [...ruleIntents].every((i) => i === "testing");
|
|
1596
|
+
return !isTestingOnly;
|
|
1597
|
+
});
|
|
1598
|
+
return cappedMediumOrHigh.slice(0, 8);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1361
1601
|
// src/scanner/scan-project.ts
|
|
1362
|
-
import { readFile } from "fs/promises";
|
|
1363
|
-
import
|
|
1602
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1603
|
+
import path17 from "path";
|
|
1364
1604
|
|
|
1365
1605
|
// src/utils/audit-checks.ts
|
|
1366
1606
|
function isRecord(v) {
|
|
@@ -1388,7 +1628,7 @@ function compareVersions(a, b) {
|
|
|
1388
1628
|
|
|
1389
1629
|
// src/scanner/detect-package-manager.ts
|
|
1390
1630
|
import path14 from "path";
|
|
1391
|
-
import
|
|
1631
|
+
import fs11 from "fs-extra";
|
|
1392
1632
|
function detectPackageManager(root, packageManagerField) {
|
|
1393
1633
|
const field = String(packageManagerField ?? "").trim();
|
|
1394
1634
|
if (field.startsWith("yarn@")) {
|
|
@@ -1406,77 +1646,394 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
1406
1646
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
1407
1647
|
return "unknown";
|
|
1408
1648
|
}
|
|
1409
|
-
if (
|
|
1410
|
-
if (
|
|
1411
|
-
if (
|
|
1649
|
+
if (fs11.existsSync(path14.join(root, "yarn.lock"))) return "yarn";
|
|
1650
|
+
if (fs11.existsSync(path14.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
1651
|
+
if (fs11.existsSync(path14.join(root, "package-lock.json"))) return "npm";
|
|
1412
1652
|
return "unknown";
|
|
1413
1653
|
}
|
|
1414
1654
|
|
|
1415
|
-
// src/scanner/
|
|
1416
|
-
var
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
"
|
|
1426
|
-
"
|
|
1427
|
-
"
|
|
1428
|
-
"
|
|
1429
|
-
"
|
|
1430
|
-
"
|
|
1431
|
-
"
|
|
1432
|
-
"routes/*.php",
|
|
1433
|
-
"app/Providers/*.php",
|
|
1434
|
-
"schema.graphql",
|
|
1435
|
-
"**/*.graphql",
|
|
1436
|
-
"**/vendure-config.*",
|
|
1437
|
-
"**/*module.ts",
|
|
1438
|
-
"web/app/**",
|
|
1439
|
-
"wp-content/plugins/**",
|
|
1440
|
-
"wp-content/themes/**",
|
|
1441
|
-
"wp-content/mu-plugins/**",
|
|
1442
|
-
"wp-content/acf-json/**",
|
|
1443
|
-
".storybook/**",
|
|
1444
|
-
".env.example",
|
|
1445
|
-
"wp-config.php",
|
|
1446
|
-
"**/*.csproj",
|
|
1447
|
-
"**/*.sln",
|
|
1448
|
-
"docker-compose.*",
|
|
1449
|
-
"Dockerfile"
|
|
1450
|
-
];
|
|
1451
|
-
var SENSITIVE = [
|
|
1452
|
-
/^\.env(\.|$)/,
|
|
1453
|
-
/(^|\/)\.env(\.|$)/,
|
|
1454
|
-
/\.pem$/,
|
|
1455
|
-
/\.key$/,
|
|
1456
|
-
/\.p12$/,
|
|
1457
|
-
/\.pfx$/,
|
|
1458
|
-
/\.sql$/,
|
|
1459
|
-
/\.dump$/,
|
|
1460
|
-
/customer-data/,
|
|
1461
|
-
/exports/,
|
|
1462
|
-
/certs/,
|
|
1463
|
-
/secrets/,
|
|
1464
|
-
/(^|\/)storage\/logs(\/|$)/,
|
|
1465
|
-
/(^|\/)wp-content\/uploads(\/|$)/,
|
|
1466
|
-
/(^|\/)uploads(\/|$)/
|
|
1655
|
+
// src/scanner/detection-registry.ts
|
|
1656
|
+
var dep = (value) => ({ kind: "dep", value });
|
|
1657
|
+
var depPrefix = (value) => ({ kind: "depPrefix", value });
|
|
1658
|
+
var depAbsent = (value) => ({ kind: "depAbsent", value });
|
|
1659
|
+
var fileEndsWith = (value) => ({ kind: "file", value, mode: "endsWith" });
|
|
1660
|
+
var fileIncludes = (value) => ({ kind: "file", value, mode: "includes" });
|
|
1661
|
+
var fileEquals = (value) => ({ kind: "file", value, mode: "equals" });
|
|
1662
|
+
var fileStartsWith = (value) => ({ kind: "file", value, mode: "startsWith" });
|
|
1663
|
+
var content = (value) => ({ kind: "content", value });
|
|
1664
|
+
var STACK_BUCKETS = [
|
|
1665
|
+
"backend",
|
|
1666
|
+
"frontend",
|
|
1667
|
+
"databases",
|
|
1668
|
+
"testing",
|
|
1669
|
+
"auth",
|
|
1670
|
+
"tooling",
|
|
1671
|
+
"packageManagers"
|
|
1467
1672
|
];
|
|
1673
|
+
function matchSignal(sig, ctx) {
|
|
1674
|
+
switch (sig.kind) {
|
|
1675
|
+
case "dep":
|
|
1676
|
+
return ctx.deps.has(sig.value);
|
|
1677
|
+
case "depPrefix":
|
|
1678
|
+
for (const d of ctx.deps) if (d.startsWith(sig.value)) return true;
|
|
1679
|
+
return false;
|
|
1680
|
+
case "depAbsent":
|
|
1681
|
+
return !ctx.deps.has(sig.value);
|
|
1682
|
+
case "content":
|
|
1683
|
+
return ctx.contentBlob.includes(sig.value);
|
|
1684
|
+
case "file":
|
|
1685
|
+
return ctx.files.some((f) => {
|
|
1686
|
+
switch (sig.mode) {
|
|
1687
|
+
case "endsWith":
|
|
1688
|
+
return f.endsWith(sig.value);
|
|
1689
|
+
case "includes":
|
|
1690
|
+
return f.includes(sig.value);
|
|
1691
|
+
case "equals":
|
|
1692
|
+
return f === sig.value;
|
|
1693
|
+
case "startsWith":
|
|
1694
|
+
return f.startsWith(sig.value);
|
|
1695
|
+
}
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
function matchRule(rule, ctx) {
|
|
1700
|
+
if (rule.all) return rule.all.every((s) => matchSignal(s, ctx));
|
|
1701
|
+
if (rule.any) return rule.any.some((s) => matchSignal(s, ctx));
|
|
1702
|
+
return false;
|
|
1703
|
+
}
|
|
1704
|
+
var ROLE_RULES = [
|
|
1705
|
+
{ role: "next-app", any: [dep("next"), fileIncludes("next.config.")] },
|
|
1706
|
+
{ role: "react-app", any: [dep("react")] },
|
|
1707
|
+
{ role: "vite-app", any: [dep("vite"), fileIncludes("vite.config.")] },
|
|
1708
|
+
{ role: "react-router-app", all: [dep("react-router"), dep("@react-router/node")] },
|
|
1709
|
+
{ role: "sanity-studio", any: [dep("sanity")] },
|
|
1710
|
+
{ role: "strapi-app", any: [dep("@strapi/strapi"), depPrefix("@strapi/")] },
|
|
1711
|
+
{ role: "expo-app", any: [dep("expo")] },
|
|
1712
|
+
{ role: "vendure-app", any: [dep("@vendure/core")] },
|
|
1713
|
+
{ role: "vendure-plugin", any: [depPrefix("@haus/vendure-"), fileIncludes("vendure-config")] },
|
|
1714
|
+
{ role: "nestjs-api", any: [dep("@nestjs/core")] },
|
|
1715
|
+
{ role: "graphql-api", any: [dep("graphql"), dep("@nestjs/graphql")] },
|
|
1716
|
+
{ role: "nx-monorepo", any: [fileEndsWith("nx.json")] },
|
|
1717
|
+
{ role: "turbo-monorepo", any: [fileEndsWith("turbo.json")] },
|
|
1718
|
+
{ role: "laravel-app", any: [fileEndsWith("artisan"), dep("laravel/framework")] },
|
|
1719
|
+
{ role: "laravel-nova-app", any: [dep("laravel/nova")] },
|
|
1720
|
+
{ role: "dotnet-service", any: [fileEndsWith(".csproj"), fileEndsWith(".sln")] },
|
|
1721
|
+
{ role: "express-service", any: [dep("express")] }
|
|
1722
|
+
];
|
|
1723
|
+
var STACK_RULES = [
|
|
1724
|
+
{ stack: ["frontend", "nextjs"], any: [dep("next")] },
|
|
1725
|
+
{ stack: ["frontend", "react19"], any: [dep("react")] },
|
|
1726
|
+
{ stack: ["frontend", "vue"], any: [dep("vue")] },
|
|
1727
|
+
{ stack: ["frontend", "vite8"], any: [dep("vite")] },
|
|
1728
|
+
{ stack: ["frontend", "react-router-v7"], all: [dep("react-router"), dep("@react-router/node")] },
|
|
1729
|
+
{ stack: ["frontend", "tailwindcss"], any: [dep("tailwindcss"), fileIncludes("tailwind.config.")] },
|
|
1730
|
+
{
|
|
1731
|
+
stack: ["frontend", "shadcn"],
|
|
1732
|
+
all: [fileEndsWith("components.json"), dep("class-variance-authority")]
|
|
1733
|
+
},
|
|
1734
|
+
{ stack: ["tooling", "typescript5"], any: [dep("typescript")] },
|
|
1735
|
+
{ stack: ["backend", "sanity"], any: [dep("sanity"), dep("next-sanity"), dep("@sanity/client")] },
|
|
1736
|
+
{ stack: ["backend", "strapi"], any: [dep("@strapi/strapi"), depPrefix("@strapi/")] },
|
|
1737
|
+
{ stack: ["backend", "prisma"], any: [dep("prisma"), dep("@prisma/client")] },
|
|
1738
|
+
{ stack: ["frontend", "expo"], any: [dep("expo")] },
|
|
1739
|
+
{ stack: ["frontend", "react-native"], any: [dep("react-native")] },
|
|
1740
|
+
{ stack: ["tooling", "i18next"], any: [dep("i18next"), dep("react-i18next")] },
|
|
1741
|
+
{ stack: ["tooling", "bullmq"], any: [dep("bullmq")] },
|
|
1742
|
+
{ stack: ["tooling", "docker"], any: [fileEquals("Dockerfile"), fileStartsWith("docker-compose")] },
|
|
1743
|
+
{ stack: ["tooling", "pm2"], any: [dep("pm2"), fileIncludes("ecosystem.config")] },
|
|
1744
|
+
{ stack: ["tooling", "sentry"], any: [depPrefix("@sentry/")] },
|
|
1745
|
+
{ stack: ["tooling", "deployer-php"], any: [dep("deployer/deployer")] },
|
|
1746
|
+
{ stack: ["tooling", "missing-prettier"], any: [depAbsent("prettier")] },
|
|
1747
|
+
{ stack: ["tooling", "missing-eslint"], any: [depAbsent("eslint")] },
|
|
1748
|
+
{
|
|
1749
|
+
stack: ["tooling", "stripe"],
|
|
1750
|
+
any: [dep("@stripe/stripe-js"), dep("@stripe/react-stripe-js")]
|
|
1751
|
+
},
|
|
1752
|
+
{ stack: ["tooling", "qliro"], any: [dep("@haus-tech/qliro-plugin")] },
|
|
1753
|
+
{
|
|
1754
|
+
stack: ["databases", "supabase"],
|
|
1755
|
+
any: [dep("@supabase/supabase-js"), depPrefix("@supabase/")]
|
|
1756
|
+
},
|
|
1757
|
+
{ stack: ["backend", "vendure3"], any: [dep("@vendure/core")] },
|
|
1758
|
+
{ stack: ["backend", "nestjs"], any: [dep("@nestjs/core")] },
|
|
1759
|
+
{ stack: ["backend", "nestjs"], any: [content("NestFactory")] },
|
|
1760
|
+
{ stack: ["backend", "vendure3"], any: [content("@VendurePlugin")] },
|
|
1761
|
+
{ stack: ["backend", "graphql"], any: [dep("graphql"), dep("@nestjs/graphql")] },
|
|
1762
|
+
{ stack: ["backend", "graphql"], any: [fileEndsWith(".graphql"), fileEndsWith("schema.graphql")] },
|
|
1763
|
+
{ stack: ["backend", "laravel"], any: [dep("laravel/framework")] },
|
|
1764
|
+
{ stack: ["backend", "laravel"], any: [fileIncludes("app/Providers/"), fileIncludes("routes/")] },
|
|
1765
|
+
{ stack: ["backend", "wordpress"], any: [fileEndsWith("wp-config.php"), dep("roots/wordpress")] },
|
|
1766
|
+
{
|
|
1767
|
+
stack: ["backend", "elementor"],
|
|
1768
|
+
any: [
|
|
1769
|
+
dep("wpackagist-plugin/elementor"),
|
|
1770
|
+
dep("wearehaus/elementor-pro"),
|
|
1771
|
+
dep("wpackagist-theme/hello-elementor")
|
|
1772
|
+
]
|
|
1773
|
+
},
|
|
1774
|
+
{
|
|
1775
|
+
stack: ["backend", "acf-pro"],
|
|
1776
|
+
any: [
|
|
1777
|
+
dep("wearehaus/advanced-custom-fields-pro"),
|
|
1778
|
+
dep("wpackagist-plugin/advanced-custom-fields")
|
|
1779
|
+
]
|
|
1780
|
+
},
|
|
1781
|
+
{ stack: ["backend", "jetengine"], any: [dep("wearehaus/jet-engine")] },
|
|
1782
|
+
{ stack: ["backend", "jetsmartfilters"], any: [dep("wearehaus/jet-smart-filters")] },
|
|
1783
|
+
{ stack: ["backend", "gravityforms"], any: [dep("wearehaus/gravityforms")] },
|
|
1784
|
+
{ stack: ["backend", "dotnet"], any: [fileEndsWith(".csproj"), fileEndsWith(".sln")] },
|
|
1785
|
+
{ stack: ["testing", "playwright"], any: [dep("@playwright/test")] },
|
|
1786
|
+
{ stack: ["testing", "storybook"], any: [fileIncludes(".storybook")] },
|
|
1787
|
+
{ stack: ["testing", "testing-library"], any: [depPrefix("@testing-library/")] },
|
|
1788
|
+
{ stack: ["testing", "phpunit"], any: [fileEndsWith("phpunit.xml")] },
|
|
1789
|
+
{ stack: ["testing", "storybook"], any: [depPrefix("@storybook/")] },
|
|
1790
|
+
{ stack: ["testing", "vitest"], any: [dep("vitest")] },
|
|
1791
|
+
{ stack: ["testing", "jest"], any: [dep("jest"), dep("jest-environment-jsdom")] },
|
|
1792
|
+
{ stack: ["databases", "postgresql"], any: [dep("pg")] },
|
|
1793
|
+
{ stack: ["databases", "mariadb"], any: [dep("mariadb"), dep("mysql2")] },
|
|
1794
|
+
{ stack: ["databases", "mysql"], any: [dep("mysql"), dep("mysql2")] },
|
|
1795
|
+
{ stack: ["databases", "mssql"], any: [dep("mssql")] },
|
|
1796
|
+
{ stack: ["databases", "elasticsearch"], any: [dep("@elastic/elasticsearch")] },
|
|
1797
|
+
{ stack: ["databases", "redis"], any: [dep("predis/predis"), dep("ioredis"), dep("redis")] },
|
|
1798
|
+
{ stack: ["auth", "oidc"], any: [content("openid")] },
|
|
1799
|
+
{ stack: ["auth", "azure-ad"], any: [content("AZURE_AD")] },
|
|
1800
|
+
{ stack: ["auth", "bankid"], any: [content("BANKID")] },
|
|
1801
|
+
{
|
|
1802
|
+
stack: ["auth", "saml2"],
|
|
1803
|
+
any: [dep("24slides/laravel-saml2"), dep("aacotroneo/laravel-saml2")]
|
|
1804
|
+
},
|
|
1805
|
+
{ stack: ["auth", "next-auth"], any: [dep("next-auth"), dep("@auth/core")] }
|
|
1806
|
+
];
|
|
1807
|
+
function runDetection(ctx, rules = STACK_RULES) {
|
|
1808
|
+
const roles = [];
|
|
1809
|
+
for (const rule of ROLE_RULES) {
|
|
1810
|
+
if (rule.role && matchRule(rule, ctx) && !roles.includes(rule.role)) roles.push(rule.role);
|
|
1811
|
+
}
|
|
1812
|
+
const stacks = {};
|
|
1813
|
+
for (const bucket of STACK_BUCKETS) stacks[bucket] = [];
|
|
1814
|
+
for (const rule of rules) {
|
|
1815
|
+
if (!rule.stack || !matchRule(rule, ctx)) continue;
|
|
1816
|
+
const [bucket, name] = rule.stack;
|
|
1817
|
+
stacks[bucket] ??= [];
|
|
1818
|
+
if (!stacks[bucket].includes(name)) stacks[bucket].push(name);
|
|
1819
|
+
}
|
|
1820
|
+
return { roles, stacks };
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// src/scanner/detection.ts
|
|
1824
|
+
import path15 from "path";
|
|
1825
|
+
var UNSUPPORTED_MARKERS = {
|
|
1826
|
+
"requirements.txt": "python",
|
|
1827
|
+
"pyproject.toml": "python",
|
|
1828
|
+
"go.mod": "go",
|
|
1829
|
+
"Cargo.toml": "rust",
|
|
1830
|
+
"pom.xml": "java",
|
|
1831
|
+
"build.gradle": "java",
|
|
1832
|
+
"build.gradle.kts": "java",
|
|
1833
|
+
Gemfile: "ruby"
|
|
1834
|
+
};
|
|
1835
|
+
var WEAK_STACK_SIGNALS = /* @__PURE__ */ new Set(["missing-prettier", "missing-eslint"]);
|
|
1836
|
+
function computeDetectionStatus(roles, stacks, unsupportedSignals) {
|
|
1837
|
+
const hasRealStack = Object.entries(stacks).some(
|
|
1838
|
+
([bucket, names]) => bucket !== "packageManagers" && names.some((n) => !WEAK_STACK_SIGNALS.has(n))
|
|
1839
|
+
);
|
|
1840
|
+
const hasRealSignal = roles.length > 0 || hasRealStack;
|
|
1841
|
+
if (!hasRealSignal) return "unknown";
|
|
1842
|
+
return unsupportedSignals.length > 0 ? "partial" : "supported";
|
|
1843
|
+
}
|
|
1468
1844
|
function blocked(rel) {
|
|
1469
|
-
return
|
|
1845
|
+
return SENSITIVE_PATH_REGEXES.some((x) => x.test(rel));
|
|
1470
1846
|
}
|
|
1847
|
+
function dependencySet(pkg, composer) {
|
|
1848
|
+
const depNames = /* @__PURE__ */ new Set();
|
|
1849
|
+
const pushObj = (obj) => {
|
|
1850
|
+
if (!isRecord(obj)) return;
|
|
1851
|
+
for (const key of Object.keys(obj)) depNames.add(key);
|
|
1852
|
+
};
|
|
1853
|
+
pushObj(pkg?.dependencies);
|
|
1854
|
+
pushObj(pkg?.devDependencies);
|
|
1855
|
+
pushObj(composer?.require);
|
|
1856
|
+
pushObj(composer?.["require-dev"]);
|
|
1857
|
+
return [...depNames].sort();
|
|
1858
|
+
}
|
|
1859
|
+
function finalizeRoles(registryRoles, deps, files) {
|
|
1860
|
+
const roles = new Set(registryRoles);
|
|
1861
|
+
const hasWpConfig = files.some((f) => f.endsWith("wp-config.php"));
|
|
1862
|
+
const hasBedrockLayout = files.some((f) => f.includes("web/app")) || deps.includes("roots/wordpress");
|
|
1863
|
+
if (hasWpConfig && hasBedrockLayout) {
|
|
1864
|
+
roles.add("wordpress-bedrock-site");
|
|
1865
|
+
roles.add("wordpress-site");
|
|
1866
|
+
} else if (hasWpConfig) {
|
|
1867
|
+
roles.add("wordpress-vanilla-site");
|
|
1868
|
+
roles.add("wordpress-site");
|
|
1869
|
+
} else if (deps.includes("roots/wordpress")) {
|
|
1870
|
+
roles.add("wordpress-bedrock-site");
|
|
1871
|
+
roles.add("wordpress-site");
|
|
1872
|
+
}
|
|
1873
|
+
return [...roles].sort();
|
|
1874
|
+
}
|
|
1875
|
+
function collectUnsupportedSignals(files) {
|
|
1876
|
+
return [
|
|
1877
|
+
...new Set(
|
|
1878
|
+
files.map((f) => UNSUPPORTED_MARKERS[path15.basename(f)]).filter((s) => Boolean(s))
|
|
1879
|
+
)
|
|
1880
|
+
].sort();
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// src/scanner/render.ts
|
|
1884
|
+
import { readFile } from "fs/promises";
|
|
1885
|
+
import path16 from "path";
|
|
1886
|
+
|
|
1887
|
+
// src/scanner/role-labels.ts
|
|
1888
|
+
var ROLE_LABELS = {
|
|
1889
|
+
"next-app": "a Next.js app",
|
|
1890
|
+
"react-app": "a React app",
|
|
1891
|
+
"vite-app": "a Vite app",
|
|
1892
|
+
"react-router-app": "a React Router app",
|
|
1893
|
+
"sanity-studio": "a Sanity Studio",
|
|
1894
|
+
"strapi-app": "a Strapi app",
|
|
1895
|
+
"expo-app": "an Expo app",
|
|
1896
|
+
"vendure-app": "a Vendure server",
|
|
1897
|
+
"vendure-plugin": "a Vendure plugin",
|
|
1898
|
+
"nestjs-api": "a NestJS API",
|
|
1899
|
+
"graphql-api": "a GraphQL API",
|
|
1900
|
+
"nx-monorepo": "an Nx monorepo",
|
|
1901
|
+
"turbo-monorepo": "a Turborepo monorepo",
|
|
1902
|
+
"laravel-app": "a Laravel app",
|
|
1903
|
+
"laravel-nova-app": "a Laravel Nova app",
|
|
1904
|
+
"dotnet-service": "a .NET service",
|
|
1905
|
+
"express-service": "an Express service",
|
|
1906
|
+
"wordpress-bedrock-site": "a WordPress (Bedrock) site",
|
|
1907
|
+
"wordpress-vanilla-site": "a WordPress site",
|
|
1908
|
+
"wordpress-site": "a WordPress site"
|
|
1909
|
+
};
|
|
1910
|
+
function article(word) {
|
|
1911
|
+
return /^[aeiou]/i.test(word) ? "an" : "a";
|
|
1912
|
+
}
|
|
1913
|
+
function friendlyRole(role) {
|
|
1914
|
+
const known = ROLE_LABELS[role];
|
|
1915
|
+
if (known) return known;
|
|
1916
|
+
const words = role.replace(/[-_]+/g, " ").trim();
|
|
1917
|
+
return words ? `${article(words)} ${words}` : "a project";
|
|
1918
|
+
}
|
|
1919
|
+
function joinRoles(labels) {
|
|
1920
|
+
if (labels.length === 0) return "";
|
|
1921
|
+
if (labels.length === 1) return labels[0];
|
|
1922
|
+
return `${labels.slice(0, -1).join(", ")} and ${labels[labels.length - 1]}`;
|
|
1923
|
+
}
|
|
1924
|
+
function describeRepo(context) {
|
|
1925
|
+
const labels = context.repoRoles.map(friendlyRole);
|
|
1926
|
+
const roleText = joinRoles(labels);
|
|
1927
|
+
if (context.detectionStatus === "unknown") {
|
|
1928
|
+
const markers = context.unsupportedSignals.join(", ");
|
|
1929
|
+
const detail = markers ? ` (I see ${markers})` : "";
|
|
1930
|
+
return `I couldn't fully recognise this stack${detail}, so I'll apply the general workflow and security guidance rather than framework-specific help.`;
|
|
1931
|
+
}
|
|
1932
|
+
const base = roleText ? `This looks like ${roleText}, using ${context.packageManager}.` : `I recognised this project's tooling (${context.packageManager}) but not a specific framework.`;
|
|
1933
|
+
if (context.detectionStatus === "partial" && context.unsupportedSignals.length > 0) {
|
|
1934
|
+
return `${base} I also see ${context.unsupportedSignals.join(", ")}, which haus doesn't fully support \u2014 guidance covers the recognised parts.`;
|
|
1935
|
+
}
|
|
1936
|
+
return base;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// src/scanner/render.ts
|
|
1940
|
+
async function buildContentBlob(root, files) {
|
|
1941
|
+
const candidates = files.filter(
|
|
1942
|
+
(f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
|
|
1943
|
+
);
|
|
1944
|
+
const slice = candidates.slice(0, 300);
|
|
1945
|
+
const CHUNK = 24;
|
|
1946
|
+
const parts = [];
|
|
1947
|
+
for (let i = 0; i < slice.length; i += CHUNK) {
|
|
1948
|
+
const batch = await Promise.all(
|
|
1949
|
+
slice.slice(i, i + CHUNK).map(async (rel) => {
|
|
1950
|
+
try {
|
|
1951
|
+
return await readFile(path16.join(root, rel), "utf8");
|
|
1952
|
+
} catch {
|
|
1953
|
+
return "";
|
|
1954
|
+
}
|
|
1955
|
+
})
|
|
1956
|
+
);
|
|
1957
|
+
parts.push(...batch);
|
|
1958
|
+
}
|
|
1959
|
+
return parts.join("\n");
|
|
1960
|
+
}
|
|
1961
|
+
function computeConfidence(roles, stacks) {
|
|
1962
|
+
const stackCount = Object.values(stacks).reduce((sum, arr) => sum + arr.length, 0);
|
|
1963
|
+
if (roles.length === 0) return 0.15;
|
|
1964
|
+
return Math.min(0.99, Number((0.4 + roles.length * 0.08 + stackCount * 0.02).toFixed(2)));
|
|
1965
|
+
}
|
|
1966
|
+
function renderSummary(context) {
|
|
1967
|
+
return `# Repo summary
|
|
1968
|
+
|
|
1969
|
+
${describeRepo(context)}
|
|
1970
|
+
|
|
1971
|
+
- Repo: ${context.repoName}
|
|
1972
|
+
- Package manager: ${context.packageManager}
|
|
1973
|
+
- Roles: ${context.repoRoles.join(", ") || "unknown"}
|
|
1974
|
+
- Generated: ${context.generatedAt}
|
|
1975
|
+
`;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// src/scanner/scan-project.ts
|
|
1979
|
+
var SAFE_FILES = [
|
|
1980
|
+
"package.json",
|
|
1981
|
+
"yarn.lock",
|
|
1982
|
+
"pnpm-lock.yaml",
|
|
1983
|
+
"composer.json",
|
|
1984
|
+
"composer.lock",
|
|
1985
|
+
"nx.json",
|
|
1986
|
+
"turbo.json",
|
|
1987
|
+
"tsconfig.json",
|
|
1988
|
+
"vite.config.*",
|
|
1989
|
+
"next.config.*",
|
|
1990
|
+
"tailwind.config.*",
|
|
1991
|
+
"components.json",
|
|
1992
|
+
"playwright.config.*",
|
|
1993
|
+
"phpunit.xml",
|
|
1994
|
+
"artisan",
|
|
1995
|
+
"routes/*.php",
|
|
1996
|
+
"app/Providers/*.php",
|
|
1997
|
+
"schema.graphql",
|
|
1998
|
+
"**/*.graphql",
|
|
1999
|
+
"**/vendure-config.*",
|
|
2000
|
+
"**/*module.ts",
|
|
2001
|
+
"web/app/**",
|
|
2002
|
+
"wp-content/plugins/**",
|
|
2003
|
+
"wp-content/themes/**",
|
|
2004
|
+
"wp-content/mu-plugins/**",
|
|
2005
|
+
"wp-content/acf-json/**",
|
|
2006
|
+
".storybook/**",
|
|
2007
|
+
".env.example",
|
|
2008
|
+
"wp-config.php",
|
|
2009
|
+
"**/*.csproj",
|
|
2010
|
+
"**/*.sln",
|
|
2011
|
+
"docker-compose.*",
|
|
2012
|
+
"Dockerfile",
|
|
2013
|
+
// Unsupported-ecosystem markers — matched by PRESENCE only (never content-read; none
|
|
2014
|
+
// match the content-blob extensions). Drive detectionStatus / unsupportedSignals.
|
|
2015
|
+
"requirements.txt",
|
|
2016
|
+
"pyproject.toml",
|
|
2017
|
+
"go.mod",
|
|
2018
|
+
"Cargo.toml",
|
|
2019
|
+
"pom.xml",
|
|
2020
|
+
"build.gradle",
|
|
2021
|
+
"build.gradle.kts",
|
|
2022
|
+
"Gemfile"
|
|
2023
|
+
];
|
|
1471
2024
|
async function scanProject(root, mode = "fast") {
|
|
1472
|
-
const pkg = await readJson(
|
|
1473
|
-
const composer = await readJson(
|
|
2025
|
+
const pkg = await readJson(path17.join(root, "package.json"));
|
|
2026
|
+
const composer = await readJson(path17.join(root, "composer.json"));
|
|
1474
2027
|
const files = await listFiles(root, SAFE_FILES);
|
|
1475
2028
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
1476
2029
|
const deps = dependencySet(pkg, composer);
|
|
1477
2030
|
const packageManager = detectPackageManager(root, String(pkg?.packageManager ?? ""));
|
|
1478
|
-
const
|
|
1479
|
-
const
|
|
2031
|
+
const contentBlob = await buildContentBlob(root, safeFiles);
|
|
2032
|
+
const detection = runDetection({ deps: new Set(deps), files: safeFiles, contentBlob });
|
|
2033
|
+
const roles = finalizeRoles(detection.roles, deps, safeFiles);
|
|
2034
|
+
const stacks = detection.stacks;
|
|
2035
|
+
if (packageManager === "yarn") stacks.packageManagers.push("yarn4");
|
|
2036
|
+
if (packageManager === "pnpm") stacks.packageManagers.push("pnpm89");
|
|
1480
2037
|
const warnings = [];
|
|
1481
2038
|
const securityRisks = [];
|
|
1482
2039
|
const crossRepoHints = [];
|
|
@@ -1494,11 +2051,13 @@ async function scanProject(root, mode = "fast") {
|
|
|
1494
2051
|
if (!safeFiles.some((f) => f.endsWith(".env.example"))) securityRisks.push("Missing env template");
|
|
1495
2052
|
if (safeFiles.some((f) => f.includes("wp-content/uploads")))
|
|
1496
2053
|
securityRisks.push("Uploads directory present");
|
|
2054
|
+
const unsupportedSignals = collectUnsupportedSignals(safeFiles);
|
|
2055
|
+
const detectionStatus = computeDetectionStatus(roles, stacks, unsupportedSignals);
|
|
1497
2056
|
const context = {
|
|
1498
2057
|
mode,
|
|
1499
2058
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1500
2059
|
root,
|
|
1501
|
-
repoName: String(pkg?.name ??
|
|
2060
|
+
repoName: String(pkg?.name ?? path17.basename(root)),
|
|
1502
2061
|
packageManager,
|
|
1503
2062
|
repoRoles: roles,
|
|
1504
2063
|
confidence: computeConfidence(roles, stacks),
|
|
@@ -1506,7 +2065,9 @@ async function scanProject(root, mode = "fast") {
|
|
|
1506
2065
|
dependencies: deps,
|
|
1507
2066
|
securityRisks,
|
|
1508
2067
|
crossRepoHints,
|
|
1509
|
-
warnings
|
|
2068
|
+
warnings,
|
|
2069
|
+
detectionStatus,
|
|
2070
|
+
unsupportedSignals
|
|
1510
2071
|
};
|
|
1511
2072
|
const dependencyMap = {
|
|
1512
2073
|
node: deps.filter((d) => !d.includes("/")),
|
|
@@ -1515,7 +2076,7 @@ async function scanProject(root, mode = "fast") {
|
|
|
1515
2076
|
const scanHashes = Object.fromEntries(
|
|
1516
2077
|
await Promise.all(
|
|
1517
2078
|
safeFiles.map(
|
|
1518
|
-
async (f) => [f, hashText(await
|
|
2079
|
+
async (f) => [f, hashText(await readFile2(path17.join(root, f), "utf8"))]
|
|
1519
2080
|
)
|
|
1520
2081
|
)
|
|
1521
2082
|
);
|
|
@@ -1526,184 +2087,6 @@ async function scanProject(root, mode = "fast") {
|
|
|
1526
2087
|
await writeText(hausPath(root, "repo-summary.md"), repoSummary);
|
|
1527
2088
|
return { ...context, dependencyMap, scanHashes, repoSummary };
|
|
1528
2089
|
}
|
|
1529
|
-
function dependencySet(pkg, composer) {
|
|
1530
|
-
const out = /* @__PURE__ */ new Set();
|
|
1531
|
-
const pushObj = (obj) => {
|
|
1532
|
-
if (!isRecord(obj)) return;
|
|
1533
|
-
for (const key of Object.keys(obj)) out.add(key);
|
|
1534
|
-
};
|
|
1535
|
-
pushObj(pkg?.dependencies);
|
|
1536
|
-
pushObj(pkg?.devDependencies);
|
|
1537
|
-
pushObj(composer?.require);
|
|
1538
|
-
pushObj(composer?.["require-dev"]);
|
|
1539
|
-
return [...out].sort();
|
|
1540
|
-
}
|
|
1541
|
-
function detectRoles(deps, files) {
|
|
1542
|
-
const roles = /* @__PURE__ */ new Set();
|
|
1543
|
-
if (deps.includes("next") || files.some((f) => f.includes("next.config."))) roles.add("next-app");
|
|
1544
|
-
if (deps.includes("react")) roles.add("react-app");
|
|
1545
|
-
if (deps.includes("vite") || files.some((f) => f.includes("vite.config."))) roles.add("vite-app");
|
|
1546
|
-
if (deps.includes("react-router") && deps.includes("@react-router/node"))
|
|
1547
|
-
roles.add("react-router-app");
|
|
1548
|
-
if (deps.includes("sanity")) roles.add("sanity-studio");
|
|
1549
|
-
if (deps.includes("@strapi/strapi") || deps.some((d) => d.startsWith("@strapi/")))
|
|
1550
|
-
roles.add("strapi-app");
|
|
1551
|
-
if (deps.includes("expo")) roles.add("expo-app");
|
|
1552
|
-
if (deps.includes("@vendure/core")) roles.add("vendure-app");
|
|
1553
|
-
if (deps.some((d) => d.startsWith("@haus/vendure-")) || files.some((f) => f.includes("vendure-config")))
|
|
1554
|
-
roles.add("vendure-plugin");
|
|
1555
|
-
if (deps.includes("@nestjs/core")) roles.add("nestjs-api");
|
|
1556
|
-
if (deps.includes("graphql") || deps.includes("@nestjs/graphql")) roles.add("graphql-api");
|
|
1557
|
-
if (files.some((f) => f.endsWith("nx.json"))) roles.add("nx-monorepo");
|
|
1558
|
-
if (files.some((f) => f.endsWith("turbo.json"))) roles.add("turbo-monorepo");
|
|
1559
|
-
if (files.some((f) => f.endsWith("artisan")) || deps.includes("laravel/framework"))
|
|
1560
|
-
roles.add("laravel-app");
|
|
1561
|
-
if (deps.includes("laravel/nova")) roles.add("laravel-nova-app");
|
|
1562
|
-
const hasWpConfig = files.some((f) => f.endsWith("wp-config.php"));
|
|
1563
|
-
const hasBedrockLayout = files.some((f) => f.includes("web/app")) || deps.includes("roots/wordpress");
|
|
1564
|
-
if (hasWpConfig && hasBedrockLayout) {
|
|
1565
|
-
roles.add("wordpress-bedrock-site");
|
|
1566
|
-
roles.add("wordpress-site");
|
|
1567
|
-
} else if (hasWpConfig) {
|
|
1568
|
-
roles.add("wordpress-vanilla-site");
|
|
1569
|
-
roles.add("wordpress-site");
|
|
1570
|
-
} else if (deps.includes("roots/wordpress")) {
|
|
1571
|
-
roles.add("wordpress-bedrock-site");
|
|
1572
|
-
roles.add("wordpress-site");
|
|
1573
|
-
}
|
|
1574
|
-
if (files.some((f) => f.endsWith(".csproj") || f.endsWith(".sln"))) roles.add("dotnet-service");
|
|
1575
|
-
if (deps.includes("express")) roles.add("express-service");
|
|
1576
|
-
return [...roles].sort();
|
|
1577
|
-
}
|
|
1578
|
-
async function detectStacks(root, deps, files, packageManager) {
|
|
1579
|
-
const out = {
|
|
1580
|
-
backend: [],
|
|
1581
|
-
frontend: [],
|
|
1582
|
-
databases: [],
|
|
1583
|
-
testing: [],
|
|
1584
|
-
auth: [],
|
|
1585
|
-
tooling: [],
|
|
1586
|
-
packageManagers: []
|
|
1587
|
-
};
|
|
1588
|
-
const add = (k, v) => {
|
|
1589
|
-
out[k] ??= [];
|
|
1590
|
-
if (!out[k].includes(v)) out[k].push(v);
|
|
1591
|
-
};
|
|
1592
|
-
if (deps.includes("next")) add("frontend", "nextjs");
|
|
1593
|
-
if (deps.includes("react")) add("frontend", "react19");
|
|
1594
|
-
if (deps.includes("vue")) add("frontend", "vue");
|
|
1595
|
-
if (deps.includes("vite")) add("frontend", "vite8");
|
|
1596
|
-
if (deps.includes("react-router") && deps.includes("@react-router/node"))
|
|
1597
|
-
add("frontend", "react-router-v7");
|
|
1598
|
-
if (deps.includes("tailwindcss") || files.some((f) => f.includes("tailwind.config."))) {
|
|
1599
|
-
add("frontend", "tailwindcss");
|
|
1600
|
-
}
|
|
1601
|
-
if (files.some((f) => f.endsWith("components.json")) && deps.includes("class-variance-authority")) {
|
|
1602
|
-
add("frontend", "shadcn");
|
|
1603
|
-
}
|
|
1604
|
-
if (deps.includes("typescript")) add("tooling", "typescript5");
|
|
1605
|
-
if (deps.includes("sanity") || deps.includes("next-sanity") || deps.includes("@sanity/client")) {
|
|
1606
|
-
add("backend", "sanity");
|
|
1607
|
-
}
|
|
1608
|
-
if (deps.includes("@strapi/strapi") || deps.some((d) => d.startsWith("@strapi/"))) {
|
|
1609
|
-
add("backend", "strapi");
|
|
1610
|
-
}
|
|
1611
|
-
if (deps.includes("prisma") || deps.includes("@prisma/client")) add("backend", "prisma");
|
|
1612
|
-
if (deps.includes("expo")) add("frontend", "expo");
|
|
1613
|
-
if (deps.includes("react-native")) add("frontend", "react-native");
|
|
1614
|
-
if (deps.includes("i18next") || deps.includes("react-i18next")) add("tooling", "i18next");
|
|
1615
|
-
if (deps.includes("bullmq")) add("tooling", "bullmq");
|
|
1616
|
-
if (files.some((f) => f === "Dockerfile" || f.startsWith("docker-compose")))
|
|
1617
|
-
add("tooling", "docker");
|
|
1618
|
-
if (deps.includes("pm2") || files.some((f) => f.includes("ecosystem.config")))
|
|
1619
|
-
add("tooling", "pm2");
|
|
1620
|
-
if (deps.some((d) => d.startsWith("@sentry/"))) add("tooling", "sentry");
|
|
1621
|
-
if (deps.includes("deployer/deployer")) add("tooling", "deployer-php");
|
|
1622
|
-
if (!deps.includes("prettier")) add("tooling", "missing-prettier");
|
|
1623
|
-
if (!deps.includes("eslint")) add("tooling", "missing-eslint");
|
|
1624
|
-
if (deps.includes("@stripe/stripe-js") || deps.includes("@stripe/react-stripe-js")) {
|
|
1625
|
-
add("tooling", "stripe");
|
|
1626
|
-
}
|
|
1627
|
-
if (deps.includes("@haus-tech/qliro-plugin")) add("tooling", "qliro");
|
|
1628
|
-
if (deps.includes("@supabase/supabase-js") || deps.some((d) => d.startsWith("@supabase/"))) {
|
|
1629
|
-
add("databases", "supabase");
|
|
1630
|
-
}
|
|
1631
|
-
if (deps.includes("@vendure/core")) add("backend", "vendure3");
|
|
1632
|
-
if (deps.includes("@nestjs/core")) add("backend", "nestjs");
|
|
1633
|
-
if (await hasNeedle(root, files, "NestFactory")) add("backend", "nestjs");
|
|
1634
|
-
if (await hasNeedle(root, files, "@VendurePlugin")) add("backend", "vendure3");
|
|
1635
|
-
if (deps.includes("graphql") || deps.includes("@nestjs/graphql")) add("backend", "graphql");
|
|
1636
|
-
if (files.some((f) => f.endsWith(".graphql") || f.endsWith("schema.graphql")))
|
|
1637
|
-
add("backend", "graphql");
|
|
1638
|
-
if (deps.includes("laravel/framework")) add("backend", "laravel");
|
|
1639
|
-
if (files.some((f) => f.includes("app/Providers/") || f.includes("routes/")))
|
|
1640
|
-
add("backend", "laravel");
|
|
1641
|
-
if (files.some((f) => f.endsWith("wp-config.php")) || deps.includes("roots/wordpress"))
|
|
1642
|
-
add("backend", "wordpress");
|
|
1643
|
-
if (deps.includes("wpackagist-plugin/elementor") || deps.includes("wearehaus/elementor-pro") || deps.includes("wpackagist-theme/hello-elementor")) {
|
|
1644
|
-
add("backend", "elementor");
|
|
1645
|
-
}
|
|
1646
|
-
if (deps.includes("wearehaus/advanced-custom-fields-pro") || deps.includes("wpackagist-plugin/advanced-custom-fields")) {
|
|
1647
|
-
add("backend", "acf-pro");
|
|
1648
|
-
}
|
|
1649
|
-
if (deps.includes("wearehaus/jet-engine")) add("backend", "jetengine");
|
|
1650
|
-
if (deps.includes("wearehaus/jet-smart-filters")) add("backend", "jetsmartfilters");
|
|
1651
|
-
if (deps.includes("wearehaus/gravityforms")) add("backend", "gravityforms");
|
|
1652
|
-
if (files.some((f) => f.endsWith(".csproj") || f.endsWith(".sln"))) add("backend", "dotnet");
|
|
1653
|
-
if (deps.includes("@playwright/test")) add("testing", "playwright");
|
|
1654
|
-
if (files.some((f) => f.includes(".storybook"))) add("testing", "storybook");
|
|
1655
|
-
if (deps.some((d) => d.startsWith("@testing-library/"))) add("testing", "testing-library");
|
|
1656
|
-
if (files.some((f) => f.endsWith("phpunit.xml"))) add("testing", "phpunit");
|
|
1657
|
-
if (deps.some((d) => d.startsWith("@storybook/"))) add("testing", "storybook");
|
|
1658
|
-
if (deps.includes("vitest")) add("testing", "vitest");
|
|
1659
|
-
if (deps.includes("jest") || deps.includes("jest-environment-jsdom")) add("testing", "jest");
|
|
1660
|
-
if (deps.includes("pg")) add("databases", "postgresql");
|
|
1661
|
-
if (deps.includes("mariadb") || deps.includes("mysql2")) add("databases", "mariadb");
|
|
1662
|
-
if (deps.includes("mysql") || deps.includes("mysql2")) add("databases", "mysql");
|
|
1663
|
-
if (deps.includes("mssql")) add("databases", "mssql");
|
|
1664
|
-
if (deps.includes("@elastic/elasticsearch")) add("databases", "elasticsearch");
|
|
1665
|
-
if (deps.includes("predis/predis") || deps.includes("ioredis") || deps.includes("redis")) {
|
|
1666
|
-
add("databases", "redis");
|
|
1667
|
-
}
|
|
1668
|
-
if (await hasNeedle(root, files, "openid")) add("auth", "oidc");
|
|
1669
|
-
if (await hasNeedle(root, files, "AZURE_AD")) add("auth", "azure-ad");
|
|
1670
|
-
if (await hasNeedle(root, files, "BANKID")) add("auth", "bankid");
|
|
1671
|
-
if (deps.includes("24slides/laravel-saml2") || deps.includes("aacotroneo/laravel-saml2")) {
|
|
1672
|
-
add("auth", "saml2");
|
|
1673
|
-
}
|
|
1674
|
-
if (deps.includes("next-auth") || deps.includes("@auth/core")) add("auth", "next-auth");
|
|
1675
|
-
if (packageManager === "yarn") add("packageManagers", "yarn4");
|
|
1676
|
-
if (packageManager === "pnpm") add("packageManagers", "pnpm89");
|
|
1677
|
-
return out;
|
|
1678
|
-
}
|
|
1679
|
-
async function hasNeedle(root, files, needle) {
|
|
1680
|
-
const candidates = files.filter(
|
|
1681
|
-
(f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
|
|
1682
|
-
);
|
|
1683
|
-
for (const rel of candidates.slice(0, 300)) {
|
|
1684
|
-
try {
|
|
1685
|
-
const content = await readFile(path15.join(root, rel), "utf8");
|
|
1686
|
-
if (content.includes(needle)) return true;
|
|
1687
|
-
} catch {
|
|
1688
|
-
continue;
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
return false;
|
|
1692
|
-
}
|
|
1693
|
-
function computeConfidence(roles, stacks) {
|
|
1694
|
-
const stackCount = Object.values(stacks).reduce((sum, arr) => sum + arr.length, 0);
|
|
1695
|
-
if (roles.length === 0) return 0.15;
|
|
1696
|
-
return Math.min(0.99, Number((0.4 + roles.length * 0.08 + stackCount * 0.02).toFixed(2)));
|
|
1697
|
-
}
|
|
1698
|
-
function renderSummary(context) {
|
|
1699
|
-
return `# Repo summary
|
|
1700
|
-
|
|
1701
|
-
- Repo: ${context.repoName}
|
|
1702
|
-
- Package manager: ${context.packageManager}
|
|
1703
|
-
- Roles: ${context.repoRoles.join(", ") || "unknown"}
|
|
1704
|
-
- Generated: ${context.generatedAt}
|
|
1705
|
-
`;
|
|
1706
|
-
}
|
|
1707
2090
|
|
|
1708
2091
|
// src/scanner/read-context.ts
|
|
1709
2092
|
async function readContextOrScan(root) {
|
|
@@ -1727,7 +2110,9 @@ async function runContext(options) {
|
|
|
1727
2110
|
(recommendationRaw?.recommended ?? []).map((item) => [item.id, item.scoreBreakdown])
|
|
1728
2111
|
);
|
|
1729
2112
|
const taskIntents = options.task ? classifyTaskIntents(options.task) : /* @__PURE__ */ new Set();
|
|
1730
|
-
const selected = pickTaskRelevantRules(recommendation, options.task, taskIntents
|
|
2113
|
+
const selected = pickTaskRelevantRules(recommendation, options.task, taskIntents, {
|
|
2114
|
+
tokenBudget: DEFAULT_CONTEXT_TOKEN_BUDGET
|
|
2115
|
+
});
|
|
1731
2116
|
const payload = {
|
|
1732
2117
|
task: options.task ?? "not provided",
|
|
1733
2118
|
taskIntents: [...taskIntents].sort(),
|
|
@@ -1775,8 +2160,8 @@ async function runContext(options) {
|
|
|
1775
2160
|
}
|
|
1776
2161
|
|
|
1777
2162
|
// src/commands/doctor.ts
|
|
1778
|
-
import
|
|
1779
|
-
import
|
|
2163
|
+
import path18 from "path";
|
|
2164
|
+
import fs12 from "fs-extra";
|
|
1780
2165
|
|
|
1781
2166
|
// src/update/npm-version.ts
|
|
1782
2167
|
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
@@ -1823,114 +2208,200 @@ async function runDoctor(options) {
|
|
|
1823
2208
|
const recommendation = await readJson(
|
|
1824
2209
|
hausPath(root, "recommendation.json")
|
|
1825
2210
|
);
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
2211
|
+
const detail = [];
|
|
2212
|
+
const attention = [];
|
|
2213
|
+
const ok = (text) => detail.push({ stream: "log", text });
|
|
2214
|
+
const flag = (text, sentence, fix) => {
|
|
2215
|
+
detail.push({ stream: "warn", text });
|
|
2216
|
+
attention.push({ sentence, fix });
|
|
2217
|
+
};
|
|
2218
|
+
ok(`Repo: ${context.repoName}`);
|
|
2219
|
+
ok(`Roles: ${context.repoRoles.join(", ") || "unknown"}`);
|
|
2220
|
+
ok(`Package manager: ${context.packageManager}`);
|
|
2221
|
+
ok(`Recommended items: ${recommendation?.recommended?.length ?? 0}`);
|
|
1831
2222
|
const warningLines = [.../* @__PURE__ */ new Set([...context.warnings, ...recommendation?.warnings ?? []])];
|
|
1832
2223
|
for (const warning of warningLines) {
|
|
1833
|
-
|
|
2224
|
+
ok(`- WARN: ${warning}`);
|
|
1834
2225
|
}
|
|
1835
2226
|
const hooks = await verifyProjectSettingsHooksContract(root);
|
|
1836
2227
|
if (hooks.skipped) {
|
|
1837
|
-
|
|
2228
|
+
ok(`- HOOKS: (skipped) ${hooks.message}`);
|
|
1838
2229
|
} else if (!hooks.ok) {
|
|
1839
|
-
|
|
2230
|
+
flag(
|
|
2231
|
+
`- HOOKS FAIL: ${hooks.message}`,
|
|
2232
|
+
"The Claude Code hooks don't match what haus expects",
|
|
2233
|
+
"haus apply --write"
|
|
2234
|
+
);
|
|
1840
2235
|
process.exitCode = 1;
|
|
1841
2236
|
} else {
|
|
1842
|
-
|
|
2237
|
+
ok(`- HOOKS OK: ${hooks.message}`);
|
|
1843
2238
|
}
|
|
1844
|
-
const gatedHooks = ["context"
|
|
2239
|
+
const gatedHooks = ["context"];
|
|
1845
2240
|
for (const key of gatedHooks) {
|
|
1846
2241
|
const enabled = await isHookEnabled(root, key);
|
|
1847
|
-
|
|
2242
|
+
ok(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
|
|
1848
2243
|
}
|
|
1849
|
-
const rootClaudeMdPath =
|
|
2244
|
+
const rootClaudeMdPath = path18.join(root, "CLAUDE.md");
|
|
1850
2245
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
1851
2246
|
if (!rootClaudeMdContent) {
|
|
1852
|
-
|
|
2247
|
+
flag(
|
|
2248
|
+
"- CLAUDE.md: missing (run `haus apply --write` to create)",
|
|
2249
|
+
"Your project's CLAUDE.md is missing, so haus guidance never loads",
|
|
2250
|
+
"haus apply --write"
|
|
2251
|
+
);
|
|
1853
2252
|
} else if (!rootClaudeMdContent.includes(BLOCK_BEGIN)) {
|
|
1854
|
-
|
|
2253
|
+
flag(
|
|
2254
|
+
"- CLAUDE.md: haus import block missing (run `haus apply --write` to add)",
|
|
2255
|
+
"The haus import block is missing from CLAUDE.md, so its guidance never loads",
|
|
2256
|
+
"haus apply --write"
|
|
2257
|
+
);
|
|
1855
2258
|
} else {
|
|
1856
|
-
|
|
2259
|
+
const beginIdx = rootClaudeMdContent.indexOf(BLOCK_BEGIN);
|
|
2260
|
+
const endIdx = rootClaudeMdContent.indexOf(BLOCK_END, beginIdx + BLOCK_BEGIN.length);
|
|
2261
|
+
if (endIdx < 0) {
|
|
2262
|
+
flag(
|
|
2263
|
+
"- CLAUDE.md: haus import block is not closed (run `haus apply --write` to repair)",
|
|
2264
|
+
"The haus import block in CLAUDE.md is broken, so its guidance may not load",
|
|
2265
|
+
"haus apply --write"
|
|
2266
|
+
);
|
|
2267
|
+
} else {
|
|
2268
|
+
ok("- CLAUDE.md: import block present");
|
|
2269
|
+
const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
|
|
2270
|
+
const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
|
|
2271
|
+
for (const target of importTargets) {
|
|
2272
|
+
if (!await fs12.pathExists(hausPath(root, target))) {
|
|
2273
|
+
flag(
|
|
2274
|
+
`- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
|
|
2275
|
+
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
2276
|
+
"haus apply --write"
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
1857
2281
|
}
|
|
1858
2282
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
1859
|
-
const workflowExists = await
|
|
2283
|
+
const workflowExists = await fs12.pathExists(workflowPath);
|
|
1860
2284
|
if (!workflowExists) {
|
|
1861
|
-
|
|
2285
|
+
flag(
|
|
2286
|
+
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
2287
|
+
"The workflow standard file is missing",
|
|
2288
|
+
"haus apply --write"
|
|
2289
|
+
);
|
|
1862
2290
|
} else {
|
|
1863
2291
|
const workflowContent = await readText(workflowPath);
|
|
1864
2292
|
const firstLine = workflowContent?.split("\n")[0] ?? "";
|
|
1865
2293
|
if (!firstLine.includes("HAUS-MANAGED")) {
|
|
1866
|
-
|
|
2294
|
+
ok("- .haus-workflow/WORKFLOW.md: OK (user-owned)");
|
|
1867
2295
|
} else {
|
|
1868
2296
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
1869
|
-
const cachePath =
|
|
1870
|
-
const bundledPath =
|
|
2297
|
+
const cachePath = path18.join(CACHE_DIR, "templates/agentic-workflow-standard.md");
|
|
2298
|
+
const bundledPath = path18.join(
|
|
1871
2299
|
packageRoot(),
|
|
1872
2300
|
"library",
|
|
1873
2301
|
"global",
|
|
1874
2302
|
"templates",
|
|
1875
2303
|
"agentic-workflow-standard.md"
|
|
1876
2304
|
);
|
|
1877
|
-
const templatePath = await
|
|
2305
|
+
const templatePath = await fs12.pathExists(cachePath) ? cachePath : bundledPath;
|
|
1878
2306
|
const templateContent = await readText(templatePath);
|
|
1879
2307
|
if (storedHashMatch && templateContent) {
|
|
1880
2308
|
const currentHash = hashText(normaliseLF(templateContent));
|
|
1881
2309
|
if (storedHashMatch[1] !== currentHash) {
|
|
1882
|
-
|
|
2310
|
+
flag(
|
|
2311
|
+
"- .haus-workflow/WORKFLOW.md: stale (template updated \u2014 run `haus apply --write`)",
|
|
2312
|
+
"The workflow standard is out of date",
|
|
2313
|
+
"haus apply --write"
|
|
2314
|
+
);
|
|
1883
2315
|
} else {
|
|
1884
|
-
|
|
2316
|
+
ok("- .haus-workflow/WORKFLOW.md: OK");
|
|
1885
2317
|
}
|
|
1886
2318
|
} else {
|
|
1887
|
-
|
|
2319
|
+
ok("- .haus-workflow/WORKFLOW.md: OK");
|
|
1888
2320
|
}
|
|
1889
2321
|
}
|
|
1890
2322
|
}
|
|
1891
2323
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
1892
|
-
const workflowConfigExists = await
|
|
2324
|
+
const workflowConfigExists = await fs12.pathExists(workflowConfigPath);
|
|
1893
2325
|
if (!workflowConfigExists) {
|
|
1894
|
-
|
|
2326
|
+
flag(
|
|
2327
|
+
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
2328
|
+
"The workflow config file is missing",
|
|
2329
|
+
"haus apply --write"
|
|
2330
|
+
);
|
|
1895
2331
|
} else {
|
|
1896
|
-
|
|
2332
|
+
const cfg = await fs12.readFile(workflowConfigPath, "utf8");
|
|
2333
|
+
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
2334
|
+
if (unfilled > 0) {
|
|
2335
|
+
flag(
|
|
2336
|
+
`- .haus-workflow/workflow-config.md: ${unfilled} field(s) still unfilled (run \`haus apply --refill-config\` to auto-fill detectable ones)`,
|
|
2337
|
+
`${unfilled} workflow-config field(s) are still blank`,
|
|
2338
|
+
"haus apply --refill-config"
|
|
2339
|
+
);
|
|
2340
|
+
} else {
|
|
2341
|
+
ok("- .haus-workflow/workflow-config.md: OK (project-owned)");
|
|
2342
|
+
}
|
|
1897
2343
|
}
|
|
1898
2344
|
const projectMdPath = hausPath(root, "project.md");
|
|
1899
|
-
const projectMdExists = await
|
|
2345
|
+
const projectMdExists = await fs12.pathExists(projectMdPath);
|
|
1900
2346
|
if (!projectMdExists) {
|
|
1901
|
-
|
|
2347
|
+
flag(
|
|
2348
|
+
"- .haus-workflow/project.md: missing (run `haus apply --write`)",
|
|
2349
|
+
"The project facts file is missing",
|
|
2350
|
+
"haus apply --write"
|
|
2351
|
+
);
|
|
1902
2352
|
} else {
|
|
1903
2353
|
const projectMdContent = await readText(projectMdPath);
|
|
1904
2354
|
const hasHeader = projectMdContent?.split("\n")[0]?.includes("HAUS-MANAGED") ?? false;
|
|
1905
2355
|
if (!hasHeader) {
|
|
1906
|
-
|
|
2356
|
+
ok("- .haus-workflow/project.md: no HAUS-MANAGED header (user-owned)");
|
|
1907
2357
|
} else {
|
|
1908
|
-
|
|
2358
|
+
ok("- .haus-workflow/project.md: OK");
|
|
1909
2359
|
}
|
|
1910
2360
|
}
|
|
1911
2361
|
const cacheAgeMs = await getCacheManifestAge();
|
|
1912
2362
|
if (cacheAgeMs === null) {
|
|
1913
|
-
|
|
2363
|
+
flag(
|
|
2364
|
+
"- CATALOG CACHE: absent (run `haus update` to populate)",
|
|
2365
|
+
"The catalog cache hasn't been downloaded yet",
|
|
2366
|
+
"haus update"
|
|
2367
|
+
);
|
|
1914
2368
|
} else {
|
|
1915
2369
|
const cacheAgeDays = Math.floor(cacheAgeMs / (1e3 * 60 * 60 * 24));
|
|
1916
2370
|
if (cacheAgeDays >= 7) {
|
|
1917
|
-
|
|
2371
|
+
flag(
|
|
2372
|
+
`- CATALOG CACHE: stale (${cacheAgeDays}d old \u2014 run \`haus update\`)`,
|
|
2373
|
+
`The catalog cache is ${cacheAgeDays} days old`,
|
|
2374
|
+
"haus update"
|
|
2375
|
+
);
|
|
1918
2376
|
} else {
|
|
1919
|
-
|
|
2377
|
+
ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
|
|
1920
2378
|
}
|
|
1921
2379
|
}
|
|
1922
|
-
const pkgJson = await readJson(
|
|
2380
|
+
const pkgJson = await readJson(path18.join(packageRoot(), "package.json"));
|
|
1923
2381
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
1924
2382
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
1925
2383
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
1926
|
-
|
|
1927
|
-
`- CLI UPDATE: ${currentVersion} \u2192 ${npmStatus.latest} available (run: npm install -g ${NPM_PACKAGE_NAME})
|
|
2384
|
+
flag(
|
|
2385
|
+
`- CLI UPDATE: ${currentVersion} \u2192 ${npmStatus.latest} available (run: npm install -g ${NPM_PACKAGE_NAME})`,
|
|
2386
|
+
`A newer haus (${npmStatus.latest}) is available`,
|
|
2387
|
+
`npm install -g ${NPM_PACKAGE_NAME}`
|
|
1928
2388
|
);
|
|
1929
2389
|
process.exitCode = 1;
|
|
1930
2390
|
} else if (npmStatus.latest !== null) {
|
|
1931
|
-
|
|
2391
|
+
ok(`- CLI: ${currentVersion} (up to date)`);
|
|
1932
2392
|
} else {
|
|
1933
|
-
|
|
2393
|
+
ok(`- CLI: ${currentVersion} (version check unavailable)`);
|
|
2394
|
+
}
|
|
2395
|
+
if (attention.length === 0) {
|
|
2396
|
+
log("\u2705 Your project is set up and healthy.");
|
|
2397
|
+
} else {
|
|
2398
|
+
log(`\u26A0\uFE0F ${attention.length} thing(s) need attention:`);
|
|
2399
|
+
for (const a of attention) log(` \u2022 ${a.sentence} \u2014 fix: ${a.fix}`);
|
|
2400
|
+
}
|
|
2401
|
+
log("Haus Doctor");
|
|
2402
|
+
for (const line2 of detail) {
|
|
2403
|
+
if (line2.stream === "warn") warn(line2.text);
|
|
2404
|
+
else log(line2.text);
|
|
1934
2405
|
}
|
|
1935
2406
|
}
|
|
1936
2407
|
|
|
@@ -1989,57 +2460,17 @@ async function runExplainRecommendation(options) {
|
|
|
1989
2460
|
// src/commands/guard.ts
|
|
1990
2461
|
import { readFileSync as readFileSync2 } from "fs";
|
|
1991
2462
|
|
|
1992
|
-
// src/security/dangerous-commands.ts
|
|
1993
|
-
var DANGEROUS_COMMANDS = [
|
|
1994
|
-
"rm -rf",
|
|
1995
|
-
"sudo",
|
|
1996
|
-
"chmod -R 777",
|
|
1997
|
-
"chown -R",
|
|
1998
|
-
"git push --force",
|
|
1999
|
-
"git reset --hard",
|
|
2000
|
-
"docker system prune",
|
|
2001
|
-
"drop database",
|
|
2002
|
-
"truncate table",
|
|
2003
|
-
"php artisan migrate --force",
|
|
2004
|
-
"npm publish",
|
|
2005
|
-
"yarn npm publish",
|
|
2006
|
-
"pnpm publish"
|
|
2007
|
-
];
|
|
2008
|
-
|
|
2009
2463
|
// src/security/guard-bash.ts
|
|
2010
2464
|
function guardBash(command) {
|
|
2011
2465
|
const matched = DANGEROUS_COMMANDS.find((token) => command.includes(token));
|
|
2012
|
-
if (matched) return `
|
|
2466
|
+
if (matched) return `I didn't run that \u2014 it can permanently change or delete things: ${command}`;
|
|
2013
2467
|
return void 0;
|
|
2014
2468
|
}
|
|
2015
2469
|
|
|
2016
|
-
// src/security/sensitive-paths.ts
|
|
2017
|
-
var SENSITIVE_PATHS = [
|
|
2018
|
-
".env",
|
|
2019
|
-
".env.*",
|
|
2020
|
-
"*.pem",
|
|
2021
|
-
"*.key",
|
|
2022
|
-
"*.p12",
|
|
2023
|
-
"*.pfx",
|
|
2024
|
-
"id_rsa",
|
|
2025
|
-
"id_ed25519",
|
|
2026
|
-
"*.sql",
|
|
2027
|
-
"*.dump",
|
|
2028
|
-
"*.backup",
|
|
2029
|
-
"*.bak",
|
|
2030
|
-
"storage/logs",
|
|
2031
|
-
"wp-content/uploads",
|
|
2032
|
-
"uploads",
|
|
2033
|
-
"customer-data",
|
|
2034
|
-
"exports",
|
|
2035
|
-
"secrets",
|
|
2036
|
-
"certs"
|
|
2037
|
-
];
|
|
2038
|
-
|
|
2039
2470
|
// src/security/guard-file-access.ts
|
|
2040
2471
|
function guardFileAccess(candidate) {
|
|
2041
2472
|
const matched = SENSITIVE_PATHS.find((token) => candidate.includes(token.replace("*", "")));
|
|
2042
|
-
if (matched) return `
|
|
2473
|
+
if (matched) return `I didn't open ${candidate} \u2014 it looks like it holds secrets or sensitive data`;
|
|
2043
2474
|
return void 0;
|
|
2044
2475
|
}
|
|
2045
2476
|
|
|
@@ -2071,23 +2502,136 @@ async function runGuard(kind, _options) {
|
|
|
2071
2502
|
const toolInput = isRecord(payload.tool_input) ? payload.tool_input : {};
|
|
2072
2503
|
if (kind === "file-access") {
|
|
2073
2504
|
const candidate = String(toolInput.path ?? toolInput.file_path ?? "");
|
|
2074
|
-
|
|
2075
|
-
|
|
2505
|
+
const reason2 = guardFileAccess(candidate);
|
|
2506
|
+
if (reason2) {
|
|
2507
|
+
deny(reason2);
|
|
2076
2508
|
process.exitCode = 1;
|
|
2077
2509
|
return;
|
|
2078
2510
|
}
|
|
2079
2511
|
return;
|
|
2080
2512
|
}
|
|
2081
2513
|
const command = String(toolInput.command ?? "");
|
|
2082
|
-
|
|
2083
|
-
|
|
2514
|
+
const reason = guardBash(command);
|
|
2515
|
+
if (reason) {
|
|
2516
|
+
deny(reason);
|
|
2084
2517
|
process.exitCode = 1;
|
|
2085
2518
|
}
|
|
2086
2519
|
}
|
|
2087
2520
|
|
|
2088
2521
|
// src/commands/init.ts
|
|
2089
|
-
import
|
|
2090
|
-
import
|
|
2522
|
+
import path19 from "path";
|
|
2523
|
+
import fs13 from "fs-extra";
|
|
2524
|
+
|
|
2525
|
+
// src/recommender/ecosystem.ts
|
|
2526
|
+
var ECOSYSTEM_GROUPS = {
|
|
2527
|
+
laravel: ["laravel-app", "laravel-nova-app"],
|
|
2528
|
+
wordpress: ["wordpress-site", "wordpress-bedrock-site", "wordpress-vanilla-site"],
|
|
2529
|
+
vendure: ["vendure-app", "vendure-plugin"],
|
|
2530
|
+
nestjs: ["nestjs-api"],
|
|
2531
|
+
nextjs: ["next-app"],
|
|
2532
|
+
react: ["react-app", "next-app", "design-system"],
|
|
2533
|
+
vue: ["vue-app"],
|
|
2534
|
+
dotnet: ["dotnet-service"],
|
|
2535
|
+
nx: ["nx-monorepo"],
|
|
2536
|
+
turbo: ["turbo-monorepo"]
|
|
2537
|
+
};
|
|
2538
|
+
var ECOSYSTEM_PRIMARY_BACKENDS = /* @__PURE__ */ new Set([
|
|
2539
|
+
"laravel",
|
|
2540
|
+
"wordpress",
|
|
2541
|
+
"vendure",
|
|
2542
|
+
"nestjs",
|
|
2543
|
+
"dotnet"
|
|
2544
|
+
]);
|
|
2545
|
+
var ECOSYSTEM_COMPATIBLE_BACKENDS = {
|
|
2546
|
+
vendure: /* @__PURE__ */ new Set(["vendure", "nestjs"]),
|
|
2547
|
+
nestjs: /* @__PURE__ */ new Set(["nestjs"]),
|
|
2548
|
+
laravel: /* @__PURE__ */ new Set(["laravel"]),
|
|
2549
|
+
wordpress: /* @__PURE__ */ new Set(["wordpress"]),
|
|
2550
|
+
dotnet: /* @__PURE__ */ new Set(["dotnet"])
|
|
2551
|
+
};
|
|
2552
|
+
function inferRepoEcosystems(roles) {
|
|
2553
|
+
const ecosystems = /* @__PURE__ */ new Set();
|
|
2554
|
+
for (const [eco, roleList] of Object.entries(ECOSYSTEM_GROUPS)) {
|
|
2555
|
+
if (roleList.some((r) => roles.includes(r))) ecosystems.add(eco);
|
|
2556
|
+
}
|
|
2557
|
+
return [...ecosystems];
|
|
2558
|
+
}
|
|
2559
|
+
function pickDominantBackend(ecosystems) {
|
|
2560
|
+
for (const eco of ecosystems) {
|
|
2561
|
+
if (ECOSYSTEM_PRIMARY_BACKENDS.has(eco)) return eco;
|
|
2562
|
+
}
|
|
2563
|
+
return void 0;
|
|
2564
|
+
}
|
|
2565
|
+
function isBackendEcosystem(eco) {
|
|
2566
|
+
return ECOSYSTEM_PRIMARY_BACKENDS.has(eco);
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
// src/recommender/policies.ts
|
|
2570
|
+
var UNSUPPORTED = [
|
|
2571
|
+
"python",
|
|
2572
|
+
"django",
|
|
2573
|
+
"go",
|
|
2574
|
+
"rust",
|
|
2575
|
+
"java",
|
|
2576
|
+
"spring",
|
|
2577
|
+
"kotlin",
|
|
2578
|
+
"swift",
|
|
2579
|
+
"android",
|
|
2580
|
+
"flutter",
|
|
2581
|
+
"dart",
|
|
2582
|
+
"c++",
|
|
2583
|
+
"perl",
|
|
2584
|
+
"defi",
|
|
2585
|
+
"trading"
|
|
2586
|
+
];
|
|
2587
|
+
function matchRequiresAny(clauses, ctx) {
|
|
2588
|
+
for (const clause of clauses) {
|
|
2589
|
+
if ("stack" in clause) {
|
|
2590
|
+
if (ctx.stackSet.has(clause.stack.toLowerCase())) {
|
|
2591
|
+
return { matched: true, signal: `stack:${clause.stack}` };
|
|
2592
|
+
}
|
|
2593
|
+
} else if ("dependency" in clause) {
|
|
2594
|
+
if (ctx.depSet.has(clause.dependency.toLowerCase())) {
|
|
2595
|
+
return { matched: true, signal: `dependency:${clause.dependency}` };
|
|
2596
|
+
}
|
|
2597
|
+
} else if ("packageNamePattern" in clause) {
|
|
2598
|
+
const pattern = clause.packageNamePattern.toLowerCase();
|
|
2599
|
+
const prefix = pattern.endsWith("*") ? pattern.slice(0, -1) : pattern;
|
|
2600
|
+
for (const dep2 of ctx.depSet) {
|
|
2601
|
+
if (pattern.endsWith("*") ? dep2.startsWith(prefix) : dep2 === pattern) {
|
|
2602
|
+
return {
|
|
2603
|
+
matched: true,
|
|
2604
|
+
signal: `packageNamePattern:${clause.packageNamePattern}`
|
|
2605
|
+
};
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
} else if ("role" in clause) {
|
|
2609
|
+
if (ctx.roleSet.has(clause.role.toLowerCase())) {
|
|
2610
|
+
return { matched: true, signal: `role:${clause.role}` };
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
return { matched: false };
|
|
2615
|
+
}
|
|
2616
|
+
function describeRequiresAny(clauses) {
|
|
2617
|
+
return clauses.map((c) => {
|
|
2618
|
+
if ("stack" in c) return `stack=${c.stack}`;
|
|
2619
|
+
if ("dependency" in c) return `dependency=${c.dependency}`;
|
|
2620
|
+
if ("packageNamePattern" in c) return `packageNamePattern=${c.packageNamePattern}`;
|
|
2621
|
+
if ("role" in c) return `role=${c.role}`;
|
|
2622
|
+
return "unknown";
|
|
2623
|
+
}).join(" | ");
|
|
2624
|
+
}
|
|
2625
|
+
function mergeRecommendationWarnings(context) {
|
|
2626
|
+
const markers = context.unsupportedSignals?.join(", ");
|
|
2627
|
+
const statusLines = context.detectionStatus === "unknown" ? [
|
|
2628
|
+
markers ? `Stack not recognised \u2014 detected ${markers}, which haus does not support. Only stack-agnostic workflow and security guidance is applied.` : "Stack not recognised \u2014 no supported framework detected. Only stack-agnostic workflow and security guidance is applied."
|
|
2629
|
+
] : context.detectionStatus === "partial" && markers ? [
|
|
2630
|
+
`Partially supported \u2014 found unsupported ${markers} alongside recognised stacks; guidance covers the supported parts only.`
|
|
2631
|
+
] : [];
|
|
2632
|
+
const riskLines = (context.securityRisks?.length ?? 0) > 0 ? [`Scan reported security signals: ${context.securityRisks.join("; ")}`] : [];
|
|
2633
|
+
return [.../* @__PURE__ */ new Set([...statusLines, ...context.warnings, ...riskLines])];
|
|
2634
|
+
}
|
|
2091
2635
|
|
|
2092
2636
|
// src/utils/exec.ts
|
|
2093
2637
|
import { execa } from "execa";
|
|
@@ -2110,49 +2654,43 @@ async function runCommand(command, args = [], options = {}) {
|
|
|
2110
2654
|
throw new Error(`Failed to run command: ${command} ${args.join(" ")} (${message})`);
|
|
2111
2655
|
}
|
|
2112
2656
|
}
|
|
2113
|
-
async function runGit(args, options = {}) {
|
|
2114
|
-
return runCommand("git", args, options);
|
|
2115
|
-
}
|
|
2657
|
+
async function runGit(args, options = {}) {
|
|
2658
|
+
return runCommand("git", args, options);
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
// src/recommender/scoring.ts
|
|
2662
|
+
function computeConfidenceLevel(args) {
|
|
2663
|
+
const { isDefaultBaseline, reasons, hasEcosystemConflict, score } = args;
|
|
2664
|
+
const positiveCodes = new Set(reasons.map((r) => r.code));
|
|
2665
|
+
positiveCodes.delete("default-baseline");
|
|
2666
|
+
const distinctSignals = positiveCodes.size;
|
|
2667
|
+
const strongCount = (positiveCodes.has("repo-role-match") ? 1 : 0) + (positiveCodes.has("stack-match") ? 1 : 0) + (positiveCodes.has("requires-any-match") ? 1 : 0);
|
|
2668
|
+
if (hasEcosystemConflict) return "low";
|
|
2669
|
+
if (isDefaultBaseline && distinctSignals === 0) return "medium";
|
|
2670
|
+
if (strongCount >= 2 && score >= 70) return "high";
|
|
2671
|
+
if (strongCount >= 1 && distinctSignals >= 2 && score >= 50) return "medium";
|
|
2672
|
+
if (distinctSignals === 1) return "low";
|
|
2673
|
+
return distinctSignals >= 2 ? "medium" : "low";
|
|
2674
|
+
}
|
|
2675
|
+
function confidenceLevelToNumber(level, score) {
|
|
2676
|
+
const base = level === "high" ? 0.85 : level === "medium" ? 0.6 : 0.3;
|
|
2677
|
+
const bonus = Math.min(0.1, Math.max(0, score - 40) / 1e3);
|
|
2678
|
+
return Number(Math.min(0.99, base + bonus).toFixed(2));
|
|
2679
|
+
}
|
|
2680
|
+
async function readChangedFiles(root) {
|
|
2681
|
+
if (process.env.HAUS_DISABLE_GIT_SIGNALS === "1") return [];
|
|
2682
|
+
try {
|
|
2683
|
+
const result = await runGit(["diff", "--name-only"], { cwd: root });
|
|
2684
|
+
if (result.exitCode !== 0) {
|
|
2685
|
+
return [];
|
|
2686
|
+
}
|
|
2687
|
+
return result.stdout.split("\n").map((x) => x.trim()).filter(Boolean).sort();
|
|
2688
|
+
} catch {
|
|
2689
|
+
return [];
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2116
2692
|
|
|
2117
2693
|
// src/recommender/recommend.ts
|
|
2118
|
-
var UNSUPPORTED = [
|
|
2119
|
-
"python",
|
|
2120
|
-
"django",
|
|
2121
|
-
"go",
|
|
2122
|
-
"rust",
|
|
2123
|
-
"java",
|
|
2124
|
-
"spring",
|
|
2125
|
-
"kotlin",
|
|
2126
|
-
"swift",
|
|
2127
|
-
"android",
|
|
2128
|
-
"flutter",
|
|
2129
|
-
"dart",
|
|
2130
|
-
"c++",
|
|
2131
|
-
"perl",
|
|
2132
|
-
"defi",
|
|
2133
|
-
"trading"
|
|
2134
|
-
];
|
|
2135
|
-
var SENSITIVE2 = [".env", "secrets", "certs", "customer-data", "exports", ".pem", ".key"];
|
|
2136
|
-
var ECOSYSTEM_GROUPS = {
|
|
2137
|
-
laravel: ["laravel-app", "laravel-nova-app"],
|
|
2138
|
-
wordpress: ["wordpress-site", "wordpress-bedrock-site", "wordpress-vanilla-site"],
|
|
2139
|
-
vendure: ["vendure-app", "vendure-plugin"],
|
|
2140
|
-
nestjs: ["nestjs-api"],
|
|
2141
|
-
nextjs: ["next-app"],
|
|
2142
|
-
react: ["react-app", "next-app", "design-system"],
|
|
2143
|
-
vue: ["vue-app"],
|
|
2144
|
-
dotnet: ["dotnet-service"],
|
|
2145
|
-
nx: ["nx-monorepo"],
|
|
2146
|
-
turbo: ["turbo-monorepo"]
|
|
2147
|
-
};
|
|
2148
|
-
var ECOSYSTEM_PRIMARY_BACKENDS = /* @__PURE__ */ new Set(["laravel", "wordpress", "vendure", "nestjs", "dotnet"]);
|
|
2149
|
-
var ECOSYSTEM_COMPATIBLE_BACKENDS = {
|
|
2150
|
-
vendure: /* @__PURE__ */ new Set(["vendure", "nestjs"]),
|
|
2151
|
-
nestjs: /* @__PURE__ */ new Set(["nestjs"]),
|
|
2152
|
-
laravel: /* @__PURE__ */ new Set(["laravel"]),
|
|
2153
|
-
wordpress: /* @__PURE__ */ new Set(["wordpress"]),
|
|
2154
|
-
dotnet: /* @__PURE__ */ new Set(["dotnet"])
|
|
2155
|
-
};
|
|
2156
2694
|
async function recommend(root, context) {
|
|
2157
2695
|
const items = await loadCatalog(root);
|
|
2158
2696
|
const setupAnswers = await readJson(hausPath(root, "setup-answers.json")) ?? {};
|
|
@@ -2171,8 +2709,8 @@ async function recommend(root, context) {
|
|
|
2171
2709
|
const changedFiles = await readChangedFiles(root);
|
|
2172
2710
|
const securityRiskCount = context.securityRisks?.length ?? 0;
|
|
2173
2711
|
for (const item of items) {
|
|
2174
|
-
const
|
|
2175
|
-
if (UNSUPPORTED.some((x) =>
|
|
2712
|
+
const itemSearchText = `${item.id} ${item.tags.join(" ")}`.toLowerCase();
|
|
2713
|
+
if (UNSUPPORTED.some((x) => itemSearchText.includes(x))) {
|
|
2176
2714
|
skipped.push({
|
|
2177
2715
|
id: item.id,
|
|
2178
2716
|
reason: "Unsupported stack policy",
|
|
@@ -2334,7 +2872,7 @@ async function recommend(root, context) {
|
|
|
2334
2872
|
);
|
|
2335
2873
|
}
|
|
2336
2874
|
}
|
|
2337
|
-
if (
|
|
2875
|
+
if (SENSITIVE_ITEM_KEYWORDS.some((x) => itemSearchText.includes(x))) {
|
|
2338
2876
|
pushSkipReason("sensitive-policy", "Sensitive content policy block", 100);
|
|
2339
2877
|
}
|
|
2340
2878
|
const trust = sourceTrust.get(item.source);
|
|
@@ -2389,7 +2927,8 @@ async function recommend(root, context) {
|
|
|
2389
2927
|
finalScore: score
|
|
2390
2928
|
},
|
|
2391
2929
|
tags: item.tags,
|
|
2392
|
-
ecosystem: item.ecosystem
|
|
2930
|
+
ecosystem: item.ecosystem,
|
|
2931
|
+
tokenEstimate: item.tokenEstimate
|
|
2393
2932
|
});
|
|
2394
2933
|
} else {
|
|
2395
2934
|
if (skipReasons.length === 0) {
|
|
@@ -2430,94 +2969,6 @@ function buildStackSet(context) {
|
|
|
2430
2969
|
)
|
|
2431
2970
|
);
|
|
2432
2971
|
}
|
|
2433
|
-
function inferRepoEcosystems(roles) {
|
|
2434
|
-
const ecosystems = /* @__PURE__ */ new Set();
|
|
2435
|
-
for (const [eco, roleList] of Object.entries(ECOSYSTEM_GROUPS)) {
|
|
2436
|
-
if (roleList.some((r) => roles.includes(r))) ecosystems.add(eco);
|
|
2437
|
-
}
|
|
2438
|
-
return [...ecosystems];
|
|
2439
|
-
}
|
|
2440
|
-
function pickDominantBackend(ecosystems) {
|
|
2441
|
-
for (const eco of ecosystems) {
|
|
2442
|
-
if (ECOSYSTEM_PRIMARY_BACKENDS.has(eco)) return eco;
|
|
2443
|
-
}
|
|
2444
|
-
return void 0;
|
|
2445
|
-
}
|
|
2446
|
-
function isBackendEcosystem(eco) {
|
|
2447
|
-
return ECOSYSTEM_PRIMARY_BACKENDS.has(eco);
|
|
2448
|
-
}
|
|
2449
|
-
function matchRequiresAny(clauses, ctx) {
|
|
2450
|
-
for (const clause of clauses) {
|
|
2451
|
-
if ("stack" in clause) {
|
|
2452
|
-
if (ctx.stackSet.has(clause.stack.toLowerCase())) {
|
|
2453
|
-
return { matched: true, signal: `stack:${clause.stack}` };
|
|
2454
|
-
}
|
|
2455
|
-
} else if ("dependency" in clause) {
|
|
2456
|
-
if (ctx.depSet.has(clause.dependency.toLowerCase())) {
|
|
2457
|
-
return { matched: true, signal: `dependency:${clause.dependency}` };
|
|
2458
|
-
}
|
|
2459
|
-
} else if ("packageNamePattern" in clause) {
|
|
2460
|
-
const pattern = clause.packageNamePattern.toLowerCase();
|
|
2461
|
-
const prefix = pattern.endsWith("*") ? pattern.slice(0, -1) : pattern;
|
|
2462
|
-
for (const dep of ctx.depSet) {
|
|
2463
|
-
if (pattern.endsWith("*") ? dep.startsWith(prefix) : dep === pattern) {
|
|
2464
|
-
return {
|
|
2465
|
-
matched: true,
|
|
2466
|
-
signal: `packageNamePattern:${clause.packageNamePattern}`
|
|
2467
|
-
};
|
|
2468
|
-
}
|
|
2469
|
-
}
|
|
2470
|
-
} else if ("role" in clause) {
|
|
2471
|
-
if (ctx.roleSet.has(clause.role.toLowerCase())) {
|
|
2472
|
-
return { matched: true, signal: `role:${clause.role}` };
|
|
2473
|
-
}
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
|
-
return { matched: false };
|
|
2477
|
-
}
|
|
2478
|
-
function describeRequiresAny(clauses) {
|
|
2479
|
-
return clauses.map((c) => {
|
|
2480
|
-
if ("stack" in c) return `stack=${c.stack}`;
|
|
2481
|
-
if ("dependency" in c) return `dependency=${c.dependency}`;
|
|
2482
|
-
if ("packageNamePattern" in c) return `packageNamePattern=${c.packageNamePattern}`;
|
|
2483
|
-
if ("role" in c) return `role=${c.role}`;
|
|
2484
|
-
return "unknown";
|
|
2485
|
-
}).join(" | ");
|
|
2486
|
-
}
|
|
2487
|
-
function computeConfidenceLevel(args) {
|
|
2488
|
-
const { isDefaultBaseline, reasons, hasEcosystemConflict, score } = args;
|
|
2489
|
-
const positiveCodes = new Set(reasons.map((r) => r.code));
|
|
2490
|
-
positiveCodes.delete("default-baseline");
|
|
2491
|
-
const distinctSignals = positiveCodes.size;
|
|
2492
|
-
const strongCount = (positiveCodes.has("repo-role-match") ? 1 : 0) + (positiveCodes.has("stack-match") ? 1 : 0) + (positiveCodes.has("requires-any-match") ? 1 : 0);
|
|
2493
|
-
if (hasEcosystemConflict) return "low";
|
|
2494
|
-
if (isDefaultBaseline && distinctSignals === 0) return "medium";
|
|
2495
|
-
if (strongCount >= 2 && score >= 70) return "high";
|
|
2496
|
-
if (strongCount >= 1 && distinctSignals >= 2 && score >= 50) return "medium";
|
|
2497
|
-
if (distinctSignals === 1) return "low";
|
|
2498
|
-
return distinctSignals >= 2 ? "medium" : "low";
|
|
2499
|
-
}
|
|
2500
|
-
function confidenceLevelToNumber(level, score) {
|
|
2501
|
-
const base = level === "high" ? 0.85 : level === "medium" ? 0.6 : 0.3;
|
|
2502
|
-
const bonus = Math.min(0.1, Math.max(0, score - 40) / 1e3);
|
|
2503
|
-
return Number(Math.min(0.99, base + bonus).toFixed(2));
|
|
2504
|
-
}
|
|
2505
|
-
function mergeRecommendationWarnings(context) {
|
|
2506
|
-
const riskLines = (context.securityRisks?.length ?? 0) > 0 ? [`Scan reported security signals: ${context.securityRisks.join("; ")}`] : [];
|
|
2507
|
-
return [.../* @__PURE__ */ new Set([...context.warnings, ...riskLines])];
|
|
2508
|
-
}
|
|
2509
|
-
async function readChangedFiles(root) {
|
|
2510
|
-
if (process.env.HAUS_DISABLE_GIT_SIGNALS === "1") return [];
|
|
2511
|
-
try {
|
|
2512
|
-
const result = await runGit(["diff", "--name-only"], { cwd: root });
|
|
2513
|
-
if (result.exitCode !== 0) {
|
|
2514
|
-
return [];
|
|
2515
|
-
}
|
|
2516
|
-
return result.stdout.split("\n").map((x) => x.trim()).filter(Boolean).sort();
|
|
2517
|
-
} catch {
|
|
2518
|
-
return [];
|
|
2519
|
-
}
|
|
2520
|
-
}
|
|
2521
2972
|
|
|
2522
2973
|
// src/utils/prompts.ts
|
|
2523
2974
|
import { stdin as input, stdout as output } from "process";
|
|
@@ -2565,8 +3016,13 @@ async function runSetupProject(options) {
|
|
|
2565
3016
|
merged[question] = existing[question] ?? "pending-user-answer";
|
|
2566
3017
|
continue;
|
|
2567
3018
|
}
|
|
3019
|
+
const prefilled = existing[question];
|
|
3020
|
+
if (prefilled && prefilled !== "pending-user-answer" && prefilled !== "no-answer") {
|
|
3021
|
+
merged[question] = prefilled;
|
|
3022
|
+
continue;
|
|
3023
|
+
}
|
|
2568
3024
|
const answer = await ask(question);
|
|
2569
|
-
merged[question] = answer ||
|
|
3025
|
+
merged[question] = answer || prefilled || "no-answer";
|
|
2570
3026
|
}
|
|
2571
3027
|
await writeJson(hausPath(root, "setup-answers.json"), merged);
|
|
2572
3028
|
}
|
|
@@ -2630,8 +3086,8 @@ async function runSetupProject(options) {
|
|
|
2630
3086
|
// src/commands/init.ts
|
|
2631
3087
|
async function runInit(options) {
|
|
2632
3088
|
const root = process.cwd();
|
|
2633
|
-
const hausDir =
|
|
2634
|
-
const alreadyInit = await
|
|
3089
|
+
const hausDir = path19.join(root, ".haus-workflow");
|
|
3090
|
+
const alreadyInit = await fs13.pathExists(hausDir);
|
|
2635
3091
|
if (alreadyInit) {
|
|
2636
3092
|
log("Haus AI already initialized in this project.");
|
|
2637
3093
|
log("Run `haus setup-project` to reconfigure.");
|
|
@@ -2643,8 +3099,21 @@ async function runInit(options) {
|
|
|
2643
3099
|
|
|
2644
3100
|
// src/install/apply.ts
|
|
2645
3101
|
import crypto2 from "crypto";
|
|
2646
|
-
import
|
|
2647
|
-
import
|
|
3102
|
+
import path22 from "path";
|
|
3103
|
+
import fs15 from "fs-extra";
|
|
3104
|
+
|
|
3105
|
+
// src/install/allow-rules.ts
|
|
3106
|
+
var ALLOWED_SUBCOMMANDS = [
|
|
3107
|
+
"setup-project",
|
|
3108
|
+
"apply",
|
|
3109
|
+
"doctor",
|
|
3110
|
+
"scan",
|
|
3111
|
+
"context",
|
|
3112
|
+
"recommend"
|
|
3113
|
+
];
|
|
3114
|
+
function buildAllowRules() {
|
|
3115
|
+
return [...new Set(ALLOWED_SUBCOMMANDS.map((sub) => `Bash(haus ${sub}:*)`))];
|
|
3116
|
+
}
|
|
2648
3117
|
|
|
2649
3118
|
// src/install/header.ts
|
|
2650
3119
|
var MD_PREFIX = "<!-- HAUS-MANAGED";
|
|
@@ -2656,35 +3125,35 @@ function parseAttrs(raw) {
|
|
|
2656
3125
|
if (!idMatch || !vMatch || !srcMatch) return void 0;
|
|
2657
3126
|
return { stableId: idMatch[1], schemaVersion: vMatch[1], source: srcMatch[1] };
|
|
2658
3127
|
}
|
|
2659
|
-
function parseMarkdownHeader(
|
|
2660
|
-
const firstLine =
|
|
3128
|
+
function parseMarkdownHeader(content2) {
|
|
3129
|
+
const firstLine = content2.split("\n")[0] ?? "";
|
|
2661
3130
|
if (!firstLine.startsWith(MD_PREFIX)) return void 0;
|
|
2662
3131
|
return parseAttrs(firstLine);
|
|
2663
3132
|
}
|
|
2664
3133
|
function buildMarkdownHeader(h) {
|
|
2665
3134
|
return `${MD_PREFIX} id=${h.stableId} v=${h.schemaVersion} source=${h.source}${MD_SUFFIX}`;
|
|
2666
3135
|
}
|
|
2667
|
-
function stampMarkdown(
|
|
3136
|
+
function stampMarkdown(content2, h) {
|
|
2668
3137
|
const header = buildMarkdownHeader(h);
|
|
2669
|
-
const existing = parseMarkdownHeader(
|
|
3138
|
+
const existing = parseMarkdownHeader(content2);
|
|
2670
3139
|
if (existing) {
|
|
2671
|
-
const rest =
|
|
3140
|
+
const rest = content2.slice(content2.indexOf("\n") + 1);
|
|
2672
3141
|
return `${header}
|
|
2673
3142
|
${rest}`;
|
|
2674
3143
|
}
|
|
2675
3144
|
return `${header}
|
|
2676
|
-
${
|
|
3145
|
+
${content2}`;
|
|
2677
3146
|
}
|
|
2678
3147
|
|
|
2679
3148
|
// src/install/manifest.ts
|
|
2680
3149
|
import os4 from "os";
|
|
2681
|
-
import
|
|
3150
|
+
import path20 from "path";
|
|
2682
3151
|
var MANIFEST_SCHEMA = "haus-install-manifest/1";
|
|
2683
3152
|
function globalClaudeDir() {
|
|
2684
|
-
return
|
|
3153
|
+
return path20.join(os4.homedir(), ".claude");
|
|
2685
3154
|
}
|
|
2686
3155
|
function hausManifestPath() {
|
|
2687
|
-
return
|
|
3156
|
+
return path20.join(globalClaudeDir(), "haus", "install-manifest.json");
|
|
2688
3157
|
}
|
|
2689
3158
|
async function readManifest() {
|
|
2690
3159
|
return readJson(hausManifestPath());
|
|
@@ -2703,10 +3172,10 @@ function buildManifest(source, files, hooks) {
|
|
|
2703
3172
|
}
|
|
2704
3173
|
|
|
2705
3174
|
// src/install/settings-merge.ts
|
|
2706
|
-
import
|
|
2707
|
-
import
|
|
3175
|
+
import path21 from "path";
|
|
3176
|
+
import fs14 from "fs-extra";
|
|
2708
3177
|
function settingsJsonPath() {
|
|
2709
|
-
return
|
|
3178
|
+
return path21.join(globalClaudeDir(), "settings.json");
|
|
2710
3179
|
}
|
|
2711
3180
|
async function readSettings() {
|
|
2712
3181
|
const parsed = await readJson(settingsJsonPath());
|
|
@@ -2738,10 +3207,95 @@ function mergeHooks(settings, fragments) {
|
|
|
2738
3207
|
}
|
|
2739
3208
|
updated._haus = {
|
|
2740
3209
|
hooks: [...existing, ...addedIds],
|
|
2741
|
-
hookCommands: [...existingCommands, ...addedCommands]
|
|
3210
|
+
hookCommands: [...existingCommands, ...addedCommands],
|
|
3211
|
+
// Preserve deny/allow tracking so hook, deny, and allow merges are order-independent.
|
|
3212
|
+
...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
|
|
3213
|
+
...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {}
|
|
2742
3214
|
};
|
|
2743
3215
|
return { settings: updated, addedIds };
|
|
2744
3216
|
}
|
|
3217
|
+
function mergeDenyRules(settings, rules) {
|
|
3218
|
+
const existingDeny = settings.permissions?.deny ?? [];
|
|
3219
|
+
const seen = new Set(existingDeny);
|
|
3220
|
+
const trackedDeny = settings._haus?.denyRules ?? [];
|
|
3221
|
+
const addedRules = [];
|
|
3222
|
+
for (const rule of rules) {
|
|
3223
|
+
if (seen.has(rule)) continue;
|
|
3224
|
+
seen.add(rule);
|
|
3225
|
+
addedRules.push(rule);
|
|
3226
|
+
}
|
|
3227
|
+
const updated = { ...settings };
|
|
3228
|
+
updated.permissions = {
|
|
3229
|
+
...settings.permissions ?? {},
|
|
3230
|
+
deny: [...existingDeny, ...addedRules]
|
|
3231
|
+
};
|
|
3232
|
+
updated._haus = {
|
|
3233
|
+
hooks: settings._haus?.hooks ?? [],
|
|
3234
|
+
...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
|
|
3235
|
+
denyRules: [...trackedDeny, ...addedRules],
|
|
3236
|
+
...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {}
|
|
3237
|
+
};
|
|
3238
|
+
return { settings: updated, addedRules };
|
|
3239
|
+
}
|
|
3240
|
+
function mergeAllowRules(settings, rules) {
|
|
3241
|
+
const existingAllow = settings.permissions?.allow ?? [];
|
|
3242
|
+
const seen = new Set(existingAllow);
|
|
3243
|
+
const trackedAllow = settings._haus?.allowRules ?? [];
|
|
3244
|
+
const addedRules = [];
|
|
3245
|
+
for (const rule of rules) {
|
|
3246
|
+
if (seen.has(rule)) continue;
|
|
3247
|
+
seen.add(rule);
|
|
3248
|
+
addedRules.push(rule);
|
|
3249
|
+
}
|
|
3250
|
+
const updated = { ...settings };
|
|
3251
|
+
updated.permissions = {
|
|
3252
|
+
...settings.permissions ?? {},
|
|
3253
|
+
allow: [...existingAllow, ...addedRules]
|
|
3254
|
+
};
|
|
3255
|
+
updated._haus = {
|
|
3256
|
+
hooks: settings._haus?.hooks ?? [],
|
|
3257
|
+
...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
|
|
3258
|
+
...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
|
|
3259
|
+
allowRules: [...trackedAllow, ...addedRules]
|
|
3260
|
+
};
|
|
3261
|
+
return { settings: updated, addedRules };
|
|
3262
|
+
}
|
|
3263
|
+
function stripHausAllow(settings) {
|
|
3264
|
+
const prevHaus = settings._haus;
|
|
3265
|
+
if (!prevHaus?.allowRules || prevHaus.allowRules.length === 0) return settings;
|
|
3266
|
+
const ownedSet = new Set(prevHaus.allowRules);
|
|
3267
|
+
const updated = { ...settings };
|
|
3268
|
+
const remainingAllow = (settings.permissions?.allow ?? []).filter((rule) => !ownedSet.has(rule));
|
|
3269
|
+
const permissions = { ...settings.permissions ?? {} };
|
|
3270
|
+
if (remainingAllow.length > 0) permissions.allow = remainingAllow;
|
|
3271
|
+
else delete permissions.allow;
|
|
3272
|
+
if (Object.keys(permissions).length > 0) updated.permissions = permissions;
|
|
3273
|
+
else delete updated.permissions;
|
|
3274
|
+
const haus = { ...prevHaus };
|
|
3275
|
+
delete haus.allowRules;
|
|
3276
|
+
const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.denyRules?.length ?? 0) > 0;
|
|
3277
|
+
if (stillTracking) updated._haus = haus;
|
|
3278
|
+
else delete updated._haus;
|
|
3279
|
+
return updated;
|
|
3280
|
+
}
|
|
3281
|
+
function stripHausDeny(settings) {
|
|
3282
|
+
const prevHaus = settings._haus;
|
|
3283
|
+
if (!prevHaus?.denyRules || prevHaus.denyRules.length === 0) return settings;
|
|
3284
|
+
const ownedSet = new Set(prevHaus.denyRules);
|
|
3285
|
+
const updated = { ...settings };
|
|
3286
|
+
const remainingDeny = (settings.permissions?.deny ?? []).filter((rule) => !ownedSet.has(rule));
|
|
3287
|
+
const permissions = { ...settings.permissions ?? {} };
|
|
3288
|
+
if (remainingDeny.length > 0) permissions.deny = remainingDeny;
|
|
3289
|
+
else delete permissions.deny;
|
|
3290
|
+
if (Object.keys(permissions).length > 0) updated.permissions = permissions;
|
|
3291
|
+
else delete updated.permissions;
|
|
3292
|
+
const haus = { ...prevHaus };
|
|
3293
|
+
delete haus.denyRules;
|
|
3294
|
+
const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.allowRules?.length ?? 0) > 0;
|
|
3295
|
+
if (stillTracking) updated._haus = haus;
|
|
3296
|
+
else delete updated._haus;
|
|
3297
|
+
return updated;
|
|
3298
|
+
}
|
|
2745
3299
|
function stripHausHooks(settings) {
|
|
2746
3300
|
if (!settings._haus) return settings;
|
|
2747
3301
|
const ownedCommands = new Set(settings._haus.hookCommands ?? []);
|
|
@@ -2762,7 +3316,7 @@ function stripHausHooks(settings) {
|
|
|
2762
3316
|
async function loadHooksFragment(fragmentPath) {
|
|
2763
3317
|
let raw;
|
|
2764
3318
|
try {
|
|
2765
|
-
raw = await
|
|
3319
|
+
raw = await fs14.readJson(fragmentPath);
|
|
2766
3320
|
} catch {
|
|
2767
3321
|
return [];
|
|
2768
3322
|
}
|
|
@@ -2772,45 +3326,45 @@ async function loadHooksFragment(fragmentPath) {
|
|
|
2772
3326
|
|
|
2773
3327
|
// src/install/apply.ts
|
|
2774
3328
|
var SCHEMA_VERSION3 = "1";
|
|
2775
|
-
function hashContent(
|
|
2776
|
-
return `sha256-${crypto2.createHash("sha256").update(
|
|
3329
|
+
function hashContent(content2) {
|
|
3330
|
+
return `sha256-${crypto2.createHash("sha256").update(content2).digest("hex")}`;
|
|
2777
3331
|
}
|
|
2778
3332
|
function sourceVersion() {
|
|
2779
3333
|
try {
|
|
2780
|
-
const pkgPath =
|
|
2781
|
-
const pkg = JSON.parse(
|
|
3334
|
+
const pkgPath = path22.join(packageRoot(), "package.json");
|
|
3335
|
+
const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf8"));
|
|
2782
3336
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
2783
3337
|
} catch {
|
|
2784
3338
|
return "haus@0.0.0";
|
|
2785
3339
|
}
|
|
2786
3340
|
}
|
|
2787
3341
|
function globalSrcDir() {
|
|
2788
|
-
return
|
|
3342
|
+
return path22.join(packageRoot(), "library", "global");
|
|
2789
3343
|
}
|
|
2790
3344
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
2791
3345
|
const entries = [];
|
|
2792
|
-
const skillsDir =
|
|
2793
|
-
if (
|
|
2794
|
-
for (const skillName of
|
|
2795
|
-
const skillFile =
|
|
2796
|
-
if (
|
|
3346
|
+
const skillsDir = path22.join(srcDir, "skills");
|
|
3347
|
+
if (fs15.pathExistsSync(skillsDir)) {
|
|
3348
|
+
for (const skillName of fs15.readdirSync(skillsDir)) {
|
|
3349
|
+
const skillFile = path22.join(skillsDir, skillName, "SKILL.md");
|
|
3350
|
+
if (fs15.pathExistsSync(skillFile)) {
|
|
2797
3351
|
entries.push({
|
|
2798
3352
|
stableId: `skill.${skillName}`,
|
|
2799
|
-
srcRelPath:
|
|
2800
|
-
destPath:
|
|
3353
|
+
srcRelPath: path22.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
3354
|
+
destPath: path22.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
2801
3355
|
});
|
|
2802
3356
|
}
|
|
2803
3357
|
}
|
|
2804
3358
|
}
|
|
2805
|
-
const
|
|
2806
|
-
if (
|
|
2807
|
-
for (const
|
|
2808
|
-
if (!
|
|
2809
|
-
const
|
|
3359
|
+
const commandsDir = path22.join(srcDir, "commands");
|
|
3360
|
+
if (fs15.pathExistsSync(commandsDir)) {
|
|
3361
|
+
for (const fileName of fs15.readdirSync(commandsDir)) {
|
|
3362
|
+
if (!fileName.endsWith(".md")) continue;
|
|
3363
|
+
const commandName = fileName.slice(0, -".md".length);
|
|
2810
3364
|
entries.push({
|
|
2811
|
-
stableId: `
|
|
2812
|
-
srcRelPath:
|
|
2813
|
-
destPath:
|
|
3365
|
+
stableId: `command.${commandName}`,
|
|
3366
|
+
srcRelPath: path22.join("library", "global", "commands", fileName),
|
|
3367
|
+
destPath: path22.join(claudeDir, "commands", fileName)
|
|
2814
3368
|
});
|
|
2815
3369
|
}
|
|
2816
3370
|
}
|
|
@@ -2834,7 +3388,7 @@ async function applyInstall(options = {}) {
|
|
|
2834
3388
|
};
|
|
2835
3389
|
const manifestFiles = [];
|
|
2836
3390
|
for (const entry of sourceFiles) {
|
|
2837
|
-
const srcPath =
|
|
3391
|
+
const srcPath = path22.join(packageRoot(), entry.srcRelPath);
|
|
2838
3392
|
const rawContent = await readText(srcPath);
|
|
2839
3393
|
if (rawContent === void 0) {
|
|
2840
3394
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -2854,7 +3408,7 @@ async function applyInstall(options = {}) {
|
|
|
2854
3408
|
}
|
|
2855
3409
|
continue;
|
|
2856
3410
|
}
|
|
2857
|
-
const destExists =
|
|
3411
|
+
const destExists = fs15.pathExistsSync(entry.destPath);
|
|
2858
3412
|
if (destExists) {
|
|
2859
3413
|
const currentContent = await readText(entry.destPath);
|
|
2860
3414
|
if (currentContent !== void 0) {
|
|
@@ -2890,22 +3444,24 @@ async function applyInstall(options = {}) {
|
|
|
2890
3444
|
schemaVersion: SCHEMA_VERSION3
|
|
2891
3445
|
});
|
|
2892
3446
|
}
|
|
2893
|
-
const fragmentPath =
|
|
3447
|
+
const fragmentPath = path22.join(srcDir, "settings-fragments", "hooks.json");
|
|
2894
3448
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
2895
3449
|
const settings = await readSettings();
|
|
2896
|
-
const { settings:
|
|
3450
|
+
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
3451
|
+
const { settings: deniedSettings } = mergeDenyRules(hookSettings, buildDenyRules());
|
|
3452
|
+
const { settings: mergedSettings } = mergeAllowRules(deniedSettings, buildAllowRules());
|
|
2897
3453
|
result.hookIds = addedIds;
|
|
2898
3454
|
if (!check && existingManifest) {
|
|
2899
3455
|
const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
|
|
2900
3456
|
for (const entry of existingManifest.files) {
|
|
2901
3457
|
if (currentDestPaths.has(entry.destPath)) continue;
|
|
2902
|
-
if (!
|
|
2903
|
-
const
|
|
2904
|
-
if (!
|
|
2905
|
-
const hasHeader = parseMarkdownHeader(
|
|
2906
|
-
const currentHash = hashContent(
|
|
3458
|
+
if (!fs15.pathExistsSync(entry.destPath)) continue;
|
|
3459
|
+
const content2 = await readText(entry.destPath);
|
|
3460
|
+
if (!content2) continue;
|
|
3461
|
+
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
3462
|
+
const currentHash = hashContent(content2);
|
|
2907
3463
|
if (hasHeader && currentHash === entry.hash) {
|
|
2908
|
-
if (!dryRun) await
|
|
3464
|
+
if (!dryRun) await fs15.remove(entry.destPath);
|
|
2909
3465
|
result.deleted.push(entry.destPath);
|
|
2910
3466
|
} else {
|
|
2911
3467
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -2957,14 +3513,27 @@ async function runInstall(options) {
|
|
|
2957
3513
|
force: options.force,
|
|
2958
3514
|
check: options.check
|
|
2959
3515
|
});
|
|
2960
|
-
printApplyResult(result, options.dryRun ?? false);
|
|
3516
|
+
if (!options.postinstall) printApplyResult(result, options.dryRun ?? false);
|
|
2961
3517
|
if (options.check && result.drift) {
|
|
2962
3518
|
process.exitCode = 1;
|
|
2963
3519
|
} else if (!options.check && !options.dryRun) {
|
|
2964
3520
|
const total = result.created.length + result.updated.length;
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
3521
|
+
if (options.postinstall) {
|
|
3522
|
+
log("haus configured Claude Code for you:");
|
|
3523
|
+
const parts = [];
|
|
3524
|
+
if (result.created.length) parts.push(`${result.created.length} file(s) added`);
|
|
3525
|
+
if (result.updated.length) parts.push(`${result.updated.length} file(s) updated`);
|
|
3526
|
+
log(
|
|
3527
|
+
parts.length ? ` \u2022 ${parts.join(", ")} in ~/.claude (skills, slash commands)` : " \u2022 already up to date \u2014 no files changed"
|
|
3528
|
+
);
|
|
3529
|
+
log(` \u2022 ensured hooks + security rules are present in ~/.claude/settings.json`);
|
|
3530
|
+
log("Undo any time with: haus uninstall");
|
|
3531
|
+
log("Disable this on install: HAUS_NO_POSTINSTALL=1");
|
|
3532
|
+
} else {
|
|
3533
|
+
log(
|
|
3534
|
+
`haus install complete (${total} file(s) written, ${result.hookIds.length} hook(s) added)`
|
|
3535
|
+
);
|
|
3536
|
+
}
|
|
2968
3537
|
}
|
|
2969
3538
|
} catch (err) {
|
|
2970
3539
|
error(`haus install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -2972,79 +3541,6 @@ async function runInstall(options) {
|
|
|
2972
3541
|
}
|
|
2973
3542
|
}
|
|
2974
3543
|
|
|
2975
|
-
// src/memory/memory-store.ts
|
|
2976
|
-
var FILES = [
|
|
2977
|
-
"project-learnings.md",
|
|
2978
|
-
"decisions.md",
|
|
2979
|
-
"recurring-issues.md",
|
|
2980
|
-
"client-context.md"
|
|
2981
|
-
];
|
|
2982
|
-
async function ensureMemory(root) {
|
|
2983
|
-
await Promise.all(
|
|
2984
|
-
FILES.map(async (name) => {
|
|
2985
|
-
const file = hausPath(root, "memory", name);
|
|
2986
|
-
const current = await readText(file);
|
|
2987
|
-
if (!current) await writeText(file, `# ${name}
|
|
2988
|
-
`);
|
|
2989
|
-
})
|
|
2990
|
-
);
|
|
2991
|
-
const indexFile = hausPath(root, "memory", "index.json");
|
|
2992
|
-
const index = await readJson(indexFile);
|
|
2993
|
-
if (!index) await writeJson(indexFile, { files: [...FILES] });
|
|
2994
|
-
}
|
|
2995
|
-
async function readMemory(root) {
|
|
2996
|
-
await ensureMemory(root);
|
|
2997
|
-
const blocks = await Promise.all(FILES.map((name) => readText(hausPath(root, "memory", name))));
|
|
2998
|
-
return blocks.filter(Boolean).join("\n");
|
|
2999
|
-
}
|
|
3000
|
-
async function appendLearning(root, line) {
|
|
3001
|
-
await ensureMemory(root);
|
|
3002
|
-
const file = hausPath(root, "memory", "project-learnings.md");
|
|
3003
|
-
const current = await readText(file) ?? "# project-learnings.md\n";
|
|
3004
|
-
await writeText(file, `${current}
|
|
3005
|
-
- ${line}
|
|
3006
|
-
`);
|
|
3007
|
-
}
|
|
3008
|
-
|
|
3009
|
-
// src/memory/redact-memory.ts
|
|
3010
|
-
function redactMemory(text) {
|
|
3011
|
-
return text.replace(/(api[_-]?key|token|secret|password)\s*[:=]\s*\S+/gi, "$1=[REDACTED]").replace(/-----BEGIN [A-Z ]+-----[\s\S]*?-----END [A-Z ]+-----/g, "[REDACTED-KEY]");
|
|
3012
|
-
}
|
|
3013
|
-
|
|
3014
|
-
// src/commands/memory.ts
|
|
3015
|
-
async function runMemory(subcommand, options) {
|
|
3016
|
-
const root = process.cwd();
|
|
3017
|
-
if (subcommand === "inject" && options.fromHook && !await isHookEnabled(root, "memoryInject")) {
|
|
3018
|
-
return;
|
|
3019
|
-
}
|
|
3020
|
-
await ensureMemory(root);
|
|
3021
|
-
if (subcommand === "status") {
|
|
3022
|
-
log("Memory ready at .haus-workflow/memory");
|
|
3023
|
-
return;
|
|
3024
|
-
}
|
|
3025
|
-
if (subcommand === "add") {
|
|
3026
|
-
if (!options.text) throw new Error("memory add requires text");
|
|
3027
|
-
await appendLearning(root, redactMemory(options.text));
|
|
3028
|
-
log("Memory added");
|
|
3029
|
-
return;
|
|
3030
|
-
}
|
|
3031
|
-
if (subcommand === "inject") {
|
|
3032
|
-
const text = redactMemory(await readMemory(root));
|
|
3033
|
-
if (!text.trim()) {
|
|
3034
|
-
log("No relevant Haus memory found.");
|
|
3035
|
-
return;
|
|
3036
|
-
}
|
|
3037
|
-
const compact = `Task: ${options.task ?? "n/a"}
|
|
3038
|
-
${text}`.slice(
|
|
3039
|
-
0,
|
|
3040
|
-
options.fromHook ? 1200 : 4e3
|
|
3041
|
-
);
|
|
3042
|
-
log(compact);
|
|
3043
|
-
return;
|
|
3044
|
-
}
|
|
3045
|
-
log("Promotion proposal: review memory and move stable rules into .claude/rules manually.");
|
|
3046
|
-
}
|
|
3047
|
-
|
|
3048
3544
|
// src/commands/recommend.ts
|
|
3049
3545
|
async function runRecommend(options) {
|
|
3050
3546
|
const root = process.cwd();
|
|
@@ -3088,20 +3584,20 @@ async function runScan(options) {
|
|
|
3088
3584
|
}
|
|
3089
3585
|
|
|
3090
3586
|
// src/commands/undo.ts
|
|
3091
|
-
import
|
|
3092
|
-
import
|
|
3587
|
+
import path23 from "path";
|
|
3588
|
+
import fs16 from "fs-extra";
|
|
3093
3589
|
var CLAUDE_DIR = ".claude";
|
|
3094
3590
|
async function runUndo(options) {
|
|
3095
3591
|
const root = process.cwd();
|
|
3096
|
-
const targets = [
|
|
3097
|
-
const existing = targets.filter((p) =>
|
|
3592
|
+
const targets = [path23.join(root, CLAUDE_DIR), path23.join(root, HAUS_DIR)];
|
|
3593
|
+
const existing = targets.filter((p) => fs16.existsSync(p));
|
|
3098
3594
|
if (existing.length === 0) {
|
|
3099
3595
|
log("Nothing to remove: no .claude/ or .haus-workflow/ in this directory.");
|
|
3100
3596
|
return;
|
|
3101
3597
|
}
|
|
3102
3598
|
if (!options.yes) {
|
|
3103
3599
|
const ok = await confirm(
|
|
3104
|
-
`Remove ${existing.map((p) =>
|
|
3600
|
+
`Remove ${existing.map((p) => path23.relative(root, p)).join(" and ")}? This cannot be undone.`
|
|
3105
3601
|
);
|
|
3106
3602
|
if (!ok) {
|
|
3107
3603
|
log("Cancelled.");
|
|
@@ -3109,15 +3605,15 @@ async function runUndo(options) {
|
|
|
3109
3605
|
}
|
|
3110
3606
|
}
|
|
3111
3607
|
for (const p of existing) {
|
|
3112
|
-
await
|
|
3113
|
-
log(`Removed ${
|
|
3608
|
+
await fs16.remove(p);
|
|
3609
|
+
log(`Removed ${path23.relative(root, p)}`);
|
|
3114
3610
|
}
|
|
3115
3611
|
}
|
|
3116
3612
|
|
|
3117
3613
|
// src/install/uninstall.ts
|
|
3118
3614
|
import crypto3 from "crypto";
|
|
3119
|
-
import
|
|
3120
|
-
import
|
|
3615
|
+
import path24 from "path";
|
|
3616
|
+
import fs17 from "fs-extra";
|
|
3121
3617
|
async function runUninstall(options = {}) {
|
|
3122
3618
|
const { force = false } = options;
|
|
3123
3619
|
const manifest = await readManifest();
|
|
@@ -3127,17 +3623,17 @@ async function runUninstall(options = {}) {
|
|
|
3127
3623
|
return result;
|
|
3128
3624
|
}
|
|
3129
3625
|
for (const entry of manifest.files) {
|
|
3130
|
-
const exists =
|
|
3626
|
+
const exists = fs17.pathExistsSync(entry.destPath);
|
|
3131
3627
|
if (!exists) continue;
|
|
3132
|
-
const
|
|
3133
|
-
if (
|
|
3134
|
-
const header = parseMarkdownHeader(
|
|
3628
|
+
const content2 = await readText(entry.destPath);
|
|
3629
|
+
if (content2 === void 0) continue;
|
|
3630
|
+
const header = parseMarkdownHeader(content2);
|
|
3135
3631
|
if (!header) {
|
|
3136
3632
|
warn(`Skipping user-owned file (no HAUS-MANAGED header): ${entry.destPath}`);
|
|
3137
3633
|
result.skipped.push(entry.destPath);
|
|
3138
3634
|
continue;
|
|
3139
3635
|
}
|
|
3140
|
-
const currentHash = `sha256-${crypto3.createHash("sha256").update(
|
|
3636
|
+
const currentHash = `sha256-${crypto3.createHash("sha256").update(content2).digest("hex")}`;
|
|
3141
3637
|
if (currentHash !== entry.hash && !force) {
|
|
3142
3638
|
warn(
|
|
3143
3639
|
`Skipping user-edited haus file (hash mismatch): ${entry.destPath} \u2014 use --force to delete`
|
|
@@ -3145,22 +3641,22 @@ async function runUninstall(options = {}) {
|
|
|
3145
3641
|
result.skipped.push(entry.destPath);
|
|
3146
3642
|
continue;
|
|
3147
3643
|
}
|
|
3148
|
-
await
|
|
3149
|
-
await pruneEmptyDir(
|
|
3644
|
+
await fs17.remove(entry.destPath);
|
|
3645
|
+
await pruneEmptyDir(path24.dirname(entry.destPath));
|
|
3150
3646
|
result.deleted.push(entry.destPath);
|
|
3151
3647
|
}
|
|
3152
3648
|
const settings = await readSettings();
|
|
3153
|
-
const stripped = stripHausHooks(settings);
|
|
3649
|
+
const stripped = stripHausHooks(stripHausAllow(stripHausDeny(settings)));
|
|
3154
3650
|
await writeSettings(stripped);
|
|
3155
3651
|
result.hooksStripped = true;
|
|
3156
|
-
const hausDir =
|
|
3652
|
+
const hausDir = path24.join(globalClaudeDir(), "haus");
|
|
3157
3653
|
const manifestPath = hausManifestPath();
|
|
3158
|
-
if (
|
|
3159
|
-
await
|
|
3654
|
+
if (fs17.pathExistsSync(manifestPath)) {
|
|
3655
|
+
await fs17.remove(manifestPath);
|
|
3160
3656
|
}
|
|
3161
|
-
if (
|
|
3162
|
-
const remaining = await
|
|
3163
|
-
if (remaining.length === 0) await
|
|
3657
|
+
if (fs17.pathExistsSync(hausDir)) {
|
|
3658
|
+
const remaining = await fs17.readdir(hausDir);
|
|
3659
|
+
if (remaining.length === 0) await fs17.remove(hausDir);
|
|
3164
3660
|
}
|
|
3165
3661
|
return result;
|
|
3166
3662
|
}
|
|
@@ -3179,8 +3675,8 @@ function printUninstallResult(result) {
|
|
|
3179
3675
|
}
|
|
3180
3676
|
async function pruneEmptyDir(dir) {
|
|
3181
3677
|
try {
|
|
3182
|
-
const entries = await
|
|
3183
|
-
if (entries.length === 0) await
|
|
3678
|
+
const entries = await fs17.readdir(dir);
|
|
3679
|
+
if (entries.length === 0) await fs17.remove(dir);
|
|
3184
3680
|
} catch {
|
|
3185
3681
|
}
|
|
3186
3682
|
}
|
|
@@ -3198,7 +3694,7 @@ async function runUninstallCommand(options) {
|
|
|
3198
3694
|
}
|
|
3199
3695
|
|
|
3200
3696
|
// src/commands/update.ts
|
|
3201
|
-
import
|
|
3697
|
+
import path26 from "path";
|
|
3202
3698
|
|
|
3203
3699
|
// src/update/diff-generated-files.ts
|
|
3204
3700
|
function diffGeneratedFiles() {
|
|
@@ -3224,8 +3720,8 @@ function summarizeLockDiff(before, after) {
|
|
|
3224
3720
|
}
|
|
3225
3721
|
|
|
3226
3722
|
// src/update/lockfile.ts
|
|
3227
|
-
import { mkdir, readFile as
|
|
3228
|
-
import
|
|
3723
|
+
import { mkdir, readFile as readFile3, copyFile } from "fs/promises";
|
|
3724
|
+
import path25 from "path";
|
|
3229
3725
|
async function checkLock(root) {
|
|
3230
3726
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
3231
3727
|
const hasValidVersions = lock.every(
|
|
@@ -3238,7 +3734,7 @@ async function applyLock(root) {
|
|
|
3238
3734
|
const lockPath = hausPath(root, "haus.lock.json");
|
|
3239
3735
|
let before = "[]";
|
|
3240
3736
|
try {
|
|
3241
|
-
before = await
|
|
3737
|
+
before = await readFile3(lockPath, "utf8");
|
|
3242
3738
|
} catch {
|
|
3243
3739
|
before = "[]";
|
|
3244
3740
|
}
|
|
@@ -3246,7 +3742,7 @@ async function applyLock(root) {
|
|
|
3246
3742
|
try {
|
|
3247
3743
|
const backupDir = hausPath(root, "backups");
|
|
3248
3744
|
await mkdir(backupDir, { recursive: true });
|
|
3249
|
-
await copyFile(lockPath,
|
|
3745
|
+
await copyFile(lockPath, path25.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
3250
3746
|
} catch {
|
|
3251
3747
|
}
|
|
3252
3748
|
const enriched = await Promise.all(
|
|
@@ -3268,7 +3764,7 @@ function diffLock(before, after) {
|
|
|
3268
3764
|
}
|
|
3269
3765
|
async function hasLocalOverrides(root) {
|
|
3270
3766
|
try {
|
|
3271
|
-
await
|
|
3767
|
+
await readFile3(path25.join(root, ".claude", "settings.json"), "utf8");
|
|
3272
3768
|
return true;
|
|
3273
3769
|
} catch {
|
|
3274
3770
|
return false;
|
|
@@ -3280,7 +3776,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
|
3280
3776
|
async function runUpdate(options) {
|
|
3281
3777
|
const root = process.cwd();
|
|
3282
3778
|
if (options.check) {
|
|
3283
|
-
const pkgJson2 = await readJson(
|
|
3779
|
+
const pkgJson2 = await readJson(path26.join(packageRoot(), "package.json"));
|
|
3284
3780
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
3285
3781
|
const [status, npmVersion, latestCatalogTag] = await Promise.all([
|
|
3286
3782
|
checkLock(root),
|
|
@@ -3307,7 +3803,7 @@ async function runUpdate(options) {
|
|
|
3307
3803
|
if (!status.ok) process.exitCode = 1;
|
|
3308
3804
|
return;
|
|
3309
3805
|
}
|
|
3310
|
-
const pkgJson = await readJson(
|
|
3806
|
+
const pkgJson = await readJson(path26.join(packageRoot(), "package.json"));
|
|
3311
3807
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
3312
3808
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
3313
3809
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -3337,49 +3833,187 @@ async function runUpdate(options) {
|
|
|
3337
3833
|
}
|
|
3338
3834
|
|
|
3339
3835
|
// src/commands/validate-catalog.ts
|
|
3340
|
-
import
|
|
3341
|
-
import
|
|
3836
|
+
import fs18 from "fs";
|
|
3837
|
+
import path27 from "path";
|
|
3342
3838
|
|
|
3343
|
-
//
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3839
|
+
// library/catalog/validation-rules.json
|
|
3840
|
+
var validation_rules_default = {
|
|
3841
|
+
forbiddenTags: [
|
|
3842
|
+
"python",
|
|
3843
|
+
"django",
|
|
3844
|
+
"go",
|
|
3845
|
+
"rust",
|
|
3846
|
+
"java",
|
|
3847
|
+
"spring",
|
|
3848
|
+
"kotlin",
|
|
3849
|
+
"swift",
|
|
3850
|
+
"android",
|
|
3851
|
+
"flutter",
|
|
3852
|
+
"dart",
|
|
3853
|
+
"c++",
|
|
3854
|
+
"perl",
|
|
3855
|
+
"defi",
|
|
3856
|
+
"trading"
|
|
3857
|
+
],
|
|
3858
|
+
bannedAgentPhrases: ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"],
|
|
3859
|
+
requiredSkillSections: ["## Use when", "## Do not use when"],
|
|
3860
|
+
requiredAgentSections: ["## Use when", "## Do not use when", "## Verification"],
|
|
3861
|
+
riskyInstallPatterns: [
|
|
3862
|
+
{ source: "\\bnpx\\s+-y\\b", flags: "i" },
|
|
3863
|
+
{ source: "\\bnpx\\s+--yes\\b", flags: "i" },
|
|
3864
|
+
{ source: "\\byarn\\s+dlx\\b", flags: "i" },
|
|
3865
|
+
{ source: "\\bpnpm\\s+dlx\\b", flags: "i" }
|
|
3866
|
+
],
|
|
3867
|
+
allowedNpxPattern: { source: "\\bnpx\\s+tsx\\b", flags: "i" },
|
|
3868
|
+
anyNpxPattern: { source: "\\bnpx\\s+\\S+", flags: "i" },
|
|
3869
|
+
httpUrlPattern: { source: "^http:\\/\\/", flags: "i" },
|
|
3870
|
+
placeholderPattern: { source: "\\bTODO\\b|\\bPLACEHOLDER\\b", flags: "i" },
|
|
3871
|
+
allowedStacks: [
|
|
3872
|
+
"haus",
|
|
3873
|
+
"security",
|
|
3874
|
+
"quality",
|
|
3875
|
+
"frontend",
|
|
3876
|
+
"backend",
|
|
3877
|
+
"testing",
|
|
3878
|
+
"review",
|
|
3879
|
+
"workflow",
|
|
3880
|
+
"reference-pack",
|
|
3881
|
+
"core-skill",
|
|
3882
|
+
"workflow-skill",
|
|
3883
|
+
"stack-skill",
|
|
3884
|
+
"review-skill",
|
|
3885
|
+
"agent",
|
|
3886
|
+
"hook",
|
|
3887
|
+
"rule",
|
|
3888
|
+
"react",
|
|
3889
|
+
"typescript",
|
|
3890
|
+
"php",
|
|
3891
|
+
"csharp",
|
|
3892
|
+
"vendure",
|
|
3893
|
+
"vendure3",
|
|
3894
|
+
"nestjs",
|
|
3895
|
+
"graphql",
|
|
3896
|
+
"nx21",
|
|
3897
|
+
"turbo",
|
|
3898
|
+
"nextjs",
|
|
3899
|
+
"react19",
|
|
3900
|
+
"typescript5",
|
|
3901
|
+
"vite8",
|
|
3902
|
+
"tanstack-query",
|
|
3903
|
+
"tanstack-router",
|
|
3904
|
+
"radix",
|
|
3905
|
+
"radix-ui",
|
|
3906
|
+
"shadcn",
|
|
3907
|
+
"shadcn-ui",
|
|
3908
|
+
"tailwind",
|
|
3909
|
+
"tailwindcss",
|
|
3910
|
+
"scss",
|
|
3911
|
+
"scss-modules",
|
|
3912
|
+
"vue",
|
|
3913
|
+
"expressjs",
|
|
3914
|
+
"soup-base",
|
|
3915
|
+
"laravel",
|
|
3916
|
+
"laravel-nova",
|
|
3917
|
+
"wordpress",
|
|
3918
|
+
"bedrock",
|
|
3919
|
+
"elementor-pro",
|
|
3920
|
+
"acf-pro",
|
|
3921
|
+
"jetengine",
|
|
3922
|
+
"dotnet",
|
|
3923
|
+
"oidc",
|
|
3924
|
+
"azure-ad",
|
|
3925
|
+
"bankid",
|
|
3926
|
+
"myid",
|
|
3927
|
+
"cgi",
|
|
3928
|
+
"crypto",
|
|
3929
|
+
"collection2",
|
|
3930
|
+
"postgresql",
|
|
3931
|
+
"mariadb",
|
|
3932
|
+
"mssql",
|
|
3933
|
+
"elasticsearch",
|
|
3934
|
+
"yarn4",
|
|
3935
|
+
"pnpm89",
|
|
3936
|
+
"playwright",
|
|
3937
|
+
"testing-library",
|
|
3938
|
+
"phpunit",
|
|
3939
|
+
"storybook",
|
|
3940
|
+
"wisest",
|
|
3941
|
+
"vitest",
|
|
3942
|
+
"jest",
|
|
3943
|
+
"redis",
|
|
3944
|
+
"sanity",
|
|
3945
|
+
"strapi",
|
|
3946
|
+
"prisma",
|
|
3947
|
+
"cms",
|
|
3948
|
+
"database",
|
|
3949
|
+
"mysql",
|
|
3950
|
+
"saml2",
|
|
3951
|
+
"next-auth",
|
|
3952
|
+
"auth",
|
|
3953
|
+
"expo",
|
|
3954
|
+
"react-native",
|
|
3955
|
+
"mobile",
|
|
3956
|
+
"i18next",
|
|
3957
|
+
"i18n",
|
|
3958
|
+
"bullmq",
|
|
3959
|
+
"queue",
|
|
3960
|
+
"sentry",
|
|
3961
|
+
"observability",
|
|
3962
|
+
"tooling",
|
|
3963
|
+
"prettier",
|
|
3964
|
+
"eslint",
|
|
3965
|
+
"missing-prettier",
|
|
3966
|
+
"missing-eslint",
|
|
3967
|
+
"docker",
|
|
3968
|
+
"pm2",
|
|
3969
|
+
"deployer-php",
|
|
3970
|
+
"stripe",
|
|
3971
|
+
"qliro",
|
|
3972
|
+
"supabase",
|
|
3973
|
+
"payments"
|
|
3974
|
+
],
|
|
3975
|
+
alwaysAllowedTags: [
|
|
3976
|
+
"haus",
|
|
3977
|
+
"security",
|
|
3978
|
+
"quality",
|
|
3979
|
+
"review",
|
|
3980
|
+
"workflow",
|
|
3981
|
+
"baseline",
|
|
3982
|
+
"project-instructions"
|
|
3983
|
+
],
|
|
3984
|
+
patternTagSuffixes: ["-patterns"]
|
|
3985
|
+
};
|
|
3351
3986
|
|
|
3352
3987
|
// src/catalog/validation-rules.ts
|
|
3353
|
-
var
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
var PLACEHOLDER_PATTERN = /\bTODO\b|\bPLACEHOLDER\b/i;
|
|
3988
|
+
var toRegExp = (r) => new RegExp(r.source, r.flags);
|
|
3989
|
+
var FORBIDDEN_TAGS = validation_rules_default.forbiddenTags;
|
|
3990
|
+
var BANNED_AGENT_PHRASES = validation_rules_default.bannedAgentPhrases;
|
|
3991
|
+
var REQUIRED_SKILL_SECTIONS = validation_rules_default.requiredSkillSections;
|
|
3992
|
+
var REQUIRED_AGENT_SECTIONS = validation_rules_default.requiredAgentSections;
|
|
3993
|
+
var RISKY_INSTALL_PATTERNS = validation_rules_default.riskyInstallPatterns.map(toRegExp);
|
|
3994
|
+
var ALLOWED_NPX_PATTERN = toRegExp(validation_rules_default.allowedNpxPattern);
|
|
3995
|
+
var ANY_NPX_PATTERN = toRegExp(validation_rules_default.anyNpxPattern);
|
|
3996
|
+
var HTTP_URL_PATTERN = toRegExp(validation_rules_default.httpUrlPattern);
|
|
3997
|
+
var PLACEHOLDER_PATTERN = toRegExp(validation_rules_default.placeholderPattern);
|
|
3998
|
+
var ALLOWED_STACKS = validation_rules_default.allowedStacks;
|
|
3999
|
+
var ALWAYS_ALLOWED_TAGS = validation_rules_default.alwaysAllowedTags;
|
|
4000
|
+
var PATTERN_TAG_SUFFIXES = validation_rules_default.patternTagSuffixes;
|
|
4001
|
+
var ALLOWED_SET = new Set([...ALLOWED_STACKS, ...ALWAYS_ALLOWED_TAGS].map((t) => t.toLowerCase()));
|
|
4002
|
+
function isTagAllowed(tag) {
|
|
4003
|
+
const lower = tag.toLowerCase();
|
|
4004
|
+
if (ALLOWED_SET.has(lower)) return true;
|
|
4005
|
+
return PATTERN_TAG_SUFFIXES.some((suffix) => lower.endsWith(suffix));
|
|
4006
|
+
}
|
|
4007
|
+
function auditDisallowedTags(items) {
|
|
4008
|
+
const failures = [];
|
|
4009
|
+
for (const item of items) {
|
|
4010
|
+
if (!item.id) continue;
|
|
4011
|
+
for (const tag of Array.isArray(item.tags) ? item.tags : []) {
|
|
4012
|
+
if (!isTagAllowed(tag)) failures.push(`${item.id}: tag not in allowlist: "${tag}"`);
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
return failures;
|
|
4016
|
+
}
|
|
3383
4017
|
|
|
3384
4018
|
// src/commands/validate-catalog.ts
|
|
3385
4019
|
function auditForbiddenStacks(items) {
|
|
@@ -3449,23 +4083,23 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
3449
4083
|
const failures = [];
|
|
3450
4084
|
for (const item of items) {
|
|
3451
4085
|
if (!item.path) continue;
|
|
3452
|
-
const absPath =
|
|
4086
|
+
const absPath = path27.join(manifestDir, item.path);
|
|
3453
4087
|
if (item.type === "skill") {
|
|
3454
|
-
const skillMd =
|
|
3455
|
-
if (!
|
|
3456
|
-
failures.push(`${item.id}: missing ${
|
|
4088
|
+
const skillMd = path27.join(absPath, "SKILL.md");
|
|
4089
|
+
if (!fs18.existsSync(skillMd)) {
|
|
4090
|
+
failures.push(`${item.id}: missing ${path27.relative(manifestDir, skillMd)}`);
|
|
3457
4091
|
continue;
|
|
3458
4092
|
}
|
|
3459
|
-
const text =
|
|
4093
|
+
const text = fs18.readFileSync(skillMd, "utf8");
|
|
3460
4094
|
for (const section of REQUIRED_SKILL_SECTIONS) {
|
|
3461
4095
|
if (!text.includes(section)) failures.push(`${item.id}: SKILL.md missing ${section}`);
|
|
3462
4096
|
}
|
|
3463
4097
|
} else if (item.type === "agent") {
|
|
3464
|
-
if (!
|
|
4098
|
+
if (!fs18.existsSync(absPath)) {
|
|
3465
4099
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
3466
4100
|
continue;
|
|
3467
4101
|
}
|
|
3468
|
-
const text =
|
|
4102
|
+
const text = fs18.readFileSync(absPath, "utf8");
|
|
3469
4103
|
if (!text.startsWith("---")) failures.push(`${item.id}: agent file missing YAML frontmatter`);
|
|
3470
4104
|
for (const section of REQUIRED_AGENT_SECTIONS) {
|
|
3471
4105
|
if (!text.includes(section)) failures.push(`${item.id}: agent file missing ${section}`);
|
|
@@ -3476,7 +4110,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
3476
4110
|
failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
|
|
3477
4111
|
}
|
|
3478
4112
|
} else if (item.type === "template") {
|
|
3479
|
-
if (!
|
|
4113
|
+
if (!fs18.existsSync(absPath)) {
|
|
3480
4114
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
3481
4115
|
}
|
|
3482
4116
|
}
|
|
@@ -3487,21 +4121,21 @@ function auditMarkdownContent(manifestDir) {
|
|
|
3487
4121
|
const failures = [];
|
|
3488
4122
|
const dirs = ["skills", "agents"];
|
|
3489
4123
|
for (const dir of dirs) {
|
|
3490
|
-
const abs =
|
|
3491
|
-
if (!
|
|
4124
|
+
const abs = path27.join(manifestDir, dir);
|
|
4125
|
+
if (!fs18.existsSync(abs)) continue;
|
|
3492
4126
|
walkMd(abs, (file) => {
|
|
3493
|
-
const text =
|
|
3494
|
-
const rel =
|
|
4127
|
+
const text = fs18.readFileSync(file, "utf8");
|
|
4128
|
+
const rel = path27.relative(manifestDir, file);
|
|
3495
4129
|
const lines = text.split(/\r?\n/);
|
|
3496
4130
|
for (let i = 0; i < lines.length; i++) {
|
|
3497
|
-
const
|
|
3498
|
-
if (PLACEHOLDER_PATTERN.test(
|
|
4131
|
+
const line2 = lines[i] ?? "";
|
|
4132
|
+
if (PLACEHOLDER_PATTERN.test(line2)) {
|
|
3499
4133
|
failures.push(`${rel}:${i + 1}: TODO or placeholder in shipped content`);
|
|
3500
4134
|
}
|
|
3501
|
-
if (RISKY_INSTALL_PATTERNS.some((re) => re.test(
|
|
4135
|
+
if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2))) {
|
|
3502
4136
|
failures.push(`${rel}:${i + 1}: risky install pattern`);
|
|
3503
4137
|
}
|
|
3504
|
-
if (ANY_NPX_PATTERN.test(
|
|
4138
|
+
if (ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
|
|
3505
4139
|
failures.push(`${rel}:${i + 1}: disallowed npx (only npx tsx allowed)`);
|
|
3506
4140
|
}
|
|
3507
4141
|
}
|
|
@@ -3510,8 +4144,8 @@ function auditMarkdownContent(manifestDir) {
|
|
|
3510
4144
|
return failures;
|
|
3511
4145
|
}
|
|
3512
4146
|
function walkMd(dir, fn) {
|
|
3513
|
-
for (const entry of
|
|
3514
|
-
const full =
|
|
4147
|
+
for (const entry of fs18.readdirSync(dir, { withFileTypes: true })) {
|
|
4148
|
+
const full = path27.join(dir, entry.name);
|
|
3515
4149
|
if (entry.isDirectory()) walkMd(full, fn);
|
|
3516
4150
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
3517
4151
|
}
|
|
@@ -3522,8 +4156,8 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3522
4156
|
process.exitCode = 1;
|
|
3523
4157
|
return;
|
|
3524
4158
|
}
|
|
3525
|
-
const abs =
|
|
3526
|
-
const manifestDir =
|
|
4159
|
+
const abs = path27.resolve(process.cwd(), manifestPath);
|
|
4160
|
+
const manifestDir = path27.dirname(abs);
|
|
3527
4161
|
const data = await readJson(abs);
|
|
3528
4162
|
if (!data?.items) {
|
|
3529
4163
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -3535,17 +4169,7 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3535
4169
|
const stackFailures = auditForbiddenStacks(items);
|
|
3536
4170
|
const fileFailures = auditShippedFiles(manifestDir, items);
|
|
3537
4171
|
const contentFailures = auditMarkdownContent(manifestDir);
|
|
3538
|
-
const
|
|
3539
|
-
const tagFailures = [];
|
|
3540
|
-
if (allowed.size > 0) {
|
|
3541
|
-
for (const item of items) {
|
|
3542
|
-
for (const tag of Array.isArray(item.tags) ? item.tags : []) {
|
|
3543
|
-
if (!allowed.has(tag.toLowerCase()) && !tag.includes("-patterns") && tag !== "haus" && tag !== "security" && tag !== "quality" && tag !== "review" && tag !== "workflow" && tag !== "baseline" && tag !== "project-instructions") {
|
|
3544
|
-
tagFailures.push(`${item.id}: tag not in allowlist: "${tag}"`);
|
|
3545
|
-
}
|
|
3546
|
-
}
|
|
3547
|
-
}
|
|
3548
|
-
}
|
|
4172
|
+
const tagFailures = auditDisallowedTags(items);
|
|
3549
4173
|
const allFailures = [
|
|
3550
4174
|
...structureFailures,
|
|
3551
4175
|
...stackFailures,
|
|
@@ -3562,7 +4186,7 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3562
4186
|
}
|
|
3563
4187
|
|
|
3564
4188
|
// src/commands/workspace.ts
|
|
3565
|
-
import
|
|
4189
|
+
import path28 from "path";
|
|
3566
4190
|
import YAML from "yaml";
|
|
3567
4191
|
async function runWorkspace(action) {
|
|
3568
4192
|
if (action === "init") {
|
|
@@ -3595,7 +4219,7 @@ relationships: []
|
|
|
3595
4219
|
const summaries = [];
|
|
3596
4220
|
const ownership = {};
|
|
3597
4221
|
for (const repo of repos) {
|
|
3598
|
-
const repoRoot =
|
|
4222
|
+
const repoRoot = path28.resolve(process.cwd(), repo.path);
|
|
3599
4223
|
const result = await scanProject(repoRoot, "fast");
|
|
3600
4224
|
summaries.push({
|
|
3601
4225
|
name: repo.name,
|
|
@@ -3604,9 +4228,9 @@ relationships: []
|
|
|
3604
4228
|
packageManager: result.packageManager,
|
|
3605
4229
|
deps: result.dependencies
|
|
3606
4230
|
});
|
|
3607
|
-
for (const
|
|
3608
|
-
ownership[
|
|
3609
|
-
ownership[
|
|
4231
|
+
for (const dep2 of result.dependencies) {
|
|
4232
|
+
ownership[dep2] ??= [];
|
|
4233
|
+
ownership[dep2].push(repo.name);
|
|
3610
4234
|
}
|
|
3611
4235
|
}
|
|
3612
4236
|
await writeJson(".haus-workflow/workspace-summary.json", {
|
|
@@ -3631,7 +4255,7 @@ ${summaries.map(
|
|
|
3631
4255
|
// src/cli.ts
|
|
3632
4256
|
function cliVersion() {
|
|
3633
4257
|
try {
|
|
3634
|
-
const pkgPath =
|
|
4258
|
+
const pkgPath = path29.join(packageRoot(), "package.json");
|
|
3635
4259
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
3636
4260
|
return pkg.version ?? "0.0.0";
|
|
3637
4261
|
} catch {
|
|
@@ -3641,7 +4265,7 @@ function cliVersion() {
|
|
|
3641
4265
|
var program = new Command();
|
|
3642
4266
|
function validateRuntimeNodeVersion() {
|
|
3643
4267
|
try {
|
|
3644
|
-
const pkgPath =
|
|
4268
|
+
const pkgPath = path29.join(packageRoot(), "package.json");
|
|
3645
4269
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
3646
4270
|
const requiredRange = pkg.engines?.node;
|
|
3647
4271
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -3662,6 +4286,9 @@ program.command("doctor").option("--hooks", "Verify .claude/settings.json matche
|
|
|
3662
4286
|
program.command("apply").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option(
|
|
3663
4287
|
"--allow-empty-cache",
|
|
3664
4288
|
"Apply core files only when catalog cache is empty (skip catalog items without error)"
|
|
4289
|
+
).option(
|
|
4290
|
+
"--refill-config",
|
|
4291
|
+
"Fill still-blank fields in an existing workflow-config.md without touching edited ones"
|
|
3665
4292
|
).action(runApply);
|
|
3666
4293
|
program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
|
|
3667
4294
|
program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
|
|
@@ -3671,20 +4298,15 @@ program.command("refresh").action(runRefresh);
|
|
|
3671
4298
|
program.command("catalog-audit").action(runCatalogAudit);
|
|
3672
4299
|
program.command("validate-catalog").argument("[manifest]").action(runValidateCatalog);
|
|
3673
4300
|
program.command("update").option("--check").action(runUpdate);
|
|
3674
|
-
program.command("install").option("--dry-run").option("--force").option("--check", "Exit non-zero if any HAUS-MANAGED file is out of date").action(runInstall);
|
|
4301
|
+
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);
|
|
3675
4302
|
program.command("uninstall").option("--force").action(runUninstallCommand);
|
|
3676
|
-
var memory = program.command("memory");
|
|
3677
|
-
memory.command("status").action(() => runMemory("status", {}));
|
|
3678
|
-
memory.command("add <text>").action((text) => runMemory("add", { text }));
|
|
3679
|
-
memory.command("inject").option("--task <task>").option("--from-hook").action((opts) => runMemory("inject", opts));
|
|
3680
|
-
memory.command("promote").action(() => runMemory("promote", {}));
|
|
3681
4303
|
var guard = program.command("guard");
|
|
3682
4304
|
guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
|
|
3683
4305
|
guard.command("bash").option("--from-hook").action((opts) => runGuard("bash", opts));
|
|
3684
4306
|
var config = program.command("config");
|
|
3685
|
-
config.command("enable <key>").description("Enable a hook (hook.context
|
|
3686
|
-
config.command("disable <key>").description("Disable a hook (hook.context
|
|
3687
|
-
config.command("status <key>").description("Show current state of a hook (hook.context
|
|
4307
|
+
config.command("enable <key>").description("Enable a hook (hook.context)").action((key) => runConfig(key, "enable"));
|
|
4308
|
+
config.command("disable <key>").description("Disable a hook (hook.context)").action((key) => runConfig(key, "disable"));
|
|
4309
|
+
config.command("status <key>").description("Show current state of a hook (hook.context)").action((key) => runConfig(key, "status"));
|
|
3688
4310
|
var workspace = program.command("workspace");
|
|
3689
4311
|
workspace.command("init").action(() => runWorkspace("init"));
|
|
3690
4312
|
workspace.command("scan").action(() => runWorkspace("scan"));
|