@drawcall/market 0.1.4 → 0.1.6

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.
Files changed (99) hide show
  1. package/dist/asset-implementation.d.ts +88 -0
  2. package/dist/asset-implementation.d.ts.map +1 -0
  3. package/dist/asset-implementation.js +2 -0
  4. package/dist/asset-implementation.js.map +1 -0
  5. package/dist/cli-client.d.ts +2 -2
  6. package/dist/cli-client.d.ts.map +1 -1
  7. package/dist/cli-client.js +4 -4
  8. package/dist/cli-client.js.map +1 -1
  9. package/dist/cli.js +104 -22
  10. package/dist/cli.js.map +1 -1
  11. package/dist/client.d.ts +1 -3
  12. package/dist/client.d.ts.map +1 -1
  13. package/dist/client.js +4 -12
  14. package/dist/client.js.map +1 -1
  15. package/dist/commands/generate.d.ts.map +1 -1
  16. package/dist/commands/generate.js +9 -4
  17. package/dist/commands/generate.js.map +1 -1
  18. package/dist/commands/install.d.ts.map +1 -1
  19. package/dist/commands/install.js +33 -27
  20. package/dist/commands/install.js.map +1 -1
  21. package/dist/commands/logout.js +3 -3
  22. package/dist/commands/logout.js.map +1 -1
  23. package/dist/commands/preview.d.ts +9 -0
  24. package/dist/commands/preview.d.ts.map +1 -0
  25. package/dist/commands/preview.js +50 -0
  26. package/dist/commands/preview.js.map +1 -0
  27. package/dist/commands/search.d.ts +4 -1
  28. package/dist/commands/search.d.ts.map +1 -1
  29. package/dist/commands/search.js +21 -11
  30. package/dist/commands/search.js.map +1 -1
  31. package/dist/commands/upload.d.ts +9 -0
  32. package/dist/commands/upload.d.ts.map +1 -0
  33. package/dist/commands/upload.js +220 -0
  34. package/dist/commands/upload.js.map +1 -0
  35. package/dist/config.d.ts +4 -7
  36. package/dist/config.d.ts.map +1 -1
  37. package/dist/config.js +8 -4
  38. package/dist/config.js.map +1 -1
  39. package/dist/constants.d.ts.map +1 -1
  40. package/dist/constants.js +2 -10
  41. package/dist/constants.js.map +1 -1
  42. package/dist/contract.d.ts +37 -134
  43. package/dist/contract.d.ts.map +1 -1
  44. package/dist/contract.js +10 -48
  45. package/dist/contract.js.map +1 -1
  46. package/dist/generate.d.ts +0 -2
  47. package/dist/generate.d.ts.map +1 -1
  48. package/dist/generate.js +5 -22
  49. package/dist/generate.js.map +1 -1
  50. package/dist/index.d.ts +5 -6
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +2 -3
  53. package/dist/index.js.map +1 -1
  54. package/dist/install.d.ts +1 -1
  55. package/dist/install.d.ts.map +1 -1
  56. package/dist/install.js +14 -11
  57. package/dist/install.js.map +1 -1
  58. package/dist/output.d.ts +26 -0
  59. package/dist/output.d.ts.map +1 -0
  60. package/dist/output.js +52 -0
  61. package/dist/output.js.map +1 -0
  62. package/dist/resolve.d.ts.map +1 -1
  63. package/dist/resolve.js +15 -26
  64. package/dist/resolve.js.map +1 -1
  65. package/dist/schemas.d.ts +31 -39
  66. package/dist/schemas.d.ts.map +1 -1
  67. package/dist/schemas.js +23 -25
  68. package/dist/schemas.js.map +1 -1
  69. package/package.json +13 -4
  70. package/src/asset-implementation.ts +114 -0
  71. package/src/cli-client.ts +7 -6
  72. package/src/cli.ts +144 -29
  73. package/src/client.ts +5 -15
  74. package/src/commands/generate.ts +9 -6
  75. package/src/commands/install.ts +41 -30
  76. package/src/commands/logout.ts +3 -3
  77. package/src/commands/preview.ts +69 -0
  78. package/src/commands/search.ts +28 -12
  79. package/src/commands/upload.ts +262 -0
  80. package/src/config.ts +11 -5
  81. package/src/constants.ts +2 -10
  82. package/src/contract.ts +22 -120
  83. package/src/generate.ts +5 -29
  84. package/src/index.ts +23 -14
  85. package/src/install.ts +24 -14
  86. package/src/output.ts +76 -0
  87. package/src/resolve.ts +22 -38
  88. package/src/schemas.ts +26 -27
  89. package/tsconfig.json +2 -1
  90. package/dist/commands/login.d.ts +0 -10
  91. package/dist/commands/login.d.ts.map +0 -1
  92. package/dist/commands/login.js +0 -92
  93. package/dist/commands/login.js.map +0 -1
  94. package/dist/internal-contract.d.ts +0 -19
  95. package/dist/internal-contract.d.ts.map +0 -1
  96. package/dist/internal-contract.js +0 -19
  97. package/dist/internal-contract.js.map +0 -1
  98. package/src/commands/login.ts +0 -113
  99. package/src/internal-contract.ts +0 -26
package/src/cli.ts CHANGED
@@ -2,56 +2,66 @@
2
2
 
3
3
  import { Command, Option } from 'commander'
4
4
  import chalk from 'chalk'
5
+ import open from 'open'
6
+ import * as oauthClient from 'openid-client'
5
7
  import { ASSET_TYPES, type AssetType } from './schemas.js'
6
8
  import { installCommand } from './commands/install.js'
7
9
  import { searchCommand } from './commands/search.js'
8
10
  import { generateCommand } from './commands/generate.js'
9
- import { login } from './commands/login.js'
11
+ import { previewCommand } from './commands/preview.js'
12
+ import { uploadCommand } from './commands/upload.js'
10
13
  import { logout } from './commands/logout.js'
11
- import { NotLoggedInError } from './cli-client.js'
14
+ import { createMarketClient } from './client.js'
15
+ import { saveConfig, getConfigPath } from './config.js'
16
+ import { errorResult, loginResult } from './output.js'
12
17
 
13
18
  const program = new Command()
14
19
 
15
20
  const DEFAULT_BASE_URL = 'https://api.market.drawcall.ai'
21
+ const AUTH_ISSUER_URL = 'https://auth.drawcall.ai/api/auth'
22
+ const DEVICE_CLIENT_ID = 'market-cli'
16
23
 
17
- const typeOption = new Option('--type <type>', 'Filter by asset type').choices([...ASSET_TYPES])
18
-
19
- const apiOption = new Option('--api <url>', 'API base URL').default(
24
+ const typeOption = new Option('--type <type>', 'Asset type').choices([...ASSET_TYPES])
25
+ const apiOption = new Option('--api <url>', 'API URL').default(
20
26
  process.env.MARKET_API_URL,
21
27
  'from MARKET_API_URL / config / default',
22
28
  )
23
29
 
24
- program.name('market').description('Install assets from the drawcall.ai market').version('0.1.0')
30
+ program.name('market').description('Find and install Drawcall Market assets').version('0.1.0')
25
31
 
26
32
  program
27
33
  .command('login')
28
- .description('Authenticate with the market via your browser')
34
+ .description('Sign in')
29
35
  .addOption(apiOption)
30
36
  .action(async (opts: { api?: string }) => {
31
- await login({ baseUrl: opts.api ?? DEFAULT_BASE_URL })
37
+ const baseUrl = opts.api ?? DEFAULT_BASE_URL
38
+ const token = await runDeviceLogin()
39
+ const client = createMarketClient({ baseUrl, authToken: token })
40
+ const profile = await client.user.getProfile()
41
+ if (!profile) throw new Error('The Market API did not accept the auth token.')
42
+ await saveConfig({ authToken: token, baseUrl })
43
+ console.log(loginResult(profile.email, getConfigPath()))
32
44
  })
33
45
 
34
46
  program
35
47
  .command('logout')
36
- .description('Remove the local API key')
48
+ .description('Sign out')
37
49
  .action(async () => {
38
50
  await logout()
39
51
  })
40
52
 
41
53
  program
42
54
  .command('install')
43
- .description(
44
- 'Install assets. Each arg is tried as an exact name, then a semantic search, then auto-generated.',
45
- )
46
- .argument('<assets...>', 'Asset names or natural-language descriptions')
55
+ .description('Install by name or prompt')
56
+ .argument('<assets...>', 'Names or prompts')
47
57
  .addOption(typeOption)
48
58
  .addOption(apiOption)
49
59
  .option('--unapproved', 'Include unapproved versions', false)
50
- .option('--cwd <dir>', 'Project directory', process.cwd())
60
+ .option('--cwd <dir>', 'Project directory')
51
61
  .action(
52
62
  async (
53
63
  args: string[],
54
- opts: { type?: AssetType; api?: string; unapproved: boolean; cwd: string },
64
+ opts: { type?: AssetType; api?: string; unapproved: boolean; cwd?: string },
55
65
  ) => {
56
66
  await installCommand(args, {
57
67
  type: opts.type,
@@ -64,22 +74,98 @@ program
64
74
 
65
75
  program
66
76
  .command('search')
67
- .description('Search the market. Prints up to 10 ranked results.')
68
- .argument('<query>', 'Natural-language query')
77
+ .description('Find assets')
78
+ .argument('<query>', 'Search query')
69
79
  .addOption(typeOption)
70
80
  .addOption(apiOption)
71
- .action(async (query: string, opts: { type?: AssetType; api?: string }) => {
72
- await searchCommand(query, { type: opts.type, baseUrl: opts.api })
73
- })
81
+ .option('--unapproved', 'Include unapproved versions', false)
82
+ .option('--limit <n>', 'Max results, 1-5', parseSearchLimit, 5)
83
+ .option('--verbose', 'Show longer descriptions', false)
84
+ .action(
85
+ async (
86
+ query: string,
87
+ opts: {
88
+ type?: AssetType
89
+ api?: string
90
+ unapproved: boolean
91
+ limit: number
92
+ verbose: boolean
93
+ },
94
+ ) => {
95
+ if (!opts.type) {
96
+ console.error(
97
+ errorResult(`Search requires --type. Available types: ${ASSET_TYPES.join(', ')}`),
98
+ )
99
+ process.exit(1)
100
+ }
101
+ await searchCommand(query, {
102
+ type: opts.type,
103
+ baseUrl: opts.api,
104
+ unapproved: opts.unapproved,
105
+ limit: opts.limit,
106
+ verbose: opts.verbose,
107
+ })
108
+ },
109
+ )
110
+
111
+ program
112
+ .command('upload')
113
+ .description('Publish one model')
114
+ .argument('<name>', 'Asset name')
115
+ .argument('<file-filter>', '.glb/.gltf path or glob')
116
+ .argument('<description>', 'Short description')
117
+ .addOption(typeOption)
118
+ .addOption(apiOption)
119
+ .option('--version <version>', 'Explicit semver version')
120
+ .option('--cwd <dir>', 'Project directory')
121
+ .action(
122
+ async (
123
+ name: string,
124
+ fileFilter: string,
125
+ description: string,
126
+ opts: { type?: AssetType; api?: string; version?: string; cwd?: string },
127
+ ) => {
128
+ await uploadCommand(name, fileFilter, description, {
129
+ type: opts.type,
130
+ version: opts.version,
131
+ baseUrl: opts.api,
132
+ cwd: opts.cwd,
133
+ })
134
+ },
135
+ )
136
+
137
+ program
138
+ .command('preview')
139
+ .description('Save preview image')
140
+ .argument('<name>', 'Asset name')
141
+ .argument('[version]', 'Semver version')
142
+ .addOption(typeOption)
143
+ .addOption(apiOption)
144
+ .option('--unapproved', 'Include unapproved versions', false)
145
+ .option('--out <file>', 'Output PNG path')
146
+ .action(
147
+ async (
148
+ name: string,
149
+ version: string | undefined,
150
+ opts: { type?: AssetType; api?: string; unapproved: boolean; out?: string },
151
+ ) => {
152
+ await previewCommand(name, version, {
153
+ type: opts.type,
154
+ baseUrl: opts.api,
155
+ unapproved: opts.unapproved,
156
+ out: opts.out,
157
+ })
158
+ },
159
+ )
74
160
 
75
161
  program
76
162
  .command('generate')
77
- .description('Generate a new asset from a description and install it.')
78
- .argument('<description>', 'Description of the asset to generate')
163
+ .description('Generate and install')
164
+ .argument('<description>', 'Asset prompt')
79
165
  .addOption(typeOption)
80
166
  .addOption(apiOption)
81
- .option('--cwd <dir>', 'Project directory', process.cwd())
82
- .action(async (description: string, opts: { type?: AssetType; api?: string; cwd: string }) => {
167
+ .option('--cwd <dir>', 'Project directory')
168
+ .action(async (description: string, opts: { type?: AssetType; api?: string; cwd?: string }) => {
83
169
  await generateCommand(description, {
84
170
  type: opts.type,
85
171
  baseUrl: opts.api,
@@ -87,11 +173,40 @@ program
87
173
  })
88
174
  })
89
175
 
176
+ if (process.argv.length <= 2) {
177
+ program.outputHelp()
178
+ process.exit(0)
179
+ }
180
+
90
181
  program.parseAsync().catch((err) => {
91
- // Commands that own a spinner print the error via spinner.fail() and rethrow,
92
- // so we only print here when nothing else did.
93
- if (err instanceof NotLoggedInError) {
94
- console.error(chalk.red(err.message))
95
- }
182
+ const message = err instanceof Error ? err.message : String(err)
183
+ console.error(errorResult(message))
96
184
  process.exit(1)
97
185
  })
186
+
187
+ function parseSearchLimit(value: string): number {
188
+ const parsed = Number.parseInt(value, 10)
189
+ if (!Number.isInteger(parsed) || parsed < 1) {
190
+ throw new Error('--limit must be a positive integer')
191
+ }
192
+ return Math.min(parsed, 5)
193
+ }
194
+
195
+ async function runDeviceLogin(): Promise<string> {
196
+ const config = await oauthClient.discovery(
197
+ new URL(AUTH_ISSUER_URL),
198
+ DEVICE_CLIENT_ID,
199
+ undefined,
200
+ oauthClient.None(),
201
+ )
202
+ const code = await oauthClient.initiateDeviceAuthorization(config, { scope: 'market' })
203
+ const verificationUri = code.verification_uri_complete ?? code.verification_uri
204
+
205
+ console.log(`Open ${chalk.cyan(verificationUri)}`)
206
+ console.log(`Code: ${chalk.bold(code.user_code)}`)
207
+ await open(verificationUri).catch(() => undefined)
208
+
209
+ const tokens = await oauthClient.pollDeviceAuthorizationGrant(config, code)
210
+ if (!tokens.access_token) throw new Error('Device login completed without an access token.')
211
+ return tokens.access_token
212
+ }
package/src/client.ts CHANGED
@@ -2,39 +2,29 @@ import { createORPCClient } from '@orpc/client'
2
2
  import { RPCLink } from '@orpc/client/fetch'
3
3
  import type { ContractRouterClient } from '@orpc/contract'
4
4
  import type { AppContract } from './contract.js'
5
- import type { InternalContract } from './internal-contract.js'
6
5
 
7
6
  export type MarketClient = ContractRouterClient<AppContract>
8
- export type InternalClient = ContractRouterClient<InternalContract>
9
7
 
10
8
  const DEFAULT_BASE_URL = 'https://api.market.drawcall.ai'
11
9
 
12
10
  export interface MarketClientOptions {
13
11
  baseUrl?: string
14
12
  fetch?: typeof globalThis.fetch
13
+ authToken?: string
15
14
  apiKey?: string
16
15
  }
17
16
 
18
- function buildHeaders(apiKey?: string): Record<string, string> | undefined {
19
- return apiKey ? { 'x-api-key': apiKey } : undefined
17
+ function buildHeaders(authToken?: string): Record<string, string> | undefined {
18
+ return authToken ? { authorization: `Bearer ${authToken}` } : undefined
20
19
  }
21
20
 
22
21
  export function createMarketClient(opts: MarketClientOptions = {}): MarketClient {
23
22
  const baseUrl = opts.baseUrl || DEFAULT_BASE_URL
23
+ const authToken = opts.authToken ?? opts.apiKey
24
24
  const link = new RPCLink({
25
25
  url: new URL('/api/rpc', baseUrl).href,
26
26
  fetch: opts.fetch,
27
- headers: buildHeaders(opts.apiKey),
27
+ headers: buildHeaders(authToken),
28
28
  })
29
29
  return createORPCClient<MarketClient>(link)
30
30
  }
31
-
32
- export function createInternalClient(opts: MarketClientOptions = {}): InternalClient {
33
- const baseUrl = opts.baseUrl || DEFAULT_BASE_URL
34
- const link = new RPCLink({
35
- url: new URL('/api/internal-rpc', baseUrl).href,
36
- fetch: opts.fetch,
37
- headers: buildHeaders(opts.apiKey),
38
- })
39
- return createORPCClient<InternalClient>(link)
40
- }
@@ -1,9 +1,9 @@
1
- import chalk from 'chalk'
2
1
  import ora from 'ora'
3
2
  import { getCliClient } from '../cli-client.js'
4
3
  import { generateAndWait } from '../generate.js'
5
4
  import { resolve } from '../resolve.js'
6
5
  import { install as runInstall } from '../install.js'
6
+ import { generatedInstallResult } from '../output.js'
7
7
  import type { AssetType } from '../schemas.js'
8
8
 
9
9
  export interface GenerateCommandOptions {
@@ -19,7 +19,11 @@ export async function generateCommand(
19
19
  ): Promise<void> {
20
20
  const { client } = await getCliClient({ baseUrl: opts.baseUrl, requireAuth: true })
21
21
 
22
- const spinner = ora(`Generating "${description}"`).start()
22
+ const spinner = ora({
23
+ text: `Generating "${description}"`,
24
+ isEnabled: Boolean(process.stderr.isTTY),
25
+ isSilent: !process.stderr.isTTY,
26
+ }).start()
23
27
  try {
24
28
  const generated = await generateAndWait(
25
29
  client,
@@ -45,11 +49,10 @@ export async function generateCommand(
45
49
  },
46
50
  })
47
51
 
48
- spinner.succeed(
49
- `Installed ${chalk.cyan(generated.assetName)}${chalk.dim('@' + generated.version)}`,
50
- )
52
+ spinner.stop()
53
+ console.log(generatedInstallResult(generated.assetName, generated.version))
51
54
  } catch (err) {
52
- spinner.fail(err instanceof Error ? err.message : String(err))
55
+ spinner.stop()
53
56
  throw err
54
57
  }
55
58
  }
@@ -1,10 +1,10 @@
1
- import chalk from 'chalk'
2
1
  import ora, { type Ora } from 'ora'
3
2
  import { resolve } from '../resolve.js'
4
3
  import { install as runInstall } from '../install.js'
5
4
  import { generateAndWait } from '../generate.js'
6
5
  import { assetNameSchema, type AssetType } from '../schemas.js'
7
6
  import { getCliClient } from '../cli-client.js'
7
+ import { compact, generatedInstallResult, installResult } from '../output.js'
8
8
  import type { MarketClient } from '../client.js'
9
9
 
10
10
  export interface InstallCommandOptions {
@@ -20,14 +20,20 @@ interface AssetRequest {
20
20
  }
21
21
 
22
22
  export async function installCommand(args: string[], opts: InstallCommandOptions): Promise<void> {
23
- const { client } = await getCliClient({ baseUrl: opts.baseUrl, requireAuth: true })
23
+ const { client, authToken } = await getCliClient({ baseUrl: opts.baseUrl })
24
+ const canGenerate = Boolean(authToken)
24
25
 
25
- const spinner = ora().start()
26
+ const spinner = ora({
27
+ isEnabled: Boolean(process.stderr.isTTY),
28
+ isSilent: !process.stderr.isTTY,
29
+ }).start()
26
30
  try {
27
31
  const requests: AssetRequest[] = []
28
32
  for (const arg of args) {
29
33
  spinner.text = `Resolving "${truncate(arg, 40)}"`
30
- requests.push(await resolveArg(client, arg, opts.type, spinner))
34
+ requests.push(
35
+ await resolveArg(client, arg, opts.type, spinner, canGenerate, opts.unapproved ?? false),
36
+ )
31
37
  }
32
38
 
33
39
  spinner.text = 'Resolving dependency tree'
@@ -42,50 +48,57 @@ export async function installCommand(args: string[], opts: InstallCommandOptions
42
48
  },
43
49
  })
44
50
 
45
- spinner.succeed(
46
- `Installed ${resolution.assets.length} asset(s): ` +
47
- resolution.assets.map((a) => chalk.cyan(a.name) + chalk.dim('@' + a.version)).join(', '),
48
- )
51
+ spinner.stop()
52
+ console.log(installResult(resolution.assets))
49
53
  } catch (err) {
50
- spinner.fail(err instanceof Error ? err.message : String(err))
54
+ spinner.stop()
51
55
  throw err
52
56
  }
53
57
  }
54
58
 
55
- /**
56
- * Fallthrough: exact-name → top-1 search hit → auto-generate.
57
- */
58
59
  async function resolveArg(
59
60
  client: MarketClient,
60
61
  arg: string,
61
62
  type: AssetType | undefined,
62
63
  spinner: Ora,
64
+ canGenerate: boolean,
65
+ includeUnapproved: boolean,
63
66
  ): Promise<AssetRequest> {
64
- // 1. Try parsing as asset-name[@range] and looking up exact
65
67
  const parsed = parseNameAndRange(arg)
66
68
  if (parsed) {
67
- const existing = await client.asset.getByName({ name: parsed.name })
69
+ const existing = await client.asset.exact({
70
+ name: parsed.name,
71
+ type,
72
+ includeUnapproved,
73
+ })
68
74
  if (existing) return parsed
69
75
  }
70
76
 
71
- // 2. Semantic search — top-1
72
77
  spinner.text = `Searching "${truncate(arg, 40)}"`
73
- const results = await client.asset.list({
74
- search: arg,
78
+ const results = await client.asset.search({
79
+ query: arg,
75
80
  type,
81
+ includeUnapproved,
76
82
  limit: 1,
77
83
  page: 1,
78
84
  sort: 'relevance',
79
85
  })
80
86
  if (results.items.length > 0) {
81
87
  const hit = results.items[0]
82
- spinner.info(`Matched "${chalk.cyan(hit.name)}" ${chalk.dim(`(${hit.type})`)} for "${arg}"`)
83
- spinner.start()
88
+ if (process.stderr.isTTY) {
89
+ spinner.info(`Matched "${compact(arg, 48)}" to ${hit.name} (${hit.type})`)
90
+ spinner.start()
91
+ }
84
92
  return { name: hit.name, range: '*' }
85
93
  }
86
94
 
87
- // 3. Auto-generate
88
- spinner.text = `No matches — generating "${truncate(arg, 40)}"`
95
+ if (!canGenerate) {
96
+ throw new Error(
97
+ `No matches for "${arg}". Run \`market login\` to enable auto-generation of missing assets.`,
98
+ )
99
+ }
100
+
101
+ spinner.text = `No matches; generating "${truncate(arg, 40)}"`
89
102
  const generated = await generateAndWait(
90
103
  client,
91
104
  { description: arg, type },
@@ -95,17 +108,15 @@ async function resolveArg(
95
108
  },
96
109
  },
97
110
  )
98
- spinner.info(
99
- `Generated ${chalk.cyan(generated.assetName)}${chalk.dim('@' + generated.version)} for "${arg}"`,
100
- )
101
- spinner.start()
111
+ if (process.stderr.isTTY) {
112
+ spinner.info(
113
+ `${generatedInstallResult(generated.assetName, generated.version)} for "${compact(arg, 48)}"`,
114
+ )
115
+ spinner.start()
116
+ }
102
117
  return { name: generated.assetName, range: generated.version }
103
118
  }
104
119
 
105
- /**
106
- * Returns { name, range } if the arg parses as `<asset-name>[@<range>]`.
107
- * Returns null if the name portion fails the asset-name schema (e.g. has spaces).
108
- */
109
120
  function parseNameAndRange(arg: string): AssetRequest | null {
110
121
  const atIdx = arg.indexOf('@')
111
122
  const name = atIdx >= 0 ? arg.slice(0, atIdx) : arg
@@ -114,5 +125,5 @@ function parseNameAndRange(arg: string): AssetRequest | null {
114
125
  }
115
126
 
116
127
  function truncate(s: string, max: number): string {
117
- return s.length > max ? s.slice(0, max - 1) + '' : s
128
+ return s.length > max ? s.slice(0, max - 3) + '...' : s
118
129
  }
@@ -1,11 +1,11 @@
1
- import chalk from 'chalk'
2
1
  import { clearConfig, getConfigPath } from '../config.js'
2
+ import { logoutResult } from '../output.js'
3
3
 
4
4
  export async function logout(): Promise<void> {
5
5
  const existed = await clearConfig()
6
6
  if (existed) {
7
- console.log(chalk.green('Logged out.') + chalk.dim(` (${getConfigPath()} removed)`))
7
+ console.log(logoutResult(getConfigPath()))
8
8
  } else {
9
- console.log(chalk.dim('Already logged out.'))
9
+ console.log(logoutResult())
10
10
  }
11
11
  }
@@ -0,0 +1,69 @@
1
+ import * as fs from 'fs/promises'
2
+ import * as path from 'path'
3
+ import ora from 'ora'
4
+ import { getCliClient } from '../cli-client.js'
5
+ import { previewResult } from '../output.js'
6
+ import { assetNameSchema, semverSchema, type AssetType } from '../schemas.js'
7
+
8
+ export interface PreviewCommandOptions {
9
+ type?: AssetType
10
+ unapproved?: boolean
11
+ out?: string
12
+ baseUrl?: string
13
+ }
14
+
15
+ export async function previewCommand(
16
+ name: string,
17
+ version: string | undefined,
18
+ opts: PreviewCommandOptions,
19
+ ): Promise<void> {
20
+ const parsedName = assetNameSchema.parse(name)
21
+ const parsedVersion = version ? semverSchema.parse(version) : undefined
22
+ const spinner = ora({
23
+ text: `Resolving preview for ${parsedName}`,
24
+ isEnabled: Boolean(process.stderr.isTTY),
25
+ isSilent: !process.stderr.isTTY,
26
+ }).start()
27
+
28
+ try {
29
+ const { client } = await getCliClient({ baseUrl: opts.baseUrl })
30
+ const resolvedVersion =
31
+ parsedVersion ??
32
+ (
33
+ await client.asset.exact({
34
+ name: parsedName,
35
+ type: opts.type ?? 'model',
36
+ includeUnapproved: opts.unapproved ?? false,
37
+ })
38
+ )?.latestVersion
39
+
40
+ if (!resolvedVersion) {
41
+ throw new Error(`Asset "${parsedName}" not found`)
42
+ }
43
+
44
+ spinner.text = `Downloading preview for ${parsedName}@${resolvedVersion}`
45
+ const preview = await client.asset.downloadPreviewImage({
46
+ name: parsedName,
47
+ version: resolvedVersion,
48
+ })
49
+ const bytes = new Uint8Array(await preview.arrayBuffer())
50
+ const out = path.resolve(
51
+ opts.out ?? `${parsedName}-${resolvedVersion}-preview${previewExtension(preview.type)}`,
52
+ )
53
+
54
+ await fs.mkdir(path.dirname(out), { recursive: true })
55
+ await fs.writeFile(out, bytes)
56
+
57
+ spinner.stop()
58
+ console.log(previewResult(parsedName, resolvedVersion, out))
59
+ } catch (err) {
60
+ spinner.stop()
61
+ throw err
62
+ }
63
+ }
64
+
65
+ function previewExtension(contentType: string): string {
66
+ if (contentType === 'image/webp') return '.webp'
67
+ if (contentType === 'image/jpeg') return '.jpg'
68
+ return '.png'
69
+ }
@@ -1,41 +1,57 @@
1
- import chalk from 'chalk'
2
1
  import ora from 'ora'
3
2
  import { getCliClient } from '../cli-client.js'
3
+ import { assetSearchResultLine, searchSummary } from '../output.js'
4
4
  import type { AssetType } from '../schemas.js'
5
5
 
6
- const SEARCH_LIMIT = 10
6
+ const SEARCH_LIMIT = 5
7
7
 
8
8
  export interface SearchCommandOptions {
9
- type?: AssetType
9
+ type: AssetType
10
+ unapproved?: boolean
10
11
  baseUrl?: string
12
+ limit?: number
13
+ verbose?: boolean
11
14
  }
12
15
 
13
16
  export async function searchCommand(query: string, opts: SearchCommandOptions): Promise<void> {
14
- const { client } = await getCliClient({ baseUrl: opts.baseUrl, requireAuth: true })
17
+ const { client } = await getCliClient({ baseUrl: opts.baseUrl })
15
18
 
16
- const spinner = ora(`Searching "${query}"`).start()
19
+ const spinner = ora({
20
+ text: `Searching "${query}"`,
21
+ isEnabled: Boolean(process.stderr.isTTY),
22
+ isSilent: !process.stderr.isTTY,
23
+ }).start()
17
24
  let results
18
25
  try {
19
- results = await client.asset.list({
20
- search: query,
26
+ results = await client.asset.search({
27
+ query,
21
28
  type: opts.type,
22
- limit: SEARCH_LIMIT,
29
+ includeUnapproved: opts.unapproved ?? false,
30
+ limit: opts.limit ?? SEARCH_LIMIT,
23
31
  page: 1,
24
32
  sort: 'relevance',
25
33
  })
26
34
  } catch (err) {
27
- spinner.fail(err instanceof Error ? err.message : String(err))
35
+ spinner.stop()
28
36
  throw err
29
37
  }
30
38
  spinner.stop()
31
39
 
40
+ console.log(
41
+ searchSummary({
42
+ query,
43
+ type: opts.type,
44
+ includeUnapproved: opts.unapproved ?? false,
45
+ count: results.items.length,
46
+ total: results.total,
47
+ }),
48
+ )
49
+
32
50
  if (results.items.length === 0) {
33
- console.log(chalk.dim('No matches.'))
34
51
  return
35
52
  }
36
53
 
37
54
  for (const item of results.items) {
38
- const desc = item.description ? chalk.dim(` — ${item.description}`) : ''
39
- console.log(` ${chalk.cyan(item.name)} ${chalk.dim(`(${item.type})`)}${desc}`)
55
+ console.log(assetSearchResultLine(item, { verbose: opts.verbose ?? false }))
40
56
  }
41
57
  }