From 57417b86f607dab9c2a88d1f74f83d2dae00f763 Mon Sep 17 00:00:00 2001 From: Yuren Ju Date: Mon, 13 Jul 2026 17:27:04 +0900 Subject: [PATCH 1/2] feat(todo): sync spec and regenerate CLI command tree with type commands --- spec/openapi.json | 52 +++++++++++++++++------ src/generated/cli/alias/add.ts | 1 + src/generated/cli/alias/ls.ts | 1 + src/generated/cli/alias/restore.ts | 1 + src/generated/cli/alias/rm.ts | 1 + src/generated/cli/auth/me.ts | 1 + src/generated/cli/domain/add.ts | 1 + src/generated/cli/domain/ls.ts | 1 + src/generated/cli/domain/rm.ts | 1 + src/generated/cli/domain/show.ts | 1 + src/generated/cli/domain/verify.ts | 1 + src/generated/cli/drive/file/history.ts | 1 + src/generated/cli/drive/file/restore.ts | 1 + src/generated/cli/drive/file/rm.ts | 1 + src/generated/cli/drive/library/add.ts | 1 + src/generated/cli/drive/library/ls.ts | 1 + src/generated/cli/drive/library/rm.ts | 1 + src/generated/cli/drive/library/show.ts | 1 + src/generated/cli/drive/library/update.ts | 1 + src/generated/cli/drive/manifest/get.ts | 1 + src/generated/cli/drive/search.ts | 1 + src/generated/cli/email/ls.ts | 1 + src/generated/cli/email/read.ts | 1 + src/generated/cli/email/restore.ts | 1 + src/generated/cli/email/rm.ts | 1 + src/generated/cli/email/show.ts | 1 + src/generated/cli/email/unread.ts | 1 + src/generated/cli/event/add.ts | 1 + src/generated/cli/event/ics.ts | 1 + src/generated/cli/event/ls.ts | 1 + src/generated/cli/event/restore.ts | 1 + src/generated/cli/event/rm.ts | 1 + src/generated/cli/event/set.ts | 1 + src/generated/cli/event/show.ts | 1 + src/generated/cli/index.ts | 8 ++++ src/generated/cli/invite/accept.ts | 1 + src/generated/cli/invite/reject.ts | 1 + src/generated/cli/invite/show.ts | 1 + src/generated/cli/invites.ts | 1 + src/generated/cli/keys/create.ts | 1 + src/generated/cli/keys/edit.ts | 1 + src/generated/cli/keys/ls.ts | 1 + src/generated/cli/keys/rm.ts | 1 + src/generated/cli/org/invite.ts | 1 + src/generated/cli/org/invite/revoke.ts | 1 + src/generated/cli/org/invites.ts | 1 + src/generated/cli/org/members.ts | 1 + src/generated/cli/org/rename.ts | 1 + src/generated/cli/org/show.ts | 1 + src/generated/cli/push/config/rm.ts | 1 + src/generated/cli/push/config/set.ts | 1 + src/generated/cli/push/config/show.ts | 1 + src/generated/cli/push/test.ts | 1 + src/generated/cli/todo/add.ts | 1 + src/generated/cli/todo/comment/add.ts | 1 + src/generated/cli/todo/comment/edit.ts | 1 + src/generated/cli/todo/comment/ls.ts | 1 + src/generated/cli/todo/comment/rm.ts | 1 + src/generated/cli/todo/ls.ts | 1 + src/generated/cli/todo/project/add.ts | 1 + src/generated/cli/todo/project/ls.ts | 1 + src/generated/cli/todo/project/rm.ts | 1 + src/generated/cli/todo/restore.ts | 1 + src/generated/cli/todo/rm.ts | 1 + src/generated/cli/todo/rule/add.ts | 1 + src/generated/cli/todo/rule/ls.ts | 1 + src/generated/cli/todo/rule/rm.ts | 1 + src/generated/cli/todo/rule/show.ts | 1 + src/generated/cli/todo/show.ts | 1 + src/generated/cli/todo/type/add.ts | 34 +++++++++++++++ src/generated/cli/todo/type/ls.ts | 1 + src/generated/cli/todo/type/restore.ts | 27 ++++++++++++ src/generated/cli/todo/type/rm.ts | 27 ++++++++++++ src/generated/cli/todo/type/set.ts | 38 +++++++++++++++++ src/generated/cli/todo/update.ts | 1 + src/generated/sdk/sdk.gen.ts | 11 +++++ test/cli-codegen.test.ts | 18 ++++++++ tools/cli-codegen/emit.ts | 15 ++++++- tools/cli-codegen/main.ts | 1 + 79 files changed, 287 insertions(+), 13 deletions(-) create mode 100644 src/generated/cli/todo/type/add.ts create mode 100644 src/generated/cli/todo/type/restore.ts create mode 100644 src/generated/cli/todo/type/rm.ts create mode 100644 src/generated/cli/todo/type/set.ts diff --git a/spec/openapi.json b/spec/openapi.json index 192e15c..e40e21b 100644 --- a/spec/openapi.json +++ b/spec/openapi.json @@ -30376,7 +30376,8 @@ }, "examples": [ "wspc todo add \"Buy milk\"", - "wspc todo add \"Buy milk\" --project prj_xxx" + "wspc todo add \"Buy milk\" --project prj_xxx", + "wspc todo add \"Fix styling issue\" --type-id typ_xxx --custom-fields '{\"severity\":\"high\",\"tags\":[\"ui\"]}'" ], "display": { "shape": "object", @@ -30395,7 +30396,7 @@ } }, "summary": "Create a todo", - "description": "### 🎯 Overview & Purpose\nCreate a new todo item under a specified project. This can either be a standalone root-level todo or a nested subtask attached to an existing root todo.\n\n### 🔍 When to Use\n* Use this to capture a fresh work item, document an ongoing task, or break a larger root todo into subtasks by creating child todos under it.\n\n### 💡 Key Features & Constraints\n* **One-Level Nesting Limit**: WSPC supports a maximum of one level of task nesting (Root ➔ Child). A root-level todo can have children, but a child todo cannot have further subtasks. Setting a child todo as a parent will fail and trigger a `PARENT_IS_CHILD` error.\n* **Description Handling**: Passing a non-empty string stores the description; passing `\"\"` explicitly stores an empty string. Passing `null` is strictly rejected.\n* **Due Date Format**: Accepts an ISO-8601 date-only format (`YYYY-MM-DD`). Pass `\"\"` or omit the field to skip setting a due date.\n* **Project Binding**: Every todo must belong to a valid active project (`project_id`).\n\n### ⚠️ Common Errors & Troubleshooting\n* **`VALIDATION_ERROR` (HTTP 400)**: Thrown if required fields are missing, if `due_at` violates the `YYYY-MM-DD` format, or if `title` exceeds 500 characters.\n* **`PARENT_IS_CHILD` (HTTP 400)**: Thrown if the target `parent_id` refers to a todo that is itself already a child todo.\n* **`WOULD_CREATE_CYCLE` (HTTP 400)**: Thrown if `parent_id` points to the todo's own ID.\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the specified `project_id` or `parent_id` does not exist or has been soft-deleted.", + "description": "### 🎯 Overview & Purpose\nCreate a new todo item under a specified project. This can either be a standalone root-level todo or a nested subtask attached to an existing root todo.\n\n### 🔍 When to Use\n* Use this to capture a fresh work item, document an ongoing task, or break a larger root todo into subtasks by creating child todos under it.\n\n### 💡 Key Features & Constraints\n* **One-Level Nesting Limit**: WSPC supports a maximum of one level of task nesting (Root ➔ Child). A root-level todo can have children, but a child todo cannot have further subtasks. Setting a child todo as a parent will fail and trigger a `PARENT_IS_CHILD` error.\n* **Description Handling**: Passing a non-empty string stores the description; passing `\"\"` explicitly stores an empty string. Passing `null` is strictly rejected.\n* **Due Date Format**: Accepts an ISO-8601 date-only format (`YYYY-MM-DD`). Pass `\"\"` or omit the field to skip setting a due date.\n* **Project Binding**: Every todo must belong to a valid active project (`project_id`).\n\n### 💡 Best Practices & Guidelines\n* **Descriptive Descriptions**: Always provide a detailed description explaining the task's context, goal, or definition of done. Avoid leaving it empty.\n* **Task Breakdown**: For complex items, split the task into 2-3 logical subtasks (by passing a parent todo ID in `parent_id`). Remember WSPC supports only one level of nesting (Root -> Child).\n* **Integration with Drive**: If a task requires storing large text blocks, research logs, checklists, or files, do not overload the description. Use the Drive feature to store a markdown file (e.g., `research/notes.md`) and place a clean reference link in the description, such as `[Notes](drive:///)`.\n* **Structured Properties (Custom Fields)**: Avoid putting structured meta-attributes (like priority, severity, story points, tags) directly in the description. Instead, use custom todo types (`todo_type`). Create a custom type schema first, then assign it with `type_id` and pass values via `custom_fields`.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`VALIDATION_ERROR` (HTTP 400)**: Thrown if required fields are missing, if `due_at` violates the `YYYY-MM-DD` format, or if `title` exceeds 500 characters.\n* **`PARENT_IS_CHILD` (HTTP 400)**: Thrown if the target `parent_id` refers to a todo that is itself already a child todo.\n* **`WOULD_CREATE_CYCLE` (HTTP 400)**: Thrown if `parent_id` points to the todo's own ID.\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the specified `project_id` or `parent_id` does not exist or has been soft-deleted.", "security": [ { "bearerAuth": [] @@ -31358,8 +31359,18 @@ "TodoTypes" ], "x-cli": { - "command": "_internal", - "hidden": true + "command": "todo type add", + "positional": [ + "label" + ], + "aliases": { + "project": "p" + }, + "examples": [ + "wspc todo type add \"Bug Report\"", + "wspc todo type add \"Bug Report\" --project prj_xxx", + "wspc todo type add \"Sprint Task\" --custom-fields '[{\"key\":\"story_points\",\"label\":\"Story Points\",\"type\":\"string\"}]'" + ] }, "summary": "Create a todo type", "description": "### 🎯 Overview & Purpose\nCreate a new custom todo type. This allows you to define specialized category schemas (e.g. \"Bug Report\") and configure custom field constraints.\n\n### 🔍 When to Use\n* Use this to set up customized task behaviors (e.g. tracking choices, additional metadata, or enforcing hidden fields) tailored to a project.\n\n### 💡 Key Features & Constraints\n* **Automatic Seeding**: The first project initialization will lazily seed a `Default Project` and a `Default` todo type if they do not already exist.\n* **Metadata Schema**: Custom field keys mapped here are evaluated during task creation/update.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`VALIDATION_ERROR` (HTTP 400)**: Thrown if required fields are missing or schema constraints are violated.", @@ -36169,7 +36180,8 @@ ], "examples": [ "wspc todo update tod_xxx --status done", - "wspc todo update tod_xxx --title \"New title\"" + "wspc todo update tod_xxx --title \"New title\"", + "wspc todo update tod_xxx --custom-fields '{\"severity\":\"critical\"}'" ], "display": { "shape": "object", @@ -36188,7 +36200,7 @@ } }, "summary": "Update a todo", - "description": "### 🎯 Overview & Purpose\nUpdate one or more fields of an existing todo item, such as its title, status, parent todo, due date, or description.\n\n### 🔍 When to Use\n* Use this to log progress by changing the status (e.g., to `in_progress` or `done`), reschedule due dates, edit title/description, or reassign/move a task by changing its `parent_id`.\n\n### 💡 Key Features & Constraints\n* **Optimistic Locking (`expected_version`)**: An optional integer representing the version you expect to update. If provided, the server matches it with the current database version. If they match, the update succeeds and increments the version; if they mismatch, a `VERSION_CONFLICT` error is thrown. Omit this field to skip version checking (Last-Write-Wins behavior).\n* **Parent Re-assignment**: Set `parent_id: null` to move a child todo back to the root level.\n* **Status Transitions**: Transitioning the `status` to `done` automatically emits a `captureTodoCompleted` analytics event.\n* **Clearing Fields**: To clear an existing description or due date, explicitly pass `\"\"`. Passing `null` is rejected.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`VERSION_CONFLICT` (HTTP 409)**: Thrown if `expected_version` does not match the current database row version.\n* **`PARENT_IS_CHILD` (HTTP 400)**: Thrown if the new `parent_id` refers to a todo that is itself already a child todo.\n* **`WOULD_CREATE_CYCLE` (HTTP 400)**: Thrown if the update attempts to make a parent todo a child of its own descendant.\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the todo `id` or the new `parent_id` does not exist or has been soft-deleted.", + "description": "### 🎯 Overview & Purpose\nUpdate one or more fields of an existing todo item, such as its title, status, parent todo, due date, or description.\n\n### 🔍 When to Use\n* Use this to log progress by changing the status (e.g., to `in_progress` or `done`), reschedule due dates, edit title/description, or reassign/move a task by changing its `parent_id`.\n\n### 💡 Key Features & Constraints\n* **Optimistic Locking (`expected_version`)**: An optional integer representing the version you expect to update. If provided, the server matches it with the current database version. If they match, the update succeeds and increments the version; if they mismatch, a `VERSION_CONFLICT` error is thrown. Omit this field to skip version checking (Last-Write-Wins behavior).\n* **Parent Re-assignment**: Set `parent_id: null` to move a child todo back to the root level.\n* **Status Transitions**: Transitioning the `status` to `done` automatically emits a `captureTodoCompleted` analytics event.\n* **Clearing Fields**: To clear an existing description or due date, explicitly pass `\"\"`. Passing `null` is rejected.\n\n### 💡 Best Practices & Guidelines\n* **Enriched Context**: Keep the description updated with definition of done, relevant progress notes, or resolution summaries.\n* **Keep Descriptions Clean**: Delegate heavy logs/documents to a Markdown file on Drive, and put a markdown link inside the description.\n* **Structured Properties**: Use custom fields (`custom_fields`) matching the todo's type to record properties like tags, priority, or severity.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`VERSION_CONFLICT` (HTTP 409)**: Thrown if `expected_version` does not match the current database row version.\n* **`PARENT_IS_CHILD` (HTTP 400)**: Thrown if the new `parent_id` refers to a todo that is itself already a child todo.\n* **`WOULD_CREATE_CYCLE` (HTTP 400)**: Thrown if the update attempts to make a parent todo a child of its own descendant.\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the todo `id` or the new `parent_id` does not exist or has been soft-deleted.", "security": [ { "bearerAuth": [] @@ -36595,8 +36607,13 @@ "TodoTypes" ], "x-cli": { - "command": "_internal", - "hidden": true + "command": "todo type rm", + "positional": [ + "id" + ], + "examples": [ + "wspc todo type rm typ_xxx" + ] }, "summary": "Soft-delete a todo type", "description": "### 🎯 Overview & Purpose\nSoft-delete/archive a custom todo type.\n\n### 🔍 When to Use\n* Use this to retire a custom task category workspace that is no longer needed.\n\n### 💡 Key Features & Constraints\n* **Default Type Protection**: The current active default type of a project cannot be deleted. You must assign another type as default first; otherwise the call fails with `CANNOT_DELETE_DEFAULT_TYPE`.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`CANNOT_DELETE_DEFAULT_TYPE` (HTTP 409)**: Thrown if the target todo type is currently the project's default type.\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the target ID does not exist.", @@ -37307,8 +37324,14 @@ "TodoTypes" ], "x-cli": { - "command": "_internal", - "hidden": true + "command": "todo type set", + "positional": [ + "id" + ], + "examples": [ + "wspc todo type set typ_xxx --label \"Feature Request\"", + "wspc todo type set typ_xxx --custom-fields '[{\"key\":\"severity\",\"label\":\"Severity\",\"type\":\"string\"}]'" + ] }, "summary": "Update a todo type", "description": "### 🎯 Overview & Purpose\nUpdate a custom todo type's label, core field overrides, or custom field schema definitions.\n\n### 🔍 When to Use\n* Use this to rename a task category category, hide native todo attributes, or adjust custom data schemas.\n\n### 💡 Key Features & Constraints\n* **Type Modification Constraints**: Changing the data `type` of an existing custom field key (e.g. converting a string field to a boolean field) is strictly rejected with `CANNOT_CHANGE_FIELD_TYPE`. To migrate, remove the key and re-add it under a brand new name.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`CANNOT_CHANGE_FIELD_TYPE` (HTTP 422)**: Thrown if you attempt to modify the declared data type of an existing custom field key.\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the target ID does not exist.", @@ -38464,8 +38487,13 @@ "TodoTypes" ], "x-cli": { - "command": "_internal", - "hidden": true + "command": "todo type restore", + "positional": [ + "id" + ], + "examples": [ + "wspc todo type restore typ_xxx" + ] }, "summary": "Restore a soft-deleted todo type", "description": "### 🎯 Overview & Purpose\nRestore a previously archived/soft-deleted custom todo type.\n\n### 🔍 When to Use\n* Use this to bring a retired task category back into active status.\n\n### 💡 Key Features & Constraints\n* **Task Re-Attachment**: Restoring a type clears its `deleted_at` timestamp. Todo items previously assigned to this type immediately become active and validated under this recovered category schema.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the target ID does not exist.", diff --git a/src/generated/cli/alias/add.ts b/src/generated/cli/alias/add.ts index e2f162c..ffb268d 100644 --- a/src/generated/cli/alias/add.ts +++ b/src/generated/cli/alias/add.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const emailAliasCreateCommand = new Command("add") .description("Create a receiving alias") + .addHelpText("after", "\n### Overview\nReserves and provisions a new passwordless/disposable receiving email alias address under the configured WSPC domain or a fully verified organization custom domain. All inbound emails received on this alias will be forwarded into the caller's inbox.\n\n### When to Use\n- Use this endpoint to spin up a fresh, dedicated email address (e.g., `alice-shop@wspc.app`) for specific websites, newsletters, or contexts to prevent spam or categorize incoming mail.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- **Alias Formatting**: The local part must be between 5 and 32 characters, start with an alphanumeric character, and only contain letters, numbers, dots, underscores, and hyphens.\n- **Custom Domains**: If the address uses a non-platform host, that domain must be registered to the caller's organization and have `status = verified`, `sending_status = verified`, and `receiving_status = verified`.\n- **Limit Check**: Each user is allowed a maximum of 10 active email aliases. Soft-deleted aliases do not count against this quota limit.\n\n### Troubleshooting\n- **401 Unauthorized**: Bearer token is missing, invalid, or expired.\n- **400 Bad Request / INVALID_CHARSET / RESERVED**: The alias local part contains invalid characters, is too short/long, or matches a reserved keyword.\n- **400 Bad Request / ALIAS_DOMAIN_NOT_FOUND**: The custom domain is not registered to the caller's organization.\n- **400 Bad Request / UNVERIFIED_DOMAIN**: The custom domain exists but is not verified yet.\n- **400 Bad Request / ALIAS_DOMAIN_NOT_READY**: The custom domain exists but has not completed sending or receiving verification.\n- **409 Conflict / ALIAS_CONFLICT**: An alias with the exact requested email address already exists globally (whether active or soft-deleted by any user).\n- **429 Too Many Requests / ALIAS_LIMIT_EXCEEDED**: The user has reached the active alias cap limit of 10. A previously deleted alias must be cleaned up or wait for quota availability.\n") .argument("", "email") .action(async (email, opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/alias/ls.ts b/src/generated/cli/alias/ls.ts index 0bd2c5e..4a867ff 100644 --- a/src/generated/cli/alias/ls.ts +++ b/src/generated/cli/alias/ls.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const emailAliasListCommand = new Command("ls") .description("List the caller's aliases") + .addHelpText("after", "\n### Overview\nRetrieves a list of all email receiving aliases owned by the authenticated user.\n\n### When to Use\n- Use this endpoint to render an alias directory in user profiles or to provide a selection list of verified sender aliases in an email compose interface.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- By default, only active receiving aliases are returned. Pass `include_deleted=true` in the query to also fetch soft-deleted aliases (which have `deleted_at` timestamps set).\n\n### Troubleshooting\n- **401 Unauthorized**: Active token is missing, expired, or invalid.\n") .option("--include-deleted", "include_deleted") .action(async (opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/alias/restore.ts b/src/generated/cli/alias/restore.ts index 61cd41d..32fb9b1 100644 --- a/src/generated/cli/alias/restore.ts +++ b/src/generated/cli/alias/restore.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const emailAliasRestoreCommand = new Command("restore") .description("Restore a soft-deleted alias") + .addHelpText("after", "\n### Overview\nReactivates a previously soft-deleted email receiving alias, immediately resuming mail forwarding to the user's inbox.\n\n### When to Use\n- Use this endpoint to re-enable a temporarily disabled alias or to recover one that was deleted by mistake.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- **Quota Check**: Reactivating an alias increases the active alias count towards the user's maximum quota of 10 active aliases. If the limit is exceeded, a `429 ALIAS_LIMIT_EXCEEDED` error is returned.\n- **Path Parameter**: The `@` character in the path parameter must be URL-encoded as `%40`.\n\n### Troubleshooting\n- **401 Unauthorized**: Missing or invalid token.\n- **404 Not Found**: No soft-deleted alias with this exact address was found for the authenticated user.\n- **429 Too Many Requests / ALIAS_LIMIT_EXCEEDED**: Reactivating this alias would exceed the per-user limit of 10 active aliases.\n") .argument("", "email") .action(async (email, opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/alias/rm.ts b/src/generated/cli/alias/rm.ts index 51d2fbd..ab62c3a 100644 --- a/src/generated/cli/alias/rm.ts +++ b/src/generated/cli/alias/rm.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const emailAliasDeleteCommand = new Command("rm") .description("Soft-delete an alias") + .addHelpText("after", "\n### Overview\nSoft-deletes a specific active email receiving alias owned by the caller. Once soft-deleted, the alias stops accepting and forwarding any new inbound emails.\n\n### When to Use\n- Use this endpoint when decommissioning a disposable alias address that is no longer needed or is receiving excessive spam.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- **Data Retention**: Soft-deletion is immediate. Inbound mail forwarding stops, but historical emails previously received on this alias remain fully readable in the inbox.\n- **Restoration**: The alias remains globally reserved and cannot be created fresh by anyone; use `POST /email/aliases/{email}/restore` to reactivate.\n- **Path Parameter**: The `@` character in the `{email}` path parameter must be URL-encoded as `%40`.\n\n### Troubleshooting\n- **401 Unauthorized**: Missing or invalid token.\n- **404 Not Found**: No active alias with this exact address was found for the authenticated user, or the alias is already deleted.\n") .argument("", "email") .action(async (email, opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/auth/me.ts b/src/generated/cli/auth/me.ts index 33d4fc9..eaae96d 100644 --- a/src/generated/cli/auth/me.ts +++ b/src/generated/cli/auth/me.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const authMeCommand = new Command("me") .description("Fetch the user identified by the bearer token") + .addHelpText("after", "\n### Overview\nRetrieves the stable identity profile (user ID, email, and optional display name) of the user associated with the active Bearer token. Works for both long-lived `wspc_*` API keys and OAuth access tokens.\n\n### When to Use\n- Use this endpoint (e.g., in `wspc verify` or `wspc whoami`) to confirm that the active environment's API key or OAuth access token remains valid.\n- Use it in UIs to display the logged-in user's profile details and retrieve the stable `user_id`.\n\n### Constraints\n- Requires a valid Bearer token (either a long-lived `wspc_*` API key or a temporary OAuth access token) in the `Authorization` header.\n- **Response Fields**: The `api_key_id` field is only returned if authenticated via a WSPC API key (prefixed with `wspc_`). OAuth access tokens will omit `api_key_id`. `display_name` is omitted if not configured.\n\n### Troubleshooting\n- **401 Unauthorized**: The Bearer token is missing, malformed, or has been revoked. Ensure the `Authorization` header matches the `Bearer ` format.\n\nExamples:\n $ wspc auth me\n $ wspc auth me --json\n") .action(async (opts) => { const client = await loadSdkClient() const result = await authMe({ diff --git a/src/generated/cli/domain/add.ts b/src/generated/cli/domain/add.ts index e859004..fef9254 100644 --- a/src/generated/cli/domain/add.ts +++ b/src/generated/cli/domain/add.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const emailDomainCreateCommand = new Command("add") .description("Register a custom email domain") + .addHelpText("after", "\n### Overview\nRegisters a new organization-owned custom email domain with the upstream provider and caches the returned DNS verification records in D1.\n\n### When to Use\n- Use this endpoint when onboarding a new custom email domain such as `mail.example.com`.\n- The response contains the DNS records the organization must publish before the domain can be verified.\n- This route registers the domain and returns DNS records. Custom-domain aliases require `status`, `sending_status`, and `receiving_status` to all be `verified`.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- Domain ownership is globally unique across the platform. Once any organization has reserved a domain, another org cannot register it.\n- This route requires custom domain provider credentials in production because it performs a live provider registration call.\n\n### Troubleshooting\n- **400 Bad Request / DOMAIN_INVALID / DOMAIN_RESERVED**: The hostname is malformed or belongs to the platform (`wspc.app` or its subdomains).\n- **409 Conflict / DOMAIN_CONFLICT**: The domain is already registered by some organization.\n- **502 Bad Gateway / DOMAIN_PROVIDER_ERROR**: The upstream provider request failed, timed out, or returned an unexpected shape.\n") .argument("", "domain") .action(async (domain, opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/domain/ls.ts b/src/generated/cli/domain/ls.ts index b6a0fd1..c590921 100644 --- a/src/generated/cli/domain/ls.ts +++ b/src/generated/cli/domain/ls.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const emailDomainListCommand = new Command("ls") .description("List cached custom domains") + .addHelpText("after", "\n### Overview\nReturns the caller organization's cached custom email domains from D1. This route does not call the upstream provider.\n\n### When to Use\n- Use this to render an admin view of all registered domains and their latest known verification state.\n- Use it to inspect DNS records that were previously fetched during create or verify operations.\n- The cached state includes DNS ownership, sending readiness, and receiving readiness used by custom-domain alias creation.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- Results are scoped to the caller organization and sorted newest-first by creation time.\n") .action(async (opts) => { const client = await loadSdkClient() const result = await emailDomainList({ diff --git a/src/generated/cli/domain/rm.ts b/src/generated/cli/domain/rm.ts index 064b1cb..310b937 100644 --- a/src/generated/cli/domain/rm.ts +++ b/src/generated/cli/domain/rm.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const emailDomainDeleteCommand = new Command("rm") .description("Delete a custom email domain") + .addHelpText("after", "\n### Overview\nDeletes one active custom email domain for the caller organization. The worker first confirms no active aliases use the domain, deletes the upstream provider resource, then soft-deletes the cached D1 row.\n\n### When to Use\n- Use this when an organization no longer wants WSPC to manage a custom email domain.\n- Delete is only allowed when no active aliases use the domain.\n- Deleted domains are hidden from active list/get/verify/delete surfaces and cannot be self-restored in this version.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- This route requires custom domain provider credentials in production because it performs a live provider delete call.\n- Provider identifiers and provider raw errors are never returned to the client.\n\n### Troubleshooting\n- **400 Bad Request / DOMAIN_INVALID / DOMAIN_RESERVED**: The path hostname is malformed or reserved.\n- **404 Not Found / DOMAIN_NOT_FOUND**: The domain does not exist, belongs to another organization, or was already deleted.\n- **409 Conflict / DOMAIN_IN_USE**: Active aliases still use this domain.\n- **502 Bad Gateway / DOMAIN_PROVIDER_ERROR**: Provider delete failed, timed out, or credentials are missing.\n") .argument("", "domain") .action(async (domain, opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/domain/show.ts b/src/generated/cli/domain/show.ts index 60eaad4..cdbb8ce 100644 --- a/src/generated/cli/domain/show.ts +++ b/src/generated/cli/domain/show.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const emailDomainGetCommand = new Command("show") .description("Get one cached custom domain") + .addHelpText("after", "\n### Overview\nReturns the caller organization's cached state for one custom email domain. This is a pure D1 read and never calls the upstream provider.\n\n### When to Use\n- Use this to inspect the latest cached DNS records or verification status for a single domain.\n- This cached view includes ownership, sending readiness, and receiving readiness state for custom-domain alias decisions.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- The `{domain}` path parameter is normalized and validated server-side before lookup.\n\n### Troubleshooting\n- **400 Bad Request / DOMAIN_INVALID / DOMAIN_RESERVED**: The path hostname is malformed or reserved.\n- **404 Not Found / DOMAIN_NOT_FOUND**: The domain does not exist or belongs to another organization.\n") .argument("", "domain") .action(async (domain, opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/domain/verify.ts b/src/generated/cli/domain/verify.ts index a256bdd..30c9cae 100644 --- a/src/generated/cli/domain/verify.ts +++ b/src/generated/cli/domain/verify.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const emailDomainVerifyCommand = new Command("verify") .description("Verify a custom domain with the provider") + .addHelpText("after", "\n### Overview\nTriggers an upstream provider verification attempt for one custom email domain, refreshes the cached DNS records/status in D1, and returns the updated row.\nThis route refreshes DNS registration and verification state. Custom-domain aliases require `status`, `sending_status`, and `receiving_status` to all be `verified`.\n\n### When to Use\n- Use this after publishing the required DNS records, or whenever you want to refresh cached provider state explicitly.\n- If the provider verify call returns incomplete DNS records, the worker performs a follow-up provider read before responding.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- This route requires custom domain provider credentials in production because it performs live provider calls.\n- Verification is asynchronous provider work; a successful response may still report `status: pending`.\n- `status: verified` plus `sending_status: verified` enables custom-domain outbound send for active aliases; `receiving_status: verified` is also required before new custom-domain aliases can be created.\n\n### Troubleshooting\n- **400 Bad Request / DOMAIN_INVALID / DOMAIN_RESERVED**: The path hostname is malformed or reserved.\n- **404 Not Found / DOMAIN_NOT_FOUND**: The domain does not exist or belongs to another organization.\n- **502 Bad Gateway / DOMAIN_PROVIDER_ERROR**: Provider verification failed, timed out, or credentials are missing.\n") .argument("", "domain") .action(async (domain, opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/drive/file/history.ts b/src/generated/cli/drive/file/history.ts index 49db4e9..61d55db 100644 --- a/src/generated/cli/drive/file/history.ts +++ b/src/generated/cli/drive/file/history.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const driveFileHistoryCommand = new Command("history") .description("Get drive file version history") + .addHelpText("after", "\nList stored versions of a file, newest first.\n") .argument("", "id") .option("--path ", "path") .action(async (id, opts) => { diff --git a/src/generated/cli/drive/file/restore.ts b/src/generated/cli/drive/file/restore.ts index 9cc6cff..b04e4ec 100644 --- a/src/generated/cli/drive/file/restore.ts +++ b/src/generated/cli/drive/file/restore.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const driveFileRestoreCommand = new Command("restore") .description("Restore a drive file version") + .addHelpText("after", "\nPromote a previous file version to be the current content.\n") .argument("", "id") .option("--path ", "path") .option("--version-id ", "version_id") diff --git a/src/generated/cli/drive/file/rm.ts b/src/generated/cli/drive/file/rm.ts index e24efae..2225daf 100644 --- a/src/generated/cli/drive/file/rm.ts +++ b/src/generated/cli/drive/file/rm.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const driveFileDeleteCommand = new Command("rm") .description("Delete a drive file") + .addHelpText("after", "\nTombstone an active file using optimistic entry version locking.\n") .argument("", "id") .argument("", "path") .option("--expected-entry-version ", "expected_entry_version") diff --git a/src/generated/cli/drive/library/add.ts b/src/generated/cli/drive/library/add.ts index 7f48801..71323aa 100644 --- a/src/generated/cli/drive/library/add.ts +++ b/src/generated/cli/drive/library/add.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const driveLibraryCreateCommand = new Command("add") .description("Create a drive library") + .addHelpText("after", "\nCreate an organization-scoped Drive / Library container.\n") .argument("", "name") .action(async (name, opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/drive/library/ls.ts b/src/generated/cli/drive/library/ls.ts index 0e2b9f7..228e4cf 100644 --- a/src/generated/cli/drive/library/ls.ts +++ b/src/generated/cli/drive/library/ls.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const driveLibraryListCommand = new Command("ls") .description("List drive libraries") + .addHelpText("after", "\nList libraries in the caller organization with cursor pagination.\n") .option("--limit ", "limit") .option("--cursor ", "cursor") .option("--include-deleted ", "include_deleted") diff --git a/src/generated/cli/drive/library/rm.ts b/src/generated/cli/drive/library/rm.ts index 44204a7..67d6e5c 100644 --- a/src/generated/cli/drive/library/rm.ts +++ b/src/generated/cli/drive/library/rm.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const driveLibraryDeleteCommand = new Command("rm") .description("Delete a drive library") + .addHelpText("after", "\nSoft-delete an empty library using optimistic version locking.\n") .argument("", "id") .option("--expected-version ", "expected_version") .action(async (id, opts) => { diff --git a/src/generated/cli/drive/library/show.ts b/src/generated/cli/drive/library/show.ts index 3eeb4b6..e7db899 100644 --- a/src/generated/cli/drive/library/show.ts +++ b/src/generated/cli/drive/library/show.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const driveLibraryGetCommand = new Command("show") .description("Get a drive library") + .addHelpText("after", "\nFetch one active library by id. Cross-org and soft-deleted rows are hidden.\n") .argument("", "id") .action(async (id, opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/drive/library/update.ts b/src/generated/cli/drive/library/update.ts index 437af37..6b68a1c 100644 --- a/src/generated/cli/drive/library/update.ts +++ b/src/generated/cli/drive/library/update.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const driveLibraryUpdateCommand = new Command("update") .description("Update a drive library") + .addHelpText("after", "\nRename a library using optimistic version locking.\n") .argument("", "id") .option("--name ", "name") .option("--expected-version ", "expected_version") diff --git a/src/generated/cli/drive/manifest/get.ts b/src/generated/cli/drive/manifest/get.ts index e40e7ab..b0f5bd7 100644 --- a/src/generated/cli/drive/manifest/get.ts +++ b/src/generated/cli/drive/manifest/get.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const driveManifestGetCommand = new Command("get") .description("Get a drive library manifest") + .addHelpText("after", "\nList file entries for sync using path/id cursor pagination.\n") .argument("", "id") .option("--limit ", "limit") .option("--cursor ", "cursor") diff --git a/src/generated/cli/drive/search.ts b/src/generated/cli/drive/search.ts index a154b81..ac94ccb 100644 --- a/src/generated/cli/drive/search.ts +++ b/src/generated/cli/drive/search.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const driveSearchCommand = new Command("search") .description("Search drive library text") + .addHelpText("after", "\nFull-text search over indexed text files in a library (FTS5).\n") .argument("", "id") .option("--query ", "query") .option("--limit ", "limit") diff --git a/src/generated/cli/email/ls.ts b/src/generated/cli/email/ls.ts index ae68f8b..6768e5b 100644 --- a/src/generated/cli/email/ls.ts +++ b/src/generated/cli/email/ls.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const emailListCommand = new Command("ls") .description("List inbound emails") + .addHelpText("after", "\n### Overview\nRetrieves a paginated directory list of all inbound emails received by the user's active aliases, sorted in descending order of ingestion time (newest first).\n\n### When to Use\n- Use this endpoint to render mailbox dashboards or inbox streams.\n- Use query parameters to perform incremental syncs (via `since` timestamp) or to filter incoming mail by read state or target alias email.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- **Pagination**: Supports cursor-based pagination. Pass the returned `next_cursor` value back as the `cursor` query parameter to list subsequent pages. The `limit` is capped between 1 and 100, defaulting to 20.\n- By default, soft-deleted emails are hidden. Pass `include_deleted=true` to retrieve them.\n\n### Troubleshooting\n- **401 Unauthorized**: Missing, invalid, or expired Bearer token.\n- **400 Bad Request**: Malformed pagination cursor or invalid query parameters (e.g., non-integer limit).\n") .option("--limit ", "Max items to return (clamped to 1-100). Defaults to 20 server-side.") .option("--alias-email ", "If set, only return emails received on this full alias email address.") .option("--unread-only ", "When `true`, only return emails with `is_read=false`.") diff --git a/src/generated/cli/email/read.ts b/src/generated/cli/email/read.ts index 731bc81..d66fe74 100644 --- a/src/generated/cli/email/read.ts +++ b/src/generated/cli/email/read.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const emailMarkReadCommand = new Command("read") .description("Mark inbound emails as read") + .addHelpText("after", "\n### Overview\nMarks a batch of inbound emails as read. This batch operation is fully idempotent.\n\n### When to Use\n- Use this endpoint when a user opens an email detail view or performs a bulk mark-read action in an inbox dashboard.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- Accepts 1 to 100 email IDs in a single call.\n- **Idempotency**: Already-read IDs are silently processed without generating errors but do not count toward the returned `marked` value. Missing, unauthorized, or soft-deleted IDs will be logged in `not_found`.\n\n### Troubleshooting\n- **401 Unauthorized**: Invalid or missing Bearer token.\n- **400 Bad Request**: The request body is malformed or exceeds the maximum limit of 100 IDs.\n") .argument("", "id") .action(async (id, opts) => { const idRaw = id as string[] diff --git a/src/generated/cli/email/restore.ts b/src/generated/cli/email/restore.ts index 7ff54a1..f66acd1 100644 --- a/src/generated/cli/email/restore.ts +++ b/src/generated/cli/email/restore.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const emailRestoreCommand = new Command("restore") .description("Restore soft-deleted inbound emails") + .addHelpText("after", "\n### Overview\nRestores a batch of soft-deleted inbound emails from the trash, making them reappear in standard inbox lists.\n\n### When to Use\n- Use this endpoint to recover email messages that were trashed by mistake.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- Accepts 1 to 100 email IDs. Already-active IDs are silently ignored.\n\n### Troubleshooting\n- **401 Unauthorized**: Invalid token.\n- **400 Bad Request**: Malformed request or batch limit exceeded.\n") .argument("", "id") .action(async (id, opts) => { const idRaw = id as string[] diff --git a/src/generated/cli/email/rm.ts b/src/generated/cli/email/rm.ts index 6a4883b..4ea9cb9 100644 --- a/src/generated/cli/email/rm.ts +++ b/src/generated/cli/email/rm.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const emailDeleteCommand = new Command("rm") .description("Soft-delete inbound emails") + .addHelpText("after", "\n### Overview\nSoft-deletes a batch of inbound emails, moving them to the trash. Soft-deleted emails are immediately excluded from default inbox lists.\n\n### When to Use\n- Use this endpoint to trash one or more email messages from a user's inbox view.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- Accepts 1 to 100 email IDs per call.\n- Deletion is fully reversible: soft-deleted rows persist in the database and can be undeleted using the restore endpoint.\n- **Data Cleanup**: Out-of-band background processes eventually purge associated raw MIME source payloads and attachment bytes from R2; deletion does not immediately free storage.\n\n### Troubleshooting\n- **401 Unauthorized**: Invalid Bearer token.\n- **400 Bad Request**: The request body is malformed or exceeds the maximum limit of 100 IDs.\n") .argument("", "id") .action(async (id, opts) => { const idRaw = id as string[] diff --git a/src/generated/cli/email/show.ts b/src/generated/cli/email/show.ts index 9b074be..2de187d 100644 --- a/src/generated/cli/email/show.ts +++ b/src/generated/cli/email/show.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const emailGetCommand = new Command("show") .description("Get an inbound email by id") + .addHelpText("after", "\n### Overview\nFetches the metadata and plain-text body of a single inbound email by its unique ID. It also returns metadata for all associated attachments and optionally resolves the rendered HTML content.\n\n### When to Use\n- Use this endpoint to display the complete detail view of an email message.\n- Use it to extract attachment files or read complex HTML layouts.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- **R2 HTML Read**: The HTML body is stored in Object Storage (R2). To fetch it, explicitly pass `include_html=true` (this incurs an extra R2 read charge; leave unset if only plain text is needed).\n- Returns a 404 error if the email has been soft-deleted, unless `include_deleted=true` is set.\n\n### Troubleshooting\n- **401 Unauthorized**: Missing or expired token.\n- **404 Not Found**: The specified email ID does not exist, belongs to another user, or has been soft-deleted (without `include_deleted=true`).\n") .argument("", "id") .option("--include-html ", "When `true`, fetch the HTML body from R2 and include it as `html_body` in the response. Costs an extra R2 read; omit if you only need text.") .option("--include-deleted ", "When `true`, allow fetching a soft-deleted email. Defaults to `false` (returns 404 for soft-deleted rows).") diff --git a/src/generated/cli/email/unread.ts b/src/generated/cli/email/unread.ts index 1552030..ff43f92 100644 --- a/src/generated/cli/email/unread.ts +++ b/src/generated/cli/email/unread.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const emailMarkUnreadCommand = new Command("unread") .description("Mark inbound emails as unread") + .addHelpText("after", "\n### Overview\nResets a batch of inbound emails back to an unread state.\n\n### When to Use\n- Use this endpoint to undo an accidental read marking or to mark messages for later review.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- Accepts 1 to 100 email IDs per call. Already-unread IDs are silently ignored but do not contribute to `marked`.\n\n### Troubleshooting\n- **401 Unauthorized**: Invalid Bearer token.\n- **400 Bad Request**: Malformed body or ID batch size limit exceeded.\n") .argument("", "id") .action(async (id, opts) => { const idRaw = id as string[] diff --git a/src/generated/cli/event/add.ts b/src/generated/cli/event/add.ts index b7b1482..6d85325 100644 --- a/src/generated/cli/event/add.ts +++ b/src/generated/cli/event/add.ts @@ -9,6 +9,7 @@ import { parseAttendee } from "../../../handwritten/utils/parse-attendee.js" export const eventCreateCommand = new Command("add") .description("Schedule a calendar event") + .addHelpText("after", "\n### Overview\nCreate a new calendar event owned by the authenticated user.\n\n### When to Use\nBook a meeting, lunch, all-day trip, or any time-bound item. Optionally provide `attendees` to automatically dispatch invitation emails containing an `.ics` REQUEST attachment to each participant as a side effect.\n\n### Constraints\n- **Format Integrity**: `start` and `end` must be of the exact same type (both ISO 8601 datetimes with offset, or both ISO date-only for all-day).\n- **Chronological Order**: `end` must be strictly after `start`.\n- **All-Day boundary**: All-day events use RFC 5545 exclusive end (e.g., a one-day event on June 1st is specified as `start=2026-06-01` and `end=2026-06-02`).\n- **Attendee Limit**: Up to 50 unique attendees are supported after case-insensitive email address deduplication.\n\n### Troubleshooting\n- Returns 400 `VALIDATION_ERROR` if `start` and `end` format mismatch, or if `end <= start`.\n- Returns 400 `ATTENDEE_LIMIT_EXCEEDED` if more than 50 unique attendees are supplied.\n- Invitation emails are processed and dispatched asynchronously via Cloudflare `waitUntil`; the analytics counter `event_created` is emitted automatically.\n\nExamples:\n $ wspc event add \"Lunch with Alice\" --start \"tomorrow 12:30pm\" --end \"tomorrow 1:30pm\"\n $ wspc event add \"Team offsite\" --all-day --start 2026-06-01 --end 2026-06-01\n") .argument("", "title") .option("--description <value>", "Free-form notes about the event (agenda, dial-in instructions, etc.). Markdown formatted (CommonMark + GFM tables, strikethrough, task lists); stored verbatim. Invitation emails include the raw source — most email clients display it as plain text.") .option("--start <value>", "Accepts ISO 8601 datetime with offset (e.g. `2026-06-01T12:30:00+08:00`) for timed events, or ISO date-only (e.g. `2026-06-01`) for all-day. The `wspc` CLI additionally accepts natural-language phrases (`tomorrow 12:30pm`, `next Monday 9am`) and resolves them to ISO before sending; the server itself only accepts ISO. All-day uses RFC 5545 exclusive end: a one-day event on 6/1 is `start=2026-06-01, end=2026-06-02`; both endpoints must be the same type.") diff --git a/src/generated/cli/event/ics.ts b/src/generated/cli/event/ics.ts index 8967b53..c13949d 100644 --- a/src/generated/cli/event/ics.ts +++ b/src/generated/cli/event/ics.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const eventIcsDownloadCommand = new Command("ics") .description("Download event as `.ics`") + .addHelpText("after", "\n### Overview\nReturn a single event rendered as an RFC 5545 `.ics` file suitable for import into major calendar clients.\n\n### When to Use\nExpose a 'Save to my calendar' link in email notifications, show a download button in a UI, or programmatically forward raw iCalendar text to third parties.\n\n### Constraints\n- **Router Match**: The path parameter `filename` must be exactly `<event_id>.ics`. The `.ics` suffix is strictly required for the router to match this endpoint ahead of the JSON detail endpoint.\n- **Response Payload**: The response is plain text containing the iCalendar specification, NOT JSON. The `Content-Type` is set to `text/calendar; charset=utf-8` with `Content-Disposition: inline; filename=\"<event_id>.ics\"`.\n- **Authentication**: Standard authentication is required (Bearer API key or OAuth access token), as this endpoint is secure.\n\n### Troubleshooting\n- Returns 404 `NOT_FOUND` if the event does not exist, is soft-deleted, or is owned by another user.\n\nExamples:\n $ wspc event ics evt_xxx > event.ics\n") .argument("<id>", "id") .action(async (id, opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/event/ls.ts b/src/generated/cli/event/ls.ts index 09b5d83..3815e06 100644 --- a/src/generated/cli/event/ls.ts +++ b/src/generated/cli/event/ls.ts @@ -7,6 +7,7 @@ import { parseTimeInput, resolveTimezone } from "../../../handwritten/utils/pars export const eventListCommand = new Command("ls") .description("List calendar events") + .addHelpText("after", "\n### Overview\nReturn the authenticated user's events, ordered by `start` ascending, with cursor pagination.\n\n### When to Use\nRender calendar list/grid views, search for specific terms using full-text search, query events within a specific time window, or retrieve historically past events.\n\n### Constraints\n- **Default Visibility**: By default, soft-deleted events and past events (events where `end` is before the current time) are automatically hidden.\n- **Time Bounds Override**: Supplying any explicit time bound query parameter (`start_from`, `start_to`, `end_from`, `end_to`) or passing `include_past=true` overrides and disables the implicit past filter.\n- **Search Scope**: `q` performs a case-insensitive substring search across `title`, `description`, and `location`.\n- **Pagination**: The `limit` query parameter is clamped to `[1, 200]`; cursor pagination is enabled via the opaque `cursor` parameter.\n\n### Troubleshooting\n- Returns 400 `VALIDATION_ERROR` if date query bounds are invalid (e.g. `start_from > start_to` or `end_from > end_to`).\n\nExamples:\n $ wspc event ls\n $ wspc event ls --from \"today\" --to \"next week\"\n") .option("--q <value>", "Optional full-text search across title, description, and location (case-insensitive substring).") .option("--from <value>", "Inclusive lower bound on the event `start` (ISO datetime with offset, or ISO date-only). When ANY of `start_from`/`start_to`/`end_from`/`end_to` is provided, the implicit past filter is disabled.") .option("--to <value>", "Inclusive upper bound on the event `start`.") diff --git a/src/generated/cli/event/restore.ts b/src/generated/cli/event/restore.ts index 47b5cd0..bd2da2f 100644 --- a/src/generated/cli/event/restore.ts +++ b/src/generated/cli/event/restore.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const eventRestoreCommand = new Command("restore") .description("Restore a soft-deleted event") + .addHelpText("after", "\n### Overview\nRestore a previously soft-deleted calendar event, making it active and visible in default list queries.\n\n### When to Use\nRecover an event that was accidentally soft-deleted.\n\n### Constraints\n- **Optimistic Locking**: Supports optional optimistic locking via `expected_version` in the request body.\n- **Side Effects**: When restoring an event with attendees, all participants will asynchronously receive a new invitation email containing an `.ics` REQUEST attachment via Cloudflare `waitUntil`.\n- **State Integrity**: You cannot restore a live (non-deleted) event. Restoring a live event is treated as an error.\n\n### Troubleshooting\n- Returns 404 `NOT_FOUND` if the event does not exist, or is NOT currently soft-deleted.\n- Returns 409 `VERSION_CONFLICT` if `expected_version` does not match the database value.\n") .argument("<id>", "id") .option("--expected-version <value>", "Optional optimistic lock. Omit to let the server use the current version; pass only to fail with 409 `VERSION_CONFLICT` if someone else has mutated the event since you last read.") .action(async (id, opts) => { diff --git a/src/generated/cli/event/rm.ts b/src/generated/cli/event/rm.ts index 939b6fd..050220d 100644 --- a/src/generated/cli/event/rm.ts +++ b/src/generated/cli/event/rm.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const eventDeleteCommand = new Command("rm") .description("Soft-delete a calendar event") + .addHelpText("after", "\n### Overview\nSoft-delete an existing calendar event, hiding it from default listings.\n\n### When to Use\nRemove an event entirely from the user's historical view and calendar client. If the meeting was cancelled but should remain in history (notifying participants of cancellation), use `PATCH /calendar/events/{id}` with `status: cancelled` instead.\n\n### Constraints\n- **Optimistic Locking**: Supports optional optimistic locking via `expected_version` in the request body.\n- **Side Effects**: When soft-deleting an event with attendees, all participants will asynchronously receive a cancellation email containing an `.ics` CANCEL attachment via Cloudflare `waitUntil`.\n- **Recovery**: The record is preserved in the database as a soft-deleted row and can be brought back using `POST /calendar/events/{id}/restore`.\n\n### Troubleshooting\n- Returns 404 `NOT_FOUND` if the event does not exist, or has already been soft-deleted.\n- Returns 409 `VERSION_CONFLICT` if `expected_version` does not match the database value.\n\nExamples:\n $ wspc event rm evt_xxx\n") .argument("<id>", "id") .option("--expected-version <value>", "Optional optimistic lock. Omit to let the server use the current version; pass only to fail with 409 `VERSION_CONFLICT` if someone else has mutated the event since you last read.") .action(async (id, opts) => { diff --git a/src/generated/cli/event/set.ts b/src/generated/cli/event/set.ts index f49f0f9..e981d18 100644 --- a/src/generated/cli/event/set.ts +++ b/src/generated/cli/event/set.ts @@ -9,6 +9,7 @@ import { parseAttendee } from "../../../handwritten/utils/parse-attendee.js" export const eventUpdateCommand = new Command("set") .description("Update a calendar event") + .addHelpText("after", "\n### Overview\nPartially update fields of an existing calendar event. All properties in the request body are optional.\n\n### When to Use\nReschedule an event, update its location or notes, cancel the meeting (retaining the record but notifying participants), or replace/update the attendee list.\n\n### Constraints\n- **Optimistic Locking**: Pass `expected_version` to fail with `VERSION_CONFLICT` if another mutation occurred concurrently since you last read. Omit to let the server force the update.\n- **Field Clearing**: Pass an empty string `\"\"` for `description`, `location`, or `url` to clear those fields in the database.\n- **Attendee replacement**: Providing the `attendees` property fully REPLACES the existing participant list. The server automatically diffs participants and asynchronously sends invitations (for newly added), updates (for kept), or cancellations (for removed) via Cloudflare `waitUntil`.\n- **Validation Rules**: Mismatched start/end formats or chronological order violations will fail the request.\n- **Attendee Limit**: A maximum of 50 unique attendees is allowed.\n\n### Troubleshooting\n- Returns 404 `NOT_FOUND` if the event does not exist or is soft-deleted.\n- Returns 409 `VERSION_CONFLICT` if `expected_version` is provided but stale.\n- Returns 400 `VALIDATION_ERROR` if `start` and `end` kinds do not match, or if `end <= start`.\n- Returns 400 `ATTENDEE_LIMIT_EXCEEDED` if unique attendees exceed 50.\n\nExamples:\n $ wspc event set evt_xxx --start \"tomorrow 1pm\" --end \"tomorrow 2pm\"\n $ wspc event set evt_xxx --status cancelled\n") .argument("<id>", "id") .option("--expected-version <value>", "Optional optimistic lock. Omit to let the server use the current version; pass only to fail the call if someone else has mutated the event since you last read. On mismatch the server returns 409 `VERSION_CONFLICT` and includes the current and sent versions in the message.") .option("--title <value>", "New event title. Omit to leave unchanged.") diff --git a/src/generated/cli/event/show.ts b/src/generated/cli/event/show.ts index e403d0e..164cbec 100644 --- a/src/generated/cli/event/show.ts +++ b/src/generated/cli/event/show.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const eventGetCommand = new Command("show") .description("Get a calendar event by id") + .addHelpText("after", "\n### Overview\nFetch a single calendar event by its unique ID, returning the complete record including all attendees.\n\n### When to Use\nDisplay a detailed view of an event, or read the latest database state (capturing `version`) prior to executing an optimistic-locked PATCH update.\n\n### Constraints\n- **Deleted Events**: Soft-deleted events return 404 `NOT_FOUND` by default. You must explicitly pass `include_deleted=true` in the query parameters to retrieve a soft-deleted row.\n- **ICS Format**: To download the RFC 5545 iCalendar text representation of this event, use `GET /calendar/events/{id}.ics` instead.\n\n### Troubleshooting\n- Returns 404 `NOT_FOUND` if the event does not exist, is soft-deleted (without `include_deleted=true`), or is owned by another user.\n\nExamples:\n $ wspc event show evt_xxx\n") .argument("<id>", "id") .option("--include-deleted <value>", "When `true`, return the row even if soft-deleted. Default `false` (returns 404).") .action(async (id, opts) => { diff --git a/src/generated/cli/index.ts b/src/generated/cli/index.ts index 483a933..a94be03 100644 --- a/src/generated/cli/index.ts +++ b/src/generated/cli/index.ts @@ -59,6 +59,7 @@ import { recurrenceRuleCreateCommand } from "./todo/rule/add.js" import { recurrenceRuleListCommand } from "./todo/rule/ls.js" import { todoCreateCommand } from "./todo/add.js" import { todoListCommand } from "./todo/ls.js" +import { todoTypeCreateCommand } from "./todo/type/add.js" import { todoTypeListCommand } from "./todo/type/ls.js" import { todoCommentDeleteCommand } from "./todo/comment/rm.js" import { todoCommentUpdateCommand } from "./todo/comment/edit.js" @@ -68,7 +69,10 @@ import { recurrenceRuleGetCommand } from "./todo/rule/show.js" import { todoDeleteCommand } from "./todo/rm.js" import { todoGetCommand } from "./todo/show.js" import { todoUpdateCommand } from "./todo/update.js" +import { todoTypeDeleteCommand } from "./todo/type/rm.js" +import { todoTypeUpdateCommand } from "./todo/type/set.js" import { todoRestoreCommand } from "./todo/restore.js" +import { todoTypeRestoreCommand } from "./todo/type/restore.js" export function registerGeneratedCommands(root: Command): void { const root_invite = root.command("invite").description("invite commands") @@ -155,7 +159,11 @@ export function registerGeneratedCommands(root: Command): void { root_todo.addCommand(todoCreateCommand) root_todo.addCommand(todoListCommand) const root_todo_type = root_todo.command("type").description("type commands") + root_todo_type.addCommand(todoTypeCreateCommand) root_todo_type.addCommand(todoTypeListCommand) + root_todo_type.addCommand(todoTypeDeleteCommand) + root_todo_type.addCommand(todoTypeUpdateCommand) + root_todo_type.addCommand(todoTypeRestoreCommand) root_todo.addCommand(todoDeleteCommand) root_todo.addCommand(todoGetCommand) root_todo.addCommand(todoUpdateCommand) diff --git a/src/generated/cli/invite/accept.ts b/src/generated/cli/invite/accept.ts index e67b23e..e0e3d9b 100644 --- a/src/generated/cli/invite/accept.ts +++ b/src/generated/cli/invite/accept.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const inviteAcceptCommand = new Command("accept") .description("Accept an invite and switch into the inviting organization") + .addHelpText("after", "\nSwitches the caller's org to the invite's org and records the previous org. The caller loses access to data scoped to their previous org.\n\nExamples:\n $ wspc invite accept inv_...\n") .argument("<id>", "id") .action(async (id, opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/invite/reject.ts b/src/generated/cli/invite/reject.ts index 68e3497..ba7c396 100644 --- a/src/generated/cli/invite/reject.ts +++ b/src/generated/cli/invite/reject.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const inviteRejectCommand = new Command("reject") .description("Reject an invite") + .addHelpText("after", "\nRejects an organization invite addressed to the caller. The invite will be marked as rejected.\n\nExamples:\n $ wspc invite reject inv_...\n") .argument("<id>", "id") .action(async (id, opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/invite/show.ts b/src/generated/cli/invite/show.ts index 5181485..82a1f4a 100644 --- a/src/generated/cli/invite/show.ts +++ b/src/generated/cli/invite/show.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const inviteGetCommand = new Command("show") .description("Get a single invite addressed to the caller") + .addHelpText("after", "\nRetrieves the metadata of a specific organization invite addressed to the caller by its ID.\n\nExamples:\n $ wspc invite show inv_...\n") .argument("<id>", "id") .action(async (id, opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/invites.ts b/src/generated/cli/invites.ts index 6c1d8e9..5b7a2a2 100644 --- a/src/generated/cli/invites.ts +++ b/src/generated/cli/invites.ts @@ -6,6 +6,7 @@ import { render } from "../../handwritten/output/render.js" export const invitesListCommand = new Command("invites") .description("List invites addressed to the authenticated user's email") + .addHelpText("after", "\nRetrieves all pending or expired organization invites addressed to the caller's verified email address.\n") .action(async (opts) => { const client = await loadSdkClient() const result = await invitesList({ diff --git a/src/generated/cli/keys/create.ts b/src/generated/cli/keys/create.ts index ad210b3..67e38d7 100644 --- a/src/generated/cli/keys/create.ts +++ b/src/generated/cli/keys/create.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const keyCreateCommand = new Command("create") .description("Create a new API key (full value returned once)") + .addHelpText("after", "\n### Overview\nCreates and provisions a new long-lived API key for the authenticated user. The complete plaintext API key value (`api_key`) is returned **only once** in this endpoint's response and cannot be retrieved again.\n\n### When to Use\n- Use this endpoint when a user requests a new API key (e.g., `wspc keys create --label \"My Agent\"`) to isolate access for specific environments, applications, or developers.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- **Key Limit**: A user is limited to a maximum of 25 active API keys. Requesting a new key beyond this limit will result in a `KEY_LIMIT_EXCEEDED` error.\n- **Label Validation**: The `label` parameter must be between 1 and 60 characters after trimming whitespace. Failing to provide a valid label results in an `INVALID_LABEL` error.\n\n### Troubleshooting\n- **401 Unauthorized**: The Bearer token is missing or invalid.\n- **400 Bad Request**: The `label` parameter is empty, too long, or missing.\n- **400 Bad Request (Limit Exceeded)**: The user has hit the maximum limit of 25 active keys. An existing active key must be revoked before creating a new one.\n") .option("--label <value>", "Human-readable label for the new key (1–60 chars after trimming). Pick something that identifies where the key will live — agent name, machine, or environment — so you can recognise it later in `wspc keys list`.") .action(async (opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/keys/edit.ts b/src/generated/cli/keys/edit.ts index 1bae0c0..5807ce8 100644 --- a/src/generated/cli/keys/edit.ts +++ b/src/generated/cli/keys/edit.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const keyUpdateCommand = new Command("edit") .description("Update an active API key's label") + .addHelpText("after", "\n### Overview\nUpdates the human-readable label of an active API key. Only active (non-revoked) keys owned by the authenticated user can be updated.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- **Label Validation**: The `label` parameter must be between 1 and 60 characters after trimming whitespace. Failing to provide a valid label results in an `INVALID_LABEL` error.\n") .argument("<id>", "id") .option("--label <value>", "Human-readable label for the key (1–60 chars after trimming).") .action(async (id, opts) => { diff --git a/src/generated/cli/keys/ls.ts b/src/generated/cli/keys/ls.ts index b5daf61..db44176 100644 --- a/src/generated/cli/keys/ls.ts +++ b/src/generated/cli/keys/ls.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const keyListCommand = new Command("ls") .description("List active API keys") + .addHelpText("after", "\n### Overview\nReturns a list of all active (non-revoked) API keys belonging to the authenticated user. It also includes the `current_key_id` identifying the specific key used to authenticate the current request.\n\n### When to Use\n- Use this endpoint to view active API keys (e.g., when running `wspc keys list` or displaying API key management screens in user profiles).\n- Use the `current_key_id` to identify which key is making the current call, facilitating self-rotation or auditing.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- Only active keys are returned; keys that have been revoked are filtered out and excluded from the response.\n- The full secret key is never returned; only the last 4 characters (`key_last4`) are provided for identification.\n\n### Troubleshooting\n- **401 Unauthorized**: The provided Bearer token is missing, expired, or invalid. Ensure you are passing a valid, active API key.\n") .action(async (opts) => { const client = await loadSdkClient() const result = await keyList({ diff --git a/src/generated/cli/keys/rm.ts b/src/generated/cli/keys/rm.ts index 3f1ff7e..5682d78 100644 --- a/src/generated/cli/keys/rm.ts +++ b/src/generated/cli/keys/rm.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const keyRevokeCommand = new Command("rm") .description("Soft-revoke an API key") + .addHelpText("after", "\n### Overview\nPermanently revokes an active API key by its unique ID. Once revoked, the key becomes immediately invalid and will be rejected by all services.\n\n### When to Use\n- Use this endpoint to permanently deactivate an API key (e.g., when running `wspc keys revoke <id>`) due to token rotation, key leakage, or decommissioning of a machine/service.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- Revocation is permanent and cannot be undone.\n- A user can revoke any key they own, including the one they are currently using to make the call. If they revoke the current key, subsequent requests using that key will return `401 Unauthorized`.\n\n### Troubleshooting\n- **401 Unauthorized**: The active token is missing, expired, or invalid.\n- **404 Not Found**: The specified key ID does not exist, belongs to another user, or has already been revoked.\n- **400 Bad Request**: The `id` path parameter format is invalid. It must be in the format `key_<ULID>`.\n") .argument("<id>", "id") .action(async (id, opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/org/invite.ts b/src/generated/cli/org/invite.ts index 4a8b9ef..d0625c5 100644 --- a/src/generated/cli/org/invite.ts +++ b/src/generated/cli/org/invite.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const orgInviteCreateCommand = new Command("invite") .description("Invite an email to join the caller's organization") + .addHelpText("after", "\nCreates a pending invite for `email` and sends an invite email. Idempotent for an existing pending invite. The invitee accepts after signing in with the invited email.\n\nExamples:\n $ wspc org invite bob@example.com\n") .option("--email <value>", "Email address to invite into the caller's organization.") .action(async (opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/org/invite/revoke.ts b/src/generated/cli/org/invite/revoke.ts index 5546cb5..565bcce 100644 --- a/src/generated/cli/org/invite/revoke.ts +++ b/src/generated/cli/org/invite/revoke.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const orgInviteRevokeCommand = new Command("revoke") .description("Revoke a pending invite") + .addHelpText("after", "\nPermanently revokes a pending organization invite. The invitee will no longer be able to accept it.\n\nExamples:\n $ wspc org invite revoke inv_...\n") .argument("<id>", "id") .action(async (id, opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/org/invites.ts b/src/generated/cli/org/invites.ts index b86feab..55f8c07 100644 --- a/src/generated/cli/org/invites.ts +++ b/src/generated/cli/org/invites.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const orgInvitesListCommand = new Command("invites") .description("List invites issued by the caller's organization") + .addHelpText("after", "\nRetrieves a list of all active pending or expired organization invites issued by the caller's organization.\n") .action(async (opts) => { const client = await loadSdkClient() const result = await orgInvitesList({ diff --git a/src/generated/cli/org/members.ts b/src/generated/cli/org/members.ts index 06d95b9..aff01e6 100644 --- a/src/generated/cli/org/members.ts +++ b/src/generated/cli/org/members.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const orgMembersListCommand = new Command("members") .description("List members of the authenticated user's organization") + .addHelpText("after", "\n### Overview\nRetrieves a paginated list of all members belonging to the authenticated user's organization, including their basic profile information, emails, and roles.\n\n### When to Use\n- Use this endpoint to list members in command-line tools (e.g., `wspc org members ls`) or to display a team directory in a user dashboard.\n- Use this to paginate through large lists of organization members using cursor-based pagination.\n\n### Constraints\n- Requires a valid Bearer token in the `Authorization` header.\n- In the current version (v1), organizations are single-user only, meaning this endpoint will always return exactly one member (the caller).\n- **Pagination**: Supports cursor-based pagination. The `limit` query parameter must be a positive integer, defaulting to 50 and capped at a maximum of 100. Pass `cursor` from the previous response's `next_cursor` to fetch subsequent pages.\n\n### Troubleshooting\n- **401 Unauthorized**: The Bearer token is invalid or has expired.\n- **400 Bad Request**: The query parameters `limit` or `cursor` are malformed. Ensure `limit` is an integer between 1 and 100.\n- **404 Not Found**: The organization associated with this user was not found.\n") .option("--cursor <value>", "Opaque pagination cursor. Pass the `next_cursor` returned by the previous page to fetch the next slice. Omit on the first call.") .option("--limit <value>", "Maximum members to return. Clamped to [1, 100]. Defaults to 50.") .action(async (opts) => { diff --git a/src/generated/cli/org/rename.ts b/src/generated/cli/org/rename.ts index 6106a0a..de6f70f 100644 --- a/src/generated/cli/org/rename.ts +++ b/src/generated/cli/org/rename.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const orgUpdateCommand = new Command("rename") .description("Update the authenticated user's organization") + .addHelpText("after", "\n### Overview\nUpdates the metadata (currently, the name) of the organization associated with the authenticated user.\n\n### Constraints\n- Requires a valid Bearer token.\n- The organization name cannot be empty or purely whitespace.\n- Maximum length is capped by `MAX_ORG_NAME_LEN`.\n\nExamples:\n $ wspc org rename \"New Name\"\n $ wspc org rename \"New Name\" --json\n") .option("--name <value>", "The new name for the organization. Cannot be empty or purely whitespace.") .action(async (opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/org/show.ts b/src/generated/cli/org/show.ts index 9d952b6..ff3c4f0 100644 --- a/src/generated/cli/org/show.ts +++ b/src/generated/cli/org/show.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const orgGetCommand = new Command("show") .description("Get the authenticated user's organization") + .addHelpText("after", "\n### Overview\nReturns the metadata of the organization owned by the authenticated user. In the current version, this represents the user's personal organization space containing all their projects and tokens.\n\n### When to Use\n- Use this endpoint to retrieve the organization ID and name for display or context setup (e.g., when running `wspc org show` or rendering user dashboards).\n- Use this to verify that the API token / credentials are linked to a valid organization.\n\n### Constraints\n- Requires a valid Bearer token (API Key or Session Token) in the `Authorization` header.\n- In the current API version (v1), every user is automatically provisioned a single personal organization. Selecting or switching organizations is not supported.\n\n### Troubleshooting\n- **401 Unauthorized**: The provided Bearer token is missing, expired, or invalid. Verify your `Authorization` header format (`Bearer <token>`).\n- **403 Forbidden**: The token does not have access to read organization metadata.\n- **404 Not Found**: The organization associated with this token could not be found or has been deactivated.\n\nExamples:\n $ wspc org show\n $ wspc org show --json\n") .action(async (opts) => { const client = await loadSdkClient() const result = await orgGet({ diff --git a/src/generated/cli/push/config/rm.ts b/src/generated/cli/push/config/rm.ts index d60078e..1d184ef 100644 --- a/src/generated/cli/push/config/rm.ts +++ b/src/generated/cli/push/config/rm.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const pushConfigDeleteCommand = new Command("rm") .description("Remove a push transport") + .addHelpText("after", "\n### Overview\nDelete the configured push transport row, immediately halting push event dispatching for the caller.\n\n### When to Use\nWhen a user disconnects their notification channel, turns off push preferences, or resets their transport target.\n\n### Constraints\n- **Idempotency**: Deleting a transport that has not been registered (or was already deleted) is handled as a no-op, returning 204 `No Content`.\n- **Side Effects**: Hard-deletes the `(user_id, transport)` configuration record and completely purges all associated test history (`last_test_at` and `last_test_status`).\n- **Transport Support**: The path parameter must be a recognized transport identifier.\n\n### Troubleshooting\n- Returns 400 `UNKNOWN_TRANSPORT` if the transport parameter contains an unrecognized transport identifier.\n") .argument("<transport>", "transport") .action(async (transport, opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/push/config/set.ts b/src/generated/cli/push/config/set.ts index e93b4ae..658e5c4 100644 --- a/src/generated/cli/push/config/set.ts +++ b/src/generated/cli/push/config/set.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const pushConfigSetCommand = new Command("set") .description("Register or update a push transport") + .addHelpText("after", "\n### Overview\nUpsert a notification transport configuration for the authenticated user. After registration, wspc can dispatch notifications to the user when registered product events fire.\n\n### When to Use\nFirst-time onboarding push configuration setup, or whenever the user updates their transport target details (e.g., pointing notifications to a new Telegram bot username).\n\n### Constraints\n- **Supported Transports**: Currently only `transport: telegram` is supported.\n- **Target Validation**: `target_bot_username` must be a valid Telegram bot name starting with `@` followed by 5–32 alphanumeric/underscore characters (`^@[A-Za-z0-9_]{5,32}$`).\n- **Uniqueness**: Up to one registration row is saved per `(user_id, transport)`. Upserting replaces any existing target config, updating `updated_at` while retaining `created_at`.\n- **No Side-effect Messages**: Registering a transport does **not** send a test notification; clients should separately trigger `POST /push/test`.\n\n### Troubleshooting\n- Returns 400 `INVALID_CONFIG` if payload structure is invalid or `target_bot_username` validation fails.\n") .option("--transport <value>", "Transport discriminator. `telegram` is the only supported value today — push delivers via a Telegram bot DM. Future transports (web push, iOS/Android, generic webhook) will be added as additional discriminator values.") .option("--target-bot-username <value>", "Telegram bot username (with leading `@`, 5–32 alphanumeric/underscore characters). This is the bot the user has already started a chat with — wspc DMs notifications to it via the Telegram Bot API.") .action(async (opts) => { diff --git a/src/generated/cli/push/config/show.ts b/src/generated/cli/push/config/show.ts index 96a46f1..52de591 100644 --- a/src/generated/cli/push/config/show.ts +++ b/src/generated/cli/push/config/show.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const pushConfigGetCommand = new Command("show") .description("List the caller's push transports") + .addHelpText("after", "\n### Overview\nRetrieve all active push transport configurations registered for the authenticated user.\n\n### When to Use\nRender settings page, determine if push notifications are enabled before prompting the user, or fetch historical health check results (`last_test_at` and `last_test_status`).\n\n### Constraints\n- **List Limitations**: Currently returns at most one active registration row (`telegram`).\n- **Data Security**: Response payload contains sensitive data (e.g. `target_bot_username`). Callers must handle these values as user secret-equivalent and prevent leakage.\n\n### Troubleshooting\n- Standard 401 Unauthorized or 403 Forbidden checks if authentication credentials are missing or invalid.\n") .action(async (opts) => { const client = await loadSdkClient() const result = await pushConfigGet({ diff --git a/src/generated/cli/push/test.ts b/src/generated/cli/push/test.ts index 0d52713..a766f06 100644 --- a/src/generated/cli/push/test.ts +++ b/src/generated/cli/push/test.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const pushTestCommand = new Command("test") .description("Send a test push notification") + .addHelpText("after", "\n### Overview\nSynchronously dispatch a static test message via the requested transport target to verify delivery health.\n\n### When to Use\nImmediately after executing `POST /push/config` to verify connection legitimacy, or when troubleshooting missing notification claims.\n\n### Constraints\n- **Target Requirement**: You must have already successfully registered the targeted transport configuration.\n- **Side Effects**: Sends a single probe message to the upstream provider (e.g. Telegram Bot API). Test details are persisted to the configuration row under `last_test_at` and `last_test_status`.\n- **No Audit Footprint**: This operation is treated strictly as an integration probe and will not generate a product audit log footprint.\n\n### Troubleshooting\n- **Upstream Error Handling**: This endpoint returns an HTTP `200 OK` status even if the upstream dispatch fails. Callers must inspect `ok: false` and review `status` and `detail` in the response JSON to verify connection health.\n- Returns 404 `NO_CONFIG` if the user has not registered configuration details for the requested transport.\n") .option("--transport <value>", "Which transport to send the test message through. Must match a transport the caller has already registered via `POST /push/config`; today only `telegram` is supported.") .action(async (opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/todo/add.ts b/src/generated/cli/todo/add.ts index 72a544a..8288805 100644 --- a/src/generated/cli/todo/add.ts +++ b/src/generated/cli/todo/add.ts @@ -7,6 +7,7 @@ import { parseJsonField } from "../../../handwritten/utils/parse-json-field.js" export const todoCreateCommand = new Command("add") .description("Create a todo") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nCreate a new todo item under a specified project. This can either be a standalone root-level todo or a nested subtask attached to an existing root todo.\n\n### 🔍 When to Use\n* Use this to capture a fresh work item, document an ongoing task, or break a larger root todo into subtasks by creating child todos under it.\n\n### 💡 Key Features & Constraints\n* **One-Level Nesting Limit**: WSPC supports a maximum of one level of task nesting (Root ➔ Child). A root-level todo can have children, but a child todo cannot have further subtasks. Setting a child todo as a parent will fail and trigger a `PARENT_IS_CHILD` error.\n* **Description Handling**: Passing a non-empty string stores the description; passing `\"\"` explicitly stores an empty string. Passing `null` is strictly rejected.\n* **Due Date Format**: Accepts an ISO-8601 date-only format (`YYYY-MM-DD`). Pass `\"\"` or omit the field to skip setting a due date.\n* **Project Binding**: Every todo must belong to a valid active project (`project_id`).\n\n### 💡 Best Practices & Guidelines\n* **Descriptive Descriptions**: Always provide a detailed description explaining the task's context, goal, or definition of done. Avoid leaving it empty.\n* **Task Breakdown**: For complex items, split the task into 2-3 logical subtasks (by passing a parent todo ID in `parent_id`). Remember WSPC supports only one level of nesting (Root -> Child).\n* **Integration with Drive**: If a task requires storing large text blocks, research logs, checklists, or files, do not overload the description. Use the Drive feature to store a markdown file (e.g., `research/notes.md`) and place a clean reference link in the description, such as `[Notes](drive://<library>/<path>)`.\n* **Structured Properties (Custom Fields)**: Avoid putting structured meta-attributes (like priority, severity, story points, tags) directly in the description. Instead, use custom todo types (`todo_type`). Create a custom type schema first, then assign it with `type_id` and pass values via `custom_fields`.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`VALIDATION_ERROR` (HTTP 400)**: Thrown if required fields are missing, if `due_at` violates the `YYYY-MM-DD` format, or if `title` exceeds 500 characters.\n* **`PARENT_IS_CHILD` (HTTP 400)**: Thrown if the target `parent_id` refers to a todo that is itself already a child todo.\n* **`WOULD_CREATE_CYCLE` (HTTP 400)**: Thrown if `parent_id` points to the todo's own ID.\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the specified `project_id` or `parent_id` does not exist or has been soft-deleted.\n\nExamples:\n $ wspc todo add \"Buy milk\"\n $ wspc todo add \"Buy milk\" --project prj_xxx\n $ wspc todo add \"Fix styling issue\" --type-id typ_xxx --custom-fields '{\"severity\":\"high\",\"tags\":[\"ui\"]}'\n") .argument("<title>", "title") .option("-p, --project <value>", "Project id to assign this todo to. It must be an active project in the caller's organization.") .option("--description <value>", "Free-form details about the todo. Fully supports GFM Markdown (tables, strikethrough, task lists). Stored verbatim; client applications are responsible for rendering. Optional. Passing `null` is strictly rejected.") diff --git a/src/generated/cli/todo/comment/add.ts b/src/generated/cli/todo/comment/add.ts index b0d1e95..dfa26e1 100644 --- a/src/generated/cli/todo/comment/add.ts +++ b/src/generated/cli/todo/comment/add.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const todoCommentCreateCommand = new Command("add") .description("Add a comment to a todo") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nAttach a free-text comment to a todo. Use this to record progress updates, notes, or remarks as a task moves along.\n\n### 💡 Key Features & Constraints\n* **Free text**: Comments are plain text up to 10000 characters; there is no separate \"progress\" vs \"remark\" type.\n* **Authorship**: The author is recorded as the calling user (`user_id`).\n\n### ⚠️ Common Errors & Troubleshooting\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the target todo does not exist or is soft-deleted.\n* **`VALIDATION_ERROR` (HTTP 400)**: Thrown if content is empty or exceeds 10000 characters.\n\nExamples:\n $ wspc todo comment add tod_01HW3K \"Verified on staging\"\n") .argument("<id>", "id") .argument("<content>", "content") .action(async (id, content, opts) => { diff --git a/src/generated/cli/todo/comment/edit.ts b/src/generated/cli/todo/comment/edit.ts index 1254102..aa74860 100644 --- a/src/generated/cli/todo/comment/edit.ts +++ b/src/generated/cli/todo/comment/edit.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const todoCommentUpdateCommand = new Command("edit") .description("Edit a comment") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nEdit the body of an existing comment.\n\n### 💡 Key Features & Constraints\n* **Last write wins**: There is no optimistic-lock version on comments; the latest edit replaces the content.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`COMMENT_NOT_FOUND` (HTTP 404)**: Thrown if the comment id is unknown, soft-deleted, or not in the caller's organization.\n* **`VALIDATION_ERROR` (HTTP 400)**: Thrown if content is empty or exceeds 10000 characters.\n\nExamples:\n $ wspc todo comment edit tdc_01HW3K \"Edited note\"\n") .argument("<id>", "id") .argument("<content>", "content") .action(async (id, content, opts) => { diff --git a/src/generated/cli/todo/comment/ls.ts b/src/generated/cli/todo/comment/ls.ts index e98f908..cbf9a1d 100644 --- a/src/generated/cli/todo/comment/ls.ts +++ b/src/generated/cli/todo/comment/ls.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const todoCommentListCommand = new Command("ls") .description("List comments on a todo") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nList the comments attached to a todo, oldest-first by default.\n\n### 💡 Key Features & Constraints\n* **Ordering**: Defaults to chronological (`asc`). Pass `order=desc` for newest-first.\n* **Soft-deleted**: Hidden by default; pass `include_deleted=true` to include them.\n* **Pagination**: Use `limit` (max 200, default 50) and `cursor` (the `next_cursor` from a previous response) to page through results. When `next_cursor` is absent in the response, you are on the last page. Returns `{ comments, next_cursor? }`. Changing `order` invalidates a cursor.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the target todo does not exist or is soft-deleted.\n* **`VALIDATION_ERROR`**: Thrown if a cursor was produced with a different `order` than the current request.\n\nExamples:\n $ wspc todo comment ls tod_01HW3K\n") .argument("<id>", "id") .option("--order <value>", "order") .option("--include-deleted <value>", "include_deleted") diff --git a/src/generated/cli/todo/comment/rm.ts b/src/generated/cli/todo/comment/rm.ts index 4898668..e297b19 100644 --- a/src/generated/cli/todo/comment/rm.ts +++ b/src/generated/cli/todo/comment/rm.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const todoCommentDeleteCommand = new Command("rm") .description("Soft-delete a comment") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nSoft-delete a comment.\n\n### 💡 Key Features & Constraints\n* **Soft delete**: The comment is hidden from default listings but retained; there is no restore endpoint.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`COMMENT_NOT_FOUND` (HTTP 404)**: Thrown if the comment id is unknown, already deleted, or not in the caller's organization.\n\nExamples:\n $ wspc todo comment rm tdc_01HW3K\n") .argument("<id>", "id") .action(async (id, opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/todo/ls.ts b/src/generated/cli/todo/ls.ts index c4961dd..f1beb34 100644 --- a/src/generated/cli/todo/ls.ts +++ b/src/generated/cli/todo/ls.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const todoListCommand = new Command("ls") .description("List todos with filters") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nReturn the caller's active or archived todos, with comprehensive options to filter by project, parent task, status, due-date window, and template visibility.\n\n### 🔍 When to Use\n* Use this to render the main todo board dashboard, query items due in a specific timeframe (using `due_after` and `due_before`), or lazy-load subtasks for an expanded parent todo by passing its ID.\n\n### 💡 Key Features & Constraints\n* **Required Parameter**: The `project_id` query parameter is strictly required and must match an active project.\n* **Parent Tasks**: Omitting `parent_id` lists root-level todos by default. Pass a todo id to list direct children of that specific task.\n* **Multi-Status Filters**: Multi-value `status` query is supported by repeating the parameter, e.g., `?status=open&status=in_progress`.\n* **Due-Date Windowing**: The `due_after` filter is inclusive, while `due_before` is exclusive, forming a half-open window `[due_after, due_before)`. Both parameters exclude todos with no due date.\n* **Template & Soft-Delete Visibility**: Soft-deleted todos are hidden unless `include_deleted=true`. Template todos backing recurrence rules are hidden unless `include_templates=true`.\n* **Custom-Field Filters (`cf.<key>=<value>`)**: Repeatable dynamic-prefix query parameters whose name follows the `cf.<key>` pattern (e.g. `?cf.priority=high&cf.team=eng`). Each pair is ANDed; for `string_array` custom fields the match is positive when the array contains the value. Keys must be declared on the project's todo type schema. Because the prefix is dynamic, these parameters cannot be expressed in the JSON Schema below — clients must construct them from the URL query string directly.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`VALIDATION_ERROR` (HTTP 400)**: Thrown if `project_id` is missing, or if query parameters fail schema validation.\n\nExamples:\n $ wspc todo ls\n $ wspc todo ls --status open\n $ wspc todo ls --project prj_xxx\n") .option("-p, --project <value>", "Filter by project. Required. Unknown, cross-organization, or soft-deleted project ids return NOT_FOUND.") .option("--user-id <value>", "user_id") .option("--parent-id <value>", "parent_id") diff --git a/src/generated/cli/todo/project/add.ts b/src/generated/cli/todo/project/add.ts index 7716f4d..d0a7d59 100644 --- a/src/generated/cli/todo/project/add.ts +++ b/src/generated/cli/todo/project/add.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const projectCreateCommand = new Command("add") .description("Create a project") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nEstablish a new isolated project workspace.\n\n### 🔍 When to Use\n* Use this to set up a new domain, team project, or separate workspace area to isolate tasks, custom types, and recurrence rules.\n\n### 💡 Key Features & Constraints\n* **Project Partitioning**: Projects act as strict boundaries. Custom todo types and recurrence rules created under this project are strictly confined to it.\n* **Name Uniqueness**: Project names are free-form and do not have to be unique.\n* **Default Type Inheritance**: Omit `default_todo_type_id` to automatically inherit the Default Project's default task type.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`VALIDATION_ERROR` (HTTP 400)**: Thrown if required fields are missing, if name is empty, or if name length constraints are violated.\n\nExamples:\n $ wspc todo project add \"Personal\"\n") .argument("<name>", "name") .option("--default-todo-type-id <value>", "default_todo_type_id") .action(async (name, opts) => { diff --git a/src/generated/cli/todo/project/ls.ts b/src/generated/cli/todo/project/ls.ts index 521e6d3..bca7c1a 100644 --- a/src/generated/cli/todo/project/ls.ts +++ b/src/generated/cli/todo/project/ls.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const projectListCommand = new Command("ls") .description("List projects") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nList all project workspaces available to the authenticated organization or user.\n\n### 🔍 When to Use\n* Use this to populate project switcher dropdown menus, load side navigation views, or find valid project IDs before listing other scoped resources.\n\n### 💡 Key Features & Constraints\n* **Archived Visibility**: Soft-deleted projects are omitted from default listings. Pass `include_deleted=true` to include them for auditing or recovery dashboards.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`AUTH_REQUIRED` (HTTP 401)**: Thrown if the caller is not authenticated.\n\nExamples:\n $ wspc todo project ls\n") .option("--include-deleted <value>", "Set to `true` to include soft-deleted projects in the response.") .action(async (opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/todo/project/rm.ts b/src/generated/cli/todo/project/rm.ts index 7086f92..a34d809 100644 --- a/src/generated/cli/todo/project/rm.ts +++ b/src/generated/cli/todo/project/rm.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const projectDeleteCommand = new Command("rm") .description("Soft-delete a project") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nSoft-delete/archive a project workspace.\n\n### 🔍 When to Use\n* Use this to archive a completed project and hide it from default listings without losing historical metrics.\n\n### 💡 Key Features & Constraints\n* **Cascading Effects**: Deleting a project automatically soft-deletes the project record and cascades to soft-delete all todos created under it.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the project ID does not exist or has already been archived.\n\nExamples:\n $ wspc todo project rm prj_01HW3K\n") .argument("<id>", "id") .action(async (id, opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/todo/restore.ts b/src/generated/cli/todo/restore.ts index 011d278..b72cc35 100644 --- a/src/generated/cli/todo/restore.ts +++ b/src/generated/cli/todo/restore.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const todoRestoreCommand = new Command("restore") .description("Restore a soft-deleted todo") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nReverse a previous soft-delete. The todo (and optionally its descendants) is recovered back to the active list.\n\n### 🔍 When to Use\n* Use this to recover a task deleted by mistake, or pull a task out of the trash to continue active work.\n\n### 💡 Key Features & Constraints\n* **Orphan Warning**: If the restored todo's parent is still in the trash, the call succeeds but returns `parent_in_trash_warning: true`, signaling that the restored todo is currently orphaned from a visible ancestor.\n* **Cascading Restore (`cascade`)**: If `cascade: true` is provided, all descendants still in the trash are also restored. Otherwise, descendants are left in the trash, and their count is reported back in `descendants_in_trash_count`.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`VERSION_CONFLICT` (HTTP 409)**: Thrown if `expected_version` is supplied and mismatches the database.\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the target todo `id` does not exist or has already been permanently purged.\n\nExamples:\n $ wspc todo restore tod_xxx\n $ wspc todo restore tod_xxx --cascade\n") .argument("<id>", "id") .option("--expected-version <value>", "expected_version") .option("--cascade <value>", "cascade") diff --git a/src/generated/cli/todo/rm.ts b/src/generated/cli/todo/rm.ts index dbadc8f..d41198f 100644 --- a/src/generated/cli/todo/rm.ts +++ b/src/generated/cli/todo/rm.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const todoDeleteCommand = new Command("rm") .description("Soft-delete a todo") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nSoft-delete a todo item so that it no longer appears in active list queries. The record remains in the database and can be recovered later.\n\n### 🔍 When to Use\n* Use this to hide an item from your active listings without permanently losing the history or metrics.\n\n### 💡 Key Features & Constraints\n* **Cascading Delete (`cascade`)**: If the target todo has active child subtasks:\n - If `cascade: false` (default), the deletion will fail and throw a `HAS_CHILDREN` error to prevent accidental orphaned tasks.\n - If `cascade: true`, the target todo and all its nested child subtasks will be soft-deleted together.\n* **Optimistic Locking**: You may optionally pass `expected_version` to ensure the todo has not been modified since you last read it.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`HAS_CHILDREN` (HTTP 400)**: Thrown if you attempt to delete a parent todo that has active subtasks without explicitly setting `cascade: true`.\n* **`VERSION_CONFLICT` (HTTP 409)**: Thrown if `expected_version` is provided and mismatches the database.\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the target todo `id` does not exist or has already been soft-deleted.\n\nExamples:\n $ wspc todo rm tod_xxx\n") .argument("<id>", "id") .option("--expected-version <value>", "expected_version") .option("--cascade <value>", "cascade") diff --git a/src/generated/cli/todo/rule/add.ts b/src/generated/cli/todo/rule/add.ts index a6ff49c..e3a1651 100644 --- a/src/generated/cli/todo/rule/add.ts +++ b/src/generated/cli/todo/rule/add.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const recurrenceRuleCreateCommand = new Command("add") .description("Create a recurring todo rule") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nCreate a recurrence rule that materializes upcoming todo instances on a repeating schedule.\n\n### 🔍 When to Use\n* Use this to set up recurring work like a weekly Standup, monthly reporting, or cyclical maintenance. The server automatically materializes upcoming todo instances on a 14-day rolling horizon.\n\n### 💡 Key Features & Constraints\n* **RFC-5545 Conformity**: The `rrule` parameter must be a valid RFC-5545 schedule string (e.g., `FREQ=WEEKLY;BYDAY=MO`) and must **not** include the `DTSTART` or `TZID` directive.\n* **Anchor Date**: `dtstart` specifies the local calendar starting date (`YYYY-MM-DD`) where the schedule rule is anchored.\n* **Nesting Constraints**: Recurrence rules can only be bound to root-level tasks. Child tasks (subtasks) cannot have recurrence rules. Setting a child task as a parent will trigger `PARENT_IS_CHILD`.\n* **Instance Independence**: Once materialized, each todo instance is fully independent with its own `status` and `due_at`.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`RRULE_INVALID` (HTTP 400)**: Thrown if the `rrule` schedule string is broken or contains illegal `DTSTART` directives.\n* **`PARENT_IS_CHILD` (HTTP 400)**: Thrown if the specified `parent_id` points to a child task.\n* **`VALIDATION_ERROR` (HTTP 400)**: Thrown if date format is invalid or required fields are missing.\n\nExamples:\n $ wspc todo rule add \"Weekly review\" --rrule \"FREQ=WEEKLY;BYDAY=MO\" --dtstart 2026-05-18 --project prj_xxx\n $ wspc todo rule add \"Weekly review\" --rrule \"FREQ=WEEKLY;BYDAY=MO\" --dtstart 2026-05-18 --project prj_xxx --type typ_xxx\n") .argument("<title>", "title") .option("--rrule <value>", "rrule") .option("--dtstart <value>", "dtstart") diff --git a/src/generated/cli/todo/rule/ls.ts b/src/generated/cli/todo/rule/ls.ts index 3d1676e..79a2ae3 100644 --- a/src/generated/cli/todo/rule/ls.ts +++ b/src/generated/cli/todo/rule/ls.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const recurrenceRuleListCommand = new Command("ls") .description("List recurring todo rules") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nReturn all active recurrence rules within a specific project owned by the caller.\n\n### 🔍 When to Use\n* Use this to render rule management panels, list scheduled automation templates, or inspect active rules.\n\n### 💡 Key Features & Constraints\n* **Project Scope**: The `project_id` query parameter is strictly required.\n* **Exclusion**: Soft-deleted/archived rules are excluded from the response by default.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`VALIDATION_ERROR` (HTTP 400)**: Thrown if `project_id` query filter is omitted.\n\nExamples:\n $ wspc todo rule ls\n") .option("--project-id <value>", "Project id filter. Required. Unknown, cross-organization, or soft-deleted project ids return NOT_FOUND.") .option("--user-id <value>", "user_id") .action(async (opts) => { diff --git a/src/generated/cli/todo/rule/rm.ts b/src/generated/cli/todo/rule/rm.ts index a4ae3d8..0dab82f 100644 --- a/src/generated/cli/todo/rule/rm.ts +++ b/src/generated/cli/todo/rule/rm.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const recurrenceRuleDeleteCommand = new Command("rm") .description("Delete a recurring todo rule") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nSoft-delete/delete a recurrence rule to immediately halt future task materialization.\n\n### 🔍 When to Use\n* Use this to permanently end an ongoing cyclical schedule automation (e.g., when a weekly standby rotation is retired).\n\n### 💡 Key Features & Constraints\n* **Historic Preservation**: Deleting a rule stops the rolling schedule generations, but **does not** delete or alter todo tasks that have already been materialized. They remain on the user's list.\n* **Optimistic Locking**: Supports optional `expected_version` checks.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`VERSION_CONFLICT` (HTTP 409)**: Thrown if `expected_version` mismatches the database.\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the target rule ID does not exist.\n\nExamples:\n $ wspc todo rule rm tdr_xxx\n $ wspc todo rule rm tdr_xxx --expected-version 3\n") .argument("<id>", "id") .option("--expected-version <value>", "expected_version") .action(async (id, opts) => { diff --git a/src/generated/cli/todo/rule/show.ts b/src/generated/cli/todo/rule/show.ts index df1f402..88b7b30 100644 --- a/src/generated/cli/todo/rule/show.ts +++ b/src/generated/cli/todo/rule/show.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const recurrenceRuleGetCommand = new Command("show") .description("Get a recurring todo rule") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nFetch a single recurrence rule along with its template todo snapshot and the count of materialized instances.\n\n### 🔍 When to Use\n* Use this to inspect rule details before editing, preview the task template that future occurrences will copy, or check the current materialization metrics.\n\n### 💡 Key Features & Constraints\n* **Snapshot Integrity**: The returned template represents a schema template snapshot — modifying the rule (PATCH) only alters future occurrences; already-materialized tasks are never mutated retroactively.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the specified rule ID does not exist.\n\nExamples:\n $ wspc todo rule show tdr_xxx\n") .argument("<id>", "id") .action(async (id, opts) => { const client = await loadSdkClient() diff --git a/src/generated/cli/todo/show.ts b/src/generated/cli/todo/show.ts index 0397c71..ea32c8f 100644 --- a/src/generated/cli/todo/show.ts +++ b/src/generated/cli/todo/show.ts @@ -6,6 +6,7 @@ import { render } from "../../../handwritten/output/render.js" export const todoGetCommand = new Command("show") .description("Get a todo by id") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nFetch the full details of a single todo item by its unique identifier.\n\n### 🔍 When to Use\n* Use this to confirm the current state of a task, inspect nested field values, or retrieve its current `version` before issuing an optimistic update (PATCH).\n\n### 💡 Key Features & Constraints\n* **Soft-Deleted Recovery**: A soft-deleted todo will return an HTTP 404 unless the query parameter `?include_deleted=true` is explicitly supplied.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the specified todo `id` does not exist, or has been soft-deleted and the request did not supply `include_deleted=true`.\n\nExamples:\n $ wspc todo show tod_xxx\n") .argument("<id>", "id") .option("--include-deleted <value>", "include_deleted") .option("--include-orphan-fields <value>", "include_orphan_fields") diff --git a/src/generated/cli/todo/type/add.ts b/src/generated/cli/todo/type/add.ts new file mode 100644 index 0000000..9d7afed --- /dev/null +++ b/src/generated/cli/todo/type/add.ts @@ -0,0 +1,34 @@ +// AUTO-GENERATED — DO NOT EDIT (source: todo_type_create) +import { Command } from "commander" +import { todoTypeCreate } from "../../../sdk/index.js" +import { loadSdkClient } from "../../../../handwritten/auth/load-sdk-client.js" +import { render } from "../../../../handwritten/output/render.js" +import { parseJsonField } from "../../../../handwritten/utils/parse-json-field.js" + +export const todoTypeCreateCommand = new Command("add") + .description("Create a todo type") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nCreate a new custom todo type. This allows you to define specialized category schemas (e.g. \"Bug Report\") and configure custom field constraints.\n\n### 🔍 When to Use\n* Use this to set up customized task behaviors (e.g. tracking choices, additional metadata, or enforcing hidden fields) tailored to a project.\n\n### 💡 Key Features & Constraints\n* **Automatic Seeding**: The first project initialization will lazily seed a `Default Project` and a `Default` todo type if they do not already exist.\n* **Metadata Schema**: Custom field keys mapped here are evaluated during task creation/update.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`VALIDATION_ERROR` (HTTP 400)**: Thrown if required fields are missing or schema constraints are violated.\n\nExamples:\n $ wspc todo type add \"Bug Report\"\n $ wspc todo type add \"Bug Report\" --project prj_xxx\n $ wspc todo type add \"Sprint Task\" --custom-fields '[{\"key\":\"story_points\",\"label\":\"Story Points\",\"type\":\"string\"}]'\n") + .argument("<label>", "label") + .option("-p, --project <value>", "Project this type belongs to. Required. It must be an active project in the caller's organization.") + .option("--hide-core-fields <value>", "hide_core_fields") + .option("--custom-fields <value>", "custom_fields") + .action(async (label, opts) => { + const client = await loadSdkClient() + const result = await todoTypeCreate({ + client: (client as unknown as { _rawClient: unknown })._rawClient as never, + body: { + label, + project_id: opts.project, + hide_core_fields: parseJsonField(opts.hideCoreFields, "hide-core-fields"), + custom_fields: parseJsonField(opts.customFields, "custom-fields"), + }, + }) + if (result.error || !result.response?.ok) { + process.stderr.write( + `HTTP ${result.response?.status ?? "?"}: ${JSON.stringify(result.error ?? "unknown error", null, 2)}\n`, + ) + process.exitCode = 1 + return + } + render({ kind: "todo_type_create", display: undefined }, result.data) + }) diff --git a/src/generated/cli/todo/type/ls.ts b/src/generated/cli/todo/type/ls.ts index b61365e..75a70f8 100644 --- a/src/generated/cli/todo/type/ls.ts +++ b/src/generated/cli/todo/type/ls.ts @@ -6,6 +6,7 @@ import { render } from "../../../../handwritten/output/render.js" export const todoTypeListCommand = new Command("ls") .description("List todo types") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nList custom todo types defined within a project.\n\n### 🔍 When to Use\n* Use this to populate task type selection dropdown elements or load category metadata for dynamic custom forms.\n\n### 💡 Key Features & Constraints\n* **Required Parameter**: The `project_id` filter is strictly required and must match an active project.\n* **Exclusion**: Soft-deleted types are excluded by default. Pass `include_deleted=true` to surface archived rows for a recovery UI.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`VALIDATION_ERROR` (HTTP 400)**: Thrown if `project_id` query parameter is omitted.\n\nExamples:\n $ wspc todo type ls\n") .option("--project-id <value>", "Project id filter. Required. Unknown, cross-organization, or soft-deleted project ids return NOT_FOUND.") .option("--user-id <value>", "user_id") .option("--include-deleted <value>", "include_deleted") diff --git a/src/generated/cli/todo/type/restore.ts b/src/generated/cli/todo/type/restore.ts new file mode 100644 index 0000000..9f56680 --- /dev/null +++ b/src/generated/cli/todo/type/restore.ts @@ -0,0 +1,27 @@ +// AUTO-GENERATED — DO NOT EDIT (source: todo_type_restore) +import { Command } from "commander" +import { todoTypeRestore } from "../../../sdk/index.js" +import { loadSdkClient } from "../../../../handwritten/auth/load-sdk-client.js" +import { render } from "../../../../handwritten/output/render.js" + +export const todoTypeRestoreCommand = new Command("restore") + .description("Restore a soft-deleted todo type") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nRestore a previously archived/soft-deleted custom todo type.\n\n### 🔍 When to Use\n* Use this to bring a retired task category back into active status.\n\n### 💡 Key Features & Constraints\n* **Task Re-Attachment**: Restoring a type clears its `deleted_at` timestamp. Todo items previously assigned to this type immediately become active and validated under this recovered category schema.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the target ID does not exist.\n\nExamples:\n $ wspc todo type restore typ_xxx\n") + .argument("<id>", "id") + .action(async (id, opts) => { + const client = await loadSdkClient() + const result = await todoTypeRestore({ + client: (client as unknown as { _rawClient: unknown })._rawClient as never, + path: { + id, + }, + }) + if (result.error || !result.response?.ok) { + process.stderr.write( + `HTTP ${result.response?.status ?? "?"}: ${JSON.stringify(result.error ?? "unknown error", null, 2)}\n`, + ) + process.exitCode = 1 + return + } + render({ kind: "todo_type_restore", display: undefined }, result.data) + }) diff --git a/src/generated/cli/todo/type/rm.ts b/src/generated/cli/todo/type/rm.ts new file mode 100644 index 0000000..a460b77 --- /dev/null +++ b/src/generated/cli/todo/type/rm.ts @@ -0,0 +1,27 @@ +// AUTO-GENERATED — DO NOT EDIT (source: todo_type_delete) +import { Command } from "commander" +import { todoTypeDelete } from "../../../sdk/index.js" +import { loadSdkClient } from "../../../../handwritten/auth/load-sdk-client.js" +import { render } from "../../../../handwritten/output/render.js" + +export const todoTypeDeleteCommand = new Command("rm") + .description("Soft-delete a todo type") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nSoft-delete/archive a custom todo type.\n\n### 🔍 When to Use\n* Use this to retire a custom task category workspace that is no longer needed.\n\n### 💡 Key Features & Constraints\n* **Default Type Protection**: The current active default type of a project cannot be deleted. You must assign another type as default first; otherwise the call fails with `CANNOT_DELETE_DEFAULT_TYPE`.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`CANNOT_DELETE_DEFAULT_TYPE` (HTTP 409)**: Thrown if the target todo type is currently the project's default type.\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the target ID does not exist.\n\nExamples:\n $ wspc todo type rm typ_xxx\n") + .argument("<id>", "id") + .action(async (id, opts) => { + const client = await loadSdkClient() + const result = await todoTypeDelete({ + client: (client as unknown as { _rawClient: unknown })._rawClient as never, + path: { + id, + }, + }) + if (result.error || !result.response?.ok) { + process.stderr.write( + `HTTP ${result.response?.status ?? "?"}: ${JSON.stringify(result.error ?? "unknown error", null, 2)}\n`, + ) + process.exitCode = 1 + return + } + render({ kind: "todo_type_delete", display: undefined }, result.data) + }) diff --git a/src/generated/cli/todo/type/set.ts b/src/generated/cli/todo/type/set.ts new file mode 100644 index 0000000..5b6a3a4 --- /dev/null +++ b/src/generated/cli/todo/type/set.ts @@ -0,0 +1,38 @@ +// AUTO-GENERATED — DO NOT EDIT (source: todo_type_update) +import { Command } from "commander" +import { todoTypeUpdate } from "../../../sdk/index.js" +import { loadSdkClient } from "../../../../handwritten/auth/load-sdk-client.js" +import { render } from "../../../../handwritten/output/render.js" +import { parseJsonField } from "../../../../handwritten/utils/parse-json-field.js" + +export const todoTypeUpdateCommand = new Command("set") + .description("Update a todo type") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nUpdate a custom todo type's label, core field overrides, or custom field schema definitions.\n\n### 🔍 When to Use\n* Use this to rename a task category category, hide native todo attributes, or adjust custom data schemas.\n\n### 💡 Key Features & Constraints\n* **Type Modification Constraints**: Changing the data `type` of an existing custom field key (e.g. converting a string field to a boolean field) is strictly rejected with `CANNOT_CHANGE_FIELD_TYPE`. To migrate, remove the key and re-add it under a brand new name.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`CANNOT_CHANGE_FIELD_TYPE` (HTTP 422)**: Thrown if you attempt to modify the declared data type of an existing custom field key.\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the target ID does not exist.\n\nExamples:\n $ wspc todo type set typ_xxx --label \"Feature Request\"\n $ wspc todo type set typ_xxx --custom-fields '[{\"key\":\"severity\",\"label\":\"Severity\",\"type\":\"string\"}]'\n") + .argument("<id>", "id") + .option("--expected-version <value>", "expected_version") + .option("--label <value>", "label") + .option("--hide-core-fields <value>", "hide_core_fields") + .option("--custom-fields <value>", "custom_fields") + .action(async (id, opts) => { + const client = await loadSdkClient() + const result = await todoTypeUpdate({ + client: (client as unknown as { _rawClient: unknown })._rawClient as never, + path: { + id, + }, + body: { + expected_version: opts.expectedVersion, + label: opts.label, + hide_core_fields: parseJsonField(opts.hideCoreFields, "hide-core-fields"), + custom_fields: parseJsonField(opts.customFields, "custom-fields"), + }, + }) + if (result.error || !result.response?.ok) { + process.stderr.write( + `HTTP ${result.response?.status ?? "?"}: ${JSON.stringify(result.error ?? "unknown error", null, 2)}\n`, + ) + process.exitCode = 1 + return + } + render({ kind: "todo_type_update", display: undefined }, result.data) + }) diff --git a/src/generated/cli/todo/update.ts b/src/generated/cli/todo/update.ts index de3dfba..f665b2f 100644 --- a/src/generated/cli/todo/update.ts +++ b/src/generated/cli/todo/update.ts @@ -7,6 +7,7 @@ import { parseJsonField } from "../../../handwritten/utils/parse-json-field.js" export const todoUpdateCommand = new Command("update") .description("Update a todo") + .addHelpText("after", "\n### 🎯 Overview & Purpose\nUpdate one or more fields of an existing todo item, such as its title, status, parent todo, due date, or description.\n\n### 🔍 When to Use\n* Use this to log progress by changing the status (e.g., to `in_progress` or `done`), reschedule due dates, edit title/description, or reassign/move a task by changing its `parent_id`.\n\n### 💡 Key Features & Constraints\n* **Optimistic Locking (`expected_version`)**: An optional integer representing the version you expect to update. If provided, the server matches it with the current database version. If they match, the update succeeds and increments the version; if they mismatch, a `VERSION_CONFLICT` error is thrown. Omit this field to skip version checking (Last-Write-Wins behavior).\n* **Parent Re-assignment**: Set `parent_id: null` to move a child todo back to the root level.\n* **Status Transitions**: Transitioning the `status` to `done` automatically emits a `captureTodoCompleted` analytics event.\n* **Clearing Fields**: To clear an existing description or due date, explicitly pass `\"\"`. Passing `null` is rejected.\n\n### 💡 Best Practices & Guidelines\n* **Enriched Context**: Keep the description updated with definition of done, relevant progress notes, or resolution summaries.\n* **Keep Descriptions Clean**: Delegate heavy logs/documents to a Markdown file on Drive, and put a markdown link inside the description.\n* **Structured Properties**: Use custom fields (`custom_fields`) matching the todo's type to record properties like tags, priority, or severity.\n\n### ⚠️ Common Errors & Troubleshooting\n* **`VERSION_CONFLICT` (HTTP 409)**: Thrown if `expected_version` does not match the current database row version.\n* **`PARENT_IS_CHILD` (HTTP 400)**: Thrown if the new `parent_id` refers to a todo that is itself already a child todo.\n* **`WOULD_CREATE_CYCLE` (HTTP 400)**: Thrown if the update attempts to make a parent todo a child of its own descendant.\n* **`NOT_FOUND` (HTTP 404)**: Thrown if the todo `id` or the new `parent_id` does not exist or has been soft-deleted.\n\nExamples:\n $ wspc todo update tod_xxx --status done\n $ wspc todo update tod_xxx --title \"New title\"\n $ wspc todo update tod_xxx --custom-fields '{\"severity\":\"critical\"}'\n") .argument("<id>", "id") .option("--expected-version <value>", "expected_version") .option("--title <value>", "New title. Omit to leave the existing title unchanged. Must be non-empty when supplied.") diff --git a/src/generated/sdk/sdk.gen.ts b/src/generated/sdk/sdk.gen.ts index 65b179a..7bcde43 100644 --- a/src/generated/sdk/sdk.gen.ts +++ b/src/generated/sdk/sdk.gen.ts @@ -1670,6 +1670,12 @@ export const todoList = <ThrowOnError extends boolean = false>(options: Options< * * **Due Date Format**: Accepts an ISO-8601 date-only format (`YYYY-MM-DD`). Pass `""` or omit the field to skip setting a due date. * * **Project Binding**: Every todo must belong to a valid active project (`project_id`). * + * ### 💡 Best Practices & Guidelines + * * **Descriptive Descriptions**: Always provide a detailed description explaining the task's context, goal, or definition of done. Avoid leaving it empty. + * * **Task Breakdown**: For complex items, split the task into 2-3 logical subtasks (by passing a parent todo ID in `parent_id`). Remember WSPC supports only one level of nesting (Root -> Child). + * * **Integration with Drive**: If a task requires storing large text blocks, research logs, checklists, or files, do not overload the description. Use the Drive feature to store a markdown file (e.g., `research/notes.md`) and place a clean reference link in the description, such as `[Notes](drive://<library>/<path>)`. + * * **Structured Properties (Custom Fields)**: Avoid putting structured meta-attributes (like priority, severity, story points, tags) directly in the description. Instead, use custom todo types (`todo_type`). Create a custom type schema first, then assign it with `type_id` and pass values via `custom_fields`. + * * ### ⚠️ Common Errors & Troubleshooting * * **`VALIDATION_ERROR` (HTTP 400)**: Thrown if required fields are missing, if `due_at` violates the `YYYY-MM-DD` format, or if `title` exceeds 500 characters. * * **`PARENT_IS_CHILD` (HTTP 400)**: Thrown if the target `parent_id` refers to a todo that is itself already a child todo. @@ -1986,6 +1992,11 @@ export const todoGet = <ThrowOnError extends boolean = false>(options: Options<T * * **Status Transitions**: Transitioning the `status` to `done` automatically emits a `captureTodoCompleted` analytics event. * * **Clearing Fields**: To clear an existing description or due date, explicitly pass `""`. Passing `null` is rejected. * + * ### 💡 Best Practices & Guidelines + * * **Enriched Context**: Keep the description updated with definition of done, relevant progress notes, or resolution summaries. + * * **Keep Descriptions Clean**: Delegate heavy logs/documents to a Markdown file on Drive, and put a markdown link inside the description. + * * **Structured Properties**: Use custom fields (`custom_fields`) matching the todo's type to record properties like tags, priority, or severity. + * * ### ⚠️ Common Errors & Troubleshooting * * **`VERSION_CONFLICT` (HTTP 409)**: Thrown if `expected_version` does not match the current database row version. * * **`PARENT_IS_CHILD` (HTTP 400)**: Thrown if the new `parent_id` refers to a todo that is itself already a child todo. diff --git a/test/cli-codegen.test.ts b/test/cli-codegen.test.ts index 7da26d9..0a78940 100644 --- a/test/cli-codegen.test.ts +++ b/test/cli-codegen.test.ts @@ -275,4 +275,22 @@ describe("emitCommand", () => { expect(allMatches.length).toBe(1) expect(code).not.toContain('.option("--all-day", "all_day")') }) + + it("emits addHelpText when description or examples are provided", () => { + const code = emitCommand({ + operationId: "todo_create", + method: "post", + path: "/todo/items", + summary: "Create a todo", + description: "Guidelines: Make sure to split tasks.", + xCli: { + command: "todo add", + examples: ['wspc todo add "My Task"'], + }, + bodyFields: [], + }) + expect(code).toContain('.addHelpText("after"') + expect(code).toContain("Guidelines: Make sure to split tasks.") + expect(code).toContain('wspc todo add \\"My Task\\"') + }) }) diff --git a/tools/cli-codegen/emit.ts b/tools/cli-codegen/emit.ts index c5fb91e..9e7c687 100644 --- a/tools/cli-codegen/emit.ts +++ b/tools/cli-codegen/emit.ts @@ -50,6 +50,7 @@ export interface EmitInput { method: string path: string summary?: string + description?: string xCli: XCli bodyFields: BodyField[] pathParams?: string[] @@ -495,12 +496,24 @@ export function emitCommand(input: EmitInput): string | null { ) } + let helpTextCall = "" + const helpParts: string[] = [] + if (input.description) { + helpParts.push(input.description) + } + if (input.xCli.examples && input.xCli.examples.length > 0) { + helpParts.push("Examples:\n" + input.xCli.examples.map((ex) => ` $ ${ex}`).join("\n")) + } + if (helpParts.length > 0) { + helpTextCall = `\n .addHelpText("after", ${JSON.stringify("\n" + helpParts.join("\n\n") + "\n")})` + } + return [ `// AUTO-GENERATED — DO NOT EDIT (source: ${input.operationId})`, ...imports, ``, `export const ${fnName}Command = new Command(${JSON.stringify(cmdLeaf)})`, - ` .description(${JSON.stringify(input.summary ?? input.xCli.command)})`, + ` .description(${JSON.stringify(input.summary ?? input.xCli.command)})${helpTextCall}`, ...args.map((a) => ` ${a}`), ...options.map((o) => ` ${o}`), ` .action(async (${[...argNames, "opts"].join(", ")}) => {`, diff --git a/tools/cli-codegen/main.ts b/tools/cli-codegen/main.ts index 82c8e8f..6b239e8 100644 --- a/tools/cli-codegen/main.ts +++ b/tools/cli-codegen/main.ts @@ -218,6 +218,7 @@ async function main(): Promise<void> { method, path: routePath, summary: op.summary, + description: op.description, xCli: op["x-cli"], bodyFields, pathParams, From c6065f235436017f481c67624c4350dc51637e3e Mon Sep 17 00:00:00 2001 From: Yuren Ju <yurenju@gmail.com> Date: Mon, 13 Jul 2026 17:33:03 +0900 Subject: [PATCH 2/2] fix(cli-codegen): add description field to OperationLike interface to fix typecheck --- tools/cli-codegen/main.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/cli-codegen/main.ts b/tools/cli-codegen/main.ts index 6b239e8..bc189d6 100644 --- a/tools/cli-codegen/main.ts +++ b/tools/cli-codegen/main.ts @@ -26,6 +26,7 @@ interface ParameterLike { export interface OperationLike { operationId?: string summary?: string + description?: string tags?: string[] parameters?: ParameterLike[] requestBody?: {