Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 23 additions & 33 deletions packages/shared/src/cli/commands/doctor/databricks-client.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,33 @@
/**
* The single seam where `appkit doctor` crosses into the Databricks SDK. The
* SDK-free `shared` package reaches it via a runtime `import(...)`, degrading
* gracefully when it's absent.
*
* `noRestrictedImports` normally routes SDK access through
* `packages/appkit/src/workspace-client`, but that isn't available here: `appkit`
* depends on `shared`, so importing it back would be a dependency cycle. `shared`
* also can't depend on the SDK directly — hence the dynamic `import(...)` and the
* per-call suppressions below. This file *is* the wrapper for this package, and
* every SDK reference in the doctor command is confined to it.
* The seam where `appkit doctor` crosses into the Databricks SDK. It reaches
* the SDK through the shared `workspace-client` facade — the one sanctioned SDK
* import site — so no dynamic import or `noRestrictedImports` suppression is
* needed. (The Lakebase probe still dynamically imports `@databricks/appkit`
* below, since that's an optional peer `shared` deliberately doesn't depend on.)
*/

/** Raised when `@databricks/sdk-experimental` is not resolvable at runtime. */
import {
createWorkspaceClient,
loadConfigFile,
} from "../../../workspace-client";

/**
* Retained for backward compatibility. Since `shared` now depends on the SDK
* (reached via the workspace-client facade), the SDK is always resolvable at
* runtime and this is no longer thrown; kept exported so existing callers and
* the `SDK_NOT_INSTALLED` diagnostic branch keep compiling.
*/
export class SdkNotInstalledError extends Error {
constructor() {
super(
"The 'doctor' command requires the Databricks SDK (a dependency of @databricks/appkit). Please install @databricks/appkit to run connection checks.",
"The 'doctor' command requires the Databricks SDK (a dependency of @databricks/appkit).",
);
this.name = "SdkNotInstalledError";
}
}

interface ServiceClientHandle {
/** WorkspaceClient, typed as unknown to keep `shared` SDK-free. */
/** WorkspaceClient, typed as unknown to keep doctor's call sites SDK-agnostic. */
client: unknown;
}

Expand All @@ -35,24 +40,15 @@ function isModuleNotFound(err: unknown): boolean {
);
}

/** Constructs a `WorkspaceClient` via the SDK's unified-auth chain. An explicit
/** Constructs a workspace client via the SDK's unified-auth chain. An explicit
* `profile` is passed through `Config.profile` rather than mutating
* `process.env`, so it doesn't leak beyond this call. */
export async function getServiceClient(
profile?: string,
): Promise<ServiceClientHandle> {
let sdk: { WorkspaceClient: new (opts: Record<string, unknown>) => unknown };
try {
// biome-ignore lint/style/noRestrictedImports: shared can't reach appkit's workspace-client wrapper (appkit depends on shared); this file is the SDK seam
sdk = (await import("@databricks/sdk-experimental")) as typeof sdk;
} catch (err) {
if (isModuleNotFound(err)) {
throw new SdkNotInstalledError();
}
throw err;
}

const client = new sdk.WorkspaceClient(profile ? { profile } : {});
const client = createWorkspaceClient(
profile ? { profile } : {},
).toLegacyWorkspaceClient();
return { client };
}

Expand All @@ -67,13 +63,7 @@ export async function getProfileHost(
profile: string,
): Promise<string | undefined> {
try {
// biome-ignore lint/style/noRestrictedImports: see the note at the top of this file — shared can't import appkit's wrapper
const sdk = (await import("@databricks/sdk-experimental")) as {
loadConfigFile: (
file?: string,
) => Promise<{ iniFile: Record<string, { host?: string }> }>;
};
const { iniFile } = await sdk.loadConfigFile(
const { iniFile } = await loadConfigFile(
process.env.DATABRICKS_CONFIG_FILE,
);
const host = iniFile?.[profile]?.host;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/workspace-client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type {
export {
ConfigError,
Context,
loadConfigFile,
Time,
TimeUnits,
} from "./legacy";
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/workspace-client/legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export type {
// named 'Time'" at ESM link time. `Time` is only reachable via the module
// object, so we fall back to `SDK.default.Time` (matching the original genie
// connector's `SDK.Time ?? SDK.default.Time` guard).
export const { ConfigError, Context, TimeUnits } = SDK;
export const { ConfigError, Context, TimeUnits, loadConfigFile } = SDK;
export const Time =
SDK.Time ?? (SDK as unknown as { default: typeof SDK }).default.Time;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ vi.mock("@databricks/sdk-experimental", () => ({
Context: class Context {},
Time: class Time {},
TimeUnits: { milliseconds: 0 },
loadConfigFile: vi.fn(),
}));

import { buildLegacyWorkspaceClient } from "../legacy";
Expand Down
14 changes: 14 additions & 0 deletions tools/dist-appkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,20 @@ if (fs.existsSync(sharedBin)) {
fs.copyFileSync(sharedNaming, "tmp/dist/naming.js");
}

// The doctor command reaches the SDK through the shared workspace-client
// facade, whose modules live outside dist/cli. Copy them so the CLI's
// relative imports (../../../workspace-client/*) resolve in the published
// tarball — appkit's own dist/workspace-client is only a re-export shim.
const sharedWorkspaceClient = path.join(
__dirname,
"../packages/shared/dist/workspace-client",
);
if (fs.existsSync(sharedWorkspaceClient)) {
fs.cpSync(sharedWorkspaceClient, "tmp/dist/workspace-client", {
recursive: true,
});
}

// Copy JSON schemas so CLI (e.g. plugin validate/sync) can load them at runtime.
// Place in both dist/schemas and dist/cli/schemas so resolution works whether
// the running module's __dirname is under dist/ or dist/cli/ (e.g. after bundling).
Expand Down
Loading