Skip to content

feat: create and maintain plugin-config.json from sdk publish and plugin generate - #318

Open
MuHamza30 wants to merge 6 commits into
devfrom
hamza/plugin-config-bootstrap
Open

feat: create and maintain plugin-config.json from sdk publish and plugin generate#318
MuHamza30 wants to merge 6 commits into
devfrom
hamza/plugin-config-bootstrap

Conversation

@MuHamza30

@MuHamza30 MuHamza30 commented Aug 13, 2026

Copy link
Copy Markdown

Follows #314, which added apimatic plugin generate for the happy path only — it assumed src/plugin-config.json already existed and named at least one language. This adds everything that creates and maintains that file.

The split

sdk publish records languages. plugin generate owns metadata. Publishing never asks for a plugin ID and never calls the account API, so a project can be published from long before anyone decides to build a context plugin.

  • sdk publish — after a successful publish, records the published language. Interactive asks; non-interactive answers with --update-plugin-config, because a prompt on the CI path would never be answered.
  • plugin generate — prompts for plugin ID / name / version when they are missing, then generates; or stops with next steps when no language has been recorded yet.

What gets written

{
  "schemaVersion": 1,
  "pluginId": "acme-payments",          // metadata: plugin generate only
  "pluginName": "Acme Payments",
  "pluginVersion": "0.1.0",
  "author": { "name": "Acme", "email": "developers@acme.com" },
  "license": "MIT",                     // written unprompted; the backend consumes it
  "languages": {                        // sdk publish only
    "csharp": {
      "source":  { "repositoryUrl": "https://github.com/acme/acme-payments-csharp", "branch": "main" },
      "package": { "packageId": "Acme.Payments.Sdk" },
      "version": "v3"                   // the codegen version the publish actually used
    }
  }
}

Package shape is per language, mapped from the publishing profile: csharp: packageId · typescript|python|ruby: name · java: groupId+artifactId · php: vendorName+projectName · go: packageName.

pluginKey is deliberately never written — codegen-v2 parses and validates it, but SdkReposExtensions.ResolveAgainst never reads it. A read-modify-write round-trip preserves fields this CLI version does not model, and backfills schemaVersion when a hand-written file omits it.

plugin generate states

Config Behaviour
unusable (unparseable, or a schemaVersion this CLI does not model) error naming the file and reason, failed()
missing / no metadata prompt for metadata, then the language check below
no languages next steps, success(), no generation
metadata + languages generate

An entry only ever claims what was published

Three cases where the CLI deliberately records nothing rather than record something untrue — each says so rather than exiting quietly:

  • A dry run. --dry-run publishes nothing but returns success, so --update-plugin-config is ignored with a notice.
  • A half not published. The entry is built from the run's --publish-type, not from whatever the profile enables — so a package-only run records no source, and a source-only run records no package.
  • A repository name that cannot be resolved. A publishing profile carries no git host (GitConfigurationItem has only credentialsId, repositoryName, branch), so only an absolute http(s) URL or a plain owner/repo can be turned into a URL. Anything else — an SSH remote, a bare single-segment name — is refused and reported with the offending value, rather than guessed at. This is the design decision most worth a second opinion: the backend validates only that repositoryUrl is non-blank, so a wrong guess would survive to a failed clone, but the guard also means a profile shape we have not seen in the wild records nothing.

Notes for review

  • --update-plugin-config is non-interactive only. sdk publish treats any argument as non-interactive (this.argv.length === 0), so the flag cannot be combined with the interactive flow; interactive asks instead.
  • Recording can never fail a publish. A write fault is reported, not thrown — the publish that already pushed the package must not become a stack trace because a config file was locked or the disk was full.
  • license: "MIT" is written unprompted, and re-added if a user removes it. Flagging rather than deciding.
  • A run with no languages exits 0 having generated nothing. Also flagging rather than deciding — failed() would make the no-op detectable by a script.

Known follow-ups

  • README is not regenerated — --update-plugin-config is undocumented. pnpm readme duplicates every section on a CRLF checkout, so this needs care.
  • The read side does not verify that an existing language entry carries a source; a malformed hand-written config is caught by the backend rather than locally. isLanguageEntry was written for that check and is currently unused.
  • Layering: the publishTypes gating sits in PluginRecordSdkAction while the rest of the entry's shape is decided in buildLanguageEntry — one rule in two places. And buildLanguageEntry is a pure transformation living in src/types/ rather than src/application/.
  • PluginRecordSdkAction.execute returns void rather than ActionResult, so that recording cannot alter the publish result. Callers ignoring the result would be the better shape.
  • The publish → record wiring has no test seam: actions declare execute as an arrow-function field, so it cannot be stubbed on the prototype.
  • plugin-config.json appears as a literal in several prompts classes rather than one shared constant.

Testing

pnpm build and pnpm lint are clean. 194 passing, 11 pending — excluding test/prompts/sdk/generate.test.ts and test/prompts/sdk/publish/publishing-details.test.ts, which call CodegenOption.resolve and abort the suite. That breakage is pre-existing on dev from #317 — the method is named create — and is not introduced here.

🤖 Generated with Claude Code

MuHamza30 and others added 5 commits August 13, 2026 18:01
The file is written by two commands with different jobs: `sdk publish` records
languages, `plugin generate` owns the plugin's own identity. `validate()` reports
those two facts separately so each caller can act on its own half, and both
upserts preserve keys this CLI version does not model, so a hand-written config
survives a round trip.

`pluginKey` is read but never written: codegen-v2 parses and validates it, then
never reads it, so writing one would only be a promise the plugin cannot keep.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A published SDK is the only thing that can name a language for the plugin, so
`sdk publish` is where the entry comes from. It writes languages alone and never
metadata, which keeps publishing free of plugin questions for anyone who does not
want a context plugin.

Interactive runs are asked. Non-interactive runs answer with
`--update-plugin-config` instead: that path is documented for CI/CD, where a
prompt is either never answered or silently declined at EOF.

Nothing here may change the publish result, so the action returns void and stays
silent when the profile names no source repository.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`sdk publish` leaves a config with languages and no identity, and a fresh project
has no config at all. Both are now filled in here rather than failing at the
backend: the run asks for an id, name and version, writes them beside whatever
languages are already recorded, and only then generates.

A config that names no language stops with next steps instead of uploading a
build the backend will reject.

Also renames the language error key to `languages`, following codegen-v2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`--dry-run` generates locally and publishes nothing, but `SdkPublishAction`
returns success for it, so `--update-plugin-config` wrote a languages entry
claiming a published SDK. `plugin generate` would then build a plugin from a
repository holding nothing.

Ignoring the flag in silence would repeat the failure it fixes, so the run
says why it was ignored.

Also drops a `CodeGenerationVersion` import left unused when this call
switched to `codegenOption.codeGenerationVersion()`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A package-only profile names no repository, so no language entry can be
built. That return happens before the confirm, so an interactive publish
simply never showed the prompt and gave no reason.

`upsertLanguage` refusing a config it cannot parse was equally silent, and
that one lands after the user has already agreed to record.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four fixes that interleave across the same files, so they land together.

Record only what the run published. `buildLanguageEntry` read whatever the
profile enabled, so `--publish-type package` recorded a source repository
this run never pushed to, and `--publish-type sourcecode` recorded a package
that was never released. Both halves are now gated on the requested types.

Never let recording crash the publish. `merge()` left `write()` unguarded, so
a read-only `src/`, a locked file or a full disk turned an already-successful
publish into a stack trace with exit 1, and `outro` never ran — despite the
action documenting that it never throws. Reporting the fault meant the write
result had to say *which* fault, so `upsertMetadata`/`upsertLanguage` return
`written | unreadable | unwritable` instead of a boolean that silently meant
"unreadable".

Refuse to invent a repository URL. A publishing profile carries no git host,
so only an absolute http(s) URL or a plain `owner/repo` can be resolved. An
SSH remote previously became `https://github.com/git@github.com:acme/sdk.git`
and was written as a clone target; the backend validates only that the URL is
non-blank, so a guess survived to a failed clone. Anything unresolvable is now
reported with the offending value.

Backfill `schemaVersion`, and refuse one this CLI does not model. A hand-written
config that omitted it round-tripped still missing it and was rejected server
side after the CLI said the config was saved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

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