@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 +6 -1
- package/skills/market/SKILL.md +69 -0
- package/tests/install-command.test.ts +0 -67
- package/tests/install-layout.test.ts +0 -307
- package/tests/list-command.test.ts +0 -76
- package/tests/output.test.ts +0 -235
- package/tests/resolve-skills.test.ts +0 -87
- package/tests/upload-deps.test.ts +0 -63
- package/tsconfig.json +0 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drawcall/market",
|
|
3
|
-
"version": "0.1.
|
|
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
|
-
})
|
package/tests/output.test.ts
DELETED
|
@@ -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
|
-
})
|