@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.
- package/README.md +58 -0
- package/dist/Transloadit.d.ts +19 -0
- package/dist/Transloadit.d.ts.map +1 -1
- package/dist/Transloadit.js +22 -0
- package/dist/Transloadit.js.map +1 -1
- package/dist/alphalib/assembly-linter.d.ts +123 -0
- package/dist/alphalib/assembly-linter.d.ts.map +1 -0
- package/dist/alphalib/assembly-linter.js +1142 -0
- package/dist/alphalib/assembly-linter.js.map +1 -0
- package/dist/alphalib/assembly-linter.lang.en.d.ts +87 -0
- package/dist/alphalib/assembly-linter.lang.en.d.ts.map +1 -0
- package/dist/alphalib/assembly-linter.lang.en.js +326 -0
- package/dist/alphalib/assembly-linter.lang.en.js.map +1 -0
- package/dist/alphalib/object.d.ts +20 -0
- package/dist/alphalib/object.d.ts.map +1 -0
- package/dist/alphalib/object.js +23 -0
- package/dist/alphalib/object.js.map +1 -0
- package/dist/alphalib/stepParsing.d.ts +93 -0
- package/dist/alphalib/stepParsing.d.ts.map +1 -0
- package/dist/alphalib/stepParsing.js +1154 -0
- package/dist/alphalib/stepParsing.js.map +1 -0
- package/dist/alphalib/templateMerge.d.ts +4 -0
- package/dist/alphalib/templateMerge.d.ts.map +1 -0
- package/dist/alphalib/templateMerge.js +22 -0
- package/dist/alphalib/templateMerge.js.map +1 -0
- package/dist/cli/commands/assemblies.d.ts +20 -1
- package/dist/cli/commands/assemblies.d.ts.map +1 -1
- package/dist/cli/commands/assemblies.js +137 -2
- package/dist/cli/commands/assemblies.js.map +1 -1
- package/dist/cli/commands/auth.d.ts.map +1 -1
- package/dist/cli/commands/auth.js +19 -19
- 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 +2 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/docs/assemblyLintingExamples.d.ts +2 -0
- package/dist/cli/docs/assemblyLintingExamples.d.ts.map +1 -0
- package/dist/cli/docs/assemblyLintingExamples.js +10 -0
- package/dist/cli/docs/assemblyLintingExamples.js.map +1 -0
- package/dist/cli/helpers.d.ts +11 -0
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/helpers.js +29 -0
- package/dist/cli/helpers.js.map +1 -1
- package/dist/lintAssemblyInput.d.ts +10 -0
- package/dist/lintAssemblyInput.d.ts.map +1 -0
- package/dist/lintAssemblyInput.js +73 -0
- package/dist/lintAssemblyInput.js.map +1 -0
- package/dist/lintAssemblyInstructions.d.ts +29 -0
- package/dist/lintAssemblyInstructions.d.ts.map +1 -0
- package/dist/lintAssemblyInstructions.js +33 -0
- package/dist/lintAssemblyInstructions.js.map +1 -0
- package/package.json +5 -2
- package/src/Transloadit.ts +39 -0
- package/src/alphalib/assembly-linter.lang.en.ts +393 -0
- package/src/alphalib/assembly-linter.ts +1475 -0
- package/src/alphalib/object.ts +27 -0
- package/src/alphalib/stepParsing.ts +1465 -0
- package/src/alphalib/templateMerge.ts +32 -0
- package/src/alphalib/typings/json-to-ast.d.ts +34 -0
- package/src/cli/commands/assemblies.ts +161 -2
- package/src/cli/commands/auth.ts +19 -22
- package/src/cli/commands/index.ts +2 -0
- package/src/cli/docs/assemblyLintingExamples.ts +9 -0
- package/src/cli/helpers.ts +50 -0
- package/src/lintAssemblyInput.ts +89 -0
- 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 {
|
|
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
|
+
}
|
package/src/cli/commands/auth.ts
CHANGED
|
@@ -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
|
|
221
|
-
|
|
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
|
|
242
|
-
|
|
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
|
|
291
|
-
const
|
|
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
|
|
344
|
-
const
|
|
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
|
+
]
|
package/src/cli/helpers.ts
CHANGED
|
@@ -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
|
+
}
|