opencode-commit-guard 1.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/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from "./src/plugin.js"
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "opencode-commit-guard",
3
+ "version": "1.0.0",
4
+ "description": "OpenCode V2 plugin enforcing git commit format rules",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "exports": {
8
+ ".": "./dist/index.js",
9
+ "./source": "./index.ts"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "index.ts",
14
+ "src"
15
+ ],
16
+ "scripts": {
17
+ "lint": "oxlint .",
18
+ "test": "bun test",
19
+ "typecheck": "tsc --noEmit",
20
+ "build": "bun build index.ts --outdir dist --target bun --format esm --external @opencode/plugin",
21
+ "prepack": "bun run build"
22
+ },
23
+ "dependencies": {
24
+ "@opencode/plugin": "2.0.2"
25
+ },
26
+ "devDependencies": {
27
+ "@oxlint/plugins": "1.80.0",
28
+ "@types/bun": "latest",
29
+ "oxlint": "1.80.0",
30
+ "typescript": "latest"
31
+ },
32
+ "engines": {
33
+ "bun": ">=1.2.0"
34
+ },
35
+ "license": "MIT",
36
+ "author": {
37
+ "name": "Omkar Chandorkar",
38
+ "email": "gotenksIN@aospa.co"
39
+ },
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/gotenksIN/opencode-commit-guard.git"
43
+ },
44
+ "homepage": "https://github.com/gotenksIN/opencode-commit-guard"
45
+ }
package/src/config.ts ADDED
@@ -0,0 +1,80 @@
1
+ import {
2
+ defaultConfig,
3
+ isJSONBoolean,
4
+ isJSONNumber,
5
+ isJSONString,
6
+ isRecord,
7
+ } from "./types.js"
8
+ import type { CommitGuardConfig, JsonValue } from "./types.js"
9
+
10
+ export function parseConfig(options: JsonValue | undefined): CommitGuardConfig {
11
+ if (options === undefined) {
12
+ return defaultConfig
13
+ }
14
+
15
+ if (!isRecord(options)) {
16
+ throw new Error("Invalid plugin options; expected an object.")
17
+ }
18
+
19
+ const rawRequireScope = options["requireScope"]
20
+ let requireScope = defaultConfig.requireScope
21
+
22
+ if (rawRequireScope !== undefined) {
23
+ if (!isJSONBoolean(rawRequireScope)) {
24
+ throw new Error("Invalid plugin option requireScope; expected a boolean.")
25
+ }
26
+
27
+ requireScope = rawRequireScope
28
+ }
29
+
30
+ const rawAllowedScopes = options["allowedScopes"]
31
+ let allowedScopes: readonly string[] | undefined = defaultConfig.allowedScopes
32
+
33
+ if (rawAllowedScopes !== undefined) {
34
+ if (!Array.isArray(rawAllowedScopes)) {
35
+ throw new Error("Invalid plugin option allowedScopes; expected an array of non-empty strings.")
36
+ }
37
+
38
+ const scopes: string[] = []
39
+
40
+ for (const item of rawAllowedScopes) {
41
+ if (!isJSONString(item) || item.trim().length === 0) {
42
+ throw new Error("Invalid plugin option allowedScopes; expected an array of non-empty strings.")
43
+ }
44
+
45
+ scopes.push(item.trim())
46
+ }
47
+
48
+ allowedScopes = scopes.length > 0 ? scopes : undefined
49
+ }
50
+
51
+ const rawMaxLineLength = options["maxLineLength"]
52
+ let maxLineLength = defaultConfig.maxLineLength
53
+
54
+ if (rawMaxLineLength !== undefined) {
55
+ if (!isJSONNumber(rawMaxLineLength) || !Number.isSafeInteger(rawMaxLineLength) || rawMaxLineLength < 0) {
56
+ throw new Error("Invalid plugin option maxLineLength; expected an integer greater than or equal to 0.")
57
+ }
58
+
59
+ // SAFETY: isJSONNumber and Number.isSafeInteger verify rawMaxLineLength is an integer number.
60
+ maxLineLength = rawMaxLineLength as number
61
+ }
62
+
63
+ const rawRequireSignoff = options["requireSignoff"]
64
+ let requireSignoff = defaultConfig.requireSignoff
65
+
66
+ if (rawRequireSignoff !== undefined) {
67
+ if (!isJSONBoolean(rawRequireSignoff)) {
68
+ throw new Error("Invalid plugin option requireSignoff; expected a boolean.")
69
+ }
70
+
71
+ requireSignoff = rawRequireSignoff
72
+ }
73
+
74
+ return {
75
+ requireScope,
76
+ allowedScopes,
77
+ maxLineLength,
78
+ requireSignoff,
79
+ }
80
+ }
package/src/plugin.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { Plugin } from "@opencode/plugin"
2
+ import { parseConfig } from "./config.js"
3
+ import { extractGitCommits } from "./shell.js"
4
+ import { isJSONString, isRecord } from "./types.js"
5
+ import type { JsonValue } from "./types.js"
6
+ import { validateGitCommits } from "./validator.js"
7
+
8
+ export const plugin = Plugin.define({
9
+ id: "opencode-commit-guard",
10
+ setup: async (ctx) => {
11
+ // SAFETY: OpenCode plugin options are passed through ctx.options as JsonValue or undefined.
12
+ const config = parseConfig(ctx.options as JsonValue | undefined)
13
+ const sessionDirectory = ctx.location.directory
14
+
15
+ await ctx.tool.hook("execute.before", async (event) => {
16
+ if (event.tool !== "shell" && event.tool !== "bash") {
17
+ return
18
+ }
19
+
20
+ // SAFETY: event.input is an unvalidated tool input payload from OpenCode runtime.
21
+ const rawInput = event.input as JsonValue | undefined
22
+
23
+ if (!isRecord(rawInput)) {
24
+ return
25
+ }
26
+
27
+ const command = rawInput["command"]
28
+
29
+ if (!isJSONString(command)) {
30
+ return
31
+ }
32
+
33
+ const invocations = extractGitCommits(command)
34
+
35
+ if (invocations.length === 0) {
36
+ return
37
+ }
38
+
39
+ const workdir = rawInput["workdir"]
40
+ validateGitCommits(
41
+ invocations,
42
+ config,
43
+ command,
44
+ isJSONString(workdir) ? workdir : sessionDirectory,
45
+ )
46
+ })
47
+ },
48
+ })
49
+
50
+ export default plugin