@drawcall/market 0.1.99 → 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/dist/asset-implementation.d.ts +2 -2
- package/dist/asset-implementation.d.ts.map +1 -1
- package/dist/cli.js +15 -3
- package/dist/cli.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 +12 -7
- package/dist/commands/upload.js.map +1 -1
- package/dist/contract.d.ts +4 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- 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 -2
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +21 -1
- 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 +2 -2
- package/src/cli.ts +20 -4
- 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 +18 -8
- package/src/index.ts +11 -0
- 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 +31 -1
- 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/skills/market/SKILL.md
CHANGED
|
@@ -31,7 +31,9 @@ Install exact refs together in one call; names need no `--type`:
|
|
|
31
31
|
|
|
32
32
|
Install prints the exact files it wrote and records the dependency so packing and later rebuilds can restore it. It never searches or generates. With no refs it installs `assetDependencies` from the nearest `package.json`. Assets may install declared skill dependencies with `npx skills add`. Use `--force` only with permission to overwrite changed local files.
|
|
33
33
|
|
|
34
|
-
`list --cwd "$PWD"` is an offline, compact inventory
|
|
34
|
+
`list --cwd "$PWD"` is an offline, compact inventory of the `assetDependencies` in the nearest `package.json`, including file aliases. When selected refs are already installed, read this inventory instead of reinstalling them.
|
|
35
|
+
|
|
36
|
+
An `assetDependencies` entry is either a range string or `{ "version": "<range>", "alias": { "<canonical path>": "<current path>" } }`. An alias records where the project keeps a renamed installed file: install writes the file at the aliased path, and packing omits exactly that one copy (extra duplicates ship as project content). Run `sync --cwd "$PWD"` after renaming installed files to record aliases in `package.json`; template packing runs the same update automatically.
|
|
35
37
|
|
|
36
38
|
## Generate
|
|
37
39
|
|
|
@@ -47,6 +49,6 @@ Add `--access public` or `--access private` when visibility matters. Omitted acc
|
|
|
47
49
|
|
|
48
50
|
## Publish only when asked
|
|
49
51
|
|
|
50
|
-
Use `pack <source> --out <zip>` to create a Market zip and `upload <name> <source> "<description>" --type <type>` to publish it. Both accept repeatable `--npm name@range`, `--asset name@range`, and `--skill label=source` dependencies. Template packing reads root `assetDependencies` and may use the API to omit
|
|
52
|
+
Use `pack <source> --out <zip>` to create a Market zip and `upload <name> <source> "<description>" --type <type>` to publish it. Both accept repeatable `--npm name@range`, `--asset name@range`, and `--skill label=source` dependencies. Template packing reads root `assetDependencies` and may use the API to omit installed dependency files matched by content hash — even when renamed — recording renames as aliases. A skill source is `owner/repo`, a git URL, a full GitHub `tree/<branch>/<subpath>` URL, or a local skill directory inside the zip. Use the command's `--help` for syntax.
|
|
51
53
|
|
|
52
54
|
Use `--unapproved` only when explicitly requested. Do not install an unapproved asset without acceptance. If login is required, ask before running `npx @drawcall/market login`.
|
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
GenerateAssetResult,
|
|
7
7
|
PaginatedList,
|
|
8
8
|
} from './contract.js'
|
|
9
|
-
import type { AssetAccess, AssetType, GenerateJobStatus } from './schemas.js'
|
|
9
|
+
import type { AssetAccess, AssetDependencies, AssetType, GenerateJobStatus } from './schemas.js'
|
|
10
10
|
|
|
11
11
|
export const ASSET_RELEVANCE_SCORE: unique symbol = Symbol('assetRelevanceScore')
|
|
12
12
|
|
|
@@ -38,7 +38,7 @@ export interface AssetUploadZipInput {
|
|
|
38
38
|
version: string
|
|
39
39
|
description?: string
|
|
40
40
|
npmDependencies: Record<string, string>
|
|
41
|
-
assetDependencies:
|
|
41
|
+
assetDependencies: AssetDependencies
|
|
42
42
|
skillDependencies: Record<string, string>
|
|
43
43
|
tags: string[]
|
|
44
44
|
zip: File
|
package/src/cli.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { generateCommand } from './commands/generate.js'
|
|
|
13
13
|
import { generateInstallCommand } from './commands/generate-install.js'
|
|
14
14
|
import { listCommand } from './commands/list.js'
|
|
15
15
|
import { packCommand } from './commands/pack.js'
|
|
16
|
+
import { syncCommand } from './commands/sync.js'
|
|
16
17
|
import { previewCommand } from './commands/preview.js'
|
|
17
18
|
import { uploadCommand } from './commands/upload.js'
|
|
18
19
|
import { typesCommand } from './commands/types.js'
|
|
@@ -125,13 +126,25 @@ program
|
|
|
125
126
|
|
|
126
127
|
program
|
|
127
128
|
.command('list')
|
|
128
|
-
.description('List
|
|
129
|
-
.option('--files', 'Include installed file paths', false)
|
|
129
|
+
.description('List asset dependencies declared in package.json, including file aliases')
|
|
130
130
|
.option('--cwd <dir>', 'Project directory')
|
|
131
|
-
.action(async (opts: { cwd?: string
|
|
131
|
+
.action(async (opts: { cwd?: string }) => {
|
|
132
132
|
await listCommand({
|
|
133
133
|
cwd: opts.cwd,
|
|
134
|
-
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
program
|
|
138
|
+
.command('sync')
|
|
139
|
+
.description(
|
|
140
|
+
'Update package.json assetDependencies aliases to match where installed files live now',
|
|
141
|
+
)
|
|
142
|
+
.addOption(apiOption)
|
|
143
|
+
.option('--cwd <dir>', 'Project directory')
|
|
144
|
+
.action(async (opts: { api?: string; cwd?: string }) => {
|
|
145
|
+
await syncCommand({
|
|
146
|
+
baseUrl: opts.api,
|
|
147
|
+
cwd: opts.cwd,
|
|
135
148
|
})
|
|
136
149
|
})
|
|
137
150
|
|
|
@@ -176,6 +189,7 @@ program
|
|
|
176
189
|
.addOption(apiOption)
|
|
177
190
|
.option('--unapproved', 'Include unapproved versions', false)
|
|
178
191
|
.option('--limit <n>', 'Max results, 1-5', parseSearchLimit, 5)
|
|
192
|
+
.option('--json', 'Emit a machine-readable JSON array instead of the summary lines', false)
|
|
179
193
|
.action(
|
|
180
194
|
async (
|
|
181
195
|
query: string,
|
|
@@ -184,6 +198,7 @@ program
|
|
|
184
198
|
api?: string
|
|
185
199
|
unapproved: boolean
|
|
186
200
|
limit: number
|
|
201
|
+
json: boolean
|
|
187
202
|
},
|
|
188
203
|
) => {
|
|
189
204
|
requireType(opts.type, 'Search')
|
|
@@ -192,6 +207,7 @@ program
|
|
|
192
207
|
baseUrl: opts.api,
|
|
193
208
|
unapproved: opts.unapproved,
|
|
194
209
|
limit: opts.limit,
|
|
210
|
+
json: opts.json,
|
|
195
211
|
})
|
|
196
212
|
},
|
|
197
213
|
)
|
package/src/commands/install.ts
CHANGED
|
@@ -4,7 +4,7 @@ import ora from 'ora'
|
|
|
4
4
|
import { resolve } from '../resolve.js'
|
|
5
5
|
import { findInstallRoot, install as runInstall, type InstallRootRequest } from '../install.js'
|
|
6
6
|
import { packageJsonAssetDependencies } from '../package-json.js'
|
|
7
|
-
import { assetNameSchema } from '../schemas.js'
|
|
7
|
+
import { assetDependencyRange, assetNameSchema, type AssetDependencies } from '../schemas.js'
|
|
8
8
|
import { getCliClient } from '../cli-client.js'
|
|
9
9
|
import { installResult } from '../output.js'
|
|
10
10
|
import type { MarketClient } from '../client.js'
|
|
@@ -125,12 +125,15 @@ async function packageJsonRequests(cwd: string | undefined): Promise<AssetReques
|
|
|
125
125
|
const installRoot = await findInstallRoot(cwd ?? process.cwd())
|
|
126
126
|
const pkgPath = path.join(installRoot, 'package.json')
|
|
127
127
|
const dependencies = await readPackageJsonAssetDependencies(pkgPath)
|
|
128
|
-
const requests = Object.entries(dependencies).map(([name,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
128
|
+
const requests = Object.entries(dependencies).map(([name, value]) => {
|
|
129
|
+
const range = assetDependencyRange(value)
|
|
130
|
+
return {
|
|
131
|
+
name,
|
|
132
|
+
range,
|
|
133
|
+
saveRange: range,
|
|
134
|
+
save: false,
|
|
135
|
+
}
|
|
136
|
+
})
|
|
134
137
|
|
|
135
138
|
if (requests.length === 0) {
|
|
136
139
|
throw new Error(
|
|
@@ -141,7 +144,7 @@ async function packageJsonRequests(cwd: string | undefined): Promise<AssetReques
|
|
|
141
144
|
return requests
|
|
142
145
|
}
|
|
143
146
|
|
|
144
|
-
async function readPackageJsonAssetDependencies(pkgPath: string): Promise<
|
|
147
|
+
async function readPackageJsonAssetDependencies(pkgPath: string): Promise<AssetDependencies> {
|
|
145
148
|
try {
|
|
146
149
|
return packageJsonAssetDependencies(await fs.readFile(pkgPath, 'utf-8'), pkgPath)
|
|
147
150
|
} catch (error) {
|
package/src/commands/list.ts
CHANGED
|
@@ -1,48 +1,62 @@
|
|
|
1
|
+
import * as fs from 'fs/promises'
|
|
1
2
|
import * as path from 'path'
|
|
2
3
|
import { findInstallRoot } from '../install.js'
|
|
3
|
-
import {
|
|
4
|
+
import { packageJsonAssetDependencies } from '../package-json.js'
|
|
5
|
+
import { assetDependencyAlias, assetDependencyRange } from '../schemas.js'
|
|
4
6
|
import { listResult } from '../output.js'
|
|
5
7
|
|
|
6
8
|
export interface ListCommandOptions {
|
|
7
9
|
cwd?: string
|
|
8
|
-
files?: boolean
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export interface InstalledAssetListing {
|
|
12
13
|
name: string
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
range: string
|
|
15
|
+
/** Canonical install path → the project's current path, for files renamed after install. */
|
|
16
|
+
alias: Record<string, string>
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export interface ListInstalledAssetsResult {
|
|
19
20
|
projectRoot: string
|
|
20
|
-
|
|
21
|
+
packageJsonPath: string
|
|
21
22
|
assets: InstalledAssetListing[]
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export async function listCommand(opts: ListCommandOptions): Promise<void> {
|
|
25
26
|
const result = await listInstalledAssets(opts.cwd)
|
|
26
|
-
console.log(listResult(result
|
|
27
|
+
console.log(listResult(result))
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/** The project's declared asset dependencies — package.json is the single source of truth. */
|
|
29
31
|
export async function listInstalledAssets(
|
|
30
32
|
cwd: string | undefined,
|
|
31
33
|
): Promise<ListInstalledAssetsResult> {
|
|
32
34
|
const projectRoot = await findInstallRoot(cwd ?? process.cwd())
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
35
|
+
const packageJsonPath = path.join(projectRoot, 'package.json')
|
|
36
|
+
const dependencies = await readAssetDependencies(packageJsonPath)
|
|
37
|
+
const assets = Object.entries(dependencies)
|
|
38
|
+
.map(([name, value]) => ({
|
|
36
39
|
name,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
files: [...asset.files].sort((a, b) => a.localeCompare(b)),
|
|
40
|
+
range: assetDependencyRange(value),
|
|
41
|
+
alias: assetDependencyAlias(value),
|
|
40
42
|
}))
|
|
41
43
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
42
44
|
|
|
43
45
|
return {
|
|
44
46
|
projectRoot,
|
|
45
|
-
|
|
47
|
+
packageJsonPath,
|
|
46
48
|
assets,
|
|
47
49
|
}
|
|
48
50
|
}
|
|
51
|
+
|
|
52
|
+
async function readAssetDependencies(packageJsonPath: string) {
|
|
53
|
+
try {
|
|
54
|
+
return packageJsonAssetDependencies(
|
|
55
|
+
await fs.readFile(packageJsonPath, 'utf-8'),
|
|
56
|
+
packageJsonPath,
|
|
57
|
+
)
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return {}
|
|
60
|
+
throw error
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/commands/pack.ts
CHANGED
|
@@ -2,7 +2,12 @@ import * as fs from 'fs/promises'
|
|
|
2
2
|
import * as path from 'path'
|
|
3
3
|
import ora from 'ora'
|
|
4
4
|
import { getCliClient } from '../cli-client.js'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
fileManifestForRange,
|
|
7
|
+
packAsset,
|
|
8
|
+
packPolicyForType,
|
|
9
|
+
parsePackDependencies,
|
|
10
|
+
} from '../pack.js'
|
|
6
11
|
import { packResult } from '../output.js'
|
|
7
12
|
import type { AssetType } from '../schemas.js'
|
|
8
13
|
|
|
@@ -39,7 +44,7 @@ export async function packCommand(zipFilter: string, opts: PackCommandOptions):
|
|
|
39
44
|
skill: opts.skill,
|
|
40
45
|
}),
|
|
41
46
|
policy: packPolicyForType(opts.type),
|
|
42
|
-
fetchFileManifest: (
|
|
47
|
+
fetchFileManifest: fileManifestForRange(client.asset),
|
|
43
48
|
})
|
|
44
49
|
const out = opts.out ? path.resolve(cwd, opts.out) : defaultPackPath(packed.sourcePath)
|
|
45
50
|
|
package/src/commands/search.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface SearchCommandOptions {
|
|
|
10
10
|
unapproved?: boolean
|
|
11
11
|
baseUrl?: string
|
|
12
12
|
limit?: number
|
|
13
|
+
/** Emit a machine-readable JSON array instead of the human-readable lines. */
|
|
14
|
+
json?: boolean
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export async function searchCommand(query: string, opts: SearchCommandOptions): Promise<void> {
|
|
@@ -17,8 +19,8 @@ export async function searchCommand(query: string, opts: SearchCommandOptions):
|
|
|
17
19
|
|
|
18
20
|
const spinner = ora({
|
|
19
21
|
text: `Searching "${query}"`,
|
|
20
|
-
isEnabled: Boolean(process.stderr.isTTY),
|
|
21
|
-
isSilent: !process.stderr.isTTY,
|
|
22
|
+
isEnabled: Boolean(process.stderr.isTTY) && !opts.json,
|
|
23
|
+
isSilent: !process.stderr.isTTY || Boolean(opts.json),
|
|
22
24
|
}).start()
|
|
23
25
|
const results = await client.asset
|
|
24
26
|
.search({
|
|
@@ -31,6 +33,23 @@ export async function searchCommand(query: string, opts: SearchCommandOptions):
|
|
|
31
33
|
})
|
|
32
34
|
.finally(() => spinner.stop())
|
|
33
35
|
|
|
36
|
+
if (opts.json) {
|
|
37
|
+
// Stable machine-readable shape for scripts; the human-readable lines below may change freely.
|
|
38
|
+
console.log(
|
|
39
|
+
JSON.stringify(
|
|
40
|
+
results.items.map((item) => ({
|
|
41
|
+
name: item.name,
|
|
42
|
+
version: item.latestVersion,
|
|
43
|
+
type: item.type,
|
|
44
|
+
approved: item.approved,
|
|
45
|
+
access: item.access,
|
|
46
|
+
description: item.description,
|
|
47
|
+
})),
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
34
53
|
console.log(
|
|
35
54
|
searchSummary({
|
|
36
55
|
query,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import ora from 'ora'
|
|
2
|
+
import { getCliClient } from '../cli-client.js'
|
|
3
|
+
import { fileManifestForRange, syncAssetDependencies } from '../pack.js'
|
|
4
|
+
import { syncResult } from '../output.js'
|
|
5
|
+
|
|
6
|
+
export interface SyncCommandOptions {
|
|
7
|
+
cwd?: string
|
|
8
|
+
baseUrl?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function syncCommand(opts: SyncCommandOptions): Promise<void> {
|
|
12
|
+
const spinner = ora({
|
|
13
|
+
text: 'Syncing assetDependencies',
|
|
14
|
+
isEnabled: Boolean(process.stderr.isTTY),
|
|
15
|
+
isSilent: !process.stderr.isTTY,
|
|
16
|
+
}).start()
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const { client } = await getCliClient({ baseUrl: opts.baseUrl })
|
|
20
|
+
const result = await syncAssetDependencies(
|
|
21
|
+
opts.cwd ?? process.cwd(),
|
|
22
|
+
fileManifestForRange(client.asset),
|
|
23
|
+
)
|
|
24
|
+
spinner.stop()
|
|
25
|
+
console.log(syncResult(result))
|
|
26
|
+
} catch (err) {
|
|
27
|
+
spinner.stop()
|
|
28
|
+
throw err
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/commands/upload.ts
CHANGED
|
@@ -2,7 +2,12 @@ import ora from 'ora'
|
|
|
2
2
|
import semver from 'semver'
|
|
3
3
|
import { downloadAssetZipBytes } from '../client.js'
|
|
4
4
|
import { getCliClient } from '../cli-client.js'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
fileManifestForRange,
|
|
7
|
+
packAsset,
|
|
8
|
+
packPolicyFromInstallMetadata,
|
|
9
|
+
parsePackDependencies,
|
|
10
|
+
} from '../pack.js'
|
|
6
11
|
import { unchangedUploadResult, uploadResult } from '../output.js'
|
|
7
12
|
import {
|
|
8
13
|
assetDescriptionSchema,
|
|
@@ -61,7 +66,7 @@ export async function uploadCommand(
|
|
|
61
66
|
cwd,
|
|
62
67
|
dependencies,
|
|
63
68
|
policy: packPolicyFromInstallMetadata(installMetadata[type]),
|
|
64
|
-
fetchFileManifest: (
|
|
69
|
+
fetchFileManifest: fileManifestForRange(client.asset),
|
|
65
70
|
})
|
|
66
71
|
const { zip, npmDependencies, assetDependencies, skillDependencies } = packed
|
|
67
72
|
const profile = await client.user.getProfile()
|
|
@@ -133,16 +138,21 @@ export function parseUploadDescription(description: string): string {
|
|
|
133
138
|
}
|
|
134
139
|
}
|
|
135
140
|
|
|
136
|
-
/** Compare a stored dependency JSON string against a freshly parsed record.
|
|
137
|
-
|
|
138
|
-
|
|
141
|
+
/** Compare a stored dependency JSON string against a freshly parsed record. Values may be nested
|
|
142
|
+
* (asset dependencies carry `{ version, alias }`), so equality is structural with sorted keys. */
|
|
143
|
+
function depsEqual(storedJson: string, next: Record<string, unknown>): boolean {
|
|
139
144
|
try {
|
|
140
|
-
|
|
145
|
+
return stableJson(JSON.parse(storedJson || '{}')) === stableJson(next)
|
|
141
146
|
} catch {
|
|
142
147
|
return false
|
|
143
148
|
}
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function stableJson(value: unknown): string {
|
|
152
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return JSON.stringify(value)
|
|
153
|
+
const record = value as Record<string, unknown>
|
|
154
|
+
const keys = Object.keys(record).sort()
|
|
155
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(',')}}`
|
|
146
156
|
}
|
|
147
157
|
|
|
148
158
|
function nextVersion(latest?: string): string {
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// The v1 REST surface (`/api/v1`): routed contract + typed client. Prefer this; the legacy RPC
|
|
2
|
+
// client below is deprecated and retires together with `/api/rpc`.
|
|
3
|
+
export * as v1 from './v1/index.js'
|
|
4
|
+
|
|
1
5
|
// Client
|
|
2
6
|
export { createMarketClient, downloadAssetZipBytes } from './client.js'
|
|
3
7
|
export type { MarketClient, MarketClientOptions } from './client.js'
|
|
@@ -62,6 +66,11 @@ export {
|
|
|
62
66
|
assetAccessSchema,
|
|
63
67
|
npmDependenciesSchema,
|
|
64
68
|
assetDependenciesSchema,
|
|
69
|
+
assetDependencyAliasSchema,
|
|
70
|
+
assetDependencyValueSchema,
|
|
71
|
+
assetDependencyRange,
|
|
72
|
+
assetDependencyAlias,
|
|
73
|
+
assetDependencyValue,
|
|
65
74
|
skillDependenciesSchema,
|
|
66
75
|
uploadZipSchema,
|
|
67
76
|
exactAssetSchema,
|
|
@@ -82,6 +91,8 @@ export {
|
|
|
82
91
|
export type {
|
|
83
92
|
AssetType,
|
|
84
93
|
AssetAccess,
|
|
94
|
+
AssetDependencies,
|
|
95
|
+
AssetDependencyValue,
|
|
85
96
|
AssetFileManifest,
|
|
86
97
|
AgentResultAsset,
|
|
87
98
|
AgentResult,
|
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[],
|