rebar-mcp 2.0.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/.claude/agents/template-writer.md +43 -0
- package/.claude/agents/test-runner.md +47 -0
- package/.claude/mcp.json +9 -0
- package/.claude/settings.json +29 -0
- package/.claude/skills/ /SKILL.md +21 -0
- package/.claude/skills/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/SKILL.md +21 -0
- package/.claude/skills/bmmibwetxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
- package/.claude/skills/bmmibwjgvxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
- package/.claude/skills/bmmibwsesxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
- package/.claude/skills/bmmibwxufxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
- package/.claude/skills/bmmibx3r9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
- package/.claude/skills/bmmji0lrkxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
- package/.claude/skills/bmmjiniphxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
- package/.claude/skills/bmmjio86zxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
- package/.claude/skills/bmmjiolfbxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
- package/.claude/skills/bmmjit1lvxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
- package/.claude/skills/bmmjita1qxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/SKILL.md +21 -0
- package/.claude/skills/bnd-mmibweu3/SKILL.md +21 -0
- package/.claude/skills/bnd-mmibwjh4/SKILL.md +21 -0
- package/.claude/skills/bnd-mmibwsey/SKILL.md +21 -0
- package/.claude/skills/bnd-mmibwxup/SKILL.md +21 -0
- package/.claude/skills/bnd-mmibx3rg/SKILL.md +21 -0
- package/.claude/skills/bnd-mmji0lrp/SKILL.md +21 -0
- package/.claude/skills/bnd-mmjinipm/SKILL.md +21 -0
- package/.claude/skills/bnd-mmjio875/SKILL.md +21 -0
- package/.claude/skills/bnd-mmjiolfg/SKILL.md +21 -0
- package/.claude/skills/bnd-mmjit1m3/SKILL.md +21 -0
- package/.claude/skills/bnd-mmjita1x/SKILL.md +21 -0
- package/.claude/skills/coercion-test/SKILL.md +50 -0
- package/.claude/skills/large-skill/SKILL.md +21 -0
- package/.claude/skills/long-desc-skill/SKILL.md +21 -0
- package/.claude/skills/mcp-dev/SKILL.md +61 -0
- package/.claude/skills/nl-mmibweus/SKILL.md +25 -0
- package/.claude/skills/nl-mmibwjhf/SKILL.md +25 -0
- package/.claude/skills/nl-mmibwsf7/SKILL.md +25 -0
- package/.claude/skills/nl-mmibwxvq/SKILL.md +25 -0
- package/.claude/skills/nl-mmibx3rt/SKILL.md +25 -0
- package/.claude/skills/nl-mmji0lrz/SKILL.md +25 -0
- package/.claude/skills/nl-mmjinipx/SKILL.md +25 -0
- package/.claude/skills/nl-mmjio87f/SKILL.md +25 -0
- package/.claude/skills/nl-mmjiolfs/SKILL.md +25 -0
- package/.claude/skills/nl-mmjit1mc/SKILL.md +25 -0
- package/.claude/skills/nl-mmjita26/SKILL.md +25 -0
- package/.claude/skills/rapid-1/SKILL.md +21 -0
- package/.claude/skills/rapid-2/SKILL.md +21 -0
- package/.claude/skills/rapid-3/SKILL.md +21 -0
- package/.claude/skills/rapid-4/SKILL.md +21 -0
- package/.claude/skills/rapid-5/SKILL.md +21 -0
- package/.claude/skills/test/", /"malicious/": /"true/SKILL.md" +69 -0
- package/.claude/skills/test-emoji-/360/237/230/200-skill/SKILL.md +69 -0
- package/.claude/skills/test-skill/SKILL.md +69 -0
- package/.claude/skills/test; rm -rf /; skill/SKILL.md +69 -0
- package/.claude/skills/test<script>alert(1)</script>skill/SKILL.md +69 -0
- package/.claudeignore +5 -0
- package/.mcp.json +3 -0
- package/CHANGELOG.md +29 -0
- package/CLAUDE.md +76 -0
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/ROADMAP.md +526 -0
- package/ccboot-PRD-v1.0.docx.md +732 -0
- package/ccboot-v1.2.0-enforcement-spec.md +1272 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +674 -0
- package/dist/cli.js.map +1 -0
- package/dist/constants.d.ts +25 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +118 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/common.d.ts +62 -0
- package/dist/schemas/common.d.ts.map +1 -0
- package/dist/schemas/common.js +15 -0
- package/dist/schemas/common.js.map +1 -0
- package/dist/schemas/scaffolding.d.ts +277 -0
- package/dist/schemas/scaffolding.d.ts.map +1 -0
- package/dist/schemas/scaffolding.js +133 -0
- package/dist/schemas/scaffolding.js.map +1 -0
- package/dist/services/claudemd-generator.d.ts +16 -0
- package/dist/services/claudemd-generator.d.ts.map +1 -0
- package/dist/services/claudemd-generator.js +426 -0
- package/dist/services/claudemd-generator.js.map +1 -0
- package/dist/services/codex-generator.d.ts +6 -0
- package/dist/services/codex-generator.d.ts.map +1 -0
- package/dist/services/codex-generator.js +35 -0
- package/dist/services/codex-generator.js.map +1 -0
- package/dist/services/cursor-generator.d.ts +15 -0
- package/dist/services/cursor-generator.d.ts.map +1 -0
- package/dist/services/cursor-generator.js +134 -0
- package/dist/services/cursor-generator.js.map +1 -0
- package/dist/services/file-ops.d.ts +48 -0
- package/dist/services/file-ops.d.ts.map +1 -0
- package/dist/services/file-ops.js +153 -0
- package/dist/services/file-ops.js.map +1 -0
- package/dist/services/output-formatter.d.ts +57 -0
- package/dist/services/output-formatter.d.ts.map +1 -0
- package/dist/services/output-formatter.js +88 -0
- package/dist/services/output-formatter.js.map +1 -0
- package/dist/services/platform-detect.d.ts +14 -0
- package/dist/services/platform-detect.d.ts.map +1 -0
- package/dist/services/platform-detect.js +63 -0
- package/dist/services/platform-detect.js.map +1 -0
- package/dist/services/project-analyzer.d.ts +71 -0
- package/dist/services/project-analyzer.d.ts.map +1 -0
- package/dist/services/project-analyzer.js +595 -0
- package/dist/services/project-analyzer.js.map +1 -0
- package/dist/services/rules-engine.d.ts +41 -0
- package/dist/services/rules-engine.d.ts.map +1 -0
- package/dist/services/rules-engine.js +304 -0
- package/dist/services/rules-engine.js.map +1 -0
- package/dist/services/strictness.d.ts +37 -0
- package/dist/services/strictness.d.ts.map +1 -0
- package/dist/services/strictness.js +182 -0
- package/dist/services/strictness.js.map +1 -0
- package/dist/services/template-engine.d.ts +16 -0
- package/dist/services/template-engine.d.ts.map +1 -0
- package/dist/services/template-engine.js +85 -0
- package/dist/services/template-engine.js.map +1 -0
- package/dist/services/validation.d.ts +41 -0
- package/dist/services/validation.d.ts.map +1 -0
- package/dist/services/validation.js +104 -0
- package/dist/services/validation.js.map +1 -0
- package/dist/services/windsurf-generator.d.ts +15 -0
- package/dist/services/windsurf-generator.d.ts.map +1 -0
- package/dist/services/windsurf-generator.js +127 -0
- package/dist/services/windsurf-generator.js.map +1 -0
- package/dist/tests/enforcement.test.d.ts +2 -0
- package/dist/tests/enforcement.test.d.ts.map +1 -0
- package/dist/tests/enforcement.test.js +541 -0
- package/dist/tests/enforcement.test.js.map +1 -0
- package/dist/tests/enterprise.test.d.ts +2 -0
- package/dist/tests/enterprise.test.d.ts.map +1 -0
- package/dist/tests/enterprise.test.js +353 -0
- package/dist/tests/enterprise.test.js.map +1 -0
- package/dist/tests/fuzzing.test.d.ts +2 -0
- package/dist/tests/fuzzing.test.d.ts.map +1 -0
- package/dist/tests/fuzzing.test.js +596 -0
- package/dist/tests/fuzzing.test.js.map +1 -0
- package/dist/tests/knowledge.test.d.ts +2 -0
- package/dist/tests/knowledge.test.d.ts.map +1 -0
- package/dist/tests/knowledge.test.js +292 -0
- package/dist/tests/knowledge.test.js.map +1 -0
- package/dist/tests/management.test.d.ts +2 -0
- package/dist/tests/management.test.d.ts.map +1 -0
- package/dist/tests/management.test.js +338 -0
- package/dist/tests/management.test.js.map +1 -0
- package/dist/tests/scaffolding.test.d.ts +2 -0
- package/dist/tests/scaffolding.test.d.ts.map +1 -0
- package/dist/tests/scaffolding.test.js +419 -0
- package/dist/tests/scaffolding.test.js.map +1 -0
- package/dist/tests/test-utils.d.ts +76 -0
- package/dist/tests/test-utils.d.ts.map +1 -0
- package/dist/tests/test-utils.js +171 -0
- package/dist/tests/test-utils.js.map +1 -0
- package/dist/tests/tool-harness.d.ts +18 -0
- package/dist/tests/tool-harness.d.ts.map +1 -0
- package/dist/tests/tool-harness.js +51 -0
- package/dist/tests/tool-harness.js.map +1 -0
- package/dist/tools/enterprise.d.ts +8 -0
- package/dist/tools/enterprise.d.ts.map +1 -0
- package/dist/tools/enterprise.js +571 -0
- package/dist/tools/enterprise.js.map +1 -0
- package/dist/tools/knowledge.d.ts +7 -0
- package/dist/tools/knowledge.d.ts.map +1 -0
- package/dist/tools/knowledge.js +120 -0
- package/dist/tools/knowledge.js.map +1 -0
- package/dist/tools/management.d.ts +10 -0
- package/dist/tools/management.d.ts.map +1 -0
- package/dist/tools/management.js +1541 -0
- package/dist/tools/management.js.map +1 -0
- package/dist/tools/scaffolding.d.ts +8 -0
- package/dist/tools/scaffolding.d.ts.map +1 -0
- package/dist/tools/scaffolding.js +736 -0
- package/dist/tools/scaffolding.js.map +1 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/landing/app/layout.tsx +30 -0
- package/landing/app/page.tsx +944 -0
- package/landing/next-env.d.ts +6 -0
- package/landing/next.config.js +6 -0
- package/landing/package-lock.json +896 -0
- package/landing/package.json +20 -0
- package/landing/tsconfig.json +40 -0
- package/package.json +49 -0
- package/rebar-v2.0.0-platform-spec.md +1567 -0
- package/server.json +20 -0
- package/src/cli.ts +735 -0
- package/src/constants.ts +131 -0
- package/src/index.ts +54 -0
- package/src/schemas/common.ts +22 -0
- package/src/schemas/scaffolding.ts +161 -0
- package/src/services/claudemd-generator.ts +481 -0
- package/src/services/codex-generator.ts +44 -0
- package/src/services/cursor-generator.ts +153 -0
- package/src/services/file-ops.ts +172 -0
- package/src/services/platform-detect.ts +80 -0
- package/src/services/project-analyzer.ts +690 -0
- package/src/services/rules-engine.ts +353 -0
- package/src/services/strictness.ts +202 -0
- package/src/services/template-engine.ts +119 -0
- package/src/services/validation.ts +138 -0
- package/src/services/windsurf-generator.ts +145 -0
- package/src/tests/enforcement.test.ts +794 -0
- package/src/tests/enterprise.test.ts +483 -0
- package/src/tests/fuzzing.test.ts +690 -0
- package/src/tests/knowledge.test.ts +371 -0
- package/src/tests/management.test.ts +451 -0
- package/src/tests/scaffolding.test.ts +575 -0
- package/src/tests/test-utils.ts +206 -0
- package/src/tests/tool-harness.ts +70 -0
- package/src/tools/enterprise.ts +666 -0
- package/src/tools/knowledge.ts +162 -0
- package/src/tools/management.ts +1706 -0
- package/src/tools/scaffolding.ts +909 -0
- package/src/types.ts +93 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/.temp/gotrue-version +1 -0
- package/supabase/.temp/pooler-url +1 -0
- package/supabase/.temp/postgres-version +1 -0
- package/supabase/.temp/project-ref +1 -0
- package/supabase/.temp/rest-version +1 -0
- package/supabase/.temp/storage-migration +1 -0
- package/supabase/.temp/storage-version +1 -0
- package/templates/agents/explore.md +41 -0
- package/templates/agents/plan.md +73 -0
- package/templates/agents/security-auditor.md +77 -0
- package/templates/agents/test-runner.md +60 -0
- package/templates/claudemd/fastapi.md +49 -0
- package/templates/claudemd/monorepo.md +48 -0
- package/templates/claudemd/nextjs.md +52 -0
- package/templates/claudemd/react-spa.md +50 -0
- package/templates/claudemd/springboot.md +50 -0
- package/templates/hooks/danger-blocker.json +11 -0
- package/templates/hooks/format-on-write.json +17 -0
- package/templates/hooks/lint-on-write.json +16 -0
- package/templates/hooks/secret-detector.json +11 -0
- package/templates/skills/code-review.md +68 -0
- package/templates/skills/documentation.md +62 -0
- package/templates/skills/performance-audit.md +80 -0
- package/templates/skills/security-scan.md +66 -0
- package/templates/skills/test-writer.md +56 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom rules engine for Rebar.
|
|
3
|
+
*
|
|
4
|
+
* Allows users to define custom quality rules in .rebar/rules.yaml
|
|
5
|
+
* that extend the built-in checks.
|
|
6
|
+
*/
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { readFileSafe, fileExists, sanitizePath } from "./file-ops.js";
|
|
9
|
+
|
|
10
|
+
export interface CustomRule {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
severity: "error" | "warning" | "info";
|
|
15
|
+
type: "file-exists" | "file-contains" | "file-not-contains" | "glob-count" | "command";
|
|
16
|
+
target?: string; // File path or glob pattern
|
|
17
|
+
pattern?: string; // Regex pattern for contains rules
|
|
18
|
+
min?: number; // Minimum count for glob-count
|
|
19
|
+
max?: number; // Maximum count for glob-count
|
|
20
|
+
command?: string; // Shell command for command rules
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RulesConfig {
|
|
25
|
+
version: string;
|
|
26
|
+
rules: CustomRule[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RuleResult {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
passed: boolean;
|
|
33
|
+
severity: "error" | "warning" | "info";
|
|
34
|
+
message: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parses a YAML-like rules file (simplified parser for basic YAML).
|
|
39
|
+
* For full YAML support, users should use JSON format.
|
|
40
|
+
*/
|
|
41
|
+
function parseSimpleYaml(content: string): RulesConfig | null {
|
|
42
|
+
try {
|
|
43
|
+
// Try JSON first
|
|
44
|
+
if (content.trim().startsWith("{")) {
|
|
45
|
+
return JSON.parse(content) as RulesConfig;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Simple YAML-like parser
|
|
49
|
+
const rules: CustomRule[] = [];
|
|
50
|
+
const lines = content.split("\n");
|
|
51
|
+
let currentRule: Partial<CustomRule> | null = null;
|
|
52
|
+
let inRules = false;
|
|
53
|
+
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
const trimmed = line.trim();
|
|
56
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
57
|
+
|
|
58
|
+
if (trimmed === "rules:") {
|
|
59
|
+
inRules = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (inRules && trimmed.startsWith("- id:")) {
|
|
64
|
+
if (currentRule && currentRule.id) {
|
|
65
|
+
rules.push(currentRule as CustomRule);
|
|
66
|
+
}
|
|
67
|
+
currentRule = { id: trimmed.slice(5).trim(), enabled: true };
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (currentRule && trimmed.includes(":")) {
|
|
72
|
+
const [key, ...valueParts] = trimmed.split(":");
|
|
73
|
+
const value = valueParts.join(":").trim().replace(/^["']|["']$/g, "");
|
|
74
|
+
|
|
75
|
+
switch (key.trim()) {
|
|
76
|
+
case "name":
|
|
77
|
+
currentRule.name = value;
|
|
78
|
+
break;
|
|
79
|
+
case "description":
|
|
80
|
+
currentRule.description = value;
|
|
81
|
+
break;
|
|
82
|
+
case "severity":
|
|
83
|
+
if (value === "error" || value === "warning" || value === "info") {
|
|
84
|
+
currentRule.severity = value;
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
case "type":
|
|
88
|
+
if (["file-exists", "file-contains", "file-not-contains", "glob-count", "command"].includes(value)) {
|
|
89
|
+
currentRule.type = value as CustomRule["type"];
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
case "target":
|
|
93
|
+
currentRule.target = value;
|
|
94
|
+
break;
|
|
95
|
+
case "pattern":
|
|
96
|
+
currentRule.pattern = value;
|
|
97
|
+
break;
|
|
98
|
+
case "min":
|
|
99
|
+
currentRule.min = parseInt(value, 10);
|
|
100
|
+
break;
|
|
101
|
+
case "max":
|
|
102
|
+
currentRule.max = parseInt(value, 10);
|
|
103
|
+
break;
|
|
104
|
+
case "command":
|
|
105
|
+
currentRule.command = value;
|
|
106
|
+
break;
|
|
107
|
+
case "enabled":
|
|
108
|
+
currentRule.enabled = value === "true";
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (currentRule && currentRule.id) {
|
|
115
|
+
rules.push(currentRule as CustomRule);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { version: "1.0", rules };
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Loads custom rules from .rebar/rules.yaml or .rebar/rules.json
|
|
126
|
+
*/
|
|
127
|
+
export async function loadCustomRules(projectPath: string): Promise<RulesConfig | null> {
|
|
128
|
+
const yamlPath = sanitizePath(projectPath, ".rebar/rules.yaml");
|
|
129
|
+
const jsonPath = sanitizePath(projectPath, ".rebar/rules.json");
|
|
130
|
+
|
|
131
|
+
// Try YAML first
|
|
132
|
+
let content = await readFileSafe(yamlPath);
|
|
133
|
+
if (content) {
|
|
134
|
+
return parseSimpleYaml(content);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Try JSON
|
|
138
|
+
content = await readFileSafe(jsonPath);
|
|
139
|
+
if (content) {
|
|
140
|
+
try {
|
|
141
|
+
return JSON.parse(content) as RulesConfig;
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Evaluates a single rule against the project.
|
|
152
|
+
*/
|
|
153
|
+
export async function evaluateRule(rule: CustomRule, projectPath: string): Promise<RuleResult> {
|
|
154
|
+
const result: RuleResult = {
|
|
155
|
+
id: rule.id,
|
|
156
|
+
name: rule.name,
|
|
157
|
+
passed: false,
|
|
158
|
+
severity: rule.severity,
|
|
159
|
+
message: "",
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (rule.enabled === false) {
|
|
163
|
+
result.passed = true;
|
|
164
|
+
result.message = "Rule disabled";
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
switch (rule.type) {
|
|
170
|
+
case "file-exists": {
|
|
171
|
+
if (!rule.target) {
|
|
172
|
+
result.message = "Rule missing 'target' field";
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
const filePath = sanitizePath(projectPath, rule.target);
|
|
176
|
+
const exists = await fileExists(filePath);
|
|
177
|
+
result.passed = exists;
|
|
178
|
+
result.message = exists
|
|
179
|
+
? `File exists: ${rule.target}`
|
|
180
|
+
: `File not found: ${rule.target}`;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
case "file-contains": {
|
|
185
|
+
if (!rule.target || !rule.pattern) {
|
|
186
|
+
result.message = "Rule missing 'target' or 'pattern' field";
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
const filePath = sanitizePath(projectPath, rule.target);
|
|
190
|
+
const content = await readFileSafe(filePath);
|
|
191
|
+
if (!content) {
|
|
192
|
+
result.passed = false;
|
|
193
|
+
result.message = `File not found: ${rule.target}`;
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
const regex = new RegExp(rule.pattern);
|
|
197
|
+
result.passed = regex.test(content);
|
|
198
|
+
result.message = result.passed
|
|
199
|
+
? `Pattern found in ${rule.target}`
|
|
200
|
+
: `Pattern not found in ${rule.target}`;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
case "file-not-contains": {
|
|
205
|
+
if (!rule.target || !rule.pattern) {
|
|
206
|
+
result.message = "Rule missing 'target' or 'pattern' field";
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
const filePath = sanitizePath(projectPath, rule.target);
|
|
210
|
+
const content = await readFileSafe(filePath);
|
|
211
|
+
if (!content) {
|
|
212
|
+
result.passed = true; // File doesn't exist, so pattern can't be in it
|
|
213
|
+
result.message = `File not found (OK): ${rule.target}`;
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
const regex = new RegExp(rule.pattern);
|
|
217
|
+
result.passed = !regex.test(content);
|
|
218
|
+
result.message = result.passed
|
|
219
|
+
? `Pattern not found in ${rule.target} (OK)`
|
|
220
|
+
: `Forbidden pattern found in ${rule.target}`;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
case "glob-count": {
|
|
225
|
+
// Simplified glob matching - count files matching pattern
|
|
226
|
+
if (!rule.target) {
|
|
227
|
+
result.message = "Rule missing 'target' glob pattern";
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
// For simplicity, just check if directory exists and count files
|
|
231
|
+
const targetDir = path.dirname(rule.target);
|
|
232
|
+
const ext = path.extname(rule.target);
|
|
233
|
+
const dirPath = sanitizePath(projectPath, targetDir === "." ? "" : targetDir);
|
|
234
|
+
|
|
235
|
+
if (!(await fileExists(dirPath))) {
|
|
236
|
+
result.passed = rule.min === 0 || rule.min === undefined;
|
|
237
|
+
result.message = `Directory not found: ${targetDir}`;
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Count matching files (simplified - just count by extension)
|
|
242
|
+
const fs = await import("node:fs/promises");
|
|
243
|
+
const files = await fs.readdir(dirPath).catch(() => [] as string[]);
|
|
244
|
+
const matchingFiles = ext
|
|
245
|
+
? files.filter((f) => f.endsWith(ext))
|
|
246
|
+
: files;
|
|
247
|
+
const count = matchingFiles.length;
|
|
248
|
+
|
|
249
|
+
const minOk = rule.min === undefined || count >= rule.min;
|
|
250
|
+
const maxOk = rule.max === undefined || count <= rule.max;
|
|
251
|
+
result.passed = minOk && maxOk;
|
|
252
|
+
|
|
253
|
+
if (!minOk) {
|
|
254
|
+
result.message = `Found ${count} files, expected at least ${rule.min}`;
|
|
255
|
+
} else if (!maxOk) {
|
|
256
|
+
result.message = `Found ${count} files, expected at most ${rule.max}`;
|
|
257
|
+
} else {
|
|
258
|
+
result.message = `Found ${count} files matching ${rule.target}`;
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
case "command": {
|
|
264
|
+
if (!rule.command) {
|
|
265
|
+
result.message = "Rule missing 'command' field";
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
// Note: Command execution is intentionally limited for security
|
|
269
|
+
result.passed = false;
|
|
270
|
+
result.message = "Command rules are disabled in MCP mode for security";
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
default:
|
|
275
|
+
result.message = `Unknown rule type: ${rule.type}`;
|
|
276
|
+
}
|
|
277
|
+
} catch (err) {
|
|
278
|
+
result.message = `Rule evaluation failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Evaluates all custom rules against the project.
|
|
286
|
+
*/
|
|
287
|
+
export async function evaluateAllRules(projectPath: string): Promise<RuleResult[]> {
|
|
288
|
+
const config = await loadCustomRules(projectPath);
|
|
289
|
+
if (!config || !config.rules || config.rules.length === 0) {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const results: RuleResult[] = [];
|
|
294
|
+
for (const rule of config.rules) {
|
|
295
|
+
results.push(await evaluateRule(rule, projectPath));
|
|
296
|
+
}
|
|
297
|
+
return results;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Generates a default rules.yaml template.
|
|
302
|
+
*/
|
|
303
|
+
export function generateDefaultRulesTemplate(): string {
|
|
304
|
+
return `# Rebar Custom Rules
|
|
305
|
+
# Define custom quality gates for your project
|
|
306
|
+
# Docs: https://github.com/RCOLKITT/rebar-mcp#custom-rules
|
|
307
|
+
|
|
308
|
+
version: "1.0"
|
|
309
|
+
|
|
310
|
+
rules:
|
|
311
|
+
# Ensure important files exist
|
|
312
|
+
- id: readme-exists
|
|
313
|
+
name: README.md exists
|
|
314
|
+
description: Every project should have a README
|
|
315
|
+
severity: warning
|
|
316
|
+
type: file-exists
|
|
317
|
+
target: README.md
|
|
318
|
+
|
|
319
|
+
- id: license-exists
|
|
320
|
+
name: LICENSE file exists
|
|
321
|
+
description: Open source projects should have a license
|
|
322
|
+
severity: info
|
|
323
|
+
type: file-exists
|
|
324
|
+
target: LICENSE
|
|
325
|
+
|
|
326
|
+
# Check for sensitive patterns
|
|
327
|
+
- id: no-hardcoded-localhost
|
|
328
|
+
name: No hardcoded localhost URLs
|
|
329
|
+
description: Avoid hardcoding localhost in production code
|
|
330
|
+
severity: warning
|
|
331
|
+
type: file-not-contains
|
|
332
|
+
target: "src/**/*.ts"
|
|
333
|
+
pattern: "localhost:\\\\d+"
|
|
334
|
+
|
|
335
|
+
# Ensure minimum test coverage
|
|
336
|
+
- id: test-files-exist
|
|
337
|
+
name: Test files exist
|
|
338
|
+
description: Project should have test files
|
|
339
|
+
severity: warning
|
|
340
|
+
type: glob-count
|
|
341
|
+
target: "**/*.test.ts"
|
|
342
|
+
min: 1
|
|
343
|
+
|
|
344
|
+
# Disabled example
|
|
345
|
+
- id: example-disabled
|
|
346
|
+
name: Example disabled rule
|
|
347
|
+
description: This rule is disabled
|
|
348
|
+
severity: info
|
|
349
|
+
type: file-exists
|
|
350
|
+
target: .example
|
|
351
|
+
enabled: false
|
|
352
|
+
`;
|
|
353
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strictness profiles for Rebar enforcement hooks.
|
|
3
|
+
*
|
|
4
|
+
* Controls how aggressively hooks enforce quality:
|
|
5
|
+
* - standard: Notify on issues, only block dangerous commands and secrets
|
|
6
|
+
* - strict: Block on test/lint failures
|
|
7
|
+
* - paranoid: Block on any types, large files, TODOs in commits
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type StrictnessProfile = "standard" | "strict" | "paranoid";
|
|
11
|
+
|
|
12
|
+
export interface HookConfig {
|
|
13
|
+
event: string;
|
|
14
|
+
matcher: string;
|
|
15
|
+
command: string;
|
|
16
|
+
description: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generates the complete set of hooks for a given strictness profile.
|
|
21
|
+
*
|
|
22
|
+
* @param profile - The strictness level
|
|
23
|
+
* @param testCommand - Project's test command (e.g., "npm test")
|
|
24
|
+
* @param lintCommand - Project's lint command (e.g., "npx eslint")
|
|
25
|
+
* @param formatCommand - Project's format command (e.g., "npx prettier --write")
|
|
26
|
+
*/
|
|
27
|
+
export function getHooksForProfile(
|
|
28
|
+
profile: StrictnessProfile,
|
|
29
|
+
testCommand: string,
|
|
30
|
+
lintCommand: string,
|
|
31
|
+
formatCommand: string
|
|
32
|
+
): HookConfig[] {
|
|
33
|
+
const hooks: HookConfig[] = [];
|
|
34
|
+
|
|
35
|
+
// ─── Format on write — always auto-formats, never blocks ───
|
|
36
|
+
hooks.push({
|
|
37
|
+
event: "PostToolUse",
|
|
38
|
+
matcher: "Write(*.ts)|Write(*.tsx)|Write(*.js)|Write(*.jsx)",
|
|
39
|
+
command: `${formatCommand} "$CLAUDE_FILE_PATH" 2>/dev/null || true`,
|
|
40
|
+
description: "Auto-format TypeScript/JavaScript files after write",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ─── Lint on write ───
|
|
44
|
+
if (profile === "standard") {
|
|
45
|
+
// Standard: shows output but does not block
|
|
46
|
+
hooks.push({
|
|
47
|
+
event: "PostToolUse",
|
|
48
|
+
matcher: "Write(*.ts)|Write(*.tsx)|Write(*.js)|Write(*.jsx)",
|
|
49
|
+
command: `${lintCommand} "$CLAUDE_FILE_PATH" 2>&1 | head -20; exit 0`,
|
|
50
|
+
description: "Show lint results after write (non-blocking)",
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
// Strict/Paranoid: blocks on lint errors
|
|
54
|
+
hooks.push({
|
|
55
|
+
event: "PostToolUse",
|
|
56
|
+
matcher: "Write(*.ts)|Write(*.tsx)|Write(*.js)|Write(*.jsx)",
|
|
57
|
+
command: `RESULT=$(${lintCommand} "$CLAUDE_FILE_PATH" 2>&1); EXITCODE=$?; if [ $EXITCODE -ne 0 ]; then echo "$RESULT" | head -20; echo '\\n❌ LINT ERRORS — Fix these before continuing.' >&2; exit 2; fi; exit 0`,
|
|
58
|
+
description: "Block on lint errors after write",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Test enforcement ───
|
|
63
|
+
if (profile === "standard") {
|
|
64
|
+
// Standard: runs tests, shows results, does not block
|
|
65
|
+
hooks.push({
|
|
66
|
+
event: "PostToolUse",
|
|
67
|
+
matcher: "Write(*.ts)|Write(*.tsx)|Write(*.js)|Write(*.jsx)|Edit(*.ts)|Edit(*.tsx)|Edit(*.js)|Edit(*.jsx)",
|
|
68
|
+
command: `${testCommand} 2>&1 | tail -10; if [ $? -ne 0 ]; then echo '\\n⚠️ TESTS FAILED — You should fix these before continuing.'; fi; exit 0`,
|
|
69
|
+
description: "Run tests after code changes (non-blocking notification)",
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
// Strict/Paranoid: blocks on test failures
|
|
73
|
+
hooks.push({
|
|
74
|
+
event: "PostToolUse",
|
|
75
|
+
matcher: "Write(*.ts)|Write(*.tsx)|Write(*.js)|Write(*.jsx)|Edit(*.ts)|Edit(*.tsx)|Edit(*.js)|Edit(*.jsx)",
|
|
76
|
+
command: `${testCommand} 2>&1 | tail -10; EXIT_CODE=\${PIPESTATUS[0]}; if [ $EXIT_CODE -ne 0 ]; then echo '\\n❌ TESTS FAILED — Fix failing tests before continuing. Do not move to the next task.' >&2; exit 2; fi; exit 0`,
|
|
77
|
+
description: "Block on test failures after code changes",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── Danger blocker — ALWAYS blocks, even in Standard ───
|
|
82
|
+
hooks.push({
|
|
83
|
+
event: "PreToolUse",
|
|
84
|
+
matcher: "Bash",
|
|
85
|
+
command: `BLOCKED='rm -rf|rm -fr|git push --force|git push -f|DROP TABLE|DROP DATABASE|TRUNCATE TABLE|chmod -R 777'; if echo "$CLAUDE_BASH_COMMAND" | grep -qiE "$BLOCKED"; then echo 'BLOCKED: Destructive command detected. Ask the developer for explicit approval.' >&2; exit 2; fi; exit 0`,
|
|
86
|
+
description: "Block destructive commands (always active)",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ─── Secret detector — ALWAYS blocks, even in Standard ───
|
|
90
|
+
hooks.push({
|
|
91
|
+
event: "PreToolUse",
|
|
92
|
+
matcher: "Bash(git commit*)|Bash(git add*)",
|
|
93
|
+
command: `STAGED=$(git diff --cached --name-only 2>/dev/null); if [ -z "$STAGED" ]; then exit 0; fi; SECRETS=$(git diff --cached | grep -inE '(api[_-]?key|secret|password|token|private[_-]?key|aws_access|AKIA[A-Z0-9]{16})\\s*[:=]\\s*[\"\\x27]?[A-Za-z0-9+/=_-]{8,}' | head -5); if [ -n "$SECRETS" ]; then echo "BLOCKED: Potential secrets in staged files:" >&2; echo "$SECRETS" >&2; exit 2; fi; exit 0`,
|
|
94
|
+
description: "Block commits containing potential secrets (always active)",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ─── Paranoid-only hooks ───
|
|
98
|
+
if (profile === "paranoid") {
|
|
99
|
+
// File size guard — blocks writes to files over 400 lines
|
|
100
|
+
hooks.push({
|
|
101
|
+
event: "PostToolUse",
|
|
102
|
+
matcher: "Write(*)|Edit(*)",
|
|
103
|
+
command: `if [ -f "$CLAUDE_FILE_PATH" ]; then LINES=$(wc -l < "$CLAUDE_FILE_PATH"); if [ "$LINES" -gt 400 ]; then echo "BLOCKED: File $CLAUDE_FILE_PATH has $LINES lines (max 400). Split this file into smaller modules." >&2; exit 2; fi; fi; exit 0`,
|
|
104
|
+
description: "Block files exceeding 400 lines",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Any type guard — blocks writes that introduce `any` type (TypeScript only)
|
|
108
|
+
hooks.push({
|
|
109
|
+
event: "PostToolUse",
|
|
110
|
+
matcher: "Write(*.ts)|Write(*.tsx)|Edit(*.ts)|Edit(*.tsx)",
|
|
111
|
+
command: `if [ -f "$CLAUDE_FILE_PATH" ]; then ANY_COUNT=$(grep -cE ':\\s*any\\b|as\\s+any\\b|<any>' "$CLAUDE_FILE_PATH" 2>/dev/null || echo 0); if [ "$ANY_COUNT" -gt 0 ]; then echo "BLOCKED: File $CLAUDE_FILE_PATH contains $ANY_COUNT uses of 'any' type. Use specific types or 'unknown' instead." >&2; exit 2; fi; fi; exit 0`,
|
|
112
|
+
description: "Block TypeScript files containing 'any' type",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Commit completeness — blocks commits with TODO/FIXME in staged files
|
|
116
|
+
hooks.push({
|
|
117
|
+
event: "PreToolUse",
|
|
118
|
+
matcher: "Bash(git commit*)",
|
|
119
|
+
command: `STAGED=$(git diff --cached --name-only 2>/dev/null); TODOS=""; for f in $STAGED; do if [ -f "$f" ]; then FOUND=$(grep -n 'TODO\\|FIXME\\|HACK\\|XXX' "$f" 2>/dev/null | head -3); if [ -n "$FOUND" ]; then TODOS="$TODOS\\n$f:\\n$FOUND"; fi; fi; done; if [ -n "$TODOS" ]; then echo "BLOCKED: Incomplete work found in staged files:" >&2; echo -e "$TODOS" >&2; echo "\\nRemove all TODO/FIXME/HACK/XXX comments before committing." >&2; exit 2; fi; exit 0`,
|
|
120
|
+
description: "Block commits containing TODO/FIXME/HACK/XXX comments",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return hooks;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Converts HookConfig[] to the settings.json format.
|
|
129
|
+
*/
|
|
130
|
+
export function hooksToSettingsFormat(hooks: HookConfig[]): Record<string, unknown[]> {
|
|
131
|
+
const result: Record<string, unknown[]> = {};
|
|
132
|
+
|
|
133
|
+
for (const hook of hooks) {
|
|
134
|
+
if (!result[hook.event]) {
|
|
135
|
+
result[hook.event] = [];
|
|
136
|
+
}
|
|
137
|
+
result[hook.event].push({
|
|
138
|
+
matcher: hook.matcher,
|
|
139
|
+
hooks: [
|
|
140
|
+
{
|
|
141
|
+
type: "command",
|
|
142
|
+
command: hook.command,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Identifies Rebar-generated hooks by their description prefix.
|
|
153
|
+
*/
|
|
154
|
+
export function isRebarHook(description: string): boolean {
|
|
155
|
+
const rebarPrefixes = [
|
|
156
|
+
"Auto-format",
|
|
157
|
+
"Show lint",
|
|
158
|
+
"Block on lint",
|
|
159
|
+
"Run tests",
|
|
160
|
+
"Block on test",
|
|
161
|
+
"Block destructive",
|
|
162
|
+
"Block commits containing potential",
|
|
163
|
+
"Block files exceeding",
|
|
164
|
+
"Block TypeScript files containing",
|
|
165
|
+
"Block commits containing TODO",
|
|
166
|
+
];
|
|
167
|
+
return rebarPrefixes.some((prefix) => description.startsWith(prefix));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Detects the current strictness profile from hooks in settings.json.
|
|
172
|
+
*/
|
|
173
|
+
export function detectCurrentProfile(hooks: Record<string, unknown[]>): StrictnessProfile | null {
|
|
174
|
+
let hasBlockingLint = false;
|
|
175
|
+
let hasBlockingTests = false;
|
|
176
|
+
let hasFileSizeGuard = false;
|
|
177
|
+
let hasAnyTypeGuard = false;
|
|
178
|
+
|
|
179
|
+
for (const eventHooks of Object.values(hooks)) {
|
|
180
|
+
if (!Array.isArray(eventHooks)) continue;
|
|
181
|
+
for (const hookEntry of eventHooks) {
|
|
182
|
+
const h = hookEntry as Record<string, unknown>;
|
|
183
|
+
const hooksArray = h.hooks as Array<Record<string, unknown>> | undefined;
|
|
184
|
+
if (!hooksArray) continue;
|
|
185
|
+
|
|
186
|
+
for (const hook of hooksArray) {
|
|
187
|
+
const cmd = hook.command as string | undefined;
|
|
188
|
+
if (!cmd) continue;
|
|
189
|
+
|
|
190
|
+
if (cmd.includes("LINT ERRORS") && cmd.includes("exit 2")) hasBlockingLint = true;
|
|
191
|
+
if (cmd.includes("TESTS FAILED") && cmd.includes("exit 2")) hasBlockingTests = true;
|
|
192
|
+
if (cmd.includes("max 400") && cmd.includes("exit 2")) hasFileSizeGuard = true;
|
|
193
|
+
if (cmd.includes("'any' type") && cmd.includes("exit 2")) hasAnyTypeGuard = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (hasFileSizeGuard && hasAnyTypeGuard) return "paranoid";
|
|
199
|
+
if (hasBlockingLint && hasBlockingTests) return "strict";
|
|
200
|
+
if (!hasBlockingLint && !hasBlockingTests) return "standard";
|
|
201
|
+
return null; // Mixed configuration
|
|
202
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight template engine with variable substitution and conditionals.
|
|
3
|
+
* Uses {{variable}} syntax and {{#if condition}}...{{/if}} blocks.
|
|
4
|
+
*/
|
|
5
|
+
import type { TemplateVariables } from "../types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Renders a template string with the given variables.
|
|
9
|
+
*
|
|
10
|
+
* Supports:
|
|
11
|
+
* - {{variable_name}} — replaced with the variable's value
|
|
12
|
+
* - {{#if variable_name}}...{{/if}} — conditional blocks
|
|
13
|
+
* - {{#unless variable_name}}...{{/unless}} — inverse conditional blocks
|
|
14
|
+
* - {{#each variable_name}}...{{/each}} — iteration over arrays
|
|
15
|
+
*/
|
|
16
|
+
export function renderTemplate(
|
|
17
|
+
template: string,
|
|
18
|
+
variables: TemplateVariables
|
|
19
|
+
): string {
|
|
20
|
+
let result = template;
|
|
21
|
+
|
|
22
|
+
// Process {{#each variable}}...{{/each}} blocks
|
|
23
|
+
result = processEachBlocks(result, variables);
|
|
24
|
+
|
|
25
|
+
// Process {{#if variable}}...{{/if}} blocks (supports nesting)
|
|
26
|
+
result = processConditionalBlocks(result, variables);
|
|
27
|
+
|
|
28
|
+
// Process {{#unless variable}}...{{/unless}} blocks
|
|
29
|
+
result = processUnlessBlocks(result, variables);
|
|
30
|
+
|
|
31
|
+
// Replace {{variable}} placeholders
|
|
32
|
+
result = replaceVariables(result, variables);
|
|
33
|
+
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function processEachBlocks(
|
|
38
|
+
template: string,
|
|
39
|
+
variables: TemplateVariables
|
|
40
|
+
): string {
|
|
41
|
+
const eachRegex = /\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g;
|
|
42
|
+
return template.replace(eachRegex, (_match, varName: string, body: string) => {
|
|
43
|
+
const value = variables[varName];
|
|
44
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
return value
|
|
48
|
+
.map((item) => body.replace(/\{\{this\}\}/g, item))
|
|
49
|
+
.join("");
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function processConditionalBlocks(
|
|
54
|
+
template: string,
|
|
55
|
+
variables: TemplateVariables
|
|
56
|
+
): string {
|
|
57
|
+
// Process innermost {{#if}}...{{/if}} first to handle nesting
|
|
58
|
+
let result = template;
|
|
59
|
+
let iterations = 0;
|
|
60
|
+
const maxIterations = 50;
|
|
61
|
+
|
|
62
|
+
while (result.includes("{{#if ") && iterations < maxIterations) {
|
|
63
|
+
const prev = result;
|
|
64
|
+
result = result.replace(
|
|
65
|
+
/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/,
|
|
66
|
+
(_match, varName: string, body: string) => {
|
|
67
|
+
const value = variables[varName];
|
|
68
|
+
if (isTruthy(value)) {
|
|
69
|
+
return body;
|
|
70
|
+
}
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
if (result === prev) break;
|
|
75
|
+
iterations++;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function processUnlessBlocks(
|
|
82
|
+
template: string,
|
|
83
|
+
variables: TemplateVariables
|
|
84
|
+
): string {
|
|
85
|
+
const unlessRegex = /\{\{#unless\s+(\w+)\}\}([\s\S]*?)\{\{\/unless\}\}/g;
|
|
86
|
+
return template.replace(unlessRegex, (_match, varName: string, body: string) => {
|
|
87
|
+
const value = variables[varName];
|
|
88
|
+
if (!isTruthy(value)) {
|
|
89
|
+
return body;
|
|
90
|
+
}
|
|
91
|
+
return "";
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function replaceVariables(
|
|
96
|
+
template: string,
|
|
97
|
+
variables: TemplateVariables
|
|
98
|
+
): string {
|
|
99
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_match, varName: string) => {
|
|
100
|
+
const value = variables[varName];
|
|
101
|
+
if (value === undefined) {
|
|
102
|
+
return `{{${varName}}}`;
|
|
103
|
+
}
|
|
104
|
+
if (Array.isArray(value)) {
|
|
105
|
+
return value.join(", ");
|
|
106
|
+
}
|
|
107
|
+
return String(value);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isTruthy(value: string | boolean | string[] | undefined): boolean {
|
|
112
|
+
if (value === undefined || value === false || value === "") {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
}
|