@verbatims/cli 0.1.2 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verbatims/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "CLI for the Verbatims quotes API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,8 +13,7 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "dev": "tsx src/index.ts",
16
- "typecheck": "tsc --noEmit",
17
- "prepublishOnly": "tsc --noEmit"
16
+ "typecheck": "tsc --noEmit"
18
17
  },
19
18
  "dependencies": {
20
19
  "@verbatims/sdk": "^1.0.0",
@@ -6,6 +6,7 @@ import { registerTagsCommand } from './tags.js'
6
6
  import { registerCollectionsCommand } from './collections.js'
7
7
  import { registerSearchCommand } from './search.js'
8
8
  import { registerConfigCommand } from './config.js'
9
+ import { registerThemesCommand } from './themes.js'
9
10
 
10
11
  export function registerCommands(program: Command) {
11
12
  registerQuotesCommand(program)
@@ -15,4 +16,5 @@ export function registerCommands(program: Command) {
15
16
  registerCollectionsCommand(program)
16
17
  registerSearchCommand(program)
17
18
  registerConfigCommand(program)
19
+ registerThemesCommand(program)
18
20
  }
@@ -0,0 +1,292 @@
1
+ import type { Command } from 'commander'
2
+ import { text, isCancel, cancel, confirm } 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
+ export function registerThemesCommand(program: Command) {
13
+ const themes = program.command('themes').description('Manage themes')
14
+
15
+ themes
16
+ .command('list')
17
+ .description('List themes')
18
+ .option('--search <q>', 'Search themes')
19
+ .option('--sort-by <field>', 'Sort field (priority|name|slug)')
20
+ .option('--sort-order <order>', 'Sort order (asc|desc)')
21
+ .option('--limit <n>', 'Number of results', '20')
22
+ .action(async (options: Record<string, string>) => {
23
+ const client = await getClient()
24
+ const format = getFormat(program)
25
+ const { data, pagination } = await withSpinner('Fetching themes', () =>
26
+ client.themes.list({
27
+ search: options.search,
28
+ sort_by: options.sortBy as 'priority' | 'name' | 'slug' | undefined,
29
+ sort_order: options.sortOrder as 'asc' | 'desc' | undefined,
30
+ limit: Number(options.limit),
31
+ }),
32
+ format,
33
+ )
34
+ output(data, format)
35
+ if (pagination && format !== 'json') {
36
+ console.log(chalk.dim(`\nPage ${pagination.page}/${pagination.totalPages} — ${pagination.total} total`))
37
+ }
38
+ })
39
+
40
+ themes
41
+ .command('get <id>')
42
+ .description('Get a theme by ID')
43
+ .action(async (id: string) => {
44
+ const client = await getClient()
45
+ const format = getFormat(program)
46
+ const { data } = await withSpinner('Fetching theme', () => client.themes.get(Number(id)), format)
47
+ output(data, format)
48
+ })
49
+
50
+ themes
51
+ .command('create')
52
+ .description('Create a theme')
53
+ .option('--slug <slug>', 'URL-friendly slug')
54
+ .option('--name <name>', 'Theme name')
55
+ .option('--description <text>', 'Description')
56
+ .option('--language <lang>', 'Language')
57
+ .option('--active', 'Set as active')
58
+ .option('--default', 'Set as default')
59
+ .option('--priority <n>', 'Priority', '0')
60
+ .action(async (options: Record<string, string>) => {
61
+ const client = await getClient()
62
+ const format = getFormat(program)
63
+
64
+ const slug = options.slug ?? await text({ message: 'Theme slug', validate: (v) => !v ? 'Required' : undefined })
65
+ if (isCancel(slug)) cancel('Cancelled')
66
+
67
+ const name = options.name ?? await text({ message: 'Theme name', validate: (v) => !v ? 'Required' : undefined })
68
+ if (isCancel(name)) cancel('Cancelled')
69
+
70
+ const description = options.description ?? await text({ message: 'Description (optional)', initialValue: '' })
71
+ if (isCancel(description)) cancel('Cancelled')
72
+
73
+ const { data } = await withSpinner('Creating theme', () =>
74
+ client.themes.create({
75
+ slug: slug as string,
76
+ name: name as string,
77
+ description: (description as string) || null,
78
+ language: options.language || null,
79
+ is_active: options.active !== undefined ? true : undefined,
80
+ is_default: options.default !== undefined ? true : undefined,
81
+ priority: options.priority ? Number(options.priority) : undefined,
82
+ }),
83
+ format,
84
+ )
85
+ console.log(chalk.green(' Theme created'))
86
+ output(data, format)
87
+ })
88
+
89
+ themes
90
+ .command('update <id>')
91
+ .description('Update a theme')
92
+ .option('--slug <slug>', 'URL-friendly slug')
93
+ .option('--name <name>', 'Theme name')
94
+ .option('--description <text>', 'Description')
95
+ .option('--language <lang>', 'Language')
96
+ .option('--priority <n>', 'Priority')
97
+ .action(async (id: string, options: Record<string, string>) => {
98
+ const client = await getClient()
99
+ const format = getFormat(program)
100
+
101
+ const data_: Record<string, unknown> = {}
102
+ if (options.slug) data_.slug = options.slug
103
+ if (options.name) data_.name = options.name
104
+ if (options.description !== undefined) data_.description = options.description || null
105
+ if (options.language !== undefined) data_.language = options.language || null
106
+ if (options.priority !== undefined) data_.priority = Number(options.priority)
107
+
108
+ const { data } = await withSpinner('Updating theme', () => client.themes.update(Number(id), data_ as any), format)
109
+ console.log(chalk.green(' Theme updated'))
110
+ output(data, format)
111
+ })
112
+
113
+ themes
114
+ .command('delete <id>')
115
+ .description('Delete a theme')
116
+ .option('--yes', 'Skip confirmation')
117
+ .action(async (id: string, options: Record<string, unknown>) => {
118
+ if (!options.yes) {
119
+ const confirmed = await confirm({ message: `Delete theme #${id}?` })
120
+ if (isCancel(confirmed) || !confirmed) cancel('Cancelled')
121
+ }
122
+
123
+ const client = await getClient()
124
+ await withSpinner('Deleting theme', () => client.themes.delete(Number(id)))
125
+ console.log(chalk.green(` Theme #${id} deleted`))
126
+ })
127
+
128
+ themes
129
+ .command('activate <id>')
130
+ .description('Activate or deactivate a theme')
131
+ .option('--active', 'Activate')
132
+ .option('--inactive', 'Deactivate')
133
+ .action(async (id: string, options: Record<string, unknown>) => {
134
+ const active = options.active ?? !options.inactive
135
+ const client = await getClient()
136
+ const format = getFormat(program)
137
+ const { data } = await withSpinner('Updating theme', () => client.themes.activate(Number(id), !!active), format)
138
+ console.log(chalk.green(` Theme ${active ? 'activated' : 'deactivated'}`))
139
+ output(data, format)
140
+ })
141
+
142
+ themes
143
+ .command('default <id>')
144
+ .description('Set or unset a theme as default')
145
+ .option('--set', 'Set as default')
146
+ .option('--unset', 'Unset as default')
147
+ .action(async (id: string, options: Record<string, unknown>) => {
148
+ const isDefault = options.set ?? !options.unset
149
+ const client = await getClient()
150
+ const format = getFormat(program)
151
+ const { data } = await withSpinner('Updating theme', () => client.themes.setDefault(Number(id), !!isDefault), format)
152
+ console.log(chalk.green(` Theme ${isDefault ? 'set as default' : 'unset as default'}`))
153
+ output(data, format)
154
+ })
155
+
156
+ themes
157
+ .command('active')
158
+ .description('Get the currently active theme')
159
+ .option('--language <lang>', 'Language')
160
+ .action(async (options: Record<string, string>) => {
161
+ const client = await getClient()
162
+ const format = getFormat(program)
163
+ const { data } = await withSpinner('Fetching active theme', () =>
164
+ client.themes.getActive({ language: options.language }),
165
+ format,
166
+ )
167
+ if (!data) {
168
+ console.log(chalk.dim('No active theme'))
169
+ return
170
+ }
171
+ output(data, format)
172
+ })
173
+
174
+ themes
175
+ .command('feed <slug>')
176
+ .description('Get the curated feed for a theme')
177
+ .option('--language <lang>', 'Language')
178
+ .action(async (slug: string, options: Record<string, string>) => {
179
+ const client = await getClient()
180
+ const format = getFormat(program)
181
+ const { data } = await withSpinner('Fetching theme feed', () =>
182
+ client.themes.getFeed(slug, { language: options.language }),
183
+ format,
184
+ )
185
+ if (!data) {
186
+ console.log(chalk.dim('No feed data'))
187
+ return
188
+ }
189
+ output(data, format)
190
+ })
191
+
192
+ themes
193
+ .command('add-filter <id>')
194
+ .description('Add a content filter to a theme')
195
+ .option('--type <type>', 'Filter type (keyword|tag_name|author_name|reference_name|author_id|reference_id)')
196
+ .option('--value <value>', 'Filter value')
197
+ .option('--match-mode <mode>', 'Match mode (any|all)', 'any')
198
+ .action(async (id: string, options: Record<string, string>) => {
199
+ const client = await getClient()
200
+ const format = getFormat(program)
201
+
202
+ const type = options.type ?? await text({ message: 'Filter type', validate: (v) => !v ? 'Required' : undefined })
203
+ if (isCancel(type)) cancel('Cancelled')
204
+
205
+ const value = options.value ?? await text({ message: 'Filter value', validate: (v) => !v ? 'Required' : undefined })
206
+ if (isCancel(value)) cancel('Cancelled')
207
+
208
+ const { data } = await withSpinner('Adding filter', () =>
209
+ client.themes.addFilter(Number(id), {
210
+ type: type as string,
211
+ value: value as string,
212
+ match_mode: options.matchMode as 'any' | 'all' | undefined,
213
+ }),
214
+ format,
215
+ )
216
+ console.log(chalk.green(' Filter added'))
217
+ output(data, format)
218
+ })
219
+
220
+ themes
221
+ .command('remove-filter <id> <filterId>')
222
+ .description('Remove a filter from a theme')
223
+ .action(async (id: string, filterId: string) => {
224
+ const confirmed = await confirm({ message: `Remove filter #${filterId} from theme #${id}?` })
225
+ if (isCancel(confirmed) || !confirmed) cancel('Cancelled')
226
+
227
+ const client = await getClient()
228
+ await withSpinner('Removing filter', () => client.themes.removeFilter(Number(id), Number(filterId)))
229
+ console.log(chalk.green(` Filter #${filterId} removed`))
230
+ })
231
+
232
+ themes
233
+ .command('suggestions')
234
+ .description('Generate theme suggestions')
235
+ .option('--ai', 'Use AI generation')
236
+ .option('--tags <tags>', 'Seed tags (comma-separated)')
237
+ .option('--language <lang>', 'Language')
238
+ .action(async (options: Record<string, string>) => {
239
+ const client = await getClient()
240
+ const format = getFormat(program)
241
+ const { data } = await withSpinner('Generating suggestions', () =>
242
+ client.themes.suggestions({
243
+ ai: options.ai ? true : undefined,
244
+ tags: options.tags,
245
+ language: options.language,
246
+ }),
247
+ format,
248
+ )
249
+ output(data, format)
250
+ })
251
+
252
+ themes
253
+ .command('filter-suggestions <q>')
254
+ .description('Search filter values')
255
+ .requiredOption('--type <type>', 'Filter type (tag_name|author_name|reference_name|keyword|language)')
256
+ .action(async (q: string, options: Record<string, string>) => {
257
+ const client = await getClient()
258
+ const format = getFormat(program)
259
+ const { data } = await withSpinner('Fetching suggestions', () =>
260
+ client.themes.filterSuggestions({ q, type: options.type! }),
261
+ format,
262
+ )
263
+ output(data, format)
264
+ })
265
+
266
+ themes
267
+ .command('filter-recommendations')
268
+ .description('Get filter recommendations based on existing filters')
269
+ .option('--name <name>', 'Theme name')
270
+ .action(async (options: Record<string, string>) => {
271
+ const client = await getClient()
272
+ const format = getFormat(program)
273
+ const { data } = await withSpinner('Getting recommendations', () =>
274
+ client.themes.filterRecommendations({ name: options.name }),
275
+ format,
276
+ )
277
+ output(data, format)
278
+ })
279
+
280
+ themes
281
+ .command('suggest-name <name>')
282
+ .description('Generate an AI-powered theme name suggestion')
283
+ .action(async (name: string) => {
284
+ const client = await getClient()
285
+ const format = getFormat(program)
286
+ const { data } = await withSpinner('Generating name suggestion', () =>
287
+ client.themes.suggestName(name),
288
+ format,
289
+ )
290
+ output(data, format)
291
+ })
292
+ }