Skip to content

feat(portal): fetch the artifacts from /portal-artifacts - #361

Merged
mrafnadeem-apimatic merged 16 commits into
devfrom
feat/portal-artifacts-service
Sep 25, 2026
Merged

mrafnadeem-apimatic merged 16 commits into
devfrom
feat/portal-artifacts-service

Conversation

@sohail2721

@sohail2721 sohail2721 commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Replaces the APIMATIC_CODE_SAMPLES_PATH stub #352 left behind with the real call, and puts what comes back into the site.

What

PortalArtifactsService.generate() zips src/, POSTs it to /portal-artifacts, polls, downloads and unpacks. One run produces three things, so it returns three:

  • code samples in memory, which is what addCodeSamples already merges into a copy of each spec
  • SDK zips, one per language
  • plugin.zip, when apimatic.json carries a plugin block

PortalProjectService.prepare copies the SDKs and the plugin into the temporary build project, and a new template plugin (portal-template/downloads.ts) serves them from there:

laid out in the build project offered at
sdk/<language>.zip downloads/sdk/<language>.zip /__downloads/sdk/<language>.zip
plugin.zip downloads/plugin.zip /__downloads/plugin.zip

Vite's one publicDir is already the user's static/, so the plugin adds a second source: middleware in portal serve, and a copy into the client bundle in portal generate. A preview and a published portal offer the same downloads at the same addresses, and nothing is written into the user's project.

portal generate and portal serve share one run

PreparePortalProjectAction fetches the artifacts, reads the source, reports what it hides or shadows, and prepares the Vite project. Each command passes in its next step, building or serving, instead of getting the project back, because the artifacts and the project sit in temporary directories that have to stay alive while the build or preview reads them. The seven prompts both commands had in common move with it. This took the shared code from forty-nine lines to six and got Sonar's duplication gate passing again.

Three differences from /plugin the poll had to be told about

/plugin /portal-artifacts
Finishing 302 to the download 302 to the download: the same. Completed is never on the wire, so the poll maps the 302 to it
Subscription refusal 403 on initiate 403 on initiate and SubscriptionError as a status, for a language or plugin the subscription lacks
Failure one reason every reason at once: one key per failed language, plus plugin

The first row was originally built for a 200 with Completed, which is what the plan predicted. Against the deployed service, that made every run fail one poll after it had succeeded.

The last row is why the error formatter is its own. A run delivers everything or nothing, so csharp: SDK generation failed and plugin: skills generation failed both have to reach the user, or they fix one and hit the other.

Ordering

The overwrite question runs after the source is read. That puts it after the generation too. Asking first would put it ahead of source problems that end the run anyway, so the user would confirm an overwrite that never happens.

Smaller decisions worth a look

  • The SDK map is keyed by the delivered file name rather than parsed into Language, so a language the server adds before this CLI knows about it still gets offered. Code samples work the other way: a catalog for a language we can't read is an error, because dropping it would publish a portal missing samples the user asked for, with no warning.
  • A run with no SDKs and no plugin sets downloadsDir to null, so a docs-only portal gets no __downloads/ directory.
  • /__downloads/ resolves only the files it was given. The middleware looks up an exact map of paths, so .. and unknown names fall through to the rest of the site.
  • The spinner is now one Generating artifacts step, which matches what one call producing three artifacts actually does.

Reviewing

Service tests run against a stub HTTP server with real zipped bundles for each case: everything delivered, nothing delivered (a docs-only portal must not look like a failure), the upload carrying the source, the 302 finish, and five failure paths. downloads.test.ts covers the template plugin both in the dev server and in the bundle it writes. The project service tests check the layout for SDKs only, plugin only, and neither.

tsc clean on src and test, eslint clean, 1131 passing / 0 failing.

Verified end to end against dev: three SDKs, the context plugin and a TypeScript code-sample catalog fetched, with the sample rendered into the built endpoint page.

Not yet linked from the portal. The downloads are served, but no page links to /__downloads/ yet.

Follows .ai/plans/quickstart-portal-artifacts.md.

🤖 Generated with Claude Code

@sohail2721
sohail2721 added this pull request to stack #362 September 24, 2026 11:51
Base automatically changed from feat/code-samples-portal to dev September 24, 2026 17:40
Muhammad Sohail and others added 5 commits September 24, 2026 22:55
`PortalArtifactsService.generate()` stood in for this endpoint, reading
catalogs from the file named by `APIMATIC_CODE_SAMPLES_PATH` and saying so
in a comment. The endpoint exists now, so it makes the call: zip `src/`,
POST it, poll, download, unpack.

It returns more than samples, because one run produces three things. The
SDKs and the plugin come back as paths into the directory the caller owns
— placing them is the next commit — while the samples stay in memory,
which is what the build already merges into a copy of each spec.

Three things the endpoint does differently from `/plugin`, all of which
the poll had to be told about. It answers 200 with `Completed` rather than
redirecting to the download. It reports `SubscriptionError` as a status,
because a run refused for a language or a plugin the subscription does not
carry is decided inside the orchestrator rather than at the door. And it
fails all at once: the error names every language that failed and the
plugin beside them, since a run delivers everything or nothing.

The SDK map is keyed by the file name as delivered rather than parsed into
`Language`, so a language the server adds before this CLI models it is
still placed rather than dropped. A catalog it cannot read is the opposite
— an error, because leaving one out would publish a portal missing the
samples for a language the user asked for and say nothing.

The spinner now says what it does: one `Generating artifacts` step for all
three, which is the step the journey describes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The artifacts were downloaded and only the samples were read. Now the SDKs
go to `src/static/sdk/<language>.zip` and the plugin goes to two places:
`src/static/plugin.zip`, which the built site offers as a download, and
`./plugin` beside `src/`, which is what the installer takes and what
`plugin publish` reads.

`PluginContext.save` does the expanding rather than a plain unzip, because
`plugin publish` turns `./plugin` into its own git repository and replacing
the directory outright would take the history with it.

The SDK directory is replaced rather than added to, so a language dropped
from `apimatic.json` stops being offered — and a run carrying no SDKs takes
the directory away, so a portal that documents no language does not grow an
empty download folder.

Placement moved ahead of `PortalSourceContext.resolve`, which records
whether `static/` exists: a project getting its first SDK download would
otherwise build without one, because the directory did not exist when the
source was read. The overwrite question moved ahead of the generation with
it — asking whether to replace a portal after a five-minute wait is asking
too late.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The service imported `PortalArtifactsGenerationStatus` to export it again,
which is how the other services read and so how this one got written. But
nothing consumes it from here, and the enum's own module is a shorter
import than this one for anybody who ever does.

It stays where it is declared, beside the response type that uses it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fetching the artifacts put the same forty lines in `portal generate` and
`portal serve`: ask `/portal-artifacts` for what the config declares, place
it, read the source, report what the source hides or shadows, and prepare
the Vite project. Only what happens to that project differs — one builds it
to disk, the other serves it — and the project service's own comment already
said it "prepares the throwaway Vite project that both run".

`PreparePortalProjectAction` is that run, and it takes the caller's next step
rather than returning the project: the artifacts and the project live in
temporary directories that have to outlive it, because the build reads them
and the preview goes on reading them until the user stops it.

The seven prompts both commands delegated identically move with it. What is
left in each is what only it says — a build log, or a live-preview note.
`stubPreparePortalProject` does the same for the tests.

The longest run either pair still shares is six lines, against forty-nine
before. Sonar was failing the gate at 8.8% duplication on new code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Against the deployed service every `portal generate` failed with "an
unexpected error occurred". The status endpoint does not answer a finished
run with `Completed`; it answers `302` to the download, exactly as plugin
generation does. The poll read that as a contract violation and gave up, one
poll after the run had actually succeeded.

The comment this replaces predicted the opposite and said so in as many
words. It was wrong: the redirect is the gateway's convention for both
endpoints, not something either one chose.

`Completed` stays in the enum because the poll needs a value to report; it is
simply never seen on the wire.

Verified end to end against dev: three SDKs, the context plugin and a
TypeScript code-sample catalog fetched, placed, and the sample rendered into
the built endpoint page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mrafnadeem-apimatic
mrafnadeem-apimatic force-pushed the feat/portal-artifacts-service branch from e532ab5 to 9269318 Compare September 24, 2026 18:14
mrafnadeem-apimatic and others added 11 commits September 25, 2026 08:49
The source is read once the artifacts are placed, so asking about the
destination up front put that question ahead of problems that end the run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…source

The SDKs and the plugin from /portal-artifacts were written into the
user's static/ and expanded beside src/. They are now laid out in the
throwaway build project and served under /__downloads/ by a template
plugin, in the preview and in the built site, so the user's project is
left as they wrote it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The build directory is the user's src/; the temporary folder the template
runs in was also called one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CodeSamples was a set of code-sample catalogs, and the file the template
reads was named after "samples JSON", which CONTEXT.md avoids.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…K code does

PortalSourceContext, PortalSource and PortalSourceProblem become
PortalBuildDirectoryContext, PortalBuildDirectoryContents and
PortalBuildDirectoryProblem, with their files, variables and prompts.
Messages shown to the user are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tion and route

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

Copy link
Copy Markdown

}

let json: unknown;
return await this.unpack(bundle.value, into);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Services should return streams as-is and let their callers deal with it in whatever manner suits them best.

@mrafnadeem-apimatic
mrafnadeem-apimatic merged commit 0e823f3 into dev Sep 25, 2026
10 checks passed
@mrafnadeem-apimatic
mrafnadeem-apimatic deleted the feat/portal-artifacts-service branch September 25, 2026 07:10
sohail2721 pushed a commit that referenced this pull request Sep 25, 2026
#361 landed, so `portal serve` is built around `PreparePortalProjectAction`
now and the flat body this branch had edited is gone. Dev's structure is taken
whole; what goes back on top of it is `onAfterServe`, which is how quickstart
says its last word after the preview is up rather than before it.

The wizard's test followed the same move. It marked the handoff by stubbing
`prepare`, which the preview no longer reaches first -- it asks
`/portal-artifacts` before it prepares anything, and an unstubbed service
would have reached the network from a unit test. Both are stubbed, and either
being called is the handoff.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sohail2721 pushed a commit that referenced this pull request Sep 25, 2026
The SDK and plugin archives stopped being written into `src/static/`. #361
landed with them going into the throwaway project the portal is compiled from
instead, under `__downloads/`, and that project leaves when the command does.
Nothing creates `src/static/sdk/` or `src/static/plugin.zip` any more, so
naming them in a `.gitignore` describes files the user will never see.

`/plugin/` stays, and is now the whole list: `plugin generate` writes it, and
`plugin publish` runs `git init` inside it.

Found by running `portal generate` against dev and looking for the artifacts
where this branch says they land.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
saeedjamshaid added a commit to apimatic/sample-docs-as-code-portal that referenced this pull request Sep 25, 2026
The README described a Petstore portal that the CLI builds without
uploading anything. Since apimatic/apimatic-cli#361, portal generate
sends src/ to APIMatic for the SDKs, their docs, the code samples and
the context plugin, and only the site is built locally. The README now
says what quickstart does with each file in src/, what the languages
and plugin blocks add to the portal, and that anything added under
src/ has to be readable by every released 2.x CLI, because quickstart
downloads the branch as it is at the time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
sohail2721 added a commit that referenced this pull request Sep 25, 2026
* feat(quickstart)!: one funnel, not a question

Quickstart opened by asking "API Portal or SDK?" and ran one of two
wizards. The journey has one: a new user gets a portal, the SDKs it
documents and a context plugin, and is never asked to choose between
product concepts before they have seen any of them.

So the question goes, and the SDK branch behind it — `SdkQuickstartAction`
and its prompts, 500 lines whose language selector, account lookup and
editor-opening the portal path already does or no longer needs. Anyone who
wants an SDK on its own runs `apimatic sdk generate`, which the journey
names as the path for exactly that.

`QuickstartAction` is now the welcome and the one wizard.

BREAKING CHANGE: `apimatic quickstart` no longer offers a choice between a
portal and an SDK. It always builds a portal, and `apimatic sdk generate`
is the command for an SDK alone.

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

* feat(quickstart): ask which languages the portal covers

The one question quickstart asks besides the spec. Every language a plugin
can carry comes up checked, because a first portal covering everything it
can is what a single Enter should give, and the four it cannot are named in
the prompt rather than left out in silence.

The answer is written to `apimatic.json` through `recordLanguages` before
anything is built, because that file is what says which SDKs a portal
documents — every command after this one reads it rather than the answer
that produced it.

`languageLabel` moves to `types/sdk/generate.ts`, where both prompts that
name a language can reach it, and TypeScript is spelled the way its own
project spells it.

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

* feat(quickstart): name the plugin after the folder, without asking

The `plugin` block is what asks the service for a context plugin, so
quickstart has to write one — and the journey allows it no question about
plugins. The identity comes from the directory the user is standing in.

A folder name is not a plugin id. `Acme Payments` and `acme_payments` are
ordinary names and neither is the kebab-case `pluginId` has to be, so the
id is made from the name while the name itself is kept as typed — the id
goes to `gh repo create`, the name is what a reader sees. A directory whose
name survives none of that falls back, because writing an id the config
would then refuse is worse than not deriving one.

`0.1.0` for the version: every plugin starts somewhere.

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

* feat(quickstart): keep generated files out of the repository

Every command that builds a portal writes `src/static/sdk/`,
`src/static/plugin.zip` and an expanded `./plugin` into the project. None
of it belongs in a repository, and `portal serve` writes it too — so
without this a preview leaves a clean checkout dirty.

`/plugin/` is the one that matters. `plugin publish` runs `git init` inside
that directory and pushes it as its own repository; a parent tracking it
would nest one repository inside another.

Quickstart writes the entries because quickstart is what makes the project.
It appends what is missing and rewrites nothing, so a `.gitignore` the user
wrote survives and re-running in an adopted directory stacks no duplicates.

The paths are ignored here before anything on this branch produces them:
the placement lands with the `/portal-artifacts` call, and a user whose
first build dirties their tree has already been failed by then.

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

* fix(quickstart): end in the preview again, and say the coming languages under the answer

Two things, both about where a line belongs.

The rebase onto dev quietly took dev's ending for the wizard -- the one that
stops at the scaffold because "the wizard does not ask for the project's SDK
languages yet". It does, three lines above that comment. The handoff to
`portal serve`, and the `onAfterServe` the serve action took for it, had both
gone; nothing failed, because no test described the funnel's own ending. One
does now.

`Next Steps` loses its first two items with them. It told the user to name
their SDK languages and then start a preview; the wizard has just done both.
What is left is what to change now that the portal is on screen.

The coming-soon languages move out of the multiselect's message and under the
answer, in grey. Inside the message they rendered above the languages the user
had picked, in the green a submitted answer is drawn in, reading like a fourth
option rather than a footnote to the three.

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

* feat(portal): give quickstart the api transform hint, with the format it needs

Quickstart checks the spec itself before it writes the project, so the
transform hint portal generate and portal serve now give never reached it.
A Postman collection or a RAML file was told only that it was not an
OpenAPI document, with nothing to do about it.

Both now end on one sentence, which names --format=openapi3yaml: the
transform refuses to run without a format, so the command as printed
before would have failed.

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

* fix(quickstart): ignore only what is still generated into the project

The SDK and plugin archives stopped being written into `src/static/`. #361
landed with them going into the throwaway project the portal is compiled from
instead, under `__downloads/`, and that project leaves when the command does.
Nothing creates `src/static/sdk/` or `src/static/plugin.zip` any more, so
naming them in a `.gitignore` describes files the user will never see.

`/plugin/` stays, and is now the whole list: `plugin generate` writes it, and
`plugin publish` runs `git init` inside it.

Found by running `portal generate` against dev and looking for the artifacts
where this branch says they land.

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

* docs(quickstart): describe it as the first portal, SDKs and plugin

The summary says "your first" again, which is what a quickstart is for, and
the description lists "SDKs" plainly instead of "the SDKs it documents".

The README section is `oclif readme`'s output for the new text. Run on
Windows, the command appended a second copy of the usage and commands blocks
rather than replacing them, so only the quickstart section was taken from it.

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

* refactor(quickstart): name the plugin config steps instead of commenting them

The comments above recording the languages and writing `.gitignore` go, and
the names say what they explained. The chain that wrote `apimatic.json` called
the result of recording the languages `languages`; it is `languagesRecorded`
now, and the two writes read as two steps rather than a `.then`.

`ProjectContext.ignoreGeneratedFiles()` is `upsertGitignore()`, next to
`upsertMetadata()` and saying what it does to the file.

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

* fix(quickstart): record a placeholder plugin identity, not the folder's name

Quickstart asks nothing about the plugin, so it took the identity from the
directory it was run in. A project downloaded from APIMatic arrives in
`portal-source/`, which made that the plugin's id and name: a name that says
nothing about the API, and one `plugin publish` would create a repository
under.

It records `my-api-plugin` and "My API Plugin" instead, the plugin's
counterpart to the portal's "My API": plainly a placeholder, and the same for
every project. `deriveMetadata` and its test go with the derivation.

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

* fix(quickstart): ignore the SDK and portal output as well as the plugin

`sdk generate` writes to `/sdk/` and `portal generate` to `/portal/` by
default, both beside `src/` in the project quickstart makes. They are build
output like `/plugin/`, so the `.gitignore` names all three.

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

* refactor(quickstart): make the wizard the quickstart action

`QuickstartAction` only printed the welcome message and handed over to
`PortalQuickstartAction`. With the SDK flow gone there was nothing left to
choose between, and the wizard builds the SDKs and the plugin as well as the
portal, so it is `QuickstartAction` now, in `src/actions/quickstart.ts`, and
prints the welcome message itself. Its test moves with it.

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

* refactor(sdk): derive the upcoming languages from the ones a plugin carries

The list was maintained by hand beside a comment that counted it, so a
language could be named by both lists or by neither, and adding a v4
renderer meant editing two places and a sentence.

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

* refactor(portal): name the serve callback for when it is called

onServing says what onAfterServe needed a line of JSDoc to say.

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

* fix(project): report a gitignore that cannot be written instead of throwing

getContents and writeContents wrap fs-extra and throw, so a read-only or
locked .gitignore threw out of the wizard, which has already written a
portal by the time it gets here. The caller decides what that is worth.

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

* feat(portal): write the source tree around a specification already in spec/

scaffold installs the specification it is given and then writes the tree.
A project downloaded from the platform already has its own spec/, so adopt
is the same tree with nothing installed, and primarySpec names the document
a portal speaks for.

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

* feat(quickstart): adopt a project that already carries its source directory

A build downloaded from the platform arrives with its specification already
in src/spec/, so the wizard validates what it finds rather than asking for
the specification the project has and then for somewhere to put it. Such a
project keeps its own spec/: the sample is written where the wizard is told
to write, so a failure there cannot offer it.

The wizard's prompts are one class again, named for the command, and the
comments that restated the code below them are gone.

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

* refactor(quickstart): call the directory holding src/ the project directory

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

---------

Co-authored-by: Muhammad Sohail <sohail.shahbaz@apimatic.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: MuHamza30 <muhammad.hamza@apimatic.io>
Co-authored-by: Muhammad Rafay Nadeem <muhammad.nadeem@apimatic.io>
github-actions Bot pushed a commit that referenced this pull request Sep 28, 2026
# [2.0.0-beta.1](v1.5.0...v2.0.0-beta.1) (2026-09-28)

* build!: move to Node 24 and TypeScript 7 ([#347](#347)) ([2528d6e](2528d6e))
* feat(quickstart)!: one funnel, and the language question ([#363](#363)) ([c270135](c270135)), closes [#361](#361)
* feat(sdk)!: retire v3 generation and let a publish record itself ([#359](#359)) ([c48f91b](c48f91b)), closes [#358](#358) [#358](#358) [apimatic-io#2233](https://github.com/apimatic-io/issues/2233)

### Bug Fixes

* **config:** keep the package configuration a publish did not write ([#392](#392)) ([bc52735](bc52735))
* **config:** nest publishing under the language ([#350](#350)) ([fbacf74](fbacf74))
* **deps:** take fumadocs 16.15.15, which fixes bold text in a built portal ([#388](#388)) ([fa178ab](fa178ab))
* **plugin:** ask only for the plugin's identity when the languages are recorded ([#389](#389)) ([d721819](d721819)), closes [apimatic/apimatic-io#2258](https://github.com/apimatic/apimatic-io/issues/2258)
* **portal:** bundle the Geist fonts instead of loading them from Google Fonts ([#366](#366)) ([95f2f7e](95f2f7e))
* **portal:** drop the TypeScript definitions panel from operation pages ([#353](#353)) ([042eb89](042eb89))
* **portal:** give a run the temp directory the filesystem names, not an alias ([#377](#377)) ([d71d23f](d71d23f))
* **portal:** keep the encoding a page slug carries ([#396](#396)) ([0bec5f4](0bec5f4)), closes [apimatic/apimatic-io#2270](https://github.com/apimatic/apimatic-io/issues/2270)
* **portal:** keep the header's links on one line ([#398](#398)) ([40dc472](40dc472))
* **portal:** keep the portal name on one line in the header ([#395](#395)) ([f3d352c](f3d352c))
* **portal:** keep the preview starting until its first page answers ([#399](#399)) ([5359442](5359442))
* **portal:** name the operations a shared page comes from, in words a spec's author knows ([#387](#387)) ([01197a7](01197a7))
* **portal:** offer the page actions on API reference pages ([#382](#382)) ([22f4873](22f4873)), closes [apimatic/apimatic-io#2247](https://github.com/apimatic/apimatic-io/issues/2247) [#383](#383)
* **portal:** refuse tag and operationId names a URL or a Windows folder cannot hold ([#400](#400)) ([0ea01b2](0ea01b2)), closes [apimatic/apimatic-io#2262](https://github.com/apimatic/apimatic-io/issues/2262) [#396](#396)
* **portal:** render a request body in a media type Fumadocs has no adapter for ([#386](#386)) ([9f89ad3](9f89ad3))
* **portal:** render an operation whose response example has no value ([#373](#373)) ([1dc84ad](1dc84ad))
* **portal:** render split OpenAPI specifications correctly ([#354](#354)) ([d97cda6](d97cda6))
* **portal:** stop printing an operation's raw description under its title ([#383](#383)) ([25b9f6e](25b9f6e)), closes [apimatic/apimatic-io#2242](https://github.com/apimatic/apimatic-io/issues/2242)
* **quickstart:** download the sample spec from its new openapi.json path ([#401](#401)) ([ebc6743](ebc6743))
* the release test's high and medium findings ([#375](#375)) ([7fe41e1](7fe41e1))

### Features

* code samples support ([#352](#352)) ([7b82568](7b82568))
* **config:** replace portal.json and plugin-config.json with apimatic.json ([#348](#348)) ([506f67f](506f67f))
* local plugin generation ([#358](#358)) ([5ba70de](5ba70de)), closes [#351](#351) [#346](#346) [#348](#348) [#318](#318)
* **portal:** build the documentation portal locally with Fumadocs ([#343](#343)) ([073d09a](073d09a))
* **portal:** build the SDK and Context Plugin pages from real templates and the portal artifacts ([#369](#369)) ([0efd7b3](0efd7b3))
* **portal:** fetch the artifacts from /portal-artifacts ([#361](#361)) ([0e823f3](0e823f3))
* **portal:** generate the SDK and context plugin pages ([#360](#360)) ([338b14a](338b14a))
* **portal:** give the portal block its v2 shape, with branding and tabs ([#355](#355)) ([d0fb189](d0fb189))
* **portal:** lead each SDK card with its install command ([#376](#376)) ([119e9d6](119e9d6)), closes [#375](#375)
* **portal:** let the root nav.json decide the tabs, and fix the nav.json QA findings ([#371](#371)) ([ecf7666](ecf7666))
* **portal:** order the sidebar from nav.json ([#346](#346)) ([306e4f9](306e4f9))
* **portal:** point to api transform when src/spec has no OpenAPI 3.x document ([#364](#364)) ([eefc917](eefc917))
* **portal:** the SDK Overview design, on the SDKs, language and plugin pages ([#391](#391)) ([fd3fe34](fd3fe34))
* **quickstart:** suggest validating with an AI agent when a spec fails ([#385](#385)) ([36fd2bb](36fd2bb))

### Reverts

* **portal:** offer the page actions on API reference pages ([#382](#382)) ([#397](#397)) ([c087ef9](c087ef9)), closes [#388](#388) [apimatic/apimatic-io#2247](https://github.com/apimatic/apimatic-io/issues/2247)

### BREAKING CHANGES

* `apimatic sdk save-changes` is removed, along with
`--codegen-version`, `--stability`, `--track-changes` and `--skip-changes`
on `sdk generate` and `--codegen-version`, `--stability` and
`--update-plugin-config` on `sdk publish`. Java, Ruby, Go and PHP cannot be
generated until they reach v4.

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

* fix(sdk): keep --stability, which outlived the version it came with

Retiring v3 took `--stability` with it on the grounds that there was one
answer left. There is one answer *today*: v4 renders every language at beta
and they reach stable one at a time, so the level is a choice that survives
the version being fixed.

The flag is back on both commands, defaulting to stable as before, and the
level a language offers is a table rather than a constant — `stableLevels`
of one is answered without asking, and the interactive flow puts the
question back the moment a language has two. `sdk publish` names the level
in its summary only when the user chose it, read from oclif's parse
metadata, because `--stability stable` and the default are the same string
and the doc's block has no such row.

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

* fix(sdk): keep --codegen-version, with one version in it

Retiring v3 removed the flag along with the version, on the grounds that a
choice of one is not a choice. But the flag is how a caller says which
generator they want, and there will be a v5: removing it makes that move a
release that changes what people get, rather than a value they pass.

`CodeGenerationVersion` now holds `V4` alone, and stays an enum so the next
one is a line in it. The flag is back on both commands, accepting `v4`,
defaulting to `v4`, and refusing `v3` by name rather than generating
something else quietly. `sdk publish` names it in its telemetry again.

It is not written down. `codegenVersion` left `apimatic.json` when the
service took over deciding the generator, so the flag says what to generate
with and nothing records what was used.

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

* fix(config): drop codegenVersion from the published schema
* `apimatic quickstart` no longer offers a choice between a
portal and an SDK. It always builds a portal, and `apimatic sdk generate`
is the command for an SDK alone.

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

* feat(quickstart): ask which languages the portal covers

The one question quickstart asks besides the spec. Every language a plugin
can carry comes up checked, because a first portal covering everything it
can is what a single Enter should give, and the four it cannot are named in
the prompt rather than left out in silence.

The answer is written to `apimatic.json` through `recordLanguages` before
anything is built, because that file is what says which SDKs a portal
documents — every command after this one reads it rather than the answer
that produced it.

`languageLabel` moves to `types/sdk/generate.ts`, where both prompts that
name a language can reach it, and TypeScript is spelled the way its own
project spells it.

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

* feat(quickstart): name the plugin after the folder, without asking

The `plugin` block is what asks the service for a context plugin, so
quickstart has to write one — and the journey allows it no question about
plugins. The identity comes from the directory the user is standing in.

A folder name is not a plugin id. `Acme Payments` and `acme_payments` are
ordinary names and neither is the kebab-case `pluginId` has to be, so the
id is made from the name while the name itself is kept as typed — the id
goes to `gh repo create`, the name is what a reader sees. A directory whose
name survives none of that falls back, because writing an id the config
would then refuse is worse than not deriving one.

`0.1.0` for the version: every plugin starts somewhere.

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

* feat(quickstart): keep generated files out of the repository

Every command that builds a portal writes `src/static/sdk/`,
`src/static/plugin.zip` and an expanded `./plugin` into the project. None
of it belongs in a repository, and `portal serve` writes it too — so
without this a preview leaves a clean checkout dirty.

`/plugin/` is the one that matters. `plugin publish` runs `git init` inside
that directory and pushes it as its own repository; a parent tracking it
would nest one repository inside another.

Quickstart writes the entries because quickstart is what makes the project.
It appends what is missing and rewrites nothing, so a `.gitignore` the user
wrote survives and re-running in an adopted directory stacks no duplicates.

The paths are ignored here before anything on this branch produces them:
the placement lands with the `/portal-artifacts` call, and a user whose
first build dirties their tree has already been failed by then.

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

* fix(quickstart): end in the preview again, and say the coming languages under the answer

Two things, both about where a line belongs.

The rebase onto dev quietly took dev's ending for the wizard -- the one that
stops at the scaffold because "the wizard does not ask for the project's SDK
languages yet". It does, three lines above that comment. The handoff to
`portal serve`, and the `onAfterServe` the serve action took for it, had both
gone; nothing failed, because no test described the funnel's own ending. One
does now.

`Next Steps` loses its first two items with them. It told the user to name
their SDK languages and then start a preview; the wizard has just done both.
What is left is what to change now that the portal is on screen.

The coming-soon languages move out of the multiselect's message and under the
answer, in grey. Inside the message they rendered above the languages the user
had picked, in the green a submitted answer is drawn in, reading like a fourth
option rather than a footnote to the three.

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

* feat(portal): give quickstart the api transform hint, with the format it needs

Quickstart checks the spec itself before it writes the project, so the
transform hint portal generate and portal serve now give never reached it.
A Postman collection or a RAML file was told only that it was not an
OpenAPI document, with nothing to do about it.

Both now end on one sentence, which names --format=openapi3yaml: the
transform refuses to run without a format, so the command as printed
before would have failed.

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

* fix(quickstart): ignore only what is still generated into the project
* engines.node is now >=24.0.0. Node 22 is no longer supported.

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

* fix(sdk): build the validation service once configDir is assigned

`validationService` was initialised from `this.configDir` in a property
initialiser, but `configDir` is a constructor parameter property. The code only
worked because TypeScript's downlevel emit assigns parameter properties before
property initialisers; under the native class-field semantics that a modern
target selects, `this.configDir` is still undefined at that point and the
service would be constructed with no config directory.

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

* build: compile with TypeScript 7, keeping TypeScript 6 for linting

TypeScript 7 is the native compiler. On this repo it builds `src` in ~1.3s
against ~4.7s, and type-checks the suite in ~1.0s against ~4.8s. Emitted
output is unchanged apart from sourcemaps, temp-variable naming and
declaration ordering.

typescript-eslint cannot run on it: every published version, canary included,
declares `typescript >=4.8.4 <6.1.0` and throws at module init on TS 7, which
would take out `lint`, `posttest` and the pre-commit hook. So the two sit side
by side in Microsoft's documented arrangement — `tsc` is TypeScript 7 and
builds, bare `typescript` is the TypeScript 6 shim that only the linter loads.
Naming them this way rather than aliasing TypeScript 7 keeps the compiler each
script gets written down, and survives `pnpm update --latest`.

TypeScript 6 stopped sweeping node_modules/@types, so the test project names
the globals the suite needs; the source project resolves its types through
`node:` imports and needs no list.

`target` moves to es2024 now that the engine floor is Node 24, which also
selects native class-field semantics. `ts-node` goes: it drives the classic
compiler API, which TypeScript 7 no longer ships, and the suite runs on tsx.
`@types/mocha` was six years behind mocha and had to be named explicitly.

The `typescript.tsdk` pin is dropped. It is deprecated in favour of
`js/ts.tsdk.path`, inert unless a developer opts in by hand, and would now
point at a TypeScript 6 build within a patch of what VS Code already bundles.

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

* ci: test on Node 24 and 26, and release on 24

The engine floor is Node 24, so the 22 legs go. The second leg is 26 rather
than the floor alone: 26 becomes LTS on 2026-10-28, inside this major's life,
so it is what most developers will be running. The job count is unchanged.

Releasing on Node 24 drops the `npm install -g npm@latest` step, whose only
purpose was that Node 22 bundles npm 10.9.8 while OIDC trusted publishing
needs 11.5.1. Node 24 bundles 11.19.0. The constraint moves to a comment on
the matrix, since pinning that job back to 22 would silently break publishing.

`npm-tag-latest.yml` carried a `node: [ '14' ]` matrix that no step consumed;
the job never sets up Node at all.

The standalone pnpm in test.yml is kept rather than reverted to corepack. Its
original reason is gone, but it is a SHA-pinned executable that needs no Node
of its own, where corepack would resolve pnpm from the registry at run time and
need the three-step setup-node dance the other workflows use. The comment now
states the reason that actually holds.

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

* chore(vscode): repair the CLI debug configuration

`preLaunchTask` named a task the TypeScript extension provides, and that
provider is not registered when the extension stands down for TypeScript 7, so
the configuration would fail to start. The npm task provider supplies `build`
regardless, and still rebuilds before launching.

`program` pointed at `bin/run`, deleted in the ESM migration; it only launched
because Node appends `.js`. The forty commented-out argument lines carried
another machine's absolute paths.

`.gitignore` re-ignored `.vscode/launch.json` in its custom section, cancelling
the allowlist above it. Inert while the file is tracked, but it would have
refused a re-add.

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

* docs: record the two-compiler toolchain and how to remove it

The `typescript` dependency no longer being the compiler is surprising enough
to look like a mistake, so state why it is there, which command gets which
compiler, and the condition for deleting the arrangement.

Also corrects the build commands, which documented `tsc -b` although the
project has no references and the scripts call plain `tsc`.

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

* ci: remove unused action

* fix: broken test

* fix: mock fs broken in node 26

* ci: install pnpm standalone in the build and release workflows

Corepack is not distributed with Node 25 and newer, so the Node 26 leg of
check_build could not bootstrap pnpm. Also correct the release floor: npm
11.5.1, which trusted publishing needs, first ships in Node 24.5.0.

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

* fix(hooks): check the running Node against the declared engine range

Read engines.node from the manifest oclif has already parsed and compare
with semver, so package.json is the single source of the floor and a
minor-version floor is honoured. Test the hook's behaviour on both sides
of the range, and hold the CLI's range to the Node floors the portal
template's dependencies declare.

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

* docs: name the e2e test that depends on the two-compiler arrangement

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

* chore: untrack launch.json and drop leftover config

launch.json goes back to being a personal, ignored file. mocha.opts named
ts-node and has been unused since mocha 8. ESLint now skips Claude Code
worktrees under .claude.

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

* fix(portal): bind the prerender preview server to 127.0.0.1

The prerender pass fetched pages from a preview server on `localhost`.
On macOS it listened on `::1` only; a stalled connect under load fell
back to 127.0.0.1 and was refused, failing the portal build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 2.0.0-beta.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants