@dbx-tools/cli 0.9.58 → 0.9.61

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 (90) hide show
  1. package/README.md +594 -57
  2. package/bin/dbx-genie.ts +13 -0
  3. package/bin/dbx-graphiti.ts +5 -0
  4. package/bin/dbx-lakebase-proxy.ts +4 -0
  5. package/bin/dbx-model-gateway.ts +5 -0
  6. package/index.ts +47 -1
  7. package/lib/bin/dbx-genie.d.ts +2 -0
  8. package/lib/bin/dbx-genie.js +10 -0
  9. package/lib/bin/dbx-graphiti.d.ts +2 -0
  10. package/lib/bin/dbx-graphiti.js +4 -0
  11. package/lib/bin/dbx-lakebase-proxy.d.ts +2 -0
  12. package/lib/bin/dbx-lakebase-proxy.js +4 -0
  13. package/lib/bin/dbx-model-gateway.d.ts +2 -0
  14. package/lib/bin/dbx-model-gateway.js +4 -0
  15. package/lib/index.d.ts +47 -1
  16. package/lib/index.js +34 -2
  17. package/lib/src/appkit/cli.d.ts +24 -0
  18. package/lib/src/appkit/cli.js +54 -0
  19. package/lib/src/appkit/env-export.d.ts +40 -0
  20. package/lib/src/appkit/env-export.js +99 -0
  21. package/lib/src/auth/cli.d.ts +20 -0
  22. package/lib/src/auth/cli.js +129 -0
  23. package/lib/src/auth/options.d.ts +45 -0
  24. package/lib/src/auth/options.js +68 -0
  25. package/lib/src/cli.d.ts +2 -0
  26. package/lib/src/cli.js +19 -9
  27. package/lib/src/genie-code/cli.d.ts +19 -0
  28. package/lib/src/genie-code/cli.js +36 -0
  29. package/lib/src/genie-code/config.d.ts +38 -0
  30. package/lib/src/genie-code/config.js +111 -0
  31. package/lib/src/genie-code/install.d.ts +21 -0
  32. package/lib/src/genie-code/install.js +69 -0
  33. package/lib/src/genie-code/options.d.ts +7 -0
  34. package/lib/src/genie-code/options.js +7 -0
  35. package/lib/src/genie-code/runner.d.ts +21 -0
  36. package/lib/src/genie-code/runner.js +90 -0
  37. package/lib/src/genie-code/runtime.d.ts +48 -0
  38. package/lib/src/genie-code/runtime.js +162 -0
  39. package/lib/src/graphiti/cli.d.ts +18 -0
  40. package/lib/src/graphiti/cli.js +43 -0
  41. package/lib/src/graphiti/options.d.ts +10 -0
  42. package/lib/src/graphiti/options.js +10 -0
  43. package/lib/src/lakebase-proxy/cancellation.d.ts +22 -0
  44. package/lib/src/lakebase-proxy/cancellation.js +100 -0
  45. package/lib/src/lakebase-proxy/cli.d.ts +13 -0
  46. package/lib/src/lakebase-proxy/cli.js +54 -0
  47. package/lib/src/lakebase-proxy/options.d.ts +23 -0
  48. package/lib/src/lakebase-proxy/options.js +41 -0
  49. package/lib/src/lakebase-proxy/protocol.d.ts +37 -0
  50. package/lib/src/lakebase-proxy/protocol.js +124 -0
  51. package/lib/src/lakebase-proxy/proxy.d.ts +22 -0
  52. package/lib/src/lakebase-proxy/proxy.js +200 -0
  53. package/lib/src/model-gateway/cli.d.ts +19 -0
  54. package/lib/src/model-gateway/cli.js +63 -0
  55. package/lib/src/model-gateway/server.d.ts +16 -0
  56. package/lib/src/model-gateway/server.js +83 -0
  57. package/lib/src/tunnel/app.d.ts +20 -0
  58. package/lib/src/tunnel/app.js +52 -0
  59. package/lib/src/tunnel/cli.d.ts +27 -0
  60. package/lib/src/tunnel/cli.js +190 -0
  61. package/lib/src/tunnel/options.d.ts +72 -0
  62. package/lib/src/tunnel/options.js +138 -0
  63. package/lib/src/tunnel/proxy.d.ts +51 -0
  64. package/lib/src/tunnel/proxy.js +143 -0
  65. package/lib/tsconfig.tsbuildinfo +1 -1
  66. package/package.json +146 -9
  67. package/src/appkit/cli.ts +72 -0
  68. package/src/appkit/env-export.ts +124 -0
  69. package/src/auth/cli.ts +175 -0
  70. package/src/auth/options.ts +83 -0
  71. package/src/cli.ts +35 -8
  72. package/src/genie-code/cli.ts +58 -0
  73. package/src/genie-code/config.ts +161 -0
  74. package/src/genie-code/install.ts +103 -0
  75. package/src/genie-code/options.ts +14 -0
  76. package/src/genie-code/runner.ts +106 -0
  77. package/src/genie-code/runtime.ts +212 -0
  78. package/src/graphiti/cli.ts +75 -0
  79. package/src/graphiti/options.ts +20 -0
  80. package/src/lakebase-proxy/cancellation.ts +116 -0
  81. package/src/lakebase-proxy/cli.ts +80 -0
  82. package/src/lakebase-proxy/options.ts +56 -0
  83. package/src/lakebase-proxy/protocol.ts +170 -0
  84. package/src/lakebase-proxy/proxy.ts +256 -0
  85. package/src/model-gateway/cli.ts +93 -0
  86. package/src/model-gateway/server.ts +94 -0
  87. package/src/tunnel/app.ts +58 -0
  88. package/src/tunnel/cli.ts +218 -0
  89. package/src/tunnel/options.ts +169 -0
  90. package/src/tunnel/proxy.ts +194 -0
@@ -0,0 +1,14 @@
1
+ /**
2
+ * CLI projection of the shared managed Genie Code options.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ export {
8
+ GenieCodeOptionsSchema as GenieCodeCliOptionsSchema,
9
+ resolveGenieCodeOptions as resolveGenieCodeCliOptions,
10
+ } from "@dbx-tools/shared-genie-code/options";
11
+ export type {
12
+ GenieCodeOptions as GenieCodeCliOptions,
13
+ ResolvedGenieCodeOptions as ResolvedGenieCodeCliOptions,
14
+ } from "@dbx-tools/shared-genie-code/options";
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Internal readiness gate and interactive Genie Code child process.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { AppKitChildProcess } from "@dbx-tools/appkit/child-process";
8
+ import { asyncUtils, log } from "@dbx-tools/shared-core";
9
+ import { GenieCodeRunnerOptionsSchema } from "@dbx-tools/shared-genie-code/options";
10
+
11
+ /** Private environment field carrying serialized runner options. */
12
+ const RUNNER_OPTIONS_ENV = "DBX_TOOLS_GENIE_RUNNER_OPTIONS";
13
+ const logger = log.logger("genie-code:runner");
14
+
15
+ /** Parse the serialized runner boundary from its private environment field. */
16
+ export function genieCodeRunnerOptions(environment: NodeJS.ProcessEnv = process.env) {
17
+ const serialized = environment[RUNNER_OPTIONS_ENV];
18
+ if (!serialized) throw new Error(`${RUNNER_OPTIONS_ENV} is required`);
19
+ return GenieCodeRunnerOptionsSchema.parse(JSON.parse(serialized));
20
+ }
21
+
22
+ /** Wait until the guarded model-gateway health route accepts the runner token. */
23
+ export async function waitForGenieCodeGateway(
24
+ healthUrl: string,
25
+ bearerToken: string,
26
+ timeoutMs: number,
27
+ ): Promise<void> {
28
+ for await (const ready of asyncUtils.poll(
29
+ async ({ signal }) => {
30
+ try {
31
+ const response = await fetch(healthUrl, {
32
+ headers: { authorization: `Bearer ${bearerToken}` },
33
+ signal,
34
+ });
35
+ return response.ok;
36
+ } catch {
37
+ return false;
38
+ }
39
+ },
40
+ {
41
+ intervalMs: 200,
42
+ timeoutMs,
43
+ predicate: (ready) => !ready,
44
+ },
45
+ )) {
46
+ if (ready) return;
47
+ }
48
+ }
49
+
50
+ /** Run Genie Code with inherited terminal streams after its gateway is ready. */
51
+ export async function runGenieCodeChild(
52
+ environment: NodeJS.ProcessEnv = process.env,
53
+ ): Promise<void> {
54
+ const options = genieCodeRunnerOptions(environment);
55
+ logger.info("waiting for model gateway", {
56
+ gateway: options.gatewayHealthUrl,
57
+ timeoutMs: options.startupTimeoutMs,
58
+ });
59
+ await waitForGenieCodeGateway(
60
+ options.gatewayHealthUrl,
61
+ options.bearerToken,
62
+ options.startupTimeoutMs,
63
+ );
64
+ logger.info("model gateway ready; starting Genie Code", {
65
+ executable: options.executable,
66
+ home: options.home,
67
+ });
68
+ const child = new AppKitChildProcess(
69
+ [
70
+ options.executable,
71
+ [...options.arguments],
72
+ {
73
+ cwd: process.cwd(),
74
+ env: {
75
+ ...environment,
76
+ GENIE_HOME: options.home,
77
+ },
78
+ stdin: "inherit",
79
+ stdout: "inherit",
80
+ stderr: "inherit",
81
+ },
82
+ ],
83
+ {
84
+ gracefulTimeoutMs: 5_000,
85
+ forceTimeoutMs: 1_000,
86
+ },
87
+ );
88
+ const running = child.start();
89
+ const shutdown = () => {
90
+ void child.shutdown();
91
+ };
92
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"] as const) {
93
+ process.once(signal, shutdown);
94
+ }
95
+ try {
96
+ const result = await running;
97
+ if (result.exitCode !== 0) process.exitCode = result.exitCode;
98
+ } finally {
99
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"] as const) {
100
+ process.off(signal, shutdown);
101
+ }
102
+ await child.shutdown();
103
+ }
104
+ }
105
+
106
+ export { RUNNER_OPTIONS_ENV };
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Supervise Genie Code and its authenticated local model gateway.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { randomBytes } from "node:crypto";
8
+ import { rm } from "node:fs/promises";
9
+ import { delimiter, dirname } from "node:path";
10
+ import { DatabricksModelRegistry, models } from "@dbx-tools/appkit-model-gateway";
11
+ import * as auth from "@dbx-tools/auth";
12
+ import { resolveServicePackageBin } from "@dbx-tools/cli-service/definition";
13
+ import type { BinContext } from "@dbx-tools/core/bin";
14
+ import { workspaceClient } from "@dbx-tools/databricks";
15
+ import { log } from "@dbx-tools/shared-core";
16
+ import {
17
+ resolveGenieCodeOptions,
18
+ type GenieCodeOptions,
19
+ type GenieCodeRunnerOptions,
20
+ } from "@dbx-tools/shared-genie-code/options";
21
+ import concurrently from "concurrently";
22
+ import getPort from "get-port";
23
+
24
+ import { writeGenieCodeConfig, type GenieCodeHome } from "./config.ts";
25
+ import { installGenieCode } from "./install.ts";
26
+ import { RUNNER_OPTIONS_ENV } from "./runner.ts";
27
+
28
+ const logger = log.logger("genie-code");
29
+
30
+ /** Injectable owners used to prepare and supervise managed Genie Code. */
31
+ export interface GenieCodeRuntimeDependencies {
32
+ createAuthClient: typeof auth.client.createAuthClient;
33
+ install: typeof installGenieCode;
34
+ port(options: { host: string; port?: number }): Promise<number>;
35
+ resolveBin(packageReference: string, binName: string): string;
36
+ resolveCodexModel(model: string, profile: string): Promise<string>;
37
+ supervise: typeof concurrently;
38
+ token(): string;
39
+ writeConfig: typeof writeGenieCodeConfig;
40
+ }
41
+
42
+ const DEFAULT_DEPENDENCIES: GenieCodeRuntimeDependencies = {
43
+ createAuthClient: auth.client.createAuthClient,
44
+ install: installGenieCode,
45
+ port: ({ host, port }) => getPort({ host, ...(port ? { port } : {}) }),
46
+ resolveBin: resolveServicePackageBin,
47
+ resolveCodexModel,
48
+ supervise: concurrently,
49
+ token: () => randomBytes(32).toString("base64url"),
50
+ writeConfig: writeGenieCodeConfig,
51
+ };
52
+
53
+ /** Resolve a Genie `--model` value to the Codex catalogue slug the gateway publishes. */
54
+ async function resolveCodexModel(model: string, profile: string): Promise<string> {
55
+ const client = await workspaceClient.createWorkspaceClient({ profile });
56
+ const target = await new DatabricksModelRegistry({ client }).resolve(model, {
57
+ requiresTools: true,
58
+ });
59
+ if (!target) throw new Error(`Genie Code model not found: ${model}`);
60
+ return models.codexModelSlug(target);
61
+ }
62
+
63
+ /** Inputs accepted by managed Genie Code preparation and execution. */
64
+ export interface RunGenieCodeOptions {
65
+ cwd?: string;
66
+ dependencies?: Partial<GenieCodeRuntimeDependencies>;
67
+ genieArgs?: readonly string[];
68
+ options?: GenieCodeOptions;
69
+ }
70
+
71
+ /** Resolved installation, auth, listener, and home for one invocation. */
72
+ export interface PreparedGenieCodeRuntime {
73
+ bearerToken: string;
74
+ gatewayBaseUrl: string;
75
+ gatewayHealthUrl: string;
76
+ gatewayListen: string;
77
+ gatewayPort: number;
78
+ home: GenieCodeHome;
79
+ installation: BinContext;
80
+ model: string;
81
+ profile: string;
82
+ }
83
+
84
+ /** Resolve auth, installation, listener, and configuration before spawning children. */
85
+ export async function prepareGenieCodeRuntime(
86
+ input: RunGenieCodeOptions = {},
87
+ ): Promise<PreparedGenieCodeRuntime> {
88
+ const dependencies = { ...DEFAULT_DEPENDENCIES, ...input.dependencies };
89
+ const options = resolveGenieCodeOptions(input.options);
90
+ logger.info("resolving Databricks authentication", {
91
+ requestedProfile: options.profile,
92
+ });
93
+ const authClient = await dependencies.createAuthClient(
94
+ options.profile ? { profile: options.profile } : {},
95
+ );
96
+ const profile = authClient.profile;
97
+ if (!profile) {
98
+ throw new Error("Genie Code requires a configured Databricks profile");
99
+ }
100
+ logger.info("preparing Genie Code runtime", {
101
+ profile,
102
+ model: options.model,
103
+ });
104
+ const installation = await dependencies.install();
105
+ const host = options.gatewayListen.host;
106
+ const requestedPort = options.gatewayListen.port;
107
+ const gatewayPort = await dependencies.port({
108
+ host,
109
+ ...(requestedPort > 0 ? { port: requestedPort } : {}),
110
+ });
111
+ if (requestedPort > 0 && gatewayPort !== requestedPort) {
112
+ throw new Error(`Genie Code gateway port ${requestedPort} is already in use`);
113
+ }
114
+ const model = await dependencies.resolveCodexModel(options.model, profile);
115
+ logger.info("resolved Genie Code model", { requested: options.model, model });
116
+ const urlHost = host.includes(":") ? `[${host}]` : host;
117
+ const gatewayBaseUrl = `http://${urlHost}:${gatewayPort}/v1`;
118
+ const gatewayHealthUrl = `http://${urlHost}:${gatewayPort}/api/healthz`;
119
+ const gatewayListen = `tcp://${urlHost}:${gatewayPort}`;
120
+ const bearerToken = dependencies.token();
121
+ const home = await dependencies.writeConfig({
122
+ bearerToken,
123
+ gatewayBaseUrl,
124
+ model,
125
+ profile,
126
+ projectDirectory: input.cwd ?? process.cwd(),
127
+ });
128
+ return {
129
+ bearerToken,
130
+ gatewayBaseUrl,
131
+ gatewayHealthUrl,
132
+ gatewayListen,
133
+ gatewayPort,
134
+ home,
135
+ installation,
136
+ model,
137
+ profile,
138
+ };
139
+ }
140
+
141
+ function shellExecutable(path: string): string {
142
+ if (process.platform === "win32") return `"${path.replaceAll('"', '""')}"`;
143
+ return `'${path.replaceAll("'", "'\\''")}'`;
144
+ }
145
+
146
+ function childPath(...paths: string[]): string {
147
+ return [...new Set([...paths.map(dirname), process.env.PATH].filter(Boolean))].join(delimiter);
148
+ }
149
+
150
+ /** Run Genie Code and the model gateway until either process exits. */
151
+ export async function runGenieCode(input: RunGenieCodeOptions = {}): Promise<void> {
152
+ const dependencies = { ...DEFAULT_DEPENDENCIES, ...input.dependencies };
153
+ const prepared = await prepareGenieCodeRuntime({
154
+ ...input,
155
+ dependencies,
156
+ });
157
+ const gatewayBin = dependencies.resolveBin(import.meta.url, "dbx-model-gateway");
158
+ const runnerBin = dependencies.resolveBin(import.meta.url, "dbx-genie");
159
+ const path = childPath(gatewayBin, runnerBin);
160
+ const runnerOptions: GenieCodeRunnerOptions = {
161
+ executable: prepared.installation.path,
162
+ arguments: ["-p", prepared.home.overlayName, ...(input.genieArgs ?? [])],
163
+ home: prepared.home.home,
164
+ gatewayHealthUrl: prepared.gatewayHealthUrl,
165
+ bearerToken: prepared.bearerToken,
166
+ startupTimeoutMs: 60_000,
167
+ };
168
+ const { result } = dependencies.supervise(
169
+ [
170
+ {
171
+ command: shellExecutable(gatewayBin),
172
+ name: "gateway",
173
+ env: {
174
+ ...process.env,
175
+ PATH: path,
176
+ LISTEN: prepared.gatewayListen,
177
+ DATABRICKS_CONFIG_PROFILE: prepared.profile,
178
+ DBX_TOOLS_MODEL_GATEWAY_BEARER_TOKEN: prepared.bearerToken,
179
+ },
180
+ },
181
+ {
182
+ command: shellExecutable(runnerBin),
183
+ name: "genie",
184
+ env: {
185
+ ...process.env,
186
+ PATH: path,
187
+ DATABRICKS_CONFIG_PROFILE: prepared.profile,
188
+ [RUNNER_OPTIONS_ENV]: JSON.stringify(runnerOptions),
189
+ },
190
+ },
191
+ ],
192
+ {
193
+ cwd: input.cwd ?? process.cwd(),
194
+ raw: true,
195
+ killOthersOn: ["failure", "success"],
196
+ killSignal: "SIGTERM",
197
+ killTimeout: 17_000,
198
+ successCondition: "command-genie",
199
+ },
200
+ );
201
+ logger.info("started Genie Code and model gateway", {
202
+ profile: prepared.profile,
203
+ model: prepared.model,
204
+ gateway: prepared.gatewayBaseUrl,
205
+ home: prepared.home.home,
206
+ });
207
+ try {
208
+ await result;
209
+ } finally {
210
+ await rm(prepared.home.overlayPath, { force: true });
211
+ }
212
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Commander-owned Graphiti CLI and shared current-user service definition.
3
+ *
4
+ * This module owns every user-facing Graphiti command. The shared Graphiti Zod
5
+ * schema owns its options, environment names, defaults, and validation. The
6
+ * Node Graphiti package owns runtime and Python interaction.
7
+ *
8
+ * @module
9
+ */
10
+ import { addArgs, parseArgs, serializeArgs } from "@dbx-tools/cli-args";
11
+ import { buildServiceCommand, type CliServiceCliDependencies } from "@dbx-tools/cli-service/cli";
12
+ import { defineService, type CliServiceDefinition } from "@dbx-tools/cli-service/definition";
13
+ import { runGraphiti, type GraphitiRuntimeOptions } from "@dbx-tools/graphiti/runtime";
14
+ import { Command } from "commander";
15
+
16
+ import { PACKAGE_VERSION } from "../../index.ts";
17
+ import {
18
+ GraphitiCliOptionsSchema,
19
+ GraphitiOptionsSchema,
20
+ type GraphitiOptions,
21
+ } from "./options.ts";
22
+
23
+ /** Injectable runtime and service lifecycle boundaries for CLI callers. */
24
+ export interface GraphitiCliDependencies {
25
+ /** Run the Graphiti stack through the Node runtime owner. */
26
+ run(options: GraphitiRuntimeOptions): Promise<void>;
27
+ /** Shared service command dependencies for tests and embedding. */
28
+ readonly service?: CliServiceCliDependencies;
29
+ }
30
+
31
+ /** Runtime options persisted when installing the Graphiti desktop service. */
32
+ export type GraphitiServiceOptions = GraphitiOptions;
33
+
34
+ /** Define a Graphiti service without duplicating installation or lifecycle policy. */
35
+ export function graphitiServiceDefinition(
36
+ options: GraphitiServiceOptions = {},
37
+ ): CliServiceDefinition {
38
+ return defineService(import.meta.url, {
39
+ id: "dbx-tools.cli-graphiti",
40
+ name: "dbx graphiti",
41
+ pythonPackage: { name: "dbx-tools-graphiti" },
42
+ command: {
43
+ binName: "dbx-graphiti",
44
+ arguments: serializeArgs(GraphitiOptionsSchema.parse(options)),
45
+ },
46
+ });
47
+ }
48
+
49
+ /** Build foreground execution and shared desktop-service commands. */
50
+ export function buildProgram(
51
+ name = "dbx graphiti",
52
+ dependencies: GraphitiCliDependencies = { run: runGraphiti },
53
+ ): Command {
54
+ const program = addArgs(
55
+ new Command()
56
+ .name(name)
57
+ .description("Run Graphiti or manage its current-user desktop service")
58
+ .version(PACKAGE_VERSION, "-v, --version"),
59
+ GraphitiCliOptionsSchema,
60
+ ).action(async () => {
61
+ await dependencies.run(graphitiOptions(program));
62
+ });
63
+
64
+ program.addCommand(
65
+ buildServiceCommand(
66
+ () => graphitiServiceDefinition(graphitiOptions(program)),
67
+ dependencies.service,
68
+ ),
69
+ );
70
+ return program;
71
+ }
72
+
73
+ function graphitiOptions(command: Command): GraphitiOptions {
74
+ return GraphitiOptionsSchema.parse(parseArgs(command, GraphitiCliOptionsSchema));
75
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Graphiti CLI option surface.
3
+ *
4
+ * The Node Graphiti runtime owns the complete shared option contract. This
5
+ * module re-exports that surface without creating parallel CLI policy.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ export {
11
+ GRAPHITI_DEFAULTS,
12
+ GraphitiCliOptionsSchema,
13
+ GraphitiOptionsSchema,
14
+ ResolvedGraphitiOptionsSchema,
15
+ graphitiOptionOverrides,
16
+ graphitiOptionsEnvironment,
17
+ graphitiOptionsFromEnvironment,
18
+ resolveGraphitiOptions,
19
+ } from "@dbx-tools/graphiti/options";
20
+ export type { GraphitiOptions, ResolvedGraphitiOptions } from "@dbx-tools/graphiti/options";
@@ -0,0 +1,116 @@
1
+ /** PostgreSQL cancellation key translation and TLS forwarding. */
2
+
3
+ import { randomBytes } from "node:crypto";
4
+ import { createConnection, isIP, type Socket } from "node:net";
5
+ import { connect as connectTls, type TLSSocket } from "node:tls";
6
+
7
+ import { cancelRequest, sslRequest, type CancelMessage } from "./protocol.ts";
8
+
9
+ /** Upstream PostgreSQL backend key used to forward a cancellation request. */
10
+ export interface CancellationTarget {
11
+ host: string;
12
+ port: number;
13
+ processId: number;
14
+ secretKey: number;
15
+ }
16
+
17
+ /** Sends one PostgreSQL cancellation request to its upstream backend. */
18
+ export type CancellationForwarder = (target: CancellationTarget) => Promise<void>;
19
+
20
+ let nextProcessId = 10_000;
21
+
22
+ /** Maps synthetic local backend keys onto real upstream cancellation targets. */
23
+ export class CancellationRegistry {
24
+ private readonly targets = new Map<string, CancellationTarget>();
25
+
26
+ constructor(private readonly forwarder: CancellationForwarder = forwardCancellation) {}
27
+
28
+ register(target: CancellationTarget): CancelMessage {
29
+ const processId = nextProcessId++;
30
+ const secretKey = randomBytes(4).readInt32BE(0);
31
+ this.targets.set(key(processId, secretKey), target);
32
+ return { kind: "cancel", processId, secretKey };
33
+ }
34
+
35
+ remove(message: CancelMessage): void {
36
+ this.targets.delete(key(message.processId, message.secretKey));
37
+ }
38
+
39
+ async forward(message: CancelMessage): Promise<boolean> {
40
+ const target = this.targets.get(key(message.processId, message.secretKey));
41
+ if (!target) return false;
42
+ await this.forwarder(target);
43
+ return true;
44
+ }
45
+ }
46
+
47
+ /** Forward a PostgreSQL cancellation request over a verified TLS connection. */
48
+ export async function forwardCancellation(target: CancellationTarget): Promise<void> {
49
+ const socket = createConnection({ host: target.host, port: target.port });
50
+ await event(socket, "connect");
51
+ socket.write(sslRequest());
52
+ const response = await readByte(socket);
53
+ if (response !== "S".charCodeAt(0)) {
54
+ socket.destroy();
55
+ throw new Error("Lakebase upstream refused TLS for cancellation");
56
+ }
57
+ const tls = connectTls({
58
+ socket,
59
+ rejectUnauthorized: true,
60
+ ...(isIP(target.host) === 0 ? { servername: target.host } : {}),
61
+ });
62
+ await event(tls, "secureConnect");
63
+ await writeAndClose(tls, cancelRequest(target.processId, target.secretKey));
64
+ }
65
+
66
+ function key(processId: number, secretKey: number): string {
67
+ return `${processId}:${secretKey}`;
68
+ }
69
+
70
+ function event(socket: Socket | TLSSocket, name: "connect" | "secureConnect"): Promise<void> {
71
+ return new Promise((resolve, reject) => {
72
+ const onEvent = () => finish();
73
+ const onError = (error: Error) => finish(error);
74
+ const finish = (error?: Error) => {
75
+ socket.off(name, onEvent);
76
+ socket.off("error", onError);
77
+ if (error) reject(error);
78
+ else resolve();
79
+ };
80
+ socket.once(name, onEvent);
81
+ socket.once("error", onError);
82
+ });
83
+ }
84
+
85
+ function readByte(socket: Socket): Promise<number> {
86
+ return new Promise((resolve, reject) => {
87
+ const onData = (chunk: Buffer) => {
88
+ finish();
89
+ if (chunk.length > 1) socket.unshift(chunk.subarray(1));
90
+ resolve(chunk[0]!);
91
+ };
92
+ const onError = (error: Error) => {
93
+ finish();
94
+ reject(error);
95
+ };
96
+ const onEnd = () => {
97
+ finish();
98
+ reject(new Error("Lakebase upstream closed during TLS negotiation"));
99
+ };
100
+ const finish = () => {
101
+ socket.off("data", onData);
102
+ socket.off("error", onError);
103
+ socket.off("end", onEnd);
104
+ };
105
+ socket.once("data", onData);
106
+ socket.once("error", onError);
107
+ socket.once("end", onEnd);
108
+ });
109
+ }
110
+
111
+ function writeAndClose(socket: TLSSocket, packet: Buffer): Promise<void> {
112
+ return new Promise((resolve, reject) => {
113
+ socket.end(packet, () => resolve());
114
+ socket.once("error", reject);
115
+ });
116
+ }
@@ -0,0 +1,80 @@
1
+ /** Commander entry point for the pure Node Lakebase proxy. */
2
+
3
+ import { addArgs, parseArgs, serializeArgs } from "@dbx-tools/cli-args";
4
+ import { buildServiceCommand, type CliServiceCliDependencies } from "@dbx-tools/cli-service/cli";
5
+ import { defineService, type CliServiceDefinition } from "@dbx-tools/cli-service/definition";
6
+ import { connectionUrl } from "@dbx-tools/lakebase";
7
+ import { Command } from "commander";
8
+
9
+ import { PACKAGE_VERSION } from "../../index.ts";
10
+ import {
11
+ LakebaseProxyOptionsSchema,
12
+ LakebaseProxyUrlOptionsSchema,
13
+ resolveLakebaseProxyOptions,
14
+ type LakebaseProxyOptions,
15
+ } from "./options.ts";
16
+ import { LakebaseProxy } from "./proxy.ts";
17
+
18
+ /** Injectable service boundary for Lakebase proxy CLI composition and tests. */
19
+ export interface LakebaseProxyCliDependencies {
20
+ readonly service?: CliServiceCliDependencies;
21
+ }
22
+
23
+ /** Build the tray-only Lakebase proxy service definition. */
24
+ export function lakebaseProxyServiceDefinition(
25
+ options: LakebaseProxyOptions = {},
26
+ ): CliServiceDefinition {
27
+ const resolved = resolveLakebaseProxyOptions(options);
28
+ return defineService(import.meta.url, {
29
+ id: "dbx-tools.cli-lakebase-proxy",
30
+ name: "dbx lakebase proxy",
31
+ command: {
32
+ binName: "dbx-lakebase-proxy",
33
+ arguments: serializeArgs(resolved),
34
+ },
35
+ });
36
+ }
37
+
38
+ /** Build the Lakebase proxy command-line program. */
39
+ export function buildProgram(
40
+ name = "dbx lakebase-proxy",
41
+ dependencies: LakebaseProxyCliDependencies = {},
42
+ ): Command {
43
+ const program = addArgs(
44
+ new Command(name)
45
+ .description("Run a loopback PostgreSQL proxy for Databricks Lakebase")
46
+ .version(PACKAGE_VERSION)
47
+ .enablePositionalOptions(),
48
+ LakebaseProxyOptionsSchema,
49
+ ).action(async () => {
50
+ const proxy = new LakebaseProxy(parseArgs(program, LakebaseProxyOptionsSchema));
51
+ await proxy.listen();
52
+ await waitForShutdown();
53
+ await proxy.close();
54
+ });
55
+
56
+ const urlCommand = addArgs(
57
+ program.command("url").description("Format a local PostgreSQL URL for a Lakebase target"),
58
+ LakebaseProxyUrlOptionsSchema,
59
+ ).action(() => {
60
+ const options = parseArgs(urlCommand, LakebaseProxyUrlOptionsSchema);
61
+ const target = options.target ?? options.lakebaseEndpoint!;
62
+ process.stdout.write(`${connectionUrl(target, options.listen.host, options.listen.port)}\n`);
63
+ });
64
+ let installCommand!: Command;
65
+ const serviceCommand = buildServiceCommand(() => {
66
+ return lakebaseProxyServiceDefinition(parseArgs(installCommand, LakebaseProxyOptionsSchema));
67
+ }, dependencies.service);
68
+ installCommand = serviceCommand.commands.find((command) => command.name() === "install")!;
69
+ addArgs(installCommand, LakebaseProxyOptionsSchema);
70
+ program.addCommand(serviceCommand);
71
+ return program;
72
+ }
73
+
74
+ function waitForShutdown(): Promise<void> {
75
+ return new Promise((resolve) => {
76
+ const shutdown = () => resolve();
77
+ process.once("SIGINT", shutdown);
78
+ process.once("SIGTERM", shutdown);
79
+ });
80
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Lakebase proxy listener, service, and URL command options.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { options } from "@dbx-tools/shared-core";
8
+ import { z } from "zod";
9
+
10
+ const text = (description: string) => z.string().trim().min(1).optional().describe(description);
11
+
12
+ export const LakebaseProxyOptionsSchema = z
13
+ .object({
14
+ ...options.PostgresOptionsSchema.shape,
15
+ listen: options
16
+ .listenAddressSchema({ port: 5432, loopback: true })
17
+ .describe("Loopback listener address."),
18
+ startupTimeoutSeconds: z.coerce
19
+ .number<number>()
20
+ .int()
21
+ .nonnegative()
22
+ .default(30)
23
+ .describe("Startup timeout in seconds."),
24
+ profile: options.DatabricksOptionsSchema.shape.profile.describe("Exact Databricks profile."),
25
+ })
26
+ .strict()
27
+ .describe("Lakebase proxy listener and service options.");
28
+
29
+ export const LakebaseProxyUrlOptionsSchema = z
30
+ .object({
31
+ target: text("Lakebase project, resource path, host, or URL."),
32
+ lakebaseEndpoint: options.LakebaseOptionsSchema.shape.lakebaseEndpoint,
33
+ listen: options.listenAddressSchema({ port: 5432 }).describe("Local proxy address."),
34
+ })
35
+ .strict()
36
+ .refine(
37
+ (configured) => configured.target !== undefined || configured.lakebaseEndpoint !== undefined,
38
+ {
39
+ message: "URL requires target or endpoint.",
40
+ path: ["target"],
41
+ },
42
+ )
43
+ .describe("Options for formatting a local Lakebase PostgreSQL URL.");
44
+
45
+ export type LakebaseProxyOptions = z.input<typeof LakebaseProxyOptionsSchema>;
46
+
47
+ export type ResolvedLakebaseProxyOptions = z.output<typeof LakebaseProxyOptionsSchema>;
48
+
49
+ export type LakebaseProxyUrlOptions = z.output<typeof LakebaseProxyUrlOptionsSchema>;
50
+
51
+ /** Validate and default Lakebase proxy listener options. */
52
+ export function resolveLakebaseProxyOptions(
53
+ options: LakebaseProxyOptions = {},
54
+ ): ResolvedLakebaseProxyOptions {
55
+ return LakebaseProxyOptionsSchema.parse(options);
56
+ }