@drawcall/market 0.1.52 → 0.1.54
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/commands/list.js +1 -1
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/pack.d.ts +1 -0
- package/dist/commands/pack.d.ts.map +1 -1
- package/dist/commands/pack.js +5 -0
- package/dist/commands/pack.js.map +1 -1
- package/dist/commands/upload.d.ts.map +1 -1
- package/dist/commands/upload.js +1 -0
- package/dist/commands/upload.js.map +1 -1
- package/dist/contract.d.ts +5 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +6 -1
- package/dist/contract.js.map +1 -1
- package/dist/index.d.ts +2 -2
- 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.map +1 -1
- package/dist/install.js +2 -5
- package/dist/install.js.map +1 -1
- package/dist/market-lock.d.ts +2 -3
- package/dist/market-lock.d.ts.map +1 -1
- package/dist/market-lock.js +7 -3
- package/dist/market-lock.js.map +1 -1
- package/dist/pack.d.ts +7 -0
- package/dist/pack.d.ts.map +1 -1
- package/dist/pack.js +71 -46
- package/dist/pack.js.map +1 -1
- package/dist/schemas.d.ts +10 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +4 -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 +2 -2
- package/package.json +1 -1
- package/skills/market/SKILL.md +2 -2
- package/src/commands/list.ts +1 -1
- package/src/commands/pack.ts +6 -0
- package/src/commands/upload.ts +1 -0
- package/src/contract.ts +8 -0
- package/src/index.ts +2 -0
- package/src/install.ts +11 -20
- package/src/market-lock.ts +10 -7
- package/src/pack.ts +94 -45
- package/src/schemas.ts +12 -0
- package/src/skill.ts +2 -2
package/src/pack.ts
CHANGED
|
@@ -28,10 +28,18 @@ export interface PackPolicy {
|
|
|
28
28
|
omitUnchangedInstalledFiles: boolean
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export type FetchFileManifest = (name: string, version: string) => Promise<Record<string, string>>
|
|
32
|
+
|
|
31
33
|
export interface PackAssetOptions {
|
|
32
34
|
cwd?: string
|
|
33
35
|
dependencies: ParsedPackDependencies
|
|
34
36
|
policy?: PackPolicy
|
|
37
|
+
/**
|
|
38
|
+
* Fetch the server's content hashes for an installed dependency version (see `asset.fileManifest`),
|
|
39
|
+
* so a template omits its unchanged installed dependency files. Content hashes are no longer stored
|
|
40
|
+
* locally, so omitting needs the API — omit this (or a failing fetch) and those files are kept.
|
|
41
|
+
*/
|
|
42
|
+
fetchFileManifest?: FetchFileManifest
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
export interface PackedAsset {
|
|
@@ -79,23 +87,28 @@ export async function packAsset(zipFilter: string, opts: PackAssetOptions): Prom
|
|
|
79
87
|
// Drop anything the zip's own .gitignore excludes (node_modules, build output, secrets…), then —
|
|
80
88
|
// for templates — omit unchanged installed dependency files. Only removals happen, so a smaller
|
|
81
89
|
// count means something was dropped; re-zip only then.
|
|
82
|
-
const withoutIgnored = applyGitignore(sourceFiles)
|
|
90
|
+
const { kept: withoutIgnored, gitignored } = applyGitignore(sourceFiles)
|
|
83
91
|
const files = policy.omitUnchangedInstalledFiles
|
|
84
|
-
? await withoutUnchangedInstalledFiles(
|
|
92
|
+
? await withoutUnchangedInstalledFiles(
|
|
93
|
+
withoutIgnored,
|
|
94
|
+
assetDependencies,
|
|
95
|
+
cwd,
|
|
96
|
+
opts.fetchFileManifest,
|
|
97
|
+
)
|
|
85
98
|
: withoutIgnored
|
|
86
99
|
|
|
87
|
-
const
|
|
100
|
+
const originalCount = Object.keys(sourceFiles).length
|
|
88
101
|
const afterIgnoreCount = Object.keys(withoutIgnored).length
|
|
89
102
|
const finalCount = Object.keys(files).length
|
|
90
103
|
|
|
91
104
|
return {
|
|
92
105
|
sourcePath: zipFile,
|
|
93
|
-
zip: finalCount <
|
|
106
|
+
zip: finalCount < originalCount ? zipSync(files) : sourceZip,
|
|
94
107
|
npmDependencies,
|
|
95
108
|
assetDependencies,
|
|
96
109
|
skillDependencies: opts.dependencies.skillDependencies,
|
|
97
110
|
omittedUnchangedInstalledFiles: finalCount < afterIgnoreCount,
|
|
98
|
-
gitignoredFiles:
|
|
111
|
+
gitignoredFiles: gitignored,
|
|
99
112
|
}
|
|
100
113
|
}
|
|
101
114
|
|
|
@@ -113,27 +126,28 @@ async function packDirectory(dir: string, opts: PackAssetOptions): Promise<Packe
|
|
|
113
126
|
|
|
114
127
|
const packageJsonFiles: Record<string, Uint8Array> = {}
|
|
115
128
|
if (policy.readPackageJsonDependencies && hasRootPackageJson) {
|
|
116
|
-
packageJsonFiles['package.json'] = new Uint8Array(
|
|
129
|
+
packageJsonFiles['package.json'] = new Uint8Array(
|
|
130
|
+
await fs.readFile(path.join(dir, 'package.json')),
|
|
131
|
+
)
|
|
117
132
|
}
|
|
118
133
|
const assetDependencies = mergeAssetDependencies(
|
|
119
|
-
policy.readPackageJsonDependencies
|
|
134
|
+
policy.readPackageJsonDependencies
|
|
135
|
+
? packageJsonAssetDependenciesFromFiles(packageJsonFiles)
|
|
136
|
+
: {},
|
|
120
137
|
opts.dependencies.assetDependencies,
|
|
121
138
|
)
|
|
122
139
|
const npmDependencies = {
|
|
123
|
-
...(policy.readPackageJsonDependencies
|
|
140
|
+
...(policy.readPackageJsonDependencies
|
|
141
|
+
? packageJsonNpmDependenciesFromFiles(packageJsonFiles)
|
|
142
|
+
: {}),
|
|
124
143
|
...opts.dependencies.npmDependencies,
|
|
125
144
|
}
|
|
126
145
|
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
for (const [name, asset] of Object.entries(lock.assets)) {
|
|
133
|
-
if (!(name in assetDependencies)) continue
|
|
134
|
-
for (const [file, metadata] of Object.entries(asset.files)) hashesByPath.set(file, metadata.sha256)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
146
|
+
// Content hashes of installed dependency files (from the server), so an unchanged one is dropped
|
|
147
|
+
// after a single read.
|
|
148
|
+
const hashesByPath = policy.omitUnchangedInstalledFiles
|
|
149
|
+
? await installedDependencyHashes(dir, assetDependencies, opts.fetchFileManifest)
|
|
150
|
+
: new Map<string, string>()
|
|
137
151
|
|
|
138
152
|
const files: Record<string, Uint8Array> = {}
|
|
139
153
|
let omittedInstalled = 0
|
|
@@ -183,7 +197,7 @@ async function walkWithGitignore(root: string): Promise<{ kept: string[]; ignore
|
|
|
183
197
|
|
|
184
198
|
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
185
199
|
for (const entry of entries) {
|
|
186
|
-
if (
|
|
200
|
+
if (ALWAYS_IGNORED_DIRS.has(entry.name)) continue
|
|
187
201
|
const relative = toPosix(path.relative(root, path.join(dir, entry.name)))
|
|
188
202
|
const candidate = entry.isDirectory() ? `${relative}/` : relative
|
|
189
203
|
if (isGitignored(candidate, scoped)) {
|
|
@@ -205,20 +219,36 @@ function toPosix(p: string): string {
|
|
|
205
219
|
|
|
206
220
|
const GITIGNORE_FILENAME = '.gitignore'
|
|
207
221
|
|
|
222
|
+
// Directories that never belong in a published asset — VCS/tooling metadata, reinstallable
|
|
223
|
+
// dependencies, and the market's own lock dir (`.drawcall/market-lock.json`). Excluded structurally
|
|
224
|
+
// in every pack path, regardless of any `.gitignore`, so a re-uploaded asset never ships them.
|
|
225
|
+
const ALWAYS_IGNORED_DIRS = new Set(['.git', 'node_modules', '.drawcall'])
|
|
226
|
+
|
|
227
|
+
function hasAlwaysIgnoredSegment(posixPath: string): boolean {
|
|
228
|
+
return posixPath.split('/').some((segment) => ALWAYS_IGNORED_DIRS.has(segment))
|
|
229
|
+
}
|
|
230
|
+
|
|
208
231
|
/**
|
|
209
|
-
*
|
|
210
|
-
* per-directory (git semantics: a nested
|
|
211
|
-
*
|
|
232
|
+
* Filter zip entries: always drop the structural dirs above, then drop whatever a `.gitignore`
|
|
233
|
+
* inside the zip excludes. `.gitignore` files apply per-directory (git semantics: a nested one only
|
|
234
|
+
* affects its own subtree). Returns the survivors plus the count of `.gitignore`-matched drops.
|
|
212
235
|
*/
|
|
213
|
-
function applyGitignore(files: Record<string, Uint8Array>):
|
|
236
|
+
function applyGitignore(files: Record<string, Uint8Array>): {
|
|
237
|
+
kept: Record<string, Uint8Array>
|
|
238
|
+
gitignored: number
|
|
239
|
+
} {
|
|
214
240
|
const matchers = gitignoreMatchers(files)
|
|
215
|
-
if (matchers.length === 0) return files
|
|
216
|
-
|
|
217
241
|
const kept: Record<string, Uint8Array> = {}
|
|
242
|
+
let gitignored = 0
|
|
218
243
|
for (const [name, content] of Object.entries(files)) {
|
|
219
|
-
if (
|
|
244
|
+
if (hasAlwaysIgnoredSegment(name)) continue
|
|
245
|
+
if (matchers.length > 0 && isGitignored(name, matchers)) {
|
|
246
|
+
gitignored += 1
|
|
247
|
+
continue
|
|
248
|
+
}
|
|
249
|
+
kept[name] = content
|
|
220
250
|
}
|
|
221
|
-
return kept
|
|
251
|
+
return { kept, gitignored }
|
|
222
252
|
}
|
|
223
253
|
|
|
224
254
|
interface GitignoreMatcher {
|
|
@@ -343,40 +373,59 @@ function inferPackPolicy(files: Record<string, Uint8Array>): PackPolicy {
|
|
|
343
373
|
}
|
|
344
374
|
}
|
|
345
375
|
|
|
346
|
-
// Drop dependency files whose bytes still match
|
|
347
|
-
//
|
|
376
|
+
// Drop dependency files whose bytes still match the installed asset's canonical content, so a
|
|
377
|
+
// template ships only its own (edited or new) files. Returns the surviving entries.
|
|
348
378
|
async function withoutUnchangedInstalledFiles(
|
|
349
379
|
files: Record<string, Uint8Array>,
|
|
350
380
|
assetDependencies: Record<string, string>,
|
|
351
381
|
cwd: string,
|
|
382
|
+
fetchFileManifest: FetchFileManifest | undefined,
|
|
352
383
|
): Promise<Record<string, Uint8Array>> {
|
|
353
|
-
const
|
|
354
|
-
if (dependencyNames.size === 0) return files
|
|
355
|
-
|
|
356
|
-
const installRoot = await findInstallRoot(cwd)
|
|
357
|
-
const lock = await readMarketLock(installRoot)
|
|
358
|
-
const hashesByPath = new Map<string, string>()
|
|
359
|
-
|
|
360
|
-
for (const [name, asset] of Object.entries(lock.assets)) {
|
|
361
|
-
if (!dependencyNames.has(name)) continue
|
|
362
|
-
for (const [file, metadata] of Object.entries(asset.files)) {
|
|
363
|
-
hashesByPath.set(file, metadata.sha256)
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
384
|
+
const hashesByPath = await installedDependencyHashes(cwd, assetDependencies, fetchFileManifest)
|
|
367
385
|
if (hashesByPath.size === 0) return files
|
|
368
386
|
|
|
369
387
|
const kept: Record<string, Uint8Array> = {}
|
|
370
388
|
for (const [file, content] of Object.entries(files)) {
|
|
371
389
|
const normalizedPath = normalizedZipPath(file)
|
|
372
|
-
const
|
|
373
|
-
if (
|
|
390
|
+
const canonicalHash = normalizedPath ? hashesByPath.get(normalizedPath) : undefined
|
|
391
|
+
if (canonicalHash && canonicalHash === sha256(content)) continue
|
|
374
392
|
kept[file] = content
|
|
375
393
|
}
|
|
376
394
|
|
|
377
395
|
return kept
|
|
378
396
|
}
|
|
379
397
|
|
|
398
|
+
/**
|
|
399
|
+
* The canonical content hash of every file belonging to a declared asset dependency, keyed by install
|
|
400
|
+
* path. Read the installed version of each dependency from the lock, then fetch that version's hashes
|
|
401
|
+
* from the server (`asset.fileManifest`). Returns an empty map when there's nothing to omit or no way
|
|
402
|
+
* to fetch (offline / no client) — pack then keeps all files rather than guessing.
|
|
403
|
+
*/
|
|
404
|
+
async function installedDependencyHashes(
|
|
405
|
+
root: string,
|
|
406
|
+
assetDependencies: Record<string, string>,
|
|
407
|
+
fetchFileManifest: FetchFileManifest | undefined,
|
|
408
|
+
): Promise<Map<string, string>> {
|
|
409
|
+
const hashesByPath = new Map<string, string>()
|
|
410
|
+
const dependencyNames = Object.keys(assetDependencies)
|
|
411
|
+
if (!fetchFileManifest || dependencyNames.length === 0) return hashesByPath
|
|
412
|
+
|
|
413
|
+
const lock = await readMarketLock(await findInstallRoot(root))
|
|
414
|
+
for (const name of dependencyNames) {
|
|
415
|
+
const installed = lock.assets[name]
|
|
416
|
+
if (!installed) continue
|
|
417
|
+
let manifest: Record<string, string>
|
|
418
|
+
try {
|
|
419
|
+
manifest = await fetchFileManifest(name, installed.version)
|
|
420
|
+
} catch {
|
|
421
|
+
// Can't reach the server for this dependency — keep its files rather than wrongly omitting them.
|
|
422
|
+
continue
|
|
423
|
+
}
|
|
424
|
+
for (const [file, hash] of Object.entries(manifest)) hashesByPath.set(file, hash)
|
|
425
|
+
}
|
|
426
|
+
return hashesByPath
|
|
427
|
+
}
|
|
428
|
+
|
|
380
429
|
function normalizedZipPath(file: string): string | null {
|
|
381
430
|
const zipPath = file.replace(/\\/g, '/')
|
|
382
431
|
if (
|
package/src/schemas.ts
CHANGED
|
@@ -93,6 +93,18 @@ export const downloadZipSchema = z.object({
|
|
|
93
93
|
version: semverSchema,
|
|
94
94
|
})
|
|
95
95
|
|
|
96
|
+
export const fileManifestSchema = z.object({
|
|
97
|
+
name: assetNameSchema,
|
|
98
|
+
version: semverSchema,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The content hash of every file in an asset version, keyed by its install path — sha256 hex, the
|
|
103
|
+
* same digest the CLI computes locally. Lets a caller check whether a local file is identical to the
|
|
104
|
+
* canonical asset without keeping hashes itself (see `pack` omit-unchanged).
|
|
105
|
+
*/
|
|
106
|
+
export type AssetFileManifest = Record<string, string>
|
|
107
|
+
|
|
96
108
|
export const generateAssetSchema = z.object({
|
|
97
109
|
description: assetDescriptionSchema.min(3),
|
|
98
110
|
type: assetTypeSchema.optional(),
|
package/src/skill.ts
CHANGED
|
@@ -26,7 +26,7 @@ market pack scene.zip --out scene.packed.zip
|
|
|
26
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.
|
|
27
27
|
6. Use \`--unapproved\` only when the user asks for unapproved/private/admin assets. Do not install unapproved assets without explicit acceptance.
|
|
28
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
|
|
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 that still match the installed dependency's canonical content (its file hashes are fetched from the server, not stored locally), so edited local files stay in the template; this omit step needs the API, but simple non-template packs stay offline. 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\`.
|
|
30
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.
|
|
31
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.
|
|
32
32
|
|
|
@@ -63,7 +63,7 @@ Saved preview for wooden-chair@1.0.0: /tmp/wooden-chair.png
|
|
|
63
63
|
|
|
64
64
|
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.
|
|
65
65
|
|
|
66
|
-
Installed non-template assets are saved to \`package.json.assetDependencies\`; templates are scaffolds and are not saved as project asset dependencies. Exact installed versions and file
|
|
66
|
+
Installed non-template assets are saved to \`package.json.assetDependencies\`; templates are scaffolds and are not saved as project asset dependencies. Exact installed versions and file paths are recorded in \`.drawcall/market-lock.json\`; file content hashes now come from the server (\`asset.fileManifest\`), not the lock.
|
|
67
67
|
|
|
68
68
|
\`list\` is offline: it reads \`.drawcall/market-lock.json\` from the nearest package root and prints exact installed names, versions, types, and installed file paths.
|
|
69
69
|
|