@drawcall/market 0.1.3 → 0.1.5
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/dist/cli-client.d.ts +17 -0
- package/dist/cli-client.d.ts.map +1 -0
- package/dist/cli-client.js +23 -0
- package/dist/cli-client.js.map +1 -0
- package/dist/cli.js +63 -48
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +5 -0
- package/dist/client.js.map +1 -1
- package/dist/commands/generate.d.ts +9 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +31 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/install.d.ts +9 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +89 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/login.d.ts +10 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +92 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +12 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/search.d.ts +7 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +32 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +47 -0
- package/dist/config.js.map +1 -0
- package/dist/contract.d.ts +25 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +20 -24
- package/dist/contract.js.map +1 -1
- package/dist/generate.d.ts +20 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +33 -0
- package/dist/generate.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/src/cli-client.ts +40 -0
- package/src/cli.ts +78 -57
- package/src/client.ts +7 -0
- package/src/commands/generate.ts +55 -0
- package/src/commands/install.ts +126 -0
- package/src/commands/login.ts +113 -0
- package/src/commands/logout.ts +11 -0
- package/src/commands/search.ts +41 -0
- package/src/config.ts +53 -0
- package/src/contract.ts +33 -23
- package/src/generate.ts +62 -0
- package/src/index.ts +5 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import ora, { type Ora } from 'ora'
|
|
3
|
+
import { resolve } from '../resolve.js'
|
|
4
|
+
import { install as runInstall } from '../install.js'
|
|
5
|
+
import { generateAndWait } from '../generate.js'
|
|
6
|
+
import { assetNameSchema, type AssetType } from '../schemas.js'
|
|
7
|
+
import { getCliClient } from '../cli-client.js'
|
|
8
|
+
import type { MarketClient } from '../client.js'
|
|
9
|
+
|
|
10
|
+
export interface InstallCommandOptions {
|
|
11
|
+
type?: AssetType
|
|
12
|
+
unapproved?: boolean
|
|
13
|
+
cwd?: string
|
|
14
|
+
baseUrl?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface AssetRequest {
|
|
18
|
+
name: string
|
|
19
|
+
range: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function installCommand(args: string[], opts: InstallCommandOptions): Promise<void> {
|
|
23
|
+
const { client, apiKey } = await getCliClient({ baseUrl: opts.baseUrl })
|
|
24
|
+
const canGenerate = Boolean(apiKey)
|
|
25
|
+
|
|
26
|
+
const spinner = ora().start()
|
|
27
|
+
try {
|
|
28
|
+
const requests: AssetRequest[] = []
|
|
29
|
+
for (const arg of args) {
|
|
30
|
+
spinner.text = `Resolving "${truncate(arg, 40)}"`
|
|
31
|
+
requests.push(await resolveArg(client, arg, opts.type, spinner, canGenerate))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
spinner.text = 'Resolving dependency tree'
|
|
35
|
+
const resolution = await resolve(client.asset, requests, {
|
|
36
|
+
includeUnapproved: opts.unapproved ?? false,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
await runInstall(client, resolution, {
|
|
40
|
+
cwd: opts.cwd,
|
|
41
|
+
onProgress: (msg) => {
|
|
42
|
+
spinner.text = msg
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
spinner.succeed(
|
|
47
|
+
`Installed ${resolution.assets.length} asset(s): ` +
|
|
48
|
+
resolution.assets.map((a) => chalk.cyan(a.name) + chalk.dim('@' + a.version)).join(', '),
|
|
49
|
+
)
|
|
50
|
+
} catch (err) {
|
|
51
|
+
spinner.fail(err instanceof Error ? err.message : String(err))
|
|
52
|
+
throw err
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Fallthrough: exact-name → top-1 search hit → auto-generate (auth only).
|
|
58
|
+
*/
|
|
59
|
+
async function resolveArg(
|
|
60
|
+
client: MarketClient,
|
|
61
|
+
arg: string,
|
|
62
|
+
type: AssetType | undefined,
|
|
63
|
+
spinner: Ora,
|
|
64
|
+
canGenerate: boolean,
|
|
65
|
+
): Promise<AssetRequest> {
|
|
66
|
+
// 1. Try parsing as asset-name[@range] and looking up exact
|
|
67
|
+
const parsed = parseNameAndRange(arg)
|
|
68
|
+
if (parsed) {
|
|
69
|
+
const existing = await client.asset.getByName({ name: parsed.name })
|
|
70
|
+
if (existing) return parsed
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 2. Semantic search — top-1
|
|
74
|
+
spinner.text = `Searching "${truncate(arg, 40)}"`
|
|
75
|
+
const results = await client.asset.list({
|
|
76
|
+
search: arg,
|
|
77
|
+
type,
|
|
78
|
+
limit: 1,
|
|
79
|
+
page: 1,
|
|
80
|
+
sort: 'relevance',
|
|
81
|
+
})
|
|
82
|
+
if (results.items.length > 0) {
|
|
83
|
+
const hit = results.items[0]
|
|
84
|
+
spinner.info(`Matched "${chalk.cyan(hit.name)}" ${chalk.dim(`(${hit.type})`)} for "${arg}"`)
|
|
85
|
+
spinner.start()
|
|
86
|
+
return { name: hit.name, range: '*' }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 3. Auto-generate (requires auth)
|
|
90
|
+
if (!canGenerate) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`No matches for "${arg}". Run \`market login\` to enable auto-generation of missing assets.`,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
spinner.text = `No matches — generating "${truncate(arg, 40)}"`
|
|
97
|
+
const generated = await generateAndWait(
|
|
98
|
+
client,
|
|
99
|
+
{ description: arg, type },
|
|
100
|
+
{
|
|
101
|
+
onProgress: (msg) => {
|
|
102
|
+
spinner.text = msg
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
spinner.info(
|
|
107
|
+
`Generated ${chalk.cyan(generated.assetName)}${chalk.dim('@' + generated.version)} for "${arg}"`,
|
|
108
|
+
)
|
|
109
|
+
spinner.start()
|
|
110
|
+
return { name: generated.assetName, range: generated.version }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Returns { name, range } if the arg parses as `<asset-name>[@<range>]`.
|
|
115
|
+
* Returns null if the name portion fails the asset-name schema (e.g. has spaces).
|
|
116
|
+
*/
|
|
117
|
+
function parseNameAndRange(arg: string): AssetRequest | null {
|
|
118
|
+
const atIdx = arg.indexOf('@')
|
|
119
|
+
const name = atIdx >= 0 ? arg.slice(0, atIdx) : arg
|
|
120
|
+
const range = atIdx >= 0 ? arg.slice(atIdx + 1) : '*'
|
|
121
|
+
return assetNameSchema.safeParse(name).success ? { name, range } : null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function truncate(s: string, max: number): string {
|
|
125
|
+
return s.length > max ? s.slice(0, max - 1) + '…' : s
|
|
126
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as http from 'http'
|
|
2
|
+
import type { AddressInfo } from 'net'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import open from 'open'
|
|
5
|
+
import ora from 'ora'
|
|
6
|
+
import { createMarketClient } from '../client.js'
|
|
7
|
+
import { saveConfig, getConfigPath } from '../config.js'
|
|
8
|
+
|
|
9
|
+
export interface LoginOptions {
|
|
10
|
+
baseUrl: string
|
|
11
|
+
timeoutMs?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Spins up a localhost HTTP server, opens the browser to {baseUrl}/cli-auth,
|
|
16
|
+
* waits for the web app to redirect back with the key, saves it to config.
|
|
17
|
+
*/
|
|
18
|
+
export async function login(opts: LoginOptions): Promise<void> {
|
|
19
|
+
const timeoutMs = opts.timeoutMs ?? 120_000
|
|
20
|
+
const state = crypto.randomUUID()
|
|
21
|
+
|
|
22
|
+
const { port, waitForKey, close } = await startCallbackServer(state, timeoutMs)
|
|
23
|
+
|
|
24
|
+
const authUrl = new URL('/cli-auth', opts.baseUrl)
|
|
25
|
+
authUrl.searchParams.set('state', state)
|
|
26
|
+
authUrl.searchParams.set('callback', `http://127.0.0.1:${port}/callback`)
|
|
27
|
+
|
|
28
|
+
console.log(chalk.dim(`If your browser doesn't open, visit:\n ${authUrl.toString()}`))
|
|
29
|
+
await open(authUrl.toString()).catch(() => {})
|
|
30
|
+
|
|
31
|
+
const spinner = ora('Waiting for browser approval…').start()
|
|
32
|
+
try {
|
|
33
|
+
const key = await waitForKey
|
|
34
|
+
|
|
35
|
+
spinner.text = 'Verifying key…'
|
|
36
|
+
const verifyClient = createMarketClient({ baseUrl: opts.baseUrl, apiKey: key })
|
|
37
|
+
const profile = await verifyClient.user.getProfile()
|
|
38
|
+
if (!profile) {
|
|
39
|
+
throw new Error('Key was not accepted by the server.')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await saveConfig({ apiKey: key, baseUrl: opts.baseUrl })
|
|
43
|
+
spinner.succeed(`Logged in as ${chalk.cyan(profile.email)}`)
|
|
44
|
+
console.log(chalk.dim(` Key saved to ${getConfigPath()}`))
|
|
45
|
+
} catch (err) {
|
|
46
|
+
spinner.fail(err instanceof Error ? err.message : String(err))
|
|
47
|
+
throw err
|
|
48
|
+
} finally {
|
|
49
|
+
close()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function startCallbackServer(
|
|
54
|
+
expectedState: string,
|
|
55
|
+
timeoutMs: number,
|
|
56
|
+
): Promise<{ port: number; waitForKey: Promise<string>; close: () => void }> {
|
|
57
|
+
let resolveKey!: (k: string) => void
|
|
58
|
+
let rejectKey!: (e: Error) => void
|
|
59
|
+
const waitForKey = new Promise<string>((resolve, reject) => {
|
|
60
|
+
resolveKey = resolve
|
|
61
|
+
rejectKey = reject
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const server = http.createServer((req, res) => {
|
|
65
|
+
if (!req.url) {
|
|
66
|
+
res.writeHead(400).end()
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
const url = new URL(req.url, 'http://localhost')
|
|
70
|
+
if (url.pathname !== '/callback') {
|
|
71
|
+
res.writeHead(404).end()
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
const receivedState = url.searchParams.get('state')
|
|
75
|
+
const receivedKey = url.searchParams.get('key')
|
|
76
|
+
|
|
77
|
+
if (receivedState !== expectedState) {
|
|
78
|
+
res.writeHead(400, { 'Content-Type': 'text/html' })
|
|
79
|
+
res.end('<h1>Invalid state</h1><p>You can close this window.</p>')
|
|
80
|
+
rejectKey(new Error('Invalid state parameter from callback.'))
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
if (!receivedKey) {
|
|
84
|
+
res.writeHead(400, { 'Content-Type': 'text/html' })
|
|
85
|
+
res.end('<h1>Missing key</h1><p>You can close this window.</p>')
|
|
86
|
+
rejectKey(new Error('Callback did not include a key.'))
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
res.writeHead(200, { 'Content-Type': 'text/html' })
|
|
91
|
+
res.end('<h1>Logged in!</h1><p>You can close this window.</p>')
|
|
92
|
+
resolveKey(receivedKey)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
await new Promise<void>((resolve, reject) => {
|
|
96
|
+
server.once('error', reject)
|
|
97
|
+
server.listen(0, '127.0.0.1', () => resolve())
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const address = server.address() as AddressInfo | null
|
|
101
|
+
if (!address) throw new Error('Failed to bind local callback server.')
|
|
102
|
+
|
|
103
|
+
const timer = setTimeout(() => {
|
|
104
|
+
rejectKey(new Error(`Timed out after ${timeoutMs / 1000}s waiting for browser approval.`))
|
|
105
|
+
}, timeoutMs)
|
|
106
|
+
|
|
107
|
+
const close = () => {
|
|
108
|
+
clearTimeout(timer)
|
|
109
|
+
server.close()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { port: address.port, waitForKey, close }
|
|
113
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { clearConfig, getConfigPath } from '../config.js'
|
|
3
|
+
|
|
4
|
+
export async function logout(): Promise<void> {
|
|
5
|
+
const existed = await clearConfig()
|
|
6
|
+
if (existed) {
|
|
7
|
+
console.log(chalk.green('Logged out.') + chalk.dim(` (${getConfigPath()} removed)`))
|
|
8
|
+
} else {
|
|
9
|
+
console.log(chalk.dim('Already logged out.'))
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import ora from 'ora'
|
|
3
|
+
import { getCliClient } from '../cli-client.js'
|
|
4
|
+
import type { AssetType } from '../schemas.js'
|
|
5
|
+
|
|
6
|
+
const SEARCH_LIMIT = 10
|
|
7
|
+
|
|
8
|
+
export interface SearchCommandOptions {
|
|
9
|
+
type?: AssetType
|
|
10
|
+
baseUrl?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function searchCommand(query: string, opts: SearchCommandOptions): Promise<void> {
|
|
14
|
+
const { client } = await getCliClient({ baseUrl: opts.baseUrl })
|
|
15
|
+
|
|
16
|
+
const spinner = ora(`Searching "${query}"`).start()
|
|
17
|
+
let results
|
|
18
|
+
try {
|
|
19
|
+
results = await client.asset.list({
|
|
20
|
+
search: query,
|
|
21
|
+
type: opts.type,
|
|
22
|
+
limit: SEARCH_LIMIT,
|
|
23
|
+
page: 1,
|
|
24
|
+
sort: 'relevance',
|
|
25
|
+
})
|
|
26
|
+
} catch (err) {
|
|
27
|
+
spinner.fail(err instanceof Error ? err.message : String(err))
|
|
28
|
+
throw err
|
|
29
|
+
}
|
|
30
|
+
spinner.stop()
|
|
31
|
+
|
|
32
|
+
if (results.items.length === 0) {
|
|
33
|
+
console.log(chalk.dim('No matches.'))
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
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}`)
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as fs from 'fs/promises'
|
|
2
|
+
import * as os from 'os'
|
|
3
|
+
import * as path from 'path'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
|
|
6
|
+
const configSchema = z.object({
|
|
7
|
+
apiKey: z.string().startsWith('mk_'),
|
|
8
|
+
baseUrl: z.string().url().optional(),
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export type Config = z.infer<typeof configSchema>
|
|
12
|
+
|
|
13
|
+
function configDir(): string {
|
|
14
|
+
if (process.platform === 'win32' && process.env.APPDATA) {
|
|
15
|
+
return path.join(process.env.APPDATA, 'drawcall-market')
|
|
16
|
+
}
|
|
17
|
+
const base = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), '.config')
|
|
18
|
+
return path.join(base, 'drawcall-market')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function configPath(): string {
|
|
22
|
+
return path.join(configDir(), 'config.json')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function loadConfig(): Promise<Config | null> {
|
|
26
|
+
try {
|
|
27
|
+
const raw = await fs.readFile(configPath(), 'utf-8')
|
|
28
|
+
const parsed = configSchema.safeParse(JSON.parse(raw))
|
|
29
|
+
return parsed.success ? parsed.data : null
|
|
30
|
+
} catch {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function saveConfig(config: Config): Promise<void> {
|
|
36
|
+
const dir = configDir()
|
|
37
|
+
await fs.mkdir(dir, { recursive: true })
|
|
38
|
+
const file = configPath()
|
|
39
|
+
await fs.writeFile(file, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function clearConfig(): Promise<boolean> {
|
|
43
|
+
try {
|
|
44
|
+
await fs.unlink(configPath())
|
|
45
|
+
return true
|
|
46
|
+
} catch {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getConfigPath(): string {
|
|
52
|
+
return configPath()
|
|
53
|
+
}
|
package/src/contract.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { oc } from '@orpc/contract'
|
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import {
|
|
4
4
|
assetNameSchema,
|
|
5
|
+
assetTypeSchema,
|
|
5
6
|
semverSchema,
|
|
6
7
|
listAssetsSchema,
|
|
7
8
|
updateProfileSchema,
|
|
@@ -106,6 +107,15 @@ export interface FileTreeEntry {
|
|
|
106
107
|
size: number
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
export interface GenerateJobStatus {
|
|
111
|
+
jobId: string
|
|
112
|
+
state: 'queued' | 'running' | 'done' | 'failed'
|
|
113
|
+
message: string | null
|
|
114
|
+
assetName: string | null
|
|
115
|
+
version: string | null
|
|
116
|
+
error: string | null
|
|
117
|
+
}
|
|
118
|
+
|
|
109
119
|
// ─── Contract ─────────────────────────────────────────────────────────────────
|
|
110
120
|
|
|
111
121
|
export const contract = {
|
|
@@ -114,9 +124,7 @@ export const contract = {
|
|
|
114
124
|
.input(z.object({ name: assetNameSchema }))
|
|
115
125
|
.output(z.custom<AssetWithVersionsAndTags | null>()),
|
|
116
126
|
|
|
117
|
-
list: oc
|
|
118
|
-
.input(listAssetsSchema)
|
|
119
|
-
.output(z.custom<PaginatedList<AssetListItem>>()),
|
|
127
|
+
list: oc.input(listAssetsSchema).output(z.custom<PaginatedList<AssetListItem>>()),
|
|
120
128
|
|
|
121
129
|
getVersionTree: oc
|
|
122
130
|
.input(z.object({ name: z.string(), version: semverSchema }))
|
|
@@ -144,44 +152,46 @@ export const contract = {
|
|
|
144
152
|
.input(uploadTypedSchema.extend({ file: z.instanceof(File) }))
|
|
145
153
|
.output(z.custom<AssetVersion>()),
|
|
146
154
|
|
|
147
|
-
material: oc
|
|
148
|
-
.input(uploadMaterialSchema)
|
|
149
|
-
.output(z.custom<AssetVersion>()),
|
|
155
|
+
material: oc.input(uploadMaterialSchema).output(z.custom<AssetVersion>()),
|
|
150
156
|
},
|
|
151
157
|
|
|
152
158
|
admin: {
|
|
153
|
-
listUnapproved: oc
|
|
154
|
-
.output(z.custom<UnapprovedItem[]>()),
|
|
159
|
+
listUnapproved: oc.output(z.custom<UnapprovedItem[]>()),
|
|
155
160
|
|
|
156
161
|
approve: oc
|
|
157
162
|
.input(z.object({ assetName: z.string(), version: z.string() }))
|
|
158
163
|
.output(z.object({ success: z.boolean() })),
|
|
159
164
|
|
|
160
|
-
backfillEmbeddings: oc
|
|
161
|
-
.output(z.object({ indexed: z.number() })),
|
|
165
|
+
backfillEmbeddings: oc.output(z.object({ indexed: z.number() })),
|
|
162
166
|
},
|
|
163
167
|
|
|
164
168
|
user: {
|
|
165
|
-
getProfile: oc
|
|
166
|
-
.output(z.custom<User | null>()),
|
|
169
|
+
getProfile: oc.output(z.custom<User | null>()),
|
|
167
170
|
|
|
168
|
-
updateProfile: oc
|
|
169
|
-
.input(updateProfileSchema)
|
|
170
|
-
.output(z.custom<User>()),
|
|
171
|
+
updateProfile: oc.input(updateProfileSchema).output(z.custom<User>()),
|
|
171
172
|
|
|
172
|
-
getApiKey: oc
|
|
173
|
-
.output(z.custom<{ prefix: string; createdAt: Date } | null>()),
|
|
173
|
+
getApiKey: oc.output(z.custom<{ prefix: string; createdAt: Date } | null>()),
|
|
174
174
|
|
|
175
|
-
regenerateApiKey: oc
|
|
176
|
-
.output(z.object({ key: z.string(), prefix: z.string() })),
|
|
175
|
+
regenerateApiKey: oc.output(z.object({ key: z.string(), prefix: z.string() })),
|
|
177
176
|
|
|
178
|
-
myAssets: oc
|
|
179
|
-
.output(z.custom<AssetWithVersions[]>()),
|
|
177
|
+
myAssets: oc.output(z.custom<AssetWithVersions[]>()),
|
|
180
178
|
},
|
|
181
179
|
|
|
182
180
|
tag: {
|
|
183
|
-
list: oc
|
|
184
|
-
|
|
181
|
+
list: oc.output(z.custom<TagWithCount[]>()),
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
generate: {
|
|
185
|
+
start: oc
|
|
186
|
+
.input(
|
|
187
|
+
z.object({
|
|
188
|
+
description: z.string().min(3).max(1000),
|
|
189
|
+
type: assetTypeSchema.optional(),
|
|
190
|
+
}),
|
|
191
|
+
)
|
|
192
|
+
.output(z.object({ jobId: z.string() })),
|
|
193
|
+
|
|
194
|
+
status: oc.input(z.object({ jobId: z.string() })).output(z.custom<GenerateJobStatus>()),
|
|
185
195
|
},
|
|
186
196
|
}
|
|
187
197
|
|
package/src/generate.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { MarketClient } from './client.js'
|
|
2
|
+
import type { AssetType } from './schemas.js'
|
|
3
|
+
|
|
4
|
+
export class GenerateError extends Error {
|
|
5
|
+
constructor(message: string) {
|
|
6
|
+
super(message)
|
|
7
|
+
this.name = 'GenerateError'
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface GenerateInput {
|
|
12
|
+
description: string
|
|
13
|
+
type?: AssetType
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface GenerateResult {
|
|
17
|
+
assetName: string
|
|
18
|
+
version: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface GenerateOptions {
|
|
22
|
+
onProgress?: (message: string) => void
|
|
23
|
+
pollIntervalMs?: number
|
|
24
|
+
timeoutMs?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function generateAndWait(
|
|
28
|
+
client: MarketClient,
|
|
29
|
+
input: GenerateInput,
|
|
30
|
+
opts: GenerateOptions = {},
|
|
31
|
+
): Promise<GenerateResult> {
|
|
32
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 1500
|
|
33
|
+
const timeoutMs = opts.timeoutMs ?? 60_000
|
|
34
|
+
const report = opts.onProgress ?? (() => {})
|
|
35
|
+
|
|
36
|
+
const { jobId } = await client.generate.start(input)
|
|
37
|
+
|
|
38
|
+
const deadline = Date.now() + timeoutMs
|
|
39
|
+
while (Date.now() < deadline) {
|
|
40
|
+
const status = await client.generate.status({ jobId })
|
|
41
|
+
if (status.message) report(status.message)
|
|
42
|
+
|
|
43
|
+
if (status.state === 'done') {
|
|
44
|
+
if (!status.assetName || !status.version) {
|
|
45
|
+
throw new GenerateError('Generation completed without an asset name.')
|
|
46
|
+
}
|
|
47
|
+
return { assetName: status.assetName, version: status.version }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (status.state === 'failed') {
|
|
51
|
+
throw new GenerateError(status.error ?? 'Generation failed.')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await sleep(pollIntervalMs)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
throw new GenerateError(`Generation timed out after ${timeoutMs}ms.`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sleep(ms: number): Promise<void> {
|
|
61
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
62
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -20,12 +20,17 @@ export type {
|
|
|
20
20
|
UnapprovedItem,
|
|
21
21
|
TagWithCount,
|
|
22
22
|
FileTreeEntry,
|
|
23
|
+
GenerateJobStatus,
|
|
23
24
|
} from './contract.js'
|
|
24
25
|
|
|
25
26
|
// Resolve
|
|
26
27
|
export { resolve, ResolutionError } from './resolve.js'
|
|
27
28
|
export type { ResolvedAsset, ResolveResult } from './resolve.js'
|
|
28
29
|
|
|
30
|
+
// Generate
|
|
31
|
+
export { generateAndWait, GenerateError } from './generate.js'
|
|
32
|
+
export type { GenerateInput, GenerateResult, GenerateOptions } from './generate.js'
|
|
33
|
+
|
|
29
34
|
// Schemas
|
|
30
35
|
export {
|
|
31
36
|
ASSET_TYPES,
|