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 diff --git a/src/infrastructure/service-error.ts b/src/infrastructure/service-error.ts index 0dca88ae..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 { @@ -30,6 +31,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, @@ -55,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 e585d77e..75257ffb 100644 --- a/src/infrastructure/services/portal-service.ts +++ b/src/infrastructure/services/portal-service.ts @@ -78,6 +78,7 @@ export class PortalService { return err(ServiceError.forbidden(errorMessage)); } } + // 401 (and other SDK ApiError statuses) are mapped centrally in handleServiceError. const serviceError = handleServiceError(error); return err(serviceError); } finally {