@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.
Files changed (104) hide show
  1. package/README.md +7 -4
  2. package/dist/asset-implementation.d.ts +3 -3
  3. package/dist/asset-implementation.d.ts.map +1 -1
  4. package/dist/cli.js +15 -3
  5. package/dist/cli.js.map +1 -1
  6. package/dist/client.d.ts +14 -0
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +18 -0
  9. package/dist/client.js.map +1 -1
  10. package/dist/commands/install.js +10 -7
  11. package/dist/commands/install.js.map +1 -1
  12. package/dist/commands/list.d.ts +5 -5
  13. package/dist/commands/list.d.ts.map +1 -1
  14. package/dist/commands/list.js +22 -9
  15. package/dist/commands/list.js.map +1 -1
  16. package/dist/commands/pack.d.ts.map +1 -1
  17. package/dist/commands/pack.js +2 -2
  18. package/dist/commands/pack.js.map +1 -1
  19. package/dist/commands/search.d.ts +2 -0
  20. package/dist/commands/search.d.ts.map +1 -1
  21. package/dist/commands/search.js +14 -2
  22. package/dist/commands/search.js.map +1 -1
  23. package/dist/commands/sync.d.ts +6 -0
  24. package/dist/commands/sync.d.ts.map +1 -0
  25. package/dist/commands/sync.js +22 -0
  26. package/dist/commands/sync.js.map +1 -0
  27. package/dist/commands/upload.d.ts.map +1 -1
  28. package/dist/commands/upload.js +20 -10
  29. package/dist/commands/upload.js.map +1 -1
  30. package/dist/contract.d.ts +4 -6
  31. package/dist/contract.d.ts.map +1 -1
  32. package/dist/contract.js +1 -2
  33. package/dist/contract.js.map +1 -1
  34. package/dist/index.d.ts +5 -4
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +5 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/install.d.ts.map +1 -1
  39. package/dist/install.js +69 -27
  40. package/dist/install.js.map +1 -1
  41. package/dist/output.d.ts +3 -4
  42. package/dist/output.d.ts.map +1 -1
  43. package/dist/output.js +40 -7
  44. package/dist/output.js.map +1 -1
  45. package/dist/pack.d.ts +36 -6
  46. package/dist/pack.d.ts.map +1 -1
  47. package/dist/pack.js +230 -57
  48. package/dist/pack.js.map +1 -1
  49. package/dist/package-json.d.ts +4 -3
  50. package/dist/package-json.d.ts.map +1 -1
  51. package/dist/package-json.js +3 -1
  52. package/dist/package-json.js.map +1 -1
  53. package/dist/resolve.d.ts +8 -1
  54. package/dist/resolve.d.ts.map +1 -1
  55. package/dist/resolve.js +35 -3
  56. package/dist/resolve.js.map +1 -1
  57. package/dist/schemas.d.ts +19 -7
  58. package/dist/schemas.d.ts.map +1 -1
  59. package/dist/schemas.js +22 -7
  60. package/dist/schemas.js.map +1 -1
  61. package/dist/skill.d.ts +1 -1
  62. package/dist/skill.d.ts.map +1 -1
  63. package/dist/skill.js +4 -2
  64. package/dist/skill.js.map +1 -1
  65. package/dist/v1/client.d.ts +15 -0
  66. package/dist/v1/client.d.ts.map +1 -0
  67. package/dist/v1/client.js +18 -0
  68. package/dist/v1/client.js.map +1 -0
  69. package/dist/v1/contract.d.ts +206 -0
  70. package/dist/v1/contract.d.ts.map +1 -0
  71. package/dist/v1/contract.js +77 -0
  72. package/dist/v1/contract.js.map +1 -0
  73. package/dist/v1/index.d.ts +3 -0
  74. package/dist/v1/index.d.ts.map +1 -0
  75. package/dist/v1/index.js +3 -0
  76. package/dist/v1/index.js.map +1 -0
  77. package/package.json +2 -1
  78. package/skills/market/SKILL.md +4 -2
  79. package/src/asset-implementation.ts +3 -3
  80. package/src/cli.ts +20 -4
  81. package/src/client.ts +26 -0
  82. package/src/commands/install.ts +11 -8
  83. package/src/commands/list.ts +28 -14
  84. package/src/commands/pack.ts +7 -2
  85. package/src/commands/search.ts +21 -2
  86. package/src/commands/sync.ts +30 -0
  87. package/src/commands/upload.ts +26 -11
  88. package/src/contract.ts +0 -3
  89. package/src/index.ts +13 -3
  90. package/src/install.ts +89 -35
  91. package/src/output.ts +52 -11
  92. package/src/pack.ts +317 -70
  93. package/src/package-json.ts +7 -4
  94. package/src/resolve.ts +47 -4
  95. package/src/schemas.ts +32 -8
  96. package/src/skill.ts +4 -2
  97. package/src/v1/client.ts +28 -0
  98. package/src/v1/contract.ts +140 -0
  99. package/src/v1/index.ts +2 -0
  100. package/dist/market-lock.d.ts +0 -16
  101. package/dist/market-lock.d.ts.map +0 -1
  102. package/dist/market-lock.js +0 -48
  103. package/dist/market-lock.js.map +0 -1
  104. 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 { readMarketLock, writeMarketLock } from './market-lock.js'
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 zipPath = relativePath.replace(/\\/g, '/')
195
- if (
196
- zipPath.split('/').includes('..') ||
197
- path.posix.isAbsolute(zipPath) ||
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
- const filePath = path.join(projectRoot, normalizedPath)
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 ${normalizedPath} from ${asset.name}@${asset.version}; file already exists. Re-run with --force to overwrite.`,
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 (normalizedPath === 'package.json') {
233
+ if (targetPath === 'package.json') {
225
234
  wrotePackageJson = true
226
235
  }
227
236
  }
228
237
 
229
- installedFiles.push(normalizedPath)
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 (mergeRecord(pkg, 'assetDependencies', assetDependencies)) {
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' | 'assetDependencies',
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 (field === 'dependencies' && name.startsWith('@drawcall/') && current[name] === 'latest') {
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 installed assets found.'
86
+ return 'No asset dependencies declared in package.json.'
89
87
  }
90
88
 
91
- const lines = [`Installed assets: ${result.assets.length}`]
89
+ const lines = [`Asset dependencies: ${result.assets.length}`]
92
90
  for (const asset of result.assets) {
93
- lines.push(assetLine(asset))
94
- if (opts.files) lines.push(...assetFileLines(asset.files))
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(label: string, deps: Record<string, string>): string[] {
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.sort(([a], [b]) => a.localeCompare(b)).map(([name, value]) => `- ${name}: ${value}`),
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
+ }