opencode-commits 0.0.1-next.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/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # opencode-commits
2
+
3
+ An [OpenCode](https://opencode.ai) plugin that enforces [Conventional Commits](https://www.conventionalcommits.org/) by validating commit messages before they reach git. Provides tools for committing, amending, diffing, and viewing git log -- all with built-in message parsing and validation.
4
+
5
+ ## Quick Start
6
+
7
+ Add `opencode-commits` to your `opencode.json` plugin list:
8
+
9
+ ```json
10
+ {
11
+ "plugin": [
12
+ "opencode-commits"
13
+ ]
14
+ }
15
+ ```
16
+
17
+ ## Tools
18
+
19
+ ### git-commit
20
+
21
+ Validate and commit staged changes with a conventional commit message.
22
+
23
+ | Parameter | Type | Required | Description |
24
+ | --------- | ------ | -------- | --------------------------------- |
25
+ | message | string | Yes | Conventional commit message |
26
+
27
+ Returns the git commit output on success. Rejects with validation errors and suggestions if the message is malformed.
28
+
29
+ ### git-amend
30
+
31
+ Amend the last commit with a new validated conventional commit message.
32
+
33
+ | Parameter | Type | Required | Description |
34
+ | --------- | ------ | -------- | --------------------------------- |
35
+ | message | string | Yes | Conventional commit message |
36
+
37
+ ### git-diff
38
+
39
+ Show the currently staged diff.
40
+
41
+ No parameters. Returns the output of `git diff --staged`, or a notice if nothing is staged.
42
+
43
+ ### git-status
44
+
45
+ Show the working tree status including staged, unstaged, and untracked files.
46
+
47
+ No parameters. Returns the output of `git status`.
48
+
49
+ ### git-log
50
+
51
+ List recent commits.
52
+
53
+ | Parameter | Type | Required | Description |
54
+ | --------- | ------ | -------- | ------------------------------------ |
55
+ | count | number | No | Number of commits to show (default: 10) |
56
+
57
+ Returns the output of `git log --oneline -n <count>`.
58
+
59
+ ## Conventional Commits
60
+
61
+ All commit and amend operations validate messages against the [Conventional Commits](https://www.conventionalcommits.org/) specification before executing.
62
+
63
+ ### Format
64
+
65
+ ```
66
+ <type>[(<scope>)]: <description>
67
+ ```
68
+
69
+ ### Supported Types
70
+
71
+ `feat` `fix` `build` `chore` `ci` `docs` `style` `refactor` `perf` `test` `revert`
72
+
73
+ ### Scopes
74
+
75
+ Scopes are optional and configurable. When provided, they must:
76
+
77
+ - Start with a lowercase letter
78
+ - Contain only letters, numbers, and hyphens
79
+ - Be enclosed in parentheses after the type
80
+
81
+ ### Validation Rules
82
+
83
+ - **Type** must be one of the supported types (lowercase only)
84
+ - **Scope** (if present) must follow the naming rules above
85
+ - **Description** must start with a lowercase letter
86
+ - **Description** must not end with punctuation (`.` `!` `,` `;` `:`)
87
+ - **Total message length** must not exceed 72 characters
88
+
89
+ Invalid messages are rejected with specific error details and suggestions for how to fix them.
90
+
91
+ ### Unsupported Features
92
+
93
+ - **Breaking change indicator (`!`)** is not currently supported. Messages like `feat!: description` or `feat(scope)!: description` will be rejected by the parser.
94
+
95
+ To ensure all git commit operations go through the plugin's validation, disable direct `git commit` access in your `opencode.json` permissions. This forces the agent to use the plugin's tools instead of calling git directly, so every commit is validated against your conventions.
96
+
97
+ ```json
98
+ {
99
+ "plugin": [
100
+ "opencode-commits"
101
+ ],
102
+ "permission": {
103
+ "bash": {
104
+ "git commit *": "deny",
105
+ "git commit": "deny"
106
+ }
107
+ }
108
+ }
109
+ ```
110
+
111
+ This blocks the agent from running `git commit` directly through the shell, forcing it to use the plugin's validated tools instead. Other git commands like `git status` and `git add` remain unaffected.
112
+
113
+ ## Configuration
114
+
115
+ Create an `opencode-commits.json` file in your project root to customize behavior:
116
+
117
+ ```json
118
+ {
119
+ "$schema": "https://raw.githubusercontent.com/whaaaley/opencode-commits/main/opencode-commits.schema.json",
120
+ "types": ["feat", "fix", "build", "chore", "ci", "docs", "style", "refactor", "perf", "test", "revert"],
121
+ "scopes": {
122
+ "app": ["portal", "dashboard", "settings"],
123
+ "layer": ["client", "server", "api", "database"],
124
+ "infra": ["ci", "build", "deploy", "docker"]
125
+ },
126
+ "maxLength": 72
127
+ }
128
+ ```
129
+
130
+ | Field | Type | Description |
131
+ | --------- | ----------------- | ------------------------------------------------ |
132
+ | types | string[] | Allowed commit types (overrides defaults) |
133
+ | scopes | Record<string, string[]> | Named groups of valid scopes |
134
+ | maxLength | number | Max commit message length (default: 72) |
135
+
136
+ When scopes are configured, only the listed scopes are accepted. When omitted, any well-formed scope is allowed.
137
+
138
+ ## License
139
+
140
+ MIT
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from '@opencode-ai/plugin';
2
+ declare const plugin: Plugin;
3
+ export default plugin;
4
+ //# sourceMappingURL=commits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commits.d.ts","sourceRoot":"","sources":["../commits.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAIjD,QAAA,MAAM,MAAM,EAAE,MAYb,CAAA;AAED,eAAe,MAAM,CAAA"}
@@ -0,0 +1,16 @@
1
+ import { loadConfig } from "./src/config.js";
2
+ import { createAmendTool, createCommitTool, createDiffTool, createLogTool, createStatusTool } from "./src/tools.js";
3
+ const plugin = async ({ directory, $ }) => {
4
+ const config = await loadConfig(directory);
5
+ return {
6
+ tool: {
7
+ 'git-commit': createCommitTool($, config),
8
+ 'git-amend': createAmendTool($, config),
9
+ 'git-diff': createDiffTool($),
10
+ 'git-log': createLogTool($),
11
+ 'git-status': createStatusTool($),
12
+ },
13
+ };
14
+ };
15
+ export default plugin;
16
+ //# sourceMappingURL=commits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commits.js","sourceRoot":"","sources":["../commits.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAEnH,MAAM,MAAM,GAAW,KAAK,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE;IAChD,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,SAAS,CAAC,CAAA;IAE1C,OAAO;QACL,IAAI,EAAE;YACJ,YAAY,EAAE,gBAAgB,CAAC,CAAC,EAAE,MAAM,CAAC;YACzC,WAAW,EAAE,eAAe,CAAC,CAAC,EAAE,MAAM,CAAC;YACvC,UAAU,EAAE,cAAc,CAAC,CAAC,CAAC;YAC7B,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC;YAC3B,YAAY,EAAE,gBAAgB,CAAC,CAAC,CAAC;SAClC;KACF,CAAA;AACH,CAAC,CAAA;AAED,eAAe,MAAM,CAAA"}
@@ -0,0 +1,10 @@
1
+ export declare const DEFAULT_TYPES: readonly ["feat", "fix", "build", "chore", "ci", "docs", "style", "refactor", "perf", "test", "revert"];
2
+ export declare const DEFAULT_MAX_LENGTH = 72;
3
+ export interface CommitsConfig {
4
+ types: string[];
5
+ scopes?: Record<string, string[]>;
6
+ maxLength: number;
7
+ }
8
+ export declare const loadConfig: (directory: string) => Promise<CommitsConfig>;
9
+ export declare const getAllScopes: (config: CommitsConfig) => string[] | undefined;
10
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,aAAa,yGAYhB,CAAA;AAEV,eAAO,MAAM,kBAAkB,KAAK,CAAA;AAQpC,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IACjC,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,eAAO,MAAM,UAAU,GAAU,WAAW,MAAM,KAAG,OAAO,CAAC,aAAa,CAoBzE,CAAA;AAED,eAAO,MAAM,YAAY,GAAI,QAAQ,aAAa,KAAG,MAAM,EAAE,GAAG,SAI/D,CAAA"}
@@ -0,0 +1,47 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { z } from 'zod/v4';
4
+ import { safeAsync } from "./utils/safe.js";
5
+ export const DEFAULT_TYPES = [
6
+ 'feat',
7
+ 'fix',
8
+ 'build',
9
+ 'chore',
10
+ 'ci',
11
+ 'docs',
12
+ 'style',
13
+ 'refactor',
14
+ 'perf',
15
+ 'test',
16
+ 'revert',
17
+ ];
18
+ export const DEFAULT_MAX_LENGTH = 72;
19
+ const rawConfigSchema = z.object({
20
+ types: z.array(z.string()).optional(),
21
+ scopes: z.record(z.string(), z.array(z.string())).optional(),
22
+ maxLength: z.number().optional(),
23
+ });
24
+ export const loadConfig = async (directory) => {
25
+ const configPath = join(directory, 'opencode-commits.json');
26
+ const result = await safeAsync(async () => {
27
+ const raw = await readFile(configPath, 'utf-8');
28
+ return rawConfigSchema.parse(JSON.parse(raw));
29
+ });
30
+ if (result.error) {
31
+ return {
32
+ types: [...DEFAULT_TYPES],
33
+ maxLength: DEFAULT_MAX_LENGTH,
34
+ };
35
+ }
36
+ return {
37
+ types: result.data.types ?? [...DEFAULT_TYPES],
38
+ scopes: result.data.scopes,
39
+ maxLength: result.data.maxLength ?? DEFAULT_MAX_LENGTH,
40
+ };
41
+ };
42
+ export const getAllScopes = (config) => {
43
+ if (!config.scopes)
44
+ return undefined;
45
+ return Object.values(config.scopes).flat();
46
+ };
47
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAA;AAC1B,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAE3C,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,MAAM;IACN,KAAK;IACL,OAAO;IACP,OAAO;IACP,IAAI;IACJ,MAAM;IACN,OAAO;IACP,UAAU;IACV,MAAM;IACN,MAAM;IACN,QAAQ;CACA,CAAA;AAEV,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAA;AAEpC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/B,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IACrC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE;IAC5D,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACjC,CAAC,CAAA;AAQF,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAAE,SAAiB,EAA0B,EAAE;IAC5E,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,uBAAuB,CAAC,CAAA;IAE3D,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,KAAK,IAAI,EAAE;QACxC,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,CAAA;QAC/C,OAAO,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAA;IAC/C,CAAC,CAAC,CAAA;IAEF,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,OAAO;YACL,KAAK,EAAE,CAAC,GAAG,aAAa,CAAC;YACzB,SAAS,EAAE,kBAAkB;SAC9B,CAAA;IACH,CAAC;IAED,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,GAAG,aAAa,CAAC;QAC9C,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM;QAC1B,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,IAAI,kBAAkB;KACvD,CAAA;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAqB,EAAwB,EAAE;IAC1E,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,OAAO,SAAS,CAAA;IAEpC,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAA;AAC5C,CAAC,CAAA"}
@@ -0,0 +1,9 @@
1
+ export type CommitErrorKind = 'CommitMessageParseError' | 'CommitValidationError';
2
+ export interface CommitError extends Error {
3
+ kind: CommitErrorKind;
4
+ suggestions: string[];
5
+ }
6
+ export declare const isCommitError: (error: unknown) => error is CommitError;
7
+ export declare const commitMessageParseError: (message: string, suggestions?: string[]) => CommitError;
8
+ export declare const commitValidationError: (message: string, suggestions?: string[]) => CommitError;
9
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG,yBAAyB,GAAG,uBAAuB,CAAA;AAEjF,MAAM,WAAW,WAAY,SAAQ,KAAK;IACxC,IAAI,EAAE,eAAe,CAAA;IACrB,WAAW,EAAE,MAAM,EAAE,CAAA;CACtB;AAWD,eAAO,MAAM,aAAa,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,WAIvD,CAAA;AAED,eAAO,MAAM,uBAAuB,GAAI,SAAS,MAAM,EAAE,cAAa,MAAM,EAAO,KAAG,WAClB,CAAA;AAEpE,eAAO,MAAM,qBAAqB,GAAI,SAAS,MAAM,EAAE,cAAa,MAAM,EAAO,KAAG,WAClB,CAAA"}
@@ -0,0 +1,15 @@
1
+ const createCommitError = (kind, message, suggestions = []) => {
2
+ const error = new Error(message);
3
+ error.name = kind;
4
+ error.kind = kind;
5
+ error.suggestions = suggestions;
6
+ return error;
7
+ };
8
+ export const isCommitError = (error) => {
9
+ if (!(error instanceof Error))
10
+ return false;
11
+ return 'kind' in error && 'suggestions' in error;
12
+ };
13
+ export const commitMessageParseError = (message, suggestions = []) => createCommitError('CommitMessageParseError', message, suggestions);
14
+ export const commitValidationError = (message, suggestions = []) => createCommitError('CommitValidationError', message, suggestions);
15
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAOA,MAAM,iBAAiB,GAAG,CAAC,IAAqB,EAAE,OAAe,EAAE,cAAwB,EAAE,EAAe,EAAE;IAC5G,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,OAAO,CAAgB,CAAA;IAC/C,KAAK,CAAC,IAAI,GAAG,IAAI,CAAA;IACjB,KAAK,CAAC,IAAI,GAAG,IAAI,CAAA;IACjB,KAAK,CAAC,WAAW,GAAG,WAAW,CAAA;IAE/B,OAAO,KAAK,CAAA;AACd,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAc,EAAwB,EAAE;IACpE,IAAI,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAE3C,OAAO,MAAM,IAAI,KAAK,IAAI,aAAa,IAAI,KAAK,CAAA;AAClD,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,OAAe,EAAE,cAAwB,EAAE,EAAe,EAAE,CAClG,iBAAiB,CAAC,yBAAyB,EAAE,OAAO,EAAE,WAAW,CAAC,CAAA;AAEpE,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,OAAe,EAAE,cAAwB,EAAE,EAAe,EAAE,CAChG,iBAAiB,CAAC,uBAAuB,EAAE,OAAO,EAAE,WAAW,CAAC,CAAA"}
@@ -0,0 +1,8 @@
1
+ export interface ParsedCommitMessage {
2
+ type: string;
3
+ scope?: string;
4
+ description: string;
5
+ raw: string;
6
+ }
7
+ export declare const parseCommitMessage: (message: string) => ParsedCommitMessage;
8
+ //# sourceMappingURL=parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/parser.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,MAAM,CAAA;CACZ;AA+DD,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG,mBA2CpD,CAAA"}
@@ -0,0 +1,84 @@
1
+ import { commitMessageParseError } from "./errors.js";
2
+ const parsePrefix = (prefix) => {
3
+ const parenOpen = prefix.indexOf('(');
4
+ const parenClose = prefix.indexOf(')');
5
+ if (parenOpen === -1 && parenClose === -1) {
6
+ return { type: prefix };
7
+ }
8
+ if (parenOpen === -1) {
9
+ throw commitMessageParseError('Found closing parenthesis without opening parenthesis', [
10
+ 'Use the format: <type>(<scope>): <description>',
11
+ ]);
12
+ }
13
+ if (parenClose === -1) {
14
+ throw commitMessageParseError('Found opening parenthesis without closing parenthesis', [
15
+ 'Use the format: <type>(<scope>): <description>',
16
+ ]);
17
+ }
18
+ if (parenClose < parenOpen) {
19
+ throw commitMessageParseError('Mismatched parentheses in commit message', [
20
+ 'Use the format: <type>(<scope>): <description>',
21
+ ]);
22
+ }
23
+ if (parenClose !== prefix.length - 1) {
24
+ throw commitMessageParseError('Unexpected characters after scope parentheses', [
25
+ 'Use the format: <type>(<scope>): <description>',
26
+ ]);
27
+ }
28
+ const type = prefix.slice(0, parenOpen);
29
+ const scope = prefix.slice(parenOpen + 1, parenClose);
30
+ if (!scope) {
31
+ throw commitMessageParseError('Scope must not be empty when parentheses are present', [
32
+ 'Either provide a scope or remove the parentheses',
33
+ ]);
34
+ }
35
+ if (!/^[a-z]/.test(scope)) {
36
+ throw commitMessageParseError('Scope must start with a lowercase letter', [
37
+ `Change "${scope}" to start with a lowercase letter`,
38
+ ]);
39
+ }
40
+ if (!/^[a-z][a-zA-Z0-9-]*$/.test(scope)) {
41
+ throw commitMessageParseError('Scope must only contain letters, numbers, and hyphens', [
42
+ `Change "${scope}" to use only letters, numbers, and hyphens`,
43
+ ]);
44
+ }
45
+ return { type, scope };
46
+ };
47
+ export const parseCommitMessage = (message) => {
48
+ const trimmed = message.trim();
49
+ if (!trimmed) {
50
+ throw commitMessageParseError('Commit message must not be empty', [
51
+ 'Provide a message in the format: <type>[(<scope>)]: <description>',
52
+ ]);
53
+ }
54
+ const colonIndex = trimmed.indexOf(':');
55
+ if (colonIndex === -1) {
56
+ throw commitMessageParseError('Commit message must contain a colon separator', [
57
+ 'Use the format: <type>[(<scope>)]: <description>',
58
+ `Example: feat: ${trimmed}`,
59
+ ]);
60
+ }
61
+ const prefix = trimmed.slice(0, colonIndex);
62
+ const description = trimmed.slice(colonIndex + 1).trim();
63
+ const { type, scope } = parsePrefix(prefix);
64
+ if (!type) {
65
+ throw commitMessageParseError('Commit type must not be empty', [
66
+ 'Provide a type before the colon',
67
+ 'Example: feat: add new feature',
68
+ ]);
69
+ }
70
+ if (!/^[a-z]+$/.test(type)) {
71
+ throw commitMessageParseError('Commit type must contain only lowercase letters', [
72
+ `Change "${type}" to use only lowercase letters`,
73
+ 'Valid types include: feat, fix, docs, style, refactor, test, chore',
74
+ ]);
75
+ }
76
+ if (!description) {
77
+ throw commitMessageParseError('Commit description must not be empty', [
78
+ 'Provide a description after the colon',
79
+ `Example: ${prefix}: add new feature`,
80
+ ]);
81
+ }
82
+ return { type, scope, description, raw: trimmed };
83
+ };
84
+ //# sourceMappingURL=parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.js","sourceRoot":"","sources":["../../src/parser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { PluginInput } from '@opencode-ai/plugin';
2
+ export type BunShell = PluginInput['$'];
3
+ //# sourceMappingURL=shell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../src/shell.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEtD,MAAM,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,CAAA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=shell.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell.js","sourceRoot":"","sources":["../../src/shell.ts"],"names":[],"mappings":""}
@@ -0,0 +1,41 @@
1
+ import type { CommitsConfig } from './config.ts';
2
+ import type { BunShell } from './shell.ts';
3
+ export declare const formatValidationError: (error: Error) => string;
4
+ export declare const createCommitTool: ($: BunShell, config: CommitsConfig) => {
5
+ description: string;
6
+ args: {
7
+ message: import("zod").ZodString;
8
+ };
9
+ execute(args: {
10
+ message: string;
11
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
12
+ };
13
+ export declare const createAmendTool: ($: BunShell, config: CommitsConfig) => {
14
+ description: string;
15
+ args: {
16
+ message: import("zod").ZodString;
17
+ };
18
+ execute(args: {
19
+ message: string;
20
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
21
+ };
22
+ export declare const createDiffTool: ($: BunShell) => {
23
+ description: string;
24
+ args: {};
25
+ execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
26
+ };
27
+ export declare const createLogTool: ($: BunShell) => {
28
+ description: string;
29
+ args: {
30
+ count: import("zod").ZodOptional<import("zod").ZodNumber>;
31
+ };
32
+ execute(args: {
33
+ count?: number | undefined;
34
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
35
+ };
36
+ export declare const createStatusTool: ($: BunShell) => {
37
+ description: string;
38
+ args: {};
39
+ execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
40
+ };
41
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/tools.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEhD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAI1C,eAAO,MAAM,qBAAqB,GAAI,OAAO,KAAK,KAAG,MAYpD,CAAA;AAED,eAAO,MAAM,gBAAgB,GAAI,GAAG,QAAQ,EAAE,QAAQ,aAAa;;;;;;;;CAoBlE,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,GAAG,QAAQ,EAAE,QAAQ,aAAa;;;;;;;;CAoBjE,CAAA;AAED,eAAO,MAAM,cAAc,GAAI,GAAG,QAAQ;;;;CAkBzC,CAAA;AAED,eAAO,MAAM,aAAa,GAAI,GAAG,QAAQ;;;;;;;;CAsBxC,CAAA;AAED,eAAO,MAAM,gBAAgB,GAAI,GAAG,QAAQ;;;;CAa3C,CAAA"}
@@ -0,0 +1,103 @@
1
+ import { tool } from '@opencode-ai/plugin';
2
+ import { isCommitError } from "./errors.js";
3
+ import { safe, safeAsync } from "./utils/safe.js";
4
+ import { validateCommitMessage } from "./validator.js";
5
+ export const formatValidationError = (error) => {
6
+ if (isCommitError(error)) {
7
+ let message = `**Error:** ${error.message}`;
8
+ if (error.suggestions.length > 0) {
9
+ message += '\n\n**Suggestions:**\n' + error.suggestions.map(s => `- ${s}`).join('\n');
10
+ }
11
+ return message;
12
+ }
13
+ return `**Error:** ${error.message}`;
14
+ };
15
+ export const createCommitTool = ($, config) => {
16
+ return tool({
17
+ description: 'Validate and commit staged changes with a conventional commit message',
18
+ args: {
19
+ message: tool.schema.string().describe('Conventional commit message (e.g. "feat(api): add endpoint")'),
20
+ },
21
+ async execute(args) {
22
+ const validation = safe(() => validateCommitMessage(args.message, config));
23
+ if (validation.error) {
24
+ return formatValidationError(validation.error);
25
+ }
26
+ const result = await safeAsync(() => $ `git commit -m ${args.message}`.text());
27
+ if (result.error) {
28
+ return `**Error:** Failed to commit staged changes: ${result.error.message}`;
29
+ }
30
+ return `**Committed successfully**\n\n\`\`\`\n${result.data.trim()}\n\`\`\``;
31
+ },
32
+ });
33
+ };
34
+ export const createAmendTool = ($, config) => {
35
+ return tool({
36
+ description: 'Amend the last commit with a new validated conventional commit message',
37
+ args: {
38
+ message: tool.schema.string().describe('New conventional commit message'),
39
+ },
40
+ async execute(args) {
41
+ const validation = safe(() => validateCommitMessage(args.message, config));
42
+ if (validation.error) {
43
+ return formatValidationError(validation.error);
44
+ }
45
+ const result = await safeAsync(() => $ `git commit --amend -m ${args.message}`.text());
46
+ if (result.error) {
47
+ return `**Error:** Failed to amend commit: ${result.error.message}`;
48
+ }
49
+ return `**Amended successfully**\n\n\`\`\`\n${result.data.trim()}\n\`\`\``;
50
+ },
51
+ });
52
+ };
53
+ export const createDiffTool = ($) => {
54
+ return tool({
55
+ description: 'Show the currently staged diff',
56
+ args: {},
57
+ async execute() {
58
+ const result = await safeAsync(() => $ `git diff --staged`.text());
59
+ if (result.error) {
60
+ return `**Error:** Failed to get staged diff: ${result.error.message}`;
61
+ }
62
+ const trimmed = result.data.trim();
63
+ if (!trimmed) {
64
+ return 'Nothing is currently staged.';
65
+ }
66
+ return `\`\`\`diff\n${trimmed}\n\`\`\``;
67
+ },
68
+ });
69
+ };
70
+ export const createLogTool = ($) => {
71
+ return tool({
72
+ description: 'List recent commits',
73
+ args: {
74
+ count: tool.schema.number().optional().describe('Number of commits to show (default: 10)'),
75
+ },
76
+ async execute(args) {
77
+ const count = args.count ?? 10;
78
+ const result = await safeAsync(() => $ `git log --oneline -n ${count}`.text());
79
+ if (result.error) {
80
+ return `**Error:** Failed to get git log: ${result.error.message}`;
81
+ }
82
+ const trimmed = result.data.trim();
83
+ if (!trimmed) {
84
+ return 'No commits found.';
85
+ }
86
+ return `\`\`\`\n${trimmed}\n\`\`\``;
87
+ },
88
+ });
89
+ };
90
+ export const createStatusTool = ($) => {
91
+ return tool({
92
+ description: 'Show the working tree status including staged, unstaged, and untracked files',
93
+ args: {},
94
+ async execute() {
95
+ const result = await safeAsync(() => $ `git status`.text());
96
+ if (result.error) {
97
+ return `**Error:** Failed to get git status: ${result.error.message}`;
98
+ }
99
+ return `\`\`\`\n${result.data.trim()}\n\`\`\``;
100
+ },
101
+ });
102
+ };
103
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../../src/tools.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ type ResultSuccess<T> = {
2
+ data: T;
3
+ error: null;
4
+ };
5
+ type ResultError<E> = {
6
+ data: null;
7
+ error: E;
8
+ };
9
+ export type Result<T, E> = ResultSuccess<T> | ResultError<E>;
10
+ type SafeResult<T> = Result<T, Error>;
11
+ export declare const safe: <T>(fn: () => T) => SafeResult<T>;
12
+ export declare const safeAsync: <T>(fn: () => Promise<T>) => Promise<SafeResult<T>>;
13
+ export {};
14
+ //# sourceMappingURL=safe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe.d.ts","sourceRoot":"","sources":["../../../src/utils/safe.ts"],"names":[],"mappings":"AAAA,KAAK,aAAa,CAAC,CAAC,IAAI;IACtB,IAAI,EAAE,CAAC,CAAA;IACP,KAAK,EAAE,IAAI,CAAA;CACZ,CAAA;AAED,KAAK,WAAW,CAAC,CAAC,IAAI;IACpB,IAAI,EAAE,IAAI,CAAA;IACV,KAAK,EAAE,CAAC,CAAA;CACT,CAAA;AAED,MAAM,MAAM,MAAM,CAAC,CAAC,EAAE,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAA;AAE5D,KAAK,UAAU,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;AAErC,eAAO,MAAM,IAAI,GAAI,CAAC,EAAE,IAAI,MAAM,CAAC,KAAG,UAAU,CAAC,CAAC,CAajD,CAAA;AAED,eAAO,MAAM,SAAS,GAAU,CAAC,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAa9E,CAAA"}
@@ -0,0 +1,31 @@
1
+ export const safe = (fn) => {
2
+ try {
3
+ const data = fn();
4
+ return {
5
+ data,
6
+ error: null,
7
+ };
8
+ }
9
+ catch (error) {
10
+ return {
11
+ data: null,
12
+ error: error instanceof Error ? error : new Error(String(error)),
13
+ };
14
+ }
15
+ };
16
+ export const safeAsync = async (fn) => {
17
+ try {
18
+ const data = await fn();
19
+ return {
20
+ data,
21
+ error: null,
22
+ };
23
+ }
24
+ catch (error) {
25
+ return {
26
+ data: null,
27
+ error: error instanceof Error ? error : new Error(String(error)),
28
+ };
29
+ }
30
+ };
31
+ //# sourceMappingURL=safe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe.js","sourceRoot":"","sources":["../../../src/utils/safe.ts"],"names":[],"mappings":"AAcA,MAAM,CAAC,MAAM,IAAI,GAAG,CAAI,EAAW,EAAiB,EAAE;IACpD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,EAAE,EAAE,CAAA;QACjB,OAAO;YACL,IAAI;YACJ,KAAK,EAAE,IAAI;SACZ,CAAA;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,IAAI,EAAE,IAAI;YACV,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;SACjE,CAAA;IACH,CAAC;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAAK,EAAoB,EAA0B,EAAE;IACjF,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,EAAE,EAAE,CAAA;QACvB,OAAO;YACL,IAAI;YACJ,KAAK,EAAE,IAAI;SACZ,CAAA;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,IAAI,EAAE,IAAI;YACV,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;SACjE,CAAA;IACH,CAAC;AACH,CAAC,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { CommitsConfig } from './config.ts';
2
+ export declare const validateCommitMessage: (message: string, config: CommitsConfig) => void;
3
+ //# sourceMappingURL=validator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAKhD,eAAO,MAAM,qBAAqB,GAAI,SAAS,MAAM,EAAE,QAAQ,aAAa,KAAG,IAyC9E,CAAA"}
@@ -0,0 +1,36 @@
1
+ import { getAllScopes } from "./config.js";
2
+ import { commitValidationError } from "./errors.js";
3
+ import { parseCommitMessage } from "./parser.js";
4
+ export const validateCommitMessage = (message, config) => {
5
+ const parsed = parseCommitMessage(message);
6
+ if (!config.types.includes(parsed.type)) {
7
+ const firstChar = parsed.type[0];
8
+ const close = firstChar ? config.types.filter(t => t.startsWith(firstChar)) : [];
9
+ const suggestions = close.length > 0
10
+ ? [`Did you mean: ${close.join(', ')}?`]
11
+ : [`Valid types are: ${config.types.join(', ')}`];
12
+ throw commitValidationError(`Invalid commit type: "${parsed.type}"`, suggestions);
13
+ }
14
+ const allowedScopes = getAllScopes(config);
15
+ if (parsed.scope && allowedScopes) {
16
+ if (!allowedScopes.includes(parsed.scope)) {
17
+ throw commitValidationError(`Invalid scope: "${parsed.scope}"`, [
18
+ `Allowed scopes are: ${allowedScopes.join(', ')}`,
19
+ ]);
20
+ }
21
+ }
22
+ if (/^[A-Z]/.test(parsed.description)) {
23
+ throw commitValidationError('Description must start with a lowercase letter', [
24
+ `Change "${parsed.description}" to start with a lowercase letter`,
25
+ ]);
26
+ }
27
+ if (/[.!,;:]$/.test(parsed.description)) {
28
+ throw commitValidationError('Description must not end with punctuation', [
29
+ `Remove the trailing "${parsed.description.slice(-1)}" from the description`,
30
+ ]);
31
+ }
32
+ if (parsed.raw.length > config.maxLength) {
33
+ throw commitValidationError(`Commit message exceeds ${config.maxLength} characters (${parsed.raw.length})`, ['Be more concise']);
34
+ }
35
+ };
36
+ //# sourceMappingURL=validator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/validator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAEhD,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,OAAe,EAAE,MAAqB,EAAQ,EAAE;IACpF,MAAM,MAAM,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAA;IAE1C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAChC,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QAEhF,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC;YAClC,CAAC,CAAC,CAAC,iBAAiB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;YACxC,CAAC,CAAC,CAAC,oBAAoB,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAEnD,MAAM,qBAAqB,CAAC,yBAAyB,MAAM,CAAC,IAAI,GAAG,EAAE,WAAW,CAAC,CAAA;IACnF,CAAC;IAED,MAAM,aAAa,GAAG,YAAY,CAAC,MAAM,CAAC,CAAA;IAC1C,IAAI,MAAM,CAAC,KAAK,IAAI,aAAa,EAAE,CAAC;QAClC,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1C,MAAM,qBAAqB,CAAC,mBAAmB,MAAM,CAAC,KAAK,GAAG,EAAE;gBAC9D,uBAAuB,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;aAClD,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC;QACtC,MAAM,qBAAqB,CAAC,gDAAgD,EAAE;YAC5E,WAAW,MAAM,CAAC,WAAW,oCAAoC;SAClE,CAAC,CAAA;IACJ,CAAC;IAED,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC;QACxC,MAAM,qBAAqB,CAAC,2CAA2C,EAAE;YACvE,wBAAwB,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,wBAAwB;SAC7E,CAAC,CAAA;IACJ,CAAC;IAED,IAAI,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;QACzC,MAAM,qBAAqB,CACzB,0BAA0B,MAAM,CAAC,SAAS,gBAAgB,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,EAC9E,CAAC,iBAAiB,CAAC,CACpB,CAAA;IACH,CAAC;AACH,CAAC,CAAA"}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "opencode-commits",
3
+ "version": "0.0.1-next.0",
4
+ "description": "An OpenCode plugin that enforces Conventional Commits by validating commit messages before they reach git",
5
+ "keywords": [
6
+ "opencode",
7
+ "opencode-plugin",
8
+ "plugin",
9
+ "git",
10
+ "commits",
11
+ "conventional-commits"
12
+ ],
13
+ "homepage": "https://github.com/whaaaley/opencode-commits#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/whaaaley/opencode-commits/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/whaaaley/opencode-commits.git"
20
+ },
21
+ "license": "MIT",
22
+ "author": "Dustin Dowell",
23
+ "type": "module",
24
+ "exports": {
25
+ ".": {
26
+ "import": "./dist/commits.js",
27
+ "types": "./dist/commits.d.ts"
28
+ }
29
+ },
30
+ "main": "./dist/commits.js",
31
+ "types": "./dist/commits.d.ts",
32
+ "files": [
33
+ "dist/",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "scripts": {
38
+ "build": "npm run clean && tsc",
39
+ "clean": "rm -rf dist",
40
+ "fmt": "bun run dprint fmt",
41
+ "prepublishOnly": "npm run build",
42
+ "test": "bun test"
43
+ },
44
+ "dependencies": {
45
+ "zod": "4.1.8"
46
+ },
47
+ "devDependencies": {
48
+ "@opencode-ai/plugin": "1.1.12",
49
+ "@types/bun": "latest",
50
+ "dprint": "^0.49.1",
51
+ "typescript": "5.9.3"
52
+ },
53
+ "peerDependencies": {
54
+ "@opencode-ai/plugin": ">=0.13.7"
55
+ }
56
+ }