@drawcall/market 0.1.26 → 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/dist/cli.js +15 -15
- package/dist/cli.js.map +1 -1
- package/dist/commands/install.d.ts +8 -4
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +24 -59
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/preview.d.ts +0 -2
- package/dist/commands/preview.d.ts.map +1 -1
- package/dist/commands/preview.js +4 -6
- package/dist/commands/preview.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/install.js +6 -8
- package/dist/install.js.map +1 -1
- package/dist/resolve.d.ts +4 -3
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +38 -19
- package/dist/resolve.js.map +1 -1
- package/dist/skill.d.ts +1 -1
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js +16 -12
- package/dist/skill.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +26 -32
- package/src/commands/install.ts +30 -77
- package/src/commands/preview.ts +5 -10
- package/src/config.ts +6 -0
- package/src/install.ts +6 -7
- package/src/resolve.ts +46 -21
- package/src/skill.ts +16 -12
- package/tests/install-command.test.ts +13 -46
package/dist/skill.js
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
|
|
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 --
|
|
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.
|
|
22
|
-
3.
|
|
23
|
-
4.
|
|
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.
|
|
26
|
-
7.
|
|
27
|
-
8.
|
|
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
|
|
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
|
|
package/dist/skill.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill.js","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG
|
|
1
|
+
{"version":3,"file":"skill.js","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6C1B,CAAA"}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -56,7 +56,10 @@ program
|
|
|
56
56
|
const client = createMarketClient({ baseUrl, authToken: token })
|
|
57
57
|
const profile = await client.user.getProfile()
|
|
58
58
|
if (!profile) throw new Error('The Market API did not accept the auth token.')
|
|
59
|
-
|
|
59
|
+
// Only pin baseUrl in config when the user explicitly chose one. Persisting
|
|
60
|
+
// the default would freeze logged-in users to a host the CLI can no longer
|
|
61
|
+
// change in a future release.
|
|
62
|
+
await saveConfig(opts.api ? { authToken: token, baseUrl: opts.api } : { authToken: token })
|
|
60
63
|
console.log(loginResult(profile.email, getConfigPath()))
|
|
61
64
|
})
|
|
62
65
|
|
|
@@ -69,25 +72,18 @@ program
|
|
|
69
72
|
|
|
70
73
|
program
|
|
71
74
|
.command('install')
|
|
72
|
-
.description('Install by name
|
|
73
|
-
.argument('<assets...>', '
|
|
74
|
-
.addOption(typeOption)
|
|
75
|
+
.description('Install assets by name')
|
|
76
|
+
.argument('<assets...>', 'Asset names, optionally with @range')
|
|
75
77
|
.addOption(apiOption)
|
|
76
78
|
.option('--unapproved', 'Include unapproved versions', false)
|
|
77
79
|
.option('--cwd <dir>', 'Project directory')
|
|
78
|
-
.action(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
baseUrl: opts.api,
|
|
86
|
-
unapproved: opts.unapproved,
|
|
87
|
-
cwd: opts.cwd,
|
|
88
|
-
})
|
|
89
|
-
},
|
|
90
|
-
)
|
|
80
|
+
.action(async (args: string[], opts: { api?: string; unapproved: boolean; cwd?: string }) => {
|
|
81
|
+
await installCommand(args, {
|
|
82
|
+
baseUrl: opts.api,
|
|
83
|
+
unapproved: opts.unapproved,
|
|
84
|
+
cwd: opts.cwd,
|
|
85
|
+
})
|
|
86
|
+
})
|
|
91
87
|
|
|
92
88
|
program
|
|
93
89
|
.command('search')
|
|
@@ -109,12 +105,7 @@ program
|
|
|
109
105
|
verbose: boolean
|
|
110
106
|
},
|
|
111
107
|
) => {
|
|
112
|
-
|
|
113
|
-
console.error(
|
|
114
|
-
errorResult(`Search requires --type. Available types: ${ASSET_TYPES.join(', ')}`),
|
|
115
|
-
)
|
|
116
|
-
process.exit(1)
|
|
117
|
-
}
|
|
108
|
+
requireType(opts.type, 'Search')
|
|
118
109
|
await searchCommand(query, {
|
|
119
110
|
type: opts.type,
|
|
120
111
|
baseUrl: opts.api,
|
|
@@ -142,12 +133,7 @@ program
|
|
|
142
133
|
description: string,
|
|
143
134
|
opts: { type?: AssetType; api?: string; version?: string; cwd?: string },
|
|
144
135
|
) => {
|
|
145
|
-
|
|
146
|
-
console.error(
|
|
147
|
-
errorResult(`Upload requires --type. Available types: ${ASSET_TYPES.join(', ')}`),
|
|
148
|
-
)
|
|
149
|
-
process.exit(1)
|
|
150
|
-
}
|
|
136
|
+
requireType(opts.type, 'Upload')
|
|
151
137
|
await uploadCommand(name, zipFilter, description, {
|
|
152
138
|
type: opts.type,
|
|
153
139
|
version: opts.version,
|
|
@@ -162,7 +148,6 @@ program
|
|
|
162
148
|
.description('Save preview image')
|
|
163
149
|
.argument('<name>', 'Asset name')
|
|
164
150
|
.argument('[version]', 'Semver version')
|
|
165
|
-
.addOption(typeOption)
|
|
166
151
|
.addOption(apiOption)
|
|
167
152
|
.option('--unapproved', 'Include unapproved versions', false)
|
|
168
153
|
.option('--out <file>', 'Output image path')
|
|
@@ -170,10 +155,9 @@ program
|
|
|
170
155
|
async (
|
|
171
156
|
name: string,
|
|
172
157
|
version: string | undefined,
|
|
173
|
-
opts: {
|
|
158
|
+
opts: { api?: string; unapproved: boolean; out?: string },
|
|
174
159
|
) => {
|
|
175
160
|
await previewCommand(name, version, {
|
|
176
|
-
type: opts.type,
|
|
177
161
|
baseUrl: opts.api,
|
|
178
162
|
unapproved: opts.unapproved,
|
|
179
163
|
out: opts.out,
|
|
@@ -189,6 +173,7 @@ program
|
|
|
189
173
|
.addOption(apiOption)
|
|
190
174
|
.option('--cwd <dir>', 'Project directory')
|
|
191
175
|
.action(async (description: string, opts: { type?: AssetType; api?: string; cwd?: string }) => {
|
|
176
|
+
requireType(opts.type, 'Generate')
|
|
192
177
|
await generateCommand(description, {
|
|
193
178
|
type: opts.type,
|
|
194
179
|
baseUrl: opts.api,
|
|
@@ -207,6 +192,15 @@ program.parseAsync().catch((err) => {
|
|
|
207
192
|
process.exit(1)
|
|
208
193
|
})
|
|
209
194
|
|
|
195
|
+
function requireType(type: AssetType | undefined, command: string): asserts type is AssetType {
|
|
196
|
+
if (!type) {
|
|
197
|
+
console.error(
|
|
198
|
+
errorResult(`${command} requires --type. Available types: ${ASSET_TYPES.join(', ')}`),
|
|
199
|
+
)
|
|
200
|
+
process.exit(1)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
210
204
|
function parseSearchLimit(value: string): number {
|
|
211
205
|
const parsed = Number.parseInt(value, 10)
|
|
212
206
|
if (!Number.isInteger(parsed) || parsed < 1) {
|
package/src/commands/install.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import ora
|
|
1
|
+
import ora from 'ora'
|
|
2
2
|
import { resolve } from '../resolve.js'
|
|
3
3
|
import { install as runInstall } from '../install.js'
|
|
4
|
-
import {
|
|
5
|
-
import { assetNameSchema, type AssetType } from '../schemas.js'
|
|
4
|
+
import { assetNameSchema } from '../schemas.js'
|
|
6
5
|
import { getCliClient } from '../cli-client.js'
|
|
7
|
-
import {
|
|
6
|
+
import { installResult } from '../output.js'
|
|
8
7
|
import type { MarketClient } from '../client.js'
|
|
9
8
|
|
|
10
9
|
export interface InstallCommandOptions {
|
|
11
|
-
type?: AssetType
|
|
12
10
|
unapproved?: boolean
|
|
13
11
|
cwd?: string
|
|
14
12
|
baseUrl?: string
|
|
@@ -20,8 +18,8 @@ interface AssetRequest {
|
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
export async function installCommand(args: string[], opts: InstallCommandOptions): Promise<void> {
|
|
23
|
-
const { client
|
|
24
|
-
const
|
|
21
|
+
const { client } = await getCliClient({ baseUrl: opts.baseUrl })
|
|
22
|
+
const includeUnapproved = opts.unapproved ?? false
|
|
25
23
|
|
|
26
24
|
const spinner = ora({
|
|
27
25
|
isEnabled: Boolean(process.stderr.isTTY),
|
|
@@ -31,15 +29,11 @@ export async function installCommand(args: string[], opts: InstallCommandOptions
|
|
|
31
29
|
const requests: AssetRequest[] = []
|
|
32
30
|
for (const arg of args) {
|
|
33
31
|
spinner.text = `Resolving "${truncate(arg, 40)}"`
|
|
34
|
-
requests.push(
|
|
35
|
-
await resolveArg(client, arg, opts.type, spinner, canGenerate, opts.unapproved ?? false),
|
|
36
|
-
)
|
|
32
|
+
requests.push(await resolveArg(client, arg, includeUnapproved))
|
|
37
33
|
}
|
|
38
34
|
|
|
39
35
|
spinner.text = 'Resolving dependency tree'
|
|
40
|
-
const resolution = await resolve(client.asset, requests, {
|
|
41
|
-
includeUnapproved: opts.unapproved ?? false,
|
|
42
|
-
})
|
|
36
|
+
const resolution = await resolve(client.asset, requests, { includeUnapproved })
|
|
43
37
|
|
|
44
38
|
const result = await runInstall(client, resolution, {
|
|
45
39
|
cwd: opts.cwd,
|
|
@@ -56,82 +50,41 @@ export async function installCommand(args: string[], opts: InstallCommandOptions
|
|
|
56
50
|
}
|
|
57
51
|
}
|
|
58
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a single `name` or `name@range` argument to an exact asset request.
|
|
55
|
+
* Asset names are globally unique, so the name fully determines the asset — no
|
|
56
|
+
* `--type` is needed. install never fuzzy-searches or generates: it points the
|
|
57
|
+
* caller at `market search` / `market generate` instead, which keeps behavior
|
|
58
|
+
* predictable for agents.
|
|
59
|
+
*/
|
|
59
60
|
export async function resolveArg(
|
|
60
61
|
client: MarketClient,
|
|
61
62
|
arg: string,
|
|
62
|
-
type: AssetType | undefined,
|
|
63
|
-
spinner: Ora,
|
|
64
|
-
canGenerate: boolean,
|
|
65
63
|
includeUnapproved: boolean,
|
|
66
64
|
): Promise<AssetRequest> {
|
|
67
65
|
const parsed = parseNameAndRange(arg)
|
|
68
|
-
if (parsed) {
|
|
69
|
-
|
|
70
|
-
name
|
|
71
|
-
|
|
72
|
-
includeUnapproved,
|
|
73
|
-
})
|
|
74
|
-
if (existing) return parsed
|
|
75
|
-
|
|
76
|
-
if (!includeUnapproved) {
|
|
77
|
-
const hidden = await client.asset.exact({
|
|
78
|
-
name: parsed.name,
|
|
79
|
-
type,
|
|
80
|
-
includeUnapproved: true,
|
|
81
|
-
})
|
|
82
|
-
if (hidden) {
|
|
83
|
-
throw new Error(
|
|
84
|
-
`Asset "${parsed.name}" exists but has no approved versions. Re-run with \`--unapproved\` if you have access.`,
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (arg.includes('@')) {
|
|
90
|
-
return parsed
|
|
91
|
-
}
|
|
66
|
+
if (!parsed) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Invalid asset name "${arg}". Use \`market search --type <type> <query>\` to find assets.`,
|
|
69
|
+
)
|
|
92
70
|
}
|
|
93
71
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
query: arg,
|
|
97
|
-
type,
|
|
98
|
-
includeUnapproved,
|
|
99
|
-
limit: 1,
|
|
100
|
-
page: 1,
|
|
101
|
-
sort: 'relevance',
|
|
102
|
-
})
|
|
103
|
-
if (results.items.length > 0) {
|
|
104
|
-
const hit = results.items[0]
|
|
105
|
-
if (process.stderr.isTTY) {
|
|
106
|
-
spinner.info(`Matched "${compact(arg, 48)}" to ${hit.name} (${hit.type})`)
|
|
107
|
-
spinner.start()
|
|
108
|
-
}
|
|
109
|
-
return { name: hit.name, range: '*' }
|
|
110
|
-
}
|
|
72
|
+
const existing = await client.asset.exact({ name: parsed.name, includeUnapproved })
|
|
73
|
+
if (existing) return parsed
|
|
111
74
|
|
|
112
|
-
if (!
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
75
|
+
if (!includeUnapproved) {
|
|
76
|
+
const hidden = await client.asset.exact({ name: parsed.name, includeUnapproved: true })
|
|
77
|
+
if (hidden) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Asset "${parsed.name}" exists but has no approved versions. Re-run with \`--unapproved\` if you have access.`,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
116
82
|
}
|
|
117
83
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
{ description: arg, type },
|
|
122
|
-
{
|
|
123
|
-
onProgress: (msg) => {
|
|
124
|
-
spinner.text = msg
|
|
125
|
-
},
|
|
126
|
-
},
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Asset "${parsed.name}" not found. Use \`market search\` to find assets or ` +
|
|
86
|
+
`\`market generate --type <type>\` to create one.`,
|
|
127
87
|
)
|
|
128
|
-
if (process.stderr.isTTY) {
|
|
129
|
-
spinner.info(
|
|
130
|
-
`${generatedInstallResult(generated.assetName, generated.version)} for "${compact(arg, 48)}"`,
|
|
131
|
-
)
|
|
132
|
-
spinner.start()
|
|
133
|
-
}
|
|
134
|
-
return { name: generated.assetName, range: generated.version }
|
|
135
88
|
}
|
|
136
89
|
|
|
137
90
|
function parseNameAndRange(arg: string): AssetRequest | null {
|
package/src/commands/preview.ts
CHANGED
|
@@ -3,10 +3,9 @@ import * as path from 'path'
|
|
|
3
3
|
import ora from 'ora'
|
|
4
4
|
import { getCliClient } from '../cli-client.js'
|
|
5
5
|
import { previewResult } from '../output.js'
|
|
6
|
-
import { assetNameSchema, semverSchema
|
|
6
|
+
import { assetNameSchema, semverSchema } from '../schemas.js'
|
|
7
7
|
|
|
8
8
|
export interface PreviewCommandOptions {
|
|
9
|
-
type?: AssetType
|
|
10
9
|
unapproved?: boolean
|
|
11
10
|
out?: string
|
|
12
11
|
baseUrl?: string
|
|
@@ -19,13 +18,10 @@ export async function previewCommand(
|
|
|
19
18
|
): Promise<void> {
|
|
20
19
|
const parsedName = assetNameSchema.parse(name)
|
|
21
20
|
const parsedVersion = version ? semverSchema.parse(version) : undefined
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
) {
|
|
27
|
-
throw new Error(`Preview images are not supported for ${opts.type}`)
|
|
28
|
-
}
|
|
21
|
+
// The asset name is globally unique, so the server resolves the type. Whether
|
|
22
|
+
// that type supports preview images is also a server concern: the API checks
|
|
23
|
+
// for a provider `downloadPreviewImage` capability and returns a clear,
|
|
24
|
+
// type-specific error rather than us mirroring a list that can drift.
|
|
29
25
|
|
|
30
26
|
const spinner = ora({
|
|
31
27
|
text: `Resolving preview for ${parsedName}`,
|
|
@@ -40,7 +36,6 @@ export async function previewCommand(
|
|
|
40
36
|
(
|
|
41
37
|
await client.asset.exact({
|
|
42
38
|
name: parsedName,
|
|
43
|
-
type: opts.type,
|
|
44
39
|
includeUnapproved: opts.unapproved ?? false,
|
|
45
40
|
})
|
|
46
41
|
)?.latestVersion
|
package/src/config.ts
CHANGED
|
@@ -41,8 +41,14 @@ export async function loadConfig(): Promise<Config | null> {
|
|
|
41
41
|
export async function saveConfig(config: Config): Promise<void> {
|
|
42
42
|
const dir = configDir()
|
|
43
43
|
await fs.mkdir(dir, { recursive: true })
|
|
44
|
+
// `mode` on writeFile/mkdir only applies when the path is created, so chmod
|
|
45
|
+
// explicitly to protect a pre-existing (possibly world-readable) token file
|
|
46
|
+
// and its directory. A chmod failure means we could not secure the token, so
|
|
47
|
+
// let it surface rather than silently leaving it exposed.
|
|
48
|
+
await fs.chmod(dir, 0o700)
|
|
44
49
|
const file = configPath()
|
|
45
50
|
await fs.writeFile(file, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 })
|
|
51
|
+
await fs.chmod(file, 0o600)
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
export async function clearConfig(): Promise<boolean> {
|
package/src/install.ts
CHANGED
|
@@ -147,19 +147,18 @@ async function installNpmDeps(
|
|
|
147
147
|
if (Object.keys(deps).length === 0) return
|
|
148
148
|
|
|
149
149
|
const pkgPath = path.join(projectRoot, 'package.json')
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
150
|
+
// Only synthesize a package.json when none exists. A malformed existing file
|
|
151
|
+
// is a real error that must surface — never silently overwrite it.
|
|
152
|
+
const pkg: PackageJson = (await isFile(pkgPath))
|
|
153
|
+
? (JSON.parse(await fs.readFile(pkgPath, 'utf-8')) as PackageJson)
|
|
154
|
+
: { name: 'my-project', private: true, dependencies: {} }
|
|
156
155
|
|
|
157
156
|
pkg.dependencies = { ...pkg.dependencies, ...deps }
|
|
158
157
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
159
158
|
|
|
160
159
|
log(`Installing npm dependencies: ${Object.keys(deps).join(', ')}`)
|
|
161
160
|
|
|
162
|
-
const pm = await detectPackageManager(projectRoot)
|
|
161
|
+
const pm = await detectPackageManager(projectRoot)
|
|
163
162
|
const pmName = pm?.name ?? 'npm'
|
|
164
163
|
|
|
165
164
|
log(`Using ${pmName}...`)
|
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,
|
|
6
|
-
* every dependent.
|
|
7
|
-
*
|
|
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
|
-
`
|
|
73
|
-
`
|
|
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
|
|
78
|
-
const assetDeps
|
|
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)
|
|
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
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
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 --
|
|
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.
|
|
22
|
-
3.
|
|
23
|
-
4.
|
|
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.
|
|
26
|
-
7.
|
|
27
|
-
8.
|
|
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
|
|
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
|
|