forge-dev-framework 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/api-patterns.md +98 -0
- package/.claude/rules/security-baseline.md +204 -0
- package/.claude/rules/testing-standards.md +177 -0
- package/.claude/rules/ui-conventions.md +142 -0
- package/README.md +261 -0
- package/bin/forge.js +14 -0
- package/dist/bin/forge.js +14 -0
- package/dist/cli/index.d.ts +22 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +116 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/commands/base.d.ts +31 -0
- package/dist/commands/base.d.ts.map +1 -0
- package/dist/commands/base.js +31 -0
- package/dist/commands/base.js.map +1 -0
- package/dist/commands/config.d.ts +14 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +175 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/generate.d.ts +17 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +159 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/help.d.ts +11 -0
- package/dist/commands/help.d.ts.map +1 -0
- package/dist/commands/help.js +65 -0
- package/dist/commands/help.js.map +1 -0
- package/dist/commands/index.d.ts +8 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +8 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +22 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/status.d.ts +13 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +101 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/stubs.d.ts +14 -0
- package/dist/commands/stubs.d.ts.map +1 -0
- package/dist/commands/stubs.js +30 -0
- package/dist/commands/stubs.js.map +1 -0
- package/dist/generators/index.d.ts +11 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +10 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/required-fields.d.ts +74 -0
- package/dist/generators/required-fields.d.ts.map +1 -0
- package/dist/generators/required-fields.js +179 -0
- package/dist/generators/required-fields.js.map +1 -0
- package/dist/generators/template-engine.d.ts +65 -0
- package/dist/generators/template-engine.d.ts.map +1 -0
- package/dist/generators/template-engine.js +209 -0
- package/dist/generators/template-engine.js.map +1 -0
- package/dist/generators/token-validator.d.ts +51 -0
- package/dist/generators/token-validator.d.ts.map +1 -0
- package/dist/generators/token-validator.js +141 -0
- package/dist/generators/token-validator.js.map +1 -0
- package/dist/generators/types.d.ts +433 -0
- package/dist/generators/types.d.ts.map +1 -0
- package/dist/generators/types.js +5 -0
- package/dist/generators/types.js.map +1 -0
- package/dist/generators/xml-task-generator.d.ts +67 -0
- package/dist/generators/xml-task-generator.d.ts.map +1 -0
- package/dist/generators/xml-task-generator.js +297 -0
- package/dist/generators/xml-task-generator.js.map +1 -0
- package/dist/git/__tests__/worktree.test.d.ts +5 -0
- package/dist/git/__tests__/worktree.test.d.ts.map +1 -0
- package/dist/git/__tests__/worktree.test.js +121 -0
- package/dist/git/__tests__/worktree.test.js.map +1 -0
- package/dist/git/codeowners.d.ts +101 -0
- package/dist/git/codeowners.d.ts.map +1 -0
- package/dist/git/codeowners.js +216 -0
- package/dist/git/codeowners.js.map +1 -0
- package/dist/git/commit.d.ts +135 -0
- package/dist/git/commit.d.ts.map +1 -0
- package/dist/git/commit.js +223 -0
- package/dist/git/commit.js.map +1 -0
- package/dist/git/hooks/commit-msg.d.ts +8 -0
- package/dist/git/hooks/commit-msg.d.ts.map +1 -0
- package/dist/git/hooks/commit-msg.js +34 -0
- package/dist/git/hooks/commit-msg.js.map +1 -0
- package/dist/git/hooks/pre-commit.d.ts +8 -0
- package/dist/git/hooks/pre-commit.d.ts.map +1 -0
- package/dist/git/hooks/pre-commit.js +34 -0
- package/dist/git/hooks/pre-commit.js.map +1 -0
- package/dist/git/pre-commit-hooks.d.ts +117 -0
- package/dist/git/pre-commit-hooks.d.ts.map +1 -0
- package/dist/git/pre-commit-hooks.js +270 -0
- package/dist/git/pre-commit-hooks.js.map +1 -0
- package/dist/git/wipe-protocol.d.ts +281 -0
- package/dist/git/wipe-protocol.d.ts.map +1 -0
- package/dist/git/wipe-protocol.js +237 -0
- package/dist/git/wipe-protocol.js.map +1 -0
- package/dist/git/worktree.d.ts +69 -0
- package/dist/git/worktree.d.ts.map +1 -0
- package/dist/git/worktree.js +202 -0
- package/dist/git/worktree.js.map +1 -0
- package/dist/scripts/install.d.ts +8 -0
- package/dist/scripts/install.d.ts.map +1 -0
- package/dist/scripts/install.js +161 -0
- package/dist/scripts/install.js.map +1 -0
- package/dist/types/config.d.ts +30 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +23 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/state.d.ts +56 -0
- package/dist/types/state.d.ts.map +1 -0
- package/dist/types/state.js +6 -0
- package/dist/types/state.js.map +1 -0
- package/dist/utils/config.d.ts +15 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +80 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/errors.d.ts +25 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +48 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/index.d.ts +11 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +34 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +73 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/state-api.d.ts +128 -0
- package/dist/utils/state-api.d.ts.map +1 -0
- package/dist/utils/state-api.js +170 -0
- package/dist/utils/state-api.js.map +1 -0
- package/dist/utils/template-client.d.ts +73 -0
- package/dist/utils/template-client.d.ts.map +1 -0
- package/dist/utils/template-client.js +151 -0
- package/dist/utils/template-client.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate .github/CODEOWNERS from task ownership maps.
|
|
3
|
+
*
|
|
4
|
+
* The CODEOWNERS file defines canonical ownership for code paths.
|
|
5
|
+
* GitHub enforces this at merge time - changes require owner approval.
|
|
6
|
+
*
|
|
7
|
+
* Generated format:
|
|
8
|
+
* # Auto-generated by FORGE
|
|
9
|
+
* src/api/** @backend-agent
|
|
10
|
+
* src/ui/** @frontend-agent
|
|
11
|
+
* src/tests/** @qa-agent
|
|
12
|
+
* docs/** @docs-agent
|
|
13
|
+
*/
|
|
14
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
// Role to GitHub username/team mapping
|
|
17
|
+
// In production, this would come from config
|
|
18
|
+
const ROLE_TO_OWNER = {
|
|
19
|
+
backend: "@backend-agent",
|
|
20
|
+
frontend: "@frontend-agent",
|
|
21
|
+
qa: "@qa-agent",
|
|
22
|
+
docs: "@docs-agent",
|
|
23
|
+
devops: "@devops-agent",
|
|
24
|
+
architect: "@architect",
|
|
25
|
+
"team-lead": "@team-lead",
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Generate CODEOWNERS content from task ownership.
|
|
29
|
+
*
|
|
30
|
+
* Aggregates allowedPaths by owner role and formats for GitHub.
|
|
31
|
+
*
|
|
32
|
+
* @param state - FORGE state with task ownership
|
|
33
|
+
* @returns CODEOWNERS file content
|
|
34
|
+
*/
|
|
35
|
+
export function generateCodeownersContent(state) {
|
|
36
|
+
const lines = [];
|
|
37
|
+
// Header
|
|
38
|
+
lines.push("# Auto-generated by FORGE");
|
|
39
|
+
lines.push("# DO NOT EDIT MANually - Regenerate with forge generate-codeowners");
|
|
40
|
+
lines.push("");
|
|
41
|
+
// Aggregate paths by owner role
|
|
42
|
+
const pathsByOwner = new Map();
|
|
43
|
+
for (const task of state.tasks) {
|
|
44
|
+
const owner = ROLE_TO_OWNER[task.ownerRole] || `@${task.ownerRole}`;
|
|
45
|
+
if (!pathsByOwner.has(owner)) {
|
|
46
|
+
pathsByOwner.set(owner, new Set());
|
|
47
|
+
}
|
|
48
|
+
const ownerPaths = pathsByOwner.get(owner);
|
|
49
|
+
for (const path of task.allowedPaths) {
|
|
50
|
+
ownerPaths.add(path);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Format: sorted by owner, then by path
|
|
54
|
+
const sortedOwners = Array.from(pathsByOwner.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
55
|
+
for (const [owner, paths] of sortedOwners) {
|
|
56
|
+
// Sort paths for consistency
|
|
57
|
+
const sortedPaths = Array.from(paths).sort();
|
|
58
|
+
for (const path of sortedPaths) {
|
|
59
|
+
// Ensure path is in gitignore-style format
|
|
60
|
+
const gitPath = path.startsWith("/") ? path.slice(1) : path;
|
|
61
|
+
lines.push(`${gitPath} ${owner}`);
|
|
62
|
+
}
|
|
63
|
+
lines.push(""); // Blank line between owners
|
|
64
|
+
}
|
|
65
|
+
// Add default fallback (team-lead owns everything else)
|
|
66
|
+
lines.push(`# Default fallback`);
|
|
67
|
+
lines.push(`* ${ROLE_TO_OWNER["team-lead"] || "@team-lead"}`);
|
|
68
|
+
return lines.join("\n");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Write CODEOWNERS file to disk.
|
|
72
|
+
*
|
|
73
|
+
* @param state - FORGE state with task ownership
|
|
74
|
+
* @param basePath - Root of the FORGE project
|
|
75
|
+
* @param forceOverwrite - Whether to overwrite existing file
|
|
76
|
+
* @throws Error if file exists and forceOverwrite is false
|
|
77
|
+
*/
|
|
78
|
+
export async function writeCodeowners(state, basePath = "/home/parz/projects/forge", forceOverwrite = false) {
|
|
79
|
+
const codeownersPath = join(basePath, ".github", "CODEOWNERS");
|
|
80
|
+
// Check if file exists
|
|
81
|
+
try {
|
|
82
|
+
await writeFile(codeownersPath, "", { flag: "rx" });
|
|
83
|
+
if (!forceOverwrite) {
|
|
84
|
+
throw new Error("CODEOWNERS file already exists. Use forceOverwrite=true to replace it.");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// File doesn't exist, which is fine
|
|
89
|
+
}
|
|
90
|
+
// Ensure .github directory exists
|
|
91
|
+
await mkdir(join(basePath, ".github"), { recursive: true });
|
|
92
|
+
// Generate content
|
|
93
|
+
const content = generateCodeownersContent(state);
|
|
94
|
+
// Write file
|
|
95
|
+
await writeFile(codeownersPath, content, "utf-8");
|
|
96
|
+
}
|
|
97
|
+
export function parseCodeowners(content) {
|
|
98
|
+
const entries = [];
|
|
99
|
+
const lines = content.split("\n");
|
|
100
|
+
for (const line of lines) {
|
|
101
|
+
const trimmed = line.trim();
|
|
102
|
+
// Skip comments and empty lines
|
|
103
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
// Parse "pattern owner"
|
|
107
|
+
const parts = trimmed.split(/\s+/);
|
|
108
|
+
if (parts.length >= 2) {
|
|
109
|
+
const [pattern, owner] = parts;
|
|
110
|
+
entries.push({ pattern, owner });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return entries;
|
|
114
|
+
}
|
|
115
|
+
export function validateCodeowners(entries) {
|
|
116
|
+
const errors = [];
|
|
117
|
+
// Check for invalid patterns (basic validation)
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
try {
|
|
120
|
+
// Convert gitignore pattern to regex for basic validation
|
|
121
|
+
const regexPattern = entry.pattern
|
|
122
|
+
.replace(/\*\*/g, ".*")
|
|
123
|
+
.replace(/\*/g, "[^/]*")
|
|
124
|
+
.replace(/\?/g, "[^/]");
|
|
125
|
+
new RegExp(regexPattern);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
errors.push({
|
|
129
|
+
type: "invalid-pattern",
|
|
130
|
+
message: `Invalid pattern: ${entry.pattern}`,
|
|
131
|
+
entries: [entry],
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
// Check for missing owner
|
|
135
|
+
if (!entry.owner || !entry.owner.startsWith("@")) {
|
|
136
|
+
errors.push({
|
|
137
|
+
type: "missing-owner",
|
|
138
|
+
message: `Missing or invalid owner for pattern: ${entry.pattern}`,
|
|
139
|
+
entries: [entry],
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Check for overlapping patterns (same file owned by different roles)
|
|
144
|
+
// This is a simplified check - real validation would need path matching
|
|
145
|
+
const byOwner = new Map();
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
if (!byOwner.has(entry.owner)) {
|
|
148
|
+
byOwner.set(entry.owner, []);
|
|
149
|
+
}
|
|
150
|
+
byOwner.get(entry.owner).push(entry.pattern);
|
|
151
|
+
}
|
|
152
|
+
// TODO: Add sophisticated overlap detection
|
|
153
|
+
return {
|
|
154
|
+
isValid: errors.length === 0,
|
|
155
|
+
errors,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export function diffCodeowners(current, proposed) {
|
|
159
|
+
const currentEntries = parseCodeowners(current);
|
|
160
|
+
const proposedEntries = parseCodeowners(proposed);
|
|
161
|
+
// Create maps for easy comparison
|
|
162
|
+
const currentMap = new Map();
|
|
163
|
+
const proposedMap = new Map();
|
|
164
|
+
for (const entry of currentEntries) {
|
|
165
|
+
const key = `${entry.pattern}|${entry.owner}`;
|
|
166
|
+
currentMap.set(key, entry);
|
|
167
|
+
}
|
|
168
|
+
for (const entry of proposedEntries) {
|
|
169
|
+
const key = `${entry.pattern}|${entry.owner}`;
|
|
170
|
+
proposedMap.set(key, entry);
|
|
171
|
+
}
|
|
172
|
+
// Find differences
|
|
173
|
+
const added = [];
|
|
174
|
+
const removed = [];
|
|
175
|
+
const changed = [];
|
|
176
|
+
// Check for removed and changed entries
|
|
177
|
+
for (const [key, entry] of currentMap) {
|
|
178
|
+
const pattern = entry.pattern;
|
|
179
|
+
// Find if this pattern exists in proposed with different owner
|
|
180
|
+
const proposedWithSamePattern = Array.from(proposedMap.values()).find((e) => e.pattern === pattern);
|
|
181
|
+
if (!proposedWithSamePattern) {
|
|
182
|
+
removed.push(entry);
|
|
183
|
+
}
|
|
184
|
+
else if (proposedWithSamePattern.owner !== entry.owner) {
|
|
185
|
+
changed.push({ from: entry, to: proposedWithSamePattern });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Check for added entries
|
|
189
|
+
for (const [key, entry] of proposedMap) {
|
|
190
|
+
if (!currentMap.has(key)) {
|
|
191
|
+
const pattern = entry.pattern;
|
|
192
|
+
const currentWithSamePattern = Array.from(currentMap.values()).find((e) => e.pattern === pattern);
|
|
193
|
+
// Only add if it's truly new, not a change (already captured above)
|
|
194
|
+
if (!currentWithSamePattern) {
|
|
195
|
+
added.push(entry);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { added, removed, changed };
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Generate CODEOWNERS from STATE.json file.
|
|
203
|
+
*
|
|
204
|
+
* This is the main entry point for CODEOWNERS generation.
|
|
205
|
+
*
|
|
206
|
+
* @param stateJsonPath - Path to STATE.json
|
|
207
|
+
* @param basePath - Root of the FORGE project
|
|
208
|
+
* @param forceOverwrite - Whether to overwrite existing file
|
|
209
|
+
*/
|
|
210
|
+
export async function generateCodeownersFromState(stateJsonPath, basePath = "/home/parz/projects/forge", forceOverwrite = false) {
|
|
211
|
+
const { readFile } = await import("node:fs/promises");
|
|
212
|
+
const stateRaw = await readFile(stateJsonPath, "utf-8");
|
|
213
|
+
const state = JSON.parse(stateRaw);
|
|
214
|
+
await writeCodeowners(state, basePath, forceOverwrite);
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=codeowners.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codeowners.js","sourceRoot":"","sources":["../../src/git/codeowners.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic Commit Enforcement for FORGE
|
|
3
|
+
*
|
|
4
|
+
* Enforces conventional commit format with task ID in scope:
|
|
5
|
+
* type(scope): description
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* - feat(api-003): implement session authentication
|
|
9
|
+
* - fix(ui-002): resolve login form validation bug
|
|
10
|
+
* - test(core-001): add database integration tests
|
|
11
|
+
* - docs(readme): update installation instructions
|
|
12
|
+
*
|
|
13
|
+
* Rules:
|
|
14
|
+
* - Every task = one commit
|
|
15
|
+
* - Commit message MUST include task ID in scope
|
|
16
|
+
* - Pre-commit hooks validate format
|
|
17
|
+
* - Reject commits that don't match pattern
|
|
18
|
+
*/
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
export declare const CommitType: z.ZodEnum<["feat", "fix", "docs", "style", "refactor", "perf", "test", "chore", "revert"]>;
|
|
21
|
+
export type CommitType = z.infer<typeof CommitType>;
|
|
22
|
+
export declare class CommitFormatError extends Error {
|
|
23
|
+
readonly commitMessage: string;
|
|
24
|
+
constructor(message: string, commitMessage: string);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Parsed commit message components.
|
|
28
|
+
*/
|
|
29
|
+
export interface ParsedCommit {
|
|
30
|
+
type: CommitType;
|
|
31
|
+
scope: string;
|
|
32
|
+
description: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parse a commit message into its components.
|
|
36
|
+
*
|
|
37
|
+
* @param message - The commit message to parse
|
|
38
|
+
* @returns Parsed commit components
|
|
39
|
+
* @throws CommitFormatError if format is invalid
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseCommitMessage(message: string): ParsedCommit;
|
|
42
|
+
/**
|
|
43
|
+
* Format a commit message from components.
|
|
44
|
+
*
|
|
45
|
+
* @param type - Commit type
|
|
46
|
+
* @param scope - Task ID (e.g., api-003)
|
|
47
|
+
* @param description - Commit description
|
|
48
|
+
* @returns Formatted commit message
|
|
49
|
+
*/
|
|
50
|
+
export declare function formatCommitMessage(type: CommitType, scope: string, description: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* Validate a commit message against FORGE standards.
|
|
53
|
+
*
|
|
54
|
+
* @param message - The commit message to validate
|
|
55
|
+
* @returns True if valid
|
|
56
|
+
* @throws CommitFormatError if invalid
|
|
57
|
+
*/
|
|
58
|
+
export declare function validateCommitMessage(message: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Check if a commit message is for a specific task.
|
|
61
|
+
*
|
|
62
|
+
* @param message - The commit message
|
|
63
|
+
* @param taskId - The task ID to check
|
|
64
|
+
* @returns True if the commit is for the specified task
|
|
65
|
+
*/
|
|
66
|
+
export declare function isCommitForTask(message: string, taskId: string): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Extract task ID from a commit message.
|
|
69
|
+
*
|
|
70
|
+
* @param message - The commit message
|
|
71
|
+
* @returns Task ID or null if format is invalid
|
|
72
|
+
*/
|
|
73
|
+
export declare function extractTaskId(message: string): string | null;
|
|
74
|
+
/**
|
|
75
|
+
* Commit message options for formatting.
|
|
76
|
+
*/
|
|
77
|
+
export interface CommitMessageOptions {
|
|
78
|
+
type: CommitType;
|
|
79
|
+
taskId: string;
|
|
80
|
+
description: string;
|
|
81
|
+
body?: string;
|
|
82
|
+
footer?: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Create a full commit message with optional body and footer.
|
|
86
|
+
*
|
|
87
|
+
* @param options - Commit message components
|
|
88
|
+
* @returns Formatted commit message
|
|
89
|
+
*/
|
|
90
|
+
export declare function createCommitMessage(options: CommitMessageOptions): string;
|
|
91
|
+
/**
|
|
92
|
+
* Batch validate multiple commit messages.
|
|
93
|
+
*
|
|
94
|
+
* Useful for pre-commit hooks that check all staged commits.
|
|
95
|
+
*
|
|
96
|
+
* @param messages - Array of commit messages
|
|
97
|
+
* @returns Object with validation results
|
|
98
|
+
*/
|
|
99
|
+
export declare function validateCommitMessages(messages: string[]): {
|
|
100
|
+
valid: boolean;
|
|
101
|
+
errors: Array<{
|
|
102
|
+
message: string;
|
|
103
|
+
error: string;
|
|
104
|
+
}>;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Generate a commit message from task information.
|
|
108
|
+
*
|
|
109
|
+
* This is a helper for agents to generate properly formatted commits.
|
|
110
|
+
*
|
|
111
|
+
* @param taskId - The task ID
|
|
112
|
+
* @param taskTitle - The task title (will be converted to description)
|
|
113
|
+
* @param type - Commit type (defaults to "feat")
|
|
114
|
+
* @returns Formatted commit message
|
|
115
|
+
*/
|
|
116
|
+
export declare function generateCommitFromTask(taskId: string, taskTitle: string, type?: CommitType): string;
|
|
117
|
+
/**
|
|
118
|
+
* Commit validation result for pre-commit hooks.
|
|
119
|
+
*/
|
|
120
|
+
export interface CommitValidationResult {
|
|
121
|
+
isValid: boolean;
|
|
122
|
+
taskId: string | null;
|
|
123
|
+
errors: string[];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Validate a commit for pre-commit hook usage.
|
|
127
|
+
*
|
|
128
|
+
* This is the main entry point for pre-commit hooks.
|
|
129
|
+
*
|
|
130
|
+
* @param message - The commit message to validate
|
|
131
|
+
* @param expectedTaskId - Optional expected task ID
|
|
132
|
+
* @returns Validation result
|
|
133
|
+
*/
|
|
134
|
+
export declare function validateForPreCommit(message: string, expectedTaskId?: string): CommitValidationResult;
|
|
135
|
+
//# sourceMappingURL=commit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commit.d.ts","sourceRoot":"","sources":["../../src/git/commit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,UAAU,4FAUrB,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAMpD,qBAAa,iBAAkB,SAAQ,KAAK;aACG,aAAa,EAAE,MAAM;gBAAtD,OAAO,EAAE,MAAM,EAAkB,aAAa,EAAE,MAAM;CAInE;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,CAuChE;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB,MAAM,CAER;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAG9D;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAOxE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO5D;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,MAAM,CAczE;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG;IAC1D,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACnD,CAoBA;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,UAAmB,GACxB,MAAM,CAMR;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,cAAc,CAAC,EAAE,MAAM,GACtB,sBAAsB,CA2BxB"}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic Commit Enforcement for FORGE
|
|
3
|
+
*
|
|
4
|
+
* Enforces conventional commit format with task ID in scope:
|
|
5
|
+
* type(scope): description
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* - feat(api-003): implement session authentication
|
|
9
|
+
* - fix(ui-002): resolve login form validation bug
|
|
10
|
+
* - test(core-001): add database integration tests
|
|
11
|
+
* - docs(readme): update installation instructions
|
|
12
|
+
*
|
|
13
|
+
* Rules:
|
|
14
|
+
* - Every task = one commit
|
|
15
|
+
* - Commit message MUST include task ID in scope
|
|
16
|
+
* - Pre-commit hooks validate format
|
|
17
|
+
* - Reject commits that don't match pattern
|
|
18
|
+
*/
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
// Conventional commit types
|
|
21
|
+
export const CommitType = z.enum([
|
|
22
|
+
"feat",
|
|
23
|
+
"fix",
|
|
24
|
+
"docs",
|
|
25
|
+
"style",
|
|
26
|
+
"refactor",
|
|
27
|
+
"perf",
|
|
28
|
+
"test",
|
|
29
|
+
"chore",
|
|
30
|
+
"revert",
|
|
31
|
+
]);
|
|
32
|
+
// Commit message format: type(scope): description
|
|
33
|
+
// Scope MUST be a task ID (e.g., api-003, ui-002)
|
|
34
|
+
const COMMIT_PATTERN = /^([a-z]+)\(([a-z]+-[0-9]+)\):\s+(.+)$/;
|
|
35
|
+
export class CommitFormatError extends Error {
|
|
36
|
+
commitMessage;
|
|
37
|
+
constructor(message, commitMessage) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.commitMessage = commitMessage;
|
|
40
|
+
this.name = "CommitFormatError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse a commit message into its components.
|
|
45
|
+
*
|
|
46
|
+
* @param message - The commit message to parse
|
|
47
|
+
* @returns Parsed commit components
|
|
48
|
+
* @throws CommitFormatError if format is invalid
|
|
49
|
+
*/
|
|
50
|
+
export function parseCommitMessage(message) {
|
|
51
|
+
const match = message.match(COMMIT_PATTERN);
|
|
52
|
+
if (!match) {
|
|
53
|
+
throw new CommitFormatError(`Commit message must follow format "type(scope): description". Got: "${message}"`, message);
|
|
54
|
+
}
|
|
55
|
+
const [, typeRaw, scope, description] = match;
|
|
56
|
+
// Validate commit type
|
|
57
|
+
const typeResult = CommitType.safeParse(typeRaw);
|
|
58
|
+
if (!typeResult.success) {
|
|
59
|
+
throw new CommitFormatError(`Invalid commit type "${typeRaw}". Must be one of: ${CommitType.options.join(", ")}`, message);
|
|
60
|
+
}
|
|
61
|
+
// Validate scope format (task ID: domain-number)
|
|
62
|
+
if (!/^[a-z]+-[0-9]+$/.test(scope)) {
|
|
63
|
+
throw new CommitFormatError(`Invalid scope "${scope}". Scope must be a task ID in format "domain-number" (e.g., api-003)`, message);
|
|
64
|
+
}
|
|
65
|
+
// Validate description is not empty
|
|
66
|
+
if (!description || description.trim().length === 0) {
|
|
67
|
+
throw new CommitFormatError(`Commit description cannot be empty`, message);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
type: typeResult.data,
|
|
71
|
+
scope,
|
|
72
|
+
description,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Format a commit message from components.
|
|
77
|
+
*
|
|
78
|
+
* @param type - Commit type
|
|
79
|
+
* @param scope - Task ID (e.g., api-003)
|
|
80
|
+
* @param description - Commit description
|
|
81
|
+
* @returns Formatted commit message
|
|
82
|
+
*/
|
|
83
|
+
export function formatCommitMessage(type, scope, description) {
|
|
84
|
+
return `${type}(${scope}): ${description}`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Validate a commit message against FORGE standards.
|
|
88
|
+
*
|
|
89
|
+
* @param message - The commit message to validate
|
|
90
|
+
* @returns True if valid
|
|
91
|
+
* @throws CommitFormatError if invalid
|
|
92
|
+
*/
|
|
93
|
+
export function validateCommitMessage(message) {
|
|
94
|
+
parseCommitMessage(message);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Check if a commit message is for a specific task.
|
|
99
|
+
*
|
|
100
|
+
* @param message - The commit message
|
|
101
|
+
* @param taskId - The task ID to check
|
|
102
|
+
* @returns True if the commit is for the specified task
|
|
103
|
+
*/
|
|
104
|
+
export function isCommitForTask(message, taskId) {
|
|
105
|
+
try {
|
|
106
|
+
const parsed = parseCommitMessage(message);
|
|
107
|
+
return parsed.scope === taskId;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Extract task ID from a commit message.
|
|
115
|
+
*
|
|
116
|
+
* @param message - The commit message
|
|
117
|
+
* @returns Task ID or null if format is invalid
|
|
118
|
+
*/
|
|
119
|
+
export function extractTaskId(message) {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = parseCommitMessage(message);
|
|
122
|
+
return parsed.scope;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Create a full commit message with optional body and footer.
|
|
130
|
+
*
|
|
131
|
+
* @param options - Commit message components
|
|
132
|
+
* @returns Formatted commit message
|
|
133
|
+
*/
|
|
134
|
+
export function createCommitMessage(options) {
|
|
135
|
+
const { type, taskId, description, body, footer } = options;
|
|
136
|
+
let message = formatCommitMessage(type, taskId, description);
|
|
137
|
+
if (body) {
|
|
138
|
+
message += `\n\n${body}`;
|
|
139
|
+
}
|
|
140
|
+
if (footer) {
|
|
141
|
+
message += `\n\n${footer}`;
|
|
142
|
+
}
|
|
143
|
+
return message;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Batch validate multiple commit messages.
|
|
147
|
+
*
|
|
148
|
+
* Useful for pre-commit hooks that check all staged commits.
|
|
149
|
+
*
|
|
150
|
+
* @param messages - Array of commit messages
|
|
151
|
+
* @returns Object with validation results
|
|
152
|
+
*/
|
|
153
|
+
export function validateCommitMessages(messages) {
|
|
154
|
+
const errors = [];
|
|
155
|
+
for (const message of messages) {
|
|
156
|
+
try {
|
|
157
|
+
validateCommitMessage(message);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
if (error instanceof CommitFormatError) {
|
|
161
|
+
errors.push({
|
|
162
|
+
message,
|
|
163
|
+
error: error.message,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
valid: errors.length === 0,
|
|
170
|
+
errors,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Generate a commit message from task information.
|
|
175
|
+
*
|
|
176
|
+
* This is a helper for agents to generate properly formatted commits.
|
|
177
|
+
*
|
|
178
|
+
* @param taskId - The task ID
|
|
179
|
+
* @param taskTitle - The task title (will be converted to description)
|
|
180
|
+
* @param type - Commit type (defaults to "feat")
|
|
181
|
+
* @returns Formatted commit message
|
|
182
|
+
*/
|
|
183
|
+
export function generateCommitFromTask(taskId, taskTitle, type = "feat") {
|
|
184
|
+
// Convert task title to commit description
|
|
185
|
+
// Lowercase first letter if it's capitalized
|
|
186
|
+
const description = taskTitle.charAt(0).toLowerCase() + taskTitle.slice(1);
|
|
187
|
+
return formatCommitMessage(type, taskId, description);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Validate a commit for pre-commit hook usage.
|
|
191
|
+
*
|
|
192
|
+
* This is the main entry point for pre-commit hooks.
|
|
193
|
+
*
|
|
194
|
+
* @param message - The commit message to validate
|
|
195
|
+
* @param expectedTaskId - Optional expected task ID
|
|
196
|
+
* @returns Validation result
|
|
197
|
+
*/
|
|
198
|
+
export function validateForPreCommit(message, expectedTaskId) {
|
|
199
|
+
const errors = [];
|
|
200
|
+
let taskId = null;
|
|
201
|
+
try {
|
|
202
|
+
const parsed = parseCommitMessage(message);
|
|
203
|
+
taskId = parsed.scope;
|
|
204
|
+
// If expected task ID is provided, check for match
|
|
205
|
+
if (expectedTaskId && parsed.scope !== expectedTaskId) {
|
|
206
|
+
errors.push(`Commit scope "${parsed.scope}" does not match expected task ID "${expectedTaskId}"`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (error instanceof CommitFormatError) {
|
|
211
|
+
errors.push(error.message);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
errors.push("Unknown validation error");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
isValid: errors.length === 0,
|
|
219
|
+
taskId,
|
|
220
|
+
errors,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=commit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commit.js","sourceRoot":"","sources":["../../src/git/commit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commit-msg.d.ts","sourceRoot":"","sources":["../../../src/git/hooks/commit-msg.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commit Message Hook Implementation
|
|
3
|
+
*
|
|
4
|
+
* This is the TypeScript file that the shell hook calls.
|
|
5
|
+
* It validates the commit message format.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { runCommitMsgValidation } from "../pre-commit-hooks.js";
|
|
9
|
+
async function main() {
|
|
10
|
+
// Get commit message from the temp file (git passes it as first arg)
|
|
11
|
+
const commitMsgFile = process.argv[2];
|
|
12
|
+
if (!commitMsgFile) {
|
|
13
|
+
console.error("Usage: commit-msg <commit-message-file>");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const commitMessage = readFileSync(commitMsgFile, "utf-8").trim();
|
|
17
|
+
// Validate commit message
|
|
18
|
+
const result = runCommitMsgValidation(commitMessage);
|
|
19
|
+
if (!result.isValid) {
|
|
20
|
+
console.error("❌ FORGE Commit Message Validation Failed:\n");
|
|
21
|
+
for (const error of result.errors) {
|
|
22
|
+
console.error(` - ${error}\n`);
|
|
23
|
+
}
|
|
24
|
+
console.error("Commit message must follow format: type(scope): description\n" +
|
|
25
|
+
"Example: feat(api-003): implement session authentication");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
main().catch((error) => {
|
|
31
|
+
console.error("Commit-msg hook error:", error);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
34
|
+
//# sourceMappingURL=commit-msg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commit-msg.js","sourceRoot":"","sources":["../../../src/git/hooks/commit-msg.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAEhE,KAAK,UAAU,IAAI;IACjB,qEAAqE;IACrE,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAEtC,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,OAAO,CAAC,KAAK,CAAC,yCAAyC,CAAC,CAAC;QACzD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,aAAa,GAAG,YAAY,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IAElE,0BAA0B;IAC1B,MAAM,MAAM,GAAG,sBAAsB,CAAC,aAAa,CAAC,CAAC;IAErD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,CAAC,KAAK,CAAC,6CAA6C,CAAC,CAAC;QAC7D,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAClC,OAAO,CAAC,KAAK,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC;QAClC,CAAC;QACD,OAAO,CAAC,KAAK,CACX,+DAA+D;YAC7D,0DAA0D,CAC7D,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,KAAK,CAAC,CAAC;IAC/C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pre-commit.d.ts","sourceRoot":"","sources":["../../../src/git/hooks/pre-commit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|