@dbx-tools/shared-core 0.9.40 → 0.9.46
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 +5 -0
- package/index.ts +5 -1
- package/lib/index.d.ts +5 -1
- package/lib/index.js +5 -2
- package/lib/src/bindings.d.ts +10 -0
- package/lib/src/bindings.js +15 -0
- package/lib/src/environment-utils.js +3 -3
- package/lib/src/log.d.ts +2 -0
- package/lib/src/log.js +2 -2
- package/lib/src/options.d.ts +120 -0
- package/lib/src/options.js +310 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -2
- package/src/bindings.ts +17 -0
- package/src/environment-utils.ts +7 -6
- package/src/log.ts +1 -1
- package/src/options.ts +482 -0
package/src/options.ts
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe option primitives shared across dbx-tools packages.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { isLoopbackHost, urlBuilder } from "./net.ts";
|
|
9
|
+
import { isRecord, toBoolean } from "./object.ts";
|
|
10
|
+
import { parseList, toIdentifierWithOptions, toSlug } from "./string-utils.ts";
|
|
11
|
+
|
|
12
|
+
const environmentName = <T extends string>(name: T, description: string) =>
|
|
13
|
+
z.literal(name).default(name).describe(description);
|
|
14
|
+
|
|
15
|
+
export const DatabricksEnvironmentNamesSchema = z
|
|
16
|
+
.object({
|
|
17
|
+
profile: environmentName("DATABRICKS_CONFIG_PROFILE", "Databricks CLI profile environment."),
|
|
18
|
+
host: environmentName("DATABRICKS_HOST", "Databricks host environment."),
|
|
19
|
+
accountId: environmentName("DATABRICKS_ACCOUNT_ID", "Databricks account ID environment."),
|
|
20
|
+
workspaceId: environmentName("DATABRICKS_WORKSPACE_ID", "Databricks workspace ID environment."),
|
|
21
|
+
configFile: environmentName(
|
|
22
|
+
"DATABRICKS_CONFIG_FILE",
|
|
23
|
+
"Databricks CLI config file environment.",
|
|
24
|
+
),
|
|
25
|
+
clientId: environmentName("DATABRICKS_CLIENT_ID", "Databricks OAuth client ID environment."),
|
|
26
|
+
clientSecret: environmentName(
|
|
27
|
+
"DATABRICKS_CLIENT_SECRET",
|
|
28
|
+
"Databricks OAuth client secret environment.",
|
|
29
|
+
),
|
|
30
|
+
accessToken: environmentName("DATABRICKS_TOKEN", "Databricks access token environment."),
|
|
31
|
+
groupId: environmentName("DATABRICKS_GROUP_ID", "Databricks group ID environment."),
|
|
32
|
+
authType: environmentName("DATABRICKS_AUTH_TYPE", "Databricks auth type environment."),
|
|
33
|
+
appName: environmentName("DATABRICKS_APP_NAME", "Databricks App name environment."),
|
|
34
|
+
appPort: environmentName("DATABRICKS_APP_PORT", "Databricks App port environment."),
|
|
35
|
+
lakebaseEndpoint: environmentName("LAKEBASE_ENDPOINT", "Lakebase endpoint environment."),
|
|
36
|
+
})
|
|
37
|
+
.strict()
|
|
38
|
+
.describe("Canonical Databricks CLI, SDK, Apps, and Lakebase environment names.");
|
|
39
|
+
|
|
40
|
+
export type DatabricksEnvironmentNames = z.output<typeof DatabricksEnvironmentNamesSchema>;
|
|
41
|
+
|
|
42
|
+
/** Canonical Databricks environment names derived from the owning schema. */
|
|
43
|
+
export const databricksEnvironmentNames = Object.freeze(DatabricksEnvironmentNamesSchema.parse({}));
|
|
44
|
+
|
|
45
|
+
/** Highest valid TCP port number. */
|
|
46
|
+
export const MAX_TCP_PORT = 65_535;
|
|
47
|
+
|
|
48
|
+
export const tcpPortSchema = z.coerce
|
|
49
|
+
.number<number>()
|
|
50
|
+
.int()
|
|
51
|
+
.min(1)
|
|
52
|
+
.max(MAX_TCP_PORT)
|
|
53
|
+
.describe("TCP port from 1 through 65535.");
|
|
54
|
+
|
|
55
|
+
export const tcpPortOrZeroSchema = z.coerce
|
|
56
|
+
.number<number>()
|
|
57
|
+
.int()
|
|
58
|
+
.min(0)
|
|
59
|
+
.max(MAX_TCP_PORT)
|
|
60
|
+
.describe("TCP port from 0 through 65535, where zero requests automatic allocation.");
|
|
61
|
+
|
|
62
|
+
export const normalizedUrlSchema = z
|
|
63
|
+
.string()
|
|
64
|
+
.trim()
|
|
65
|
+
.min(1)
|
|
66
|
+
.refine((value) => urlBuilder(value) !== undefined, {
|
|
67
|
+
message: "Expected a valid URL or host.",
|
|
68
|
+
})
|
|
69
|
+
.overwrite(normalizeUrl)
|
|
70
|
+
.describe("URL normalized through the shared URL builder.");
|
|
71
|
+
|
|
72
|
+
const databricksText = (description: string) =>
|
|
73
|
+
z.string().trim().min(1).optional().describe(description);
|
|
74
|
+
const databricksOptionMeta = (env: string, flag = true) => ({
|
|
75
|
+
env,
|
|
76
|
+
flag,
|
|
77
|
+
helpDefault: false,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export const DatabricksOptionsSchema = z
|
|
81
|
+
.object({
|
|
82
|
+
profile: databricksText("Databricks CLI profile.").meta(
|
|
83
|
+
databricksOptionMeta(databricksEnvironmentNames.profile),
|
|
84
|
+
),
|
|
85
|
+
host: normalizedUrlSchema
|
|
86
|
+
.optional()
|
|
87
|
+
.describe("Databricks host.")
|
|
88
|
+
.meta(databricksOptionMeta(databricksEnvironmentNames.host, false)),
|
|
89
|
+
accountId: databricksText("Databricks account ID.").meta(
|
|
90
|
+
databricksOptionMeta(databricksEnvironmentNames.accountId, false),
|
|
91
|
+
),
|
|
92
|
+
workspaceId: databricksText("Databricks workspace ID.").meta(
|
|
93
|
+
databricksOptionMeta(databricksEnvironmentNames.workspaceId, false),
|
|
94
|
+
),
|
|
95
|
+
configFile: databricksText("Databricks CLI config file.").meta(
|
|
96
|
+
databricksOptionMeta(databricksEnvironmentNames.configFile, false),
|
|
97
|
+
),
|
|
98
|
+
clientId: databricksText("Databricks OAuth client ID.").meta(
|
|
99
|
+
databricksOptionMeta(databricksEnvironmentNames.clientId, false),
|
|
100
|
+
),
|
|
101
|
+
clientSecret: databricksText("Databricks OAuth client secret.").meta(
|
|
102
|
+
databricksOptionMeta(databricksEnvironmentNames.clientSecret, false),
|
|
103
|
+
),
|
|
104
|
+
accessToken: databricksText("Databricks access token.").meta(
|
|
105
|
+
databricksOptionMeta(databricksEnvironmentNames.accessToken, false),
|
|
106
|
+
),
|
|
107
|
+
groupId: databricksText("Databricks group ID.").meta(
|
|
108
|
+
databricksOptionMeta(databricksEnvironmentNames.groupId, false),
|
|
109
|
+
),
|
|
110
|
+
authType: databricksText("Databricks authentication type.").meta(
|
|
111
|
+
databricksOptionMeta(databricksEnvironmentNames.authType, false),
|
|
112
|
+
),
|
|
113
|
+
appName: databricksText("Databricks App name.").meta(
|
|
114
|
+
databricksOptionMeta(databricksEnvironmentNames.appName, false),
|
|
115
|
+
),
|
|
116
|
+
appPort: tcpPortSchema
|
|
117
|
+
.optional()
|
|
118
|
+
.describe("Databricks App port.")
|
|
119
|
+
.meta(databricksOptionMeta(databricksEnvironmentNames.appPort, false)),
|
|
120
|
+
lakebaseEndpoint: databricksText("Lakebase endpoint.").meta(
|
|
121
|
+
databricksOptionMeta(databricksEnvironmentNames.lakebaseEndpoint, false),
|
|
122
|
+
),
|
|
123
|
+
})
|
|
124
|
+
.strict()
|
|
125
|
+
.describe("Common Databricks CLI, SDK, Apps, and Lakebase options.");
|
|
126
|
+
|
|
127
|
+
export type DatabricksOptions = z.input<typeof DatabricksOptionsSchema>;
|
|
128
|
+
|
|
129
|
+
/** Key format emitted by {@link serializeOpts}. */
|
|
130
|
+
export type OptionSerializationFormat = "flag" | "env";
|
|
131
|
+
|
|
132
|
+
/** Namespace every option field while retaining its owning Zod field schemas. */
|
|
133
|
+
export type NamespacedOptionShape<T extends z.ZodRawShape, Prefix extends string> = {
|
|
134
|
+
[Key in keyof T as Key extends string ? `${Prefix}${Capitalize<Key>}` : never]: T[Key];
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
interface OptionProperty {
|
|
138
|
+
readonly key: string;
|
|
139
|
+
readonly type?: string;
|
|
140
|
+
readonly env?: string | readonly string[];
|
|
141
|
+
readonly flag?: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Serialize complete parsed options as JSON keyed by flags or environment names. */
|
|
145
|
+
export function serializeOpts<T extends z.ZodRawShape>(
|
|
146
|
+
schema: z.ZodObject<T>,
|
|
147
|
+
values: z.input<z.ZodObject<T>>,
|
|
148
|
+
format: OptionSerializationFormat,
|
|
149
|
+
): string {
|
|
150
|
+
return JSON.stringify(serializedOpts(schema, values, format), null, 2);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Serialize complete parsed options as a process environment map. */
|
|
154
|
+
export function serializeOptsEnvironment<T extends z.ZodRawShape>(
|
|
155
|
+
schema: z.ZodObject<T>,
|
|
156
|
+
values: z.input<z.ZodObject<T>>,
|
|
157
|
+
): Readonly<Record<string, string>> {
|
|
158
|
+
return Object.fromEntries(
|
|
159
|
+
Object.entries(serializedOpts(schema, values, "env")).map(([key, value]) => [
|
|
160
|
+
key,
|
|
161
|
+
environmentOptionValue(value),
|
|
162
|
+
]),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Namespace a reusable option schema so composed CLI flags render as `--<namespace>-<field>`. */
|
|
167
|
+
export function namespaceOpts<T extends z.ZodRawShape, Prefix extends string>(
|
|
168
|
+
schema: z.ZodObject<T>,
|
|
169
|
+
prefix: Prefix,
|
|
170
|
+
): z.ZodObject<NamespacedOptionShape<T, Prefix>> {
|
|
171
|
+
const shape = Object.fromEntries(
|
|
172
|
+
Object.entries(schema.shape).map(([key, field]) => [namespacedOptionKey(prefix, key), field]),
|
|
173
|
+
);
|
|
174
|
+
return z.object(shape).strict() as z.ZodObject<NamespacedOptionShape<T, Prefix>>;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Parse namespaced option values back through their owning schema. */
|
|
178
|
+
export function unnamespaceOpts<T extends z.ZodRawShape>(
|
|
179
|
+
schema: z.ZodObject<T>,
|
|
180
|
+
values: object,
|
|
181
|
+
prefix: string,
|
|
182
|
+
): z.output<z.ZodObject<T>> {
|
|
183
|
+
const source = values as Readonly<Record<string, unknown>>;
|
|
184
|
+
return schema.parse(
|
|
185
|
+
Object.fromEntries(
|
|
186
|
+
Object.keys(schema.shape).map((key) => [key, source[namespacedOptionKey(prefix, key)]]),
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function namespacedOptionKey(prefix: string, key: string): string {
|
|
192
|
+
return `${prefix}${key.charAt(0).toUpperCase()}${key.slice(1)}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function serializedOpts<T extends z.ZodRawShape>(
|
|
196
|
+
schema: z.ZodObject<T>,
|
|
197
|
+
values: z.input<z.ZodObject<T>>,
|
|
198
|
+
format: OptionSerializationFormat,
|
|
199
|
+
): Readonly<Record<string, unknown>> {
|
|
200
|
+
const parsed = schema.parse(values) as Readonly<Record<string, unknown>>;
|
|
201
|
+
const entries = optionProperties(schema).flatMap((property) => {
|
|
202
|
+
if (format === "flag" && property.flag === false) return [];
|
|
203
|
+
const key =
|
|
204
|
+
format === "flag"
|
|
205
|
+
? `--${toSlug(property.key)}`
|
|
206
|
+
: (optionEnvironmentNames(property.key, property.env)[0] ?? "");
|
|
207
|
+
if (!key) return [];
|
|
208
|
+
const value = parsed[property.key];
|
|
209
|
+
return value === undefined ? [] : [[key, serializedOptionValue(value)] as const];
|
|
210
|
+
});
|
|
211
|
+
return Object.fromEntries(entries);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const optionalText = (description: string, env: string) =>
|
|
215
|
+
z.string().trim().min(1).optional().describe(description).meta({ env, flag: false });
|
|
216
|
+
|
|
217
|
+
export const LakebaseOptionsSchema = z
|
|
218
|
+
.object({
|
|
219
|
+
lakebaseEndpoint: optionalText(
|
|
220
|
+
"Lakebase project, resource path, host, or URL.",
|
|
221
|
+
databricksEnvironmentNames.lakebaseEndpoint,
|
|
222
|
+
),
|
|
223
|
+
})
|
|
224
|
+
.strict()
|
|
225
|
+
.describe("Common Lakebase options.");
|
|
226
|
+
|
|
227
|
+
export type LakebaseOptions = z.input<typeof LakebaseOptionsSchema>;
|
|
228
|
+
|
|
229
|
+
/** Explicit option values or environment entries consumed by option parsing helpers. */
|
|
230
|
+
export type OptionValueMap = Readonly<Record<string, unknown>> | null;
|
|
231
|
+
|
|
232
|
+
/** Parse only supplied flag and environment values through an owning Zod object schema. */
|
|
233
|
+
export function parseOpts<T extends z.ZodRawShape>(
|
|
234
|
+
schema: z.ZodObject<T>,
|
|
235
|
+
flags: OptionValueMap = null,
|
|
236
|
+
environment: OptionValueMap = null,
|
|
237
|
+
): Partial<z.output<z.ZodObject<T>>> {
|
|
238
|
+
const configured = optionValues(schema, flags, environment);
|
|
239
|
+
const parsed = schema.parse(configured);
|
|
240
|
+
return Object.fromEntries(
|
|
241
|
+
Object.keys(configured).map((key) => [key, parsed[key as keyof typeof parsed]]),
|
|
242
|
+
) as Partial<z.output<z.ZodObject<T>>>;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Supported listener transport schemes. */
|
|
246
|
+
export type ListenScheme = "tcp" | "unix";
|
|
247
|
+
|
|
248
|
+
/** TCP listener returned by {@link listenAddressSchema}. */
|
|
249
|
+
export interface TcpListenAddress {
|
|
250
|
+
readonly scheme: "tcp";
|
|
251
|
+
readonly host: string;
|
|
252
|
+
readonly port: number;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Unix-domain listener returned by {@link listenAddressSchema}. */
|
|
256
|
+
export interface UnixListenAddress {
|
|
257
|
+
readonly scheme: "unix";
|
|
258
|
+
readonly path: string;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Transport-aware listener address. */
|
|
262
|
+
export type ListenAddress = TcpListenAddress | UnixListenAddress;
|
|
263
|
+
|
|
264
|
+
/** Defaults and validation policy for {@link listenAddressSchema}. */
|
|
265
|
+
export interface ListenAddressOptions {
|
|
266
|
+
readonly port: number;
|
|
267
|
+
readonly host?: string;
|
|
268
|
+
readonly loopback?: boolean;
|
|
269
|
+
readonly scheme?: ListenScheme;
|
|
270
|
+
readonly schemes?: readonly ListenScheme[];
|
|
271
|
+
readonly withDefault?: boolean;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Raw listener input accepted before transport-aware normalization. */
|
|
275
|
+
export type ListenAddressInput = ListenAddress | string | number | undefined;
|
|
276
|
+
|
|
277
|
+
export function listenAddressSchema(
|
|
278
|
+
options: ListenAddressOptions & {
|
|
279
|
+
readonly schemes: readonly ["tcp", "unix"];
|
|
280
|
+
readonly withDefault: false;
|
|
281
|
+
},
|
|
282
|
+
): z.ZodType<ListenAddress, unknown>;
|
|
283
|
+
|
|
284
|
+
export function listenAddressSchema(
|
|
285
|
+
options: ListenAddressOptions & { readonly schemes: readonly ["tcp", "unix"] },
|
|
286
|
+
): z.ZodDefault<z.ZodType<ListenAddress, unknown>>;
|
|
287
|
+
|
|
288
|
+
export function listenAddressSchema(
|
|
289
|
+
options: ListenAddressOptions & { readonly withDefault: false },
|
|
290
|
+
): z.ZodType<TcpListenAddress, unknown>;
|
|
291
|
+
|
|
292
|
+
export function listenAddressSchema(
|
|
293
|
+
options: ListenAddressOptions,
|
|
294
|
+
): z.ZodDefault<z.ZodType<TcpListenAddress, unknown>>;
|
|
295
|
+
|
|
296
|
+
/** Build a transport-aware listener schema. Bare addresses use the configured default scheme. */
|
|
297
|
+
export function listenAddressSchema(
|
|
298
|
+
options: ListenAddressOptions,
|
|
299
|
+
): z.ZodType<ListenAddress, unknown> | z.ZodDefault<z.ZodType<ListenAddress, unknown>> {
|
|
300
|
+
const defaults: TcpListenAddress = {
|
|
301
|
+
scheme: "tcp",
|
|
302
|
+
host: options.host ?? "localhost",
|
|
303
|
+
port: options.port,
|
|
304
|
+
};
|
|
305
|
+
const allowed = new Set<ListenScheme>(options.schemes ?? ["tcp"]);
|
|
306
|
+
const schema = z
|
|
307
|
+
.preprocess(
|
|
308
|
+
(value) => parseListenAddress(value, defaults, options.scheme ?? "tcp"),
|
|
309
|
+
z.discriminatedUnion("scheme", [
|
|
310
|
+
z
|
|
311
|
+
.object({
|
|
312
|
+
scheme: z.literal("tcp"),
|
|
313
|
+
host: z.string().trim().toLowerCase().min(1).default(defaults.host),
|
|
314
|
+
port: tcpPortOrZeroSchema.default(defaults.port),
|
|
315
|
+
})
|
|
316
|
+
.strict(),
|
|
317
|
+
z
|
|
318
|
+
.object({
|
|
319
|
+
scheme: z.literal("unix"),
|
|
320
|
+
path: z.string().trim().min(1),
|
|
321
|
+
})
|
|
322
|
+
.strict(),
|
|
323
|
+
]),
|
|
324
|
+
)
|
|
325
|
+
.refine((address) => allowed.has(address.scheme), "Listener scheme is not allowed.")
|
|
326
|
+
.refine(
|
|
327
|
+
(address) => address.scheme !== "tcp" || !options.loopback || isLoopbackHost(address.host),
|
|
328
|
+
"Listener host must be loopback.",
|
|
329
|
+
)
|
|
330
|
+
.describe("TCP or Unix-domain listener address.") as z.ZodType<ListenAddress, unknown>;
|
|
331
|
+
return options.withDefault === false ? schema : schema.default(defaults);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Render a listener address as a `tcp://` or `unix://` URL. */
|
|
335
|
+
export function formatListenAddress(address: ListenAddress): string {
|
|
336
|
+
if (address.scheme === "unix") {
|
|
337
|
+
return `unix://${address.path.startsWith("/") ? "" : "/"}${address.path}`;
|
|
338
|
+
}
|
|
339
|
+
const host = address.host.includes(":") ? `[${address.host}]` : address.host;
|
|
340
|
+
return `tcp://${host}:${address.port}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function parseListenAddress(
|
|
344
|
+
value: unknown,
|
|
345
|
+
defaults: TcpListenAddress,
|
|
346
|
+
defaultScheme: ListenScheme,
|
|
347
|
+
): unknown {
|
|
348
|
+
if (isRecord(value) && value.scheme === undefined) {
|
|
349
|
+
if (typeof value.host === "string" && value.port !== undefined) {
|
|
350
|
+
return { ...value, scheme: "tcp" };
|
|
351
|
+
}
|
|
352
|
+
if (typeof value.path === "string") {
|
|
353
|
+
return { ...value, scheme: "unix" };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (typeof value === "number") {
|
|
357
|
+
return defaultScheme === "tcp"
|
|
358
|
+
? { scheme: "tcp", host: defaults.host, port: value }
|
|
359
|
+
: { scheme: "unix", path: String(value) };
|
|
360
|
+
}
|
|
361
|
+
if (typeof value !== "string") return value;
|
|
362
|
+
const text = value.trim();
|
|
363
|
+
if (text.startsWith("unix://")) {
|
|
364
|
+
const path = text.slice("unix://".length);
|
|
365
|
+
return { scheme: "unix", path: path.startsWith("/") ? path : `/${path}` };
|
|
366
|
+
}
|
|
367
|
+
const tcp = text.startsWith("tcp://") ? text.slice("tcp://".length) : text;
|
|
368
|
+
if (defaultScheme === "unix" && !text.startsWith("tcp://")) {
|
|
369
|
+
return { scheme: "unix", path: text };
|
|
370
|
+
}
|
|
371
|
+
if (/^\d+$/.test(tcp)) return { scheme: "tcp", host: defaults.host, port: tcp };
|
|
372
|
+
if (/^:\d+$/.test(tcp)) {
|
|
373
|
+
return { scheme: "tcp", host: defaults.host, port: tcp.slice(1) };
|
|
374
|
+
}
|
|
375
|
+
const bracketed = tcp.match(/^\[([^\]]+)\](?::(\d+))?$/);
|
|
376
|
+
if (bracketed) {
|
|
377
|
+
return {
|
|
378
|
+
scheme: "tcp",
|
|
379
|
+
host: bracketed[1],
|
|
380
|
+
port: bracketed[2] ?? defaults.port,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const separator = tcp.lastIndexOf(":");
|
|
384
|
+
if (separator > 0 && !tcp.slice(0, separator).includes(":")) {
|
|
385
|
+
return {
|
|
386
|
+
scheme: "tcp",
|
|
387
|
+
host: tcp.slice(0, separator),
|
|
388
|
+
port: tcp.slice(separator + 1),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
return { scheme: "tcp", host: tcp || defaults.host, port: defaults.port };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function optionProperties(schema: z.ZodObject<z.ZodRawShape>): OptionProperty[] {
|
|
395
|
+
const document = z.toJSONSchema(schema);
|
|
396
|
+
if (!isRecord(document.properties)) return [];
|
|
397
|
+
return Object.entries(document.properties).flatMap(([key, value]) => {
|
|
398
|
+
if (!isRecord(value)) return [];
|
|
399
|
+
return [
|
|
400
|
+
{
|
|
401
|
+
key,
|
|
402
|
+
type: typeof value.type === "string" ? value.type : undefined,
|
|
403
|
+
env:
|
|
404
|
+
typeof value.env === "string" || Array.isArray(value.env)
|
|
405
|
+
? (value.env as string | readonly string[])
|
|
406
|
+
: undefined,
|
|
407
|
+
flag: typeof value.flag === "boolean" ? value.flag : undefined,
|
|
408
|
+
},
|
|
409
|
+
];
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function serializedOptionValue(value: unknown): unknown {
|
|
414
|
+
if (isRecord(value) && value.scheme === "tcp") {
|
|
415
|
+
if (typeof value.host === "string" && typeof value.port === "number") {
|
|
416
|
+
return formatListenAddress({ scheme: "tcp", host: value.host, port: value.port });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (isRecord(value) && value.scheme === "unix" && typeof value.path === "string") {
|
|
420
|
+
return formatListenAddress({ scheme: "unix", path: value.path });
|
|
421
|
+
}
|
|
422
|
+
return value;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function environmentOptionValue(value: unknown): string {
|
|
426
|
+
if (Array.isArray(value)) return value.map(String).join(",");
|
|
427
|
+
if (typeof value === "object" && value !== null) return JSON.stringify(value);
|
|
428
|
+
return String(value);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function optionValues<T extends z.ZodRawShape>(
|
|
432
|
+
schema: z.ZodObject<T>,
|
|
433
|
+
values: OptionValueMap,
|
|
434
|
+
environment: OptionValueMap,
|
|
435
|
+
): Record<string, unknown> {
|
|
436
|
+
const document = z.toJSONSchema(schema);
|
|
437
|
+
const properties = isRecord(document.properties) ? document.properties : {};
|
|
438
|
+
const configured: Record<string, unknown> = {};
|
|
439
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
440
|
+
if (!isRecord(property)) continue;
|
|
441
|
+
const explicit = firstOptionValue(values, [key, `--${toSlug(key)}`]);
|
|
442
|
+
if (explicit !== undefined) {
|
|
443
|
+
configured[key] = optionEnvironmentValue(property.type, explicit);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const names = optionEnvironmentNames(key, property.env);
|
|
447
|
+
const sourced = firstOptionValue(environment, names);
|
|
448
|
+
if (sourced === undefined) continue;
|
|
449
|
+
configured[key] = optionEnvironmentValue(property.type, sourced);
|
|
450
|
+
}
|
|
451
|
+
return configured;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function optionEnvironmentNames(key: string, configured: unknown): string[] {
|
|
455
|
+
if (typeof configured === "string") return [configured];
|
|
456
|
+
if (Array.isArray(configured)) {
|
|
457
|
+
return configured.filter((value): value is string => typeof value === "string");
|
|
458
|
+
}
|
|
459
|
+
return [toIdentifierWithOptions({ delimiter: "_" }, key).toUpperCase()];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function optionEnvironmentValue(type: unknown, value: unknown): unknown {
|
|
463
|
+
if (type === "boolean") return toBoolean(value) ?? value;
|
|
464
|
+
if (type === "array" && typeof value === "string") return parseList(value);
|
|
465
|
+
return value;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function firstOptionValue(source: OptionValueMap, keys: readonly string[]): unknown | undefined {
|
|
469
|
+
if (!source) return undefined;
|
|
470
|
+
for (const key of keys) {
|
|
471
|
+
const value = source[key];
|
|
472
|
+
if (value !== undefined && value !== null && value !== "") return value;
|
|
473
|
+
}
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function normalizeUrl(value: string): string {
|
|
478
|
+
const url = urlBuilder(value);
|
|
479
|
+
if (!url) return value;
|
|
480
|
+
const normalized = url.toString();
|
|
481
|
+
return url.pathname === "/" && !url.search && !url.hash ? normalized.slice(0, -1) : normalized;
|
|
482
|
+
}
|