@drawcall/market 0.1.25 → 0.1.27

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/src/resolve.ts CHANGED
@@ -2,9 +2,10 @@
2
2
  * Dependency resolver for market assets.
3
3
  *
4
4
  * 1. Collects the target assets and all transitive asset dependencies.
5
- * 2. For each asset, finds a version that satisfies ALL constraints from
6
- * every dependent.
7
- * 3. Merges npm dependency ranges across all resolved assets and checks
5
+ * 2. For each asset, takes the latest published version and verifies it
6
+ * satisfies ALL constraints from every dependent. (Only the latest
7
+ * version is resolved; older versions are not considered.)
8
+ * 3. Intersects npm dependency ranges across all resolved assets and checks
8
9
  * that they are compatible.
9
10
  */
10
11
 
@@ -58,24 +59,26 @@ export async function resolve(
58
59
  }
59
60
 
60
61
  if (!opts.includeUnapproved && !meta.approved) {
61
- throw new ResolutionError(
62
- `Asset "${assetName}" has no ${opts.includeUnapproved ? '' : 'approved '}versions.`,
63
- )
62
+ throw new ResolutionError(`Asset "${assetName}" has no approved versions.`)
64
63
  }
65
64
 
65
+ // The market publishes immutable versions and resolution targets the
66
+ // latest published version of each asset. A range that excludes the
67
+ // latest version is therefore unsatisfiable (older versions are not
68
+ // resolved), which this message makes explicit.
66
69
  const assetConstraints = constraints.get(assetName) ?? []
67
70
  if (!assetConstraints.every((c) => semver.satisfies(meta.latestVersion, c.range))) {
68
71
  const constraintDesc = assetConstraints
69
72
  .map((c) => ` ${c.range} (from ${c.from})`)
70
73
  .join('\n')
71
74
  throw new ResolutionError(
72
- `No version of "${assetName}" satisfies all constraints:\n${constraintDesc}\n` +
73
- `Available${opts.includeUnapproved ? '' : ' approved'}: ${meta.latestVersion}`,
75
+ `The latest${opts.includeUnapproved ? '' : ' approved'} version of "${assetName}" ` +
76
+ `(${meta.latestVersion}) does not satisfy all constraints:\n${constraintDesc}`,
74
77
  )
75
78
  }
76
79
 
77
- const npmDeps: Record<string, string> = JSON.parse(meta.npmDependencies)
78
- const assetDeps: Record<string, string> = JSON.parse(meta.assetDependencies)
80
+ const npmDeps = parseDependencies(meta.npmDependencies, assetName, 'npm')
81
+ const assetDeps = parseDependencies(meta.assetDependencies, assetName, 'asset')
79
82
 
80
83
  resolved.set(assetName, {
81
84
  name: assetName,
@@ -115,6 +118,22 @@ export async function resolve(
115
118
  }
116
119
  }
117
120
 
121
+ function parseDependencies(
122
+ value: string,
123
+ assetName: string,
124
+ kind: 'npm' | 'asset',
125
+ ): Record<string, string> {
126
+ try {
127
+ const parsed = JSON.parse(value || '{}') as unknown
128
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
129
+ return parsed as Record<string, string>
130
+ }
131
+ } catch {
132
+ // fall through to a clear error below
133
+ }
134
+ throw new ResolutionError(`Asset "${assetName}" has malformed ${kind} dependency metadata.`)
135
+ }
136
+
118
137
  function addConstraint(
119
138
  constraints: Map<string, { range: string; from: string }[]>,
120
139
  name: string,
@@ -151,7 +170,8 @@ async function fetchMeta(
151
170
  /**
152
171
  * Merge npm dependency ranges from all resolved assets.
153
172
  * For each package, check that all declared ranges are compatible
154
- * (using semver.intersects). Return the narrowest range.
173
+ * (using semver.intersects), then return the single declared range that is a
174
+ * subset of every other — the true intersection.
155
175
  */
156
176
  function mergeNpmDependencies(assets: ResolvedAsset[]): Record<string, string> {
157
177
  // Collect all ranges per package
@@ -181,18 +201,23 @@ function mergeNpmDependencies(assets: ResolvedAsset[]): Record<string, string> {
181
201
  }
182
202
  }
183
203
 
184
- // Use the narrowest (most constrained) range.
185
- // Simple heuristic: pick the range with the highest minimum version.
186
- let narrowest = ranges[0].range
187
- for (const r of ranges.slice(1)) {
188
- const minCurrent = semver.minVersion(narrowest)
189
- const minNew = semver.minVersion(r.range)
190
- if (minCurrent && minNew && semver.gt(minNew, minCurrent)) {
191
- narrowest = r.range
192
- }
204
+ // Use the single declared range that is contained in every other declared
205
+ // range the true intersection of all constraints. Picking by "highest
206
+ // minimum version" is unsound: a range with a higher minimum can also have
207
+ // a higher (or open) upper bound, silently dropping another dependent's
208
+ // upper bound. If no single range represents the intersection, it cannot be
209
+ // expressed as one package.json range, so fail loudly.
210
+ const narrowest = ranges.find((candidate) =>
211
+ ranges.every((other) => semver.subset(candidate.range, other.range)),
212
+ )
213
+ if (!narrowest) {
214
+ throw new ResolutionError(
215
+ `npm dependency ranges for "${pkg}" overlap but cannot be combined into a single range:\n` +
216
+ ranges.map((r) => ` ${r.range} (from ${r.from})`).join('\n'),
217
+ )
193
218
  }
194
219
 
195
- merged[pkg] = narrowest
220
+ merged[pkg] = narrowest.range
196
221
  }
197
222
 
198
223
  return merged
package/src/skill.ts CHANGED
@@ -5,36 +5,40 @@ description: Find, preview, install, generate, and publish Drawcall Market asset
5
5
 
6
6
  # Drawcall Market
7
7
 
8
- Use the \`market\` CLI. Keep commands short and read the summary line plus asset bullets.
8
+ Use the \`market\` CLI. Keep commands short and read the summary lines.
9
9
 
10
10
  ## Quick Start
11
11
 
12
12
  \`\`\`sh
13
13
  market search "wooden chair" --type model --limit 3
14
- market install wooden-chair --type model --cwd "$PWD"
14
+ market install wooden-chair --cwd "$PWD"
15
15
  market preview wooden-chair --out /tmp/wooden-chair.png
16
16
  \`\`\`
17
17
 
18
18
  ## Workflow
19
19
 
20
- 1. Search first unless the user gave an exact asset name.
21
- 2. Search requires \`--type\`; use \`model\` unless the user names another supported type: \`humanoid-model\`, \`texture\`, \`humanoid-animation\`, \`template\`, \`sound-effect\`, \`background-music\`, or \`environment\`.
22
- 3. Use \`--limit 1\` for lookup, \`--limit 3\` for choice. Search caps at 5.
23
- 4. Use \`--verbose\` only when the one-line descriptions are not enough.
20
+ 1. 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\`, or \`environment\`.
21
+ 2. Use \`--limit 1\` for lookup, \`--limit 3\` for choice. Search caps at 5. Add \`--verbose\` only when the one-line descriptions are not enough.
22
+ 3. \`install\` takes one or more exact asset names (optionally \`name@range\`); it does not search or generate. Find names with \`search\` first. No \`--type\` is needed — asset names are unique.
23
+ 4. \`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.
24
24
  5. Use \`--unapproved\` only when the user asks for unapproved/private/admin assets. Do not install unapproved assets without explicit acceptance.
25
- 6. Preview before installing when visual fit matters: \`market preview <name> --out <file>\`. Preview is not supported for \`humanoid-animation\`, \`template\`, \`sound-effect\`, or \`background-music\`.
26
- 7. Generate only when the user wants a new asset, auth is available, and the requested asset type supports generation. No asset type currently supports generation.
27
- 8. Upload only when publishing is requested.
28
- 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.
25
+ 6. \`generate --type <type> "<prompt>"\` creates a new asset; it requires login and a type that supports generation. No asset type currently supports generation.
26
+ 7. Upload only when publishing is requested: \`market upload <name> <zip> "<description>" --type <type>\`.
27
+ 8. 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.
29
28
 
30
29
  ## Output
31
30
 
32
- Commands print concise summaries:
31
+ Commands print concise, line-oriented summaries:
33
32
 
34
33
  \`\`\`text
35
34
  Results: 2/8 query="wooden chair" type=model approval=approved
36
35
  - wooden-chair@1.0.0 | model | approved | Low-poly wooden chair
37
- Installed 1 asset: wooden-chair@1.0.0
36
+ Installed:
37
+ - wooden-chair@1.0.0 (asset)
38
+ files:
39
+ public/model
40
+ └─ wooden-chair.glb
41
+ - three@^0.178.0 (npm)
38
42
  Saved preview for wooden-chair@1.0.0: /tmp/wooden-chair.png
39
43
  \`\`\`
40
44
 
@@ -2,17 +2,10 @@ import assert from 'node:assert/strict'
2
2
  import test from 'node:test'
3
3
  import { resolveArg } from '../src/commands/install.js'
4
4
 
5
- test('resolveArg reports unapproved explicit assets instead of falling through to generation', async () => {
6
- let searchCalls = 0
5
+ test('resolveArg reports unapproved explicit assets clearly', async () => {
7
6
  const client = {
8
7
  asset: {
9
- exact: async ({
10
- includeUnapproved,
11
- }: {
12
- name: string
13
- type?: string
14
- includeUnapproved: boolean
15
- }) =>
8
+ exact: async ({ includeUnapproved }: { name: string; includeUnapproved: boolean }) =>
16
9
  includeUnapproved
17
10
  ? {
18
11
  name: 'qwantani-moon-noon-puresky',
@@ -20,59 +13,33 @@ test('resolveArg reports unapproved explicit assets instead of falling through t
20
13
  approved: false,
21
14
  }
22
15
  : null,
23
- search: async () => {
24
- searchCalls += 1
25
- return { items: [], total: 0, page: 1, limit: 1, totalPages: 0 }
26
- },
27
16
  },
28
17
  } as never
29
18
 
30
19
  await assert.rejects(
31
- resolveArg(
32
- client,
33
- 'qwantani-moon-noon-puresky@1.0.2',
34
- 'environment',
35
- silentSpinner(),
36
- true,
37
- false,
38
- ),
20
+ resolveArg(client, 'qwantani-moon-noon-puresky@1.0.2', false),
39
21
  /has no approved versions/u,
40
22
  )
41
-
42
- assert.equal(searchCalls, 0)
43
23
  })
44
24
 
45
- test('resolveArg keeps explicit versioned asset refs exact when nothing matches yet', async () => {
46
- let searchCalls = 0
25
+ test('resolveArg keeps explicit versioned refs exact when the asset exists', async () => {
47
26
  const client = {
48
27
  asset: {
49
- exact: async () => null,
50
- search: async () => {
51
- searchCalls += 1
52
- return { items: [], total: 0, page: 1, limit: 1, totalPages: 0 }
53
- },
28
+ exact: async () => ({ name: 'fresh-sky', latestVersion: '1.2.3', approved: true }),
54
29
  },
55
30
  } as never
56
31
 
57
- const request = await resolveArg(
58
- client,
59
- 'fresh-sky@1.2.3',
60
- 'environment',
61
- silentSpinner(),
62
- true,
63
- false,
64
- )
32
+ const request = await resolveArg(client, 'fresh-sky@1.2.3', false)
65
33
 
66
34
  assert.deepEqual(request, { name: 'fresh-sky', range: '1.2.3' })
67
- assert.equal(searchCalls, 0)
68
35
  })
69
36
 
70
- function silentSpinner() {
71
- return {
72
- text: '',
73
- info() {},
74
- start() {
75
- return this
37
+ test('resolveArg errors when the asset does not exist instead of deferring', async () => {
38
+ const client = {
39
+ asset: {
40
+ exact: async () => null,
76
41
  },
77
42
  } as never
78
- }
43
+
44
+ await assert.rejects(resolveArg(client, 'does-not-exist@1.2.3', false), /not found/u)
45
+ })