@drawcall/market 0.1.35 → 0.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +6 -3
  2. package/dist/asset-implementation.d.ts +16 -7
  3. package/dist/asset-implementation.d.ts.map +1 -1
  4. package/dist/asset-implementation.js +20 -1
  5. package/dist/asset-implementation.js.map +1 -1
  6. package/dist/cli.js +14 -2
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands/install.d.ts +3 -4
  9. package/dist/commands/install.d.ts.map +1 -1
  10. package/dist/commands/install.js +57 -11
  11. package/dist/commands/install.js.map +1 -1
  12. package/dist/commands/list.d.ts +17 -0
  13. package/dist/commands/list.d.ts.map +1 -0
  14. package/dist/commands/list.js +26 -0
  15. package/dist/commands/list.js.map +1 -0
  16. package/dist/commands/upload.d.ts.map +1 -1
  17. package/dist/commands/upload.js +64 -2
  18. package/dist/commands/upload.js.map +1 -1
  19. package/dist/contract.d.ts +9 -1
  20. package/dist/contract.d.ts.map +1 -1
  21. package/dist/contract.js +1 -1
  22. package/dist/contract.js.map +1 -1
  23. package/dist/index.d.ts +5 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +2 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/install.d.ts +14 -0
  28. package/dist/install.d.ts.map +1 -1
  29. package/dist/install.js +133 -23
  30. package/dist/install.js.map +1 -1
  31. package/dist/market-lock.d.ts +17 -0
  32. package/dist/market-lock.d.ts.map +1 -0
  33. package/dist/market-lock.js +44 -0
  34. package/dist/market-lock.js.map +1 -0
  35. package/dist/output.d.ts +4 -2
  36. package/dist/output.d.ts.map +1 -1
  37. package/dist/output.js +28 -7
  38. package/dist/output.js.map +1 -1
  39. package/dist/package-json.d.ts +11 -0
  40. package/dist/package-json.d.ts.map +1 -0
  41. package/dist/package-json.js +23 -0
  42. package/dist/package-json.js.map +1 -0
  43. package/dist/skill.d.ts +1 -1
  44. package/dist/skill.d.ts.map +1 -1
  45. package/dist/skill.js +20 -9
  46. package/dist/skill.js.map +1 -1
  47. package/package.json +2 -2
  48. package/src/asset-implementation.ts +40 -6
  49. package/src/cli.ts +24 -6
  50. package/src/commands/install.ts +76 -16
  51. package/src/commands/list.ts +47 -0
  52. package/src/commands/upload.ts +84 -2
  53. package/src/contract.ts +10 -1
  54. package/src/index.ts +15 -0
  55. package/src/install.ts +197 -35
  56. package/src/market-lock.ts +65 -0
  57. package/src/output.ts +36 -8
  58. package/src/package-json.ts +37 -0
  59. package/src/skill.ts +20 -9
  60. package/tests/install-command.test.ts +23 -1
  61. package/tests/install-layout.test.ts +117 -0
  62. package/tests/list-command.test.ts +76 -0
  63. package/tests/output.test.ts +69 -3
@@ -5,6 +5,7 @@ import * as path from 'node:path'
5
5
  import test from 'node:test'
6
6
  import { zipSync } from 'fflate'
7
7
  import { findInstallRoot, install } from '../src/install.js'
8
+ import { MARKET_LOCK_PATH } from '../src/market-lock.js'
8
9
 
9
10
  const textEncoder = new TextEncoder()
10
11
 
@@ -53,6 +54,7 @@ test('install writes zip files into the package root', async () => {
53
54
  ],
54
55
  npmDependencies: {},
55
56
  skillDependencies: {},
57
+ warnings: [],
56
58
  })
57
59
  assert.equal(
58
60
  await fs.readFile(path.join(appRoot, 'public', 'humanoid-animation', 'idle-loop.glb'), 'utf-8'),
@@ -64,6 +66,15 @@ test('install writes zip files into the package root', async () => {
64
66
  )
65
67
  assert.equal(await fs.readFile(path.join(appRoot, 'README.md'), 'utf-8'), 'project readme\n')
66
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
+ )
67
78
  })
68
79
 
69
80
  test('install rejects zip paths that escape through parent segments', async () => {
@@ -149,6 +160,112 @@ test('install runs the skills runner per dependency, resolving local paths again
149
160
  'web-design-guidelines': 'vercel-labs/agent-skills/tree/main/skills/web-design-guidelines',
150
161
  'local-skill': './public/skills/local-skill',
151
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'])
152
269
  })
153
270
 
154
271
  test('findInstallRoot uses the nearest package.json, not the monorepo root', async () => {
@@ -0,0 +1,76 @@
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,6 +1,6 @@
1
1
  import assert from 'node:assert/strict'
2
2
  import test from 'node:test'
3
- import { assetSearchResultLine, installResult } from '../src/output.js'
3
+ import { assetSearchResultLine, installResult, listResult } from '../src/output.js'
4
4
 
5
5
  test('assetSearchResultLine prints the full description', () => {
6
6
  const description =
@@ -47,6 +47,7 @@ test('installResult prints asset type, file tree, and npm list', () => {
47
47
  ],
48
48
  npmDependencies: { '@react-three/drei': 'latest' },
49
49
  skillDependencies: {},
50
+ warnings: [],
50
51
  }),
51
52
  `Installed:
52
53
  - wooden-chair@1.2.0 (model)
@@ -87,9 +88,15 @@ test('installResult attaches each post-install note under its asset', () => {
87
88
  ],
88
89
  npmDependencies: {},
89
90
  skillDependencies: {},
91
+ warnings: [],
90
92
  },
91
93
  {
92
- model: 'Load the model.',
94
+ model: {
95
+ installMessage: 'Load the model.',
96
+ saveOnInstall: true,
97
+ readAssetDependenciesFromPackageJson: false,
98
+ omitUnchangedInstalledFilesOnUpload: false,
99
+ },
93
100
  // sound-effect intentionally omitted -> no note for `beep`.
94
101
  },
95
102
  ),
@@ -107,8 +114,16 @@ test('installResult word-wraps a long note with a hanging indent', () => {
107
114
  assets: [{ name: 'thing', type: 'model', version: '1.0.0', description: null, files: [] }],
108
115
  npmDependencies: {},
109
116
  skillDependencies: {},
117
+ warnings: [],
118
+ },
119
+ {
120
+ model: {
121
+ installMessage: message,
122
+ saveOnInstall: true,
123
+ readAssetDependenciesFromPackageJson: false,
124
+ omitUnchangedInstalledFilesOnUpload: false,
125
+ },
110
126
  },
111
- { model: message },
112
127
  )
113
128
 
114
129
  assert.equal(
@@ -137,6 +152,7 @@ test('installResult lists installed skills with their source', () => {
137
152
  'web-design-guidelines': 'vercel-labs/agent-skills/tree/main/skills/web-design-guidelines',
138
153
  'local-skill': '/abs/public/skills/local-skill',
139
154
  },
155
+ warnings: [],
140
156
  }),
141
157
  `Installed:
142
158
  - wooden-chair@1.2.0 (model)
@@ -160,6 +176,7 @@ test('installResult shows each installed asset description', () => {
160
176
  ],
161
177
  npmDependencies: {},
162
178
  skillDependencies: {},
179
+ warnings: [],
163
180
  }),
164
181
  `Installed:
165
182
  - wooden-chair@1.2.0 (model)
@@ -167,3 +184,52 @@ test('installResult shows each installed asset description', () => {
167
184
  placement for room scenes.`,
168
185
  )
169
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
+ })