@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.
- package/dist/appkit/package.js +1 -1
- package/dist/cli/commands/doctor/bundle.js +128 -0
- package/dist/cli/commands/doctor/bundle.js.map +1 -0
- package/dist/cli/commands/doctor/checks-existence.js +247 -0
- package/dist/cli/commands/doctor/checks-existence.js.map +1 -0
- package/dist/cli/commands/doctor/checks-wiring.js +35 -0
- package/dist/cli/commands/doctor/checks-wiring.js.map +1 -0
- package/dist/cli/commands/doctor/checks.js +206 -0
- package/dist/cli/commands/doctor/checks.js.map +1 -0
- package/dist/cli/commands/doctor/databricks-client.js +80 -0
- package/dist/cli/commands/doctor/databricks-client.js.map +1 -0
- package/dist/cli/commands/doctor/index.js +40 -0
- package/dist/cli/commands/doctor/index.js.map +1 -0
- package/dist/cli/commands/doctor/report.js +177 -0
- package/dist/cli/commands/doctor/report.js.map +1 -0
- package/dist/cli/commands/doctor/resolve-targets.js +86 -0
- package/dist/cli/commands/doctor/resolve-targets.js.map +1 -0
- package/dist/cli/commands/doctor/run.js +153 -0
- package/dist/cli/commands/doctor/run.js.map +1 -0
- package/dist/cli/commands/doctor/types.js +27 -0
- package/dist/cli/commands/doctor/types.js.map +1 -0
- package/dist/cli/commands/doctor/utils.js +34 -0
- package/dist/cli/commands/doctor/utils.js.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/connectors/lakebase/index.js +2 -3
- package/dist/connectors/lakebase/index.js.map +1 -1
- package/dist/plugins/ai-search/ai-search.d.ts +21 -0
- package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
- package/dist/plugins/ai-search/ai-search.js +74 -13
- package/dist/plugins/ai-search/ai-search.js.map +1 -1
- package/dist/plugins/ai-search/defaults.js +4 -1
- package/dist/plugins/ai-search/defaults.js.map +1 -1
- package/docs/plugins/ai-search.md +10 -0
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { errorMessage } from "./utils.js";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
//#region src/cli/commands/doctor/resolve-targets.ts
|
|
6
|
+
/**
|
|
7
|
+
* Reads the app's resolved template manifest (`appkit.plugins.json`) and
|
|
8
|
+
* flattens each plugin's declared resources into the flat {@link ResourceTarget}
|
|
9
|
+
* shape the checks consume. Offline and SDK-free — parses the same file
|
|
10
|
+
* `appkit plugin list` reads.
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_MANIFEST_FILE = "appkit.plugins.json";
|
|
13
|
+
/** A field's env var is the developer's to supply only when it has no static
|
|
14
|
+
* default and isn't platform-injected at deploy time. */
|
|
15
|
+
function isUserSuppliedEnv(field) {
|
|
16
|
+
if (field.value !== void 0) return false;
|
|
17
|
+
if (field.origin === "platform" || field.origin === "static") return false;
|
|
18
|
+
if (field.localOnly) return false;
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
/** Env vars the config layer should presence-check (i.e. user-supplied ones). */
|
|
22
|
+
function envVarsOf(resource) {
|
|
23
|
+
const fields = resource.fields ?? {};
|
|
24
|
+
const envs = [];
|
|
25
|
+
for (const field of Object.values(fields)) if (field?.env && isUserSuppliedEnv(field)) envs.push(field.env);
|
|
26
|
+
return envs;
|
|
27
|
+
}
|
|
28
|
+
/** Resolves each field's value keyed by manifest field name, preferring the
|
|
29
|
+
* environment over a static `value` default. Unset/empty fields are omitted. */
|
|
30
|
+
function fieldValuesOf(resource) {
|
|
31
|
+
const fields = resource.fields ?? {};
|
|
32
|
+
const values = {};
|
|
33
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
34
|
+
if (!field) continue;
|
|
35
|
+
const envValue = field.env ? process.env[field.env] : void 0;
|
|
36
|
+
const resolved = envValue !== void 0 && envValue.trim().length > 0 ? envValue : field.value;
|
|
37
|
+
if (resolved !== void 0 && resolved.trim().length > 0) values[fieldName] = resolved;
|
|
38
|
+
}
|
|
39
|
+
return values;
|
|
40
|
+
}
|
|
41
|
+
function toTarget(plugin, resource, required) {
|
|
42
|
+
return {
|
|
43
|
+
type: resource.type,
|
|
44
|
+
resourceKey: resource.resourceKey,
|
|
45
|
+
alias: resource.alias ?? resource.resourceKey,
|
|
46
|
+
plugin,
|
|
47
|
+
requiredPermission: resource.permission,
|
|
48
|
+
required,
|
|
49
|
+
envVars: envVarsOf(resource),
|
|
50
|
+
fieldValues: fieldValuesOf(resource)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** @throws if the file cannot be read or parsed. */
|
|
54
|
+
function targetsFromManifestFile(manifestPath) {
|
|
55
|
+
let raw;
|
|
56
|
+
try {
|
|
57
|
+
raw = fs.readFileSync(manifestPath, "utf-8");
|
|
58
|
+
} catch (err) {
|
|
59
|
+
throw new Error(`Failed to read manifest file ${manifestPath}: ${errorMessage(err)}`);
|
|
60
|
+
}
|
|
61
|
+
let data;
|
|
62
|
+
try {
|
|
63
|
+
data = JSON.parse(raw);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
throw new Error(`Failed to parse manifest file ${manifestPath}: ${errorMessage(err)}`);
|
|
66
|
+
}
|
|
67
|
+
const selected = Object.entries(data.plugins ?? {}).filter(([, plugin]) => plugin.requiredByTemplate === true);
|
|
68
|
+
const targets = [];
|
|
69
|
+
for (const [pluginName, plugin] of selected) {
|
|
70
|
+
const resources = plugin.resources ?? {};
|
|
71
|
+
for (const resource of resources.required ?? []) targets.push(toTarget(pluginName, resource, true));
|
|
72
|
+
for (const resource of resources.optional ?? []) targets.push(toTarget(pluginName, resource, false));
|
|
73
|
+
}
|
|
74
|
+
return targets;
|
|
75
|
+
}
|
|
76
|
+
/** Returns an empty list if the manifest is absent — an app may legitimately
|
|
77
|
+
* declare no resources. */
|
|
78
|
+
function resolveTargetsFromCwd(cwd = process.cwd(), manifestFile = DEFAULT_MANIFEST_FILE) {
|
|
79
|
+
const manifestPath = path.resolve(cwd, manifestFile);
|
|
80
|
+
if (!fs.existsSync(manifestPath)) return [];
|
|
81
|
+
return targetsFromManifestFile(manifestPath);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
//#endregion
|
|
85
|
+
export { DEFAULT_MANIFEST_FILE, resolveTargetsFromCwd };
|
|
86
|
+
//# sourceMappingURL=resolve-targets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-targets.js","names":[],"sources":["../../../../src/cli/commands/doctor/resolve-targets.ts"],"sourcesContent":["/**\n * Reads the app's resolved template manifest (`appkit.plugins.json`) and\n * flattens each plugin's declared resources into the flat {@link ResourceTarget}\n * shape the checks consume. Offline and SDK-free — parses the same file\n * `appkit plugin list` reads.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { ResourceTarget } from \"./types\";\nimport { errorMessage } from \"./utils\";\n\nexport const DEFAULT_MANIFEST_FILE = \"appkit.plugins.json\";\n\ninterface ManifestField {\n env?: string;\n /** Static default value baked into the manifest. */\n value?: string;\n origin?: \"user\" | \"platform\" | \"static\" | \"cli\";\n /** Only generated into the local .env; the platform injects it at deploy. */\n localOnly?: boolean;\n}\n\ninterface ManifestResource {\n type: string;\n resourceKey: string;\n alias?: string;\n permission: string;\n fields?: Record<string, ManifestField>;\n}\n\ninterface ManifestPlugin {\n resources?: {\n required?: ManifestResource[];\n optional?: ManifestResource[];\n };\n /**\n * True when the plugin is actually used by the app (imported and passed to\n * `createApp`). `plugin sync` discovers *every* plugin shipped by installed\n * packages but only marks the used ones; doctor checks only those, so an\n * unused built-in doesn't produce phantom \"missing env var\" errors.\n */\n requiredByTemplate?: boolean;\n}\n\ninterface TemplateManifest {\n plugins?: Record<string, ManifestPlugin>;\n}\n\n/** A field's env var is the developer's to supply only when it has no static\n * default and isn't platform-injected at deploy time. */\nfunction isUserSuppliedEnv(field: ManifestField): boolean {\n if (field.value !== undefined) return false;\n if (field.origin === \"platform\" || field.origin === \"static\") return false;\n if (field.localOnly) return false;\n return true;\n}\n\n/** Env vars the config layer should presence-check (i.e. user-supplied ones). */\nfunction envVarsOf(resource: ManifestResource): string[] {\n const fields = resource.fields ?? {};\n const envs: string[] = [];\n for (const field of Object.values(fields)) {\n if (field?.env && isUserSuppliedEnv(field)) envs.push(field.env);\n }\n return envs;\n}\n\n/** Resolves each field's value keyed by manifest field name, preferring the\n * environment over a static `value` default. Unset/empty fields are omitted. */\nfunction fieldValuesOf(resource: ManifestResource): Record<string, string> {\n const fields = resource.fields ?? {};\n const values: Record<string, string> = {};\n for (const [fieldName, field] of Object.entries(fields)) {\n if (!field) continue;\n const envValue = field.env ? process.env[field.env] : undefined;\n const resolved =\n envValue !== undefined && envValue.trim().length > 0\n ? envValue\n : field.value;\n if (resolved !== undefined && resolved.trim().length > 0) {\n values[fieldName] = resolved;\n }\n }\n return values;\n}\n\nfunction toTarget(\n plugin: string,\n resource: ManifestResource,\n required: boolean,\n): ResourceTarget {\n return {\n type: resource.type,\n resourceKey: resource.resourceKey,\n alias: resource.alias ?? resource.resourceKey,\n plugin,\n requiredPermission: resource.permission,\n required,\n envVars: envVarsOf(resource),\n fieldValues: fieldValuesOf(resource),\n };\n}\n\n/** @throws if the file cannot be read or parsed. */\nexport function targetsFromManifestFile(\n manifestPath: string,\n): ResourceTarget[] {\n let raw: string;\n try {\n raw = fs.readFileSync(manifestPath, \"utf-8\");\n } catch (err) {\n throw new Error(\n `Failed to read manifest file ${manifestPath}: ${errorMessage(err)}`,\n );\n }\n\n let data: TemplateManifest;\n try {\n data = JSON.parse(raw) as TemplateManifest;\n } catch (err) {\n throw new Error(\n `Failed to parse manifest file ${manifestPath}: ${errorMessage(err)}`,\n );\n }\n\n // `plugin sync` catalogues every plugin the installed packages ship, marking\n // only those wired into `createApp` with `requiredByTemplate`. Check exactly\n // those, else doctor reports phantom \"missing env var\" errors for unimported\n // plugins.\n // KNOWN LIMITATION: sync strips `requiredByTemplate` for non-GA plugins, so a\n // used beta/experimental plugin is not checked here yet. See the README.\n const selected = Object.entries(data.plugins ?? {}).filter(\n ([, plugin]) => plugin.requiredByTemplate === true,\n );\n\n const targets: ResourceTarget[] = [];\n for (const [pluginName, plugin] of selected) {\n const resources = plugin.resources ?? {};\n for (const resource of resources.required ?? []) {\n targets.push(toTarget(pluginName, resource, true));\n }\n for (const resource of resources.optional ?? []) {\n targets.push(toTarget(pluginName, resource, false));\n }\n }\n return targets;\n}\n\n/** Returns an empty list if the manifest is absent — an app may legitimately\n * declare no resources. */\nexport function resolveTargetsFromCwd(\n cwd: string = process.cwd(),\n manifestFile: string = DEFAULT_MANIFEST_FILE,\n): ResourceTarget[] {\n const manifestPath = path.resolve(cwd, manifestFile);\n if (!fs.existsSync(manifestPath)) {\n return [];\n }\n return targetsFromManifestFile(manifestPath);\n}\n"],"mappings":";;;;;;;;;;;AAYA,MAAa,wBAAwB;;;AAuCrC,SAAS,kBAAkB,OAA+B;AACxD,KAAI,MAAM,UAAU,OAAW,QAAO;AACtC,KAAI,MAAM,WAAW,cAAc,MAAM,WAAW,SAAU,QAAO;AACrE,KAAI,MAAM,UAAW,QAAO;AAC5B,QAAO;;;AAIT,SAAS,UAAU,UAAsC;CACvD,MAAM,SAAS,SAAS,UAAU,EAAE;CACpC,MAAM,OAAiB,EAAE;AACzB,MAAK,MAAM,SAAS,OAAO,OAAO,OAAO,CACvC,KAAI,OAAO,OAAO,kBAAkB,MAAM,CAAE,MAAK,KAAK,MAAM,IAAI;AAElE,QAAO;;;;AAKT,SAAS,cAAc,UAAoD;CACzE,MAAM,SAAS,SAAS,UAAU,EAAE;CACpC,MAAM,SAAiC,EAAE;AACzC,MAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,OAAO,EAAE;AACvD,MAAI,CAAC,MAAO;EACZ,MAAM,WAAW,MAAM,MAAM,QAAQ,IAAI,MAAM,OAAO;EACtD,MAAM,WACJ,aAAa,UAAa,SAAS,MAAM,CAAC,SAAS,IAC/C,WACA,MAAM;AACZ,MAAI,aAAa,UAAa,SAAS,MAAM,CAAC,SAAS,EACrD,QAAO,aAAa;;AAGxB,QAAO;;AAGT,SAAS,SACP,QACA,UACA,UACgB;AAChB,QAAO;EACL,MAAM,SAAS;EACf,aAAa,SAAS;EACtB,OAAO,SAAS,SAAS,SAAS;EAClC;EACA,oBAAoB,SAAS;EAC7B;EACA,SAAS,UAAU,SAAS;EAC5B,aAAa,cAAc,SAAS;EACrC;;;AAIH,SAAgB,wBACd,cACkB;CAClB,IAAI;AACJ,KAAI;AACF,QAAM,GAAG,aAAa,cAAc,QAAQ;UACrC,KAAK;AACZ,QAAM,IAAI,MACR,gCAAgC,aAAa,IAAI,aAAa,IAAI,GACnE;;CAGH,IAAI;AACJ,KAAI;AACF,SAAO,KAAK,MAAM,IAAI;UACf,KAAK;AACZ,QAAM,IAAI,MACR,iCAAiC,aAAa,IAAI,aAAa,IAAI,GACpE;;CASH,MAAM,WAAW,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC,CAAC,QACjD,GAAG,YAAY,OAAO,uBAAuB,KAC/C;CAED,MAAM,UAA4B,EAAE;AACpC,MAAK,MAAM,CAAC,YAAY,WAAW,UAAU;EAC3C,MAAM,YAAY,OAAO,aAAa,EAAE;AACxC,OAAK,MAAM,YAAY,UAAU,YAAY,EAAE,CAC7C,SAAQ,KAAK,SAAS,YAAY,UAAU,KAAK,CAAC;AAEpD,OAAK,MAAM,YAAY,UAAU,YAAY,EAAE,CAC7C,SAAQ,KAAK,SAAS,YAAY,UAAU,MAAM,CAAC;;AAGvD,QAAO;;;;AAKT,SAAgB,sBACd,MAAc,QAAQ,KAAK,EAC3B,eAAuB,uBACL;CAClB,MAAM,eAAe,KAAK,QAAQ,KAAK,aAAa;AACpD,KAAI,CAAC,GAAG,WAAW,aAAa,CAC9B,QAAO,EAAE;AAEX,QAAO,wBAAwB,aAAa"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { AUTH_UNAVAILABLE_CODE, BUNDLE_MANAGED_CODE, STATUS_SEVERITY } from "./types.js";
|
|
2
|
+
import { TimeoutError, errorMessage, withTimeout } from "./utils.js";
|
|
3
|
+
import { originForEnvVars, readBundleInfo } from "./bundle.js";
|
|
4
|
+
import { DEFAULT_ENV_FILE, checkAuth, checkConfig } from "./checks.js";
|
|
5
|
+
import { runExistenceProbe } from "./checks-existence.js";
|
|
6
|
+
import { checkWiring } from "./checks-wiring.js";
|
|
7
|
+
import { DEFAULT_MANIFEST_FILE, resolveTargetsFromCwd } from "./resolve-targets.js";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
//#region src/cli/commands/doctor/run.ts
|
|
12
|
+
/** Orchestration for `appkit doctor`. */
|
|
13
|
+
function worst(a, b) {
|
|
14
|
+
return STATUS_SEVERITY[b] > STATUS_SEVERITY[a] ? b : a;
|
|
15
|
+
}
|
|
16
|
+
async function checkResource(target, client, configCtx) {
|
|
17
|
+
const layers = [];
|
|
18
|
+
let rolled = "ok";
|
|
19
|
+
if (target.origin === "bundle-managed") return {
|
|
20
|
+
target,
|
|
21
|
+
status: "skipped",
|
|
22
|
+
layers: [{
|
|
23
|
+
layer: "existence",
|
|
24
|
+
status: "skipped",
|
|
25
|
+
code: BUNDLE_MANAGED_CODE,
|
|
26
|
+
detail: "created by this bundle on deploy — not probed"
|
|
27
|
+
}]
|
|
28
|
+
};
|
|
29
|
+
const configResult = checkConfig(target, configCtx);
|
|
30
|
+
layers.push(configResult);
|
|
31
|
+
rolled = worst(rolled, configResult.status);
|
|
32
|
+
if (configResult.status === "error") return {
|
|
33
|
+
target,
|
|
34
|
+
status: rolled,
|
|
35
|
+
layers
|
|
36
|
+
};
|
|
37
|
+
if (client === void 0) {
|
|
38
|
+
layers.push({
|
|
39
|
+
layer: "existence",
|
|
40
|
+
status: "skipped",
|
|
41
|
+
code: AUTH_UNAVAILABLE_CODE,
|
|
42
|
+
detail: "skipped because workspace authentication failed"
|
|
43
|
+
});
|
|
44
|
+
rolled = worst(rolled, "skipped");
|
|
45
|
+
} else {
|
|
46
|
+
const result = await withTimeout(runExistenceProbe(client, target)).catch((err) => ({
|
|
47
|
+
layer: "existence",
|
|
48
|
+
status: "error",
|
|
49
|
+
code: err instanceof TimeoutError ? "PROBE_TIMEOUT" : "PROBE_FAILED",
|
|
50
|
+
detail: `probe did not complete: ${errorMessage(err)}`
|
|
51
|
+
}));
|
|
52
|
+
layers.push(result);
|
|
53
|
+
rolled = worst(rolled, result.status);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
target,
|
|
57
|
+
status: rolled,
|
|
58
|
+
layers
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Runs {@link checkResource} behind a guaranteed boundary: any unexpected throw
|
|
63
|
+
* (a probe's own `.catch` handles the common case, but this covers config
|
|
64
|
+
* errors and anything else) becomes a single error row instead of rejecting the
|
|
65
|
+
* `Promise.all` and losing the entire report to a stack trace.
|
|
66
|
+
*/
|
|
67
|
+
async function checkResourceSafe(target, client, configCtx) {
|
|
68
|
+
try {
|
|
69
|
+
return await checkResource(target, client, configCtx);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return {
|
|
72
|
+
target,
|
|
73
|
+
status: "error",
|
|
74
|
+
layers: [{
|
|
75
|
+
layer: "existence",
|
|
76
|
+
status: "error",
|
|
77
|
+
code: "PROBE_EXCEPTION",
|
|
78
|
+
detail: `unexpected error while checking: ${errorMessage(err)}`
|
|
79
|
+
}]
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Checks doctor's own inputs, which otherwise produce a misleading report: run
|
|
85
|
+
* from the wrong directory (say `server/` instead of the app root), nothing is
|
|
86
|
+
* found and the result is a near-empty all-clear — a CI gate passing an app it
|
|
87
|
+
* never looked at.
|
|
88
|
+
*
|
|
89
|
+
* The manifest is the signal for "this is an app root". Without it there's one
|
|
90
|
+
* cause worth reporting (wrong directory), so it reports only that; a missing
|
|
91
|
+
* `.env` would be noise on top. With it, a missing `.env` is worth its own notice
|
|
92
|
+
* because local values could then only have come from the shell.
|
|
93
|
+
*
|
|
94
|
+
* Warnings, not errors: an app may legitimately have no `.env` (values exported
|
|
95
|
+
* in the shell, or running in a deployed container), so this must not fail a
|
|
96
|
+
* build on its own.
|
|
97
|
+
*/
|
|
98
|
+
function checkSetup(cwd, options) {
|
|
99
|
+
if (!fs.existsSync(path.join(cwd, DEFAULT_MANIFEST_FILE))) return [{
|
|
100
|
+
status: "warn",
|
|
101
|
+
code: "NO_RESOURCES_CHECKED",
|
|
102
|
+
label: "no resources checked",
|
|
103
|
+
detail: `no ${DEFAULT_MANIFEST_FILE} found in ${cwd}, so no plugin resources were checked`,
|
|
104
|
+
hint: `Run doctor from the app root (where ${DEFAULT_MANIFEST_FILE} lives), or run \`appkit plugin sync --write\` if it's missing.`
|
|
105
|
+
}];
|
|
106
|
+
if (!options.envFile && !fs.existsSync(path.join(cwd, DEFAULT_ENV_FILE))) return [{
|
|
107
|
+
status: "warn",
|
|
108
|
+
code: "ENV_FILE_MISSING",
|
|
109
|
+
label: DEFAULT_ENV_FILE,
|
|
110
|
+
detail: `no ${DEFAULT_ENV_FILE} found in ${cwd} — local values can only come from the shell`,
|
|
111
|
+
hint: `Create one (see .env.example), or pass --env-file <path>.`
|
|
112
|
+
}];
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
async function runDoctor(options) {
|
|
116
|
+
const { result: auth, client } = await checkAuth(options);
|
|
117
|
+
const cwd = process.cwd();
|
|
118
|
+
const targets = resolveTargetsFromCwd(cwd);
|
|
119
|
+
const bundle = readBundleInfo(cwd);
|
|
120
|
+
for (const target of targets) {
|
|
121
|
+
const origin = originForEnvVars(target.envVars, bundle);
|
|
122
|
+
if (origin) target.origin = origin;
|
|
123
|
+
}
|
|
124
|
+
const configCtx = {
|
|
125
|
+
envFile: options.envFile ?? DEFAULT_ENV_FILE,
|
|
126
|
+
wiredEnvVars: new Set(bundle.envToBinding.keys())
|
|
127
|
+
};
|
|
128
|
+
const setup = checkSetup(cwd, options);
|
|
129
|
+
const resources = await Promise.all(targets.map((target) => checkResourceSafe(target, client, configCtx)));
|
|
130
|
+
const wiring = checkWiring(bundle, targets);
|
|
131
|
+
const summary = {
|
|
132
|
+
ok: 0,
|
|
133
|
+
warn: 0,
|
|
134
|
+
error: 0,
|
|
135
|
+
skipped: 0
|
|
136
|
+
};
|
|
137
|
+
for (const result of resources) summary[result.status] += 1;
|
|
138
|
+
summary[auth.status] += 1;
|
|
139
|
+
for (const finding of wiring) summary[finding.status] += 1;
|
|
140
|
+
for (const finding of setup) summary[finding.status] += 1;
|
|
141
|
+
return {
|
|
142
|
+
auth,
|
|
143
|
+
resources,
|
|
144
|
+
wiring,
|
|
145
|
+
setup,
|
|
146
|
+
summary,
|
|
147
|
+
exitCode: summary.error > 0 ? 1 : 0
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
export { runDoctor };
|
|
153
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","names":[],"sources":["../../../../src/cli/commands/doctor/run.ts"],"sourcesContent":["/** Orchestration for `appkit doctor`. */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { originForEnvVars, readBundleInfo } from \"./bundle\";\nimport {\n type ConfigCheckContext,\n checkAuth,\n checkConfig,\n DEFAULT_ENV_FILE,\n} from \"./checks\";\nimport { runExistenceProbe } from \"./checks-existence\";\nimport { checkWiring } from \"./checks-wiring\";\nimport {\n DEFAULT_MANIFEST_FILE,\n resolveTargetsFromCwd,\n} from \"./resolve-targets\";\nimport {\n AUTH_UNAVAILABLE_CODE,\n BUNDLE_MANAGED_CODE,\n type CheckStatus,\n type DoctorOptions,\n type DoctorReport,\n type LayerResult,\n type ResourceCheckResult,\n type ResourceTarget,\n type SetupFinding,\n STATUS_SEVERITY,\n} from \"./types\";\nimport { errorMessage, TimeoutError, withTimeout } from \"./utils\";\n\nfunction worst(a: CheckStatus, b: CheckStatus): CheckStatus {\n return STATUS_SEVERITY[b] > STATUS_SEVERITY[a] ? b : a;\n}\n\nasync function checkResource(\n target: ResourceTarget,\n // undefined = auth failed, so the live existence layer is skipped.\n client: unknown,\n configCtx: ConfigCheckContext,\n): Promise<ResourceCheckResult> {\n const layers: LayerResult[] = [];\n let rolled: CheckStatus = \"ok\";\n\n // A bundle-managed resource is checked before anything else, because neither\n // remaining layer can say anything true about it: its value comes from\n // `${resources.*}` at deploy time, so an unset env var locally is the *normal*\n // state (not a config error), and probing would be a false NOT_FOUND. A\n // mismatch between the three files is the wiring layer's job, and problems\n // inside databricks.yml are `databricks bundle validate`'s.\n //\n // This must run first: checking config here used to error on the unset var and\n // return early, never reaching this branch — which failed CI for a correctly\n // configured app while the report collapsed the row to a green-looking\n // \"will be created on deploy\" with the error hidden.\n if (target.origin === \"bundle-managed\") {\n return {\n target,\n status: \"skipped\",\n layers: [\n {\n layer: \"existence\",\n status: \"skipped\",\n code: BUNDLE_MANAGED_CODE,\n detail: \"created by this bundle on deploy — not probed\",\n },\n ],\n };\n }\n\n const configResult = checkConfig(target, configCtx);\n layers.push(configResult);\n rolled = worst(rolled, configResult.status);\n // A hard config failure makes the existence probe meaningless.\n if (configResult.status === \"error\") {\n return { target, status: rolled, layers };\n }\n\n if (client === undefined) {\n layers.push({\n layer: \"existence\",\n status: \"skipped\",\n code: AUTH_UNAVAILABLE_CODE,\n detail: \"skipped because workspace authentication failed\",\n });\n rolled = worst(rolled, \"skipped\");\n } else {\n // A reachable-but-unresponsive endpoint must not hang doctor, so bound the\n // probe; a timeout becomes an error row rather than a hung process.\n const result = await withTimeout(runExistenceProbe(client, target)).catch(\n (err): LayerResult => ({\n layer: \"existence\",\n status: \"error\",\n code: err instanceof TimeoutError ? \"PROBE_TIMEOUT\" : \"PROBE_FAILED\",\n detail: `probe did not complete: ${errorMessage(err)}`,\n }),\n );\n layers.push(result);\n rolled = worst(rolled, result.status);\n }\n\n return { target, status: rolled, layers };\n}\n\n/**\n * Runs {@link checkResource} behind a guaranteed boundary: any unexpected throw\n * (a probe's own `.catch` handles the common case, but this covers config\n * errors and anything else) becomes a single error row instead of rejecting the\n * `Promise.all` and losing the entire report to a stack trace.\n */\nasync function checkResourceSafe(\n target: ResourceTarget,\n client: unknown,\n configCtx: ConfigCheckContext,\n): Promise<ResourceCheckResult> {\n try {\n return await checkResource(target, client, configCtx);\n } catch (err) {\n return {\n target,\n status: \"error\",\n layers: [\n {\n layer: \"existence\",\n status: \"error\",\n code: \"PROBE_EXCEPTION\",\n detail: `unexpected error while checking: ${errorMessage(err)}`,\n },\n ],\n };\n }\n}\n\n/**\n * Checks doctor's own inputs, which otherwise produce a misleading report: run\n * from the wrong directory (say `server/` instead of the app root), nothing is\n * found and the result is a near-empty all-clear — a CI gate passing an app it\n * never looked at.\n *\n * The manifest is the signal for \"this is an app root\". Without it there's one\n * cause worth reporting (wrong directory), so it reports only that; a missing\n * `.env` would be noise on top. With it, a missing `.env` is worth its own notice\n * because local values could then only have come from the shell.\n *\n * Warnings, not errors: an app may legitimately have no `.env` (values exported\n * in the shell, or running in a deployed container), so this must not fail a\n * build on its own.\n */\nexport function checkSetup(\n cwd: string,\n options: DoctorOptions,\n): SetupFinding[] {\n // Presence of the manifest, not the target count: a manifest that declares no\n // resources is legitimate and shouldn't be reported as a missing file.\n if (!fs.existsSync(path.join(cwd, DEFAULT_MANIFEST_FILE))) {\n return [\n {\n status: \"warn\",\n code: \"NO_RESOURCES_CHECKED\",\n label: \"no resources checked\",\n detail: `no ${DEFAULT_MANIFEST_FILE} found in ${cwd}, so no plugin resources were checked`,\n hint: `Run doctor from the app root (where ${DEFAULT_MANIFEST_FILE} lives), or run \\`appkit plugin sync --write\\` if it's missing.`,\n },\n ];\n }\n\n // An explicit --env-file that's missing already throws in the CLI, so only the\n // auto-loaded default is worth reporting here.\n if (!options.envFile && !fs.existsSync(path.join(cwd, DEFAULT_ENV_FILE))) {\n return [\n {\n status: \"warn\",\n code: \"ENV_FILE_MISSING\",\n label: DEFAULT_ENV_FILE,\n detail: `no ${DEFAULT_ENV_FILE} found in ${cwd} — local values can only come from the shell`,\n hint: `Create one (see .env.example), or pass --env-file <path>.`,\n },\n ];\n }\n\n return [];\n}\n\nexport async function runDoctor(options: DoctorOptions): Promise<DoctorReport> {\n const { result: auth, client } = await checkAuth(options);\n\n // Resolve and config-check resources even when auth failed, so a bad\n // connection still surfaces config problems instead of hiding them.\n const cwd = process.cwd();\n const targets = resolveTargetsFromCwd(cwd);\n\n const bundle = readBundleInfo(cwd);\n for (const target of targets) {\n const origin = originForEnvVars(target.envVars, bundle);\n if (origin) target.origin = origin;\n }\n\n // Name the file local values actually came from, and let the config layer see\n // the deploy wiring so it doesn't advise fixing what's already correct.\n const configCtx: ConfigCheckContext = {\n envFile: options.envFile ?? DEFAULT_ENV_FILE,\n wiredEnvVars: new Set(bundle.envToBinding.keys()),\n };\n\n const setup = checkSetup(cwd, options);\n\n // Probes are independent reads; Promise.all preserves order for a\n // deterministic report.\n const resources = await Promise.all(\n targets.map((target) => checkResourceSafe(target, client, configCtx)),\n );\n const wiring = checkWiring(bundle, targets);\n\n // The summary counts *everything* with a status — resources, the auth check,\n // wiring findings, and setup notices — so a --json consumer reading\n // summary.error can trust it, and the human/JSON outputs share one source of\n // truth.\n const summary = { ok: 0, warn: 0, error: 0, skipped: 0 };\n for (const result of resources) summary[result.status] += 1;\n summary[auth.status] += 1;\n for (const finding of wiring) summary[finding.status] += 1;\n for (const finding of setup) summary[finding.status] += 1;\n\n const exitCode = summary.error > 0 ? 1 : 0;\n\n return { auth, resources, wiring, setup, summary, exitCode };\n}\n"],"mappings":";;;;;;;;;;;;AA+BA,SAAS,MAAM,GAAgB,GAA6B;AAC1D,QAAO,gBAAgB,KAAK,gBAAgB,KAAK,IAAI;;AAGvD,eAAe,cACb,QAEA,QACA,WAC8B;CAC9B,MAAM,SAAwB,EAAE;CAChC,IAAI,SAAsB;AAa1B,KAAI,OAAO,WAAW,iBACpB,QAAO;EACL;EACA,QAAQ;EACR,QAAQ,CACN;GACE,OAAO;GACP,QAAQ;GACR,MAAM;GACN,QAAQ;GACT,CACF;EACF;CAGH,MAAM,eAAe,YAAY,QAAQ,UAAU;AACnD,QAAO,KAAK,aAAa;AACzB,UAAS,MAAM,QAAQ,aAAa,OAAO;AAE3C,KAAI,aAAa,WAAW,QAC1B,QAAO;EAAE;EAAQ,QAAQ;EAAQ;EAAQ;AAG3C,KAAI,WAAW,QAAW;AACxB,SAAO,KAAK;GACV,OAAO;GACP,QAAQ;GACR,MAAM;GACN,QAAQ;GACT,CAAC;AACF,WAAS,MAAM,QAAQ,UAAU;QAC5B;EAGL,MAAM,SAAS,MAAM,YAAY,kBAAkB,QAAQ,OAAO,CAAC,CAAC,OACjE,SAAsB;GACrB,OAAO;GACP,QAAQ;GACR,MAAM,eAAe,eAAe,kBAAkB;GACtD,QAAQ,2BAA2B,aAAa,IAAI;GACrD,EACF;AACD,SAAO,KAAK,OAAO;AACnB,WAAS,MAAM,QAAQ,OAAO,OAAO;;AAGvC,QAAO;EAAE;EAAQ,QAAQ;EAAQ;EAAQ;;;;;;;;AAS3C,eAAe,kBACb,QACA,QACA,WAC8B;AAC9B,KAAI;AACF,SAAO,MAAM,cAAc,QAAQ,QAAQ,UAAU;UAC9C,KAAK;AACZ,SAAO;GACL;GACA,QAAQ;GACR,QAAQ,CACN;IACE,OAAO;IACP,QAAQ;IACR,MAAM;IACN,QAAQ,oCAAoC,aAAa,IAAI;IAC9D,CACF;GACF;;;;;;;;;;;;;;;;;;AAmBL,SAAgB,WACd,KACA,SACgB;AAGhB,KAAI,CAAC,GAAG,WAAW,KAAK,KAAK,KAAK,sBAAsB,CAAC,CACvD,QAAO,CACL;EACE,QAAQ;EACR,MAAM;EACN,OAAO;EACP,QAAQ,MAAM,sBAAsB,YAAY,IAAI;EACpD,MAAM,uCAAuC,sBAAsB;EACpE,CACF;AAKH,KAAI,CAAC,QAAQ,WAAW,CAAC,GAAG,WAAW,KAAK,KAAK,KAAK,iBAAiB,CAAC,CACtE,QAAO,CACL;EACE,QAAQ;EACR,MAAM;EACN,OAAO;EACP,QAAQ,MAAM,iBAAiB,YAAY,IAAI;EAC/C,MAAM;EACP,CACF;AAGH,QAAO,EAAE;;AAGX,eAAsB,UAAU,SAA+C;CAC7E,MAAM,EAAE,QAAQ,MAAM,WAAW,MAAM,UAAU,QAAQ;CAIzD,MAAM,MAAM,QAAQ,KAAK;CACzB,MAAM,UAAU,sBAAsB,IAAI;CAE1C,MAAM,SAAS,eAAe,IAAI;AAClC,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,SAAS,iBAAiB,OAAO,SAAS,OAAO;AACvD,MAAI,OAAQ,QAAO,SAAS;;CAK9B,MAAM,YAAgC;EACpC,SAAS,QAAQ,WAAW;EAC5B,cAAc,IAAI,IAAI,OAAO,aAAa,MAAM,CAAC;EAClD;CAED,MAAM,QAAQ,WAAW,KAAK,QAAQ;CAItC,MAAM,YAAY,MAAM,QAAQ,IAC9B,QAAQ,KAAK,WAAW,kBAAkB,QAAQ,QAAQ,UAAU,CAAC,CACtE;CACD,MAAM,SAAS,YAAY,QAAQ,QAAQ;CAM3C,MAAM,UAAU;EAAE,IAAI;EAAG,MAAM;EAAG,OAAO;EAAG,SAAS;EAAG;AACxD,MAAK,MAAM,UAAU,UAAW,SAAQ,OAAO,WAAW;AAC1D,SAAQ,KAAK,WAAW;AACxB,MAAK,MAAM,WAAW,OAAQ,SAAQ,QAAQ,WAAW;AACzD,MAAK,MAAM,WAAW,MAAO,SAAQ,QAAQ,WAAW;AAIxD,QAAO;EAAE;EAAM;EAAW;EAAQ;EAAO;EAAS,UAFjC,QAAQ,QAAQ,IAAI,IAAI;EAEmB"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/cli/commands/doctor/types.ts
|
|
2
|
+
/**
|
|
3
|
+
* The existence-layer `code` marking a probe skipped because auth failed. Set in
|
|
4
|
+
* `run.ts` and matched in `report.ts` to collapse those resources into one line;
|
|
5
|
+
* shared so the two sides can't drift.
|
|
6
|
+
*/
|
|
7
|
+
const AUTH_UNAVAILABLE_CODE = "AUTH_UNAVAILABLE";
|
|
8
|
+
/**
|
|
9
|
+
* The existence-layer `code` marking a resource created by this bundle on
|
|
10
|
+
* deploy. Set in `run.ts` and matched in `report.ts` to keep the expected skip
|
|
11
|
+
* quiet; shared so the two sides can't drift.
|
|
12
|
+
*/
|
|
13
|
+
const BUNDLE_MANAGED_CODE = "BUNDLE_MANAGED";
|
|
14
|
+
/**
|
|
15
|
+
* Severity ranking for a status. Used both to roll up the worst status across
|
|
16
|
+
* layers and to sort report rows most-severe-first (descending severity).
|
|
17
|
+
*/
|
|
18
|
+
const STATUS_SEVERITY = {
|
|
19
|
+
ok: 0,
|
|
20
|
+
skipped: 1,
|
|
21
|
+
warn: 2,
|
|
22
|
+
error: 3
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
export { AUTH_UNAVAILABLE_CODE, BUNDLE_MANAGED_CODE, STATUS_SEVERITY };
|
|
27
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","names":[],"sources":["../../../../src/cli/commands/doctor/types.ts"],"sourcesContent":["/** Type definitions for the `appkit doctor` command. */\n\n/** Layers run per resource, in order; a hard failure short-circuits the rest. */\nexport type CheckLayer = \"auth\" | \"config\" | \"existence\";\n\nexport type CheckStatus = \"ok\" | \"warn\" | \"error\" | \"skipped\";\n\n/**\n * The existence-layer `code` marking a probe skipped because auth failed. Set in\n * `run.ts` and matched in `report.ts` to collapse those resources into one line;\n * shared so the two sides can't drift.\n */\nexport const AUTH_UNAVAILABLE_CODE = \"AUTH_UNAVAILABLE\";\n\n/**\n * The existence-layer `code` marking a resource created by this bundle on\n * deploy. Set in `run.ts` and matched in `report.ts` to keep the expected skip\n * quiet; shared so the two sides can't drift.\n */\nexport const BUNDLE_MANAGED_CODE = \"BUNDLE_MANAGED\";\n\n/**\n * Severity ranking for a status. Used both to roll up the worst status across\n * layers and to sort report rows most-severe-first (descending severity).\n */\nexport const STATUS_SEVERITY: Record<CheckStatus, number> = {\n ok: 0,\n skipped: 1,\n warn: 2,\n error: 3,\n};\n\n/**\n * Where a resource's value comes from: `external` (exists now, so probe it) or\n * `bundle-managed` (created by this bundle on deploy, so not probed).\n * Undefined ⇒ external.\n */\nexport type ResourceOrigin = \"external\" | \"bundle-managed\";\n\nexport interface LayerResult {\n layer: CheckLayer;\n status: CheckStatus;\n detail?: string;\n /** Inferred guidance for fixing the finding. */\n hint?: string;\n /** Machine-readable code for `--json` consumers (e.g. `NOT_FOUND`). */\n code?: string;\n}\n\nexport interface ResourceTarget {\n type: string;\n resourceKey: string;\n /** Human-readable label. */\n alias: string;\n plugin: string;\n /** Declared permission level; shown for context, not checked. */\n requiredPermission: string;\n /** Mandatory (vs optional) for the app. */\n required: boolean;\n envVars: string[];\n /** Resolved field values keyed by manifest field name; unset fields omitted. */\n fieldValues: Record<string, string>;\n origin?: ResourceOrigin;\n}\n\nexport interface ResourceCheckResult {\n target: ResourceTarget;\n /** Worst status across all layers run for this resource. */\n status: CheckStatus;\n layers: LayerResult[];\n}\n\nexport interface AuthCheckResult {\n status: CheckStatus;\n detail?: string;\n hint?: string;\n code?: string;\n host?: string;\n profile?: string;\n /** Full underlying error, shown only with `--detail` or in `--json`. */\n raw?: string;\n}\n\n/** A finding from the offline three-file wiring check. */\nexport interface WiringFinding {\n status: CheckStatus;\n /** Machine-readable code (e.g. `VALUEFROM_UNBOUND`, `BUNDLE_REF_MISSING`). */\n code: string;\n /** The env var or binding at fault. */\n label: string;\n detail: string;\n hint?: string;\n}\n\n/**\n * A finding about doctor's own inputs rather than a resource — e.g. no `.env`\n * where one was expected. Shares {@link WiringFinding}'s shape so the report can\n * render both with one code path.\n */\nexport type SetupFinding = WiringFinding;\n\nexport interface DoctorReport {\n auth: AuthCheckResult;\n resources: ResourceCheckResult[];\n wiring: WiringFinding[];\n /** Findings about doctor's own inputs (e.g. a missing `.env`), which explain\n * an otherwise-empty or misleading report. */\n setup: SetupFinding[];\n /**\n * Authoritative counts across *everything* that has a status — resources,\n * the auth check, and wiring findings — not just resources. A `--json`\n * consumer can trust `summary.error === 0` to mean \"nothing failed\".\n */\n summary: { ok: number; warn: number; error: number; skipped: number };\n /** Process exit code (0 ok, 1 if anything errored). The single unambiguous\n * pass/fail signal for programmatic consumers. */\n exitCode: number;\n}\n\nexport interface DoctorOptions {\n profile?: string;\n json?: boolean;\n /** Show full underlying error messages in the human report. */\n detail?: boolean;\n /** Env file to load before checking (e.g. `.env.local`); overrides the .env\n * the CLI auto-loads at startup. */\n envFile?: string;\n}\n"],"mappings":";;;;;;AAYA,MAAa,wBAAwB;;;;;;AAOrC,MAAa,sBAAsB;;;;;AAMnC,MAAa,kBAA+C;CAC1D,IAAI;CACJ,SAAS;CACT,MAAM;CACN,OAAO;CACR"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/cli/commands/doctor/utils.ts
|
|
2
|
+
/** Small shared helpers for the `appkit doctor` command. */
|
|
3
|
+
/** Extracts a human-readable message from an unknown thrown value. */
|
|
4
|
+
function errorMessage(err) {
|
|
5
|
+
return err instanceof Error ? err.message : String(err);
|
|
6
|
+
}
|
|
7
|
+
/** Default per-check wall-clock deadline (ms). A reachable-but-unresponsive
|
|
8
|
+
* endpoint must never hang doctor — it's a CI gate that has to return. */
|
|
9
|
+
const DEFAULT_CHECK_TIMEOUT_MS = 1e4;
|
|
10
|
+
/** Thrown when a check exceeds its deadline. */
|
|
11
|
+
var TimeoutError = class extends Error {
|
|
12
|
+
constructor(ms) {
|
|
13
|
+
super(`timed out after ${ms}ms`);
|
|
14
|
+
this.name = "TimeoutError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Races `promise` against a wall-clock deadline, rejecting with
|
|
19
|
+
* {@link TimeoutError} if it isn't settled in time. This bounds doctor even
|
|
20
|
+
* when the underlying SDK/driver call has no cancellation of its own — we can't
|
|
21
|
+
* abort the request, but we stop *waiting* on it so the report still returns.
|
|
22
|
+
* The timer is always cleared so a fast result leaves nothing pending.
|
|
23
|
+
*/
|
|
24
|
+
function withTimeout(promise, ms = DEFAULT_CHECK_TIMEOUT_MS) {
|
|
25
|
+
let timer;
|
|
26
|
+
const deadline = new Promise((_, reject) => {
|
|
27
|
+
timer = setTimeout(() => reject(new TimeoutError(ms)), ms);
|
|
28
|
+
});
|
|
29
|
+
return Promise.race([promise, deadline]).finally(() => clearTimeout(timer));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
export { TimeoutError, errorMessage, withTimeout };
|
|
34
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","names":[],"sources":["../../../../src/cli/commands/doctor/utils.ts"],"sourcesContent":["/** Small shared helpers for the `appkit doctor` command. */\n\n/** Extracts a human-readable message from an unknown thrown value. */\nexport function errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** Default per-check wall-clock deadline (ms). A reachable-but-unresponsive\n * endpoint must never hang doctor — it's a CI gate that has to return. */\nexport const DEFAULT_CHECK_TIMEOUT_MS = 10_000;\n\n/** Thrown when a check exceeds its deadline. */\nexport class TimeoutError extends Error {\n constructor(ms: number) {\n super(`timed out after ${ms}ms`);\n this.name = \"TimeoutError\";\n }\n}\n\n/**\n * Races `promise` against a wall-clock deadline, rejecting with\n * {@link TimeoutError} if it isn't settled in time. This bounds doctor even\n * when the underlying SDK/driver call has no cancellation of its own — we can't\n * abort the request, but we stop *waiting* on it so the report still returns.\n * The timer is always cleared so a fast result leaves nothing pending.\n */\nexport function withTimeout<T>(\n promise: Promise<T>,\n ms = DEFAULT_CHECK_TIMEOUT_MS,\n): Promise<T> {\n let timer: ReturnType<typeof setTimeout>;\n const deadline = new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new TimeoutError(ms)), ms);\n });\n return Promise.race([promise, deadline]).finally(() =>\n clearTimeout(timer),\n ) as Promise<T>;\n}\n"],"mappings":";;;AAGA,SAAgB,aAAa,KAAsB;AACjD,QAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;;;;AAKzD,MAAa,2BAA2B;;AAGxC,IAAa,eAAb,cAAkC,MAAM;CACtC,YAAY,IAAY;AACtB,QAAM,mBAAmB,GAAG,IAAI;AAChC,OAAK,OAAO;;;;;;;;;;AAWhB,SAAgB,YACd,SACA,KAAK,0BACO;CACZ,IAAI;CACJ,MAAM,WAAW,IAAI,SAAgB,GAAG,WAAW;AACjD,UAAQ,iBAAiB,OAAO,IAAI,aAAa,GAAG,CAAC,EAAE,GAAG;GAC1D;AACF,QAAO,QAAQ,KAAK,CAAC,SAAS,SAAS,CAAC,CAAC,cACvC,aAAa,MAAM,CACpB"}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { codemodCommand } from "./commands/codemod/index.js";
|
|
3
3
|
import { docsCommand } from "./commands/docs.js";
|
|
4
|
+
import { doctorCommand } from "./commands/doctor/index.js";
|
|
4
5
|
import { generateTypesCommand } from "./commands/generate-types.js";
|
|
5
6
|
import { lintCommand } from "./commands/lint.js";
|
|
6
7
|
import { pluginCommand } from "./commands/plugin/index.js";
|
|
@@ -22,6 +23,7 @@ cmd.addCommand(lintCommand);
|
|
|
22
23
|
cmd.addCommand(docsCommand);
|
|
23
24
|
cmd.addCommand(pluginCommand);
|
|
24
25
|
cmd.addCommand(codemodCommand);
|
|
26
|
+
cmd.addCommand(doctorCommand);
|
|
25
27
|
await cmd.parseAsync();
|
|
26
28
|
|
|
27
29
|
//#endregion
|
package/dist/cli/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport \"dotenv/config\";\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Command } from \"commander\";\nimport { codemodCommand } from \"./commands/codemod/index.js\";\nimport { docsCommand } from \"./commands/docs.js\";\nimport { generateTypesCommand } from \"./commands/generate-types.js\";\nimport { lintCommand } from \"./commands/lint.js\";\nimport { pluginCommand } from \"./commands/plugin/index.js\";\nimport { setupCommand } from \"./commands/setup.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst pkgPath = join(__dirname, \"../../package.json\");\nconst pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n\nconst cmd = new Command();\n\ncmd\n .name(\"appkit\")\n .description(\"CLI tools for Databricks AppKit\")\n .version(pkg.version);\n\ncmd.addCommand(setupCommand);\ncmd.addCommand(generateTypesCommand);\ncmd.addCommand(lintCommand);\ncmd.addCommand(docsCommand);\ncmd.addCommand(pluginCommand);\ncmd.addCommand(codemodCommand);\n\nawait cmd.parseAsync();\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport \"dotenv/config\";\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Command } from \"commander\";\nimport { codemodCommand } from \"./commands/codemod/index.js\";\nimport { docsCommand } from \"./commands/docs.js\";\nimport { doctorCommand } from \"./commands/doctor/index.js\";\nimport { generateTypesCommand } from \"./commands/generate-types.js\";\nimport { lintCommand } from \"./commands/lint.js\";\nimport { pluginCommand } from \"./commands/plugin/index.js\";\nimport { setupCommand } from \"./commands/setup.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst pkgPath = join(__dirname, \"../../package.json\");\nconst pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n\nconst cmd = new Command();\n\ncmd\n .name(\"appkit\")\n .description(\"CLI tools for Databricks AppKit\")\n .version(pkg.version);\n\ncmd.addCommand(setupCommand);\ncmd.addCommand(generateTypesCommand);\ncmd.addCommand(lintCommand);\ncmd.addCommand(docsCommand);\ncmd.addCommand(pluginCommand);\ncmd.addCommand(codemodCommand);\ncmd.addCommand(doctorCommand);\n\nawait cmd.parseAsync();\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAM,UAAU,KADE,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EACzB,qBAAqB;AACrD,MAAM,MAAM,KAAK,MAAM,aAAa,SAAS,QAAQ,CAAC;AAEtD,MAAM,MAAM,IAAI,SAAS;AAEzB,IACG,KAAK,SAAS,CACd,YAAY,kCAAkC,CAC9C,QAAQ,IAAI,QAAQ;AAEvB,IAAI,WAAW,aAAa;AAC5B,IAAI,WAAW,qBAAqB;AACpC,IAAI,WAAW,YAAY;AAC3B,IAAI,WAAW,YAAY;AAC3B,IAAI,WAAW,cAAc;AAC7B,IAAI,WAAW,eAAe;AAC9B,IAAI,WAAW,cAAc;AAE7B,MAAM,IAAI,YAAY"}
|
|
@@ -12,10 +12,9 @@ import { RequestedClaimsPermissionSet, createLakebasePool, generateDatabaseCrede
|
|
|
12
12
|
* @returns PostgreSQL pool with appkit integration
|
|
13
13
|
*/
|
|
14
14
|
function createLakebasePool$1(config) {
|
|
15
|
-
const logger = createLogger("connectors:lakebase");
|
|
16
15
|
return createLakebasePool({
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
logger: createLogger("connectors:lakebase"),
|
|
17
|
+
...config
|
|
19
18
|
});
|
|
20
19
|
}
|
|
21
20
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["createLakebasePool","createLakebasePoolBase"],"sources":["../../../src/connectors/lakebase/index.ts"],"sourcesContent":["import {\n createLakebasePool as createLakebasePoolBase,\n type LakebasePoolConfig,\n} from \"@databricks/lakebase\";\nimport type { Pool } from \"pg\";\nimport { createLogger } from \"../../logging/logger\";\n\n/**\n * Create a Lakebase pool with appkit's logger integration.\n * Telemetry automatically uses appkit's OpenTelemetry configuration via global registry.\n *\n * @param config - Lakebase pool configuration\n * @returns PostgreSQL pool with appkit integration\n */\nexport function createLakebasePool(config?: Partial<LakebasePoolConfig>): Pool {\n
|
|
1
|
+
{"version":3,"file":"index.js","names":["createLakebasePool","createLakebasePoolBase"],"sources":["../../../src/connectors/lakebase/index.ts"],"sourcesContent":["import {\n createLakebasePool as createLakebasePoolBase,\n type LakebasePoolConfig,\n} from \"@databricks/lakebase\";\nimport type { Pool } from \"pg\";\nimport { createLogger } from \"../../logging/logger\";\n\n/**\n * Create a Lakebase pool with appkit's logger integration.\n * Telemetry automatically uses appkit's OpenTelemetry configuration via global registry.\n *\n * @param config - Lakebase pool configuration\n * @returns PostgreSQL pool with appkit integration\n */\nexport function createLakebasePool(config?: Partial<LakebasePoolConfig>): Pool {\n return createLakebasePoolBase({\n logger: createLogger(\"connectors:lakebase\"),\n ...config,\n });\n}\n\n// Re-export everything else from lakebase\nexport {\n type DatabaseCredential,\n type GenerateDatabaseCredentialRequest,\n generateDatabaseCredential,\n getLakebaseOrmConfig,\n getLakebasePgConfig,\n getUsernameWithApiLookup,\n getWorkspaceClient,\n type LakebasePoolConfig,\n type RequestedClaims,\n RequestedClaimsPermissionSet,\n type RequestedResource,\n} from \"@databricks/lakebase\";\n\nexport {\n createLakebasePoolManager,\n type LakebasePoolManager,\n} from \"./pool-manager\";\n\nexport { type LakebasePool, RoutingPool } from \"./routing-pool\";\n"],"mappings":";;;;;;;;;;;;;AAcA,SAAgBA,qBAAmB,QAA4C;AAC7E,QAAOC,mBAAuB;EAC5B,QAAQ,aAAa,sBAAsB;EAC3C,GAAG;EACJ,CAAC"}
|
|
@@ -52,7 +52,28 @@ declare class AiSearchPlugin extends Plugin<IAiSearchConfig> {
|
|
|
52
52
|
private _resolveOr404;
|
|
53
53
|
/** Send an execution result as JSON, or its error status/message. */
|
|
54
54
|
private _sendResult;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve request-vs-index defaults for the result-determining fields.
|
|
57
|
+
* Shared by `_prepareQuery` (the payload) and `_cacheKeyFor` (the key) so
|
|
58
|
+
* the two can't drift.
|
|
59
|
+
*/
|
|
60
|
+
private _resolveQueryParams;
|
|
55
61
|
private _prepareQuery;
|
|
62
|
+
/**
|
|
63
|
+
* Cache key for a query: every input that changes the VS result, resolved
|
|
64
|
+
* via `_resolveQueryParams` so the key matches the payload (post-allowlist
|
|
65
|
+
* `columns`, not the raw request). `queryVector` is hashed (vectors are
|
|
66
|
+
* large); the key uses `queryText`, not the derived embedding, since the
|
|
67
|
+
* embedding is a function of `queryText` and hasn't run at key-build time.
|
|
68
|
+
* `columns`/`filters` are order-normalized so equivalent requests share an
|
|
69
|
+
* entry.
|
|
70
|
+
*/
|
|
71
|
+
private _cacheKeyFor;
|
|
72
|
+
private _hashVector;
|
|
73
|
+
/** Execute settings with the per-call cache key folded in. */
|
|
74
|
+
private _executeSettings;
|
|
75
|
+
/** `JSON.stringify` with object keys sorted recursively; array order kept. */
|
|
76
|
+
private _stableStringify;
|
|
56
77
|
private _resolveReranker;
|
|
57
78
|
private _parseResponse;
|
|
58
79
|
private _handleError;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-search.d.ts","names":[],"sources":["../../../src/plugins/ai-search/ai-search.ts"],"mappings":";;;;;;;;;
|
|
1
|
+
{"version":3,"file":"ai-search.d.ts","names":[],"sources":["../../../src/plugins/ai-search/ai-search.ts"],"mappings":";;;;;;;;;cA+Ba,cAAA,SAAuB,MAAA,CAAO,eAAA;EAAA,OAClC,QAAA,EAAuB,cAAA;EAAA,iBAEb,WAAA;EAAA,UAEC,MAAA,EAAQ,eAAA;EAAA,QAElB,SAAA;cAEI,MAAA,EAAQ,eAAA;EATM;;;;EAAA,QAyBlB,eAAA;EAKF,KAAA,CAAA,GAAS,OAAA;EAAA;;;;;;EAAA,QAgCD,oBAAA;EAAA,QAoCN,4BAAA;EAkBR,YAAA,CAAa,MAAA,EAAQ,UAAA;EAgMH;;;;EAtDlB,YAAA,CAAA;IAAkB,OAAA,EAAS,YAAA;EAAA;EA9PO;;;;;;;EAgR5B,KAAA,WAAgB,MAAA,oBAA0B,MAAA,kBAAA,CAC9C,KAAA,UACA,OAAA,EAAS,aAAA,GACR,OAAA,CAAQ,cAAA,CAAe,CAAA;EAiCpB,QAAA,CAAA,GAAY,OAAA;EAIlB,OAAA,CAAA;sBAxCsB,MAAA,oBAAuB,MAAA,mBAAA,KAAA,UAC9B,OAAA,EACJ,aAAA,KACR,OAAA,CAAQ,cAAA,CAAe,CAAA;EAAA;EAAA,QA2ClB,aAAA;EArTI;EAAA,QAgUJ,aAAA;EA3SF;EAAA,QA2TE,WAAA;EA3RM;;;;;EAAA,QA8SN,mBAAA;EAAA,QAcM,aAAA;EA5Ha;;;;;;;;;EAAA,QA0KnB,YAAA;EAAA,QAsBA,WAAA;EA1IF;EAAA,QA+IE,gBAAA;EA3IR;EAAA,QA4JQ,gBAAA;EAAA,QAUA,gBAAA;EAAA,QAiBA,cAAA;EAAA,QA2BA,YAAA;AAAA;AAAA,cAcG,QAAA,EAAQ,QAAA,QAAA,cAAA,EAAA,eAAA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createLogger } from "../../logging/logger.js";
|
|
2
|
-
import { getWorkspaceClient } from "../../context/execution-context.js";
|
|
2
|
+
import { getCurrentUserId, getWorkspaceClient } from "../../context/execution-context.js";
|
|
3
3
|
import { formatWarningBanner } from "../../utils/banner.js";
|
|
4
4
|
import "../../context/index.js";
|
|
5
5
|
import { Plugin } from "../../plugin/plugin.js";
|
|
@@ -8,6 +8,7 @@ import "../../plugin/index.js";
|
|
|
8
8
|
import { AiSearchConnector } from "../../connectors/ai-search/client.js";
|
|
9
9
|
import { aiSearchDefaults } from "./defaults.js";
|
|
10
10
|
import manifest_default from "./manifest.js";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
11
12
|
|
|
12
13
|
//#region src/plugins/ai-search/ai-search.ts
|
|
13
14
|
const logger = createLogger("ai-search");
|
|
@@ -93,8 +94,10 @@ var AiSearchPlugin = class extends Plugin {
|
|
|
93
94
|
return;
|
|
94
95
|
}
|
|
95
96
|
const { columns: _clientColumns, ...safeBody } = body;
|
|
96
|
-
const
|
|
97
|
+
const isAsUser = indexConfig.auth === "on-behalf-of-user";
|
|
98
|
+
const plugin = isAsUser ? this.asUser(req) : this;
|
|
97
99
|
const queryType = safeBody.queryType ?? indexConfig.queryType ?? "hybrid";
|
|
100
|
+
const executorKey = isAsUser ? this.resolveUserId(req) : "global";
|
|
98
101
|
try {
|
|
99
102
|
const result = await plugin.execute(async (signal) => {
|
|
100
103
|
const prepared = await this._prepareQuery(safeBody, indexConfig);
|
|
@@ -102,7 +105,7 @@ var AiSearchPlugin = class extends Plugin {
|
|
|
102
105
|
indexName: indexConfig.indexName,
|
|
103
106
|
...prepared
|
|
104
107
|
}, signal);
|
|
105
|
-
},
|
|
108
|
+
}, this._executeSettings(safeBody, indexConfig, executorKey));
|
|
106
109
|
this._sendResult(res, result, queryType);
|
|
107
110
|
} catch (error) {
|
|
108
111
|
this._handleError(res, error, "Query failed");
|
|
@@ -190,13 +193,16 @@ var AiSearchPlugin = class extends Plugin {
|
|
|
190
193
|
async query(alias, request) {
|
|
191
194
|
const indexConfig = this._resolveIndex(alias);
|
|
192
195
|
if (!indexConfig) throw new Error(`No index configured with alias "${alias}"`);
|
|
193
|
-
const
|
|
194
|
-
const result = await this.execute(async (signal) =>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
const { queryType } = this._resolveQueryParams(request, indexConfig);
|
|
197
|
+
const result = await this.execute(async (signal) => {
|
|
198
|
+
const prepared = await this._prepareQuery(request, indexConfig);
|
|
199
|
+
return this.connector.query(getWorkspaceClient(), {
|
|
200
|
+
indexName: indexConfig.indexName,
|
|
201
|
+
...prepared
|
|
202
|
+
}, signal);
|
|
203
|
+
}, this._executeSettings(request, indexConfig, getCurrentUserId()));
|
|
198
204
|
if (!result.ok) throw new Error(`Vector search query failed for index "${alias}": ${result.message}`);
|
|
199
|
-
return this._parseResponse(result.data,
|
|
205
|
+
return this._parseResponse(result.data, queryType);
|
|
200
206
|
}
|
|
201
207
|
async shutdown() {}
|
|
202
208
|
exports() {
|
|
@@ -235,8 +241,23 @@ var AiSearchPlugin = class extends Plugin {
|
|
|
235
241
|
}
|
|
236
242
|
res.json(this._parseResponse(result.data, queryType));
|
|
237
243
|
}
|
|
238
|
-
|
|
244
|
+
/**
|
|
245
|
+
* Resolve request-vs-index defaults for the result-determining fields.
|
|
246
|
+
* Shared by `_prepareQuery` (the payload) and `_cacheKeyFor` (the key) so
|
|
247
|
+
* the two can't drift.
|
|
248
|
+
*/
|
|
249
|
+
_resolveQueryParams(request, indexConfig) {
|
|
239
250
|
const queryType = request.queryType ?? indexConfig.queryType ?? "hybrid";
|
|
251
|
+
const columns = request.columns ?? indexConfig.columns ?? [];
|
|
252
|
+
return {
|
|
253
|
+
queryType,
|
|
254
|
+
columns,
|
|
255
|
+
numResults: request.numResults ?? indexConfig.numResults ?? 20,
|
|
256
|
+
reranker: this._resolveReranker(request.reranker, indexConfig, columns)
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
async _prepareQuery(request, indexConfig) {
|
|
260
|
+
const { queryType, columns, numResults, reranker } = this._resolveQueryParams(request, indexConfig);
|
|
240
261
|
let queryText = request.queryText;
|
|
241
262
|
let queryVector = request.queryVector;
|
|
242
263
|
if (indexConfig.embeddingFn && queryText && !queryVector && queryType !== "full_text") try {
|
|
@@ -245,17 +266,57 @@ var AiSearchPlugin = class extends Plugin {
|
|
|
245
266
|
} catch (error) {
|
|
246
267
|
throw new Error(`Embedding generation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
247
268
|
}
|
|
248
|
-
const columns = request.columns ?? indexConfig.columns ?? [];
|
|
249
269
|
return {
|
|
250
270
|
queryText,
|
|
251
271
|
queryVector,
|
|
252
272
|
queryType,
|
|
253
273
|
columns,
|
|
254
|
-
numResults
|
|
274
|
+
numResults,
|
|
255
275
|
filters: request.filters,
|
|
256
|
-
reranker
|
|
276
|
+
reranker
|
|
257
277
|
};
|
|
258
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* Cache key for a query: every input that changes the VS result, resolved
|
|
281
|
+
* via `_resolveQueryParams` so the key matches the payload (post-allowlist
|
|
282
|
+
* `columns`, not the raw request). `queryVector` is hashed (vectors are
|
|
283
|
+
* large); the key uses `queryText`, not the derived embedding, since the
|
|
284
|
+
* embedding is a function of `queryText` and hasn't run at key-build time.
|
|
285
|
+
* `columns`/`filters` are order-normalized so equivalent requests share an
|
|
286
|
+
* entry.
|
|
287
|
+
*/
|
|
288
|
+
_cacheKeyFor(request, indexConfig, executorKey) {
|
|
289
|
+
const { queryType, columns, numResults, reranker } = this._resolveQueryParams(request, indexConfig);
|
|
290
|
+
return [
|
|
291
|
+
"ai-search:query",
|
|
292
|
+
indexConfig.indexName,
|
|
293
|
+
request.queryText ?? "",
|
|
294
|
+
request.queryVector ? this._hashVector(request.queryVector) : "",
|
|
295
|
+
queryType,
|
|
296
|
+
numResults,
|
|
297
|
+
JSON.stringify([...columns].sort()),
|
|
298
|
+
this._stableStringify(request.filters ?? null),
|
|
299
|
+
String(!!reranker),
|
|
300
|
+
executorKey
|
|
301
|
+
];
|
|
302
|
+
}
|
|
303
|
+
_hashVector(vector) {
|
|
304
|
+
return createHash("sha256").update(JSON.stringify(vector)).digest("hex");
|
|
305
|
+
}
|
|
306
|
+
/** Execute settings with the per-call cache key folded in. */
|
|
307
|
+
_executeSettings(request, indexConfig, executorKey) {
|
|
308
|
+
return { default: {
|
|
309
|
+
...aiSearchDefaults,
|
|
310
|
+
cache: {
|
|
311
|
+
...aiSearchDefaults.cache,
|
|
312
|
+
cacheKey: this._cacheKeyFor(request, indexConfig, executorKey)
|
|
313
|
+
}
|
|
314
|
+
} };
|
|
315
|
+
}
|
|
316
|
+
/** `JSON.stringify` with object keys sorted recursively; array order kept. */
|
|
317
|
+
_stableStringify(value) {
|
|
318
|
+
return JSON.stringify(value, (_key, val) => val && typeof val === "object" && !Array.isArray(val) ? Object.fromEntries(Object.entries(val).sort(([a], [b]) => a.localeCompare(b))) : val);
|
|
319
|
+
}
|
|
259
320
|
_resolveReranker(requestReranker, indexConfig, columns) {
|
|
260
321
|
if (!(requestReranker ?? indexConfig.reranker)) return void 0;
|
|
261
322
|
if (typeof indexConfig.reranker === "object") return indexConfig.reranker;
|