@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.
Files changed (64) hide show
  1. package/dist/asset-implementation.d.ts +1 -0
  2. package/dist/asset-implementation.d.ts.map +1 -1
  3. package/dist/cli.js +23 -15
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/install.d.ts +8 -4
  6. package/dist/commands/install.d.ts.map +1 -1
  7. package/dist/commands/install.js +24 -59
  8. package/dist/commands/install.js.map +1 -1
  9. package/dist/commands/preview.d.ts +0 -2
  10. package/dist/commands/preview.d.ts.map +1 -1
  11. package/dist/commands/preview.js +4 -6
  12. package/dist/commands/preview.js.map +1 -1
  13. package/dist/commands/upload.d.ts +18 -0
  14. package/dist/commands/upload.d.ts.map +1 -1
  15. package/dist/commands/upload.js +67 -3
  16. package/dist/commands/upload.js.map +1 -1
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +6 -0
  19. package/dist/config.js.map +1 -1
  20. package/dist/contract.d.ts +3 -0
  21. package/dist/contract.d.ts.map +1 -1
  22. package/dist/contract.js.map +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/install.d.ts +11 -2
  28. package/dist/install.d.ts.map +1 -1
  29. package/dist/install.js +69 -19
  30. package/dist/install.js.map +1 -1
  31. package/dist/output.d.ts.map +1 -1
  32. package/dist/output.js +6 -0
  33. package/dist/output.js.map +1 -1
  34. package/dist/resolve.d.ts +6 -3
  35. package/dist/resolve.d.ts.map +1 -1
  36. package/dist/resolve.js +66 -20
  37. package/dist/resolve.js.map +1 -1
  38. package/dist/schemas.d.ts +2 -0
  39. package/dist/schemas.d.ts.map +1 -1
  40. package/dist/schemas.js +5 -0
  41. package/dist/schemas.js.map +1 -1
  42. package/dist/skill.d.ts +1 -1
  43. package/dist/skill.d.ts.map +1 -1
  44. package/dist/skill.js +19 -12
  45. package/dist/skill.js.map +1 -1
  46. package/package.json +1 -1
  47. package/src/asset-implementation.ts +1 -0
  48. package/src/cli.ts +44 -33
  49. package/src/commands/install.ts +30 -77
  50. package/src/commands/preview.ts +5 -10
  51. package/src/commands/upload.ts +79 -3
  52. package/src/config.ts +6 -0
  53. package/src/contract.ts +2 -0
  54. package/src/index.ts +1 -0
  55. package/src/install.ts +95 -19
  56. package/src/output.ts +9 -0
  57. package/src/resolve.ts +81 -22
  58. package/src/schemas.ts +6 -0
  59. package/src/skill.ts +19 -12
  60. package/tests/install-command.test.ts +13 -46
  61. package/tests/install-layout.test.ts +60 -2
  62. package/tests/output.test.ts +18 -0
  63. package/tests/resolve-skills.test.ts +87 -0
  64. 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 line plus asset bullets.
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 --type model --cwd "$PWD"
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. Search requires \`--type\`; use \`model\` unless the user names another supported type: \`humanoid-model\`, \`texture\`, \`humanoid-animation\`, \`template\`, \`sound-effect\`, \`background-music\`, or \`environment\`.
22
- 3. Use \`--limit 1\` for lookup, \`--limit 3\` for choice. Search caps at 5.
23
- 4. Use \`--verbose\` only when the one-line descriptions are not enough.
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. Preview before installing when visual fit matters: \`market preview <name> --out <file>\`. Preview is not supported for \`humanoid-animation\`, \`template\`, \`sound-effect\`, or \`background-music\`.
26
- 7. Generate only when the user wants a new asset, auth is available, and the requested asset type supports generation. No asset type currently supports generation.
27
- 8. Upload only when publishing is requested.
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 1 asset: wooden-chair@1.0.0
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyC1B,CAAA"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawcall/market",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/drawcall-ai/market",
@@ -28,6 +28,7 @@ export interface AssetUploadZipInput {
28
28
  description?: string
29
29
  npmDependencies: Record<string, string>
30
30
  assetDependencies: Record<string, string>
31
+ skillDependencies: Record<string, string>
31
32
  tags: string[]
32
33
  zip: File
33
34
  }
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
- await saveConfig({ authToken: token, baseUrl })
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 or prompt')
73
- .argument('<assets...>', 'Names or prompts')
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
- async (
80
- args: string[],
81
- opts: { type?: AssetType; api?: string; unapproved: boolean; cwd?: string },
82
- ) => {
83
- await installCommand(args, {
84
- type: opts.type,
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
- if (!opts.type) {
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: { type?: AssetType; api?: string; version?: string; cwd?: string },
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
- if (!opts.type) {
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: { type?: AssetType; api?: string; unapproved: boolean; out?: string },
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) {
@@ -1,14 +1,12 @@
1
- import ora, { type Ora } from 'ora'
1
+ import ora from 'ora'
2
2
  import { resolve } from '../resolve.js'
3
3
  import { install as runInstall } from '../install.js'
4
- import { generateAndWait } from '../generate.js'
5
- import { assetNameSchema, type AssetType } from '../schemas.js'
4
+ import { assetNameSchema } from '../schemas.js'
6
5
  import { getCliClient } from '../cli-client.js'
7
- import { compact, generatedInstallResult, installResult } from '../output.js'
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, authToken } = await getCliClient({ baseUrl: opts.baseUrl })
24
- const canGenerate = Boolean(authToken)
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
- const existing = await client.asset.exact({
70
- name: parsed.name,
71
- type,
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
- spinner.text = `Searching "${truncate(arg, 40)}"`
95
- const results = await client.asset.search({
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 (!canGenerate) {
113
- throw new Error(
114
- `No matches for "${arg}". Run \`market login\` to enable auto-generation of missing assets.`,
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
- spinner.text = `No matches; generating "${truncate(arg, 40)}"`
119
- const generated = await generateAndWait(
120
- client,
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 {
@@ -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, type AssetType } from '../schemas.js'
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
- if (
23
- opts.type === 'humanoid-animation' ||
24
- opts.type === 'template' ||
25
- opts.type === 'sound-effect'
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
@@ -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 (existing.description === description && bytesEqual(latestBytes, zip)) {
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> {
package/src/index.ts CHANGED
@@ -43,6 +43,7 @@ export {
43
43
  assetNameSchema,
44
44
  npmDependenciesSchema,
45
45
  assetDependenciesSchema,
46
+ skillDependenciesSchema,
46
47
  uploadZipSchema,
47
48
  downloadZipSchema,
48
49
  exactAssetSchema,