From bacbe91cf504318bf8657fafeddacba4b5e6a305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Misi=C5=82o?= Date: Sat, 18 Jul 2026 14:47:07 +0200 Subject: [PATCH] chore: prepare imessage cli stable release --- .changeset/pre.json | 3 +- .changeset/tidy-cli-help.md | 6 ++++ README.md | 2 +- examples/basic-blooio/README.md | 6 ++-- examples/basic-blooio/package.json | 19 ------------ examples/basic-blooio/tsup.config.ts | 5 +-- package.json | 11 +++++-- packages/cli/README.md | 20 ++++++++---- packages/cli/src/commands/default.ts | 3 +- packages/cli/src/help.ts | 29 ++++++++++++++++++ packages/cli/src/program.ts | 10 ++++++ packages/cli/test/cli.integration.test.ts | 26 +++++++++++++++- packages/cli/test/cli.test.ts | 31 +++++++++++++++++++ pnpm-lock.yaml | 37 ++++++++--------------- pnpm-workspace.yaml | 1 - 15 files changed, 146 insertions(+), 63 deletions(-) create mode 100644 .changeset/tidy-cli-help.md delete mode 100644 examples/basic-blooio/package.json create mode 100644 packages/cli/src/help.ts diff --git a/.changeset/pre.json b/.changeset/pre.json index 1d188d3..2a32e4c 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,8 +1,7 @@ { - "mode": "pre", + "mode": "exit", "tag": "beta", "initialVersions": { - "@imessage-sdk/example-basic-blooio": "0.0.0", "@imessage-sdk/chat-adapter": "0.1.0", "imessage-cli": "0.0.0", "imessage-sdk": "0.1.3", diff --git a/.changeset/tidy-cli-help.md b/.changeset/tidy-cli-help.md new file mode 100644 index 0000000..885e26d --- /dev/null +++ b/.changeset/tidy-cli-help.md @@ -0,0 +1,6 @@ +--- +'imessage-cli': patch +--- + +Replace the verbose root help screen with a concise command overview and support `-V` as a version +shortcut. diff --git a/README.md b/README.md index 1df1e08..a010794 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ JSON input/output, attachments, replies, normalized operations, and an experimen local signed-webhook server: ```bash -npx imessage-cli@beta send \ +npx imessage-cli send \ --provider blooio \ --to +15551234567 \ --text 'Hello' diff --git a/examples/basic-blooio/README.md b/examples/basic-blooio/README.md index 862fda2..b5dae25 100644 --- a/examples/basic-blooio/README.md +++ b/examples/basic-blooio/README.md @@ -1,6 +1,6 @@ # Basic Blooio example -This private workspace example exercises the public `imessage-sdk` and +This repository example exercises the public `imessage-sdk` and `@imessage-sdk/blooio` packages against a real Blooio account. It covers: @@ -40,7 +40,7 @@ The acknowledgement is required because the example sends real messages and performs interactions. Run it from the repository root: ```bash -pnpm --filter @imessage-sdk/example-basic-blooio start +pnpm start:example:basic-blooio ``` ## Inbound webhooks @@ -54,7 +54,7 @@ BLOOIO_WEBHOOK_SECRET=your-webhook-secret `BLOOIO_WEBHOOK_PORT` is optional and defaults to `3000`. Run: ```bash -pnpm --filter @imessage-sdk/example-basic-blooio webhook +pnpm start:example:basic-blooio:webhook ``` Expose this route through an HTTPS tunnel and configure it in Blooio: diff --git a/examples/basic-blooio/package.json b/examples/basic-blooio/package.json deleted file mode 100644 index 1927f54..0000000 --- a/examples/basic-blooio/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@imessage-sdk/example-basic-blooio", - "version": "0.0.0", - "private": true, - "description": "Live Blooio example for imessage-sdk", - "type": "module", - "scripts": { - "build": "tsup", - "start": "pnpm build && node --env-file=.env dist/index.js", - "typecheck": "tsc --project tsconfig.json --noEmit", - "webhook": "pnpm build && node --env-file=.env dist/webhook.js" - }, - "dependencies": { - "@hono/node-server": "^1.19.9", - "@imessage-sdk/blooio": "workspace:*", - "hono": "^4.11.9", - "imessage-sdk": "workspace:*" - } -} diff --git a/examples/basic-blooio/tsup.config.ts b/examples/basic-blooio/tsup.config.ts index 6735695..5240525 100644 --- a/examples/basic-blooio/tsup.config.ts +++ b/examples/basic-blooio/tsup.config.ts @@ -2,10 +2,11 @@ import { defineConfig } from 'tsup'; export default defineConfig({ entry: { - index: 'src/index.ts', - webhook: 'src/webhook.ts', + index: 'examples/basic-blooio/src/index.ts', + webhook: 'examples/basic-blooio/src/webhook.ts', }, format: ['esm'], + outDir: 'examples/basic-blooio/dist', target: 'es2022', platform: 'node', clean: true, diff --git a/package.json b/package.json index f127b4a..0415b88 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "node": "^20.19.0 || ^22.13.0 || >=24" }, "scripts": { - "build": "pnpm --recursive --if-present build", + "build": "pnpm --recursive --if-present build && pnpm build:example:basic-blooio", + "build:example:basic-blooio": "tsup --config examples/basic-blooio/tsup.config.ts", "changeset": "changeset", "format": "prettier --write .", "format:check": "prettier --check .", @@ -27,10 +28,14 @@ "package:check": "bash scripts/check-packages.sh", "release": "pnpm lint && pnpm build && pnpm typecheck && pnpm test && pnpm package:check && node scripts/publish-packages.mjs", "test": "pnpm --recursive --if-present test", - "typecheck": "pnpm --recursive --if-present typecheck", + "start:example:basic-blooio": "pnpm build:example:basic-blooio && node --env-file=examples/basic-blooio/.env examples/basic-blooio/dist/index.js", + "start:example:basic-blooio:webhook": "pnpm build:example:basic-blooio && node --env-file=examples/basic-blooio/.env examples/basic-blooio/dist/webhook.js", + "typecheck": "pnpm --recursive --if-present typecheck && tsc --project examples/basic-blooio/tsconfig.json --noEmit", "version-packages": "changeset version" }, "devDependencies": { + "@hono/node-server": "^2.0.10", + "@imessage-sdk/blooio": "workspace:^", "@arethetypeswrong/cli": "^0.18.5", "@changesets/changelog-github": "^0.5.2", "@changesets/cli": "^2.29.8", @@ -38,6 +43,8 @@ "@ianvs/prettier-plugin-sort-imports": "^4.7.1", "@types/node": "^20.19.43", "eslint": "^10.7.0", + "hono": "^4.12.30", + "imessage-sdk": "workspace:^", "jiti": "^2.7.0", "prettier": "^3.9.5", "publint": "^0.3.21", diff --git a/packages/cli/README.md b/packages/cli/README.md index 8d0557f..3fbb24c 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -7,21 +7,18 @@ Provider-neutral terminal access to iMessage infrastructure through keeps one provider connection per command, accepts flags or validated JSON input, and emits stable machine-readable results. -> The CLI is currently in beta. Its command and JSON schemas may evolve before the first stable -> release. - ## Installation -Install the current beta globally: +Install globally: ```bash -npm install --global imessage-cli@beta +npm install --global imessage-cli ``` Or run it without a global installation: ```bash -npx imessage-cli@beta --help +npx imessage-cli --help ``` The CLI requires Node.js `^20.19.0`, `^22.13.0`, or `>=24`. @@ -60,6 +57,17 @@ Then run: pnpm --filter imessage-cli test:integration ``` +To verify default saved connections instead of environment credentials, keep the fixture variables +in `.env.cli-test` and run: + +```bash +IMESSAGE_CLI_USE_SAVED_CONNECTIONS=1 \ +pnpm --filter imessage-cli test:integration +``` + +In this mode, the spawned CLI processes receive none of the provider credential environment +variables, so each `--provider` selection must resolve its saved default connection. + The test file is ignored by Git. The runner loads it directly, redacts known secrets from its own failure messages, and uses only per-command environment credentials—no persistent CLI connection or keychain record is created. diff --git a/packages/cli/src/commands/default.ts b/packages/cli/src/commands/default.ts index 2dd4277..7832e66 100644 --- a/packages/cli/src/commands/default.ts +++ b/packages/cli/src/commands/default.ts @@ -1,11 +1,12 @@ import { Command } from 'clipanion'; import type { CliContext } from '../context.js'; +import { rootHelp } from '../help.js'; export class DefaultCommand extends Command { static override paths = [Command.Default]; async execute(): Promise { - this.context.stdout.write(this.cli.usage(null, { detailed: false })); + this.context.stdout.write(rootHelp()); } } diff --git a/packages/cli/src/help.ts b/packages/cli/src/help.ts new file mode 100644 index 0000000..8ef5362 --- /dev/null +++ b/packages/cli/src/help.ts @@ -0,0 +1,29 @@ +export function rootHelp(): string { + return `Usage: imessage-cli [options] + +Send and interact with iMessage through Blooio, Photon, or Sendblue. + +Options: + -V, -v, --version output the version number + --json output stable JSON instead of text + --config use a specific CLI configuration file + --no-input error instead of prompting for missing values + -h, --help display help for command + +Commands: + send send text, attachments, or replies + message get get a provider-native message + conversation open, get, or mark a conversation as read + attachment download download an inbound attachment + reaction add or remove a reaction + typing start or stop a typing indicator + provider inspect providers and provider extensions + connection manage secure saved connections + config initialize or validate local configuration + schema [command] print agent-facing JSON Schemas + completion generate shell completion + webhook serve run the experimental local webhook server + +Run imessage-cli --help for detailed command options. +`; +} diff --git a/packages/cli/src/program.ts b/packages/cli/src/program.ts index daae313..ab7dbd8 100644 --- a/packages/cli/src/program.ts +++ b/packages/cli/src/program.ts @@ -35,6 +35,7 @@ import { SendCommand } from './commands/send.js'; import { TypingStartCommand, TypingStopCommand } from './commands/typing.js'; import { WebhookServeCommand } from './commands/webhook.js'; import { CliUsageError } from './errors.js'; +import { rootHelp } from './help.js'; import { CommandOutput } from './output.js'; const COMMANDS = [ @@ -85,6 +86,15 @@ export function createCli(): Cli { } export async function runCli(args: readonly string[], context: CliContext): Promise { + if (args.length === 1 && (args[0] === '-h' || args[0] === '--help')) { + context.stdout.write(rootHelp()); + return 0; + } + if (args.length === 1 && args[0] === '-V') { + context.stdout.write(`${packageJson.version}\n`); + return 0; + } + const cli = createCli(); let command; try { diff --git a/packages/cli/test/cli.integration.test.ts b/packages/cli/test/cli.integration.test.ts index 791b776..19ce21f 100644 --- a/packages/cli/test/cli.integration.test.ts +++ b/packages/cli/test/cli.integration.test.ts @@ -7,8 +7,23 @@ const execFile = promisify(executeFile); const rootDirectory = new URL('../../../', import.meta.url); const binaryPath = new URL('../dist/cli.js', import.meta.url); const enabled = process.env['IMESSAGE_CLI_RUN_LIVE'] === '1'; +const useSavedConnections = process.env['IMESSAGE_CLI_USE_SAVED_CONNECTIONS'] === '1'; const runId = `cli-live-${Date.now()}`; +const PROVIDER_CREDENTIAL_ENVIRONMENT_VARIABLES = [ + 'BLOOIO_API_KEY', + 'BLOOIO_FROM_NUMBER', + 'BLOOIO_WEBHOOK_SECRET', + 'PHOTON_PROJECT_ID', + 'PHOTON_PROJECT_SECRET', + 'PHOTON_PHONE_NUMBER', + 'PHOTON_WEBHOOK_SECRET', + 'SENDBLUE_API_KEY', + 'SENDBLUE_API_SECRET', + 'SENDBLUE_FROM_NUMBER', + 'SENDBLUE_WEBHOOK_SECRET', +] as const; + interface CommandResult { readonly schemaVersion: 1; readonly ok: true; @@ -329,10 +344,15 @@ async function testCommonInteractions( } async function command(arguments_: readonly string[]): Promise { + const environment = { ...process.env }; + if (useSavedConnections) { + for (const name of PROVIDER_CREDENTIAL_ENVIRONMENT_VARIABLES) delete environment[name]; + } + try { const { stdout } = await execFile(process.execPath, [binaryPath.pathname, ...arguments_], { cwd: rootDirectory.pathname, - env: process.env, + env: environment, maxBuffer: 1024 * 1024, }); const parsed = JSON.parse(stdout) as unknown; @@ -365,6 +385,10 @@ function field(value: unknown, name: string): string { } function required(name: string): string { + if (name === 'IMESSAGE_CLI_TEST_RECIPIENT') { + const override = process.env['IMESSAGE_CLI_TEST_RECIPIENT_OVERRIDE']; + if (override !== undefined && override.length > 0) return override; + } const value = process.env[name]; if (value === undefined || value.length === 0) { throw new Error(`${name} is required when IMESSAGE_CLI_RUN_LIVE=1.`); diff --git a/packages/cli/test/cli.test.ts b/packages/cli/test/cli.test.ts index c5cc82e..3d52b08 100644 --- a/packages/cli/test/cli.test.ts +++ b/packages/cli/test/cli.test.ts @@ -105,6 +105,37 @@ async function temporaryConfig(): Promise<{ readonly directory: string; readonly } describe('imessage-cli', () => { + it.each([ + { name: 'no arguments', arguments: [] as const }, + { name: '-h', arguments: ['-h'] as const }, + { name: '--help', arguments: ['--help'] as const }, + ])('prints concise root help for $name', async ({ arguments: arguments_ }) => { + const config = await temporaryConfig(); + const test = context(config.path); + + const code = await runCli(arguments_, test.context); + + expect(code).toBe(0); + expect(test.stdout.text()).toContain('Usage: imessage-cli [options]'); + expect(test.stdout.text()).toContain('send send text'); + expect(test.stdout.text()).toContain( + 'Run imessage-cli --help for detailed command options.', + ); + expect(test.stdout.text()).not.toContain('--project-secret'); + expect(test.stderr.text()).toBe(''); + }); + + it('supports the conventional uppercase version shortcut', async () => { + const config = await temporaryConfig(); + const test = context(config.path); + + const code = await runCli(['-V'], test.context); + + expect(code).toBe(0); + expect(test.stdout.text()).toMatch(/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\n$/u); + expect(test.stderr.text()).toBe(''); + }); + it('requires an explicit opt-in for the experimental webhook server', async () => { const config = await temporaryConfig(); const test = context(config.path); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af7644e..8ecf4cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,15 +20,27 @@ importers: '@eslint/js': specifier: ^10.0.1 version: 10.0.1(eslint@10.7.0(jiti@2.7.0)) + '@hono/node-server': + specifier: ^2.0.10 + version: 2.0.10(hono@4.12.30) '@ianvs/prettier-plugin-sort-imports': specifier: ^4.7.1 version: 4.7.1(prettier@3.9.5) + '@imessage-sdk/blooio': + specifier: workspace:^ + version: link:packages/providers/blooio '@types/node': specifier: ^20.19.43 version: 20.19.43 eslint: specifier: ^10.7.0 version: 10.7.0(jiti@2.7.0) + hono: + specifier: ^4.12.30 + version: 4.12.30 + imessage-sdk: + specifier: workspace:^ + version: link:packages/imessage-sdk jiti: specifier: ^2.7.0 version: 2.7.0 @@ -51,21 +63,6 @@ importers: specifier: ^4.1.10 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(vite@8.1.4(@types/node@20.19.43)(esbuild@0.27.7)(jiti@2.7.0)) - examples/basic-blooio: - dependencies: - '@hono/node-server': - specifier: ^1.19.9 - version: 1.19.14(hono@4.12.30) - '@imessage-sdk/blooio': - specifier: workspace:* - version: link:../../packages/providers/blooio - hono: - specifier: ^4.11.9 - version: 4.12.30 - imessage-sdk: - specifier: workspace:* - version: link:../../packages/imessage-sdk - packages/chat-adapter: dependencies: '@chat-adapter/shared': @@ -509,12 +506,6 @@ packages: engines: {node: '>=6'} hasBin: true - '@hono/node-server@1.19.14': - resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} - engines: {node: '>=18.14.1'} - peerDependencies: - hono: ^4 - '@hono/node-server@2.0.10': resolution: {integrity: sha512-ZcnNVhKTmyDJeg0UlnZjvM73JBsTAuhrH/J4fjwGOw59PwOW51r4J+p6CsKZWXdKSme4MFqU62CZMOsdDrU4CA==} engines: {node: '>=20'} @@ -3239,10 +3230,6 @@ snapshots: protobufjs: 7.6.5 yargs: 17.7.3 - '@hono/node-server@1.19.14(hono@4.12.30)': - dependencies: - hono: 4.12.30 - '@hono/node-server@2.0.10(hono@4.12.30)': dependencies: hono: 4.12.30 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 335fe43..67aed9b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,7 +1,6 @@ packages: - 'packages/*' - 'packages/providers/*' - - 'examples/*' onlyBuiltDependencies: - esbuild