@drawcall/market 0.1.98 → 0.2.0
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 +7 -4
- package/dist/asset-implementation.d.ts +3 -3
- package/dist/asset-implementation.d.ts.map +1 -1
- package/dist/cli.js +15 -3
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +14 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +18 -0
- package/dist/client.js.map +1 -1
- package/dist/commands/install.js +10 -7
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/list.d.ts +5 -5
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +22 -9
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/pack.d.ts.map +1 -1
- package/dist/commands/pack.js +2 -2
- package/dist/commands/pack.js.map +1 -1
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +14 -2
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/sync.d.ts +6 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +22 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/upload.d.ts.map +1 -1
- package/dist/commands/upload.js +20 -10
- package/dist/commands/upload.js.map +1 -1
- package/dist/contract.d.ts +4 -6
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -2
- package/dist/contract.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +69 -27
- package/dist/install.js.map +1 -1
- package/dist/output.d.ts +3 -4
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +40 -7
- package/dist/output.js.map +1 -1
- package/dist/pack.d.ts +36 -6
- package/dist/pack.d.ts.map +1 -1
- package/dist/pack.js +230 -57
- package/dist/pack.js.map +1 -1
- package/dist/package-json.d.ts +4 -3
- package/dist/package-json.d.ts.map +1 -1
- package/dist/package-json.js +3 -1
- package/dist/package-json.js.map +1 -1
- package/dist/resolve.d.ts +8 -1
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +35 -3
- package/dist/resolve.js.map +1 -1
- package/dist/schemas.d.ts +19 -7
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +22 -7
- 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 +4 -2
- package/dist/skill.js.map +1 -1
- package/dist/v1/client.d.ts +15 -0
- package/dist/v1/client.d.ts.map +1 -0
- package/dist/v1/client.js +18 -0
- package/dist/v1/client.js.map +1 -0
- package/dist/v1/contract.d.ts +206 -0
- package/dist/v1/contract.d.ts.map +1 -0
- package/dist/v1/contract.js +77 -0
- package/dist/v1/contract.js.map +1 -0
- package/dist/v1/index.d.ts +3 -0
- package/dist/v1/index.d.ts.map +1 -0
- package/dist/v1/index.js +3 -0
- package/dist/v1/index.js.map +1 -0
- package/package.json +2 -1
- package/skills/market/SKILL.md +4 -2
- package/src/asset-implementation.ts +3 -3
- package/src/cli.ts +20 -4
- package/src/client.ts +26 -0
- package/src/commands/install.ts +11 -8
- package/src/commands/list.ts +28 -14
- package/src/commands/pack.ts +7 -2
- package/src/commands/search.ts +21 -2
- package/src/commands/sync.ts +30 -0
- package/src/commands/upload.ts +26 -11
- package/src/contract.ts +0 -3
- package/src/index.ts +13 -3
- package/src/install.ts +89 -35
- package/src/output.ts +52 -11
- package/src/pack.ts +317 -70
- package/src/package-json.ts +7 -4
- package/src/resolve.ts +47 -4
- package/src/schemas.ts +32 -8
- package/src/skill.ts +4 -2
- package/src/v1/client.ts +28 -0
- package/src/v1/contract.ts +140 -0
- package/src/v1/index.ts +2 -0
- package/dist/market-lock.d.ts +0 -16
- package/dist/market-lock.d.ts.map +0 -1
- package/dist/market-lock.js +0 -48
- package/dist/market-lock.js.map +0 -1
- package/src/market-lock.ts +0 -68
package/src/install.ts
CHANGED
|
@@ -18,9 +18,9 @@ import { detectPackageManager, installDependencies } from 'nypm'
|
|
|
18
18
|
import type { MarketClient } from './client.js'
|
|
19
19
|
import type { AssetFilesResult, AssetInstallMetadata } from './contract.js'
|
|
20
20
|
import { withProjectInstallLock } from './install-lock.js'
|
|
21
|
-
import {
|
|
22
|
-
import { parsePackageJson, type PackageJson } from './package-json.js'
|
|
21
|
+
import { packageJsonAssetDependencies, parsePackageJson, type PackageJson } from './package-json.js'
|
|
23
22
|
import type { ResolveResult } from './resolve.js'
|
|
23
|
+
import { assetDependencyAlias, assetDependencyRange, assetDependencyValue } from './schemas.js'
|
|
24
24
|
|
|
25
25
|
export interface InstallOptions {
|
|
26
26
|
/** Directory to start project root discovery from (default: cwd) */
|
|
@@ -78,6 +78,8 @@ interface DownloadOptions {
|
|
|
78
78
|
force: boolean
|
|
79
79
|
log: (message: string) => void
|
|
80
80
|
key?: string
|
|
81
|
+
/** File aliases per asset name: canonical install path → the project's current path for it. */
|
|
82
|
+
aliases: Record<string, Record<string, string>>
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
export async function install(
|
|
@@ -89,15 +91,16 @@ export async function install(
|
|
|
89
91
|
const installRoot = await findInstallRoot(opts.cwd ?? process.cwd())
|
|
90
92
|
const metadata = opts.installMetadata ?? {}
|
|
91
93
|
|
|
94
|
+
const aliases = await projectAssetAliases(installRoot, resolution)
|
|
92
95
|
const download = await downloadAssets(client, resolution, installRoot, {
|
|
93
96
|
force: opts.force ?? false,
|
|
94
97
|
log,
|
|
95
98
|
key: opts.key,
|
|
99
|
+
aliases,
|
|
96
100
|
})
|
|
97
101
|
const integration = await withProjectInstallLock(
|
|
98
102
|
installRoot,
|
|
99
103
|
async () => {
|
|
100
|
-
await writeInstalledAssetLock(installRoot, download.assets)
|
|
101
104
|
const packageJsonUpdate = await updatePackageJson(resolution, installRoot, {
|
|
102
105
|
rootRequests: opts.rootRequests ?? [],
|
|
103
106
|
installMetadata: metadata,
|
|
@@ -186,21 +189,16 @@ async function downloadOneAsset(
|
|
|
186
189
|
// Only the network fetches are retried — downloads are the sole transient step; file writes are
|
|
187
190
|
// deterministic local work and a failure there is a real error, not worth re-attempting.
|
|
188
191
|
const files = await fetchAssetEntries(client, asset, opts)
|
|
192
|
+
const alias = opts.aliases[asset.name] ?? {}
|
|
189
193
|
const installedFiles: string[] = []
|
|
190
194
|
const warnings: string[] = []
|
|
191
195
|
let wrotePackageJson = false
|
|
192
196
|
|
|
193
197
|
for (const [relativePath, content] of files) {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
path.win32.isAbsolute(zipPath)
|
|
199
|
-
) {
|
|
200
|
-
throw new Error(`Zip contains an unsafe path: ${relativePath}`)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const normalizedPath = path.posix.normalize(zipPath)
|
|
198
|
+
const normalizedPath = safeRelativePath(
|
|
199
|
+
relativePath,
|
|
200
|
+
() => `Zip contains an unsafe path: ${relativePath}`,
|
|
201
|
+
)
|
|
204
202
|
if (
|
|
205
203
|
normalizedPath === '.' ||
|
|
206
204
|
(normalizedPath === 'README.md' && asset.type !== 'template') ||
|
|
@@ -209,11 +207,22 @@ async function downloadOneAsset(
|
|
|
209
207
|
continue
|
|
210
208
|
}
|
|
211
209
|
|
|
212
|
-
|
|
210
|
+
// An alias redirects the file to where this project keeps it (a rename recorded by pack or a
|
|
211
|
+
// dependent asset). Alias targets come from metadata, so they pass the same traversal guard.
|
|
212
|
+
const aliased = alias[normalizedPath]
|
|
213
|
+
const targetPath =
|
|
214
|
+
aliased === undefined
|
|
215
|
+
? normalizedPath
|
|
216
|
+
: safeRelativePath(
|
|
217
|
+
aliased,
|
|
218
|
+
() => `Alias for ${normalizedPath} in ${asset.name} is an unsafe path: ${aliased}`,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
const filePath = path.join(projectRoot, targetPath)
|
|
213
222
|
const existing = await maybeReadFile(filePath)
|
|
214
223
|
if (existing && !bytesEqual(existing, content) && !opts.force) {
|
|
215
224
|
warnings.push(
|
|
216
|
-
`Skipped ${
|
|
225
|
+
`Skipped ${targetPath} from ${asset.name}@${asset.version}; file already exists. Re-run with --force to overwrite.`,
|
|
217
226
|
)
|
|
218
227
|
continue
|
|
219
228
|
}
|
|
@@ -221,12 +230,12 @@ async function downloadOneAsset(
|
|
|
221
230
|
if (!existing || !bytesEqual(existing, content)) {
|
|
222
231
|
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
223
232
|
await fs.writeFile(filePath, content)
|
|
224
|
-
if (
|
|
233
|
+
if (targetPath === 'package.json') {
|
|
225
234
|
wrotePackageJson = true
|
|
226
235
|
}
|
|
227
236
|
}
|
|
228
237
|
|
|
229
|
-
installedFiles.push(
|
|
238
|
+
installedFiles.push(targetPath)
|
|
230
239
|
}
|
|
231
240
|
|
|
232
241
|
opts.log(`Downloaded ${installedFiles.length} files from ${asset.name}@${asset.version}.`)
|
|
@@ -243,6 +252,46 @@ async function downloadOneAsset(
|
|
|
243
252
|
}
|
|
244
253
|
}
|
|
245
254
|
|
|
255
|
+
/** Validate + normalize a posix-relative install path; throws `message()` on traversal/absolute. */
|
|
256
|
+
function safeRelativePath(candidate: string, message: () => string): string {
|
|
257
|
+
const posixPath = candidate.replace(/\\/g, '/')
|
|
258
|
+
if (
|
|
259
|
+
posixPath.split('/').includes('..') ||
|
|
260
|
+
path.posix.isAbsolute(posixPath) ||
|
|
261
|
+
path.win32.isAbsolute(posixPath)
|
|
262
|
+
) {
|
|
263
|
+
throw new Error(message())
|
|
264
|
+
}
|
|
265
|
+
return path.posix.normalize(posixPath)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* File aliases per asset, merged from the project's own package.json `assetDependencies` and the
|
|
270
|
+
* aliases dependents declared in their metadata. The project's entries win: the user's local
|
|
271
|
+
* layout beats what a template recorded at publish time.
|
|
272
|
+
*/
|
|
273
|
+
async function projectAssetAliases(
|
|
274
|
+
projectRoot: string,
|
|
275
|
+
resolution: ResolveResult,
|
|
276
|
+
): Promise<Record<string, Record<string, string>>> {
|
|
277
|
+
const merged: Record<string, Record<string, string>> = {}
|
|
278
|
+
for (const [name, alias] of Object.entries(resolution.assetAliases ?? {})) {
|
|
279
|
+
merged[name] = { ...alias }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const pkgPath = path.join(projectRoot, 'package.json')
|
|
283
|
+
const pkgJson = await maybeReadFile(pkgPath)
|
|
284
|
+
if (!pkgJson) return merged
|
|
285
|
+
|
|
286
|
+
const declared = packageJsonAssetDependencies(new TextDecoder().decode(pkgJson), pkgPath)
|
|
287
|
+
for (const [name, value] of Object.entries(declared)) {
|
|
288
|
+
const alias = assetDependencyAlias(value)
|
|
289
|
+
if (Object.keys(alias).length === 0) continue
|
|
290
|
+
merged[name] = { ...(merged[name] ?? {}), ...alias }
|
|
291
|
+
}
|
|
292
|
+
return merged
|
|
293
|
+
}
|
|
294
|
+
|
|
246
295
|
/** Files fetched in flight per asset. Assets themselves download 8-wide, so this bounds total
|
|
247
296
|
* connections at 8×4 against edge-cached objects — cheap for the CDN, fast for the install. */
|
|
248
297
|
const FILE_DOWNLOAD_CONCURRENCY = 4
|
|
@@ -396,7 +445,7 @@ async function updatePackageJson(
|
|
|
396
445
|
opts.rootRequests,
|
|
397
446
|
opts.installMetadata,
|
|
398
447
|
)
|
|
399
|
-
if (
|
|
448
|
+
if (mergeAssetDependencyRanges(pkg, assetDependencies)) {
|
|
400
449
|
changed = true
|
|
401
450
|
}
|
|
402
451
|
|
|
@@ -417,28 +466,13 @@ async function runPackageManagerInstall(projectRoot: string, log: (msg: string)
|
|
|
417
466
|
log('npm dependencies installed.')
|
|
418
467
|
}
|
|
419
468
|
|
|
420
|
-
async function writeInstalledAssetLock(
|
|
421
|
-
projectRoot: string,
|
|
422
|
-
assets: InstalledAsset[],
|
|
423
|
-
): Promise<void> {
|
|
424
|
-
const lock = await readMarketLock(projectRoot)
|
|
425
|
-
for (const asset of assets) {
|
|
426
|
-
lock.assets[asset.name] = {
|
|
427
|
-
version: asset.version,
|
|
428
|
-
type: asset.type,
|
|
429
|
-
files: asset.files,
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
await writeMarketLock(projectRoot, lock)
|
|
433
|
-
}
|
|
434
|
-
|
|
435
469
|
function defaultPackageJson(): PackageJson {
|
|
436
470
|
return { name: 'my-project', private: true, dependencies: {} }
|
|
437
471
|
}
|
|
438
472
|
|
|
439
473
|
function mergeRecord(
|
|
440
474
|
pkg: PackageJson,
|
|
441
|
-
field: 'dependencies'
|
|
475
|
+
field: 'dependencies',
|
|
442
476
|
values: Record<string, string>,
|
|
443
477
|
): boolean {
|
|
444
478
|
const entries = Object.entries(values)
|
|
@@ -448,7 +482,7 @@ function mergeRecord(
|
|
|
448
482
|
let changed = false
|
|
449
483
|
|
|
450
484
|
for (const [name, range] of entries) {
|
|
451
|
-
if (
|
|
485
|
+
if (name.startsWith('@drawcall/') && current[name] === 'latest') {
|
|
452
486
|
continue
|
|
453
487
|
}
|
|
454
488
|
if (current[name] === range) continue
|
|
@@ -460,6 +494,26 @@ function mergeRecord(
|
|
|
460
494
|
return changed
|
|
461
495
|
}
|
|
462
496
|
|
|
497
|
+
/** Merge saved ranges into `assetDependencies`, preserving any aliases an entry already carries. */
|
|
498
|
+
function mergeAssetDependencyRanges(pkg: PackageJson, values: Record<string, string>): boolean {
|
|
499
|
+
const entries = Object.entries(values)
|
|
500
|
+
if (entries.length === 0) return false
|
|
501
|
+
|
|
502
|
+
const current = pkg.assetDependencies ?? {}
|
|
503
|
+
let changed = false
|
|
504
|
+
|
|
505
|
+
for (const [name, range] of entries) {
|
|
506
|
+
const existing = current[name]
|
|
507
|
+
if (existing !== undefined && assetDependencyRange(existing) === range) continue
|
|
508
|
+
current[name] =
|
|
509
|
+
existing === undefined ? range : assetDependencyValue(range, assetDependencyAlias(existing))
|
|
510
|
+
changed = true
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
pkg.assetDependencies = current
|
|
514
|
+
return changed
|
|
515
|
+
}
|
|
516
|
+
|
|
463
517
|
function assetDependenciesToSave(
|
|
464
518
|
resolution: ResolveResult,
|
|
465
519
|
rootRequests: InstallRootRequest[],
|
package/src/output.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { AssetInstallMetadata, AssetSearchResult } from './contract.js'
|
|
2
2
|
import type { ListInstalledAssetsResult } from './commands/list.js'
|
|
3
3
|
import type { InstallResult } from './install.js'
|
|
4
|
-
import type { PackedAsset } from './pack.js'
|
|
4
|
+
import type { AssetDependencySync, PackedAsset } from './pack.js'
|
|
5
|
+
import { assetDependencyAlias, assetDependencyRange } from './schemas.js'
|
|
5
6
|
|
|
6
7
|
export function assetVersionRef(name: string, version?: string): string {
|
|
7
8
|
return version ? `${name}@${version}` : name
|
|
@@ -80,18 +81,16 @@ export function installResult(
|
|
|
80
81
|
return lines.join('\n')
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
export function listResult(
|
|
84
|
-
result: ListInstalledAssetsResult,
|
|
85
|
-
opts: { files?: boolean } = {},
|
|
86
|
-
): string {
|
|
84
|
+
export function listResult(result: ListInstalledAssetsResult): string {
|
|
87
85
|
if (result.assets.length === 0) {
|
|
88
|
-
return 'No
|
|
86
|
+
return 'No asset dependencies declared in package.json.'
|
|
89
87
|
}
|
|
90
88
|
|
|
91
|
-
const lines = [`
|
|
89
|
+
const lines = [`Asset dependencies: ${result.assets.length}`]
|
|
92
90
|
for (const asset of result.assets) {
|
|
93
|
-
lines.push(
|
|
94
|
-
|
|
91
|
+
lines.push(`- ${assetVersionRef(asset.name, asset.range)}`)
|
|
92
|
+
const aliases = Object.entries(asset.alias).sort(([a], [b]) => a.localeCompare(b))
|
|
93
|
+
lines.push(...aliases.map(([canonical, current]) => ` ${canonical} -> ${current}`))
|
|
95
94
|
}
|
|
96
95
|
|
|
97
96
|
return lines.join('\n')
|
|
@@ -183,6 +182,35 @@ export function unchangedUploadResult(name: string, version: string): string {
|
|
|
183
182
|
return `No upload needed: ${assetVersionRef(name, version)} is unchanged`
|
|
184
183
|
}
|
|
185
184
|
|
|
185
|
+
export function syncResult(result: AssetDependencySync): string {
|
|
186
|
+
const entries = Object.entries(result.assetDependencies)
|
|
187
|
+
if (entries.length === 0) {
|
|
188
|
+
return 'No asset dependencies declared in package.json.'
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const lines = [
|
|
192
|
+
result.changed
|
|
193
|
+
? 'Updated assetDependencies in package.json:'
|
|
194
|
+
: 'assetDependencies already up to date:',
|
|
195
|
+
]
|
|
196
|
+
for (const [name, value] of entries.sort(([a], [b]) => a.localeCompare(b))) {
|
|
197
|
+
lines.push(`- ${assetVersionRef(name, assetDependencyRange(value))}`)
|
|
198
|
+
const aliases = Object.entries(assetDependencyAlias(value)).sort(([a], [b]) =>
|
|
199
|
+
a.localeCompare(b),
|
|
200
|
+
)
|
|
201
|
+
lines.push(...aliases.map(([canonical, current]) => ` ${canonical} -> ${current}`))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (result.missing.length > 0) {
|
|
205
|
+
lines.push('Dependency files not found (locally modified or deleted):')
|
|
206
|
+
lines.push(...result.missing.map((file) => `- ${file.name}: ${file.path}`))
|
|
207
|
+
}
|
|
208
|
+
if (result.skipped.length > 0) {
|
|
209
|
+
lines.push(`Skipped (manifest unavailable): ${[...result.skipped].sort().join(', ')}`)
|
|
210
|
+
}
|
|
211
|
+
return lines.join('\n')
|
|
212
|
+
}
|
|
213
|
+
|
|
186
214
|
export function packResult(out: string, packed: PackedAsset): string {
|
|
187
215
|
const lines = [`Packed: ${out}`]
|
|
188
216
|
if (packed.gitignoredFiles > 0) {
|
|
@@ -226,11 +254,24 @@ function unique<T>(values: T[]): T[] {
|
|
|
226
254
|
return [...new Set(values)]
|
|
227
255
|
}
|
|
228
256
|
|
|
229
|
-
function dependencyLines(
|
|
257
|
+
function dependencyLines(
|
|
258
|
+
label: string,
|
|
259
|
+
deps: Record<string, string | { version: string; alias?: Record<string, string> }>,
|
|
260
|
+
): string[] {
|
|
230
261
|
const entries = Object.entries(deps)
|
|
231
262
|
if (entries.length === 0) return []
|
|
232
263
|
return [
|
|
233
264
|
`${label}:`,
|
|
234
|
-
...entries
|
|
265
|
+
...entries
|
|
266
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
267
|
+
.map(([name, value]) => `- ${name}: ${dependencyValueLabel(value)}`),
|
|
235
268
|
]
|
|
236
269
|
}
|
|
270
|
+
|
|
271
|
+
function dependencyValueLabel(
|
|
272
|
+
value: string | { version: string; alias?: Record<string, string> },
|
|
273
|
+
): string {
|
|
274
|
+
if (typeof value === 'string') return value
|
|
275
|
+
const aliased = Object.keys(value.alias ?? {}).length
|
|
276
|
+
return aliased > 0 ? `${value.version} (${aliased} aliased file(s))` : value.version
|
|
277
|
+
}
|