transloadit 4.7.3 → 4.7.6
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 +897 -5
- package/dist/Transloadit.d.ts +13 -3
- package/dist/Transloadit.d.ts.map +1 -1
- package/dist/Transloadit.js +22 -2
- package/dist/Transloadit.js.map +1 -1
- package/dist/alphalib/types/assembliesGet.d.ts +5 -0
- package/dist/alphalib/types/assembliesGet.d.ts.map +1 -1
- package/dist/alphalib/types/assemblyReplay.d.ts +5 -0
- package/dist/alphalib/types/assemblyReplay.d.ts.map +1 -1
- package/dist/alphalib/types/assemblyReplayNotification.d.ts +5 -0
- package/dist/alphalib/types/assemblyReplayNotification.d.ts.map +1 -1
- package/dist/alphalib/types/assemblyStatus.d.ts +25 -25
- package/dist/alphalib/types/assemblyStatus.d.ts.map +1 -1
- package/dist/alphalib/types/assemblyStatus.js +4 -1
- package/dist/alphalib/types/assemblyStatus.js.map +1 -1
- package/dist/alphalib/types/bill.d.ts +5 -0
- package/dist/alphalib/types/bill.d.ts.map +1 -1
- package/dist/alphalib/types/builtinTemplates.d.ts +83 -0
- package/dist/alphalib/types/builtinTemplates.d.ts.map +1 -0
- package/dist/alphalib/types/builtinTemplates.js +19 -0
- package/dist/alphalib/types/builtinTemplates.js.map +1 -0
- package/dist/alphalib/types/robots/ai-chat.d.ts.map +1 -1
- package/dist/alphalib/types/robots/ai-chat.js +1 -0
- package/dist/alphalib/types/robots/ai-chat.js.map +1 -1
- package/dist/alphalib/types/skillFrontmatter.d.ts +29 -0
- package/dist/alphalib/types/skillFrontmatter.d.ts.map +1 -0
- package/dist/alphalib/types/skillFrontmatter.js +19 -0
- package/dist/alphalib/types/skillFrontmatter.js.map +1 -0
- package/dist/alphalib/types/template.d.ts +36 -0
- package/dist/alphalib/types/template.d.ts.map +1 -1
- package/dist/alphalib/types/template.js +10 -0
- package/dist/alphalib/types/template.js.map +1 -1
- package/dist/alphalib/types/templateCredential.d.ts +10 -0
- package/dist/alphalib/types/templateCredential.d.ts.map +1 -1
- package/dist/bearerToken.d.ts +31 -0
- package/dist/bearerToken.d.ts.map +1 -0
- package/dist/bearerToken.js +158 -0
- package/dist/bearerToken.js.map +1 -0
- package/dist/cli/commands/assemblies.d.ts +8 -2
- package/dist/cli/commands/assemblies.d.ts.map +1 -1
- package/dist/cli/commands/assemblies.js +566 -411
- package/dist/cli/commands/assemblies.js.map +1 -1
- package/dist/cli/commands/auth.d.ts +1 -4
- package/dist/cli/commands/auth.d.ts.map +1 -1
- package/dist/cli/commands/auth.js +7 -123
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +5 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/templates.d.ts.map +1 -1
- package/dist/cli/commands/templates.js +4 -14
- package/dist/cli/commands/templates.js.map +1 -1
- package/dist/cli/fileProcessingOptions.d.ts +35 -0
- package/dist/cli/fileProcessingOptions.d.ts.map +1 -0
- package/dist/cli/fileProcessingOptions.js +182 -0
- package/dist/cli/fileProcessingOptions.js.map +1 -0
- package/dist/cli/generateIntentDocs.d.ts +2 -0
- package/dist/cli/generateIntentDocs.d.ts.map +1 -0
- package/dist/cli/generateIntentDocs.js +321 -0
- package/dist/cli/generateIntentDocs.js.map +1 -0
- package/dist/cli/intentCommandSpecs.d.ts +36 -0
- package/dist/cli/intentCommandSpecs.d.ts.map +1 -0
- package/dist/cli/intentCommandSpecs.js +181 -0
- package/dist/cli/intentCommandSpecs.js.map +1 -0
- package/dist/cli/intentCommands.d.ts +13 -0
- package/dist/cli/intentCommands.d.ts.map +1 -0
- package/dist/cli/intentCommands.js +368 -0
- package/dist/cli/intentCommands.js.map +1 -0
- package/dist/cli/intentFields.d.ts +25 -0
- package/dist/cli/intentFields.d.ts.map +1 -0
- package/dist/cli/intentFields.js +298 -0
- package/dist/cli/intentFields.js.map +1 -0
- package/dist/cli/intentInputPolicy.d.ts +10 -0
- package/dist/cli/intentInputPolicy.d.ts.map +1 -0
- package/dist/cli/intentInputPolicy.js +2 -0
- package/dist/cli/intentInputPolicy.js.map +1 -0
- package/dist/cli/intentRuntime.d.ts +114 -0
- package/dist/cli/intentRuntime.d.ts.map +1 -0
- package/dist/cli/intentRuntime.js +464 -0
- package/dist/cli/intentRuntime.js.map +1 -0
- package/dist/cli/resultFiles.d.ts +19 -0
- package/dist/cli/resultFiles.d.ts.map +1 -0
- package/dist/cli/resultFiles.js +66 -0
- package/dist/cli/resultFiles.js.map +1 -0
- package/dist/cli/resultUrls.d.ts +19 -0
- package/dist/cli/resultUrls.d.ts.map +1 -0
- package/dist/cli/resultUrls.js +36 -0
- package/dist/cli/resultUrls.js.map +1 -0
- package/dist/cli/semanticIntents/imageDescribe.d.ts +43 -0
- package/dist/cli/semanticIntents/imageDescribe.d.ts.map +1 -0
- package/dist/cli/semanticIntents/imageDescribe.js +188 -0
- package/dist/cli/semanticIntents/imageDescribe.js.map +1 -0
- package/dist/cli/semanticIntents/index.d.ts +18 -0
- package/dist/cli/semanticIntents/index.d.ts.map +1 -0
- package/dist/cli/semanticIntents/index.js +18 -0
- package/dist/cli/semanticIntents/index.js.map +1 -0
- package/dist/cli/semanticIntents/markdownPdf.d.ts +4 -0
- package/dist/cli/semanticIntents/markdownPdf.d.ts.map +1 -0
- package/dist/cli/semanticIntents/markdownPdf.js +93 -0
- package/dist/cli/semanticIntents/markdownPdf.js.map +1 -0
- package/dist/cli/semanticIntents/parsing.d.ts +11 -0
- package/dist/cli/semanticIntents/parsing.d.ts.map +1 -0
- package/dist/cli/semanticIntents/parsing.js +29 -0
- package/dist/cli/semanticIntents/parsing.js.map +1 -0
- package/dist/cli/stepsInput.d.ts +4 -0
- package/dist/cli/stepsInput.d.ts.map +1 -0
- package/dist/cli/stepsInput.js +23 -0
- package/dist/cli/stepsInput.js.map +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +5 -4
- package/dist/cli.js.map +1 -1
- package/dist/ensureUniqueCounter.d.ts +8 -0
- package/dist/ensureUniqueCounter.d.ts.map +1 -0
- package/dist/ensureUniqueCounter.js +48 -0
- package/dist/ensureUniqueCounter.js.map +1 -0
- package/dist/inputFiles.d.ts +9 -0
- package/dist/inputFiles.d.ts.map +1 -1
- package/dist/inputFiles.js +177 -26
- package/dist/inputFiles.js.map +1 -1
- package/dist/robots.js +1 -1
- package/dist/robots.js.map +1 -1
- package/package.json +9 -7
- package/src/Transloadit.ts +35 -3
- package/src/alphalib/types/assemblyStatus.ts +4 -1
- package/src/alphalib/types/builtinTemplates.ts +24 -0
- package/src/alphalib/types/robots/ai-chat.ts +1 -0
- package/src/alphalib/types/skillFrontmatter.ts +24 -0
- package/src/alphalib/types/template.ts +14 -0
- package/src/bearerToken.ts +208 -0
- package/src/cli/commands/assemblies.ts +825 -505
- package/src/cli/commands/auth.ts +9 -151
- package/src/cli/commands/index.ts +6 -3
- package/src/cli/commands/templates.ts +6 -17
- package/src/cli/fileProcessingOptions.ts +294 -0
- package/src/cli/generateIntentDocs.ts +419 -0
- package/src/cli/intentCommandSpecs.ts +282 -0
- package/src/cli/intentCommands.ts +525 -0
- package/src/cli/intentFields.ts +403 -0
- package/src/cli/intentInputPolicy.ts +11 -0
- package/src/cli/intentRuntime.ts +734 -0
- package/src/cli/resultFiles.ts +105 -0
- package/src/cli/resultUrls.ts +72 -0
- package/src/cli/semanticIntents/imageDescribe.ts +254 -0
- package/src/cli/semanticIntents/index.ts +48 -0
- package/src/cli/semanticIntents/markdownPdf.ts +120 -0
- package/src/cli/semanticIntents/parsing.ts +56 -0
- package/src/cli/stepsInput.ts +32 -0
- package/src/cli.ts +5 -4
- package/src/ensureUniqueCounter.ts +75 -0
- package/src/inputFiles.ts +277 -26
- package/src/robots.ts +1 -1
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
1
2
|
import EventEmitter from 'node:events'
|
|
2
3
|
import fs from 'node:fs'
|
|
3
4
|
import fsp from 'node:fs/promises'
|
|
4
5
|
import path from 'node:path'
|
|
5
6
|
import process from 'node:process'
|
|
6
|
-
import type { Readable
|
|
7
|
+
import type { Readable } from 'node:stream'
|
|
8
|
+
import { Writable } from 'node:stream'
|
|
7
9
|
import { pipeline } from 'node:stream/promises'
|
|
8
10
|
import { setTimeout as delay } from 'node:timers/promises'
|
|
9
|
-
import tty from 'node:tty'
|
|
10
11
|
import { promisify } from 'node:util'
|
|
11
12
|
import { Command, Option } from 'clipanion'
|
|
12
13
|
import got from 'got'
|
|
@@ -15,15 +16,31 @@ import * as t from 'typanion'
|
|
|
15
16
|
import { z } from 'zod'
|
|
16
17
|
import { formatLintIssue } from '../../alphalib/assembly-linter.lang.en.ts'
|
|
17
18
|
import { tryCatch } from '../../alphalib/tryCatch.ts'
|
|
18
|
-
import type {
|
|
19
|
-
import { stepsSchema } from '../../alphalib/types/template.ts'
|
|
19
|
+
import type { StepsInput } from '../../alphalib/types/template.ts'
|
|
20
20
|
import type { CreateAssemblyParams, ReplayAssemblyParams } from '../../apiTypes.ts'
|
|
21
|
+
import { ensureUniqueCounterValue } from '../../ensureUniqueCounter.ts'
|
|
21
22
|
import type { LintFatalLevel } from '../../lintAssemblyInstructions.ts'
|
|
22
23
|
import { lintAssemblyInstructions } from '../../lintAssemblyInstructions.ts'
|
|
23
24
|
import type { CreateAssemblyOptions, Transloadit } from '../../Transloadit.ts'
|
|
24
25
|
import { lintingExamples } from '../docs/assemblyLintingExamples.ts'
|
|
25
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
concurrencyOption,
|
|
28
|
+
deleteAfterProcessingOption,
|
|
29
|
+
inputPathsOption,
|
|
30
|
+
printUrlsOption,
|
|
31
|
+
recursiveOption,
|
|
32
|
+
reprocessStaleOption,
|
|
33
|
+
singleAssemblyOption,
|
|
34
|
+
validateSharedFileProcessingOptions,
|
|
35
|
+
watchOption,
|
|
36
|
+
} from '../fileProcessingOptions.ts'
|
|
37
|
+
import { formatAPIError, readCliInput } from '../helpers.ts'
|
|
26
38
|
import type { IOutputCtl } from '../OutputCtl.ts'
|
|
39
|
+
import type { NormalizedAssemblyResultFile, NormalizedAssemblyResults } from '../resultFiles.ts'
|
|
40
|
+
import { normalizeAssemblyResults } from '../resultFiles.ts'
|
|
41
|
+
import type { ResultUrlRow } from '../resultUrls.ts'
|
|
42
|
+
import { collectNormalizedResultUrlRows, printResultUrls } from '../resultUrls.ts'
|
|
43
|
+
import { readStepsInputFile } from '../stepsInput.ts'
|
|
27
44
|
import { ensureError, isErrnoException } from '../types.ts'
|
|
28
45
|
import { AuthenticatedCommand, UnauthenticatedCommand } from './BaseCommand.ts'
|
|
29
46
|
|
|
@@ -61,6 +78,30 @@ export interface AssemblyLintOptions {
|
|
|
61
78
|
json?: boolean
|
|
62
79
|
}
|
|
63
80
|
|
|
81
|
+
function parseTemplateFieldAssignments(
|
|
82
|
+
output: IOutputCtl,
|
|
83
|
+
fields: string[] | undefined,
|
|
84
|
+
): Record<string, string> | undefined {
|
|
85
|
+
if (fields == null || fields.length === 0) {
|
|
86
|
+
return undefined
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const fieldsMap: Record<string, string> = {}
|
|
90
|
+
for (const field of fields) {
|
|
91
|
+
const eqIndex = field.indexOf('=')
|
|
92
|
+
if (eqIndex === -1) {
|
|
93
|
+
output.error(`invalid argument for --field: '${field}'`)
|
|
94
|
+
return undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const key = field.slice(0, eqIndex)
|
|
98
|
+
const value = field.slice(eqIndex + 1)
|
|
99
|
+
fieldsMap[key] = value
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return fieldsMap
|
|
103
|
+
}
|
|
104
|
+
|
|
64
105
|
const AssemblySchema = z.object({
|
|
65
106
|
id: z.string(),
|
|
66
107
|
})
|
|
@@ -148,13 +189,7 @@ export async function replay(
|
|
|
148
189
|
): Promise<void> {
|
|
149
190
|
if (steps) {
|
|
150
191
|
try {
|
|
151
|
-
|
|
152
|
-
const parsed: unknown = JSON.parse(buf.toString())
|
|
153
|
-
const validated = stepsSchema.safeParse(parsed)
|
|
154
|
-
if (!validated.success) {
|
|
155
|
-
throw new Error(`Invalid steps format: ${validated.error.message}`)
|
|
156
|
-
}
|
|
157
|
-
await apiCall(validated.data)
|
|
192
|
+
await apiCall(await readStepsInputFile(steps))
|
|
158
193
|
} catch (err) {
|
|
159
194
|
const error = ensureError(err)
|
|
160
195
|
output.error(error.message)
|
|
@@ -163,14 +198,13 @@ export async function replay(
|
|
|
163
198
|
await apiCall()
|
|
164
199
|
}
|
|
165
200
|
|
|
166
|
-
async function apiCall(stepsOverride?:
|
|
201
|
+
async function apiCall(stepsOverride?: StepsInput): Promise<void> {
|
|
167
202
|
const promises = assemblies.map(async (assembly) => {
|
|
168
203
|
const [err] = await tryCatch(
|
|
169
204
|
client.replayAssembly(assembly, {
|
|
170
205
|
reparse_template: reparse ? 1 : 0,
|
|
171
206
|
fields,
|
|
172
207
|
notify_url,
|
|
173
|
-
// Steps (validated) is assignable to StepsInput at runtime; cast for TS
|
|
174
208
|
steps: stepsOverride as ReplayAssemblyParams['steps'],
|
|
175
209
|
}),
|
|
176
210
|
)
|
|
@@ -298,49 +332,44 @@ async function getNodeWatch(): Promise<NodeWatchFn> {
|
|
|
298
332
|
const stdinWithPath = process.stdin as unknown as { path: string }
|
|
299
333
|
stdinWithPath.path = '/dev/stdin'
|
|
300
334
|
|
|
301
|
-
interface
|
|
335
|
+
interface OutputPlan {
|
|
336
|
+
mtime: Date
|
|
302
337
|
path?: string
|
|
303
|
-
mtime?: Date
|
|
304
338
|
}
|
|
305
339
|
|
|
306
340
|
interface Job {
|
|
307
|
-
|
|
308
|
-
out:
|
|
341
|
+
inputPath: string | null
|
|
342
|
+
out: OutputPlan | null
|
|
343
|
+
watchEvent?: boolean
|
|
309
344
|
}
|
|
310
345
|
|
|
311
|
-
type
|
|
312
|
-
|
|
313
|
-
interface StreamRegistry {
|
|
314
|
-
[key: string]: OutStream | undefined
|
|
315
|
-
}
|
|
346
|
+
type OutputPlanProvider = (inpath: string | null, indir?: string) => Promise<OutputPlan | null>
|
|
316
347
|
|
|
317
348
|
interface JobEmitterOptions {
|
|
349
|
+
allowOutputCollisions?: boolean
|
|
318
350
|
recursive?: boolean
|
|
319
|
-
|
|
320
|
-
|
|
351
|
+
outputPlanProvider: OutputPlanProvider
|
|
352
|
+
singleAssembly?: boolean
|
|
321
353
|
watch?: boolean
|
|
322
354
|
reprocessStale?: boolean
|
|
323
355
|
}
|
|
324
356
|
|
|
325
357
|
interface ReaddirJobEmitterOptions {
|
|
326
358
|
dir: string
|
|
327
|
-
streamRegistry: StreamRegistry
|
|
328
359
|
recursive?: boolean
|
|
329
|
-
|
|
360
|
+
outputPlanProvider: OutputPlanProvider
|
|
330
361
|
topdir?: string
|
|
331
362
|
}
|
|
332
363
|
|
|
333
364
|
interface SingleJobEmitterOptions {
|
|
334
365
|
file: string
|
|
335
|
-
|
|
336
|
-
outstreamProvider: OutstreamProvider
|
|
366
|
+
outputPlanProvider: OutputPlanProvider
|
|
337
367
|
}
|
|
338
368
|
|
|
339
369
|
interface WatchJobEmitterOptions {
|
|
340
370
|
file: string
|
|
341
|
-
streamRegistry: StreamRegistry
|
|
342
371
|
recursive?: boolean
|
|
343
|
-
|
|
372
|
+
outputPlanProvider: OutputPlanProvider
|
|
344
373
|
}
|
|
345
374
|
|
|
346
375
|
interface StatLike {
|
|
@@ -360,12 +389,49 @@ async function myStat(
|
|
|
360
389
|
return await fsp.stat(filepath)
|
|
361
390
|
}
|
|
362
391
|
|
|
363
|
-
function
|
|
392
|
+
function getJobInputPath(filepath: string): string {
|
|
393
|
+
const normalizedFile = path.normalize(filepath)
|
|
394
|
+
if (normalizedFile === '-') {
|
|
395
|
+
return stdinWithPath.path
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return normalizedFile
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function createInputUploadStream(filepath: string): Readable {
|
|
402
|
+
const instream = fs.createReadStream(filepath)
|
|
403
|
+
// Attach a no-op error handler to prevent unhandled errors if stream is destroyed
|
|
404
|
+
// before being consumed (e.g., due to output collision detection)
|
|
405
|
+
instream.on('error', () => {})
|
|
406
|
+
return instream
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function createOutputPlan(pathname: string | undefined, mtime: Date): OutputPlan {
|
|
410
|
+
if (pathname == null) {
|
|
411
|
+
return {
|
|
412
|
+
mtime,
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
mtime,
|
|
418
|
+
path: pathname,
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function createExistingPathOutputPlan(outputPath: string | undefined): Promise<OutputPlan> {
|
|
423
|
+
if (outputPath == null) {
|
|
424
|
+
return createOutputPlan(undefined, new Date(0))
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const [, stats] = await tryCatch(fsp.stat(outputPath))
|
|
428
|
+
return createOutputPlan(outputPath, stats?.mtime ?? new Date(0))
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function dirProvider(output: string): OutputPlanProvider {
|
|
364
432
|
return async (inpath, indir = process.cwd()) => {
|
|
365
|
-
// Inputless assemblies can still write into a directory, but output paths are derived from
|
|
366
|
-
// assembly results rather than an input file path (handled later).
|
|
367
433
|
if (inpath == null) {
|
|
368
|
-
return
|
|
434
|
+
return await createExistingPathOutputPlan(output)
|
|
369
435
|
}
|
|
370
436
|
if (inpath === '-') {
|
|
371
437
|
throw new Error('You must provide an input to output to a directory')
|
|
@@ -374,41 +440,372 @@ function dirProvider(output: string): OutstreamProvider {
|
|
|
374
440
|
let relpath = path.relative(indir, inpath)
|
|
375
441
|
relpath = relpath.replace(/^(\.\.\/)+/, '')
|
|
376
442
|
const outpath = path.join(output, relpath)
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
await fsp.mkdir(outdir, { recursive: true })
|
|
380
|
-
const [, stats] = await tryCatch(fsp.stat(outpath))
|
|
381
|
-
const mtime = stats?.mtime ?? new Date(0)
|
|
382
|
-
const outstream = fs.createWriteStream(outpath) as OutStream
|
|
383
|
-
// Attach a no-op error handler to prevent unhandled errors if stream is destroyed
|
|
384
|
-
// before being consumed (e.g., due to output collision detection)
|
|
385
|
-
outstream.on('error', () => {})
|
|
386
|
-
outstream.mtime = mtime
|
|
387
|
-
return outstream
|
|
443
|
+
return await createExistingPathOutputPlan(outpath)
|
|
388
444
|
}
|
|
389
445
|
}
|
|
390
446
|
|
|
391
|
-
function fileProvider(output: string):
|
|
392
|
-
const dirExistsP = fsp.mkdir(path.dirname(output), { recursive: true })
|
|
447
|
+
function fileProvider(output: string): OutputPlanProvider {
|
|
393
448
|
return async (_inpath) => {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const outstream = fs.createWriteStream(output) as OutStream
|
|
400
|
-
// Attach a no-op error handler to prevent unhandled errors if stream is destroyed
|
|
401
|
-
// before being consumed (e.g., due to output collision detection)
|
|
402
|
-
outstream.on('error', () => {})
|
|
403
|
-
outstream.mtime = mtime
|
|
404
|
-
return outstream
|
|
449
|
+
if (output === '-') {
|
|
450
|
+
return await createExistingPathOutputPlan(undefined)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return await createExistingPathOutputPlan(output)
|
|
405
454
|
}
|
|
406
455
|
}
|
|
407
456
|
|
|
408
|
-
function nullProvider():
|
|
457
|
+
function nullProvider(): OutputPlanProvider {
|
|
409
458
|
return async (_inpath) => null
|
|
410
459
|
}
|
|
411
460
|
|
|
461
|
+
async function downloadResultToFile(
|
|
462
|
+
resultUrl: string,
|
|
463
|
+
outPath: string,
|
|
464
|
+
signal: AbortSignal,
|
|
465
|
+
): Promise<void> {
|
|
466
|
+
await fsp.mkdir(path.dirname(outPath), { recursive: true })
|
|
467
|
+
|
|
468
|
+
const tempPath = path.join(
|
|
469
|
+
path.dirname(outPath),
|
|
470
|
+
`.${path.basename(outPath)}.${randomUUID()}.tmp`,
|
|
471
|
+
)
|
|
472
|
+
const outStream = fs.createWriteStream(tempPath)
|
|
473
|
+
outStream.on('error', () => {})
|
|
474
|
+
|
|
475
|
+
const [dlErr] = await tryCatch(pipeline(got.stream(resultUrl, { signal }), outStream))
|
|
476
|
+
if (dlErr) {
|
|
477
|
+
await fsp.rm(tempPath, { force: true })
|
|
478
|
+
throw dlErr
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
await fsp.rename(tempPath, outPath)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function downloadResultToStdout(resultUrl: string, signal: AbortSignal): Promise<void> {
|
|
485
|
+
const stdoutStream = new Writable({
|
|
486
|
+
write(chunk, _encoding, callback) {
|
|
487
|
+
let settled = false
|
|
488
|
+
|
|
489
|
+
const finish = (err?: Error | null) => {
|
|
490
|
+
if (settled) return
|
|
491
|
+
settled = true
|
|
492
|
+
process.stdout.off('drain', onDrain)
|
|
493
|
+
process.stdout.off('error', onError)
|
|
494
|
+
callback(err ?? undefined)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const onDrain = () => finish()
|
|
498
|
+
const onError = (err: Error) => finish(err)
|
|
499
|
+
|
|
500
|
+
process.stdout.once('error', onError)
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
if (process.stdout.write(chunk)) {
|
|
504
|
+
finish()
|
|
505
|
+
return
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
process.stdout.once('drain', onDrain)
|
|
509
|
+
} catch (err) {
|
|
510
|
+
finish(ensureError(err))
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
final(callback) {
|
|
514
|
+
callback()
|
|
515
|
+
},
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
await pipeline(got.stream(resultUrl, { signal }), stdoutStream)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function sanitizeResultName(value: string): string {
|
|
522
|
+
const base = path.basename(value)
|
|
523
|
+
return base.replaceAll('\\', '_').replaceAll('/', '_').replaceAll('\u0000', '')
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function ensureUniquePath(targetPath: string, reservedPaths: Set<string>): Promise<string> {
|
|
527
|
+
const parsed = path.parse(targetPath)
|
|
528
|
+
return await ensureUniqueCounterValue({
|
|
529
|
+
initialValue: targetPath,
|
|
530
|
+
isTaken: async (candidate) => {
|
|
531
|
+
if (reservedPaths.has(candidate)) {
|
|
532
|
+
return true
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const [statErr] = await tryCatch(fsp.stat(candidate))
|
|
536
|
+
return statErr == null
|
|
537
|
+
},
|
|
538
|
+
reserve: (candidate) => {
|
|
539
|
+
reservedPaths.add(candidate)
|
|
540
|
+
},
|
|
541
|
+
nextValue: (counter) => path.join(parsed.dir, `${parsed.name}__${counter}${parsed.ext}`),
|
|
542
|
+
scope: reservedPaths,
|
|
543
|
+
})
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function getResultFileName(file: NormalizedAssemblyResultFile): string {
|
|
547
|
+
return sanitizeResultName(file.name)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
interface AssemblyDownloadTarget {
|
|
551
|
+
resultUrl: string
|
|
552
|
+
targetPath: string | null
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const STALE_OUTPUT_GRACE_MS = 1000
|
|
556
|
+
|
|
557
|
+
function isMeaningfullyNewer(newer: Date, older: Date): boolean {
|
|
558
|
+
return newer.getTime() - older.getTime() > STALE_OUTPUT_GRACE_MS
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function buildDirectoryDownloadTargets({
|
|
562
|
+
allFiles,
|
|
563
|
+
baseDir,
|
|
564
|
+
groupByStep,
|
|
565
|
+
reservedPaths,
|
|
566
|
+
}: {
|
|
567
|
+
allFiles: NormalizedAssemblyResultFile[]
|
|
568
|
+
baseDir: string
|
|
569
|
+
groupByStep: boolean
|
|
570
|
+
reservedPaths: Set<string>
|
|
571
|
+
}): Promise<AssemblyDownloadTarget[]> {
|
|
572
|
+
await fsp.mkdir(baseDir, { recursive: true })
|
|
573
|
+
|
|
574
|
+
const targets: AssemblyDownloadTarget[] = []
|
|
575
|
+
for (const resultFile of allFiles) {
|
|
576
|
+
const targetDir = groupByStep ? path.join(baseDir, resultFile.stepName) : baseDir
|
|
577
|
+
await fsp.mkdir(targetDir, { recursive: true })
|
|
578
|
+
|
|
579
|
+
targets.push({
|
|
580
|
+
resultUrl: resultFile.url,
|
|
581
|
+
targetPath: await ensureUniquePath(
|
|
582
|
+
path.join(targetDir, getResultFileName(resultFile)),
|
|
583
|
+
reservedPaths,
|
|
584
|
+
),
|
|
585
|
+
})
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return targets
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function getSingleResultDownloadTarget(
|
|
592
|
+
allFiles: NormalizedAssemblyResultFile[],
|
|
593
|
+
targetPath: string | null,
|
|
594
|
+
): AssemblyDownloadTarget[] {
|
|
595
|
+
const first = allFiles[0]
|
|
596
|
+
const resultUrl = first?.url ?? null
|
|
597
|
+
if (resultUrl == null) {
|
|
598
|
+
return []
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return [{ resultUrl, targetPath }]
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function resolveResultDownloadTargets({
|
|
605
|
+
hasDirectoryInput,
|
|
606
|
+
inPath,
|
|
607
|
+
inputs,
|
|
608
|
+
normalizedResults,
|
|
609
|
+
outputMode,
|
|
610
|
+
outputPath,
|
|
611
|
+
outputRoot,
|
|
612
|
+
outputRootIsDirectory,
|
|
613
|
+
reservedPaths,
|
|
614
|
+
singleAssembly,
|
|
615
|
+
}: {
|
|
616
|
+
hasDirectoryInput: boolean
|
|
617
|
+
inPath: string | null
|
|
618
|
+
inputs: string[]
|
|
619
|
+
normalizedResults: NormalizedAssemblyResults
|
|
620
|
+
outputMode?: 'directory' | 'file'
|
|
621
|
+
outputPath: string | null
|
|
622
|
+
outputRoot: string
|
|
623
|
+
outputRootIsDirectory: boolean
|
|
624
|
+
reservedPaths: Set<string>
|
|
625
|
+
singleAssembly?: boolean
|
|
626
|
+
}): Promise<AssemblyDownloadTarget[]> {
|
|
627
|
+
const { allFiles, entries } = normalizedResults
|
|
628
|
+
const shouldGroupByInput =
|
|
629
|
+
!singleAssembly && inPath != null && (hasDirectoryInput || inputs.length > 1)
|
|
630
|
+
|
|
631
|
+
const resolveDirectoryBaseDir = (): string => {
|
|
632
|
+
if (!shouldGroupByInput || inPath == null) {
|
|
633
|
+
return outputRoot
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (hasDirectoryInput && outputPath != null) {
|
|
637
|
+
const mappedRelative = path.relative(outputRoot, outputPath)
|
|
638
|
+
const mappedDir = path.dirname(mappedRelative)
|
|
639
|
+
const mappedStem = path.parse(mappedRelative).name
|
|
640
|
+
return path.join(outputRoot, mappedDir === '.' ? '' : mappedDir, mappedStem)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return path.join(outputRoot, path.parse(path.basename(inPath)).name)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (!outputRootIsDirectory) {
|
|
647
|
+
if (allFiles.length > 1) {
|
|
648
|
+
if (outputPath == null) {
|
|
649
|
+
throw new Error('stdout can only receive a single result file')
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
throw new Error('file outputs can only receive a single result file')
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return getSingleResultDownloadTarget(allFiles, outputPath)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (singleAssembly) {
|
|
659
|
+
return await buildDirectoryDownloadTargets({
|
|
660
|
+
allFiles,
|
|
661
|
+
baseDir: outputRoot,
|
|
662
|
+
groupByStep: false,
|
|
663
|
+
reservedPaths,
|
|
664
|
+
})
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (outputMode === 'directory' || outputPath == null || inPath == null) {
|
|
668
|
+
return await buildDirectoryDownloadTargets({
|
|
669
|
+
allFiles,
|
|
670
|
+
baseDir: resolveDirectoryBaseDir(),
|
|
671
|
+
groupByStep: entries.length > 1,
|
|
672
|
+
reservedPaths,
|
|
673
|
+
})
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (allFiles.length === 1) {
|
|
677
|
+
return getSingleResultDownloadTarget(allFiles, outputPath)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return await buildDirectoryDownloadTargets({
|
|
681
|
+
allFiles,
|
|
682
|
+
baseDir: path.join(path.dirname(outputPath), path.parse(outputPath).name),
|
|
683
|
+
groupByStep: true,
|
|
684
|
+
reservedPaths,
|
|
685
|
+
})
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async function shouldSkipStaleOutput({
|
|
689
|
+
inputPaths,
|
|
690
|
+
outputPath,
|
|
691
|
+
outputPlanMtime,
|
|
692
|
+
outputRootIsDirectory,
|
|
693
|
+
reprocessStale,
|
|
694
|
+
singleInputReference = 'output-plan',
|
|
695
|
+
}: {
|
|
696
|
+
inputPaths: string[]
|
|
697
|
+
outputPath: string | null
|
|
698
|
+
outputPlanMtime: Date
|
|
699
|
+
outputRootIsDirectory: boolean
|
|
700
|
+
reprocessStale?: boolean
|
|
701
|
+
singleInputReference?: 'input' | 'output-plan'
|
|
702
|
+
}): Promise<boolean> {
|
|
703
|
+
if (reprocessStale || outputPath == null || outputRootIsDirectory) {
|
|
704
|
+
return false
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (inputPaths.length === 0 || inputPaths.some((inputPath) => inputPath === stdinWithPath.path)) {
|
|
708
|
+
return false
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const [outputErr, outputStat] = await tryCatch(fsp.stat(outputPath))
|
|
712
|
+
if (outputErr != null || outputStat == null) {
|
|
713
|
+
return false
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (inputPaths.length === 1) {
|
|
717
|
+
if (singleInputReference === 'output-plan') {
|
|
718
|
+
return isMeaningfullyNewer(outputStat.mtime, outputPlanMtime)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const [inputErr, inputStat] = await tryCatch(fsp.stat(inputPaths[0]))
|
|
722
|
+
if (inputErr != null || inputStat == null) {
|
|
723
|
+
return false
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return isMeaningfullyNewer(outputStat.mtime, inputStat.mtime)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const inputStats = await Promise.all(
|
|
730
|
+
inputPaths.map(async (inputPath) => {
|
|
731
|
+
const [inputErr, inputStat] = await tryCatch(fsp.stat(inputPath))
|
|
732
|
+
if (inputErr != null || inputStat == null) {
|
|
733
|
+
return null
|
|
734
|
+
}
|
|
735
|
+
return inputStat
|
|
736
|
+
}),
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
if (inputStats.some((inputStat) => inputStat == null)) {
|
|
740
|
+
return false
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return inputStats.every((inputStat) => {
|
|
744
|
+
return inputStat != null && isMeaningfullyNewer(outputStat.mtime, inputStat.mtime)
|
|
745
|
+
})
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
async function materializeAssemblyResults({
|
|
749
|
+
abortSignal,
|
|
750
|
+
hasDirectoryInput,
|
|
751
|
+
inPath,
|
|
752
|
+
inputs,
|
|
753
|
+
normalizedResults,
|
|
754
|
+
outputMode,
|
|
755
|
+
outputPath,
|
|
756
|
+
outputRoot,
|
|
757
|
+
outputRootIsDirectory,
|
|
758
|
+
outputctl,
|
|
759
|
+
reservedPaths,
|
|
760
|
+
singleAssembly,
|
|
761
|
+
}: {
|
|
762
|
+
abortSignal: AbortSignal
|
|
763
|
+
hasDirectoryInput: boolean
|
|
764
|
+
inPath: string | null
|
|
765
|
+
inputs: string[]
|
|
766
|
+
normalizedResults: NormalizedAssemblyResults
|
|
767
|
+
outputMode?: 'directory' | 'file'
|
|
768
|
+
outputPath: string | null
|
|
769
|
+
outputRoot: string | null
|
|
770
|
+
outputRootIsDirectory: boolean
|
|
771
|
+
outputctl: IOutputCtl
|
|
772
|
+
reservedPaths: Set<string>
|
|
773
|
+
singleAssembly?: boolean
|
|
774
|
+
}): Promise<void> {
|
|
775
|
+
if (outputRoot == null) {
|
|
776
|
+
return
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const targets = await resolveResultDownloadTargets({
|
|
780
|
+
hasDirectoryInput,
|
|
781
|
+
inPath,
|
|
782
|
+
inputs,
|
|
783
|
+
normalizedResults,
|
|
784
|
+
outputMode,
|
|
785
|
+
outputPath,
|
|
786
|
+
outputRoot,
|
|
787
|
+
outputRootIsDirectory,
|
|
788
|
+
reservedPaths,
|
|
789
|
+
singleAssembly,
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
for (const { resultUrl, targetPath } of targets) {
|
|
793
|
+
outputctl.debug('DOWNLOADING')
|
|
794
|
+
const [dlErr] = await tryCatch(
|
|
795
|
+
targetPath == null
|
|
796
|
+
? downloadResultToStdout(resultUrl, abortSignal)
|
|
797
|
+
: downloadResultToFile(resultUrl, targetPath, abortSignal),
|
|
798
|
+
)
|
|
799
|
+
if (dlErr) {
|
|
800
|
+
if (dlErr.name === 'AbortError') {
|
|
801
|
+
continue
|
|
802
|
+
}
|
|
803
|
+
outputctl.error(dlErr.message)
|
|
804
|
+
throw dlErr
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
412
809
|
class MyEventEmitter extends EventEmitter {
|
|
413
810
|
protected hasEnded: boolean
|
|
414
811
|
|
|
@@ -428,29 +825,25 @@ class MyEventEmitter extends EventEmitter {
|
|
|
428
825
|
}
|
|
429
826
|
|
|
430
827
|
class ReaddirJobEmitter extends MyEventEmitter {
|
|
431
|
-
constructor({
|
|
432
|
-
dir,
|
|
433
|
-
streamRegistry,
|
|
434
|
-
recursive,
|
|
435
|
-
outstreamProvider,
|
|
436
|
-
topdir = dir,
|
|
437
|
-
}: ReaddirJobEmitterOptions) {
|
|
828
|
+
constructor({ dir, recursive, outputPlanProvider, topdir = dir }: ReaddirJobEmitterOptions) {
|
|
438
829
|
super()
|
|
439
830
|
|
|
440
831
|
process.nextTick(() => {
|
|
441
|
-
this.processDirectory({
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
832
|
+
this.processDirectory({
|
|
833
|
+
dir,
|
|
834
|
+
recursive,
|
|
835
|
+
outputPlanProvider,
|
|
836
|
+
topdir,
|
|
837
|
+
}).catch((err) => {
|
|
838
|
+
this.emit('error', err)
|
|
839
|
+
})
|
|
446
840
|
})
|
|
447
841
|
}
|
|
448
842
|
|
|
449
843
|
private async processDirectory({
|
|
450
844
|
dir,
|
|
451
|
-
streamRegistry,
|
|
452
845
|
recursive,
|
|
453
|
-
|
|
846
|
+
outputPlanProvider,
|
|
454
847
|
topdir,
|
|
455
848
|
}: ReaddirJobEmitterOptions & { topdir: string }): Promise<void> {
|
|
456
849
|
const files = await fsp.readdir(dir)
|
|
@@ -459,9 +852,7 @@ class ReaddirJobEmitter extends MyEventEmitter {
|
|
|
459
852
|
|
|
460
853
|
for (const filename of files) {
|
|
461
854
|
const file = path.normalize(path.join(dir, filename))
|
|
462
|
-
pendingOperations.push(
|
|
463
|
-
this.processFile({ file, streamRegistry, recursive, outstreamProvider, topdir }),
|
|
464
|
-
)
|
|
855
|
+
pendingOperations.push(this.processFile({ file, recursive, outputPlanProvider, topdir }))
|
|
465
856
|
}
|
|
466
857
|
|
|
467
858
|
await Promise.all(pendingOperations)
|
|
@@ -470,15 +861,13 @@ class ReaddirJobEmitter extends MyEventEmitter {
|
|
|
470
861
|
|
|
471
862
|
private async processFile({
|
|
472
863
|
file,
|
|
473
|
-
streamRegistry,
|
|
474
864
|
recursive = false,
|
|
475
|
-
|
|
865
|
+
outputPlanProvider,
|
|
476
866
|
topdir,
|
|
477
867
|
}: {
|
|
478
868
|
file: string
|
|
479
|
-
streamRegistry: StreamRegistry
|
|
480
869
|
recursive?: boolean
|
|
481
|
-
|
|
870
|
+
outputPlanProvider: OutputPlanProvider
|
|
482
871
|
topdir: string
|
|
483
872
|
}): Promise<void> {
|
|
484
873
|
const stats = await fsp.stat(file)
|
|
@@ -488,9 +877,8 @@ class ReaddirJobEmitter extends MyEventEmitter {
|
|
|
488
877
|
await new Promise<void>((resolve, reject) => {
|
|
489
878
|
const subdirEmitter = new ReaddirJobEmitter({
|
|
490
879
|
dir: file,
|
|
491
|
-
streamRegistry,
|
|
492
880
|
recursive,
|
|
493
|
-
|
|
881
|
+
outputPlanProvider,
|
|
494
882
|
topdir,
|
|
495
883
|
})
|
|
496
884
|
subdirEmitter.on('job', (job: Job) => this.emit('job', job))
|
|
@@ -499,67 +887,51 @@ class ReaddirJobEmitter extends MyEventEmitter {
|
|
|
499
887
|
})
|
|
500
888
|
}
|
|
501
889
|
} else {
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
const outstream = await outstreamProvider(file, topdir)
|
|
505
|
-
streamRegistry[file] = outstream ?? undefined
|
|
506
|
-
const instream = fs.createReadStream(file)
|
|
507
|
-
// Attach a no-op error handler to prevent unhandled errors if stream is destroyed
|
|
508
|
-
// before being consumed (e.g., due to output collision detection)
|
|
509
|
-
instream.on('error', () => {})
|
|
510
|
-
this.emit('job', { in: instream, out: outstream })
|
|
890
|
+
const outputPlan = await outputPlanProvider(file, topdir)
|
|
891
|
+
this.emit('job', { inputPath: getJobInputPath(file), out: outputPlan })
|
|
511
892
|
}
|
|
512
893
|
}
|
|
513
894
|
}
|
|
514
895
|
|
|
515
896
|
class SingleJobEmitter extends MyEventEmitter {
|
|
516
|
-
constructor({ file,
|
|
897
|
+
constructor({ file, outputPlanProvider }: SingleJobEmitterOptions) {
|
|
517
898
|
super()
|
|
518
899
|
|
|
519
900
|
const normalizedFile = path.normalize(file)
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
532
|
-
} else {
|
|
533
|
-
instream = fs.createReadStream(normalizedFile)
|
|
534
|
-
// Attach a no-op error handler to prevent unhandled errors if stream is destroyed
|
|
535
|
-
// before being consumed (e.g., due to output collision detection)
|
|
536
|
-
instream.on('error', () => {})
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
process.nextTick(() => {
|
|
540
|
-
this.emit('job', { in: instream, out: outstream })
|
|
541
|
-
this.emit('end')
|
|
901
|
+
outputPlanProvider(normalizedFile)
|
|
902
|
+
.then((outputPlan) => {
|
|
903
|
+
process.nextTick(() => {
|
|
904
|
+
this.emit('job', { inputPath: getJobInputPath(normalizedFile), out: outputPlan })
|
|
905
|
+
this.emit('end')
|
|
906
|
+
})
|
|
907
|
+
})
|
|
908
|
+
.catch((err: unknown) => {
|
|
909
|
+
process.nextTick(() => {
|
|
910
|
+
this.emit('error', ensureError(err))
|
|
911
|
+
})
|
|
542
912
|
})
|
|
543
|
-
})
|
|
544
913
|
}
|
|
545
914
|
}
|
|
546
915
|
|
|
547
916
|
class InputlessJobEmitter extends MyEventEmitter {
|
|
548
|
-
constructor({
|
|
549
|
-
outstreamProvider,
|
|
550
|
-
}: { streamRegistry: StreamRegistry; outstreamProvider: OutstreamProvider }) {
|
|
917
|
+
constructor({ outputPlanProvider }: { outputPlanProvider: OutputPlanProvider }) {
|
|
551
918
|
super()
|
|
552
919
|
|
|
553
920
|
process.nextTick(() => {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
921
|
+
outputPlanProvider(null)
|
|
922
|
+
.then((outputPlan) => {
|
|
923
|
+
try {
|
|
924
|
+
this.emit('job', { inputPath: null, out: outputPlan })
|
|
925
|
+
} catch (err) {
|
|
926
|
+
this.emit('error', ensureError(err))
|
|
927
|
+
return
|
|
928
|
+
}
|
|
560
929
|
|
|
561
|
-
|
|
562
|
-
|
|
930
|
+
this.emit('end')
|
|
931
|
+
})
|
|
932
|
+
.catch((err: unknown) => {
|
|
933
|
+
this.emit('error', ensureError(err))
|
|
934
|
+
})
|
|
563
935
|
})
|
|
564
936
|
}
|
|
565
937
|
}
|
|
@@ -574,10 +946,10 @@ class NullJobEmitter extends MyEventEmitter {
|
|
|
574
946
|
class WatchJobEmitter extends MyEventEmitter {
|
|
575
947
|
private watcher: NodeWatcher | null = null
|
|
576
948
|
|
|
577
|
-
constructor({ file,
|
|
949
|
+
constructor({ file, recursive, outputPlanProvider }: WatchJobEmitterOptions) {
|
|
578
950
|
super()
|
|
579
951
|
|
|
580
|
-
this.init({ file,
|
|
952
|
+
this.init({ file, recursive, outputPlanProvider }).catch((err) => {
|
|
581
953
|
this.emit('error', err)
|
|
582
954
|
})
|
|
583
955
|
|
|
@@ -597,9 +969,8 @@ class WatchJobEmitter extends MyEventEmitter {
|
|
|
597
969
|
|
|
598
970
|
private async init({
|
|
599
971
|
file,
|
|
600
|
-
streamRegistry,
|
|
601
972
|
recursive,
|
|
602
|
-
|
|
973
|
+
outputPlanProvider,
|
|
603
974
|
}: WatchJobEmitterOptions): Promise<void> {
|
|
604
975
|
const stats = await fsp.stat(file)
|
|
605
976
|
const topdir = stats.isDirectory() ? file : undefined
|
|
@@ -614,7 +985,7 @@ class WatchJobEmitter extends MyEventEmitter {
|
|
|
614
985
|
this.watcher.on('close', () => this.emit('end'))
|
|
615
986
|
this.watcher.on('change', (_evt: string, filename: string) => {
|
|
616
987
|
const normalizedFile = path.normalize(filename)
|
|
617
|
-
this.handleChange(normalizedFile, topdir,
|
|
988
|
+
this.handleChange(normalizedFile, topdir, outputPlanProvider).catch((err) => {
|
|
618
989
|
this.emit('error', err)
|
|
619
990
|
})
|
|
620
991
|
})
|
|
@@ -623,23 +994,17 @@ class WatchJobEmitter extends MyEventEmitter {
|
|
|
623
994
|
private async handleChange(
|
|
624
995
|
normalizedFile: string,
|
|
625
996
|
topdir: string | undefined,
|
|
626
|
-
|
|
627
|
-
outstreamProvider: OutstreamProvider,
|
|
997
|
+
outputPlanProvider: OutputPlanProvider,
|
|
628
998
|
): Promise<void> {
|
|
629
999
|
const stats = await fsp.stat(normalizedFile)
|
|
630
1000
|
if (stats.isDirectory()) return
|
|
631
1001
|
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const instream = fs.createReadStream(normalizedFile)
|
|
639
|
-
// Attach a no-op error handler to prevent unhandled errors if stream is destroyed
|
|
640
|
-
// before being consumed (e.g., due to output collision detection)
|
|
641
|
-
instream.on('error', () => {})
|
|
642
|
-
this.emit('job', { in: instream, out: outstream })
|
|
1002
|
+
const outputPlan = await outputPlanProvider(normalizedFile, topdir)
|
|
1003
|
+
this.emit('job', {
|
|
1004
|
+
inputPath: getJobInputPath(normalizedFile),
|
|
1005
|
+
out: outputPlan,
|
|
1006
|
+
watchEvent: true,
|
|
1007
|
+
})
|
|
643
1008
|
}
|
|
644
1009
|
}
|
|
645
1010
|
|
|
@@ -697,12 +1062,21 @@ function detectConflicts(jobEmitter: EventEmitter): MyEventEmitter {
|
|
|
697
1062
|
jobEmitter.on('end', () => emitter.emit('end'))
|
|
698
1063
|
jobEmitter.on('error', (err: Error) => emitter.emit('error', err))
|
|
699
1064
|
jobEmitter.on('job', (job: Job) => {
|
|
700
|
-
if (job.
|
|
1065
|
+
if (job.watchEvent) {
|
|
1066
|
+
emitter.emit('job', job)
|
|
1067
|
+
return
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
if (job.inputPath == null || job.out == null) {
|
|
1071
|
+
emitter.emit('job', job)
|
|
1072
|
+
return
|
|
1073
|
+
}
|
|
1074
|
+
const inPath = job.inputPath
|
|
1075
|
+
const outPath = job.out.path
|
|
1076
|
+
if (outPath == null) {
|
|
701
1077
|
emitter.emit('job', job)
|
|
702
1078
|
return
|
|
703
1079
|
}
|
|
704
|
-
const inPath = (job.in as fs.ReadStream).path as string
|
|
705
|
-
const outPath = job.out.path as string
|
|
706
1080
|
if (Object.hasOwn(outfileAssociations, outPath) && outfileAssociations[outPath] !== inPath) {
|
|
707
1081
|
emitter.emit(
|
|
708
1082
|
'error',
|
|
@@ -724,12 +1098,12 @@ function dismissStaleJobs(jobEmitter: EventEmitter): MyEventEmitter {
|
|
|
724
1098
|
jobEmitter.on('end', () => Promise.all(pendingChecks).then(() => emitter.emit('end')))
|
|
725
1099
|
jobEmitter.on('error', (err: Error) => emitter.emit('error', err))
|
|
726
1100
|
jobEmitter.on('job', (job: Job) => {
|
|
727
|
-
if (job.
|
|
1101
|
+
if (job.inputPath == null || job.out == null) {
|
|
728
1102
|
emitter.emit('job', job)
|
|
729
1103
|
return
|
|
730
1104
|
}
|
|
731
1105
|
|
|
732
|
-
const inPath =
|
|
1106
|
+
const inPath = job.inputPath
|
|
733
1107
|
const checkPromise = fsp
|
|
734
1108
|
.stat(inPath)
|
|
735
1109
|
.then((stats) => {
|
|
@@ -747,12 +1121,23 @@ function dismissStaleJobs(jobEmitter: EventEmitter): MyEventEmitter {
|
|
|
747
1121
|
return emitter
|
|
748
1122
|
}
|
|
749
1123
|
|
|
1124
|
+
function passthroughJobs(jobEmitter: EventEmitter): MyEventEmitter {
|
|
1125
|
+
const emitter = new MyEventEmitter()
|
|
1126
|
+
|
|
1127
|
+
jobEmitter.on('end', () => emitter.emit('end'))
|
|
1128
|
+
jobEmitter.on('error', (err: Error) => emitter.emit('error', err))
|
|
1129
|
+
jobEmitter.on('job', (job: Job) => emitter.emit('job', job))
|
|
1130
|
+
|
|
1131
|
+
return emitter
|
|
1132
|
+
}
|
|
1133
|
+
|
|
750
1134
|
function makeJobEmitter(
|
|
751
1135
|
inputs: string[],
|
|
752
1136
|
{
|
|
1137
|
+
allowOutputCollisions,
|
|
753
1138
|
recursive,
|
|
754
|
-
|
|
755
|
-
|
|
1139
|
+
outputPlanProvider,
|
|
1140
|
+
singleAssembly,
|
|
756
1141
|
watch: watchOption,
|
|
757
1142
|
reprocessStale,
|
|
758
1143
|
}: JobEmitterOptions,
|
|
@@ -765,35 +1150,43 @@ function makeJobEmitter(
|
|
|
765
1150
|
async function processInputs(): Promise<void> {
|
|
766
1151
|
for (const input of inputs) {
|
|
767
1152
|
if (input === '-') {
|
|
768
|
-
emitterFns.push(
|
|
769
|
-
() => new SingleJobEmitter({ file: input, outstreamProvider, streamRegistry }),
|
|
770
|
-
)
|
|
1153
|
+
emitterFns.push(() => new SingleJobEmitter({ file: input, outputPlanProvider }))
|
|
771
1154
|
watcherFns.push(() => new NullJobEmitter())
|
|
772
1155
|
} else {
|
|
773
1156
|
const stats = await fsp.stat(input)
|
|
774
1157
|
if (stats.isDirectory()) {
|
|
775
1158
|
emitterFns.push(
|
|
776
1159
|
() =>
|
|
777
|
-
new ReaddirJobEmitter({
|
|
1160
|
+
new ReaddirJobEmitter({
|
|
1161
|
+
dir: input,
|
|
1162
|
+
recursive,
|
|
1163
|
+
outputPlanProvider,
|
|
1164
|
+
}),
|
|
778
1165
|
)
|
|
779
1166
|
watcherFns.push(
|
|
780
1167
|
() =>
|
|
781
|
-
new WatchJobEmitter({
|
|
1168
|
+
new WatchJobEmitter({
|
|
1169
|
+
file: input,
|
|
1170
|
+
recursive,
|
|
1171
|
+
outputPlanProvider,
|
|
1172
|
+
}),
|
|
782
1173
|
)
|
|
783
1174
|
} else {
|
|
784
|
-
emitterFns.push(
|
|
785
|
-
() => new SingleJobEmitter({ file: input, outstreamProvider, streamRegistry }),
|
|
786
|
-
)
|
|
1175
|
+
emitterFns.push(() => new SingleJobEmitter({ file: input, outputPlanProvider }))
|
|
787
1176
|
watcherFns.push(
|
|
788
1177
|
() =>
|
|
789
|
-
new WatchJobEmitter({
|
|
1178
|
+
new WatchJobEmitter({
|
|
1179
|
+
file: input,
|
|
1180
|
+
recursive,
|
|
1181
|
+
outputPlanProvider,
|
|
1182
|
+
}),
|
|
790
1183
|
)
|
|
791
1184
|
}
|
|
792
1185
|
}
|
|
793
1186
|
}
|
|
794
1187
|
|
|
795
1188
|
if (inputs.length === 0) {
|
|
796
|
-
emitterFns.push(() => new InputlessJobEmitter({
|
|
1189
|
+
emitterFns.push(() => new InputlessJobEmitter({ outputPlanProvider }))
|
|
797
1190
|
}
|
|
798
1191
|
|
|
799
1192
|
startEmitting()
|
|
@@ -818,14 +1211,18 @@ function makeJobEmitter(
|
|
|
818
1211
|
emitter.emit('error', err)
|
|
819
1212
|
})
|
|
820
1213
|
|
|
821
|
-
const
|
|
822
|
-
|
|
1214
|
+
const conflictFilter = allowOutputCollisions ? passthroughJobs : detectConflicts
|
|
1215
|
+
const staleFilter = reprocessStale || singleAssembly ? passthroughJobs : dismissStaleJobs
|
|
1216
|
+
|
|
1217
|
+
return staleFilter(conflictFilter(emitter))
|
|
823
1218
|
}
|
|
824
1219
|
|
|
825
1220
|
export interface AssembliesCreateOptions {
|
|
826
1221
|
steps?: string
|
|
1222
|
+
stepsData?: StepsInput
|
|
827
1223
|
template?: string
|
|
828
1224
|
fields?: Record<string, string>
|
|
1225
|
+
outputMode?: 'directory' | 'file'
|
|
829
1226
|
watch?: boolean
|
|
830
1227
|
recursive?: boolean
|
|
831
1228
|
inputs: string[]
|
|
@@ -844,8 +1241,10 @@ export async function create(
|
|
|
844
1241
|
client: Transloadit,
|
|
845
1242
|
{
|
|
846
1243
|
steps,
|
|
1244
|
+
stepsData,
|
|
847
1245
|
template,
|
|
848
1246
|
fields,
|
|
1247
|
+
outputMode,
|
|
849
1248
|
watch: watchOption,
|
|
850
1249
|
recursive,
|
|
851
1250
|
inputs,
|
|
@@ -855,35 +1254,18 @@ export async function create(
|
|
|
855
1254
|
singleAssembly,
|
|
856
1255
|
concurrency = DEFAULT_CONCURRENCY,
|
|
857
1256
|
}: AssembliesCreateOptions,
|
|
858
|
-
): Promise<{ results: unknown[]; hasFailures: boolean }> {
|
|
1257
|
+
): Promise<{ resultUrls: ResultUrlRow[]; results: unknown[]; hasFailures: boolean }> {
|
|
859
1258
|
// Quick fix for https://github.com/transloadit/transloadify/issues/13
|
|
860
1259
|
// Only default to stdout when output is undefined (not provided), not when explicitly null
|
|
861
1260
|
let resolvedOutput = output
|
|
862
1261
|
if (resolvedOutput === undefined && !process.stdout.isTTY) resolvedOutput = '-'
|
|
863
1262
|
|
|
864
1263
|
// Read steps file async before entering the Promise constructor
|
|
865
|
-
// We use StepsInput (the input type) rather than
|
|
1264
|
+
// We use StepsInput (the input type) rather than the transformed output type
|
|
866
1265
|
// to avoid zod adding default values that the API may reject
|
|
867
|
-
let
|
|
1266
|
+
let effectiveStepsData = stepsData
|
|
868
1267
|
if (steps) {
|
|
869
|
-
|
|
870
|
-
const parsed: unknown = JSON.parse(stepsContent)
|
|
871
|
-
// Basic structural validation: must be an object with step names as keys
|
|
872
|
-
if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
873
|
-
throw new Error('Invalid steps format: expected an object with step names as keys')
|
|
874
|
-
}
|
|
875
|
-
// Validate each step has a robot field
|
|
876
|
-
for (const [stepName, step] of Object.entries(parsed)) {
|
|
877
|
-
if (step == null || typeof step !== 'object' || Array.isArray(step)) {
|
|
878
|
-
throw new Error(`Invalid steps format: step '${stepName}' must be an object`)
|
|
879
|
-
}
|
|
880
|
-
if (!('robot' in step) || typeof (step as Record<string, unknown>).robot !== 'string') {
|
|
881
|
-
throw new Error(
|
|
882
|
-
`Invalid steps format: step '${stepName}' must have a 'robot' string property`,
|
|
883
|
-
)
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
stepsData = parsed as StepsInput
|
|
1268
|
+
effectiveStepsData = await readStepsInputFile(steps)
|
|
887
1269
|
}
|
|
888
1270
|
|
|
889
1271
|
// Determine output stat async before entering the Promise constructor
|
|
@@ -891,9 +1273,19 @@ export async function create(
|
|
|
891
1273
|
if (resolvedOutput != null) {
|
|
892
1274
|
const [err, stat] = await tryCatch(myStat(process.stdout, resolvedOutput))
|
|
893
1275
|
if (err && (!isErrnoException(err) || err.code !== 'ENOENT')) throw err
|
|
894
|
-
outstat =
|
|
1276
|
+
outstat =
|
|
1277
|
+
stat ??
|
|
1278
|
+
({
|
|
1279
|
+
isDirectory: () => outputMode === 'directory',
|
|
1280
|
+
} satisfies StatLike)
|
|
1281
|
+
|
|
1282
|
+
if (outputMode === 'directory' && stat != null && !stat.isDirectory()) {
|
|
1283
|
+
const msg = 'Output must be a directory for this command'
|
|
1284
|
+
outputctl.error(msg)
|
|
1285
|
+
throw new Error(msg)
|
|
1286
|
+
}
|
|
895
1287
|
|
|
896
|
-
if (!outstat.isDirectory() && inputs.length !== 0) {
|
|
1288
|
+
if (!outstat.isDirectory() && inputs.length !== 0 && !singleAssembly) {
|
|
897
1289
|
const firstInput = inputs[0]
|
|
898
1290
|
if (firstInput) {
|
|
899
1291
|
const firstInputStat = await myStat(process.stdin, firstInput)
|
|
@@ -906,333 +1298,294 @@ export async function create(
|
|
|
906
1298
|
}
|
|
907
1299
|
}
|
|
908
1300
|
|
|
1301
|
+
const inputStats = await Promise.all(
|
|
1302
|
+
inputs.map(async (input) => {
|
|
1303
|
+
if (input === '-') return null
|
|
1304
|
+
return await myStat(process.stdin, input)
|
|
1305
|
+
}),
|
|
1306
|
+
)
|
|
1307
|
+
const hasDirectoryInput = inputStats.some((stat) => stat?.isDirectory() === true)
|
|
1308
|
+
|
|
909
1309
|
return new Promise((resolve, reject) => {
|
|
910
1310
|
const params: CreateAssemblyParams = (
|
|
911
|
-
|
|
1311
|
+
effectiveStepsData
|
|
1312
|
+
? { steps: effectiveStepsData as CreateAssemblyParams['steps'] }
|
|
1313
|
+
: { template_id: template }
|
|
912
1314
|
) as CreateAssemblyParams
|
|
913
1315
|
if (fields) {
|
|
914
1316
|
params.fields = fields
|
|
915
1317
|
}
|
|
916
1318
|
|
|
917
|
-
const
|
|
1319
|
+
const outputPlanProvider: OutputPlanProvider =
|
|
918
1320
|
resolvedOutput == null
|
|
919
1321
|
? nullProvider()
|
|
920
1322
|
: outstat?.isDirectory()
|
|
921
1323
|
? dirProvider(resolvedOutput)
|
|
922
1324
|
: fileProvider(resolvedOutput)
|
|
923
|
-
const streamRegistry: StreamRegistry = {}
|
|
924
1325
|
|
|
925
1326
|
const emitter = makeJobEmitter(inputs, {
|
|
1327
|
+
allowOutputCollisions: singleAssembly,
|
|
1328
|
+
outputPlanProvider,
|
|
926
1329
|
recursive,
|
|
927
1330
|
watch: watchOption,
|
|
928
|
-
|
|
929
|
-
streamRegistry,
|
|
1331
|
+
singleAssembly,
|
|
930
1332
|
reprocessStale,
|
|
931
1333
|
})
|
|
932
1334
|
|
|
933
1335
|
// Use p-queue for concurrency management
|
|
934
1336
|
const queue = new PQueue({ concurrency })
|
|
935
1337
|
const results: unknown[] = []
|
|
1338
|
+
const resultUrls: ResultUrlRow[] = []
|
|
1339
|
+
const reservedResultPaths = new Set<string>()
|
|
1340
|
+
const latestWatchJobTokenByOutputPath = new Map<string, number>()
|
|
936
1341
|
let hasFailures = false
|
|
1342
|
+
let nextWatchJobToken = 0
|
|
937
1343
|
// AbortController to cancel all in-flight createAssembly calls when an error occurs
|
|
938
1344
|
const abortController = new AbortController()
|
|
1345
|
+
const outputRootIsDirectory = Boolean(resolvedOutput != null && outstat?.isDirectory())
|
|
939
1346
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
// When writing to a file path (non-directory output), we treat finish as a supersede signal.
|
|
954
|
-
// Directory-output multi-download mode does not use a single shared outstream.
|
|
955
|
-
const markSupersededOnFinish = (stream: OutStream) => {
|
|
956
|
-
stream.on('finish', () => {
|
|
957
|
-
superceded = true
|
|
958
|
-
})
|
|
1347
|
+
function reserveWatchJobToken(outputPath: string | null): number | null {
|
|
1348
|
+
if (!watchOption || outputPath == null) {
|
|
1349
|
+
return null
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
const token = ++nextWatchJobToken
|
|
1353
|
+
latestWatchJobTokenByOutputPath.set(outputPath, token)
|
|
1354
|
+
return token
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function isSupersededWatchJob(outputPath: string | null, token: number | null): boolean {
|
|
1358
|
+
if (!watchOption || outputPath == null || token == null) {
|
|
1359
|
+
return false
|
|
959
1360
|
}
|
|
960
1361
|
|
|
1362
|
+
return latestWatchJobTokenByOutputPath.get(outputPath) !== token
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function createAssemblyOptions({
|
|
1366
|
+
files,
|
|
1367
|
+
uploads,
|
|
1368
|
+
}: {
|
|
1369
|
+
files?: Record<string, string>
|
|
1370
|
+
uploads?: Record<string, Readable>
|
|
1371
|
+
} = {}): CreateAssemblyOptions {
|
|
961
1372
|
const createOptions: CreateAssemblyOptions = {
|
|
962
1373
|
params,
|
|
963
1374
|
signal: abortController.signal,
|
|
964
1375
|
}
|
|
965
|
-
if (
|
|
966
|
-
createOptions.
|
|
1376
|
+
if (files != null && Object.keys(files).length > 0) {
|
|
1377
|
+
createOptions.files = files
|
|
967
1378
|
}
|
|
1379
|
+
if (uploads != null && Object.keys(uploads).length > 0) {
|
|
1380
|
+
createOptions.uploads = uploads
|
|
1381
|
+
}
|
|
1382
|
+
return createOptions
|
|
1383
|
+
}
|
|
968
1384
|
|
|
1385
|
+
async function awaitCompletedAssembly(createOptions: CreateAssemblyOptions): Promise<{
|
|
1386
|
+
assembly: Awaited<ReturnType<typeof client.awaitAssemblyCompletion>>
|
|
1387
|
+
assemblyId: string
|
|
1388
|
+
}> {
|
|
969
1389
|
const result = await client.createAssembly(createOptions)
|
|
970
|
-
if (superceded) return undefined
|
|
971
|
-
|
|
972
1390
|
const assemblyId = result.assembly_id
|
|
973
1391
|
if (!assemblyId) throw new Error('No assembly_id in result')
|
|
974
1392
|
|
|
975
1393
|
const assembly = await client.awaitAssemblyCompletion(assemblyId, {
|
|
976
1394
|
signal: abortController.signal,
|
|
977
|
-
onPoll: () =>
|
|
978
|
-
if (superceded) return false
|
|
979
|
-
return true
|
|
980
|
-
},
|
|
1395
|
+
onPoll: () => true,
|
|
981
1396
|
onAssemblyProgress: (status) => {
|
|
982
1397
|
outputctl.debug(`Assembly status: ${status.ok}`)
|
|
983
1398
|
},
|
|
984
1399
|
})
|
|
985
1400
|
|
|
986
|
-
if (superceded) return undefined
|
|
987
|
-
|
|
988
1401
|
if (assembly.error || (assembly.ok && assembly.ok !== 'ASSEMBLY_COMPLETED')) {
|
|
989
1402
|
const msg = `Assembly failed: ${assembly.error || assembly.message} (Status: ${assembly.ok})`
|
|
990
1403
|
outputctl.error(msg)
|
|
991
1404
|
throw new Error(msg)
|
|
992
1405
|
}
|
|
993
1406
|
|
|
1407
|
+
return { assembly, assemblyId }
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
async function executeAssemblyLifecycle({
|
|
1411
|
+
createOptions,
|
|
1412
|
+
inPath,
|
|
1413
|
+
inputPaths,
|
|
1414
|
+
outputPlan,
|
|
1415
|
+
outputToken,
|
|
1416
|
+
singleAssemblyMode,
|
|
1417
|
+
}: {
|
|
1418
|
+
createOptions: CreateAssemblyOptions
|
|
1419
|
+
inPath: string | null
|
|
1420
|
+
inputPaths: string[]
|
|
1421
|
+
outputPlan: OutputPlan | null
|
|
1422
|
+
outputToken: number | null
|
|
1423
|
+
singleAssemblyMode?: boolean
|
|
1424
|
+
}): Promise<unknown> {
|
|
1425
|
+
outputctl.debug(`PROCESSING JOB ${inPath ?? 'null'} ${outputPlan?.path ?? 'null'}`)
|
|
1426
|
+
|
|
1427
|
+
const { assembly, assemblyId } = await awaitCompletedAssembly(createOptions)
|
|
994
1428
|
if (!assembly.results) throw new Error('No results in assembly')
|
|
1429
|
+
const normalizedResults = normalizeAssemblyResults(assembly.results)
|
|
995
1430
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
}> = []
|
|
1002
|
-
for (const [stepName, stepResults] of entries) {
|
|
1003
|
-
for (const file of stepResults as Array<{
|
|
1004
|
-
name?: string
|
|
1005
|
-
basename?: string
|
|
1006
|
-
ext?: string
|
|
1007
|
-
ssl_url?: string
|
|
1008
|
-
url?: string
|
|
1009
|
-
}>) {
|
|
1010
|
-
allFiles.push({ stepName, file })
|
|
1011
|
-
}
|
|
1431
|
+
if (isSupersededWatchJob(outputPlan?.path ?? null, outputToken)) {
|
|
1432
|
+
outputctl.debug(
|
|
1433
|
+
`SKIPPED SUPERSEDED WATCH RESULT ${inPath ?? 'null'} ${outputPlan?.path ?? 'null'}`,
|
|
1434
|
+
)
|
|
1435
|
+
return assembly
|
|
1012
1436
|
}
|
|
1013
1437
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1438
|
+
if (
|
|
1439
|
+
!singleAssemblyMode &&
|
|
1440
|
+
!watchOption &&
|
|
1441
|
+
(await shouldSkipStaleOutput({
|
|
1442
|
+
inputPaths,
|
|
1443
|
+
outputPath: outputPlan?.path ?? null,
|
|
1444
|
+
outputPlanMtime: outputPlan?.mtime ?? new Date(0),
|
|
1445
|
+
outputRootIsDirectory,
|
|
1446
|
+
reprocessStale,
|
|
1447
|
+
}))
|
|
1448
|
+
) {
|
|
1449
|
+
outputctl.debug(`SKIPPED STALE RESULT ${inPath ?? 'null'} ${outputPlan?.path ?? 'null'}`)
|
|
1450
|
+
return assembly
|
|
1020
1451
|
}
|
|
1021
1452
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1453
|
+
resultUrls.push(...collectNormalizedResultUrlRows({ assemblyId, normalizedResults }))
|
|
1454
|
+
|
|
1455
|
+
await materializeAssemblyResults({
|
|
1456
|
+
abortSignal: abortController.signal,
|
|
1457
|
+
hasDirectoryInput: singleAssemblyMode ? false : hasDirectoryInput,
|
|
1458
|
+
inPath,
|
|
1459
|
+
inputs: inputPaths,
|
|
1460
|
+
normalizedResults,
|
|
1461
|
+
outputMode,
|
|
1462
|
+
outputPath: outputPlan?.path ?? null,
|
|
1463
|
+
outputRoot: resolvedOutput ?? null,
|
|
1464
|
+
outputRootIsDirectory,
|
|
1465
|
+
outputctl,
|
|
1466
|
+
reservedPaths: reservedResultPaths,
|
|
1467
|
+
singleAssembly: singleAssemblyMode,
|
|
1468
|
+
})
|
|
1033
1469
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
if (inPath != null) {
|
|
1041
|
-
let relpath = path.relative(process.cwd(), inPath)
|
|
1042
|
-
relpath = relpath.replace(/^(\.\.\/)+/, '')
|
|
1043
|
-
baseDir = path.join(resolvedOutput, path.dirname(relpath), path.parse(relpath).name)
|
|
1044
|
-
}
|
|
1045
|
-
await fsp.mkdir(baseDir, { recursive: true })
|
|
1046
|
-
|
|
1047
|
-
for (const { stepName, file } of allFiles) {
|
|
1048
|
-
const resultUrl = getFileUrl(file)
|
|
1049
|
-
if (!resultUrl) continue
|
|
1050
|
-
|
|
1051
|
-
const stepDir = path.join(baseDir, stepName)
|
|
1052
|
-
await fsp.mkdir(stepDir, { recursive: true })
|
|
1053
|
-
|
|
1054
|
-
const rawName =
|
|
1055
|
-
file.name ??
|
|
1056
|
-
(file.basename && file.ext ? `${file.basename}.${file.ext}` : undefined) ??
|
|
1057
|
-
`${stepName}_result`
|
|
1058
|
-
const safeName = sanitizeName(rawName)
|
|
1059
|
-
const targetPath = await ensureUniquePath(path.join(stepDir, safeName))
|
|
1060
|
-
|
|
1061
|
-
outputctl.debug('DOWNLOADING')
|
|
1062
|
-
const outStream = fs.createWriteStream(targetPath) as OutStream
|
|
1063
|
-
outStream.on('error', () => {})
|
|
1064
|
-
const [dlErr] = await tryCatch(
|
|
1065
|
-
pipeline(got.stream(resultUrl, { signal: abortController.signal }), outStream),
|
|
1066
|
-
)
|
|
1067
|
-
if (dlErr) {
|
|
1068
|
-
if (dlErr.name === 'AbortError') continue
|
|
1069
|
-
outputctl.error(dlErr.message)
|
|
1070
|
-
throw dlErr
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
} else if (!outIsDirectory && outPath != null) {
|
|
1074
|
-
const first = allFiles[0]
|
|
1075
|
-
const resultUrl = first ? getFileUrl(first.file) : null
|
|
1076
|
-
if (resultUrl) {
|
|
1077
|
-
outputctl.debug('DOWNLOADING')
|
|
1078
|
-
const outStream = fs.createWriteStream(outPath) as OutStream
|
|
1079
|
-
outStream.on('error', () => {})
|
|
1080
|
-
outStream.mtime = outMtime
|
|
1081
|
-
markSupersededOnFinish(outStream)
|
|
1082
|
-
|
|
1083
|
-
const [dlErr] = await tryCatch(
|
|
1084
|
-
pipeline(got.stream(resultUrl, { signal: abortController.signal }), outStream),
|
|
1085
|
-
)
|
|
1086
|
-
if (dlErr) {
|
|
1087
|
-
if (dlErr.name !== 'AbortError') {
|
|
1088
|
-
outputctl.error(dlErr.message)
|
|
1089
|
-
throw dlErr
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
} else if (outIsDirectory && outPath != null) {
|
|
1094
|
-
// Single-result, input-backed job: preserve existing file mapping in outdir.
|
|
1095
|
-
const first = allFiles[0]
|
|
1096
|
-
const resultUrl = first ? getFileUrl(first.file) : null
|
|
1097
|
-
if (resultUrl) {
|
|
1098
|
-
outputctl.debug('DOWNLOADING')
|
|
1099
|
-
const outStream = fs.createWriteStream(outPath) as OutStream
|
|
1100
|
-
outStream.on('error', () => {})
|
|
1101
|
-
outStream.mtime = outMtime
|
|
1102
|
-
markSupersededOnFinish(outStream)
|
|
1103
|
-
|
|
1104
|
-
const [dlErr] = await tryCatch(
|
|
1105
|
-
pipeline(got.stream(resultUrl, { signal: abortController.signal }), outStream),
|
|
1106
|
-
)
|
|
1107
|
-
if (dlErr) {
|
|
1108
|
-
if (dlErr.name !== 'AbortError') {
|
|
1109
|
-
outputctl.error(dlErr.message)
|
|
1110
|
-
throw dlErr
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1470
|
+
outputctl.debug(`COMPLETED ${inPath ?? 'null'} ${outputPlan?.path ?? 'null'}`)
|
|
1471
|
+
|
|
1472
|
+
if (del) {
|
|
1473
|
+
for (const inputPath of inputPaths) {
|
|
1474
|
+
if (inputPath === stdinWithPath.path) {
|
|
1475
|
+
continue
|
|
1113
1476
|
}
|
|
1477
|
+
await fsp.unlink(inputPath)
|
|
1114
1478
|
}
|
|
1115
1479
|
}
|
|
1480
|
+
return assembly
|
|
1481
|
+
}
|
|
1116
1482
|
|
|
1117
|
-
|
|
1483
|
+
// Helper to process a single assembly job
|
|
1484
|
+
async function processAssemblyJob(
|
|
1485
|
+
inPath: string | null,
|
|
1486
|
+
outputPlan: OutputPlan | null,
|
|
1487
|
+
outputToken: number | null,
|
|
1488
|
+
): Promise<unknown> {
|
|
1489
|
+
const files =
|
|
1490
|
+
inPath != null && inPath !== stdinWithPath.path
|
|
1491
|
+
? {
|
|
1492
|
+
in: inPath,
|
|
1493
|
+
}
|
|
1494
|
+
: undefined
|
|
1495
|
+
const uploads =
|
|
1496
|
+
inPath === stdinWithPath.path
|
|
1497
|
+
? {
|
|
1498
|
+
in: createInputUploadStream(inPath),
|
|
1499
|
+
}
|
|
1500
|
+
: undefined
|
|
1501
|
+
|
|
1502
|
+
return await executeAssemblyLifecycle({
|
|
1503
|
+
createOptions: createAssemblyOptions({ files, uploads }),
|
|
1504
|
+
inPath,
|
|
1505
|
+
inputPaths: inPath == null ? [] : [inPath],
|
|
1506
|
+
outputPlan,
|
|
1507
|
+
outputToken,
|
|
1508
|
+
})
|
|
1509
|
+
}
|
|
1118
1510
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1511
|
+
function handleEmitterError(err: Error): void {
|
|
1512
|
+
abortController.abort()
|
|
1513
|
+
queue.clear()
|
|
1514
|
+
outputctl.error(err)
|
|
1515
|
+
reject(err)
|
|
1123
1516
|
}
|
|
1124
1517
|
|
|
1125
|
-
|
|
1126
|
-
// Single-assembly mode: collect file paths, then create one assembly with all inputs
|
|
1127
|
-
// We close streams immediately to avoid exhausting file descriptors with many files
|
|
1518
|
+
function runSingleAssemblyEmitter(): void {
|
|
1128
1519
|
const collectedPaths: string[] = []
|
|
1520
|
+
let inputlessOutputPlan: OutputPlan | null = null
|
|
1129
1521
|
|
|
1130
1522
|
emitter.on('job', (job: Job) => {
|
|
1131
|
-
if (job.
|
|
1132
|
-
const inPath =
|
|
1523
|
+
if (job.inputPath != null) {
|
|
1524
|
+
const inPath = job.inputPath
|
|
1133
1525
|
outputctl.debug(`COLLECTING JOB ${inPath}`)
|
|
1134
1526
|
collectedPaths.push(inPath)
|
|
1135
|
-
|
|
1136
|
-
;(job.in as fs.ReadStream).destroy()
|
|
1137
|
-
outputctl.debug(`STREAM CLOSED ${inPath}`)
|
|
1527
|
+
return
|
|
1138
1528
|
}
|
|
1139
|
-
})
|
|
1140
1529
|
|
|
1141
|
-
|
|
1142
|
-
abortController.abort()
|
|
1143
|
-
queue.clear()
|
|
1144
|
-
outputctl.error(err)
|
|
1145
|
-
reject(err)
|
|
1530
|
+
inputlessOutputPlan = job.out ?? null
|
|
1146
1531
|
})
|
|
1147
1532
|
|
|
1148
1533
|
emitter.on('end', async () => {
|
|
1149
|
-
if (
|
|
1150
|
-
|
|
1534
|
+
if (
|
|
1535
|
+
await shouldSkipStaleOutput({
|
|
1536
|
+
inputPaths: collectedPaths,
|
|
1537
|
+
outputPath: resolvedOutput ?? null,
|
|
1538
|
+
outputPlanMtime: new Date(0),
|
|
1539
|
+
outputRootIsDirectory,
|
|
1540
|
+
reprocessStale,
|
|
1541
|
+
singleInputReference: 'input',
|
|
1542
|
+
})
|
|
1543
|
+
) {
|
|
1544
|
+
outputctl.debug(`SKIPPED STALE SINGLE ASSEMBLY ${resolvedOutput ?? 'null'}`)
|
|
1545
|
+
resolve({ resultUrls, results: [], hasFailures: false })
|
|
1151
1546
|
return
|
|
1152
1547
|
}
|
|
1153
1548
|
|
|
1154
|
-
//
|
|
1549
|
+
// Preserve original basenames/extensions for filesystem uploads so the backend
|
|
1550
|
+
// can infer types like Markdown correctly.
|
|
1551
|
+
const files: Record<string, string> = {}
|
|
1155
1552
|
const uploads: Record<string, Readable> = {}
|
|
1156
1553
|
const inputPaths: string[] = []
|
|
1157
1554
|
for (const inPath of collectedPaths) {
|
|
1158
1555
|
const basename = path.basename(inPath)
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
counter
|
|
1556
|
+
const collection = inPath === stdinWithPath.path ? uploads : files
|
|
1557
|
+
const key = await ensureUniqueCounterValue({
|
|
1558
|
+
initialValue: basename,
|
|
1559
|
+
isTaken: (candidate) => candidate in collection,
|
|
1560
|
+
nextValue: (counter) =>
|
|
1561
|
+
`${path.parse(basename).name}_${counter}${path.parse(basename).ext}`,
|
|
1562
|
+
reserve: () => {},
|
|
1563
|
+
scope: collection,
|
|
1564
|
+
})
|
|
1565
|
+
if (inPath === stdinWithPath.path) {
|
|
1566
|
+
uploads[key] = createInputUploadStream(inPath)
|
|
1567
|
+
} else {
|
|
1568
|
+
files[key] = inPath
|
|
1164
1569
|
}
|
|
1165
|
-
uploads[key] = fs.createReadStream(inPath)
|
|
1166
1570
|
inputPaths.push(inPath)
|
|
1167
1571
|
}
|
|
1168
1572
|
|
|
1169
|
-
outputctl.debug(
|
|
1573
|
+
outputctl.debug(
|
|
1574
|
+
`Creating single assembly with ${Object.keys(files).length + Object.keys(uploads).length} files`,
|
|
1575
|
+
)
|
|
1170
1576
|
|
|
1171
1577
|
try {
|
|
1172
1578
|
const assembly = await queue.add(async () => {
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
const assemblyId = result.assembly_id
|
|
1183
|
-
if (!assemblyId) throw new Error('No assembly_id in result')
|
|
1184
|
-
|
|
1185
|
-
const asm = await client.awaitAssemblyCompletion(assemblyId, {
|
|
1186
|
-
signal: abortController.signal,
|
|
1187
|
-
onAssemblyProgress: (status) => {
|
|
1188
|
-
outputctl.debug(`Assembly status: ${status.ok}`)
|
|
1189
|
-
},
|
|
1579
|
+
return await executeAssemblyLifecycle({
|
|
1580
|
+
createOptions: createAssemblyOptions({ files, uploads }),
|
|
1581
|
+
inPath: null,
|
|
1582
|
+
inputPaths,
|
|
1583
|
+
outputPlan:
|
|
1584
|
+
inputlessOutputPlan ??
|
|
1585
|
+
(resolvedOutput == null ? null : createOutputPlan(resolvedOutput, new Date(0))),
|
|
1586
|
+
outputToken: null,
|
|
1587
|
+
singleAssemblyMode: true,
|
|
1190
1588
|
})
|
|
1191
|
-
|
|
1192
|
-
if (asm.error || (asm.ok && asm.ok !== 'ASSEMBLY_COMPLETED')) {
|
|
1193
|
-
const msg = `Assembly failed: ${asm.error || asm.message} (Status: ${asm.ok})`
|
|
1194
|
-
outputctl.error(msg)
|
|
1195
|
-
throw new Error(msg)
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
// Download all results
|
|
1199
|
-
if (asm.results && resolvedOutput != null) {
|
|
1200
|
-
for (const [stepName, stepResults] of Object.entries(asm.results)) {
|
|
1201
|
-
for (const stepResult of stepResults) {
|
|
1202
|
-
const resultUrl =
|
|
1203
|
-
(stepResult as { ssl_url?: string; url?: string }).ssl_url ?? stepResult.url
|
|
1204
|
-
if (!resultUrl) continue
|
|
1205
|
-
|
|
1206
|
-
let outPath: string
|
|
1207
|
-
if (outstat?.isDirectory()) {
|
|
1208
|
-
outPath = path.join(resolvedOutput, stepResult.name || `${stepName}_result`)
|
|
1209
|
-
} else {
|
|
1210
|
-
outPath = resolvedOutput
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
outputctl.debug(`DOWNLOADING ${stepResult.name} to ${outPath}`)
|
|
1214
|
-
const [dlErr] = await tryCatch(
|
|
1215
|
-
pipeline(
|
|
1216
|
-
got.stream(resultUrl, { signal: abortController.signal }),
|
|
1217
|
-
fs.createWriteStream(outPath),
|
|
1218
|
-
),
|
|
1219
|
-
)
|
|
1220
|
-
if (dlErr) {
|
|
1221
|
-
if (dlErr.name === 'AbortError') continue
|
|
1222
|
-
outputctl.error(dlErr.message)
|
|
1223
|
-
throw dlErr
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
// Delete input files if requested
|
|
1230
|
-
if (del) {
|
|
1231
|
-
for (const inPath of inputPaths) {
|
|
1232
|
-
await fsp.unlink(inPath)
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
return asm
|
|
1236
1589
|
})
|
|
1237
1590
|
results.push(assembly)
|
|
1238
1591
|
} catch (err) {
|
|
@@ -1240,30 +1593,19 @@ export async function create(
|
|
|
1240
1593
|
outputctl.error(err as Error)
|
|
1241
1594
|
}
|
|
1242
1595
|
|
|
1243
|
-
resolve({ results, hasFailures })
|
|
1596
|
+
resolve({ resultUrls, results, hasFailures })
|
|
1244
1597
|
})
|
|
1245
|
-
}
|
|
1246
|
-
// Default mode: one assembly per file with p-queue concurrency limiting
|
|
1247
|
-
emitter.on('job', (job: Job) => {
|
|
1248
|
-
const inPath = job.in
|
|
1249
|
-
? (((job.in as fs.ReadStream).path as string | undefined) ?? null)
|
|
1250
|
-
: null
|
|
1251
|
-
const outPath = job.out?.path ?? null
|
|
1252
|
-
const outMtime = job.out?.mtime
|
|
1253
|
-
outputctl.debug(`GOT JOB ${inPath ?? 'null'} ${outPath ?? 'null'}`)
|
|
1254
|
-
|
|
1255
|
-
// Close the original streams immediately - we'll create fresh ones when processing
|
|
1256
|
-
if (job.in != null) {
|
|
1257
|
-
;(job.in as fs.ReadStream).destroy()
|
|
1258
|
-
}
|
|
1259
|
-
if (job.out != null) {
|
|
1260
|
-
job.out.destroy()
|
|
1261
|
-
}
|
|
1598
|
+
}
|
|
1262
1599
|
|
|
1263
|
-
|
|
1600
|
+
function runPerFileEmitter(): void {
|
|
1601
|
+
emitter.on('job', (job: Job) => {
|
|
1602
|
+
const inPath = job.inputPath
|
|
1603
|
+
const outputPlan = job.out
|
|
1604
|
+
const outputToken = reserveWatchJobToken(outputPlan?.path ?? null)
|
|
1605
|
+
outputctl.debug(`GOT JOB ${inPath ?? 'null'} ${outputPlan?.path ?? 'null'}`)
|
|
1264
1606
|
queue
|
|
1265
1607
|
.add(async () => {
|
|
1266
|
-
const result = await processAssemblyJob(inPath,
|
|
1608
|
+
const result = await processAssemblyJob(inPath, outputPlan, outputToken)
|
|
1267
1609
|
if (result !== undefined) {
|
|
1268
1610
|
results.push(result)
|
|
1269
1611
|
}
|
|
@@ -1274,19 +1616,19 @@ export async function create(
|
|
|
1274
1616
|
})
|
|
1275
1617
|
})
|
|
1276
1618
|
|
|
1277
|
-
emitter.on('error', (err: Error) => {
|
|
1278
|
-
abortController.abort()
|
|
1279
|
-
queue.clear()
|
|
1280
|
-
outputctl.error(err)
|
|
1281
|
-
reject(err)
|
|
1282
|
-
})
|
|
1283
|
-
|
|
1284
1619
|
emitter.on('end', async () => {
|
|
1285
|
-
// Wait for all queued jobs to complete
|
|
1286
1620
|
await queue.onIdle()
|
|
1287
|
-
resolve({ results, hasFailures })
|
|
1621
|
+
resolve({ resultUrls, results, hasFailures })
|
|
1288
1622
|
})
|
|
1289
1623
|
}
|
|
1624
|
+
|
|
1625
|
+
emitter.on('error', handleEmitterError)
|
|
1626
|
+
|
|
1627
|
+
if (singleAssembly) {
|
|
1628
|
+
runSingleAssemblyEmitter()
|
|
1629
|
+
} else {
|
|
1630
|
+
runPerFileEmitter()
|
|
1631
|
+
}
|
|
1290
1632
|
})
|
|
1291
1633
|
}
|
|
1292
1634
|
|
|
@@ -1330,9 +1672,7 @@ export class AssembliesCreateCommand extends AuthenticatedCommand {
|
|
|
1330
1672
|
description: 'Specify a template to use for these assemblies',
|
|
1331
1673
|
})
|
|
1332
1674
|
|
|
1333
|
-
inputs =
|
|
1334
|
-
description: 'Provide an input file or a directory',
|
|
1335
|
-
})
|
|
1675
|
+
inputs = inputPathsOption()
|
|
1336
1676
|
|
|
1337
1677
|
outputPath = Option.String('--output,-o', {
|
|
1338
1678
|
description: 'Specify an output file or directory',
|
|
@@ -1342,30 +1682,19 @@ export class AssembliesCreateCommand extends AuthenticatedCommand {
|
|
|
1342
1682
|
description: 'Set a template field (KEY=VAL)',
|
|
1343
1683
|
})
|
|
1344
1684
|
|
|
1345
|
-
watch =
|
|
1346
|
-
description: 'Watch inputs for changes',
|
|
1347
|
-
})
|
|
1685
|
+
watch = watchOption()
|
|
1348
1686
|
|
|
1349
|
-
recursive =
|
|
1350
|
-
description: 'Enumerate input directories recursively',
|
|
1351
|
-
})
|
|
1687
|
+
recursive = recursiveOption()
|
|
1352
1688
|
|
|
1353
|
-
deleteAfterProcessing =
|
|
1354
|
-
description: 'Delete input files after they are processed',
|
|
1355
|
-
})
|
|
1689
|
+
deleteAfterProcessing = deleteAfterProcessingOption()
|
|
1356
1690
|
|
|
1357
|
-
reprocessStale =
|
|
1358
|
-
description: 'Process inputs even if output is newer',
|
|
1359
|
-
})
|
|
1691
|
+
reprocessStale = reprocessStaleOption()
|
|
1360
1692
|
|
|
1361
|
-
singleAssembly =
|
|
1362
|
-
description: 'Pass all input files to a single assembly instead of one assembly per file',
|
|
1363
|
-
})
|
|
1693
|
+
singleAssembly = singleAssemblyOption()
|
|
1364
1694
|
|
|
1365
|
-
concurrency =
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
})
|
|
1695
|
+
concurrency = concurrencyOption()
|
|
1696
|
+
|
|
1697
|
+
printUrls = printUrlsOption()
|
|
1369
1698
|
|
|
1370
1699
|
protected async run(): Promise<number | undefined> {
|
|
1371
1700
|
if (!this.steps && !this.template) {
|
|
@@ -1378,10 +1707,6 @@ export class AssembliesCreateCommand extends AuthenticatedCommand {
|
|
|
1378
1707
|
}
|
|
1379
1708
|
|
|
1380
1709
|
const inputList = this.inputs ?? []
|
|
1381
|
-
if (inputList.length === 0 && this.watch) {
|
|
1382
|
-
this.output.error('assemblies create --watch requires at least one input')
|
|
1383
|
-
return 1
|
|
1384
|
-
}
|
|
1385
1710
|
|
|
1386
1711
|
// Default to stdin only for `--steps` mode (common "pipe a file into a one-off assembly" use case).
|
|
1387
1712
|
// For `--template` mode, templates may be inputless or use /http/import, so stdin should be explicit (`--input -`).
|
|
@@ -1389,27 +1714,26 @@ export class AssembliesCreateCommand extends AuthenticatedCommand {
|
|
|
1389
1714
|
inputList.push('-')
|
|
1390
1715
|
}
|
|
1391
1716
|
|
|
1392
|
-
const fieldsMap
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
if (eqIndex === -1) {
|
|
1396
|
-
this.output.error(`invalid argument for --field: '${field}'`)
|
|
1397
|
-
return 1
|
|
1398
|
-
}
|
|
1399
|
-
const key = field.slice(0, eqIndex)
|
|
1400
|
-
const value = field.slice(eqIndex + 1)
|
|
1401
|
-
fieldsMap[key] = value
|
|
1717
|
+
const fieldsMap = parseTemplateFieldAssignments(this.output, this.fields)
|
|
1718
|
+
if (this.fields != null && fieldsMap == null) {
|
|
1719
|
+
return 1
|
|
1402
1720
|
}
|
|
1403
1721
|
|
|
1404
|
-
|
|
1405
|
-
this.
|
|
1722
|
+
const sharedValidationError = validateSharedFileProcessingOptions({
|
|
1723
|
+
explicitInputCount: this.inputs?.length ?? 0,
|
|
1724
|
+
singleAssembly: this.singleAssembly,
|
|
1725
|
+
watch: this.watch,
|
|
1726
|
+
watchRequiresInputsMessage: 'assemblies create --watch requires at least one input',
|
|
1727
|
+
})
|
|
1728
|
+
if (sharedValidationError != null) {
|
|
1729
|
+
this.output.error(sharedValidationError)
|
|
1406
1730
|
return 1
|
|
1407
1731
|
}
|
|
1408
1732
|
|
|
1409
|
-
const { hasFailures } = await create(this.output, this.client, {
|
|
1733
|
+
const { hasFailures, resultUrls } = await create(this.output, this.client, {
|
|
1410
1734
|
steps: this.steps,
|
|
1411
1735
|
template: this.template,
|
|
1412
|
-
fields: fieldsMap,
|
|
1736
|
+
fields: fieldsMap ?? {},
|
|
1413
1737
|
watch: this.watch,
|
|
1414
1738
|
recursive: this.recursive,
|
|
1415
1739
|
inputs: inputList,
|
|
@@ -1417,8 +1741,11 @@ export class AssembliesCreateCommand extends AuthenticatedCommand {
|
|
|
1417
1741
|
del: this.deleteAfterProcessing,
|
|
1418
1742
|
reprocessStale: this.reprocessStale,
|
|
1419
1743
|
singleAssembly: this.singleAssembly,
|
|
1420
|
-
concurrency: this.concurrency,
|
|
1744
|
+
concurrency: this.concurrency == null ? undefined : Number(this.concurrency),
|
|
1421
1745
|
})
|
|
1746
|
+
if (this.printUrls) {
|
|
1747
|
+
printResultUrls(this.output, resultUrls)
|
|
1748
|
+
}
|
|
1422
1749
|
return hasFailures ? 1 : undefined
|
|
1423
1750
|
}
|
|
1424
1751
|
}
|
|
@@ -1567,20 +1894,13 @@ export class AssembliesReplayCommand extends AuthenticatedCommand {
|
|
|
1567
1894
|
assemblyIds = Option.Rest({ required: 1 })
|
|
1568
1895
|
|
|
1569
1896
|
protected async run(): Promise<number | undefined> {
|
|
1570
|
-
const fieldsMap
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
if (eqIndex === -1) {
|
|
1574
|
-
this.output.error(`invalid argument for --field: '${field}'`)
|
|
1575
|
-
return 1
|
|
1576
|
-
}
|
|
1577
|
-
const key = field.slice(0, eqIndex)
|
|
1578
|
-
const value = field.slice(eqIndex + 1)
|
|
1579
|
-
fieldsMap[key] = value
|
|
1897
|
+
const fieldsMap = parseTemplateFieldAssignments(this.output, this.fields)
|
|
1898
|
+
if (this.fields != null && fieldsMap == null) {
|
|
1899
|
+
return 1
|
|
1580
1900
|
}
|
|
1581
1901
|
|
|
1582
1902
|
await replay(this.output, this.client, {
|
|
1583
|
-
fields: fieldsMap,
|
|
1903
|
+
fields: fieldsMap ?? {},
|
|
1584
1904
|
reparse: this.reparseTemplate,
|
|
1585
1905
|
steps: this.steps,
|
|
1586
1906
|
notify_url: this.notifyUrl,
|