-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcliCommandWrapper.ts
More file actions
156 lines (138 loc) · 4.87 KB
/
Copy pathcliCommandWrapper.ts
File metadata and controls
156 lines (138 loc) · 4.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import { randomUUIDv7 } from 'node:crypto'
import { type ParseArgsOptionsConfig, parseArgs } from 'node:util'
import type { RequestContext } from '@lokalise/fastify-extras'
import {
globalLogger,
isError,
resolveGlobalErrorLogObject,
stringValueSerializer,
} from '@lokalise/node-core'
import { ENABLE_ALL } from 'opinionated-machine'
import pino from 'pino'
import z from 'zod/v4'
import { getApp } from '../../src/app.ts'
import type { Dependencies } from '../../src/infrastructure/CommonModule.ts'
const unwrapSchemaIfNeeded = (schema: z.Schema) =>
schema instanceof z.ZodOptional || schema instanceof z.ZodNullable ? schema.unwrap() : schema
const resolveInputObjectSchema = (schema: z.Schema): z.ZodObject | undefined => {
if (schema instanceof z.ZodObject) return schema
// schemas with transforms/pipes define their CLI flags on the input side of the pipe
if (schema instanceof z.ZodPipe) return resolveInputObjectSchema(schema.in as z.Schema)
return undefined
}
const deriveParseArgsOptions = (schema: z.Schema): ParseArgsOptionsConfig | undefined => {
const objectSchema = resolveInputObjectSchema(schema)
if (!objectSchema) return undefined
const options: ParseArgsOptionsConfig = {}
for (const [key, fieldSchema] of Object.entries(objectSchema.shape as Record<string, z.Schema>)) {
const unwrappedFieldSchema = unwrapSchemaIfNeeded(fieldSchema)
const isMultiple = unwrappedFieldSchema instanceof z.ZodArray
const elementSchema = isMultiple
? unwrapSchemaIfNeeded(unwrappedFieldSchema.element as z.ZodSchema)
: unwrappedFieldSchema
options[key] = {
type: elementSchema instanceof z.ZodBoolean ? 'boolean' : 'string',
multiple: isMultiple,
}
}
return options
}
const getArgs = (argsSchema: z.Schema) => {
const { values } = parseArgs({
args: process.argv,
strict: false,
options: deriveParseArgsOptions(argsSchema),
})
return values
}
export type CliCommandLifecycle = {
signal: AbortSignal
}
export type CliCommand<
ArgsSchema extends z.Schema | undefined,
Args = ArgsSchema extends z.Schema ? z.infer<ArgsSchema> : undefined,
> = (
dependencies: Dependencies,
requestContext: RequestContext,
args: Args,
lifecycle: CliCommandLifecycle,
) => Promise<void> | void
export const cliCommandWrapper = async <ArgsSchema extends z.Schema | undefined>(
cliCommandName: string,
command: CliCommand<ArgsSchema>,
argsSchema?: ArgsSchema,
): Promise<void> => {
let app: Awaited<ReturnType<typeof getApp>>
try {
app = await getApp({
healthchecksEnabled: false,
monitoringEnabled: false,
periodicJobsEnabled: false,
messageQueueConsumersEnabled: false,
enqueuedJobWorkersEnabled: false,
jobQueuesEnabled: ENABLE_ALL,
})
} catch (err) {
globalLogger.error(resolveGlobalErrorLogObject(err), `Failed to start ${cliCommandName}`)
return process.exit(1)
}
const requestId = randomUUIDv7()
const reqContext: RequestContext = {
reqId: requestId,
logger: app.diContainer.cradle.logger.child({
origin: cliCommandName,
'x-request-id': requestId,
}),
}
// The app-scoped abort signal is aborted by app.ts's gracefulShutdown handler
// (only registered in non-dev). Commands can observe `lifecycle.signal.aborted`
// and/or pass `lifecycle.signal` to any AbortSignal-aware API.
//
// The handler below must still await `commandDone` — abort is a notification,
// not synchronization, so without this the plugin would call `app.close()`
// while the command is still unwinding.
const lifecycle: CliCommandLifecycle = {
signal: app.diContainer.cradle.appAbortController.signal,
}
let resolveCommandDone!: () => void
const commandDone = new Promise<void>((resolve) => {
resolveCommandDone = resolve
})
if (typeof app.gracefulShutdown === 'function') {
app.gracefulShutdown(async (signal) => {
reqContext.logger.warn({ signal }, 'Shutdown signal received, stopping after current step')
await commandDone
})
}
let args = undefined as ArgsSchema extends z.Schema ? z.infer<ArgsSchema> : undefined
if (argsSchema) {
const parseResult = argsSchema.safeParse(getArgs(argsSchema))
if (!parseResult.success) {
reqContext.logger.error(
{
errors: JSON.stringify(parseResult.error.issues),
},
'Invalid arguments',
)
await app.close()
process.exit(1)
}
args = parseResult.data as ArgsSchema extends z.Schema ? z.infer<ArgsSchema> : undefined
}
let isSuccess = true
try {
await command(app.diContainer.cradle, reqContext, args, lifecycle)
} catch (err) {
isSuccess = false
reqContext.logger.error(
{
error: isError(err) ? pino.stdSerializers.err(err) : stringValueSerializer(err),
},
'Error running command',
)
} finally {
resolveCommandDone()
}
await app.close()
process.exit(isSuccess ? 0 : 1)
}