@verbatims/cli 0.1.14 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,6 +25,38 @@ verbatims quotes browse
25
25
  # Search
26
26
  verbatims search "love" --type quotes
27
27
 
28
+ # List social platforms with queue stats
29
+ verbatims social platforms
30
+
31
+ # List the social queue (defaults to platform x)
32
+ verbatims social queue list --platform bluesky --status queued
33
+
34
+ # Add approved quotes to the queue
35
+ verbatims social queue add 42 57 --platform bluesky
36
+
37
+ # Publish the next eligible queued item immediately
38
+ verbatims social queue run-now --platform bluesky
39
+
28
40
  # See all commands
29
41
  verbatims --help
30
42
  ```
43
+
44
+ ### Social commands
45
+
46
+ ```bash
47
+ verbatims social platforms # list platforms + queue stats
48
+ verbatims social posts [--platform <p>] [--status <s>] # audit trail of published posts
49
+ verbatims social queue list [--platform <p>] [--status <s>] [--search <q>] [--limit <n>]
50
+ verbatims social queue get <id> # single queue item
51
+ verbatims social queue add <ids...> [--platform <p>] [--scheduled-for <iso>]
52
+ verbatims social queue random [--platform <p>] [--count <n>] [--language <lang>]
53
+ verbatims social queue remove <id> [--yes]
54
+ verbatims social queue requeue <id> # reset a failed item to queued
55
+ verbatims social queue reorder <id> [--direction up|down] [--before <id>]
56
+ verbatims social queue clear [--platform <p>] [--scope all|finished] [--yes]
57
+ verbatims social queue run-now [--platform <p>] # publish immediately (1/min)
58
+ ```
59
+
60
+ Platforms: `x`, `bluesky`, `instagram`, `threads`, `facebook`, `pinterest`.
61
+ Queue statuses: `queued`, `processing`, `posted`, `failed`, `active`.
62
+ Requires an API key with the `social:read` / `social:write` permissions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verbatims/cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "CLI for the Verbatims quotes API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,6 +7,7 @@ import { registerCollectionsCommand } from './collections.js'
7
7
  import { registerSearchCommand } from './search.js'
8
8
  import { registerConfigCommand } from './config.js'
9
9
  import { registerThemesCommand } from './themes.js'
10
+ import { registerSocialCommand } from './social.js'
10
11
 
11
12
  export function registerCommands(program: Command) {
12
13
  registerQuotesCommand(program)
@@ -17,4 +18,5 @@ export function registerCommands(program: Command) {
17
18
  registerSearchCommand(program)
18
19
  registerConfigCommand(program)
19
20
  registerThemesCommand(program)
21
+ registerSocialCommand(program)
20
22
  }
@@ -0,0 +1,247 @@
1
+ import type { Command } from 'commander'
2
+ import { text, isCancel, cancel, confirm, select } from '@clack/prompts'
3
+ import chalk from 'chalk'
4
+ import { getClient } from '../utils/client.js'
5
+ import { output, type Format } from '../utils/format.js'
6
+ import { withSpinner } from '../utils/spinner.js'
7
+
8
+ function getFormat(program: Command): Format {
9
+ return (program.opts().format ?? 'table') as Format
10
+ }
11
+
12
+ const PLATFORM_CHOICES = ['x', 'bluesky', 'instagram', 'threads', 'facebook', 'pinterest']
13
+
14
+ export function registerSocialCommand(program: Command) {
15
+ const social = program.command('social').description('Manage the social auto-post queue')
16
+
17
+ social
18
+ .command('platforms')
19
+ .description('List social platforms with their queue stats')
20
+ .action(async () => {
21
+ const client = await getClient()
22
+ const format = getFormat(program)
23
+ const { data } = await withSpinner('Fetching platforms', () => client.social.listPlatforms(), format)
24
+ output(data, format)
25
+ })
26
+
27
+ social
28
+ .command('posts')
29
+ .description('List published social posts (audit trail)')
30
+ .option('--platform <platform>', 'Filter by platform')
31
+ .option('--status <status>', 'Filter by status (success|failed)')
32
+ .option('--limit <n>', 'Number of results', '20')
33
+ .action(async (options: Record<string, string>) => {
34
+ const client = await getClient()
35
+ const format = getFormat(program)
36
+ const { data, pagination } = await withSpinner('Fetching posts', () =>
37
+ client.social.listPosts({
38
+ platform: options.platform as any,
39
+ status: options.status as any,
40
+ limit: Number(options.limit),
41
+ }),
42
+ format,
43
+ )
44
+ if (data) output(data.posts, format)
45
+ if (pagination && format !== 'json') {
46
+ console.log(chalk.dim(`\nPage ${pagination.page}/${pagination.totalPages} — ${pagination.total} total`))
47
+ }
48
+ })
49
+
50
+ const queue = social.command('queue').description('Manage the social queue')
51
+
52
+ queue
53
+ .command('list')
54
+ .description('List the social queue for a platform')
55
+ .option('--platform <platform>', 'Platform (default: x)')
56
+ .option('--status <status>', 'Filter by status (queued|processing|posted|failed|active)')
57
+ .option('--search <q>', 'Search by quote text, author, reference, or source')
58
+ .option('--limit <n>', 'Number of results', '20')
59
+ .action(async (options: Record<string, string>) => {
60
+ const client = await getClient()
61
+ const format = getFormat(program)
62
+ const { data, pagination } = await withSpinner('Fetching queue', () =>
63
+ client.social.listQueue({
64
+ platform: options.platform as any,
65
+ status: options.status as any,
66
+ search: options.search,
67
+ limit: Number(options.limit),
68
+ }),
69
+ format,
70
+ )
71
+ if (data) {
72
+ if (format !== 'json') {
73
+ const stats = data.stats
74
+ console.log(chalk.dim(
75
+ `Queued: ${stats.queued} · Processing: ${stats.processing} · Posted: ${stats.posted} · Failed: ${stats.failed}`,
76
+ ))
77
+ }
78
+ output(data.queue, format)
79
+ }
80
+ if (pagination && format !== 'json') {
81
+ console.log(chalk.dim(`\nPage ${pagination.page}/${pagination.totalPages} — ${pagination.total} total`))
82
+ }
83
+ })
84
+
85
+ queue
86
+ .command('get <id>')
87
+ .description('Get a single queue item')
88
+ .action(async (id: string) => {
89
+ const client = await getClient()
90
+ const format = getFormat(program)
91
+ const { data } = await withSpinner('Fetching queue item', () => client.social.getQueueItem(Number(id)), format)
92
+ output(data, format)
93
+ })
94
+
95
+ queue
96
+ .command('add <ids...>')
97
+ .description('Add approved quotes to the queue')
98
+ .option('--platform <platform>', 'Platform (default: x)')
99
+ .option('--scheduled-for <iso>', 'Optional future publish time (ISO 8601)')
100
+ .action(async (ids: string[], options: Record<string, string>) => {
101
+ const client = await getClient()
102
+ const format = getFormat(program)
103
+ const quoteIds = ids.map(Number).filter((n) => Number.isInteger(n) && n > 0)
104
+ if (!quoteIds.length) {
105
+ console.log(chalk.red(' Provide at least one quote ID'))
106
+ process.exit(1)
107
+ }
108
+
109
+ const { data, count } = await withSpinner('Adding quotes to queue', () =>
110
+ client.social.addToQueue({
111
+ quote_ids: quoteIds,
112
+ platform: options.platform as any,
113
+ scheduled_for: options.scheduledFor || null,
114
+ }),
115
+ format,
116
+ )
117
+ console.log(chalk.green(` ${count ?? data?.length ?? 0} quote(s) queued`))
118
+ output(data, format)
119
+ })
120
+
121
+ queue
122
+ .command('random')
123
+ .description('Enqueue N random approved quotes')
124
+ .option('--platform <platform>', 'Platform (default: x)')
125
+ .option('--count <n>', 'Number of quotes (default: 5, max 100)', '5')
126
+ .option('--language <lang>', 'Filter by quote language')
127
+ .action(async (options: Record<string, string>) => {
128
+ const client = await getClient()
129
+ const format = getFormat(program)
130
+ const { data, count } = await withSpinner('Enqueueing random quotes', () =>
131
+ client.social.addRandomToQueue({
132
+ platform: options.platform as any,
133
+ count: Number(options.count),
134
+ language: options.language,
135
+ }),
136
+ format,
137
+ )
138
+ console.log(chalk.green(` ${count ?? data?.length ?? 0} quote(s) queued`))
139
+ output(data, format)
140
+ })
141
+
142
+ queue
143
+ .command('remove <id>')
144
+ .description('Remove a queue item')
145
+ .option('--yes', 'Skip confirmation')
146
+ .action(async (id: string, options: Record<string, unknown>) => {
147
+ if (!options.yes) {
148
+ const confirmed = await confirm({ message: `Remove queue item #${id}?` })
149
+ if (isCancel(confirmed) || !confirmed) cancel('Cancelled')
150
+ }
151
+
152
+ const client = await getClient()
153
+ await withSpinner('Removing queue item', () => client.social.removeQueueItem(Number(id)))
154
+ console.log(chalk.green(` Queue item #${id} removed`))
155
+ })
156
+
157
+ queue
158
+ .command('requeue <id>')
159
+ .description('Reset a failed queue item back to queued')
160
+ .action(async (id: string) => {
161
+ const client = await getClient()
162
+ const format = getFormat(program)
163
+ const { data } = await withSpinner('Requeueing item', () => client.social.requeueQueueItem(Number(id)), format)
164
+ console.log(chalk.green(` Queue item #${id} requeued`))
165
+ output(data, format)
166
+ })
167
+
168
+ queue
169
+ .command('reorder <id>')
170
+ .description('Reorder a queued item (relative to another item or up/down)')
171
+ .option('--direction <dir>', 'Move direction (up|down)')
172
+ .option('--before <id>', 'Place before this queue item ID (or "end" to move to the end)')
173
+ .action(async (id: string, options: Record<string, string>) => {
174
+ const client = await getClient()
175
+ const format = getFormat(program)
176
+
177
+ let beforeId: number | null | undefined
178
+ if (options.before !== undefined) {
179
+ beforeId = options.before === 'end' ? null : Number(options.before)
180
+ }
181
+
182
+ const direction = (options.direction ?? '') as 'up' | 'down' | ''
183
+ if (beforeId === undefined && direction !== 'up' && direction !== 'down') {
184
+ console.log(chalk.red(' Provide --direction up|down or --before <id>'))
185
+ process.exit(1)
186
+ }
187
+
188
+ const { data } = await withSpinner('Reordering queue item', () =>
189
+ client.social.reorderQueueItem({
190
+ id: Number(id),
191
+ direction: direction || undefined,
192
+ before_id: beforeId,
193
+ }),
194
+ format,
195
+ )
196
+ if (!data?.moved) {
197
+ console.log(chalk.dim(' Item did not move'))
198
+ return
199
+ }
200
+ console.log(chalk.green(` Queue item #${id} moved to position ${data.position}`))
201
+ })
202
+
203
+ queue
204
+ .command('clear')
205
+ .description('Clear the queue for a platform')
206
+ .option('--platform <platform>', 'Platform', 'x')
207
+ .option('--scope <scope>', 'Scope: all or finished (posted/failed)', 'all')
208
+ .option('--yes', 'Skip confirmation')
209
+ .action(async (options: Record<string, string>) => {
210
+ const platform = options.platform ?? await select({
211
+ message: 'Platform',
212
+ options: PLATFORM_CHOICES.map((p) => ({ value: p, label: p })),
213
+ })
214
+ if (isCancel(platform)) cancel('Cancelled')
215
+
216
+ const scope = (options.scope ?? 'all') as 'all' | 'finished'
217
+ const label = scope === 'finished' ? 'finished items (posted/failed)' : 'entire queue'
218
+
219
+ if (!options.yes) {
220
+ const confirmed = await confirm({ message: `Clear the ${label} for ${String(platform)}? This cannot be undone.` })
221
+ if (isCancel(confirmed) || !confirmed) cancel('Cancelled')
222
+ }
223
+
224
+ const client = await getClient()
225
+ const format = getFormat(program)
226
+ const { data } = await withSpinner('Clearing queue', () =>
227
+ client.social.clearQueue({ platform: platform as string as any, confirm: true, scope }),
228
+ format,
229
+ )
230
+ console.log(chalk.green(` ${data?.deletedCount ?? 0} item(s) cleared from ${data?.platform ?? String(platform)}`))
231
+ output(data, format)
232
+ })
233
+
234
+ queue
235
+ .command('run-now')
236
+ .description('Immediately run the social autopost (throttled to 1 per minute)')
237
+ .option('--platform <platform>', 'Target platform (all enabled if omitted)')
238
+ .action(async (options: Record<string, string>) => {
239
+ const client = await getClient()
240
+ const format = getFormat(program)
241
+ const { data } = await withSpinner('Running autopost', () =>
242
+ client.social.runNow({ platform: options.platform as any }),
243
+ format,
244
+ )
245
+ output(data, format)
246
+ })
247
+ }
@@ -137,7 +137,7 @@ export function output(data: unknown, format: Format): void {
137
137
  }
138
138
 
139
139
  const allKeys = Object.keys(data[0] as Record<string, unknown>)
140
- const keyOrder = ['id', 'content', 'name', 'language', 'type', 'author', 'reference', 'quotes_count']
140
+ const keyOrder = ['id', 'content', 'name', 'language', 'type', 'author', 'reference', 'quotes_count', 'platform', 'label', 'enabled', 'status', 'position', 'quote_text', 'quote_language', 'author_name', 'reference_name', 'post_text', 'post_url', 'posted_at', 'error_message']
141
141
  const columns = keyOrder
142
142
  .filter((k) => allKeys.includes(k))
143
143
  .map((k) => ({ key: k, label: k }))