@drawcall/market 0.1.26 → 0.1.28
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/asset-implementation.d.ts +1 -0
- package/dist/asset-implementation.d.ts.map +1 -1
- package/dist/cli.js +23 -15
- package/dist/cli.js.map +1 -1
- package/dist/commands/install.d.ts +8 -4
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +24 -59
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/preview.d.ts +0 -2
- package/dist/commands/preview.d.ts.map +1 -1
- package/dist/commands/preview.js +4 -6
- package/dist/commands/preview.js.map +1 -1
- package/dist/commands/upload.d.ts +18 -0
- package/dist/commands/upload.d.ts.map +1 -1
- package/dist/commands/upload.js +67 -3
- package/dist/commands/upload.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/contract.d.ts +3 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +11 -2
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +69 -19
- package/dist/install.js.map +1 -1
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +6 -0
- package/dist/output.js.map +1 -1
- package/dist/resolve.d.ts +6 -3
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +66 -20
- package/dist/resolve.js.map +1 -1
- package/dist/schemas.d.ts +2 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +5 -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 +19 -12
- package/dist/skill.js.map +1 -1
- package/package.json +1 -1
- package/src/asset-implementation.ts +1 -0
- package/src/cli.ts +44 -33
- package/src/commands/install.ts +30 -77
- package/src/commands/preview.ts +5 -10
- package/src/commands/upload.ts +79 -3
- package/src/config.ts +6 -0
- package/src/contract.ts +2 -0
- package/src/index.ts +1 -0
- package/src/install.ts +95 -19
- package/src/output.ts +9 -0
- package/src/resolve.ts +81 -22
- package/src/schemas.ts +6 -0
- package/src/skill.ts +19 -12
- package/tests/install-command.test.ts +13 -46
- package/tests/install-layout.test.ts +60 -2
- package/tests/output.test.ts +18 -0
- package/tests/resolve-skills.test.ts +87 -0
- package/tests/upload-deps.test.ts +44 -0
package/dist/skill.js
CHANGED
|
@@ -5,39 +5,46 @@ description: Find, preview, install, generate, and publish Drawcall Market asset
|
|
|
5
5
|
|
|
6
6
|
# Drawcall Market
|
|
7
7
|
|
|
8
|
-
Use the \`market\` CLI. Keep commands short and read the summary
|
|
8
|
+
Use the \`market\` CLI. Keep commands short and read the summary lines.
|
|
9
9
|
|
|
10
10
|
## Quick Start
|
|
11
11
|
|
|
12
12
|
\`\`\`sh
|
|
13
13
|
market search "wooden chair" --type model --limit 3
|
|
14
|
-
market install wooden-chair --
|
|
14
|
+
market install wooden-chair --cwd "$PWD"
|
|
15
15
|
market preview wooden-chair --out /tmp/wooden-chair.png
|
|
16
16
|
\`\`\`
|
|
17
17
|
|
|
18
18
|
## Workflow
|
|
19
19
|
|
|
20
|
-
1. Search first unless the user gave an exact asset name.
|
|
21
|
-
2.
|
|
22
|
-
3.
|
|
23
|
-
4.
|
|
20
|
+
1. Search first unless the user already gave an exact asset name. \`search\` requires \`--type\`; use \`model\` unless the user names another supported type: \`humanoid-model\`, \`texture\`, \`humanoid-animation\`, \`template\`, \`sound-effect\`, \`background-music\`, or \`environment\`.
|
|
21
|
+
2. Use \`--limit 1\` for lookup, \`--limit 3\` for choice. Search caps at 5. Add \`--verbose\` only when the one-line descriptions are not enough.
|
|
22
|
+
3. \`install\` takes one or more exact asset names (optionally \`name@range\`); it does not search or generate. Find names with \`search\` first. No \`--type\` is needed — asset names are unique.
|
|
23
|
+
4. \`preview <name>\` saves the preview image; no \`--type\` is needed. Not every type has previews (e.g. \`humanoid-animation\`, \`template\`, \`sound-effect\`, \`background-music\`); the CLI reports when one is unavailable.
|
|
24
24
|
5. Use \`--unapproved\` only when the user asks for unapproved/private/admin assets. Do not install unapproved assets without explicit acceptance.
|
|
25
|
-
6.
|
|
26
|
-
7.
|
|
27
|
-
8.
|
|
28
|
-
9. Installed \`environment\` assets contain \`public/environment/<name>.hdr\` for Three.js IBL lighting and \`public/environment/<name>-background.webp\` for the visible equirectangular background. Use \`market preview\` to fetch the preview image separately.
|
|
25
|
+
6. \`generate --type <type> "<prompt>"\` creates a new asset; it requires login and a type that supports generation. No asset type currently supports generation.
|
|
26
|
+
7. Upload only when publishing is requested: \`market upload <name> <zip> "<description>" --type <type>\`. Declare dependencies with repeatable flags: \`--npm name@range\`, \`--asset name@range\`, \`--skill label=source\` (source is a GitHub/git ref or a local path to a skill directory inside the zip). Example: \`market upload my-scene scene.zip "A scene" --type model --npm three@^0.178.0 --skill web-design=vercel-labs/agent-skills\`.
|
|
27
|
+
8. Installed \`environment\` assets contain \`public/environment/<name>.hdr\` for Three.js IBL lighting and \`public/environment/<name>-background.webp\` for the visible equirectangular background. Use \`market preview\` to fetch the preview image separately.
|
|
29
28
|
|
|
30
29
|
## Output
|
|
31
30
|
|
|
32
|
-
Commands print concise summaries:
|
|
31
|
+
Commands print concise, line-oriented summaries:
|
|
33
32
|
|
|
34
33
|
\`\`\`text
|
|
35
34
|
Results: 2/8 query="wooden chair" type=model approval=approved
|
|
36
35
|
- wooden-chair@1.0.0 | model | approved | Low-poly wooden chair
|
|
37
|
-
Installed
|
|
36
|
+
Installed:
|
|
37
|
+
- wooden-chair@1.0.0 (asset)
|
|
38
|
+
files:
|
|
39
|
+
public/model
|
|
40
|
+
└─ wooden-chair.glb
|
|
41
|
+
- three@^0.178.0 (npm)
|
|
42
|
+
- web-design-guidelines ← vercel-labs/agent-skills/tree/main/skills/web-design-guidelines (skill)
|
|
38
43
|
Saved preview for wooden-chair@1.0.0: /tmp/wooden-chair.png
|
|
39
44
|
\`\`\`
|
|
40
45
|
|
|
46
|
+
Assets may also declare \`skill\` dependencies, installed for you via the \`skills\` CLI (\`skills add\`) during \`install\`. Sources are either a GitHub/git ref or a local path to a skill directory shipped inside the asset. This requires \`npx\` to be available.
|
|
47
|
+
|
|
41
48
|
If search returns no results, try one broader noun phrase. If a command returns \`Error: Not logged in...\`, ask before running \`market login\`.
|
|
42
49
|
`;
|
|
43
50
|
//# sourceMappingURL=skill.js.map
|
package/dist/skill.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill.js","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG
|
|
1
|
+
{"version":3,"file":"skill.js","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgD1B,CAAA"}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -24,6 +24,9 @@ const DEFAULT_BASE_URL = 'https://api.market.drawcall.ai'
|
|
|
24
24
|
const AUTH_ISSUER_URL = 'https://auth.drawcall.ai/api/auth'
|
|
25
25
|
const DEVICE_CLIENT_ID = 'market-cli'
|
|
26
26
|
|
|
27
|
+
/** Accumulate a repeatable option into an array. */
|
|
28
|
+
const collect = (value: string, previous: string[]): string[] => previous.concat(value)
|
|
29
|
+
|
|
27
30
|
const typeOption = new Option('--type <type>', 'Asset type').choices([...ASSET_TYPES])
|
|
28
31
|
const apiOption = new Option('--api <url>', 'API URL').default(
|
|
29
32
|
process.env.MARKET_API_URL,
|
|
@@ -56,7 +59,10 @@ program
|
|
|
56
59
|
const client = createMarketClient({ baseUrl, authToken: token })
|
|
57
60
|
const profile = await client.user.getProfile()
|
|
58
61
|
if (!profile) throw new Error('The Market API did not accept the auth token.')
|
|
59
|
-
|
|
62
|
+
// Only pin baseUrl in config when the user explicitly chose one. Persisting
|
|
63
|
+
// the default would freeze logged-in users to a host the CLI can no longer
|
|
64
|
+
// change in a future release.
|
|
65
|
+
await saveConfig(opts.api ? { authToken: token, baseUrl: opts.api } : { authToken: token })
|
|
60
66
|
console.log(loginResult(profile.email, getConfigPath()))
|
|
61
67
|
})
|
|
62
68
|
|
|
@@ -69,25 +75,18 @@ program
|
|
|
69
75
|
|
|
70
76
|
program
|
|
71
77
|
.command('install')
|
|
72
|
-
.description('Install by name
|
|
73
|
-
.argument('<assets...>', '
|
|
74
|
-
.addOption(typeOption)
|
|
78
|
+
.description('Install assets by name')
|
|
79
|
+
.argument('<assets...>', 'Asset names, optionally with @range')
|
|
75
80
|
.addOption(apiOption)
|
|
76
81
|
.option('--unapproved', 'Include unapproved versions', false)
|
|
77
82
|
.option('--cwd <dir>', 'Project directory')
|
|
78
|
-
.action(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
baseUrl: opts.api,
|
|
86
|
-
unapproved: opts.unapproved,
|
|
87
|
-
cwd: opts.cwd,
|
|
88
|
-
})
|
|
89
|
-
},
|
|
90
|
-
)
|
|
83
|
+
.action(async (args: string[], opts: { api?: string; unapproved: boolean; cwd?: string }) => {
|
|
84
|
+
await installCommand(args, {
|
|
85
|
+
baseUrl: opts.api,
|
|
86
|
+
unapproved: opts.unapproved,
|
|
87
|
+
cwd: opts.cwd,
|
|
88
|
+
})
|
|
89
|
+
})
|
|
91
90
|
|
|
92
91
|
program
|
|
93
92
|
.command('search')
|
|
@@ -109,12 +108,7 @@ program
|
|
|
109
108
|
verbose: boolean
|
|
110
109
|
},
|
|
111
110
|
) => {
|
|
112
|
-
|
|
113
|
-
console.error(
|
|
114
|
-
errorResult(`Search requires --type. Available types: ${ASSET_TYPES.join(', ')}`),
|
|
115
|
-
)
|
|
116
|
-
process.exit(1)
|
|
117
|
-
}
|
|
111
|
+
requireType(opts.type, 'Search')
|
|
118
112
|
await searchCommand(query, {
|
|
119
113
|
type: opts.type,
|
|
120
114
|
baseUrl: opts.api,
|
|
@@ -135,24 +129,33 @@ program
|
|
|
135
129
|
.addOption(apiOption)
|
|
136
130
|
.option('--version <version>', 'Explicit semver version')
|
|
137
131
|
.option('--cwd <dir>', 'Project directory')
|
|
132
|
+
.option('--npm <dep>', 'npm dependency name@range (repeatable)', collect, [])
|
|
133
|
+
.option('--asset <dep>', 'asset dependency name@range (repeatable)', collect, [])
|
|
134
|
+
.option('--skill <dep>', 'skill dependency label=source (repeatable)', collect, [])
|
|
138
135
|
.action(
|
|
139
136
|
async (
|
|
140
137
|
name: string,
|
|
141
138
|
zipFilter: string,
|
|
142
139
|
description: string,
|
|
143
|
-
opts: {
|
|
140
|
+
opts: {
|
|
141
|
+
type?: AssetType
|
|
142
|
+
api?: string
|
|
143
|
+
version?: string
|
|
144
|
+
cwd?: string
|
|
145
|
+
npm: string[]
|
|
146
|
+
asset: string[]
|
|
147
|
+
skill: string[]
|
|
148
|
+
},
|
|
144
149
|
) => {
|
|
145
|
-
|
|
146
|
-
console.error(
|
|
147
|
-
errorResult(`Upload requires --type. Available types: ${ASSET_TYPES.join(', ')}`),
|
|
148
|
-
)
|
|
149
|
-
process.exit(1)
|
|
150
|
-
}
|
|
150
|
+
requireType(opts.type, 'Upload')
|
|
151
151
|
await uploadCommand(name, zipFilter, description, {
|
|
152
152
|
type: opts.type,
|
|
153
153
|
version: opts.version,
|
|
154
154
|
baseUrl: opts.api,
|
|
155
155
|
cwd: opts.cwd,
|
|
156
|
+
npm: opts.npm,
|
|
157
|
+
asset: opts.asset,
|
|
158
|
+
skill: opts.skill,
|
|
156
159
|
})
|
|
157
160
|
},
|
|
158
161
|
)
|
|
@@ -162,7 +165,6 @@ program
|
|
|
162
165
|
.description('Save preview image')
|
|
163
166
|
.argument('<name>', 'Asset name')
|
|
164
167
|
.argument('[version]', 'Semver version')
|
|
165
|
-
.addOption(typeOption)
|
|
166
168
|
.addOption(apiOption)
|
|
167
169
|
.option('--unapproved', 'Include unapproved versions', false)
|
|
168
170
|
.option('--out <file>', 'Output image path')
|
|
@@ -170,10 +172,9 @@ program
|
|
|
170
172
|
async (
|
|
171
173
|
name: string,
|
|
172
174
|
version: string | undefined,
|
|
173
|
-
opts: {
|
|
175
|
+
opts: { api?: string; unapproved: boolean; out?: string },
|
|
174
176
|
) => {
|
|
175
177
|
await previewCommand(name, version, {
|
|
176
|
-
type: opts.type,
|
|
177
178
|
baseUrl: opts.api,
|
|
178
179
|
unapproved: opts.unapproved,
|
|
179
180
|
out: opts.out,
|
|
@@ -189,6 +190,7 @@ program
|
|
|
189
190
|
.addOption(apiOption)
|
|
190
191
|
.option('--cwd <dir>', 'Project directory')
|
|
191
192
|
.action(async (description: string, opts: { type?: AssetType; api?: string; cwd?: string }) => {
|
|
193
|
+
requireType(opts.type, 'Generate')
|
|
192
194
|
await generateCommand(description, {
|
|
193
195
|
type: opts.type,
|
|
194
196
|
baseUrl: opts.api,
|
|
@@ -207,6 +209,15 @@ program.parseAsync().catch((err) => {
|
|
|
207
209
|
process.exit(1)
|
|
208
210
|
})
|
|
209
211
|
|
|
212
|
+
function requireType(type: AssetType | undefined, command: string): asserts type is AssetType {
|
|
213
|
+
if (!type) {
|
|
214
|
+
console.error(
|
|
215
|
+
errorResult(`${command} requires --type. Available types: ${ASSET_TYPES.join(', ')}`),
|
|
216
|
+
)
|
|
217
|
+
process.exit(1)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
210
221
|
function parseSearchLimit(value: string): number {
|
|
211
222
|
const parsed = Number.parseInt(value, 10)
|
|
212
223
|
if (!Number.isInteger(parsed) || parsed < 1) {
|
package/src/commands/install.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import ora
|
|
1
|
+
import ora from 'ora'
|
|
2
2
|
import { resolve } from '../resolve.js'
|
|
3
3
|
import { install as runInstall } from '../install.js'
|
|
4
|
-
import {
|
|
5
|
-
import { assetNameSchema, type AssetType } from '../schemas.js'
|
|
4
|
+
import { assetNameSchema } from '../schemas.js'
|
|
6
5
|
import { getCliClient } from '../cli-client.js'
|
|
7
|
-
import {
|
|
6
|
+
import { installResult } from '../output.js'
|
|
8
7
|
import type { MarketClient } from '../client.js'
|
|
9
8
|
|
|
10
9
|
export interface InstallCommandOptions {
|
|
11
|
-
type?: AssetType
|
|
12
10
|
unapproved?: boolean
|
|
13
11
|
cwd?: string
|
|
14
12
|
baseUrl?: string
|
|
@@ -20,8 +18,8 @@ interface AssetRequest {
|
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
export async function installCommand(args: string[], opts: InstallCommandOptions): Promise<void> {
|
|
23
|
-
const { client
|
|
24
|
-
const
|
|
21
|
+
const { client } = await getCliClient({ baseUrl: opts.baseUrl })
|
|
22
|
+
const includeUnapproved = opts.unapproved ?? false
|
|
25
23
|
|
|
26
24
|
const spinner = ora({
|
|
27
25
|
isEnabled: Boolean(process.stderr.isTTY),
|
|
@@ -31,15 +29,11 @@ export async function installCommand(args: string[], opts: InstallCommandOptions
|
|
|
31
29
|
const requests: AssetRequest[] = []
|
|
32
30
|
for (const arg of args) {
|
|
33
31
|
spinner.text = `Resolving "${truncate(arg, 40)}"`
|
|
34
|
-
requests.push(
|
|
35
|
-
await resolveArg(client, arg, opts.type, spinner, canGenerate, opts.unapproved ?? false),
|
|
36
|
-
)
|
|
32
|
+
requests.push(await resolveArg(client, arg, includeUnapproved))
|
|
37
33
|
}
|
|
38
34
|
|
|
39
35
|
spinner.text = 'Resolving dependency tree'
|
|
40
|
-
const resolution = await resolve(client.asset, requests, {
|
|
41
|
-
includeUnapproved: opts.unapproved ?? false,
|
|
42
|
-
})
|
|
36
|
+
const resolution = await resolve(client.asset, requests, { includeUnapproved })
|
|
43
37
|
|
|
44
38
|
const result = await runInstall(client, resolution, {
|
|
45
39
|
cwd: opts.cwd,
|
|
@@ -56,82 +50,41 @@ export async function installCommand(args: string[], opts: InstallCommandOptions
|
|
|
56
50
|
}
|
|
57
51
|
}
|
|
58
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a single `name` or `name@range` argument to an exact asset request.
|
|
55
|
+
* Asset names are globally unique, so the name fully determines the asset — no
|
|
56
|
+
* `--type` is needed. install never fuzzy-searches or generates: it points the
|
|
57
|
+
* caller at `market search` / `market generate` instead, which keeps behavior
|
|
58
|
+
* predictable for agents.
|
|
59
|
+
*/
|
|
59
60
|
export async function resolveArg(
|
|
60
61
|
client: MarketClient,
|
|
61
62
|
arg: string,
|
|
62
|
-
type: AssetType | undefined,
|
|
63
|
-
spinner: Ora,
|
|
64
|
-
canGenerate: boolean,
|
|
65
63
|
includeUnapproved: boolean,
|
|
66
64
|
): Promise<AssetRequest> {
|
|
67
65
|
const parsed = parseNameAndRange(arg)
|
|
68
|
-
if (parsed) {
|
|
69
|
-
|
|
70
|
-
name
|
|
71
|
-
|
|
72
|
-
includeUnapproved,
|
|
73
|
-
})
|
|
74
|
-
if (existing) return parsed
|
|
75
|
-
|
|
76
|
-
if (!includeUnapproved) {
|
|
77
|
-
const hidden = await client.asset.exact({
|
|
78
|
-
name: parsed.name,
|
|
79
|
-
type,
|
|
80
|
-
includeUnapproved: true,
|
|
81
|
-
})
|
|
82
|
-
if (hidden) {
|
|
83
|
-
throw new Error(
|
|
84
|
-
`Asset "${parsed.name}" exists but has no approved versions. Re-run with \`--unapproved\` if you have access.`,
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (arg.includes('@')) {
|
|
90
|
-
return parsed
|
|
91
|
-
}
|
|
66
|
+
if (!parsed) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Invalid asset name "${arg}". Use \`market search --type <type> <query>\` to find assets.`,
|
|
69
|
+
)
|
|
92
70
|
}
|
|
93
71
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
query: arg,
|
|
97
|
-
type,
|
|
98
|
-
includeUnapproved,
|
|
99
|
-
limit: 1,
|
|
100
|
-
page: 1,
|
|
101
|
-
sort: 'relevance',
|
|
102
|
-
})
|
|
103
|
-
if (results.items.length > 0) {
|
|
104
|
-
const hit = results.items[0]
|
|
105
|
-
if (process.stderr.isTTY) {
|
|
106
|
-
spinner.info(`Matched "${compact(arg, 48)}" to ${hit.name} (${hit.type})`)
|
|
107
|
-
spinner.start()
|
|
108
|
-
}
|
|
109
|
-
return { name: hit.name, range: '*' }
|
|
110
|
-
}
|
|
72
|
+
const existing = await client.asset.exact({ name: parsed.name, includeUnapproved })
|
|
73
|
+
if (existing) return parsed
|
|
111
74
|
|
|
112
|
-
if (!
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
75
|
+
if (!includeUnapproved) {
|
|
76
|
+
const hidden = await client.asset.exact({ name: parsed.name, includeUnapproved: true })
|
|
77
|
+
if (hidden) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Asset "${parsed.name}" exists but has no approved versions. Re-run with \`--unapproved\` if you have access.`,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
116
82
|
}
|
|
117
83
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
{ description: arg, type },
|
|
122
|
-
{
|
|
123
|
-
onProgress: (msg) => {
|
|
124
|
-
spinner.text = msg
|
|
125
|
-
},
|
|
126
|
-
},
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Asset "${parsed.name}" not found. Use \`market search\` to find assets or ` +
|
|
86
|
+
`\`market generate --type <type>\` to create one.`,
|
|
127
87
|
)
|
|
128
|
-
if (process.stderr.isTTY) {
|
|
129
|
-
spinner.info(
|
|
130
|
-
`${generatedInstallResult(generated.assetName, generated.version)} for "${compact(arg, 48)}"`,
|
|
131
|
-
)
|
|
132
|
-
spinner.start()
|
|
133
|
-
}
|
|
134
|
-
return { name: generated.assetName, range: generated.version }
|
|
135
88
|
}
|
|
136
89
|
|
|
137
90
|
function parseNameAndRange(arg: string): AssetRequest | null {
|
package/src/commands/preview.ts
CHANGED
|
@@ -3,10 +3,9 @@ import * as path from 'path'
|
|
|
3
3
|
import ora from 'ora'
|
|
4
4
|
import { getCliClient } from '../cli-client.js'
|
|
5
5
|
import { previewResult } from '../output.js'
|
|
6
|
-
import { assetNameSchema, semverSchema
|
|
6
|
+
import { assetNameSchema, semverSchema } from '../schemas.js'
|
|
7
7
|
|
|
8
8
|
export interface PreviewCommandOptions {
|
|
9
|
-
type?: AssetType
|
|
10
9
|
unapproved?: boolean
|
|
11
10
|
out?: string
|
|
12
11
|
baseUrl?: string
|
|
@@ -19,13 +18,10 @@ export async function previewCommand(
|
|
|
19
18
|
): Promise<void> {
|
|
20
19
|
const parsedName = assetNameSchema.parse(name)
|
|
21
20
|
const parsedVersion = version ? semverSchema.parse(version) : undefined
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
) {
|
|
27
|
-
throw new Error(`Preview images are not supported for ${opts.type}`)
|
|
28
|
-
}
|
|
21
|
+
// The asset name is globally unique, so the server resolves the type. Whether
|
|
22
|
+
// that type supports preview images is also a server concern: the API checks
|
|
23
|
+
// for a provider `downloadPreviewImage` capability and returns a clear,
|
|
24
|
+
// type-specific error rather than us mirroring a list that can drift.
|
|
29
25
|
|
|
30
26
|
const spinner = ora({
|
|
31
27
|
text: `Resolving preview for ${parsedName}`,
|
|
@@ -40,7 +36,6 @@ export async function previewCommand(
|
|
|
40
36
|
(
|
|
41
37
|
await client.asset.exact({
|
|
42
38
|
name: parsedName,
|
|
43
|
-
type: opts.type,
|
|
44
39
|
includeUnapproved: opts.unapproved ?? false,
|
|
45
40
|
})
|
|
46
41
|
)?.latestVersion
|
package/src/commands/upload.ts
CHANGED
|
@@ -12,6 +12,12 @@ export interface UploadCommandOptions {
|
|
|
12
12
|
version?: string
|
|
13
13
|
cwd?: string
|
|
14
14
|
baseUrl?: string
|
|
15
|
+
/** npm dependencies, each `name@range` (range defaults to `*`). */
|
|
16
|
+
npm?: string[]
|
|
17
|
+
/** asset dependencies, each `name@range` (range defaults to `*`). */
|
|
18
|
+
asset?: string[]
|
|
19
|
+
/** skill dependencies, each `label=source` passed to `skills add`. */
|
|
20
|
+
skill?: string[]
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
export async function uploadCommand(
|
|
@@ -29,6 +35,10 @@ export async function uploadCommand(
|
|
|
29
35
|
try {
|
|
30
36
|
const parsedName = assetNameSchema.parse(name)
|
|
31
37
|
const parsedVersion = opts.version ? semverSchema.parse(opts.version) : undefined
|
|
38
|
+
// Parse dep flags before any network work so a malformed spec fails fast.
|
|
39
|
+
const npmDependencies = parseVersionedDeps(opts.npm ?? [], 'npm')
|
|
40
|
+
const assetDependencies = parseVersionedDeps(opts.asset ?? [], 'asset')
|
|
41
|
+
const skillDependencies = parseSkillDeps(opts.skill ?? [])
|
|
32
42
|
const cwd = opts.cwd ?? process.cwd()
|
|
33
43
|
const type = opts.type
|
|
34
44
|
const zipFile = await resolveOneZipFile(cwd, zipFilter)
|
|
@@ -58,7 +68,13 @@ export async function uploadCommand(
|
|
|
58
68
|
version: existing.latestVersion,
|
|
59
69
|
})
|
|
60
70
|
const latestBytes = new Uint8Array(await latestZip.arrayBuffer())
|
|
61
|
-
if (
|
|
71
|
+
if (
|
|
72
|
+
existing.description === description &&
|
|
73
|
+
depsEqual(existing.npmDependencies, npmDependencies) &&
|
|
74
|
+
depsEqual(existing.assetDependencies, assetDependencies) &&
|
|
75
|
+
depsEqual(existing.skillDependencies, skillDependencies) &&
|
|
76
|
+
bytesEqual(latestBytes, zip)
|
|
77
|
+
) {
|
|
62
78
|
spinner.stop()
|
|
63
79
|
console.log(unchangedUploadResult(parsedName, existing.latestVersion))
|
|
64
80
|
return
|
|
@@ -72,8 +88,9 @@ export async function uploadCommand(
|
|
|
72
88
|
type,
|
|
73
89
|
version,
|
|
74
90
|
description,
|
|
75
|
-
npmDependencies
|
|
76
|
-
assetDependencies
|
|
91
|
+
npmDependencies,
|
|
92
|
+
assetDependencies,
|
|
93
|
+
skillDependencies,
|
|
77
94
|
tags: [],
|
|
78
95
|
zip: new File([toArrayBuffer(zip)], `${parsedName}-${version}.zip`, {
|
|
79
96
|
type: 'application/zip',
|
|
@@ -88,6 +105,65 @@ export async function uploadCommand(
|
|
|
88
105
|
}
|
|
89
106
|
}
|
|
90
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Parse `name@range` specs (npm or asset deps) into a name→range record. The
|
|
110
|
+
* range is optional and defaults to `*`. A leading `@` is treated as a scope
|
|
111
|
+
* marker, so `@scope/pkg@^1.0.0` splits into `@scope/pkg` and `^1.0.0`.
|
|
112
|
+
*/
|
|
113
|
+
export function parseVersionedDeps(specs: string[], kind: 'npm' | 'asset'): Record<string, string> {
|
|
114
|
+
const out: Record<string, string> = {}
|
|
115
|
+
for (const spec of specs) {
|
|
116
|
+
const at = spec.lastIndexOf('@')
|
|
117
|
+
const hasRange = at > 0
|
|
118
|
+
const name = hasRange ? spec.slice(0, at) : spec
|
|
119
|
+
const range = hasRange ? spec.slice(at + 1) : '*'
|
|
120
|
+
if (!name || !range) {
|
|
121
|
+
throw new Error(`Invalid ${kind} dependency "${spec}". Use name@range (e.g. three@^0.178.0).`)
|
|
122
|
+
}
|
|
123
|
+
if (name in out) {
|
|
124
|
+
throw new Error(`Duplicate ${kind} dependency "${name}".`)
|
|
125
|
+
}
|
|
126
|
+
out[name] = range
|
|
127
|
+
}
|
|
128
|
+
return out
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Parse `label=source` specs into a label→source record. The source is passed
|
|
133
|
+
* verbatim to `skills add` (a GitHub/git ref or a local path), so only the
|
|
134
|
+
* first `=` is treated as the separator.
|
|
135
|
+
*/
|
|
136
|
+
export function parseSkillDeps(specs: string[]): Record<string, string> {
|
|
137
|
+
const out: Record<string, string> = {}
|
|
138
|
+
for (const spec of specs) {
|
|
139
|
+
const eq = spec.indexOf('=')
|
|
140
|
+
if (eq <= 0 || eq === spec.length - 1) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Invalid skill dependency "${spec}". Use label=source ` +
|
|
143
|
+
`(e.g. web-design=vercel-labs/agent-skills).`,
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
const label = spec.slice(0, eq)
|
|
147
|
+
if (label in out) {
|
|
148
|
+
throw new Error(`Duplicate skill dependency "${label}".`)
|
|
149
|
+
}
|
|
150
|
+
out[label] = spec.slice(eq + 1)
|
|
151
|
+
}
|
|
152
|
+
return out
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Compare a stored dependency JSON string against a freshly parsed record. */
|
|
156
|
+
function depsEqual(storedJson: string, next: Record<string, string>): boolean {
|
|
157
|
+
let stored: Record<string, string>
|
|
158
|
+
try {
|
|
159
|
+
stored = JSON.parse(storedJson || '{}') as Record<string, string>
|
|
160
|
+
} catch {
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
const keys = Object.keys(stored)
|
|
164
|
+
return keys.length === Object.keys(next).length && keys.every((key) => stored[key] === next[key])
|
|
165
|
+
}
|
|
166
|
+
|
|
91
167
|
async function resolveOneZipFile(cwd: string, zipFilter: string): Promise<string> {
|
|
92
168
|
const absolute = path.resolve(cwd, zipFilter)
|
|
93
169
|
const stat = await maybeStat(absolute)
|
package/src/config.ts
CHANGED
|
@@ -41,8 +41,14 @@ export async function loadConfig(): Promise<Config | null> {
|
|
|
41
41
|
export async function saveConfig(config: Config): Promise<void> {
|
|
42
42
|
const dir = configDir()
|
|
43
43
|
await fs.mkdir(dir, { recursive: true })
|
|
44
|
+
// `mode` on writeFile/mkdir only applies when the path is created, so chmod
|
|
45
|
+
// explicitly to protect a pre-existing (possibly world-readable) token file
|
|
46
|
+
// and its directory. A chmod failure means we could not secure the token, so
|
|
47
|
+
// let it surface rather than silently leaving it exposed.
|
|
48
|
+
await fs.chmod(dir, 0o700)
|
|
44
49
|
const file = configPath()
|
|
45
50
|
await fs.writeFile(file, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 })
|
|
51
|
+
await fs.chmod(file, 0o600)
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
export async function clearConfig(): Promise<boolean> {
|
package/src/contract.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface AssetVersion {
|
|
|
17
17
|
approved: boolean
|
|
18
18
|
npmDependencies: string
|
|
19
19
|
assetDependencies: string
|
|
20
|
+
skillDependencies: string
|
|
20
21
|
sourceKey: string
|
|
21
22
|
createdAt: Date
|
|
22
23
|
}
|
|
@@ -43,6 +44,7 @@ export interface AssetSearchResult {
|
|
|
43
44
|
approved: boolean
|
|
44
45
|
npmDependencies: string
|
|
45
46
|
assetDependencies: string
|
|
47
|
+
skillDependencies: string
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
export interface PaginatedList<T> {
|