Cli versioned resource plugins#332
Conversation
Adapt the CLI to the versioned-resource-plugins API. Plugin references take the form name[@<semver>] everywhere the user types or sees a plugin: - YAML loader splits the top-level name on '@' and sets both Name and Version on the request body. - apply detects 409 (ApplyResourcePluginConflict) and reports versions are immutable, prompting the user to bump the version field. - get / delete take NAME[@Version] positionally; concrete versions flow into the new WithVersion param. Omitted version resolves to latest on the server. - delete -f file also uses the version from the loaded YAML. - Table NAME column and details Name: line render the joined name@version form so the output grep-pastes back into other commands. - New `versions` subcommand calls ListResourcePluginVersions and reuses the same table printer; server returns versions highest-semver-first. - go-sdk pinned to a versioned-resource-plugins commit (pre-merge); will re-pin to the merged tag before opening the PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address review findings on the prior commit: - Accept the version as either a `name: foo@1.2.0` suffix or a separate top-level `version:` field, since the wire model exposes them separately and users coming from the SDK/Notion design will reach for the top-level form. Reject if the version is supplied in both places (even when the values agree) so the spec has a single source of truth. - Type-check the top-level `version:` so a YAML float (e.g. `version: 1.0`) fails loudly rather than being silently dropped. - Preserve `version` through `utils.extractName` so `delete -f file` picks up the top-level form too (the `@version` suffix already survived because it travels inside the `name` value). - Sharpen apply's user-facing messages: the 409 conflict text now says "default version (0.0.0)" instead of `version ""` when the user never set one, and the success message always renders the effective `name@version` so the user sees what was actually published. Add focused tests for the loader's name/version resolution rules. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pick up the go-sdk regen against the symmetric-name swagger (sandboxes #28a6d0e9d). models.ResourcePlugin no longer carries a separate Version field; identity is a single `name` of the form "bareName[@semver]" both on input and on output. - subst.go: the YAML loader still accepts a name@version suffix or a top-level `version:` field (CLI ergonomics), and still rejects the two forms together. After resolution it joins them into the wire shape on rp.Name; rp.Version is gone. - apply.go: split rp.Name into a bare URL-path name and a version for the user-facing conflict message and the "Created …" line. - delete.go: when --filename is used, split rp.Name to recover the version that flows into the ?version= query param. - printers.go: the table NAME column and details Name line just use rp.Name now — the server already returns it in joined form. - SKILL.md: clarify that the wire identity is the combined `name` field; the top-level `version:` is CLI YAML sugar only and never appears in server-returned JSON/YAML. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Now that the wire identity is a single name field, supporting two YAML forms for the same thing widens the input space beyond what get -o yaml produces. The CLI only accepts the suffix form (name: foo@1.2.0) — a top-level version: field is silently ignored, like any other unknown top-level field. - Drop the optionalStringField helper and the XOR check from the YAML loader; rp.Name now comes straight from the YAML name: value (already the wire form). - Trim the related test cases; keep the bare-name and name-suffix ones. - Drop version from utils.extractName's keep-list for delete-by-file — name alone carries the identity now. - SKILL.md: drop the 'Equivalent form' block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pick up the server-side bare-default change (sandboxes #b1faf65f5) and mirror its display rule on the CLI side. The post-success message after 'apply' for a plugin published at the default version now reads 'Created resource plugin foo', matching the bare 'name: foo' the server will return on subsequent GETs and the pre-versioning CLI output. The @semver suffix continues to appear for any concretely-versioned plugin. Drop the effectiveNameRef helper that explicitly rendered '@0.0.0'; formatNameRef (bare when version is empty, joined otherwise) is the single display rule everywhere now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest push (3 commits,
The CLI commits in this push correspond 1:1 to the three layers of change:
Net: this PR no longer adds any local string-splitting "cleverness" to paper over a server asymmetry — the server now exposes the same shape the CLI always wanted. |
IMO this is verbose, we are repeating the same word over an over ( Also the |
Match sandbox/routegroup/planrunnergroup, which all render the CREATED
column via timeago ("3 minutes ago") rather than the raw ISO timestamp.
Detail view is untouched — it already uses utils.FormatTimestamp.
This is a pre-existing inconsistency (predates versioning), but folded
into this PR since the user-visible `rp list` surface is being touched
anyway.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Yes but IMO it should be verbose because this is tabular output and hence both expected and easier to automate, which in my mind outweighs the repetition. wdyt in light of this?
This PR did not change the time, it was pre-existing problem. But, I agree it would be better to be aligned, so I've added that in 5577eba. |
|
Maybe we can do a regular version of the command that just lists the plugin name and latest version, and an --all-versions flag that can list the full expanded table (for using with CLI automation)? |
|
Can be a flag, but not --all-version cause we are already showing all the versions. I think is more --full-version |
I was saying by default when listed, show just the latest version (1 entry per plugin), and then |
To me, it is not a question of whether there exists an alternative which is more expensive and takes more care to craft. There is a long long history of tools outputting tables and using things like awk, cut, paste, sed, etc. IMO by far the gold standard for CLI tools outputting to stdout is for piping. And the example you give is a photo of a UI, not tabular text output for piping, it doesn't really address the issue IMO. It is worth noting that one could separate the version in a different column. The reason it's not like that now is to make the presentation of name[@Version] uniform everywhere. But, in any output which is a table/matrix, repetition in some columns at least is expected IMO. I am ok with @foxish's suggestion to only show the latest version unless a flag is provided, which would be another way to reduce the repetition across columns. I guess it might make it less ergonomic to navigate the version history, requiring a separate call to the versions subcommand, but the tradeoff seems reasonable to me. |
I think this is a good idea also, to have the xyz@vN identifier being shorthand but allowing the long form split representation for outputs. I don't feel strongly about this, but if it is not too brittle to split, it could be nicer. |
Address PR review feedback that the combined `name@version` token, while fine as a shorthand for input, is redundant when displayed row-by-row. - `rp list`: split into NAME | VERSION | CREATED | STATUS. A plugin published at the default version (bare wire name) renders as VERSION=0.0.0, explicit rather than blank, so the column has no ambiguous cells. - `rp versions NAME`: drop the NAME column entirely. The caller passed the name as an argument; repeating it on every row was noise. Output is still a plain table — no header line above it — so the shape stays uniform across subcommands. New row type and printer function avoid conditional column rendering. JSON / YAML output is unchanged (both still emit the combined `name` field exactly as the server returns it). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Please see last commit regarding the listing /versions. Upon doing that, I realised I (or we) had conflated the rp list with the rp versions list through the discussion. In fact rp list was already latest only, and the repeated text example was from the Last commit
wdyt? |
…rnings) Address findings #3, #4, #5, #6 from the versioned-resource-plugins UX pass. #4 — apply immutable-version error: the previous code matched on *ApplyResourcePluginConflict, but the go-sdk's transport middleware (FixAPIErrors) intercepts every 4xx/5xx response before the per-endpoint typed reader runs, so that type is never in the error chain and the catch-and-rewrite was dead code. Users saw the generic '409 Conflict: plugin version already exists'. Switch to errors.As against *transport.APIError + Code==409 (the same pattern used in planexec/logs). Also #11 — branch the message text on the bare-name vs. concrete-version case: a bare-name author has no '@semver' suffix to bump, so the 'bump the version field' advice was a dead end; now they get a concrete 'add an @<semver> suffix to publish a new revision (e.g. my-plugin@0.0.1)'. #5 — apply success line: previously stayed silent on the version for bare-name authors ('Created resource plugin my-plugin') while explicit-version authors saw '@0.0.1'. The asymmetry hid the versioning rule from exactly the population that most needed to learn it. Always echo the explicit version on the apply success line (rendering '@0.0.0' for bare publishes). The list/table/get-yaml output continues to bare the default version for backward compatibility with pre-versioning clients — this one message is explicit on purpose, as a teaching moment. #3 — bare-form delete: previously silently removed only the latest version with no indication that others remained, a footgun for users who assumed 'delete my-plugin' meant 'delete the plugin'. Resolve the latest version explicitly before the delete (one List call), then report which version was removed and how many remain, e.g. 'Deleted resource plugin my-plugin@0.0.1. 1 other version(s) remain (use 'signadot resourceplugin versions my-plugin' to list).' #6 — delete-in-use: previously surfaced the server's generic '400 Bad Request: plugin is currently in use' with no hint about which sandbox(es) were holding the version. Enrich client-side: on that specific 400 we GET the plugin and surface Status.Resources sandbox names: 'resource plugin foo@1.2.0 is still referenced by sandbox(es): a, b'. Falls back to the original error if the enrichment can't be done. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed #4 apply immutable-version error wording. The previous Folded in finding #11 while we're here: bare-name authors got the dead-end advice "bump the version field" they didn't have. The branch now emits a concrete #5 apply success silent on version for bare-name authors. Was emitting #3 bare-form delete silently removes only the latest. Now resolves the latest version up-front (one List call), deletes it explicitly, then reports both what was removed and how many versions remain: Small race remains (someone publishes a newer version between the List and the Delete); we still delete the version we resolved, which matches the user's mental model of "delete whatever was latest when I asked". #6 delete-in-use doesn't name holding sandboxes. Server's generic Falls back to the original error if the enrichment GET fails.
|
…lugin Address findings #8 and #13 from the versioned-resource-plugins UX pass. #8 — versions --help was one short line with no example. Expand the short / long / example block to spell out: that the NAME argument is the bare name (no @semver suffix); that 'get NAME@VERSION' is the way to fetch a single version's spec; and that the output is sorted highest-semver first. #13 — versions <nonexistent> was returning exit 0 with empty rows, because the server replies 200 with an empty payload for an unknown name (it has no equivalent of the 404 'get' returns). A resource plugin always has at least one version once it exists — publishing creates the (name, version) row — so an empty list reliably means 'no such plugin'. Surface that as an error instead of silently exiting 0, matching the behavior of 'resourceplugin get <nonexistent>'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pick up the server-side ?version=all support from signadot/signadot#7025 (go-sdk regen on the versioned-resource-plugins branch) and expose it via two changes to 'rp list': - New --all-versions / -A flag. When set, the CLI passes ?version=all to the list endpoint and the table renders one row per (name, version), sorted by name then semver-descending. The existing NAME / VERSION column split (c67307a) makes the expanded output naturally scannable. - When --all-versions is NOT set (i.e. the default latest-only list), a cheap second list-with-?version=all is issued to count how many bare names show up more than once. If any do, a footer hint appears under the table: (3 plugins have multiple versions — pass --all-versions to expand, or 'resourceplugin versions NAME') This addresses finding #7 from the UX pass: the default list previously gave the user no indication that a row might be one of many for that name. The hint is supplementary — errors fetching it are silently dropped, so the exit status reflects only the primary list call. The hint is skipped under JSON/YAML output (those callers can compute it themselves from the all-versions payload if they care). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed
Multi-version hint on the default list. Addresses finding #7. When the user runs the plain The hint is supplementary — errors fetching it are silently dropped, so the exit status reflects only the primary list call. The hint is skipped under JSON/YAML output (those callers can compute it from the all-versions payload if they need to).
|

This PR addresses signadot/community#116
What changes for users
Plugin references take the form
name[@<semver>]everywhere a user types or sees a plugin reference:In YAML, the version travels with
name:as an@<semver>suffix — there is no separateversion:field. A bare name publishes the plugin at the default version (0.0.0):A stray top-level
version:field is silently ignored, like any other unknown top-level field.What
get/listlook likeFor plugins published at the default version,
get -o yamlreturns the barename: my-plugin(no@0.0.0suffix), matching the pre-versioning CLI surface byte-for-byte. For any concretely-versioned plugin, the response carries the version in thenamefield as a single token.get -o yamlround-trips back intoapply -fwithout renaming.Server contract
namefield; the version is the optional@semversuffix.(name, version)— including the implicit default0.0.0— returns 409. The CLI maps that toresource plugin %q version %q already exists; versions are immutable — bump the version field to publish a new revision, and usesdefault version (0.0.0)when the user never set one.getanddeleteaccept?version=<v>or omit/?version=latestfor the highest semver.ListResourcePluginsreturns latest-only; per-plugin version listing is the newversionssubcommand.Test plan
go test ./...passes locally.@versionsuffix, and the now-ignored top-levelversion:field.--helpforresourceplugin,get,versionsrenders the newNAME[@VERSION]usage.name: my-plugin.name: foo@1.2.0, list, get by version, list versions, delete by version.Before merging
versioned-resource-pluginsbranch tip).