@databricks/appkit 0.55.1 → 0.57.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/commands/lint.js +67 -8
- package/dist/cli/commands/lint.js.map +1 -1
- 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/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":"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"}
|
|
@@ -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"}
|
|
@@ -4,6 +4,55 @@ import { Command } from "commander";
|
|
|
4
4
|
import { Lang, parse } from "@ast-grep/napi";
|
|
5
5
|
|
|
6
6
|
//#region src/cli/commands/lint.ts
|
|
7
|
+
/** Value node of an object literal's `key: value` pair, or undefined. */
|
|
8
|
+
function objectPropertyValue(obj, key) {
|
|
9
|
+
for (const pair of obj.children()) {
|
|
10
|
+
if (pair.kind() !== "pair") continue;
|
|
11
|
+
const k = pair.field("key");
|
|
12
|
+
if (k && k.text().replace(/^["']|["']$/g, "") === key) return pair.field("value") ?? void 0;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** True for `[]` / `[ ]` — an array literal with no elements. */
|
|
16
|
+
function isEmptyArrayLiteral(node) {
|
|
17
|
+
return node.kind() === "array" && node.children().every((c) => !c.isNamed());
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Flags `aiSearch(...)` configs whose indexes lack usable `columns`. Production
|
|
21
|
+
* does not auto-discover columns (dev-only), so a missing or empty `columns`
|
|
22
|
+
* fails at query time. Passes anything it can't statically prove bad (constant
|
|
23
|
+
* or dynamically-built columns/indexes) to avoid false positives.
|
|
24
|
+
*/
|
|
25
|
+
function findAiSearchIndexesMissingColumns(root) {
|
|
26
|
+
const flagged = [];
|
|
27
|
+
for (const call of root.findAll("aiSearch($$$ARGS)")) {
|
|
28
|
+
const argNodes = call.field("arguments")?.children().filter((c) => c.isNamed()) ?? [];
|
|
29
|
+
if (argNodes.length === 0) {
|
|
30
|
+
flagged.push(call);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const configObj = argNodes[0].kind() === "object" ? argNodes[0] : void 0;
|
|
34
|
+
if (!configObj) continue;
|
|
35
|
+
const indexes = objectPropertyValue(configObj, "indexes");
|
|
36
|
+
if (!indexes) {
|
|
37
|
+
flagged.push(call);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (indexes.kind() !== "object") continue;
|
|
41
|
+
const indexPairs = indexes.children().filter((c) => c.kind() === "pair");
|
|
42
|
+
if (indexPairs.length === 0) {
|
|
43
|
+
flagged.push(call);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
for (const pair of indexPairs) {
|
|
47
|
+
const idxObj = pair.field("value");
|
|
48
|
+
if (!idxObj || idxObj.kind() !== "object") continue;
|
|
49
|
+
if (idxObj.children().some((c) => c.kind() === "spread_element")) continue;
|
|
50
|
+
const columns = objectPropertyValue(idxObj, "columns");
|
|
51
|
+
if (!columns || isEmptyArrayLiteral(columns)) flagged.push(pair);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return flagged;
|
|
55
|
+
}
|
|
7
56
|
const rules = [
|
|
8
57
|
{
|
|
9
58
|
id: "no-double-type-assertion",
|
|
@@ -32,6 +81,12 @@ const rules = [
|
|
|
32
81
|
pattern: "<Variants $$$P>$$$C</Variants>",
|
|
33
82
|
message: "<Variants> is a development-only variant picker and must not be shipped. Finalize the chosen <Variant> before deploying.",
|
|
34
83
|
includeTests: false
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "ai-search-index-requires-columns",
|
|
87
|
+
message: "AI Search index has no `columns`; it will fail in production (columns are auto-discovered only in dev). Set `columns` explicitly on each index.",
|
|
88
|
+
includeTests: false,
|
|
89
|
+
find: findAiSearchIndexesMissingColumns
|
|
35
90
|
}
|
|
36
91
|
];
|
|
37
92
|
function isTestFile(filePath, rootDir) {
|
|
@@ -54,15 +109,16 @@ function findTsFiles(dir, files = []) {
|
|
|
54
109
|
}
|
|
55
110
|
return files;
|
|
56
111
|
}
|
|
57
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Runs all applicable rules against a file's source. Exported so tests can lint
|
|
114
|
+
* an in-memory string without touching disk.
|
|
115
|
+
*/
|
|
116
|
+
function lintSource(content, filePath, activeRules = rules, isTest = false) {
|
|
58
117
|
const violations = [];
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
for (const rule of rules) {
|
|
64
|
-
if (testFile && rule.includeTests === false) continue;
|
|
65
|
-
const matches = root.findAll(rule.pattern);
|
|
118
|
+
const root = parse(filePath.endsWith(".tsx") ? Lang.Tsx : Lang.TypeScript, content).root();
|
|
119
|
+
for (const rule of activeRules) {
|
|
120
|
+
if (isTest && rule.includeTests === false) continue;
|
|
121
|
+
const matches = rule.find ? rule.find(root) : root.findAll(rule.pattern);
|
|
66
122
|
for (const match of matches) {
|
|
67
123
|
const code = match.text();
|
|
68
124
|
if (rule.filter && !rule.filter(code)) continue;
|
|
@@ -79,6 +135,9 @@ function lintFile(filePath, rules, rootDir) {
|
|
|
79
135
|
}
|
|
80
136
|
return violations;
|
|
81
137
|
}
|
|
138
|
+
function lintFile(filePath, activeRules, rootDir) {
|
|
139
|
+
return lintSource(fs.readFileSync(filePath, "utf-8"), filePath, activeRules, isTestFile(filePath, rootDir));
|
|
140
|
+
}
|
|
82
141
|
/**
|
|
83
142
|
* Lint command implementation
|
|
84
143
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lint.js","names":[],"sources":["../../../src/cli/commands/lint.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Lang, parse } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\n\ninterface Rule {\n id: string;\n pattern: string;\n message: string;\n includeTests?: boolean;\n filter?: (code: string) => boolean;\n}\n\nconst rules: Rule[] = [\n {\n id: \"no-double-type-assertion\",\n pattern: \"$X as unknown as $Y\",\n message:\n \"Avoid double type assertion (as unknown as). Use proper type guards or fix the source type.\",\n },\n {\n id: \"no-as-any\",\n pattern: \"$X as any\",\n message:\n 'Avoid \"as any\" type assertion. Use proper typing or unknown with type guards.',\n includeTests: false, // acceptable in test mocks\n },\n {\n id: \"no-array-index-key\",\n pattern: \"key={$IDX}\",\n message:\n \"Avoid using array index as React key. Use a stable unique identifier.\",\n filter: (code) => /key=\\{(idx|index|i)\\}/.test(code),\n },\n {\n id: \"no-parse-float-without-validation\",\n pattern: \"parseFloat($X).toFixed($Y)\",\n message:\n \"parseFloat can return NaN. Validate input or use toNumber() helper from shared/types.ts.\",\n },\n {\n // <Variants> is the dev-only variant picker from @databricks/appkit-ui,\n // meant to drive the local edit loop. Finalize the chosen variant before\n // deploying so the picker chrome never reaches production.\n id: \"no-variants-in-prod\",\n pattern: \"<Variants $$$P>$$$C</Variants>\",\n message:\n \"<Variants> is a development-only variant picker and must not be shipped. Finalize the chosen <Variant> before deploying.\",\n includeTests: false,\n },\n];\n\nfunction isTestFile(filePath: string, rootDir: string): boolean {\n // Relative to scan root: an ancestor `tests` dir must not mark the whole project as tests.\n const rel = path.relative(rootDir, filePath);\n return (\n /\\.(test|spec)\\.(ts|tsx)$/.test(rel) || /(^|[/\\\\])tests[/\\\\]/.test(rel)\n );\n}\n\nfunction findTsFiles(dir: string, files: string[] = []): string[] {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n if ([\"node_modules\", \"dist\", \"build\", \".git\"].includes(entry.name))\n continue;\n findTsFiles(fullPath, files);\n } else if (entry.isFile() && /\\.(ts|tsx)$/.test(entry.name)) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\ninterface Violation {\n file: string;\n line: number;\n column: number;\n rule: string;\n message: string;\n code: string;\n}\n\nfunction lintFile(\n filePath: string,\n rules: Rule[],\n rootDir: string,\n): Violation[] {\n const violations: Violation[] = [];\n const content = fs.readFileSync(filePath, \"utf-8\");\n const lang = filePath.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const testFile = isTestFile(filePath, rootDir);\n\n const ast = parse(lang, content);\n const root = ast.root();\n\n for (const rule of rules) {\n // skip rules that don't apply to test files\n if (testFile && rule.includeTests === false) continue;\n\n const matches = root.findAll(rule.pattern);\n\n for (const match of matches) {\n const code = match.text();\n\n if (rule.filter && !rule.filter(code)) continue;\n\n const range = match.range();\n violations.push({\n file: filePath,\n line: range.start.line + 1,\n column: range.start.column + 1,\n rule: rule.id,\n message: rule.message,\n code: code.length > 80 ? `${code.slice(0, 77)}...` : code,\n });\n }\n }\n\n return violations;\n}\n\n/**\n * Lint command implementation\n */\nfunction runLint() {\n const rootDir = process.cwd();\n const files = findTsFiles(rootDir);\n\n console.log(`Scanning ${files.length} TypeScript files...\\n`);\n\n const allViolations: Violation[] = [];\n\n for (const file of files) {\n const violations = lintFile(file, rules, rootDir);\n allViolations.push(...violations);\n }\n\n if (allViolations.length === 0) {\n console.log(\"No ast-grep lint violations found.\");\n process.exit(0);\n }\n\n console.log(`Found ${allViolations.length} violation(s):\\n`);\n\n for (const v of allViolations) {\n const relPath = path.relative(rootDir, v.file);\n console.log(`${relPath}:${v.line}:${v.column}`);\n console.log(` ${v.rule}: ${v.message}`);\n console.log(` > ${v.code}\\n`);\n }\n\n process.exit(1);\n}\n\nexport const lintCommand = new Command(\"lint\")\n .description(\"Run AST-based linting on TypeScript files\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit lint`,\n )\n .action(runLint);\n"],"mappings":";;;;;;AAaA,MAAM,QAAgB;CACpB;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACH;CACD;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACF,cAAc;EACf;CACD;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACF,SAAS,SAAS,wBAAwB,KAAK,KAAK;EACrD;CACD;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACH;CACD;EAIE,IAAI;EACJ,SAAS;EACT,SACE;EACF,cAAc;EACf;CACF;AAED,SAAS,WAAW,UAAkB,SAA0B;CAE9D,MAAM,MAAM,KAAK,SAAS,SAAS,SAAS;AAC5C,QACE,2BAA2B,KAAK,IAAI,IAAI,sBAAsB,KAAK,IAAI;;AAI3E,SAAS,YAAY,KAAa,QAAkB,EAAE,EAAY;CAChE,MAAM,UAAU,GAAG,YAAY,KAAK,EAAE,eAAe,MAAM,CAAC;AAE5D,MAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,WAAW,KAAK,KAAK,KAAK,MAAM,KAAK;AAE3C,MAAI,MAAM,aAAa,EAAE;AACvB,OAAI;IAAC;IAAgB;IAAQ;IAAS;IAAO,CAAC,SAAS,MAAM,KAAK,CAChE;AACF,eAAY,UAAU,MAAM;aACnB,MAAM,QAAQ,IAAI,cAAc,KAAK,MAAM,KAAK,CACzD,OAAM,KAAK,SAAS;;AAIxB,QAAO;;AAYT,SAAS,SACP,UACA,OACA,SACa;CACb,MAAM,aAA0B,EAAE;CAClC,MAAM,UAAU,GAAG,aAAa,UAAU,QAAQ;CAClD,MAAM,OAAO,SAAS,SAAS,OAAO,GAAG,KAAK,MAAM,KAAK;CACzD,MAAM,WAAW,WAAW,UAAU,QAAQ;CAG9C,MAAM,OADM,MAAM,MAAM,QAAQ,CACf,MAAM;AAEvB,MAAK,MAAM,QAAQ,OAAO;AAExB,MAAI,YAAY,KAAK,iBAAiB,MAAO;EAE7C,MAAM,UAAU,KAAK,QAAQ,KAAK,QAAQ;AAE1C,OAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,OAAO,MAAM,MAAM;AAEzB,OAAI,KAAK,UAAU,CAAC,KAAK,OAAO,KAAK,CAAE;GAEvC,MAAM,QAAQ,MAAM,OAAO;AAC3B,cAAW,KAAK;IACd,MAAM;IACN,MAAM,MAAM,MAAM,OAAO;IACzB,QAAQ,MAAM,MAAM,SAAS;IAC7B,MAAM,KAAK;IACX,SAAS,KAAK;IACd,MAAM,KAAK,SAAS,KAAK,GAAG,KAAK,MAAM,GAAG,GAAG,CAAC,OAAO;IACtD,CAAC;;;AAIN,QAAO;;;;;AAMT,SAAS,UAAU;CACjB,MAAM,UAAU,QAAQ,KAAK;CAC7B,MAAM,QAAQ,YAAY,QAAQ;AAElC,SAAQ,IAAI,YAAY,MAAM,OAAO,wBAAwB;CAE7D,MAAM,gBAA6B,EAAE;AAErC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,aAAa,SAAS,MAAM,OAAO,QAAQ;AACjD,gBAAc,KAAK,GAAG,WAAW;;AAGnC,KAAI,cAAc,WAAW,GAAG;AAC9B,UAAQ,IAAI,qCAAqC;AACjD,UAAQ,KAAK,EAAE;;AAGjB,SAAQ,IAAI,SAAS,cAAc,OAAO,kBAAkB;AAE5D,MAAK,MAAM,KAAK,eAAe;EAC7B,MAAM,UAAU,KAAK,SAAS,SAAS,EAAE,KAAK;AAC9C,UAAQ,IAAI,GAAG,QAAQ,GAAG,EAAE,KAAK,GAAG,EAAE,SAAS;AAC/C,UAAQ,IAAI,KAAK,EAAE,KAAK,IAAI,EAAE,UAAU;AACxC,UAAQ,IAAI,OAAO,EAAE,KAAK,IAAI;;AAGhC,SAAQ,KAAK,EAAE;;AAGjB,MAAa,cAAc,IAAI,QAAQ,OAAO,CAC3C,YAAY,4CAA4C,CACxD,YACC,SACA;;iBAGD,CACA,OAAO,QAAQ"}
|
|
1
|
+
{"version":3,"file":"lint.js","names":[],"sources":["../../../src/cli/commands/lint.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Lang, parse, type SgNode } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\n\ninterface BaseRule {\n id: string;\n message: string;\n includeTests?: boolean;\n filter?: (code: string) => boolean;\n}\n\n/**\n * A rule matches via exactly one of `pattern` (an ast-grep pattern for\n * `root.findAll`) or `find` (an escape hatch for checks a single pattern can't\n * express). Discriminated union so a rule with neither or both fails to compile.\n */\ntype Rule =\n | (BaseRule & { pattern: string; find?: never })\n | (BaseRule & { find: (root: SgNode) => SgNode[]; pattern?: never });\n\n/** Value node of an object literal's `key: value` pair, or undefined. */\nfunction objectPropertyValue(obj: SgNode, key: string): SgNode | undefined {\n for (const pair of obj.children()) {\n if (pair.kind() !== \"pair\") continue;\n const k = pair.field(\"key\");\n // Strip quotes so `columns` and `\"columns\"` are treated the same.\n if (k && k.text().replace(/^[\"']|[\"']$/g, \"\") === key) {\n return pair.field(\"value\") ?? undefined;\n }\n }\n return undefined;\n}\n\n/** True for `[]` / `[ ]` — an array literal with no elements. */\nfunction isEmptyArrayLiteral(node: SgNode): boolean {\n return node.kind() === \"array\" && node.children().every((c) => !c.isNamed());\n}\n\n/**\n * Flags `aiSearch(...)` configs whose indexes lack usable `columns`. Production\n * does not auto-discover columns (dev-only), so a missing or empty `columns`\n * fails at query time. Passes anything it can't statically prove bad (constant\n * or dynamically-built columns/indexes) to avoid false positives.\n */\nfunction findAiSearchIndexesMissingColumns(root: SgNode): SgNode[] {\n const flagged: SgNode[] = [];\n\n for (const call of root.findAll(\"aiSearch($$$ARGS)\")) {\n const argNodes =\n call\n .field(\"arguments\")\n ?.children()\n .filter((c) => c.isNamed()) ?? [];\n\n // Bare aiSearch(): relies on the env default index (no columns).\n if (argNodes.length === 0) {\n flagged.push(call);\n continue;\n }\n\n // aiSearch(dynamicConfig) — not an object literal, can't inspect. Pass.\n const configObj = argNodes[0].kind() === \"object\" ? argNodes[0] : undefined;\n if (!configObj) continue;\n\n const indexes = objectPropertyValue(configObj, \"indexes\");\n\n // No `indexes` key => falls back to the env default index (no columns).\n if (!indexes) {\n flagged.push(call);\n continue;\n }\n if (indexes.kind() !== \"object\") continue; // dynamic indexes: pass.\n\n const indexPairs = indexes.children().filter((c) => c.kind() === \"pair\");\n\n // Empty `indexes: {}` => no configured index, same as bare.\n if (indexPairs.length === 0) {\n flagged.push(call);\n continue;\n }\n\n for (const pair of indexPairs) {\n const idxObj = pair.field(\"value\");\n if (!idxObj || idxObj.kind() !== \"object\") continue; // dynamic: pass.\n // A spread (`{ ...base }`) may carry `columns` we can't see: pass.\n if (idxObj.children().some((c) => c.kind() === \"spread_element\"))\n continue;\n const columns = objectPropertyValue(idxObj, \"columns\");\n if (!columns || isEmptyArrayLiteral(columns)) {\n flagged.push(pair); // points at the offending index alias.\n }\n }\n }\n\n return flagged;\n}\n\nconst rules: Rule[] = [\n {\n id: \"no-double-type-assertion\",\n pattern: \"$X as unknown as $Y\",\n message:\n \"Avoid double type assertion (as unknown as). Use proper type guards or fix the source type.\",\n },\n {\n id: \"no-as-any\",\n pattern: \"$X as any\",\n message:\n 'Avoid \"as any\" type assertion. Use proper typing or unknown with type guards.',\n includeTests: false, // acceptable in test mocks\n },\n {\n id: \"no-array-index-key\",\n pattern: \"key={$IDX}\",\n message:\n \"Avoid using array index as React key. Use a stable unique identifier.\",\n filter: (code) => /key=\\{(idx|index|i)\\}/.test(code),\n },\n {\n id: \"no-parse-float-without-validation\",\n pattern: \"parseFloat($X).toFixed($Y)\",\n message:\n \"parseFloat can return NaN. Validate input or use toNumber() helper from shared/types.ts.\",\n },\n {\n // <Variants> is the dev-only variant picker from @databricks/appkit-ui,\n // meant to drive the local edit loop. Finalize the chosen variant before\n // deploying so the picker chrome never reaches production.\n id: \"no-variants-in-prod\",\n pattern: \"<Variants $$$P>$$$C</Variants>\",\n message:\n \"<Variants> is a development-only variant picker and must not be shipped. Finalize the chosen <Variant> before deploying.\",\n includeTests: false,\n },\n {\n id: \"ai-search-index-requires-columns\",\n message:\n \"AI Search index has no `columns`; it will fail in production (columns are auto-discovered only in dev). Set `columns` explicitly on each index.\",\n includeTests: false,\n find: findAiSearchIndexesMissingColumns,\n },\n];\n\nfunction isTestFile(filePath: string, rootDir: string): boolean {\n // Relative to scan root: an ancestor `tests` dir must not mark the whole project as tests.\n const rel = path.relative(rootDir, filePath);\n return (\n /\\.(test|spec)\\.(ts|tsx)$/.test(rel) || /(^|[/\\\\])tests[/\\\\]/.test(rel)\n );\n}\n\nfunction findTsFiles(dir: string, files: string[] = []): string[] {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n if ([\"node_modules\", \"dist\", \"build\", \".git\"].includes(entry.name))\n continue;\n findTsFiles(fullPath, files);\n } else if (entry.isFile() && /\\.(ts|tsx)$/.test(entry.name)) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\ninterface Violation {\n file: string;\n line: number;\n column: number;\n rule: string;\n message: string;\n code: string;\n}\n\n/**\n * Runs all applicable rules against a file's source. Exported so tests can lint\n * an in-memory string without touching disk.\n */\nexport function lintSource(\n content: string,\n filePath: string,\n activeRules: Rule[] = rules,\n isTest = false,\n): Violation[] {\n const violations: Violation[] = [];\n const lang = filePath.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const root = parse(lang, content).root();\n\n for (const rule of activeRules) {\n // skip rules that don't apply to test files\n if (isTest && rule.includeTests === false) continue;\n\n const matches = rule.find ? rule.find(root) : root.findAll(rule.pattern);\n\n for (const match of matches) {\n const code = match.text();\n\n if (rule.filter && !rule.filter(code)) continue;\n\n const range = match.range();\n violations.push({\n file: filePath,\n line: range.start.line + 1,\n column: range.start.column + 1,\n rule: rule.id,\n message: rule.message,\n code: code.length > 80 ? `${code.slice(0, 77)}...` : code,\n });\n }\n }\n\n return violations;\n}\n\nfunction lintFile(\n filePath: string,\n activeRules: Rule[],\n rootDir: string,\n): Violation[] {\n const content = fs.readFileSync(filePath, \"utf-8\");\n return lintSource(\n content,\n filePath,\n activeRules,\n isTestFile(filePath, rootDir),\n );\n}\n\n/**\n * Lint command implementation\n */\nfunction runLint() {\n const rootDir = process.cwd();\n const files = findTsFiles(rootDir);\n\n console.log(`Scanning ${files.length} TypeScript files...\\n`);\n\n const allViolations: Violation[] = [];\n\n for (const file of files) {\n const violations = lintFile(file, rules, rootDir);\n allViolations.push(...violations);\n }\n\n if (allViolations.length === 0) {\n console.log(\"No ast-grep lint violations found.\");\n process.exit(0);\n }\n\n console.log(`Found ${allViolations.length} violation(s):\\n`);\n\n for (const v of allViolations) {\n const relPath = path.relative(rootDir, v.file);\n console.log(`${relPath}:${v.line}:${v.column}`);\n console.log(` ${v.rule}: ${v.message}`);\n console.log(` > ${v.code}\\n`);\n }\n\n process.exit(1);\n}\n\nexport const lintCommand = new Command(\"lint\")\n .description(\"Run AST-based linting on TypeScript files\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit lint`,\n )\n .action(runLint);\n"],"mappings":"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"}
|
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"}
|