@drawcall/market 0.2.3 → 0.4.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 +8 -8
- package/dist/client.d.ts +4 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -16
- package/dist/client.js.map +1 -1
- package/dist/commands/upload.d.ts.map +1 -1
- package/dist/commands/upload.js +4 -1
- package/dist/commands/upload.js.map +1 -1
- package/dist/contract.d.ts +0 -170
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -46
- package/dist/contract.js.map +1 -1
- package/dist/index.d.ts +3 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -7
- package/dist/index.js.map +1 -1
- package/dist/install-entry.d.ts +0 -1
- package/dist/install-entry.d.ts.map +1 -1
- package/dist/install-entry.js +0 -1
- package/dist/install-entry.js.map +1 -1
- package/dist/install.d.ts +2 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +138 -34
- package/dist/install.js.map +1 -1
- package/dist/output.d.ts +6 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +17 -0
- package/dist/output.js.map +1 -1
- package/dist/pack.d.ts +49 -9
- package/dist/pack.d.ts.map +1 -1
- package/dist/pack.js +77 -77
- package/dist/pack.js.map +1 -1
- package/dist/project-walk.d.ts +18 -0
- package/dist/project-walk.d.ts.map +1 -0
- package/dist/project-walk.js +83 -0
- package/dist/project-walk.js.map +1 -0
- package/dist/schemas.d.ts +9 -8
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +3 -2
- package/dist/schemas.js.map +1 -1
- package/dist/v1/contract.d.ts +4 -4
- package/dist/v1/contract.d.ts.map +1 -1
- package/dist/v1/contract.js +6 -4
- package/dist/v1/contract.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +8 -28
- package/src/commands/upload.ts +3 -1
- package/src/contract.ts +0 -80
- package/src/index.ts +6 -10
- package/src/install-entry.ts +0 -1
- package/src/install.ts +199 -42
- package/src/output.ts +22 -0
- package/src/pack.ts +118 -94
- package/src/project-walk.ts +95 -0
- package/src/schemas.ts +11 -8
- package/src/v1/contract.ts +8 -6
package/dist/v1/contract.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/v1/contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAA;AACnC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAWvB,OAAO,EACL,yBAAyB,EACzB,oBAAoB,EACpB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/v1/contract.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -1,32 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import {
|
|
2
|
+
createClient as createV1Client,
|
|
3
|
+
type MarketV1Client,
|
|
4
|
+
type MarketV1ClientOptions,
|
|
5
|
+
} from './v1/client.js'
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export interface MarketClientOptions {
|
|
12
|
-
baseUrl?: string
|
|
13
|
-
fetch?: typeof globalThis.fetch
|
|
14
|
-
authToken?: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function buildHeaders(authToken?: string): Record<string, string> | undefined {
|
|
18
|
-
return authToken ? { authorization: `Bearer ${authToken}` } : undefined
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function createMarketClient(opts: MarketClientOptions = {}): MarketClient {
|
|
22
|
-
const baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL
|
|
23
|
-
const link = new RPCLink({
|
|
24
|
-
url: new URL('/api/rpc', baseUrl).href,
|
|
25
|
-
fetch: opts.fetch,
|
|
26
|
-
headers: buildHeaders(opts.authToken),
|
|
27
|
-
})
|
|
28
|
-
return createORPCClient<MarketClient>(link)
|
|
29
|
-
}
|
|
7
|
+
/** The Market client — the v1 REST client; the RPC wire format is gone. */
|
|
8
|
+
export type MarketClient = MarketV1Client
|
|
9
|
+
export type MarketClientOptions = MarketV1ClientOptions
|
|
30
10
|
|
|
31
11
|
/**
|
|
32
12
|
* A version's zip as bytes, fetched from the content-plane URL the v1 manifest mints. The stream
|
package/src/commands/upload.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
packPolicyFromInstallMetadata,
|
|
9
9
|
parsePackDependencies,
|
|
10
10
|
} from '../pack.js'
|
|
11
|
-
import { unchangedUploadResult, uploadResult } from '../output.js'
|
|
11
|
+
import { packWarnings, unchangedUploadResult, uploadResult } from '../output.js'
|
|
12
12
|
import {
|
|
13
13
|
assetDescriptionSchema,
|
|
14
14
|
assetNameSchema,
|
|
@@ -69,6 +69,8 @@ export async function uploadCommand(
|
|
|
69
69
|
fetchFileManifest: fileManifestForRange(client.asset),
|
|
70
70
|
})
|
|
71
71
|
const { zip, npmDependencies, assetDependencies, skillDependencies } = packed
|
|
72
|
+
// Loud fail-open: what pack could not verify ships in the zip — say so before publishing it.
|
|
73
|
+
for (const line of packWarnings(packed)) console.warn(line)
|
|
72
74
|
const profile = await client.user.getProfile()
|
|
73
75
|
if (!profile) {
|
|
74
76
|
throw new Error('Not logged in. Run `market login` first.')
|
package/src/contract.ts
CHANGED
|
@@ -1,23 +1,3 @@
|
|
|
1
|
-
import { oc } from '@orpc/contract'
|
|
2
|
-
import { z } from 'zod'
|
|
3
|
-
import type { AssetFileManifest } from './schemas.js'
|
|
4
|
-
import {
|
|
5
|
-
MAX_UPLOAD_ZIP_SIZE_BYTES,
|
|
6
|
-
agentRunStatusSchema,
|
|
7
|
-
agentStartSchema,
|
|
8
|
-
agentStatusSchema,
|
|
9
|
-
assetFilesSchema,
|
|
10
|
-
exactAssetSchema,
|
|
11
|
-
fileManifestSchema,
|
|
12
|
-
generateAssetSchema,
|
|
13
|
-
generateJobStatusSchema,
|
|
14
|
-
generateResponseSchema,
|
|
15
|
-
generateStatusInputSchema,
|
|
16
|
-
listAssetsSchema,
|
|
17
|
-
updateProfileSchema,
|
|
18
|
-
uploadZipSchema,
|
|
19
|
-
} from './schemas.js'
|
|
20
|
-
|
|
21
1
|
export interface AssetVersion {
|
|
22
2
|
id: string
|
|
23
3
|
assetId: string
|
|
@@ -128,63 +108,3 @@ export interface AssetInstallMetadata {
|
|
|
128
108
|
readAssetDependenciesFromPackageJson: boolean
|
|
129
109
|
omitUnchangedInstalledFilesOnUpload: boolean
|
|
130
110
|
}
|
|
131
|
-
|
|
132
|
-
export const contract = {
|
|
133
|
-
asset: {
|
|
134
|
-
search: oc.input(listAssetsSchema).output(z.custom<PaginatedList<AssetSearchResult>>()),
|
|
135
|
-
|
|
136
|
-
exact: oc.input(exactAssetSchema).output(z.custom<AssetExactResult | null>()),
|
|
137
|
-
|
|
138
|
-
uploadZip: oc
|
|
139
|
-
.input(
|
|
140
|
-
uploadZipSchema
|
|
141
|
-
.extend({ zip: z.instanceof(File) })
|
|
142
|
-
.refine((input) => input.zip.size < MAX_UPLOAD_ZIP_SIZE_BYTES, {
|
|
143
|
-
message: 'Upload zip must be smaller than 1 GB',
|
|
144
|
-
path: ['zip'],
|
|
145
|
-
}),
|
|
146
|
-
)
|
|
147
|
-
.output(z.custom<AssetVersion>()),
|
|
148
|
-
|
|
149
|
-
// The derived file index of a version — every stored file's path, size and md5 etag plus the
|
|
150
|
-
// fetch-ready data-plane URLs, so installs fetch files in parallel through the same edge cache
|
|
151
|
-
// browsers warm.
|
|
152
|
-
files: oc.input(assetFilesSchema).output(z.custom<AssetFilesResult>()),
|
|
153
|
-
|
|
154
|
-
// The content hashes of a version's files, keyed by install path. Computed once from the stored
|
|
155
|
-
// source zip and cached, so a caller (e.g. `pack`) can tell whether a local file is an unchanged
|
|
156
|
-
// copy of an installed asset without keeping any hashes itself. Private assets are owner-only,
|
|
157
|
-
// same as download.
|
|
158
|
-
fileManifest: oc.input(fileManifestSchema).output(z.custom<AssetFileManifest>()),
|
|
159
|
-
|
|
160
|
-
// Generation answers immediately: `completed` for fast providers, or `pending` with an
|
|
161
|
-
// unguessable Market Workflow id for slow providers. The Workflow polls and finalizes the
|
|
162
|
-
// upstream job even when the caller disconnects; `generateStatus` only reads its state.
|
|
163
|
-
generate: oc.input(generateAssetSchema).output(generateResponseSchema),
|
|
164
|
-
|
|
165
|
-
generateStatus: oc.input(generateStatusInputSchema).output(generateJobStatusSchema),
|
|
166
|
-
|
|
167
|
-
installMetadata: oc.output(z.custom<Record<string, AssetInstallMetadata>>()),
|
|
168
|
-
|
|
169
|
-
// The agent plans, searches, judges and (where supported) generates a set of
|
|
170
|
-
// assets from a goal. `start` kicks off an async run; `status` long-polls it
|
|
171
|
-
// (blocks until the run settles or a heartbeat window elapses). The
|
|
172
|
-
// `agentAndWait` client helper drives both.
|
|
173
|
-
agent: {
|
|
174
|
-
start: oc.input(agentStartSchema).output(z.object({ runId: z.string() })),
|
|
175
|
-
status: oc.input(agentStatusSchema).output(agentRunStatusSchema),
|
|
176
|
-
},
|
|
177
|
-
},
|
|
178
|
-
|
|
179
|
-
user: {
|
|
180
|
-
getProfile: oc.output(z.custom<User | null>()),
|
|
181
|
-
|
|
182
|
-
updateProfile: oc.input(updateProfileSchema).output(z.custom<User>()),
|
|
183
|
-
|
|
184
|
-
getAuthToken: oc.output(z.custom<{ prefix: string; createdAt: Date } | null>()),
|
|
185
|
-
|
|
186
|
-
createAuthToken: oc.output(z.object({ token: z.string(), prefix: z.string() })),
|
|
187
|
-
},
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export type AppContract = typeof contract
|
package/src/index.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
// The v1 REST surface (`/api/v1`): routed contract + typed client.
|
|
2
|
-
// client below is deprecated and retires together with `/api/rpc`.
|
|
1
|
+
// The v1 REST surface (`/api/v1`): routed contract + typed client.
|
|
3
2
|
export * as v1 from './v1/index.js'
|
|
4
3
|
|
|
5
|
-
// Client
|
|
6
|
-
export {
|
|
4
|
+
// Client (an alias of the v1 client)
|
|
5
|
+
export { downloadAssetZipBytes } from './client.js'
|
|
7
6
|
export type { MarketClient, MarketClientOptions } from './client.js'
|
|
8
7
|
|
|
9
|
-
// Contracts
|
|
10
|
-
export { contract } from './contract.js'
|
|
11
|
-
export type { AppContract } from './contract.js'
|
|
12
|
-
|
|
13
8
|
// Contract output types
|
|
14
9
|
export type {
|
|
15
10
|
Asset,
|
|
@@ -74,7 +69,7 @@ export {
|
|
|
74
69
|
skillDependenciesSchema,
|
|
75
70
|
uploadZipSchema,
|
|
76
71
|
exactAssetSchema,
|
|
77
|
-
|
|
72
|
+
assetVersionsSchema,
|
|
78
73
|
generateAssetSchema,
|
|
79
74
|
updateProfileSchema,
|
|
80
75
|
listAssetsSchema,
|
|
@@ -93,7 +88,8 @@ export type {
|
|
|
93
88
|
AssetAccess,
|
|
94
89
|
AssetDependencies,
|
|
95
90
|
AssetDependencyValue,
|
|
96
|
-
|
|
91
|
+
AssetVersionListing,
|
|
92
|
+
AssetVersionsResult,
|
|
97
93
|
AgentResultAsset,
|
|
98
94
|
AgentResult,
|
|
99
95
|
AgentRunStatus,
|
package/src/install-entry.ts
CHANGED
package/src/install.ts
CHANGED
|
@@ -11,15 +11,17 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { execFile } from 'child_process'
|
|
14
|
-
import { createHash } from 'crypto'
|
|
15
14
|
import * as fs from 'fs/promises'
|
|
16
15
|
import * as path from 'path'
|
|
17
16
|
import { detectPackageManager, installDependencies } from 'nypm'
|
|
18
17
|
import type { MarketClient } from './client.js'
|
|
19
18
|
import type { AssetFilesResult, AssetInstallMetadata } from './contract.js'
|
|
20
19
|
import { withProjectInstallLock } from './install-lock.js'
|
|
20
|
+
import { md5, resolveDependencyLayout } from './pack.js'
|
|
21
|
+
import { findInstallRoot, walkWithGitignore } from './project-walk.js'
|
|
21
22
|
import { packageJsonAssetDependencies, parsePackageJson, type PackageJson } from './package-json.js'
|
|
22
23
|
import type { ResolveResult } from './resolve.js'
|
|
24
|
+
export { findInstallRoot }
|
|
23
25
|
import { assetDependencyAlias, assetDependencyRange, assetDependencyValue } from './schemas.js'
|
|
24
26
|
|
|
25
27
|
export interface InstallOptions {
|
|
@@ -72,6 +74,8 @@ interface DownloadResult {
|
|
|
72
74
|
assets: InstalledAsset[]
|
|
73
75
|
wrotePackageJson: boolean
|
|
74
76
|
warnings: string[]
|
|
77
|
+
/** Renames adopted during this install (per asset: canonical path → adopted project path). */
|
|
78
|
+
adoptedAliases: Record<string, Record<string, string>>
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
interface DownloadOptions {
|
|
@@ -80,6 +84,14 @@ interface DownloadOptions {
|
|
|
80
84
|
key?: string
|
|
81
85
|
/** File aliases per asset name: canonical install path → the project's current path for it. */
|
|
82
86
|
aliases: Record<string, Record<string, string>>
|
|
87
|
+
/** md5 of every project file, computed at most once per install (rename adoption). */
|
|
88
|
+
projectHashes: () => Promise<Map<string, string>>
|
|
89
|
+
/**
|
|
90
|
+
* Etags across an asset's published versions — the "recognized bytes" set for content-aware
|
|
91
|
+
* updates. `null` when the server cannot answer: fail-open in the conservative direction
|
|
92
|
+
* (never overwrite on uncertainty).
|
|
93
|
+
*/
|
|
94
|
+
knownHashes: (name: string) => Promise<Set<string> | null>
|
|
83
95
|
}
|
|
84
96
|
|
|
85
97
|
export async function install(
|
|
@@ -97,6 +109,8 @@ export async function install(
|
|
|
97
109
|
log,
|
|
98
110
|
key: opts.key,
|
|
99
111
|
aliases,
|
|
112
|
+
projectHashes: memoizedProjectHashes(installRoot),
|
|
113
|
+
knownHashes: memoizedKnownHashes(client),
|
|
100
114
|
})
|
|
101
115
|
const integration = await withProjectInstallLock(
|
|
102
116
|
installRoot,
|
|
@@ -105,6 +119,7 @@ export async function install(
|
|
|
105
119
|
rootRequests: opts.rootRequests ?? [],
|
|
106
120
|
installMetadata: metadata,
|
|
107
121
|
packageManagerNeeded: download.wrotePackageJson,
|
|
122
|
+
adoptedAliases: download.adoptedAliases,
|
|
108
123
|
})
|
|
109
124
|
if (packageJsonUpdate.packageManagerNeeded) {
|
|
110
125
|
await (opts.runPackageManagerInstall ?? runPackageManagerInstall)(installRoot, log)
|
|
@@ -137,19 +152,6 @@ export async function install(
|
|
|
137
152
|
}
|
|
138
153
|
}
|
|
139
154
|
|
|
140
|
-
export async function findInstallRoot(cwd: string = process.cwd()): Promise<string> {
|
|
141
|
-
const start = path.resolve(cwd)
|
|
142
|
-
|
|
143
|
-
for (let dir = start; ; dir = path.dirname(dir)) {
|
|
144
|
-
if (await isFile(path.join(dir, 'package.json'))) {
|
|
145
|
-
return dir
|
|
146
|
-
}
|
|
147
|
-
if (path.dirname(dir) === dir) {
|
|
148
|
-
return start
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
155
|
/** Max asset downloads in flight at once. Assets write disjoint file trees, so downloading them
|
|
154
156
|
* concurrently is safe and turns a ~40-asset install from serial round-trips into a few batched
|
|
155
157
|
* waves. Kept modest so one install does not itself overwhelm the worker's per-request memory. */
|
|
@@ -175,6 +177,11 @@ async function downloadAssets(
|
|
|
175
177
|
// the package manager to bring its npm deps.
|
|
176
178
|
wrotePackageJson: perAsset.some((r) => r.wrotePackageJson),
|
|
177
179
|
warnings: perAsset.flatMap((r) => r.warnings),
|
|
180
|
+
adoptedAliases: Object.fromEntries(
|
|
181
|
+
perAsset
|
|
182
|
+
.filter((r) => Object.keys(r.adopted).length > 0)
|
|
183
|
+
.map((r) => [r.asset.name, r.adopted]),
|
|
184
|
+
),
|
|
178
185
|
}
|
|
179
186
|
}
|
|
180
187
|
|
|
@@ -183,53 +190,78 @@ async function downloadOneAsset(
|
|
|
183
190
|
asset: ResolveResult['assets'][number],
|
|
184
191
|
projectRoot: string,
|
|
185
192
|
opts: DownloadOptions,
|
|
186
|
-
): Promise<{
|
|
193
|
+
): Promise<{
|
|
194
|
+
asset: InstalledAsset
|
|
195
|
+
wrotePackageJson: boolean
|
|
196
|
+
warnings: string[]
|
|
197
|
+
adopted: Record<string, string>
|
|
198
|
+
}> {
|
|
187
199
|
opts.log(`Downloading ${asset.name}@${asset.version}...`)
|
|
188
200
|
|
|
189
201
|
// Only the network fetches are retried — downloads are the sole transient step; file writes are
|
|
190
202
|
// deterministic local work and a failure there is a real error, not worth re-attempting.
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
const installedFiles: string[] = []
|
|
194
|
-
const warnings: string[] = []
|
|
195
|
-
let wrotePackageJson = false
|
|
196
|
-
|
|
197
|
-
for (const [relativePath, content] of files) {
|
|
203
|
+
const entries = await fetchAssetEntries(client, asset, opts)
|
|
204
|
+
const planned = entries.flatMap((entry) => {
|
|
198
205
|
const normalizedPath = safeRelativePath(
|
|
199
|
-
|
|
200
|
-
() => `Zip contains an unsafe path: ${
|
|
206
|
+
entry.path,
|
|
207
|
+
() => `Zip contains an unsafe path: ${entry.path}`,
|
|
201
208
|
)
|
|
202
209
|
if (
|
|
203
210
|
normalizedPath === '.' ||
|
|
204
211
|
(normalizedPath === 'README.md' && asset.type !== 'template') ||
|
|
205
212
|
normalizedPath.endsWith('/')
|
|
206
213
|
) {
|
|
207
|
-
|
|
214
|
+
return []
|
|
208
215
|
}
|
|
216
|
+
return [{ ...entry, normalizedPath }]
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// Alias targets come from metadata; reject traversal before any path is stat'd or resolved —
|
|
220
|
+
// adoption must never quietly route around unsafe metadata.
|
|
221
|
+
const declaredAlias = opts.aliases[asset.name] ?? {}
|
|
222
|
+
for (const [canonical, target] of Object.entries(declaredAlias)) {
|
|
223
|
+
safeRelativePath(
|
|
224
|
+
target,
|
|
225
|
+
() => `Alias for ${canonical} in ${asset.name} is an unsafe path: ${target}`,
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
const adopted = await adoptRenamedFiles(planned, asset.name, declaredAlias, projectRoot, opts)
|
|
229
|
+
const alias = { ...declaredAlias, ...adopted }
|
|
230
|
+
|
|
231
|
+
const installedFiles: string[] = []
|
|
232
|
+
const warnings: string[] = []
|
|
233
|
+
let wrotePackageJson = false
|
|
209
234
|
|
|
210
|
-
|
|
211
|
-
//
|
|
212
|
-
|
|
235
|
+
for (const entry of planned) {
|
|
236
|
+
// An alias redirects the file to where this project keeps it (a rename recorded by pack, a
|
|
237
|
+
// dependent asset, or adoption above). Alias targets pass the same traversal guard.
|
|
238
|
+
const aliased = alias[entry.normalizedPath]
|
|
213
239
|
const targetPath =
|
|
214
240
|
aliased === undefined
|
|
215
|
-
? normalizedPath
|
|
241
|
+
? entry.normalizedPath
|
|
216
242
|
: safeRelativePath(
|
|
217
243
|
aliased,
|
|
218
|
-
() =>
|
|
244
|
+
() =>
|
|
245
|
+
`Alias for ${entry.normalizedPath} in ${asset.name} is an unsafe path: ${aliased}`,
|
|
219
246
|
)
|
|
220
247
|
|
|
221
248
|
const filePath = path.join(projectRoot, targetPath)
|
|
222
249
|
const existing = await maybeReadFile(filePath)
|
|
223
|
-
if (existing && !bytesEqual(existing,
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
)
|
|
227
|
-
|
|
250
|
+
if (existing && !bytesEqual(existing, entry.bytes)) {
|
|
251
|
+
// Recognized bytes — a published version of this same dependency file — are replaceable
|
|
252
|
+
// without --force; unrecognized bytes are the user's and keep the skip-and-warn behavior.
|
|
253
|
+
const known = opts.force ? null : await opts.knownHashes(asset.name)
|
|
254
|
+
if (!opts.force && !known?.has(md5(existing))) {
|
|
255
|
+
warnings.push(
|
|
256
|
+
`Skipped ${targetPath} from ${asset.name}@${asset.version}; file already exists. Re-run with --force to overwrite.`,
|
|
257
|
+
)
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
228
260
|
}
|
|
229
261
|
|
|
230
|
-
if (!existing || !bytesEqual(existing,
|
|
262
|
+
if (!existing || !bytesEqual(existing, entry.bytes)) {
|
|
231
263
|
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
232
|
-
await fs.writeFile(filePath,
|
|
264
|
+
await fs.writeFile(filePath, entry.bytes)
|
|
233
265
|
if (targetPath === 'package.json') {
|
|
234
266
|
wrotePackageJson = true
|
|
235
267
|
}
|
|
@@ -249,6 +281,103 @@ async function downloadOneAsset(
|
|
|
249
281
|
},
|
|
250
282
|
wrotePackageJson,
|
|
251
283
|
warnings,
|
|
284
|
+
adopted,
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
interface PlannedEntry {
|
|
289
|
+
normalizedPath: string
|
|
290
|
+
etag: string
|
|
291
|
+
bytes: Uint8Array
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Install-time adoption of unsynced renames: when a file's write target is absent but identical
|
|
296
|
+
* bytes already live elsewhere in the project, that location is used instead of creating a
|
|
297
|
+
* duplicate — an implicit `sync` before writing. Uses the same layout resolution as pack/sync
|
|
298
|
+
* (recorded alias → canonical → first match), so the selection rule stays identical everywhere.
|
|
299
|
+
* The project is only hashed when some target is actually absent; a reinstall with every file in
|
|
300
|
+
* place never scans.
|
|
301
|
+
*/
|
|
302
|
+
async function adoptRenamedFiles(
|
|
303
|
+
planned: PlannedEntry[],
|
|
304
|
+
name: string,
|
|
305
|
+
declaredAlias: Record<string, string>,
|
|
306
|
+
projectRoot: string,
|
|
307
|
+
opts: DownloadOptions,
|
|
308
|
+
): Promise<Record<string, string>> {
|
|
309
|
+
const provisionalTargets = planned.map(
|
|
310
|
+
(entry) => declaredAlias[entry.normalizedPath] ?? entry.normalizedPath,
|
|
311
|
+
)
|
|
312
|
+
if (!(await anyPathMissing(projectRoot, provisionalTargets))) return {}
|
|
313
|
+
|
|
314
|
+
const layout = resolveDependencyLayout(
|
|
315
|
+
{ [name]: assetDependencyValue('*', declaredAlias) },
|
|
316
|
+
{
|
|
317
|
+
files: planned.map((entry) => ({
|
|
318
|
+
name,
|
|
319
|
+
canonicalPath: entry.normalizedPath,
|
|
320
|
+
hash: entry.etag,
|
|
321
|
+
})),
|
|
322
|
+
fetched: new Set([name]),
|
|
323
|
+
},
|
|
324
|
+
await opts.projectHashes(),
|
|
325
|
+
)
|
|
326
|
+
const resolved = assetDependencyAlias(layout.assetDependencies[name])
|
|
327
|
+
return Object.fromEntries(
|
|
328
|
+
Object.entries(resolved).filter(([canonical, target]) => declaredAlias[canonical] !== target),
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function anyPathMissing(projectRoot: string, targets: string[]): Promise<boolean> {
|
|
333
|
+
for (const target of targets) {
|
|
334
|
+
if (!(await isFile(path.join(projectRoot, target)))) return true
|
|
335
|
+
}
|
|
336
|
+
return false
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** md5 of every project file (gitignored trees excluded), computed at most once per install. */
|
|
340
|
+
function memoizedProjectHashes(projectRoot: string): () => Promise<Map<string, string>> {
|
|
341
|
+
let cached: Promise<Map<string, string>> | undefined
|
|
342
|
+
return () =>
|
|
343
|
+
(cached ??= (async () => {
|
|
344
|
+
const walk = await walkWithGitignore(projectRoot)
|
|
345
|
+
const hashes = new Map<string, string>()
|
|
346
|
+
for (const relative of walk.kept) {
|
|
347
|
+
hashes.set(
|
|
348
|
+
relative,
|
|
349
|
+
md5(new Uint8Array(await fs.readFile(path.join(projectRoot, relative)))),
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
return hashes
|
|
353
|
+
})())
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Newest published versions contributing to the recognized-bytes set (same bound as pack). */
|
|
357
|
+
const KNOWN_HASH_VERSION_CAP = 20
|
|
358
|
+
|
|
359
|
+
function memoizedKnownHashes(client: MarketClient): (name: string) => Promise<Set<string> | null> {
|
|
360
|
+
const cache = new Map<string, Promise<Set<string> | null>>()
|
|
361
|
+
return (name) => {
|
|
362
|
+
const hit = cache.get(name)
|
|
363
|
+
if (hit) return hit
|
|
364
|
+
const result = (async () => {
|
|
365
|
+
try {
|
|
366
|
+
const { versions } = await client.asset.versions({ name })
|
|
367
|
+
const recent = versions.map((entry) => entry.version).slice(-KNOWN_HASH_VERSION_CAP)
|
|
368
|
+
const etags = await Promise.all(
|
|
369
|
+
recent.map(async (version) =>
|
|
370
|
+
(await client.asset.files({ name, version })).files.map((file) => file.etag),
|
|
371
|
+
),
|
|
372
|
+
)
|
|
373
|
+
return new Set(etags.flat())
|
|
374
|
+
} catch {
|
|
375
|
+
// Offline or not visible — the conservative fail-open: the caller keeps skip-and-warn.
|
|
376
|
+
return null
|
|
377
|
+
}
|
|
378
|
+
})()
|
|
379
|
+
cache.set(name, result)
|
|
380
|
+
return result
|
|
252
381
|
}
|
|
253
382
|
}
|
|
254
383
|
|
|
@@ -304,7 +433,7 @@ async function fetchAssetEntries(
|
|
|
304
433
|
client: MarketClient,
|
|
305
434
|
asset: ResolveResult['assets'][number],
|
|
306
435
|
opts: DownloadOptions,
|
|
307
|
-
): Promise<Array<
|
|
436
|
+
): Promise<Array<{ path: string; etag: string; bytes: Uint8Array }>> {
|
|
308
437
|
const index = await client.asset.files({
|
|
309
438
|
name: asset.name,
|
|
310
439
|
version: asset.version,
|
|
@@ -317,7 +446,7 @@ async function fetchAssetEntries(
|
|
|
317
446
|
attempts: DOWNLOAD_ATTEMPTS,
|
|
318
447
|
onRetry: (msg) => opts.log(`Downloading ${label} ${file.path}: ${msg}`),
|
|
319
448
|
})
|
|
320
|
-
return
|
|
449
|
+
return { path: file.path, etag: file.etag, bytes }
|
|
321
450
|
})
|
|
322
451
|
}
|
|
323
452
|
|
|
@@ -329,9 +458,9 @@ async function fetchVerifiedFile(file: AssetFilesResult['files'][number]): Promi
|
|
|
329
458
|
const bytes = new Uint8Array(await res.arrayBuffer())
|
|
330
459
|
// A digest mismatch is a corrupt transfer; thrown without a status so withRetry treats it as
|
|
331
460
|
// transient and re-fetches.
|
|
332
|
-
const
|
|
333
|
-
if (
|
|
334
|
-
throw new Error(`md5 mismatch for ${file.path}: expected ${file.etag}, got ${
|
|
461
|
+
const digest = md5(bytes)
|
|
462
|
+
if (digest !== file.etag) {
|
|
463
|
+
throw new Error(`md5 mismatch for ${file.path}: expected ${file.etag}, got ${digest}`)
|
|
335
464
|
}
|
|
336
465
|
return bytes
|
|
337
466
|
}
|
|
@@ -424,6 +553,7 @@ async function updatePackageJson(
|
|
|
424
553
|
rootRequests: InstallRootRequest[]
|
|
425
554
|
installMetadata: Record<string, AssetInstallMetadata>
|
|
426
555
|
packageManagerNeeded: boolean
|
|
556
|
+
adoptedAliases: Record<string, Record<string, string>>
|
|
427
557
|
},
|
|
428
558
|
): Promise<{ packageManagerNeeded: boolean }> {
|
|
429
559
|
const pkgPath = path.join(projectRoot, 'package.json')
|
|
@@ -448,6 +578,9 @@ async function updatePackageJson(
|
|
|
448
578
|
if (mergeAssetDependencyRanges(pkg, assetDependencies)) {
|
|
449
579
|
changed = true
|
|
450
580
|
}
|
|
581
|
+
if (mergeAdoptedAliases(pkg, opts.adoptedAliases)) {
|
|
582
|
+
changed = true
|
|
583
|
+
}
|
|
451
584
|
|
|
452
585
|
if (changed) {
|
|
453
586
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
@@ -456,6 +589,30 @@ async function updatePackageJson(
|
|
|
456
589
|
return { packageManagerNeeded }
|
|
457
590
|
}
|
|
458
591
|
|
|
592
|
+
/** Record renames adopted during this install on the entries package.json already declares —
|
|
593
|
+
* adoption is thereby persisted, not just applied. Undeclared (transitive) deps stay out. */
|
|
594
|
+
function mergeAdoptedAliases(
|
|
595
|
+
pkg: PackageJson,
|
|
596
|
+
adopted: Record<string, Record<string, string>>,
|
|
597
|
+
): boolean {
|
|
598
|
+
const current = pkg.assetDependencies
|
|
599
|
+
if (!current) return false
|
|
600
|
+
|
|
601
|
+
let changed = false
|
|
602
|
+
for (const [name, alias] of Object.entries(adopted)) {
|
|
603
|
+
const existing = current[name]
|
|
604
|
+
if (existing === undefined) continue
|
|
605
|
+
const next = assetDependencyValue(assetDependencyRange(existing), {
|
|
606
|
+
...assetDependencyAlias(existing),
|
|
607
|
+
...alias,
|
|
608
|
+
})
|
|
609
|
+
if (JSON.stringify(existing) === JSON.stringify(next)) continue
|
|
610
|
+
current[name] = next
|
|
611
|
+
changed = true
|
|
612
|
+
}
|
|
613
|
+
return changed
|
|
614
|
+
}
|
|
615
|
+
|
|
459
616
|
async function runPackageManagerInstall(projectRoot: string, log: (msg: string) => void) {
|
|
460
617
|
log('Installing npm dependencies...')
|
|
461
618
|
const pm = await detectPackageManager(projectRoot)
|
package/src/output.ts
CHANGED
|
@@ -219,12 +219,34 @@ export function packResult(out: string, packed: PackedAsset): string {
|
|
|
219
219
|
if (packed.omittedUnchangedInstalledFiles) {
|
|
220
220
|
lines.push('Omitted unchanged installed dependency files.')
|
|
221
221
|
}
|
|
222
|
+
lines.push(...packWarnings(packed))
|
|
222
223
|
lines.push(...dependencyLines('npm dependencies', packed.npmDependencies))
|
|
223
224
|
lines.push(...dependencyLines('asset dependencies', packed.assetDependencies))
|
|
224
225
|
lines.push(...dependencyLines('skill dependencies', packed.skillDependencies))
|
|
225
226
|
return lines.join('\n')
|
|
226
227
|
}
|
|
227
228
|
|
|
229
|
+
/**
|
|
230
|
+
* What pack could not check — the silent-fail-open cases with real consequences: an unfetchable
|
|
231
|
+
* dependency ships its files inside the zip, and a missing dependency file means a local edit or
|
|
232
|
+
* deletion the packer should know about.
|
|
233
|
+
*/
|
|
234
|
+
export function packWarnings(packed: PackedAsset): string[] {
|
|
235
|
+
const lines: string[] = []
|
|
236
|
+
if (packed.skippedDependencies.length > 0) {
|
|
237
|
+
lines.push(
|
|
238
|
+
`Dependency file indexes unavailable (their files ship in the zip): ${[...packed.skippedDependencies].sort().join(', ')}`,
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
if (packed.missingDependencyFiles.length > 0) {
|
|
242
|
+
lines.push('Dependency files not found (locally modified or deleted):')
|
|
243
|
+
lines.push(
|
|
244
|
+
...packed.missingDependencyFiles.map((file) => `- ${file.name}: ${file.path}`).sort(),
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
return lines
|
|
248
|
+
}
|
|
249
|
+
|
|
228
250
|
export function previewResult(name: string, version: string, out: string): string {
|
|
229
251
|
return `Saved preview for ${assetVersionRef(name, version)}: ${out}`
|
|
230
252
|
}
|