@drawcall/market 0.1.34 → 0.1.36
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 +2 -2
- package/dist/asset-implementation.d.ts +10 -7
- package/dist/asset-implementation.d.ts.map +1 -1
- package/dist/asset-implementation.js +8 -1
- package/dist/asset-implementation.js.map +1 -1
- package/dist/cli.js +4 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/install.d.ts +3 -4
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +57 -11
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/upload.d.ts.map +1 -1
- package/dist/commands/upload.js +64 -2
- package/dist/commands/upload.js.map +1 -1
- package/dist/contract.d.ts +7 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +14 -0
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +133 -23
- package/dist/install.js.map +1 -1
- package/dist/market-lock.d.ts +17 -0
- package/dist/market-lock.d.ts.map +1 -0
- package/dist/market-lock.js +44 -0
- package/dist/market-lock.js.map +1 -0
- package/dist/output.d.ts +2 -2
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +5 -2
- package/dist/output.js.map +1 -1
- package/dist/package-json.d.ts +11 -0
- package/dist/package-json.d.ts.map +1 -0
- package/dist/package-json.js +23 -0
- package/dist/package-json.js.map +1 -0
- package/dist/skill.d.ts +1 -1
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js +5 -3
- package/dist/skill.js.map +1 -1
- package/package.json +1 -1
- package/src/asset-implementation.ts +18 -6
- package/src/cli.ts +16 -9
- package/src/commands/install.ts +76 -16
- package/src/commands/upload.ts +84 -2
- package/src/contract.ts +8 -1
- package/src/index.ts +9 -0
- package/src/install.ts +197 -35
- package/src/market-lock.ts +65 -0
- package/src/output.ts +7 -3
- package/src/package-json.ts +37 -0
- package/src/skill.ts +5 -3
- package/tests/install-command.test.ts +23 -1
- package/tests/install-layout.test.ts +117 -0
- package/tests/output.test.ts +19 -2
package/src/commands/install.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import * as fs from 'fs/promises'
|
|
2
|
+
import * as path from 'path'
|
|
1
3
|
import ora from 'ora'
|
|
2
4
|
import { resolve } from '../resolve.js'
|
|
3
|
-
import { install as runInstall } from '../install.js'
|
|
5
|
+
import { findInstallRoot, install as runInstall, type InstallRootRequest } from '../install.js'
|
|
6
|
+
import { packageJsonAssetDependencies } from '../package-json.js'
|
|
4
7
|
import { assetNameSchema } from '../schemas.js'
|
|
5
8
|
import { getCliClient } from '../cli-client.js'
|
|
6
9
|
import { installResult } from '../output.js'
|
|
@@ -10,42 +13,41 @@ export interface InstallCommandOptions {
|
|
|
10
13
|
unapproved?: boolean
|
|
11
14
|
cwd?: string
|
|
12
15
|
baseUrl?: string
|
|
16
|
+
force?: boolean
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
name: string
|
|
17
|
-
range: string
|
|
18
|
-
}
|
|
19
|
+
type AssetRequest = InstallRootRequest
|
|
19
20
|
|
|
20
21
|
export async function installCommand(args: string[], opts: InstallCommandOptions): Promise<void> {
|
|
21
22
|
const { client } = await getCliClient({ baseUrl: opts.baseUrl })
|
|
22
23
|
const includeUnapproved = opts.unapproved ?? false
|
|
24
|
+
const metadata = await client.asset.installMetadata()
|
|
23
25
|
|
|
24
26
|
const spinner = ora({
|
|
25
27
|
isEnabled: Boolean(process.stderr.isTTY),
|
|
26
28
|
isSilent: !process.stderr.isTTY,
|
|
27
29
|
}).start()
|
|
28
30
|
try {
|
|
29
|
-
const requests
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
31
|
+
const requests =
|
|
32
|
+
args.length > 0
|
|
33
|
+
? await resolveArgs(client, args, includeUnapproved, spinner)
|
|
34
|
+
: await packageJsonRequests(opts.cwd)
|
|
34
35
|
|
|
35
36
|
spinner.text = 'Resolving dependency tree'
|
|
36
37
|
const resolution = await resolve(client.asset, requests, { includeUnapproved })
|
|
37
38
|
|
|
38
39
|
const result = await runInstall(client, resolution, {
|
|
39
40
|
cwd: opts.cwd,
|
|
41
|
+
force: opts.force ?? false,
|
|
42
|
+
rootRequests: requests,
|
|
43
|
+
installMetadata: metadata,
|
|
40
44
|
onProgress: (msg) => {
|
|
41
45
|
spinner.text = msg
|
|
42
46
|
},
|
|
43
47
|
})
|
|
44
48
|
|
|
45
|
-
const messages = await client.asset.postInstallMessages()
|
|
46
|
-
|
|
47
49
|
spinner.stop()
|
|
48
|
-
console.log(installResult(result,
|
|
50
|
+
console.log(installResult(result, metadata))
|
|
49
51
|
} catch (err) {
|
|
50
52
|
spinner.stop()
|
|
51
53
|
throw err
|
|
@@ -72,7 +74,14 @@ export async function resolveArg(
|
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
const existing = await client.asset.exact({ name: parsed.name, includeUnapproved })
|
|
75
|
-
if (existing)
|
|
77
|
+
if (existing) {
|
|
78
|
+
return {
|
|
79
|
+
name: parsed.name,
|
|
80
|
+
range: parsed.range,
|
|
81
|
+
saveRange: parsed.explicitRange ? parsed.range : `^${existing.latestVersion}`,
|
|
82
|
+
save: true,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
76
85
|
|
|
77
86
|
if (!includeUnapproved) {
|
|
78
87
|
const hidden = await client.asset.exact({ name: parsed.name, includeUnapproved: true })
|
|
@@ -89,11 +98,62 @@ export async function resolveArg(
|
|
|
89
98
|
)
|
|
90
99
|
}
|
|
91
100
|
|
|
92
|
-
function
|
|
101
|
+
async function resolveArgs(
|
|
102
|
+
client: MarketClient,
|
|
103
|
+
args: string[],
|
|
104
|
+
includeUnapproved: boolean,
|
|
105
|
+
spinner: { text: string },
|
|
106
|
+
): Promise<AssetRequest[]> {
|
|
107
|
+
const requests: AssetRequest[] = []
|
|
108
|
+
for (const arg of args) {
|
|
109
|
+
spinner.text = `Resolving "${truncate(arg, 40)}"`
|
|
110
|
+
requests.push(await resolveArg(client, arg, includeUnapproved))
|
|
111
|
+
}
|
|
112
|
+
return requests
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function packageJsonRequests(cwd: string | undefined): Promise<AssetRequest[]> {
|
|
116
|
+
const installRoot = await findInstallRoot(cwd ?? process.cwd())
|
|
117
|
+
const pkgPath = path.join(installRoot, 'package.json')
|
|
118
|
+
const dependencies = await readPackageJsonAssetDependencies(pkgPath)
|
|
119
|
+
const requests = Object.entries(dependencies).map(([name, range]) => ({
|
|
120
|
+
name,
|
|
121
|
+
range,
|
|
122
|
+
saveRange: range,
|
|
123
|
+
save: false,
|
|
124
|
+
}))
|
|
125
|
+
|
|
126
|
+
if (requests.length === 0) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
'No assets specified and package.json has no assetDependencies. Pass asset names or add assetDependencies.',
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return requests
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function readPackageJsonAssetDependencies(pkgPath: string): Promise<Record<string, string>> {
|
|
136
|
+
try {
|
|
137
|
+
return packageJsonAssetDependencies(await fs.readFile(pkgPath, 'utf-8'), pkgPath)
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (isMissingFile(error)) return {}
|
|
140
|
+
throw error
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isMissingFile(error: unknown): boolean {
|
|
145
|
+
return error instanceof Error && 'code' in error && error.code === 'ENOENT'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parseNameAndRange(arg: string):
|
|
149
|
+
| (Pick<AssetRequest, 'name' | 'range'> & {
|
|
150
|
+
explicitRange: boolean
|
|
151
|
+
})
|
|
152
|
+
| null {
|
|
93
153
|
const atIdx = arg.indexOf('@')
|
|
94
154
|
const name = atIdx >= 0 ? arg.slice(0, atIdx) : arg
|
|
95
155
|
const range = atIdx >= 0 ? arg.slice(atIdx + 1) : '*'
|
|
96
|
-
return assetNameSchema.safeParse(name).success ? { name, range } : null
|
|
156
|
+
return assetNameSchema.safeParse(name).success ? { name, range, explicitRange: atIdx >= 0 } : null
|
|
97
157
|
}
|
|
98
158
|
|
|
99
159
|
function truncate(s: string, max: number): string {
|
package/src/commands/upload.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import * as fs from 'fs/promises'
|
|
2
2
|
import * as path from 'path'
|
|
3
|
+
import { unzipSync, zipSync } from 'fflate'
|
|
3
4
|
import ora from 'ora'
|
|
4
5
|
import semver from 'semver'
|
|
5
6
|
import { getCliClient } from '../cli-client.js'
|
|
7
|
+
import { findInstallRoot } from '../install.js'
|
|
8
|
+
import { readMarketLock, sha256 } from '../market-lock.js'
|
|
9
|
+
import { packageJsonAssetDependenciesFromFiles } from '../package-json.js'
|
|
6
10
|
import { MAX_ASSET_DESCRIPTION_LENGTH, MAX_UPLOAD_ZIP_SIZE_BYTES } from '../schemas.js'
|
|
7
11
|
import { unchangedUploadResult, uploadResult } from '../output.js'
|
|
8
12
|
import {
|
|
@@ -43,7 +47,7 @@ export async function uploadCommand(
|
|
|
43
47
|
const parsedDescription = parseUploadDescription(description)
|
|
44
48
|
// Parse dep flags before any network work so a malformed spec fails fast.
|
|
45
49
|
const npmDependencies = parseVersionedDeps(opts.npm ?? [], 'npm')
|
|
46
|
-
const
|
|
50
|
+
const explicitAssetDependencies = parseVersionedDeps(opts.asset ?? [], 'asset')
|
|
47
51
|
const skillDependencies = parseSkillDeps(opts.skill ?? [])
|
|
48
52
|
const cwd = opts.cwd ?? process.cwd()
|
|
49
53
|
const type = opts.type
|
|
@@ -52,8 +56,21 @@ export async function uploadCommand(
|
|
|
52
56
|
if (zipStat.size >= MAX_UPLOAD_ZIP_SIZE_BYTES) {
|
|
53
57
|
throw new Error('Upload zip must be smaller than 1 GB')
|
|
54
58
|
}
|
|
55
|
-
const
|
|
59
|
+
const sourceZip = new Uint8Array(await fs.readFile(zipFile))
|
|
56
60
|
const { client } = await getCliClient({ baseUrl: opts.baseUrl, requireAuth: true })
|
|
61
|
+
const installMetadata = await client.asset.installMetadata()
|
|
62
|
+
const typeMetadata = installMetadata[type]
|
|
63
|
+
const sourceFiles = unzipSync(sourceZip)
|
|
64
|
+
const packageJsonAssetDependencies = typeMetadata?.readAssetDependenciesFromPackageJson
|
|
65
|
+
? packageJsonAssetDependenciesFromFiles(sourceFiles)
|
|
66
|
+
: {}
|
|
67
|
+
const assetDependencies = mergeAssetDependencies(
|
|
68
|
+
packageJsonAssetDependencies,
|
|
69
|
+
explicitAssetDependencies,
|
|
70
|
+
)
|
|
71
|
+
const zip = typeMetadata?.omitUnchangedInstalledFilesOnUpload
|
|
72
|
+
? await omitUnchangedInstalledFiles(sourceZip, sourceFiles, assetDependencies, cwd)
|
|
73
|
+
: sourceZip
|
|
57
74
|
const profile = await client.user.getProfile()
|
|
58
75
|
if (!profile) {
|
|
59
76
|
throw new Error('Not logged in. Run `market login` first.')
|
|
@@ -168,6 +185,71 @@ export function parseSkillDeps(specs: string[]): Record<string, string> {
|
|
|
168
185
|
return out
|
|
169
186
|
}
|
|
170
187
|
|
|
188
|
+
function mergeAssetDependencies(
|
|
189
|
+
fromPackageJson: Record<string, string>,
|
|
190
|
+
explicit: Record<string, string>,
|
|
191
|
+
): Record<string, string> {
|
|
192
|
+
const merged = { ...fromPackageJson }
|
|
193
|
+
for (const [name, range] of Object.entries(explicit)) {
|
|
194
|
+
if (name in merged && merged[name] !== range) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`Conflicting asset dependency "${name}": package.json has ${merged[name]}, --asset has ${range}`,
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
merged[name] = range
|
|
200
|
+
}
|
|
201
|
+
return merged
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function omitUnchangedInstalledFiles(
|
|
205
|
+
sourceZip: Uint8Array,
|
|
206
|
+
sourceFiles: Record<string, Uint8Array>,
|
|
207
|
+
assetDependencies: Record<string, string>,
|
|
208
|
+
cwd: string,
|
|
209
|
+
): Promise<Uint8Array> {
|
|
210
|
+
const dependencyNames = new Set(Object.keys(assetDependencies))
|
|
211
|
+
if (dependencyNames.size === 0) return sourceZip
|
|
212
|
+
|
|
213
|
+
const installRoot = await findInstallRoot(cwd)
|
|
214
|
+
const lock = await readMarketLock(installRoot)
|
|
215
|
+
const hashesByPath = new Map<string, string>()
|
|
216
|
+
|
|
217
|
+
for (const [name, asset] of Object.entries(lock.assets)) {
|
|
218
|
+
if (!dependencyNames.has(name)) continue
|
|
219
|
+
for (const [file, metadata] of Object.entries(asset.files)) {
|
|
220
|
+
hashesByPath.set(file, metadata.sha256)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (hashesByPath.size === 0) return sourceZip
|
|
225
|
+
|
|
226
|
+
let omitted = false
|
|
227
|
+
const filtered: Record<string, Uint8Array> = {}
|
|
228
|
+
for (const [file, content] of Object.entries(sourceFiles)) {
|
|
229
|
+
const normalizedPath = normalizedZipPath(file)
|
|
230
|
+
const lockedHash = normalizedPath ? hashesByPath.get(normalizedPath) : undefined
|
|
231
|
+
if (lockedHash && lockedHash === sha256(content)) {
|
|
232
|
+
omitted = true
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
filtered[file] = content
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return omitted ? zipSync(filtered) : sourceZip
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function normalizedZipPath(file: string): string | null {
|
|
242
|
+
const zipPath = file.replace(/\\/g, '/')
|
|
243
|
+
if (
|
|
244
|
+
zipPath.split('/').includes('..') ||
|
|
245
|
+
path.posix.isAbsolute(zipPath) ||
|
|
246
|
+
path.win32.isAbsolute(zipPath)
|
|
247
|
+
) {
|
|
248
|
+
return null
|
|
249
|
+
}
|
|
250
|
+
return path.posix.normalize(zipPath)
|
|
251
|
+
}
|
|
252
|
+
|
|
171
253
|
/** Compare a stored dependency JSON string against a freshly parsed record. */
|
|
172
254
|
function depsEqual(storedJson: string, next: Record<string, string>): boolean {
|
|
173
255
|
let stored: Record<string, string>
|
package/src/contract.ts
CHANGED
|
@@ -74,6 +74,13 @@ export interface GenerateAssetResult {
|
|
|
74
74
|
version: string
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
export interface AssetInstallMetadata {
|
|
78
|
+
installMessage?: string
|
|
79
|
+
saveOnInstall: boolean
|
|
80
|
+
readAssetDependenciesFromPackageJson: boolean
|
|
81
|
+
omitUnchangedInstalledFilesOnUpload: boolean
|
|
82
|
+
}
|
|
83
|
+
|
|
77
84
|
export const contract = {
|
|
78
85
|
asset: {
|
|
79
86
|
search: oc.input(listAssetsSchema).output(z.custom<PaginatedList<AssetSearchResult>>()),
|
|
@@ -95,7 +102,7 @@ export const contract = {
|
|
|
95
102
|
|
|
96
103
|
generate: oc.input(generateAssetSchema).output(z.custom<GenerateAssetResult>()),
|
|
97
104
|
|
|
98
|
-
|
|
105
|
+
installMetadata: oc.output(z.custom<Record<string, AssetInstallMetadata>>()),
|
|
99
106
|
},
|
|
100
107
|
|
|
101
108
|
user: {
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type { AppContract } from './contract.js'
|
|
|
9
9
|
// Contract output types
|
|
10
10
|
export type {
|
|
11
11
|
Asset,
|
|
12
|
+
AssetInstallMetadata,
|
|
12
13
|
AssetVersion,
|
|
13
14
|
AssetSearchResult,
|
|
14
15
|
PaginatedList,
|
|
@@ -25,6 +26,7 @@ export type {
|
|
|
25
26
|
AssetSearchInput,
|
|
26
27
|
AssetUploadZipInput,
|
|
27
28
|
} from './asset-implementation.js'
|
|
29
|
+
export { installMetadataForProvider } from './asset-implementation.js'
|
|
28
30
|
|
|
29
31
|
// Resolve
|
|
30
32
|
export { resolve, ResolutionError } from './resolve.js'
|
|
@@ -55,3 +57,10 @@ export type { AssetType } from './schemas.js'
|
|
|
55
57
|
|
|
56
58
|
// Constants
|
|
57
59
|
export { MAX_FILE_SIZE, ALLOWED_EXTENSIONS, ASSET_TYPE_LABELS } from './constants.js'
|
|
60
|
+
|
|
61
|
+
export {
|
|
62
|
+
packageJsonAssetDependencies,
|
|
63
|
+
packageJsonAssetDependenciesFromFiles,
|
|
64
|
+
parsePackageJson,
|
|
65
|
+
} from './package-json.js'
|
|
66
|
+
export type { PackageJson } from './package-json.js'
|
package/src/install.ts
CHANGED
|
@@ -16,18 +16,20 @@ import * as path from 'path'
|
|
|
16
16
|
import { unzipSync } from 'fflate'
|
|
17
17
|
import { detectPackageManager, installDependencies } from 'nypm'
|
|
18
18
|
import type { MarketClient } from './client.js'
|
|
19
|
+
import type { AssetInstallMetadata } from './contract.js'
|
|
20
|
+
import { readMarketLock, sha256, writeMarketLock } from './market-lock.js'
|
|
21
|
+
import { parsePackageJson, type PackageJson } from './package-json.js'
|
|
19
22
|
import type { ResolveResult } from './resolve.js'
|
|
20
23
|
|
|
21
|
-
interface PackageJson {
|
|
22
|
-
name?: string
|
|
23
|
-
private?: boolean
|
|
24
|
-
dependencies?: Record<string, string>
|
|
25
|
-
[key: string]: unknown
|
|
26
|
-
}
|
|
27
|
-
|
|
28
24
|
export interface InstallOptions {
|
|
29
25
|
/** Directory to start project root discovery from (default: cwd) */
|
|
30
26
|
cwd?: string
|
|
27
|
+
/** Overwrite existing files that differ from the asset zip. */
|
|
28
|
+
force?: boolean
|
|
29
|
+
/** Top-level assets requested by the user, used for package.json assetDependencies. */
|
|
30
|
+
rootRequests?: InstallRootRequest[]
|
|
31
|
+
/** Asset-type install policy from the Market API. */
|
|
32
|
+
installMetadata?: Record<string, AssetInstallMetadata>
|
|
31
33
|
/** Log progress */
|
|
32
34
|
onProgress?: (message: string) => void
|
|
33
35
|
/**
|
|
@@ -38,6 +40,13 @@ export interface InstallOptions {
|
|
|
38
40
|
runSkillAdd?: (source: string, cwd: string) => Promise<void>
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
export interface InstallRootRequest {
|
|
44
|
+
name: string
|
|
45
|
+
range: string
|
|
46
|
+
saveRange: string
|
|
47
|
+
save: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
41
50
|
export interface InstalledAsset {
|
|
42
51
|
name: string
|
|
43
52
|
type: string
|
|
@@ -51,6 +60,17 @@ export interface InstallResult {
|
|
|
51
60
|
npmDependencies: Record<string, string>
|
|
52
61
|
/** Installed skills, keyed by label, with the source passed to `skills add`. */
|
|
53
62
|
skillDependencies: Record<string, string>
|
|
63
|
+
warnings: string[]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface DownloadedAsset extends InstalledAsset {
|
|
67
|
+
fileHashes: Record<string, { sha256: string }>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface DownloadResult {
|
|
71
|
+
assets: DownloadedAsset[]
|
|
72
|
+
wrotePackageJson: boolean
|
|
73
|
+
warnings: string[]
|
|
54
74
|
}
|
|
55
75
|
|
|
56
76
|
export async function install(
|
|
@@ -60,11 +80,21 @@ export async function install(
|
|
|
60
80
|
): Promise<InstallResult> {
|
|
61
81
|
const log = opts.onProgress ?? (() => {})
|
|
62
82
|
const installRoot = await findInstallRoot(opts.cwd ?? process.cwd())
|
|
83
|
+
const metadata = opts.installMetadata ?? {}
|
|
63
84
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
85
|
+
const download = await downloadAssets(client, resolution, installRoot, {
|
|
86
|
+
force: opts.force ?? false,
|
|
87
|
+
log,
|
|
88
|
+
})
|
|
89
|
+
await writeInstalledAssetLock(installRoot, download.assets)
|
|
90
|
+
const packageJsonUpdate = await updatePackageJson(resolution, installRoot, {
|
|
91
|
+
rootRequests: opts.rootRequests ?? [],
|
|
92
|
+
installMetadata: metadata,
|
|
93
|
+
packageManagerNeeded: download.wrotePackageJson,
|
|
94
|
+
})
|
|
95
|
+
if (packageJsonUpdate.packageManagerNeeded) {
|
|
96
|
+
await runPackageManagerInstall(installRoot, log)
|
|
97
|
+
}
|
|
68
98
|
|
|
69
99
|
// Skills are installed after asset files land on disk: a skill source may be
|
|
70
100
|
// a local path pointing at a skill directory shipped inside an installed
|
|
@@ -77,9 +107,16 @@ export async function install(
|
|
|
77
107
|
)
|
|
78
108
|
|
|
79
109
|
return {
|
|
80
|
-
assets
|
|
110
|
+
assets: download.assets.map((asset) => ({
|
|
111
|
+
name: asset.name,
|
|
112
|
+
type: asset.type,
|
|
113
|
+
version: asset.version,
|
|
114
|
+
description: asset.description,
|
|
115
|
+
files: asset.files,
|
|
116
|
+
})),
|
|
81
117
|
npmDependencies: resolution.npmDependencies,
|
|
82
118
|
skillDependencies,
|
|
119
|
+
warnings: download.warnings,
|
|
83
120
|
}
|
|
84
121
|
}
|
|
85
122
|
|
|
@@ -100,16 +137,22 @@ async function downloadAssets(
|
|
|
100
137
|
client: MarketClient,
|
|
101
138
|
resolution: ResolveResult,
|
|
102
139
|
projectRoot: string,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
140
|
+
opts: {
|
|
141
|
+
force: boolean
|
|
142
|
+
log: (msg: string) => void
|
|
143
|
+
},
|
|
144
|
+
): Promise<DownloadResult> {
|
|
145
|
+
const installedAssets: DownloadedAsset[] = []
|
|
146
|
+
const warnings: string[] = []
|
|
147
|
+
let wrotePackageJson = false
|
|
106
148
|
|
|
107
149
|
for (const asset of resolution.assets) {
|
|
108
|
-
log(`Downloading ${asset.name}@${asset.version}...`)
|
|
150
|
+
opts.log(`Downloading ${asset.name}@${asset.version}...`)
|
|
109
151
|
|
|
110
152
|
const zip = await client.asset.downloadZip({ name: asset.name, version: asset.version })
|
|
111
153
|
const files = unzipSync(new Uint8Array(await zip.arrayBuffer()))
|
|
112
154
|
const installedFiles: string[] = []
|
|
155
|
+
const fileHashes: Record<string, { sha256: string }> = {}
|
|
113
156
|
|
|
114
157
|
for (const [relativePath, content] of Object.entries(files) as [string, Uint8Array][]) {
|
|
115
158
|
const zipPath = relativePath.replace(/\\/g, '/')
|
|
@@ -124,16 +167,31 @@ async function downloadAssets(
|
|
|
124
167
|
const normalizedPath = path.posix.normalize(zipPath)
|
|
125
168
|
if (
|
|
126
169
|
normalizedPath === '.' ||
|
|
127
|
-
normalizedPath === 'README.md' ||
|
|
170
|
+
(normalizedPath === 'README.md' && asset.type !== 'template') ||
|
|
128
171
|
normalizedPath.endsWith('/')
|
|
129
172
|
) {
|
|
130
173
|
continue
|
|
131
174
|
}
|
|
132
175
|
|
|
133
176
|
const filePath = path.join(projectRoot, normalizedPath)
|
|
134
|
-
await
|
|
135
|
-
|
|
177
|
+
const existing = await maybeReadFile(filePath)
|
|
178
|
+
if (existing && !bytesEqual(existing, content) && !opts.force) {
|
|
179
|
+
warnings.push(
|
|
180
|
+
`Skipped ${normalizedPath} from ${asset.name}@${asset.version}; file already exists. Re-run with --force to overwrite.`,
|
|
181
|
+
)
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!existing || !bytesEqual(existing, content)) {
|
|
186
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
187
|
+
await fs.writeFile(filePath, content)
|
|
188
|
+
if (normalizedPath === 'package.json') {
|
|
189
|
+
wrotePackageJson = true
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
136
193
|
installedFiles.push(normalizedPath)
|
|
194
|
+
fileHashes[normalizedPath] = { sha256: sha256(content) }
|
|
137
195
|
}
|
|
138
196
|
|
|
139
197
|
installedAssets.push({
|
|
@@ -142,12 +200,13 @@ async function downloadAssets(
|
|
|
142
200
|
version: asset.version,
|
|
143
201
|
description: asset.description,
|
|
144
202
|
files: installedFiles,
|
|
203
|
+
fileHashes,
|
|
145
204
|
})
|
|
146
205
|
|
|
147
|
-
log(`Downloaded ${installedFiles.length} files.`)
|
|
206
|
+
opts.log(`Downloaded ${installedFiles.length} files.`)
|
|
148
207
|
}
|
|
149
208
|
|
|
150
|
-
return installedAssets
|
|
209
|
+
return { assets: installedAssets, wrotePackageJson, warnings }
|
|
151
210
|
}
|
|
152
211
|
|
|
153
212
|
async function isFile(file: string): Promise<boolean> {
|
|
@@ -158,26 +217,60 @@ async function isFile(file: string): Promise<boolean> {
|
|
|
158
217
|
}
|
|
159
218
|
}
|
|
160
219
|
|
|
161
|
-
async function
|
|
220
|
+
async function maybeReadFile(file: string): Promise<Uint8Array | null> {
|
|
221
|
+
try {
|
|
222
|
+
return await fs.readFile(file)
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (isMissingFile(error)) return null
|
|
225
|
+
throw error
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isMissingFile(error: unknown): boolean {
|
|
230
|
+
return error instanceof Error && 'code' in error && error.code === 'ENOENT'
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function updatePackageJson(
|
|
162
234
|
resolution: ResolveResult,
|
|
163
235
|
projectRoot: string,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
236
|
+
opts: {
|
|
237
|
+
rootRequests: InstallRootRequest[]
|
|
238
|
+
installMetadata: Record<string, AssetInstallMetadata>
|
|
239
|
+
packageManagerNeeded: boolean
|
|
240
|
+
},
|
|
241
|
+
): Promise<{ packageManagerNeeded: boolean }> {
|
|
169
242
|
const pkgPath = path.join(projectRoot, 'package.json')
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
243
|
+
const pkg = (await isFile(pkgPath))
|
|
244
|
+
? parsePackageJson(await fs.readFile(pkgPath, 'utf-8'), pkgPath)
|
|
245
|
+
: defaultPackageJson()
|
|
246
|
+
|
|
247
|
+
let changed = false
|
|
248
|
+
let packageManagerNeeded = opts.packageManagerNeeded
|
|
175
249
|
|
|
176
|
-
|
|
177
|
-
|
|
250
|
+
const npmChanged = mergeRecord(pkg, 'dependencies', resolution.npmDependencies)
|
|
251
|
+
if (npmChanged) {
|
|
252
|
+
changed = true
|
|
253
|
+
packageManagerNeeded = true
|
|
254
|
+
}
|
|
178
255
|
|
|
179
|
-
|
|
256
|
+
const assetDependencies = assetDependenciesToSave(
|
|
257
|
+
resolution,
|
|
258
|
+
opts.rootRequests,
|
|
259
|
+
opts.installMetadata,
|
|
260
|
+
)
|
|
261
|
+
if (mergeRecord(pkg, 'assetDependencies', assetDependencies)) {
|
|
262
|
+
changed = true
|
|
263
|
+
}
|
|
180
264
|
|
|
265
|
+
if (changed) {
|
|
266
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return { packageManagerNeeded }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function runPackageManagerInstall(projectRoot: string, log: (msg: string) => void) {
|
|
273
|
+
log('Installing npm dependencies...')
|
|
181
274
|
const pm = await detectPackageManager(projectRoot)
|
|
182
275
|
const pmName = pm?.name ?? 'npm'
|
|
183
276
|
|
|
@@ -186,6 +279,75 @@ async function installNpmDeps(
|
|
|
186
279
|
log('npm dependencies installed.')
|
|
187
280
|
}
|
|
188
281
|
|
|
282
|
+
async function writeInstalledAssetLock(
|
|
283
|
+
projectRoot: string,
|
|
284
|
+
assets: DownloadedAsset[],
|
|
285
|
+
): Promise<void> {
|
|
286
|
+
const lock = await readMarketLock(projectRoot)
|
|
287
|
+
for (const asset of assets) {
|
|
288
|
+
lock.assets[asset.name] = {
|
|
289
|
+
version: asset.version,
|
|
290
|
+
type: asset.type,
|
|
291
|
+
files: asset.fileHashes,
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
await writeMarketLock(projectRoot, lock)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function defaultPackageJson(): PackageJson {
|
|
298
|
+
return { name: 'my-project', private: true, dependencies: {} }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function mergeRecord(
|
|
302
|
+
pkg: PackageJson,
|
|
303
|
+
field: 'dependencies' | 'assetDependencies',
|
|
304
|
+
values: Record<string, string>,
|
|
305
|
+
): boolean {
|
|
306
|
+
const entries = Object.entries(values)
|
|
307
|
+
if (entries.length === 0) return false
|
|
308
|
+
|
|
309
|
+
const current = pkg[field] ?? {}
|
|
310
|
+
let changed = false
|
|
311
|
+
|
|
312
|
+
for (const [name, range] of entries) {
|
|
313
|
+
if (current[name] === range) continue
|
|
314
|
+
current[name] = range
|
|
315
|
+
changed = true
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
pkg[field] = current
|
|
319
|
+
return changed
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function assetDependenciesToSave(
|
|
323
|
+
resolution: ResolveResult,
|
|
324
|
+
rootRequests: InstallRootRequest[],
|
|
325
|
+
metadata: Record<string, AssetInstallMetadata>,
|
|
326
|
+
): Record<string, string> {
|
|
327
|
+
const resolvedByName = new Map(resolution.assets.map((asset) => [asset.name, asset]))
|
|
328
|
+
const out: Record<string, string> = {}
|
|
329
|
+
|
|
330
|
+
for (const request of rootRequests) {
|
|
331
|
+
if (!request.save) continue
|
|
332
|
+
|
|
333
|
+
const asset = resolvedByName.get(request.name)
|
|
334
|
+
if (!asset) continue
|
|
335
|
+
|
|
336
|
+
if ((metadata[asset.type]?.saveOnInstall ?? true) === false) continue
|
|
337
|
+
out[asset.name] = request.saveRange
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return out
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
344
|
+
if (a.length !== b.length) return false
|
|
345
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
346
|
+
if (a[i] !== b[i]) return false
|
|
347
|
+
}
|
|
348
|
+
return true
|
|
349
|
+
}
|
|
350
|
+
|
|
189
351
|
async function installSkills(
|
|
190
352
|
resolution: ResolveResult,
|
|
191
353
|
projectRoot: string,
|