Skip to content

Apimatic plugin generate #304

Description

@MuHamza30

Add apimatic plugin generate and the pluginconfig.json lifecycle

Prerequisite: #311 (v4 codegen support in apimatic sdk publish).
Languages.<lang>.version records the codegen version an SDK was published with. While
sdk publish hardcodes CodeGenerationVersion.V3, that field can only ever be "v3", so a
v4 plugin is unreachable — a plugin cannot reference a v4 SDK that was never published as
v4. #311 adds --codegen-version (and --stability) to sdk publish and threads both through to
GenerateAction. A4 depends on it directly; A1, A2 and A3 do not and can proceed in parallel.

The dependency is on #311's PR 1 (non-interactive) only. #311 ships in two PRs — PR 1 threads
the flags through the non-interactive path, PR 2 adds a codegen prompt to the interactive wizard.
A4 is itself non-interactive-only (see Deferred), so it never reads a codegen version from the
wizard. A4 is unblocked the moment PR 1 lands; #311 PR 2 proceeds in parallel with the rest of
this issue.

Summary

Introduce a new command, apimatic plugin generate, that produces a plugin/ directory the
same way apimatic sdk generate produces sdk/ and apimatic portal generate produces
portal/. The command's input is not the src/ build directory but a new config file,
src/pluginconfig.json, which is sent to APIMatic; the CLI then polls for completion and
unpacks the returned artifacts into the plugin directory.

The distinguishing feature of this work is that pluginconfig.json is assembled across two
commands
. plugin generate owns the plugin's identity (id, name, Copilot key, language
selection); sdk publish owns each language's publishing details (source repository, package
identity, codegen version). Either command may create the file; each fills in the parts it owns
and leaves the rest alone. plugin generate only performs a generation once the file is
complete — otherwise it writes what it can and directs the user to sdk publish.

Background

  • apimatic sdk publish already resolves a publishing profile and publishes to a package
    registry and/or a git repository. It knows, per language, the package identity and the git
    repository/branch. Today it discards that knowledge after the run.
  • apimatic portal copilot already resolves an API Copilot key from
    SubscriptionInfo.ApiCopilotKeys and writes it into APIMATIC-BUILD.json. The plugin config
    needs the same key under a different name (pluginKey), so that resolution logic is reused.
  • Contents of the generated plugin/ directory are produced server-side and are out of scope
    here.

The config file

Path: src/pluginconfig.json — inside the build directory, alongside APIMATIC-BUILD.json.

{
  "schemaVersion": 1,
  "pluginId": "acme-payments",
  "pluginName": "Acme Payments",
  "pluginVersion": "0.1.0",
  "pluginKey": "8f3a91c2-4b7e-11ef-9c3d-0242ac120002",
  "author": { "name": "Acme", "email": "developers@acme.com" },
  "license": "MIT",
  "Languages": {
    "python": {
      "source":  { "repository": "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/acme/acme-python", "branch": "main" },
      "package": { "packageId": "acme-checkout-sdk", "registry": "pypi" },
      "version": "v3"
    },
    "csharp": {
      "source":  { "repository": "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/acme/acme-csharp", "branch": "main" },
      "package": { "packageId": "Acme.Checkout.Sdk", "registry": "nuget" },
      "version": "v4"
    }
  }
}

Field ownership

Field Written by Source Required
schemaVersion whichever command creates the file constant 1 yes
pluginId plugin generate prompted (or --plugin-id) yes
pluginName plugin generate prompted (or --plugin-name) no
pluginVersion creator default 0.1.0 no
pluginKey plugin generate SubscriptionInfo.ApiCopilotKeys yes
author creator account info (FullName, Email) no
license not written by the CLI no
Languages.<lang> (key) plugin generate (multiselect) or sdk publish (the language it published) yes, ≥1
Languages.<lang>.source sdk publish profile git config yes
Languages.<lang>.package sdk publish profile package config yes
Languages.<lang>.version sdk publish codegen version yes

A config is complete when pluginId, pluginKey, and at least one language are present,
and every language entry has source, package and version.


Flows

Flow 1 — plugin-first

apimatic auth login
apimatic plugin generate
  1. No pluginconfig.json, or one missing plugin-level fields.
  2. Prompt for pluginId (required) and pluginName (optional, skippable).
  3. Resolve the API Copilot key → pluginKey.
  4. Multiselect languages → create Languages entries with source/package/version absent.
  5. Write the file.
  6. Config is incomplete → print next steps naming apimatic sdk publishexit 130.
apimatic sdk publish …        # fills source + package + version for the published language
apimatic plugin generate      # now complete → generates

Flow 2 — publish-first

apimatic auth login
apimatic sdk publish …
  1. No pluginconfig.jsonsdk publish creates one.
  2. Adds/updates the Languages entry for the language it published, with source, package
    and version filled.
  3. Plugin-level fields (pluginId, pluginKey) remain absent.
apimatic plugin generate
  1. Prompts for pluginId/pluginName, resolves pluginKey.
  2. Languages is already populated and complete → does not prompt for languages.
  3. Generates.

Notes on both flows

  • sdk publish handles one language per invocation, so Languages accumulates over
    repeated runs. Re-publishing an existing language overwrites that entry only.
  • If the user has no publishing profile, sdk publish behaves exactly as it does today
    (directing them to create one in the App). No change.
  • The two flows converge on the same file; neither command clobbers fields it doesn't own.

Scope

A1 — Config model and context (unblocked)

  • src/types/plugin/plugin-config.tsPluginConfig rich class mirroring BuildConfig:
    static parse(content), static createEmpty(), immutable with* methods
    (withIdentity, withPluginKey, withLanguages, withLanguagePublishing), and
    predicates (isComplete(), missingLanguages(): Language[], hasLanguages(),
    getPluginId()).
  • src/types/plugin/plugin-id.tsPluginId value object following ProfileId:
    private field, static tryCreate(value): Result<PluginId, string>, isEqual, toString.
  • src/types/plugin-config-context.tsPluginConfigContext, constructed with the
    build directory (like BuildContext). Derives pluginconfig.json internally as a
    private get. Public surface is behavioural only:
    exists(), read(), save(config), isComplete(), missingLanguages().
    Reads must tolerate a missing file by returning an empty config.

A2 — plugin generate command triple (unblocked apart from the API call)

  • src/commands/plugin/generate.tsexport default class PluginGenerate extends Command
    - flags: --plugin-id, --plugin-name, --language (multiple), then
    ...FlagsProvider.input...FlagsProvider.destination("plugin", "plugin")
    ...FlagsProvider.force...FlagsProvider.authKey (last)
    - static readonly cmdTxt = format.cmd("apimatic", "plugin", "generate")
    - run(): parse → type-convert → CommandMetadataintro("Generate Plugin")
    action.execute(...)outro(result)
    - default destination <input>/plugin
  • src/actions/plugin/generate.tsPluginGenerateAction, standard variant
    (configDir, commandMetadata, authKey: string | null = null)
  • src/prompts/plugin/generate.tsPluginGeneratePrompts

A3 — Plugin generation service (scaffold; blocked on API contract)

  • src/infrastructure/services/plugin-service.tsPluginService, modelled on
    PortalService.generatePortal: submit → poll every 3s → map Failed /
    ValidationError / SubscriptionError onto ServiceError → download → Result.
  • Endpoint, request encoding and response shape are not yet defined. Implement against
    the portal-generation shape and keep the URL in a single as const constant with a
    TODO so it is a one-line change later.

A4 — sdk publish write hook (blocked on #311, plus open questions)

  • After a successful publish, write the language entry into src/pluginconfig.json,
    creating the file if absent.
  • Non-interactive path only for now (see Deferred).
  • version ← the codegen version actually used for the run, i.e. the value threaded from
    --codegen-version by Implementation: v4 codegen support in apimatic sdk publish #311. Do not hardcode "v3". Landing A4 before Implementation: v4 codegen support in apimatic sdk publish #311 would bake
    in a constant that makes v4 plugins unreachable, which is the whole reason Implementation: v4 codegen support in apimatic sdk publish #311 is a
    prerequisite rather than a follow-up.
  • source.repositoryhttps://github.com/${repositoryName}, source.branch
    branch, both from the profile's GitConfiguration. Isolate the URL construction in
    one named function
    — it hardcodes GitHub and will need revisiting for other hosts.
  • package ← per-language mapper. Shape pending (see Open questions); land the mapper
    with a single language wired and the rest throwing a clear TODO until the shape is
    confirmed.
  • A language published with --publish-type sourcecode only has no package config, and
    vice versa. Such an entry is incomplete and will block plugin generate until the
    other half is published.

Behaviour matrix — plugin generate

# Precondition Behaviour Exit
1 Not logged in unauthorizedWithHint message 1
2 No config file create it, prompt id/name, resolve key, multiselect languages, write, redirect to sdk publish 130
3 Config exists, no pluginId prompt for it, write continues
4 Config exists, no pluginKey resolve Copilot key, write continues
5 Config exists, Languages empty multiselect languages, write, redirect 130
6 Config exists, Languages populated do not prompt for languages continues
7 One or more languages missing source/package/version name the incomplete languages, redirect to sdk publish 130
8 Account has zero Copilot keys error, direct to support (same message as portal copilot) 1
9 Account has one Copilot key use it; confirm unless --force continues
10 Account has many Copilot keys select prompt continues
11 Config complete, plugin dir non-empty, no --force confirm overwrite; decline → Please enter a different destination folder… 130
12 Config complete submit, poll, unpack into <destination>, report path 0
13 Generation service error surface ServiceError message 1
14 Any prompt cancelled (Ctrl+C) specific message per step 130

Adopted defaults (flag now if any is wrong)

  • Incomplete config exits 130 (cancelled) — the command did useful work but generated
    nothing; failed overstates it and success would mislead CI.
  • Copilot key resolution mirrors portal copilot exactly, including the
    "only active on one Portal at a time" caution and the support-contact message on zero keys.
  • An existing pluginKey in the config is trusted — not re-prompted, not re-validated.
  • Partial languages block the whole run rather than generating a subset.
  • author auto-filled from account info (already fetched for the Copilot key, so free).
  • license never written by the CLI.
  • pluginVersion defaults to 0.1.0 on creation and is never auto-bumped.
  • schemaVersion written as 1 on creation, never prompted.

Open questions

Blocking sub-parts of A3/A4 only; A1 and A2 can proceed.

  1. repositoryName format. Is it owner/repo or the bare repo name? GitConfiguration
    carries { isEnabled, credentialsId, repositoryName, branch } — no host, no owner. If it is
    bare, the owner lives with the credentials server-side and source.repository cannot be
    constructed; we would need sourceUrl from the publish log instead.
  2. package object shape per registry — to be supplied. The sample uses
    { packageId, registry }, but package identity differs by language: C# packageId,
    Python/Ruby/TypeScript name, Go packageName, Java groupId + artifactId,
    PHP vendorName + projectName. Also confirm the exact registry strings
    (pypi, nuget, npm, maven, packagist, rubygems, and Go).
  3. Languages capitalisation. Every other key is camelCase; the sample uses a capital L.
    Confirm this is the real wire format — a mismatch here fails server-side, not locally.
  4. Plugin generation API contract — endpoint, request encoding (multipart, as the other
    generators use, or JSON?), whether the whole src/ build is uploaded or only
    pluginconfig.json, poll endpoint, terminal statuses, response body.
  5. Does sdk publish write the plugin config unconditionally? This is a behaviour change
    on a shipped command: a user publishing an SDK with no interest in plugins would find a new
    src/pluginconfig.json in their repo. Alternatives: only update when the file already
    exists, or gate behind a --plugin flag. Flow 2 implies unconditional.

Deferred / follow-ups

  • Interactive sdk publish does not update the plugin config. Interactive mode never polls
    the publish log — it prints the log URL and exits as soon as publishing is initiated. Only
    the non-interactive path runs to completion. Explicitly out of scope for now.
  • v4 publishing support — tracked separately. No longer deferred — promoted to a
    prerequisite, Implementation: v4 codegen support in apimatic sdk publish #311.
    The earlier scoping treated this as an optional follow-up with
    Languages.<lang>.version pinned to "v3" in the meantime. That was wrong: a v4 plugin
    cannot exist unless a v4 SDK can be published, so pinning the field would have shipped a
    schema whose v4 case was permanently unreachable. Implementation: v4 codegen support in apimatic sdk publish #311 covers the flag and the threading;
    of the three gaps originally listed here, its current state resolves the first two and leaves
    the third open:
    1. generateV4Sdk(buildPath, language, stability, configDir, commandMetadata, authKey) takes
      no packageVersion, while the v3 generateSdk does. Confirmed to be a real defect, not
      a redundancy: the CLI's publish endpoint (POST /publish/{profileId}/{language}
      PublishGeneratedSdk) uploads the SDK zip verbatim and never calls the transforming
      GeneratePublishReadySdk path, and the worker packs the generated project file unmodified
      (dotnet pack /sdk/*.Standard/*.Standard.csproj, no version override). So the published version
      is whatever the generator baked in; --version reaches only the publish log, the tracking event
      and the constructed package URL. v3 is correct today solely because the CLI passes the version
      to the generator (portal-service.ts:133). The fix belongs in Implementation: v4 codegen support in apimatic sdk publish #311 — send the version to the v4
      endpoint as v3 does; a TODO marks the call site until the generated client exposes the
      parameter.
    2. The v4 branch returns before MergeSourceTreeAction, so SDK customizations are silently
      skipped. Implementation: v4 codegen support in apimatic sdk publish #311 accepts this as a documented known limitation of the v4 path and adds no
      guard; the warning sdkCustomizationsNotSupportedForV4() surfaces during publish.
    3. Confirmed: the v4 generator does not consume package-settings/ at all. The CLI does
      ship it on the v4 path — getBuildZipPath(tempDirectory, packageSettingsDirectory) is called
      at generate.ts:106, before the v4/v3 branch — but the generator ignores it, and per (1)
      publishing does not compensate. The file carries package identity only, not the version
      (package-settings-context.ts:17-22), so a v4 publish gets both the wrong package name and the
      wrong version, from two independent causes. This half needs a generator-side fix and is not
      resolvable in the CLI.
  • sdk quickstart is v3-only (actions/sdk/quickstart.ts:204, hardcoded V3 + STABLE).
    Not changed here. portal quickstart does not generate SDKs at all.

Acceptance criteria

  • apimatic plugin generate appears in --help under a plugin topic, space-separated.
  • Running it with no pluginconfig.json creates src/pluginconfig.json containing
    schemaVersion, pluginId, pluginKey, author, pluginVersion and the selected
    languages as empty entries, then exits 130 with next steps naming apimatic sdk publish.
  • Running it again after sdk publish has filled a language does not re-prompt for
    languages and proceeds to generation.
  • Generation writes artifacts into <input>/plugin by default and honours --destination.
  • The overwrite guard behaves as in portal generate / sdk generate.
  • sdk publish (non-interactive) adds or updates exactly one language entry and leaves all
    plugin-level fields untouched.
  • Neither command discards fields written by the other; round-tripping the file is lossless.
  • All output goes through the Prompts layer; no console.log; no raw string paths.
  • Exit codes: 0 success, 1 failure, 130 cancelled/incomplete.

Testing

Mirroring source paths under test/:

  • test/types/plugin-config-context.test.ts — create/read/merge/save, missing file, malformed
    JSON, completeness and missingLanguages() across partial states (use mock-fs).
  • test/actions/plugin/generate.test.ts — the behaviour matrix above; stub the service with
    sinon, assert ActionResult variants and the written config.
  • test/commands/plugin/generate.test.ts — flag parsing, defaults, interactive/non-interactive
    split.
  • test/actions/sdk/publish/non-interactive.test.ts — extend for the config write hook,
    including round-trip preservation of plugin-level fields.
  • nock for the generation and polling endpoints once the contract is known.

Conventions

Follow .ai/instructions.md and the skills in .ai/skills/:
command.md (Command + Action + Prompts triple), action.md, prompt.md,
context.md (no public path getters; behavioural methods only), value-object.md
(PluginId), and service.md (PluginService returning Result<T, ServiceError>).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions