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