@databricks/appkit 0.56.0 → 0.58.0

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 (36) hide show
  1. package/dist/appkit/package.js +1 -1
  2. package/dist/cli/commands/doctor/bundle.js +128 -0
  3. package/dist/cli/commands/doctor/bundle.js.map +1 -0
  4. package/dist/cli/commands/doctor/checks-existence.js +247 -0
  5. package/dist/cli/commands/doctor/checks-existence.js.map +1 -0
  6. package/dist/cli/commands/doctor/checks-wiring.js +35 -0
  7. package/dist/cli/commands/doctor/checks-wiring.js.map +1 -0
  8. package/dist/cli/commands/doctor/checks.js +206 -0
  9. package/dist/cli/commands/doctor/checks.js.map +1 -0
  10. package/dist/cli/commands/doctor/databricks-client.js +80 -0
  11. package/dist/cli/commands/doctor/databricks-client.js.map +1 -0
  12. package/dist/cli/commands/doctor/index.js +40 -0
  13. package/dist/cli/commands/doctor/index.js.map +1 -0
  14. package/dist/cli/commands/doctor/report.js +177 -0
  15. package/dist/cli/commands/doctor/report.js.map +1 -0
  16. package/dist/cli/commands/doctor/resolve-targets.js +86 -0
  17. package/dist/cli/commands/doctor/resolve-targets.js.map +1 -0
  18. package/dist/cli/commands/doctor/run.js +153 -0
  19. package/dist/cli/commands/doctor/run.js.map +1 -0
  20. package/dist/cli/commands/doctor/types.js +27 -0
  21. package/dist/cli/commands/doctor/types.js.map +1 -0
  22. package/dist/cli/commands/doctor/utils.js +34 -0
  23. package/dist/cli/commands/doctor/utils.js.map +1 -0
  24. package/dist/cli/index.js +2 -0
  25. package/dist/cli/index.js.map +1 -1
  26. package/dist/connectors/lakebase/index.js +2 -3
  27. package/dist/connectors/lakebase/index.js.map +1 -1
  28. package/dist/plugins/ai-search/ai-search.d.ts +21 -0
  29. package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
  30. package/dist/plugins/ai-search/ai-search.js +74 -13
  31. package/dist/plugins/ai-search/ai-search.js.map +1 -1
  32. package/dist/plugins/ai-search/defaults.js +4 -1
  33. package/dist/plugins/ai-search/defaults.js.map +1 -1
  34. package/docs/plugins/ai-search.md +10 -0
  35. package/package.json +1 -1
  36. package/sbom.cdx.json +1 -1
@@ -0,0 +1,206 @@
1
+ import { errorMessage, withTimeout } from "./utils.js";
2
+ import { SdkNotInstalledError, getProfileHost, getServiceClient } from "./databricks-client.js";
3
+
4
+ //#region src/cli/commands/doctor/checks.ts
5
+ /**
6
+ * The `auth` and `config` layer checks. (The `existence` layer's per-type
7
+ * probes live in `checks-existence.ts`.)
8
+ */
9
+ /**
10
+ * The host the SDK actually resolved. This is the authority for what was
11
+ * contacted: `DATABRICKS_HOST` wins the host when set, but a profile supplies it
12
+ * otherwise, so the env var alone can't tell you the workspace in play.
13
+ *
14
+ * Only populated once the SDK has resolved its config, which it does lazily on
15
+ * the first API call — reading it right after construction yields undefined.
16
+ */
17
+ function resolvedHostOf(client) {
18
+ const host = client?.config?.host;
19
+ return typeof host === "string" && host.length > 0 ? host : void 0;
20
+ }
21
+ /** Compares hosts ignoring scheme, trailing slash, and case, so
22
+ * `https://foo.com/` and `foo.com` count as the same workspace. */
23
+ function sameHost(a, b) {
24
+ const normalize = (h) => h.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/+$/, "");
25
+ return normalize(a) === normalize(b);
26
+ }
27
+ /**
28
+ * Warns when `DATABRICKS_HOST` and the named profile point at different
29
+ * workspaces. The SDK resolves these per-field — env wins the host while the
30
+ * profile still supplies the credentials — so this combination silently
31
+ * authenticates with one workspace's token against another's URL. Almost always
32
+ * a mistake, and the resulting 401/403 gives no hint of the split.
33
+ */
34
+ async function hostProfileConflict(envHost, profile) {
35
+ if (!envHost || !profile) return void 0;
36
+ const profileHost = await getProfileHost(profile);
37
+ if (!profileHost || sameHost(envHost, profileHost)) return void 0;
38
+ return `DATABRICKS_HOST (${envHost}) and profile "${profile}" (${profileHost}) point at different workspaces. DATABRICKS_HOST wins the host while the profile still supplies credentials — unset one of them.`;
39
+ }
40
+ /**
41
+ * Last-resort host recovery for when the client never got built: the SDK's
42
+ * ConfigError appends the resolved config as `host=<url>, profile=…`.
43
+ */
44
+ function hostFromError(message) {
45
+ const match = message.match(/host=([^\s,]+)/);
46
+ return match ? match[1].replace(/[.,]+$/, "") : void 0;
47
+ }
48
+ /**
49
+ * Strips URL userinfo (`user:pass@`) from a host, so credentials someone
50
+ * embedded in `DATABRICKS_HOST` never reach the report or `--json`. Returns the
51
+ * input unchanged when it has no userinfo or isn't a parseable URL.
52
+ */
53
+ function sanitizeHost(host) {
54
+ if (host === void 0) return void 0;
55
+ try {
56
+ const url = new URL(host);
57
+ if (!url.username && !url.password) return host;
58
+ url.username = "";
59
+ url.password = "";
60
+ return url.toString();
61
+ } catch {
62
+ return host.replace(/^([^/@]*\/\/)?[^/@]*@/, (_, scheme) => scheme ?? "");
63
+ }
64
+ }
65
+ /**
66
+ * Validates `DATABRICKS_HOST` before the SDK sees it, so an unfilled placeholder
67
+ * gets a clear message instead of the SDK's opaque credentials error. Returns an
68
+ * error message, or null when the host is acceptable or unset.
69
+ *
70
+ * Every message quotes the **sanitized** host: `detail` prints unconditionally in
71
+ * both the human report and `--json` (only `raw` is `--detail`-gated), so echoing
72
+ * the raw value would leak `user:pass@` credentials into CI logs.
73
+ */
74
+ function validateHost(host) {
75
+ if (host === void 0 || host.trim().length === 0) return null;
76
+ const shown = sanitizeHost(host);
77
+ let url;
78
+ try {
79
+ url = new URL(host);
80
+ } catch {
81
+ return `DATABRICKS_HOST is not a valid URL: "${shown}"`;
82
+ }
83
+ if (url.protocol !== "https:" && url.protocol !== "http:") return `DATABRICKS_HOST must be an http(s) URL: "${shown}"`;
84
+ if (!/[a-z0-9]/i.test(url.hostname)) return `DATABRICKS_HOST looks like an unfilled placeholder: "${shown}"`;
85
+ return null;
86
+ }
87
+ /**
88
+ * Layer: auth. Runs once, app-wide; a failure short-circuits every resource's
89
+ * existence check (they all need the client).
90
+ */
91
+ async function checkAuth(options) {
92
+ const rawHost = process.env.DATABRICKS_HOST;
93
+ const host = sanitizeHost(rawHost);
94
+ const profile = options.profile ?? process.env.DATABRICKS_CONFIG_PROFILE;
95
+ const hostError = validateHost(rawHost);
96
+ if (hostError) return { result: {
97
+ status: "error",
98
+ code: "HOST_INVALID",
99
+ detail: hostError,
100
+ host,
101
+ profile
102
+ } };
103
+ const conflict = await hostProfileConflict(host, profile);
104
+ let built;
105
+ try {
106
+ const { client } = await getServiceClient(options.profile);
107
+ built = client;
108
+ const me = await withTimeout(client.currentUser.me());
109
+ const who = me.userName ?? me.id ?? "unknown";
110
+ const resolvedHost = sanitizeHost(resolvedHostOf(client)) ?? host;
111
+ if (conflict) return {
112
+ client,
113
+ result: {
114
+ status: "warn",
115
+ code: "HOST_PROFILE_CONFLICT",
116
+ detail: `authenticated as ${who}`,
117
+ hint: conflict,
118
+ host: resolvedHost,
119
+ profile
120
+ }
121
+ };
122
+ return {
123
+ client,
124
+ result: {
125
+ status: "ok",
126
+ code: "AUTH_OK",
127
+ detail: `authenticated as ${who}`,
128
+ host: resolvedHost,
129
+ profile
130
+ }
131
+ };
132
+ } catch (err) {
133
+ if (err instanceof SdkNotInstalledError) return { result: {
134
+ status: "error",
135
+ code: "SDK_NOT_INSTALLED",
136
+ detail: err.message,
137
+ profile
138
+ } };
139
+ const raw = errorMessage(err);
140
+ const sdkProfile = raw.match(/--profile\s+(\S+)/)?.[1];
141
+ const usedProfile = profile ?? sdkProfile;
142
+ const shownProfile = profile ?? (sdkProfile ? `${sdkProfile} (resolved)` : void 0);
143
+ const shownHost = sanitizeHost(resolvedHostOf(built) ?? hostFromError(raw)) ?? host;
144
+ return { result: {
145
+ status: "error",
146
+ code: "AUTH_FAILED",
147
+ detail: "authentication failed",
148
+ hint: conflict ?? authFailureHint(raw, usedProfile),
149
+ host: shownHost,
150
+ profile: shownProfile,
151
+ raw
152
+ } };
153
+ }
154
+ }
155
+ /**
156
+ * Suggests a next action for common auth failures, matched most specific first.
157
+ * Returns undefined for an unrecognized failure.
158
+ *
159
+ * `profile` is the explicit --profile, else the one the SDK resolved (DEFAULT /
160
+ * DATABRICKS_CONFIG_PROFILE) and embedded in its error, so the login hint
161
+ * targets what actually failed.
162
+ */
163
+ function authFailureHint(message, profile) {
164
+ const loginCmd = profile ? `databricks auth login --profile ${profile}` : "databricks auth login";
165
+ if (/ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ECONNRESET|ETIMEDOUT|getaddrinfo|certificate|self.signed|unable to verify|TLS/i.test(message)) return "Check that the workspace host is correct and reachable (verify DATABRICKS_HOST or the profile's host, and that you're online).";
166
+ if (/has no .* profile configured|profile .* (does not exist|not found)/i.test(message)) return `Run \`${loginCmd}\`, or pass an existing profile via --profile.`;
167
+ if (/cannot get access token|refresh token|reauthenticate|databricks auth token|token .*expired|cannot configure default credentials|default auth|no .*credentials/i.test(message)) return `Run \`${loginCmd}\` and confirm the profile/host is the one you intend.`;
168
+ }
169
+ /** The default local env file, matching the `.env` the CLI auto-loads. */
170
+ const DEFAULT_ENV_FILE = ".env";
171
+ /**
172
+ * Layer: config. Offline presence check of each declared env var; whether a set
173
+ * value points at a real resource is the existence layer's job.
174
+ *
175
+ * The message names the env file explicitly, because "not set" alone reads as
176
+ * ambiguous between the three files a var can be declared in — the local env
177
+ * file, `app.yaml`, and `databricks.yml`. This layer only ever means the first.
178
+ */
179
+ function checkConfig(target, ctx) {
180
+ const missing = [];
181
+ for (const envVar of target.envVars) {
182
+ const value = process.env[envVar];
183
+ if (value === void 0 || value.trim().length === 0) missing.push(envVar);
184
+ }
185
+ if (missing.length > 0) {
186
+ const plural = missing.length > 1;
187
+ const names = missing.join(", ");
188
+ const them = plural ? "them" : "it";
189
+ const allWired = missing.every((name) => ctx.wiredEnvVars.has(name));
190
+ return {
191
+ layer: "config",
192
+ status: target.required ? "error" : "warn",
193
+ code: target.required ? "ENV_MISSING" : "ENV_MISSING_OPTIONAL",
194
+ detail: `${names} ${plural ? "are" : "is"} not set in \`${ctx.envFile}\`${target.required ? "" : " (optional)"}`,
195
+ hint: allWired ? void 0 : `Add ${them} to \`${ctx.envFile}\` to run locally, and wire ${them} through app.yaml + databricks.yml for deploy.`
196
+ };
197
+ }
198
+ return {
199
+ layer: "config",
200
+ status: "ok"
201
+ };
202
+ }
203
+
204
+ //#endregion
205
+ export { DEFAULT_ENV_FILE, checkAuth, checkConfig };
206
+ //# sourceMappingURL=checks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checks.js","names":[],"sources":["../../../../src/cli/commands/doctor/checks.ts"],"sourcesContent":["/**\n * The `auth` and `config` layer checks. (The `existence` layer's per-type\n * probes live in `checks-existence.ts`.)\n */\n\nimport {\n getProfileHost,\n getServiceClient,\n SdkNotInstalledError,\n} from \"./databricks-client\";\nimport type {\n AuthCheckResult,\n DoctorOptions,\n LayerResult,\n ResourceTarget,\n} from \"./types\";\nimport { errorMessage, withTimeout } from \"./utils\";\n\n/** The auth result plus the resolved client (present only on success). */\nexport interface AuthOutcome {\n result: AuthCheckResult;\n client?: unknown;\n}\n\ninterface CurrentUserClient {\n currentUser: {\n me: () => Promise<{ id?: string; userName?: string }>;\n };\n}\n\n/** Just the resolved config off a WorkspaceClient, read structurally to keep\n * `shared` SDK-free. */\ninterface ConfiguredClient {\n config?: { host?: unknown };\n}\n\n/**\n * The host the SDK actually resolved. This is the authority for what was\n * contacted: `DATABRICKS_HOST` wins the host when set, but a profile supplies it\n * otherwise, so the env var alone can't tell you the workspace in play.\n *\n * Only populated once the SDK has resolved its config, which it does lazily on\n * the first API call — reading it right after construction yields undefined.\n */\nfunction resolvedHostOf(client: unknown): string | undefined {\n const host = (client as ConfiguredClient | undefined)?.config?.host;\n return typeof host === \"string\" && host.length > 0 ? host : undefined;\n}\n\n/** Compares hosts ignoring scheme, trailing slash, and case, so\n * `https://foo.com/` and `foo.com` count as the same workspace. */\nfunction sameHost(a: string, b: string): boolean {\n const normalize = (h: string) =>\n h\n .trim()\n .toLowerCase()\n .replace(/^https?:\\/\\//, \"\")\n .replace(/\\/+$/, \"\");\n return normalize(a) === normalize(b);\n}\n\n/**\n * Warns when `DATABRICKS_HOST` and the named profile point at different\n * workspaces. The SDK resolves these per-field — env wins the host while the\n * profile still supplies the credentials — so this combination silently\n * authenticates with one workspace's token against another's URL. Almost always\n * a mistake, and the resulting 401/403 gives no hint of the split.\n */\nasync function hostProfileConflict(\n envHost: string | undefined,\n profile: string | undefined,\n): Promise<string | undefined> {\n if (!envHost || !profile) return undefined;\n const profileHost = await getProfileHost(profile);\n if (!profileHost || sameHost(envHost, profileHost)) return undefined;\n return (\n `DATABRICKS_HOST (${envHost}) and profile \"${profile}\" ` +\n `(${profileHost}) point at different workspaces. DATABRICKS_HOST wins the ` +\n `host while the profile still supplies credentials — unset one of them.`\n );\n}\n\n/**\n * Last-resort host recovery for when the client never got built: the SDK's\n * ConfigError appends the resolved config as `host=<url>, profile=…`.\n */\nfunction hostFromError(message: string): string | undefined {\n const match = message.match(/host=([^\\s,]+)/);\n // The config list is prose-terminated (\"…databricks.com.\"), so drop trailing\n // punctuation the capture picked up.\n return match ? match[1].replace(/[.,]+$/, \"\") : undefined;\n}\n\n/**\n * Strips URL userinfo (`user:pass@`) from a host, so credentials someone\n * embedded in `DATABRICKS_HOST` never reach the report or `--json`. Returns the\n * input unchanged when it has no userinfo or isn't a parseable URL.\n */\nexport function sanitizeHost(host: string | undefined): string | undefined {\n if (host === undefined) return undefined;\n try {\n const url = new URL(host);\n if (!url.username && !url.password) return host;\n url.username = \"\";\n url.password = \"\";\n return url.toString();\n } catch {\n // An unparseable host still can't be echoed verbatim — a typo'd scheme\n // (`ht!tp://user:pass@x`) keeps its credentials otherwise. Strip any\n // `userinfo@` span textually, since URL parsing is unavailable here. Covers\n // both a scheme-prefixed host and a bare `user:pass@host`.\n return host.replace(/^([^/@]*\\/\\/)?[^/@]*@/, (_, scheme) => scheme ?? \"\");\n }\n}\n\n/**\n * Validates `DATABRICKS_HOST` before the SDK sees it, so an unfilled placeholder\n * gets a clear message instead of the SDK's opaque credentials error. Returns an\n * error message, or null when the host is acceptable or unset.\n *\n * Every message quotes the **sanitized** host: `detail` prints unconditionally in\n * both the human report and `--json` (only `raw` is `--detail`-gated), so echoing\n * the raw value would leak `user:pass@` credentials into CI logs.\n */\nexport function validateHost(host: string | undefined): string | null {\n if (host === undefined || host.trim().length === 0) return null;\n const shown = sanitizeHost(host);\n\n let url: URL;\n try {\n url = new URL(host);\n } catch {\n return `DATABRICKS_HOST is not a valid URL: \"${shown}\"`;\n }\n\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n return `DATABRICKS_HOST must be an http(s) URL: \"${shown}\"`;\n }\n\n // Reject only what can't name a host at all: a hostname with no alphanumeric\n // character, e.g. the template's unfilled \"https://...\". Requiring a dotted\n // label instead would reject legitimate single-label hosts — `localhost`, a\n // tunnel, or an internal DNS name.\n if (!/[a-z0-9]/i.test(url.hostname)) {\n return `DATABRICKS_HOST looks like an unfilled placeholder: \"${shown}\"`;\n }\n\n return null;\n}\n\n/**\n * Layer: auth. Runs once, app-wide; a failure short-circuits every resource's\n * existence check (they all need the client).\n */\nexport async function checkAuth(options: DoctorOptions): Promise<AuthOutcome> {\n // Validate the raw value, but only ever store/report the sanitized one so\n // credentials embedded in the URL (user:pass@) never reach the report/--json.\n const rawHost = process.env.DATABRICKS_HOST;\n const host = sanitizeHost(rawHost);\n // The identity source shown in the report: explicit --profile, else the env\n // var the SDK would pick up.\n const profile = options.profile ?? process.env.DATABRICKS_CONFIG_PROFILE;\n const hostError = validateHost(rawHost);\n if (hostError) {\n return {\n result: {\n status: \"error\",\n code: \"HOST_INVALID\",\n detail: hostError,\n host,\n profile,\n },\n };\n }\n\n // An env/profile split is a config problem, so it's worth reporting whether or\n // not the credentials happen to work. Resolved offline from ~/.databrickscfg.\n const conflict = await hostProfileConflict(host, profile);\n\n // Holds the client so the catch block can read the host the SDK resolved\n // (which may have come from the profile, not env).\n let built: unknown;\n try {\n const { client } = await getServiceClient(options.profile);\n built = client;\n // Bound the live call so an unresponsive workspace can't hang the CLI; a\n // timeout throws and is reported as an auth failure below.\n const me = await withTimeout(\n (client as CurrentUserClient).currentUser.me(),\n );\n const who = me.userName ?? me.id ?? \"unknown\";\n // Read only now: the SDK resolves its config lazily on the first call, so\n // before me() the host is still undefined.\n const resolvedHost = sanitizeHost(resolvedHostOf(client)) ?? host;\n\n // Credentials work, but the env/profile split still needs fixing — surface\n // it as a warning rather than letting a green tick imply all is well.\n if (conflict) {\n return {\n client,\n result: {\n status: \"warn\",\n code: \"HOST_PROFILE_CONFLICT\",\n detail: `authenticated as ${who}`,\n hint: conflict,\n host: resolvedHost,\n profile,\n },\n };\n }\n\n return {\n client,\n result: {\n status: \"ok\",\n code: \"AUTH_OK\",\n detail: `authenticated as ${who}`,\n host: resolvedHost,\n profile,\n },\n };\n } catch (err) {\n if (err instanceof SdkNotInstalledError) {\n return {\n result: {\n status: \"error\",\n code: \"SDK_NOT_INSTALLED\",\n detail: err.message,\n profile,\n },\n };\n }\n const raw = errorMessage(err);\n // When no profile was given up front, recover the one the SDK resolved (it's\n // embedded in the error) and mark it \"(resolved)\".\n const sdkProfile = raw.match(/--profile\\s+(\\S+)/)?.[1];\n const usedProfile = profile ?? sdkProfile;\n const shownProfile =\n profile ?? (sdkProfile ? `${sdkProfile} (resolved)` : undefined);\n // Prefer the host the SDK resolved (covers a profile-supplied host), then\n // the one embedded in its error, then the raw env var.\n const shownHost =\n sanitizeHost(resolvedHostOf(built) ?? hostFromError(raw)) ?? host;\n return {\n result: {\n status: \"error\",\n code: \"AUTH_FAILED\",\n detail: \"authentication failed\",\n // A conflict explains the failure better than a generic login hint.\n hint: conflict ?? authFailureHint(raw, usedProfile),\n host: shownHost,\n profile: shownProfile,\n raw,\n },\n };\n }\n}\n\n/**\n * Suggests a next action for common auth failures, matched most specific first.\n * Returns undefined for an unrecognized failure.\n *\n * `profile` is the explicit --profile, else the one the SDK resolved (DEFAULT /\n * DATABRICKS_CONFIG_PROFILE) and embedded in its error, so the login hint\n * targets what actually failed.\n */\nfunction authFailureHint(\n message: string,\n profile?: string,\n): string | undefined {\n const loginCmd = profile\n ? `databricks auth login --profile ${profile}`\n : \"databricks auth login\";\n // Network-level failure (unreachable host, DNS, TLS). Matched first — more\n // specific than a credential problem and needs a different fix.\n if (\n /ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ECONNRESET|ETIMEDOUT|getaddrinfo|certificate|self.signed|unable to verify|TLS/i.test(\n message,\n )\n ) {\n return \"Check that the workspace host is correct and reachable (verify DATABRICKS_HOST or the profile's host, and that you're online).\";\n }\n if (\n /has no .* profile configured|profile .* (does not exist|not found)/i.test(\n message,\n )\n ) {\n return `Run \\`${loginCmd}\\`, or pass an existing profile via --profile.`;\n }\n // Expired/failed token, or no credentials resolved: same next action.\n if (\n /cannot get access token|refresh token|reauthenticate|databricks auth token|token .*expired|cannot configure default credentials|default auth|no .*credentials/i.test(\n message,\n )\n ) {\n return `Run \\`${loginCmd}\\` and confirm the profile/host is the one you intend.`;\n }\n return undefined;\n}\n\n/** The default local env file, matching the `.env` the CLI auto-loads. */\nexport const DEFAULT_ENV_FILE = \".env\";\n\n/** What the config layer needs beyond the target to name the *right* file and\n * give advice that fits the app's actual deploy wiring. */\nexport interface ConfigCheckContext {\n /** Where local values were loaded from: `.env`, or `--env-file`'s path. Named\n * in the message so it's clear this is the local env file, not `app.yaml` /\n * `databricks.yml`. */\n envFile: string;\n /** Env vars `app.yaml` already wires. Lets the hint be dropped entirely for a\n * var whose deploy wiring is already correct. */\n wiredEnvVars: ReadonlySet<string>;\n}\n\n/**\n * Layer: config. Offline presence check of each declared env var; whether a set\n * value points at a real resource is the existence layer's job.\n *\n * The message names the env file explicitly, because \"not set\" alone reads as\n * ambiguous between the three files a var can be declared in — the local env\n * file, `app.yaml`, and `databricks.yml`. This layer only ever means the first.\n */\nexport function checkConfig(\n target: ResourceTarget,\n ctx: ConfigCheckContext,\n): LayerResult {\n const missing: string[] = [];\n\n for (const envVar of target.envVars) {\n const value = process.env[envVar];\n if (value === undefined || value.trim().length === 0) {\n missing.push(envVar);\n }\n }\n\n if (missing.length > 0) {\n const plural = missing.length > 1;\n const names = missing.join(\", \");\n const them = plural ? \"them\" : \"it\";\n // Deploy reads app.yaml, never the env file. When wiring is already correct\n // there's nothing to add beyond \"set it\", which the detail line just said —\n // so no hint at all. Only genuinely-missing wiring earns one.\n const allWired = missing.every((name) => ctx.wiredEnvVars.has(name));\n return {\n layer: \"config\",\n status: target.required ? \"error\" : \"warn\",\n code: target.required ? \"ENV_MISSING\" : \"ENV_MISSING_OPTIONAL\",\n detail: `${names} ${plural ? \"are\" : \"is\"} not set in \\`${ctx.envFile}\\`${\n target.required ? \"\" : \" (optional)\"\n }`,\n hint: allWired\n ? undefined\n : `Add ${them} to \\`${ctx.envFile}\\` to run locally, and wire ${them} through app.yaml + databricks.yml for deploy.`,\n };\n }\n\n return { layer: \"config\", status: \"ok\" };\n}\n"],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ //#region src/cli/commands/doctor/databricks-client.ts
2
+ /**
3
+ * The single seam where `appkit doctor` crosses into the Databricks SDK. The
4
+ * SDK-free `shared` package reaches it via a runtime `import(...)`, degrading
5
+ * gracefully when it's absent.
6
+ *
7
+ * `noRestrictedImports` normally routes SDK access through
8
+ * `packages/appkit/src/workspace-client`, but that isn't available here: `appkit`
9
+ * depends on `shared`, so importing it back would be a dependency cycle. `shared`
10
+ * also can't depend on the SDK directly — hence the dynamic `import(...)` and the
11
+ * per-call suppressions below. This file *is* the wrapper for this package, and
12
+ * every SDK reference in the doctor command is confined to it.
13
+ */
14
+ /** Raised when `@databricks/sdk-experimental` is not resolvable at runtime. */
15
+ var SdkNotInstalledError = class extends Error {
16
+ constructor() {
17
+ super("The 'doctor' command requires the Databricks SDK (a dependency of @databricks/appkit). Please install @databricks/appkit to run connection checks.");
18
+ this.name = "SdkNotInstalledError";
19
+ }
20
+ };
21
+ function isModuleNotFound(err) {
22
+ return err instanceof Error && (err.message.includes("Cannot find module") || err.message.includes("Cannot find package") || err.code === "ERR_MODULE_NOT_FOUND");
23
+ }
24
+ /** Constructs a `WorkspaceClient` via the SDK's unified-auth chain. An explicit
25
+ * `profile` is passed through `Config.profile` rather than mutating
26
+ * `process.env`, so it doesn't leak beyond this call. */
27
+ async function getServiceClient(profile) {
28
+ let sdk;
29
+ try {
30
+ sdk = await import("@databricks/sdk-experimental");
31
+ } catch (err) {
32
+ if (isModuleNotFound(err)) throw new SdkNotInstalledError();
33
+ throw err;
34
+ }
35
+ return { client: new sdk.WorkspaceClient(profile ? { profile } : {}) };
36
+ }
37
+ /**
38
+ * The host a named profile declares in `~/.databrickscfg`, read offline. Doctor
39
+ * uses it to detect a profile/host conflict, so it must not depend on the
40
+ * resolved config (where `DATABRICKS_HOST` has already won). Returns undefined
41
+ * for any unreadable file, missing profile, or hostless profile — a conflict we
42
+ * can't prove isn't worth reporting.
43
+ */
44
+ async function getProfileHost(profile) {
45
+ try {
46
+ const { iniFile } = await (await import("@databricks/sdk-experimental")).loadConfigFile(process.env.DATABRICKS_CONFIG_FILE);
47
+ const host = iniFile?.[profile]?.host;
48
+ return typeof host === "string" && host.length > 0 ? host : void 0;
49
+ } catch {
50
+ return;
51
+ }
52
+ }
53
+ /** Raised when `@databricks/appkit` (needed for the Lakebase probe) is absent. */
54
+ var AppkitNotInstalledError = class extends Error {
55
+ constructor() {
56
+ super("The Lakebase connection check requires @databricks/appkit. Please install it to run this check.");
57
+ this.name = "AppkitNotInstalledError";
58
+ }
59
+ };
60
+ /** Builds a Lakebase pool via `createLakebasePool`. Connection settings
61
+ * (`PGHOST`, `LAKEBASE_ENDPOINT`, …) come from the environment; we inject only
62
+ * the resolved workspace client for OAuth token minting. */
63
+ async function getLakebasePool(client) {
64
+ const appkitPkg = "@databricks/appkit";
65
+ let appkit;
66
+ try {
67
+ appkit = await import(appkitPkg);
68
+ } catch (err) {
69
+ if (isModuleNotFound(err)) throw new AppkitNotInstalledError();
70
+ throw err;
71
+ }
72
+ return appkit.createLakebasePool({
73
+ workspaceClient: client,
74
+ logger: { error: false }
75
+ });
76
+ }
77
+
78
+ //#endregion
79
+ export { AppkitNotInstalledError, SdkNotInstalledError, getLakebasePool, getProfileHost, getServiceClient };
80
+ //# sourceMappingURL=databricks-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"databricks-client.js","names":[],"sources":["../../../../src/cli/commands/doctor/databricks-client.ts"],"sourcesContent":["/**\n * The single seam where `appkit doctor` crosses into the Databricks SDK. The\n * SDK-free `shared` package reaches it via a runtime `import(...)`, degrading\n * gracefully when it's absent.\n *\n * `noRestrictedImports` normally routes SDK access through\n * `packages/appkit/src/workspace-client`, but that isn't available here: `appkit`\n * depends on `shared`, so importing it back would be a dependency cycle. `shared`\n * also can't depend on the SDK directly — hence the dynamic `import(...)` and the\n * per-call suppressions below. This file *is* the wrapper for this package, and\n * every SDK reference in the doctor command is confined to it.\n */\n\n/** Raised when `@databricks/sdk-experimental` is not resolvable at runtime. */\nexport class SdkNotInstalledError extends Error {\n constructor() {\n super(\n \"The 'doctor' command requires the Databricks SDK (a dependency of @databricks/appkit). Please install @databricks/appkit to run connection checks.\",\n );\n this.name = \"SdkNotInstalledError\";\n }\n}\n\ninterface ServiceClientHandle {\n /** WorkspaceClient, typed as unknown to keep `shared` SDK-free. */\n client: unknown;\n}\n\nfunction isModuleNotFound(err: unknown): boolean {\n return (\n err instanceof Error &&\n (err.message.includes(\"Cannot find module\") ||\n err.message.includes(\"Cannot find package\") ||\n (err as NodeJS.ErrnoException).code === \"ERR_MODULE_NOT_FOUND\")\n );\n}\n\n/** Constructs a `WorkspaceClient` via the SDK's unified-auth chain. An explicit\n * `profile` is passed through `Config.profile` rather than mutating\n * `process.env`, so it doesn't leak beyond this call. */\nexport async function getServiceClient(\n profile?: string,\n): Promise<ServiceClientHandle> {\n let sdk: { WorkspaceClient: new (opts: Record<string, unknown>) => unknown };\n try {\n // biome-ignore lint/style/noRestrictedImports: shared can't reach appkit's workspace-client wrapper (appkit depends on shared); this file is the SDK seam\n sdk = (await import(\"@databricks/sdk-experimental\")) as typeof sdk;\n } catch (err) {\n if (isModuleNotFound(err)) {\n throw new SdkNotInstalledError();\n }\n throw err;\n }\n\n const client = new sdk.WorkspaceClient(profile ? { profile } : {});\n return { client };\n}\n\n/**\n * The host a named profile declares in `~/.databrickscfg`, read offline. Doctor\n * uses it to detect a profile/host conflict, so it must not depend on the\n * resolved config (where `DATABRICKS_HOST` has already won). Returns undefined\n * for any unreadable file, missing profile, or hostless profile — a conflict we\n * can't prove isn't worth reporting.\n */\nexport async function getProfileHost(\n profile: string,\n): Promise<string | undefined> {\n try {\n // biome-ignore lint/style/noRestrictedImports: see the note at the top of this file — shared can't import appkit's wrapper\n const sdk = (await import(\"@databricks/sdk-experimental\")) as {\n loadConfigFile: (\n file?: string,\n ) => Promise<{ iniFile: Record<string, { host?: string }> }>;\n };\n const { iniFile } = await sdk.loadConfigFile(\n process.env.DATABRICKS_CONFIG_FILE,\n );\n const host = iniFile?.[profile]?.host;\n return typeof host === \"string\" && host.length > 0 ? host : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Raised when `@databricks/appkit` (needed for the Lakebase probe) is absent. */\nexport class AppkitNotInstalledError extends Error {\n constructor() {\n super(\n \"The Lakebase connection check requires @databricks/appkit. Please install it to run this check.\",\n );\n this.name = \"AppkitNotInstalledError\";\n }\n}\n\n/** Minimal `pg.Pool`-shaped handle. The caller owns it and must call `end()`. */\nexport interface LakebasePoolHandle {\n query: (sql: string) => Promise<unknown>;\n end: () => Promise<void>;\n}\n\n/** Builds a Lakebase pool via `createLakebasePool`. Connection settings\n * (`PGHOST`, `LAKEBASE_ENDPOINT`, …) come from the environment; we inject only\n * the resolved workspace client for OAuth token minting. */\nexport async function getLakebasePool(\n client: unknown,\n): Promise<LakebasePoolHandle> {\n // A variable specifier stops TS from statically resolving `@databricks/appkit`,\n // an optional peer that `shared` has no dependency on.\n const appkitPkg = \"@databricks/appkit\";\n let appkit: {\n createLakebasePool: (cfg: Record<string, unknown>) => unknown;\n };\n try {\n appkit = await import(appkitPkg);\n } catch (err) {\n if (isModuleNotFound(err)) {\n throw new AppkitNotInstalledError();\n }\n throw err;\n }\n\n // Silence Lakebase's own logger. It defaults to an error-only console logger\n // that dumps the raw SDK ApiError (stack + full response blob) to stderr on a\n // failed token fetch. doctor classifies and prints that failure itself, so the\n // library's dump is just noise on top of our clean one-line report.\n const pool = appkit.createLakebasePool({\n workspaceClient: client,\n logger: { error: false },\n });\n return pool as unknown as LakebasePoolHandle;\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,IAAa,uBAAb,cAA0C,MAAM;CAC9C,cAAc;AACZ,QACE,qJACD;AACD,OAAK,OAAO;;;AAShB,SAAS,iBAAiB,KAAuB;AAC/C,QACE,eAAe,UACd,IAAI,QAAQ,SAAS,qBAAqB,IACzC,IAAI,QAAQ,SAAS,sBAAsB,IAC1C,IAA8B,SAAS;;;;;AAO9C,eAAsB,iBACpB,SAC8B;CAC9B,IAAI;AACJ,KAAI;AAEF,QAAO,MAAM,OAAO;UACb,KAAK;AACZ,MAAI,iBAAiB,IAAI,CACvB,OAAM,IAAI,sBAAsB;AAElC,QAAM;;AAIR,QAAO,EAAE,QADM,IAAI,IAAI,gBAAgB,UAAU,EAAE,SAAS,GAAG,EAAE,CAAC,EACjD;;;;;;;;;AAUnB,eAAsB,eACpB,SAC6B;AAC7B,KAAI;EAOF,MAAM,EAAE,YAAY,OALP,MAAM,OAAO,iCAKI,eAC5B,QAAQ,IAAI,uBACb;EACD,MAAM,OAAO,UAAU,UAAU;AACjC,SAAO,OAAO,SAAS,YAAY,KAAK,SAAS,IAAI,OAAO;SACtD;AACN;;;;AAKJ,IAAa,0BAAb,cAA6C,MAAM;CACjD,cAAc;AACZ,QACE,kGACD;AACD,OAAK,OAAO;;;;;;AAahB,eAAsB,gBACpB,QAC6B;CAG7B,MAAM,YAAY;CAClB,IAAI;AAGJ,KAAI;AACF,WAAS,MAAM,OAAO;UACf,KAAK;AACZ,MAAI,iBAAiB,IAAI,CACvB,OAAM,IAAI,yBAAyB;AAErC,QAAM;;AAWR,QAJa,OAAO,mBAAmB;EACrC,iBAAiB;EACjB,QAAQ,EAAE,OAAO,OAAO;EACzB,CAAC"}
@@ -0,0 +1,40 @@
1
+ import { printReport, printReportJson } from "./report.js";
2
+ import { runDoctor } from "./run.js";
3
+ import fs from "node:fs";
4
+ import { Command } from "commander";
5
+ import process from "node:process";
6
+ import dotenv from "dotenv";
7
+
8
+ //#region src/cli/commands/doctor/index.ts
9
+ /**
10
+ * Loads an explicit env file into `process.env`, overriding the auto-loaded
11
+ * `.env`. Throws if the file is missing.
12
+ */
13
+ function loadEnvFile(envFile) {
14
+ if (!fs.existsSync(envFile)) throw new Error(`env file not found: ${envFile}`);
15
+ dotenv.config({
16
+ path: envFile,
17
+ override: true
18
+ });
19
+ }
20
+ async function runDoctorCommand(options) {
21
+ if (options.envFile) loadEnvFile(options.envFile);
22
+ const report = await runDoctor(options);
23
+ if (options.json) printReportJson(report, options.detail);
24
+ else printReport(report, options.detail);
25
+ process.exit(report.exitCode);
26
+ }
27
+ const doctorCommand = new Command("doctor").description("Diagnose an AppKit app's Databricks resources: authentication, config, and resource existence/reachability").option("-p, --profile <name>", "Databricks CLI profile to authenticate with").option("-e, --env-file <path>", "Load this env file before checking (overrides the auto-loaded .env), e.g. .env.local").option("-d, --detail", "Show full underlying error messages (raw SDK output) for failures").option("--json", "Output the diagnostic report as JSON").addHelpText("after", `
28
+ Examples:
29
+ $ appkit doctor
30
+ $ appkit doctor --profile my-profile
31
+ $ appkit doctor --env-file .env.local
32
+ $ appkit doctor --detail
33
+ $ appkit doctor --json`).action((opts) => runDoctorCommand(opts).catch((err) => {
34
+ console.error(err);
35
+ process.exit(1);
36
+ }));
37
+
38
+ //#endregion
39
+ export { doctorCommand };
40
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/cli/commands/doctor/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport process from \"node:process\";\nimport { Command } from \"commander\";\nimport dotenv from \"dotenv\";\nimport { printReport, printReportJson } from \"./report\";\nimport { runDoctor } from \"./run\";\nimport type { DoctorOptions } from \"./types\";\n\n/**\n * Loads an explicit env file into `process.env`, overriding the auto-loaded\n * `.env`. Throws if the file is missing.\n */\nexport function loadEnvFile(envFile: string): void {\n if (!fs.existsSync(envFile)) {\n throw new Error(`env file not found: ${envFile}`);\n }\n dotenv.config({ path: envFile, override: true });\n}\n\nasync function runDoctorCommand(options: DoctorOptions): Promise<void> {\n if (options.envFile) loadEnvFile(options.envFile);\n\n const report = await runDoctor(options);\n\n if (options.json) {\n printReportJson(report, options.detail);\n } else {\n printReport(report, options.detail);\n }\n\n process.exit(report.exitCode);\n}\n\nexport const doctorCommand = new Command(\"doctor\")\n .description(\n \"Diagnose an AppKit app's Databricks resources: authentication, config, and resource existence/reachability\",\n )\n .option(\"-p, --profile <name>\", \"Databricks CLI profile to authenticate with\")\n .option(\n \"-e, --env-file <path>\",\n \"Load this env file before checking (overrides the auto-loaded .env), e.g. .env.local\",\n )\n .option(\n \"-d, --detail\",\n \"Show full underlying error messages (raw SDK output) for failures\",\n )\n .option(\"--json\", \"Output the diagnostic report as JSON\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit doctor\n $ appkit doctor --profile my-profile\n $ appkit doctor --env-file .env.local\n $ appkit doctor --detail\n $ appkit doctor --json`,\n )\n .action((opts: DoctorOptions) =>\n runDoctorCommand(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgB,YAAY,SAAuB;AACjD,KAAI,CAAC,GAAG,WAAW,QAAQ,CACzB,OAAM,IAAI,MAAM,uBAAuB,UAAU;AAEnD,QAAO,OAAO;EAAE,MAAM;EAAS,UAAU;EAAM,CAAC;;AAGlD,eAAe,iBAAiB,SAAuC;AACrE,KAAI,QAAQ,QAAS,aAAY,QAAQ,QAAQ;CAEjD,MAAM,SAAS,MAAM,UAAU,QAAQ;AAEvC,KAAI,QAAQ,KACV,iBAAgB,QAAQ,QAAQ,OAAO;KAEvC,aAAY,QAAQ,QAAQ,OAAO;AAGrC,SAAQ,KAAK,OAAO,SAAS;;AAG/B,MAAa,gBAAgB,IAAI,QAAQ,SAAS,CAC/C,YACC,6GACD,CACA,OAAO,wBAAwB,8CAA8C,CAC7E,OACC,yBACA,uFACD,CACA,OACC,gBACA,oEACD,CACA,OAAO,UAAU,uCAAuC,CACxD,YACC,SACA;;;;;;0BAOD,CACA,QAAQ,SACP,iBAAiB,KAAK,CAAC,OAAO,QAAQ;AACpC,SAAQ,MAAM,IAAI;AAClB,SAAQ,KAAK,EAAE;EACf,CACH"}
@@ -0,0 +1,177 @@
1
+ import { AUTH_UNAVAILABLE_CODE, BUNDLE_MANAGED_CODE, STATUS_SEVERITY } from "./types.js";
2
+ import pc from "picocolors";
3
+
4
+ //#region src/cli/commands/doctor/report.ts
5
+ /**
6
+ * Rendering for `appkit doctor` — turns a {@link DoctorReport} into a
7
+ * human-readable console report or JSON.
8
+ */
9
+ /** Column at which a row's label starts, to align detail/hint lines beneath. */
10
+ const SUB_INDENT = " ";
11
+ /**
12
+ * True when a resource's only finding is an auth-skipped existence probe, so it
13
+ * can be collapsed into one summary line.
14
+ */
15
+ function isOnlyAuthSkipped(r) {
16
+ if (r.status !== "skipped") return false;
17
+ if (!r.layers.some((l) => l.layer === "existence" && l.code === AUTH_UNAVAILABLE_CODE)) return false;
18
+ return r.layers.every((l) => l.status === "ok" || l.layer === "existence" && l.code === AUTH_UNAVAILABLE_CODE);
19
+ }
20
+ /** Left-pads every line of `text` by `spaces`, for indented detail blocks. */
21
+ function indent(text, spaces) {
22
+ const pad = " ".repeat(spaces);
23
+ return text.split("\n").map((l) => `${pad}${l}`).join("\n");
24
+ }
25
+ /** A status glyph, coloured by severity. */
26
+ function glyph(status) {
27
+ switch (status) {
28
+ case "ok": return pc.green("✓");
29
+ case "warn": return pc.yellow("⚠");
30
+ case "error": return pc.red("✗");
31
+ default: return pc.dim("•");
32
+ }
33
+ }
34
+ /**
35
+ * Highlights actionable tokens in a detail/hint line: cyan for a "quoted" id or
36
+ * `backticked` code span (delimiters stripped), bold for a SCREAMING_SNAKE env
37
+ * var. Nesting is safe — bold's reset (22) doesn't clear the cyan (39).
38
+ *
39
+ * Bold runs *first*: the cyan passes wrap their match in ANSI escapes, which
40
+ * would put a `\x1b[36m` between the word boundary and the env-var name and stop
41
+ * the `\b` anchor matching — so an env var inside a code span or quotes would
42
+ * never get bold.
43
+ */
44
+ function highlight(text) {
45
+ return text.replace(/\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b/g, (id) => pc.bold(id)).replace(/`([^`]+)`/g, (_, code) => pc.cyan(code)).replace(/"([^"]+)"/g, (_, id) => pc.cyan(id));
46
+ }
47
+ /** A sub-line beneath a row (detail or hint), consistently indented. */
48
+ function subLine(text) {
49
+ console.log(`${SUB_INDENT}${text}`);
50
+ }
51
+ /**
52
+ * Prints the expanded body of a non-ok row: each indented `detail` line, then
53
+ * any `Hint:` set off by a blank line. Callers own the surrounding blank lines.
54
+ */
55
+ function printRowBody(details, hints) {
56
+ for (const d of details) if (d) subLine(highlight(d));
57
+ const realHints = hints.filter((h) => Boolean(h));
58
+ if (realHints.length > 0) {
59
+ console.log("");
60
+ for (const h of realHints) subLine(`${pc.dim("Hint:")} ${highlight(h)}`);
61
+ }
62
+ }
63
+ /** Shows only the non-zero categories, coloured by severity. */
64
+ function summaryLine(counts) {
65
+ const parts = [];
66
+ if (counts.error) parts.push(pc.red(`${counts.error} error${counts.error > 1 ? "s" : ""}`));
67
+ if (counts.warn) parts.push(pc.yellow(`${counts.warn} warning${counts.warn > 1 ? "s" : ""}`));
68
+ if (counts.ok) parts.push(pc.green(`${counts.ok} ok`));
69
+ if (counts.skipped) parts.push(pc.dim(`${counts.skipped} skipped`));
70
+ return parts.length > 0 ? parts.join(pc.dim(", ")) : "nothing to check";
71
+ }
72
+ /** Prints the leading `Auth` check row (always first). On failure, appends the
73
+ * workspace and profile in play, a hint, and — with `--detail` — the raw SDK
74
+ * error. The host matters as much as the profile: `DATABRICKS_HOST` overrides a
75
+ * profile's host, so the two can come from different places. */
76
+ function printAuthRow(report, detail) {
77
+ const { auth } = report;
78
+ console.log(` ${glyph(auth.status)} Auth — ${auth.detail ?? auth.status}`);
79
+ if (auth.status !== "error" && auth.status !== "warn") return;
80
+ if (auth.host) subLine(`${pc.dim("host:")} ${pc.cyan(auth.host)}`);
81
+ if (auth.profile) subLine(`${pc.dim("profile:")} ${pc.cyan(auth.profile)}`);
82
+ if (auth.hint) {
83
+ console.log("");
84
+ subLine(`${pc.dim("Hint:")} ${highlight(auth.hint)}`);
85
+ }
86
+ if (detail && auth.raw) {
87
+ console.log("");
88
+ subLine(pc.dim("Details:"));
89
+ console.log(pc.dim(indent(auth.raw, 7)));
90
+ }
91
+ }
92
+ /**
93
+ * Prints one resource row: a tick and the name on the happy path, actionable
94
+ * layer details beneath otherwise. Bundle-managed resources are noted as
95
+ * deploy-created, not probed.
96
+ */
97
+ function printResourceRow(r) {
98
+ const { target } = r;
99
+ const bundleManaged = target.origin === "bundle-managed";
100
+ console.log(bundleManaged ? ` ${pc.dim("⧗")} ${target.alias} ${pc.dim("will be created on deploy")}` : ` ${glyph(r.status)} ${target.alias}`);
101
+ const problems = r.layers.filter((l) => l.status !== "ok" && l.code !== BUNDLE_MANAGED_CODE);
102
+ if (problems.length > 0) printRowBody(problems.map((l) => l.detail), problems.map((l) => l.hint));
103
+ }
104
+ /** Prints a wiring finding with the same shape as a resource row:
105
+ * `glyph label` on the first line, then the indented detail and hint. */
106
+ function printWiringRow(f) {
107
+ console.log(` ${glyph(f.status)} ${highlight(f.label)}`);
108
+ printRowBody([f.detail], [f.hint]);
109
+ }
110
+ function printReport(report, detail = false) {
111
+ const { resources, wiring, setup, summary, auth } = report;
112
+ const rows = [];
113
+ rows.push({
114
+ status: auth.status,
115
+ expanded: auth.status === "error" || auth.status === "warn",
116
+ print: () => printAuthRow(report, detail)
117
+ });
118
+ const authSkipped = [];
119
+ const checked = [];
120
+ for (const r of resources) {
121
+ if (isOnlyAuthSkipped(r)) {
122
+ authSkipped.push(r);
123
+ continue;
124
+ }
125
+ checked.push({
126
+ status: r.status,
127
+ expanded: r.status !== "ok" && r.layers.some((l) => l.status !== "ok" && l.code !== BUNDLE_MANAGED_CODE),
128
+ print: () => printResourceRow(r)
129
+ });
130
+ }
131
+ if (authSkipped.length > 0) checked.push({
132
+ status: "skipped",
133
+ expanded: false,
134
+ print: () => console.log(` ${pc.dim("•")} ${pc.dim(`${authSkipped.length} resource${authSkipped.length > 1 ? "s" : ""} not checked (auth failed)`)}`)
135
+ });
136
+ for (const f of wiring) checked.push({
137
+ status: f.status,
138
+ expanded: true,
139
+ print: () => printWiringRow(f)
140
+ });
141
+ for (const f of setup) checked.push({
142
+ status: f.status,
143
+ expanded: true,
144
+ print: () => printWiringRow(f)
145
+ });
146
+ checked.sort((a, b) => STATUS_SEVERITY[b.status] - STATUS_SEVERITY[a.status]);
147
+ rows.push(...checked);
148
+ console.log("");
149
+ rows.forEach((row, i) => {
150
+ const prev = rows[i - 1];
151
+ if (i > 0 && (row.expanded || prev.expanded)) console.log("");
152
+ row.print();
153
+ });
154
+ console.log("");
155
+ console.log(summaryLine(summary));
156
+ if (!detail && auth.raw) console.log(pc.dim("\nRun with --detail for full error output."));
157
+ console.log("");
158
+ }
159
+ /**
160
+ * Emits the report as JSON. The raw SDK error (`auth.raw`) can carry sensitive
161
+ * detail, so — matching the human report's `--detail` gate — it's stripped
162
+ * unless `detail` is set. `--json --detail` opts back into the full error.
163
+ */
164
+ function printReportJson(report, detail = false) {
165
+ const emitted = detail || report.auth.raw === void 0 ? report : {
166
+ ...report,
167
+ auth: {
168
+ ...report.auth,
169
+ raw: void 0
170
+ }
171
+ };
172
+ console.log(JSON.stringify(emitted, null, 2));
173
+ }
174
+
175
+ //#endregion
176
+ export { printReport, printReportJson };
177
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","names":[],"sources":["../../../../src/cli/commands/doctor/report.ts"],"sourcesContent":["/**\n * Rendering for `appkit doctor` — turns a {@link DoctorReport} into a\n * human-readable console report or JSON.\n */\n\nimport pc from \"picocolors\";\nimport {\n AUTH_UNAVAILABLE_CODE,\n BUNDLE_MANAGED_CODE,\n type CheckStatus,\n type DoctorReport,\n type ResourceCheckResult,\n STATUS_SEVERITY,\n type WiringFinding,\n} from \"./types\";\n\n/** Column at which a row's label starts, to align detail/hint lines beneath. */\nconst SUB_INDENT = \" \";\n\n/**\n * True when a resource's only finding is an auth-skipped existence probe, so it\n * can be collapsed into one summary line.\n */\nfunction isOnlyAuthSkipped(r: ResourceCheckResult): boolean {\n if (r.status !== \"skipped\") return false;\n const hasAuthSkip = r.layers.some(\n (l) => l.layer === \"existence\" && l.code === AUTH_UNAVAILABLE_CODE,\n );\n if (!hasAuthSkip) return false;\n return r.layers.every(\n (l) =>\n l.status === \"ok\" ||\n (l.layer === \"existence\" && l.code === AUTH_UNAVAILABLE_CODE),\n );\n}\n\n/** Left-pads every line of `text` by `spaces`, for indented detail blocks. */\nfunction indent(text: string, spaces: number): string {\n const pad = \" \".repeat(spaces);\n return text\n .split(\"\\n\")\n .map((l) => `${pad}${l}`)\n .join(\"\\n\");\n}\n\n/** A status glyph, coloured by severity. */\nfunction glyph(status: CheckStatus): string {\n switch (status) {\n case \"ok\":\n return pc.green(\"✓\");\n case \"warn\":\n return pc.yellow(\"⚠\");\n case \"error\":\n return pc.red(\"✗\");\n default:\n return pc.dim(\"•\");\n }\n}\n\n/**\n * Highlights actionable tokens in a detail/hint line: cyan for a \"quoted\" id or\n * `backticked` code span (delimiters stripped), bold for a SCREAMING_SNAKE env\n * var. Nesting is safe — bold's reset (22) doesn't clear the cyan (39).\n *\n * Bold runs *first*: the cyan passes wrap their match in ANSI escapes, which\n * would put a `\\x1b[36m` between the word boundary and the env-var name and stop\n * the `\\b` anchor matching — so an env var inside a code span or quotes would\n * never get bold.\n */\nfunction highlight(text: string): string {\n return text\n .replace(/\\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\\b/g, (id) => pc.bold(id))\n .replace(/`([^`]+)`/g, (_, code) => pc.cyan(code))\n .replace(/\"([^\"]+)\"/g, (_, id) => pc.cyan(id));\n}\n\n/** A sub-line beneath a row (detail or hint), consistently indented. */\nfunction subLine(text: string): void {\n console.log(`${SUB_INDENT}${text}`);\n}\n\n/**\n * Prints the expanded body of a non-ok row: each indented `detail` line, then\n * any `Hint:` set off by a blank line. Callers own the surrounding blank lines.\n */\nfunction printRowBody(\n details: Array<string | undefined>,\n hints: Array<string | undefined>,\n): void {\n for (const d of details) if (d) subLine(highlight(d));\n const realHints = hints.filter((h): h is string => Boolean(h));\n if (realHints.length > 0) {\n console.log(\"\");\n for (const h of realHints) subLine(`${pc.dim(\"Hint:\")} ${highlight(h)}`);\n }\n}\n\n/** Shows only the non-zero categories, coloured by severity. */\nfunction summaryLine(counts: {\n ok: number;\n warn: number;\n error: number;\n skipped: number;\n}): string {\n const parts: string[] = [];\n if (counts.error)\n parts.push(pc.red(`${counts.error} error${counts.error > 1 ? \"s\" : \"\"}`));\n if (counts.warn)\n parts.push(\n pc.yellow(`${counts.warn} warning${counts.warn > 1 ? \"s\" : \"\"}`),\n );\n if (counts.ok) parts.push(pc.green(`${counts.ok} ok`));\n if (counts.skipped) parts.push(pc.dim(`${counts.skipped} skipped`));\n return parts.length > 0 ? parts.join(pc.dim(\", \")) : \"nothing to check\";\n}\n\n/** Prints the leading `Auth` check row (always first). On failure, appends the\n * workspace and profile in play, a hint, and — with `--detail` — the raw SDK\n * error. The host matters as much as the profile: `DATABRICKS_HOST` overrides a\n * profile's host, so the two can come from different places. */\nfunction printAuthRow(report: DoctorReport, detail: boolean): void {\n const { auth } = report;\n console.log(` ${glyph(auth.status)} Auth — ${auth.detail ?? auth.status}`);\n // A warn carries a config problem (e.g. a host/profile split) whose hint needs\n // the same host/profile context as an outright failure.\n if (auth.status !== \"error\" && auth.status !== \"warn\") return;\n\n if (auth.host) {\n subLine(`${pc.dim(\"host:\")} ${pc.cyan(auth.host)}`);\n }\n if (auth.profile) {\n subLine(`${pc.dim(\"profile:\")} ${pc.cyan(auth.profile)}`);\n }\n if (auth.hint) {\n console.log(\"\");\n subLine(`${pc.dim(\"Hint:\")} ${highlight(auth.hint)}`);\n }\n if (detail && auth.raw) {\n console.log(\"\");\n subLine(pc.dim(\"Details:\"));\n console.log(pc.dim(indent(auth.raw, SUB_INDENT.length + 2)));\n }\n}\n\n/**\n * Prints one resource row: a tick and the name on the happy path, actionable\n * layer details beneath otherwise. Bundle-managed resources are noted as\n * deploy-created, not probed.\n */\nfunction printResourceRow(r: ResourceCheckResult): void {\n const { target } = r;\n const bundleManaged = target.origin === \"bundle-managed\";\n console.log(\n bundleManaged\n ? ` ${pc.dim(\"⧗\")} ${target.alias} ${pc.dim(\"will be created on deploy\")}`\n : ` ${glyph(r.status)} ${target.alias}`,\n );\n // Never swallow a finding: a bundle-managed row used to drop its layers, so an\n // error counted in the summary had no visible cause. `BUNDLE_MANAGED` is the\n // expected skip and would just restate the row, so it alone stays quiet.\n const problems = r.layers.filter(\n (l) => l.status !== \"ok\" && l.code !== BUNDLE_MANAGED_CODE,\n );\n if (problems.length > 0) {\n printRowBody(\n problems.map((l) => l.detail),\n problems.map((l) => l.hint),\n );\n }\n}\n\n/** Prints a wiring finding with the same shape as a resource row:\n * `glyph label` on the first line, then the indented detail and hint. */\nfunction printWiringRow(f: WiringFinding): void {\n console.log(` ${glyph(f.status)} ${highlight(f.label)}`);\n printRowBody([f.detail], [f.hint]);\n}\n\n/** A unified printable row. `expanded` rows get a blank line on either side;\n * plain `ok` rows pack together. */\ninterface Row {\n status: CheckStatus;\n expanded: boolean;\n print: () => void;\n}\n\nexport function printReport(report: DoctorReport, detail = false): void {\n const { resources, wiring, setup, summary, auth } = report;\n\n // One flat, severity-sorted list. Auth leads and never sorts.\n const rows: Row[] = [];\n rows.push({\n status: auth.status,\n expanded: auth.status === \"error\" || auth.status === \"warn\",\n print: () => printAuthRow(report, detail),\n });\n\n // When auth failed, resources whose only finding is the auth-skip carry no\n // new information, so collapse them into a single line.\n const authSkipped: ResourceCheckResult[] = [];\n const checked: Row[] = [];\n for (const r of resources) {\n if (isOnlyAuthSkipped(r)) {\n authSkipped.push(r);\n continue;\n }\n checked.push({\n status: r.status,\n // A bundle-managed row is a one-liner unless it carries a real finding, in\n // which case it needs the spacing every expanded row gets.\n expanded:\n r.status !== \"ok\" &&\n r.layers.some(\n (l) => l.status !== \"ok\" && l.code !== BUNDLE_MANAGED_CODE,\n ),\n print: () => printResourceRow(r),\n });\n }\n if (authSkipped.length > 0) {\n checked.push({\n status: \"skipped\",\n expanded: false,\n print: () =>\n console.log(\n ` ${pc.dim(\"•\")} ${pc.dim(\n `${authSkipped.length} resource${authSkipped.length > 1 ? \"s\" : \"\"} not checked (auth failed)`,\n )}`,\n ),\n });\n }\n for (const f of wiring) {\n checked.push({\n status: f.status,\n expanded: true,\n print: () => printWiringRow(f),\n });\n }\n // Setup notices share the wiring row shape (same `label`/`detail`/`hint`).\n for (const f of setup) {\n checked.push({\n status: f.status,\n expanded: true,\n print: () => printWiringRow(f),\n });\n }\n\n // Most severe first (descending severity); stable within a status.\n checked.sort((a, b) => STATUS_SEVERITY[b.status] - STATUS_SEVERITY[a.status]);\n rows.push(...checked);\n\n // A blank line sets any expanded row apart from its neighbours.\n console.log(\"\");\n rows.forEach((row, i) => {\n const prev = rows[i - 1];\n if (i > 0 && (row.expanded || prev.expanded)) console.log(\"\");\n row.print();\n });\n\n // `summary` already counts auth + wiring (built authoritatively in run.ts),\n // so render it directly — no re-folding, and it matches what --json emits.\n console.log(\"\");\n console.log(summaryLine(summary));\n // Point at --detail when there's more to show and it wasn't requested.\n if (!detail && auth.raw) {\n console.log(pc.dim(\"\\nRun with --detail for full error output.\"));\n }\n console.log(\"\");\n}\n\n/**\n * Emits the report as JSON. The raw SDK error (`auth.raw`) can carry sensitive\n * detail, so — matching the human report's `--detail` gate — it's stripped\n * unless `detail` is set. `--json --detail` opts back into the full error.\n */\nexport function printReportJson(report: DoctorReport, detail = false): void {\n const emitted =\n detail || report.auth.raw === undefined\n ? report\n : { ...report, auth: { ...report.auth, raw: undefined } };\n console.log(JSON.stringify(emitted, null, 2));\n}\n"],"mappings":"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"}