diff --git a/README.md b/README.md index 02f8700..68731f7 100644 --- a/README.md +++ b/README.md @@ -52,15 +52,26 @@ OAuth authorize/token endpoints derive from this base, so logging in targets the ``` kit login Authenticate via OAuth (PKCE) kit logout Clear stored OAuth tokens -kit account View account info kit config show Show all config and auth status ``` +### account + +``` +account View account info +account colors +account set-colors Replace brand colors (up to 10) +account creator-profile +account email-stats +account growth-stats [options] +``` + ### subscribers ``` list [options] get [options] +filter [options] Filter by engagement, sign-up date, state, tags create [options] update [options] unsubscribe @@ -68,17 +79,36 @@ tags [options] stats [options] ``` +`list` takes `--slim` to drop the expensive optional fields. + +`filter` reads its conditions from `--json ` or `--file `, as either a +bare conditions array or a full body with an `all` key: + +``` +kit subscribers filter --json '[{"type":"subscriber_state","states":["active"]}]' +kit subscribers filter --file conditions.json --include tags,stats --stats-start 2026-05-01 +``` + +`create` and `update` print a warning on stderr when the API ignores a custom +field key. Keys are the field's `key`, not its label, so `last_name` rather than +`Last Name`. + ### tags ``` list [options] create +update Rename a tag subscribers [options] add add-by-email remove +remove-by-email ``` +`subscribers` filters on `--state`, `--created-after`, `--created-before`, +`--tagged-after`, and `--tagged-before`. + ### forms ``` @@ -92,11 +122,23 @@ add-by-email ``` list [options] +get [options] +create [options] --name +update [options] +delete subscribers [options] add add-by-email +emails list [options] +emails get [options] +emails create [options] --subject --delay-value --delay-unit +emails update [options] +emails delete ``` +`list` and `get` take `--include stats`. `emails list` also takes +`--include-content`. + ### broadcasts ``` @@ -105,9 +147,13 @@ get [options] create [options] update [options] delete -stats [options] +stats [options] [id] One broadcast, or every broadcast with no ID +clicks [options] Link click stats ``` +`list` and `stats` filter on `--status `, +`--sent-after`, and `--sent-before`. + ### custom-fields ``` @@ -122,6 +168,7 @@ delete ``` list [options] get [options] +create --file Record a purchase from JSON ``` ### webhooks @@ -132,6 +179,25 @@ create [options] delete ``` +### posts + +``` +list [options] --include-content for post bodies +get [options] +``` + +### snippets + +``` +list [options] --snippet-type , --archived +get [options] +create [options] --type +update [options] --name, --content, --html, --archive, --restore +``` + +An inline snippet holds Liquid text, passed with `--content`. A block snippet +holds HTML, passed with `--html`. + ### segments · email-templates ``` @@ -145,6 +211,7 @@ All bulk commands take `--file ` (JSON array) and optional `--callback-url ``` bulk subscribers create --file [{email_address, first_name?, state?}, ...] bulk tags create --file [{name}, ...] +bulk tags delete --file [{id}, ...] bulk tags add --file [{tag_id, subscriber_id}, ...] bulk tags remove --file [{tag_id, subscriber_id}, ...] bulk forms add --file [{form_id, subscriber_id, referrer?}, ...] @@ -163,6 +230,14 @@ bulk custom-fields update-values --file [{subscriber_id, subscriber_cust Run `kit --help` for full flag details on any command. +## API coverage + +[`spec/coverage.js`](spec/coverage.js) maps every operation in the stored API spec +to the command that reaches it. A test holds the map to the spec and to the +command tree, so a spec change that adds or drops an endpoint fails the suite +until someone triages it, and the map can never name a command that no longer +exists. Today it covers all 73 operations. + ## Claude Code Skill ``` diff --git a/bin/kit.js b/bin/kit.js index d826e67..bd208f8 100755 --- a/bin/kit.js +++ b/bin/kit.js @@ -1,46 +1,5 @@ #!/usr/bin/env node -import { Command } from 'commander'; -import { accountCommand, configCommand, setupSkillCommand } from '../src/commands/account.js'; -import { loginCommand, logoutCommand } from '../src/commands/auth.js'; -import { bulkCommand } from '../src/commands/bulk.js'; -import { subscribersCommand } from '../src/commands/subscribers.js'; -import { tagsCommand } from '../src/commands/tags.js'; -import { formsCommand } from '../src/commands/forms.js'; -import { sequencesCommand } from '../src/commands/sequences.js'; -import { broadcastsCommand } from '../src/commands/broadcasts.js'; -import { customFieldsCommand } from '../src/commands/custom-fields.js'; -import { purchasesCommand } from '../src/commands/purchases.js'; -import { webhooksCommand } from '../src/commands/webhooks.js'; -import { segmentsCommand } from '../src/commands/segments.js'; -import { emailTemplatesCommand } from '../src/commands/email-templates.js'; -import { postsCommand } from '../src/commands/posts.js'; -import { snippetsCommand } from '../src/commands/snippets.js'; +import { buildProgram } from '../src/program.js'; -const program = new Command(); - -program - .name('kit') - .description('CLI for the Kit (ConvertKit) email marketing API (V4)') - .version('1.0.0'); - -program.addCommand(loginCommand()); -program.addCommand(logoutCommand()); -program.addCommand(accountCommand()); -program.addCommand(configCommand()); -program.addCommand(setupSkillCommand()); -program.addCommand(subscribersCommand()); -program.addCommand(tagsCommand()); -program.addCommand(formsCommand()); -program.addCommand(sequencesCommand()); -program.addCommand(broadcastsCommand()); -program.addCommand(customFieldsCommand()); -program.addCommand(purchasesCommand()); -program.addCommand(webhooksCommand()); -program.addCommand(segmentsCommand()); -program.addCommand(emailTemplatesCommand()); -program.addCommand(postsCommand()); -program.addCommand(snippetsCommand()); -program.addCommand(bulkCommand()); - -program.parse(); +buildProgram().parse(); diff --git a/scripts/spec-coverage.test.js b/scripts/spec-coverage.test.js new file mode 100644 index 0000000..aad9301 --- /dev/null +++ b/scripts/spec-coverage.test.js @@ -0,0 +1,125 @@ +/** + * Holds spec/coverage.js to the spec and to the command tree. + * + * This is the test that makes an api-spec-change issue actionable. When the spec + * gains or loses an endpoint, the first two tests here fail and name it. When a + * command is renamed or removed, the third fails and names it. + */ +import { test, describe } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { COVERAGE, NOT_EXPOSED, specOperations } from '../spec/coverage.js'; +import { buildProgram } from '../src/program.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const spec = JSON.parse(readFileSync(join(__dirname, '..', 'spec', 'v4.json'), 'utf8')); + +const OPERATIONS = specOperations(spec); + +/** Every command path in the tree, as space-separated strings. */ +function commandPaths(cmd, prefix = []) { + const paths = []; + for (const child of cmd.commands) { + if (child.name() === 'help') continue; + const path = [...prefix, child.name()]; + paths.push(path.join(' ')); + paths.push(...commandPaths(child, path)); + } + return paths; +} + +const COMMANDS = new Set(commandPaths(buildProgram())); + +describe('spec coverage', () => { + test('the spec has operations to check', () => { + assert.ok(OPERATIONS.length > 40, `only found ${OPERATIONS.length} operations`); + }); + + test('every spec operation is accounted for', () => { + const missing = OPERATIONS.filter((op) => !(op in COVERAGE) && !(op in NOT_EXPOSED)); + assert.deepEqual( + missing, + [], + `The spec has operations that spec/coverage.js does not mention. Add a CLI ` + + `command for each one, or add it to NOT_EXPOSED with a reason:\n ` + + missing.join('\n ') + ); + }); + + test('no coverage entry names an operation the spec dropped', () => { + const known = new Set(OPERATIONS); + const stale = [...Object.keys(COVERAGE), ...Object.keys(NOT_EXPOSED)].filter((op) => !known.has(op)); + assert.deepEqual( + stale, + [], + `spec/coverage.js mentions operations the spec no longer has:\n ` + stale.join('\n ') + ); + }); + + test('every command named in the map exists in the command tree', () => { + const broken = Object.entries(COVERAGE) + .filter(([, command]) => !COMMANDS.has(command)) + .map(([op, command]) => `${op} -> ${command}`); + assert.deepEqual( + broken, + [], + `spec/coverage.js names commands that do not exist:\n ` + broken.join('\n ') + ); + }); + + test('every NOT_EXPOSED entry gives a reason', () => { + const unexplained = Object.entries(NOT_EXPOSED) + .filter(([, reason]) => typeof reason !== 'string' || reason.trim().length === 0) + .map(([op]) => op); + assert.deepEqual(unexplained, []); + }); + + test('an operation is not both covered and skipped', () => { + const both = Object.keys(COVERAGE).filter((op) => op in NOT_EXPOSED); + assert.deepEqual(both, []); + }); +}); + +describe('command tree', () => { + test('every top-level command has a description', () => { + const undescribed = buildProgram() + .commands.filter((c) => c.name() !== 'help' && !c.description()) + .map((c) => c.name()); + assert.deepEqual(undescribed, []); + }); + + test('every leaf command has a description', () => { + const walk = (cmd, prefix = []) => { + const bad = []; + for (const child of cmd.commands) { + if (child.name() === 'help') continue; + const path = [...prefix, child.name()]; + if (!child.description()) bad.push(path.join(' ')); + bad.push(...walk(child, path)); + } + return bad; + }; + assert.deepEqual(walk(buildProgram()), []); + }); + + test('no two sibling commands share a name', () => { + const walk = (cmd, prefix = []) => { + const dupes = []; + const seen = new Set(); + for (const child of cmd.commands) { + const name = child.name(); + if (seen.has(name)) dupes.push([...prefix, name].join(' ')); + seen.add(name); + dupes.push(...walk(child, [...prefix, name])); + } + return dupes; + }; + assert.deepEqual(walk(buildProgram()), []); + }); + + test('the program reports its version', () => { + assert.match(buildProgram().version(), /^\d+\.\d+\.\d+$/); + }); +}); diff --git a/skills/kit/SKILL.md b/skills/kit/SKILL.md index 0a23a3b..6629d68 100644 --- a/skills/kit/SKILL.md +++ b/skills/kit/SKILL.md @@ -1,6 +1,6 @@ --- name: kit -description: Manage your Kit (ConvertKit) email marketing account. Use this skill when the user wants to manage subscribers, tags, forms, sequences, broadcasts, custom fields, purchases, webhooks, segments, email templates, or bulk operations via the Kit API. Examples - "list my subscribers", "create a broadcast", "tag a subscriber", "show my account", "check broadcast stats", "bulk import subscribers". +description: Manage your Kit (ConvertKit) email marketing account. Use this skill when the user wants to manage subscribers, tags, forms, sequences, sequence emails, broadcasts, posts, snippets, custom fields, purchases, webhooks, segments, email templates, or bulk operations via the Kit API. Examples - "list my subscribers", "create a broadcast", "tag a subscriber", "show my account", "check broadcast stats", "bulk import subscribers". argument-hint: "[action or question about your Kit account]" allowed-tools: Bash --- @@ -58,7 +58,12 @@ Use the `kit` CLI to fulfill the user's request: `$ARGUMENTS` **Auth & Config:** - `kit login` — Authenticate via OAuth (PKCE) — opens browser - `kit logout` — Clear stored OAuth tokens -- `kit account` — View account info (name, plan, email) +- `kit account` — View account info (name, plan, sending addresses, time zone) +- `kit account colors` — List brand colors +- `kit account set-colors ` — Replace brand colors (up to 10, e.g. `#ff0000`) +- `kit account creator-profile` — Show the creator profile +- `kit account email-stats` — Lifetime email stats +- `kit account growth-stats [--starting ] [--ending ]` — Subscriber growth stats - `kit config show` — Show full config and auth status - `kit config set-client-id ` — Save OAuth client ID - `kit config set-redirect-uri ` — Save OAuth redirect URI @@ -67,21 +72,24 @@ Use the `kit` CLI to fulfill the user's request: `$ARGUMENTS` - `kit config set-per-page ` — Change default page size **Subscribers:** -- `kit subscribers list` — List subscribers (filters: `-e/--email`, `-s/--state`, `--created-after`, `--created-before`, `--sort-field`, `--sort-order`) +- `kit subscribers list` — List subscribers (filters: `-e/--email`, `-s/--state`, `--created-after`, `--created-before`, `--sort-field`, `--sort-order`, `--slim`) - `kit subscribers get ` — Get subscriber details - `kit subscribers create ` — Create/upsert subscriber (`-n/--first-name`, `--fields '{"key":"val"}'`) - `kit subscribers update ` — Update subscriber (`-e/--email`, `-n/--first-name`, `--fields`) - `kit subscribers unsubscribe ` — Unsubscribe a subscriber - `kit subscribers tags ` — List tags for a subscriber - `kit subscribers stats ` — Get engagement stats +- `kit subscribers filter --json ''` — Filter by engagement, sign-up date, state, and tags. Also takes `--file `, `--counting-mode `, `--include `, `--stats-start`, `--stats-end` **Tags:** - `kit tags list` — List all tags - `kit tags create ` — Create a tag -- `kit tags subscribers ` — List subscribers with a tag +- `kit tags subscribers ` — List subscribers with a tag (filters: `-s/--state`, `--created-after`, `--created-before`, `--tagged-after`, `--tagged-before`) - `kit tags add ` — Tag a subscriber by ID - `kit tags add-by-email ` — Tag a subscriber by email - `kit tags remove ` — Remove a tag from a subscriber +- `kit tags remove-by-email ` — Remove a tag from a subscriber by email +- `kit tags update ` — Rename a tag **Forms:** - `kit forms list` — List all forms (filters: `-s/--status`, `-t/--type`) @@ -90,18 +98,31 @@ Use the `kit` CLI to fulfill the user's request: `$ARGUMENTS` - `kit forms add-by-email ` — Add subscriber by email **Sequences:** -- `kit sequences list` — List all sequences +- `kit sequences list [--include stats]` — List all sequences +- `kit sequences get [--include stats]` — Get sequence details +- `kit sequences create --name "..."` — Create a sequence (`--send-days`, `--send-hour`, `--time-zone`, `--active/--no-active`, `--repeat`, `--hold`, `--exclude-tag-ids`, `--exclude-sequence-ids`, `--exclude-form-ids`, `--exclude-segment-ids`, `--email-address`, `--email-template-id`) +- `kit sequences update ` — Update a sequence (same flags as create) +- `kit sequences delete ` — Delete a sequence - `kit sequences subscribers ` — List subscribers for a sequence - `kit sequences add ` — Add subscriber to sequence - `kit sequences add-by-email ` — Add subscriber by email +**Sequence Emails:** +- `kit sequences emails list [--include-content] [--include stats]` — List the emails in a sequence +- `kit sequences emails get [--include stats]` — Get one sequence email +- `kit sequences emails create --subject "..." --delay-value --delay-unit ` — Add an email (`--content`, `--preview-text`, `--published/--no-published`, `--send-days`, `--position`, `--email-template-id`) +- `kit sequences emails update ` — Update an email (same flags as create) +- `kit sequences emails delete ` — Delete an email + **Broadcasts:** -- `kit broadcasts list` — List all broadcasts +- `kit broadcasts list` — List all broadcasts (filters: `-s/--status `, `--sent-after`, `--sent-before`) - `kit broadcasts get ` — Get broadcast details - `kit broadcasts create --subject "..." --content "..." [--send-at ISO8601] [--public] [--tag-ids 1,2] [--segment-ids 1,2]` — Create broadcast - `kit broadcasts update [--subject] [--content] [--send-at] [--public/--no-public]` — Update broadcast - `kit broadcasts delete ` — Delete a draft/scheduled broadcast -- `kit broadcasts stats ` — Get broadcast engagement stats +- `kit broadcasts stats ` — Get engagement stats for one broadcast +- `kit broadcasts stats` — Get stats for every broadcast (filters: `-s/--status`, `--sent-after`, `--sent-before`, `--include-total-count`) +- `kit broadcasts clicks ` — Get link click stats for a broadcast **Custom Fields:** - `kit custom-fields list` — List all custom fields @@ -112,6 +133,7 @@ Use the `kit` CLI to fulfill the user's request: `$ARGUMENTS` **Purchases:** - `kit purchases list` — List all purchases - `kit purchases get ` — Get purchase details +- `kit purchases create --file ` — Record a purchase from JSON **Webhooks:** - `kit webhooks list` — List all webhooks @@ -124,12 +146,24 @@ Use the `kit` CLI to fulfill the user's request: `$ARGUMENTS` **Email Templates:** - `kit email-templates list` — List all email templates +**Posts:** +- `kit posts list [--include-content]` — List published posts +- `kit posts get ` — Get post details + +**Snippets:** +- `kit snippets list [--snippet-type ] [--archived] [--include-content]` — List snippets +- `kit snippets get ` — Get snippet details +- `kit snippets create --type inline --content "..."` — Create an inline (Liquid text) snippet +- `kit snippets create --type block --html "..."` — Create a block (HTML) snippet +- `kit snippets update ` — Update a snippet (`--name`, `--content`, `--html`, `--archive`, `--restore`) + **Bulk (requires OAuth):** All bulk commands take `--file ` (a JSON file containing an array) and optional `--callback-url `. Batches of ≤100 are synchronous; larger batches are queued and POSTed to the callback URL when done. - `kit bulk subscribers create --file ` — Upsert many subscribers. Array of `{email_address, first_name?, state?}` - `kit bulk tags create --file ` — Create many tags. Array of `{name}` +- `kit bulk tags delete --file ` — Delete many tags. Array of `{id}` - `kit bulk tags add --file ` — Tag many subscribers. Array of `{tag_id, subscriber_id}` - `kit bulk tags remove --file ` — Remove tags from many subscribers. Array of `{tag_id, subscriber_id}` - `kit bulk forms add --file ` — Add many subscribers to forms. Array of `{form_id, subscriber_id, referrer?}` @@ -152,4 +186,5 @@ All list commands support: 4. **Confirm destructive actions.** Before deleting broadcasts, webhooks, custom fields, or unsubscribing users, confirm with the user. 5. **Use JSON format for piping.** When you need to process data programmatically (e.g., to extract IDs for a follow-up command), use `-f json` and parse with `jq` or node. 6. **Show pagination info.** When results are paginated, let the user know there are more results and offer to fetch the next page. -7. **Handle errors gracefully.** If a command fails, explain what went wrong and suggest a fix. If a bulk command fails with 401, remind the user that bulk requires OAuth. +7. **Watch for warnings.** `kit subscribers create` and `kit subscribers update` print warnings on stderr when the API ignores a custom field key. Custom field keys are the field's `key`, not its label, so `last_name` rather than `Last Name`. If you see a warning, check the key with `kit custom-fields list`. +8. **Handle errors gracefully.** If a command fails, explain what went wrong and suggest a fix. If a bulk command fails with 401, remind the user that bulk requires OAuth. diff --git a/spec/coverage.js b/spec/coverage.js new file mode 100644 index 0000000..ffe10e8 --- /dev/null +++ b/spec/coverage.js @@ -0,0 +1,140 @@ +/** + * Maps every operation in spec/v4.json to the CLI command that reaches it. + * + * The API spec check workflow opens an issue whenever the spec moves. Triaging + * that issue means answering one question per changed endpoint: does the CLI + * cover this? This map is that answer, written down. + * + * scripts/spec-coverage.test.js holds the map to the spec: + * + * - every spec operation appears here + * - every entry here is still in the spec + * - every command named here exists in the command tree + * + * So a spec change that adds or removes an endpoint fails the tests until + * someone updates this file, and an entry can never name a command that was + * renamed or deleted. + * + * Set `command` to null and give a `reason` for an operation the CLI leaves + * alone on purpose. + */ +export const COVERAGE = { + // ── Account ────────────────────────────────────────────────────────────── + 'GET /v4/account': 'account', + 'GET /v4/account/colors': 'account colors', + 'PUT /v4/account/colors': 'account set-colors', + 'GET /v4/account/creator_profile': 'account creator-profile', + 'GET /v4/account/email_stats': 'account email-stats', + 'GET /v4/account/growth_stats': 'account growth-stats', + + // ── Broadcasts ─────────────────────────────────────────────────────────── + 'GET /v4/broadcasts': 'broadcasts list', + 'POST /v4/broadcasts': 'broadcasts create', + 'GET /v4/broadcasts/{id}': 'broadcasts get', + 'PUT /v4/broadcasts/{id}': 'broadcasts update', + 'DELETE /v4/broadcasts/{id}': 'broadcasts delete', + // One subcommand covers both stats endpoints. With an ID it asks for one + // broadcast, without one it asks for every broadcast. + 'GET /v4/broadcasts/stats': 'broadcasts stats', + 'GET /v4/broadcasts/{broadcast_id}/stats': 'broadcasts stats', + 'GET /v4/broadcasts/{broadcast_id}/clicks': 'broadcasts clicks', + + // ── Bulk ───────────────────────────────────────────────────────────────── + 'POST /v4/bulk/subscribers': 'bulk subscribers create', + 'POST /v4/bulk/tags': 'bulk tags create', + 'DELETE /v4/bulk/tags': 'bulk tags delete', + 'POST /v4/bulk/tags/subscribers': 'bulk tags add', + 'DELETE /v4/bulk/tags/subscribers': 'bulk tags remove', + 'POST /v4/bulk/forms/subscribers': 'bulk forms add', + 'POST /v4/bulk/custom_fields': 'bulk custom-fields create', + 'POST /v4/bulk/custom_fields/subscribers': 'bulk custom-fields update-values', + + // ── Custom fields ──────────────────────────────────────────────────────── + 'GET /v4/custom_fields': 'custom-fields list', + 'POST /v4/custom_fields': 'custom-fields create', + 'PUT /v4/custom_fields/{id}': 'custom-fields update', + 'DELETE /v4/custom_fields/{id}': 'custom-fields delete', + + // ── Email templates ────────────────────────────────────────────────────── + 'GET /v4/email_templates': 'email-templates list', + + // ── Forms ──────────────────────────────────────────────────────────────── + 'GET /v4/forms': 'forms list', + 'GET /v4/forms/{form_id}/subscribers': 'forms subscribers', + 'POST /v4/forms/{form_id}/subscribers': 'forms add-by-email', + 'POST /v4/forms/{form_id}/subscribers/{id}': 'forms add', + + // ── Posts ──────────────────────────────────────────────────────────────── + 'GET /v4/posts': 'posts list', + 'GET /v4/posts/{id}': 'posts get', + + // ── Purchases ──────────────────────────────────────────────────────────── + 'GET /v4/purchases': 'purchases list', + 'POST /v4/purchases': 'purchases create', + 'GET /v4/purchases/{id}': 'purchases get', + + // ── Segments ───────────────────────────────────────────────────────────── + 'GET /v4/segments': 'segments list', + + // ── Sequences ──────────────────────────────────────────────────────────── + 'GET /v4/sequences': 'sequences list', + 'POST /v4/sequences': 'sequences create', + 'GET /v4/sequences/{id}': 'sequences get', + 'PUT /v4/sequences/{id}': 'sequences update', + 'DELETE /v4/sequences/{id}': 'sequences delete', + 'GET /v4/sequences/{sequence_id}/subscribers': 'sequences subscribers', + 'POST /v4/sequences/{sequence_id}/subscribers': 'sequences add-by-email', + 'POST /v4/sequences/{sequence_id}/subscribers/{id}': 'sequences add', + + // ── Sequence emails ────────────────────────────────────────────────────── + 'GET /v4/sequences/{sequence_id}/emails': 'sequences emails list', + 'POST /v4/sequences/{sequence_id}/emails': 'sequences emails create', + 'GET /v4/sequences/{sequence_id}/emails/{id}': 'sequences emails get', + 'PUT /v4/sequences/{sequence_id}/emails/{id}': 'sequences emails update', + 'DELETE /v4/sequences/{sequence_id}/emails/{id}': 'sequences emails delete', + + // ── Snippets ───────────────────────────────────────────────────────────── + 'GET /v4/snippets': 'snippets list', + 'POST /v4/snippets': 'snippets create', + 'GET /v4/snippets/{id}': 'snippets get', + 'PUT /v4/snippets/{id}': 'snippets update', + + // ── Subscribers ────────────────────────────────────────────────────────── + 'GET /v4/subscribers': 'subscribers list', + 'POST /v4/subscribers': 'subscribers create', + 'POST /v4/subscribers/filter': 'subscribers filter', + 'GET /v4/subscribers/{id}': 'subscribers get', + 'PUT /v4/subscribers/{id}': 'subscribers update', + 'POST /v4/subscribers/{id}/unsubscribe': 'subscribers unsubscribe', + 'GET /v4/subscribers/{subscriber_id}/stats': 'subscribers stats', + 'GET /v4/subscribers/{subscriber_id}/tags': 'subscribers tags', + + // ── Tags ───────────────────────────────────────────────────────────────── + 'GET /v4/tags': 'tags list', + 'POST /v4/tags': 'tags create', + 'PUT /v4/tags/{id}': 'tags update', + 'GET /v4/tags/{tag_id}/subscribers': 'tags subscribers', + 'POST /v4/tags/{tag_id}/subscribers': 'tags add-by-email', + 'DELETE /v4/tags/{tag_id}/subscribers': 'tags remove-by-email', + 'POST /v4/tags/{tag_id}/subscribers/{id}': 'tags add', + 'DELETE /v4/tags/{tag_id}/subscribers/{id}': 'tags remove', + + // ── Webhooks ───────────────────────────────────────────────────────────── + 'GET /v4/webhooks': 'webhooks list', + 'POST /v4/webhooks': 'webhooks create', + 'DELETE /v4/webhooks/{id}': 'webhooks delete', +}; + +/** Operations the CLI leaves alone on purpose, with the reason. */ +export const NOT_EXPOSED = {}; + +/** Lists every operation key in a spec document, in the same shape as COVERAGE. */ +export function specOperations(spec) { + const keys = []; + for (const [path, item] of Object.entries(spec.paths || {})) { + for (const method of ['get', 'post', 'put', 'patch', 'delete']) { + if (item[method]) keys.push(`${method.toUpperCase()} ${path}`); + } + } + return keys.sort(); +} diff --git a/src/program.js b/src/program.js new file mode 100644 index 0000000..c11eecf --- /dev/null +++ b/src/program.js @@ -0,0 +1,53 @@ +import { Command } from 'commander'; +import { accountCommand, configCommand, setupSkillCommand } from './commands/account.js'; +import { loginCommand, logoutCommand } from './commands/auth.js'; +import { bulkCommand } from './commands/bulk.js'; +import { subscribersCommand } from './commands/subscribers.js'; +import { tagsCommand } from './commands/tags.js'; +import { formsCommand } from './commands/forms.js'; +import { sequencesCommand } from './commands/sequences.js'; +import { broadcastsCommand } from './commands/broadcasts.js'; +import { customFieldsCommand } from './commands/custom-fields.js'; +import { purchasesCommand } from './commands/purchases.js'; +import { webhooksCommand } from './commands/webhooks.js'; +import { segmentsCommand } from './commands/segments.js'; +import { emailTemplatesCommand } from './commands/email-templates.js'; +import { postsCommand } from './commands/posts.js'; +import { snippetsCommand } from './commands/snippets.js'; + +export const VERSION = '1.0.0'; + +/** + * Builds the whole command tree. + * + * Kept apart from bin/kit.js so tests can walk the tree without parsing argv. + */ +export function buildProgram() { + const program = new Command(); + + program + .name('kit') + .description('CLI for the Kit (ConvertKit) email marketing API (V4)') + .version(VERSION); + + program.addCommand(loginCommand()); + program.addCommand(logoutCommand()); + program.addCommand(accountCommand()); + program.addCommand(configCommand()); + program.addCommand(setupSkillCommand()); + program.addCommand(subscribersCommand()); + program.addCommand(tagsCommand()); + program.addCommand(formsCommand()); + program.addCommand(sequencesCommand()); + program.addCommand(broadcastsCommand()); + program.addCommand(customFieldsCommand()); + program.addCommand(purchasesCommand()); + program.addCommand(webhooksCommand()); + program.addCommand(segmentsCommand()); + program.addCommand(emailTemplatesCommand()); + program.addCommand(postsCommand()); + program.addCommand(snippetsCommand()); + program.addCommand(bulkCommand()); + + return program; +}