@haus-tech/haus-workflow 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -8
- package/README.md +40 -9
- package/dist/cli.js +1446 -836
- 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,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { readFileSync as readFileSync3 } from "fs";
|
|
5
|
-
import
|
|
5
|
+
import path30 from "path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/commands/apply.ts
|
|
9
|
-
import
|
|
9
|
+
import path12 from "path";
|
|
10
10
|
import checkbox from "@inquirer/checkbox";
|
|
11
11
|
|
|
12
12
|
// src/catalog/remote-catalog.ts
|
|
@@ -158,8 +158,8 @@ async function getCacheManifestAge() {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
// src/claude/write-claude-files.ts
|
|
161
|
-
import
|
|
162
|
-
import
|
|
161
|
+
import path11 from "path";
|
|
162
|
+
import fs10 from "fs-extra";
|
|
163
163
|
|
|
164
164
|
// src/update/hash-installed.ts
|
|
165
165
|
import path3 from "path";
|
|
@@ -254,10 +254,10 @@ function summarizeDiff(diffText) {
|
|
|
254
254
|
const lines = diffText.split("\n");
|
|
255
255
|
let additions = 0;
|
|
256
256
|
let deletions = 0;
|
|
257
|
-
for (const
|
|
258
|
-
if (
|
|
259
|
-
if (
|
|
260
|
-
if (
|
|
257
|
+
for (const line2 of lines) {
|
|
258
|
+
if (line2.startsWith("+++ ") || line2.startsWith("--- ")) continue;
|
|
259
|
+
if (line2.startsWith("+")) additions += 1;
|
|
260
|
+
if (line2.startsWith("-")) deletions += 1;
|
|
261
261
|
}
|
|
262
262
|
return { additions, deletions };
|
|
263
263
|
}
|
|
@@ -313,8 +313,7 @@ import path5 from "path";
|
|
|
313
313
|
var CONFIG_PATH = ".haus-workflow/config.json";
|
|
314
314
|
var DEFAULT_HOOKS_CONFIG = {
|
|
315
315
|
hooks: {
|
|
316
|
-
context: { enabled: false }
|
|
317
|
-
memoryInject: { enabled: false }
|
|
316
|
+
context: { enabled: false }
|
|
318
317
|
}
|
|
319
318
|
};
|
|
320
319
|
async function isHookEnabled(root, key) {
|
|
@@ -322,15 +321,103 @@ async function isHookEnabled(root, key) {
|
|
|
322
321
|
return cfg?.hooks?.[key]?.enabled === true;
|
|
323
322
|
}
|
|
324
323
|
|
|
324
|
+
// src/security/dangerous-commands.ts
|
|
325
|
+
var DANGEROUS_COMMANDS = [
|
|
326
|
+
"rm -rf",
|
|
327
|
+
"sudo",
|
|
328
|
+
"chmod -R 777",
|
|
329
|
+
"chown -R",
|
|
330
|
+
"git push --force",
|
|
331
|
+
"git reset --hard",
|
|
332
|
+
"docker system prune",
|
|
333
|
+
"drop database",
|
|
334
|
+
"truncate table",
|
|
335
|
+
"php artisan migrate --force",
|
|
336
|
+
"npm publish",
|
|
337
|
+
"yarn npm publish",
|
|
338
|
+
"pnpm publish"
|
|
339
|
+
];
|
|
340
|
+
|
|
341
|
+
// src/security/sensitive-paths.ts
|
|
342
|
+
var SENSITIVE_PATHS = [
|
|
343
|
+
".env",
|
|
344
|
+
".env.*",
|
|
345
|
+
"*.pem",
|
|
346
|
+
"*.key",
|
|
347
|
+
"*.p12",
|
|
348
|
+
"*.pfx",
|
|
349
|
+
"id_rsa",
|
|
350
|
+
"id_ed25519",
|
|
351
|
+
"*.sql",
|
|
352
|
+
"*.dump",
|
|
353
|
+
"*.backup",
|
|
354
|
+
"*.bak",
|
|
355
|
+
"storage/logs",
|
|
356
|
+
"wp-content/uploads",
|
|
357
|
+
"uploads",
|
|
358
|
+
"customer-data",
|
|
359
|
+
"exports",
|
|
360
|
+
"secrets",
|
|
361
|
+
"certs"
|
|
362
|
+
];
|
|
363
|
+
var SENSITIVE_PATH_REGEXES = [
|
|
364
|
+
/^\.env(\.|$)/,
|
|
365
|
+
/(^|\/)\.env(\.|$)/,
|
|
366
|
+
/\.pem$/,
|
|
367
|
+
/\.key$/,
|
|
368
|
+
/\.p12$/,
|
|
369
|
+
/\.pfx$/,
|
|
370
|
+
/\.sql$/,
|
|
371
|
+
/\.dump$/,
|
|
372
|
+
/customer-data/,
|
|
373
|
+
/exports/,
|
|
374
|
+
/certs/,
|
|
375
|
+
/secrets/,
|
|
376
|
+
/(^|\/)storage\/logs(\/|$)/,
|
|
377
|
+
/(^|\/)wp-content\/uploads(\/|$)/,
|
|
378
|
+
/(^|\/)uploads(\/|$)/
|
|
379
|
+
];
|
|
380
|
+
var SENSITIVE_ITEM_KEYWORDS = [
|
|
381
|
+
".env",
|
|
382
|
+
"secrets",
|
|
383
|
+
"certs",
|
|
384
|
+
"customer-data",
|
|
385
|
+
"exports",
|
|
386
|
+
".pem",
|
|
387
|
+
".key"
|
|
388
|
+
];
|
|
389
|
+
|
|
390
|
+
// src/security/deny-rules.ts
|
|
391
|
+
var SENSITIVE_DIRS = /* @__PURE__ */ new Set([
|
|
392
|
+
"storage/logs",
|
|
393
|
+
"wp-content/uploads",
|
|
394
|
+
"uploads",
|
|
395
|
+
"customer-data",
|
|
396
|
+
"exports",
|
|
397
|
+
"secrets",
|
|
398
|
+
"certs"
|
|
399
|
+
]);
|
|
400
|
+
var FILE_TOOLS = ["Read", "Edit", "Write"];
|
|
401
|
+
function buildDenyRules() {
|
|
402
|
+
const rules = [];
|
|
403
|
+
for (const command of DANGEROUS_COMMANDS) {
|
|
404
|
+
rules.push(`Bash(${command}:*)`);
|
|
405
|
+
}
|
|
406
|
+
for (const path31 of SENSITIVE_PATHS) {
|
|
407
|
+
const pattern = SENSITIVE_DIRS.has(path31) ? `${path31}/**` : path31;
|
|
408
|
+
for (const tool of FILE_TOOLS) {
|
|
409
|
+
rules.push(`${tool}(${pattern})`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return [...new Set(rules)];
|
|
413
|
+
}
|
|
414
|
+
|
|
325
415
|
// src/claude/load-hooks.ts
|
|
326
416
|
var CANONICAL_HOOKS = {
|
|
327
417
|
hooks: {
|
|
328
418
|
UserPromptSubmit: [
|
|
329
419
|
{
|
|
330
|
-
hooks: [
|
|
331
|
-
{ type: "command", command: "haus context --from-hook || true" },
|
|
332
|
-
{ type: "command", command: "haus memory inject --from-hook || true" }
|
|
333
|
-
]
|
|
420
|
+
hooks: [{ type: "command", command: "haus context --from-hook || true" }]
|
|
334
421
|
}
|
|
335
422
|
],
|
|
336
423
|
PreToolUse: [
|
|
@@ -347,12 +434,11 @@ var CANONICAL_HOOKS = {
|
|
|
347
434
|
};
|
|
348
435
|
var STABLE_HOOK_IDS = {
|
|
349
436
|
"haus context --from-hook || true": "haus.context-hook",
|
|
350
|
-
"haus memory inject --from-hook || true": "haus.memory-hook",
|
|
351
437
|
"haus guard file-access --from-hook || true": "haus.guard-file",
|
|
352
438
|
"haus guard bash --from-hook || true": "haus.guard-bash"
|
|
353
439
|
};
|
|
354
440
|
async function loadClaudeHooksSettings() {
|
|
355
|
-
return CANONICAL_HOOKS;
|
|
441
|
+
return { ...CANONICAL_HOOKS, permissions: { deny: buildDenyRules() } };
|
|
356
442
|
}
|
|
357
443
|
function flattenRecommendedHooks(settings) {
|
|
358
444
|
const out = [];
|
|
@@ -438,9 +524,11 @@ function renderProjectFacts(ctx, rec, pkgVersion) {
|
|
|
438
524
|
const repoName = ctx.repoName ?? path6.basename(ctx.root ?? "unknown");
|
|
439
525
|
return `${header}
|
|
440
526
|
|
|
441
|
-
#
|
|
527
|
+
# What haus found in this project
|
|
442
528
|
|
|
443
|
-
>
|
|
529
|
+
> This is a plain summary of your project that haus wrote automatically, so Claude
|
|
530
|
+
> always has the basics to hand. haus rewrites it on every \`haus apply\`, so don't
|
|
531
|
+
> edit it by hand \u2014 your changes would be replaced next time.
|
|
444
532
|
|
|
445
533
|
**Repo:** ${repoName}
|
|
446
534
|
**Package manager:** ${ctx.packageManager ?? "unknown"}
|
|
@@ -472,7 +560,9 @@ async function writeProjectFacts(root, pkgVersion, dryRun) {
|
|
|
472
560
|
dependencies: [],
|
|
473
561
|
securityRisks: [],
|
|
474
562
|
crossRepoHints: [],
|
|
475
|
-
warnings: []
|
|
563
|
+
warnings: [],
|
|
564
|
+
detectionStatus: "unknown",
|
|
565
|
+
unsupportedSignals: []
|
|
476
566
|
};
|
|
477
567
|
const rec = await readJson(hausPath(root, "recommendation.json")) ?? {
|
|
478
568
|
mode: "fast",
|
|
@@ -564,84 +654,190 @@ async function writeRootClaudeMd(root, dryRun) {
|
|
|
564
654
|
}
|
|
565
655
|
|
|
566
656
|
// src/claude/write-workflow-config.ts
|
|
657
|
+
import path9 from "path";
|
|
658
|
+
import fs8 from "fs-extra";
|
|
659
|
+
|
|
660
|
+
// src/claude/derive-workflow-config.ts
|
|
567
661
|
import path8 from "path";
|
|
568
662
|
import fs7 from "fs-extra";
|
|
569
|
-
|
|
663
|
+
var VALIDATION_LIBS = [
|
|
664
|
+
"zod",
|
|
665
|
+
"valibot",
|
|
666
|
+
"yup",
|
|
667
|
+
"joi",
|
|
668
|
+
"@hapi/joi",
|
|
669
|
+
"class-validator",
|
|
670
|
+
"superstruct",
|
|
671
|
+
"ajv"
|
|
672
|
+
];
|
|
673
|
+
function binCmd(pm, bin, args) {
|
|
674
|
+
const tail = args ? ` ${args}` : "";
|
|
675
|
+
if (pm === "yarn") return `yarn ${bin}${tail}`;
|
|
676
|
+
if (pm === "pnpm") return `pnpm exec ${bin}${tail}`;
|
|
677
|
+
return `npx --no-install ${bin}${tail}`;
|
|
678
|
+
}
|
|
679
|
+
async function deriveWorkflowConfig(root, ctx) {
|
|
570
680
|
const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
|
|
571
|
-
const
|
|
572
|
-
const
|
|
573
|
-
|
|
681
|
+
const pkg = await readJson(path8.join(root, "package.json"));
|
|
682
|
+
const scripts = pkg?.scripts ?? {};
|
|
683
|
+
const deps = new Set(ctx.dependencies);
|
|
684
|
+
const stacks = Object.values(ctx.detectedStacks ?? {}).flat();
|
|
685
|
+
const script = (name) => scripts[name] ? `${pm} run ${name}` : null;
|
|
686
|
+
const firstScript = (...names) => {
|
|
687
|
+
for (const n of names) if (scripts[n]) return `${pm} run ${n}`;
|
|
688
|
+
return null;
|
|
689
|
+
};
|
|
690
|
+
const hasDep = (name) => deps.has(name);
|
|
691
|
+
const exists = (rel) => fs7.pathExistsSync(path8.join(root, rel));
|
|
692
|
+
const hasTypeScript = hasDep("typescript") || exists("tsconfig.json");
|
|
693
|
+
const hasEslint = hasDep("eslint");
|
|
694
|
+
const hasPrettier = hasDep("prettier");
|
|
695
|
+
const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
|
|
696
|
+
const hasCypress = hasDep("cypress");
|
|
697
|
+
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;
|
|
698
|
+
return {
|
|
699
|
+
test: script("test") ?? `${pm} test`,
|
|
700
|
+
testE2E: firstScript("test:e2e", "e2e", "test:integration") ?? (hasPlaywright ? binCmd(pm, "playwright", "test") : null) ?? (hasCypress ? binCmd(pm, "cypress", "run") : null),
|
|
701
|
+
typecheck: firstScript("typecheck", "type-check", "tsc") ?? (hasTypeScript ? binCmd(pm, "tsc", "--noEmit") : null),
|
|
702
|
+
lint: script("lint") ?? (hasEslint ? binCmd(pm, "eslint", ".") : null),
|
|
703
|
+
lintFix: firstScript("lint:fix", "lint-fix") ?? (scripts.lint ? `${pm} run lint -- --fix` : hasEslint ? binCmd(pm, "eslint", ". --fix") : null),
|
|
704
|
+
formatCheck: firstScript("format:check", "format-check", "prettier:check") ?? (hasPrettier ? binCmd(pm, "prettier", "--check .") : null),
|
|
705
|
+
securityAudit: `${pm} audit`,
|
|
706
|
+
validationLibrary: VALIDATION_LIBS.find((lib) => deps.has(lib)) ?? null,
|
|
707
|
+
preCommitTool,
|
|
708
|
+
specPath: exists("docs/SPEC.md") ? "docs/SPEC.md" : null,
|
|
709
|
+
designPath: exists("docs/DESIGN.md") ? "docs/DESIGN.md" : null,
|
|
710
|
+
uxPath: exists("docs/UX.md") ? "docs/UX.md" : null
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// src/claude/write-workflow-config.ts
|
|
715
|
+
function fields(v) {
|
|
716
|
+
return [
|
|
717
|
+
{ prefix: "- Spec: ", value: v.specPath, hint: "path, e.g. docs/SPEC.md" },
|
|
718
|
+
{ prefix: "- Design: ", value: v.designPath, hint: "path, e.g. docs/DESIGN.md" },
|
|
719
|
+
{ prefix: "- UX flows: ", value: v.uxPath, hint: "path, e.g. docs/UX.md" },
|
|
720
|
+
{ prefix: "- Test (unit + integration): ", value: v.test, hint: "command", code: true },
|
|
721
|
+
{ prefix: "- Test (E2E): ", value: v.testE2E, hint: "command, e.g. playwright test", code: true },
|
|
722
|
+
{ prefix: "- Type check: ", value: v.typecheck, hint: "command, e.g. tsc --noEmit", code: true },
|
|
723
|
+
{ prefix: "- Lint: ", value: v.lint, hint: "command, e.g. eslint .", code: true },
|
|
724
|
+
{ prefix: "- Lint fix: ", value: v.lintFix, hint: "command, e.g. eslint . --fix", code: true },
|
|
725
|
+
{ prefix: "- Format check: ", value: v.formatCheck, hint: "command, e.g. prettier --check .", code: true },
|
|
726
|
+
{ prefix: "- Security audit: ", value: v.securityAudit, hint: "command", code: true },
|
|
727
|
+
{ prefix: "- Library: ", value: v.validationLibrary, hint: "e.g. zod, yup, joi" },
|
|
728
|
+
{ prefix: "- Tool: ", value: v.preCommitTool, hint: "e.g. lefthook, husky" }
|
|
729
|
+
];
|
|
574
730
|
}
|
|
575
|
-
|
|
731
|
+
function renderValue(f) {
|
|
732
|
+
if (f.value === null) return `<!-- fill in ${f.hint} -->`;
|
|
733
|
+
return f.code ? `\`${f.value}\`` : f.value;
|
|
734
|
+
}
|
|
735
|
+
function line(f) {
|
|
736
|
+
return `${f.prefix}${renderValue(f)}`;
|
|
737
|
+
}
|
|
738
|
+
function buildWorkflowConfig(v) {
|
|
739
|
+
const f = fields(v);
|
|
740
|
+
const byPrefix = (p) => line(f.find((x) => x.prefix === p));
|
|
741
|
+
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";
|
|
742
|
+
}
|
|
743
|
+
function refillContent(existing, v) {
|
|
744
|
+
const f = fields(v);
|
|
745
|
+
return existing.split("\n").map((ln) => {
|
|
746
|
+
const field = f.find((x) => ln.startsWith(x.prefix));
|
|
747
|
+
if (!field || field.value === null) return ln;
|
|
748
|
+
const rest = ln.slice(field.prefix.length).trim();
|
|
749
|
+
return rest.startsWith("<!-- fill in") ? line(field) : ln;
|
|
750
|
+
}).join("\n");
|
|
751
|
+
}
|
|
752
|
+
var FALLBACK_CONTEXT = {
|
|
753
|
+
mode: "fast",
|
|
754
|
+
generatedAt: "",
|
|
755
|
+
root: "",
|
|
756
|
+
repoName: "",
|
|
757
|
+
packageManager: "unknown",
|
|
758
|
+
repoRoles: [],
|
|
759
|
+
confidence: 0,
|
|
760
|
+
detectedStacks: {},
|
|
761
|
+
dependencies: [],
|
|
762
|
+
securityRisks: [],
|
|
763
|
+
crossRepoHints: [],
|
|
764
|
+
warnings: [],
|
|
765
|
+
detectionStatus: "unknown",
|
|
766
|
+
unsupportedSignals: []
|
|
767
|
+
};
|
|
768
|
+
async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
576
769
|
const destPath = hausPath(root, "workflow-config.md");
|
|
577
770
|
const printable = displayPath(root, destPath);
|
|
578
|
-
|
|
771
|
+
const exists = await fs8.pathExists(destPath);
|
|
772
|
+
if (exists && !opts.refill) {
|
|
579
773
|
if (dryRun) log(printable + ": exists (project-owned, skipping)");
|
|
580
774
|
return null;
|
|
581
775
|
}
|
|
582
776
|
const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
|
|
583
|
-
|
|
584
|
-
generatedAt: "",
|
|
777
|
+
...FALLBACK_CONTEXT,
|
|
585
778
|
root,
|
|
586
|
-
repoName:
|
|
587
|
-
packageManager: "unknown",
|
|
588
|
-
repoRoles: [],
|
|
589
|
-
confidence: 0,
|
|
590
|
-
detectedStacks: {},
|
|
591
|
-
dependencies: [],
|
|
592
|
-
securityRisks: [],
|
|
593
|
-
crossRepoHints: [],
|
|
594
|
-
warnings: []
|
|
779
|
+
repoName: path9.basename(root)
|
|
595
780
|
};
|
|
596
|
-
const
|
|
781
|
+
const values = await deriveWorkflowConfig(root, ctx);
|
|
782
|
+
if (exists) {
|
|
783
|
+
const current = await fs8.readFile(destPath, "utf8");
|
|
784
|
+
const refilled = refillContent(current, values);
|
|
785
|
+
if (refilled === current) {
|
|
786
|
+
if (dryRun) log(printable + ": no blank fields to refill");
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
if (dryRun) {
|
|
790
|
+
log(printable + ": would refill blank fields");
|
|
791
|
+
return destPath;
|
|
792
|
+
}
|
|
793
|
+
await writeText(destPath, refilled);
|
|
794
|
+
return destPath;
|
|
795
|
+
}
|
|
597
796
|
if (dryRun) {
|
|
598
797
|
log(printable + ": would create");
|
|
599
798
|
return destPath;
|
|
600
799
|
}
|
|
601
|
-
await writeText(destPath,
|
|
800
|
+
await writeText(destPath, buildWorkflowConfig(values));
|
|
602
801
|
return destPath;
|
|
603
802
|
}
|
|
604
803
|
|
|
605
804
|
// src/claude/write-workflow.ts
|
|
606
|
-
import
|
|
607
|
-
import
|
|
805
|
+
import path10 from "path";
|
|
806
|
+
import fs9 from "fs-extra";
|
|
608
807
|
|
|
609
808
|
// src/claude/managed-template.ts
|
|
610
|
-
function normaliseLF(
|
|
611
|
-
return
|
|
809
|
+
function normaliseLF(content2) {
|
|
810
|
+
return content2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
612
811
|
}
|
|
613
|
-
function parseHausManagedHeader(
|
|
614
|
-
const match =
|
|
812
|
+
function parseHausManagedHeader(line2) {
|
|
813
|
+
const match = line2.match(/<!-- HAUS-MANAGED id=([\w.:-]+)/);
|
|
615
814
|
if (!match) return null;
|
|
616
|
-
const hashMatch =
|
|
815
|
+
const hashMatch = line2.match(/hash=(sha256-[a-f0-9]+)/);
|
|
617
816
|
return { id: match[1], hash: hashMatch?.[1] };
|
|
618
817
|
}
|
|
619
818
|
|
|
620
819
|
// src/claude/write-workflow.ts
|
|
621
820
|
var STABLE_ID2 = "template.workflow";
|
|
622
821
|
var SCHEMA_VERSION2 = "1";
|
|
623
|
-
var
|
|
624
|
-
var CATALOG_CACHE_TEMPLATE = path9.join(CACHE_DIR, "templates/agentic-workflow-standard.md");
|
|
822
|
+
var CATALOG_CACHE_TEMPLATE = path10.join(CACHE_DIR, "templates/agentic-workflow-standard.md");
|
|
625
823
|
function makeWorkflowHeader(pkgVersion, contentHash) {
|
|
626
824
|
return `<!-- HAUS-MANAGED id=${STABLE_ID2} v=${SCHEMA_VERSION2} source=@haus-tech/haus-workflow@${pkgVersion} hash=${contentHash} -->`;
|
|
627
825
|
}
|
|
628
826
|
async function writeWorkflow(root, pkgVersion, dryRun) {
|
|
629
|
-
|
|
630
|
-
const packagePath = path9.join(packageRoot(), TEMPLATE_REL);
|
|
631
|
-
const templatePath = await fs8.pathExists(cachePath) ? cachePath : packagePath;
|
|
632
|
-
if (!await fs8.pathExists(templatePath)) {
|
|
827
|
+
if (!await fs9.pathExists(CATALOG_CACHE_TEMPLATE)) {
|
|
633
828
|
warn(`Workflow template not found \u2014 run \`haus update\` to fetch from catalog`);
|
|
634
829
|
return null;
|
|
635
830
|
}
|
|
636
|
-
const
|
|
831
|
+
const templatePath = CATALOG_CACHE_TEMPLATE;
|
|
832
|
+
const templateContent = await fs9.readFile(templatePath, "utf8");
|
|
637
833
|
const contentHash = hashText(normaliseLF(templateContent));
|
|
638
834
|
const header = makeWorkflowHeader(pkgVersion, contentHash);
|
|
639
835
|
const next = `${header}
|
|
640
836
|
${templateContent}`;
|
|
641
837
|
const destPath = hausPath(root, "WORKFLOW.md");
|
|
642
838
|
const printable = displayPath(root, destPath);
|
|
643
|
-
if (await
|
|
644
|
-
const existing = await
|
|
839
|
+
if (await fs9.pathExists(destPath)) {
|
|
840
|
+
const existing = await fs9.readFile(destPath, "utf8");
|
|
645
841
|
const firstLine = existing.split("\n")[0] ?? "";
|
|
646
842
|
const parsed = parseHausManagedHeader(firstLine);
|
|
647
843
|
if (!parsed) {
|
|
@@ -663,7 +859,7 @@ ${templateContent}`;
|
|
|
663
859
|
}
|
|
664
860
|
}
|
|
665
861
|
if (dryRun) {
|
|
666
|
-
const prev = await
|
|
862
|
+
const prev = await fs9.pathExists(destPath) ? await fs9.readFile(destPath, "utf8") : "";
|
|
667
863
|
if (!prev) {
|
|
668
864
|
log(createUnifiedDiff(printable, "", next));
|
|
669
865
|
} else {
|
|
@@ -678,7 +874,7 @@ ${templateContent}`;
|
|
|
678
874
|
}
|
|
679
875
|
|
|
680
876
|
// src/claude/write-claude-files.ts
|
|
681
|
-
async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
877
|
+
async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
682
878
|
const rec = await readJson(hausPath(root, "recommendation.json")) ?? {
|
|
683
879
|
mode: "fast",
|
|
684
880
|
recommended: [],
|
|
@@ -690,7 +886,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
690
886
|
estimatedTokenReductionPct: 0
|
|
691
887
|
};
|
|
692
888
|
const pkgRoot = packageRoot();
|
|
693
|
-
const hausVersion = (await readJson(
|
|
889
|
+
const hausVersion = (await readJson(path11.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
|
|
694
890
|
const coreFiles = [
|
|
695
891
|
claudePath(root, "settings.json"),
|
|
696
892
|
claudePath(root, "rules", "haus.md"),
|
|
@@ -700,7 +896,9 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
700
896
|
];
|
|
701
897
|
const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun);
|
|
702
898
|
const workflowPath = await writeWorkflow(root, hausVersion, dryRun);
|
|
703
|
-
const workflowConfigPath = await writeWorkflowConfig(root, dryRun
|
|
899
|
+
const workflowConfigPath = await writeWorkflowConfig(root, dryRun, {
|
|
900
|
+
refill: opts.refillConfig
|
|
901
|
+
});
|
|
704
902
|
const projectFactsPath = await writeProjectFacts(root, hausVersion, dryRun);
|
|
705
903
|
const p6Files = [
|
|
706
904
|
rootClaudeMdPath,
|
|
@@ -718,7 +916,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
718
916
|
await writeManagedJson(root, claudePath(root, "settings.json"), hookSettings, dryRun);
|
|
719
917
|
if (!dryRun) await assertPostApplySettingsMatchCanonical(root, hookSettings);
|
|
720
918
|
const configPath = hausPath(root, "config.json");
|
|
721
|
-
if (!await
|
|
919
|
+
if (!await fs10.pathExists(configPath)) {
|
|
722
920
|
await writeManagedJson(root, configPath, DEFAULT_HOOKS_CONFIG, dryRun);
|
|
723
921
|
}
|
|
724
922
|
await writeManagedText(
|
|
@@ -736,7 +934,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
736
934
|
await writeManagedText(
|
|
737
935
|
root,
|
|
738
936
|
claudePath(root, "rules", "haus.md"),
|
|
739
|
-
"- Keep context minimal.\n- Follow project conventions.\n",
|
|
937
|
+
"- 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
938
|
dryRun
|
|
741
939
|
);
|
|
742
940
|
await writeManagedText(
|
|
@@ -746,12 +944,12 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
746
944
|
dryRun
|
|
747
945
|
);
|
|
748
946
|
const fixtureManifestPath = process.env["HAUS_FIXTURE_CATALOG"];
|
|
749
|
-
const manifestPath = fixtureManifestPath ??
|
|
750
|
-
const manifestDir =
|
|
947
|
+
const manifestPath = fixtureManifestPath ?? path11.join(pkgRoot, "library", "catalog", "manifest.json");
|
|
948
|
+
const manifestDir = path11.dirname(manifestPath);
|
|
751
949
|
const manifest = await readJson(manifestPath) ?? { items: [] };
|
|
752
950
|
const manifestById = new Map((manifest.items ?? []).map((item) => [item.id, item]));
|
|
753
951
|
const cacheManifest = await readJson(
|
|
754
|
-
|
|
952
|
+
path11.join(CACHE_DIR, "manifest.json")
|
|
755
953
|
);
|
|
756
954
|
const cacheManifestById = new Map((cacheManifest?.items ?? []).map((item) => [item.id, item]));
|
|
757
955
|
const installedPathsByItem = /* @__PURE__ */ new Map();
|
|
@@ -773,23 +971,23 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
773
971
|
}
|
|
774
972
|
}
|
|
775
973
|
const cachedItem = cacheManifestById.get(item.id);
|
|
776
|
-
const cachePath = cachedItem?.path ?
|
|
777
|
-
const sourcePath = cachePath && await
|
|
974
|
+
const cachePath = cachedItem?.path ? path11.join(CACHE_DIR, cachedItem.path) : null;
|
|
975
|
+
const sourcePath = cachePath && await fs10.pathExists(cachePath) ? cachePath : path11.join(manifestDir, manifestItem.path);
|
|
778
976
|
const target = item.type === "agent" ? "agents" : item.type === "template" ? "templates" : "skills";
|
|
779
|
-
const destination = claudePath(root, target,
|
|
780
|
-
if (await
|
|
977
|
+
const destination = claudePath(root, target, path11.basename(sourcePath));
|
|
978
|
+
if (await fs10.pathExists(sourcePath)) {
|
|
781
979
|
if (dryRun) {
|
|
782
|
-
const exists = await
|
|
980
|
+
const exists = await fs10.pathExists(destination);
|
|
783
981
|
log(
|
|
784
982
|
`${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`
|
|
785
983
|
);
|
|
786
984
|
} else {
|
|
787
|
-
await
|
|
788
|
-
await
|
|
985
|
+
await fs10.ensureDir(path11.dirname(destination));
|
|
986
|
+
await fs10.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
789
987
|
}
|
|
790
988
|
files.push(destination);
|
|
791
989
|
const current = installedPathsByItem.get(item.id) ?? [];
|
|
792
|
-
installedPathsByItem.set(item.id, [...current,
|
|
990
|
+
installedPathsByItem.set(item.id, [...current, path11.relative(root, destination)]);
|
|
793
991
|
installedIds.add(item.id);
|
|
794
992
|
} else {
|
|
795
993
|
warn(
|
|
@@ -840,7 +1038,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
840
1038
|
return [...new Set(files)];
|
|
841
1039
|
}
|
|
842
1040
|
async function writeManagedText(root, filePath, nextText, dryRun) {
|
|
843
|
-
const prev = await
|
|
1041
|
+
const prev = await fs10.pathExists(filePath) ? await fs10.readFile(filePath, "utf8") : "";
|
|
844
1042
|
const printable = displayPath(root, filePath);
|
|
845
1043
|
if (dryRun) {
|
|
846
1044
|
if (!prev) {
|
|
@@ -867,7 +1065,7 @@ async function writeManagedJson(root, filePath, value, dryRun) {
|
|
|
867
1065
|
|
|
868
1066
|
// src/commands/apply.ts
|
|
869
1067
|
async function cacheHasItems() {
|
|
870
|
-
const data = await readJson(
|
|
1068
|
+
const data = await readJson(path12.join(CACHE_DIR, "manifest.json"));
|
|
871
1069
|
return Array.isArray(data?.items) && data.items.length > 0;
|
|
872
1070
|
}
|
|
873
1071
|
async function runApply(options) {
|
|
@@ -924,7 +1122,9 @@ async function runApply(options) {
|
|
|
924
1122
|
}
|
|
925
1123
|
}
|
|
926
1124
|
}
|
|
927
|
-
const files = await writeClaudeFiles(root, isDryRun, selectedIds
|
|
1125
|
+
const files = await writeClaudeFiles(root, isDryRun, selectedIds, {
|
|
1126
|
+
refillConfig: options.refillConfig
|
|
1127
|
+
});
|
|
928
1128
|
if (isDryRun) {
|
|
929
1129
|
log(`Dry-run complete \u2014 ${files.length} file(s) planned, none written. Run --write to apply.`);
|
|
930
1130
|
} else {
|
|
@@ -935,8 +1135,8 @@ async function runApply(options) {
|
|
|
935
1135
|
|
|
936
1136
|
// src/catalog/load-catalog.ts
|
|
937
1137
|
import os3 from "os";
|
|
938
|
-
import
|
|
939
|
-
var CACHE_MANIFEST =
|
|
1138
|
+
import path13 from "path";
|
|
1139
|
+
var CACHE_MANIFEST = path13.join(os3.homedir(), CATALOG_CACHE_SUBDIR, "manifest.json");
|
|
940
1140
|
async function loadCatalog(root) {
|
|
941
1141
|
const envPath = process.env["HAUS_FIXTURE_CATALOG"];
|
|
942
1142
|
if (envPath) {
|
|
@@ -945,10 +1145,10 @@ async function loadCatalog(root) {
|
|
|
945
1145
|
}
|
|
946
1146
|
const cacheData = await readJson(CACHE_MANIFEST);
|
|
947
1147
|
if (cacheData?.items?.length) return cacheData.items;
|
|
948
|
-
const localManifest =
|
|
1148
|
+
const localManifest = path13.join(root, "library/catalog/manifest.json");
|
|
949
1149
|
const localData = await readJson(localManifest);
|
|
950
1150
|
if (localData?.items?.length) return localData.items;
|
|
951
|
-
const packageManifest =
|
|
1151
|
+
const packageManifest = path13.join(packageRoot(), "library/catalog/manifest.json");
|
|
952
1152
|
const data = await readJson(packageManifest);
|
|
953
1153
|
return data?.items ?? [];
|
|
954
1154
|
}
|
|
@@ -988,11 +1188,10 @@ async function runCatalogAudit() {
|
|
|
988
1188
|
}
|
|
989
1189
|
|
|
990
1190
|
// src/commands/config.ts
|
|
991
|
-
import
|
|
1191
|
+
import path14 from "path";
|
|
992
1192
|
var CONFIG_PATH2 = ".haus-workflow/config.json";
|
|
993
1193
|
var HOOK_ALIASES = {
|
|
994
|
-
"hook.context": "context"
|
|
995
|
-
"hook.memory": "memoryInject"
|
|
1194
|
+
"hook.context": "context"
|
|
996
1195
|
};
|
|
997
1196
|
async function runConfig(key, action) {
|
|
998
1197
|
const hookKey = HOOK_ALIASES[key];
|
|
@@ -1002,7 +1201,7 @@ async function runConfig(key, action) {
|
|
|
1002
1201
|
);
|
|
1003
1202
|
}
|
|
1004
1203
|
const root = process.cwd();
|
|
1005
|
-
const configPath =
|
|
1204
|
+
const configPath = path14.join(root, CONFIG_PATH2);
|
|
1006
1205
|
const existing = await readJson(configPath);
|
|
1007
1206
|
const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
|
|
1008
1207
|
cfg.hooks ??= {};
|
|
@@ -1045,7 +1244,8 @@ function normalizeRecommendation(input2) {
|
|
|
1045
1244
|
finalScore: item.score ?? 0
|
|
1046
1245
|
},
|
|
1047
1246
|
tags: item.tags,
|
|
1048
|
-
ecosystem: item.ecosystem
|
|
1247
|
+
ecosystem: item.ecosystem,
|
|
1248
|
+
tokenEstimate: item.tokenEstimate
|
|
1049
1249
|
};
|
|
1050
1250
|
});
|
|
1051
1251
|
const skipped = (input2.skipped ?? []).map((item) => ({
|
|
@@ -1105,46 +1305,7 @@ function buildRecommendationExplanation(recommendation) {
|
|
|
1105
1305
|
};
|
|
1106
1306
|
}
|
|
1107
1307
|
|
|
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
|
-
}
|
|
1308
|
+
// src/recommender/task-classification.ts
|
|
1148
1309
|
var ALL_INTENTS = [
|
|
1149
1310
|
"backend",
|
|
1150
1311
|
"frontend",
|
|
@@ -1358,9 +1519,76 @@ function computeRuleIntents(rule) {
|
|
|
1358
1519
|
return intents;
|
|
1359
1520
|
}
|
|
1360
1521
|
|
|
1522
|
+
// src/recommender/rule-selection.ts
|
|
1523
|
+
var DEFAULT_CONTEXT_TOKEN_BUDGET = 12e3;
|
|
1524
|
+
function pickTaskRelevantRules(recommendation, task, taskIntents = /* @__PURE__ */ new Set(), opts = {}) {
|
|
1525
|
+
const recommended = recommendation?.recommended ?? [];
|
|
1526
|
+
return applyTokenBudget(selectRules(recommended, task, taskIntents), opts.tokenBudget);
|
|
1527
|
+
}
|
|
1528
|
+
function applyTokenBudget(rules, budget) {
|
|
1529
|
+
if (!budget || budget <= 0) return rules;
|
|
1530
|
+
const total = rules.reduce((sum, r) => sum + (r.tokenEstimate ?? 0), 0);
|
|
1531
|
+
if (total <= budget) return rules;
|
|
1532
|
+
const keep = /* @__PURE__ */ new Set();
|
|
1533
|
+
let used = 0;
|
|
1534
|
+
for (const r of rules) {
|
|
1535
|
+
if (r.selectionMode === "baseline") {
|
|
1536
|
+
keep.add(r.id);
|
|
1537
|
+
used += r.tokenEstimate ?? 0;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
const matched = rules.filter((r) => r.selectionMode !== "baseline").sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
1541
|
+
for (const r of matched) {
|
|
1542
|
+
const est = r.tokenEstimate ?? 0;
|
|
1543
|
+
if (used + est <= budget) {
|
|
1544
|
+
keep.add(r.id);
|
|
1545
|
+
used += est;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
return rules.filter((r) => keep.has(r.id));
|
|
1549
|
+
}
|
|
1550
|
+
function selectRules(recommended, task, taskIntents) {
|
|
1551
|
+
if (!task) return recommended;
|
|
1552
|
+
if (taskIntents.size > 0) {
|
|
1553
|
+
const intentMatches = recommended.filter((rule) => {
|
|
1554
|
+
if (rule.selectionMode === "baseline") return false;
|
|
1555
|
+
const ruleIntents = computeRuleIntents(rule);
|
|
1556
|
+
if (ruleIntents.size === 0) return false;
|
|
1557
|
+
for (const ti of taskIntents) {
|
|
1558
|
+
if (ruleIntents.has(ti)) return true;
|
|
1559
|
+
}
|
|
1560
|
+
return false;
|
|
1561
|
+
});
|
|
1562
|
+
if (intentMatches.length > 0) return intentMatches;
|
|
1563
|
+
}
|
|
1564
|
+
const tokens = task.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
|
|
1565
|
+
const tokenMatches = recommended.filter((rule) => {
|
|
1566
|
+
if (rule.selectionMode === "baseline") return false;
|
|
1567
|
+
const corpus = [
|
|
1568
|
+
rule.id,
|
|
1569
|
+
rule.ecosystem ?? "",
|
|
1570
|
+
...rule.tags ?? [],
|
|
1571
|
+
rule.reason ?? "",
|
|
1572
|
+
...rule.reasons.map((r) => r.message)
|
|
1573
|
+
].join(" ").toLowerCase();
|
|
1574
|
+
return tokens.some((token) => corpus.includes(token));
|
|
1575
|
+
});
|
|
1576
|
+
if (tokenMatches.length > 0) return tokenMatches;
|
|
1577
|
+
const taskWantsTesting = taskIntents.has("testing");
|
|
1578
|
+
const cappedMediumOrHigh = recommended.filter((rule) => {
|
|
1579
|
+
if (rule.selectionMode === "baseline") return false;
|
|
1580
|
+
if (rule.confidenceLevel === "low") return false;
|
|
1581
|
+
if (taskWantsTesting) return true;
|
|
1582
|
+
const ruleIntents = computeRuleIntents(rule);
|
|
1583
|
+
const isTestingOnly = ruleIntents.size > 0 && [...ruleIntents].every((i) => i === "testing");
|
|
1584
|
+
return !isTestingOnly;
|
|
1585
|
+
});
|
|
1586
|
+
return cappedMediumOrHigh.slice(0, 8);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1361
1589
|
// src/scanner/scan-project.ts
|
|
1362
|
-
import { readFile } from "fs/promises";
|
|
1363
|
-
import
|
|
1590
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1591
|
+
import path18 from "path";
|
|
1364
1592
|
|
|
1365
1593
|
// src/utils/audit-checks.ts
|
|
1366
1594
|
function isRecord(v) {
|
|
@@ -1387,8 +1615,8 @@ function compareVersions(a, b) {
|
|
|
1387
1615
|
}
|
|
1388
1616
|
|
|
1389
1617
|
// src/scanner/detect-package-manager.ts
|
|
1390
|
-
import
|
|
1391
|
-
import
|
|
1618
|
+
import path15 from "path";
|
|
1619
|
+
import fs11 from "fs-extra";
|
|
1392
1620
|
function detectPackageManager(root, packageManagerField) {
|
|
1393
1621
|
const field = String(packageManagerField ?? "").trim();
|
|
1394
1622
|
if (field.startsWith("yarn@")) {
|
|
@@ -1406,77 +1634,394 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
1406
1634
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
1407
1635
|
return "unknown";
|
|
1408
1636
|
}
|
|
1409
|
-
if (
|
|
1410
|
-
if (
|
|
1411
|
-
if (
|
|
1637
|
+
if (fs11.existsSync(path15.join(root, "yarn.lock"))) return "yarn";
|
|
1638
|
+
if (fs11.existsSync(path15.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
1639
|
+
if (fs11.existsSync(path15.join(root, "package-lock.json"))) return "npm";
|
|
1412
1640
|
return "unknown";
|
|
1413
1641
|
}
|
|
1414
1642
|
|
|
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"
|
|
1643
|
+
// src/scanner/detection-registry.ts
|
|
1644
|
+
var dep = (value) => ({ kind: "dep", value });
|
|
1645
|
+
var depPrefix = (value) => ({ kind: "depPrefix", value });
|
|
1646
|
+
var depAbsent = (value) => ({ kind: "depAbsent", value });
|
|
1647
|
+
var fileEndsWith = (value) => ({ kind: "file", value, mode: "endsWith" });
|
|
1648
|
+
var fileIncludes = (value) => ({ kind: "file", value, mode: "includes" });
|
|
1649
|
+
var fileEquals = (value) => ({ kind: "file", value, mode: "equals" });
|
|
1650
|
+
var fileStartsWith = (value) => ({ kind: "file", value, mode: "startsWith" });
|
|
1651
|
+
var content = (value) => ({ kind: "content", value });
|
|
1652
|
+
var STACK_BUCKETS = [
|
|
1653
|
+
"backend",
|
|
1654
|
+
"frontend",
|
|
1655
|
+
"databases",
|
|
1656
|
+
"testing",
|
|
1657
|
+
"auth",
|
|
1658
|
+
"tooling",
|
|
1659
|
+
"packageManagers"
|
|
1450
1660
|
];
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1661
|
+
function matchSignal(sig, ctx) {
|
|
1662
|
+
switch (sig.kind) {
|
|
1663
|
+
case "dep":
|
|
1664
|
+
return ctx.deps.has(sig.value);
|
|
1665
|
+
case "depPrefix":
|
|
1666
|
+
for (const d of ctx.deps) if (d.startsWith(sig.value)) return true;
|
|
1667
|
+
return false;
|
|
1668
|
+
case "depAbsent":
|
|
1669
|
+
return !ctx.deps.has(sig.value);
|
|
1670
|
+
case "content":
|
|
1671
|
+
return ctx.contentBlob.includes(sig.value);
|
|
1672
|
+
case "file":
|
|
1673
|
+
return ctx.files.some((f) => {
|
|
1674
|
+
switch (sig.mode) {
|
|
1675
|
+
case "endsWith":
|
|
1676
|
+
return f.endsWith(sig.value);
|
|
1677
|
+
case "includes":
|
|
1678
|
+
return f.includes(sig.value);
|
|
1679
|
+
case "equals":
|
|
1680
|
+
return f === sig.value;
|
|
1681
|
+
case "startsWith":
|
|
1682
|
+
return f.startsWith(sig.value);
|
|
1683
|
+
}
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
function matchRule(rule, ctx) {
|
|
1688
|
+
if (rule.all) return rule.all.every((s) => matchSignal(s, ctx));
|
|
1689
|
+
if (rule.any) return rule.any.some((s) => matchSignal(s, ctx));
|
|
1690
|
+
return false;
|
|
1691
|
+
}
|
|
1692
|
+
var ROLE_RULES = [
|
|
1693
|
+
{ role: "next-app", any: [dep("next"), fileIncludes("next.config.")] },
|
|
1694
|
+
{ role: "react-app", any: [dep("react")] },
|
|
1695
|
+
{ role: "vite-app", any: [dep("vite"), fileIncludes("vite.config.")] },
|
|
1696
|
+
{ role: "react-router-app", all: [dep("react-router"), dep("@react-router/node")] },
|
|
1697
|
+
{ role: "sanity-studio", any: [dep("sanity")] },
|
|
1698
|
+
{ role: "strapi-app", any: [dep("@strapi/strapi"), depPrefix("@strapi/")] },
|
|
1699
|
+
{ role: "expo-app", any: [dep("expo")] },
|
|
1700
|
+
{ role: "vendure-app", any: [dep("@vendure/core")] },
|
|
1701
|
+
{ role: "vendure-plugin", any: [depPrefix("@haus/vendure-"), fileIncludes("vendure-config")] },
|
|
1702
|
+
{ role: "nestjs-api", any: [dep("@nestjs/core")] },
|
|
1703
|
+
{ role: "graphql-api", any: [dep("graphql"), dep("@nestjs/graphql")] },
|
|
1704
|
+
{ role: "nx-monorepo", any: [fileEndsWith("nx.json")] },
|
|
1705
|
+
{ role: "turbo-monorepo", any: [fileEndsWith("turbo.json")] },
|
|
1706
|
+
{ role: "laravel-app", any: [fileEndsWith("artisan"), dep("laravel/framework")] },
|
|
1707
|
+
{ role: "laravel-nova-app", any: [dep("laravel/nova")] },
|
|
1708
|
+
{ role: "dotnet-service", any: [fileEndsWith(".csproj"), fileEndsWith(".sln")] },
|
|
1709
|
+
{ role: "express-service", any: [dep("express")] }
|
|
1467
1710
|
];
|
|
1711
|
+
var STACK_RULES = [
|
|
1712
|
+
{ stack: ["frontend", "nextjs"], any: [dep("next")] },
|
|
1713
|
+
{ stack: ["frontend", "react19"], any: [dep("react")] },
|
|
1714
|
+
{ stack: ["frontend", "vue"], any: [dep("vue")] },
|
|
1715
|
+
{ stack: ["frontend", "vite8"], any: [dep("vite")] },
|
|
1716
|
+
{ stack: ["frontend", "react-router-v7"], all: [dep("react-router"), dep("@react-router/node")] },
|
|
1717
|
+
{ stack: ["frontend", "tailwindcss"], any: [dep("tailwindcss"), fileIncludes("tailwind.config.")] },
|
|
1718
|
+
{
|
|
1719
|
+
stack: ["frontend", "shadcn"],
|
|
1720
|
+
all: [fileEndsWith("components.json"), dep("class-variance-authority")]
|
|
1721
|
+
},
|
|
1722
|
+
{ stack: ["tooling", "typescript5"], any: [dep("typescript")] },
|
|
1723
|
+
{ stack: ["backend", "sanity"], any: [dep("sanity"), dep("next-sanity"), dep("@sanity/client")] },
|
|
1724
|
+
{ stack: ["backend", "strapi"], any: [dep("@strapi/strapi"), depPrefix("@strapi/")] },
|
|
1725
|
+
{ stack: ["backend", "prisma"], any: [dep("prisma"), dep("@prisma/client")] },
|
|
1726
|
+
{ stack: ["frontend", "expo"], any: [dep("expo")] },
|
|
1727
|
+
{ stack: ["frontend", "react-native"], any: [dep("react-native")] },
|
|
1728
|
+
{ stack: ["tooling", "i18next"], any: [dep("i18next"), dep("react-i18next")] },
|
|
1729
|
+
{ stack: ["tooling", "bullmq"], any: [dep("bullmq")] },
|
|
1730
|
+
{ stack: ["tooling", "docker"], any: [fileEquals("Dockerfile"), fileStartsWith("docker-compose")] },
|
|
1731
|
+
{ stack: ["tooling", "pm2"], any: [dep("pm2"), fileIncludes("ecosystem.config")] },
|
|
1732
|
+
{ stack: ["tooling", "sentry"], any: [depPrefix("@sentry/")] },
|
|
1733
|
+
{ stack: ["tooling", "deployer-php"], any: [dep("deployer/deployer")] },
|
|
1734
|
+
{ stack: ["tooling", "missing-prettier"], any: [depAbsent("prettier")] },
|
|
1735
|
+
{ stack: ["tooling", "missing-eslint"], any: [depAbsent("eslint")] },
|
|
1736
|
+
{
|
|
1737
|
+
stack: ["tooling", "stripe"],
|
|
1738
|
+
any: [dep("@stripe/stripe-js"), dep("@stripe/react-stripe-js")]
|
|
1739
|
+
},
|
|
1740
|
+
{ stack: ["tooling", "qliro"], any: [dep("@haus-tech/qliro-plugin")] },
|
|
1741
|
+
{
|
|
1742
|
+
stack: ["databases", "supabase"],
|
|
1743
|
+
any: [dep("@supabase/supabase-js"), depPrefix("@supabase/")]
|
|
1744
|
+
},
|
|
1745
|
+
{ stack: ["backend", "vendure3"], any: [dep("@vendure/core")] },
|
|
1746
|
+
{ stack: ["backend", "nestjs"], any: [dep("@nestjs/core")] },
|
|
1747
|
+
{ stack: ["backend", "nestjs"], any: [content("NestFactory")] },
|
|
1748
|
+
{ stack: ["backend", "vendure3"], any: [content("@VendurePlugin")] },
|
|
1749
|
+
{ stack: ["backend", "graphql"], any: [dep("graphql"), dep("@nestjs/graphql")] },
|
|
1750
|
+
{ stack: ["backend", "graphql"], any: [fileEndsWith(".graphql"), fileEndsWith("schema.graphql")] },
|
|
1751
|
+
{ stack: ["backend", "laravel"], any: [dep("laravel/framework")] },
|
|
1752
|
+
{ stack: ["backend", "laravel"], any: [fileIncludes("app/Providers/"), fileIncludes("routes/")] },
|
|
1753
|
+
{ stack: ["backend", "wordpress"], any: [fileEndsWith("wp-config.php"), dep("roots/wordpress")] },
|
|
1754
|
+
{
|
|
1755
|
+
stack: ["backend", "elementor"],
|
|
1756
|
+
any: [
|
|
1757
|
+
dep("wpackagist-plugin/elementor"),
|
|
1758
|
+
dep("wearehaus/elementor-pro"),
|
|
1759
|
+
dep("wpackagist-theme/hello-elementor")
|
|
1760
|
+
]
|
|
1761
|
+
},
|
|
1762
|
+
{
|
|
1763
|
+
stack: ["backend", "acf-pro"],
|
|
1764
|
+
any: [
|
|
1765
|
+
dep("wearehaus/advanced-custom-fields-pro"),
|
|
1766
|
+
dep("wpackagist-plugin/advanced-custom-fields")
|
|
1767
|
+
]
|
|
1768
|
+
},
|
|
1769
|
+
{ stack: ["backend", "jetengine"], any: [dep("wearehaus/jet-engine")] },
|
|
1770
|
+
{ stack: ["backend", "jetsmartfilters"], any: [dep("wearehaus/jet-smart-filters")] },
|
|
1771
|
+
{ stack: ["backend", "gravityforms"], any: [dep("wearehaus/gravityforms")] },
|
|
1772
|
+
{ stack: ["backend", "dotnet"], any: [fileEndsWith(".csproj"), fileEndsWith(".sln")] },
|
|
1773
|
+
{ stack: ["testing", "playwright"], any: [dep("@playwright/test")] },
|
|
1774
|
+
{ stack: ["testing", "storybook"], any: [fileIncludes(".storybook")] },
|
|
1775
|
+
{ stack: ["testing", "testing-library"], any: [depPrefix("@testing-library/")] },
|
|
1776
|
+
{ stack: ["testing", "phpunit"], any: [fileEndsWith("phpunit.xml")] },
|
|
1777
|
+
{ stack: ["testing", "storybook"], any: [depPrefix("@storybook/")] },
|
|
1778
|
+
{ stack: ["testing", "vitest"], any: [dep("vitest")] },
|
|
1779
|
+
{ stack: ["testing", "jest"], any: [dep("jest"), dep("jest-environment-jsdom")] },
|
|
1780
|
+
{ stack: ["databases", "postgresql"], any: [dep("pg")] },
|
|
1781
|
+
{ stack: ["databases", "mariadb"], any: [dep("mariadb"), dep("mysql2")] },
|
|
1782
|
+
{ stack: ["databases", "mysql"], any: [dep("mysql"), dep("mysql2")] },
|
|
1783
|
+
{ stack: ["databases", "mssql"], any: [dep("mssql")] },
|
|
1784
|
+
{ stack: ["databases", "elasticsearch"], any: [dep("@elastic/elasticsearch")] },
|
|
1785
|
+
{ stack: ["databases", "redis"], any: [dep("predis/predis"), dep("ioredis"), dep("redis")] },
|
|
1786
|
+
{ stack: ["auth", "oidc"], any: [content("openid")] },
|
|
1787
|
+
{ stack: ["auth", "azure-ad"], any: [content("AZURE_AD")] },
|
|
1788
|
+
{ stack: ["auth", "bankid"], any: [content("BANKID")] },
|
|
1789
|
+
{
|
|
1790
|
+
stack: ["auth", "saml2"],
|
|
1791
|
+
any: [dep("24slides/laravel-saml2"), dep("aacotroneo/laravel-saml2")]
|
|
1792
|
+
},
|
|
1793
|
+
{ stack: ["auth", "next-auth"], any: [dep("next-auth"), dep("@auth/core")] }
|
|
1794
|
+
];
|
|
1795
|
+
function runDetection(ctx, rules = STACK_RULES) {
|
|
1796
|
+
const roles = [];
|
|
1797
|
+
for (const rule of ROLE_RULES) {
|
|
1798
|
+
if (rule.role && matchRule(rule, ctx) && !roles.includes(rule.role)) roles.push(rule.role);
|
|
1799
|
+
}
|
|
1800
|
+
const stacks = {};
|
|
1801
|
+
for (const bucket of STACK_BUCKETS) stacks[bucket] = [];
|
|
1802
|
+
for (const rule of rules) {
|
|
1803
|
+
if (!rule.stack || !matchRule(rule, ctx)) continue;
|
|
1804
|
+
const [bucket, name] = rule.stack;
|
|
1805
|
+
stacks[bucket] ??= [];
|
|
1806
|
+
if (!stacks[bucket].includes(name)) stacks[bucket].push(name);
|
|
1807
|
+
}
|
|
1808
|
+
return { roles, stacks };
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// src/scanner/detection.ts
|
|
1812
|
+
import path16 from "path";
|
|
1813
|
+
var UNSUPPORTED_MARKERS = {
|
|
1814
|
+
"requirements.txt": "python",
|
|
1815
|
+
"pyproject.toml": "python",
|
|
1816
|
+
"go.mod": "go",
|
|
1817
|
+
"Cargo.toml": "rust",
|
|
1818
|
+
"pom.xml": "java",
|
|
1819
|
+
"build.gradle": "java",
|
|
1820
|
+
"build.gradle.kts": "java",
|
|
1821
|
+
Gemfile: "ruby"
|
|
1822
|
+
};
|
|
1823
|
+
var WEAK_STACK_SIGNALS = /* @__PURE__ */ new Set(["missing-prettier", "missing-eslint"]);
|
|
1824
|
+
function computeDetectionStatus(roles, stacks, unsupportedSignals) {
|
|
1825
|
+
const hasRealStack = Object.entries(stacks).some(
|
|
1826
|
+
([bucket, names]) => bucket !== "packageManagers" && names.some((n) => !WEAK_STACK_SIGNALS.has(n))
|
|
1827
|
+
);
|
|
1828
|
+
const hasRealSignal = roles.length > 0 || hasRealStack;
|
|
1829
|
+
if (!hasRealSignal) return "unknown";
|
|
1830
|
+
return unsupportedSignals.length > 0 ? "partial" : "supported";
|
|
1831
|
+
}
|
|
1468
1832
|
function blocked(rel) {
|
|
1469
|
-
return
|
|
1833
|
+
return SENSITIVE_PATH_REGEXES.some((x) => x.test(rel));
|
|
1834
|
+
}
|
|
1835
|
+
function dependencySet(pkg, composer) {
|
|
1836
|
+
const depNames = /* @__PURE__ */ new Set();
|
|
1837
|
+
const pushObj = (obj) => {
|
|
1838
|
+
if (!isRecord(obj)) return;
|
|
1839
|
+
for (const key of Object.keys(obj)) depNames.add(key);
|
|
1840
|
+
};
|
|
1841
|
+
pushObj(pkg?.dependencies);
|
|
1842
|
+
pushObj(pkg?.devDependencies);
|
|
1843
|
+
pushObj(composer?.require);
|
|
1844
|
+
pushObj(composer?.["require-dev"]);
|
|
1845
|
+
return [...depNames].sort();
|
|
1846
|
+
}
|
|
1847
|
+
function finalizeRoles(registryRoles, deps, files) {
|
|
1848
|
+
const roles = new Set(registryRoles);
|
|
1849
|
+
const hasWpConfig = files.some((f) => f.endsWith("wp-config.php"));
|
|
1850
|
+
const hasBedrockLayout = files.some((f) => f.includes("web/app")) || deps.includes("roots/wordpress");
|
|
1851
|
+
if (hasWpConfig && hasBedrockLayout) {
|
|
1852
|
+
roles.add("wordpress-bedrock-site");
|
|
1853
|
+
roles.add("wordpress-site");
|
|
1854
|
+
} else if (hasWpConfig) {
|
|
1855
|
+
roles.add("wordpress-vanilla-site");
|
|
1856
|
+
roles.add("wordpress-site");
|
|
1857
|
+
} else if (deps.includes("roots/wordpress")) {
|
|
1858
|
+
roles.add("wordpress-bedrock-site");
|
|
1859
|
+
roles.add("wordpress-site");
|
|
1860
|
+
}
|
|
1861
|
+
return [...roles].sort();
|
|
1862
|
+
}
|
|
1863
|
+
function collectUnsupportedSignals(files) {
|
|
1864
|
+
return [
|
|
1865
|
+
...new Set(
|
|
1866
|
+
files.map((f) => UNSUPPORTED_MARKERS[path16.basename(f)]).filter((s) => Boolean(s))
|
|
1867
|
+
)
|
|
1868
|
+
].sort();
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// src/scanner/render.ts
|
|
1872
|
+
import { readFile } from "fs/promises";
|
|
1873
|
+
import path17 from "path";
|
|
1874
|
+
|
|
1875
|
+
// src/scanner/role-labels.ts
|
|
1876
|
+
var ROLE_LABELS = {
|
|
1877
|
+
"next-app": "a Next.js app",
|
|
1878
|
+
"react-app": "a React app",
|
|
1879
|
+
"vite-app": "a Vite app",
|
|
1880
|
+
"react-router-app": "a React Router app",
|
|
1881
|
+
"sanity-studio": "a Sanity Studio",
|
|
1882
|
+
"strapi-app": "a Strapi app",
|
|
1883
|
+
"expo-app": "an Expo app",
|
|
1884
|
+
"vendure-app": "a Vendure server",
|
|
1885
|
+
"vendure-plugin": "a Vendure plugin",
|
|
1886
|
+
"nestjs-api": "a NestJS API",
|
|
1887
|
+
"graphql-api": "a GraphQL API",
|
|
1888
|
+
"nx-monorepo": "an Nx monorepo",
|
|
1889
|
+
"turbo-monorepo": "a Turborepo monorepo",
|
|
1890
|
+
"laravel-app": "a Laravel app",
|
|
1891
|
+
"laravel-nova-app": "a Laravel Nova app",
|
|
1892
|
+
"dotnet-service": "a .NET service",
|
|
1893
|
+
"express-service": "an Express service",
|
|
1894
|
+
"wordpress-bedrock-site": "a WordPress (Bedrock) site",
|
|
1895
|
+
"wordpress-vanilla-site": "a WordPress site",
|
|
1896
|
+
"wordpress-site": "a WordPress site"
|
|
1897
|
+
};
|
|
1898
|
+
function article(word) {
|
|
1899
|
+
return /^[aeiou]/i.test(word) ? "an" : "a";
|
|
1900
|
+
}
|
|
1901
|
+
function friendlyRole(role) {
|
|
1902
|
+
const known = ROLE_LABELS[role];
|
|
1903
|
+
if (known) return known;
|
|
1904
|
+
const words = role.replace(/[-_]+/g, " ").trim();
|
|
1905
|
+
return words ? `${article(words)} ${words}` : "a project";
|
|
1906
|
+
}
|
|
1907
|
+
function joinRoles(labels) {
|
|
1908
|
+
if (labels.length === 0) return "";
|
|
1909
|
+
if (labels.length === 1) return labels[0];
|
|
1910
|
+
return `${labels.slice(0, -1).join(", ")} and ${labels[labels.length - 1]}`;
|
|
1911
|
+
}
|
|
1912
|
+
function describeRepo(context) {
|
|
1913
|
+
const labels = context.repoRoles.map(friendlyRole);
|
|
1914
|
+
const roleText = joinRoles(labels);
|
|
1915
|
+
if (context.detectionStatus === "unknown") {
|
|
1916
|
+
const markers = context.unsupportedSignals.join(", ");
|
|
1917
|
+
const detail = markers ? ` (I see ${markers})` : "";
|
|
1918
|
+
return `I couldn't fully recognise this stack${detail}, so I'll apply the general workflow and security guidance rather than framework-specific help.`;
|
|
1919
|
+
}
|
|
1920
|
+
const base = roleText ? `This looks like ${roleText}, using ${context.packageManager}.` : `I recognised this project's tooling (${context.packageManager}) but not a specific framework.`;
|
|
1921
|
+
if (context.detectionStatus === "partial" && context.unsupportedSignals.length > 0) {
|
|
1922
|
+
return `${base} I also see ${context.unsupportedSignals.join(", ")}, which haus doesn't fully support \u2014 guidance covers the recognised parts.`;
|
|
1923
|
+
}
|
|
1924
|
+
return base;
|
|
1470
1925
|
}
|
|
1926
|
+
|
|
1927
|
+
// src/scanner/render.ts
|
|
1928
|
+
async function buildContentBlob(root, files) {
|
|
1929
|
+
const candidates = files.filter(
|
|
1930
|
+
(f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
|
|
1931
|
+
);
|
|
1932
|
+
const slice = candidates.slice(0, 300);
|
|
1933
|
+
const CHUNK = 24;
|
|
1934
|
+
const parts = [];
|
|
1935
|
+
for (let i = 0; i < slice.length; i += CHUNK) {
|
|
1936
|
+
const batch = await Promise.all(
|
|
1937
|
+
slice.slice(i, i + CHUNK).map(async (rel) => {
|
|
1938
|
+
try {
|
|
1939
|
+
return await readFile(path17.join(root, rel), "utf8");
|
|
1940
|
+
} catch {
|
|
1941
|
+
return "";
|
|
1942
|
+
}
|
|
1943
|
+
})
|
|
1944
|
+
);
|
|
1945
|
+
parts.push(...batch);
|
|
1946
|
+
}
|
|
1947
|
+
return parts.join("\n");
|
|
1948
|
+
}
|
|
1949
|
+
function computeConfidence(roles, stacks) {
|
|
1950
|
+
const stackCount = Object.values(stacks).reduce((sum, arr) => sum + arr.length, 0);
|
|
1951
|
+
if (roles.length === 0) return 0.15;
|
|
1952
|
+
return Math.min(0.99, Number((0.4 + roles.length * 0.08 + stackCount * 0.02).toFixed(2)));
|
|
1953
|
+
}
|
|
1954
|
+
function renderSummary(context) {
|
|
1955
|
+
return `# Repo summary
|
|
1956
|
+
|
|
1957
|
+
${describeRepo(context)}
|
|
1958
|
+
|
|
1959
|
+
- Repo: ${context.repoName}
|
|
1960
|
+
- Package manager: ${context.packageManager}
|
|
1961
|
+
- Roles: ${context.repoRoles.join(", ") || "unknown"}
|
|
1962
|
+
- Generated: ${context.generatedAt}
|
|
1963
|
+
`;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
// src/scanner/scan-project.ts
|
|
1967
|
+
var SAFE_FILES = [
|
|
1968
|
+
"package.json",
|
|
1969
|
+
"yarn.lock",
|
|
1970
|
+
"pnpm-lock.yaml",
|
|
1971
|
+
"composer.json",
|
|
1972
|
+
"composer.lock",
|
|
1973
|
+
"nx.json",
|
|
1974
|
+
"turbo.json",
|
|
1975
|
+
"tsconfig.json",
|
|
1976
|
+
"vite.config.*",
|
|
1977
|
+
"next.config.*",
|
|
1978
|
+
"tailwind.config.*",
|
|
1979
|
+
"components.json",
|
|
1980
|
+
"playwright.config.*",
|
|
1981
|
+
"phpunit.xml",
|
|
1982
|
+
"artisan",
|
|
1983
|
+
"routes/*.php",
|
|
1984
|
+
"app/Providers/*.php",
|
|
1985
|
+
"schema.graphql",
|
|
1986
|
+
"**/*.graphql",
|
|
1987
|
+
"**/vendure-config.*",
|
|
1988
|
+
"**/*module.ts",
|
|
1989
|
+
"web/app/**",
|
|
1990
|
+
"wp-content/plugins/**",
|
|
1991
|
+
"wp-content/themes/**",
|
|
1992
|
+
"wp-content/mu-plugins/**",
|
|
1993
|
+
"wp-content/acf-json/**",
|
|
1994
|
+
".storybook/**",
|
|
1995
|
+
".env.example",
|
|
1996
|
+
"wp-config.php",
|
|
1997
|
+
"**/*.csproj",
|
|
1998
|
+
"**/*.sln",
|
|
1999
|
+
"docker-compose.*",
|
|
2000
|
+
"Dockerfile",
|
|
2001
|
+
// Unsupported-ecosystem markers — matched by PRESENCE only (never content-read; none
|
|
2002
|
+
// match the content-blob extensions). Drive detectionStatus / unsupportedSignals.
|
|
2003
|
+
"requirements.txt",
|
|
2004
|
+
"pyproject.toml",
|
|
2005
|
+
"go.mod",
|
|
2006
|
+
"Cargo.toml",
|
|
2007
|
+
"pom.xml",
|
|
2008
|
+
"build.gradle",
|
|
2009
|
+
"build.gradle.kts",
|
|
2010
|
+
"Gemfile"
|
|
2011
|
+
];
|
|
1471
2012
|
async function scanProject(root, mode = "fast") {
|
|
1472
|
-
const pkg = await readJson(
|
|
1473
|
-
const composer = await readJson(
|
|
2013
|
+
const pkg = await readJson(path18.join(root, "package.json"));
|
|
2014
|
+
const composer = await readJson(path18.join(root, "composer.json"));
|
|
1474
2015
|
const files = await listFiles(root, SAFE_FILES);
|
|
1475
2016
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
1476
2017
|
const deps = dependencySet(pkg, composer);
|
|
1477
2018
|
const packageManager = detectPackageManager(root, String(pkg?.packageManager ?? ""));
|
|
1478
|
-
const
|
|
1479
|
-
const
|
|
2019
|
+
const contentBlob = await buildContentBlob(root, safeFiles);
|
|
2020
|
+
const detection = runDetection({ deps: new Set(deps), files: safeFiles, contentBlob });
|
|
2021
|
+
const roles = finalizeRoles(detection.roles, deps, safeFiles);
|
|
2022
|
+
const stacks = detection.stacks;
|
|
2023
|
+
if (packageManager === "yarn") stacks.packageManagers.push("yarn4");
|
|
2024
|
+
if (packageManager === "pnpm") stacks.packageManagers.push("pnpm89");
|
|
1480
2025
|
const warnings = [];
|
|
1481
2026
|
const securityRisks = [];
|
|
1482
2027
|
const crossRepoHints = [];
|
|
@@ -1494,11 +2039,13 @@ async function scanProject(root, mode = "fast") {
|
|
|
1494
2039
|
if (!safeFiles.some((f) => f.endsWith(".env.example"))) securityRisks.push("Missing env template");
|
|
1495
2040
|
if (safeFiles.some((f) => f.includes("wp-content/uploads")))
|
|
1496
2041
|
securityRisks.push("Uploads directory present");
|
|
2042
|
+
const unsupportedSignals = collectUnsupportedSignals(safeFiles);
|
|
2043
|
+
const detectionStatus = computeDetectionStatus(roles, stacks, unsupportedSignals);
|
|
1497
2044
|
const context = {
|
|
1498
2045
|
mode,
|
|
1499
2046
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1500
2047
|
root,
|
|
1501
|
-
repoName: String(pkg?.name ??
|
|
2048
|
+
repoName: String(pkg?.name ?? path18.basename(root)),
|
|
1502
2049
|
packageManager,
|
|
1503
2050
|
repoRoles: roles,
|
|
1504
2051
|
confidence: computeConfidence(roles, stacks),
|
|
@@ -1506,7 +2053,9 @@ async function scanProject(root, mode = "fast") {
|
|
|
1506
2053
|
dependencies: deps,
|
|
1507
2054
|
securityRisks,
|
|
1508
2055
|
crossRepoHints,
|
|
1509
|
-
warnings
|
|
2056
|
+
warnings,
|
|
2057
|
+
detectionStatus,
|
|
2058
|
+
unsupportedSignals
|
|
1510
2059
|
};
|
|
1511
2060
|
const dependencyMap = {
|
|
1512
2061
|
node: deps.filter((d) => !d.includes("/")),
|
|
@@ -1515,7 +2064,7 @@ async function scanProject(root, mode = "fast") {
|
|
|
1515
2064
|
const scanHashes = Object.fromEntries(
|
|
1516
2065
|
await Promise.all(
|
|
1517
2066
|
safeFiles.map(
|
|
1518
|
-
async (f) => [f, hashText(await
|
|
2067
|
+
async (f) => [f, hashText(await readFile2(path18.join(root, f), "utf8"))]
|
|
1519
2068
|
)
|
|
1520
2069
|
)
|
|
1521
2070
|
);
|
|
@@ -1526,184 +2075,6 @@ async function scanProject(root, mode = "fast") {
|
|
|
1526
2075
|
await writeText(hausPath(root, "repo-summary.md"), repoSummary);
|
|
1527
2076
|
return { ...context, dependencyMap, scanHashes, repoSummary };
|
|
1528
2077
|
}
|
|
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
2078
|
|
|
1708
2079
|
// src/scanner/read-context.ts
|
|
1709
2080
|
async function readContextOrScan(root) {
|
|
@@ -1727,7 +2098,9 @@ async function runContext(options) {
|
|
|
1727
2098
|
(recommendationRaw?.recommended ?? []).map((item) => [item.id, item.scoreBreakdown])
|
|
1728
2099
|
);
|
|
1729
2100
|
const taskIntents = options.task ? classifyTaskIntents(options.task) : /* @__PURE__ */ new Set();
|
|
1730
|
-
const selected = pickTaskRelevantRules(recommendation, options.task, taskIntents
|
|
2101
|
+
const selected = pickTaskRelevantRules(recommendation, options.task, taskIntents, {
|
|
2102
|
+
tokenBudget: DEFAULT_CONTEXT_TOKEN_BUDGET
|
|
2103
|
+
});
|
|
1731
2104
|
const payload = {
|
|
1732
2105
|
task: options.task ?? "not provided",
|
|
1733
2106
|
taskIntents: [...taskIntents].sort(),
|
|
@@ -1775,8 +2148,8 @@ async function runContext(options) {
|
|
|
1775
2148
|
}
|
|
1776
2149
|
|
|
1777
2150
|
// src/commands/doctor.ts
|
|
1778
|
-
import
|
|
1779
|
-
import
|
|
2151
|
+
import path19 from "path";
|
|
2152
|
+
import fs12 from "fs-extra";
|
|
1780
2153
|
|
|
1781
2154
|
// src/update/npm-version.ts
|
|
1782
2155
|
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
@@ -1823,114 +2196,200 @@ async function runDoctor(options) {
|
|
|
1823
2196
|
const recommendation = await readJson(
|
|
1824
2197
|
hausPath(root, "recommendation.json")
|
|
1825
2198
|
);
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
2199
|
+
const detail = [];
|
|
2200
|
+
const attention = [];
|
|
2201
|
+
const ok = (text) => detail.push({ stream: "log", text });
|
|
2202
|
+
const flag = (text, sentence, fix) => {
|
|
2203
|
+
detail.push({ stream: "warn", text });
|
|
2204
|
+
attention.push({ sentence, fix });
|
|
2205
|
+
};
|
|
2206
|
+
ok(`Repo: ${context.repoName}`);
|
|
2207
|
+
ok(`Roles: ${context.repoRoles.join(", ") || "unknown"}`);
|
|
2208
|
+
ok(`Package manager: ${context.packageManager}`);
|
|
2209
|
+
ok(`Recommended items: ${recommendation?.recommended?.length ?? 0}`);
|
|
1831
2210
|
const warningLines = [.../* @__PURE__ */ new Set([...context.warnings, ...recommendation?.warnings ?? []])];
|
|
1832
2211
|
for (const warning of warningLines) {
|
|
1833
|
-
|
|
2212
|
+
ok(`- WARN: ${warning}`);
|
|
1834
2213
|
}
|
|
1835
2214
|
const hooks = await verifyProjectSettingsHooksContract(root);
|
|
1836
2215
|
if (hooks.skipped) {
|
|
1837
|
-
|
|
2216
|
+
ok(`- HOOKS: (skipped) ${hooks.message}`);
|
|
1838
2217
|
} else if (!hooks.ok) {
|
|
1839
|
-
|
|
2218
|
+
flag(
|
|
2219
|
+
`- HOOKS FAIL: ${hooks.message}`,
|
|
2220
|
+
"The Claude Code hooks don't match what haus expects",
|
|
2221
|
+
"haus apply --write"
|
|
2222
|
+
);
|
|
1840
2223
|
process.exitCode = 1;
|
|
1841
2224
|
} else {
|
|
1842
|
-
|
|
2225
|
+
ok(`- HOOKS OK: ${hooks.message}`);
|
|
1843
2226
|
}
|
|
1844
|
-
const gatedHooks = ["context"
|
|
2227
|
+
const gatedHooks = ["context"];
|
|
1845
2228
|
for (const key of gatedHooks) {
|
|
1846
2229
|
const enabled = await isHookEnabled(root, key);
|
|
1847
|
-
|
|
2230
|
+
ok(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
|
|
1848
2231
|
}
|
|
1849
|
-
const rootClaudeMdPath =
|
|
2232
|
+
const rootClaudeMdPath = path19.join(root, "CLAUDE.md");
|
|
1850
2233
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
1851
2234
|
if (!rootClaudeMdContent) {
|
|
1852
|
-
|
|
2235
|
+
flag(
|
|
2236
|
+
"- CLAUDE.md: missing (run `haus apply --write` to create)",
|
|
2237
|
+
"Your project's CLAUDE.md is missing, so haus guidance never loads",
|
|
2238
|
+
"haus apply --write"
|
|
2239
|
+
);
|
|
1853
2240
|
} else if (!rootClaudeMdContent.includes(BLOCK_BEGIN)) {
|
|
1854
|
-
|
|
2241
|
+
flag(
|
|
2242
|
+
"- CLAUDE.md: haus import block missing (run `haus apply --write` to add)",
|
|
2243
|
+
"The haus import block is missing from CLAUDE.md, so its guidance never loads",
|
|
2244
|
+
"haus apply --write"
|
|
2245
|
+
);
|
|
1855
2246
|
} else {
|
|
1856
|
-
|
|
2247
|
+
const beginIdx = rootClaudeMdContent.indexOf(BLOCK_BEGIN);
|
|
2248
|
+
const endIdx = rootClaudeMdContent.indexOf(BLOCK_END, beginIdx + BLOCK_BEGIN.length);
|
|
2249
|
+
if (endIdx < 0) {
|
|
2250
|
+
flag(
|
|
2251
|
+
"- CLAUDE.md: haus import block is not closed (run `haus apply --write` to repair)",
|
|
2252
|
+
"The haus import block in CLAUDE.md is broken, so its guidance may not load",
|
|
2253
|
+
"haus apply --write"
|
|
2254
|
+
);
|
|
2255
|
+
} else {
|
|
2256
|
+
ok("- CLAUDE.md: import block present");
|
|
2257
|
+
const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
|
|
2258
|
+
const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
|
|
2259
|
+
for (const target of importTargets) {
|
|
2260
|
+
if (!await fs12.pathExists(hausPath(root, target))) {
|
|
2261
|
+
flag(
|
|
2262
|
+
`- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
|
|
2263
|
+
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
2264
|
+
"haus apply --write"
|
|
2265
|
+
);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
1857
2269
|
}
|
|
1858
2270
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
1859
|
-
const workflowExists = await
|
|
2271
|
+
const workflowExists = await fs12.pathExists(workflowPath);
|
|
1860
2272
|
if (!workflowExists) {
|
|
1861
|
-
|
|
2273
|
+
flag(
|
|
2274
|
+
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
2275
|
+
"The workflow standard file is missing",
|
|
2276
|
+
"haus apply --write"
|
|
2277
|
+
);
|
|
1862
2278
|
} else {
|
|
1863
2279
|
const workflowContent = await readText(workflowPath);
|
|
1864
2280
|
const firstLine = workflowContent?.split("\n")[0] ?? "";
|
|
1865
2281
|
if (!firstLine.includes("HAUS-MANAGED")) {
|
|
1866
|
-
|
|
2282
|
+
ok("- .haus-workflow/WORKFLOW.md: OK (user-owned)");
|
|
1867
2283
|
} else {
|
|
1868
2284
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
1869
|
-
const cachePath =
|
|
1870
|
-
const bundledPath =
|
|
2285
|
+
const cachePath = path19.join(CACHE_DIR, "templates/agentic-workflow-standard.md");
|
|
2286
|
+
const bundledPath = path19.join(
|
|
1871
2287
|
packageRoot(),
|
|
1872
2288
|
"library",
|
|
1873
2289
|
"global",
|
|
1874
2290
|
"templates",
|
|
1875
2291
|
"agentic-workflow-standard.md"
|
|
1876
2292
|
);
|
|
1877
|
-
const templatePath = await
|
|
2293
|
+
const templatePath = await fs12.pathExists(cachePath) ? cachePath : bundledPath;
|
|
1878
2294
|
const templateContent = await readText(templatePath);
|
|
1879
2295
|
if (storedHashMatch && templateContent) {
|
|
1880
2296
|
const currentHash = hashText(normaliseLF(templateContent));
|
|
1881
2297
|
if (storedHashMatch[1] !== currentHash) {
|
|
1882
|
-
|
|
2298
|
+
flag(
|
|
2299
|
+
"- .haus-workflow/WORKFLOW.md: stale (template updated \u2014 run `haus apply --write`)",
|
|
2300
|
+
"The workflow standard is out of date",
|
|
2301
|
+
"haus apply --write"
|
|
2302
|
+
);
|
|
1883
2303
|
} else {
|
|
1884
|
-
|
|
2304
|
+
ok("- .haus-workflow/WORKFLOW.md: OK");
|
|
1885
2305
|
}
|
|
1886
2306
|
} else {
|
|
1887
|
-
|
|
2307
|
+
ok("- .haus-workflow/WORKFLOW.md: OK");
|
|
1888
2308
|
}
|
|
1889
2309
|
}
|
|
1890
2310
|
}
|
|
1891
2311
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
1892
|
-
const workflowConfigExists = await
|
|
2312
|
+
const workflowConfigExists = await fs12.pathExists(workflowConfigPath);
|
|
1893
2313
|
if (!workflowConfigExists) {
|
|
1894
|
-
|
|
2314
|
+
flag(
|
|
2315
|
+
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
2316
|
+
"The workflow config file is missing",
|
|
2317
|
+
"haus apply --write"
|
|
2318
|
+
);
|
|
1895
2319
|
} else {
|
|
1896
|
-
|
|
2320
|
+
const cfg = await fs12.readFile(workflowConfigPath, "utf8");
|
|
2321
|
+
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
2322
|
+
if (unfilled > 0) {
|
|
2323
|
+
flag(
|
|
2324
|
+
`- .haus-workflow/workflow-config.md: ${unfilled} field(s) still unfilled (run \`haus apply --refill-config\` to auto-fill detectable ones)`,
|
|
2325
|
+
`${unfilled} workflow-config field(s) are still blank`,
|
|
2326
|
+
"haus apply --refill-config"
|
|
2327
|
+
);
|
|
2328
|
+
} else {
|
|
2329
|
+
ok("- .haus-workflow/workflow-config.md: OK (project-owned)");
|
|
2330
|
+
}
|
|
1897
2331
|
}
|
|
1898
2332
|
const projectMdPath = hausPath(root, "project.md");
|
|
1899
|
-
const projectMdExists = await
|
|
2333
|
+
const projectMdExists = await fs12.pathExists(projectMdPath);
|
|
1900
2334
|
if (!projectMdExists) {
|
|
1901
|
-
|
|
2335
|
+
flag(
|
|
2336
|
+
"- .haus-workflow/project.md: missing (run `haus apply --write`)",
|
|
2337
|
+
"The project facts file is missing",
|
|
2338
|
+
"haus apply --write"
|
|
2339
|
+
);
|
|
1902
2340
|
} else {
|
|
1903
2341
|
const projectMdContent = await readText(projectMdPath);
|
|
1904
2342
|
const hasHeader = projectMdContent?.split("\n")[0]?.includes("HAUS-MANAGED") ?? false;
|
|
1905
2343
|
if (!hasHeader) {
|
|
1906
|
-
|
|
2344
|
+
ok("- .haus-workflow/project.md: no HAUS-MANAGED header (user-owned)");
|
|
1907
2345
|
} else {
|
|
1908
|
-
|
|
2346
|
+
ok("- .haus-workflow/project.md: OK");
|
|
1909
2347
|
}
|
|
1910
2348
|
}
|
|
1911
2349
|
const cacheAgeMs = await getCacheManifestAge();
|
|
1912
2350
|
if (cacheAgeMs === null) {
|
|
1913
|
-
|
|
2351
|
+
flag(
|
|
2352
|
+
"- CATALOG CACHE: absent (run `haus update` to populate)",
|
|
2353
|
+
"The catalog cache hasn't been downloaded yet",
|
|
2354
|
+
"haus update"
|
|
2355
|
+
);
|
|
1914
2356
|
} else {
|
|
1915
2357
|
const cacheAgeDays = Math.floor(cacheAgeMs / (1e3 * 60 * 60 * 24));
|
|
1916
2358
|
if (cacheAgeDays >= 7) {
|
|
1917
|
-
|
|
2359
|
+
flag(
|
|
2360
|
+
`- CATALOG CACHE: stale (${cacheAgeDays}d old \u2014 run \`haus update\`)`,
|
|
2361
|
+
`The catalog cache is ${cacheAgeDays} days old`,
|
|
2362
|
+
"haus update"
|
|
2363
|
+
);
|
|
1918
2364
|
} else {
|
|
1919
|
-
|
|
2365
|
+
ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
|
|
1920
2366
|
}
|
|
1921
2367
|
}
|
|
1922
|
-
const pkgJson = await readJson(
|
|
2368
|
+
const pkgJson = await readJson(path19.join(packageRoot(), "package.json"));
|
|
1923
2369
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
1924
2370
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
1925
2371
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
1926
|
-
|
|
1927
|
-
`- CLI UPDATE: ${currentVersion} \u2192 ${npmStatus.latest} available (run: npm install -g ${NPM_PACKAGE_NAME})
|
|
2372
|
+
flag(
|
|
2373
|
+
`- CLI UPDATE: ${currentVersion} \u2192 ${npmStatus.latest} available (run: npm install -g ${NPM_PACKAGE_NAME})`,
|
|
2374
|
+
`A newer haus (${npmStatus.latest}) is available`,
|
|
2375
|
+
`npm install -g ${NPM_PACKAGE_NAME}`
|
|
1928
2376
|
);
|
|
1929
2377
|
process.exitCode = 1;
|
|
1930
2378
|
} else if (npmStatus.latest !== null) {
|
|
1931
|
-
|
|
2379
|
+
ok(`- CLI: ${currentVersion} (up to date)`);
|
|
1932
2380
|
} else {
|
|
1933
|
-
|
|
2381
|
+
ok(`- CLI: ${currentVersion} (version check unavailable)`);
|
|
2382
|
+
}
|
|
2383
|
+
if (attention.length === 0) {
|
|
2384
|
+
log("\u2705 Your project is set up and healthy.");
|
|
2385
|
+
} else {
|
|
2386
|
+
log(`\u26A0\uFE0F ${attention.length} thing(s) need attention:`);
|
|
2387
|
+
for (const a of attention) log(` \u2022 ${a.sentence} \u2014 fix: ${a.fix}`);
|
|
2388
|
+
}
|
|
2389
|
+
log("Haus Doctor");
|
|
2390
|
+
for (const line2 of detail) {
|
|
2391
|
+
if (line2.stream === "warn") warn(line2.text);
|
|
2392
|
+
else log(line2.text);
|
|
1934
2393
|
}
|
|
1935
2394
|
}
|
|
1936
2395
|
|
|
@@ -1989,57 +2448,17 @@ async function runExplainRecommendation(options) {
|
|
|
1989
2448
|
// src/commands/guard.ts
|
|
1990
2449
|
import { readFileSync as readFileSync2 } from "fs";
|
|
1991
2450
|
|
|
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
2451
|
// src/security/guard-bash.ts
|
|
2010
2452
|
function guardBash(command) {
|
|
2011
2453
|
const matched = DANGEROUS_COMMANDS.find((token) => command.includes(token));
|
|
2012
|
-
if (matched) return `
|
|
2454
|
+
if (matched) return `I didn't run that \u2014 it can permanently change or delete things: ${command}`;
|
|
2013
2455
|
return void 0;
|
|
2014
2456
|
}
|
|
2015
2457
|
|
|
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
2458
|
// src/security/guard-file-access.ts
|
|
2040
2459
|
function guardFileAccess(candidate) {
|
|
2041
2460
|
const matched = SENSITIVE_PATHS.find((token) => candidate.includes(token.replace("*", "")));
|
|
2042
|
-
if (matched) return `
|
|
2461
|
+
if (matched) return `I didn't open ${candidate} \u2014 it looks like it holds secrets or sensitive data`;
|
|
2043
2462
|
return void 0;
|
|
2044
2463
|
}
|
|
2045
2464
|
|
|
@@ -2071,23 +2490,136 @@ async function runGuard(kind, _options) {
|
|
|
2071
2490
|
const toolInput = isRecord(payload.tool_input) ? payload.tool_input : {};
|
|
2072
2491
|
if (kind === "file-access") {
|
|
2073
2492
|
const candidate = String(toolInput.path ?? toolInput.file_path ?? "");
|
|
2074
|
-
|
|
2075
|
-
|
|
2493
|
+
const reason2 = guardFileAccess(candidate);
|
|
2494
|
+
if (reason2) {
|
|
2495
|
+
deny(reason2);
|
|
2076
2496
|
process.exitCode = 1;
|
|
2077
2497
|
return;
|
|
2078
2498
|
}
|
|
2079
2499
|
return;
|
|
2080
2500
|
}
|
|
2081
2501
|
const command = String(toolInput.command ?? "");
|
|
2082
|
-
|
|
2083
|
-
|
|
2502
|
+
const reason = guardBash(command);
|
|
2503
|
+
if (reason) {
|
|
2504
|
+
deny(reason);
|
|
2084
2505
|
process.exitCode = 1;
|
|
2085
2506
|
}
|
|
2086
2507
|
}
|
|
2087
2508
|
|
|
2088
2509
|
// src/commands/init.ts
|
|
2089
|
-
import
|
|
2090
|
-
import
|
|
2510
|
+
import path20 from "path";
|
|
2511
|
+
import fs13 from "fs-extra";
|
|
2512
|
+
|
|
2513
|
+
// src/recommender/ecosystem.ts
|
|
2514
|
+
var ECOSYSTEM_GROUPS = {
|
|
2515
|
+
laravel: ["laravel-app", "laravel-nova-app"],
|
|
2516
|
+
wordpress: ["wordpress-site", "wordpress-bedrock-site", "wordpress-vanilla-site"],
|
|
2517
|
+
vendure: ["vendure-app", "vendure-plugin"],
|
|
2518
|
+
nestjs: ["nestjs-api"],
|
|
2519
|
+
nextjs: ["next-app"],
|
|
2520
|
+
react: ["react-app", "next-app", "design-system"],
|
|
2521
|
+
vue: ["vue-app"],
|
|
2522
|
+
dotnet: ["dotnet-service"],
|
|
2523
|
+
nx: ["nx-monorepo"],
|
|
2524
|
+
turbo: ["turbo-monorepo"]
|
|
2525
|
+
};
|
|
2526
|
+
var ECOSYSTEM_PRIMARY_BACKENDS = /* @__PURE__ */ new Set([
|
|
2527
|
+
"laravel",
|
|
2528
|
+
"wordpress",
|
|
2529
|
+
"vendure",
|
|
2530
|
+
"nestjs",
|
|
2531
|
+
"dotnet"
|
|
2532
|
+
]);
|
|
2533
|
+
var ECOSYSTEM_COMPATIBLE_BACKENDS = {
|
|
2534
|
+
vendure: /* @__PURE__ */ new Set(["vendure", "nestjs"]),
|
|
2535
|
+
nestjs: /* @__PURE__ */ new Set(["nestjs"]),
|
|
2536
|
+
laravel: /* @__PURE__ */ new Set(["laravel"]),
|
|
2537
|
+
wordpress: /* @__PURE__ */ new Set(["wordpress"]),
|
|
2538
|
+
dotnet: /* @__PURE__ */ new Set(["dotnet"])
|
|
2539
|
+
};
|
|
2540
|
+
function inferRepoEcosystems(roles) {
|
|
2541
|
+
const ecosystems = /* @__PURE__ */ new Set();
|
|
2542
|
+
for (const [eco, roleList] of Object.entries(ECOSYSTEM_GROUPS)) {
|
|
2543
|
+
if (roleList.some((r) => roles.includes(r))) ecosystems.add(eco);
|
|
2544
|
+
}
|
|
2545
|
+
return [...ecosystems];
|
|
2546
|
+
}
|
|
2547
|
+
function pickDominantBackend(ecosystems) {
|
|
2548
|
+
for (const eco of ecosystems) {
|
|
2549
|
+
if (ECOSYSTEM_PRIMARY_BACKENDS.has(eco)) return eco;
|
|
2550
|
+
}
|
|
2551
|
+
return void 0;
|
|
2552
|
+
}
|
|
2553
|
+
function isBackendEcosystem(eco) {
|
|
2554
|
+
return ECOSYSTEM_PRIMARY_BACKENDS.has(eco);
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
// src/recommender/policies.ts
|
|
2558
|
+
var UNSUPPORTED = [
|
|
2559
|
+
"python",
|
|
2560
|
+
"django",
|
|
2561
|
+
"go",
|
|
2562
|
+
"rust",
|
|
2563
|
+
"java",
|
|
2564
|
+
"spring",
|
|
2565
|
+
"kotlin",
|
|
2566
|
+
"swift",
|
|
2567
|
+
"android",
|
|
2568
|
+
"flutter",
|
|
2569
|
+
"dart",
|
|
2570
|
+
"c++",
|
|
2571
|
+
"perl",
|
|
2572
|
+
"defi",
|
|
2573
|
+
"trading"
|
|
2574
|
+
];
|
|
2575
|
+
function matchRequiresAny(clauses, ctx) {
|
|
2576
|
+
for (const clause of clauses) {
|
|
2577
|
+
if ("stack" in clause) {
|
|
2578
|
+
if (ctx.stackSet.has(clause.stack.toLowerCase())) {
|
|
2579
|
+
return { matched: true, signal: `stack:${clause.stack}` };
|
|
2580
|
+
}
|
|
2581
|
+
} else if ("dependency" in clause) {
|
|
2582
|
+
if (ctx.depSet.has(clause.dependency.toLowerCase())) {
|
|
2583
|
+
return { matched: true, signal: `dependency:${clause.dependency}` };
|
|
2584
|
+
}
|
|
2585
|
+
} else if ("packageNamePattern" in clause) {
|
|
2586
|
+
const pattern = clause.packageNamePattern.toLowerCase();
|
|
2587
|
+
const prefix = pattern.endsWith("*") ? pattern.slice(0, -1) : pattern;
|
|
2588
|
+
for (const dep2 of ctx.depSet) {
|
|
2589
|
+
if (pattern.endsWith("*") ? dep2.startsWith(prefix) : dep2 === pattern) {
|
|
2590
|
+
return {
|
|
2591
|
+
matched: true,
|
|
2592
|
+
signal: `packageNamePattern:${clause.packageNamePattern}`
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
} else if ("role" in clause) {
|
|
2597
|
+
if (ctx.roleSet.has(clause.role.toLowerCase())) {
|
|
2598
|
+
return { matched: true, signal: `role:${clause.role}` };
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
return { matched: false };
|
|
2603
|
+
}
|
|
2604
|
+
function describeRequiresAny(clauses) {
|
|
2605
|
+
return clauses.map((c) => {
|
|
2606
|
+
if ("stack" in c) return `stack=${c.stack}`;
|
|
2607
|
+
if ("dependency" in c) return `dependency=${c.dependency}`;
|
|
2608
|
+
if ("packageNamePattern" in c) return `packageNamePattern=${c.packageNamePattern}`;
|
|
2609
|
+
if ("role" in c) return `role=${c.role}`;
|
|
2610
|
+
return "unknown";
|
|
2611
|
+
}).join(" | ");
|
|
2612
|
+
}
|
|
2613
|
+
function mergeRecommendationWarnings(context) {
|
|
2614
|
+
const markers = context.unsupportedSignals?.join(", ");
|
|
2615
|
+
const statusLines = context.detectionStatus === "unknown" ? [
|
|
2616
|
+
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."
|
|
2617
|
+
] : context.detectionStatus === "partial" && markers ? [
|
|
2618
|
+
`Partially supported \u2014 found unsupported ${markers} alongside recognised stacks; guidance covers the supported parts only.`
|
|
2619
|
+
] : [];
|
|
2620
|
+
const riskLines = (context.securityRisks?.length ?? 0) > 0 ? [`Scan reported security signals: ${context.securityRisks.join("; ")}`] : [];
|
|
2621
|
+
return [.../* @__PURE__ */ new Set([...statusLines, ...context.warnings, ...riskLines])];
|
|
2622
|
+
}
|
|
2091
2623
|
|
|
2092
2624
|
// src/utils/exec.ts
|
|
2093
2625
|
import { execa } from "execa";
|
|
@@ -2110,49 +2642,43 @@ async function runCommand(command, args = [], options = {}) {
|
|
|
2110
2642
|
throw new Error(`Failed to run command: ${command} ${args.join(" ")} (${message})`);
|
|
2111
2643
|
}
|
|
2112
2644
|
}
|
|
2113
|
-
async function runGit(args, options = {}) {
|
|
2114
|
-
return runCommand("git", args, options);
|
|
2115
|
-
}
|
|
2645
|
+
async function runGit(args, options = {}) {
|
|
2646
|
+
return runCommand("git", args, options);
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
// src/recommender/scoring.ts
|
|
2650
|
+
function computeConfidenceLevel(args) {
|
|
2651
|
+
const { isDefaultBaseline, reasons, hasEcosystemConflict, score } = args;
|
|
2652
|
+
const positiveCodes = new Set(reasons.map((r) => r.code));
|
|
2653
|
+
positiveCodes.delete("default-baseline");
|
|
2654
|
+
const distinctSignals = positiveCodes.size;
|
|
2655
|
+
const strongCount = (positiveCodes.has("repo-role-match") ? 1 : 0) + (positiveCodes.has("stack-match") ? 1 : 0) + (positiveCodes.has("requires-any-match") ? 1 : 0);
|
|
2656
|
+
if (hasEcosystemConflict) return "low";
|
|
2657
|
+
if (isDefaultBaseline && distinctSignals === 0) return "medium";
|
|
2658
|
+
if (strongCount >= 2 && score >= 70) return "high";
|
|
2659
|
+
if (strongCount >= 1 && distinctSignals >= 2 && score >= 50) return "medium";
|
|
2660
|
+
if (distinctSignals === 1) return "low";
|
|
2661
|
+
return distinctSignals >= 2 ? "medium" : "low";
|
|
2662
|
+
}
|
|
2663
|
+
function confidenceLevelToNumber(level, score) {
|
|
2664
|
+
const base = level === "high" ? 0.85 : level === "medium" ? 0.6 : 0.3;
|
|
2665
|
+
const bonus = Math.min(0.1, Math.max(0, score - 40) / 1e3);
|
|
2666
|
+
return Number(Math.min(0.99, base + bonus).toFixed(2));
|
|
2667
|
+
}
|
|
2668
|
+
async function readChangedFiles(root) {
|
|
2669
|
+
if (process.env.HAUS_DISABLE_GIT_SIGNALS === "1") return [];
|
|
2670
|
+
try {
|
|
2671
|
+
const result = await runGit(["diff", "--name-only"], { cwd: root });
|
|
2672
|
+
if (result.exitCode !== 0) {
|
|
2673
|
+
return [];
|
|
2674
|
+
}
|
|
2675
|
+
return result.stdout.split("\n").map((x) => x.trim()).filter(Boolean).sort();
|
|
2676
|
+
} catch {
|
|
2677
|
+
return [];
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2116
2680
|
|
|
2117
2681
|
// 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
2682
|
async function recommend(root, context) {
|
|
2157
2683
|
const items = await loadCatalog(root);
|
|
2158
2684
|
const setupAnswers = await readJson(hausPath(root, "setup-answers.json")) ?? {};
|
|
@@ -2171,8 +2697,8 @@ async function recommend(root, context) {
|
|
|
2171
2697
|
const changedFiles = await readChangedFiles(root);
|
|
2172
2698
|
const securityRiskCount = context.securityRisks?.length ?? 0;
|
|
2173
2699
|
for (const item of items) {
|
|
2174
|
-
const
|
|
2175
|
-
if (UNSUPPORTED.some((x) =>
|
|
2700
|
+
const itemSearchText = `${item.id} ${item.tags.join(" ")}`.toLowerCase();
|
|
2701
|
+
if (UNSUPPORTED.some((x) => itemSearchText.includes(x))) {
|
|
2176
2702
|
skipped.push({
|
|
2177
2703
|
id: item.id,
|
|
2178
2704
|
reason: "Unsupported stack policy",
|
|
@@ -2334,7 +2860,7 @@ async function recommend(root, context) {
|
|
|
2334
2860
|
);
|
|
2335
2861
|
}
|
|
2336
2862
|
}
|
|
2337
|
-
if (
|
|
2863
|
+
if (SENSITIVE_ITEM_KEYWORDS.some((x) => itemSearchText.includes(x))) {
|
|
2338
2864
|
pushSkipReason("sensitive-policy", "Sensitive content policy block", 100);
|
|
2339
2865
|
}
|
|
2340
2866
|
const trust = sourceTrust.get(item.source);
|
|
@@ -2389,7 +2915,8 @@ async function recommend(root, context) {
|
|
|
2389
2915
|
finalScore: score
|
|
2390
2916
|
},
|
|
2391
2917
|
tags: item.tags,
|
|
2392
|
-
ecosystem: item.ecosystem
|
|
2918
|
+
ecosystem: item.ecosystem,
|
|
2919
|
+
tokenEstimate: item.tokenEstimate
|
|
2393
2920
|
});
|
|
2394
2921
|
} else {
|
|
2395
2922
|
if (skipReasons.length === 0) {
|
|
@@ -2430,94 +2957,6 @@ function buildStackSet(context) {
|
|
|
2430
2957
|
)
|
|
2431
2958
|
);
|
|
2432
2959
|
}
|
|
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
2960
|
|
|
2522
2961
|
// src/utils/prompts.ts
|
|
2523
2962
|
import { stdin as input, stdout as output } from "process";
|
|
@@ -2565,8 +3004,13 @@ async function runSetupProject(options) {
|
|
|
2565
3004
|
merged[question] = existing[question] ?? "pending-user-answer";
|
|
2566
3005
|
continue;
|
|
2567
3006
|
}
|
|
3007
|
+
const prefilled = existing[question];
|
|
3008
|
+
if (prefilled && prefilled !== "pending-user-answer" && prefilled !== "no-answer") {
|
|
3009
|
+
merged[question] = prefilled;
|
|
3010
|
+
continue;
|
|
3011
|
+
}
|
|
2568
3012
|
const answer = await ask(question);
|
|
2569
|
-
merged[question] = answer ||
|
|
3013
|
+
merged[question] = answer || prefilled || "no-answer";
|
|
2570
3014
|
}
|
|
2571
3015
|
await writeJson(hausPath(root, "setup-answers.json"), merged);
|
|
2572
3016
|
}
|
|
@@ -2630,8 +3074,8 @@ async function runSetupProject(options) {
|
|
|
2630
3074
|
// src/commands/init.ts
|
|
2631
3075
|
async function runInit(options) {
|
|
2632
3076
|
const root = process.cwd();
|
|
2633
|
-
const hausDir =
|
|
2634
|
-
const alreadyInit = await
|
|
3077
|
+
const hausDir = path20.join(root, ".haus-workflow");
|
|
3078
|
+
const alreadyInit = await fs13.pathExists(hausDir);
|
|
2635
3079
|
if (alreadyInit) {
|
|
2636
3080
|
log("Haus AI already initialized in this project.");
|
|
2637
3081
|
log("Run `haus setup-project` to reconfigure.");
|
|
@@ -2643,8 +3087,21 @@ async function runInit(options) {
|
|
|
2643
3087
|
|
|
2644
3088
|
// src/install/apply.ts
|
|
2645
3089
|
import crypto2 from "crypto";
|
|
2646
|
-
import
|
|
2647
|
-
import
|
|
3090
|
+
import path23 from "path";
|
|
3091
|
+
import fs15 from "fs-extra";
|
|
3092
|
+
|
|
3093
|
+
// src/install/allow-rules.ts
|
|
3094
|
+
var ALLOWED_SUBCOMMANDS = [
|
|
3095
|
+
"setup-project",
|
|
3096
|
+
"apply",
|
|
3097
|
+
"doctor",
|
|
3098
|
+
"scan",
|
|
3099
|
+
"context",
|
|
3100
|
+
"recommend"
|
|
3101
|
+
];
|
|
3102
|
+
function buildAllowRules() {
|
|
3103
|
+
return [...new Set(ALLOWED_SUBCOMMANDS.map((sub) => `Bash(haus ${sub}:*)`))];
|
|
3104
|
+
}
|
|
2648
3105
|
|
|
2649
3106
|
// src/install/header.ts
|
|
2650
3107
|
var MD_PREFIX = "<!-- HAUS-MANAGED";
|
|
@@ -2656,35 +3113,35 @@ function parseAttrs(raw) {
|
|
|
2656
3113
|
if (!idMatch || !vMatch || !srcMatch) return void 0;
|
|
2657
3114
|
return { stableId: idMatch[1], schemaVersion: vMatch[1], source: srcMatch[1] };
|
|
2658
3115
|
}
|
|
2659
|
-
function parseMarkdownHeader(
|
|
2660
|
-
const firstLine =
|
|
3116
|
+
function parseMarkdownHeader(content2) {
|
|
3117
|
+
const firstLine = content2.split("\n")[0] ?? "";
|
|
2661
3118
|
if (!firstLine.startsWith(MD_PREFIX)) return void 0;
|
|
2662
3119
|
return parseAttrs(firstLine);
|
|
2663
3120
|
}
|
|
2664
3121
|
function buildMarkdownHeader(h) {
|
|
2665
3122
|
return `${MD_PREFIX} id=${h.stableId} v=${h.schemaVersion} source=${h.source}${MD_SUFFIX}`;
|
|
2666
3123
|
}
|
|
2667
|
-
function stampMarkdown(
|
|
3124
|
+
function stampMarkdown(content2, h) {
|
|
2668
3125
|
const header = buildMarkdownHeader(h);
|
|
2669
|
-
const existing = parseMarkdownHeader(
|
|
3126
|
+
const existing = parseMarkdownHeader(content2);
|
|
2670
3127
|
if (existing) {
|
|
2671
|
-
const rest =
|
|
3128
|
+
const rest = content2.slice(content2.indexOf("\n") + 1);
|
|
2672
3129
|
return `${header}
|
|
2673
3130
|
${rest}`;
|
|
2674
3131
|
}
|
|
2675
3132
|
return `${header}
|
|
2676
|
-
${
|
|
3133
|
+
${content2}`;
|
|
2677
3134
|
}
|
|
2678
3135
|
|
|
2679
3136
|
// src/install/manifest.ts
|
|
2680
3137
|
import os4 from "os";
|
|
2681
|
-
import
|
|
3138
|
+
import path21 from "path";
|
|
2682
3139
|
var MANIFEST_SCHEMA = "haus-install-manifest/1";
|
|
2683
3140
|
function globalClaudeDir() {
|
|
2684
|
-
return
|
|
3141
|
+
return path21.join(os4.homedir(), ".claude");
|
|
2685
3142
|
}
|
|
2686
3143
|
function hausManifestPath() {
|
|
2687
|
-
return
|
|
3144
|
+
return path21.join(globalClaudeDir(), "haus", "install-manifest.json");
|
|
2688
3145
|
}
|
|
2689
3146
|
async function readManifest() {
|
|
2690
3147
|
return readJson(hausManifestPath());
|
|
@@ -2703,10 +3160,10 @@ function buildManifest(source, files, hooks) {
|
|
|
2703
3160
|
}
|
|
2704
3161
|
|
|
2705
3162
|
// src/install/settings-merge.ts
|
|
2706
|
-
import
|
|
2707
|
-
import
|
|
3163
|
+
import path22 from "path";
|
|
3164
|
+
import fs14 from "fs-extra";
|
|
2708
3165
|
function settingsJsonPath() {
|
|
2709
|
-
return
|
|
3166
|
+
return path22.join(globalClaudeDir(), "settings.json");
|
|
2710
3167
|
}
|
|
2711
3168
|
async function readSettings() {
|
|
2712
3169
|
const parsed = await readJson(settingsJsonPath());
|
|
@@ -2738,10 +3195,95 @@ function mergeHooks(settings, fragments) {
|
|
|
2738
3195
|
}
|
|
2739
3196
|
updated._haus = {
|
|
2740
3197
|
hooks: [...existing, ...addedIds],
|
|
2741
|
-
hookCommands: [...existingCommands, ...addedCommands]
|
|
3198
|
+
hookCommands: [...existingCommands, ...addedCommands],
|
|
3199
|
+
// Preserve deny/allow tracking so hook, deny, and allow merges are order-independent.
|
|
3200
|
+
...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
|
|
3201
|
+
...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {}
|
|
2742
3202
|
};
|
|
2743
3203
|
return { settings: updated, addedIds };
|
|
2744
3204
|
}
|
|
3205
|
+
function mergeDenyRules(settings, rules) {
|
|
3206
|
+
const existingDeny = settings.permissions?.deny ?? [];
|
|
3207
|
+
const seen = new Set(existingDeny);
|
|
3208
|
+
const trackedDeny = settings._haus?.denyRules ?? [];
|
|
3209
|
+
const addedRules = [];
|
|
3210
|
+
for (const rule of rules) {
|
|
3211
|
+
if (seen.has(rule)) continue;
|
|
3212
|
+
seen.add(rule);
|
|
3213
|
+
addedRules.push(rule);
|
|
3214
|
+
}
|
|
3215
|
+
const updated = { ...settings };
|
|
3216
|
+
updated.permissions = {
|
|
3217
|
+
...settings.permissions ?? {},
|
|
3218
|
+
deny: [...existingDeny, ...addedRules]
|
|
3219
|
+
};
|
|
3220
|
+
updated._haus = {
|
|
3221
|
+
hooks: settings._haus?.hooks ?? [],
|
|
3222
|
+
...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
|
|
3223
|
+
denyRules: [...trackedDeny, ...addedRules],
|
|
3224
|
+
...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {}
|
|
3225
|
+
};
|
|
3226
|
+
return { settings: updated, addedRules };
|
|
3227
|
+
}
|
|
3228
|
+
function mergeAllowRules(settings, rules) {
|
|
3229
|
+
const existingAllow = settings.permissions?.allow ?? [];
|
|
3230
|
+
const seen = new Set(existingAllow);
|
|
3231
|
+
const trackedAllow = settings._haus?.allowRules ?? [];
|
|
3232
|
+
const addedRules = [];
|
|
3233
|
+
for (const rule of rules) {
|
|
3234
|
+
if (seen.has(rule)) continue;
|
|
3235
|
+
seen.add(rule);
|
|
3236
|
+
addedRules.push(rule);
|
|
3237
|
+
}
|
|
3238
|
+
const updated = { ...settings };
|
|
3239
|
+
updated.permissions = {
|
|
3240
|
+
...settings.permissions ?? {},
|
|
3241
|
+
allow: [...existingAllow, ...addedRules]
|
|
3242
|
+
};
|
|
3243
|
+
updated._haus = {
|
|
3244
|
+
hooks: settings._haus?.hooks ?? [],
|
|
3245
|
+
...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
|
|
3246
|
+
...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
|
|
3247
|
+
allowRules: [...trackedAllow, ...addedRules]
|
|
3248
|
+
};
|
|
3249
|
+
return { settings: updated, addedRules };
|
|
3250
|
+
}
|
|
3251
|
+
function stripHausAllow(settings) {
|
|
3252
|
+
const prevHaus = settings._haus;
|
|
3253
|
+
if (!prevHaus?.allowRules || prevHaus.allowRules.length === 0) return settings;
|
|
3254
|
+
const ownedSet = new Set(prevHaus.allowRules);
|
|
3255
|
+
const updated = { ...settings };
|
|
3256
|
+
const remainingAllow = (settings.permissions?.allow ?? []).filter((rule) => !ownedSet.has(rule));
|
|
3257
|
+
const permissions = { ...settings.permissions ?? {} };
|
|
3258
|
+
if (remainingAllow.length > 0) permissions.allow = remainingAllow;
|
|
3259
|
+
else delete permissions.allow;
|
|
3260
|
+
if (Object.keys(permissions).length > 0) updated.permissions = permissions;
|
|
3261
|
+
else delete updated.permissions;
|
|
3262
|
+
const haus = { ...prevHaus };
|
|
3263
|
+
delete haus.allowRules;
|
|
3264
|
+
const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.denyRules?.length ?? 0) > 0;
|
|
3265
|
+
if (stillTracking) updated._haus = haus;
|
|
3266
|
+
else delete updated._haus;
|
|
3267
|
+
return updated;
|
|
3268
|
+
}
|
|
3269
|
+
function stripHausDeny(settings) {
|
|
3270
|
+
const prevHaus = settings._haus;
|
|
3271
|
+
if (!prevHaus?.denyRules || prevHaus.denyRules.length === 0) return settings;
|
|
3272
|
+
const ownedSet = new Set(prevHaus.denyRules);
|
|
3273
|
+
const updated = { ...settings };
|
|
3274
|
+
const remainingDeny = (settings.permissions?.deny ?? []).filter((rule) => !ownedSet.has(rule));
|
|
3275
|
+
const permissions = { ...settings.permissions ?? {} };
|
|
3276
|
+
if (remainingDeny.length > 0) permissions.deny = remainingDeny;
|
|
3277
|
+
else delete permissions.deny;
|
|
3278
|
+
if (Object.keys(permissions).length > 0) updated.permissions = permissions;
|
|
3279
|
+
else delete updated.permissions;
|
|
3280
|
+
const haus = { ...prevHaus };
|
|
3281
|
+
delete haus.denyRules;
|
|
3282
|
+
const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.allowRules?.length ?? 0) > 0;
|
|
3283
|
+
if (stillTracking) updated._haus = haus;
|
|
3284
|
+
else delete updated._haus;
|
|
3285
|
+
return updated;
|
|
3286
|
+
}
|
|
2745
3287
|
function stripHausHooks(settings) {
|
|
2746
3288
|
if (!settings._haus) return settings;
|
|
2747
3289
|
const ownedCommands = new Set(settings._haus.hookCommands ?? []);
|
|
@@ -2762,7 +3304,7 @@ function stripHausHooks(settings) {
|
|
|
2762
3304
|
async function loadHooksFragment(fragmentPath) {
|
|
2763
3305
|
let raw;
|
|
2764
3306
|
try {
|
|
2765
|
-
raw = await
|
|
3307
|
+
raw = await fs14.readJson(fragmentPath);
|
|
2766
3308
|
} catch {
|
|
2767
3309
|
return [];
|
|
2768
3310
|
}
|
|
@@ -2772,45 +3314,45 @@ async function loadHooksFragment(fragmentPath) {
|
|
|
2772
3314
|
|
|
2773
3315
|
// src/install/apply.ts
|
|
2774
3316
|
var SCHEMA_VERSION3 = "1";
|
|
2775
|
-
function hashContent(
|
|
2776
|
-
return `sha256-${crypto2.createHash("sha256").update(
|
|
3317
|
+
function hashContent(content2) {
|
|
3318
|
+
return `sha256-${crypto2.createHash("sha256").update(content2).digest("hex")}`;
|
|
2777
3319
|
}
|
|
2778
3320
|
function sourceVersion() {
|
|
2779
3321
|
try {
|
|
2780
|
-
const pkgPath =
|
|
2781
|
-
const pkg = JSON.parse(
|
|
3322
|
+
const pkgPath = path23.join(packageRoot(), "package.json");
|
|
3323
|
+
const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf8"));
|
|
2782
3324
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
2783
3325
|
} catch {
|
|
2784
3326
|
return "haus@0.0.0";
|
|
2785
3327
|
}
|
|
2786
3328
|
}
|
|
2787
3329
|
function globalSrcDir() {
|
|
2788
|
-
return
|
|
3330
|
+
return path23.join(packageRoot(), "library", "global");
|
|
2789
3331
|
}
|
|
2790
3332
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
2791
3333
|
const entries = [];
|
|
2792
|
-
const skillsDir =
|
|
2793
|
-
if (
|
|
2794
|
-
for (const skillName of
|
|
2795
|
-
const skillFile =
|
|
2796
|
-
if (
|
|
3334
|
+
const skillsDir = path23.join(srcDir, "skills");
|
|
3335
|
+
if (fs15.pathExistsSync(skillsDir)) {
|
|
3336
|
+
for (const skillName of fs15.readdirSync(skillsDir)) {
|
|
3337
|
+
const skillFile = path23.join(skillsDir, skillName, "SKILL.md");
|
|
3338
|
+
if (fs15.pathExistsSync(skillFile)) {
|
|
2797
3339
|
entries.push({
|
|
2798
3340
|
stableId: `skill.${skillName}`,
|
|
2799
|
-
srcRelPath:
|
|
2800
|
-
destPath:
|
|
3341
|
+
srcRelPath: path23.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
3342
|
+
destPath: path23.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
2801
3343
|
});
|
|
2802
3344
|
}
|
|
2803
3345
|
}
|
|
2804
3346
|
}
|
|
2805
|
-
const
|
|
2806
|
-
if (
|
|
2807
|
-
for (const
|
|
2808
|
-
if (!
|
|
2809
|
-
const
|
|
3347
|
+
const commandsDir = path23.join(srcDir, "commands");
|
|
3348
|
+
if (fs15.pathExistsSync(commandsDir)) {
|
|
3349
|
+
for (const fileName of fs15.readdirSync(commandsDir)) {
|
|
3350
|
+
if (!fileName.endsWith(".md")) continue;
|
|
3351
|
+
const commandName = fileName.slice(0, -".md".length);
|
|
2810
3352
|
entries.push({
|
|
2811
|
-
stableId: `
|
|
2812
|
-
srcRelPath:
|
|
2813
|
-
destPath:
|
|
3353
|
+
stableId: `command.${commandName}`,
|
|
3354
|
+
srcRelPath: path23.join("library", "global", "commands", fileName),
|
|
3355
|
+
destPath: path23.join(claudeDir, "commands", fileName)
|
|
2814
3356
|
});
|
|
2815
3357
|
}
|
|
2816
3358
|
}
|
|
@@ -2834,7 +3376,7 @@ async function applyInstall(options = {}) {
|
|
|
2834
3376
|
};
|
|
2835
3377
|
const manifestFiles = [];
|
|
2836
3378
|
for (const entry of sourceFiles) {
|
|
2837
|
-
const srcPath =
|
|
3379
|
+
const srcPath = path23.join(packageRoot(), entry.srcRelPath);
|
|
2838
3380
|
const rawContent = await readText(srcPath);
|
|
2839
3381
|
if (rawContent === void 0) {
|
|
2840
3382
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -2854,7 +3396,7 @@ async function applyInstall(options = {}) {
|
|
|
2854
3396
|
}
|
|
2855
3397
|
continue;
|
|
2856
3398
|
}
|
|
2857
|
-
const destExists =
|
|
3399
|
+
const destExists = fs15.pathExistsSync(entry.destPath);
|
|
2858
3400
|
if (destExists) {
|
|
2859
3401
|
const currentContent = await readText(entry.destPath);
|
|
2860
3402
|
if (currentContent !== void 0) {
|
|
@@ -2890,22 +3432,24 @@ async function applyInstall(options = {}) {
|
|
|
2890
3432
|
schemaVersion: SCHEMA_VERSION3
|
|
2891
3433
|
});
|
|
2892
3434
|
}
|
|
2893
|
-
const fragmentPath =
|
|
3435
|
+
const fragmentPath = path23.join(srcDir, "settings-fragments", "hooks.json");
|
|
2894
3436
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
2895
3437
|
const settings = await readSettings();
|
|
2896
|
-
const { settings:
|
|
3438
|
+
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
3439
|
+
const { settings: deniedSettings } = mergeDenyRules(hookSettings, buildDenyRules());
|
|
3440
|
+
const { settings: mergedSettings } = mergeAllowRules(deniedSettings, buildAllowRules());
|
|
2897
3441
|
result.hookIds = addedIds;
|
|
2898
3442
|
if (!check && existingManifest) {
|
|
2899
3443
|
const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
|
|
2900
3444
|
for (const entry of existingManifest.files) {
|
|
2901
3445
|
if (currentDestPaths.has(entry.destPath)) continue;
|
|
2902
|
-
if (!
|
|
2903
|
-
const
|
|
2904
|
-
if (!
|
|
2905
|
-
const hasHeader = parseMarkdownHeader(
|
|
2906
|
-
const currentHash = hashContent(
|
|
3446
|
+
if (!fs15.pathExistsSync(entry.destPath)) continue;
|
|
3447
|
+
const content2 = await readText(entry.destPath);
|
|
3448
|
+
if (!content2) continue;
|
|
3449
|
+
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
3450
|
+
const currentHash = hashContent(content2);
|
|
2907
3451
|
if (hasHeader && currentHash === entry.hash) {
|
|
2908
|
-
if (!dryRun) await
|
|
3452
|
+
if (!dryRun) await fs15.remove(entry.destPath);
|
|
2909
3453
|
result.deleted.push(entry.destPath);
|
|
2910
3454
|
} else {
|
|
2911
3455
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -2957,14 +3501,27 @@ async function runInstall(options) {
|
|
|
2957
3501
|
force: options.force,
|
|
2958
3502
|
check: options.check
|
|
2959
3503
|
});
|
|
2960
|
-
printApplyResult(result, options.dryRun ?? false);
|
|
3504
|
+
if (!options.postinstall) printApplyResult(result, options.dryRun ?? false);
|
|
2961
3505
|
if (options.check && result.drift) {
|
|
2962
3506
|
process.exitCode = 1;
|
|
2963
3507
|
} else if (!options.check && !options.dryRun) {
|
|
2964
3508
|
const total = result.created.length + result.updated.length;
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
3509
|
+
if (options.postinstall) {
|
|
3510
|
+
log("haus configured Claude Code for you:");
|
|
3511
|
+
const parts = [];
|
|
3512
|
+
if (result.created.length) parts.push(`${result.created.length} file(s) added`);
|
|
3513
|
+
if (result.updated.length) parts.push(`${result.updated.length} file(s) updated`);
|
|
3514
|
+
log(
|
|
3515
|
+
parts.length ? ` \u2022 ${parts.join(", ")} in ~/.claude (skills, slash commands)` : " \u2022 already up to date \u2014 no files changed"
|
|
3516
|
+
);
|
|
3517
|
+
log(` \u2022 ensured hooks + security rules are present in ~/.claude/settings.json`);
|
|
3518
|
+
log("Undo any time with: haus uninstall");
|
|
3519
|
+
log("Disable this on install: HAUS_NO_POSTINSTALL=1");
|
|
3520
|
+
} else {
|
|
3521
|
+
log(
|
|
3522
|
+
`haus install complete (${total} file(s) written, ${result.hookIds.length} hook(s) added)`
|
|
3523
|
+
);
|
|
3524
|
+
}
|
|
2968
3525
|
}
|
|
2969
3526
|
} catch (err) {
|
|
2970
3527
|
error(`haus install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -2972,79 +3529,6 @@ async function runInstall(options) {
|
|
|
2972
3529
|
}
|
|
2973
3530
|
}
|
|
2974
3531
|
|
|
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
3532
|
// src/commands/recommend.ts
|
|
3049
3533
|
async function runRecommend(options) {
|
|
3050
3534
|
const root = process.cwd();
|
|
@@ -3088,20 +3572,20 @@ async function runScan(options) {
|
|
|
3088
3572
|
}
|
|
3089
3573
|
|
|
3090
3574
|
// src/commands/undo.ts
|
|
3091
|
-
import
|
|
3092
|
-
import
|
|
3575
|
+
import path24 from "path";
|
|
3576
|
+
import fs16 from "fs-extra";
|
|
3093
3577
|
var CLAUDE_DIR = ".claude";
|
|
3094
3578
|
async function runUndo(options) {
|
|
3095
3579
|
const root = process.cwd();
|
|
3096
|
-
const targets = [
|
|
3097
|
-
const existing = targets.filter((p) =>
|
|
3580
|
+
const targets = [path24.join(root, CLAUDE_DIR), path24.join(root, HAUS_DIR)];
|
|
3581
|
+
const existing = targets.filter((p) => fs16.existsSync(p));
|
|
3098
3582
|
if (existing.length === 0) {
|
|
3099
3583
|
log("Nothing to remove: no .claude/ or .haus-workflow/ in this directory.");
|
|
3100
3584
|
return;
|
|
3101
3585
|
}
|
|
3102
3586
|
if (!options.yes) {
|
|
3103
3587
|
const ok = await confirm(
|
|
3104
|
-
`Remove ${existing.map((p) =>
|
|
3588
|
+
`Remove ${existing.map((p) => path24.relative(root, p)).join(" and ")}? This cannot be undone.`
|
|
3105
3589
|
);
|
|
3106
3590
|
if (!ok) {
|
|
3107
3591
|
log("Cancelled.");
|
|
@@ -3109,15 +3593,15 @@ async function runUndo(options) {
|
|
|
3109
3593
|
}
|
|
3110
3594
|
}
|
|
3111
3595
|
for (const p of existing) {
|
|
3112
|
-
await
|
|
3113
|
-
log(`Removed ${
|
|
3596
|
+
await fs16.remove(p);
|
|
3597
|
+
log(`Removed ${path24.relative(root, p)}`);
|
|
3114
3598
|
}
|
|
3115
3599
|
}
|
|
3116
3600
|
|
|
3117
3601
|
// src/install/uninstall.ts
|
|
3118
3602
|
import crypto3 from "crypto";
|
|
3119
|
-
import
|
|
3120
|
-
import
|
|
3603
|
+
import path25 from "path";
|
|
3604
|
+
import fs17 from "fs-extra";
|
|
3121
3605
|
async function runUninstall(options = {}) {
|
|
3122
3606
|
const { force = false } = options;
|
|
3123
3607
|
const manifest = await readManifest();
|
|
@@ -3127,17 +3611,17 @@ async function runUninstall(options = {}) {
|
|
|
3127
3611
|
return result;
|
|
3128
3612
|
}
|
|
3129
3613
|
for (const entry of manifest.files) {
|
|
3130
|
-
const exists =
|
|
3614
|
+
const exists = fs17.pathExistsSync(entry.destPath);
|
|
3131
3615
|
if (!exists) continue;
|
|
3132
|
-
const
|
|
3133
|
-
if (
|
|
3134
|
-
const header = parseMarkdownHeader(
|
|
3616
|
+
const content2 = await readText(entry.destPath);
|
|
3617
|
+
if (content2 === void 0) continue;
|
|
3618
|
+
const header = parseMarkdownHeader(content2);
|
|
3135
3619
|
if (!header) {
|
|
3136
3620
|
warn(`Skipping user-owned file (no HAUS-MANAGED header): ${entry.destPath}`);
|
|
3137
3621
|
result.skipped.push(entry.destPath);
|
|
3138
3622
|
continue;
|
|
3139
3623
|
}
|
|
3140
|
-
const currentHash = `sha256-${crypto3.createHash("sha256").update(
|
|
3624
|
+
const currentHash = `sha256-${crypto3.createHash("sha256").update(content2).digest("hex")}`;
|
|
3141
3625
|
if (currentHash !== entry.hash && !force) {
|
|
3142
3626
|
warn(
|
|
3143
3627
|
`Skipping user-edited haus file (hash mismatch): ${entry.destPath} \u2014 use --force to delete`
|
|
@@ -3145,22 +3629,22 @@ async function runUninstall(options = {}) {
|
|
|
3145
3629
|
result.skipped.push(entry.destPath);
|
|
3146
3630
|
continue;
|
|
3147
3631
|
}
|
|
3148
|
-
await
|
|
3149
|
-
await pruneEmptyDir(
|
|
3632
|
+
await fs17.remove(entry.destPath);
|
|
3633
|
+
await pruneEmptyDir(path25.dirname(entry.destPath));
|
|
3150
3634
|
result.deleted.push(entry.destPath);
|
|
3151
3635
|
}
|
|
3152
3636
|
const settings = await readSettings();
|
|
3153
|
-
const stripped = stripHausHooks(settings);
|
|
3637
|
+
const stripped = stripHausHooks(stripHausAllow(stripHausDeny(settings)));
|
|
3154
3638
|
await writeSettings(stripped);
|
|
3155
3639
|
result.hooksStripped = true;
|
|
3156
|
-
const hausDir =
|
|
3640
|
+
const hausDir = path25.join(globalClaudeDir(), "haus");
|
|
3157
3641
|
const manifestPath = hausManifestPath();
|
|
3158
|
-
if (
|
|
3159
|
-
await
|
|
3642
|
+
if (fs17.pathExistsSync(manifestPath)) {
|
|
3643
|
+
await fs17.remove(manifestPath);
|
|
3160
3644
|
}
|
|
3161
|
-
if (
|
|
3162
|
-
const remaining = await
|
|
3163
|
-
if (remaining.length === 0) await
|
|
3645
|
+
if (fs17.pathExistsSync(hausDir)) {
|
|
3646
|
+
const remaining = await fs17.readdir(hausDir);
|
|
3647
|
+
if (remaining.length === 0) await fs17.remove(hausDir);
|
|
3164
3648
|
}
|
|
3165
3649
|
return result;
|
|
3166
3650
|
}
|
|
@@ -3179,8 +3663,8 @@ function printUninstallResult(result) {
|
|
|
3179
3663
|
}
|
|
3180
3664
|
async function pruneEmptyDir(dir) {
|
|
3181
3665
|
try {
|
|
3182
|
-
const entries = await
|
|
3183
|
-
if (entries.length === 0) await
|
|
3666
|
+
const entries = await fs17.readdir(dir);
|
|
3667
|
+
if (entries.length === 0) await fs17.remove(dir);
|
|
3184
3668
|
} catch {
|
|
3185
3669
|
}
|
|
3186
3670
|
}
|
|
@@ -3198,7 +3682,7 @@ async function runUninstallCommand(options) {
|
|
|
3198
3682
|
}
|
|
3199
3683
|
|
|
3200
3684
|
// src/commands/update.ts
|
|
3201
|
-
import
|
|
3685
|
+
import path27 from "path";
|
|
3202
3686
|
|
|
3203
3687
|
// src/update/diff-generated-files.ts
|
|
3204
3688
|
function diffGeneratedFiles() {
|
|
@@ -3224,8 +3708,8 @@ function summarizeLockDiff(before, after) {
|
|
|
3224
3708
|
}
|
|
3225
3709
|
|
|
3226
3710
|
// src/update/lockfile.ts
|
|
3227
|
-
import { mkdir, readFile as
|
|
3228
|
-
import
|
|
3711
|
+
import { mkdir, readFile as readFile3, copyFile } from "fs/promises";
|
|
3712
|
+
import path26 from "path";
|
|
3229
3713
|
async function checkLock(root) {
|
|
3230
3714
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
3231
3715
|
const hasValidVersions = lock.every(
|
|
@@ -3238,7 +3722,7 @@ async function applyLock(root) {
|
|
|
3238
3722
|
const lockPath = hausPath(root, "haus.lock.json");
|
|
3239
3723
|
let before = "[]";
|
|
3240
3724
|
try {
|
|
3241
|
-
before = await
|
|
3725
|
+
before = await readFile3(lockPath, "utf8");
|
|
3242
3726
|
} catch {
|
|
3243
3727
|
before = "[]";
|
|
3244
3728
|
}
|
|
@@ -3246,7 +3730,7 @@ async function applyLock(root) {
|
|
|
3246
3730
|
try {
|
|
3247
3731
|
const backupDir = hausPath(root, "backups");
|
|
3248
3732
|
await mkdir(backupDir, { recursive: true });
|
|
3249
|
-
await copyFile(lockPath,
|
|
3733
|
+
await copyFile(lockPath, path26.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
3250
3734
|
} catch {
|
|
3251
3735
|
}
|
|
3252
3736
|
const enriched = await Promise.all(
|
|
@@ -3268,7 +3752,7 @@ function diffLock(before, after) {
|
|
|
3268
3752
|
}
|
|
3269
3753
|
async function hasLocalOverrides(root) {
|
|
3270
3754
|
try {
|
|
3271
|
-
await
|
|
3755
|
+
await readFile3(path26.join(root, ".claude", "settings.json"), "utf8");
|
|
3272
3756
|
return true;
|
|
3273
3757
|
} catch {
|
|
3274
3758
|
return false;
|
|
@@ -3280,7 +3764,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
|
3280
3764
|
async function runUpdate(options) {
|
|
3281
3765
|
const root = process.cwd();
|
|
3282
3766
|
if (options.check) {
|
|
3283
|
-
const pkgJson2 = await readJson(
|
|
3767
|
+
const pkgJson2 = await readJson(path27.join(packageRoot(), "package.json"));
|
|
3284
3768
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
3285
3769
|
const [status, npmVersion, latestCatalogTag] = await Promise.all([
|
|
3286
3770
|
checkLock(root),
|
|
@@ -3307,7 +3791,7 @@ async function runUpdate(options) {
|
|
|
3307
3791
|
if (!status.ok) process.exitCode = 1;
|
|
3308
3792
|
return;
|
|
3309
3793
|
}
|
|
3310
|
-
const pkgJson = await readJson(
|
|
3794
|
+
const pkgJson = await readJson(path27.join(packageRoot(), "package.json"));
|
|
3311
3795
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
3312
3796
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
3313
3797
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -3337,49 +3821,187 @@ async function runUpdate(options) {
|
|
|
3337
3821
|
}
|
|
3338
3822
|
|
|
3339
3823
|
// src/commands/validate-catalog.ts
|
|
3340
|
-
import
|
|
3341
|
-
import
|
|
3824
|
+
import fs18 from "fs";
|
|
3825
|
+
import path28 from "path";
|
|
3342
3826
|
|
|
3343
|
-
//
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3827
|
+
// library/catalog/validation-rules.json
|
|
3828
|
+
var validation_rules_default = {
|
|
3829
|
+
forbiddenTags: [
|
|
3830
|
+
"python",
|
|
3831
|
+
"django",
|
|
3832
|
+
"go",
|
|
3833
|
+
"rust",
|
|
3834
|
+
"java",
|
|
3835
|
+
"spring",
|
|
3836
|
+
"kotlin",
|
|
3837
|
+
"swift",
|
|
3838
|
+
"android",
|
|
3839
|
+
"flutter",
|
|
3840
|
+
"dart",
|
|
3841
|
+
"c++",
|
|
3842
|
+
"perl",
|
|
3843
|
+
"defi",
|
|
3844
|
+
"trading"
|
|
3845
|
+
],
|
|
3846
|
+
bannedAgentPhrases: ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"],
|
|
3847
|
+
requiredSkillSections: ["## Use when", "## Do not use when"],
|
|
3848
|
+
requiredAgentSections: ["## Use when", "## Do not use when", "## Verification"],
|
|
3849
|
+
riskyInstallPatterns: [
|
|
3850
|
+
{ source: "\\bnpx\\s+-y\\b", flags: "i" },
|
|
3851
|
+
{ source: "\\bnpx\\s+--yes\\b", flags: "i" },
|
|
3852
|
+
{ source: "\\byarn\\s+dlx\\b", flags: "i" },
|
|
3853
|
+
{ source: "\\bpnpm\\s+dlx\\b", flags: "i" }
|
|
3854
|
+
],
|
|
3855
|
+
allowedNpxPattern: { source: "\\bnpx\\s+tsx\\b", flags: "i" },
|
|
3856
|
+
anyNpxPattern: { source: "\\bnpx\\s+\\S+", flags: "i" },
|
|
3857
|
+
httpUrlPattern: { source: "^http:\\/\\/", flags: "i" },
|
|
3858
|
+
placeholderPattern: { source: "\\bTODO\\b|\\bPLACEHOLDER\\b", flags: "i" },
|
|
3859
|
+
allowedStacks: [
|
|
3860
|
+
"haus",
|
|
3861
|
+
"security",
|
|
3862
|
+
"quality",
|
|
3863
|
+
"frontend",
|
|
3864
|
+
"backend",
|
|
3865
|
+
"testing",
|
|
3866
|
+
"review",
|
|
3867
|
+
"workflow",
|
|
3868
|
+
"reference-pack",
|
|
3869
|
+
"core-skill",
|
|
3870
|
+
"workflow-skill",
|
|
3871
|
+
"stack-skill",
|
|
3872
|
+
"review-skill",
|
|
3873
|
+
"agent",
|
|
3874
|
+
"hook",
|
|
3875
|
+
"rule",
|
|
3876
|
+
"react",
|
|
3877
|
+
"typescript",
|
|
3878
|
+
"php",
|
|
3879
|
+
"csharp",
|
|
3880
|
+
"vendure",
|
|
3881
|
+
"vendure3",
|
|
3882
|
+
"nestjs",
|
|
3883
|
+
"graphql",
|
|
3884
|
+
"nx21",
|
|
3885
|
+
"turbo",
|
|
3886
|
+
"nextjs",
|
|
3887
|
+
"react19",
|
|
3888
|
+
"typescript5",
|
|
3889
|
+
"vite8",
|
|
3890
|
+
"tanstack-query",
|
|
3891
|
+
"tanstack-router",
|
|
3892
|
+
"radix",
|
|
3893
|
+
"radix-ui",
|
|
3894
|
+
"shadcn",
|
|
3895
|
+
"shadcn-ui",
|
|
3896
|
+
"tailwind",
|
|
3897
|
+
"tailwindcss",
|
|
3898
|
+
"scss",
|
|
3899
|
+
"scss-modules",
|
|
3900
|
+
"vue",
|
|
3901
|
+
"expressjs",
|
|
3902
|
+
"soup-base",
|
|
3903
|
+
"laravel",
|
|
3904
|
+
"laravel-nova",
|
|
3905
|
+
"wordpress",
|
|
3906
|
+
"bedrock",
|
|
3907
|
+
"elementor-pro",
|
|
3908
|
+
"acf-pro",
|
|
3909
|
+
"jetengine",
|
|
3910
|
+
"dotnet",
|
|
3911
|
+
"oidc",
|
|
3912
|
+
"azure-ad",
|
|
3913
|
+
"bankid",
|
|
3914
|
+
"myid",
|
|
3915
|
+
"cgi",
|
|
3916
|
+
"crypto",
|
|
3917
|
+
"collection2",
|
|
3918
|
+
"postgresql",
|
|
3919
|
+
"mariadb",
|
|
3920
|
+
"mssql",
|
|
3921
|
+
"elasticsearch",
|
|
3922
|
+
"yarn4",
|
|
3923
|
+
"pnpm89",
|
|
3924
|
+
"playwright",
|
|
3925
|
+
"testing-library",
|
|
3926
|
+
"phpunit",
|
|
3927
|
+
"storybook",
|
|
3928
|
+
"wisest",
|
|
3929
|
+
"vitest",
|
|
3930
|
+
"jest",
|
|
3931
|
+
"redis",
|
|
3932
|
+
"sanity",
|
|
3933
|
+
"strapi",
|
|
3934
|
+
"prisma",
|
|
3935
|
+
"cms",
|
|
3936
|
+
"database",
|
|
3937
|
+
"mysql",
|
|
3938
|
+
"saml2",
|
|
3939
|
+
"next-auth",
|
|
3940
|
+
"auth",
|
|
3941
|
+
"expo",
|
|
3942
|
+
"react-native",
|
|
3943
|
+
"mobile",
|
|
3944
|
+
"i18next",
|
|
3945
|
+
"i18n",
|
|
3946
|
+
"bullmq",
|
|
3947
|
+
"queue",
|
|
3948
|
+
"sentry",
|
|
3949
|
+
"observability",
|
|
3950
|
+
"tooling",
|
|
3951
|
+
"prettier",
|
|
3952
|
+
"eslint",
|
|
3953
|
+
"missing-prettier",
|
|
3954
|
+
"missing-eslint",
|
|
3955
|
+
"docker",
|
|
3956
|
+
"pm2",
|
|
3957
|
+
"deployer-php",
|
|
3958
|
+
"stripe",
|
|
3959
|
+
"qliro",
|
|
3960
|
+
"supabase",
|
|
3961
|
+
"payments"
|
|
3962
|
+
],
|
|
3963
|
+
alwaysAllowedTags: [
|
|
3964
|
+
"haus",
|
|
3965
|
+
"security",
|
|
3966
|
+
"quality",
|
|
3967
|
+
"review",
|
|
3968
|
+
"workflow",
|
|
3969
|
+
"baseline",
|
|
3970
|
+
"project-instructions"
|
|
3971
|
+
],
|
|
3972
|
+
patternTagSuffixes: ["-patterns"]
|
|
3973
|
+
};
|
|
3351
3974
|
|
|
3352
3975
|
// 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;
|
|
3976
|
+
var toRegExp = (r) => new RegExp(r.source, r.flags);
|
|
3977
|
+
var FORBIDDEN_TAGS = validation_rules_default.forbiddenTags;
|
|
3978
|
+
var BANNED_AGENT_PHRASES = validation_rules_default.bannedAgentPhrases;
|
|
3979
|
+
var REQUIRED_SKILL_SECTIONS = validation_rules_default.requiredSkillSections;
|
|
3980
|
+
var REQUIRED_AGENT_SECTIONS = validation_rules_default.requiredAgentSections;
|
|
3981
|
+
var RISKY_INSTALL_PATTERNS = validation_rules_default.riskyInstallPatterns.map(toRegExp);
|
|
3982
|
+
var ALLOWED_NPX_PATTERN = toRegExp(validation_rules_default.allowedNpxPattern);
|
|
3983
|
+
var ANY_NPX_PATTERN = toRegExp(validation_rules_default.anyNpxPattern);
|
|
3984
|
+
var HTTP_URL_PATTERN = toRegExp(validation_rules_default.httpUrlPattern);
|
|
3985
|
+
var PLACEHOLDER_PATTERN = toRegExp(validation_rules_default.placeholderPattern);
|
|
3986
|
+
var ALLOWED_STACKS = validation_rules_default.allowedStacks;
|
|
3987
|
+
var ALWAYS_ALLOWED_TAGS = validation_rules_default.alwaysAllowedTags;
|
|
3988
|
+
var PATTERN_TAG_SUFFIXES = validation_rules_default.patternTagSuffixes;
|
|
3989
|
+
var ALLOWED_SET = new Set([...ALLOWED_STACKS, ...ALWAYS_ALLOWED_TAGS].map((t) => t.toLowerCase()));
|
|
3990
|
+
function isTagAllowed(tag) {
|
|
3991
|
+
const lower = tag.toLowerCase();
|
|
3992
|
+
if (ALLOWED_SET.has(lower)) return true;
|
|
3993
|
+
return PATTERN_TAG_SUFFIXES.some((suffix) => lower.endsWith(suffix));
|
|
3994
|
+
}
|
|
3995
|
+
function auditDisallowedTags(items) {
|
|
3996
|
+
const failures = [];
|
|
3997
|
+
for (const item of items) {
|
|
3998
|
+
if (!item.id) continue;
|
|
3999
|
+
for (const tag of Array.isArray(item.tags) ? item.tags : []) {
|
|
4000
|
+
if (!isTagAllowed(tag)) failures.push(`${item.id}: tag not in allowlist: "${tag}"`);
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
return failures;
|
|
4004
|
+
}
|
|
3383
4005
|
|
|
3384
4006
|
// src/commands/validate-catalog.ts
|
|
3385
4007
|
function auditForbiddenStacks(items) {
|
|
@@ -3449,23 +4071,23 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
3449
4071
|
const failures = [];
|
|
3450
4072
|
for (const item of items) {
|
|
3451
4073
|
if (!item.path) continue;
|
|
3452
|
-
const absPath =
|
|
4074
|
+
const absPath = path28.join(manifestDir, item.path);
|
|
3453
4075
|
if (item.type === "skill") {
|
|
3454
|
-
const skillMd =
|
|
3455
|
-
if (!
|
|
3456
|
-
failures.push(`${item.id}: missing ${
|
|
4076
|
+
const skillMd = path28.join(absPath, "SKILL.md");
|
|
4077
|
+
if (!fs18.existsSync(skillMd)) {
|
|
4078
|
+
failures.push(`${item.id}: missing ${path28.relative(manifestDir, skillMd)}`);
|
|
3457
4079
|
continue;
|
|
3458
4080
|
}
|
|
3459
|
-
const text =
|
|
4081
|
+
const text = fs18.readFileSync(skillMd, "utf8");
|
|
3460
4082
|
for (const section of REQUIRED_SKILL_SECTIONS) {
|
|
3461
4083
|
if (!text.includes(section)) failures.push(`${item.id}: SKILL.md missing ${section}`);
|
|
3462
4084
|
}
|
|
3463
4085
|
} else if (item.type === "agent") {
|
|
3464
|
-
if (!
|
|
4086
|
+
if (!fs18.existsSync(absPath)) {
|
|
3465
4087
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
3466
4088
|
continue;
|
|
3467
4089
|
}
|
|
3468
|
-
const text =
|
|
4090
|
+
const text = fs18.readFileSync(absPath, "utf8");
|
|
3469
4091
|
if (!text.startsWith("---")) failures.push(`${item.id}: agent file missing YAML frontmatter`);
|
|
3470
4092
|
for (const section of REQUIRED_AGENT_SECTIONS) {
|
|
3471
4093
|
if (!text.includes(section)) failures.push(`${item.id}: agent file missing ${section}`);
|
|
@@ -3476,7 +4098,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
3476
4098
|
failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
|
|
3477
4099
|
}
|
|
3478
4100
|
} else if (item.type === "template") {
|
|
3479
|
-
if (!
|
|
4101
|
+
if (!fs18.existsSync(absPath)) {
|
|
3480
4102
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
3481
4103
|
}
|
|
3482
4104
|
}
|
|
@@ -3487,21 +4109,21 @@ function auditMarkdownContent(manifestDir) {
|
|
|
3487
4109
|
const failures = [];
|
|
3488
4110
|
const dirs = ["skills", "agents"];
|
|
3489
4111
|
for (const dir of dirs) {
|
|
3490
|
-
const abs =
|
|
3491
|
-
if (!
|
|
4112
|
+
const abs = path28.join(manifestDir, dir);
|
|
4113
|
+
if (!fs18.existsSync(abs)) continue;
|
|
3492
4114
|
walkMd(abs, (file) => {
|
|
3493
|
-
const text =
|
|
3494
|
-
const rel =
|
|
4115
|
+
const text = fs18.readFileSync(file, "utf8");
|
|
4116
|
+
const rel = path28.relative(manifestDir, file);
|
|
3495
4117
|
const lines = text.split(/\r?\n/);
|
|
3496
4118
|
for (let i = 0; i < lines.length; i++) {
|
|
3497
|
-
const
|
|
3498
|
-
if (PLACEHOLDER_PATTERN.test(
|
|
4119
|
+
const line2 = lines[i] ?? "";
|
|
4120
|
+
if (PLACEHOLDER_PATTERN.test(line2)) {
|
|
3499
4121
|
failures.push(`${rel}:${i + 1}: TODO or placeholder in shipped content`);
|
|
3500
4122
|
}
|
|
3501
|
-
if (RISKY_INSTALL_PATTERNS.some((re) => re.test(
|
|
4123
|
+
if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2))) {
|
|
3502
4124
|
failures.push(`${rel}:${i + 1}: risky install pattern`);
|
|
3503
4125
|
}
|
|
3504
|
-
if (ANY_NPX_PATTERN.test(
|
|
4126
|
+
if (ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
|
|
3505
4127
|
failures.push(`${rel}:${i + 1}: disallowed npx (only npx tsx allowed)`);
|
|
3506
4128
|
}
|
|
3507
4129
|
}
|
|
@@ -3510,8 +4132,8 @@ function auditMarkdownContent(manifestDir) {
|
|
|
3510
4132
|
return failures;
|
|
3511
4133
|
}
|
|
3512
4134
|
function walkMd(dir, fn) {
|
|
3513
|
-
for (const entry of
|
|
3514
|
-
const full =
|
|
4135
|
+
for (const entry of fs18.readdirSync(dir, { withFileTypes: true })) {
|
|
4136
|
+
const full = path28.join(dir, entry.name);
|
|
3515
4137
|
if (entry.isDirectory()) walkMd(full, fn);
|
|
3516
4138
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
3517
4139
|
}
|
|
@@ -3522,8 +4144,8 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3522
4144
|
process.exitCode = 1;
|
|
3523
4145
|
return;
|
|
3524
4146
|
}
|
|
3525
|
-
const abs =
|
|
3526
|
-
const manifestDir =
|
|
4147
|
+
const abs = path28.resolve(process.cwd(), manifestPath);
|
|
4148
|
+
const manifestDir = path28.dirname(abs);
|
|
3527
4149
|
const data = await readJson(abs);
|
|
3528
4150
|
if (!data?.items) {
|
|
3529
4151
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -3535,17 +4157,7 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3535
4157
|
const stackFailures = auditForbiddenStacks(items);
|
|
3536
4158
|
const fileFailures = auditShippedFiles(manifestDir, items);
|
|
3537
4159
|
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
|
-
}
|
|
4160
|
+
const tagFailures = auditDisallowedTags(items);
|
|
3549
4161
|
const allFailures = [
|
|
3550
4162
|
...structureFailures,
|
|
3551
4163
|
...stackFailures,
|
|
@@ -3562,7 +4174,7 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3562
4174
|
}
|
|
3563
4175
|
|
|
3564
4176
|
// src/commands/workspace.ts
|
|
3565
|
-
import
|
|
4177
|
+
import path29 from "path";
|
|
3566
4178
|
import YAML from "yaml";
|
|
3567
4179
|
async function runWorkspace(action) {
|
|
3568
4180
|
if (action === "init") {
|
|
@@ -3595,7 +4207,7 @@ relationships: []
|
|
|
3595
4207
|
const summaries = [];
|
|
3596
4208
|
const ownership = {};
|
|
3597
4209
|
for (const repo of repos) {
|
|
3598
|
-
const repoRoot =
|
|
4210
|
+
const repoRoot = path29.resolve(process.cwd(), repo.path);
|
|
3599
4211
|
const result = await scanProject(repoRoot, "fast");
|
|
3600
4212
|
summaries.push({
|
|
3601
4213
|
name: repo.name,
|
|
@@ -3604,9 +4216,9 @@ relationships: []
|
|
|
3604
4216
|
packageManager: result.packageManager,
|
|
3605
4217
|
deps: result.dependencies
|
|
3606
4218
|
});
|
|
3607
|
-
for (const
|
|
3608
|
-
ownership[
|
|
3609
|
-
ownership[
|
|
4219
|
+
for (const dep2 of result.dependencies) {
|
|
4220
|
+
ownership[dep2] ??= [];
|
|
4221
|
+
ownership[dep2].push(repo.name);
|
|
3610
4222
|
}
|
|
3611
4223
|
}
|
|
3612
4224
|
await writeJson(".haus-workflow/workspace-summary.json", {
|
|
@@ -3631,7 +4243,7 @@ ${summaries.map(
|
|
|
3631
4243
|
// src/cli.ts
|
|
3632
4244
|
function cliVersion() {
|
|
3633
4245
|
try {
|
|
3634
|
-
const pkgPath =
|
|
4246
|
+
const pkgPath = path30.join(packageRoot(), "package.json");
|
|
3635
4247
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
3636
4248
|
return pkg.version ?? "0.0.0";
|
|
3637
4249
|
} catch {
|
|
@@ -3641,7 +4253,7 @@ function cliVersion() {
|
|
|
3641
4253
|
var program = new Command();
|
|
3642
4254
|
function validateRuntimeNodeVersion() {
|
|
3643
4255
|
try {
|
|
3644
|
-
const pkgPath =
|
|
4256
|
+
const pkgPath = path30.join(packageRoot(), "package.json");
|
|
3645
4257
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
3646
4258
|
const requiredRange = pkg.engines?.node;
|
|
3647
4259
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -3662,6 +4274,9 @@ program.command("doctor").option("--hooks", "Verify .claude/settings.json matche
|
|
|
3662
4274
|
program.command("apply").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option(
|
|
3663
4275
|
"--allow-empty-cache",
|
|
3664
4276
|
"Apply core files only when catalog cache is empty (skip catalog items without error)"
|
|
4277
|
+
).option(
|
|
4278
|
+
"--refill-config",
|
|
4279
|
+
"Fill still-blank fields in an existing workflow-config.md without touching edited ones"
|
|
3665
4280
|
).action(runApply);
|
|
3666
4281
|
program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
|
|
3667
4282
|
program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
|
|
@@ -3671,20 +4286,15 @@ program.command("refresh").action(runRefresh);
|
|
|
3671
4286
|
program.command("catalog-audit").action(runCatalogAudit);
|
|
3672
4287
|
program.command("validate-catalog").argument("[manifest]").action(runValidateCatalog);
|
|
3673
4288
|
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);
|
|
4289
|
+
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
4290
|
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
4291
|
var guard = program.command("guard");
|
|
3682
4292
|
guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
|
|
3683
4293
|
guard.command("bash").option("--from-hook").action((opts) => runGuard("bash", opts));
|
|
3684
4294
|
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
|
|
4295
|
+
config.command("enable <key>").description("Enable a hook (hook.context)").action((key) => runConfig(key, "enable"));
|
|
4296
|
+
config.command("disable <key>").description("Disable a hook (hook.context)").action((key) => runConfig(key, "disable"));
|
|
4297
|
+
config.command("status <key>").description("Show current state of a hook (hook.context)").action((key) => runConfig(key, "status"));
|
|
3688
4298
|
var workspace = program.command("workspace");
|
|
3689
4299
|
workspace.command("init").action(() => runWorkspace("init"));
|
|
3690
4300
|
workspace.command("scan").action(() => runWorkspace("scan"));
|