From 0ea30362a51b97c6bde5edf00c0948e551addc90 Mon Sep 17 00:00:00 2001 From: Artur Jankowski Date: Thu, 22 Oct 2026 10:07:48 +0200 Subject: [PATCH 1/2] chore: add agent skill for adding CLI commands Add the add-box-cli-command skill under .claude/skills, read by Claude Code and Cursor. It guides agents through wrapping a box-node-sdk method as a CLI command: resolving the SDK method, agreeing the command interface with the user, writing tests first, and regenerating docs. --- .claude/skills/add-box-cli-command/SKILL.md | 145 +++++++++++++++ .../references/command-patterns.md | 170 ++++++++++++++++++ .../add-box-cli-command/references/testing.md | 141 +++++++++++++++ 3 files changed, 456 insertions(+) create mode 100644 .claude/skills/add-box-cli-command/SKILL.md create mode 100644 .claude/skills/add-box-cli-command/references/command-patterns.md create mode 100644 .claude/skills/add-box-cli-command/references/testing.md diff --git a/.claude/skills/add-box-cli-command/SKILL.md b/.claude/skills/add-box-cli-command/SKILL.md new file mode 100644 index 00000000..3e127a57 --- /dev/null +++ b/.claude/skills/add-box-cli-command/SKILL.md @@ -0,0 +1,145 @@ +--- +name: add-box-cli-command +description: Use when adding or changing a Box CLI command under `src/commands`, including exposing a `box-node-sdk` manager method as a new command, or editing a command's tests, fixtures, or generated docs. +--- + +# Add Box CLI Command + +Commands wrap `box-node-sdk` manager methods. Copy the shape of a nearby command, agree on the command's interface with the user, write the test first, and make the test prove the real CLI output. + +## Inputs + +- **SDK method** as `.`, e.g. `hubCollaborations.createHubCollaborationV2025R0`. If the user describes the operation instead, find the method by its API path (step 1). +- **Command name**, e.g. `hubs:collaborations:create`. If not given, propose one in step 4. +- **Anything else the user states**: allowed values, permissions, flag names, help text wording. User-stated facts override what you infer. + +## Ask the user + +Ask whenever a decision is not settled by the user's request, the SDK types, or an existing sibling command. Questions come before code: gather them, ask them in one batch, and wait for the answers. + +Use the harness's structured question tool when it has one (`AskQuestion` in Cursor, `AskUserQuestion` in Claude Code), with your recommended option first and marked "(Recommended)". Otherwise ask in chat as a short numbered list. + +Ask about: + +- **Which SDK method**, when several match (e.g. a `V2025R0` method and an older one for the same path). +- **Command name and placement**: new top-level topic or nested under an existing one, and aliases such as `:list` or `:add` when siblings have them. +- **Accepted values** for fields the SDK types as plain `string`. +- **Defaults and required flags**, when the API reference and SDK disagree or say nothing. +- **Permissions and scopes** the command needs, for the help text. +- **Flag names**, when the API field name reads badly as a flag (e.g. `is_ai_enabled` as `--is-ai-enabled` or `--ai-enabled`). +- **Output changes to a released command**: breaking change with `!` in the commit, or opt-in `--raw-json`. +- **An existing command for the same endpoint**: change it, add a new command beside it, or stop. + +After the user approves the plan in step 4, work through the remaining steps without further questions unless a new decision comes up. + +When no user can answer, as in a scheduled or background run, take the recommended option for each question and list every such choice under "Assumptions" in the step 8 report. + +## Workflow + +### 1. Resolve the SDK method + +SDK sources live in `node_modules/box-node-sdk/lib/sdk-gen/`. Agent search tools respect `.gitignore` and skip `node_modules`, so use shell `rg` and `ls` there. + +| File | Gives you | +| --- | --- | +| `managers/.d.ts` | Method signature, parameter and return types, and request body or query interfaces such as `CreateArchiveV2025R0RequestBody` | +| `managers/.js` | HTTP method and path, e.g. `'/2.0/hub_collaborations'` | +| `schemas/v2025R0/.d.ts` or `schemas/.d.ts` | Request and response fields | +| `client.d.ts` | Manager property name on `this.tsClient` | + +To find a method from an API path: `rg -n "'/2.0/hub_collaborations" node_modules/box-node-sdk/lib/sdk-gen/managers/*.js`. To list a manager's methods: `rg -o "^\s+(\w+)\(" -r '$1' node_modules/box-node-sdk/lib/sdk-gen/managers/.d.ts`. + +The HTTP method and path also give the `_endpoint` value and the fixture name: lowercase method, path after `/2.0/`, `/` replaced by `_`, path parameters replaced by `id`. `PUT /2.0/hub_collaborations/{id}` becomes `put_hub_collaborations_id`. + +Check whether a command already covers the endpoint: `rg -n "_endpoint = 'put_hub_collaborations_id'" src/commands`. A match turns the task into changing that command, so confirm with the user in step 4. + +Done when you know the client property, method, HTTP method and path, the request and response types, and whether a command already exists. + +### 2. Collect what the SDK does not encode + +The SDK types many constrained fields as plain `string`. Hub collaboration `role` is `string` in the SDK, while the API accepts only `editor`, `viewer`, and `co-owner`. The SDK also omits required permissions and scopes, and which errors deserve an explanation in help text. + +Check the [Box API reference](https://developer.box.com/reference/) for the endpoint. Every gap left after that becomes a question for step 4. + +Done when every flag has known accepted values, format, default, and permission requirements, or an open question. + +### 3. Read the closest existing pair + +Read the nearest sibling command in `src/commands//`, its cases in `test/commands/.test.js`, its fixtures in `test/fixtures//`, and `docs/.md`. Then read the section of [references/command-patterns.md](references/command-patterns.md) for your command kind (get, list, create, update, delete, compound flags). + +New commands call `this.tsClient`. Most existing commands still call `this.client`, the legacy SDK client, with different method names and output handling. When the nearest sibling uses `this.client`, take its flag names, help text, and test style, and take the SDK call and output code from the reference commands named in command-patterns.md. + +### 4. Propose the plan and ask + +Show the user a short plan, then ask the open questions from steps 1–3 in the same message: + +```text +Command: box archives:create +SDK: this.tsClient.archives.createArchiveV2025R0(requestBody) +Endpoint: POST /2.0/archives (_endpoint: post_archives) +Args: name (required) - Name of the archive +Flags: --description Description of the archive + --storage-policy-id ID of the storage policy for the archive +Output: created archive, API field names (rawData) +Files: src/commands/archives/create.js, test/commands/archives.test.js, + test/fixtures/archives/post_archives.json, docs/archives.md +``` + +Done when the user has approved the plan and answered every question. + +### 5. Test first + +Read [references/testing.md](references/testing.md), write the test, then run only it: + +```bash +./node_modules/.bin/mocha test/commands/.test.js --grep "" +``` + +Done when the test fails for the expected reason: command not found, request mismatch, or wrong output. + +### 6. Implement, then go green + +Write the minimum command code. Re-run the targeted test, then the whole topic file. + +### 7. Lint and docs + +```bash +./node_modules/.bin/eslint --fix +npm run updatemd +``` + +Run `updatemd` whenever description, examples, flags, args, aliases, or command registration changed. It runs `oclif readme --multi` and then fixes the `.ts` links that oclif writes back to `.js`, so always use the npm script. Its `sed -i ''` is macOS syntax. On Linux, run the same two steps with GNU sed: + +```bash +./node_modules/.bin/oclif readme --multi && find docs -type f -name '*.md' -exec sed -i 's/\.ts/\.js/g' {} + +``` + +Keep the diff to the topic's `docs/*.md` and `README.md`, and restore other regenerated files with `git checkout -- `. For a new top-level topic, see "New topic" in [references/command-patterns.md](references/command-patterns.md). + +### 8. Report + +Summarize the new command, the files changed, and the test results. Suggest a conventional commit message: `feat: add archives:create command`, or `feat!: ...` when existing output or flags changed incompatibly. Use the same text as the pull request title, since CI checks the title's format and spelling. Commit only when the user asks. + +## Command conventions + +- Extend `BoxCommand`, `await this.parse(Command)`, call `this.tsClient..(...)`, `await this.output(...)`, `module.exports = Command`. +- Set `description`, `examples`, `_endpoint`, `flags` (spread `...BoxCommand.flags`), `args`, and `aliases` when siblings have them. +- The CLI is used mostly by AI agents. Descriptions state accepted values, formats, defaults, and required permissions. +- Restrict small value sets with Oclif `options`, list the values in the description, and add a negative test. +- Objects passed to the SDK use SDK field names (`accessibleBy`, `hubId`). Output, nock expectations, and fixtures use API field names (`accessible_by`, `hub_id`). + +## JSON output + +The SDK renames response fields to camelCase and wraps dates as `{ value: ... }`. The original API payload is kept on `rawData` of the top-level object only. + +- New command: `await this.output(result.rawData ?? result);` and keep the `?? result` fallback. +- Released command with SDK-shaped output: switching to `rawData` breaks users. Add `...BoxCommand.rawJsonFlags` to `flags` and output `this.getOutputContentWithRawJsonSupport(result)`. Default output stays the same and `--raw-json` prints the API shape. Reference: `src/commands/ai/ask.js`. +- List: `this.markerPagination((query) => this.tsClient..(query), queryParams)` returns raw entries and honors `--max-items`. Output its result directly and spread `...PaginationUtilities.flags` from `src/pagination-utils.js`. + +## Done + +- The user approved the plan and every question was answered. +- Every new test failed before the implementation and passes after it. +- The whole topic test file passes. +- Lint is clean on edited files. +- If help text changed, docs are regenerated and the diff is limited to the topic's docs and `README.md`. diff --git a/.claude/skills/add-box-cli-command/references/command-patterns.md b/.claude/skills/add-box-cli-command/references/command-patterns.md new file mode 100644 index 00000000..0a5d0a1e --- /dev/null +++ b/.claude/skills/add-box-cli-command/references/command-patterns.md @@ -0,0 +1,170 @@ +# Command patterns + +Patterns from the current TypeScript SDK commands. Read the section for your command kind, then copy the named file rather than these excerpts when they differ. + +## Anatomy + +`src/commands/hubs/get.js` is the smallest complete command: + +```js +'use strict'; + +const { Args } = require('@oclif/core'); +const BoxCommand = require('../../box-command'); + +class HubsGetCommand extends BoxCommand { + async run() { + const { args } = await this.parse(HubsGetCommand); + const hub = await this.tsClient.hubs.getHubByIdV2025R0(args.id); + await this.output(hub.rawData ?? hub); + } +} + +HubsGetCommand.description = 'Get details for a Box Hub'; +HubsGetCommand.examples = ['box hubs:get 12345']; +HubsGetCommand._endpoint = 'get_hubs_id'; + +HubsGetCommand.flags = { + ...BoxCommand.flags, +}; + +HubsGetCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the Box Hub', + }), +}; + +module.exports = HubsGetCommand; +``` + +The file path is the command name: `src/commands/hubs/collaborations/create.js` is `hubs:collaborations:create`, and `index.js` is the topic itself (`src/commands/hubs/index.js` is `hubs`). The `require` depth for `box-command` follows the folder depth. + +## Get + +Reference: `src/commands/hubs/get.js`, `src/commands/hubs/collaborations/get.js`. + +One required `id` arg, no extra flags, output `result.rawData ?? result`. + +## List + +Reference: `src/commands/hubs/index.js` (filters), `src/commands/hubs/collaborations/index.js` (parent ID). + +- Put list commands in `index.js` and add a `:list` alias: `HubsListCommand.aliases = ['hubs:list'];`. +- Build `queryParams` from only the filter flags the user set, then call `markerPagination`: + +```js +const queryParams = {}; +if (flags.scope) { + queryParams.scope = flags.scope; +} + +const hubs = await this.markerPagination( + (pageQueryParams) => this.tsClient.hubs.getHubsV2025R0(pageQueryParams), + queryParams +); +await this.output(hubs); +``` + +- Spread `...PaginationUtilities.flags` (from `src/pagination-utils.js`) after `...BoxCommand.flags`. It adds `--max-items`. +- A parent ID arg goes into `queryParams` under its SDK name: `{ hubId: args.id }`. +- Endpoints that use offset pagination or return a single unpaged collection do not fit `markerPagination`. Find a sibling command that calls the same kind of endpoint, or ask the user. + +## Create + +Reference: `src/commands/hubs/collaborations/create.js`. + +- Build `requestBody` with SDK field names and pass it to the SDK method. +- When the API accepts one of several alternatives (user ID, group ID, or login), give each its own flag, make them mutually `exclusive`, and build the nested object in a small helper above the class: + +```js +function getAccessibleBy(flags) { + if (flags['user-id']) { + return { type: 'user', id: flags['user-id'] }; + } + if (flags['group-id']) { + return { type: 'group', id: flags['group-id'] }; + } + if (flags.login) { + return { type: 'user', login: flags.login }; + } + throw new Error('Please provide one of --user-id, --group-id, or --login.'); +} +``` + +- Add an `:add` alias when siblings have one. + +## Update + +Reference: `src/commands/hubs/update.js`. + +- Send only the fields the user passed. Check string flags for a value and boolean flags with `Object.hasOwn`, so `--no-` sends `false`: + +```js +const requestBody = {}; +if (flags.title) { + requestBody.title = flags.title; +} +if (Object.hasOwn(flags, 'ai-enabled')) { + requestBody.isAiEnabled = flags['ai-enabled']; +} +if (Object.keys(requestBody).length === 0) { + this.error('Please provide at least one update flag.'); +} +``` + +- Boolean update flags use `allowNo: true`, which adds the `--no-` form. + +## Delete + +Reference: `src/commands/hubs/delete.js`, `src/commands/hubs/collaborations/delete.js`. + +Call the SDK, then report on stderr with `this.info`, which `--quiet` suppresses: + +```js +await this.tsClient.hubCollaborations.deleteHubCollaborationByIdV2025R0(args.id); +this.info(`Deleted hub collaboration ${args.id}`); +``` + +## Compound flags + +Reference: `src/commands/hubs/items/manage.js`. + +For repeated structured input such as `--add id=11111,type=file,parent-id=67890`: + +- Declare the flag with `multiple: true` and a `parse(input)` function, so oclif returns already-built objects. +- Inside `parse`, call `utilities.parseStringToObject(input, ALLOWED_KEYS)` from `src/util.js`, check required keys, check enumerated values, and throw an `Error` naming the bad key or value. +- Put the format, the allowed values, and what each key means in the flag description. + +## Flags and args + +- Flag names are kebab-case versions of the API field names: `storage_policy_id` becomes `--storage-policy-id`. Bulk input (`--bulk-file-path`) matches CSV columns and JSON keys to arg and flag names, ignoring case, `-`, and `_`, so a `storage_policy_id` column fills `--storage-policy-id`. +- Common properties: `required`, `options` (allowed values), `multiple`, `exclusive`, `allowNo` (booleans), `char` (short form), `parse`. +- Free-text flags such as descriptions use `parse: utilities.unescapeSlashes` so `\n` typed in the shell becomes a newline. +- Global flags already use `char` values `t`, `s`, `h`, `v`, `y`, and `q`. Pick a different letter or omit `char`. +- Args use `Args.string({ name, required, hidden: false, description })`. +- Throw a plain `Error` or call `this.error(...)` for input errors. `BoxCommand` prints the message to stderr and sets exit code 2. + +## Inherited global flags + +Spreading `...BoxCommand.flags` gives every command these flags with no extra code: + +| Flag | Effect | +| --- | --- | +| `--token`, `-t` | Use this access token | +| `--as-user` | Sends the `as-user` header through `this.tsClient` | +| `--json`, `--csv` | Output format, mutually exclusive. The default is human-readable key-value text | +| `--fields` | Keeps only the listed keys of the output, so it matches API field names only when the command outputs `rawData` | +| `--save`, `--save-to-file-path` | Write output to a file | +| `--bulk-file-path` | Run the command once per row of a CSV or JSON file | +| `--verbose`, `--quiet`, `--yes`, `--no-color` | Logging, prompts, and color | + +## New topic + +A command in a folder that does not exist yet creates a new topic. + +- `npm run updatemd` creates `docs/.md` and adds a line under "Command Topics" in `README.md`. +- Check that line's description. If it does not describe the whole topic, add an entry under `oclif.topics` in `package.json`, e.g. `"archives": { "description": "Manage archives" }`, and run `npm run updatemd` again. +- Create `test/commands/.test.js` and `test/fixtures//`. diff --git a/.claude/skills/add-box-cli-command/references/testing.md b/.claude/skills/add-box-cli-command/references/testing.md new file mode 100644 index 00000000..bc657451 --- /dev/null +++ b/.claude/skills/add-box-cli-command/references/testing.md @@ -0,0 +1,141 @@ +# Testing commands + +Tests use `@oclif/test` (`test` chains built on fancy-test), `nock` for HTTP mocks, and chai `assert`. Reference file: `test/commands/hubs.test.js`. + +## Running + +```bash +./node_modules/.bin/mocha test/commands/.test.js --grep "" # one command +./node_modules/.bin/mocha test/commands/.test.js # whole topic +``` + +`npm test -- ` still runs the whole suite, then lints. `--grep` matches the `describe` and `.it` titles, so name the inner `describe` after the command. + +## File layout + +```js +'use strict'; + +const { test } = require('@oclif/test'); +const { assert } = require('chai'); +const { TEST_API_ROOT, getFixture } = require('../helpers/test-helper'); + +describe('Hubs', function () { + describe('hubs:get', function () { + const response = JSON.parse(getFixture('hubs/get_hubs_id')); + + test.nock(TEST_API_ROOT, (api) => + api.get('/2.0/hubs/12345').reply(200, response) + ) + .stdout() + .command(['hubs:get', '12345', '--json', '--token=test']) + .it('gets a hub by ID', (context) => { + assert.deepEqual(JSON.parse(context.stdout), response); + }); + }); +}); +``` + +Each case is a `test` chain inside a `describe`, ending in `.it(...)`. A chain without `.it(...)` never runs, and the surrounding test passes anyway. Pass `--token=test` so no stored credentials are used, and `--json` so output can be parsed. + +## Recipes + +### Request body + +Give nock the expected body with API field names. A request with a different body does not match, and the test fails. + +```js +test.nock(TEST_API_ROOT, (api) => + api + .post('/2.0/hub_collaborations', { + hub: { id: '12345', type: 'hubs' }, + accessible_by: { type: 'user', id: '22222' }, + role: 'editor', + }) + .reply(200, response) +) + .stdout() + .command(['hubs:collaborations:create', '12345', '--role=editor', '--user-id=22222', '--json', '--token=test']) + .it('creates a collaboration for a hub', (context) => { + assert.deepEqual(JSON.parse(context.stdout), response); + }); +``` + +### List, single page + +List calls always send `limit`: 1000 by default, or the `--max-items` value. Include it in `.query({...})`, and omit `next_marker` from the reply. List output is the array of entries, not the whole response. + +```js +test.nock(TEST_API_ROOT, (api) => + api + .get('/2.0/hubs') + .query({ query: 'team', limit: 10 }) + .reply(200, { entries: [hubResponse], limit: 10 }) +) + .stdout() + .command(['hubs', '--query=team', '--max-items=10', '--json', '--token=test']) + .it('lists hubs with provided filters', (context) => { + assert.deepEqual(JSON.parse(context.stdout), [hubResponse]); + }); +``` + +### List, multiple pages + +Mock every page. The first reply carries `next_marker`, and the next request sends it as `marker` with `limit` reduced to the items still needed. See "follows next_marker until max-items is reached" in `test/commands/hubs.test.js`: + +```js +api + .get('/2.0/hubs') + .query({ query: 'team', limit: 1000 }) + .reply(200, { entries: firstPageEntries, limit: 1000, next_marker: 'next-page' }) + .get('/2.0/hubs') + .query({ query: 'team', limit: 1, marker: 'next-page' }) + .reply(200, { entries: secondPageEntries, limit: 1 }); +``` + +### Delete + +Delete commands print to stderr. Compare the whole line, ending in `os.EOL` rather than `\n`, because CI also runs on Windows: + +```js +const os = require('node:os'); + +test.nock(TEST_API_ROOT, (api) => + api.delete('/2.0/hub_collaborations/99999').reply(204) +) + .stderr() + .command(['hubs:collaborations:delete', '99999', '--token=test']) + .it('deletes a hub collaboration by ID', (context) => { + assert.equal(context.stderr, `Deleted hub collaboration 99999${os.EOL}`); + }); +``` + +### Errors + +Flag validation errors and errors thrown by the command are written to stderr, not thrown, so assert on stderr. No nock is needed when the command fails before calling the API. + +```js +test + .stderr() + .command(['hubs:collaborations:create', '12345', '--role=owner', '--user-id=22222', '--token=test']) + .it('rejects unsupported hub collaboration roles', (context) => { + assert.include(context.stderr, 'Expected --role=owner to be one of: editor, viewer, co-owner'); + }); +``` + +Cover each `options` list and each validation branch in the command (missing required key, unsupported value, no update flags) with one such test. + +## Fixtures + +- Name each file after `_endpoint`, e.g. `test/fixtures/hubs/post_hub_collaborations.json`, and load it with `getFixture('hubs/post_hub_collaborations')`. +- Use full payloads when output shape matters and minimal ones when only the request matters. +- Write fixtures with API field names (`snake_case`), as the API returns them. +- The SDK deserializes mocked responses, so enum fields need values its schema accepts. A hub needs `type: 'hubs'`. Check the response type in `schemas/` when unsure. +- Use placeholder identities such as `ceo@example.com`. + +## When a test fails + +- **`JSON.parse` fails or stdout is empty**: the command failed. Its error is printed to the terminal above the mocha failure, because only stdout was captured. +- **"Nock: No match for request"**: the path, query, or body differs from the mock. Compare them against what the command sends, including `limit` on list calls. +- **"Can't deserialize …"**: a fixture field has a value the SDK schema rejects. +- **Exact stderr comparison picks up a Node deprecation warning**: run with `NODE_OPTIONS=--no-deprecation`. From f9912b8676aa76ae77c001af6c20e452a6cb419d Mon Sep 17 00:00:00 2001 From: Artur Jankowski Date: Fri, 23 Oct 2026 13:31:18 +0200 Subject: [PATCH 2/2] chore: address review feedback on add-box-cli-command skill - use plain `grep` instead of `rg`, which macOS does not ship - take test structure from references/testing.md rather than sibling tests, document the two `test` chain shapes that never run, and set up per-case stubs with `.do()` - require the generated SDK (`sdk-gen`, `this.tsClient`) for new commands - read accepted values, defaults, and required fields from the Box OpenAPI schema, keeping the API reference for scopes and permissions --- .claude/skills/add-box-cli-command/SKILL.md | 26 ++++++++++++++----- .../add-box-cli-command/references/testing.md | 7 ++++- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/.claude/skills/add-box-cli-command/SKILL.md b/.claude/skills/add-box-cli-command/SKILL.md index 3e127a57..0af0aef5 100644 --- a/.claude/skills/add-box-cli-command/SKILL.md +++ b/.claude/skills/add-box-cli-command/SKILL.md @@ -10,6 +10,8 @@ Commands wrap `box-node-sdk` manager methods. Copy the shape of a nearby command ## Inputs - **SDK method** as `.`, e.g. `hubCollaborations.createHubCollaborationV2025R0`. If the user describes the operation instead, find the method by its API path (step 1). + +New commands always use the generated SDK (`box-node-sdk/sdk-gen`) through `this.tsClient`, even when the nearest sibling does not. Never call the legacy client `this.client` or use its managers in `node_modules/box-node-sdk/lib/managers/` for a new command. - **Command name**, e.g. `hubs:collaborations:create`. If not given, propose one in step 4. - **Anything else the user states**: allowed values, permissions, flag names, help text wording. User-stated facts override what you infer. @@ -24,7 +26,7 @@ Ask about: - **Which SDK method**, when several match (e.g. a `V2025R0` method and an older one for the same path). - **Command name and placement**: new top-level topic or nested under an existing one, and aliases such as `:list` or `:add` when siblings have them. - **Accepted values** for fields the SDK types as plain `string`. -- **Defaults and required flags**, when the API reference and SDK disagree or say nothing. +- **Defaults and required flags**, when the OpenAPI schema and SDK disagree or say nothing. - **Permissions and scopes** the command needs, for the help text. - **Flag names**, when the API field name reads badly as a flag (e.g. `is_ai_enabled` as `--is-ai-enabled` or `--ai-enabled`). - **Output changes to a released command**: breaking change with `!` in the commit, or opt-in `--raw-json`. @@ -38,7 +40,7 @@ When no user can answer, as in a scheduled or background run, take the recommend ### 1. Resolve the SDK method -SDK sources live in `node_modules/box-node-sdk/lib/sdk-gen/`. Agent search tools respect `.gitignore` and skip `node_modules`, so use shell `rg` and `ls` there. +SDK sources live in `node_modules/box-node-sdk/lib/sdk-gen/`. Agent search tools respect `.gitignore` and skip `node_modules`, so use shell `grep` and `ls` there. | File | Gives you | | --- | --- | @@ -47,11 +49,11 @@ SDK sources live in `node_modules/box-node-sdk/lib/sdk-gen/`. Agent search tools | `schemas/v2025R0/.d.ts` or `schemas/.d.ts` | Request and response fields | | `client.d.ts` | Manager property name on `this.tsClient` | -To find a method from an API path: `rg -n "'/2.0/hub_collaborations" node_modules/box-node-sdk/lib/sdk-gen/managers/*.js`. To list a manager's methods: `rg -o "^\s+(\w+)\(" -r '$1' node_modules/box-node-sdk/lib/sdk-gen/managers/.d.ts`. +To find a method from an API path: `grep -n "'/2.0/hub_collaborations" node_modules/box-node-sdk/lib/sdk-gen/managers/*.js`. To list a manager's methods with their signatures: `grep -E '^ [A-Za-z0-9]+\(' node_modules/box-node-sdk/lib/sdk-gen/managers/.d.ts | grep -v constructor`. The HTTP method and path also give the `_endpoint` value and the fixture name: lowercase method, path after `/2.0/`, `/` replaced by `_`, path parameters replaced by `id`. `PUT /2.0/hub_collaborations/{id}` becomes `put_hub_collaborations_id`. -Check whether a command already covers the endpoint: `rg -n "_endpoint = 'put_hub_collaborations_id'" src/commands`. A match turns the task into changing that command, so confirm with the user in step 4. +Check whether a command already covers the endpoint: `grep -rn "_endpoint = 'put_hub_collaborations_id'" src/commands`. A match turns the task into changing that command, so confirm with the user in step 4. Done when you know the client property, method, HTTP method and path, the request and response types, and whether a command already exists. @@ -59,7 +61,15 @@ Done when you know the client property, method, HTTP method and path, the reques The SDK types many constrained fields as plain `string`. Hub collaboration `role` is `string` in the SDK, while the API accepts only `editor`, `viewer`, and `co-owner`. The SDK also omits required permissions and scopes, and which errors deserve an explanation in help text. -Check the [Box API reference](https://developer.box.com/reference/) for the endpoint. Every gap left after that becomes a question for step 4. +Read the endpoint in the [Box OpenAPI schema](https://github.com/box/box-openapi), which the SDK and the API reference are generated from. Its `openapi/` directory holds one JSON file per API version: `openapi.json` for endpoints without a version, and `openapi-v.json` for each version. A method ending in `VR` is in `openapi-v..json`, e.g. `V2025R0` in `openapi-v2025.0.json`. List the directory when unsure, since new versions add new files. Download the file: + +```bash +curl -sL https://raw.githubusercontent.com/box/box-openapi/main/openapi/openapi-v2025.0.json -o /tmp/box-openapi.json +``` + +In it, read the operation under `paths`, keyed by the path without `/2.0` and the lowercase HTTP method, e.g. `paths["/hub_collaborations"].post`: its `parameters`, its `requestBody`, and every `$ref` they point to under `components`. + +Take accepted values from `enum`, or from the description when it lists them instead, as it does for hub collaboration `role`. Take defaults from `default`, required fields from `required`, and admin-only endpoints from `x-box-requires-admin`. The schema has no structured scopes or permissions, so read them from the operation description and the [Box API reference](https://developer.box.com/reference/) page. Every gap left after that becomes a question for step 4. Done when every flag has known accepted values, format, default, and permission requirements, or an open question. @@ -67,7 +77,9 @@ Done when every flag has known accepted values, format, default, and permission Read the nearest sibling command in `src/commands//`, its cases in `test/commands/.test.js`, its fixtures in `test/fixtures//`, and `docs/.md`. Then read the section of [references/command-patterns.md](references/command-patterns.md) for your command kind (get, list, create, update, delete, compound flags). -New commands call `this.tsClient`. Most existing commands still call `this.client`, the legacy SDK client, with different method names and output handling. When the nearest sibling uses `this.client`, take its flag names, help text, and test style, and take the SDK call and output code from the reference commands named in command-patterns.md. +Siblings show naming, flags, help text, mocked paths, and fixtures. Take test structure from [references/testing.md](references/testing.md) instead: a `test` chain returned or awaited inside a mocha `it(...)` never runs, so a sibling written that way, and any copy of it, passes whatever it asserts. + +Most existing commands still call `this.client`, the legacy SDK client, with different method names and output handling. When the nearest sibling uses `this.client`, take its flag names and help text, and take the SDK call and output code from the reference commands named in command-patterns.md. When changing an existing command that uses `this.client`, keep it unless the user asks to migrate. ### 4. Propose the plan and ask @@ -95,7 +107,7 @@ Read [references/testing.md](references/testing.md), write the test, then run on ./node_modules/.bin/mocha test/commands/.test.js --grep "" ``` -Done when the test fails for the expected reason: command not found, request mismatch, or wrong output. +Done when each new test fails for the expected reason: command not found, request mismatch, or wrong output. A test that passes before the command exists is not running; fix its structure before going on. ### 6. Implement, then go green diff --git a/.claude/skills/add-box-cli-command/references/testing.md b/.claude/skills/add-box-cli-command/references/testing.md index bc657451..7aa8bd3a 100644 --- a/.claude/skills/add-box-cli-command/references/testing.md +++ b/.claude/skills/add-box-cli-command/references/testing.md @@ -36,7 +36,12 @@ describe('Hubs', function () { }); ``` -Each case is a `test` chain inside a `describe`, ending in `.it(...)`. A chain without `.it(...)` never runs, and the surrounding test passes anyway. Pass `--token=test` so no stored credentials are used, and `--json` so output can be parsed. +Each case is a `test` chain placed directly inside a `describe`, ending in `.it(...)`. These two shapes never run, and the surrounding mocha test passes whatever they assert: + +- `it('…', async function () { return test.command([...]).catch(...); })`: the chain has no `.it(...)`. +- `it('…', async function () { await test.command([...]).it('…'); })`: an `.it(...)` nested inside a running mocha `it` is never scheduled. + +To set up stubs for one case, start the chain with `.do(() => { ... })`, which runs inside the test after `beforeEach`, instead of wrapping the chain in a mocha `it`. Pass `--token=test` so no stored credentials are used, and `--json` so output can be parsed. ## Recipes