@drawcall/market 0.1.26 → 0.1.28
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/asset-implementation.d.ts +1 -0
- package/dist/asset-implementation.d.ts.map +1 -1
- package/dist/cli.js +23 -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/commands/upload.d.ts +18 -0
- package/dist/commands/upload.d.ts.map +1 -1
- package/dist/commands/upload.js +67 -3
- package/dist/commands/upload.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/contract.d.ts +3 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +11 -2
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +69 -19
- package/dist/install.js.map +1 -1
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +6 -0
- package/dist/output.js.map +1 -1
- package/dist/resolve.d.ts +6 -3
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +66 -20
- package/dist/resolve.js.map +1 -1
- package/dist/schemas.d.ts +2 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +5 -0
- package/dist/schemas.js.map +1 -1
- package/dist/skill.d.ts +1 -1
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js +19 -12
- package/dist/skill.js.map +1 -1
- package/package.json +1 -1
- package/src/asset-implementation.ts +1 -0
- package/src/cli.ts +44 -33
- package/src/commands/install.ts +30 -77
- package/src/commands/preview.ts +5 -10
- package/src/commands/upload.ts +79 -3
- package/src/config.ts +6 -0
- package/src/contract.ts +2 -0
- package/src/index.ts +1 -0
- package/src/install.ts +95 -19
- package/src/output.ts +9 -0
- package/src/resolve.ts +81 -22
- package/src/schemas.ts +6 -0
- package/src/skill.ts +19 -12
- package/tests/install-command.test.ts +13 -46
- package/tests/install-layout.test.ts +60 -2
- package/tests/output.test.ts +18 -0
- package/tests/resolve-skills.test.ts +87 -0
- package/tests/upload-deps.test.ts +44 -0
package/src/install.ts
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
* 1. Downloads public asset files into ./public/... via the oRPC client.
|
|
5
5
|
* 2. Merges npm dependencies into package.json.
|
|
6
6
|
* 3. Runs the package manager to install npm deps.
|
|
7
|
+
* 4. Installs skill dependencies via the `skills` CLI (`skills add <source>`).
|
|
7
8
|
*
|
|
8
|
-
* NOTE: This module uses Node.js APIs (fs, path, nypm) and is
|
|
9
|
-
* used by the CLI binary; it is NOT exported from the package index.
|
|
9
|
+
* NOTE: This module uses Node.js APIs (fs, path, nypm, child_process) and is
|
|
10
|
+
* only used by the CLI binary; it is NOT exported from the package index.
|
|
10
11
|
*/
|
|
11
12
|
|
|
13
|
+
import { execFile } from 'child_process'
|
|
12
14
|
import * as fs from 'fs/promises'
|
|
13
15
|
import * as path from 'path'
|
|
14
16
|
import { unzipSync } from 'fflate'
|
|
@@ -28,6 +30,12 @@ export interface InstallOptions {
|
|
|
28
30
|
cwd?: string
|
|
29
31
|
/** Log progress */
|
|
30
32
|
onProgress?: (message: string) => void
|
|
33
|
+
/**
|
|
34
|
+
* Override how a single resolved skill source is installed. The default
|
|
35
|
+
* shells out to `npx skills add <source> -y`. Exposed for tests and for
|
|
36
|
+
* callers that want to drive a different skills runner.
|
|
37
|
+
*/
|
|
38
|
+
runSkillAdd?: (source: string, cwd: string) => Promise<void>
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
export interface InstalledAsset {
|
|
@@ -39,6 +47,8 @@ export interface InstalledAsset {
|
|
|
39
47
|
export interface InstallResult {
|
|
40
48
|
assets: InstalledAsset[]
|
|
41
49
|
npmDependencies: Record<string, string>
|
|
50
|
+
/** Installed skills, keyed by label, with the source passed to `skills add`. */
|
|
51
|
+
skillDependencies: Record<string, string>
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
export async function install(
|
|
@@ -54,28 +64,34 @@ export async function install(
|
|
|
54
64
|
installNpmDeps(resolution, installRoot, log),
|
|
55
65
|
])
|
|
56
66
|
|
|
67
|
+
// Skills are installed after asset files land on disk: a skill source may be
|
|
68
|
+
// a local path pointing at a skill directory shipped inside an installed
|
|
69
|
+
// asset, which only exists once downloadAssets has completed.
|
|
70
|
+
const skillDependencies = await installSkills(
|
|
71
|
+
resolution,
|
|
72
|
+
installRoot,
|
|
73
|
+
log,
|
|
74
|
+
opts.runSkillAdd ?? defaultRunSkillAdd,
|
|
75
|
+
)
|
|
76
|
+
|
|
57
77
|
return {
|
|
58
78
|
assets,
|
|
59
79
|
npmDependencies: resolution.npmDependencies,
|
|
80
|
+
skillDependencies,
|
|
60
81
|
}
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
export async function findInstallRoot(cwd: string = process.cwd()): Promise<string> {
|
|
64
85
|
const start = path.resolve(cwd)
|
|
65
|
-
let packageRoot: string | null = null
|
|
66
86
|
|
|
67
|
-
let dir = start
|
|
68
|
-
while (true) {
|
|
87
|
+
for (let dir = start; ; dir = path.dirname(dir)) {
|
|
69
88
|
if (await isFile(path.join(dir, 'package.json'))) {
|
|
70
|
-
|
|
89
|
+
return dir
|
|
90
|
+
}
|
|
91
|
+
if (path.dirname(dir) === dir) {
|
|
92
|
+
return start
|
|
71
93
|
}
|
|
72
|
-
|
|
73
|
-
const parent = path.dirname(dir)
|
|
74
|
-
if (parent === dir) break
|
|
75
|
-
dir = parent
|
|
76
94
|
}
|
|
77
|
-
|
|
78
|
-
return packageRoot ?? start
|
|
79
95
|
}
|
|
80
96
|
|
|
81
97
|
async function downloadAssets(
|
|
@@ -147,22 +163,82 @@ async function installNpmDeps(
|
|
|
147
163
|
if (Object.keys(deps).length === 0) return
|
|
148
164
|
|
|
149
165
|
const pkgPath = path.join(projectRoot, 'package.json')
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
166
|
+
// Only synthesize a package.json when none exists. A malformed existing file
|
|
167
|
+
// is a real error that must surface — never silently overwrite it.
|
|
168
|
+
const pkg: PackageJson = (await isFile(pkgPath))
|
|
169
|
+
? (JSON.parse(await fs.readFile(pkgPath, 'utf-8')) as PackageJson)
|
|
170
|
+
: { name: 'my-project', private: true, dependencies: {} }
|
|
156
171
|
|
|
157
172
|
pkg.dependencies = { ...pkg.dependencies, ...deps }
|
|
158
173
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
159
174
|
|
|
160
175
|
log(`Installing npm dependencies: ${Object.keys(deps).join(', ')}`)
|
|
161
176
|
|
|
162
|
-
const pm = await detectPackageManager(projectRoot)
|
|
177
|
+
const pm = await detectPackageManager(projectRoot)
|
|
163
178
|
const pmName = pm?.name ?? 'npm'
|
|
164
179
|
|
|
165
180
|
log(`Using ${pmName}...`)
|
|
166
181
|
await installDependencies({ cwd: projectRoot, packageManager: { name: pmName, command: pmName } })
|
|
167
182
|
log('npm dependencies installed.')
|
|
168
183
|
}
|
|
184
|
+
|
|
185
|
+
async function installSkills(
|
|
186
|
+
resolution: ResolveResult,
|
|
187
|
+
projectRoot: string,
|
|
188
|
+
log: (msg: string) => void,
|
|
189
|
+
runSkillAdd: (source: string, cwd: string) => Promise<void>,
|
|
190
|
+
): Promise<Record<string, string>> {
|
|
191
|
+
const skills = resolution.skillDependencies ?? {}
|
|
192
|
+
const entries = Object.entries(skills)
|
|
193
|
+
if (entries.length === 0) return {}
|
|
194
|
+
|
|
195
|
+
for (const [label, source] of entries) {
|
|
196
|
+
const resolvedSource = resolveSkillSource(source, projectRoot)
|
|
197
|
+
log(`Installing skill ${label} (${source})...`)
|
|
198
|
+
await runSkillAdd(resolvedSource, projectRoot)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
log(`Skills installed: ${entries.map(([label]) => label).join(', ')}`)
|
|
202
|
+
return skills
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Resolve a skill source to the argument passed to `skills add`. Local paths
|
|
207
|
+
* (`./`, `../`, `.`, `..`, or absolute) are resolved against the install root
|
|
208
|
+
* so they point at a skill directory shipped inside an installed asset; every
|
|
209
|
+
* other form (GitHub shorthand, git/HTTP(S) URL, `tree/<branch>/<subpath>`) is
|
|
210
|
+
* a remote ref and is passed through untouched.
|
|
211
|
+
*/
|
|
212
|
+
function resolveSkillSource(source: string, projectRoot: string): string {
|
|
213
|
+
return isLocalSkillPath(source) ? path.resolve(projectRoot, source) : source
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function isLocalSkillPath(source: string): boolean {
|
|
217
|
+
return (
|
|
218
|
+
path.isAbsolute(source) ||
|
|
219
|
+
source === '.' ||
|
|
220
|
+
source === '..' ||
|
|
221
|
+
source.startsWith('./') ||
|
|
222
|
+
source.startsWith('../') ||
|
|
223
|
+
/^[a-zA-Z]:[/\\]/.test(source)
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Install one skill via `npx skills add <source> -y`. When run inside a coding
|
|
229
|
+
* agent the `skills` CLI auto-detects the agent and installs non-interactively;
|
|
230
|
+
* `-y` keeps it non-interactive everywhere else. npx resolves (and caches) the
|
|
231
|
+
* `skills` package on first use.
|
|
232
|
+
*/
|
|
233
|
+
function defaultRunSkillAdd(source: string, cwd: string): Promise<void> {
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
execFile('npx', ['--yes', 'skills', 'add', source, '-y'], { cwd }, (error, _stdout, stderr) => {
|
|
236
|
+
if (error) {
|
|
237
|
+
const detail = stderr.trim() || error.message
|
|
238
|
+
reject(new Error(`Failed to install skill "${source}": ${detail}`))
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
resolve()
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
}
|
package/src/output.ts
CHANGED
|
@@ -57,6 +57,15 @@ export function installResult(result: InstallResult): string {
|
|
|
57
57
|
)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
const skillDependencies = Object.entries(result.skillDependencies ?? {})
|
|
61
|
+
if (skillDependencies.length > 0) {
|
|
62
|
+
lines.push(
|
|
63
|
+
...skillDependencies
|
|
64
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
65
|
+
.map(([label, source]) => `- ${label} ← ${source} (skill)`),
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
return lines.join('\n')
|
|
61
70
|
}
|
|
62
71
|
|
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
|
|
|
@@ -17,11 +18,13 @@ export interface ResolvedAsset {
|
|
|
17
18
|
version: string
|
|
18
19
|
npmDependencies: Record<string, string>
|
|
19
20
|
assetDependencies: Record<string, string>
|
|
21
|
+
skillDependencies: Record<string, string>
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export interface ResolveResult {
|
|
23
25
|
assets: ResolvedAsset[]
|
|
24
26
|
npmDependencies: Record<string, string>
|
|
27
|
+
skillDependencies: Record<string, string>
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
interface AssetRequest {
|
|
@@ -58,30 +61,34 @@ export async function resolve(
|
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
if (!opts.includeUnapproved && !meta.approved) {
|
|
61
|
-
throw new ResolutionError(
|
|
62
|
-
`Asset "${assetName}" has no ${opts.includeUnapproved ? '' : 'approved '}versions.`,
|
|
63
|
-
)
|
|
64
|
+
throw new ResolutionError(`Asset "${assetName}" has no approved versions.`)
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
// The market publishes immutable versions and resolution targets the
|
|
68
|
+
// latest published version of each asset. A range that excludes the
|
|
69
|
+
// latest version is therefore unsatisfiable (older versions are not
|
|
70
|
+
// resolved), which this message makes explicit.
|
|
66
71
|
const assetConstraints = constraints.get(assetName) ?? []
|
|
67
72
|
if (!assetConstraints.every((c) => semver.satisfies(meta.latestVersion, c.range))) {
|
|
68
73
|
const constraintDesc = assetConstraints
|
|
69
74
|
.map((c) => ` ${c.range} (from ${c.from})`)
|
|
70
75
|
.join('\n')
|
|
71
76
|
throw new ResolutionError(
|
|
72
|
-
`
|
|
73
|
-
`
|
|
77
|
+
`The latest${opts.includeUnapproved ? '' : ' approved'} version of "${assetName}" ` +
|
|
78
|
+
`(${meta.latestVersion}) does not satisfy all constraints:\n${constraintDesc}`,
|
|
74
79
|
)
|
|
75
80
|
}
|
|
76
81
|
|
|
77
|
-
const npmDeps
|
|
78
|
-
const assetDeps
|
|
82
|
+
const npmDeps = parseDependencies(meta.npmDependencies, assetName, 'npm')
|
|
83
|
+
const assetDeps = parseDependencies(meta.assetDependencies, assetName, 'asset')
|
|
84
|
+
const skillDeps = parseDependencies(meta.skillDependencies, assetName, 'skill')
|
|
79
85
|
|
|
80
86
|
resolved.set(assetName, {
|
|
81
87
|
name: assetName,
|
|
82
88
|
version: meta.latestVersion,
|
|
83
89
|
npmDependencies: npmDeps,
|
|
84
90
|
assetDependencies: assetDeps,
|
|
91
|
+
skillDependencies: skillDeps,
|
|
85
92
|
})
|
|
86
93
|
|
|
87
94
|
for (const [depName, depRange] of Object.entries(assetDeps)) {
|
|
@@ -106,15 +113,61 @@ export async function resolve(
|
|
|
106
113
|
}
|
|
107
114
|
}
|
|
108
115
|
|
|
109
|
-
// Merge npm dependencies across all resolved assets
|
|
116
|
+
// Merge npm and skill dependencies across all resolved assets
|
|
110
117
|
const mergedNpm = mergeNpmDependencies(Array.from(resolved.values()))
|
|
118
|
+
const mergedSkills = mergeSkillDependencies(Array.from(resolved.values()))
|
|
111
119
|
|
|
112
120
|
return {
|
|
113
121
|
assets: Array.from(resolved.values()),
|
|
114
122
|
npmDependencies: mergedNpm,
|
|
123
|
+
skillDependencies: mergedSkills,
|
|
115
124
|
}
|
|
116
125
|
}
|
|
117
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Merge skill dependencies from all resolved assets. Skills are not versioned
|
|
129
|
+
* by the resolver — each entry is a label mapped to a `skills add` source — so
|
|
130
|
+
* the merge is a simple union. The same label declared by two assets must point
|
|
131
|
+
* at the same source; otherwise installing one would silently shadow the other.
|
|
132
|
+
*/
|
|
133
|
+
function mergeSkillDependencies(assets: ResolvedAsset[]): Record<string, string> {
|
|
134
|
+
const merged: Record<string, string> = {}
|
|
135
|
+
const origin: Record<string, string> = {}
|
|
136
|
+
|
|
137
|
+
for (const asset of assets) {
|
|
138
|
+
const from = `${asset.name}@${asset.version}`
|
|
139
|
+
for (const [label, source] of Object.entries(asset.skillDependencies)) {
|
|
140
|
+
if (label in merged && merged[label] !== source) {
|
|
141
|
+
throw new ResolutionError(
|
|
142
|
+
`skill dependency conflict for "${label}":\n` +
|
|
143
|
+
` ${merged[label]} (from ${origin[label]})\n` +
|
|
144
|
+
` ${source} (from ${from})`,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
merged[label] = source
|
|
148
|
+
origin[label] = from
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return merged
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function parseDependencies(
|
|
156
|
+
value: string,
|
|
157
|
+
assetName: string,
|
|
158
|
+
kind: 'npm' | 'asset' | 'skill',
|
|
159
|
+
): Record<string, string> {
|
|
160
|
+
try {
|
|
161
|
+
const parsed = JSON.parse(value || '{}') as unknown
|
|
162
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
163
|
+
return parsed as Record<string, string>
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// fall through to a clear error below
|
|
167
|
+
}
|
|
168
|
+
throw new ResolutionError(`Asset "${assetName}" has malformed ${kind} dependency metadata.`)
|
|
169
|
+
}
|
|
170
|
+
|
|
118
171
|
function addConstraint(
|
|
119
172
|
constraints: Map<string, { range: string; from: string }[]>,
|
|
120
173
|
name: string,
|
|
@@ -151,7 +204,8 @@ async function fetchMeta(
|
|
|
151
204
|
/**
|
|
152
205
|
* Merge npm dependency ranges from all resolved assets.
|
|
153
206
|
* For each package, check that all declared ranges are compatible
|
|
154
|
-
* (using semver.intersects)
|
|
207
|
+
* (using semver.intersects), then return the single declared range that is a
|
|
208
|
+
* subset of every other — the true intersection.
|
|
155
209
|
*/
|
|
156
210
|
function mergeNpmDependencies(assets: ResolvedAsset[]): Record<string, string> {
|
|
157
211
|
// Collect all ranges per package
|
|
@@ -181,18 +235,23 @@ function mergeNpmDependencies(assets: ResolvedAsset[]): Record<string, string> {
|
|
|
181
235
|
}
|
|
182
236
|
}
|
|
183
237
|
|
|
184
|
-
// Use the
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
238
|
+
// Use the single declared range that is contained in every other declared
|
|
239
|
+
// range — the true intersection of all constraints. Picking by "highest
|
|
240
|
+
// minimum version" is unsound: a range with a higher minimum can also have
|
|
241
|
+
// a higher (or open) upper bound, silently dropping another dependent's
|
|
242
|
+
// upper bound. If no single range represents the intersection, it cannot be
|
|
243
|
+
// expressed as one package.json range, so fail loudly.
|
|
244
|
+
const narrowest = ranges.find((candidate) =>
|
|
245
|
+
ranges.every((other) => semver.subset(candidate.range, other.range)),
|
|
246
|
+
)
|
|
247
|
+
if (!narrowest) {
|
|
248
|
+
throw new ResolutionError(
|
|
249
|
+
`npm dependency ranges for "${pkg}" overlap but cannot be combined into a single range:\n` +
|
|
250
|
+
ranges.map((r) => ` ${r.range} (from ${r.from})`).join('\n'),
|
|
251
|
+
)
|
|
193
252
|
}
|
|
194
253
|
|
|
195
|
-
merged[pkg] = narrowest
|
|
254
|
+
merged[pkg] = narrowest.range
|
|
196
255
|
}
|
|
197
256
|
|
|
198
257
|
return merged
|
package/src/schemas.ts
CHANGED
|
@@ -39,6 +39,11 @@ export const npmDependenciesSchema = z.record(z.string(), z.string()).default({}
|
|
|
39
39
|
|
|
40
40
|
export const assetDependenciesSchema = z.record(z.string(), z.string()).default({})
|
|
41
41
|
|
|
42
|
+
// Maps a skill label to a `skills add` source: a remote ref (owner/repo,
|
|
43
|
+
// git/HTTP URL, tree/<branch>/<subpath>) or a local path to a skill directory
|
|
44
|
+
// shipped inside the asset.
|
|
45
|
+
export const skillDependenciesSchema = z.record(z.string(), z.string()).default({})
|
|
46
|
+
|
|
42
47
|
export const updateProfileSchema = z.object({
|
|
43
48
|
name: z.string().min(1).max(100).optional(),
|
|
44
49
|
image: z.string().url().optional(),
|
|
@@ -66,6 +71,7 @@ export const uploadZipSchema = z.object({
|
|
|
66
71
|
description: z.string().max(1000).optional(),
|
|
67
72
|
npmDependencies: npmDependenciesSchema,
|
|
68
73
|
assetDependencies: assetDependenciesSchema,
|
|
74
|
+
skillDependencies: skillDependenciesSchema,
|
|
69
75
|
tags: z.array(z.string()).default([]),
|
|
70
76
|
})
|
|
71
77
|
|
package/src/skill.ts
CHANGED
|
@@ -5,38 +5,45 @@ 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>\`. Declare dependencies with repeatable flags: \`--npm name@range\`, \`--asset name@range\`, \`--skill label=source\` (source is a GitHub/git ref 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=vercel-labs/agent-skills\`.
|
|
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)
|
|
42
|
+
- web-design-guidelines ← vercel-labs/agent-skills/tree/main/skills/web-design-guidelines (skill)
|
|
38
43
|
Saved preview for wooden-chair@1.0.0: /tmp/wooden-chair.png
|
|
39
44
|
\`\`\`
|
|
40
45
|
|
|
46
|
+
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.
|
|
47
|
+
|
|
41
48
|
If search returns no results, try one broader noun phrase. If a command returns \`Error: Not logged in...\`, ask before running \`market login\`.
|
|
42
49
|
`
|
|
@@ -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
|
|
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
|
|
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 () =>
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
})
|
|
@@ -30,9 +30,11 @@ test('install writes zip files into the package root', async () => {
|
|
|
30
30
|
version: '1.0.0',
|
|
31
31
|
npmDependencies: {},
|
|
32
32
|
assetDependencies: {},
|
|
33
|
+
skillDependencies: {},
|
|
33
34
|
},
|
|
34
35
|
],
|
|
35
36
|
npmDependencies: {},
|
|
37
|
+
skillDependencies: {},
|
|
36
38
|
},
|
|
37
39
|
{ cwd },
|
|
38
40
|
)
|
|
@@ -46,6 +48,7 @@ test('install writes zip files into the package root', async () => {
|
|
|
46
48
|
},
|
|
47
49
|
],
|
|
48
50
|
npmDependencies: {},
|
|
51
|
+
skillDependencies: {},
|
|
49
52
|
})
|
|
50
53
|
assert.equal(
|
|
51
54
|
await fs.readFile(path.join(appRoot, 'public', 'humanoid-animation', 'idle-loop.glb'), 'utf-8'),
|
|
@@ -75,9 +78,11 @@ test('install rejects zip paths that escape through parent segments', async () =
|
|
|
75
78
|
version: '1.0.0',
|
|
76
79
|
npmDependencies: {},
|
|
77
80
|
assetDependencies: {},
|
|
81
|
+
skillDependencies: {},
|
|
78
82
|
},
|
|
79
83
|
],
|
|
80
84
|
npmDependencies: {},
|
|
85
|
+
skillDependencies: {},
|
|
81
86
|
},
|
|
82
87
|
{ cwd: tempDir },
|
|
83
88
|
),
|
|
@@ -85,7 +90,60 @@ test('install rejects zip paths that escape through parent segments', async () =
|
|
|
85
90
|
)
|
|
86
91
|
})
|
|
87
92
|
|
|
88
|
-
test('
|
|
93
|
+
test('install runs the skills runner per dependency, resolving local paths against the root', async () => {
|
|
94
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-skills-'))
|
|
95
|
+
const appRoot = path.join(tempDir, 'app')
|
|
96
|
+
const cwd = path.join(appRoot, 'src')
|
|
97
|
+
await fs.mkdir(cwd, { recursive: true })
|
|
98
|
+
await fs.writeFile(path.join(appRoot, 'package.json'), '{}\n')
|
|
99
|
+
|
|
100
|
+
const calls: Array<{ source: string; cwd: string }> = []
|
|
101
|
+
|
|
102
|
+
const result = await install(
|
|
103
|
+
clientWithZip({
|
|
104
|
+
'public/skills/local-skill/SKILL.md': textEncoder.encode('---\nname: local-skill\n---\n'),
|
|
105
|
+
}),
|
|
106
|
+
{
|
|
107
|
+
assets: [
|
|
108
|
+
{
|
|
109
|
+
name: 'with-skills',
|
|
110
|
+
version: '1.0.0',
|
|
111
|
+
npmDependencies: {},
|
|
112
|
+
assetDependencies: {},
|
|
113
|
+
skillDependencies: {},
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
npmDependencies: {},
|
|
117
|
+
skillDependencies: {
|
|
118
|
+
'web-design-guidelines': 'vercel-labs/agent-skills/tree/main/skills/web-design-guidelines',
|
|
119
|
+
'local-skill': './public/skills/local-skill',
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
cwd,
|
|
124
|
+
runSkillAdd: async (source, runnerCwd) => {
|
|
125
|
+
calls.push({ source, cwd: runnerCwd })
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
// Every runner call uses the install root (the dir holding package.json).
|
|
131
|
+
assert.deepEqual(new Set(calls.map((c) => c.cwd)), new Set([appRoot]))
|
|
132
|
+
|
|
133
|
+
const bySource = new Map(calls.map((c) => [c.source, c]))
|
|
134
|
+
// Remote refs are passed through untouched.
|
|
135
|
+
assert.ok(bySource.has('vercel-labs/agent-skills/tree/main/skills/web-design-guidelines'))
|
|
136
|
+
// Local paths are resolved against the install root, not the cwd.
|
|
137
|
+
assert.ok(bySource.has(path.join(appRoot, 'public', 'skills', 'local-skill')))
|
|
138
|
+
assert.equal(calls.length, 2)
|
|
139
|
+
|
|
140
|
+
assert.deepEqual(result.skillDependencies, {
|
|
141
|
+
'web-design-guidelines': 'vercel-labs/agent-skills/tree/main/skills/web-design-guidelines',
|
|
142
|
+
'local-skill': './public/skills/local-skill',
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('findInstallRoot uses the nearest package.json, not the monorepo root', async () => {
|
|
89
147
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-root-'))
|
|
90
148
|
const repoRoot = path.join(tempDir, 'repo')
|
|
91
149
|
const appRoot = path.join(repoRoot, 'packages', 'app')
|
|
@@ -95,7 +153,7 @@ test('findInstallRoot ignores public directories and uses the highest package.js
|
|
|
95
153
|
await fs.writeFile(path.join(repoRoot, 'package.json'), '{}\n')
|
|
96
154
|
await fs.writeFile(path.join(appRoot, 'package.json'), '{}\n')
|
|
97
155
|
|
|
98
|
-
assert.equal(await findInstallRoot(cwd),
|
|
156
|
+
assert.equal(await findInstallRoot(cwd), appRoot)
|
|
99
157
|
})
|
|
100
158
|
|
|
101
159
|
test('findInstallRoot falls back to cwd when no package.json exists', async () => {
|
package/tests/output.test.ts
CHANGED
|
@@ -18,6 +18,7 @@ test('installResult prints asset and npm lists with a file tree', () => {
|
|
|
18
18
|
},
|
|
19
19
|
],
|
|
20
20
|
npmDependencies: { '@react-three/drei': 'latest' },
|
|
21
|
+
skillDependencies: {},
|
|
21
22
|
}),
|
|
22
23
|
`Installed:
|
|
23
24
|
- wooden-chair@1.2.0 (asset)
|
|
@@ -35,3 +36,20 @@ test('installResult prints asset and npm lists with a file tree', () => {
|
|
|
35
36
|
- @react-three/drei@latest (npm)`,
|
|
36
37
|
)
|
|
37
38
|
})
|
|
39
|
+
|
|
40
|
+
test('installResult lists installed skills with their source', () => {
|
|
41
|
+
assert.equal(
|
|
42
|
+
installResult({
|
|
43
|
+
assets: [{ name: 'wooden-chair', version: '1.2.0', files: [] }],
|
|
44
|
+
npmDependencies: {},
|
|
45
|
+
skillDependencies: {
|
|
46
|
+
'web-design-guidelines': 'vercel-labs/agent-skills/tree/main/skills/web-design-guidelines',
|
|
47
|
+
'local-skill': '/abs/public/skills/local-skill',
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
`Installed:
|
|
51
|
+
- wooden-chair@1.2.0 (asset)
|
|
52
|
+
- local-skill ← /abs/public/skills/local-skill (skill)
|
|
53
|
+
- web-design-guidelines ← vercel-labs/agent-skills/tree/main/skills/web-design-guidelines (skill)`,
|
|
54
|
+
)
|
|
55
|
+
})
|