@verbatims/cli 1.0.2 → 1.0.4
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 +15 -0
- package/package.json +5 -2
- package/src/commands/collections.ts +79 -1
- package/src/commands/quotes.ts +3 -2
- package/src/commands/themes.ts +40 -0
package/README.md
CHANGED
|
@@ -42,6 +42,19 @@ verbatims quotes browse
|
|
|
42
42
|
# Search
|
|
43
43
|
verbatims search "love" --type quotes
|
|
44
44
|
|
|
45
|
+
# Manage collections
|
|
46
|
+
verbatims collections list --limit 20
|
|
47
|
+
verbatims collections get 12 --page 1 --limit 20
|
|
48
|
+
verbatims collections create --name "Favorite quotes" --public
|
|
49
|
+
verbatims collections update 12 --name "Reading notes" --private
|
|
50
|
+
verbatims collections add 12 123
|
|
51
|
+
verbatims collections remove 12 123
|
|
52
|
+
verbatims collections delete 12 --yes
|
|
53
|
+
|
|
54
|
+
# Apply a reproducible theme manifest (safe to preview first)
|
|
55
|
+
verbatims themes apply ./theme.json --dry-run
|
|
56
|
+
verbatims themes apply ./theme.json
|
|
57
|
+
|
|
45
58
|
# List social platforms with queue stats
|
|
46
59
|
verbatims social platforms
|
|
47
60
|
|
|
@@ -58,6 +71,8 @@ verbatims social queue run-now --platform bluesky
|
|
|
58
71
|
verbatims --help
|
|
59
72
|
```
|
|
60
73
|
|
|
74
|
+
Theme manifests use `schema_version: 1`, a stable `slug`, a `name`, and an explicit `filters` array. Optional translations (including homepage copy), language, priority, description, and config are validated before a write. Applying the same manifest reconciles filters by type/value, retaining IDs for unchanged filters; translations reconcile by language, and omitted homepage copy is preserved. Activation and default status are left unchanged. Keep manifests in version control to review theme changes before applying them. See [`examples/themes/quiet-courage.json`](examples/themes/quiet-courage.json).
|
|
75
|
+
|
|
61
76
|
### Social commands
|
|
62
77
|
|
|
63
78
|
```bash
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@verbatims/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "CLI for the Verbatims quotes API",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": "^22 || ^24"
|
|
8
|
+
},
|
|
6
9
|
"bin": {
|
|
7
10
|
"verbatims": "./bin/verbatims.js"
|
|
8
11
|
},
|
|
@@ -16,7 +19,7 @@
|
|
|
16
19
|
"typecheck": "tsc --noEmit"
|
|
17
20
|
},
|
|
18
21
|
"dependencies": {
|
|
19
|
-
"@verbatims/sdk": "^2.0.
|
|
22
|
+
"@verbatims/sdk": "^2.0.3",
|
|
20
23
|
"@clack/prompts": "^0.10.1",
|
|
21
24
|
"chalk": "^5.4.1",
|
|
22
25
|
"commander": "^13.1.0",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Command } from 'commander'
|
|
2
|
-
import { text, isCancel, cancel } from '@clack/prompts'
|
|
2
|
+
import { text, isCancel, cancel, confirm } from '@clack/prompts'
|
|
3
3
|
import chalk from 'chalk'
|
|
4
4
|
import { getClient } from '../utils/client.js'
|
|
5
5
|
import { output, type Format } from '../utils/format.js'
|
|
@@ -12,6 +12,39 @@ function getFormat(program: Command): Format {
|
|
|
12
12
|
export function registerCollectionsCommand(program: Command) {
|
|
13
13
|
const collections = program.command('collections').description('Manage collections')
|
|
14
14
|
|
|
15
|
+
collections
|
|
16
|
+
.command('list')
|
|
17
|
+
.description('List collections')
|
|
18
|
+
.option('--page <n>', 'Page number', '1')
|
|
19
|
+
.option('--limit <n>', 'Number of results', '20')
|
|
20
|
+
.action(async (options: Record<string, string>) => {
|
|
21
|
+
const client = await getClient()
|
|
22
|
+
const format = getFormat(program)
|
|
23
|
+
const { data, pagination } = await withSpinner('Fetching collections', () =>
|
|
24
|
+
client.collections.list({ page: Number(options.page), limit: Number(options.limit) }),
|
|
25
|
+
format,
|
|
26
|
+
)
|
|
27
|
+
output(data, format)
|
|
28
|
+
if (pagination && format !== 'json') {
|
|
29
|
+
console.log(chalk.dim(`\nPage ${pagination.page}/${pagination.totalPages} — ${pagination.total} total`))
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
collections
|
|
34
|
+
.command('get <id>')
|
|
35
|
+
.description('Get a collection and its quotes')
|
|
36
|
+
.option('--page <n>', 'Quote page number', '1')
|
|
37
|
+
.option('--limit <n>', 'Quotes per page', '20')
|
|
38
|
+
.action(async (id: string, options: Record<string, string>) => {
|
|
39
|
+
const client = await getClient()
|
|
40
|
+
const format = getFormat(program)
|
|
41
|
+
const { data } = await withSpinner('Fetching collection', () =>
|
|
42
|
+
client.collections.get(Number(id), { page: Number(options.page), limit: Number(options.limit) }),
|
|
43
|
+
format,
|
|
44
|
+
)
|
|
45
|
+
output(data, format)
|
|
46
|
+
})
|
|
47
|
+
|
|
15
48
|
collections
|
|
16
49
|
.command('create')
|
|
17
50
|
.description('Create a collection')
|
|
@@ -33,6 +66,51 @@ export function registerCollectionsCommand(program: Command) {
|
|
|
33
66
|
output(data, format)
|
|
34
67
|
})
|
|
35
68
|
|
|
69
|
+
collections
|
|
70
|
+
.command('update <id>')
|
|
71
|
+
.description('Update a collection')
|
|
72
|
+
.option('--name <name>', 'Collection name')
|
|
73
|
+
.option('--description <text>', 'Description (use an empty value to clear)')
|
|
74
|
+
.option('--public', 'Make collection public')
|
|
75
|
+
.option('--private', 'Make collection private')
|
|
76
|
+
.action(async (id: string, options: Record<string, string | boolean>) => {
|
|
77
|
+
if (options.public && options.private) {
|
|
78
|
+
throw new Error('Choose either --public or --private')
|
|
79
|
+
}
|
|
80
|
+
const data: { name?: string; description?: string | null; is_public?: boolean } = {}
|
|
81
|
+
if (options.name) data.name = options.name as string
|
|
82
|
+
if (options.description !== undefined) data.description = options.description ? options.description as string : null
|
|
83
|
+
if (options.public) data.is_public = true
|
|
84
|
+
if (options.private) data.is_public = false
|
|
85
|
+
if (Object.keys(data).length === 0) {
|
|
86
|
+
throw new Error('Provide at least one field to update')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const client = await getClient()
|
|
90
|
+
const format = getFormat(program)
|
|
91
|
+
const { data: updated } = await withSpinner('Updating collection', () =>
|
|
92
|
+
client.collections.update(Number(id), data),
|
|
93
|
+
format,
|
|
94
|
+
)
|
|
95
|
+
console.log(chalk.green(' Collection updated'))
|
|
96
|
+
output(updated, format)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
collections
|
|
100
|
+
.command('delete <id>')
|
|
101
|
+
.description('Delete a collection')
|
|
102
|
+
.option('--yes', 'Skip confirmation')
|
|
103
|
+
.action(async (id: string, options: { yes?: boolean }) => {
|
|
104
|
+
if (!options.yes) {
|
|
105
|
+
const confirmed = await confirm({ message: `Delete collection #${id}?` })
|
|
106
|
+
if (isCancel(confirmed) || !confirmed) cancel('Cancelled')
|
|
107
|
+
}
|
|
108
|
+
const client = await getClient()
|
|
109
|
+
const format = getFormat(program)
|
|
110
|
+
await withSpinner('Deleting collection', () => client.collections.delete(Number(id)), format)
|
|
111
|
+
console.log(chalk.green(` Collection #${id} deleted`))
|
|
112
|
+
})
|
|
113
|
+
|
|
36
114
|
collections
|
|
37
115
|
.command('add <collection-id> <quote-id>')
|
|
38
116
|
.description('Add a quote to a collection')
|
package/src/commands/quotes.ts
CHANGED
|
@@ -95,12 +95,13 @@ export function registerQuotesCommand(program: Command) {
|
|
|
95
95
|
}),
|
|
96
96
|
format,
|
|
97
97
|
)
|
|
98
|
-
|
|
98
|
+
const sourceType = options.sourceType
|
|
99
|
+
if (sourceType && data) {
|
|
99
100
|
const primary = data.attributions?.find(attribution => attribution.is_primary)
|
|
100
101
|
if (!primary) throw new Error('The created quote has no primary attribution')
|
|
101
102
|
await withSpinner('Creating source', () => client.quotes.createSource(data.id, {
|
|
102
103
|
attribution_id: primary.id,
|
|
103
|
-
source_type:
|
|
104
|
+
source_type: sourceType,
|
|
104
105
|
source_url: options.sourceUrl,
|
|
105
106
|
is_primary: true,
|
|
106
107
|
}), format)
|
package/src/commands/themes.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Command } from 'commander'
|
|
2
|
+
import { readFile } from 'node:fs/promises'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
2
4
|
import { text, isCancel, cancel, confirm } from '@clack/prompts'
|
|
3
5
|
import chalk from 'chalk'
|
|
4
6
|
import { getClient } from '../utils/client.js'
|
|
@@ -47,6 +49,44 @@ export function registerThemesCommand(program: Command) {
|
|
|
47
49
|
output(data, format)
|
|
48
50
|
})
|
|
49
51
|
|
|
52
|
+
themes
|
|
53
|
+
.command('apply <file>')
|
|
54
|
+
.description('Create or reconcile a theme from a versioned JSON manifest')
|
|
55
|
+
.option('--dry-run', 'Validate and show the planned operation without changing data')
|
|
56
|
+
.action(async (file: string, options: { dryRun?: boolean }) => {
|
|
57
|
+
const sdk = await import('@verbatims/sdk')
|
|
58
|
+
if (!('parseThemeManifest' in sdk) || typeof sdk.parseThemeManifest !== 'function') {
|
|
59
|
+
throw new Error('Theme manifests require a newer @verbatims/sdk. Update the SDK and try again.')
|
|
60
|
+
}
|
|
61
|
+
const parseThemeManifest = sdk.parseThemeManifest
|
|
62
|
+
const manifest = parseThemeManifest(JSON.parse(await readFile(resolve(file), 'utf8')))
|
|
63
|
+
const client = await getClient()
|
|
64
|
+
if (typeof client.themes.apply !== 'function') {
|
|
65
|
+
throw new Error('Theme manifests require a newer @verbatims/sdk. Update the SDK and try again.')
|
|
66
|
+
}
|
|
67
|
+
const format = getFormat(program)
|
|
68
|
+
|
|
69
|
+
if (options.dryRun) {
|
|
70
|
+
const { data } = await withSpinner('Checking existing themes', () =>
|
|
71
|
+
client.themes.list({ search: manifest.slug, limit: 100 }), format)
|
|
72
|
+
const existing = data?.find(theme => theme.slug === manifest.slug)
|
|
73
|
+
output({
|
|
74
|
+
operation: existing ? 'update' : 'create',
|
|
75
|
+
slug: manifest.slug,
|
|
76
|
+
name: manifest.name,
|
|
77
|
+
filters: manifest.filters?.length ?? 0,
|
|
78
|
+
translations: manifest.translations?.length ?? 'unchanged',
|
|
79
|
+
activation: 'unchanged',
|
|
80
|
+
defaultStatus: 'unchanged',
|
|
81
|
+
}, format)
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const result = await withSpinner('Applying theme manifest', () => client.themes.apply(manifest), format)
|
|
86
|
+
console.log(chalk.green(` Theme ${result.operation}`))
|
|
87
|
+
output(result.data, format)
|
|
88
|
+
})
|
|
89
|
+
|
|
50
90
|
themes
|
|
51
91
|
.command('create')
|
|
52
92
|
.description('Create a theme')
|