@transloadit/node 4.2.0 → 4.3.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 (66) hide show
  1. package/README.md +58 -0
  2. package/dist/Transloadit.d.ts +19 -0
  3. package/dist/Transloadit.d.ts.map +1 -1
  4. package/dist/Transloadit.js +22 -0
  5. package/dist/Transloadit.js.map +1 -1
  6. package/dist/alphalib/assembly-linter.d.ts +123 -0
  7. package/dist/alphalib/assembly-linter.d.ts.map +1 -0
  8. package/dist/alphalib/assembly-linter.js +1142 -0
  9. package/dist/alphalib/assembly-linter.js.map +1 -0
  10. package/dist/alphalib/assembly-linter.lang.en.d.ts +87 -0
  11. package/dist/alphalib/assembly-linter.lang.en.d.ts.map +1 -0
  12. package/dist/alphalib/assembly-linter.lang.en.js +326 -0
  13. package/dist/alphalib/assembly-linter.lang.en.js.map +1 -0
  14. package/dist/alphalib/object.d.ts +20 -0
  15. package/dist/alphalib/object.d.ts.map +1 -0
  16. package/dist/alphalib/object.js +23 -0
  17. package/dist/alphalib/object.js.map +1 -0
  18. package/dist/alphalib/stepParsing.d.ts +93 -0
  19. package/dist/alphalib/stepParsing.d.ts.map +1 -0
  20. package/dist/alphalib/stepParsing.js +1154 -0
  21. package/dist/alphalib/stepParsing.js.map +1 -0
  22. package/dist/alphalib/templateMerge.d.ts +4 -0
  23. package/dist/alphalib/templateMerge.d.ts.map +1 -0
  24. package/dist/alphalib/templateMerge.js +22 -0
  25. package/dist/alphalib/templateMerge.js.map +1 -0
  26. package/dist/cli/commands/assemblies.d.ts +20 -1
  27. package/dist/cli/commands/assemblies.d.ts.map +1 -1
  28. package/dist/cli/commands/assemblies.js +137 -2
  29. package/dist/cli/commands/assemblies.js.map +1 -1
  30. package/dist/cli/commands/auth.d.ts.map +1 -1
  31. package/dist/cli/commands/auth.js +19 -19
  32. package/dist/cli/commands/auth.js.map +1 -1
  33. package/dist/cli/commands/index.d.ts.map +1 -1
  34. package/dist/cli/commands/index.js +2 -1
  35. package/dist/cli/commands/index.js.map +1 -1
  36. package/dist/cli/docs/assemblyLintingExamples.d.ts +2 -0
  37. package/dist/cli/docs/assemblyLintingExamples.d.ts.map +1 -0
  38. package/dist/cli/docs/assemblyLintingExamples.js +10 -0
  39. package/dist/cli/docs/assemblyLintingExamples.js.map +1 -0
  40. package/dist/cli/helpers.d.ts +11 -0
  41. package/dist/cli/helpers.d.ts.map +1 -1
  42. package/dist/cli/helpers.js +29 -0
  43. package/dist/cli/helpers.js.map +1 -1
  44. package/dist/lintAssemblyInput.d.ts +10 -0
  45. package/dist/lintAssemblyInput.d.ts.map +1 -0
  46. package/dist/lintAssemblyInput.js +73 -0
  47. package/dist/lintAssemblyInput.js.map +1 -0
  48. package/dist/lintAssemblyInstructions.d.ts +29 -0
  49. package/dist/lintAssemblyInstructions.d.ts.map +1 -0
  50. package/dist/lintAssemblyInstructions.js +33 -0
  51. package/dist/lintAssemblyInstructions.js.map +1 -0
  52. package/package.json +5 -2
  53. package/src/Transloadit.ts +39 -0
  54. package/src/alphalib/assembly-linter.lang.en.ts +393 -0
  55. package/src/alphalib/assembly-linter.ts +1475 -0
  56. package/src/alphalib/object.ts +27 -0
  57. package/src/alphalib/stepParsing.ts +1465 -0
  58. package/src/alphalib/templateMerge.ts +32 -0
  59. package/src/alphalib/typings/json-to-ast.d.ts +34 -0
  60. package/src/cli/commands/assemblies.ts +161 -2
  61. package/src/cli/commands/auth.ts +19 -22
  62. package/src/cli/commands/index.ts +2 -0
  63. package/src/cli/docs/assemblyLintingExamples.ts +9 -0
  64. package/src/cli/helpers.ts +50 -0
  65. package/src/lintAssemblyInput.ts +89 -0
  66. package/src/lintAssemblyInstructions.ts +72 -0
@@ -0,0 +1,32 @@
1
+ import merge from 'lodash-es/merge.js'
2
+ import type { ResponseTemplateContent, TemplateContent } from '../apiTypes.ts'
3
+ import type { AssemblyInstructionsInput } from './types/template.ts'
4
+
5
+ export function mergeTemplateContent(
6
+ template: TemplateContent | ResponseTemplateContent,
7
+ params?: AssemblyInstructionsInput,
8
+ ): AssemblyInstructionsInput {
9
+ const templateContent = structuredClone(template) as TemplateContent | ResponseTemplateContent
10
+ const templateRecord = templateContent as Record<string, unknown>
11
+
12
+ if (templateContent.allow_steps_override == null) {
13
+ templateContent.allow_steps_override = true
14
+ }
15
+
16
+ if (params?.steps != null && templateContent.allow_steps_override === false) {
17
+ throw new Error('TEMPLATE_DENIES_STEPS_OVERRIDE')
18
+ }
19
+
20
+ if (params == null) {
21
+ return templateContent as AssemblyInstructionsInput
22
+ }
23
+
24
+ const paramsRecord = { ...params } as Record<string, unknown>
25
+ for (const key of Object.keys(templateRecord)) {
26
+ if (paramsRecord[key] === null && templateRecord[key] !== null) {
27
+ paramsRecord[key] = templateRecord[key]
28
+ }
29
+ }
30
+
31
+ return merge({}, templateRecord, paramsRecord) as AssemblyInstructionsInput
32
+ }
@@ -0,0 +1,34 @@
1
+ declare module 'json-to-ast' {
2
+ export interface JsonToAstLocation {
3
+ start: { line: number; column: number }
4
+ }
5
+
6
+ export interface ObjectPropertyNode {
7
+ key: { value: string; loc?: JsonToAstLocation }
8
+ value: ValueNode
9
+ loc?: JsonToAstLocation
10
+ }
11
+
12
+ export interface LiteralNode {
13
+ type: 'Literal'
14
+ value: unknown
15
+ loc?: JsonToAstLocation
16
+ }
17
+
18
+ export interface ArrayNode {
19
+ type: 'Array'
20
+ children: ValueNode[]
21
+ loc?: JsonToAstLocation
22
+ }
23
+
24
+ export interface ObjectNode {
25
+ type: 'Object'
26
+ children: ObjectPropertyNode[]
27
+ loc?: JsonToAstLocation
28
+ }
29
+
30
+ export type ValueNode = LiteralNode | ArrayNode | ObjectNode
31
+
32
+ const parse: (source: string, options?: { loc?: boolean }) => ValueNode
33
+ export default parse
34
+ }
@@ -13,15 +13,19 @@ import got from 'got'
13
13
  import PQueue from 'p-queue'
14
14
  import * as t from 'typanion'
15
15
  import { z } from 'zod'
16
+ import { formatLintIssue } from '../../alphalib/assembly-linter.lang.en.ts'
16
17
  import { tryCatch } from '../../alphalib/tryCatch.ts'
17
18
  import type { Steps, StepsInput } from '../../alphalib/types/template.ts'
18
19
  import { stepsSchema } from '../../alphalib/types/template.ts'
19
20
  import type { CreateAssemblyParams, ReplayAssemblyParams } from '../../apiTypes.ts'
21
+ import type { LintFatalLevel } from '../../lintAssemblyInstructions.ts'
22
+ import { lintAssemblyInstructions } from '../../lintAssemblyInstructions.ts'
20
23
  import type { CreateAssemblyOptions, Transloadit } from '../../Transloadit.ts'
21
- import { createReadStream, formatAPIError, streamToBuffer } from '../helpers.ts'
24
+ import { lintingExamples } from '../docs/assemblyLintingExamples.ts'
25
+ import { createReadStream, formatAPIError, readCliInput, streamToBuffer } from '../helpers.ts'
22
26
  import type { IOutputCtl } from '../OutputCtl.ts'
23
27
  import { ensureError, isErrnoException } from '../types.ts'
24
- import { AuthenticatedCommand } from './BaseCommand.ts'
28
+ import { AuthenticatedCommand, UnauthenticatedCommand } from './BaseCommand.ts'
25
29
 
26
30
  // --- From assemblies.ts: Schemas and interfaces ---
27
31
  export interface AssemblyListOptions {
@@ -48,6 +52,15 @@ export interface AssemblyReplayOptions {
48
52
  assemblies: string[]
49
53
  }
50
54
 
55
+ export interface AssemblyLintOptions {
56
+ steps?: string
57
+ template?: string
58
+ fatal?: LintFatalLevel
59
+ fix?: boolean
60
+ providedInput?: string
61
+ json?: boolean
62
+ }
63
+
51
64
  const AssemblySchema = z.object({
52
65
  id: z.string(),
53
66
  })
@@ -163,6 +176,97 @@ export async function replay(
163
176
  }
164
177
  }
165
178
 
179
+ export async function lint(
180
+ output: IOutputCtl,
181
+ client: Transloadit | null,
182
+ { steps, template, fatal, fix, providedInput, json }: AssemblyLintOptions,
183
+ ): Promise<number> {
184
+ let content: string | null
185
+ let isStdin: boolean
186
+ let inputPath: string | undefined
187
+ try {
188
+ ;({
189
+ content,
190
+ isStdin,
191
+ path: inputPath,
192
+ } = await readCliInput({
193
+ inputPath: steps,
194
+ providedInput,
195
+ allowStdinWhenNoPath: true,
196
+ }))
197
+ } catch (error) {
198
+ output.error(ensureError(error).message)
199
+ return 1
200
+ }
201
+
202
+ if (content == null && template == null) {
203
+ output.error('assemblies lint requires --steps or stdin input unless --template is provided')
204
+ return 1
205
+ }
206
+
207
+ if (fix && content == null && template != null) {
208
+ output.error('assemblies lint --fix requires local instructions (stdin or --steps)')
209
+ return 1
210
+ }
211
+
212
+ let result: Awaited<ReturnType<typeof lintAssemblyInstructions>>
213
+ try {
214
+ if (template != null) {
215
+ if (!client) {
216
+ output.error('Missing client for template lookup')
217
+ return 1
218
+ }
219
+ result = await client.lintAssemblyInstructions({
220
+ assemblyInstructions: content ?? undefined,
221
+ templateId: template,
222
+ fatal,
223
+ fix,
224
+ })
225
+ } else {
226
+ result = await lintAssemblyInstructions({
227
+ assemblyInstructions: content ?? undefined,
228
+ fatal,
229
+ fix,
230
+ })
231
+ }
232
+ } catch (error) {
233
+ output.error(ensureError(error).message)
234
+ return 1
235
+ }
236
+
237
+ const issues = result.issues
238
+
239
+ if (fix && isStdin) {
240
+ if (result.fixedInstructions == null) {
241
+ output.error('No fixed output available.')
242
+ return 1
243
+ }
244
+ process.stdout.write(`${result.fixedInstructions}\n`)
245
+ for (const issue of issues) {
246
+ const line = formatLintIssue(issue)
247
+ if (issue.type === 'warning') output.warn(line)
248
+ else output.error(line)
249
+ }
250
+ return result.success ? 0 : 1
251
+ }
252
+
253
+ if (fix && inputPath && result.fixedInstructions != null) {
254
+ await fsp.writeFile(inputPath, result.fixedInstructions)
255
+ }
256
+
257
+ if (json) {
258
+ output.print({ ...result, issues }, result)
259
+ } else if (issues.length === 0) {
260
+ output.print('No issues found', result)
261
+ } else {
262
+ for (const issue of issues) {
263
+ output.print(formatLintIssue(issue), issue)
264
+ }
265
+ }
266
+
267
+ return result.success ? 0 : 1
268
+ }
269
+
166
270
  // --- From assemblies-create.ts: Helper classes and functions ---
167
271
  interface NodeWatcher {
168
272
  on(event: 'error', listener: (err: Error) => void): void
@@ -1373,3 +1477,58 @@ export class AssembliesReplayCommand extends AuthenticatedCommand {
1373
1477
  return undefined
1374
1478
  }
1375
1479
  }
1480
+
1481
+ export class AssembliesLintCommand extends UnauthenticatedCommand {
1482
+ static override paths = [
1483
+ ['assemblies', 'lint'],
1484
+ ['assembly', 'lint'],
1485
+ ['a', 'lint'],
1486
+ ]
1487
+
1488
+ static override usage = Command.Usage({
1489
+ category: 'Assemblies',
1490
+ description: 'Lint Assembly Instructions',
1491
+ details: `
1492
+ Lint Assembly Instructions locally using Transloadit's linter.
1493
+ Provide instructions via --steps or stdin (steps-only JSON is accepted).
1494
+ Optionally pass --template to
1495
+ merge template content with steps before linting (same merge behavior as the API).
1496
+ `,
1497
+ examples: lintingExamples,
1498
+ })
1499
+
1500
+ steps = Option.String('--steps,-s', {
1501
+ description: 'JSON file with Assembly Instructions (use "-" for stdin)',
1502
+ })
1503
+
1504
+ template = Option.String('--template,-t', {
1505
+ description:
1506
+ 'Template ID to merge before linting. If the template forbids step overrides, linting will fail when steps are provided.',
1507
+ })
1508
+
1509
+ fatal = Option.String('--fatal', {
1510
+ description: 'Treat issues at this level as fatal (error or warning)',
1511
+ validator: t.isEnum(['error', 'warning']),
1512
+ })
1513
+
1514
+ fix = Option.Boolean('--fix', false, {
1515
+ description:
1516
+ 'Apply auto-fixes. For files, overwrites in place. For stdin, writes fixed JSON to stdout.',
1517
+ })
1518
+
1519
+ protected async run(): Promise<number | undefined> {
1520
+ let client: Transloadit | null = null
1521
+ if (this.template) {
1522
+ if (!this.setupClient()) return 1
1523
+ client = this.client
1524
+ }
1525
+
1526
+ return await lint(this.output, client, {
1527
+ steps: this.steps,
1528
+ template: this.template,
1529
+ fatal: this.fatal as LintFatalLevel | undefined,
1530
+ fix: this.fix,
1531
+ json: this.json,
1532
+ })
1533
+ }
1534
+ }
@@ -8,7 +8,7 @@ import {
8
8
  } from '../../alphalib/types/template.ts'
9
9
  import type { OptionalAuthParams } from '../../apiTypes.ts'
10
10
  import { Transloadit } from '../../Transloadit.ts'
11
- import { getEnvCredentials } from '../helpers.ts'
11
+ import { getEnvCredentials, readCliInput } from '../helpers.ts'
12
12
  import { UnauthenticatedCommand } from './BaseCommand.ts'
13
13
 
14
14
  type UrlParamPrimitive = string | number | boolean
@@ -68,19 +68,6 @@ function normalizeUrlParams(params?: Record<string, unknown>): NormalizedUrlPara
68
68
  return normalized
69
69
  }
70
70
 
71
- async function readStdin(): Promise<string> {
72
- if (process.stdin.isTTY) return ''
73
-
74
- process.stdin.setEncoding('utf8')
75
- let data = ''
76
-
77
- for await (const chunk of process.stdin) {
78
- data += chunk
79
- }
80
-
81
- return data
82
- }
83
-
84
71
  const getCredentials = getEnvCredentials
85
72
 
86
73
  // Result type for signature operations
@@ -217,8 +204,12 @@ export async function runSig(options: RunSigOptions = {}): Promise<void> {
217
204
  return
218
205
  }
219
206
 
220
- const rawInput = options.providedInput ?? (await readStdin())
221
- const result = generateSignature(rawInput.trim(), credentials, options.algorithm)
207
+ const { content } = await readCliInput({
208
+ providedInput: options.providedInput,
209
+ allowStdinWhenNoPath: true,
210
+ })
211
+ const rawInput = (content ?? '').trim()
212
+ const result = generateSignature(rawInput, credentials, options.algorithm)
222
213
 
223
214
  if (result.ok) {
224
215
  process.stdout.write(`${result.output}\n`)
@@ -238,8 +229,12 @@ export async function runSmartSig(options: RunSmartSigOptions = {}): Promise<voi
238
229
  return
239
230
  }
240
231
 
241
- const rawInput = options.providedInput ?? (await readStdin())
242
- const result = generateSmartCdnUrl(rawInput.trim(), credentials)
232
+ const { content } = await readCliInput({
233
+ providedInput: options.providedInput,
234
+ allowStdinWhenNoPath: true,
235
+ })
236
+ const rawInput = (content ?? '').trim()
237
+ const result = generateSmartCdnUrl(rawInput, credentials)
243
238
 
244
239
  if (result.ok) {
245
240
  process.stdout.write(`${result.output}\n`)
@@ -287,8 +282,9 @@ export class SignatureCommand extends UnauthenticatedCommand {
287
282
  return 1
288
283
  }
289
284
 
290
- const rawInput = await readStdin()
291
- const result = generateSignature(rawInput.trim(), credentials, this.algorithm)
285
+ const { content } = await readCliInput({ allowStdinWhenNoPath: true })
286
+ const rawInput = (content ?? '').trim()
287
+ const result = generateSignature(rawInput, credentials, this.algorithm)
292
288
 
293
289
  if (result.ok) {
294
290
  process.stdout.write(`${result.output}\n`)
@@ -340,8 +336,9 @@ export class SmartCdnSignatureCommand extends UnauthenticatedCommand {
340
336
  return 1
341
337
  }
342
338
 
343
- const rawInput = await readStdin()
344
- const result = generateSmartCdnUrl(rawInput.trim(), credentials)
339
+ const { content } = await readCliInput({ allowStdinWhenNoPath: true })
340
+ const rawInput = (content ?? '').trim()
341
+ const result = generateSmartCdnUrl(rawInput, credentials)
345
342
 
346
343
  if (result.ok) {
347
344
  process.stdout.write(`${result.output}\n`)
@@ -6,6 +6,7 @@ import {
6
6
  AssembliesCreateCommand,
7
7
  AssembliesDeleteCommand,
8
8
  AssembliesGetCommand,
9
+ AssembliesLintCommand,
9
10
  AssembliesListCommand,
10
11
  AssembliesReplayCommand,
11
12
  } from './assemblies.ts'
@@ -46,6 +47,7 @@ export function createCli(): Cli {
46
47
  cli.register(AssembliesGetCommand)
47
48
  cli.register(AssembliesDeleteCommand)
48
49
  cli.register(AssembliesReplayCommand)
50
+ cli.register(AssembliesLintCommand)
49
51
 
50
52
  // Templates commands
51
53
  cli.register(TemplatesCreateCommand)
@@ -0,0 +1,9 @@
1
+ export const lintingExamples: Array<[string, string]> = [
2
+ ['Lint a steps file', 'transloadit assemblies lint --steps steps.json'],
3
+ ['Lint from stdin', 'cat steps.json | transloadit assemblies lint --steps -'],
4
+ [
5
+ 'Lint with template merge',
6
+ 'transloadit assemblies lint --template TEMPLATE_ID --steps steps.json',
7
+ ],
8
+ ['Auto-fix in place', 'transloadit assemblies lint --steps steps.json --fix'],
9
+ ]
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs'
2
+ import fsp from 'node:fs/promises'
2
3
  import type { Readable } from 'node:stream'
3
4
  import { isAPIError } from './types.ts'
4
5
 
@@ -24,6 +25,55 @@ export async function streamToBuffer(stream: Readable): Promise<Buffer> {
24
25
  return Buffer.concat(chunks)
25
26
  }
26
27
 
28
+ async function readStdin(): Promise<string> {
29
+ if (process.stdin.isTTY) return ''
30
+
31
+ process.stdin.setEncoding('utf8')
32
+ let data = ''
33
+
34
+ for await (const chunk of process.stdin) {
35
+ data += chunk
36
+ }
37
+
38
+ return data
39
+ }
40
+
41
+ export interface CliInputResult {
42
+ content: string | null
43
+ isStdin: boolean
44
+ path?: string
45
+ }
46
+
47
+ export interface ReadCliInputOptions {
48
+ inputPath?: string
49
+ providedInput?: string
50
+ allowStdinWhenNoPath?: boolean
51
+ }
52
+
53
+ export async function readCliInput(options: ReadCliInputOptions): Promise<CliInputResult> {
54
+ const { inputPath, providedInput, allowStdinWhenNoPath = false } = options
55
+ const canUseProvided = providedInput != null && (inputPath == null || inputPath === '-')
56
+
57
+ if (canUseProvided) {
58
+ return { content: providedInput, isStdin: inputPath === '-' || inputPath == null }
59
+ }
60
+
61
+ if (inputPath === '-') {
62
+ return { content: await readStdin(), isStdin: true }
63
+ }
64
+
65
+ if (inputPath != null) {
66
+ const content = await fsp.readFile(inputPath, 'utf8')
67
+ return { content, isStdin: false, path: inputPath }
68
+ }
69
+
70
+ if (allowStdinWhenNoPath && !process.stdin.isTTY) {
71
+ return { content: await readStdin(), isStdin: true }
72
+ }
73
+
74
+ return { content: null, isStdin: false }
75
+ }
76
+
27
77
  export function formatAPIError(err: unknown): string {
28
78
  if (isAPIError(err)) {
29
79
  return `${err.error}: ${err.message}`
@@ -0,0 +1,89 @@
1
+ import { getIndentation } from './alphalib/stepParsing.ts'
2
+ import { mergeTemplateContent } from './alphalib/templateMerge.ts'
3
+ import type { AssemblyInstructionsInput, StepsInput } from './alphalib/types/template.ts'
4
+ import type { ResponseTemplateContent, TemplateContent } from './apiTypes.ts'
5
+
6
+ const DEFAULT_INDENT = ' '
7
+
8
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
9
+ typeof value === 'object' && value !== null && !Array.isArray(value)
10
+
11
+ export interface BuildLintInputResult {
12
+ lintContent: string
13
+ wasStepsOnly: boolean
14
+ indent: string
15
+ }
16
+
17
+ export const unwrapStepsOnly = (content: string, indent: string): string => {
18
+ try {
19
+ const parsed = JSON.parse(content)
20
+ if (isRecord(parsed) && 'steps' in parsed) {
21
+ return JSON.stringify((parsed as { steps?: unknown }).steps ?? {}, null, indent)
22
+ }
23
+ } catch (_err) {
24
+ return content
25
+ }
26
+ return content
27
+ }
28
+
29
+ export const buildLintInput = (
30
+ assemblyInstructions?: AssemblyInstructionsInput | StepsInput | string,
31
+ template?: TemplateContent | ResponseTemplateContent,
32
+ ): BuildLintInputResult => {
33
+ let inputString: string | undefined
34
+ let parsedInput: unknown | undefined
35
+ let parseFailed = false
36
+ let indent = DEFAULT_INDENT
37
+
38
+ if (typeof assemblyInstructions === 'string') {
39
+ inputString = assemblyInstructions
40
+ indent = getIndentation(assemblyInstructions)
41
+ try {
42
+ parsedInput = JSON.parse(assemblyInstructions)
43
+ } catch (_err) {
44
+ parseFailed = true
45
+ }
46
+ } else if (assemblyInstructions != null) {
47
+ parsedInput = assemblyInstructions
48
+ }
49
+
50
+ let wasStepsOnly = false
51
+ let instructions: AssemblyInstructionsInput | undefined
52
+
53
+ if (parsedInput !== undefined) {
54
+ if (isRecord(parsedInput)) {
55
+ if ('steps' in parsedInput) {
56
+ instructions = parsedInput as AssemblyInstructionsInput
57
+ } else {
58
+ instructions = { steps: parsedInput as StepsInput }
59
+ wasStepsOnly = true
60
+ }
61
+ } else {
62
+ instructions = { steps: parsedInput as StepsInput }
63
+ wasStepsOnly = true
64
+ }
65
+ }
66
+
67
+ const shouldMergeTemplate = template != null && !parseFailed
68
+ if (shouldMergeTemplate) {
69
+ instructions = mergeTemplateContent(template, instructions)
70
+ }
71
+
72
+ let lintContent = ''
73
+ if (instructions != null) {
74
+ if (
75
+ typeof assemblyInstructions === 'string' &&
76
+ !wasStepsOnly &&
77
+ !parseFailed &&
78
+ !shouldMergeTemplate
79
+ ) {
80
+ lintContent = assemblyInstructions
81
+ } else {
82
+ lintContent = JSON.stringify(instructions, null, indent)
83
+ }
84
+ } else if (inputString != null) {
85
+ lintContent = inputString
86
+ }
87
+
88
+ return { lintContent, wasStepsOnly, indent }
89
+ }
@@ -0,0 +1,72 @@
1
+ import type { HydratedLintIssue } from './alphalib/assembly-linter.lang.en.ts'
2
+ import { hydrateLintIssues } from './alphalib/assembly-linter.lang.en.ts'
3
+ import { applyFix, parseAndLint } from './alphalib/assembly-linter.ts'
4
+ import type { AssemblyInstructionsInput, StepsInput } from './alphalib/types/template.ts'
5
+ import type { ResponseTemplateContent, TemplateContent } from './apiTypes.ts'
6
+ import { buildLintInput, unwrapStepsOnly } from './lintAssemblyInput.ts'
7
+
8
+ export type LintFatalLevel = 'error' | 'warning'
9
+
10
+ export interface LintAssemblyInstructionsInput {
11
+ /**
12
+ * Assembly Instructions as a JSON string, a full instructions object, or a steps-only object.
13
+ */
14
+ assemblyInstructions?: AssemblyInstructionsInput | StepsInput | string
15
+ /**
16
+ * Optional template content to merge with the provided instructions.
17
+ */
18
+ template?: TemplateContent | ResponseTemplateContent
19
+ /**
20
+ * Treat issues at this level or above as fatal. Defaults to "error".
21
+ */
22
+ fatal?: LintFatalLevel
23
+ /**
24
+ * Apply auto-fixes where possible and return the fixed JSON.
25
+ */
26
+ fix?: boolean
27
+ }
28
+
29
+ export interface LintAssemblyInstructionsResult {
30
+ success: boolean
31
+ issues: HydratedLintIssue[]
32
+ fixedInstructions?: string
33
+ }
34
+
35
+ export async function lintAssemblyInstructions(
36
+ options: LintAssemblyInstructionsInput,
37
+ ): Promise<LintAssemblyInstructionsResult> {
38
+ const { assemblyInstructions, template, fix = false, fatal = 'error' } = options
39
+
40
+ if (assemblyInstructions == null && template == null) {
41
+ throw new Error('Provide assemblyInstructions or template content to lint.')
42
+ }
43
+
44
+ const { lintContent, wasStepsOnly, indent } = buildLintInput(assemblyInstructions, template)
45
+
46
+ let issues = await parseAndLint(lintContent)
47
+ let fixedContent = lintContent
48
+
49
+ if (fix) {
50
+ for (const issue of issues) {
51
+ if (!issue.fixId) continue
52
+ // applyFix validates fixData against the fix schema for the fixId.
53
+ fixedContent = applyFix(fixedContent, issue.fixId, issue.fixData as never)
54
+ }
55
+ issues = await parseAndLint(fixedContent)
56
+ }
57
+
58
+ const describedIssues = hydrateLintIssues(issues)
59
+ const fatalTypes = fatal === 'warning' ? new Set(['warning', 'error']) : new Set(['error'])
60
+ const success = !describedIssues.some((issue) => fatalTypes.has(issue.type))
61
+
62
+ const result: LintAssemblyInstructionsResult = {
63
+ success,
64
+ issues: describedIssues,
65
+ }
66
+
67
+ if (fix) {
68
+ result.fixedInstructions = wasStepsOnly ? unwrapStepsOnly(fixedContent, indent) : fixedContent
69
+ }
70
+
71
+ return result
72
+ }