@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.
Files changed (70) hide show
  1. package/README.md +4 -1
  2. package/dist/asset-implementation.d.ts +5 -1
  3. package/dist/asset-implementation.d.ts.map +1 -1
  4. package/dist/asset-implementation.js.map +1 -1
  5. package/dist/cli.js +56 -3
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/generate-install.d.ts +6 -0
  8. package/dist/commands/generate-install.d.ts.map +1 -0
  9. package/dist/commands/generate-install.js +29 -0
  10. package/dist/commands/generate-install.js.map +1 -0
  11. package/dist/commands/generate.d.ts +15 -1
  12. package/dist/commands/generate.d.ts.map +1 -1
  13. package/dist/commands/generate.js +57 -20
  14. package/dist/commands/generate.js.map +1 -1
  15. package/dist/commands/pack.d.ts +14 -0
  16. package/dist/commands/pack.d.ts.map +1 -0
  17. package/dist/commands/pack.js +39 -0
  18. package/dist/commands/pack.js.map +1 -0
  19. package/dist/commands/upload.d.ts +3 -13
  20. package/dist/commands/upload.d.ts.map +1 -1
  21. package/dist/commands/upload.js +15 -187
  22. package/dist/commands/upload.js.map +1 -1
  23. package/dist/contract.d.ts +15 -0
  24. package/dist/contract.d.ts.map +1 -1
  25. package/dist/contract.js.map +1 -1
  26. package/dist/generate-job.d.ts +4 -0
  27. package/dist/generate-job.d.ts.map +1 -1
  28. package/dist/generate-job.js +5 -1
  29. package/dist/generate-job.js.map +1 -1
  30. package/dist/generate.d.ts +16 -6
  31. package/dist/generate.d.ts.map +1 -1
  32. package/dist/generate.js +30 -19
  33. package/dist/generate.js.map +1 -1
  34. package/dist/index.d.ts +4 -4
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/output.d.ts +4 -0
  39. package/dist/output.d.ts.map +1 -1
  40. package/dist/output.js +31 -0
  41. package/dist/output.js.map +1 -1
  42. package/dist/pack.d.ts +46 -0
  43. package/dist/pack.d.ts.map +1 -0
  44. package/dist/pack.js +220 -0
  45. package/dist/pack.js.map +1 -0
  46. package/dist/schemas.d.ts +13 -0
  47. package/dist/schemas.d.ts.map +1 -1
  48. package/dist/schemas.js +9 -0
  49. package/dist/schemas.js.map +1 -1
  50. package/dist/skill.d.ts +1 -1
  51. package/dist/skill.d.ts.map +1 -1
  52. package/dist/skill.js +3 -2
  53. package/dist/skill.js.map +1 -1
  54. package/package.json +2 -2
  55. package/skills/market/SKILL.md +3 -2
  56. package/src/asset-implementation.ts +5 -1
  57. package/src/cli.ts +84 -11
  58. package/src/commands/agent.ts +1 -1
  59. package/src/commands/generate-install.ts +36 -0
  60. package/src/commands/generate.ts +79 -31
  61. package/src/commands/pack.ts +55 -0
  62. package/src/commands/upload.ts +18 -219
  63. package/src/contract.ts +7 -0
  64. package/src/generate-job.ts +5 -1
  65. package/src/generate.ts +45 -31
  66. package/src/index.ts +4 -2
  67. package/src/output.ts +35 -0
  68. package/src/pack.ts +294 -0
  69. package/src/schemas.ts +11 -0
  70. 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('<description>', 'Asset prompt')
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
- .action(async (description: string, opts: { type?: AssetType; api?: string; cwd?: string }) => {
213
- requireType(opts.type, 'Generate')
214
- await generateCommand(description, {
215
- type: opts.type,
216
- baseUrl: opts.api,
217
- cwd: opts.cwd,
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) {
@@ -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 '../index.js'
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
+ }
@@ -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 { generateAndWait } from '../generate.js'
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 generated = await generateAndWait(
28
- client,
29
- { description, type: opts.type },
30
- {
31
- onProgress: (msg) => {
32
- spinner.text = msg
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
- onProgress: (msg) => {
46
- spinner.text = msg
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
+ }
@@ -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 { findInstallRoot } from '../install.js'
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 explicitNpmDependencies = parseVersionedDeps(opts.npm ?? [], 'npm')
53
- const explicitAssetDependencies = parseVersionedDeps(opts.asset ?? [], 'asset')
54
- const skillDependencies = parseSkillDeps(opts.skill ?? [])
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 typeMetadata = installMetadata[type]
66
- const sourceFiles = unzipSync(sourceZip)
67
- const packageJsonAssetDependencies = typeMetadata?.readAssetDependenciesFromPackageJson
68
- ? packageJsonAssetDependenciesFromFiles(sourceFiles)
69
- : {}
70
- const assetDependencies = mergeAssetDependencies(
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 new Uint8Array(bytes).buffer
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
  }