@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,175 @@
1
+ /**
2
+ * `dbx auth` Commander program for Databricks OAuth.
3
+ *
4
+ * The command delegates profile resolution and token lifecycle to
5
+ * `@dbx-tools/auth`.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ import * as databricks from "@dbx-tools/auth";
11
+ import { addArgs, parseArgs } from "@dbx-tools/cli-args";
12
+ import type { DatabricksAuthClientInfo } from "@dbx-tools/shared-auth/client";
13
+ import { Command, CommanderError } from "commander";
14
+
15
+ import {
16
+ AuthCliOptionsSchema,
17
+ databricksAuthOptions,
18
+ TokenCommandOptionsSchema,
19
+ type AuthCliOptions,
20
+ } from "./options.ts";
21
+
22
+ interface AuthContext {
23
+ auth: databricks.AuthClient;
24
+ close(): Promise<void>;
25
+ }
26
+
27
+ interface AuthCliDependencies {
28
+ createAuthClient: typeof databricks.client.createAuthClient;
29
+ writeJson(value: unknown): void;
30
+ writeText(value: string): void;
31
+ }
32
+
33
+ const DEFAULT_DEPENDENCIES: AuthCliDependencies = {
34
+ createAuthClient: databricks.client.createAuthClient,
35
+ writeJson: (value) => {
36
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
37
+ },
38
+ writeText: (value) => {
39
+ process.stdout.write(`${value}\n`);
40
+ },
41
+ };
42
+
43
+ /** Open the selected authentication client. */
44
+ async function openAuth(
45
+ options: AuthCliOptions,
46
+ dependencies: AuthCliDependencies,
47
+ ): Promise<AuthContext> {
48
+ return {
49
+ auth: await dependencies.createAuthClient(databricksAuthOptions(options)),
50
+ close: async () => {},
51
+ };
52
+ }
53
+
54
+ /** Execute an auth action and close resources owned by the command. */
55
+ async function withAuth(
56
+ options: AuthCliOptions,
57
+ dependencies: AuthCliDependencies,
58
+ action: (context: AuthContext) => Promise<void>,
59
+ ): Promise<void> {
60
+ const context = await openAuth(options, dependencies);
61
+ try {
62
+ await action(context);
63
+ } finally {
64
+ await context.close();
65
+ }
66
+ }
67
+
68
+ /** Shape a generated token record for stable CLI JSON output. */
69
+ function tokenJson(token: databricks.AccessToken): Record<string, unknown> {
70
+ return {
71
+ access_token: token.accessToken,
72
+ token_type: token.tokenType,
73
+ ...(token.expiry ? { expiry: token.expiry } : {}),
74
+ ...(token.scopes.length ? { scopes: token.scopes } : {}),
75
+ };
76
+ }
77
+
78
+ /** Select the serializable identity fields from an authentication client. */
79
+ function clientInfo(auth: databricks.AuthClient): DatabricksAuthClientInfo {
80
+ return {
81
+ ...(auth.profile ? { profile: auth.profile } : {}),
82
+ host: auth.host,
83
+ ...(auth.accountId ? { accountId: auth.accountId } : {}),
84
+ ...(auth.workspaceId ? { workspaceId: auth.workspaceId } : {}),
85
+ target: auth.target,
86
+ authType: auth.authType,
87
+ principal: auth.principal,
88
+ };
89
+ }
90
+
91
+ /** Build the `dbx auth` Commander program without parsing arguments. */
92
+ export function buildProgram(
93
+ name = "dbx auth",
94
+ overrides: Partial<AuthCliDependencies> = {},
95
+ ): Command {
96
+ const dependencies = { ...DEFAULT_DEPENDENCIES, ...overrides };
97
+ const program = addArgs(
98
+ new Command()
99
+ .name(name)
100
+ .description("Authenticate to Databricks with user or machine OAuth")
101
+ .showHelpAfterError(),
102
+ AuthCliOptionsSchema,
103
+ );
104
+ const options = (): AuthCliOptions => parseArgs(program, AuthCliOptionsSchema);
105
+
106
+ program
107
+ .command("login")
108
+ .description("Force browser login and return an access token")
109
+ .action(async () => {
110
+ await withAuth(options(), dependencies, async ({ auth }) => {
111
+ dependencies.writeJson(tokenJson(await auth.token({ login: true })));
112
+ });
113
+ });
114
+
115
+ const tokenCommand = addArgs(
116
+ program.command("token").description("Return a valid access token, logging in when needed"),
117
+ TokenCommandOptionsSchema,
118
+ );
119
+ tokenCommand.action(async () => {
120
+ const tokenOptions = parseArgs(tokenCommand, TokenCommandOptionsSchema);
121
+ await withAuth(options(), dependencies, async ({ auth }) => {
122
+ const token = await auth.token({
123
+ ...(tokenOptions.login ? {} : { login: false }),
124
+ ...(tokenOptions.forceRefresh ? { refresh: true } : {}),
125
+ });
126
+ if (tokenOptions.format === "text") dependencies.writeText(token.accessToken);
127
+ else dependencies.writeJson(tokenJson(token));
128
+ });
129
+ });
130
+
131
+ program
132
+ .command("headers")
133
+ .description("Return current Databricks authentication headers")
134
+ .action(async () => {
135
+ await withAuth(options(), dependencies, async ({ auth }) => {
136
+ dependencies.writeJson(await auth.headers());
137
+ });
138
+ });
139
+
140
+ program
141
+ .command("profile")
142
+ .description("Print the configured or automatically detected profile")
143
+ .action(async () => {
144
+ await withAuth(options(), dependencies, async ({ auth }) => {
145
+ dependencies.writeText(auth.profile ?? "ambient");
146
+ });
147
+ });
148
+
149
+ program
150
+ .command("logout")
151
+ .description("Delete the stored credential for the selected profile")
152
+ .action(async () => {
153
+ await withAuth(options(), dependencies, async ({ auth }) => {
154
+ await auth.logout();
155
+ });
156
+ });
157
+
158
+ program
159
+ .command("status")
160
+ .description("Show the resolved authentication client configuration")
161
+ .action(async () => {
162
+ await withAuth(options(), dependencies, async ({ auth }) => {
163
+ dependencies.writeJson(clientInfo(auth));
164
+ });
165
+ });
166
+
167
+ return program;
168
+ }
169
+
170
+ /** Parse `argv` and run the selected auth operation. */
171
+ export async function runCli(argv: string[]): Promise<void> {
172
+ await buildProgram().parseAsync(argv);
173
+ }
174
+
175
+ export { CommanderError };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Databricks auth command-line option schemas and runtime projection.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import {
8
+ AuthOptionsSchema,
9
+ DatabricksAuthOptionsSchema,
10
+ type DatabricksAuthOptions,
11
+ } from "@dbx-tools/auth/config";
12
+ import { stringUtils } from "@dbx-tools/shared-core";
13
+ import { z } from "zod";
14
+
15
+ export const AuthCliOptionsSchema = z
16
+ .object({
17
+ profile: DatabricksAuthOptionsSchema.shape.profile,
18
+ host: DatabricksAuthOptionsSchema.shape.host,
19
+ accountId: DatabricksAuthOptionsSchema.shape.accountId,
20
+ workspaceId: DatabricksAuthOptionsSchema.shape.workspaceId,
21
+ configFile: DatabricksAuthOptionsSchema.shape.configFile,
22
+ clientId: DatabricksAuthOptionsSchema.shape.clientId,
23
+ groupId: DatabricksAuthOptionsSchema.shape.groupId,
24
+ authType: DatabricksAuthOptionsSchema.shape.authType,
25
+ scopes: z
26
+ .preprocess(
27
+ (value) =>
28
+ Array.isArray(value)
29
+ ? value.flatMap((entry) => stringUtils.parseList(String(entry)))
30
+ : value,
31
+ z.array(z.string().trim().min(1)),
32
+ )
33
+ .default([])
34
+ .describe("OAuth scopes."),
35
+ target: DatabricksAuthOptionsSchema.shape.target,
36
+ lockTimeoutMs: AuthOptionsSchema.shape.lockTimeoutMs,
37
+ loginTimeoutMs: AuthOptionsSchema.shape.loginTimeoutMs,
38
+ refreshBufferMs: AuthOptionsSchema.shape.refreshBufferMs,
39
+ preferUserToMachine: z
40
+ .boolean()
41
+ .default(true)
42
+ .describe("Prefer a matching user profile over selected machine credentials."),
43
+ })
44
+ .strict()
45
+ .describe("Options shared by every Databricks auth command.");
46
+
47
+ export const TokenCommandOptionsSchema = z
48
+ .object({
49
+ forceRefresh: z.boolean().default(false).describe("Refresh the token before returning it."),
50
+ login: z.boolean().default(true).describe("Log in when credentials are missing or invalid."),
51
+ format: z
52
+ .enum(["json", "text"])
53
+ .default("json")
54
+ .describe("Output structured token metadata or only the access token."),
55
+ })
56
+ .strict()
57
+ .describe("Options for returning a Databricks access token.");
58
+
59
+ export type AuthCliOptions = z.output<typeof AuthCliOptionsSchema>;
60
+
61
+ export type TokenCommandOptions = z.output<typeof TokenCommandOptionsSchema>;
62
+
63
+ /** Convert flat CLI fields into the Node auth package's nested options. */
64
+ export function databricksAuthOptions(options: AuthCliOptions): DatabricksAuthOptions {
65
+ return {
66
+ profile: options.profile,
67
+ host: options.host,
68
+ accountId: options.accountId,
69
+ workspaceId: options.workspaceId,
70
+ configFile: options.configFile,
71
+ clientId: options.clientId,
72
+ groupId: options.groupId,
73
+ authType: options.authType,
74
+ scopes: options.scopes.length > 0 ? options.scopes : undefined,
75
+ target: options.target,
76
+ auth: {
77
+ lockTimeoutMs: options.lockTimeoutMs,
78
+ loginTimeoutMs: options.loginTimeoutMs,
79
+ refreshBufferMs: options.refreshBufferMs,
80
+ },
81
+ preferUserToMachine: options.preferUserToMachine,
82
+ };
83
+ }
package/src/cli.ts CHANGED
@@ -59,8 +59,9 @@ export async function prepareAndRunProjen(projenArgs: string[], startDir?: strin
59
59
  /**
60
60
  * Mount `name` as a command that captures every following token verbatim and
61
61
  * hands it to the program `load()` resolves. `helpOption(false)` is what lets
62
- * `--help` through to the child instead of being answered here, and the child
63
- * program owns its own flags, so this wrapper never has to restate them.
62
+ * `--help` through to the child instead of being answered here, and
63
+ * `passThroughOptions()` keeps unknown flags in that token list so the child
64
+ * program owns them.
64
65
  */
65
66
  function addForwardedCommand(
66
67
  program: Command,
@@ -73,6 +74,7 @@ function addForwardedCommand(
73
74
  .description(description)
74
75
  .argument("[args...]", `arguments forwarded to ${name}`)
75
76
  .allowUnknownOption()
77
+ .passThroughOptions()
76
78
  .allowExcessArguments()
77
79
  .helpOption(false)
78
80
  .action(async (args: string[]) => {
@@ -88,6 +90,7 @@ export function buildProgram(name: string = PROGRAM_NAMES[0]): Command {
88
90
  .description(
89
91
  "Databricks developer tools: workspace lifecycle, AppKit env, auth, tunnels, and local proxies",
90
92
  )
93
+ .enablePositionalOptions()
91
94
  .showHelpAfterError()
92
95
  .helpOption("-h, --help", `Show ${name} help`);
93
96
 
@@ -106,47 +109,71 @@ export function buildProgram(name: string = PROGRAM_NAMES[0]): Command {
106
109
  program,
107
110
  "appkit",
108
111
  "AppKit helpers (env: print the environment an AppKit app resolves)",
109
- async () => (await import("@dbx-tools/cli-appkit-env/cli")).buildProgram,
112
+ async () => (await import("./appkit/cli.ts")).buildProgram,
110
113
  );
111
114
 
112
115
  addForwardedCommand(
113
116
  program,
114
117
  "auth",
115
118
  "Authenticate to Databricks and manage OAuth tokens",
116
- async () => (await import("@dbx-tools/cli-auth/cli")).buildProgram,
119
+ async () => (await import("./auth/cli.ts")).buildProgram,
117
120
  );
118
121
 
119
122
  addForwardedCommand(
120
123
  program,
121
124
  "tunnel",
122
125
  "Run a public portr tunnel with an email-OTP gate",
123
- async () => (await import("@dbx-tools/cli-tunnel/cli")).buildProgram,
126
+ async () => (await import("./tunnel/cli.ts")).buildProgram,
124
127
  );
125
128
 
126
129
  addForwardedCommand(
127
130
  program,
128
131
  "lakebase-proxy",
129
132
  "Run the Node Databricks Lakebase PostgreSQL proxy",
130
- async () => (await import("@dbx-tools/cli-lakebase-proxy/cli")).buildProgram,
133
+ async () => (await import("./lakebase-proxy/cli.ts")).buildProgram,
131
134
  );
132
135
 
133
136
  addForwardedCommand(
134
137
  program,
135
138
  "model-gateway",
136
139
  "Run the foreground AppKit Databricks model gateway",
137
- async () => (await import("@dbx-tools/cli-model-gateway/cli")).buildProgram,
140
+ async () => (await import("./model-gateway/cli.ts")).buildProgram,
141
+ );
142
+
143
+ addForwardedCommand(
144
+ program,
145
+ "genie",
146
+ "Run managed Genie Code through a local Databricks model gateway",
147
+ async () => (await import("./genie-code/cli.ts")).buildProgram,
138
148
  );
139
149
 
140
150
  addForwardedCommand(
141
151
  program,
142
152
  "graphiti",
143
153
  "Run Graphiti or manage its current-user desktop service",
144
- async () => (await import("@dbx-tools/cli-graphiti/cli")).buildProgram,
154
+ async () => (await import("./graphiti/cli.ts")).buildProgram,
145
155
  );
146
156
 
147
157
  return program;
148
158
  }
149
159
 
160
+ /** Build the complete command tree used for generated documentation. */
161
+ export async function buildDocumentationProgram(name: string = PROGRAM_NAMES[0]): Promise<Command> {
162
+ const program = buildProgram(name);
163
+ (program.commands as Command[]).splice(
164
+ 1,
165
+ program.commands.length - 1,
166
+ (await import("./appkit/cli.ts")).buildProgram("appkit"),
167
+ (await import("./auth/cli.ts")).buildProgram("auth"),
168
+ (await import("./tunnel/cli.ts")).buildProgram("tunnel"),
169
+ (await import("./lakebase-proxy/cli.ts")).buildProgram("lakebase-proxy"),
170
+ (await import("./model-gateway/cli.ts")).buildProgram("model-gateway"),
171
+ (await import("./genie-code/cli.ts")).buildProgram("genie"),
172
+ (await import("./graphiti/cli.ts")).buildProgram("graphiti"),
173
+ );
174
+ return program;
175
+ }
176
+
150
177
  /**
151
178
  * Program name for help output: whichever bin the user actually typed, so
152
179
  * `dbx-tools --help` doesn't document itself as `dbx`. Falls back to `dbx`.
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Managed `dbx genie` command.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { addArgs, parseArgs } from "@dbx-tools/cli-args";
8
+ import { Command } from "commander";
9
+
10
+ import { GenieCodeCliOptionsSchema, type GenieCodeCliOptions } from "./options.ts";
11
+ import { runGenieCode } from "./runtime.ts";
12
+
13
+ /** Injectable managed Genie execution boundary for parser tests and embedding. */
14
+ export interface GenieCodeCliDependencies {
15
+ run(input: {
16
+ cwd: string;
17
+ genieArgs: readonly string[];
18
+ options: GenieCodeCliOptions;
19
+ }): Promise<void>;
20
+ }
21
+
22
+ const DEFAULT_DEPENDENCIES: GenieCodeCliDependencies = {
23
+ run: runGenieCode,
24
+ };
25
+
26
+ /** Build the managed Genie Code command without executing it. */
27
+ export function buildProgram(
28
+ name = "dbx genie",
29
+ dependencies: GenieCodeCliDependencies = DEFAULT_DEPENDENCIES,
30
+ ): Command {
31
+ const program = addArgs(
32
+ new Command()
33
+ .name(name)
34
+ .description("Run Genie Code through an authenticated local Databricks model gateway")
35
+ .argument(
36
+ "[genieArgs...]",
37
+ "arguments and unknown flags forwarded to Genie Code; wrapper options may be separated with --",
38
+ )
39
+ .allowUnknownOption()
40
+ .passThroughOptions()
41
+ .allowExcessArguments()
42
+ .showHelpAfterError(),
43
+ GenieCodeCliOptionsSchema,
44
+ );
45
+ program.action(async (genieArgs: string[]) => {
46
+ await dependencies.run({
47
+ cwd: process.cwd(),
48
+ genieArgs,
49
+ options: parseArgs(program, GenieCodeCliOptionsSchema),
50
+ });
51
+ });
52
+ return program;
53
+ }
54
+
55
+ /** Parse argv and run managed Genie Code. */
56
+ export async function runCli(argv: string[]): Promise<void> {
57
+ await buildProgram().parseAsync(argv);
58
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Generate one persistent Genie Code home per exact profile-model pairing.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { createHash, randomUUID } from "node:crypto";
8
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
9
+ import { homedir } from "node:os";
10
+ import { dirname, join, resolve } from "node:path";
11
+ import { withFileLock } from "@dbx-tools/core/file-lock";
12
+ import { object } from "@dbx-tools/shared-core";
13
+ import {
14
+ GenieCodeBaseConfigSchema,
15
+ GenieCodeConfigSchema,
16
+ GenieCodeProviderOverlaySchema,
17
+ type GenieCodeBaseConfig,
18
+ type GenieCodeConfig,
19
+ type GenieCodeProviderOverlay,
20
+ } from "@dbx-tools/shared-genie-code/config";
21
+ import { genieCodePairingName } from "@dbx-tools/shared-genie-code/options";
22
+ import { parse, stringify } from "smol-toml";
23
+
24
+ /** Provider identifier written to the base and invocation overlay configurations. */
25
+ export const GENIE_CODE_GATEWAY_PROVIDER = "dbx_tools_model_gateway";
26
+
27
+ /** Inputs used to update a persistent home and create one invocation overlay. */
28
+ export interface WriteGenieCodeConfigOptions {
29
+ bearerToken: string;
30
+ gatewayBaseUrl: string;
31
+ homeDirectory?: string;
32
+ model: string;
33
+ profile: string;
34
+ projectDirectory: string;
35
+ }
36
+
37
+ /** Stable persistent home for one exact profile-model pairing. */
38
+ export interface GenieCodePairingHome {
39
+ configPath: string;
40
+ home: string;
41
+ name: string;
42
+ }
43
+
44
+ /** Persistent pairing home and invocation-specific provider overlay. */
45
+ export interface GenieCodeHome extends GenieCodePairingHome {
46
+ overlayName: string;
47
+ overlayPath: string;
48
+ }
49
+
50
+ /** Derive the stable home for one exact profile-model pair. */
51
+ export function genieCodeHome(
52
+ profile: string,
53
+ model: string,
54
+ homeDirectory: string = homedir(),
55
+ ): GenieCodePairingHome {
56
+ const digest = createHash("sha256")
57
+ .update(JSON.stringify([profile, model]))
58
+ .digest("hex")
59
+ .slice(0, 12);
60
+ const name = genieCodePairingName({ profile, model, digest });
61
+ const home = join(homeDirectory, ".dbx-tools", "genie", "profiles", name);
62
+ return { name, home, configPath: join(home, "config.toml") };
63
+ }
64
+
65
+ /** Render the persistent profile-model configuration. */
66
+ export function genieCodeBaseConfig(
67
+ options: WriteGenieCodeConfigOptions,
68
+ existingProjects: Readonly<Record<string, unknown>> = {},
69
+ ): GenieCodeBaseConfig {
70
+ const projects = Object.fromEntries(
71
+ Object.entries(existingProjects).filter(
72
+ ([, value]) => object.isRecord(value) && value.trust_level === "trusted",
73
+ ),
74
+ );
75
+ return GenieCodeBaseConfigSchema.parse({
76
+ model_provider: GENIE_CODE_GATEWAY_PROVIDER,
77
+ model: options.model,
78
+ databricks_profile: options.profile,
79
+ projects: {
80
+ ...projects,
81
+ [resolve(options.projectDirectory)]: {
82
+ trust_level: "trusted",
83
+ },
84
+ },
85
+ tui: {
86
+ model_availability_nux: {
87
+ [options.model]: 1,
88
+ },
89
+ },
90
+ });
91
+ }
92
+
93
+ /** Render the invocation-specific guarded provider configuration. */
94
+ export function genieCodeProviderOverlay(
95
+ options: WriteGenieCodeConfigOptions,
96
+ ): GenieCodeProviderOverlay {
97
+ return GenieCodeProviderOverlaySchema.parse({
98
+ model_providers: {
99
+ [GENIE_CODE_GATEWAY_PROVIDER]: {
100
+ name: "dbx-tools model gateway",
101
+ base_url: options.gatewayBaseUrl,
102
+ wire_api: "responses",
103
+ requires_openai_auth: false,
104
+ supports_websockets: false,
105
+ http_headers: {
106
+ Authorization: `Bearer ${options.bearerToken}`,
107
+ Originator: "codex",
108
+ },
109
+ },
110
+ },
111
+ });
112
+ }
113
+
114
+ /** Render the merged configuration used by validation and tests. */
115
+ export function genieCodeConfig(options: WriteGenieCodeConfigOptions): GenieCodeConfig {
116
+ return GenieCodeConfigSchema.parse({
117
+ ...genieCodeBaseConfig(options),
118
+ ...genieCodeProviderOverlay(options),
119
+ });
120
+ }
121
+
122
+ async function existingProjects(path: string): Promise<Record<string, unknown>> {
123
+ try {
124
+ const value = parse(await readFile(path, "utf8"));
125
+ return object.isRecord(value.projects) ? value.projects : {};
126
+ } catch (error) {
127
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return {};
128
+ throw error;
129
+ }
130
+ }
131
+
132
+ async function writePrivateToml(path: string, value: unknown): Promise<void> {
133
+ const staged = join(dirname(path), `.${randomUUID()}.toml`);
134
+ try {
135
+ await writeFile(staged, stringify(value), { mode: 0o600 });
136
+ await chmod(staged, 0o600);
137
+ await rename(staged, path);
138
+ } finally {
139
+ await rm(staged, { force: true });
140
+ }
141
+ }
142
+
143
+ /** Update the persistent base config and write one private invocation overlay. */
144
+ export async function writeGenieCodeConfig(
145
+ options: WriteGenieCodeConfigOptions,
146
+ ): Promise<GenieCodeHome> {
147
+ const base = genieCodeHome(options.profile, options.model, options.homeDirectory);
148
+ const overlayName = `dbx-${randomUUID().replaceAll("-", "")}`;
149
+ const destination = {
150
+ ...base,
151
+ overlayName,
152
+ overlayPath: join(base.home, `${overlayName}.config.toml`),
153
+ };
154
+ await mkdir(destination.home, { recursive: true });
155
+ await withFileLock(["genie-code-config", destination.configPath], async () => {
156
+ const projects = await existingProjects(destination.configPath);
157
+ await writePrivateToml(destination.configPath, genieCodeBaseConfig(options, projects));
158
+ });
159
+ await writePrivateToml(destination.overlayPath, genieCodeProviderOverlay(options));
160
+ return destination;
161
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Install the pinned multi-file Genie Code runtime under dbx-tools ownership.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { homedir } from "node:os";
8
+ import { dirname, join } from "node:path";
9
+ import { bin } from "@dbx-tools/core";
10
+ import { GENIE_CODE_VERSION } from "@dbx-tools/shared-genie-code/options";
11
+
12
+ interface GenieCodeAsset {
13
+ target: string;
14
+ sha256: string;
15
+ }
16
+
17
+ const GENIE_CODE_ASSETS = {
18
+ "darwin:arm64": {
19
+ target: "aarch64-apple-darwin",
20
+ sha256: "78cc7e358c8bfc62d6da26364c762be37fc2e2a10b7e83f21a3b166f901f528e",
21
+ },
22
+ "darwin:x64": {
23
+ target: "x86_64-apple-darwin",
24
+ sha256: "2021b8b27ec3116fc015c1676b5f65c3ce11d57f1dff9c302aae135f6e4f1a5f",
25
+ },
26
+ "linux:arm64": {
27
+ target: "aarch64-unknown-linux-musl",
28
+ sha256: "dfd278c267dde43e1fe7e7d7a0da8d7a1a148cdf3d2004bd30b351645c4fe0a7",
29
+ },
30
+ "linux:x64": {
31
+ target: "x86_64-unknown-linux-musl",
32
+ sha256: "e3d9a55f6d51bdc745eda4744fb827ba1df6dacf1069518f7347a59f48c89430",
33
+ },
34
+ "win32:arm64": {
35
+ target: "aarch64-pc-windows-msvc",
36
+ sha256: "aeac95bd0148f942194270ba932245cec32e98da67292ba7b008bcab675ba7f8",
37
+ },
38
+ "win32:x64": {
39
+ target: "x86_64-pc-windows-msvc",
40
+ sha256: "c9f92c8d60273250262527bca525451501c4e8351e59927a1686c305be29a922",
41
+ },
42
+ } as const satisfies Record<string, GenieCodeAsset>;
43
+
44
+ /** Host overrides used by installation tests and embedded callers. */
45
+ export interface InstallGenieCodeOptions {
46
+ homeDirectory?: string;
47
+ platform?: NodeJS.Platform;
48
+ architecture?: NodeJS.Architecture;
49
+ }
50
+
51
+ /** Resolve the pinned release asset for one supported host. */
52
+ export function genieCodeAsset(
53
+ platform: NodeJS.Platform = process.platform,
54
+ architecture: NodeJS.Architecture = process.arch,
55
+ ): GenieCodeAsset {
56
+ const asset = GENIE_CODE_ASSETS[`${platform}:${architecture}` as keyof typeof GENIE_CODE_ASSETS];
57
+ if (!asset) {
58
+ throw new Error(
59
+ `Genie Code ${GENIE_CODE_VERSION} does not support ${platform}/${architecture}`,
60
+ );
61
+ }
62
+ return asset;
63
+ }
64
+
65
+ /** Install and return the shared Genie Code package entrypoint. */
66
+ export async function installGenieCode(
67
+ options: InstallGenieCodeOptions = {},
68
+ ): Promise<bin.BinContext> {
69
+ const platform = options.platform ?? process.platform;
70
+ const asset = genieCodeAsset(platform, options.architecture ?? process.arch);
71
+ const executable = platform === "win32" ? "genie.exe" : "genie";
72
+ const helper = platform === "win32" ? "genie-code-mode-host.exe" : "genie-code-mode-host";
73
+ const entrypoint = join("bin", executable);
74
+ const root = join(
75
+ options.homeDirectory ?? homedir(),
76
+ ".dbx-tools",
77
+ "genie",
78
+ "releases",
79
+ `${GENIE_CODE_VERSION}-${asset.target}`,
80
+ );
81
+ const destination: bin.BinContext = {
82
+ root,
83
+ binDir: join(root, dirname(entrypoint)),
84
+ path: join(root, entrypoint),
85
+ };
86
+ const url =
87
+ `https://github.com/databricks/genie-code-cli/releases/download/v${GENIE_CODE_VERSION}/` +
88
+ `genie-package-${asset.target}.tar.gz`;
89
+ return bin.ensure(
90
+ "genie",
91
+ { url, sha256: asset.sha256 },
92
+ {
93
+ destination,
94
+ minVersion: "0.1.0",
95
+ versionParser: ({ stdout }) =>
96
+ stdout.trim() === `genie ${GENIE_CODE_VERSION}` ? "0.1.0" : undefined,
97
+ package: {
98
+ entrypoint,
99
+ requiredPaths: ["genie-package.json", join("bin", helper), "genie-path", "genie-resources"],
100
+ },
101
+ },
102
+ );
103
+ }