Browse Source

feat: publish JSON Schema for oh-my-opencode-slim.jsonc (#172)

* feat: publish JSON Schema for oh-my-opencode-slim.jsonc

- Add scripts/generate-schema.ts using Zod v4's built-in z.toJSONSchema()
- Generate oh-my-opencode-slim.schema.json from PluginConfigSchema
- Integrate schema generation into the build step
- Document $schema reference in README and installation docs
- Add schema file to npm package files

Fixes #160

* fix: use io: 'input' so defaulted fields are optional in schema

Addresses Greptile review: io: 'output' was marking all defaulted fields
(e.g., fallback.enabled, tmux.layout, background.maxConcurrentStarts) as
required, causing false validation errors in editors for valid configs
that omit those optional fields.

* Update scripts/generate-schema.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: alvinreal <alvinreal@users.noreply.github.com>
Co-authored-by: Alvin <alvin@cmngoal.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
alvinreal 1 month ago
parent
commit
8a3a769951
5 changed files with 503 additions and 1 deletions
  1. 13 0
      README.md
  2. 13 0
      docs/installation.md
  3. 440 0
      oh-my-opencode-slim.schema.json
  4. 3 1
      package.json
  5. 34 0
      scripts/generate-schema.ts

+ 13 - 0
README.md

@@ -27,6 +27,19 @@ bunx oh-my-opencode-slim@latest install --no-tui --tmux=no --skills=yes
 
 
 The default configuration uses OpenAI. To use Kimi, GitHub Copilot, or ZAI Coding Plan, see **[Provider Configurations](docs/provider-configurations.md)** for step-by-step instructions and config examples.
 The default configuration uses OpenAI. To use Kimi, GitHub Copilot, or ZAI Coding Plan, see **[Provider Configurations](docs/provider-configurations.md)** for step-by-step instructions and config examples.
 
 
+### JSON Schema
+
+An official JSON Schema is included in the package for editor validation and autocomplete. Add a `$schema` reference to your config file:
+
+```jsonc
+{
+  "$schema": "https://unpkg.com/oh-my-opencode-slim@latest/oh-my-opencode-slim.schema.json",
+  // your config...
+}
+```
+
+This enables autocomplete and inline validation in VS Code, Neovim, and other editors that support JSON Schema.
+
 ### For LLM Agents
 ### For LLM Agents
 
 
 Paste this into any coding agent:
 Paste this into any coding agent:

+ 13 - 0
docs/installation.md

@@ -216,6 +216,19 @@ If providers are not working:
    cat ~/.config/opencode/oh-my-opencode-slim.json
    cat ~/.config/opencode/oh-my-opencode-slim.json
    ```
    ```
 
 
+### Editor Validation
+
+Add a `$schema` reference to your config for autocomplete and inline validation:
+
+```jsonc
+{
+  "$schema": "https://unpkg.com/oh-my-opencode-slim@latest/oh-my-opencode-slim.schema.json",
+  // your config...
+}
+```
+
+Works in VS Code, Neovim (with `jsonls`), and any editor that supports JSON Schema. Catches typos and wrong nesting immediately (e.g., placing `chains` directly under `fallback` instead of `fallback.chains`).
+
 ### Tmux Integration Not Working
 ### Tmux Integration Not Working
 
 
 Make sure you're running OpenCode with the `--port` flag and the port matches your `OPENCODE_PORT` environment variable:
 Make sure you're running OpenCode with the `--port` flag and the port matches your `OPENCODE_PORT` environment variable:

+ 440 - 0
oh-my-opencode-slim.schema.json

@@ -0,0 +1,440 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "title": "oh-my-opencode-slim",
+  "description": "Configuration schema for oh-my-opencode-slim plugin for OpenCode",
+  "type": "object",
+  "properties": {
+    "preset": {
+      "type": "string"
+    },
+    "scoringEngineVersion": {
+      "type": "string",
+      "enum": [
+        "v1",
+        "v2-shadow",
+        "v2"
+      ]
+    },
+    "balanceProviderUsage": {
+      "type": "boolean"
+    },
+    "manualPlan": {
+      "type": "object",
+      "properties": {
+        "orchestrator": {
+          "type": "object",
+          "properties": {
+            "primary": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback1": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback2": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback3": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            }
+          },
+          "required": [
+            "primary",
+            "fallback1",
+            "fallback2",
+            "fallback3"
+          ]
+        },
+        "oracle": {
+          "type": "object",
+          "properties": {
+            "primary": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback1": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback2": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback3": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            }
+          },
+          "required": [
+            "primary",
+            "fallback1",
+            "fallback2",
+            "fallback3"
+          ]
+        },
+        "designer": {
+          "type": "object",
+          "properties": {
+            "primary": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback1": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback2": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback3": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            }
+          },
+          "required": [
+            "primary",
+            "fallback1",
+            "fallback2",
+            "fallback3"
+          ]
+        },
+        "explorer": {
+          "type": "object",
+          "properties": {
+            "primary": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback1": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback2": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback3": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            }
+          },
+          "required": [
+            "primary",
+            "fallback1",
+            "fallback2",
+            "fallback3"
+          ]
+        },
+        "librarian": {
+          "type": "object",
+          "properties": {
+            "primary": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback1": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback2": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback3": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            }
+          },
+          "required": [
+            "primary",
+            "fallback1",
+            "fallback2",
+            "fallback3"
+          ]
+        },
+        "fixer": {
+          "type": "object",
+          "properties": {
+            "primary": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback1": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback2": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback3": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            }
+          },
+          "required": [
+            "primary",
+            "fallback1",
+            "fallback2",
+            "fallback3"
+          ]
+        }
+      },
+      "required": [
+        "orchestrator",
+        "oracle",
+        "designer",
+        "explorer",
+        "librarian",
+        "fixer"
+      ],
+      "additionalProperties": false
+    },
+    "presets": {
+      "type": "object",
+      "propertyNames": {
+        "type": "string"
+      },
+      "additionalProperties": {
+        "type": "object",
+        "propertyNames": {
+          "type": "string"
+        },
+        "additionalProperties": {
+          "type": "object",
+          "properties": {
+            "model": {
+              "anyOf": [
+                {
+                  "type": "string"
+                },
+                {
+                  "type": "array",
+                  "items": {
+                    "anyOf": [
+                      {
+                        "type": "string"
+                      },
+                      {
+                        "type": "object",
+                        "properties": {
+                          "id": {
+                            "type": "string"
+                          },
+                          "variant": {
+                            "type": "string"
+                          }
+                        },
+                        "required": [
+                          "id"
+                        ]
+                      }
+                    ]
+                  }
+                }
+              ]
+            },
+            "temperature": {
+              "type": "number",
+              "minimum": 0,
+              "maximum": 2
+            },
+            "variant": {
+              "type": "string"
+            },
+            "skills": {
+              "type": "array",
+              "items": {
+                "type": "string"
+              }
+            },
+            "mcps": {
+              "type": "array",
+              "items": {
+                "type": "string"
+              }
+            }
+          }
+        }
+      }
+    },
+    "agents": {
+      "type": "object",
+      "propertyNames": {
+        "type": "string"
+      },
+      "additionalProperties": {
+        "type": "object",
+        "properties": {
+          "model": {
+            "anyOf": [
+              {
+                "type": "string"
+              },
+              {
+                "type": "array",
+                "items": {
+                  "anyOf": [
+                    {
+                      "type": "string"
+                    },
+                    {
+                      "type": "object",
+                      "properties": {
+                        "id": {
+                          "type": "string"
+                        },
+                        "variant": {
+                          "type": "string"
+                        }
+                      },
+                      "required": [
+                        "id"
+                      ]
+                    }
+                  ]
+                }
+              }
+            ]
+          },
+          "temperature": {
+            "type": "number",
+            "minimum": 0,
+            "maximum": 2
+          },
+          "variant": {
+            "type": "string"
+          },
+          "skills": {
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          },
+          "mcps": {
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          }
+        }
+      }
+    },
+    "disabled_mcps": {
+      "type": "array",
+      "items": {
+        "type": "string"
+      }
+    },
+    "tmux": {
+      "type": "object",
+      "properties": {
+        "enabled": {
+          "default": false,
+          "type": "boolean"
+        },
+        "layout": {
+          "default": "main-vertical",
+          "type": "string",
+          "enum": [
+            "main-horizontal",
+            "main-vertical",
+            "tiled",
+            "even-horizontal",
+            "even-vertical"
+          ]
+        },
+        "main_pane_size": {
+          "default": 60,
+          "type": "number",
+          "minimum": 20,
+          "maximum": 80
+        }
+      }
+    },
+    "background": {
+      "type": "object",
+      "properties": {
+        "maxConcurrentStarts": {
+          "default": 10,
+          "type": "number",
+          "minimum": 1,
+          "maximum": 50
+        }
+      }
+    },
+    "fallback": {
+      "type": "object",
+      "properties": {
+        "enabled": {
+          "default": true,
+          "type": "boolean"
+        },
+        "timeoutMs": {
+          "default": 15000,
+          "type": "number",
+          "minimum": 0
+        },
+        "chains": {
+          "default": {},
+          "type": "object",
+          "properties": {
+            "orchestrator": {
+              "minItems": 1,
+              "type": "array",
+              "items": {
+                "type": "string"
+              }
+            },
+            "oracle": {
+              "minItems": 1,
+              "type": "array",
+              "items": {
+                "type": "string"
+              }
+            },
+            "designer": {
+              "minItems": 1,
+              "type": "array",
+              "items": {
+                "type": "string"
+              }
+            },
+            "explorer": {
+              "minItems": 1,
+              "type": "array",
+              "items": {
+                "type": "string"
+              }
+            },
+            "librarian": {
+              "minItems": 1,
+              "type": "array",
+              "items": {
+                "type": "string"
+              }
+            },
+            "fixer": {
+              "minItems": 1,
+              "type": "array",
+              "items": {
+                "type": "string"
+              }
+            }
+          },
+          "additionalProperties": {
+            "minItems": 1,
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          }
+        }
+      }
+    }
+  }
+}

+ 3 - 1
package.json

@@ -31,11 +31,13 @@
   "files": [
   "files": [
     "dist",
     "dist",
     "src/skills",
     "src/skills",
+    "oh-my-opencode-slim.schema.json",
     "README.md",
     "README.md",
     "LICENSE"
     "LICENSE"
   ],
   ],
   "scripts": {
   "scripts": {
-    "build": "bun build src/index.ts --outdir dist --target bun --format esm && bun build src/cli/index.ts --outdir dist/cli --target bun --format esm && tsc --emitDeclarationOnly",
+    "build": "bun build src/index.ts --outdir dist --target bun --format esm && bun build src/cli/index.ts --outdir dist/cli --target bun --format esm && tsc --emitDeclarationOnly && bun run generate-schema",
+    "generate-schema": "bun run scripts/generate-schema.ts",
     "typecheck": "tsc --noEmit",
     "typecheck": "tsc --noEmit",
     "test": "bun test",
     "test": "bun test",
     "lint": "biome lint .",
     "lint": "biome lint .",

+ 34 - 0
scripts/generate-schema.ts

@@ -0,0 +1,34 @@
+#!/usr/bin/env bun
+/**
+ * Generates a JSON Schema from the Zod PluginConfigSchema.
+ * Run as part of the build step so the schema stays in sync with the source.
+ */
+
+import { z } from 'zod';
+import { PluginConfigSchema } from '../src/config/schema';
+import { writeFileSync } from 'node:fs';
+import { join, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const rootDir = join(__dirname, '..');
+const outputPath = join(rootDir, 'oh-my-opencode-slim.schema.json');
+
+const schema = z.toJSONSchema(PluginConfigSchema, {
+  // Use 'input' so defaulted fields are optional in the schema,
+  // matching how users actually write their config files
+  io: 'input',
+});
+
+const jsonSchema = {
+  ...schema,
+  $schema: 'https://json-schema.org/draft/2020-12/schema',
+  title: 'oh-my-opencode-slim',
+  description:
+    'Configuration schema for oh-my-opencode-slim plugin for OpenCode',
+};
+
+const json = JSON.stringify(jsonSchema, null, 2);
+writeFileSync(outputPath, json + '\n');
+
+console.log(`✅ Schema written to ${outputPath}`);