@verbatims/cli 0.1.13 → 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 +32 -0
- package/package.json +1 -1
- package/src/commands/index.ts +2 -0
- package/src/commands/social.ts +247 -0
- package/src/utils/format.ts +1 -1
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
package/src/commands/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/utils/format.ts
CHANGED
|
@@ -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 }))
|