Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
6 changes: 6 additions & 0 deletions .changeset/tidy-cli-help.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
6 changes: 3 additions & 3 deletions examples/basic-blooio/README.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
19 changes: 0 additions & 19 deletions examples/basic-blooio/package.json

This file was deleted.

5 changes: 3 additions & 2 deletions examples/basic-blooio/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 .",
Expand All @@ -27,17 +28,23 @@
"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",
"@eslint/js": "^10.0.1",
"@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",
Expand Down
20 changes: 14 additions & 6 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/default.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Command } from 'clipanion';

import type { CliContext } from '../context.js';
import { rootHelp } from '../help.js';

export class DefaultCommand extends Command<CliContext> {
static override paths = [Command.Default];

async execute(): Promise<void> {
this.context.stdout.write(this.cli.usage(null, { detailed: false }));
this.context.stdout.write(rootHelp());
}
}
29 changes: 29 additions & 0 deletions packages/cli/src/help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export function rootHelp(): string {
return `Usage: imessage-cli <command> [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 <path> 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 <command> open, get, or mark a conversation as read
attachment download download an inbound attachment
reaction <command> add or remove a reaction
typing <command> start or stop a typing indicator
provider <command> inspect providers and provider extensions
connection <command> manage secure saved connections
config <command> initialize or validate local configuration
schema [command] print agent-facing JSON Schemas
completion <shell> generate shell completion
webhook serve run the experimental local webhook server

Run imessage-cli <command> --help for detailed command options.
`;
}
10 changes: 10 additions & 0 deletions packages/cli/src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -85,6 +86,15 @@ export function createCli(): Cli<CliContext> {
}

export async function runCli(args: readonly string[], context: CliContext): Promise<number> {
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 {
Expand Down
26 changes: 25 additions & 1 deletion packages/cli/test/cli.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -329,10 +344,15 @@ async function testCommonInteractions(
}

async function command(arguments_: readonly string[]): Promise<CommandResult> {
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;
Expand Down Expand Up @@ -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.`);
Expand Down
31 changes: 31 additions & 0 deletions packages/cli/test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <command> [options]');
expect(test.stdout.text()).toContain('send send text');
expect(test.stdout.text()).toContain(
'Run imessage-cli <command> --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);
Expand Down
37 changes: 12 additions & 25 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading