@drawcall/market 0.8.25 → 0.8.26

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/src/cli.ts CHANGED
@@ -1,443 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { createRequire } from 'node:module'
4
- import { Command, Option } from 'commander'
5
- import { ASSET_TYPES, type AssetAccess, type AssetType } from './schemas.js'
6
- import { installCommand } from './commands/install.js'
7
- import { searchCommand } from './commands/search.js'
8
- import { agentCommand } from './commands/agent.js'
9
- import { generateCommand } from './commands/generate.js'
10
- import { generateInstallCommand } from './commands/generate-install.js'
11
- import { listCommand } from './commands/list.js'
12
- import { packCommand } from './commands/pack.js'
13
- import { syncCommand } from './commands/sync.js'
14
- import { previewCommand } from './commands/preview.js'
15
- import { urlsCommand } from './commands/urls.js'
16
- import { uploadCommand } from './commands/upload.js'
17
- import { typesCommand } from './commands/types.js'
18
- import { logout } from './commands/logout.js'
19
- import { v1 } from './index.js'
20
- import { saveConfig, getConfigPath } from './config.js'
21
- import { errorResult, loginResult } from './output.js'
22
- import { marketSkill } from './skill.js'
3
+ import { createMarketCommand } from './command.js'
4
+ import { errorResult } from './output.js'
23
5
 
24
- const packageVersion = readPackageVersion()
25
- const program = new Command()
26
-
27
- const DEFAULT_BASE_URL = 'https://market.drawcall.ai'
28
- const AUTH_ISSUER_URL = 'https://auth.drawcall.ai/api/auth'
29
- const DEVICE_CLIENT_ID = 'market-cli'
30
-
31
- /** Accumulate a repeatable option into an array. */
32
- const collect = (value: string, previous: string[]): string[] => previous.concat(value)
33
-
34
- const typeOption = new Option('--type <type>', 'Asset type').choices([...ASSET_TYPES])
35
- const referenceImageOption = new Option(
36
- '--reference-image <file-or-url>',
37
- 'Reference image the result should match: file path or http(s) URL (repeatable)',
38
- )
39
- .argParser(collect)
40
- .default([])
41
- // Omitted → the server picks the default from your entitlement (private if you hold market:private,
42
- // else public). `--access private` requires that entitlement.
43
- const accessOption = new Option('--access <access>', 'Asset visibility').choices([
44
- 'public',
45
- 'private',
46
- ])
47
- const apiOption = new Option('--api <url>', 'API URL').default(
48
- process.env.MARKET_API_URL,
49
- 'from MARKET_API_URL / config / default',
50
- )
51
-
52
- program
53
- .name('market')
54
- .description('Find and use Drawcall Market assets')
55
- .version(packageVersion)
56
- .enablePositionalOptions()
57
- .addHelpText(
58
- 'after',
59
- '\nAI agents that have not already seen the Market skill should run `market skill` before using this CLI.',
60
- )
61
-
62
- program
63
- .command('skill')
64
- .description('Print the Market agent skill')
65
- .action(() => {
66
- process.stdout.write(marketSkill)
67
- })
68
-
69
- program
70
- .command('types')
71
- .description('List current asset types, generation support, and search guidance')
72
- .addOption(apiOption)
73
- .action(async (opts: { api?: string }) => {
74
- await typesCommand(opts.api)
75
- })
76
-
77
- program
78
- .command('login')
79
- .description('Sign in')
80
- .addOption(apiOption)
81
- .action(async (opts: { api?: string }) => {
82
- const baseUrl = opts.api ?? DEFAULT_BASE_URL
83
- const token = await runDeviceLogin()
84
- const client = v1.createClient({ baseUrl, authToken: token })
85
- const profile = await client.user.getProfile()
86
- if (!profile) throw new Error('The Market API did not accept the auth token.')
87
- // Only pin baseUrl in config when the user explicitly chose one. Persisting
88
- // the default would freeze logged-in users to a host the CLI can no longer
89
- // change in a future release.
90
- await saveConfig(opts.api ? { authToken: token, baseUrl: opts.api } : { authToken: token })
91
- console.log(loginResult(profile.email, getConfigPath()))
92
- })
93
-
94
- program
95
- .command('logout')
96
- .description('Sign out')
97
- .action(async () => {
98
- await logout()
99
- })
100
-
101
- program
102
- .command('install')
103
- .description('Install assets by name, or package.json assetDependencies when no names are given')
104
- .argument('[assets...]', 'Asset names, optionally with @range')
105
- .addOption(apiOption)
106
- .option('--unapproved', 'Include unapproved versions', false)
107
- .option('--force', 'Overwrite existing files', false)
108
- .option('--key <key>', 'Access key of a shared private asset (no account needed)')
109
- .option('--cwd <dir>', 'Project directory')
110
- .option(
111
- '--redirect <folder>',
112
- 'Do not download asset files destined for this static root (e.g. public); write <folder>/_redirects rules to their canonical URLs instead. Repeatable.',
113
- collect,
114
- [],
115
- )
116
- .action(
117
- async (
118
- args: string[],
119
- opts: {
120
- api?: string
121
- unapproved: boolean
122
- force: boolean
123
- key?: string
124
- cwd?: string
125
- redirect: string[]
126
- },
127
- ) => {
128
- await installCommand(args, {
129
- baseUrl: opts.api,
130
- unapproved: opts.unapproved,
131
- force: opts.force,
132
- key: opts.key,
133
- cwd: opts.cwd,
134
- redirect: opts.redirect,
135
- })
136
- },
137
- )
138
-
139
- program
140
- .command('list')
141
- .description('List asset dependencies declared in package.json, including file aliases')
142
- .option('--cwd <dir>', 'Project directory')
143
- .action(async (opts: { cwd?: string }) => {
144
- await listCommand({
145
- cwd: opts.cwd,
146
- })
147
- })
148
-
149
- program
150
- .command('sync')
151
- .description(
152
- 'Update package.json assetDependencies aliases to match where installed files live now',
153
- )
154
- .addOption(apiOption)
155
- .option('--cwd <dir>', 'Project directory')
156
- .action(async (opts: { api?: string; cwd?: string }) => {
157
- await syncCommand({
158
- baseUrl: opts.api,
159
- cwd: opts.cwd,
160
- })
161
- })
162
-
163
- program
164
- .command('pack')
165
- .description('Create a Market asset zip using the same packaging step as upload')
166
- .argument('<source>', 'project directory, .zip path, or glob')
167
- .addOption(typeOption)
168
- .option('--out <file>', 'Output zip path')
169
- .option('--cwd <dir>', 'Project directory')
170
- .option('--npm <dep>', 'npm dependency name@range (repeatable)', collect, [])
171
- .option('--asset <dep>', 'asset dependency name@range (repeatable)', collect, [])
172
- .option('--skill <dep>', 'skill dependency label=source (repeatable)', collect, [])
173
- .action(
174
- async (
175
- zipFilter: string,
176
- opts: {
177
- type?: AssetType
178
- out?: string
179
- cwd?: string
180
- npm: string[]
181
- asset: string[]
182
- skill: string[]
183
- },
184
- ) => {
185
- await packCommand(zipFilter, {
186
- type: opts.type,
187
- out: opts.out,
188
- cwd: opts.cwd,
189
- npm: opts.npm,
190
- asset: opts.asset,
191
- skill: opts.skill,
192
- })
193
- },
194
- )
195
-
196
- program
197
- .command('search')
198
- .description('Find assets')
199
- .argument('<query>', 'Search query')
200
- .addOption(typeOption)
201
- .addOption(apiOption)
202
- .option('--unapproved', 'Include unapproved versions', false)
203
- .option('--limit <n>', 'Max results, 1-5', parseSearchLimit, 5)
204
- .option('--json', 'Emit a machine-readable JSON array instead of the summary lines', false)
205
- .action(
206
- async (
207
- query: string,
208
- opts: {
209
- type?: AssetType
210
- api?: string
211
- unapproved: boolean
212
- limit: number
213
- json: boolean
214
- },
215
- ) => {
216
- requireType(opts.type, 'Search')
217
- await searchCommand(query, {
218
- type: opts.type,
219
- baseUrl: opts.api,
220
- unapproved: opts.unapproved,
221
- limit: opts.limit,
222
- json: opts.json,
223
- })
224
- },
225
- )
226
-
227
- const agent = program
228
- .command('agent')
229
- .description('Plan, find, judge and generate a set of assets from a goal')
230
- .argument('[goal]', 'What you need, in plain language')
231
- .addOption(apiOption)
232
- .addOption(referenceImageOption)
233
- .option('--json', 'Output JSON', false)
234
- .action(
235
- async (
236
- goal: string | undefined,
237
- opts: { api?: string; referenceImage: string[]; json: boolean },
238
- ) => {
239
- if (!goal) {
240
- agent.help({ error: true })
241
- return
242
- }
243
- await agentCommand(goal, {
244
- baseUrl: opts.api,
245
- referenceImages: opts.referenceImage,
246
- json: opts.json,
247
- })
248
- },
249
- )
250
-
251
- program
252
- .command('upload')
253
- .description('Publish one asset')
254
- .argument('<name>', 'Asset name')
255
- .argument('<zip-filter>', '.zip path or glob')
256
- .argument('<description>', 'Short description')
257
- .addOption(typeOption)
258
- .addOption(apiOption)
259
- .option('--version <version>', 'Explicit semver version')
260
- .option('--cwd <dir>', 'Project directory')
261
- .option('--npm <dep>', 'npm dependency name@range (repeatable)', collect, [])
262
- .option('--asset <dep>', 'asset dependency name@range (repeatable)', collect, [])
263
- .option('--skill <dep>', 'skill dependency label=source (repeatable)', collect, [])
264
- .addOption(accessOption)
265
- .action(
266
- async (
267
- name: string,
268
- zipFilter: string,
269
- description: string,
270
- opts: {
271
- type?: AssetType
272
- api?: string
273
- version?: string
274
- cwd?: string
275
- npm: string[]
276
- asset: string[]
277
- skill: string[]
278
- access?: AssetAccess
279
- },
280
- ) => {
281
- requireType(opts.type, 'Upload')
282
- await uploadCommand(name, zipFilter, description, {
283
- type: opts.type,
284
- version: opts.version,
285
- baseUrl: opts.api,
286
- cwd: opts.cwd,
287
- npm: opts.npm,
288
- asset: opts.asset,
289
- skill: opts.skill,
290
- access: opts.access,
291
- })
292
- },
293
- )
294
-
295
- program
296
- .command('preview')
297
- .description("Save an asset's preview image")
298
- .argument('<asset>', 'Asset name, optionally with @version')
299
- .addOption(apiOption)
300
- .option('--unapproved', 'Include unapproved versions', false)
301
- .option('--out <file>', 'Output image path')
302
- .action(async (name: string, opts: { api?: string; unapproved: boolean; out?: string }) => {
303
- await previewCommand(name, {
304
- baseUrl: opts.api,
305
- unapproved: opts.unapproved,
306
- out: opts.out,
307
- })
308
- })
309
-
310
- program
311
- .command('urls')
312
- .description("Print an asset's remote file base URL, subpaths, and preview URL")
313
- .argument('<asset>', 'Asset name, optionally with @version')
314
- .addOption(apiOption)
315
- .option('--unapproved', 'Include unapproved versions', false)
316
- .option('--key <key>', 'Access key of a shared private asset (no account needed)')
317
- .action(async (ref: string, opts: { api?: string; unapproved: boolean; key?: string }) => {
318
- await urlsCommand(ref, {
319
- baseUrl: opts.api,
320
- unapproved: opts.unapproved,
321
- key: opts.key,
322
- })
323
- })
324
-
325
- const generate = program
326
- .command('generate')
327
- .description('Generate and install an asset, or check a generation job')
328
- .argument('[description]', 'Asset prompt (omit when using a subcommand)')
329
- .addOption(typeOption)
330
- .addOption(apiOption)
331
- .addOption(referenceImageOption)
332
- .option('--cwd <dir>', 'Project directory')
333
- .addOption(accessOption)
334
- .action(
335
- async (
336
- description: string | undefined,
337
- opts: {
338
- type?: AssetType
339
- api?: string
340
- referenceImage: string[]
341
- cwd?: string
342
- access?: AssetAccess
343
- },
344
- ) => {
345
- if (!description) {
346
- generate.help({ error: true })
347
- return
348
- }
349
- requireType(opts.type, 'Generate')
350
- await generateCommand(description, {
351
- type: opts.type,
352
- baseUrl: opts.api,
353
- referenceImages: opts.referenceImage,
354
- cwd: opts.cwd,
355
- access: opts.access,
356
- })
357
- },
358
- )
359
-
360
- // `generate install <jobId>` finishes a slow (job-based) generation: it checks the job once and, when
361
- // it has completed, installs the produced asset into the project. Still running → prints a note and
362
- // exits 0 (run again later); failed → exits 1. Fast asset types never need this — `generate` installs
363
- // them inline. ("install" over "status": the command's job is to integrate the asset, not just report.)
364
- generate
365
- .command('install')
366
- .description('Install the asset from a generation job once it has completed')
367
- .argument('<jobId>', 'Job id printed by `market generate`')
368
- .addOption(apiOption)
369
- .option('--cwd <dir>', 'Project directory')
370
- // Read merged options: `--cwd`/`--api` after `generate install` are otherwise captured by the
371
- // parent `generate` command (which declares the same options), leaving this subcommand's own opts
372
- // undefined. `optsWithGlobals()` surfaces whichever level parsed them.
373
- .action(async (jobId: string, _options, command: Command) => {
374
- const { api, cwd } = command.optsWithGlobals()
375
- await generateInstallCommand(jobId, { baseUrl: api, cwd })
376
- })
6
+ const program = createMarketCommand()
377
7
 
378
8
  if (process.argv.length <= 2) {
379
9
  program.outputHelp()
380
10
  process.exit(0)
381
11
  }
382
12
 
383
- program.parseAsync().catch((err) => {
384
- const message = err instanceof Error ? err.message : String(err)
13
+ program.parseAsync().catch((error: unknown) => {
14
+ const message = error instanceof Error ? error.message : String(error)
385
15
  console.error(errorResult(message))
386
16
  process.exit(1)
387
17
  })
388
-
389
- function requireType(type: AssetType | undefined, command: string): asserts type is AssetType {
390
- if (!type) {
391
- console.error(
392
- errorResult(`${command} requires --type. Available types: ${ASSET_TYPES.join(', ')}`),
393
- )
394
- process.exit(1)
395
- }
396
- }
397
-
398
- function parseSearchLimit(value: string): number {
399
- const parsed = Number.parseInt(value, 10)
400
- if (!Number.isInteger(parsed) || parsed < 1) {
401
- throw new Error('--limit must be a positive integer')
402
- }
403
- return Math.min(parsed, 5)
404
- }
405
-
406
- function readPackageVersion(): string {
407
- const value: unknown = createRequire(import.meta.url)('../package.json')
408
- if (
409
- !value ||
410
- typeof value !== 'object' ||
411
- Array.isArray(value) ||
412
- !('version' in value) ||
413
- typeof value.version !== 'string'
414
- ) {
415
- throw new Error('Market package.json is missing a valid version')
416
- }
417
- return value.version
418
- }
419
-
420
- async function runDeviceLogin(): Promise<string> {
421
- // Login-only dependencies load lazily: every other command skips their startup cost.
422
- const [{ default: chalk }, { default: open }, oauthClient] = await Promise.all([
423
- import('chalk'),
424
- import('open'),
425
- import('openid-client'),
426
- ])
427
- const config = await oauthClient.discovery(
428
- new URL(AUTH_ISSUER_URL),
429
- DEVICE_CLIENT_ID,
430
- undefined,
431
- oauthClient.None(),
432
- )
433
- const code = await oauthClient.initiateDeviceAuthorization(config, { scope: 'market' })
434
- const verificationUri = code.verification_uri_complete ?? code.verification_uri
435
-
436
- console.log(`Open ${chalk.cyan(verificationUri)}`)
437
- console.log(`Code: ${chalk.bold(code.user_code)}`)
438
- await open(verificationUri).catch(() => undefined)
439
-
440
- const tokens = await oauthClient.pollDeviceAuthorizationGrant(config, code)
441
- if (!tokens.access_token) throw new Error('Device login completed without an access token.')
442
- return tokens.access_token
443
- }
@@ -0,0 +1 @@
1
+ export { createMarketCommand, type MarketCommandOptions } from './command.js'