From adaa8aebd2a10bea57eec2e62a4409180be23ca4 Mon Sep 17 00:00:00 2001 From: MuhammadRafay1 Date: Tue, 14 Jul 2026 16:46:51 +0500 Subject: [PATCH 1/3] feat: surface API 401 error message with login suggestion in portal generation Previously, running portal commands while logged out showed the generic "An unexpected error occurred" message because the SDK's typed 401 error (UnauthorizedResponseError) was not recognized by the axios-only handleServiceError. Handle ApiError with status 401 in generatePortal, extract the API's message, and pair it with a suggestion to run `apimatic auth login` or pass `--auth-key`. Co-Authored-By: Claude Fable 5 --- src/infrastructure/service-error.ts | 8 ++++++++ src/infrastructure/services/portal-service.ts | 10 ++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/infrastructure/service-error.ts b/src/infrastructure/service-error.ts index 0dca88ae..d2fca9f0 100644 --- a/src/infrastructure/service-error.ts +++ b/src/infrastructure/service-error.ts @@ -30,6 +30,14 @@ export class ServiceError { static notFound(customMessage: string): ServiceError { return new ServiceError(ServiceErrorCode.NotFound, customMessage, {}); } + static unauthorized(apiMessage: string | null): ServiceError { + const message = `${apiMessage ?? "You are not authorized to perform this action."} Please run ${f.cmdAlt( + "apimatic", + "auth", + "login" + )} to log in, or provide a valid auth key using the ${f.flag("auth-key")} flag.`; + return new ServiceError(ServiceErrorCode.UnAuthorized, message, {}); + } static readonly values: ServiceError[] = [ ServiceError.NotFound, diff --git a/src/infrastructure/services/portal-service.ts b/src/infrastructure/services/portal-service.ts index e585d77e..6cb0a656 100644 --- a/src/infrastructure/services/portal-service.ts +++ b/src/infrastructure/services/portal-service.ts @@ -74,10 +74,16 @@ export class PortalService { if (error.statusCode === 400) { return err(ServiceError.badRequest(errorMessage, errors)); } - if (error.statusCode === 403) { - return err(ServiceError.forbidden(errorMessage)); + if (error.statusCode === 401) { + return err(ServiceError.UnAuthorized); } } + if (error instanceof ApiError && error.statusCode === 401) { + // The API reports the reason as {"message": "..."} which the SDK + // deserializes into `result` for its typed 401 error. + const apiMessage = (error.result as { message?: string } | undefined)?.message ?? null; + return err(ServiceError.unauthorized(apiMessage)); + } const serviceError = handleServiceError(error); return err(serviceError); } finally { From 0d32c05c3c67ea0fd58c383a27db49a16451e348 Mon Sep 17 00:00:00 2001 From: MuhammadRafay1 Date: Tue, 14 Jul 2026 16:47:26 +0500 Subject: [PATCH 2/3] docs: add contribution guide skill with pnpm-based commands Add the contribution workflow (branching, local debugging/packaging, README updates, CLI and Platform SDK release process, NPM tagging) as an AI skill file, with package-manager commands updated from npm to pnpm, and index it in CLAUDE.md and .ai/instructions.md. Co-Authored-By: Claude Fable 5 --- .ai/instructions.md | 1 + .ai/skills/contribution-guide.md | 85 ++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + 3 files changed, 87 insertions(+) create mode 100644 .ai/skills/contribution-guide.md diff --git a/.ai/instructions.md b/.ai/instructions.md index df8edc83..10d74571 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -92,3 +92,4 @@ Reference these files for scaffolding new code: - `.ai/skills/prompt.md` — Prompts class conventions; simple / standard / delegation / wizard variants - `.ai/skills/service.md` — Infrastructure Service conventions; SDK controller / axios-auth / axios-stateless variants - `.ai/skills/value-object.md` — Value object (rich class) conventions; encapsulation, boundary unwrapping, composition rules +- `.ai/skills/contribution-guide.md` — Contribution workflow: branching from `dev`, local debugging/packaging, README updates, releasing the CLI and Platform SDK, NPM tagging diff --git a/.ai/skills/contribution-guide.md b/.ai/skills/contribution-guide.md new file mode 100644 index 00000000..934aab42 --- /dev/null +++ b/.ai/skills/contribution-guide.md @@ -0,0 +1,85 @@ +# Contribution Guide + +Please create your feature branch from the `dev` branch and add your changes. It is recommended to delete the local copy of the `dev` branch (if you have one) and pull the latest remote copy whenever you create your feature branch, this is needed because the `dev` branch is not a long-running branch and is deleted whenever we squash-and-merge to the release branch i.e. the `beta` branch. + +``` +Note: At the time of writing this Wiki, the CLI follows the `beta` release branch whereas the SDK follows the `alpha` release branch. +``` + +## Debugging the CLI Locally + +The `.vscode/launch.json` file serves as your entry point to debugging the CLI locally. Add the command + flags you want to debug in the `"args"` of the `"configurations"` section and press the F5 key to start debugging in VS Code. + +If you want to switch to the dev environment of the Platform API, add the `"env"` property to the `"configurations"` section as follows: + +```json +"env": { + "NODE_TLS_REJECT_UNAUTHORIZED": "0", + "APIMATIC_BASE_URL": "https://app.dev.apimatic.io/api;https://auth.dev.apimatic.io;https://api.package-publishing.dev.apimatic.io/api" +}, +``` + +If you're looking to run the CLI on dev environment, just set the `APIMATIC_BASE_URL` in your environment variables as mentioned above. + +## Installing the Packaged CLI Locally + +1. Run `pnpm install` and `pnpm run build` after making your changes. +2. Run `pnpm pack` to package the CLI, this will create a tar file in the repo. +3. Uninstall any existing installations by running `pnpm remove -g @apimatic/cli`. +4. Install the locally packaged CLI by running `pnpm add -g `. + +## Updating the README + +Whenever you add a new command or change the descriptions, flags, examples etc. properties within an existing command, remember to update the README using the command `oclif readme`. You need to install the `oclif` package first or run it on-demand by using the command `pnpm dlx oclif readme`. By default, this will generate the README changes automatically under the `` and `` comments in the README file. + +## Releasing the CLI + +1. Get your changes in your feature branch merged to the `dev` branch first. +2. Create a release PR targeting the `beta` branch from the `dev` branch. +3. *⚠️IMPORTANT⚠️* Before you can merge the `dev` branch to the `beta` branch, you must ask someone from the platform team to disable branch protection on the repo. We are using `semantic-release` package in our github action for releasing & publishing the CLI to NPM, and we have a limitation where `semantic-release` needs to create and push a commit to the `beta` branch right after release and create a tag for it. If branch protection is disabled, this step gets skipped and the release is incomplete. +4. Squash-and-merge your changes and verify the release github action succeeds. + +## Updating the Latest Version on NPM for CLI after Release + +Before you can tag the released version as the `latest` version on NPM, you need an auth token to authenticate with NPM. You will also need to be added to the `apimatic` organisation on NPM. Contact someone from the Platform team if you are unclear about these steps. + +1. Switch to the `beta` branch and pull the latest changes after the release. +2. Update the `.npmrc` file to add the line `//registry.npmjs.org/:_authToken=`. *⚠️DO NOT COMMIT THE AUTH TOKEN TO THE REPOSITORY⚠️* +3. Run the command `npm dist-tag add @apimatic/cli@ latest`. Replace the `` placeholder with the actual version to tag as the latest, for e.g. `1.1.0-beta.10`. + +## Updating the Platform SDK + +Whenever you make any changes in the Platform API i.e. `apimatic-io` that are required in the CLI, like adding/updating an endpoint, you will need to update the Platform API's SDK by following these steps: + +1. Update the Platform API spec in the [`apimatic-docs`](https://github.com/apimatic/apimatic-docs/tree/v3-master/src/apimatic-sdk-docs) repo. +2. Rename the `platform-sdk-generation-build.json` file to use it as your `APIMATIC-BUILD.json` file. +3. Generate the SDK for the build using the `apimatic sdk generate` command in `typescript`. +4. Create a feature branch in the [`apimatic-sdk-for-js`](https://github.com/apimatic/apimatic-sdk-for-js) repo. It is recommended to delete the local copy of the `dev` branch (if you have one) and pull the latest remote copy whenever you create your feature branch, this is needed because the `dev` branch is not a long-running branch and is deleted whenever we squash-and-merge to the release branch i.e. the `alpha` branch. +5. Copy the contents of the generated SDK to your feature branch. Ensure that the `package.json` does not contain any unintended changes, reach out to someone from the platform team if you are unsure. +6. Commit your changes and create a PR for merging your feature branch to the `dev` branch. + +## Testing the Updated SDK Changes with the CLI Locally + +1. Run `pnpm install` and `pnpm run build` in the SDK repo. +2. Package the SDK using the command `pnpm pack`. It is highly recommended to change the version in the `package.json` and set it to something like `0.2.0-alpha.5-test` with the word `test` appended to the end and the version incremented by one (usually the patch version is incremented in MAJOR.MINOR.PATCH). +3. Copy package tar file to the CLI repo where you have your feature branch open. +4. Update the `package.json` `"dependencies"` section, set the `"@apimatic/sdk": "file:apimatic-sdk-0.2.0-alpha.5-test.tgz"`, with the correct tar file name. +5. Update the `package.json` `"files"` property and add a new value in the array like `"/apimatic-sdk-0.2.0-alpha.5-test.tgz"`. +6. Run `pnpm install` and `pnpm run build`, and you can locally debug to verify your changes. Feel free to run `pnpm pack` in case you want to test the packaged CLI. +7. The packaged CLI build will contain the updated SDK. +8. When installing the packaged CLI, keep the sdk package tar file in the same folder/directory as the CLI (at the same level). + +## Releasing the Platform SDK + +1. Get your changes in your feature branch merged to the `dev` branch first. +2. Create a release PR targeting the `alpha` branch from the `dev` branch. Remember to create a proper commit message explaining the changes since the CLI is public-facing. +3. *⚠️IMPORTANT⚠️* Before you can merge the `dev` branch to the `alpha` branch, you must ask someone from the platform team to disable branch protection on the repo. We are using `semantic-release` package in our github action for releasing & publishing the SDK to NPM, and we have a limitation where `semantic-release` needs to create and push a commit to the `alpha` branch right after release and create a tag for it. If branch protection is disabled, this step gets skipped and the release is incomplete. +4. Squash-and-merge your changes and verify the release github action succeeds. + +## Updating the Latest Version on NPM for Platform SDK after Release + +Before you can tag the released version as the `latest` version on NPM, you need an auth token to authenticate with NPM. You will also need to be added to the `apimatic` organisation on NPM. Contact someone from the Platform team if you are unclear about these steps. + +1. Switch to the `alpha` branch and pull the latest changes after the release. +2. Update the `.npmrc` file to add the line `//registry.npmjs.org/:_authToken=`. *⚠️DO NOT COMMIT THE AUTH TOKEN TO THE REPOSITORY⚠️* +3. Run the command `npm dist-tag add @apimatic/sdk@ latest`. Replace the `` placeholder with the actual version to tag as the latest, for e.g. `0.2.0-alpha.7`. diff --git a/CLAUDE.md b/CLAUDE.md index e1c17fc7..5227e780 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,3 +15,4 @@ Reference these files as needed for scaffolding: - `.ai/skills/service.md` — Infrastructure Service conventions and scaffolding - `.ai/skills/value-object.md` — Value object (rich class) conventions: encapsulation, boundary unwrapping, composition - `.ai/skills/event.md` — Domain event conventions: past-tense naming, variants, where to fire +- `.ai/skills/contribution-guide.md` — Contribution workflow: branching from `dev`, local debugging/packaging, README updates, releasing the CLI and Platform SDK, NPM tagging From b29ad9cee49a77105c49b92249e4632336ef59f4 Mon Sep 17 00:00:00 2001 From: Sohail Date: Wed, 15 Jul 2026 17:57:33 +0500 Subject: [PATCH 3/3] refactor: centralize SDK ApiError-to-ServiceError mapping in handleServiceError Move the 401 message extraction out of generatePortal's catch and into handleServiceError, which now recognizes SDK ApiError statuses (401/404/500) alongside axios errors. This surfaces the API's 401 message + login hint for every SDK-based flow, not just portal generation, and removes the unreachable 401 branch inside the ProblemDetailsError block. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/infrastructure/service-error.ts | 10 ++++++++++ src/infrastructure/services/portal-service.ts | 11 +++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/infrastructure/service-error.ts b/src/infrastructure/service-error.ts index d2fca9f0..4a95d84e 100644 --- a/src/infrastructure/service-error.ts +++ b/src/infrastructure/service-error.ts @@ -1,4 +1,5 @@ import axios from "axios"; +import { ApiError } from "@apimatic/sdk"; import { format as f } from "../prompts/format.js"; export enum ServiceErrorCode { @@ -63,6 +64,15 @@ export class ServiceError { } export function handleServiceError(error: unknown): ServiceError { + // SDK controllers throw typed `ApiError`s (not axios errors). The API reports + // the reason as {"message": "..."} deserialized into `result`. + if (error instanceof ApiError) { + const apiMessage = (error.result as { message?: string } | undefined)?.message ?? null; + if (error.statusCode === 401) return ServiceError.unauthorized(apiMessage); + if (error.statusCode === 404) return ServiceError.NotFound; + if (error.statusCode === 500) return ServiceError.ServerError; + } + if (axios.isAxiosError(error)) { const status = error.response?.status; if (status === 401) return ServiceError.UnAuthorized; diff --git a/src/infrastructure/services/portal-service.ts b/src/infrastructure/services/portal-service.ts index 6cb0a656..75257ffb 100644 --- a/src/infrastructure/services/portal-service.ts +++ b/src/infrastructure/services/portal-service.ts @@ -74,16 +74,11 @@ export class PortalService { if (error.statusCode === 400) { return err(ServiceError.badRequest(errorMessage, errors)); } - if (error.statusCode === 401) { - return err(ServiceError.UnAuthorized); + if (error.statusCode === 403) { + return err(ServiceError.forbidden(errorMessage)); } } - if (error instanceof ApiError && error.statusCode === 401) { - // The API reports the reason as {"message": "..."} which the SDK - // deserializes into `result` for its typed 401 error. - const apiMessage = (error.result as { message?: string } | undefined)?.message ?? null; - return err(ServiceError.unauthorized(apiMessage)); - } + // 401 (and other SDK ApiError statuses) are mapped centrally in handleServiceError. const serviceError = handleServiceError(error); return err(serviceError); } finally {