@haus-tech/haus-workflow 0.10.0 → 0.11.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 +140 -123
- package/README.md +2 -4
- package/dist/cli.js +389 -208
- package/library/catalog/manifest.json +269 -499
- package/library/catalog/sources.yaml +56 -56
- package/library/global/agents/haus-code-reviewer.md +3 -2
- package/library/global/agents/haus-docs-researcher.md +3 -2
- package/library/global/agents/haus-planner.md +3 -2
- package/library/global/agents/haus-security-reviewer.md +3 -2
- package/library/global/agents/haus-test-reviewer.md +3 -2
- package/library/global/skills/haus-workflow/SKILL.md +21 -14
- package/library/global/templates/agentic-workflow-standard.md +279 -0
- package/package.json +3 -5
- package/tests/README.md +19 -19
- package/tests/fixtures/catalog/manifest.json +111 -78
- package/library/global/templates/haus-way-of-work.md +0 -40
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 path28 from "path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/commands/apply.ts
|
|
9
|
-
import
|
|
9
|
+
import path11 from "path";
|
|
10
10
|
import checkbox from "@inquirer/checkbox";
|
|
11
11
|
|
|
12
12
|
// src/catalog/remote-catalog.ts
|
|
@@ -70,8 +70,12 @@ async function syncRemoteCatalog() {
|
|
|
70
70
|
return { newItems: [], unchanged: 0, failed: [] };
|
|
71
71
|
}
|
|
72
72
|
await fs.ensureDir(CACHE_DIR);
|
|
73
|
-
await fs.writeFile(
|
|
74
|
-
|
|
73
|
+
await fs.writeFile(
|
|
74
|
+
path.join(CACHE_DIR, "manifest.json"),
|
|
75
|
+
`${JSON.stringify({ items }, null, 2)}
|
|
76
|
+
`,
|
|
77
|
+
"utf8"
|
|
78
|
+
);
|
|
75
79
|
const newItems = [];
|
|
76
80
|
let unchanged = 0;
|
|
77
81
|
const failed = [];
|
|
@@ -154,8 +158,8 @@ async function getCacheManifestAge() {
|
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
// src/claude/write-claude-files.ts
|
|
157
|
-
import
|
|
158
|
-
import
|
|
161
|
+
import path10 from "path";
|
|
162
|
+
import fs9 from "fs-extra";
|
|
159
163
|
|
|
160
164
|
// src/update/hash-installed.ts
|
|
161
165
|
import path3 from "path";
|
|
@@ -374,10 +378,14 @@ import fs4 from "fs-extra";
|
|
|
374
378
|
async function assertPostApplySettingsMatchCanonical(root, canonical) {
|
|
375
379
|
const written = await readJson(claudePath(root, "settings.json"));
|
|
376
380
|
if (written == null || typeof written !== "object") {
|
|
377
|
-
throw new Error(
|
|
381
|
+
throw new Error(
|
|
382
|
+
"haus: post-apply self-check failed: .claude/settings.json missing or unreadable"
|
|
383
|
+
);
|
|
378
384
|
}
|
|
379
385
|
if (!isDeepStrictEqual(canonical, written)) {
|
|
380
|
-
throw new Error(
|
|
386
|
+
throw new Error(
|
|
387
|
+
"haus: post-apply self-check failed: .claude/settings.json does not match canonical hook contract"
|
|
388
|
+
);
|
|
381
389
|
}
|
|
382
390
|
}
|
|
383
391
|
async function verifyProjectSettingsHooksContract(root) {
|
|
@@ -504,7 +512,8 @@ import path7 from "path";
|
|
|
504
512
|
import fs6 from "fs-extra";
|
|
505
513
|
var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
|
|
506
514
|
var BLOCK_END = "<!-- HAUS:END haus-imports -->";
|
|
507
|
-
var IMPORT_CONTENT = `@.haus-workflow/
|
|
515
|
+
var IMPORT_CONTENT = `@.haus-workflow/WORKFLOW.md
|
|
516
|
+
@.haus-workflow/workflow-config.md
|
|
508
517
|
@.haus-workflow/project.md`;
|
|
509
518
|
function buildImportBlock() {
|
|
510
519
|
return `${BLOCK_BEGIN}
|
|
@@ -554,16 +563,52 @@ async function writeRootClaudeMd(root, dryRun) {
|
|
|
554
563
|
return filePath;
|
|
555
564
|
}
|
|
556
565
|
|
|
557
|
-
// src/claude/write-
|
|
558
|
-
import os3 from "os";
|
|
566
|
+
// src/claude/write-workflow-config.ts
|
|
559
567
|
import path8 from "path";
|
|
560
568
|
import fs7 from "fs-extra";
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
569
|
+
function buildWorkflowConfig(ctx) {
|
|
570
|
+
const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
|
|
571
|
+
const testCmd = pm + " test";
|
|
572
|
+
const auditCmd = pm + " audit";
|
|
573
|
+
return "# Project workflow configuration\n\n> Project-specific values for the workflow standard in WORKFLOW.md.\n> Edit freely \u2014 this file is project-owned and will not be overwritten by haus.\n\n## Source-of-truth documents\n- Spec: <!-- fill in path, e.g. docs/SPEC.md -->\n- Design: <!-- fill in path, e.g. docs/DESIGN.md -->\n- UX flows: <!-- fill in path, e.g. docs/UX.md -->\n\n## Commands\n- Test (unit + integration): `" + testCmd + "`\n- Test (E2E): <!-- fill in command -->\n- Type check: <!-- fill in command, e.g. tsc --noEmit -->\n- Lint: <!-- fill in command, e.g. npm run lint -->\n- Lint fix: <!-- fill in command, e.g. npm run lint -- --fix -->\n- Format check: <!-- fill in command, e.g. prettier --check . -->\n- Security audit: `" + auditCmd + "`\n\n## Validation library\n<!-- fill in, e.g. zod, yup, joi -->\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<!-- fill in, e.g. lefthook, husky -->\n";
|
|
574
|
+
}
|
|
575
|
+
async function writeWorkflowConfig(root, dryRun) {
|
|
576
|
+
const destPath = hausPath(root, "workflow-config.md");
|
|
577
|
+
const printable = displayPath(root, destPath);
|
|
578
|
+
if (await fs7.pathExists(destPath)) {
|
|
579
|
+
if (dryRun) log(printable + ": exists (project-owned, skipping)");
|
|
580
|
+
return destPath;
|
|
581
|
+
}
|
|
582
|
+
const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
|
|
583
|
+
mode: "fast",
|
|
584
|
+
generatedAt: "",
|
|
585
|
+
root,
|
|
586
|
+
repoName: path8.basename(root),
|
|
587
|
+
packageManager: "unknown",
|
|
588
|
+
repoRoles: [],
|
|
589
|
+
confidence: 0,
|
|
590
|
+
detectedStacks: {},
|
|
591
|
+
dependencies: [],
|
|
592
|
+
securityRisks: [],
|
|
593
|
+
crossRepoHints: [],
|
|
594
|
+
warnings: []
|
|
595
|
+
};
|
|
596
|
+
const content = buildWorkflowConfig(ctx);
|
|
597
|
+
if (dryRun) {
|
|
598
|
+
log(printable + ": would create");
|
|
599
|
+
return destPath;
|
|
600
|
+
}
|
|
601
|
+
await writeText(destPath, content);
|
|
602
|
+
return destPath;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/claude/write-workflow.ts
|
|
606
|
+
import path9 from "path";
|
|
607
|
+
import fs8 from "fs-extra";
|
|
608
|
+
|
|
609
|
+
// src/claude/managed-template.ts
|
|
610
|
+
function normaliseLF(content) {
|
|
611
|
+
return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
567
612
|
}
|
|
568
613
|
function parseHausManagedHeader(line) {
|
|
569
614
|
const match = line.match(/<!-- HAUS-MANAGED id=([\w.:-]+)/);
|
|
@@ -571,23 +616,32 @@ function parseHausManagedHeader(line) {
|
|
|
571
616
|
const hashMatch = line.match(/hash=(sha256-[a-f0-9]+)/);
|
|
572
617
|
return { id: match[1], hash: hashMatch?.[1] };
|
|
573
618
|
}
|
|
574
|
-
|
|
619
|
+
|
|
620
|
+
// src/claude/write-workflow.ts
|
|
621
|
+
var STABLE_ID2 = "template.workflow";
|
|
622
|
+
var SCHEMA_VERSION2 = "1";
|
|
623
|
+
var TEMPLATE_REL = "library/global/templates/agentic-workflow-standard.md";
|
|
624
|
+
var CATALOG_CACHE_TEMPLATE = path9.join(CACHE_DIR, "templates/agentic-workflow-standard.md");
|
|
625
|
+
function makeWorkflowHeader(pkgVersion, contentHash) {
|
|
626
|
+
return `<!-- HAUS-MANAGED id=${STABLE_ID2} v=${SCHEMA_VERSION2} source=@haus-tech/haus-workflow@${pkgVersion} hash=${contentHash} -->`;
|
|
627
|
+
}
|
|
628
|
+
async function writeWorkflow(root, pkgVersion, dryRun) {
|
|
575
629
|
const cachePath = CATALOG_CACHE_TEMPLATE;
|
|
576
|
-
const packagePath =
|
|
577
|
-
const templatePath = await
|
|
578
|
-
if (!await
|
|
579
|
-
warn(`
|
|
630
|
+
const packagePath = path9.join(packageRoot(), TEMPLATE_REL);
|
|
631
|
+
const templatePath = await fs8.pathExists(cachePath) ? cachePath : packagePath;
|
|
632
|
+
if (!await fs8.pathExists(templatePath)) {
|
|
633
|
+
warn(`Workflow template not found \u2014 run \`haus update\` to fetch from catalog`);
|
|
580
634
|
return null;
|
|
581
635
|
}
|
|
582
|
-
const templateContent = await
|
|
583
|
-
const contentHash = hashText(templateContent);
|
|
584
|
-
const header =
|
|
636
|
+
const templateContent = await fs8.readFile(templatePath, "utf8");
|
|
637
|
+
const contentHash = hashText(normaliseLF(templateContent));
|
|
638
|
+
const header = makeWorkflowHeader(pkgVersion, contentHash);
|
|
585
639
|
const next = `${header}
|
|
586
640
|
${templateContent}`;
|
|
587
|
-
const destPath = hausPath(root, "
|
|
641
|
+
const destPath = hausPath(root, "WORKFLOW.md");
|
|
588
642
|
const printable = displayPath(root, destPath);
|
|
589
|
-
if (await
|
|
590
|
-
const existing = await
|
|
643
|
+
if (await fs8.pathExists(destPath)) {
|
|
644
|
+
const existing = await fs8.readFile(destPath, "utf8");
|
|
591
645
|
const firstLine = existing.split("\n")[0] ?? "";
|
|
592
646
|
const parsed = parseHausManagedHeader(firstLine);
|
|
593
647
|
if (!parsed) {
|
|
@@ -599,7 +653,7 @@ ${templateContent}`;
|
|
|
599
653
|
return null;
|
|
600
654
|
}
|
|
601
655
|
const existingContent = existing.slice(firstLine.length + 1);
|
|
602
|
-
if (parsed.hash && hashText(existingContent) !== parsed.hash) {
|
|
656
|
+
if (parsed.hash && hashText(normaliseLF(existingContent)) !== parsed.hash) {
|
|
603
657
|
warn(`${printable}: content modified by user \u2014 skipping. Use --force to overwrite.`);
|
|
604
658
|
return null;
|
|
605
659
|
}
|
|
@@ -609,7 +663,7 @@ ${templateContent}`;
|
|
|
609
663
|
}
|
|
610
664
|
}
|
|
611
665
|
if (dryRun) {
|
|
612
|
-
const prev = await
|
|
666
|
+
const prev = await fs8.pathExists(destPath) ? await fs8.readFile(destPath, "utf8") : "";
|
|
613
667
|
if (!prev) {
|
|
614
668
|
log(createUnifiedDiff(printable, "", next));
|
|
615
669
|
} else {
|
|
@@ -636,7 +690,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
636
690
|
estimatedTokenReductionPct: 0
|
|
637
691
|
};
|
|
638
692
|
const pkgRoot = packageRoot();
|
|
639
|
-
const hausVersion = (await readJson(
|
|
693
|
+
const hausVersion = (await readJson(path10.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
|
|
640
694
|
const coreFiles = [
|
|
641
695
|
claudePath(root, "settings.json"),
|
|
642
696
|
claudePath(root, "rules", "haus.md"),
|
|
@@ -645,18 +699,34 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
645
699
|
claudePath(root, "commands", "haus-review.md")
|
|
646
700
|
];
|
|
647
701
|
const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun);
|
|
648
|
-
const
|
|
702
|
+
const workflowPath = await writeWorkflow(root, hausVersion, dryRun);
|
|
703
|
+
const workflowConfigPath = await writeWorkflowConfig(root, dryRun);
|
|
649
704
|
const projectFactsPath = await writeProjectFacts(root, hausVersion, dryRun);
|
|
650
|
-
const p6Files = [
|
|
651
|
-
|
|
705
|
+
const p6Files = [
|
|
706
|
+
rootClaudeMdPath,
|
|
707
|
+
projectFactsPath,
|
|
708
|
+
...workflowPath ? [workflowPath] : [],
|
|
709
|
+
...workflowConfigPath ? [workflowConfigPath] : []
|
|
710
|
+
];
|
|
711
|
+
const files = dryRun ? [...coreFiles, ...p6Files] : [
|
|
712
|
+
...coreFiles,
|
|
713
|
+
...p6Files,
|
|
714
|
+
hausPath(root, "selected-context.json"),
|
|
715
|
+
hausPath(root, "haus.lock.json")
|
|
716
|
+
];
|
|
652
717
|
const hookSettings = await loadClaudeHooksSettings();
|
|
653
718
|
await writeManagedJson(root, claudePath(root, "settings.json"), hookSettings, dryRun);
|
|
654
719
|
if (!dryRun) await assertPostApplySettingsMatchCanonical(root, hookSettings);
|
|
655
720
|
const configPath = hausPath(root, "config.json");
|
|
656
|
-
if (!await
|
|
721
|
+
if (!await fs9.pathExists(configPath)) {
|
|
657
722
|
await writeManagedJson(root, configPath, DEFAULT_HOOKS_CONFIG, dryRun);
|
|
658
723
|
}
|
|
659
|
-
await writeManagedText(
|
|
724
|
+
await writeManagedText(
|
|
725
|
+
root,
|
|
726
|
+
claudePath(root, "commands", "haus-doctor.md"),
|
|
727
|
+
"Run `haus doctor`.",
|
|
728
|
+
dryRun
|
|
729
|
+
);
|
|
660
730
|
await writeManagedText(
|
|
661
731
|
root,
|
|
662
732
|
claudePath(root, "commands", "haus-review.md"),
|
|
@@ -676,11 +746,13 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
676
746
|
dryRun
|
|
677
747
|
);
|
|
678
748
|
const fixtureManifestPath = process.env["HAUS_FIXTURE_CATALOG"];
|
|
679
|
-
const manifestPath = fixtureManifestPath ??
|
|
680
|
-
const manifestDir =
|
|
749
|
+
const manifestPath = fixtureManifestPath ?? path10.join(pkgRoot, "library", "catalog", "manifest.json");
|
|
750
|
+
const manifestDir = path10.dirname(manifestPath);
|
|
681
751
|
const manifest = await readJson(manifestPath) ?? { items: [] };
|
|
682
752
|
const manifestById = new Map((manifest.items ?? []).map((item) => [item.id, item]));
|
|
683
|
-
const cacheManifest = await readJson(
|
|
753
|
+
const cacheManifest = await readJson(
|
|
754
|
+
path10.join(CACHE_DIR, "manifest.json")
|
|
755
|
+
);
|
|
684
756
|
const cacheManifestById = new Map((cacheManifest?.items ?? []).map((item) => [item.id, item]));
|
|
685
757
|
const installedPathsByItem = /* @__PURE__ */ new Map();
|
|
686
758
|
const installedIds = /* @__PURE__ */ new Set();
|
|
@@ -701,24 +773,28 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
701
773
|
}
|
|
702
774
|
}
|
|
703
775
|
const cachedItem = cacheManifestById.get(item.id);
|
|
704
|
-
const cachePath = cachedItem?.path ?
|
|
705
|
-
const sourcePath = cachePath && await
|
|
776
|
+
const cachePath = cachedItem?.path ? path10.join(CACHE_DIR, cachedItem.path) : null;
|
|
777
|
+
const sourcePath = cachePath && await fs9.pathExists(cachePath) ? cachePath : path10.join(manifestDir, manifestItem.path);
|
|
706
778
|
const target = item.type === "agent" ? "agents" : item.type === "template" ? "templates" : "skills";
|
|
707
|
-
const destination = claudePath(root, target,
|
|
708
|
-
if (await
|
|
779
|
+
const destination = claudePath(root, target, path10.basename(sourcePath));
|
|
780
|
+
if (await fs9.pathExists(sourcePath)) {
|
|
709
781
|
if (dryRun) {
|
|
710
|
-
const exists = await
|
|
711
|
-
log(
|
|
782
|
+
const exists = await fs9.pathExists(destination);
|
|
783
|
+
log(
|
|
784
|
+
`${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`
|
|
785
|
+
);
|
|
712
786
|
} else {
|
|
713
|
-
await
|
|
714
|
-
await
|
|
787
|
+
await fs9.ensureDir(path10.dirname(destination));
|
|
788
|
+
await fs9.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
715
789
|
}
|
|
716
790
|
files.push(destination);
|
|
717
791
|
const current = installedPathsByItem.get(item.id) ?? [];
|
|
718
|
-
installedPathsByItem.set(item.id, [...current,
|
|
792
|
+
installedPathsByItem.set(item.id, [...current, path10.relative(root, destination)]);
|
|
719
793
|
installedIds.add(item.id);
|
|
720
794
|
} else {
|
|
721
|
-
warn(
|
|
795
|
+
warn(
|
|
796
|
+
`Skipping ${item.id}: source not found at ${sourcePath} \u2014 run \`haus update\` to populate catalog cache`
|
|
797
|
+
);
|
|
722
798
|
}
|
|
723
799
|
}
|
|
724
800
|
if (dryRun) return [...new Set(files)];
|
|
@@ -726,7 +802,12 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
726
802
|
await writeManagedJson(
|
|
727
803
|
root,
|
|
728
804
|
hausPath(root, "selected-context.json"),
|
|
729
|
-
installedItems.map((r) => ({
|
|
805
|
+
installedItems.map((r) => ({
|
|
806
|
+
id: r.id,
|
|
807
|
+
type: r.type,
|
|
808
|
+
reason: r.reason,
|
|
809
|
+
confidenceLevel: r.confidenceLevel
|
|
810
|
+
})),
|
|
730
811
|
false
|
|
731
812
|
);
|
|
732
813
|
const lock = await Promise.all(
|
|
@@ -759,7 +840,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
759
840
|
return [...new Set(files)];
|
|
760
841
|
}
|
|
761
842
|
async function writeManagedText(root, filePath, nextText, dryRun) {
|
|
762
|
-
const prev = await
|
|
843
|
+
const prev = await fs9.pathExists(filePath) ? await fs9.readFile(filePath, "utf8") : "";
|
|
763
844
|
const printable = displayPath(root, filePath);
|
|
764
845
|
if (dryRun) {
|
|
765
846
|
if (!prev) {
|
|
@@ -786,7 +867,7 @@ async function writeManagedJson(root, filePath, value, dryRun) {
|
|
|
786
867
|
|
|
787
868
|
// src/commands/apply.ts
|
|
788
869
|
async function cacheHasItems() {
|
|
789
|
-
const data = await readJson(
|
|
870
|
+
const data = await readJson(path11.join(CACHE_DIR, "manifest.json"));
|
|
790
871
|
return Array.isArray(data?.items) && data.items.length > 0;
|
|
791
872
|
}
|
|
792
873
|
async function runApply(options) {
|
|
@@ -831,7 +912,9 @@ async function runApply(options) {
|
|
|
831
912
|
const catalogItemCount = selectedIds !== void 0 ? selectedIds.length : rec?.recommended.length ?? 0;
|
|
832
913
|
if (catalogItemCount > 0 && !await cacheHasItems()) {
|
|
833
914
|
if (isDryRun) {
|
|
834
|
-
warn(
|
|
915
|
+
warn(
|
|
916
|
+
"Catalog cache is empty \u2014 `haus apply --write` will skip catalog items. Run `haus update` first."
|
|
917
|
+
);
|
|
835
918
|
} else {
|
|
836
919
|
error(
|
|
837
920
|
"Catalog cache is empty \u2014 cannot install catalog items. Run `haus update` first, or pass --allow-empty-cache to apply core files only."
|
|
@@ -851,9 +934,9 @@ async function runApply(options) {
|
|
|
851
934
|
}
|
|
852
935
|
|
|
853
936
|
// src/catalog/load-catalog.ts
|
|
854
|
-
import
|
|
855
|
-
import
|
|
856
|
-
var CACHE_MANIFEST =
|
|
937
|
+
import os3 from "os";
|
|
938
|
+
import path12 from "path";
|
|
939
|
+
var CACHE_MANIFEST = path12.join(os3.homedir(), CATALOG_CACHE_SUBDIR, "manifest.json");
|
|
857
940
|
async function loadCatalog(root) {
|
|
858
941
|
const envPath = process.env["HAUS_FIXTURE_CATALOG"];
|
|
859
942
|
if (envPath) {
|
|
@@ -862,10 +945,10 @@ async function loadCatalog(root) {
|
|
|
862
945
|
}
|
|
863
946
|
const cacheData = await readJson(CACHE_MANIFEST);
|
|
864
947
|
if (cacheData?.items?.length) return cacheData.items;
|
|
865
|
-
const localManifest =
|
|
948
|
+
const localManifest = path12.join(root, "library/catalog/manifest.json");
|
|
866
949
|
const localData = await readJson(localManifest);
|
|
867
950
|
if (localData?.items?.length) return localData.items;
|
|
868
|
-
const packageManifest =
|
|
951
|
+
const packageManifest = path12.join(packageRoot(), "library/catalog/manifest.json");
|
|
869
952
|
const data = await readJson(packageManifest);
|
|
870
953
|
return data?.items ?? [];
|
|
871
954
|
}
|
|
@@ -893,7 +976,8 @@ async function runCatalogAudit() {
|
|
|
893
976
|
const failures = [];
|
|
894
977
|
for (const item of items) {
|
|
895
978
|
const text = `${item.id} ${item.tags.join(" ")}`.toLowerCase();
|
|
896
|
-
for (const word of FORBIDDEN)
|
|
979
|
+
for (const word of FORBIDDEN)
|
|
980
|
+
if (text.includes(word)) failures.push(`${item.id} has unsupported tag ${word}`);
|
|
897
981
|
}
|
|
898
982
|
if (failures.length) {
|
|
899
983
|
failures.forEach((f) => error(f));
|
|
@@ -904,7 +988,7 @@ async function runCatalogAudit() {
|
|
|
904
988
|
}
|
|
905
989
|
|
|
906
990
|
// src/commands/config.ts
|
|
907
|
-
import
|
|
991
|
+
import path13 from "path";
|
|
908
992
|
var CONFIG_PATH2 = ".haus-workflow/config.json";
|
|
909
993
|
var HOOK_ALIASES = {
|
|
910
994
|
"hook.context": "context",
|
|
@@ -913,10 +997,12 @@ var HOOK_ALIASES = {
|
|
|
913
997
|
async function runConfig(key, action) {
|
|
914
998
|
const hookKey = HOOK_ALIASES[key];
|
|
915
999
|
if (!hookKey) {
|
|
916
|
-
throw new Error(
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
`Unknown config key "${key}". Valid keys: ${Object.keys(HOOK_ALIASES).join(", ")}`
|
|
1002
|
+
);
|
|
917
1003
|
}
|
|
918
1004
|
const root = process.cwd();
|
|
919
|
-
const configPath =
|
|
1005
|
+
const configPath = path13.join(root, CONFIG_PATH2);
|
|
920
1006
|
const existing = await readJson(configPath);
|
|
921
1007
|
const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
|
|
922
1008
|
cfg.hooks ??= {};
|
|
@@ -939,7 +1025,9 @@ function normalizeRecommendation(input2) {
|
|
|
939
1025
|
message: reason.message ?? item.reason ?? "legacy recommendation reason",
|
|
940
1026
|
weight: reason.weight ?? 0,
|
|
941
1027
|
...reason.signal ? { signal: reason.signal } : {}
|
|
942
|
-
})) ?? [
|
|
1028
|
+
})) ?? [
|
|
1029
|
+
{ code: "legacy-reason", message: item.reason ?? "legacy recommendation reason", weight: 0 }
|
|
1030
|
+
];
|
|
943
1031
|
const confidence = item.confidence ?? 0;
|
|
944
1032
|
return {
|
|
945
1033
|
id: item.id,
|
|
@@ -984,7 +1072,10 @@ function normalizeRecommendation(input2) {
|
|
|
984
1072
|
estimatedContextTokens: input2.estimatedContextTokens ?? recommended.length * 320,
|
|
985
1073
|
selectedRules: input2.selectedRules ?? recommended.length,
|
|
986
1074
|
skippedRules: input2.skippedRules ?? skipped.length,
|
|
987
|
-
estimatedTokenReductionPct: input2.estimatedTokenReductionPct ?? Math.max(
|
|
1075
|
+
estimatedTokenReductionPct: input2.estimatedTokenReductionPct ?? Math.max(
|
|
1076
|
+
0,
|
|
1077
|
+
Math.round(skipped.length / Math.max(recommended.length + skipped.length, 1) * 100)
|
|
1078
|
+
)
|
|
988
1079
|
};
|
|
989
1080
|
}
|
|
990
1081
|
function buildRecommendationExplanation(recommendation) {
|
|
@@ -1269,7 +1360,7 @@ function computeRuleIntents(rule) {
|
|
|
1269
1360
|
|
|
1270
1361
|
// src/scanner/scan-project.ts
|
|
1271
1362
|
import { readFile } from "fs/promises";
|
|
1272
|
-
import
|
|
1363
|
+
import path15 from "path";
|
|
1273
1364
|
|
|
1274
1365
|
// src/utils/audit-checks.ts
|
|
1275
1366
|
function isRecord(v) {
|
|
@@ -1296,8 +1387,8 @@ function compareVersions(a, b) {
|
|
|
1296
1387
|
}
|
|
1297
1388
|
|
|
1298
1389
|
// src/scanner/detect-package-manager.ts
|
|
1299
|
-
import
|
|
1300
|
-
import
|
|
1390
|
+
import path14 from "path";
|
|
1391
|
+
import fs10 from "fs-extra";
|
|
1301
1392
|
function detectPackageManager(root, packageManagerField) {
|
|
1302
1393
|
const field = String(packageManagerField ?? "").trim();
|
|
1303
1394
|
if (field.startsWith("yarn@")) {
|
|
@@ -1315,9 +1406,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
1315
1406
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
1316
1407
|
return "unknown";
|
|
1317
1408
|
}
|
|
1318
|
-
if (
|
|
1319
|
-
if (
|
|
1320
|
-
if (
|
|
1409
|
+
if (fs10.existsSync(path14.join(root, "yarn.lock"))) return "yarn";
|
|
1410
|
+
if (fs10.existsSync(path14.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
1411
|
+
if (fs10.existsSync(path14.join(root, "package-lock.json"))) return "npm";
|
|
1321
1412
|
return "unknown";
|
|
1322
1413
|
}
|
|
1323
1414
|
|
|
@@ -1378,8 +1469,8 @@ function blocked(rel) {
|
|
|
1378
1469
|
return SENSITIVE.some((x) => x.test(rel));
|
|
1379
1470
|
}
|
|
1380
1471
|
async function scanProject(root, mode = "fast") {
|
|
1381
|
-
const pkg = await readJson(
|
|
1382
|
-
const composer = await readJson(
|
|
1472
|
+
const pkg = await readJson(path15.join(root, "package.json"));
|
|
1473
|
+
const composer = await readJson(path15.join(root, "composer.json"));
|
|
1383
1474
|
const files = await listFiles(root, SAFE_FILES);
|
|
1384
1475
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
1385
1476
|
const deps = dependencySet(pkg, composer);
|
|
@@ -1396,16 +1487,18 @@ async function scanProject(root, mode = "fast") {
|
|
|
1396
1487
|
if (nodeEngine && !satisfiesVersion(process.version, nodeEngine)) {
|
|
1397
1488
|
warnings.push(`Current Node ${process.version} does not satisfy package engine ${nodeEngine}`);
|
|
1398
1489
|
}
|
|
1399
|
-
if (safeFiles.some((f) => f.includes("docker-compose")))
|
|
1490
|
+
if (safeFiles.some((f) => f.includes("docker-compose")))
|
|
1491
|
+
crossRepoHints.push("Containerized services detected");
|
|
1400
1492
|
if (safeFiles.some((f) => f.includes("turbo.json") || f.includes("nx.json")))
|
|
1401
1493
|
crossRepoHints.push("Monorepo orchestration detected");
|
|
1402
1494
|
if (!safeFiles.some((f) => f.endsWith(".env.example"))) securityRisks.push("Missing env template");
|
|
1403
|
-
if (safeFiles.some((f) => f.includes("wp-content/uploads")))
|
|
1495
|
+
if (safeFiles.some((f) => f.includes("wp-content/uploads")))
|
|
1496
|
+
securityRisks.push("Uploads directory present");
|
|
1404
1497
|
const context = {
|
|
1405
1498
|
mode,
|
|
1406
1499
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1407
1500
|
root,
|
|
1408
|
-
repoName: String(pkg?.name ??
|
|
1501
|
+
repoName: String(pkg?.name ?? path15.basename(root)),
|
|
1409
1502
|
packageManager,
|
|
1410
1503
|
repoRoles: roles,
|
|
1411
1504
|
confidence: computeConfidence(roles, stacks),
|
|
@@ -1420,7 +1513,11 @@ async function scanProject(root, mode = "fast") {
|
|
|
1420
1513
|
composer: isRecord(composer?.require) ? Object.keys(composer.require) : []
|
|
1421
1514
|
};
|
|
1422
1515
|
const scanHashes = Object.fromEntries(
|
|
1423
|
-
await Promise.all(
|
|
1516
|
+
await Promise.all(
|
|
1517
|
+
safeFiles.map(
|
|
1518
|
+
async (f) => [f, hashText(await readFile(path15.join(root, f), "utf8"))]
|
|
1519
|
+
)
|
|
1520
|
+
)
|
|
1424
1521
|
);
|
|
1425
1522
|
const repoSummary = renderSummary(context);
|
|
1426
1523
|
await writeJson(hausPath(root, "context-map.json"), context);
|
|
@@ -1446,9 +1543,11 @@ function detectRoles(deps, files) {
|
|
|
1446
1543
|
if (deps.includes("next") || files.some((f) => f.includes("next.config."))) roles.add("next-app");
|
|
1447
1544
|
if (deps.includes("react")) roles.add("react-app");
|
|
1448
1545
|
if (deps.includes("vite") || files.some((f) => f.includes("vite.config."))) roles.add("vite-app");
|
|
1449
|
-
if (deps.includes("react-router") && deps.includes("@react-router/node"))
|
|
1546
|
+
if (deps.includes("react-router") && deps.includes("@react-router/node"))
|
|
1547
|
+
roles.add("react-router-app");
|
|
1450
1548
|
if (deps.includes("sanity")) roles.add("sanity-studio");
|
|
1451
|
-
if (deps.includes("@strapi/strapi") || deps.some((d) => d.startsWith("@strapi/")))
|
|
1549
|
+
if (deps.includes("@strapi/strapi") || deps.some((d) => d.startsWith("@strapi/")))
|
|
1550
|
+
roles.add("strapi-app");
|
|
1452
1551
|
if (deps.includes("expo")) roles.add("expo-app");
|
|
1453
1552
|
if (deps.includes("@vendure/core")) roles.add("vendure-app");
|
|
1454
1553
|
if (deps.some((d) => d.startsWith("@haus/vendure-")) || files.some((f) => f.includes("vendure-config")))
|
|
@@ -1457,7 +1556,8 @@ function detectRoles(deps, files) {
|
|
|
1457
1556
|
if (deps.includes("graphql") || deps.includes("@nestjs/graphql")) roles.add("graphql-api");
|
|
1458
1557
|
if (files.some((f) => f.endsWith("nx.json"))) roles.add("nx-monorepo");
|
|
1459
1558
|
if (files.some((f) => f.endsWith("turbo.json"))) roles.add("turbo-monorepo");
|
|
1460
|
-
if (files.some((f) => f.endsWith("artisan")) || deps.includes("laravel/framework"))
|
|
1559
|
+
if (files.some((f) => f.endsWith("artisan")) || deps.includes("laravel/framework"))
|
|
1560
|
+
roles.add("laravel-app");
|
|
1461
1561
|
if (deps.includes("laravel/nova")) roles.add("laravel-nova-app");
|
|
1462
1562
|
const hasWpConfig = files.some((f) => f.endsWith("wp-config.php"));
|
|
1463
1563
|
const hasBedrockLayout = files.some((f) => f.includes("web/app")) || deps.includes("roots/wordpress");
|
|
@@ -1493,7 +1593,8 @@ async function detectStacks(root, deps, files, packageManager) {
|
|
|
1493
1593
|
if (deps.includes("react")) add("frontend", "react19");
|
|
1494
1594
|
if (deps.includes("vue")) add("frontend", "vue");
|
|
1495
1595
|
if (deps.includes("vite")) add("frontend", "vite8");
|
|
1496
|
-
if (deps.includes("react-router") && deps.includes("@react-router/node"))
|
|
1596
|
+
if (deps.includes("react-router") && deps.includes("@react-router/node"))
|
|
1597
|
+
add("frontend", "react-router-v7");
|
|
1497
1598
|
if (deps.includes("tailwindcss") || files.some((f) => f.includes("tailwind.config."))) {
|
|
1498
1599
|
add("frontend", "tailwindcss");
|
|
1499
1600
|
}
|
|
@@ -1512,8 +1613,10 @@ async function detectStacks(root, deps, files, packageManager) {
|
|
|
1512
1613
|
if (deps.includes("react-native")) add("frontend", "react-native");
|
|
1513
1614
|
if (deps.includes("i18next") || deps.includes("react-i18next")) add("tooling", "i18next");
|
|
1514
1615
|
if (deps.includes("bullmq")) add("tooling", "bullmq");
|
|
1515
|
-
if (files.some((f) => f === "Dockerfile" || f.startsWith("docker-compose")))
|
|
1516
|
-
|
|
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");
|
|
1517
1620
|
if (deps.some((d) => d.startsWith("@sentry/"))) add("tooling", "sentry");
|
|
1518
1621
|
if (deps.includes("deployer/deployer")) add("tooling", "deployer-php");
|
|
1519
1622
|
if (!deps.includes("prettier")) add("tooling", "missing-prettier");
|
|
@@ -1530,10 +1633,13 @@ async function detectStacks(root, deps, files, packageManager) {
|
|
|
1530
1633
|
if (await hasNeedle(root, files, "NestFactory")) add("backend", "nestjs");
|
|
1531
1634
|
if (await hasNeedle(root, files, "@VendurePlugin")) add("backend", "vendure3");
|
|
1532
1635
|
if (deps.includes("graphql") || deps.includes("@nestjs/graphql")) add("backend", "graphql");
|
|
1533
|
-
if (files.some((f) => f.endsWith(".graphql") || f.endsWith("schema.graphql")))
|
|
1636
|
+
if (files.some((f) => f.endsWith(".graphql") || f.endsWith("schema.graphql")))
|
|
1637
|
+
add("backend", "graphql");
|
|
1534
1638
|
if (deps.includes("laravel/framework")) add("backend", "laravel");
|
|
1535
|
-
if (files.some((f) => f.includes("app/Providers/") || f.includes("routes/")))
|
|
1536
|
-
|
|
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");
|
|
1537
1643
|
if (deps.includes("wpackagist-plugin/elementor") || deps.includes("wearehaus/elementor-pro") || deps.includes("wpackagist-theme/hello-elementor")) {
|
|
1538
1644
|
add("backend", "elementor");
|
|
1539
1645
|
}
|
|
@@ -1576,7 +1682,7 @@ async function hasNeedle(root, files, needle) {
|
|
|
1576
1682
|
);
|
|
1577
1683
|
for (const rel of candidates.slice(0, 300)) {
|
|
1578
1684
|
try {
|
|
1579
|
-
const content = await readFile(
|
|
1685
|
+
const content = await readFile(path15.join(root, rel), "utf8");
|
|
1580
1686
|
if (content.includes(needle)) return true;
|
|
1581
1687
|
} catch {
|
|
1582
1688
|
continue;
|
|
@@ -1669,16 +1775,19 @@ async function runContext(options) {
|
|
|
1669
1775
|
}
|
|
1670
1776
|
|
|
1671
1777
|
// src/commands/doctor.ts
|
|
1672
|
-
import
|
|
1673
|
-
import
|
|
1778
|
+
import path16 from "path";
|
|
1779
|
+
import fs11 from "fs-extra";
|
|
1674
1780
|
|
|
1675
1781
|
// src/update/npm-version.ts
|
|
1676
1782
|
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
1677
1783
|
async function fetchNpmVersionStatus(currentVersion) {
|
|
1678
1784
|
try {
|
|
1679
|
-
const res = await fetch(
|
|
1680
|
-
|
|
1681
|
-
|
|
1785
|
+
const res = await fetch(
|
|
1786
|
+
`https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE_NAME)}/latest`,
|
|
1787
|
+
{
|
|
1788
|
+
signal: AbortSignal.timeout(8e3)
|
|
1789
|
+
}
|
|
1790
|
+
);
|
|
1682
1791
|
if (!res.ok) return { current: currentVersion, latest: null, updateAvailable: false };
|
|
1683
1792
|
const data = await res.json();
|
|
1684
1793
|
const latest = data?.version;
|
|
@@ -1737,7 +1846,7 @@ async function runDoctor(options) {
|
|
|
1737
1846
|
const enabled = await isHookEnabled(root, key);
|
|
1738
1847
|
log(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
|
|
1739
1848
|
}
|
|
1740
|
-
const rootClaudeMdPath =
|
|
1849
|
+
const rootClaudeMdPath = path16.join(root, "CLAUDE.md");
|
|
1741
1850
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
1742
1851
|
if (!rootClaudeMdContent) {
|
|
1743
1852
|
warn("- CLAUDE.md: missing (run `haus apply --write` to create)");
|
|
@@ -1746,33 +1855,48 @@ async function runDoctor(options) {
|
|
|
1746
1855
|
} else {
|
|
1747
1856
|
log("- CLAUDE.md: import block present");
|
|
1748
1857
|
}
|
|
1749
|
-
const
|
|
1750
|
-
const
|
|
1751
|
-
if (!
|
|
1752
|
-
warn("- .haus-workflow/
|
|
1858
|
+
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
1859
|
+
const workflowExists = await fs11.pathExists(workflowPath);
|
|
1860
|
+
if (!workflowExists) {
|
|
1861
|
+
warn("- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)");
|
|
1753
1862
|
} else {
|
|
1754
|
-
const
|
|
1755
|
-
const firstLine =
|
|
1863
|
+
const workflowContent = await readText(workflowPath);
|
|
1864
|
+
const firstLine = workflowContent?.split("\n")[0] ?? "";
|
|
1756
1865
|
if (!firstLine.includes("HAUS-MANAGED")) {
|
|
1757
|
-
|
|
1866
|
+
log("- .haus-workflow/WORKFLOW.md: OK (user-owned)");
|
|
1758
1867
|
} else {
|
|
1759
1868
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
1760
|
-
const
|
|
1869
|
+
const cachePath = path16.join(CACHE_DIR, "templates/agentic-workflow-standard.md");
|
|
1870
|
+
const bundledPath = path16.join(
|
|
1871
|
+
packageRoot(),
|
|
1872
|
+
"library",
|
|
1873
|
+
"global",
|
|
1874
|
+
"templates",
|
|
1875
|
+
"agentic-workflow-standard.md"
|
|
1876
|
+
);
|
|
1877
|
+
const templatePath = await fs11.pathExists(cachePath) ? cachePath : bundledPath;
|
|
1761
1878
|
const templateContent = await readText(templatePath);
|
|
1762
1879
|
if (storedHashMatch && templateContent) {
|
|
1763
|
-
const currentHash = hashText(templateContent);
|
|
1880
|
+
const currentHash = hashText(normaliseLF(templateContent));
|
|
1764
1881
|
if (storedHashMatch[1] !== currentHash) {
|
|
1765
|
-
warn("- .haus-workflow/
|
|
1882
|
+
warn("- .haus-workflow/WORKFLOW.md: stale (template updated \u2014 run `haus apply --write`)");
|
|
1766
1883
|
} else {
|
|
1767
|
-
log("- .haus-workflow/
|
|
1884
|
+
log("- .haus-workflow/WORKFLOW.md: OK");
|
|
1768
1885
|
}
|
|
1769
1886
|
} else {
|
|
1770
|
-
log("- .haus-workflow/
|
|
1887
|
+
log("- .haus-workflow/WORKFLOW.md: OK");
|
|
1771
1888
|
}
|
|
1772
1889
|
}
|
|
1773
1890
|
}
|
|
1891
|
+
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
1892
|
+
const workflowConfigExists = await fs11.pathExists(workflowConfigPath);
|
|
1893
|
+
if (!workflowConfigExists) {
|
|
1894
|
+
warn("- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)");
|
|
1895
|
+
} else {
|
|
1896
|
+
log("- .haus-workflow/workflow-config.md: OK (project-owned)");
|
|
1897
|
+
}
|
|
1774
1898
|
const projectMdPath = hausPath(root, "project.md");
|
|
1775
|
-
const projectMdExists = await
|
|
1899
|
+
const projectMdExists = await fs11.pathExists(projectMdPath);
|
|
1776
1900
|
if (!projectMdExists) {
|
|
1777
1901
|
warn("- .haus-workflow/project.md: missing (run `haus apply --write`)");
|
|
1778
1902
|
} else {
|
|
@@ -1795,11 +1919,13 @@ async function runDoctor(options) {
|
|
|
1795
1919
|
log(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
|
|
1796
1920
|
}
|
|
1797
1921
|
}
|
|
1798
|
-
const pkgJson = await readJson(
|
|
1922
|
+
const pkgJson = await readJson(path16.join(packageRoot(), "package.json"));
|
|
1799
1923
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
1800
1924
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
1801
1925
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
1802
|
-
warn(
|
|
1926
|
+
warn(
|
|
1927
|
+
`- CLI UPDATE: ${currentVersion} \u2192 ${npmStatus.latest} available (run: npm install -g ${NPM_PACKAGE_NAME})`
|
|
1928
|
+
);
|
|
1803
1929
|
process.exitCode = 1;
|
|
1804
1930
|
} else if (npmStatus.latest !== null) {
|
|
1805
1931
|
log(`- CLI: ${currentVersion} (up to date)`);
|
|
@@ -1960,8 +2086,8 @@ async function runGuard(kind, _options) {
|
|
|
1960
2086
|
}
|
|
1961
2087
|
|
|
1962
2088
|
// src/commands/init.ts
|
|
1963
|
-
import
|
|
1964
|
-
import
|
|
2089
|
+
import path17 from "path";
|
|
2090
|
+
import fs12 from "fs-extra";
|
|
1965
2091
|
|
|
1966
2092
|
// src/utils/exec.ts
|
|
1967
2093
|
import { execa } from "execa";
|
|
@@ -2030,7 +2156,9 @@ var ECOSYSTEM_COMPATIBLE_BACKENDS = {
|
|
|
2030
2156
|
async function recommend(root, context) {
|
|
2031
2157
|
const items = await loadCatalog(root);
|
|
2032
2158
|
const setupAnswers = await readJson(hausPath(root, "setup-answers.json")) ?? {};
|
|
2033
|
-
const sources = await readJson(
|
|
2159
|
+
const sources = await readJson(
|
|
2160
|
+
hausPath(root, "sources-report.json")
|
|
2161
|
+
) ?? {};
|
|
2034
2162
|
const stackSet = buildStackSet(context);
|
|
2035
2163
|
const depSet = new Set(context.dependencies.map((d) => d.toLowerCase()));
|
|
2036
2164
|
const roleSet = new Set(context.repoRoles.map((r) => r.toLowerCase()));
|
|
@@ -2114,14 +2242,23 @@ async function recommend(root, context) {
|
|
|
2114
2242
|
if (tagMatch) {
|
|
2115
2243
|
pushReason("stack-match", "stack/dependency match", 30, `tag:${tagMatch}`);
|
|
2116
2244
|
}
|
|
2117
|
-
const goalMatch = item.tags.find(
|
|
2245
|
+
const goalMatch = item.tags.find(
|
|
2246
|
+
(t) => goals.includes(t) || goals.includes(t.replace(/-/g, " "))
|
|
2247
|
+
);
|
|
2118
2248
|
if (goalMatch) {
|
|
2119
2249
|
pushReason("goal-match", "guided goal match", 15, `goal:${goalMatch}`);
|
|
2120
2250
|
}
|
|
2121
2251
|
if (item.tags.includes(context.packageManager) || item.tags.includes(`${context.packageManager}4`) || item.tags.includes(`${context.packageManager}89`)) {
|
|
2122
|
-
pushReason(
|
|
2252
|
+
pushReason(
|
|
2253
|
+
"package-manager-match",
|
|
2254
|
+
"package manager match",
|
|
2255
|
+
10,
|
|
2256
|
+
`packageManager:${context.packageManager}`
|
|
2257
|
+
);
|
|
2123
2258
|
}
|
|
2124
|
-
const configSignal = item.tags.find(
|
|
2259
|
+
const configSignal = item.tags.find(
|
|
2260
|
+
(t) => context.warnings.join(" ").toLowerCase().includes(t.toLowerCase())
|
|
2261
|
+
);
|
|
2125
2262
|
if (configSignal) {
|
|
2126
2263
|
pushReason("config-signal-match", "config signal match", 20, `warning:${configSignal}`);
|
|
2127
2264
|
}
|
|
@@ -2208,9 +2345,15 @@ async function recommend(root, context) {
|
|
|
2208
2345
|
pushSkipReason("source-approval", "Source not approved", 100);
|
|
2209
2346
|
}
|
|
2210
2347
|
if (securityRiskCount > 0 && !isDefaultBaseline && (item.tags.includes("security") || item.id.includes("security"))) {
|
|
2211
|
-
pushSkipReason(
|
|
2348
|
+
pushSkipReason(
|
|
2349
|
+
"security-risk-penalty",
|
|
2350
|
+
"Security-tagged item penalized by active risk signals",
|
|
2351
|
+
20
|
|
2352
|
+
);
|
|
2212
2353
|
}
|
|
2213
|
-
const positiveReasonCodes = new Set(
|
|
2354
|
+
const positiveReasonCodes = new Set(
|
|
2355
|
+
reasons.map((r) => r.code).filter((c) => c !== "default-baseline")
|
|
2356
|
+
);
|
|
2214
2357
|
const hasRoleSignal = positiveReasonCodes.has("repo-role-match");
|
|
2215
2358
|
const hasDepOrStackSignal = positiveReasonCodes.has("stack-match") || positiveReasonCodes.has("requires-any-match");
|
|
2216
2359
|
if (hasRoleSignal && !hasDepOrStackSignal && !isDefaultBaseline && requiresAny.length === 0) {
|
|
@@ -2281,7 +2424,11 @@ async function recommend(root, context) {
|
|
|
2281
2424
|
};
|
|
2282
2425
|
}
|
|
2283
2426
|
function buildStackSet(context) {
|
|
2284
|
-
return new Set(
|
|
2427
|
+
return new Set(
|
|
2428
|
+
[...context.repoRoles, ...Object.values(context.detectedStacks).flat()].map(
|
|
2429
|
+
(x) => x.toLowerCase()
|
|
2430
|
+
)
|
|
2431
|
+
);
|
|
2285
2432
|
}
|
|
2286
2433
|
function inferRepoEcosystems(roles) {
|
|
2287
2434
|
const ecosystems = /* @__PURE__ */ new Set();
|
|
@@ -2483,8 +2630,8 @@ async function runSetupProject(options) {
|
|
|
2483
2630
|
// src/commands/init.ts
|
|
2484
2631
|
async function runInit(options) {
|
|
2485
2632
|
const root = process.cwd();
|
|
2486
|
-
const hausDir =
|
|
2487
|
-
const alreadyInit = await
|
|
2633
|
+
const hausDir = path17.join(root, ".haus-workflow");
|
|
2634
|
+
const alreadyInit = await fs12.pathExists(hausDir);
|
|
2488
2635
|
if (alreadyInit) {
|
|
2489
2636
|
log("Haus AI already initialized in this project.");
|
|
2490
2637
|
log("Run `haus setup-project` to reconfigure.");
|
|
@@ -2496,8 +2643,8 @@ async function runInit(options) {
|
|
|
2496
2643
|
|
|
2497
2644
|
// src/install/apply.ts
|
|
2498
2645
|
import crypto2 from "crypto";
|
|
2499
|
-
import
|
|
2500
|
-
import
|
|
2646
|
+
import path20 from "path";
|
|
2647
|
+
import fs14 from "fs-extra";
|
|
2501
2648
|
|
|
2502
2649
|
// src/install/header.ts
|
|
2503
2650
|
var MD_PREFIX = "<!-- HAUS-MANAGED";
|
|
@@ -2530,14 +2677,14 @@ ${content}`;
|
|
|
2530
2677
|
}
|
|
2531
2678
|
|
|
2532
2679
|
// src/install/manifest.ts
|
|
2533
|
-
import
|
|
2534
|
-
import
|
|
2680
|
+
import os4 from "os";
|
|
2681
|
+
import path18 from "path";
|
|
2535
2682
|
var MANIFEST_SCHEMA = "haus-install-manifest/1";
|
|
2536
2683
|
function globalClaudeDir() {
|
|
2537
|
-
return
|
|
2684
|
+
return path18.join(os4.homedir(), ".claude");
|
|
2538
2685
|
}
|
|
2539
2686
|
function hausManifestPath() {
|
|
2540
|
-
return
|
|
2687
|
+
return path18.join(globalClaudeDir(), "haus", "install-manifest.json");
|
|
2541
2688
|
}
|
|
2542
2689
|
async function readManifest() {
|
|
2543
2690
|
return readJson(hausManifestPath());
|
|
@@ -2556,10 +2703,10 @@ function buildManifest(source, files, hooks) {
|
|
|
2556
2703
|
}
|
|
2557
2704
|
|
|
2558
2705
|
// src/install/settings-merge.ts
|
|
2559
|
-
import
|
|
2560
|
-
import
|
|
2706
|
+
import path19 from "path";
|
|
2707
|
+
import fs13 from "fs-extra";
|
|
2561
2708
|
function settingsJsonPath() {
|
|
2562
|
-
return
|
|
2709
|
+
return path19.join(globalClaudeDir(), "settings.json");
|
|
2563
2710
|
}
|
|
2564
2711
|
async function readSettings() {
|
|
2565
2712
|
const parsed = await readJson(settingsJsonPath());
|
|
@@ -2615,7 +2762,7 @@ function stripHausHooks(settings) {
|
|
|
2615
2762
|
async function loadHooksFragment(fragmentPath) {
|
|
2616
2763
|
let raw;
|
|
2617
2764
|
try {
|
|
2618
|
-
raw = await
|
|
2765
|
+
raw = await fs13.readJson(fragmentPath);
|
|
2619
2766
|
} catch {
|
|
2620
2767
|
return [];
|
|
2621
2768
|
}
|
|
@@ -2630,40 +2777,40 @@ function hashContent(content) {
|
|
|
2630
2777
|
}
|
|
2631
2778
|
function sourceVersion() {
|
|
2632
2779
|
try {
|
|
2633
|
-
const pkgPath =
|
|
2634
|
-
const pkg = JSON.parse(
|
|
2780
|
+
const pkgPath = path20.join(packageRoot(), "package.json");
|
|
2781
|
+
const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf8"));
|
|
2635
2782
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
2636
2783
|
} catch {
|
|
2637
2784
|
return "haus@0.0.0";
|
|
2638
2785
|
}
|
|
2639
2786
|
}
|
|
2640
2787
|
function globalSrcDir() {
|
|
2641
|
-
return
|
|
2788
|
+
return path20.join(packageRoot(), "library", "global");
|
|
2642
2789
|
}
|
|
2643
2790
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
2644
2791
|
const entries = [];
|
|
2645
|
-
const skillsDir =
|
|
2646
|
-
if (
|
|
2647
|
-
for (const skillName of
|
|
2648
|
-
const skillFile =
|
|
2649
|
-
if (
|
|
2792
|
+
const skillsDir = path20.join(srcDir, "skills");
|
|
2793
|
+
if (fs14.pathExistsSync(skillsDir)) {
|
|
2794
|
+
for (const skillName of fs14.readdirSync(skillsDir)) {
|
|
2795
|
+
const skillFile = path20.join(skillsDir, skillName, "SKILL.md");
|
|
2796
|
+
if (fs14.pathExistsSync(skillFile)) {
|
|
2650
2797
|
entries.push({
|
|
2651
2798
|
stableId: `skill.${skillName}`,
|
|
2652
|
-
srcRelPath:
|
|
2653
|
-
destPath:
|
|
2799
|
+
srcRelPath: path20.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
2800
|
+
destPath: path20.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
2654
2801
|
});
|
|
2655
2802
|
}
|
|
2656
2803
|
}
|
|
2657
2804
|
}
|
|
2658
|
-
const agentsDir =
|
|
2659
|
-
if (
|
|
2660
|
-
for (const agentFile of
|
|
2805
|
+
const agentsDir = path20.join(srcDir, "agents");
|
|
2806
|
+
if (fs14.pathExistsSync(agentsDir)) {
|
|
2807
|
+
for (const agentFile of fs14.readdirSync(agentsDir)) {
|
|
2661
2808
|
if (!agentFile.endsWith(".md")) continue;
|
|
2662
2809
|
const agentName = agentFile.replace(/\.md$/, "");
|
|
2663
2810
|
entries.push({
|
|
2664
2811
|
stableId: `agent.${agentName}`,
|
|
2665
|
-
srcRelPath:
|
|
2666
|
-
destPath:
|
|
2812
|
+
srcRelPath: path20.join("library", "global", "agents", agentFile),
|
|
2813
|
+
destPath: path20.join(claudeDir, "agents", agentFile)
|
|
2667
2814
|
});
|
|
2668
2815
|
}
|
|
2669
2816
|
}
|
|
@@ -2687,7 +2834,7 @@ async function applyInstall(options = {}) {
|
|
|
2687
2834
|
};
|
|
2688
2835
|
const manifestFiles = [];
|
|
2689
2836
|
for (const entry of sourceFiles) {
|
|
2690
|
-
const srcPath =
|
|
2837
|
+
const srcPath = path20.join(packageRoot(), entry.srcRelPath);
|
|
2691
2838
|
const rawContent = await readText(srcPath);
|
|
2692
2839
|
if (rawContent === void 0) {
|
|
2693
2840
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -2707,7 +2854,7 @@ async function applyInstall(options = {}) {
|
|
|
2707
2854
|
}
|
|
2708
2855
|
continue;
|
|
2709
2856
|
}
|
|
2710
|
-
const destExists =
|
|
2857
|
+
const destExists = fs14.pathExistsSync(entry.destPath);
|
|
2711
2858
|
if (destExists) {
|
|
2712
2859
|
const currentContent = await readText(entry.destPath);
|
|
2713
2860
|
if (currentContent !== void 0) {
|
|
@@ -2743,7 +2890,7 @@ async function applyInstall(options = {}) {
|
|
|
2743
2890
|
schemaVersion: SCHEMA_VERSION3
|
|
2744
2891
|
});
|
|
2745
2892
|
}
|
|
2746
|
-
const fragmentPath =
|
|
2893
|
+
const fragmentPath = path20.join(srcDir, "settings-fragments", "hooks.json");
|
|
2747
2894
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
2748
2895
|
const settings = await readSettings();
|
|
2749
2896
|
const { settings: mergedSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -2752,13 +2899,13 @@ async function applyInstall(options = {}) {
|
|
|
2752
2899
|
const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
|
|
2753
2900
|
for (const entry of existingManifest.files) {
|
|
2754
2901
|
if (currentDestPaths.has(entry.destPath)) continue;
|
|
2755
|
-
if (!
|
|
2902
|
+
if (!fs14.pathExistsSync(entry.destPath)) continue;
|
|
2756
2903
|
const content = await readText(entry.destPath);
|
|
2757
2904
|
if (!content) continue;
|
|
2758
2905
|
const hasHeader = parseMarkdownHeader(content) !== void 0;
|
|
2759
2906
|
const currentHash = hashContent(content);
|
|
2760
2907
|
if (hasHeader && currentHash === entry.hash) {
|
|
2761
|
-
if (!dryRun) await
|
|
2908
|
+
if (!dryRun) await fs14.remove(entry.destPath);
|
|
2762
2909
|
result.deleted.push(entry.destPath);
|
|
2763
2910
|
} else {
|
|
2764
2911
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -2768,7 +2915,10 @@ async function applyInstall(options = {}) {
|
|
|
2768
2915
|
}
|
|
2769
2916
|
if (!dryRun && !check) {
|
|
2770
2917
|
await writeSettings(mergedSettings);
|
|
2771
|
-
const manifest = buildManifest(source, manifestFiles, [
|
|
2918
|
+
const manifest = buildManifest(source, manifestFiles, [
|
|
2919
|
+
...existingManifest?.hooks ?? [],
|
|
2920
|
+
...addedIds
|
|
2921
|
+
]);
|
|
2772
2922
|
await writeManifest(manifest);
|
|
2773
2923
|
}
|
|
2774
2924
|
return result;
|
|
@@ -2812,7 +2962,9 @@ async function runInstall(options) {
|
|
|
2812
2962
|
process.exitCode = 1;
|
|
2813
2963
|
} else if (!options.check && !options.dryRun) {
|
|
2814
2964
|
const total = result.created.length + result.updated.length;
|
|
2815
|
-
log(
|
|
2965
|
+
log(
|
|
2966
|
+
`haus install complete (${total} file(s) written, ${result.hookIds.length} hook(s) added)`
|
|
2967
|
+
);
|
|
2816
2968
|
}
|
|
2817
2969
|
} catch (err) {
|
|
2818
2970
|
error(`haus install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -2821,7 +2973,12 @@ async function runInstall(options) {
|
|
|
2821
2973
|
}
|
|
2822
2974
|
|
|
2823
2975
|
// src/memory/memory-store.ts
|
|
2824
|
-
var FILES = [
|
|
2976
|
+
var FILES = [
|
|
2977
|
+
"project-learnings.md",
|
|
2978
|
+
"decisions.md",
|
|
2979
|
+
"recurring-issues.md",
|
|
2980
|
+
"client-context.md"
|
|
2981
|
+
];
|
|
2825
2982
|
async function ensureMemory(root) {
|
|
2826
2983
|
await Promise.all(
|
|
2827
2984
|
FILES.map(async (name) => {
|
|
@@ -2878,7 +3035,10 @@ async function runMemory(subcommand, options) {
|
|
|
2878
3035
|
return;
|
|
2879
3036
|
}
|
|
2880
3037
|
const compact = `Task: ${options.task ?? "n/a"}
|
|
2881
|
-
${text}`.slice(
|
|
3038
|
+
${text}`.slice(
|
|
3039
|
+
0,
|
|
3040
|
+
options.fromHook ? 1200 : 4e3
|
|
3041
|
+
);
|
|
2882
3042
|
log(compact);
|
|
2883
3043
|
return;
|
|
2884
3044
|
}
|
|
@@ -2928,20 +3088,20 @@ async function runScan(options) {
|
|
|
2928
3088
|
}
|
|
2929
3089
|
|
|
2930
3090
|
// src/commands/undo.ts
|
|
2931
|
-
import
|
|
2932
|
-
import
|
|
3091
|
+
import path21 from "path";
|
|
3092
|
+
import fs15 from "fs-extra";
|
|
2933
3093
|
var CLAUDE_DIR = ".claude";
|
|
2934
3094
|
async function runUndo(options) {
|
|
2935
3095
|
const root = process.cwd();
|
|
2936
|
-
const targets = [
|
|
2937
|
-
const existing = targets.filter((p) =>
|
|
3096
|
+
const targets = [path21.join(root, CLAUDE_DIR), path21.join(root, HAUS_DIR)];
|
|
3097
|
+
const existing = targets.filter((p) => fs15.existsSync(p));
|
|
2938
3098
|
if (existing.length === 0) {
|
|
2939
3099
|
log("Nothing to remove: no .claude/ or .haus-workflow/ in this directory.");
|
|
2940
3100
|
return;
|
|
2941
3101
|
}
|
|
2942
3102
|
if (!options.yes) {
|
|
2943
3103
|
const ok = await confirm(
|
|
2944
|
-
`Remove ${existing.map((p) =>
|
|
3104
|
+
`Remove ${existing.map((p) => path21.relative(root, p)).join(" and ")}? This cannot be undone.`
|
|
2945
3105
|
);
|
|
2946
3106
|
if (!ok) {
|
|
2947
3107
|
log("Cancelled.");
|
|
@@ -2949,15 +3109,15 @@ async function runUndo(options) {
|
|
|
2949
3109
|
}
|
|
2950
3110
|
}
|
|
2951
3111
|
for (const p of existing) {
|
|
2952
|
-
await
|
|
2953
|
-
log(`Removed ${
|
|
3112
|
+
await fs15.remove(p);
|
|
3113
|
+
log(`Removed ${path21.relative(root, p)}`);
|
|
2954
3114
|
}
|
|
2955
3115
|
}
|
|
2956
3116
|
|
|
2957
3117
|
// src/install/uninstall.ts
|
|
2958
3118
|
import crypto3 from "crypto";
|
|
2959
|
-
import
|
|
2960
|
-
import
|
|
3119
|
+
import path22 from "path";
|
|
3120
|
+
import fs16 from "fs-extra";
|
|
2961
3121
|
async function runUninstall(options = {}) {
|
|
2962
3122
|
const { force = false } = options;
|
|
2963
3123
|
const manifest = await readManifest();
|
|
@@ -2967,7 +3127,7 @@ async function runUninstall(options = {}) {
|
|
|
2967
3127
|
return result;
|
|
2968
3128
|
}
|
|
2969
3129
|
for (const entry of manifest.files) {
|
|
2970
|
-
const exists =
|
|
3130
|
+
const exists = fs16.pathExistsSync(entry.destPath);
|
|
2971
3131
|
if (!exists) continue;
|
|
2972
3132
|
const content = await readText(entry.destPath);
|
|
2973
3133
|
if (content === void 0) continue;
|
|
@@ -2979,26 +3139,28 @@ async function runUninstall(options = {}) {
|
|
|
2979
3139
|
}
|
|
2980
3140
|
const currentHash = `sha256-${crypto3.createHash("sha256").update(content).digest("hex")}`;
|
|
2981
3141
|
if (currentHash !== entry.hash && !force) {
|
|
2982
|
-
warn(
|
|
3142
|
+
warn(
|
|
3143
|
+
`Skipping user-edited haus file (hash mismatch): ${entry.destPath} \u2014 use --force to delete`
|
|
3144
|
+
);
|
|
2983
3145
|
result.skipped.push(entry.destPath);
|
|
2984
3146
|
continue;
|
|
2985
3147
|
}
|
|
2986
|
-
await
|
|
2987
|
-
await pruneEmptyDir(
|
|
3148
|
+
await fs16.remove(entry.destPath);
|
|
3149
|
+
await pruneEmptyDir(path22.dirname(entry.destPath));
|
|
2988
3150
|
result.deleted.push(entry.destPath);
|
|
2989
3151
|
}
|
|
2990
3152
|
const settings = await readSettings();
|
|
2991
3153
|
const stripped = stripHausHooks(settings);
|
|
2992
3154
|
await writeSettings(stripped);
|
|
2993
3155
|
result.hooksStripped = true;
|
|
2994
|
-
const hausDir =
|
|
3156
|
+
const hausDir = path22.join(globalClaudeDir(), "haus");
|
|
2995
3157
|
const manifestPath = hausManifestPath();
|
|
2996
|
-
if (
|
|
2997
|
-
await
|
|
3158
|
+
if (fs16.pathExistsSync(manifestPath)) {
|
|
3159
|
+
await fs16.remove(manifestPath);
|
|
2998
3160
|
}
|
|
2999
|
-
if (
|
|
3000
|
-
const remaining = await
|
|
3001
|
-
if (remaining.length === 0) await
|
|
3161
|
+
if (fs16.pathExistsSync(hausDir)) {
|
|
3162
|
+
const remaining = await fs16.readdir(hausDir);
|
|
3163
|
+
if (remaining.length === 0) await fs16.remove(hausDir);
|
|
3002
3164
|
}
|
|
3003
3165
|
return result;
|
|
3004
3166
|
}
|
|
@@ -3017,8 +3179,8 @@ function printUninstallResult(result) {
|
|
|
3017
3179
|
}
|
|
3018
3180
|
async function pruneEmptyDir(dir) {
|
|
3019
3181
|
try {
|
|
3020
|
-
const entries = await
|
|
3021
|
-
if (entries.length === 0) await
|
|
3182
|
+
const entries = await fs16.readdir(dir);
|
|
3183
|
+
if (entries.length === 0) await fs16.remove(dir);
|
|
3022
3184
|
} catch {
|
|
3023
3185
|
}
|
|
3024
3186
|
}
|
|
@@ -3036,7 +3198,7 @@ async function runUninstallCommand(options) {
|
|
|
3036
3198
|
}
|
|
3037
3199
|
|
|
3038
3200
|
// src/commands/update.ts
|
|
3039
|
-
import
|
|
3201
|
+
import path24 from "path";
|
|
3040
3202
|
|
|
3041
3203
|
// src/update/diff-generated-files.ts
|
|
3042
3204
|
function diffGeneratedFiles() {
|
|
@@ -3063,10 +3225,12 @@ function summarizeLockDiff(before, after) {
|
|
|
3063
3225
|
|
|
3064
3226
|
// src/update/lockfile.ts
|
|
3065
3227
|
import { mkdir, readFile as readFile2, copyFile } from "fs/promises";
|
|
3066
|
-
import
|
|
3228
|
+
import path23 from "path";
|
|
3067
3229
|
async function checkLock(root) {
|
|
3068
3230
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
3069
|
-
const hasValidVersions = lock.every(
|
|
3231
|
+
const hasValidVersions = lock.every(
|
|
3232
|
+
(item) => !item.version || normalizeVersion(item.version) !== null
|
|
3233
|
+
);
|
|
3070
3234
|
const catalogRef = lock[0]?.catalogRef ?? null;
|
|
3071
3235
|
return { ok: lock.length > 0 && hasValidVersions, count: lock.length, catalogRef };
|
|
3072
3236
|
}
|
|
@@ -3082,7 +3246,7 @@ async function applyLock(root) {
|
|
|
3082
3246
|
try {
|
|
3083
3247
|
const backupDir = hausPath(root, "backups");
|
|
3084
3248
|
await mkdir(backupDir, { recursive: true });
|
|
3085
|
-
await copyFile(lockPath,
|
|
3249
|
+
await copyFile(lockPath, path23.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
3086
3250
|
} catch {
|
|
3087
3251
|
}
|
|
3088
3252
|
const enriched = await Promise.all(
|
|
@@ -3104,7 +3268,7 @@ function diffLock(before, after) {
|
|
|
3104
3268
|
}
|
|
3105
3269
|
async function hasLocalOverrides(root) {
|
|
3106
3270
|
try {
|
|
3107
|
-
await readFile2(
|
|
3271
|
+
await readFile2(path23.join(root, ".claude", "settings.json"), "utf8");
|
|
3108
3272
|
return true;
|
|
3109
3273
|
} catch {
|
|
3110
3274
|
return false;
|
|
@@ -3116,7 +3280,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
|
3116
3280
|
async function runUpdate(options) {
|
|
3117
3281
|
const root = process.cwd();
|
|
3118
3282
|
if (options.check) {
|
|
3119
|
-
const pkgJson2 = await readJson(
|
|
3283
|
+
const pkgJson2 = await readJson(path24.join(packageRoot(), "package.json"));
|
|
3120
3284
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
3121
3285
|
const [status, npmVersion, latestCatalogTag] = await Promise.all([
|
|
3122
3286
|
checkLock(root),
|
|
@@ -3143,7 +3307,7 @@ async function runUpdate(options) {
|
|
|
3143
3307
|
if (!status.ok) process.exitCode = 1;
|
|
3144
3308
|
return;
|
|
3145
3309
|
}
|
|
3146
|
-
const pkgJson = await readJson(
|
|
3310
|
+
const pkgJson = await readJson(path24.join(packageRoot(), "package.json"));
|
|
3147
3311
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
3148
3312
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
3149
3313
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -3173,13 +3337,15 @@ async function runUpdate(options) {
|
|
|
3173
3337
|
}
|
|
3174
3338
|
|
|
3175
3339
|
// src/commands/validate-catalog.ts
|
|
3176
|
-
import
|
|
3177
|
-
import
|
|
3340
|
+
import fs17 from "fs";
|
|
3341
|
+
import path26 from "path";
|
|
3178
3342
|
|
|
3179
3343
|
// src/catalog/allowed-stacks.ts
|
|
3180
|
-
import
|
|
3344
|
+
import path25 from "path";
|
|
3181
3345
|
async function readAllowedStacks(root) {
|
|
3182
|
-
const data = await readJson(
|
|
3346
|
+
const data = await readJson(
|
|
3347
|
+
path25.join(root, "library", "catalog", "allowed-stacks.json")
|
|
3348
|
+
);
|
|
3183
3349
|
return data?.stacks ?? [];
|
|
3184
3350
|
}
|
|
3185
3351
|
|
|
@@ -3204,7 +3370,12 @@ var FORBIDDEN_TAGS = [
|
|
|
3204
3370
|
var BANNED_AGENT_PHRASES = ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"];
|
|
3205
3371
|
var REQUIRED_SKILL_SECTIONS = ["## Use when", "## Do not use when"];
|
|
3206
3372
|
var REQUIRED_AGENT_SECTIONS = ["## Use when", "## Do not use when", "## Verification"];
|
|
3207
|
-
var RISKY_INSTALL_PATTERNS = [
|
|
3373
|
+
var RISKY_INSTALL_PATTERNS = [
|
|
3374
|
+
/\bnpx\s+-y\b/i,
|
|
3375
|
+
/\bnpx\s+--yes\b/i,
|
|
3376
|
+
/\byarn\s+dlx\b/i,
|
|
3377
|
+
/\bpnpm\s+dlx\b/i
|
|
3378
|
+
];
|
|
3208
3379
|
var ALLOWED_NPX_PATTERN = /\bnpx\s+tsx\b/i;
|
|
3209
3380
|
var ANY_NPX_PATTERN = /\bnpx\s+\S+/i;
|
|
3210
3381
|
var HTTP_URL_PATTERN = /^http:\/\//i;
|
|
@@ -3278,33 +3449,34 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
3278
3449
|
const failures = [];
|
|
3279
3450
|
for (const item of items) {
|
|
3280
3451
|
if (!item.path) continue;
|
|
3281
|
-
const absPath =
|
|
3452
|
+
const absPath = path26.join(manifestDir, item.path);
|
|
3282
3453
|
if (item.type === "skill") {
|
|
3283
|
-
const skillMd =
|
|
3284
|
-
if (!
|
|
3285
|
-
failures.push(`${item.id}: missing ${
|
|
3454
|
+
const skillMd = path26.join(absPath, "SKILL.md");
|
|
3455
|
+
if (!fs17.existsSync(skillMd)) {
|
|
3456
|
+
failures.push(`${item.id}: missing ${path26.relative(manifestDir, skillMd)}`);
|
|
3286
3457
|
continue;
|
|
3287
3458
|
}
|
|
3288
|
-
const text =
|
|
3459
|
+
const text = fs17.readFileSync(skillMd, "utf8");
|
|
3289
3460
|
for (const section of REQUIRED_SKILL_SECTIONS) {
|
|
3290
3461
|
if (!text.includes(section)) failures.push(`${item.id}: SKILL.md missing ${section}`);
|
|
3291
3462
|
}
|
|
3292
3463
|
} else if (item.type === "agent") {
|
|
3293
|
-
if (!
|
|
3464
|
+
if (!fs17.existsSync(absPath)) {
|
|
3294
3465
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
3295
3466
|
continue;
|
|
3296
3467
|
}
|
|
3297
|
-
const text =
|
|
3468
|
+
const text = fs17.readFileSync(absPath, "utf8");
|
|
3298
3469
|
if (!text.startsWith("---")) failures.push(`${item.id}: agent file missing YAML frontmatter`);
|
|
3299
3470
|
for (const section of REQUIRED_AGENT_SECTIONS) {
|
|
3300
3471
|
if (!text.includes(section)) failures.push(`${item.id}: agent file missing ${section}`);
|
|
3301
3472
|
}
|
|
3302
3473
|
const lower = text.toLowerCase();
|
|
3303
3474
|
for (const phrase of BANNED_AGENT_PHRASES) {
|
|
3304
|
-
if (lower.includes(phrase))
|
|
3475
|
+
if (lower.includes(phrase))
|
|
3476
|
+
failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
|
|
3305
3477
|
}
|
|
3306
3478
|
} else if (item.type === "template") {
|
|
3307
|
-
if (!
|
|
3479
|
+
if (!fs17.existsSync(absPath)) {
|
|
3308
3480
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
3309
3481
|
}
|
|
3310
3482
|
}
|
|
@@ -3315,11 +3487,11 @@ function auditMarkdownContent(manifestDir) {
|
|
|
3315
3487
|
const failures = [];
|
|
3316
3488
|
const dirs = ["skills", "agents"];
|
|
3317
3489
|
for (const dir of dirs) {
|
|
3318
|
-
const abs =
|
|
3319
|
-
if (!
|
|
3490
|
+
const abs = path26.join(manifestDir, dir);
|
|
3491
|
+
if (!fs17.existsSync(abs)) continue;
|
|
3320
3492
|
walkMd(abs, (file) => {
|
|
3321
|
-
const text =
|
|
3322
|
-
const rel =
|
|
3493
|
+
const text = fs17.readFileSync(file, "utf8");
|
|
3494
|
+
const rel = path26.relative(manifestDir, file);
|
|
3323
3495
|
const lines = text.split(/\r?\n/);
|
|
3324
3496
|
for (let i = 0; i < lines.length; i++) {
|
|
3325
3497
|
const line = lines[i] ?? "";
|
|
@@ -3338,8 +3510,8 @@ function auditMarkdownContent(manifestDir) {
|
|
|
3338
3510
|
return failures;
|
|
3339
3511
|
}
|
|
3340
3512
|
function walkMd(dir, fn) {
|
|
3341
|
-
for (const entry of
|
|
3342
|
-
const full =
|
|
3513
|
+
for (const entry of fs17.readdirSync(dir, { withFileTypes: true })) {
|
|
3514
|
+
const full = path26.join(dir, entry.name);
|
|
3343
3515
|
if (entry.isDirectory()) walkMd(full, fn);
|
|
3344
3516
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
3345
3517
|
}
|
|
@@ -3350,8 +3522,8 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3350
3522
|
process.exitCode = 1;
|
|
3351
3523
|
return;
|
|
3352
3524
|
}
|
|
3353
|
-
const abs =
|
|
3354
|
-
const manifestDir =
|
|
3525
|
+
const abs = path26.resolve(process.cwd(), manifestPath);
|
|
3526
|
+
const manifestDir = path26.dirname(abs);
|
|
3355
3527
|
const data = await readJson(abs);
|
|
3356
3528
|
if (!data?.items) {
|
|
3357
3529
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -3374,7 +3546,13 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3374
3546
|
}
|
|
3375
3547
|
}
|
|
3376
3548
|
}
|
|
3377
|
-
const allFailures = [
|
|
3549
|
+
const allFailures = [
|
|
3550
|
+
...structureFailures,
|
|
3551
|
+
...stackFailures,
|
|
3552
|
+
...fileFailures,
|
|
3553
|
+
...contentFailures,
|
|
3554
|
+
...tagFailures
|
|
3555
|
+
];
|
|
3378
3556
|
if (allFailures.length) {
|
|
3379
3557
|
allFailures.forEach((f) => error(f));
|
|
3380
3558
|
process.exitCode = 1;
|
|
@@ -3384,7 +3562,7 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3384
3562
|
}
|
|
3385
3563
|
|
|
3386
3564
|
// src/commands/workspace.ts
|
|
3387
|
-
import
|
|
3565
|
+
import path27 from "path";
|
|
3388
3566
|
import YAML from "yaml";
|
|
3389
3567
|
async function runWorkspace(action) {
|
|
3390
3568
|
if (action === "init") {
|
|
@@ -3417,7 +3595,7 @@ relationships: []
|
|
|
3417
3595
|
const summaries = [];
|
|
3418
3596
|
const ownership = {};
|
|
3419
3597
|
for (const repo of repos) {
|
|
3420
|
-
const repoRoot =
|
|
3598
|
+
const repoRoot = path27.resolve(process.cwd(), repo.path);
|
|
3421
3599
|
const result = await scanProject(repoRoot, "fast");
|
|
3422
3600
|
summaries.push({
|
|
3423
3601
|
name: repo.name,
|
|
@@ -3453,7 +3631,7 @@ ${summaries.map(
|
|
|
3453
3631
|
// src/cli.ts
|
|
3454
3632
|
function cliVersion() {
|
|
3455
3633
|
try {
|
|
3456
|
-
const pkgPath =
|
|
3634
|
+
const pkgPath = path28.join(packageRoot(), "package.json");
|
|
3457
3635
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
3458
3636
|
return pkg.version ?? "0.0.0";
|
|
3459
3637
|
} catch {
|
|
@@ -3463,7 +3641,7 @@ function cliVersion() {
|
|
|
3463
3641
|
var program = new Command();
|
|
3464
3642
|
function validateRuntimeNodeVersion() {
|
|
3465
3643
|
try {
|
|
3466
|
-
const pkgPath =
|
|
3644
|
+
const pkgPath = path28.join(packageRoot(), "package.json");
|
|
3467
3645
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
3468
3646
|
const requiredRange = pkg.engines?.node;
|
|
3469
3647
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -3481,7 +3659,10 @@ program.command("scan").option("--json").action(runScan);
|
|
|
3481
3659
|
program.command("recommend").option("--json").action(runRecommend);
|
|
3482
3660
|
program.command("setup-project").option("--guided").option("--fast").option("--json").action(runSetupProject);
|
|
3483
3661
|
program.command("doctor").option("--hooks", "Verify .claude/settings.json matches the hook contract").action(runDoctor);
|
|
3484
|
-
program.command("apply").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option(
|
|
3662
|
+
program.command("apply").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option(
|
|
3663
|
+
"--allow-empty-cache",
|
|
3664
|
+
"Apply core files only when catalog cache is empty (skip catalog items without error)"
|
|
3665
|
+
).action(runApply);
|
|
3485
3666
|
program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
|
|
3486
3667
|
program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
|
|
3487
3668
|
program.command("context").option("--task <task>").option("--from-hook").option("--json").option("--verbose").action(runContext);
|