@drawcall/market 0.1.48 → 0.1.50
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/README.md +4 -1
- package/dist/asset-implementation.d.ts +5 -1
- package/dist/asset-implementation.d.ts.map +1 -1
- package/dist/asset-implementation.js.map +1 -1
- package/dist/cli.js +56 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/generate-install.d.ts +6 -0
- package/dist/commands/generate-install.d.ts.map +1 -0
- package/dist/commands/generate-install.js +29 -0
- package/dist/commands/generate-install.js.map +1 -0
- package/dist/commands/generate.d.ts +15 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +57 -20
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/pack.d.ts +14 -0
- package/dist/commands/pack.d.ts.map +1 -0
- package/dist/commands/pack.js +39 -0
- package/dist/commands/pack.js.map +1 -0
- package/dist/commands/upload.d.ts +3 -13
- package/dist/commands/upload.d.ts.map +1 -1
- package/dist/commands/upload.js +15 -187
- package/dist/commands/upload.js.map +1 -1
- package/dist/contract.d.ts +15 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/generate-job.d.ts +4 -0
- package/dist/generate-job.d.ts.map +1 -1
- package/dist/generate-job.js +5 -1
- package/dist/generate-job.js.map +1 -1
- package/dist/generate.d.ts +16 -6
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +30 -19
- package/dist/generate.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/output.d.ts +4 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +31 -0
- package/dist/output.js.map +1 -1
- package/dist/pack.d.ts +46 -0
- package/dist/pack.d.ts.map +1 -0
- package/dist/pack.js +220 -0
- package/dist/pack.js.map +1 -0
- package/dist/schemas.d.ts +13 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +9 -0
- package/dist/schemas.js.map +1 -1
- package/dist/skill.d.ts +1 -1
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js +3 -2
- package/dist/skill.js.map +1 -1
- package/package.json +2 -2
- package/skills/market/SKILL.md +3 -2
- package/src/asset-implementation.ts +5 -1
- package/src/cli.ts +84 -11
- package/src/commands/agent.ts +1 -1
- package/src/commands/generate-install.ts +36 -0
- package/src/commands/generate.ts +79 -31
- package/src/commands/pack.ts +55 -0
- package/src/commands/upload.ts +18 -219
- package/src/contract.ts +7 -0
- package/src/generate-job.ts +5 -1
- package/src/generate.ts +45 -31
- package/src/index.ts +4 -2
- package/src/output.ts +35 -0
- package/src/pack.ts +294 -0
- package/src/schemas.ts +11 -0
- package/src/skill.ts +3 -2
package/src/cli.ts
CHANGED
|
@@ -5,12 +5,14 @@ import { Command, Option } from 'commander'
|
|
|
5
5
|
import chalk from 'chalk'
|
|
6
6
|
import open from 'open'
|
|
7
7
|
import * as oauthClient from 'openid-client'
|
|
8
|
-
import { ASSET_TYPES, type AssetType } from './schemas.js'
|
|
8
|
+
import { ASSET_TYPES, type AssetAccess, type AssetType } from './schemas.js'
|
|
9
9
|
import { installCommand } from './commands/install.js'
|
|
10
10
|
import { searchCommand } from './commands/search.js'
|
|
11
11
|
import { agentCommand } from './commands/agent.js'
|
|
12
12
|
import { generateCommand } from './commands/generate.js'
|
|
13
|
+
import { generateInstallCommand } from './commands/generate-install.js'
|
|
13
14
|
import { listCommand } from './commands/list.js'
|
|
15
|
+
import { packCommand } from './commands/pack.js'
|
|
14
16
|
import { previewCommand } from './commands/preview.js'
|
|
15
17
|
import { uploadCommand } from './commands/upload.js'
|
|
16
18
|
import { logout } from './commands/logout.js'
|
|
@@ -30,6 +32,12 @@ const DEVICE_CLIENT_ID = 'market-cli'
|
|
|
30
32
|
const collect = (value: string, previous: string[]): string[] => previous.concat(value)
|
|
31
33
|
|
|
32
34
|
const typeOption = new Option('--type <type>', 'Asset type').choices([...ASSET_TYPES])
|
|
35
|
+
// Omitted → the server picks the default from your entitlement (private if you hold market:private,
|
|
36
|
+
// else public). `--access private` requires that entitlement.
|
|
37
|
+
const accessOption = new Option('--access <access>', 'Asset visibility').choices([
|
|
38
|
+
'public',
|
|
39
|
+
'private',
|
|
40
|
+
])
|
|
33
41
|
const apiOption = new Option('--api <url>', 'API URL').default(
|
|
34
42
|
process.env.MARKET_API_URL,
|
|
35
43
|
'from MARKET_API_URL / config / default',
|
|
@@ -107,6 +115,39 @@ program
|
|
|
107
115
|
})
|
|
108
116
|
})
|
|
109
117
|
|
|
118
|
+
program
|
|
119
|
+
.command('pack')
|
|
120
|
+
.description('Create a Market asset zip using the same packaging step as upload')
|
|
121
|
+
.argument('<zip-filter>', '.zip path or glob')
|
|
122
|
+
.addOption(typeOption)
|
|
123
|
+
.option('--out <file>', 'Output zip path')
|
|
124
|
+
.option('--cwd <dir>', 'Project directory')
|
|
125
|
+
.option('--npm <dep>', 'npm dependency name@range (repeatable)', collect, [])
|
|
126
|
+
.option('--asset <dep>', 'asset dependency name@range (repeatable)', collect, [])
|
|
127
|
+
.option('--skill <dep>', 'skill dependency label=source (repeatable)', collect, [])
|
|
128
|
+
.action(
|
|
129
|
+
async (
|
|
130
|
+
zipFilter: string,
|
|
131
|
+
opts: {
|
|
132
|
+
type?: AssetType
|
|
133
|
+
out?: string
|
|
134
|
+
cwd?: string
|
|
135
|
+
npm: string[]
|
|
136
|
+
asset: string[]
|
|
137
|
+
skill: string[]
|
|
138
|
+
},
|
|
139
|
+
) => {
|
|
140
|
+
await packCommand(zipFilter, {
|
|
141
|
+
type: opts.type,
|
|
142
|
+
out: opts.out,
|
|
143
|
+
cwd: opts.cwd,
|
|
144
|
+
npm: opts.npm,
|
|
145
|
+
asset: opts.asset,
|
|
146
|
+
skill: opts.skill,
|
|
147
|
+
})
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
|
|
110
151
|
program
|
|
111
152
|
.command('search')
|
|
112
153
|
.description('Find assets')
|
|
@@ -159,6 +200,7 @@ program
|
|
|
159
200
|
.option('--npm <dep>', 'npm dependency name@range (repeatable)', collect, [])
|
|
160
201
|
.option('--asset <dep>', 'asset dependency name@range (repeatable)', collect, [])
|
|
161
202
|
.option('--skill <dep>', 'skill dependency label=source (repeatable)', collect, [])
|
|
203
|
+
.addOption(accessOption)
|
|
162
204
|
.action(
|
|
163
205
|
async (
|
|
164
206
|
name: string,
|
|
@@ -172,6 +214,7 @@ program
|
|
|
172
214
|
npm: string[]
|
|
173
215
|
asset: string[]
|
|
174
216
|
skill: string[]
|
|
217
|
+
access?: AssetAccess
|
|
175
218
|
},
|
|
176
219
|
) => {
|
|
177
220
|
requireType(opts.type, 'Upload')
|
|
@@ -183,6 +226,7 @@ program
|
|
|
183
226
|
npm: opts.npm,
|
|
184
227
|
asset: opts.asset,
|
|
185
228
|
skill: opts.skill,
|
|
229
|
+
access: opts.access,
|
|
186
230
|
})
|
|
187
231
|
},
|
|
188
232
|
)
|
|
@@ -202,20 +246,49 @@ program
|
|
|
202
246
|
})
|
|
203
247
|
})
|
|
204
248
|
|
|
205
|
-
program
|
|
249
|
+
const generate = program
|
|
206
250
|
.command('generate')
|
|
207
|
-
.description('Generate and install')
|
|
208
|
-
.argument('
|
|
251
|
+
.description('Generate and install an asset, or check a generation job')
|
|
252
|
+
.argument('[description]', 'Asset prompt (omit when using a subcommand)')
|
|
209
253
|
.addOption(typeOption)
|
|
210
254
|
.addOption(apiOption)
|
|
211
255
|
.option('--cwd <dir>', 'Project directory')
|
|
212
|
-
.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
256
|
+
.addOption(accessOption)
|
|
257
|
+
.action(
|
|
258
|
+
async (
|
|
259
|
+
description: string | undefined,
|
|
260
|
+
opts: { type?: AssetType; api?: string; cwd?: string; access?: AssetAccess },
|
|
261
|
+
) => {
|
|
262
|
+
if (!description) {
|
|
263
|
+
generate.help({ error: true })
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
requireType(opts.type, 'Generate')
|
|
267
|
+
await generateCommand(description, {
|
|
268
|
+
type: opts.type,
|
|
269
|
+
baseUrl: opts.api,
|
|
270
|
+
cwd: opts.cwd,
|
|
271
|
+
access: opts.access,
|
|
272
|
+
})
|
|
273
|
+
},
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
// `generate install <jobId>` finishes a slow (job-based) generation: it checks the job once and, when
|
|
277
|
+
// it has completed, installs the produced asset into the project. Still running → prints a note and
|
|
278
|
+
// exits 0 (run again later); failed → exits 1. Fast asset types never need this — `generate` installs
|
|
279
|
+
// them inline. ("install" over "status": the command's job is to integrate the asset, not just report.)
|
|
280
|
+
generate
|
|
281
|
+
.command('install')
|
|
282
|
+
.description('Install the asset from a generation job once it has completed')
|
|
283
|
+
.argument('<jobId>', 'Job id printed by `market generate`')
|
|
284
|
+
.addOption(apiOption)
|
|
285
|
+
.option('--cwd <dir>', 'Project directory')
|
|
286
|
+
// Read merged options: `--cwd`/`--api` after `generate install` are otherwise captured by the
|
|
287
|
+
// parent `generate` command (which declares the same options), leaving this subcommand's own opts
|
|
288
|
+
// undefined. `optsWithGlobals()` surfaces whichever level parsed them.
|
|
289
|
+
.action(async (jobId: string, _options, command: Command) => {
|
|
290
|
+
const { api, cwd } = command.optsWithGlobals()
|
|
291
|
+
await generateInstallCommand(jobId, { baseUrl: api, cwd })
|
|
219
292
|
})
|
|
220
293
|
|
|
221
294
|
if (process.argv.length <= 2) {
|
package/src/commands/agent.ts
CHANGED
|
@@ -4,7 +4,7 @@ import ora from 'ora'
|
|
|
4
4
|
import { getCliClient } from '../cli-client.js'
|
|
5
5
|
import { agentAndWait } from '../agent.js'
|
|
6
6
|
import { assetVersionRef } from '../output.js'
|
|
7
|
-
import type { AgentResult } from '../
|
|
7
|
+
import type { AgentResult } from '../schemas.js'
|
|
8
8
|
|
|
9
9
|
export interface AgentCommandOptions {
|
|
10
10
|
images?: string[]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getCliClient } from '../cli-client.js'
|
|
2
|
+
import { waitForGeneration } from '../generate.js'
|
|
3
|
+
import { generationRunningResult } from '../output.js'
|
|
4
|
+
import { finishGeneration, startSpinner } from './generate.js'
|
|
5
|
+
|
|
6
|
+
export interface GenerateInstallCommandOptions {
|
|
7
|
+
cwd?: string
|
|
8
|
+
baseUrl?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Install the asset from a generation job. Block-and-polls for a bounded window (the command waits,
|
|
12
|
+
// not the agent): installs on completion, fails loudly on error, or prints a note so the caller runs
|
|
13
|
+
// it again to continue the same job.
|
|
14
|
+
export async function generateInstallCommand(
|
|
15
|
+
jobId: string,
|
|
16
|
+
opts: GenerateInstallCommandOptions,
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
const { client, baseUrl } = await getCliClient({ baseUrl: opts.baseUrl, requireAuth: true })
|
|
19
|
+
const spinner = startSpinner('Generating asset')
|
|
20
|
+
try {
|
|
21
|
+
const outcome = await waitForGeneration(client, jobId, {
|
|
22
|
+
onProgress: (message) => {
|
|
23
|
+
spinner.text = message
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
await finishGeneration(client, outcome, {
|
|
27
|
+
cwd: opts.cwd,
|
|
28
|
+
baseUrl,
|
|
29
|
+
spinner,
|
|
30
|
+
stillRunning: generationRunningResult(jobId),
|
|
31
|
+
})
|
|
32
|
+
} catch (err) {
|
|
33
|
+
spinner.stop()
|
|
34
|
+
throw err
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/commands/generate.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import ora from 'ora'
|
|
1
|
+
import ora, { type Ora } from 'ora'
|
|
2
2
|
import { getCliClient } from '../cli-client.js'
|
|
3
|
-
import {
|
|
3
|
+
import { GenerateError, startGeneration, waitForGeneration, type PollOutcome } from '../generate.js'
|
|
4
|
+
import type { MarketClient } from '../client.js'
|
|
4
5
|
import { resolve } from '../resolve.js'
|
|
5
6
|
import { install as runInstall } from '../install.js'
|
|
6
|
-
import { generatedInstallResult } from '../output.js'
|
|
7
|
-
import type { AssetType } from '../schemas.js'
|
|
7
|
+
import { generatedInstallResult, generationStartedResult } from '../output.js'
|
|
8
|
+
import type { AssetAccess, AssetType } from '../schemas.js'
|
|
8
9
|
|
|
9
10
|
export interface GenerateCommandOptions {
|
|
10
11
|
type?: AssetType
|
|
11
12
|
cwd?: string
|
|
12
13
|
baseUrl?: string
|
|
14
|
+
access?: AssetAccess
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export async function generateCommand(
|
|
@@ -17,40 +19,86 @@ export async function generateCommand(
|
|
|
17
19
|
opts: GenerateCommandOptions,
|
|
18
20
|
): Promise<void> {
|
|
19
21
|
const { client, baseUrl } = await getCliClient({ baseUrl: opts.baseUrl, requireAuth: true })
|
|
20
|
-
|
|
21
|
-
const spinner = ora({
|
|
22
|
-
text: `Generating "${description}"`,
|
|
23
|
-
isEnabled: Boolean(process.stderr.isTTY),
|
|
24
|
-
isSilent: !process.stderr.isTTY,
|
|
25
|
-
}).start()
|
|
22
|
+
const spinner = startSpinner(`Generating "${description}"`)
|
|
26
23
|
try {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
const started = await startGeneration(client, {
|
|
25
|
+
description,
|
|
26
|
+
type: opts.type,
|
|
27
|
+
access: opts.access,
|
|
28
|
+
})
|
|
29
|
+
// Fast providers finish inline; slow, job-based ones return a job to poll.
|
|
30
|
+
if (started.status === 'completed') {
|
|
31
|
+
await installGenerated(client, started, { cwd: opts.cwd, baseUrl, spinner })
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
// Block-and-poll for a bounded window (the command waits, not the agent). If the job outlasts the
|
|
35
|
+
// window, hand back its id so the caller continues with `generate install`.
|
|
36
|
+
const outcome = await waitForGeneration(client, started.jobId, {
|
|
37
|
+
onProgress: (message) => {
|
|
38
|
+
spinner.text = message
|
|
34
39
|
},
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
spinner.text = `Resolving ${generated.assetName}@${generated.version}`
|
|
38
|
-
const resolution = await resolve(client.asset, [
|
|
39
|
-
{ name: generated.assetName, range: generated.version },
|
|
40
|
-
])
|
|
41
|
-
|
|
42
|
-
await runInstall(client, resolution, {
|
|
40
|
+
})
|
|
41
|
+
await finishGeneration(client, outcome, {
|
|
43
42
|
cwd: opts.cwd,
|
|
44
43
|
baseUrl,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
},
|
|
44
|
+
spinner,
|
|
45
|
+
stillRunning: generationStartedResult(started.jobId),
|
|
48
46
|
})
|
|
49
|
-
|
|
50
|
-
spinner.stop()
|
|
51
|
-
console.log(generatedInstallResult(generated.assetName, generated.version))
|
|
52
47
|
} catch (err) {
|
|
53
48
|
spinner.stop()
|
|
54
49
|
throw err
|
|
55
50
|
}
|
|
56
51
|
}
|
|
52
|
+
|
|
53
|
+
interface InstallContext {
|
|
54
|
+
cwd?: string
|
|
55
|
+
baseUrl: string
|
|
56
|
+
spinner: Ora
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Finish a poll outcome: install on completion, fail loudly on error, or print the still-running note
|
|
60
|
+
// (the job keeps running server-side). Shared by `generate` and `generate install`.
|
|
61
|
+
export async function finishGeneration(
|
|
62
|
+
client: MarketClient,
|
|
63
|
+
outcome: PollOutcome,
|
|
64
|
+
ctx: InstallContext & { stillRunning: string },
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
if (outcome.status === 'completed') {
|
|
67
|
+
await installGenerated(client, outcome, ctx)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
ctx.spinner.stop()
|
|
71
|
+
if (outcome.status === 'failed') {
|
|
72
|
+
throw new GenerateError(outcome.error)
|
|
73
|
+
}
|
|
74
|
+
console.log(ctx.stillRunning)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Install a finished generation into the project and report it.
|
|
78
|
+
async function installGenerated(
|
|
79
|
+
client: MarketClient,
|
|
80
|
+
generated: { assetName: string; version: string },
|
|
81
|
+
ctx: InstallContext,
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
ctx.spinner.text = `Resolving ${generated.assetName}@${generated.version}`
|
|
84
|
+
const resolution = await resolve(client.asset, [
|
|
85
|
+
{ name: generated.assetName, range: generated.version },
|
|
86
|
+
])
|
|
87
|
+
await runInstall(client, resolution, {
|
|
88
|
+
cwd: ctx.cwd,
|
|
89
|
+
baseUrl: ctx.baseUrl,
|
|
90
|
+
onProgress: (message) => {
|
|
91
|
+
ctx.spinner.text = message
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
ctx.spinner.stop()
|
|
95
|
+
console.log(generatedInstallResult(generated.assetName, generated.version))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function startSpinner(text: string): Ora {
|
|
99
|
+
return ora({
|
|
100
|
+
text,
|
|
101
|
+
isEnabled: Boolean(process.stderr.isTTY),
|
|
102
|
+
isSilent: !process.stderr.isTTY,
|
|
103
|
+
}).start()
|
|
104
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as fs from 'fs/promises'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import ora from 'ora'
|
|
4
|
+
import { packAsset, packPolicyForType, parsePackDependencies } from '../pack.js'
|
|
5
|
+
import { packResult } from '../output.js'
|
|
6
|
+
import type { AssetType } from '../schemas.js'
|
|
7
|
+
|
|
8
|
+
export interface PackCommandOptions {
|
|
9
|
+
type?: AssetType
|
|
10
|
+
cwd?: string
|
|
11
|
+
out?: string
|
|
12
|
+
/** npm dependencies, each `name@range` (range defaults to `*`). */
|
|
13
|
+
npm?: string[]
|
|
14
|
+
/** asset dependencies, each `name@range` (range defaults to `*`). */
|
|
15
|
+
asset?: string[]
|
|
16
|
+
/** skill dependencies, each `label=source` passed to `skills add`. */
|
|
17
|
+
skill?: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function packCommand(zipFilter: string, opts: PackCommandOptions): Promise<void> {
|
|
21
|
+
const spinner = ora({
|
|
22
|
+
text: 'Preparing pack',
|
|
23
|
+
isEnabled: Boolean(process.stderr.isTTY),
|
|
24
|
+
isSilent: !process.stderr.isTTY,
|
|
25
|
+
}).start()
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const cwd = opts.cwd ?? process.cwd()
|
|
29
|
+
const packed = await packAsset(zipFilter, {
|
|
30
|
+
cwd,
|
|
31
|
+
dependencies: parsePackDependencies({
|
|
32
|
+
npm: opts.npm,
|
|
33
|
+
asset: opts.asset,
|
|
34
|
+
skill: opts.skill,
|
|
35
|
+
}),
|
|
36
|
+
policy: packPolicyForType(opts.type),
|
|
37
|
+
})
|
|
38
|
+
const out = opts.out ? path.resolve(cwd, opts.out) : defaultPackPath(packed.sourcePath)
|
|
39
|
+
|
|
40
|
+
spinner.text = `Writing ${path.relative(cwd, out) || out}`
|
|
41
|
+
await fs.mkdir(path.dirname(out), { recursive: true })
|
|
42
|
+
await fs.writeFile(out, packed.zip)
|
|
43
|
+
|
|
44
|
+
spinner.stop()
|
|
45
|
+
console.log(packResult(out, packed))
|
|
46
|
+
} catch (err) {
|
|
47
|
+
spinner.stop()
|
|
48
|
+
throw err
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function defaultPackPath(sourcePath: string): string {
|
|
53
|
+
const ext = path.extname(sourcePath)
|
|
54
|
+
return path.join(path.dirname(sourcePath), `${path.basename(sourcePath, ext)}.packed${ext}`)
|
|
55
|
+
}
|
package/src/commands/upload.ts
CHANGED
|
@@ -1,21 +1,14 @@
|
|
|
1
|
-
import * as fs from 'fs/promises'
|
|
2
|
-
import * as path from 'path'
|
|
3
|
-
import { unzipSync, zipSync } from 'fflate'
|
|
4
1
|
import ora from 'ora'
|
|
5
2
|
import semver from 'semver'
|
|
6
3
|
import { getCliClient } from '../cli-client.js'
|
|
7
|
-
import {
|
|
8
|
-
import { readMarketLock, sha256 } from '../market-lock.js'
|
|
9
|
-
import {
|
|
10
|
-
packageJsonAssetDependenciesFromFiles,
|
|
11
|
-
packageJsonNpmDependenciesFromFiles,
|
|
12
|
-
} from '../package-json.js'
|
|
13
|
-
import { MAX_ASSET_DESCRIPTION_LENGTH, MAX_UPLOAD_ZIP_SIZE_BYTES } from '../schemas.js'
|
|
4
|
+
import { packAsset, packPolicyFromInstallMetadata, parsePackDependencies } from '../pack.js'
|
|
14
5
|
import { unchangedUploadResult, uploadResult } from '../output.js'
|
|
15
6
|
import {
|
|
16
7
|
assetDescriptionSchema,
|
|
17
8
|
assetNameSchema,
|
|
18
9
|
semverSchema,
|
|
10
|
+
MAX_ASSET_DESCRIPTION_LENGTH,
|
|
11
|
+
type AssetAccess,
|
|
19
12
|
type AssetType,
|
|
20
13
|
} from '../schemas.js'
|
|
21
14
|
|
|
@@ -30,6 +23,8 @@ export interface UploadCommandOptions {
|
|
|
30
23
|
asset?: string[]
|
|
31
24
|
/** skill dependencies, each `label=source` passed to `skills add`. */
|
|
32
25
|
skill?: string[]
|
|
26
|
+
/** Requested visibility; omitted lets the server resolve it from entitlement. */
|
|
27
|
+
access?: AssetAccess
|
|
33
28
|
}
|
|
34
29
|
|
|
35
30
|
export async function uploadCommand(
|
|
@@ -49,37 +44,21 @@ export async function uploadCommand(
|
|
|
49
44
|
const parsedVersion = opts.version ? semverSchema.parse(opts.version) : undefined
|
|
50
45
|
const parsedDescription = parseUploadDescription(description)
|
|
51
46
|
// Parse dep flags before any network work so a malformed spec fails fast.
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
const dependencies = parsePackDependencies({
|
|
48
|
+
npm: opts.npm,
|
|
49
|
+
asset: opts.asset,
|
|
50
|
+
skill: opts.skill,
|
|
51
|
+
})
|
|
55
52
|
const cwd = opts.cwd ?? process.cwd()
|
|
56
53
|
const type = opts.type
|
|
57
|
-
const zipFile = await resolveOneZipFile(cwd, zipFilter)
|
|
58
|
-
const zipStat = await fs.stat(zipFile)
|
|
59
|
-
if (zipStat.size >= MAX_UPLOAD_ZIP_SIZE_BYTES) {
|
|
60
|
-
throw new Error('Upload zip must be smaller than 1 GB')
|
|
61
|
-
}
|
|
62
|
-
const sourceZip = new Uint8Array(await fs.readFile(zipFile))
|
|
63
54
|
const { client } = await getCliClient({ baseUrl: opts.baseUrl, requireAuth: true })
|
|
64
55
|
const installMetadata = await client.asset.installMetadata()
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const assetDependencies =
|
|
71
|
-
packageJsonAssetDependencies,
|
|
72
|
-
explicitAssetDependencies,
|
|
73
|
-
)
|
|
74
|
-
// A type that ships a package.json (templates) also carries its npm dependencies there; capture
|
|
75
|
-
// them so an install over an existing package.json brings them. Explicit --npm wins on conflict.
|
|
76
|
-
const packageJsonNpmDependencies = typeMetadata?.readAssetDependenciesFromPackageJson
|
|
77
|
-
? packageJsonNpmDependenciesFromFiles(sourceFiles)
|
|
78
|
-
: {}
|
|
79
|
-
const npmDependencies = { ...packageJsonNpmDependencies, ...explicitNpmDependencies }
|
|
80
|
-
const zip = typeMetadata?.omitUnchangedInstalledFilesOnUpload
|
|
81
|
-
? await omitUnchangedInstalledFiles(sourceZip, sourceFiles, assetDependencies, cwd)
|
|
82
|
-
: sourceZip
|
|
56
|
+
const packed = await packAsset(zipFilter, {
|
|
57
|
+
cwd,
|
|
58
|
+
dependencies,
|
|
59
|
+
policy: packPolicyFromInstallMetadata(installMetadata[type]),
|
|
60
|
+
})
|
|
61
|
+
const { zip, npmDependencies, assetDependencies, skillDependencies } = packed
|
|
83
62
|
const profile = await client.user.getProfile()
|
|
84
63
|
if (!profile) {
|
|
85
64
|
throw new Error('Not logged in. Run `market login` first.')
|
|
@@ -124,6 +103,7 @@ export async function uploadCommand(
|
|
|
124
103
|
assetDependencies,
|
|
125
104
|
skillDependencies,
|
|
126
105
|
tags: [],
|
|
106
|
+
access: opts.access,
|
|
127
107
|
zip: new File([toArrayBuffer(zip)], `${parsedName}-${version}.zip`, {
|
|
128
108
|
type: 'application/zip',
|
|
129
109
|
}),
|
|
@@ -147,118 +127,6 @@ export function parseUploadDescription(description: string): string {
|
|
|
147
127
|
}
|
|
148
128
|
}
|
|
149
129
|
|
|
150
|
-
/**
|
|
151
|
-
* Parse `name@range` specs (npm or asset deps) into a name→range record. The
|
|
152
|
-
* range is optional and defaults to `*`. A leading `@` is treated as a scope
|
|
153
|
-
* marker, so `@scope/pkg@^1.0.0` splits into `@scope/pkg` and `^1.0.0`.
|
|
154
|
-
*/
|
|
155
|
-
export function parseVersionedDeps(specs: string[], kind: 'npm' | 'asset'): Record<string, string> {
|
|
156
|
-
const out: Record<string, string> = {}
|
|
157
|
-
for (const spec of specs) {
|
|
158
|
-
const at = spec.lastIndexOf('@')
|
|
159
|
-
const hasRange = at > 0
|
|
160
|
-
const name = hasRange ? spec.slice(0, at) : spec
|
|
161
|
-
const range = hasRange ? spec.slice(at + 1) : '*'
|
|
162
|
-
if (!name || !range) {
|
|
163
|
-
throw new Error(`Invalid ${kind} dependency "${spec}". Use name@range (e.g. three@^0.178.0).`)
|
|
164
|
-
}
|
|
165
|
-
if (name in out) {
|
|
166
|
-
throw new Error(`Duplicate ${kind} dependency "${name}".`)
|
|
167
|
-
}
|
|
168
|
-
out[name] = range
|
|
169
|
-
}
|
|
170
|
-
return out
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Parse `label=source` specs into a label→source record. The source is passed
|
|
175
|
-
* verbatim to `skills add` (a GitHub/git ref or a local path), so only the
|
|
176
|
-
* first `=` is treated as the separator.
|
|
177
|
-
*/
|
|
178
|
-
export function parseSkillDeps(specs: string[]): Record<string, string> {
|
|
179
|
-
const out: Record<string, string> = {}
|
|
180
|
-
for (const spec of specs) {
|
|
181
|
-
const eq = spec.indexOf('=')
|
|
182
|
-
if (eq <= 0 || eq === spec.length - 1) {
|
|
183
|
-
throw new Error(
|
|
184
|
-
`Invalid skill dependency "${spec}". Use label=source ` +
|
|
185
|
-
`(e.g. web-design=vercel-labs/agent-skills).`,
|
|
186
|
-
)
|
|
187
|
-
}
|
|
188
|
-
const label = spec.slice(0, eq)
|
|
189
|
-
if (label in out) {
|
|
190
|
-
throw new Error(`Duplicate skill dependency "${label}".`)
|
|
191
|
-
}
|
|
192
|
-
out[label] = spec.slice(eq + 1)
|
|
193
|
-
}
|
|
194
|
-
return out
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function mergeAssetDependencies(
|
|
198
|
-
fromPackageJson: Record<string, string>,
|
|
199
|
-
explicit: Record<string, string>,
|
|
200
|
-
): Record<string, string> {
|
|
201
|
-
const merged = { ...fromPackageJson }
|
|
202
|
-
for (const [name, range] of Object.entries(explicit)) {
|
|
203
|
-
if (name in merged && merged[name] !== range) {
|
|
204
|
-
throw new Error(
|
|
205
|
-
`Conflicting asset dependency "${name}": package.json has ${merged[name]}, --asset has ${range}`,
|
|
206
|
-
)
|
|
207
|
-
}
|
|
208
|
-
merged[name] = range
|
|
209
|
-
}
|
|
210
|
-
return merged
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async function omitUnchangedInstalledFiles(
|
|
214
|
-
sourceZip: Uint8Array,
|
|
215
|
-
sourceFiles: Record<string, Uint8Array>,
|
|
216
|
-
assetDependencies: Record<string, string>,
|
|
217
|
-
cwd: string,
|
|
218
|
-
): Promise<Uint8Array> {
|
|
219
|
-
const dependencyNames = new Set(Object.keys(assetDependencies))
|
|
220
|
-
if (dependencyNames.size === 0) return sourceZip
|
|
221
|
-
|
|
222
|
-
const installRoot = await findInstallRoot(cwd)
|
|
223
|
-
const lock = await readMarketLock(installRoot)
|
|
224
|
-
const hashesByPath = new Map<string, string>()
|
|
225
|
-
|
|
226
|
-
for (const [name, asset] of Object.entries(lock.assets)) {
|
|
227
|
-
if (!dependencyNames.has(name)) continue
|
|
228
|
-
for (const [file, metadata] of Object.entries(asset.files)) {
|
|
229
|
-
hashesByPath.set(file, metadata.sha256)
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (hashesByPath.size === 0) return sourceZip
|
|
234
|
-
|
|
235
|
-
let omitted = false
|
|
236
|
-
const filtered: Record<string, Uint8Array> = {}
|
|
237
|
-
for (const [file, content] of Object.entries(sourceFiles)) {
|
|
238
|
-
const normalizedPath = normalizedZipPath(file)
|
|
239
|
-
const lockedHash = normalizedPath ? hashesByPath.get(normalizedPath) : undefined
|
|
240
|
-
if (lockedHash && lockedHash === sha256(content)) {
|
|
241
|
-
omitted = true
|
|
242
|
-
continue
|
|
243
|
-
}
|
|
244
|
-
filtered[file] = content
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return omitted ? zipSync(filtered) : sourceZip
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function normalizedZipPath(file: string): string | null {
|
|
251
|
-
const zipPath = file.replace(/\\/g, '/')
|
|
252
|
-
if (
|
|
253
|
-
zipPath.split('/').includes('..') ||
|
|
254
|
-
path.posix.isAbsolute(zipPath) ||
|
|
255
|
-
path.win32.isAbsolute(zipPath)
|
|
256
|
-
) {
|
|
257
|
-
return null
|
|
258
|
-
}
|
|
259
|
-
return path.posix.normalize(zipPath)
|
|
260
|
-
}
|
|
261
|
-
|
|
262
130
|
/** Compare a stored dependency JSON string against a freshly parsed record. */
|
|
263
131
|
function depsEqual(storedJson: string, next: Record<string, string>): boolean {
|
|
264
132
|
let stored: Record<string, string>
|
|
@@ -271,71 +139,6 @@ function depsEqual(storedJson: string, next: Record<string, string>): boolean {
|
|
|
271
139
|
return keys.length === Object.keys(next).length && keys.every((key) => stored[key] === next[key])
|
|
272
140
|
}
|
|
273
141
|
|
|
274
|
-
async function resolveOneZipFile(cwd: string, zipFilter: string): Promise<string> {
|
|
275
|
-
const absolute = path.resolve(cwd, zipFilter)
|
|
276
|
-
const stat = await maybeStat(absolute)
|
|
277
|
-
if (stat?.isFile()) return assertZipFile(absolute)
|
|
278
|
-
|
|
279
|
-
const files = await listFiles(cwd)
|
|
280
|
-
const matches = files
|
|
281
|
-
.filter((file) => matchesFilter(path.relative(cwd, file), zipFilter))
|
|
282
|
-
.filter(isZipFile)
|
|
283
|
-
.sort()
|
|
284
|
-
|
|
285
|
-
if (matches.length === 0) {
|
|
286
|
-
throw new Error(`No .zip files matched "${zipFilter}"`)
|
|
287
|
-
}
|
|
288
|
-
if (matches.length > 1) {
|
|
289
|
-
throw new Error(`File filter matched ${matches.length} zips; upload one asset at a time`)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return matches[0]
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function assertZipFile(file: string): string {
|
|
296
|
-
if (!isZipFile(file)) throw new Error(`Upload file must be a .zip: ${file}`)
|
|
297
|
-
return file
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function isZipFile(file: string): boolean {
|
|
301
|
-
return /\.zip$/i.test(file)
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
async function maybeStat(file: string) {
|
|
305
|
-
try {
|
|
306
|
-
return await fs.stat(file)
|
|
307
|
-
} catch {
|
|
308
|
-
return null
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
async function listFiles(dir: string): Promise<string[]> {
|
|
313
|
-
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
314
|
-
const files: string[] = []
|
|
315
|
-
for (const entry of entries) {
|
|
316
|
-
if (entry.name === 'node_modules' || entry.name === '.git') continue
|
|
317
|
-
const fullPath = path.join(dir, entry.name)
|
|
318
|
-
if (entry.isDirectory()) {
|
|
319
|
-
files.push(...(await listFiles(fullPath)))
|
|
320
|
-
} else if (entry.isFile()) {
|
|
321
|
-
files.push(fullPath)
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
return files
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function matchesFilter(file: string, filter: string): boolean {
|
|
328
|
-
const normalizedFile = file.split(path.sep).join('/')
|
|
329
|
-
const normalizedFilter = filter.split(path.sep).join('/')
|
|
330
|
-
const pattern =
|
|
331
|
-
'^' +
|
|
332
|
-
escapeRegExp(normalizedFilter)
|
|
333
|
-
.replace(/\\\*\\\*/g, '.*')
|
|
334
|
-
.replace(/\\\*/g, '[^/]*') +
|
|
335
|
-
'$'
|
|
336
|
-
return new RegExp(pattern).test(normalizedFile)
|
|
337
|
-
}
|
|
338
|
-
|
|
339
142
|
function nextVersion(latest?: string): string {
|
|
340
143
|
if (!latest) return '1.0.0'
|
|
341
144
|
const version = semver.inc(latest, 'patch')
|
|
@@ -352,9 +155,5 @@ function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
|
352
155
|
}
|
|
353
156
|
|
|
354
157
|
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
355
|
-
return
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function escapeRegExp(value: string): string {
|
|
359
|
-
return value.replace(/[|\\{}()[\]^$+?.*]/g, '\\$&')
|
|
158
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer
|
|
360
159
|
}
|