From a403e1a5696d5f0aa502aea90723307d651e3199 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Fri, 2 Oct 2026 12:36:44 +0300 Subject: [PATCH 01/14] =?UTF-8?q?feat(cli):=20agent=20contract=20=E2=80=94?= =?UTF-8?q?=20exit=20codes,=20error=20envelope,=20confirmation=20without?= =?UTF-8?q?=20a=20terminal,=20dry=20run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan step [1] of docs/tasks/agent-native-cli.md. - src/lib/exit-codes.ts: the documented exit-code table (0–9), identical with and without --json. No oclif import, so the MCP server can share it. - src/lib/cli-error.ts: CliError + toEnvelope, one shape for every failure (SDK errors, parser errors, JSON input, prompts, bugs): code, message, status, exit, fieldErrors, url, traceId, hint, docs. No oclif import. - BaseCommand.catch prints the envelope (stdout with --json, stderr as text) and exits with the documented code; oclif's own JSON error path (22k-line parse-context dump) never runs. Unknown commands go through a command_not_found hook and get the same envelope. - confirmOrFail: destructive commands (delete, stop, regenerate, block, disable, --many updates) ask in a terminal; without a terminal and without --yes they exit 3 and send nothing; "no" at the prompt exits 9. The old `isTTY` check let `norbix users delete x --- README.md | 51 +++-- package.json | 4 + src/base.ts | 279 ++++++++++++++++++++--- src/commands/api.ts | 5 + src/commands/apikeys/regenerate.ts | 12 +- src/commands/configure.ts | 13 +- src/commands/db/delete.ts | 15 +- src/commands/db/update.ts | 8 +- src/commands/email/delete.ts | 10 +- src/commands/email/stop.ts | 10 +- src/commands/env/delete.ts | 13 +- src/commands/files/delete.ts | 10 +- src/commands/hub.ts | 5 + src/commands/login.ts | 49 +++- src/commands/module.ts | 13 +- src/commands/payments/disable.ts | 10 +- src/commands/push/campaign/delete.ts | 10 +- src/commands/push/delete.ts | 10 +- src/commands/push/disable.ts | 9 +- src/commands/push/integration/delete.ts | 10 +- src/commands/push/integration/disable.ts | 10 +- src/commands/push/stop.ts | 10 +- src/commands/scheduler/delete.ts | 10 +- src/commands/scheduler/disable.ts | 10 +- src/commands/sms/delete.ts | 10 +- src/commands/sms/stop.ts | 10 +- src/commands/users/block.ts | 10 +- src/commands/users/delete.ts | 10 +- src/commands/webhooks/disable.ts | 10 +- src/commands/webhooks/remove.ts | 13 +- src/commands/webhooks/secret.ts | 11 +- src/hooks/command-not-found.ts | 29 +++ src/hooks/init.ts | 10 + src/lib/cli-error.ts | 279 +++++++++++++++++++++++ src/lib/color.ts | 19 ++ src/lib/exit-codes.ts | 118 ++++++++++ src/lib/json.ts | 7 +- src/lib/namespace-command.ts | 20 +- test/files.test.ts | 14 +- tests/push-routes.test.ts | 2 +- tests/push.test.ts | 4 +- 41 files changed, 932 insertions(+), 230 deletions(-) create mode 100644 src/hooks/command-not-found.ts create mode 100644 src/hooks/init.ts create mode 100644 src/lib/cli-error.ts create mode 100644 src/lib/color.ts create mode 100644 src/lib/exit-codes.ts diff --git a/README.md b/README.md index f7f5b4c..4205449 100644 --- a/README.md +++ b/README.md @@ -126,28 +126,47 @@ pipe into `jq`: norbix db find orders --json | jq '.list.items[] | ._id' ``` -## Errors +## Errors and exit codes -A failed call prints one line: the error code, the message, and the HTTP -status. +A failed call prints the message, the details the server gave (code, HTTP +status, URL, trace id, field errors) and what to do next — on **stderr**, so +stdout stays clean for data: ```sh -$ norbix files info nbin_1 a/b.txt - › Error: CM-ERRORS-FILES-016: File not found: "a/b.txt" does not exist in - › Local (nbin_1). (HTTP 404) +$ norbix files info a/b.txt +Error: File not found: "a/b.txt" does not exist in Local (nbin_1). + code: CM-ERRORS-FILES-016 + status: 404 + url: https://nb-eu-germany.api.norbix.ai/v2/files/... +Hint: Check the id, the --env and the --project. List items first (e.g. `norbix files list --json`). +Docs: norbix files info --help ``` -The code and the message are the gateway's own — they come out of -`responseStatus.errors[]`, where the gateway puts them. Before this, every -failure read "Request failed with status 404" with no code. +With `--json` the same information is one JSON document on stdout: +`{"error": {"code", "message", "status", "exit", "fieldErrors", "url", "traceId", "hint", "docs"}}`. -Exit codes do not change: `1` for a failed call, `2` for a check that ran and -came back negative (`norbix files integrations test`). +The exit code tells a script or an agent what went wrong, with or without +`--json`: -**A refusal the gateway answers with HTTP 200** and -`responseStatus.isSuccess = false` now exits non-zero as well, wherever it did -not before — `norbix files publish` / `unpublish` were the two commands that -used to treat it as a success. +| code | meaning | +| --- | --- | +| 0 | success | +| 1 | unexpected / internal error | +| 2 | usage: bad flags or arguments, invalid JSON input, unknown command, missing config | +| 3 | confirmation required — a destructive command ran without a terminal and without `--yes` | +| 4 | not authenticated / auth rejected (401, 403, no key) | +| 5 | not found (404) | +| 6 | validation rejected by the server (400 / 422) | +| 7 | network, timeout, endpoint unreachable | +| 8 | rate limited or server error (429, 5xx) | +| 9 | cancelled by you at a prompt | + +Destructive commands (`delete`, `stop`, `regenerate`, `block`, `disable`, +`--many` updates) ask for confirmation in a terminal. In a script, in CI or +under a coding agent there is nobody to ask, so they **exit 3 and send +nothing** unless you pass `--yes`. Preview any of them with `--dry-run`: +the request is built with your real auth, region and project, printed, and +not sent. ## Versioning @@ -191,7 +210,7 @@ Or pass `--integration ` (env var: `NORBIX_FILES_INTEGRATION_ID`). | `norbix files sign [--expires ]` | Get a temporary web address for the file that anyone with the link can open. | | `norbix files upload [remote]` | Upload a file. Without `remote` it keeps its own name in the root folder. | | `norbix files download [local]` | Download a file. Without `local` it keeps its own name in the current folder. | -| `norbix files delete [--yes]` | Delete one file. Asks first unless you pass `--yes`. | +| `norbix files delete [--yes]` | Delete one file. Asks first unless you pass `--yes`; without a terminal it exits 3 unless `--yes`. | | `norbix files publish [--folder]` | Give the file — or the whole folder — a link anyone can open, and print it. | | `norbix files unpublish [--folder]` | Take that link away again. | | `norbix files integrations test ` | Check that the storage behind an integration really works. See below. | diff --git a/package.json b/package.json index 4c1de79..f93a914 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,10 @@ "webhooks": { "description": "Inspect the webhook integration and its destinations" } + }, + "hooks": { + "init": "./dist/hooks/init.js", + "command_not_found": "./dist/hooks/command-not-found.js" } }, "publishConfig": { diff --git a/src/base.ts b/src/base.ts index 322ec25..911f048 100644 --- a/src/base.ts +++ b/src/base.ts @@ -1,6 +1,8 @@ -import {Norbix, NorbixError} from '@norbix.ai/ts' -import {Command, Flags} from '@oclif/core' +import {Norbix} from '@norbix.ai/ts' +import {Command, Flags, ux} from '@oclif/core' +import {CliError, formatErrorText, toEnvelope, usageError} from './lib/cli-error.js' +import {EXIT} from './lib/exit-codes.js' import { DEFAULT_API_URL, DEFAULT_HUB_URL, @@ -11,6 +13,14 @@ import { } from './lib/profiles.js' import {readStore, type StoredConfig} from './lib/store.js' +/** One link of the SDK's transport middleware chain. */ +type NorbixMiddleware = (ctx: { + url: string + init: RequestInit + attempt: number + next: () => Promise +}) => Promise + /** Stands in for a projectId the SDK constructor insists on; never sent. */ const NO_PROJECT_PLACEHOLDER = 'no-project' @@ -33,6 +43,8 @@ export interface GlobalFlags { 'api-key'?: string account?: string profile?: string + 'dry-run'?: boolean + yes?: boolean } export interface ResolvedContext { @@ -54,6 +66,31 @@ export interface ResolvedContext { stored: StoredConfig } +/** The HTTP request a dry run would have sent (Authorization redacted). */ +export interface DryRunRequest { + method: string + url: string + headers: Record + body?: unknown +} + +/** What `--dry-run` prints: the SDK call and the HTTP request behind it. */ +export interface DryRunReport { + dryRun: true + /** `api.membership.deleteUser` — or a local action like `config.set`. */ + method: string + request: unknown + http?: DryRunRequest +} + +/** Thrown by the dry-run middleware instead of sending; caught in `catch`. */ +class DryRunStop extends Error { + constructor(readonly http: DryRunRequest) { + super('dry run') + this.name = 'DryRunStop' + } +} + /** * Base class for every Norbix CLI command. * @@ -69,6 +106,9 @@ export interface ResolvedContext { * * Default endpoints are https://api.norbix.ai and https://hub.norbix.ai; * a profile can override them (self-hosted, localhost, custom domain). + * + * Agent contract (docs/agent-contract.md): never hang, never act silently, + * one JSON document on stdout with --json, documented exit codes. */ export abstract class BaseCommand extends Command { static enableJsonFlag = true @@ -106,10 +146,91 @@ export abstract class BaseCommand extends Command { }), } + /** Flags of a command that changes something but needs no confirmation (create, update, archive). */ + static dryRunFlags = { + 'dry-run': Flags.boolean({ + description: 'Resolve context and print the request that would be sent; send nothing', + default: false, + }), + } + + /** Flags of a destructive command (delete, stop, regenerate, block, disable): confirmation + dry run. */ + static mutatingFlags = { + yes: Flags.boolean({ + char: 'y', + description: 'Skip the confirmation prompt (required in non-interactive shells)', + default: false, + }), + ...BaseCommand.dryRunFlags, + } + + /** The SDK call captured by a dry run, filled by the client proxy. */ + private dryRunCall?: {method: string; request: unknown} + protected readStore(): StoredConfig { return readStore(this.config.configDir) } + /** The id of this command as the user types it: `users delete`. */ + protected get commandName(): string { + return (this.id ?? '').replaceAll(':', ' ') + } + + /** + * True only when a human can answer a prompt: both stdout and stdin are a + * terminal, not CI, not --json. Anything else is an agent or a script. + */ + protected isInteractive(): boolean { + return Boolean(process.stdout.isTTY && process.stdin.isTTY) && !process.env.CI && !this.jsonEnabled() + } + + /** + * Ask before a destructive call. `--yes` skips it; a dry run needs none; + * a non-interactive shell without `--yes` fails with exit 3 and sends + * nothing; "no" at the prompt exits 9. + */ + protected async confirmOrFail(message: string, flags: {yes?: boolean; 'dry-run'?: boolean}): Promise { + if (flags.yes || flags['dry-run']) return + if (!this.isInteractive()) { + throw new CliError({ + exit: EXIT.CONFIRMATION_REQUIRED, + code: 'CONFIRMATION_REQUIRED', + message: `Confirmation required: ${message}`, + hint: 'Re-run with --yes. Preview first with --dry-run.', + docs: `norbix ${this.commandName} --help`, + }) + } + + const {confirm} = await import('@inquirer/prompts') + if (!(await confirm({message, default: false}))) { + throw new CliError({exit: EXIT.CANCELLED, code: 'CANCELLED', message: 'Cancelled.'}) + } + } + + /** + * Report a dry run of a LOCAL action (config write, file write) — SDK calls + * are captured automatically by `client()`. Prints the report and returns + * it, so the command can `return this.dryRun(...)`. + */ + protected dryRun(call: {method: string; request: unknown; http?: DryRunRequest}): DryRunReport { + const report: DryRunReport = {dryRun: true, method: call.method, request: call.request} + if (call.http) report.http = call.http + this.printDryRun(report) + return report + } + + private printDryRun(report: DryRunReport): void { + if (this.jsonEnabled()) return // oclif prints the returned value + const lines = [`Dry run — nothing was sent.`, `Would call: ${report.method}`] + if (report.http) { + lines.push(` ${report.http.method} ${report.http.url}`) + if (report.http.body !== undefined) lines.push(` body: ${JSON.stringify(report.http.body)}`) + } + + lines.push(`Request: ${JSON.stringify(report.request, null, 2)}`) + this.log(lines.join('\n')) + } + protected resolveContext(flags: GlobalFlags): ResolvedContext { const profiles = readProfiles() const legacy = this.readStore() @@ -123,9 +244,10 @@ export abstract class BaseCommand extends Command { prof = profiles[explicitProfile] ?? {} profileName = explicitProfile if (!profiles[explicitProfile]) { - this.error( - `Profile "${explicitProfile}" not found in ~/.norbix/config.\n` + - `Run \`norbix configure --profile ${explicitProfile}\` to create it, or \`norbix profiles\` to list existing ones.`, + throw usageError( + `Profile "${explicitProfile}" not found in ~/.norbix/config.`, + `Run \`norbix configure --profile ${explicitProfile}\` to create it, or \`norbix profiles\` to list existing ones.`, + 'norbix profiles --help', ) } } else { @@ -195,10 +317,11 @@ export abstract class BaseCommand extends Command { */ protected assertEndpoints(ctx: ResolvedContext): void { if (ctx.usesDefaultEndpoints && !ctx.region) { - this.error( - 'Region is required when using the default norbix.ai endpoints.\n' + - 'Set it with `norbix configure` (region field), pass --region (e.g. nb-eu-germany),\n' + + throw usageError( + 'Region is required when using the default norbix.ai endpoints.', + 'Set it with `norbix configure` (region field), pass --region (e.g. nb-eu-germany), ' + 'or set custom api_url / hub_url in the profile for self-hosted installations.', + 'norbix configure --help', ) } } @@ -211,6 +334,11 @@ export abstract class BaseCommand extends Command { * `requireProject: false` — the call does not need a project (a signed link carries it). * The SDK constructor still demands a projectId, so a placeholder is passed and the * project headers are taken off the wire again: the gateway never sees a fake project. + * + * With `--dry-run` the client is real up to the socket: auth, region and + * project are resolved exactly as for a live call, the SDK builds the + * request, and a middleware stops it just before `fetch`. `catch` then + * prints the request and exits 0. */ protected client( flags: GlobalFlags, @@ -220,21 +348,30 @@ export abstract class BaseCommand extends Command { this.assertEndpoints(ctx) const noProject = !ctx.projectId && opts.requireProject === false if (!ctx.projectId && !noProject) { - this.error( - 'No project ID configured.\nRun `norbix configure` (or `norbix login`), or pass --project.', + throw usageError( + 'No project ID configured.', + 'Run `norbix configure` (or `norbix login`), pass --project , or set NORBIX_PROJECT_ID.', + 'norbix configure --help', ) } if (opts.requireAuth !== false && !ctx.apiKey && !ctx.bearerToken) { - this.error( - 'Not authenticated.\nRun `norbix login` (browser/user session) or `norbix configure` (API key profile), or pass --api-key.', - ) + throw new CliError({ + exit: EXIT.AUTH, + code: 'UNAUTHENTICATED', + message: 'Not authenticated.', + hint: 'Run `norbix login` (browser/user session) or `norbix configure` (API key profile), pass --api-key, or set NORBIX_API_KEY.', + docs: 'norbix login --help', + }) } - return new Norbix( + const middleware: NorbixMiddleware[] = noProject ? [dropProjectHeaders] : [] + if (flags['dry-run']) middleware.push(dryRunMiddleware) + + const client = new Norbix( { projectId: noProject ? NO_PROJECT_PLACEHOLDER : ctx.projectId, - middleware: noProject ? [dropProjectHeaders] : [], + middleware, accountId: ctx.accountId, apiKey: ctx.apiKey, bearerToken: ctx.bearerToken, @@ -243,10 +380,42 @@ export abstract class BaseCommand extends Command { // Always explicit: CLI defaults are api/hub.norbix.ai (the SDK's own // defaults still point at .dev — tracked as an SDK bug). baseUrl: {api: ctx.apiUrl, hub: ctx.hubUrl}, + // A dry run stops in the middleware; the SDK must not retry it. + ...(flags['dry-run'] ? {retry: {maxRetries: 0}} : {}), }, // The CLI already resolved env vars itself — don't let the SDK re-read them. {envSource: {}}, ) + + return flags['dry-run'] ? this.recordingClient(client) : client + } + + /** Wrap `client.api.*` / `client.hub.*` so a dry run knows which SDK method was called. */ + private recordingClient(client: Norbix): Norbix { + const record = (target: 'api' | 'hub') => + new Proxy(client[target] as unknown as Record, { + get: (namespace, moduleName: string) => { + const mod = namespace[moduleName] + if (typeof mod !== 'object' || mod === null) return mod + return new Proxy(mod as Record, { + get: (m, methodName: string) => { + const fn = m[methodName] + if (typeof fn !== 'function') return fn + return (request: unknown = {}, ...rest: unknown[]) => { + this.dryRunCall = {method: `${target}.${moduleName}.${methodName}`, request} + return (fn as (...a: unknown[]) => unknown)(request, ...rest) + } + }, + }) + }, + }) + + return new Proxy(client, { + get: (c, prop: string | symbol) => { + if (prop === 'api' || prop === 'hub') return record(prop) + return (c as unknown as Record)[prop] + }, + }) } /** Pretty-print a result unless --json is active (oclif prints the return value then). */ @@ -256,27 +425,73 @@ export abstract class BaseCommand extends Command { } } + /** Plain JSON on stdout: no theme, no ANSI codes, one document. */ + protected logJson(json: unknown): void { + ux.stdout(JSON.stringify(json, null, 2)) + } + /** - * One line for a failed call: `: (HTTP )`. - * - * The code comes first because it is the part a reader can search for and a - * script can match on. It is the gateway's own code — the SDK reads it out - * of `responseStatus.errors[]`, where the gateway puts it (10b-files slice - * ERRORS, issue #66). The exit codes do not change. + * Every failure ends here and becomes the one error envelope + * (docs/agent-contract.md): `{"error": {...}}` on stdout with --json, + * `Error: ... / Hint: ...` on stderr otherwise. The exit code is the same + * in both modes. A dry run is "caught" here too: it is the middleware + * stopping the request, and exits 0. */ protected async catch(error: Error & {exitCode?: number}): Promise { - if (error instanceof NorbixError) { - const {code, status} = error as {code?: string; status?: number} - const prefix = code ? `${code}: ` : '' - // The SDK's last-resort message already ends in "(HTTP )" — - // do not say it twice. - const suffix = - status && !error.message.includes(`(HTTP ${status})`) ? ` (HTTP ${status})` : '' - const hint = - status === 401 ? '\nYour session may have expired. Run `norbix login` again.' : '' - return this.error(`${prefix}${error.message}${suffix}${hint}`) + // The SDK wraps whatever its middleware throws into a network error, so + // the dry-run stop arrives as `raw` of that wrapper. + const stop = error instanceof DryRunStop ? error : dryRunStopOf(error) + if (stop) { + const report: DryRunReport = { + dryRun: true, + method: this.dryRunCall?.method ?? 'unknown', + request: this.dryRunCall?.request ?? {}, + http: stop.http, + } + if (this.jsonEnabled()) this.logJson(report) + else this.printDryRun(report) + return report + } + + // `this.exit(n)` — already decided, nothing to print. + if ((error as {code?: string}).code === 'EEXIT') throw error + + const envelope = toEnvelope(error, {command: this.commandName}) + if (this.jsonEnabled()) { + this.logJson({error: envelope}) + } else { + process.stderr.write(formatErrorText(envelope) + '\n') } - return super.catch(error) + // Rethrow the original error with the exit code attached: oclif's + // handler then exits with it and prints nothing more (the envelope was + // the output), and a test still sees the real error. + const marked = error as Error & {oclif?: {exit?: number}; skipOclifErrorHandling?: boolean} + marked.oclif = {...marked.oclif, exit: envelope.exit} + marked.skipOclifErrorHandling = true + throw marked } } + +function dryRunStopOf(error: unknown): DryRunStop | undefined { + const raw = (error as {raw?: unknown} | undefined)?.raw + return raw instanceof DryRunStop ? raw : undefined +} + +/** Stops the SDK just before `fetch` and hands the request back to `catch`. */ +async function dryRunMiddleware(ctx: {url: string; init: RequestInit; next: () => Promise}): Promise { + const headers: Record = {} + new Headers(ctx.init.headers).forEach((value, key) => { + headers[key] = key.toLowerCase() === 'authorization' ? 'Bearer ***' : value + }) + let body: unknown + if (typeof ctx.init.body === 'string') { + try { + body = JSON.parse(ctx.init.body) + } catch { + body = ctx.init.body + } + } + + throw new DryRunStop({method: ctx.init.method ?? 'GET', url: ctx.url, headers, body}) +} diff --git a/src/commands/api.ts b/src/commands/api.ts index 8d23645..4bbcf1b 100644 --- a/src/commands/api.ts +++ b/src/commands/api.ts @@ -1,3 +1,4 @@ +import {BaseCommand} from '../base.js' import {NamespaceCommand} from '../lib/namespace-command.js' export default class ApiNs extends NamespaceCommand { @@ -11,6 +12,10 @@ use \`norbix raw\`.` static strict = false + static flags = { + ...BaseCommand.mutatingFlags, + } + static examples = [ '<%= config.bin %> api # list modules', '<%= config.bin %> api database # list database methods', diff --git a/src/commands/apikeys/regenerate.ts b/src/commands/apikeys/regenerate.ts index 3dec5d1..7412c54 100644 --- a/src/commands/apikeys/regenerate.ts +++ b/src/commands/apikeys/regenerate.ts @@ -1,5 +1,3 @@ -import {confirm} from '@inquirer/prompts' -import {Flags} from '@oclif/core' import {BaseCommand} from '../../base.js' @@ -9,20 +7,14 @@ export default class ApikeysRegenerate extends BaseCommand { static examples = ['<%= config.bin %> apikeys regenerate --yes'] static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {flags} = await this.parse(ApikeysRegenerate) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({ - message: 'Regenerate API keys? Every service using the old keys will lose access.', - default: false, - }) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail('Regenerate API keys? Every service using the old keys will lose access.', flags) const res = await client.api.apikeys.regenerateApiKeys({environment: flags.env}) this.print(res) diff --git a/src/commands/configure.ts b/src/commands/configure.ts index 087ea97..8c7dc97 100644 --- a/src/commands/configure.ts +++ b/src/commands/configure.ts @@ -1,7 +1,7 @@ -import {input, password as passwordPrompt} from '@inquirer/prompts' import {Flags} from '@oclif/core' import {BaseCommand} from '../base.js' +import {usageError} from '../lib/cli-error.js' import { DEFAULT_API_URL, DEFAULT_HUB_URL, @@ -35,6 +35,17 @@ environment. Use --profile to create or edit a named profile; without it the const name = flags.profile ?? 'default' const existing = readProfiles()[name] ?? {} + if (!this.isInteractive()) { + throw usageError( + 'configure is interactive and this shell is not.', + `Write the profile without prompts: norbix login --api-key --project --region --profile ${name}; ` + + 'or set NORBIX_API_KEY / NORBIX_PROJECT_ID / NORBIX_REGION in the environment.', + 'norbix login --help', + ) + } + + const {input, password: passwordPrompt} = await import('@inquirer/prompts') + this.log(`Configuring profile [${name}] in ${PROFILES_PATH}`) // Same interaction style as `aws configure`: show the current value diff --git a/src/commands/db/delete.ts b/src/commands/db/delete.ts index b4dc978..2c11370 100644 --- a/src/commands/db/delete.ts +++ b/src/commands/db/delete.ts @@ -1,4 +1,3 @@ -import {confirm} from '@inquirer/prompts' import {Args, Flags} from '@oclif/core' import {BaseCommand} from '../../base.js' @@ -8,7 +7,8 @@ export default class DbDelete extends BaseCommand { static description = 'Delete records: one by --id, or many by --filter with --many' static examples = [ - '<%= config.bin %> db delete orders --id 66b2f0a1...', + '<%= config.bin %> db delete orders --id 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + '<%= config.bin %> db delete orders --id 66b2f0a1c3d4e5f6a7b8c9d0 --yes', `<%= config.bin %> db delete orders --filter '{"status":"cancelled"}' --many --yes`, ] @@ -20,7 +20,7 @@ export default class DbDelete extends BaseCommand { id: Flags.string({description: 'Record ID (single delete)', exclusive: ['many', 'filter']}), filter: Flags.string({char: 'f', description: 'JSON filter (with --many)', dependsOn: ['many']}), many: Flags.boolean({description: 'Delete every record matching --filter', default: false}), - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { @@ -30,13 +30,7 @@ export default class DbDelete extends BaseCommand { if (flags.many) { if (!flags.filter) this.error('--many requires --filter.') const filter = await readJsonInput(flags.filter, 'filter') - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({ - message: `Delete ALL records in "${args.collection}" matching ${filter}?`, - default: false, - }) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Delete ALL records in "${args.collection}" matching ${filter}?`, flags) const res = await client.api.database.deleteMany({collectionName: args.collection, filter}) this.print(res) @@ -44,6 +38,7 @@ export default class DbDelete extends BaseCommand { } if (!flags.id) this.error('Pass --id for a single delete, or --filter with --many.') + await this.confirmOrFail(`Delete record ${flags.id} from "${args.collection}"?`, flags) const res = await client.api.database.deleteOne({collectionName: args.collection, id: flags.id}) this.print(res) return res diff --git a/src/commands/db/update.ts b/src/commands/db/update.ts index 705ab94..9e2806c 100644 --- a/src/commands/db/update.ts +++ b/src/commands/db/update.ts @@ -8,7 +8,8 @@ export default class DbUpdate extends BaseCommand { static examples = [ `<%= config.bin %> db update orders --id 66b2f0a1... --update '{"$set":{"status":"shipped"}}'`, - `<%= config.bin %> db update orders --filter '{"status":"new"}' --update '{"$set":{"status":"queued"}}' --many`, + `<%= config.bin %> db update orders --filter '{"status":"new"}' --update '{"$set":{"status":"queued"}}' --many --dry-run`, + `<%= config.bin %> db update orders --filter '{"status":"new"}' --update '{"$set":{"status":"queued"}}' --many --yes`, ] static args = { @@ -20,6 +21,7 @@ export default class DbUpdate extends BaseCommand { filter: Flags.string({char: 'f', description: 'JSON filter (with --many)', dependsOn: ['many']}), update: Flags.string({char: 'u', required: true, description: 'JSON update (e.g. {"$set":{...}}) or `-` for stdin'}), many: Flags.boolean({description: 'Update every record matching --filter', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { @@ -29,9 +31,11 @@ export default class DbUpdate extends BaseCommand { if (flags.many) { if (!flags.filter) this.error('--many requires --filter.') + const filter = await readJsonInput(flags.filter, 'filter') + await this.confirmOrFail(`Update ALL records in "${args.collection}" matching ${filter}?`, flags) const res = await client.api.database.updateMany({ collectionName: args.collection, - filter: await readJsonInput(flags.filter, 'filter'), + filter, update, }) this.print(res) diff --git a/src/commands/email/delete.ts b/src/commands/email/delete.ts index 7d4078e..a219dad 100644 --- a/src/commands/email/delete.ts +++ b/src/commands/email/delete.ts @@ -1,5 +1,4 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../base.js' @@ -13,17 +12,14 @@ export default class EmailDelete extends BaseCommand { } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(EmailDelete) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Delete email template ${args.id}?`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Delete email template ${args.id}?`, flags) const res = await client.hub.notifications.deleteEmailTemplate({id: args.id}) this.print(`Template ${args.id} deleted.`) diff --git a/src/commands/email/stop.ts b/src/commands/email/stop.ts index cb18f2d..98e22d2 100644 --- a/src/commands/email/stop.ts +++ b/src/commands/email/stop.ts @@ -1,5 +1,4 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../base.js' @@ -13,17 +12,14 @@ export default class EmailStop extends BaseCommand { } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(EmailStop) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Stop email campaign ${args.id}?`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Stop email campaign ${args.id}?`, flags) // stop* methods are newer than SDK 1.2.0 — guard so older SDKs fail nicely. const n = client.hub.notifications as unknown as { diff --git a/src/commands/env/delete.ts b/src/commands/env/delete.ts index edc12b6..328ea55 100644 --- a/src/commands/env/delete.ts +++ b/src/commands/env/delete.ts @@ -1,5 +1,4 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../base.js' @@ -13,20 +12,14 @@ export default class EnvDelete extends BaseCommand { } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(EnvDelete) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({ - message: `Delete environment "${args.name}" and everything inside it?`, - default: false, - }) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Delete environment "${args.name}" and everything inside it?`, flags) const res = await client.hub.environments.delete({environmentName: args.name}) this.print(`Environment ${args.name} deleted.`) diff --git a/src/commands/files/delete.ts b/src/commands/files/delete.ts index 103f715..8550690 100644 --- a/src/commands/files/delete.ts +++ b/src/commands/files/delete.ts @@ -1,5 +1,4 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../base.js' import {integrationFlag, resolveIntegration} from '../../lib/files.js' @@ -15,7 +14,7 @@ export default class FilesDelete extends BaseCommand { static flags = { integration: integrationFlag, - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { @@ -26,10 +25,7 @@ export default class FilesDelete extends BaseCommand { this.error('No files integration ID. Pass --integration or run `norbix config set filesIntegrationId `.') } - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Delete file "${args.remote}"?`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Delete file "${args.remote}"?`, flags) const res = await client.api.files.deleteFileApi({ filesIntegrationId: integration, diff --git a/src/commands/hub.ts b/src/commands/hub.ts index 5864902..3f12f36 100644 --- a/src/commands/hub.ts +++ b/src/commands/hub.ts @@ -1,3 +1,4 @@ +import {BaseCommand} from '../base.js' import {NamespaceCommand} from '../lib/namespace-command.js' export default class Hub extends NamespaceCommand { @@ -12,6 +13,10 @@ positional id) means one item. Destructive verbs ask for confirmation static strict = false + static flags = { + ...BaseCommand.mutatingFlags, + } + static examples = [ '<%= config.bin %> hub # list modules', '<%= config.bin %> hub database # list database methods', diff --git a/src/commands/login.ts b/src/commands/login.ts index fcd17f2..b049e1c 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -1,8 +1,8 @@ -import {input, password as passwordPrompt} from '@inquirer/prompts' import {Norbix} from '@norbix.ai/ts' import {Flags} from '@oclif/core' import {BaseCommand} from '../base.js' +import {usageError} from '../lib/cli-error.js' import { DeviceFlowUnsupportedError, openBrowser, @@ -24,12 +24,17 @@ support browser login yet, the CLI falls back to user + password. (same as \`norbix configure\`). Commands run with --profile skip the session on purpose and use that -profile's API key.` +profile's API key. + +Non-interactive shells (scripts, CI, coding agents) cannot answer prompts: +pass --api-key (with --project), or --user with --password, or set +NORBIX_API_KEY in the environment. Without one of these the command exits 2.` static examples = [ '<%= config.bin %> login', '<%= config.bin %> login --user alice@example.com', - '<%= config.bin %> login --api-key nbk_... --project 5f1a... --profile ci', + '<%= config.bin %> login --api-key nbk_live_7hK2abc --project 66b2f0a1c3d4e5f6a7b8c9d0 --region nb-eu-germany --profile ci', + '<%= config.bin %> login --user alice@example.com --password "$NORBIX_PASSWORD" --project 66b2f0a1c3d4e5f6a7b8c9d0', ] static flags = { @@ -43,10 +48,19 @@ profile's API key.` const profName = flags.profile ?? 'default' const existing = profiles[profName] ?? {} + // No terminal to answer a prompt: only the flag-driven modes can work. + if (!this.isInteractive() && !flags['api-key'] && !flags.password) { + throw usageError( + 'login needs a terminal to ask for credentials, and this shell is not interactive.', + 'Non-interactive options: --api-key --project (saves a profile); ' + + '--user --password (password login); or set NORBIX_API_KEY and NORBIX_PROJECT_ID in the environment and skip login.', + 'norbix login --help', + ) + } + // Mode 1: API key → saved as a profile (long-lived identity). if (flags['api-key']) { - const projectId = - flags.project ?? existing.project_id ?? (await input({message: 'Project ID:', required: true})) + const projectId = flags.project ?? existing.project_id ?? (await this.ask('Project ID:')) writeProfile(profName, { ...existing, api_key: flags['api-key'], @@ -76,6 +90,21 @@ profile's API key.` return this.passwordLogin(flags, existing) } + /** Prompt for a value — or, with no terminal, fail with the flag to pass instead. */ + private async ask(message: string, secret = false): Promise { + if (!this.isInteractive()) { + const flag = message.startsWith('Project') ? '--project' : message.startsWith('User') ? '--user' : '--password' + throw usageError( + `login needs "${message.replace(/:$/, '')}" and cannot prompt for it in a non-interactive shell.`, + `Pass ${flag} on the command line.`, + 'norbix login --help', + ) + } + + const prompts = await import('@inquirer/prompts') + return secret ? prompts.password({message, mask: '*'}) : prompts.input({message, required: true}) + } + private async browserLogin( hubUrl: string, projectId: string | undefined, @@ -84,7 +113,8 @@ profile's API key.` const start = await startDeviceFlow(hubUrl, projectId) this.log(`First, copy your one-time code: ${start.userCode}`) - if (process.stdout.isTTY) { + if (this.isInteractive()) { + const {input} = await import('@inquirer/prompts') await input({message: 'Press ENTER to open the browser...'}) openBrowser(start.verificationUriComplete ?? start.verificationUri) } @@ -124,11 +154,10 @@ profile's API key.` }, existing: {project_id?: string; account_id?: string; env?: string; region?: string}, ): Promise { - const projectId = - flags.project ?? existing.project_id ?? (await input({message: 'Project ID:', required: true})) + const projectId = flags.project ?? existing.project_id ?? (await this.ask('Project ID:')) const region = flags.region ?? existing.region - const userName = flags.user ?? (await input({message: 'User name (email):', required: true})) - const pwd = flags.password ?? (await passwordPrompt({message: 'Password:', mask: '*'})) + const userName = flags.user ?? (await this.ask('User name (email):')) + const pwd = flags.password ?? (await this.ask('Password:', true)) const ctx = this.resolveContext({...flags, project: projectId, region}) const client = new Norbix( diff --git a/src/commands/module.ts b/src/commands/module.ts index 113aae6..c4c3ca8 100644 --- a/src/commands/module.ts +++ b/src/commands/module.ts @@ -1,6 +1,5 @@ -import {confirm} from '@inquirer/prompts' import {Norbix} from '@norbix.ai/ts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../base.js' @@ -35,19 +34,15 @@ export default class Module extends BaseCommand { } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt (disable only)', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(Module) const client = this.client(flags) - if (args.action === 'disable' && !flags.yes && process.stdout.isTTY) { - const ok = await confirm({ - message: `Disable the "${args.name}" module for this project?`, - default: false, - }) - if (!ok) return this.print('Cancelled.') + if (args.action === 'disable') { + await this.confirmOrFail(`Disable the "${args.name}" module for this project?`, flags) } const res = await MODULES[args.name][args.action as 'enable' | 'disable'](client) diff --git a/src/commands/payments/disable.ts b/src/commands/payments/disable.ts index a0a635d..8c2f1a3 100644 --- a/src/commands/payments/disable.ts +++ b/src/commands/payments/disable.ts @@ -5,15 +5,23 @@ import {BaseCommand} from '../../base.js' export default class PaymentsDisable extends BaseCommand { static description = 'Disable a payment trigger' - static examples = ['<%= config.bin %> payments disable 66b2f0a1...'] + static examples = [ + '<%= config.bin %> payments disable 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + '<%= config.bin %> payments disable 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + ] static args = { id: Args.string({required: true, description: 'Trigger ID'}), } + static flags = { + ...BaseCommand.mutatingFlags, + } + async run(): Promise { const {args, flags} = await this.parse(PaymentsDisable) const client = this.client(flags) + await this.confirmOrFail(`Disable payment trigger ${args.id}?`, flags) const res = await client.hub.payments.disablePaymentsTrigger({triggerId: args.id}) this.print(`Trigger ${args.id} disabled.`) diff --git a/src/commands/push/campaign/delete.ts b/src/commands/push/campaign/delete.ts index 8a501c8..093bb94 100644 --- a/src/commands/push/campaign/delete.ts +++ b/src/commands/push/campaign/delete.ts @@ -1,5 +1,4 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../../base.js' @@ -13,17 +12,14 @@ export default class PushCampaignDelete extends BaseCommand { } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(PushCampaignDelete) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Delete push campaign ${args.id}?`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Delete push campaign ${args.id}?`, flags) // The route is `/campaigns/{Id}` but the generated request type is empty, // so the id goes in as `Id` — the transport fills tokens by exact name. diff --git a/src/commands/push/delete.ts b/src/commands/push/delete.ts index 8a5ce7f..dbc93d9 100644 --- a/src/commands/push/delete.ts +++ b/src/commands/push/delete.ts @@ -1,5 +1,4 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../base.js' @@ -13,17 +12,14 @@ export default class PushDelete extends BaseCommand { } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(PushDelete) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Delete push template ${args.id}?`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Delete push template ${args.id}?`, flags) // The route token is `{Id}` and the transport fills tokens by exact name, // so the request must carry `Id`. The generated type calls the field `id`, diff --git a/src/commands/push/disable.ts b/src/commands/push/disable.ts index d01523b..69835aa 100644 --- a/src/commands/push/disable.ts +++ b/src/commands/push/disable.ts @@ -1,5 +1,3 @@ -import {confirm} from '@inquirer/prompts' -import {Flags} from '@oclif/core' import {BaseCommand} from '../../base.js' @@ -11,17 +9,14 @@ Run \`push disable-dependencies\` first to see what still depends on push.` static examples = ['<%= config.bin %> push disable --yes'] static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {flags} = await this.parse(PushDisable) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: 'Turn off push for this project?', default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail('Turn off push for this project?', flags) const res = await client.hub.notifications.disablePush({}) diff --git a/src/commands/push/integration/delete.ts b/src/commands/push/integration/delete.ts index 855e4fa..ff8d1dd 100644 --- a/src/commands/push/integration/delete.ts +++ b/src/commands/push/integration/delete.ts @@ -1,5 +1,4 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../../base.js' @@ -13,17 +12,14 @@ export default class PushIntegrationDelete extends BaseCommand { } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(PushIntegrationDelete) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Delete push integration ${args.id}?`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Delete push integration ${args.id}?`, flags) // The generated request type calls this field `id`, but the route token is // `{Id}` and the transport fills tokens by exact name — passing `id` diff --git a/src/commands/push/integration/disable.ts b/src/commands/push/integration/disable.ts index 4394491..1df997d 100644 --- a/src/commands/push/integration/disable.ts +++ b/src/commands/push/integration/disable.ts @@ -5,15 +5,23 @@ import {BaseCommand} from '../../../base.js' export default class PushIntegrationDisable extends BaseCommand { static description = 'Turn a push integration off' - static examples = ['<%= config.bin %> push integration disable 66b2f0a1...'] + static examples = [ + '<%= config.bin %> push integration disable 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + '<%= config.bin %> push integration disable 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } + static flags = { + ...BaseCommand.mutatingFlags, + } + async run(): Promise { const {args, flags} = await this.parse(PushIntegrationDisable) const client = this.client(flags) + await this.confirmOrFail(`Turn off push integration ${args.id}?`, flags) // The generated request type calls this field `id`, but the route token is // `{Id}` and the transport fills tokens by exact name — passing `id` diff --git a/src/commands/push/stop.ts b/src/commands/push/stop.ts index 784c144..ecbcf82 100644 --- a/src/commands/push/stop.ts +++ b/src/commands/push/stop.ts @@ -1,5 +1,4 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../base.js' @@ -13,17 +12,14 @@ export default class PushStop extends BaseCommand { } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(PushStop) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Stop push campaign ${args.id}?`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Stop push campaign ${args.id}?`, flags) // The route is `/campaigns/{Id}/stop` and the transport fills tokens by // exact name, but the generated request type has no field at all — so the diff --git a/src/commands/scheduler/delete.ts b/src/commands/scheduler/delete.ts index 05b9ca7..db463a7 100644 --- a/src/commands/scheduler/delete.ts +++ b/src/commands/scheduler/delete.ts @@ -1,5 +1,4 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../base.js' @@ -13,17 +12,14 @@ export default class SchedulerDelete extends BaseCommand { } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(SchedulerDelete) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Delete scheduler task ${args.id}?`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Delete scheduler task ${args.id}?`, flags) const res = await client.hub.scheduler.deleteSchedulerTask({id: args.id}) this.print(`Task ${args.id} deleted.`) diff --git a/src/commands/scheduler/disable.ts b/src/commands/scheduler/disable.ts index 9b94178..9c8aec3 100644 --- a/src/commands/scheduler/disable.ts +++ b/src/commands/scheduler/disable.ts @@ -5,15 +5,23 @@ import {BaseCommand} from '../../base.js' export default class SchedulerDisable extends BaseCommand { static description = 'Disable a scheduler task' - static examples = ['<%= config.bin %> scheduler disable 66b2f0a1...'] + static examples = [ + '<%= config.bin %> scheduler disable 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + '<%= config.bin %> scheduler disable 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + ] static args = { id: Args.string({required: true, description: 'Task ID'}), } + static flags = { + ...BaseCommand.mutatingFlags, + } + async run(): Promise { const {args, flags} = await this.parse(SchedulerDisable) const client = this.client(flags) + await this.confirmOrFail(`Disable scheduler task ${args.id}?`, flags) const res = await client.hub.scheduler.disableSchedulerTask({id: args.id}) this.print(`Task ${args.id} disabled.`) diff --git a/src/commands/sms/delete.ts b/src/commands/sms/delete.ts index e6f3077..de2be30 100644 --- a/src/commands/sms/delete.ts +++ b/src/commands/sms/delete.ts @@ -1,5 +1,4 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../base.js' @@ -13,17 +12,14 @@ export default class SmsDelete extends BaseCommand { } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(SmsDelete) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Delete SMS template ${args.id}?`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Delete SMS template ${args.id}?`, flags) const res = await client.hub.notifications.deleteSmsTemplate({id: args.id}) this.print(`Template ${args.id} deleted.`) diff --git a/src/commands/sms/stop.ts b/src/commands/sms/stop.ts index 23698cb..ace4ce7 100644 --- a/src/commands/sms/stop.ts +++ b/src/commands/sms/stop.ts @@ -1,5 +1,4 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../base.js' @@ -13,17 +12,14 @@ export default class SmsStop extends BaseCommand { } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(SmsStop) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Stop SMS campaign ${args.id}?`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Stop SMS campaign ${args.id}?`, flags) const n = client.hub.notifications as unknown as { stopSmsCampaign?: (r: {id: string}) => Promise diff --git a/src/commands/users/block.ts b/src/commands/users/block.ts index 1ca628e..e1953c3 100644 --- a/src/commands/users/block.ts +++ b/src/commands/users/block.ts @@ -5,15 +5,23 @@ import {BaseCommand} from '../../base.js' export default class UsersBlock extends BaseCommand { static description = 'Block a user (they can no longer log in)' - static examples = ['<%= config.bin %> users block 66b2f0a1...'] + static examples = [ + '<%= config.bin %> users block 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + '<%= config.bin %> users block 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + ] static args = { id: Args.string({required: true, description: 'User ID'}), } + static flags = { + ...BaseCommand.mutatingFlags, + } + async run(): Promise { const {args, flags} = await this.parse(UsersBlock) const client = this.client(flags) + await this.confirmOrFail(`Block user ${args.id}? They can no longer log in.`, flags) const res = await client.api.membership.blockUser({id: args.id}) this.print(`User ${args.id} blocked.`) diff --git a/src/commands/users/delete.ts b/src/commands/users/delete.ts index 06892bd..9e74461 100644 --- a/src/commands/users/delete.ts +++ b/src/commands/users/delete.ts @@ -1,5 +1,4 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../base.js' @@ -13,17 +12,14 @@ export default class UsersDelete extends BaseCommand { } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(UsersDelete) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Delete user ${args.id}?`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Delete user ${args.id}?`, flags) const res = await client.api.membership.deleteUser({id: args.id}) this.print(`User ${args.id} deleted.`) diff --git a/src/commands/webhooks/disable.ts b/src/commands/webhooks/disable.ts index 9503b13..7e24615 100644 --- a/src/commands/webhooks/disable.ts +++ b/src/commands/webhooks/disable.ts @@ -5,15 +5,23 @@ import {BaseCommand} from '../../base.js' export default class WebhooksDisable extends BaseCommand { static description = 'Disable a webhook destination' - static examples = ['<%= config.bin %> webhooks disable 66b2f0a1...'] + static examples = [ + '<%= config.bin %> webhooks disable 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + '<%= config.bin %> webhooks disable 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + ] static args = { destinationId: Args.string({required: true, description: 'Destination ID'}), } + static flags = { + ...BaseCommand.mutatingFlags, + } + async run(): Promise { const {args, flags} = await this.parse(WebhooksDisable) const client = this.client(flags) + await this.confirmOrFail(`Disable webhook destination ${args.destinationId}?`, flags) const res = await client.hub.webhooks.disableWebhookDestination({ destinationId: args.destinationId, diff --git a/src/commands/webhooks/remove.ts b/src/commands/webhooks/remove.ts index 3266ba5..ffe3168 100644 --- a/src/commands/webhooks/remove.ts +++ b/src/commands/webhooks/remove.ts @@ -1,5 +1,4 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../base.js' @@ -13,20 +12,14 @@ export default class WebhooksRemove extends BaseCommand { } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(WebhooksRemove) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({ - message: `Remove webhook destination ${args.destinationId}?`, - default: false, - }) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Remove webhook destination ${args.destinationId}?`, flags) const res = await client.hub.webhooks.removeWebhookDestination({ destinationId: args.destinationId, diff --git a/src/commands/webhooks/secret.ts b/src/commands/webhooks/secret.ts index 59e1822..bd939b1 100644 --- a/src/commands/webhooks/secret.ts +++ b/src/commands/webhooks/secret.ts @@ -1,4 +1,3 @@ -import {confirm} from '@inquirer/prompts' import {Flags} from '@oclif/core' import {BaseCommand} from '../../base.js' @@ -13,7 +12,7 @@ export default class WebhooksSecret extends BaseCommand { static flags = { rotate: Flags.boolean({description: 'Rotate the secret (old secret stops working)', default: false}), - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { @@ -21,13 +20,7 @@ export default class WebhooksSecret extends BaseCommand { const client = this.client(flags) if (flags.rotate) { - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({ - message: 'Rotate the webhook secret? Consumers still verifying with the old secret will fail.', - default: false, - }) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail('Rotate the webhook secret? Consumers still verifying with the old secret will fail.', flags) const res = await client.hub.webhooks.rotateWebhookIntegrationSecret() this.print(res) diff --git a/src/hooks/command-not-found.ts b/src/hooks/command-not-found.ts new file mode 100644 index 0000000..c842124 --- /dev/null +++ b/src/hooks/command-not-found.ts @@ -0,0 +1,29 @@ +import type {Hook} from '@oclif/core' + +import {EXIT} from '../lib/exit-codes.js' +import {formatErrorText, type ErrorEnvelope} from '../lib/cli-error.js' + +/** + * Unknown command: same envelope as every other error, so `--json` callers + * never get oclif's own text. Exit 2 (usage). + */ +const hook: Hook<'command_not_found'> = async function (opts) { + const id = opts.id.replaceAll(':', ' ') + const envelope: ErrorEnvelope = { + code: 'UNKNOWN_COMMAND', + message: `Unknown command "${id}".`, + exit: EXIT.USAGE, + hint: 'List commands with `norbix --help` or `norbix schema --json`. Any SDK endpoint is reachable with `norbix hub ...` / `norbix api ...`.', + docs: 'norbix --help', + } + + if (opts.argv?.includes('--json')) { + process.stdout.write(JSON.stringify({error: envelope}, null, 2) + '\n') + } else { + process.stderr.write(formatErrorText(envelope) + '\n') + } + + this.exit(EXIT.USAGE) +} + +export default hook diff --git a/src/hooks/init.ts b/src/hooks/init.ts new file mode 100644 index 0000000..6ec807d --- /dev/null +++ b/src/hooks/init.ts @@ -0,0 +1,10 @@ +import type {Hook} from '@oclif/core' + +import {disableColorIfNeeded} from '../lib/color.js' + +/** Runs before every command: no ANSI codes with --json, NO_COLOR or a pipe. */ +const hook: Hook<'init'> = async function (opts) { + disableColorIfNeeded(opts.argv) +} + +export default hook diff --git a/src/lib/cli-error.ts b/src/lib/cli-error.ts new file mode 100644 index 0000000..a4bf04c --- /dev/null +++ b/src/lib/cli-error.ts @@ -0,0 +1,279 @@ +import {EXIT, codeForExit, exitForStatus, type ExitCode} from './exit-codes.js' + +/** + * One error shape for the whole CLI: `{"error": {...}}` on stdout in JSON + * mode, `Error: ... / Hint: ...` on stderr in text mode. Every failure + * (SDK, parser, JSON input, prompt, bug) goes through `toEnvelope`. + * + * No oclif import here on purpose: the MCP server shares this envelope. + */ + +export interface ErrorEnvelope { + /** Machine code: the SDK / gateway code when there is one, else derived. */ + code: string + message: string + /** HTTP status when the server answered. */ + status?: number + exit: ExitCode + /** Field → messages, only when the server named fields. */ + fieldErrors?: Record + /** The URL that was called, when known. */ + url?: string + /** Server trace / correlation id, when the server returned one. */ + traceId?: string + /** What to do next. */ + hint?: string + /** Where to read more: a URL or `norbix --help`. */ + docs?: string +} + +export interface CliErrorInit { + exit: ExitCode + code?: string + message: string + hint?: string + docs?: string + status?: number + fieldErrors?: Record + url?: string + traceId?: string +} + +/** An error the CLI raises itself, already carrying its exit code and hint. */ +export class CliError extends Error { + readonly exit: ExitCode + readonly code: string + readonly hint?: string + readonly docs?: string + readonly status?: number + readonly fieldErrors?: Record + readonly url?: string + readonly traceId?: string + + constructor(init: CliErrorInit) { + super(init.message) + this.name = 'CliError' + this.exit = init.exit + this.code = init.code ?? codeForExit(init.exit) + this.hint = init.hint + this.docs = init.docs + this.status = init.status + this.fieldErrors = init.fieldErrors + this.url = init.url + this.traceId = init.traceId + } +} + +/** Shortcut for the most common CLI-raised error: a usage problem (exit 2). */ +export function usageError(message: string, hint?: string, docs?: string): CliError { + return new CliError({exit: EXIT.USAGE, code: 'USAGE_ERROR', message, hint, docs}) +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function text(value: unknown): string | undefined { + return typeof value === 'string' && value.length > 0 ? value : undefined +} + +/** `responseStatus` block of a gateway body, whatever the casing of the key. */ +function responseStatusOf(raw: unknown): Record | undefined { + if (!isRecord(raw)) return undefined + for (const key of Object.keys(raw)) { + if (key.toLowerCase() === 'responsestatus' && isRecord(raw[key])) return raw[key] as Record + } + + return undefined +} + +/** Trace id from wherever the gateway puts it (top level or responseStatus/meta). */ +export function traceIdOf(raw: unknown): string | undefined { + const rs = responseStatusOf(raw) + const meta = isRecord(rs?.meta) ? rs.meta : undefined + const top = isRecord(raw) ? raw : undefined + return ( + text(top?.traceId) ?? + text(top?.correlationId) ?? + text(rs?.traceId) ?? + text(rs?.correlationId) ?? + text(meta?.traceId) ?? + text(meta?.correlationId) + ) +} + +/** + * Field errors as `{field: [messages]}` from both shapes the gateway uses: + * an `errors` array of `{fieldName, message}` items (ServiceStack) or an + * `errors` object keyed by field. + */ +export function fieldErrorsOf(raw: unknown, items?: Array<{fieldName?: string; message?: string; errorCode?: string}>): Record | undefined { + const out: Record = {} + for (const item of items ?? []) { + if (!item.fieldName) continue + ;(out[item.fieldName] ??= []).push(item.message ?? item.errorCode ?? 'invalid') + } + + const source = responseStatusOf(raw) ?? (isRecord(raw) ? raw : undefined) + const errors = source?.errors + if (isRecord(errors)) { + for (const [field, value] of Object.entries(errors)) { + const messages = Array.isArray(value) ? value.map(String) : [String(value)] + out[field] = [...(out[field] ?? []), ...messages] + } + } + + return Object.keys(out).length > 0 ? out : undefined +} + +interface NorbixErrorLike { + name: string + message: string + status?: number + code?: string + fieldErrors?: Array<{fieldName?: string; message?: string; errorCode?: string}> + raw?: unknown + url?: string +} + +/** Duck-typed: the SDK's NorbixError family, without importing the SDK here. */ +function isNorbixError(err: unknown): err is NorbixErrorLike { + return err instanceof Error && typeof (err as {status?: unknown}).status === 'number' && err.name.startsWith('Norbix') +} + +const SDK_LOCAL_CODES: Record = { + NORBIX_NOT_AUTHENTICATED: EXIT.AUTH, + NORBIX_ACCOUNT_SCOPE_REQUIRED: EXIT.USAGE, + NORBIX_MISSING_PATH_PARAM: EXIT.USAGE, +} + +function hintForExit(exit: ExitCode, env: {status?: number; url?: string}, command?: string): string { + const help = command ? `norbix ${command} --help` : 'norbix --help' + switch (exit) { + case EXIT.AUTH: + return env.status === 403 + ? 'The key is valid but has no permission for this call. Use a key with the right permissions, or --profile .' + : 'Not authenticated or the session expired. Run `norbix login`, pass --api-key, or use --profile .' + case EXIT.NOT_FOUND: + return 'Check the id, the --env and the --project. List items first (e.g. `norbix users list --json`).' + case EXIT.VALIDATION: + return `The server rejected the request. fieldErrors names the fields; see ${help}.` + case EXIT.NETWORK: + return `Could not reach ${env.url ?? 'the endpoint'}. Check --region, api_url / hub_url in the profile, and connectivity.` + case EXIT.SERVER: + return 'Retry with backoff. If it keeps failing, report it with the traceId.' + case EXIT.USAGE: + return `Run ${help} or \`norbix schema ${command ?? ''} --json\`.`.replace(' ', ' ') + default: + return 'Unexpected error. Re-run with --json for details and report it with the traceId.' + } +} + +/** Map any thrown value to the one error envelope. */ +export function toEnvelope(err: unknown, ctx: {command?: string} = {}): ErrorEnvelope { + const docs = ctx.command ? `norbix ${ctx.command} --help` : 'norbix --help' + + if (err instanceof CliError) { + return compact({ + code: err.code, + message: err.message, + status: err.status, + exit: err.exit, + fieldErrors: err.fieldErrors, + url: err.url, + traceId: err.traceId, + hint: err.hint ?? hintForExit(err.exit, {status: err.status, url: err.url}, ctx.command), + docs: err.docs ?? docs, + }) + } + + if (isNorbixError(err)) { + const isNetwork = err.name === 'NorbixNetworkError' || err.name === 'NorbixTimeoutError' + const exit: ExitCode = isNetwork + ? EXIT.NETWORK + : (err.code && SDK_LOCAL_CODES[err.code]) || exitForStatus(err.status === 0 ? undefined : err.status) + // The SDK reads `errorCode`; some answers carry `code` instead. + const bodyCode = isRecord(err.raw) ? (text(err.raw.code) ?? text(err.raw.errorCode)) : undefined + const code = err.code ?? bodyCode ?? (isNetwork && err.name === 'NorbixTimeoutError' ? 'TIMEOUT' : codeForExit(exit)) + return compact({ + code, + message: err.message, + status: err.status && err.status > 0 ? err.status : undefined, + exit, + fieldErrors: fieldErrorsOf(err.raw, err.fieldErrors), + url: err.url, + traceId: traceIdOf(err.raw), + hint: hintForExit(exit, {status: err.status, url: err.url}, ctx.command), + docs, + }) + } + + if (err instanceof Error) { + const anyErr = err as Error & {oclif?: {exit?: number}; code?: string; parse?: unknown} + // Ctrl+C at an @inquirer prompt. + if (err.name === 'ExitPromptError') { + return compact({code: 'CANCELLED', message: 'Cancelled.', exit: EXIT.CANCELLED}) + } + + // oclif parser errors (missing arg, unknown flag, bad option) and every + // `this.error(...)` call: usage, exit 2. + if (anyErr.parse !== undefined || anyErr.oclif?.exit === 2 || /Error$/.test(err.name) && anyErr.oclif !== undefined) { + return compact({ + code: 'USAGE_ERROR', + message: cleanParserMessage(err.message), + exit: EXIT.USAGE, + hint: hintForExit(EXIT.USAGE, {}, ctx.command), + docs, + }) + } + + return compact({ + code: 'INTERNAL_ERROR', + message: err.message || String(err), + exit: EXIT.INTERNAL, + hint: hintForExit(EXIT.INTERNAL, {}, ctx.command), + docs, + }) + } + + return compact({ + code: 'INTERNAL_ERROR', + message: String(err), + exit: EXIT.INTERNAL, + hint: hintForExit(EXIT.INTERNAL, {}, ctx.command), + docs, + }) +} + +/** oclif's parser wraps its message in boilerplate the envelope already covers. */ +function cleanParserMessage(message: string): string { + return message + .replace(/^The following errors? occurred:\s*/, '') + .replace(/\s*See more help with --help\s*$/, '') + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .join('\n') +} + +/** Text rendering for stderr (no ANSI codes). */ +export function formatErrorText(env: ErrorEnvelope): string { + const lines = [`Error: ${env.message}`] + const details: string[] = [] + if (env.code && env.code !== 'INTERNAL_ERROR' && env.code !== 'USAGE_ERROR') details.push(`code: ${env.code}`) + if (env.status !== undefined) details.push(`status: ${env.status}`) + if (env.url) details.push(`url: ${env.url}`) + if (env.traceId) details.push(`traceId: ${env.traceId}`) + if (env.fieldErrors) { + for (const [field, messages] of Object.entries(env.fieldErrors)) details.push(`${field}: ${messages.join('; ')}`) + } + + for (const d of details) lines.push(` ${d}`) + if (env.hint) lines.push(`Hint: ${env.hint}`) + if (env.docs) lines.push(`Docs: ${env.docs}`) + return lines.join('\n') +} + +function compact(env: ErrorEnvelope): ErrorEnvelope { + return Object.fromEntries(Object.entries(env).filter(([, v]) => v !== undefined)) as unknown as ErrorEnvelope +} diff --git a/src/lib/color.ts b/src/lib/color.ts new file mode 100644 index 0000000..c768f09 --- /dev/null +++ b/src/lib/color.ts @@ -0,0 +1,19 @@ +/** + * The CLI uses no colour library. This check exists so that stays true in + * the places that could colour (oclif's own help / error theme): no ANSI + * codes with --json, NO_COLOR, or when stdout is not a terminal. + */ +export function colorEnabled(argv: string[] = process.argv.slice(2)): boolean { + if (argv.includes('--json')) return false + if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '') return false + if (process.env.FORCE_COLOR === '0') return false + return Boolean(process.stdout.isTTY) +} + +/** Tell oclif (and anything else that honours the convention) to stay plain. */ +export function disableColorIfNeeded(argv?: string[]): void { + if (!colorEnabled(argv)) { + process.env.FORCE_COLOR = '0' + process.env.NO_COLOR ??= '1' + } +} diff --git a/src/lib/exit-codes.ts b/src/lib/exit-codes.ts new file mode 100644 index 0000000..0e98f0c --- /dev/null +++ b/src/lib/exit-codes.ts @@ -0,0 +1,118 @@ +/** + * Exit codes of the Norbix CLI — the agent contract (docs/agent-contract.md). + * + * No oclif import here on purpose: the MCP server shares this table. + * Codes are identical with and without `--json`. + */ + +export const EXIT = { + OK: 0, + /** Unexpected / internal error — file an issue, include `traceId`. */ + INTERNAL: 1, + /** Usage: bad flags/args, invalid JSON input, unknown command, missing config. */ + USAGE: 2, + /** Confirmation required: non-interactive shell and no `--yes`. */ + CONFIRMATION_REQUIRED: 3, + /** Not authenticated / auth rejected (401, 403, no key). */ + AUTH: 4, + /** Not found (404). */ + NOT_FOUND: 5, + /** Validation rejected by the server (400/422, or a 200 with isSuccess=false). */ + VALIDATION: 6, + /** Network / timeout / endpoint unreachable. */ + NETWORK: 7, + /** Rate limited / server error (429, 5xx). */ + SERVER: 8, + /** Cancelled by the user at a prompt. */ + CANCELLED: 9, +} as const + +export type ExitCode = (typeof EXIT)[keyof typeof EXIT] + +export interface ExitCodeInfo { + code: ExitCode + name: keyof typeof EXIT + meaning: string + hint: string +} + +/** The documented table, in order — `norbix schema --json` prints it. */ +export const EXIT_CODES: ExitCodeInfo[] = [ + {code: EXIT.OK, name: 'OK', meaning: 'success', hint: ''}, + {code: EXIT.INTERNAL, name: 'INTERNAL', meaning: 'unexpected / internal error', hint: 'file an issue, include traceId'}, + { + code: EXIT.USAGE, + name: 'USAGE', + meaning: 'usage: bad flags/args, invalid JSON input, unknown command, missing config', + hint: 'norbix --help / norbix schema --json', + }, + { + code: EXIT.CONFIRMATION_REQUIRED, + name: 'CONFIRMATION_REQUIRED', + meaning: 'confirmation required (non-interactive, no --yes)', + hint: 're-run with --yes after a --dry-run', + }, + { + code: EXIT.AUTH, + name: 'AUTH', + meaning: 'not authenticated / auth rejected (401, 403, no key)', + hint: 'norbix login, --api-key, --profile', + }, + {code: EXIT.NOT_FOUND, name: 'NOT_FOUND', meaning: 'not found (404)', hint: 'check id / env / project'}, + { + code: EXIT.VALIDATION, + name: 'VALIDATION', + meaning: 'validation rejected by server (400/422)', + hint: 'fieldErrors lists the fields', + }, + { + code: EXIT.NETWORK, + name: 'NETWORK', + meaning: 'network / timeout / endpoint unreachable', + hint: 'shows the URL; check --region, api_url, connectivity', + }, + { + code: EXIT.SERVER, + name: 'SERVER', + meaning: 'rate limited / server error (429, 5xx)', + hint: 'retry with backoff; include traceId', + }, + {code: EXIT.CANCELLED, name: 'CANCELLED', meaning: 'cancelled by user at a prompt', hint: ''}, +] + +/** Exit code for an HTTP status the server answered with. */ +export function exitForStatus(status: number | undefined): ExitCode { + if (status === undefined) return EXIT.INTERNAL + if (status === 401 || status === 403) return EXIT.AUTH + if (status === 404) return EXIT.NOT_FOUND + if (status === 400 || status === 422) return EXIT.VALIDATION + if (status === 429 || (status >= 500 && status <= 599)) return EXIT.SERVER + // A business refusal comes back as HTTP 200 with isSuccess=false. + if (status === 200) return EXIT.VALIDATION + if (status >= 400) return EXIT.VALIDATION + return EXIT.INTERNAL +} + +/** Default machine code for an exit code when the server gave none. */ +export function codeForExit(exit: ExitCode): string { + switch (exit) { + case EXIT.USAGE: + return 'USAGE_ERROR' + case EXIT.CONFIRMATION_REQUIRED: + return 'CONFIRMATION_REQUIRED' + case EXIT.AUTH: + return 'UNAUTHENTICATED' + case EXIT.NOT_FOUND: + return 'NOT_FOUND' + case EXIT.VALIDATION: + return 'VALIDATION_FAILED' + case EXIT.NETWORK: + return 'NETWORK_ERROR' + case EXIT.SERVER: + return 'SERVER_ERROR' + case EXIT.CANCELLED: + return 'CANCELLED' + default: + return 'INTERNAL_ERROR' + } +} diff --git a/src/lib/json.ts b/src/lib/json.ts index 3d20701..fa6ee5a 100644 --- a/src/lib/json.ts +++ b/src/lib/json.ts @@ -1,3 +1,5 @@ +import {usageError} from './cli-error.js' + /** Helpers for JSON flags: accept inline JSON or `-` to read from stdin. */ export async function readJsonInput(value: string, flagName: string): Promise { @@ -7,7 +9,10 @@ export async function readJsonInput(value: string, flagName: string): Promise)[moduleName] const res = await liveModule[method](request) this.print(res) diff --git a/test/files.test.ts b/test/files.test.ts index 720c86f..3f703de 100644 --- a/test/files.test.ts +++ b/test/files.test.ts @@ -487,9 +487,12 @@ describe('norbix files integrations test', () => { it('turns a 403 into the usual error line: code, message, HTTP status', async () => { answer({responseStatus: {errorCode: 'Forbidden', message: 'Missing permission files:create'}}, 403) - const {error} = await runCommand(['files', 'integrations', 'test', INTEGRATION, ...auth]) + const {error, stderr} = await runCommand(['files', 'integrations', 'test', INTEGRATION, ...auth]) - expect(error?.message).toBe('Forbidden: Missing permission files:create (HTTP 403)') + expect(error).toMatchObject({message: 'Missing permission files:create', code: 'Forbidden', status: 403}) + expect(stderr).toContain('Error: Missing permission files:create') + expect(stderr).toContain('code: Forbidden') + expect(stderr).toContain('status: 403') }) /** @@ -511,9 +514,12 @@ describe('norbix files integrations test', () => { 404, ) - const {error} = await runCommand(['files', 'integrations', 'test', INTEGRATION, ...auth]) + const {error, stderr} = await runCommand(['files', 'integrations', 'test', INTEGRATION, ...auth]) - expect(error?.message).toBe('CM-ERRORS-FILES-004: Files integration was not found. (HTTP 404)') + expect(error).toMatchObject({message: 'Files integration was not found.', code: 'CM-ERRORS-FILES-004', status: 404}) + expect(stderr).toContain('Error: Files integration was not found.') + expect(stderr).toContain('code: CM-ERRORS-FILES-004') + expect(stderr).toContain('status: 404') }) it('falls back to a plain line when a 500 body is not JSON', async () => { diff --git a/tests/push-routes.test.ts b/tests/push-routes.test.ts index f851cd4..5b52ef7 100644 --- a/tests/push-routes.test.ts +++ b/tests/push-routes.test.ts @@ -65,7 +65,7 @@ const routes: Array<[string[], string, string]> = [ [['integrations'], 'GET', `${P}/integrations`], [['integration', ID], 'GET', `${P}/integrations/${ID}`], [['integration', 'enable', ID], 'PUT', `${P}/integrations/${ID}/enable`], - [['integration', 'disable', ID], 'PUT', `${P}/integrations/${ID}/disable`], + [['integration', 'disable', ID, '--yes'], 'PUT', `${P}/integrations/${ID}/disable`], [['integration', 'default', ID], 'PUT', `${P}/integrations/${ID}/default`], [['integration', 'delete', ID, '--yes'], 'DELETE', `${P}/integrations/${ID}`], [['integration', 'test', '--integration', ID, '--token', 'dGVzdA==', '--family', 'Ios'], 'POST', `${P}/integrations/test`], diff --git a/tests/push.test.ts b/tests/push.test.ts index 066333f..c95b8f3 100644 --- a/tests/push.test.ts +++ b/tests/push.test.ts @@ -113,7 +113,7 @@ describe('push preview — signed link, no login, no project', () => { it('today the installed SDK still refuses a call with no token', async () => { const {error} = await runOclif(['push', 'preview', '--hash', 'abc.def', '--region', 'nb-eu-germany']) - expect(error?.message).toMatch(/NORBIX_NOT_AUTHENTICATED/) + expect((error as {code?: string} | undefined)?.code).toBe('NORBIX_NOT_AUTHENTICATED') expect(sent).toHaveLength(0) }) }) @@ -142,7 +142,7 @@ describe('push integration state changes', () => { // throws NORBIX_MISSING_PATH_PARAM at runtime, which is why it is pinned. const cases = [ {command: PushIntegrationEnable, method: 'enablePushIntegration', argv: [ID]}, - {command: PushIntegrationDisable, method: 'disablePushIntegration', argv: [ID]}, + {command: PushIntegrationDisable, method: 'disablePushIntegration', argv: [ID, '--yes']}, {command: PushIntegrationDefault, method: 'setPushIntegrationAsDefault', argv: [ID]}, {command: PushIntegrationDelete, method: 'deletePushIntegration', argv: [ID, '--yes']}, ] From e68cf87a65cde92701ff7d1cb806c377e4b78b61 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Fri, 2 Oct 2026 12:36:45 +0300 Subject: [PATCH 02/14] =?UTF-8?q?test(cli):=20agent-mode=20tests=20?= =?UTF-8?q?=E2=80=94=20exit=20codes,=20envelope,=20dry=20run,=20destructiv?= =?UTF-8?q?e=20sweep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan step [1] of docs/tasks/agent-native-cli.md. The built CLI runs as a child process (pipes everywhere, like an agent sees it) against a fake gateway on a free port. Covers: every destructive command exits 3 without --yes and sends nothing, runs with --yes; --dry-run prints the call and sends nothing, and still fails on a missing region or key; the JSON envelope and exit code for 404, 400 with fieldErrors, 401, 500, network, usage, unknown command, bad JSON input, unknown profile and login without a terminal — the same exit code with and without --json, stdout one JSON document, stderr empty; no ANSI codes. Plus unit tests for toEnvelope. Co-Authored-By: Claude Fable 5.1 --- tests/_cli.ts | 155 ++++++++++++++++++++++ tests/agent-mode.test.ts | 271 +++++++++++++++++++++++++++++++++++++++ tests/cli-error.test.ts | 142 ++++++++++++++++++++ 3 files changed, 568 insertions(+) create mode 100644 tests/_cli.ts create mode 100644 tests/agent-mode.test.ts create mode 100644 tests/cli-error.test.ts diff --git a/tests/_cli.ts b/tests/_cli.ts new file mode 100644 index 0000000..279e529 --- /dev/null +++ b/tests/_cli.ts @@ -0,0 +1,155 @@ +import {execFile} from 'node:child_process' +import {mkdirSync, mkdtempSync, writeFileSync} from 'node:fs' +import http from 'node:http' +import {tmpdir} from 'node:os' +import {join} from 'node:path' +import {fileURLToPath} from 'node:url' + +/** + * Test seam for the agent contract: the BUILT CLI is run as a child process + * (stdin, stdout and stderr are pipes — exactly what a coding agent or a + * script sees) against a fake gateway on a free local port. Nothing else is + * contacted. Exit codes, stdout/stderr separation and the JSON envelope are + * observed from the outside, as an agent would observe them. + */ + +export const CLI = fileURLToPath(new URL('../bin/run.js', import.meta.url)) + +export interface Hit { + method: string + url: string + body: string +} + +export interface FakeGateway { + port: number + hits: Hit[] + close: () => Promise +} + +/** A gateway that answers by path: missing→404, bad→400, unauth→401, boom→500, else 200 {ok:true}. */ +export async function startFakeGateway(): Promise { + const hits: Hit[] = [] + const server = http.createServer((req, res) => { + let body = '' + req.on('data', (chunk) => (body += chunk)) + req.on('end', () => { + hits.push({method: req.method ?? '', url: req.url ?? '', body}) + res.setHeader('content-type', 'application/json') + const url = req.url ?? '' + if (url.includes('missing')) { + res.statusCode = 404 + return res.end('{"message":"User not found","code":"USER_NOT_FOUND","traceId":"t-1"}') + } + + if (url.includes('bad')) { + res.statusCode = 400 + return res.end('{"message":"Validation failed","errors":{"email":["is required"]}}') + } + + if (url.includes('unauth')) { + res.statusCode = 401 + return res.end('{"message":"bad key"}') + } + + if (url.includes('boom')) { + res.statusCode = 500 + return res.end('{"responseStatus":{"errorCode":"CM-ERRORS-INTERNAL","message":"boom","meta":{"correlationId":"c-9"}}}') + } + + res.end('{"ok":true}') + }) + }) + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) + const address = server.address() + const port = typeof address === 'object' && address ? address.port : 0 + return { + port, + hits, + close: () => new Promise((resolve) => server.close(() => resolve())), + } +} + +/** + * A HOME with four profiles: + * x → the fake gateway + * y → a closed port (network error) + * noauth → the fake gateway, no api key + * z → default norbix.ai endpoints, no region (context error) + */ +export function makeHome(port: number): string { + const home = mkdtempSync(join(tmpdir(), 'norbix-agent-')) + mkdirSync(join(home, '.norbix'), {recursive: true}) + writeFileSync( + join(home, '.norbix', 'config'), + [ + '[x]', + 'api_key=k', + 'project_id=p', + `api_url=http://127.0.0.1:${port}`, + `hub_url=http://127.0.0.1:${port}`, + '[y]', + 'api_key=k', + 'project_id=p', + 'api_url=http://127.0.0.1:1', + 'hub_url=http://127.0.0.1:1', + '[noauth]', + 'project_id=p', + `api_url=http://127.0.0.1:${port}`, + `hub_url=http://127.0.0.1:${port}`, + '[z]', + 'api_key=k', + 'project_id=p', + '', + ].join('\n'), + ) + return home +} + +export interface CliResult { + code: number + stdout: string + stderr: string +} + +/** Run the built CLI with pipes for every stream (non-interactive). */ +export function cli(home: string, args: string[], env: Record = {}): Promise { + return new Promise((resolve) => { + const child = execFile( + process.execPath, + [CLI, ...args], + { + env: {...process.env, HOME: home, USERPROFILE: home, CI: undefined, NO_COLOR: undefined, FORCE_COLOR: undefined, ...env}, + cwd: home, + maxBuffer: 10 * 1024 * 1024, + }, + (error, stdout, stderr) => { + const code = error && typeof (error as {code?: unknown}).code === 'number' ? ((error as {code: number}).code) : 0 + resolve({code, stdout: String(stdout), stderr: String(stderr)}) + }, + ) + child.stdin?.end() + }) +} + +/** Run jobs with a small concurrency so a loaded machine still finishes. */ +export async function pool(items: T[], limit: number, job: (item: T) => Promise): Promise { + const results: R[] = Array.from({length: items.length}) + let next = 0 + const workers = Array.from({length: Math.min(limit, items.length)}, async () => { + while (next < items.length) { + const i = next++ + results[i] = await job(items[i]) + } + }) + await Promise.all(workers) + return results +} + +/** stdout must be exactly one JSON document. */ +export function parseSingleJson(stdout: string): unknown { + const parsed = JSON.parse(stdout) as unknown + // JSON.parse would already throw on two documents; also refuse leading text. + if (!/^\s*[{[]/.test(stdout)) throw new Error(`stdout is not a JSON document: ${stdout.slice(0, 80)}`) + return parsed +} diff --git a/tests/agent-mode.test.ts b/tests/agent-mode.test.ts new file mode 100644 index 0000000..2e7aec9 --- /dev/null +++ b/tests/agent-mode.test.ts @@ -0,0 +1,271 @@ +import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest' + +import {cli, makeHome, parseSingleJson, pool, startFakeGateway, type FakeGateway} from './_cli.js' + +/** + * The agent contract (docs/agent-contract.md), observed from outside the + * process: exit codes, one JSON document on stdout, nothing sent without + * --yes, dry runs that send nothing. + */ + +let gateway: FakeGateway +let home: string + +beforeAll(async () => { + gateway = await startFakeGateway() + home = makeHome(gateway.port) +}) + +afterAll(async () => { + await gateway.close() +}) + +beforeEach(() => { + gateway.hits.length = 0 +}) + +/** Every destructive command, with the arguments it needs — without --yes. */ +const DESTRUCTIVE: string[][] = [ + ['users', 'delete', 'abc123'], + ['users', 'block', 'abc123'], + ['sms', 'delete', 'abc123'], + ['sms', 'stop', 'abc123'], + ['email', 'delete', 'abc123'], + ['email', 'stop', 'abc123'], + ['push', 'delete', 'abc123'], + ['push', 'stop', 'abc123'], + ['push', 'disable'], + ['push', 'campaign', 'delete', 'abc123'], + ['push', 'integration', 'delete', 'abc123'], + ['push', 'integration', 'disable', 'abc123'], + ['files', 'delete', 'invoices/2026/invoice.pdf', '--integration', 'fi_1'], + ['scheduler', 'delete', 'abc123'], + ['scheduler', 'disable', 'abc123'], + ['env', 'delete', 'STAGING'], + ['apikeys', 'regenerate'], + ['webhooks', 'remove', 'abc123'], + ['webhooks', 'disable', 'abc123'], + ['webhooks', 'secret', '--rotate'], + ['payments', 'disable', 'abc123'], + ['module', 'disable', 'sms'], + ['db', 'delete', 'orders', '--id', 'abc123'], + ['db', 'delete', 'orders', '--many', '--filter', '{}'], + ['db', 'update', 'orders', '--many', '--filter', '{}', '--update', '{"$set":{"a":1}}'], + ['api', 'membership', 'user', 'delete', 'abc123'], + ['hub', 'scheduler', 'task', 'delete', 'abc123'], +] + +describe('destructive commands in a non-interactive shell', () => { + it('every one exits 3 without --yes and sends nothing', async () => { + const results = await pool(DESTRUCTIVE, 4, async (argv) => ({argv, ...(await cli(home, [...argv, '--profile', 'x']))})) + const wrong = results.filter((r) => r.code !== 3 || !/--yes/.test(r.stderr) || r.stdout !== '') + expect(wrong.map((r) => `${r.argv.join(' ')} → exit ${r.code}: ${r.stderr.slice(0, 120)}`)).toEqual([]) + expect(gateway.hits).toEqual([]) + }) + + it('every one exits 3 with --json too, with the envelope on stdout', async () => { + const results = await pool(DESTRUCTIVE, 4, async (argv) => ({argv, ...(await cli(home, [...argv, '--profile', 'x', '--json']))})) + for (const r of results) { + expect(r.code, r.argv.join(' ')).toBe(3) + expect(r.stderr, r.argv.join(' ')).toBe('') + const doc = parseSingleJson(r.stdout) as {error: {code: string; exit: number; hint: string}} + expect(doc.error.code).toBe('CONFIRMATION_REQUIRED') + expect(doc.error.exit).toBe(3) + expect(doc.error.hint).toMatch(/--yes/) + } + + expect(gateway.hits).toEqual([]) + }) + + it('every one runs with --yes and reaches the gateway', async () => { + const results = await pool(DESTRUCTIVE, 4, async (argv) => ({argv, ...(await cli(home, [...argv, '--profile', 'x', '--yes']))})) + const wrong = results.filter((r) => r.code !== 0) + expect(wrong.map((r) => `${r.argv.join(' ')} → exit ${r.code}: ${r.stderr.slice(0, 160)}`)).toEqual([]) + expect(gateway.hits.length).toBe(DESTRUCTIVE.length) + }) +}) + +describe('--dry-run', () => { + it('prints the SDK call and the HTTP request, sends nothing, exits 0 (json)', async () => { + const r = await cli(home, ['users', 'delete', 'abc123', '--profile', 'x', '--dry-run', '--json']) + expect(r.code).toBe(0) + expect(r.stderr).toBe('') + const doc = parseSingleJson(r.stdout) as {dryRun: boolean; method: string; request: unknown; http: {method: string; url: string; headers: Record}} + expect(doc.dryRun).toBe(true) + expect(doc.method).toBe('api.membership.deleteUser') + expect(doc.request).toEqual({id: 'abc123'}) + expect(doc.http.method).toBe('DELETE') + expect(doc.http.url).toMatch(/\/v2\/membership\/auth\?id=abc123$/) + expect(doc.http.headers.authorization).toBe('Bearer ***') + expect(gateway.hits).toEqual([]) + }) + + it('prints the SDK call in text mode too', async () => { + const r = await cli(home, ['db', 'update', 'orders', '--many', '--filter', '{}', '--update', '{"$set":{"a":1}}', '--profile', 'x', '--dry-run']) + expect(r.code).toBe(0) + expect(r.stdout).toContain('api.database.updateMany') + expect(r.stdout).toContain('PUT ') + expect(r.stdout).toContain('"collectionName": "orders"') + expect(gateway.hits).toEqual([]) + }) + + it('works for the plain-word hub/api commands', async () => { + const r = await cli(home, ['hub', 'scheduler', 'task', 'delete', 'abc123', '--profile', 'x', '--dry-run', '--json']) + expect(r.code).toBe(0) + const doc = parseSingleJson(r.stdout) as {method: string; request: unknown} + expect(doc.method).toBe('hub.scheduler.deleteSchedulerTask') + expect(doc.request).toEqual({id: 'abc123'}) + expect(gateway.hits).toEqual([]) + }) + + it('still resolves the context: a missing region fails', async () => { + const r = await cli(home, ['users', 'delete', 'abc123', '--profile', 'z', '--dry-run', '--json']) + expect(r.code).toBe(2) + const doc = parseSingleJson(r.stdout) as {error: {message: string}} + expect(doc.error.message).toMatch(/Region is required/) + }) + + it('still resolves the context: missing auth fails with exit 4', async () => { + const r = await cli(home, ['users', 'delete', 'abc123', '--profile', 'noauth', '--dry-run', '--json']) + expect(r.code).toBe(4) + const doc = parseSingleJson(r.stdout) as {error: {code: string}} + expect(doc.error.code).toBe('UNAUTHENTICATED') + }) +}) + +interface Case { + name: string + argv: string[] + exit: number + code: string + check?: (error: Record) => void +} + +const CASES: Case[] = [ + { + name: '404 not found', + argv: ['users', 'get', 'missing', '--profile', 'x'], + exit: 5, + code: 'USER_NOT_FOUND', + check: (e) => { + expect(e.status).toBe(404) + expect(e.traceId).toBe('t-1') + expect(e.url).toMatch(/\/v2\/membership\/auth\/missing$/) + }, + }, + { + name: '400 with field errors', + argv: ['users', 'get', 'bad', '--profile', 'x'], + exit: 6, + code: 'NORBIX_VALIDATION_ERROR', + check: (e) => { + expect(e.status).toBe(400) + expect(e.fieldErrors).toEqual({email: ['is required']}) + }, + }, + { + name: '401 rejected', + argv: ['users', 'get', 'unauth', '--profile', 'x'], + exit: 4, + code: 'NORBIX_AUTH_ERROR', + check: (e) => expect(e.status).toBe(401), + }, + { + name: '500 server error with a trace id', + argv: ['users', 'get', 'boom', '--profile', 'x'], + exit: 8, + code: 'CM-ERRORS-INTERNAL', + check: (e) => { + expect(e.status).toBe(500) + expect(e.traceId).toBe('c-9') + }, + }, + { + name: 'network failure', + argv: ['users', 'list', '--profile', 'y'], + exit: 7, + code: 'NORBIX_NETWORK_ERROR', + check: (e) => expect(e.url).toMatch(/^http:\/\/127\.0\.0\.1:1\//), + }, + { + name: 'usage error: missing required flag', + argv: ['db', 'update', 'orders', '--profile', 'x'], + exit: 2, + code: 'USAGE_ERROR', + check: (e) => expect(e.message).toMatch(/Missing required flag update/), + }, + { + name: 'unknown command', + argv: ['nothing'], + exit: 2, + code: 'UNKNOWN_COMMAND', + }, + { + name: 'bad --filter JSON', + argv: ['db', 'find', 'orders', '--filter', '{bad', '--profile', 'x'], + exit: 2, + code: 'USAGE_ERROR', + check: (e) => expect(e.message).toMatch(/--filter is not valid JSON/), + }, + { + name: 'unknown profile', + argv: ['users', 'list', '--profile', 'nope'], + exit: 2, + code: 'USAGE_ERROR', + }, + { + name: 'login without a terminal', + argv: ['login', '--user', 'alice@example.com'], + exit: 2, + code: 'USAGE_ERROR', + check: (e) => expect(e.hint).toMatch(/--api-key/), + }, +] + +describe('error envelope', () => { + for (const c of CASES) { + it(`${c.name}: --json gives one document, exit ${c.exit}`, async () => { + const r = await cli(home, [...c.argv, '--json']) + expect(r.code).toBe(c.exit) + expect(r.stderr).toBe('') + const doc = parseSingleJson(r.stdout) as {error: Record} + expect(doc.error.code).toBe(c.code) + expect(doc.error.exit).toBe(c.exit) + expect(typeof doc.error.message).toBe('string') + expect(typeof doc.error.hint).toBe('string') + expect(typeof doc.error.docs).toBe('string') + c.check?.(doc.error) + }) + + it(`${c.name}: same exit code without --json, text on stderr only`, async () => { + const r = await cli(home, c.argv) + expect(r.code).toBe(c.exit) + expect(r.stdout).toBe('') + expect(r.stderr).toMatch(/^Error: /) + expect(r.stderr).toMatch(/\nHint: /) + expect(r.stderr).not.toMatch(/\u001B\[/) + expect(r.stderr).not.toMatch(/at .*\.js:\d+/) + }) + } + + it('the usage envelope stays short (no parse-context dump)', async () => { + const r = await cli(home, ['db', 'update', 'orders', '--profile', 'x', '--json']) + expect(r.stdout.split('\n').length).toBeLessThan(30) + }) +}) + +describe('plain output', () => { + it('success with --json is one JSON document and no ANSI codes', async () => { + const r = await cli(home, ['users', 'get', 'ok1', '--profile', 'x', '--json']) + expect(r.code).toBe(0) + expect(r.stderr).toBe('') + expect(parseSingleJson(r.stdout)).toEqual({ok: true}) + expect(r.stdout).not.toMatch(/\u001B\[/) + }) + + it('a destructive command with --yes prints its text line on stdout', async () => { + const r = await cli(home, ['users', 'delete', 'abc123', '--profile', 'x', '--yes']) + expect(r.code).toBe(0) + expect(r.stdout.trim()).toBe('User abc123 deleted.') + }) +}) diff --git a/tests/cli-error.test.ts b/tests/cli-error.test.ts new file mode 100644 index 0000000..9d3e359 --- /dev/null +++ b/tests/cli-error.test.ts @@ -0,0 +1,142 @@ +import {describe, expect, it} from 'vitest' + +import {CliError, fieldErrorsOf, formatErrorText, toEnvelope, traceIdOf, usageError} from '../src/lib/cli-error.js' +import {EXIT, EXIT_CODES, exitForStatus} from '../src/lib/exit-codes.js' + +/** A stand-in for the SDK's NorbixError family (duck-typed by name + status). */ +function sdkError(name: string, init: {message: string; status: number; code?: string; raw?: unknown; url?: string; fieldErrors?: unknown[]}): Error { + const err = Object.assign(new Error(init.message), init) + err.name = name + return err +} + +describe('exit codes', () => { + it('maps HTTP statuses to the documented codes', () => { + expect(exitForStatus(401)).toBe(EXIT.AUTH) + expect(exitForStatus(403)).toBe(EXIT.AUTH) + expect(exitForStatus(404)).toBe(EXIT.NOT_FOUND) + expect(exitForStatus(400)).toBe(EXIT.VALIDATION) + expect(exitForStatus(422)).toBe(EXIT.VALIDATION) + expect(exitForStatus(429)).toBe(EXIT.SERVER) + expect(exitForStatus(503)).toBe(EXIT.SERVER) + expect(exitForStatus(200)).toBe(EXIT.VALIDATION) + }) + + it('documents every code once, 0 to 9', () => { + expect(EXIT_CODES.map((c) => c.code)).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + }) +}) + +describe('toEnvelope', () => { + it('passes a CliError through with its hint and docs', () => { + const env = toEnvelope(new CliError({exit: EXIT.CONFIRMATION_REQUIRED, code: 'CONFIRMATION_REQUIRED', message: 'Confirmation required: x', hint: 'Re-run with --yes.'}), {command: 'users delete'}) + expect(env).toEqual({ + code: 'CONFIRMATION_REQUIRED', + message: 'Confirmation required: x', + exit: 3, + hint: 'Re-run with --yes.', + docs: 'norbix users delete --help', + }) + }) + + it('maps a 404 from the SDK, keeping the body code and trace id', () => { + const env = toEnvelope(sdkError('NorbixError', {message: 'User not found', status: 404, raw: {code: 'USER_NOT_FOUND', traceId: 't-1'}, url: 'http://x/u/1'}), {command: 'users get'}) + expect(env.exit).toBe(5) + expect(env.code).toBe('USER_NOT_FOUND') + expect(env.status).toBe(404) + expect(env.traceId).toBe('t-1') + expect(env.url).toBe('http://x/u/1') + expect(env.hint).toMatch(/Check the id/) + }) + + it('maps a network error to exit 7 without a status', () => { + const env = toEnvelope(sdkError('NorbixNetworkError', {message: 'fetch failed', status: 0, code: 'NORBIX_NETWORK_ERROR', url: 'http://localhost:1/v2/x'})) + expect(env.exit).toBe(7) + expect(env.status).toBeUndefined() + expect(env.hint).toContain('http://localhost:1/v2/x') + }) + + it('maps the SDK’s local "not authenticated" to exit 4', () => { + const env = toEnvelope(sdkError('NorbixError', {message: 'Norbix is not authenticated.', status: 0, code: 'NORBIX_NOT_AUTHENTICATED'})) + expect(env.exit).toBe(4) + }) + + it('maps a 403 to exit 4 with the permission hint', () => { + const env = toEnvelope(sdkError('NorbixAuthError', {message: 'Forbidden', status: 403})) + expect(env.exit).toBe(4) + expect(env.hint).toMatch(/permission/) + }) + + it('maps a 500 to exit 8', () => { + expect(toEnvelope(sdkError('NorbixError', {message: 'boom', status: 500})).exit).toBe(8) + }) + + it('cleans the oclif parser message and exits 2', () => { + const err = Object.assign(new Error('The following error occurred:\n Missing required flag update\nSee more help with --help'), {oclif: {exit: 2}, parse: {}}) + const env = toEnvelope(err, {command: 'db update'}) + expect(env).toMatchObject({code: 'USAGE_ERROR', exit: 2, message: 'Missing required flag update', docs: 'norbix db update --help'}) + }) + + it('treats Ctrl+C at a prompt as cancelled, exit 9', () => { + const err = new Error('User force closed the prompt') + err.name = 'ExitPromptError' + expect(toEnvelope(err)).toEqual({code: 'CANCELLED', message: 'Cancelled.', exit: 9}) + }) + + it('maps an unknown error to exit 1', () => { + const env = toEnvelope(new TypeError('x is not a function')) + expect(env).toMatchObject({code: 'INTERNAL_ERROR', exit: 1, message: 'x is not a function'}) + }) + + it('usageError is exit 2', () => { + expect(toEnvelope(usageError('bad', 'do this')).exit).toBe(2) + }) +}) + +describe('field errors and trace ids', () => { + it('reads both the item list and the object map', () => { + expect(fieldErrorsOf({errors: {email: ['is required']}}, [{fieldName: 'name', message: 'too short'}])).toEqual({ + name: ['too short'], + email: ['is required'], + }) + }) + + it('is absent when nothing names a field', () => { + expect(fieldErrorsOf({message: 'x'}, [{message: 'no field'}])).toBeUndefined() + }) + + it('finds the correlation id inside responseStatus.meta', () => { + expect(traceIdOf({responseStatus: {meta: {correlationId: 'c-9'}}})).toBe('c-9') + expect(traceIdOf({correlationId: 'top'})).toBe('top') + expect(traceIdOf('text')).toBeUndefined() + }) +}) + +describe('formatErrorText', () => { + it('renders message, details, hint and docs with no ANSI codes', () => { + const text = formatErrorText({ + code: 'USER_NOT_FOUND', + message: 'User not found', + status: 404, + exit: 5, + url: 'http://x/u/1', + traceId: 't-1', + fieldErrors: {email: ['is required']}, + hint: 'Check the id.', + docs: 'norbix users get --help', + }) + expect(text).toBe( + [ + 'Error: User not found', + ' code: USER_NOT_FOUND', + ' status: 404', + ' url: http://x/u/1', + ' traceId: t-1', + ' email: is required', + 'Hint: Check the id.', + 'Docs: norbix users get --help', + ].join('\n'), + ) + expect(text).not.toMatch(/\u001B\[/) + }) +}) From d237297fca7279c0a4d178e6a3822101990697e0 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Fri, 2 Oct 2026 12:36:46 +0300 Subject: [PATCH 03/14] docs(cli): task file for the agent-native CLI work The handover (target behaviour, verified findings, specification, PR order, reproduction harness, acceptance tests) plus the work log kept while implementing it. Co-Authored-By: Claude Fable 5.1 --- docs/tasks/agent-native-cli.md | 428 +++++++++++++++++++++++++++++++++ 1 file changed, 428 insertions(+) create mode 100644 docs/tasks/agent-native-cli.md diff --git a/docs/tasks/agent-native-cli.md b/docs/tasks/agent-native-cli.md new file mode 100644 index 0000000..0063219 --- /dev/null +++ b/docs/tasks/agent-native-cli.md @@ -0,0 +1,428 @@ +# Agent-native CLI — hardening + Claude Code integration via CLI + +Status: open · Branch: create your own worktree from `main` (suggested name `feat/agent-native-cli`) +Related, in parallel (do NOT touch): MCP server work for Norbix. This task is the **CLI path**: +a frontier coding agent (Claude Code, Cursor, OpenCode, Codex) is told to use `norbix` from a shell. + +## 0. Why this exists + +Norbix is about to launch "works with frontier models". One integration path is MCP; the other is +this CLI. Agents already know how to run shell commands, so a CLI that behaves predictably in a +non-interactive shell is zero-setup integration. Today the CLI is good for humans but has gaps that +make it unsafe or noisy for agents. This document lists the verified gaps, the target behaviour, and +the integration deliverables. The human experience (text output, prompts in a terminal) must stay. + +Everything in §2 was verified on 2026-10-02 against the current `main` by running the built CLI +against a local fake HTTP server (script in §6). Re-run the script before and after your changes. + +## 1. Target behaviour ("the agent contract") + +These are the rules the CLI must satisfy. Put a copy of this section into `docs/agent-contract.md` +and link it from README; `norbix schema` (§3.4) must print its URL/path too. + +1. **Never hang, never act silently.** In a non-interactive shell (stdout or stdin not a TTY, or + `CI` set), any command that needs a confirmation and did not get `--yes` MUST exit non-zero + (code 3) with a one-line stderr message that names the flag. It must not send the request. + In a terminal without `--yes`, ask as today. +2. **`--json` means stdout is exactly one JSON document, always.** Success: the result. Failure: + `{"error": {...}}` with the envelope in §3.2. No text on stdout in JSON mode, ever. Progress and + warnings go to stderr. Exit code is the same with and without `--json`. +3. **Errors carry everything the SDK knows** (status, code, fieldErrors, url) plus a `hint` + (what to do next) and `docs` (URL or `norbix --help`). +4. **Documented exit codes** (see §3.1). Agents branch on them. +5. **`--dry-run` on every mutating command.** It resolves auth/endpoints/project exactly as a real + call would (so a dry run that would fail for real also fails), prints the request that would be + sent, sends nothing, exits 0. +6. **Discoverable without reading source.** `norbix schema [command] --json` returns the full + command surface (args, flags, types, required, env var, default, examples, destructive: bool, + supportsDryRun: bool). `--help` on every command includes at least one example with real-looking + values and, for mutating commands, the `--dry-run` / `--yes` pattern. +7. **Cheap to run.** Startup under 200 ms for `--version`; heavy modules loaded lazily. +8. **No ANSI codes when `--json`, `NO_COLOR`, or non-TTY.** (No colour lib today — add the check + now so it stays true.) + +## 2. Verified findings (with reproductions) + +### 2.1 Destructive commands run with no confirmation when there is no terminal — CRITICAL + +Pattern in ~15 commands (`users/delete.ts`, `sms/delete.ts`, `sms/stop.ts`, `push/delete.ts`, +`push/stop.ts`, `push/integration/delete.ts`, `email/*`, `files/delete.ts`, `scheduler/delete.ts`, +`env/delete.ts`, `apikeys/regenerate.ts`, `module.ts`, `db/delete.ts --many`, +`lib/namespace-command.ts` for `hub`/`api`): + +```ts +if (!flags.yes && process.stdout.isTTY) { + const ok = await confirm({message: `Delete user ${args.id}?`, default: false}) + if (!ok) return this.print('Cancelled.') +} +const res = await client.api.membership.deleteUser({id: args.id}) // reached when !isTTY +``` + +Observed (` --help` / `norbix schema --json` | +| 3 | confirmation required (non-interactive, no `--yes`) | re-run with `--yes` after `--dry-run` | +| 4 | not authenticated / auth rejected (401, 403, no key) | `norbix login`, `--api-key`, `--profile` | +| 5 | not found (404) | check id / env / project | +| 6 | validation rejected by server (400/422) | `fieldErrors` lists fields | +| 7 | network / timeout / endpoint unreachable | shows URL; check `--region`, `api_url`, connectivity | +| 8 | rate limited / server error (429/5xx) | retry with backoff; include `traceId` | +| 9 | cancelled by user at a prompt | | + +Exit codes must be identical with and without `--json`. + +### 3.2 JSON error envelope (stdout, one document) + +```json +{ + "error": { + "code": "NOT_FOUND", // SDK code if present, else derived from status / category + "message": "User not found", + "status": 404, // HTTP status when applicable + "exit": 5, + "fieldErrors": {"email": ["is required"]}, // only when present + "url": "https://nb-eu-germany.api.norbix.ai/v2/membership/auth/abc", // when known + "traceId": "…", // when the server returns one (check NorbixError.raw) + "hint": "Check the id, or list users with: norbix users list --json", + "docs": "norbix users get --help" + } +} +``` +Implement in `BaseCommand.catch`: map every error (NorbixError family, oclif parse errors, JSON +input errors, unknown command, generic Error) to `{exit, code, message, hint, ...}`; in JSON mode +`logJson` this object and `this.exit(exit)`; in text mode print `Error: ` + details + +`Hint: …` to stderr. Never let oclif's default JSON error path run (it is what dumps 22k lines). +For unknown commands hook `bin/run.js` / a `command_not_found` hook so JSON mode still emits the +envelope. + +### 3.3 Confirmation + dry-run helpers (`src/base.ts`) + +```ts +static mutatingFlags = { + yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt (required in non-interactive shells)'}), + 'dry-run': Flags.boolean({description: 'Resolve context and print the request that would be sent; send nothing'}), +} + +protected isInteractive(): boolean { + return Boolean(process.stdout.isTTY && process.stdin.isTTY) && !process.env.CI && !this.jsonEnabled() +} + +protected async confirmOrFail(message: string, flags: {yes?: boolean}): Promise { + if (flags.yes) return + if (!this.isInteractive()) { + throw new CliError({exit: 3, code: 'CONFIRMATION_REQUIRED', + message: `Confirmation required: ${message}`, + hint: 'Re-run with --yes. Preview first with --dry-run.'}) + } + const {confirm} = await import('@inquirer/prompts') // lazy + if (!(await confirm({message, default: false}))) { + throw new CliError({exit: 9, code: 'CANCELLED', message: 'Cancelled.'}) + } +} + +/** Call AFTER this.client(flags) so auth/region/project are validated. */ +protected dryRun(flags, call: {method: string; http?: string; request: unknown}): never-ish +``` +Apply to every mutating command (list in §2.1 plus `db delete --id`, `db update --id/--many`, +`db insert*`, `db replace`, `files upload/publish/unpublish`, `users block/invite/roles/policies`, +`webhooks *`, `scheduler enable/disable`, `payments enable/disable`, `*/archive`, `*/unarchive`, +`*/clone`, `config set/unset`, `env use`). Read-only commands do not get these flags. +Decide `destructive` per command (delete/stop/regenerate/block/disable = destructive → confirmation; +create/update/archive = mutating → `--dry-run` but no confirmation unless `--many`). + +### 3.4 `norbix schema [command] --json` + +New command `src/commands/schema.ts`. Source: `this.config.commands` (the oclif manifest). Output, +trimmed and stable: +```json +{ "cli": "norbix", "version": "x.y.z", "contract": "docs/agent-contract.md", + "exitCodes": {...}, + "commands": [{ "id": "users delete", "description": "...", "destructive": true, "supportsDryRun": true, + "args": [{"name":"id","required":true,"description":"User ID"}], + "flags": [{"name":"yes","char":"y","type":"boolean","description":"..."}, + {"name":"project","type":"string","env":"NORBIX_PROJECT_ID","global":true}], + "examples": ["norbix users delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run", "… --yes --json"] }], + "dynamic": { "hub": "norbix hub --help | norbix hub --json lists methods", "api": "…" } } +``` +Without `--json` print a compact table. For `hub`/`api`, extend scope help to list, per method, the +request field names + types (from the SDK `.d.ts` or a generated map — check what `@norbix.ai/ts` +exposes at runtime; if nothing, generate `src/generated/request-fields.json` in `scripts/`). + +### 3.5 `hub` / `api` parser fixes (`src/lib/dispatch.ts`) + +- Add `--body ''` (or `-`) for the whole request object; when present, individual + `--field` flags are rejected with exit 2 (no silent merge). +- Add `--field:str value` / `--field=@str:value` escape (or document that `--body` is the way to + pass exact types) — pick one, document it in scope help and `schema.dynamic`. +- Boolean flag followed by a value: only treat the next token as a value if the field is not known + to be boolean (needs the request-field map from §3.4) — otherwise document and keep. +- `--dry-run` must run after `this.client(flags)`. + +### 3.6 `login` + +Guard every prompt with `isInteractive()`. Non-interactive without `--password`/`--api-key` → exit +2 with hint listing the three non-interactive options (`--api-key`, `--user --password`, +`NORBIX_API_KEY` env). Remove the stack trace / unsettled-await warning. + +### 3.7 Startup + +`npm run build` also runs `oclif manifest` (keep `postpack` cleanup or commit the manifest — pick +one and document). Lazy-import `@inquirer/prompts` everywhere (only §3.3 helper and `login` need +it). Target: `norbix --version` < 200 ms; report before/after numbers. + +### 3.8 Claude Code / agent integration deliverables (the "AI integration via CLI" part) + +1. `docs/agent-contract.md` — §1 + §3.1 + §3.2, written for an agent to read once. +2. `docs/AGENTS.md` (also referenced from README "Use with AI coding agents"): the recipe an agent + follows — auth with `--profile`/`NORBIX_*` env; always `--json`; discover with + `norbix schema --json` then `norbix --help`; mutate with `--dry-run` → inspect → `--yes`; + branch on exit codes; prefer named commands, fall back to `hub`/`api` with `--body`. +3. `norbix ai init` (new command, name open — alternatives `norbix agent setup`): writes, into the + current project, a Claude Code skill `.claude/skills/norbix/SKILL.md` (frontmatter name + + description "Use when the task touches the Norbix backend…", body = short recipe + link to + `norbix schema --json`) and optionally appends a 10-line block to `CLAUDE.md` / `AGENTS.md` + (`--target claude|agents|cursor|all`, `--dry-run` shows the files it would write, never + overwrites an existing file without `--force`). Keep the skill body short; the CLI's own + `--help` and `schema` are the source of truth, not the skill file. +4. README section "Use with Claude Code / AI agents" (5–10 lines + `norbix ai init`). +5. Make sure the hub/portal docs team can link `docs/AGENTS.md` (ask Domantas where it will live; + do not block on it). + +## 4. Non-goals + +- No MCP code in this branch (separate parallel work). Where the MCP server and the CLI should + share things (exit-code table, error envelope, schema JSON), put them in `src/lib/` with no oclif + imports so MCP can import them later, and say so in the final report. +- Do not remove or change human text output beyond what §1 requires. Do not add colour. +- No new runtime dependencies without saying why. + +## 5. Order of work / PR split + +PR 1 — Safety + errors (merge first, smallest reviewable unit): + §3.1, §3.2, §3.3 applied to all destructive commands, §3.6, "Cancelled" → exit 9, tests (§7). +PR 2 — Discovery + dry-run everywhere: §3.4 `schema`, `--dry-run` on all mutating commands, + `--help` examples with realistic values, §3.5. +PR 3 — Startup + agent integration: §3.7, §3.8 (docs, `ai init`, README). +Each PR: conventional commit titles (`feat(cli): …`, `fix(cli): …`; breaking → `feat!:`), CI green, +`COVERAGE.md` updated if it lists commands. + +## 6. Reproduction harness (use before and after) + +```bash +# fake server +cat > /tmp/mock.mjs <<'EOS' +import http from 'node:http' +http.createServer((req,res)=>{ console.error('MOCK',req.method,req.url); res.setHeader('content-type','application/json') + if (req.url.includes('missing')) { res.statusCode=404; return res.end('{"message":"User not found","code":"USER_NOT_FOUND","traceId":"t-1"}') } + if (req.url.includes('bad')) { res.statusCode=400; return res.end('{"message":"Validation failed","errors":{"email":["is required"]}}') } + if (req.url.includes('unauth')) { res.statusCode=401; return res.end('{"message":"bad key"}') } + res.end('{"ok":true}') }).listen(8799) +EOS +node /tmp/mock.mjs & export HOME=/tmp/h; mkdir -p $HOME/.norbix +printf '[x]\napi_key=k\nproject_id=p\napi_url=http://localhost:8799\nhub_url=http://localhost:8799\n[y]\napi_key=k\nproject_id=p\napi_url=http://localhost:1\nhub_url=http://localhost:1\n' > $HOME/.norbix/config +run(){ node bin/run.js "$@" /tmp/o 2>/tmp/e; echo "exit=$? stdout_lines=$(wc -l Date: Fri, 2 Oct 2026 12:50:33 +0300 Subject: [PATCH 04/14] =?UTF-8?q?feat(cli):=20hub/api=20=E2=80=94=20reques?= =?UTF-8?q?t=20fields=20from=20the=20SDK,=20typed=20values,=20--body?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan step [2] of docs/tasks/agent-native-cli.md. - scripts/gen-request-fields.mjs reads the installed @norbix.ai/ts (dist/ index.js for method → route, dist/types/*.d.ts for the request DTO) and writes src/generated/request-fields.json: 578 methods with their fields, kinds and required flags. `npm run gen:fields` after an SDK bump; the build copies it into dist/. - `norbix hub --help` lists every method with its fields; `--json` returns them as data (words, destructive, http, path, fields). - Values are typed from the map: a string field keeps "0042", a boolean flag no longer swallows the word after it (it goes back to being the id). Explicit types win: --field:str / :num / :bool / :json. - --body '' (or -) passes the whole request; mixing it with --field flags is a usage error (exit 2), never a silent merge. - disable / block count as destructive verbs, like the named commands. Co-Authored-By: Claude Fable 5.1 --- package.json | 5 +- scripts/gen-request-fields.mjs | 180 ++++++++++++++++++++++++++++++ src/commands/api.ts | 10 +- src/commands/hub.ts | 19 +++- src/generated/request-fields.json | 1 + src/lib/dispatch.ts | 154 +++++++++++++++++++++++-- src/lib/namespace-command.ts | 142 ++++++++++++++++++----- src/lib/request-fields.ts | 61 ++++++++++ 8 files changed, 530 insertions(+), 42 deletions(-) create mode 100644 scripts/gen-request-fields.mjs create mode 100644 src/generated/request-fields.json create mode 100644 src/lib/request-fields.ts diff --git a/package.json b/package.json index f93a914..e0f39f4 100644 --- a/package.json +++ b/package.json @@ -36,14 +36,15 @@ ], "scripts": { "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"", - "build": "npm run clean && tsc", + "build": "npm run clean && tsc && node -e \"require('node:fs').cpSync('src/generated','dist/generated',{recursive:true})\"", "typecheck": "tsc --noEmit", "dev": "npm run build && node ./bin/run.js", "prepack": "npm run build && oclif manifest", "postpack": "node -e \"require('node:fs').rmSync('oclif.manifest.json',{force:true})\"", "test": "vitest run", "test:watch": "vitest", - "pretest": "npm run build" + "pretest": "npm run build", + "gen:fields": "node scripts/gen-request-fields.mjs" }, "dependencies": { "@inquirer/prompts": "^8.7.2", diff --git a/scripts/gen-request-fields.mjs b/scripts/gen-request-fields.mjs new file mode 100644 index 0000000..444f197 --- /dev/null +++ b/scripts/gen-request-fields.mjs @@ -0,0 +1,180 @@ +#!/usr/bin/env node +/** + * Generates src/generated/request-fields.json from the installed + * `@norbix.ai/ts`: for every SDK method of `api.*` and `hub.*`, the HTTP + * method, the path, the request DTO and its fields (name, type, required). + * + * `norbix hub --help` lists the fields from it, and the plain-word + * parser uses it to type values (a string field stays "0042", a boolean flag + * does not swallow the next word). Run `npm run gen:fields` after bumping the + * SDK and commit the result. + * + * Sources (no runtime cost — pure text parsing at build time): + * dist/index.js method → {target, path, method, dto} (JSDoc + send call) + * dist/types/*.dtos.d.ts dto → fields (class bodies) + */ +import {readFileSync, writeFileSync, mkdirSync} from 'node:fs' +import {createRequire} from 'node:module' +import {dirname, join} from 'node:path' +import {fileURLToPath} from 'node:url' + +const require = createRequire(import.meta.url) +const root = dirname(dirname(fileURLToPath(import.meta.url))) +// The package exports map hides package.json; resolve the entry point and walk up. +const sdkDir = dirname(dirname(require.resolve('@norbix.ai/ts'))) +const sdkVersion = JSON.parse(readFileSync(join(sdkDir, 'package.json'), 'utf8')).version +const indexJs = readFileSync(join(sdkDir, 'dist/index.js'), 'utf8') + +// ---- 1. module classes → methods ------------------------------------------- + +/** `var FooModule = class {` … up to the next top-level `var X = class`. */ +function classBlocks(source) { + const blocks = new Map() + const re = /^var (\w+) = class \{$/gm + const starts = [] + let m + while ((m = re.exec(source))) starts.push({name: m[1], at: m.index}) + for (let i = 0; i < starts.length; i++) { + const end = i + 1 < starts.length ? starts[i + 1].at : source.length + blocks.set(starts[i].name, source.slice(starts[i].at, end)) + } + + return blocks +} + +const METHOD_RE = + /\/\*\*\s*\n\s*\* (GET|POST|PUT|PATCH|DELETE) (\S+)\s*\n\s*\* Request DTO: (\w+)\s*\n\s*\*\/\s*\n\s*(\w+) = \(request = \{\}, options = \{\}\) => \{\s*\n\s*return this\.transport\.send\(\{\s*\n\s*target: "(api|hub)",/g + +function methodsOf(block) { + const out = {} + let m + while ((m = METHOD_RE.exec(block))) { + const [, http, path, dto, name, target] = m + out[name] = {http, path, dto, target} + } + + return out +} + +/** `this.membership = new MembershipModule(transport)` inside a namespace class. */ +function modulesOf(block) { + const out = {} + const re = /this\.(\w+) = new (\w+)\(transport\)/g + let m + while ((m = re.exec(block))) out[m[1]] = m[2] + return out +} + +const blocks = classBlocks(indexJs) +const namespaces = {api: modulesOf(blocks.get('ApiNamespace')), hub: modulesOf(blocks.get('HubNamespace'))} + +// ---- 2. DTO declarations → fields ------------------------------------------- + +function parseDts(file) { + const text = readFileSync(file, 'utf8') + const lines = text.split('\n') + const classes = new Map() + const enums = new Map() + for (let i = 0; i < lines.length; i++) { + const cls = lines[i].match(/^\s+class (\w+)(?: extends (\w+))?/) + if (cls) { + const fields = [] + for (let j = i + 1; j < lines.length; j++) { + const line = lines[j] + if (/^\s+constructor\(|^\s+\}/.test(line)) break + const f = line.match(/^\s+(\w+)(\?)?: ([^;]+);$/) + if (f) fields.push({name: f[1], type: f[3].trim(), required: f[2] === undefined}) + } + + classes.set(cls[1], {extends: cls[2], fields}) + continue + } + + const en = lines[i].match(/^\s+enum (\w+)/) + if (en) { + const values = [] + for (let j = i + 1; j < lines.length && !/^\s+\}/.test(lines[j]); j++) { + const v = lines[j].match(/=\s*"([^"]*)"/) + if (v) values.push(v[1]) + } + + enums.set(en[1], values) + } + } + + return {classes, enums} +} + +/** Request base classes carry headers / path tokens the CLI fills itself. */ +const HIDDEN_FIELDS = new Set(['cultureCode', 'timeZoneId', 'version', 'correlationId', 'projectId', 'env', 'resolvedEnv']) + +function kindOf(type, enums, classes) { + const array = type.endsWith('[]') + const base = array ? type.slice(0, -2) : type + let kind + let options + if (base === 'string') kind = 'string' + else if (base === 'number') kind = 'number' + else if (base === 'boolean') kind = 'boolean' + else if (enums.has(base)) { + kind = 'string' + options = enums.get(base) + } else if (/^(string|number|boolean) \| /.test(type)) kind = 'json' + else if (/Id$/.test(base) && classes.has(base)) kind = 'string' + else kind = 'json' + return {kind: array ? `${kind}[]` : kind, options} +} + +function fieldsOf(dto, {classes, enums}) { + const seen = new Map() + let name = dto + const chain = [] + while (name && classes.has(name)) { + chain.unshift(classes.get(name)) + name = classes.get(name).extends + } + + for (const cls of chain) { + for (const f of cls.fields) { + if (HIDDEN_FIELDS.has(f.name)) continue + const {kind, options} = kindOf(f.type, enums, classes) + const entry = {name: f.name, type: f.type, kind, required: f.required} + if (options) entry.options = options + seen.set(f.name, entry) + } + } + + return [...seen.values()] +} + +const dts = { + api: parseDts(join(sdkDir, 'dist/types/api2.dtos.d.ts')), + hub: parseDts(join(sdkDir, 'dist/types/hub2.dtos.d.ts')), +} + +// ---- 3. assemble -------------------------------------------------------------- + +const out = {sdk: `@norbix.ai/ts@${sdkVersion}`, api: {}, hub: {}} +let methodCount = 0 +let missingDto = 0 +for (const target of ['api', 'hub']) { + for (const [moduleName, className] of Object.entries(namespaces[target])) { + const block = blocks.get(className) + if (!block) continue + const methods = methodsOf(block) + const entry = {} + for (const [method, info] of Object.entries(methods)) { + const fields = dts[target].classes.has(info.dto) ? fieldsOf(info.dto, dts[target]) : null + if (!fields) missingDto++ + entry[method] = {http: info.http, path: info.path, dto: info.dto, fields: fields ?? []} + methodCount++ + } + + out[target][moduleName] = entry + } +} + +const target = join(root, 'src/generated/request-fields.json') +mkdirSync(dirname(target), {recursive: true}) +writeFileSync(target, JSON.stringify(out) + '\n') +console.log(`wrote ${target}: ${methodCount} methods (${missingDto} without a DTO declaration), sdk ${sdkVersion}`) diff --git a/src/commands/api.ts b/src/commands/api.ts index 4bbcf1b..37d47f1 100644 --- a/src/commands/api.ts +++ b/src/commands/api.ts @@ -1,3 +1,5 @@ +import {Flags} from '@oclif/core' + import {BaseCommand} from '../base.js' import {NamespaceCommand} from '../lib/namespace-command.js' @@ -6,7 +8,7 @@ export default class ApiNs extends NamespaceCommand { static description = `Call any data-plane API endpoint with plain words. -Grammar: norbix api [id] [--field value] +Grammar: norbix api [id] [--field value | --body ''] Same engine as \`norbix hub\` — see \`norbix hub --help\`. For raw HTTP paths use \`norbix raw\`.` @@ -14,6 +16,9 @@ use \`norbix raw\`.` static flags = { ...BaseCommand.mutatingFlags, + body: Flags.string({ + description: 'The whole request as a JSON object (or `-` for stdin); cannot be mixed with --field flags', + }), } static examples = [ @@ -21,6 +26,9 @@ use \`norbix raw\`.` '<%= config.bin %> api database # list database methods', '<%= config.bin %> api database find --collectionName orders --pageSize 20', '<%= config.bin %> api membership users get', + '<%= config.bin %> api membership user delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + '<%= config.bin %> api membership user delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes --json', + `<%= config.bin %> api database insert --body '{"collectionName":"orders","document":"{\\"status\\":\\"new\\"}"}'`, ] async run(): Promise { diff --git a/src/commands/hub.ts b/src/commands/hub.ts index 3f12f36..766b3a8 100644 --- a/src/commands/hub.ts +++ b/src/commands/hub.ts @@ -1,3 +1,5 @@ +import {Flags} from '@oclif/core' + import {BaseCommand} from '../base.js' import {NamespaceCommand} from '../lib/namespace-command.js' @@ -6,15 +8,22 @@ export default class Hub extends NamespaceCommand { static description = `Call any Hub endpoint with plain words. -Grammar: norbix hub [id] [--field value] +Grammar: norbix hub [id] [--field value | --body ''] The method is resolved from the words — plural means "list", singular (or a -positional id) means one item. Destructive verbs ask for confirmation -(skip with --yes). Add --dry-run to see what would be called.` +positional id) means one item. Destructive verbs (delete, remove, stop, +disable, block, regenerate, rotate) ask for confirmation; without a terminal +they exit 3 unless --yes. Add --dry-run to see the exact request first. +Field values are typed from the SDK's request type; force one with +--field:str / :num / :bool / :json, or pass the whole request with --body. +\`norbix hub --help\` lists every method with its fields.` static strict = false static flags = { ...BaseCommand.mutatingFlags, + body: Flags.string({ + description: 'The whole request as a JSON object (or `-` for stdin); cannot be mixed with --field flags', + }), } static examples = [ @@ -24,6 +33,10 @@ positional id) means one item. Destructive verbs ask for confirmation '<%= config.bin %> hub database aggregates delete maggr_123 --schemaId sch_456', '<%= config.bin %> hub scheduler tasks get --pageSize 100', '<%= config.bin %> hub logs get --level Error', + '<%= config.bin %> hub scheduler task delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + '<%= config.bin %> hub scheduler task delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes --json', + `<%= config.bin %> hub scheduler task save --body '{"name":"nightly","cronExpression":"0 2 * * *"}'`, + '<%= config.bin %> hub membership user get --userId:str 0042', ] async run(): Promise { diff --git a/src/generated/request-fields.json b/src/generated/request-fields.json new file mode 100644 index 0000000..61d271f --- /dev/null +++ b/src/generated/request-fields.json @@ -0,0 +1 @@ +{"sdk":"@norbix.ai/ts@2.1.0","api":{"accessToken":{"getAccessToken":{"http":"POST","path":"/access-token","dto":"GetAccessToken","fields":[{"name":"refreshToken","type":"string","kind":"string","required":false}]}},"apikeys":{},"auth":{},"chat":{"askChat":{"http":"POST","path":"/{version}/chat/complete","dto":"AskChatRequest","fields":[{"name":"prompt","type":"string","kind":"string","required":true}]}},"database":{"findMergedTermTree":{"http":"GET","path":"/{version}/database/taxonomies/{taxonomyName}/merged-tree","dto":"FindMergedTermTreeRequest","fields":[{"name":"taxonomyName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"findTaxonomyTree":{"http":"GET","path":"/{version}/database/taxonomies/tree","dto":"FindTaxonomyTreeRequest","fields":[{"name":"includeTerms","type":"boolean","kind":"boolean","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"findTerms":{"http":"GET","path":"/{version}/database/taxonomies/{taxonomyName}/terms","dto":"FindTermsRequest","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"taxonomyName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"filter","type":"string","kind":"string","required":false},{"name":"sortDescending","type":"boolean","kind":"boolean","required":true},{"name":"pagingArgs","type":"PagingArgs","kind":"json","required":false}]},"findTermsChildren":{"http":"GET","path":"/{version}/database/taxonomies/{taxonomyName}/terms/{parentId}/children","dto":"FindTermsChildrenRequest","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"taxonomyName","type":"string","kind":"string","required":true},{"name":"parentId","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"filter","type":"string","kind":"string","required":false},{"name":"pagingArgs","type":"PagingArgs","kind":"json","required":false}]},"findTermTree":{"http":"GET","path":"/{version}/database/taxonomies/{taxonomyName}/terms/tree","dto":"FindTermTreeRequest","fields":[{"name":"taxonomyName","type":"string","kind":"string","required":true},{"name":"rootTermId","type":"string","kind":"string","required":false},{"name":"depth","type":"number","kind":"number","required":false},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getDatabaseSchema":{"http":"GET","path":"/{version}/database/schemas/{id}","dto":"GetDatabaseSchemaRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getDatabaseSchemas":{"http":"GET","path":"/{version}/database/schemas","dto":"GetDatabaseSchemasRequest","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"pagingArgs","type":"PagingArgs","kind":"json","required":false}]},"aggregate":{"http":"POST","path":"/{version}/database/collections/{collectionName}/aggregate","dto":"AggregateRequest","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"pipeline","type":"string","kind":"string","required":true}]},"changeResponsibility":{"http":"PUT","path":"/{version}/database/collections/{collectionName}/{id}/responsibility","dto":"ChangeResponsibilityRequest","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"newResponsibleUserId","type":"string","kind":"string","required":true}]},"count":{"http":"GET","path":"/{version}/database/collections/{collectionName}/count","dto":"CountRequest","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"filter","type":"string","kind":"string","required":false},{"name":"schemaVersion","type":"number","kind":"number","required":false}]},"deleteMany":{"http":"DELETE","path":"/{version}/database/collections/{collectionName}/many","dto":"DeleteManyRequest","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"filter","type":"string","kind":"string","required":true}]},"deleteOne":{"http":"DELETE","path":"/{version}/database/collections/{collectionName}/{id}","dto":"DeleteOneRequest","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"distinct":{"http":"GET","path":"/{version}/database/collections/{collectionName}/distinct","dto":"DistinctRequest","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"field","type":"string","kind":"string","required":true},{"name":"filter","type":"string","kind":"string","required":false},{"name":"schemaVersion","type":"number","kind":"number","required":false}]},"executeAggregate":{"http":"POST","path":"/{version}/database/collections/{collectionName}/aggregates/{aggregateId}/execute","dto":"ExecuteAggregateRequest","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"aggregateId","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"find":{"http":"GET","path":"/{version}/database/collections/{collectionName}","dto":"FindRequest","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"filter","type":"string","kind":"string","required":false},{"name":"schemaVersion","type":"number","kind":"number","required":false},{"name":"pagingArgs","type":"PagingArgs","kind":"json","required":false},{"name":"sortBy","type":"string","kind":"string","required":false},{"name":"sortOrder","type":"number","kind":"number","required":false}]},"findOne":{"http":"GET","path":"/{version}/database/collections/{collectionName}/{id}","dto":"FindOneRequest","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"findOwn":{"http":"GET","path":"/{version}/database/collections/{collectionName}/own","dto":"FindOwnRequest","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"filter","type":"string","kind":"string","required":false},{"name":"schemaVersion","type":"number","kind":"number","required":false},{"name":"pagingArgs","type":"PagingArgs","kind":"json","required":false}]},"insertMany":{"http":"POST","path":"/{version}/database/collections/{collectionName}/many","dto":"InsertManyRequest","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"documents","type":"string","kind":"string","required":true}]},"insertOne":{"http":"POST","path":"/{version}/database/collections/{collectionName}","dto":"InsertOneRequest","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"document","type":"string","kind":"string","required":true}]},"replaceOne":{"http":"PUT","path":"/{version}/database/collections/{collectionName}/{id}/replace","dto":"ReplaceOneRequest","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"replacement","type":"string","kind":"string","required":true}]},"updateMany":{"http":"PUT","path":"/{version}/database/collections/{collectionName}/many","dto":"UpdateManyRequest","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"filter","type":"string","kind":"string","required":true},{"name":"update","type":"string","kind":"string","required":true}]},"updateOne":{"http":"PUT","path":"/{version}/database/collections/{collectionName}/{id}","dto":"UpdateOneRequest","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"update","type":"string","kind":"string","required":true}]}},"echo":{"echo":{"http":"GET","path":"/{version}/echo","dto":"Echo","fields":[]}},"files":{"commitUpload":{"http":"POST","path":"/{version}/files/{filesIntegrationId}/commit","dto":"CommitUploadRequest","fields":[{"name":"filesIntegrationId","type":"string","kind":"string","required":true},{"name":"path","type":"string","kind":"string","required":true},{"name":"contentType","type":"string","kind":"string","required":false},{"name":"sizeBytes","type":"number","kind":"number","required":false},{"name":"fileName","type":"string","kind":"string","required":false}]},"getFileContent":{"http":"GET","path":"/{version}/files/{filesIntegrationId}/content","dto":"GetFileContentRequest","fields":[{"name":"filesIntegrationId","type":"string","kind":"string","required":true},{"name":"path","type":"string","kind":"string","required":true},{"name":"token","type":"string","kind":"string","required":false}]},"putFileContent":{"http":"PUT","path":"/{version}/files/{filesIntegrationId}/content","dto":"PutFileContentRequest","fields":[{"name":"filesIntegrationId","type":"string","kind":"string","required":true},{"name":"path","type":"string","kind":"string","required":true},{"name":"token","type":"string","kind":"string","required":false}]},"deleteFileApi":{"http":"DELETE","path":"/{version}/files/{filesIntegrationId}","dto":"DeleteFileApiRequest","fields":[{"name":"filesIntegrationId","type":"string","kind":"string","required":true},{"name":"path","type":"string","kind":"string","required":true}]},"deleteManyFilesApi":{"http":"DELETE","path":"/{version}/files/{filesIntegrationId}/bulk","dto":"DeleteManyFilesApiRequest","fields":[{"name":"filesIntegrationId","type":"string","kind":"string","required":true},{"name":"paths__","type":"string[]","kind":"string[]","required":true}]},"downloadFileApi":{"http":"GET","path":"/{version}/files/{filesIntegrationId}/download","dto":"DownloadFileApiRequest","fields":[{"name":"filesIntegrationId","type":"string","kind":"string","required":true},{"name":"path","type":"string","kind":"string","required":true}]},"getFileInfo":{"http":"GET","path":"/{version}/files/{filesIntegrationId}/info","dto":"GetFileInfoRequest","fields":[{"name":"filesIntegrationId","type":"string","kind":"string","required":true},{"name":"path","type":"string","kind":"string","required":true}]},"getSignedUrl":{"http":"GET","path":"/{version}/files/{filesIntegrationId}/sign","dto":"GetSignedUrlRequest","fields":[{"name":"filesIntegrationId","type":"string","kind":"string","required":true},{"name":"path","type":"string","kind":"string","required":true},{"name":"expirationSeconds","type":"number","kind":"number","required":false}]},"listFiles":{"http":"GET","path":"/{version}/files/{filesIntegrationId}","dto":"ListFilesRequest","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"filesIntegrationId","type":"string","kind":"string","required":true},{"name":"path","type":"string","kind":"string","required":false}]},"getPublicFile":{"http":"GET","path":"/{version}/files/public/{PublicId}/{Name*}","dto":"GetPublicFileRequest","fields":[{"name":"publicId","type":"string","kind":"string","required":false},{"name":"name","type":"string","kind":"string","required":false}]},"requestUploadUrl":{"http":"POST","path":"/{version}/files/{filesIntegrationId}/upload-url","dto":"RequestUploadUrlRequest","fields":[{"name":"filesIntegrationId","type":"string","kind":"string","required":true},{"name":"path","type":"string","kind":"string","required":true},{"name":"contentType","type":"string","kind":"string","required":true},{"name":"expirationSeconds","type":"number","kind":"number","required":false}]},"testFilesIntegration":{"http":"POST","path":"/{version}/files/{filesIntegrationId}/test","dto":"TestFilesIntegrationRequest","fields":[{"name":"filesIntegrationId","type":"string","kind":"string","required":true}]}},"membership":{"blockUser":{"http":"PATCH","path":"/{version}/membership/auth/block","dto":"BlockUserRequest","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"saveSystemUserWithPermissions":{"http":"POST","path":"/{version}/membership/auth/register/service","dto":"SaveSystemUserWithPermissions","fields":[{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"userGeneralInfo","type":"UserGeneralInfoDto","kind":"json","required":false},{"name":"userId","type":"string","kind":"string","required":false},{"name":"ignoreUserRegistersAsRole","type":"boolean","kind":"boolean","required":true},{"name":"roles","type":"string[]","kind":"string[]","required":true}]},"saveGuestUser":{"http":"POST","path":"/{version}/membership/auth/register/guest","dto":"SaveGuestUser","fields":[{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"userGeneralInfo","type":"UserGeneralInfoDto","kind":"json","required":false},{"name":"userId","type":"string","kind":"string","required":false},{"name":"ignoreUserRegistersAsRole","type":"boolean","kind":"boolean","required":true}]},"saveUserNameUser":{"http":"POST","path":"/{version}/membership/auth/register/user-name","dto":"SaveUserNameUser","fields":[{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"userGeneralInfo","type":"UserGeneralInfoDto","kind":"json","required":false},{"name":"userId","type":"string","kind":"string","required":false},{"name":"ignoreUserRegistersAsRole","type":"boolean","kind":"boolean","required":true},{"name":"password","type":"string","kind":"string","required":true},{"name":"userName","type":"string","kind":"string","required":true}]},"saveEmailUser":{"http":"POST","path":"/{version}/membership/auth/register/email","dto":"SaveEmailUser","fields":[{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"userGeneralInfo","type":"UserGeneralInfoDto","kind":"json","required":false},{"name":"userId","type":"string","kind":"string","required":false},{"name":"ignoreUserRegistersAsRole","type":"boolean","kind":"boolean","required":true},{"name":"password","type":"string","kind":"string","required":true},{"name":"email","type":"string","kind":"string","required":true}]},"savePhoneUser":{"http":"POST","path":"/{version}/membership/auth/register/phone","dto":"SavePhoneUser","fields":[{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"userGeneralInfo","type":"UserGeneralInfoDto","kind":"json","required":false},{"name":"userId","type":"string","kind":"string","required":false},{"name":"ignoreUserRegistersAsRole","type":"boolean","kind":"boolean","required":true},{"name":"phone","type":"string","kind":"string","required":true}]},"savePhoneUserNameWithPermissions":{"http":"POST","path":"/{version}/membership/auth/register/phone-with-permissions","dto":"SavePhoneUserNameWithPermissions","fields":[{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"userGeneralInfo","type":"UserGeneralInfoDto","kind":"json","required":false},{"name":"userId","type":"string","kind":"string","required":false},{"name":"ignoreUserRegistersAsRole","type":"boolean","kind":"boolean","required":true},{"name":"roles","type":"string[]","kind":"string[]","required":true},{"name":"phone","type":"string","kind":"string","required":true}]},"saveEmailUserNameWithPermissions":{"http":"POST","path":"/{version}/membership/auth/register/email-with-permissions","dto":"SaveEmailUserNameWithPermissions","fields":[{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"userGeneralInfo","type":"UserGeneralInfoDto","kind":"json","required":false},{"name":"userId","type":"string","kind":"string","required":false},{"name":"ignoreUserRegistersAsRole","type":"boolean","kind":"boolean","required":true},{"name":"roles","type":"string[]","kind":"string[]","required":true},{"name":"password","type":"string","kind":"string","required":true},{"name":"email","type":"string","kind":"string","required":true}]},"saveUserNameWithPermissions":{"http":"POST","path":"/{version}/membership/auth/register/user-name-with-permissions","dto":"SaveUserNameWithPermissions","fields":[{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"userGeneralInfo","type":"UserGeneralInfoDto","kind":"json","required":false},{"name":"userId","type":"string","kind":"string","required":false},{"name":"ignoreUserRegistersAsRole","type":"boolean","kind":"boolean","required":true},{"name":"roles","type":"string[]","kind":"string[]","required":true},{"name":"password","type":"string","kind":"string","required":true},{"name":"userName","type":"string","kind":"string","required":true}]},"deleteUser":{"http":"DELETE","path":"/{version}/membership/auth","dto":"DeleteUserRequest","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getUser":{"http":"GET","path":"/{version}/membership/auth/{id}","dto":"GetUserRequest","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getUsers":{"http":"GET","path":"/{version}/membership/auth","dto":"GetUsersRequest","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"includePermissions","type":"boolean","kind":"boolean","required":true},{"name":"userShouldHavePushDevice","type":"boolean","kind":"boolean","required":true},{"name":"userShouldHaveEmail","type":"boolean","kind":"boolean","required":true},{"name":"includeMeta","type":"boolean","kind":"boolean","required":true},{"name":"roleNames","type":"string[]","kind":"string[]","required":false},{"name":"userIds","type":"string[]","kind":"string[]","required":false}]},"getUserPreferences":{"http":"GET","path":"/{version}/membership/auth/{id}/preferences","dto":"GetUserPreferencesRequest","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"grantContactConsent":{"http":"POST","path":"/{version}/membership/users/{contactId}/marketing-state/{channel}/consent","dto":"GrantContactConsentRequest","fields":[{"name":"contactId","type":"string","kind":"string","required":true},{"name":"channel","type":"string","kind":"string","required":true},{"name":"lawfulBasis","type":"string","kind":"string","required":true},{"name":"source","type":"string","kind":"string","required":true},{"name":"evidenceRef","type":"string","kind":"string","required":false}]},"inviteUser":{"http":"POST","path":"/{version}/membership/auth/invite","dto":"InviteUserRequest","fields":[{"name":"email","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"linkIdentity":{"http":"POST","path":"/{version}/membership/auth/{userId}/link-identity","dto":"LinkIdentityRequest","fields":[{"name":"userId","type":"string","kind":"string","required":true},{"name":"provider","type":"string","kind":"string","required":true},{"name":"providerToken","type":"string","kind":"string","required":true},{"name":"emailToVerify","type":"string","kind":"string","required":false},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"mapAuthToUser":{"http":"POST","path":"/{version}/membership/users/{userId}/map-auth","dto":"MapAuthToUserRequest","fields":[{"name":"userId","type":"string","kind":"string","required":true},{"name":"authId","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"assignRolePermissions":{"http":"PUT","path":"/{version}/membership/auth/assign-roles","dto":"AssignRolePermissionsRequest","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"roles","type":"string[]","kind":"string[]","required":false}]},"setContactRoles":{"http":"PUT","path":"/{version}/membership/users/{userId}/roles","dto":"SetContactRolesRequest","fields":[{"name":"userId","type":"string","kind":"string","required":true},{"name":"roles","type":"string[]","kind":"string[]","required":false},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"setContactTagSubscription":{"http":"PUT","path":"/{version}/membership/users/{contactId}/marketing-state/{commChannel}/{channel}/tags/{tag}","dto":"SetContactTagSubscriptionRequest","fields":[{"name":"contactId","type":"string","kind":"string","required":true},{"name":"commChannel","type":"string","kind":"string","required":true},{"name":"channel","type":"string","kind":"string","required":true},{"name":"tag","type":"string","kind":"string","required":true},{"name":"subscribed","type":"boolean","kind":"boolean","required":true}]},"unblockUser":{"http":"PATCH","path":"/{version}/membership/auth/unblock","dto":"UnblockUserRequest","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"unsubscribeContact":{"http":"POST","path":"/{version}/membership/users/{contactId}/marketing-state/{channel}/unsubscribe","dto":"UnsubscribeContactRequest","fields":[{"name":"contactId","type":"string","kind":"string","required":true},{"name":"channel","type":"string","kind":"string","required":true},{"name":"reason","type":"string","kind":"string","required":false}]},"updateUser":{"http":"PUT","path":"/{version}/membership/auth","dto":"UpdateUserRequest","fields":[{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"userGeneralInfo","type":"UserGeneralInfoDto","kind":"json","required":false},{"name":"userId","type":"string","kind":"string","required":false},{"name":"ignoreUserRegistersAsRole","type":"boolean","kind":"boolean","required":true},{"name":"id","type":"string","kind":"string","required":true}]},"updateUserPreferences":{"http":"PUT","path":"/{version}/membership/auth/{id}/preferences","dto":"UpdateUserPreferencesRequest","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"blockAllMarketingMessages","type":"boolean","kind":"boolean","required":true}]},"changePassword":{"http":"POST","path":"/{version}/membership/userauth/password/change","dto":"ChangePasswordRequest","fields":[{"name":"currentPassword","type":"string","kind":"string","required":true},{"name":"newPassword","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"requestPasswordReset":{"http":"POST","path":"/{version}/membership/userauth/password/reset/request","dto":"RequestPasswordResetRequest","fields":[{"name":"email","type":"string","kind":"string","required":true}]},"confirmPasswordReset":{"http":"POST","path":"/{version}/membership/userauth/password/reset/confirm","dto":"ConfirmPasswordResetRequest","fields":[{"name":"token","type":"string","kind":"string","required":true},{"name":"newPassword","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"passkeyAuthenticationOptions":{"http":"POST","path":"/{version}/membership/userauth/passkey/authentication-options","dto":"PasskeyAuthenticationOptionsRequest","fields":[{"name":"email","type":"string","kind":"string","required":true}]},"verifyPasskeyAuthentication":{"http":"POST","path":"/{version}/membership/userauth/passkey/verify-authentication","dto":"VerifyPasskeyAuthenticationRequest","fields":[{"name":"ceremonyId","type":"string","kind":"string","required":true},{"name":"assertionResponse","type":"string","kind":"string","required":true}]},"listPasskeys":{"http":"GET","path":"/{version}/membership/userauth/passkeys","dto":"ListPasskeysRequest","fields":[]},"renamePasskey":{"http":"POST","path":"/{version}/membership/userauth/passkeys/{CredentialId}/rename","dto":"RenamePasskeyRequest","fields":[{"name":"credentialId","type":"string","kind":"string","required":true},{"name":"friendlyName","type":"string","kind":"string","required":true}]},"revokePasskey":{"http":"POST","path":"/{version}/membership/userauth/passkeys/{CredentialId}/revoke","dto":"RevokePasskeyRequest","fields":[{"name":"credentialId","type":"string","kind":"string","required":true}]},"useRecoveryCode":{"http":"POST","path":"/{version}/membership/userauth/recovery/use-code","dto":"UseRecoveryCodeRequest","fields":[{"name":"email","type":"string","kind":"string","required":true},{"name":"recoveryCode","type":"string","kind":"string","required":true}]},"requestMagicLink":{"http":"POST","path":"/{version}/membership/userauth/recovery/magic-link/request","dto":"RequestMagicLinkRequest","fields":[{"name":"email","type":"string","kind":"string","required":true}]},"consumeMagicLink":{"http":"POST","path":"/{version}/membership/userauth/recovery/magic-link/consume","dto":"ConsumeMagicLinkRequest","fields":[{"name":"token","type":"string","kind":"string","required":true}]},"hasPasskey":{"http":"POST","path":"/{version}/membership/userauth/has-passkey","dto":"HasPasskeyRequest","fields":[{"name":"email","type":"string","kind":"string","required":true}]},"startEmailVerification":{"http":"POST","path":"/{version}/membership/userauth/email/start-verification","dto":"StartEmailVerificationRequest","fields":[{"name":"email","type":"string","kind":"string","required":true}]},"confirmEmailVerification":{"http":"POST","path":"/{version}/membership/userauth/email/confirm-verification","dto":"ConfirmEmailVerificationRequest","fields":[{"name":"email","type":"string","kind":"string","required":true},{"name":"code","type":"string","kind":"string","required":true}]},"passkeyRegistrationOptions":{"http":"POST","path":"/{version}/membership/userauth/passkey/registration-options","dto":"PasskeyRegistrationOptionsRequest","fields":[{"name":"verificationToken","type":"string","kind":"string","required":true}]},"verifyPasskeyRegistration":{"http":"POST","path":"/{version}/membership/userauth/passkey/verify-registration","dto":"VerifyPasskeyRegistrationRequest","fields":[{"name":"verificationToken","type":"string","kind":"string","required":true},{"name":"ceremonyId","type":"string","kind":"string","required":true},{"name":"attestationResponse","type":"string","kind":"string","required":true},{"name":"friendlyName","type":"string","kind":"string","required":false}]},"refreshPasskeyToken":{"http":"POST","path":"/{version}/membership/userauth/token/refresh","dto":"RefreshPasskeyTokenRequest","fields":[{"name":"refreshToken","type":"string","kind":"string","required":false}]},"passkeyLogout":{"http":"POST","path":"/{version}/membership/userauth/logout","dto":"PasskeyLogoutRequest","fields":[{"name":"refreshToken","type":"string","kind":"string","required":false}]}},"public":{"getPublicProjectConfig":{"http":"GET","path":"/{version}/public/projects/{ProjectId}/config","dto":"GetPublicProjectConfig","fields":[]},"getPublicProjectLegal":{"http":"GET","path":"/{version}/public/projects/{ProjectId}/legal/{Kind}","dto":"GetPublicProjectLegal","fields":[{"name":"kind","type":"string","kind":"string","required":false}]}}},"hub":{"accessToken":{"getAccessToken":{"http":"POST","path":"/access-token","dto":"GetAccessToken","fields":[{"name":"refreshToken","type":"string","kind":"string","required":false}]}},"account":{"getAccountProfile":{"http":"GET","path":"/{version}/account/profile","dto":"GetAccountProfile","fields":[]},"updateAccountProfile":{"http":"PUT","path":"/{version}/account/profile","dto":"UpdateAccountProfile","fields":[{"name":"displayName","type":"string","kind":"string","required":true},{"name":"billingEmail","type":"string","kind":"string","required":false},{"name":"operationsEmail","type":"string","kind":"string","required":false},{"name":"securityEmail","type":"string","kind":"string","required":false}]},"resendAccountVerificationToken":{"http":"GET","path":"/{version}/account/verify/resend","dto":"ResendAccountVerificationToken","fields":[]},"getAccountStatus":{"http":"GET","path":"/{version}/account/status","dto":"GetAccountStatus","fields":[]},"createStripeCheckoutSession":{"http":"POST","path":"/{version}/account/stripe/create-checkout-session","dto":"CreateStripeCheckoutSession","fields":[{"name":"subscriptionType","type":"SubscriptionType","kind":"string","required":true,"options":["ManagedService","License"]},{"name":"domain","type":"string","kind":"string","required":false},{"name":"projectCap","type":"number","kind":"number","required":true},{"name":"newProjectSessionId","type":"string","kind":"string","required":false},{"name":"returnUrl","type":"string","kind":"string","required":false}]},"getStripeBillingPortalUrl":{"http":"POST","path":"/{version}/account/stripe/get-portal-url","dto":"GetStripeBillingPortalUrl","fields":[{"name":"subscriptionType","type":"SubscriptionType","kind":"string","required":true,"options":["ManagedService","License"]},{"name":"returnUrl","type":"string","kind":"string","required":false}]},"createTeamMemberFromInvitation":{"http":"POST","path":"/{version}/account/team/member","dto":"CreateTeamMemberFromInvitation","fields":[{"name":"displayName","type":"string","kind":"string","required":true},{"name":"token","type":"string","kind":"string","required":true},{"name":"password","type":"string","kind":"string","required":true}]},"getAccountUsageBilling":{"http":"GET","path":"/{version}/account/usage-billing","dto":"GetAccountUsageBilling","fields":[]},"deleteNotificationsGroup":{"http":"DELETE","path":"/{version}/account/projects/{projectId}/notifications/settings/group","dto":"DeleteNotificationsGroup","fields":[{"name":"groupTag","type":"string","kind":"string","required":true}]},"deleteNotificationsTag":{"http":"DELETE","path":"/{version}/account/projects/{projectId}/notifications/settings/tag","dto":"DeleteNotificationsTag","fields":[{"name":"tag","type":"string","kind":"string","required":true}]},"removeTagFromNotificationsGroup":{"http":"DELETE","path":"/{version}/account/projects/{projectId}/notifications/settings/group/tag","dto":"RemoveTagFromNotificationsGroup","fields":[{"name":"groupTag","type":"string","kind":"string","required":true},{"name":"tag","type":"string","kind":"string","required":true}]},"saveNotificationsGroup":{"http":"POST","path":"/{version}/account/projects/{projectId}/notifications/settings/group","dto":"SaveNotificationsGroup","fields":[{"name":"groupDefinition","type":"GroupDefinitionDto","kind":"json","required":true},{"name":"channel","type":"CommunicationChannel","kind":"string","required":true,"options":["Transactional","Marketing","System"]},{"name":"originChannel","type":"CommunicationChannel","kind":"string","required":false,"options":["Transactional","Marketing","System"]}]},"saveNotificationsTag":{"http":"POST","path":"/{version}/account/projects/{projectId}/notifications/settings/tag","dto":"SaveNotificationsTag","fields":[{"name":"tagDefinition","type":"TagDefinitionDto","kind":"json","required":true},{"name":"channel","type":"CommunicationChannel","kind":"string","required":false,"options":["Transactional","Marketing","System"]},{"name":"groupTag","type":"string","kind":"string","required":false}]},"createProject":{"http":"POST","path":"/{version}/account/projects","dto":"CreateProjectRequest","fields":[{"name":"integration","type":"DatabaseIntegrationRequest","kind":"json","required":true},{"name":"projectName","type":"string","kind":"string","required":true},{"name":"primaryRegion","type":"string","kind":"string","required":false},{"name":"additionalRegions","type":"string[]","kind":"string[]","required":false},{"name":"description","type":"string","kind":"string","required":false}]},"deleteProject":{"http":"DELETE","path":"/{version}/account/projects/{projectId}","dto":"DeleteProject","fields":[]},"createProjectEnvironment":{"http":"POST","path":"/{version}/account/projects/environments","dto":"CreateProjectEnvironmentRequest","fields":[{"name":"environmentName","type":"string","kind":"string","required":true},{"name":"integration","type":"DatabaseIntegrationRequest","kind":"json","required":true}]},"deleteProjectEnvironment":{"http":"DELETE","path":"/{version}/account/projects/environments/{environmentName}","dto":"DeleteProjectEnvironmentRequest","fields":[{"name":"environmentName","type":"string","kind":"string","required":true}]},"setEnvironmentRank":{"http":"PATCH","path":"/{version}/account/projects/environments/{environmentName}/rank","dto":"SetEnvironmentRankRequest","fields":[{"name":"environmentName","type":"string","kind":"string","required":true},{"name":"rank","type":"number","kind":"number","required":true}]},"promoteEnvironment":{"http":"POST","path":"/{version}/account/projects/environments/promote","dto":"PromoteEnvironmentRequest","fields":[{"name":"sourceEnv","type":"string","kind":"string","required":true},{"name":"targetEnv","type":"string","kind":"string","required":true},{"name":"dryRun","type":"boolean","kind":"boolean","required":true}]},"rollbackPromotion":{"http":"POST","path":"/{version}/account/projects/environments/promote/rollback","dto":"RollbackPromotionRequest","fields":[{"name":"targetEnv","type":"string","kind":"string","required":true},{"name":"fromVersion","type":"number","kind":"number","required":true}]},"getProjectEnvironments":{"http":"GET","path":"/{version}/account/projects/environments","dto":"GetProjectEnvironments","fields":[]},"getProject":{"http":"GET","path":"/{version}/account/projects/{projectId}","dto":"GetProject","fields":[]},"getProjects":{"http":"GET","path":"/{version}/account/projects","dto":"GetProjects","fields":[]},"getAccountRegions":{"http":"GET","path":"/{version}/account/regions","dto":"GetAccountRegions","fields":[]},"waitForProjectActive":{"http":"GET","path":"/{version}/account/projects/{projectId}/wait-active","dto":"WaitForProjectActiveRequest","fields":[{"name":"timeoutSeconds","type":"number","kind":"number","required":false}]},"getProjectTokens":{"http":"GET","path":"/{version}/account/projects/{projectId}/tokens","dto":"GetProjectTokens","fields":[{"name":"initiatorId","type":"string","kind":"string","required":false},{"name":"recipientId","type":"string","kind":"string","required":false},{"name":"targetUserId","type":"string","kind":"string","required":false},{"name":"membershipTriggerOldUserId","type":"string","kind":"string","required":false},{"name":"membershipTriggerNewUserId","type":"string","kind":"string","required":false}]},"assignAdminPortalServiceUser":{"http":"PUT","path":"/{version}/account/projects/{projectId}/settings/admin-portal/service-user","dto":"AssignAdminPortalServiceUserRequest","fields":[{"name":"serviceUserId","type":"string","kind":"string","required":true}]},"getAdminPortalStructure":{"http":"GET","path":"/{version}/account/projects/{projectId}/admin-portal/structure","dto":"GetAdminPortalStructure","fields":[]},"updateProjectAdminUrl":{"http":"PATCH","path":"/{version}/account/projects/{projectId}/settings/admin-url","dto":"UpdateProjectAdminUrl","fields":[{"name":"url","type":"string","kind":"string","required":false}]},"updateProjectAccentColor":{"http":"PATCH","path":"/{version}/account/projects/{projectId}/settings/accent-color","dto":"UpdateProjectAccentColor","fields":[{"name":"color","type":"string","kind":"string","required":true}]},"updateProjectIcon":{"http":"PATCH","path":"/{version}/account/projects/{projectId}/settings/icon","dto":"UpdateProjectIcon","fields":[{"name":"fileResource","type":"FileResourceRefDto","kind":"json","required":false}]},"updateProjectLogo":{"http":"PATCH","path":"/{version}/account/projects/{projectId}/settings/logo","dto":"UpdateProjectLogo","fields":[{"name":"fileResource","type":"FileResourceRefDto","kind":"json","required":false}]},"updateProjectMainColor":{"http":"PATCH","path":"/{version}/account/projects/{projectId}/settings/main-color","dto":"UpdateProjectMainColor","fields":[{"name":"color","type":"string","kind":"string","required":true}]},"updateProjectAllowedOrigins":{"http":"PATCH","path":"/{version}/account/projects/{projectId}/settings/origins","dto":"UpdateProjectAllowedOrigins","fields":[{"name":"origins","type":"string[]","kind":"string[]","required":false}]},"updateProjectDefaultLanguage":{"http":"PATCH","path":"/{version}/account/projects/{projectId}/settings/default-language","dto":"UpdateProjectDefaultLanguage","fields":[{"name":"defaultLanguage","type":"string","kind":"string","required":true}]},"updateProjectDescription":{"http":"PATCH","path":"/{version}/account/projects/{projectId}/settings/description","dto":"UpdateProjectDescription","fields":[{"name":"description","type":"string","kind":"string","required":false}]},"disableProject":{"http":"PATCH","path":"/{version}/account/projects/{projectId}/disable","dto":"DisableProject","fields":[]},"enableProject":{"http":"PATCH","path":"/{version}/account/projects/{projectId}/enable","dto":"EnableProject","fields":[]},"updateProjectLanguages":{"http":"PATCH","path":"/{version}/account/projects/{projectId}/settings/languages","dto":"UpdateProjectLanguages","fields":[{"name":"languages","type":"string[]","kind":"string[]","required":true}]},"updateProjectLegalDocuments":{"http":"PATCH","path":"/{version}/account/projects/{projectId}/settings/legal","dto":"UpdateProjectLegalDocuments","fields":[{"name":"termsMarkdown","type":"string","kind":"string","required":false},{"name":"privacyMarkdown","type":"string","kind":"string","required":false}]},"updateProjectExposeLegal":{"http":"PATCH","path":"/{version}/account/projects/{projectId}/settings/legal/expose","dto":"UpdateProjectExposeLegal","fields":[{"name":"exposed","type":"boolean","kind":"boolean","required":true}]},"updateProjectUrl":{"http":"PATCH","path":"/{version}/account/projects/{projectId}/settings/url","dto":"UpdateProjectUrl","fields":[{"name":"url","type":"string","kind":"string","required":false}]},"updateProjectName":{"http":"PATCH","path":"/{version}/account/projects/{projectId}/settings/name","dto":"UpdateProjectName","fields":[{"name":"name","type":"string","kind":"string","required":true}]},"updateProjectRegions":{"http":"PATCH","path":"/{version}/account/projects/{projectId}/settings/regions","dto":"UpdateProjectRegions","fields":[{"name":"primaryRegion","type":"string","kind":"string","required":false},{"name":"additionalRegions","type":"string[]","kind":"string[]","required":false}]},"createAccount":{"http":"POST","path":"/{version}/account","dto":"CreateAccount","fields":[{"name":"displayName","type":"string","kind":"string","required":true},{"name":"email","type":"string","kind":"string","required":true},{"name":"password","type":"string","kind":"string","required":true}]},"changeTeamMemberPassword":{"http":"POST","path":"/{version}/account/team/member/password","dto":"ChangeTeamMemberPassword","fields":[{"name":"email","type":"string","kind":"string","required":true},{"name":"currentPassword","type":"string","kind":"string","required":true},{"name":"newPassword","type":"string","kind":"string","required":true}]},"createTeamMember":{"http":"POST","path":"/{version}/account/team/member/create","dto":"CreateTeamMember","fields":[{"name":"email","type":"string","kind":"string","required":true},{"name":"displayName","type":"string","kind":"string","required":false},{"name":"password","type":"string","kind":"string","required":false},{"name":"roles","type":"string[]","kind":"string[]","required":false},{"name":"sendInvitation","type":"boolean","kind":"boolean","required":true}]},"createAccountPolicy":{"http":"POST","path":"/{version}/account/team/policies","dto":"CreateAccountPolicy","fields":[{"name":"policyName","type":"string","kind":"string","required":true},{"name":"description","type":"string","kind":"string","required":false},{"name":"policyDocumentJson","type":"string","kind":"string","required":false}]},"createAccountRole":{"http":"POST","path":"/{version}/account/team/roles","dto":"CreateAccountRole","fields":[{"name":"roleName","type":"string","kind":"string","required":true},{"name":"description","type":"string","kind":"string","required":false},{"name":"policies","type":"string[]","kind":"string[]","required":false}]},"deleteAccountPolicy":{"http":"DELETE","path":"/{version}/account/team/policies/{Id}","dto":"DeleteAccountPolicy","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"deleteAccountRole":{"http":"DELETE","path":"/{version}/account/team/roles/{Id}","dto":"DeleteAccountRole","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getAccountCollaborators":{"http":"GET","path":"/{version}/account/collaborators","dto":"GetAccountCollaborators","fields":[{"name":"includeAccountOwner","type":"boolean","kind":"boolean","required":true},{"name":"userShouldHavePushDevice","type":"boolean","kind":"boolean","required":true},{"name":"userIds","type":"string[]","kind":"string[]","required":false},{"name":"roleNames","type":"string[]","kind":"string[]","required":false},{"name":"pagingArgs","type":"PagingArgs","kind":"json","required":false}]},"getAccountPasswordPolicy":{"http":"GET","path":"/{version}/account/team/password-policy","dto":"GetAccountPasswordPolicy","fields":[]},"getAccountTeamPolicies":{"http":"GET","path":"/{version}/account/team/policies","dto":"GetAccountTeamPolicies","fields":[]},"getAccountTeamRoles":{"http":"GET","path":"/{version}/account/team/roles","dto":"GetAccountTeamRoles","fields":[]},"sendInviteToTeamMember":{"http":"POST","path":"/{version}/account/team/member/invite","dto":"SendInviteToTeamMember","fields":[{"name":"email","type":"string","kind":"string","required":true},{"name":"roles","type":"string[]","kind":"string[]","required":false}]},"updateAccountPolicy":{"http":"PUT","path":"/{version}/account/team/policies","dto":"UpdateAccountPolicy","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"policyName","type":"string","kind":"string","required":true},{"name":"description","type":"string","kind":"string","required":false},{"name":"policyDocumentJson","type":"string","kind":"string","required":false}]},"updateAccountRole":{"http":"PUT","path":"/{version}/account/team/roles","dto":"UpdateAccountRole","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"roleName","type":"string","kind":"string","required":true},{"name":"description","type":"string","kind":"string","required":false},{"name":"policies","type":"string[]","kind":"string[]","required":false}]},"accountHasPasskey":{"http":"POST","path":"/{version}/account/userauth/has-passkey","dto":"AccountHasPasskeyRequest","fields":[{"name":"email","type":"string","kind":"string","required":true}]},"accountStartEmailVerification":{"http":"POST","path":"/{version}/account/userauth/email/start-verification","dto":"AccountStartEmailVerificationRequest","fields":[{"name":"email","type":"string","kind":"string","required":true}]},"accountConfirmEmailVerification":{"http":"POST","path":"/{version}/account/userauth/email/confirm-verification","dto":"AccountConfirmEmailVerificationRequest","fields":[{"name":"email","type":"string","kind":"string","required":true},{"name":"code","type":"string","kind":"string","required":true}]},"accountPasskeyRegistrationOptions":{"http":"POST","path":"/{version}/account/userauth/passkey/registration-options","dto":"AccountPasskeyRegistrationOptionsRequest","fields":[{"name":"verificationToken","type":"string","kind":"string","required":true}]},"accountVerifyPasskeyRegistration":{"http":"POST","path":"/{version}/account/userauth/passkey/verify-registration","dto":"AccountVerifyPasskeyRegistrationRequest","fields":[{"name":"verificationToken","type":"string","kind":"string","required":true},{"name":"ceremonyId","type":"string","kind":"string","required":true},{"name":"attestationResponse","type":"string","kind":"string","required":true},{"name":"friendlyName","type":"string","kind":"string","required":false}]},"accountPasskeyAuthenticationOptions":{"http":"POST","path":"/{version}/account/userauth/passkey/authentication-options","dto":"AccountPasskeyAuthenticationOptionsRequest","fields":[{"name":"email","type":"string","kind":"string","required":true}]},"accountVerifyPasskeyAuthentication":{"http":"POST","path":"/{version}/account/userauth/passkey/verify-authentication","dto":"AccountVerifyPasskeyAuthenticationRequest","fields":[{"name":"ceremonyId","type":"string","kind":"string","required":true},{"name":"assertionResponse","type":"string","kind":"string","required":true}]},"listAccountPasskeys":{"http":"GET","path":"/{version}/account/userauth/passkeys","dto":"ListAccountPasskeysRequest","fields":[]},"renameAccountPasskey":{"http":"POST","path":"/{version}/account/userauth/passkeys/{CredentialId}/rename","dto":"RenameAccountPasskeyRequest","fields":[{"name":"credentialId","type":"string","kind":"string","required":true},{"name":"friendlyName","type":"string","kind":"string","required":true}]},"revokeAccountPasskey":{"http":"POST","path":"/{version}/account/userauth/passkeys/{CredentialId}/revoke","dto":"RevokeAccountPasskeyRequest","fields":[{"name":"credentialId","type":"string","kind":"string","required":true}]},"accountPasskeyEnrollmentOptions":{"http":"POST","path":"/{version}/account/userauth/passkey/enrollment-options","dto":"AccountPasskeyEnrollmentOptionsRequest","fields":[]},"accountVerifyPasskeyEnrollment":{"http":"POST","path":"/{version}/account/userauth/passkey/verify-enrollment","dto":"AccountVerifyPasskeyEnrollmentRequest","fields":[{"name":"ceremonyId","type":"string","kind":"string","required":true},{"name":"attestationResponse","type":"string","kind":"string","required":true},{"name":"friendlyName","type":"string","kind":"string","required":false}]},"getLicenseDomainDnsStatus":{"http":"GET","path":"/{version}/account/licensing/dns-status","dto":"GetLicenseDomainDnsStatus","fields":[{"name":"domain","type":"string","kind":"string","required":false}]},"getLicenses":{"http":"GET","path":"/{version}/account/licenses","dto":"GetLicenses","fields":[]},"getInstallationLicenseStatus":{"http":"GET","path":"/{version}/account/licensing/status","dto":"GetInstallationLicenseStatus","fields":[]},"getAiTools":{"http":"GET","path":"/{version}/account/ai/tools","dto":"GetAiToolsRequest","fields":[{"name":"toolset","type":"string","kind":"string","required":false}]},"invokeAiTool":{"http":"POST","path":"/{version}/account/ai/tools/{ToolName}","dto":"InvokeAiToolRequest","fields":[{"name":"toolName","type":"string","kind":"string","required":true},{"name":"argumentsJson","type":"string","kind":"string","required":false}]},"askChat":{"http":"POST","path":"/{version}/account/chat/complete","dto":"AskChatRequest","fields":[{"name":"prompt","type":"string","kind":"string","required":true},{"name":"profile","type":"string","kind":"string","required":false}]},"uploadChatAttachment":{"http":"POST","path":"/{version}/account/chat/attachments","dto":"UploadChatAttachmentRequest","fields":[{"name":"sessionId","type":"string","kind":"string","required":false},{"name":"fileName","type":"string","kind":"string","required":true},{"name":"contentType","type":"string","kind":"string","required":true},{"name":"base64Content","type":"string","kind":"string","required":true},{"name":"profile","type":"string","kind":"string","required":false},{"name":"topic","type":"string","kind":"string","required":false}]},"chatAvailability":{"http":"GET","path":"/{version}/account/chat/availability","dto":"ChatAvailabilityRequest","fields":[]},"getChatMemory":{"http":"GET","path":"/{version}/account/chat/memory","dto":"GetChatMemoryRequest","fields":[]},"forgetChatMemory":{"http":"DELETE","path":"/{version}/account/chat/memory/{NoteId}","dto":"ForgetChatMemoryRequest","fields":[{"name":"noteId","type":"string","kind":"string","required":true}]},"deleteChatSession":{"http":"DELETE","path":"/{version}/account/chat/sessions/{SessionId}","dto":"DeleteChatSessionRequest","fields":[{"name":"sessionId","type":"string","kind":"string","required":true}]},"setChatSessionArchived":{"http":"PATCH","path":"/{version}/account/chat/sessions/{SessionId}/archive","dto":"SetChatSessionArchivedRequest","fields":[{"name":"sessionId","type":"string","kind":"string","required":true},{"name":"archived","type":"boolean","kind":"boolean","required":true}]},"setChatSessionPinned":{"http":"PATCH","path":"/{version}/account/chat/sessions/{SessionId}/pin","dto":"SetChatSessionPinnedRequest","fields":[{"name":"sessionId","type":"string","kind":"string","required":true},{"name":"pinned","type":"boolean","kind":"boolean","required":true}]},"setChatSessionSharing":{"http":"PATCH","path":"/{version}/account/chat/sessions/{SessionId}/sharing","dto":"SetChatSessionSharingRequest","fields":[{"name":"sessionId","type":"string","kind":"string","required":true},{"name":"doNotShare","type":"boolean","kind":"boolean","required":true}]},"getChatSessions":{"http":"GET","path":"/{version}/account/chat/sessions","dto":"GetChatSessionsRequest","fields":[{"name":"take","type":"number","kind":"number","required":false},{"name":"includeArchived","type":"boolean","kind":"boolean","required":false}]},"getChatSessionEntries":{"http":"GET","path":"/{version}/account/chat/sessions/{SessionId}/entries","dto":"GetChatSessionEntriesRequest","fields":[{"name":"sessionId","type":"string","kind":"string","required":true},{"name":"sinceSeq","type":"number","kind":"number","required":false}]},"setChatEntryFeedback":{"http":"POST","path":"/{version}/account/chat/sessions/{SessionId}/entries/{EntryId}/feedback","dto":"SetChatEntryFeedbackRequest","fields":[{"name":"sessionId","type":"string","kind":"string","required":true},{"name":"entryId","type":"string","kind":"string","required":true},{"name":"feedback","type":"string","kind":"string","required":false}]},"answerChatQuestion":{"http":"POST","path":"/{version}/account/chat/sessions/{SessionId}/questions/{EntryId}/answer","dto":"AnswerChatQuestionRequest","fields":[{"name":"sessionId","type":"string","kind":"string","required":true},{"name":"entryId","type":"string","kind":"string","required":true}]},"decideChatPlan":{"http":"POST","path":"/{version}/account/chat/sessions/{SessionId}/plans/{EntryId}/decision","dto":"DecideChatPlanRequest","fields":[{"name":"sessionId","type":"string","kind":"string","required":true},{"name":"entryId","type":"string","kind":"string","required":true},{"name":"decision","type":"string","kind":"string","required":true},{"name":"comment","type":"string","kind":"string","required":false}]},"stopChatRunStep":{"http":"POST","path":"/{version}/account/chat/sessions/{SessionId}/steps/{EntryId}/stop","dto":"StopChatRunStepRequest","fields":[{"name":"sessionId","type":"string","kind":"string","required":true},{"name":"entryId","type":"string","kind":"string","required":true}]},"chatTurn":{"http":"POST","path":"/{version}/account/chat/turn","dto":"ChatTurnRequest","fields":[{"name":"sessionId","type":"string","kind":"string","required":false},{"name":"message","type":"string","kind":"string","required":true},{"name":"profile","type":"string","kind":"string","required":false},{"name":"topic","type":"string","kind":"string","required":false},{"name":"llmIntegrationId","type":"string","kind":"string","required":false},{"name":"model","type":"string","kind":"string","required":false},{"name":"screenContext","type":"ChatScreenContextDto","kind":"json","required":false}]},"mcp":{"http":"POST","path":"/{version}/account/mcp","dto":"McpRequest","fields":[{"name":"requestStream","type":"string","kind":"string","required":true}]}},"ai":{"deleteLlmIntegration":{"http":"DELETE","path":"/{version}/ai/integrations/llms/{Id}","dto":"DeleteLlmIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"disableLlmIntegration":{"http":"PUT","path":"/{version}/ai/integrations/llms/{Id}/disable","dto":"DisableLlmIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"enableLlmIntegration":{"http":"PUT","path":"/{version}/ai/integrations/llms/{Id}/enable","dto":"EnableLlmIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getLlmIntegration":{"http":"GET","path":"/{version}/ai/integrations/llms/{id}","dto":"GetLlmIntegration","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getLlmIntegrations":{"http":"GET","path":"/{version}/ai/integrations/llms/integrations","dto":"GetLlmIntegrations","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false}]},"saveLlmIntegration":{"http":"POST","path":"/{version}/ai/integrations/llms/","dto":"SaveLlmIntegration","fields":[{"name":"integration","type":"LlmIntegrationRequest","kind":"json","required":true}]},"testLlmIntegration":{"http":"POST","path":"/{version}/ai/integrations/llms/test","dto":"TestLlmIntegration","fields":[{"name":"integrationId","type":"string","kind":"string","required":true}]},"deleteMcpIntegration":{"http":"DELETE","path":"/{version}/ai/integrations/mcp/{Id}","dto":"DeleteMcpIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"disableMcpIntegration":{"http":"PUT","path":"/{version}/ai/integrations/mcp/{Id}/disable","dto":"DisableMcpIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"enableMcpIntegration":{"http":"PUT","path":"/{version}/ai/integrations/mcp/{Id}/enable","dto":"EnableMcpIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getMcpIntegration":{"http":"GET","path":"/{version}/ai/integrations/mcp/{id}","dto":"GetMcpIntegration","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getMcpIntegrations":{"http":"GET","path":"/{version}/ai/integrations/mcp/integrations","dto":"GetMcpIntegrations","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false}]},"saveMcpIntegration":{"http":"POST","path":"/{version}/ai/integrations/mcp/","dto":"SaveMcpIntegration","fields":[{"name":"integration","type":"McpIntegrationRequest","kind":"json","required":true}]},"testMcpIntegration":{"http":"POST","path":"/{version}/ai/integrations/mcp/test","dto":"TestMcpIntegration","fields":[{"name":"integrationId","type":"string","kind":"string","required":true}]}},"apikeys":{},"auth":{},"code":{"enableCode":{"http":"GET","path":"/{version}/code/enable","dto":"EnableCode","fields":[]},"disableCode":{"http":"GET","path":"/{version}/code/disable","dto":"DisableCode","fields":[]},"getCodeIntegrations":{"http":"GET","path":"/{version}/code/integrations","dto":"GetCodeIntegrations","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false}]},"getCodeIntegration":{"http":"GET","path":"/{version}/code/integrations/{id}","dto":"GetCodeIntegration","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"saveCodeIntegration":{"http":"POST","path":"/{version}/code/integrations","dto":"SaveCodeIntegration","fields":[{"name":"integration","type":"CodeIntegrationRequest","kind":"json","required":true}]},"testCodeIntegration":{"http":"POST","path":"/{version}/code/integrations/test","dto":"TestCodeIntegration","fields":[{"name":"integrationId","type":"string","kind":"string","required":true}]},"confirmCodeIntegrationHumanDelivery":{"http":"POST","path":"/{version}/code/integrations/confirm-human-delivery","dto":"ConfirmCodeIntegrationHumanDeliveryRequest","fields":[{"name":"integrationId","type":"string","kind":"string","required":true}]},"setCodeIntegrationAsDefault":{"http":"PUT","path":"/{version}/code/integrations/{Id}/default","dto":"SetCodeIntegrationAsDefault","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"deleteCodeIntegration":{"http":"DELETE","path":"/{version}/code/integrations/{Id}","dto":"DeleteCodeIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"enableCodeIntegration":{"http":"PUT","path":"/{version}/code/integrations/{Id}/enable","dto":"EnableCodeIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"disableCodeIntegration":{"http":"PUT","path":"/{version}/code/integrations/{Id}/disable","dto":"DisableCodeIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getMarketplaceListings":{"http":"GET","path":"/{version}/code/marketplace/listings","dto":"GetMarketplaceListings","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"categories","type":"MarketplaceCategory[]","kind":"string[]","required":false,"options":["Other","Crm","Erp","Marketing","Communication","Productivity","Storage","Analytics","Identity","Payments","DevTools","Ai","Files","Database","Calendar"]},{"name":"transports","type":"MarketplaceTransport[]","kind":"string[]","required":false,"options":["Mcp","Rest","Code","Internal","Sdk"]},{"name":"search","type":"string","kind":"string","required":false},{"name":"officialOnly","type":"boolean","kind":"boolean","required":false},{"name":"tags","type":"string[]","kind":"string[]","required":false}]},"getMarketplaceListingFunctionTokens":{"http":"GET","path":"/{version}/code/marketplace/listings/{ListingViewId}/functions/{FunctionKey}/tokens","dto":"GetMarketplaceListingFunctionTokens","fields":[{"name":"listingViewId","type":"string","kind":"string","required":true},{"name":"functionKey","type":"string","kind":"string","required":true}]},"getMarketplaceIntegrations":{"http":"GET","path":"/{version}/code/marketplace/integrations","dto":"GetMarketplaceIntegrations","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false}]},"getMarketplaceIntegration":{"http":"GET","path":"/{version}/code/marketplace/integrations/{IntegrationViewId}","dto":"GetMarketplaceIntegration","fields":[{"name":"integrationViewId","type":"string","kind":"string","required":true}]},"saveMarketplaceIntegration":{"http":"POST","path":"/{version}/code/marketplace/integrations","dto":"SaveMarketplaceIntegration","fields":[{"name":"integration","type":"MarketplaceIntegrationDto","kind":"json","required":true}]},"deleteMarketplaceIntegration":{"http":"DELETE","path":"/{version}/code/marketplace/integrations/{IntegrationViewId}","dto":"DeleteMarketplaceIntegration","fields":[{"name":"integrationViewId","type":"string","kind":"string","required":true}]},"enableMarketplaceIntegration":{"http":"POST","path":"/{version}/code/marketplace/integrations/{IntegrationViewId}/enable","dto":"EnableMarketplaceIntegration","fields":[{"name":"integrationViewId","type":"string","kind":"string","required":true}]},"disableMarketplaceIntegration":{"http":"POST","path":"/{version}/code/marketplace/integrations/{IntegrationViewId}/disable","dto":"DisableMarketplaceIntegration","fields":[{"name":"integrationViewId","type":"string","kind":"string","required":true}]},"getMarketplaceFunctions":{"http":"GET","path":"/{version}/code/marketplace/integrations/{IntegrationViewId}/functions","dto":"GetMarketplaceFunctions","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"integrationViewId","type":"string","kind":"string","required":true}]},"getMarketplaceFunction":{"http":"GET","path":"/{version}/code/marketplace/integrations/{IntegrationViewId}/functions/{FunctionViewId}","dto":"GetMarketplaceFunction","fields":[{"name":"integrationViewId","type":"string","kind":"string","required":true},{"name":"functionViewId","type":"string","kind":"string","required":true}]},"saveMarketplaceFunction":{"http":"POST","path":"/{version}/code/marketplace/integrations/{IntegrationViewId}/functions","dto":"SaveMarketplaceFunction","fields":[{"name":"integrationViewId","type":"string","kind":"string","required":true},{"name":"function","type":"MarketplaceFunctionDto","kind":"json","required":true}]},"deleteMarketplaceFunction":{"http":"DELETE","path":"/{version}/code/marketplace/integrations/{IntegrationViewId}/functions/{FunctionViewId}","dto":"DeleteMarketplaceFunction","fields":[{"name":"integrationViewId","type":"string","kind":"string","required":true},{"name":"functionViewId","type":"string","kind":"string","required":true}]},"enableMarketplaceFunction":{"http":"POST","path":"/{version}/code/marketplace/integrations/{IntegrationViewId}/functions/{FunctionViewId}/enable","dto":"EnableMarketplaceFunction","fields":[{"name":"integrationViewId","type":"string","kind":"string","required":true},{"name":"functionViewId","type":"string","kind":"string","required":true}]},"disableMarketplaceFunction":{"http":"POST","path":"/{version}/code/marketplace/integrations/{IntegrationViewId}/functions/{FunctionViewId}/disable","dto":"DisableMarketplaceFunction","fields":[{"name":"integrationViewId","type":"string","kind":"string","required":true},{"name":"functionViewId","type":"string","kind":"string","required":true}]},"getMarketplaceFunctionTokens":{"http":"GET","path":"/{version}/code/marketplace/integrations/{IntegrationViewId}/functions/{FunctionViewId}/tokens","dto":"GetMarketplaceFunctionTokens","fields":[{"name":"integrationViewId","type":"string","kind":"string","required":true},{"name":"functionViewId","type":"string","kind":"string","required":true}]},"invokeMarketplaceFunction":{"http":"POST","path":"/{version}/code/marketplace/functions/{FunctionViewId}/invoke","dto":"InvokeMarketplaceFunction","fields":[{"name":"functionViewId","type":"string","kind":"string","required":true}]},"getMarketplaceListing":{"http":"GET","path":"/{version}/code/marketplace/listings/{ListingViewId}","dto":"GetMarketplaceListing","fields":[{"name":"listingViewId","type":"string","kind":"string","required":true}]},"testMarketplaceIntegration":{"http":"POST","path":"/{version}/code/marketplace/integrations/{IntegrationViewId}/test","dto":"TestMarketplaceIntegration","fields":[{"name":"integrationViewId","type":"string","kind":"string","required":true}]},"replaceMarketplaceIntegrationSecrets":{"http":"PUT","path":"/{version}/code/marketplace/integrations/{IntegrationViewId}/secrets","dto":"ReplaceMarketplaceIntegrationSecretsRequest","fields":[{"name":"integrationViewId","type":"string","kind":"string","required":true}]},"revealMarketplaceIntegrationSecrets":{"http":"POST","path":"/{version}/code/marketplace/integrations/{IntegrationViewId}/secrets/reveal","dto":"RevealMarketplaceIntegrationSecretsRequest","fields":[{"name":"integrationViewId","type":"string","kind":"string","required":true}]},"setMarketplaceIntegrationTokenMappings":{"http":"PUT","path":"/{version}/code/marketplace/integrations/{IntegrationViewId}/token-mappings","dto":"SetMarketplaceIntegrationTokenMappingsRequest","fields":[{"name":"integrationViewId","type":"string","kind":"string","required":true},{"name":"tokenMappings","type":"MarketplaceTokenMappingDto[]","kind":"json[]","required":true}]},"getMarketplaceFunctionCatalog":{"http":"GET","path":"/{version}/code/marketplace/integrations/{IntegrationViewId}/catalog","dto":"GetMarketplaceFunctionCatalog","fields":[{"name":"integrationViewId","type":"string","kind":"string","required":true}]}},"compliance":{"getComplianceSettings":{"http":"GET","path":"/{version}/compliance/settings","dto":"GetComplianceSettings","fields":[]},"removeRetentionWindow":{"http":"DELETE","path":"/{version}/compliance/retention","dto":"RemoveRetentionWindowRequest","fields":[{"name":"dataKind","type":"string","kind":"string","required":true}]},"saveRetentionWindow":{"http":"POST","path":"/{version}/compliance/retention","dto":"SaveRetentionWindowRequest","fields":[{"name":"dataKind","type":"string","kind":"string","required":true},{"name":"days","type":"number","kind":"number","required":true},{"name":"action","type":"string","kind":"string","required":true}]},"assignRegime":{"http":"POST","path":"/{version}/compliance/regimes","dto":"AssignRegimeRequest","fields":[{"name":"regime","type":"string","kind":"string","required":true}]},"clearRegime":{"http":"DELETE","path":"/{version}/compliance/regimes","dto":"ClearRegimeRequest","fields":[{"name":"regime","type":"string","kind":"string","required":true}]},"defineConsentPurpose":{"http":"POST","path":"/{version}/compliance/purposes","dto":"DefineConsentPurposeRequest","fields":[{"name":"key","type":"string","kind":"string","required":true},{"name":"name","type":"string","kind":"string","required":true},{"name":"channel","type":"string","kind":"string","required":true},{"name":"mappedTags","type":"string[]","kind":"string[]","required":true},{"name":"regulatoryBasis","type":"string[]","kind":"string[]","required":true},{"name":"description","type":"string","kind":"string","required":false}]},"deprecateConsentPurpose":{"http":"POST","path":"/{version}/compliance/purposes/deprecate","dto":"DeprecateConsentPurposeRequest","fields":[{"name":"key","type":"string","kind":"string","required":true}]},"getLegalHolds":{"http":"GET","path":"/{version}/compliance/holds","dto":"GetLegalHolds","fields":[]},"placeLegalHold":{"http":"POST","path":"/{version}/compliance/holds","dto":"PlaceLegalHoldRequest","fields":[{"name":"subjectKind","type":"string","kind":"string","required":true},{"name":"subjectId","type":"string","kind":"string","required":true},{"name":"reason","type":"string","kind":"string","required":true}]},"releaseLegalHold":{"http":"POST","path":"/{version}/compliance/holds/release","dto":"ReleaseLegalHoldRequest","fields":[{"name":"holdId","type":"string","kind":"string","required":true}]},"approveDsarRequest":{"http":"POST","path":"/{version}/compliance/dsar/approve","dto":"ApproveDsarRequestRequest","fields":[{"name":"requestId","type":"string","kind":"string","required":true}]},"rejectDsarRequest":{"http":"POST","path":"/{version}/compliance/dsar/reject","dto":"RejectDsarRequestRequest","fields":[{"name":"requestId","type":"string","kind":"string","required":true},{"name":"reason","type":"string","kind":"string","required":true}]},"getDsarRequests":{"http":"GET","path":"/{version}/compliance/dsar","dto":"GetDsarRequests","fields":[]},"openDsarRequest":{"http":"POST","path":"/{version}/compliance/dsar","dto":"OpenDsarRequestRequest","fields":[{"name":"subjectKind","type":"string","kind":"string","required":true},{"name":"subjectId","type":"string","kind":"string","required":true}]},"getComplianceAuditLog":{"http":"GET","path":"/{version}/compliance/audit","dto":"GetComplianceAuditLog","fields":[{"name":"from","type":"string","kind":"string","required":false},{"name":"to","type":"string","kind":"string","required":false},{"name":"subjectKind","type":"string","kind":"string","required":false},{"name":"subjectId","type":"string","kind":"string","required":false},{"name":"limit","type":"number","kind":"number","required":true}]},"getAccountCompliance":{"http":"GET","path":"/{version}/compliance/account","dto":"GetAccountCompliance","fields":[]},"saveDsarPolicy":{"http":"POST","path":"/{version}/compliance/account/dsar-policy","dto":"SaveDsarPolicyRequest","fields":[{"name":"mode","type":"string","kind":"string","required":true},{"name":"delayDays","type":"number","kind":"number","required":true}]},"saveIncidentRouting":{"http":"POST","path":"/{version}/compliance/account/incident-routing","dto":"SaveIncidentRoutingRequest","fields":[{"name":"autoForwardAdvisories","type":"boolean","kind":"boolean","required":true},{"name":"securityContact","type":"string","kind":"string","required":false}]}},"database":{"disableDatabase":{"http":"GET","path":"/{version}/database/disable","dto":"DisableDatabase","fields":[]},"enableDatabase":{"http":"GET","path":"/{version}/database/enable","dto":"EnableDatabase","fields":[]},"deleteSchemaTrigger":{"http":"DELETE","path":"/{version}/database/schemas/triggers/{triggerId}","dto":"DeleteSchemaTrigger","fields":[{"name":"triggerId","type":"string","kind":"string","required":true},{"name":"triggerType","type":"TriggerType","kind":"string","required":true,"options":["Membership","Schema","Files","Payments"]},{"name":"schemaId","type":"string","kind":"string","required":false}]},"disableSchemaTrigger":{"http":"PATCH","path":"/{version}/database/schemas/triggers/{triggerId}/disable","dto":"DisableSchemaTrigger","fields":[{"name":"triggerId","type":"string","kind":"string","required":true},{"name":"triggerType","type":"TriggerType","kind":"string","required":true,"options":["Membership","Schema","Files","Payments"]},{"name":"schemaId","type":"string","kind":"string","required":false}]},"enableSchemaTrigger":{"http":"PATCH","path":"/{version}/database/schemas/triggers/{triggerId}/enable","dto":"EnableSchemaTrigger","fields":[{"name":"triggerId","type":"string","kind":"string","required":true},{"name":"triggerType","type":"TriggerType","kind":"string","required":true,"options":["Membership","Schema","Files","Payments"]},{"name":"schemaId","type":"string","kind":"string","required":false}]},"getSchemaTrigger":{"http":"GET","path":"/{version}/database/schemas/triggers/{id}","dto":"GetSchemaTrigger","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"schemaId","type":"string","kind":"string","required":false}]},"getSchemaTriggers":{"http":"GET","path":"/{version}/database/schemas/triggers","dto":"GetSchemaTriggers","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"schemaId","type":"string","kind":"string","required":false}]},"saveSchemaTrigger":{"http":"POST","path":"/{version}/database/schemas/triggers","dto":"SaveSchemaTrigger","fields":[{"name":"trigger","type":"SaveTriggerRequest","kind":"json","required":true}]},"deleteDatabaseTaxonomy":{"http":"DELETE","path":"/{version}/database/taxonomies/{Id}","dto":"DeleteDatabaseTaxonomyRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getDatabaseTaxonomy":{"http":"GET","path":"/{version}/database/taxonomies/{id}","dto":"GetDatabaseTaxonomy","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getDatabaseTaxonomies":{"http":"GET","path":"/{version}/database/taxonomies","dto":"GetDatabaseTaxonomies","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"pagingArgs","type":"PagingArgs","kind":"json","required":false}]},"getDatabaseTaxonomyTree":{"http":"GET","path":"/{version}/database/taxonomies/tree","dto":"GetDatabaseTaxonomyTreeRequest","fields":[{"name":"includeTerms","type":"boolean","kind":"boolean","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"saveDatabaseTaxonomy":{"http":"POST","path":"/{version}/database/taxonomies","dto":"SaveDatabaseTaxonomyRequest","fields":[{"name":"viewId","type":"string","kind":"string","required":false},{"name":"taxonomyName","type":"string","kind":"string","required":true},{"name":"description","type":"string","kind":"string","required":false},{"name":"termsMetaDataSchema","type":"string","kind":"string","required":false},{"name":"termsMetaVisualSchema","type":"string","kind":"string","required":false},{"name":"parentId","type":"string","kind":"string","required":false},{"name":"dependencies","type":"string[]","kind":"string[]","required":false}]},"deleteDatabaseTaxonomyTerm":{"http":"DELETE","path":"/{version}/database/taxonomies/{TaxonomyId}/terms/{Id}","dto":"DeleteDatabaseTaxonomyTermRequest","fields":[{"name":"taxonomyId","type":"string","kind":"string","required":true},{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"deleteManyDatabaseTaxonomyTerms":{"http":"DELETE","path":"/{version}/database/taxonomies/{TaxonomyId}/terms/many","dto":"DeleteManyDatabaseTaxonomyTermsRequest","fields":[{"name":"taxonomyId","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"filter","type":"string","kind":"string","required":true}]},"getDatabaseTaxonomyTerm":{"http":"GET","path":"/{version}/database/taxonomies/{TaxonomyId}/terms/{Id}","dto":"GetDatabaseTaxonomyTermRequest","fields":[{"name":"taxonomyId","type":"string","kind":"string","required":true},{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getDatabaseMergedTermTree":{"http":"GET","path":"/{version}/database/taxonomies/{TaxonomyName}/merged-tree","dto":"GetDatabaseMergedTermTreeRequest","fields":[{"name":"taxonomyName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getDatabaseTaxonomyTermTree":{"http":"GET","path":"/{version}/database/taxonomies/{TaxonomyName}/terms/tree","dto":"GetDatabaseTaxonomyTermTreeRequest","fields":[{"name":"taxonomyName","type":"string","kind":"string","required":true},{"name":"rootTermId","type":"string","kind":"string","required":false},{"name":"depth","type":"number","kind":"number","required":false},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"saveDatabaseTaxonomyTerm":{"http":"POST","path":"/{version}/database/taxonomies/{TaxonomyId}/terms","dto":"SaveDatabaseTaxonomyTermRequest","fields":[{"name":"taxonomyId","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"document","type":"string","kind":"string","required":true}]},"updateDatabaseTaxonomyTerm":{"http":"PUT","path":"/{version}/database/taxonomies/{TaxonomyId}/terms/{Id}","dto":"UpdateDatabaseTaxonomyTermRequest","fields":[{"name":"taxonomyId","type":"string","kind":"string","required":true},{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"update","type":"string","kind":"string","required":true}]},"applyDatabaseSchemaBundle":{"http":"POST","path":"/{version}/database/schemas/apply-bundle","dto":"ApplyDatabaseSchemaBundleRequest","fields":[{"name":"entities","type":"string","kind":"string","required":false},{"name":"bundleJson","type":"string","kind":"string","required":false},{"name":"tier","type":"string","kind":"string","required":false},{"name":"translatable","type":"boolean","kind":"boolean","required":true},{"name":"publish","type":"boolean","kind":"boolean","required":true}]},"deleteDatabaseSchema":{"http":"DELETE","path":"/{version}/database/schemas/{Id}","dto":"DeleteDatabaseSchemaRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"discardDatabaseSchemaDraft":{"http":"DELETE","path":"/{version}/database/schemas/{Id}/draft","dto":"DiscardDatabaseSchemaDraftRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getDatabaseSchema":{"http":"GET","path":"/{version}/database/schemas/{id}","dto":"GetDatabaseSchema","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getDatabaseSchemas":{"http":"GET","path":"/{version}/database/schemas","dto":"GetDatabaseSchemas","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"pagingArgs","type":"PagingArgs","kind":"json","required":false}]},"getDatabaseSchemaDraft":{"http":"GET","path":"/{version}/database/schemas/{Id}/draft","dto":"GetDatabaseSchemaDraft","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getDatabaseSchemaListSettings":{"http":"GET","path":"/{version}/database/schemas/{Id}/list-settings","dto":"GetDatabaseSchemaListSettings","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getDatabaseSchemaVersionDiff":{"http":"GET","path":"/{version}/database/schemas/{Id}/versions/diff","dto":"GetDatabaseSchemaVersionDiff","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"fromVersion","type":"number","kind":"number","required":true},{"name":"toVersion","type":"number","kind":"number","required":true}]},"getDatabaseSchemaVersions":{"http":"GET","path":"/{version}/database/schemas/{Id}/versions","dto":"GetDatabaseSchemaVersions","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"publishDatabaseSchema":{"http":"POST","path":"/{version}/database/schemas/{Id}/publish","dto":"PublishDatabaseSchemaRequest","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"confirmed","type":"boolean","kind":"boolean","required":true}]},"renameDatabaseSchema":{"http":"PUT","path":"/{version}/database/schemas/{Id}/rename","dto":"RenameDatabaseSchemaRequest","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"title","type":"string","kind":"string","required":true},{"name":"renameUniqueName","type":"boolean","kind":"boolean","required":true}]},"saveDatabaseSchema":{"http":"POST","path":"/{version}/database/schemas","dto":"SaveDatabaseSchemaRequest","fields":[{"name":"viewId","type":"string","kind":"string","required":false},{"name":"schemaName","type":"string","kind":"string","required":true},{"name":"dataSchema","type":"string","kind":"string","required":false},{"name":"visualSchema","type":"string","kind":"string","required":false},{"name":"settings","type":"SchemaSettingsDto","kind":"json","required":false}]},"updateDatabaseSchemaDraft":{"http":"PUT","path":"/{version}/database/schemas/{Id}/draft","dto":"UpdateDatabaseSchemaDraftRequest","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"dataSchema","type":"string","kind":"string","required":false},{"name":"visualSchema","type":"string","kind":"string","required":false}]},"updateDatabaseSchemaListSettings":{"http":"PUT","path":"/{version}/database/schemas/{Id}/list-settings","dto":"UpdateDatabaseSchemaListSettingsRequest","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"settings","type":"SchemaListSettingsDto","kind":"json","required":true}]},"updateDatabaseSchemaSettings":{"http":"PUT","path":"/{version}/database/schemas/{Id}/settings","dto":"UpdateDatabaseSchemaSettingsRequest","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"settings","type":"SchemaSettingsDto","kind":"json","required":true}]},"aggregateRecords":{"http":"POST","path":"/{version}/database/collections/{collectionName}/aggregate","dto":"AggregateRecords","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"pipeline","type":"string","kind":"string","required":true}]},"changeRecordResponsibility":{"http":"PUT","path":"/{version}/database/collections/{collectionName}/{id}/responsibility","dto":"ChangeRecordResponsibility","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"newResponsibleUserId","type":"string","kind":"string","required":true}]},"countRecords":{"http":"GET","path":"/{version}/database/collections/{collectionName}/count","dto":"CountRecords","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"filter","type":"string","kind":"string","required":false},{"name":"schemaVersion","type":"number","kind":"number","required":false}]},"deleteManyRecords":{"http":"DELETE","path":"/{version}/database/collections/{collectionName}/many","dto":"DeleteManyRecords","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"filter","type":"string","kind":"string","required":true}]},"deleteRecord":{"http":"DELETE","path":"/{version}/database/collections/{collectionName}/{id}","dto":"DeleteRecord","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"distinctRecordValues":{"http":"GET","path":"/{version}/database/collections/{collectionName}/distinct","dto":"DistinctRecordValues","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"field","type":"string","kind":"string","required":true},{"name":"filter","type":"string","kind":"string","required":false},{"name":"schemaVersion","type":"number","kind":"number","required":false}]},"executeRecordsAggregate":{"http":"POST","path":"/{version}/database/collections/{collectionName}/aggregates/{aggregateId}/execute","dto":"ExecuteRecordsAggregate","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"aggregateId","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"findRecords":{"http":"GET","path":"/{version}/database/collections/{collectionName}","dto":"FindRecords","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"filter","type":"string","kind":"string","required":false},{"name":"contactId","type":"string","kind":"string","required":false},{"name":"schemaVersion","type":"number","kind":"number","required":false},{"name":"pagingArgs","type":"PagingArgs","kind":"json","required":false},{"name":"sortBy","type":"string","kind":"string","required":false},{"name":"sortOrder","type":"number","kind":"number","required":false}]},"findOneRecord":{"http":"GET","path":"/{version}/database/collections/{collectionName}/{id}","dto":"FindOneRecord","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getCollectionIndexes":{"http":"GET","path":"/{version}/database/collections/{collectionName}/indexes","dto":"GetCollectionIndexes","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"insertManyRecords":{"http":"POST","path":"/{version}/database/collections/{collectionName}/many","dto":"InsertManyRecords","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"documents","type":"string","kind":"string","required":true}]},"insertRecord":{"http":"POST","path":"/{version}/database/collections/{collectionName}","dto":"InsertRecord","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"document","type":"string","kind":"string","required":true}]},"replaceRecord":{"http":"PUT","path":"/{version}/database/collections/{collectionName}/{id}/replace","dto":"ReplaceRecord","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"replacement","type":"string","kind":"string","required":true}]},"seedCollectionRecords":{"http":"POST","path":"/{version}/database/collections/seed","dto":"SeedCollectionRecords","fields":[{"name":"mode","type":"string","kind":"string","required":false},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"collections","type":"string","kind":"string","required":true}]},"updateManyRecords":{"http":"PUT","path":"/{version}/database/collections/{collectionName}/many","dto":"UpdateManyRecords","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"filter","type":"string","kind":"string","required":true},{"name":"update","type":"string","kind":"string","required":true}]},"updateOneRecord":{"http":"PUT","path":"/{version}/database/collections/{collectionName}/{id}","dto":"UpdateOneRecord","fields":[{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"update","type":"string","kind":"string","required":true}]},"deleteDatabaseIntegration":{"http":"DELETE","path":"/{version}/database/integrations/{Id}","dto":"DeleteDatabaseIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"disableDatabaseIntegration":{"http":"PUT","path":"/{version}/database/integrations/{Id}/disable","dto":"DisableDatabaseIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"enableDatabaseIntegration":{"http":"PUT","path":"/{version}/database/integrations/{Id}/enable","dto":"EnableDatabaseIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getDatabaseIntegration":{"http":"GET","path":"/{version}/database/integrations/{id}","dto":"GetDatabaseIntegration","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getDatabaseIntegrations":{"http":"GET","path":"/{version}/database/integrations","dto":"GetDatabaseIntegrations","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"pagingArgs","type":"PagingArgs","kind":"json","required":false}]},"getAllowedFlexTiers":{"http":"GET","path":"/{version}/database/integrations/flex-tiers","dto":"GetAllowedFlexTiers","fields":[]},"revealManagedFlexConnectionString":{"http":"GET","path":"/{version}/database/integrations/{Id}/connection-string","dto":"RevealManagedFlexConnectionString","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"saveDatabaseIntegration":{"http":"POST","path":"/{version}/database/integrations","dto":"SaveDatabaseIntegration","fields":[{"name":"integration","type":"DatabaseIntegrationRequest","kind":"json","required":true}]},"setDatabaseIntegrationAsDefault":{"http":"PUT","path":"/{version}/database/integrations/{Id}/default","dto":"SetDatabaseIntegrationAsDefaultRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"testDatabaseIntegration":{"http":"POST","path":"/{version}/database/integrations/test","dto":"TestDatabaseIntegration","fields":[{"name":"integrationId","type":"string","kind":"string","required":true}]},"createCollectionImport":{"http":"POST","path":"/{version}/database/imports","dto":"CreateCollectionImport","fields":[{"name":"file","type":"FileResourceRefDto","kind":"json","required":true},{"name":"schemaId","type":"string","kind":"string","required":true},{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"delimiter","type":"string","kind":"string","required":true},{"name":"hasHeader","type":"boolean","kind":"boolean","required":true},{"name":"mapping","type":"ImportColumnMappingDto[]","kind":"json[]","required":true}]},"deleteCollectionImport":{"http":"DELETE","path":"/{version}/database/imports/{Id}","dto":"DeleteCollectionImportRequest","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getCollectionImport":{"http":"GET","path":"/{version}/database/imports/{Id}","dto":"GetCollectionImport","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getCollectionImports":{"http":"GET","path":"/{version}/database/imports","dto":"GetCollectionImports","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"requestImportUploadUrl":{"http":"POST","path":"/{version}/database/imports/upload-url","dto":"RequestImportUploadUrlRequest","fields":[{"name":"fileAccountId","type":"string","kind":"string","required":false},{"name":"fileName","type":"string","kind":"string","required":true}]},"analyzeImportFile":{"http":"POST","path":"/{version}/database/imports/analyze","dto":"AnalyzeImportFileRequest","fields":[{"name":"file","type":"FileResourceRefDto","kind":"json","required":true},{"name":"delimiter","type":"string","kind":"string","required":true},{"name":"hasHeader","type":"boolean","kind":"boolean","required":true}]},"deleteDatabaseAggregate":{"http":"DELETE","path":"/{version}/database/aggregates/{Id}","dto":"DeleteDatabaseAggregateRequest","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"schemaId","type":"string","kind":"string","required":true}]},"getDatabaseAggregate":{"http":"GET","path":"/{version}/database/aggregates/{Id}","dto":"GetDatabaseAggregate","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"schemaId","type":"string","kind":"string","required":true}]},"getDatabaseAggregates":{"http":"GET","path":"/{version}/database/aggregates","dto":"GetDatabaseAggregates","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"schemaId","type":"string","kind":"string","required":true},{"name":"pagingArgs","type":"PagingArgs","kind":"json","required":false}]},"saveDatabaseAggregate":{"http":"POST","path":"/{version}/database/aggregates","dto":"SaveDatabaseAggregateRequest","fields":[{"name":"viewId","type":"string","kind":"string","required":false},{"name":"schemaId","type":"string","kind":"string","required":true},{"name":"displayName","type":"string","kind":"string","required":true},{"name":"description","type":"string","kind":"string","required":false},{"name":"pipeline","type":"string","kind":"string","required":true}]},"testDatabaseAggregate":{"http":"POST","path":"/{version}/database/aggregates/test","dto":"TestDatabaseAggregateRequest","fields":[{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"collectionName","type":"string","kind":"string","required":true},{"name":"pipeline","type":"string","kind":"string","required":true}]}},"diagnostics":{"getDiagnosticPacks":{"http":"GET","path":"/{version}/diagnostics/packs","dto":"GetDiagnosticPacks","fields":[]},"runDiagnosticPack":{"http":"POST","path":"/{version}/diagnostics/packs/{PackName}/run","dto":"RunDiagnosticPackRequest","fields":[{"name":"packName","type":"string","kind":"string","required":true},{"name":"packVersion","type":"number","kind":"number","required":false},{"name":"caseId","type":"string","kind":"string","required":false}]},"getDiagnosticEcho":{"http":"GET","path":"/{version}/diagnostics/echo","dto":"GetDiagnosticEcho","fields":[{"name":"caseId","type":"string","kind":"string","required":false}]},"readDiagnosticEvents":{"http":"GET","path":"/{version}/diagnostics/events","dto":"ReadDiagnosticEventsRequest","fields":[{"name":"stream","type":"string","kind":"string","required":true},{"name":"from","type":"number","kind":"number","required":true},{"name":"count","type":"number","kind":"number","required":true},{"name":"caseId","type":"string","kind":"string","required":false}]},"queryDiagnosticLogs":{"http":"GET","path":"/{version}/diagnostics/logs","dto":"QueryDiagnosticLogsRequest","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"level","type":"string","kind":"string","required":false},{"name":"module","type":"string","kind":"string","required":false},{"name":"logCorrelationId","type":"string","kind":"string","required":false},{"name":"eventCode","type":"string","kind":"string","required":false},{"name":"search","type":"string","kind":"string","required":false},{"name":"fromUtc","type":"string","kind":"string","required":false},{"name":"toUtc","type":"string","kind":"string","required":false},{"name":"caseId","type":"string","kind":"string","required":false}]},"inspectDiagnosticRedis":{"http":"GET","path":"/{version}/diagnostics/redis","dto":"InspectDiagnosticRedisRequest","fields":[{"name":"keyPattern","type":"string","kind":"string","required":true},{"name":"caseId","type":"string","kind":"string","required":false}]},"runDiagnosticHealthCheck":{"http":"POST","path":"/{version}/diagnostics/health/{CheckId}","dto":"RunDiagnosticHealthCheckRequest","fields":[{"name":"checkId","type":"string","kind":"string","required":true},{"name":"caseId","type":"string","kind":"string","required":false}]}},"echo":{"echo":{"http":"GET","path":"/{version}/echo","dto":"Echo","fields":[]}},"email":{"oneClickUnsubscribe":{"http":"POST","path":"/{version}/email/one-click-unsubscribe","dto":"OneClickUnsubscribeRequest","fields":[{"name":"token","type":"string","kind":"string","required":true}]}},"environments":{},"files":{"disableFiles":{"http":"GET","path":"/{version}/files/disable","dto":"DisableFiles","fields":[]},"enableFiles":{"http":"GET","path":"/{version}/files/enable","dto":"EnableFiles","fields":[]},"deleteFilesTrigger":{"http":"DELETE","path":"/{version}/files/triggers/{triggerId}","dto":"DeleteFilesTrigger","fields":[{"name":"triggerId","type":"string","kind":"string","required":true},{"name":"triggerType","type":"TriggerType","kind":"string","required":true,"options":["Membership","Schema","Files","Payments"]},{"name":"schemaId","type":"string","kind":"string","required":false}]},"disableFilesTrigger":{"http":"PATCH","path":"/{version}/files/triggers/{triggerId}/disable","dto":"DisableFilesTrigger","fields":[{"name":"triggerId","type":"string","kind":"string","required":true},{"name":"triggerType","type":"TriggerType","kind":"string","required":true,"options":["Membership","Schema","Files","Payments"]},{"name":"schemaId","type":"string","kind":"string","required":false}]},"enableFilesTrigger":{"http":"PATCH","path":"/{version}/files/triggers/{triggerId}/enable","dto":"EnableFilesTrigger","fields":[{"name":"triggerId","type":"string","kind":"string","required":true},{"name":"triggerType","type":"TriggerType","kind":"string","required":true,"options":["Membership","Schema","Files","Payments"]},{"name":"schemaId","type":"string","kind":"string","required":false}]},"getFilesTrigger":{"http":"GET","path":"/{version}/files/triggers/{id}","dto":"GetFilesTrigger","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"schemaId","type":"string","kind":"string","required":false}]},"getFilesTriggers":{"http":"GET","path":"/{version}/files/triggers","dto":"GetFilesTriggers","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"schemaId","type":"string","kind":"string","required":false}]},"saveFilesTrigger":{"http":"POST","path":"/{version}/files/triggers","dto":"SaveFilesTrigger","fields":[{"name":"trigger","type":"SaveTriggerRequest","kind":"json","required":true}]},"makeFilePrivate":{"http":"POST","path":"/{version}/files/item/private","dto":"MakeFilePrivateRequest","fields":[{"name":"filesIntegrationId","type":"string","kind":"string","required":true},{"name":"path","type":"string","kind":"string","required":true}]},"makeFilePublic":{"http":"POST","path":"/{version}/files/item/public","dto":"MakeFilePublicRequest","fields":[{"name":"filesIntegrationId","type":"string","kind":"string","required":true},{"name":"path","type":"string","kind":"string","required":true}]},"makeFolderPrivate":{"http":"POST","path":"/{version}/files/folder/private","dto":"MakeFolderPrivateRequest","fields":[{"name":"filesIntegrationId","type":"string","kind":"string","required":true},{"name":"path","type":"string","kind":"string","required":true}]},"makeFolderPublic":{"http":"POST","path":"/{version}/files/folder/public","dto":"MakeFolderPublicRequest","fields":[{"name":"filesIntegrationId","type":"string","kind":"string","required":true},{"name":"path","type":"string","kind":"string","required":true}]},"deleteFilesIntegration":{"http":"DELETE","path":"/{version}/files/integrations/{Id}","dto":"DeleteFilesIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"disableFilesIntegration":{"http":"PUT","path":"/{version}/files/integrations/{Id}/disable","dto":"DisableFilesIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"enableFilesIntegration":{"http":"PUT","path":"/{version}/files/integrations/{Id}/enable","dto":"EnableFilesIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getFilesIntegration":{"http":"GET","path":"/{version}/files/integrations/{id}","dto":"GetFilesIntegration","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getFilesIntegrations":{"http":"GET","path":"/{version}/files/integrations","dto":"GetFilesIntegrations","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false}]},"saveFilesIntegration":{"http":"POST","path":"/{version}/files/integrations","dto":"SaveFilesIntegration","fields":[{"name":"integration","type":"FilesIntegrationRequest","kind":"json","required":true}]},"setFilesIntegrationAsDefault":{"http":"PUT","path":"/{version}/files/integrations/{Id}/default","dto":"SetFilesIntegrationAsDefaultRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"testFilesIntegration":{"http":"POST","path":"/{version}/files/integrations/test","dto":"TestFilesIntegration","fields":[{"name":"integrationId","type":"string","kind":"string","required":true}]},"getFile":{"http":"GET","path":"/{version}/files/item","dto":"GetFile","fields":[{"name":"filesIntegrationId","type":"string","kind":"string","required":true},{"name":"path","type":"string","kind":"string","required":true}]},"getFolderFiles":{"http":"GET","path":"/{version}/files/folder","dto":"GetFolderFiles","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"filesIntegrationId","type":"string","kind":"string","required":true},{"name":"path","type":"string","kind":"string","required":false}]}},"internal":{"internalsTypeGen":{"http":"GET","path":"/internal/_typegen","dto":"InternalsTypeGen","fields":[{"name":"typegen_0_SmtpEmailIntegrationRequest","type":"SmtpEmailIntegrationRequest","kind":"json","required":false},{"name":"typegen_1_AwsSesEmailIntegrationRequest","type":"AwsSesEmailIntegrationRequest","kind":"json","required":false},{"name":"typegen_2_SendGridEmailIntegrationRequest","type":"SendGridEmailIntegrationRequest","kind":"json","required":false},{"name":"typegen_3_MailGunEmailIntegrationRequest","type":"MailGunEmailIntegrationRequest","kind":"json","required":false},{"name":"typegen_4_EmailToAllUsersDeliverySettingsRequest","type":"EmailToAllUsersDeliverySettingsRequest","kind":"json","required":false},{"name":"typegen_5_EmailToAccountUsersDeliverySettingsRequest","type":"EmailToAccountUsersDeliverySettingsRequest","kind":"json","required":false},{"name":"typegen_6_EmailToCollectionRecordsDeliverySettingsRequest","type":"EmailToCollectionRecordsDeliverySettingsRequest","kind":"json","required":false},{"name":"typegen_7_EmailToEmailsDeliverySettingsRequest","type":"EmailToEmailsDeliverySettingsRequest","kind":"json","required":false},{"name":"typegen_8_EmailToUsersDeliverySettingsRequest","type":"EmailToUsersDeliverySettingsRequest","kind":"json","required":false},{"name":"typegen_9_MembershipTriggerRequest","type":"MembershipTriggerRequest","kind":"json","required":false},{"name":"typegen_10_SchemaTriggerRequest","type":"SchemaTriggerRequest","kind":"json","required":false},{"name":"typegen_11_FilesTriggerRequest","type":"FilesTriggerRequest","kind":"json","required":false},{"name":"typegen_12_PaymentTriggerRequest","type":"PaymentTriggerRequest","kind":"json","required":false},{"name":"typegen_15_MongoDbConnectionStringDatabaseIntegrationRequest","type":"MongoDbConnectionStringDatabaseIntegrationRequest","kind":"json","required":false},{"name":"typegen_16_MongoDbAtlasFlexManagedDatabaseIntegrationRequest","type":"MongoDbAtlasFlexManagedDatabaseIntegrationRequest","kind":"json","required":false},{"name":"typegen_16_GoogleDriveFilesIntegrationRequest","type":"GoogleDriveFilesIntegrationRequest","kind":"json","required":false},{"name":"typegen_17_FtpFilesIntegrationRequest","type":"FtpFilesIntegrationRequest","kind":"json","required":false},{"name":"typegen_18_DropBoxFilesIntegrationRequest","type":"DropBoxFilesIntegrationRequest","kind":"json","required":false},{"name":"typegen_19_AppleICloudFilesIntegrationRequest","type":"AppleICloudFilesIntegrationRequest","kind":"json","required":false},{"name":"typegen_20_AwsS3FilesIntegrationRequest","type":"AwsS3FilesIntegrationRequest","kind":"json","required":false},{"name":"typegen_21_GoogleCloudFilesIntegrationRequest","type":"GoogleCloudFilesIntegrationRequest","kind":"json","required":false},{"name":"typegen_22_AzureBlobFilesIntegrationRequest","type":"AzureBlobFilesIntegrationRequest","kind":"json","required":false},{"name":"typegen_23_LocalFilesIntegrationRequest","type":"LocalFilesIntegrationRequest","kind":"json","required":false},{"name":"typegen_24_AmqpLoggingIntegrationRequest","type":"AmqpLoggingIntegrationRequest","kind":"json","required":false},{"name":"typegen_25_AwsKinesisLoggingIntegrationRequest","type":"AwsKinesisLoggingIntegrationRequest","kind":"json","required":false},{"name":"typegen_26_AwsS3LoggingIntegrationRequest","type":"AwsS3LoggingIntegrationRequest","kind":"json","required":false},{"name":"typegen_28_NewRelicLoggingIntegrationRequest","type":"NewRelicLoggingIntegrationRequest","kind":"json","required":false},{"name":"typegen_30_MongoDbLoggingIntegrationRequest","type":"MongoDbLoggingIntegrationRequest","kind":"json","required":false},{"name":"typegen_31_KafkaLoggingIntegrationRequest","type":"KafkaLoggingIntegrationRequest","kind":"json","required":false},{"name":"typegen_32_PrometheusLoggingIntegrationRequest","type":"PrometheusLoggingIntegrationRequest","kind":"json","required":false},{"name":"typegen_33_DataDogLoggingIntegrationRequest","type":"DataDogLoggingIntegrationRequest","kind":"json","required":false},{"name":"typegen_34_InternalKafkaLoggingIntegrationRequest","type":"InternalKafkaLoggingIntegrationRequest","kind":"json","required":false},{"name":"typegen_35_ElasticSearchLoggingIntegrationRequest","type":"ElasticSearchLoggingIntegrationRequest","kind":"json","required":false},{"name":"typegen_37_SplunkLoggingIntegrationRequest","type":"SplunkLoggingIntegrationRequest","kind":"json","required":false},{"name":"typegen_38_AzureOtelLoggingIntegrationRequest","type":"AzureOtelLoggingIntegrationRequest","kind":"json","required":false},{"name":"typegen_39_KibanaLoggingIntegrationRequest","type":"KibanaLoggingIntegrationRequest","kind":"json","required":false},{"name":"typegen_40_LocalFileLoggingIntegrationRequest","type":"LocalFileLoggingIntegrationRequest","kind":"json","required":false},{"name":"typegen_41_OktaMembershipIntegrationRequest","type":"OktaMembershipIntegrationRequest","kind":"json","required":false},{"name":"typegen_42_XMembershipIntegrationRequest","type":"XMembershipIntegrationRequest","kind":"json","required":false},{"name":"typegen_43_GoogleMembershipIntegrationRequest","type":"GoogleMembershipIntegrationRequest","kind":"json","required":false},{"name":"typegen_44_MicrosoftMembershipIntegrationRequest","type":"MicrosoftMembershipIntegrationRequest","kind":"json","required":false},{"name":"typegen_45_GitHubMembershipIntegrationRequest","type":"GitHubMembershipIntegrationRequest","kind":"json","required":false},{"name":"typegen_46_MetaMembershipIntegrationRequest","type":"MetaMembershipIntegrationRequest","kind":"json","required":false},{"name":"typegen_47_AppleMembershipIntegrationRequest","type":"AppleMembershipIntegrationRequest","kind":"json","required":false},{"name":"typegen_48_LemonSqueezyPaymentIntegrationRequest","type":"LemonSqueezyPaymentIntegrationRequest","kind":"json","required":false},{"name":"typegen_49_AdyenPaymentIntegrationRequest","type":"AdyenPaymentIntegrationRequest","kind":"json","required":false},{"name":"typegen_50_MolliePaymentIntegrationRequest","type":"MolliePaymentIntegrationRequest","kind":"json","required":false},{"name":"typegen_51_PaddlePaymentIntegrationRequest","type":"PaddlePaymentIntegrationRequest","kind":"json","required":false},{"name":"typegen_52_PayPalPaymentIntegrationRequest","type":"PayPalPaymentIntegrationRequest","kind":"json","required":false},{"name":"typegen_53_StripePaymentIntegrationRequest","type":"StripePaymentIntegrationRequest","kind":"json","required":false},{"name":"typegen_54_AppleInAppPaymentIntegrationRequest","type":"AppleInAppPaymentIntegrationRequest","kind":"json","required":false},{"name":"typegen_55_GoogleInAppPaymentIntegrationRequest","type":"GoogleInAppPaymentIntegrationRequest","kind":"json","required":false},{"name":"typegen_56_EdgeWebPushIntegrationRequest","type":"EdgeWebPushIntegrationRequest","kind":"json","required":false},{"name":"typegen_57_ChromePluginPushIntegrationRequest","type":"ChromePluginPushIntegrationRequest","kind":"json","required":false},{"name":"typegen_58_SafariPushIntegrationRequest","type":"SafariPushIntegrationRequest","kind":"json","required":false},{"name":"typegen_59_ChromeWebPushIntegrationRequest","type":"ChromeWebPushIntegrationRequest","kind":"json","required":false},{"name":"typegen_60_FirefoxWebPushIntegrationRequest","type":"FirefoxWebPushIntegrationRequest","kind":"json","required":false},{"name":"typegen_61_AndroidFirebasePushIntegrationRequest","type":"AndroidFirebasePushIntegrationRequest","kind":"json","required":false},{"name":"typegen_62_AppleApnsPushIntegrationRequest","type":"AppleApnsPushIntegrationRequest","kind":"json","required":false},{"name":"typegen_65_AwsLambdaCodeIntegrationRequest","type":"AwsLambdaCodeIntegrationRequest","kind":"json","required":false},{"name":"typegen_66_AzureFunctionsCodeIntegrationRequest","type":"AzureFunctionsCodeIntegrationRequest","kind":"json","required":false},{"name":"typegen_67_GoogleCloudFunctionsCodeIntegrationRequest","type":"GoogleCloudFunctionsCodeIntegrationRequest","kind":"json","required":false},{"name":"typegen_68_OllamaLlmIntegrationRequest","type":"OllamaLlmIntegrationRequest","kind":"json","required":false},{"name":"typegen_69_OpenRouterLlmIntegrationRequest","type":"OpenRouterLlmIntegrationRequest","kind":"json","required":false},{"name":"typegen_70_MistralLlmIntegrationRequest","type":"MistralLlmIntegrationRequest","kind":"json","required":false},{"name":"typegen_71_GrokLlmIntegrationRequest","type":"GrokLlmIntegrationRequest","kind":"json","required":false},{"name":"typegen_72_GroqLlmIntegrationRequest","type":"GroqLlmIntegrationRequest","kind":"json","required":false},{"name":"typegen_73_GoogleLlmIntegrationRequest","type":"GoogleLlmIntegrationRequest","kind":"json","required":false},{"name":"typegen_74_AnthropicLlmIntegrationRequest","type":"AnthropicLlmIntegrationRequest","kind":"json","required":false},{"name":"typegen_75_OpenAiLlmIntegrationRequest","type":"OpenAiLlmIntegrationRequest","kind":"json","required":false},{"name":"typegen_76_PlaywrightMcpIntegrationRequest","type":"PlaywrightMcpIntegrationRequest","kind":"json","required":false},{"name":"typegen_77_MongoDbMcpIntegrationRequest","type":"MongoDbMcpIntegrationRequest","kind":"json","required":false},{"name":"typegen_78_GitHubMcpIntegrationRequest","type":"GitHubMcpIntegrationRequest","kind":"json","required":false},{"name":"typegen_79_StripeMcpIntegrationRequest","type":"StripeMcpIntegrationRequest","kind":"json","required":false},{"name":"typegen_80_BraveSearchMcpIntegrationRequest","type":"BraveSearchMcpIntegrationRequest","kind":"json","required":false},{"name":"typegen_81_ObsidianMcpIntegrationRequest","type":"ObsidianMcpIntegrationRequest","kind":"json","required":false},{"name":"typegen_82_EmailTemplateDto","type":"EmailTemplateDto","kind":"json","required":false},{"name":"typegen_83_PushTemplateDto","type":"PushTemplateDto","kind":"json","required":false},{"name":"typegen_84_SmsTemplateDto","type":"SmsTemplateDto","kind":"json","required":false},{"name":"typegen_85_SystemEmailTemplateDto","type":"SystemEmailTemplateDto","kind":"json","required":false},{"name":"typegen_86_TriggerActionEmailDto","type":"TriggerActionEmailDto","kind":"json","required":false},{"name":"typegen_87_TriggerActionPushDto","type":"TriggerActionPushDto","kind":"json","required":false},{"name":"typegen_88_TriggerActionCodeDto","type":"TriggerActionCodeDto","kind":"json","required":false},{"name":"typegen_89_TriggerActionWebhookDto","type":"TriggerActionWebhookDto","kind":"json","required":false},{"name":"typegen_236_TriggerActionSmsDto","type":"TriggerActionSmsDto","kind":"json","required":false},{"name":"typegen_237_TriggerActionSseDto","type":"TriggerActionSseDto","kind":"json","required":false},{"name":"typegen_238_TriggerActionMarketplaceDto","type":"TriggerActionMarketplaceDto","kind":"json","required":false},{"name":"typegen_239_SseDeliverySettingsDto","type":"SseDeliverySettingsDto","kind":"json","required":false},{"name":"typegen_240_GetTriggers","type":"GetTriggers","kind":"json","required":false},{"name":"typegen_241_GetTriggersResponse","type":"GetTriggersResponse","kind":"json","required":false},{"name":"typegen_90_EmailToAllUsersDeliverySettingsDto","type":"EmailToAllUsersDeliverySettingsDto","kind":"json","required":false},{"name":"typegen_91_EmailToAccountUsersDeliverySettingsDto","type":"EmailToAccountUsersDeliverySettingsDto","kind":"json","required":false},{"name":"typegen_92_EmailToUsersDeliverySettingsDto","type":"EmailToUsersDeliverySettingsDto","kind":"json","required":false},{"name":"typegen_93_EmailToEmailAddressesDeliverySettingsDto","type":"EmailToEmailAddressesDeliverySettingsDto","kind":"json","required":false},{"name":"typegen_94_EmailToCollectionRecordsDeliverySettingsDto","type":"EmailToCollectionRecordsDeliverySettingsDto","kind":"json","required":false},{"name":"typegen_95_PushToAllUsersDeliverySettingsDto","type":"PushToAllUsersDeliverySettingsDto","kind":"json","required":false},{"name":"typegen_96_PushToUsersDeliverySettingsDto","type":"PushToUsersDeliverySettingsDto","kind":"json","required":false},{"name":"typegen_229_PushToAccountUsersDeliverySettingsDto","type":"PushToAccountUsersDeliverySettingsDto","kind":"json","required":false},{"name":"typegen_97_PushToCollectionRecordsDeliverySettingsDto","type":"PushToCollectionRecordsDeliverySettingsDto","kind":"json","required":false},{"name":"typegen_98_PushToDevicesDeliverySettingsDto","type":"PushToDevicesDeliverySettingsDto","kind":"json","required":false},{"name":"typegen_99_SmsToAllUsersDeliverySettingsDto","type":"SmsToAllUsersDeliverySettingsDto","kind":"json","required":false},{"name":"typegen_100_SmsToUsersDeliverySettingsDto","type":"SmsToUsersDeliverySettingsDto","kind":"json","required":false},{"name":"typegen_101_SmsToCollectionRecordsDeliverySettingsDto","type":"SmsToCollectionRecordsDeliverySettingsDto","kind":"json","required":false},{"name":"typegen_102_SmsToPhoneNumbersDeliverySettingsDto","type":"SmsToPhoneNumbersDeliverySettingsDto","kind":"json","required":false},{"name":"typegen_103_OpenAiLlmIntegrationDto","type":"OpenAiLlmIntegrationDto","kind":"json","required":false},{"name":"typegen_104_AnthropicLlmIntegrationDto","type":"AnthropicLlmIntegrationDto","kind":"json","required":false},{"name":"typegen_105_OllamaLlmIntegrationDto","type":"OllamaLlmIntegrationDto","kind":"json","required":false},{"name":"typegen_106_GroqLlmIntegrationDto","type":"GroqLlmIntegrationDto","kind":"json","required":false},{"name":"typegen_107_GoogleLlmIntegrationDto","type":"GoogleLlmIntegrationDto","kind":"json","required":false},{"name":"typegen_108_MistralLlmIntegrationDto","type":"MistralLlmIntegrationDto","kind":"json","required":false},{"name":"typegen_109_OpenRouterLlmIntegrationDto","type":"OpenRouterLlmIntegrationDto","kind":"json","required":false},{"name":"typegen_110_GrokLlmIntegrationDto","type":"GrokLlmIntegrationDto","kind":"json","required":false},{"name":"typegen_111_DockerMcpIntegrationDto","type":"DockerMcpIntegrationDto","kind":"json","required":false},{"name":"typegen_112_GoogleCalendarMcpIntegrationDto","type":"GoogleCalendarMcpIntegrationDto","kind":"json","required":false},{"name":"typegen_113_ObsidianMcpIntegrationDto","type":"ObsidianMcpIntegrationDto","kind":"json","required":false},{"name":"typegen_114_AwsLambdaCrossAccountRoleCodeIntegrationDto","type":"AwsLambdaCrossAccountRoleCodeIntegrationDto","kind":"json","required":false},{"name":"typegen_115_AwsLambdaIamCodeIntegrationDto","type":"AwsLambdaIamCodeIntegrationDto","kind":"json","required":false},{"name":"typegen_116_AzureFunctionsCodeIntegrationDto","type":"AzureFunctionsCodeIntegrationDto","kind":"json","required":false},{"name":"typegen_118_GoogleCloudFunctionsCodeIntegrationDto","type":"GoogleCloudFunctionsCodeIntegrationDto","kind":"json","required":false},{"name":"typegen_120_AdyenPaymentIntegrationDto","type":"AdyenPaymentIntegrationDto","kind":"json","required":false},{"name":"typegen_121_AppleInAppPaymentIntegrationDto","type":"AppleInAppPaymentIntegrationDto","kind":"json","required":false},{"name":"typegen_122_GoogleInAppPaymentIntegrationDto","type":"GoogleInAppPaymentIntegrationDto","kind":"json","required":false},{"name":"typegen_123_LemonSqueezyPaymentIntegrationDto","type":"LemonSqueezyPaymentIntegrationDto","kind":"json","required":false},{"name":"typegen_124_MolliePaymentIntegrationDto","type":"MolliePaymentIntegrationDto","kind":"json","required":false},{"name":"typegen_125_PaddlePaymentIntegrationDto","type":"PaddlePaymentIntegrationDto","kind":"json","required":false},{"name":"typegen_126_PayPalPaymentIntegrationDto","type":"PayPalPaymentIntegrationDto","kind":"json","required":false},{"name":"typegen_127_StripePaymentIntegrationDto","type":"StripePaymentIntegrationDto","kind":"json","required":false},{"name":"typegen_184_ShopifyPaymentIntegrationDto","type":"ShopifyPaymentIntegrationDto","kind":"json","required":false},{"name":"typegen_185_WooCommercePaymentIntegrationDto","type":"WooCommercePaymentIntegrationDto","kind":"json","required":false},{"name":"typegen_186_MagentoPaymentIntegrationDto","type":"MagentoPaymentIntegrationDto","kind":"json","required":false},{"name":"typegen_187_BraintreePaymentIntegrationDto","type":"BraintreePaymentIntegrationDto","kind":"json","required":false},{"name":"typegen_188_AuthorizeNetPaymentIntegrationDto","type":"AuthorizeNetPaymentIntegrationDto","kind":"json","required":false},{"name":"typegen_189_CheckOutComPaymentIntegrationDto","type":"CheckOutComPaymentIntegrationDto","kind":"json","required":false},{"name":"typegen_190_WorldpayPaymentIntegrationDto","type":"WorldpayPaymentIntegrationDto","kind":"json","required":false},{"name":"typegen_128_AppleSignInMembershipIntegrationDto","type":"AppleSignInMembershipIntegrationDto","kind":"json","required":false},{"name":"typegen_129_GitHubMembershipIntegrationDto","type":"GitHubMembershipIntegrationDto","kind":"json","required":false},{"name":"typegen_130_GoogleMembershipIntegrationDto","type":"GoogleMembershipIntegrationDto","kind":"json","required":false},{"name":"typegen_131_MetaMembershipIntegrationDto","type":"MetaMembershipIntegrationDto","kind":"json","required":false},{"name":"typegen_132_MicrosoftMembershipIntegrationDto","type":"MicrosoftMembershipIntegrationDto","kind":"json","required":false},{"name":"typegen_133_OktaMembershipIntegrationDto","type":"OktaMembershipIntegrationDto","kind":"json","required":false},{"name":"typegen_134_XMembershipIntegrationDto","type":"XMembershipIntegrationDto","kind":"json","required":false},{"name":"typegen_135_AmqpLoggingIntegrationDto","type":"AmqpLoggingIntegrationDto","kind":"json","required":false},{"name":"typegen_136_AwsKinesisLoggingIntegrationDto","type":"AwsKinesisLoggingIntegrationDto","kind":"json","required":false},{"name":"typegen_137_AwsS3CrossAccountRoleLoggingIntegrationDto","type":"AwsS3CrossAccountRoleLoggingIntegrationDto","kind":"json","required":false},{"name":"typegen_138_AwsS3IamLoggingIntegrationDto","type":"AwsS3IamLoggingIntegrationDto","kind":"json","required":false},{"name":"typegen_139_AzureOtelLoggingIntegrationDto","type":"AzureOtelLoggingIntegrationDto","kind":"json","required":false},{"name":"typegen_140_DataDogLoggingIntegrationDto","type":"DataDogLoggingIntegrationDto","kind":"json","required":false},{"name":"typegen_141_ElasticSearchLoggingIntegrationDto","type":"ElasticSearchLoggingIntegrationDto","kind":"json","required":false},{"name":"typegen_142_InternalKafkaLoggingIntegrationDto","type":"InternalKafkaLoggingIntegrationDto","kind":"json","required":false},{"name":"typegen_143_KafkaLoggingIntegrationDto","type":"KafkaLoggingIntegrationDto","kind":"json","required":false},{"name":"typegen_144_KibanaLoggingIntegrationDto","type":"KibanaLoggingIntegrationDto","kind":"json","required":false},{"name":"typegen_145_LocalFileLoggingIntegrationDto","type":"LocalFileLoggingIntegrationDto","kind":"json","required":false},{"name":"typegen_147_MongoDbLoggingIntegrationDto","type":"MongoDbLoggingIntegrationDto","kind":"json","required":false},{"name":"typegen_148_NewRelicLoggingIntegrationDto","type":"NewRelicLoggingIntegrationDto","kind":"json","required":false},{"name":"typegen_149_PrometheusLoggingIntegrationDto","type":"PrometheusLoggingIntegrationDto","kind":"json","required":false},{"name":"typegen_150_SplunkLoggingIntegrationDto","type":"SplunkLoggingIntegrationDto","kind":"json","required":false},{"name":"typegen_153_AppleICloudFilesIntegrationDto","type":"AppleICloudFilesIntegrationDto","kind":"json","required":false},{"name":"typegen_154_AwsS3CrossAccountRoleFilesIntegrationDto","type":"AwsS3CrossAccountRoleFilesIntegrationDto","kind":"json","required":false},{"name":"typegen_155_AwsS3IamFilesIntegrationDto","type":"AwsS3IamFilesIntegrationDto","kind":"json","required":false},{"name":"typegen_156_AzureBlobFilesIntegrationDto","type":"AzureBlobFilesIntegrationDto","kind":"json","required":false},{"name":"typegen_157_DropBoxFilesIntegrationDto","type":"DropBoxFilesIntegrationDto","kind":"json","required":false},{"name":"typegen_158_FtpFilesIntegrationDto","type":"FtpFilesIntegrationDto","kind":"json","required":false},{"name":"typegen_159_GoogleCloudFilesIntegrationDto","type":"GoogleCloudFilesIntegrationDto","kind":"json","required":false},{"name":"typegen_160_GoogleDriveFilesIntegrationDto","type":"GoogleDriveFilesIntegrationDto","kind":"json","required":false},{"name":"typegen_161_LocalFilesIntegrationDto","type":"LocalFilesIntegrationDto","kind":"json","required":false},{"name":"typegen_164_MongoDbConnectionStringIntegrationDto","type":"MongoDbConnectionStringIntegrationDto","kind":"json","required":false},{"name":"typegen_165_MongoDbAtlasFlexManagedIntegrationDto","type":"MongoDbAtlasFlexManagedIntegrationDto","kind":"json","required":false},{"name":"typegen_165_BirdSmsIntegrationDto","type":"BirdSmsIntegrationDto","kind":"json","required":false},{"name":"typegen_166_PlivoSmsIntegrationDto","type":"PlivoSmsIntegrationDto","kind":"json","required":false},{"name":"typegen_167_SinchSmsIntegrationDto","type":"SinchSmsIntegrationDto","kind":"json","required":false},{"name":"typegen_168_TelesignSmsIntegrationDto","type":"TelesignSmsIntegrationDto","kind":"json","required":false},{"name":"typegen_169_TelnyxSmsIntegrationDto","type":"TelnyxSmsIntegrationDto","kind":"json","required":false},{"name":"typegen_170_TwilioSmsIntegrationDto","type":"TwilioSmsIntegrationDto","kind":"json","required":false},{"name":"typegen_171_VonageSmsIntegrationDto","type":"VonageSmsIntegrationDto","kind":"json","required":false},{"name":"typegen_246_FakeSmsIntegrationDto","type":"FakeSmsIntegrationDto","kind":"json","required":false},{"name":"typegen_172_AndroidFirebasePushIntegrationDto","type":"AndroidFirebasePushIntegrationDto","kind":"json","required":false},{"name":"typegen_173_AppleApnsPushIntegrationDto","type":"AppleApnsPushIntegrationDto","kind":"json","required":false},{"name":"typegen_174_ChromePluginPushIntegrationDto","type":"ChromePluginPushIntegrationDto","kind":"json","required":false},{"name":"typegen_175_ChromeWebPushIntegrationDto","type":"ChromeWebPushIntegrationDto","kind":"json","required":false},{"name":"typegen_176_EdgeWebPushIntegrationDto","type":"EdgeWebPushIntegrationDto","kind":"json","required":false},{"name":"typegen_177_FirefoxWebPushIntegrationDto","type":"FirefoxWebPushIntegrationDto","kind":"json","required":false},{"name":"typegen_178_SafariPushIntegrationDto","type":"SafariPushIntegrationDto","kind":"json","required":false},{"name":"typegen_247_FakePushIntegrationDto","type":"FakePushIntegrationDto","kind":"json","required":false},{"name":"typegen_179_AwsCrossAccountRoleEmailIntegrationDto","type":"AwsCrossAccountRoleEmailIntegrationDto","kind":"json","required":false},{"name":"typegen_180_AwsIamEmailIntegrationDto","type":"AwsIamEmailIntegrationDto","kind":"json","required":false},{"name":"typegen_181_MailGunEmailIntegrationDto","type":"MailGunEmailIntegrationDto","kind":"json","required":false},{"name":"typegen_182_SendGridEmailIntegrationDto","type":"SendGridEmailIntegrationDto","kind":"json","required":false},{"name":"typegen_183_SmtpEmailIntegrationDto","type":"SmtpEmailIntegrationDto","kind":"json","required":false},{"name":"typegen_248_FakeEmailIntegrationDto","type":"FakeEmailIntegrationDto","kind":"json","required":false},{"name":"typegen_192_WebhookIntegrationDto","type":"WebhookIntegrationDto","kind":"json","required":false},{"name":"typegen_193_WebhookDestinationDto","type":"WebhookDestinationDto","kind":"json","required":false},{"name":"typegen_194_SchedulerTaskDto","type":"SchedulerTaskDto","kind":"json","required":false},{"name":"typegen_195_MongoDbAggregateDto","type":"MongoDbAggregateDto","kind":"json","required":false},{"name":"typegen_196_MarketplaceIntegrationDto","type":"MarketplaceIntegrationDto","kind":"json","required":false},{"name":"typegen_197_MarketplaceFunctionDto","type":"MarketplaceFunctionDto","kind":"json","required":false},{"name":"typegen_198_MarketplaceListingDto","type":"MarketplaceListingDto","kind":"json","required":false},{"name":"typegen_199_MarketplaceFunctionDefinitionDto","type":"MarketplaceFunctionDefinitionDto","kind":"json","required":false},{"name":"typegen_200_MarketplaceFunctionParameterDto","type":"MarketplaceFunctionParameterDto","kind":"json","required":false},{"name":"typegen_201_EnableCode","type":"EnableCode","kind":"json","required":false},{"name":"typegen_202_DisableCode","type":"DisableCode","kind":"json","required":false},{"name":"typegen_203_GetCodeIntegrations","type":"GetCodeIntegrations","kind":"json","required":false},{"name":"typegen_204_GetCodeIntegration","type":"GetCodeIntegration","kind":"json","required":false},{"name":"typegen_205_SaveCodeIntegration","type":"SaveCodeIntegration","kind":"json","required":false},{"name":"typegen_206_TestCodeIntegration","type":"TestCodeIntegration","kind":"json","required":false},{"name":"typegen_207_ConfirmCodeIntegrationHumanDeliveryRequest","type":"ConfirmCodeIntegrationHumanDeliveryRequest","kind":"json","required":false},{"name":"typegen_208_SetCodeIntegrationAsDefault","type":"SetCodeIntegrationAsDefault","kind":"json","required":false},{"name":"typegen_209_DeleteCodeIntegrationRequest","type":"DeleteCodeIntegrationRequest","kind":"json","required":false},{"name":"typegen_210_EnableCodeIntegrationRequest","type":"EnableCodeIntegrationRequest","kind":"json","required":false},{"name":"typegen_211_DisableCodeIntegrationRequest","type":"DisableCodeIntegrationRequest","kind":"json","required":false},{"name":"typegen_212_GetMarketplaceListings","type":"GetMarketplaceListings","kind":"json","required":false},{"name":"typegen_213_GetMarketplaceListingFunctionTokens","type":"GetMarketplaceListingFunctionTokens","kind":"json","required":false},{"name":"typegen_214_GetMarketplaceIntegrations","type":"GetMarketplaceIntegrations","kind":"json","required":false},{"name":"typegen_215_GetMarketplaceIntegration","type":"GetMarketplaceIntegration","kind":"json","required":false},{"name":"typegen_216_SaveMarketplaceIntegration","type":"SaveMarketplaceIntegration","kind":"json","required":false},{"name":"typegen_217_DeleteMarketplaceIntegration","type":"DeleteMarketplaceIntegration","kind":"json","required":false},{"name":"typegen_218_EnableMarketplaceIntegration","type":"EnableMarketplaceIntegration","kind":"json","required":false},{"name":"typegen_219_DisableMarketplaceIntegration","type":"DisableMarketplaceIntegration","kind":"json","required":false},{"name":"typegen_221_GetMarketplaceFunctions","type":"GetMarketplaceFunctions","kind":"json","required":false},{"name":"typegen_222_GetMarketplaceFunction","type":"GetMarketplaceFunction","kind":"json","required":false},{"name":"typegen_223_SaveMarketplaceFunction","type":"SaveMarketplaceFunction","kind":"json","required":false},{"name":"typegen_224_DeleteMarketplaceFunction","type":"DeleteMarketplaceFunction","kind":"json","required":false},{"name":"typegen_225_EnableMarketplaceFunction","type":"EnableMarketplaceFunction","kind":"json","required":false},{"name":"typegen_226_DisableMarketplaceFunction","type":"DisableMarketplaceFunction","kind":"json","required":false},{"name":"typegen_227_GetMarketplaceFunctionTokens","type":"GetMarketplaceFunctionTokens","kind":"json","required":false},{"name":"typegen_228_InvokeMarketplaceFunction","type":"InvokeMarketplaceFunction","kind":"json","required":false},{"name":"typegen_232_GetMarketplaceListing","type":"GetMarketplaceListing","kind":"json","required":false},{"name":"typegen_233_TestMarketplaceIntegration","type":"TestMarketplaceIntegration","kind":"json","required":false},{"name":"typegen_234_TestMarketplaceIntegrationResponse","type":"TestMarketplaceIntegrationResponse","kind":"json","required":false},{"name":"typegen_235_GetMarketplaceListingResponse","type":"GetMarketplaceListingResponse","kind":"json","required":false},{"name":"typegen_230_AdminPortalStructureDto","type":"AdminPortalStructureDto","kind":"json","required":false},{"name":"typegen_231_AdminPortalModuleDto","type":"AdminPortalModuleDto","kind":"json","required":false},{"name":"typegen_236_UserMessageEntryWireDto","type":"UserMessageEntryWireDto","kind":"json","required":false},{"name":"typegen_237_AssistantTextEntryWireDto","type":"AssistantTextEntryWireDto","kind":"json","required":false},{"name":"typegen_238_AssistantQuestionEntryWireDto","type":"AssistantQuestionEntryWireDto","kind":"json","required":false},{"name":"typegen_239_UserAnswerEntryWireDto","type":"UserAnswerEntryWireDto","kind":"json","required":false},{"name":"typegen_240_PlanEntryWireDto","type":"PlanEntryWireDto","kind":"json","required":false},{"name":"typegen_241_UserDecisionEntryWireDto","type":"UserDecisionEntryWireDto","kind":"json","required":false},{"name":"typegen_242_RunStepEntryWireDto","type":"RunStepEntryWireDto","kind":"json","required":false},{"name":"typegen_243_ActionPendingEntryWireDto","type":"ActionPendingEntryWireDto","kind":"json","required":false},{"name":"typegen_244_NoticeEntryWireDto","type":"NoticeEntryWireDto","kind":"json","required":false},{"name":"typegen_245_ConversationSnapshotEntryWireDto","type":"ConversationSnapshotEntryWireDto","kind":"json","required":false}]}},"licensing":{"startLicenseDomainVerification":{"http":"POST","path":"/{version}/licensing/domain-verification/start","dto":"StartLicenseDomainVerificationRequest","fields":[{"name":"domain","type":"string","kind":"string","required":false}]},"getLicenseDomainVerificationStatus":{"http":"GET","path":"/{version}/licensing/domain-verification/status","dto":"GetLicenseDomainVerificationStatus","fields":[{"name":"domain","type":"string","kind":"string","required":false}]},"postLicenseHeartbeat":{"http":"POST","path":"/{version}/licensing/heartbeat","dto":"PostLicenseHeartbeat","fields":[{"name":"license","type":"string","kind":"string","required":false},{"name":"licenseAccountId","type":"string","kind":"string","required":false},{"name":"installationId","type":"string","kind":"string","required":false},{"name":"domain","type":"string","kind":"string","required":false},{"name":"hostKind","type":"string","kind":"string","required":false},{"name":"release","type":"string","kind":"string","required":false},{"name":"instanceVersion","type":"string","kind":"string","required":false}]}},"logs":{"disableLogging":{"http":"GET","path":"/{version}/logs/disable","dto":"DisableLogging","fields":[]},"enableLogging":{"http":"GET","path":"/{version}/logs/enable","dto":"EnableLogging","fields":[{"name":"createNorbixLogging","type":"boolean","kind":"boolean","required":true}]},"deleteLoggingIntegration":{"http":"DELETE","path":"/{version}/logs/integrations/{Id}","dto":"DeleteLoggingIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"wipeLogs","type":"boolean","kind":"boolean","required":true}]},"disableLoggingIntegration":{"http":"PUT","path":"/{version}/logs/integrations/{Id}/disable","dto":"DisableLoggingIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"enableLoggingIntegration":{"http":"PUT","path":"/{version}/logs/integrations/{Id}/enable","dto":"EnableLoggingIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getLoggingIntegration":{"http":"GET","path":"/{version}/logs/integrations/{id}","dto":"GetLoggingIntegration","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getLoggingIntegrations":{"http":"GET","path":"/{version}/logs/integrations","dto":"GetLoggingIntegrations","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false}]},"saveLoggingIntegration":{"http":"POST","path":"/{version}/logs/integrations","dto":"SaveLoggingIntegration","fields":[{"name":"integration","type":"LoggingIntegrationRequest","kind":"json","required":true}]},"testLoggingIntegration":{"http":"POST","path":"/{version}/logs/integrations/test","dto":"TestLoggingIntegration","fields":[{"name":"integrationId","type":"string","kind":"string","required":true}]},"cleanLogs":{"http":"POST","path":"/{version}/logs/clean","dto":"CleanLogs","fields":[]},"getLogsByCorrelationId":{"http":"GET","path":"/{version}/logs/audit","dto":"GetLogsByCorrelationId","fields":[{"name":"targetCorrelationId","type":"string","kind":"string","required":true}]},"getLogs":{"http":"GET","path":"/{version}/logs","dto":"GetLogs","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"level","type":"string","kind":"string","required":false},{"name":"module","type":"string","kind":"string","required":false},{"name":"logCorrelationId","type":"string","kind":"string","required":false},{"name":"eventCode","type":"string","kind":"string","required":false},{"name":"search","type":"string","kind":"string","required":false},{"name":"fromUtc","type":"string","kind":"string","required":false},{"name":"toUtc","type":"string","kind":"string","required":false}]},"getLogSettings":{"http":"GET","path":"/{version}/logs/settings","dto":"GetLogSettings","fields":[]},"saveLogSettings":{"http":"POST","path":"/{version}/logs/settings","dto":"SaveLogSettings","fields":[{"name":"skipCloudDashboardLogs","type":"boolean","kind":"boolean","required":true},{"name":"skipHttpBodyMeta","type":"boolean","kind":"boolean","required":true},{"name":"aiChatLoggingEnabled","type":"boolean","kind":"boolean","required":true}]}},"membership":{"disableMembership":{"http":"GET","path":"/{version}/membership/disable","dto":"DisableMembership","fields":[]},"enableMembership":{"http":"GET","path":"/{version}/membership/enable","dto":"EnableMembership","fields":[]},"issueServiceUserApiKey":{"http":"POST","path":"/{version}/membership/users/{Id}/api-keys","dto":"IssueServiceUserApiKeyRequest","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"name","type":"string","kind":"string","required":true},{"name":"scopes","type":"string[]","kind":"string[]","required":false},{"name":"expiresInDays","type":"number","kind":"number","required":false},{"name":"notes","type":"string","kind":"string","required":false}]},"listServiceUserApiKeys":{"http":"GET","path":"/{version}/membership/users/{Id}/api-keys","dto":"ListServiceUserApiKeysRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"deleteServiceUserApiKey":{"http":"DELETE","path":"/{version}/membership/users/{Id}/api-keys/{KeyId}","dto":"DeleteServiceUserApiKeyRequest","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"keyId","type":"number","kind":"number","required":true}]},"deleteMembershipTrigger":{"http":"DELETE","path":"/{version}/membership/triggers/{triggerId}","dto":"DeleteMembershipTrigger","fields":[{"name":"triggerId","type":"string","kind":"string","required":true},{"name":"triggerType","type":"TriggerType","kind":"string","required":true,"options":["Membership","Schema","Files","Payments"]},{"name":"schemaId","type":"string","kind":"string","required":false}]},"disableMembershipTrigger":{"http":"PATCH","path":"/{version}/membership/triggers/{triggerId}/disable","dto":"DisableMembershipTrigger","fields":[{"name":"triggerId","type":"string","kind":"string","required":true},{"name":"triggerType","type":"TriggerType","kind":"string","required":true,"options":["Membership","Schema","Files","Payments"]},{"name":"schemaId","type":"string","kind":"string","required":false}]},"enableMembershipTrigger":{"http":"PATCH","path":"/{version}/membership/triggers/{triggerId}/enable","dto":"EnableMembershipTrigger","fields":[{"name":"triggerId","type":"string","kind":"string","required":true},{"name":"triggerType","type":"TriggerType","kind":"string","required":true,"options":["Membership","Schema","Files","Payments"]},{"name":"schemaId","type":"string","kind":"string","required":false}]},"getMembershipTrigger":{"http":"GET","path":"/{version}/membership/triggers/{id}","dto":"GetMembershipTrigger","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"schemaId","type":"string","kind":"string","required":false}]},"getMembershipTriggers":{"http":"GET","path":"/{version}/membership/triggers","dto":"GetMembershipTriggers","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"schemaId","type":"string","kind":"string","required":false}]},"saveMembershipTrigger":{"http":"POST","path":"/{version}/membership/triggers","dto":"SaveMembershipTrigger","fields":[{"name":"trigger","type":"SaveTriggerRequest","kind":"json","required":true}]},"createRole":{"http":"POST","path":"/{version}/membership/roles","dto":"CreateRole","fields":[{"name":"roleName","type":"string","kind":"string","required":true},{"name":"description","type":"string","kind":"string","required":false},{"name":"policies","type":"string[]","kind":"string[]","required":false}]},"deleteRole":{"http":"DELETE","path":"/{version}/membership/roles","dto":"DeleteRole","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getRole":{"http":"GET","path":"/{version}/membership/roles/{Id}","dto":"GetRole","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getRoles":{"http":"GET","path":"/{version}/membership/roles","dto":"GetRoles","fields":[]},"updateRolePolicies":{"http":"PATCH","path":"/{version}/membership/roles","dto":"UpdateRolePolicies","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"roleName","type":"string","kind":"string","required":true},{"name":"description","type":"string","kind":"string","required":false},{"name":"policies","type":"string[]","kind":"string[]","required":false}]},"createPolicy":{"http":"POST","path":"/{version}/membership/policies","dto":"CreatePolicy","fields":[{"name":"policyName","type":"string","kind":"string","required":true},{"name":"description","type":"string","kind":"string","required":false},{"name":"policyDocumentJson","type":"string","kind":"string","required":false}]},"deletePolicy":{"http":"DELETE","path":"/{version}/membership/policies","dto":"DeletePolicy","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getPolicy":{"http":"GET","path":"/{version}/membership/policies/{Id}","dto":"GetPolicy","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getPolicies":{"http":"GET","path":"/{version}/membership/policies","dto":"GetPolicies","fields":[]},"updatePolicy":{"http":"PUT","path":"/{version}/membership/policies","dto":"UpdatePolicy","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"policyName","type":"string","kind":"string","required":true},{"name":"description","type":"string","kind":"string","required":false},{"name":"policyDocumentJson","type":"string","kind":"string","required":false}]},"getPasskeySettings":{"http":"GET","path":"/{version}/membership/passkey/settings","dto":"GetPasskeySettings","fields":[]},"savePasskeySettings":{"http":"POST","path":"/{version}/membership/passkey/settings","dto":"SavePasskeySettings","fields":[{"name":"enabled","type":"boolean","kind":"boolean","required":true},{"name":"codeTtlMinutes","type":"number","kind":"number","required":true},{"name":"maxCredentialsPerUser","type":"number","kind":"number","required":true},{"name":"recoveryCodeCount","type":"number","kind":"number","required":true},{"name":"generateRecoveryCodesAtSignup","type":"boolean","kind":"boolean","required":true},{"name":"authenticatorAttachment","type":"string","kind":"string","required":true},{"name":"allowMagicLinkRecovery","type":"boolean","kind":"boolean","required":true},{"name":"refreshTokenTtlDays","type":"number","kind":"number","required":true},{"name":"rpId","type":"string","kind":"string","required":false}]},"deleteMembershipIntegration":{"http":"DELETE","path":"/{version}/membership/integrations/{Id}","dto":"DeleteMembershipIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"disableMembershipIntegration":{"http":"PUT","path":"/{version}/membership/integrations/{Id}/disable","dto":"DisableMembershipIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"enableMembershipIntegration":{"http":"PUT","path":"/{version}/membership/integrations/{Id}/enable","dto":"EnableMembershipIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getMembershipIntegration":{"http":"GET","path":"/{version}/membership/integrations/{id}","dto":"GetMembershipIntegration","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getMembershipIntegrations":{"http":"GET","path":"/{version}/membership/integrations","dto":"GetMembershipIntegrations","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false}]},"saveMembershipIntegration":{"http":"POST","path":"/{version}/membership/integrations","dto":"SaveMembershipIntegration","fields":[{"name":"integration","type":"MembershipIntegrationRequest","kind":"json","required":true}]},"setMembershipIntegrationAsDefault":{"http":"PUT","path":"/{version}/membership/integrations/{Id}/default","dto":"SetMembershipIntegrationAsDefaultRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getAuthorizationSettings":{"http":"GET","path":"/{version}/membership/authorization","dto":"GetAuthorizationSettings","fields":[]},"updateAuthorizationSettings":{"http":"PUT","path":"/{version}/membership/authorization","dto":"UpdateAuthorizationSettings","fields":[{"name":"setting","type":"string","kind":"string","required":false},{"name":"defaultRoles","type":"string[]","kind":"string[]","required":false},{"name":"allowedRegistrationRoles","type":"string[]","kind":"string[]","required":false},{"name":"allowGuestUsers","type":"boolean","kind":"boolean","required":true},{"name":"guestCleanupPeriodDays","type":"number","kind":"number","required":false},{"name":"userRegistersAsRole","type":"string","kind":"string","required":false},{"name":"guestRegistersAsRole","type":"string","kind":"string","required":false},{"name":"allowedRegisterRoles","type":"string[]","kind":"string[]","required":false},{"name":"needVerification","type":"boolean","kind":"boolean","required":true},{"name":"verificationEmailTemplate","type":"string","kind":"string","required":false},{"name":"deactivationEmailTemplate","type":"string","kind":"string","required":false},{"name":"allowInviteUsers","type":"boolean","kind":"boolean","required":true},{"name":"allowDeactivateUsers","type":"boolean","kind":"boolean","required":true},{"name":"inviteUserEmailTemplate","type":"string","kind":"string","required":false},{"name":"invitationExpiration","type":"number","kind":"number","required":false},{"name":"emailVerificationExpiration","type":"number","kind":"number","required":false},{"name":"deactivationExpiration","type":"number","kind":"number","required":false},{"name":"defaultSubscribeToNews","type":"boolean","kind":"boolean","required":true},{"name":"minLength","type":"number","kind":"number","required":true},{"name":"maxLength","type":"number","kind":"number","required":false},{"name":"minNumbers","type":"number","kind":"number","required":false},{"name":"maxNumbers","type":"number","kind":"number","required":false},{"name":"minUpper","type":"number","kind":"number","required":false},{"name":"maxUpper","type":"number","kind":"number","required":false},{"name":"minLower","type":"number","kind":"number","required":false},{"name":"maxLower","type":"number","kind":"number","required":false},{"name":"minSpecial","type":"number","kind":"number","required":false},{"name":"maxSpecial","type":"number","kind":"number","required":false},{"name":"allowedSpecial","type":"string","kind":"string","required":false}]},"updatePasswordComplexity":{"http":"PUT","path":"/{version}/membership/authorization/password-complexity","dto":"UpdatePasswordComplexity","fields":[{"name":"minLength","type":"number","kind":"number","required":true},{"name":"maxLength","type":"number","kind":"number","required":false},{"name":"minNumbers","type":"number","kind":"number","required":false},{"name":"maxNumbers","type":"number","kind":"number","required":false},{"name":"minUpper","type":"number","kind":"number","required":false},{"name":"maxUpper","type":"number","kind":"number","required":false},{"name":"minLower","type":"number","kind":"number","required":false},{"name":"maxLower","type":"number","kind":"number","required":false},{"name":"minSpecial","type":"number","kind":"number","required":false},{"name":"maxSpecial","type":"number","kind":"number","required":false},{"name":"allowedSpecial","type":"string","kind":"string","required":false}]},"getAuthenticationSettings":{"http":"GET","path":"/{version}/membership/authentication","dto":"GetAuthenticationSettings","fields":[]},"updateAuthenticationSettings":{"http":"PUT","path":"/{version}/membership/authentication","dto":"UpdateAuthenticationSettings","fields":[{"name":"logoutUrl","type":"string","kind":"string","required":false},{"name":"allowUsernames","type":"boolean","kind":"boolean","required":true},{"name":"modes","type":"CredentialsSettingsModeDto[]","kind":"json[]","required":false}]},"createContact":{"http":"POST","path":"/{version}/membership/users","dto":"CreateContactRequest","fields":[{"name":"primaryEmail","type":"string","kind":"string","required":false},{"name":"primaryPhone","type":"string","kind":"string","required":false},{"name":"displayName","type":"string","kind":"string","required":false},{"name":"firstName","type":"string","kind":"string","required":false},{"name":"lastName","type":"string","kind":"string","required":false}]},"deleteContact":{"http":"DELETE","path":"/{version}/membership/users/{contactId}","dto":"DeleteContact","fields":[{"name":"contactId","type":"string","kind":"string","required":true}]},"getContact":{"http":"GET","path":"/{version}/membership/users/{contactId}","dto":"GetContact","fields":[{"name":"contactId","type":"string","kind":"string","required":true}]},"getAllContacts":{"http":"GET","path":"/{version}/membership/users","dto":"GetAllContacts","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false}]},"mergeContacts":{"http":"POST","path":"/{version}/membership/users/merge","dto":"MergeContactsRequest","fields":[{"name":"survivorId","type":"string","kind":"string","required":true},{"name":"mergedIds","type":"string[]","kind":"string[]","required":true}]},"updateContact":{"http":"PATCH","path":"/{version}/membership/users/{contactId}","dto":"UpdateContactRequest","fields":[{"name":"contactId","type":"string","kind":"string","required":true},{"name":"displayName","type":"string","kind":"string","required":false},{"name":"firstName","type":"string","kind":"string","required":false},{"name":"lastName","type":"string","kind":"string","required":false},{"name":"fullName","type":"string","kind":"string","required":false},{"name":"company","type":"string","kind":"string","required":false},{"name":"notes","type":"string","kind":"string","required":false},{"name":"gender","type":"string","kind":"string","required":false},{"name":"birthDate","type":"number","kind":"number","required":false},{"name":"timeZone","type":"string","kind":"string","required":false},{"name":"language","type":"string","kind":"string","required":false},{"name":"addressLine1","type":"string","kind":"string","required":false},{"name":"addressLine2","type":"string","kind":"string","required":false},{"name":"country","type":"string","kind":"string","required":false},{"name":"city","type":"string","kind":"string","required":false},{"name":"state","type":"string","kind":"string","required":false},{"name":"postalCode","type":"string","kind":"string","required":false}]},"addContactIdentity":{"http":"POST","path":"/{version}/membership/users/{contactId}/identities","dto":"AddContactIdentityRequest","fields":[{"name":"contactId","type":"string","kind":"string","required":true},{"name":"authId","type":"string","kind":"string","required":true}]},"promoteContactIdentity":{"http":"POST","path":"/{version}/membership/users/{contactId}/identities/{authId}/promote","dto":"PromoteContactIdentityRequest","fields":[{"name":"contactId","type":"string","kind":"string","required":true},{"name":"authId","type":"string","kind":"string","required":true}]},"removeContactIdentity":{"http":"DELETE","path":"/{version}/membership/users/{contactId}/identities/{authId}","dto":"RemoveContactIdentityRequest","fields":[{"name":"contactId","type":"string","kind":"string","required":true},{"name":"authId","type":"string","kind":"string","required":true}]}},"notifications":{"disableEmail":{"http":"GET","path":"/{version}/notifications/email/disable","dto":"DisableEmail","fields":[]},"getEmailDisableDependencies":{"http":"GET","path":"/{version}/notifications/email/disable-dependencies","dto":"GetEmailDisableDependencies","fields":[]},"enableEmail":{"http":"GET","path":"/{version}/notifications/email/enable","dto":"EnableEmail","fields":[]},"saveEmailValidationIntegration":{"http":"POST","path":"/{version}/notifications/email/validation/integrations","dto":"SaveEmailValidationIntegration","fields":[{"name":"integration","type":"EmailValidationIntegrationRequest","kind":"json","required":true}]},"testEmailValidationIntegration":{"http":"POST","path":"/{version}/notifications/email/validation/integrations/test","dto":"TestEmailValidationIntegration","fields":[{"name":"integrationId","type":"string","kind":"string","required":true}]},"attachFileToTemplate":{"http":"POST","path":"/{version}/notifications/email/templates/attachments","dto":"AttachFileToTemplateRequest","fields":[{"name":"language","type":"string","kind":"string","required":false},{"name":"templateId","type":"string","kind":"string","required":true},{"name":"fileRef","type":"FileResourceRefDto","kind":"json","required":true}]},"createEmailTemplate":{"http":"POST","path":"/{version}/notifications/email/templates","dto":"CreateEmailTemplateRequest","fields":[{"name":"templateName","type":"string","kind":"string","required":true},{"name":"description","type":"string","kind":"string","required":false},{"name":"communicationChannel","type":"CommunicationChannel","kind":"string","required":true,"options":["Transactional","Marketing","System"]},{"name":"tags","type":"string[]","kind":"string[]","required":false},{"name":"staticAttachments","type":"FileResourceRefDto[]","kind":"json[]","required":false},{"name":"translations","type":"EmailMessageTranslationDto[]","kind":"json[]","required":true}]},"deleteEmailTemplate":{"http":"DELETE","path":"/{version}/notifications/email/templates/{Id}","dto":"DeleteEmailTemplateRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getEmailTemplate":{"http":"GET","path":"/{version}/notifications/email/templates/{id}","dto":"GetEmailTemplate","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getEmailTemplates":{"http":"GET","path":"/{version}/notifications/email/templates","dto":"GetEmailTemplates","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"showArchived","type":"boolean","kind":"boolean","required":false},{"name":"templateId","type":"string","kind":"string","required":false}]},"getMjml":{"http":"POST","path":"/{version}/notifications/email/templates/mjml","dto":"GetMjml","fields":[{"name":"code","type":"string","kind":"string","required":true},{"name":"tokens","type":"TokenMappingDto[]","kind":"json[]","required":false},{"name":"isForPreview","type":"boolean","kind":"boolean","required":true}]},"getSystemEmailTemplate":{"http":"GET","path":"/{version}/notifications/email/system-templates/{id}","dto":"GetSystemEmailTemplate","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getSystemEmailTemplates":{"http":"GET","path":"/{version}/notifications/email/system-templates","dto":"GetSystemEmailTemplates","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"groupTags","type":"string[]","kind":"string[]","required":false},{"name":"themes","type":"string[]","kind":"string[]","required":false},{"name":"communicationChannel","type":"CommunicationChannel","kind":"string","required":false,"options":["Transactional","Marketing","System"]},{"name":"forTrigger","type":"TriggerType","kind":"string","required":false,"options":["Membership","Schema","Files","Payments"]}]},"getEmailTemplateAvailableTokens":{"http":"GET","path":"/{version}/notifications/email/templates/{id}/tokens","dto":"GetEmailTemplateAvailableTokens","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"updateEmailTemplate":{"http":"PUT","path":"/{version}/notifications/email/templates","dto":"UpdateEmailTemplateRequest","fields":[{"name":"templateName","type":"string","kind":"string","required":true},{"name":"description","type":"string","kind":"string","required":false},{"name":"communicationChannel","type":"CommunicationChannel","kind":"string","required":true,"options":["Transactional","Marketing","System"]},{"name":"tags","type":"string[]","kind":"string[]","required":false},{"name":"staticAttachments","type":"FileResourceRefDto[]","kind":"json[]","required":false},{"name":"translations","type":"EmailMessageTranslationDto[]","kind":"json[]","required":true},{"name":"viewId","type":"string","kind":"string","required":true}]},"deleteEmailSignature":{"http":"DELETE","path":"/{version}/notifications/email/signatures/{id}","dto":"DeleteEmailSignature","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getEmailSignature":{"http":"GET","path":"/{version}/notifications/email/signatures/{id}","dto":"GetEmailSignature","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getEmailSignatures":{"http":"GET","path":"/{version}/notifications/email/signatures","dto":"GetEmailSignatures","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false}]},"saveEmailSignature":{"http":"POST","path":"/{version}/notifications/email/signatures","dto":"SaveEmailSignatureRequest","fields":[{"name":"viewId","type":"string","kind":"string","required":false},{"name":"displayName","type":"string","kind":"string","required":true},{"name":"translations","type":"TranslationDto[]","kind":"json[]","required":true}]},"getEmailSettings":{"http":"GET","path":"/{version}/notifications/email/settings","dto":"GetEmailSettings","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"confirmEmailIntegrationHumanDelivery":{"http":"POST","path":"/{version}/notifications/email/integrations/confirm-human-delivery","dto":"ConfirmEmailIntegrationHumanDeliveryRequest","fields":[{"name":"integrationId","type":"string","kind":"string","required":true}]},"deleteEmailIntegration":{"http":"DELETE","path":"/{version}/notifications/email/integrations/{Id}","dto":"DeleteEmailIntegration","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"disableEmailIntegration":{"http":"PUT","path":"/{version}/notifications/email/integrations/{Id}/disable","dto":"DisableEmailIntegration","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"checkEmailIntegrationDomainHealth":{"http":"POST","path":"/{version}/notifications/email/integrations/domain-health","dto":"CheckEmailIntegrationDomainHealthRequest","fields":[{"name":"integrationId","type":"string","kind":"string","required":true}]},"enableEmailIntegration":{"http":"PUT","path":"/{version}/notifications/email/integrations/{Id}/enable","dto":"EnableEmailIntegration","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getEmailIntegration":{"http":"GET","path":"/{version}/notifications/email/integrations/{id}","dto":"GetEmailIntegration","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getEmailIntegrations":{"http":"GET","path":"/{version}/notifications/email/integrations","dto":"GetEmailIntegrations","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false}]},"saveEmailIntegration":{"http":"POST","path":"/{version}/notifications/email/integrations","dto":"SaveEmailIntegration","fields":[{"name":"integration","type":"EmailIntegrationRequest","kind":"json","required":true}]},"setEmailsIntegrationAsDefault":{"http":"PUT","path":"/{version}/notifications/email/integrations/{Id}/default","dto":"SetEmailsIntegrationAsDefault","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"testEmailIntegration":{"http":"POST","path":"/{version}/notifications/email/integrations/test","dto":"TestEmailIntegration","fields":[{"name":"integrationId","type":"string","kind":"string","required":true},{"name":"to","type":"string","kind":"string","required":true}]},"archiveEmailTemplate":{"http":"PUT","path":"/{version}/notifications/email/templates/{Id}/archive","dto":"ArchiveEmailTemplateRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"cloneEmailTemplate":{"http":"POST","path":"/{version}/notifications/email/templates/{Id}/clone","dto":"CloneEmailTemplateRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"unArchiveEmailTemplate":{"http":"PUT","path":"/{version}/notifications/email/templates/{Id}/unarchive","dto":"UnArchiveEmailTemplateRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"deleteEmailFooter":{"http":"DELETE","path":"/{version}/notifications/email/footers/{id}","dto":"DeleteEmailFooter","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getEmailFooter":{"http":"GET","path":"/{version}/notifications/email/footers/{id}","dto":"GetEmailFooter","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getEmailFooters":{"http":"GET","path":"/{version}/notifications/email/footers","dto":"GetEmailFooters","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false}]},"saveEmailFooter":{"http":"POST","path":"/{version}/notifications/email/footers","dto":"SaveEmailFooterRequest","fields":[{"name":"viewId","type":"string","kind":"string","required":false},{"name":"displayName","type":"string","kind":"string","required":true},{"name":"translations","type":"TranslationDto[]","kind":"json[]","required":true}]},"createEmailCampaign":{"http":"POST","path":"/{version}/notifications/email/campaigns","dto":"CreateEmailCampaignRequest","fields":[{"name":"campaign","type":"EmailCampaignRequest","kind":"json","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"deleteEmailCampaign":{"http":"DELETE","path":"/{version}/notifications/email/campaigns/{Id}","dto":"DeleteEmailCampaignRequest","fields":[]},"getEmailCampaign":{"http":"GET","path":"/{version}/notifications/email/campaigns/{id}","dto":"GetEmailCampaign","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getEmailCampaigns":{"http":"GET","path":"/{version}/notifications/email/campaigns","dto":"GetEmailCampaigns","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"campaignId","type":"string","kind":"string","required":false},{"name":"emailAddress","type":"string","kind":"string","required":false},{"name":"templateId","type":"string","kind":"string","required":false},{"name":"from","type":"number","kind":"number","required":false},{"name":"to","type":"number","kind":"number","required":false}]},"getEmailCampaignBatches":{"http":"GET","path":"/{version}/notifications/email/campaigns/{id}/batches","dto":"GetEmailCampaignBatches","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"batchId","type":"string","kind":"string","required":false},{"name":"emailAddress","type":"string","kind":"string","required":false}]},"getEmailCampaignBatchNotification":{"http":"GET","path":"/{version}/notifications/email/campaigns/{id}/batches/{batchId}/{notificationId}","dto":"GetEmailCampaignBatchNotification","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"id","type":"string","kind":"string","required":true},{"name":"batchId","type":"string","kind":"string","required":true},{"name":"notificationId","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getEmailCampaignBatchNotifications":{"http":"GET","path":"/{version}/notifications/email/campaigns/{id}/batches/{batchId}","dto":"GetEmailCampaignBatchNotifications","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"id","type":"string","kind":"string","required":true},{"name":"batchId","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getEmailCampaignStatistics":{"http":"GET","path":"/{version}/notifications/email/campaigns/{id}/stats","dto":"GetEmailCampaignStatistics","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"previewEmailNotification":{"http":"GET","path":"/{version}/notifications/email/preview","dto":"PreviewEmailNotification","fields":[{"name":"hash","type":"string","kind":"string","required":true}]},"stopEmailCampaign":{"http":"POST","path":"/{version}/notifications/email/campaigns/{Id}/stop","dto":"StopEmailCampaignRequest","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getEmailCampaignMessage":{"http":"GET","path":"/{version}/notifications/emails/campaigns/{campaignId}/messages/{notificationId}","dto":"GetEmailCampaignMessage","fields":[{"name":"campaignId","type":"string","kind":"string","required":true},{"name":"campaignBatchId","type":"string","kind":"string","required":true},{"name":"notificationId","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getEmailCampaignMessages":{"http":"GET","path":"/{version}/notifications/emails/campaigns/{campaignId}/messages","dto":"GetEmailCampaignMessagesRequest","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"campaignId","type":"string","kind":"string","required":true},{"name":"campaignBatchId","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"disableSms":{"http":"GET","path":"/{version}/notifications/sms/disable","dto":"DisableSms","fields":[]},"getSmsDisableDependencies":{"http":"GET","path":"/{version}/notifications/sms/disable-dependencies","dto":"GetSmsDisableDependencies","fields":[]},"enableSms":{"http":"GET","path":"/{version}/notifications/sms/enable","dto":"EnableSms","fields":[]},"archiveSmsTemplate":{"http":"PUT","path":"/{version}/notifications/sms/templates/{Id}/archive","dto":"ArchiveSmsTemplateRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"cloneSmsTemplate":{"http":"POST","path":"/{version}/notifications/sms/templates/{Id}/clone","dto":"CloneSmsTemplateRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"createSmsTemplate":{"http":"POST","path":"/{version}/notifications/sms/templates","dto":"CreateSmsTemplateRequest","fields":[{"name":"templateName","type":"string","kind":"string","required":true},{"name":"description","type":"string","kind":"string","required":false},{"name":"communicationChannel","type":"CommunicationChannel","kind":"string","required":true,"options":["Transactional","Marketing","System"]},{"name":"tags","type":"string[]","kind":"string[]","required":false},{"name":"translations","type":"SmsMessageTranslationDto[]","kind":"json[]","required":true}]},"deleteSmsTemplate":{"http":"DELETE","path":"/{version}/notifications/sms/templates/{Id}","dto":"DeleteSmsTemplateRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getSmsTemplate":{"http":"GET","path":"/{version}/notifications/sms/templates/{id}","dto":"GetSmsTemplate","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getSmsTemplates":{"http":"GET","path":"/{version}/notifications/sms/templates","dto":"GetSmsTemplates","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"showArchived","type":"boolean","kind":"boolean","required":false},{"name":"templateId","type":"string","kind":"string","required":false}]},"getSmsMessageContentTokens":{"http":"GET","path":"/{version}/notifications/sms/templates/{id}/tokens","dto":"GetSmsMessageContentTokens","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"renderSms":{"http":"POST","path":"/{version}/notifications/sms/templates/render","dto":"RenderSms","fields":[{"name":"code","type":"string","kind":"string","required":true},{"name":"tokens","type":"TokenMappingDto[]","kind":"json[]","required":false},{"name":"isForPreview","type":"boolean","kind":"boolean","required":true}]},"unArchiveSmsTemplate":{"http":"PUT","path":"/{version}/notifications/sms/templates/{Id}/unarchive","dto":"UnArchiveSmsTemplateRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"updateSmsTemplate":{"http":"PUT","path":"/{version}/notifications/sms/templates","dto":"UpdateSmsTemplateRequest","fields":[{"name":"templateName","type":"string","kind":"string","required":true},{"name":"description","type":"string","kind":"string","required":false},{"name":"communicationChannel","type":"CommunicationChannel","kind":"string","required":true,"options":["Transactional","Marketing","System"]},{"name":"tags","type":"string[]","kind":"string[]","required":false},{"name":"translations","type":"SmsMessageTranslationDto[]","kind":"json[]","required":true},{"name":"viewId","type":"string","kind":"string","required":true}]},"getSmsSettings":{"http":"GET","path":"/{version}/notifications/sms/settings","dto":"GetSmsSettings","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"confirmSmsIntegrationHumanDelivery":{"http":"POST","path":"/{version}/notifications/sms/integrations/confirm-human-delivery","dto":"ConfirmSmsIntegrationHumanDeliveryRequest","fields":[{"name":"integrationId","type":"string","kind":"string","required":true}]},"deleteSmsIntegration":{"http":"DELETE","path":"/{version}/notifications/sms/integrations/{Id}","dto":"DeleteSmsIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"disableSmsIntegration":{"http":"PUT","path":"/{version}/notifications/sms/integrations/{Id}/disable","dto":"DisableSmsIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"enableSmsIntegration":{"http":"PUT","path":"/{version}/notifications/sms/integrations/{Id}/enable","dto":"EnableSmsIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getSmsIntegration":{"http":"GET","path":"/{version}/notifications/sms/integrations/{id}","dto":"GetSmsIntegration","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getSmsIntegrations":{"http":"GET","path":"/{version}/notifications/sms/integrations","dto":"GetSmsIntegrations","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false}]},"saveSmsIntegration":{"http":"POST","path":"/{version}/notifications/sms/integrations","dto":"SaveSmsIntegration","fields":[{"name":"integration","type":"SmsIntegrationRequest","kind":"json","required":true}]},"setSmsIntegrationAsDefault":{"http":"PUT","path":"/{version}/notifications/sms/integrations/{Id}/default","dto":"SetSmsIntegrationAsDefaultRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"testSmsIntegration":{"http":"POST","path":"/{version}/notifications/sms/integrations/test","dto":"TestSmsIntegration","fields":[{"name":"integrationId","type":"string","kind":"string","required":true},{"name":"to","type":"string","kind":"string","required":false}]},"createSmsCampaign":{"http":"POST","path":"/{version}/notifications/sms/campaigns","dto":"CreateSmsCampaignRequest","fields":[{"name":"templateId","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"language","type":"string","kind":"string","required":false},{"name":"initiatorId","type":"string","kind":"string","required":false},{"name":"deliveryType","type":"SmsCampaignRecipientsSourceTypes","kind":"string","required":true,"options":["AllUsers","SpecifiedUsers","AccountUsers","PhoneNumbers","Collection"]},{"name":"allUsers","type":"SmsToAllUsersDeliverySettingsDto","kind":"json","required":false},{"name":"specifiedUsers","type":"SmsToUsersDeliverySettingsDto","kind":"json","required":false},{"name":"collection","type":"SmsToCollectionRecordsDeliverySettingsDto","kind":"json","required":false},{"name":"phoneNumbers","type":"SmsToPhoneNumbersDeliverySettingsDto","kind":"json","required":false}]},"deleteSmsCampaign":{"http":"DELETE","path":"/{version}/notifications/sms/campaigns/{id}","dto":"DeleteSmsCampaign","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getSmsCampaign":{"http":"GET","path":"/{version}/notifications/sms/campaigns/{id}","dto":"GetSmsCampaign","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getSmsCampaigns":{"http":"GET","path":"/{version}/notifications/sms/campaigns","dto":"GetSmsCampaigns","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"templateId","type":"string","kind":"string","required":false},{"name":"from","type":"number","kind":"number","required":false},{"name":"to","type":"number","kind":"number","required":false}]},"getSmsCampaignBatches":{"http":"GET","path":"/{version}/notifications/sms/campaigns/{id}/batches","dto":"GetSmsCampaignBatches","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"id","type":"string","kind":"string","required":false},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getSmsCampaignBatchNotification":{"http":"GET","path":"/{version}/notifications/sms/campaigns/{id}/batches/{batchId}/{notificationId}","dto":"GetSmsCampaignBatchNotification","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"id","type":"string","kind":"string","required":true},{"name":"batchId","type":"string","kind":"string","required":true},{"name":"notificationId","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getSmsCampaignBatchNotifications":{"http":"GET","path":"/{version}/notifications/sms/campaigns/{id}/batches/{batchId}","dto":"GetSmsCampaignBatchNotifications","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"id","type":"string","kind":"string","required":true},{"name":"batchId","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getSmsCampaignStatistics":{"http":"GET","path":"/{version}/notifications/sms/campaigns/{id}/stats","dto":"GetSmsCampaignStatistics","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"previewSmsNotification":{"http":"GET","path":"/{version}/notifications/sms/preview","dto":"PreviewSmsNotification","fields":[{"name":"hash","type":"string","kind":"string","required":true}]},"stopSmsCampaign":{"http":"POST","path":"/{version}/notifications/sms/campaigns/{Id}/stop","dto":"StopSmsCampaignRequest","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getSmsCampaignMessage":{"http":"GET","path":"/{version}/notifications/sms/campaigns/{campaignId}/messages/{notificationId}","dto":"GetSmsCampaignMessage","fields":[{"name":"campaignId","type":"string","kind":"string","required":true},{"name":"campaignBatchId","type":"string","kind":"string","required":true},{"name":"notificationId","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getSmsCampaignMessages":{"http":"GET","path":"/{version}/notifications/sms/campaigns/{campaignId}/messages","dto":"GetSmsCampaignMessagesRequest","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"campaignId","type":"string","kind":"string","required":true},{"name":"campaignBatchId","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"disablePush":{"http":"GET","path":"/{version}/notifications/push/disable","dto":"DisablePush","fields":[]},"getPushDisableDependencies":{"http":"GET","path":"/{version}/notifications/push/disable-dependencies","dto":"GetPushDisableDependencies","fields":[]},"enablePush":{"http":"GET","path":"/{version}/notifications/push/enable","dto":"EnablePush","fields":[]},"archivePushTemplate":{"http":"PUT","path":"/{version}/notifications/push/templates/{Id}/archive","dto":"ArchivePushTemplateRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"clonePushTemplate":{"http":"POST","path":"/{version}/notifications/push/templates/{Id}/clone","dto":"ClonePushTemplateRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"createPushTemplate":{"http":"POST","path":"/{version}/notifications/push/templates","dto":"CreatePushTemplateRequest","fields":[{"name":"templateName","type":"string","kind":"string","required":true},{"name":"description","type":"string","kind":"string","required":false},{"name":"communicationChannel","type":"CommunicationChannel","kind":"string","required":true,"options":["Transactional","Marketing","System"]},{"name":"tags","type":"string[]","kind":"string[]","required":false},{"name":"translations","type":"PushMessageTranslationDto[]","kind":"json[]","required":true}]},"deletePushTemplate":{"http":"DELETE","path":"/{version}/notifications/push/templates/{Id}","dto":"DeletePushTemplateRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getPushTemplate":{"http":"GET","path":"/{version}/notifications/push/templates/{id}","dto":"GetPushTemplate","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getPushTemplates":{"http":"GET","path":"/{version}/notifications/push/templates","dto":"GetPushTemplates","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"showArchived","type":"boolean","kind":"boolean","required":false},{"name":"templateId","type":"string","kind":"string","required":false}]},"getPushMessageContentTokens":{"http":"GET","path":"/{version}/notifications/push/templates/{id}/tokens","dto":"GetPushMessageContentTokens","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"renderPush":{"http":"POST","path":"/{version}/notifications/push/templates/render","dto":"RenderPush","fields":[{"name":"code","type":"string","kind":"string","required":true},{"name":"tokens","type":"TokenMappingDto[]","kind":"json[]","required":false},{"name":"isForPreview","type":"boolean","kind":"boolean","required":true}]},"unArchivePushTemplate":{"http":"PUT","path":"/{version}/notifications/push/templates/{Id}/unarchive","dto":"UnArchivePushTemplateRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"updatePushTemplate":{"http":"PUT","path":"/{version}/notifications/push/templates","dto":"UpdatePushTemplateRequest","fields":[{"name":"templateName","type":"string","kind":"string","required":true},{"name":"description","type":"string","kind":"string","required":false},{"name":"communicationChannel","type":"CommunicationChannel","kind":"string","required":true,"options":["Transactional","Marketing","System"]},{"name":"tags","type":"string[]","kind":"string[]","required":false},{"name":"translations","type":"PushMessageTranslationDto[]","kind":"json[]","required":true},{"name":"viewId","type":"string","kind":"string","required":true}]},"getPushSettings":{"http":"GET","path":"/{version}/notifications/push/settings","dto":"GetPushSettings","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"confirmPushIntegrationHumanDelivery":{"http":"POST","path":"/{version}/notifications/push/integrations/confirm-human-delivery","dto":"ConfirmPushIntegrationHumanDeliveryRequest","fields":[{"name":"integrationId","type":"string","kind":"string","required":true}]},"deletePushIntegration":{"http":"DELETE","path":"/{version}/notifications/push/integrations/{Id}","dto":"DeletePushIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"disablePushIntegration":{"http":"PUT","path":"/{version}/notifications/push/integrations/{Id}/disable","dto":"DisablePushIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"enablePushIntegration":{"http":"PUT","path":"/{version}/notifications/push/integrations/{Id}/enable","dto":"EnablePushIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getPushIntegration":{"http":"GET","path":"/{version}/notifications/push/integrations/{id}","dto":"GetPushIntegration","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getPushIntegrations":{"http":"GET","path":"/{version}/notifications/push/integrations","dto":"GetPushIntegrations","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false}]},"savePushIntegration":{"http":"POST","path":"/{version}/notifications/push/integrations","dto":"SavePushIntegration","fields":[{"name":"integration","type":"PushIntegrationRequest","kind":"json","required":true}]},"setPushIntegrationAsDefault":{"http":"PUT","path":"/{version}/notifications/push/integrations/{Id}/default","dto":"SetPushIntegrationAsDefaultRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"testPushIntegration":{"http":"POST","path":"/{version}/notifications/push/integrations/test","dto":"TestPushIntegration","fields":[{"name":"integrationId","type":"string","kind":"string","required":true},{"name":"testToken","type":"string","kind":"string","required":false},{"name":"deliveryFamily","type":"string","kind":"string","required":false}]},"registerDevice":{"http":"POST","path":"/{version}/notifications/push/devices","dto":"RegisterDevice","fields":[{"name":"pushDeviceDto","type":"PushDeviceDto","kind":"json","required":true},{"name":"userId","type":"string","kind":"string","required":true},{"name":"accountId","type":"string","kind":"string","required":false},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getPushDevice":{"http":"GET","path":"/{version}/notifications/push/devices/{id}","dto":"GetPushDevice","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"createPushCampaign":{"http":"POST","path":"/{version}/notifications/push/campaigns","dto":"CreatePushCampaignRequest","fields":[{"name":"campaign","type":"PushCampaignRequest","kind":"json","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"deletePushCampaign":{"http":"DELETE","path":"/{version}/notifications/push/campaigns/{Id}","dto":"DeletePushCampaignRequest","fields":[]},"getPushCampaign":{"http":"GET","path":"/{version}/notifications/push/campaigns/{id}","dto":"GetPushCampaign","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getPushCampaigns":{"http":"GET","path":"/{version}/notifications/push/campaigns","dto":"GetPushCampaigns","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"templateId","type":"string","kind":"string","required":false},{"name":"from","type":"number","kind":"number","required":false},{"name":"to","type":"number","kind":"number","required":false}]},"getPushCampaignBatches":{"http":"GET","path":"/{version}/notifications/push/campaigns/{id}/batches","dto":"GetPushCampaignBatches","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false},{"name":"batchId","type":"string","kind":"string","required":false}]},"getPushCampaignBatchNotification":{"http":"GET","path":"/{version}/notifications/push/campaigns/{id}/batches/{batchId}/{notificationId}","dto":"GetPushCampaignBatchNotification","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"id","type":"string","kind":"string","required":true},{"name":"batchId","type":"string","kind":"string","required":true},{"name":"notificationId","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getPushCampaignBatchNotifications":{"http":"GET","path":"/{version}/notifications/push/campaigns/{id}/batches/{batchId}","dto":"GetPushCampaignBatchNotifications","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"id","type":"string","kind":"string","required":true},{"name":"batchId","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getPushCampaignStatistics":{"http":"GET","path":"/{version}/notifications/push/campaigns/{id}/stats","dto":"GetPushCampaignStatistics","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"previewPushNotification":{"http":"GET","path":"/{version}/notifications/push/preview","dto":"PreviewPushNotification","fields":[{"name":"hash","type":"string","kind":"string","required":true}]},"stopPushCampaign":{"http":"POST","path":"/{version}/notifications/push/campaigns/{Id}/stop","dto":"StopPushCampaignRequest","fields":[]},"getPushCampaignMessage":{"http":"GET","path":"/{version}/notifications/push/campaigns/{campaignId}/messages/{notificationId}","dto":"GetPushCampaignMessage","fields":[{"name":"campaignId","type":"string","kind":"string","required":true},{"name":"campaignBatchId","type":"string","kind":"string","required":true},{"name":"notificationId","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getPushCampaignMessages":{"http":"GET","path":"/{version}/notifications/push/campaigns/{campaignId}/messages","dto":"GetPushCampaignMessagesRequest","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"campaignId","type":"string","kind":"string","required":true},{"name":"campaignBatchId","type":"string","kind":"string","required":true},{"name":"databaseIntegrationId","type":"string","kind":"string","required":false}]},"getUserNotificationPreferences":{"http":"GET","path":"/{version}/notifications/user/preferences","dto":"GetUserNotificationPreferences","fields":[{"name":"userId","type":"string","kind":"string","required":true}]},"updateUserNotificationsPreferences":{"http":"PUT","path":"/{version}/notifications/user/preferences","dto":"UpdateUserNotificationsPreferences","fields":[{"name":"userId","type":"string","kind":"string","required":true},{"name":"blockAllMarketingMessages","type":"boolean","kind":"boolean","required":true}]},"grantContactConsent":{"http":"POST","path":"/{version}/notifications/contacts/{contactId}/marketing-state/{channel}/consent","dto":"GrantContactConsentRequest","fields":[{"name":"contactId","type":"string","kind":"string","required":true},{"name":"channel","type":"string","kind":"string","required":true},{"name":"lawfulBasis","type":"string","kind":"string","required":true},{"name":"source","type":"string","kind":"string","required":true},{"name":"evidenceRef","type":"string","kind":"string","required":false}]},"unsubscribeContact":{"http":"POST","path":"/{version}/notifications/contacts/{contactId}/marketing-state/{channel}/unsubscribe","dto":"UnsubscribeContactRequest","fields":[{"name":"contactId","type":"string","kind":"string","required":true},{"name":"channel","type":"string","kind":"string","required":true},{"name":"reason","type":"string","kind":"string","required":false}]}},"payments":{"disablePayments":{"http":"GET","path":"/{version}/payments/disable","dto":"DisablePayments","fields":[]},"enablePayments":{"http":"GET","path":"/{version}/payments/enable","dto":"EnablePayments","fields":[]},"getPaymentsWebhookLog":{"http":"GET","path":"/{version}/payments/webhooks/log","dto":"GetPaymentsWebhookLog","fields":[{"name":"integrationId","type":"string","kind":"string","required":false},{"name":"limit","type":"number","kind":"number","required":false}]},"deletePaymentsTrigger":{"http":"DELETE","path":"/{version}/payments/triggers/{triggerId}","dto":"DeletePaymentsTrigger","fields":[{"name":"triggerId","type":"string","kind":"string","required":true},{"name":"triggerType","type":"TriggerType","kind":"string","required":true,"options":["Membership","Schema","Files","Payments"]},{"name":"schemaId","type":"string","kind":"string","required":false}]},"disablePaymentsTrigger":{"http":"PATCH","path":"/{version}/payments/triggers/{triggerId}/disable","dto":"DisablePaymentsTrigger","fields":[{"name":"triggerId","type":"string","kind":"string","required":true},{"name":"triggerType","type":"TriggerType","kind":"string","required":true,"options":["Membership","Schema","Files","Payments"]},{"name":"schemaId","type":"string","kind":"string","required":false}]},"enablePaymentsTrigger":{"http":"PATCH","path":"/{version}/payments/triggers/{triggerId}/enable","dto":"EnablePaymentsTrigger","fields":[{"name":"triggerId","type":"string","kind":"string","required":true},{"name":"triggerType","type":"TriggerType","kind":"string","required":true,"options":["Membership","Schema","Files","Payments"]},{"name":"schemaId","type":"string","kind":"string","required":false}]},"getPaymentsTrigger":{"http":"GET","path":"/{version}/payments/triggers/{id}","dto":"GetPaymentsTrigger","fields":[{"name":"id","type":"string","kind":"string","required":true},{"name":"schemaId","type":"string","kind":"string","required":false}]},"getPaymentsTriggers":{"http":"GET","path":"/{version}/payments/triggers","dto":"GetPaymentsTriggers","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"schemaId","type":"string","kind":"string","required":false}]},"savePaymentsTrigger":{"http":"POST","path":"/{version}/payments/triggers","dto":"SavePaymentsTrigger","fields":[{"name":"trigger","type":"SaveTriggerRequest","kind":"json","required":true}]},"confirmPaymentsIntegrationHumanDelivery":{"http":"POST","path":"/{version}/payments/integrations/confirm-human-delivery","dto":"ConfirmPaymentsIntegrationHumanDeliveryRequest","fields":[{"name":"integrationId","type":"string","kind":"string","required":true}]},"deletePaymentsIntegration":{"http":"DELETE","path":"/{version}/payments/integrations/{Id}","dto":"DeletePaymentsIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"disablePaymentsIntegration":{"http":"PUT","path":"/{version}/payments/integrations/{Id}/disable","dto":"DisablePaymentsIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"enablePaymentsIntegration":{"http":"PUT","path":"/{version}/payments/integrations/{Id}/enable","dto":"EnablePaymentsIntegrationRequest","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getPaymentsIntegration":{"http":"GET","path":"/{version}/payments/integrations/{id}","dto":"GetPaymentsIntegration","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getPaymentsIntegrations":{"http":"GET","path":"/{version}/payments/integrations","dto":"GetPaymentsIntegrations","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false}]},"savePaymentsIntegration":{"http":"POST","path":"/{version}/payments/integrations","dto":"SavePaymentsIntegration","fields":[{"name":"integration","type":"PaymentIntegrationRequest","kind":"json","required":true}]},"testPaymentsIntegration":{"http":"POST","path":"/{version}/payments/integrations/test","dto":"TestPaymentsIntegration","fields":[{"name":"integrationId","type":"string","kind":"string","required":true}]}},"projects":{"getProjectBrief":{"http":"GET","path":"/{version}/projects/{projectId}/ai/brief","dto":"GetProjectBriefRequest","fields":[{"name":"sinceSeq","type":"number","kind":"number","required":false}]},"getWorkItems":{"http":"GET","path":"/{version}/projects/{projectId}/ai/work-items","dto":"GetWorkItemsRequest","fields":[{"name":"status","type":"string","kind":"string","required":false}]},"getWorkItem":{"http":"GET","path":"/{version}/projects/{projectId}/ai/work-items/{WorkItemId}","dto":"GetWorkItemRequest","fields":[{"name":"workItemId","type":"string","kind":"string","required":true}]},"exportWorkItem":{"http":"GET","path":"/{version}/projects/{projectId}/ai/work-items/{WorkItemId}/export.md","dto":"ExportWorkItemRequest","fields":[{"name":"workItemId","type":"string","kind":"string","required":true}]},"markNeedsYouDone":{"http":"POST","path":"/{version}/projects/{projectId}/ai/work-items/{WorkItemId}/needs-you/{Index}/done","dto":"MarkNeedsYouDoneRequest","fields":[{"name":"workItemId","type":"string","kind":"string","required":true},{"name":"index","type":"number","kind":"number","required":true},{"name":"done","type":"boolean","kind":"boolean","required":false}]}},"public":{"getPublicProjectConfig":{"http":"GET","path":"/{version}/public/projects/{ProjectId}/config","dto":"GetPublicProjectConfig","fields":[]},"getPublicProjectLegal":{"http":"GET","path":"/{version}/public/projects/{ProjectId}/legal/{Kind}","dto":"GetPublicProjectLegal","fields":[{"name":"kind","type":"string","kind":"string","required":false}]}},"regions":{},"resources":{"resolveResources":{"http":"POST","path":"/{version}/resources/resolve","dto":"ResolveResources","fields":[{"name":"refs","type":"IReadOnlyList","kind":"json","required":true}]}},"scheduler":{"disableScheduler":{"http":"GET","path":"/{version}/scheduler/disable","dto":"DisableScheduler","fields":[]},"enableScheduler":{"http":"GET","path":"/{version}/scheduler/enable","dto":"EnableScheduler","fields":[]},"deleteSchedulerTask":{"http":"DELETE","path":"/{version}/scheduler/tasks/{Id}","dto":"DeleteSchedulerTask","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"disableSchedulerTask":{"http":"PUT","path":"/{version}/scheduler/tasks/{Id}/disable","dto":"DisableSchedulerTask","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"enableSchedulerTask":{"http":"PUT","path":"/{version}/scheduler/tasks/{Id}/enable","dto":"EnableSchedulerTask","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getSchedulerTask":{"http":"GET","path":"/{version}/scheduler/tasks/{id}","dto":"GetSchedulerTask","fields":[{"name":"id","type":"string","kind":"string","required":true}]},"getSchedulerTasks":{"http":"GET","path":"/{version}/scheduler/tasks","dto":"GetSchedulerTasks","fields":[{"name":"startingAfter","type":"string","kind":"string","required":false},{"name":"endingBefore","type":"string","kind":"string","required":false},{"name":"pageSize","type":"number","kind":"number","required":false},{"name":"paging","type":"PagingArgs","kind":"json","required":false},{"name":"type","type":"SchedulerTaskType","kind":"string","required":false,"options":["EmailCampaign","PushCampaign","SmsCampaign","CodeFunctionalCall","WebhookCall"]},{"name":"enabled","type":"boolean","kind":"boolean","required":false}]},"saveSchedulerTask":{"http":"POST","path":"/{version}/scheduler/tasks","dto":"SaveSchedulerTaskRequest","fields":[{"name":"taskId","type":"string","kind":"string","required":false},{"name":"name","type":"string","kind":"string","required":true},{"name":"description","type":"string","kind":"string","required":false},{"name":"cron","type":"string","kind":"string","required":true},{"name":"initiatorUserId","type":"string","kind":"string","required":true},{"name":"isEnabled","type":"boolean","kind":"boolean","required":true},{"name":"stopOnError","type":"boolean","kind":"boolean","required":true},{"name":"task","type":"SchedulerTaskRequest","kind":"json","required":true}]}},"support":{"closeSupportCase":{"http":"POST","path":"/{version}/support/cases/{CaseId}/close","dto":"CloseSupportCaseRequest","fields":[{"name":"caseId","type":"string","kind":"string","required":true}]},"reopenSupportCase":{"http":"POST","path":"/{version}/support/cases/{CaseId}/reopen","dto":"ReopenSupportCaseRequest","fields":[{"name":"caseId","type":"string","kind":"string","required":true},{"name":"reason","type":"string","kind":"string","required":true}]},"resolveSupportCase":{"http":"POST","path":"/{version}/support/cases/{CaseId}/resolve","dto":"ResolveSupportCaseRequest","fields":[{"name":"caseId","type":"string","kind":"string","required":true},{"name":"resolution","type":"CaseResolutionDto","kind":"json","required":false}]},"appendSupportCaseMessage":{"http":"POST","path":"/{version}/support/cases/{CaseId}/messages","dto":"AppendSupportCaseMessageRequest","fields":[{"name":"caseId","type":"string","kind":"string","required":true},{"name":"message","type":"string","kind":"string","required":true}]},"getSupportCase":{"http":"GET","path":"/{version}/support/cases/{CaseId}","dto":"GetSupportCase","fields":[{"name":"caseId","type":"string","kind":"string","required":true}]},"getSupportCases":{"http":"GET","path":"/{version}/support/cases","dto":"GetSupportCases","fields":[{"name":"pagingArgs","type":"PagingArgs","kind":"json","required":false}]},"openSupportCase":{"http":"POST","path":"/{version}/support/cases","dto":"OpenSupportCaseRequest","fields":[{"name":"kind","type":"string","kind":"string","required":true},{"name":"severity","type":"string","kind":"string","required":true},{"name":"subject","type":"string","kind":"string","required":true},{"name":"message","type":"string","kind":"string","required":true}]}},"webhooks":{"getWebhookIntegration":{"http":"GET","path":"/{version}/webhooks/integration","dto":"GetWebhookIntegration","fields":[]},"revealWebhookIntegrationSecret":{"http":"GET","path":"/{version}/webhooks/integration/secret","dto":"RevealWebhookIntegrationSecretRequest","fields":[]},"rotateWebhookIntegrationSecret":{"http":"POST","path":"/{version}/webhooks/integration/secret/rotate","dto":"RotateWebhookIntegrationSecretRequest","fields":[]},"updateWebhookIntegrationExtraHeaders":{"http":"PUT","path":"/{version}/webhooks/integration/extra-headers","dto":"UpdateWebhookIntegrationExtraHeadersRequest","fields":[]},"receiveWebhook":{"http":"POST","path":"/{version}/webhooks/{source}/{integrationInstanceId}","dto":"ReceiveWebhook","fields":[{"name":"source","type":"string","kind":"string","required":true},{"name":"integrationInstanceId","type":"string","kind":"string","required":true},{"name":"requestStream","type":"string","kind":"string","required":true}]},"disableWebhookDestination":{"http":"PUT","path":"/{version}/webhooks/destinations/{DestinationId}/disable","dto":"DisableWebhookDestinationRequest","fields":[{"name":"destinationId","type":"string","kind":"string","required":true}]},"enableWebhookDestination":{"http":"PUT","path":"/{version}/webhooks/destinations/{DestinationId}/enable","dto":"EnableWebhookDestinationRequest","fields":[{"name":"destinationId","type":"string","kind":"string","required":true}]},"removeWebhookDestination":{"http":"DELETE","path":"/{version}/webhooks/destinations/{DestinationId}","dto":"RemoveWebhookDestinationRequest","fields":[{"name":"destinationId","type":"string","kind":"string","required":true}]},"saveWebhookDestination":{"http":"POST","path":"/{version}/webhooks/destinations","dto":"SaveWebhookDestinationRequest","fields":[{"name":"destinationId","type":"string","kind":"string","required":false},{"name":"destinationName","type":"string","kind":"string","required":true},{"name":"endpointUrl","type":"string","kind":"string","required":true},{"name":"selectedEvents","type":"string[]","kind":"string[]","required":true}]}}}} diff --git a/src/lib/dispatch.ts b/src/lib/dispatch.ts index 9b7be67..71079c4 100644 --- a/src/lib/dispatch.ts +++ b/src/lib/dispatch.ts @@ -10,21 +10,48 @@ * The SDK namespaces are auto-generated and uniform (every method takes one * request object), so the CLI can discover modules and methods at runtime — * every current AND future SDK method is callable without new command files. + * + * Values are typed in two steps: the raw tokens are collected first (the + * method is not known yet), then `typeFields` applies the request-field map + * of the matched method — a `string` field keeps "0042", a `boolean` flag + * does not swallow the word after it. An explicit type wins over both: + * `--field:str 0042`, `--field:num 7`, `--field:bool false`, `--field:json '{...}'`. + * `--body ''` replaces the field flags with one object. + * + * No oclif import here on purpose. */ +import type {FieldKind} from './request-fields.js' + +export type ExplicitType = 'str' | 'num' | 'bool' | 'json' + +export interface RawField { + name: string + /** Undefined when the flag stood alone (`--archived`). */ + value?: string + /** True when the value was the next argv token (not `--name=value`). */ + fromNextToken: boolean + /** `--name:str value` — explicit type, wins over the field map. */ + explicit?: ExplicitType +} + export interface ParsedInvocation { words: string[] positionals: string[] + rawFields: RawField[] + /** Fields typed by the heuristic only — call `typeFields` once the method is known. */ fields: Record yes: boolean dryRun: boolean } +const EXPLICIT_TYPES = new Set(['str', 'num', 'bool', 'json']) + /** Split leftover argv into words, positional values and --field values. */ export function parseArgv(argv: string[]): ParsedInvocation { const words: string[] = [] const positionals: string[] = [] - const fields: Record = {} + const rawFields: RawField[] = [] let yes = false let dryRun = false @@ -37,16 +64,17 @@ export function parseArgv(argv: string[]): ParsedInvocation { } else if (token.startsWith('--')) { const body = token.slice(2) const eq = body.indexOf('=') + const head = eq > 0 ? body.slice(0, eq) : body + const {name, explicit} = splitType(head) if (eq > 0) { - // --field=value syntax - fields[body.slice(0, eq)] = coerce(body.slice(eq + 1)) + rawFields.push({name, value: body.slice(eq + 1), fromNextToken: false, explicit}) } else { const next = argv[i + 1] if (next !== undefined && !next.startsWith('--')) { - fields[body] = coerce(next) + rawFields.push({name, value: next, fromNextToken: true, explicit}) i++ } else { - fields[body] = true + rawFields.push({name, fromNextToken: false, explicit}) } } } else if (/^[a-z][a-z-]*$/i.test(token) && !looksLikeId(token)) { @@ -56,7 +84,111 @@ export function parseArgv(argv: string[]): ParsedInvocation { } } - return {words, positionals, fields, yes, dryRun} + return {words, positionals, rawFields, fields: typeFields(rawFields, {}).fields, yes, dryRun} +} + +/** `name:str` → {name, explicit: 'str'}; a plain name passes through. */ +function splitType(head: string): {name: string; explicit?: ExplicitType} { + const colon = head.lastIndexOf(':') + if (colon > 0) { + const suffix = head.slice(colon + 1) + if (EXPLICIT_TYPES.has(suffix)) return {name: head.slice(0, colon), explicit: suffix as ExplicitType} + } + + return {name: head} +} + +/** + * Apply the request-field kinds of the matched method. A known boolean flag + * that took the next token gives it back as a positional. Unknown fields + * fall back to the old heuristic (true/false/integers are converted). + */ +export function typeFields( + rawFields: RawField[], + kinds: Record, +): {fields: Record; returned: string[]; errors: string[]} { + const fields: Record = {} + const returned: string[] = [] + const errors: string[] = [] + + for (const raw of rawFields) { + const kind = kinds[raw.name] + if (raw.explicit) { + if (raw.value === undefined) { + if (raw.explicit === 'bool') fields[raw.name] = true + else errors.push(`--${raw.name}:${raw.explicit} needs a value`) + continue + } + + const typed = convert(raw.value, raw.explicit, `--${raw.name}:${raw.explicit}`) + if (typed.error) errors.push(typed.error) + else fields[raw.name] = typed.value + continue + } + + if (kind === 'boolean') { + if (raw.value === undefined || !raw.fromNextToken) { + fields[raw.name] = raw.value === undefined ? true : raw.value !== 'false' + } else if (raw.value === 'true' || raw.value === 'false') { + fields[raw.name] = raw.value === 'true' + } else { + // `--archived some_id`: the flag is boolean, the token was not for it. + fields[raw.name] = true + returned.push(raw.value) + } + + continue + } + + if (raw.value === undefined) { + fields[raw.name] = true + continue + } + + if (kind === 'string') fields[raw.name] = raw.value + else if (kind === 'number') { + const n = Number(raw.value) + if (Number.isNaN(n)) errors.push(`--${raw.name} expects a number, got "${raw.value}"`) + else fields[raw.name] = n + } else if (kind === 'json' || kind === 'json[]' || kind === 'string[]' || kind === 'number[]' || kind === 'boolean[]') { + fields[raw.name] = looksLikeJson(raw.value) ? parseJsonOr(raw.value) : kind === 'string[]' ? raw.value.split(',') : coerce(raw.value) + } else fields[raw.name] = coerce(raw.value) + } + + return {fields, returned, errors} +} + +function convert(value: string, type: ExplicitType, label: string): {value?: unknown; error?: string} { + switch (type) { + case 'str': + return {value} + case 'num': { + const n = Number(value) + return Number.isNaN(n) ? {error: `${label} expects a number, got "${value}"`} : {value: n} + } + + case 'bool': + if (value === 'true' || value === 'false') return {value: value === 'true'} + return {error: `${label} expects true or false, got "${value}"`} + default: + try { + return {value: JSON.parse(value)} + } catch { + return {error: `${label} is not valid JSON: ${value.slice(0, 80)}`} + } + } +} + +function looksLikeJson(value: string): boolean { + return /^\s*[[{]/.test(value) +} + +function parseJsonOr(value: string): unknown { + try { + return JSON.parse(value) + } catch { + return value + } } /** IDs look like maggr_..., sch_..., 66b2f0a1..., UPPER env names, etc. */ @@ -64,6 +196,7 @@ function looksLikeId(token: string): boolean { return /[_0-9]/.test(token) || token === token.toUpperCase() } +/** The old heuristic, for fields the map does not know. */ function coerce(value: string): unknown { if (value === 'true') return true if (value === 'false') return false @@ -151,12 +284,19 @@ export function matchMethods( return matches } -const DESTRUCTIVE_VERBS = new Set(['delete', 'remove', 'clean', 'regenerate', 'rotate', 'stop']) +const DESTRUCTIVE_VERBS = new Set(['delete', 'remove', 'clean', 'regenerate', 'rotate', 'stop', 'disable', 'block']) export function isDestructive(method: string): boolean { return DESTRUCTIVE_VERBS.has(camelSplit(method)[0]) } +/** Read-only verbs: no --dry-run needed, nothing changes. */ +const READ_VERBS = new Set(['get', 'list', 'find', 'count', 'search', 'preview', 'render', 'reveal', 'check', 'validate', 'export', 'download', 'ping', 'echo', 'test', 'verify']) + +export function isReadOnly(method: string): boolean { + return READ_VERBS.has(camelSplit(method)[0]) +} + /** * Module aliases so daily language works: `norbix hub db ...`, and * `norbix hub email templates get` routes into the notifications module diff --git a/src/lib/namespace-command.ts b/src/lib/namespace-command.ts index b4c62a0..5ec180f 100644 --- a/src/lib/namespace-command.ts +++ b/src/lib/namespace-command.ts @@ -1,10 +1,25 @@ import {Norbix} from '@norbix.ai/ts' import {BaseCommand, type GlobalFlags} from '../base.js' -import {MODULE_ALIASES, camelSplit, isDestructive, matchMethods, parseArgv} from './dispatch.js' +import {usageError} from './cli-error.js' +import {MODULE_ALIASES, camelSplit, isDestructive, matchMethods, parseArgv, typeFields} from './dispatch.js' +import {readJsonInput} from './json.js' +import {describeFields, fieldKinds, methodInfo, type FieldKind} from './request-fields.js' type SdkModule = Record) => Promise> +export interface NamespaceFlags extends GlobalFlags { + body?: string +} + +/** The text every scope help ends with: how values are typed. */ +export const VALUE_RULES = `Values: words are lowercase letters; anything with digits, "_" or capitals is a +value (the first one becomes "id"). --field value is typed from the SDK's +request type (a string field keeps "0042"; a boolean flag never swallows the +next word). Force a type with --field:str, --field:num, --field:bool or +--field:json. Or pass the whole request as JSON: --body '{"id":"..."}' (or +--body - for stdin) — then no --field flags are allowed.` + /** * Shared engine for `norbix hub ...` and `norbix api ...` — resolves the * module and method from plain words and calls the SDK. @@ -18,7 +33,7 @@ export abstract class NamespaceCommand extends BaseCommand { * known global flags go to oclif, everything else goes to the dispatcher. */ protected splitArgv(): {flagArgv: string[]; rest: string[]; help: boolean} { - const VALUE_FLAGS = new Set(['--project', '--env', '--region', '--api-key', '--account', '--profile']) + const VALUE_FLAGS = new Set(['--project', '--env', '--region', '--api-key', '--account', '--profile', '--body']) const BOOL_FLAGS = new Set(['--json', '--yes', '-y', '--dry-run']) const flagArgv: string[] = [] const rest: string[] = [] @@ -52,6 +67,19 @@ export abstract class NamespaceCommand extends BaseCommand { return [...nouns, verb].join(' ') } + /** What `--json` returns for one method: word form, route and request fields. */ + protected describeMethod(moduleName: string, method: string, hide: Set): Record { + const info = methodInfo(this.target, moduleName, method) + return { + method, + words: this.wordForm(method, moduleName, hide), + destructive: isDestructive(method), + http: info?.http, + path: info?.path, + fields: info?.fields ?? [], + } + } + /** Scoped help: everything callable under `norbix {target} {module} {words...}`. */ protected scopeHelp( moduleWord: string, @@ -74,7 +102,8 @@ export abstract class NamespaceCommand extends BaseCommand { .map((m) => { const form = this.wordForm(m, moduleName, hide) const marker = isDestructive(m) ? ' (asks confirmation)' : '' - return ` norbix ${this.target} ${moduleWord} ${form}${marker}` + const fields = describeFields(methodInfo(this.target, moduleName, m)) + return ` norbix ${this.target} ${moduleWord} ${form}${marker}${fields ? `\n fields: ${fields}` : ''}` }) .join('\n') @@ -83,13 +112,18 @@ export abstract class NamespaceCommand extends BaseCommand { `${scoped.length === 0 ? ' (no exact scope match — showing the whole module)' : ''}:\n\n${rows}\n\n` + `Add an ID as a plain value and request fields as flags:\n` + ` norbix ${this.target} ${moduleWord} ${this.wordForm(list[0], moduleName, hide)} --someField value\n\n` + - `Useful extras: --dry-run (preview the call), --yes (skip confirmation),\n` + + `${VALUE_RULES}\n\n` + + `Useful extras: --dry-run (preview the exact request), --yes (skip confirmation),\n` + `--json (machine output), --profile/--env/--region/--project (context).`, ) - return {scope: `${this.target}.${moduleName}`, commands: list.map((m) => this.wordForm(m, moduleName, hide))} + return { + scope: `${this.target}.${moduleName}`, + commands: list.map((m) => this.describeMethod(moduleName, m, hide)), + valueRules: VALUE_RULES, + } } - protected async dispatch(argv: string[], flags: GlobalFlags, help = false): Promise { + protected async dispatch(argv: string[], flags: NamespaceFlags, help = false): Promise { const parsed = parseArgv(argv) const [moduleWord, ...restWords] = parsed.words @@ -100,7 +134,7 @@ export abstract class NamespaceCommand extends BaseCommand { if (!moduleWord) { this.print( - `Usage: norbix ${this.target} [id] [--field value]\n\nModules:\n ${moduleNames.join('\n ')}\n\n` + + `Usage: norbix ${this.target} [id] [--field value | --body '']\n\nModules:\n ${moduleNames.join('\n ')}\n\n` + `Get help at any level:\n norbix ${this.target} --help\n norbix ${this.target} --help\n\n` + `Example: norbix ${this.target} database aggregates delete maggr_123 --schemaId sch_456`, ) @@ -111,8 +145,10 @@ export abstract class NamespaceCommand extends BaseCommand { const moduleName = alias && namespace[alias.module] ? alias.module : moduleWord const sdkModule = namespace[moduleName] if (!sdkModule) { - this.error( - `Unknown module "${moduleWord}" on ${this.target}.\nAvailable: ${moduleNames.join(', ')}`, + throw usageError( + `Unknown module "${moduleWord}" on ${this.target}.`, + `Available: ${moduleNames.join(', ')}`, + `norbix ${this.target} --help`, ) } @@ -121,50 +157,48 @@ export abstract class NamespaceCommand extends BaseCommand { .filter((k) => typeof sdkModule[k] === 'function') .sort() + const injected = alias?.injectWord && moduleName !== moduleWord ? alias.injectWord : undefined + const hide = new Set(injected ? [injected] : []) + if (help) { - const injected = alias?.injectWord && moduleName !== moduleWord ? alias.injectWord : undefined const helpWords = injected ? [injected, ...restWords] : [...restWords] return this.scopeHelp(moduleWord, moduleName, methods, helpWords, restWords, injected) } - if (restWords.length === 0 && parsed.positionals.length === 0) { + if (restWords.length === 0 && parsed.positionals.length === 0 && parsed.rawFields.length === 0 && !flags.body) { this.print( - `Methods of ${this.target}.${moduleName}:\n ${methods.join('\n ')}\n\nCall one with plain words, e.g.:\n norbix ${this.target} ${moduleWord} aggregates get\n norbix ${this.target} ${moduleWord} aggregates delete `, + `Methods of ${this.target}.${moduleName}:\n ${methods.join('\n ')}\n\nCall one with plain words, e.g.:\n norbix ${this.target} ${moduleWord} aggregates get\n norbix ${this.target} ${moduleWord} aggregates delete \n\nSee fields per method: norbix ${this.target} ${moduleWord} --help (or --json)`, ) - return {module: moduleName, methods} + return {module: moduleName, methods: methods.map((m) => this.describeMethod(moduleName, m, hide))} } const words = [...restWords] - if (alias?.injectWord && moduleName !== moduleWord) words.unshift(alias.injectWord) + if (injected) words.unshift(injected) const matches = matchMethods(methods, words, moduleName) if (matches.length === 0) { // Show only methods sharing at least one word — the full list can be 100+. const lower = words.map((w) => w.toLowerCase().replace(/s$/, '')) const related = methods.filter((m) => lower.some((w) => m.toLowerCase().includes(w))) const shown = (related.length > 0 ? related : methods).slice(0, 25) - this.error( - `No ${this.target}.${moduleName} method matches "${words.join(' ')}".\n` + - `${related.length > 0 ? 'Close methods' : 'Methods'}:\n ${shown.join('\n ')}` + - `${shown.length < methods.length ? `\n … run \`norbix ${this.target} ${moduleWord}\` for the full list` : ''}`, + throw usageError( + `No ${this.target}.${moduleName} method matches "${words.join(' ')}".`, + `${related.length > 0 ? 'Close methods' : 'Methods'}: ${shown.join(', ')}` + + `${shown.length < methods.length ? ` … run \`norbix ${this.target} ${moduleWord}\` for the full list` : ''}`, + `norbix ${this.target} ${moduleWord} --help`, ) } if (matches.length > 1) { - this.error( - `"${words.join(' ')}" is ambiguous — did you mean:\n ${matches.map((m) => m.method).join('\n ')}\nAdd a word (e.g. the verb) to be specific.`, + throw usageError( + `"${words.join(' ')}" is ambiguous — did you mean: ${matches.map((m) => m.method).join(', ')}`, + 'Add a word (e.g. the verb) to be specific, or type the camelCase method name itself.', + `norbix ${this.target} ${moduleWord} --help`, ) } const method = matches[0].method - const request: Record = {...parsed.fields} - if (parsed.positionals.length > 0 && request.id === undefined) { - request.id = parsed.positionals[0] - if (parsed.positionals.length > 1) { - this.error( - `Only one positional value is allowed (became "id"). Pass the rest as flags, e.g. --schemaId ${parsed.positionals[1]}`, - ) - } - } + const info = methodInfo(this.target, moduleName, method) + const request = await this.buildRequest(parsed, flags, info === undefined ? {} : fieldKinds(info), moduleName, method) // Context (auth, region, project) is resolved before any confirmation or // dry run, so a dry run that would fail for real fails here too. @@ -178,4 +212,54 @@ export abstract class NamespaceCommand extends BaseCommand { this.print(res) return res } + + /** The request object: `--body` as a whole, or the typed `--field` flags plus the positional id. */ + private async buildRequest( + parsed: ReturnType, + flags: NamespaceFlags, + kinds: Record, + moduleName: string, + method: string, + ): Promise> { + let request: Record + let positionals = parsed.positionals + + if (flags.body !== undefined) { + if (parsed.rawFields.length > 0) { + throw usageError( + `--body carries the whole request; --${parsed.rawFields[0].name} cannot be combined with it.`, + 'Put every field inside the --body JSON object, or drop --body and use --field flags only.', + `norbix ${this.target} --help`, + ) + } + + const body = JSON.parse(await readJsonInput(flags.body, 'body')) as unknown + if (body === null || typeof body !== 'object' || Array.isArray(body)) { + throw usageError('--body must be a JSON object.', `Example: --body '{"id":"${positionals[0] ?? 'abc123'}"}'`) + } + + request = {...(body as Record)} + } else { + const typed = typeFields(parsed.rawFields, kinds) + if (typed.errors.length > 0) { + throw usageError(typed.errors.join('; '), `Fields of ${method}: ${describeFields(methodInfo(this.target, moduleName, method)) || 'see --help'}`) + } + + request = typed.fields + positionals = [...positionals, ...typed.returned] + } + + if (positionals.length > 0 && request.id === undefined) { + request.id = positionals[0] + if (positionals.length > 1) { + throw usageError( + `Only one positional value is allowed (became "id"). Pass the rest as flags, e.g. --schemaId ${positionals[1]}`, + 'Use --field value for every other request field, or --body for the whole request.', + `norbix ${this.target} --help`, + ) + } + } + + return request + } } diff --git a/src/lib/request-fields.ts b/src/lib/request-fields.ts new file mode 100644 index 0000000..3ee0511 --- /dev/null +++ b/src/lib/request-fields.ts @@ -0,0 +1,61 @@ +import {readFileSync} from 'node:fs' + +/** + * The request-field map generated from the installed SDK + * (`scripts/gen-request-fields.mjs` → `src/generated/request-fields.json`): + * for every `api.*` / `hub.*` method, its HTTP route and request fields. + * Loaded lazily — only `hub`/`api` and `schema` read it. + * + * No oclif import: the MCP server can list the same fields. + */ + +export type FieldKind = 'string' | 'number' | 'boolean' | 'json' | 'string[]' | 'number[]' | 'boolean[]' | 'json[]' + +export interface RequestField { + name: string + /** The TypeScript type as the SDK declares it (for display). */ + type: string + kind: FieldKind + required: boolean + /** Enum values when the type is an enum. */ + options?: string[] +} + +export interface MethodInfo { + http: string + path: string + dto: string + fields: RequestField[] +} + +export interface RequestFieldMap { + sdk: string + api: Record> + hub: Record> +} + +let cached: RequestFieldMap | undefined + +export function loadRequestFields(): RequestFieldMap { + cached ??= JSON.parse(readFileSync(new URL('../generated/request-fields.json', import.meta.url), 'utf8')) as RequestFieldMap + return cached +} + +export function methodInfo(target: 'api' | 'hub', moduleName: string, method: string): MethodInfo | undefined { + return loadRequestFields()[target][moduleName]?.[method] +} + +/** `name` → kind, for the plain-word parser. */ +export function fieldKinds(info: MethodInfo | undefined): Record { + const out: Record = {} + for (const f of info?.fields ?? []) out[f.name] = f.kind + return out +} + +/** One line per field for help: `id (string, required)`. */ +export function describeFields(info: MethodInfo | undefined): string { + if (!info) return '' + return info.fields + .map((f) => `${f.name} (${f.options ? f.options.join('|') : f.kind}${f.required ? ', required' : ''})`) + .join(', ') +} From 5b831e89b14c7fdabd5da59c27cad4448ef760e9 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Fri, 2 Oct 2026 12:50:33 +0300 Subject: [PATCH 05/14] =?UTF-8?q?feat(cli):=20`norbix=20schema=20[command]?= =?UTF-8?q?=20--json`=20=E2=80=94=20the=20command=20surface=20as=20data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan step [2] of docs/tasks/agent-native-cli.md. Every command with its args, flags (type, env var, default, global), examples, and two booleans an agent branches on: `destructive` (asks for confirmation; exit 3 without a terminal unless --yes) and `supportsDryRun`. Plus the exit-code table, the contract path and how to discover the dynamic hub/api methods. Without --json: a compact table. The builder in src/lib/schema.ts has no oclif import so the MCP server can emit the same document. Co-Authored-By: Claude Fable 5.1 --- src/commands/schema.ts | 59 +++++++++++++++ src/lib/schema.ts | 165 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 src/commands/schema.ts create mode 100644 src/lib/schema.ts diff --git a/src/commands/schema.ts b/src/commands/schema.ts new file mode 100644 index 0000000..7bc449a --- /dev/null +++ b/src/commands/schema.ts @@ -0,0 +1,59 @@ +import {Args} from '@oclif/core' + +import {BaseCommand} from '../base.js' +import {usageError} from '../lib/cli-error.js' +import {VALUE_RULES} from '../lib/namespace-command.js' +import {buildSchema, formatSchemaTable, type CommandDescriptor} from '../lib/schema.js' + +export default class Schema extends BaseCommand { + static description = `Describe the CLI as data: every command with its args, flags, examples, whether it is destructive and whether it supports --dry-run. + +Made for coding agents and scripts: run it once with --json, then +\`norbix --help\` for details. The plain-word \`hub\` / \`api\` +commands are dynamic — their methods and request fields come from +\`norbix hub --json\`.` + + static examples = [ + '<%= config.bin %> schema', + '<%= config.bin %> schema --json', + '<%= config.bin %> schema users delete --json', + '<%= config.bin %> schema db --json', + ] + + static strict = false + + static args = { + command: Args.string({required: false, description: 'A command ("users delete") or topic ("db") to describe; omit for everything'}), + } + + async run(): Promise { + const {argv, flags} = await this.parse(Schema) + void flags + const wanted = (argv as string[]).join(' ').trim() + + const own = this.config.commands.filter((c) => c.pluginName === this.config.pjson.name) + const descriptors: CommandDescriptor[] = own.map((c) => ({ + id: c.id, + description: c.description, + hidden: c.hidden, + args: c.args as CommandDescriptor['args'], + flags: c.flags as CommandDescriptor['flags'], + examples: c.examples as CommandDescriptor['examples'], + })) + + const schema = buildSchema({version: this.config.version, bin: this.config.bin, commands: descriptors, valueRules: VALUE_RULES}) + + if (wanted) { + const id = wanted.replaceAll(':', ' ') + const matched = schema.commands.filter((c) => c.id === id || c.id.startsWith(`${id} `)) + if (matched.length === 0) { + throw usageError(`No command or topic "${wanted}".`, 'Run `norbix schema` for the list.', 'norbix schema --help') + } + + schema.commands = matched + } + + this.print(formatSchemaTable(schema)) + return schema + } +} diff --git a/src/lib/schema.ts b/src/lib/schema.ts new file mode 100644 index 0000000..1aec281 --- /dev/null +++ b/src/lib/schema.ts @@ -0,0 +1,165 @@ +import {EXIT_CODES} from './exit-codes.js' + +/** + * `norbix schema [command] --json`: the whole command surface as data, so an + * agent can discover the CLI without reading source. Built from plain + * descriptors (the command converts oclif's manifest into them) — no oclif + * import here, so the MCP server can emit the same document. + */ + +export interface SchemaArg { + name: string + required: boolean + description?: string + options?: string[] +} + +export interface SchemaFlag { + name: string + char?: string + type: 'boolean' | 'string' | 'integer' | 'option' + description?: string + required?: boolean + multiple?: boolean + options?: string[] + env?: string + default?: unknown + /** True for the context flags every command takes (--project, --profile, --json, ...). */ + global?: boolean +} + +export interface SchemaCommand { + id: string + description: string + /** Asks for confirmation; exits 3 without a terminal unless --yes. */ + destructive: boolean + /** Accepts --dry-run: resolves context, prints the request, sends nothing. */ + supportsDryRun: boolean + args: SchemaArg[] + flags: SchemaFlag[] + examples: string[] +} + +export interface CliSchema { + cli: 'norbix' + version: string + contract: string + exitCodes: Record + commands: SchemaCommand[] + dynamic: {hub: string; api: string; valueRules: string} +} + +/** What the command hands in for every entry of the manifest. */ +export interface CommandDescriptor { + id: string + description?: string + hidden?: boolean + args: Record + flags: Record< + string, + { + name?: string + char?: string + type?: string + description?: string + required?: boolean + multiple?: boolean + options?: string[] + env?: string + default?: unknown + helpGroup?: string + hidden?: boolean + } + > + examples?: Array +} + +export const CONTRACT_PATH = 'docs/agent-contract.md' +export const CONTRACT_URL = 'https://github.com/norbix-code/cli/blob/main/docs/agent-contract.md' + +export const DYNAMIC_HELP = { + hub: 'norbix hub --help lists every method with its request fields; norbix hub --json returns them as data; call with plain words: norbix hub [id] [--field value | --body ]', + api: 'norbix api --help / --json — same engine as hub, for the data-plane API', +} + +export function buildSchema(input: {version: string; bin?: string; commands: CommandDescriptor[]; valueRules: string}): CliSchema { + const bin = input.bin ?? 'norbix' + const commands = input.commands + .filter((c) => !c.hidden) + .map((c) => toSchemaCommand(c, bin)) + .sort((a, b) => a.id.localeCompare(b.id)) + + const exitCodes: CliSchema['exitCodes'] = {} + for (const e of EXIT_CODES) exitCodes[String(e.code)] = {name: e.name, meaning: e.meaning, hint: e.hint} + + return { + cli: 'norbix', + version: input.version, + contract: `${CONTRACT_PATH} (${CONTRACT_URL})`, + exitCodes, + commands, + dynamic: {...DYNAMIC_HELP, valueRules: input.valueRules}, + } +} + +function toSchemaCommand(c: CommandDescriptor, bin: string): SchemaCommand { + const flags = Object.entries(c.flags) + .filter(([, f]) => !f.hidden) + .map(([name, f]) => { + const flag: SchemaFlag = {name: f.name ?? name, type: flagType(f.type, f.options)} + if (f.char) flag.char = f.char + if (f.description) flag.description = f.description + if (f.required) flag.required = true + if (f.multiple) flag.multiple = true + if (f.options) flag.options = f.options + if (f.env) flag.env = f.env + if (f.default !== undefined && f.default !== false) flag.default = f.default + if (f.helpGroup === 'GLOBAL' || name === 'json') flag.global = true + return flag + }) + + const args = Object.entries(c.args).map(([name, a]) => { + const arg: SchemaArg = {name: a.name ?? name, required: Boolean(a.required)} + if (a.description) arg.description = a.description + if (a.options) arg.options = a.options + return arg + }) + + const names = new Set(flags.map((f) => f.name)) + return { + id: c.id.replaceAll(':', ' '), + description: (c.description ?? '').split('\n')[0], + destructive: names.has('yes'), + supportsDryRun: names.has('dry-run'), + args, + flags, + examples: (c.examples ?? []).map((e) => (typeof e === 'string' ? e : e.command).replaceAll('<%= config.bin %>', bin)), + } +} + +function flagType(type: string | undefined, options: string[] | undefined): SchemaFlag['type'] { + if (type === 'boolean') return 'boolean' + if (options && options.length > 0) return 'option' + return 'string' +} + +/** Compact text table for humans. */ +export function formatSchemaTable(schema: CliSchema): string { + const width = Math.max(...schema.commands.map((c) => c.id.length)) + const lines = schema.commands.map((c) => { + const marks = [c.destructive ? 'destructive' : '', c.supportsDryRun ? 'dry-run' : ''].filter(Boolean).join(', ') + return `${c.id.padEnd(width)} ${c.description}${marks ? ` [${marks}]` : ''}` + }) + return [ + `norbix ${schema.version} — ${schema.commands.length} commands. Contract: ${schema.contract}`, + '', + ...lines, + '', + `Dynamic: ${schema.dynamic.hub}`, + ` ${schema.dynamic.api}`, + '', + 'Exit codes: ' + Object.entries(schema.exitCodes).map(([code, e]) => `${code}=${e.name}`).join(' '), + '', + 'Machine form: norbix schema --json, or norbix schema --json for one command.', + ].join('\n') +} From 0f61bca1f487ab577e0a0d4147fae46d76238175 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Fri, 2 Oct 2026 12:50:33 +0300 Subject: [PATCH 06/14] feat(cli): --dry-run on every mutating command, realistic help examples Plan step [2] of docs/tasks/agent-native-cli.md. - Every command that changes something takes --dry-run: SDK calls are stopped in the transport (auth, region and project resolved first); config set/unset, env use, logout, raw and files publish/unpublish print the file or the HTTP request they would write / send. Read-only commands get no flag. - Every example uses real-looking ids; every mutating command shows the --dry-run pattern and every destructive one the --yes pattern. Co-Authored-By: Claude Fable 5.1 --- src/commands/apikeys/regenerate.ts | 5 ++++- src/commands/config/set.ts | 18 +++++++++++++++--- src/commands/config/unset.ts | 14 +++++++++++--- src/commands/db/aggregate.ts | 2 +- src/commands/db/aggregates.ts | 2 +- src/commands/db/get.ts | 2 +- src/commands/db/insert-many.ts | 2 ++ src/commands/db/insert.ts | 2 ++ src/commands/db/replace.ts | 6 +++++- src/commands/db/schema.ts | 2 +- src/commands/db/taxonomy.ts | 2 +- src/commands/db/update.ts | 2 +- src/commands/email/archive.ts | 9 ++++++++- src/commands/email/campaign.ts | 2 +- src/commands/email/clone.ts | 9 ++++++++- src/commands/email/delete.ts | 5 ++++- src/commands/email/stop.ts | 5 ++++- src/commands/email/template.ts | 2 +- src/commands/email/unarchive.ts | 9 ++++++++- src/commands/env/delete.ts | 5 ++++- src/commands/env/use.ts | 15 +++++++++++++-- src/commands/files/delete.ts | 5 ++++- src/commands/files/list.ts | 2 +- src/commands/files/publish.ts | 16 ++++++++++------ src/commands/files/unpublish.ts | 16 ++++++++++------ src/commands/files/upload.ts | 2 ++ src/commands/logout.ts | 18 ++++++++++++++---- src/commands/logs/trail.ts | 2 +- src/commands/module.ts | 1 + src/commands/payments/enable.ts | 9 ++++++++- src/commands/payments/trigger.ts | 2 +- src/commands/push/archive.ts | 9 ++++++++- src/commands/push/campaign.ts | 2 +- src/commands/push/campaign/batch.ts | 4 ++-- src/commands/push/campaign/batches.ts | 2 +- src/commands/push/campaign/create.ts | 8 +++++--- src/commands/push/campaign/delete.ts | 5 ++++- src/commands/push/campaign/message.ts | 2 +- src/commands/push/campaign/messages.ts | 2 +- src/commands/push/clone.ts | 9 ++++++++- src/commands/push/delete.ts | 5 ++++- src/commands/push/device/register.ts | 2 ++ src/commands/push/disable.ts | 5 ++++- src/commands/push/enable.ts | 9 ++++++++- src/commands/push/integration.ts | 2 +- .../push/integration/confirm-delivery.ts | 9 ++++++++- src/commands/push/integration/default.ts | 9 ++++++++- src/commands/push/integration/delete.ts | 5 ++++- src/commands/push/integration/enable.ts | 9 ++++++++- src/commands/push/integration/save.ts | 2 ++ src/commands/push/integration/test.ts | 4 +++- src/commands/push/preview.ts | 4 ++-- src/commands/push/stop.ts | 5 ++++- src/commands/push/template.ts | 2 +- src/commands/push/template/create.ts | 6 +++++- src/commands/push/template/tokens.ts | 2 +- src/commands/push/template/update.ts | 8 ++++++-- src/commands/push/unarchive.ts | 9 ++++++++- src/commands/raw.ts | 15 ++++++++++++--- src/commands/scheduler/delete.ts | 5 ++++- src/commands/scheduler/enable.ts | 9 ++++++++- src/commands/scheduler/get.ts | 2 +- src/commands/sms/archive.ts | 9 ++++++++- src/commands/sms/campaign.ts | 2 +- src/commands/sms/clone.ts | 9 ++++++++- src/commands/sms/delete.ts | 5 ++++- src/commands/sms/stop.ts | 5 ++++- src/commands/sms/template.ts | 2 +- src/commands/sms/unarchive.ts | 9 ++++++++- src/commands/users/delete.ts | 5 ++++- src/commands/users/get.ts | 2 +- src/commands/users/invite.ts | 9 ++++++++- src/commands/users/unblock.ts | 9 ++++++++- src/commands/webhooks/enable.ts | 9 ++++++++- src/commands/webhooks/remove.ts | 5 ++++- src/commands/webhooks/secret.ts | 1 + src/lib/publicFiles.ts | 16 ++++++++++++++++ 77 files changed, 367 insertions(+), 94 deletions(-) diff --git a/src/commands/apikeys/regenerate.ts b/src/commands/apikeys/regenerate.ts index 7412c54..c612aed 100644 --- a/src/commands/apikeys/regenerate.ts +++ b/src/commands/apikeys/regenerate.ts @@ -4,7 +4,10 @@ import {BaseCommand} from '../../base.js' export default class ApikeysRegenerate extends BaseCommand { static description = 'Regenerate the API keys for the current environment (old keys stop working!)' - static examples = ['<%= config.bin %> apikeys regenerate --yes'] + static examples = [ + '<%= config.bin %> apikeys regenerate --yes', + '<%= config.bin %> apikeys regenerate --dry-run', + ] static flags = { ...BaseCommand.mutatingFlags, diff --git a/src/commands/config/set.ts b/src/commands/config/set.ts index d28704e..fad486f 100644 --- a/src/commands/config/set.ts +++ b/src/commands/config/set.ts @@ -1,15 +1,16 @@ import {Args} from '@oclif/core' import {BaseCommand} from '../../base.js' -import {SETTABLE_KEYS, isSettableKey, writeStore} from '../../lib/store.js' +import {SETTABLE_KEYS, configFilePath, isSettableKey, writeStore} from '../../lib/store.js' export default class ConfigSet extends BaseCommand { static description = 'Set one configuration value' static examples = [ - '<%= config.bin %> config set projectId 5f1a9f7e...', + '<%= config.bin %> config set projectId 5f1a9f7e2b3c4d5e6f708192', '<%= config.bin %> config set region nb-eu-germany', '<%= config.bin %> config set env TEST', + '<%= config.bin %> config set region nb-eu-germany --dry-run', ] static args = { @@ -17,12 +18,23 @@ export default class ConfigSet extends BaseCommand { value: Args.string({required: true, description: 'New value'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { - const {args} = await this.parse(ConfigSet) + const {args, flags} = await this.parse(ConfigSet) if (!isSettableKey(args.key)) { this.error(`Unknown key "${args.key}". Valid keys: ${SETTABLE_KEYS.join(', ')}`) } + if (flags['dry-run']) { + return this.dryRun({ + method: 'config.set', + request: {file: configFilePath(this.config.configDir), key: args.key, value: args.key === 'apiKey' ? '***' : args.value}, + }) + } + const stored = this.readStore() writeStore(this.config.configDir, {...stored, [args.key]: args.value}) this.print(`${args.key} = ${args.key === 'apiKey' ? '(saved)' : args.value}`) diff --git a/src/commands/config/unset.ts b/src/commands/config/unset.ts index 9fbd627..29de181 100644 --- a/src/commands/config/unset.ts +++ b/src/commands/config/unset.ts @@ -1,23 +1,31 @@ import {Args} from '@oclif/core' import {BaseCommand} from '../../base.js' -import {SETTABLE_KEYS, isSettableKey, writeStore} from '../../lib/store.js' +import {SETTABLE_KEYS, configFilePath, isSettableKey, writeStore} from '../../lib/store.js' export default class ConfigUnset extends BaseCommand { static description = 'Remove one configuration value' - static examples = ['<%= config.bin %> config unset region'] + static examples = ['<%= config.bin %> config unset region', '<%= config.bin %> config unset region --dry-run'] static args = { key: Args.string({required: true, description: `One of: ${SETTABLE_KEYS.join(', ')}`}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { - const {args} = await this.parse(ConfigUnset) + const {args, flags} = await this.parse(ConfigUnset) if (!isSettableKey(args.key)) { this.error(`Unknown key "${args.key}". Valid keys: ${SETTABLE_KEYS.join(', ')}`) } + if (flags['dry-run']) { + return this.dryRun({method: 'config.unset', request: {file: configFilePath(this.config.configDir), key: args.key}}) + } + const stored = this.readStore() writeStore(this.config.configDir, {...stored, [args.key]: undefined}) this.print(`${args.key} removed.`) diff --git a/src/commands/db/aggregate.ts b/src/commands/db/aggregate.ts index 7e5094a..839b45b 100644 --- a/src/commands/db/aggregate.ts +++ b/src/commands/db/aggregate.ts @@ -8,7 +8,7 @@ export default class DbAggregate extends BaseCommand { static examples = [ `<%= config.bin %> db aggregate orders --pipeline '[{"$group":{"_id":"$status","n":{"$sum":1}}}]'`, - '<%= config.bin %> db aggregate orders --id 66b2f0a1...', + '<%= config.bin %> db aggregate orders --id 66b2f0a1c3d4e5f6a7b8c9d0', ] static args = { diff --git a/src/commands/db/aggregates.ts b/src/commands/db/aggregates.ts index f6e821f..f39cfcd 100644 --- a/src/commands/db/aggregates.ts +++ b/src/commands/db/aggregates.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class DbAggregates extends BaseCommand { static description = 'List saved database aggregates' - static examples = ['<%= config.bin %> db aggregates', '<%= config.bin %> db aggregates --schema 66b2...'] + static examples = ['<%= config.bin %> db aggregates', '<%= config.bin %> db aggregates --schema 66b2f0a1c3d4e5f6a7b8c9d0'] static flags = { schema: Flags.string({description: 'Only aggregates of this schema ID'}), diff --git a/src/commands/db/get.ts b/src/commands/db/get.ts index 3ba24f2..4eefb6a 100644 --- a/src/commands/db/get.ts +++ b/src/commands/db/get.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class DbGet extends BaseCommand { static description = 'Get one record by ID' - static examples = ['<%= config.bin %> db get orders 66b2f0a1...'] + static examples = ['<%= config.bin %> db get orders 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { collection: Args.string({required: true, description: 'Collection name'}), diff --git a/src/commands/db/insert-many.ts b/src/commands/db/insert-many.ts index 4d597bb..f883108 100644 --- a/src/commands/db/insert-many.ts +++ b/src/commands/db/insert-many.ts @@ -9,6 +9,7 @@ export default class DbInsertMany extends BaseCommand { static examples = [ `<%= config.bin %> db insert-many orders --docs '[{"a":1},{"a":2}]'`, 'cat orders.json | <%= config.bin %> db insert-many orders --docs -', + `<%= config.bin %> db insert-many orders --docs '[{"a":1},{"a":2}]' --dry-run`, ] static args = { @@ -16,6 +17,7 @@ export default class DbInsertMany extends BaseCommand { } static flags = { + ...BaseCommand.dryRunFlags, docs: Flags.string({char: 'd', required: true, description: 'JSON array of documents (or `-` for stdin)'}), } diff --git a/src/commands/db/insert.ts b/src/commands/db/insert.ts index 5cbbdb4..35d31a0 100644 --- a/src/commands/db/insert.ts +++ b/src/commands/db/insert.ts @@ -9,6 +9,7 @@ export default class DbInsert extends BaseCommand { static examples = [ `<%= config.bin %> db insert orders --doc '{"status":"new","total":9.99}'`, 'cat order.json | <%= config.bin %> db insert orders --doc -', + `<%= config.bin %> db insert orders --doc '{"status":"new","total":9.99}' --dry-run`, ] static args = { @@ -16,6 +17,7 @@ export default class DbInsert extends BaseCommand { } static flags = { + ...BaseCommand.dryRunFlags, doc: Flags.string({char: 'd', required: true, description: 'JSON document (or `-` for stdin)'}), } diff --git a/src/commands/db/replace.ts b/src/commands/db/replace.ts index 131c1b3..eec3c1a 100644 --- a/src/commands/db/replace.ts +++ b/src/commands/db/replace.ts @@ -6,13 +6,17 @@ import {readJsonInput} from '../../lib/json.js' export default class DbReplace extends BaseCommand { static description = 'Replace one record fully (unlike update, the whole document is swapped)' - static examples = [`<%= config.bin %> db replace orders --id 66b2f0a1... --doc '{"status":"new"}'`] + static examples = [ + `<%= config.bin %> db replace orders --id 66b2f0a1c3d4e5f6a7b8c9d0 --doc '{"status":"new"}'`, + `<%= config.bin %> db replace orders --id 66b2f0a1c3d4e5f6a7b8c9d0 --doc '{"status":"new"}' --dry-run`, + ] static args = { collection: Args.string({required: true, description: 'Collection name'}), } static flags = { + ...BaseCommand.dryRunFlags, id: Flags.string({required: true, description: 'Record ID'}), doc: Flags.string({char: 'd', required: true, description: 'JSON replacement document (or `-` for stdin)'}), } diff --git a/src/commands/db/schema.ts b/src/commands/db/schema.ts index 0e6659d..93ea809 100644 --- a/src/commands/db/schema.ts +++ b/src/commands/db/schema.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class DbSchema extends BaseCommand { static description = 'Show one database schema' - static examples = ['<%= config.bin %> db schema 66b2f0a1...'] + static examples = ['<%= config.bin %> db schema 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Schema ID'}), diff --git a/src/commands/db/taxonomy.ts b/src/commands/db/taxonomy.ts index aadc2fd..2fa6008 100644 --- a/src/commands/db/taxonomy.ts +++ b/src/commands/db/taxonomy.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class DbTaxonomy extends BaseCommand { static description = 'Show one database taxonomy' - static examples = ['<%= config.bin %> db taxonomy 66b2f0a1...'] + static examples = ['<%= config.bin %> db taxonomy 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Taxonomy ID'}), diff --git a/src/commands/db/update.ts b/src/commands/db/update.ts index 9e2806c..e254914 100644 --- a/src/commands/db/update.ts +++ b/src/commands/db/update.ts @@ -7,7 +7,7 @@ export default class DbUpdate extends BaseCommand { static description = 'Update records: one by --id, or many by --filter with --many' static examples = [ - `<%= config.bin %> db update orders --id 66b2f0a1... --update '{"$set":{"status":"shipped"}}'`, + `<%= config.bin %> db update orders --id 66b2f0a1c3d4e5f6a7b8c9d0 --update '{"$set":{"status":"shipped"}}'`, `<%= config.bin %> db update orders --filter '{"status":"new"}' --update '{"$set":{"status":"queued"}}' --many --dry-run`, `<%= config.bin %> db update orders --filter '{"status":"new"}' --update '{"$set":{"status":"queued"}}' --many --yes`, ] diff --git a/src/commands/email/archive.ts b/src/commands/email/archive.ts index cd6bdc2..20133ff 100644 --- a/src/commands/email/archive.ts +++ b/src/commands/email/archive.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../base.js' export default class EmailArchive extends BaseCommand { static description = 'Archive an email template' - static examples = ['<%= config.bin %> email archive 66b2f0a1...'] + static examples = [ + '<%= config.bin %> email archive 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> email archive 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Template ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(EmailArchive) const client = this.client(flags) diff --git a/src/commands/email/campaign.ts b/src/commands/email/campaign.ts index 2f17cb0..50e061f 100644 --- a/src/commands/email/campaign.ts +++ b/src/commands/email/campaign.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class EmailCampaign extends BaseCommand { static description = 'Show one email campaign (or its statistics with --stats)' - static examples = ['<%= config.bin %> email campaign 66b2f0a1... --stats'] + static examples = ['<%= config.bin %> email campaign 66b2f0a1c3d4e5f6a7b8c9d0 --stats'] static args = { id: Args.string({required: true, description: 'Campaign ID'}), diff --git a/src/commands/email/clone.ts b/src/commands/email/clone.ts index 130b2bc..8c819c2 100644 --- a/src/commands/email/clone.ts +++ b/src/commands/email/clone.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../base.js' export default class EmailClone extends BaseCommand { static description = 'Clone an email template' - static examples = ['<%= config.bin %> email clone 66b2f0a1...'] + static examples = [ + '<%= config.bin %> email clone 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> email clone 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Template ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(EmailClone) const client = this.client(flags) diff --git a/src/commands/email/delete.ts b/src/commands/email/delete.ts index a219dad..384ced2 100644 --- a/src/commands/email/delete.ts +++ b/src/commands/email/delete.ts @@ -5,7 +5,10 @@ import {BaseCommand} from '../../base.js' export default class EmailDelete extends BaseCommand { static description = 'Delete an email template' - static examples = ['<%= config.bin %> email delete 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> email delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> email delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Template ID'}), diff --git a/src/commands/email/stop.ts b/src/commands/email/stop.ts index 98e22d2..353eb94 100644 --- a/src/commands/email/stop.ts +++ b/src/commands/email/stop.ts @@ -5,7 +5,10 @@ import {BaseCommand} from '../../base.js' export default class EmailStop extends BaseCommand { static description = 'Stop a running email campaign' - static examples = ['<%= config.bin %> email stop 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> email stop 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> email stop 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Campaign ID'}), diff --git a/src/commands/email/template.ts b/src/commands/email/template.ts index 432d5f4..d17562e 100644 --- a/src/commands/email/template.ts +++ b/src/commands/email/template.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class EmailTemplate extends BaseCommand { static description = 'Show one email template' - static examples = ['<%= config.bin %> email template 66b2f0a1...'] + static examples = ['<%= config.bin %> email template 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Template ID'}), diff --git a/src/commands/email/unarchive.ts b/src/commands/email/unarchive.ts index 8094b66..f7549fb 100644 --- a/src/commands/email/unarchive.ts +++ b/src/commands/email/unarchive.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../base.js' export default class EmailUnarchive extends BaseCommand { static description = 'Restore an archived email template' - static examples = ['<%= config.bin %> email unarchive 66b2f0a1...'] + static examples = [ + '<%= config.bin %> email unarchive 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> email unarchive 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Template ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(EmailUnarchive) const client = this.client(flags) diff --git a/src/commands/env/delete.ts b/src/commands/env/delete.ts index 328ea55..e9e3dfc 100644 --- a/src/commands/env/delete.ts +++ b/src/commands/env/delete.ts @@ -5,7 +5,10 @@ import {BaseCommand} from '../../base.js' export default class EnvDelete extends BaseCommand { static description = 'Delete a non-PROD environment (cascades its integrations)' - static examples = ['<%= config.bin %> env delete STAGING --yes'] + static examples = [ + '<%= config.bin %> env delete STAGING --yes', + '<%= config.bin %> env delete STAGING --dry-run', + ] static args = { name: Args.string({required: true, description: 'Environment name (PROD cannot be deleted)'}), diff --git a/src/commands/env/use.ts b/src/commands/env/use.ts index 1d7bdbb..ef542bf 100644 --- a/src/commands/env/use.ts +++ b/src/commands/env/use.ts @@ -1,8 +1,8 @@ import {Args} from '@oclif/core' import {BaseCommand} from '../../base.js' -import {isSessionValid, readProfiles, readSession, writeProfile, writeSession} from '../../lib/profiles.js' -import {writeStore} from '../../lib/store.js' +import {PROFILES_PATH, SESSION_PATH, isSessionValid, readProfiles, readSession, writeProfile, writeSession} from '../../lib/profiles.js' +import {configFilePath, writeStore} from '../../lib/store.js' export default class EnvUse extends BaseCommand { static description = `Set the default environment for future commands. @@ -15,18 +15,26 @@ profile. Override per command with --env.` '<%= config.bin %> env use TEST', '<%= config.bin %> env use PROD', '<%= config.bin %> env use STAGING --profile fitskin-prod', + '<%= config.bin %> env use TEST --dry-run', ] static args = { name: Args.string({required: true, description: 'Environment name (e.g. PROD, TEST)'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(EnvUse) + const dry = (target: string, file: string) => + this.dryRun({method: 'env.use', request: {env: args.name, target, file}}) if (flags.profile) { const prof = readProfiles()[flags.profile] if (!prof) this.error(`Profile "${flags.profile}" not found. Run \`norbix configure --profile ${flags.profile}\` first.`) + if (flags['dry-run']) return dry(`profile [${flags.profile}]`, PROFILES_PATH) writeProfile(flags.profile, {...prof, env: args.name}) this.print(`Profile [${flags.profile}] now defaults to environment ${args.name}.`) return {env: args.name, profile: flags.profile} @@ -34,6 +42,7 @@ profile. Override per command with --env.` const session = readSession() if (isSessionValid(session)) { + if (flags['dry-run']) return dry('session', SESSION_PATH) writeSession({...session, env: args.name}) this.print(`Session now targets environment ${args.name}.`) return {env: args.name, target: 'session'} @@ -41,12 +50,14 @@ profile. Override per command with --env.` const prof = readProfiles().default if (prof) { + if (flags['dry-run']) return dry('profile [default]', PROFILES_PATH) writeProfile('default', {...prof, env: args.name}) this.print(`Profile [default] now defaults to environment ${args.name}.`) return {env: args.name, profile: 'default'} } // Last resort: legacy config location. + if (flags['dry-run']) return dry('legacy config', configFilePath(this.config.configDir)) writeStore(this.config.configDir, {...this.readStore(), env: args.name}) this.print(`Default environment set to ${args.name}.`) return {env: args.name, target: 'legacy'} diff --git a/src/commands/files/delete.ts b/src/commands/files/delete.ts index 8550690..2bdf884 100644 --- a/src/commands/files/delete.ts +++ b/src/commands/files/delete.ts @@ -6,7 +6,10 @@ import {integrationFlag, resolveIntegration} from '../../lib/files.js' export default class FilesDelete extends BaseCommand { static description = 'Delete one file' - static examples = ['<%= config.bin %> files delete invoices/2026/invoice.pdf --yes'] + static examples = [ + '<%= config.bin %> files delete invoices/2026/invoice.pdf --yes', + '<%= config.bin %> files delete invoices/2026/invoice.pdf --dry-run', + ] static args = { remote: Args.string({required: true, description: 'Remote file path'}), diff --git a/src/commands/files/list.ts b/src/commands/files/list.ts index 78b1168..121a7f7 100644 --- a/src/commands/files/list.ts +++ b/src/commands/files/list.ts @@ -7,7 +7,7 @@ export default class FilesList extends BaseCommand { static description = 'List files in a folder of a files integration' static examples = [ - '<%= config.bin %> files list --integration 66b2...', + '<%= config.bin %> files list --integration 66b2f0a1c3d4e5f6a7b8c9d0', '<%= config.bin %> files list invoices/2026', ] diff --git a/src/commands/files/publish.ts b/src/commands/files/publish.ts index 31de975..fdca457 100644 --- a/src/commands/files/publish.ts +++ b/src/commands/files/publish.ts @@ -2,7 +2,7 @@ import {Args, Flags} from '@oclif/core' import {BaseCommand} from '../../base.js' import {integrationFlag, resolveIntegration} from '../../lib/files.js' -import {callPublicFiles, publicUrlFor} from '../../lib/publicFiles.js' +import {callPublicFiles, publicFilesRequest, publicUrlFor} from '../../lib/publicFiles.js' export default class FilesPublish extends BaseCommand { static args = { @@ -15,9 +15,11 @@ export default class FilesPublish extends BaseCommand { static examples = [ '<%= config.bin %> files publish invoices/2026/invoice.pdf', '<%= config.bin %> files publish invoices --folder', + '<%= config.bin %> files publish invoices/2026/invoice.pdf --dry-run', ] static flags = { + ...BaseCommand.dryRunFlags, folder: Flags.boolean({ description: 'Publish the whole folder prefix instead of one file', }), @@ -36,11 +38,13 @@ export default class FilesPublish extends BaseCommand { this.error('No files integration ID. Pass --integration or run `norbix config set filesIntegrationId `.') } - const result = await callPublicFiles( - ctx, - flags.folder ? 'makeFolderPublic' : 'makeFilePublic', - {filesIntegrationId: integration, path: args.remote}, - ) + const operation = flags.folder ? 'makeFolderPublic' : 'makeFilePublic' + const body = {filesIntegrationId: integration, path: args.remote} + if (flags['dry-run']) { + return this.dryRun({method: `hub.files.${operation}`, request: body, http: publicFilesRequest(ctx, operation, body)}) + } + + const result = await callPublicFiles(ctx, operation, body) if (!result.id) { this.error('The gateway did not return a public id.') diff --git a/src/commands/files/unpublish.ts b/src/commands/files/unpublish.ts index 3009466..87442e7 100644 --- a/src/commands/files/unpublish.ts +++ b/src/commands/files/unpublish.ts @@ -2,7 +2,7 @@ import {Args, Flags} from '@oclif/core' import {BaseCommand} from '../../base.js' import {integrationFlag, resolveIntegration} from '../../lib/files.js' -import {callPublicFiles} from '../../lib/publicFiles.js' +import {callPublicFiles, publicFilesRequest} from '../../lib/publicFiles.js' export default class FilesUnpublish extends BaseCommand { static args = { @@ -14,9 +14,11 @@ export default class FilesUnpublish extends BaseCommand { static examples = [ '<%= config.bin %> files unpublish invoices/2026/invoice.pdf', '<%= config.bin %> files unpublish invoices --folder', + '<%= config.bin %> files unpublish invoices/2026/invoice.pdf --dry-run', ] static flags = { + ...BaseCommand.dryRunFlags, folder: Flags.boolean({ description: 'Unpublish the whole folder prefix, and every link inside it', }), @@ -33,11 +35,13 @@ export default class FilesUnpublish extends BaseCommand { this.error('No files integration ID. Pass --integration or run `norbix config set filesIntegrationId `.') } - await callPublicFiles( - ctx, - flags.folder ? 'makeFolderPrivate' : 'makeFilePrivate', - {filesIntegrationId: integration, path: args.remote}, - ) + const operation = flags.folder ? 'makeFolderPrivate' : 'makeFilePrivate' + const body = {filesIntegrationId: integration, path: args.remote} + if (flags['dry-run']) { + return this.dryRun({method: `hub.files.${operation}`, request: body, http: publicFilesRequest(ctx, operation, body)}) + } + + await callPublicFiles(ctx, operation, body) this.print( flags.folder diff --git a/src/commands/files/upload.ts b/src/commands/files/upload.ts index 6025d5a..9a8429c 100644 --- a/src/commands/files/upload.ts +++ b/src/commands/files/upload.ts @@ -11,6 +11,7 @@ export default class FilesUpload extends BaseCommand { static examples = [ '<%= config.bin %> files upload ./invoice.pdf', '<%= config.bin %> files upload ./invoice.pdf invoices/2026/invoice.pdf', + '<%= config.bin %> files upload ./invoice.pdf --dry-run', ] static args = { @@ -22,6 +23,7 @@ export default class FilesUpload extends BaseCommand { } static flags = { + ...BaseCommand.dryRunFlags, integration: integrationFlag, 'content-type': Flags.string({description: 'MIME type (default: guessed from extension)'}), } diff --git a/src/commands/logout.ts b/src/commands/logout.ts index 143780c..1b50cae 100644 --- a/src/commands/logout.ts +++ b/src/commands/logout.ts @@ -1,6 +1,6 @@ import {BaseCommand} from '../base.js' -import {clearSession} from '../lib/profiles.js' -import {writeStore} from '../lib/store.js' +import {SESSION_PATH, clearSession} from '../lib/profiles.js' +import {configFilePath, writeStore} from '../lib/store.js' export default class Logout extends BaseCommand { static description = `Remove the login session from this machine. @@ -8,10 +8,20 @@ export default class Logout extends BaseCommand { Profiles in ~/.norbix/config (API keys) are NOT touched — manage those with \`norbix configure\` or by editing the file.` - static examples = ['<%= config.bin %> logout'] + static examples = ['<%= config.bin %> logout', '<%= config.bin %> logout --dry-run'] + + static flags = { + ...BaseCommand.dryRunFlags, + } async run(): Promise { - await this.parse(Logout) + const {flags} = await this.parse(Logout) + if (flags['dry-run']) { + return this.dryRun({ + method: 'logout', + request: {removes: SESSION_PATH, clearsTokensIn: configFilePath(this.config.configDir)}, + }) + } clearSession() diff --git a/src/commands/logs/trail.ts b/src/commands/logs/trail.ts index 301cdcf..421a212 100644 --- a/src/commands/logs/trail.ts +++ b/src/commands/logs/trail.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class LogsTrail extends BaseCommand { static description = 'Show the full request trail for one correlation ID' - static examples = ['<%= config.bin %> logs trail 8f2c1b...'] + static examples = ['<%= config.bin %> logs trail 8f2c1b4d-9e3a-4f7b-a1c2-d3e4f5a6b7c8'] static args = { correlationId: Args.string({required: true, description: 'Correlation ID'}), diff --git a/src/commands/module.ts b/src/commands/module.ts index c4c3ca8..fca73de 100644 --- a/src/commands/module.ts +++ b/src/commands/module.ts @@ -22,6 +22,7 @@ export default class Module extends BaseCommand { static examples = [ '<%= config.bin %> module enable database', '<%= config.bin %> module disable sms --yes', + '<%= config.bin %> module enable database --dry-run', ] static args = { diff --git a/src/commands/payments/enable.ts b/src/commands/payments/enable.ts index e9c79c9..b654440 100644 --- a/src/commands/payments/enable.ts +++ b/src/commands/payments/enable.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../base.js' export default class PaymentsEnable extends BaseCommand { static description = 'Enable a payment trigger' - static examples = ['<%= config.bin %> payments enable 66b2f0a1...'] + static examples = [ + '<%= config.bin %> payments enable 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> payments enable 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Trigger ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(PaymentsEnable) const client = this.client(flags) diff --git a/src/commands/payments/trigger.ts b/src/commands/payments/trigger.ts index 5fa8380..fbe0b9f 100644 --- a/src/commands/payments/trigger.ts +++ b/src/commands/payments/trigger.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class PaymentsTrigger extends BaseCommand { static description = 'Show one payment trigger' - static examples = ['<%= config.bin %> payments trigger 66b2f0a1...'] + static examples = ['<%= config.bin %> payments trigger 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Trigger ID'}), diff --git a/src/commands/push/archive.ts b/src/commands/push/archive.ts index b524bcc..07ae432 100644 --- a/src/commands/push/archive.ts +++ b/src/commands/push/archive.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../base.js' export default class PushArchive extends BaseCommand { static description = 'Archive a push template' - static examples = ['<%= config.bin %> push archive 66b2f0a1...'] + static examples = [ + '<%= config.bin %> push archive 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> push archive 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Template ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(PushArchive) const client = this.client(flags) diff --git a/src/commands/push/campaign.ts b/src/commands/push/campaign.ts index 3a6fc5b..14ec3e7 100644 --- a/src/commands/push/campaign.ts +++ b/src/commands/push/campaign.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class PushCampaign extends BaseCommand { static description = 'Show one push campaign (or its statistics with --stats)' - static examples = ['<%= config.bin %> push campaign 66b2f0a1... --stats'] + static examples = ['<%= config.bin %> push campaign 66b2f0a1c3d4e5f6a7b8c9d0 --stats'] static args = { id: Args.string({required: true, description: 'Campaign ID'}), diff --git a/src/commands/push/campaign/batch.ts b/src/commands/push/campaign/batch.ts index 49b4b56..2c5194a 100644 --- a/src/commands/push/campaign/batch.ts +++ b/src/commands/push/campaign/batch.ts @@ -8,8 +8,8 @@ export default class PushCampaignBatch extends BaseCommand { Give a notification ID as the third argument to see just that notification.` static examples = [ - '<%= config.bin %> push campaign batch 66b2f0a1... b7c3...', - '<%= config.bin %> push campaign batch 66b2f0a1... b7c3... n9d4...', + '<%= config.bin %> push campaign batch 66b2f0a1c3d4e5f6a7b8c9d0 b7c3d4e5f6a7b8c9d0e1f2a3', + '<%= config.bin %> push campaign batch 66b2f0a1c3d4e5f6a7b8c9d0 b7c3d4e5f6a7b8c9d0e1f2a3 n9d4e5f6a7b8c9d0e1f2a3b4', ] static args = { diff --git a/src/commands/push/campaign/batches.ts b/src/commands/push/campaign/batches.ts index b29c9d1..2af70c0 100644 --- a/src/commands/push/campaign/batches.ts +++ b/src/commands/push/campaign/batches.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../../base.js' export default class PushCampaignBatches extends BaseCommand { static description = 'List the send batches of a push campaign' - static examples = ['<%= config.bin %> push campaign batches 66b2f0a1...'] + static examples = ['<%= config.bin %> push campaign batches 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Campaign ID'}), diff --git a/src/commands/push/campaign/create.ts b/src/commands/push/campaign/create.ts index 39878b6..3f442e7 100644 --- a/src/commands/push/campaign/create.ts +++ b/src/commands/push/campaign/create.ts @@ -26,12 +26,14 @@ Without --at the campaign is sent right away. Anything the flags do not cover goes in --config as a JSON object and is merged into the campaign.` static examples = [ - '<%= config.bin %> push campaign create --template 66b2f0a1... --audience all-users --tag beta', - '<%= config.bin %> push campaign create --template 66b2f0a1... --audience users --user usr_1 --user usr_2', - '<%= config.bin %> push campaign create --template 66b2f0a1... --audience devices --device dGVzdA==:Ios', + '<%= config.bin %> push campaign create --template 66b2f0a1c3d4e5f6a7b8c9d0 --audience all-users --tag beta', + '<%= config.bin %> push campaign create --template 66b2f0a1c3d4e5f6a7b8c9d0 --audience users --user usr_1 --user usr_2', + '<%= config.bin %> push campaign create --template 66b2f0a1c3d4e5f6a7b8c9d0 --audience devices --device dGVzdA==:Ios', + '<%= config.bin %> push campaign create --template 66b2f0a1c3d4e5f6a7b8c9d0 --audience all-users --tag beta --dry-run', ] static flags = { + ...BaseCommand.dryRunFlags, template: Flags.string({required: true, description: 'Template ID'}), audience: Flags.string({required: true, description: 'Who receives it', options: Object.keys(AUDIENCES)}), user: Flags.string({description: 'User ID (users / account-users; repeat for several)', multiple: true}), diff --git a/src/commands/push/campaign/delete.ts b/src/commands/push/campaign/delete.ts index 093bb94..0ad3c45 100644 --- a/src/commands/push/campaign/delete.ts +++ b/src/commands/push/campaign/delete.ts @@ -5,7 +5,10 @@ import {BaseCommand} from '../../../base.js' export default class PushCampaignDelete extends BaseCommand { static description = 'Delete a push campaign' - static examples = ['<%= config.bin %> push campaign delete 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> push campaign delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> push campaign delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Campaign ID'}), diff --git a/src/commands/push/campaign/message.ts b/src/commands/push/campaign/message.ts index 428430c..e88b18b 100644 --- a/src/commands/push/campaign/message.ts +++ b/src/commands/push/campaign/message.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../../base.js' export default class PushCampaignMessage extends BaseCommand { static description = 'Show one push message a campaign sent' - static examples = ['<%= config.bin %> push campaign message 66b2f0a1... n9d4... --batch b7c3...'] + static examples = ['<%= config.bin %> push campaign message 66b2f0a1c3d4e5f6a7b8c9d0 n9d4e5f6a7b8c9d0e1f2a3b4 --batch b7c3d4e5f6a7b8c9d0e1f2a3'] static args = { id: Args.string({required: true, description: 'Campaign ID'}), diff --git a/src/commands/push/campaign/messages.ts b/src/commands/push/campaign/messages.ts index 5f6a319..c9a3ec2 100644 --- a/src/commands/push/campaign/messages.ts +++ b/src/commands/push/campaign/messages.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../../base.js' export default class PushCampaignMessages extends BaseCommand { static description = 'List the push messages a campaign sent' - static examples = ['<%= config.bin %> push campaign messages 66b2f0a1... --batch b7c3...'] + static examples = ['<%= config.bin %> push campaign messages 66b2f0a1c3d4e5f6a7b8c9d0 --batch b7c3d4e5f6a7b8c9d0e1f2a3'] static args = { id: Args.string({required: true, description: 'Campaign ID'}), diff --git a/src/commands/push/clone.ts b/src/commands/push/clone.ts index 95681b3..cd2c17f 100644 --- a/src/commands/push/clone.ts +++ b/src/commands/push/clone.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../base.js' export default class PushClone extends BaseCommand { static description = 'Clone a push template' - static examples = ['<%= config.bin %> push clone 66b2f0a1...'] + static examples = [ + '<%= config.bin %> push clone 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> push clone 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Template ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(PushClone) const client = this.client(flags) diff --git a/src/commands/push/delete.ts b/src/commands/push/delete.ts index dbc93d9..ceb5958 100644 --- a/src/commands/push/delete.ts +++ b/src/commands/push/delete.ts @@ -5,7 +5,10 @@ import {BaseCommand} from '../../base.js' export default class PushDelete extends BaseCommand { static description = 'Delete a push template' - static examples = ['<%= config.bin %> push delete 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> push delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> push delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Template ID'}), diff --git a/src/commands/push/device/register.ts b/src/commands/push/device/register.ts index c833c15..666bfa0 100644 --- a/src/commands/push/device/register.ts +++ b/src/commands/push/device/register.ts @@ -10,9 +10,11 @@ system the token came from; the backend uses it to pick a provider.` static examples = [ '<%= config.bin %> push device register --user usr_123 --token dGVzdA== --os iOS', + '<%= config.bin %> push device register --user usr_123 --token dGVzdA== --os iOS --dry-run', ] static flags = { + ...BaseCommand.dryRunFlags, user: Flags.string({required: true, description: 'User ID the device belongs to'}), token: Flags.string({required: true, description: 'Device token from the platform'}), os: Flags.string({required: true, description: 'Operating system, e.g. iOS or Android'}), diff --git a/src/commands/push/disable.ts b/src/commands/push/disable.ts index 69835aa..416b71f 100644 --- a/src/commands/push/disable.ts +++ b/src/commands/push/disable.ts @@ -6,7 +6,10 @@ export default class PushDisable extends BaseCommand { Run \`push disable-dependencies\` first to see what still depends on push.` - static examples = ['<%= config.bin %> push disable --yes'] + static examples = [ + '<%= config.bin %> push disable --yes', + '<%= config.bin %> push disable --dry-run', + ] static flags = { ...BaseCommand.mutatingFlags, diff --git a/src/commands/push/enable.ts b/src/commands/push/enable.ts index ec5be03..a7621fb 100644 --- a/src/commands/push/enable.ts +++ b/src/commands/push/enable.ts @@ -3,7 +3,14 @@ import {BaseCommand} from '../../base.js' export default class PushEnable extends BaseCommand { static description = 'Turn on the push module for the project' - static examples = ['<%= config.bin %> push enable'] + static examples = [ + '<%= config.bin %> push enable', + '<%= config.bin %> push enable --dry-run', + ] + + static flags = { + ...BaseCommand.dryRunFlags, + } async run(): Promise { const {flags} = await this.parse(PushEnable) diff --git a/src/commands/push/integration.ts b/src/commands/push/integration.ts index 22a6065..02dc415 100644 --- a/src/commands/push/integration.ts +++ b/src/commands/push/integration.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class PushIntegration extends BaseCommand { static description = 'Show one push integration' - static examples = ['<%= config.bin %> push integration 66b2f0a1...'] + static examples = ['<%= config.bin %> push integration 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Integration ID'}), diff --git a/src/commands/push/integration/confirm-delivery.ts b/src/commands/push/integration/confirm-delivery.ts index 820d609..33e075e 100644 --- a/src/commands/push/integration/confirm-delivery.ts +++ b/src/commands/push/integration/confirm-delivery.ts @@ -8,12 +8,19 @@ export default class PushIntegrationConfirmDelivery extends BaseCommand { After \`push integration test\`, run this once you have seen the push on the device. It marks the integration as proven to deliver.` - static examples = ['<%= config.bin %> push integration confirm-delivery 66b2f0a1...'] + static examples = [ + '<%= config.bin %> push integration confirm-delivery 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> push integration confirm-delivery 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(PushIntegrationConfirmDelivery) const client = this.client(flags) diff --git a/src/commands/push/integration/default.ts b/src/commands/push/integration/default.ts index c227f52..fa352a0 100644 --- a/src/commands/push/integration/default.ts +++ b/src/commands/push/integration/default.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../../base.js' export default class PushIntegrationDefault extends BaseCommand { static description = 'Make a push integration the default one for this project' - static examples = ['<%= config.bin %> push integration default 66b2f0a1...'] + static examples = [ + '<%= config.bin %> push integration default 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> push integration default 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(PushIntegrationDefault) const client = this.client(flags) diff --git a/src/commands/push/integration/delete.ts b/src/commands/push/integration/delete.ts index ff8d1dd..7c8b923 100644 --- a/src/commands/push/integration/delete.ts +++ b/src/commands/push/integration/delete.ts @@ -5,7 +5,10 @@ import {BaseCommand} from '../../../base.js' export default class PushIntegrationDelete extends BaseCommand { static description = 'Delete a push integration' - static examples = ['<%= config.bin %> push integration delete 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> push integration delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> push integration delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), diff --git a/src/commands/push/integration/enable.ts b/src/commands/push/integration/enable.ts index a043fe2..e9b9192 100644 --- a/src/commands/push/integration/enable.ts +++ b/src/commands/push/integration/enable.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../../base.js' export default class PushIntegrationEnable extends BaseCommand { static description = 'Turn a push integration on' - static examples = ['<%= config.bin %> push integration enable 66b2f0a1...'] + static examples = [ + '<%= config.bin %> push integration enable 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> push integration enable 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(PushIntegrationEnable) const client = this.client(flags) diff --git a/src/commands/push/integration/save.ts b/src/commands/push/integration/save.ts index ef9b84c..29115dd 100644 --- a/src/commands/push/integration/save.ts +++ b/src/commands/push/integration/save.ts @@ -17,9 +17,11 @@ JSON, @file.json, or - for stdin). Pass --id to update an existing integration.` static examples = [ '<%= config.bin %> push integration save --provider Fake', '<%= config.bin %> push integration save --provider AndroidFirebase --name Android --config @firebase.json', + '<%= config.bin %> push integration save --provider Fake --dry-run', ] static flags = { + ...BaseCommand.dryRunFlags, provider: Flags.string({required: true, description: 'Push provider', options: PROVIDERS}), name: Flags.string({description: 'Integration name (Fake ignores it and uses its own)'}), id: Flags.string({description: 'Integration ID — set it to update instead of create'}), diff --git a/src/commands/push/integration/test.ts b/src/commands/push/integration/test.ts index 5be776e..4def92b 100644 --- a/src/commands/push/integration/test.ts +++ b/src/commands/push/integration/test.ts @@ -9,10 +9,12 @@ Point this at the Fake integration while you are developing — it accepts the send and contacts no push service, so nothing reaches a real device.` static examples = [ - '<%= config.bin %> push integration test --integration 66b2f0a1... --token dGVzdA== --family Ios', + '<%= config.bin %> push integration test --integration 66b2f0a1c3d4e5f6a7b8c9d0 --token dGVzdA== --family Ios', + '<%= config.bin %> push integration test --integration 66b2f0a1c3d4e5f6a7b8c9d0 --token dGVzdA== --family Ios --dry-run', ] static flags = { + ...BaseCommand.dryRunFlags, integration: Flags.string({required: true, description: 'Integration ID to send through'}), token: Flags.string({description: 'Device token to send the test to'}), family: Flags.string({ diff --git a/src/commands/push/preview.ts b/src/commands/push/preview.ts index 8bc65f5..9e4358f 100644 --- a/src/commands/push/preview.ts +++ b/src/commands/push/preview.ts @@ -13,8 +13,8 @@ The signed link is the key: no login and no project are needed. When you are logged in, your session is still sent.` static examples = [ - '<%= config.bin %> push preview 8f2a91c4...', - '<%= config.bin %> push preview --hash 8f2a91c4... --region nb-eu-germany', + '<%= config.bin %> push preview 8f2a91c4d7e6b5a4c3d2e1f0', + '<%= config.bin %> push preview --hash 8f2a91c4d7e6b5a4c3d2e1f0 --region nb-eu-germany', ] static args = { diff --git a/src/commands/push/stop.ts b/src/commands/push/stop.ts index ecbcf82..3fdfe03 100644 --- a/src/commands/push/stop.ts +++ b/src/commands/push/stop.ts @@ -5,7 +5,10 @@ import {BaseCommand} from '../../base.js' export default class PushStop extends BaseCommand { static description = 'Stop a running push campaign' - static examples = ['<%= config.bin %> push stop 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> push stop 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> push stop 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Campaign ID'}), diff --git a/src/commands/push/template.ts b/src/commands/push/template.ts index 6f539c0..1f96e4a 100644 --- a/src/commands/push/template.ts +++ b/src/commands/push/template.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class PushTemplate extends BaseCommand { static description = 'Show one push template' - static examples = ['<%= config.bin %> push template 66b2f0a1...'] + static examples = ['<%= config.bin %> push template 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Template ID'}), diff --git a/src/commands/push/template/create.ts b/src/commands/push/template/create.ts index 0a71901..45c4de5 100644 --- a/src/commands/push/template/create.ts +++ b/src/commands/push/template/create.ts @@ -10,9 +10,13 @@ JSON array of {language, content: {title, body}}.` static examples = [ '<%= config.bin %> push template create --name Welcome --title "Hi @Model.Name" --body "Thanks for joining"', '<%= config.bin %> push template create --name Welcome --translations @welcome.json', + '<%= config.bin %> push template create --name Welcome --title "Hi @Model.Name" --body "Thanks for joining" --dry-run', ] - static flags = templateFlags + static flags = { + ...BaseCommand.dryRunFlags, + ...templateFlags, + } async run(): Promise { const {flags} = await this.parse(PushTemplateCreate) diff --git a/src/commands/push/template/tokens.ts b/src/commands/push/template/tokens.ts index 66ab40b..d448c2e 100644 --- a/src/commands/push/template/tokens.ts +++ b/src/commands/push/template/tokens.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../../base.js' export default class PushTemplateTokens extends BaseCommand { static description = 'List the tokens a push template uses — the values a campaign must fill' - static examples = ['<%= config.bin %> push template tokens 66b2f0a1...'] + static examples = ['<%= config.bin %> push template tokens 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Template ID'}), diff --git a/src/commands/push/template/update.ts b/src/commands/push/template/update.ts index 1fcd861..4ac188b 100644 --- a/src/commands/push/template/update.ts +++ b/src/commands/push/template/update.ts @@ -9,14 +9,18 @@ export default class PushTemplateUpdate extends BaseCommand { The whole template is sent, so pass every language you want to keep.` static examples = [ - '<%= config.bin %> push template update 66b2f0a1... --name Welcome --title "Hi" --body "Thanks for joining"', + '<%= config.bin %> push template update 66b2f0a1c3d4e5f6a7b8c9d0 --name Welcome --title "Hi" --body "Thanks for joining"', + '<%= config.bin %> push template update 66b2f0a1c3d4e5f6a7b8c9d0 --name Welcome --title "Hi" --body "Thanks for joining" --dry-run', ] static args = { id: Args.string({required: true, description: 'Template ID'}), } - static flags = templateFlags + static flags = { + ...BaseCommand.dryRunFlags, + ...templateFlags, + } async run(): Promise { const {args, flags} = await this.parse(PushTemplateUpdate) diff --git a/src/commands/push/unarchive.ts b/src/commands/push/unarchive.ts index 46f9d55..85c8b6c 100644 --- a/src/commands/push/unarchive.ts +++ b/src/commands/push/unarchive.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../base.js' export default class PushUnarchive extends BaseCommand { static description = 'Restore an archived push template' - static examples = ['<%= config.bin %> push unarchive 66b2f0a1...'] + static examples = [ + '<%= config.bin %> push unarchive 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> push unarchive 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Template ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(PushUnarchive) const client = this.client(flags) diff --git a/src/commands/raw.ts b/src/commands/raw.ts index b649bab..c7a34e0 100644 --- a/src/commands/raw.ts +++ b/src/commands/raw.ts @@ -17,9 +17,9 @@ plus project/account/env/region headers automatically. Use {version} in the path and it becomes v2.` static examples = [ - '<%= config.bin %> api /v2/logs/settings --hub', - `<%= config.bin %> api '/{version}/database/collections/orders/count' --query filter='{"status":"paid"}'`, - `<%= config.bin %> api /v2/scheduler/tasks --hub --method POST --body '{"name":"nightly",...}'`, + '<%= config.bin %> raw /v2/logs/settings', + `<%= config.bin %> raw '/{version}/database/collections/orders/count' --api --query filter='{"status":"paid"}'`, + `<%= config.bin %> raw /v2/scheduler/tasks --method POST --body '{"name":"nightly","cronExpression":"0 2 * * *"}' --dry-run`, ] static args = { @@ -27,6 +27,7 @@ path and it becomes v2.` } static flags = { + ...BaseCommand.dryRunFlags, api: Flags.boolean({description: 'Call the data-plane API service instead of the hub (hub is the default)', default: false}), method: Flags.string({char: 'X', description: 'HTTP method', default: 'GET', options: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']}), body: Flags.string({char: 'b', description: 'JSON request body (or `-` for stdin)'}), @@ -73,6 +74,14 @@ path and it becomes v2.` headers['Content-Type'] = 'application/json' } + if (flags['dry-run']) { + return this.dryRun({ + method: `raw ${flags.method} ${path}`, + request: body === undefined ? {} : (JSON.parse(body) as unknown), + http: {method: flags.method, url: url.toString(), headers: {...headers, Authorization: 'Bearer ***'}, body: body === undefined ? undefined : (JSON.parse(body) as unknown)}, + }) + } + const res = await fetch(url, {method: flags.method, headers, body}) const text = await res.text() let data: unknown diff --git a/src/commands/scheduler/delete.ts b/src/commands/scheduler/delete.ts index db463a7..546d903 100644 --- a/src/commands/scheduler/delete.ts +++ b/src/commands/scheduler/delete.ts @@ -5,7 +5,10 @@ import {BaseCommand} from '../../base.js' export default class SchedulerDelete extends BaseCommand { static description = 'Delete a scheduler task' - static examples = ['<%= config.bin %> scheduler delete 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> scheduler delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> scheduler delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Task ID'}), diff --git a/src/commands/scheduler/enable.ts b/src/commands/scheduler/enable.ts index 7597e7c..6759eda 100644 --- a/src/commands/scheduler/enable.ts +++ b/src/commands/scheduler/enable.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../base.js' export default class SchedulerEnable extends BaseCommand { static description = 'Enable a scheduler task' - static examples = ['<%= config.bin %> scheduler enable 66b2f0a1...'] + static examples = [ + '<%= config.bin %> scheduler enable 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> scheduler enable 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Task ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(SchedulerEnable) const client = this.client(flags) diff --git a/src/commands/scheduler/get.ts b/src/commands/scheduler/get.ts index 0a3f01f..32c0f4a 100644 --- a/src/commands/scheduler/get.ts +++ b/src/commands/scheduler/get.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class SchedulerGet extends BaseCommand { static description = 'Show one scheduler task' - static examples = ['<%= config.bin %> scheduler get 66b2f0a1...'] + static examples = ['<%= config.bin %> scheduler get 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Task ID'}), diff --git a/src/commands/sms/archive.ts b/src/commands/sms/archive.ts index dc49c87..e7a264e 100644 --- a/src/commands/sms/archive.ts +++ b/src/commands/sms/archive.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../base.js' export default class SmsArchive extends BaseCommand { static description = 'Archive an SMS template' - static examples = ['<%= config.bin %> sms archive 66b2f0a1...'] + static examples = [ + '<%= config.bin %> sms archive 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> sms archive 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Template ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(SmsArchive) const client = this.client(flags) diff --git a/src/commands/sms/campaign.ts b/src/commands/sms/campaign.ts index c5f9d59..0d57cb0 100644 --- a/src/commands/sms/campaign.ts +++ b/src/commands/sms/campaign.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class SmsCampaign extends BaseCommand { static description = 'Show one SMS campaign (or its statistics with --stats)' - static examples = ['<%= config.bin %> sms campaign 66b2f0a1... --stats'] + static examples = ['<%= config.bin %> sms campaign 66b2f0a1c3d4e5f6a7b8c9d0 --stats'] static args = { id: Args.string({required: true, description: 'Campaign ID'}), diff --git a/src/commands/sms/clone.ts b/src/commands/sms/clone.ts index bbc745a..3bd3477 100644 --- a/src/commands/sms/clone.ts +++ b/src/commands/sms/clone.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../base.js' export default class SmsClone extends BaseCommand { static description = 'Clone an SMS template' - static examples = ['<%= config.bin %> sms clone 66b2f0a1...'] + static examples = [ + '<%= config.bin %> sms clone 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> sms clone 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Template ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(SmsClone) const client = this.client(flags) diff --git a/src/commands/sms/delete.ts b/src/commands/sms/delete.ts index de2be30..63fc90f 100644 --- a/src/commands/sms/delete.ts +++ b/src/commands/sms/delete.ts @@ -5,7 +5,10 @@ import {BaseCommand} from '../../base.js' export default class SmsDelete extends BaseCommand { static description = 'Delete an SMS template' - static examples = ['<%= config.bin %> sms delete 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> sms delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> sms delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Template ID'}), diff --git a/src/commands/sms/stop.ts b/src/commands/sms/stop.ts index ace4ce7..e3bdcd1 100644 --- a/src/commands/sms/stop.ts +++ b/src/commands/sms/stop.ts @@ -5,7 +5,10 @@ import {BaseCommand} from '../../base.js' export default class SmsStop extends BaseCommand { static description = 'Stop a running SMS campaign' - static examples = ['<%= config.bin %> sms stop 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> sms stop 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> sms stop 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Campaign ID'}), diff --git a/src/commands/sms/template.ts b/src/commands/sms/template.ts index 1c74bc9..74b5ade 100644 --- a/src/commands/sms/template.ts +++ b/src/commands/sms/template.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class SmsTemplate extends BaseCommand { static description = 'Show one SMS template' - static examples = ['<%= config.bin %> sms template 66b2f0a1...'] + static examples = ['<%= config.bin %> sms template 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Template ID'}), diff --git a/src/commands/sms/unarchive.ts b/src/commands/sms/unarchive.ts index 24aa708..84f0c22 100644 --- a/src/commands/sms/unarchive.ts +++ b/src/commands/sms/unarchive.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../base.js' export default class SmsUnarchive extends BaseCommand { static description = 'Restore an archived SMS template' - static examples = ['<%= config.bin %> sms unarchive 66b2f0a1...'] + static examples = [ + '<%= config.bin %> sms unarchive 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> sms unarchive 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Template ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(SmsUnarchive) const client = this.client(flags) diff --git a/src/commands/users/delete.ts b/src/commands/users/delete.ts index 9e74461..fb9d571 100644 --- a/src/commands/users/delete.ts +++ b/src/commands/users/delete.ts @@ -5,7 +5,10 @@ import {BaseCommand} from '../../base.js' export default class UsersDelete extends BaseCommand { static description = 'Delete a user' - static examples = ['<%= config.bin %> users delete 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> users delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> users delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'User ID'}), diff --git a/src/commands/users/get.ts b/src/commands/users/get.ts index ee080ea..58662d3 100644 --- a/src/commands/users/get.ts +++ b/src/commands/users/get.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class UsersGet extends BaseCommand { static description = 'Show one user' - static examples = ['<%= config.bin %> users get 66b2f0a1...'] + static examples = ['<%= config.bin %> users get 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'User ID'}), diff --git a/src/commands/users/invite.ts b/src/commands/users/invite.ts index 45fbd56..dafa365 100644 --- a/src/commands/users/invite.ts +++ b/src/commands/users/invite.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../base.js' export default class UsersInvite extends BaseCommand { static description = 'Invite a user by email' - static examples = ['<%= config.bin %> users invite alice@example.com'] + static examples = [ + '<%= config.bin %> users invite alice@example.com', + '<%= config.bin %> users invite alice@example.com --dry-run', + ] static args = { email: Args.string({required: true, description: 'Email address to invite'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(UsersInvite) const client = this.client(flags) diff --git a/src/commands/users/unblock.ts b/src/commands/users/unblock.ts index 7942783..df9ea3a 100644 --- a/src/commands/users/unblock.ts +++ b/src/commands/users/unblock.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../base.js' export default class UsersUnblock extends BaseCommand { static description = 'Unblock a user' - static examples = ['<%= config.bin %> users unblock 66b2f0a1...'] + static examples = [ + '<%= config.bin %> users unblock 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> users unblock 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'User ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(UsersUnblock) const client = this.client(flags) diff --git a/src/commands/webhooks/enable.ts b/src/commands/webhooks/enable.ts index 2ac2aef..c8be59a 100644 --- a/src/commands/webhooks/enable.ts +++ b/src/commands/webhooks/enable.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../base.js' export default class WebhooksEnable extends BaseCommand { static description = 'Enable a webhook destination' - static examples = ['<%= config.bin %> webhooks enable 66b2f0a1...'] + static examples = [ + '<%= config.bin %> webhooks enable 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> webhooks enable 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { destinationId: Args.string({required: true, description: 'Destination ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(WebhooksEnable) const client = this.client(flags) diff --git a/src/commands/webhooks/remove.ts b/src/commands/webhooks/remove.ts index ffe3168..d731ef7 100644 --- a/src/commands/webhooks/remove.ts +++ b/src/commands/webhooks/remove.ts @@ -5,7 +5,10 @@ import {BaseCommand} from '../../base.js' export default class WebhooksRemove extends BaseCommand { static description = 'Remove a webhook destination' - static examples = ['<%= config.bin %> webhooks remove 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> webhooks remove 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> webhooks remove 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { destinationId: Args.string({required: true, description: 'Destination ID'}), diff --git a/src/commands/webhooks/secret.ts b/src/commands/webhooks/secret.ts index bd939b1..037a88b 100644 --- a/src/commands/webhooks/secret.ts +++ b/src/commands/webhooks/secret.ts @@ -8,6 +8,7 @@ export default class WebhooksSecret extends BaseCommand { static examples = [ '<%= config.bin %> webhooks secret', '<%= config.bin %> webhooks secret --rotate --yes', + '<%= config.bin %> webhooks secret --dry-run', ] static flags = { diff --git a/src/lib/publicFiles.ts b/src/lib/publicFiles.ts index 4520594..7e813a1 100644 --- a/src/lib/publicFiles.ts +++ b/src/lib/publicFiles.ts @@ -42,6 +42,22 @@ export interface PublicFilesResult { /** The Hub version the SDK talks by default. Kept in one place. */ const HUB_VERSION = 'v2' +/** The exact request `callPublicFiles` sends — for `--dry-run`. */ +export function publicFilesRequest( + ctx: ResolvedContext, + operation: PublicFilesOperation, + body: {filesIntegrationId: string; path: string}, +): {method: string; url: string; headers: Record; body: unknown} { + const base = ctx.hubUrl.endsWith('/') ? ctx.hubUrl.slice(0, -1) : ctx.hubUrl + const headers: Record = {Accept: 'application/json', 'Content-Type': 'application/json'} + if (ctx.bearerToken ?? ctx.apiKey) headers.Authorization = 'Bearer ***' + if (ctx.projectId) headers['X-CM-ProjectId'] = ctx.projectId + if (ctx.accountId) headers['X-CM-AccountId'] = ctx.accountId + if (ctx.env && ctx.env !== 'PROD') headers['norbix-env'] = ctx.env + if (ctx.region) headers['nb-region'] = ctx.region + return {method: 'POST', url: `${base}/${HUB_VERSION}/${ROUTES[operation]}`, headers, body} +} + export async function callPublicFiles( ctx: ResolvedContext, operation: PublicFilesOperation, From 5d46b3891619514d494f8ea8a75a55ed6bf99275 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Fri, 2 Oct 2026 12:50:33 +0300 Subject: [PATCH 07/14] test(cli): schema snapshot, parser typing, dry-run on mutating commands Plan step [2] of docs/tasks/agent-native-cli.md. - schema: lists every file in dist/commands, booleans present, destructive implies dry-run, every mutating command has a --dry-run example and no placeholder values, one command / one topic / unknown, snapshot of ids, args, flags and markers. - dispatch: string keeps zeros, number rejects text, boolean gives the next token back, explicit types, JSON fields, destructive verbs. - agent mode: dry run on users invite, files publish (raw hub request), config set (file untouched), hub typed fields, --body and its mixing error. Co-Authored-By: Claude Fable 5.1 --- tests/__snapshots__/schema.test.ts.snap | 1420 +++++++++++++++++++++++ tests/agent-mode.test.ts | 51 + tests/dispatch.test.ts | 78 ++ tests/schema.test.ts | 146 +++ 4 files changed, 1695 insertions(+) create mode 100644 tests/__snapshots__/schema.test.ts.snap create mode 100644 tests/dispatch.test.ts create mode 100644 tests/schema.test.ts diff --git a/tests/__snapshots__/schema.test.ts.snap b/tests/__snapshots__/schema.test.ts.snap new file mode 100644 index 0000000..1399e1d --- /dev/null +++ b/tests/__snapshots__/schema.test.ts.snap @@ -0,0 +1,1420 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` +[ + { + "args": [], + "destructive": false, + "flags": [ + "type:string", + "return-url:string", + ], + "id": "account billing-portal", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "account profile", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "account projects", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "account regions", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "account status", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "account team", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "account usage", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + "body:string", + ], + "id": "api", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "apikeys list", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "apikeys regenerate", + "supportsDryRun": true, + }, + { + "args": [ + "key", + ], + "destructive": false, + "flags": [], + "id": "config get", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "config list", + "supportsDryRun": false, + }, + { + "args": [ + "key", + "value", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "config set", + "supportsDryRun": true, + }, + { + "args": [ + "key", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "config unset", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "api-url:string", + "hub-url:string", + ], + "id": "configure", + "supportsDryRun": false, + }, + { + "args": [ + "collection", + ], + "destructive": false, + "flags": [ + "pipeline:string", + "id:string", + "tokens:string", + ], + "id": "db aggregate", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "schema:string", + ], + "id": "db aggregates", + "supportsDryRun": false, + }, + { + "args": [ + "collection", + ], + "destructive": false, + "flags": [ + "filter:string", + ], + "id": "db count", + "supportsDryRun": false, + }, + { + "args": [ + "collection", + ], + "destructive": true, + "flags": [ + "id:string", + "filter:string", + "many:boolean", + "yes:boolean", + "dry-run:boolean", + ], + "id": "db delete", + "supportsDryRun": true, + }, + { + "args": [ + "collection", + "field", + ], + "destructive": false, + "flags": [ + "filter:string", + ], + "id": "db distinct", + "supportsDryRun": false, + }, + { + "args": [ + "collection", + ], + "destructive": false, + "flags": [ + "filter:string", + "page-size:string", + "after:string", + "before:string", + ], + "id": "db find", + "supportsDryRun": false, + }, + { + "args": [ + "collection", + "id", + ], + "destructive": false, + "flags": [], + "id": "db get", + "supportsDryRun": false, + }, + { + "args": [ + "collection", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + "doc:string", + ], + "id": "db insert", + "supportsDryRun": true, + }, + { + "args": [ + "collection", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + "docs:string", + ], + "id": "db insert-many", + "supportsDryRun": true, + }, + { + "args": [ + "collection", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + "id:string", + "doc:string", + ], + "id": "db replace", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "db schema", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "page-size:string", + "after:string", + ], + "id": "db schemas", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "page-size:string", + "after:string", + ], + "id": "db taxonomies", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "db taxonomy", + "supportsDryRun": false, + }, + { + "args": [ + "collection", + ], + "destructive": true, + "flags": [ + "id:string", + "filter:string", + "update:string", + "many:boolean", + "yes:boolean", + "dry-run:boolean", + ], + "id": "db update", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "email archive", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "stats:boolean", + ], + "id": "email campaign", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "page-size:string", + "after:string", + ], + "id": "email campaigns", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "email clone", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "email delete", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "email stop", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "email template", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "archived:boolean", + ], + "id": "email templates", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "email unarchive", + "supportsDryRun": true, + }, + { + "args": [ + "name", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "env delete", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "env list", + "supportsDryRun": false, + }, + { + "args": [ + "name", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "env use", + "supportsDryRun": true, + }, + { + "args": [ + "remote", + ], + "destructive": true, + "flags": [ + "integration:string", + "yes:boolean", + "dry-run:boolean", + ], + "id": "files delete", + "supportsDryRun": true, + }, + { + "args": [ + "remote", + "[file]", + ], + "destructive": false, + "flags": [ + "integration:string", + ], + "id": "files download", + "supportsDryRun": false, + }, + { + "args": [ + "remote", + ], + "destructive": false, + "flags": [ + "integration:string", + ], + "id": "files info", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "files integrations test", + "supportsDryRun": false, + }, + { + "args": [ + "[path]", + ], + "destructive": false, + "flags": [ + "integration:string", + ], + "id": "files list", + "supportsDryRun": false, + }, + { + "args": [ + "remote", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + "folder:boolean", + "integration:string", + ], + "id": "files publish", + "supportsDryRun": true, + }, + { + "args": [ + "remote", + ], + "destructive": false, + "flags": [ + "integration:string", + "expires:string", + ], + "id": "files sign", + "supportsDryRun": false, + }, + { + "args": [ + "remote", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + "folder:boolean", + "integration:string", + ], + "id": "files unpublish", + "supportsDryRun": true, + }, + { + "args": [ + "file", + "[remote]", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + "integration:string", + "content-type:string", + ], + "id": "files upload", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + "body:string", + ], + "id": "hub", + "supportsDryRun": true, + }, + { + "args": [ + "module", + ], + "destructive": false, + "flags": [], + "id": "integrations", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "user:string", + "password:string", + ], + "id": "login", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "logout", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "level:string", + "module:string", + "search:string", + "event-code:string", + "correlation:string", + "from:string", + "to:string", + "page-size:string", + "after:string", + ], + "id": "logs list", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "logs settings", + "supportsDryRun": false, + }, + { + "args": [ + "correlationId", + ], + "destructive": false, + "flags": [], + "id": "logs trail", + "supportsDryRun": false, + }, + { + "args": [ + "action", + "name", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "module", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "payments disable", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "payments enable", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "payments integrations", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "payments trigger", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "payments triggers", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "profiles", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "push archive", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "stats:boolean", + ], + "id": "push campaign", + "supportsDryRun": false, + }, + { + "args": [ + "id", + "batchId", + "[notificationId]", + ], + "destructive": false, + "flags": [ + "page-size:string", + "after:string", + ], + "id": "push campaign batch", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "page-size:string", + "after:string", + ], + "id": "push campaign batches", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "template:string", + "audience:option", + "user:string", + "role:string", + "tag:string", + "schema:string", + "field:string", + "field-type:option", + "device:string", + "integration:string", + "language:string", + "notes:string", + "at:string", + "token:string", + "database-integration:string", + "config:string", + ], + "id": "push campaign create", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "push campaign delete", + "supportsDryRun": true, + }, + { + "args": [ + "id", + "messageId", + ], + "destructive": false, + "flags": [ + "batch:string", + ], + "id": "push campaign message", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "batch:string", + "page-size:string", + "after:string", + ], + "id": "push campaign messages", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "page-size:string", + "after:string", + ], + "id": "push campaigns", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "push clone", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "push delete", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "push device", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "user:string", + "token:string", + "os:string", + "device-id:string", + "model:string", + "name:string", + ], + "id": "push device register", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "user:string", + "token:string", + "platform:option", + ], + "id": "push devices", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "push disable", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "push disable-dependencies", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "push enable", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "push integration", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "push integration confirm-delivery", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "push integration default", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "push integration delete", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "push integration disable", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "push integration enable", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "provider:option", + "name:string", + "id:string", + "disabled:boolean", + "config:string", + ], + "id": "push integration save", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "integration:string", + "token:string", + "family:option", + ], + "id": "push integration test", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "page-size:string", + "after:string", + ], + "id": "push integrations", + "supportsDryRun": false, + }, + { + "args": [ + "[hash]", + ], + "destructive": false, + "flags": [ + "hash:string", + ], + "id": "push preview", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "id:string", + ], + "id": "push settings", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "push stop", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "push template", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "name:string", + "description:string", + "channel:option", + "tag:string", + "language:string", + "title:string", + "body:string", + "translations:string", + ], + "id": "push template create", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "code:string", + "token:string", + "preview:boolean", + ], + "id": "push template render", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "push template tokens", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + "name:string", + "description:string", + "channel:option", + "tag:string", + "language:string", + "title:string", + "body:string", + "translations:string", + ], + "id": "push template update", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "archived:boolean", + ], + "id": "push templates", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "push unarchive", + "supportsDryRun": true, + }, + { + "args": [ + "path", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + "api:boolean", + "method:option", + "body:string", + "query:string", + ], + "id": "raw", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "scheduler delete", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "scheduler disable", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "scheduler enable", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "scheduler get", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "page-size:string", + "after:string", + ], + "id": "scheduler list", + "supportsDryRun": false, + }, + { + "args": [ + "[command]", + ], + "destructive": false, + "flags": [], + "id": "schema", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "sms archive", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "stats:boolean", + ], + "id": "sms campaign", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "page-size:string", + "after:string", + ], + "id": "sms campaigns", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "sms clone", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "sms delete", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "sms stop", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "sms template", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "archived:boolean", + ], + "id": "sms templates", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "sms unarchive", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "users block", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "users delete", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "users get", + "supportsDryRun": false, + }, + { + "args": [ + "email", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "users invite", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "role:string", + "permissions:boolean", + "page-size:string", + "after:string", + ], + "id": "users list", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "users policies", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "users roles", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "users unblock", + "supportsDryRun": true, + }, + { + "args": [ + "destinationId", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "webhooks disable", + "supportsDryRun": true, + }, + { + "args": [ + "destinationId", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "webhooks enable", + "supportsDryRun": true, + }, + { + "args": [ + "destinationId", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "webhooks remove", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": true, + "flags": [ + "rotate:boolean", + "yes:boolean", + "dry-run:boolean", + ], + "id": "webhooks secret", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "webhooks show", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "whoami", + "supportsDryRun": false, + }, +] +`; diff --git a/tests/agent-mode.test.ts b/tests/agent-mode.test.ts index 2e7aec9..6d9dc83 100644 --- a/tests/agent-mode.test.ts +++ b/tests/agent-mode.test.ts @@ -269,3 +269,54 @@ describe('plain output', () => { expect(r.stdout.trim()).toBe('User abc123 deleted.') }) }) + +describe('--dry-run on non-destructive mutating commands', () => { + it('users invite: prints the call and sends nothing', async () => { + const r = await cli(home, ['users', 'invite', 'alice@example.com', '--profile', 'x', '--dry-run', '--json']) + expect(r.code).toBe(0) + const doc = parseSingleJson(r.stdout) as {method: string; request: unknown; http: {method: string}} + expect(doc.method).toBe('api.membership.inviteUser') + expect(doc.request).toEqual({email: 'alice@example.com'}) + expect(doc.http.method).toBe('POST') + expect(gateway.hits).toEqual([]) + }) + + it('files publish: shows the raw hub request it would send', async () => { + const r = await cli(home, ['files', 'publish', 'invoices/2026/invoice.pdf', '--integration', 'fi_1', '--profile', 'x', '--dry-run', '--json']) + expect(r.code).toBe(0) + const doc = parseSingleJson(r.stdout) as {method: string; http: {method: string; url: string; headers: Record; body: unknown}} + expect(doc.method).toBe('hub.files.makeFilePublic') + expect(doc.http.method).toBe('POST') + expect(doc.http.url).toMatch(/\/v2\/files\/item\/public$/) + expect(doc.http.headers.Authorization).toBe('Bearer ***') + expect(doc.http.body).toEqual({filesIntegrationId: 'fi_1', path: 'invoices/2026/invoice.pdf'}) + expect(gateway.hits).toEqual([]) + }) + + it('config set: shows the file it would write and writes nothing', async () => { + const r = await cli(home, ['config', 'set', 'region', 'nb-eu-germany', '--dry-run', '--json']) + expect(r.code).toBe(0) + const doc = parseSingleJson(r.stdout) as {dryRun: boolean; method: string; request: {key: string; value: string; file: string}} + expect(doc).toMatchObject({dryRun: true, method: 'config.set', request: {key: 'region', value: 'nb-eu-germany'}}) + const after = await cli(home, ['config', 'get', 'region', '--json']) + expect(parseSingleJson(after.stdout)).toEqual({key: 'region'}) + }) + + it('hub: a boolean field never swallows the id, a string field keeps its zeros', async () => { + const r = await cli(home, ['hub', 'email', 'templates', 'get', '--showArchived', 'tpl_1', '--templateId:str', '0042', '--profile', 'x', '--dry-run', '--json']) + expect(r.code).toBe(0) + const doc = parseSingleJson(r.stdout) as {request: unknown} + expect(doc.request).toEqual({showArchived: true, templateId: '0042', id: 'tpl_1'}) + }) + + it('hub --body: the whole request as JSON, no mixing with --field', async () => { + const ok = await cli(home, ['hub', 'scheduler', 'task', 'delete', '--body', '{"id":"abc123"}', '--profile', 'x', '--dry-run', '--json']) + expect(ok.code).toBe(0) + expect((parseSingleJson(ok.stdout) as {request: unknown}).request).toEqual({id: 'abc123'}) + + const mixed = await cli(home, ['hub', 'scheduler', 'task', 'delete', '--body', '{"id":"abc123"}', '--name', 'x', '--profile', 'x', '--json']) + expect(mixed.code).toBe(2) + expect((parseSingleJson(mixed.stdout) as {error: {message: string}}).error.message).toMatch(/--body carries the whole request/) + expect(gateway.hits).toEqual([]) + }) +}) diff --git a/tests/dispatch.test.ts b/tests/dispatch.test.ts new file mode 100644 index 0000000..fda849f --- /dev/null +++ b/tests/dispatch.test.ts @@ -0,0 +1,78 @@ +import {describe, expect, it} from 'vitest' + +import {isDestructive, matchMethods, parseArgv, typeFields} from '../src/lib/dispatch.js' + +describe('parseArgv', () => { + it('splits words, the id and the fields', () => { + const p = parseArgv(['database', 'aggregates', 'delete', 'maggr_123', '--schemaId', 'sch_456']) + expect(p.words).toEqual(['database', 'aggregates', 'delete']) + expect(p.positionals).toEqual(['maggr_123']) + expect(p.rawFields).toEqual([{name: 'schemaId', value: 'sch_456', fromNextToken: true, explicit: undefined}]) + }) + + it('reads an explicit type suffix', () => { + const p = parseArgv(['--userId:str', '0042', '--count:num=7', '--archived:bool']) + expect(p.rawFields).toEqual([ + {name: 'userId', value: '0042', fromNextToken: true, explicit: 'str'}, + {name: 'count', value: '7', fromNextToken: false, explicit: 'num'}, + {name: 'archived', fromNextToken: false, explicit: 'bool'}, + ]) + }) +}) + +describe('typeFields', () => { + it('keeps a string field as text even when it looks like a number', () => { + const {fields} = typeFields(parseArgv(['--userId', '0042']).rawFields, {userId: 'string'}) + expect(fields).toEqual({userId: '0042'}) + }) + + it('converts a number field and rejects text in it', () => { + expect(typeFields(parseArgv(['--pageSize', '100']).rawFields, {pageSize: 'number'}).fields).toEqual({pageSize: 100}) + expect(typeFields(parseArgv(['--pageSize', 'many']).rawFields, {pageSize: 'number'}).errors).toEqual(['--pageSize expects a number, got "many"']) + }) + + it('does not let a boolean flag swallow the next word', () => { + const {fields, returned} = typeFields(parseArgv(['--showArchived', 'tpl_1']).rawFields, {showArchived: 'boolean'}) + expect(fields).toEqual({showArchived: true}) + expect(returned).toEqual(['tpl_1']) + }) + + it('still takes true/false for a boolean flag', () => { + expect(typeFields(parseArgv(['--showArchived', 'false']).rawFields, {showArchived: 'boolean'}).fields).toEqual({showArchived: false}) + }) + + it('lets an explicit type win over the map and the heuristic', () => { + const {fields} = typeFields(parseArgv(['--id:str', '0042', '--n:num', '7', '--ok:bool', 'false', '--doc:json', '{"a":1}']).rawFields, {id: 'number'}) + expect(fields).toEqual({id: '0042', n: 7, ok: false, doc: {a: 1}}) + }) + + it('reports a bad explicit value', () => { + expect(typeFields(parseArgv(['--n:num', 'x']).rawFields, {}).errors).toEqual(['--n:num expects a number, got "x"']) + expect(typeFields(parseArgv(['--doc:json', '{bad']).rawFields, {}).errors[0]).toMatch(/not valid JSON/) + }) + + it('falls back to the old heuristic for unknown fields', () => { + expect(typeFields(parseArgv(['--a', 'true', '--b', '42', '--c', 'x']).rawFields, {}).fields).toEqual({a: true, b: 42, c: 'x'}) + }) + + it('parses JSON for object fields and splits string lists', () => { + const {fields} = typeFields(parseArgv(['--paging', '{"pageSize":5}', '--tags', 'a,b']).rawFields, {paging: 'json', tags: 'string[]'}) + expect(fields).toEqual({paging: {pageSize: 5}, tags: ['a', 'b']}) + }) +}) + +describe('method matching', () => { + const methods = ['getSchedulerTask', 'getSchedulerTasks', 'deleteSchedulerTask', 'disableSchedulerTask', 'enableSchedulerTask'] + + it('plural lists, singular gets one', () => { + expect(matchMethods(methods, ['tasks', 'get'], 'scheduler').map((m) => m.method)).toEqual(['getSchedulerTasks']) + expect(matchMethods(methods, ['task', 'get'], 'scheduler').map((m) => m.method)).toEqual(['getSchedulerTask']) + }) + + it('treats delete, disable and block as destructive', () => { + expect(isDestructive('deleteSchedulerTask')).toBe(true) + expect(isDestructive('disableSchedulerTask')).toBe(true) + expect(isDestructive('blockUser')).toBe(true) + expect(isDestructive('enableSchedulerTask')).toBe(false) + }) +}) diff --git a/tests/schema.test.ts b/tests/schema.test.ts new file mode 100644 index 0000000..1bd28d0 --- /dev/null +++ b/tests/schema.test.ts @@ -0,0 +1,146 @@ +import {readdirSync, statSync} from 'node:fs' +import {join} from 'node:path' +import {fileURLToPath} from 'node:url' + +import {afterAll, beforeAll, describe, expect, it} from 'vitest' + +import {cli, makeHome, parseSingleJson, startFakeGateway, type FakeGateway} from './_cli.js' + +/** Every command file under dist/commands, as the id a user types. */ +function commandIdsOnDisk(): string[] { + const root = fileURLToPath(new URL('../dist/commands', import.meta.url)) + const walk = (dir: string, prefix: string[]): string[] => + readdirSync(dir).flatMap((name) => { + const full = join(dir, name) + if (statSync(full).isDirectory()) return walk(full, [...prefix, name]) + return name.endsWith('.js') ? [[...prefix, name.slice(0, -3)].join(' ')] : [] + }) + return walk(root, []).sort() +} + +interface SchemaDoc { + cli: string + version: string + contract: string + exitCodes: Record + commands: Array<{ + id: string + description: string + destructive: boolean + supportsDryRun: boolean + args: Array<{name: string; required: boolean}> + flags: Array<{name: string; type: string; global?: boolean; env?: string}> + examples: string[] + }> + dynamic: {hub: string; api: string; valueRules: string} +} + +let gateway: FakeGateway +let home: string + +beforeAll(async () => { + gateway = await startFakeGateway() + home = makeHome(gateway.port) +}) + +afterAll(async () => { + await gateway.close() +}) + +describe('norbix schema', () => { + let schema: SchemaDoc + + beforeAll(async () => { + const r = await cli(home, ['schema', '--json']) + expect(r.code).toBe(0) + expect(r.stderr).toBe('') + schema = parseSingleJson(r.stdout) as SchemaDoc + }) + + it('lists every command in dist/commands', () => { + expect(schema.commands.map((c) => c.id)).toEqual(commandIdsOnDisk()) + }) + + it('marks every command with destructive and supportsDryRun booleans', () => { + for (const c of schema.commands) { + expect(typeof c.destructive, c.id).toBe('boolean') + expect(typeof c.supportsDryRun, c.id).toBe('boolean') + if (c.destructive) expect(c.supportsDryRun, `${c.id} is destructive so it must support --dry-run`).toBe(true) + } + }) + + it('names the contract and the exit codes', () => { + expect(schema.cli).toBe('norbix') + expect(schema.contract).toContain('docs/agent-contract.md') + expect(Object.keys(schema.exitCodes)).toEqual(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']) + expect(schema.dynamic.hub).toContain('norbix hub --help') + }) + + it('every mutating command has a --dry-run example, every destructive one a --yes example', () => { + const missing: string[] = [] + for (const c of schema.commands) { + if (c.supportsDryRun && !c.examples.some((e) => e.includes('--dry-run'))) missing.push(`${c.id}: --dry-run`) + if (c.destructive && !c.examples.some((e) => e.includes('--yes'))) missing.push(`${c.id}: --yes`) + if (c.examples.length === 0) missing.push(`${c.id}: no example`) + if (c.examples.some((e) => e.includes('...'))) missing.push(`${c.id}: placeholder value`) + } + + expect(missing).toEqual([]) + }) + + it('carries the global flags with their env vars', () => { + const users = schema.commands.find((c) => c.id === 'users delete')! + const project = users.flags.find((f) => f.name === 'project')! + expect(project).toMatchObject({type: 'string', env: 'NORBIX_PROJECT_ID', global: true}) + expect(users.flags.find((f) => f.name === 'yes')).toMatchObject({type: 'boolean'}) + expect(users.args).toEqual([{name: 'id', required: true, description: 'User ID'}]) + }) + + it('describes one command or one topic on request', async () => { + const one = parseSingleJson((await cli(home, ['schema', 'users', 'delete', '--json'])).stdout) as SchemaDoc + expect(one.commands.map((c) => c.id)).toEqual(['users delete']) + + const topic = parseSingleJson((await cli(home, ['schema', 'db', '--json'])).stdout) as SchemaDoc + expect(topic.commands.length).toBeGreaterThan(5) + expect(topic.commands.every((c) => c.id.startsWith('db '))).toBe(true) + + const r = await cli(home, ['schema', 'nothing', '--json']) + expect(r.code).toBe(2) + }) + + it('prints a table without --json', async () => { + const r = await cli(home, ['schema']) + expect(r.code).toBe(0) + expect(r.stdout).toMatch(/users delete\s+Delete a user\s+\[destructive, dry-run\]/) + }) + + it('is stable (snapshot of ids, flags and markers)', () => { + const compact = schema.commands.map((c) => ({ + id: c.id, + destructive: c.destructive, + supportsDryRun: c.supportsDryRun, + args: c.args.map((a) => (a.required ? a.name : `[${a.name}]`)), + flags: c.flags.filter((f) => !f.global).map((f) => `${f.name}:${f.type}`), + })) + expect(compact).toMatchSnapshot() + }) +}) + +describe('norbix hub --json', () => { + it('lists every method with its route and request fields', async () => { + const r = await cli(home, ['hub', 'scheduler', '--json']) + expect(r.code).toBe(0) + const doc = parseSingleJson(r.stdout) as {module: string; methods: Array<{method: string; words: string; destructive: boolean; http: string; path: string; fields: Array<{name: string; kind: string; required: boolean}>}>} + expect(doc.module).toBe('scheduler') + const del = doc.methods.find((m) => m.method === 'deleteSchedulerTask')! + expect(del).toMatchObject({words: 'task delete', destructive: true, http: 'DELETE', path: '/{version}/scheduler/tasks/{Id}'}) + expect(del.fields).toEqual([{name: 'id', type: 'string', kind: 'string', required: true}]) + }) + + it('shows the fields in scope help', async () => { + const r = await cli(home, ['hub', 'scheduler', 'task', 'delete', '--help']) + expect(r.code).toBe(0) + expect(r.stdout).toContain('fields: id (string, required)') + expect(r.stdout).toContain('--field:str') + }) +}) From 63fea3ab48e37fe2e03ea0e26c01e7fa2cb9d735 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Fri, 2 Oct 2026 12:50:33 +0300 Subject: [PATCH 08/14] docs(cli): schema, typed hub values and --body in the README; work log Co-Authored-By: Claude Fable 5.1 --- README.md | 14 +++++++++++--- docs/tasks/agent-native-cli.md | 23 +++++++++++++++++++++-- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4205449..ec2063f 100644 --- a/README.md +++ b/README.md @@ -75,9 +75,17 @@ norbix hub email templates get # email/sms/push route into notif ``` The first positional value becomes `id`; `--field value` flags become request -fields. Destructive verbs (delete, remove, rotate, ...) ask for confirmation -unless `--yes`. Add `--dry-run` to preview the exact SDK call. -| `norbix autocomplete` | Set up shell tab-completion (bash/zsh) | +fields, typed from the SDK's request type (`norbix hub --help` lists +every method with its fields; `--json` returns them as data). Force a type +with `--field:str`, `--field:num`, `--field:bool` or `--field:json`, or pass +the whole request with `--body ''`. Destructive verbs (delete, remove, +stop, disable, block, regenerate, rotate) ask for confirmation unless +`--yes` — and exit 3 without a terminal. Add `--dry-run` to preview the exact +HTTP request. + +`norbix schema --json` describes every command (args, flags, examples, +destructive, dry-run) for scripts and coding agents; `norbix autocomplete` +sets up shell tab-completion (bash/zsh). Run `norbix --help` for flags and examples. diff --git a/docs/tasks/agent-native-cli.md b/docs/tasks/agent-native-cli.md index 0063219..e207294 100644 --- a/docs/tasks/agent-native-cli.md +++ b/docs/tasks/agent-native-cli.md @@ -367,7 +367,7 @@ code (parallel work), colour, new runtime dependencies. `login`/`configure` guarded, "Cancelled" → exit 9, tests — **done** 2. PR 2 — discovery: `norbix schema`, `--dry-run` on every mutating command, realistic `--help` examples, hub/api parser fixes (`--body`, typed - fields, boolean traps) — todo + fields, boolean traps) — **done** 3. PR 3 — startup (`oclif manifest` in build, lazy prompts), docs (`agent-contract.md`, `AGENTS.md`), `norbix ai init`, README section — todo @@ -389,6 +389,14 @@ code (parallel work), colour, new runtime dependencies. | `package.json` | `oclif.hooks` | 1 | | `tests/_cli.ts`, `tests/agent-mode.test.ts`, `tests/cli-error.test.ts` | new: child-process tests against a fake gateway; envelope unit tests | 1 | | `README.md` | "Errors and exit codes" section | 1 | +| `scripts/gen-request-fields.mjs`, `src/generated/request-fields.json` | new: request-field map generated from the installed SDK (578 methods: route, DTO, fields with kind/required); `npm run gen:fields`; copied into `dist/` by the build | 2 | +| `src/lib/request-fields.ts` | new: lazy loader + helpers for the map (no oclif import) | 2 | +| `src/lib/dispatch.ts` | two-step typing (`parseArgv` → `typeFields` with the method's kinds), `--field:str/:num/:bool/:json`, boolean flags give the next token back, disable/block destructive | 2 | +| `src/lib/namespace-command.ts`, `hub.ts`, `api.ts` | `--body` (rejects mixing with `--field`), fields in scope help and in `--json` listings, value rules text | 2 | +| `src/lib/schema.ts`, `src/commands/schema.ts` | new: `norbix schema [command] --json` (lib has no oclif import) | 2 | +| 35 mutating command files | `...BaseCommand.dryRunFlags`; explicit dry runs in `config set/unset`, `env use`, `logout`, `raw`, `files publish/unpublish` (`publicFilesRequest`) | 2 | +| every command file | realistic example values; a `--dry-run` example on every mutating command, `--yes` on every destructive one | 2 | +| `tests/schema.test.ts`, `tests/dispatch.test.ts`, `tests/agent-mode.test.ts` | schema snapshot + completeness, parser unit tests, dry-run on non-destructive / local / raw-fetch commands, `--body` | 2 | ### Findings @@ -415,9 +423,20 @@ code (parallel work), colour, new runtime dependencies. (heavy load); the before/after numbers in the report use the best of several runs. +- The SDK's `hub2.dtos.d.ts` names some request DTOs without the `Request` + suffix (`DeleteSchedulerTask`) — the generator keys on the JSDoc + "Request DTO:" line in `dist/index.js`, so it does not matter. +- `files upload` runs three calls (signed URL, PUT, commit); `--dry-run` + stops at the first SDK call and shows that one. +- `looksLikeId`: "v2", "PROD", "SMS" are still values, not words. Documented + in the scope help ("words are lowercase letters") instead of changed — + every current method is reachable with lowercase words. + ### Rejected / moved out -- (none yet) +- Parser: changing `looksLikeId` (uppercase = value) was part of the ask + (§2.5); kept and documented, because an uppercase token is almost always + an environment name or an id, and lowercase words reach every method. ### Needs you From a4795381f6257c84d6ef03a58217793e92700f29 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Fri, 2 Oct 2026 12:55:43 +0300 Subject: [PATCH 09/14] perf(cli): --version fast path and the oclif manifest in every build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan step [3] of docs/tasks/agent-native-cli.md. - bin/run.js answers `norbix --version` from package.json without loading oclif: 0.17 s → 0.03 s on an idle machine (0.5–3 s → 0.03 s under load). - `npm run build` ends with `oclif manifest`, so dev runs and the tests use the manifest instead of scanning dist/commands (`users delete --help` 0.18 s → 0.12 s). The manifest stays gitignored and `postpack` still removes it; `prepack` is just the build now. - `ai` topic for `norbix ai init`. Co-Authored-By: Claude Fable 5.1 --- bin/run.js | 13 +++++++++++-- package.json | 19 +++++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/bin/run.js b/bin/run.js index 7202228..a2b8746 100644 --- a/bin/run.js +++ b/bin/run.js @@ -1,4 +1,13 @@ #!/usr/bin/env node -import {execute} from '@oclif/core' -await execute({dir: import.meta.url}) +// Fast path: `norbix --version` answers from package.json without loading +// oclif (the agent contract asks for < 200 ms; oclif alone costs more on a +// slow machine). Same string oclif prints: name/version platform-arch node-v. +if (process.argv.length === 3 && process.argv[2] === '--version') { + const {readFileSync} = await import('node:fs') + const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) + process.stdout.write(`${pkg.name}/${pkg.version} ${process.platform}-${process.arch} node-${process.version}\n`) +} else { + const {execute} = await import('@oclif/core') + await execute({dir: import.meta.url}) +} diff --git a/package.json b/package.json index e0f39f4..e017af9 100644 --- a/package.json +++ b/package.json @@ -36,10 +36,10 @@ ], "scripts": { "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"", - "build": "npm run clean && tsc && node -e \"require('node:fs').cpSync('src/generated','dist/generated',{recursive:true})\"", + "build": "npm run clean && tsc && node -e \"require('node:fs').cpSync('src/generated','dist/generated',{recursive:true})\" && oclif manifest", "typecheck": "tsc --noEmit", "dev": "npm run build && node ./bin/run.js", - "prepack": "npm run build && oclif manifest", + "prepack": "npm run build", "postpack": "node -e \"require('node:fs').rmSync('oclif.manifest.json',{force:true})\"", "test": "vitest run", "test:watch": "vitest", @@ -80,18 +80,24 @@ "account": { "description": "Account profile, projects, team, usage, billing" }, + "ai": { + "description": "Set up AI coding agents (Claude Code, Cursor, AGENTS.md) to use this CLI" + }, "apikeys": { "description": "Show or regenerate project API keys" }, - "email": { - "description": "Email templates and campaigns" - }, "config": { "description": "Read and write the local CLI configuration" }, "db": { "description": "Work with database collections (find, insert, update, delete)" }, + "email": { + "description": "Email templates and campaigns" + }, + "env": { + "description": "Manage project environments (PROD, TEST, ...)" + }, "files": { "description": "Upload, download and manage files", "subtopics": { @@ -100,9 +106,6 @@ } } }, - "env": { - "description": "Manage project environments (PROD, TEST, ...)" - }, "logs": { "description": "Read project logs" }, From 0ec3efc17d1bd992c860472165f61a0a4525fd8d Mon Sep 17 00:00:00 2001 From: CodeMash Date: Fri, 2 Oct 2026 12:55:44 +0300 Subject: [PATCH 10/14] =?UTF-8?q?feat(cli):=20`norbix=20ai=20init`=20?= =?UTF-8?q?=E2=80=94=20set=20a=20project=20up=20for=20Claude=20Code=20and?= =?UTF-8?q?=20other=20agents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan step [3] of docs/tasks/agent-native-cli.md. Writes into the current project (or --dir): a Claude Code skill at .claude/skills/norbix/SKILL.md plus a short block in CLAUDE.md (--target claude, the default); the same block in AGENTS.md (--target agents); a Cursor rule at .cursor/rules/norbix.mdc (--target cursor); or all three. The files are short on purpose — `norbix schema --json` and `--help` stay the source of truth. --dry-run lists what would be written; an existing file is never overwritten and an existing block never duplicated without --force, which rewrites the file / replaces the block in place between its markers. Co-Authored-By: Claude Fable 5.1 --- src/commands/ai/init.ts | 92 +++++++++++++++++++ src/lib/agent-files.ts | 117 ++++++++++++++++++++++++ tests/__snapshots__/schema.test.ts.snap | 12 +++ tests/ai-init.test.ts | 103 +++++++++++++++++++++ 4 files changed, 324 insertions(+) create mode 100644 src/commands/ai/init.ts create mode 100644 src/lib/agent-files.ts create mode 100644 tests/ai-init.test.ts diff --git a/src/commands/ai/init.ts b/src/commands/ai/init.ts new file mode 100644 index 0000000..b8ba9d0 --- /dev/null +++ b/src/commands/ai/init.ts @@ -0,0 +1,92 @@ +import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'node:fs' +import {dirname, join, resolve} from 'node:path' + +import {Flags} from '@oclif/core' + +import {BaseCommand} from '../../base.js' +import {hasBlock, planTarget, type PlannedFile, type Target} from '../../lib/agent-files.js' +import {usageError} from '../../lib/cli-error.js' + +const TARGETS: Target[] = ['claude', 'agents', 'cursor'] + +export default class AiInit extends BaseCommand { + static description = `Set up this project so a coding agent uses the norbix CLI correctly. + +Writes, into the current folder: + claude .claude/skills/norbix/SKILL.md (a Claude Code skill) and a short + block in CLAUDE.md + agents the same block in AGENTS.md (Codex, OpenCode, Cursor and others + read it) + cursor .cursor/rules/norbix.mdc + all all of the above + +The files are short on purpose: \`norbix schema --json\` and \`--help\` are the +source of truth. An existing file is never overwritten without --force; a +block that is already present is left alone. --dry-run lists what would be +written.` + + static examples = [ + '<%= config.bin %> ai init', + '<%= config.bin %> ai init --dry-run', + '<%= config.bin %> ai init --target all', + '<%= config.bin %> ai init --target agents --force', + ] + + static flags = { + ...BaseCommand.dryRunFlags, + target: Flags.string({ + char: 't', + description: 'Which agent files to write', + options: [...TARGETS, 'all'], + default: 'claude', + }), + force: Flags.boolean({description: 'Overwrite an existing skill / rule file and rewrite an existing block', default: false}), + dir: Flags.string({description: 'Project folder (default: current folder)'}), + } + + async run(): Promise { + const {flags} = await this.parse(AiInit) + const root = resolve(flags.dir ?? process.cwd()) + const targets: Target[] = flags.target === 'all' ? TARGETS : [flags.target as Target] + + const exists = (p: string) => existsSync(join(root, p)) + const read = (p: string) => readFileSync(join(root, p), 'utf8') + const planned = targets.flatMap((t) => planTarget(t, exists, read, flags.force)) + + const refused = planned.filter((f) => { + if (f.action === 'create' && exists(f.path) && !flags.force) return true + if (f.action === 'append' && hasBlock(read(f.path))) return true + return false + }) + if (refused.length > 0) { + throw usageError( + `Already set up: ${refused.map((f) => f.path).join(', ')}.`, + 'Pass --force to rewrite the skill / rule file and the block, or --dry-run to see what would change.', + 'norbix ai init --help', + ) + } + + const files = planned.map((f) => ({path: f.path, action: f.action, bytes: Buffer.byteLength(f.content)})) + if (flags['dry-run']) { + return this.dryRun({method: 'ai.init', request: {dir: root, targets, files}}) + } + + for (const f of planned) this.write(root, f) + + this.print( + [ + ...files.map((f) => `${f.action === 'append' ? 'updated' : f.action === 'replace' ? 'replaced' : 'wrote'} ${f.path}`), + '', + 'Done. The agent now knows to use `norbix` with --json, --dry-run and --yes.', + 'Give it credentials with a profile (`norbix login --api-key ... --profile `) or NORBIX_* env vars.', + ].join('\n'), + ) + return {dir: root, targets, files} + } + + private write(root: string, f: PlannedFile): void { + const full = join(root, f.path) + mkdirSync(dirname(full), {recursive: true}) + writeFileSync(full, f.content) + } +} diff --git a/src/lib/agent-files.ts b/src/lib/agent-files.ts new file mode 100644 index 0000000..f16ea7f --- /dev/null +++ b/src/lib/agent-files.ts @@ -0,0 +1,117 @@ +/** + * What `norbix ai init` writes into a project so a coding agent uses the + * CLI correctly. Kept short on purpose: the CLI's own `--help` and + * `norbix schema --json` are the source of truth, not these files. + * + * No oclif import. + */ + +export const MARKER_START = '' +export const MARKER_END = '' + +export type Target = 'claude' | 'agents' | 'cursor' + +export interface PlannedFile { + path: string + /** create a new file, append a block to an existing one, or replace the block / file (--force). */ + action: 'create' | 'append' | 'replace' + content: string +} + +/** The recipe every target shares (Markdown, ~10 lines). */ +export function recipeBlock(): string { + return [ + MARKER_START, + '## Norbix backend — use the `norbix` CLI', + '', + 'This project uses Norbix (database, users, files, templates, campaigns, scheduler, logs). Talk to it with the `norbix` CLI from the shell; never edit the backend by hand.', + '', + '- Check who you are first: `norbix whoami --json`. If it fails with exit 4, stop and ask for credentials (`--profile ` or `NORBIX_API_KEY` + `NORBIX_PROJECT_ID` + `NORBIX_REGION`). Never run `norbix login` or `norbix configure` — they need a terminal.', + '- Always pass `--json`: stdout is then exactly one JSON document (a result, or `{"error": {code, message, status, exit, fieldErrors, url, traceId, hint, docs}}`).', + '- Discover before guessing: `norbix schema --json` (every command, `destructive`, `supportsDryRun`), then `norbix --help`. Any SDK endpoint without a named command: `norbix hub --json` lists methods and request fields; call with `norbix hub [id] --body \'\'`.', + '- Change something only in two steps: `--dry-run` (prints the exact request, sends nothing), inspect, then re-run with `--yes` when it is destructive. Without `--yes` a destructive command exits 3 and sends nothing.', + '- Branch on the exit code: 0 ok · 2 usage (fix the call) · 3 needs `--yes` · 4 auth · 5 not found · 6 validation (`fieldErrors`) · 7 network · 8 server (retry with backoff) · 9 cancelled.', + '- Full contract: `docs/agent-contract.md` in the norbix-code/cli repo; the hint and docs fields of every error tell you the next step.', + MARKER_END, + '', + ].join('\n') +} + +/** Claude Code skill: frontmatter + the same recipe. */ +export function claudeSkill(): string { + return [ + '---', + 'name: norbix', + 'description: Use when the task touches the Norbix backend — database records, users, files, email/SMS/push templates and campaigns, scheduler tasks, logs, environments, API keys, webhooks — or when the user mentions norbix. Runs the `norbix` CLI from the shell (never the browser login).', + '---', + '', + '# Norbix via the CLI', + '', + 'Use the `norbix` command for everything on the Norbix backend. It is built for agents: one JSON document per call, documented exit codes, nothing destructive without `--yes`, and `--dry-run` on every change.', + '', + '## Steps', + '', + '1. `norbix whoami --json` — confirms the profile, project, environment and region. Exit 4 means no credentials: ask the user for `--profile ` or the `NORBIX_API_KEY`, `NORBIX_PROJECT_ID` and `NORBIX_REGION` variables. Do not run `norbix login` / `norbix configure`; they need a terminal.', + '2. Find the command: `norbix schema --json` lists every command with `destructive` and `supportsDryRun`; `norbix --help` has an example with real-looking values. No named command? `norbix hub --json` (or `norbix api --json`) lists every SDK method with its request fields; call one with plain words and `--body \'\'`.', + '3. Read with `--json` and pipe or parse the one document on stdout.', + '4. Change in two steps: `norbix ... --dry-run --json` prints the exact HTTP request and sends nothing. When it is right, re-run without `--dry-run`, adding `--yes` if the command is destructive. Never add `--yes` to the first attempt.', + '5. Branch on the exit code, and read `error.hint` / `error.docs` on failure: 0 ok · 2 usage · 3 add `--yes` after a dry run · 4 auth · 5 not found (check id / `--env` / `--project`) · 6 validation (`error.fieldErrors`) · 7 network (check `--region`, endpoints) · 8 server (retry with backoff, keep `traceId`) · 9 cancelled.', + '', + '## Examples', + '', + '```sh', + 'norbix db find orders --filter \'{"status":"paid"}\' --page-size 20 --json', + 'norbix db update orders --id 66b2f0a1c3d4e5f6a7b8c9d0 --update \'{"$set":{"status":"shipped"}}\' --dry-run --json', + 'norbix users delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run --json # then: --yes --json', + 'norbix hub scheduler --json # methods + fields', + 'norbix hub scheduler task delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes --json', + '```', + '', + 'Context flags on every command: `--profile`, `--project`, `--env`, `--region`, `--api-key` (or `NORBIX_*` env vars). Full contract: docs/agent-contract.md in norbix-code/cli.', + '', + ].join('\n') +} + +/** Cursor rule file (.mdc): frontmatter + the recipe. */ +export function cursorRule(): string { + return ['---', 'description: Use the norbix CLI for anything on the Norbix backend', 'alwaysApply: false', 'globs:', '---', '', recipeBlock()].join('\n') +} + +/** Files one target needs, given what is already on disk. */ +export function planTarget(target: Target, exists: (path: string) => boolean, read: (path: string) => string, force: boolean): PlannedFile[] { + switch (target) { + case 'claude': + return [ + fileOrReplace('.claude/skills/norbix/SKILL.md', claudeSkill(), exists, force), + blockIn('CLAUDE.md', exists, read, force), + ] + case 'agents': + return [blockIn('AGENTS.md', exists, read, force)] + default: + return [fileOrReplace('.cursor/rules/norbix.mdc', cursorRule(), exists, force)] + } +} + +function fileOrReplace(path: string, content: string, exists: (p: string) => boolean, force: boolean): PlannedFile { + return {path, action: exists(path) ? (force ? 'replace' : 'create') : 'create', content} +} + +/** Append the block to a Markdown file, or replace the block between the markers with --force. */ +function blockIn(path: string, exists: (p: string) => boolean, read: (p: string) => string, force: boolean): PlannedFile { + const block = recipeBlock() + if (!exists(path)) return {path, action: 'create', content: block} + const current = read(path) + const start = current.indexOf(MARKER_START) + const end = current.indexOf(MARKER_END) + if (start !== -1 && end !== -1) { + if (!force) return {path, action: 'append', content: block} // refused later: already present + return {path, action: 'replace', content: current.slice(0, start) + block.trimEnd() + current.slice(end + MARKER_END.length)} + } + + return {path, action: 'append', content: (current.endsWith('\n') ? current : current + '\n') + '\n' + block} +} + +/** True when the file already carries the block (append would duplicate it). */ +export function hasBlock(content: string): boolean { + return content.includes(MARKER_START) +} diff --git a/tests/__snapshots__/schema.test.ts.snap b/tests/__snapshots__/schema.test.ts.snap index 1399e1d..3e9d1bb 100644 --- a/tests/__snapshots__/schema.test.ts.snap +++ b/tests/__snapshots__/schema.test.ts.snap @@ -54,6 +54,18 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "id": "account usage", "supportsDryRun": false, }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "target:option", + "force:boolean", + "dir:string", + ], + "id": "ai init", + "supportsDryRun": true, + }, { "args": [], "destructive": true, diff --git a/tests/ai-init.test.ts b/tests/ai-init.test.ts new file mode 100644 index 0000000..ca8e960 --- /dev/null +++ b/tests/ai-init.test.ts @@ -0,0 +1,103 @@ +import {existsSync, mkdtempSync, readFileSync, writeFileSync} from 'node:fs' +import {tmpdir} from 'node:os' +import {join} from 'node:path' + +import {beforeEach, describe, expect, it} from 'vitest' + +import {cli, makeHome, parseSingleJson} from './_cli.js' + +const home = makeHome(1) +let project: string + +beforeEach(() => { + project = mkdtempSync(join(tmpdir(), 'norbix-ai-init-')) +}) + +interface Report { + dryRun?: boolean + method?: string + request?: {files: Array<{path: string; action: string}>} + files?: Array<{path: string; action: string}> +} + +describe('norbix ai init', () => { + it('--dry-run lists the files and writes nothing', async () => { + const r = await cli(home, ['ai', 'init', '--dry-run', '--dir', project, '--json']) + expect(r.code).toBe(0) + const doc = parseSingleJson(r.stdout) as Report + expect(doc.dryRun).toBe(true) + expect(doc.request?.files.map((f) => `${f.action} ${f.path}`)).toEqual(['create .claude/skills/norbix/SKILL.md', 'create CLAUDE.md']) + expect(existsSync(join(project, '.claude'))).toBe(false) + expect(existsSync(join(project, 'CLAUDE.md'))).toBe(false) + }) + + it('writes the skill and the CLAUDE.md block, then refuses a second run without --force', async () => { + const first = await cli(home, ['ai', 'init', '--dir', project, '--json']) + expect(first.code).toBe(0) + expect((parseSingleJson(first.stdout) as Report).files?.map((f) => f.path)).toEqual(['.claude/skills/norbix/SKILL.md', 'CLAUDE.md']) + + const skill = readFileSync(join(project, '.claude/skills/norbix/SKILL.md'), 'utf8') + expect(skill).toMatch(/^---\nname: norbix\ndescription: Use when the task touches the Norbix backend/) + expect(skill).toContain('norbix schema --json') + expect(skill).toContain('--dry-run') + const claude = readFileSync(join(project, 'CLAUDE.md'), 'utf8') + expect(claude).toContain('') + expect(claude.split('\n').filter((l) => l.trim()).length).toBeLessThanOrEqual(12) + + const second = await cli(home, ['ai', 'init', '--dir', project, '--json']) + expect(second.code).toBe(2) + const err = (parseSingleJson(second.stdout) as {error: {message: string; hint: string}}).error + expect(err.message).toMatch(/Already set up/) + expect(err.hint).toMatch(/--force/) + }) + + it('appends the block to an existing CLAUDE.md without touching the rest', async () => { + writeFileSync(join(project, 'CLAUDE.md'), '# My project\n\nKeep this.\n') + const r = await cli(home, ['ai', 'init', '--dir', project, '--json']) + expect(r.code).toBe(0) + expect((parseSingleJson(r.stdout) as Report).files?.find((f) => f.path === 'CLAUDE.md')?.action).toBe('append') + const claude = readFileSync(join(project, 'CLAUDE.md'), 'utf8') + expect(claude.startsWith('# My project\n\nKeep this.\n')).toBe(true) + expect(claude).toContain('') + }) + + it('--force rewrites the skill and replaces the block in place', async () => { + await cli(home, ['ai', 'init', '--dir', project]) + writeFileSync(join(project, '.claude/skills/norbix/SKILL.md'), 'stale') + const claude = readFileSync(join(project, 'CLAUDE.md'), 'utf8') + writeFileSync(join(project, 'CLAUDE.md'), `# Top\n\n${claude}\n# Bottom\n`) + + const r = await cli(home, ['ai', 'init', '--dir', project, '--force', '--json']) + expect(r.code).toBe(0) + expect(readFileSync(join(project, '.claude/skills/norbix/SKILL.md'), 'utf8')).not.toBe('stale') + const after = readFileSync(join(project, 'CLAUDE.md'), 'utf8') + expect(after.startsWith('# Top\n')).toBe(true) + expect(after.endsWith('# Bottom\n')).toBe(true) + expect(after.split('').length).toBe(2) + }) + + it('--target all writes AGENTS.md and the Cursor rule too', async () => { + const r = await cli(home, ['ai', 'init', '--target', 'all', '--dir', project, '--json']) + expect(r.code).toBe(0) + expect((parseSingleJson(r.stdout) as Report).files?.map((f) => f.path)).toEqual([ + '.claude/skills/norbix/SKILL.md', + 'CLAUDE.md', + 'AGENTS.md', + '.cursor/rules/norbix.mdc', + ]) + expect(readFileSync(join(project, '.cursor/rules/norbix.mdc'), 'utf8')).toMatch(/^---\ndescription: /) + }) + + it('rejects an unknown target with exit 2', async () => { + const r = await cli(home, ['ai', 'init', '--target', 'emacs', '--dir', project, '--json']) + expect(r.code).toBe(2) + }) +}) + +describe('norbix --version', () => { + it('prints the oclif-style user agent', async () => { + const r = await cli(home, ['--version']) + expect(r.code).toBe(0) + expect(r.stdout).toMatch(/^@norbix\.ai\/cli\/\d+\.\d+\.\d+ \w+-\w+ node-v\d+/) + }) +}) From 09e9c1450dcaa0e5dec64364ea0d48719917faae Mon Sep 17 00:00:00 2001 From: CodeMash Date: Fri, 2 Oct 2026 12:55:44 +0300 Subject: [PATCH 11/14] docs(cli): the agent contract, the agent recipe, and a README section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan step [3] of docs/tasks/agent-native-cli.md. - docs/agent-contract.md: the eight rules, the exit-code table, the JSON error envelope, what a dry run prints, which commands confirm / dry-run, and which src/lib files the MCP server can share. - docs/AGENTS.md: the recipe an agent follows — auth without a terminal, always --json, discover with schema / --help / hub --json, change in two steps (--dry-run then --yes), branch on exit codes, `norbix ai init`. - README: "Use with Claude Code / AI agents"; schema and ai init in the command table. Co-Authored-By: Claude Fable 5.1 --- README.md | 19 +++++ docs/AGENTS.md | 95 ++++++++++++++++++++++ docs/agent-contract.md | 139 +++++++++++++++++++++++++++++++++ docs/tasks/agent-native-cli.md | 24 +++++- 4 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 docs/AGENTS.md create mode 100644 docs/agent-contract.md diff --git a/README.md b/README.md index ec2063f..69127d0 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ norbix scheduler list | `norbix hub ` | Call ANY hub endpoint with plain words (see below) | | `norbix api ` | Same for the data-plane API | | `norbix raw ` | Low-level HTTP escape hatch (hub by default, `--api` for API) | +| `norbix schema [command]` | Describe every command as data (`--json`) — for scripts and agents | +| `norbix ai init` | Set up a project for Claude Code / Cursor / AGENTS.md agents | ### Plain-word access to every endpoint @@ -176,6 +178,23 @@ nothing** unless you pass `--yes`. Preview any of them with `--dry-run`: the request is built with your real auth, region and project, printed, and not sent. +## Use with Claude Code / AI agents + +The CLI is built to be driven by a coding agent from the shell — no MCP +server needed: one JSON document per call (`--json`), documented exit codes +(0–9), nothing destructive without `--yes`, `--dry-run` on every change, and +`norbix schema --json` to discover every command. Set a project up once: + +```sh +norbix ai init # writes .claude/skills/norbix/SKILL.md + a CLAUDE.md block +norbix ai init --target all # also AGENTS.md (Codex, OpenCode) and a Cursor rule +``` + +Then give the agent credentials with a profile (`norbix login --api-key … --profile ci`) +or `NORBIX_API_KEY` / `NORBIX_PROJECT_ID` / `NORBIX_REGION`. The recipe the +agent follows is in [docs/AGENTS.md](docs/AGENTS.md); the guarantees it +relies on are in [docs/agent-contract.md](docs/agent-contract.md). + ## Versioning The major version is frozen at **v1** until the public launch. diff --git a/docs/AGENTS.md b/docs/AGENTS.md new file mode 100644 index 0000000..15a3196 --- /dev/null +++ b/docs/AGENTS.md @@ -0,0 +1,95 @@ +# Using the Norbix CLI from a coding agent + +The recipe a coding agent (Claude Code, Cursor, Codex, OpenCode) follows to +work on a Norbix backend from the shell. `norbix ai init` writes a short +version of this into your project (a Claude Code skill, a `CLAUDE.md` / +`AGENTS.md` block, a Cursor rule). The guarantees behind it are in +[`agent-contract.md`](agent-contract.md). + +## 0. Install + +```sh +npm install -g @norbix.ai/cli # or: npx @norbix.ai/cli … +norbix --version +``` + +## 1. Authenticate without a terminal + +Agents cannot answer prompts, so never run `norbix login` or +`norbix configure` interactively. Give the agent one of: + +- a **profile** created once by a human: `norbix login --api-key nbk_… --project --region nb-eu-germany --profile ci`, then every call takes `--profile ci`; +- **environment variables**: `NORBIX_API_KEY`, `NORBIX_PROJECT_ID`, `NORBIX_REGION` (and `NORBIX_ENV`, `NORBIX_ACCOUNT_ID` when needed). + +First call of a session: `norbix whoami --json`. Exit 4 means no usable +credentials — stop and ask. + +## 2. Always `--json` + +Stdout is then exactly one JSON document: the result, or +`{"error": {code, message, status, exit, fieldErrors, url, traceId, hint, docs}}`. +Nothing else is printed on stdout; stderr carries progress and warnings. + +## 3. Discover, do not guess + +```sh +norbix schema --json # every command: args, flags, examples, destructive, supportsDryRun +norbix schema users delete --json # one command +norbix users delete --help # the human page, with a realistic example +norbix hub scheduler --json # SDK methods of a module with their request fields +norbix hub scheduler task delete --help +``` + +Prefer the named commands (`db`, `users`, `files`, `email`, `sms`, `push`, +`scheduler`, `logs`, `env`, `apikeys`, `webhooks`, `payments`, `account`). +When there is none, every SDK endpoint is reachable with plain words: + +```sh +norbix hub scheduler task delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes --json +norbix hub scheduler task save --body '{"name":"nightly","cronExpression":"0 2 * * *"}' --json +``` + +`--body` takes the whole request as JSON (nothing is merged with +`--field` flags); `--field:str value` forces a string when a value looks +like a number. + +## 4. Change things in two steps + +```sh +norbix db update orders --id 66b2f0a1c3d4e5f6a7b8c9d0 --update '{"$set":{"status":"shipped"}}' --dry-run --json +# inspect "http" and "request" … +norbix db update orders --id 66b2f0a1c3d4e5f6a7b8c9d0 --update '{"$set":{"status":"shipped"}}' --json +``` + +A **destructive** command (`destructive: true` in the schema) additionally +needs `--yes`; without it, and without a terminal, it exits 3 and sends +nothing. Never add `--yes` to a first attempt — dry-run first. + +## 5. Branch on the exit code + +| exit | meaning | next step | +|---|---|---| +| 0 | ok | | +| 2 | usage error | read `error.message`, fix the call (`--help`) | +| 3 | confirmation required | re-run with `--yes` after a dry run | +| 4 | not authenticated / forbidden | ask for credentials or another profile | +| 5 | not found | check the id, `--env`, `--project`; list first | +| 6 | validation | `error.fieldErrors` names the fields | +| 7 | network | check `--region`, endpoints, connectivity | +| 8 | server / rate limit | retry with backoff; keep `error.traceId` | +| 9 | cancelled | | +| 1 | internal | report with `error.traceId` | + +`error.hint` says what to do; `error.docs` says where to read. + +## 6. Set up a project once + +```sh +cd my-project +norbix ai init # Claude Code skill + CLAUDE.md block +norbix ai init --target all # also AGENTS.md and a Cursor rule +norbix ai init --dry-run # see what it would write +``` + +Files are never overwritten without `--force`; an existing block is left +alone. diff --git a/docs/agent-contract.md b/docs/agent-contract.md new file mode 100644 index 0000000..1efcbbe --- /dev/null +++ b/docs/agent-contract.md @@ -0,0 +1,139 @@ +# The agent contract + +What a script or a coding agent (Claude Code, Cursor, Codex, OpenCode) can +rely on when it runs `norbix` from a shell. Read it once; every error the CLI +prints points back here through its `hint` and `docs` fields. +`norbix schema --json` names this file too. + +## 1. The rules + +1. **Never hang, never act silently.** In a non-interactive shell (stdout or + stdin is not a terminal, or `CI` is set, or `--json` is passed) a command + that needs a confirmation and did not get `--yes` exits **3** with one + line on stderr that names `--yes`. It sends nothing. In a terminal + without `--yes` it asks, as before. +2. **`--json` means stdout is exactly one JSON document, always.** Success: + the result. Failure: `{"error": {...}}` (section 3). No text on stdout in + JSON mode, ever. Progress and warnings go to stderr. The exit code is the + same with and without `--json`. +3. **Errors carry everything the SDK knows** — `status`, `code`, + `fieldErrors`, `url`, `traceId` — plus a `hint` (what to do next) and + `docs` (`norbix --help` or a URL). +4. **Documented exit codes** (section 2). Branch on them. +5. **`--dry-run` on every mutating command.** It resolves auth, region and + project exactly as a real call would (so a dry run that would fail for + real fails here too), prints the request that would be sent, sends + nothing, exits 0. +6. **Discoverable without reading source.** `norbix schema --json` returns + the whole command surface (args, flags, types, required, env var, + default, examples, `destructive`, `supportsDryRun`); `norbix schema + --json` one command. `--help` on every command has an example + with real-looking values and, for mutating commands, the `--dry-run` / + `--yes` pattern. `norbix hub --json` lists the SDK methods with + their request fields. +7. **Cheap to run.** `norbix --version` answers in well under 200 ms; + prompts and other heavy modules load only when used. +8. **No ANSI codes** with `--json`, with `NO_COLOR` set, or when stdout is + not a terminal. + +## 2. Exit codes + +| code | meaning | what to do | +|---|---|---| +| 0 | success | | +| 1 | unexpected / internal error | file an issue, include `traceId` | +| 2 | usage: bad flags or args, invalid JSON input, unknown command, missing config | `norbix --help` / `norbix schema --json` | +| 3 | confirmation required (non-interactive shell, no `--yes`) | re-run with `--yes` after a `--dry-run` | +| 4 | not authenticated / auth rejected (401, 403, no key) | `norbix login`, `--api-key`, `--profile`, `NORBIX_API_KEY` | +| 5 | not found (404) | check the id, `--env`, `--project` | +| 6 | validation rejected by the server (400 / 422, or a 200 with `isSuccess: false`) | `fieldErrors` lists the fields | +| 7 | network / timeout / endpoint unreachable | the error shows the URL; check `--region`, `api_url` / `hub_url`, connectivity | +| 8 | rate limited / server error (429, 5xx) | retry with backoff; include `traceId` | +| 9 | cancelled by the user at a prompt | | + +Exit codes are identical with and without `--json`. The table is also in +`norbix schema --json` under `exitCodes`, and in code at +`src/lib/exit-codes.ts`. + +## 3. The JSON error envelope + +One document on stdout, nothing on stderr: + +```json +{ + "error": { + "code": "USER_NOT_FOUND", + "message": "User not found", + "status": 404, + "exit": 5, + "fieldErrors": {"email": ["is required"]}, + "url": "https://nb-eu-germany.api.norbix.ai/v2/membership/auth/66b2f0a1c3d4e5f6a7b8c9d0", + "traceId": "7f3c…", + "hint": "Check the id, the --env and the --project. List items first (e.g. `norbix users list --json`).", + "docs": "norbix users get --help" + } +} +``` + +- `code` — the gateway's or the SDK's own code when there is one + (`CM-ERRORS-FILES-016`, `NORBIX_NETWORK_ERROR`), else derived from the + category (`NOT_FOUND`, `USAGE_ERROR`, `CONFIRMATION_REQUIRED`, + `UNKNOWN_COMMAND`, `CANCELLED`, `INTERNAL_ERROR`). +- `status` — HTTP status, only when the server answered. +- `exit` — the process exit code (section 2). +- `fieldErrors` — only when the server named fields: `{field: [messages]}`. +- `url` — the URL that was called, when known (network errors always have it). +- `traceId` — when the server returned one (`traceId`, `correlationId`, + or `responseStatus.meta.correlationId`). +- `hint`, `docs` — always present. + +Without `--json` the same information goes to **stderr** as text: + +``` +Error: User not found + code: USER_NOT_FOUND + status: 404 + url: https://…/v2/membership/auth/66b2f0a1c3d4e5f6a7b8c9d0 + traceId: 7f3c… +Hint: Check the id, the --env and the --project. … +Docs: norbix users get --help +``` + +## 4. What a dry run prints + +```json +{ + "dryRun": true, + "method": "api.membership.deleteUser", + "request": {"id": "66b2f0a1c3d4e5f6a7b8c9d0"}, + "http": { + "method": "DELETE", + "url": "https://nb-eu-germany.api.norbix.ai/v2/membership/auth?id=66b2f0a1c3d4e5f6a7b8c9d0", + "headers": {"accept": "application/json", "authorization": "Bearer ***", "norbix-project-id": "…"}, + "body": null + } +} +``` + +`method` is the SDK call (or a local action such as `config.set`); `http` is +the request as it would leave the machine, with the token redacted. A +command that makes several calls (`files upload`) shows the first. + +## 5. Which commands confirm, which dry-run + +- **Destructive** (`destructive: true` in the schema; take `--yes` and + `--dry-run`): `delete`, `stop`, `regenerate`, `rotate`, `block`, + `disable`, `remove`, `db update --many`, `db delete`, and any `hub` / + `api` method whose verb is one of those. +- **Mutating** (`supportsDryRun: true`, no confirmation): `create`, + `insert`, `update --id`, `replace`, `archive`, `unarchive`, `clone`, + `enable`, `invite`, `unblock`, `upload`, `publish`, `unpublish`, + `config set / unset`, `env use`, `logout`, `raw`. +- **Read-only**: everything else; no `--yes`, no `--dry-run`. + +## 6. Shared code + +`src/lib/exit-codes.ts`, `src/lib/cli-error.ts` (envelope), +`src/lib/schema.ts` (schema document) and `src/lib/request-fields.ts` +(SDK method → request fields) have no oclif dependency, so the MCP server +can import them and speak the same contract. diff --git a/docs/tasks/agent-native-cli.md b/docs/tasks/agent-native-cli.md index e207294..23fa3f4 100644 --- a/docs/tasks/agent-native-cli.md +++ b/docs/tasks/agent-native-cli.md @@ -369,7 +369,7 @@ code (parallel work), colour, new runtime dependencies. realistic `--help` examples, hub/api parser fixes (`--body`, typed fields, boolean traps) — **done** 3. PR 3 — startup (`oclif manifest` in build, lazy prompts), docs - (`agent-contract.md`, `AGENTS.md`), `norbix ai init`, README section — todo + (`agent-contract.md`, `AGENTS.md`), `norbix ai init`, README section — **done** ### Changes @@ -397,6 +397,13 @@ code (parallel work), colour, new runtime dependencies. | 35 mutating command files | `...BaseCommand.dryRunFlags`; explicit dry runs in `config set/unset`, `env use`, `logout`, `raw`, `files publish/unpublish` (`publicFilesRequest`) | 2 | | every command file | realistic example values; a `--dry-run` example on every mutating command, `--yes` on every destructive one | 2 | | `tests/schema.test.ts`, `tests/dispatch.test.ts`, `tests/agent-mode.test.ts` | schema snapshot + completeness, parser unit tests, dry-run on non-destructive / local / raw-fetch commands, `--body` | 2 | +| `bin/run.js` | `--version` fast path (answers from package.json, no oclif load) | 3 | +| `package.json` | `build` ends with `oclif manifest` (manifest stays gitignored, `postpack` still cleans it); `prepack` = build; `ai` topic | 3 | +| `src/lib/agent-files.ts` | new: the skill, the CLAUDE.md / AGENTS.md block, the Cursor rule, and the plan per target (no oclif import) | 3 | +| `src/commands/ai/init.ts` | new: `norbix ai init [--target claude\|agents\|cursor\|all] [--dry-run] [--force] [--dir]` | 3 | +| `docs/agent-contract.md`, `docs/AGENTS.md` | new: the contract (§1 + §3.1 + §3.2 + dry-run shape + which commands confirm) and the agent recipe | 3 | +| `README.md` | "Use with Claude Code / AI agents" section; `schema` and `ai init` in the command table | 3 | +| `tests/ai-init.test.ts` | dry-run lists, writes, refuses without `--force`, appends to an existing CLAUDE.md, `--force` replaces the block in place, `--target all`, `--version` string | 3 | ### Findings @@ -421,7 +428,15 @@ code (parallel work), colour, new runtime dependencies. "See more help with --help"); `toEnvelope` strips it. - `--version` timings on this machine vary 0.5 s – 3.2 s between runs (heavy load); the before/after numbers in the report use the best of - several runs. + several runs. Idle machine: oclif's own `--version` is 0.17 s with or + without a manifest (loading @oclif/core dominates), so the fast path in + `bin/run.js` answers from package.json in 0.03 s. `users delete --help`: + 0.18 s without manifest → 0.12 s with. +- `@inquirer/prompts` was already lazy after PR 1 (dynamic import in + `confirmOrFail`, `login`, `configure`); no static import is left. +- `oclif manifest` choice: generated by `npm run build`, never committed + (`.gitignore` unchanged, `postpack` still removes it) — a committed + manifest goes stale with every flag change. - The SDK's `hub2.dtos.d.ts` names some request DTOs without the `Request` suffix (`DeleteSchedulerTask`) — the generator keys on the JSDoc @@ -444,4 +459,7 @@ code (parallel work), colour, new runtime dependencies. ### Open questions -- (none blocking) +- Where will `docs/AGENTS.md` live for the hub/portal docs team? Not + blocking: the README links to the file in this repo; recommended default + is to publish it unchanged at docs.norbix.ai/cli/agents and keep the repo + copy as the source. From f81d7d8079f5846fb2e8414f2ddaba1635d875dc Mon Sep 17 00:00:00 2001 From: CodeMash Date: Sat, 3 Oct 2026 16:59:48 +0300 Subject: [PATCH 12/14] test(agent-mode): give the spawn-every-destructive-command tests 120 s Each of the three tests spawns the CLI once per destructive command, four at a time. Windows runners take ~3 s per spawn, so they ran past vitest's 20 s default, and the timed-out pool kept sending requests into the next test (the --dry-run test then saw DELETE/PUT hits it never made). Co-Authored-By: Claude Opus 5.5 --- tests/agent-mode.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/agent-mode.test.ts b/tests/agent-mode.test.ts index 2e7aec9..a894601 100644 --- a/tests/agent-mode.test.ts +++ b/tests/agent-mode.test.ts @@ -56,12 +56,17 @@ const DESTRUCTIVE: string[][] = [ ] describe('destructive commands in a non-interactive shell', () => { + // Each test spawns the CLI once per command (DESTRUCTIVE.length processes, + // 4 at a time). Windows runners need ~3 s per spawn, so the default 20 s + // runs out — and a timed-out pool keeps sending into the next test. + const SPAWN_ALL_TIMEOUT = 120_000 + it('every one exits 3 without --yes and sends nothing', async () => { const results = await pool(DESTRUCTIVE, 4, async (argv) => ({argv, ...(await cli(home, [...argv, '--profile', 'x']))})) const wrong = results.filter((r) => r.code !== 3 || !/--yes/.test(r.stderr) || r.stdout !== '') expect(wrong.map((r) => `${r.argv.join(' ')} → exit ${r.code}: ${r.stderr.slice(0, 120)}`)).toEqual([]) expect(gateway.hits).toEqual([]) - }) + }, SPAWN_ALL_TIMEOUT) it('every one exits 3 with --json too, with the envelope on stdout', async () => { const results = await pool(DESTRUCTIVE, 4, async (argv) => ({argv, ...(await cli(home, [...argv, '--profile', 'x', '--json']))})) @@ -75,14 +80,14 @@ describe('destructive commands in a non-interactive shell', () => { } expect(gateway.hits).toEqual([]) - }) + }, SPAWN_ALL_TIMEOUT) it('every one runs with --yes and reaches the gateway', async () => { const results = await pool(DESTRUCTIVE, 4, async (argv) => ({argv, ...(await cli(home, [...argv, '--profile', 'x', '--yes']))})) const wrong = results.filter((r) => r.code !== 0) expect(wrong.map((r) => `${r.argv.join(' ')} → exit ${r.code}: ${r.stderr.slice(0, 160)}`)).toEqual([]) expect(gateway.hits.length).toBe(DESTRUCTIVE.length) - }) + }, SPAWN_ALL_TIMEOUT) }) describe('--dry-run', () => { From 44b6d049d5f1cf9830e18bbad4ff0733999c4a77 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Sat, 3 Oct 2026 17:05:21 +0300 Subject: [PATCH 13/14] feat(cli): the agent contract for the commands main added (ai, email, sms, project) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merging main brought in 115 commands written before the contract. They now follow it the same way the older commands do: - destructive ones (delete, disable, revoke-key, cors remove) take mutatingFlags and call confirmOrFail — exit 3 without --yes when there is no terminal, and --dry-run. The four integration disables and cors remove had no confirmation at all. - every other mutating command takes dryRunFlags and has a --dry-run example. - examples carry real-looking IDs instead of `66b2f0a1...`. - ProjectCommand.confirmOrStop is gone (confirmOrFail replaces it), and projectClient also returns `reader`: in a dry run the read before a write (current origins, current AI settings) goes out live and only the write is stopped, so the printed request is the change itself. Tests: --yes added where the commands now confirm, the destructive ones added to agent-mode's DESTRUCTIVE list, schema snapshot regenerated (115 entries added, none of the existing ones changed). Co-Authored-By: Claude Opus 5.5 --- src/commands/ai/llm.ts | 2 +- src/commands/ai/llm/default.ts | 9 +- src/commands/ai/llm/delete.ts | 15 +- src/commands/ai/llm/disable.ts | 10 +- src/commands/ai/llm/enable.ts | 9 +- src/commands/ai/llm/save.ts | 2 + src/commands/ai/llm/test.ts | 9 +- src/commands/ai/mcp.ts | 2 +- src/commands/ai/mcp/delete.ts | 15 +- src/commands/ai/mcp/disable.ts | 10 +- src/commands/ai/mcp/enable.ts | 9 +- src/commands/ai/mcp/save.ts | 2 + src/commands/ai/mcp/test.ts | 9 +- src/commands/ai/service-user/create.ts | 2 + src/commands/ai/service-user/delete.ts | 15 +- src/commands/ai/service-user/revoke-key.ts | 15 +- src/commands/ai/service-user/rotate-key.ts | 6 +- src/commands/email/campaign/batch.ts | 4 +- src/commands/email/campaign/batches.ts | 4 +- src/commands/email/campaign/create.ts | 8 +- src/commands/email/campaign/delete.ts | 15 +- src/commands/email/campaign/messages.ts | 2 +- src/commands/email/disable.ts | 15 +- src/commands/email/enable.ts | 9 +- src/commands/email/footer.ts | 2 +- src/commands/email/footer/delete.ts | 15 +- src/commands/email/footer/save.ts | 8 +- src/commands/email/integration.ts | 2 +- .../email/integration/confirm-delivery.ts | 9 +- src/commands/email/integration/default.ts | 9 +- src/commands/email/integration/delete.ts | 15 +- src/commands/email/integration/disable.ts | 10 +- .../email/integration/domain-health.ts | 2 +- src/commands/email/integration/enable.ts | 9 +- src/commands/email/integration/save.ts | 2 + src/commands/email/integration/test.ts | 6 +- src/commands/email/preview.ts | 6 +- src/commands/email/signature.ts | 2 +- src/commands/email/signature/delete.ts | 15 +- src/commands/email/signature/save.ts | 8 +- src/commands/email/system-template.ts | 2 +- src/commands/email/template/attach.ts | 6 +- src/commands/email/template/create.ts | 6 +- src/commands/email/template/tokens.ts | 2 +- src/commands/email/template/update.ts | 8 +- src/commands/email/validation/save.ts | 6 +- src/commands/email/validation/test.ts | 9 +- src/commands/project.ts | 2 +- src/commands/project/admin-portal/disable.ts | 11 +- src/commands/project/admin-portal/enable.ts | 9 +- .../project/admin-portal/set-service-user.ts | 9 +- src/commands/project/admin-portal/set-url.ts | 2 + src/commands/project/ai/assistant/create.ts | 6 +- src/commands/project/ai/assistant/delete.ts | 11 +- src/commands/project/ai/assistant/update.ts | 12 +- src/commands/project/ai/settings/set.ts | 8 +- src/commands/project/cors/add.ts | 13 +- src/commands/project/cors/remove.ts | 15 +- src/commands/project/cors/set.ts | 2 + src/commands/project/delete.ts | 11 +- src/commands/project/disable.ts | 11 +- src/commands/project/enable.ts | 9 +- src/commands/project/legal/expose.ts | 9 +- src/commands/project/legal/hide.ts | 9 +- src/commands/project/legal/set.ts | 6 +- src/commands/project/public-config.ts | 2 +- src/commands/project/set-colors.ts | 2 + src/commands/project/set-default-language.ts | 9 +- src/commands/project/set-description.ts | 2 + src/commands/project/set-icon.ts | 2 + src/commands/project/set-languages.ts | 9 +- src/commands/project/set-logo.ts | 2 + src/commands/project/set-name.ts | 9 +- src/commands/project/set-regions.ts | 6 +- src/commands/project/set-url.ts | 2 + src/commands/project/tokens.ts | 2 +- src/commands/sms/campaign/batch.ts | 4 +- src/commands/sms/campaign/batches.ts | 2 +- src/commands/sms/campaign/create.ts | 8 +- src/commands/sms/campaign/delete.ts | 15 +- src/commands/sms/campaign/message.ts | 2 +- src/commands/sms/campaign/messages.ts | 2 +- src/commands/sms/disable.ts | 15 +- src/commands/sms/enable.ts | 9 +- src/commands/sms/integration.ts | 2 +- .../sms/integration/confirm-delivery.ts | 9 +- src/commands/sms/integration/default.ts | 9 +- src/commands/sms/integration/delete.ts | 15 +- src/commands/sms/integration/disable.ts | 10 +- src/commands/sms/integration/enable.ts | 9 +- src/commands/sms/integration/save.ts | 2 + src/commands/sms/integration/test.ts | 6 +- src/commands/sms/preview.ts | 4 +- src/commands/sms/template/create.ts | 6 +- src/commands/sms/template/tokens.ts | 2 +- src/commands/sms/template/update.ts | 8 +- src/lib/project.ts | 30 +- tests/__snapshots__/schema.test.ts.snap | 2072 ++++++++++++++--- tests/agent-mode.test.ts | 20 + tests/email-routes.test.ts | 2 +- tests/email.test.ts | 2 +- tests/project-routes.test.ts | 10 +- tests/sms-routes.test.ts | 2 +- tests/sms.test.ts | 2 +- 104 files changed, 2238 insertions(+), 600 deletions(-) diff --git a/src/commands/ai/llm.ts b/src/commands/ai/llm.ts index 7146b2b..c0818d6 100644 --- a/src/commands/ai/llm.ts +++ b/src/commands/ai/llm.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class AiLlm extends BaseCommand { static description = 'Show one LLM integration' - static examples = ['<%= config.bin %> ai llm 66b2f0a1...'] + static examples = ['<%= config.bin %> ai llm 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Integration ID'}), diff --git a/src/commands/ai/llm/default.ts b/src/commands/ai/llm/default.ts index e14dd1d..1f4b150 100644 --- a/src/commands/ai/llm/default.ts +++ b/src/commands/ai/llm/default.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../../base.js' export default class AiLlmDefault extends BaseCommand { static description = 'Make an LLM integration the project default' - static examples = ['<%= config.bin %> ai llm default 66b2f0a1...'] + static examples = [ + '<%= config.bin %> ai llm default 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> ai llm default 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(AiLlmDefault) const client = this.client(flags) diff --git a/src/commands/ai/llm/delete.ts b/src/commands/ai/llm/delete.ts index d69e9c0..f60ceb4 100644 --- a/src/commands/ai/llm/delete.ts +++ b/src/commands/ai/llm/delete.ts @@ -1,29 +1,28 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../../base.js' export default class AiLlmDelete extends BaseCommand { static description = 'Delete an LLM integration' - static examples = ['<%= config.bin %> ai llm delete 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> ai llm delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> ai llm delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(AiLlmDelete) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Delete LLM integration ${args.id}?`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Delete LLM integration ${args.id}?`, flags) const res = await client.hub.ai.deleteLlmIntegration({id: args.id}) diff --git a/src/commands/ai/llm/disable.ts b/src/commands/ai/llm/disable.ts index 3f6f1f0..f01e871 100644 --- a/src/commands/ai/llm/disable.ts +++ b/src/commands/ai/llm/disable.ts @@ -5,15 +5,23 @@ import {BaseCommand} from '../../../base.js' export default class AiLlmDisable extends BaseCommand { static description = 'Turn off an LLM integration' - static examples = ['<%= config.bin %> ai llm disable 66b2f0a1...'] + static examples = [ + '<%= config.bin %> ai llm disable 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> ai llm disable 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } + static flags = { + ...BaseCommand.mutatingFlags, + } + async run(): Promise { const {args, flags} = await this.parse(AiLlmDisable) const client = this.client(flags) + await this.confirmOrFail(`Turn off LLM integration ${args.id}?`, flags) const res = await client.hub.ai.disableLlmIntegration({id: args.id}) diff --git a/src/commands/ai/llm/enable.ts b/src/commands/ai/llm/enable.ts index e55b80d..4166606 100644 --- a/src/commands/ai/llm/enable.ts +++ b/src/commands/ai/llm/enable.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../../base.js' export default class AiLlmEnable extends BaseCommand { static description = 'Turn on an LLM integration' - static examples = ['<%= config.bin %> ai llm enable 66b2f0a1...'] + static examples = [ + '<%= config.bin %> ai llm enable 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> ai llm enable 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(AiLlmEnable) const client = this.client(flags) diff --git a/src/commands/ai/llm/save.ts b/src/commands/ai/llm/save.ts index 44bc40b..ff796fe 100644 --- a/src/commands/ai/llm/save.ts +++ b/src/commands/ai/llm/save.ts @@ -17,9 +17,11 @@ update an existing integration.` static examples = [ '<%= config.bin %> ai llm save --provider OpenAI --name OpenAI --model gpt-4o-mini --config @openai.json', '<%= config.bin %> ai llm save --provider Ollama --name Local --endpoint http://localhost:11434 --model llama3', + '<%= config.bin %> ai llm save --provider OpenAI --name OpenAI --model gpt-4o-mini --config @openai.json --dry-run', ] static flags = { + ...BaseCommand.dryRunFlags, provider: Flags.string({required: true, description: 'LLM provider', options: PROVIDERS}), name: Flags.string({required: true, description: 'Integration name'}), id: Flags.string({description: 'Integration ID — set it to update instead of create'}), diff --git a/src/commands/ai/llm/test.ts b/src/commands/ai/llm/test.ts index 4373c41..e5c21c9 100644 --- a/src/commands/ai/llm/test.ts +++ b/src/commands/ai/llm/test.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../../base.js' export default class AiLlmTest extends BaseCommand { static description = 'Check that an LLM integration works: a live call to the provider' - static examples = ['<%= config.bin %> ai llm test 66b2f0a1...'] + static examples = [ + '<%= config.bin %> ai llm test 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> ai llm test 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(AiLlmTest) const client = this.client(flags) diff --git a/src/commands/ai/mcp.ts b/src/commands/ai/mcp.ts index af30628..254e270 100644 --- a/src/commands/ai/mcp.ts +++ b/src/commands/ai/mcp.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class AiMcp extends BaseCommand { static description = 'Show one MCP server integration' - static examples = ['<%= config.bin %> ai mcp 66b2f0a1...'] + static examples = ['<%= config.bin %> ai mcp 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Integration ID'}), diff --git a/src/commands/ai/mcp/delete.ts b/src/commands/ai/mcp/delete.ts index 791c6fb..c70e414 100644 --- a/src/commands/ai/mcp/delete.ts +++ b/src/commands/ai/mcp/delete.ts @@ -1,29 +1,28 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../../base.js' export default class AiMcpDelete extends BaseCommand { static description = 'Delete an MCP server integration' - static examples = ['<%= config.bin %> ai mcp delete 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> ai mcp delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> ai mcp delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(AiMcpDelete) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Delete MCP server integration ${args.id}?`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Delete MCP server integration ${args.id}?`, flags) const res = await client.hub.ai.deleteMcpIntegration({id: args.id}) diff --git a/src/commands/ai/mcp/disable.ts b/src/commands/ai/mcp/disable.ts index 11f11a6..dd96ea0 100644 --- a/src/commands/ai/mcp/disable.ts +++ b/src/commands/ai/mcp/disable.ts @@ -5,15 +5,23 @@ import {BaseCommand} from '../../../base.js' export default class AiMcpDisable extends BaseCommand { static description = 'Turn off an MCP server integration' - static examples = ['<%= config.bin %> ai mcp disable 66b2f0a1...'] + static examples = [ + '<%= config.bin %> ai mcp disable 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> ai mcp disable 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } + static flags = { + ...BaseCommand.mutatingFlags, + } + async run(): Promise { const {args, flags} = await this.parse(AiMcpDisable) const client = this.client(flags) + await this.confirmOrFail(`Turn off MCP integration ${args.id}?`, flags) const res = await client.hub.ai.disableMcpIntegration({id: args.id}) diff --git a/src/commands/ai/mcp/enable.ts b/src/commands/ai/mcp/enable.ts index 2c859e4..746c609 100644 --- a/src/commands/ai/mcp/enable.ts +++ b/src/commands/ai/mcp/enable.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../../base.js' export default class AiMcpEnable extends BaseCommand { static description = 'Turn on an MCP server integration' - static examples = ['<%= config.bin %> ai mcp enable 66b2f0a1...'] + static examples = [ + '<%= config.bin %> ai mcp enable 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> ai mcp enable 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(AiMcpEnable) const client = this.client(flags) diff --git a/src/commands/ai/mcp/save.ts b/src/commands/ai/mcp/save.ts index 6ce2e4d..2408696 100644 --- a/src/commands/ai/mcp/save.ts +++ b/src/commands/ai/mcp/save.ts @@ -17,9 +17,11 @@ Pass --id to update an existing integration.` static examples = [ '<%= config.bin %> ai mcp save --provider GitHub --name GitHub --server-name GitHub --category Code --description "Issues and pull requests" --icon github --config @github.json', + '<%= config.bin %> ai mcp save --provider GitHub --name GitHub --server-name GitHub --category Code --description "Issues and pull requests" --icon github --config @github.json --dry-run', ] static flags = { + ...BaseCommand.dryRunFlags, provider: Flags.string({required: true, description: 'MCP server provider', options: PROVIDERS}), name: Flags.string({required: true, description: 'Integration name'}), id: Flags.string({description: 'Integration ID — set it to update instead of create'}), diff --git a/src/commands/ai/mcp/test.ts b/src/commands/ai/mcp/test.ts index ae42fbe..bbe826d 100644 --- a/src/commands/ai/mcp/test.ts +++ b/src/commands/ai/mcp/test.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../../base.js' export default class AiMcpTest extends BaseCommand { static description = 'Check that an MCP server integration works: connect to the server' - static examples = ['<%= config.bin %> ai mcp test 66b2f0a1...'] + static examples = [ + '<%= config.bin %> ai mcp test 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> ai mcp test 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(AiMcpTest) const client = this.client(flags) diff --git a/src/commands/ai/service-user/create.ts b/src/commands/ai/service-user/create.ts index 5aa1714..2994273 100644 --- a/src/commands/ai/service-user/create.ts +++ b/src/commands/ai/service-user/create.ts @@ -12,9 +12,11 @@ admin can change things. --env picks the environments (repeat the flag).` static examples = [ '<%= config.bin %> ai service-user create --name "Claude Code on my laptop" --reach project --rights read --env TEST', '<%= config.bin %> ai service-user create --name ci-bot --reach account --rights admin --env TEST --env PROD', + '<%= config.bin %> ai service-user create --name "Claude Code on my laptop" --reach project --rights read --env TEST --dry-run', ] static flags = { + ...BaseCommand.dryRunFlags, name: Flags.string({required: true, description: 'A name people recognise'}), reach: Flags.string({description: 'Whole account, or one project', options: ['account', 'project'], default: 'project'}), rights: Flags.string({description: 'What it may do', options: ['read', 'admin'], default: 'read'}), diff --git a/src/commands/ai/service-user/delete.ts b/src/commands/ai/service-user/delete.ts index 56d851d..6743d6d 100644 --- a/src/commands/ai/service-user/delete.ts +++ b/src/commands/ai/service-user/delete.ts @@ -1,29 +1,28 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../../base.js' export default class AiServiceUserDelete extends BaseCommand { static description = 'Delete an AI service user; its keys stop working at once' - static examples = ['<%= config.bin %> ai service-user delete 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> ai service-user delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> ai service-user delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Service user ID'}), } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(AiServiceUserDelete) const client = this.client(flags, {requireProject: false}) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Delete AI service user ${args.id}? Its keys stop working.`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Delete AI service user ${args.id}? Its keys stop working.`, flags) const res = await client.hub.account.deleteAiServiceUser({id: args.id}) diff --git a/src/commands/ai/service-user/revoke-key.ts b/src/commands/ai/service-user/revoke-key.ts index 0b93d84..28dc1d9 100644 --- a/src/commands/ai/service-user/revoke-key.ts +++ b/src/commands/ai/service-user/revoke-key.ts @@ -1,12 +1,14 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../../base.js' export default class AiServiceUserRevokeKey extends BaseCommand { static description = 'Revoke one API key of an AI service user; it stops working at once' - static examples = ['<%= config.bin %> ai service-user revoke-key 66b2f0a1... key_123 --yes'] + static examples = [ + '<%= config.bin %> ai service-user revoke-key 66b2f0a1c3d4e5f6a7b8c9d0 key_123 --yes', + '<%= config.bin %> ai service-user revoke-key 66b2f0a1c3d4e5f6a7b8c9d0 key_123 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Service user ID'}), @@ -14,17 +16,14 @@ export default class AiServiceUserRevokeKey extends BaseCommand { } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(AiServiceUserRevokeKey) const client = this.client(flags, {requireProject: false}) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Revoke key ${args.keyId}? It stops working at once.`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Revoke key ${args.keyId}? It stops working at once.`, flags) const res = await client.hub.account.revokeAiServiceUserKey({id: args.id, keyId: args.keyId}) diff --git a/src/commands/ai/service-user/rotate-key.ts b/src/commands/ai/service-user/rotate-key.ts index 3ee33eb..ade6ff4 100644 --- a/src/commands/ai/service-user/rotate-key.ts +++ b/src/commands/ai/service-user/rotate-key.ts @@ -9,8 +9,9 @@ The new key is shown once — store it now. The old keys keep working until you revoke them; --revoke revokes one in the same call.` static examples = [ - '<%= config.bin %> ai service-user rotate-key 66b2f0a1...', - '<%= config.bin %> ai service-user rotate-key 66b2f0a1... --revoke key_123', + '<%= config.bin %> ai service-user rotate-key 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> ai service-user rotate-key 66b2f0a1c3d4e5f6a7b8c9d0 --revoke key_123', + '<%= config.bin %> ai service-user rotate-key 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', ] static args = { @@ -18,6 +19,7 @@ revoke them; --revoke revokes one in the same call.` } static flags = { + ...BaseCommand.dryRunFlags, revoke: Flags.string({description: 'Key ID to revoke at the same time'}), } diff --git a/src/commands/email/campaign/batch.ts b/src/commands/email/campaign/batch.ts index 1d93111..4e8448a 100644 --- a/src/commands/email/campaign/batch.ts +++ b/src/commands/email/campaign/batch.ts @@ -9,8 +9,8 @@ Give a notification ID as the third argument to see just that e-mail. To see how it looked, run \`email preview --notification \`.` static examples = [ - '<%= config.bin %> email campaign batch 66b2f0a1... b7c3...', - '<%= config.bin %> email campaign batch 66b2f0a1... b7c3... n9d4...', + '<%= config.bin %> email campaign batch 66b2f0a1c3d4e5f6a7b8c9d0 b7c3d2e1f0a9b8c7d6e5f4a3', + '<%= config.bin %> email campaign batch 66b2f0a1c3d4e5f6a7b8c9d0 b7c3d2e1f0a9b8c7d6e5f4a3 9d4e5f6a7b8c9d0e1f2a3b4c', ] static args = { diff --git a/src/commands/email/campaign/batches.ts b/src/commands/email/campaign/batches.ts index b9f5771..0abbd10 100644 --- a/src/commands/email/campaign/batches.ts +++ b/src/commands/email/campaign/batches.ts @@ -6,8 +6,8 @@ export default class EmailCampaignBatches extends BaseCommand { static description = 'List the send batches of an email campaign' static examples = [ - '<%= config.bin %> email campaign batches 66b2f0a1...', - '<%= config.bin %> email campaign batches 66b2f0a1... --email ada@example.com', + '<%= config.bin %> email campaign batches 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> email campaign batches 66b2f0a1c3d4e5f6a7b8c9d0 --email ada@example.com', ] static args = { diff --git a/src/commands/email/campaign/create.ts b/src/commands/email/campaign/create.ts index 0baf32b..c37f589 100644 --- a/src/commands/email/campaign/create.ts +++ b/src/commands/email/campaign/create.ts @@ -19,12 +19,14 @@ every address first. Anything the flags do not cover goes in --config as a JSON object and is merged into the campaign.` static examples = [ - '<%= config.bin %> email campaign create --template 66b2f0a1... --audience all-users --tag beta', - '<%= config.bin %> email campaign create --template 66b2f0a1... --audience emails --email ada@example.com --at 2026-10-01T09:00:00Z', - '<%= config.bin %> email campaign create --template 66b2f0a1... --audience collection --schema subscribers --field email --field-type Email', + '<%= config.bin %> email campaign create --template 66b2f0a1c3d4e5f6a7b8c9d0 --audience all-users --tag beta', + '<%= config.bin %> email campaign create --template 66b2f0a1c3d4e5f6a7b8c9d0 --audience emails --email ada@example.com --at 2026-10-01T09:00:00Z', + '<%= config.bin %> email campaign create --template 66b2f0a1c3d4e5f6a7b8c9d0 --audience collection --schema subscribers --field email --field-type Email', + '<%= config.bin %> email campaign create --template 66b2f0a1c3d4e5f6a7b8c9d0 --audience all-users --tag beta --dry-run', ] static flags = { + ...BaseCommand.dryRunFlags, template: Flags.string({required: true, description: 'Template ID'}), audience: Flags.string({required: true, description: 'Who receives it', options: Object.keys(EMAIL_AUDIENCES)}), user: Flags.string({description: 'User ID (users / account-users; repeat for several)', multiple: true}), diff --git a/src/commands/email/campaign/delete.ts b/src/commands/email/campaign/delete.ts index 3054d93..d72b2d8 100644 --- a/src/commands/email/campaign/delete.ts +++ b/src/commands/email/campaign/delete.ts @@ -1,29 +1,28 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../../base.js' export default class EmailCampaignDelete extends BaseCommand { static description = 'Delete an email campaign' - static examples = ['<%= config.bin %> email campaign delete 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> email campaign delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> email campaign delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Campaign ID'}), } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(EmailCampaignDelete) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Delete email campaign ${args.id}?`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Delete email campaign ${args.id}?`, flags) const res = await client.hub.notifications.deleteEmailCampaign({id: args.id}) diff --git a/src/commands/email/campaign/messages.ts b/src/commands/email/campaign/messages.ts index 9eb6210..0270452 100644 --- a/src/commands/email/campaign/messages.ts +++ b/src/commands/email/campaign/messages.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../../base.js' export default class EmailCampaignMessages extends BaseCommand { static description = 'List the e-mails a campaign sent in one batch' - static examples = ['<%= config.bin %> email campaign messages 66b2f0a1... --batch b7c3...'] + static examples = ['<%= config.bin %> email campaign messages 66b2f0a1c3d4e5f6a7b8c9d0 --batch b7c3d2e1f0a9b8c7d6e5f4a3'] static args = { id: Args.string({required: true, description: 'Campaign ID'}), diff --git a/src/commands/email/disable.ts b/src/commands/email/disable.ts index 708ea54..d335aea 100644 --- a/src/commands/email/disable.ts +++ b/src/commands/email/disable.ts @@ -1,6 +1,3 @@ -import {confirm} from '@inquirer/prompts' -import {Flags} from '@oclif/core' - import {BaseCommand} from '../../base.js' export default class EmailDisable extends BaseCommand { @@ -8,20 +5,20 @@ export default class EmailDisable extends BaseCommand { Run \`email disable-dependencies\` first to see what still depends on email.` - static examples = ['<%= config.bin %> email disable --yes'] + static examples = [ + '<%= config.bin %> email disable --yes', + '<%= config.bin %> email disable --dry-run', + ] static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {flags} = await this.parse(EmailDisable) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: 'Turn off email for this project?', default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail('Turn off email for this project?', flags) const res = await client.hub.notifications.disableEmail({}) diff --git a/src/commands/email/enable.ts b/src/commands/email/enable.ts index ea0cbdf..e0c0d46 100644 --- a/src/commands/email/enable.ts +++ b/src/commands/email/enable.ts @@ -3,7 +3,14 @@ import {BaseCommand} from '../../base.js' export default class EmailEnable extends BaseCommand { static description = 'Turn on the email module for the project' - static examples = ['<%= config.bin %> email enable'] + static examples = [ + '<%= config.bin %> email enable', + '<%= config.bin %> email enable --dry-run', + ] + + static flags = { + ...BaseCommand.dryRunFlags, + } async run(): Promise { const {flags} = await this.parse(EmailEnable) diff --git a/src/commands/email/footer.ts b/src/commands/email/footer.ts index ec36049..97f2053 100644 --- a/src/commands/email/footer.ts +++ b/src/commands/email/footer.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class EmailFooter extends BaseCommand { static description = 'Show one email footer' - static examples = ['<%= config.bin %> email footer 66b2f0a1...'] + static examples = ['<%= config.bin %> email footer 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Footer ID'}), diff --git a/src/commands/email/footer/delete.ts b/src/commands/email/footer/delete.ts index 5e7cebd..abb00a6 100644 --- a/src/commands/email/footer/delete.ts +++ b/src/commands/email/footer/delete.ts @@ -1,29 +1,28 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../../base.js' export default class EmailFooterDelete extends BaseCommand { static description = 'Delete an email footer' - static examples = ['<%= config.bin %> email footer delete 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> email footer delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> email footer delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Footer ID'}), } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(EmailFooterDelete) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Delete email footer ${args.id}?`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Delete email footer ${args.id}?`, flags) const res = await client.hub.notifications.deleteEmailFooter({id: args.id}) diff --git a/src/commands/email/footer/save.ts b/src/commands/email/footer/save.ts index 1e28579..ebdf339 100644 --- a/src/commands/email/footer/save.ts +++ b/src/commands/email/footer/save.ts @@ -10,10 +10,14 @@ to update; the whole footer is sent, so include every language you want to keep. static examples = [ '<%= config.bin %> email footer save --name Default --content-file footer.html', - '<%= config.bin %> email footer save --id 66b2f0a1... --name Default --translations @footer.json', + '<%= config.bin %> email footer save --id 66b2f0a1c3d4e5f6a7b8c9d0 --name Default --translations @footer.json', + '<%= config.bin %> email footer save --name Default --content-file footer.html --dry-run', ] - static flags = snippetFlags('Footer') + static flags = { + ...BaseCommand.dryRunFlags, + ...snippetFlags('Footer'), + } async run(): Promise { const {flags} = await this.parse(EmailFooterSave) diff --git a/src/commands/email/integration.ts b/src/commands/email/integration.ts index 3ada84e..734adee 100644 --- a/src/commands/email/integration.ts +++ b/src/commands/email/integration.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class EmailIntegration extends BaseCommand { static description = 'Show one email integration' - static examples = ['<%= config.bin %> email integration 66b2f0a1...'] + static examples = ['<%= config.bin %> email integration 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Integration ID'}), diff --git a/src/commands/email/integration/confirm-delivery.ts b/src/commands/email/integration/confirm-delivery.ts index d0014c5..b284b57 100644 --- a/src/commands/email/integration/confirm-delivery.ts +++ b/src/commands/email/integration/confirm-delivery.ts @@ -7,12 +7,19 @@ export default class EmailIntegrationConfirmDelivery extends BaseCommand { Run it after \`email integration test\` once you saw the e-mail in the inbox.` - static examples = ['<%= config.bin %> email integration confirm-delivery 66b2f0a1...'] + static examples = [ + '<%= config.bin %> email integration confirm-delivery 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> email integration confirm-delivery 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(EmailIntegrationConfirmDelivery) const client = this.client(flags) diff --git a/src/commands/email/integration/default.ts b/src/commands/email/integration/default.ts index 68cd869..83f5027 100644 --- a/src/commands/email/integration/default.ts +++ b/src/commands/email/integration/default.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../../base.js' export default class EmailIntegrationDefault extends BaseCommand { static description = 'Make an email integration the project default (campaigns use it when they name none)' - static examples = ['<%= config.bin %> email integration default 66b2f0a1...'] + static examples = [ + '<%= config.bin %> email integration default 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> email integration default 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(EmailIntegrationDefault) const client = this.client(flags) diff --git a/src/commands/email/integration/delete.ts b/src/commands/email/integration/delete.ts index 63fc6db..969e797 100644 --- a/src/commands/email/integration/delete.ts +++ b/src/commands/email/integration/delete.ts @@ -1,29 +1,28 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../../base.js' export default class EmailIntegrationDelete extends BaseCommand { static description = 'Delete an email integration' - static examples = ['<%= config.bin %> email integration delete 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> email integration delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> email integration delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(EmailIntegrationDelete) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Delete email integration ${args.id}?`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Delete email integration ${args.id}?`, flags) const res = await client.hub.notifications.deleteEmailIntegration({id: args.id}) diff --git a/src/commands/email/integration/disable.ts b/src/commands/email/integration/disable.ts index 09b77de..ae39fe6 100644 --- a/src/commands/email/integration/disable.ts +++ b/src/commands/email/integration/disable.ts @@ -5,15 +5,23 @@ import {BaseCommand} from '../../../base.js' export default class EmailIntegrationDisable extends BaseCommand { static description = 'Turn an email integration off' - static examples = ['<%= config.bin %> email integration disable 66b2f0a1...'] + static examples = [ + '<%= config.bin %> email integration disable 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> email integration disable 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } + static flags = { + ...BaseCommand.mutatingFlags, + } + async run(): Promise { const {args, flags} = await this.parse(EmailIntegrationDisable) const client = this.client(flags) + await this.confirmOrFail(`Turn off email integration ${args.id}?`, flags) const res = await client.hub.notifications.disableEmailIntegration({id: args.id}) diff --git a/src/commands/email/integration/domain-health.ts b/src/commands/email/integration/domain-health.ts index 555b66a..14c687b 100644 --- a/src/commands/email/integration/domain-health.ts +++ b/src/commands/email/integration/domain-health.ts @@ -7,7 +7,7 @@ export default class EmailIntegrationDomainHealth extends BaseCommand { Read only — nothing is sent.` - static examples = ['<%= config.bin %> email integration domain-health 66b2f0a1...'] + static examples = ['<%= config.bin %> email integration domain-health 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Integration ID'}), diff --git a/src/commands/email/integration/enable.ts b/src/commands/email/integration/enable.ts index 510ef6f..0b57836 100644 --- a/src/commands/email/integration/enable.ts +++ b/src/commands/email/integration/enable.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../../base.js' export default class EmailIntegrationEnable extends BaseCommand { static description = 'Turn an email integration on' - static examples = ['<%= config.bin %> email integration enable 66b2f0a1...'] + static examples = [ + '<%= config.bin %> email integration enable 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> email integration enable 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(EmailIntegrationEnable) const client = this.client(flags) diff --git a/src/commands/email/integration/save.ts b/src/commands/email/integration/save.ts index 07d2bcb..df074c6 100644 --- a/src/commands/email/integration/save.ts +++ b/src/commands/email/integration/save.ts @@ -19,9 +19,11 @@ Pass --id to update an existing integration.` static examples = [ '<%= config.bin %> email integration save --provider Fake', '<%= config.bin %> email integration save --provider SendGrid --name SendGrid --from hello@example.com --config @sendgrid.json', + '<%= config.bin %> email integration save --provider Fake --dry-run', ] static flags = { + ...BaseCommand.dryRunFlags, provider: Flags.string({required: true, description: 'Email provider', options: PROVIDERS}), name: Flags.string({description: 'Integration name (Fake ignores it and uses its own)'}), id: Flags.string({description: 'Integration ID — set it to update instead of create'}), diff --git a/src/commands/email/integration/test.ts b/src/commands/email/integration/test.ts index 029fae9..94f5ad2 100644 --- a/src/commands/email/integration/test.ts +++ b/src/commands/email/integration/test.ts @@ -9,9 +9,13 @@ Point this at the Fake integration while you are developing — it accepts the send and contacts no mail service, so nothing reaches a real inbox. With a real provider, run \`email integration confirm-delivery\` once the e-mail arrived.` - static examples = ['<%= config.bin %> email integration test --integration 66b2f0a1... --to dev@example.com'] + static examples = [ + '<%= config.bin %> email integration test --integration 66b2f0a1c3d4e5f6a7b8c9d0 --to dev@example.com', + '<%= config.bin %> email integration test --integration 66b2f0a1c3d4e5f6a7b8c9d0 --to dev@example.com --dry-run', + ] static flags = { + ...BaseCommand.dryRunFlags, integration: Flags.string({required: true, description: 'Integration ID to send through'}), to: Flags.string({required: true, description: 'E-mail address to send the test to'}), } diff --git a/src/commands/email/preview.ts b/src/commands/email/preview.ts index 15833f0..05fb753 100644 --- a/src/commands/email/preview.ts +++ b/src/commands/email/preview.ts @@ -15,9 +15,9 @@ Two ways in: When you are logged in, your session is sent with a hash too.` static examples = [ - '<%= config.bin %> email preview 8f2a91c4...', - '<%= config.bin %> email preview --hash 8f2a91c4... --region nb-eu-germany', - '<%= config.bin %> email preview --notification n9d4...', + '<%= config.bin %> email preview 8f2a91c4d7e6b5a4c3d2e1f0', + '<%= config.bin %> email preview --hash 8f2a91c4d7e6b5a4c3d2e1f0 --region nb-eu-germany', + '<%= config.bin %> email preview --notification 9d4e5f6a7b8c9d0e1f2a3b4c', ] static args = { diff --git a/src/commands/email/signature.ts b/src/commands/email/signature.ts index 5fbdd9e..3ab4837 100644 --- a/src/commands/email/signature.ts +++ b/src/commands/email/signature.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class EmailSignature extends BaseCommand { static description = 'Show one email signature' - static examples = ['<%= config.bin %> email signature 66b2f0a1...'] + static examples = ['<%= config.bin %> email signature 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Signature ID'}), diff --git a/src/commands/email/signature/delete.ts b/src/commands/email/signature/delete.ts index 3ac54d0..bb1cbb3 100644 --- a/src/commands/email/signature/delete.ts +++ b/src/commands/email/signature/delete.ts @@ -1,29 +1,28 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../../base.js' export default class EmailSignatureDelete extends BaseCommand { static description = 'Delete an email signature' - static examples = ['<%= config.bin %> email signature delete 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> email signature delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> email signature delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Signature ID'}), } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(EmailSignatureDelete) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Delete email signature ${args.id}?`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Delete email signature ${args.id}?`, flags) const res = await client.hub.notifications.deleteEmailSignature({id: args.id}) diff --git a/src/commands/email/signature/save.ts b/src/commands/email/signature/save.ts index 2896223..d22c4dc 100644 --- a/src/commands/email/signature/save.ts +++ b/src/commands/email/signature/save.ts @@ -10,10 +10,14 @@ to update; the whole signature is sent, so include every language you want to ke static examples = [ '<%= config.bin %> email signature save --name Default --content-file signature.html', - '<%= config.bin %> email signature save --id 66b2f0a1... --name Default --translations @signature.json', + '<%= config.bin %> email signature save --id 66b2f0a1c3d4e5f6a7b8c9d0 --name Default --translations @signature.json', + '<%= config.bin %> email signature save --name Default --content-file signature.html --dry-run', ] - static flags = snippetFlags('Signature') + static flags = { + ...BaseCommand.dryRunFlags, + ...snippetFlags('Signature'), + } async run(): Promise { const {flags} = await this.parse(EmailSignatureSave) diff --git a/src/commands/email/system-template.ts b/src/commands/email/system-template.ts index e78ca2a..f77a3fc 100644 --- a/src/commands/email/system-template.ts +++ b/src/commands/email/system-template.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class EmailSystemTemplate extends BaseCommand { static description = 'Show one ready-made (system) email template' - static examples = ['<%= config.bin %> email system-template 66b2f0a1...'] + static examples = ['<%= config.bin %> email system-template 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'System template ID'}), diff --git a/src/commands/email/template/attach.ts b/src/commands/email/template/attach.ts index e4d87ac..b88fb30 100644 --- a/src/commands/email/template/attach.ts +++ b/src/commands/email/template/attach.ts @@ -13,8 +13,9 @@ JSON, @file.json, or - for stdin). --language attaches it to one translation only.` static examples = [ - '<%= config.bin %> email template attach 66b2f0a1... --file-ref @terms-ref.json', - '<%= config.bin %> email template attach 66b2f0a1... --file-ref @terms-ref.json --language lt', + '<%= config.bin %> email template attach 66b2f0a1c3d4e5f6a7b8c9d0 --file-ref @terms-ref.json', + '<%= config.bin %> email template attach 66b2f0a1c3d4e5f6a7b8c9d0 --file-ref @terms-ref.json --language lt', + '<%= config.bin %> email template attach 66b2f0a1c3d4e5f6a7b8c9d0 --file-ref @terms-ref.json --dry-run', ] static args = { @@ -22,6 +23,7 @@ only.` } static flags = { + ...BaseCommand.dryRunFlags, 'file-ref': Flags.string({required: true, description: 'File reference as a JSON object, @file or -'}), language: Flags.string({description: 'Attach to this translation only'}), } diff --git a/src/commands/email/template/create.ts b/src/commands/email/template/create.ts index 0c4d139..ce88867 100644 --- a/src/commands/email/template/create.ts +++ b/src/commands/email/template/create.ts @@ -14,9 +14,13 @@ Check the code first with \`email template render\`.` static examples = [ '<%= config.bin %> email template create --name Welcome --subject "Hi @Model.Name" --body-file welcome.mjml', '<%= config.bin %> email template create --name Welcome --translations @welcome.json', + '<%= config.bin %> email template create --name Welcome --subject "Hi @Model.Name" --body-file welcome.mjml --dry-run', ] - static flags = emailTemplateFlags + static flags = { + ...BaseCommand.dryRunFlags, + ...emailTemplateFlags, + } async run(): Promise { const {flags} = await this.parse(EmailTemplateCreate) diff --git a/src/commands/email/template/tokens.ts b/src/commands/email/template/tokens.ts index 607f3df..26c94bb 100644 --- a/src/commands/email/template/tokens.ts +++ b/src/commands/email/template/tokens.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../../base.js' export default class EmailTemplateTokens extends BaseCommand { static description = 'List the tokens an email template uses' - static examples = ['<%= config.bin %> email template tokens 66b2f0a1...'] + static examples = ['<%= config.bin %> email template tokens 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Template ID'}), diff --git a/src/commands/email/template/update.ts b/src/commands/email/template/update.ts index 594daae..e5b09e4 100644 --- a/src/commands/email/template/update.ts +++ b/src/commands/email/template/update.ts @@ -9,14 +9,18 @@ export default class EmailTemplateUpdate extends BaseCommand { The whole template is sent, so pass every language you want to keep.` static examples = [ - '<%= config.bin %> email template update 66b2f0a1... --name Welcome --subject "Hi @Model.Name" --body-file welcome.mjml', + '<%= config.bin %> email template update 66b2f0a1c3d4e5f6a7b8c9d0 --name Welcome --subject "Hi @Model.Name" --body-file welcome.mjml', + '<%= config.bin %> email template update 66b2f0a1c3d4e5f6a7b8c9d0 --name Welcome --subject "Hi @Model.Name" --body-file welcome.mjml --dry-run', ] static args = { id: Args.string({required: true, description: 'Template ID'}), } - static flags = emailTemplateFlags + static flags = { + ...BaseCommand.dryRunFlags, + ...emailTemplateFlags, + } async run(): Promise { const {args, flags} = await this.parse(EmailTemplateUpdate) diff --git a/src/commands/email/validation/save.ts b/src/commands/email/validation/save.ts index cd5a180..0ad3da0 100644 --- a/src/commands/email/validation/save.ts +++ b/src/commands/email/validation/save.ts @@ -15,9 +15,13 @@ credentials (for example {"apiKey": "..."}) in --config — inline JSON, @file.json, or - for stdin — so the key does not land in your shell history. Pass --id to update an existing integration.` - static examples = ['<%= config.bin %> email validation save --provider ZeroBounce --name ZeroBounce --config @zerobounce.json'] + static examples = [ + '<%= config.bin %> email validation save --provider ZeroBounce --name ZeroBounce --config @zerobounce.json', + '<%= config.bin %> email validation save --provider ZeroBounce --name ZeroBounce --config @zerobounce.json --dry-run', + ] static flags = { + ...BaseCommand.dryRunFlags, provider: Flags.string({required: true, description: 'Validation provider', options: PROVIDERS}), name: Flags.string({required: true, description: 'Integration name'}), id: Flags.string({description: 'Integration ID — set it to update instead of create'}), diff --git a/src/commands/email/validation/test.ts b/src/commands/email/validation/test.ts index f4a068f..3c1ad93 100644 --- a/src/commands/email/validation/test.ts +++ b/src/commands/email/validation/test.ts @@ -7,12 +7,19 @@ export default class EmailValidationTest extends BaseCommand { The provider checks one test address; no e-mail is sent.` - static examples = ['<%= config.bin %> email validation test 66b2f0a1...'] + static examples = [ + '<%= config.bin %> email validation test 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> email validation test 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Validation integration ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(EmailValidationTest) const client = this.client(flags) diff --git a/src/commands/project.ts b/src/commands/project.ts index b0e3e5f..49c140a 100644 --- a/src/commands/project.ts +++ b/src/commands/project.ts @@ -8,7 +8,7 @@ export default class Project extends ProjectCommand { Shows the configured project unless you pass another project ID. List every project of the account with \`account projects\`.` - static examples = ['<%= config.bin %> project', '<%= config.bin %> project 66b2f0a1... --json'] + static examples = ['<%= config.bin %> project', '<%= config.bin %> project 66b2f0a1c3d4e5f6a7b8c9d0 --json'] static args = { id: Args.string({description: 'Project ID (defaults to the configured project)'}), diff --git a/src/commands/project/admin-portal/disable.ts b/src/commands/project/admin-portal/disable.ts index 38d8989..0a8851f 100644 --- a/src/commands/project/admin-portal/disable.ts +++ b/src/commands/project/admin-portal/disable.ts @@ -1,21 +1,22 @@ -import {Flags} from '@oclif/core' - import {ProjectCommand} from '../../../lib/project.js' export default class ProjectAdminPortalDisable extends ProjectCommand { static description = "Turn off the project's admin portal (its users can no longer sign in there)" - static examples = ['<%= config.bin %> project admin-portal disable --yes'] + static examples = [ + '<%= config.bin %> project admin-portal disable --yes', + '<%= config.bin %> project admin-portal disable --dry-run', + ] static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...ProjectCommand.mutatingFlags, } async run(): Promise { const {flags} = await this.parse(ProjectAdminPortalDisable) const {client, projectId} = this.projectClient(flags) - if (!(await this.confirmOrStop(flags.yes, 'Turn off the admin portal? Its users can no longer sign in there.'))) return + await this.confirmOrFail('Turn off the admin portal? Its users can no longer sign in there.', flags) const res = await client.hub.account.setAdminPortalEnabled({projectId, enabled: false}) diff --git a/src/commands/project/admin-portal/enable.ts b/src/commands/project/admin-portal/enable.ts index 1c89186..372d21d 100644 --- a/src/commands/project/admin-portal/enable.ts +++ b/src/commands/project/admin-portal/enable.ts @@ -6,7 +6,14 @@ export default class ProjectAdminPortalEnable extends ProjectCommand { The admin portal is the end-user site Norbix generates for the project. See its address with \`project\` (effectiveAdminUrl).` - static examples = ['<%= config.bin %> project admin-portal enable'] + static examples = [ + '<%= config.bin %> project admin-portal enable', + '<%= config.bin %> project admin-portal enable --dry-run', + ] + + static flags = { + ...ProjectCommand.dryRunFlags, + } async run(): Promise { const {flags} = await this.parse(ProjectAdminPortalEnable) diff --git a/src/commands/project/admin-portal/set-service-user.ts b/src/commands/project/admin-portal/set-service-user.ts index d79a8b1..8eac7c9 100644 --- a/src/commands/project/admin-portal/set-service-user.ts +++ b/src/commands/project/admin-portal/set-service-user.ts @@ -7,12 +7,19 @@ export default class ProjectAdminPortalSetServiceUser extends ProjectCommand { The admin portal calls the project's APIs under this user's permissions.` - static examples = ['<%= config.bin %> project admin-portal set-service-user 66b2f0a1...'] + static examples = [ + '<%= config.bin %> project admin-portal set-service-user 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> project admin-portal set-service-user 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Service user ID'}), } + static flags = { + ...ProjectCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(ProjectAdminPortalSetServiceUser) const {client, projectId} = this.projectClient(flags) diff --git a/src/commands/project/admin-portal/set-url.ts b/src/commands/project/admin-portal/set-url.ts index d0545b5..b8bef89 100644 --- a/src/commands/project/admin-portal/set-url.ts +++ b/src/commands/project/admin-portal/set-url.ts @@ -10,6 +10,7 @@ export default class ProjectAdminPortalSetUrl extends ProjectCommand { static examples = [ '<%= config.bin %> project admin-portal set-url https://admin.example.com', '<%= config.bin %> project admin-portal set-url --clear', + '<%= config.bin %> project admin-portal set-url https://admin.example.com --dry-run', ] static args = { @@ -17,6 +18,7 @@ export default class ProjectAdminPortalSetUrl extends ProjectCommand { } static flags = { + ...ProjectCommand.dryRunFlags, clear: Flags.boolean({description: 'Use the Norbix address again', default: false}), } diff --git a/src/commands/project/ai/assistant/create.ts b/src/commands/project/ai/assistant/create.ts index 5e64e00..3253007 100644 --- a/src/commands/project/ai/assistant/create.ts +++ b/src/commands/project/ai/assistant/create.ts @@ -9,9 +9,13 @@ the LLM and model it runs on. List them with \`project ai settings\`.` static examples = [ '<%= config.bin %> project ai assistant create --name Support --system-prompt-file support.md --toolset ai:database-read', + '<%= config.bin %> project ai assistant create --name Support --system-prompt-file support.md --toolset ai:database-read --dry-run', ] - static flags = assistantFlags + static flags = { + ...ProjectCommand.dryRunFlags, + ...assistantFlags, + } async run(): Promise { const {flags} = await this.parse(ProjectAiAssistantCreate) diff --git a/src/commands/project/ai/assistant/delete.ts b/src/commands/project/ai/assistant/delete.ts index fc292dd..838ecd8 100644 --- a/src/commands/project/ai/assistant/delete.ts +++ b/src/commands/project/ai/assistant/delete.ts @@ -1,25 +1,28 @@ -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {ProjectCommand} from '../../../../lib/project.js' export default class ProjectAiAssistantDelete extends ProjectCommand { static description = 'Delete an AI assistant' - static examples = ['<%= config.bin %> project ai assistant delete 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> project ai assistant delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> project ai assistant delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Assistant ID'}), } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...ProjectCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(ProjectAiAssistantDelete) const {client, projectId} = this.projectClient(flags) - if (!(await this.confirmOrStop(flags.yes, `Delete AI assistant ${args.id}?`))) return + await this.confirmOrFail(`Delete AI assistant ${args.id}?`, flags) const res = await client.hub.account.deleteProjectAiAssistant({projectId, assistantId: args.id}) diff --git a/src/commands/project/ai/assistant/update.ts b/src/commands/project/ai/assistant/update.ts index 77ccba6..4a839bf 100644 --- a/src/commands/project/ai/assistant/update.ts +++ b/src/commands/project/ai/assistant/update.ts @@ -10,20 +10,24 @@ The server replaces the whole assistant, so this reads it first and changes only what you pass. --no-memory / --no-default turn those off.` static examples = [ - '<%= config.bin %> project ai assistant update 66b2f0a1... --model gpt-4o --no-memory', + '<%= config.bin %> project ai assistant update 66b2f0a1c3d4e5f6a7b8c9d0 --model gpt-4o --no-memory', + '<%= config.bin %> project ai assistant update 66b2f0a1c3d4e5f6a7b8c9d0 --model gpt-4o --no-memory --dry-run', ] static args = { id: Args.string({required: true, description: 'Assistant ID'}), } - static flags = assistantFlags + static flags = { + ...ProjectCommand.dryRunFlags, + ...assistantFlags, + } async run(): Promise { const {args, flags} = await this.parse(ProjectAiAssistantUpdate) - const {client, projectId} = this.projectClient(flags) + const {client, reader, projectId} = this.projectClient(flags) - const settings = (await client.hub.account.getProjectAiSettings({projectId})) as {result?: {assistants?: Assistant[]}} + const settings = (await reader.hub.account.getProjectAiSettings({projectId})) as {result?: {assistants?: Assistant[]}} const current = settings.result?.assistants?.find((a) => a.id === args.id) if (!current) this.error(`No assistant ${args.id} in this project. See them with \`norbix project ai settings\`.`) diff --git a/src/commands/project/ai/settings/set.ts b/src/commands/project/ai/settings/set.ts index addcbd2..21186c6 100644 --- a/src/commands/project/ai/settings/set.ts +++ b/src/commands/project/ai/settings/set.ts @@ -15,11 +15,13 @@ The server replaces all three settings at once, so this reads the current ones first and changes only what you pass.` static examples = [ - '<%= config.bin %> project ai settings set --enable --llm 66b2f0a1... --model gpt-4o-mini', + '<%= config.bin %> project ai settings set --enable --llm 66b2f0a1c3d4e5f6a7b8c9d0 --model gpt-4o-mini', '<%= config.bin %> project ai settings set --disable', + '<%= config.bin %> project ai settings set --enable --llm 66b2f0a1c3d4e5f6a7b8c9d0 --model gpt-4o-mini --dry-run', ] static flags = { + ...ProjectCommand.dryRunFlags, enable: Flags.boolean({description: 'Turn AI chat on', exclusive: ['disable']}), disable: Flags.boolean({description: 'Turn AI chat off', exclusive: ['enable']}), llm: Flags.string({description: 'Default LLM integration ID (see `ai llms`)'}), @@ -32,9 +34,9 @@ first and changes only what you pass.` this.error('Pass --enable, --disable, --llm or --model.') } - const {client, projectId} = this.projectClient(flags) + const {client, reader, projectId} = this.projectClient(flags) - const current = ((await client.hub.account.getProjectAiSettings({projectId})) as {result?: AiSettings}).result ?? {} + const current = ((await reader.hub.account.getProjectAiSettings({projectId})) as {result?: AiSettings}).result ?? {} const res = await client.hub.account.updateProjectAiSettings({ projectId, enabled: flags.enable ? true : flags.disable ? false : (current.enabled ?? false), diff --git a/src/commands/project/cors/add.ts b/src/commands/project/cors/add.ts index 89e8ac3..000f27b 100644 --- a/src/commands/project/cors/add.ts +++ b/src/commands/project/cors/add.ts @@ -8,7 +8,10 @@ export default class ProjectCorsAdd extends ProjectCommand { The server stores the full list, so this reads the current origins first and writes them back with the new ones.` - static examples = ['<%= config.bin %> project cors add https://staging.example.com'] + static examples = [ + '<%= config.bin %> project cors add https://staging.example.com', + '<%= config.bin %> project cors add https://staging.example.com --dry-run', + ] static strict = false @@ -16,12 +19,16 @@ writes them back with the new ones.` origin: Args.string({required: true, description: 'Origins to allow (one or more)'}), } + static flags = { + ...ProjectCommand.dryRunFlags, + } + async run(): Promise { const {argv, flags} = await this.parse(ProjectCorsAdd) const wanted = argv as string[] - const {client, projectId} = this.projectClient(flags) + const {client, reader, projectId} = this.projectClient(flags) - const current = originsOf(await client.hub.account.getProject({projectId})) + const current = originsOf(await reader.hub.account.getProject({projectId})) const added = wanted.filter((o) => !current.some((c) => sameOrigin(c, o))) if (added.length === 0) { this.print('Already allowed — nothing to change.') diff --git a/src/commands/project/cors/remove.ts b/src/commands/project/cors/remove.ts index 85f88b9..4f35f4a 100644 --- a/src/commands/project/cors/remove.ts +++ b/src/commands/project/cors/remove.ts @@ -9,7 +9,10 @@ The server stores the full list, so this reads the current origins first and writes them back without the ones you name. Removing the project's own admin portal origin needs --remove-admin-portal-origin.` - static examples = ['<%= config.bin %> project cors remove https://staging.example.com'] + static examples = [ + '<%= config.bin %> project cors remove https://staging.example.com --yes', + '<%= config.bin %> project cors remove https://staging.example.com --dry-run', + ] static strict = false @@ -18,6 +21,7 @@ portal origin needs --remove-admin-portal-origin.` } static flags = { + ...ProjectCommand.mutatingFlags, 'remove-admin-portal-origin': Flags.boolean({ description: "Allow removing the project's own admin portal origin", default: false, @@ -27,22 +31,25 @@ portal origin needs --remove-admin-portal-origin.` async run(): Promise { const {argv, flags} = await this.parse(ProjectCorsRemove) const unwanted = argv as string[] - const {client, projectId} = this.projectClient(flags) + const {client, reader, projectId} = this.projectClient(flags) - const current = originsOf(await client.hub.account.getProject({projectId})) + const current = originsOf(await reader.hub.account.getProject({projectId})) const origins = current.filter((c) => !unwanted.some((o) => sameOrigin(c, o))) if (origins.length === current.length) { this.print('None of these origins is allowed — nothing to change.') return {origins: current} } + const removed = current.filter((c) => !origins.includes(c)) + await this.confirmOrFail(`Stop allowing ${removed.join(', ')}? Browsers on these origins are refused.`, flags) + await client.hub.account.updateProjectAllowedOrigins({ projectId, origins, removeAdminPortalOrigin: flags['remove-admin-portal-origin'] || undefined, }) - this.print(`Removed: ${current.filter((c) => !origins.includes(c)).join(', ')}.`) + this.print(`Removed: ${removed.join(', ')}.`) return {origins} } } diff --git a/src/commands/project/cors/set.ts b/src/commands/project/cors/set.ts index 39a47c7..29d16fb 100644 --- a/src/commands/project/cors/set.ts +++ b/src/commands/project/cors/set.ts @@ -12,6 +12,7 @@ Every origin not in the list stops being allowed. An origin with no scheme static examples = [ '<%= config.bin %> project cors set https://app.example.com https://example.com', '<%= config.bin %> project cors set https://app.example.com --remove-admin-portal-origin', + '<%= config.bin %> project cors set https://app.example.com https://example.com --dry-run', ] static strict = false @@ -21,6 +22,7 @@ Every origin not in the list stops being allowed. An origin with no scheme } static flags = { + ...ProjectCommand.dryRunFlags, 'remove-admin-portal-origin': Flags.boolean({ description: "Allow the list to drop the project's own admin portal origin", default: false, diff --git a/src/commands/project/delete.ts b/src/commands/project/delete.ts index 67ad74e..97a5a89 100644 --- a/src/commands/project/delete.ts +++ b/src/commands/project/delete.ts @@ -1,5 +1,3 @@ -import {Flags} from '@oclif/core' - import {ProjectCommand} from '../../lib/project.js' export default class ProjectDelete extends ProjectCommand { @@ -8,17 +6,20 @@ export default class ProjectDelete extends ProjectCommand { This removes the project with its data and cannot be undone. Without --yes the command asks first, and refuses when there is no terminal to ask in.` - static examples = ['<%= config.bin %> project delete --project 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> project delete --project 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> project delete --project 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...ProjectCommand.mutatingFlags, } async run(): Promise { const {flags} = await this.parse(ProjectDelete) const {client, projectId} = this.projectClient(flags) - if (!(await this.confirmOrStop(flags.yes, `Delete project ${projectId} and all its data? This cannot be undone.`))) return + await this.confirmOrFail(`Delete project ${projectId} and all its data? This cannot be undone.`, flags) const res = await client.hub.account.deleteProject({projectId}) diff --git a/src/commands/project/disable.ts b/src/commands/project/disable.ts index 1ffd0d5..4223584 100644 --- a/src/commands/project/disable.ts +++ b/src/commands/project/disable.ts @@ -1,5 +1,3 @@ -import {Flags} from '@oclif/core' - import {ProjectCommand} from '../../lib/project.js' export default class ProjectDisable extends ProjectCommand { @@ -7,17 +5,20 @@ export default class ProjectDisable extends ProjectCommand { Its APIs stop answering until \`project enable\`. Nothing is deleted.` - static examples = ['<%= config.bin %> project disable --yes'] + static examples = [ + '<%= config.bin %> project disable --yes', + '<%= config.bin %> project disable --dry-run', + ] static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...ProjectCommand.mutatingFlags, } async run(): Promise { const {flags} = await this.parse(ProjectDisable) const {client, projectId} = this.projectClient(flags) - if (!(await this.confirmOrStop(flags.yes, `Turn off project ${projectId}? Its APIs stop answering.`))) return + await this.confirmOrFail(`Turn off project ${projectId}? Its APIs stop answering.`, flags) const res = await client.hub.account.disableProject({projectId}) diff --git a/src/commands/project/enable.ts b/src/commands/project/enable.ts index c63ec76..04a155d 100644 --- a/src/commands/project/enable.ts +++ b/src/commands/project/enable.ts @@ -3,7 +3,14 @@ import {ProjectCommand} from '../../lib/project.js' export default class ProjectEnable extends ProjectCommand { static description = 'Turn a disabled project back on' - static examples = ['<%= config.bin %> project enable'] + static examples = [ + '<%= config.bin %> project enable', + '<%= config.bin %> project enable --dry-run', + ] + + static flags = { + ...ProjectCommand.dryRunFlags, + } async run(): Promise { const {flags} = await this.parse(ProjectEnable) diff --git a/src/commands/project/legal/expose.ts b/src/commands/project/legal/expose.ts index 5ccc5f3..ddbdb4a 100644 --- a/src/commands/project/legal/expose.ts +++ b/src/commands/project/legal/expose.ts @@ -3,7 +3,14 @@ import {ProjectCommand} from '../../../lib/project.js' export default class ProjectLegalExpose extends ProjectCommand { static description = "Show the project's legal documents to the public (admin portal and the public legal route)" - static examples = ['<%= config.bin %> project legal expose'] + static examples = [ + '<%= config.bin %> project legal expose', + '<%= config.bin %> project legal expose --dry-run', + ] + + static flags = { + ...ProjectCommand.dryRunFlags, + } async run(): Promise { const {flags} = await this.parse(ProjectLegalExpose) diff --git a/src/commands/project/legal/hide.ts b/src/commands/project/legal/hide.ts index 435941a..2c4ef2c 100644 --- a/src/commands/project/legal/hide.ts +++ b/src/commands/project/legal/hide.ts @@ -3,7 +3,14 @@ import {ProjectCommand} from '../../../lib/project.js' export default class ProjectLegalHide extends ProjectCommand { static description = "Stop showing the project's legal documents to the public" - static examples = ['<%= config.bin %> project legal hide'] + static examples = [ + '<%= config.bin %> project legal hide', + '<%= config.bin %> project legal hide --dry-run', + ] + + static flags = { + ...ProjectCommand.dryRunFlags, + } async run(): Promise { const {flags} = await this.parse(ProjectLegalHide) diff --git a/src/commands/project/legal/set.ts b/src/commands/project/legal/set.ts index 745b23b..e8541ee 100644 --- a/src/commands/project/legal/set.ts +++ b/src/commands/project/legal/set.ts @@ -14,9 +14,11 @@ the public with \`project legal expose\`.` '<%= config.bin %> project legal set --terms-file terms.md --privacy-file privacy.md', '<%= config.bin %> project legal set --terms-file terms.md', '<%= config.bin %> project legal set --clear-privacy', + '<%= config.bin %> project legal set --terms-file terms.md --privacy-file privacy.md --dry-run', ] static flags = { + ...ProjectCommand.dryRunFlags, 'terms-file': Flags.string({description: 'Terms & Conditions, a Markdown file', exclusive: ['clear-terms']}), 'privacy-file': Flags.string({description: 'Privacy Policy, a Markdown file', exclusive: ['clear-privacy']}), 'clear-terms': Flags.boolean({description: 'Remove the Terms & Conditions', default: false}), @@ -31,12 +33,12 @@ the public with \`project legal expose\`.` this.error('Pass --terms-file, --privacy-file, --clear-terms or --clear-privacy.') } - const {client, projectId} = this.projectClient(flags) + const {client, reader, projectId} = this.projectClient(flags) let termsMarkdown = terms let privacyMarkdown = privacy if (termsMarkdown === undefined || privacyMarkdown === undefined) { - const current = projectOf(await client.hub.account.getProject({projectId})) + const current = projectOf(await reader.hub.account.getProject({projectId})) termsMarkdown ??= current.legalTermsMarkdown as string | undefined privacyMarkdown ??= current.legalPrivacyMarkdown as string | undefined } diff --git a/src/commands/project/public-config.ts b/src/commands/project/public-config.ts index a663aa5..b783985 100644 --- a/src/commands/project/public-config.ts +++ b/src/commands/project/public-config.ts @@ -8,7 +8,7 @@ export default class ProjectPublicConfig extends ProjectCommand { This is what the admin portal and other apps read before anyone signs in (API host, public route).` - static examples = ['<%= config.bin %> project public-config', '<%= config.bin %> project public-config 66b2f0a1... --json'] + static examples = ['<%= config.bin %> project public-config', '<%= config.bin %> project public-config 66b2f0a1c3d4e5f6a7b8c9d0 --json'] static args = { id: Args.string({description: 'Project ID (defaults to the configured project)'}), diff --git a/src/commands/project/set-colors.ts b/src/commands/project/set-colors.ts index a145e4b..989bae6 100644 --- a/src/commands/project/set-colors.ts +++ b/src/commands/project/set-colors.ts @@ -11,9 +11,11 @@ Colours are hex values such as #1F6FEB.` static examples = [ '<%= config.bin %> project set-colors --main "#1F6FEB" --accent "#F78166"', '<%= config.bin %> project set-colors --accent "#F78166"', + '<%= config.bin %> project set-colors --main "#1F6FEB" --accent "#F78166" --dry-run', ] static flags = { + ...ProjectCommand.dryRunFlags, main: Flags.string({description: 'Main brand colour (hex)'}), accent: Flags.string({description: 'Accent colour (hex)'}), } diff --git a/src/commands/project/set-default-language.ts b/src/commands/project/set-default-language.ts index ed35b2d..6c1ab48 100644 --- a/src/commands/project/set-default-language.ts +++ b/src/commands/project/set-default-language.ts @@ -5,12 +5,19 @@ import {ProjectCommand} from '../../lib/project.js' export default class ProjectSetDefaultLanguage extends ProjectCommand { static description = 'Set the project default language (one of its languages)' - static examples = ['<%= config.bin %> project set-default-language en'] + static examples = [ + '<%= config.bin %> project set-default-language en', + '<%= config.bin %> project set-default-language en --dry-run', + ] static args = { language: Args.string({required: true, description: 'Language code'}), } + static flags = { + ...ProjectCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(ProjectSetDefaultLanguage) const {client, projectId} = this.projectClient(flags) diff --git a/src/commands/project/set-description.ts b/src/commands/project/set-description.ts index 9f9e304..1df474a 100644 --- a/src/commands/project/set-description.ts +++ b/src/commands/project/set-description.ts @@ -8,6 +8,7 @@ export default class ProjectSetDescription extends ProjectCommand { static examples = [ '<%= config.bin %> project set-description "Orders and invoices for the web shop"', '<%= config.bin %> project set-description --clear', + '<%= config.bin %> project set-description "Orders and invoices for the web shop" --dry-run', ] static args = { @@ -15,6 +16,7 @@ export default class ProjectSetDescription extends ProjectCommand { } static flags = { + ...ProjectCommand.dryRunFlags, clear: Flags.boolean({description: 'Remove the description', default: false}), } diff --git a/src/commands/project/set-icon.ts b/src/commands/project/set-icon.ts index 2cb1e41..6dca26f 100644 --- a/src/commands/project/set-icon.ts +++ b/src/commands/project/set-icon.ts @@ -14,9 +14,11 @@ Upload the image first with \`files upload\`, then point at it here.` static examples = [ '<%= config.bin %> project set-icon --file-resource @icon-ref.json', '<%= config.bin %> project set-icon --clear', + '<%= config.bin %> project set-icon --file-resource @icon-ref.json --dry-run', ] static flags = { + ...ProjectCommand.dryRunFlags, 'file-resource': Flags.string({description: 'The stored file as a JSON object, @file or -', exclusive: ['clear']}), clear: Flags.boolean({description: 'Remove the icon', default: false}), } diff --git a/src/commands/project/set-languages.ts b/src/commands/project/set-languages.ts index f32a69c..61b1c0d 100644 --- a/src/commands/project/set-languages.ts +++ b/src/commands/project/set-languages.ts @@ -8,7 +8,10 @@ export default class ProjectSetLanguages extends ProjectCommand { Pass the full list — it replaces the one stored. The default language must stay in it (\`project set-default-language\`).` - static examples = ['<%= config.bin %> project set-languages en lt de'] + static examples = [ + '<%= config.bin %> project set-languages en lt de', + '<%= config.bin %> project set-languages en lt de --dry-run', + ] static strict = false @@ -16,6 +19,10 @@ in it (\`project set-default-language\`).` language: Args.string({required: true, description: 'Language codes (one or more)'}), } + static flags = { + ...ProjectCommand.dryRunFlags, + } + async run(): Promise { const {argv, flags} = await this.parse(ProjectSetLanguages) const languages = argv as string[] diff --git a/src/commands/project/set-logo.ts b/src/commands/project/set-logo.ts index 748ea85..b823e12 100644 --- a/src/commands/project/set-logo.ts +++ b/src/commands/project/set-logo.ts @@ -14,9 +14,11 @@ Upload the image first with \`files upload\`, then point at it here.` static examples = [ '<%= config.bin %> project set-logo --file-resource @logo-ref.json', '<%= config.bin %> project set-logo --clear', + '<%= config.bin %> project set-logo --file-resource @logo-ref.json --dry-run', ] static flags = { + ...ProjectCommand.dryRunFlags, 'file-resource': Flags.string({description: 'The stored file as a JSON object, @file or -', exclusive: ['clear']}), clear: Flags.boolean({description: 'Remove the logo', default: false}), } diff --git a/src/commands/project/set-name.ts b/src/commands/project/set-name.ts index 06f215b..5b2263f 100644 --- a/src/commands/project/set-name.ts +++ b/src/commands/project/set-name.ts @@ -5,12 +5,19 @@ import {ProjectCommand} from '../../lib/project.js' export default class ProjectSetName extends ProjectCommand { static description = 'Rename the project' - static examples = ['<%= config.bin %> project set-name "Shop backend"'] + static examples = [ + '<%= config.bin %> project set-name "Shop backend"', + '<%= config.bin %> project set-name "Shop backend" --dry-run', + ] static args = { name: Args.string({required: true, description: 'New project name'}), } + static flags = { + ...ProjectCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(ProjectSetName) const {client, projectId} = this.projectClient(flags) diff --git a/src/commands/project/set-regions.ts b/src/commands/project/set-regions.ts index 29e79d1..6737602 100644 --- a/src/commands/project/set-regions.ts +++ b/src/commands/project/set-regions.ts @@ -15,9 +15,11 @@ with \`account regions\`.` '<%= config.bin %> project set-regions --additional nb-us-east --additional nb-ap-singapore', '<%= config.bin %> project set-regions --primary nb-eu-germany', '<%= config.bin %> project set-regions --clear-additional', + '<%= config.bin %> project set-regions --additional nb-us-east --additional nb-ap-singapore --dry-run', ] static flags = { + ...ProjectCommand.dryRunFlags, primary: Flags.string({description: 'Primary region code (only when it is not set yet)'}), additional: Flags.string({description: 'Additional region code (repeat for several)', multiple: true, exclusive: ['clear-additional']}), 'clear-additional': Flags.boolean({description: 'Remove every additional region', default: false}), @@ -29,12 +31,12 @@ with \`account regions\`.` this.error('Pass --primary, --additional or --clear-additional.') } - const {client, projectId} = this.projectClient(flags) + const {client, reader, projectId} = this.projectClient(flags) let additionalRegions = flags['clear-additional'] ? [] : flags.additional if (!additionalRegions) { // The list is a full replacement: keep the current one. - const current = projectOf(await client.hub.account.getProject({projectId})) + const current = projectOf(await reader.hub.account.getProject({projectId})) additionalRegions = ((current.additionalRegions as Array<{id: string}> | undefined) ?? []).map((r) => r.id) } diff --git a/src/commands/project/set-url.ts b/src/commands/project/set-url.ts index dee0b21..1f49fa6 100644 --- a/src/commands/project/set-url.ts +++ b/src/commands/project/set-url.ts @@ -11,6 +11,7 @@ in e-mails). The admin portal's own address is \`project admin-portal set-url\`. static examples = [ '<%= config.bin %> project set-url https://shop.example.com', '<%= config.bin %> project set-url --clear', + '<%= config.bin %> project set-url https://shop.example.com --dry-run', ] static args = { @@ -18,6 +19,7 @@ in e-mails). The admin portal's own address is \`project admin-portal set-url\`. } static flags = { + ...ProjectCommand.dryRunFlags, clear: Flags.boolean({description: 'Remove the address', default: false}), } diff --git a/src/commands/project/tokens.ts b/src/commands/project/tokens.ts index 8ec60ed..d71ca09 100644 --- a/src/commands/project/tokens.ts +++ b/src/commands/project/tokens.ts @@ -10,7 +10,7 @@ ids to see the tokens filled in for those users.` static examples = [ '<%= config.bin %> project tokens', - '<%= config.bin %> project tokens --recipient 66b2f0a1...', + '<%= config.bin %> project tokens --recipient 66b2f0a1c3d4e5f6a7b8c9d0', ] static flags = { diff --git a/src/commands/sms/campaign/batch.ts b/src/commands/sms/campaign/batch.ts index 1d581f6..1dc8ad3 100644 --- a/src/commands/sms/campaign/batch.ts +++ b/src/commands/sms/campaign/batch.ts @@ -8,8 +8,8 @@ export default class SmsCampaignBatch extends BaseCommand { Give a notification ID as the third argument to see just that notification.` static examples = [ - '<%= config.bin %> sms campaign batch 66b2f0a1... b7c3...', - '<%= config.bin %> sms campaign batch 66b2f0a1... b7c3... n9d4...', + '<%= config.bin %> sms campaign batch 66b2f0a1c3d4e5f6a7b8c9d0 b7c3d2e1f0a9b8c7d6e5f4a3', + '<%= config.bin %> sms campaign batch 66b2f0a1c3d4e5f6a7b8c9d0 b7c3d2e1f0a9b8c7d6e5f4a3 9d4e5f6a7b8c9d0e1f2a3b4c', ] static args = { diff --git a/src/commands/sms/campaign/batches.ts b/src/commands/sms/campaign/batches.ts index 7d1b3dd..33aa5bf 100644 --- a/src/commands/sms/campaign/batches.ts +++ b/src/commands/sms/campaign/batches.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../../base.js' export default class SmsCampaignBatches extends BaseCommand { static description = 'List the send batches of an SMS campaign' - static examples = ['<%= config.bin %> sms campaign batches 66b2f0a1...'] + static examples = ['<%= config.bin %> sms campaign batches 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Campaign ID'}), diff --git a/src/commands/sms/campaign/create.ts b/src/commands/sms/campaign/create.ts index bd4c9d9..b92d628 100644 --- a/src/commands/sms/campaign/create.ts +++ b/src/commands/sms/campaign/create.ts @@ -16,12 +16,14 @@ Without --at the campaign is sent right away. Anything the flags do not cover goes in --config as a JSON object and is merged into the delivery settings.` static examples = [ - '<%= config.bin %> sms campaign create --template 66b2f0a1... --audience all-users --tag beta', - '<%= config.bin %> sms campaign create --template 66b2f0a1... --audience users --user usr_1 --user usr_2', - '<%= config.bin %> sms campaign create --template 66b2f0a1... --audience phone-numbers --phone +37060000000 --at 2026-10-01T09:00:00Z', + '<%= config.bin %> sms campaign create --template 66b2f0a1c3d4e5f6a7b8c9d0 --audience all-users --tag beta', + '<%= config.bin %> sms campaign create --template 66b2f0a1c3d4e5f6a7b8c9d0 --audience users --user usr_1 --user usr_2', + '<%= config.bin %> sms campaign create --template 66b2f0a1c3d4e5f6a7b8c9d0 --audience phone-numbers --phone +37060000000 --at 2026-10-01T09:00:00Z', + '<%= config.bin %> sms campaign create --template 66b2f0a1c3d4e5f6a7b8c9d0 --audience all-users --tag beta --dry-run', ] static flags = { + ...BaseCommand.dryRunFlags, template: Flags.string({required: true, description: 'Template ID'}), audience: Flags.string({required: true, description: 'Who receives it', options: Object.keys(SMS_AUDIENCES)}), user: Flags.string({description: 'User ID (users; repeat for several)', multiple: true}), diff --git a/src/commands/sms/campaign/delete.ts b/src/commands/sms/campaign/delete.ts index f3fb30c..b8e1505 100644 --- a/src/commands/sms/campaign/delete.ts +++ b/src/commands/sms/campaign/delete.ts @@ -1,29 +1,28 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../../base.js' export default class SmsCampaignDelete extends BaseCommand { static description = 'Delete an SMS campaign' - static examples = ['<%= config.bin %> sms campaign delete 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> sms campaign delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> sms campaign delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Campaign ID'}), } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(SmsCampaignDelete) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Delete SMS campaign ${args.id}?`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Delete SMS campaign ${args.id}?`, flags) const res = await client.hub.notifications.deleteSmsCampaign({id: args.id}) diff --git a/src/commands/sms/campaign/message.ts b/src/commands/sms/campaign/message.ts index ff7a197..a2e5359 100644 --- a/src/commands/sms/campaign/message.ts +++ b/src/commands/sms/campaign/message.ts @@ -7,7 +7,7 @@ export default class SmsCampaignMessage extends BaseCommand { Same as \`sms campaign batch \`.` - static examples = ['<%= config.bin %> sms campaign message 66b2f0a1... n9d4... --batch b7c3...'] + static examples = ['<%= config.bin %> sms campaign message 66b2f0a1c3d4e5f6a7b8c9d0 9d4e5f6a7b8c9d0e1f2a3b4c --batch b7c3d2e1f0a9b8c7d6e5f4a3'] static args = { id: Args.string({required: true, description: 'Campaign ID'}), diff --git a/src/commands/sms/campaign/messages.ts b/src/commands/sms/campaign/messages.ts index c23394e..725eacd 100644 --- a/src/commands/sms/campaign/messages.ts +++ b/src/commands/sms/campaign/messages.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../../base.js' export default class SmsCampaignMessages extends BaseCommand { static description = 'List the SMS messages a campaign sent' - static examples = ['<%= config.bin %> sms campaign messages 66b2f0a1... --batch b7c3...'] + static examples = ['<%= config.bin %> sms campaign messages 66b2f0a1c3d4e5f6a7b8c9d0 --batch b7c3d2e1f0a9b8c7d6e5f4a3'] static args = { id: Args.string({required: true, description: 'Campaign ID'}), diff --git a/src/commands/sms/disable.ts b/src/commands/sms/disable.ts index f93354e..7b4f186 100644 --- a/src/commands/sms/disable.ts +++ b/src/commands/sms/disable.ts @@ -1,6 +1,3 @@ -import {confirm} from '@inquirer/prompts' -import {Flags} from '@oclif/core' - import {BaseCommand} from '../../base.js' export default class SmsDisable extends BaseCommand { @@ -8,20 +5,20 @@ export default class SmsDisable extends BaseCommand { Run \`sms disable-dependencies\` first to see what still depends on SMS.` - static examples = ['<%= config.bin %> sms disable --yes'] + static examples = [ + '<%= config.bin %> sms disable --yes', + '<%= config.bin %> sms disable --dry-run', + ] static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {flags} = await this.parse(SmsDisable) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: 'Turn off SMS for this project?', default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail('Turn off SMS for this project?', flags) const res = await client.hub.notifications.disableSms({}) diff --git a/src/commands/sms/enable.ts b/src/commands/sms/enable.ts index 1cd47ad..0eb09ae 100644 --- a/src/commands/sms/enable.ts +++ b/src/commands/sms/enable.ts @@ -3,7 +3,14 @@ import {BaseCommand} from '../../base.js' export default class SmsEnable extends BaseCommand { static description = 'Turn on the SMS module for the project' - static examples = ['<%= config.bin %> sms enable'] + static examples = [ + '<%= config.bin %> sms enable', + '<%= config.bin %> sms enable --dry-run', + ] + + static flags = { + ...BaseCommand.dryRunFlags, + } async run(): Promise { const {flags} = await this.parse(SmsEnable) diff --git a/src/commands/sms/integration.ts b/src/commands/sms/integration.ts index eca8d35..3cb1251 100644 --- a/src/commands/sms/integration.ts +++ b/src/commands/sms/integration.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../base.js' export default class SmsIntegration extends BaseCommand { static description = 'Show one SMS integration' - static examples = ['<%= config.bin %> sms integration 66b2f0a1...'] + static examples = ['<%= config.bin %> sms integration 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Integration ID'}), diff --git a/src/commands/sms/integration/confirm-delivery.ts b/src/commands/sms/integration/confirm-delivery.ts index 12e51d5..59d4abf 100644 --- a/src/commands/sms/integration/confirm-delivery.ts +++ b/src/commands/sms/integration/confirm-delivery.ts @@ -8,12 +8,19 @@ export default class SmsIntegrationConfirmDelivery extends BaseCommand { After \`sms integration test\`, run this once you have seen the SMS on the phone. It marks the integration as proven to deliver.` - static examples = ['<%= config.bin %> sms integration confirm-delivery 66b2f0a1...'] + static examples = [ + '<%= config.bin %> sms integration confirm-delivery 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> sms integration confirm-delivery 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(SmsIntegrationConfirmDelivery) const client = this.client(flags) diff --git a/src/commands/sms/integration/default.ts b/src/commands/sms/integration/default.ts index b1fa88d..9092151 100644 --- a/src/commands/sms/integration/default.ts +++ b/src/commands/sms/integration/default.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../../base.js' export default class SmsIntegrationDefault extends BaseCommand { static description = 'Make an SMS integration the default one for this project' - static examples = ['<%= config.bin %> sms integration default 66b2f0a1...'] + static examples = [ + '<%= config.bin %> sms integration default 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> sms integration default 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(SmsIntegrationDefault) const client = this.client(flags) diff --git a/src/commands/sms/integration/delete.ts b/src/commands/sms/integration/delete.ts index 843a052..2343389 100644 --- a/src/commands/sms/integration/delete.ts +++ b/src/commands/sms/integration/delete.ts @@ -1,29 +1,28 @@ -import {confirm} from '@inquirer/prompts' -import {Args, Flags} from '@oclif/core' +import {Args} from '@oclif/core' import {BaseCommand} from '../../../base.js' export default class SmsIntegrationDelete extends BaseCommand { static description = 'Delete an SMS integration' - static examples = ['<%= config.bin %> sms integration delete 66b2f0a1... --yes'] + static examples = [ + '<%= config.bin %> sms integration delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> sms integration delete 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } static flags = { - yes: Flags.boolean({char: 'y', description: 'Skip the confirmation prompt', default: false}), + ...BaseCommand.mutatingFlags, } async run(): Promise { const {args, flags} = await this.parse(SmsIntegrationDelete) const client = this.client(flags) - if (!flags.yes && process.stdout.isTTY) { - const ok = await confirm({message: `Delete SMS integration ${args.id}?`, default: false}) - if (!ok) return this.print('Cancelled.') - } + await this.confirmOrFail(`Delete SMS integration ${args.id}?`, flags) const res = await client.hub.notifications.deleteSmsIntegration({id: args.id}) diff --git a/src/commands/sms/integration/disable.ts b/src/commands/sms/integration/disable.ts index e0a30b5..5881d3d 100644 --- a/src/commands/sms/integration/disable.ts +++ b/src/commands/sms/integration/disable.ts @@ -5,15 +5,23 @@ import {BaseCommand} from '../../../base.js' export default class SmsIntegrationDisable extends BaseCommand { static description = 'Turn an SMS integration off' - static examples = ['<%= config.bin %> sms integration disable 66b2f0a1...'] + static examples = [ + '<%= config.bin %> sms integration disable 66b2f0a1c3d4e5f6a7b8c9d0 --yes', + '<%= config.bin %> sms integration disable 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } + static flags = { + ...BaseCommand.mutatingFlags, + } + async run(): Promise { const {args, flags} = await this.parse(SmsIntegrationDisable) const client = this.client(flags) + await this.confirmOrFail(`Turn off SMS integration ${args.id}?`, flags) // The route token is `{Id}`; the SDK (4.2.0) fills it from `id` — the // lookup is case-insensitive — so the generated field is enough. diff --git a/src/commands/sms/integration/enable.ts b/src/commands/sms/integration/enable.ts index afbbb0a..e76fd7a 100644 --- a/src/commands/sms/integration/enable.ts +++ b/src/commands/sms/integration/enable.ts @@ -5,12 +5,19 @@ import {BaseCommand} from '../../../base.js' export default class SmsIntegrationEnable extends BaseCommand { static description = 'Turn an SMS integration on' - static examples = ['<%= config.bin %> sms integration enable 66b2f0a1...'] + static examples = [ + '<%= config.bin %> sms integration enable 66b2f0a1c3d4e5f6a7b8c9d0', + '<%= config.bin %> sms integration enable 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run', + ] static args = { id: Args.string({required: true, description: 'Integration ID'}), } + static flags = { + ...BaseCommand.dryRunFlags, + } + async run(): Promise { const {args, flags} = await this.parse(SmsIntegrationEnable) const client = this.client(flags) diff --git a/src/commands/sms/integration/save.ts b/src/commands/sms/integration/save.ts index bb52211..ccb4f1c 100644 --- a/src/commands/sms/integration/save.ts +++ b/src/commands/sms/integration/save.ts @@ -18,9 +18,11 @@ credentials — put the provider-specific fields in --config (inline JSON, static examples = [ '<%= config.bin %> sms integration save --provider Fake', '<%= config.bin %> sms integration save --provider Twilio --name Twilio --config @twilio.json', + '<%= config.bin %> sms integration save --provider Fake --dry-run', ] static flags = { + ...BaseCommand.dryRunFlags, provider: Flags.string({required: true, description: 'SMS provider', options: PROVIDERS}), name: Flags.string({description: 'Integration name (Fake ignores it and uses its own)'}), id: Flags.string({description: 'Integration ID — set it to update instead of create'}), diff --git a/src/commands/sms/integration/test.ts b/src/commands/sms/integration/test.ts index 483e571..82e33b4 100644 --- a/src/commands/sms/integration/test.ts +++ b/src/commands/sms/integration/test.ts @@ -8,9 +8,13 @@ export default class SmsIntegrationTest extends BaseCommand { Point this at the Fake integration while you are developing — it accepts the send and contacts no SMS service, so nothing reaches a real phone.` - static examples = ['<%= config.bin %> sms integration test --integration 66b2f0a1... --to +37060000000'] + static examples = [ + '<%= config.bin %> sms integration test --integration 66b2f0a1c3d4e5f6a7b8c9d0 --to +37060000000', + '<%= config.bin %> sms integration test --integration 66b2f0a1c3d4e5f6a7b8c9d0 --to +37060000000 --dry-run', + ] static flags = { + ...BaseCommand.dryRunFlags, integration: Flags.string({required: true, description: 'Integration ID to send through'}), to: Flags.string({description: 'Phone number (international format) to send the test to'}), } diff --git a/src/commands/sms/preview.ts b/src/commands/sms/preview.ts index 6537202..068944c 100644 --- a/src/commands/sms/preview.ts +++ b/src/commands/sms/preview.ts @@ -13,8 +13,8 @@ The signed link is the key: no login and no project are needed. When you are logged in, your session is still sent.` static examples = [ - '<%= config.bin %> sms preview 8f2a91c4...', - '<%= config.bin %> sms preview --hash 8f2a91c4... --region nb-eu-germany', + '<%= config.bin %> sms preview 8f2a91c4d7e6b5a4c3d2e1f0', + '<%= config.bin %> sms preview --hash 8f2a91c4d7e6b5a4c3d2e1f0 --region nb-eu-germany', ] static args = { diff --git a/src/commands/sms/template/create.ts b/src/commands/sms/template/create.ts index 3674155..b395f36 100644 --- a/src/commands/sms/template/create.ts +++ b/src/commands/sms/template/create.ts @@ -10,9 +10,13 @@ languages: --translations with a JSON array of {language, content: {subject, bod static examples = [ '<%= config.bin %> sms template create --name Welcome --body "Hi @Model.Name, your code is @Model.Code"', '<%= config.bin %> sms template create --name Welcome --translations @welcome.json', + '<%= config.bin %> sms template create --name Welcome --body "Hi @Model.Name, your code is @Model.Code" --dry-run', ] - static flags = smsTemplateFlags + static flags = { + ...BaseCommand.dryRunFlags, + ...smsTemplateFlags, + } async run(): Promise { const {flags} = await this.parse(SmsTemplateCreate) diff --git a/src/commands/sms/template/tokens.ts b/src/commands/sms/template/tokens.ts index ffe52cb..03a74e9 100644 --- a/src/commands/sms/template/tokens.ts +++ b/src/commands/sms/template/tokens.ts @@ -5,7 +5,7 @@ import {BaseCommand} from '../../../base.js' export default class SmsTemplateTokens extends BaseCommand { static description = 'List the tokens an SMS template uses — the values a campaign must fill' - static examples = ['<%= config.bin %> sms template tokens 66b2f0a1...'] + static examples = ['<%= config.bin %> sms template tokens 66b2f0a1c3d4e5f6a7b8c9d0'] static args = { id: Args.string({required: true, description: 'Template ID'}), diff --git a/src/commands/sms/template/update.ts b/src/commands/sms/template/update.ts index 58259e6..a8993e7 100644 --- a/src/commands/sms/template/update.ts +++ b/src/commands/sms/template/update.ts @@ -9,14 +9,18 @@ export default class SmsTemplateUpdate extends BaseCommand { The whole template is sent, so pass every language you want to keep.` static examples = [ - '<%= config.bin %> sms template update 66b2f0a1... --name Welcome --body "Hi @Model.Name"', + '<%= config.bin %> sms template update 66b2f0a1c3d4e5f6a7b8c9d0 --name Welcome --body "Hi @Model.Name"', + '<%= config.bin %> sms template update 66b2f0a1c3d4e5f6a7b8c9d0 --name Welcome --body "Hi @Model.Name" --dry-run', ] static args = { id: Args.string({required: true, description: 'Template ID'}), } - static flags = smsTemplateFlags + static flags = { + ...BaseCommand.dryRunFlags, + ...smsTemplateFlags, + } async run(): Promise { const {args, flags} = await this.parse(SmsTemplateUpdate) diff --git a/src/lib/project.ts b/src/lib/project.ts index 4d8ba16..9456242 100644 --- a/src/lib/project.ts +++ b/src/lib/project.ts @@ -1,6 +1,5 @@ import {readFileSync} from 'node:fs' -import {confirm} from '@inquirer/prompts' import type {Norbix} from '@norbix.ai/ts' import {BaseCommand, type GlobalFlags} from '../base.js' @@ -14,27 +13,20 @@ import {BaseCommand, type GlobalFlags} from '../base.js' * context (`--project`, NORBIX_PROJECT_ID, profile or login session). */ export abstract class ProjectCommand extends BaseCommand { - protected projectClient(flags: GlobalFlags, projectId?: string): {client: Norbix; projectId: string} { + /** + * `reader` is for the read that comes before a write (current origins, + * current settings). A dry run stops every request, so the read goes out + * live — it changes nothing — and only the write is stopped and printed. + */ + protected projectClient( + flags: GlobalFlags, + projectId?: string, + ): {client: Norbix; reader: Norbix; projectId: string} { const client = this.client(flags) + const reader = flags['dry-run'] ? this.client({...flags, 'dry-run': false}) : client const id = projectId ?? this.resolveContext(flags).projectId if (!id) this.error('No project ID configured.\nRun `norbix configure` (or `norbix login`), or pass --project.') - return {client, projectId: id} - } - - /** - * Ask before a change that is hard to undo. `--yes` skips the question. With - * no terminal to ask in (a script, a pipe) the command refuses instead of - * going ahead — a script must say `--yes` on purpose. - */ - protected async confirmOrStop(yes: boolean, message: string): Promise { - if (yes) return true - if (!process.stdin.isTTY || !process.stdout.isTTY) { - this.error(`${message}\nNo terminal to confirm in — pass --yes to go ahead.`) - } - - const ok = await confirm({message, default: false}) - if (!ok) this.print('Cancelled.') - return ok + return {client, reader, projectId: id} } } diff --git a/tests/__snapshots__/schema.test.ts.snap b/tests/__snapshots__/schema.test.ts.snap index 1399e1d..e1ce592 100644 --- a/tests/__snapshots__/schema.test.ts.snap +++ b/tests/__snapshots__/schema.test.ts.snap @@ -54,6 +54,239 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "id": "account usage", "supportsDryRun": false, }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "ai llm", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "ai llm default", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "ai llm delete", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "ai llm disable", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "ai llm enable", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "provider:option", + "name:string", + "id:string", + "endpoint:string", + "model:string", + "default:boolean", + "disabled:boolean", + "config:string", + ], + "id": "ai llm save", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "ai llm test", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "page-size:string", + "after:string", + ], + "id": "ai llms", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "ai mcp", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "ai mcp delete", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "ai mcp disable", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "ai mcp enable", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "provider:option", + "name:string", + "id:string", + "server-name:string", + "category:string", + "description:string", + "icon:string", + "disabled:boolean", + "config:string", + ], + "id": "ai mcp save", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "ai mcp test", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "page-size:string", + "after:string", + ], + "id": "ai mcps", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "env:string", + "dry-run:boolean", + "name:string", + "reach:option", + "rights:option", + ], + "id": "ai service-user create", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "ai service-user delete", + "supportsDryRun": true, + }, + { + "args": [ + "id", + "keyId", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "ai service-user revoke-key", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + "revoke:string", + ], + "id": "ai service-user rotate-key", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "ai service-users", + "supportsDryRun": false, + }, { "args": [], "destructive": true, @@ -329,13 +562,18 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "supportsDryRun": false, }, { - "args": [], + "args": [ + "id", + "batchId", + "[notificationId]", + ], "destructive": false, "flags": [ + "database-integration:string", "page-size:string", "after:string", ], - "id": "email campaigns", + "id": "email campaign batch", "supportsDryRun": false, }, { @@ -344,21 +582,42 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` ], "destructive": false, "flags": [ - "dry-run:boolean", + "batch:string", + "email:string", + "database-integration:string", + "page-size:string", + "after:string", ], - "id": "email clone", - "supportsDryRun": true, + "id": "email campaign batches", + "supportsDryRun": false, }, { - "args": [ - "id", - ], - "destructive": true, + "args": [], + "destructive": false, "flags": [ - "yes:boolean", "dry-run:boolean", + "template:string", + "audience:option", + "user:string", + "email:string", + "cc:string", + "bcc:string", + "one-each:boolean", + "role:string", + "tag:string", + "schema:string", + "field:string", + "field-type:option", + "integration:string", + "validation-integration:string", + "language:string", + "notes:string", + "at:string", + "token:string", + "database-integration:string", + "config:string", ], - "id": "email delete", + "id": "email campaign create", "supportsDryRun": true, }, { @@ -370,7 +629,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "yes:boolean", "dry-run:boolean", ], - "id": "email stop", + "id": "email campaign delete", "supportsDryRun": true, }, { @@ -378,17 +637,23 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "id", ], "destructive": false, - "flags": [], - "id": "email template", + "flags": [ + "batch:string", + "database-integration:string", + "page-size:string", + "after:string", + ], + "id": "email campaign messages", "supportsDryRun": false, }, { "args": [], "destructive": false, "flags": [ - "archived:boolean", + "page-size:string", + "after:string", ], - "id": "email templates", + "id": "email campaigns", "supportsDryRun": false, }, { @@ -399,73 +664,91 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "flags": [ "dry-run:boolean", ], - "id": "email unarchive", + "id": "email clone", "supportsDryRun": true, }, { "args": [ - "name", + "id", ], "destructive": true, "flags": [ "yes:boolean", "dry-run:boolean", ], - "id": "env delete", + "id": "email delete", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "email disable", "supportsDryRun": true, }, { "args": [], "destructive": false, "flags": [], - "id": "env list", + "id": "email disable-dependencies", "supportsDryRun": false, }, { - "args": [ - "name", - ], + "args": [], "destructive": false, "flags": [ "dry-run:boolean", ], - "id": "env use", + "id": "email enable", "supportsDryRun": true, }, { "args": [ - "remote", + "id", + ], + "destructive": false, + "flags": [], + "id": "email footer", + "supportsDryRun": false, + }, + { + "args": [ + "id", ], "destructive": true, "flags": [ - "integration:string", "yes:boolean", "dry-run:boolean", ], - "id": "files delete", + "id": "email footer delete", "supportsDryRun": true, }, { - "args": [ - "remote", - "[file]", - ], + "args": [], "destructive": false, "flags": [ - "integration:string", + "dry-run:boolean", + "name:string", + "id:string", + "language:string", + "content:string", + "content-file:string", + "translations:string", ], - "id": "files download", - "supportsDryRun": false, + "id": "email footer save", + "supportsDryRun": true, }, { - "args": [ - "remote", - ], + "args": [], "destructive": false, "flags": [ - "integration:string", + "page-size:string", + "after:string", ], - "id": "files info", + "id": "email footers", "supportsDryRun": false, }, { @@ -474,157 +757,179 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` ], "destructive": false, "flags": [], - "id": "files integrations test", - "supportsDryRun": false, - }, - { - "args": [ - "[path]", - ], - "destructive": false, - "flags": [ - "integration:string", - ], - "id": "files list", + "id": "email integration", "supportsDryRun": false, }, { "args": [ - "remote", + "id", ], "destructive": false, "flags": [ "dry-run:boolean", - "folder:boolean", - "integration:string", ], - "id": "files publish", + "id": "email integration confirm-delivery", "supportsDryRun": true, }, { "args": [ - "remote", - ], - "destructive": false, - "flags": [ - "integration:string", - "expires:string", - ], - "id": "files sign", - "supportsDryRun": false, - }, - { - "args": [ - "remote", + "id", ], "destructive": false, "flags": [ "dry-run:boolean", - "folder:boolean", - "integration:string", ], - "id": "files unpublish", + "id": "email integration default", "supportsDryRun": true, }, { "args": [ - "file", - "[remote]", + "id", ], - "destructive": false, + "destructive": true, "flags": [ + "yes:boolean", "dry-run:boolean", - "integration:string", - "content-type:string", ], - "id": "files upload", + "id": "email integration delete", "supportsDryRun": true, }, { - "args": [], + "args": [ + "id", + ], "destructive": true, "flags": [ "yes:boolean", "dry-run:boolean", - "body:string", ], - "id": "hub", + "id": "email integration disable", "supportsDryRun": true, }, { "args": [ - "module", + "id", ], "destructive": false, "flags": [], - "id": "integrations", + "id": "email integration domain-health", "supportsDryRun": false, }, { - "args": [], + "args": [ + "id", + ], "destructive": false, "flags": [ - "user:string", - "password:string", + "dry-run:boolean", ], - "id": "login", - "supportsDryRun": false, + "id": "email integration enable", + "supportsDryRun": true, }, { "args": [], "destructive": false, "flags": [ "dry-run:boolean", + "provider:option", + "name:string", + "id:string", + "from:string", + "sender-name:string", + "disabled:boolean", + "config:string", ], - "id": "logout", + "id": "email integration save", "supportsDryRun": true, }, { "args": [], "destructive": false, "flags": [ - "level:string", - "module:string", - "search:string", - "event-code:string", - "correlation:string", - "from:string", + "dry-run:boolean", + "integration:string", "to:string", + ], + "id": "email integration test", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ "page-size:string", "after:string", ], - "id": "logs list", + "id": "email integrations", + "supportsDryRun": false, + }, + { + "args": [ + "[hash]", + ], + "destructive": false, + "flags": [ + "hash:string", + "notification:string", + ], + "id": "email preview", "supportsDryRun": false, }, { "args": [], "destructive": false, - "flags": [], - "id": "logs settings", + "flags": [ + "id:string", + ], + "id": "email settings", "supportsDryRun": false, }, { "args": [ - "correlationId", + "id", ], "destructive": false, "flags": [], - "id": "logs trail", + "id": "email signature", "supportsDryRun": false, }, { "args": [ - "action", - "name", + "id", ], "destructive": true, "flags": [ "yes:boolean", "dry-run:boolean", ], - "id": "module", + "id": "email signature delete", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "name:string", + "id:string", + "language:string", + "content:string", + "content-file:string", + "translations:string", + ], + "id": "email signature save", "supportsDryRun": true, }, + { + "args": [], + "destructive": false, + "flags": [ + "page-size:string", + "after:string", + ], + "id": "email signatures", + "supportsDryRun": false, + }, { "args": [ "id", @@ -634,9 +939,41 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "yes:boolean", "dry-run:boolean", ], - "id": "payments disable", + "id": "email stop", "supportsDryRun": true, }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "email system-template", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "tag:string", + "theme:string", + "channel:option", + "trigger:option", + "page-size:string", + "after:string", + ], + "id": "email system-templates", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "email template", + "supportsDryRun": false, + }, { "args": [ "id", @@ -644,15 +981,41 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "destructive": false, "flags": [ "dry-run:boolean", + "file-ref:string", + "language:string", ], - "id": "payments enable", + "id": "email template attach", "supportsDryRun": true, }, { "args": [], "destructive": false, - "flags": [], - "id": "payments integrations", + "flags": [ + "dry-run:boolean", + "name:string", + "description:string", + "channel:option", + "tag:string", + "language:string", + "subject:string", + "body:string", + "body-file:string", + "engine:option", + "translations:string", + ], + "id": "email template create", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "code:string", + "file:string", + "token:string", + "preview:boolean", + ], + "id": "email template render", "supportsDryRun": false, }, { @@ -661,21 +1024,1223 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` ], "destructive": false, "flags": [], - "id": "payments trigger", + "id": "email template tokens", "supportsDryRun": false, }, { - "args": [], + "args": [ + "id", + ], "destructive": false, - "flags": [], - "id": "payments triggers", - "supportsDryRun": false, + "flags": [ + "dry-run:boolean", + "name:string", + "description:string", + "channel:option", + "tag:string", + "language:string", + "subject:string", + "body:string", + "body-file:string", + "engine:option", + "translations:string", + ], + "id": "email template update", + "supportsDryRun": true, }, { "args": [], "destructive": false, + "flags": [ + "archived:boolean", + ], + "id": "email templates", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "email unarchive", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "provider:option", + "name:string", + "id:string", + "disabled:boolean", + "config:string", + ], + "id": "email validation save", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "email validation test", + "supportsDryRun": true, + }, + { + "args": [ + "name", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "env delete", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "env list", + "supportsDryRun": false, + }, + { + "args": [ + "name", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "env use", + "supportsDryRun": true, + }, + { + "args": [ + "remote", + ], + "destructive": true, + "flags": [ + "integration:string", + "yes:boolean", + "dry-run:boolean", + ], + "id": "files delete", + "supportsDryRun": true, + }, + { + "args": [ + "remote", + "[file]", + ], + "destructive": false, + "flags": [ + "integration:string", + ], + "id": "files download", + "supportsDryRun": false, + }, + { + "args": [ + "remote", + ], + "destructive": false, + "flags": [ + "integration:string", + ], + "id": "files info", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "files integrations test", + "supportsDryRun": false, + }, + { + "args": [ + "[path]", + ], + "destructive": false, + "flags": [ + "integration:string", + ], + "id": "files list", + "supportsDryRun": false, + }, + { + "args": [ + "remote", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + "folder:boolean", + "integration:string", + ], + "id": "files publish", + "supportsDryRun": true, + }, + { + "args": [ + "remote", + ], + "destructive": false, + "flags": [ + "integration:string", + "expires:string", + ], + "id": "files sign", + "supportsDryRun": false, + }, + { + "args": [ + "remote", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + "folder:boolean", + "integration:string", + ], + "id": "files unpublish", + "supportsDryRun": true, + }, + { + "args": [ + "file", + "[remote]", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + "integration:string", + "content-type:string", + ], + "id": "files upload", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + "body:string", + ], + "id": "hub", + "supportsDryRun": true, + }, + { + "args": [ + "module", + ], + "destructive": false, + "flags": [], + "id": "integrations", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "user:string", + "password:string", + ], + "id": "login", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "logout", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "level:string", + "module:string", + "search:string", + "event-code:string", + "correlation:string", + "from:string", + "to:string", + "page-size:string", + "after:string", + ], + "id": "logs list", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "logs settings", + "supportsDryRun": false, + }, + { + "args": [ + "correlationId", + ], + "destructive": false, + "flags": [], + "id": "logs trail", + "supportsDryRun": false, + }, + { + "args": [ + "action", + "name", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "module", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "payments disable", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "payments enable", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "payments integrations", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "payments trigger", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "payments triggers", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "profiles", + "supportsDryRun": false, + }, + { + "args": [ + "[id]", + ], + "destructive": false, + "flags": [], + "id": "project", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "project admin-portal disable", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "project admin-portal enable", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "project admin-portal set-service-user", + "supportsDryRun": true, + }, + { + "args": [ + "[url]", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + "clear:boolean", + ], + "id": "project admin-portal set-url", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "project admin-portal structure", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "name:string", + "welcome:string", + "system-prompt:string", + "system-prompt-file:string", + "toolset:string", + "llm:string", + "model:string", + "memory:boolean", + "rag-source:string", + "default:boolean", + ], + "id": "project ai assistant create", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "project ai assistant delete", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + "name:string", + "welcome:string", + "system-prompt:string", + "system-prompt-file:string", + "toolset:string", + "llm:string", + "model:string", + "memory:boolean", + "rag-source:string", + "default:boolean", + ], + "id": "project ai assistant update", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "project ai settings", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "enable:boolean", + "disable:boolean", + "llm:string", + "model:string", + ], + "id": "project ai settings set", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "top:string", + ], + "id": "project ai usage", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "project cors", + "supportsDryRun": false, + }, + { + "args": [ + "origin", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "project cors add", + "supportsDryRun": true, + }, + { + "args": [ + "origin", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + "remove-admin-portal-origin:boolean", + ], + "id": "project cors remove", + "supportsDryRun": true, + }, + { + "args": [ + "origin", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + "remove-admin-portal-origin:boolean", + ], + "id": "project cors set", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "project delete", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "project disable", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "project enable", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "project legal expose", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "project legal hide", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "terms-file:string", + "privacy-file:string", + "clear-terms:boolean", + "clear-privacy:boolean", + ], + "id": "project legal set", + "supportsDryRun": true, + }, + { + "args": [ + "kind", + ], + "destructive": false, + "flags": [], + "id": "project legal show", + "supportsDryRun": false, + }, + { + "args": [ + "[id]", + ], + "destructive": false, + "flags": [], + "id": "project public-config", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "main:string", + "accent:string", + ], + "id": "project set-colors", + "supportsDryRun": true, + }, + { + "args": [ + "language", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "project set-default-language", + "supportsDryRun": true, + }, + { + "args": [ + "[description]", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + "clear:boolean", + ], + "id": "project set-description", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "file-resource:string", + "clear:boolean", + ], + "id": "project set-icon", + "supportsDryRun": true, + }, + { + "args": [ + "language", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "project set-languages", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "file-resource:string", + "clear:boolean", + ], + "id": "project set-logo", + "supportsDryRun": true, + }, + { + "args": [ + "name", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "project set-name", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "primary:string", + "additional:string", + "clear-additional:boolean", + ], + "id": "project set-regions", + "supportsDryRun": true, + }, + { + "args": [ + "[url]", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + "clear:boolean", + ], + "id": "project set-url", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "initiator:string", + "recipient:string", + "target-user:string", + ], + "id": "project tokens", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "push archive", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "stats:boolean", + ], + "id": "push campaign", + "supportsDryRun": false, + }, + { + "args": [ + "id", + "batchId", + "[notificationId]", + ], + "destructive": false, + "flags": [ + "page-size:string", + "after:string", + ], + "id": "push campaign batch", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "page-size:string", + "after:string", + ], + "id": "push campaign batches", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "template:string", + "audience:option", + "user:string", + "role:string", + "tag:string", + "schema:string", + "field:string", + "field-type:option", + "device:string", + "integration:string", + "language:string", + "notes:string", + "at:string", + "token:string", + "database-integration:string", + "config:string", + ], + "id": "push campaign create", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "push campaign delete", + "supportsDryRun": true, + }, + { + "args": [ + "id", + "messageId", + ], + "destructive": false, + "flags": [ + "batch:string", + ], + "id": "push campaign message", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "batch:string", + "page-size:string", + "after:string", + ], + "id": "push campaign messages", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "page-size:string", + "after:string", + ], + "id": "push campaigns", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "push clone", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "push delete", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "push device", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "user:string", + "token:string", + "os:string", + "device-id:string", + "model:string", + "name:string", + ], + "id": "push device register", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "user:string", + "token:string", + "platform:option", + ], + "id": "push devices", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "push disable", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [], + "id": "push disable-dependencies", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "push enable", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "push integration", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "push integration confirm-delivery", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "push integration default", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "push integration delete", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "push integration disable", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "push integration enable", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "provider:option", + "name:string", + "id:string", + "disabled:boolean", + "config:string", + ], + "id": "push integration save", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "integration:string", + "token:string", + "family:option", + ], + "id": "push integration test", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "page-size:string", + "after:string", + ], + "id": "push integrations", + "supportsDryRun": false, + }, + { + "args": [ + "[hash]", + ], + "destructive": false, + "flags": [ + "hash:string", + ], + "id": "push preview", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "id:string", + ], + "id": "push settings", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "push stop", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, "flags": [], - "id": "profiles", + "id": "push template", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "name:string", + "description:string", + "channel:option", + "tag:string", + "language:string", + "title:string", + "body:string", + "translations:string", + ], + "id": "push template create", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "code:string", + "token:string", + "preview:boolean", + ], + "id": "push template render", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "push template tokens", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + "name:string", + "description:string", + "channel:option", + "tag:string", + "language:string", + "title:string", + "body:string", + "translations:string", + ], + "id": "push template update", + "supportsDryRun": true, + }, + { + "args": [], + "destructive": false, + "flags": [ + "archived:boolean", + ], + "id": "push templates", + "supportsDryRun": false, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "push unarchive", + "supportsDryRun": true, + }, + { + "args": [ + "path", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + "api:boolean", + "method:option", + "body:string", + "query:string", + ], + "id": "raw", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "scheduler delete", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": true, + "flags": [ + "yes:boolean", + "dry-run:boolean", + ], + "id": "scheduler disable", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [ + "dry-run:boolean", + ], + "id": "scheduler enable", + "supportsDryRun": true, + }, + { + "args": [ + "id", + ], + "destructive": false, + "flags": [], + "id": "scheduler get", + "supportsDryRun": false, + }, + { + "args": [], + "destructive": false, + "flags": [ + "page-size:string", + "after:string", + ], + "id": "scheduler list", + "supportsDryRun": false, + }, + { + "args": [ + "[command]", + ], + "destructive": false, + "flags": [], + "id": "schema", "supportsDryRun": false, }, { @@ -686,7 +2251,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "flags": [ "dry-run:boolean", ], - "id": "push archive", + "id": "sms archive", "supportsDryRun": true, }, { @@ -697,7 +2262,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "flags": [ "stats:boolean", ], - "id": "push campaign", + "id": "sms campaign", "supportsDryRun": false, }, { @@ -711,7 +2276,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "page-size:string", "after:string", ], - "id": "push campaign batch", + "id": "sms campaign batch", "supportsDryRun": false, }, { @@ -723,7 +2288,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "page-size:string", "after:string", ], - "id": "push campaign batches", + "id": "sms campaign batches", "supportsDryRun": false, }, { @@ -739,16 +2304,15 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "schema:string", "field:string", "field-type:option", - "device:string", - "integration:string", + "phone:string", "language:string", - "notes:string", "at:string", + "respect-time-zone:option", "token:string", "database-integration:string", "config:string", ], - "id": "push campaign create", + "id": "sms campaign create", "supportsDryRun": true, }, { @@ -760,7 +2324,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "yes:boolean", "dry-run:boolean", ], - "id": "push campaign delete", + "id": "sms campaign delete", "supportsDryRun": true, }, { @@ -772,7 +2336,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "flags": [ "batch:string", ], - "id": "push campaign message", + "id": "sms campaign message", "supportsDryRun": false, }, { @@ -785,7 +2349,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "page-size:string", "after:string", ], - "id": "push campaign messages", + "id": "sms campaign messages", "supportsDryRun": false, }, { @@ -795,7 +2359,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "page-size:string", "after:string", ], - "id": "push campaigns", + "id": "sms campaigns", "supportsDryRun": false, }, { @@ -806,7 +2370,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "flags": [ "dry-run:boolean", ], - "id": "push clone", + "id": "sms clone", "supportsDryRun": true, }, { @@ -818,44 +2382,9 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "yes:boolean", "dry-run:boolean", ], - "id": "push delete", - "supportsDryRun": true, - }, - { - "args": [ - "id", - ], - "destructive": false, - "flags": [], - "id": "push device", - "supportsDryRun": false, - }, - { - "args": [], - "destructive": false, - "flags": [ - "dry-run:boolean", - "user:string", - "token:string", - "os:string", - "device-id:string", - "model:string", - "name:string", - ], - "id": "push device register", + "id": "sms delete", "supportsDryRun": true, }, - { - "args": [], - "destructive": false, - "flags": [ - "user:string", - "token:string", - "platform:option", - ], - "id": "push devices", - "supportsDryRun": false, - }, { "args": [], "destructive": true, @@ -863,14 +2392,14 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "yes:boolean", "dry-run:boolean", ], - "id": "push disable", + "id": "sms disable", "supportsDryRun": true, }, { "args": [], "destructive": false, "flags": [], - "id": "push disable-dependencies", + "id": "sms disable-dependencies", "supportsDryRun": false, }, { @@ -879,7 +2408,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "flags": [ "dry-run:boolean", ], - "id": "push enable", + "id": "sms enable", "supportsDryRun": true, }, { @@ -888,7 +2417,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` ], "destructive": false, "flags": [], - "id": "push integration", + "id": "sms integration", "supportsDryRun": false, }, { @@ -899,7 +2428,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "flags": [ "dry-run:boolean", ], - "id": "push integration confirm-delivery", + "id": "sms integration confirm-delivery", "supportsDryRun": true, }, { @@ -910,7 +2439,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "flags": [ "dry-run:boolean", ], - "id": "push integration default", + "id": "sms integration default", "supportsDryRun": true, }, { @@ -922,7 +2451,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "yes:boolean", "dry-run:boolean", ], - "id": "push integration delete", + "id": "sms integration delete", "supportsDryRun": true, }, { @@ -934,7 +2463,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "yes:boolean", "dry-run:boolean", ], - "id": "push integration disable", + "id": "sms integration disable", "supportsDryRun": true, }, { @@ -945,7 +2474,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "flags": [ "dry-run:boolean", ], - "id": "push integration enable", + "id": "sms integration enable", "supportsDryRun": true, }, { @@ -959,7 +2488,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "disabled:boolean", "config:string", ], - "id": "push integration save", + "id": "sms integration save", "supportsDryRun": true, }, { @@ -968,10 +2497,9 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "flags": [ "dry-run:boolean", "integration:string", - "token:string", - "family:option", + "to:string", ], - "id": "push integration test", + "id": "sms integration test", "supportsDryRun": true, }, { @@ -981,7 +2509,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "page-size:string", "after:string", ], - "id": "push integrations", + "id": "sms integrations", "supportsDryRun": false, }, { @@ -992,7 +2520,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "flags": [ "hash:string", ], - "id": "push preview", + "id": "sms preview", "supportsDryRun": false, }, { @@ -1001,7 +2529,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "flags": [ "id:string", ], - "id": "push settings", + "id": "sms settings", "supportsDryRun": false, }, { @@ -1013,7 +2541,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "yes:boolean", "dry-run:boolean", ], - "id": "push stop", + "id": "sms stop", "supportsDryRun": true, }, { @@ -1022,7 +2550,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` ], "destructive": false, "flags": [], - "id": "push template", + "id": "sms template", "supportsDryRun": false, }, { @@ -1035,11 +2563,11 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "channel:option", "tag:string", "language:string", - "title:string", + "subject:string", "body:string", "translations:string", ], - "id": "push template create", + "id": "sms template create", "supportsDryRun": true, }, { @@ -1050,7 +2578,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "token:string", "preview:boolean", ], - "id": "push template render", + "id": "sms template render", "supportsDryRun": false, }, { @@ -1059,7 +2587,7 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` ], "destructive": false, "flags": [], - "id": "push template tokens", + "id": "sms template tokens", "supportsDryRun": false, }, { @@ -1074,187 +2602,13 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "channel:option", "tag:string", "language:string", - "title:string", + "subject:string", "body:string", "translations:string", ], - "id": "push template update", - "supportsDryRun": true, - }, - { - "args": [], - "destructive": false, - "flags": [ - "archived:boolean", - ], - "id": "push templates", - "supportsDryRun": false, - }, - { - "args": [ - "id", - ], - "destructive": false, - "flags": [ - "dry-run:boolean", - ], - "id": "push unarchive", - "supportsDryRun": true, - }, - { - "args": [ - "path", - ], - "destructive": false, - "flags": [ - "dry-run:boolean", - "api:boolean", - "method:option", - "body:string", - "query:string", - ], - "id": "raw", - "supportsDryRun": true, - }, - { - "args": [ - "id", - ], - "destructive": true, - "flags": [ - "yes:boolean", - "dry-run:boolean", - ], - "id": "scheduler delete", - "supportsDryRun": true, - }, - { - "args": [ - "id", - ], - "destructive": true, - "flags": [ - "yes:boolean", - "dry-run:boolean", - ], - "id": "scheduler disable", - "supportsDryRun": true, - }, - { - "args": [ - "id", - ], - "destructive": false, - "flags": [ - "dry-run:boolean", - ], - "id": "scheduler enable", - "supportsDryRun": true, - }, - { - "args": [ - "id", - ], - "destructive": false, - "flags": [], - "id": "scheduler get", - "supportsDryRun": false, - }, - { - "args": [], - "destructive": false, - "flags": [ - "page-size:string", - "after:string", - ], - "id": "scheduler list", - "supportsDryRun": false, - }, - { - "args": [ - "[command]", - ], - "destructive": false, - "flags": [], - "id": "schema", - "supportsDryRun": false, - }, - { - "args": [ - "id", - ], - "destructive": false, - "flags": [ - "dry-run:boolean", - ], - "id": "sms archive", - "supportsDryRun": true, - }, - { - "args": [ - "id", - ], - "destructive": false, - "flags": [ - "stats:boolean", - ], - "id": "sms campaign", - "supportsDryRun": false, - }, - { - "args": [], - "destructive": false, - "flags": [ - "page-size:string", - "after:string", - ], - "id": "sms campaigns", - "supportsDryRun": false, - }, - { - "args": [ - "id", - ], - "destructive": false, - "flags": [ - "dry-run:boolean", - ], - "id": "sms clone", - "supportsDryRun": true, - }, - { - "args": [ - "id", - ], - "destructive": true, - "flags": [ - "yes:boolean", - "dry-run:boolean", - ], - "id": "sms delete", - "supportsDryRun": true, - }, - { - "args": [ - "id", - ], - "destructive": true, - "flags": [ - "yes:boolean", - "dry-run:boolean", - ], - "id": "sms stop", + "id": "sms template update", "supportsDryRun": true, }, - { - "args": [ - "id", - ], - "destructive": false, - "flags": [], - "id": "sms template", - "supportsDryRun": false, - }, { "args": [], "destructive": false, diff --git a/tests/agent-mode.test.ts b/tests/agent-mode.test.ts index da18d49..f0ae997 100644 --- a/tests/agent-mode.test.ts +++ b/tests/agent-mode.test.ts @@ -53,6 +53,26 @@ const DESTRUCTIVE: string[][] = [ ['db', 'update', 'orders', '--many', '--filter', '{}', '--update', '{"$set":{"a":1}}'], ['api', 'membership', 'user', 'delete', 'abc123'], ['hub', 'scheduler', 'task', 'delete', 'abc123'], + ['ai', 'llm', 'delete', 'abc123'], + ['ai', 'llm', 'disable', 'abc123'], + ['ai', 'mcp', 'delete', 'abc123'], + ['ai', 'mcp', 'disable', 'abc123'], + ['ai', 'service-user', 'delete', 'abc123'], + ['ai', 'service-user', 'revoke-key', 'abc123', 'key_1'], + ['email', 'disable'], + ['email', 'campaign', 'delete', 'abc123'], + ['email', 'footer', 'delete', 'abc123'], + ['email', 'signature', 'delete', 'abc123'], + ['email', 'integration', 'delete', 'abc123'], + ['email', 'integration', 'disable', 'abc123'], + ['sms', 'disable'], + ['sms', 'campaign', 'delete', 'abc123'], + ['sms', 'integration', 'delete', 'abc123'], + ['sms', 'integration', 'disable', 'abc123'], + ['project', 'delete'], + ['project', 'disable'], + ['project', 'admin-portal', 'disable'], + ['project', 'ai', 'assistant', 'delete', 'abc123'], ] describe('destructive commands in a non-interactive shell', () => { diff --git a/tests/email-routes.test.ts b/tests/email-routes.test.ts index 0f86874..4ebac2e 100644 --- a/tests/email-routes.test.ts +++ b/tests/email-routes.test.ts @@ -63,7 +63,7 @@ const routes: Array<[string[], string, string]> = [ [['integration', ID], 'GET', `${P}/integrations/${ID}`], [['integration', 'save', '--provider', 'Fake'], 'POST', `${P}/integrations`], [['integration', 'enable', ID], 'PUT', `${P}/integrations/${ID}/enable`], - [['integration', 'disable', ID], 'PUT', `${P}/integrations/${ID}/disable`], + [['integration', 'disable', ID, '--yes'], 'PUT', `${P}/integrations/${ID}/disable`], [['integration', 'default', ID], 'PUT', `${P}/integrations/${ID}/default`], [['integration', 'delete', ID, '--yes'], 'DELETE', `${P}/integrations/${ID}`], [['integration', 'test', '--integration', ID, '--to', TO], 'POST', `${P}/integrations/test`], diff --git a/tests/email.test.ts b/tests/email.test.ts index affcea2..e12b87a 100644 --- a/tests/email.test.ts +++ b/tests/email.test.ts @@ -81,7 +81,7 @@ describe('email integrations', () => { const byId = [ {command: EmailIntegrationEnable, argv: [ID], method: 'enableEmailIntegration', request: {id: ID}, output: `Integration ${ID} enabled.`}, - {command: EmailIntegrationDisable, argv: [ID], method: 'disableEmailIntegration', request: {id: ID}, output: `Integration ${ID} disabled.`}, + {command: EmailIntegrationDisable, argv: [ID, '--yes'], method: 'disableEmailIntegration', request: {id: ID}, output: `Integration ${ID} disabled.`}, {command: EmailIntegrationDefault, argv: [ID], method: 'setEmailsIntegrationAsDefault', request: {id: ID}, output: `Integration ${ID} is now the default.`}, {command: EmailIntegrationDelete, argv: [ID, '--yes'], method: 'deleteEmailIntegration', request: {id: ID}, output: `Integration ${ID} deleted.`}, { diff --git a/tests/project-routes.test.ts b/tests/project-routes.test.ts index 201e967..85c2758 100644 --- a/tests/project-routes.test.ts +++ b/tests/project-routes.test.ts @@ -126,7 +126,7 @@ const routes: Array<[string[], Route[]]> = [ [['project', 'cors'], [['GET', P]]], [['project', 'cors', 'set', 'https://a.example.com'], [['PATCH', `${S}/origins`]]], [['project', 'cors', 'add', 'https://b.example.com'], [['GET', P], ['PATCH', `${S}/origins`]]], - [['project', 'cors', 'remove', 'https://app.example.com'], [['GET', P], ['PATCH', `${S}/origins`]]], + [['project', 'cors', 'remove', 'https://app.example.com', '--yes'], [['GET', P], ['PATCH', `${S}/origins`]]], // Admin portal [['project', 'admin-portal', 'enable'], [['PUT', `${P}/admin-portal/enabled`]]], @@ -156,7 +156,7 @@ const routes: Array<[string[], Route[]]> = [ [['ai', 'llm', 'save', '--provider', 'OpenAI', '--name', 'OpenAI'], [['POST', `${LLM}/`]]], [['ai', 'llm', 'test', ID], [['POST', `${LLM}/test`]]], [['ai', 'llm', 'enable', ID], [['PUT', `${LLM}/${ID}/enable`]]], - [['ai', 'llm', 'disable', ID], [['PUT', `${LLM}/${ID}/disable`]]], + [['ai', 'llm', 'disable', ID, '--yes'], [['PUT', `${LLM}/${ID}/disable`]]], [['ai', 'llm', 'default', ID], [['PUT', `${LLM}/${ID}/default`]]], [['ai', 'llm', 'delete', ID, '--yes'], [['DELETE', `${LLM}/${ID}`]]], @@ -166,7 +166,7 @@ const routes: Array<[string[], Route[]]> = [ [['ai', 'mcp', 'save', '--provider', 'GitHub', '--name', 'GitHub'], [['POST', `${MCP}/`]]], [['ai', 'mcp', 'test', ID], [['POST', `${MCP}/test`]]], [['ai', 'mcp', 'enable', ID], [['PUT', `${MCP}/${ID}/enable`]]], - [['ai', 'mcp', 'disable', ID], [['PUT', `${MCP}/${ID}/disable`]]], + [['ai', 'mcp', 'disable', ID, '--yes'], [['PUT', `${MCP}/${ID}/disable`]]], [['ai', 'mcp', 'delete', ID, '--yes'], [['DELETE', `${MCP}/${ID}`]]], // AI service users (account-level) @@ -250,7 +250,7 @@ describe('project settings bodies', () => { it('delete refuses without --yes when there is no terminal', async () => { const {error} = await runCommand(['project', 'delete', ...globalArgs]) - expect(error?.message).toMatch(/pass --yes/) + expect((error as {code?: string} | undefined)?.code).toBe('CONFIRMATION_REQUIRED') expect(calls).toHaveLength(0) }) }) @@ -279,7 +279,7 @@ describe('project cors bodies', () => { }) it('remove keeps the rest', async () => { - expect(await bodyOf(['project', 'cors', 'remove', 'https://app.example.com'])).toEqual({ + expect(await bodyOf(['project', 'cors', 'remove', 'https://app.example.com', '--yes'])).toEqual({ origins: ['https://pr_test-project.admin.norbix.ai'], }) }) diff --git a/tests/sms-routes.test.ts b/tests/sms-routes.test.ts index fb202b7..bc20880 100644 --- a/tests/sms-routes.test.ts +++ b/tests/sms-routes.test.ts @@ -63,7 +63,7 @@ const routes: Array<[string[], string, string]> = [ [['integrations'], 'GET', `${P}/integrations`], [['integration', ID], 'GET', `${P}/integrations/${ID}`], [['integration', 'enable', ID], 'PUT', `${P}/integrations/${ID}/enable`], - [['integration', 'disable', ID], 'PUT', `${P}/integrations/${ID}/disable`], + [['integration', 'disable', ID, '--yes'], 'PUT', `${P}/integrations/${ID}/disable`], [['integration', 'default', ID], 'PUT', `${P}/integrations/${ID}/default`], [['integration', 'delete', ID, '--yes'], 'DELETE', `${P}/integrations/${ID}`], [['integration', 'test', '--integration', ID, '--to', PHONE], 'POST', `${P}/integrations/test`], diff --git a/tests/sms.test.ts b/tests/sms.test.ts index 1dfd1bf..f8d6e3e 100644 --- a/tests/sms.test.ts +++ b/tests/sms.test.ts @@ -161,7 +161,7 @@ describe('sms integrations', () => { describe('sms integration state changes', () => { const cases = [ {command: SmsIntegrationEnable, method: 'enableSmsIntegration', argv: [ID], output: `Integration ${ID} enabled.`}, - {command: SmsIntegrationDisable, method: 'disableSmsIntegration', argv: [ID], output: `Integration ${ID} disabled.`}, + {command: SmsIntegrationDisable, method: 'disableSmsIntegration', argv: [ID, '--yes'], output: `Integration ${ID} disabled.`}, {command: SmsIntegrationDefault, method: 'setSmsIntegrationAsDefault', argv: [ID], output: `Integration ${ID} is now the default.`}, {command: SmsIntegrationDelete, method: 'deleteSmsIntegration', argv: [ID, '--yes'], output: `Integration ${ID} deleted.`}, ] From 38e283cfca1fd3131728cdea3a1371ee961c1373 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Sat, 3 Oct 2026 17:06:44 +0300 Subject: [PATCH 14/14] test(schema): snapshot with ai init and the commands from main Co-Authored-By: Claude Opus 5.5 --- tests/__snapshots__/schema.test.ts.snap | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/__snapshots__/schema.test.ts.snap b/tests/__snapshots__/schema.test.ts.snap index 71fbbdd..2ae0c65 100644 --- a/tests/__snapshots__/schema.test.ts.snap +++ b/tests/__snapshots__/schema.test.ts.snap @@ -54,6 +54,18 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "id": "account usage", "supportsDryRun": false, }, + { + "args": [], + "destructive": false, + "flags": [ + "dry-run:boolean", + "target:option", + "force:boolean", + "dir:string", + ], + "id": "ai init", + "supportsDryRun": true, + }, { "args": [ "id", @@ -287,18 +299,6 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "id": "ai service-users", "supportsDryRun": false, }, - { - "args": [], - "destructive": false, - "flags": [ - "dry-run:boolean", - "target:option", - "force:boolean", - "dir:string", - ], - "id": "ai init", - "supportsDryRun": true, - }, { "args": [], "destructive": true,