@dbx-tools/shared-core 0.9.43 → 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/src/options.ts CHANGED
@@ -71,60 +71,55 @@ export const normalizedUrlSchema = z
71
71
 
72
72
  const databricksText = (description: string) =>
73
73
  z.string().trim().min(1).optional().describe(description);
74
+ const databricksOptionMeta = (env: string, flag = true) => ({
75
+ env,
76
+ flag,
77
+ helpDefault: false,
78
+ });
74
79
 
75
80
  export const DatabricksOptionsSchema = z
76
81
  .object({
77
- profile: databricksText("Databricks CLI profile.").meta({
78
- env: databricksEnvironmentNames.profile,
79
- }),
82
+ profile: databricksText("Databricks CLI profile.").meta(
83
+ databricksOptionMeta(databricksEnvironmentNames.profile),
84
+ ),
80
85
  host: normalizedUrlSchema
81
86
  .optional()
82
87
  .describe("Databricks host.")
83
- .meta({ env: databricksEnvironmentNames.host, flag: false }),
84
- accountId: databricksText("Databricks account ID.").meta({
85
- env: databricksEnvironmentNames.accountId,
86
- flag: false,
87
- }),
88
- workspaceId: databricksText("Databricks workspace ID.").meta({
89
- env: databricksEnvironmentNames.workspaceId,
90
- flag: false,
91
- }),
92
- configFile: databricksText("Databricks CLI config file.").meta({
93
- env: databricksEnvironmentNames.configFile,
94
- flag: false,
95
- }),
96
- clientId: databricksText("Databricks OAuth client ID.").meta({
97
- env: databricksEnvironmentNames.clientId,
98
- flag: false,
99
- }),
100
- clientSecret: databricksText("Databricks OAuth client secret.").meta({
101
- env: databricksEnvironmentNames.clientSecret,
102
- flag: false,
103
- }),
104
- accessToken: databricksText("Databricks access token.").meta({
105
- env: databricksEnvironmentNames.accessToken,
106
- flag: false,
107
- }),
108
- groupId: databricksText("Databricks group ID.").meta({
109
- env: databricksEnvironmentNames.groupId,
110
- flag: false,
111
- }),
112
- authType: databricksText("Databricks authentication type.").meta({
113
- env: databricksEnvironmentNames.authType,
114
- flag: false,
115
- }),
116
- appName: databricksText("Databricks App name.").meta({
117
- env: databricksEnvironmentNames.appName,
118
- flag: false,
119
- }),
120
- appPort: tcpPortSchema.optional().describe("Databricks App port.").meta({
121
- env: databricksEnvironmentNames.appPort,
122
- flag: false,
123
- }),
124
- lakebaseEndpoint: databricksText("Lakebase endpoint.").meta({
125
- env: databricksEnvironmentNames.lakebaseEndpoint,
126
- flag: false,
127
- }),
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
+ ),
128
123
  })
129
124
  .strict()
130
125
  .describe("Common Databricks CLI, SDK, Apps, and Lakebase options.");
@@ -134,6 +129,11 @@ export type DatabricksOptions = z.input<typeof DatabricksOptionsSchema>;
134
129
  /** Key format emitted by {@link serializeOpts}. */
135
130
  export type OptionSerializationFormat = "flag" | "env";
136
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
137
  interface OptionProperty {
138
138
  readonly key: string;
139
139
  readonly type?: string;
@@ -147,6 +147,56 @@ export function serializeOpts<T extends z.ZodRawShape>(
147
147
  values: z.input<z.ZodObject<T>>,
148
148
  format: OptionSerializationFormat,
149
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>> {
150
200
  const parsed = schema.parse(values) as Readonly<Record<string, unknown>>;
151
201
  const entries = optionProperties(schema).flatMap((property) => {
152
202
  if (format === "flag" && property.flag === false) return [];
@@ -158,7 +208,7 @@ export function serializeOpts<T extends z.ZodRawShape>(
158
208
  const value = parsed[property.key];
159
209
  return value === undefined ? [] : [[key, serializedOptionValue(value)] as const];
160
210
  });
161
- return JSON.stringify(Object.fromEntries(entries), null, 2);
211
+ return Object.fromEntries(entries);
162
212
  }
163
213
 
164
214
  const optionalText = (description: string, env: string) =>
@@ -192,67 +242,153 @@ export function parseOpts<T extends z.ZodRawShape>(
192
242
  ) as Partial<z.output<z.ZodObject<T>>>;
193
243
  }
194
244
 
195
- /** Host and TCP port returned by {@link listenAddressSchema}. */
196
- export interface ListenAddress {
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";
197
251
  readonly host: string;
198
252
  readonly port: number;
199
253
  }
200
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
+
201
264
  /** Defaults and validation policy for {@link listenAddressSchema}. */
202
265
  export interface ListenAddressOptions {
203
266
  readonly port: number;
204
267
  readonly host?: string;
205
268
  readonly loopback?: boolean;
269
+ readonly scheme?: ListenScheme;
270
+ readonly schemes?: readonly ListenScheme[];
271
+ readonly withDefault?: boolean;
206
272
  }
207
273
 
208
- /** Build a listener address schema accepting a port, `:port`, `host:port`, or object. */
209
- export function listenAddressSchema(options: ListenAddressOptions) {
210
- const defaults: ListenAddress = {
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",
211
302
  host: options.host ?? "localhost",
212
303
  port: options.port,
213
304
  };
214
- return z
305
+ const allowed = new Set<ListenScheme>(options.schemes ?? ["tcp"]);
306
+ const schema = z
215
307
  .preprocess(
216
- (value) => parseListenAddress(value, defaults),
217
- z
218
- .object({
219
- host: z.string().trim().toLowerCase().min(1).default(defaults.host),
220
- port: tcpPortOrZeroSchema.default(defaults.port),
221
- })
222
- .strict()
223
- .refine(
224
- (address) => !options.loopback || isLoopbackHost(address.host),
225
- "Listener host must be loopback.",
226
- ),
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
+ ]),
227
324
  )
228
- .default(defaults)
229
- .describe("Listener host and port.");
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);
230
332
  }
231
333
 
232
- /** Render a listener address as `host:port`, with IPv6 hosts bracketed. */
334
+ /** Render a listener address as a `tcp://` or `unix://` URL. */
233
335
  export function formatListenAddress(address: ListenAddress): string {
336
+ if (address.scheme === "unix") {
337
+ return `unix://${address.path.startsWith("/") ? "" : "/"}${address.path}`;
338
+ }
234
339
  const host = address.host.includes(":") ? `[${address.host}]` : address.host;
235
- return `${host}:${address.port}`;
340
+ return `tcp://${host}:${address.port}`;
236
341
  }
237
342
 
238
- function parseListenAddress(value: unknown, defaults: ListenAddress): unknown {
239
- if (typeof value === "number") return { host: defaults.host, port: value };
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
+ }
240
361
  if (typeof value !== "string") return value;
241
362
  const text = value.trim();
242
- if (/^\d+$/.test(text)) return { host: defaults.host, port: text };
243
- if (/^:\d+$/.test(text)) return { host: defaults.host, port: text.slice(1) };
244
- const bracketed = text.match(/^\[([^\]]+)\](?::(\d+))?$/);
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+))?$/);
245
376
  if (bracketed) {
246
- return { host: bracketed[1], port: bracketed[2] ?? defaults.port };
377
+ return {
378
+ scheme: "tcp",
379
+ host: bracketed[1],
380
+ port: bracketed[2] ?? defaults.port,
381
+ };
247
382
  }
248
- const separator = text.lastIndexOf(":");
249
- if (separator > 0 && !text.slice(0, separator).includes(":")) {
383
+ const separator = tcp.lastIndexOf(":");
384
+ if (separator > 0 && !tcp.slice(0, separator).includes(":")) {
250
385
  return {
251
- host: text.slice(0, separator),
252
- port: text.slice(separator + 1),
386
+ scheme: "tcp",
387
+ host: tcp.slice(0, separator),
388
+ port: tcp.slice(separator + 1),
253
389
  };
254
390
  }
255
- return { host: text || defaults.host, port: defaults.port };
391
+ return { scheme: "tcp", host: tcp || defaults.host, port: defaults.port };
256
392
  }
257
393
 
258
394
  function optionProperties(schema: z.ZodObject<z.ZodRawShape>): OptionProperty[] {
@@ -275,12 +411,23 @@ function optionProperties(schema: z.ZodObject<z.ZodRawShape>): OptionProperty[]
275
411
  }
276
412
 
277
413
  function serializedOptionValue(value: unknown): unknown {
278
- if (isRecord(value) && typeof value.host === "string" && typeof value.port === "number") {
279
- return formatListenAddress({ host: value.host, port: value.port });
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 });
280
421
  }
281
422
  return value;
282
423
  }
283
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
+
284
431
  function optionValues<T extends z.ZodRawShape>(
285
432
  schema: z.ZodObject<T>,
286
433
  values: OptionValueMap,