Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0744f9a
feat(cli): add appkit doctor command
IamGalymzhan Jul 23, 2026
7569c08
Merge branch 'main' into feat/doctor-command
IamGalymzhan Jul 27, 2026
99ec294
refactor(cli): simplify appkit doctor command
IamGalymzhan Jul 27, 2026
93315f6
feat(shared): improve appkit doctor auth diagnostics and report reada…
IamGalymzhan Jul 29, 2026
463560c
test(shared): fix doctor report detail-block assertion
IamGalymzhan Jul 29, 2026
87271e6
Merge branch 'main' into feat/doctor-command
IamGalymzhan Jul 29, 2026
cbf5100
Merge branch 'main' into feat/doctor-command
IamGalymzhan Jul 30, 2026
12191f8
feat(shared): make doctor DAB-aware with wiring checks and refined re…
IamGalymzhan Jul 30, 2026
d8a4cbd
test(shared): strip ANSI codes in doctor report test capture
IamGalymzhan Jul 30, 2026
f955f06
fix(shared): address doctor review — declare js-yaml, fail on bad YAM…
IamGalymzhan Jul 30, 2026
ce641bf
fix(shared): pass doctor profile via SDK config, not process.env muta…
IamGalymzhan Jul 30, 2026
f2d3139
fix(shared): redact secrets from doctor report and --json
IamGalymzhan Jul 31, 2026
bbe3b24
fix(shared): error (not warn) on an unwired REQUIRED env var
IamGalymzhan Jul 31, 2026
5a2568e
fix(shared): bound doctor's live calls with a 10s timeout
IamGalymzhan Jul 31, 2026
b7e54b1
fix(shared): make doctor summary authoritative + add exitCode to report
IamGalymzhan Jul 31, 2026
121ce81
fix(shared): guard checkResource so one throw can't crash the report
IamGalymzhan Jul 31, 2026
b64f14e
fix(shared): silence Lakebase connector's raw error dump in doctor
IamGalymzhan Jul 31, 2026
006b7a7
Merge branch 'main' into feat/doctor-command
IamGalymzhan Jul 31, 2026
e3a5f78
refactor(shared): simplify doctor internals
IamGalymzhan Aug 3, 2026
3eaff24
Merge branch 'main' into feat/doctor-command
IamGalymzhan Aug 3, 2026
92bb74d
Merge branch 'main' into feat/doctor-command
IamGalymzhan Aug 3, 2026
fc70a4c
Merge branch 'main' into feat/doctor-command
IamGalymzhan Aug 4, 2026
5a8a310
fix(shared): sharpen doctor's auth, env-var, and 403 diagnostics
IamGalymzhan Aug 6, 2026
fb1d3d6
fix(shared): exempt doctor's SDK seam from noRestrictedImports
IamGalymzhan Aug 6, 2026
156cb99
fix(shared): address doctor review — bundle-managed false error, host…
IamGalymzhan Aug 7, 2026
cd6a0f6
Merge branch 'main' into feat/doctor-command
IamGalymzhan Aug 7, 2026
644f569
refactor(shared): tidy doctor internals after the review fixes
IamGalymzhan Aug 7, 2026
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
4 changes: 1 addition & 3 deletions packages/appkit/src/connectors/lakebase/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,9 @@ import { createLogger } from "../../logging/logger";
* @returns PostgreSQL pool with appkit integration
*/
export function createLakebasePool(config?: Partial<LakebasePoolConfig>): Pool {
const logger = createLogger("connectors:lakebase");

return createLakebasePoolBase({
logger: createLogger("connectors:lakebase"),
...config,
logger,
});
}

Expand Down
4 changes: 4 additions & 0 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
},
"devDependencies": {
"@types/express": "4.17.23",
"@types/js-yaml": "4.0.9",
"@types/json-schema": "7.0.15",
"@types/node": "25.2.3",
"@types/ws": "8.18.1"
Expand All @@ -43,6 +44,9 @@
"@standard-schema/spec": "1.1.0",
"@clack/prompts": "1.0.1",
"commander": "12.1.0",
"dotenv": "16.6.1",
"js-yaml": "4.2.0",
"picocolors": "1.1.1",
"zod": "4.3.6"
}
}
308 changes: 308 additions & 0 deletions packages/shared/src/cli/commands/doctor/README.md

Large diffs are not rendered by default.

116 changes: 116 additions & 0 deletions packages/shared/src/cli/commands/doctor/bundle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { originForEnvVars, readBundleInfo } from "./bundle";

function tmp(files: Record<string, string>): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "doctor-bundle-"));
for (const [name, content] of Object.entries(files)) {
fs.writeFileSync(path.join(dir, name), content);
}
return dir;
}

const BUNDLE = `
resources:
apps:
app:
name: my-app
resources:
- name: sql-warehouse # external: \${var.*}
sql_warehouse:
id: \${var.sql_warehouse_id}
permission: CAN_USE
- name: report-job # bundle-managed: \${resources.*}
job:
id: \${resources.jobs.report.id}
permission: CAN_MANAGE_RUN
jobs:
report:
name: report
`;

const APP_YAML = `
env:
- name: DATABRICKS_WAREHOUSE_ID
valueFrom: sql-warehouse
- name: DATABRICKS_JOB_REPORT
valueFrom: report-job
`;

describe("readBundleInfo", () => {
const dirs: string[] = [];
afterEach(() => {
for (const d of dirs) fs.rmSync(d, { recursive: true, force: true });
dirs.length = 0;
});

it("returns present:false when no databricks.yml exists", () => {
const dir = tmp({});
dirs.push(dir);
const info = readBundleInfo(dir);
expect(info.present).toBe(false);
expect(info.bindings.size).toBe(0);
});

it("throws on malformed databricks.yml instead of treating it as absent", () => {
// A present-but-unparseable bundle is a deploy-breaking error; swallowing it
// would let doctor skip all wiring checks and report a false all-clear.
const dir = tmp({ "databricks.yml": "resources: [ this: is: not: valid" });
dirs.push(dir);
expect(() => readBundleInfo(dir)).toThrow(/databricks\.yml/);
});

it("throws on malformed app.yaml", () => {
const dir = tmp({
"databricks.yml": BUNDLE,
"app.yaml": "env:\n - name: X\n valueFrom: bad-indent",
});
dirs.push(dir);
expect(() => readBundleInfo(dir)).toThrow(/app\.yaml/);
});

it("classifies external (var) vs bundle-managed (resources ref) bindings", () => {
const dir = tmp({ "databricks.yml": BUNDLE, "app.yaml": APP_YAML });
dirs.push(dir);
const info = readBundleInfo(dir);
expect(info.present).toBe(true);
expect(info.bindings.get("sql-warehouse")?.origin).toBe("external");
const job = info.bindings.get("report-job");
expect(job?.origin).toBe("bundle-managed");
expect(job?.ref).toEqual({ type: "jobs", key: "report" });
});

it("records declared bundle resources and the app.yaml env→binding map", () => {
const dir = tmp({ "databricks.yml": BUNDLE, "app.yaml": APP_YAML });
dirs.push(dir);
const info = readBundleInfo(dir);
expect(info.declaredResources.has("jobs.report")).toBe(true);
expect(info.envToBinding.get("DATABRICKS_WAREHOUSE_ID")).toBe(
"sql-warehouse",
);
});
});

describe("originForEnvVars", () => {
const dirs: string[] = [];
afterEach(() => {
for (const d of dirs) fs.rmSync(d, { recursive: true, force: true });
dirs.length = 0;
});

it("resolves origin by walking env var → binding → origin", () => {
const dir = tmp({ "databricks.yml": BUNDLE, "app.yaml": APP_YAML });
dirs.push(dir);
const info = readBundleInfo(dir);
expect(originForEnvVars(["DATABRICKS_WAREHOUSE_ID"], info)).toBe(
"external",
);
expect(originForEnvVars(["DATABRICKS_JOB_REPORT"], info)).toBe(
"bundle-managed",
);
// Unknown env / no bundle → undefined (caller treats as external).
expect(originForEnvVars(["NOPE"], info)).toBeUndefined();
});
});
187 changes: 187 additions & 0 deletions packages/shared/src/cli/commands/doctor/bundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/**
* Reads `databricks.yml` + `app.yaml` for two things doctor can't get from
* `appkit.plugins.json`:
*
* 1. **Provenance** — whether each binding references an existing resource
* (`${var.*}`/literal → `external`) or one this bundle creates
* (`${resources.<type>.<key>.*}` → `bundle-managed`).
* 2. **Wiring** — the binding `name` joins the three files: `app.yaml`'s
* `valueFrom: <name>` must match a `databricks.yml` binding `name`.
*
* SDK-free; pure YAML parsing. Absent files degrade to empty results.
*/

import fs from "node:fs";
import path from "node:path";
import yaml from "js-yaml";
import type { ResourceOrigin } from "./types";
import { errorMessage } from "./utils";

export const DEFAULT_BUNDLE_FILE = "databricks.yml";
export const DEFAULT_APP_YAML_FILE = "app.yaml";

/** A `${resources.<type>.<key>.<field>}` reference — a bundle-created resource. */
const RESOURCES_REF = /\$\{resources\.([^.]+)\.([^.}]+)\.[^}]+\}/;

/** One app resource binding from `resources.apps.<app>.resources[]`. */
export interface BundleBinding {
/** The binding name — the join key with `app.yaml` `valueFrom`. */
name: string;
/** The typed sub-key that names the kind (`sql_warehouse`, `genie_space`, …). */
type?: string;
origin: ResourceOrigin;
/** For bundle-managed bindings: the `<type>.<key>` it references, if parseable. */
ref?: { type: string; key: string };
}

export interface BundleInfo {
/** Bindings under every app, keyed by binding name. */
bindings: Map<string, BundleBinding>;
/** `app.yaml` env var name → binding name (from `valueFrom`). */
envToBinding: Map<string, string>;
/** Declared bundle resource keys, as `"<type>.<key>"`, for ref-integrity. */
declaredResources: Set<string>;
/** True when a `databricks.yml` was found and parsed. */
present: boolean;
}

interface AppResourceBlock {
name?: string;
[k: string]: unknown;
}
interface BundleAppBlock {
resources?: AppResourceBlock[];
}
interface BundleDoc {
resources?: {
apps?: Record<string, BundleAppBlock>;
[otherType: string]: Record<string, unknown> | undefined;
};
}
interface AppYamlDoc {
env?: Array<{ name?: string; valueFrom?: string }>;
}

/** The typed sub-key of a binding is its single non-`name` object property. */
function bindingType(block: AppResourceBlock): string | undefined {
for (const [k, v] of Object.entries(block)) {
if (k === "name") continue;
if (v && typeof v === "object") return k;
}
return undefined;
}

/** Classifies a binding by its typed sub-key and origin: scanning its field
* values for a `${resources.*}` reference (bundle-managed) vs anything else
* (external). */
function classifyBinding(block: AppResourceBlock): {
type?: string;
origin: ResourceOrigin;
ref?: { type: string; key: string };
} {
const type = bindingType(block);
const typed = type ? block[type] : undefined;
if (typed && typeof typed === "object") {
for (const value of Object.values(typed as Record<string, unknown>)) {
if (typeof value !== "string") continue;
const m = value.match(RESOURCES_REF);
if (m) {
return {
type,
origin: "bundle-managed",
ref: { type: m[1], key: m[2] },
};
}
}
}
return { type, origin: "external" };
}

/**
* Reads and parses a YAML file. Returns `null` when the file is absent (a
* legitimate "no bundle" state); throws on invalid YAML, since silently
* ignoring it would let doctor report a false all-clear.
*/
function readYaml<T>(filePath: string): T | null {
let raw: string;
try {
raw = fs.readFileSync(filePath, "utf-8");
} catch (err) {
if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
throw err;
}
try {
return (yaml.load(raw) ?? {}) as T;
} catch (err) {
throw new Error(
`Failed to parse ${path.basename(filePath)}: ${errorMessage(err)}`,
);
}
}

/**
* Parses `databricks.yml` (+ `app.yaml`) into {@link BundleInfo}. Returns an
* empty-but-`present:false` result when no bundle is found, so callers can treat
* "no bundle" as "everything external" without special-casing.
*/
export function readBundleInfo(
cwd: string = process.cwd(),
bundleFile: string = DEFAULT_BUNDLE_FILE,
appYamlFile: string = DEFAULT_APP_YAML_FILE,
): BundleInfo {
const bindings = new Map<string, BundleBinding>();
const envToBinding = new Map<string, string>();
const declaredResources = new Set<string>();

const doc = readYaml<BundleDoc>(path.resolve(cwd, bundleFile));
if (!doc) {
return { bindings, envToBinding, declaredResources, present: false };
}

for (const [type, group] of Object.entries(doc.resources ?? {})) {
if (!group || typeof group !== "object") continue;
for (const key of Object.keys(group))
declaredResources.add(`${type}.${key}`);
}

for (const app of Object.values(doc.resources?.apps ?? {})) {
for (const block of app.resources ?? []) {
if (!block?.name) continue;
const { type, origin, ref } = classifyBinding(block);
bindings.set(block.name, {
name: block.name,
type,
origin,
ref,
});
}
}

const appYaml = readYaml<AppYamlDoc>(path.resolve(cwd, appYamlFile));
for (const entry of appYaml?.env ?? []) {
if (entry?.name && entry.valueFrom) {
envToBinding.set(entry.name, entry.valueFrom);
}
}

return { bindings, envToBinding, declaredResources, present: true };
}

/**
* Resolves the origin for a target given its env vars, by walking
* env var → binding name → binding origin. Returns undefined when the bundle
* has nothing to say (so the target stays external by default).
*/
export function originForEnvVars(
envVars: string[],
info: BundleInfo,
): ResourceOrigin | undefined {
if (!info.present) return undefined;
for (const env of envVars) {
const bindingName = info.envToBinding.get(env);
if (!bindingName) continue;
const binding = info.bindings.get(bindingName);
if (binding) return binding.origin;
}
return undefined;
}
Loading
Loading