@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 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.2",
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.2",
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')
@@ -95,12 +95,13 @@ export function registerQuotesCommand(program: Command) {
95
95
  }),
96
96
  format,
97
97
  )
98
- if (options.sourceType && data) {
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: options.sourceType,
104
+ source_type: sourceType,
104
105
  source_url: options.sourceUrl,
105
106
  is_primary: true,
106
107
  }), format)
@@ -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')