@drawcall/market 0.1.49 → 0.1.51
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 +4 -1
- package/dist/cli.js +46 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/generate-install.d.ts +6 -0
- package/dist/commands/generate-install.d.ts.map +1 -0
- package/dist/commands/generate-install.js +29 -0
- package/dist/commands/generate-install.js.map +1 -0
- package/dist/commands/generate.d.ts +13 -0
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +57 -20
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/pack.d.ts +14 -0
- package/dist/commands/pack.d.ts.map +1 -0
- package/dist/commands/pack.js +39 -0
- package/dist/commands/pack.js.map +1 -0
- package/dist/commands/upload.d.ts +0 -12
- package/dist/commands/upload.d.ts.map +1 -1
- package/dist/commands/upload.js +14 -187
- package/dist/commands/upload.js.map +1 -1
- package/dist/generate.d.ts +14 -6
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +30 -19
- package/dist/generate.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/output.d.ts +4 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +34 -0
- package/dist/output.js.map +1 -1
- package/dist/pack.d.ts +48 -0
- package/dist/pack.d.ts.map +1 -0
- package/dist/pack.js +265 -0
- package/dist/pack.js.map +1 -0
- package/dist/skill.d.ts +1 -1
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js +3 -2
- package/dist/skill.js.map +1 -1
- package/package.json +3 -2
- package/skills/market/SKILL.md +3 -2
- package/src/cli.ts +61 -4
- package/src/commands/agent.ts +1 -1
- package/src/commands/generate-install.ts +36 -0
- package/src/commands/generate.ts +77 -30
- package/src/commands/pack.ts +55 -0
- package/src/commands/upload.ts +14 -219
- package/src/generate.ts +43 -31
- package/src/index.ts +2 -2
- package/src/output.ts +38 -0
- package/src/pack.ts +348 -0
- package/src/skill.ts +3 -2
package/src/commands/generate.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import ora from 'ora'
|
|
1
|
+
import ora, { type Ora } from 'ora'
|
|
2
2
|
import { getCliClient } from '../cli-client.js'
|
|
3
|
-
import {
|
|
3
|
+
import { GenerateError, startGeneration, waitForGeneration, type PollOutcome } from '../generate.js'
|
|
4
|
+
import type { MarketClient } from '../client.js'
|
|
4
5
|
import { resolve } from '../resolve.js'
|
|
5
6
|
import { install as runInstall } from '../install.js'
|
|
6
|
-
import { generatedInstallResult } from '../output.js'
|
|
7
|
+
import { generatedInstallResult, generationStartedResult } from '../output.js'
|
|
7
8
|
import type { AssetAccess, AssetType } from '../schemas.js'
|
|
8
9
|
|
|
9
10
|
export interface GenerateCommandOptions {
|
|
@@ -18,40 +19,86 @@ export async function generateCommand(
|
|
|
18
19
|
opts: GenerateCommandOptions,
|
|
19
20
|
): Promise<void> {
|
|
20
21
|
const { client, baseUrl } = await getCliClient({ baseUrl: opts.baseUrl, requireAuth: true })
|
|
21
|
-
|
|
22
|
-
const spinner = ora({
|
|
23
|
-
text: `Generating "${description}"`,
|
|
24
|
-
isEnabled: Boolean(process.stderr.isTTY),
|
|
25
|
-
isSilent: !process.stderr.isTTY,
|
|
26
|
-
}).start()
|
|
22
|
+
const spinner = startSpinner(`Generating "${description}"`)
|
|
27
23
|
try {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
const started = await startGeneration(client, {
|
|
25
|
+
description,
|
|
26
|
+
type: opts.type,
|
|
27
|
+
access: opts.access,
|
|
28
|
+
})
|
|
29
|
+
// Fast providers finish inline; slow, job-based ones return a job to poll.
|
|
30
|
+
if (started.status === 'completed') {
|
|
31
|
+
await installGenerated(client, started, { cwd: opts.cwd, baseUrl, spinner })
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
// Block-and-poll for a bounded window (the command waits, not the agent). If the job outlasts the
|
|
35
|
+
// window, hand back its id so the caller continues with `generate install`.
|
|
36
|
+
const outcome = await waitForGeneration(client, started.jobId, {
|
|
37
|
+
onProgress: (message) => {
|
|
38
|
+
spinner.text = message
|
|
35
39
|
},
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
spinner.text = `Resolving ${generated.assetName}@${generated.version}`
|
|
39
|
-
const resolution = await resolve(client.asset, [
|
|
40
|
-
{ name: generated.assetName, range: generated.version },
|
|
41
|
-
])
|
|
42
|
-
|
|
43
|
-
await runInstall(client, resolution, {
|
|
40
|
+
})
|
|
41
|
+
await finishGeneration(client, outcome, {
|
|
44
42
|
cwd: opts.cwd,
|
|
45
43
|
baseUrl,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
},
|
|
44
|
+
spinner,
|
|
45
|
+
stillRunning: generationStartedResult(started.jobId),
|
|
49
46
|
})
|
|
50
|
-
|
|
51
|
-
spinner.stop()
|
|
52
|
-
console.log(generatedInstallResult(generated.assetName, generated.version))
|
|
53
47
|
} catch (err) {
|
|
54
48
|
spinner.stop()
|
|
55
49
|
throw err
|
|
56
50
|
}
|
|
57
51
|
}
|
|
52
|
+
|
|
53
|
+
interface InstallContext {
|
|
54
|
+
cwd?: string
|
|
55
|
+
baseUrl: string
|
|
56
|
+
spinner: Ora
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Finish a poll outcome: install on completion, fail loudly on error, or print the still-running note
|
|
60
|
+
// (the job keeps running server-side). Shared by `generate` and `generate install`.
|
|
61
|
+
export async function finishGeneration(
|
|
62
|
+
client: MarketClient,
|
|
63
|
+
outcome: PollOutcome,
|
|
64
|
+
ctx: InstallContext & { stillRunning: string },
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
if (outcome.status === 'completed') {
|
|
67
|
+
await installGenerated(client, outcome, ctx)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
ctx.spinner.stop()
|
|
71
|
+
if (outcome.status === 'failed') {
|
|
72
|
+
throw new GenerateError(outcome.error)
|
|
73
|
+
}
|
|
74
|
+
console.log(ctx.stillRunning)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Install a finished generation into the project and report it.
|
|
78
|
+
async function installGenerated(
|
|
79
|
+
client: MarketClient,
|
|
80
|
+
generated: { assetName: string; version: string },
|
|
81
|
+
ctx: InstallContext,
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
ctx.spinner.text = `Resolving ${generated.assetName}@${generated.version}`
|
|
84
|
+
const resolution = await resolve(client.asset, [
|
|
85
|
+
{ name: generated.assetName, range: generated.version },
|
|
86
|
+
])
|
|
87
|
+
await runInstall(client, resolution, {
|
|
88
|
+
cwd: ctx.cwd,
|
|
89
|
+
baseUrl: ctx.baseUrl,
|
|
90
|
+
onProgress: (message) => {
|
|
91
|
+
ctx.spinner.text = message
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
ctx.spinner.stop()
|
|
95
|
+
console.log(generatedInstallResult(generated.assetName, generated.version))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function startSpinner(text: string): Ora {
|
|
99
|
+
return ora({
|
|
100
|
+
text,
|
|
101
|
+
isEnabled: Boolean(process.stderr.isTTY),
|
|
102
|
+
isSilent: !process.stderr.isTTY,
|
|
103
|
+
}).start()
|
|
104
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as fs from 'fs/promises'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import ora from 'ora'
|
|
4
|
+
import { packAsset, packPolicyForType, parsePackDependencies } from '../pack.js'
|
|
5
|
+
import { packResult } from '../output.js'
|
|
6
|
+
import type { AssetType } from '../schemas.js'
|
|
7
|
+
|
|
8
|
+
export interface PackCommandOptions {
|
|
9
|
+
type?: AssetType
|
|
10
|
+
cwd?: string
|
|
11
|
+
out?: string
|
|
12
|
+
/** npm dependencies, each `name@range` (range defaults to `*`). */
|
|
13
|
+
npm?: string[]
|
|
14
|
+
/** asset dependencies, each `name@range` (range defaults to `*`). */
|
|
15
|
+
asset?: string[]
|
|
16
|
+
/** skill dependencies, each `label=source` passed to `skills add`. */
|
|
17
|
+
skill?: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function packCommand(zipFilter: string, opts: PackCommandOptions): Promise<void> {
|
|
21
|
+
const spinner = ora({
|
|
22
|
+
text: 'Preparing pack',
|
|
23
|
+
isEnabled: Boolean(process.stderr.isTTY),
|
|
24
|
+
isSilent: !process.stderr.isTTY,
|
|
25
|
+
}).start()
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const cwd = opts.cwd ?? process.cwd()
|
|
29
|
+
const packed = await packAsset(zipFilter, {
|
|
30
|
+
cwd,
|
|
31
|
+
dependencies: parsePackDependencies({
|
|
32
|
+
npm: opts.npm,
|
|
33
|
+
asset: opts.asset,
|
|
34
|
+
skill: opts.skill,
|
|
35
|
+
}),
|
|
36
|
+
policy: packPolicyForType(opts.type),
|
|
37
|
+
})
|
|
38
|
+
const out = opts.out ? path.resolve(cwd, opts.out) : defaultPackPath(packed.sourcePath)
|
|
39
|
+
|
|
40
|
+
spinner.text = `Writing ${path.relative(cwd, out) || out}`
|
|
41
|
+
await fs.mkdir(path.dirname(out), { recursive: true })
|
|
42
|
+
await fs.writeFile(out, packed.zip)
|
|
43
|
+
|
|
44
|
+
spinner.stop()
|
|
45
|
+
console.log(packResult(out, packed))
|
|
46
|
+
} catch (err) {
|
|
47
|
+
spinner.stop()
|
|
48
|
+
throw err
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function defaultPackPath(sourcePath: string): string {
|
|
53
|
+
const ext = path.extname(sourcePath)
|
|
54
|
+
return path.join(path.dirname(sourcePath), `${path.basename(sourcePath, ext)}.packed${ext}`)
|
|
55
|
+
}
|
package/src/commands/upload.ts
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
import * as fs from 'fs/promises'
|
|
2
|
-
import * as path from 'path'
|
|
3
|
-
import { unzipSync, zipSync } from 'fflate'
|
|
4
1
|
import ora from 'ora'
|
|
5
2
|
import semver from 'semver'
|
|
6
3
|
import { getCliClient } from '../cli-client.js'
|
|
7
|
-
import {
|
|
8
|
-
import { readMarketLock, sha256 } from '../market-lock.js'
|
|
9
|
-
import {
|
|
10
|
-
packageJsonAssetDependenciesFromFiles,
|
|
11
|
-
packageJsonNpmDependenciesFromFiles,
|
|
12
|
-
} from '../package-json.js'
|
|
13
|
-
import { MAX_ASSET_DESCRIPTION_LENGTH, MAX_UPLOAD_ZIP_SIZE_BYTES } from '../schemas.js'
|
|
4
|
+
import { packAsset, packPolicyFromInstallMetadata, parsePackDependencies } from '../pack.js'
|
|
14
5
|
import { unchangedUploadResult, uploadResult } from '../output.js'
|
|
15
6
|
import {
|
|
16
7
|
assetDescriptionSchema,
|
|
17
8
|
assetNameSchema,
|
|
18
9
|
semverSchema,
|
|
10
|
+
MAX_ASSET_DESCRIPTION_LENGTH,
|
|
19
11
|
type AssetAccess,
|
|
20
12
|
type AssetType,
|
|
21
13
|
} from '../schemas.js'
|
|
@@ -52,37 +44,21 @@ export async function uploadCommand(
|
|
|
52
44
|
const parsedVersion = opts.version ? semverSchema.parse(opts.version) : undefined
|
|
53
45
|
const parsedDescription = parseUploadDescription(description)
|
|
54
46
|
// Parse dep flags before any network work so a malformed spec fails fast.
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
const dependencies = parsePackDependencies({
|
|
48
|
+
npm: opts.npm,
|
|
49
|
+
asset: opts.asset,
|
|
50
|
+
skill: opts.skill,
|
|
51
|
+
})
|
|
58
52
|
const cwd = opts.cwd ?? process.cwd()
|
|
59
53
|
const type = opts.type
|
|
60
|
-
const zipFile = await resolveOneZipFile(cwd, zipFilter)
|
|
61
|
-
const zipStat = await fs.stat(zipFile)
|
|
62
|
-
if (zipStat.size >= MAX_UPLOAD_ZIP_SIZE_BYTES) {
|
|
63
|
-
throw new Error('Upload zip must be smaller than 1 GB')
|
|
64
|
-
}
|
|
65
|
-
const sourceZip = new Uint8Array(await fs.readFile(zipFile))
|
|
66
54
|
const { client } = await getCliClient({ baseUrl: opts.baseUrl, requireAuth: true })
|
|
67
55
|
const installMetadata = await client.asset.installMetadata()
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const assetDependencies =
|
|
74
|
-
packageJsonAssetDependencies,
|
|
75
|
-
explicitAssetDependencies,
|
|
76
|
-
)
|
|
77
|
-
// A type that ships a package.json (templates) also carries its npm dependencies there; capture
|
|
78
|
-
// them so an install over an existing package.json brings them. Explicit --npm wins on conflict.
|
|
79
|
-
const packageJsonNpmDependencies = typeMetadata?.readAssetDependenciesFromPackageJson
|
|
80
|
-
? packageJsonNpmDependenciesFromFiles(sourceFiles)
|
|
81
|
-
: {}
|
|
82
|
-
const npmDependencies = { ...packageJsonNpmDependencies, ...explicitNpmDependencies }
|
|
83
|
-
const zip = typeMetadata?.omitUnchangedInstalledFilesOnUpload
|
|
84
|
-
? await omitUnchangedInstalledFiles(sourceZip, sourceFiles, assetDependencies, cwd)
|
|
85
|
-
: sourceZip
|
|
56
|
+
const packed = await packAsset(zipFilter, {
|
|
57
|
+
cwd,
|
|
58
|
+
dependencies,
|
|
59
|
+
policy: packPolicyFromInstallMetadata(installMetadata[type]),
|
|
60
|
+
})
|
|
61
|
+
const { zip, npmDependencies, assetDependencies, skillDependencies } = packed
|
|
86
62
|
const profile = await client.user.getProfile()
|
|
87
63
|
if (!profile) {
|
|
88
64
|
throw new Error('Not logged in. Run `market login` first.')
|
|
@@ -151,118 +127,6 @@ export function parseUploadDescription(description: string): string {
|
|
|
151
127
|
}
|
|
152
128
|
}
|
|
153
129
|
|
|
154
|
-
/**
|
|
155
|
-
* Parse `name@range` specs (npm or asset deps) into a name→range record. The
|
|
156
|
-
* range is optional and defaults to `*`. A leading `@` is treated as a scope
|
|
157
|
-
* marker, so `@scope/pkg@^1.0.0` splits into `@scope/pkg` and `^1.0.0`.
|
|
158
|
-
*/
|
|
159
|
-
export function parseVersionedDeps(specs: string[], kind: 'npm' | 'asset'): Record<string, string> {
|
|
160
|
-
const out: Record<string, string> = {}
|
|
161
|
-
for (const spec of specs) {
|
|
162
|
-
const at = spec.lastIndexOf('@')
|
|
163
|
-
const hasRange = at > 0
|
|
164
|
-
const name = hasRange ? spec.slice(0, at) : spec
|
|
165
|
-
const range = hasRange ? spec.slice(at + 1) : '*'
|
|
166
|
-
if (!name || !range) {
|
|
167
|
-
throw new Error(`Invalid ${kind} dependency "${spec}". Use name@range (e.g. three@^0.178.0).`)
|
|
168
|
-
}
|
|
169
|
-
if (name in out) {
|
|
170
|
-
throw new Error(`Duplicate ${kind} dependency "${name}".`)
|
|
171
|
-
}
|
|
172
|
-
out[name] = range
|
|
173
|
-
}
|
|
174
|
-
return out
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Parse `label=source` specs into a label→source record. The source is passed
|
|
179
|
-
* verbatim to `skills add` (a GitHub/git ref or a local path), so only the
|
|
180
|
-
* first `=` is treated as the separator.
|
|
181
|
-
*/
|
|
182
|
-
export function parseSkillDeps(specs: string[]): Record<string, string> {
|
|
183
|
-
const out: Record<string, string> = {}
|
|
184
|
-
for (const spec of specs) {
|
|
185
|
-
const eq = spec.indexOf('=')
|
|
186
|
-
if (eq <= 0 || eq === spec.length - 1) {
|
|
187
|
-
throw new Error(
|
|
188
|
-
`Invalid skill dependency "${spec}". Use label=source ` +
|
|
189
|
-
`(e.g. web-design=vercel-labs/agent-skills).`,
|
|
190
|
-
)
|
|
191
|
-
}
|
|
192
|
-
const label = spec.slice(0, eq)
|
|
193
|
-
if (label in out) {
|
|
194
|
-
throw new Error(`Duplicate skill dependency "${label}".`)
|
|
195
|
-
}
|
|
196
|
-
out[label] = spec.slice(eq + 1)
|
|
197
|
-
}
|
|
198
|
-
return out
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function mergeAssetDependencies(
|
|
202
|
-
fromPackageJson: Record<string, string>,
|
|
203
|
-
explicit: Record<string, string>,
|
|
204
|
-
): Record<string, string> {
|
|
205
|
-
const merged = { ...fromPackageJson }
|
|
206
|
-
for (const [name, range] of Object.entries(explicit)) {
|
|
207
|
-
if (name in merged && merged[name] !== range) {
|
|
208
|
-
throw new Error(
|
|
209
|
-
`Conflicting asset dependency "${name}": package.json has ${merged[name]}, --asset has ${range}`,
|
|
210
|
-
)
|
|
211
|
-
}
|
|
212
|
-
merged[name] = range
|
|
213
|
-
}
|
|
214
|
-
return merged
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async function omitUnchangedInstalledFiles(
|
|
218
|
-
sourceZip: Uint8Array,
|
|
219
|
-
sourceFiles: Record<string, Uint8Array>,
|
|
220
|
-
assetDependencies: Record<string, string>,
|
|
221
|
-
cwd: string,
|
|
222
|
-
): Promise<Uint8Array> {
|
|
223
|
-
const dependencyNames = new Set(Object.keys(assetDependencies))
|
|
224
|
-
if (dependencyNames.size === 0) return sourceZip
|
|
225
|
-
|
|
226
|
-
const installRoot = await findInstallRoot(cwd)
|
|
227
|
-
const lock = await readMarketLock(installRoot)
|
|
228
|
-
const hashesByPath = new Map<string, string>()
|
|
229
|
-
|
|
230
|
-
for (const [name, asset] of Object.entries(lock.assets)) {
|
|
231
|
-
if (!dependencyNames.has(name)) continue
|
|
232
|
-
for (const [file, metadata] of Object.entries(asset.files)) {
|
|
233
|
-
hashesByPath.set(file, metadata.sha256)
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (hashesByPath.size === 0) return sourceZip
|
|
238
|
-
|
|
239
|
-
let omitted = false
|
|
240
|
-
const filtered: Record<string, Uint8Array> = {}
|
|
241
|
-
for (const [file, content] of Object.entries(sourceFiles)) {
|
|
242
|
-
const normalizedPath = normalizedZipPath(file)
|
|
243
|
-
const lockedHash = normalizedPath ? hashesByPath.get(normalizedPath) : undefined
|
|
244
|
-
if (lockedHash && lockedHash === sha256(content)) {
|
|
245
|
-
omitted = true
|
|
246
|
-
continue
|
|
247
|
-
}
|
|
248
|
-
filtered[file] = content
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return omitted ? zipSync(filtered) : sourceZip
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function normalizedZipPath(file: string): string | null {
|
|
255
|
-
const zipPath = file.replace(/\\/g, '/')
|
|
256
|
-
if (
|
|
257
|
-
zipPath.split('/').includes('..') ||
|
|
258
|
-
path.posix.isAbsolute(zipPath) ||
|
|
259
|
-
path.win32.isAbsolute(zipPath)
|
|
260
|
-
) {
|
|
261
|
-
return null
|
|
262
|
-
}
|
|
263
|
-
return path.posix.normalize(zipPath)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
130
|
/** Compare a stored dependency JSON string against a freshly parsed record. */
|
|
267
131
|
function depsEqual(storedJson: string, next: Record<string, string>): boolean {
|
|
268
132
|
let stored: Record<string, string>
|
|
@@ -275,71 +139,6 @@ function depsEqual(storedJson: string, next: Record<string, string>): boolean {
|
|
|
275
139
|
return keys.length === Object.keys(next).length && keys.every((key) => stored[key] === next[key])
|
|
276
140
|
}
|
|
277
141
|
|
|
278
|
-
async function resolveOneZipFile(cwd: string, zipFilter: string): Promise<string> {
|
|
279
|
-
const absolute = path.resolve(cwd, zipFilter)
|
|
280
|
-
const stat = await maybeStat(absolute)
|
|
281
|
-
if (stat?.isFile()) return assertZipFile(absolute)
|
|
282
|
-
|
|
283
|
-
const files = await listFiles(cwd)
|
|
284
|
-
const matches = files
|
|
285
|
-
.filter((file) => matchesFilter(path.relative(cwd, file), zipFilter))
|
|
286
|
-
.filter(isZipFile)
|
|
287
|
-
.sort()
|
|
288
|
-
|
|
289
|
-
if (matches.length === 0) {
|
|
290
|
-
throw new Error(`No .zip files matched "${zipFilter}"`)
|
|
291
|
-
}
|
|
292
|
-
if (matches.length > 1) {
|
|
293
|
-
throw new Error(`File filter matched ${matches.length} zips; upload one asset at a time`)
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return matches[0]
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function assertZipFile(file: string): string {
|
|
300
|
-
if (!isZipFile(file)) throw new Error(`Upload file must be a .zip: ${file}`)
|
|
301
|
-
return file
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function isZipFile(file: string): boolean {
|
|
305
|
-
return /\.zip$/i.test(file)
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async function maybeStat(file: string) {
|
|
309
|
-
try {
|
|
310
|
-
return await fs.stat(file)
|
|
311
|
-
} catch {
|
|
312
|
-
return null
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
async function listFiles(dir: string): Promise<string[]> {
|
|
317
|
-
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
318
|
-
const files: string[] = []
|
|
319
|
-
for (const entry of entries) {
|
|
320
|
-
if (entry.name === 'node_modules' || entry.name === '.git') continue
|
|
321
|
-
const fullPath = path.join(dir, entry.name)
|
|
322
|
-
if (entry.isDirectory()) {
|
|
323
|
-
files.push(...(await listFiles(fullPath)))
|
|
324
|
-
} else if (entry.isFile()) {
|
|
325
|
-
files.push(fullPath)
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
return files
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function matchesFilter(file: string, filter: string): boolean {
|
|
332
|
-
const normalizedFile = file.split(path.sep).join('/')
|
|
333
|
-
const normalizedFilter = filter.split(path.sep).join('/')
|
|
334
|
-
const pattern =
|
|
335
|
-
'^' +
|
|
336
|
-
escapeRegExp(normalizedFilter)
|
|
337
|
-
.replace(/\\\*\\\*/g, '.*')
|
|
338
|
-
.replace(/\\\*/g, '[^/]*') +
|
|
339
|
-
'$'
|
|
340
|
-
return new RegExp(pattern).test(normalizedFile)
|
|
341
|
-
}
|
|
342
|
-
|
|
343
142
|
function nextVersion(latest?: string): string {
|
|
344
143
|
if (!latest) return '1.0.0'
|
|
345
144
|
const version = semver.inc(latest, 'patch')
|
|
@@ -356,9 +155,5 @@ function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
|
356
155
|
}
|
|
357
156
|
|
|
358
157
|
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
359
|
-
return
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function escapeRegExp(value: string): string {
|
|
363
|
-
return value.replace(/[|\\{}()[\]^$+?.*]/g, '\\$&')
|
|
158
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer
|
|
364
159
|
}
|
package/src/generate.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MarketClient } from './client.js'
|
|
2
|
-
import type { AssetAccess, AssetType } from './schemas.js'
|
|
2
|
+
import type { AssetAccess, AssetType, GenerateJobStatus, GenerateResponse } from './schemas.js'
|
|
3
3
|
|
|
4
4
|
export class GenerateError extends Error {
|
|
5
5
|
constructor(message: string) {
|
|
@@ -15,49 +15,61 @@ export interface GenerateInput {
|
|
|
15
15
|
access?: AssetAccess
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
// How often the CLI polls a running job.
|
|
19
|
+
const POLL_INTERVAL_MS = 4_000
|
|
20
|
+
// How long one `generate` / `generate install` call blocks-and-polls before handing back control.
|
|
21
|
+
// The command does the waiting (the agent never sleeps between checks). When the window elapses the
|
|
22
|
+
// call returns and the caller just runs `generate install <jobId>` again, which picks up the SAME
|
|
23
|
+
// server-side job where this left off — so no asset type needs to be flagged "slow" ahead of time;
|
|
24
|
+
// anything that outlasts one window simply continues on the next call.
|
|
25
|
+
const WINDOW_MS = 120_000
|
|
26
|
+
|
|
27
|
+
// Start a generation. Fast providers answer `completed` inline; slow, job-based providers answer
|
|
28
|
+
// `pending` with a jobId to poll (see `waitForGeneration` / `generate install`).
|
|
29
|
+
export function startGeneration(
|
|
30
|
+
client: MarketClient,
|
|
31
|
+
input: GenerateInput,
|
|
32
|
+
): Promise<GenerateResponse> {
|
|
33
|
+
return client.asset.generate(input)
|
|
21
34
|
}
|
|
22
35
|
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
// One poll of a generation job.
|
|
37
|
+
export function pollGeneration(client: MarketClient, jobId: string): Promise<GenerateJobStatus> {
|
|
38
|
+
return client.asset.generateStatus({ jobId })
|
|
25
39
|
}
|
|
26
40
|
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
// The bounded outcome of waiting on a job: it settled (completed/failed), or the window elapsed while
|
|
42
|
+
// it was still running.
|
|
43
|
+
export type PollOutcome =
|
|
44
|
+
| { status: 'completed'; assetName: string; version: string }
|
|
45
|
+
| { status: 'failed'; error: string }
|
|
46
|
+
| { status: 'running' }
|
|
33
47
|
|
|
34
|
-
|
|
48
|
+
// Poll a job until it settles or `windowMs` elapses. Bounded so a single call stays under a harness
|
|
49
|
+
// timeout; on timeout it returns `running` (the job keeps going server-side — run again to continue).
|
|
50
|
+
export async function waitForGeneration(
|
|
35
51
|
client: MarketClient,
|
|
36
|
-
|
|
37
|
-
opts:
|
|
38
|
-
): Promise<
|
|
52
|
+
jobId: string,
|
|
53
|
+
opts: { windowMs?: number; onProgress?: (message: string) => void } = {},
|
|
54
|
+
): Promise<PollOutcome> {
|
|
55
|
+
const windowMs = opts.windowMs ?? WINDOW_MS
|
|
39
56
|
const report = opts.onProgress ?? (() => {})
|
|
40
|
-
|
|
41
|
-
report('Generating asset')
|
|
42
|
-
const started = await client.asset.generate(input)
|
|
43
|
-
if (started.status === 'completed') {
|
|
44
|
-
return { assetName: started.assetName, version: started.version }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
57
|
const startedAt = Date.now()
|
|
48
58
|
for (;;) {
|
|
49
|
-
|
|
50
|
-
throw new GenerateError('Generation timed out.')
|
|
51
|
-
}
|
|
52
|
-
await delay(POLL_INTERVAL_MS)
|
|
53
|
-
report('Generating asset')
|
|
54
|
-
const status = await client.asset.generateStatus({ jobId: started.jobId })
|
|
59
|
+
const status = await pollGeneration(client, jobId)
|
|
55
60
|
if (status.status === 'completed') {
|
|
56
|
-
return { assetName: status.assetName, version: status.version }
|
|
61
|
+
return { status: 'completed', assetName: status.assetName, version: status.version }
|
|
57
62
|
}
|
|
58
63
|
if (status.status === 'failed') {
|
|
59
|
-
|
|
64
|
+
return { status: 'failed', error: status.error }
|
|
65
|
+
}
|
|
66
|
+
const remaining = windowMs - (Date.now() - startedAt)
|
|
67
|
+
if (remaining <= 0) {
|
|
68
|
+
return { status: 'running' }
|
|
60
69
|
}
|
|
70
|
+
report('Generating asset')
|
|
71
|
+
// Trim the last sleep so the call returns as soon as the window passes, not a poll-interval later.
|
|
72
|
+
await delay(Math.min(POLL_INTERVAL_MS, remaining))
|
|
61
73
|
}
|
|
62
74
|
}
|
|
63
75
|
|
package/src/index.ts
CHANGED
|
@@ -40,8 +40,8 @@ export { resolve, ResolutionError } from './resolve.js'
|
|
|
40
40
|
export type { ResolvedAsset, ResolveResult } from './resolve.js'
|
|
41
41
|
|
|
42
42
|
// Generate
|
|
43
|
-
export {
|
|
44
|
-
export type { GenerateInput
|
|
43
|
+
export { startGeneration, pollGeneration, GenerateError } from './generate.js'
|
|
44
|
+
export type { GenerateInput } from './generate.js'
|
|
45
45
|
|
|
46
46
|
// Agent
|
|
47
47
|
export { agentAndWait, AgentError } from './agent.js'
|
package/src/output.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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
5
|
|
|
5
6
|
export function assetVersionRef(name: string, version?: string): string {
|
|
6
7
|
return version ? `${name}@${version}` : name
|
|
@@ -125,6 +126,20 @@ export function generatedInstallResult(name: string, version: string): string {
|
|
|
125
126
|
return `Generated and installed ${assetVersionRef(name, version)}`
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
// Printed when `generate` returns before the job finished: it keeps running server-side and the
|
|
130
|
+
// caller continues it with `generate install`, which installs the asset once it is ready.
|
|
131
|
+
export function generationStartedResult(jobId: string): string {
|
|
132
|
+
return [
|
|
133
|
+
'Still generating after a couple of minutes — it continues in the background.',
|
|
134
|
+
`Run this to pick up where it left off and install when ready: market generate install ${jobId}`,
|
|
135
|
+
].join('\n')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Printed by `generate install` when the job is still running after its wait window.
|
|
139
|
+
export function generationRunningResult(jobId: string): string {
|
|
140
|
+
return `Still generating. Run again to continue: market generate install ${jobId}`
|
|
141
|
+
}
|
|
142
|
+
|
|
128
143
|
export function uploadResult(name: string, version: string): string {
|
|
129
144
|
return `Uploaded ${assetVersionRef(name, version)}`
|
|
130
145
|
}
|
|
@@ -133,6 +148,20 @@ export function unchangedUploadResult(name: string, version: string): string {
|
|
|
133
148
|
return `No upload needed: ${assetVersionRef(name, version)} is unchanged`
|
|
134
149
|
}
|
|
135
150
|
|
|
151
|
+
export function packResult(out: string, packed: PackedAsset): string {
|
|
152
|
+
const lines = [`Packed: ${out}`]
|
|
153
|
+
if (packed.gitignoredFiles > 0) {
|
|
154
|
+
lines.push(`Excluded ${packed.gitignoredFiles} .gitignore'd file(s).`)
|
|
155
|
+
}
|
|
156
|
+
if (packed.omittedUnchangedInstalledFiles) {
|
|
157
|
+
lines.push('Omitted unchanged installed dependency files.')
|
|
158
|
+
}
|
|
159
|
+
lines.push(...dependencyLines('npm dependencies', packed.npmDependencies))
|
|
160
|
+
lines.push(...dependencyLines('asset dependencies', packed.assetDependencies))
|
|
161
|
+
lines.push(...dependencyLines('skill dependencies', packed.skillDependencies))
|
|
162
|
+
return lines.join('\n')
|
|
163
|
+
}
|
|
164
|
+
|
|
136
165
|
export function previewResult(name: string, version: string, out: string): string {
|
|
137
166
|
return `Saved preview for ${assetVersionRef(name, version)}: ${out}`
|
|
138
167
|
}
|
|
@@ -204,3 +233,12 @@ function unique<T>(values: T[]): T[] {
|
|
|
204
233
|
function indent(lines: string[], prefix: string): string[] {
|
|
205
234
|
return lines.map((line) => `${prefix}${line}`)
|
|
206
235
|
}
|
|
236
|
+
|
|
237
|
+
function dependencyLines(label: string, deps: Record<string, string>): string[] {
|
|
238
|
+
const entries = Object.entries(deps)
|
|
239
|
+
if (entries.length === 0) return []
|
|
240
|
+
return [
|
|
241
|
+
`${label}:`,
|
|
242
|
+
...entries.sort(([a], [b]) => a.localeCompare(b)).map(([name, value]) => `- ${name}: ${value}`),
|
|
243
|
+
]
|
|
244
|
+
}
|