@verbatims/cli 0.1.15 → 0.1.17
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 +13 -0
- package/package.json +1 -1
- package/src/commands/quotes.ts +143 -3
- package/src/utils/browse.ts +1 -1
- package/src/utils/format.ts +1 -1
package/README.md
CHANGED
|
@@ -19,6 +19,19 @@ verbatims config
|
|
|
19
19
|
# List quotes
|
|
20
20
|
verbatims quotes list --language fr --limit 10
|
|
21
21
|
|
|
22
|
+
# Filter quotes by reference and inspect provenance
|
|
23
|
+
verbatims quotes list --reference 42
|
|
24
|
+
verbatims quotes get 123 --format json
|
|
25
|
+
|
|
26
|
+
# Manage provenance (moderator/admin API key)
|
|
27
|
+
verbatims quotes attributions list 123
|
|
28
|
+
verbatims quotes attributions create 123 --author-id 1 --reference-id 2 --status manually_verified --primary
|
|
29
|
+
verbatims quotes sources list 123
|
|
30
|
+
verbatims quotes sources create 123 --source-type book --source-url https://example.com/edition --status externally_verified
|
|
31
|
+
|
|
32
|
+
# Create a quote with a source
|
|
33
|
+
verbatims quotes create --source-type book --source-url https://example.com/edition
|
|
34
|
+
|
|
22
35
|
# Browse interactively
|
|
23
36
|
verbatims quotes browse
|
|
24
37
|
|
package/package.json
CHANGED
package/src/commands/quotes.ts
CHANGED
|
@@ -19,6 +19,7 @@ export function registerQuotesCommand(program: Command) {
|
|
|
19
19
|
.option('--language <lang>', 'Filter by language')
|
|
20
20
|
.option('--limit <n>', 'Number of results', '20')
|
|
21
21
|
.option('--author <id>', 'Filter by author ID')
|
|
22
|
+
.option('--reference <id>', 'Filter by reference ID')
|
|
22
23
|
.option('--tag <name>', 'Filter by tag')
|
|
23
24
|
.option('--search <q>', 'Search in quote text')
|
|
24
25
|
.option('--status <status>', 'Filter by status (draft|pending|approved|rejected)')
|
|
@@ -33,6 +34,7 @@ export function registerQuotesCommand(program: Command) {
|
|
|
33
34
|
language: options.language,
|
|
34
35
|
limit: Number(options.limit),
|
|
35
36
|
author_id: options.author ? Number(options.author) : undefined,
|
|
37
|
+
reference_id: options.reference ? Number(options.reference) : undefined,
|
|
36
38
|
tag: options.tag,
|
|
37
39
|
search: options.search,
|
|
38
40
|
q: options.search,
|
|
@@ -66,6 +68,8 @@ export function registerQuotesCommand(program: Command) {
|
|
|
66
68
|
.option('--language <lang>', 'Language')
|
|
67
69
|
.option('--author-id <id>', 'Author ID')
|
|
68
70
|
.option('--reference-id <id>', 'Reference ID')
|
|
71
|
+
.option('--source-type <type>', 'Source type')
|
|
72
|
+
.option('--source-url <url>', 'Source URL')
|
|
69
73
|
.action(async (options: Record<string, string>) => {
|
|
70
74
|
const client = await getClient()
|
|
71
75
|
const format = getFormat(program)
|
|
@@ -80,7 +84,16 @@ export function registerQuotesCommand(program: Command) {
|
|
|
80
84
|
const referenceId = options.referenceId ? Number(options.referenceId) : undefined
|
|
81
85
|
|
|
82
86
|
const { data } = await withSpinner('Creating quote', () =>
|
|
83
|
-
client.quotes.create({
|
|
87
|
+
client.quotes.create({
|
|
88
|
+
content: name as string,
|
|
89
|
+
name: name as string,
|
|
90
|
+
language: language as string,
|
|
91
|
+
provenance: {
|
|
92
|
+
author_id: authorId,
|
|
93
|
+
reference_id: referenceId,
|
|
94
|
+
source: options.sourceType ? { source_type: options.sourceType, source_url: options.sourceUrl } : undefined,
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
84
97
|
format,
|
|
85
98
|
)
|
|
86
99
|
console.log(chalk.green(' Quote created'))
|
|
@@ -94,6 +107,8 @@ export function registerQuotesCommand(program: Command) {
|
|
|
94
107
|
.option('--language <lang>', 'Language')
|
|
95
108
|
.option('--author-id <id>', 'Author ID')
|
|
96
109
|
.option('--reference-id <id>', 'Reference ID')
|
|
110
|
+
.option('--source-type <type>', 'Source type')
|
|
111
|
+
.option('--source-url <url>', 'Source URL')
|
|
97
112
|
.action(async (id: string, options: Record<string, string>) => {
|
|
98
113
|
const client = await getClient()
|
|
99
114
|
const format = getFormat(program)
|
|
@@ -101,8 +116,14 @@ export function registerQuotesCommand(program: Command) {
|
|
|
101
116
|
const data_: Record<string, unknown> = {}
|
|
102
117
|
if (options.name) { data_.name = options.name; data_.content = options.name }
|
|
103
118
|
if (options.language) data_.language = options.language
|
|
104
|
-
if (options.authorId !== undefined) data_.
|
|
105
|
-
if (options.referenceId !== undefined) data_.
|
|
119
|
+
if (options.authorId !== undefined) data_.provenance = { ...(data_.provenance as object), author_id: options.authorId === 'null' ? null : Number(options.authorId) }
|
|
120
|
+
if (options.referenceId !== undefined) data_.provenance = { ...(data_.provenance as object), reference_id: options.referenceId === 'null' ? null : Number(options.referenceId) }
|
|
121
|
+
if (options.sourceType !== undefined || options.sourceUrl !== undefined) {
|
|
122
|
+
data_.provenance = {
|
|
123
|
+
...(data_.provenance as object),
|
|
124
|
+
source: { source_type: options.sourceType ?? 'manual', source_url: options.sourceUrl },
|
|
125
|
+
}
|
|
126
|
+
}
|
|
106
127
|
|
|
107
128
|
const { data } = await withSpinner('Updating quote', () => client.quotes.update(Number(id), data_ as any), format)
|
|
108
129
|
console.log(chalk.green(' Quote updated'))
|
|
@@ -172,6 +193,7 @@ export function registerQuotesCommand(program: Command) {
|
|
|
172
193
|
.description('Browse quotes interactively')
|
|
173
194
|
.option('--language <lang>', 'Filter by language')
|
|
174
195
|
.option('--author <id>', 'Filter by author ID')
|
|
196
|
+
.option('--reference <id>', 'Filter by reference ID')
|
|
175
197
|
.option('--tag <name>', 'Filter by tag')
|
|
176
198
|
.option('--search <q>', 'Search in quote text')
|
|
177
199
|
.option('--status <status>', 'Filter by status (draft|pending|approved|rejected)')
|
|
@@ -185,6 +207,7 @@ export function registerQuotesCommand(program: Command) {
|
|
|
185
207
|
page,
|
|
186
208
|
language: options.language,
|
|
187
209
|
author_id: options.author ? Number(options.author) : undefined,
|
|
210
|
+
reference_id: options.reference ? Number(options.reference) : undefined,
|
|
188
211
|
tag: options.tag,
|
|
189
212
|
search: options.search,
|
|
190
213
|
q: options.search,
|
|
@@ -196,6 +219,123 @@ export function registerQuotesCommand(program: Command) {
|
|
|
196
219
|
)
|
|
197
220
|
})
|
|
198
221
|
|
|
222
|
+
quotes
|
|
223
|
+
.command('attributions <action> <quoteId> [attributionId]')
|
|
224
|
+
.description('Manage quote attributions (moderator/admin API key required)')
|
|
225
|
+
.option('--author-id <id>', 'Author ID')
|
|
226
|
+
.option('--reference-id <id>', 'Reference ID')
|
|
227
|
+
.option('--status <status>', 'Verification status')
|
|
228
|
+
.option('--notes <text>', 'Internal notes')
|
|
229
|
+
.option('--primary', 'Set as primary attribution')
|
|
230
|
+
.option('--yes', 'Skip confirmation')
|
|
231
|
+
.action(async (action: string, quoteId: string, attributionId: string | undefined, options: Record<string, any>) => {
|
|
232
|
+
const client = await getClient()
|
|
233
|
+
const format = getFormat(program)
|
|
234
|
+
const id = Number(quoteId)
|
|
235
|
+
|
|
236
|
+
if (action === 'list') {
|
|
237
|
+
const { data } = await withSpinner('Fetching attributions', () => client.quotes.listAttributions(id), format)
|
|
238
|
+
output(data, format)
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
if (action === 'create') {
|
|
242
|
+
const { data } = await withSpinner('Creating attribution', () => client.quotes.createAttribution(id, {
|
|
243
|
+
author_id: options.authorId ? Number(options.authorId) : null,
|
|
244
|
+
reference_id: options.referenceId ? Number(options.referenceId) : null,
|
|
245
|
+
status: options.status,
|
|
246
|
+
notes: options.notes,
|
|
247
|
+
is_primary: options.primary,
|
|
248
|
+
}), format)
|
|
249
|
+
output(data, format)
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
if (!attributionId) throw new Error('Attribution ID is required')
|
|
253
|
+
const attribution = Number(attributionId)
|
|
254
|
+
if (action === 'update') {
|
|
255
|
+
const data_: Record<string, unknown> = {}
|
|
256
|
+
if (options.authorId !== undefined) data_.author_id = Number(options.authorId)
|
|
257
|
+
if (options.referenceId !== undefined) data_.reference_id = Number(options.referenceId)
|
|
258
|
+
if (options.status !== undefined) data_.status = options.status
|
|
259
|
+
if (options.notes !== undefined) data_.notes = options.notes
|
|
260
|
+
if (options.primary !== undefined) data_.is_primary = options.primary
|
|
261
|
+
const { data } = await withSpinner('Updating attribution', () => client.quotes.updateAttribution(id, attribution, data_), format)
|
|
262
|
+
output(data, format)
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
if (action === 'delete') {
|
|
266
|
+
if (!options.yes) {
|
|
267
|
+
const confirmed = await confirm({ message: `Delete attribution #${attribution} from quote #${id}?` })
|
|
268
|
+
if (isCancel(confirmed) || !confirmed) cancel('Cancelled')
|
|
269
|
+
}
|
|
270
|
+
await withSpinner('Deleting attribution', () => client.quotes.deleteAttribution(id, attribution), format)
|
|
271
|
+
console.log(chalk.green(' Attribution deleted'))
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
throw new Error('Action must be list, create, update, or delete')
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
quotes
|
|
278
|
+
.command('sources <action> <quoteId> [sourceId]')
|
|
279
|
+
.description('Manage quote sources (moderator/admin API key required)')
|
|
280
|
+
.option('--attribution-id <id>', 'Attribution ID')
|
|
281
|
+
.option('--source-type <type>', 'Source type')
|
|
282
|
+
.option('--source-url <url>', 'Source URL')
|
|
283
|
+
.option('--label <label>', 'Source label')
|
|
284
|
+
.option('--status <status>', 'Verification status')
|
|
285
|
+
.option('--notes <text>', 'Internal notes')
|
|
286
|
+
.option('--primary', 'Set as primary source')
|
|
287
|
+
.option('--yes', 'Skip confirmation')
|
|
288
|
+
.action(async (action: string, quoteId: string, sourceId: string | undefined, options: Record<string, any>) => {
|
|
289
|
+
const client = await getClient()
|
|
290
|
+
const format = getFormat(program)
|
|
291
|
+
const id = Number(quoteId)
|
|
292
|
+
|
|
293
|
+
if (action === 'list') {
|
|
294
|
+
const { data } = await withSpinner('Fetching sources', () => client.quotes.listSources(id), format)
|
|
295
|
+
output(data, format)
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
if (action === 'create') {
|
|
299
|
+
if (!options.sourceType) throw new Error('--source-type is required')
|
|
300
|
+
const { data } = await withSpinner('Creating source', () => client.quotes.createSource(id, {
|
|
301
|
+
attribution_id: options.attributionId ? Number(options.attributionId) : null,
|
|
302
|
+
source_type: options.sourceType,
|
|
303
|
+
source_url: options.sourceUrl,
|
|
304
|
+
label: options.label,
|
|
305
|
+
verification_status: options.status,
|
|
306
|
+
notes: options.notes,
|
|
307
|
+
is_primary: options.primary,
|
|
308
|
+
}), format)
|
|
309
|
+
output(data, format)
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
if (!sourceId) throw new Error('Source ID is required')
|
|
313
|
+
const source = Number(sourceId)
|
|
314
|
+
if (action === 'update') {
|
|
315
|
+
const data_: Record<string, unknown> = {}
|
|
316
|
+
if (options.attributionId !== undefined) data_.attribution_id = Number(options.attributionId)
|
|
317
|
+
if (options.sourceType !== undefined) data_.source_type = options.sourceType
|
|
318
|
+
if (options.sourceUrl !== undefined) data_.source_url = options.sourceUrl
|
|
319
|
+
if (options.label !== undefined) data_.label = options.label
|
|
320
|
+
if (options.status !== undefined) data_.verification_status = options.status
|
|
321
|
+
if (options.notes !== undefined) data_.notes = options.notes
|
|
322
|
+
if (options.primary !== undefined) data_.is_primary = options.primary
|
|
323
|
+
const { data } = await withSpinner('Updating source', () => client.quotes.updateSource(id, source, data_), format)
|
|
324
|
+
output(data, format)
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
if (action === 'delete') {
|
|
328
|
+
if (!options.yes) {
|
|
329
|
+
const confirmed = await confirm({ message: `Delete source #${source} from quote #${id}?` })
|
|
330
|
+
if (isCancel(confirmed) || !confirmed) cancel('Cancelled')
|
|
331
|
+
}
|
|
332
|
+
await withSpinner('Deleting source', () => client.quotes.deleteSource(id, source), format)
|
|
333
|
+
console.log(chalk.green(' Source deleted'))
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
throw new Error('Action must be list, create, update, or delete')
|
|
337
|
+
})
|
|
338
|
+
|
|
199
339
|
const tags = quotes.command('tags').description('Manage quote tags')
|
|
200
340
|
|
|
201
341
|
tags
|
package/src/utils/browse.ts
CHANGED
|
@@ -32,7 +32,7 @@ export async function browse(fetchPage: PageFetcher, format: Format): Promise<vo
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const allKeys = Object.keys(items[0] as Record<string, unknown>)
|
|
35
|
-
const keyOrder = ['id', 'content', 'name', 'language', 'type', 'author', 'reference', 'quotes_count']
|
|
35
|
+
const keyOrder = ['id', 'content', 'name', 'language', 'type', 'author', 'reference', 'source', 'attributions', 'sources', 'quotes_count']
|
|
36
36
|
const columns = keyOrder
|
|
37
37
|
.filter((k) => allKeys.includes(k))
|
|
38
38
|
.map((k) => ({ key: k, label: k }))
|
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', 'platform', 'label', 'enabled', 'status', 'position', 'quote_text', 'quote_language', 'author_name', 'reference_name', 'post_text', 'post_url', 'posted_at', 'error_message']
|
|
140
|
+
const keyOrder = ['id', 'content', 'name', 'language', 'type', 'author', 'reference', 'source', 'attributions', 'sources', '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 }))
|