@wntic/ocm 0.1.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 +286 -0
- package/bin/ocm.ts +7 -0
- package/loader/config.js +47 -0
- package/loader/core.d.ts +256 -0
- package/loader/core.js +40 -0
- package/loader/discovery.js +162 -0
- package/loader/links.js +193 -0
- package/loader/lint.js +83 -0
- package/loader/manifest.js +158 -0
- package/loader/marketplace.js +199 -0
- package/loader/materialize.js +218 -0
- package/loader/mcp.js +124 -0
- package/loader/mutations.js +102 -0
- package/loader/ocm-loader.js +21 -0
- package/loader/paths.js +20 -0
- package/loader/registry.js +163 -0
- package/loader/search.js +55 -0
- package/loader/source.js +100 -0
- package/loader/sync.js +151 -0
- package/loader/trust.js +114 -0
- package/loader/ui-dialog.js +46 -0
- package/loader/ui-marketplaces.js +187 -0
- package/loader/ui-plugins.js +101 -0
- package/loader/ui-trust.js +68 -0
- package/loader/ui.js +109 -0
- package/package.json +32 -0
- package/src/commands/doctor-config.ts +79 -0
- package/src/commands/doctor-links.ts +170 -0
- package/src/commands/doctor.ts +144 -0
- package/src/commands/info.ts +159 -0
- package/src/commands/list.ts +44 -0
- package/src/commands/marketplace.ts +99 -0
- package/src/commands/plugins.ts +127 -0
- package/src/commands/search.ts +86 -0
- package/src/commands/trust.ts +146 -0
- package/src/commands/update-report.ts +123 -0
- package/src/commands/update.ts +160 -0
- package/src/commands/validate-files.ts +145 -0
- package/src/commands/validate.ts +77 -0
- package/src/discovery.ts +36 -0
- package/src/findings.ts +33 -0
- package/src/git.ts +24 -0
- package/src/index.ts +175 -0
- package/src/install.ts +15 -0
- package/src/loader.ts +185 -0
- package/src/manifest-lint.ts +189 -0
- package/src/migrate.ts +117 -0
- package/src/paths.ts +22 -0
- package/src/probe.ts +52 -0
- package/src/registry.ts +25 -0
- package/src/renames.ts +83 -0
- package/src/report.ts +13 -0
- package/src/types.ts +70 -0
- package/template/marketplace.json +17 -0
- package/template/plugins/demo-kit/agents/reviewer.md +20 -0
- package/template/plugins/demo-kit/commands/tdd.md +12 -0
- package/template/plugins/demo-kit/mcp.json +3 -0
- package/template/plugins/demo-kit/plugin/notify.js +4 -0
- package/template/plugins/demo-kit/plugin.json +6 -0
- package/template/plugins/demo-kit/skills/code-review/SKILL.md +21 -0
- package/template/plugins/release-kit/commands/ship.md +11 -0
- package/template/plugins/release-kit/commands.claude/ship.md +11 -0
- package/template/plugins/release-kit/plugin.json +6 -0
- package/template/plugins/release-kit/skills/release-notes/SKILL.md +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# ocm — file-based plugin marketplace for opencode
|
|
2
|
+
|
|
3
|
+
Distribute **skills, agents, commands, JS plugins and MCP servers** for
|
|
4
|
+
[opencode](https://opencode.ai) via plain git repositories. Installed once at
|
|
5
|
+
user-global scope — available in **every project**, even without `.opencode/`
|
|
6
|
+
or `.claude/` directories.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
bunx @wntic/ocm add https://github.com/you/my-marketplace
|
|
12
|
+
# or, for the `ocm` binary on PATH:
|
|
13
|
+
bun install -g @wntic/ocm
|
|
14
|
+
ocm add https://github.com/you/my-marketplace
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The npm package is `@wntic/ocm`; the binary it installs is `ocm`. Requires
|
|
18
|
+
[opencode](https://opencode.ai), git and [bun](https://bun.com).
|
|
19
|
+
|
|
20
|
+
## How it works
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
github.com/you/my-marketplace single source of truth (markdown + manifests)
|
|
24
|
+
│
|
|
25
|
+
ocm add (clone to ~/.cache/ocm/marketplaces/<name>)
|
|
26
|
+
│
|
|
27
|
+
├── commands → ~/.config/opencode/commands/<plugin>:<command>.md
|
|
28
|
+
├── agents → ~/.config/opencode/agents/<plugin>:<agent>.md
|
|
29
|
+
├── skills → ~/.cache/ocm/links/<mp>/skills/<plugin>--<skill>/SKILL.md
|
|
30
|
+
│ (rendered: name rewritten to <plugin>:<skill>)
|
|
31
|
+
│ + one skills.paths entry in global opencode.json
|
|
32
|
+
├── js plugins → ~/.config/opencode/plugins/ocm--<plugin>--<file>.js
|
|
33
|
+
└── mcp → ocm--<plugin>--<server> keys in global opencode.json
|
|
34
|
+
│
|
|
35
|
+
ocm-loader.js (global plugin) syncs on opencode start, throttled
|
|
36
|
+
per marketplace
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
No file copying into projects. Update the repo — every project picks it up on
|
|
40
|
+
the next opencode start.
|
|
41
|
+
|
|
42
|
+
## Changes apply on the next opencode start
|
|
43
|
+
|
|
44
|
+
opencode reads commands, agents, skills, plugins and MCP config **once at
|
|
45
|
+
startup**. There is no reload API: nothing ocm does — install, uninstall,
|
|
46
|
+
update — can make a change visible in a session that is already running.
|
|
47
|
+
Every ocm operation that materializes something ends with:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
restart opencode to activate
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The loader's startup sync exists to make the *next* start current, not the
|
|
54
|
+
current one. This is a verified property of opencode, not a choice — see
|
|
55
|
+
[docs/opencode-contract.md](docs/opencode-contract.md) for what was checked
|
|
56
|
+
against which opencode version.
|
|
57
|
+
|
|
58
|
+
## Naming
|
|
59
|
+
|
|
60
|
+
Commands and agents are namespaced by plugin:
|
|
61
|
+
`plugins/adw/commands/commit.md` becomes `/adw:commit` in the opencode TUI,
|
|
62
|
+
`plugins/adw/agents/reviewer.md` becomes `adw:reviewer`.
|
|
63
|
+
|
|
64
|
+
Skills are named by their **frontmatter `name`**, not their folder. ocm
|
|
65
|
+
therefore renders each installed skill into its own mirror directory with the
|
|
66
|
+
name rewritten to `<plugin>:<skill>` — `adw:python-style`. opencode never
|
|
67
|
+
lists skills in the `/` autocomplete by design — skills are invoked by the
|
|
68
|
+
model through the `skill` tool, or browsed via the `/skills` picker.
|
|
69
|
+
|
|
70
|
+
### Breaking change: skill names
|
|
71
|
+
|
|
72
|
+
Earlier versions symlinked a plugin's whole `skills/` directory, so an
|
|
73
|
+
installed skill kept its own name (`python-style`). Skills now install under
|
|
74
|
+
their namespaced name (`adw:python-style`). If you referenced an installed
|
|
75
|
+
skill by name — in a project file or a prompt — update the reference. The
|
|
76
|
+
automatic migration (below) prints the old → new mapping when it relinks.
|
|
77
|
+
|
|
78
|
+
## Marketplace repository format
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
my-marketplace/
|
|
82
|
+
├── marketplace.json # optional
|
|
83
|
+
└── plugins/
|
|
84
|
+
├── demo-kit/
|
|
85
|
+
│ ├── plugin.json # optional
|
|
86
|
+
│ ├── commands/
|
|
87
|
+
│ │ └── tdd.md # → /demo-kit:tdd
|
|
88
|
+
│ ├── commands.claude/ # Claude Code sibling, ignored by ocm
|
|
89
|
+
│ │ └── tdd.md
|
|
90
|
+
│ ├── agents/
|
|
91
|
+
│ │ └── reviewer.md # → demo-kit:reviewer
|
|
92
|
+
│ ├── skills/
|
|
93
|
+
│ │ └── code-review/
|
|
94
|
+
│ │ └── SKILL.md # skill "demo-kit:code-review"
|
|
95
|
+
│ ├── plugin/
|
|
96
|
+
│ │ └── notify.js # opencode server plugin (trust-gated)
|
|
97
|
+
│ ├── mcp.json # mcp servers (trust-gated)
|
|
98
|
+
│ └── scripts/, templates/ # supporting files, never materialized
|
|
99
|
+
└── release-kit/
|
|
100
|
+
└── … # a second plugin, same rules
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
A working example with two plugins ships in [template/](template/). A
|
|
104
|
+
marketplace is any git repo with `plugins/<name>/`. Every directory under
|
|
105
|
+
`plugins/` with at least one component is an installable plugin, manifest or
|
|
106
|
+
not — manifests add metadata, they never hide a plugin.
|
|
107
|
+
|
|
108
|
+
`command`/`commands`, `agent`/`agents`, `skill`/`skills` and `plugin`/`plugins`
|
|
109
|
+
are all accepted (singular matches opencode's own globs); a name clash between
|
|
110
|
+
the singular and plural form of the same type is an error.
|
|
111
|
+
|
|
112
|
+
### Cross-tool authoring
|
|
113
|
+
|
|
114
|
+
Unsuffixed directories are opencode's. A `commands.claude/` sibling holds the
|
|
115
|
+
Claude Code form of the same command; ocm ignores it, Claude Code reads it
|
|
116
|
+
through its own manifest. `skills/` is shared unchanged between the tools —
|
|
117
|
+
keep skill frontmatter to the portable subset (`name`, `description`,
|
|
118
|
+
`license`, `compatibility`, `metadata`); `ocm validate` warns on anything
|
|
119
|
+
else. Command bodies that reference supporting scripts use
|
|
120
|
+
`"${OCM_PLUGIN_ROOT}/plugins/<name>/scripts/…"` — the loader exports
|
|
121
|
+
`OCM_PLUGIN_ROOT` (and `CLAUDE_PLUGIN_ROOT` as an alias) pointing at the
|
|
122
|
+
marketplace root.
|
|
123
|
+
|
|
124
|
+
### `marketplace.json`
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"name": "my-marketplace",
|
|
129
|
+
"description": "Team plugin catalog",
|
|
130
|
+
"owner": { "name": "…", "email": "…", "url": "…" },
|
|
131
|
+
"homepage": "https://…",
|
|
132
|
+
"renames": { "old-plugin": "new-plugin", "dead-plugin": null },
|
|
133
|
+
"plugins": [
|
|
134
|
+
{
|
|
135
|
+
"name": "demo-kit",
|
|
136
|
+
"source": "./plugins/demo-kit",
|
|
137
|
+
"description": "…",
|
|
138
|
+
"version": "1.2.0",
|
|
139
|
+
"defaultEnabled": true,
|
|
140
|
+
"mcpServers": "./mcp.json"
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Only a plugin entry's `name` and `source` are required. `source` is a
|
|
147
|
+
`./`-relative path inside the marketplace — `../` and absolute paths are
|
|
148
|
+
rejected; the marketplace repo is the distribution unit. `defaultEnabled:
|
|
149
|
+
false` keeps a plugin disabled in an `auto` marketplace.
|
|
150
|
+
|
|
151
|
+
### `plugin.json`
|
|
152
|
+
|
|
153
|
+
The same fields as a `plugins[]` entry, minus `source`, `defaultEnabled` and
|
|
154
|
+
`mcpServers` — a plugin directory is self-describing when vendored or read on
|
|
155
|
+
its own. Metadata precedence: marketplace entry > `plugin.json` > filesystem
|
|
156
|
+
inference (name from the directory, components from the scan).
|
|
157
|
+
|
|
158
|
+
### JS plugins and MCP servers
|
|
159
|
+
|
|
160
|
+
`plugin/*.{js,ts}` are opencode server plugins. They execute, so they
|
|
161
|
+
materialize only after trust is granted for their marketplace — until then
|
|
162
|
+
they are reported as `blocked (untrusted)`. Each must default-export
|
|
163
|
+
`{ id, server }` (see `template/plugins/demo-kit/plugin/notify.js`).
|
|
164
|
+
|
|
165
|
+
`mcp.json` uses exactly opencode's `mcp` entry shape:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"time": { "type": "local", "command": ["date"], "enabled": true }
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Both are namespaced under ocm's ownership prefix: JS plugins link as
|
|
174
|
+
`~/.config/opencode/plugins/ocm--<plugin>--<file>`, MCP servers as
|
|
175
|
+
`ocm--<plugin>--<server>` keys in the global `opencode.json`. MCP components
|
|
176
|
+
are trust-gated too — a local server is a command line ocm caused to run.
|
|
177
|
+
The user's own `mcp` keys are preserved.
|
|
178
|
+
|
|
179
|
+
## Per-plugin install
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
ocm add https://github.com/user/my-marketplace # clone + register
|
|
183
|
+
ocm add ~/plugins/my-marketplace # local dir, never written to
|
|
184
|
+
ocm add <url> --explicit # register nothing until asked
|
|
185
|
+
ocm install commit-tools # enable one plugin
|
|
186
|
+
ocm install commit-tools@my-marketplace # … disambiguated
|
|
187
|
+
ocm uninstall commit-tools # disable, keep the record
|
|
188
|
+
ocm mode my-marketplace explicit # change how new plugins land
|
|
189
|
+
ocm list [--all] [--json] # what is installed
|
|
190
|
+
ocm search <query> # search cached metadata
|
|
191
|
+
ocm info <plugin>[@<marketplace>] # one plugin's record
|
|
192
|
+
ocm update [name|plugin@mp] # pull + re-materialize
|
|
193
|
+
ocm pin <name> <ref> # follow a branch or tag
|
|
194
|
+
ocm pin <name> --clear # back to the default branch
|
|
195
|
+
ocm remove my-marketplace # full teardown
|
|
196
|
+
ocm scan <url|path|plugin> # dry run
|
|
197
|
+
ocm validate [path] # lint a marketplace repo
|
|
198
|
+
ocm doctor [--fix] # diagnose this installation
|
|
199
|
+
ocm init # reinstall the auto-sync loader
|
|
200
|
+
ocm loader uninstall # remove the auto-sync loader
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
A marketplace with ten plugins and two installs is the normal case. In `auto`
|
|
204
|
+
mode (the default) every discovered plugin is enabled on add and new upstream
|
|
205
|
+
plugins install as they appear; `--explicit` (or `ocm mode <mp> explicit`)
|
|
206
|
+
installs nothing until you name it. `uninstall` keeps the record, so
|
|
207
|
+
re-installing is instant and offline.
|
|
208
|
+
|
|
209
|
+
## Precedence
|
|
210
|
+
|
|
211
|
+
- **Project beats global.** ocm installs at user-global scope only; a
|
|
212
|
+
project's own `.opencode/` always wins over anything ocm installed.
|
|
213
|
+
- **ocm never writes into `~/.claude`, `.claude`, `~/.agents` or `.agents`.**
|
|
214
|
+
Those paths belong to other tools.
|
|
215
|
+
- **Hand-authored files beat marketplace-installed ones.** ocm never
|
|
216
|
+
overwrites a file it does not own: a collision is reported and skipped;
|
|
217
|
+
`ocm install --force` takes over and names the displaced path (the file
|
|
218
|
+
moves to `~/.cache/ocm/displaced/`, it is never deleted).
|
|
219
|
+
- **Plugin names are globally unique across marketplaces.** Adding a
|
|
220
|
+
marketplace that ships an already-provided name fails with both sources
|
|
221
|
+
named; an upstream collision registers the newcomer disabled — the
|
|
222
|
+
incumbent is never displaced.
|
|
223
|
+
- **No ordering promise between JS plugins.** opencode loads
|
|
224
|
+
directory-discovered plugins in filesystem order; two plugins hooking the
|
|
225
|
+
same event both run, in unspecified order.
|
|
226
|
+
- **A failing marketplace is isolated.** A failed pull records
|
|
227
|
+
`lastSync.ok = false` and leaves that marketplace's links intact; every
|
|
228
|
+
other marketplace still syncs.
|
|
229
|
+
|
|
230
|
+
## Trust
|
|
231
|
+
|
|
232
|
+
JS plugins and MCP servers execute. They materialize only after you approve
|
|
233
|
+
their marketplace (`ocm trust <name>`); until then they are reported as
|
|
234
|
+
`blocked (untrusted)` while the rest of the plugin installs normally.
|
|
235
|
+
`ocm untrust` revokes and removes them. A grant is fingerprinted per
|
|
236
|
+
component — an upstream change to an executable component re-blocks it until
|
|
237
|
+
re-approved.
|
|
238
|
+
|
|
239
|
+
## Loader (auto-sync)
|
|
240
|
+
|
|
241
|
+
`ocm add` installs `~/.config/opencode/plugins/ocm-loader.js` — the only ocm
|
|
242
|
+
file in that directory — plus the runtime core and the TUI plugin under
|
|
243
|
+
`~/.config/opencode/ocm/`. Your `opencode.json` gains nothing but a
|
|
244
|
+
`skills.paths` entry; `tui.json` gains the single `./ocm/ui.js` plugin entry.
|
|
245
|
+
|
|
246
|
+
On every opencode start the loader:
|
|
247
|
+
|
|
248
|
+
1. pulls each git marketplace (shallow-clone safe), throttled to once an hour
|
|
249
|
+
per marketplace by default — `syncIntervalMs` per marketplace or
|
|
250
|
+
`OCM_SYNC_INTERVAL_MS` globally; `OCM_SYNC_DISABLE=1` turns sync off;
|
|
251
|
+
2. re-materializes links, so newly added or removed upstream plugins are
|
|
252
|
+
picked up;
|
|
253
|
+
3. records `lastSync` per marketplace — a failed pull is recorded and never
|
|
254
|
+
touches that marketplace's links.
|
|
255
|
+
|
|
256
|
+
A sync makes the *next* opencode start current. Remove the loader any time
|
|
257
|
+
with `ocm loader uninstall`.
|
|
258
|
+
|
|
259
|
+
## TUI integration (`/ocm`)
|
|
260
|
+
|
|
261
|
+
`ocm init` also installs a TUI plugin under `~/.config/opencode/ocm/` and
|
|
262
|
+
registers it in `~/.config/opencode/tui.json` — the only entry ocm owns in
|
|
263
|
+
that file; theme, keybinds and other user settings are preserved. Restart
|
|
264
|
+
opencode, then type `/ocm` in the prompt: browse plugins across marketplaces,
|
|
265
|
+
update all of them, or update one marketplace. `OPENCODE_PURE=1` disables the
|
|
266
|
+
TUI plugin along with all other external plugins.
|
|
267
|
+
|
|
268
|
+
## Migration
|
|
269
|
+
|
|
270
|
+
Any `ocm` command — including `ocm doctor` — automatically migrates an
|
|
271
|
+
installation from the earlier layouts: loader files move from `plugins/` to
|
|
272
|
+
`ocm/`, the registry upgrades to v2 with marketplace-relative sources, the
|
|
273
|
+
global sync stamp folds into per-marketplace `lastSync`, skills relink under
|
|
274
|
+
namespaced names (the one breaking change, above), and legacy `ocm--<mp>`
|
|
275
|
+
containers are removed. The migration is idempotent, prints one line per
|
|
276
|
+
thing moved, and never touches files ocm does not own.
|
|
277
|
+
|
|
278
|
+
## Requirements
|
|
279
|
+
|
|
280
|
+
- [opencode](https://opencode.ai)
|
|
281
|
+
- git
|
|
282
|
+
- [bun](https://bun.com) (to run the CLI)
|
|
283
|
+
|
|
284
|
+
## License
|
|
285
|
+
|
|
286
|
+
MIT
|
package/bin/ocm.ts
ADDED
package/loader/config.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { OPENCODE_CONFIG_FILE, OPENCODE_DIR } from "./paths.js"
|
|
3
|
+
import { isRecord } from "./registry.js"
|
|
4
|
+
|
|
5
|
+
export function setSkillsPath(skillsDir, present) {
|
|
6
|
+
let config = {}
|
|
7
|
+
let raw
|
|
8
|
+
try {
|
|
9
|
+
raw = readFileSync(OPENCODE_CONFIG_FILE, "utf8")
|
|
10
|
+
} catch {}
|
|
11
|
+
if (raw !== undefined) {
|
|
12
|
+
try {
|
|
13
|
+
config = JSON.parse(raw)
|
|
14
|
+
} catch {
|
|
15
|
+
return `skipped ${OPENCODE_CONFIG_FILE}: not valid JSON, left untouched`
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (!isRecord(config)) return `skipped ${OPENCODE_CONFIG_FILE}: not a JSON object`
|
|
19
|
+
if (config.skills !== undefined && !isRecord(config.skills)) {
|
|
20
|
+
return `skipped ${OPENCODE_CONFIG_FILE}: "skills" is not an object`
|
|
21
|
+
}
|
|
22
|
+
const skills = isRecord(config.skills) ? config.skills : {}
|
|
23
|
+
if (skills.paths !== undefined && !Array.isArray(skills.paths)) {
|
|
24
|
+
return `skipped ${OPENCODE_CONFIG_FILE}: "skills.paths" is not an array`
|
|
25
|
+
}
|
|
26
|
+
const paths = Array.isArray(skills.paths) ? skills.paths : []
|
|
27
|
+
if (paths.includes(skillsDir) === present) return null
|
|
28
|
+
const next = present ? [...paths, skillsDir] : paths.filter((p) => p !== skillsDir)
|
|
29
|
+
const updated = { ...config }
|
|
30
|
+
if (present || next.length) {
|
|
31
|
+
updated.skills = { ...skills, paths: next }
|
|
32
|
+
} else if (Object.keys(skills).some((key) => key !== "paths")) {
|
|
33
|
+
updated.skills = { ...skills }
|
|
34
|
+
delete updated.skills.paths
|
|
35
|
+
} else {
|
|
36
|
+
delete updated.skills
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
mkdirSync(OPENCODE_DIR, { recursive: true })
|
|
40
|
+
const tmp = `${OPENCODE_CONFIG_FILE}.tmp`
|
|
41
|
+
writeFileSync(tmp, `${JSON.stringify(updated, null, 2)}\n`)
|
|
42
|
+
renameSync(tmp, OPENCODE_CONFIG_FILE)
|
|
43
|
+
} catch (err) {
|
|
44
|
+
return `failed ${OPENCODE_CONFIG_FILE}: ${err instanceof Error ? err.message : String(err)}`
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
package/loader/core.d.ts
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
export interface CorePluginComponents {
|
|
2
|
+
agent?: string[]
|
|
3
|
+
command?: string[]
|
|
4
|
+
skill?: string[]
|
|
5
|
+
plugin?: string[]
|
|
6
|
+
mcp?: string[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface CoreDiscoveredPlugin {
|
|
10
|
+
name: string
|
|
11
|
+
dir: string
|
|
12
|
+
components: CorePluginComponents
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CorePullResult {
|
|
16
|
+
ok: boolean
|
|
17
|
+
changed: boolean
|
|
18
|
+
before: string
|
|
19
|
+
after: string
|
|
20
|
+
dirty: boolean
|
|
21
|
+
output: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CoreMaterializeReport {
|
|
25
|
+
counts: { command: number; agent: number; skill: number; plugin: number; mcp: number }
|
|
26
|
+
created: number
|
|
27
|
+
removed: number
|
|
28
|
+
skipped: number
|
|
29
|
+
warnings: string[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CoreSyncResult {
|
|
33
|
+
ran: boolean
|
|
34
|
+
changed: boolean
|
|
35
|
+
updated: string[]
|
|
36
|
+
unchanged: string[]
|
|
37
|
+
failed: string[]
|
|
38
|
+
errors: Record<string, string>
|
|
39
|
+
warnings?: string[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CoreExecutableComponent {
|
|
43
|
+
rel: string
|
|
44
|
+
hash: string
|
|
45
|
+
kind: "plugin" | "mcp"
|
|
46
|
+
plugin: string
|
|
47
|
+
name: string
|
|
48
|
+
value?: unknown
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// the merged manifest cached in the registry (spec 06); a structural mirror
|
|
52
|
+
// of PluginManifest in src/types.ts so src types and core types assign
|
|
53
|
+
// without casts
|
|
54
|
+
export interface CorePluginManifest {
|
|
55
|
+
description?: string
|
|
56
|
+
category?: string
|
|
57
|
+
tags?: string[]
|
|
58
|
+
keywords?: string[]
|
|
59
|
+
version?: string
|
|
60
|
+
homepage?: string
|
|
61
|
+
license?: string
|
|
62
|
+
defaultEnabled?: boolean
|
|
63
|
+
mcpServers?: string
|
|
64
|
+
conflicts?: string[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface CoreManifestPlugin {
|
|
68
|
+
name: string
|
|
69
|
+
dir: string
|
|
70
|
+
source: string
|
|
71
|
+
components: Partial<Record<"agent" | "command" | "skill" | "plugin" | "mcp", string[]>>
|
|
72
|
+
manifest: CorePluginManifest
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface CoreDiscoveredMarketplace {
|
|
76
|
+
plugins: Map<string, CoreManifestPlugin>
|
|
77
|
+
warnings: string[]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface CoreMarketplacePlugin {
|
|
81
|
+
source: string
|
|
82
|
+
components: Partial<Record<"agent" | "command" | "skill" | "plugin" | "mcp", string[]>>
|
|
83
|
+
enabled: boolean
|
|
84
|
+
collision?: string
|
|
85
|
+
installedAt: string | null
|
|
86
|
+
version: string | null
|
|
87
|
+
manifest: CorePluginManifest
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface CoreMarketplaceEntry {
|
|
91
|
+
url: string
|
|
92
|
+
dir: string
|
|
93
|
+
local: boolean
|
|
94
|
+
addedAt: string
|
|
95
|
+
mode: "auto" | "explicit"
|
|
96
|
+
ref: string | null
|
|
97
|
+
subdir?: string | null
|
|
98
|
+
revision: string | null
|
|
99
|
+
syncIntervalMs: number | null
|
|
100
|
+
trust: {
|
|
101
|
+
code: "none" | "granted" | "denied"
|
|
102
|
+
grantedAt?: string
|
|
103
|
+
fingerprint?: string
|
|
104
|
+
components?: Record<string, string>
|
|
105
|
+
}
|
|
106
|
+
trustPending?: boolean
|
|
107
|
+
lastSync: { at: string; ok: boolean; error: string | null } | null
|
|
108
|
+
plugins: Record<string, CoreMarketplacePlugin>
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface CoreRegistry {
|
|
112
|
+
version: 2
|
|
113
|
+
marketplaces: Record<string, CoreMarketplaceEntry>
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface CoreParsedSource {
|
|
117
|
+
url: string
|
|
118
|
+
name: string
|
|
119
|
+
subdir: string | null
|
|
120
|
+
isGit: boolean
|
|
121
|
+
ref: string | null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface CoreAddOptions {
|
|
125
|
+
explicit?: boolean
|
|
126
|
+
name?: string
|
|
127
|
+
ref?: string
|
|
128
|
+
trust?: boolean
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface CoreAddResult {
|
|
132
|
+
name: string
|
|
133
|
+
url: string
|
|
134
|
+
dir: string
|
|
135
|
+
root: string
|
|
136
|
+
mode: "auto" | "explicit"
|
|
137
|
+
plugins: { name: string; components: CorePluginComponents }[]
|
|
138
|
+
warnings: string[]
|
|
139
|
+
report: CoreMaterializeReport
|
|
140
|
+
trustComponents: CoreExecutableComponent[]
|
|
141
|
+
wasV1: boolean
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface CoreRemoveResult {
|
|
145
|
+
name: string
|
|
146
|
+
owned: { name: string; components: CorePluginComponents }[]
|
|
147
|
+
warnings: string[]
|
|
148
|
+
wasV1: boolean
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface CoreSetEnabledResult {
|
|
152
|
+
marketplace: string
|
|
153
|
+
plugin: string
|
|
154
|
+
components: CorePluginComponents
|
|
155
|
+
disagreement: string | null
|
|
156
|
+
wasV1: boolean
|
|
157
|
+
report: CoreMaterializeReport
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface CoreGrantResult {
|
|
161
|
+
granted: boolean
|
|
162
|
+
report: CoreMaterializeReport | null
|
|
163
|
+
wasV1: boolean
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface CoreDenyResult {
|
|
167
|
+
report: CoreMaterializeReport
|
|
168
|
+
wasV1: boolean
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface CorePinResult {
|
|
172
|
+
name: string
|
|
173
|
+
ref: string | null
|
|
174
|
+
cleared: boolean
|
|
175
|
+
wasV1: boolean
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface CoreSearchMatch {
|
|
179
|
+
marketplace: string
|
|
180
|
+
plugin: string
|
|
181
|
+
entry: CoreMarketplaceEntry
|
|
182
|
+
record: CoreMarketplacePlugin
|
|
183
|
+
rank: number
|
|
184
|
+
matched: string[]
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface CoreResolvedPlugin {
|
|
188
|
+
marketplace: string
|
|
189
|
+
plugin: string
|
|
190
|
+
entry: CoreMarketplaceEntry
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export declare const HOME: string
|
|
194
|
+
export declare const OPENCODE_DIR: string
|
|
195
|
+
export declare const OPENCODE_CONFIG_FILE: string
|
|
196
|
+
export declare const OPENCODE_COMMANDS_DIR: string
|
|
197
|
+
export declare const OPENCODE_AGENTS_DIR: string
|
|
198
|
+
export declare const OPENCODE_PLUGINS_DIR: string
|
|
199
|
+
export declare const OCM_DIR: string
|
|
200
|
+
export declare const CACHE_DIR: string
|
|
201
|
+
export declare const MARKETPLACES_DIR: string
|
|
202
|
+
export declare const LINKS_DIR: string
|
|
203
|
+
export declare const REGISTRY_FILE: string
|
|
204
|
+
export declare const LEGACY_REGISTRY_FILE: string
|
|
205
|
+
export declare const STAMP_FILE: string
|
|
206
|
+
export declare const DEFAULT_SYNC_INTERVAL_MS: number
|
|
207
|
+
|
|
208
|
+
export declare function discoverPlugins(marketplaceDir: string): CoreDiscoveredPlugin[]
|
|
209
|
+
export declare function dirClashes(pluginDir: string): string[]
|
|
210
|
+
export declare function lintCrossTool(marketplaceDir: string): string[]
|
|
211
|
+
export declare function isGitRepo(dir: string): boolean
|
|
212
|
+
export declare function git(args: string[], cwd?: string): Promise<{ ok: boolean; stdout: string; stderr: string }>
|
|
213
|
+
export declare function pullRepo(dir: string, ref?: string | null): Promise<CorePullResult>
|
|
214
|
+
export declare function readRegistry(): CoreRegistry
|
|
215
|
+
export declare function pluginRootEnv(): Record<string, string>
|
|
216
|
+
export declare function normalizeRegistry(raw: unknown): CoreRegistry
|
|
217
|
+
export declare function loadRegistryForWrite(): { registry: CoreRegistry; wasV1: boolean }
|
|
218
|
+
export declare function saveRegistry(registry: CoreRegistry): void
|
|
219
|
+
export declare function materialize(
|
|
220
|
+
name: string,
|
|
221
|
+
dir: string,
|
|
222
|
+
options?: { enabled?: Set<string> | null; force?: boolean; plugin?: string },
|
|
223
|
+
): CoreMaterializeReport
|
|
224
|
+
export declare function enabledPlugins(entry: unknown, dir: string): Set<string> | null
|
|
225
|
+
export declare function setSkillsPath(skillsDir: string, present: boolean): string | null
|
|
226
|
+
export declare function removeLinksFor(name: string, marketplaceDir: string): void
|
|
227
|
+
export declare function executableComponents(dir: string, entry: unknown): CoreExecutableComponent[]
|
|
228
|
+
export declare function trustFingerprint(components: CoreExecutableComponent[]): string
|
|
229
|
+
export declare function grantEntry(entry: CoreMarketplaceEntry, components: CoreExecutableComponent[]): void
|
|
230
|
+
export declare function denyEntry(entry: CoreMarketplaceEntry): void
|
|
231
|
+
export declare function syncAll(options?: {
|
|
232
|
+
force?: boolean
|
|
233
|
+
reason?: string
|
|
234
|
+
}): Promise<CoreSyncResult>
|
|
235
|
+
export declare function isGitUrl(source: string): boolean
|
|
236
|
+
export declare function manifestName(name: string | undefined): string | undefined
|
|
237
|
+
export declare function normaliseMarketplaceName(name: string): string
|
|
238
|
+
export declare function marketplaceNameFromUrl(url: string): string
|
|
239
|
+
export declare function parseSource(source: string): CoreParsedSource
|
|
240
|
+
export declare function discoverMarketplace(marketplaceDir: string): CoreDiscoveredMarketplace
|
|
241
|
+
export declare function discoveryError(plugins: CoreManifestPlugin[]): string | null
|
|
242
|
+
export declare function nameDisagreement(pluginDir: string, pluginName: string): string | null
|
|
243
|
+
export declare function readManifest(marketplaceDir: string): { name?: string; description?: string }
|
|
244
|
+
export declare function componentRoot(entry: CoreMarketplaceEntry): string
|
|
245
|
+
export declare function incumbentMarketplace(registry: CoreRegistry, self: string, pluginName: string): string | undefined
|
|
246
|
+
export declare function registerPlugins(registry: CoreRegistry, name: string, plugins: CoreManifestPlugin[]): void
|
|
247
|
+
export declare function addMarketplace(source: string, options?: CoreAddOptions): Promise<CoreAddResult>
|
|
248
|
+
export declare function removeMarketplace(name: string): CoreRemoveResult
|
|
249
|
+
export declare function pinMarketplace(name: string, ref?: string | null): Promise<CorePinResult>
|
|
250
|
+
export declare function resolvePlugin(registry: CoreRegistry, arg: string): CoreResolvedPlugin
|
|
251
|
+
export declare function setEnabled(arg: string, enabled: boolean, options?: { force?: boolean }): CoreSetEnabledResult
|
|
252
|
+
export declare function grantTrust(name: string): CoreGrantResult
|
|
253
|
+
export declare function denyTrust(name: string): CoreDenyResult
|
|
254
|
+
export declare function revokeTrust(name: string): CoreDenyResult
|
|
255
|
+
export declare function removeMcpKeys(pluginNames: string[]): string | null
|
|
256
|
+
export declare function searchPlugins(query: string, options?: { enabledOnly?: boolean }): CoreSearchMatch[]
|
package/loader/core.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Public surface of the ocm runtime core. Consumers (the startup loader, the
|
|
2
|
+
// TUI plugin, and src/ via core.d.ts) import this module; the implementation
|
|
3
|
+
// lives in the siblings it re-exports. Siblings never import core.js — shared
|
|
4
|
+
// constants come from paths.js, so no module's top level depends on this one.
|
|
5
|
+
export {
|
|
6
|
+
CACHE_DIR,
|
|
7
|
+
DEFAULT_SYNC_INTERVAL_MS,
|
|
8
|
+
HOME,
|
|
9
|
+
LEGACY_REGISTRY_FILE,
|
|
10
|
+
LINKS_DIR,
|
|
11
|
+
MARKETPLACES_DIR,
|
|
12
|
+
OCM_DIR,
|
|
13
|
+
OPENCODE_AGENTS_DIR,
|
|
14
|
+
OPENCODE_COMMANDS_DIR,
|
|
15
|
+
OPENCODE_CONFIG_FILE,
|
|
16
|
+
OPENCODE_DIR,
|
|
17
|
+
OPENCODE_PLUGINS_DIR,
|
|
18
|
+
REGISTRY_FILE,
|
|
19
|
+
STAMP_FILE,
|
|
20
|
+
} from "./paths.js"
|
|
21
|
+
export { dirClashes, discoverPlugins } from "./discovery.js"
|
|
22
|
+
export { lintCrossTool } from "./lint.js"
|
|
23
|
+
export { loadRegistryForWrite, normalizeRegistry, pluginRootEnv, readRegistry, saveRegistry } from "./registry.js"
|
|
24
|
+
export { enabledPlugins, materialize, removeLinksFor } from "./materialize.js"
|
|
25
|
+
export { setSkillsPath } from "./config.js"
|
|
26
|
+
export { git, isGitRepo, pullRepo, syncAll } from "./sync.js"
|
|
27
|
+
export { denyEntry, executableComponents, grantEntry, trustFingerprint } from "./trust.js"
|
|
28
|
+
export { isGitUrl, manifestName, marketplaceNameFromUrl, normaliseMarketplaceName, parseSource } from "./source.js"
|
|
29
|
+
export { discoveryError, discoverMarketplace, nameDisagreement, readManifest } from "./manifest.js"
|
|
30
|
+
export {
|
|
31
|
+
addMarketplace,
|
|
32
|
+
componentRoot,
|
|
33
|
+
incumbentMarketplace,
|
|
34
|
+
pinMarketplace,
|
|
35
|
+
registerPlugins,
|
|
36
|
+
removeMarketplace,
|
|
37
|
+
} from "./marketplace.js"
|
|
38
|
+
export { denyTrust, grantTrust, resolvePlugin, revokeTrust, setEnabled } from "./mutations.js"
|
|
39
|
+
export { removeMcpKeys } from "./mcp.js"
|
|
40
|
+
export { searchPlugins } from "./search.js"
|