diff --git a/.dependency-cruiser.cjs b/.dependency-cruiser.cjs new file mode 100644 index 0000000..4427288 --- /dev/null +++ b/.dependency-cruiser.cjs @@ -0,0 +1,48 @@ +// Owners and constraints for this repository's actual runtime roots. +module.exports = { + forbidden: [ + { + name: 'no-runtime-cycles', + severity: 'error', + from: {}, + to: { + circular: true, + dependencyTypesNot: ['type-only'], + }, + }, + { + name: 'no-unresolved-dependencies', + severity: 'error', + from: {}, + to: { + couldNotResolve: true, + }, + }, + { + name: 'no-runtime-test-dependencies', + severity: 'error', + from: { + path: '^(?:src)/', + pathNot: '(?:__tests__|test|tests)/|\\.(?:test|spec)\\.', + }, + to: { + path: '(?:^|/)(?:__tests__|tests|test)/|(?:^|node_modules/)(?:jest|vitest|ts-jest)', + }, + }, + ], + options: { + doNotFollow: { + path: 'node_modules', + }, + tsConfig: { + fileName: 'tsconfig.quality.json', + }, + tsPreCompilationDeps: true, + enhancedResolveOptions: { + exportsFields: ['exports'], + conditionNames: ['types', 'require', 'node', 'default'], + extensions: ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts', '.json'], + }, + exclude: '(?:^|/)node_modules/', + }, +}; diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..d0f6b62 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,72 @@ +name: Quality +on: + pull_request: + push: + branches: [main] +permissions: + contents: read +concurrency: + group: quality-${{ github.ref }} + cancel-in-progress: true +jobs: + checks: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: '24.19.0' + - run: npm ci + - run: npm --prefix tools/quality ci + - run: npm run test:preset + - run: npm run quality:guards + - run: npm run format:check + - run: npm run lint:static + - run: npm run lint:typed + - run: npm run typecheck + - run: npm run lint:arch + - run: npm run lint:unused:production + - run: npm run lint:unused:full + - run: npm run lint:duplicates + - run: npm run lint:duplicates:full + - run: npm run lint:complexity + - run: npm run test:quality + - run: npm test + - run: npm run build + - run: npm run test:pack + - uses: actions/upload-artifact@v4 + if: always() + with: + name: quality-reports + path: quality/reports/ + if-no-files-found: error + package-consumer: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + node: ['18', '20', '22', '24'] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '24.19.0' + - run: npm ci --ignore-scripts + - run: npm run build + - name: Pack the actual distributable + run: mkdir -p "$RUNNER_TEMP/packed" && npm pack --ignore-scripts --pack-destination "$RUNNER_TEMP/packed" + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - run: bash scripts/consumer-smoke.sh "$RUNNER_TEMP"/packed/*.tgz '@1msg/cli' + quality-required: + if: always() + needs: [checks, package-consumer] + runs-on: ubuntu-24.04 + steps: + - env: + RESULTS: ${{ toJSON(needs) }} + run: node -e 'const jobs=JSON.parse(process.env.RESULTS); if(!Object.values(jobs).every(job=>job.result==="success")) process.exit(1)' diff --git a/.gitignore b/.gitignore index 0fd6563..9d32e6f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,10 @@ coverage/ .env.* !.env.example npm-debug.log* + +quality/reports/ + +.husky/_/ + +# Local shared-tool dependency links are never repository content. +tools/quality/node_modules diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..81e401a --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +node tools/quality/node_modules/lint-staged/bin/lint-staged.js diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..60ade1a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.19.0 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..9eba92f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +node_modules +dist +tools/quality +quality/reports + + +# Byte-pinned producer inputs; integrity is checked by the contract tests. +quality/contracts/openapi.yaml +quality/contracts/CLI.md diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..2fec1f2 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100 +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3835bea --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,15 @@ +# Quality and ownership + +Profile A, Node 24.19.0. SDK/tooling maintainers own this repository. The public package entrypoints are defined in package.json; framework registration and executable consumers must remain intact. Imports must be acyclic at runtime, production cannot import tests, and dependency boundaries are enforced in `.dependency-cruiser.cjs`. `quality.config.mjs` is the source inventory and exact generated-path declaration. + +Configuration belongs in the existing config/bootstrap adapters; pure conversion/validation helpers must not gain transport or environment reads. SDK and HTTP calls belong in the existing transport/client adapters. Keep complete scenarios readable at their application entrypoints; avoid pass-through facade layers. Comments on new/changed functions describe purpose, inputs/results, invariants and relevant failure/resource ownership. Check comments against behavior rather than repeating the function name. + +Install with the frozen lockfile, then `npm --prefix tools/quality ci`. Run `npm run hooks:install` for local Husky/lint-staged feedback. Run all commands listed in `.github/workflows/quality.yml` before review: formatter, static and typed ESLint, strict production/test typecheck, dependency-cruiser, both Knip graphs, jscpd runtime gate and full report, complexity, guard negative fixtures, tests, build and packed-entrypoint smoke. No CI skip variable disables typed lint. + +Historical debt is recorded per finding in `quality/baselines`, with adoption SHA, evidence, owner, issue and expiry. Initial allowances are replayed against the default branch; new findings and increases are forbidden. Remove entries when fixed. Generated paths have producer provenance; authored tooling is always checked. + +The `quality-required` job fails on failed/skipped/cancelled prerequisites. Configure branch protection to require it; YAML alone does not change server protection. Tests use isolated/local inputs; never point them at production services. + +Standalone contract tests use the byte-pinned public API and CLI RFC in `quality/contracts`. Their provenance records the producer commit and checksums. Missing or edited inputs fail; they never skip based on checkout layout. Update the input snapshots deliberately from a reviewed producer commit, retaining full operation coverage and help assertions. These checks compare the pinned contract, not the latest remote API. + +Knip classifies `src/__tests__` as a test graph input and retains it in the full graph. The obsolete production allowance for the test-only `helpers.ts` was removed; runtime modules used only by a test remain production findings, as the real Knip fixture verifies. diff --git a/README.md b/README.md index 5f0c616..3a40071 100644 --- a/README.md +++ b/README.md @@ -10,20 +10,20 @@ HTTP never goes through `fetch` / `axios`. Every API call uses [`@1msg/sdk`](htt [![node](https://img.shields.io/node/v/@1msg/cli.svg)](https://nodejs.org) [![license](https://img.shields.io/npm/l/@1msg/cli.svg)](./LICENSE) -| | | -|--|--| -| **npm** | [`@1msg/cli`](https://www.npmjs.com/package/@1msg/cli) | -| **Binary** | `1msg` (`npx @1msg/cli` is the same program) | -| **SDK** | [`@1msg/sdk`](https://www.npmjs.com/package/@1msg/sdk) | -| **Requires** | Node.js ≥ 18 | -| **Language** | English (`--help`, tables, errors) | -| **Docs** | [https://docs.1msg.io/](https://docs.1msg.io/) | +| | | +| ------------ | ------------------------------------------------------ | +| **npm** | [`@1msg/cli`](https://www.npmjs.com/package/@1msg/cli) | +| **Binary** | `1msg` (`npx @1msg/cli` is the same program) | +| **SDK** | [`@1msg/sdk`](https://www.npmjs.com/package/@1msg/sdk) | +| **Requires** | Node.js ≥ 18 | +| **Language** | English (`--help`, tables, errors) | +| **Docs** | [https://docs.1msg.io/](https://docs.1msg.io/) | API hosts: -| Environment | `ONE_MSG_BASE_URL` | -|-------------|--------------------| -| Live channels | `https://api.1msg.io` | +| Environment | `ONE_MSG_BASE_URL` | +| ------------- | ------------------------- | +| Live channels | `https://api.1msg.io` | | Test channels | `https://sandbox.1msg.io` | Bare hostnames (`api.1msg.io`, `sandbox.1msg.io`) are accepted and normalized to `https://`. Stage / local (`api-stage.1msg.io`, `http://127.0.0.1:3050`) work for `init` / `channel add`. @@ -97,10 +97,10 @@ Session messages (`1msg send`, most `1msg message *`) need an open 24-hour windo Config file mode `0600`: -| OS | Path | -|----|------| +| OS | Path | +| ------------- | ------------------------------------------------------------------ | | Linux / macOS | `~/.config/1msg/config.yaml` (`$XDG_CONFIG_HOME/1msg/config.yaml`) | -| Windows | `%AppData%\1msg\config.yaml` | +| Windows | `%AppData%\1msg\config.yaml` | ```yaml # ~/.config/1msg/config.yaml @@ -114,7 +114,7 @@ channels: acme-test: base_url: https://sandbox.1msg.io instance_id: ODI371267301 - token: "…" # optional; file mode 0600 + token: '…' # optional; file mode 0600 ``` There is **no API** to list a customer's channels. `channel list` / `use` / `add` / `remove` / `current` are always local. @@ -123,12 +123,12 @@ Tokens are never printed in tables, logs, or `--json`. ### Environment -| Variable | Description | -|----------|-------------| -| `ONE_MSG_CHANNEL` | Same as `--channel` (config **name**, not instance id) | -| `ONE_MSG_BASE_URL` | `https://api.1msg.io` or `https://sandbox.1msg.io` | -| `ONE_MSG_INSTANCE_ID` | Channel instance id | -| `ONE_MSG_TOKEN` | Channel API token | +| Variable | Description | +| --------------------- | ------------------------------------------------------ | +| `ONE_MSG_CHANNEL` | Same as `--channel` (config **name**, not instance id) | +| `ONE_MSG_BASE_URL` | `https://api.1msg.io` or `https://sandbox.1msg.io` | +| `ONE_MSG_INSTANCE_ID` | Channel instance id | +| `ONE_MSG_TOKEN` | Channel API token | Deprecated aliases match MCP: `CHAT_API_BASE_URL`, `CHAT_API_TOKEN`, `CHAT_API_INSTANCE_ID`, `CHAT_API_KEY`, `TOKEN`, `INSTANCE_ID`. Prefer `ONE_MSG_*`. @@ -177,16 +177,16 @@ export ONE_MSG_TOKEN=*** Available on every command: -| Flag | Description | -|------|-------------| -| `-c, --channel ` | Named channel from the config file | -| `--instance-id ` | Override instance id | -| `--base-url ` | Override API root | -| `--token ` | Override token (prefer `ONE_MSG_TOKEN`) | -| `--json` | Print the API response body as JSON | -| `--no-color` | Disable ANSI color | -| `-h, --help` | Help for this command | -| `-v, --version` | Version (root only) | +| Flag | Description | +| ---------------------- | --------------------------------------- | +| `-c, --channel ` | Named channel from the config file | +| `--instance-id ` | Override instance id | +| `--base-url ` | Override API root | +| `--token ` | Override token (prefer `ONE_MSG_TOKEN`) | +| `--json` | Print the API response body as JSON | +| `--no-color` | Disable ANSI color | +| `-h, --help` | Help for this command | +| `-v, --version` | Version (root only) | `--json` is the **raw API body**. There is no `--raw`. The one CLI-built object is `channel info --json`: `{ "status": , "me": }`. @@ -194,11 +194,11 @@ Available on every command: Used by send-like commands unless a command says otherwise. Exactly one of `--to` or `--chat-id`. Exception: `message payment` is `--to` only (public schema has no `chatId`). -| Flag | Description | -|------|-------------| -| `--to ` | Phone with country code, no `+` | -| `--chat-id ` | `phone@c.us` · `group@g.us` · `BSUID@lid` | -| `--quote ` | Reply to this message (`wamid.*`) | +| Flag | Description | +| ----------------- | ----------------------------------------- | +| `--to ` | Phone with country code, no `+` | +| `--chat-id ` | `phone@c.us` · `group@g.us` · `BSUID@lid` | +| `--quote ` | Reply to this message (`wamid.*`) | Complex bodies (`template add`, interactive messages, calling SDP) take flags for scalars and `--from-file` for JSON (`-` = stdin). @@ -222,18 +222,18 @@ printf '%s' "$ONE_MSG_TOKEN" | 1msg init --name prod \ Local named channels, plus remote status/settings of the selected channel. -| Subcommand | Where | What | -|------------|-------|------| -| `list` | local | Channels in the file (`*` = default). Columns: `default`, `name`, `instance_id`, `base_url`. Never `token`. | -| `use ` | local | Set default channel | -| `add` | local | Add a channel (first channel becomes default unless `--default`) | -| `remove ` | local | Remove by **config name** only | -| `current` | local | Default name and instance id | -| `info` | API | `GET /status` then `GET /me` | -| `status` | API | `GET /status` (alias: `1msg status`) | -| `settings` / `settings set` | API | `GET/POST /settings` (only flags you pass are sent) | -| `mm-lite` | API | `GET /mmLiteStatus` | -| `automation` / `automation set` | API | Conversational automation | +| Subcommand | Where | What | +| ------------------------------- | ----- | ----------------------------------------------------------------------------------------------------------- | +| `list` | local | Channels in the file (`*` = default). Columns: `default`, `name`, `instance_id`, `base_url`. Never `token`. | +| `use ` | local | Set default channel | +| `add` | local | Add a channel (first channel becomes default unless `--default`) | +| `remove ` | local | Remove by **config name** only | +| `current` | local | Default name and instance id | +| `info` | API | `GET /status` then `GET /me` | +| `status` | API | `GET /status` (alias: `1msg status`) | +| `settings` / `settings set` | API | `GET/POST /settings` (only flags you pass are sent) | +| `mm-lite` | API | `GET /mmLiteStatus` | +| `automation` / `automation set` | API | Conversational automation | ```bash 1msg channel list @@ -283,25 +283,25 @@ to 12020721369@c.us ### `message` -| Command | API | Notes | -|---------|-----|-------| -| `send` | `POST /sendMessage` or `/sendFile` | Same as `1msg send` | -| `list` | `GET /messages` | Inbox history. Columns: `time`, `chat_id`, `from`, `type`, `id`, `body` (body truncated to 60). If the API returns `notice` and no rows, print the notice and exit 0. | -| `read` | `POST /readMessage` | `--message-id` only; no chat id. `--typing` = typing indicator (max 25s) | -| `react` | `POST /sendReaction` | `--emoji` → API `body`; `--message-id` → `quotedMsgId`. Empty emoji removes the reaction | -| `location` | `POST /sendLocation` | `--lat` / `--lng` | -| `location-request` | `POST /sendLocationRequest` | Optional `--text` | -| `contact` | `POST /sendContact` | Flags or `--from-file` | -| `buttons` | `POST /sendButton` | `--button id:title` repeatable, max 3. Interactive header only via `--from-file` | -| `menu` | `POST /sendList` | WhatsApp list. `--from-file` required (`sections[]`) | -| `carousel` | `POST /sendCarousel` | `--from-file` with `cards[]` and/or `params[]`. Carousel **outside** 24h is `template send --params`, not this command | -| `product` | `POST /sendProduct` | `--catalog-id` + `--product-id`, or `--from-file` for lists | -| `cta` | `POST /sendCtaUrl` | `--text` / `--button` / `--url` | -| `address` | `POST /sendAddressMessage` | India and Singapore only (`--country IN\|SG`) | -| `order` | `POST /sendOrderDetails` | India payments; `--from-file` for order body | -| `payment` | `POST /sendPaymentRequest` | `--to` + `--region IN\|SG\|BR` only (no `--chat-id`) | -| `sticker` | `POST /sendSticker` | Exactly one of `--link`, `--media-id`, `--file` | -| `flow` | `POST /sendFlow` | Session window. Outside 24h send a template with a FLOW button | +| Command | API | Notes | +| ------------------ | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `send` | `POST /sendMessage` or `/sendFile` | Same as `1msg send` | +| `list` | `GET /messages` | Inbox history. Columns: `time`, `chat_id`, `from`, `type`, `id`, `body` (body truncated to 60). If the API returns `notice` and no rows, print the notice and exit 0. | +| `read` | `POST /readMessage` | `--message-id` only; no chat id. `--typing` = typing indicator (max 25s) | +| `react` | `POST /sendReaction` | `--emoji` → API `body`; `--message-id` → `quotedMsgId`. Empty emoji removes the reaction | +| `location` | `POST /sendLocation` | `--lat` / `--lng` | +| `location-request` | `POST /sendLocationRequest` | Optional `--text` | +| `contact` | `POST /sendContact` | Flags or `--from-file` | +| `buttons` | `POST /sendButton` | `--button id:title` repeatable, max 3. Interactive header only via `--from-file` | +| `menu` | `POST /sendList` | WhatsApp list. `--from-file` required (`sections[]`) | +| `carousel` | `POST /sendCarousel` | `--from-file` with `cards[]` and/or `params[]`. Carousel **outside** 24h is `template send --params`, not this command | +| `product` | `POST /sendProduct` | `--catalog-id` + `--product-id`, or `--from-file` for lists | +| `cta` | `POST /sendCtaUrl` | `--text` / `--button` / `--url` | +| `address` | `POST /sendAddressMessage` | India and Singapore only (`--country IN\|SG`) | +| `order` | `POST /sendOrderDetails` | India payments; `--from-file` for order body | +| `payment` | `POST /sendPaymentRequest` | `--to` + `--region IN\|SG\|BR` only (no `--chat-id`) | +| `sticker` | `POST /sendSticker` | Exactly one of `--link`, `--media-id`, `--file` | +| `flow` | `POST /sendFlow` | Session window. Outside 24h send a template with a FLOW button | ```bash 1msg message list --chat-id 12020721369@c.us --limit 20 --last @@ -486,14 +486,14 @@ Error: channel "missing" not in config See: 1msg channel list ``` -| Exit | Meaning | -|------|---------| -| `0` | OK | -| `1` | Usage | -| `2` | API 4xx | -| `3` | API 5xx | -| `4` | Network | -| `5` | Not configured (`1msg init`) | +| Exit | Meaning | +| ---- | ---------------------------- | +| `0` | OK | +| `1` | Usage | +| `2` | API 4xx | +| `3` | API 5xx | +| `4` | Network | +| `5` | Not configured (`1msg init`) | --- @@ -509,19 +509,19 @@ Print a script to stdout. The CLI never writes into `/etc` or `$fpath` by itself What is completed: -| Kind | Source | -|------|--------| -| Commands, subcommands, flags | Always, from the command tree | -| Channel names (`-c` / `--channel` / `channel use` / `remove`) | Keys of `channels:` in the config file (local, no API) | -| Template names (`template send --name`, `template remove --name`) | Cache written after a successful `1msg template list` | -| Flow ids / group ids | Not cached; type them | +| Kind | Source | +| ----------------------------------------------------------------- | ------------------------------------------------------ | +| Commands, subcommands, flags | Always, from the command tree | +| Channel names (`-c` / `--channel` / `channel use` / `remove`) | Keys of `channels:` in the config file (local, no API) | +| Template names (`template send --name`, `template remove --name`) | Cache written after a successful `1msg template list` | +| Flow ids / group ids | Not cached; type them | Template cache: -| OS | Path | -|----|------| -| Linux / macOS | `~/.cache/1msg/completion.json` | -| Windows | `%LOCALAPPDATA%\1msg\completion.json` | +| OS | Path | +| ------------- | ------------------------------------- | +| Linux / macOS | `~/.cache/1msg/completion.json` | +| Windows | `%LOCALAPPDATA%\1msg\completion.json` | Shape: `{ "updated_at": "", "channel": "", "templates": ["hello_world", …] }`. Cache is per default channel name; after `channel use`, run `template list` again or names may be stale. Completing `--name` with no cache: no values (commands/flags still complete). @@ -568,70 +568,70 @@ Once in `$PROFILE`: `. (Join-Path (Split-Path $PROFILE) '1msg.ps1')`. 61 mapped public `operationId`s + explicit skip `deleteMediaLegacy` = all 62 operations in the public OpenAPI spec. -| operationId | CLI | -|---|---| -| `sendMessage` | `message send --text` / `send --text` | -| `sendFile` | `message send --file` / `--media-id` | -| `createUploadMedia` | `media upload` | -| `listMessages` | `message list` | -| `createReadMessage` | `message read` | -| `sendTemplate` | `template send` | -| `listTemplates` | `template list` | -| `addTemplate` | `template add` | -| `removeTemplate` | `template remove` | -| `getStatus` | `status` / `channel status` | -| `getMe` | `me` | -| `updateMe` | `me update` | -| `retrieveMedia` | `media get` | -| `deleteMedia` | `media delete` | -| `sendReaction` | `message react` | -| `sendLocation` | `message location` | -| `sendLocationRequest` | `message location-request` | -| `sendContact` | `message contact` | -| `sendButton` | `message buttons` | -| `sendList` | `message menu` | -| `sendCarousel` | `message carousel` | -| `sendProduct` | `message product` | -| `sendCtaUrl` | `message cta` | -| `sendAddressMessage` | `message address` | -| `sendOrderDetails` | `message order` | -| `sendPaymentRequest` | `message payment` | -| `sendSticker` | `message sticker` | -| `sendFlow` | `message flow` | -| `getWebhook` | `webhook get` | -| `setWebhook` | `webhook set` | -| `listSettings` | `channel settings` | -| `createSettings` | `channel settings set` / `webhook clear` | -| `getMmLiteStatus` | `channel mm-lite` | -| `getCommerce` | `catalog get` | -| `createCommerce` | `catalog set` | -| `getConversationalAutomation` | `channel automation` | -| `setConversationalAutomation` | `channel automation set` | -| `blockUser` | `user block` | -| `unblockUser` | `user unblock` | -| `listBlockedUsers` | `user blocked` | -| `createGroups` | `group create` | -| `listGroups` | `group list` | -| `getGroupsGroupId` | `group get` | -| `createGroupsGroupId` | `group update` | -| `deleteGroupsGroupId` | `group delete` | -| `getGroupsGroupIdInvitelink` | `group invite-link` | -| `createGroupsGroupIdInvitelink` | `group invite-link reset` | -| `createFlows` | `flow create` | -| `listFlows` | `flow list` | -| `getFlowsFlowId` | `flow get` | -| `deleteFlowsFlowId` | `flow delete` | -| `patchFlowsFlowIdMetadata` | `flow metadata` | -| `patchFlowsFlowIdAssets` | `flow assets` | -| `getFlowsFlowIdPreview` | `flow preview` | -| `createFlowsFlowIdPublish` | `flow publish` | -| `createFlowsFlowIdDeprecate` | `flow deprecate` | -| `getCallingSettings` | `call settings` | -| `updateCallingSettings` | `call settings set` | -| `initiateCall` | `call connect` / `pre-accept` / `accept` / `reject` / `hangup` | -| `getWhatsappBusinessEncryption` | `flow encryption` | -| `setWhatsappBusinessEncryption` | `flow encryption set` | -| `deleteMediaLegacy` | not exposed | +| operationId | CLI | +| ------------------------------- | -------------------------------------------------------------- | +| `sendMessage` | `message send --text` / `send --text` | +| `sendFile` | `message send --file` / `--media-id` | +| `createUploadMedia` | `media upload` | +| `listMessages` | `message list` | +| `createReadMessage` | `message read` | +| `sendTemplate` | `template send` | +| `listTemplates` | `template list` | +| `addTemplate` | `template add` | +| `removeTemplate` | `template remove` | +| `getStatus` | `status` / `channel status` | +| `getMe` | `me` | +| `updateMe` | `me update` | +| `retrieveMedia` | `media get` | +| `deleteMedia` | `media delete` | +| `sendReaction` | `message react` | +| `sendLocation` | `message location` | +| `sendLocationRequest` | `message location-request` | +| `sendContact` | `message contact` | +| `sendButton` | `message buttons` | +| `sendList` | `message menu` | +| `sendCarousel` | `message carousel` | +| `sendProduct` | `message product` | +| `sendCtaUrl` | `message cta` | +| `sendAddressMessage` | `message address` | +| `sendOrderDetails` | `message order` | +| `sendPaymentRequest` | `message payment` | +| `sendSticker` | `message sticker` | +| `sendFlow` | `message flow` | +| `getWebhook` | `webhook get` | +| `setWebhook` | `webhook set` | +| `listSettings` | `channel settings` | +| `createSettings` | `channel settings set` / `webhook clear` | +| `getMmLiteStatus` | `channel mm-lite` | +| `getCommerce` | `catalog get` | +| `createCommerce` | `catalog set` | +| `getConversationalAutomation` | `channel automation` | +| `setConversationalAutomation` | `channel automation set` | +| `blockUser` | `user block` | +| `unblockUser` | `user unblock` | +| `listBlockedUsers` | `user blocked` | +| `createGroups` | `group create` | +| `listGroups` | `group list` | +| `getGroupsGroupId` | `group get` | +| `createGroupsGroupId` | `group update` | +| `deleteGroupsGroupId` | `group delete` | +| `getGroupsGroupIdInvitelink` | `group invite-link` | +| `createGroupsGroupIdInvitelink` | `group invite-link reset` | +| `createFlows` | `flow create` | +| `listFlows` | `flow list` | +| `getFlowsFlowId` | `flow get` | +| `deleteFlowsFlowId` | `flow delete` | +| `patchFlowsFlowIdMetadata` | `flow metadata` | +| `patchFlowsFlowIdAssets` | `flow assets` | +| `getFlowsFlowIdPreview` | `flow preview` | +| `createFlowsFlowIdPublish` | `flow publish` | +| `createFlowsFlowIdDeprecate` | `flow deprecate` | +| `getCallingSettings` | `call settings` | +| `updateCallingSettings` | `call settings set` | +| `initiateCall` | `call connect` / `pre-accept` / `accept` / `reject` / `hangup` | +| `getWhatsappBusinessEncryption` | `flow encryption` | +| `setWhatsappBusinessEncryption` | `flow encryption set` | +| `deleteMediaLegacy` | not exposed | Local-only (no `operationId`): `init`, `channel list|use|add|remove|current`, `completion`, `version`. @@ -651,17 +651,17 @@ OpenAPI YAML (1msg-api monorepo) See [ARCHITECTURE.md](./ARCHITECTURE.md). -Coverage is asserted in the `1msg-api` monorepo: 61 mapped public `operationId`s + explicit `deleteMediaLegacy` skip = 62. `--help` text is snapshot-tested against the RFC. +Coverage is asserted in this standalone package using the public contract pinned in `quality/contracts/provenance.json`: 61 mapped public `operationId`s + explicit `deleteMediaLegacy` exclusion = 62. Every `--help` page is checked against the RFC from the same producer commit. Missing or changed input files fail the tests. --- ## Development -| Script | Purpose | -|--------|---------| -| `npm run build` | Compile TypeScript → `dist/` | -| `npm test` | Jest unit tests (`--help` snapshots, precedence, SDK-only architecture) | -| `npm start` | Run `dist/index.js` | +| Script | Purpose | +| -------------------- | --------------------------------------------------------------------------- | +| `npm run build` | Compile TypeScript → `dist/` | +| `npm test` | Jest unit tests (`--help` snapshots, precedence, SDK-only architecture) | +| `npm start` | Run `dist/index.js` | | `npm pack --dry-run` | What npm would publish (`dist/`, this README, `ARCHITECTURE.md`, `LICENSE`) | CI (`.github/workflows/ci.yml`): install, build, test, smoke `--help` / `version`, `npm pack --dry-run`. diff --git a/eslint.complexity.config.mjs b/eslint.complexity.config.mjs new file mode 100644 index 0000000..3654818 --- /dev/null +++ b/eslint.complexity.config.mjs @@ -0,0 +1,12 @@ +import { maintainabilityConfig, tseslint } from './tools/quality/eslint.mjs'; +export default [ + { files: ['**/*.jsx'], languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } }, + { linterOptions: { reportUnusedDisableDirectives: 'off' } }, + { ignores: ['**/node_modules/**', '**/dist/**', '**/tools/quality/**', '**/quality/reports/**'] }, + { + files: ['**/*.{ts,tsx,cts,mts}'], + languageOptions: { parser: tseslint.parser }, + plugins: { '@typescript-eslint': tseslint.plugin }, + }, + ...maintainabilityConfig(), +]; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..b0f839c --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,15 @@ +import { staticConfig, globals } from './tools/quality/eslint.mjs'; + +export default [ + { files: ['**/*.jsx'], languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } }, + { ignores: ['**/node_modules/**', '**/dist/**', '**/tools/quality/**', '**/quality/reports/**'] }, + ...staticConfig({ globals: { ...globals.node, ...globals.jest } }), + { + settings: { + 'import/resolver': { + typescript: { project: './tsconfig.quality.json' }, + node: { extensions: ['.js', '.json', '.ts', '.mjs', '.cjs'] }, + }, + }, + }, +]; diff --git a/eslint.typed.config.mjs b/eslint.typed.config.mjs new file mode 100644 index 0000000..48de3a5 --- /dev/null +++ b/eslint.typed.config.mjs @@ -0,0 +1,5 @@ +import { typedConfig } from './tools/quality/eslint.mjs'; +export default [ + { ignores: ['**/node_modules/**', '**/dist/**', '**/tools/quality/**', '**/quality/reports/**'] }, + ...typedConfig({ project: './tsconfig.quality.json', tsconfigRootDir: import.meta.dirname }), +]; diff --git a/knip.json b/knip.json new file mode 100644 index 0000000..0f580fc --- /dev/null +++ b/knip.json @@ -0,0 +1,19 @@ +{ + "entry": [ + "src/index.ts!", + "scripts/pack-smoke.mjs", + "scripts/quality-extensions.test.mjs", + "scripts/quality-hook.test.mjs", + "scripts/quality-negative.mjs", + "jest.config.js", + "eslint*.mjs", + "quality.config.mjs" + ], + "project": [ + "src/**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}!", + "!src/__tests__/**!", + "src/__tests__/**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}", + "scripts/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}" + ], + "ignore": ["tools/quality/**"] +} diff --git a/package.json b/package.json index e8a0e5d..7358eae 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,23 @@ "clean": "rm -rf dist", "start": "node dist/index.js", "test": "jest", - "prepublishOnly": "npm run build" + "prepublishOnly": "npm run build", + "typecheck": "tsc -p tsconfig.quality.json", + "lint:static": "node tools/quality/node_modules/eslint/bin/eslint.js . --max-warnings=0", + "lint:typed": "node tools/quality/node_modules/eslint/bin/eslint.js -c eslint.typed.config.mjs . --max-warnings=0", + "lint:unused:production": "node tools/quality/run.mjs unused-production", + "lint:unused:full": "node tools/quality/run.mjs unused-full", + "lint:duplicates": "node tools/quality/run.mjs duplicates", + "quality:guards": "node tools/quality/run.mjs guards", + "lint:arch": "node tools/quality/node_modules/dependency-cruiser/bin/dependency-cruiser.mjs --config .dependency-cruiser.cjs src", + "test:quality": "node scripts/quality-negative.mjs src && node --test --test-concurrency=1 scripts/quality-hook.test.mjs scripts/quality-extensions.test.mjs", + "lint:complexity": "node tools/quality/run.mjs report --kind eslint --name complexity -- node tools/quality/node_modules/eslint/bin/eslint.js -c eslint.complexity.config.mjs . --format json", + "lint:duplicates:full": "node tools/quality/run.mjs duplicates-full", + "format:check": "node tools/quality/node_modules/prettier/bin/prettier.cjs --check .", + "format": "node tools/quality/node_modules/prettier/bin/prettier.cjs --write .", + "test:pack": "node scripts/pack-smoke.mjs", + "test:preset": "node --test tools/quality/test/*.test.mjs", + "hooks:install": "node tools/quality/node_modules/husky/bin.js" }, "dependencies": { "@1msg/sdk": "^1.0.4", @@ -47,5 +63,20 @@ }, "engines": { "node": ">=18.0.0" + }, + "packageManager": "npm@11.17.0", + "lint-staged": { + "*.{js,jsx,mjs,cjs}": [ + "node tools/quality/staged.mjs --config eslint.config.mjs --", + "node tools/quality/node_modules/prettier/bin/prettier.cjs --write", + "node tools/quality/node_modules/eslint/bin/eslint.js --config eslint.config.mjs --max-warnings=0 --no-warn-ignored" + ], + "*.{ts,tsx,mts,cts}": [ + "node tools/quality/staged.mjs --config eslint.config.mjs --", + "node tools/quality/node_modules/prettier/bin/prettier.cjs --write", + "node tools/quality/node_modules/eslint/bin/eslint.js --config eslint.config.mjs --max-warnings=0 --no-warn-ignored", + "node tools/quality/node_modules/eslint/bin/eslint.js --config eslint.typed.config.mjs --max-warnings=0 --no-warn-ignored" + ], + "*.{json,md,yml,yaml}": "node tools/quality/node_modules/prettier/bin/prettier.cjs --write" } } diff --git a/quality.config.mjs b/quality.config.mjs new file mode 100644 index 0000000..ca9529f --- /dev/null +++ b/quality.config.mjs @@ -0,0 +1,26 @@ +export default { + schemaVersion: 1, + profile: 'A', + defaultBranch: 'main', + sourceRoots: [ + 'src', + 'scripts', + 'jest.config.js', + 'eslint.config.mjs', + 'eslint.typed.config.mjs', + 'eslint.complexity.config.mjs', + '.dependency-cruiser.cjs', + 'quality.config.mjs', + ], + generated: [], + comments: { + full: ['scripts/pack-smoke.mjs', 'scripts/quality-negative.mjs'], + }, + knipConfig: 'knip.json', + duplicatePaths: ['src', 'scripts'], + duplicateIgnore: ['**/__tests__/**', '**/*.test.*', '**/*.spec.*'], + baselineDir: 'quality/baselines', + duplicateMaxPercentage: 5, + duplicateFullPaths: ['src', 'scripts'], + duplicateFullIgnore: [], +}; diff --git a/quality/baselines/complexity.json b/quality/baselines/complexity.json new file mode 100644 index 0000000..af46221 --- /dev/null +++ b/quality/baselines/complexity.json @@ -0,0 +1,146 @@ +{ + "schemaVersion": 1, + "adoptionBase": "e5c0a856387326a9786ee9669441fb787de36926", + "entries": [ + { + "key": "src/errors.ts|complexity|[[\"FunctionDeclaration\",\"normalizeThrown\",\"instance\"]]", + "value": 23, + "path": "src/errors.ts", + "rule": "complexity", + "scope": "[[\"FunctionDeclaration\",\"normalizeThrown\",\"instance\"]]", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical maintainability findings replayed at the adoption base; individual measurements cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/errors.ts": "0b1bfdd43cf890f83f948cc29243e04a57098481abc94c8fbacad379fa56338e" + } + }, + { + "key": "src/errors.ts|sonarjs/cognitive-complexity|[[\"FunctionDeclaration\",\"normalizeThrown\",\"instance\"]]", + "value": 18, + "path": "src/errors.ts", + "rule": "sonarjs/cognitive-complexity", + "scope": "[[\"FunctionDeclaration\",\"normalizeThrown\",\"instance\"]]", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical maintainability findings replayed at the adoption base; individual measurements cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/errors.ts": "0b1bfdd43cf890f83f948cc29243e04a57098481abc94c8fbacad379fa56338e" + } + }, + { + "key": "src/local.ts|sonarjs/cognitive-complexity|[[\"FunctionDeclaration\",\"promptChannel\",\"instance\"]]", + "value": 18, + "path": "src/local.ts", + "rule": "sonarjs/cognitive-complexity", + "scope": "[[\"FunctionDeclaration\",\"promptChannel\",\"instance\"]]", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical maintainability findings replayed at the adoption base; individual measurements cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/local.ts": "4daabceb4d1ba74ca9e60e3674cf0458a360cf4385399d6c59a45a5e0973d613" + } + }, + { + "key": "src/program.ts|max-lines-per-function|[[\"FunctionDeclaration\",\"createProgram\",\"instance\"]]", + "value": 421, + "path": "src/program.ts", + "rule": "max-lines-per-function", + "scope": "[[\"FunctionDeclaration\",\"createProgram\",\"instance\"]]", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical maintainability findings replayed at the adoption base; individual measurements cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/program.ts": "3d32a585101f10cd8b70613697848a8dfce8df0f643118b30b5f79d782a564c7" + } + }, + { + "key": "src/program.ts|max-lines|", + "value": 506, + "path": "src/program.ts", + "rule": "max-lines", + "scope": "", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical maintainability findings replayed at the adoption base; individual measurements cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/program.ts": "3d32a585101f10cd8b70613697848a8dfce8df0f643118b30b5f79d782a564c7" + } + }, + { + "key": "src/rest.ts|max-lines|", + "value": 648, + "path": "src/rest.ts", + "rule": "max-lines", + "scope": "", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical maintainability findings replayed at the adoption base; individual measurements cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/rest.ts": "3ed26d4e118210cf7d6e7bafb590c9a8f7b11c9dd0376a9bfd0eeff2b78b8f52" + } + }, + { + "key": "src/send.ts|complexity|[[\"FunctionDeclaration\",\"handleMessageContact\",\"instance\"]]", + "value": 21, + "path": "src/send.ts", + "rule": "complexity", + "scope": "[[\"FunctionDeclaration\",\"handleMessageContact\",\"instance\"]]", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical maintainability findings replayed at the adoption base; individual measurements cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/send.ts": "9dc6f0a974e9d928255cbb82f929a700130fac7aff815d5915ef731622bb93a8" + } + }, + { + "key": "src/send.ts|max-lines|", + "value": 713, + "path": "src/send.ts", + "rule": "max-lines", + "scope": "", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical maintainability findings replayed at the adoption base; individual measurements cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/send.ts": "9dc6f0a974e9d928255cbb82f929a700130fac7aff815d5915ef731622bb93a8" + } + }, + { + "key": "src/send.ts|max-params|[[\"FunctionDeclaration\",\"handleMessageCarousel\",\"instance\"],[\"VariableDeclaration\",\"body\"],[\"CallExpression\",[\"identifier\",\"withClient\"],2],[\"ArrowFunction\",\"\",\"instance\"]]", + "value": 7, + "path": "src/send.ts", + "rule": "max-params", + "scope": "[[\"FunctionDeclaration\",\"handleMessageCarousel\",\"instance\"],[\"VariableDeclaration\",\"body\"],[\"CallExpression\",[\"identifier\",\"withClient\"],2],[\"ArrowFunction\",\"\",\"instance\"]]", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical maintainability findings replayed at the adoption base; individual measurements cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/send.ts": "9dc6f0a974e9d928255cbb82f929a700130fac7aff815d5915ef731622bb93a8" + } + }, + { + "key": "src/send.ts|sonarjs/cognitive-complexity|[[\"FunctionDeclaration\",\"handleMessageContact\",\"instance\"]]", + "value": 21, + "path": "src/send.ts", + "rule": "sonarjs/cognitive-complexity", + "scope": "[[\"FunctionDeclaration\",\"handleMessageContact\",\"instance\"]]", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical maintainability findings replayed at the adoption base; individual measurements cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/send.ts": "9dc6f0a974e9d928255cbb82f929a700130fac7aff815d5915ef731622bb93a8" + } + } + ] +} diff --git a/quality/baselines/duplicates.json b/quality/baselines/duplicates.json new file mode 100644 index 0000000..e40e461 --- /dev/null +++ b/quality/baselines/duplicates.json @@ -0,0 +1,45 @@ +{ + "schemaVersion": 1, + "adoptionBase": "e5c0a856387326a9786ee9669441fb787de36926", + "entries": [ + { + "key": "typescript|296f1f362436f27192adda6d8dcb2e81f73eab7f8c979766f196f6169b88af10|src/send.ts", + "value": 1, + "paths": ["src/send.ts"], + "tokens": 59, + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/send.ts": "9dc6f0a974e9d928255cbb82f929a700130fac7aff815d5915ef731622bb93a8" + } + }, + { + "key": "typescript|481a91f0567f2761220752e2ca37d954ab23af4c0d83d427319cff8c7bcb667c|src/rest.ts", + "value": 1, + "paths": ["src/rest.ts"], + "tokens": 56, + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/rest.ts": "3ed26d4e118210cf7d6e7bafb590c9a8f7b11c9dd0376a9bfd0eeff2b78b8f52" + } + }, + { + "key": "typescript|b0ade4b300c5e369d4264c263e8700d48683131beed1fe560e97169d00c59afb|src/rest.ts", + "value": 1, + "paths": ["src/rest.ts"], + "tokens": 65, + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/rest.ts": "3ed26d4e118210cf7d6e7bafb590c9a8f7b11c9dd0376a9bfd0eeff2b78b8f52" + } + } + ] +} diff --git a/quality/baselines/unused-full.json b/quality/baselines/unused-full.json new file mode 100644 index 0000000..f92c2cf --- /dev/null +++ b/quality/baselines/unused-full.json @@ -0,0 +1,258 @@ +{ + "schemaVersion": 1, + "adoptionBase": "e5c0a856387326a9786ee9669441fb787de36926", + "entries": [ + { + "key": "src/completion.ts|exports|loadCompletionCache", + "value": 1, + "path": "src/completion.ts", + "rule": "exports", + "symbol": "loadCompletionCache", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/completion.ts": "fed6ce2eb4baedce716a3ab1bb25d76b2b18b182258d7688a855b0375e622185" + } + }, + { + "key": "src/completion.ts|types|CompletionCache", + "value": 1, + "path": "src/completion.ts", + "rule": "types", + "symbol": "CompletionCache", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/completion.ts": "fed6ce2eb4baedce716a3ab1bb25d76b2b18b182258d7688a855b0375e622185" + } + }, + { + "key": "src/config.ts|exports|envBaseUrl", + "value": 1, + "path": "src/config.ts", + "rule": "exports", + "symbol": "envBaseUrl", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/config.ts": "195a13ec65c483ff0518cc81294ea3d215dbea84d73bba7b20c459efbda5f246" + } + }, + { + "key": "src/config.ts|exports|envChannel", + "value": 1, + "path": "src/config.ts", + "rule": "exports", + "symbol": "envChannel", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/config.ts": "195a13ec65c483ff0518cc81294ea3d215dbea84d73bba7b20c459efbda5f246" + } + }, + { + "key": "src/config.ts|types|ChannelBlock", + "value": 1, + "path": "src/config.ts", + "rule": "types", + "symbol": "ChannelBlock", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/config.ts": "195a13ec65c483ff0518cc81294ea3d215dbea84d73bba7b20c459efbda5f246" + } + }, + { + "key": "src/context.ts|types|CliFs", + "value": 1, + "path": "src/context.ts", + "rule": "types", + "symbol": "CliFs", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/context.ts": "d3d2c2c84129e7276dab7febb23b78ffe0a8c9bf2e421396213660a027e6c7d9" + } + }, + { + "key": "src/dest.ts|exports|mergeDest", + "value": 1, + "path": "src/dest.ts", + "rule": "exports", + "symbol": "mergeDest", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/dest.ts": "f149244887863f1d70364c5d501ec51b0dc15a7f6950eaf9fece13e15f214eb7" + } + }, + { + "key": "src/errors.ts|exports|EXIT_API_5XX", + "value": 1, + "path": "src/errors.ts", + "rule": "exports", + "symbol": "EXIT_API_5XX", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/errors.ts": "0b1bfdd43cf890f83f948cc29243e04a57098481abc94c8fbacad379fa56338e" + } + }, + { + "key": "src/errors.ts|exports|EXIT_NETWORK", + "value": 1, + "path": "src/errors.ts", + "rule": "exports", + "symbol": "EXIT_NETWORK", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/errors.ts": "0b1bfdd43cf890f83f948cc29243e04a57098481abc94c8fbacad379fa56338e" + } + }, + { + "key": "src/errors.ts|exports|EXIT_NOT_CONFIGURED", + "value": 1, + "path": "src/errors.ts", + "rule": "exports", + "symbol": "EXIT_NOT_CONFIGURED", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/errors.ts": "0b1bfdd43cf890f83f948cc29243e04a57098481abc94c8fbacad379fa56338e" + } + }, + { + "key": "src/errors.ts|exports|EXIT_USAGE", + "value": 1, + "path": "src/errors.ts", + "rule": "exports", + "symbol": "EXIT_USAGE", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/errors.ts": "0b1bfdd43cf890f83f948cc29243e04a57098481abc94c8fbacad379fa56338e" + } + }, + { + "key": "src/errors.ts|exports|NetworkError", + "value": 1, + "path": "src/errors.ts", + "rule": "exports", + "symbol": "NetworkError", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/errors.ts": "0b1bfdd43cf890f83f948cc29243e04a57098481abc94c8fbacad379fa56338e" + } + }, + { + "key": "src/io.ts|exports|mimeFromName", + "value": 1, + "path": "src/io.ts", + "rule": "exports", + "symbol": "mimeFromName", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/io.ts": "2f9cbed38ec998a0d7ddb33b65507873e16fd97ae571b2e7fb7094a53c764d0a" + } + }, + { + "key": "src/io.ts|exports|splitOnce", + "value": 1, + "path": "src/io.ts", + "rule": "exports", + "symbol": "splitOnce", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/io.ts": "2f9cbed38ec998a0d7ddb33b65507873e16fd97ae571b2e7fb7094a53c764d0a" + } + }, + { + "key": "src/local.ts|exports|resolveHelp", + "value": 1, + "path": "src/local.ts", + "rule": "exports", + "symbol": "resolveHelp", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/local.ts": "4daabceb4d1ba74ca9e60e3674cf0458a360cf4385399d6c59a45a5e0973d613" + } + }, + { + "key": "src/output.ts|exports|normalizeMe", + "value": 1, + "path": "src/output.ts", + "rule": "exports", + "symbol": "normalizeMe", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/output.ts": "fc443a7c02bce1551be7f19e37866baac99936c761481ecf0456cd8ee41cf876" + } + }, + { + "key": "src/output.ts|exports|sanitizeForOutput", + "value": 1, + "path": "src/output.ts", + "rule": "exports", + "symbol": "sanitizeForOutput", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/output.ts": "fc443a7c02bce1551be7f19e37866baac99936c761481ecf0456cd8ee41cf876" + } + }, + { + "key": "src/version.ts|exports|CLI_NAME", + "value": 1, + "path": "src/version.ts", + "rule": "exports", + "symbol": "CLI_NAME", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/version.ts": "53188cecf399e0983fe133085f7e2f7a99b77f5b6eb36456983ad072a8616af1" + } + } + ] +} diff --git a/quality/baselines/unused-production.json b/quality/baselines/unused-production.json new file mode 100644 index 0000000..6b9bb95 --- /dev/null +++ b/quality/baselines/unused-production.json @@ -0,0 +1,300 @@ +{ + "schemaVersion": 1, + "adoptionBase": "e5c0a856387326a9786ee9669441fb787de36926", + "entries": [ + { + "key": "src/commands.ts|exports|OPERATION_COMMANDS", + "value": 1, + "path": "src/commands.ts", + "rule": "exports", + "symbol": "OPERATION_COMMANDS", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/commands.ts": "5d20aa57e5e77a8f0b2dc9c4ad1aca75abefe7f5df6741bb032ec631b296750b" + } + }, + { + "key": "src/commands.ts|exports|SKIPPED_OPERATION_IDS", + "value": 1, + "path": "src/commands.ts", + "rule": "exports", + "symbol": "SKIPPED_OPERATION_IDS", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/commands.ts": "5d20aa57e5e77a8f0b2dc9c4ad1aca75abefe7f5df6741bb032ec631b296750b" + } + }, + { + "key": "src/completion.ts|exports|loadCompletionCache", + "value": 1, + "path": "src/completion.ts", + "rule": "exports", + "symbol": "loadCompletionCache", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/completion.ts": "fed6ce2eb4baedce716a3ab1bb25d76b2b18b182258d7688a855b0375e622185" + } + }, + { + "key": "src/completion.ts|types|CompletionCache", + "value": 1, + "path": "src/completion.ts", + "rule": "types", + "symbol": "CompletionCache", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/completion.ts": "fed6ce2eb4baedce716a3ab1bb25d76b2b18b182258d7688a855b0375e622185" + } + }, + { + "key": "src/config.ts|exports|envBaseUrl", + "value": 1, + "path": "src/config.ts", + "rule": "exports", + "symbol": "envBaseUrl", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/config.ts": "195a13ec65c483ff0518cc81294ea3d215dbea84d73bba7b20c459efbda5f246" + } + }, + { + "key": "src/config.ts|exports|envChannel", + "value": 1, + "path": "src/config.ts", + "rule": "exports", + "symbol": "envChannel", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/config.ts": "195a13ec65c483ff0518cc81294ea3d215dbea84d73bba7b20c459efbda5f246" + } + }, + { + "key": "src/config.ts|types|ChannelBlock", + "value": 1, + "path": "src/config.ts", + "rule": "types", + "symbol": "ChannelBlock", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/config.ts": "195a13ec65c483ff0518cc81294ea3d215dbea84d73bba7b20c459efbda5f246" + } + }, + { + "key": "src/context.ts|types|CliFs", + "value": 1, + "path": "src/context.ts", + "rule": "types", + "symbol": "CliFs", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/context.ts": "d3d2c2c84129e7276dab7febb23b78ffe0a8c9bf2e421396213660a027e6c7d9" + } + }, + { + "key": "src/dest.ts|exports|mergeDest", + "value": 1, + "path": "src/dest.ts", + "rule": "exports", + "symbol": "mergeDest", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/dest.ts": "f149244887863f1d70364c5d501ec51b0dc15a7f6950eaf9fece13e15f214eb7" + } + }, + { + "key": "src/errors.ts|exports|EXIT_API_5XX", + "value": 1, + "path": "src/errors.ts", + "rule": "exports", + "symbol": "EXIT_API_5XX", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/errors.ts": "0b1bfdd43cf890f83f948cc29243e04a57098481abc94c8fbacad379fa56338e" + } + }, + { + "key": "src/errors.ts|exports|EXIT_NETWORK", + "value": 1, + "path": "src/errors.ts", + "rule": "exports", + "symbol": "EXIT_NETWORK", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/errors.ts": "0b1bfdd43cf890f83f948cc29243e04a57098481abc94c8fbacad379fa56338e" + } + }, + { + "key": "src/errors.ts|exports|EXIT_NOT_CONFIGURED", + "value": 1, + "path": "src/errors.ts", + "rule": "exports", + "symbol": "EXIT_NOT_CONFIGURED", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/errors.ts": "0b1bfdd43cf890f83f948cc29243e04a57098481abc94c8fbacad379fa56338e" + } + }, + { + "key": "src/errors.ts|exports|EXIT_USAGE", + "value": 1, + "path": "src/errors.ts", + "rule": "exports", + "symbol": "EXIT_USAGE", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/errors.ts": "0b1bfdd43cf890f83f948cc29243e04a57098481abc94c8fbacad379fa56338e" + } + }, + { + "key": "src/errors.ts|exports|NetworkError", + "value": 1, + "path": "src/errors.ts", + "rule": "exports", + "symbol": "NetworkError", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/errors.ts": "0b1bfdd43cf890f83f948cc29243e04a57098481abc94c8fbacad379fa56338e" + } + }, + { + "key": "src/help-text.ts|exports|HELP_TEXT", + "value": 1, + "path": "src/help-text.ts", + "rule": "exports", + "symbol": "HELP_TEXT", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/help-text.ts": "88cfda8aa4f8fee00a089d7e2b557e80d7e7473d9038ba29bce24a314d0f3527" + } + }, + { + "key": "src/io.ts|exports|mimeFromName", + "value": 1, + "path": "src/io.ts", + "rule": "exports", + "symbol": "mimeFromName", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/io.ts": "2f9cbed38ec998a0d7ddb33b65507873e16fd97ae571b2e7fb7094a53c764d0a" + } + }, + { + "key": "src/io.ts|exports|splitOnce", + "value": 1, + "path": "src/io.ts", + "rule": "exports", + "symbol": "splitOnce", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/io.ts": "2f9cbed38ec998a0d7ddb33b65507873e16fd97ae571b2e7fb7094a53c764d0a" + } + }, + { + "key": "src/local.ts|exports|resolveHelp", + "value": 1, + "path": "src/local.ts", + "rule": "exports", + "symbol": "resolveHelp", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/local.ts": "4daabceb4d1ba74ca9e60e3674cf0458a360cf4385399d6c59a45a5e0973d613" + } + }, + { + "key": "src/output.ts|exports|normalizeMe", + "value": 1, + "path": "src/output.ts", + "rule": "exports", + "symbol": "normalizeMe", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/output.ts": "fc443a7c02bce1551be7f19e37866baac99936c761481ecf0456cd8ee41cf876" + } + }, + { + "key": "src/output.ts|exports|sanitizeForOutput", + "value": 1, + "path": "src/output.ts", + "rule": "exports", + "symbol": "sanitizeForOutput", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/output.ts": "fc443a7c02bce1551be7f19e37866baac99936c761481ecf0456cd8ee41cf876" + } + }, + { + "key": "src/version.ts|exports|CLI_NAME", + "value": 1, + "path": "src/version.ts", + "rule": "exports", + "symbol": "CLI_NAME", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/version.ts": "53188cecf399e0983fe133085f7e2f7a99b77f5b6eb36456983ad072a8616af1" + } + } + ] +} diff --git a/quality/contracts/CLI.md b/quality/contracts/CLI.md new file mode 100644 index 0000000..499f3b2 --- /dev/null +++ b/quality/contracts/CLI.md @@ -0,0 +1,1425 @@ +# 1msg CLI — design (1MSG-164) + +Third client for the public 1msg API, after [`@1msg/sdk`](./SDK.md) and [`@1msg/mcp`](./MCP.md). **The product is the full public API, not an MVP.** `--help` below is the acceptance artifact: every public `operationId` except `deleteMediaLegacy` has a command. Nothing prints `not implemented yet`. + +| | | +|--|--| +| **Binary** | `1msg` | +| **npm** | [`@1msg/cli`](https://www.npmjs.com/package/@1msg/cli) | +| **HTTP** | `@1msg/sdk` only — same invariant as MCP | +| **Coverage** | 61 of 62 public operations (legacy `deleteMediaLegacy` is not exposed) | +| **Hosts** | `https://api.1msg.io` (live) · `https://sandbox.1msg.io` (test) | +| **Distribution repo** | [`1msg/1msg-cli`](https://github.com/1msg/1msg-cli) (public; preferred for npx/clone) | + +> **Distribution:** For `npx @1msg/cli` and clone, use [`1msg/1msg-cli`](https://github.com/1msg/1msg-cli). This monorepo package (`packages/cli`) remains the implementation home. Commands are hand-mapped, not generated from OpenAPI. + +CLI language is English. Complex bodies (`template add`, interactive messages, calling SDP) take flags for scalars and `--from-file` for JSON. + +--- + +## Help (acceptance artifact) + +### `1msg --help` + +``` +1msg — command-line client for the 1msg WhatsApp Business API + +USAGE + 1msg [flags] + +A channel is one WhatsApp instance (instance id + token). Commands use the +default channel from the config file unless --channel is set. Environment +variables fill empty fields on that channel; they do not override a named +channel that already has a value. + +COMMANDS + init Interactive setup: config file, API host, instance, token + send Send a text or file message (alias of message send) + message Send, list, and mark messages + template List, send, create, and delete message templates + channel Local channel switcher; remote status and settings + me WhatsApp Business profile (GET/POST /me) + status Connection status (alias of channel status) + media Upload, fetch, and delete WABA media + group Create and manage WhatsApp groups + flow WhatsApp Flows and business encryption key + webhook Get or set the client webhook URL + user Block, unblock, and list blocked users + catalog Catalog and cart commerce settings + call WhatsApp Calling API (beta, signaling only) + completion Print shell completion script (bash | zsh | powershell) + version Print CLI and SDK versions + +FLAGS + -c, --channel Named channel from the config file + --instance-id Override instance id + --base-url Override API root + --token Override token (prefer ONE_MSG_TOKEN) + --json Print the API response body as JSON + --no-color Disable ANSI color + -h, --help Help for this command + -v, --version Version + +EXAMPLES + $ 1msg init + $ 1msg channel list + $ 1msg channel use acme-prod + $ 1msg channel use ODI371267300 + $ 1msg channel info + $ 1msg channel info --json + $ 1msg send --to 12020721369 --text "Hello" + $ 1msg send --to 12020721369 --file ./invoice.pdf --caption "Invoice" + $ 1msg template list + $ 1msg status --json + $ 1msg message list --chat-id 12020721369@c.us --limit 20 + +ENVIRONMENT + ONE_MSG_CHANNEL Same as --channel + ONE_MSG_BASE_URL https://api.1msg.io or https://sandbox.1msg.io + ONE_MSG_INSTANCE_ID Channel instance id + ONE_MSG_TOKEN Channel API token + + Deprecated aliases (same as MCP): CHAT_API_BASE_URL, CHAT_API_TOKEN, + CHAT_API_INSTANCE_ID, CHAT_API_KEY, TOKEN, INSTANCE_ID. + +Config: ~/.config/1msg/config.yaml (Windows: %AppData%\1msg\config.yaml) +Docs: https://docs.1msg.io/ +``` + +`--json` is the raw API body (ticket: clean server JSON). Default output is a table / key-value. There is no `--raw`. The one CLI-built object is `channel info --json`: `{ "status": , "me": }`. + +### Shared destination flags + +Used by send-like commands unless a command says otherwise. +Exactly one of `--to` or `--chat-id`. Exception: `message payment` is `--to` only +(public schema has no `chatId`). + +``` + --to Phone with country code, no + + --chat-id phone@c.us | group@g.us | BSUID@lid + --quote Reply to this message (wamid.*) +``` + +### Human tables (list commands) + +Default output (no `--json`) is a table. Column names are stable. Extra API +fields are omitted from the table and appear only with `--json`. + +| Command | Columns | Notes | +|---|---|---| +| `channel list` | `default`, `name`, `instance_id`, `base_url` | Local config. `default` is `*` or empty. Never `token`. | +| `message list` | `time`, `chat_id`, `from`, `type`, `id`, `body` | `body` truncated to 60 chars. If API returns `notice` and no rows, print the notice and exit 0. | +| `template list` | `name`, `status`, `language`, `category`, `id` | From normalized `{total, templates[]}`. Footer line: `total N`. | +| `group list` | `id`, `subject` | WABA group id without `@g.us` in `id`. | +| `flow list` | `id`, `name`, `status`, `categories` | `categories` joined with comma. HTTP 501: print API error. | +| `user blocked` | `phone` | First present of `phone`, `wa_id`, `id`. | +| `catalog get` | `id`, `cart`, `visible` | `cart`/`visible` are `true`/`false` from `is_cart_enabled` / `is_catalog_visible`. | +| `channel settings` | key-value, one setting per line | Not a multi-row table. | +| `me` | key-value (`about`, `phone`, `email`, …) | Not a multi-row table. | +| `status` | key-value (`status`, `accountStatus`, `mm_lite_available`) | Not a multi-row table. | + +Empty list: print `No rows.` to stderr and exit 0. + +### `1msg init --help` + +``` +Create a config file and register the first channel. + +USAGE + 1msg init [flags] + +FLAGS + --name Channel name in the config file (default: default) + --base-url https://api.1msg.io or https://sandbox.1msg.io + (also api-stage.1msg.io / http://127.0.0.1:3050) + --instance-id Channel instance id + --token-stdin Read token from stdin (non-interactive) + --no-verify Skip GET /status after save + -h, --help + +EXAMPLES + $ 1msg init + $ printf '%s' "$ONE_MSG_TOKEN" | 1msg init --name prod \ + --base-url https://api.1msg.io --instance-id ODI371267300 --token-stdin + +Interactive prompts: name, live vs sandbox, instance id, token (hidden), +set as default. Writes the config file with mode 0600 and runs 1msg status +unless --no-verify. +``` + +### `1msg channel --help` + +``` +Local named channels, and remote status/settings of the selected channel. + +USAGE + 1msg channel [flags] + +There is no API to list a customer's channels. list / use / add / remove / +current are always local. Other subcommands call the public API. + +LOCAL + list Channels in the config file (* = default) + use Set the default channel (see `1msg channel use --help`) + add Add a channel (see `1msg channel add --help`) + remove Remove a channel from the config file + current Print the default channel name and instance id + +REMOTE + info GET /status + GET /me (human summary, or one JSON object) + status GET /status + settings GET /settings + settings set POST /settings (only flags you pass are sent) + mm-lite GET /mmLiteStatus + automation GET /conversationalAutomation + automation set POST /conversationalAutomation + +EXAMPLES + $ 1msg channel add --name acme-test --base-url https://sandbox.1msg.io \ + --instance-id ODI… --token-stdin + $ 1msg channel use acme-test + $ 1msg channel use ODI371267300 + $ 1msg channel list + $ 1msg channel current + $ 1msg -c acme-prod channel info + $ 1msg channel info --json + $ 1msg channel status --json + $ 1msg me --json + $ 1msg channel settings set --ack-notifications --no-guaranteed-hooks + +COLUMNS (channel list) + default name instance_id base_url + +`channel info` calls GET /status then GET /me. `--json` prints one object: + + { "status": , "me": } + +This is the only CLI-built JSON (not a single server body). If either call +fails, do not print a partial object: emit that call's error (raw body with +`--json`) and that HTTP status; skip the second call if the first failed. +`-c` / `--channel` take a config name, not an instance id. + + +FLAGS (settings set) + --webhook-url Repeatable, max 5; empty string clears webhooks + --guaranteed-hooks Retry webhooks until acknowledged + --no-guaranteed-hooks + --ack-notifications Include ACK / status events + --no-ack-notifications + --raw-hooks Forward raw provider payloads + --no-raw-hooks + --template-analytics Enable template analytics + --no-template-analytics + --cta-tracking-opt-out Opt out of CTA URL click tracking + --no-cta-tracking-opt-out + +FLAGS (automation set) + --welcome Enable welcome message + --no-welcome + --prompt Repeatable, max 4, each ≤ 80 chars + --command Repeatable slash commands + +FLAGS + -c, --channel Selects which config channel (does not filter `list`) + -h, --help +``` + +### `1msg channel use --help` + +``` +Set the default channel in the config file. Local only; no API call. + +USAGE + 1msg channel use + +LOOKUP + 1. Exact key under channels: in the config file (e.g. acme-prod) + 2. Else unique instance_id among channels + 3. Zero matches: exit 1, "channel \"…\" not in config" + 4. Two or more names share that instance_id: exit 1, + "instance id … matches NAME and NAME; pass a config name" + +`--channel` / ONE_MSG_CHANNEL / tab-completion still use config names. +`remove` takes a config name only. + +EXAMPLES + $ 1msg channel use acme-test + $ 1msg channel use ODI371267300 +``` + +### `1msg channel info --help` + +``` +Human summary of the selected channel: GET /status then GET /me. + +USAGE + 1msg channel info [flags] + +FLAGS + --json One object: { "status": , "me": } + -c, --channel + -h, --help + +EXAMPLES + $ 1msg channel info + $ 1msg channel info --json + $ 1msg -c acme-prod channel info --json + +Without --json: key-value from both responses (status, accountStatus, +mm_lite_available, then profile fields from /me). +With --json: the two raw bodies nested under "status" and "me". Not a server +schema — CLI-only. A single endpoint remains `1msg status --json` / `1msg me --json`. +``` + +```json +{ + "status": { + "isCloud": true, + "status": "connected", + "accountStatus": "authenticated", + "mm_lite_available": false + }, + "me": { + "about": "Available 9–18", + "phone": "12020721369" + } +} +``` + +### `1msg channel add --help` + +``` +Add a named channel to the config file. Does not change the default unless +this is the first channel (then it becomes default). + +USAGE + 1msg channel add [flags] + +FLAGS + --name Required. Config name (e.g. acme-prod), not instance id + --base-url https://api.1msg.io | https://sandbox.1msg.io + Also accepts https://api-stage.1msg.io and http://127.0.0.1:3050 + --instance-id Required unless ONE_MSG_INSTANCE_ID is set + --token-stdin Read token from stdin + --default Make this channel the default + --no-verify Skip GET /status after save + -h, --help + +EXAMPLES + $ 1msg channel add --name acme-test --base-url https://sandbox.1msg.io \ + --instance-id ODI371267301 --token-stdin + $ 1msg channel add --name prod --base-url https://api.1msg.io \ + --instance-id ODI371267300 --token-stdin --default + +Non-interactive: --name, --base-url, --instance-id, and a token (stdin or env) +are required. Interactive prompts match `1msg init` when flags are omitted. +Writes ~/.config/1msg/config.yaml mode 0600. +``` + +### `1msg status --help` + +Alias of `1msg channel status` → `GET /status`. + +``` +USAGE + 1msg status [flags] + 1msg channel status [flags] + +Prints connection status (typically `connected` when the number can send), +accountStatus, and mm_lite_available. + +EXAMPLES + $ 1msg status + $ 1msg status --json + $ 1msg -c acme-test status + +COLUMNS (human) + key-value: status, accountStatus, mm_lite_available +``` + +### `1msg send --help` / `1msg message send --help` + +``` +Send a text or file message (session window must be open). +Outside 24h use: 1msg template send + +USAGE + 1msg send [flags] + 1msg message send [flags] + +Exactly one destination: --to or --chat-id. +Exactly one body: --text, --file, or --media-id. + +FLAGS + --to + --chat-id + --text UTF-8 text (max 4096) → sendMessage + --file Local file or https URL → sendFile + --caption Caption when sending a file (max 1024) + --filename File name with extension. Required with an + https --file URL (API requires filename with + body). Optional for a local path (default: + basename). Ignored with --media-id. + --media-id WABA media id from `1msg media upload` + --media-type image | video | audio | document (required with --media-id) + --voice Native voice note (audio/ogg only) + --quote + -h, --help + +EXAMPLES + $ 1msg send --to 12020721369 --text "Hello" + $ 1msg send --chat-id 12020721369@c.us --text "Hello" --quote wamid.HBgN… + $ 1msg send --to 12020721369 --file ./photo.jpg --caption "Look" + $ 1msg send --to 12020721369 --file https://example.com/a.pdf --filename invoice.pdf + $ 1msg send --to 12020721369 --media-id 123456 --media-type image + +A local --file is read from disk, encoded as a data URI, and sent as +sendFile.body. An https URL is passed through as sendFile.body and **requires +--filename**. This does not call uploadMedia unless you run `1msg media upload` +yourself. Exit 1 if --file is an https URL without --filename. + +`1msg send` is only sendMessage + sendFile. The other 15 public send +operations are under `1msg message --help` and `1msg template send`. +``` + +### `1msg message --help` + +``` +Send, list, and mark messages. + +USAGE + 1msg message [flags] + +COMMANDS + send Text or file (see 1msg send) + list Stored chat history (GET /messages) + read Mark a message as read (POST /readMessage) + react Send or remove a reaction + location Send a location pin + location-request Ask the user to share location + contact Send a contact card + buttons Interactive reply buttons (max 3) + menu Interactive list message (WhatsApp list) + carousel Carousel + product Product message + cta CTA URL button + address Address message + order Order details + payment Payment request + sticker Sticker + flow Send a Flow message + +SEND FORMATS (all 17 public send* operations) + send --text POST /sendMessage + send --file|--media-id POST /sendFile (url, data URI, mediaId, --voice) + template send POST /sendTemplate (Cloud API components, MM Lite) + react POST /sendReaction + location POST /sendLocation + location-request POST /sendLocationRequest + contact POST /sendContact + buttons POST /sendButton + menu POST /sendList + carousel POST /sendCarousel + product POST /sendProduct + cta POST /sendCtaUrl + address POST /sendAddressMessage + order POST /sendOrderDetails + payment POST /sendPaymentRequest + sticker POST /sendSticker + flow POST /sendFlow + +`message list` is inbox history. WhatsApp interactive lists are `message menu`. +Carousel-as-template (no 24h window) is `1msg template send --params`, not +`message carousel`. + +EXAMPLES + $ 1msg message send --to 12020721369 --text "Hello" + $ 1msg message list --chat-id 12020721369@c.us --limit 20 + $ 1msg message location --to 12020721369 --lat 55.7558 --lng 37.6173 + $ 1msg message buttons --to 12020721369 --text "Choose:" --button yes:Yes + $ 1msg template send --to 12020721369 --name hello_world --lang en +``` + +#### `1msg message list --help` + +``` +USAGE + 1msg message list [flags] + +FLAGS + --chat-id Filter one chat + --limit Max rows (API default 100) + --last Latest first (DESC) + --msg-id Filter by stored message id + --min-time Lower bound + --max-time Upper bound + --first-number + --last-number + +COLUMNS + time chat_id from type id body + +EXAMPLES + $ 1msg message list + $ 1msg message list --chat-id 12020721369@c.us --limit 20 --last + $ 1msg message list --limit 5 --json +``` + +#### `1msg message read --help` + +``` +USAGE + 1msg message read --message-id [flags] + +FLAGS + --message-id Required. API field messageId / msgId + --typing Show typing indicator (max 25s) + +The read API does not take a chat id. + +EXAMPLES + $ 1msg message read --message-id wamid.HBgNMTIwMjA3MjEzNjkVAgARGBI5RTBCNUY0QUE2RjZBQzhGNDkA + $ 1msg message read --message-id wamid.… --typing +``` + +#### `1msg message react --help` + +Public `POST /sendReaction` fields are `body` (emoji) and `quotedMsgId` (target). +CLI names stay verb+noun: + +``` +USAGE + 1msg message react --message-id --emoji [flags] + +FLAGS + --to / --chat-id Required destination + --message-id Target message → API quotedMsgId + --emoji Reaction → API body. Empty string removes it + +EXAMPLES + $ 1msg message react --to 12020721369 --message-id wamid.… --emoji "👍" + $ 1msg message react --chat-id 12020721369@c.us --message-id wamid.… --emoji "" +``` + +#### `1msg message location --help` + +``` +USAGE + 1msg message location --lat --lng [flags] + +FLAGS + --to / --chat-id / --quote + --lat Required, -90..90 + --lng Required, -180..180 + --name + --address + +EXAMPLES + $ 1msg message location --to 12020721369 --lat 55.7558 --lng 37.6173 \ + --name "Red Square" --address "Moscow" +``` + +#### `1msg message location-request --help` + +``` +USAGE + 1msg message location-request [flags] + +FLAGS + --to / --chat-id / --quote + --text Optional prompt + +EXAMPLES + $ 1msg message location-request --to 12020721369 --text "Share your location" +``` + +#### `1msg message contact --help` + +``` +USAGE + 1msg message contact --name --phone [flags] + +FLAGS + --to / --chat-id / --quote + --name Required (formatted_name) + --first-name + --last-name + --phone Contact's phone (not the destination) + --email + --org + --title + --url + --from-file Full contact JSON (SendContactRequest) + +EXAMPLES + $ 1msg message contact --to 12020721369 --name "John Doe" --phone +12020721369 + $ 1msg message contact --to 12020721369 --from-file contact.json +``` + +#### `1msg message buttons --help` + +``` +USAGE + 1msg message buttons --text --button [flags] + +FLAGS + --to / --chat-id / --quote + --text Required, max 1024 + --footer Max 60 + --button Repeatable, max 3 + --from-file Full SendButtonRequest JSON. Optional + interactive header (text/image/video/document) + is only via --from-file, not a flag. + +EXAMPLES + $ 1msg message buttons --to 12020721369 --text "Choose:" \ + --button yes:Yes --button no:No + $ 1msg message buttons --to 12020721369 --from-file buttons.json +``` + +#### `1msg message menu --help` + +``` +USAGE + 1msg message menu --from-file [flags] + +API requires body, buttonText, and sections[]. sections[] is nested JSON, +so --from-file is required. --text / --button-text / --title / --footer +override the file when set. Destination may be on the CLI or in the file. + +FLAGS + --to / --chat-id / --quote + --from-file Required. `-` = stdin. Public sendList body + --text Override body (max 1024) + --button-text Override list button label (max 20) + --title Max 60 + --footer Max 60 + +EXAMPLES + $ 1msg message menu --to 12020721369 --from-file menu.json + $ 1msg message menu --from-file menu.json --text "Pick one" --button-text "Open" +``` + +#### `1msg message carousel --help` + +Public `POST /sendCarousel` prefers `cards` (2–10). `params` with a CAROUSEL +component is the template-like alternative. Shared `SendCarouselRequest.yaml` +(catalogId/sections) is not the public bundled schema — do not use it. + +``` +USAGE + 1msg message carousel --from-file [flags] + +FLAGS + --to / --chat-id / --quote + --text Text above the carousel + --from-file Required. JSON with cards[] and/or params[] + +EXAMPLES + $ 1msg message carousel --to 12020721369 --from-file carousel.json + $ 1msg message carousel --to 12020721369 --text "Pick a product" --from-file cards.json +``` + +#### `1msg message product --help` + +``` +USAGE + 1msg message product [flags] + +Exactly one of: --from-file, or --catalog-id plus --product-id (single product). +Product lists use --from-file (action.sections). + +FLAGS + --to / --chat-id / --quote + --text + --header Product list only + --footer + --catalog-id + --product-id Single product (action.product_retailer_id) + --from-file Full body including action + +EXAMPLES + $ 1msg message product --to 12020721369 --catalog-id 448008021021719 --product-id 0001 + $ 1msg message product --to 12020721369 --from-file product-list.json +``` + +#### `1msg message cta --help` + +``` +USAGE + 1msg message cta --text --button