Skip to content

feat(status): resolve the effective version a session loads - #43

Merged
diegohb merged 7 commits into
mainfrom
openchamber/status-effective-version
Oct 9, 2026
Merged

diegohb merged 7 commits into
mainfrom
openchamber/status-effective-version

Conversation

@diegohb

@diegohb diegohb commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Summary

status now resolves and reports the effective version a session would load, per scope — rather than echoing the manifest version alone.

bunx opencode-architect status --online --path ../other --package some-pkg

ConfigEntriesReader      # raw plugin entries per config file (json/jsonc)
  LoadedVersionResolver  # entry → resolved source on disk
    EntrySpec           # classify: npm spec | path/file:// | { package } object
    NpmCache            # $XDG_CACHE_HOME/opencode/npm/<name>@<spec>
    PluginEntryResolver.packageRoot
  RegistryVersionChecker # --online: npm /latest, warn-and-continue, injectable fetch
→ per-scope line + effective verdict

Effective verdict (strongest source wins):

should load: 1.0.0 (cache copy, 2h old)          # resolved copy on disk
should load: 1.0.0 (pinned spec, local)          # exact semver, no copy cached
should load: 2.0.0 (npm latest)                  # @latest + --online
should load: 1.0.0 (manifest)                    # fallback
should load: nothing (not registered in any scope)

Key changes:

  • Per-scope line carries config file, raw entry as written, resolved source + relative age, npm-latest= under --online.
  • Double-scope annotates the verdict (local + global; both scopes register — double-load) and warns.
  • Manifest without registration reports unregistered — the report answers what would load, not what was once installed.
  • --online/--path are status-only (rejected elsewhere); --package applies to status/clear-cache; --path dir must exist.
  • Relative path entries resolve against the config file's directory (not cwd); name matching is case-insensitive on Windows.
  • Installer slimmed to install/uninstall/cache + manifestAt() seam; status()/StatusOutcome retired. Templates mirror suite (byte-sync test guards src ↔ templates).

Evidence

$ bun test
tests/status-effective-version.test.ts  → 502 lines: per-scope resolution, verdict sources, age hints, all warning cases
tests/cli.test.ts                       → status-only flag rejection, --path, --package
tests/templates.test.ts                 → src ↔ template byte-sync

Sample per-scope output:

local scope: mode=plugin config=/proj/.opencode/opencode.json entry=opencode-architect@latest resolved=1.0.0 (cache copy, 2h old) npm-latest=1.2.0
global scope: mode=none config=- entry=- resolved=-
should load: 1.0.0 (cache copy, local, npm-latest=1.2.0)
Warning: local: resolves 1.0.0 but npm publishes 1.2.0 (stale)

Merge Danger

Door: two-way — additive reporting, read-only; revert restores the old status output shape.

Blast Radius: moderate

  • Generated packages inherit the new resolution — templates/installer.template.txt ships status() with full resolution; the byte-sync test catches any src drift from templates.
  • Installer.status() removed; no in-repo callers remain after this change.
  • Installer.manifestAt() added as the new seam for external callers.
  • main was merged in (no rebase/squash): the branch now carries main's baseDir config-directory threading and case-insensitive plugin-name matching, applied to the status resolution as well. src/plugin-entry.ts stays self-contained and byte-identical to its template, per the byte-sync invariant.

diegohb added 7 commits October 8, 2026 20:52
Per scope, status now reports the config file holding the registration, the raw entry as written (npm spec, pinned, { package } object, or path/file:// form), and the resolved source on disk: an npm spec against the OpenCode package-cache key <name>@<spec> (version read from that copy's package.json, newest generation wins, mtime carried as a staleness hint), a path entry through PluginEntryResolver.packageRoot. The effective verdict annotates a double-scope registration, the resolved line carries a relative age, and warnings cover double-scope registration, manifest/resolved mismatch, missing or incomplete cached copies, unparseable configs, and --online registry staleness (warn-and-continue, fetch injectable). --online and --path are status-only and rejected elsewhere; --path validates and normalizes the directory.

Resolution lives in StatusReporter (src/status-reporter.ts) over LoadedVersionResolver, ConfigEntriesReader, and RegistryVersionChecker; Installer is install/uninstall/cache only and exposes manifestAt() as the ManifestLookup seam, retiring status()/StatusOutcome. Entry classification is unified in EntrySpec (src/entry-spec.ts).

A manifest without a live registration now reports unregistered: the report answers what a session would load, not what was once installed.
The generated installer's status() mirrors the suite's resolution: registration inventory from the registration detector, the raw entry as written, the resolved source on disk (package-cache key <name>@<spec> or path checkout via PluginEntryResolver.packageRoot), a relative-age staleness hint, an effective verdict annotating a double-scope registration, and --online registry staleness over an injectable fetch. The generated CLI renders the same shape and rejects the status-only --online/--path flags on every other command. A version segment carrying a path separator no longer produces a bogus cache lookup.

registration.template.txt gains the inventory() accessor alongside detect() (detection behavior unchanged), plugin-entry.template.txt exposes packageRoot(), and the load-bearing notes of all four templates now describe the status behavior they ship with.
README documents the per-scope resolution, the resolved line's age hint, the annotated double-load verdict, the warn-and-continue --online lookup, --path, and the status-only flag rejection. CHANGELOG gains the unreleased entry describing StatusReporter, the EntrySpec unification, and the manifestAt() seam that replaces the retired Installer.status(). Packager and publisher instructions describe the generated status behavior they now ship.
… entries

--online results were easy to miss: the published version appeared only as a trailing ', latest X' on the verdict, and a registration whose cache copy could not be resolved printed 'unknown (manifest, ...)' — claiming a manifest source for a version nothing provided. The published latest now prints per scope as npm-latest=<version>, and the verdict labels an unresolvable load 'unresolved' instead of 'manifest'. Path-form registrations had no npm name at all, so --online was silent for them; the name now derives from the resolved checkout's package.json.

Reported from bunx opencode-architect status --online --path <aurelia-expert checkout>, where the pinned @1.0.0 cache key was partial and the npm result was invisible.
… when no copy is cached

A pinned entry like opencode-architect@1.0.0 with no cached copy and no manifest produced 'should load: unknown' even though the spec itself names the version the host will fetch at next start. The verdict now answers from the strongest source available: a resolved copy on disk (cache copy / checkout copy), else the spec (a pinned exact semver answers as 'pinned spec'; @latest answers with the published version under --online as 'npm latest'; ranges stay unresolved), else the manifest version. --online runs before the verdict is computed so the published version can feed it.

Reported against bunx opencode-architect status --online --path <aurelia-expert checkout>.
@diegohb
diegohb merged commit 71fe3b8 into main Oct 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant