transloadit 6.1.1 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +7 -0
  2. package/dist/alphalib/stepParsing.d.ts +1 -0
  3. package/dist/alphalib/stepParsing.d.ts.map +1 -1
  4. package/dist/alphalib/stepParsing.js +15 -4
  5. package/dist/alphalib/stepParsing.js.map +1 -1
  6. package/dist/alphalib/types/builtinTemplates.d.ts +40 -0
  7. package/dist/alphalib/types/builtinTemplates.d.ts.map +1 -1
  8. package/dist/alphalib/types/builtinTemplates.js +29 -0
  9. package/dist/alphalib/types/builtinTemplates.js.map +1 -1
  10. package/dist/alphalib/types/responseErrors/DocumentConvertRobot.d.ts +17 -0
  11. package/dist/alphalib/types/responseErrors/DocumentConvertRobot.d.ts.map +1 -0
  12. package/dist/alphalib/types/responseErrors/DocumentConvertRobot.js +17 -0
  13. package/dist/alphalib/types/responseErrors/DocumentConvertRobot.js.map +1 -0
  14. package/dist/alphalib/types/robots/document-convert.d.ts.map +1 -1
  15. package/dist/alphalib/types/robots/document-convert.js +19 -0
  16. package/dist/alphalib/types/robots/document-convert.js.map +1 -1
  17. package/dist/alphalib/types/robots/document-extract.d.ts +32 -20
  18. package/dist/alphalib/types/robots/document-extract.d.ts.map +1 -1
  19. package/dist/alphalib/types/robots/document-extract.js +51 -1
  20. package/dist/alphalib/types/robots/document-extract.js.map +1 -1
  21. package/dist/alphalib/types/robots/html-convert.d.ts.map +1 -1
  22. package/dist/alphalib/types/robots/html-convert.js +1 -0
  23. package/dist/alphalib/types/robots/html-convert.js.map +1 -1
  24. package/dist/alphalib/types/robots/speech-transcribe.d.ts +14 -0
  25. package/dist/alphalib/types/robots/speech-transcribe.d.ts.map +1 -1
  26. package/dist/alphalib/types/robots/speech-transcribe.js +42 -2
  27. package/dist/alphalib/types/robots/speech-transcribe.js.map +1 -1
  28. package/dist/alphalib/types/robots/video-split.d.ts +11 -0
  29. package/dist/alphalib/types/robots/video-split.d.ts.map +1 -1
  30. package/dist/alphalib/types/robots/video-split.js +2 -1
  31. package/dist/alphalib/types/robots/video-split.js.map +1 -1
  32. package/dist/alphalib/types/robots/video-subtitle.d.ts +24 -24
  33. package/dist/alphalib/types/robots/video-subtitle.d.ts.map +1 -1
  34. package/dist/alphalib/types/robots/video-subtitle.js +12 -11
  35. package/dist/alphalib/types/robots/video-subtitle.js.map +1 -1
  36. package/dist/alphalib/types/robots/video-thumbs.d.ts +36 -36
  37. package/dist/alphalib/types/robots/video-thumbs.d.ts.map +1 -1
  38. package/dist/alphalib/types/robots/video-thumbs.js +8 -3
  39. package/dist/alphalib/types/robots/video-thumbs.js.map +1 -1
  40. package/dist/alphalib/types/template.d.ts.map +1 -1
  41. package/dist/alphalib/types/template.js +5 -2
  42. package/dist/alphalib/types/template.js.map +1 -1
  43. package/dist/cli/commands/assemblies.d.ts.map +1 -1
  44. package/dist/cli/commands/assemblies.js +8 -7
  45. package/dist/cli/commands/assemblies.js.map +1 -1
  46. package/dist/cli/commands/auth.d.ts.map +1 -1
  47. package/dist/cli/commands/auth.js +4 -3
  48. package/dist/cli/commands/auth.js.map +1 -1
  49. package/dist/cli/commands/templates.d.ts.map +1 -1
  50. package/dist/cli/commands/templates.js +11 -5
  51. package/dist/cli/commands/templates.js.map +1 -1
  52. package/dist/cli/stepsInput.d.ts +6 -2
  53. package/dist/cli/stepsInput.d.ts.map +1 -1
  54. package/dist/cli/stepsInput.js +24 -5
  55. package/dist/cli/stepsInput.js.map +1 -1
  56. package/dist/cli/types.d.ts +0 -1
  57. package/dist/cli/types.d.ts.map +1 -1
  58. package/dist/cli/types.js +0 -7
  59. package/dist/cli/types.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/alphalib/stepParsing.ts +16 -4
  62. package/src/alphalib/types/builtinTemplates.ts +37 -0
  63. package/src/alphalib/types/responseErrors/DocumentConvertRobot.ts +21 -0
  64. package/src/alphalib/types/robots/document-convert.ts +25 -0
  65. package/src/alphalib/types/robots/document-extract.ts +76 -0
  66. package/src/alphalib/types/robots/html-convert.ts +1 -0
  67. package/src/alphalib/types/robots/speech-transcribe.ts +47 -2
  68. package/src/alphalib/types/robots/video-split.ts +2 -1
  69. package/src/alphalib/types/robots/video-subtitle.ts +12 -11
  70. package/src/alphalib/types/robots/video-thumbs.ts +8 -3
  71. package/src/alphalib/types/template.ts +3 -0
  72. package/src/cli/commands/assemblies.ts +13 -7
  73. package/src/cli/commands/auth.ts +7 -2
  74. package/src/cli/commands/templates.ts +13 -6
  75. package/src/cli/stepsInput.ts +34 -5
  76. package/src/cli/types.ts +0 -9
@@ -59,7 +59,7 @@ export const meta: RobotMetaInput = {
59
59
  },
60
60
  },
61
61
  example_code_description:
62
- 'Transcribe an uploaded video into timestamped words, then burn in uppercase social captions with the spoken word highlighted. Captions normally contain up to three words, breaking at sentence endings and pauses longer than 0.4 seconds. A trailing function word can move to the next caption, allowing one extra word there:',
62
+ 'Transcribe an uploaded video into timestamped words, then burn in uppercase social captions with the spoken word highlighted. Captions normally contain up to three words, breaking at sentence endings and pauses longer than 0.4 seconds. Social phrasing can use one extra word to keep a short phrase together or avoid a sentence-final orphan:',
63
63
  purpose_sentence: 'adds subtitles and closed captions to videos',
64
64
  purpose_verb: 'subtitle',
65
65
  purpose_word: 'subtitle',
@@ -89,7 +89,7 @@ Determines if subtitles are added as a separate stream to the video (value \`"ex
89
89
  .enum(['social'])
90
90
  .optional()
91
91
  .describe(`
92
- Applies a ready-made appearance for burned subtitles: heavy Poppins text and a large font scaled with the video height. JSON captions render in uppercase in the lower third with a thick outline and a soft black shadow. Each caption grows from 85% to full size over 120 milliseconds, then its words stay in place while the highlight changes color. The transcript file is preserved. Existing styling parameters override the preset. ASS files retain their own styles. With timestamped JSON from \`/speech/transcribe\`, the active word is highlighted until the next word or caption starts; other words use \`font_color\`. Trailing commas and periods are omitted from social JSON captions; question marks and exclamation marks are preserved. Word highlighting and grouping require JSON input.
92
+ Applies a ready-made appearance for burned subtitles: heavy Poppins text, a thick outline, and a soft black shadow. JSON captions render in uppercase in the lower third. By default, JSON font sizing uses the shorter side of the video and fits each caption within the safe width, preferring one line and using at most two. Words are never split. Each caption grows from 85% to full size over 120 milliseconds, then its words stay in place while the highlight changes color. The transcript file is preserved. Existing styling parameters override the preset; an explicit \`font_size\` disables automatic fitting. ASS files retain their own styles. With timestamped JSON from \`/speech/transcribe\`, the active word is highlighted until the next word or caption starts; other words use \`font_color\`. Trailing commas and periods are omitted from social JSON captions; question marks and exclamation marks are preserved. Word highlighting and grouping require JSON input.
93
93
  `),
94
94
  highlight_color: subtitleColorSchema.default('FFD60A').describe(`
95
95
  The accent color of the active word in JSON subtitles. Only applies to burned JSON subtitles. With \`"style": "social"\`, the previous word stays highlighted through short gaps until the next word or caption starts. Without the social style, gaps use \`font_color\` for the entire caption.
@@ -102,7 +102,7 @@ ${subtitleColorDescription}
102
102
  .min(1)
103
103
  .default(3)
104
104
  .describe(`
105
- The target maximum number of timestamped JSON words shown in a caption. Captions break earlier at sentence-ending punctuation or pauses longer than 0.4 seconds. With \`"style": "social"\`, a trailing English function word such as “by,” “to,” or “the” moves to the next caption when possible, allowing at most one extra word in that caption. Single-word captions are preserved when the limit is 1. A caption stays visible until the next caption starts when the pause is at most 0.4 seconds, and disappears at its last word’s end during longer pauses. Does not change SRT, VTT, or ASS cues.
105
+ The target maximum number of timestamped JSON words shown in a caption. Captions break earlier at sentence-ending punctuation or pauses longer than 0.4 seconds. With \`"style": "social"\`, a trailing English function word such as “by,” “to,” or “the” moves to the next caption when possible. Short phrases such as “line of code” can stay together, and a sentence-final single word can join the previous caption. These adjustments allow at most one extra word per caption, without crossing sentence endings or long pauses. Single-word captions are preserved when the limit is 1. A caption stays visible until the next caption starts when the pause is at most 0.4 seconds, and disappears at its last word’s end during longer pauses. Does not change SRT, VTT, or ASS cues.
106
106
  `),
107
107
  border_style: z
108
108
  .enum(['box', 'outline', 'shadow'])
@@ -115,12 +115,13 @@ The color for the subtitle border. The default is opaque black. Only applies to
115
115
 
116
116
  ${subtitleColorDescription}
117
117
  `),
118
+ // Preserve omitted appearance fields so parsing cannot override the selected style's defaults.
118
119
  // TODO: Make font an enum
119
120
  font: z
120
121
  .string()
121
- .default('Arial')
122
+ .optional()
122
123
  .describe(`
123
- The font family to use. Also includes boldness and style of the font. With \`"style": "social"\`, the default is Poppins, using its ExtraBold weight for JSON captions. An explicit font overrides the style.
124
+ The font family to use. Also includes boldness and style of the font. The default is Arial. With \`"style": "social"\`, the default is ${videoSubtitleSocialStyle.font}, using its ExtraBold weight for JSON captions. An explicit font overrides the style.
124
125
 
125
126
  [Here](/docs/supported-formats/fonts/) is a list of all supported fonts.
126
127
  `),
@@ -133,15 +134,15 @@ ${subtitleColorDescription}
133
134
  .number()
134
135
  .int()
135
136
  .min(1)
136
- .default(16)
137
+ .optional()
137
138
  .describe(`
138
- Specifies the size of the text in the 288-pixel subtitle coordinate space, scaled with the video height. With \`"style": "social"\`, the default is 54. Poppins capital letters are about 7.5% of the video height at this size. An explicit value overrides the style.
139
+ Specifies the size of the text in the 288-pixel subtitle coordinate space, scaled with the video height. The default is 16. With \`"style": "social"\`, the default is ${videoSubtitleSocialStyle.font_size}. For social JSON captions, an omitted value scales this default with the shorter side of the video, increases it by 18% for portrait video, and shrinks captions as needed to fit at most two lines. Poppins capital letters are about 7.5% of the shorter side before fitting, or about 9% for portrait video. An explicit value keeps height-relative sizing and disables automatic fitting.
139
140
  `),
140
141
  bold: z
141
142
  .boolean()
142
- .default(false)
143
+ .optional()
143
144
  .describe(`
144
- Specifies whether the subtitle text should be bold. Only applies to burned subtitles. With \`"style": "social"\`, the default is \`true\`; an explicit \`false\` overrides the style.
145
+ Specifies whether the subtitle text should be bold. Only applies to burned subtitles. The default is \`false\`. With \`"style": "social"\`, the default is \`${videoSubtitleSocialStyle.bold}\`; an explicit \`false\` overrides the style.
145
146
  `),
146
147
  italic: z
147
148
  .boolean()
@@ -157,10 +158,10 @@ Specifies whether the subtitle text should be italic. Only applies to burned sub
157
158
  .optional()
158
159
  .nullable()
159
160
  .describe(`
160
- Specifies the width of the text outline in the 288-pixel subtitle coordinate space, scaled with the video height. Numeric strings such as \`"4"\` are also accepted. Only applies to burned subtitles with \`"outline"\` or \`"box"\` border styles. If omitted or \`null\`, the default width is used (2 with \`"style": "social"\`, about 3.7 pixels on a video 534 pixels high).
161
+ Specifies the width of the text outline in the 288-pixel subtitle coordinate space, scaled with the video height. Numeric strings such as \`"4"\` are also accepted. Only applies to burned subtitles with \`"outline"\` or \`"box"\` border styles. If omitted or \`null\`, the default width is used (2 with \`"style": "social"\`, proportional to the final fitted font size for social JSON captions).
161
162
  `),
162
163
  position: positionSchema.default('bottom').describe(`
163
- Specifies the position of the subtitles. With \`"style": "social"\`, the default bottom position places JSON captions in the lower third. An explicit value overrides the style.
164
+ Specifies the position of the subtitles. With \`"style": "social"\`, the default bottom position places JSON captions in the lower third, with extra bottom clearance on portrait video. An explicit value overrides the style.
164
165
  `),
165
166
  language: z
166
167
  .string()
@@ -94,7 +94,8 @@ The thumbnails are taken at regular intervals, determined by dividing the video
94
94
  To extract thumbnails for specific timestamps, use the \`offsets\` parameter.
95
95
  `),
96
96
  offsets: z
97
- .union([z.array(z.number()), z.array(percentageSchema)])
97
+ .union([z.array(z.number().finite()), z.array(percentageSchema)])
98
+ .nullable()
98
99
  .default([])
99
100
  .describe(`
100
101
  An array of offsets representing seconds of the file duration, such as \`[ 2, 45, 120 ]\`. Millisecond durations of a file can also be used by using decimal place values. For example, an offset from 1250 milliseconds would be represented with \`1.25\`. Offsets can also be percentage values such as \`[ "2%", "50%", "75%" ]\`.
@@ -102,6 +103,8 @@ An array of offsets representing seconds of the file duration, such as \`[ 2, 45
102
103
  This option cannot be used with the \`count\` parameter, and takes precedence if both are specified. Out-of-range offsets are silently ignored.
103
104
 
104
105
  When \`smart\` is \`true\`, smart selection ignores \`offsets\` and uses \`count\` to select from its own candidate timestamps. If no smart candidates can be extracted, the Robot falls back to standard extraction, where \`offsets\` takes precedence. Use \`smart: false\` to consistently extract the specified timestamps.
106
+
107
+ Use \`null\` to leave timestamp selection to \`count\` and \`smart\`.
105
108
  `),
106
109
  format: z
107
110
  .enum(['jpeg', 'jpg', 'png'])
@@ -114,18 +117,20 @@ The format of the extracted thumbnail. Supported values are \`"jpg"\`, \`"jpeg"\
114
117
  .int()
115
118
  .min(1)
116
119
  .max(1920)
120
+ .nullable()
117
121
  .optional()
118
122
  .describe(`
119
- The width of the thumbnail, in pixels. Defaults to the original width of the video.
123
+ The width of the thumbnail, in pixels. Omit or use \`null\` for the original width of the video.
120
124
  `),
121
125
  height: z
122
126
  .number()
123
127
  .int()
124
128
  .min(1)
125
129
  .max(1080)
130
+ .nullable()
126
131
  .optional()
127
132
  .describe(`
128
- The height of the thumbnail, in pixels. Defaults to the original height of the video.
133
+ The height of the thumbnail, in pixels. Omit or use \`null\` for the original height of the video.
129
134
  `),
130
135
  resize_strategy: resize_strategy.describe(`
131
136
  One of the [available resize strategies](/docs/topics/resize-strategies/).
@@ -9,6 +9,7 @@ import type {
9
9
  RobotsWithHiddenBotsAndFields,
10
10
  } from './robots/_index.ts'
11
11
  import type { RobotUse } from './robots/_instructions-primitives.ts'
12
+ import { refineDocumentExtractOcr } from './robots/document-extract.ts'
12
13
  import { refineTransloaditImportSelector } from './robots/transloadit-import.ts'
13
14
 
14
15
  export type Step = RobotsSchema
@@ -26,6 +27,7 @@ export const stepSchema: z.ZodType<Step> = z
26
27
  })
27
28
  .and(robotsSchema)
28
29
  .superRefine(refineTransloaditImportSelector)
30
+ .superRefine(refineDocumentExtractOcr)
29
31
  export const stepsSchema: z.ZodType<Steps> = z
30
32
  .record(stepSchema)
31
33
  .describe('Contains Assembly Instructions.')
@@ -45,6 +47,7 @@ export const stepSchemaWithHiddenFields: z.ZodType<StepWithHiddenFields> = z
45
47
  })
46
48
  .and(robotsWithHiddenBotsAndFieldsSchema)
47
49
  .superRefine(refineTransloaditImportSelector)
50
+ .superRefine(refineDocumentExtractOcr)
48
51
  export const stepsSchemaWithHiddenFields: z.ZodType<StepsWithHiddenFields> = z
49
52
  .record(stepSchemaWithHiddenFields)
50
53
  .describe('Contains Assembly Instructions.')
@@ -227,7 +227,7 @@ export async function replay(
227
227
  ): Promise<void> {
228
228
  if (steps) {
229
229
  try {
230
- await apiCall(await readStepsInputFile(steps))
230
+ await apiCall(await readStepsInputFile(steps, output))
231
231
  } catch (err) {
232
232
  const error = ensureError(err)
233
233
  output.error(error.message)
@@ -735,7 +735,9 @@ async function resolveResultDownloadTargets({
735
735
  throw new Error('stdout can only receive a single result file')
736
736
  }
737
737
 
738
- throw new Error('file outputs can only receive a single result file')
738
+ throw new Error(
739
+ 'file outputs can only receive a single result file; use -o <dir>/ to write multiple results to a directory',
740
+ )
739
741
  }
740
742
 
741
743
  return getSingleResultDownloadTarget(allFiles, outputPath)
@@ -1351,18 +1353,20 @@ export async function create(
1351
1353
  // to avoid zod adding default values that the API may reject
1352
1354
  let effectiveStepsData = stepsData
1353
1355
  if (steps) {
1354
- effectiveStepsData = await readStepsInputFile(steps)
1356
+ effectiveStepsData = await readStepsInputFile(steps, outputctl)
1355
1357
  }
1356
1358
 
1357
1359
  // Determine output stat async before entering the Promise constructor
1358
1360
  let outstat: StatLike | undefined
1359
1361
  if (resolvedOutput != null) {
1362
+ const isDirectoryOutput =
1363
+ outputMode === 'directory' || resolvedOutput.endsWith('/') || resolvedOutput.endsWith('\\')
1360
1364
  const [err, stat] = await tryCatch(myStat(process.stdout, resolvedOutput))
1361
1365
  if (err && (!isErrnoException(err) || err.code !== 'ENOENT')) throw err
1362
1366
  outstat =
1363
1367
  stat ??
1364
1368
  ({
1365
- isDirectory: () => outputMode === 'directory',
1369
+ isDirectory: () => isDirectoryOutput,
1366
1370
  } satisfies StatLike)
1367
1371
 
1368
1372
  if (outputMode === 'directory' && stat != null && !stat.isDirectory()) {
@@ -1826,7 +1830,8 @@ export class AssembliesCreateCommand extends AuthenticatedCommand {
1826
1830
  inputs = inputPathsOption()
1827
1831
 
1828
1832
  outputPath = Option.String('--output,-o', {
1829
- description: 'Specify an output file or directory',
1833
+ description:
1834
+ 'Specify an output file or directory; a trailing / or \\ creates a directory recursively',
1830
1835
  })
1831
1836
 
1832
1837
  fields = Option.Array('--field,-f', {
@@ -1945,7 +1950,8 @@ export class RunCommand extends AuthenticatedCommand {
1945
1950
  inputs = inputPathsOption()
1946
1951
 
1947
1952
  outputPath = Option.String('--output,-o', {
1948
- description: 'Specify an output file or directory',
1953
+ description:
1954
+ 'Specify an output file or directory; a trailing / or \\ creates a directory recursively',
1949
1955
  })
1950
1956
 
1951
1957
  fields = Option.Array('--field,-f', {
@@ -2012,7 +2018,7 @@ export class RunCommand extends AuthenticatedCommand {
2012
2018
  }
2013
2019
 
2014
2020
  const { hasFailures, resultUrls } = await create(this.output, this.client, {
2015
- stepsData: parseStepsInputJson(JSON.stringify(compiled.instructions.steps)),
2021
+ stepsData: parseStepsInputJson(JSON.stringify(compiled.instructions.steps), this.output),
2016
2022
  fields: fieldsMap ?? {},
2017
2023
  watch: this.watch,
2018
2024
  recursive: this.recursive,
@@ -22,6 +22,7 @@ import {
22
22
  requireCliCredentials,
23
23
  resolveCliConfig,
24
24
  } from '../helpers.ts'
25
+ import { createCliStepsSchema } from '../stepsInput.ts'
25
26
  import { UnauthenticatedCommand } from './BaseCommand.ts'
26
27
 
27
28
  type UrlParamPrimitive = string | number | boolean
@@ -113,6 +114,7 @@ function generateSignature(
113
114
  input: string,
114
115
  credentials: CliKeySecretCredentials,
115
116
  algorithm?: string,
117
+ output: Pick<IOutputCtl, 'warn'> = console,
116
118
  ): OutputResult {
117
119
  const { authKey } = credentials
118
120
  let params: CliSignatureParams
@@ -120,7 +122,10 @@ function generateSignature(
120
122
  if (input === '') {
121
123
  params = { auth: { key: authKey } }
122
124
  } else {
123
- const parsedResult = parseJsonObject(input, cliSignatureParamsSchema)
125
+ const parsedResult = parseJsonObject(
126
+ input,
127
+ cliSignatureParamsSchema.extend({ steps: createCliStepsSchema(output).optional() }),
128
+ )
124
129
  if (!parsedResult.ok) {
125
130
  return { ok: false, error: parsedResult.error }
126
131
  }
@@ -347,7 +352,7 @@ export class SignatureCommand extends UnauthenticatedCommand {
347
352
 
348
353
  const { content } = await readCliInput({ allowStdinWhenNoPath: true })
349
354
  const rawInput = (content ?? '').trim()
350
- const result = generateSignature(rawInput, credentials, this.algorithm)
355
+ const result = generateSignature(rawInput, credentials, this.algorithm, this.output)
351
356
 
352
357
  if (result.ok) {
353
358
  process.stdout.write(`${result.output}\n`)
@@ -1,7 +1,7 @@
1
1
  import type { TemplateContent } from '../../apiTypes.ts'
2
2
  import type { Transloadit } from '../../Transloadit.ts'
3
3
  import type { IOutputCtl } from '../OutputCtl.ts'
4
- import type { TemplateFile } from '../types.ts'
4
+ import type { TemplateFile, TemplateFileData } from '../types.ts'
5
5
 
6
6
  import fsp from 'node:fs/promises'
7
7
  import path from 'node:path'
@@ -13,9 +13,9 @@ import { z } from 'zod'
13
13
 
14
14
  import { tryCatch } from '../../alphalib/tryCatch.ts'
15
15
  import { createReadStream, formatAPIError, streamToBuffer } from '../helpers.ts'
16
- import { parseStepsInputJson } from '../stepsInput.ts'
16
+ import { createCliStepsSchema, parseStepsInputJson } from '../stepsInput.ts'
17
17
  import ModifiedLookup from '../template-last-modified.ts'
18
- import { ensureError, isTransloaditAPIError, TemplateFileDataSchema } from '../types.ts'
18
+ import { ensureError, isTransloaditAPIError } from '../types.ts'
19
19
  import { AuthenticatedCommand } from './BaseCommand.ts'
20
20
 
21
21
  const rreaddirAsync = promisify(rreaddir)
@@ -62,7 +62,7 @@ export async function create(
62
62
  try {
63
63
  const buf = await streamToBuffer(createReadStream(file))
64
64
 
65
- const steps = parseStepsInputJson(buf.toString())
65
+ const steps = parseStepsInputJson(buf.toString(), output)
66
66
 
67
67
  const result = await client.createTemplate({
68
68
  name,
@@ -107,7 +107,7 @@ export async function modify(
107
107
  let newName = name
108
108
 
109
109
  if (buf.length > 0) {
110
- steps = parseStepsInputJson(buf.toString()) as TemplateContent['steps']
110
+ steps = parseStepsInputJson(buf.toString(), output) as TemplateContent['steps']
111
111
  }
112
112
 
113
113
  if (!name || buf.length === 0) {
@@ -225,6 +225,13 @@ export async function sync(
225
225
  client: Transloadit,
226
226
  { files, recursive }: TemplateSyncOptions,
227
227
  ): Promise<void> {
228
+ const templateFileDataSchema: z.ZodType<TemplateFileData> = z
229
+ .object({
230
+ transloadit_template_id: z.string().optional(),
231
+ steps: createCliStepsSchema(output).optional(),
232
+ })
233
+ .passthrough()
234
+
228
235
  // Promise [String] -- all files in the directory tree
229
236
  const relevantFilesNested = await Promise.all(
230
237
  files.map(async (file) => {
@@ -263,7 +270,7 @@ export async function sync(
263
270
  try {
264
271
  const data = await fsp.readFile(file, 'utf8')
265
272
  const parsed: unknown = JSON.parse(data)
266
- const validated = TemplateFileDataSchema.safeParse(parsed)
273
+ const validated = templateFileDataSchema.safeParse(parsed)
267
274
  if (!validated.success) return null
268
275
  return 'transloadit_template_id' in validated.data ? { file, data: validated.data } : null
269
276
  } catch (e) {
@@ -1,14 +1,40 @@
1
1
  import type { StepsInput } from '../alphalib/types/template.ts'
2
+ import type { IOutputCtl } from './OutputCtl.ts'
2
3
 
3
4
  import fsp from 'node:fs/promises'
4
5
 
5
6
  import { z } from 'zod'
6
7
 
7
- import { stepsSchema } from '../alphalib/types/template.ts'
8
+ import { robotsSchema } from '../alphalib/types/robots/_index.ts'
9
+ import { refineDocumentExtractOcr } from '../alphalib/types/robots/document-extract.ts'
10
+ import { refineTransloaditImportSelector } from '../alphalib/types/robots/transloadit-import.ts'
8
11
 
9
12
  const stepsWrapperSchema = z.object({ steps: z.record(z.unknown()) })
10
13
 
11
- export function parseStepsInputJson(content: string): StepsInput {
14
+ /** CLI inputs can be partial overrides; API2 validates OCR after merging their runtime context. */
15
+ export function createCliStepsSchema(
16
+ output: Pick<IOutputCtl, 'warn'> = console,
17
+ ): z.ZodType<StepsInput> {
18
+ const cliStepSchema = robotsSchema
19
+ .superRefine(refineTransloaditImportSelector)
20
+ .superRefine((step, context) => {
21
+ // API2 merges Template defaults before Robot validation. New OCR checks must not reject
22
+ // partial --steps overrides in a minor release; explicit Zod schemas remain strict.
23
+ refineDocumentExtractOcr(step, {
24
+ ...context,
25
+ addIssue(issue) {
26
+ const field = [...context.path, ...(issue.path ?? [])].join('.')
27
+ output.warn(`${field}: ${issue.message ?? 'Invalid OCR dependency.'}`)
28
+ },
29
+ })
30
+ })
31
+ return z.record(cliStepSchema)
32
+ }
33
+
34
+ export function parseStepsInputJson(
35
+ content: string,
36
+ output: Pick<IOutputCtl, 'warn'> = console,
37
+ ): StepsInput {
12
38
  const parsed: unknown = JSON.parse(content)
13
39
  const wrapped = stepsWrapperSchema.safeParse(parsed)
14
40
  let stepsInput = parsed
@@ -21,7 +47,7 @@ export function parseStepsInputJson(content: string): StepsInput {
21
47
  }
22
48
  stepsInput = validatedWrapper.data.steps
23
49
  }
24
- const validated = stepsSchema.safeParse(stepsInput)
50
+ const validated = createCliStepsSchema(output).safeParse(stepsInput)
25
51
  if (!validated.success) {
26
52
  throw new Error(`Invalid steps format: ${validated.error.message}`)
27
53
  }
@@ -42,7 +68,10 @@ export function parseStepsInputJson(content: string): StepsInput {
42
68
  ) as StepsInput
43
69
  }
44
70
 
45
- export async function readStepsInputFile(filePath: string): Promise<StepsInput> {
71
+ export async function readStepsInputFile(
72
+ filePath: string,
73
+ output: Pick<IOutputCtl, 'warn'> = console,
74
+ ): Promise<StepsInput> {
46
75
  const content = await fsp.readFile(filePath, 'utf8')
47
- return parseStepsInputJson(content)
76
+ return parseStepsInputJson(content, output)
48
77
  }
package/src/cli/types.ts CHANGED
@@ -2,8 +2,6 @@ import type { Steps } from '../alphalib/types/template.ts'
2
2
 
3
3
  import { z } from 'zod'
4
4
 
5
- import { optionalStepsSchema } from '../alphalib/types/template.ts'
6
-
7
5
  // Zod schemas for runtime validation
8
6
  const APIErrorSchema = z.object({
9
7
  error: z.string(),
@@ -36,13 +34,6 @@ export interface TemplateFileData {
36
34
  [key: string]: unknown // passthrough
37
35
  }
38
36
 
39
- export const TemplateFileDataSchema: z.ZodType<TemplateFileData> = z
40
- .object({
41
- transloadit_template_id: z.string().optional(),
42
- steps: optionalStepsSchema,
43
- })
44
- .passthrough() as z.ZodType<TemplateFileData>
45
-
46
37
  export interface TemplateFile {
47
38
  file: string
48
39
  data: TemplateFileData