@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.
Files changed (64) hide show
  1. package/README.md +286 -0
  2. package/bin/ocm.ts +7 -0
  3. package/loader/config.js +47 -0
  4. package/loader/core.d.ts +256 -0
  5. package/loader/core.js +40 -0
  6. package/loader/discovery.js +162 -0
  7. package/loader/links.js +193 -0
  8. package/loader/lint.js +83 -0
  9. package/loader/manifest.js +158 -0
  10. package/loader/marketplace.js +199 -0
  11. package/loader/materialize.js +218 -0
  12. package/loader/mcp.js +124 -0
  13. package/loader/mutations.js +102 -0
  14. package/loader/ocm-loader.js +21 -0
  15. package/loader/paths.js +20 -0
  16. package/loader/registry.js +163 -0
  17. package/loader/search.js +55 -0
  18. package/loader/source.js +100 -0
  19. package/loader/sync.js +151 -0
  20. package/loader/trust.js +114 -0
  21. package/loader/ui-dialog.js +46 -0
  22. package/loader/ui-marketplaces.js +187 -0
  23. package/loader/ui-plugins.js +101 -0
  24. package/loader/ui-trust.js +68 -0
  25. package/loader/ui.js +109 -0
  26. package/package.json +32 -0
  27. package/src/commands/doctor-config.ts +79 -0
  28. package/src/commands/doctor-links.ts +170 -0
  29. package/src/commands/doctor.ts +144 -0
  30. package/src/commands/info.ts +159 -0
  31. package/src/commands/list.ts +44 -0
  32. package/src/commands/marketplace.ts +99 -0
  33. package/src/commands/plugins.ts +127 -0
  34. package/src/commands/search.ts +86 -0
  35. package/src/commands/trust.ts +146 -0
  36. package/src/commands/update-report.ts +123 -0
  37. package/src/commands/update.ts +160 -0
  38. package/src/commands/validate-files.ts +145 -0
  39. package/src/commands/validate.ts +77 -0
  40. package/src/discovery.ts +36 -0
  41. package/src/findings.ts +33 -0
  42. package/src/git.ts +24 -0
  43. package/src/index.ts +175 -0
  44. package/src/install.ts +15 -0
  45. package/src/loader.ts +185 -0
  46. package/src/manifest-lint.ts +189 -0
  47. package/src/migrate.ts +117 -0
  48. package/src/paths.ts +22 -0
  49. package/src/probe.ts +52 -0
  50. package/src/registry.ts +25 -0
  51. package/src/renames.ts +83 -0
  52. package/src/report.ts +13 -0
  53. package/src/types.ts +70 -0
  54. package/template/marketplace.json +17 -0
  55. package/template/plugins/demo-kit/agents/reviewer.md +20 -0
  56. package/template/plugins/demo-kit/commands/tdd.md +12 -0
  57. package/template/plugins/demo-kit/mcp.json +3 -0
  58. package/template/plugins/demo-kit/plugin/notify.js +4 -0
  59. package/template/plugins/demo-kit/plugin.json +6 -0
  60. package/template/plugins/demo-kit/skills/code-review/SKILL.md +21 -0
  61. package/template/plugins/release-kit/commands/ship.md +11 -0
  62. package/template/plugins/release-kit/commands.claude/ship.md +11 -0
  63. package/template/plugins/release-kit/plugin.json +6 -0
  64. 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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bun
2
+ import { main } from "../src/index"
3
+
4
+ main(process.argv.slice(2)).catch((err) => {
5
+ console.error(err instanceof Error ? err.message : String(err))
6
+ process.exit(1)
7
+ })
@@ -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
+ }
@@ -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"