From 879f92acec9eda1d6c44ec295ba866e47b36d961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Thu, 21 May 2026 02:48:07 +0200 Subject: [PATCH 01/22] wip --- packages/cli/README.md | 4 +- packages/cli/src/commands/setup.ts | 100 ++++++++++++++++++----------- packages/cli/src/commands/watch.ts | 4 +- packages/cli/test/cli-smoke.ts | 27 +++++++- packages/npm/scripts/build.ts | 31 ++++++++- 5 files changed, 122 insertions(+), 44 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index f1167857..6c70613b 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -2415,8 +2415,8 @@ Flags: | Flag | Type | Description | | --- | --- | --- | | `-c, --chat=...` | option | Chat ID to subscribe to. Defaults to all chats. | -| `--exclude-type=...` | option | Drop events of these types. Repeat for multiple. | -| `--include-type=...` | option | Only forward events of these types. Repeat for multiple. | +| `--exclude-type=...` | option | Drop events of these types. Repeat for multiple. | +| `--include-type=...` | option | Only forward events of these types. Repeat for multiple. | | `--webhook=` | option | Forward each event to this URL as a POST request (best-effort, fire-and-forget) | | `--webhook-queue=` | option | Maximum pending webhook deliveries before dropping events Default: 64 | | `--webhook-secret=` | option | HMAC-SHA256 secret. Signs payloads with X-Beeper-Signature: sha256= | diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 5b45031b..96c06787 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -16,6 +16,7 @@ import { readConfig, readTarget, listTargets, + pathExists, saveTargetAuth, updateConfig, writeTarget, @@ -102,7 +103,7 @@ export default class Setup extends BeeperCommand { return } if (flags.json || !process.stdin.isTTY) { - await printData(setupSessionFoundOutput(local, setupCmd), flags.json ? 'json' : 'human') + await printData(setupSessionFoundOutput(local, setupCmd, detected.serverInstalled), flags.json ? 'json' : 'human') return } printLocalDesktopPreview(local) @@ -250,12 +251,14 @@ export default class Setup extends BeeperCommand { } private async setupFromChoice(flags: SetupFlags): Promise { + const serverInstalled = await isServerInstalled() process.stdout.write('No usable Beeper Desktop session was found on this device.\n\n') process.stdout.write('How do you want to connect Beeper CLI?\n\n') process.stdout.write(' 1. Install Beeper Desktop\n') - process.stdout.write(' 2. Install local Beeper Server\n') + process.stdout.write(` 2. ${serverInstalled ? 'Use installed local Beeper Server' : 'Install local Beeper Server'}\n`) process.stdout.write(' 3. Connect with Desktop API on another device\n\n') - const choice = await promptChoice('Choose [1]: ', ['1', '2', '3'], '1') + const defaultChoice = serverInstalled ? '2' : '1' + const choice = await promptChoice(`Choose [${defaultChoice}]: `, ['1', '2', '3'], defaultChoice) if (choice === '1') { if (!await promptYesNoDefaultYes('Install Beeper Desktop stable from beeper.com?')) return await installWithCopy('desktop', { ...flags, channel: 'stable' }) @@ -264,8 +267,10 @@ export default class Setup extends BeeperCommand { return } if (choice === '2') { - if (!await promptYesNoDefaultYes('Install local Beeper Server stable from beeper.com?')) return - await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'production' }) + if (!serverInstalled) { + if (!await promptYesNoDefaultYes('Install local Beeper Server stable from beeper.com?')) return + await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'production' }) + } await this.setupManaged('server', { ...flags, install: false, server: true, channel: 'stable' }) return } @@ -275,12 +280,13 @@ export default class Setup extends BeeperCommand { } private async handleBrokenCurrentTarget(target: Target, readiness: Readiness, flags: SetupFlags): Promise { + const serverInstalled = await isServerInstalled() process.stdout.write(`Beeper CLI is set up for ${target.name ?? target.id}, but it is not reachable.\n\n`) if (readiness.message) process.stdout.write(`${readiness.message}\n\n`) process.stdout.write('What do you want to do?\n\n') process.stdout.write(` 1. Retry ${target.name ?? target.id}\n`) process.stdout.write(' 2. Use Beeper Desktop on this device\n') - process.stdout.write(' 3. Install local Beeper Server\n') + process.stdout.write(` 3. ${serverInstalled ? 'Use installed local Beeper Server' : 'Install local Beeper Server'}\n`) process.stdout.write(' 4. Connect with Desktop API on another device\n\n') const choice = await promptChoice('Choose [1]: ', ['1', '2', '3', '4'], '1') if (choice === '1') return false @@ -290,8 +296,10 @@ export default class Setup extends BeeperCommand { return true } if (choice === '3') { - if (!await promptYesNoDefaultYes('Install local Beeper Server stable from beeper.com?')) return true - await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'production' }) + if (!serverInstalled) { + if (!await promptYesNoDefaultYes('Install local Beeper Server stable from beeper.com?')) return true + await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'production' }) + } await this.setupManaged('server', { ...flags, install: false, server: true, channel: 'stable' }) return true } @@ -336,11 +344,11 @@ type PreparedLocalDesktopSetup = { } type DesktopSetupDetection = - | { kind: 'session-found'; local: PreparedLocalDesktopSetup } - | { kind: 'installed-not-running' } - | { kind: 'running-signed-out'; readiness?: Readiness } - | { kind: 'session-unreadable'; reason: string; readiness?: Readiness } - | { kind: 'not-installed' } + | { kind: 'session-found'; local: PreparedLocalDesktopSetup; serverInstalled: boolean } + | { kind: 'installed-not-running'; serverInstalled: boolean } + | { kind: 'running-signed-out'; readiness?: Readiness; serverInstalled: boolean } + | { kind: 'session-unreadable'; reason: string; readiness?: Readiness; serverInstalled: boolean } + | { kind: 'not-installed'; serverInstalled: boolean } async function setupTarget(flags: SetupFlags): Promise { if (flags['base-url']) return { id: customTargetID, type: 'desktop', baseURL: flags['base-url'] } @@ -397,29 +405,33 @@ async function prepareLocalDesktopSetup(target: Target, flags: SetupFlags): Prom async function detectDesktopSetup(target: Target, flags: SetupFlags): Promise { printProgress(flags, 'Checking Beeper Desktop') - const appInstalled = await isDesktopAppInstalled() + const installations = await readInstallations().catch((): Awaited> => ({})) + const serverInstalled = await isServerInstalled(installations) + const appInstalled = Boolean(installations.desktop?.path || await findDesktopAppPath()) printProgress(flags, 'Reading local Desktop session') const local = await prepareLocalDesktopSetup(target, flags).catch(error => ({ error })) - if (!('error' in local)) return { kind: 'session-found', local } + if (!('error' in local)) return { kind: 'session-found', local, serverInstalled } printProgress(flags, 'Checking Desktop readiness') const desktop = await findLocalDesktop({ baseURL: target.baseURL, scan: target.id === builtInDesktopTargetID, timeoutMs: 500 }).catch(() => undefined) if (desktop) { const readiness = await evaluateReadiness({ baseURL: desktop.baseURL, target: target.id, token: false }) - if (readiness.state === 'needs-login') return { kind: 'running-signed-out', readiness } + if (readiness.state === 'needs-login') return { kind: 'running-signed-out', readiness, serverInstalled } return { kind: 'session-unreadable', reason: local.error instanceof Error ? local.error.message : String(local.error), readiness, + serverInstalled, } } - return appInstalled ? { kind: 'installed-not-running' } : { kind: 'not-installed' } + return appInstalled ? { kind: 'installed-not-running', serverInstalled } : { kind: 'not-installed', serverInstalled } } -async function isDesktopAppInstalled(): Promise { - const installations = await readInstallations().catch((): Awaited> => ({})) - return Boolean(installations.desktop?.path || await findDesktopAppPath()) +async function isServerInstalled(installations?: Awaited>): Promise { + if (process.env.BEEPER_SERVER_BIN) return true + const installation = installations ?? await readInstallations().catch((): Awaited> => ({})) + return Boolean(installation.server?.path && await pathExists(installation.server.path)) } async function commitLocalDesktopSetup(prepared: PreparedLocalDesktopSetup): Promise { @@ -498,7 +510,13 @@ function printLocalDesktopPreview(prepared: PreparedLocalDesktopSetup): void { process.stdout.write('\n') } -function setupSessionFoundOutput(local: PreparedLocalDesktopSetup, setupCmd: string): Record { +function setupSessionFoundOutput(local: PreparedLocalDesktopSetup, setupCmd: string, serverInstalled: boolean): Record { + const availableActions = [ + action('use-desktop-session', `${setupCmd} --local`), + action('desktop-oauth', `${setupCmd} --oauth`), + action('connect-remote', 'beeper setup --remote '), + ] + if (serverInstalled) availableActions.push(installedServerAction(true)) return { state: local.readiness.state === 'ready' ? 'desktop-ready' : 'desktop-session-found', message: local.readiness.state === 'ready' @@ -508,11 +526,7 @@ function setupSessionFoundOutput(local: PreparedLocalDesktopSetup, setupCmd: str readiness: local.readiness, localDesktop: localDesktopPreview(local), recommendedAction: action('use-desktop-session', `${setupCmd} --local`), - availableActions: [ - action('use-desktop-session', `${setupCmd} --local`), - action('desktop-oauth', `${setupCmd} --oauth`), - action('connect-remote', 'beeper setup --remote '), - ], + availableActions, } } @@ -611,6 +625,7 @@ function printNextSteps(): void { function setupStateOutput(detected: Exclude, target: Target): Record { if (detected.kind === 'installed-not-running') { + const serverAction = installedServerAction(detected.serverInstalled) return setupActionEnvelope({ state: 'desktop-installed-not-running', message: 'Beeper Desktop is installed but not running.', @@ -619,24 +634,31 @@ function setupStateOutput(detected: Exclude'), - action('install-server', 'beeper setup --server --install --yes'), + serverAction, ], }) } if (detected.kind === 'running-signed-out') { + const availableActions = [ + action('open-desktop', 'beeper setup --desktop --yes'), + action('connect-remote', 'beeper setup --remote '), + ] + if (detected.serverInstalled) availableActions.push(installedServerAction(true)) return setupActionEnvelope({ state: 'desktop-running-signed-out', message: 'Beeper Desktop is running but not signed in.', target, readiness: detected.readiness, recommendedAction: action('open-desktop', 'beeper setup --desktop --yes'), - availableActions: [ - action('open-desktop', 'beeper setup --desktop --yes'), - action('connect-remote', 'beeper setup --remote '), - ], + availableActions, }) } if (detected.kind === 'session-unreadable') { + const availableActions = [ + action('desktop-oauth', 'beeper setup --oauth --yes'), + action('connect-remote', 'beeper setup --remote '), + ] + if (detected.serverInstalled) availableActions.push(installedServerAction(true)) return setupActionEnvelope({ state: 'desktop-running-session-unreadable', message: 'Beeper Desktop is running, but CLI could not read the local session.', @@ -644,25 +666,29 @@ function setupStateOutput(detected: Exclude'), - ], + availableActions, }) } + const serverAction = installedServerAction(detected.serverInstalled) return setupActionEnvelope({ state: 'desktop-not-installed', message: 'No Beeper Desktop installation was found on this device.', target, - recommendedAction: action('install-desktop', 'beeper setup --desktop --install --yes'), + recommendedAction: detected.serverInstalled ? serverAction : action('install-desktop', 'beeper setup --desktop --install --yes'), availableActions: [ action('install-desktop', 'beeper setup --desktop --install --yes'), - action('install-server', 'beeper setup --server --install --yes'), + serverAction, action('connect-remote', 'beeper setup --remote '), ], }) } +function installedServerAction(installed: boolean): { id: string; command: string } { + return installed + ? action('use-installed-server', 'beeper setup --server --yes') + : action('install-server', 'beeper setup --server --install --yes') +} + function currentTargetBrokenOutput(target: Target, readiness: Readiness): Record { return { state: 'current-target-unreachable', diff --git a/packages/cli/src/commands/watch.ts b/packages/cli/src/commands/watch.ts index 4a4e7e5f..38e28d74 100644 --- a/packages/cli/src/commands/watch.ts +++ b/packages/cli/src/commands/watch.ts @@ -14,8 +14,8 @@ export default class Watch extends BeeperCommand { static override flags = { chat: Flags.string({ char: 'c', multiple: true, description: 'Chat ID to subscribe to. Defaults to all chats.' }), json: Flags.boolean({ default: false, description: 'Print raw JSON, one event per line' }), - 'include-type': Flags.string({ multiple: true, options: ['chat.upserted', 'chat.deleted', 'message.upserted', 'message.deleted'], description: 'Only forward events of these types. Repeat for multiple.' }), - 'exclude-type': Flags.string({ multiple: true, options: ['chat.upserted', 'chat.deleted', 'message.upserted', 'message.deleted'], description: 'Drop events of these types. Repeat for multiple.' }), + 'include-type': Flags.string({ multiple: true, options: ['chat.upserted', 'chat.deleted', 'message.upserted', 'message.deleted', 'message.stream'], description: 'Only forward events of these types. Repeat for multiple.' }), + 'exclude-type': Flags.string({ multiple: true, options: ['chat.upserted', 'chat.deleted', 'message.upserted', 'message.deleted', 'message.stream'], description: 'Drop events of these types. Repeat for multiple.' }), webhook: Flags.string({ description: 'Forward each event to this URL as a POST request (best-effort, fire-and-forget)' }), 'webhook-secret': Flags.string({ description: 'HMAC-SHA256 secret. Signs payloads with X-Beeper-Signature: sha256=' }), 'webhook-queue': Flags.integer({ default: 64, description: 'Maximum pending webhook deliveries before dropping events' }), diff --git a/packages/cli/test/cli-smoke.ts b/packages/cli/test/cli-smoke.ts index f10fb246..1dd76d51 100644 --- a/packages/cli/test/cli-smoke.ts +++ b/packages/cli/test/cli-smoke.ts @@ -1,6 +1,6 @@ import assert from 'node:assert/strict' import { spawnSync } from 'node:child_process' -import { existsSync, readdirSync, rmSync } from 'node:fs' +import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { commandManifest } from '../dist/lib/manifest.js' @@ -235,6 +235,31 @@ envelope = JSON.parse(result.stderr) assert.equal(envelope.success, false) assert.match(envelope.error, /Unknown Beeper target/) +rmSync(configDir, { recursive: true, force: true }) +const fakeServerPath = join(configDir, 'bin', 'beeper-server') +mkdirSync(join(configDir, 'bin'), { recursive: true }) +writeFileSync(fakeServerPath, '#!/bin/sh\n', { mode: 0o755 }) +writeFileSync(join(configDir, 'installations.json'), `${JSON.stringify({ + server: { + kind: 'server', + channel: 'stable', + serverEnv: 'production', + bundleID: 'com.automattic.beeper.server', + version: 'test', + path: fakeServerPath, + feedURL: 'https://example.invalid/feed', + downloadURL: 'https://example.invalid/download', + installedAt: '2026-05-18T00:00:00.000Z', + updatedAt: '2026-05-18T00:00:00.000Z', + }, +}, null, 2)}\n`) +result = run('setup', '--json') +assert.equal(result.status, 0, result.stderr) +envelope = JSON.parse(result.stdout) +assert.equal(envelope.success, true) +assert(envelope.data.availableActions.some(action => action.id === 'use-installed-server' && action.command === 'beeper setup --server --yes')) +assert(!envelope.data.availableActions.some(action => action.id === 'install-server'), 'setup must not offer to reinstall an already installed Server') + const rpcResult = spawnSync(process.execPath, ['./bin/dev.js', 'rpc'], { cwd: root, encoding: 'utf8', diff --git a/packages/npm/scripts/build.ts b/packages/npm/scripts/build.ts index 95b406b9..b8dd6405 100644 --- a/packages/npm/scripts/build.ts +++ b/packages/npm/scripts/build.ts @@ -57,30 +57,41 @@ const expectedBinarySha256 = artifact.binarySha256 || artifact.sha256 if (!existsSync(binPath) || await sha256(binPath).catch(() => '') !== expectedBinarySha256) { const tempDir = join(tmpdir(), \`beeper-cli-\${manifest.version}-\${process.pid}\`) const archivePath = join(tempDir, artifact.file) + const downloadURL = \`https://github.com/beeper/cli/releases/download/v\${manifest.version}/\${artifact.file}\` + logStep(\`installing beeper-cli \${manifest.version} for \${platform}\`) await rm(tempDir, { recursive: true, force: true }) await mkdir(tempDir, { recursive: true }) - await download(\`https://github.com/beeper/cli/releases/download/v\${manifest.version}/\${artifact.file}\`, archivePath) + await download(downloadURL, archivePath) + logStep('verifying download') const actual = await sha256(archivePath) if (actual !== artifact.sha256) { await rm(tempDir, { recursive: true, force: true }) console.error(\`beeper-cli binary checksum mismatch for \${artifact.file}.\`) process.exit(1) } + logStep('extracting binary') await extract(archivePath, tempDir) const extractedBin = join(tempDir, 'bin', manifest.command || 'beeper') await chmod(extractedBin, 0o755) + logStep(\`caching binary in \${cacheDir}\`) await rm(cacheDir, { recursive: true, force: true }) await mkdir(dirname(binPath), { recursive: true }) await rename(extractedBin, binPath) await rm(tempDir, { recursive: true, force: true }) + logStep('ready') } +if (process.env.BEEPER_CLI_LAUNCHER_DEBUG === '1') logStep(\`starting \${binPath}\`) const child = spawn(binPath, process.argv.slice(2), { stdio: 'inherit', env: process.env }) child.on('exit', (code, signal) => { if (signal) process.kill(process.pid, signal) process.exit(code ?? 1) }) +function logStep(message) { + console.error(\`beeper-cli: \${message}\`) +} + function targetPlatform() { const os = osPlatform() const cpu = osArch() @@ -96,11 +107,14 @@ async function sha256(path) { } async function download(url, destination) { + logStep(\`downloading \${artifact.file}\`) await new Promise((resolve, reject) => { get(url, response => { if ([301, 302, 303, 307, 308].includes(response.statusCode ?? 0) && response.headers.location) { response.resume() - download(response.headers.location, destination).then(resolve, reject) + const nextURL = new URL(response.headers.location, url).toString() + logStep(\`redirecting to \${new URL(nextURL).host}\`) + download(nextURL, destination).then(resolve, reject) return } if (response.statusCode !== 200) { @@ -108,7 +122,20 @@ async function download(url, destination) { reject(new Error(\`Download failed with HTTP \${response.statusCode}: \${url}\`)) return } + const total = Number(response.headers['content-length'] ?? 0) + let downloaded = 0 + let nextLoggedPercent = 25 const file = createWriteStream(destination, { mode: 0o755 }) + response.on('data', chunk => { + downloaded += chunk.length + if (!total) return + const percent = Math.floor(downloaded / total * 100) + if (percent >= nextLoggedPercent || percent === 100) { + const milestone = percent === 100 ? 100 : nextLoggedPercent + logStep(\`downloaded \${milestone}%\`) + nextLoggedPercent = milestone + 25 + } + }) response.pipe(file) file.on('finish', () => file.close(resolve)) file.on('error', reject) From 6d302f301215ce5530a92d05cfab1dea72f91bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 17:59:48 +0200 Subject: [PATCH 02/22] wip --- README.md | 36 +- docs/.gitignore | 16 + docs/astro.config.mjs | 100 ++ docs/bun.lock | 871 ++++++++++++++++++ docs/package.json | 19 + docs/public/favicon.svg | 4 + docs/src/assets/logo.svg | 4 + docs/src/content.config.ts | 7 + .../src/content/docs/accounts.mdx | 45 +- docs/src/content/docs/api.mdx | 47 + .../auth.md => docs/src/content/docs/auth.mdx | 38 +- .../src/content/docs/chats.mdx | 35 +- docs/src/content/docs/config.mdx | 55 ++ docs/src/content/docs/connect.mdx | 146 +++ .../src/content/docs/contacts.mdx | 20 +- docs/src/content/docs/exit-codes.mdx | 25 + docs/src/content/docs/export.mdx | 53 ++ docs/src/content/docs/index.mdx | 88 ++ docs/src/content/docs/install.mdx | 76 ++ .../src/content/docs/media.mdx | 22 +- .../src/content/docs/messages.mdx | 42 +- docs/src/content/docs/plugins.mdx | 45 + .../src/content/docs/presence.mdx | 21 +- docs/src/content/docs/quickstart.mdx | 89 ++ .../rpc.md => docs/src/content/docs/rpc.mdx | 26 +- docs/src/content/docs/scripting.mdx | 85 ++ .../send.md => docs/src/content/docs/send.mdx | 46 +- .../src/content/docs/targets.mdx | 34 +- docs/src/content/docs/update.mdx | 39 + docs/src/content/docs/watch.mdx | 66 ++ docs/src/styles/theme.css | 37 + docs/tsconfig.json | 5 + packages/cli/README.md | 412 ++++++--- packages/cli/docs/api.md | 29 - packages/cli/docs/config.md | 32 - packages/cli/docs/export.md | 39 - packages/cli/docs/setup.md | 38 - packages/cli/docs/update.md | 27 - packages/cli/docs/watch.md | 35 - packages/cli/package.json | 3 + packages/cli/scripts/generate-command-map.ts | 4 +- packages/cli/scripts/generate-readme.ts | 44 +- packages/cli/src/commands.generated.ts | 173 ++-- packages/cli/src/commands/accounts/add.ts | 18 +- packages/cli/src/commands/accounts/remove.ts | 6 +- packages/cli/src/commands/accounts/use.ts | 10 +- packages/cli/src/commands/api/post.ts | 6 +- packages/cli/src/commands/api/request.ts | 6 +- .../cli/src/commands/auth/email/response.ts | 6 +- packages/cli/src/commands/auth/logout.ts | 6 +- packages/cli/src/commands/chats/archive.ts | 6 +- packages/cli/src/commands/chats/avatar.ts | 6 +- .../cli/src/commands/chats/description.ts | 6 +- packages/cli/src/commands/chats/disappear.ts | 6 +- packages/cli/src/commands/chats/draft.ts | 10 +- packages/cli/src/commands/chats/focus.ts | 6 +- packages/cli/src/commands/chats/mark-read.ts | 6 +- .../cli/src/commands/chats/mark-unread.ts | 6 +- packages/cli/src/commands/chats/mute.ts | 6 +- .../cli/src/commands/chats/notify-anyway.ts | 6 +- packages/cli/src/commands/chats/pin.ts | 6 +- packages/cli/src/commands/chats/priority.ts | 6 +- packages/cli/src/commands/chats/remind.ts | 6 +- packages/cli/src/commands/chats/rename.ts | 6 +- packages/cli/src/commands/chats/start.ts | 6 +- packages/cli/src/commands/chats/unarchive.ts | 6 +- packages/cli/src/commands/chats/unmute.ts | 6 +- packages/cli/src/commands/chats/unpin.ts | 6 +- packages/cli/src/commands/chats/unremind.ts | 6 +- packages/cli/src/commands/config/reset.ts | 6 +- packages/cli/src/commands/config/set.ts | 6 +- packages/cli/src/commands/contacts/search.ts | 4 +- packages/cli/src/commands/export.ts | 17 +- packages/cli/src/commands/install/desktop.ts | 6 +- packages/cli/src/commands/install/server.ts | 6 +- packages/cli/src/commands/man.ts | 51 +- packages/cli/src/commands/media/download.ts | 7 +- packages/cli/src/commands/messages/delete.ts | 6 +- packages/cli/src/commands/messages/edit.ts | 6 +- packages/cli/src/commands/messages/export.ts | 17 +- packages/cli/src/commands/messages/search.ts | 4 +- packages/cli/src/commands/presence.ts | 6 +- packages/cli/src/commands/resolve/account.ts | 46 + packages/cli/src/commands/resolve/bridge.ts | 49 + packages/cli/src/commands/resolve/chat.ts | 68 ++ packages/cli/src/commands/resolve/contact.ts | 55 ++ packages/cli/src/commands/resolve/target.ts | 52 ++ packages/cli/src/commands/schema.ts | 125 +++ packages/cli/src/commands/send/file.ts | 9 +- packages/cli/src/commands/send/react.ts | 7 +- packages/cli/src/commands/send/sticker.ts | 29 +- packages/cli/src/commands/send/text.ts | 9 +- packages/cli/src/commands/send/unreact.ts | 7 +- packages/cli/src/commands/send/voice.ts | 31 +- packages/cli/src/commands/setup.ts | 18 +- .../cli/src/commands/targets/add/desktop.ts | 6 +- .../cli/src/commands/targets/add/remote.ts | 6 +- .../cli/src/commands/targets/add/server.ts | 6 +- packages/cli/src/commands/targets/disable.ts | 6 +- packages/cli/src/commands/targets/enable.ts | 6 +- packages/cli/src/commands/targets/remove.ts | 6 +- packages/cli/src/commands/targets/restart.ts | 6 +- packages/cli/src/commands/targets/start.ts | 10 +- packages/cli/src/commands/targets/stop.ts | 6 +- packages/cli/src/commands/targets/use.ts | 6 +- packages/cli/src/commands/update.ts | 6 +- packages/cli/src/commands/verify.ts | 6 +- packages/cli/src/commands/verify/approve.ts | 6 +- packages/cli/src/commands/verify/cancel.ts | 6 +- .../cli/src/commands/verify/qr-confirm.ts | 6 +- packages/cli/src/commands/verify/qr-scan.ts | 6 +- .../cli/src/commands/verify/recovery-key.ts | 6 +- .../src/commands/verify/reset-recovery-key.ts | 6 +- .../cli/src/commands/verify/sas-confirm.ts | 6 +- packages/cli/src/commands/verify/sas.ts | 6 +- packages/cli/src/commands/verify/start.ts | 6 +- packages/cli/src/commands/watch.ts | 4 +- packages/cli/src/lib/command-metadata.ts | 54 ++ packages/cli/src/lib/command.ts | 92 +- packages/cli/src/lib/errors.ts | 16 +- packages/cli/src/lib/manifest.ts | 41 +- packages/cli/src/lib/output.ts | 178 +++- packages/cli/src/lib/resolve.ts | 29 +- packages/cli/test/cli-smoke.ts | 35 +- .../test/messages-search-validation.test.ts | 6 +- 125 files changed, 3780 insertions(+), 706 deletions(-) create mode 100644 docs/.gitignore create mode 100644 docs/astro.config.mjs create mode 100644 docs/bun.lock create mode 100644 docs/package.json create mode 100644 docs/public/favicon.svg create mode 100644 docs/src/assets/logo.svg create mode 100644 docs/src/content.config.ts rename packages/cli/docs/accounts.md => docs/src/content/docs/accounts.mdx (55%) create mode 100644 docs/src/content/docs/api.mdx rename packages/cli/docs/auth.md => docs/src/content/docs/auth.mdx (61%) rename packages/cli/docs/chats.md => docs/src/content/docs/chats.mdx (72%) create mode 100644 docs/src/content/docs/config.mdx create mode 100644 docs/src/content/docs/connect.mdx rename packages/cli/docs/contacts.md => docs/src/content/docs/contacts.mdx (60%) create mode 100644 docs/src/content/docs/exit-codes.mdx create mode 100644 docs/src/content/docs/export.mdx create mode 100644 docs/src/content/docs/index.mdx create mode 100644 docs/src/content/docs/install.mdx rename packages/cli/docs/media.md => docs/src/content/docs/media.mdx (50%) rename packages/cli/docs/messages.md => docs/src/content/docs/messages.mdx (70%) create mode 100644 docs/src/content/docs/plugins.mdx rename packages/cli/docs/presence.md => docs/src/content/docs/presence.mdx (56%) create mode 100644 docs/src/content/docs/quickstart.mdx rename packages/cli/docs/rpc.md => docs/src/content/docs/rpc.mdx (58%) create mode 100644 docs/src/content/docs/scripting.mdx rename packages/cli/docs/send.md => docs/src/content/docs/send.mdx (60%) rename packages/cli/docs/targets.md => docs/src/content/docs/targets.mdx (51%) create mode 100644 docs/src/content/docs/update.mdx create mode 100644 docs/src/content/docs/watch.mdx create mode 100644 docs/src/styles/theme.css create mode 100644 docs/tsconfig.json delete mode 100644 packages/cli/docs/api.md delete mode 100644 packages/cli/docs/config.md delete mode 100644 packages/cli/docs/export.md delete mode 100644 packages/cli/docs/setup.md delete mode 100644 packages/cli/docs/update.md delete mode 100644 packages/cli/docs/watch.md create mode 100644 packages/cli/src/commands/resolve/account.ts create mode 100644 packages/cli/src/commands/resolve/bridge.ts create mode 100644 packages/cli/src/commands/resolve/chat.ts create mode 100644 packages/cli/src/commands/resolve/contact.ts create mode 100644 packages/cli/src/commands/resolve/target.ts create mode 100644 packages/cli/src/commands/schema.ts create mode 100644 packages/cli/src/lib/command-metadata.ts diff --git a/README.md b/README.md index 6e4e5cc5..30abd8d7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,15 @@ -# beeper — One CLI for all your chats +
-> Built for you and your agent. Batteries included. +# beeper + +**One CLI for all your chats.** Built for you and your agent — batteries included. + +[![npm](https://img.shields.io/npm/v/beeper-cli.svg?label=npm&color=6E56F8)](https://www.npmjs.com/package/beeper-cli) +[![license](https://img.shields.io/badge/license-MIT-6E56F8.svg)](https://github.com/beeper/desktop-api-cli/blob/main/packages/cli/LICENSE) +[![docs](https://img.shields.io/badge/docs-online-6E56F8.svg)](https://example.com) +[![built with Bun](https://img.shields.io/badge/built%20with-Bun-6E56F8.svg)](https://bun.sh) + +
Talks to Beeper Desktop on this machine, to a Beeper Server you self-host, or to either one running somewhere else. Send and receive across the chat @@ -13,7 +22,7 @@ Facebook Messenger · X (Twitter) DMs · LinkedIn · Slack · Google Messages (RCS/SMS) · Google Chat · Matrix · IRC · Bluesky. Run `beeper bridges list` for the live list on your target. -Command manual: `beeper man` · CLI docs: `beeper docs` +📖 **[Read the docs](https://example.com)** · command manual: `beeper man` · open docs: `beeper docs` ## Features @@ -193,19 +202,22 @@ WhatsApp, Telegram, Discord, iMessage, and the rest show up under `accounts list ## Documentation +Full documentation lives at **[example.com](https://example.com)** +(built from [`docs/`](docs/) with Astro Starlight — a fully static site). + | Topic | Page | Commands | | --- | --- | --- | -| **Setup + install** | [setup](docs/setup.md) · [auth](docs/auth.md) | `setup` · `install desktop` · `install server` · `verify` · `status` · `doctor` · `auth status` | -| **Targets** | [targets](docs/targets.md) | `targets list` · `targets add desktop` · `targets add server` · `targets add remote` · `targets use` · `targets status` · `targets logs` | -| **Bridges + accounts** | [accounts](docs/accounts.md) | `bridges list` · `bridges show` · `accounts list` · `accounts add` · `accounts show` · `accounts use` · `accounts remove` | -| **Chats** | [chats](docs/chats.md) | `chats list` · `chats search` · `chats show` · `chats start` · `chats archive` · `chats pin` · `chats mute` · `chats priority` · `chats remind` · `chats rename` · `chats draft` · `chats focus` | -| **Messages** | [messages](docs/messages.md) · [send](docs/send.md) · [presence](docs/presence.md) | `messages list` · `messages search` · `messages export` · `send text` · `send file` · `send sticker` · `send voice` · `send react` · `presence` | -| **Contacts + media** | [contacts](docs/contacts.md) · [media](docs/media.md) · [export](docs/export.md) | `contacts list` · `contacts search` · `media download` · `export` | -| **Automation** | [watch](docs/watch.md) · [rpc](docs/rpc.md) · [api](docs/api.md) | `watch` · `watch --webhook` · `rpc` · `man` · `api get` · `api post` · `api request` | -| **Maintenance** | [config](docs/config.md) · [update](docs/update.md) | `update` · `config` · `completion` · `docs` · `version` | +| **Setup + install** | [connect](https://example.com/connect/) · [install](https://example.com/install/) · [auth](https://example.com/auth/) | `setup` · `install desktop` · `install server` · `verify` · `status` · `doctor` · `auth status` | +| **Targets** | [targets](https://example.com/targets/) | `targets list` · `targets add desktop` · `targets add server` · `targets add remote` · `targets use` · `targets status` · `targets logs` | +| **Bridges + accounts** | [accounts](https://example.com/accounts/) | `bridges list` · `bridges show` · `accounts list` · `accounts add` · `accounts show` · `accounts use` · `accounts remove` | +| **Chats** | [chats](https://example.com/chats/) | `chats list` · `chats search` · `chats show` · `chats start` · `chats archive` · `chats pin` · `chats mute` · `chats priority` · `chats remind` · `chats rename` · `chats draft` · `chats focus` | +| **Messages** | [messages](https://example.com/messages/) · [send](https://example.com/send/) · [presence](https://example.com/presence/) | `messages list` · `messages search` · `messages export` · `send text` · `send file` · `send sticker` · `send voice` · `send react` · `presence` | +| **Contacts + media** | [contacts](https://example.com/contacts/) · [media](https://example.com/media/) · [export](https://example.com/export/) | `contacts list` · `contacts search` · `media download` · `export` | +| **Automation** | [scripting](https://example.com/scripting/) · [watch](https://example.com/watch/) · [rpc](https://example.com/rpc/) · [api](https://example.com/api/) | `watch` · `watch --webhook` · `rpc` · `man` · `api get` · `api post` · `api request` | +| **Maintenance** | [config](https://example.com/config/) · [update](https://example.com/update/) | `update` · `config` · `completion` · `docs` · `version` | Use `beeper docs` to open the CLI docs and `beeper man` to print the local -command manual. +command manual. To work on the docs site locally: `cd docs && bun install && bun run dev`. ## Configuration diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..294d3b6a --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,16 @@ +# build output +dist/ +# generated types +.astro/ +# dependencies +node_modules/ +# environment +.env +.env.production +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +# macOS +.DS_Store diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs new file mode 100644 index 00000000..2cf60acd --- /dev/null +++ b/docs/astro.config.mjs @@ -0,0 +1,100 @@ +// @ts-check +import { defineConfig } from 'astro/config'; +import starlight from '@astrojs/starlight'; + +// Fully static export. `output: 'static'` is Astro's default; it is set +// explicitly here so the intent is obvious and `astro build` always emits a +// self-contained `./dist` you can drop on any static host or CDN. +// +// When you pick a home for the docs, set `site` to the canonical origin (used +// for sitemap + canonical URLs) and, if serving from a sub-path, set `base`. +export default defineConfig({ + site: 'https://example.com', + base: '/', + output: 'static', + trailingSlash: 'always', + integrations: [ + starlight({ + title: 'Beeper CLI', + description: + 'One CLI for all your chats — WhatsApp, iMessage, Telegram, Signal, Discord and more, shaped for scripts, agents, and humans in a hurry.', + tagline: 'One CLI for all your chats. Built for you and your agent.', + logo: { + src: './src/assets/logo.svg', + replacesTitle: false, + }, + social: [ + { + icon: 'github', + label: 'GitHub', + href: 'https://github.com/beeper/desktop-api-cli', + }, + ], + editLink: { + baseUrl: + 'https://github.com/beeper/desktop-api-cli/edit/main/docs/', + }, + customCss: ['./src/styles/theme.css'], + // Starlight ships full-text search (Pagefind) and dark mode by default. + sidebar: [ + { + label: 'Start here', + items: [ + { label: 'Overview', link: '/' }, + { label: 'Install', link: '/install/' }, + { label: 'Connect a target', link: '/connect/' }, + { label: 'Quick start', link: '/quickstart/' }, + ], + }, + { + label: 'Targets & accounts', + items: [ + { label: 'Targets', link: '/targets/' }, + { label: 'Bridges & accounts', link: '/accounts/' }, + { label: 'Auth & verification', link: '/auth/' }, + ], + }, + { + label: 'Messaging', + items: [ + { label: 'Chats', link: '/chats/' }, + { label: 'Messages', link: '/messages/' }, + { label: 'Sending', link: '/send/' }, + { label: 'Contacts', link: '/contacts/' }, + { label: 'Media', link: '/media/' }, + { label: 'Export', link: '/export/' }, + { label: 'Presence', link: '/presence/' }, + ], + }, + { + label: 'Automation & agents', + items: [ + { label: 'Output & scripting', link: '/scripting/' }, + { label: 'Watch (live events)', link: '/watch/' }, + { label: 'RPC', link: '/rpc/' }, + { label: 'Raw API access', link: '/api/' }, + { label: 'Exit codes', link: '/exit-codes/' }, + ], + }, + { + label: 'Reference', + items: [ + { label: 'Configuration', link: '/config/' }, + { label: 'Plugins', link: '/plugins/' }, + { label: 'Updating', link: '/update/' }, + { + label: 'Full command reference', + link: 'https://github.com/beeper/desktop-api-cli/blob/main/packages/cli/README.md', + attrs: { target: '_blank' }, + }, + { + label: 'Desktop API reference', + link: 'https://developers.beeper.com/desktop-api-reference', + attrs: { target: '_blank' }, + }, + ], + }, + ], + }), + ], +}); diff --git a/docs/bun.lock b/docs/bun.lock new file mode 100644 index 00000000..5aa85d3f --- /dev/null +++ b/docs/bun.lock @@ -0,0 +1,871 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@beeper/cli-docs", + "dependencies": { + "@astrojs/starlight": "^0.39.2", + "astro": "^6.4.2", + "sharp": "^0.34.5", + }, + }, + }, + "packages": { + "@astrojs/compiler": ["@astrojs/compiler@4.0.0", "", {}, "sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA=="], + + "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.0", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-Ry2R3VPeIN4uPCSA4xQc+e+vsJXkalKpEbDc07hV+a/o5Bs2N/s/uDcPJH/05L19DKh9tAy7e6JM3YZ6Cxfezw=="], + + "@astrojs/markdown-remark": ["@astrojs/markdown-remark@7.2.0", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.0", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-+YxmVQu1Bd+MFfSzjq1rOJvD9+nIOJzz5YIIhdIH01RrxRkKbyKoEgyIqP3yv51MhzMDgd79QaPv+kCVPT8vHw=="], + + "@astrojs/mdx": ["@astrojs/mdx@5.0.6", "", { "dependencies": { "@astrojs/markdown-remark": "7.1.2", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.16.0", "es-module-lexer": "^2.0.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^6.0.0" } }, "sha512-4dKe0ZMmqujofPNDHahzClkwinn9f8jHPcaXcgdGvPAlboD2mjzkUCofli2cBnxYAkdfhC6d50gBJ8i/cH8gHw=="], + + "@astrojs/prism": ["@astrojs/prism@4.0.2", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA=="], + + "@astrojs/sitemap": ["@astrojs/sitemap@3.7.3", "", { "dependencies": { "sitemap": "^9.0.0", "stream-replace-string": "^2.0.0", "zod": "^4.3.6" } }, "sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA=="], + + "@astrojs/starlight": ["@astrojs/starlight@0.39.2", "", { "dependencies": { "@astrojs/markdown-remark": "^7.1.1", "@astrojs/mdx": "^5.0.4", "@astrojs/sitemap": "^3.7.2", "@pagefind/default-ui": "^1.3.0", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", "@types/mdast": "^4.0.4", "astro-expressive-code": "^0.42.0", "bcp-47": "^2.1.0", "hast-util-from-html": "^2.0.3", "hast-util-select": "^6.0.4", "hast-util-to-string": "^3.0.1", "hastscript": "^9.0.1", "i18next": "^26.0.7", "js-yaml": "^4.1.1", "klona": "^2.0.6", "magic-string": "^0.30.21", "mdast-util-directive": "^3.1.0", "mdast-util-to-markdown": "^2.1.2", "mdast-util-to-string": "^4.0.0", "pagefind": "^1.3.0", "rehype": "^13.0.2", "rehype-format": "^5.0.1", "remark-directive": "^4.0.0", "ultrahtml": "^1.6.0", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^6.0.0" } }, "sha512-vlw+bwnjtf5buCTUtLU7JfV6D3knslxqnspr6LKs6hfRuFZiyr5hT44F7GyDqR9FKANUqFxnIzWM81F1k/kOUA=="], + + "@astrojs/telemetry": ["@astrojs/telemetry@3.3.2", "", { "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", "is-wsl": "^3.1.1", "which-pm-runs": "^1.1.0" } }, "sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@capsizecss/unpack": ["@capsizecss/unpack@4.0.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA=="], + + "@clack/core": ["@clack/core@1.4.0", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-7Wctjq6f7c1CPz8sPpkwUnz8yRgVANkpNupb81q432FjcJg4l+Sw7XANdNSdWfAKq0IHI0JTcUeK5dxs/HrGPw=="], + + "@clack/prompts": ["@clack/prompts@1.5.0", "", { "dependencies": { "@clack/core": "1.4.0", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-wKh+wTjmrUoUdkZg8KpJO5X+p9PWV+KE9mePseq9UYWkukgTKsGS47RRL2HstwVcvDQH+PenrPJWII8+MfiiyA=="], + + "@ctrl/tinycolor": ["@ctrl/tinycolor@4.2.0", "", {}, "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + + "@expressive-code/core": ["@expressive-code/core@0.42.0", "", { "dependencies": { "@ctrl/tinycolor": "^4.0.4", "hast-util-select": "^6.0.2", "hast-util-to-html": "^9.0.1", "hast-util-to-text": "^4.0.1", "hastscript": "^9.0.0", "postcss": "^8.4.38", "postcss-nested": "^6.0.1", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1" } }, "sha512-MN11+9nfmaC7sYu2BZJXAXqwkBRt8t1xTSqP+Ti1NfTEskgl6xUnzDxoaiQkg0BMzpglA0pys4dpDKquP/cyIw=="], + + "@expressive-code/plugin-frames": ["@expressive-code/plugin-frames@0.42.0", "", { "dependencies": { "@expressive-code/core": "^0.42.0" } }, "sha512-XtkPm+941Uta7Y+81Acv+OA/20F1NJmJhCX6UYGKpqEIGqplNh3PTOhcURp6tcruhlzJcWcvpWy6Oigz3SrjqA=="], + + "@expressive-code/plugin-shiki": ["@expressive-code/plugin-shiki@0.42.0", "", { "dependencies": { "@expressive-code/core": "^0.42.0", "shiki": "^4.0.2" } }, "sha512-PMKey/kLmewttAHQezL+Y5Fx3vVssfDi3+FJOYQQS2mXP3tQspFELtKKAfsXfmSXdToZYgwoO69HJndqfE+09g=="], + + "@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.42.0", "", { "dependencies": { "@expressive-code/core": "^0.42.0" } }, "sha512-l59lUx8fq1v5g6SpmbDjiU0+7IdfbiWnAyRmtTVSpfhyq+nZMN4UcmYyu2b9Mynhzt7Gr+O+cXyEPDNb2AVWVQ=="], + + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], + + "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], + + "@pagefind/darwin-arm64": ["@pagefind/darwin-arm64@1.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ=="], + + "@pagefind/darwin-x64": ["@pagefind/darwin-x64@1.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw=="], + + "@pagefind/default-ui": ["@pagefind/default-ui@1.5.2", "", {}, "sha512-pm1LMnQg8N2B3n2TnjKlhaFihpz6zTiA4HiGQ6/slKO/+8K9CAU5kcjdSSPgpuk1PMuuN4hxLipUIifnrkl3Sg=="], + + "@pagefind/freebsd-x64": ["@pagefind/freebsd-x64@1.5.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA=="], + + "@pagefind/linux-arm64": ["@pagefind/linux-arm64@1.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw=="], + + "@pagefind/linux-x64": ["@pagefind/linux-x64@1.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA=="], + + "@pagefind/windows-arm64": ["@pagefind/windows-arm64@1.5.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g=="], + + "@pagefind/windows-x64": ["@pagefind/windows-x64@1.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg=="], + + "@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.4", "", { "os": "android", "cpu": "arm" }, "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.4", "", { "os": "android", "cpu": "arm64" }, "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.4", "", { "os": "none", "cpu": "arm64" }, "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw=="], + + "@shikijs/core": ["@shikijs/core@4.1.0", "", { "dependencies": { "@shikijs/primitive": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg=="], + + "@shikijs/langs": ["@shikijs/langs@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg=="], + + "@shikijs/primitive": ["@shikijs/primitive@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw=="], + + "@shikijs/themes": ["@shikijs/themes@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw=="], + + "@shikijs/types": ["@shikijs/types@4.1.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + + "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], + + "@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], + + "@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="], + + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="], + + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], + + "astro": ["astro@6.4.2", "", { "dependencies": { "@astrojs/compiler": "^4.0.0", "@astrojs/internal-helpers": "0.10.0", "@astrojs/markdown-remark": "7.2.0", "@astrojs/telemetry": "3.3.2", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^1.1.1", "devalue": "^5.6.3", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.27.3", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "rehype": "^13.0.2", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unist-util-visit": "^5.1.0", "unstorage": "^1.17.5", "vfile": "^6.0.3", "vite": "^7.3.2", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "./bin/astro.mjs" } }, "sha512-8H89CH2dKL5SCU99OCqdU9BGjmPkSJqaPurywj5XMo7eMFGUFD3vsNhdEKnEh4mK4LgGje3/QDTTSIIGst0G0Q=="], + + "astro-expressive-code": ["astro-expressive-code@0.42.0", "", { "dependencies": { "rehype-expressive-code": "^0.42.0" }, "peerDependencies": { "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta" } }, "sha512-aiTePi2Cn0mJPYWZSzP1GcxCinX9mNtJyCCshVVPSg1yRwM7ADvFJOx0FnS440M9t65hp8JH//dc2qr22Bm4ag=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + + "bcp-47": ["bcp-47@2.1.0", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w=="], + + "bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="], + + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + + "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + + "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + + "common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], + + "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], + + "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], + + "css-selector-parser": ["css-selector-parser@3.3.0", "", {}, "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g=="], + + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], + + "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + + "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + + "direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="], + + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + + "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], + + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], + + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], + + "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], + + "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], + + "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], + + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + + "estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="], + + "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], + + "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], + + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + + "expressive-code": ["expressive-code@0.42.0", "", { "dependencies": { "@expressive-code/core": "^0.42.0", "@expressive-code/plugin-frames": "^0.42.0", "@expressive-code/plugin-shiki": "^0.42.0", "@expressive-code/plugin-text-markers": "^0.42.0" } }, "sha512-V5DtJLEKuj4wf9O6IRtPtRObkMVy2ggR+S0MdjrTw6m58krZnDioyhW1si3Y04c5YPeooP4nd85Yq9NwEVHS4g=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], + + "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], + + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="], + + "fontace": ["fontace@0.4.1", "", { "dependencies": { "fontkitten": "^1.0.2" } }, "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw=="], + + "fontkitten": ["fontkitten@1.0.3", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "get-tsconfig": ["get-tsconfig@5.0.0-beta.4", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ=="], + + "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], + + "h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], + + "hast-util-embedded": ["hast-util-embedded@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA=="], + + "hast-util-format": ["hast-util-format@1.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-minify-whitespace": "^1.0.0", "hast-util-phrasing": "^3.0.0", "hast-util-whitespace": "^3.0.0", "html-whitespace-sensitive-tag-names": "^3.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA=="], + + "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], + + "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], + + "hast-util-has-property": ["hast-util-has-property@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA=="], + + "hast-util-is-body-ok-link": ["hast-util-is-body-ok-link@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ=="], + + "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], + + "hast-util-minify-whitespace": ["hast-util-minify-whitespace@1.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-is-element": "^3.0.0", "hast-util-whitespace": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw=="], + + "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], + + "hast-util-phrasing": ["hast-util-phrasing@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-has-property": "^3.0.0", "hast-util-is-body-ok-link": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ=="], + + "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], + + "hast-util-select": ["hast-util-select@6.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "bcp-47-match": "^2.0.0", "comma-separated-tokens": "^2.0.0", "css-selector-parser": "^3.0.0", "devlop": "^1.0.0", "direction": "^2.0.0", "hast-util-has-property": "^3.0.0", "hast-util-to-string": "^3.0.0", "hast-util-whitespace": "^3.0.0", "nth-check": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw=="], + + "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], + + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + + "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], + + "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], + + "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + + "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + + "html-whitespace-sensitive-tag-names": ["html-whitespace-sensitive-tag-names@3.0.1", "", {}, "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA=="], + + "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], + + "i18next": ["i18next@26.3.0", "", { "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-gHSgGpUXVmuqE2El1W61DmxeyeTlFfZgdJRWMo9jScAn5pu7TuTuiccb1zh3E2J9hEBVGJ23+96x0ieBhfuIHA=="], + + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + + "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], + + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + + "is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="], + + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + + "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], + + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + + "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="], + + "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], + + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + + "mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="], + + "mdast-util-directive": ["mdast-util-directive@3.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q=="], + + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-directive": ["micromark-extension-directive@4.0.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], + + "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], + + "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], + + "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], + + "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + + "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], + + "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], + + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + + "node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], + + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + + "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], + + "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + + "p-limit": ["p-limit@7.3.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw=="], + + "p-queue": ["p-queue@9.3.0", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang=="], + + "p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="], + + "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + + "pagefind": ["pagefind@1.5.2", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.5.2", "@pagefind/darwin-x64": "1.5.2", "@pagefind/freebsd-x64": "1.5.2", "@pagefind/linux-arm64": "1.5.2", "@pagefind/linux-x64": "1.5.2", "@pagefind/windows-arm64": "1.5.2", "@pagefind/windows-x64": "1.5.2" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q=="], + + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + + "parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], + + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + + "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], + + "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + + "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], + + "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], + + "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], + + "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + + "rehype": ["rehype@13.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "unified": "^11.0.0" } }, "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A=="], + + "rehype-expressive-code": ["rehype-expressive-code@0.42.0", "", { "dependencies": { "expressive-code": "^0.42.0" } }, "sha512-8rp/1YMEVVSYbtz+bFBx+uSx3vA4i4T8RwRm5Q/IWbucQnnQqQ0hDqtmKOr8tv+59Cik6cu5aH3WPo0I7csuTA=="], + + "rehype-format": ["rehype-format@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-format": "^1.0.0" } }, "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ=="], + + "rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="], + + "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], + + "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], + + "rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="], + + "remark-directive": ["remark-directive@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-directive": "^3.0.0", "micromark-extension-directive": "^4.0.0", "unified": "^11.0.0" } }, "sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA=="], + + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + + "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], + + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + + "remark-smartypants": ["remark-smartypants@3.0.2", "", { "dependencies": { "retext": "^9.0.0", "retext-smartypants": "^6.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA=="], + + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="], + + "retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="], + + "retext-smartypants": ["retext-smartypants@6.2.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="], + + "retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="], + + "rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="], + + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], + + "semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], + + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + + "shiki": ["shiki@4.1.0", "", { "dependencies": { "@shikijs/core": "4.1.0", "@shikijs/engine-javascript": "4.1.0", "@shikijs/engine-oniguruma": "4.1.0", "@shikijs/langs": "4.1.0", "@shikijs/themes": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "sitemap": ["sitemap@9.0.1", "", { "dependencies": { "@types/node": "^24.9.2", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.4.1" }, "bin": { "sitemap": "dist/esm/cli.js" } }, "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ=="], + + "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], + + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="], + + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], + + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + + "svgo": ["svgo@4.0.1", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w=="], + + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], + + "tinyclip": ["tinyclip@0.1.13", "", {}, "sha512-8OqlXQ35euK9+e7L68u8UwcODxkHoIkjbGsgXuARKNyQ5G6xt8nw1YPeMbxMLgCPFkToU+UEK5j05t2t8edKpQ=="], + + "tinyexec": ["tinyexec@1.2.3", "", {}, "sha512-g62dB+w1/OEFnPvmX0yd/HnetYITOL+1nJW7kitOycOeAvmbWC/nu0fwmmQ/kupNojqExzyC/T++pST/jRJ2mQ=="], + + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], + + "ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="], + + "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + + "unifont": ["unifont@0.7.4", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg=="], + + "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-modify-children": ["unist-util-modify-children@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" } }, "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], + + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-children": ["unist-util-visit-children@3.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + + "unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + + "vite": ["vite@7.3.3", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA=="], + + "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], + + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], + + "which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="], + + "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], + + "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + + "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + + "@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@7.1.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.9.1", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "retext-smartypants": "^6.2.0", "shiki": "^4.0.0", "smol-toml": "^1.6.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-caXZ4Dc2St2dW8luEg22GlP0gupLdztCTQE4EzZOxW1pqWXz9mbeJEuHUkgDYcKWW8tjIHkydYDhWLVoxJ327Q=="], + + "@mdx-js/mdx/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], + + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "estree-util-build-jsx/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "@astrojs/mdx/@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.9.1", "", { "dependencies": { "picomatch": "^4.0.4" } }, "sha512-1pWuARqYom/TzuU3+0ZugsTrKlUydWKuULmDqSMTuonY+9IRDUEGKX/8PXQ1nBxRq3w85uGtd9q9SXfqEldMIQ=="], + + "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..6a69220a --- /dev/null +++ b/docs/package.json @@ -0,0 +1,19 @@ +{ + "name": "@beeper/cli-docs", + "private": true, + "type": "module", + "version": "0.0.0", + "description": "Documentation site for the Beeper CLI (Astro Starlight, fully static export).", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro build", + "preview": "astro preview", + "check": "astro check" + }, + "dependencies": { + "@astrojs/starlight": "^0.39.2", + "astro": "^6.4.2", + "sharp": "^0.34.5" + } +} diff --git a/docs/public/favicon.svg b/docs/public/favicon.svg new file mode 100644 index 00000000..1648cdfc --- /dev/null +++ b/docs/public/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/src/assets/logo.svg b/docs/src/assets/logo.svg new file mode 100644 index 00000000..baf84afc --- /dev/null +++ b/docs/src/assets/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/src/content.config.ts b/docs/src/content.config.ts new file mode 100644 index 00000000..6a7b7a02 --- /dev/null +++ b/docs/src/content.config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; +import { docsLoader } from '@astrojs/starlight/loaders'; +import { docsSchema } from '@astrojs/starlight/schema'; + +export const collections = { + docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), +}; diff --git a/packages/cli/docs/accounts.md b/docs/src/content/docs/accounts.mdx similarity index 55% rename from packages/cli/docs/accounts.md rename to docs/src/content/docs/accounts.mdx index 11610a42..7dae136a 100644 --- a/packages/cli/docs/accounts.md +++ b/docs/src/content/docs/accounts.mdx @@ -1,12 +1,26 @@ -# accounts +--- +title: Bridges & accounts +description: List or add chat-network accounts (WhatsApp, Discord, iMessage, …), choose a default account for --account-filtered commands, or remove one. +sidebar: + label: Bridges & accounts +--- -Read when: listing or adding chat-network accounts (WhatsApp, Discord, -iMessage, etc.), choosing a default account for `--account`-filtered -commands, or removing one. +import { Aside } from '@astrojs/starlight/components'; + +A **bridge** is the connector used to add or reconnect a chat account. An +**account** is a signed-in instance of a network on your target. + + ## Commands ```sh +beeper bridges list +beeper bridges show + beeper accounts list [--account SELECTOR]... [--ids] beeper accounts add [bridge] [--flow ID] [--login-id ID] [--cookie name=value]... [--field id=value]... [--webview] [--non-interactive] [--no-guided] beeper accounts show @@ -14,21 +28,22 @@ beeper accounts use # "" clears defaultAccount beeper accounts remove ``` +## Account selectors + +An **account selector** matches by account ID, network name, bridge type/id, or +user identity (display name, username, email, phone). A network name can expand +to multiple matching accounts. + ## Notes -- An *account selector* matches by account ID, network name, bridge type/id, - or user identity (display name, username, email, phone). -- A *bridge* is the connector used to add or reconnect a chat account. -- `accounts add` without a bridge opens the account-connection chooser. -- `bridges list` is the scriptable catalog; `accounts add` is the guided - account connection flow. +- `bridges list` is the scriptable catalog; `accounts add` is the guided account + connection flow. `accounts add` without a bridge opens the connection chooser. - `accounts use NAME` persists `defaultAccount` in CLI config. Subsequent - account-scoped commands fall back to that default when `--account` is - omitted. -- `accounts use ""` clears the default. + account-scoped commands fall back to that default when `--account` is omitted. + `accounts use ""` clears it. - `accounts list --json` annotates the default account with `default: true`. -- For non-interactive sign-in, pass `--flow`, `--field`, and `--cookie` and - add `--non-interactive` to fail instead of prompting. +- For non-interactive sign-in, pass `--flow`, `--field`, and `--cookie`, and add + `--non-interactive` to fail instead of prompting. - For cookie-based sign-in, `--webview` can use Bun.WebView with Chrome to collect cookie fields before falling back to prompts. Chrome remote debugging must be enabled for a visible interactive tab; otherwise Bun may spawn a diff --git a/docs/src/content/docs/api.mdx b/docs/src/content/docs/api.mdx new file mode 100644 index 00000000..5e85ede1 --- /dev/null +++ b/docs/src/content/docs/api.mdx @@ -0,0 +1,47 @@ +--- +title: Raw API access +description: Call raw Desktop API endpoints the CLI doesn't yet wrap with a workflow command. +sidebar: + label: Raw API access +--- + +import { Aside } from '@astrojs/starlight/components'; + + + +## Commands + +```sh +beeper api get [--no-auth] +beeper api post [--body JSON] [--no-auth] +beeper api request [--body JSON] [--no-auth] +``` + +## Notes + +- `` is a Desktop API path, e.g. `/v1/info` or `/v1/chats/{chatID}/read`. +- `--no-auth` calls a public path without the bearer token. +- `--body` is sent as `application/json`; default is `{}` for `post`. +- `api request` lets you hit `GET | POST | PUT | PATCH | DELETE`; the others are + convenience shortcuts. +- `--read-only` blocks `api post` / `api put` / `api patch` / `api delete` / + `api request `. + +## Examples + +```sh +beeper api get /v1/info +beeper api get /v1/chats --json +beeper api post /v1/chats/abc/read --body '{"messageID":"x"}' +beeper api request PATCH /v1/chats/abc --body '{"isPinned":true}' +beeper api request DELETE /v1/chats/abc/messages/def/reactions --body '{"reactionKey":"👍"}' +``` + +## See also + +The full Desktop API surface is documented at the +[Beeper Desktop API reference](https://developers.beeper.com/desktop-api-reference). diff --git a/packages/cli/docs/auth.md b/docs/src/content/docs/auth.mdx similarity index 61% rename from packages/cli/docs/auth.md rename to docs/src/content/docs/auth.mdx index dfd1bbff..b32a6583 100644 --- a/packages/cli/docs/auth.md +++ b/docs/src/content/docs/auth.mdx @@ -1,12 +1,21 @@ -# auth +--- +title: Auth & verification +description: Check sign-in status, clear stored tokens, or drive an end-to-end device-verification flow for encrypted messages. +sidebar: + label: Auth & verification +--- -Read when: checking sign-in status, clearing stored tokens, or driving an -end-to-end device-verification flow for encrypted messages. +import { Aside } from '@astrojs/starlight/components'; `auth` commands inspect and manage CLI-side authentication state and encryption-readiness. The selected target's stored OAuth token lives in the target file under `~/.beeper/targets/`; `BEEPER_ACCESS_TOKEN` overrides it. + + ## Commands ```sh @@ -28,12 +37,18 @@ beeper auth verify cancel ## Notes -- `auth status` reports the token source (env vs. target file) and metadata; it does not call the network. -- `auth logout` revokes the token at the Desktop OAuth endpoint and clears the local copy. -- `auth verify` (no subcommand) walks the most common SAS/emoji verification flow interactively. -- For agents, drive the explicit subcommands (`start` → `sas` → `sas-confirm`) and use `--json` to inspect state. -- `verify status` returns the encryption-readiness state (`ready`, `needs-verification`, `verification-in-progress`). -- `recovery-key` and `reset-recovery-key` apply to the encrypted-messages key, not to Beeper account login. +- `auth status` reports the token source (env vs. target file) and metadata; it + does not call the network. +- `auth logout` revokes the token at the Desktop OAuth endpoint and clears the + local copy. +- `auth verify` (no subcommand) walks the most common SAS/emoji verification + flow interactively. +- For agents, drive the explicit subcommands (`start` → `sas` → `sas-confirm`) + and use `--json` to inspect state. +- `verify status` returns the encryption-readiness state (`ready`, + `needs-verification`, `verification-in-progress`). +- `recovery-key` and `reset-recovery-key` apply to the encrypted-messages key, + not to Beeper account login. ## Examples @@ -44,3 +59,8 @@ beeper auth verify recovery-key --code ABCD-EFGH-IJKL-MNOP beeper auth verify reset-recovery-key beeper auth logout ``` + + diff --git a/packages/cli/docs/chats.md b/docs/src/content/docs/chats.mdx similarity index 72% rename from packages/cli/docs/chats.md rename to docs/src/content/docs/chats.mdx index 11f9f361..7e0fc70d 100644 --- a/packages/cli/docs/chats.md +++ b/docs/src/content/docs/chats.mdx @@ -1,8 +1,15 @@ -# chats +--- +title: Chats +description: List, search, inspect, and change chat state — archive, pin, mute, mark-read, priority, rename, draft, focus, disappearing timer, reminders. +--- -Read when: listing, searching, inspecting, or changing chat state — archive, -pin, mute, mark-read, priority (Inbox vs Low Priority), rename, draft, focus, -disappear timer, reminders. +import { Aside } from '@astrojs/starlight/components'; + + ## Commands @@ -27,15 +34,23 @@ beeper chats unremind --chat SEL [--pick N] beeper chats focus --chat SEL [--message MSG_ID] [--draft TEXT] [--attachment PATH] [--pick N] ``` +## Selecting a chat + +All `--chat` flags accept a **Beeper chat ID, a local chat ID, the exact title, +or search text**. Ambiguous matches return numbered choices; pass `--pick N` to +select one in scripts. + ## Notes -- All `--chat` flags accept a Beeper chat ID, a local chat ID, the exact title, or search text. -- Ambiguous matches return numbered choices; pass `--pick N` to select one. -- `chats list` filters compose: e.g. `--unread --no-muted --pinned` returns only pinned, unread, non-muted chats. -- `chats mute` is currently boolean — the Desktop API does not yet expose a mute duration. -- `chats focus` opens Beeper Desktop on the selected chat (and optionally scrolls to a message or prefills the composer). +- `chats list` filters compose: e.g. `--unread --no-muted --pinned` returns only + pinned, unread, non-muted chats. +- `chats mute` is currently boolean — the Desktop API does not yet expose a mute + duration. +- `chats focus` opens Beeper Desktop on the selected chat (and optionally scrolls + to a message or prefills the composer). - `chats disappear --seconds 0` turns disappearing messages off. -- Labels are not yet supported by the Desktop API; there is no `chats label` command in this CLI. +- Labels are not yet supported by the Desktop API; there is no `chats label` + command in this CLI. ## Examples diff --git a/docs/src/content/docs/config.mdx b/docs/src/content/docs/config.mdx new file mode 100644 index 00000000..b763a9a3 --- /dev/null +++ b/docs/src/content/docs/config.mdx @@ -0,0 +1,55 @@ +--- +title: Configuration +description: Inspect, change, or reset the CLI's local configuration, and the environment variables that override it. +--- + +import { Aside } from '@astrojs/starlight/components'; + +CLI configuration is stored under your user config dir — `~/.beeper/config.json`, +or wherever `BEEPER_CLI_CONFIG_DIR` points. Print the path with `beeper config +path`. The default Beeper Client API target is `http://127.0.0.1:23373`. + + + +## Commands + +```sh +beeper config path +beeper config get [defaultTarget | defaultAccount | baseURL | auth] +beeper config set +beeper config reset +``` + +## Notes + +- `config path` prints the JSON config path (suitable for `cat` or `cd + $(dirname …)`). +- `config get` without a key prints the full config; passing a key prints just + that field. `auth.accessToken` is always redacted. +- `config set ""` clears the field. Only `defaultTarget` and + `defaultAccount` are settable here; other fields are written by commands like + `targets use` and `auth verify`. +- `config reset` deletes the config file. + +## Environment overrides + +| Variable | Effect | +| --- | --- | +| `BEEPER_ACCESS_TOKEN` | Bearer token for the selected target. Overrides stored OAuth login. | +| `BEEPER_DESKTOP_BASE_URL` | Beeper Client API base URL (Desktop or Server). Defaults to `http://127.0.0.1:23373`. | +| `BEEPER_TARGET` | Selects a configured target by name for a single shell. | +| `BEEPER_READONLY` | `1`/`true`/`yes`/`on` enables read-only mode globally. | +| `BEEPER_CLI_CONFIG_DIR` | Override config directory for testing or isolated profiles. | + +## Examples + +```sh +beeper config path +beeper config get --json +beeper config get defaultTarget +beeper config set defaultTarget work +beeper config set defaultAccount "" +beeper config reset +``` diff --git a/docs/src/content/docs/connect.mdx b/docs/src/content/docs/connect.mdx new file mode 100644 index 00000000..b072a530 --- /dev/null +++ b/docs/src/content/docs/connect.mdx @@ -0,0 +1,146 @@ +--- +title: Connect a target +description: A target is the Beeper endpoint the CLI talks to. Connect local Desktop, a self-hosted Server, a remote target over OAuth, or a bearer token in CI. +sidebar: + label: Connect a target + order: 2 +--- + +import { Tabs, TabItem, Aside, Steps } from '@astrojs/starlight/components'; + +A **target** is the Beeper endpoint `beeper` talks to — local Beeper Desktop, +local Beeper Server, or a remote Beeper Desktop or Beeper Server. Pick one of +four paths. `beeper setup` orchestrates all of them. + +The selected target is persisted in `~/.beeper/config.json` (override the whole +config directory with `BEEPER_CLI_CONFIG_DIR`). See [Targets](/targets/) to +manage more than one. + + + + **Default, recommended.** If Beeper Desktop is installed and signed in here, + `beeper setup` discovers it on `http://127.0.0.1:23373` and adopts the + existing session. If it's installed but not running, `setup` offers to launch + it. If it isn't installed at all, `--install` does that in one step. + + ```text + $ beeper setup --desktop --install + ▎ Installed Beeper Desktop (stable) + ▎ Launched Beeper Desktop + next Sign in to Beeper Desktop, then re-run `beeper setup`. + + $ beeper setup + ▎ Connected desktop + accounts whatsapp, telegram + ``` + + Variants: `beeper setup --local` skips discovery and forces the local path; + `beeper install desktop --channel nightly` uses the nightly channel. + + + + For a headless, long-running setup on this machine, install and adopt a + local Beeper Server. The CLI manages the process — `targets + start/stop/restart/logs/enable`. + + ```text + $ beeper setup --server --install + ▎ Installed Beeper Server (stable) + ▎ Started server on http://127.0.0.1:23373 + auth Opening browser to authorize this server… + ▎ Connected server + accounts (none) + next Run `beeper accounts add` to connect a network. + ``` + + Then connect a network — `beeper accounts add` walks each bridge through its + own login (QR, code, OAuth, cookie): + + ```text + $ beeper accounts add + ? Which bridge? whatsapp + Scan this QR code with WhatsApp on your phone: + ▄▄▄▄▄▄▄ ▄ ▄ ▄▄▄▄▄▄▄ + █ ███ █ ▄█▄ █ ███ █ + █ ███ █ ▀█▀ █ ███ █ + ▀▀▀▀▀▀▀ ▀ ▀ ▀▀▀▀▀▀▀ + ▎ Connected whatsapp · +1•••4242 + ``` + + Variants: `beeper install server`, `beeper install server --server-env staging`. + + + + For a Beeper Desktop or Server running on another machine, authorize the CLI + through a browser-based OAuth/PKCE flow. + + ```text + $ beeper setup --remote https://desktop.example.com + ▎ Authorizing https://desktop.example.com + flow OAuth (PKCE) — opening browser… + ▎ Connected remote (desktop.example.com) + accounts whatsapp, telegram, signal + ``` + + Variants: `beeper setup --oauth` (PKCE against the default Beeper auth); + `beeper targets add remote work https://desktop.example.com --default` + registers additional remotes. + + + + For agents, CI, and scripts, hand the CLI a bearer token directly — no + browser, no interactive prompts. + + ```sh + BEEPER_ACCESS_TOKEN=... beeper chats list --json + BEEPER_ACCESS_TOKEN=... BEEPER_DESKTOP_BASE_URL=https://desktop.example.com \ + beeper messages list --chat 10313 --json + ``` + + `BEEPER_ACCESS_TOKEN` overrides any stored OAuth login for the selected + target. See [Configuration](/config/) for every environment override. + + + +## The happy path + +If Beeper Desktop is already on this machine, there's nothing to choose: + + + +1. Run `beeper setup`. It finds Desktop, offers to launch it if needed, and + adopts the session. + + ```text + $ beeper setup + Looking for Beeper Desktop… found, not running. + Launch it now? [Y/n] y + ▎ Launched Beeper Desktop + + $ beeper setup + Use this Desktop session for CLI access? [Y/n] y + ▎ Connected desktop + accounts whatsapp, telegram, imessage + endpoint http://127.0.0.1:23373 + ``` + +2. Confirm you're ready with `beeper status` (or `beeper doctor` for full + setup/auth/encryption diagnostics). + +3. You're connected. Head to the [Quick start](/quickstart/). + + + + + +## Encrypted messages + +Reaching some networks requires device verification for end-to-end encrypted +messages. `beeper status` / `beeper doctor` tell you whether the target is +encryption-ready; [Auth & verification](/auth/) covers the SAS/QR and +recovery-key flows. diff --git a/packages/cli/docs/contacts.md b/docs/src/content/docs/contacts.mdx similarity index 60% rename from packages/cli/docs/contacts.md rename to docs/src/content/docs/contacts.mdx index 06f6e0f1..1aba9d6b 100644 --- a/packages/cli/docs/contacts.md +++ b/docs/src/content/docs/contacts.mdx @@ -1,6 +1,13 @@ -# contacts +--- +title: Contacts +description: Look up contacts across one or more accounts. +--- -Read when: looking up contacts across one or more accounts. +import { Aside } from '@astrojs/starlight/components'; + + ## Commands @@ -12,9 +19,12 @@ beeper contacts show [--account SEL]... ## Notes -- `contacts list` reads merged account contacts; without `--account` it iterates all accounts. -- `contacts search` runs the network search where available and returns merged results across accounts; omitting `--account` searches every account. -- `contacts show` accepts a user ID, display name, or phone/handle and finds it on the first matching account. +- `contacts list` reads merged account contacts; without `--account` it iterates + all accounts. +- `contacts search` runs the network search where available and returns merged + results across accounts; omitting `--account` searches every account. +- `contacts show` accepts a user ID, display name, or phone/handle and finds it + on the first matching account. ## Examples diff --git a/docs/src/content/docs/exit-codes.mdx b/docs/src/content/docs/exit-codes.mdx new file mode 100644 index 00000000..f9a1cce3 --- /dev/null +++ b/docs/src/content/docs/exit-codes.mdx @@ -0,0 +1,25 @@ +--- +title: Exit codes +description: The deterministic exit codes the Beeper CLI returns, so scripts and agents can branch on failure reasons. +--- + +Every command returns a deterministic exit code so scripts and agents can branch +on the failure reason without parsing text. + +| Code | Meaning | +| --- | --- | +| `0` | Success. | +| `1` | Generic runtime error. | +| `2` | Usage error (parsing, validation, missing required flag/arg, read-only refusal). | +| `3` | Auth required (no stored token; sign in or set `BEEPER_ACCESS_TOKEN`). | +| `4` | Target/account not ready (`doctor` reports this when readiness is not `ready`). | +| `5` | Selector matched nothing (unknown target, account, chat, contact). | +| `6` | Ambiguous selector (multiple matches; pass an exact ID or `--pick N`). | + +JSON output preserves the same envelope on failure, written to stderr: + +```json +{"success":false,"data":null,"error":"…","exitCode":N} +``` + +See [Output & scripting](/scripting/) for the full envelope and global flags. diff --git a/docs/src/content/docs/export.mdx b/docs/src/content/docs/export.mdx new file mode 100644 index 00000000..1c603f10 --- /dev/null +++ b/docs/src/content/docs/export.mdx @@ -0,0 +1,53 @@ +--- +title: Export +description: Make a heavy, multi-chat, attachment-including export of Beeper data to disk. Resumable. +--- + +import { Aside } from '@astrojs/starlight/components'; + + + +## Command + +```sh +beeper export + [-o, --out DIR] + [--account SEL]... + [--chat SEL]... + [--limit-chats N] + [--limit-messages N] + [--max-participants N] + [--no-attachments] + [--force] + [--quiet] + [--pick N] +``` + +## On-disk layout + +The export directory contains `accounts.json`, `chats.json`, `manifest.json`, +plus one directory per chat with `chat.json`, `messages.json`, +`messages.markdown`, `messages.html`, attachments, and per-chat checkpoint state. + +## Notes + +- Default `--out` directory is `beeper-export`. +- Exports are **resumable**. Re-running picks up where the last run left off + unless `--force` is set. +- `--max-participants` (default 500) bounds the participant list stored in each + `chat.json`. +- `--no-attachments` skips downloading media; metadata is still recorded. +- `--limit-chats` / `--limit-messages` are intended for sanity-checking large + exports. + +## Examples + +```sh +beeper export --out ./beeper-export +beeper export --chat "Family" --out ./family +beeper export --account whatsapp --no-attachments --quiet +beeper export --force --out ./beeper-export +``` diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx new file mode 100644 index 00000000..07cc2d8b --- /dev/null +++ b/docs/src/content/docs/index.mdx @@ -0,0 +1,88 @@ +--- +title: Beeper CLI +description: One CLI for all your chats — WhatsApp, iMessage, Telegram, Signal, Discord and more. Built for scripts, agents, and humans in a hurry. +template: splash +hero: + tagline: One CLI for all your chats. Built for you and your agent — batteries included. + image: + file: ../../assets/logo.svg + actions: + - text: Quick start + link: /quickstart/ + icon: right-arrow + variant: primary + - text: Install + link: /install/ + icon: download + variant: secondary + - text: View on GitHub + link: https://github.com/beeper/desktop-api-cli + icon: external + variant: minimal +--- + +import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components'; + +`beeper` talks to **Beeper Desktop** on this machine, to a **Beeper Server** you +self-host, or to either one running somewhere else. Send and receive across +every chat network Beeper bridges — from one CLI shaped for scripts, agents, and +humans in a hurry. + +```sh +brew install beeper/tap/cli +beeper setup +beeper send text --to Family --message "on my way" +``` + +## What it does + + + + Local Beeper Desktop (default), a self-hosted Beeper Server you manage from + the CLI, or a remote target over OAuth/PKCE — or a bearer token in CI. + + + List, search, start, archive, pin, mute, rename, focus. Read, edit, delete, + react. Send text, files, stickers, voice, and typing indicators. + + + `--json` everywhere, NDJSON `--events`, a `watch` stream with HMAC-signed + webhooks, `rpc` over stdin/stdout, and `man --json` tool manifests. + + + `--read-only` rejects every mutating command. Writes stay explicit. Plugins + extend the CLI without forking it. + + + +## Supported chat networks + +Everything Beeper's bridges reach — run `beeper bridges list` for the live list +on your target. + +
    +
  • WhatsApp
  • +
  • iMessage
  • +
  • Telegram
  • +
  • Discord
  • +
  • Signal
  • +
  • Instagram DMs
  • +
  • Facebook Messenger
  • +
  • X (Twitter) DMs
  • +
  • LinkedIn
  • +
  • Slack
  • +
  • Google Messages (RCS/SMS)
  • +
  • Google Chat
  • +
  • Matrix
  • +
  • IRC
  • +
  • Bluesky
  • +
+ +## Start here + + + + + + + diff --git a/docs/src/content/docs/install.mdx b/docs/src/content/docs/install.mdx new file mode 100644 index 00000000..4798574f --- /dev/null +++ b/docs/src/content/docs/install.mdx @@ -0,0 +1,76 @@ +--- +title: Install +description: Install the Beeper CLI via Homebrew, npm, or from source. The installed command is `beeper`. +sidebar: + order: 1 +--- + +import { Tabs, TabItem, Aside } from '@astrojs/starlight/components'; + +The package name is `beeper-cli`; the installed command is `beeper`. + + + + Recommended on macOS and Linux. Installs a signed standalone binary. + + ```sh + brew install beeper/tap/cli + ``` + + Upgrade later with `brew upgrade beeper/tap/cli`, or run `beeper update --cli` + to print the right command for your install method. + + + Run it once without installing: + + ```sh + npx beeper-cli --help + ``` + + Or install it globally: + + ```sh + npm install -g beeper-cli + ``` + + The npm package is a thin launcher that downloads, verifies, and runs the + matching standalone binary for your platform. + + + This repo is a [Bun](https://bun.sh) workspace. From the repo root: + + ```sh + bun install + bun --filter @beeper/cli run build + bun --filter @beeper/cli run dev -- --help + ``` + + For local CLI development inside `packages/cli`: + + ```sh + bun run dev -- --help + ``` + + + +## Verify the install + +```sh +beeper version +beeper --help +``` + +The CLI checks for updates in the background and prints a one-line notice when a +newer release is available. It never upgrades itself — see [Updating](/update/). + + + +## Next step + +You have the CLI, but it isn't talking to anything yet. Point it at a Beeper +target — see [Connect a target](/connect/). diff --git a/packages/cli/docs/media.md b/docs/src/content/docs/media.mdx similarity index 50% rename from packages/cli/docs/media.md rename to docs/src/content/docs/media.mdx index 9fc9dd55..79f77a3b 100644 --- a/packages/cli/docs/media.md +++ b/docs/src/content/docs/media.mdx @@ -1,6 +1,13 @@ -# media +--- +title: Media +description: Download a media file attached to a message. +--- -Read when: downloading a media file attached to a message. +import { Aside } from '@astrojs/starlight/components'; + + ## Commands @@ -10,8 +17,10 @@ beeper media download [-o, --out DIR | -] ## Notes -- `` accepts `mxc://` and `localmxc://` URLs (typically taken from a message payload). -- `--out` defaults to `.` (current directory); the file is named from the URL path. +- `` accepts `mxc://` and `localmxc://` URLs (typically taken from a message + payload). +- `--out` defaults to `.` (current directory); the file is named from the URL + path. - `--out -` streams the binary to stdout for piping. ## Examples @@ -20,3 +29,8 @@ beeper media download [-o, --out DIR | -] beeper media download mxc://beeper.com/abc --out ./downloads beeper media download mxc://beeper.com/abc -o - > photo.jpg ``` + +## See also + +For bulk media, [`export`](/export/) downloads every chat's attachments into a +resumable on-disk archive. diff --git a/packages/cli/docs/messages.md b/docs/src/content/docs/messages.mdx similarity index 70% rename from packages/cli/docs/messages.md rename to docs/src/content/docs/messages.mdx index 70ccb009..1d258268 100644 --- a/packages/cli/docs/messages.md +++ b/docs/src/content/docs/messages.mdx @@ -1,7 +1,14 @@ -# messages +--- +title: Messages +description: List, search, show, contextualize, edit, delete, react to, or export messages from chats. +--- -Read when: listing, searching, showing, contextualizing, editing, deleting, -reacting to, or exporting messages from chats. +import { Aside } from '@astrojs/starlight/components'; + + ## Commands @@ -17,19 +24,28 @@ beeper messages unreact --chat SEL --id MSG_ID --reaction KEY [--pick N] # hi beeper messages export --chat SEL [--before-cursor MSG_ID | --after-cursor MSG_ID] [--after ISO] [--before ISO] [--limit N] [--output PATH | -o -] [--asc] [--pick N] ``` -## Notes +## Pagination & filtering -- `--before-cursor` / `--after-cursor` paginate by message ID (the SDK's cursor model). -- `--before` / `--after` in `messages search` and `messages export` filter by ISO timestamp. -- `messages search` rejects an empty query *and* no filter flags with exit code 2 (`usageError`). -- `messages list --sender` filters client-side: `me` (your own messages), `others`, or an exact user ID. +- `--before-cursor` / `--after-cursor` paginate by message ID (the SDK's cursor + model). +- `--before` / `--after` in `messages search` and `messages export` filter by + ISO timestamp. +- `messages list --sender` filters client-side: `me` (your own messages), + `others`, or an exact user ID. - `messages list --asc` reverses the default newest-first order. -- `messages export` writes one chat to JSON. Use top-level `export` for a full - export with transcripts, attachments, and multiple chats. -- `messages export --output -` writes JSON to stdout for piping. -- `messages delete --for-everyone` requires the network supports it; otherwise it falls back to delete-for-you. + +## Notes + +- `messages search` rejects an empty query *and* no filter flags with exit code + `2` (usage error). +- `messages export` writes one chat to JSON. Use top-level [`export`](/export/) + for a full export with transcripts, attachments, and multiple chats. + `messages export --output -` writes JSON to stdout for piping. +- `messages delete --for-everyone` requires the network to support it; otherwise + it falls back to delete-for-you. - `messages edit` only succeeds on your own text messages with no attachments. -- `messages react`/`unreact` are hidden in `--help` in favor of `send react`/`send unreact`. +- `messages react` / `unreact` are hidden in `--help` in favor of + [`send react`](/send/) / `send unreact`. ## Examples diff --git a/docs/src/content/docs/plugins.mdx b/docs/src/content/docs/plugins.mdx new file mode 100644 index 00000000..4fb7916b --- /dev/null +++ b/docs/src/content/docs/plugins.mdx @@ -0,0 +1,45 @@ +--- +title: Plugins +description: Extend the Beeper CLI with optional oclif plugins without forking it, and build your own with the plugin SDK. +--- + +import { Aside } from '@astrojs/starlight/components'; + +Beeper CLI supports optional [oclif](https://oclif.io) plugins, so you can extend +the CLI without forking it. + +## Using plugins + +List recommended Beeper plugins: + +```sh +beeper plugins available +``` + +Install a published plugin: + +```sh +beeper plugins install @beeper/cli-plugin-cloudflare +``` + +## First-party plugins + +| Package | Adds | +| --- | --- | +| `@beeper/cli-plugin-cloudflare` | `targets tunnel` — expose a selected Beeper target through Cloudflare Tunnel. | + +## Building a plugin + +Import from `@beeper/cli/plugin-sdk` and expose oclif commands from your package. +Link a local plugin while working on it: + +```sh +beeper plugins link ./packages/cli-plugin-cloudflare +beeper targets tunnel --help +``` + + diff --git a/packages/cli/docs/presence.md b/docs/src/content/docs/presence.mdx similarity index 56% rename from packages/cli/docs/presence.md rename to docs/src/content/docs/presence.mdx index 85e16d77..5f199844 100644 --- a/packages/cli/docs/presence.md +++ b/docs/src/content/docs/presence.mdx @@ -1,7 +1,13 @@ -# presence +--- +title: Presence +description: Send typing/paused indicators into a chat from a script or agent. +--- -Read when: sending typing/paused indicators into a chat from a script or -agent. +import { Aside } from '@astrojs/starlight/components'; + + ## Commands @@ -11,10 +17,13 @@ beeper presence --chat SEL [--state typing|paused] [--duration SECONDS] [--pick ## Notes -- Requires server-side support; networks without typing notifications return an error. +- Requires server-side support; networks without typing notifications return an + error. - `--state` defaults to `typing`. -- `--duration N` (only valid with `--state typing`) sends `typing`, sleeps N seconds, then sends `paused`. -- The selected chat must be addressable via the usual selector rules (ID, local ID, title, or search text). +- `--duration N` (only valid with `--state typing`) sends `typing`, sleeps N + seconds, then sends `paused`. +- The selected chat must be addressable via the usual selector rules (ID, local + ID, title, or search text). ## Examples diff --git a/docs/src/content/docs/quickstart.mdx b/docs/src/content/docs/quickstart.mdx new file mode 100644 index 00000000..81790cd9 --- /dev/null +++ b/docs/src/content/docs/quickstart.mdx @@ -0,0 +1,89 @@ +--- +title: Quick start +description: From zero to your first sent message in about a minute, on the happy path where Beeper Desktop is already on this machine. +sidebar: + order: 3 +--- + +import { Steps, Aside, LinkCard } from '@astrojs/starlight/components'; + +The happy path: Beeper Desktop is already on this machine. `beeper setup` finds +it, offers to launch it if it isn't running, and adopts the session. + + + +1. **Install** the CLI: + + ```sh + brew install beeper/tap/cli + ``` + +2. **Connect** to your local Desktop: + + ```text + $ beeper setup + Use this Desktop session for CLI access? [Y/n] y + ▎ Connected desktop + accounts whatsapp, telegram, imessage + endpoint http://127.0.0.1:23373 + ``` + +3. **List** your chats: + + ```text + $ beeper chats list --limit 3 + 10313 Family 3 unread + 8951 Alice · + 7204 Eng standup 12 unread + ``` + +4. **Search** across every network at once: + + ```text + $ beeper messages search "flight" + 8951 Alice · "your flight is at 6:40, gate B23" 2d ago + 10313 Family · "what flight are you on?" 1w ago + ``` + +5. **Send** a message: + + ```text + $ beeper send text --to Family --message "on my way" + ▎ Sent Family + message "on my way" + at 2026-05-18T14:02:11Z + ``` + +6. **Export** everything to disk when you want a backup: + + ```text + $ beeper export --out ./beeper-export + ▎ Exported ./beeper-export + chats 214 messages 38,901 attachments 1,205 + ``` + + + +## Addressing chats + +Recipients (`--to`, `--chat`) accept a numeric local chat ID, a full +Beeper/Matrix chat ID, an iMessage chat ID, an exact title, or search text. +Ambiguous matches prompt in a TTY; pass `--pick N` in scripts. + +```sh +beeper send text --to 10313 --message "by local id" +beeper send text --to "Family" --message "by exact title" +beeper send text --to "@alice:beeper.com" --message "by full chat id" +``` + + + +## Where to next + + + + diff --git a/packages/cli/docs/rpc.md b/docs/src/content/docs/rpc.mdx similarity index 58% rename from packages/cli/docs/rpc.md rename to docs/src/content/docs/rpc.mdx index d58ccb64..518b95b8 100644 --- a/packages/cli/docs/rpc.md +++ b/docs/src/content/docs/rpc.mdx @@ -1,7 +1,14 @@ -# rpc +--- +title: RPC +description: Drive many CLI commands from a long-lived process over newline-delimited JSON on stdin/stdout — no new process per command. +--- -Read when: scripting many CLI commands from a long-lived process (an agent, a -web server) without spawning a new node process per command. +import { Aside } from '@astrojs/starlight/components'; + + ## Command @@ -9,8 +16,8 @@ web server) without spawning a new node process per command. beeper rpc ``` -Reads newline-delimited JSON requests on stdin and writes one response line -per request on stdout. +Reads newline-delimited JSON requests on stdin and writes one response line per +request on stdout. ## Request shape @@ -21,9 +28,9 @@ per request on stdout. Each request must include one of: -- `command` — a single string parsed with shell-like quoting -- `args` — an explicit `argv` array -- `argv` — alias for `args` +- `command` — a single string parsed with shell-like quoting; +- `args` — an explicit `argv` array; +- `argv` — alias for `args`. `id` is echoed back in the response (string, number, or null). @@ -40,7 +47,8 @@ Each request must include one of: - Nesting `rpc` or `shell` is rejected to avoid recursion. - `--json` on inner commands produces the standard envelope inside `stdout`. -- Exit codes use the same table as direct CLI invocation; see [exit codes](../README.md#exit-codes). +- Exit codes use the same table as direct CLI invocation; see [Exit + codes](/exit-codes/). ## Examples diff --git a/docs/src/content/docs/scripting.mdx b/docs/src/content/docs/scripting.mdx new file mode 100644 index 00000000..1ece1386 --- /dev/null +++ b/docs/src/content/docs/scripting.mdx @@ -0,0 +1,85 @@ +--- +title: Output & scripting +description: JSON output, NDJSON events, global flags, addressing rules, and the conventions that make the Beeper CLI safe to drive from scripts and agents. +sidebar: + label: Output & scripting + order: 1 +--- + +import { Aside } from '@astrojs/starlight/components'; + +The CLI is designed to be driven by humans *and* programs. Every command prints +human-friendly text by default and switches to a stable machine envelope on +`--json`. + +## Output modes + +Most commands support: + +- **app-like text by default**, optimized for scanning chats, messages, contacts, + accounts, and media; +- **`--json`** for a `{"success":true,"data":...,"error":null}` envelope on stdout; +- **`--events`** for NDJSON lifecycle events on stderr from long-running commands; +- **`--read-only`** to reject commands that modify Beeper or local CLI state; +- **`--full`** to disable truncation; +- **`--debug`** for SDK debug logging; +- **`--target`** or **`--base-url`** to point at a different target. + +The JSON envelope is stable across success and failure: + +```json +{"success":true,"data":{ /* … */ },"error":null} +{"success":false,"data":null,"error":"…","exitCode":3} +``` + +On failure the envelope is written to **stderr** and the process exits with the +matching [exit code](/exit-codes/). + +## Global flags + +`--base-url` · `--target` · `--json` · `--events` · `--full` · `--timeout` · +`--read-only` · `--debug` · `--yes` · `--quiet` + + + +## Addressing + +- Chat arguments accept numeric local chat IDs, full Beeper/Matrix chat IDs, + iMessage chat IDs, exact titles, or search text. +- For scripts on the same target/profile, prefer the **numeric local chat ID** + shown by `beeper chats list`; use the **full Beeper/Matrix chat ID** when the + selector must work across targets or profiles. +- Numeric local chat IDs come from the selected Desktop database — treat them as + local to that target/profile. +- Ambiguous chat matches return numbered choices; pass `--pick N` to select one. +- Account arguments accept account IDs, network names, bridge type/id, or + account user identity. A network name can expand to multiple matching accounts. +- `contacts search` and `chats start` search across all accounts when + `--account` is omitted; `contacts list` accepts the same account selectors as + other account-scoped commands. + +## For agents + +- `man --json` prints a compact command manifest for tools and agents. +- `rpc` runs newline-delimited JSON command RPC over stdin/stdout — see [RPC](/rpc/). +- `watch` streams live events and can forward them to an HMAC-signed webhook — + see [Watch](/watch/). +- Raw, un-wrapped endpoints are reachable under [`api`](/api/). + +## Example: a JSON pipeline + +```sh +# Unread chat titles, newest first, as a plain list: +beeper chats list --unread --json \ + | jq -r '.data[].title' + +# Send to every pinned chat: +beeper chats list --pinned --json \ + | jq -r '.data[].id' \ + | while read -r id; do + beeper send text --to "$id" --message "heads up" --json + done +``` diff --git a/packages/cli/docs/send.md b/docs/src/content/docs/send.mdx similarity index 60% rename from packages/cli/docs/send.md rename to docs/src/content/docs/send.mdx index d8d01f04..b57b8cf5 100644 --- a/packages/cli/docs/send.md +++ b/docs/src/content/docs/send.mdx @@ -1,7 +1,16 @@ -# send +--- +title: Sending +description: Send text, files, reactions, stickers, or voice notes from scripts or interactive use. +sidebar: + label: Sending +--- -Read when: sending text, files, reactions, stickers, or voice notes from -scripts or interactive use. +import { Aside } from '@astrojs/starlight/components'; + + ## Commands @@ -14,21 +23,30 @@ beeper send react --to SEL --id MSG_ID --reaction KEY [--transaction TX_ID] [- beeper send unreact --to SEL --id MSG_ID --reaction KEY [--pick N] ``` +## Addressing + +`--to` accepts a chat ID, local chat ID, exact title, or search text. Prefer +numeric local chat IDs from `beeper chats list` when scripting against the same +target/profile; use full Beeper/Matrix chat IDs for selectors that need to work +across targets or profiles. + +## Confirming delivery + +Send commands return when Desktop **accepts** the send request. Use `--wait` when +you need to know whether the message left the pending state or failed — it blocks +until the message leaves pending (or fails). Default poll cap: `--wait-timeout +30000` ms. + ## Notes -- `--to` accepts a chat ID, local chat ID, exact title, or search text. -- Prefer numeric local chat IDs from `beeper chats list` when scripting against - the same target/profile. Use full Beeper/Matrix chat IDs for selectors that - need to work across targets or profiles. -- Send commands return when Desktop accepts the send request. Use `--wait` when - you need to know whether the message left the pending state or failed. -- `--wait` blocks until the message leaves the pending state (or fails). Default poll cap: `--wait-timeout 30000` ms. - `--reply-to` quotes an existing message ID. -- `send text --mention ` adds a Matrix mention; repeat for multiple users. -- `send text --no-preview` disables automatic link previews. +- `send text --mention ` adds a Matrix mention; repeat for multiple + users. `send text --no-preview` disables automatic link previews. - `send sticker` defaults `--mime` to `image/webp`; stickers should be 512×512. -- `send voice` defaults `--mime` to `audio/ogg`; pass `--duration` to override the detected length. -- `send file` accepts any file up to 500 MB. MIME type is detected from the upload if `--mime` is omitted. +- `send voice` defaults `--mime` to `audio/ogg`; pass `--duration` to override + the detected length. +- `send file` accepts any file up to 500 MB. MIME type is detected from the + upload if `--mime` is omitted. ## Examples diff --git a/packages/cli/docs/targets.md b/docs/src/content/docs/targets.mdx similarity index 51% rename from packages/cli/docs/targets.md rename to docs/src/content/docs/targets.mdx index d77ee831..5349538d 100644 --- a/packages/cli/docs/targets.md +++ b/docs/src/content/docs/targets.mdx @@ -1,13 +1,18 @@ -# targets +--- +title: Targets +description: Manage local Desktop, managed Server, and remote Beeper targets — add, switch, start/stop a managed runtime, or remove a target. +--- -Read when: managing local Desktop, managed Server, or remote Beeper API -targets — adding, switching, starting/stopping a managed runtime, or removing -a target. +import { Aside } from '@astrojs/starlight/components'; -A *target* is a runnable or reachable Beeper endpoint profile: local Server, -local Desktop, Desktop API, or a profile that combines Desktop/Server runtime -state. The CLI tracks an optional default; commands use it unless -`--target ` overrides. +A **target** is a runnable or reachable Beeper endpoint profile: local Desktop, +local Server, or a remote Desktop/Server. The CLI tracks an optional default; +commands use it unless `--target ` overrides. + + ## Commands @@ -27,12 +32,13 @@ beeper targets remove ## Notes -- `list` prints all configured targets; the one used by default has `default: true`. +- `list` prints all configured targets; the default one has `default: true`. - `show` defaults to the currently-selected target if no name is given. - `status` checks endpoint and process reachability. For setup/auth/encryption diagnostics use `beeper doctor`. -- `start`/`stop`/`restart` only apply to managed targets (`type: desktop|server`); they error for `remote`. -- `enable`/`disable` registers/unregisters the launchd or systemd unit that +- `start` / `stop` / `restart` only apply to managed targets (`type: + desktop|server`); they error for `remote`. +- `enable` / `disable` registers or unregisters the launchd or systemd unit that starts the managed target at login. - Removing the active default clears the `defaultTarget` config field. - `BEEPER_TARGET=` overrides the default for a single shell. @@ -48,3 +54,9 @@ beeper targets use work beeper targets logs work | less beeper targets restart work ``` + +## See also + +- [Connect a target](/connect/) — first-time setup for each target type. +- [Auth & verification](/auth/) — sign-in state and encryption readiness. +- [Configuration](/config/) — where the selected target is stored. diff --git a/docs/src/content/docs/update.mdx b/docs/src/content/docs/update.mdx new file mode 100644 index 00000000..5a6a827d --- /dev/null +++ b/docs/src/content/docs/update.mdx @@ -0,0 +1,39 @@ +--- +title: Updating +description: Check for new versions of the CLI, the CLI-managed Desktop install, or the CLI-managed Server install — and choose whether to install. +sidebar: + label: Updating +--- + +import { Aside } from '@astrojs/starlight/components'; + + + +## Command + +```sh +beeper update [--check] [--cli] [--desktop] [--server] +``` + +## Notes + +- With no kind flag, checks all three (CLI, Desktop, Server) that apply. +- `--check` prints what's available without installing. +- The CLI itself is **never auto-upgraded**; `--cli` prints the right command for + your install method (Homebrew, npm-global, or in-repo git build). +- `--desktop` reports on the CLI-owned Desktop install; updating Desktop itself + happens inside the Desktop app. +- `--server` updates the CLI-managed Server install in place, then restarts any + running managed Server targets. + +## Examples + +```sh +beeper update --check +beeper update --cli +beeper update --desktop --json +beeper update --server +``` diff --git a/docs/src/content/docs/watch.mdx b/docs/src/content/docs/watch.mdx new file mode 100644 index 00000000..79e15916 --- /dev/null +++ b/docs/src/content/docs/watch.mdx @@ -0,0 +1,66 @@ +--- +title: Watch (live events) +description: Subscribe to live Desktop API events — new/updated/deleted chats and messages — and optionally forward them to a webhook. +sidebar: + label: Watch (live events) +--- + +import { Aside } from '@astrojs/starlight/components'; + + + +## Commands + +```sh +beeper watch + [-c, --chat CHAT_ID]... + [--include-type EVENT_TYPE]... + [--exclude-type EVENT_TYPE]... + [--webhook URL [--webhook-secret SECRET] [--webhook-queue N]] + [--json] +``` + +## Notes + +- Subscribes to the Desktop API WebSocket at the path returned by `/v1/info` + (defaults to `/v1/ws`). +- Without `--chat`, subscribes to all chats. +- Event types come from the Desktop API: `chat.upserted`, `chat.deleted`, + `message.upserted`, `message.deleted`. +- `--include-type` and `--exclude-type` are mutually exclusive. +- `--webhook URL` forwards every event as a POST body (best-effort, + fire-and-forget). +- `--webhook-secret SECRET` signs the body with HMAC-SHA256 and sets + `X-Beeper-Signature: sha256=`. +- `--webhook-queue` (default 64) caps pending deliveries; excess events are + dropped with a stderr warning. +- `--quiet` suppresses the human-mode status line; `--json` prints raw events + line-delimited. + +## Examples + +```sh +beeper watch +beeper watch --chat '!abc:beeper.com' --json +beeper watch --include-type message.upserted --include-type message.deleted +beeper watch --webhook https://example.com/hook --webhook-secret "$BEEPER_WEBHOOK_SECRET" +``` + +## Verifying webhook signatures + +Each delivery is signed with `X-Beeper-Signature: sha256=` over the raw +request body. Recompute the HMAC with your shared secret and compare: + +```js +import { createHmac, timingSafeEqual } from 'node:crypto'; + +function verify(rawBody, header, secret) { + const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex'); + const a = Buffer.from(header); + const b = Buffer.from(expected); + return a.length === b.length && timingSafeEqual(a, b); +} +``` diff --git a/docs/src/styles/theme.css b/docs/src/styles/theme.css new file mode 100644 index 00000000..83769cc8 --- /dev/null +++ b/docs/src/styles/theme.css @@ -0,0 +1,37 @@ +/* Beeper CLI docs — brand accent on top of Starlight defaults. */ +:root { + /* Purple accent ramp (light) */ + --sl-color-accent-low: #e0d9ff; + --sl-color-accent: #6e56f8; + --sl-color-accent-high: #3f2db8; +} + +:root[data-theme='dark'] { + /* Purple accent ramp (dark) */ + --sl-color-accent-low: #2a2160; + --sl-color-accent: #8b78ff; + --sl-color-accent-high: #d6cdff; +} + +/* Roomier hero on the landing page. */ +.hero > .stack { + gap: clamp(1rem, 5vw, 2rem); +} + +/* Tighten the supported-networks list rendered on the overview page. */ +.networks { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + margin-block: 1rem; + padding: 0; + list-style: none; +} + +.networks li { + border: 1px solid var(--sl-color-gray-5); + border-radius: 999px; + padding: 0.15rem 0.7rem; + font-size: var(--sl-text-sm); + white-space: nowrap; +} diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 00000000..8bf91d3b --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +} diff --git a/packages/cli/README.md b/packages/cli/README.md index 6c70613b..3173f930 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,6 +1,15 @@ -# beeper — One CLI for all your chats +
-> Built for you and your agent. Batteries included. +# beeper + +**One CLI for all your chats.** Built for you and your agent — batteries included. + +[![npm](https://img.shields.io/npm/v/beeper-cli.svg?label=npm&color=6E56F8)](https://www.npmjs.com/package/beeper-cli) +[![license](https://img.shields.io/badge/license-MIT-6E56F8.svg)](https://github.com/beeper/desktop-api-cli/blob/main/packages/cli/LICENSE) +[![docs](https://img.shields.io/badge/docs-online-6E56F8.svg)](https://example.com) +[![built with Bun](https://img.shields.io/badge/built%20with-Bun-6E56F8.svg)](https://bun.sh) + +
Talks to Beeper Desktop on this machine, to a Beeper Server you self-host, or to either one running somewhere else. Send and receive across the chat @@ -13,7 +22,7 @@ Facebook Messenger · X (Twitter) DMs · LinkedIn · Slack · Google Messages (RCS/SMS) · Google Chat · Matrix · IRC · Bluesky. Run `beeper bridges list` for the live list on your target. -Command manual: `beeper man` · CLI docs: `beeper docs` +📖 **[Read the docs](https://example.com)** · command manual: `beeper man` · open docs: `beeper docs` ## Features @@ -193,19 +202,22 @@ WhatsApp, Telegram, Discord, iMessage, and the rest show up under `accounts list ## Documentation +Full documentation lives at **[example.com](https://example.com)** +(built from [`docs/`](docs/) with Astro Starlight — a fully static site). + | Topic | Page | Commands | | --- | --- | --- | -| **Setup + install** | [setup](docs/setup.md) · [auth](docs/auth.md) | `setup` · `install desktop` · `install server` · `verify` · `status` · `doctor` · `auth status` | -| **Targets** | [targets](docs/targets.md) | `targets list` · `targets add desktop` · `targets add server` · `targets add remote` · `targets use` · `targets status` · `targets logs` | -| **Bridges + accounts** | [accounts](docs/accounts.md) | `bridges list` · `bridges show` · `accounts list` · `accounts add` · `accounts show` · `accounts use` · `accounts remove` | -| **Chats** | [chats](docs/chats.md) | `chats list` · `chats search` · `chats show` · `chats start` · `chats archive` · `chats pin` · `chats mute` · `chats priority` · `chats remind` · `chats rename` · `chats draft` · `chats focus` | -| **Messages** | [messages](docs/messages.md) · [send](docs/send.md) · [presence](docs/presence.md) | `messages list` · `messages search` · `messages export` · `send text` · `send file` · `send sticker` · `send voice` · `send react` · `presence` | -| **Contacts + media** | [contacts](docs/contacts.md) · [media](docs/media.md) · [export](docs/export.md) | `contacts list` · `contacts search` · `media download` · `export` | -| **Automation** | [watch](docs/watch.md) · [rpc](docs/rpc.md) · [api](docs/api.md) | `watch` · `watch --webhook` · `rpc` · `man` · `api get` · `api post` · `api request` | -| **Maintenance** | [config](docs/config.md) · [update](docs/update.md) | `update` · `config` · `completion` · `docs` · `version` | +| **Setup + install** | [connect](https://example.com/connect/) · [install](https://example.com/install/) · [auth](https://example.com/auth/) | `setup` · `install desktop` · `install server` · `verify` · `status` · `doctor` · `auth status` | +| **Targets** | [targets](https://example.com/targets/) | `targets list` · `targets add desktop` · `targets add server` · `targets add remote` · `targets use` · `targets status` · `targets logs` | +| **Bridges + accounts** | [accounts](https://example.com/accounts/) | `bridges list` · `bridges show` · `accounts list` · `accounts add` · `accounts show` · `accounts use` · `accounts remove` | +| **Chats** | [chats](https://example.com/chats/) | `chats list` · `chats search` · `chats show` · `chats start` · `chats archive` · `chats pin` · `chats mute` · `chats priority` · `chats remind` · `chats rename` · `chats draft` · `chats focus` | +| **Messages** | [messages](https://example.com/messages/) · [send](https://example.com/send/) · [presence](https://example.com/presence/) | `messages list` · `messages search` · `messages export` · `send text` · `send file` · `send sticker` · `send voice` · `send react` · `presence` | +| **Contacts + media** | [contacts](https://example.com/contacts/) · [media](https://example.com/media/) · [export](https://example.com/export/) | `contacts list` · `contacts search` · `media download` · `export` | +| **Automation** | [scripting](https://example.com/scripting/) · [watch](https://example.com/watch/) · [rpc](https://example.com/rpc/) · [api](https://example.com/api/) | `watch` · `watch --webhook` · `rpc` · `man` · `api get` · `api post` · `api request` | +| **Maintenance** | [config](https://example.com/config/) · [update](https://example.com/update/) | `update` · `config` · `completion` · `docs` · `version` | Use `beeper docs` to open the CLI docs and `beeper man` to print the local -command manual. +command manual. To work on the docs site locally: `cd docs && bun install && bun run dev`. ## Configuration @@ -389,11 +401,17 @@ First-party optional plugins: | `contacts list` | List contacts | | `contacts search` | Search contacts | | `contacts show` | Show contact details | +| `resolve chat` | Resolve a chat selector to concrete chat candidates | +| `resolve account` | Resolve an account selector | +| `resolve contact` | Resolve a contact selector | +| `resolve target` | Resolve a target selector | +| `resolve bridge` | Resolve a bridge selector | | `media download` | Download message media | | `export` | Export accounts, chats, messages, Markdown transcripts, and attachments | | `watch` | Stream Desktop API WebSocket events | | `rpc` | Run newline-delimited JSON command RPC over stdin/stdout | | `man` | Print the command manual | +| `schema` | Print machine-readable command/flag schema | | `doctor` | Probe the target live and report diagnostics | | `status` | Show selected target and setup readiness | | `docs` | Open Beeper CLI docs | @@ -444,7 +462,7 @@ beeper setup --remote https://desktop.example.com beeper setup --desktop --install ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper install desktop` Install Beeper Desktop locally @@ -466,7 +484,7 @@ beeper install desktop beeper install desktop --channel nightly ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper install server` Install Beeper Server locally @@ -489,7 +507,7 @@ beeper install server beeper install server --server-env staging ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets list` List configured Beeper targets @@ -505,7 +523,7 @@ beeper targets list beeper targets list --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper bridges list` List bridges that can connect chat accounts @@ -530,7 +548,7 @@ beeper bridges list beeper bridges list --provider local --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper bridges show` Show bridge details, login flows, and connected accounts @@ -552,7 +570,7 @@ beeper bridges show local-whatsapp beeper bridges show telegram ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets add desktop` Add a managed Beeper Desktop target @@ -581,7 +599,7 @@ Examples: beeper targets add desktop work --default ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets add server` Add a managed Beeper Server target @@ -610,7 +628,7 @@ Examples: beeper targets add server prod --server-env production --default ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets add remote` Add a remote Beeper Desktop or Server target @@ -638,7 +656,7 @@ Examples: beeper targets add remote work https://desktop.example.com --default ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets use` Set the default target @@ -659,7 +677,7 @@ Examples: beeper targets use work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets show` Show target details @@ -681,7 +699,7 @@ beeper targets show beeper targets show work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets status` Check endpoint and process reachability for a target @@ -703,7 +721,7 @@ beeper targets status beeper targets status work --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets start` Start a local Server target or open Beeper Desktop @@ -724,7 +742,7 @@ Examples: beeper targets start work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets stop` Stop a local Beeper Server target @@ -745,7 +763,7 @@ Examples: beeper targets stop work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets restart` Restart a local Beeper Server target @@ -766,7 +784,7 @@ Examples: beeper targets restart work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets logs` Print logs for a local Beeper Desktop or Server install @@ -795,7 +813,7 @@ Examples: beeper targets logs work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets enable` Enable a local Beeper Server target at login @@ -816,7 +834,7 @@ Examples: beeper targets enable work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets disable` Disable a local Beeper Server target at login @@ -837,7 +855,7 @@ Examples: beeper targets disable work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets remove` Remove a target @@ -858,7 +876,7 @@ Examples: beeper targets remove work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets tunnel` Expose a local Desktop API over a public Cloudflare tunnel @@ -889,7 +907,7 @@ beeper auth status beeper auth status --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper auth logout` Clear stored authentication @@ -904,7 +922,7 @@ Examples: beeper auth logout ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper auth email start` Start email sign-in for a target @@ -925,7 +943,7 @@ Examples: beeper auth email start --email you@example.com --target work --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper auth email response` Finish email sign-in with a verification code @@ -948,7 +966,7 @@ Examples: beeper auth email response --setup-request-id --code --target work --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify` Finish setup verification or verify another device @@ -970,7 +988,7 @@ beeper verify beeper verify --user @alice:beeper.com ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify status` Show encryption and device-verification readiness @@ -985,7 +1003,7 @@ Examples: beeper verify status --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify approve` Approve a pending device verification request @@ -1006,7 +1024,7 @@ Examples: beeper verify approve --id active ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify recovery-key` Unlock encrypted messages with a recovery key @@ -1027,7 +1045,7 @@ Examples: beeper verify recovery-key --key ABCD-EFGH-IJKL ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify reset-recovery-key` Create a new encrypted-messages recovery key @@ -1042,7 +1060,7 @@ Examples: beeper verify reset-recovery-key ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify cancel` Cancel an in-progress device verification @@ -1063,7 +1081,7 @@ Examples: beeper verify cancel ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify list` List active verification work @@ -1078,7 +1096,7 @@ Examples: beeper verify list ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify start` Start a device verification request @@ -1099,7 +1117,7 @@ Examples: beeper verify start --user @alice:beeper.com ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify show` Show the current active verification request @@ -1114,7 +1132,7 @@ Examples: beeper verify show --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify sas` Start emoji verification @@ -1135,7 +1153,7 @@ Examples: beeper verify sas ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify sas-confirm` Confirm matching emoji verification @@ -1156,7 +1174,7 @@ Examples: beeper verify sas-confirm ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify qr-scan` Submit a scanned QR-code verification payload @@ -1178,7 +1196,7 @@ Examples: beeper verify qr-scan --payload "..." ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify qr-confirm` Confirm that the other device scanned your QR code @@ -1199,7 +1217,7 @@ Examples: beeper verify qr-confirm ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper accounts list` List connected accounts @@ -1222,7 +1240,7 @@ beeper accounts list beeper accounts list --account whatsapp --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper accounts add` Connect a chat account by bridge @@ -1262,7 +1280,7 @@ beeper accounts add discord --non-interactive --cookie sessiontoken=... beeper accounts add discord --webview --webview-backend chrome ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper accounts show` Show account details @@ -1283,7 +1301,7 @@ Examples: beeper accounts show whatsapp-main ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper accounts remove` Remove an account @@ -1304,7 +1322,7 @@ Examples: beeper accounts remove whatsapp-main ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper accounts use` Select a default account for account-scoped commands @@ -1327,7 +1345,7 @@ Examples: beeper accounts use whatsapp-main ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats list` List chats @@ -1354,10 +1372,11 @@ Examples: ```sh beeper chats list beeper chats list --pinned --limit 50 -beeper chats list --unread --no-muted --json +beeper chats list --unread --no-muted --format json +beeper ls --format ids ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats search` Search chats @@ -1386,7 +1405,7 @@ Examples: beeper chats search Family ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats show` Show chat details @@ -1410,7 +1429,7 @@ beeper chats show --chat 10313 beeper chats show --chat '!plUOsWkvMmJmJPVAjS:beeper.com' ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats start` Start a chat @@ -1439,7 +1458,7 @@ beeper chats start +15551234567 beeper chats start @alice:beeper.com --title "Alice" ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats archive` Archive a chat @@ -1461,7 +1480,7 @@ Examples: beeper chats archive --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats unarchive` Unarchive a chat @@ -1483,7 +1502,7 @@ Examples: beeper chats unarchive --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats pin` Pin a chat @@ -1505,7 +1524,7 @@ Examples: beeper chats pin --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats unpin` Unpin a chat @@ -1527,7 +1546,7 @@ Examples: beeper chats unpin --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats mute` Mute a chat @@ -1549,7 +1568,7 @@ Examples: beeper chats mute --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats unmute` Unmute a chat @@ -1571,7 +1590,7 @@ Examples: beeper chats unmute --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats mark-read` Mark a chat as read @@ -1594,7 +1613,7 @@ Examples: beeper chats mark-read --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats mark-unread` Mark a chat as unread @@ -1617,7 +1636,7 @@ Examples: beeper chats mark-unread --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats priority` Move a chat to the Inbox or Low Priority @@ -1641,7 +1660,7 @@ beeper chats priority --chat 10313 --level inbox beeper chats priority --chat '!plUOsWkvMmJmJPVAjS:beeper.com' --level low ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats notify-anyway` Send an iMessage Notify Anyway alert @@ -1663,7 +1682,7 @@ Examples: beeper chats notify-anyway --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats rename` Rename a chat @@ -1686,7 +1705,7 @@ Examples: beeper chats rename --chat 10313 --title "Family" ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats description` Set a chat description @@ -1711,7 +1730,7 @@ beeper chats description --chat 10313 --description "Engineering chat" beeper chats description --chat 10313 --clear ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats avatar` Set a chat avatar @@ -1735,7 +1754,7 @@ Examples: beeper chats avatar --chat 10313 --file ./team.png ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats draft` Set or clear a chat draft @@ -1763,7 +1782,7 @@ beeper chats draft --chat 10313 --text "on my way" beeper chats draft --chat 10313 --clear ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats disappear` Set disappearing-message expiry @@ -1786,7 +1805,7 @@ Examples: beeper chats disappear --chat 10313 --seconds 86400 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats remind` Set a chat reminder @@ -1811,7 +1830,7 @@ beeper chats remind --chat 10313 --when 2026-06-01T09:00:00Z beeper chats remind --chat 10313 --when 2026-06-01T09:00:00Z --dismiss-on-message ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats unremind` Clear a chat reminder @@ -1833,7 +1852,7 @@ Examples: beeper chats unremind --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats focus` Focus Beeper Desktop on a chat @@ -1858,7 +1877,7 @@ Examples: beeper chats focus --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper messages list` List chat messages @@ -1888,7 +1907,7 @@ beeper messages list --chat 10313 --before-cursor "" --limit 100 beeper messages list --chat 10313 --sender me --asc ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper messages search` Search messages across chats @@ -1923,11 +1942,12 @@ Examples: ```sh beeper messages search invoice +beeper search invoice --format jsonl --select id,chatID,text beeper messages search --chat 10313 --sender me --media image beeper messages search "flight" --after 2026-01-01 --before 2026-02-01 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper messages show` Show one message @@ -1950,7 +1970,7 @@ Examples: beeper messages show --chat 10313 --id ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper messages context` Show message context @@ -1975,7 +1995,7 @@ Examples: beeper messages context --chat 10313 --id --before 5 --after 5 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper messages edit` Edit a message @@ -1999,7 +2019,7 @@ Examples: beeper messages edit --chat 10313 --id --message "fixed" ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper messages delete` Delete a message @@ -2023,7 +2043,7 @@ Examples: beeper messages delete --chat 10313 --id --for-everyone ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper messages export` Export one chat to JSON @@ -2056,7 +2076,7 @@ beeper messages export --chat 8951 --after 2026-01-01T00:00:00Z --output - beeper messages export --chat '!plUOsWkvMmJmJPVAjS:beeper.com' --before-cursor "" --limit 500 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper send text` Send a text message @@ -2083,12 +2103,13 @@ Flags: Examples: ```sh +beeper send --to 10313 --message "on my way" --dry-run --format json beeper send text --to 10313 --message "on my way" beeper send text --to 8951 --message "hi" beeper send text --to "Family" --message "hi" --pick 1 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper send file` Send a file @@ -2119,7 +2140,7 @@ Examples: beeper send file --to 8951 --file ./photo.jpg --caption "from today" ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper send react` Send a reaction to a message @@ -2144,7 +2165,7 @@ Examples: beeper send react --to 10313 --id --reaction "+1" ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper send sticker` Send a sticker @@ -2174,7 +2195,7 @@ Examples: beeper send sticker --to 10313 --file ./hi.webp ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper send unreact` Remove a reaction from a message @@ -2199,7 +2220,7 @@ Examples: beeper send unreact --to 10313 --id --reaction "+1" ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper send voice` Send a voice note @@ -2231,7 +2252,7 @@ beeper send voice --to 10313 --file ./note.ogg beeper send voice --to 10313 --file ./note.ogg --duration 12 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper presence` Send a typing (or paused) indicator to a chat @@ -2259,7 +2280,7 @@ beeper presence --chat 10313 --state paused beeper presence --chat 10313 --duration 5 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper contacts list` List contacts @@ -2285,7 +2306,7 @@ Examples: beeper contacts list --account whatsapp --query alice ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper contacts search` Search contacts @@ -2314,7 +2335,7 @@ Examples: beeper contacts search alice ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper contacts show` Show contact details @@ -2341,7 +2362,147 @@ Examples: beeper contacts show "Alice" --account whatsapp ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. + +### `beeper resolve chat` +Resolve a chat selector to concrete chat candidates + +```sh +beeper resolve chat +``` + +Arguments: + +| Name | Required | Description | +| --- | --- | --- | +| `selector` | yes | Chat ID, local ID, exact title, or search text | + +Flags: + +| Flag | Type | Description | +| --- | --- | --- | +| `--account=...` | option | Limit to account selector. Repeat for multiple. | +| `--limit=` | option | Maximum candidates to return Default: 10 | +| `--pick=` | option | Select the Nth candidate (1-indexed) | + +Examples: + +```sh +beeper resolve chat Family --format json +beeper resolve chat Family --pick 1 --results-only +``` + +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. + +### `beeper resolve account` +Resolve an account selector + +```sh +beeper resolve account +``` + +Arguments: + +| Name | Required | Description | +| --- | --- | --- | +| `selector` | yes | Account ID, network, bridge, or account user selector | + +Flags: + +| Flag | Type | Description | +| --- | --- | --- | +| `--pick=` | option | Select the Nth candidate (1-indexed) | + +Examples: + +```sh +beeper resolve account whatsapp --format json +``` + +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. + +### `beeper resolve contact` +Resolve a contact selector + +```sh +beeper resolve contact +``` + +Arguments: + +| Name | Required | Description | +| --- | --- | --- | +| `selector` | yes | Contact name, username, phone, email, or ID | + +Flags: + +| Flag | Type | Description | +| --- | --- | --- | +| `--account=...` | option | Limit to account selector. Repeat for multiple. | +| `--limit=` | option | Maximum candidates to return per account Default: 10 | +| `--pick=` | option | Select the Nth candidate (1-indexed) | + +Examples: + +```sh +beeper resolve contact Alice --account whatsapp --format json +``` + +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. + +### `beeper resolve target` +Resolve a target selector + +```sh +beeper resolve target +``` + +Arguments: + +| Name | Required | Description | +| --- | --- | --- | +| `selector` | yes | Target name, ID, type, or base URL | + +Flags: + +| Flag | Type | Description | +| --- | --- | --- | +| `--pick=` | option | Select the Nth candidate (1-indexed) | + +Examples: + +```sh +beeper resolve target desktop --format json +``` + +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. + +### `beeper resolve bridge` +Resolve a bridge selector + +```sh +beeper resolve bridge +``` + +Arguments: + +| Name | Required | Description | +| --- | --- | --- | +| `selector` | yes | Bridge ID, type, provider, or display name | + +Flags: + +| Flag | Type | Description | +| --- | --- | --- | +| `--pick=` | option | Select the Nth candidate (1-indexed) | + +Examples: + +```sh +beeper resolve bridge whatsapp --format json +``` + +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper media download` Download message media @@ -2369,7 +2530,7 @@ beeper media download mxc://beeper.com/abc --out ./downloads beeper media download mxc://beeper.com/abc -o - > photo.jpg ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper export` Export accounts, chats, messages, Markdown transcripts, and attachments @@ -2386,7 +2547,6 @@ Flags: | --- | --- | --- | | `--account=...` | option | Limit to an account selector. Repeat to include more accounts. | | `--chat=...` | option | Limit to a chat selector. Repeat to include more chats. | -| `--force` | boolean | Re-export chats even if checkpoint state says they are complete. | | `--limit-chats=` | option | Maximum chats to export. Intended for testing large exports. | | `--limit-messages=` | option | Maximum messages per chat. Intended for testing large exports. | | `--max-participants=` | option | Maximum participants to include in each chat.json. Default: 500 | @@ -2401,7 +2561,7 @@ beeper export --out ./beeper-export beeper export --chat 10313 --out ./chat ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper watch` Stream Desktop API WebSocket events @@ -2430,7 +2590,7 @@ beeper watch --include-type message.upserted --include-type message.deleted beeper watch --webhook https://example.com/hook --webhook-secret "$BEEPER_WEBHOOK_SECRET" ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper rpc` Run newline-delimited JSON command RPC over stdin/stdout @@ -2447,7 +2607,7 @@ Examples: printf '{"id":1,"command":"chats list --json"}\n' | beeper rpc ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper man` Print the command manual @@ -2460,10 +2620,36 @@ Examples: ```sh beeper man -beeper man --json +beeper man --format json +beeper man --format ids +``` + +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. + +### `beeper schema` +Print machine-readable command/flag schema + +```sh +beeper schema [command] +``` + +Agent-first schema for commands, flags, args, examples, mutation metadata, selectors, output shapes, and related commands. + +Arguments: + +| Name | Required | Description | +| --- | --- | --- | +| `command` | no | Optional command path, such as "messages search" | + +Examples: + +```sh +beeper schema +beeper schema send --results-only +beeper schema --select commands.path,commands.flags.name --results-only ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper doctor` Probe the target live and report diagnostics @@ -2481,7 +2667,7 @@ beeper doctor beeper doctor --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper status` Show selected target and setup readiness @@ -2499,7 +2685,7 @@ beeper status beeper status --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper docs` Open Beeper CLI docs @@ -2514,7 +2700,7 @@ Examples: beeper docs ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper version` Print CLI version @@ -2529,7 +2715,7 @@ Examples: beeper version ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper completion` Print shell completion setup @@ -2589,7 +2775,7 @@ beeper plugins available beeper plugins available --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper update` Check and install Beeper updates @@ -2615,7 +2801,7 @@ beeper update --cli beeper update --server ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper config get` Print CLI configuration @@ -2637,7 +2823,7 @@ beeper config get beeper config get defaultTarget ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper config set` Set a CLI configuration value @@ -2659,7 +2845,7 @@ Examples: beeper config set defaultTarget work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper config path` Print the CLI config path @@ -2674,7 +2860,7 @@ Examples: beeper config path ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper config reset` Reset CLI configuration @@ -2689,7 +2875,7 @@ Examples: beeper config reset ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper api get` Call a raw Desktop API GET path @@ -2717,7 +2903,7 @@ beeper api get /v1/info beeper api get /v1/chats --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper api post` Call a raw Desktop API POST path with a JSON body @@ -2745,7 +2931,7 @@ Examples: beeper api post /v1/chats/abc/read --body '{"messageID":"x"}' ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper api request` Call a raw Desktop API path with any supported HTTP method @@ -2774,7 +2960,7 @@ Examples: beeper api request DELETE /v1/chats/abc/messages/def/reactions --body '{"reactionKey":"👍"}' ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ## Publishing diff --git a/packages/cli/docs/api.md b/packages/cli/docs/api.md deleted file mode 100644 index b9cf1de1..00000000 --- a/packages/cli/docs/api.md +++ /dev/null @@ -1,29 +0,0 @@ -# api - -Read when: calling raw Desktop API endpoints that the CLI doesn't yet wrap -with a workflow command. - -## Commands - -```sh -beeper api get [--no-auth] -beeper api post [--body JSON] [--no-auth] -beeper api request [--body JSON] [--no-auth] -``` - -## Notes - -- `` is a Desktop API path, e.g. `/v1/info` or `/v1/chats/{chatID}/read`. -- `--no-auth` calls a public path without the bearer token. -- `--body` is sent as `application/json`; default is `{}` for `post`. -- `api request` lets you hit `GET | POST | PUT | PATCH | DELETE`; the others are convenience shortcuts. -- `--read-only` blocks `api post` / `api put` / `api patch` / `api delete` / `api request `. - -## Examples - -```sh -beeper api get /v1/info -beeper api get /v1/chats --json -beeper api post /v1/chats/abc/read --body '{"messageID":"x"}' -beeper api request PATCH /v1/chats/abc --body '{"isPinned":true}' -``` diff --git a/packages/cli/docs/config.md b/packages/cli/docs/config.md deleted file mode 100644 index 333135a7..00000000 --- a/packages/cli/docs/config.md +++ /dev/null @@ -1,32 +0,0 @@ -# config - -Read when: inspecting, changing, or resetting the CLI's local configuration -file (`~/.beeper/config.json`, or wherever `BEEPER_CLI_CONFIG_DIR` points). - -## Commands - -```sh -beeper config path -beeper config get [defaultTarget | defaultAccount | baseURL | auth] -beeper config set -beeper config reset -``` - -## Notes - -- `config path` prints the JSON config path (suitable for `cat` or `cd $(dirname …)`). -- `config get` without a key prints the full config; passing a key prints just that field. -- `auth.accessToken` is always redacted in `config get` output. -- `config set ""` clears the field. Only `defaultTarget` and `defaultAccount` are settable here; other fields are written by commands like `targets use` and `auth verify`. -- `config reset` deletes the config file. - -## Examples - -```sh -beeper config path -beeper config get --json -beeper config get defaultTarget -beeper config set defaultTarget work -beeper config set defaultAccount "" -beeper config reset -``` diff --git a/packages/cli/docs/export.md b/packages/cli/docs/export.md deleted file mode 100644 index f3830af0..00000000 --- a/packages/cli/docs/export.md +++ /dev/null @@ -1,39 +0,0 @@ -# export - -Read when: making a heavy, multi-chat, attachment-including export of Beeper -data to disk. For a lightweight per-chat JSON dump, see [messages -export](messages.md). - -## Command - -```sh -beeper export - [-o, --out DIR] - [--account SEL]... - [--chat SEL]... - [--limit-chats N] - [--limit-messages N] - [--max-participants N] - [--no-attachments] - [--force] - [--quiet] - [--pick N] -``` - -## Notes - -- Default `--out` directory is `beeper-export`. -- Layout: `accounts.json`, `chats.json`, `manifest.json`, plus one directory per chat with `chat.json`, `messages.json`, `messages.markdown`, `messages.html`, attachments, and per-chat checkpoint state. -- Exports are resumable. Re-running picks up where the last run left off unless `--force` is set. -- `--max-participants` (default 500) bounds the participant list stored in each `chat.json`. -- `--no-attachments` skips downloading media; metadata is still recorded. -- `--limit-chats` / `--limit-messages` are intended for sanity-checking large exports. - -## Examples - -```sh -beeper export --out ./beeper-export -beeper export --chat "Family" --out ./family -beeper export --account whatsapp --no-attachments --quiet -beeper export --force --out ./beeper-export -``` diff --git a/packages/cli/docs/setup.md b/packages/cli/docs/setup.md deleted file mode 100644 index 9bf22140..00000000 --- a/packages/cli/docs/setup.md +++ /dev/null @@ -1,38 +0,0 @@ -# setup - -Read when: making a Beeper target ready for the first time, switching to a -different target, or installing a managed runtime. - -`beeper setup` orchestrates the path from "I have nothing" to "the selected -target is ready". By default it detects a running local Beeper Desktop, offers -to reuse that session, and falls back to a guided choice between Desktop / -Server / remote targets. - -## Commands - -```sh -beeper setup [--local | --oauth | --remote URL | --desktop | --server] [--install] [--channel stable|nightly] -beeper install desktop [--channel stable|nightly] -beeper install server [--channel stable|nightly] [--server-env production|staging] -``` - -## Notes - -- `setup --local` reuses the local Beeper Desktop session (fastest trusted-device path). -- `setup --oauth` runs browser-based OAuth/PKCE against the resolved target. -- `setup --remote URL` configures a remote Beeper Desktop or Server target. -- `setup --desktop --install` or `setup --server --install` installs the runtime if missing, then sets up. -- `install desktop|server` installs without changing the selected target. -- The selected target is persisted in `~/.beeper/config.json` (override with `BEEPER_CLI_CONFIG_DIR`). -- For non-interactive use, pass a token in the environment: `BEEPER_ACCESS_TOKEN=… beeper …`. - -## Examples - -```sh -beeper setup -beeper setup --local -beeper setup --oauth -beeper setup --remote https://desktop.example.com -beeper setup --desktop --install --channel nightly -beeper install server --server-env staging -``` diff --git a/packages/cli/docs/update.md b/packages/cli/docs/update.md deleted file mode 100644 index 8403eff0..00000000 --- a/packages/cli/docs/update.md +++ /dev/null @@ -1,27 +0,0 @@ -# update - -Read when: checking for new versions of the CLI, the CLI-managed Desktop -install, or the CLI-managed Server install — and choosing whether to install. - -## Command - -```sh -beeper update [--check] [--cli] [--desktop] [--server] -``` - -## Notes - -- With no kind flag, checks all three (CLI, Desktop, Server) that apply. -- `--check` prints what's available without installing. -- The CLI itself is never auto-upgraded; `--cli` prints the right command for your install method (Homebrew, npm-global, or in-repo git build). -- `--desktop` reports on the CLI-owned Desktop install; updating Desktop itself happens inside the Desktop app. -- `--server` updates the CLI-managed Server install in place, then restarts any running managed Server targets. - -## Examples - -```sh -beeper update --check -beeper update --cli -beeper update --desktop --json -beeper update --server -``` diff --git a/packages/cli/docs/watch.md b/packages/cli/docs/watch.md deleted file mode 100644 index a8df6c74..00000000 --- a/packages/cli/docs/watch.md +++ /dev/null @@ -1,35 +0,0 @@ -# watch - -Read when: subscribing to live Desktop API events (new/updated/deleted chats -and messages), optionally forwarding them to a webhook. - -## Commands - -```sh -beeper watch - [-c, --chat CHAT_ID]... - [--include-type EVENT_TYPE]... - [--exclude-type EVENT_TYPE]... - [--webhook URL [--webhook-secret SECRET] [--webhook-queue N]] - [--json] -``` - -## Notes - -- Subscribes to the Desktop API WebSocket at the path returned by `/v1/info` (defaults to `/v1/ws`). -- Without `--chat`, subscribes to all chats. -- Event types come from the Desktop API: `chat.upserted`, `chat.deleted`, `message.upserted`, `message.deleted`. -- `--include-type` and `--exclude-type` are mutually exclusive. -- `--webhook URL` forwards every event as a POST body (best-effort, fire-and-forget). -- `--webhook-secret SECRET` signs the body with HMAC-SHA256 and sets `X-Beeper-Signature: sha256=`. -- `--webhook-queue` (default 64) caps pending deliveries; excess events are dropped with a stderr warning. -- `--quiet` suppresses the human-mode status line; `--json` prints raw events line-delimited. - -## Examples - -```sh -beeper watch -beeper watch --chat '!abc:beeper.com' --json -beeper watch --include-type message.upserted --include-type message.deleted -beeper watch --webhook https://example.com/hook --webhook-secret "$BEEPER_WEBHOOK_SECRET" -``` diff --git a/packages/cli/package.json b/packages/cli/package.json index f79ec98f..b35a43b9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -100,6 +100,9 @@ "messages": { "description": "List, search, show, edit, delete, and export messages" }, + "resolve": { + "description": "Resolve selectors into concrete candidates" + }, "send": { "description": "Send text, files, and reactions" }, diff --git a/packages/cli/scripts/generate-command-map.ts b/packages/cli/scripts/generate-command-map.ts index 3e68239d..e809e4e9 100644 --- a/packages/cli/scripts/generate-command-map.ts +++ b/packages/cli/scripts/generate-command-map.ts @@ -10,8 +10,10 @@ const outPath = join(root, 'src', 'commands.generated.ts') const listAliases: Record = { 'accounts:list': ['accounts'], 'bridges:list': ['bridges'], - 'chats:list': ['chats', 'accounts:chats'], + 'chats:list': ['chats', 'accounts:chats', 'ls'], 'contacts:list': ['contacts'], + 'messages:search': ['search'], + 'send:text': ['send'], 'targets:list': ['targets'], } diff --git a/packages/cli/scripts/generate-readme.ts b/packages/cli/scripts/generate-readme.ts index f9529adc..60b7a47a 100644 --- a/packages/cli/scripts/generate-readme.ts +++ b/packages/cli/scripts/generate-readme.ts @@ -24,7 +24,7 @@ const commands = commandManifest.map(item => { }; }); -const globalFlags = new Set(['base-url', 'debug', 'events', 'full', 'json', 'quiet', 'read-only', 'target', 'timeout', 'yes']); +const globalFlags = new Set(['base-url', 'debug', 'dry-run', 'events', 'force', 'format', 'full', 'json', 'no-input', 'quiet', 'read-only', 'results-only', 'select', 'target', 'timeout', 'yes']); const commandList = commands.map(command => { const id = displayID(command.id); return `| \`${id}\` | ${escapeTable(text(command.summary || command.description || ''))} |`; @@ -33,9 +33,24 @@ const commandList = commands.map(command => { const examplesByID = new Map(commandManifest.map(item => [item.command, item.examples ?? []])); const commandSections = commands.map(command => commandSection(command)).join('\n\n'); -const intro = `# beeper — One CLI for all your chats +// Origin where the Astro docs site (in `docs/`) is published. Keep this in sync +// with `site` in `docs/astro.config.mjs`. Until the docs have a permanent home +// this is a placeholder; flip both in one commit when you pick a host. +const docsUrl = 'https://example.com'; +const repoUrl = 'https://github.com/beeper/desktop-api-cli'; -> Built for you and your agent. Batteries included. +const intro = `
+ +# beeper + +**One CLI for all your chats.** Built for you and your agent — batteries included. + +[![npm](https://img.shields.io/npm/v/beeper-cli.svg?label=npm&color=6E56F8)](https://www.npmjs.com/package/beeper-cli) +[![license](https://img.shields.io/badge/license-MIT-6E56F8.svg)](${repoUrl}/blob/main/packages/cli/LICENSE) +[![docs](https://img.shields.io/badge/docs-online-6E56F8.svg)](${docsUrl}) +[![built with Bun](https://img.shields.io/badge/built%20with-Bun-6E56F8.svg)](https://bun.sh) + +
Talks to Beeper Desktop on this machine, to a Beeper Server you self-host, or to either one running somewhere else. Send and receive across the chat @@ -48,7 +63,7 @@ Facebook Messenger · X (Twitter) DMs · LinkedIn · Slack · Google Messages (RCS/SMS) · Google Chat · Matrix · IRC · Bluesky. Run \`beeper bridges list\` for the live list on your target. -Command manual: \`beeper man\` · CLI docs: \`beeper docs\` +📖 **[Read the docs](${docsUrl})** · command manual: \`beeper man\` · open docs: \`beeper docs\` ## Features @@ -228,19 +243,22 @@ WhatsApp, Telegram, Discord, iMessage, and the rest show up under \`accounts lis ## Documentation +Full documentation lives at **[${docsUrl.replace(/^https?:\/\//, '')}](${docsUrl})** +(built from [\`docs/\`](docs/) with Astro Starlight — a fully static site). + | Topic | Page | Commands | | --- | --- | --- | -| **Setup + install** | [setup](docs/setup.md) · [auth](docs/auth.md) | \`setup\` · \`install desktop\` · \`install server\` · \`verify\` · \`status\` · \`doctor\` · \`auth status\` | -| **Targets** | [targets](docs/targets.md) | \`targets list\` · \`targets add desktop\` · \`targets add server\` · \`targets add remote\` · \`targets use\` · \`targets status\` · \`targets logs\` | -| **Bridges + accounts** | [accounts](docs/accounts.md) | \`bridges list\` · \`bridges show\` · \`accounts list\` · \`accounts add\` · \`accounts show\` · \`accounts use\` · \`accounts remove\` | -| **Chats** | [chats](docs/chats.md) | \`chats list\` · \`chats search\` · \`chats show\` · \`chats start\` · \`chats archive\` · \`chats pin\` · \`chats mute\` · \`chats priority\` · \`chats remind\` · \`chats rename\` · \`chats draft\` · \`chats focus\` | -| **Messages** | [messages](docs/messages.md) · [send](docs/send.md) · [presence](docs/presence.md) | \`messages list\` · \`messages search\` · \`messages export\` · \`send text\` · \`send file\` · \`send sticker\` · \`send voice\` · \`send react\` · \`presence\` | -| **Contacts + media** | [contacts](docs/contacts.md) · [media](docs/media.md) · [export](docs/export.md) | \`contacts list\` · \`contacts search\` · \`media download\` · \`export\` | -| **Automation** | [watch](docs/watch.md) · [rpc](docs/rpc.md) · [api](docs/api.md) | \`watch\` · \`watch --webhook\` · \`rpc\` · \`man\` · \`api get\` · \`api post\` · \`api request\` | -| **Maintenance** | [config](docs/config.md) · [update](docs/update.md) | \`update\` · \`config\` · \`completion\` · \`docs\` · \`version\` | +| **Setup + install** | [connect](${docsUrl}/connect/) · [install](${docsUrl}/install/) · [auth](${docsUrl}/auth/) | \`setup\` · \`install desktop\` · \`install server\` · \`verify\` · \`status\` · \`doctor\` · \`auth status\` | +| **Targets** | [targets](${docsUrl}/targets/) | \`targets list\` · \`targets add desktop\` · \`targets add server\` · \`targets add remote\` · \`targets use\` · \`targets status\` · \`targets logs\` | +| **Bridges + accounts** | [accounts](${docsUrl}/accounts/) | \`bridges list\` · \`bridges show\` · \`accounts list\` · \`accounts add\` · \`accounts show\` · \`accounts use\` · \`accounts remove\` | +| **Chats** | [chats](${docsUrl}/chats/) | \`chats list\` · \`chats search\` · \`chats show\` · \`chats start\` · \`chats archive\` · \`chats pin\` · \`chats mute\` · \`chats priority\` · \`chats remind\` · \`chats rename\` · \`chats draft\` · \`chats focus\` | +| **Messages** | [messages](${docsUrl}/messages/) · [send](${docsUrl}/send/) · [presence](${docsUrl}/presence/) | \`messages list\` · \`messages search\` · \`messages export\` · \`send text\` · \`send file\` · \`send sticker\` · \`send voice\` · \`send react\` · \`presence\` | +| **Contacts + media** | [contacts](${docsUrl}/contacts/) · [media](${docsUrl}/media/) · [export](${docsUrl}/export/) | \`contacts list\` · \`contacts search\` · \`media download\` · \`export\` | +| **Automation** | [scripting](${docsUrl}/scripting/) · [watch](${docsUrl}/watch/) · [rpc](${docsUrl}/rpc/) · [api](${docsUrl}/api/) | \`watch\` · \`watch --webhook\` · \`rpc\` · \`man\` · \`api get\` · \`api post\` · \`api request\` | +| **Maintenance** | [config](${docsUrl}/config/) · [update](${docsUrl}/update/) | \`update\` · \`config\` · \`completion\` · \`docs\` · \`version\` | Use \`beeper docs\` to open the CLI docs and \`beeper man\` to print the local -command manual. +command manual. To work on the docs site locally: \`cd docs && bun install && bun run dev\`. ## Configuration diff --git a/packages/cli/src/commands.generated.ts b/packages/cli/src/commands.generated.ts index ed34e12a..f8170748 100644 --- a/packages/cli/src/commands.generated.ts +++ b/packages/cli/src/commands.generated.ts @@ -60,45 +60,51 @@ import Command58 from './commands/messages/show.js' import Command59 from './commands/plugins.js' import Command60 from './commands/plugins/available.js' import Command61 from './commands/presence.js' -import Command62 from './commands/rpc.js' -import Command63 from './commands/send/file.js' -import Command64 from './commands/send/react.js' -import Command65 from './commands/send/sticker.js' -import Command66 from './commands/send/text.js' -import Command67 from './commands/send/unreact.js' -import Command68 from './commands/send/voice.js' -import Command69 from './commands/setup.js' -import Command70 from './commands/status.js' -import Command71 from './commands/targets/add/desktop.js' -import Command72 from './commands/targets/add/remote.js' -import Command73 from './commands/targets/add/server.js' -import Command74 from './commands/targets/disable.js' -import Command75 from './commands/targets/enable.js' -import Command76 from './commands/targets/list.js' -import Command77 from './commands/targets/logs.js' -import Command78 from './commands/targets/remove.js' -import Command79 from './commands/targets/restart.js' -import Command80 from './commands/targets/show.js' -import Command81 from './commands/targets/start.js' -import Command82 from './commands/targets/status.js' -import Command83 from './commands/targets/stop.js' -import Command84 from './commands/targets/use.js' -import Command85 from './commands/update.js' -import Command86 from './commands/verify.js' -import Command87 from './commands/verify/approve.js' -import Command88 from './commands/verify/cancel.js' -import Command89 from './commands/verify/list.js' -import Command90 from './commands/verify/qr-confirm.js' -import Command91 from './commands/verify/qr-scan.js' -import Command92 from './commands/verify/recovery-key.js' -import Command93 from './commands/verify/reset-recovery-key.js' -import Command94 from './commands/verify/sas.js' -import Command95 from './commands/verify/sas-confirm.js' -import Command96 from './commands/verify/show.js' -import Command97 from './commands/verify/start.js' -import Command98 from './commands/verify/status.js' -import Command99 from './commands/version.js' -import Command100 from './commands/watch.js' +import Command62 from './commands/resolve/account.js' +import Command63 from './commands/resolve/bridge.js' +import Command64 from './commands/resolve/chat.js' +import Command65 from './commands/resolve/contact.js' +import Command66 from './commands/resolve/target.js' +import Command67 from './commands/rpc.js' +import Command68 from './commands/schema.js' +import Command69 from './commands/send/file.js' +import Command70 from './commands/send/react.js' +import Command71 from './commands/send/sticker.js' +import Command72 from './commands/send/text.js' +import Command73 from './commands/send/unreact.js' +import Command74 from './commands/send/voice.js' +import Command75 from './commands/setup.js' +import Command76 from './commands/status.js' +import Command77 from './commands/targets/add/desktop.js' +import Command78 from './commands/targets/add/remote.js' +import Command79 from './commands/targets/add/server.js' +import Command80 from './commands/targets/disable.js' +import Command81 from './commands/targets/enable.js' +import Command82 from './commands/targets/list.js' +import Command83 from './commands/targets/logs.js' +import Command84 from './commands/targets/remove.js' +import Command85 from './commands/targets/restart.js' +import Command86 from './commands/targets/show.js' +import Command87 from './commands/targets/start.js' +import Command88 from './commands/targets/status.js' +import Command89 from './commands/targets/stop.js' +import Command90 from './commands/targets/use.js' +import Command91 from './commands/update.js' +import Command92 from './commands/verify.js' +import Command93 from './commands/verify/approve.js' +import Command94 from './commands/verify/cancel.js' +import Command95 from './commands/verify/list.js' +import Command96 from './commands/verify/qr-confirm.js' +import Command97 from './commands/verify/qr-scan.js' +import Command98 from './commands/verify/recovery-key.js' +import Command99 from './commands/verify/reset-recovery-key.js' +import Command100 from './commands/verify/sas.js' +import Command101 from './commands/verify/sas-confirm.js' +import Command102 from './commands/verify/show.js' +import Command103 from './commands/verify/start.js' +import Command104 from './commands/verify/status.js' +import Command105 from './commands/version.js' +import Command106 from './commands/watch.js' export const commands = { 'accounts': Command1, @@ -156,6 +162,7 @@ export const commands = { 'export': Command47, 'install:desktop': Command48, 'install:server': Command49, + 'ls': Command21, 'man': Command50, 'media:download': Command51, 'messages:context': Command52, @@ -168,44 +175,52 @@ export const commands = { 'plugins': Command59, 'plugins:available': Command60, 'presence': Command61, - 'rpc': Command62, - 'send:file': Command63, - 'send:react': Command64, - 'send:sticker': Command65, - 'send:text': Command66, - 'send:unreact': Command67, - 'send:voice': Command68, - 'setup': Command69, - 'status': Command70, - 'targets': Command76, - 'targets:add:desktop': Command71, - 'targets:add:remote': Command72, - 'targets:add:server': Command73, - 'targets:disable': Command74, - 'targets:enable': Command75, - 'targets:list': Command76, - 'targets:logs': Command77, - 'targets:remove': Command78, - 'targets:restart': Command79, - 'targets:show': Command80, - 'targets:start': Command81, - 'targets:status': Command82, - 'targets:stop': Command83, - 'targets:use': Command84, - 'update': Command85, - 'verify': Command86, - 'verify:approve': Command87, - 'verify:cancel': Command88, - 'verify:list': Command89, - 'verify:qr-confirm': Command90, - 'verify:qr-scan': Command91, - 'verify:recovery-key': Command92, - 'verify:reset-recovery-key': Command93, - 'verify:sas': Command94, - 'verify:sas-confirm': Command95, - 'verify:show': Command96, - 'verify:start': Command97, - 'verify:status': Command98, - 'version': Command99, - 'watch': Command100, + 'resolve:account': Command62, + 'resolve:bridge': Command63, + 'resolve:chat': Command64, + 'resolve:contact': Command65, + 'resolve:target': Command66, + 'rpc': Command67, + 'schema': Command68, + 'search': Command57, + 'send': Command72, + 'send:file': Command69, + 'send:react': Command70, + 'send:sticker': Command71, + 'send:text': Command72, + 'send:unreact': Command73, + 'send:voice': Command74, + 'setup': Command75, + 'status': Command76, + 'targets': Command82, + 'targets:add:desktop': Command77, + 'targets:add:remote': Command78, + 'targets:add:server': Command79, + 'targets:disable': Command80, + 'targets:enable': Command81, + 'targets:list': Command82, + 'targets:logs': Command83, + 'targets:remove': Command84, + 'targets:restart': Command85, + 'targets:show': Command86, + 'targets:start': Command87, + 'targets:status': Command88, + 'targets:stop': Command89, + 'targets:use': Command90, + 'update': Command91, + 'verify': Command92, + 'verify:approve': Command93, + 'verify:cancel': Command94, + 'verify:list': Command95, + 'verify:qr-confirm': Command96, + 'verify:qr-scan': Command97, + 'verify:recovery-key': Command98, + 'verify:reset-recovery-key': Command99, + 'verify:sas': Command100, + 'verify:sas-confirm': Command101, + 'verify:show': Command102, + 'verify:start': Command103, + 'verify:status': Command104, + 'version': Command105, + 'watch': Command106, } diff --git a/packages/cli/src/commands/accounts/add.ts b/packages/cli/src/commands/accounts/add.ts index 1db3c81b..a4b1865e 100644 --- a/packages/cli/src/commands/accounts/add.ts +++ b/packages/cli/src/commands/accounts/add.ts @@ -5,7 +5,7 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import type { Bridge, LoginFlow } from '@beeper/desktop-api/resources/bridges.js' import { createClient } from '../../lib/client.js' import { printAccountLoginStep, runGuidedAccountLogin } from '../../lib/account-login.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' type AccountType = Bridge @@ -67,6 +67,22 @@ export default class AccountsAdd extends BeeperCommand { if (!flags.json && loginFlows.length > 1) this.log(`Using flow ${flowID}`) } + if (flags['dry-run']) { + await printDryRun('accounts.add', { + bridgeID: accountType.id, + bridgeName: accountType.displayName, + flowID, + loginID: flags['login-id'], + guided: flags.guided, + nonInteractive: flags['non-interactive'], + cookieKeys: Object.keys(parseKeyValueFlags(flags.cookie, '--cookie')), + fieldKeys: Object.keys(parseKeyValueFlags(flags.field, '--field')), + webview: flags.webview, + webviewBackend: flags['webview-backend'], + }, flags.json ? 'json' : 'human') + return + } + const step = await client.bridges.loginSessions.create(accountType.id, { flowID, loginID: flags['login-id'], diff --git a/packages/cli/src/commands/accounts/remove.ts b/packages/cli/src/commands/accounts/remove.ts index 6bf7180f..93e20aa5 100644 --- a/packages/cli/src/commands/accounts/remove.ts +++ b/packages/cli/src/commands/accounts/remove.ts @@ -1,7 +1,7 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' import { resolveAccountID } from '../../lib/resolve.js' export default class AccountsRemove extends BeeperCommand { @@ -14,6 +14,10 @@ export default class AccountsRemove extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const accountID = await resolveAccountID(client, args.account) + if (flags['dry-run']) { + await printDryRun('accounts.remove', { accountID }, flags.json ? 'json' : 'human') + return + } const accounts = client.accounts as any if (accounts.delete) await accounts.delete(accountID) else if (accounts.remove) await accounts.remove(accountID) diff --git a/packages/cli/src/commands/accounts/use.ts b/packages/cli/src/commands/accounts/use.ts index cbb88f4f..441f560c 100644 --- a/packages/cli/src/commands/accounts/use.ts +++ b/packages/cli/src/commands/accounts/use.ts @@ -1,7 +1,7 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' import { resolveAccountID } from '../../lib/resolve.js' import { updateConfig } from '../../lib/targets.js' @@ -15,12 +15,20 @@ export default class AccountsUse extends BeeperCommand { const { args, flags } = await this.parse(AccountsUse) ensureWritable(flags) if (args.account === '') { + if (flags['dry-run']) { + await printDryRun('accounts.use', { defaultAccount: undefined }, flags.json ? 'json' : 'human') + return + } await updateConfig(config => ({ ...config, defaultAccount: undefined })) await printSuccess({ message: 'Cleared default account' }, flags.json ? 'json' : 'human') return } const client = await createClient(flags) const accountID = await resolveAccountID(client, args.account) + if (flags['dry-run']) { + await printDryRun('accounts.use', { defaultAccount: accountID }, flags.json ? 'json' : 'human') + return + } await updateConfig(config => ({ ...config, defaultAccount: accountID })) await printSuccess({ message: `Default account: ${accountID}`, data: { accountID } }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/api/post.ts b/packages/cli/src/commands/api/post.ts index 95c573ce..429f9e22 100644 --- a/packages/cli/src/commands/api/post.ts +++ b/packages/cli/src/commands/api/post.ts @@ -2,7 +2,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { appRequest } from '../../lib/app-api.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class ApiPost extends BeeperCommand { static override summary = 'Call a raw Desktop API POST path with a JSON body' @@ -24,6 +24,10 @@ export default class ApiPost extends BeeperCommand { } catch { throw new Error(`--body is not valid JSON: ${flags.body}`) } + if (flags['dry-run']) { + await printDryRun('api.post', { method: 'POST', path: args.path, body, noAuth: flags['no-auth'], target: flags.target, baseURL: flags['base-url'] }, flags.json ? 'json' : 'human') + return + } if (flags['no-auth']) { await printData(await appRequest('POST', args.path, { baseURL: flags['base-url'], body, target: flags.target, token: false }), flags.json ? 'json' : 'human') return diff --git a/packages/cli/src/commands/api/request.ts b/packages/cli/src/commands/api/request.ts index db3c297b..f75cd3d5 100644 --- a/packages/cli/src/commands/api/request.ts +++ b/packages/cli/src/commands/api/request.ts @@ -1,7 +1,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { appRequest, type AppRequestMethod } from '../../lib/app-api.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class ApiRequest extends BeeperCommand { static override summary = 'Call a raw Desktop API path with any supported HTTP method' @@ -20,6 +20,10 @@ export default class ApiRequest extends BeeperCommand { const method = args.method as AppRequestMethod if (method !== 'GET') ensureWritable(flags) const body = flags.body ? JSON.parse(flags.body) as Record : undefined + if (flags['dry-run'] && method !== 'GET') { + await printDryRun('api.request', { method, path: args.path, body, noAuth: flags['no-auth'], target: flags.target, baseURL: flags['base-url'] }, flags.json ? 'json' : 'human') + return + } if (flags['no-auth']) { await printData(await appRequest(method, args.path, { baseURL: flags['base-url'], body, target: flags.target, token: false }), flags.json ? 'json' : 'human') return diff --git a/packages/cli/src/commands/auth/email/response.ts b/packages/cli/src/commands/auth/email/response.ts index 1a517ecb..f4a34d9d 100644 --- a/packages/cli/src/commands/auth/email/response.ts +++ b/packages/cli/src/commands/auth/email/response.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { resolveTarget } from '../../../lib/targets.js' import { finishEmailSetup } from '../../../lib/setup-login.js' -import { printData } from '../../../lib/output.js' +import { printData, printDryRun } from '../../../lib/output.js' export default class AuthEmailResponse extends BeeperCommand { static override summary = 'Finish email sign-in with a verification code' @@ -17,6 +17,10 @@ export default class AuthEmailResponse extends BeeperCommand { const { flags } = await this.parse(AuthEmailResponse) ensureWritable(flags) const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] }) + if (flags['dry-run']) { + await printDryRun('auth.email.response', { target: target.id, baseURL: target.baseURL, setupRequestID: flags['setup-request-id'], username: flags.username, yes: flags.yes }, flags.json ? 'json' : 'human') + return + } const data = await finishEmailSetup(target, { code: flags.code, json: flags.json, diff --git a/packages/cli/src/commands/auth/logout.ts b/packages/cli/src/commands/auth/logout.ts index a100569f..df2e90b1 100644 --- a/packages/cli/src/commands/auth/logout.ts +++ b/packages/cli/src/commands/auth/logout.ts @@ -1,6 +1,6 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { clearTargetAuth, resolveTarget } from '../../lib/targets.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class AuthLogout extends BeeperCommand { static override summary = 'Clear stored authentication' @@ -13,6 +13,10 @@ export default class AuthLogout extends BeeperCommand { throw new Error('auth logout cannot clear BEEPER_ACCESS_TOKEN from the environment; unset it in the calling process.') } const token = target.auth?.accessToken + if (flags['dry-run']) { + await printDryRun('auth.logout', { target: target.id, baseURL: target.baseURL, hadToken: Boolean(token), revokeToken: Boolean(token) }, flags.json ? 'json' : 'human') + return + } let revoked = false if (token) { const response = await fetch(new URL('/oauth/revoke', target.baseURL), { diff --git a/packages/cli/src/commands/chats/archive.ts b/packages/cli/src/commands/chats/archive.ts index 17b27e61..cf690869 100644 --- a/packages/cli/src/commands/chats/archive.ts +++ b/packages/cli/src/commands/chats/archive.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsArchive extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsArchive extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.archive', { chatID, isArchived: true }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { isArchived: true }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/avatar.ts b/packages/cli/src/commands/chats/avatar.ts index cee1f6b1..c511dbfc 100644 --- a/packages/cli/src/commands/chats/avatar.ts +++ b/packages/cli/src/commands/chats/avatar.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsAvatar extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsAvatar extends BeeperCommand { if (!flags.clear && !flags.file) throw new Error('Provide --file or --clear') const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.avatar', { chatID, imgURL: flags.clear ? null : flags.file }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { imgURL: flags.clear ? null : flags.file }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/description.ts b/packages/cli/src/commands/chats/description.ts index 429c7ac6..6c382ae8 100644 --- a/packages/cli/src/commands/chats/description.ts +++ b/packages/cli/src/commands/chats/description.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsDescription extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsDescription extends BeeperCommand { if (!flags.clear && !flags.description) throw new Error('Provide --description or --clear') const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.description', { chatID, description: flags.clear ? null : flags.description }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { description: flags.clear ? null : flags.description }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/disappear.ts b/packages/cli/src/commands/chats/disappear.ts index 16eace97..07c858fe 100644 --- a/packages/cli/src/commands/chats/disappear.ts +++ b/packages/cli/src/commands/chats/disappear.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsDisappear extends BeeperCommand { @@ -22,6 +22,10 @@ export default class ChatsDisappear extends BeeperCommand { const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) const expiry = flags.seconds.toLowerCase() === 'off' ? null : Number(flags.seconds) if (expiry !== null && (!Number.isInteger(expiry) || expiry < 0)) throw new Error('--seconds must be a positive integer or "off"') + if (flags['dry-run']) { + await printDryRun('chats.disappear', { chatID, messageExpirySeconds: expiry }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { messageExpirySeconds: expiry }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/draft.ts b/packages/cli/src/commands/chats/draft.ts index 15c8df79..2db6abb3 100644 --- a/packages/cli/src/commands/chats/draft.ts +++ b/packages/cli/src/commands/chats/draft.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsDraft extends BeeperCommand { @@ -24,9 +24,17 @@ export default class ChatsDraft extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) if (flags.clear) { + if (flags['dry-run']) { + await printDryRun('chats.draft', { chatID, draft: null }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { draft: null }), flags.json ? 'json' : 'human') return } + if (flags['dry-run']) { + await printDryRun('chats.draft', { chatID, draft: { text: flags.text!, file: flags.file, fileName: flags.filename, mimeType: flags.mime } }, flags.json ? 'json' : 'human') + return + } const upload = flags.file ? await client.assets.upload({ file: createReadStream(flags.file), fileName: flags.filename, mimeType: flags.mime }) : undefined await printData(await client.chats.update(chatID, { draft: { text: flags.text!, attachments: upload?.uploadID ? { [upload.uploadID]: upload as any } : undefined } }), flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/chats/focus.ts b/packages/cli/src/commands/chats/focus.ts index ef30b469..e8b68036 100644 --- a/packages/cli/src/commands/chats/focus.ts +++ b/packages/cli/src/commands/chats/focus.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsFocus extends BeeperCommand { @@ -20,6 +20,10 @@ export default class ChatsFocus extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.focus', { chatID, messageID: flags.message, draftText: flags.draft, draftAttachmentPath: flags.attachment }, flags.json ? 'json' : 'human') + return + } await printData(await client.focus({ chatID, messageID: flags.message, draftText: flags.draft, draftAttachmentPath: flags.attachment }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/mark-read.ts b/packages/cli/src/commands/chats/mark-read.ts index 4f942867..176ff556 100644 --- a/packages/cli/src/commands/chats/mark-read.ts +++ b/packages/cli/src/commands/chats/mark-read.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsMarkRead extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsMarkRead extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.mark-read', { chatID, messageID: flags.message }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.markRead(chatID, { messageID: flags.message }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/mark-unread.ts b/packages/cli/src/commands/chats/mark-unread.ts index 26bd3df7..e70043ff 100644 --- a/packages/cli/src/commands/chats/mark-unread.ts +++ b/packages/cli/src/commands/chats/mark-unread.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsMarkUnread extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsMarkUnread extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.mark-unread', { chatID, messageID: flags.message }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.markUnread(chatID, { messageID: flags.message }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/mute.ts b/packages/cli/src/commands/chats/mute.ts index 082fbcbb..39d6e338 100644 --- a/packages/cli/src/commands/chats/mute.ts +++ b/packages/cli/src/commands/chats/mute.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsMute extends BeeperCommand { @@ -16,6 +16,10 @@ export default class ChatsMute extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.mute', { chatID, isMuted: true }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { isMuted: true }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/notify-anyway.ts b/packages/cli/src/commands/chats/notify-anyway.ts index 27f20517..1ffdf72e 100644 --- a/packages/cli/src/commands/chats/notify-anyway.ts +++ b/packages/cli/src/commands/chats/notify-anyway.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsNotifyAnyway extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsNotifyAnyway extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.notify-anyway', { chatID }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.notifyAnyway(chatID), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/pin.ts b/packages/cli/src/commands/chats/pin.ts index a5e0b024..26543935 100644 --- a/packages/cli/src/commands/chats/pin.ts +++ b/packages/cli/src/commands/chats/pin.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsPin extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsPin extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.pin', { chatID, isPinned: true }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { isPinned: true }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/priority.ts b/packages/cli/src/commands/chats/priority.ts index 68d75ec5..1ae932f9 100644 --- a/packages/cli/src/commands/chats/priority.ts +++ b/packages/cli/src/commands/chats/priority.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsPriority extends BeeperCommand { @@ -19,6 +19,10 @@ export default class ChatsPriority extends BeeperCommand { const update = flags.level === 'inbox' ? { isArchived: false, isLowPriority: false } : { isLowPriority: true } + if (flags['dry-run']) { + await printDryRun('chats.priority', { chatID, level: flags.level, update }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, update), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/remind.ts b/packages/cli/src/commands/chats/remind.ts index 7edd7d6e..fce00d4e 100644 --- a/packages/cli/src/commands/chats/remind.ts +++ b/packages/cli/src/commands/chats/remind.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsRemind extends BeeperCommand { @@ -18,6 +18,10 @@ export default class ChatsRemind extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.remind', { chatID, reminder: { remindAt: flags.when, dismissOnIncomingMessage: flags['dismiss-on-message'] || undefined } }, flags.json ? 'json' : 'human') + return + } await client.chats.reminders.create(chatID, { reminder: { remindAt: flags.when, dismissOnIncomingMessage: flags['dismiss-on-message'] || undefined } }) await printSuccess({ message: 'Reminder set', detail: flags.when, data: { chatID, remindAt: flags.when } }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/chats/rename.ts b/packages/cli/src/commands/chats/rename.ts index 07139fe1..d965bff8 100644 --- a/packages/cli/src/commands/chats/rename.ts +++ b/packages/cli/src/commands/chats/rename.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsRename extends BeeperCommand { @@ -16,6 +16,10 @@ export default class ChatsRename extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.rename', { chatID, title: flags.title }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { title: flags.title }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/start.ts b/packages/cli/src/commands/chats/start.ts index 59f5c6f7..170bd82b 100644 --- a/packages/cli/src/commands/chats/start.ts +++ b/packages/cli/src/commands/chats/start.ts @@ -2,7 +2,7 @@ import { Args, Flags } from '@oclif/core' import type { ChatStartParams } from '@beeper/desktop-api/resources/chats' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { listAccountIDs, resolveAccountID, userQueryFromInput } from '../../lib/resolve.js' export default class ChatsStart extends BeeperCommand { @@ -20,6 +20,10 @@ export default class ChatsStart extends BeeperCommand { const accountID = flags.account ? await resolveAccountID(client, flags.account) : await defaultAccountID(client) const user = userQueryFromInput(args.user) const payload: ChatStartParams & { title?: string } = { accountID, user, title: flags.title } + if (flags['dry-run']) { + await printDryRun('chats.start', payload as unknown as Record, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.start(payload), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/unarchive.ts b/packages/cli/src/commands/chats/unarchive.ts index 0da9088a..8df63c14 100644 --- a/packages/cli/src/commands/chats/unarchive.ts +++ b/packages/cli/src/commands/chats/unarchive.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsUnarchive extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsUnarchive extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.unarchive', { chatID, isArchived: false }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { isArchived: false }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/unmute.ts b/packages/cli/src/commands/chats/unmute.ts index 022ae568..3c7d1ac0 100644 --- a/packages/cli/src/commands/chats/unmute.ts +++ b/packages/cli/src/commands/chats/unmute.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsUnmute extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsUnmute extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.unmute', { chatID, isMuted: false }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { isMuted: false }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/unpin.ts b/packages/cli/src/commands/chats/unpin.ts index cb4ada38..750f6348 100644 --- a/packages/cli/src/commands/chats/unpin.ts +++ b/packages/cli/src/commands/chats/unpin.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsUnpin extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsUnpin extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.unpin', { chatID, isPinned: false }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { isPinned: false }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/unremind.ts b/packages/cli/src/commands/chats/unremind.ts index 437ceca5..15b28aad 100644 --- a/packages/cli/src/commands/chats/unremind.ts +++ b/packages/cli/src/commands/chats/unremind.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsUnremind extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsUnremind extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.unremind', { chatID }, flags.json ? 'json' : 'human') + return + } await client.chats.reminders.delete(chatID) await printSuccess({ message: 'Reminder cleared', data: { chatID } }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/config/reset.ts b/packages/cli/src/commands/config/reset.ts index e5ee7a1a..e3066dad 100644 --- a/packages/cli/src/commands/config/reset.ts +++ b/packages/cli/src/commands/config/reset.ts @@ -1,6 +1,6 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { resetConfig } from '../../lib/targets.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class ConfigReset extends BeeperCommand { static override summary = 'Reset CLI configuration' @@ -8,6 +8,10 @@ export default class ConfigReset extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(ConfigReset) ensureWritable(flags) + if (flags['dry-run']) { + await printDryRun('config.reset', {}, flags.json ? 'json' : 'human') + return + } await resetConfig() await printSuccess({ message: 'Config reset' }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/config/set.ts b/packages/cli/src/commands/config/set.ts index 2bdc2a05..1bc9e5b2 100644 --- a/packages/cli/src/commands/config/set.ts +++ b/packages/cli/src/commands/config/set.ts @@ -1,7 +1,7 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { updateConfig } from '../../lib/targets.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class ConfigSet extends BeeperCommand { static override summary = 'Set a CLI configuration value' @@ -14,6 +14,10 @@ export default class ConfigSet extends BeeperCommand { const { args, flags } = await this.parse(ConfigSet) ensureWritable(flags) const nextValue = args.value === '' ? undefined : args.value + if (flags['dry-run']) { + await printDryRun('config.set', { [args.key]: nextValue }, flags.json ? 'json' : 'human') + return + } await updateConfig(config => ({ ...config, [args.key]: nextValue })) await printSuccess({ message: nextValue === undefined ? `Cleared ${args.key}` : `Set ${args.key}`, diff --git a/packages/cli/src/commands/contacts/search.ts b/packages/cli/src/commands/contacts/search.ts index 7a809f6d..2c9d1d08 100644 --- a/packages/cli/src/commands/contacts/search.ts +++ b/packages/cli/src/commands/contacts/search.ts @@ -2,7 +2,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { apiCopy, cliCopy } from '../../lib/copy.js' -import { printList } from '../../lib/output.js' +import { isMachineReadableOutput, printList } from '../../lib/output.js' import { listAccountIDs, resolveAccountIDs } from '../../lib/resolve.js' import { withInkSpinner as withSpinner } from '../../lib/ink/spinner.js' @@ -32,7 +32,7 @@ export default class ContactsSearch extends BeeperCommand { } return collected } - const useSpinner = !flags.json + const useSpinner = !isMachineReadableOutput(flags.json ? 'json' : 'human') const results = useSpinner ? await withSpinner(`Searching contacts for "${args.query}"…`, load, { done: value => `${value.length} match${value.length === 1 ? '' : 'es'} across ${accountIDs.length} account${accountIDs.length === 1 ? '' : 's'}`, diff --git a/packages/cli/src/commands/export.ts b/packages/cli/src/commands/export.ts index 525bc66a..ef7af0ae 100644 --- a/packages/cli/src/commands/export.ts +++ b/packages/cli/src/commands/export.ts @@ -1,7 +1,8 @@ import { Flags } from '@oclif/core' -import { BeeperCommand } from '../lib/command.js' +import { BeeperCommand, ensureWritable } from '../lib/command.js' import { createClient } from '../lib/client.js' import { exportBeeperData } from '../lib/export/index.js' +import { printDryRun } from '../lib/output.js' import { resolveAccountIDs, resolveChatID } from '../lib/resolve.js' export default class Export extends BeeperCommand { @@ -26,11 +27,25 @@ export default class Export extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(Export) + ensureWritable(flags) const client = await createClient(flags) const accountIDs = await resolveAccountIDs(client, flags.account, { allowMultiplePerInput: true }) const chatIDs = flags.chat?.length ? await Promise.all(flags.chat.map(chat => resolveChatID(client, chat, { accountIDs, pick: flags.pick }))) : undefined + if (flags['dry-run']) { + await printDryRun('export', { + accountIDs, + chatIDs, + downloadAttachments: !flags['no-attachments'], + force: flags.force, + limitChats: flags['limit-chats'], + limitMessages: flags['limit-messages'], + maxParticipants: flags['max-participants'], + outDir: flags.out, + }, flags.json ? 'json' : 'human') + return + } const manifest = await exportBeeperData(client, { accountIDs, diff --git a/packages/cli/src/commands/install/desktop.ts b/packages/cli/src/commands/install/desktop.ts index f617e172..37901fb8 100644 --- a/packages/cli/src/commands/install/desktop.ts +++ b/packages/cli/src/commands/install/desktop.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { installDesktop, type InstallChannel } from '../../lib/installations.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class SetupInstallDesktop extends BeeperCommand { static override summary = 'Install Beeper Desktop locally' @@ -12,6 +12,10 @@ export default class SetupInstallDesktop extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(SetupInstallDesktop) ensureWritable(flags) + if (flags['dry-run']) { + await printDryRun('install.desktop', { channel: flags.channel }, flags.json ? 'json' : 'human') + return + } const installation = await installDesktop({ channel: flags.channel as InstallChannel }) await printSuccess({ message: `Installed Beeper Desktop ${installation.version ?? ''}`.trim(), diff --git a/packages/cli/src/commands/install/server.ts b/packages/cli/src/commands/install/server.ts index c612a131..a2493a83 100644 --- a/packages/cli/src/commands/install/server.ts +++ b/packages/cli/src/commands/install/server.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { installServer, type InstallChannel } from '../../lib/installations.js' import { pathSetupHint } from '../../lib/env.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class SetupInstallServer extends BeeperCommand { static override summary = 'Install Beeper Server locally' @@ -14,6 +14,10 @@ export default class SetupInstallServer extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(SetupInstallServer) ensureWritable(flags) + if (flags['dry-run']) { + await printDryRun('install.server', { channel: flags.channel, serverEnv: flags['server-env'] }, flags.json ? 'json' : 'human') + return + } const installation = await installServer({ channel: flags.channel as InstallChannel, serverEnv: flags['server-env'] }) await printSuccess({ message: `Installed Beeper Server ${installation.version ?? ''}`.trim(), diff --git a/packages/cli/src/commands/man.ts b/packages/cli/src/commands/man.ts index 275fa1ee..c8fd9508 100644 --- a/packages/cli/src/commands/man.ts +++ b/packages/cli/src/commands/man.ts @@ -1,4 +1,5 @@ import { BeeperCommand } from '../lib/command.js' +import { metadataForCommand } from '../lib/command-metadata.js' import { commandManifest } from '../lib/manifest.js' import { printCommands } from '../lib/output.js' export default class Man extends BeeperCommand { @@ -17,53 +18,3 @@ export default class Man extends BeeperCommand { await printCommands(commands, flags.json ? 'json' : 'human', { title: 'Beeper CLI' }) } } - -function metadataForCommand(command: string): { - mutates: boolean - requiresAuth: boolean - selectors: string[] - output: 'data' | 'list' | 'stream' | 'success' | 'send-result' | 'manual' - related: string[] -} { - const parts = command.split(' ') - const root = parts[0] ?? '' - const mutatingRoots = new Set(['setup', 'install', 'send', 'update']) - const mutatingVerbs = new Set([ - 'add', 'archive', 'unarchive', 'pin', 'unpin', 'mute', 'unmute', 'mark-read', 'mark-unread', - 'priority', 'notify-anyway', 'rename', 'description', 'avatar', 'draft', 'disappear', 'remind', - 'unremind', 'focus', 'edit', 'delete', 'remove', 'use', 'set', 'reset', 'logout', 'start', 'stop', - 'restart', 'enable', 'disable', 'approve', 'recovery-key', 'reset-recovery-key', 'cancel', 'sas', - 'sas-confirm', 'qr-scan', 'qr-confirm', - ]) - const mutates = mutatingRoots.has(root) || parts.some(part => mutatingVerbs.has(part ?? '')) - const localOnly = root === 'config' || root === 'completion' || root === 'docs' || root === 'version' || root === 'man' - const requiresAuth = !localOnly && command !== 'targets list' && !command.startsWith('targets add') && !command.startsWith('install ') - const selectors = [ - command.includes('chats ') || command.includes('messages ') || command.startsWith('send ') || command === 'presence' ? 'chat' : undefined, - command.includes('accounts ') || command.includes('contacts ') || command === 'chats start' ? 'account' : undefined, - command.includes('targets ') || command === 'status' || command === 'doctor' || command.startsWith('auth ') || command.startsWith('verify') ? 'target' : undefined, - command.startsWith('bridges ') || command === 'accounts add' ? 'bridge' : undefined, - command.includes('messages ') || command.startsWith('send react') || command.startsWith('send unreact') ? 'message' : undefined, - ].filter((value): value is string => Boolean(value)) - const output = command.startsWith('send ') ? 'send-result' - : command === 'watch' || command === 'rpc' ? 'stream' - : command === 'man' ? 'manual' - : command.endsWith('list') || command.includes('search') || command === 'bridges list' ? 'list' - : mutates ? 'success' - : 'data' - const related = relatedForCommand(command) - return { mutates, requiresAuth, selectors, output, related } -} - -function relatedForCommand(command: string): string[] { - if (command.startsWith('send ')) return ['messages list', 'watch'] - if (command.startsWith('messages ')) return ['chats list', 'send text'] - if (command.startsWith('chats ')) return ['messages list', 'send text'] - if (command.startsWith('bridges ')) return ['accounts add', 'accounts list'] - if (command.startsWith('accounts ')) return ['bridges list', 'chats list'] - if (command.startsWith('targets ')) return ['status', 'doctor'] - if (command === 'status') return ['doctor', 'setup'] - if (command === 'doctor') return ['status', 'setup'] - if (command.startsWith('verify')) return ['setup', 'status'] - return [] -} diff --git a/packages/cli/src/commands/media/download.ts b/packages/cli/src/commands/media/download.ts index 9636d4d7..a6c01001 100644 --- a/packages/cli/src/commands/media/download.ts +++ b/packages/cli/src/commands/media/download.ts @@ -3,7 +3,7 @@ import { mkdir, writeFile } from 'node:fs/promises' import { basename, join } from 'node:path' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class MediaDownload extends BeeperCommand { static override summary = 'Download message media' static override args = { url: Args.string({ required: true, description: 'mxc:// or localmxc:// URL' }) } @@ -12,6 +12,11 @@ export default class MediaDownload extends BeeperCommand { } async run(): Promise { const { args, flags } = await this.parse(MediaDownload) + if (flags['dry-run'] && flags.out !== '-') { + ensureWritable(flags) + await printDryRun('media.download', { url: args.url, out: flags.out }, flags.json ? 'json' : 'human') + return + } const client = await createClient(flags) const response = await client.assets.serve({ url: args.url }) const buffer = Buffer.from(await response.arrayBuffer()) diff --git a/packages/cli/src/commands/messages/delete.ts b/packages/cli/src/commands/messages/delete.ts index 4488cdac..ac011a6f 100644 --- a/packages/cli/src/commands/messages/delete.ts +++ b/packages/cli/src/commands/messages/delete.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class MessagesDelete extends BeeperCommand { @@ -17,6 +17,10 @@ export default class MessagesDelete extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('messages.delete', { chatID, messageID: flags.id, forEveryone: flags['for-everyone'] }, flags.json ? 'json' : 'human') + return + } await client.messages.delete(flags.id, { chatID, forEveryone: flags['for-everyone'] || undefined }) await printSuccess({ message: flags['for-everyone'] ? 'Deleted for everyone' : 'Deleted', data: { chatID, messageID: flags.id, forEveryone: flags['for-everyone'] } }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/messages/edit.ts b/packages/cli/src/commands/messages/edit.ts index bc5a2cc4..a473017b 100644 --- a/packages/cli/src/commands/messages/edit.ts +++ b/packages/cli/src/commands/messages/edit.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class MessagesEdit extends BeeperCommand { @@ -17,6 +17,10 @@ export default class MessagesEdit extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('messages.edit', { chatID, messageID: flags.id, text: flags.message }, flags.json ? 'json' : 'human') + return + } await printData(await client.messages.update(flags.id, { chatID, text: flags.message }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/messages/export.ts b/packages/cli/src/commands/messages/export.ts index 74080e8f..9615fd16 100644 --- a/packages/cli/src/commands/messages/export.ts +++ b/packages/cli/src/commands/messages/export.ts @@ -1,7 +1,8 @@ import { writeFile } from 'node:fs/promises' import { Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' +import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' +import { printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class MessagesExport extends BeeperCommand { @@ -22,8 +23,22 @@ export default class MessagesExport extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(MessagesExport) if (flags['before-cursor'] && flags['after-cursor']) throw new Error('Use only one of --before-cursor or --after-cursor') + if (flags.output !== '-') ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('messages.export', { + chatID, + output: flags.output, + beforeCursor: flags['before-cursor'], + afterCursor: flags['after-cursor'], + after: flags.after, + before: flags.before, + limit: flags.limit, + asc: flags.asc, + }, flags.json ? 'json' : 'human') + return + } const cursor = flags['before-cursor'] ?? flags['after-cursor'] const direction = flags['before-cursor'] ? 'before' : flags['after-cursor'] ? 'after' : undefined const afterTs = flags.after ? Date.parse(flags.after) : undefined diff --git a/packages/cli/src/commands/messages/search.ts b/packages/cli/src/commands/messages/search.ts index d88cd8f6..fe82ed54 100644 --- a/packages/cli/src/commands/messages/search.ts +++ b/packages/cli/src/commands/messages/search.ts @@ -2,7 +2,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { usageError } from '../../lib/errors.js' -import { collectPage, printIDs, printList } from '../../lib/output.js' +import { collectPage, isMachineReadableOutput, printIDs, printList } from '../../lib/output.js' import { resolveAccountIDs, resolveChatID } from '../../lib/resolve.js' import { withInkSpinner as withSpinner } from '../../lib/ink/spinner.js' @@ -58,7 +58,7 @@ export default class MessagesSearch extends BeeperCommand { query: args.query, sender: flags.sender as 'me' | 'others' | (string & {}) | undefined, } - const useSpinner = !flags.json && !flags.ids + const useSpinner = !isMachineReadableOutput(flags.ids ? 'ids' : flags.json ? 'json' : 'human') const label = args.query ? `Searching messages for "${args.query}"…` : 'Searching messages…' const items = useSpinner ? await withSpinner(label, () => collectPage(client.messages.search(params), flags.limit), { diff --git a/packages/cli/src/commands/presence.ts b/packages/cli/src/commands/presence.ts index ffcc3df5..e914148f 100644 --- a/packages/cli/src/commands/presence.ts +++ b/packages/cli/src/commands/presence.ts @@ -2,7 +2,7 @@ import { setTimeout as delay } from 'node:timers/promises' import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../lib/command.js' import { createClient } from '../lib/client.js' -import { printSuccess } from '../lib/output.js' +import { printDryRun, printSuccess } from '../lib/output.js' import { resolveChatID } from '../lib/resolve.js' export default class Presence extends BeeperCommand { @@ -22,6 +22,10 @@ export default class Presence extends BeeperCommand { if (flags.duration !== undefined && flags.state !== 'typing') throw new Error('--duration only applies when --state is typing') const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('presence', { chatID, state: flags.state, durationSeconds: flags.duration }, flags.json ? 'json' : 'human') + return + } const post = (state: 'typing' | 'paused') => client.post(`/v1/chats/${encodeURIComponent(chatID)}/typing`, { body: { state } }) diff --git a/packages/cli/src/commands/resolve/account.ts b/packages/cli/src/commands/resolve/account.ts new file mode 100644 index 00000000..a62ce1b9 --- /dev/null +++ b/packages/cli/src/commands/resolve/account.ts @@ -0,0 +1,46 @@ +import { Args, Flags } from '@oclif/core' +import { BeeperCommand } from '../../lib/command.js' +import { createClient } from '../../lib/client.js' +import { notFound } from '../../lib/errors.js' +import { printData } from '../../lib/output.js' +import { resolveAccountIDs } from '../../lib/resolve.js' + +export default class ResolveAccount extends BeeperCommand { + static override summary = 'Resolve an account selector' + static override args = { + selector: Args.string({ required: true, description: 'Account ID, network, bridge, or account user selector' }), + } + static override flags = { + pick: Flags.integer({ description: 'Select the Nth candidate (1-indexed)' }), + } + + async run(): Promise { + const { args, flags } = await this.parse(ResolveAccount) + const client = await createClient(flags) + const response = await client.accounts.list() + const rows = Array.isArray(response) ? response : ((response as any).items ?? []) + const ids = await resolveAccountIDs(client, [args.selector], { allowMultiplePerInput: true, applyDefault: false }) + const candidates = rows.filter((row: any) => ids?.includes(String(row.accountID ?? row.id))) + if (!candidates.length) throw notFound(`No account matches "${args.selector}"`, { selector: args.selector, kind: 'account' }) + const selected = flags.pick ? candidates[flags.pick - 1] : candidates.length === 1 ? candidates[0] : undefined + if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${candidates.length} matching accounts`, { selector: args.selector, pick: flags.pick, count: candidates.length }) + await printData({ + selector: args.selector, + kind: 'account', + selected: selected ? accountCandidate(selected, candidates.indexOf(selected) + 1) : null, + candidates: candidates.map((account: any, index: number) => accountCandidate(account, index + 1)), + }, flags.json ? 'json' : 'human') + } +} + +function accountCandidate(account: any, pick: number): Record { + return { + pick, + id: account.accountID ?? account.id, + accountID: account.accountID, + network: account.network, + bridge: account.bridge, + user: account.user, + raw: account, + } +} diff --git a/packages/cli/src/commands/resolve/bridge.ts b/packages/cli/src/commands/resolve/bridge.ts new file mode 100644 index 00000000..eb0a42f6 --- /dev/null +++ b/packages/cli/src/commands/resolve/bridge.ts @@ -0,0 +1,49 @@ +import { Args, Flags } from '@oclif/core' +import { BeeperCommand } from '../../lib/command.js' +import { createClient } from '../../lib/client.js' +import { notFound } from '../../lib/errors.js' +import { printData } from '../../lib/output.js' + +export default class ResolveBridge extends BeeperCommand { + static override summary = 'Resolve a bridge selector' + static override args = { + selector: Args.string({ required: true, description: 'Bridge ID, type, provider, or display name' }), + } + static override flags = { + pick: Flags.integer({ description: 'Select the Nth candidate (1-indexed)' }), + } + + async run(): Promise { + const { args, flags } = await this.parse(ResolveBridge) + const client = await createClient(flags) + const response = await client.bridges.list() + const rows = ((response as unknown as { items?: Array> }).items ?? []) + const normalized = normalize(args.selector) + const candidates = rows.filter(bridge => + normalize(bridge.id) === normalized || + normalize(bridge.type) === normalized || + normalize(bridge.provider) === normalized || + normalize(bridge.name) === normalized || + normalize(bridge.displayName) === normalized || + normalize(bridge.id).includes(normalized) || + normalize(bridge.displayName).includes(normalized) + ) + if (!candidates.length) throw notFound(`No bridge matches "${args.selector}"`, { selector: args.selector, kind: 'bridge' }) + const selected = flags.pick ? candidates[flags.pick - 1] : candidates.length === 1 ? candidates[0] : undefined + if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${candidates.length} matching bridges`, { selector: args.selector, pick: flags.pick, count: candidates.length }) + await printData({ + selector: args.selector, + kind: 'bridge', + selected: selected ? bridgeCandidate(selected, candidates.indexOf(selected) + 1) : null, + candidates: candidates.map((bridge, index) => bridgeCandidate(bridge, index + 1)), + }, flags.json ? 'json' : 'human') + } +} + +function bridgeCandidate(bridge: Record, pick: number): Record { + return { pick, id: bridge.id, type: bridge.type, provider: bridge.provider, displayName: bridge.displayName ?? bridge.name, status: bridge.status, raw: bridge } +} + +function normalize(value: unknown): string { + return String(value ?? '').trim().toLowerCase().replace(/[\s._-]+/g, '') +} diff --git a/packages/cli/src/commands/resolve/chat.ts b/packages/cli/src/commands/resolve/chat.ts new file mode 100644 index 00000000..82240f61 --- /dev/null +++ b/packages/cli/src/commands/resolve/chat.ts @@ -0,0 +1,68 @@ +import { Args, Flags } from '@oclif/core' +import { BeeperCommand } from '../../lib/command.js' +import { createClient } from '../../lib/client.js' +import { notFound } from '../../lib/errors.js' +import { printData } from '../../lib/output.js' +import { resolveAccountIDs } from '../../lib/resolve.js' + +export default class ResolveChat extends BeeperCommand { + static override summary = 'Resolve a chat selector to concrete chat candidates' + static override args = { + selector: Args.string({ required: true, description: 'Chat ID, local ID, exact title, or search text' }), + } + static override flags = { + account: Flags.string({ multiple: true, description: 'Limit to account selector. Repeat for multiple.' }), + pick: Flags.integer({ description: 'Select the Nth candidate (1-indexed)' }), + limit: Flags.integer({ default: 10, description: 'Maximum candidates to return' }), + } + + async run(): Promise { + const { args, flags } = await this.parse(ResolveChat) + const client = await createClient(flags) + const accountIDs = await resolveAccountIDs(client, flags.account, { allowMultiplePerInput: true }) + const candidates = await collect(client.chats.search({ accountIDs, query: args.selector, scope: 'titles' }), flags.limit) + const normalized = normalize(args.selector) + const exact = candidates.filter(chat => + normalize(chat.id) === normalized || + normalize(chat.localChatID) === normalized || + normalize(chat.title) === normalized + ) + const matches = exact.length ? exact : candidates + if (!matches.length) throw notFound(`No chat matches "${args.selector}"`, { selector: args.selector, kind: 'chat' }) + const selected = flags.pick ? matches[flags.pick - 1] : matches.length === 1 ? matches[0] : undefined + if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${matches.length} matching chats`, { selector: args.selector, pick: flags.pick, count: matches.length }) + await printData({ + selector: args.selector, + kind: 'chat', + selected: selected ? chatCandidate(selected, matches.indexOf(selected) + 1) : null, + candidates: matches.map((chat, index) => chatCandidate(chat, index + 1)), + }, flags.json ? 'json' : 'human') + } +} + +type Chat = Record + +async function collect(iterable: AsyncIterable, limit: number): Promise { + const items: Chat[] = [] + for await (const item of iterable) { + items.push(item as Chat) + if (items.length >= limit) break + } + return items +} + +function chatCandidate(chat: Chat, pick: number): Record { + return { + pick, + id: chat.id, + localChatID: chat.localChatID, + title: chat.title, + network: chat.network, + accountID: chat.accountID, + raw: chat, + } +} + +function normalize(value: unknown): string { + return String(value ?? '').trim().toLowerCase().replace(/[\s._-]+/g, '') +} diff --git a/packages/cli/src/commands/resolve/contact.ts b/packages/cli/src/commands/resolve/contact.ts new file mode 100644 index 00000000..f85f1bd4 --- /dev/null +++ b/packages/cli/src/commands/resolve/contact.ts @@ -0,0 +1,55 @@ +import { Args, Flags } from '@oclif/core' +import { BeeperCommand } from '../../lib/command.js' +import { createClient } from '../../lib/client.js' +import { notFound } from '../../lib/errors.js' +import { printData } from '../../lib/output.js' +import { listAccountIDs, resolveAccountIDs } from '../../lib/resolve.js' + +export default class ResolveContact extends BeeperCommand { + static override summary = 'Resolve a contact selector' + static override args = { + selector: Args.string({ required: true, description: 'Contact name, username, phone, email, or ID' }), + } + static override flags = { + account: Flags.string({ multiple: true, description: 'Limit to account selector. Repeat for multiple.' }), + pick: Flags.integer({ description: 'Select the Nth candidate (1-indexed)' }), + limit: Flags.integer({ default: 10, description: 'Maximum candidates to return per account' }), + } + + async run(): Promise { + const { args, flags } = await this.parse(ResolveContact) + const client = await createClient(flags) + const accountIDs = await resolveAccountIDs(client, flags.account, { allowMultiplePerInput: true }) ?? await listAccountIDs(client) + const candidates: Array> = [] + for (const accountID of accountIDs) { + try { + const result = await client.accounts.contacts.search(accountID, { query: args.selector }) + candidates.push(...result.items.slice(0, flags.limit).map((item: unknown) => ({ ...(item as Record), accountID }))) + } catch { + // Keep searching accounts that support lookup for this selector shape. + } + } + if (!candidates.length) throw notFound(`No contact matches "${args.selector}"`, { selector: args.selector, kind: 'contact' }) + const selected = flags.pick ? candidates[flags.pick - 1] : candidates.length === 1 ? candidates[0] : undefined + if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${candidates.length} matching contacts`, { selector: args.selector, pick: flags.pick, count: candidates.length }) + await printData({ + selector: args.selector, + kind: 'contact', + selected: selected ? contactCandidate(selected, candidates.indexOf(selected) + 1) : null, + candidates: candidates.map((contact, index) => contactCandidate(contact, index + 1)), + }, flags.json ? 'json' : 'human') + } +} + +function contactCandidate(contact: Record, pick: number): Record { + return { + pick, + id: contact.id, + accountID: contact.accountID, + displayName: contact.displayName ?? contact.fullName ?? contact.name, + username: contact.username, + phoneNumber: contact.phoneNumber, + email: contact.email, + raw: contact, + } +} diff --git a/packages/cli/src/commands/resolve/target.ts b/packages/cli/src/commands/resolve/target.ts new file mode 100644 index 00000000..a6b8b325 --- /dev/null +++ b/packages/cli/src/commands/resolve/target.ts @@ -0,0 +1,52 @@ +import { Args, Flags } from '@oclif/core' +import { BeeperCommand } from '../../lib/command.js' +import { notFound } from '../../lib/errors.js' +import { printData } from '../../lib/output.js' +import { builtInDesktopTargetID, listTargets, readConfig, type Target } from '../../lib/targets.js' + +export default class ResolveTarget extends BeeperCommand { + static override summary = 'Resolve a target selector' + static override args = { + selector: Args.string({ required: true, description: 'Target name, ID, type, or base URL' }), + } + static override flags = { + pick: Flags.integer({ description: 'Select the Nth candidate (1-indexed)' }), + } + + async run(): Promise { + const { args, flags } = await this.parse(ResolveTarget) + const config = await readConfig() + const builtIn: Target = { + id: builtInDesktopTargetID, + type: 'desktop', + name: 'Beeper Desktop', + baseURL: process.env.BEEPER_DESKTOP_BASE_URL || config.baseURL || 'http://127.0.0.1:23373', + auth: config.auth, + } + const targets = [builtIn, ...await listTargets()] + const normalized = normalize(args.selector) + const candidates = targets.filter(target => + normalize(target.id) === normalized || + normalize(target.name) === normalized || + normalize(target.type) === normalized || + normalize(target.baseURL).includes(normalized) + ) + if (!candidates.length) throw notFound(`No target matches "${args.selector}"`, { selector: args.selector, kind: 'target' }) + const selected = flags.pick ? candidates[flags.pick - 1] : candidates.length === 1 ? candidates[0] : undefined + if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${candidates.length} matching targets`, { selector: args.selector, pick: flags.pick, count: candidates.length }) + await printData({ + selector: args.selector, + kind: 'target', + selected: selected ? targetCandidate(selected, candidates.indexOf(selected) + 1) : null, + candidates: candidates.map((target, index) => targetCandidate(target, index + 1)), + }, flags.json ? 'json' : 'human') + } +} + +function targetCandidate(target: Target, pick: number): Record { + return { pick, id: target.id, name: target.name, type: target.type, baseURL: target.baseURL, managed: target.managed, raw: target } +} + +function normalize(value: unknown): string { + return String(value ?? '').trim().toLowerCase().replace(/[\s._-]+/g, '') +} diff --git a/packages/cli/src/commands/schema.ts b/packages/cli/src/commands/schema.ts new file mode 100644 index 00000000..94e872c0 --- /dev/null +++ b/packages/cli/src/commands/schema.ts @@ -0,0 +1,125 @@ +import { Args } from '@oclif/core' +import { BeeperCommand } from '../lib/command.js' +import { metadataForCommand } from '../lib/command-metadata.js' +import { commandManifest } from '../lib/manifest.js' +import { printData } from '../lib/output.js' + +type RawCommand = { + id: string + aliases?: string[] + args?: Record + description?: string + flags?: Record + hidden?: boolean + pluginName?: string + summary?: string +} + +export default class Schema extends BeeperCommand { + static override summary = 'Print machine-readable command/flag schema' + static override description = 'Agent-first schema for commands, flags, args, examples, mutation metadata, selectors, output shapes, and related commands.' + static override args = { + command: Args.string({ required: false, description: 'Optional command path, such as "messages search"', multiple: true }), + } + + async run(): Promise { + const { args } = await this.parse(Schema) + const requested = Array.isArray(args.command) ? args.command.join(' ') : undefined + const manifestByCommand = new Map(commandManifest.map(item => [item.command, item])) + const commands = (this.config.commands as RawCommand[]) + .filter(command => !command.hidden) + .map(command => { + const path = command.id.replaceAll(':', ' ') + const manifest = manifestByCommand.get(path) + const metadata = metadataForCommand(path) + return { + path, + id: command.id, + aliases: (command.aliases ?? []).map(alias => alias.replaceAll(':', ' ')), + summary: command.summary ?? manifest?.description ?? command.description ?? '', + description: command.description ?? manifest?.description ?? command.summary ?? '', + examples: manifest?.examples ?? [], + args: normalizeFields(command.args), + flags: normalizeFields(command.flags), + ...metadata, + supports: { + dryRun: metadata.mutates, + force: metadata.mutates, + format: true, + noInput: true, + readOnly: true, + select: true, + }, + outputShape: outputShape(metadata.output), + } + }) + .sort((a, b) => a.path.localeCompare(b.path)) + + const filtered = requested + ? commands.filter(command => command.path === requested || command.path.startsWith(`${requested} `)) + : commands + + await printData({ + schemaVersion: 1, + bin: this.config.bin, + version: this.config.version, + defaults: { + stdout: 'primary command output only', + stderr: 'diagnostics, progress, events, and structured errors', + nonTTYFormat: 'json', + ttyFormat: 'table', + }, + formats: ['json', 'jsonl', 'table', 'text', 'ids'], + exitCodes: { + 0: 'success', + 1: 'generic runtime error', + 2: 'usage error', + 3: 'auth required', + 4: 'target/account not ready', + 5: 'selector matched nothing', + 6: 'ambiguous selector', + 127: 'declined did-you-mean suggestion', + }, + commands: filtered, + }, 'json') + } +} + +function normalizeFields(fields: Record | undefined): Array> { + if (!fields) return [] + return Object.entries(fields).map(([name, raw]) => normalizeField(name, raw)) +} + +function normalizeField(name: string, raw: unknown): Record { + const record = raw && typeof raw === 'object' ? raw as Record : {} + return { + name, + description: record.description ?? record.summary ?? '', + required: Boolean(record.required), + multiple: Boolean(record.multiple), + default: record.default, + options: record.options, + char: record.char, + type: typeName(record), + } +} + +function typeName(record: Record): string { + if (Array.isArray(record.options)) return 'enum' + if (record.type === 'boolean' || record.type === 'option') return String(record.type) + if (typeof record.parse === 'function') return 'string' + if (typeof record.default === 'boolean') return 'boolean' + if (typeof record.default === 'number') return 'integer' + return 'string' +} + +function outputShape(kind: string): Record { + const envelope = { ok: true, data: '', error: null, meta: '' } + switch (kind) { + case 'list': return { kind, envelope, data: 'array' } + case 'stream': return { kind, data: 'jsonl events or RPC lines' } + case 'success': return { kind, envelope, data: { message: 'string', detail: 'string?', entity: 'object?' } } + case 'send-result': return { kind, envelope, data: { chatID: 'string', pendingMessageID: 'string?', state: 'string?' } } + default: return { kind, envelope, data: 'object' } + } +} diff --git a/packages/cli/src/commands/send/file.ts b/packages/cli/src/commands/send/file.ts index 67ed8ae6..d021ccb8 100644 --- a/packages/cli/src/commands/send/file.ts +++ b/packages/cli/src/commands/send/file.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' import { sendMessage } from '../../lib/send-message.js' @@ -29,6 +29,11 @@ export default class SendFile extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) - await printData(await sendMessage(client, { chatID, file: flags.file, fileName: flags.filename, mimeType: flags.mime, replyTo: flags['reply-to'], text: flags.caption || '', wait: flags.wait, waitTimeoutMs: flags['wait-timeout'] }), flags.json ? 'json' : 'human') + const request = { chatID, file: flags.file, fileName: flags.filename, mimeType: flags.mime, replyTo: flags['reply-to'], text: flags.caption || '', wait: flags.wait, waitTimeoutMs: flags['wait-timeout'] } + if (flags['dry-run']) { + await printDryRun('send.file', request, flags.json ? 'json' : 'human') + return + } + await printData(await sendMessage(client, request), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/send/react.ts b/packages/cli/src/commands/send/react.ts index af3fde14..c4d5cea5 100644 --- a/packages/cli/src/commands/send/react.ts +++ b/packages/cli/src/commands/send/react.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class SendReact extends BeeperCommand { @@ -18,6 +18,11 @@ export default class SendReact extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) + const request = { chatID, messageID: flags.id, reactionKey: flags.reaction, transactionID: flags.transaction } + if (flags['dry-run']) { + await printDryRun('send.react', request, flags.json ? 'json' : 'human') + return + } await printData( await client.chats.messages.reactions.add(flags.id, { chatID, reactionKey: flags.reaction, transactionID: flags.transaction }), flags.json ? 'json' : 'human', diff --git a/packages/cli/src/commands/send/sticker.ts b/packages/cli/src/commands/send/sticker.ts index 3fb53bb2..284c8de0 100644 --- a/packages/cli/src/commands/send/sticker.ts +++ b/packages/cli/src/commands/send/sticker.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' import { sendMessage } from '../../lib/send-message.js' @@ -23,18 +23,23 @@ export default class SendSticker extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) + const request = { + chatID, + file: flags.file, + fileName: flags.filename, + mimeType: flags.mime, + replyTo: flags['reply-to'], + text: '', + attachmentType: 'sticker' as const, + wait: flags.wait, + waitTimeoutMs: flags['wait-timeout'], + } + if (flags['dry-run']) { + await printDryRun('send.sticker', request, flags.json ? 'json' : 'human') + return + } await printData( - await sendMessage(client, { - chatID, - file: flags.file, - fileName: flags.filename, - mimeType: flags.mime, - replyTo: flags['reply-to'], - text: '', - attachmentType: 'sticker', - wait: flags.wait, - waitTimeoutMs: flags['wait-timeout'], - }), + await sendMessage(client, request), flags.json ? 'json' : 'human', ) } diff --git a/packages/cli/src/commands/send/text.ts b/packages/cli/src/commands/send/text.ts index 42fdacfe..d5bffad8 100644 --- a/packages/cli/src/commands/send/text.ts +++ b/packages/cli/src/commands/send/text.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' import { sendMessage } from '../../lib/send-message.js' @@ -28,6 +28,11 @@ export default class SendText extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) - await printData(await sendMessage(client, { chatID, text: flags.message, replyTo: flags['reply-to'], mentions: flags.mention, noPreview: flags['no-preview'], wait: flags.wait, waitTimeoutMs: flags['wait-timeout'] }), flags.json ? 'json' : 'human') + const request = { chatID, text: flags.message, replyTo: flags['reply-to'], mentions: flags.mention, noPreview: flags['no-preview'], wait: flags.wait, waitTimeoutMs: flags['wait-timeout'] } + if (flags['dry-run']) { + await printDryRun('send.text', request, flags.json ? 'json' : 'human') + return + } + await printData(await sendMessage(client, request), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/send/unreact.ts b/packages/cli/src/commands/send/unreact.ts index b49bf7c9..c1494ea6 100644 --- a/packages/cli/src/commands/send/unreact.ts +++ b/packages/cli/src/commands/send/unreact.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class SendUnreact extends BeeperCommand { @@ -19,6 +19,11 @@ export default class SendUnreact extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) + const request = { chatID, messageID: flags.id, reactionKey: flags.reaction } + if (flags['dry-run']) { + await printDryRun('send.unreact', request, flags.json ? 'json' : 'human') + return + } await printData( await client.chats.messages.reactions.delete(flags.reaction, { chatID, messageID: flags.id }), flags.json ? 'json' : 'human', diff --git a/packages/cli/src/commands/send/voice.ts b/packages/cli/src/commands/send/voice.ts index 5c831b72..3682957a 100644 --- a/packages/cli/src/commands/send/voice.ts +++ b/packages/cli/src/commands/send/voice.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' import { sendMessage } from '../../lib/send-message.js' @@ -24,19 +24,24 @@ export default class SendVoice extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) + const request = { + chatID, + file: flags.file, + fileName: flags.filename, + mimeType: flags.mime, + replyTo: flags['reply-to'], + text: '', + attachmentType: 'voice-note' as const, + duration: flags.duration, + wait: flags.wait, + waitTimeoutMs: flags['wait-timeout'], + } + if (flags['dry-run']) { + await printDryRun('send.voice', request, flags.json ? 'json' : 'human') + return + } await printData( - await sendMessage(client, { - chatID, - file: flags.file, - fileName: flags.filename, - mimeType: flags.mime, - replyTo: flags['reply-to'], - text: '', - attachmentType: 'voice-note', - duration: flags.duration, - wait: flags.wait, - waitTimeoutMs: flags['wait-timeout'], - }), + await sendMessage(client, request), flags.json ? 'json' : 'human', ) } diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 96c06787..f9a8c864 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -23,7 +23,7 @@ import { type AuthSource, type Target, } from '../lib/targets.js' -import { printData, printSuccess } from '../lib/output.js' +import { printData, printDryRun, printSuccess } from '../lib/output.js' export default class Setup extends BeeperCommand { static override summary = 'Make the selected target ready for messaging' @@ -58,6 +58,22 @@ export default class Setup extends BeeperCommand { if ((flags.local || flags.oauth) && (flags.remote || flags.server || flags.desktop)) { throw new Error('Use --local or --oauth with an existing target, not with --remote, --server, or --desktop.') } + if (flags['dry-run']) { + await printDryRun('setup', { + target: flags.target, + baseURL: flags['base-url'], + targetMode: flags.remote ? 'remote' : flags.server ? 'server' : flags.desktop ? 'desktop' : 'selected', + authMode: flags.local ? 'local' : flags.oauth ? 'oauth' : flags.email ? 'email' : 'auto', + remote: flags.remote, + install: flags.install, + channel: flags.channel, + serverEnv: flags['server-env'], + email: flags.email, + username: flags.username, + yes: flags.yes, + }, flags.json ? 'json' : 'human') + return + } if (flags.events) writeEvent('setup_step', { step: 'start', target: flags.target }) if (flags.remote) { diff --git a/packages/cli/src/commands/targets/add/desktop.ts b/packages/cli/src/commands/targets/add/desktop.ts index 34af8af4..06d1e4c2 100644 --- a/packages/cli/src/commands/targets/add/desktop.ts +++ b/packages/cli/src/commands/targets/add/desktop.ts @@ -1,7 +1,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { createProfileTarget, readTarget, updateConfig } from '../../../lib/targets.js' -import { printSuccess } from '../../../lib/output.js' +import { printDryRun, printSuccess } from '../../../lib/output.js' export default class TargetsAddDesktop extends BeeperCommand { static override summary = 'Add a managed Beeper Desktop target' @@ -16,6 +16,10 @@ export default class TargetsAddDesktop extends BeeperCommand { ensureWritable(flags) const id = args.name ?? 'desktop' if (await readTarget(id)) throw new Error(`Target "${id}" already exists.`) + if (flags['dry-run']) { + await printDryRun('targets.add.desktop', { id, type: 'desktop', serverEnv: flags['server-env'], port: flags.port, default: flags.default }, flags.json ? 'json' : 'human') + return + } const target = await createProfileTarget('desktop', id, { serverEnv: flags['server-env'], port: flags.port }) if (flags.default) await updateConfig(config => ({ ...config, defaultTarget: target.id })) await printSuccess({ message: `Added target: ${target.id}`, detail: target.baseURL, data: target }, flags.json ? 'json' : 'human') diff --git a/packages/cli/src/commands/targets/add/remote.ts b/packages/cli/src/commands/targets/add/remote.ts index 1036872f..6c21ddcb 100644 --- a/packages/cli/src/commands/targets/add/remote.ts +++ b/packages/cli/src/commands/targets/add/remote.ts @@ -1,7 +1,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { readTarget, updateConfig, writeTarget, type Target } from '../../../lib/targets.js' -import { printSuccess } from '../../../lib/output.js' +import { printDryRun, printSuccess } from '../../../lib/output.js' export default class TargetsAddRemote extends BeeperCommand { static override summary = 'Add a remote Beeper Desktop or Server target' @@ -17,6 +17,10 @@ export default class TargetsAddRemote extends BeeperCommand { ensureWritable(flags) if (await readTarget(args.name)) throw new Error(`Target "${args.name}" already exists.`) const target: Target = { id: args.name, name: args.name, type: 'remote', baseURL: args.url, managed: false } + if (flags['dry-run']) { + await printDryRun('targets.add.remote', { target, default: flags.default }, flags.json ? 'json' : 'human') + return + } await writeTarget(target) if (flags.default) await updateConfig(config => ({ ...config, defaultTarget: target.id })) await printSuccess({ message: `Added target: ${target.id}`, detail: target.baseURL, data: target }, flags.json ? 'json' : 'human') diff --git a/packages/cli/src/commands/targets/add/server.ts b/packages/cli/src/commands/targets/add/server.ts index b5b10aa2..756125c4 100644 --- a/packages/cli/src/commands/targets/add/server.ts +++ b/packages/cli/src/commands/targets/add/server.ts @@ -1,7 +1,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { createProfileTarget, readTarget, updateConfig } from '../../../lib/targets.js' -import { printSuccess } from '../../../lib/output.js' +import { printDryRun, printSuccess } from '../../../lib/output.js' export default class TargetsAddServer extends BeeperCommand { static override summary = 'Add a managed Beeper Server target' @@ -16,6 +16,10 @@ export default class TargetsAddServer extends BeeperCommand { ensureWritable(flags) const id = args.name ?? 'server' if (await readTarget(id)) throw new Error(`Target "${id}" already exists.`) + if (flags['dry-run']) { + await printDryRun('targets.add.server', { id, type: 'server', serverEnv: flags['server-env'], port: flags.port, default: flags.default }, flags.json ? 'json' : 'human') + return + } const target = await createProfileTarget('server', id, { serverEnv: flags['server-env'], port: flags.port }) if (flags.default) await updateConfig(config => ({ ...config, defaultTarget: target.id })) await printSuccess({ message: `Added target: ${target.id}`, detail: target.baseURL, data: target }, flags.json ? 'json' : 'human') diff --git a/packages/cli/src/commands/targets/disable.ts b/packages/cli/src/commands/targets/disable.ts index e0aa652e..80f61a5a 100644 --- a/packages/cli/src/commands/targets/disable.ts +++ b/packages/cli/src/commands/targets/disable.ts @@ -2,7 +2,7 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { readTarget, resolveTarget } from '../../lib/targets.js' import { assertServerProfile, disableProfile } from '../../lib/profiles.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsDisable extends BeeperCommand { static override summary = 'Disable a local Beeper Server target at login' @@ -13,6 +13,10 @@ export default class TargetsDisable extends BeeperCommand { const target = await resolveTarget({ target: args.name ?? flags.target, baseURL: flags['base-url'] }) if (!target) throw new Error(`Unknown Beeper target "${args.name}".`) assertServerProfile(target) + if (flags['dry-run']) { + await printDryRun('targets.disable', { target }, flags.json ? 'json' : 'human') + return + } const path = await disableProfile(target) await printSuccess({ message: `Disabled target at login: ${target.id}`, detail: path, data: { target, path } }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/targets/enable.ts b/packages/cli/src/commands/targets/enable.ts index a709806d..468f747a 100644 --- a/packages/cli/src/commands/targets/enable.ts +++ b/packages/cli/src/commands/targets/enable.ts @@ -2,7 +2,7 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { readTarget, resolveTarget } from '../../lib/targets.js' import { assertServerProfile, enableProfile } from '../../lib/profiles.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsEnable extends BeeperCommand { static override summary = 'Enable a local Beeper Server target at login' @@ -13,6 +13,10 @@ export default class TargetsEnable extends BeeperCommand { const target = await resolveTarget({ target: args.name ?? flags.target, baseURL: flags['base-url'] }) if (!target) throw new Error(`Unknown Beeper target "${args.name}".`) assertServerProfile(target) + if (flags['dry-run']) { + await printDryRun('targets.enable', { target }, flags.json ? 'json' : 'human') + return + } const path = await enableProfile(target) await printSuccess({ message: `Enabled target at login: ${target.id}`, detail: path, data: { target, path } }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/targets/remove.ts b/packages/cli/src/commands/targets/remove.ts index 099ae16f..5aa2c45b 100644 --- a/packages/cli/src/commands/targets/remove.ts +++ b/packages/cli/src/commands/targets/remove.ts @@ -4,7 +4,7 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createProfileTarget, listTargets, readConfig, readTarget, removeTarget, resolveTarget, updateConfig, writeTarget, type Target } from '../../lib/targets.js' import { disableProfile, enableProfile, profileErrorLogPath, profileLogPath, profileStatus, startProfile, stopProfile } from '../../lib/profiles.js' import { targetLiveStatus } from '../../lib/target-status.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsRemove extends BeeperCommand { static override summary = 'Remove a target' @@ -12,6 +12,10 @@ export default class TargetsRemove extends BeeperCommand { async run(): Promise { const { args, flags } = await this.parse(TargetsRemove) ensureWritable(flags) + if (flags['dry-run']) { + await printDryRun('targets.remove', { id: args.name }, flags.json ? 'json' : 'human') + return + } await removeTarget(args.name) await printSuccess({ message: `Removed target: ${args.name}`, data: { id: args.name } }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/targets/restart.ts b/packages/cli/src/commands/targets/restart.ts index 414e004b..a97ccea5 100644 --- a/packages/cli/src/commands/targets/restart.ts +++ b/packages/cli/src/commands/targets/restart.ts @@ -2,7 +2,7 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { readTarget, resolveTarget } from '../../lib/targets.js' import { assertServerProfile, startProfile, stopProfile } from '../../lib/profiles.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsRestart extends BeeperCommand { static override summary = 'Restart a local Beeper Server target' @@ -13,6 +13,10 @@ export default class TargetsRestart extends BeeperCommand { const target = await resolveTarget({ target: args.name ?? flags.target, baseURL: flags['base-url'] }) if (!target) throw new Error(`Unknown Beeper target "${args.name}".`) assertServerProfile(target) + if (flags['dry-run']) { + await printDryRun('targets.restart', { target }, flags.json ? 'json' : 'human') + return + } await stopProfile(target).catch(() => undefined) const result = await startProfile(target) await printSuccess({ message: `Restarted target: ${target.id}`, detail: target.baseURL, data: { target, result } }, flags.json ? 'json' : 'human') diff --git a/packages/cli/src/commands/targets/start.ts b/packages/cli/src/commands/targets/start.ts index e51ca9b8..c9cd6a5e 100644 --- a/packages/cli/src/commands/targets/start.ts +++ b/packages/cli/src/commands/targets/start.ts @@ -2,7 +2,7 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { customTargetID, readTarget, resolveTarget } from '../../lib/targets.js' import { launchDesktopApp, startProfile } from '../../lib/profiles.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsStart extends BeeperCommand { static override summary = 'Start a local Server target or open Beeper Desktop' @@ -13,6 +13,10 @@ export default class TargetsStart extends BeeperCommand { const target = await resolveTarget({ target: args.name ?? flags.target, baseURL: flags['base-url'] }) if (!target) throw new Error(`Unknown Beeper target "${args.name}".`) if (target.type === 'desktop' && target.id !== customTargetID) { + if (flags['dry-run']) { + await printDryRun('targets.start', { target, launchDesktop: true }, flags.json ? 'json' : 'human') + return + } const result = await launchDesktopApp(target.managed ? target : undefined) await printSuccess({ message: 'Opened Beeper Desktop', detail: target.baseURL, data: { target, result } }, flags.json ? 'json' : 'human') return @@ -20,6 +24,10 @@ export default class TargetsStart extends BeeperCommand { if (!target.managed || target.type !== 'server') { throw new Error(`Target "${target.id}" is not a local Beeper Server install.`) } + if (flags['dry-run']) { + await printDryRun('targets.start', { target, startProfile: true }, flags.json ? 'json' : 'human') + return + } const result = await startProfile(target) await printSuccess({ message: `Started target: ${target.id}`, detail: target.baseURL, data: { target, result } }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/targets/stop.ts b/packages/cli/src/commands/targets/stop.ts index badd49cc..65444ad0 100644 --- a/packages/cli/src/commands/targets/stop.ts +++ b/packages/cli/src/commands/targets/stop.ts @@ -2,7 +2,7 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { readTarget, resolveTarget } from '../../lib/targets.js' import { assertServerProfile, stopProfile } from '../../lib/profiles.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsStop extends BeeperCommand { static override summary = 'Stop a local Beeper Server target' @@ -13,6 +13,10 @@ export default class TargetsStop extends BeeperCommand { const target = await resolveTarget({ target: args.name ?? flags.target, baseURL: flags['base-url'] }) if (!target) throw new Error(`Unknown Beeper target "${args.name}".`) assertServerProfile(target) + if (flags['dry-run']) { + await printDryRun('targets.stop', { target }, flags.json ? 'json' : 'human') + return + } await stopProfile(target) await printSuccess({ message: `Stopped target: ${target.id}`, data: { target } }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/targets/use.ts b/packages/cli/src/commands/targets/use.ts index cf46e5a3..107a03ea 100644 --- a/packages/cli/src/commands/targets/use.ts +++ b/packages/cli/src/commands/targets/use.ts @@ -4,7 +4,7 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createProfileTarget, listTargets, readConfig, readTarget, removeTarget, resolveTarget, updateConfig, writeTarget, type Target } from '../../lib/targets.js' import { disableProfile, enableProfile, profileErrorLogPath, profileLogPath, profileStatus, startProfile, stopProfile } from '../../lib/profiles.js' import { targetLiveStatus } from '../../lib/target-status.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsUse extends BeeperCommand { static override summary = 'Set the default target' @@ -14,6 +14,10 @@ export default class TargetsUse extends BeeperCommand { ensureWritable(flags) const target = await readTarget(args.name) if (!target) throw new Error(`Unknown Beeper target "${args.name}". Run \`beeper targets list\`.`) + if (flags['dry-run']) { + await printDryRun('targets.use', { defaultTarget: target.id, target }, flags.json ? 'json' : 'human') + return + } await updateConfig(config => ({ ...config, defaultTarget: target.id })) await printSuccess({ message: `Using target: ${target.id}`, detail: target.baseURL, data: target }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/update.ts b/packages/cli/src/commands/update.ts index 587fda05..5f613f3b 100644 --- a/packages/cli/src/commands/update.ts +++ b/packages/cli/src/commands/update.ts @@ -9,7 +9,7 @@ import { import { profileStatus, startProfile, stopProfile } from '../lib/profiles.js' import { listTargets } from '../lib/targets.js' import { pathSetupHint } from '../lib/env.js' -import { printData } from '../lib/output.js' +import { printData, printDryRun } from '../lib/output.js' import pkg from '../../package.json' with { type: 'json' } export default class Update extends BeeperCommand { @@ -25,6 +25,10 @@ export default class Update extends BeeperCommand { const { flags } = await this.parse(Update) if (!flags.check) ensureWritable(flags) const selected = flags.cli || flags.desktop || flags.server + if (flags['dry-run'] && !flags.check) { + await printDryRun('update', { cli: !selected || flags.cli, desktop: !selected || flags.desktop, server: !selected || flags.server }, flags.json ? 'json' : 'human') + return + } const installations = await readInstallations() const results: Array> = [] diff --git a/packages/cli/src/commands/verify.ts b/packages/cli/src/commands/verify.ts index a95b8ab1..a8e3dd78 100644 --- a/packages/cli/src/commands/verify.ts +++ b/packages/cli/src/commands/verify.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../lib/command.js' import { driveVerification } from '../lib/app-state.js' -import { printData } from '../lib/output.js' +import { printData, printDryRun } from '../lib/output.js' export default class AuthVerify extends BeeperCommand { static override summary = 'Finish setup verification or verify another device' static override flags = { @@ -10,6 +10,10 @@ export default class AuthVerify extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(AuthVerify) ensureWritable(flags) + if (flags['dry-run']) { + await printDryRun('verify', { baseURL: flags['base-url'], target: flags.target, userID: flags.user, yes: flags.yes }, flags.json ? 'json' : 'human') + return + } await printData(await driveVerification({ baseURL: flags['base-url'], target: flags.target, userID: flags.user, yes: flags.yes }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/approve.ts b/packages/cli/src/commands/verify/approve.ts index 489f8bc4..caf4c6e5 100644 --- a/packages/cli/src/commands/verify/approve.ts +++ b/packages/cli/src/commands/verify/approve.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class AuthVerifyApprove extends BeeperCommand { static override summary = 'Approve a pending device verification request' static override flags = { @@ -11,6 +11,10 @@ export default class AuthVerifyApprove extends BeeperCommand { const { flags } = await this.parse(AuthVerifyApprove) ensureWritable(flags) const client = await createClient(flags) + if (flags['dry-run']) { + await printDryRun('verify.approve', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') + return + } await printData(await client.app.verifications.accept(flags.id ?? 'active'), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/cancel.ts b/packages/cli/src/commands/verify/cancel.ts index f30ddd33..ce8df653 100644 --- a/packages/cli/src/commands/verify/cancel.ts +++ b/packages/cli/src/commands/verify/cancel.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class AuthVerifyCancel extends BeeperCommand { static override summary = 'Cancel an in-progress device verification' static override flags = { @@ -11,6 +11,10 @@ export default class AuthVerifyCancel extends BeeperCommand { const { flags } = await this.parse(AuthVerifyCancel) ensureWritable(flags) const client = await createClient(flags) + if (flags['dry-run']) { + await printDryRun('verify.cancel', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') + return + } await printData(await client.app.verifications.cancel(flags.id ?? 'active', {}), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/qr-confirm.ts b/packages/cli/src/commands/verify/qr-confirm.ts index 0cb190e0..662f4e63 100644 --- a/packages/cli/src/commands/verify/qr-confirm.ts +++ b/packages/cli/src/commands/verify/qr-confirm.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class AuthVerifyQrConfirm extends BeeperCommand { static override summary = 'Confirm that the other device scanned your QR code' static override flags = { @@ -11,6 +11,10 @@ export default class AuthVerifyQrConfirm extends BeeperCommand { const { flags } = await this.parse(AuthVerifyQrConfirm) ensureWritable(flags) const client = await createClient(flags) + if (flags['dry-run']) { + await printDryRun('verify.qr-confirm', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') + return + } await printData(await client.app.verifications.qr.confirmScanned(flags.id ?? 'active'), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/qr-scan.ts b/packages/cli/src/commands/verify/qr-scan.ts index baf39624..553b57c8 100644 --- a/packages/cli/src/commands/verify/qr-scan.ts +++ b/packages/cli/src/commands/verify/qr-scan.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class AuthVerifyQrScan extends BeeperCommand { static override summary = 'Submit a scanned QR-code verification payload' static override flags = { @@ -12,6 +12,10 @@ export default class AuthVerifyQrScan extends BeeperCommand { const { flags } = await this.parse(AuthVerifyQrScan) ensureWritable(flags) const client = await createClient(flags) + if (flags['dry-run']) { + await printDryRun('verify.qr-scan', { id: flags.id ?? 'active', payload: flags.payload }, flags.json ? 'json' : 'human') + return + } await printData(await client.app.verifications.qr.scan({ data: flags.payload }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/recovery-key.ts b/packages/cli/src/commands/verify/recovery-key.ts index 1c13b696..b2ef5e3d 100644 --- a/packages/cli/src/commands/verify/recovery-key.ts +++ b/packages/cli/src/commands/verify/recovery-key.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class AuthVerifyRecoveryKey extends BeeperCommand { static override summary = 'Unlock encrypted messages with a recovery key' static override flags = { @@ -11,6 +11,10 @@ export default class AuthVerifyRecoveryKey extends BeeperCommand { const { flags } = await this.parse(AuthVerifyRecoveryKey) ensureWritable(flags) const client = await createClient(flags) + if (flags['dry-run']) { + await printDryRun('verify.recovery-key', { keyProvided: true }, flags.json ? 'json' : 'human') + return + } await printData(await client.app.login.verification.recoveryKey.verify({ recoveryKey: flags.key }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/reset-recovery-key.ts b/packages/cli/src/commands/verify/reset-recovery-key.ts index f2676d98..bc891026 100644 --- a/packages/cli/src/commands/verify/reset-recovery-key.ts +++ b/packages/cli/src/commands/verify/reset-recovery-key.ts @@ -1,6 +1,6 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { promptYesNoDefaultYes } from '../../lib/app-api.js' export default class AuthVerifyResetRecoveryKey extends BeeperCommand { @@ -10,6 +10,10 @@ export default class AuthVerifyResetRecoveryKey extends BeeperCommand { const { flags } = await this.parse(AuthVerifyResetRecoveryKey) ensureWritable(flags) const client = await createClient(flags) + if (flags['dry-run']) { + await printDryRun('verify.reset-recovery-key', { confirmWithYes: flags.yes }, flags.json ? 'json' : 'human') + return + } const reset = await client.app.login.verification.recoveryKey.reset.create({}) if ((flags.json || !process.stdin.isTTY) && !flags.yes) { diff --git a/packages/cli/src/commands/verify/sas-confirm.ts b/packages/cli/src/commands/verify/sas-confirm.ts index dbd618b0..472b1298 100644 --- a/packages/cli/src/commands/verify/sas-confirm.ts +++ b/packages/cli/src/commands/verify/sas-confirm.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class AuthVerifySasConfirm extends BeeperCommand { static override summary = 'Confirm matching emoji verification' static override flags = { @@ -11,6 +11,10 @@ export default class AuthVerifySasConfirm extends BeeperCommand { const { flags } = await this.parse(AuthVerifySasConfirm) ensureWritable(flags) const client = await createClient(flags) + if (flags['dry-run']) { + await printDryRun('verify.sas-confirm', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') + return + } await printData(await client.app.verifications.sas.confirm(flags.id ?? 'active'), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/sas.ts b/packages/cli/src/commands/verify/sas.ts index d184102e..f116f6c6 100644 --- a/packages/cli/src/commands/verify/sas.ts +++ b/packages/cli/src/commands/verify/sas.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class AuthVerifySas extends BeeperCommand { static override summary = 'Start emoji verification' static override flags = { @@ -11,6 +11,10 @@ export default class AuthVerifySas extends BeeperCommand { const { flags } = await this.parse(AuthVerifySas) ensureWritable(flags) const client = await createClient(flags) + if (flags['dry-run']) { + await printDryRun('verify.sas', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') + return + } await printData(await client.app.verifications.sas.start(flags.id ?? 'active'), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/start.ts b/packages/cli/src/commands/verify/start.ts index 58f58871..66594609 100644 --- a/packages/cli/src/commands/verify/start.ts +++ b/packages/cli/src/commands/verify/start.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class AuthVerifyStart extends BeeperCommand { static override summary = 'Start a device verification request' static override flags = { @@ -11,6 +11,10 @@ export default class AuthVerifyStart extends BeeperCommand { const { flags } = await this.parse(AuthVerifyStart) ensureWritable(flags) const client = await createClient(flags) + if (flags['dry-run']) { + await printDryRun('verify.start', { userID: flags.user }, flags.json ? 'json' : 'human') + return + } await printData(await client.app.verifications.create({ userID: flags.user }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/watch.ts b/packages/cli/src/commands/watch.ts index 38e28d74..e585a010 100644 --- a/packages/cli/src/commands/watch.ts +++ b/packages/cli/src/commands/watch.ts @@ -4,7 +4,7 @@ import WebSocket from 'ws' import { BeeperCommand, writeEvent } from '../lib/command.js' import { requireToken } from '../lib/client.js' import { getBaseURL } from '../lib/targets.js' -import { startStream } from '../lib/output.js' +import { isMachineReadableOutput, startStream } from '../lib/output.js' type WebhookConfig = { url: string; secret?: string; queue: Array<{ body: string; signature?: string }>; inflight: number; max: number } export type EventFilter = { include?: Set; exclude?: Set } @@ -44,7 +44,7 @@ export default class Watch extends BeeperCommand { ? { url: flags.webhook, secret: flags['webhook-secret'], queue: [], inflight: 0, max: flags['webhook-queue'] } : undefined - if (flags.json) { + if (flags.json || isMachineReadableOutput('human')) { await this.runJSON(ws, subscribed, flags.events, webhook, filter) return } diff --git a/packages/cli/src/lib/command-metadata.ts b/packages/cli/src/lib/command-metadata.ts new file mode 100644 index 00000000..69b27dc1 --- /dev/null +++ b/packages/cli/src/lib/command-metadata.ts @@ -0,0 +1,54 @@ +export type CommandMetadata = { + mutates: boolean + requiresAuth: boolean + selectors: string[] + output: 'data' | 'list' | 'stream' | 'success' | 'send-result' | 'manual' | 'schema' + related: string[] +} + +export function metadataForCommand(command: string): CommandMetadata { + const parts = command.split(' ') + const root = parts[0] ?? '' + const mutatingRoots = new Set(['setup', 'install', 'send', 'update', 'export', 'presence']) + const mutatingVerbs = new Set([ + 'add', 'archive', 'unarchive', 'pin', 'unpin', 'mute', 'unmute', 'mark-read', 'mark-unread', + 'priority', 'notify-anyway', 'rename', 'description', 'avatar', 'draft', 'disappear', 'remind', + 'unremind', 'focus', 'edit', 'delete', 'remove', 'use', 'set', 'reset', 'logout', 'start', 'stop', + 'restart', 'enable', 'disable', 'download', 'export', 'post', 'response', 'approve', 'recovery-key', 'reset-recovery-key', 'cancel', 'sas', + 'sas-confirm', 'qr-scan', 'qr-confirm', + ]) + const mutates = command === 'verify' || command === 'api request' || mutatingRoots.has(root) || parts.some(part => mutatingVerbs.has(part ?? '')) + const localOnly = new Set(['config', 'completion', 'docs', 'version', 'man', 'schema']) + const requiresAuth = !localOnly.has(root) && command !== 'targets list' && !command.startsWith('targets add') && !command.startsWith('install ') + const selectors = [ + command.includes('chats ') || command.includes('messages ') || command.startsWith('send ') || command === 'presence' || command.startsWith('resolve chat') ? 'chat' : undefined, + command.includes('accounts ') || command.includes('contacts ') || command === 'chats start' || command.startsWith('resolve account') || command.startsWith('resolve contact') ? 'account' : undefined, + command.includes('targets ') || command === 'status' || command === 'doctor' || command.startsWith('auth ') || command.startsWith('verify') || command.startsWith('resolve target') ? 'target' : undefined, + command.startsWith('bridges ') || command === 'accounts add' || command.startsWith('resolve bridge') ? 'bridge' : undefined, + command.includes('messages ') || command.startsWith('send react') || command.startsWith('send unreact') ? 'message' : undefined, + ].filter((value): value is string => Boolean(value)) + const output = command === 'schema' ? 'schema' + : command.startsWith('send ') ? 'send-result' + : command === 'watch' || command === 'rpc' ? 'stream' + : command === 'man' ? 'manual' + : command.endsWith('list') || command.includes('search') || command === 'bridges list' || command.startsWith('resolve ') ? 'list' + : mutates ? 'success' + : 'data' + const related = relatedForCommand(command) + return { mutates, requiresAuth, selectors, output, related } +} + +function relatedForCommand(command: string): string[] { + if (command.startsWith('send ')) return ['messages list', 'watch'] + if (command.startsWith('messages ')) return ['chats list', 'send text'] + if (command.startsWith('chats ')) return ['messages list', 'send text'] + if (command.startsWith('bridges ')) return ['accounts add', 'accounts list'] + if (command.startsWith('accounts ')) return ['bridges list', 'chats list'] + if (command.startsWith('targets ')) return ['status', 'doctor'] + if (command.startsWith('resolve ')) return ['chats search', 'accounts list', 'targets list', 'bridges list'] + if (command === 'status') return ['doctor', 'setup'] + if (command === 'doctor') return ['status', 'setup'] + if (command === 'schema') return ['man'] + if (command.startsWith('verify')) return ['setup', 'status'] + return [] +} diff --git a/packages/cli/src/lib/command.ts b/packages/cli/src/lib/command.ts index a5cecd4b..08bb4f37 100644 --- a/packages/cli/src/lib/command.ts +++ b/packages/cli/src/lib/command.ts @@ -6,13 +6,19 @@ export abstract class BeeperCommand extends Command { 'base-url': Flags.string({ description: 'Beeper Desktop API base URL (overrides --target)' }), target: Flags.string({ char: 't', description: 'Named Beeper target to use for this command' }), debug: Flags.boolean({ default: false, description: 'Print SDK debug logging on stderr' }), + 'dry-run': Flags.boolean({ default: false, description: 'Do not make changes; print intended actions when supported' }), events: Flags.boolean({ default: false, description: 'Emit NDJSON lifecycle events on stderr (long-running commands)' }), + force: Flags.boolean({ char: 'f', default: false, description: 'Skip confirmations for destructive commands' }), + format: Flags.string({ options: ['json', 'jsonl', 'table', 'text', 'ids'], description: 'Output format. Defaults to json for agents/non-TTY, table for TTY.' }), full: Flags.boolean({ default: false, description: 'Disable text-output truncation; print full IDs and bodies' }), - json: Flags.boolean({ default: false, description: 'Print machine-readable JSON envelope on stdout' }), + json: Flags.boolean({ default: false, description: 'Alias for --format json' }), + 'no-input': Flags.boolean({ default: false, description: 'Never prompt; fail instead (useful for agents and CI)' }), quiet: Flags.boolean({ char: 'q', default: false, description: 'Suppress spinners and success lines (errors still print). Honored with or without --json.' }), 'read-only': Flags.boolean({ default: false, description: 'Reject commands that would modify Beeper or local CLI state (or set BEEPER_READONLY=1)' }), + 'results-only': Flags.boolean({ default: false, description: 'In JSON mode, emit only the primary result instead of the envelope' }), + select: Flags.string({ description: 'In JSON/JSONL mode, project comma-separated fields; dot paths supported' }), timeout: Flags.string({ description: 'Maximum time to wait, such as 30s, 2m, or 1h' }), - yes: Flags.boolean({ char: 'y', default: false, description: 'Skip interactive confirmation prompts' }), + yes: Flags.boolean({ char: 'y', default: false, description: 'Alias for --force' }), } public override async init(): Promise { @@ -20,22 +26,38 @@ export abstract class BeeperCommand extends Command { if (this.argv.includes('--quiet') || this.argv.includes('-q')) { process.env.BEEPER_QUIET = '1' } + const format = outputFormatFromArgv(this.argv) + if (format) { + process.env.BEEPER_OUTPUT_FORMAT = format + } else if (this.argv.includes('--json')) { + process.env.BEEPER_OUTPUT_FORMAT = 'json' + } else if (process.env.BEEPER_AGENT === '1' || !process.stdout.isTTY) { + process.env.BEEPER_OUTPUT_FORMAT = 'json' + } + const select = stringFlagFromArgv(this.argv, '--select') + if (select) process.env.BEEPER_OUTPUT_SELECT = select + if (this.argv.includes('--results-only')) process.env.BEEPER_OUTPUT_RESULTS_ONLY = '1' + if (this.argv.includes('--no-input') || process.env.BEEPER_AGENT === '1') process.env.BEEPER_NO_INPUT = '1' + if (this.argv.includes('--force') || this.argv.includes('-f') || this.argv.includes('--yes') || this.argv.includes('-y')) process.env.BEEPER_FORCE = '1' } protected override async catch(error: Error & { exitCode?: number }): Promise { - const code = error instanceof CLIError ? error.exitCode : error.exitCode ?? ExitCodes.Generic - process.exitCode = process.exitCode ?? code const message = error.message || String(error) + const inferredCode = error instanceof CLIError ? error.exitCode : inferExitCode(message) + const code = inferredCode ?? error.exitCode ?? ExitCodes.Generic + process.exitCode = process.exitCode ?? code const tryMessage = error instanceof CLIError ? error.tryMessage : undefined - const isBug = !(error instanceof CLIError) || error instanceof BugError + const isBug = error instanceof BugError || (!(error instanceof CLIError) && inferredCode === undefined) if (this.argv.includes('--events')) { writeEvent('error', { message, exitCode: code, kind: isBug ? 'bug' : 'abort', tryMessage }) return } - if (this.argv.includes('--json')) { - process.stderr.write(`${JSON.stringify({ success: false, data: null, error: message, exitCode: code, kind: isBug ? 'bug' : 'abort', tryMessage })}\n`) + if (isMachineOutput(this.argv)) { + const errorCodeValue = error instanceof CLIError && error.code ? error.code : errorCode(code, isBug) + const data = error instanceof CLIError ? error.data : undefined + process.stderr.write(`${JSON.stringify({ ok: false, data: data ?? null, error: { code: errorCodeValue, message, exitCode: code, kind: isBug ? 'bug' : 'abort', hint: tryMessage } })}\n`) return } @@ -49,6 +71,14 @@ export abstract class BeeperCommand extends Command { } } +function inferExitCode(message: string): number | undefined { + if (/\b401\b|unauthorized|invalid token|auth(?:entication)? required/i.test(message)) return ExitCodes.AuthRequired + if (/\b404\b|not\s+found|unknown .*target|no .*matches/i.test(message)) return ExitCodes.NotFound + if (/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|fetch failed|not reachable|not ready/i.test(message)) return ExitCodes.NotReady + if (/usage|invalid|must provide|required|unknown flag|parse/i.test(message)) return ExitCodes.Usage + return undefined +} + function formatBugPanel(error: Error, version: string): string { const bar = '─'.repeat(60) const stack = error.stack?.split('\n').slice(0, 8).join('\n') ?? error.message @@ -72,6 +102,10 @@ export function ensureWritable(flags: { 'read-only'?: boolean }): void { if (readOnly) throw new CLIError('read-only mode: command would modify Beeper or local CLI state', ExitCodes.Usage) } +export function ensureNotDryRun(flags: { 'dry-run'?: boolean }, action: string): void { + if (flags['dry-run']) throw new CLIError(`dry-run: ${action}`, ExitCodes.Success) +} + export function writeEvent(event: string, data: Record = {}): void { process.stderr.write(`${JSON.stringify({ event, data, ts: Date.now() })}\n`) } @@ -79,3 +113,47 @@ export function writeEvent(event: string, data: Record = {}): v export function isQuiet(): boolean { return process.env.BEEPER_QUIET === '1' } + +export function isNoInput(): boolean { + return process.env.BEEPER_NO_INPUT === '1' +} + +export function isForce(flags?: { force?: boolean; yes?: boolean }): boolean { + return Boolean(flags?.force || flags?.yes || process.env.BEEPER_FORCE === '1') +} + +function outputFormatFromArgv(argv: string[]): string | undefined { + for (let i = 0; i < argv.length; i++) { + const arg = argv[i] + if (arg === '--format') return argv[i + 1] + if (arg?.startsWith('--format=')) return arg.slice('--format='.length) + } + return undefined +} + +function stringFlagFromArgv(argv: string[], name: string): string | undefined { + for (let i = 0; i < argv.length; i++) { + const arg = argv[i] + if (arg === name) return argv[i + 1] + if (arg?.startsWith(`${name}=`)) return arg.slice(name.length + 1) + } + return undefined +} + +function isMachineOutput(argv: string[]): boolean { + const format = outputFormatFromArgv(argv) ?? process.env.BEEPER_OUTPUT_FORMAT + return argv.includes('--json') || format === 'json' || format === 'jsonl' +} + +function errorCode(code: number, isBug: boolean): string { + if (isBug) return 'internal_error' + switch (code) { + case ExitCodes.Usage: return 'usage_error' + case ExitCodes.AuthRequired: return 'auth_required' + case ExitCodes.NotReady: return 'not_ready' + case ExitCodes.NotFound: return 'not_found' + case ExitCodes.Ambiguous: return 'ambiguous_selector' + case ExitCodes.CommandNotFound: return 'command_not_found' + default: return 'runtime_error' + } +} diff --git a/packages/cli/src/lib/errors.ts b/packages/cli/src/lib/errors.ts index ce198333..894c93a4 100644 --- a/packages/cli/src/lib/errors.ts +++ b/packages/cli/src/lib/errors.ts @@ -25,10 +25,14 @@ export type ExitCode = typeof ExitCodes[keyof typeof ExitCodes] export class CLIError extends Error { readonly exitCode: ExitCode readonly tryMessage?: string - constructor(message: string, exitCode: ExitCode, tryMessage?: string) { + readonly code?: string + readonly data?: Record + constructor(message: string, exitCode: ExitCode, tryMessage?: string, options: { code?: string; data?: Record } = {}) { super(message) this.exitCode = exitCode this.tryMessage = tryMessage + this.code = options.code + this.data = options.data this.name = 'CLIError' } } @@ -38,8 +42,8 @@ export class CLIError extends Error { * Renders as a single-line red message. Do not include a stack trace. */ export class AbortError extends CLIError { - constructor(message: string, exitCode: ExitCode = ExitCodes.Generic, tryMessage?: string) { - super(message, exitCode, tryMessage) + constructor(message: string, exitCode: ExitCode = ExitCodes.Generic, tryMessage?: string, options: { code?: string; data?: Record } = {}) { + super(message, exitCode, tryMessage, options) this.name = 'AbortError' } } @@ -50,7 +54,7 @@ export class AbortError extends CLIError { */ export class BugError extends CLIError { constructor(message: string, tryMessage?: string) { - super(message, ExitCodes.Generic, tryMessage) + super(message, ExitCodes.Generic, tryMessage, { code: 'internal_error' }) this.name = 'BugError' } } @@ -58,5 +62,5 @@ export class BugError extends CLIError { export const usageError = (message: string) => new AbortError(message, ExitCodes.Usage) export const authRequired = (message: string) => new AbortError(message, ExitCodes.AuthRequired) export const notReady = (message: string) => new AbortError(message, ExitCodes.NotReady) -export const notFound = (message: string) => new AbortError(message, ExitCodes.NotFound) -export const ambiguous = (message: string) => new AbortError(message, ExitCodes.Ambiguous) +export const notFound = (message: string, data?: Record) => new AbortError(message, ExitCodes.NotFound, undefined, { code: 'not_found', data }) +export const ambiguous = (message: string, data?: Record) => new AbortError(message, ExitCodes.Ambiguous, 'Pass an exact ID or --pick N.', { code: 'ambiguous_selector', data }) diff --git a/packages/cli/src/lib/manifest.ts b/packages/cli/src/lib/manifest.ts index 5b7dfe63..2d2aff33 100644 --- a/packages/cli/src/lib/manifest.ts +++ b/packages/cli/src/lib/manifest.ts @@ -236,7 +236,8 @@ export const commandManifest: ManifestCommand[] = [ examples: [ 'beeper chats list', 'beeper chats list --pinned --limit 50', - 'beeper chats list --unread --no-muted --json', + 'beeper chats list --unread --no-muted --format json', + 'beeper ls --format ids', ], }, { @@ -370,6 +371,7 @@ export const commandManifest: ManifestCommand[] = [ description: 'Search messages across chats', examples: [ 'beeper messages search invoice', + 'beeper search invoice --format jsonl --select id,chatID,text', 'beeper messages search --chat 10313 --sender me --media image', 'beeper messages search "flight" --after 2026-01-01 --before 2026-02-01', ], @@ -407,6 +409,7 @@ export const commandManifest: ManifestCommand[] = [ command: 'send text', description: 'Send a text message', examples: [ + 'beeper send --to 10313 --message "on my way" --dry-run --format json', 'beeper send text --to 10313 --message "on my way"', 'beeper send text --to 8951 --message "hi"', 'beeper send text --to "Family" --message "hi" --pick 1', @@ -464,6 +467,31 @@ export const commandManifest: ManifestCommand[] = [ description: 'Show contact details', examples: ['beeper contacts show "Alice" --account whatsapp'], }, + { + command: 'resolve chat', + description: 'Resolve a chat selector to concrete chat candidates', + examples: ['beeper resolve chat Family --format json', 'beeper resolve chat Family --pick 1 --results-only'], + }, + { + command: 'resolve account', + description: 'Resolve an account selector', + examples: ['beeper resolve account whatsapp --format json'], + }, + { + command: 'resolve contact', + description: 'Resolve a contact selector', + examples: ['beeper resolve contact Alice --account whatsapp --format json'], + }, + { + command: 'resolve target', + description: 'Resolve a target selector', + examples: ['beeper resolve target desktop --format json'], + }, + { + command: 'resolve bridge', + description: 'Resolve a bridge selector', + examples: ['beeper resolve bridge whatsapp --format json'], + }, { command: 'media download', description: 'Download message media', @@ -495,7 +523,16 @@ export const commandManifest: ManifestCommand[] = [ { command: 'man', description: 'Print the command manual', - examples: ['beeper man', 'beeper man --json'], + examples: ['beeper man', 'beeper man --format json', 'beeper man --format ids'], + }, + { + command: 'schema', + description: 'Print machine-readable command/flag schema', + examples: [ + 'beeper schema', + 'beeper schema send --results-only', + 'beeper schema --select commands.path,commands.flags.name --results-only', + ], }, { command: 'doctor', diff --git a/packages/cli/src/lib/output.ts b/packages/cli/src/lib/output.ts index f697d2d9..724bfdea 100644 --- a/packages/cli/src/lib/output.ts +++ b/packages/cli/src/lib/output.ts @@ -1,22 +1,28 @@ import type { StreamController, Suggestion } from './ink/render.js' -export type OutputFormat = 'human' | 'json' | 'jsonl' +export type OutputFormat = 'human' | 'json' | 'jsonl' | 'table' | 'text' | 'ids' type RecordValue = Record const writeJSON = (value: unknown, format: 'json' | 'jsonl'): void => { process.stdout.write(`${JSON.stringify(value, null, format === 'json' ? 2 : 0)}\n`) } -const envelope = (data: unknown) => ({ success: true, data, error: null }) +const envelope = (data: unknown, meta: Record = {}) => ({ ok: true, data, error: null, meta }) const loadInk = () => import('./ink/render.js') export async function printData(value: unknown, format: OutputFormat): Promise { + format = effectiveFormat(format) + if (format === 'ids') { + printIDs(Array.isArray(value) ? value : [value]) + return + } if (format === 'json') { - writeJSON(envelope(value), 'json') + writeJSON(jsonPayload(value), 'json') return } if (format === 'jsonl') { + value = projectJSON(value) if (Array.isArray(value)) { for (const item of value) process.stdout.write(`${JSON.stringify(item)}\n`) return @@ -24,21 +30,39 @@ export async function printData(value: unknown, format: OutputFormat): Promise, format: OutputFormat): Promise { + await printData({ dryRun: true, action, request }, format) +} + export async function printList( value: unknown[], format: OutputFormat, empty: { title: string; subtitle?: string; suggestions?: Suggestion[] }, ): Promise { + format = effectiveFormat(format) + if (format === 'ids') { + printIDs(value) + return + } if (format === 'json') { - writeJSON(envelope(value), 'json') + writeJSON(jsonPayload(value), 'json') return } if (format === 'jsonl') { - for (const item of value) process.stdout.write(`${JSON.stringify(item)}\n`) + const projected = projectJSON(value) + for (const item of Array.isArray(projected) ? projected : [projected]) process.stdout.write(`${JSON.stringify(item)}\n`) + return + } + if (format === 'text') { + printText(value) return } const { renderList } = await loadInk() @@ -73,8 +97,17 @@ export async function printSuccess( opts: { message: string; detail?: string; entity?: unknown; data?: Record }, format: OutputFormat, ): Promise { + format = effectiveFormat(format) if (format === 'json' || format === 'jsonl') { - writeJSON(envelope({ message: opts.message, detail: opts.detail, entity: opts.entity, ...(opts.data ?? {}) }), format) + writeJSON(jsonPayload({ message: opts.message, detail: opts.detail, entity: opts.entity, ...(opts.data ?? {}) }), format) + return + } + if (format === 'ids') { + printIDs([opts.entity ?? opts.data ?? {}]) + return + } + if (format === 'text') { + process.stdout.write(`${opts.message}${opts.detail ? `\t${opts.detail}` : ''}\n`) return } if (process.env.BEEPER_QUIET === '1') return @@ -86,8 +119,9 @@ export async function printFailure( opts: { message: string; detail?: string; data?: Record }, format: OutputFormat, ): Promise { + format = effectiveFormat(format) if (format === 'json' || format === 'jsonl') { - writeJSON({ success: false, data: opts.data ?? null, error: opts.message }, format) + writeJSON({ ok: false, data: opts.data ?? null, error: { message: opts.message, detail: opts.detail } }, format) return } const { renderFailure } = await loadInk() @@ -95,8 +129,17 @@ export async function printFailure( } export async function printConfig(data: Record, format: OutputFormat): Promise { + format = effectiveFormat(format) if (format === 'json' || format === 'jsonl') { - writeJSON(envelope(data), format) + writeJSON(jsonPayload(data), format) + return + } + if (format === 'ids') { + printIDs([data]) + return + } + if (format === 'text') { + printText(data) return } const { renderConfig } = await loadInk() @@ -108,8 +151,17 @@ export async function printCommands( format: OutputFormat, opts?: { title?: string; intro?: string[] }, ): Promise { + format = effectiveFormat(format) if (format === 'json' || format === 'jsonl') { - writeJSON(envelope(items), format) + writeJSON(jsonPayload(items, opts ? { title: opts.title } : {}), format) + return + } + if (format === 'ids') { + for (const item of items) process.stdout.write(`${item.command}\n`) + return + } + if (format === 'text') { + for (const item of items) process.stdout.write(`${item.command}\t${item.description}\n`) return } const { renderCommands } = await loadInk() @@ -122,3 +174,111 @@ export async function startStream(opts: { baseURL: string; subscribed: string[] } export type { Suggestion } from './ink/render.js' + +export function isMachineReadableOutput(format?: OutputFormat): boolean { + const effective = effectiveFormat(format ?? 'human') + return effective === 'json' || effective === 'jsonl' || effective === 'ids' || effective === 'text' +} + +function effectiveFormat(format: OutputFormat): OutputFormat { + const env = process.env.BEEPER_OUTPUT_FORMAT as OutputFormat | undefined + if (env && ['json', 'jsonl', 'table', 'text', 'ids'].includes(env)) return env === 'table' ? 'human' : env + return format === 'table' ? 'human' : format +} + +function jsonPayload(value: unknown, meta: Record = {}): unknown { + const projected = projectJSON(value) + if (process.env.BEEPER_OUTPUT_RESULTS_ONLY === '1') return unwrapPrimary(projected) + return envelope(projected, meta) +} + +function projectJSON(value: unknown): unknown { + const fields = (process.env.BEEPER_OUTPUT_SELECT ?? '') + .split(',') + .map(item => item.trim()) + .filter(Boolean) + let output = value + if (process.env.BEEPER_OUTPUT_RESULTS_ONLY === '1') output = unwrapPrimary(output) + if (!fields.length) return output + return selectFields(output, fields) +} + +function unwrapPrimary(value: unknown): unknown { + if (!value || typeof value !== 'object' || Array.isArray(value)) return value + const record = value as Record + if ('items' in record) return record.items + if ('results' in record) return record.results + if ('data' in record) return record.data + const metaKeys = new Set(['nextCursor', 'nextPageToken', 'cursor', 'hasMore', 'count', 'query']) + const keys = Object.keys(record).filter(key => !metaKeys.has(key)) + if (keys.length === 1) return record[keys[0]!] + return value +} + +function selectFields(value: unknown, fields: string[]): unknown { + if (Array.isArray(value)) return value.map(item => selectFields(item, fields)) + if (!value || typeof value !== 'object') return value + const out: Record = {} + for (const field of fields) { + const selected = selectPath(value, field.split('.')) + if (selected !== undefined) mergeSelected(out, selected) + } + return out +} + +function selectPath(value: unknown, parts: string[]): unknown { + if (!parts.length) return value + if (Array.isArray(value)) { + const items = value.map(item => selectPath(item, parts)).filter(item => item !== undefined) + return items.length ? items : undefined + } + if (!value || typeof value !== 'object') return undefined + const [part, ...rest] = parts + if (!part) return undefined + const child = (value as Record)[part] + if (child === undefined) return undefined + const selected = selectPath(child, rest) + return selected === undefined ? undefined : { [part]: selected } +} + +function mergeSelected(target: Record, selected: unknown): void { + if (!selected || typeof selected !== 'object' || Array.isArray(selected)) return + for (const [key, value] of Object.entries(selected as Record)) { + const current = target[key] + if (Array.isArray(value)) { + const currentItems = Array.isArray(current) ? current : [] + target[key] = value.map((item, index) => { + const base = currentItems[index] + if (item && typeof item === 'object' && !Array.isArray(item) && base && typeof base === 'object' && !Array.isArray(base)) { + return mergeObjects(base as Record, item as Record) + } + return item + }) + } else if (value && typeof value === 'object' && !Array.isArray(value) && current && typeof current === 'object' && !Array.isArray(current)) { + target[key] = mergeObjects(current as Record, value as Record) + } else { + target[key] = value + } + } +} + +function mergeObjects(left: Record, right: Record): Record { + const out = { ...left } + mergeSelected(out, right) + return out +} + +function printText(value: unknown): void { + if (Array.isArray(value)) { + for (const item of value) printText(item) + return + } + if (!value || typeof value !== 'object') { + if (value !== undefined) process.stdout.write(`${String(value)}\n`) + return + } + for (const [key, item] of Object.entries(value as Record)) { + if (item == null) continue + process.stdout.write(`${key}\t${typeof item === 'object' ? JSON.stringify(item) : String(item)}\n`) + } +} diff --git a/packages/cli/src/lib/resolve.ts b/packages/cli/src/lib/resolve.ts index d83eb3b7..bf25940e 100644 --- a/packages/cli/src/lib/resolve.ts +++ b/packages/cli/src/lib/resolve.ts @@ -31,9 +31,13 @@ export async function resolveAccountIDs( const resolved: string[] = [] for (const input of effectiveInputs) { const matches = matchAccounts(accounts, input) - if (matches.length === 0) throw notFound(`No account matches "${input}"`) + if (matches.length === 0) throw notFound(`No account matches "${input}"`, { selector: input, kind: 'account' }) if (matches.length > 1 && !options.allowMultiplePerInput) { - throw ambiguous(formatAmbiguous(`account "${input}"`, matches.map(formatAccount))) + throw ambiguous(formatAmbiguous(`account "${input}"`, matches.map(formatAccount)), { + selector: input, + kind: 'account', + candidates: matches.map((account, index) => ({ pick: index + 1, id: String(account.accountID ?? account.id), label: formatAccount(account), raw: account })), + }) } resolved.push(...matches.map(account => String(account.accountID))) } @@ -43,7 +47,7 @@ export async function resolveAccountIDs( export async function resolveAccountID(client: any, input: string): Promise { const [accountID] = await resolveAccountIDs(client, [input]) ?? [] - if (!accountID) throw notFound(`No account matches "${input}"`) + if (!accountID) throw notFound(`No account matches "${input}"`, { selector: input, kind: 'account' }) return accountID } @@ -73,20 +77,33 @@ export async function resolveChatID(client: any, input: string, options: ChatRes if (matches.length === 0) { const suggestion = await suggestChat(client, input, options) if (suggestion) return suggestion - return input + throw notFound(`No chat matches "${input}"`, { selector: input, kind: 'chat' }) } if (matches.length === 1) return chatInputID(matches[0]!) if (options.pick) { const selected = matches[options.pick - 1] - if (!selected) throw notFound(`--pick ${options.pick} is outside the ${matches.length} matching chats`) + if (!selected) throw notFound(`--pick ${options.pick} is outside the ${matches.length} matching chats`, { selector: input, kind: 'chat', pick: options.pick, count: matches.length }) return chatInputID(selected) } - throw ambiguous(formatAmbiguous(`chat "${input}"`, matches.map(formatChat))) + throw ambiguous(formatAmbiguous(`chat "${input}"`, matches.map(formatChat)), { + selector: input, + kind: 'chat', + candidates: matches.map((chat, index) => ({ + pick: index + 1, + id: String(chat.id), + localChatID: chat.localChatID ? String(chat.localChatID) : undefined, + title: chat.title ? String(chat.title) : undefined, + network: chat.network ? String(chat.network) : undefined, + label: formatChat(chat), + raw: chat, + })), + }) } async function suggestChat(client: any, input: string, options: ChatResolutionOptions): Promise { + if (process.env.BEEPER_NO_INPUT === '1') return undefined let pool: AnyRecord[] try { pool = await collect(client.chats.list({ accountIDs: options.accountIDs, limit: 100 }), 100) diff --git a/packages/cli/test/cli-smoke.ts b/packages/cli/test/cli-smoke.ts index 1dd76d51..58a945ec 100644 --- a/packages/cli/test/cli-smoke.ts +++ b/packages/cli/test/cli-smoke.ts @@ -106,11 +106,17 @@ const expectedCommands = [ 'contacts list', 'contacts search', 'contacts show', + 'resolve chat', + 'resolve account', + 'resolve contact', + 'resolve target', + 'resolve bridge', 'media download', 'export', 'watch', 'rpc', 'man', + 'schema', 'doctor', 'status', 'docs', @@ -176,12 +182,12 @@ assert.match(setupHelp, /--email/, 'setup should expose email setup start') assert.doesNotMatch(setupHelp, /--code|--accept-terms/, 'setup must not accept OTP or terms flags in the first command') const man = JSON.parse(ok('man', '--json')) -assert.equal(man.success, true) +assert.equal(man.ok, true) assert.equal(man.error, null) assert.deepEqual(man.data.map(item => item.command), expectedCommands) const availablePlugins = JSON.parse(ok('plugins', 'available', '--json')) -assert.equal(availablePlugins.success, true) +assert.equal(availablePlugins.ok, true) assert.equal(availablePlugins.data[0].name, '@beeper/cli-plugin-cloudflare') assert.equal(availablePlugins.data[0].status, 'not installed') assert.deepEqual(availablePlugins.data[0].commands, ['targets tunnel']) @@ -199,41 +205,41 @@ rmSync(configDir, { recursive: true, force: true }) let result = run('targets', 'add', 'remote', 'work', 'http://127.0.0.1:23373', '--default', '--json') assert.equal(result.status, 0, result.stderr) let envelope = JSON.parse(result.stdout) -assert.equal(envelope.success, true) +assert.equal(envelope.ok, true) assert.equal(envelope.data.id, 'work') assert.equal(envelope.data.type, 'remote') result = run('targets', 'list', '--json') assert.equal(result.status, 0, result.stderr) envelope = JSON.parse(result.stdout) -assert.equal(envelope.success, true) +assert.equal(envelope.ok, true) assert(envelope.data.some(item => item.id === 'work' && item.default)) result = run('auth', 'status', '--json') assert.equal(result.status, 0, result.stderr) envelope = JSON.parse(result.stdout) -assert.equal(envelope.success, true) +assert.equal(envelope.ok, true) assert.equal(envelope.data.authenticated, false) assert.equal(envelope.data.target, 'work') result = run('send', 'text', '--to', 'family', '--message', 'on my way', '--read-only', '--json') assert.notEqual(result.status, 0) envelope = JSON.parse(result.stderr) -assert.equal(envelope.success, false) -assert.match(envelope.error, /read-only mode/) +assert.equal(envelope.ok, false) +assert.match(envelope.error.message, /read-only mode/) result = run('setup', '--remote', 'http://127.0.0.1:9', '--target', 'email-remote', '--email', 'staging-user-123456@example.invalid', '--json') assert.notEqual(result.status, 0) envelope = JSON.parse(result.stderr) -assert.equal(envelope.success, false) -assert.match(envelope.error, /auth email start/) -assert.doesNotMatch(envelope.error, /--code|OTP/i, 'setup must direct automation to the two-step email commands without accepting OTP itself') +assert.equal(envelope.ok, false) +assert.match(envelope.error.message, /auth email start/) +assert.doesNotMatch(envelope.error.message, /--code|OTP/i, 'setup must direct automation to the two-step email commands without accepting OTP itself') result = run('targets', 'show', 'email-remote', '--json') assert.notEqual(result.status, 0) envelope = JSON.parse(result.stderr) -assert.equal(envelope.success, false) -assert.match(envelope.error, /Unknown Beeper target/) +assert.equal(envelope.ok, false) +assert.match(envelope.error.message, /Unknown Beeper target/) rmSync(configDir, { recursive: true, force: true }) const fakeServerPath = join(configDir, 'bin', 'beeper-server') @@ -256,7 +262,7 @@ writeFileSync(join(configDir, 'installations.json'), `${JSON.stringify({ result = run('setup', '--json') assert.equal(result.status, 0, result.stderr) envelope = JSON.parse(result.stdout) -assert.equal(envelope.success, true) +assert.equal(envelope.ok, true) assert(envelope.data.availableActions.some(action => action.id === 'use-installed-server' && action.command === 'beeper setup --server --yes')) assert(!envelope.data.availableActions.some(action => action.id === 'install-server'), 'setup must not offer to reinstall an already installed Server') @@ -273,7 +279,7 @@ assert.equal(rpcResult.status, 0, rpcResult.stderr) const rpcLine = JSON.parse(rpcResult.stdout) assert.equal(rpcLine.id, 1) assert.equal(rpcLine.ok, true) -assert.match(rpcLine.stdout, /"success": true/) +assert.match(rpcLine.stdout, /"ok": true/) const stagingServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'staging', channel: 'stable', platform: 'darwin', arch: 'arm64' }) assert.equal(stagingServerRequest.channel, 'nightly') @@ -314,6 +320,7 @@ assert.equal(await resolveChatID(fakeClient, '10313'), '10313') assert.equal(await resolveChatID(fakeClient, 'Family Work'), '8951') assert.equal(await resolveChatID(fakeClient, 'fam', { pick: 2 }), '8951') await assert.rejects(() => resolveChatID(fakeClient, 'fam'), /Ambiguous chat/) +await assert.rejects(() => resolveChatID(fakeClient, 'missing'), /No chat matches/) function listCommandFiles(dir) { const output = [] diff --git a/packages/cli/test/messages-search-validation.test.ts b/packages/cli/test/messages-search-validation.test.ts index ad053898..bb4c2117 100644 --- a/packages/cli/test/messages-search-validation.test.ts +++ b/packages/cli/test/messages-search-validation.test.ts @@ -17,9 +17,9 @@ describe('messages search query-or-filter requirement', () => { const result = run('messages', 'search', '--json') expect(result.status).toBe(2) const envelope = JSON.parse(result.stderr) - expect(envelope.success).toBe(false) - expect(envelope.exitCode).toBe(2) - expect(envelope.error).toMatch(/Provide a search query or at least one filter flag/) + expect(envelope.ok).toBe(false) + expect(envelope.error.exitCode).toBe(2) + expect(envelope.error.message).toMatch(/Provide a search query or at least one filter flag/) }) it('accepts a bare query', () => { From b8fa69a5cf5bafee0a53a092bf735378ba908aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:07:50 +0200 Subject: [PATCH 03/22] wip --- packages/cli/README.md | 8 ++++---- packages/cli/src/commands/install/server.ts | 2 +- packages/cli/src/commands/setup.ts | 2 +- .../cli/src/commands/targets/add/desktop.ts | 2 +- .../cli/src/commands/targets/add/server.ts | 2 +- packages/cli/src/lib/installations.ts | 12 ++++------- packages/cli/test/cli-smoke.ts | 20 +++++++++++++++---- 7 files changed, 28 insertions(+), 20 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index 3173f930..19ee4216 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -449,7 +449,7 @@ Flags: | `--oauth` | boolean | Authorize the target with browser OAuth/PKCE | | `--remote=` | option | Connect to a remote Beeper Desktop or Server URL | | `--server` | boolean | Set up a local Beeper Server target | -| `--server-env=` | option | Server environment. Staging forces nightly. Default: production | +| `--server-env=` | option | Server feed environment Default: production | | `--username=` | option | Username to use if setup creates a new account | Examples: @@ -498,7 +498,7 @@ Flags: | Flag | Type | Description | | --- | --- | --- | | `--channel=` | option | Server release channel Default: stable | -| `--server-env=` | option | Server environment. Staging forces nightly. Default: production | +| `--server-env=` | option | Server feed environment Default: production | Examples: @@ -591,7 +591,7 @@ Flags: | --- | --- | --- | | `--default` | boolean | Set this target as the default after creation | | `--port=` | option | TCP port the managed Desktop will expose its API on | -| `--server-env=` | option | Server environment. Staging forces nightly. Default: production | +| `--server-env=` | option | Server feed environment Default: production | Examples: @@ -620,7 +620,7 @@ Flags: | --- | --- | --- | | `--default` | boolean | Set this target as the default after creation | | `--port=` | option | TCP port the managed Server will expose its API on | -| `--server-env=` | option | Server environment. Staging forces nightly. Default: production | +| `--server-env=` | option | Server feed environment Default: production | Examples: diff --git a/packages/cli/src/commands/install/server.ts b/packages/cli/src/commands/install/server.ts index a2493a83..c109f711 100644 --- a/packages/cli/src/commands/install/server.ts +++ b/packages/cli/src/commands/install/server.ts @@ -8,7 +8,7 @@ export default class SetupInstallServer extends BeeperCommand { static override summary = 'Install Beeper Server locally' static override flags = { channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Server release channel' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server environment. Staging forces nightly.' }), + 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server feed environment' }), } async run(): Promise { diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index f9a8c864..a0508bdf 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -35,7 +35,7 @@ export default class Setup extends BeeperCommand { desktop: Flags.boolean({ default: false, description: 'Set up a local Beeper Desktop target' }), install: Flags.boolean({ default: false, description: 'Allow installing missing managed runtime' }), channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Install release channel' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server environment. Staging forces nightly.' }), + 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server feed environment' }), email: Flags.string({ description: 'Sign in with an email address' }), username: Flags.string({ description: 'Username to use if setup creates a new account' }), } diff --git a/packages/cli/src/commands/targets/add/desktop.ts b/packages/cli/src/commands/targets/add/desktop.ts index 06d1e4c2..931f8c2f 100644 --- a/packages/cli/src/commands/targets/add/desktop.ts +++ b/packages/cli/src/commands/targets/add/desktop.ts @@ -9,7 +9,7 @@ export default class TargetsAddDesktop extends BeeperCommand { static override flags = { port: Flags.integer({ description: 'TCP port the managed Desktop will expose its API on' }), default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server environment. Staging forces nightly.' }), + 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server feed environment' }), } async run(): Promise { const { args, flags } = await this.parse(TargetsAddDesktop) diff --git a/packages/cli/src/commands/targets/add/server.ts b/packages/cli/src/commands/targets/add/server.ts index 756125c4..435a76f2 100644 --- a/packages/cli/src/commands/targets/add/server.ts +++ b/packages/cli/src/commands/targets/add/server.ts @@ -9,7 +9,7 @@ export default class TargetsAddServer extends BeeperCommand { static override flags = { port: Flags.integer({ description: 'TCP port the managed Server will expose its API on' }), default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server environment. Staging forces nightly.' }), + 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server feed environment' }), } async run(): Promise { const { args, flags } = await this.parse(TargetsAddServer) diff --git a/packages/cli/src/lib/installations.ts b/packages/cli/src/lib/installations.ts index 19abe049..3ad7338a 100644 --- a/packages/cli/src/lib/installations.ts +++ b/packages/cli/src/lib/installations.ts @@ -87,11 +87,8 @@ export function normalizeInstallRequest(options: { bundleID: string apiBaseURL: string } { - // TODO: switch Server installs back to production once the production download - // endpoint returns a beeper-server artifact instead of the Desktop app bundle. - const serverEnv = options.kind === 'server' ? 'staging' : normalizeServerEnv(options.serverEnv) - let channel = options.channel ?? 'stable' - if (serverEnv === 'staging') channel = 'nightly' + const serverEnv = normalizeServerEnv(options.serverEnv) + const channel = options.channel ?? 'stable' const platform = normalizeDownloadPlatform(options.platform ?? process.platform) const feedPlatform = normalizeFeedPlatform(options.platform ?? process.platform) const arch = normalizeArch(options.arch ?? process.arch) @@ -104,7 +101,7 @@ export function normalizeInstallRequest(options: { feedPlatform, arch, bundleID, - apiBaseURL: options.kind === 'server' || serverEnv === 'staging' ? 'https://api.beeper-staging.com' : 'https://api.beeper.com', + apiBaseURL: serverEnv === 'staging' ? 'https://api.beeper-staging.com' : 'https://api.beeper.com', } } @@ -118,8 +115,7 @@ export function feedURLFor(options: ReturnType): } export function downloadURLFor(options: ReturnType): string { - const channelSegment = options.serverEnv === 'staging' && options.kind === 'server' ? 'stable' : options.channel - return `${options.apiBaseURL}/desktop/download/${options.platform}/${options.arch}/${channelSegment}/${options.bundleID}` + return `${options.apiBaseURL}/desktop/download/${options.platform}/${options.arch}/${options.channel}/${options.bundleID}` } export async function fetchFeed(feedURL: string): Promise { diff --git a/packages/cli/test/cli-smoke.ts b/packages/cli/test/cli-smoke.ts index 58a945ec..1f5828d6 100644 --- a/packages/cli/test/cli-smoke.ts +++ b/packages/cli/test/cli-smoke.ts @@ -282,10 +282,22 @@ assert.equal(rpcLine.ok, true) assert.match(rpcLine.stdout, /"ok": true/) const stagingServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'staging', channel: 'stable', platform: 'darwin', arch: 'arm64' }) -assert.equal(stagingServerRequest.channel, 'nightly') -assert.equal(stagingServerRequest.bundleID, 'com.automattic.beeper.server.nightly') -assert.equal(feedURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server.nightly&platform=darwin&channel=nightly&arch=arm64') -assert.equal(downloadURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server.nightly') +assert.equal(stagingServerRequest.channel, 'stable') +assert.equal(stagingServerRequest.bundleID, 'com.automattic.beeper.server') +assert.equal(feedURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') +assert.equal(downloadURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server') + +const productionServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'production', channel: 'stable', platform: 'darwin', arch: 'arm64' }) +assert.equal(productionServerRequest.channel, 'stable') +assert.equal(productionServerRequest.bundleID, 'com.automattic.beeper.server') +assert.equal(feedURLFor(productionServerRequest), 'https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') +assert.equal(downloadURLFor(productionServerRequest), 'https://api.beeper.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server') + +const stagingNightlyServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'staging', channel: 'nightly', platform: 'darwin', arch: 'arm64' }) +assert.equal(stagingNightlyServerRequest.channel, 'nightly') +assert.equal(stagingNightlyServerRequest.bundleID, 'com.automattic.beeper.server.nightly') +assert.equal(feedURLFor(stagingNightlyServerRequest), 'https://api.beeper-staging.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server.nightly&platform=darwin&channel=nightly&arch=arm64') +assert.equal(downloadURLFor(stagingNightlyServerRequest), 'https://api.beeper-staging.com/desktop/download/macos/arm64/nightly/com.automattic.beeper.server.nightly') const desktopNightlyRequest = normalizeInstallRequest({ kind: 'desktop', channel: 'nightly', platform: 'darwin', arch: 'arm64' }) assert.equal(downloadURLFor(desktopNightlyRequest), 'https://api.beeper.com/desktop/download/macos/arm64/nightly/com.automattic.beeper.desktop.nightly') From 0ec1a225a70529fd69a24ef0ea879d96602a4ca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:13:38 +0200 Subject: [PATCH 04/22] wip --- docs/src/content/docs/targets.mdx | 6 ++--- packages/cli/README.md | 10 ++++---- packages/cli/src/commands/install/server.ts | 3 ++- packages/cli/src/commands/setup.ts | 12 ++++++---- .../cli/src/commands/targets/add/desktop.ts | 3 ++- .../cli/src/commands/targets/add/server.ts | 3 ++- packages/cli/src/lib/installations.ts | 14 ++++------- packages/cli/src/lib/manifest.ts | 2 +- packages/cli/src/lib/server-env.ts | 16 +++++++++++++ packages/cli/src/lib/targets.ts | 3 ++- packages/cli/test/cli-smoke.ts | 23 ++++++++++++++----- 11 files changed, 62 insertions(+), 33 deletions(-) create mode 100644 packages/cli/src/lib/server-env.ts diff --git a/docs/src/content/docs/targets.mdx b/docs/src/content/docs/targets.mdx index 5349538d..64c20cc9 100644 --- a/docs/src/content/docs/targets.mdx +++ b/docs/src/content/docs/targets.mdx @@ -18,8 +18,8 @@ commands use it unless `--target ` overrides. ```sh beeper targets list -beeper targets add desktop [name] [--port N] [--server-env production|staging] [--default] -beeper targets add server [name] [--port N] [--server-env production|staging] [--default] +beeper targets add desktop [name] [--port N] [--server-env local|dev|staging|prod] [--default] +beeper targets add server [name] [--port N] [--server-env local|dev|staging|prod] [--default] beeper targets add remote [--default] beeper targets use beeper targets show [name] @@ -48,7 +48,7 @@ beeper targets remove ```sh beeper targets list --json beeper targets add desktop work --default -beeper targets add server prod --server-env production --default +beeper targets add server prod --server-env prod --default beeper targets add remote office https://desktop.office.example.com --default beeper targets use work beeper targets logs work | less diff --git a/packages/cli/README.md b/packages/cli/README.md index 19ee4216..5cc332b9 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -449,7 +449,7 @@ Flags: | `--oauth` | boolean | Authorize the target with browser OAuth/PKCE | | `--remote=` | option | Connect to a remote Beeper Desktop or Server URL | | `--server` | boolean | Set up a local Beeper Server target | -| `--server-env=` | option | Server feed environment Default: production | +| `--server-env=` | option | Server feed environment Default: prod | | `--username=` | option | Username to use if setup creates a new account | Examples: @@ -498,7 +498,7 @@ Flags: | Flag | Type | Description | | --- | --- | --- | | `--channel=` | option | Server release channel Default: stable | -| `--server-env=` | option | Server feed environment Default: production | +| `--server-env=` | option | Server feed environment Default: prod | Examples: @@ -591,7 +591,7 @@ Flags: | --- | --- | --- | | `--default` | boolean | Set this target as the default after creation | | `--port=` | option | TCP port the managed Desktop will expose its API on | -| `--server-env=` | option | Server feed environment Default: production | +| `--server-env=` | option | Server feed environment Default: prod | Examples: @@ -620,12 +620,12 @@ Flags: | --- | --- | --- | | `--default` | boolean | Set this target as the default after creation | | `--port=` | option | TCP port the managed Server will expose its API on | -| `--server-env=` | option | Server feed environment Default: production | +| `--server-env=` | option | Server feed environment Default: prod | Examples: ```sh -beeper targets add server prod --server-env production --default +beeper targets add server prod --server-env prod --default ``` Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. diff --git a/packages/cli/src/commands/install/server.ts b/packages/cli/src/commands/install/server.ts index c109f711..f417165a 100644 --- a/packages/cli/src/commands/install/server.ts +++ b/packages/cli/src/commands/install/server.ts @@ -3,12 +3,13 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { installServer, type InstallChannel } from '../../lib/installations.js' import { pathSetupHint } from '../../lib/env.js' import { printDryRun, printSuccess } from '../../lib/output.js' +import { SERVER_ENVIRONMENTS } from '../../lib/server-env.js' export default class SetupInstallServer extends BeeperCommand { static override summary = 'Install Beeper Server locally' static override flags = { channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Server release channel' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server feed environment' }), + 'server-env': Flags.string({ options: SERVER_ENVIRONMENTS, default: 'prod', description: 'Server feed environment' }), } async run(): Promise { diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index a0508bdf..cc2243a7 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -9,6 +9,7 @@ import { loginWithPKCE } from '../lib/oauth.js' import { findDesktopAppPath, launchDesktopApp, startProfile } from '../lib/profiles.js' import { interactiveEmailSetup } from '../lib/setup-login.js' import { renderStartupLogo } from '../lib/logo.js' +import { SERVER_ENVIRONMENTS, SERVER_ENV_API_BASE_URLS, normalizeServerEnv } from '../lib/server-env.js' import { builtInDesktopTargetID, createProfileTarget, @@ -35,7 +36,7 @@ export default class Setup extends BeeperCommand { desktop: Flags.boolean({ default: false, description: 'Set up a local Beeper Desktop target' }), install: Flags.boolean({ default: false, description: 'Allow installing missing managed runtime' }), channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Install release channel' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server feed environment' }), + 'server-env': Flags.string({ options: SERVER_ENVIRONMENTS, default: 'prod', description: 'Server feed environment' }), email: Flags.string({ description: 'Sign in with an email address' }), username: Flags.string({ description: 'Username to use if setup creates a new account' }), } @@ -285,7 +286,7 @@ export default class Setup extends BeeperCommand { if (choice === '2') { if (!serverInstalled) { if (!await promptYesNoDefaultYes('Install local Beeper Server stable from beeper.com?')) return - await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'production' }) + await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'prod' }) } await this.setupManaged('server', { ...flags, install: false, server: true, channel: 'stable' }) return @@ -314,7 +315,7 @@ export default class Setup extends BeeperCommand { if (choice === '3') { if (!serverInstalled) { if (!await promptYesNoDefaultYes('Install local Beeper Server stable from beeper.com?')) return true - await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'production' }) + await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'prod' }) } await this.setupManaged('server', { ...flags, install: false, server: true, channel: 'stable' }) return true @@ -619,8 +620,9 @@ async function maybeDriveOnboarding(result: SetupResult, flags: SetupFlags): Pro async function installWithCopy(type: 'desktop' | 'server', flags: SetupFlags): Promise { const label = type === 'desktop' ? 'Beeper Desktop' : 'local Beeper Server' const channel = flags.channel === 'nightly' ? 'nightly' : 'stable' - const serverEnv = flags['server-env'] === 'staging' ? 'staging' : 'production' - if (!flags.json && process.stdin.isTTY) process.stdout.write(`Installing ${label} ${channel} from beeper.com...\n`) + const serverEnv = normalizeServerEnv(flags['server-env']) + const source = type === 'server' ? new URL(SERVER_ENV_API_BASE_URLS[serverEnv]).host : 'beeper.com' + if (!flags.json && process.stdin.isTTY) process.stdout.write(`Installing ${label} ${channel} from ${source}...\n`) if (type === 'desktop') await installDesktop({ channel, serverEnv }) else await installServer({ channel, serverEnv }) if (!flags.json && process.stdin.isTTY) process.stdout.write(`Installed ${label} ${channel}.\n\n`) diff --git a/packages/cli/src/commands/targets/add/desktop.ts b/packages/cli/src/commands/targets/add/desktop.ts index 931f8c2f..91a8f6cc 100644 --- a/packages/cli/src/commands/targets/add/desktop.ts +++ b/packages/cli/src/commands/targets/add/desktop.ts @@ -2,6 +2,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { createProfileTarget, readTarget, updateConfig } from '../../../lib/targets.js' import { printDryRun, printSuccess } from '../../../lib/output.js' +import { SERVER_ENVIRONMENTS } from '../../../lib/server-env.js' export default class TargetsAddDesktop extends BeeperCommand { static override summary = 'Add a managed Beeper Desktop target' @@ -9,7 +10,7 @@ export default class TargetsAddDesktop extends BeeperCommand { static override flags = { port: Flags.integer({ description: 'TCP port the managed Desktop will expose its API on' }), default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server feed environment' }), + 'server-env': Flags.string({ options: SERVER_ENVIRONMENTS, default: 'prod', description: 'Server feed environment' }), } async run(): Promise { const { args, flags } = await this.parse(TargetsAddDesktop) diff --git a/packages/cli/src/commands/targets/add/server.ts b/packages/cli/src/commands/targets/add/server.ts index 435a76f2..fed575ad 100644 --- a/packages/cli/src/commands/targets/add/server.ts +++ b/packages/cli/src/commands/targets/add/server.ts @@ -2,6 +2,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { createProfileTarget, readTarget, updateConfig } from '../../../lib/targets.js' import { printDryRun, printSuccess } from '../../../lib/output.js' +import { SERVER_ENVIRONMENTS } from '../../../lib/server-env.js' export default class TargetsAddServer extends BeeperCommand { static override summary = 'Add a managed Beeper Server target' @@ -9,7 +10,7 @@ export default class TargetsAddServer extends BeeperCommand { static override flags = { port: Flags.integer({ description: 'TCP port the managed Server will expose its API on' }), default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server feed environment' }), + 'server-env': Flags.string({ options: SERVER_ENVIRONMENTS, default: 'prod', description: 'Server feed environment' }), } async run(): Promise { const { args, flags } = await this.parse(TargetsAddServer) diff --git a/packages/cli/src/lib/installations.ts b/packages/cli/src/lib/installations.ts index 3ad7338a..8c7a394b 100644 --- a/packages/cli/src/lib/installations.ts +++ b/packages/cli/src/lib/installations.ts @@ -8,12 +8,14 @@ import type { ReadableStream } from 'node:stream/web' import { execFile } from 'node:child_process' import { promisify } from 'node:util' import { beeperDir } from './targets.js' +import { SERVER_ENV_API_BASE_URLS, normalizeServerEnv, type ServerEnv } from './server-env.js' + +export type { ServerEnv } from './server-env.js' const execFileAsync = promisify(execFile) export type InstallKind = 'desktop' | 'server' export type InstallChannel = 'stable' | 'nightly' -export type ServerEnv = 'production' | 'staging' export type Installation = { kind: InstallKind @@ -101,7 +103,7 @@ export function normalizeInstallRequest(options: { feedPlatform, arch, bundleID, - apiBaseURL: serverEnv === 'staging' ? 'https://api.beeper-staging.com' : 'https://api.beeper.com', + apiBaseURL: SERVER_ENV_API_BASE_URLS[serverEnv], } } @@ -146,7 +148,7 @@ export async function checkInstallationUpdate(installation: Installation): Promi export async function installDesktop(options: { channel?: InstallChannel; serverEnv?: string } = {}): Promise { const request = normalizeInstallRequest({ kind: 'desktop', channel: options.channel, serverEnv: options.serverEnv }) - if (request.serverEnv === 'staging') throw new Error('Desktop staging installs are not supported by the CLI.') + if (request.serverEnv !== 'prod') throw new Error('Desktop non-production installs are not supported by the CLI.') const feedURL = feedURLFor(request) const feed = await fetchFeed(feedURL) const downloadURL = feed.url @@ -322,12 +324,6 @@ async function findServerExecutable(dir: string): Promise { throw new Error('Downloaded Beeper Server artifact did not contain a beeper-server executable.') } -function normalizeServerEnv(value?: string): ServerEnv { - if (!value || value === 'production' || value === 'prod') return 'production' - if (value === 'staging') return 'staging' - throw new Error(`Unsupported server env "${value}". Expected production or staging.`) -} - function normalizeDownloadPlatform(platform: NodeJS.Platform): 'macos' | 'windows' | 'linux' { if (platform === 'darwin') return 'macos' if (platform === 'win32') return 'windows' diff --git a/packages/cli/src/lib/manifest.ts b/packages/cli/src/lib/manifest.ts index 2d2aff33..8ae53c47 100644 --- a/packages/cli/src/lib/manifest.ts +++ b/packages/cli/src/lib/manifest.ts @@ -49,7 +49,7 @@ export const commandManifest: ManifestCommand[] = [ { command: 'targets add server', description: 'Add a managed Beeper Server target', - examples: ['beeper targets add server prod --server-env production --default'], + examples: ['beeper targets add server prod --server-env prod --default'], }, { command: 'targets add remote', diff --git a/packages/cli/src/lib/server-env.ts b/packages/cli/src/lib/server-env.ts new file mode 100644 index 00000000..d4a3bc0c --- /dev/null +++ b/packages/cli/src/lib/server-env.ts @@ -0,0 +1,16 @@ +export const SERVER_ENVIRONMENTS = ['local', 'dev', 'staging', 'prod'] as const + +export type ServerEnv = typeof SERVER_ENVIRONMENTS[number] + +export const SERVER_ENV_API_BASE_URLS: Record = { + local: 'https://api.beeper.localtest.me', + dev: 'https://api.beeper-dev.com', + staging: 'https://api.beeper-staging.com', + prod: 'https://api.beeper.com', +} + +export function normalizeServerEnv(value?: string): ServerEnv { + if (!value || value === 'prod' || value === 'production') return 'prod' + if (value === 'local' || value === 'dev' || value === 'staging') return value + throw new Error(`Unsupported server env "${value}". Expected local, dev, staging, or prod.`) +} diff --git a/packages/cli/src/lib/targets.ts b/packages/cli/src/lib/targets.ts index e16730c5..8c1623ad 100644 --- a/packages/cli/src/lib/targets.ts +++ b/packages/cli/src/lib/targets.ts @@ -3,6 +3,7 @@ import { access, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promise import { homedir } from 'node:os' import { dirname, join } from 'node:path' import { notFound } from './errors.js' +import { normalizeServerEnv } from './server-env.js' export type AuthSource = 'desktop-db' | 'desktop-cache' | 'desktop-oauth' | 'remote-oauth' | 'manual' @@ -201,7 +202,7 @@ function normalizeLocalTarget(target: Target): Target { } export async function createProfileTarget(type: ManagedTargetType, id: string, options: { serverEnv?: string; port?: number } = {}): Promise { - const serverEnv = options.serverEnv ?? 'production' + const serverEnv = normalizeServerEnv(options.serverEnv) const port = options.port ?? await nextPort() const target: Target = { id, diff --git a/packages/cli/test/cli-smoke.ts b/packages/cli/test/cli-smoke.ts index 1f5828d6..43a736f4 100644 --- a/packages/cli/test/cli-smoke.ts +++ b/packages/cli/test/cli-smoke.ts @@ -249,7 +249,7 @@ writeFileSync(join(configDir, 'installations.json'), `${JSON.stringify({ server: { kind: 'server', channel: 'stable', - serverEnv: 'production', + serverEnv: 'prod', bundleID: 'com.automattic.beeper.server', version: 'test', path: fakeServerPath, @@ -287,11 +287,22 @@ assert.equal(stagingServerRequest.bundleID, 'com.automattic.beeper.server') assert.equal(feedURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') assert.equal(downloadURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server') -const productionServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'production', channel: 'stable', platform: 'darwin', arch: 'arm64' }) -assert.equal(productionServerRequest.channel, 'stable') -assert.equal(productionServerRequest.bundleID, 'com.automattic.beeper.server') -assert.equal(feedURLFor(productionServerRequest), 'https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') -assert.equal(downloadURLFor(productionServerRequest), 'https://api.beeper.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server') +const prodServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'prod', channel: 'stable', platform: 'darwin', arch: 'arm64' }) +assert.equal(prodServerRequest.channel, 'stable') +assert.equal(prodServerRequest.bundleID, 'com.automattic.beeper.server') +assert.equal(feedURLFor(prodServerRequest), 'https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') +assert.equal(downloadURLFor(prodServerRequest), 'https://api.beeper.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server') + +const productionAliasServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'production', channel: 'stable', platform: 'darwin', arch: 'arm64' }) +assert.equal(productionAliasServerRequest.serverEnv, 'prod') + +const localServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'local', channel: 'stable', platform: 'darwin', arch: 'arm64' }) +assert.equal(feedURLFor(localServerRequest), 'https://api.beeper.localtest.me/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') +assert.equal(downloadURLFor(localServerRequest), 'https://api.beeper.localtest.me/desktop/download/macos/arm64/stable/com.automattic.beeper.server') + +const devServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'dev', channel: 'stable', platform: 'darwin', arch: 'arm64' }) +assert.equal(feedURLFor(devServerRequest), 'https://api.beeper-dev.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') +assert.equal(downloadURLFor(devServerRequest), 'https://api.beeper-dev.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server') const stagingNightlyServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'staging', channel: 'nightly', platform: 'darwin', arch: 'arm64' }) assert.equal(stagingNightlyServerRequest.channel, 'nightly') From 12ed6118a3c998f4223d5c88f94aa4c039951a70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:14:17 +0200 Subject: [PATCH 05/22] wip --- docs/src/content/docs/targets.mdx | 4 ++-- packages/cli/README.md | 8 ++++---- packages/cli/src/commands/install/server.ts | 3 +-- packages/cli/src/commands/setup.ts | 4 ++-- packages/cli/src/commands/targets/add/desktop.ts | 3 +-- packages/cli/src/commands/targets/add/server.ts | 3 +-- 6 files changed, 11 insertions(+), 14 deletions(-) diff --git a/docs/src/content/docs/targets.mdx b/docs/src/content/docs/targets.mdx index 64c20cc9..85cd276e 100644 --- a/docs/src/content/docs/targets.mdx +++ b/docs/src/content/docs/targets.mdx @@ -18,8 +18,8 @@ commands use it unless `--target ` overrides. ```sh beeper targets list -beeper targets add desktop [name] [--port N] [--server-env local|dev|staging|prod] [--default] -beeper targets add server [name] [--port N] [--server-env local|dev|staging|prod] [--default] +beeper targets add desktop [name] [--port N] [--server-env prod|staging] [--default] +beeper targets add server [name] [--port N] [--server-env prod|staging] [--default] beeper targets add remote [--default] beeper targets use beeper targets show [name] diff --git a/packages/cli/README.md b/packages/cli/README.md index 5cc332b9..f0690a0a 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -449,7 +449,7 @@ Flags: | `--oauth` | boolean | Authorize the target with browser OAuth/PKCE | | `--remote=` | option | Connect to a remote Beeper Desktop or Server URL | | `--server` | boolean | Set up a local Beeper Server target | -| `--server-env=` | option | Server feed environment Default: prod | +| `--server-env=` | option | Server feed environment: prod or staging Default: prod | | `--username=` | option | Username to use if setup creates a new account | Examples: @@ -498,7 +498,7 @@ Flags: | Flag | Type | Description | | --- | --- | --- | | `--channel=` | option | Server release channel Default: stable | -| `--server-env=` | option | Server feed environment Default: prod | +| `--server-env=` | option | Server feed environment: prod or staging Default: prod | Examples: @@ -591,7 +591,7 @@ Flags: | --- | --- | --- | | `--default` | boolean | Set this target as the default after creation | | `--port=` | option | TCP port the managed Desktop will expose its API on | -| `--server-env=` | option | Server feed environment Default: prod | +| `--server-env=` | option | Server feed environment: prod or staging Default: prod | Examples: @@ -620,7 +620,7 @@ Flags: | --- | --- | --- | | `--default` | boolean | Set this target as the default after creation | | `--port=` | option | TCP port the managed Server will expose its API on | -| `--server-env=` | option | Server feed environment Default: prod | +| `--server-env=` | option | Server feed environment: prod or staging Default: prod | Examples: diff --git a/packages/cli/src/commands/install/server.ts b/packages/cli/src/commands/install/server.ts index f417165a..55f96306 100644 --- a/packages/cli/src/commands/install/server.ts +++ b/packages/cli/src/commands/install/server.ts @@ -3,13 +3,12 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { installServer, type InstallChannel } from '../../lib/installations.js' import { pathSetupHint } from '../../lib/env.js' import { printDryRun, printSuccess } from '../../lib/output.js' -import { SERVER_ENVIRONMENTS } from '../../lib/server-env.js' export default class SetupInstallServer extends BeeperCommand { static override summary = 'Install Beeper Server locally' static override flags = { channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Server release channel' }), - 'server-env': Flags.string({ options: SERVER_ENVIRONMENTS, default: 'prod', description: 'Server feed environment' }), + 'server-env': Flags.string({ default: 'prod', description: 'Server feed environment: prod or staging' }), } async run(): Promise { diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index cc2243a7..fa26259c 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -9,7 +9,7 @@ import { loginWithPKCE } from '../lib/oauth.js' import { findDesktopAppPath, launchDesktopApp, startProfile } from '../lib/profiles.js' import { interactiveEmailSetup } from '../lib/setup-login.js' import { renderStartupLogo } from '../lib/logo.js' -import { SERVER_ENVIRONMENTS, SERVER_ENV_API_BASE_URLS, normalizeServerEnv } from '../lib/server-env.js' +import { SERVER_ENV_API_BASE_URLS, normalizeServerEnv } from '../lib/server-env.js' import { builtInDesktopTargetID, createProfileTarget, @@ -36,7 +36,7 @@ export default class Setup extends BeeperCommand { desktop: Flags.boolean({ default: false, description: 'Set up a local Beeper Desktop target' }), install: Flags.boolean({ default: false, description: 'Allow installing missing managed runtime' }), channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Install release channel' }), - 'server-env': Flags.string({ options: SERVER_ENVIRONMENTS, default: 'prod', description: 'Server feed environment' }), + 'server-env': Flags.string({ default: 'prod', description: 'Server feed environment: prod or staging' }), email: Flags.string({ description: 'Sign in with an email address' }), username: Flags.string({ description: 'Username to use if setup creates a new account' }), } diff --git a/packages/cli/src/commands/targets/add/desktop.ts b/packages/cli/src/commands/targets/add/desktop.ts index 91a8f6cc..035127e3 100644 --- a/packages/cli/src/commands/targets/add/desktop.ts +++ b/packages/cli/src/commands/targets/add/desktop.ts @@ -2,7 +2,6 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { createProfileTarget, readTarget, updateConfig } from '../../../lib/targets.js' import { printDryRun, printSuccess } from '../../../lib/output.js' -import { SERVER_ENVIRONMENTS } from '../../../lib/server-env.js' export default class TargetsAddDesktop extends BeeperCommand { static override summary = 'Add a managed Beeper Desktop target' @@ -10,7 +9,7 @@ export default class TargetsAddDesktop extends BeeperCommand { static override flags = { port: Flags.integer({ description: 'TCP port the managed Desktop will expose its API on' }), default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ options: SERVER_ENVIRONMENTS, default: 'prod', description: 'Server feed environment' }), + 'server-env': Flags.string({ default: 'prod', description: 'Server feed environment: prod or staging' }), } async run(): Promise { const { args, flags } = await this.parse(TargetsAddDesktop) diff --git a/packages/cli/src/commands/targets/add/server.ts b/packages/cli/src/commands/targets/add/server.ts index fed575ad..59fb4d3a 100644 --- a/packages/cli/src/commands/targets/add/server.ts +++ b/packages/cli/src/commands/targets/add/server.ts @@ -2,7 +2,6 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { createProfileTarget, readTarget, updateConfig } from '../../../lib/targets.js' import { printDryRun, printSuccess } from '../../../lib/output.js' -import { SERVER_ENVIRONMENTS } from '../../../lib/server-env.js' export default class TargetsAddServer extends BeeperCommand { static override summary = 'Add a managed Beeper Server target' @@ -10,7 +9,7 @@ export default class TargetsAddServer extends BeeperCommand { static override flags = { port: Flags.integer({ description: 'TCP port the managed Server will expose its API on' }), default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ options: SERVER_ENVIRONMENTS, default: 'prod', description: 'Server feed environment' }), + 'server-env': Flags.string({ default: 'prod', description: 'Server feed environment: prod or staging' }), } async run(): Promise { const { args, flags } = await this.parse(TargetsAddServer) From 3212e03d14e70a77cdf110700f2559c82fc2a4ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:38:59 +0200 Subject: [PATCH 06/22] fix npm launcher download progress --- packages/npm/scripts/build.ts | 41 ++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/npm/scripts/build.ts b/packages/npm/scripts/build.ts index b8dd6405..cd53ffeb 100644 --- a/packages/npm/scripts/build.ts +++ b/packages/npm/scripts/build.ts @@ -45,7 +45,7 @@ const platform = targetPlatform() const artifact = manifest.artifacts.find(item => item.platform === platform) if (!artifact) { - console.error(\`beeper-cli does not ship a binary for \${process.platform}/\${process.arch}.\`) + console.error(`beeper-cli does not ship a binary for ${process.platform}/${process.arch}.`) process.exit(1) } @@ -55,10 +55,10 @@ const binPath = join(cacheDir, 'bin', manifest.command || 'beeper') const expectedBinarySha256 = artifact.binarySha256 || artifact.sha256 if (!existsSync(binPath) || await sha256(binPath).catch(() => '') !== expectedBinarySha256) { - const tempDir = join(tmpdir(), \`beeper-cli-\${manifest.version}-\${process.pid}\`) + const tempDir = join(tmpdir(), `beeper-cli-${manifest.version}-${process.pid}`) const archivePath = join(tempDir, artifact.file) - const downloadURL = \`https://github.com/beeper/cli/releases/download/v\${manifest.version}/\${artifact.file}\` - logStep(\`installing beeper-cli \${manifest.version} for \${platform}\`) + const downloadURL = `https://github.com/beeper/cli/releases/download/v${manifest.version}/${artifact.file}` + logStep(`installing beeper-cli ${manifest.version} for ${platform}`) await rm(tempDir, { recursive: true, force: true }) await mkdir(tempDir, { recursive: true }) await download(downloadURL, archivePath) @@ -66,14 +66,14 @@ if (!existsSync(binPath) || await sha256(binPath).catch(() => '') !== expectedBi const actual = await sha256(archivePath) if (actual !== artifact.sha256) { await rm(tempDir, { recursive: true, force: true }) - console.error(\`beeper-cli binary checksum mismatch for \${artifact.file}.\`) + console.error(`beeper-cli binary checksum mismatch for ${artifact.file}.`) process.exit(1) } logStep('extracting binary') await extract(archivePath, tempDir) const extractedBin = join(tempDir, 'bin', manifest.command || 'beeper') await chmod(extractedBin, 0o755) - logStep(\`caching binary in \${cacheDir}\`) + logStep(`caching binary in ${cacheDir}`) await rm(cacheDir, { recursive: true, force: true }) await mkdir(dirname(binPath), { recursive: true }) await rename(extractedBin, binPath) @@ -81,7 +81,7 @@ if (!existsSync(binPath) || await sha256(binPath).catch(() => '') !== expectedBi logStep('ready') } -if (process.env.BEEPER_CLI_LAUNCHER_DEBUG === '1') logStep(\`starting \${binPath}\`) +if (process.env.BEEPER_CLI_LAUNCHER_DEBUG === '1') logStep(`starting ${binPath}`) const child = spawn(binPath, process.argv.slice(2), { stdio: 'inherit', env: process.env }) child.on('exit', (code, signal) => { if (signal) process.kill(process.pid, signal) @@ -89,7 +89,7 @@ child.on('exit', (code, signal) => { }) function logStep(message) { - console.error(\`beeper-cli: \${message}\`) + console.error(`beeper-cli: ${message}`) } function targetPlatform() { @@ -97,7 +97,7 @@ function targetPlatform() { const cpu = osArch() const normalizedOS = os === 'darwin' || os === 'linux' ? os : os === 'win32' ? 'windows' : os const normalizedArch = cpu === 'x64' || cpu === 'arm64' ? cpu : cpu - return \`\${normalizedOS}-\${normalizedArch}\` + return `${normalizedOS}-${normalizedArch}` } async function sha256(path) { @@ -106,20 +106,22 @@ async function sha256(path) { return hash.digest('hex') } -async function download(url, destination) { - logStep(\`downloading \${artifact.file}\`) +async function download(url, destination, redirects = 0) { + if (redirects > 10) throw new Error(`Too many redirects while downloading ${artifact.file}`) + + logStep(`downloading ${artifact.file}`) await new Promise((resolve, reject) => { get(url, response => { if ([301, 302, 303, 307, 308].includes(response.statusCode ?? 0) && response.headers.location) { response.resume() const nextURL = new URL(response.headers.location, url).toString() - logStep(\`redirecting to \${new URL(nextURL).host}\`) - download(nextURL, destination).then(resolve, reject) + logStep(`redirecting to ${new URL(nextURL).host}`) + download(nextURL, destination, redirects + 1).then(resolve, reject) return } if (response.statusCode !== 200) { response.resume() - reject(new Error(\`Download failed with HTTP \${response.statusCode}: \${url}\`)) + reject(new Error(`Download failed with HTTP ${response.statusCode}: ${url}`)) return } const total = Number(response.headers['content-length'] ?? 0) @@ -130,10 +132,9 @@ async function download(url, destination) { downloaded += chunk.length if (!total) return const percent = Math.floor(downloaded / total * 100) - if (percent >= nextLoggedPercent || percent === 100) { - const milestone = percent === 100 ? 100 : nextLoggedPercent - logStep(\`downloaded \${milestone}%\`) - nextLoggedPercent = milestone + 25 + while (percent >= nextLoggedPercent && nextLoggedPercent <= 100) { + logStep(`downloaded ${nextLoggedPercent}%`) + nextLoggedPercent += 25 } }) response.pipe(file) @@ -152,7 +153,7 @@ async function extract(archivePath, destination) { await run('tar', ['-xzf', archivePath, '-C', destination]) return } - throw new Error(\`Unsupported beeper-cli archive: \${artifact.file}\`) + throw new Error(`Unsupported beeper-cli archive: ${artifact.file}`) } async function run(command, args) { @@ -161,7 +162,7 @@ async function run(command, args) { child.on('error', reject) child.on('exit', code => { if (code === 0) resolve() - else reject(new Error(\`\${command} \${args.join(' ')} exited with \${code}\`)) + else reject(new Error(`${command} ${args.join(' ')} exited with ${code}`)) }) }) } From c5cfb050e9d9e24659910664fe2826cd86a52581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:03 +0200 Subject: [PATCH 07/22] fix schema command filtering and shapes --- packages/cli/src/commands/schema.ts | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/commands/schema.ts b/packages/cli/src/commands/schema.ts index 94e872c0..24eab64d 100644 --- a/packages/cli/src/commands/schema.ts +++ b/packages/cli/src/commands/schema.ts @@ -16,15 +16,17 @@ type RawCommand = { } export default class Schema extends BeeperCommand { + static override strict = false static override summary = 'Print machine-readable command/flag schema' static override description = 'Agent-first schema for commands, flags, args, examples, mutation metadata, selectors, output shapes, and related commands.' static override args = { - command: Args.string({ required: false, description: 'Optional command path, such as "messages search"', multiple: true }), + command: Args.string({ required: false, description: 'Optional command path, such as "messages search"' }), } async run(): Promise { - const { args } = await this.parse(Schema) - const requested = Array.isArray(args.command) ? args.command.join(' ') : undefined + await this.parse(Schema) + const pathArgs = this.argv.filter(arg => !arg.startsWith('-')) + const requested = pathArgs.length > 0 ? pathArgs.join(' ') : undefined const manifestByCommand = new Map(commandManifest.map(item => [item.command, item])) const commands = (this.config.commands as RawCommand[]) .filter(command => !command.hidden) @@ -116,10 +118,20 @@ function typeName(record: Record): string { function outputShape(kind: string): Record { const envelope = { ok: true, data: '', error: null, meta: '' } switch (kind) { - case 'list': return { kind, envelope, data: 'array' } - case 'stream': return { kind, data: 'jsonl events or RPC lines' } - case 'success': return { kind, envelope, data: { message: 'string', detail: 'string?', entity: 'object?' } } - case 'send-result': return { kind, envelope, data: { chatID: 'string', pendingMessageID: 'string?', state: 'string?' } } - default: return { kind, envelope, data: 'object' } + case 'list': { + return { kind, envelope, data: 'array' } + } + case 'send-result': { + return { kind, envelope, data: { chatID: 'string', pendingMessageID: 'string?', state: 'string?' } } + } + case 'stream': { + return { kind, data: 'jsonl events or RPC lines' } + } + case 'success': { + return { kind, envelope, data: { message: 'string', detail: 'string?', data: 'object?' } } + } + default: { + return { kind, envelope, data: 'object' } + } } } From ffcff36b42217a27a74984a9ed6c6eb1d6bc1c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:06 +0200 Subject: [PATCH 08/22] fix command error classification --- packages/cli/src/lib/command.ts | 34 +++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/cli/src/lib/command.ts b/packages/cli/src/lib/command.ts index 08bb4f37..2f9243e6 100644 --- a/packages/cli/src/lib/command.ts +++ b/packages/cli/src/lib/command.ts @@ -47,7 +47,7 @@ export abstract class BeeperCommand extends Command { const code = inferredCode ?? error.exitCode ?? ExitCodes.Generic process.exitCode = process.exitCode ?? code const tryMessage = error instanceof CLIError ? error.tryMessage : undefined - const isBug = error instanceof BugError || (!(error instanceof CLIError) && inferredCode === undefined) + const isBug = error instanceof BugError || !(error instanceof CLIError) if (this.argv.includes('--events')) { writeEvent('error', { message, exitCode: code, kind: isBug ? 'bug' : 'abort', tryMessage }) @@ -102,10 +102,6 @@ export function ensureWritable(flags: { 'read-only'?: boolean }): void { if (readOnly) throw new CLIError('read-only mode: command would modify Beeper or local CLI state', ExitCodes.Usage) } -export function ensureNotDryRun(flags: { 'dry-run'?: boolean }, action: string): void { - if (flags['dry-run']) throw new CLIError(`dry-run: ${action}`, ExitCodes.Success) -} - export function writeEvent(event: string, data: Record = {}): void { process.stderr.write(`${JSON.stringify({ event, data, ts: Date.now() })}\n`) } @@ -148,12 +144,26 @@ function isMachineOutput(argv: string[]): boolean { function errorCode(code: number, isBug: boolean): string { if (isBug) return 'internal_error' switch (code) { - case ExitCodes.Usage: return 'usage_error' - case ExitCodes.AuthRequired: return 'auth_required' - case ExitCodes.NotReady: return 'not_ready' - case ExitCodes.NotFound: return 'not_found' - case ExitCodes.Ambiguous: return 'ambiguous_selector' - case ExitCodes.CommandNotFound: return 'command_not_found' - default: return 'runtime_error' + case ExitCodes.Ambiguous: { + return 'ambiguous_selector' + } + case ExitCodes.AuthRequired: { + return 'auth_required' + } + case ExitCodes.CommandNotFound: { + return 'command_not_found' + } + case ExitCodes.NotFound: { + return 'not_found' + } + case ExitCodes.NotReady: { + return 'not_ready' + } + case ExitCodes.Usage: { + return 'usage_error' + } + default: { + return 'runtime_error' + } } } From a42c57be1c685754e5ec5f928ccb4d4031b60f5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:09 +0200 Subject: [PATCH 09/22] fix machine-readable failure output --- packages/cli/src/lib/output.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/lib/output.ts b/packages/cli/src/lib/output.ts index 724bfdea..2f836aaf 100644 --- a/packages/cli/src/lib/output.ts +++ b/packages/cli/src/lib/output.ts @@ -99,7 +99,7 @@ export async function printSuccess( ): Promise { format = effectiveFormat(format) if (format === 'json' || format === 'jsonl') { - writeJSON(jsonPayload({ message: opts.message, detail: opts.detail, entity: opts.entity, ...(opts.data ?? {}) }), format) + writeJSON(jsonPayload({ message: opts.message, detail: opts.detail, entity: opts.entity, ...opts.data }), format) return } if (format === 'ids') { @@ -124,6 +124,14 @@ export async function printFailure( writeJSON({ ok: false, data: opts.data ?? null, error: { message: opts.message, detail: opts.detail } }, format) return } + if (format === 'text') { + process.stdout.write(`${opts.message}${opts.detail ? `\t${opts.detail}` : ''}\n`) + return + } + if (format === 'ids') { + if (opts.data) printIDs([opts.data]) + return + } const { renderFailure } = await loadInk() await renderFailure(opts) } @@ -278,7 +286,7 @@ function printText(value: unknown): void { return } for (const [key, item] of Object.entries(value as Record)) { - if (item == null) continue + if (item === null || item === undefined) continue process.stdout.write(`${key}\t${typeof item === 'object' ? JSON.stringify(item) : String(item)}\n`) } } From cb259c98187e3ec2f5ae1686182533c8d0f1681f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:15 +0200 Subject: [PATCH 10/22] fix resolver error payloads and pick validation --- packages/cli/src/commands/resolve/account.ts | 5 +++-- packages/cli/src/commands/resolve/bridge.ts | 5 +++-- packages/cli/src/commands/resolve/contact.ts | 18 +++++++++++++----- packages/cli/src/lib/resolve.ts | 3 +-- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/cli/src/commands/resolve/account.ts b/packages/cli/src/commands/resolve/account.ts index a62ce1b9..ade03dc9 100644 --- a/packages/cli/src/commands/resolve/account.ts +++ b/packages/cli/src/commands/resolve/account.ts @@ -22,8 +22,9 @@ export default class ResolveAccount extends BeeperCommand { const ids = await resolveAccountIDs(client, [args.selector], { allowMultiplePerInput: true, applyDefault: false }) const candidates = rows.filter((row: any) => ids?.includes(String(row.accountID ?? row.id))) if (!candidates.length) throw notFound(`No account matches "${args.selector}"`, { selector: args.selector, kind: 'account' }) - const selected = flags.pick ? candidates[flags.pick - 1] : candidates.length === 1 ? candidates[0] : undefined - if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${candidates.length} matching accounts`, { selector: args.selector, pick: flags.pick, count: candidates.length }) + const pick = flags.pick + const selected = pick !== undefined ? candidates[pick - 1] : candidates.length === 1 ? candidates[0] : undefined + if (pick !== undefined && !selected) throw notFound(`--pick ${pick} is outside the ${candidates.length} matching accounts`, { selector: args.selector, pick, count: candidates.length }) await printData({ selector: args.selector, kind: 'account', diff --git a/packages/cli/src/commands/resolve/bridge.ts b/packages/cli/src/commands/resolve/bridge.ts index eb0a42f6..44781dfe 100644 --- a/packages/cli/src/commands/resolve/bridge.ts +++ b/packages/cli/src/commands/resolve/bridge.ts @@ -29,8 +29,9 @@ export default class ResolveBridge extends BeeperCommand { normalize(bridge.displayName).includes(normalized) ) if (!candidates.length) throw notFound(`No bridge matches "${args.selector}"`, { selector: args.selector, kind: 'bridge' }) - const selected = flags.pick ? candidates[flags.pick - 1] : candidates.length === 1 ? candidates[0] : undefined - if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${candidates.length} matching bridges`, { selector: args.selector, pick: flags.pick, count: candidates.length }) + const pick = flags.pick + const selected = pick !== undefined ? candidates[pick - 1] : candidates.length === 1 ? candidates[0] : undefined + if (pick !== undefined && !selected) throw notFound(`--pick ${pick} is outside the ${candidates.length} matching bridges`, { selector: args.selector, pick, count: candidates.length }) await printData({ selector: args.selector, kind: 'bridge', diff --git a/packages/cli/src/commands/resolve/contact.ts b/packages/cli/src/commands/resolve/contact.ts index f85f1bd4..8cfd2491 100644 --- a/packages/cli/src/commands/resolve/contact.ts +++ b/packages/cli/src/commands/resolve/contact.ts @@ -25,13 +25,15 @@ export default class ResolveContact extends BeeperCommand { try { const result = await client.accounts.contacts.search(accountID, { query: args.selector }) candidates.push(...result.items.slice(0, flags.limit).map((item: unknown) => ({ ...(item as Record), accountID }))) - } catch { - // Keep searching accounts that support lookup for this selector shape. + } catch (error) { + if (shouldIgnoreLookupError(error)) continue + throw error } } if (!candidates.length) throw notFound(`No contact matches "${args.selector}"`, { selector: args.selector, kind: 'contact' }) - const selected = flags.pick ? candidates[flags.pick - 1] : candidates.length === 1 ? candidates[0] : undefined - if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${candidates.length} matching contacts`, { selector: args.selector, pick: flags.pick, count: candidates.length }) + const pick = flags.pick + const selected = pick !== undefined ? candidates[pick - 1] : candidates.length === 1 ? candidates[0] : undefined + if (pick !== undefined && !selected) throw notFound(`--pick ${pick} is outside the ${candidates.length} matching contacts`, { selector: args.selector, pick, count: candidates.length }) await printData({ selector: args.selector, kind: 'contact', @@ -50,6 +52,12 @@ function contactCandidate(contact: Record, pick: number): Recor username: contact.username, phoneNumber: contact.phoneNumber, email: contact.email, - raw: contact, } } + +function shouldIgnoreLookupError(error: unknown): boolean { + if (!(error instanceof Error)) return false + const status = (error as Error & { status?: number; statusCode?: number }).status ?? (error as Error & { status?: number; statusCode?: number }).statusCode + if (status === 400 || status === 404) return true + return /\b(400|404)\b|not supported|not found/i.test(error.message) +} diff --git a/packages/cli/src/lib/resolve.ts b/packages/cli/src/lib/resolve.ts index bf25940e..a3f11a32 100644 --- a/packages/cli/src/lib/resolve.ts +++ b/packages/cli/src/lib/resolve.ts @@ -36,7 +36,7 @@ export async function resolveAccountIDs( throw ambiguous(formatAmbiguous(`account "${input}"`, matches.map(formatAccount)), { selector: input, kind: 'account', - candidates: matches.map((account, index) => ({ pick: index + 1, id: String(account.accountID ?? account.id), label: formatAccount(account), raw: account })), + candidates: matches.map((account, index) => ({ pick: index + 1, id: String(account.accountID ?? account.id), label: formatAccount(account) })), }) } resolved.push(...matches.map(account => String(account.accountID))) @@ -97,7 +97,6 @@ export async function resolveChatID(client: any, input: string, options: ChatRes title: chat.title ? String(chat.title) : undefined, network: chat.network ? String(chat.network) : undefined, label: formatChat(chat), - raw: chat, })), }) } From 619c53e2248b6131e491a4e120c9dc4da890fe20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:18 +0200 Subject: [PATCH 11/22] fix command metadata lint issues --- packages/cli/src/lib/command-metadata.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/lib/command-metadata.ts b/packages/cli/src/lib/command-metadata.ts index 69b27dc1..3d3f3c4a 100644 --- a/packages/cli/src/lib/command-metadata.ts +++ b/packages/cli/src/lib/command-metadata.ts @@ -9,16 +9,16 @@ export type CommandMetadata = { export function metadataForCommand(command: string): CommandMetadata { const parts = command.split(' ') const root = parts[0] ?? '' - const mutatingRoots = new Set(['setup', 'install', 'send', 'update', 'export', 'presence']) + const mutatingRoots = new Set(['export', 'install', 'presence', 'send', 'setup', 'update']) const mutatingVerbs = new Set([ - 'add', 'archive', 'unarchive', 'pin', 'unpin', 'mute', 'unmute', 'mark-read', 'mark-unread', - 'priority', 'notify-anyway', 'rename', 'description', 'avatar', 'draft', 'disappear', 'remind', - 'unremind', 'focus', 'edit', 'delete', 'remove', 'use', 'set', 'reset', 'logout', 'start', 'stop', - 'restart', 'enable', 'disable', 'download', 'export', 'post', 'response', 'approve', 'recovery-key', 'reset-recovery-key', 'cancel', 'sas', - 'sas-confirm', 'qr-scan', 'qr-confirm', + 'add', 'approve', 'archive', 'avatar', 'cancel', 'delete', 'description', 'disable', 'disappear', 'download', + 'draft', 'edit', 'enable', 'export', 'focus', 'logout', 'mark-read', 'mark-unread', 'mute', 'notify-anyway', + 'pin', 'post', 'priority', 'qr-confirm', 'qr-scan', 'recovery-key', 'remind', 'remove', 'rename', 'reset', + 'reset-recovery-key', 'response', 'restart', 'sas', 'sas-confirm', 'set', 'start', 'stop', 'unarchive', + 'unmute', 'unpin', 'unremind', 'use', ]) const mutates = command === 'verify' || command === 'api request' || mutatingRoots.has(root) || parts.some(part => mutatingVerbs.has(part ?? '')) - const localOnly = new Set(['config', 'completion', 'docs', 'version', 'man', 'schema']) + const localOnly = new Set(['completion', 'config', 'docs', 'man', 'schema', 'version']) const requiresAuth = !localOnly.has(root) && command !== 'targets list' && !command.startsWith('targets add') && !command.startsWith('install ') const selectors = [ command.includes('chats ') || command.includes('messages ') || command.startsWith('send ') || command === 'presence' || command.startsWith('resolve chat') ? 'chat' : undefined, @@ -26,7 +26,7 @@ export function metadataForCommand(command: string): CommandMetadata { command.includes('targets ') || command === 'status' || command === 'doctor' || command.startsWith('auth ') || command.startsWith('verify') || command.startsWith('resolve target') ? 'target' : undefined, command.startsWith('bridges ') || command === 'accounts add' || command.startsWith('resolve bridge') ? 'bridge' : undefined, command.includes('messages ') || command.startsWith('send react') || command.startsWith('send unreact') ? 'message' : undefined, - ].filter((value): value is string => Boolean(value)) + ].filter(Boolean) as string[] const output = command === 'schema' ? 'schema' : command.startsWith('send ') ? 'send-result' : command === 'watch' || command === 'rpc' ? 'stream' From 3cebc504fc242821a92c1178ef917291bd8af445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:22 +0200 Subject: [PATCH 12/22] remove unused output imports --- packages/cli/src/commands/chats/archive.ts | 2 +- packages/cli/src/commands/chats/avatar.ts | 2 +- packages/cli/src/commands/chats/description.ts | 2 +- packages/cli/src/commands/chats/draft.ts | 2 +- packages/cli/src/commands/chats/focus.ts | 2 +- packages/cli/src/commands/chats/mark-read.ts | 2 +- packages/cli/src/commands/chats/mark-unread.ts | 2 +- packages/cli/src/commands/chats/notify-anyway.ts | 2 +- packages/cli/src/commands/chats/pin.ts | 2 +- packages/cli/src/commands/chats/remind.ts | 2 +- packages/cli/src/commands/chats/unarchive.ts | 2 +- packages/cli/src/commands/chats/unmute.ts | 2 +- packages/cli/src/commands/chats/unpin.ts | 2 +- packages/cli/src/commands/chats/unremind.ts | 2 +- packages/cli/src/commands/targets/remove.ts | 2 +- packages/cli/src/commands/targets/use.ts | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/cli/src/commands/chats/archive.ts b/packages/cli/src/commands/chats/archive.ts index cf690869..150a2883 100644 --- a/packages/cli/src/commands/chats/archive.ts +++ b/packages/cli/src/commands/chats/archive.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsArchive extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/avatar.ts b/packages/cli/src/commands/chats/avatar.ts index c511dbfc..01da3592 100644 --- a/packages/cli/src/commands/chats/avatar.ts +++ b/packages/cli/src/commands/chats/avatar.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsAvatar extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/description.ts b/packages/cli/src/commands/chats/description.ts index 6c382ae8..1f081774 100644 --- a/packages/cli/src/commands/chats/description.ts +++ b/packages/cli/src/commands/chats/description.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsDescription extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/draft.ts b/packages/cli/src/commands/chats/draft.ts index 2db6abb3..ec4f22ed 100644 --- a/packages/cli/src/commands/chats/draft.ts +++ b/packages/cli/src/commands/chats/draft.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsDraft extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/focus.ts b/packages/cli/src/commands/chats/focus.ts index e8b68036..3903edf8 100644 --- a/packages/cli/src/commands/chats/focus.ts +++ b/packages/cli/src/commands/chats/focus.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsFocus extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/mark-read.ts b/packages/cli/src/commands/chats/mark-read.ts index 176ff556..72ddf591 100644 --- a/packages/cli/src/commands/chats/mark-read.ts +++ b/packages/cli/src/commands/chats/mark-read.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsMarkRead extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/mark-unread.ts b/packages/cli/src/commands/chats/mark-unread.ts index e70043ff..73acc5c7 100644 --- a/packages/cli/src/commands/chats/mark-unread.ts +++ b/packages/cli/src/commands/chats/mark-unread.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsMarkUnread extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/notify-anyway.ts b/packages/cli/src/commands/chats/notify-anyway.ts index 1ffdf72e..418d5237 100644 --- a/packages/cli/src/commands/chats/notify-anyway.ts +++ b/packages/cli/src/commands/chats/notify-anyway.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsNotifyAnyway extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/pin.ts b/packages/cli/src/commands/chats/pin.ts index 26543935..642265e9 100644 --- a/packages/cli/src/commands/chats/pin.ts +++ b/packages/cli/src/commands/chats/pin.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsPin extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/remind.ts b/packages/cli/src/commands/chats/remind.ts index fce00d4e..4bd61a01 100644 --- a/packages/cli/src/commands/chats/remind.ts +++ b/packages/cli/src/commands/chats/remind.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsRemind extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/unarchive.ts b/packages/cli/src/commands/chats/unarchive.ts index 8df63c14..2da004c4 100644 --- a/packages/cli/src/commands/chats/unarchive.ts +++ b/packages/cli/src/commands/chats/unarchive.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsUnarchive extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/unmute.ts b/packages/cli/src/commands/chats/unmute.ts index 3c7d1ac0..46148e78 100644 --- a/packages/cli/src/commands/chats/unmute.ts +++ b/packages/cli/src/commands/chats/unmute.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsUnmute extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/unpin.ts b/packages/cli/src/commands/chats/unpin.ts index 750f6348..2839626d 100644 --- a/packages/cli/src/commands/chats/unpin.ts +++ b/packages/cli/src/commands/chats/unpin.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsUnpin extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/unremind.ts b/packages/cli/src/commands/chats/unremind.ts index 15b28aad..92dd5a73 100644 --- a/packages/cli/src/commands/chats/unremind.ts +++ b/packages/cli/src/commands/chats/unremind.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsUnremind extends BeeperCommand { diff --git a/packages/cli/src/commands/targets/remove.ts b/packages/cli/src/commands/targets/remove.ts index 5aa2c45b..82cbb948 100644 --- a/packages/cli/src/commands/targets/remove.ts +++ b/packages/cli/src/commands/targets/remove.ts @@ -4,7 +4,7 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createProfileTarget, listTargets, readConfig, readTarget, removeTarget, resolveTarget, updateConfig, writeTarget, type Target } from '../../lib/targets.js' import { disableProfile, enableProfile, profileErrorLogPath, profileLogPath, profileStatus, startProfile, stopProfile } from '../../lib/profiles.js' import { targetLiveStatus } from '../../lib/target-status.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsRemove extends BeeperCommand { static override summary = 'Remove a target' diff --git a/packages/cli/src/commands/targets/use.ts b/packages/cli/src/commands/targets/use.ts index 107a03ea..92e385d2 100644 --- a/packages/cli/src/commands/targets/use.ts +++ b/packages/cli/src/commands/targets/use.ts @@ -4,7 +4,7 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createProfileTarget, listTargets, readConfig, readTarget, removeTarget, resolveTarget, updateConfig, writeTarget, type Target } from '../../lib/targets.js' import { disableProfile, enableProfile, profileErrorLogPath, profileLogPath, profileStatus, startProfile, stopProfile } from '../../lib/profiles.js' import { targetLiveStatus } from '../../lib/target-status.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsUse extends BeeperCommand { static override summary = 'Set the default target' From 7b17068a98aaca2ca394e49e20da2cfeae5cf7d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:26 +0200 Subject: [PATCH 13/22] fix dry-run guard ordering --- packages/cli/src/commands/accounts/add.ts | 2 +- packages/cli/src/commands/auth/logout.ts | 8 ++++---- packages/cli/src/commands/chats/disappear.ts | 8 ++++---- packages/cli/src/commands/messages/export.ts | 9 +++++---- packages/cli/src/commands/presence.ts | 8 ++++---- packages/cli/src/commands/send/voice.ts | 16 +++++++++------- packages/cli/src/commands/update.ts | 2 +- packages/cli/src/commands/verify/approve.ts | 4 ++-- 8 files changed, 30 insertions(+), 27 deletions(-) diff --git a/packages/cli/src/commands/accounts/add.ts b/packages/cli/src/commands/accounts/add.ts index a4b1865e..d28138e0 100644 --- a/packages/cli/src/commands/accounts/add.ts +++ b/packages/cli/src/commands/accounts/add.ts @@ -29,7 +29,6 @@ export default class AccountsAdd extends BeeperCommand { async run(): Promise { const { args, flags } = await this.parse(AccountsAdd) - ensureWritable(flags) const client = await createClient(flags) if (!args.bridge) { @@ -83,6 +82,7 @@ export default class AccountsAdd extends BeeperCommand { return } + ensureWritable(flags) const step = await client.bridges.loginSessions.create(accountType.id, { flowID, loginID: flags['login-id'], diff --git a/packages/cli/src/commands/auth/logout.ts b/packages/cli/src/commands/auth/logout.ts index df2e90b1..d4e9e8ab 100644 --- a/packages/cli/src/commands/auth/logout.ts +++ b/packages/cli/src/commands/auth/logout.ts @@ -7,16 +7,16 @@ export default class AuthLogout extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(AuthLogout) - ensureWritable(flags) + if (!flags['dry-run']) ensureWritable(flags) const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] }) - if (process.env.BEEPER_ACCESS_TOKEN && !target.auth?.accessToken) { - throw new Error('auth logout cannot clear BEEPER_ACCESS_TOKEN from the environment; unset it in the calling process.') - } const token = target.auth?.accessToken if (flags['dry-run']) { await printDryRun('auth.logout', { target: target.id, baseURL: target.baseURL, hadToken: Boolean(token), revokeToken: Boolean(token) }, flags.json ? 'json' : 'human') return } + if (process.env.BEEPER_ACCESS_TOKEN && !target.auth?.accessToken) { + throw new Error('auth logout cannot clear BEEPER_ACCESS_TOKEN from the environment; unset it in the calling process.') + } let revoked = false if (token) { const response = await fetch(new URL('/oauth/revoke', target.baseURL), { diff --git a/packages/cli/src/commands/chats/disappear.ts b/packages/cli/src/commands/chats/disappear.ts index 07c858fe..6e9237c7 100644 --- a/packages/cli/src/commands/chats/disappear.ts +++ b/packages/cli/src/commands/chats/disappear.ts @@ -17,15 +17,15 @@ export default class ChatsDisappear extends BeeperCommand { } async run(): Promise { const { flags } = await this.parse(ChatsDisappear) - ensureWritable(flags) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) const expiry = flags.seconds.toLowerCase() === 'off' ? null : Number(flags.seconds) if (expiry !== null && (!Number.isInteger(expiry) || expiry < 0)) throw new Error('--seconds must be a positive integer or "off"') if (flags['dry-run']) { - await printDryRun('chats.disappear', { chatID, messageExpirySeconds: expiry }, flags.json ? 'json' : 'human') + await printDryRun('chats.disappear', { chat: flags.chat, pick: flags.pick, messageExpirySeconds: expiry }, flags.json ? 'json' : 'human') return } + ensureWritable(flags) + const client = await createClient(flags) + const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) await printData(await client.chats.update(chatID, { messageExpirySeconds: expiry }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/messages/export.ts b/packages/cli/src/commands/messages/export.ts index 9615fd16..03d8ac38 100644 --- a/packages/cli/src/commands/messages/export.ts +++ b/packages/cli/src/commands/messages/export.ts @@ -23,12 +23,10 @@ export default class MessagesExport extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(MessagesExport) if (flags['before-cursor'] && flags['after-cursor']) throw new Error('Use only one of --before-cursor or --after-cursor') - if (flags.output !== '-') ensureWritable(flags) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) if (flags['dry-run']) { await printDryRun('messages.export', { - chatID, + chat: flags.chat, + pick: flags.pick, output: flags.output, beforeCursor: flags['before-cursor'], afterCursor: flags['after-cursor'], @@ -39,6 +37,9 @@ export default class MessagesExport extends BeeperCommand { }, flags.json ? 'json' : 'human') return } + if (flags.output !== '-') ensureWritable(flags) + const client = await createClient(flags) + const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) const cursor = flags['before-cursor'] ?? flags['after-cursor'] const direction = flags['before-cursor'] ? 'before' : flags['after-cursor'] ? 'after' : undefined const afterTs = flags.after ? Date.parse(flags.after) : undefined diff --git a/packages/cli/src/commands/presence.ts b/packages/cli/src/commands/presence.ts index e914148f..709f927b 100644 --- a/packages/cli/src/commands/presence.ts +++ b/packages/cli/src/commands/presence.ts @@ -17,15 +17,15 @@ export default class Presence extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(Presence) - ensureWritable(flags) if (flags.duration !== undefined && flags.duration <= 0) throw new Error('--duration must be a positive integer (seconds)') if (flags.duration !== undefined && flags.state !== 'typing') throw new Error('--duration only applies when --state is typing') - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) if (flags['dry-run']) { - await printDryRun('presence', { chatID, state: flags.state, durationSeconds: flags.duration }, flags.json ? 'json' : 'human') + await printDryRun('presence', { chat: flags.chat, pick: flags.pick, state: flags.state, durationSeconds: flags.duration }, flags.json ? 'json' : 'human') return } + ensureWritable(flags) + const client = await createClient(flags) + const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) const post = (state: 'typing' | 'paused') => client.post(`/v1/chats/${encodeURIComponent(chatID)}/typing`, { body: { state } }) diff --git a/packages/cli/src/commands/send/voice.ts b/packages/cli/src/commands/send/voice.ts index 3682957a..e89f8aa9 100644 --- a/packages/cli/src/commands/send/voice.ts +++ b/packages/cli/src/commands/send/voice.ts @@ -21,11 +21,9 @@ export default class SendVoice extends BeeperCommand { } async run(): Promise { const { flags } = await this.parse(SendVoice) - ensureWritable(flags) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) - const request = { - chatID, + const dryRunRequest = { + chat: flags.to, + pick: flags.pick, file: flags.file, fileName: flags.filename, mimeType: flags.mime, @@ -37,11 +35,15 @@ export default class SendVoice extends BeeperCommand { waitTimeoutMs: flags['wait-timeout'], } if (flags['dry-run']) { - await printDryRun('send.voice', request, flags.json ? 'json' : 'human') + await printDryRun('send.voice', dryRunRequest, flags.json ? 'json' : 'human') return } + + ensureWritable(flags) + const client = await createClient(flags) + const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) await printData( - await sendMessage(client, request), + await sendMessage(client, { ...dryRunRequest, chatID }), flags.json ? 'json' : 'human', ) } diff --git a/packages/cli/src/commands/update.ts b/packages/cli/src/commands/update.ts index 5f613f3b..f14f9f61 100644 --- a/packages/cli/src/commands/update.ts +++ b/packages/cli/src/commands/update.ts @@ -23,7 +23,7 @@ export default class Update extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(Update) - if (!flags.check) ensureWritable(flags) + if (!flags.check && !flags['dry-run']) ensureWritable(flags) const selected = flags.cli || flags.desktop || flags.server if (flags['dry-run'] && !flags.check) { await printDryRun('update', { cli: !selected || flags.cli, desktop: !selected || flags.desktop, server: !selected || flags.server }, flags.json ? 'json' : 'human') diff --git a/packages/cli/src/commands/verify/approve.ts b/packages/cli/src/commands/verify/approve.ts index caf4c6e5..dc99e52a 100644 --- a/packages/cli/src/commands/verify/approve.ts +++ b/packages/cli/src/commands/verify/approve.ts @@ -9,12 +9,12 @@ export default class AuthVerifyApprove extends BeeperCommand { } async run(): Promise { const { flags } = await this.parse(AuthVerifyApprove) - ensureWritable(flags) - const client = await createClient(flags) if (flags['dry-run']) { await printDryRun('verify.approve', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') return } + ensureWritable(flags) + const client = await createClient(flags) await printData(await client.app.verifications.accept(flags.id ?? 'active'), flags.json ? 'json' : 'human') } } From f14e0abb0f2f42b73018f86bba84973caeba24c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:29 +0200 Subject: [PATCH 14/22] fix dry-run padding lint --- packages/cli/src/commands/send/file.ts | 1 + packages/cli/src/commands/send/text.ts | 1 + packages/cli/src/commands/targets/start.ts | 2 ++ 3 files changed, 4 insertions(+) diff --git a/packages/cli/src/commands/send/file.ts b/packages/cli/src/commands/send/file.ts index d021ccb8..2e5442fc 100644 --- a/packages/cli/src/commands/send/file.ts +++ b/packages/cli/src/commands/send/file.ts @@ -34,6 +34,7 @@ export default class SendFile extends BeeperCommand { await printDryRun('send.file', request, flags.json ? 'json' : 'human') return } + await printData(await sendMessage(client, request), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/send/text.ts b/packages/cli/src/commands/send/text.ts index d5bffad8..3363907a 100644 --- a/packages/cli/src/commands/send/text.ts +++ b/packages/cli/src/commands/send/text.ts @@ -33,6 +33,7 @@ export default class SendText extends BeeperCommand { await printDryRun('send.text', request, flags.json ? 'json' : 'human') return } + await printData(await sendMessage(client, request), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/targets/start.ts b/packages/cli/src/commands/targets/start.ts index c9cd6a5e..956d621f 100644 --- a/packages/cli/src/commands/targets/start.ts +++ b/packages/cli/src/commands/targets/start.ts @@ -21,9 +21,11 @@ export default class TargetsStart extends BeeperCommand { await printSuccess({ message: 'Opened Beeper Desktop', detail: target.baseURL, data: { target, result } }, flags.json ? 'json' : 'human') return } + if (!target.managed || target.type !== 'server') { throw new Error(`Target "${target.id}" is not a local Beeper Server install.`) } + if (flags['dry-run']) { await printDryRun('targets.start', { target, startProfile: true }, flags.json ? 'json' : 'human') return From 337c94c8bfde4577aecdcc3960f6cd994d213895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:35 +0200 Subject: [PATCH 15/22] fix server environment setup handling --- README.md | 24 +++++++------- packages/cli/README.md | 32 +++++++++---------- packages/cli/src/commands/install/server.ts | 3 +- packages/cli/src/commands/setup.ts | 11 ++++--- .../cli/src/commands/targets/add/desktop.ts | 3 +- .../cli/src/commands/targets/add/server.ts | 3 +- packages/cli/src/lib/installations.ts | 9 ++++-- 7 files changed, 47 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 30abd8d7..385ff825 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ **One CLI for all your chats.** Built for you and your agent — batteries included. [![npm](https://img.shields.io/npm/v/beeper-cli.svg?label=npm&color=6E56F8)](https://www.npmjs.com/package/beeper-cli) -[![license](https://img.shields.io/badge/license-MIT-6E56F8.svg)](https://github.com/beeper/desktop-api-cli/blob/main/packages/cli/LICENSE) -[![docs](https://img.shields.io/badge/docs-online-6E56F8.svg)](https://example.com) +[![license](https://img.shields.io/badge/license-MIT-6E56F8.svg)](https://github.com/beeper/cli/blob/main/packages/cli/LICENSE) +[![docs](https://img.shields.io/badge/docs-online-6E56F8.svg)](https://beeper.github.io/cli) [![built with Bun](https://img.shields.io/badge/built%20with-Bun-6E56F8.svg)](https://bun.sh) @@ -22,7 +22,7 @@ Facebook Messenger · X (Twitter) DMs · LinkedIn · Slack · Google Messages (RCS/SMS) · Google Chat · Matrix · IRC · Bluesky. Run `beeper bridges list` for the live list on your target. -📖 **[Read the docs](https://example.com)** · command manual: `beeper man` · open docs: `beeper docs` +📖 **[Read the docs](https://beeper.github.io/cli)** · command manual: `beeper man` · open docs: `beeper docs` ## Features @@ -202,19 +202,19 @@ WhatsApp, Telegram, Discord, iMessage, and the rest show up under `accounts list ## Documentation -Full documentation lives at **[example.com](https://example.com)** +Full documentation lives at **[beeper.github.io/cli](https://beeper.github.io/cli)** (built from [`docs/`](docs/) with Astro Starlight — a fully static site). | Topic | Page | Commands | | --- | --- | --- | -| **Setup + install** | [connect](https://example.com/connect/) · [install](https://example.com/install/) · [auth](https://example.com/auth/) | `setup` · `install desktop` · `install server` · `verify` · `status` · `doctor` · `auth status` | -| **Targets** | [targets](https://example.com/targets/) | `targets list` · `targets add desktop` · `targets add server` · `targets add remote` · `targets use` · `targets status` · `targets logs` | -| **Bridges + accounts** | [accounts](https://example.com/accounts/) | `bridges list` · `bridges show` · `accounts list` · `accounts add` · `accounts show` · `accounts use` · `accounts remove` | -| **Chats** | [chats](https://example.com/chats/) | `chats list` · `chats search` · `chats show` · `chats start` · `chats archive` · `chats pin` · `chats mute` · `chats priority` · `chats remind` · `chats rename` · `chats draft` · `chats focus` | -| **Messages** | [messages](https://example.com/messages/) · [send](https://example.com/send/) · [presence](https://example.com/presence/) | `messages list` · `messages search` · `messages export` · `send text` · `send file` · `send sticker` · `send voice` · `send react` · `presence` | -| **Contacts + media** | [contacts](https://example.com/contacts/) · [media](https://example.com/media/) · [export](https://example.com/export/) | `contacts list` · `contacts search` · `media download` · `export` | -| **Automation** | [scripting](https://example.com/scripting/) · [watch](https://example.com/watch/) · [rpc](https://example.com/rpc/) · [api](https://example.com/api/) | `watch` · `watch --webhook` · `rpc` · `man` · `api get` · `api post` · `api request` | -| **Maintenance** | [config](https://example.com/config/) · [update](https://example.com/update/) | `update` · `config` · `completion` · `docs` · `version` | +| **Setup + install** | [connect](https://beeper.github.io/cli/connect/) · [install](https://beeper.github.io/cli/install/) · [auth](https://beeper.github.io/cli/auth/) | `setup` · `install desktop` · `install server` · `verify` · `status` · `doctor` · `auth status` | +| **Targets** | [targets](https://beeper.github.io/cli/targets/) | `targets list` · `targets add desktop` · `targets add server` · `targets add remote` · `targets use` · `targets status` · `targets logs` | +| **Bridges + accounts** | [accounts](https://beeper.github.io/cli/accounts/) | `bridges list` · `bridges show` · `accounts list` · `accounts add` · `accounts show` · `accounts use` · `accounts remove` | +| **Chats** | [chats](https://beeper.github.io/cli/chats/) | `chats list` · `chats search` · `chats show` · `chats start` · `chats archive` · `chats pin` · `chats mute` · `chats priority` · `chats remind` · `chats rename` · `chats draft` · `chats focus` | +| **Messages** | [messages](https://beeper.github.io/cli/messages/) · [send](https://beeper.github.io/cli/send/) · [presence](https://beeper.github.io/cli/presence/) | `messages list` · `messages search` · `messages export` · `send text` · `send file` · `send sticker` · `send voice` · `send react` · `presence` | +| **Contacts + media** | [contacts](https://beeper.github.io/cli/contacts/) · [media](https://beeper.github.io/cli/media/) · [export](https://beeper.github.io/cli/export/) | `contacts list` · `contacts search` · `media download` · `export` | +| **Automation** | [scripting](https://beeper.github.io/cli/scripting/) · [watch](https://beeper.github.io/cli/watch/) · [rpc](https://beeper.github.io/cli/rpc/) · [api](https://beeper.github.io/cli/api/) | `watch` · `watch --webhook` · `rpc` · `man` · `api get` · `api post` · `api request` | +| **Maintenance** | [config](https://beeper.github.io/cli/config/) · [update](https://beeper.github.io/cli/update/) | `update` · `config` · `completion` · `docs` · `version` | Use `beeper docs` to open the CLI docs and `beeper man` to print the local command manual. To work on the docs site locally: `cd docs && bun install && bun run dev`. diff --git a/packages/cli/README.md b/packages/cli/README.md index f0690a0a..2d1e574d 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -5,8 +5,8 @@ **One CLI for all your chats.** Built for you and your agent — batteries included. [![npm](https://img.shields.io/npm/v/beeper-cli.svg?label=npm&color=6E56F8)](https://www.npmjs.com/package/beeper-cli) -[![license](https://img.shields.io/badge/license-MIT-6E56F8.svg)](https://github.com/beeper/desktop-api-cli/blob/main/packages/cli/LICENSE) -[![docs](https://img.shields.io/badge/docs-online-6E56F8.svg)](https://example.com) +[![license](https://img.shields.io/badge/license-MIT-6E56F8.svg)](https://github.com/beeper/cli/blob/main/packages/cli/LICENSE) +[![docs](https://img.shields.io/badge/docs-online-6E56F8.svg)](https://beeper.github.io/cli) [![built with Bun](https://img.shields.io/badge/built%20with-Bun-6E56F8.svg)](https://bun.sh) @@ -22,7 +22,7 @@ Facebook Messenger · X (Twitter) DMs · LinkedIn · Slack · Google Messages (RCS/SMS) · Google Chat · Matrix · IRC · Bluesky. Run `beeper bridges list` for the live list on your target. -📖 **[Read the docs](https://example.com)** · command manual: `beeper man` · open docs: `beeper docs` +📖 **[Read the docs](https://beeper.github.io/cli)** · command manual: `beeper man` · open docs: `beeper docs` ## Features @@ -202,19 +202,19 @@ WhatsApp, Telegram, Discord, iMessage, and the rest show up under `accounts list ## Documentation -Full documentation lives at **[example.com](https://example.com)** +Full documentation lives at **[beeper.github.io/cli](https://beeper.github.io/cli)** (built from [`docs/`](docs/) with Astro Starlight — a fully static site). | Topic | Page | Commands | | --- | --- | --- | -| **Setup + install** | [connect](https://example.com/connect/) · [install](https://example.com/install/) · [auth](https://example.com/auth/) | `setup` · `install desktop` · `install server` · `verify` · `status` · `doctor` · `auth status` | -| **Targets** | [targets](https://example.com/targets/) | `targets list` · `targets add desktop` · `targets add server` · `targets add remote` · `targets use` · `targets status` · `targets logs` | -| **Bridges + accounts** | [accounts](https://example.com/accounts/) | `bridges list` · `bridges show` · `accounts list` · `accounts add` · `accounts show` · `accounts use` · `accounts remove` | -| **Chats** | [chats](https://example.com/chats/) | `chats list` · `chats search` · `chats show` · `chats start` · `chats archive` · `chats pin` · `chats mute` · `chats priority` · `chats remind` · `chats rename` · `chats draft` · `chats focus` | -| **Messages** | [messages](https://example.com/messages/) · [send](https://example.com/send/) · [presence](https://example.com/presence/) | `messages list` · `messages search` · `messages export` · `send text` · `send file` · `send sticker` · `send voice` · `send react` · `presence` | -| **Contacts + media** | [contacts](https://example.com/contacts/) · [media](https://example.com/media/) · [export](https://example.com/export/) | `contacts list` · `contacts search` · `media download` · `export` | -| **Automation** | [scripting](https://example.com/scripting/) · [watch](https://example.com/watch/) · [rpc](https://example.com/rpc/) · [api](https://example.com/api/) | `watch` · `watch --webhook` · `rpc` · `man` · `api get` · `api post` · `api request` | -| **Maintenance** | [config](https://example.com/config/) · [update](https://example.com/update/) | `update` · `config` · `completion` · `docs` · `version` | +| **Setup + install** | [connect](https://beeper.github.io/cli/connect/) · [install](https://beeper.github.io/cli/install/) · [auth](https://beeper.github.io/cli/auth/) | `setup` · `install desktop` · `install server` · `verify` · `status` · `doctor` · `auth status` | +| **Targets** | [targets](https://beeper.github.io/cli/targets/) | `targets list` · `targets add desktop` · `targets add server` · `targets add remote` · `targets use` · `targets status` · `targets logs` | +| **Bridges + accounts** | [accounts](https://beeper.github.io/cli/accounts/) | `bridges list` · `bridges show` · `accounts list` · `accounts add` · `accounts show` · `accounts use` · `accounts remove` | +| **Chats** | [chats](https://beeper.github.io/cli/chats/) | `chats list` · `chats search` · `chats show` · `chats start` · `chats archive` · `chats pin` · `chats mute` · `chats priority` · `chats remind` · `chats rename` · `chats draft` · `chats focus` | +| **Messages** | [messages](https://beeper.github.io/cli/messages/) · [send](https://beeper.github.io/cli/send/) · [presence](https://beeper.github.io/cli/presence/) | `messages list` · `messages search` · `messages export` · `send text` · `send file` · `send sticker` · `send voice` · `send react` · `presence` | +| **Contacts + media** | [contacts](https://beeper.github.io/cli/contacts/) · [media](https://beeper.github.io/cli/media/) · [export](https://beeper.github.io/cli/export/) | `contacts list` · `contacts search` · `media download` · `export` | +| **Automation** | [scripting](https://beeper.github.io/cli/scripting/) · [watch](https://beeper.github.io/cli/watch/) · [rpc](https://beeper.github.io/cli/rpc/) · [api](https://beeper.github.io/cli/api/) | `watch` · `watch --webhook` · `rpc` · `man` · `api get` · `api post` · `api request` | +| **Maintenance** | [config](https://beeper.github.io/cli/config/) · [update](https://beeper.github.io/cli/update/) | `update` · `config` · `completion` · `docs` · `version` | Use `beeper docs` to open the CLI docs and `beeper man` to print the local command manual. To work on the docs site locally: `cd docs && bun install && bun run dev`. @@ -449,7 +449,7 @@ Flags: | `--oauth` | boolean | Authorize the target with browser OAuth/PKCE | | `--remote=` | option | Connect to a remote Beeper Desktop or Server URL | | `--server` | boolean | Set up a local Beeper Server target | -| `--server-env=` | option | Server feed environment: prod or staging Default: prod | +| `--server-env=` | option | Server environment: local, dev, staging, or prod Default: prod | | `--username=` | option | Username to use if setup creates a new account | Examples: @@ -498,7 +498,7 @@ Flags: | Flag | Type | Description | | --- | --- | --- | | `--channel=` | option | Server release channel Default: stable | -| `--server-env=` | option | Server feed environment: prod or staging Default: prod | +| `--server-env=` | option | Server environment: local, dev, staging, or prod Default: prod | Examples: @@ -591,7 +591,7 @@ Flags: | --- | --- | --- | | `--default` | boolean | Set this target as the default after creation | | `--port=` | option | TCP port the managed Desktop will expose its API on | -| `--server-env=` | option | Server feed environment: prod or staging Default: prod | +| `--server-env=` | option | Server environment: local, dev, staging, or prod Default: prod | Examples: @@ -620,7 +620,7 @@ Flags: | --- | --- | --- | | `--default` | boolean | Set this target as the default after creation | | `--port=` | option | TCP port the managed Server will expose its API on | -| `--server-env=` | option | Server feed environment: prod or staging Default: prod | +| `--server-env=` | option | Server environment: local, dev, staging, or prod Default: prod | Examples: diff --git a/packages/cli/src/commands/install/server.ts b/packages/cli/src/commands/install/server.ts index 55f96306..78c7bc65 100644 --- a/packages/cli/src/commands/install/server.ts +++ b/packages/cli/src/commands/install/server.ts @@ -3,12 +3,13 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { installServer, type InstallChannel } from '../../lib/installations.js' import { pathSetupHint } from '../../lib/env.js' import { printDryRun, printSuccess } from '../../lib/output.js' +import { SERVER_ENVIRONMENTS, normalizeServerEnv } from '../../lib/server-env.js' export default class SetupInstallServer extends BeeperCommand { static override summary = 'Install Beeper Server locally' static override flags = { channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Server release channel' }), - 'server-env': Flags.string({ default: 'prod', description: 'Server feed environment: prod or staging' }), + 'server-env': Flags.string({ default: 'prod', description: 'Server environment: local, dev, staging, or prod', options: [...SERVER_ENVIRONMENTS], parse: async value => normalizeServerEnv(value) }), } async run(): Promise { diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index fa26259c..6b9ded05 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -9,7 +9,7 @@ import { loginWithPKCE } from '../lib/oauth.js' import { findDesktopAppPath, launchDesktopApp, startProfile } from '../lib/profiles.js' import { interactiveEmailSetup } from '../lib/setup-login.js' import { renderStartupLogo } from '../lib/logo.js' -import { SERVER_ENV_API_BASE_URLS, normalizeServerEnv } from '../lib/server-env.js' +import { SERVER_ENVIRONMENTS, SERVER_ENV_API_BASE_URLS, normalizeServerEnv } from '../lib/server-env.js' import { builtInDesktopTargetID, createProfileTarget, @@ -36,7 +36,7 @@ export default class Setup extends BeeperCommand { desktop: Flags.boolean({ default: false, description: 'Set up a local Beeper Desktop target' }), install: Flags.boolean({ default: false, description: 'Allow installing missing managed runtime' }), channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Install release channel' }), - 'server-env': Flags.string({ default: 'prod', description: 'Server feed environment: prod or staging' }), + 'server-env': Flags.string({ default: 'prod', description: 'Server environment: local, dev, staging, or prod', options: [...SERVER_ENVIRONMENTS], parse: async value => normalizeServerEnv(value) }), email: Flags.string({ description: 'Sign in with an email address' }), username: Flags.string({ description: 'Username to use if setup creates a new account' }), } @@ -170,7 +170,8 @@ export default class Setup extends BeeperCommand { const readiness = await evaluateReadiness({ baseURL: target.baseURL, target: target.id }) if (readiness.state === 'target-unreachable' && target.type !== 'desktop') { if (flags.json || !process.stdin.isTTY) { - await printData(currentTargetBrokenOutput(target, readiness), flags.json ? 'json' : 'human') + const serverInstalled = await isServerInstalled() + await printData(currentTargetBrokenOutput(target, readiness, serverInstalled), flags.json ? 'json' : 'human') return } if (await this.handleBrokenCurrentTarget(target, readiness, flags)) return @@ -707,7 +708,7 @@ function installedServerAction(installed: boolean): { id: string; command: strin : action('install-server', 'beeper setup --server --install --yes') } -function currentTargetBrokenOutput(target: Target, readiness: Readiness): Record { +function currentTargetBrokenOutput(target: Target, readiness: Readiness, serverInstalled: boolean): Record { return { state: 'current-target-unreachable', message: `Beeper CLI is set up for ${target.name ?? target.id}, but it is not reachable.`, @@ -717,7 +718,7 @@ function currentTargetBrokenOutput(target: Target, readiness: Readiness): Record availableActions: [ action('retry-current', `beeper setup -t ${target.id}`), action('use-desktop', 'beeper setup --desktop'), - action('install-server', 'beeper setup --server --install --yes'), + installedServerAction(serverInstalled), action('connect-remote', 'beeper setup --remote '), ], } diff --git a/packages/cli/src/commands/targets/add/desktop.ts b/packages/cli/src/commands/targets/add/desktop.ts index 035127e3..2c1732d5 100644 --- a/packages/cli/src/commands/targets/add/desktop.ts +++ b/packages/cli/src/commands/targets/add/desktop.ts @@ -2,6 +2,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { createProfileTarget, readTarget, updateConfig } from '../../../lib/targets.js' import { printDryRun, printSuccess } from '../../../lib/output.js' +import { SERVER_ENVIRONMENTS, normalizeServerEnv } from '../../../lib/server-env.js' export default class TargetsAddDesktop extends BeeperCommand { static override summary = 'Add a managed Beeper Desktop target' @@ -9,7 +10,7 @@ export default class TargetsAddDesktop extends BeeperCommand { static override flags = { port: Flags.integer({ description: 'TCP port the managed Desktop will expose its API on' }), default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ default: 'prod', description: 'Server feed environment: prod or staging' }), + 'server-env': Flags.string({ default: 'prod', description: 'Server environment: local, dev, staging, or prod', options: [...SERVER_ENVIRONMENTS], parse: async value => normalizeServerEnv(value) }), } async run(): Promise { const { args, flags } = await this.parse(TargetsAddDesktop) diff --git a/packages/cli/src/commands/targets/add/server.ts b/packages/cli/src/commands/targets/add/server.ts index 59fb4d3a..ae3f4a3e 100644 --- a/packages/cli/src/commands/targets/add/server.ts +++ b/packages/cli/src/commands/targets/add/server.ts @@ -2,6 +2,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { createProfileTarget, readTarget, updateConfig } from '../../../lib/targets.js' import { printDryRun, printSuccess } from '../../../lib/output.js' +import { SERVER_ENVIRONMENTS, normalizeServerEnv } from '../../../lib/server-env.js' export default class TargetsAddServer extends BeeperCommand { static override summary = 'Add a managed Beeper Server target' @@ -9,7 +10,7 @@ export default class TargetsAddServer extends BeeperCommand { static override flags = { port: Flags.integer({ description: 'TCP port the managed Server will expose its API on' }), default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ default: 'prod', description: 'Server feed environment: prod or staging' }), + 'server-env': Flags.string({ default: 'prod', description: 'Server environment: local, dev, staging, or prod', options: [...SERVER_ENVIRONMENTS], parse: async value => normalizeServerEnv(value) }), } async run(): Promise { const { args, flags } = await this.parse(TargetsAddServer) diff --git a/packages/cli/src/lib/installations.ts b/packages/cli/src/lib/installations.ts index 8c7a394b..ad7b9995 100644 --- a/packages/cli/src/lib/installations.ts +++ b/packages/cli/src/lib/installations.ts @@ -148,8 +148,13 @@ export async function checkInstallationUpdate(installation: Installation): Promi export async function installDesktop(options: { channel?: InstallChannel; serverEnv?: string } = {}): Promise { const request = normalizeInstallRequest({ kind: 'desktop', channel: options.channel, serverEnv: options.serverEnv }) - if (request.serverEnv !== 'prod') throw new Error('Desktop non-production installs are not supported by the CLI.') - const feedURL = feedURLFor(request) + const feedRequest = request.serverEnv === 'prod' + ? request + : { ...request, serverEnv: 'prod' as const, apiBaseURL: SERVER_ENV_API_BASE_URLS.prod } + if (request.serverEnv !== feedRequest.serverEnv) { + process.stderr.write(`Desktop ${request.serverEnv} installs use the production update feed; the app will still launch against ${request.serverEnv}.\n`) + } + const feedURL = feedURLFor(feedRequest) const feed = await fetchFeed(feedURL) const downloadURL = feed.url if (!downloadURL) throw new Error('Desktop update feed did not include a download URL.') From f22ce037b3d69efc9e4aeaff9605fb6ad9ce159c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:51 +0200 Subject: [PATCH 16/22] fix documentation links --- docs/astro.config.mjs | 10 +++++----- packages/cli/scripts/generate-readme.ts | 9 ++++----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 2cf60acd..313d8f39 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -9,8 +9,8 @@ import starlight from '@astrojs/starlight'; // When you pick a home for the docs, set `site` to the canonical origin (used // for sitemap + canonical URLs) and, if serving from a sub-path, set `base`. export default defineConfig({ - site: 'https://example.com', - base: '/', + site: 'https://beeper.github.io', + base: '/cli/', output: 'static', trailingSlash: 'always', integrations: [ @@ -27,12 +27,12 @@ export default defineConfig({ { icon: 'github', label: 'GitHub', - href: 'https://github.com/beeper/desktop-api-cli', + href: 'https://github.com/beeper/cli', }, ], editLink: { baseUrl: - 'https://github.com/beeper/desktop-api-cli/edit/main/docs/', + 'https://github.com/beeper/cli/edit/main/docs/', }, customCss: ['./src/styles/theme.css'], // Starlight ships full-text search (Pagefind) and dark mode by default. @@ -84,7 +84,7 @@ export default defineConfig({ { label: 'Updating', link: '/update/' }, { label: 'Full command reference', - link: 'https://github.com/beeper/desktop-api-cli/blob/main/packages/cli/README.md', + link: 'https://github.com/beeper/cli/blob/main/packages/cli/README.md', attrs: { target: '_blank' }, }, { diff --git a/packages/cli/scripts/generate-readme.ts b/packages/cli/scripts/generate-readme.ts index 60b7a47a..cfc17595 100644 --- a/packages/cli/scripts/generate-readme.ts +++ b/packages/cli/scripts/generate-readme.ts @@ -33,11 +33,10 @@ const commandList = commands.map(command => { const examplesByID = new Map(commandManifest.map(item => [item.command, item.examples ?? []])); const commandSections = commands.map(command => commandSection(command)).join('\n\n'); -// Origin where the Astro docs site (in `docs/`) is published. Keep this in sync -// with `site` in `docs/astro.config.mjs`. Until the docs have a permanent home -// this is a placeholder; flip both in one commit when you pick a host. -const docsUrl = 'https://example.com'; -const repoUrl = 'https://github.com/beeper/desktop-api-cli'; +// Public URL where the Astro docs site (in `docs/`) is published. Keep this in +// sync with `site` + `base` in `docs/astro.config.mjs`. +const docsUrl = 'https://beeper.github.io/cli'; +const repoUrl = 'https://github.com/beeper/cli'; const intro = `
From db163a24f6feb77682cba30b706e989f16ea5f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:51:43 +0200 Subject: [PATCH 17/22] cleanup --- .claude/scheduled_tasks.lock | 1 + packages/cli/src/commands/api/get.ts | 5 +++-- packages/cli/src/commands/api/post.ts | 7 ++++--- packages/cli/src/commands/api/request.ts | 7 ++++--- .../cli/src/commands/auth/email/response.ts | 5 +++-- packages/cli/src/commands/auth/logout.ts | 5 +++-- packages/cli/src/commands/bridges/show.ts | 16 ++++------------ packages/cli/src/commands/chats/archive.ts | 1 - packages/cli/src/commands/chats/avatar.ts | 1 - .../cli/src/commands/chats/description.ts | 1 - packages/cli/src/commands/chats/focus.ts | 1 - packages/cli/src/commands/chats/mark-read.ts | 1 - .../cli/src/commands/chats/mark-unread.ts | 1 - .../cli/src/commands/chats/notify-anyway.ts | 1 - packages/cli/src/commands/chats/pin.ts | 1 - packages/cli/src/commands/chats/remind.ts | 1 - packages/cli/src/commands/chats/unarchive.ts | 1 - packages/cli/src/commands/chats/unmute.ts | 1 - packages/cli/src/commands/chats/unpin.ts | 1 - packages/cli/src/commands/chats/unremind.ts | 1 - packages/cli/src/commands/config/reset.ts | 5 +++-- packages/cli/src/commands/config/set.ts | 5 +++-- packages/cli/src/commands/doctor.ts | 6 +++++- packages/cli/src/commands/media/download.ts | 5 +++-- packages/cli/src/commands/messages/search.ts | 5 +++-- .../cli/src/commands/plugins/available.ts | 15 +++++++++------ packages/cli/src/commands/resolve/chat.ts | 13 ++----------- packages/cli/src/commands/setup.ts | 11 +++++------ packages/cli/src/commands/targets/disable.ts | 2 +- packages/cli/src/commands/targets/enable.ts | 2 +- packages/cli/src/commands/targets/list.ts | 9 +++------ packages/cli/src/commands/targets/logs.ts | 5 ++--- packages/cli/src/commands/targets/remove.ts | 7 ++----- packages/cli/src/commands/targets/restart.ts | 2 +- packages/cli/src/commands/targets/show.ts | 11 ++++------- packages/cli/src/commands/targets/start.ts | 2 +- packages/cli/src/commands/targets/status.ts | 10 ++++------ packages/cli/src/commands/targets/stop.ts | 2 +- packages/cli/src/commands/targets/use.ts | 7 ++----- packages/cli/src/commands/verify/cancel.ts | 2 +- packages/cli/src/lib/command-metadata.ts | 19 ++++++++++--------- packages/cli/src/lib/command.ts | 7 +------ packages/cli/src/lib/export/index.ts | 2 -- packages/cli/src/lib/ink/render.tsx | 7 +++---- packages/cli/src/lib/ink/theme.ts | 3 +-- packages/cli/src/lib/installations.ts | 4 +--- packages/cli/src/lib/profiles.ts | 17 +++-------------- packages/cli/src/lib/resolve.ts | 14 +++----------- packages/cli/src/lib/setup-login.ts | 5 +++-- 49 files changed, 103 insertions(+), 160 deletions(-) create mode 100644 .claude/scheduled_tasks.lock diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 00000000..b9663f66 --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"2da33c33-52d4-4916-961e-72cd6ae9594a","pid":18713,"procStart":"Sat May 30 16:14:01 2026","acquiredAt":1780159461036} \ No newline at end of file diff --git a/packages/cli/src/commands/api/get.ts b/packages/cli/src/commands/api/get.ts index a36515d4..850156d4 100644 --- a/packages/cli/src/commands/api/get.ts +++ b/packages/cli/src/commands/api/get.ts @@ -16,11 +16,12 @@ export default class ApiGet extends BeeperCommand { async run(): Promise { const { args, flags } = await this.parse(ApiGet) + const format = flags.json ? 'json' : 'human' if (flags['no-auth']) { - await printData(await appRequest('GET', args.path, { baseURL: flags['base-url'], target: flags.target, token: false }), flags.json ? 'json' : 'human') + await printData(await appRequest('GET', args.path, { baseURL: flags['base-url'], target: flags.target, token: false }), format) return } const client = await createClient(flags) - await printData(await client.get(args.path), flags.json ? 'json' : 'human') + await printData(await client.get(args.path), format) } } diff --git a/packages/cli/src/commands/api/post.ts b/packages/cli/src/commands/api/post.ts index 429f9e22..c381a3bf 100644 --- a/packages/cli/src/commands/api/post.ts +++ b/packages/cli/src/commands/api/post.ts @@ -18,6 +18,7 @@ export default class ApiPost extends BeeperCommand { async run(): Promise { const { args, flags } = await this.parse(ApiPost) ensureWritable(flags) + const format = flags.json ? 'json' : 'human' let body: Record try { body = JSON.parse(flags.body) as Record @@ -25,14 +26,14 @@ export default class ApiPost extends BeeperCommand { throw new Error(`--body is not valid JSON: ${flags.body}`) } if (flags['dry-run']) { - await printDryRun('api.post', { method: 'POST', path: args.path, body, noAuth: flags['no-auth'], target: flags.target, baseURL: flags['base-url'] }, flags.json ? 'json' : 'human') + await printDryRun('api.post', { method: 'POST', path: args.path, body, noAuth: flags['no-auth'], target: flags.target, baseURL: flags['base-url'] }, format) return } if (flags['no-auth']) { - await printData(await appRequest('POST', args.path, { baseURL: flags['base-url'], body, target: flags.target, token: false }), flags.json ? 'json' : 'human') + await printData(await appRequest('POST', args.path, { baseURL: flags['base-url'], body, target: flags.target, token: false }), format) return } const client = await createClient(flags) - await printData(await client.post(args.path, { body }), flags.json ? 'json' : 'human') + await printData(await client.post(args.path, { body }), format) } } diff --git a/packages/cli/src/commands/api/request.ts b/packages/cli/src/commands/api/request.ts index f75cd3d5..c551e450 100644 --- a/packages/cli/src/commands/api/request.ts +++ b/packages/cli/src/commands/api/request.ts @@ -19,15 +19,16 @@ export default class ApiRequest extends BeeperCommand { const { args, flags } = await this.parse(ApiRequest) const method = args.method as AppRequestMethod if (method !== 'GET') ensureWritable(flags) + const format = flags.json ? 'json' : 'human' const body = flags.body ? JSON.parse(flags.body) as Record : undefined if (flags['dry-run'] && method !== 'GET') { - await printDryRun('api.request', { method, path: args.path, body, noAuth: flags['no-auth'], target: flags.target, baseURL: flags['base-url'] }, flags.json ? 'json' : 'human') + await printDryRun('api.request', { method, path: args.path, body, noAuth: flags['no-auth'], target: flags.target, baseURL: flags['base-url'] }, format) return } if (flags['no-auth']) { - await printData(await appRequest(method, args.path, { baseURL: flags['base-url'], body, target: flags.target, token: false }), flags.json ? 'json' : 'human') + await printData(await appRequest(method, args.path, { baseURL: flags['base-url'], body, target: flags.target, token: false }), format) return } - await printData(await appRequest(method, args.path, { baseURL: flags['base-url'], body, target: flags.target }), flags.json ? 'json' : 'human') + await printData(await appRequest(method, args.path, { baseURL: flags['base-url'], body, target: flags.target }), format) } } diff --git a/packages/cli/src/commands/auth/email/response.ts b/packages/cli/src/commands/auth/email/response.ts index f4a34d9d..3268af76 100644 --- a/packages/cli/src/commands/auth/email/response.ts +++ b/packages/cli/src/commands/auth/email/response.ts @@ -16,9 +16,10 @@ export default class AuthEmailResponse extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(AuthEmailResponse) ensureWritable(flags) + const format = flags.json ? 'json' : 'human' const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] }) if (flags['dry-run']) { - await printDryRun('auth.email.response', { target: target.id, baseURL: target.baseURL, setupRequestID: flags['setup-request-id'], username: flags.username, yes: flags.yes }, flags.json ? 'json' : 'human') + await printDryRun('auth.email.response', { target: target.id, baseURL: target.baseURL, setupRequestID: flags['setup-request-id'], username: flags.username, yes: flags.yes }, format) return } const data = await finishEmailSetup(target, { @@ -28,6 +29,6 @@ export default class AuthEmailResponse extends BeeperCommand { username: flags.username, yes: flags.yes, }) - await printData(data, flags.json ? 'json' : 'human') + await printData(data, format) } } diff --git a/packages/cli/src/commands/auth/logout.ts b/packages/cli/src/commands/auth/logout.ts index d4e9e8ab..fc8756d5 100644 --- a/packages/cli/src/commands/auth/logout.ts +++ b/packages/cli/src/commands/auth/logout.ts @@ -8,10 +8,11 @@ export default class AuthLogout extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(AuthLogout) if (!flags['dry-run']) ensureWritable(flags) + const format = flags.json ? 'json' : 'human' const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] }) const token = target.auth?.accessToken if (flags['dry-run']) { - await printDryRun('auth.logout', { target: target.id, baseURL: target.baseURL, hadToken: Boolean(token), revokeToken: Boolean(token) }, flags.json ? 'json' : 'human') + await printDryRun('auth.logout', { target: target.id, baseURL: target.baseURL, hadToken: Boolean(token), revokeToken: Boolean(token) }, format) return } if (process.env.BEEPER_ACCESS_TOKEN && !target.auth?.accessToken) { @@ -28,6 +29,6 @@ export default class AuthLogout extends BeeperCommand { revoked = Boolean(response?.ok) await clearTargetAuth(target) } - await printSuccess({ message: 'Logged out', detail: token ? 'local token cleared' : 'no token was stored', data: { revoked, hadToken: Boolean(token) } }, flags.json ? 'json' : 'human') + await printSuccess({ message: 'Logged out', detail: token ? 'local token cleared' : 'no token was stored', data: { revoked, hadToken: Boolean(token) } }, format) } } diff --git a/packages/cli/src/commands/bridges/show.ts b/packages/cli/src/commands/bridges/show.ts index 76840af4..592a32eb 100644 --- a/packages/cli/src/commands/bridges/show.ts +++ b/packages/cli/src/commands/bridges/show.ts @@ -30,21 +30,13 @@ export default class BridgesShow extends BeeperCommand { function resolveBridge(items: Array>, input: string): Record { const normalizedInput = normalize(input) - const exact = items.filter(item => [ - item.id, - item.displayName, - item.network, - item.type, - ].some(value => normalize(value) === normalizedInput)) + const fields = (item: Record): unknown[] => [item.id, item.displayName, item.network, item.type] + + const exact = items.filter(item => fields(item).some(value => normalize(value) === normalizedInput)) if (exact.length === 1) return exact[0]! if (exact.length > 1) throw ambiguousBridge(input, exact) - const partial = items.filter(item => [ - item.id, - item.displayName, - item.network, - item.type, - ].some(value => normalize(value).includes(normalizedInput))) + const partial = items.filter(item => fields(item).some(value => normalize(value).includes(normalizedInput))) if (partial.length === 1) return partial[0]! if (partial.length > 1) throw ambiguousBridge(input, partial) diff --git a/packages/cli/src/commands/chats/archive.ts b/packages/cli/src/commands/chats/archive.ts index 150a2883..da099632 100644 --- a/packages/cli/src/commands/chats/archive.ts +++ b/packages/cli/src/commands/chats/archive.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/avatar.ts b/packages/cli/src/commands/chats/avatar.ts index 01da3592..7c97c2bc 100644 --- a/packages/cli/src/commands/chats/avatar.ts +++ b/packages/cli/src/commands/chats/avatar.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/description.ts b/packages/cli/src/commands/chats/description.ts index 1f081774..0cb2022a 100644 --- a/packages/cli/src/commands/chats/description.ts +++ b/packages/cli/src/commands/chats/description.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/focus.ts b/packages/cli/src/commands/chats/focus.ts index 3903edf8..c9e811b8 100644 --- a/packages/cli/src/commands/chats/focus.ts +++ b/packages/cli/src/commands/chats/focus.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/mark-read.ts b/packages/cli/src/commands/chats/mark-read.ts index 72ddf591..82dceed9 100644 --- a/packages/cli/src/commands/chats/mark-read.ts +++ b/packages/cli/src/commands/chats/mark-read.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/mark-unread.ts b/packages/cli/src/commands/chats/mark-unread.ts index 73acc5c7..e27c9839 100644 --- a/packages/cli/src/commands/chats/mark-unread.ts +++ b/packages/cli/src/commands/chats/mark-unread.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/notify-anyway.ts b/packages/cli/src/commands/chats/notify-anyway.ts index 418d5237..4174098d 100644 --- a/packages/cli/src/commands/chats/notify-anyway.ts +++ b/packages/cli/src/commands/chats/notify-anyway.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/pin.ts b/packages/cli/src/commands/chats/pin.ts index 642265e9..7ee34d16 100644 --- a/packages/cli/src/commands/chats/pin.ts +++ b/packages/cli/src/commands/chats/pin.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/remind.ts b/packages/cli/src/commands/chats/remind.ts index 4bd61a01..f2599030 100644 --- a/packages/cli/src/commands/chats/remind.ts +++ b/packages/cli/src/commands/chats/remind.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printDryRun, printSuccess } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/unarchive.ts b/packages/cli/src/commands/chats/unarchive.ts index 2da004c4..17f55f9e 100644 --- a/packages/cli/src/commands/chats/unarchive.ts +++ b/packages/cli/src/commands/chats/unarchive.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/unmute.ts b/packages/cli/src/commands/chats/unmute.ts index 46148e78..6466e892 100644 --- a/packages/cli/src/commands/chats/unmute.ts +++ b/packages/cli/src/commands/chats/unmute.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/unpin.ts b/packages/cli/src/commands/chats/unpin.ts index 2839626d..1053c681 100644 --- a/packages/cli/src/commands/chats/unpin.ts +++ b/packages/cli/src/commands/chats/unpin.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/unremind.ts b/packages/cli/src/commands/chats/unremind.ts index 92dd5a73..5fa5e9ef 100644 --- a/packages/cli/src/commands/chats/unremind.ts +++ b/packages/cli/src/commands/chats/unremind.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printDryRun, printSuccess } from '../../lib/output.js' diff --git a/packages/cli/src/commands/config/reset.ts b/packages/cli/src/commands/config/reset.ts index e3066dad..8d3b9636 100644 --- a/packages/cli/src/commands/config/reset.ts +++ b/packages/cli/src/commands/config/reset.ts @@ -8,11 +8,12 @@ export default class ConfigReset extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(ConfigReset) ensureWritable(flags) + const format = flags.json ? 'json' : 'human' if (flags['dry-run']) { - await printDryRun('config.reset', {}, flags.json ? 'json' : 'human') + await printDryRun('config.reset', {}, format) return } await resetConfig() - await printSuccess({ message: 'Config reset' }, flags.json ? 'json' : 'human') + await printSuccess({ message: 'Config reset' }, format) } } diff --git a/packages/cli/src/commands/config/set.ts b/packages/cli/src/commands/config/set.ts index 1bc9e5b2..2cd95099 100644 --- a/packages/cli/src/commands/config/set.ts +++ b/packages/cli/src/commands/config/set.ts @@ -13,9 +13,10 @@ export default class ConfigSet extends BeeperCommand { async run(): Promise { const { args, flags } = await this.parse(ConfigSet) ensureWritable(flags) + const format = flags.json ? 'json' : 'human' const nextValue = args.value === '' ? undefined : args.value if (flags['dry-run']) { - await printDryRun('config.set', { [args.key]: nextValue }, flags.json ? 'json' : 'human') + await printDryRun('config.set', { [args.key]: nextValue }, format) return } await updateConfig(config => ({ ...config, [args.key]: nextValue })) @@ -23,6 +24,6 @@ export default class ConfigSet extends BeeperCommand { message: nextValue === undefined ? `Cleared ${args.key}` : `Set ${args.key}`, detail: nextValue, data: { [args.key]: nextValue }, - }, flags.json ? 'json' : 'human') + }, format) } } diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts index e0b428d1..a07a5581 100644 --- a/packages/cli/src/commands/doctor.ts +++ b/packages/cli/src/commands/doctor.ts @@ -10,7 +10,11 @@ export default class Doctor extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(Doctor) const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] }) - const checks = { target: await targetLiveStatus(target), readiness: await evaluateReadiness({ baseURL: target.baseURL, target: target.id }) } + const [targetStatus, readiness] = await Promise.all([ + targetLiveStatus(target), + evaluateReadiness({ baseURL: target.baseURL, target: target.id }), + ]) + const checks = { target: targetStatus, readiness } await printData({ ok: checks.readiness.state === 'ready', checks }, flags.json ? 'json' : 'human') if (checks.readiness.state !== 'ready') this.exit(ExitCodes.NotReady) } diff --git a/packages/cli/src/commands/media/download.ts b/packages/cli/src/commands/media/download.ts index a6c01001..3c86c8d7 100644 --- a/packages/cli/src/commands/media/download.ts +++ b/packages/cli/src/commands/media/download.ts @@ -12,9 +12,10 @@ export default class MediaDownload extends BeeperCommand { } async run(): Promise { const { args, flags } = await this.parse(MediaDownload) + const format = flags.json ? 'json' : 'human' if (flags['dry-run'] && flags.out !== '-') { ensureWritable(flags) - await printDryRun('media.download', { url: args.url, out: flags.out }, flags.json ? 'json' : 'human') + await printDryRun('media.download', { url: args.url, out: flags.out }, format) return } const client = await createClient(flags) @@ -28,6 +29,6 @@ export default class MediaDownload extends BeeperCommand { await mkdir(flags.out, { recursive: true }) const path = join(flags.out, basename(new URL(args.url).pathname) || 'media') await writeFile(path, buffer) - await printSuccess({ message: 'Downloaded media', detail: path, data: { path, bytes: buffer.length } }, flags.json ? 'json' : 'human') + await printSuccess({ message: 'Downloaded media', detail: path, data: { path, bytes: buffer.length } }, format) } } diff --git a/packages/cli/src/commands/messages/search.ts b/packages/cli/src/commands/messages/search.ts index fe82ed54..2515087b 100644 --- a/packages/cli/src/commands/messages/search.ts +++ b/packages/cli/src/commands/messages/search.ts @@ -60,11 +60,12 @@ export default class MessagesSearch extends BeeperCommand { } const useSpinner = !isMachineReadableOutput(flags.ids ? 'ids' : flags.json ? 'json' : 'human') const label = args.query ? `Searching messages for "${args.query}"…` : 'Searching messages…' + const collect = () => collectPage(client.messages.search(params), flags.limit) const items = useSpinner - ? await withSpinner(label, () => collectPage(client.messages.search(params), flags.limit), { + ? await withSpinner(label, collect, { done: value => `${value.length} match${value.length === 1 ? '' : 'es'}`, }) - : await collectPage(client.messages.search(params), flags.limit) + : await collect() if (flags.ids) { printIDs(items) return diff --git a/packages/cli/src/commands/plugins/available.ts b/packages/cli/src/commands/plugins/available.ts index 4147b30b..e2f16dd6 100644 --- a/packages/cli/src/commands/plugins/available.ts +++ b/packages/cli/src/commands/plugins/available.ts @@ -9,12 +9,15 @@ export default class PluginsAvailable extends BeeperCommand { const { flags } = await this.parse(PluginsAvailable) const installed = new Set(this.config.plugins.keys()) const corePlugins = new Set((this.config.pjson.oclif.plugins ?? []) as string[]) - const plugins = recommendedPlugins.map(plugin => ({ - ...plugin, - installed: installed.has(plugin.name), - status: installed.has(plugin.name) ? 'installed' : 'not installed', - core: corePlugins.has(plugin.name), - })) + const plugins = recommendedPlugins.map(plugin => { + const isInstalled = installed.has(plugin.name) + return { + ...plugin, + installed: isInstalled, + status: isInstalled ? 'installed' : 'not installed', + core: corePlugins.has(plugin.name), + } + }) await printData(plugins, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/resolve/chat.ts b/packages/cli/src/commands/resolve/chat.ts index 82240f61..4586e190 100644 --- a/packages/cli/src/commands/resolve/chat.ts +++ b/packages/cli/src/commands/resolve/chat.ts @@ -2,7 +2,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { notFound } from '../../lib/errors.js' -import { printData } from '../../lib/output.js' +import { collectPage, printData } from '../../lib/output.js' import { resolveAccountIDs } from '../../lib/resolve.js' export default class ResolveChat extends BeeperCommand { @@ -20,7 +20,7 @@ export default class ResolveChat extends BeeperCommand { const { args, flags } = await this.parse(ResolveChat) const client = await createClient(flags) const accountIDs = await resolveAccountIDs(client, flags.account, { allowMultiplePerInput: true }) - const candidates = await collect(client.chats.search({ accountIDs, query: args.selector, scope: 'titles' }), flags.limit) + const candidates = await collectPage(client.chats.search({ accountIDs, query: args.selector, scope: 'titles' }), flags.limit) const normalized = normalize(args.selector) const exact = candidates.filter(chat => normalize(chat.id) === normalized || @@ -42,15 +42,6 @@ export default class ResolveChat extends BeeperCommand { type Chat = Record -async function collect(iterable: AsyncIterable, limit: number): Promise { - const items: Chat[] = [] - for await (const item of iterable) { - items.push(item as Chat) - if (items.length >= limit) break - } - return items -} - function chatCandidate(chat: Chat, pick: number): Record { return { pick, diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 6b9ded05..f1ab07a3 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -137,21 +137,21 @@ export default class Setup extends BeeperCommand { } else if (flags.json || !process.stdin.isTTY) { await printData(setupStateOutput(detected, target), flags.json ? 'json' : 'human') return - } else if (detected.kind === 'installed-not-running' && !flags.json && process.stdin.isTTY) { + } else if (detected.kind === 'installed-not-running') { printStatus('Found Beeper Desktop on this device.', 'installed, not running') const shouldLaunch = flags.yes || await promptYesNoDefaultYes('Launch Beeper Desktop now?') if (shouldLaunch) { await launchAndPoll(target, setupCmd, flags) return } - } else if (detected.kind === 'running-signed-out' && !flags.json && process.stdin.isTTY) { + } else if (detected.kind === 'running-signed-out') { printStatus('Found Beeper Desktop on this device.', 'running, signed out') const shouldOpen = flags.yes || await promptYesNoDefaultYes('Open Beeper Desktop so you can sign in?') if (shouldOpen) { await launchAndPoll(target, setupCmd, flags) return } - } else if (detected.kind === 'session-unreadable' && !flags.json && process.stdin.isTTY) { + } else if (detected.kind === 'session-unreadable') { printStatus('Found Beeper Desktop on this device.', 'signed in, but CLI could not read the local session') process.stdout.write('You can still connect through Beeper Desktop.\n') if (flags.debug) process.stdout.write(`\n${detected.reason}\n`) @@ -161,7 +161,7 @@ export default class Setup extends BeeperCommand { await this.setupOAuth(target, flags) return } - } else if (detected.kind === 'not-installed' && !flags.json && process.stdin.isTTY) { + } else if (detected.kind === 'not-installed') { await this.setupFromChoice(flags) return } @@ -234,8 +234,7 @@ export default class Setup extends BeeperCommand { private async setupManaged(type: 'desktop' | 'server', flags: SetupFlags): Promise { if (flags.install) { if ((flags.json || !process.stdin.isTTY) && !flags.yes) throw new Error('Install requires --install --yes in non-interactive mode.') - if (type === 'desktop') await installWithCopy('desktop', flags) - else await installWithCopy('server', flags) + await installWithCopy(type, flags) } const id = flags.target ?? type const target = await readTarget(id) ?? await createProfileTarget(type, id, { serverEnv: flags['server-env'], port: undefined }) diff --git a/packages/cli/src/commands/targets/disable.ts b/packages/cli/src/commands/targets/disable.ts index 80f61a5a..b2afabc0 100644 --- a/packages/cli/src/commands/targets/disable.ts +++ b/packages/cli/src/commands/targets/disable.ts @@ -1,6 +1,6 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { readTarget, resolveTarget } from '../../lib/targets.js' +import { resolveTarget } from '../../lib/targets.js' import { assertServerProfile, disableProfile } from '../../lib/profiles.js' import { printDryRun, printSuccess } from '../../lib/output.js' diff --git a/packages/cli/src/commands/targets/enable.ts b/packages/cli/src/commands/targets/enable.ts index 468f747a..89ea1e68 100644 --- a/packages/cli/src/commands/targets/enable.ts +++ b/packages/cli/src/commands/targets/enable.ts @@ -1,6 +1,6 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { readTarget, resolveTarget } from '../../lib/targets.js' +import { resolveTarget } from '../../lib/targets.js' import { assertServerProfile, enableProfile } from '../../lib/profiles.js' import { printDryRun, printSuccess } from '../../lib/output.js' diff --git a/packages/cli/src/commands/targets/list.ts b/packages/cli/src/commands/targets/list.ts index e8721e76..9b05baa2 100644 --- a/packages/cli/src/commands/targets/list.ts +++ b/packages/cli/src/commands/targets/list.ts @@ -1,10 +1,7 @@ -import { Args, Flags } from '@oclif/core' -import { readFile } from 'node:fs/promises' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { builtInDesktopTargetID, createProfileTarget, listTargets, readConfig, readTarget, removeTarget, resolveTarget, updateConfig, writeTarget, type Target } from '../../lib/targets.js' -import { disableProfile, enableProfile, profileErrorLogPath, profileLogPath, profileStatus, startProfile, stopProfile } from '../../lib/profiles.js' +import { BeeperCommand } from '../../lib/command.js' +import { builtInDesktopTargetID, listTargets, readConfig, type Target } from '../../lib/targets.js' import { targetLiveStatus } from '../../lib/target-status.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData } from '../../lib/output.js' export default class TargetsList extends BeeperCommand { static override summary = 'List configured Beeper targets' diff --git a/packages/cli/src/commands/targets/logs.ts b/packages/cli/src/commands/targets/logs.ts index 21c23dc2..2844eb8b 100644 --- a/packages/cli/src/commands/targets/logs.ts +++ b/packages/cli/src/commands/targets/logs.ts @@ -2,7 +2,7 @@ import { Args, Flags } from '@oclif/core' import { readdir, readFile, stat } from 'node:fs/promises' import { join } from 'node:path' import { BeeperCommand } from '../../lib/command.js' -import { customTargetID, readTarget, resolveTarget } from '../../lib/targets.js' +import { customTargetID, resolveTarget } from '../../lib/targets.js' import { desktopLogDir, profileErrorLogPath, profileLogPath } from '../../lib/profiles.js' export default class TargetsLogs extends BeeperCommand { @@ -26,8 +26,7 @@ export default class TargetsLogs extends BeeperCommand { } const files = await listLogFiles(desktopLogDir(target.managed ? target : undefined)) const selected = flags.all ? files : files.slice(0, flags.files) - for (const file of files) { - if (!selected.includes(file)) continue + for (const file of selected) { await printLogFile(file, flags.lines) } } diff --git a/packages/cli/src/commands/targets/remove.ts b/packages/cli/src/commands/targets/remove.ts index 82cbb948..988e25ad 100644 --- a/packages/cli/src/commands/targets/remove.ts +++ b/packages/cli/src/commands/targets/remove.ts @@ -1,9 +1,6 @@ -import { Args, Flags } from '@oclif/core' -import { readFile } from 'node:fs/promises' +import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createProfileTarget, listTargets, readConfig, readTarget, removeTarget, resolveTarget, updateConfig, writeTarget, type Target } from '../../lib/targets.js' -import { disableProfile, enableProfile, profileErrorLogPath, profileLogPath, profileStatus, startProfile, stopProfile } from '../../lib/profiles.js' -import { targetLiveStatus } from '../../lib/target-status.js' +import { removeTarget } from '../../lib/targets.js' import { printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsRemove extends BeeperCommand { diff --git a/packages/cli/src/commands/targets/restart.ts b/packages/cli/src/commands/targets/restart.ts index a97ccea5..30966768 100644 --- a/packages/cli/src/commands/targets/restart.ts +++ b/packages/cli/src/commands/targets/restart.ts @@ -1,6 +1,6 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { readTarget, resolveTarget } from '../../lib/targets.js' +import { resolveTarget } from '../../lib/targets.js' import { assertServerProfile, startProfile, stopProfile } from '../../lib/profiles.js' import { printDryRun, printSuccess } from '../../lib/output.js' diff --git a/packages/cli/src/commands/targets/show.ts b/packages/cli/src/commands/targets/show.ts index 2fc64d07..892672c4 100644 --- a/packages/cli/src/commands/targets/show.ts +++ b/packages/cli/src/commands/targets/show.ts @@ -1,10 +1,7 @@ -import { Args, Flags } from '@oclif/core' -import { readFile } from 'node:fs/promises' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createProfileTarget, listTargets, readConfig, readTarget, removeTarget, resolveTarget, updateConfig, writeTarget, type Target } from '../../lib/targets.js' -import { disableProfile, enableProfile, profileErrorLogPath, profileLogPath, profileStatus, startProfile, stopProfile } from '../../lib/profiles.js' -import { targetLiveStatus } from '../../lib/target-status.js' -import { printData, printSuccess } from '../../lib/output.js' +import { Args } from '@oclif/core' +import { BeeperCommand } from '../../lib/command.js' +import { resolveTarget } from '../../lib/targets.js' +import { printData } from '../../lib/output.js' export default class TargetsShow extends BeeperCommand { static override summary = 'Show target details' diff --git a/packages/cli/src/commands/targets/start.ts b/packages/cli/src/commands/targets/start.ts index 956d621f..b659aaaf 100644 --- a/packages/cli/src/commands/targets/start.ts +++ b/packages/cli/src/commands/targets/start.ts @@ -1,6 +1,6 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { customTargetID, readTarget, resolveTarget } from '../../lib/targets.js' +import { customTargetID, resolveTarget } from '../../lib/targets.js' import { launchDesktopApp, startProfile } from '../../lib/profiles.js' import { printDryRun, printSuccess } from '../../lib/output.js' diff --git a/packages/cli/src/commands/targets/status.ts b/packages/cli/src/commands/targets/status.ts index 531ce441..a8cf970d 100644 --- a/packages/cli/src/commands/targets/status.ts +++ b/packages/cli/src/commands/targets/status.ts @@ -1,10 +1,8 @@ -import { Args, Flags } from '@oclif/core' -import { readFile } from 'node:fs/promises' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createProfileTarget, listTargets, readConfig, readTarget, removeTarget, resolveTarget, updateConfig, writeTarget, type Target } from '../../lib/targets.js' -import { disableProfile, enableProfile, profileErrorLogPath, profileLogPath, profileStatus, startProfile, stopProfile } from '../../lib/profiles.js' +import { Args } from '@oclif/core' +import { BeeperCommand } from '../../lib/command.js' +import { resolveTarget } from '../../lib/targets.js' import { targetLiveStatus } from '../../lib/target-status.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData } from '../../lib/output.js' export default class TargetsStatus extends BeeperCommand { static override summary = 'Check endpoint and process reachability for a target' diff --git a/packages/cli/src/commands/targets/stop.ts b/packages/cli/src/commands/targets/stop.ts index 65444ad0..31d96a95 100644 --- a/packages/cli/src/commands/targets/stop.ts +++ b/packages/cli/src/commands/targets/stop.ts @@ -1,6 +1,6 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { readTarget, resolveTarget } from '../../lib/targets.js' +import { resolveTarget } from '../../lib/targets.js' import { assertServerProfile, stopProfile } from '../../lib/profiles.js' import { printDryRun, printSuccess } from '../../lib/output.js' diff --git a/packages/cli/src/commands/targets/use.ts b/packages/cli/src/commands/targets/use.ts index 92e385d2..3dbeca6b 100644 --- a/packages/cli/src/commands/targets/use.ts +++ b/packages/cli/src/commands/targets/use.ts @@ -1,9 +1,6 @@ -import { Args, Flags } from '@oclif/core' -import { readFile } from 'node:fs/promises' +import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createProfileTarget, listTargets, readConfig, readTarget, removeTarget, resolveTarget, updateConfig, writeTarget, type Target } from '../../lib/targets.js' -import { disableProfile, enableProfile, profileErrorLogPath, profileLogPath, profileStatus, startProfile, stopProfile } from '../../lib/profiles.js' -import { targetLiveStatus } from '../../lib/target-status.js' +import { readTarget, updateConfig } from '../../lib/targets.js' import { printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsUse extends BeeperCommand { diff --git a/packages/cli/src/commands/verify/cancel.ts b/packages/cli/src/commands/verify/cancel.ts index ce8df653..a2c50f02 100644 --- a/packages/cli/src/commands/verify/cancel.ts +++ b/packages/cli/src/commands/verify/cancel.ts @@ -10,11 +10,11 @@ export default class AuthVerifyCancel extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(AuthVerifyCancel) ensureWritable(flags) - const client = await createClient(flags) if (flags['dry-run']) { await printDryRun('verify.cancel', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') return } + const client = await createClient(flags) await printData(await client.app.verifications.cancel(flags.id ?? 'active', {}), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/lib/command-metadata.ts b/packages/cli/src/lib/command-metadata.ts index 3d3f3c4a..214abf19 100644 --- a/packages/cli/src/lib/command-metadata.ts +++ b/packages/cli/src/lib/command-metadata.ts @@ -6,19 +6,20 @@ export type CommandMetadata = { related: string[] } +const mutatingRoots = new Set(['export', 'install', 'presence', 'send', 'setup', 'update']) +const mutatingVerbs = new Set([ + 'add', 'approve', 'archive', 'avatar', 'cancel', 'delete', 'description', 'disable', 'disappear', 'download', + 'draft', 'edit', 'enable', 'export', 'focus', 'logout', 'mark-read', 'mark-unread', 'mute', 'notify-anyway', + 'pin', 'post', 'priority', 'qr-confirm', 'qr-scan', 'recovery-key', 'remind', 'remove', 'rename', 'reset', + 'reset-recovery-key', 'response', 'restart', 'sas', 'sas-confirm', 'set', 'start', 'stop', 'unarchive', + 'unmute', 'unpin', 'unremind', 'use', +]) +const localOnly = new Set(['completion', 'config', 'docs', 'man', 'schema', 'version']) + export function metadataForCommand(command: string): CommandMetadata { const parts = command.split(' ') const root = parts[0] ?? '' - const mutatingRoots = new Set(['export', 'install', 'presence', 'send', 'setup', 'update']) - const mutatingVerbs = new Set([ - 'add', 'approve', 'archive', 'avatar', 'cancel', 'delete', 'description', 'disable', 'disappear', 'download', - 'draft', 'edit', 'enable', 'export', 'focus', 'logout', 'mark-read', 'mark-unread', 'mute', 'notify-anyway', - 'pin', 'post', 'priority', 'qr-confirm', 'qr-scan', 'recovery-key', 'remind', 'remove', 'rename', 'reset', - 'reset-recovery-key', 'response', 'restart', 'sas', 'sas-confirm', 'set', 'start', 'stop', 'unarchive', - 'unmute', 'unpin', 'unremind', 'use', - ]) const mutates = command === 'verify' || command === 'api request' || mutatingRoots.has(root) || parts.some(part => mutatingVerbs.has(part ?? '')) - const localOnly = new Set(['completion', 'config', 'docs', 'man', 'schema', 'version']) const requiresAuth = !localOnly.has(root) && command !== 'targets list' && !command.startsWith('targets add') && !command.startsWith('install ') const selectors = [ command.includes('chats ') || command.includes('messages ') || command.startsWith('send ') || command === 'presence' || command.startsWith('resolve chat') ? 'chat' : undefined, diff --git a/packages/cli/src/lib/command.ts b/packages/cli/src/lib/command.ts index 2f9243e6..5bafba7b 100644 --- a/packages/cli/src/lib/command.ts +++ b/packages/cli/src/lib/command.ts @@ -119,12 +119,7 @@ export function isForce(flags?: { force?: boolean; yes?: boolean }): boolean { } function outputFormatFromArgv(argv: string[]): string | undefined { - for (let i = 0; i < argv.length; i++) { - const arg = argv[i] - if (arg === '--format') return argv[i + 1] - if (arg?.startsWith('--format=')) return arg.slice('--format='.length) - } - return undefined + return stringFlagFromArgv(argv, '--format') } function stringFlagFromArgv(argv: string[], name: string): string | undefined { diff --git a/packages/cli/src/lib/export/index.ts b/packages/cli/src/lib/export/index.ts index 5422f73d..dca90d82 100644 --- a/packages/cli/src/lib/export/index.ts +++ b/packages/cli/src/lib/export/index.ts @@ -6,8 +6,6 @@ import { fileURLToPath } from 'node:url' import type { Chat } from '@beeper/desktop-api/resources/chats/chats' import type { Attachment, Message } from '@beeper/desktop-api/resources/shared' -type AnyRecord = Record - export type ExportOptions = { accountIDs?: string[] chatIDs?: string[] diff --git a/packages/cli/src/lib/ink/render.tsx b/packages/cli/src/lib/ink/render.tsx index 19110b9b..007c629f 100644 --- a/packages/cli/src/lib/ink/render.tsx +++ b/packages/cli/src/lib/ink/render.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { Box, render as inkRender, Static, Text, useApp, useInput } from 'ink' import Spinner from 'ink-spinner' import { @@ -28,7 +28,7 @@ import { UserRow, } from './components.js' import type { RecordValue } from './format.js' -import { glyphs, theme } from './theme.js' +import { theme } from './theme.js' const App: React.FC<{ children: React.ReactNode }> = ({ children }) => { const { exit } = useApp() @@ -127,7 +127,7 @@ export async function renderValue(value: unknown): Promise { await renderOnce() return case 'doctor': { - const rawChecks = Array.isArray(record.checks) + const checks = Array.isArray(record.checks) ? record.checks as Array<{ ok: boolean; name: string; detail?: string }> : record.checks && typeof record.checks === 'object' ? Object.entries(record.checks as Record).map(([name, value]) => { @@ -140,7 +140,6 @@ export async function renderValue(value: unknown): Promise { return { ok, name, detail } }) : [] - const checks = rawChecks await renderOnce() return } diff --git a/packages/cli/src/lib/ink/theme.ts b/packages/cli/src/lib/ink/theme.ts index 32986f4d..31bed95c 100644 --- a/packages/cli/src/lib/ink/theme.ts +++ b/packages/cli/src/lib/ink/theme.ts @@ -76,8 +76,7 @@ export function senderColor(id: string | undefined | null): string { if (!id) return theme.text let hash = 0 for (let i = 0; i < id.length; i++) hash = ((hash << 5) - hash + id.charCodeAt(i)) | 0 - const palette = groupSenderPalette - return palette[Math.abs(hash) % palette.length]! + return groupSenderPalette[Math.abs(hash) % groupSenderPalette.length]! } // Glyphs — every visual cue we use sits in this map so a single audit covers them. diff --git a/packages/cli/src/lib/installations.ts b/packages/cli/src/lib/installations.ts index ad7b9995..6f3a2b58 100644 --- a/packages/cli/src/lib/installations.ts +++ b/packages/cli/src/lib/installations.ts @@ -1,5 +1,5 @@ import { createWriteStream } from 'node:fs' -import { chmod, cp, mkdir, readFile, rename, rm, symlink, writeFile } from 'node:fs/promises' +import { chmod, cp, mkdir, readFile, readdir, rename, rm, stat, symlink, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { basename, dirname, extname, join } from 'node:path' import { Readable } from 'node:stream' @@ -299,7 +299,6 @@ async function copyPath(source: string, destination: string): Promise { } async function findAppBundle(dir: string): Promise { - const { readdir, stat } = await import('node:fs/promises') const entries = await readdir(dir) for (const entry of entries) { const path = join(dir, entry) @@ -314,7 +313,6 @@ async function findAppBundle(dir: string): Promise { } async function findServerExecutable(dir: string): Promise { - const { readdir, stat } = await import('node:fs/promises') const entries = await readdir(dir) for (const entry of entries) { const path = join(dir, entry) diff --git a/packages/cli/src/lib/profiles.ts b/packages/cli/src/lib/profiles.ts index 6599bcad..38437c03 100644 --- a/packages/cli/src/lib/profiles.ts +++ b/packages/cli/src/lib/profiles.ts @@ -1,11 +1,11 @@ import { spawn } from 'node:child_process' import { execFile } from 'node:child_process' import { closeSync, openSync } from 'node:fs' -import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { homedir } from 'node:os' import { join } from 'node:path' import { promisify } from 'node:util' -import { beeperDir, type Target } from './targets.js' +import { beeperDir, pathExists, type Target } from './targets.js' import { readInstallations } from './installations.js' import { usageError } from './errors.js' @@ -148,9 +148,7 @@ export async function stopProfile(target: Target): Promise { export async function profileStatus(target: Target): Promise> { assertProfile(target) const run = await readRun(target.id) - const reachable = await fetch(new URL('/v1/info', target.baseURL), { signal: AbortSignal.timeout(1000) }) - .then(response => response.ok) - .catch(() => false) + const reachable = await isReachable(target) return { id: target.id, type: target.type, @@ -381,12 +379,3 @@ async function waitForExit(pid: number, timeoutMs: number): Promise { async function sleep(ms: number): Promise { await new Promise(resolve => setTimeout(resolve, ms)) } - -async function pathExists(path: string): Promise { - try { - await access(path) - return true - } catch { - return false - } -} diff --git a/packages/cli/src/lib/resolve.ts b/packages/cli/src/lib/resolve.ts index a3f11a32..4414df5b 100644 --- a/packages/cli/src/lib/resolve.ts +++ b/packages/cli/src/lib/resolve.ts @@ -1,6 +1,7 @@ import { readConfig } from './targets.js' import { ambiguous, notFound } from './errors.js' import { confirmSuggestion, declineWithExit127, rankSuggestions } from './did-you-mean.js' +import { collectPage } from './output.js' type AnyRecord = Record @@ -61,7 +62,7 @@ export async function resolveChatID(client: any, input: string, options: ChatRes const exact = await retrieveChat(client, input) if (exact) return chatInputID(exact) - const candidates = await collect(client.chats.search({ + const candidates = await collectPage(client.chats.search({ accountIDs: options.accountIDs, query: input, scope: 'titles', @@ -105,7 +106,7 @@ async function suggestChat(client: any, input: string, options: ChatResolutionOp if (process.env.BEEPER_NO_INPUT === '1') return undefined let pool: AnyRecord[] try { - pool = await collect(client.chats.list({ accountIDs: options.accountIDs, limit: 100 }), 100) + pool = await collectPage(client.chats.list({ accountIDs: options.accountIDs, limit: 100 }), 100) } catch { return undefined } @@ -163,15 +164,6 @@ async function retrieveChat(client: any, input: string): Promise(iterable: AsyncIterable, limit: number): Promise { - const items: T[] = [] - for await (const item of iterable) { - items.push(item) - if (items.length >= limit) break - } - return items -} - function normalize(value: unknown): string { return String(value ?? '').trim().toLowerCase().replace(/[\s._-]+/g, '') } diff --git a/packages/cli/src/lib/setup-login.ts b/packages/cli/src/lib/setup-login.ts index 41fab57f..1768c885 100644 --- a/packages/cli/src/lib/setup-login.ts +++ b/packages/cli/src/lib/setup-login.ts @@ -29,8 +29,9 @@ export async function finishEmailSetup(target: Target, options: { const client = setupClient(target) let output = await client.app.login.response({ setupRequestID: options.setupRequestID, response: options.code }) if (isRegistrationRequired(output)) { - if ((options.json || !process.stdin.isTTY) && !options.yes) throw new Error('Registration requires --yes to accept the Beeper terms in non-interactive setup.') - const username = options.username ?? (options.json || !process.stdin.isTTY ? undefined : await promptUsername(output.usernameSuggestions)) + const nonInteractive = options.json || !process.stdin.isTTY + if (nonInteractive && !options.yes) throw new Error('Registration requires --yes to accept the Beeper terms in non-interactive setup.') + const username = options.username ?? (nonInteractive ? undefined : await promptUsername(output.usernameSuggestions)) if (!username) throw new Error('Registration requires --username.') if (!options.yes && !await promptYesNoDefaultYes('Accept the Beeper terms and create this account?')) throw new Error('Registration cancelled.') output = await client.app.login.register({ From a819a5ae006a57adfb3a7bba6dee811fa07c08a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 19:08:20 +0200 Subject: [PATCH 18/22] fix npm launcher source quoting --- packages/npm/scripts/build.ts | 139 +--------------------------------- 1 file changed, 2 insertions(+), 137 deletions(-) diff --git a/packages/npm/scripts/build.ts b/packages/npm/scripts/build.ts index cd53ffeb..fa7be4c8 100644 --- a/packages/npm/scripts/build.ts +++ b/packages/npm/scripts/build.ts @@ -1,4 +1,5 @@ #!/usr/bin/env bun +/* eslint-disable no-template-curly-in-string */ import { chmod, cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { existsSync } from 'node:fs' import { join } from 'node:path' @@ -29,142 +30,6 @@ await writeFile(join(root, 'bin', 'beeper.js'), launcher()) await chmod(join(root, 'bin', 'beeper.js'), 0o755) function launcher() { - return `#!/usr/bin/env node -import { createHash } from 'node:crypto' -import { createWriteStream, existsSync } from 'node:fs' -import { chmod, mkdir, readFile, rename, rm } from 'node:fs/promises' -import { get } from 'node:https' -import { homedir, platform as osPlatform, arch as osArch, tmpdir } from 'node:os' -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' -import { spawn } from 'node:child_process' - -const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..') -const manifest = JSON.parse(await readFile(join(packageRoot, 'binaries.json'), 'utf8')) -const platform = targetPlatform() -const artifact = manifest.artifacts.find(item => item.platform === platform) - -if (!artifact) { - console.error(`beeper-cli does not ship a binary for ${process.platform}/${process.arch}.`) - process.exit(1) + return "#!/usr/bin/env node\nimport { createHash } from 'node:crypto'\nimport { createWriteStream, existsSync } from 'node:fs'\nimport { chmod, mkdir, readFile, rename, rm } from 'node:fs/promises'\nimport { get } from 'node:https'\nimport { homedir, platform as osPlatform, arch as osArch, tmpdir } from 'node:os'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { spawn } from 'node:child_process'\n\nconst packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..')\nconst manifest = JSON.parse(await readFile(join(packageRoot, 'binaries.json'), 'utf8'))\nconst platform = targetPlatform()\nconst artifact = manifest.artifacts.find(item => item.platform === platform)\n\nif (!artifact) {\n console.error(`beeper-cli does not ship a binary for ${process.platform}/${process.arch}.`)\n process.exit(1)\n}\n\nconst cacheDir = process.env.BEEPER_CLI_BINARY_CACHE_DIR || join(homedir(), '.cache', 'beeper-cli', manifest.version)\nconst binPath = join(cacheDir, 'bin', manifest.command || 'beeper')\n\nconst expectedBinarySha256 = artifact.binarySha256 || artifact.sha256\n\nif (!existsSync(binPath) || await sha256(binPath).catch(() => '') !== expectedBinarySha256) {\n const tempDir = join(tmpdir(), `beeper-cli-${manifest.version}-${process.pid}`)\n const archivePath = join(tempDir, artifact.file)\n const downloadURL = `https://github.com/beeper/cli/releases/download/v${manifest.version}/${artifact.file}`\n logStep(`installing beeper-cli ${manifest.version} for ${platform}`)\n await rm(tempDir, { recursive: true, force: true })\n await mkdir(tempDir, { recursive: true })\n await download(downloadURL, archivePath)\n logStep('verifying download')\n const actual = await sha256(archivePath)\n if (actual !== artifact.sha256) {\n await rm(tempDir, { recursive: true, force: true })\n console.error(`beeper-cli binary checksum mismatch for ${artifact.file}.`)\n process.exit(1)\n }\n logStep('extracting binary')\n await extract(archivePath, tempDir)\n const extractedBin = join(tempDir, 'bin', manifest.command || 'beeper')\n await chmod(extractedBin, 0o755)\n logStep(`caching binary in ${cacheDir}`)\n await rm(cacheDir, { recursive: true, force: true })\n await mkdir(dirname(binPath), { recursive: true })\n await rename(extractedBin, binPath)\n await rm(tempDir, { recursive: true, force: true })\n logStep('ready')\n}\n\nif (process.env.BEEPER_CLI_LAUNCHER_DEBUG === '1') logStep(`starting ${binPath}`)\nconst child = spawn(binPath, process.argv.slice(2), { stdio: 'inherit', env: process.env })\nchild.on('exit', (code, signal) => {\n if (signal) process.kill(process.pid, signal)\n process.exit(code ?? 1)\n})\n\nfunction logStep(message) {\n console.error(`beeper-cli: ${message}`)\n}\n\nfunction targetPlatform() {\n const os = osPlatform()\n const cpu = osArch()\n const normalizedOS = os === 'darwin' || os === 'linux' ? os : os === 'win32' ? 'windows' : os\n const normalizedArch = cpu === 'x64' || cpu === 'arm64' ? cpu : cpu\n return `${normalizedOS}-${normalizedArch}`\n}\n\nasync function sha256(path) {\n const hash = createHash('sha256')\n hash.update(await readFile(path))\n return hash.digest('hex')\n}\n\nasync function download(url, destination, redirects = 0) {\n if (redirects > 10) throw new Error(`Too many redirects while downloading ${artifact.file}`)\n\n logStep(`downloading ${artifact.file}`)\n await new Promise((resolve, reject) => {\n get(url, response => {\n if ([301, 302, 303, 307, 308].includes(response.statusCode ?? 0) && response.headers.location) {\n response.resume()\n const nextURL = new URL(response.headers.location, url).toString()\n logStep(`redirecting to ${new URL(nextURL).host}`)\n download(nextURL, destination, redirects + 1).then(resolve, reject)\n return\n }\n if (response.statusCode !== 200) {\n response.resume()\n reject(new Error(`Download failed with HTTP ${response.statusCode}: ${url}`))\n return\n }\n const total = Number(response.headers['content-length'] ?? 0)\n let downloaded = 0\n let nextLoggedPercent = 25\n const file = createWriteStream(destination, { mode: 0o755 })\n response.on('data', chunk => {\n downloaded += chunk.length\n if (!total) return\n const percent = Math.floor(downloaded / total * 100)\n while (percent >= nextLoggedPercent && nextLoggedPercent <= 100) {\n logStep(`downloaded ${nextLoggedPercent}%`)\n nextLoggedPercent += 25\n }\n })\n response.pipe(file)\n file.on('finish', () => file.close(resolve))\n file.on('error', reject)\n }).on('error', reject)\n })\n}\n\nasync function extract(archivePath, destination) {\n if (artifact.file.endsWith('.zip')) {\n await run('/usr/bin/ditto', ['-x', '-k', archivePath, destination])\n return\n }\n if (artifact.file.endsWith('.tar.gz')) {\n await run('tar', ['-xzf', archivePath, '-C', destination])\n return\n }\n throw new Error(`Unsupported beeper-cli archive: ${artifact.file}`)\n}\n\nasync function run(command, args) {\n await new Promise((resolve, reject) => {\n const child = spawn(command, args, { stdio: 'ignore' })\n child.on('error', reject)\n child.on('exit', code => {\n if (code === 0) resolve()\n else reject(new Error(`${command} ${args.join(' ')} exited with ${code}`))\n })\n })\n}" } -const cacheDir = process.env.BEEPER_CLI_BINARY_CACHE_DIR || join(homedir(), '.cache', 'beeper-cli', manifest.version) -const binPath = join(cacheDir, 'bin', manifest.command || 'beeper') - -const expectedBinarySha256 = artifact.binarySha256 || artifact.sha256 - -if (!existsSync(binPath) || await sha256(binPath).catch(() => '') !== expectedBinarySha256) { - const tempDir = join(tmpdir(), `beeper-cli-${manifest.version}-${process.pid}`) - const archivePath = join(tempDir, artifact.file) - const downloadURL = `https://github.com/beeper/cli/releases/download/v${manifest.version}/${artifact.file}` - logStep(`installing beeper-cli ${manifest.version} for ${platform}`) - await rm(tempDir, { recursive: true, force: true }) - await mkdir(tempDir, { recursive: true }) - await download(downloadURL, archivePath) - logStep('verifying download') - const actual = await sha256(archivePath) - if (actual !== artifact.sha256) { - await rm(tempDir, { recursive: true, force: true }) - console.error(`beeper-cli binary checksum mismatch for ${artifact.file}.`) - process.exit(1) - } - logStep('extracting binary') - await extract(archivePath, tempDir) - const extractedBin = join(tempDir, 'bin', manifest.command || 'beeper') - await chmod(extractedBin, 0o755) - logStep(`caching binary in ${cacheDir}`) - await rm(cacheDir, { recursive: true, force: true }) - await mkdir(dirname(binPath), { recursive: true }) - await rename(extractedBin, binPath) - await rm(tempDir, { recursive: true, force: true }) - logStep('ready') -} - -if (process.env.BEEPER_CLI_LAUNCHER_DEBUG === '1') logStep(`starting ${binPath}`) -const child = spawn(binPath, process.argv.slice(2), { stdio: 'inherit', env: process.env }) -child.on('exit', (code, signal) => { - if (signal) process.kill(process.pid, signal) - process.exit(code ?? 1) -}) - -function logStep(message) { - console.error(`beeper-cli: ${message}`) -} - -function targetPlatform() { - const os = osPlatform() - const cpu = osArch() - const normalizedOS = os === 'darwin' || os === 'linux' ? os : os === 'win32' ? 'windows' : os - const normalizedArch = cpu === 'x64' || cpu === 'arm64' ? cpu : cpu - return `${normalizedOS}-${normalizedArch}` -} - -async function sha256(path) { - const hash = createHash('sha256') - hash.update(await readFile(path)) - return hash.digest('hex') -} - -async function download(url, destination, redirects = 0) { - if (redirects > 10) throw new Error(`Too many redirects while downloading ${artifact.file}`) - - logStep(`downloading ${artifact.file}`) - await new Promise((resolve, reject) => { - get(url, response => { - if ([301, 302, 303, 307, 308].includes(response.statusCode ?? 0) && response.headers.location) { - response.resume() - const nextURL = new URL(response.headers.location, url).toString() - logStep(`redirecting to ${new URL(nextURL).host}`) - download(nextURL, destination, redirects + 1).then(resolve, reject) - return - } - if (response.statusCode !== 200) { - response.resume() - reject(new Error(`Download failed with HTTP ${response.statusCode}: ${url}`)) - return - } - const total = Number(response.headers['content-length'] ?? 0) - let downloaded = 0 - let nextLoggedPercent = 25 - const file = createWriteStream(destination, { mode: 0o755 }) - response.on('data', chunk => { - downloaded += chunk.length - if (!total) return - const percent = Math.floor(downloaded / total * 100) - while (percent >= nextLoggedPercent && nextLoggedPercent <= 100) { - logStep(`downloaded ${nextLoggedPercent}%`) - nextLoggedPercent += 25 - } - }) - response.pipe(file) - file.on('finish', () => file.close(resolve)) - file.on('error', reject) - }).on('error', reject) - }) -} - -async function extract(archivePath, destination) { - if (artifact.file.endsWith('.zip')) { - await run('/usr/bin/ditto', ['-x', '-k', archivePath, destination]) - return - } - if (artifact.file.endsWith('.tar.gz')) { - await run('tar', ['-xzf', archivePath, '-C', destination]) - return - } - throw new Error(`Unsupported beeper-cli archive: ${artifact.file}`) -} - -async function run(command, args) { - await new Promise((resolve, reject) => { - const child = spawn(command, args, { stdio: 'ignore' }) - child.on('error', reject) - child.on('exit', code => { - if (code === 0) resolve() - else reject(new Error(`${command} ${args.join(' ')} exited with ${code}`)) - }) - }) -} -` -} From 31c9d558cd984f79ca04acaf3cbc2bcc22c4cb96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 19:08:23 +0200 Subject: [PATCH 19/22] fix schema positional parsing --- packages/cli/src/commands/schema.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/schema.ts b/packages/cli/src/commands/schema.ts index 24eab64d..f990d858 100644 --- a/packages/cli/src/commands/schema.ts +++ b/packages/cli/src/commands/schema.ts @@ -24,8 +24,8 @@ export default class Schema extends BeeperCommand { } async run(): Promise { - await this.parse(Schema) - const pathArgs = this.argv.filter(arg => !arg.startsWith('-')) + const { argv } = await this.parse(Schema) + const pathArgs = argv as string[] const requested = pathArgs.length > 0 ? pathArgs.join(' ') : undefined const manifestByCommand = new Map(commandManifest.map(item => [item.command, item])) const commands = (this.config.commands as RawCommand[]) @@ -121,15 +121,19 @@ function outputShape(kind: string): Record { case 'list': { return { kind, envelope, data: 'array' } } + case 'send-result': { return { kind, envelope, data: { chatID: 'string', pendingMessageID: 'string?', state: 'string?' } } } + case 'stream': { return { kind, data: 'jsonl events or RPC lines' } } + case 'success': { return { kind, envelope, data: { message: 'string', detail: 'string?', data: 'object?' } } } + default: { return { kind, envelope, data: 'object' } } From 5e5e695f05fe25eff05dd997f91ce82fd749aefe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 19:08:27 +0200 Subject: [PATCH 20/22] fix dry-run padding lint --- packages/cli/src/commands/presence.ts | 2 ++ packages/cli/src/commands/verify/approve.ts | 3 +++ 2 files changed, 5 insertions(+) diff --git a/packages/cli/src/commands/presence.ts b/packages/cli/src/commands/presence.ts index 709f927b..fe85d783 100644 --- a/packages/cli/src/commands/presence.ts +++ b/packages/cli/src/commands/presence.ts @@ -23,6 +23,7 @@ export default class Presence extends BeeperCommand { await printDryRun('presence', { chat: flags.chat, pick: flags.pick, state: flags.state, durationSeconds: flags.duration }, flags.json ? 'json' : 'human') return } + ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) @@ -36,6 +37,7 @@ export default class Presence extends BeeperCommand { await printSuccess({ message: `Sent typing then paused after ${flags.duration}s`, data: { chatID, state: 'paused', durationSeconds: flags.duration } }, flags.json ? 'json' : 'human') return } + await printSuccess({ message: `Sent ${flags.state} indicator`, data: { chatID, state: flags.state } }, flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/approve.ts b/packages/cli/src/commands/verify/approve.ts index dc99e52a..115937fa 100644 --- a/packages/cli/src/commands/verify/approve.ts +++ b/packages/cli/src/commands/verify/approve.ts @@ -2,17 +2,20 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' + export default class AuthVerifyApprove extends BeeperCommand { static override summary = 'Approve a pending device verification request' static override flags = { id: Flags.string({ description: 'Verification request ID. Defaults to the active request.' }), } + async run(): Promise { const { flags } = await this.parse(AuthVerifyApprove) if (flags['dry-run']) { await printDryRun('verify.approve', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') return } + ensureWritable(flags) const client = await createClient(flags) await printData(await client.app.verifications.accept(flags.id ?? 'active'), flags.json ? 'json' : 'human') From 3bf94b773635186d7eeed89ca02a21cd1cd10b9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 19:08:31 +0200 Subject: [PATCH 21/22] fix command error classification lint --- packages/cli/src/lib/command.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/lib/command.ts b/packages/cli/src/lib/command.ts index 5bafba7b..ac7faa13 100644 --- a/packages/cli/src/lib/command.ts +++ b/packages/cli/src/lib/command.ts @@ -26,6 +26,7 @@ export abstract class BeeperCommand extends Command { if (this.argv.includes('--quiet') || this.argv.includes('-q')) { process.env.BEEPER_QUIET = '1' } + const format = outputFormatFromArgv(this.argv) if (format) { process.env.BEEPER_OUTPUT_FORMAT = format @@ -34,6 +35,7 @@ export abstract class BeeperCommand extends Command { } else if (process.env.BEEPER_AGENT === '1' || !process.stdout.isTTY) { process.env.BEEPER_OUTPUT_FORMAT = 'json' } + const select = stringFlagFromArgv(this.argv, '--select') if (select) process.env.BEEPER_OUTPUT_SELECT = select if (this.argv.includes('--results-only')) process.env.BEEPER_OUTPUT_RESULTS_ONLY = '1' @@ -47,7 +49,7 @@ export abstract class BeeperCommand extends Command { const code = inferredCode ?? error.exitCode ?? ExitCodes.Generic process.exitCode = process.exitCode ?? code const tryMessage = error instanceof CLIError ? error.tryMessage : undefined - const isBug = error instanceof BugError || !(error instanceof CLIError) + const isBug = error instanceof BugError || (!(error instanceof CLIError) && inferredCode === undefined) if (this.argv.includes('--events')) { writeEvent('error', { message, exitCode: code, kind: isBug ? 'bug' : 'abort', tryMessage }) @@ -75,7 +77,7 @@ function inferExitCode(message: string): number | undefined { if (/\b401\b|unauthorized|invalid token|auth(?:entication)? required/i.test(message)) return ExitCodes.AuthRequired if (/\b404\b|not\s+found|unknown .*target|no .*matches/i.test(message)) return ExitCodes.NotFound if (/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|fetch failed|not reachable|not ready/i.test(message)) return ExitCodes.NotReady - if (/usage|invalid|must provide|required|unknown flag|parse/i.test(message)) return ExitCodes.Usage + if (/\busage\b|\binvalid (?:argument|option|flag|value|input)\b|\bmust provide\b|\brequired (?:flag|argument|option|value)\b|\bunknown flag\b|\bparse error\b/i.test(message)) return ExitCodes.Usage return undefined } @@ -98,7 +100,7 @@ function formatBugPanel(error: Error, version: string): string { export function ensureWritable(flags: { 'read-only'?: boolean }): void { const env = process.env.BEEPER_READONLY - const readOnly = flags['read-only'] || ['1', 'true', 'yes', 'on'].includes(String(env ?? '').toLowerCase()) + const readOnly = flags['read-only'] || ['1', 'on', 'true', 'yes'].includes(String(env ?? '').toLowerCase()) if (readOnly) throw new CLIError('read-only mode: command would modify Beeper or local CLI state', ExitCodes.Usage) } @@ -128,6 +130,7 @@ function stringFlagFromArgv(argv: string[], name: string): string | undefined { if (arg === name) return argv[i + 1] if (arg?.startsWith(`${name}=`)) return arg.slice(name.length + 1) } + return undefined } @@ -142,21 +145,27 @@ function errorCode(code: number, isBug: boolean): string { case ExitCodes.Ambiguous: { return 'ambiguous_selector' } + case ExitCodes.AuthRequired: { return 'auth_required' } + case ExitCodes.CommandNotFound: { return 'command_not_found' } + case ExitCodes.NotFound: { return 'not_found' } + case ExitCodes.NotReady: { return 'not_ready' } + case ExitCodes.Usage: { return 'usage_error' } + default: { return 'runtime_error' } From fefbf4e26048b04a0360e1e1fb01b4b6a2e34e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 19:08:34 +0200 Subject: [PATCH 22/22] remove local claude runtime lock --- .claude/scheduled_tasks.lock | 1 - .gitignore | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 .claude/scheduled_tasks.lock diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock deleted file mode 100644 index b9663f66..00000000 --- a/.claude/scheduled_tasks.lock +++ /dev/null @@ -1 +0,0 @@ -{"sessionId":"2da33c33-52d4-4916-961e-72cd6ae9594a","pid":18713,"procStart":"Sat May 30 16:14:01 2026","acquiredAt":1780159461036} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 13b643ec..bf7d3f55 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ node_modules/ /beeper-desktop-cli .upstream/ *.exe + +.claude/