opencode-codeops 1.4.0 → 1.6.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/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to CodeOps are recorded here.
4
4
 
5
+ ## 1.6.0 — 2026-09-19
6
+
7
+ ### Features
8
+
9
+ - installer: register the plugin automatically on install/update
10
+ - installer: unify install/update into one command
11
+ - installer: add npm-first install, agents installer, and release tooling
12
+
13
+ ## 1.5.0 — 2026-09-19
14
+
15
+ ### Features
16
+
17
+ - installer: unify install/update into one command
18
+ - installer: add npm-first install, agents installer, and release tooling
19
+
5
20
  ## 1.4.0 — 2026-09-19
6
21
 
7
22
  ### Features
package/README.md CHANGED
@@ -27,108 +27,97 @@ Turn an idea or existing system into ambiguity-free requirements, grounded speci
27
27
 
28
28
  ## Installation
29
29
 
30
- CodeOps has two parts that OpenCode loads differently: the **plugin** (standards and hooks) is
31
- installed by OpenCode from npm, and the **skills** and **subagents** must be copied onto the
32
- filesystem because OpenCode only discovers those from disk.
33
-
34
- ### 1. Add the plugin
35
-
36
- Add to your `opencode.json`:
37
-
38
- ```json
39
- {
40
- "$schema": "https://opencode.ai/config.json",
41
- "plugin": ["opencode-codeops"]
42
- }
43
- ```
44
-
45
- OpenCode installs the plugin automatically via Bun on next startup. A global config
46
- (`~/.config/opencode/opencode.json`) is recommended so CodeOps is active in every project.
47
-
48
- ### 2. Install the skills
49
-
50
- OpenCode discovers skills only from the filesystem (`.opencode/skills/` or
51
- `~/.config/opencode/skills/`); it never reads them from a plugin package. The installer is a thin
52
- `npx` wrapper around this package, so the skills always match the published version:
30
+ One command installs everything CodeOps owns: the skills, the subagents, and the OpenCode plugin
31
+ entry in your config.
53
32
 
54
33
  ```bash
55
34
  # Global (recommended) — available in every OpenCode project
56
35
  curl -fsSL https://cdn.jsdelivr.net/npm/opencode-codeops@latest/install.sh | bash
57
36
 
58
- # Project-only — skills live in ./.opencode/skills and are committed with the repo
37
+ # Project-only — files live in ./.opencode and are committed with the repo
59
38
  curl -fsSL https://cdn.jsdelivr.net/npm/opencode-codeops@latest/install.sh | bash -s -- --project
60
39
  ```
61
40
 
62
41
  The same installer runs directly through npm:
63
42
 
64
43
  ```bash
65
- npx -y opencode-codeops@latest install-skills
44
+ npx -y opencode-codeops@latest install
45
+ npx -y opencode-codeops@latest update # alias of install
66
46
  ```
67
47
 
68
- Pin a version with `CODEOPS_VERSION` (an npm dist-tag or exact version; defaults to `latest`):
48
+ `install`/`update` writes the skills and subagents onto the filesystem (OpenCode discovers those
49
+ only from disk), then registers the plugin in the OpenCode config by calling OpenCode's own
50
+ `opencode plugin` command, so standards injection and `CODEOPS_PLUGIN_ROOT` are enabled. Restart
51
+ OpenCode after installing for the plugin to load. Pass `--no-plugin` to manage the config yourself.
69
52
 
70
- ```bash
71
- CODEOPS_VERSION=1.4.0 curl -fsSL https://cdn.jsdelivr.net/npm/opencode-codeops@latest/install.sh | bash
72
- ```
53
+ The scope is auto-detected: inside a CodeOps project (a git repo with `.opencode/` or
54
+ `codeops/.codeops.yml`) it installs into `./.opencode`, and registers the plugin in the project
55
+ config; anywhere else it installs globally into `~/.config/opencode` and the global config. Pass
56
+ `--project` or `--global` to force one.
73
57
 
74
- Pin the plugin to the same version so the two cannot drift apart:
58
+ If you prefer to register the plugin manually instead, add it to your `opencode.json`:
75
59
 
76
60
  ```json
77
- { "plugin": ["opencode-codeops@1.4.0"] }
61
+ {
62
+ "$schema": "https://opencode.ai/config.json",
63
+ "plugin": ["opencode-codeops"]
64
+ }
78
65
  ```
79
66
 
80
- Re-running the installer upgrades an existing install in place. It replaces only the files this
81
- package owns, recorded in `<skills-dir>/.opencode-codeops.json`. Skills you author yourself, or
82
- install with another tool, are left untouched.
83
-
84
- Check or remove an install:
67
+ Pin a version with `CODEOPS_VERSION` (an npm dist-tag or exact version; defaults to `latest`):
85
68
 
86
69
  ```bash
87
- npx -y opencode-codeops@latest status
88
- npx -y opencode-codeops@latest uninstall
70
+ CODEOPS_VERSION=1.6.0 curl -fsSL https://cdn.jsdelivr.net/npm/opencode-codeops@latest/install.sh | bash
89
71
  ```
90
72
 
91
- `status` reports the installed version next to the current package version, so a plugin/skills
92
- mismatch is visible. Use `--dry-run` to preview an install; a same-named directory the package
93
- does not own is skipped with a warning, and `--force` replaces it.
73
+ `install`/`update` pins the plugin in the OpenCode config to the installer's own version, so the
74
+ plugin and the installed files cannot drift apart; run `update` to move both to a new version.
94
75
 
95
- ### 3. Install the subagents
76
+ Re-running the installer upgrades an existing install in place. It replaces only the files this
77
+ package owns, recorded in `.opencode-codeops.json`. Files you author yourself, or install with
78
+ another tool, are left untouched.
96
79
 
97
- The 12 CodeOps subagents install into `.opencode/agents/` (project) or `~/.config/opencode/agents/`
98
- (global). The `/setup-codeops` skill does this for a project; do it manually with:
80
+ Check or remove an install:
99
81
 
100
82
  ```bash
101
- npx -y opencode-codeops@latest install-agents --project
83
+ npx -y opencode-codeops@latest status
84
+ npx -y opencode-codeops@latest uninstall
102
85
  ```
103
86
 
87
+ `status` reports the installed skills and agents versions and the configured plugin entry, so a
88
+ mismatch is visible. Use `--dry-run` to preview an install; a same-named file the package does not
89
+ own is skipped with a warning, and `--force` replaces it. `uninstall` removes the skills and
90
+ subagents but leaves the plugin entry in your config; remove `opencode-codeops` from the `plugin`
91
+ array by hand to fully disable it.
92
+
104
93
  ### Local development
105
94
 
106
- Symlink the plugin into your OpenCode plugin directory and link the skills to a checkout, so edits
107
- are picked up without reinstalling:
95
+ Symlink the plugin into your OpenCode plugin directory and link the installed files to a checkout,
96
+ so edits are picked up without reinstalling. Pass `--no-plugin` so the checkout is not overwritten
97
+ by a registered npm plugin:
108
98
 
109
99
  ```bash
110
100
  # Plugin (project or global plugin directory)
111
101
  ln -s /path/to/opencode-codeops/plugin/index.ts ~/.config/opencode/plugins/codeops.ts
112
102
 
113
103
  # Skills and agents — link instead of copy
114
- node /path/to/opencode-codeops/bin/index.mjs install-skills --link --global
115
- node /path/to/opencode-codeops/bin/index.mjs install-agents --link --global
104
+ node /path/to/opencode-codeops/bin/index.mjs install --link --global --no-plugin
116
105
  ```
117
106
 
118
107
  ## Setup
119
108
 
120
- After installing the plugin and the skills, initialize CodeOps in your project:
109
+ After installing the plugin and the files, initialize CodeOps in your project:
121
110
 
122
111
  ```
123
112
  /setup-codeops
124
113
  ```
125
114
 
126
- This creates the `codeops/` layout, scaffolds `codeops/codeops.json` and `codeops/.codeops.yml`, installs the 12 CodeOps subagent files into `.opencode/agents/`, and adds a managed section to `AGENTS.md`.
115
+ This creates the `codeops/` layout, scaffolds `codeops/codeops.json` and `codeops/.codeops.yml`, installs the skills and the 12 CodeOps subagent files into `.opencode/`, and adds a managed section to `AGENTS.md`.
127
116
 
128
117
  Commit the result:
129
118
 
130
119
  ```bash
131
- git add codeops/ .opencode/agents/ AGENTS.md
120
+ git add codeops/ .opencode/ AGENTS.md
132
121
  git commit -m "chore: initialize CodeOps"
133
122
  ```
134
123
 
@@ -140,7 +129,7 @@ On every OpenCode session start and after every compaction, the plugin injects:
140
129
 
141
130
  These standards are active without any user action. They do not need to be copied into `AGENTS.md`.
142
131
 
143
- The plugin also warns (non-blocking) if any tool attempts to edit `codeops/.codeops.yml` directly — that file is managed exclusively by the `setup-codeops` skill.
132
+ The plugin also warns (non-blocking) if any tool attempts to edit `codeops/.codeops.yml` directly — that file is managed exclusively by the `setup-codeops` skill — and if the installed skills version differs from the plugin version, so a stale install is visible.
144
133
 
145
134
  ## Agent model configuration
146
135
 
package/bin/index.mjs CHANGED
@@ -2,92 +2,211 @@
2
2
  /**
3
3
  * The `opencode-codeops` command-line entry point.
4
4
  *
5
- * Routes each command to the skills installer (`install-skills.mjs`) or the
6
- * agent installer (`install-agents.mjs`), so one package binary exposes both.
7
- * `status` reports on both installs because the skills and the plugin can drift
8
- * in version and that is the check a user needs most.
5
+ * There is exactly one installation path: a command installs or updates the
6
+ * skills and the subagents together. The two filesystems installers
7
+ * (`install-skills.mjs` and `install-agents.mjs`) are implementation details
8
+ * this module orchestrates; they are never exposed as separate user commands.
9
+ *
10
+ * The scope is auto-detected. Inside a CodeOps project the files go to
11
+ * `.opencode/skills` and `.opencode/agents`; anywhere else they go to the
12
+ * global `~/.config/opencode/` directories. `--project` and `--global` override
13
+ * the detection.
9
14
  *
10
15
  * Usage:
11
- * opencode-codeops install-skills [options] Install or upgrade skills
12
- * opencode-codeops install-agents [options] Install or upgrade subagents
13
- * opencode-codeops status [options] Show installed skills and agents
14
- * opencode-codeops uninstall [options] Remove the managed skills
15
- * opencode-codeops help Show this help
16
+ * opencode-codeops install [options] Install or upgrade skills and agents
17
+ * opencode-codeops update [options] Alias of install
18
+ * opencode-codeops status [options] Show installed versions
19
+ * opencode-codeops uninstall [options] Remove managed files
20
+ * opencode-codeops help Show this help
16
21
  *
17
22
  * @module index
18
23
  */
19
24
 
20
- import { realpathSync } from "node:fs"
25
+ import { execFileSync } from "node:child_process"
26
+ import { existsSync, realpathSync } from "node:fs"
27
+ import { join } from "node:path"
21
28
  import { fileURLToPath } from "node:url"
22
29
 
30
+ import { PLUGIN_NAME, readConfiguredPlugin, registerPlugin } from "./lib/opencode-plugin.mjs"
31
+
32
+ /** Commands this CLI understands. */
33
+ const COMMANDS = new Set(["install", "update", "status", "uninstall", "help"])
34
+
23
35
  /** Prints command usage. */
24
36
  function printUsage() {
25
37
  console.log(`CodeOps installer for OpenCode.
26
38
 
27
39
  Usage:
28
- opencode-codeops install-skills [options] Install or upgrade skills (default)
29
- opencode-codeops install-agents [options] Install or upgrade subagents
30
- opencode-codeops status [options] Show installed skills and agents
31
- opencode-codeops uninstall [options] Remove the managed skills
32
- opencode-codeops help Show this help
33
-
34
- Run \`opencode-codeops install-skills --help\` or
35
- \`opencode-codeops install-agents --help\` for the option list.`)
40
+ opencode-codeops install [options] Install or upgrade skills and agents (default)
41
+ opencode-codeops update [options] Alias of install
42
+ opencode-codeops status [options] Show installed skills and agents
43
+ opencode-codeops uninstall [options] Remove managed skills and agents
44
+ opencode-codeops help Show this help
45
+
46
+ Options:
47
+ --project Force the project scope (./.opencode)
48
+ --global Force the global scope (~/.config/opencode)
49
+ --dry-run Show what would happen without writing files
50
+ --force Replace same-named files this package does not own
51
+ --link Symlink to the source instead of copying (development)
52
+ --no-plugin Do not register the plugin in the OpenCode config
53
+ -h, --help Show this help
54
+
55
+ Scope is auto-detected: inside a CodeOps project (a git repo with .opencode/ or
56
+ codeops/.codeops.yml) the files go to .opencode/; otherwise to ~/.config/opencode/.
57
+
58
+ install/update also register the CodeOps plugin in the OpenCode config (unless
59
+ --no-plugin), so standards injection and CODEOPS_PLUGIN_ROOT are enabled. Restart
60
+ OpenCode after installing for the plugin to load.`)
36
61
  }
37
62
 
38
63
  /**
39
- * Chooses the installer module for a command line.
64
+ * Finds the git top-level directory for a working directory.
40
65
  *
41
- * @param argv - Arguments after the executable
42
- * @returns `"agents"` for agent commands, otherwise `"skills"`
66
+ * @param cwd - Directory to start from
67
+ * @returns The repository root, or `cwd` when it is not a git checkout
43
68
  */
44
- export function route(argv) {
45
- const first = argv[0]
46
- if (first === "install-agents" || first === "agents-status" || first === "agents-uninstall") {
47
- return "agents"
69
+ function findProjectRoot(cwd) {
70
+ try {
71
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
72
+ cwd,
73
+ encoding: "utf-8",
74
+ stdio: ["ignore", "pipe", "ignore"],
75
+ }).trim()
76
+ } catch {
77
+ return cwd
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Resolves whether files install into the project or globally.
83
+ *
84
+ * Explicit flags win. Otherwise a git repository is a CodeOps project when it
85
+ * already contains `.opencode/` or `codeops/.codeops.yml`; anything else uses
86
+ * the global scope.
87
+ *
88
+ * @param options - Scope flags from the command line
89
+ * @param options.project - Force the project scope
90
+ * @param options.global - Force the global scope
91
+ * @param cwd - Directory the command ran from
92
+ * @returns `"project"` or `"global"`
93
+ */
94
+ export function resolveScope(options, cwd) {
95
+ if (options.project) return "project"
96
+ if (options.global) return "global"
97
+
98
+ const root = findProjectRoot(cwd)
99
+ if (existsSync(join(root, ".opencode")) || existsSync(join(root, "codeops", ".codeops.yml"))) {
100
+ return "project"
48
101
  }
49
- return "skills"
102
+ return "global"
50
103
  }
51
104
 
52
105
  /**
53
- * Runs the requested command.
106
+ * Runs one command against both installers.
54
107
  *
55
- * The modules are imported lazily so a skills-only invocation never loads the
56
- * agent installer, and vice versa.
108
+ * @param command - `install`, `status`, or `uninstall`
109
+ * @param rest - Options to forward, without the scope flags (re-added below)
110
+ * @param io - Injectable environment (`cwd`, `home`) for tests
111
+ * @returns The combined process exit code
57
112
  */
58
- async function run() {
59
- const args = process.argv.slice(2)
60
- const first = args[0]
113
+ async function runCombined(command, rest, io) {
114
+ const skills = await import("./install-skills.mjs")
115
+ const agents = await import("./install-agents.mjs")
116
+
117
+ const cwd = io.cwd ?? process.cwd()
118
+ const noPlugin = rest.includes("--no-plugin")
119
+ const dryRun = rest.includes("--dry-run")
120
+ const scope = resolveScope(
121
+ { project: rest.includes("--project"), global: rest.includes("--global") },
122
+ cwd
123
+ )
124
+ const passed = rest.filter(
125
+ (arg) => arg !== "--project" && arg !== "--global" && arg !== "--no-plugin"
126
+ )
127
+ const scoped = [scope === "project" ? "--project" : "--global", ...passed]
128
+
129
+ const skillsCode = skills.main([command, ...scoped], io)
130
+ const agentsCode = agents.main([command, ...scoped], io)
131
+ const code = skillsCode || agentsCode
132
+
133
+ if (command === "install" && code === 0 && !noPlugin && !dryRun) {
134
+ const version = io.version ?? skills.readPackageVersion()
135
+ const result = registerPlugin({ scope, version, cwd, run: io.run })
136
+ if (result.ok) {
137
+ console.log(`Plugin: registered ${result.spec} in the ${scope} OpenCode config.`)
138
+ console.log("Restart OpenCode to load the plugin.")
139
+ } else {
140
+ console.log(
141
+ `Plugin: not registered (${result.reason}). ` +
142
+ `Add "${PLUGIN_NAME}" to the "plugin" array in your opencode.json.`
143
+ )
144
+ }
145
+ }
146
+
147
+ if (command === "status") {
148
+ const plugins = readConfiguredPlugin({ cwd, run: io.run })
149
+ if (plugins === undefined) {
150
+ console.log("plugin: opencode CLI unavailable; cannot read the config")
151
+ } else {
152
+ const entry = plugins.find((item) => String(item).startsWith(PLUGIN_NAME))
153
+ console.log(entry ? `plugin: configured (${entry})` : "plugin: not configured")
154
+ }
155
+ }
156
+
157
+ return code
158
+ }
159
+
160
+ /**
161
+ * Dispatches a command line.
162
+ *
163
+ * Bare options (for example `opencode-codeops --project`) mean `install`, so the
164
+ * common case needs no subcommand.
165
+ *
166
+ * @param argv - Arguments after the executable
167
+ * @param io - Injectable environment (`cwd`, `home`) for tests
168
+ * @returns The process exit code
169
+ */
170
+ export async function dispatch(argv, io = {}) {
171
+ const first = argv[0]
172
+ const rest = argv.slice(1)
61
173
 
62
174
  if (!first || first === "help" || first === "-h" || first === "--help") {
63
175
  printUsage()
64
- return
176
+ return 0
65
177
  }
66
178
 
67
- if (first === "status") {
68
- const skills = await import("./install-skills.mjs")
69
- const agents = await import("./install-agents.mjs")
70
- const skillsCode = skills.main(["status", ...args.slice(1)])
71
- const agentsCode = agents.main(["agents-status", ...args.slice(1)])
72
- process.exitCode = skillsCode || agentsCode
73
- return
179
+ // Keep the unified help; the internal installers still document the old
180
+ // component commands in their own usage text.
181
+ if (rest.includes("--help") || rest.includes("-h")) {
182
+ printUsage()
183
+ return 0
74
184
  }
75
185
 
76
- if (route(args) === "agents") {
77
- const { main } = await import("./install-agents.mjs")
78
- process.exitCode = main(args)
79
- return
186
+ if (first.startsWith("-")) return runCombined("install", argv, io)
187
+
188
+ if (!COMMANDS.has(first)) {
189
+ console.error(`error: unknown command '${first}'`)
190
+ printUsage()
191
+ return 2
80
192
  }
81
193
 
82
- const { main } = await import("./install-skills.mjs")
83
- process.exitCode = main(args)
194
+ const command = first === "update" ? "install" : first
195
+ return runCombined(command, rest, io)
84
196
  }
85
197
 
86
198
  /**
87
- * True when this module is the process entry point.
199
+ * Runs the CLI when this module is the process entry point.
88
200
  *
89
201
  * The comparison resolves symlinks because npm installs the bin as a symlink in
90
202
  * `node_modules/.bin`, so `process.argv[1]` is the link path, not the real path.
203
+ */
204
+ async function run() {
205
+ process.exitCode = await dispatch(process.argv.slice(2))
206
+ }
207
+
208
+ /**
209
+ * True when this module is the process entry point.
91
210
  *
92
211
  * @returns True when this file is the entry point
93
212
  */
@@ -81,20 +81,17 @@ export function readPackageVersion() {
81
81
  /**
82
82
  * Resolves the directory that holds the packaged agent definitions.
83
83
  *
84
- * The `CODEOPS_PLUGIN_ROOT` environment variable overrides the default, which
85
- * lets a checkout run the installer before the package is published.
84
+ * The package's own `agents/` directory is always the source, so the installed
85
+ * agents match the package that was invoked. The `CODEOPS_PLUGIN_ROOT`
86
+ * environment variable is deliberately ignored: the plugin exports it into
87
+ * every shell, and honouring it would install a different checkout's agents.
88
+ * Pass `--source` to override for development.
86
89
  *
87
90
  * @param override - Optional explicit agents directory
88
91
  * @returns Absolute path to the agents directory
89
92
  */
90
93
  export function resolveSourceDir(override) {
91
94
  if (override) return resolve(override)
92
-
93
- const envRoot = process.env.CODEOPS_PLUGIN_ROOT
94
- if (envRoot && existsSync(join(envRoot, "agents"))) {
95
- return join(envRoot, "agents")
96
- }
97
-
98
95
  return join(PACKAGE_ROOT, "agents")
99
96
  }
100
97
 
@@ -375,7 +372,7 @@ export function main(argv, io = {}) {
375
372
  const cwd = io.cwd ?? process.cwd()
376
373
  const version = io.version ?? readPackageVersion()
377
374
  const sourceDir = io.source ?? resolveSourceDir(options.source)
378
- const targetDir = resolveTarget(options, cwd, "agents")
375
+ const targetDir = resolveTarget(options, cwd, "agents", io.home)
379
376
 
380
377
  if (!existsSync(sourceDir)) {
381
378
  console.error(`error: agents directory not found: ${sourceDir}`)
@@ -404,8 +401,8 @@ export function main(argv, io = {}) {
404
401
  if (command === "uninstall") {
405
402
  const result = uninstallAgents({ targetDir, dryRun: options.dryRun })
406
403
  if (result.error) {
407
- console.error(`error: ${result.error} at ${targetDir}`)
408
- return 1
404
+ console.log(`nothing to remove at ${targetDir} (${result.error})`)
405
+ return 0
409
406
  }
410
407
  console.log(
411
408
  `${options.dryRun ? "would remove" : "removed"} ${result.removed.length} agent(s); ` +
@@ -81,20 +81,17 @@ export function readPackageVersion() {
81
81
  /**
82
82
  * Resolves the directory that holds the shipped skills.
83
83
  *
84
- * The `CODEOPS_PLUGIN_ROOT` environment variable overrides the default. That
85
- * lets a checkout run the installer before the package is published.
84
+ * The package's own `skills/` directory is always the source, so the installed
85
+ * skills match the package that was invoked. The `CODEOPS_PLUGIN_ROOT`
86
+ * environment variable is deliberately ignored: the plugin exports it into
87
+ * every shell, and honouring it would install a different checkout's skills.
88
+ * Pass `--source` to override for development.
86
89
  *
87
90
  * @param override - Optional explicit skills directory
88
91
  * @returns Absolute path to the skills directory
89
92
  */
90
93
  export function resolveSourceDir(override) {
91
94
  if (override) return resolve(override)
92
-
93
- const envRoot = process.env.CODEOPS_PLUGIN_ROOT
94
- if (envRoot && existsSync(join(envRoot, "skills"))) {
95
- return join(envRoot, "skills")
96
- }
97
-
98
95
  return join(PACKAGE_ROOT, "skills")
99
96
  }
100
97
 
@@ -385,7 +382,7 @@ export function main(argv, io = {}) {
385
382
  const cwd = io.cwd ?? process.cwd()
386
383
  const version = io.version ?? readPackageVersion()
387
384
  const sourceDir = io.source ?? resolveSourceDir(options.source)
388
- const targetDir = resolveTarget(options, cwd, "skills")
385
+ const targetDir = resolveTarget(options, cwd, "skills", io.home)
389
386
 
390
387
  if (!existsSync(sourceDir)) {
391
388
  console.error(`error: skills directory not found: ${sourceDir}`)
@@ -414,8 +411,8 @@ export function main(argv, io = {}) {
414
411
  if (command === "uninstall") {
415
412
  const result = uninstallSkills({ targetDir, dryRun: options.dryRun })
416
413
  if (result.error) {
417
- console.error(`error: ${result.error} at ${targetDir}`)
418
- return 1
414
+ console.log(`nothing to remove at ${targetDir} (${result.error})`)
415
+ return 0
419
416
  }
420
417
  console.log(
421
418
  `${options.dryRun ? "would remove" : "removed"} ${result.removed.length} skill(s); ` +
@@ -167,12 +167,13 @@ export function linkEntry({ from, dest, type = "dir" }) {
167
167
  * @param options.project - Use a project-relative directory instead of global
168
168
  * @param cwd - Project root used for `--project`
169
169
  * @param kind - Subdirectory name under `.opencode` (for example `skills`)
170
+ * @param home - Home directory used for the global default (injectable for tests)
170
171
  * @returns Absolute path to the target directory
171
172
  */
172
- export function resolveTarget(options, cwd, kind) {
173
+ export function resolveTarget(options, cwd, kind, home = homedir()) {
173
174
  if (options.target) return resolve(options.target)
174
175
  if (options.project) return resolve(cwd, ".opencode", kind)
175
- return join(homedir(), ".config", "opencode", kind)
176
+ return join(home, ".config", "opencode", kind)
176
177
  }
177
178
 
178
179
  /**
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Registers (or inspects) the CodeOps plugin in OpenCode's own config.
4
+ *
5
+ * OpenCode ships a command that installs a plugin and updates the config
6
+ * safely, including JSON/JSONC formatting and version replacement:
7
+ *
8
+ * opencode plugin <module> [--global] [--force]
9
+ *
10
+ * The installer delegates to that command instead of editing `opencode.json`
11
+ * itself, so it never has to parse JSONC or risk clobbering a user's config.
12
+ * All calls are best-effort: if the `opencode` executable is missing or the
13
+ * command fails, the caller still completes the file install and tells the user
14
+ * how to register the plugin manually.
15
+ *
16
+ * @module lib/opencode-plugin
17
+ */
18
+
19
+ import { spawnSync } from "node:child_process"
20
+
21
+ /** The npm package name of the plugin. */
22
+ export const PLUGIN_NAME = "opencode-codeops"
23
+
24
+ /**
25
+ * Runs a command and normalizes the result.
26
+ *
27
+ * @param command - Executable to run
28
+ * @param args - Argument list
29
+ * @param options - Spawn options (for example `cwd`)
30
+ * @returns The exit status, stdout, stderr, and any spawn error
31
+ */
32
+ export function defaultRun(command, args, options = {}) {
33
+ const result = spawnSync(command, args, { encoding: "utf-8", ...options })
34
+ return {
35
+ status: result.status ?? 1,
36
+ stdout: result.stdout ?? "",
37
+ stderr: result.stderr ?? "",
38
+ error: result.error,
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Builds the arguments for `opencode plugin`.
44
+ *
45
+ * @param details - Plugin registration inputs
46
+ * @param details.scope - `"global"` adds `--global`; `"project"` omits it
47
+ * @param details.version - Exact version to pin, or `undefined` for the bare name
48
+ * @returns Arguments after the `opencode` executable
49
+ * @example
50
+ * buildPluginArgs({ scope: "global", version: "1.0.0" })
51
+ * // ["plugin", "opencode-codeops@1.0.0", "--global", "--force"]
52
+ */
53
+ export function buildPluginArgs({ scope, version }) {
54
+ const module = version ? `${PLUGIN_NAME}@${version}` : PLUGIN_NAME
55
+ const args = ["plugin", module]
56
+ if (scope === "global") args.push("--global")
57
+ args.push("--force")
58
+ return args
59
+ }
60
+
61
+ /**
62
+ * Registers the plugin in the OpenCode config.
63
+ *
64
+ * Tries the exact version first so plugin and files stay in sync; if the CLI
65
+ * rejects the versioned spec, falls back to the bare package name. Never throws.
66
+ *
67
+ * @param details - Registration inputs
68
+ * @param details.scope - `"global"` or `"project"`
69
+ * @param details.version - Version to pin
70
+ * @param details.cwd - Working directory (used for project scope)
71
+ * @param details.run - Command runner, injectable for tests
72
+ * @returns Whether registration succeeded, the spec used, and a reason on failure
73
+ */
74
+ export function registerPlugin({ scope, version, cwd, run = defaultRun }) {
75
+ try {
76
+ const probe = run("opencode", ["--version"], { cwd })
77
+ if (probe.error || probe.status !== 0) {
78
+ return { ok: false, spec: null, reason: "opencode CLI not found on PATH" }
79
+ }
80
+
81
+ const pinned = run("opencode", buildPluginArgs({ scope, version }), { cwd })
82
+ if (!pinned.error && pinned.status === 0) {
83
+ return { ok: true, spec: `${PLUGIN_NAME}@${version}` }
84
+ }
85
+
86
+ const bare = run("opencode", buildPluginArgs({ scope, version: null }), { cwd })
87
+ if (!bare.error && bare.status === 0) {
88
+ return { ok: true, spec: PLUGIN_NAME }
89
+ }
90
+
91
+ const message = (bare.stderr || bare.stdout || pinned.stderr || pinned.stdout || "").trim()
92
+ return { ok: false, spec: null, reason: message.split("\n").pop() || "opencode plugin failed" }
93
+ } catch (caught) {
94
+ return { ok: false, spec: null, reason: caught.message }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Reads the resolved `plugin` list from OpenCode's effective config.
100
+ *
101
+ * @param details - Inspection inputs
102
+ * @param details.cwd - Working directory
103
+ * @param details.run - Command runner, injectable for tests
104
+ * @returns The plugin entries, or `undefined` when the config cannot be read
105
+ */
106
+ export function readConfiguredPlugin({ cwd, run = defaultRun } = {}) {
107
+ try {
108
+ const result = run("opencode", ["debug", "config"], { cwd })
109
+ if (result.error || result.status !== 0) return undefined
110
+
111
+ const config = JSON.parse(result.stdout)
112
+ return Array.isArray(config.plugin) ? config.plugin : []
113
+ } catch {
114
+ return undefined
115
+ }
116
+ }
package/install.sh CHANGED
@@ -17,11 +17,11 @@
17
17
  # defaults to "latest").
18
18
  #
19
19
  # Subcommands (see `npx opencode-codeops help` for the full option list):
20
- # install-skills Install or upgrade the skills (default)
21
- # install-agents Install or upgrade the subagents
22
- # status Show the installed skills and agents version
23
- # uninstall Remove the managed skills
24
- # help Show installer help
20
+ # install Install or upgrade skills and subagents (default)
21
+ # update Alias of install
22
+ # status Show the installed versions
23
+ # uninstall Remove the managed files
24
+ # help Show installer help
25
25
 
26
26
  set -euo pipefail
27
27
 
@@ -40,12 +40,12 @@ if ! command -v npx >/dev/null 2>&1; then
40
40
  exit 1
41
41
  fi
42
42
 
43
- # The subcommand is optional and defaults to install-skills. Any remaining
44
- # arguments are passed through to the installer unchanged.
45
- subcommand="install-skills"
43
+ # The subcommand is optional and defaults to install. Any remaining arguments
44
+ # are passed through to the installer unchanged.
45
+ subcommand="install"
46
46
  if (( $# > 0 )); then
47
47
  case "$1" in
48
- install | install-skills | install-agents | status | uninstall | help)
48
+ install | update | status | uninstall | help)
49
49
  subcommand="$1"
50
50
  shift
51
51
  ;;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codeops",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Specification-first engineering for complex systems — CodeOps plugin for OpenCode",
5
5
  "type": "module",
6
6
  "engines": {
package/plugin/index.ts CHANGED
@@ -79,9 +79,9 @@ function installedSkillsVersion(skillsDir: string): string | undefined {
79
79
 
80
80
  // ---------------------------------------------------------------------------
81
81
  // Helper — warn (non-blocking) when the installed skills were written by a
82
- // different CodeOps version than this plugin. The plugin and the skills are
82
+ // different CodeOps version than this plugin. The plugin and the files are
83
83
  // installed by separate commands, so their versions can drift; a mismatch
84
- // usually means the skills need `npx opencode-codeops install-skills` again.
84
+ // usually means the skills need `npx opencode-codeops update` again.
85
85
  // ---------------------------------------------------------------------------
86
86
  async function warnOnVersionSkew(
87
87
  client: Parameters<Plugin>[0]["client"],
@@ -103,7 +103,7 @@ async function warnOnVersionSkew(
103
103
  message:
104
104
  `CodeOps skills at ${skillsDir} are version ${installed}, ` +
105
105
  `but the plugin is version ${packageVersion}. ` +
106
- `Run \`npx opencode-codeops@${packageVersion} install-skills\` to match them.`,
106
+ `Run \`npx opencode-codeops@${packageVersion} update\` to match them.`,
107
107
  },
108
108
  })
109
109
  }
@@ -76,24 +76,24 @@ default when it is absent — so this line is a convenience/pin, not a requireme
76
76
  - Scaffolding is intentionally simple, so it lives in skill prose; only the *migration* path
77
77
  needs the deterministic engine. For migration, see [migration.md](migration.md).
78
78
 
79
- ## Agent file installation
79
+ ## Project file installation
80
80
 
81
- After scaffolding, install the CodeOps OpenCode agent definitions into the project. Prefer the
82
- installer bundled with the running plugin, so the agent version always matches the plugin version:
81
+ After scaffolding, install the CodeOps skills and subagents into the project. Prefer the installer
82
+ bundled with the running plugin, so the installed files always match the plugin version:
83
83
 
84
84
  ```bash
85
- node "${CODEOPS_PLUGIN_ROOT}/bin/install-agents.mjs" --project
85
+ node "${CODEOPS_PLUGIN_ROOT}/bin/index.mjs" install --project
86
86
  ```
87
87
 
88
88
  When the plugin is not active, the published installer does the same thing:
89
89
 
90
90
  ```bash
91
- npx -y opencode-codeops@latest install-agents --project
91
+ npx -y opencode-codeops@latest install --project
92
92
  ```
93
93
 
94
- This installs the 12 CodeOps subagent definitions (`executor`, `explorer`, `correctness-reviewer`,
95
- etc.) into `.opencode/agents/` where OpenCode will discover and load them automatically. These files
96
- are safe to commit to git. Files the installer owns are replaced on upgrade; same-named files it
97
- does not own are left untouched (pass `--force` to replace them). Users can override individual
98
- agent models in `opencode.json` under the `agent` key.
94
+ This installs the skills into `.opencode/skills/` and the 12 subagent definitions (`executor`,
95
+ `explorer`, `correctness-reviewer`, etc.) into `.opencode/agents/`, where OpenCode discovers and
96
+ loads them automatically. These files are safe to commit to git. Files the installer owns are
97
+ replaced on upgrade; same-named files it does not own are left untouched (pass `--force` to replace
98
+ them). Users can override individual agent models in `opencode.json` under the `agent` key.
99
99