* feat(tools): add commit tool with validation * feat(tools): add create-pr tool with validation * feat(tools): add create-issue tool with validation * test(tools): add tests for validation tools * docs(handoff): add handoff and ADR for validation tools * docs(handoff): set PR number * docs: update project map + fix ADR-015 typo --------- Co-authored-by: opencode-agent <agent@slaid098.dev>
63 lines
No EOL
2.3 KiB
TypeScript
63 lines
No EOL
2.3 KiB
TypeScript
import { spawnSync } from "child_process"
|
|
import { tool } from "@opencode-ai/plugin"
|
|
|
|
const COMMIT_REGEX = /^(feat|fix|chore|docs|refactor|test|style|perf)\([^)]+\): .{1,72}$/
|
|
const CYRILLIC = /[\u0400-\u04FF]/
|
|
|
|
const RULES = `Rules:
|
|
- Single-line (no newlines)
|
|
- Format: type(scope): description
|
|
- Types: feat, fix, chore, docs, refactor, test, style, perf
|
|
- Scope is mandatory
|
|
- Description: 1-72 characters
|
|
- English only (no Cyrillic)`
|
|
|
|
function recentCommits(worktree) {
|
|
const r = spawnSync("git", ["log", "--oneline", "-5"], {
|
|
encoding: "utf-8",
|
|
cwd: worktree,
|
|
})
|
|
if (r.status !== 0 || !r.stdout.trim()) return "(no commits yet)"
|
|
return r.stdout.trim()
|
|
}
|
|
|
|
export default tool({
|
|
description: "Commit staged files with conventional commit format validation. Validates: single-line, format type(scope): description (<=72 chars description), English only, staged files exist. On error returns rules + recent commits as examples.",
|
|
args: {
|
|
message: tool.schema.string().describe("Commit message (single-line, conventional format: type(scope): description)"),
|
|
},
|
|
async execute(args, context) {
|
|
const msg = args.message
|
|
|
|
if (msg.includes("\n")) {
|
|
return `❌ Commit message must be single-line\n\n${RULES}\n\nRecent commits:\n${recentCommits(context.worktree)}`
|
|
}
|
|
if (!COMMIT_REGEX.test(msg)) {
|
|
return `❌ Commit message must match format: type(scope): description\n\n${RULES}\n\nRecent commits:\n${recentCommits(context.worktree)}`
|
|
}
|
|
if (CYRILLIC.test(msg)) {
|
|
return `❌ Commit message must be in English\n\n${RULES}\n\nRecent commits:\n${recentCommits(context.worktree)}`
|
|
}
|
|
|
|
const staged = spawnSync("git", ["diff", "--cached", "--name-only"], {
|
|
encoding: "utf-8",
|
|
cwd: context.worktree,
|
|
})
|
|
if (staged.status !== 0) {
|
|
return `⚠️ git diff --cached failed (exit ${staged.status}): ${staged.stderr || staged.stdout}`
|
|
}
|
|
if (!staged.stdout.trim()) {
|
|
return `❌ No staged files to commit\n\n${RULES}\n\nRecent commits:\n${recentCommits(context.worktree)}`
|
|
}
|
|
|
|
const r = spawnSync("git", ["commit", "-m", msg], {
|
|
encoding: "utf-8",
|
|
cwd: context.worktree,
|
|
})
|
|
if (r.status !== 0) {
|
|
return `⚠️ git commit failed (exit ${r.status}): ${r.stderr || r.stdout}`
|
|
}
|
|
|
|
return `Committed: ${msg}`
|
|
},
|
|
}) |