@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.
- package/README.md +6 -3
- package/dist/asset-implementation.d.ts +16 -7
- package/dist/asset-implementation.d.ts.map +1 -1
- package/dist/asset-implementation.js +20 -1
- package/dist/asset-implementation.js.map +1 -1
- package/dist/cli.js +14 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/install.d.ts +3 -4
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +57 -11
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/list.d.ts +17 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +26 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/upload.d.ts.map +1 -1
- package/dist/commands/upload.js +64 -2
- package/dist/commands/upload.js.map +1 -1
- package/dist/contract.d.ts +9 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +14 -0
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +133 -23
- package/dist/install.js.map +1 -1
- package/dist/market-lock.d.ts +17 -0
- package/dist/market-lock.d.ts.map +1 -0
- package/dist/market-lock.js +44 -0
- package/dist/market-lock.js.map +1 -0
- package/dist/output.d.ts +4 -2
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +28 -7
- package/dist/output.js.map +1 -1
- package/dist/package-json.d.ts +11 -0
- package/dist/package-json.d.ts.map +1 -0
- package/dist/package-json.js +23 -0
- package/dist/package-json.js.map +1 -0
- package/dist/skill.d.ts +1 -1
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js +20 -9
- package/dist/skill.js.map +1 -1
- package/package.json +2 -2
- package/src/asset-implementation.ts +40 -6
- package/src/cli.ts +24 -6
- package/src/commands/install.ts +76 -16
- package/src/commands/list.ts +47 -0
- package/src/commands/upload.ts +84 -2
- package/src/contract.ts +10 -1
- package/src/index.ts +15 -0
- package/src/install.ts +197 -35
- package/src/market-lock.ts +65 -0
- package/src/output.ts +36 -8
- package/src/package-json.ts +37 -0
- package/src/skill.ts +20 -9
- package/tests/install-command.test.ts +23 -1
- package/tests/install-layout.test.ts +117 -0
- package/tests/list-command.test.ts +76 -0
- package/tests/output.test.ts +69 -3
package/src/install.ts
CHANGED
|
@@ -16,18 +16,20 @@ import * as path from 'path'
|
|
|
16
16
|
import { unzipSync } from 'fflate'
|
|
17
17
|
import { detectPackageManager, installDependencies } from 'nypm'
|
|
18
18
|
import type { MarketClient } from './client.js'
|
|
19
|
+
import type { AssetInstallMetadata } from './contract.js'
|
|
20
|
+
import { readMarketLock, sha256, writeMarketLock } from './market-lock.js'
|
|
21
|
+
import { parsePackageJson, type PackageJson } from './package-json.js'
|
|
19
22
|
import type { ResolveResult } from './resolve.js'
|
|
20
23
|
|
|
21
|
-
interface PackageJson {
|
|
22
|
-
name?: string
|
|
23
|
-
private?: boolean
|
|
24
|
-
dependencies?: Record<string, string>
|
|
25
|
-
[key: string]: unknown
|
|
26
|
-
}
|
|
27
|
-
|
|
28
24
|
export interface InstallOptions {
|
|
29
25
|
/** Directory to start project root discovery from (default: cwd) */
|
|
30
26
|
cwd?: string
|
|
27
|
+
/** Overwrite existing files that differ from the asset zip. */
|
|
28
|
+
force?: boolean
|
|
29
|
+
/** Top-level assets requested by the user, used for package.json assetDependencies. */
|
|
30
|
+
rootRequests?: InstallRootRequest[]
|
|
31
|
+
/** Asset-type install policy from the Market API. */
|
|
32
|
+
installMetadata?: Record<string, AssetInstallMetadata>
|
|
31
33
|
/** Log progress */
|
|
32
34
|
onProgress?: (message: string) => void
|
|
33
35
|
/**
|
|
@@ -38,6 +40,13 @@ export interface InstallOptions {
|
|
|
38
40
|
runSkillAdd?: (source: string, cwd: string) => Promise<void>
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
export interface InstallRootRequest {
|
|
44
|
+
name: string
|
|
45
|
+
range: string
|
|
46
|
+
saveRange: string
|
|
47
|
+
save: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
41
50
|
export interface InstalledAsset {
|
|
42
51
|
name: string
|
|
43
52
|
type: string
|
|
@@ -51,6 +60,17 @@ export interface InstallResult {
|
|
|
51
60
|
npmDependencies: Record<string, string>
|
|
52
61
|
/** Installed skills, keyed by label, with the source passed to `skills add`. */
|
|
53
62
|
skillDependencies: Record<string, string>
|
|
63
|
+
warnings: string[]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface DownloadedAsset extends InstalledAsset {
|
|
67
|
+
fileHashes: Record<string, { sha256: string }>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface DownloadResult {
|
|
71
|
+
assets: DownloadedAsset[]
|
|
72
|
+
wrotePackageJson: boolean
|
|
73
|
+
warnings: string[]
|
|
54
74
|
}
|
|
55
75
|
|
|
56
76
|
export async function install(
|
|
@@ -60,11 +80,21 @@ export async function install(
|
|
|
60
80
|
): Promise<InstallResult> {
|
|
61
81
|
const log = opts.onProgress ?? (() => {})
|
|
62
82
|
const installRoot = await findInstallRoot(opts.cwd ?? process.cwd())
|
|
83
|
+
const metadata = opts.installMetadata ?? {}
|
|
63
84
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
85
|
+
const download = await downloadAssets(client, resolution, installRoot, {
|
|
86
|
+
force: opts.force ?? false,
|
|
87
|
+
log,
|
|
88
|
+
})
|
|
89
|
+
await writeInstalledAssetLock(installRoot, download.assets)
|
|
90
|
+
const packageJsonUpdate = await updatePackageJson(resolution, installRoot, {
|
|
91
|
+
rootRequests: opts.rootRequests ?? [],
|
|
92
|
+
installMetadata: metadata,
|
|
93
|
+
packageManagerNeeded: download.wrotePackageJson,
|
|
94
|
+
})
|
|
95
|
+
if (packageJsonUpdate.packageManagerNeeded) {
|
|
96
|
+
await runPackageManagerInstall(installRoot, log)
|
|
97
|
+
}
|
|
68
98
|
|
|
69
99
|
// Skills are installed after asset files land on disk: a skill source may be
|
|
70
100
|
// a local path pointing at a skill directory shipped inside an installed
|
|
@@ -77,9 +107,16 @@ export async function install(
|
|
|
77
107
|
)
|
|
78
108
|
|
|
79
109
|
return {
|
|
80
|
-
assets
|
|
110
|
+
assets: download.assets.map((asset) => ({
|
|
111
|
+
name: asset.name,
|
|
112
|
+
type: asset.type,
|
|
113
|
+
version: asset.version,
|
|
114
|
+
description: asset.description,
|
|
115
|
+
files: asset.files,
|
|
116
|
+
})),
|
|
81
117
|
npmDependencies: resolution.npmDependencies,
|
|
82
118
|
skillDependencies,
|
|
119
|
+
warnings: download.warnings,
|
|
83
120
|
}
|
|
84
121
|
}
|
|
85
122
|
|
|
@@ -100,16 +137,22 @@ async function downloadAssets(
|
|
|
100
137
|
client: MarketClient,
|
|
101
138
|
resolution: ResolveResult,
|
|
102
139
|
projectRoot: string,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
140
|
+
opts: {
|
|
141
|
+
force: boolean
|
|
142
|
+
log: (msg: string) => void
|
|
143
|
+
},
|
|
144
|
+
): Promise<DownloadResult> {
|
|
145
|
+
const installedAssets: DownloadedAsset[] = []
|
|
146
|
+
const warnings: string[] = []
|
|
147
|
+
let wrotePackageJson = false
|
|
106
148
|
|
|
107
149
|
for (const asset of resolution.assets) {
|
|
108
|
-
log(`Downloading ${asset.name}@${asset.version}...`)
|
|
150
|
+
opts.log(`Downloading ${asset.name}@${asset.version}...`)
|
|
109
151
|
|
|
110
152
|
const zip = await client.asset.downloadZip({ name: asset.name, version: asset.version })
|
|
111
153
|
const files = unzipSync(new Uint8Array(await zip.arrayBuffer()))
|
|
112
154
|
const installedFiles: string[] = []
|
|
155
|
+
const fileHashes: Record<string, { sha256: string }> = {}
|
|
113
156
|
|
|
114
157
|
for (const [relativePath, content] of Object.entries(files) as [string, Uint8Array][]) {
|
|
115
158
|
const zipPath = relativePath.replace(/\\/g, '/')
|
|
@@ -124,16 +167,31 @@ async function downloadAssets(
|
|
|
124
167
|
const normalizedPath = path.posix.normalize(zipPath)
|
|
125
168
|
if (
|
|
126
169
|
normalizedPath === '.' ||
|
|
127
|
-
normalizedPath === 'README.md' ||
|
|
170
|
+
(normalizedPath === 'README.md' && asset.type !== 'template') ||
|
|
128
171
|
normalizedPath.endsWith('/')
|
|
129
172
|
) {
|
|
130
173
|
continue
|
|
131
174
|
}
|
|
132
175
|
|
|
133
176
|
const filePath = path.join(projectRoot, normalizedPath)
|
|
134
|
-
await
|
|
135
|
-
|
|
177
|
+
const existing = await maybeReadFile(filePath)
|
|
178
|
+
if (existing && !bytesEqual(existing, content) && !opts.force) {
|
|
179
|
+
warnings.push(
|
|
180
|
+
`Skipped ${normalizedPath} from ${asset.name}@${asset.version}; file already exists. Re-run with --force to overwrite.`,
|
|
181
|
+
)
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!existing || !bytesEqual(existing, content)) {
|
|
186
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
187
|
+
await fs.writeFile(filePath, content)
|
|
188
|
+
if (normalizedPath === 'package.json') {
|
|
189
|
+
wrotePackageJson = true
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
136
193
|
installedFiles.push(normalizedPath)
|
|
194
|
+
fileHashes[normalizedPath] = { sha256: sha256(content) }
|
|
137
195
|
}
|
|
138
196
|
|
|
139
197
|
installedAssets.push({
|
|
@@ -142,12 +200,13 @@ async function downloadAssets(
|
|
|
142
200
|
version: asset.version,
|
|
143
201
|
description: asset.description,
|
|
144
202
|
files: installedFiles,
|
|
203
|
+
fileHashes,
|
|
145
204
|
})
|
|
146
205
|
|
|
147
|
-
log(`Downloaded ${installedFiles.length} files.`)
|
|
206
|
+
opts.log(`Downloaded ${installedFiles.length} files.`)
|
|
148
207
|
}
|
|
149
208
|
|
|
150
|
-
return installedAssets
|
|
209
|
+
return { assets: installedAssets, wrotePackageJson, warnings }
|
|
151
210
|
}
|
|
152
211
|
|
|
153
212
|
async function isFile(file: string): Promise<boolean> {
|
|
@@ -158,26 +217,60 @@ async function isFile(file: string): Promise<boolean> {
|
|
|
158
217
|
}
|
|
159
218
|
}
|
|
160
219
|
|
|
161
|
-
async function
|
|
220
|
+
async function maybeReadFile(file: string): Promise<Uint8Array | null> {
|
|
221
|
+
try {
|
|
222
|
+
return await fs.readFile(file)
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (isMissingFile(error)) return null
|
|
225
|
+
throw error
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isMissingFile(error: unknown): boolean {
|
|
230
|
+
return error instanceof Error && 'code' in error && error.code === 'ENOENT'
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function updatePackageJson(
|
|
162
234
|
resolution: ResolveResult,
|
|
163
235
|
projectRoot: string,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
236
|
+
opts: {
|
|
237
|
+
rootRequests: InstallRootRequest[]
|
|
238
|
+
installMetadata: Record<string, AssetInstallMetadata>
|
|
239
|
+
packageManagerNeeded: boolean
|
|
240
|
+
},
|
|
241
|
+
): Promise<{ packageManagerNeeded: boolean }> {
|
|
169
242
|
const pkgPath = path.join(projectRoot, 'package.json')
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
243
|
+
const pkg = (await isFile(pkgPath))
|
|
244
|
+
? parsePackageJson(await fs.readFile(pkgPath, 'utf-8'), pkgPath)
|
|
245
|
+
: defaultPackageJson()
|
|
246
|
+
|
|
247
|
+
let changed = false
|
|
248
|
+
let packageManagerNeeded = opts.packageManagerNeeded
|
|
175
249
|
|
|
176
|
-
|
|
177
|
-
|
|
250
|
+
const npmChanged = mergeRecord(pkg, 'dependencies', resolution.npmDependencies)
|
|
251
|
+
if (npmChanged) {
|
|
252
|
+
changed = true
|
|
253
|
+
packageManagerNeeded = true
|
|
254
|
+
}
|
|
178
255
|
|
|
179
|
-
|
|
256
|
+
const assetDependencies = assetDependenciesToSave(
|
|
257
|
+
resolution,
|
|
258
|
+
opts.rootRequests,
|
|
259
|
+
opts.installMetadata,
|
|
260
|
+
)
|
|
261
|
+
if (mergeRecord(pkg, 'assetDependencies', assetDependencies)) {
|
|
262
|
+
changed = true
|
|
263
|
+
}
|
|
180
264
|
|
|
265
|
+
if (changed) {
|
|
266
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return { packageManagerNeeded }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function runPackageManagerInstall(projectRoot: string, log: (msg: string) => void) {
|
|
273
|
+
log('Installing npm dependencies...')
|
|
181
274
|
const pm = await detectPackageManager(projectRoot)
|
|
182
275
|
const pmName = pm?.name ?? 'npm'
|
|
183
276
|
|
|
@@ -186,6 +279,75 @@ async function installNpmDeps(
|
|
|
186
279
|
log('npm dependencies installed.')
|
|
187
280
|
}
|
|
188
281
|
|
|
282
|
+
async function writeInstalledAssetLock(
|
|
283
|
+
projectRoot: string,
|
|
284
|
+
assets: DownloadedAsset[],
|
|
285
|
+
): Promise<void> {
|
|
286
|
+
const lock = await readMarketLock(projectRoot)
|
|
287
|
+
for (const asset of assets) {
|
|
288
|
+
lock.assets[asset.name] = {
|
|
289
|
+
version: asset.version,
|
|
290
|
+
type: asset.type,
|
|
291
|
+
files: asset.fileHashes,
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
await writeMarketLock(projectRoot, lock)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function defaultPackageJson(): PackageJson {
|
|
298
|
+
return { name: 'my-project', private: true, dependencies: {} }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function mergeRecord(
|
|
302
|
+
pkg: PackageJson,
|
|
303
|
+
field: 'dependencies' | 'assetDependencies',
|
|
304
|
+
values: Record<string, string>,
|
|
305
|
+
): boolean {
|
|
306
|
+
const entries = Object.entries(values)
|
|
307
|
+
if (entries.length === 0) return false
|
|
308
|
+
|
|
309
|
+
const current = pkg[field] ?? {}
|
|
310
|
+
let changed = false
|
|
311
|
+
|
|
312
|
+
for (const [name, range] of entries) {
|
|
313
|
+
if (current[name] === range) continue
|
|
314
|
+
current[name] = range
|
|
315
|
+
changed = true
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
pkg[field] = current
|
|
319
|
+
return changed
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function assetDependenciesToSave(
|
|
323
|
+
resolution: ResolveResult,
|
|
324
|
+
rootRequests: InstallRootRequest[],
|
|
325
|
+
metadata: Record<string, AssetInstallMetadata>,
|
|
326
|
+
): Record<string, string> {
|
|
327
|
+
const resolvedByName = new Map(resolution.assets.map((asset) => [asset.name, asset]))
|
|
328
|
+
const out: Record<string, string> = {}
|
|
329
|
+
|
|
330
|
+
for (const request of rootRequests) {
|
|
331
|
+
if (!request.save) continue
|
|
332
|
+
|
|
333
|
+
const asset = resolvedByName.get(request.name)
|
|
334
|
+
if (!asset) continue
|
|
335
|
+
|
|
336
|
+
if ((metadata[asset.type]?.saveOnInstall ?? true) === false) continue
|
|
337
|
+
out[asset.name] = request.saveRange
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return out
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
344
|
+
if (a.length !== b.length) return false
|
|
345
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
346
|
+
if (a[i] !== b[i]) return false
|
|
347
|
+
}
|
|
348
|
+
return true
|
|
349
|
+
}
|
|
350
|
+
|
|
189
351
|
async function installSkills(
|
|
190
352
|
resolution: ResolveResult,
|
|
191
353
|
projectRoot: string,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHash } from 'crypto'
|
|
2
|
+
import * as fs from 'fs/promises'
|
|
3
|
+
import * as path from 'path'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
|
|
6
|
+
export const MARKET_LOCK_PATH = '.drawcall/market-lock.json'
|
|
7
|
+
|
|
8
|
+
export interface MarketLockAsset {
|
|
9
|
+
version: string
|
|
10
|
+
type: string
|
|
11
|
+
files: Record<string, { sha256: string }>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface MarketLock {
|
|
15
|
+
version: 1
|
|
16
|
+
assets: Record<string, MarketLockAsset>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const marketLockSchema = z.object({
|
|
20
|
+
version: z.literal(1),
|
|
21
|
+
assets: z.record(
|
|
22
|
+
z.string(),
|
|
23
|
+
z.object({
|
|
24
|
+
version: z.string(),
|
|
25
|
+
type: z.string(),
|
|
26
|
+
files: z.record(
|
|
27
|
+
z.string(),
|
|
28
|
+
z.object({
|
|
29
|
+
sha256: z.string(),
|
|
30
|
+
}),
|
|
31
|
+
),
|
|
32
|
+
}),
|
|
33
|
+
),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export async function readMarketLock(projectRoot: string): Promise<MarketLock> {
|
|
37
|
+
const lockPath = path.join(projectRoot, MARKET_LOCK_PATH)
|
|
38
|
+
try {
|
|
39
|
+
return marketLockSchema.parse(JSON.parse(await fs.readFile(lockPath, 'utf-8')))
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (isMissingFile(error)) return emptyMarketLock()
|
|
42
|
+
throw error
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function writeMarketLock(projectRoot: string, lock: MarketLock): Promise<void> {
|
|
47
|
+
const lockPath = path.join(projectRoot, MARKET_LOCK_PATH)
|
|
48
|
+
await fs.mkdir(path.dirname(lockPath), { recursive: true })
|
|
49
|
+
await fs.writeFile(lockPath, JSON.stringify(lock, null, 2) + '\n')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function emptyMarketLock(): MarketLock {
|
|
53
|
+
return {
|
|
54
|
+
version: 1,
|
|
55
|
+
assets: {},
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function sha256(bytes: Uint8Array): string {
|
|
60
|
+
return createHash('sha256').update(bytes).digest('hex')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isMissingFile(error: unknown): boolean {
|
|
64
|
+
return error instanceof Error && 'code' in error && error.code === 'ENOENT'
|
|
65
|
+
}
|
package/src/output.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AssetSearchResult } from './contract.js'
|
|
1
|
+
import type { AssetInstallMetadata, AssetSearchResult } from './contract.js'
|
|
2
|
+
import type { ListInstalledAssetsResult } from './commands/list.js'
|
|
2
3
|
import type { InstallResult } from './install.js'
|
|
3
4
|
|
|
4
5
|
export function assetVersionRef(name: string, version?: string): string {
|
|
@@ -33,22 +34,19 @@ export function assetSearchResultLine(item: AssetSearchResult): string {
|
|
|
33
34
|
|
|
34
35
|
export function installResult(
|
|
35
36
|
result: InstallResult,
|
|
36
|
-
|
|
37
|
+
installMetadata: Record<string, AssetInstallMetadata> = {},
|
|
37
38
|
): string {
|
|
38
39
|
const npmDependencies = Object.entries(result.npmDependencies)
|
|
39
40
|
|
|
40
41
|
const lines = ['Installed:']
|
|
41
42
|
|
|
42
43
|
for (const asset of result.assets) {
|
|
43
|
-
|
|
44
|
-
lines.push(`- ${assetVersionRef(asset.name, asset.version)} (${asset.type})`)
|
|
44
|
+
lines.push(assetLine(asset))
|
|
45
45
|
if (asset.description) {
|
|
46
46
|
lines.push(...block(' description: ', asset.description))
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
const message = postInstallMessages[asset.type]
|
|
48
|
+
lines.push(...assetFileLines(asset.files))
|
|
49
|
+
const message = installMetadata[asset.type]?.installMessage
|
|
52
50
|
if (message) {
|
|
53
51
|
lines.push(...block(' note: ', message))
|
|
54
52
|
}
|
|
@@ -71,9 +69,39 @@ export function installResult(
|
|
|
71
69
|
)
|
|
72
70
|
}
|
|
73
71
|
|
|
72
|
+
if (result.warnings.length > 0) {
|
|
73
|
+
lines.push('Warnings:', ...result.warnings.map((warning) => `- ${warning}`))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return lines.join('\n')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function listResult(result: ListInstalledAssetsResult): string {
|
|
80
|
+
if (result.assets.length === 0) {
|
|
81
|
+
return 'No installed assets found.'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const lines = [`Installed assets: ${result.assets.length}`]
|
|
85
|
+
for (const asset of result.assets) {
|
|
86
|
+
lines.push(assetLine(asset), ...assetFileLines(asset.files))
|
|
87
|
+
}
|
|
88
|
+
|
|
74
89
|
return lines.join('\n')
|
|
75
90
|
}
|
|
76
91
|
|
|
92
|
+
function assetLine(asset: { name: string; version: string; type: string }): string {
|
|
93
|
+
return `- ${assetVersionRef(asset.name, asset.version)} (${asset.type})`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function assetFileLines(paths: string[]): string[] {
|
|
97
|
+
const lines: string[] = []
|
|
98
|
+
const files = unique(paths)
|
|
99
|
+
if (files.length > 0) {
|
|
100
|
+
lines.push(' files:', ...indent(fileTree(files), ' '))
|
|
101
|
+
}
|
|
102
|
+
return lines
|
|
103
|
+
}
|
|
104
|
+
|
|
77
105
|
function block(label: string, message: string, width = 68): string[] {
|
|
78
106
|
const hang = ' '.repeat(label.length)
|
|
79
107
|
const words = normalizeInline(message).split(' ').filter(Boolean)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export interface PackageJson {
|
|
4
|
+
name?: string
|
|
5
|
+
private?: boolean
|
|
6
|
+
dependencies?: Record<string, string>
|
|
7
|
+
assetDependencies?: Record<string, string>
|
|
8
|
+
[key: string]: unknown
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const stringRecordSchema = z.record(z.string(), z.string())
|
|
12
|
+
|
|
13
|
+
export function packageJsonAssetDependenciesFromFiles(
|
|
14
|
+
files: Record<string, Uint8Array>,
|
|
15
|
+
): Record<string, string> {
|
|
16
|
+
const bytes = files['package.json']
|
|
17
|
+
if (!bytes) return {}
|
|
18
|
+
|
|
19
|
+
return packageJsonAssetDependencies(new TextDecoder().decode(bytes), 'package.json')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function packageJsonAssetDependencies(json: string, source: string): Record<string, string> {
|
|
23
|
+
const parsed = parsePackageJson(json, source)
|
|
24
|
+
const dependencies = parsed.assetDependencies
|
|
25
|
+
if (dependencies === undefined) return {}
|
|
26
|
+
|
|
27
|
+
return stringRecordSchema.parse(dependencies)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function parsePackageJson(json: string, source: string): PackageJson {
|
|
31
|
+
const parsed = JSON.parse(json) as unknown
|
|
32
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
33
|
+
throw new Error(`${source} must contain a JSON object`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return parsed as PackageJson
|
|
37
|
+
}
|
package/src/skill.ts
CHANGED
|
@@ -12,20 +12,22 @@ Use the \`market\` CLI. Keep commands short and read the summary lines.
|
|
|
12
12
|
\`\`\`sh
|
|
13
13
|
market search "wooden chair" --type model --limit 3
|
|
14
14
|
market install wooden-chair --cwd "$PWD"
|
|
15
|
+
market list --cwd "$PWD"
|
|
15
16
|
market preview wooden-chair --out /tmp/wooden-chair.png
|
|
16
17
|
\`\`\`
|
|
17
18
|
|
|
18
19
|
## Workflow
|
|
19
20
|
|
|
20
|
-
1.
|
|
21
|
-
2.
|
|
22
|
-
3.
|
|
23
|
-
4. \`
|
|
24
|
-
5.
|
|
25
|
-
6.
|
|
26
|
-
7.
|
|
27
|
-
8.
|
|
28
|
-
9. Installed \`
|
|
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.
|
|
29
31
|
|
|
30
32
|
## Humanoid animations
|
|
31
33
|
|
|
@@ -48,10 +50,19 @@ Installed:
|
|
|
48
50
|
└─ wooden-chair.glb
|
|
49
51
|
- three@^0.178.0 (npm)
|
|
50
52
|
- web-design ← https://github.com/vercel-labs/agent-skills/tree/main/skills/web-design-guidelines (skill)
|
|
53
|
+
Installed assets: 1
|
|
54
|
+
- wooden-chair@1.0.0 (model)
|
|
55
|
+
files:
|
|
56
|
+
public/model
|
|
57
|
+
└─ wooden-chair.glb
|
|
51
58
|
Saved preview for wooden-chair@1.0.0: /tmp/wooden-chair.png
|
|
52
59
|
\`\`\`
|
|
53
60
|
|
|
54
61
|
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.
|
|
55
62
|
|
|
63
|
+
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\`.
|
|
64
|
+
|
|
65
|
+
\`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.
|
|
66
|
+
|
|
56
67
|
If search returns no results, try one broader noun phrase. If a command returns \`Error: Not logged in...\`, ask before running \`market login\`.
|
|
57
68
|
`
|
|
@@ -31,7 +31,29 @@ test('resolveArg keeps explicit versioned refs exact when the asset exists', asy
|
|
|
31
31
|
|
|
32
32
|
const request = await resolveArg(client, 'fresh-sky@1.2.3', false)
|
|
33
33
|
|
|
34
|
-
assert.deepEqual(request, {
|
|
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
|
+
})
|
|
35
57
|
})
|
|
36
58
|
|
|
37
59
|
test('resolveArg errors when the asset does not exist instead of deferring', async () => {
|