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,11 +1,12 @@
|
|
|
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';
|
|
7
|
+
import { Writable } from 'node:stream';
|
|
6
8
|
import { pipeline } from 'node:stream/promises';
|
|
7
9
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
8
|
-
import tty from 'node:tty';
|
|
9
10
|
import { promisify } from 'node:util';
|
|
10
11
|
import { Command, Option } from 'clipanion';
|
|
11
12
|
import got from 'got';
|
|
@@ -14,12 +15,33 @@ import * as t from 'typanion';
|
|
|
14
15
|
import { z } from 'zod';
|
|
15
16
|
import { formatLintIssue } from "../../alphalib/assembly-linter.lang.en.js";
|
|
16
17
|
import { tryCatch } from "../../alphalib/tryCatch.js";
|
|
17
|
-
import {
|
|
18
|
+
import { ensureUniqueCounterValue } from "../../ensureUniqueCounter.js";
|
|
18
19
|
import { lintAssemblyInstructions } from "../../lintAssemblyInstructions.js";
|
|
19
20
|
import { lintingExamples } from "../docs/assemblyLintingExamples.js";
|
|
20
|
-
import {
|
|
21
|
+
import { concurrencyOption, deleteAfterProcessingOption, inputPathsOption, printUrlsOption, recursiveOption, reprocessStaleOption, singleAssemblyOption, validateSharedFileProcessingOptions, watchOption, } from "../fileProcessingOptions.js";
|
|
22
|
+
import { formatAPIError, readCliInput } from "../helpers.js";
|
|
23
|
+
import { normalizeAssemblyResults } from "../resultFiles.js";
|
|
24
|
+
import { collectNormalizedResultUrlRows, printResultUrls } from "../resultUrls.js";
|
|
25
|
+
import { readStepsInputFile } from "../stepsInput.js";
|
|
21
26
|
import { ensureError, isErrnoException } from "../types.js";
|
|
22
27
|
import { AuthenticatedCommand, UnauthenticatedCommand } from "./BaseCommand.js";
|
|
28
|
+
function parseTemplateFieldAssignments(output, fields) {
|
|
29
|
+
if (fields == null || fields.length === 0) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const fieldsMap = {};
|
|
33
|
+
for (const field of fields) {
|
|
34
|
+
const eqIndex = field.indexOf('=');
|
|
35
|
+
if (eqIndex === -1) {
|
|
36
|
+
output.error(`invalid argument for --field: '${field}'`);
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const key = field.slice(0, eqIndex);
|
|
40
|
+
const value = field.slice(eqIndex + 1);
|
|
41
|
+
fieldsMap[key] = value;
|
|
42
|
+
}
|
|
43
|
+
return fieldsMap;
|
|
44
|
+
}
|
|
23
45
|
const AssemblySchema = z.object({
|
|
24
46
|
id: z.string(),
|
|
25
47
|
});
|
|
@@ -84,13 +106,7 @@ export { deleteAssemblies as delete };
|
|
|
84
106
|
export async function replay(output, client, { fields, reparse, steps, notify_url, assemblies }) {
|
|
85
107
|
if (steps) {
|
|
86
108
|
try {
|
|
87
|
-
|
|
88
|
-
const parsed = JSON.parse(buf.toString());
|
|
89
|
-
const validated = stepsSchema.safeParse(parsed);
|
|
90
|
-
if (!validated.success) {
|
|
91
|
-
throw new Error(`Invalid steps format: ${validated.error.message}`);
|
|
92
|
-
}
|
|
93
|
-
await apiCall(validated.data);
|
|
109
|
+
await apiCall(await readStepsInputFile(steps));
|
|
94
110
|
}
|
|
95
111
|
catch (err) {
|
|
96
112
|
const error = ensureError(err);
|
|
@@ -106,7 +122,6 @@ export async function replay(output, client, { fields, reparse, steps, notify_ur
|
|
|
106
122
|
reparse_template: reparse ? 1 : 0,
|
|
107
123
|
fields,
|
|
108
124
|
notify_url,
|
|
109
|
-
// Steps (validated) is assignable to StepsInput at runtime; cast for TS
|
|
110
125
|
steps: stepsOverride,
|
|
111
126
|
}));
|
|
112
127
|
if (err) {
|
|
@@ -221,12 +236,42 @@ async function myStat(stdioStream, filepath) {
|
|
|
221
236
|
}
|
|
222
237
|
return await fsp.stat(filepath);
|
|
223
238
|
}
|
|
239
|
+
function getJobInputPath(filepath) {
|
|
240
|
+
const normalizedFile = path.normalize(filepath);
|
|
241
|
+
if (normalizedFile === '-') {
|
|
242
|
+
return stdinWithPath.path;
|
|
243
|
+
}
|
|
244
|
+
return normalizedFile;
|
|
245
|
+
}
|
|
246
|
+
function createInputUploadStream(filepath) {
|
|
247
|
+
const instream = fs.createReadStream(filepath);
|
|
248
|
+
// Attach a no-op error handler to prevent unhandled errors if stream is destroyed
|
|
249
|
+
// before being consumed (e.g., due to output collision detection)
|
|
250
|
+
instream.on('error', () => { });
|
|
251
|
+
return instream;
|
|
252
|
+
}
|
|
253
|
+
function createOutputPlan(pathname, mtime) {
|
|
254
|
+
if (pathname == null) {
|
|
255
|
+
return {
|
|
256
|
+
mtime,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
mtime,
|
|
261
|
+
path: pathname,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
async function createExistingPathOutputPlan(outputPath) {
|
|
265
|
+
if (outputPath == null) {
|
|
266
|
+
return createOutputPlan(undefined, new Date(0));
|
|
267
|
+
}
|
|
268
|
+
const [, stats] = await tryCatch(fsp.stat(outputPath));
|
|
269
|
+
return createOutputPlan(outputPath, stats?.mtime ?? new Date(0));
|
|
270
|
+
}
|
|
224
271
|
function dirProvider(output) {
|
|
225
272
|
return async (inpath, indir = process.cwd()) => {
|
|
226
|
-
// Inputless assemblies can still write into a directory, but output paths are derived from
|
|
227
|
-
// assembly results rather than an input file path (handled later).
|
|
228
273
|
if (inpath == null) {
|
|
229
|
-
return
|
|
274
|
+
return await createExistingPathOutputPlan(output);
|
|
230
275
|
}
|
|
231
276
|
if (inpath === '-') {
|
|
232
277
|
throw new Error('You must provide an input to output to a directory');
|
|
@@ -234,37 +279,229 @@ function dirProvider(output) {
|
|
|
234
279
|
let relpath = path.relative(indir, inpath);
|
|
235
280
|
relpath = relpath.replace(/^(\.\.\/)+/, '');
|
|
236
281
|
const outpath = path.join(output, relpath);
|
|
237
|
-
|
|
238
|
-
await fsp.mkdir(outdir, { recursive: true });
|
|
239
|
-
const [, stats] = await tryCatch(fsp.stat(outpath));
|
|
240
|
-
const mtime = stats?.mtime ?? new Date(0);
|
|
241
|
-
const outstream = fs.createWriteStream(outpath);
|
|
242
|
-
// Attach a no-op error handler to prevent unhandled errors if stream is destroyed
|
|
243
|
-
// before being consumed (e.g., due to output collision detection)
|
|
244
|
-
outstream.on('error', () => { });
|
|
245
|
-
outstream.mtime = mtime;
|
|
246
|
-
return outstream;
|
|
282
|
+
return await createExistingPathOutputPlan(outpath);
|
|
247
283
|
};
|
|
248
284
|
}
|
|
249
285
|
function fileProvider(output) {
|
|
250
|
-
const dirExistsP = fsp.mkdir(path.dirname(output), { recursive: true });
|
|
251
286
|
return async (_inpath) => {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const mtime = stats?.mtime ?? new Date(0);
|
|
257
|
-
const outstream = fs.createWriteStream(output);
|
|
258
|
-
// Attach a no-op error handler to prevent unhandled errors if stream is destroyed
|
|
259
|
-
// before being consumed (e.g., due to output collision detection)
|
|
260
|
-
outstream.on('error', () => { });
|
|
261
|
-
outstream.mtime = mtime;
|
|
262
|
-
return outstream;
|
|
287
|
+
if (output === '-') {
|
|
288
|
+
return await createExistingPathOutputPlan(undefined);
|
|
289
|
+
}
|
|
290
|
+
return await createExistingPathOutputPlan(output);
|
|
263
291
|
};
|
|
264
292
|
}
|
|
265
293
|
function nullProvider() {
|
|
266
294
|
return async (_inpath) => null;
|
|
267
295
|
}
|
|
296
|
+
async function downloadResultToFile(resultUrl, outPath, signal) {
|
|
297
|
+
await fsp.mkdir(path.dirname(outPath), { recursive: true });
|
|
298
|
+
const tempPath = path.join(path.dirname(outPath), `.${path.basename(outPath)}.${randomUUID()}.tmp`);
|
|
299
|
+
const outStream = fs.createWriteStream(tempPath);
|
|
300
|
+
outStream.on('error', () => { });
|
|
301
|
+
const [dlErr] = await tryCatch(pipeline(got.stream(resultUrl, { signal }), outStream));
|
|
302
|
+
if (dlErr) {
|
|
303
|
+
await fsp.rm(tempPath, { force: true });
|
|
304
|
+
throw dlErr;
|
|
305
|
+
}
|
|
306
|
+
await fsp.rename(tempPath, outPath);
|
|
307
|
+
}
|
|
308
|
+
async function downloadResultToStdout(resultUrl, signal) {
|
|
309
|
+
const stdoutStream = new Writable({
|
|
310
|
+
write(chunk, _encoding, callback) {
|
|
311
|
+
let settled = false;
|
|
312
|
+
const finish = (err) => {
|
|
313
|
+
if (settled)
|
|
314
|
+
return;
|
|
315
|
+
settled = true;
|
|
316
|
+
process.stdout.off('drain', onDrain);
|
|
317
|
+
process.stdout.off('error', onError);
|
|
318
|
+
callback(err ?? undefined);
|
|
319
|
+
};
|
|
320
|
+
const onDrain = () => finish();
|
|
321
|
+
const onError = (err) => finish(err);
|
|
322
|
+
process.stdout.once('error', onError);
|
|
323
|
+
try {
|
|
324
|
+
if (process.stdout.write(chunk)) {
|
|
325
|
+
finish();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
process.stdout.once('drain', onDrain);
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
finish(ensureError(err));
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
final(callback) {
|
|
335
|
+
callback();
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
await pipeline(got.stream(resultUrl, { signal }), stdoutStream);
|
|
339
|
+
}
|
|
340
|
+
function sanitizeResultName(value) {
|
|
341
|
+
const base = path.basename(value);
|
|
342
|
+
return base.replaceAll('\\', '_').replaceAll('/', '_').replaceAll('\u0000', '');
|
|
343
|
+
}
|
|
344
|
+
async function ensureUniquePath(targetPath, reservedPaths) {
|
|
345
|
+
const parsed = path.parse(targetPath);
|
|
346
|
+
return await ensureUniqueCounterValue({
|
|
347
|
+
initialValue: targetPath,
|
|
348
|
+
isTaken: async (candidate) => {
|
|
349
|
+
if (reservedPaths.has(candidate)) {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
const [statErr] = await tryCatch(fsp.stat(candidate));
|
|
353
|
+
return statErr == null;
|
|
354
|
+
},
|
|
355
|
+
reserve: (candidate) => {
|
|
356
|
+
reservedPaths.add(candidate);
|
|
357
|
+
},
|
|
358
|
+
nextValue: (counter) => path.join(parsed.dir, `${parsed.name}__${counter}${parsed.ext}`),
|
|
359
|
+
scope: reservedPaths,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
function getResultFileName(file) {
|
|
363
|
+
return sanitizeResultName(file.name);
|
|
364
|
+
}
|
|
365
|
+
const STALE_OUTPUT_GRACE_MS = 1000;
|
|
366
|
+
function isMeaningfullyNewer(newer, older) {
|
|
367
|
+
return newer.getTime() - older.getTime() > STALE_OUTPUT_GRACE_MS;
|
|
368
|
+
}
|
|
369
|
+
async function buildDirectoryDownloadTargets({ allFiles, baseDir, groupByStep, reservedPaths, }) {
|
|
370
|
+
await fsp.mkdir(baseDir, { recursive: true });
|
|
371
|
+
const targets = [];
|
|
372
|
+
for (const resultFile of allFiles) {
|
|
373
|
+
const targetDir = groupByStep ? path.join(baseDir, resultFile.stepName) : baseDir;
|
|
374
|
+
await fsp.mkdir(targetDir, { recursive: true });
|
|
375
|
+
targets.push({
|
|
376
|
+
resultUrl: resultFile.url,
|
|
377
|
+
targetPath: await ensureUniquePath(path.join(targetDir, getResultFileName(resultFile)), reservedPaths),
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return targets;
|
|
381
|
+
}
|
|
382
|
+
function getSingleResultDownloadTarget(allFiles, targetPath) {
|
|
383
|
+
const first = allFiles[0];
|
|
384
|
+
const resultUrl = first?.url ?? null;
|
|
385
|
+
if (resultUrl == null) {
|
|
386
|
+
return [];
|
|
387
|
+
}
|
|
388
|
+
return [{ resultUrl, targetPath }];
|
|
389
|
+
}
|
|
390
|
+
async function resolveResultDownloadTargets({ hasDirectoryInput, inPath, inputs, normalizedResults, outputMode, outputPath, outputRoot, outputRootIsDirectory, reservedPaths, singleAssembly, }) {
|
|
391
|
+
const { allFiles, entries } = normalizedResults;
|
|
392
|
+
const shouldGroupByInput = !singleAssembly && inPath != null && (hasDirectoryInput || inputs.length > 1);
|
|
393
|
+
const resolveDirectoryBaseDir = () => {
|
|
394
|
+
if (!shouldGroupByInput || inPath == null) {
|
|
395
|
+
return outputRoot;
|
|
396
|
+
}
|
|
397
|
+
if (hasDirectoryInput && outputPath != null) {
|
|
398
|
+
const mappedRelative = path.relative(outputRoot, outputPath);
|
|
399
|
+
const mappedDir = path.dirname(mappedRelative);
|
|
400
|
+
const mappedStem = path.parse(mappedRelative).name;
|
|
401
|
+
return path.join(outputRoot, mappedDir === '.' ? '' : mappedDir, mappedStem);
|
|
402
|
+
}
|
|
403
|
+
return path.join(outputRoot, path.parse(path.basename(inPath)).name);
|
|
404
|
+
};
|
|
405
|
+
if (!outputRootIsDirectory) {
|
|
406
|
+
if (allFiles.length > 1) {
|
|
407
|
+
if (outputPath == null) {
|
|
408
|
+
throw new Error('stdout can only receive a single result file');
|
|
409
|
+
}
|
|
410
|
+
throw new Error('file outputs can only receive a single result file');
|
|
411
|
+
}
|
|
412
|
+
return getSingleResultDownloadTarget(allFiles, outputPath);
|
|
413
|
+
}
|
|
414
|
+
if (singleAssembly) {
|
|
415
|
+
return await buildDirectoryDownloadTargets({
|
|
416
|
+
allFiles,
|
|
417
|
+
baseDir: outputRoot,
|
|
418
|
+
groupByStep: false,
|
|
419
|
+
reservedPaths,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
if (outputMode === 'directory' || outputPath == null || inPath == null) {
|
|
423
|
+
return await buildDirectoryDownloadTargets({
|
|
424
|
+
allFiles,
|
|
425
|
+
baseDir: resolveDirectoryBaseDir(),
|
|
426
|
+
groupByStep: entries.length > 1,
|
|
427
|
+
reservedPaths,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
if (allFiles.length === 1) {
|
|
431
|
+
return getSingleResultDownloadTarget(allFiles, outputPath);
|
|
432
|
+
}
|
|
433
|
+
return await buildDirectoryDownloadTargets({
|
|
434
|
+
allFiles,
|
|
435
|
+
baseDir: path.join(path.dirname(outputPath), path.parse(outputPath).name),
|
|
436
|
+
groupByStep: true,
|
|
437
|
+
reservedPaths,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
async function shouldSkipStaleOutput({ inputPaths, outputPath, outputPlanMtime, outputRootIsDirectory, reprocessStale, singleInputReference = 'output-plan', }) {
|
|
441
|
+
if (reprocessStale || outputPath == null || outputRootIsDirectory) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
if (inputPaths.length === 0 || inputPaths.some((inputPath) => inputPath === stdinWithPath.path)) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
const [outputErr, outputStat] = await tryCatch(fsp.stat(outputPath));
|
|
448
|
+
if (outputErr != null || outputStat == null) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
if (inputPaths.length === 1) {
|
|
452
|
+
if (singleInputReference === 'output-plan') {
|
|
453
|
+
return isMeaningfullyNewer(outputStat.mtime, outputPlanMtime);
|
|
454
|
+
}
|
|
455
|
+
const [inputErr, inputStat] = await tryCatch(fsp.stat(inputPaths[0]));
|
|
456
|
+
if (inputErr != null || inputStat == null) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
return isMeaningfullyNewer(outputStat.mtime, inputStat.mtime);
|
|
460
|
+
}
|
|
461
|
+
const inputStats = await Promise.all(inputPaths.map(async (inputPath) => {
|
|
462
|
+
const [inputErr, inputStat] = await tryCatch(fsp.stat(inputPath));
|
|
463
|
+
if (inputErr != null || inputStat == null) {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
return inputStat;
|
|
467
|
+
}));
|
|
468
|
+
if (inputStats.some((inputStat) => inputStat == null)) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
return inputStats.every((inputStat) => {
|
|
472
|
+
return inputStat != null && isMeaningfullyNewer(outputStat.mtime, inputStat.mtime);
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
async function materializeAssemblyResults({ abortSignal, hasDirectoryInput, inPath, inputs, normalizedResults, outputMode, outputPath, outputRoot, outputRootIsDirectory, outputctl, reservedPaths, singleAssembly, }) {
|
|
476
|
+
if (outputRoot == null) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const targets = await resolveResultDownloadTargets({
|
|
480
|
+
hasDirectoryInput,
|
|
481
|
+
inPath,
|
|
482
|
+
inputs,
|
|
483
|
+
normalizedResults,
|
|
484
|
+
outputMode,
|
|
485
|
+
outputPath,
|
|
486
|
+
outputRoot,
|
|
487
|
+
outputRootIsDirectory,
|
|
488
|
+
reservedPaths,
|
|
489
|
+
singleAssembly,
|
|
490
|
+
});
|
|
491
|
+
for (const { resultUrl, targetPath } of targets) {
|
|
492
|
+
outputctl.debug('DOWNLOADING');
|
|
493
|
+
const [dlErr] = await tryCatch(targetPath == null
|
|
494
|
+
? downloadResultToStdout(resultUrl, abortSignal)
|
|
495
|
+
: downloadResultToFile(resultUrl, targetPath, abortSignal));
|
|
496
|
+
if (dlErr) {
|
|
497
|
+
if (dlErr.name === 'AbortError') {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
outputctl.error(dlErr.message);
|
|
501
|
+
throw dlErr;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
268
505
|
class MyEventEmitter extends EventEmitter {
|
|
269
506
|
hasEnded;
|
|
270
507
|
constructor() {
|
|
@@ -282,34 +519,38 @@ class MyEventEmitter extends EventEmitter {
|
|
|
282
519
|
}
|
|
283
520
|
}
|
|
284
521
|
class ReaddirJobEmitter extends MyEventEmitter {
|
|
285
|
-
constructor({ dir,
|
|
522
|
+
constructor({ dir, recursive, outputPlanProvider, topdir = dir }) {
|
|
286
523
|
super();
|
|
287
524
|
process.nextTick(() => {
|
|
288
|
-
this.processDirectory({
|
|
525
|
+
this.processDirectory({
|
|
526
|
+
dir,
|
|
527
|
+
recursive,
|
|
528
|
+
outputPlanProvider,
|
|
529
|
+
topdir,
|
|
530
|
+
}).catch((err) => {
|
|
289
531
|
this.emit('error', err);
|
|
290
532
|
});
|
|
291
533
|
});
|
|
292
534
|
}
|
|
293
|
-
async processDirectory({ dir,
|
|
535
|
+
async processDirectory({ dir, recursive, outputPlanProvider, topdir, }) {
|
|
294
536
|
const files = await fsp.readdir(dir);
|
|
295
537
|
const pendingOperations = [];
|
|
296
538
|
for (const filename of files) {
|
|
297
539
|
const file = path.normalize(path.join(dir, filename));
|
|
298
|
-
pendingOperations.push(this.processFile({ file,
|
|
540
|
+
pendingOperations.push(this.processFile({ file, recursive, outputPlanProvider, topdir }));
|
|
299
541
|
}
|
|
300
542
|
await Promise.all(pendingOperations);
|
|
301
543
|
this.emit('end');
|
|
302
544
|
}
|
|
303
|
-
async processFile({ file,
|
|
545
|
+
async processFile({ file, recursive = false, outputPlanProvider, topdir, }) {
|
|
304
546
|
const stats = await fsp.stat(file);
|
|
305
547
|
if (stats.isDirectory()) {
|
|
306
548
|
if (recursive) {
|
|
307
549
|
await new Promise((resolve, reject) => {
|
|
308
550
|
const subdirEmitter = new ReaddirJobEmitter({
|
|
309
551
|
dir: file,
|
|
310
|
-
streamRegistry,
|
|
311
552
|
recursive,
|
|
312
|
-
|
|
553
|
+
outputPlanProvider,
|
|
313
554
|
topdir,
|
|
314
555
|
});
|
|
315
556
|
subdirEmitter.on('job', (job) => this.emit('job', job));
|
|
@@ -319,62 +560,46 @@ class ReaddirJobEmitter extends MyEventEmitter {
|
|
|
319
560
|
}
|
|
320
561
|
}
|
|
321
562
|
else {
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
existing.end();
|
|
325
|
-
const outstream = await outstreamProvider(file, topdir);
|
|
326
|
-
streamRegistry[file] = outstream ?? undefined;
|
|
327
|
-
const instream = fs.createReadStream(file);
|
|
328
|
-
// Attach a no-op error handler to prevent unhandled errors if stream is destroyed
|
|
329
|
-
// before being consumed (e.g., due to output collision detection)
|
|
330
|
-
instream.on('error', () => { });
|
|
331
|
-
this.emit('job', { in: instream, out: outstream });
|
|
563
|
+
const outputPlan = await outputPlanProvider(file, topdir);
|
|
564
|
+
this.emit('job', { inputPath: getJobInputPath(file), out: outputPlan });
|
|
332
565
|
}
|
|
333
566
|
}
|
|
334
567
|
}
|
|
335
568
|
class SingleJobEmitter extends MyEventEmitter {
|
|
336
|
-
constructor({ file,
|
|
569
|
+
constructor({ file, outputPlanProvider }) {
|
|
337
570
|
super();
|
|
338
571
|
const normalizedFile = path.normalize(file);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
existing.end();
|
|
342
|
-
outstreamProvider(normalizedFile).then((outstream) => {
|
|
343
|
-
streamRegistry[normalizedFile] = outstream ?? undefined;
|
|
344
|
-
let instream;
|
|
345
|
-
if (normalizedFile === '-') {
|
|
346
|
-
if (tty.isatty(process.stdin.fd)) {
|
|
347
|
-
instream = null;
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
instream = process.stdin;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
else {
|
|
354
|
-
instream = fs.createReadStream(normalizedFile);
|
|
355
|
-
// Attach a no-op error handler to prevent unhandled errors if stream is destroyed
|
|
356
|
-
// before being consumed (e.g., due to output collision detection)
|
|
357
|
-
instream.on('error', () => { });
|
|
358
|
-
}
|
|
572
|
+
outputPlanProvider(normalizedFile)
|
|
573
|
+
.then((outputPlan) => {
|
|
359
574
|
process.nextTick(() => {
|
|
360
|
-
this.emit('job', {
|
|
575
|
+
this.emit('job', { inputPath: getJobInputPath(normalizedFile), out: outputPlan });
|
|
361
576
|
this.emit('end');
|
|
362
577
|
});
|
|
578
|
+
})
|
|
579
|
+
.catch((err) => {
|
|
580
|
+
process.nextTick(() => {
|
|
581
|
+
this.emit('error', ensureError(err));
|
|
582
|
+
});
|
|
363
583
|
});
|
|
364
584
|
}
|
|
365
585
|
}
|
|
366
586
|
class InputlessJobEmitter extends MyEventEmitter {
|
|
367
|
-
constructor({
|
|
587
|
+
constructor({ outputPlanProvider }) {
|
|
368
588
|
super();
|
|
369
589
|
process.nextTick(() => {
|
|
370
|
-
|
|
590
|
+
outputPlanProvider(null)
|
|
591
|
+
.then((outputPlan) => {
|
|
371
592
|
try {
|
|
372
|
-
this.emit('job', {
|
|
593
|
+
this.emit('job', { inputPath: null, out: outputPlan });
|
|
373
594
|
}
|
|
374
595
|
catch (err) {
|
|
375
|
-
this.emit('error', err);
|
|
596
|
+
this.emit('error', ensureError(err));
|
|
597
|
+
return;
|
|
376
598
|
}
|
|
377
599
|
this.emit('end');
|
|
600
|
+
})
|
|
601
|
+
.catch((err) => {
|
|
602
|
+
this.emit('error', ensureError(err));
|
|
378
603
|
});
|
|
379
604
|
});
|
|
380
605
|
}
|
|
@@ -387,9 +612,9 @@ class NullJobEmitter extends MyEventEmitter {
|
|
|
387
612
|
}
|
|
388
613
|
class WatchJobEmitter extends MyEventEmitter {
|
|
389
614
|
watcher = null;
|
|
390
|
-
constructor({ file,
|
|
615
|
+
constructor({ file, recursive, outputPlanProvider }) {
|
|
391
616
|
super();
|
|
392
|
-
this.init({ file,
|
|
617
|
+
this.init({ file, recursive, outputPlanProvider }).catch((err) => {
|
|
393
618
|
this.emit('error', err);
|
|
394
619
|
});
|
|
395
620
|
// Clean up watcher on process exit signals
|
|
@@ -404,7 +629,7 @@ class WatchJobEmitter extends MyEventEmitter {
|
|
|
404
629
|
this.watcher = null;
|
|
405
630
|
}
|
|
406
631
|
}
|
|
407
|
-
async init({ file,
|
|
632
|
+
async init({ file, recursive, outputPlanProvider, }) {
|
|
408
633
|
const stats = await fsp.stat(file);
|
|
409
634
|
const topdir = stats.isDirectory() ? file : undefined;
|
|
410
635
|
const watchFn = await getNodeWatch();
|
|
@@ -416,25 +641,21 @@ class WatchJobEmitter extends MyEventEmitter {
|
|
|
416
641
|
this.watcher.on('close', () => this.emit('end'));
|
|
417
642
|
this.watcher.on('change', (_evt, filename) => {
|
|
418
643
|
const normalizedFile = path.normalize(filename);
|
|
419
|
-
this.handleChange(normalizedFile, topdir,
|
|
644
|
+
this.handleChange(normalizedFile, topdir, outputPlanProvider).catch((err) => {
|
|
420
645
|
this.emit('error', err);
|
|
421
646
|
});
|
|
422
647
|
});
|
|
423
648
|
}
|
|
424
|
-
async handleChange(normalizedFile, topdir,
|
|
649
|
+
async handleChange(normalizedFile, topdir, outputPlanProvider) {
|
|
425
650
|
const stats = await fsp.stat(normalizedFile);
|
|
426
651
|
if (stats.isDirectory())
|
|
427
652
|
return;
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
// Attach a no-op error handler to prevent unhandled errors if stream is destroyed
|
|
435
|
-
// before being consumed (e.g., due to output collision detection)
|
|
436
|
-
instream.on('error', () => { });
|
|
437
|
-
this.emit('job', { in: instream, out: outstream });
|
|
653
|
+
const outputPlan = await outputPlanProvider(normalizedFile, topdir);
|
|
654
|
+
this.emit('job', {
|
|
655
|
+
inputPath: getJobInputPath(normalizedFile),
|
|
656
|
+
out: outputPlan,
|
|
657
|
+
watchEvent: true,
|
|
658
|
+
});
|
|
438
659
|
}
|
|
439
660
|
}
|
|
440
661
|
class MergedJobEmitter extends MyEventEmitter {
|
|
@@ -484,12 +705,20 @@ function detectConflicts(jobEmitter) {
|
|
|
484
705
|
jobEmitter.on('end', () => emitter.emit('end'));
|
|
485
706
|
jobEmitter.on('error', (err) => emitter.emit('error', err));
|
|
486
707
|
jobEmitter.on('job', (job) => {
|
|
487
|
-
if (job.
|
|
708
|
+
if (job.watchEvent) {
|
|
488
709
|
emitter.emit('job', job);
|
|
489
710
|
return;
|
|
490
711
|
}
|
|
491
|
-
|
|
712
|
+
if (job.inputPath == null || job.out == null) {
|
|
713
|
+
emitter.emit('job', job);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const inPath = job.inputPath;
|
|
492
717
|
const outPath = job.out.path;
|
|
718
|
+
if (outPath == null) {
|
|
719
|
+
emitter.emit('job', job);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
493
722
|
if (Object.hasOwn(outfileAssociations, outPath) && outfileAssociations[outPath] !== inPath) {
|
|
494
723
|
emitter.emit('error', new Error(`Output collision between '${inPath}' and '${outfileAssociations[outPath]}'`));
|
|
495
724
|
}
|
|
@@ -506,11 +735,11 @@ function dismissStaleJobs(jobEmitter) {
|
|
|
506
735
|
jobEmitter.on('end', () => Promise.all(pendingChecks).then(() => emitter.emit('end')));
|
|
507
736
|
jobEmitter.on('error', (err) => emitter.emit('error', err));
|
|
508
737
|
jobEmitter.on('job', (job) => {
|
|
509
|
-
if (job.
|
|
738
|
+
if (job.inputPath == null || job.out == null) {
|
|
510
739
|
emitter.emit('job', job);
|
|
511
740
|
return;
|
|
512
741
|
}
|
|
513
|
-
const inPath = job.
|
|
742
|
+
const inPath = job.inputPath;
|
|
514
743
|
const checkPromise = fsp
|
|
515
744
|
.stat(inPath)
|
|
516
745
|
.then((stats) => {
|
|
@@ -526,30 +755,49 @@ function dismissStaleJobs(jobEmitter) {
|
|
|
526
755
|
});
|
|
527
756
|
return emitter;
|
|
528
757
|
}
|
|
529
|
-
function
|
|
758
|
+
function passthroughJobs(jobEmitter) {
|
|
759
|
+
const emitter = new MyEventEmitter();
|
|
760
|
+
jobEmitter.on('end', () => emitter.emit('end'));
|
|
761
|
+
jobEmitter.on('error', (err) => emitter.emit('error', err));
|
|
762
|
+
jobEmitter.on('job', (job) => emitter.emit('job', job));
|
|
763
|
+
return emitter;
|
|
764
|
+
}
|
|
765
|
+
function makeJobEmitter(inputs, { allowOutputCollisions, recursive, outputPlanProvider, singleAssembly, watch: watchOption, reprocessStale, }) {
|
|
530
766
|
const emitter = new EventEmitter();
|
|
531
767
|
const emitterFns = [];
|
|
532
768
|
const watcherFns = [];
|
|
533
769
|
async function processInputs() {
|
|
534
770
|
for (const input of inputs) {
|
|
535
771
|
if (input === '-') {
|
|
536
|
-
emitterFns.push(() => new SingleJobEmitter({ file: input,
|
|
772
|
+
emitterFns.push(() => new SingleJobEmitter({ file: input, outputPlanProvider }));
|
|
537
773
|
watcherFns.push(() => new NullJobEmitter());
|
|
538
774
|
}
|
|
539
775
|
else {
|
|
540
776
|
const stats = await fsp.stat(input);
|
|
541
777
|
if (stats.isDirectory()) {
|
|
542
|
-
emitterFns.push(() => new ReaddirJobEmitter({
|
|
543
|
-
|
|
778
|
+
emitterFns.push(() => new ReaddirJobEmitter({
|
|
779
|
+
dir: input,
|
|
780
|
+
recursive,
|
|
781
|
+
outputPlanProvider,
|
|
782
|
+
}));
|
|
783
|
+
watcherFns.push(() => new WatchJobEmitter({
|
|
784
|
+
file: input,
|
|
785
|
+
recursive,
|
|
786
|
+
outputPlanProvider,
|
|
787
|
+
}));
|
|
544
788
|
}
|
|
545
789
|
else {
|
|
546
|
-
emitterFns.push(() => new SingleJobEmitter({ file: input,
|
|
547
|
-
watcherFns.push(() => new WatchJobEmitter({
|
|
790
|
+
emitterFns.push(() => new SingleJobEmitter({ file: input, outputPlanProvider }));
|
|
791
|
+
watcherFns.push(() => new WatchJobEmitter({
|
|
792
|
+
file: input,
|
|
793
|
+
recursive,
|
|
794
|
+
outputPlanProvider,
|
|
795
|
+
}));
|
|
548
796
|
}
|
|
549
797
|
}
|
|
550
798
|
}
|
|
551
799
|
if (inputs.length === 0) {
|
|
552
|
-
emitterFns.push(() => new InputlessJobEmitter({
|
|
800
|
+
emitterFns.push(() => new InputlessJobEmitter({ outputPlanProvider }));
|
|
553
801
|
}
|
|
554
802
|
startEmitting();
|
|
555
803
|
}
|
|
@@ -565,38 +813,24 @@ function makeJobEmitter(inputs, { recursive, outstreamProvider, streamRegistry,
|
|
|
565
813
|
processInputs().catch((err) => {
|
|
566
814
|
emitter.emit('error', err);
|
|
567
815
|
});
|
|
568
|
-
const
|
|
569
|
-
|
|
816
|
+
const conflictFilter = allowOutputCollisions ? passthroughJobs : detectConflicts;
|
|
817
|
+
const staleFilter = reprocessStale || singleAssembly ? passthroughJobs : dismissStaleJobs;
|
|
818
|
+
return staleFilter(conflictFilter(emitter));
|
|
570
819
|
}
|
|
571
820
|
const DEFAULT_CONCURRENCY = 5;
|
|
572
821
|
// --- Main assembly create function ---
|
|
573
|
-
export async function create(outputctl, client, { steps, template, fields, watch: watchOption, recursive, inputs, output, del, reprocessStale, singleAssembly, concurrency = DEFAULT_CONCURRENCY, }) {
|
|
822
|
+
export async function create(outputctl, client, { steps, stepsData, template, fields, outputMode, watch: watchOption, recursive, inputs, output, del, reprocessStale, singleAssembly, concurrency = DEFAULT_CONCURRENCY, }) {
|
|
574
823
|
// Quick fix for https://github.com/transloadit/transloadify/issues/13
|
|
575
824
|
// Only default to stdout when output is undefined (not provided), not when explicitly null
|
|
576
825
|
let resolvedOutput = output;
|
|
577
826
|
if (resolvedOutput === undefined && !process.stdout.isTTY)
|
|
578
827
|
resolvedOutput = '-';
|
|
579
828
|
// Read steps file async before entering the Promise constructor
|
|
580
|
-
// We use StepsInput (the input type) rather than
|
|
829
|
+
// We use StepsInput (the input type) rather than the transformed output type
|
|
581
830
|
// to avoid zod adding default values that the API may reject
|
|
582
|
-
let stepsData;
|
|
831
|
+
let effectiveStepsData = stepsData;
|
|
583
832
|
if (steps) {
|
|
584
|
-
|
|
585
|
-
const parsed = JSON.parse(stepsContent);
|
|
586
|
-
// Basic structural validation: must be an object with step names as keys
|
|
587
|
-
if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
588
|
-
throw new Error('Invalid steps format: expected an object with step names as keys');
|
|
589
|
-
}
|
|
590
|
-
// Validate each step has a robot field
|
|
591
|
-
for (const [stepName, step] of Object.entries(parsed)) {
|
|
592
|
-
if (step == null || typeof step !== 'object' || Array.isArray(step)) {
|
|
593
|
-
throw new Error(`Invalid steps format: step '${stepName}' must be an object`);
|
|
594
|
-
}
|
|
595
|
-
if (!('robot' in step) || typeof step.robot !== 'string') {
|
|
596
|
-
throw new Error(`Invalid steps format: step '${stepName}' must have a 'robot' string property`);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
stepsData = parsed;
|
|
833
|
+
effectiveStepsData = await readStepsInputFile(steps);
|
|
600
834
|
}
|
|
601
835
|
// Determine output stat async before entering the Promise constructor
|
|
602
836
|
let outstat;
|
|
@@ -604,8 +838,17 @@ export async function create(outputctl, client, { steps, template, fields, watch
|
|
|
604
838
|
const [err, stat] = await tryCatch(myStat(process.stdout, resolvedOutput));
|
|
605
839
|
if (err && (!isErrnoException(err) || err.code !== 'ENOENT'))
|
|
606
840
|
throw err;
|
|
607
|
-
outstat =
|
|
608
|
-
|
|
841
|
+
outstat =
|
|
842
|
+
stat ??
|
|
843
|
+
{
|
|
844
|
+
isDirectory: () => outputMode === 'directory',
|
|
845
|
+
};
|
|
846
|
+
if (outputMode === 'directory' && stat != null && !stat.isDirectory()) {
|
|
847
|
+
const msg = 'Output must be a directory for this command';
|
|
848
|
+
outputctl.error(msg);
|
|
849
|
+
throw new Error(msg);
|
|
850
|
+
}
|
|
851
|
+
if (!outstat.isDirectory() && inputs.length !== 0 && !singleAssembly) {
|
|
609
852
|
const firstInput = inputs[0];
|
|
610
853
|
if (firstInput) {
|
|
611
854
|
const firstInputStat = await myStat(process.stdin, firstInput);
|
|
@@ -617,276 +860,223 @@ export async function create(outputctl, client, { steps, template, fields, watch
|
|
|
617
860
|
}
|
|
618
861
|
}
|
|
619
862
|
}
|
|
863
|
+
const inputStats = await Promise.all(inputs.map(async (input) => {
|
|
864
|
+
if (input === '-')
|
|
865
|
+
return null;
|
|
866
|
+
return await myStat(process.stdin, input);
|
|
867
|
+
}));
|
|
868
|
+
const hasDirectoryInput = inputStats.some((stat) => stat?.isDirectory() === true);
|
|
620
869
|
return new Promise((resolve, reject) => {
|
|
621
|
-
const params = (
|
|
870
|
+
const params = (effectiveStepsData
|
|
871
|
+
? { steps: effectiveStepsData }
|
|
872
|
+
: { template_id: template });
|
|
622
873
|
if (fields) {
|
|
623
874
|
params.fields = fields;
|
|
624
875
|
}
|
|
625
|
-
const
|
|
876
|
+
const outputPlanProvider = resolvedOutput == null
|
|
626
877
|
? nullProvider()
|
|
627
878
|
: outstat?.isDirectory()
|
|
628
879
|
? dirProvider(resolvedOutput)
|
|
629
880
|
: fileProvider(resolvedOutput);
|
|
630
|
-
const streamRegistry = {};
|
|
631
881
|
const emitter = makeJobEmitter(inputs, {
|
|
882
|
+
allowOutputCollisions: singleAssembly,
|
|
883
|
+
outputPlanProvider,
|
|
632
884
|
recursive,
|
|
633
885
|
watch: watchOption,
|
|
634
|
-
|
|
635
|
-
streamRegistry,
|
|
886
|
+
singleAssembly,
|
|
636
887
|
reprocessStale,
|
|
637
888
|
});
|
|
638
889
|
// Use p-queue for concurrency management
|
|
639
890
|
const queue = new PQueue({ concurrency });
|
|
640
891
|
const results = [];
|
|
892
|
+
const resultUrls = [];
|
|
893
|
+
const reservedResultPaths = new Set();
|
|
894
|
+
const latestWatchJobTokenByOutputPath = new Map();
|
|
641
895
|
let hasFailures = false;
|
|
896
|
+
let nextWatchJobToken = 0;
|
|
642
897
|
// AbortController to cancel all in-flight createAssembly calls when an error occurs
|
|
643
898
|
const abortController = new AbortController();
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
899
|
+
const outputRootIsDirectory = Boolean(resolvedOutput != null && outstat?.isDirectory());
|
|
900
|
+
function reserveWatchJobToken(outputPath) {
|
|
901
|
+
if (!watchOption || outputPath == null) {
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
const token = ++nextWatchJobToken;
|
|
905
|
+
latestWatchJobTokenByOutputPath.set(outputPath, token);
|
|
906
|
+
return token;
|
|
907
|
+
}
|
|
908
|
+
function isSupersededWatchJob(outputPath, token) {
|
|
909
|
+
if (!watchOption || outputPath == null || token == null) {
|
|
910
|
+
return false;
|
|
911
|
+
}
|
|
912
|
+
return latestWatchJobTokenByOutputPath.get(outputPath) !== token;
|
|
913
|
+
}
|
|
914
|
+
function createAssemblyOptions({ files, uploads, } = {}) {
|
|
658
915
|
const createOptions = {
|
|
659
916
|
params,
|
|
660
917
|
signal: abortController.signal,
|
|
661
918
|
};
|
|
662
|
-
if (
|
|
663
|
-
createOptions.
|
|
919
|
+
if (files != null && Object.keys(files).length > 0) {
|
|
920
|
+
createOptions.files = files;
|
|
921
|
+
}
|
|
922
|
+
if (uploads != null && Object.keys(uploads).length > 0) {
|
|
923
|
+
createOptions.uploads = uploads;
|
|
664
924
|
}
|
|
925
|
+
return createOptions;
|
|
926
|
+
}
|
|
927
|
+
async function awaitCompletedAssembly(createOptions) {
|
|
665
928
|
const result = await client.createAssembly(createOptions);
|
|
666
|
-
if (superceded)
|
|
667
|
-
return undefined;
|
|
668
929
|
const assemblyId = result.assembly_id;
|
|
669
930
|
if (!assemblyId)
|
|
670
931
|
throw new Error('No assembly_id in result');
|
|
671
932
|
const assembly = await client.awaitAssemblyCompletion(assemblyId, {
|
|
672
933
|
signal: abortController.signal,
|
|
673
|
-
onPoll: () =>
|
|
674
|
-
if (superceded)
|
|
675
|
-
return false;
|
|
676
|
-
return true;
|
|
677
|
-
},
|
|
934
|
+
onPoll: () => true,
|
|
678
935
|
onAssemblyProgress: (status) => {
|
|
679
936
|
outputctl.debug(`Assembly status: ${status.ok}`);
|
|
680
937
|
},
|
|
681
938
|
});
|
|
682
|
-
if (superceded)
|
|
683
|
-
return undefined;
|
|
684
939
|
if (assembly.error || (assembly.ok && assembly.ok !== 'ASSEMBLY_COMPLETED')) {
|
|
685
940
|
const msg = `Assembly failed: ${assembly.error || assembly.message} (Status: ${assembly.ok})`;
|
|
686
941
|
outputctl.error(msg);
|
|
687
942
|
throw new Error(msg);
|
|
688
943
|
}
|
|
944
|
+
return { assembly, assemblyId };
|
|
945
|
+
}
|
|
946
|
+
async function executeAssemblyLifecycle({ createOptions, inPath, inputPaths, outputPlan, outputToken, singleAssemblyMode, }) {
|
|
947
|
+
outputctl.debug(`PROCESSING JOB ${inPath ?? 'null'} ${outputPlan?.path ?? 'null'}`);
|
|
948
|
+
const { assembly, assemblyId } = await awaitCompletedAssembly(createOptions);
|
|
689
949
|
if (!assembly.results)
|
|
690
950
|
throw new Error('No results in assembly');
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
for (const file of stepResults) {
|
|
696
|
-
allFiles.push({ stepName, file });
|
|
697
|
-
}
|
|
951
|
+
const normalizedResults = normalizeAssemblyResults(assembly.results);
|
|
952
|
+
if (isSupersededWatchJob(outputPlan?.path ?? null, outputToken)) {
|
|
953
|
+
outputctl.debug(`SKIPPED SUPERSEDED WATCH RESULT ${inPath ?? 'null'} ${outputPlan?.path ?? 'null'}`);
|
|
954
|
+
return assembly;
|
|
698
955
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
continue;
|
|
732
|
-
const stepDir = path.join(baseDir, stepName);
|
|
733
|
-
await fsp.mkdir(stepDir, { recursive: true });
|
|
734
|
-
const rawName = file.name ??
|
|
735
|
-
(file.basename && file.ext ? `${file.basename}.${file.ext}` : undefined) ??
|
|
736
|
-
`${stepName}_result`;
|
|
737
|
-
const safeName = sanitizeName(rawName);
|
|
738
|
-
const targetPath = await ensureUniquePath(path.join(stepDir, safeName));
|
|
739
|
-
outputctl.debug('DOWNLOADING');
|
|
740
|
-
const outStream = fs.createWriteStream(targetPath);
|
|
741
|
-
outStream.on('error', () => { });
|
|
742
|
-
const [dlErr] = await tryCatch(pipeline(got.stream(resultUrl, { signal: abortController.signal }), outStream));
|
|
743
|
-
if (dlErr) {
|
|
744
|
-
if (dlErr.name === 'AbortError')
|
|
745
|
-
continue;
|
|
746
|
-
outputctl.error(dlErr.message);
|
|
747
|
-
throw dlErr;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
else if (!outIsDirectory && outPath != null) {
|
|
752
|
-
const first = allFiles[0];
|
|
753
|
-
const resultUrl = first ? getFileUrl(first.file) : null;
|
|
754
|
-
if (resultUrl) {
|
|
755
|
-
outputctl.debug('DOWNLOADING');
|
|
756
|
-
const outStream = fs.createWriteStream(outPath);
|
|
757
|
-
outStream.on('error', () => { });
|
|
758
|
-
outStream.mtime = outMtime;
|
|
759
|
-
markSupersededOnFinish(outStream);
|
|
760
|
-
const [dlErr] = await tryCatch(pipeline(got.stream(resultUrl, { signal: abortController.signal }), outStream));
|
|
761
|
-
if (dlErr) {
|
|
762
|
-
if (dlErr.name !== 'AbortError') {
|
|
763
|
-
outputctl.error(dlErr.message);
|
|
764
|
-
throw dlErr;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
else if (outIsDirectory && outPath != null) {
|
|
770
|
-
// Single-result, input-backed job: preserve existing file mapping in outdir.
|
|
771
|
-
const first = allFiles[0];
|
|
772
|
-
const resultUrl = first ? getFileUrl(first.file) : null;
|
|
773
|
-
if (resultUrl) {
|
|
774
|
-
outputctl.debug('DOWNLOADING');
|
|
775
|
-
const outStream = fs.createWriteStream(outPath);
|
|
776
|
-
outStream.on('error', () => { });
|
|
777
|
-
outStream.mtime = outMtime;
|
|
778
|
-
markSupersededOnFinish(outStream);
|
|
779
|
-
const [dlErr] = await tryCatch(pipeline(got.stream(resultUrl, { signal: abortController.signal }), outStream));
|
|
780
|
-
if (dlErr) {
|
|
781
|
-
if (dlErr.name !== 'AbortError') {
|
|
782
|
-
outputctl.error(dlErr.message);
|
|
783
|
-
throw dlErr;
|
|
784
|
-
}
|
|
785
|
-
}
|
|
956
|
+
if (!singleAssemblyMode &&
|
|
957
|
+
!watchOption &&
|
|
958
|
+
(await shouldSkipStaleOutput({
|
|
959
|
+
inputPaths,
|
|
960
|
+
outputPath: outputPlan?.path ?? null,
|
|
961
|
+
outputPlanMtime: outputPlan?.mtime ?? new Date(0),
|
|
962
|
+
outputRootIsDirectory,
|
|
963
|
+
reprocessStale,
|
|
964
|
+
}))) {
|
|
965
|
+
outputctl.debug(`SKIPPED STALE RESULT ${inPath ?? 'null'} ${outputPlan?.path ?? 'null'}`);
|
|
966
|
+
return assembly;
|
|
967
|
+
}
|
|
968
|
+
resultUrls.push(...collectNormalizedResultUrlRows({ assemblyId, normalizedResults }));
|
|
969
|
+
await materializeAssemblyResults({
|
|
970
|
+
abortSignal: abortController.signal,
|
|
971
|
+
hasDirectoryInput: singleAssemblyMode ? false : hasDirectoryInput,
|
|
972
|
+
inPath,
|
|
973
|
+
inputs: inputPaths,
|
|
974
|
+
normalizedResults,
|
|
975
|
+
outputMode,
|
|
976
|
+
outputPath: outputPlan?.path ?? null,
|
|
977
|
+
outputRoot: resolvedOutput ?? null,
|
|
978
|
+
outputRootIsDirectory,
|
|
979
|
+
outputctl,
|
|
980
|
+
reservedPaths: reservedResultPaths,
|
|
981
|
+
singleAssembly: singleAssemblyMode,
|
|
982
|
+
});
|
|
983
|
+
outputctl.debug(`COMPLETED ${inPath ?? 'null'} ${outputPlan?.path ?? 'null'}`);
|
|
984
|
+
if (del) {
|
|
985
|
+
for (const inputPath of inputPaths) {
|
|
986
|
+
if (inputPath === stdinWithPath.path) {
|
|
987
|
+
continue;
|
|
786
988
|
}
|
|
989
|
+
await fsp.unlink(inputPath);
|
|
787
990
|
}
|
|
788
991
|
}
|
|
789
|
-
outputctl.debug(`COMPLETED ${inPath ?? 'null'} ${outPath ?? 'null'}`);
|
|
790
|
-
if (del && inPath) {
|
|
791
|
-
await fsp.unlink(inPath);
|
|
792
|
-
}
|
|
793
992
|
return assembly;
|
|
794
993
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
994
|
+
// Helper to process a single assembly job
|
|
995
|
+
async function processAssemblyJob(inPath, outputPlan, outputToken) {
|
|
996
|
+
const files = inPath != null && inPath !== stdinWithPath.path
|
|
997
|
+
? {
|
|
998
|
+
in: inPath,
|
|
999
|
+
}
|
|
1000
|
+
: undefined;
|
|
1001
|
+
const uploads = inPath === stdinWithPath.path
|
|
1002
|
+
? {
|
|
1003
|
+
in: createInputUploadStream(inPath),
|
|
1004
|
+
}
|
|
1005
|
+
: undefined;
|
|
1006
|
+
return await executeAssemblyLifecycle({
|
|
1007
|
+
createOptions: createAssemblyOptions({ files, uploads }),
|
|
1008
|
+
inPath,
|
|
1009
|
+
inputPaths: inPath == null ? [] : [inPath],
|
|
1010
|
+
outputPlan,
|
|
1011
|
+
outputToken,
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
function handleEmitterError(err) {
|
|
1015
|
+
abortController.abort();
|
|
1016
|
+
queue.clear();
|
|
1017
|
+
outputctl.error(err);
|
|
1018
|
+
reject(err);
|
|
1019
|
+
}
|
|
1020
|
+
function runSingleAssemblyEmitter() {
|
|
798
1021
|
const collectedPaths = [];
|
|
1022
|
+
let inputlessOutputPlan = null;
|
|
799
1023
|
emitter.on('job', (job) => {
|
|
800
|
-
if (job.
|
|
801
|
-
const inPath = job.
|
|
1024
|
+
if (job.inputPath != null) {
|
|
1025
|
+
const inPath = job.inputPath;
|
|
802
1026
|
outputctl.debug(`COLLECTING JOB ${inPath}`);
|
|
803
1027
|
collectedPaths.push(inPath);
|
|
804
|
-
|
|
805
|
-
outputctl.debug(`STREAM CLOSED ${inPath}`);
|
|
1028
|
+
return;
|
|
806
1029
|
}
|
|
807
|
-
|
|
808
|
-
emitter.on('error', (err) => {
|
|
809
|
-
abortController.abort();
|
|
810
|
-
queue.clear();
|
|
811
|
-
outputctl.error(err);
|
|
812
|
-
reject(err);
|
|
1030
|
+
inputlessOutputPlan = job.out ?? null;
|
|
813
1031
|
});
|
|
814
1032
|
emitter.on('end', async () => {
|
|
815
|
-
if (
|
|
816
|
-
|
|
1033
|
+
if (await shouldSkipStaleOutput({
|
|
1034
|
+
inputPaths: collectedPaths,
|
|
1035
|
+
outputPath: resolvedOutput ?? null,
|
|
1036
|
+
outputPlanMtime: new Date(0),
|
|
1037
|
+
outputRootIsDirectory,
|
|
1038
|
+
reprocessStale,
|
|
1039
|
+
singleInputReference: 'input',
|
|
1040
|
+
})) {
|
|
1041
|
+
outputctl.debug(`SKIPPED STALE SINGLE ASSEMBLY ${resolvedOutput ?? 'null'}`);
|
|
1042
|
+
resolve({ resultUrls, results: [], hasFailures: false });
|
|
817
1043
|
return;
|
|
818
1044
|
}
|
|
819
|
-
//
|
|
1045
|
+
// Preserve original basenames/extensions for filesystem uploads so the backend
|
|
1046
|
+
// can infer types like Markdown correctly.
|
|
1047
|
+
const files = {};
|
|
820
1048
|
const uploads = {};
|
|
821
1049
|
const inputPaths = [];
|
|
822
1050
|
for (const inPath of collectedPaths) {
|
|
823
1051
|
const basename = path.basename(inPath);
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
counter
|
|
1052
|
+
const collection = inPath === stdinWithPath.path ? uploads : files;
|
|
1053
|
+
const key = await ensureUniqueCounterValue({
|
|
1054
|
+
initialValue: basename,
|
|
1055
|
+
isTaken: (candidate) => candidate in collection,
|
|
1056
|
+
nextValue: (counter) => `${path.parse(basename).name}_${counter}${path.parse(basename).ext}`,
|
|
1057
|
+
reserve: () => { },
|
|
1058
|
+
scope: collection,
|
|
1059
|
+
});
|
|
1060
|
+
if (inPath === stdinWithPath.path) {
|
|
1061
|
+
uploads[key] = createInputUploadStream(inPath);
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
files[key] = inPath;
|
|
829
1065
|
}
|
|
830
|
-
uploads[key] = fs.createReadStream(inPath);
|
|
831
1066
|
inputPaths.push(inPath);
|
|
832
1067
|
}
|
|
833
|
-
outputctl.debug(`Creating single assembly with ${Object.keys(uploads).length} files`);
|
|
1068
|
+
outputctl.debug(`Creating single assembly with ${Object.keys(files).length + Object.keys(uploads).length} files`);
|
|
834
1069
|
try {
|
|
835
1070
|
const assembly = await queue.add(async () => {
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
const assemblyId = result.assembly_id;
|
|
845
|
-
if (!assemblyId)
|
|
846
|
-
throw new Error('No assembly_id in result');
|
|
847
|
-
const asm = await client.awaitAssemblyCompletion(assemblyId, {
|
|
848
|
-
signal: abortController.signal,
|
|
849
|
-
onAssemblyProgress: (status) => {
|
|
850
|
-
outputctl.debug(`Assembly status: ${status.ok}`);
|
|
851
|
-
},
|
|
1071
|
+
return await executeAssemblyLifecycle({
|
|
1072
|
+
createOptions: createAssemblyOptions({ files, uploads }),
|
|
1073
|
+
inPath: null,
|
|
1074
|
+
inputPaths,
|
|
1075
|
+
outputPlan: inputlessOutputPlan ??
|
|
1076
|
+
(resolvedOutput == null ? null : createOutputPlan(resolvedOutput, new Date(0))),
|
|
1077
|
+
outputToken: null,
|
|
1078
|
+
singleAssemblyMode: true,
|
|
852
1079
|
});
|
|
853
|
-
if (asm.error || (asm.ok && asm.ok !== 'ASSEMBLY_COMPLETED')) {
|
|
854
|
-
const msg = `Assembly failed: ${asm.error || asm.message} (Status: ${asm.ok})`;
|
|
855
|
-
outputctl.error(msg);
|
|
856
|
-
throw new Error(msg);
|
|
857
|
-
}
|
|
858
|
-
// Download all results
|
|
859
|
-
if (asm.results && resolvedOutput != null) {
|
|
860
|
-
for (const [stepName, stepResults] of Object.entries(asm.results)) {
|
|
861
|
-
for (const stepResult of stepResults) {
|
|
862
|
-
const resultUrl = stepResult.ssl_url ?? stepResult.url;
|
|
863
|
-
if (!resultUrl)
|
|
864
|
-
continue;
|
|
865
|
-
let outPath;
|
|
866
|
-
if (outstat?.isDirectory()) {
|
|
867
|
-
outPath = path.join(resolvedOutput, stepResult.name || `${stepName}_result`);
|
|
868
|
-
}
|
|
869
|
-
else {
|
|
870
|
-
outPath = resolvedOutput;
|
|
871
|
-
}
|
|
872
|
-
outputctl.debug(`DOWNLOADING ${stepResult.name} to ${outPath}`);
|
|
873
|
-
const [dlErr] = await tryCatch(pipeline(got.stream(resultUrl, { signal: abortController.signal }), fs.createWriteStream(outPath)));
|
|
874
|
-
if (dlErr) {
|
|
875
|
-
if (dlErr.name === 'AbortError')
|
|
876
|
-
continue;
|
|
877
|
-
outputctl.error(dlErr.message);
|
|
878
|
-
throw dlErr;
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
// Delete input files if requested
|
|
884
|
-
if (del) {
|
|
885
|
-
for (const inPath of inputPaths) {
|
|
886
|
-
await fsp.unlink(inPath);
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
return asm;
|
|
890
1080
|
});
|
|
891
1081
|
results.push(assembly);
|
|
892
1082
|
}
|
|
@@ -894,30 +1084,18 @@ export async function create(outputctl, client, { steps, template, fields, watch
|
|
|
894
1084
|
hasFailures = true;
|
|
895
1085
|
outputctl.error(err);
|
|
896
1086
|
}
|
|
897
|
-
resolve({ results, hasFailures });
|
|
1087
|
+
resolve({ resultUrls, results, hasFailures });
|
|
898
1088
|
});
|
|
899
1089
|
}
|
|
900
|
-
|
|
901
|
-
// Default mode: one assembly per file with p-queue concurrency limiting
|
|
1090
|
+
function runPerFileEmitter() {
|
|
902
1091
|
emitter.on('job', (job) => {
|
|
903
|
-
const inPath = job.
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
const outMtime = job.out?.mtime;
|
|
908
|
-
outputctl.debug(`GOT JOB ${inPath ?? 'null'} ${outPath ?? 'null'}`);
|
|
909
|
-
// Close the original streams immediately - we'll create fresh ones when processing
|
|
910
|
-
if (job.in != null) {
|
|
911
|
-
;
|
|
912
|
-
job.in.destroy();
|
|
913
|
-
}
|
|
914
|
-
if (job.out != null) {
|
|
915
|
-
job.out.destroy();
|
|
916
|
-
}
|
|
917
|
-
// Add job to queue - p-queue handles concurrency automatically
|
|
1092
|
+
const inPath = job.inputPath;
|
|
1093
|
+
const outputPlan = job.out;
|
|
1094
|
+
const outputToken = reserveWatchJobToken(outputPlan?.path ?? null);
|
|
1095
|
+
outputctl.debug(`GOT JOB ${inPath ?? 'null'} ${outputPlan?.path ?? 'null'}`);
|
|
918
1096
|
queue
|
|
919
1097
|
.add(async () => {
|
|
920
|
-
const result = await processAssemblyJob(inPath,
|
|
1098
|
+
const result = await processAssemblyJob(inPath, outputPlan, outputToken);
|
|
921
1099
|
if (result !== undefined) {
|
|
922
1100
|
results.push(result);
|
|
923
1101
|
}
|
|
@@ -927,18 +1105,18 @@ export async function create(outputctl, client, { steps, template, fields, watch
|
|
|
927
1105
|
outputctl.error(err);
|
|
928
1106
|
});
|
|
929
1107
|
});
|
|
930
|
-
emitter.on('error', (err) => {
|
|
931
|
-
abortController.abort();
|
|
932
|
-
queue.clear();
|
|
933
|
-
outputctl.error(err);
|
|
934
|
-
reject(err);
|
|
935
|
-
});
|
|
936
1108
|
emitter.on('end', async () => {
|
|
937
|
-
// Wait for all queued jobs to complete
|
|
938
1109
|
await queue.onIdle();
|
|
939
|
-
resolve({ results, hasFailures });
|
|
1110
|
+
resolve({ resultUrls, results, hasFailures });
|
|
940
1111
|
});
|
|
941
1112
|
}
|
|
1113
|
+
emitter.on('error', handleEmitterError);
|
|
1114
|
+
if (singleAssembly) {
|
|
1115
|
+
runSingleAssemblyEmitter();
|
|
1116
|
+
}
|
|
1117
|
+
else {
|
|
1118
|
+
runPerFileEmitter();
|
|
1119
|
+
}
|
|
942
1120
|
});
|
|
943
1121
|
}
|
|
944
1122
|
// --- Command classes ---
|
|
@@ -977,34 +1155,20 @@ export class AssembliesCreateCommand extends AuthenticatedCommand {
|
|
|
977
1155
|
template = Option.String('--template,-t', {
|
|
978
1156
|
description: 'Specify a template to use for these assemblies',
|
|
979
1157
|
});
|
|
980
|
-
inputs =
|
|
981
|
-
description: 'Provide an input file or a directory',
|
|
982
|
-
});
|
|
1158
|
+
inputs = inputPathsOption();
|
|
983
1159
|
outputPath = Option.String('--output,-o', {
|
|
984
1160
|
description: 'Specify an output file or directory',
|
|
985
1161
|
});
|
|
986
1162
|
fields = Option.Array('--field,-f', {
|
|
987
1163
|
description: 'Set a template field (KEY=VAL)',
|
|
988
1164
|
});
|
|
989
|
-
watch =
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
description: 'Delete input files after they are processed',
|
|
997
|
-
});
|
|
998
|
-
reprocessStale = Option.Boolean('--reprocess-stale', false, {
|
|
999
|
-
description: 'Process inputs even if output is newer',
|
|
1000
|
-
});
|
|
1001
|
-
singleAssembly = Option.Boolean('--single-assembly', false, {
|
|
1002
|
-
description: 'Pass all input files to a single assembly instead of one assembly per file',
|
|
1003
|
-
});
|
|
1004
|
-
concurrency = Option.String('--concurrency,-c', {
|
|
1005
|
-
description: 'Maximum number of concurrent assemblies (default: 5)',
|
|
1006
|
-
validator: t.isNumber(),
|
|
1007
|
-
});
|
|
1165
|
+
watch = watchOption();
|
|
1166
|
+
recursive = recursiveOption();
|
|
1167
|
+
deleteAfterProcessing = deleteAfterProcessingOption();
|
|
1168
|
+
reprocessStale = reprocessStaleOption();
|
|
1169
|
+
singleAssembly = singleAssemblyOption();
|
|
1170
|
+
concurrency = concurrencyOption();
|
|
1171
|
+
printUrls = printUrlsOption();
|
|
1008
1172
|
async run() {
|
|
1009
1173
|
if (!this.steps && !this.template) {
|
|
1010
1174
|
this.output.error('assemblies create requires exactly one of either --steps or --template');
|
|
@@ -1015,34 +1179,29 @@ export class AssembliesCreateCommand extends AuthenticatedCommand {
|
|
|
1015
1179
|
return 1;
|
|
1016
1180
|
}
|
|
1017
1181
|
const inputList = this.inputs ?? [];
|
|
1018
|
-
if (inputList.length === 0 && this.watch) {
|
|
1019
|
-
this.output.error('assemblies create --watch requires at least one input');
|
|
1020
|
-
return 1;
|
|
1021
|
-
}
|
|
1022
1182
|
// Default to stdin only for `--steps` mode (common "pipe a file into a one-off assembly" use case).
|
|
1023
1183
|
// For `--template` mode, templates may be inputless or use /http/import, so stdin should be explicit (`--input -`).
|
|
1024
1184
|
if (this.steps && inputList.length === 0 && !process.stdin.isTTY) {
|
|
1025
1185
|
inputList.push('-');
|
|
1026
1186
|
}
|
|
1027
|
-
const fieldsMap =
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
if (eqIndex === -1) {
|
|
1031
|
-
this.output.error(`invalid argument for --field: '${field}'`);
|
|
1032
|
-
return 1;
|
|
1033
|
-
}
|
|
1034
|
-
const key = field.slice(0, eqIndex);
|
|
1035
|
-
const value = field.slice(eqIndex + 1);
|
|
1036
|
-
fieldsMap[key] = value;
|
|
1187
|
+
const fieldsMap = parseTemplateFieldAssignments(this.output, this.fields);
|
|
1188
|
+
if (this.fields != null && fieldsMap == null) {
|
|
1189
|
+
return 1;
|
|
1037
1190
|
}
|
|
1038
|
-
|
|
1039
|
-
this.
|
|
1191
|
+
const sharedValidationError = validateSharedFileProcessingOptions({
|
|
1192
|
+
explicitInputCount: this.inputs?.length ?? 0,
|
|
1193
|
+
singleAssembly: this.singleAssembly,
|
|
1194
|
+
watch: this.watch,
|
|
1195
|
+
watchRequiresInputsMessage: 'assemblies create --watch requires at least one input',
|
|
1196
|
+
});
|
|
1197
|
+
if (sharedValidationError != null) {
|
|
1198
|
+
this.output.error(sharedValidationError);
|
|
1040
1199
|
return 1;
|
|
1041
1200
|
}
|
|
1042
|
-
const { hasFailures } = await create(this.output, this.client, {
|
|
1201
|
+
const { hasFailures, resultUrls } = await create(this.output, this.client, {
|
|
1043
1202
|
steps: this.steps,
|
|
1044
1203
|
template: this.template,
|
|
1045
|
-
fields: fieldsMap,
|
|
1204
|
+
fields: fieldsMap ?? {},
|
|
1046
1205
|
watch: this.watch,
|
|
1047
1206
|
recursive: this.recursive,
|
|
1048
1207
|
inputs: inputList,
|
|
@@ -1050,8 +1209,11 @@ export class AssembliesCreateCommand extends AuthenticatedCommand {
|
|
|
1050
1209
|
del: this.deleteAfterProcessing,
|
|
1051
1210
|
reprocessStale: this.reprocessStale,
|
|
1052
1211
|
singleAssembly: this.singleAssembly,
|
|
1053
|
-
concurrency: this.concurrency,
|
|
1212
|
+
concurrency: this.concurrency == null ? undefined : Number(this.concurrency),
|
|
1054
1213
|
});
|
|
1214
|
+
if (this.printUrls) {
|
|
1215
|
+
printResultUrls(this.output, resultUrls);
|
|
1216
|
+
}
|
|
1055
1217
|
return hasFailures ? 1 : undefined;
|
|
1056
1218
|
}
|
|
1057
1219
|
}
|
|
@@ -1176,19 +1338,12 @@ export class AssembliesReplayCommand extends AuthenticatedCommand {
|
|
|
1176
1338
|
});
|
|
1177
1339
|
assemblyIds = Option.Rest({ required: 1 });
|
|
1178
1340
|
async run() {
|
|
1179
|
-
const fieldsMap =
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
if (eqIndex === -1) {
|
|
1183
|
-
this.output.error(`invalid argument for --field: '${field}'`);
|
|
1184
|
-
return 1;
|
|
1185
|
-
}
|
|
1186
|
-
const key = field.slice(0, eqIndex);
|
|
1187
|
-
const value = field.slice(eqIndex + 1);
|
|
1188
|
-
fieldsMap[key] = value;
|
|
1341
|
+
const fieldsMap = parseTemplateFieldAssignments(this.output, this.fields);
|
|
1342
|
+
if (this.fields != null && fieldsMap == null) {
|
|
1343
|
+
return 1;
|
|
1189
1344
|
}
|
|
1190
1345
|
await replay(this.output, this.client, {
|
|
1191
|
-
fields: fieldsMap,
|
|
1346
|
+
fields: fieldsMap ?? {},
|
|
1192
1347
|
reparse: this.reparseTemplate,
|
|
1193
1348
|
steps: this.steps,
|
|
1194
1349
|
notify_url: this.notifyUrl,
|