Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `status` now resolves the **effective version** a session would load, per scope, instead of echoing the manifest version. For each scope it reports the config file holding the registration, the raw entry as written (`name@latest`, a pinned spec, a `{ package }` object, or a path/`file://` form), and the resolved source on disk — an npm spec to OpenCode's package cache (`$XDG_CACHE_HOME/opencode/npm`, falling back to `~/.cache/opencode/npm`) with the version read from that copy's `package.json` and the copy's age as a staleness hint, a path entry to the package checkout via `PluginEntryResolver.packageRoot` — and closes with an effective verdict that annotates a double-scope registration (`should load: 1.0.0 (cache copy, local + global; both scopes register — double-load)`) plus warnings for that double-load, a manifest/resolved version mismatch, a missing or incomplete cached copy, and an unparseable config. The verdict answers from the strongest source available: a resolved copy on disk, else the registration spec (a pinned exact version answers directly; `@latest` answers with the published version under `--online`; ranges stay `unresolved`), else the manifest version. A manifest without a live registration reports unregistered: the report answers what would load, not what was once installed. New `--online` queries the npm registry for every registered package — the name comes from the entry, or from the checkout's `package.json` for path-form registrations — and prints the published latest per scope (`npm-latest=<version>`, also `, latest <version>` on the verdict), flagging staleness with warn-and-continue on network failure. New `--path <dir>` resolves against another project directory; both flags are status-only and rejected elsewhere. `status` remains read-only. Resolution lives in the new `StatusReporter` (`src/status-reporter.ts`) over `src/loaded-version.ts`, `src/config-entries.ts`, and `src/registry-version-checker.ts`, with entry classification unified in `src/entry-spec.ts`; `Installer` is install/uninstall/cache only and exposes `manifestAt()` in place of the retired `status()`/`StatusOutcome`. The generated-package templates (`registration`, `plugin-entry`, `installer`, `cli`) gain the same resolution so packaged extensions inherit it.

- Path-form plugin-entry recognition (the `aurelia-expert@db0736e` lesson, issue #3): the new `src/plugin-entry.ts` and `templates/plugin-entry.template.txt` resolve a `file://` URL or filesystem config entry to a package by `package.json` name. Registration-scope detection (`templates/registration.template.txt`) and the surgical config editor (`src/plugin-config.ts`, `templates/plugin-config.template.txt`) now count a live path-form entry as registered, so installation never adds a duplicate `name@latest` entry beside it. The packager and publisher template maps gained `src/plugin-entry.ts`, and conformance-checklist items B2 and C1 gained the rule.

### Changed
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ bunx opencode-architect install --scope global
Useful flags and commands:

```bash
bunx opencode-architect status # show mode, version, and the config file holding the entry
bunx opencode-architect status # resolve what a session would load, per scope, plus an effective verdict
bunx opencode-architect status --scope global # narrow the resolution to one scope
bunx opencode-architect status --online # also query the npm registry and flag a stale resolved copy
bunx opencode-architect status --path ../other # resolve against another project directory
bunx opencode-architect status --package other # resolve another installed package by name
bunx opencode-architect uninstall # remove the plugin entry, the manifest, and any residual payload
bunx opencode-architect clear-cache # remove cached copies of this package from OpenCode's package cache
bunx opencode-architect clear-cache --all --yes # remove the whole OpenCode cache directory (destructive)
Expand All @@ -45,6 +49,8 @@ bunx opencode-architect install --force # re-register and rewrite the ma
bunx opencode-architect --help # full usage
```

`status` does not report the manifest version alone: for each scope it resolves what a session would actually load. It prints the config file that holds the registration, the raw entry as written (`name@latest`, a pinned spec, a `{ package }` object, or a path/`file://` form), and the resolved source on disk — an npm spec resolves to OpenCode's package cache (`$XDG_CACHE_HOME/opencode/npm`, falling back to `~/.cache/opencode/npm`), a path entry to the package checkout — with the version read from that copy's `package.json` and the copy's age as a staleness hint (`resolved=1.0.0 (cache copy, 2h old)`). It closes with an effective verdict and warns when a package is registered in both scopes — the verdict annotates the double-load (`should load: 1.0.0 (cache copy, local + global; both scopes register — double-load)`) — when the manifest version disagrees with the resolved copy, when the cached copy for a spec is missing or incomplete, or when a config file cannot be parsed. The verdict answers from the strongest source it can: a resolved copy on disk (`cache copy` / `checkout copy`), else the registration spec itself — a pinned exact version answers directly (`should load: 1.0.0 (pinned spec, local)`, since the host fetches exactly that at next start) and `@latest` answers with the published version under `--online` (`npm latest`); a range like `^1.7.0` stays `unresolved` until a copy exists, and only then falls back to the manifest version. `status` is read-only. Without `--online` it never touches the network; `--online` queries the npm registry for every registered package — the name comes from the entry, or from the checkout's `package.json` for path-form registrations — and prints it per scope (`npm-latest=1.0.0`) plus `, latest <version>` on the verdict, flagging staleness; network failures warn rather than fail. `--path <dir>` resolves against another project directory (the directory must exist), so a package installed elsewhere can be checked without `cd`-ing into it. `--package <name>` resolves `<name>` instead of `opencode-architect`: it matches a plugins entry or a path checkout whose `package.json` name is `<name>`, reads that scope's `<name>.manifest.json` (or the legacy `<name>.json`), and, under `--online`, queries the registry for `<name>` — so a package installed from another repo can be inspected in place. `--online` and `--path` are status-only; `--package` applies to `status` and `clear-cache` only; any other command rejects them with an explanatory error.

Re-running install when the manifest matches reality is a zero-write no-op. `--mode copy` is refused with an explanatory error: this package is code-backed (it ships agents), and copying cannot express plugin registration.

Every install — including a no-op — also clears this package's stale cache keys from OpenCode's plugin cache (`$XDG_CACHE_HOME/opencode/npm`, falling back to `~/.cache/opencode/npm`): `opencode-architect`, `opencode-architect@latest`, and `opencode-architect@<installed version>` — each key holding every cached generation beneath it. Pinned keys like `opencode-architect@0.6.0` and other packages' cache keys are left untouched. This makes OpenCode re-fetch the just-installed version on next start instead of reusing a stale or partial extraction. Removal is best-effort: a failure prints a warning but the install still succeeds.
Expand Down
2 changes: 1 addition & 1 deletion agents/opencode-packager.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ opencode-myextension/
└── tsconfig.json
```

6. **Create src/plugin.ts** from `../templates/plugin-local.template.txt`, plus `src/plugin-name.ts` from `../templates/plugin-name.template.txt`, `src/manifest.ts` from `../templates/manifest.template.txt`, `src/registration.ts` from `../templates/registration.template.txt`, `src/plugin-config.ts` from `../templates/plugin-config.template.txt`, `src/plugin-entry.ts` from `../templates/plugin-entry.template.txt`, `src/installer.ts` from `../templates/installer.template.txt` (src/plugin.ts imports it), and `src/cli.ts` from `../templates/cli.template.txt`: the load hook performs read-only registration-scope detection, then ensures skills, commands, and agents only for the scopes where the plugin is registered, gated by the per-scope install manifest (version, mode, registration, per-file sha256). The plugin-entry module resolves path-form config entries (`file://` URL or filesystem path) to this package, so a path-registered package counts as registered and installation never adds a duplicate `name@latest` entry beside it (aurelia-expert issue #3). Never write outside the detected scopes, never edit `plugins` arrays or root configs at load, and never rewrite a config that fails to parse. The install logic is content-based (ADR-0008): the same installer serves both the global scope base and the project scope base with identical behavior — copy install touches no config, plugin registration goes through the surgical editor, and code-backed packages refuse `--mode copy`. Cache hygiene is self-scoped (checklist A6): the installer template prunes this package's own cache copies on every install (warn-and-continue, other packages untouched), and the hook's failure advisory tells consumers to run `bunx <package> clear-cache` — the hook itself never deletes cache entries.
6. **Create src/plugin.ts** from `../templates/plugin-local.template.txt`, plus `src/plugin-name.ts` from `../templates/plugin-name.template.txt`, `src/manifest.ts` from `../templates/manifest.template.txt`, `src/registration.ts` from `../templates/registration.template.txt`, `src/plugin-config.ts` from `../templates/plugin-config.template.txt`, `src/plugin-entry.ts` from `../templates/plugin-entry.template.txt`, `src/installer.ts` from `../templates/installer.template.txt` (src/plugin.ts imports it), and `src/cli.ts` from `../templates/cli.template.txt`: the load hook performs read-only registration-scope detection, then ensures skills, commands, and agents only for the scopes where the plugin is registered, gated by the per-scope install manifest (version, mode, registration, per-file sha256). The plugin-entry module resolves path-form config entries (`file://` URL or filesystem path) to this package, so a path-registered package counts as registered and installation never adds a duplicate `name@latest` entry beside it (aurelia-expert issue #3). Never write outside the detected scopes, never edit `plugins` arrays or root configs at load, and never rewrite a config that fails to parse. The install logic is content-based (ADR-0008): the same installer serves both the global scope base and the project scope base with identical behavior — copy install touches no config, plugin registration goes through the surgical editor, and code-backed packages refuse `--mode copy`. Cache hygiene is self-scoped (checklist A6): the installer template prunes this package's own cache copies on every install (warn-and-continue, other packages untouched), and the hook's failure advisory tells consumers to run `bunx <package> clear-cache` — the hook itself never deletes cache entries. The generated `status()` resolves the effective load per scope — registration, target config file, raw entry as written, resolved source on disk (npm package cache copy or path checkout, with the copy's age as a staleness hint), and an effective verdict that annotates a double-scope registration — with `--online` registry staleness (warn-and-continue, injectable fetch) and `--path <dir>`, both rejected outside status, so packaged extensions inherit the suite's status behavior.

7. **Create package.json** from `../templates/package-basics.template.json` and tsconfig.json from `../templates/tsconfig.template.json`. The template declares `exports` with both `.` and `"./server": "./index.ts"` — keep the "./server" export: v2 resolves the plugin entrypoint through it on every observed runtime, and the root-index fallback is runtime-dependent and must not be relied on (per opencode-v2-facts §7, §13 row 9, §14.6). Keep `"@opencode/plugin": "latest"` and `"effect": "latest"` — never pin consumer dependency versions (per opencode-v2-facts §15). The template ships with `"content": "assets"`; the asset inventory is the source of truth for the declaration (ADR-0008): keep `"assets"` only when the package ships skills and/or commands alone; set `"code"` when the source contained any agent, tool, plugin, hook, or other plugin integration — code-backed packages may also ship skills and commands. The `files` list names the content directories actually shipped at the package root (`skills`, `commands`, plus `agents`, `plugins`, `tools`, `src` as present) — never an `assets` entry. The `bin` entry (`"<package name>": "src/cli.ts"`) and the `check`/`test` scripts ship at packager time: every load-hook advisory and README instruction says `bunx <package> ...`, so the entry point must resolve before any publishing step. Report the decision: "Content declaration: code (1 agent, 2 tools found)" or "Content declaration: assets (skills and commands only)".

Expand Down
2 changes: 1 addition & 1 deletion agents/opencode-publisher.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ You are an OpenCode extension publisher: you transform locally-packaged extensio

2. **Extract install logic to src/installer.ts.** Move install(), uninstall(), status(), scope detection, path resolution, and config management out of src/plugin.ts, keeping the manifest module (src/manifest.ts), plugin-name normalizer (src/plugin-name.ts), registration detector (src/registration.ts), and surgical config editor (src/plugin-config.ts) as separate files; update src/plugin.ts to call install() from src/installer.ts. Preserve the content-based deployment plan (ADR-0008): the installer reads the `content` declaration — assets-only packages copy-install by default (copy touches no config file) with `--mode plugin` as the opt-in; code-backed packages always register and `--mode copy` is a hard error. Preserve the invariants: manifest-gated idempotency (no `.version` markers; the plugin-mode no-op includes a present entry), semantic `@latest` plugin dedup written canonically as `name@latest` via the surgical editor only, abort-with-warning on unparseable config (never rewrite from `{}`), skip consumer-modified files unless `--force`, and root-config migration CLI-only behind explicit consent. Preserve the self-scoped cache hygiene (checklist A6): install() prunes the package's own cache copies (`<package>`, `<package>@latest`, `<package>@<version>`) on every invocation — including no-ops — best-effort with warn-and-continue, and clearPackageCache() backs the CLI's `clear-cache` subcommand.

3. **Create the CLI entry point.** Build src/cli.ts from `../templates/cli.template.txt`: install command calls install(scope, projectDir, { mode, force }) — it resolves the mode from the content declaration and surfaces `CopyModeUnsupportedError` as an explanatory exit-1 error — uninstall calls uninstall(scope, projectDir), status calls status(projectDir) and reports mode, version, entry, and the target config file per scope, migrate calls migrateRootConfig only behind `--force` consent, and clear-cache calls clearPackageCache() — strictly self-only (this package's own cache copies, warn-and-continue, idempotent); it must not offer `--package` or `--all` modes, which exist only in the suite's own CLI.
3. **Create the CLI entry point.** Build src/cli.ts from `../templates/cli.template.txt`: install command calls install(scope, projectDir, { mode, force }) — it resolves the mode from the content declaration and surfaces `CopyModeUnsupportedError` as an explanatory exit-1 error — uninstall calls uninstall(scope, projectDir), status calls status(projectDir, online) and resolves the effective load per scope — the registration, the target config file, the raw entry as written, the resolved source on disk (npm package cache copy or path checkout, with the copy's age as a staleness hint), and an effective verdict that annotates a double-scope registration — with `--online` registry staleness (warn-and-continue, injectable fetch) and `--path <dir>` for another project directory, both rejected outside status — migrate calls migrateRootConfig only behind `--force` consent, and clear-cache calls clearPackageCache() — strictly self-only (this package's own cache copies, warn-and-continue, idempotent); it must not offer `--package` or `--all` modes, which exist only in the suite's own CLI.

4. **Expand package.json** from `../templates/package-full.template.json`: bin field for the CLI, scripts (check, test), expanded dependencies, npm fields (repository, bugs, license, author). The `exports` map keeps `"./server": "./index.ts"` — every distributed package must declare it; the root-index fallback is runtime-dependent and must not be relied on (per opencode-v2-facts §7, §13 row 9, §14.6). `"@opencode/plugin": "latest"` and `"effect": "latest"` stay unpinned (per opencode-v2-facts §15). Carry the packager's `content` declaration through unchanged — expansion adds npm fields, never alters the declaration.

Expand Down
Loading
Loading