@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/contract.ts CHANGED
@@ -49,6 +49,8 @@ export interface AssetSearchResult {
49
49
  updatedAt: Date
50
50
  latestVersion: string
51
51
  approved: boolean
52
+ /** Visibility. `private` assets are owner-only (resolvable only by their owner or an admin). */
53
+ access: 'public' | 'private'
52
54
  npmDependencies: string
53
55
  assetDependencies: string
54
56
  skillDependencies: string
@@ -73,6 +75,11 @@ export interface User {
73
75
  image: string | null
74
76
  role: string
75
77
  isAdmin: boolean
78
+ /**
79
+ * Capability entitlements from the auth service (e.g. `market:private`, `market:generate`). Lets
80
+ * the frontend gate features (a private-visibility toggle) without re-deriving role logic.
81
+ */
82
+ entitledScopes: string[]
76
83
  createdAt: Date
77
84
  updatedAt: Date
78
85
  }
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod'
2
- import { assetTypeSchema } from './schemas.js'
2
+ import { assetAccessSchema, assetTypeSchema } from './schemas.js'
3
3
 
4
4
  // A long-running generation is exposed to the caller as a job it polls (see `generate` /
5
5
  // `generateStatus` in the contract). The market itself keeps NO state between polls: everything
@@ -13,6 +13,10 @@ export const generationJobSchema = z.object({
13
13
  providerJobId: z.string().min(1),
14
14
  description: z.string().min(1),
15
15
  assetName: z.string().min(1),
16
+ // The resolved visibility to persist when the job finalizes. Carried in the (stateless) jobId so
17
+ // the finalizing poll sets the same access the caller chose at start. Defaults to public for jobs
18
+ // minted before this field existed.
19
+ access: assetAccessSchema.default('public'),
16
20
  })
17
21
  export type GenerationJob = z.infer<typeof generationJobSchema>
18
22
 
package/src/generate.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { MarketClient } from './client.js'
2
- import type { AssetType } from './schemas.js'
2
+ import type { AssetAccess, AssetType, GenerateJobStatus, GenerateResponse } from './schemas.js'
3
3
 
4
4
  export class GenerateError extends Error {
5
5
  constructor(message: string) {
@@ -11,51 +11,65 @@ export class GenerateError extends Error {
11
11
  export interface GenerateInput {
12
12
  description: string
13
13
  type?: AssetType
14
+ /** Requested visibility; omitted lets the server resolve it from entitlement. */
15
+ access?: AssetAccess
14
16
  }
15
17
 
16
- export interface GenerateResult {
17
- assetName: string
18
- version: string
18
+ // How often the CLI polls a running job.
19
+ const POLL_INTERVAL_MS = 4_000
20
+ // How long one `generate` / `generate install` call blocks-and-polls before handing back control.
21
+ // The command does the waiting (the agent never sleeps between checks). When the window elapses the
22
+ // call returns and the caller just runs `generate install <jobId>` again, which picks up the SAME
23
+ // server-side job where this left off — so no asset type needs to be flagged "slow" ahead of time;
24
+ // anything that outlasts one window simply continues on the next call.
25
+ const WINDOW_MS = 120_000
26
+
27
+ // Start a generation. Fast providers answer `completed` inline; slow, job-based providers answer
28
+ // `pending` with a jobId to poll (see `waitForGeneration` / `generate install`).
29
+ export function startGeneration(
30
+ client: MarketClient,
31
+ input: GenerateInput,
32
+ ): Promise<GenerateResponse> {
33
+ return client.asset.generate(input)
19
34
  }
20
35
 
21
- export interface GenerateOptions {
22
- onProgress?: (message: string) => void
36
+ // One poll of a generation job.
37
+ export function pollGeneration(client: MarketClient, jobId: string): Promise<GenerateJobStatus> {
38
+ return client.asset.generateStatus({ jobId })
23
39
  }
24
40
 
25
- // Poll cadence for a job-based generation. Each poll is a short, independently-retriable request;
26
- // the client sleeps between them, so no single request is held open for the length of the job (that
27
- // is exactly the client/proxy-timeout trap the async flow removes). The deadline sits above the
28
- // slowest provider's own budget (the humanoid model's upstream job runs up to ~10 min).
29
- const POLL_INTERVAL_MS = 4_000
30
- const POLL_DEADLINE_MS = 15 * 60_000
41
+ // The bounded outcome of waiting on a job: it settled (completed/failed), or the window elapsed while
42
+ // it was still running.
43
+ export type PollOutcome =
44
+ | { status: 'completed'; assetName: string; version: string }
45
+ | { status: 'failed'; error: string }
46
+ | { status: 'running' }
31
47
 
32
- export async function generateAndWait(
48
+ // Poll a job until it settles or `windowMs` elapses. Bounded so a single call stays under a harness
49
+ // timeout; on timeout it returns `running` (the job keeps going server-side — run again to continue).
50
+ export async function waitForGeneration(
33
51
  client: MarketClient,
34
- input: GenerateInput,
35
- opts: GenerateOptions = {},
36
- ): Promise<GenerateResult> {
52
+ jobId: string,
53
+ opts: { windowMs?: number; onProgress?: (message: string) => void } = {},
54
+ ): Promise<PollOutcome> {
55
+ const windowMs = opts.windowMs ?? WINDOW_MS
37
56
  const report = opts.onProgress ?? (() => {})
38
-
39
- report('Generating asset')
40
- const started = await client.asset.generate(input)
41
- if (started.status === 'completed') {
42
- return { assetName: started.assetName, version: started.version }
43
- }
44
-
45
57
  const startedAt = Date.now()
46
58
  for (;;) {
47
- if (Date.now() - startedAt > POLL_DEADLINE_MS) {
48
- throw new GenerateError('Generation timed out.')
49
- }
50
- await delay(POLL_INTERVAL_MS)
51
- report('Generating asset')
52
- const status = await client.asset.generateStatus({ jobId: started.jobId })
59
+ const status = await pollGeneration(client, jobId)
53
60
  if (status.status === 'completed') {
54
- return { assetName: status.assetName, version: status.version }
61
+ return { status: 'completed', assetName: status.assetName, version: status.version }
55
62
  }
56
63
  if (status.status === 'failed') {
57
- throw new GenerateError(status.error)
64
+ return { status: 'failed', error: status.error }
65
+ }
66
+ const remaining = windowMs - (Date.now() - startedAt)
67
+ if (remaining <= 0) {
68
+ return { status: 'running' }
58
69
  }
70
+ report('Generating asset')
71
+ // Trim the last sleep so the call returns as soon as the window passes, not a poll-interval later.
72
+ await delay(Math.min(POLL_INTERVAL_MS, remaining))
59
73
  }
60
74
  }
61
75
 
package/src/index.ts CHANGED
@@ -40,8 +40,8 @@ export { resolve, ResolutionError } from './resolve.js'
40
40
  export type { ResolvedAsset, ResolveResult } from './resolve.js'
41
41
 
42
42
  // Generate
43
- export { generateAndWait, GenerateError } from './generate.js'
44
- export type { GenerateInput, GenerateResult, GenerateOptions } from './generate.js'
43
+ export { startGeneration, pollGeneration, GenerateError } from './generate.js'
44
+ export type { GenerateInput } from './generate.js'
45
45
 
46
46
  // Agent
47
47
  export { agentAndWait, AgentError } from './agent.js'
@@ -54,6 +54,7 @@ export {
54
54
  assetTypeSchema,
55
55
  semverSchema,
56
56
  assetNameSchema,
57
+ assetAccessSchema,
57
58
  npmDependenciesSchema,
58
59
  assetDependenciesSchema,
59
60
  skillDependenciesSchema,
@@ -75,6 +76,7 @@ export {
75
76
  } from './schemas.js'
76
77
  export type {
77
78
  AssetType,
79
+ AssetAccess,
78
80
  AgentResultAsset,
79
81
  AgentResult,
80
82
  AgentRunStatus,
package/src/output.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { AssetInstallMetadata, AssetSearchResult } from './contract.js'
2
2
  import type { ListInstalledAssetsResult } from './commands/list.js'
3
3
  import type { InstallResult } from './install.js'
4
+ import type { PackedAsset } from './pack.js'
4
5
 
5
6
  export function assetVersionRef(name: string, version?: string): string {
6
7
  return version ? `${name}@${version}` : name
@@ -125,6 +126,20 @@ export function generatedInstallResult(name: string, version: string): string {
125
126
  return `Generated and installed ${assetVersionRef(name, version)}`
126
127
  }
127
128
 
129
+ // Printed when `generate` returns before the job finished: it keeps running server-side and the
130
+ // caller continues it with `generate install`, which installs the asset once it is ready.
131
+ export function generationStartedResult(jobId: string): string {
132
+ return [
133
+ 'Still generating after a couple of minutes — it continues in the background.',
134
+ `Run this to pick up where it left off and install when ready: market generate install ${jobId}`,
135
+ ].join('\n')
136
+ }
137
+
138
+ // Printed by `generate install` when the job is still running after its wait window.
139
+ export function generationRunningResult(jobId: string): string {
140
+ return `Still generating. Run again to continue: market generate install ${jobId}`
141
+ }
142
+
128
143
  export function uploadResult(name: string, version: string): string {
129
144
  return `Uploaded ${assetVersionRef(name, version)}`
130
145
  }
@@ -133,6 +148,17 @@ export function unchangedUploadResult(name: string, version: string): string {
133
148
  return `No upload needed: ${assetVersionRef(name, version)} is unchanged`
134
149
  }
135
150
 
151
+ export function packResult(out: string, packed: PackedAsset): string {
152
+ const lines = [`Packed: ${out}`]
153
+ if (packed.omittedUnchangedInstalledFiles) {
154
+ lines.push('Omitted unchanged installed dependency files.')
155
+ }
156
+ lines.push(...dependencyLines('npm dependencies', packed.npmDependencies))
157
+ lines.push(...dependencyLines('asset dependencies', packed.assetDependencies))
158
+ lines.push(...dependencyLines('skill dependencies', packed.skillDependencies))
159
+ return lines.join('\n')
160
+ }
161
+
136
162
  export function previewResult(name: string, version: string, out: string): string {
137
163
  return `Saved preview for ${assetVersionRef(name, version)}: ${out}`
138
164
  }
@@ -204,3 +230,12 @@ function unique<T>(values: T[]): T[] {
204
230
  function indent(lines: string[], prefix: string): string[] {
205
231
  return lines.map((line) => `${prefix}${line}`)
206
232
  }
233
+
234
+ function dependencyLines(label: string, deps: Record<string, string>): string[] {
235
+ const entries = Object.entries(deps)
236
+ if (entries.length === 0) return []
237
+ return [
238
+ `${label}:`,
239
+ ...entries.sort(([a], [b]) => a.localeCompare(b)).map(([name, value]) => `- ${name}: ${value}`),
240
+ ]
241
+ }
package/src/pack.ts ADDED
@@ -0,0 +1,294 @@
1
+ import * as fs from 'fs/promises'
2
+ import * as path from 'path'
3
+ import { unzipSync, zipSync } from 'fflate'
4
+ import type { AssetInstallMetadata } from './contract.js'
5
+ import { findInstallRoot } from './install.js'
6
+ import { readMarketLock, sha256 } from './market-lock.js'
7
+ import {
8
+ packageJsonAssetDependenciesFromFiles,
9
+ packageJsonNpmDependenciesFromFiles,
10
+ } from './package-json.js'
11
+ import { MAX_UPLOAD_ZIP_SIZE_BYTES, type AssetType } from './schemas.js'
12
+
13
+ export interface PackDependencySpecs {
14
+ npm?: string[]
15
+ asset?: string[]
16
+ skill?: string[]
17
+ }
18
+
19
+ export interface ParsedPackDependencies {
20
+ npmDependencies: Record<string, string>
21
+ assetDependencies: Record<string, string>
22
+ skillDependencies: Record<string, string>
23
+ }
24
+
25
+ export interface PackPolicy {
26
+ readPackageJsonDependencies: boolean
27
+ omitUnchangedInstalledFiles: boolean
28
+ }
29
+
30
+ export interface PackAssetOptions {
31
+ cwd?: string
32
+ dependencies: ParsedPackDependencies
33
+ policy?: PackPolicy
34
+ }
35
+
36
+ export interface PackedAsset {
37
+ sourcePath: string
38
+ zip: Uint8Array
39
+ npmDependencies: Record<string, string>
40
+ assetDependencies: Record<string, string>
41
+ skillDependencies: Record<string, string>
42
+ omittedUnchangedInstalledFiles: boolean
43
+ }
44
+
45
+ export async function packAsset(zipFilter: string, opts: PackAssetOptions): Promise<PackedAsset> {
46
+ const cwd = opts.cwd ?? process.cwd()
47
+ const zipFile = await resolveOneZipFile(cwd, zipFilter)
48
+ const zipStat = await fs.stat(zipFile)
49
+ if (zipStat.size >= MAX_UPLOAD_ZIP_SIZE_BYTES) {
50
+ throw new Error('Packed zip must be smaller than 1 GB')
51
+ }
52
+
53
+ const sourceZip = new Uint8Array(await fs.readFile(zipFile))
54
+ const sourceFiles = unzipSync(sourceZip)
55
+ const policy = opts.policy ?? inferPackPolicy(sourceFiles)
56
+ const packageJsonAssetDependencies = policy.readPackageJsonDependencies
57
+ ? packageJsonAssetDependenciesFromFiles(sourceFiles)
58
+ : {}
59
+ const assetDependencies = mergeAssetDependencies(
60
+ packageJsonAssetDependencies,
61
+ opts.dependencies.assetDependencies,
62
+ )
63
+
64
+ const packageJsonNpmDependencies = policy.readPackageJsonDependencies
65
+ ? packageJsonNpmDependenciesFromFiles(sourceFiles)
66
+ : {}
67
+ const npmDependencies = { ...packageJsonNpmDependencies, ...opts.dependencies.npmDependencies }
68
+ const packed = policy.omitUnchangedInstalledFiles
69
+ ? await omitUnchangedInstalledFiles(sourceZip, sourceFiles, assetDependencies, cwd)
70
+ : { zip: sourceZip, omitted: false }
71
+
72
+ return {
73
+ sourcePath: zipFile,
74
+ zip: packed.zip,
75
+ npmDependencies,
76
+ assetDependencies,
77
+ skillDependencies: opts.dependencies.skillDependencies,
78
+ omittedUnchangedInstalledFiles: packed.omitted,
79
+ }
80
+ }
81
+
82
+ export function parsePackDependencies(specs: PackDependencySpecs): ParsedPackDependencies {
83
+ return {
84
+ npmDependencies: parseVersionedDeps(specs.npm ?? [], 'npm'),
85
+ assetDependencies: parseVersionedDeps(specs.asset ?? [], 'asset'),
86
+ skillDependencies: parseSkillDeps(specs.skill ?? []),
87
+ }
88
+ }
89
+
90
+ export function packPolicyForType(type: AssetType | undefined): PackPolicy | undefined {
91
+ if (!type) return undefined
92
+ return {
93
+ readPackageJsonDependencies: type === 'template',
94
+ omitUnchangedInstalledFiles: type === 'template',
95
+ }
96
+ }
97
+
98
+ export function packPolicyFromInstallMetadata(
99
+ metadata: AssetInstallMetadata | undefined,
100
+ ): PackPolicy {
101
+ return {
102
+ readPackageJsonDependencies: metadata?.readAssetDependenciesFromPackageJson ?? false,
103
+ omitUnchangedInstalledFiles: metadata?.omitUnchangedInstalledFilesOnUpload ?? false,
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Parse `name@range` specs (npm or asset deps) into a name→range record. The
109
+ * range is optional and defaults to `*`. A leading `@` is treated as a scope
110
+ * marker, so `@scope/pkg@^1.0.0` splits into `@scope/pkg` and `^1.0.0`.
111
+ */
112
+ export function parseVersionedDeps(specs: string[], kind: 'npm' | 'asset'): Record<string, string> {
113
+ const out: Record<string, string> = {}
114
+ for (const spec of specs) {
115
+ const at = spec.lastIndexOf('@')
116
+ const hasRange = at > 0
117
+ const name = hasRange ? spec.slice(0, at) : spec
118
+ const range = hasRange ? spec.slice(at + 1) : '*'
119
+ if (!name || !range) {
120
+ throw new Error(`Invalid ${kind} dependency "${spec}". Use name@range (e.g. three@^0.178.0).`)
121
+ }
122
+ if (name in out) {
123
+ throw new Error(`Duplicate ${kind} dependency "${name}".`)
124
+ }
125
+ out[name] = range
126
+ }
127
+ return out
128
+ }
129
+
130
+ /**
131
+ * Parse `label=source` specs into a label→source record. The source is passed
132
+ * verbatim to `skills add` (a GitHub/git ref or a local path), so only the
133
+ * first `=` is treated as the separator.
134
+ */
135
+ export function parseSkillDeps(specs: string[]): Record<string, string> {
136
+ const out: Record<string, string> = {}
137
+ for (const spec of specs) {
138
+ const eq = spec.indexOf('=')
139
+ if (eq <= 0 || eq === spec.length - 1) {
140
+ throw new Error(
141
+ `Invalid skill dependency "${spec}". Use label=source ` +
142
+ `(e.g. web-design=vercel-labs/agent-skills).`,
143
+ )
144
+ }
145
+ const label = spec.slice(0, eq)
146
+ if (label in out) {
147
+ throw new Error(`Duplicate skill dependency "${label}".`)
148
+ }
149
+ out[label] = spec.slice(eq + 1)
150
+ }
151
+ return out
152
+ }
153
+
154
+ function mergeAssetDependencies(
155
+ fromPackageJson: Record<string, string>,
156
+ explicit: Record<string, string>,
157
+ ): Record<string, string> {
158
+ const merged = { ...fromPackageJson }
159
+ for (const [name, range] of Object.entries(explicit)) {
160
+ if (name in merged && merged[name] !== range) {
161
+ throw new Error(
162
+ `Conflicting asset dependency "${name}": package.json has ${merged[name]}, --asset has ${range}`,
163
+ )
164
+ }
165
+ merged[name] = range
166
+ }
167
+ return merged
168
+ }
169
+
170
+ function inferPackPolicy(files: Record<string, Uint8Array>): PackPolicy {
171
+ const hasRootPackageJson = Boolean(files['package.json'])
172
+ return {
173
+ readPackageJsonDependencies: hasRootPackageJson,
174
+ omitUnchangedInstalledFiles: hasRootPackageJson,
175
+ }
176
+ }
177
+
178
+ async function omitUnchangedInstalledFiles(
179
+ sourceZip: Uint8Array,
180
+ sourceFiles: Record<string, Uint8Array>,
181
+ assetDependencies: Record<string, string>,
182
+ cwd: string,
183
+ ): Promise<{ zip: Uint8Array; omitted: boolean }> {
184
+ const dependencyNames = new Set(Object.keys(assetDependencies))
185
+ if (dependencyNames.size === 0) return { zip: sourceZip, omitted: false }
186
+
187
+ const installRoot = await findInstallRoot(cwd)
188
+ const lock = await readMarketLock(installRoot)
189
+ const hashesByPath = new Map<string, string>()
190
+
191
+ for (const [name, asset] of Object.entries(lock.assets)) {
192
+ if (!dependencyNames.has(name)) continue
193
+ for (const [file, metadata] of Object.entries(asset.files)) {
194
+ hashesByPath.set(file, metadata.sha256)
195
+ }
196
+ }
197
+
198
+ if (hashesByPath.size === 0) return { zip: sourceZip, omitted: false }
199
+
200
+ let omitted = false
201
+ const filtered: Record<string, Uint8Array> = {}
202
+ for (const [file, content] of Object.entries(sourceFiles)) {
203
+ const normalizedPath = normalizedZipPath(file)
204
+ const lockedHash = normalizedPath ? hashesByPath.get(normalizedPath) : undefined
205
+ if (lockedHash && lockedHash === sha256(content)) {
206
+ omitted = true
207
+ continue
208
+ }
209
+ filtered[file] = content
210
+ }
211
+
212
+ return omitted ? { zip: zipSync(filtered), omitted } : { zip: sourceZip, omitted }
213
+ }
214
+
215
+ function normalizedZipPath(file: string): string | null {
216
+ const zipPath = file.replace(/\\/g, '/')
217
+ if (
218
+ zipPath.split('/').includes('..') ||
219
+ path.posix.isAbsolute(zipPath) ||
220
+ path.win32.isAbsolute(zipPath)
221
+ ) {
222
+ return null
223
+ }
224
+ return path.posix.normalize(zipPath)
225
+ }
226
+
227
+ async function resolveOneZipFile(cwd: string, zipFilter: string): Promise<string> {
228
+ const absolute = path.resolve(cwd, zipFilter)
229
+ const stat = await maybeStat(absolute)
230
+ if (stat?.isFile()) return assertZipFile(absolute)
231
+
232
+ const files = await listFiles(cwd)
233
+ const matches = files
234
+ .filter((file) => matchesFilter(path.relative(cwd, file), zipFilter))
235
+ .filter(isZipFile)
236
+ .sort()
237
+
238
+ if (matches.length === 0) {
239
+ throw new Error(`No .zip files matched "${zipFilter}"`)
240
+ }
241
+ if (matches.length > 1) {
242
+ throw new Error(`File filter matched ${matches.length} zips; pack one asset at a time`)
243
+ }
244
+
245
+ return matches[0]
246
+ }
247
+
248
+ function assertZipFile(file: string): string {
249
+ if (!isZipFile(file)) throw new Error(`Pack source must be a .zip: ${file}`)
250
+ return file
251
+ }
252
+
253
+ function isZipFile(file: string): boolean {
254
+ return /\.zip$/i.test(file)
255
+ }
256
+
257
+ async function maybeStat(file: string) {
258
+ try {
259
+ return await fs.stat(file)
260
+ } catch {
261
+ return null
262
+ }
263
+ }
264
+
265
+ async function listFiles(dir: string): Promise<string[]> {
266
+ const entries = await fs.readdir(dir, { withFileTypes: true })
267
+ const files: string[] = []
268
+ for (const entry of entries) {
269
+ if (entry.name === 'node_modules' || entry.name === '.git') continue
270
+ const fullPath = path.join(dir, entry.name)
271
+ if (entry.isDirectory()) {
272
+ files.push(...(await listFiles(fullPath)))
273
+ } else if (entry.isFile()) {
274
+ files.push(fullPath)
275
+ }
276
+ }
277
+ return files
278
+ }
279
+
280
+ function matchesFilter(file: string, filter: string): boolean {
281
+ const normalizedFile = file.split(path.sep).join('/')
282
+ const normalizedFilter = filter.split(path.sep).join('/')
283
+ const pattern =
284
+ '^' +
285
+ escapeRegExp(normalizedFilter)
286
+ .replace(/\\\*\\\*/g, '.*')
287
+ .replace(/\\\*/g, '[^/]*') +
288
+ '$'
289
+ return new RegExp(pattern).test(normalizedFile)
290
+ }
291
+
292
+ function escapeRegExp(s: string): string {
293
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
294
+ }
package/src/schemas.ts CHANGED
@@ -48,6 +48,12 @@ export const skillDependenciesSchema = z.record(z.string(), z.string()).default(
48
48
 
49
49
  export const assetDescriptionSchema = z.string().max(MAX_ASSET_DESCRIPTION_LENGTH)
50
50
 
51
+ // Asset visibility. `public` behaves as before (subject to the approval flow); `private` is
52
+ // owner-only. Omitted on upload/generate — the server resolves the default from the caller's
53
+ // entitlement (private when they hold `market:private`, else public).
54
+ export const assetAccessSchema = z.enum(['public', 'private'])
55
+ export type AssetAccess = z.infer<typeof assetAccessSchema>
56
+
51
57
  export const updateProfileSchema = z.object({
52
58
  name: z.string().min(1).max(100).optional(),
53
59
  image: z.string().url().optional(),
@@ -77,6 +83,9 @@ export const uploadZipSchema = z.object({
77
83
  assetDependencies: assetDependenciesSchema,
78
84
  skillDependencies: skillDependenciesSchema,
79
85
  tags: z.array(z.string()).default([]),
86
+ // Requested visibility. Omitted → server resolves from entitlement; `private` requires the
87
+ // `market:private` scope (rejected otherwise).
88
+ access: assetAccessSchema.optional(),
80
89
  })
81
90
 
82
91
  export const downloadZipSchema = z.object({
@@ -87,6 +96,8 @@ export const downloadZipSchema = z.object({
87
96
  export const generateAssetSchema = z.object({
88
97
  description: assetDescriptionSchema.min(3),
89
98
  type: assetTypeSchema.optional(),
99
+ // Requested visibility of the generated asset (same resolution as upload).
100
+ access: assetAccessSchema.optional(),
90
101
  })
91
102
 
92
103
  // The completed asset of a generation: an installable name@version.
package/src/skill.ts CHANGED
@@ -14,6 +14,7 @@ market search "wooden chair" --type model --limit 3
14
14
  market install wooden-chair --cwd "$PWD"
15
15
  market list --cwd "$PWD"
16
16
  market preview wooden-chair --out /tmp/wooden-chair.png
17
+ market pack scene.zip --out scene.packed.zip
17
18
  \`\`\`
18
19
 
19
20
  ## Workflow
@@ -24,8 +25,8 @@ market preview wooden-chair --out /tmp/wooden-chair.png
24
25
  4. \`install\` takes zero or more exact asset names (optionally \`name@range\`). With names, it installs those assets; with no names, it installs \`assetDependencies\` from the nearest \`package.json\`. It does not search or generate. Find names with \`search\` first. No \`--type\` is needed — asset names are unique. Use \`--force\` only when the user agrees to overwrite changed local files.
25
26
  5. \`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.
26
27
  6. Use \`--unapproved\` only when the user asks for unapproved/private/admin assets. Do not install unapproved assets without explicit acceptance.
27
- 7. \`generate --type <type> "<prompt>"\` creates and installs a generated asset when that asset type has a generator; it requires login. Currently supported generated types are \`sound-effect\`, \`background-music\`, \`flipbook\`, and \`humanoid-model\`. Generation is provider-specific: prompt style, generated files, indexing fields, and install layout are owned by the asset type. If a type does not support generation yet, the CLI reports unsupported generation.
28
- 8. 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\`. Template uploads also read root \`package.json.assetDependencies\`; \`--asset\` flags are additive and must not conflict. Template upload omits installed dependency files whose hashes still match \`.drawcall/market-lock.json\`, so edited local files stay in the template. A skill source is a \`skills add\` argument: a whole repo (\`owner/repo\` or a git URL), a single skill via the full URL form \`https://github.com/owner/repo/tree/<branch>/<subpath>\` (the \`tree/<branch>/<subpath>\` shorthand needs the full URL, not \`owner/repo\`), 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=https://github.com/vercel-labs/agent-skills/tree/main/skills/web-design-guidelines\`.
28
+ 7. \`generate --type <type> "<prompt>"\` creates and installs a generated asset when that asset type has a generator; it requires login. Currently supported generated types are \`sound-effect\`, \`background-music\`, \`flipbook\`, \`humanoid-model\`, and \`environment\` (a fitting HDRI sky + equirectangular background, generated in ~1-2 min). Generation is provider-specific: prompt style, generated files, indexing fields, and install layout are owned by the asset type. If a type does not support generation yet, the CLI reports unsupported generation. Add \`--access public\` to publish the generated asset publicly, or \`--access private\` to keep it owner-only; when omitted the server defaults to private if you hold the \`market:private\` entitlement, else public (\`--access private\` requires that entitlement). \`generate\` waits for the asset and installs it — one command for quick types. For a long one (e.g. \`humanoid-model\`, >2 min) the call returns after ~2 min with a job id instead of hanging your shell; run \`market generate install <jobId>\` to continue — it resumes the SAME job where the last call left off and installs when ready. Just re-run \`generate install <jobId>\` until it prints "Generated and installed" (it exits 0 while still generating, 1 on failure). No type is flagged "slow" — anything that outlasts one wait just continues on the next call.
29
+ 8. Use \`pack <zip>\` to create the same Market asset zip that \`upload\` sends. \`pack\` runs offline, infers template packing from a root \`package.json\`, and accepts \`--type\` only when you need to override that inference. \`upload\` runs the shared pack step internally, then publishes: \`market upload <name> <zip> "<description>" --type <type>\`. Declare dependencies with repeatable flags on either command: \`--npm name@range\`, \`--asset name@range\`, \`--skill label=source\`. Template pack/upload also reads root \`package.json.assetDependencies\`; \`--asset\` flags are additive and must not conflict. Template pack/upload omits installed dependency files whose hashes still match \`.drawcall/market-lock.json\`, so edited local files stay in the template. A skill source is a \`skills add\` argument: a whole repo (\`owner/repo\` or a git URL), a single skill via the full URL form \`https://github.com/owner/repo/tree/<branch>/<subpath>\` (the \`tree/<branch>/<subpath>\` shorthand needs the full URL, not \`owner/repo\`), 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=https://github.com/vercel-labs/agent-skills/tree/main/skills/web-design-guidelines\`.
29
30
  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.
30
31
  10. Installed \`flipbook\` assets contain \`public/flipbook/<name>.ktx2\`. Render them with \`@drawcall/flipbook\`'s \`Flipbook\` class and Three.js \`KTX2Loader\` for Basis-compressed files; \`market preview\` fetches the middle frame from the flipbook.
31
32