@drawcall/market 0.1.37 → 0.1.38

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawcall/market",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/drawcall-ai/market",
@@ -12,6 +12,11 @@
12
12
  ".": "./src/index.ts",
13
13
  "./install": "./src/install-entry.ts"
14
14
  },
15
+ "files": [
16
+ "dist",
17
+ "src",
18
+ "skills"
19
+ ],
15
20
  "publishConfig": {
16
21
  "exports": {
17
22
  ".": {
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: market
3
+ description: Find, preview, install, generate, and publish Drawcall Market assets from a coding agent.
4
+ ---
5
+
6
+ # Drawcall Market
7
+
8
+ Use the `npx @drawcall/market` CLI. Keep commands short and read the summary lines.
9
+
10
+ ## Quick Start
11
+
12
+ ```sh
13
+ npx @drawcall/market search "wooden chair" --type model --limit 3
14
+ npx @drawcall/market install wooden-chair --cwd "$PWD"
15
+ npx @drawcall/market list --cwd "$PWD"
16
+ npx @drawcall/market preview wooden-chair --out /tmp/wooden-chair.png
17
+ ```
18
+
19
+ ## Workflow
20
+
21
+ 1. In an existing repo, run `list --cwd "$PWD"` first to see installed local assets from `.drawcall/market-lock.json`. Use the listed names with `preview <name>` when you want preview images.
22
+ 2. 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`, `environment`, or `flipbook`.
23
+ 3. Use `--limit 1` for lookup, `--limit 3` for choice. Search caps at 5 and prints full descriptions.
24
+ 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
+ 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
+ 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 a new asset; it requires login and a type that supports generation. No asset type currently supports 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`.
29
+ 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
+ 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
+ ## Humanoid animations
33
+
34
+ `humanoid-animation` assets are single-clip GLBs — one motion per asset (one idle, one walk-forward, one jump, one attack) on a normalized skeleton that retargets onto any humanoid, whatever its role. A behaving character is therefore a *set* of clips, not one asset: from what the character actually does, budget the clips it needs — an idle, its locomotion (walk/run, often split by direction: fwd/bwd/left/right), and one clip per distinct action and reaction it performs — then search for each separately.
35
+
36
+ `humanoid-model` assets share that same normalized skeleton and are authored to a **consistent real-world scale** — they come in at roughly the same height as each other. So you do **not** need to rescale one humanoid to match another (player vs. enemy vs. NPC); dropped in as-is they already stand at a consistent size. Avoid the trap of measuring one character's height and scaling others to it — besides being unnecessary, measuring a rigged/animated character's bounding box is unreliable and produces giants (see the `math` skill on `Box3` and skinned meshes). If you ever do need a deliberate size difference (a boss, a child NPC), apply an explicit chosen multiplier, not a measured one.
37
+
38
+ Search one motion per query, named by the motion, because results rank by keyword overlap: a query naming several motions at once is dominated by whichever word matches the most assets and buries the others, so real clips look like a gap when they exist. Names describe the motion, not the character — so search the motion (`"walk forward"`, `"reload"`, `"jump"`), not the role (`"player run"`, `"boss attack"`). If a motion finds nothing, retry with synonyms (run/jog/sprint, attack/swing/strike).
39
+
40
+ ## Output
41
+
42
+ Commands print concise, line-oriented summaries:
43
+
44
+ ```text
45
+ Results: 2/8 query="wooden chair" type=model approval=approved
46
+ - wooden-chair@1.0.0 | model | approved | Low-poly wooden chair
47
+ Installed:
48
+ - wooden-chair@1.0.0 (asset)
49
+ description: Low-poly wooden chair
50
+ files:
51
+ public/model
52
+ └─ wooden-chair.glb
53
+ - three@^0.178.0 (npm)
54
+ - web-design ← https://github.com/vercel-labs/agent-skills/tree/main/skills/web-design-guidelines (skill)
55
+ Installed assets: 1
56
+ - wooden-chair@1.0.0 (model)
57
+ files:
58
+ public/model
59
+ └─ wooden-chair.glb
60
+ Saved preview for wooden-chair@1.0.0: /tmp/wooden-chair.png
61
+ ```
62
+
63
+ 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.
64
+
65
+ 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 hashes are recorded in `.drawcall/market-lock.json`.
66
+
67
+ `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.
68
+
69
+ If search returns no results, try one broader noun phrase. If a command returns `Error: Not logged in...`, ask before running `market login`.
@@ -1,67 +0,0 @@
1
- import assert from 'node:assert/strict'
2
- import test from 'node:test'
3
- import { resolveArg } from '../src/commands/install.js'
4
-
5
- test('resolveArg reports unapproved explicit assets clearly', async () => {
6
- const client = {
7
- asset: {
8
- exact: async ({ includeUnapproved }: { name: string; includeUnapproved: boolean }) =>
9
- includeUnapproved
10
- ? {
11
- name: 'qwantani-moon-noon-puresky',
12
- latestVersion: '1.0.2',
13
- approved: false,
14
- }
15
- : null,
16
- },
17
- } as never
18
-
19
- await assert.rejects(
20
- resolveArg(client, 'qwantani-moon-noon-puresky@1.0.2', false),
21
- /has no approved versions/u,
22
- )
23
- })
24
-
25
- test('resolveArg keeps explicit versioned refs exact when the asset exists', async () => {
26
- const client = {
27
- asset: {
28
- exact: async () => ({ name: 'fresh-sky', latestVersion: '1.2.3', approved: true }),
29
- },
30
- } as never
31
-
32
- const request = await resolveArg(client, 'fresh-sky@1.2.3', false)
33
-
34
- assert.deepEqual(request, {
35
- name: 'fresh-sky',
36
- range: '1.2.3',
37
- saveRange: '1.2.3',
38
- save: true,
39
- })
40
- })
41
-
42
- test('resolveArg saves an implicit range from the selected latest version', async () => {
43
- const client = {
44
- asset: {
45
- exact: async () => ({ name: 'fresh-sky', latestVersion: '1.2.3', approved: true }),
46
- },
47
- } as never
48
-
49
- const request = await resolveArg(client, 'fresh-sky', false)
50
-
51
- assert.deepEqual(request, {
52
- name: 'fresh-sky',
53
- range: '*',
54
- saveRange: '^1.2.3',
55
- save: true,
56
- })
57
- })
58
-
59
- test('resolveArg errors when the asset does not exist instead of deferring', async () => {
60
- const client = {
61
- asset: {
62
- exact: async () => null,
63
- },
64
- } as never
65
-
66
- await assert.rejects(resolveArg(client, 'does-not-exist@1.2.3', false), /not found/u)
67
- })
@@ -1,307 +0,0 @@
1
- import assert from 'node:assert/strict'
2
- import * as fs from 'node:fs/promises'
3
- import * as os from 'node:os'
4
- import * as path from 'node:path'
5
- import test from 'node:test'
6
- import { zipSync } from 'fflate'
7
- import { findInstallRoot, install } from '../src/install.js'
8
- import { MARKET_LOCK_PATH } from '../src/market-lock.js'
9
-
10
- const textEncoder = new TextEncoder()
11
-
12
- test('install writes zip files into the package root', async () => {
13
- const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-install-'))
14
- const appRoot = path.join(tempDir, 'app')
15
- const cwd = path.join(appRoot, 'src', 'feature')
16
- await fs.mkdir(path.join(appRoot, 'public'), { recursive: true })
17
- await fs.mkdir(cwd, { recursive: true })
18
- await fs.writeFile(path.join(appRoot, 'package.json'), '{}\n')
19
- await fs.writeFile(path.join(appRoot, 'README.md'), 'project readme\n')
20
-
21
- const result = await install(
22
- clientWithZip({
23
- 'public/humanoid-animation/idle-loop.glb': textEncoder.encode('glb'),
24
- 'src/generated/idle-loop.ts': textEncoder.encode('export const idleLoop = true\n'),
25
- 'README.md': textEncoder.encode('asset readme'),
26
- }),
27
- {
28
- assets: [
29
- {
30
- name: 'idle-loop',
31
- type: 'humanoid-animation',
32
- version: '1.0.0',
33
- description: 'Idle loop animation',
34
- npmDependencies: {},
35
- assetDependencies: {},
36
- skillDependencies: {},
37
- },
38
- ],
39
- npmDependencies: {},
40
- skillDependencies: {},
41
- },
42
- { cwd },
43
- )
44
-
45
- assert.deepEqual(result, {
46
- assets: [
47
- {
48
- name: 'idle-loop',
49
- type: 'humanoid-animation',
50
- version: '1.0.0',
51
- description: 'Idle loop animation',
52
- files: ['public/humanoid-animation/idle-loop.glb', 'src/generated/idle-loop.ts'],
53
- },
54
- ],
55
- npmDependencies: {},
56
- skillDependencies: {},
57
- warnings: [],
58
- })
59
- assert.equal(
60
- await fs.readFile(path.join(appRoot, 'public', 'humanoid-animation', 'idle-loop.glb'), 'utf-8'),
61
- 'glb',
62
- )
63
- assert.equal(
64
- await fs.readFile(path.join(appRoot, 'src', 'generated', 'idle-loop.ts'), 'utf-8'),
65
- 'export const idleLoop = true\n',
66
- )
67
- assert.equal(await fs.readFile(path.join(appRoot, 'README.md'), 'utf-8'), 'project readme\n')
68
- assert.equal(await exists(path.join(cwd, 'src', 'idle-loop', 'public')), false)
69
-
70
- const lock = JSON.parse(await fs.readFile(path.join(appRoot, MARKET_LOCK_PATH), 'utf-8')) as {
71
- assets: Record<string, { version: string; files: Record<string, { sha256: string }> }>
72
- }
73
- assert.equal(lock.assets['idle-loop'].version, '1.0.0')
74
- assert.equal(
75
- typeof lock.assets['idle-loop'].files['public/humanoid-animation/idle-loop.glb'].sha256,
76
- 'string',
77
- )
78
- })
79
-
80
- test('install rejects zip paths that escape through parent segments', async () => {
81
- const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-install-'))
82
- await fs.writeFile(path.join(tempDir, 'package.json'), '{}\n')
83
-
84
- await assert.rejects(
85
- install(
86
- clientWithZip({
87
- 'public/../package.json': textEncoder.encode('nope\n'),
88
- }),
89
- {
90
- assets: [
91
- {
92
- name: 'bad-path',
93
- type: 'template',
94
- version: '1.0.0',
95
- description: null,
96
- npmDependencies: {},
97
- assetDependencies: {},
98
- skillDependencies: {},
99
- },
100
- ],
101
- npmDependencies: {},
102
- skillDependencies: {},
103
- },
104
- { cwd: tempDir },
105
- ),
106
- /unsafe path/u,
107
- )
108
- })
109
-
110
- test('install runs the skills runner per dependency, resolving local paths against the root', async () => {
111
- const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-skills-'))
112
- const appRoot = path.join(tempDir, 'app')
113
- const cwd = path.join(appRoot, 'src')
114
- await fs.mkdir(cwd, { recursive: true })
115
- await fs.writeFile(path.join(appRoot, 'package.json'), '{}\n')
116
-
117
- const calls: Array<{ source: string; cwd: string }> = []
118
-
119
- const result = await install(
120
- clientWithZip({
121
- 'public/skills/local-skill/SKILL.md': textEncoder.encode('---\nname: local-skill\n---\n'),
122
- }),
123
- {
124
- assets: [
125
- {
126
- name: 'with-skills',
127
- type: 'template',
128
- version: '1.0.0',
129
- description: null,
130
- npmDependencies: {},
131
- assetDependencies: {},
132
- skillDependencies: {},
133
- },
134
- ],
135
- npmDependencies: {},
136
- skillDependencies: {
137
- 'web-design-guidelines': 'vercel-labs/agent-skills/tree/main/skills/web-design-guidelines',
138
- 'local-skill': './public/skills/local-skill',
139
- },
140
- },
141
- {
142
- cwd,
143
- runSkillAdd: async (source, runnerCwd) => {
144
- calls.push({ source, cwd: runnerCwd })
145
- },
146
- },
147
- )
148
-
149
- // Every runner call uses the install root (the dir holding package.json).
150
- assert.deepEqual(new Set(calls.map((c) => c.cwd)), new Set([appRoot]))
151
-
152
- const bySource = new Map(calls.map((c) => [c.source, c]))
153
- // Remote refs are passed through untouched.
154
- assert.ok(bySource.has('vercel-labs/agent-skills/tree/main/skills/web-design-guidelines'))
155
- // Local paths are resolved against the install root, not the cwd.
156
- assert.ok(bySource.has(path.join(appRoot, 'public', 'skills', 'local-skill')))
157
- assert.equal(calls.length, 2)
158
-
159
- assert.deepEqual(result.skillDependencies, {
160
- 'web-design-guidelines': 'vercel-labs/agent-skills/tree/main/skills/web-design-guidelines',
161
- 'local-skill': './public/skills/local-skill',
162
- })
163
- assert.deepEqual(result.warnings, [])
164
- })
165
-
166
- test('install saves top-level assets according to provider policy', async () => {
167
- const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-save-policy-'))
168
- await fs.writeFile(path.join(tempDir, 'package.json'), '{}\n')
169
-
170
- await install(
171
- clientWithZip({
172
- 'public/model/chair.glb': textEncoder.encode('chair'),
173
- 'src/template.ts': textEncoder.encode('export {}\n'),
174
- }),
175
- {
176
- assets: [
177
- {
178
- name: 'chair',
179
- type: 'model',
180
- version: '1.2.0',
181
- description: null,
182
- npmDependencies: {},
183
- assetDependencies: {},
184
- skillDependencies: {},
185
- },
186
- {
187
- name: 'starter-template',
188
- type: 'template',
189
- version: '2.0.0',
190
- description: null,
191
- npmDependencies: {},
192
- assetDependencies: {},
193
- skillDependencies: {},
194
- },
195
- ],
196
- npmDependencies: {},
197
- skillDependencies: {},
198
- },
199
- {
200
- cwd: tempDir,
201
- rootRequests: [
202
- { name: 'chair', range: '*', saveRange: '^1.2.0', save: true },
203
- { name: 'starter-template', range: '*', saveRange: '^2.0.0', save: true },
204
- ],
205
- installMetadata: {
206
- template: {
207
- saveOnInstall: false,
208
- readAssetDependenciesFromPackageJson: true,
209
- omitUnchangedInstalledFilesOnUpload: true,
210
- },
211
- },
212
- },
213
- )
214
-
215
- const pkg = JSON.parse(await fs.readFile(path.join(tempDir, 'package.json'), 'utf-8')) as {
216
- assetDependencies: Record<string, string>
217
- }
218
- assert.deepEqual(pkg.assetDependencies, { chair: '^1.2.0' })
219
- })
220
-
221
- test('install skips existing changed files unless force is set', async () => {
222
- const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-conflict-'))
223
- await fs.mkdir(path.join(tempDir, 'public', 'model'), { recursive: true })
224
- await fs.writeFile(path.join(tempDir, 'package.json'), '{}\n')
225
- await fs.writeFile(path.join(tempDir, 'public', 'model', 'chair.glb'), 'local edit')
226
-
227
- const resolution = {
228
- assets: [
229
- {
230
- name: 'chair',
231
- type: 'model',
232
- version: '1.0.0',
233
- description: null,
234
- npmDependencies: {},
235
- assetDependencies: {},
236
- skillDependencies: {},
237
- },
238
- ],
239
- npmDependencies: {},
240
- skillDependencies: {},
241
- }
242
-
243
- const skipped = await install(
244
- clientWithZip({
245
- 'public/model/chair.glb': textEncoder.encode('market bytes'),
246
- }),
247
- resolution,
248
- { cwd: tempDir },
249
- )
250
-
251
- assert.equal(await fs.readFile(path.join(tempDir, 'public', 'model', 'chair.glb'), 'utf-8'), 'local edit')
252
- assert.match(skipped.warnings[0], /Skipped public\/model\/chair\.glb/u)
253
- assert.deepEqual(skipped.assets[0].files, [])
254
-
255
- const forced = await install(
256
- clientWithZip({
257
- 'public/model/chair.glb': textEncoder.encode('market bytes'),
258
- }),
259
- resolution,
260
- { cwd: tempDir, force: true },
261
- )
262
-
263
- assert.equal(
264
- await fs.readFile(path.join(tempDir, 'public', 'model', 'chair.glb'), 'utf-8'),
265
- 'market bytes',
266
- )
267
- assert.deepEqual(forced.warnings, [])
268
- assert.deepEqual(forced.assets[0].files, ['public/model/chair.glb'])
269
- })
270
-
271
- test('findInstallRoot uses the nearest package.json, not the monorepo root', async () => {
272
- const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-root-'))
273
- const repoRoot = path.join(tempDir, 'repo')
274
- const appRoot = path.join(repoRoot, 'packages', 'app')
275
- const cwd = path.join(appRoot, 'src', 'routes')
276
- await fs.mkdir(path.join(appRoot, 'public'), { recursive: true })
277
- await fs.mkdir(cwd, { recursive: true })
278
- await fs.writeFile(path.join(repoRoot, 'package.json'), '{}\n')
279
- await fs.writeFile(path.join(appRoot, 'package.json'), '{}\n')
280
-
281
- assert.equal(await findInstallRoot(cwd), appRoot)
282
- })
283
-
284
- test('findInstallRoot falls back to cwd when no package.json exists', async () => {
285
- const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-root-'))
286
- const cwd = path.join(tempDir, 'repo', 'src')
287
- await fs.mkdir(cwd, { recursive: true })
288
-
289
- assert.equal(await findInstallRoot(cwd), cwd)
290
- })
291
-
292
- function clientWithZip(files: Record<string, Uint8Array>) {
293
- return {
294
- asset: {
295
- downloadZip: async () => new Blob([zipSync(files)]),
296
- },
297
- } as never
298
- }
299
-
300
- async function exists(file: string): Promise<boolean> {
301
- try {
302
- await fs.stat(file)
303
- return true
304
- } catch {
305
- return false
306
- }
307
- }
@@ -1,76 +0,0 @@
1
- import assert from 'node:assert/strict'
2
- import * as fs from 'node:fs/promises'
3
- import * as os from 'node:os'
4
- import * as path from 'node:path'
5
- import test from 'node:test'
6
- import { listInstalledAssets } from '../src/commands/list.js'
7
- import { MARKET_LOCK_PATH } from '../src/market-lock.js'
8
-
9
- test('listInstalledAssets reads the nearest project lockfile', async () => {
10
- const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-list-'))
11
- const appRoot = path.join(tempDir, 'app')
12
- const cwd = path.join(appRoot, 'src', 'feature')
13
- await fs.mkdir(path.join(appRoot, '.drawcall'), { recursive: true })
14
- await fs.mkdir(cwd, { recursive: true })
15
- await fs.writeFile(path.join(appRoot, 'package.json'), '{}\n')
16
- await fs.writeFile(
17
- path.join(appRoot, MARKET_LOCK_PATH),
18
- JSON.stringify(
19
- {
20
- version: 1,
21
- assets: {
22
- 'wooden-chair': {
23
- version: '1.2.0',
24
- type: 'model',
25
- files: {
26
- 'public/models/wooden-chair.glb': { sha256: 'abc' },
27
- },
28
- },
29
- 'forest-sky': {
30
- version: '1.0.0',
31
- type: 'environment',
32
- files: {
33
- 'public/environment/forest-sky.hdr': { sha256: 'def' },
34
- 'public/environment/forest-sky-background.webp': { sha256: 'ghi' },
35
- },
36
- },
37
- },
38
- },
39
- null,
40
- 2,
41
- ) + '\n',
42
- )
43
-
44
- assert.deepEqual(await listInstalledAssets(cwd), {
45
- projectRoot: appRoot,
46
- lockPath: path.join(appRoot, MARKET_LOCK_PATH),
47
- assets: [
48
- {
49
- name: 'forest-sky',
50
- version: '1.0.0',
51
- type: 'environment',
52
- files: [
53
- 'public/environment/forest-sky-background.webp',
54
- 'public/environment/forest-sky.hdr',
55
- ],
56
- },
57
- {
58
- name: 'wooden-chair',
59
- version: '1.2.0',
60
- type: 'model',
61
- files: ['public/models/wooden-chair.glb'],
62
- },
63
- ],
64
- })
65
- })
66
-
67
- test('listInstalledAssets returns an empty inventory when no lockfile exists', async () => {
68
- const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-list-empty-'))
69
- await fs.writeFile(path.join(tempDir, 'package.json'), '{}\n')
70
-
71
- assert.deepEqual(await listInstalledAssets(tempDir), {
72
- projectRoot: tempDir,
73
- lockPath: path.join(tempDir, MARKET_LOCK_PATH),
74
- assets: [],
75
- })
76
- })
@@ -1,235 +0,0 @@
1
- import assert from 'node:assert/strict'
2
- import test from 'node:test'
3
- import { assetSearchResultLine, installResult, listResult } from '../src/output.js'
4
-
5
- test('assetSearchResultLine prints the full description', () => {
6
- const description =
7
- 'A detailed model description that is intentionally longer than the old one-line search cap, with material notes, scale guidance, and usage context.'
8
-
9
- assert.equal(
10
- assetSearchResultLine({
11
- id: 'asset-1',
12
- name: 'wooden-chair',
13
- type: 'model',
14
- description,
15
- ownerId: 'user-1',
16
- createdAt: new Date('2026-01-01T00:00:00.000Z'),
17
- updatedAt: new Date('2026-01-01T00:00:00.000Z'),
18
- latestVersion: '1.2.0',
19
- approved: true,
20
- npmDependencies: '{}',
21
- assetDependencies: '{}',
22
- skillDependencies: '{}',
23
- previewUrl: null,
24
- }),
25
- `- wooden-chair@1.2.0 | model | approved | ${description}`,
26
- )
27
- })
28
-
29
- test('installResult prints asset type, file tree, and npm list', () => {
30
- assert.equal(
31
- installResult({
32
- assets: [
33
- {
34
- name: 'wooden-chair',
35
- type: 'model',
36
- version: '1.2.0',
37
- description: null,
38
- files: ['public/models/wooden-chair.glb'],
39
- },
40
- {
41
- name: 'oak-material',
42
- type: 'texture',
43
- version: '1.0.0',
44
- description: null,
45
- files: ['public/textures/oak/basecolor.png', 'public/textures/oak/normal.png'],
46
- },
47
- ],
48
- npmDependencies: { '@react-three/drei': 'latest' },
49
- skillDependencies: {},
50
- warnings: [],
51
- }),
52
- `Installed:
53
- - wooden-chair@1.2.0 (model)
54
- files:
55
- public
56
- └─ models
57
- └─ wooden-chair.glb
58
- - oak-material@1.0.0 (texture)
59
- files:
60
- public
61
- └─ textures
62
- └─ oak
63
- ├─ basecolor.png
64
- └─ normal.png
65
- - @react-three/drei@latest (npm)`,
66
- )
67
- })
68
-
69
- test('installResult attaches each post-install note under its asset', () => {
70
- assert.equal(
71
- installResult(
72
- {
73
- assets: [
74
- {
75
- name: 'wooden-chair',
76
- type: 'model',
77
- version: '1.2.0',
78
- description: null,
79
- files: [],
80
- },
81
- {
82
- name: 'beep',
83
- type: 'sound-effect',
84
- version: '1.0.0',
85
- description: null,
86
- files: [],
87
- },
88
- ],
89
- npmDependencies: {},
90
- skillDependencies: {},
91
- warnings: [],
92
- },
93
- {
94
- model: {
95
- installMessage: 'Load the model.',
96
- saveOnInstall: true,
97
- readAssetDependenciesFromPackageJson: false,
98
- omitUnchangedInstalledFilesOnUpload: false,
99
- },
100
- // sound-effect intentionally omitted -> no note for `beep`.
101
- },
102
- ),
103
- `Installed:
104
- - wooden-chair@1.2.0 (model)
105
- note: Load the model.
106
- - beep@1.0.0 (sound-effect)`,
107
- )
108
- })
109
-
110
- test('installResult word-wraps a long note with a hanging indent', () => {
111
- const message = Array(20).fill('word').join(' ')
112
- const output = installResult(
113
- {
114
- assets: [{ name: 'thing', type: 'model', version: '1.0.0', description: null, files: [] }],
115
- npmDependencies: {},
116
- skillDependencies: {},
117
- warnings: [],
118
- },
119
- {
120
- model: {
121
- installMessage: message,
122
- saveOnInstall: true,
123
- readAssetDependenciesFromPackageJson: false,
124
- omitUnchangedInstalledFilesOnUpload: false,
125
- },
126
- },
127
- )
128
-
129
- assert.equal(
130
- output,
131
- `Installed:
132
- - thing@1.0.0 (model)
133
- note: ${Array(13).fill('word').join(' ')}
134
- ${Array(7).fill('word').join(' ')}`,
135
- )
136
- })
137
-
138
- test('installResult lists installed skills with their source', () => {
139
- assert.equal(
140
- installResult({
141
- assets: [
142
- {
143
- name: 'wooden-chair',
144
- type: 'model',
145
- version: '1.2.0',
146
- description: null,
147
- files: [],
148
- },
149
- ],
150
- npmDependencies: {},
151
- skillDependencies: {
152
- 'web-design-guidelines': 'vercel-labs/agent-skills/tree/main/skills/web-design-guidelines',
153
- 'local-skill': '/abs/public/skills/local-skill',
154
- },
155
- warnings: [],
156
- }),
157
- `Installed:
158
- - wooden-chair@1.2.0 (model)
159
- - local-skill ← /abs/public/skills/local-skill (skill)
160
- - web-design-guidelines ← vercel-labs/agent-skills/tree/main/skills/web-design-guidelines (skill)`,
161
- )
162
- })
163
-
164
- test('installResult shows each installed asset description', () => {
165
- assert.equal(
166
- installResult({
167
- assets: [
168
- {
169
- name: 'wooden-chair',
170
- type: 'model',
171
- version: '1.2.0',
172
- description:
173
- 'A game-ready chair with baked texture detail and clean origin placement for room scenes.',
174
- files: [],
175
- },
176
- ],
177
- npmDependencies: {},
178
- skillDependencies: {},
179
- warnings: [],
180
- }),
181
- `Installed:
182
- - wooden-chair@1.2.0 (model)
183
- description: A game-ready chair with baked texture detail and clean origin
184
- placement for room scenes.`,
185
- )
186
- })
187
-
188
- test('listResult prints installed assets with file trees', () => {
189
- assert.equal(
190
- listResult({
191
- projectRoot: '/project',
192
- lockPath: '/project/.drawcall/market-lock.json',
193
- assets: [
194
- {
195
- name: 'wooden-chair',
196
- type: 'model',
197
- version: '1.2.0',
198
- files: ['public/models/wooden-chair.glb'],
199
- },
200
- {
201
- name: 'forest-sky',
202
- type: 'environment',
203
- version: '1.0.0',
204
- files: [
205
- 'public/environment/forest-sky-background.webp',
206
- 'public/environment/forest-sky.hdr',
207
- ],
208
- },
209
- ],
210
- }),
211
- `Installed assets: 2
212
- - wooden-chair@1.2.0 (model)
213
- files:
214
- public
215
- └─ models
216
- └─ wooden-chair.glb
217
- - forest-sky@1.0.0 (environment)
218
- files:
219
- public
220
- └─ environment
221
- ├─ forest-sky-background.webp
222
- └─ forest-sky.hdr`,
223
- )
224
- })
225
-
226
- test('listResult handles an empty inventory', () => {
227
- assert.equal(
228
- listResult({
229
- projectRoot: '/project',
230
- lockPath: '/project/.drawcall/market-lock.json',
231
- assets: [],
232
- }),
233
- 'No installed assets found.',
234
- )
235
- })
@@ -1,87 +0,0 @@
1
- import assert from 'node:assert/strict'
2
- import test from 'node:test'
3
- import { resolve, ResolutionError } from '../src/resolve.js'
4
-
5
- interface FakeAsset {
6
- latestVersion: string
7
- approved?: boolean
8
- npmDependencies?: Record<string, string>
9
- assetDependencies?: Record<string, string>
10
- skillDependencies?: Record<string, string>
11
- }
12
-
13
- function clientWith(assets: Record<string, FakeAsset>) {
14
- return {
15
- exact: async ({ name }: { name: string }) => {
16
- const asset = assets[name]
17
- if (!asset) return null
18
- return {
19
- name,
20
- latestVersion: asset.latestVersion,
21
- approved: asset.approved ?? true,
22
- npmDependencies: JSON.stringify(asset.npmDependencies ?? {}),
23
- assetDependencies: JSON.stringify(asset.assetDependencies ?? {}),
24
- skillDependencies: JSON.stringify(asset.skillDependencies ?? {}),
25
- }
26
- },
27
- } as never
28
- }
29
-
30
- test('resolve collects skill dependencies across transitive assets', async () => {
31
- const client = clientWith({
32
- root: {
33
- latestVersion: '1.0.0',
34
- assetDependencies: { dep: '*' },
35
- skillDependencies: { 'web-design-guidelines': 'vercel-labs/agent-skills' },
36
- },
37
- dep: {
38
- latestVersion: '2.0.0',
39
- skillDependencies: { 'local-skill': './public/skills/local-skill' },
40
- },
41
- })
42
-
43
- const result = await resolve(client, [{ name: 'root', range: '*' }])
44
-
45
- assert.deepEqual(result.skillDependencies, {
46
- 'web-design-guidelines': 'vercel-labs/agent-skills',
47
- 'local-skill': './public/skills/local-skill',
48
- })
49
- })
50
-
51
- test('resolve unions identical skill sources without complaint', async () => {
52
- const client = clientWith({
53
- root: {
54
- latestVersion: '1.0.0',
55
- assetDependencies: { dep: '*' },
56
- skillDependencies: { shared: 'vercel-labs/agent-skills' },
57
- },
58
- dep: {
59
- latestVersion: '1.0.0',
60
- skillDependencies: { shared: 'vercel-labs/agent-skills' },
61
- },
62
- })
63
-
64
- const result = await resolve(client, [{ name: 'root', range: '*' }])
65
-
66
- assert.deepEqual(result.skillDependencies, { shared: 'vercel-labs/agent-skills' })
67
- })
68
-
69
- test('resolve rejects the same skill label with conflicting sources', async () => {
70
- const client = clientWith({
71
- root: {
72
- latestVersion: '1.0.0',
73
- assetDependencies: { dep: '*' },
74
- skillDependencies: { shared: 'vercel-labs/agent-skills' },
75
- },
76
- dep: {
77
- latestVersion: '1.0.0',
78
- skillDependencies: { shared: 'other-org/other-skills' },
79
- },
80
- })
81
-
82
- await assert.rejects(
83
- resolve(client, [{ name: 'root', range: '*' }]),
84
- (err: unknown) =>
85
- err instanceof ResolutionError && /skill dependency conflict for "shared"/u.test(err.message),
86
- )
87
- })
@@ -1,63 +0,0 @@
1
- import assert from 'node:assert/strict'
2
- import test from 'node:test'
3
- import {
4
- parseSkillDeps,
5
- parseUploadDescription,
6
- parseVersionedDeps,
7
- } from '../src/commands/upload.js'
8
- import { MAX_ASSET_DESCRIPTION_LENGTH } from '../src/schemas.js'
9
-
10
- test('parseVersionedDeps parses name@range and defaults a missing range to *', () => {
11
- assert.deepEqual(parseVersionedDeps(['three@^0.178.0', 'gsap'], 'npm'), {
12
- three: '^0.178.0',
13
- gsap: '*',
14
- })
15
- })
16
-
17
- test('parseVersionedDeps keeps the scope marker on scoped npm packages', () => {
18
- assert.deepEqual(parseVersionedDeps(['@react-three/drei@^9.0.0', '@types/node'], 'npm'), {
19
- '@react-three/drei': '^9.0.0',
20
- '@types/node': '*',
21
- })
22
- })
23
-
24
- test('parseVersionedDeps rejects duplicates', () => {
25
- assert.throws(
26
- () => parseVersionedDeps(['three@^1', 'three@^2'], 'npm'),
27
- /Duplicate npm dependency/u,
28
- )
29
- })
30
-
31
- test('parseSkillDeps splits label=source on the first = only', () => {
32
- assert.deepEqual(
33
- parseSkillDeps([
34
- 'web-design=vercel-labs/agent-skills/tree/main/skills/web-design-guidelines',
35
- 'local=./public/skills/local',
36
- ]),
37
- {
38
- 'web-design': 'vercel-labs/agent-skills/tree/main/skills/web-design-guidelines',
39
- local: './public/skills/local',
40
- },
41
- )
42
- })
43
-
44
- test('parseSkillDeps rejects specs without a usable label=source', () => {
45
- assert.throws(() => parseSkillDeps(['no-separator']), /Use label=source/u)
46
- assert.throws(() => parseSkillDeps(['=missing-label']), /Use label=source/u)
47
- assert.throws(() => parseSkillDeps(['missing-source=']), /Use label=source/u)
48
- })
49
-
50
- test('parseSkillDeps rejects duplicate labels', () => {
51
- assert.throws(() => parseSkillDeps(['a=one', 'a=two']), /Duplicate skill dependency "a"/u)
52
- })
53
-
54
- test('parseUploadDescription rejects descriptions beyond the shared max length', () => {
55
- assert.equal(
56
- parseUploadDescription('A compact asset description.'),
57
- 'A compact asset description.',
58
- )
59
- assert.throws(
60
- () => parseUploadDescription('x'.repeat(MAX_ASSET_DESCRIPTION_LENGTH + 1)),
61
- /1000 characters or fewer/u,
62
- )
63
- })
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src",
6
- "types": ["node"]
7
- },
8
- "include": ["src"]
9
- }