@databricks/appkit-ui 0.59.0 → 0.61.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/NOTICE.md +11 -10
- package/dist/cli/commands/doctor/bundle.js +3 -11
- package/dist/cli/commands/doctor/bundle.js.map +1 -1
- package/dist/cli/commands/doctor/databricks-client.js +18 -22
- package/dist/cli/commands/doctor/databricks-client.js.map +1 -1
- package/dist/cli/commands/registry/add.js +410 -0
- package/dist/cli/commands/registry/add.js.map +1 -0
- package/dist/cli/commands/registry/client.js +94 -0
- package/dist/cli/commands/registry/client.js.map +1 -0
- package/dist/cli/commands/registry/config-plan.js +110 -0
- package/dist/cli/commands/registry/config-plan.js.map +1 -0
- package/dist/cli/commands/registry/config-writer.js +176 -0
- package/dist/cli/commands/registry/config-writer.js.map +1 -0
- package/dist/cli/commands/registry/constants.js +62 -0
- package/dist/cli/commands/registry/constants.js.map +1 -0
- package/dist/cli/commands/registry/env-reconcile.js +116 -0
- package/dist/cli/commands/registry/env-reconcile.js.map +1 -0
- package/dist/cli/commands/registry/env-writer.js +198 -0
- package/dist/cli/commands/registry/env-writer.js.map +1 -0
- package/dist/cli/commands/registry/index.js +25 -0
- package/dist/cli/commands/registry/index.js.map +1 -0
- package/dist/cli/commands/registry/info.js +45 -0
- package/dist/cli/commands/registry/info.js.map +1 -0
- package/dist/cli/commands/registry/list.js +130 -0
- package/dist/cli/commands/registry/list.js.map +1 -0
- package/dist/cli/commands/registry/requirements.js +92 -0
- package/dist/cli/commands/registry/requirements.js.map +1 -0
- package/dist/cli/commands/registry/server-register.js +124 -0
- package/dist/cli/commands/registry/server-register.js.map +1 -0
- package/dist/cli/commands/registry/workspace-picker.js +353 -0
- package/dist/cli/commands/registry/workspace-picker.js.map +1 -0
- package/dist/cli/deploy-config.js +36 -0
- package/dist/cli/deploy-config.js.map +1 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/schemas/manifest.js +4 -4
- package/dist/schemas/manifest.js.map +1 -1
- package/dist/shared/src/index.d.ts +2 -1
- package/dist/shared/src/workspace-client/errors.d.ts +1 -0
- package/dist/shared/src/workspace-client/factory.d.ts +2 -0
- package/dist/shared/src/workspace-client/index.d.ts +4 -0
- package/dist/shared/src/workspace-client/legacy.d.ts +3 -0
- package/dist/shared/src/workspace-client/types.d.ts +2 -0
- package/dist/workspace-client/client.js +57 -0
- package/dist/workspace-client/client.js.map +1 -0
- package/dist/workspace-client/errors.d.ts +2 -0
- package/dist/workspace-client/errors.js +3 -0
- package/dist/workspace-client/factory.d.ts +19 -0
- package/dist/workspace-client/factory.d.ts.map +1 -0
- package/dist/workspace-client/factory.js +24 -0
- package/dist/workspace-client/factory.js.map +1 -0
- package/dist/workspace-client/index.d.ts +5 -0
- package/dist/workspace-client/index.js +5 -0
- package/dist/workspace-client/legacy.d.ts +34 -0
- package/dist/workspace-client/legacy.d.ts.map +1 -0
- package/dist/workspace-client/legacy.js +23 -0
- package/dist/workspace-client/legacy.js.map +1 -0
- package/dist/workspace-client/types.d.ts +49 -0
- package/dist/workspace-client/types.d.ts.map +1 -0
- package/docs/api/appkit/Function.createWorkspaceClient.md +4 -4
- package/docs/api/appkit/Interface.WorkspaceClientOptions.md +11 -0
- package/package.json +4 -2
- package/sbom.cdx.json +1 -1
package/NOTICE.md
CHANGED
|
@@ -9,19 +9,19 @@ This Software contains code from the following open source projects:
|
|
|
9
9
|
| [@ast-grep/napi](https://www.npmjs.com/package/@ast-grep/napi) | 0.37.0 | MIT | https://ast-grep.github.io |
|
|
10
10
|
| [@clack/prompts](https://www.npmjs.com/package/@clack/prompts) | 1.0.1 | MIT | https://github.com/bombshell-dev/clack/tree/main/packages/prompts#readme |
|
|
11
11
|
| [@opentelemetry/api](https://www.npmjs.com/package/@opentelemetry/api) | 1.9.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/api |
|
|
12
|
-
| [@opentelemetry/api-logs](https://www.npmjs.com/package/@opentelemetry/api-logs) | 0.219.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/api-logs |
|
|
12
|
+
| [@opentelemetry/api-logs](https://www.npmjs.com/package/@opentelemetry/api-logs) | 0.205.0, 0.219.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/api-logs |
|
|
13
13
|
| [@opentelemetry/auto-instrumentations-node](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node) | 0.77.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/auto-instrumentations-node#readme |
|
|
14
|
-
| [@opentelemetry/exporter-logs-otlp-proto](https://www.npmjs.com/package/@opentelemetry/exporter-logs-otlp-proto) | 0.219.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/exporter-logs-otlp-proto |
|
|
15
|
-
| [@opentelemetry/exporter-metrics-otlp-proto](https://www.npmjs.com/package/@opentelemetry/exporter-metrics-otlp-proto) | 0.219.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-metrics-otlp-proto |
|
|
16
|
-
| [@opentelemetry/exporter-trace-otlp-proto](https://www.npmjs.com/package/@opentelemetry/exporter-trace-otlp-proto) | 0.219.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/exporter-trace-otlp-proto |
|
|
17
|
-
| [@opentelemetry/instrumentation](https://www.npmjs.com/package/@opentelemetry/instrumentation) | 0.219.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation |
|
|
14
|
+
| [@opentelemetry/exporter-logs-otlp-proto](https://www.npmjs.com/package/@opentelemetry/exporter-logs-otlp-proto) | 0.205.0, 0.219.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/exporter-logs-otlp-proto |
|
|
15
|
+
| [@opentelemetry/exporter-metrics-otlp-proto](https://www.npmjs.com/package/@opentelemetry/exporter-metrics-otlp-proto) | 0.205.0, 0.219.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-metrics-otlp-proto |
|
|
16
|
+
| [@opentelemetry/exporter-trace-otlp-proto](https://www.npmjs.com/package/@opentelemetry/exporter-trace-otlp-proto) | 0.205.0, 0.219.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/exporter-trace-otlp-proto |
|
|
17
|
+
| [@opentelemetry/instrumentation](https://www.npmjs.com/package/@opentelemetry/instrumentation) | 0.205.0, 0.219.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation |
|
|
18
18
|
| [@opentelemetry/instrumentation-express](https://www.npmjs.com/package/@opentelemetry/instrumentation-express) | 0.67.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-express#readme |
|
|
19
19
|
| [@opentelemetry/instrumentation-http](https://www.npmjs.com/package/@opentelemetry/instrumentation-http) | 0.219.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-http |
|
|
20
|
-
| [@opentelemetry/resources](https://www.npmjs.com/package/@opentelemetry/resources) | 2.8.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-resources |
|
|
21
|
-
| [@opentelemetry/sdk-logs](https://www.npmjs.com/package/@opentelemetry/sdk-logs) | 0.219.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/sdk-logs |
|
|
22
|
-
| [@opentelemetry/sdk-metrics](https://www.npmjs.com/package/@opentelemetry/sdk-metrics) | 2.8.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/sdk-metrics |
|
|
23
|
-
| [@opentelemetry/sdk-node](https://www.npmjs.com/package/@opentelemetry/sdk-node) | 0.219.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-sdk-node |
|
|
24
|
-
| [@opentelemetry/sdk-trace-base](https://www.npmjs.com/package/@opentelemetry/sdk-trace-base) | 2.8.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-sdk-trace-base |
|
|
20
|
+
| [@opentelemetry/resources](https://www.npmjs.com/package/@opentelemetry/resources) | 2.1.0, 2.8.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-resources |
|
|
21
|
+
| [@opentelemetry/sdk-logs](https://www.npmjs.com/package/@opentelemetry/sdk-logs) | 0.205.0, 0.219.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/sdk-logs |
|
|
22
|
+
| [@opentelemetry/sdk-metrics](https://www.npmjs.com/package/@opentelemetry/sdk-metrics) | 2.1.0, 2.8.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/sdk-metrics |
|
|
23
|
+
| [@opentelemetry/sdk-node](https://www.npmjs.com/package/@opentelemetry/sdk-node) | 0.205.0, 0.219.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-sdk-node |
|
|
24
|
+
| [@opentelemetry/sdk-trace-base](https://www.npmjs.com/package/@opentelemetry/sdk-trace-base) | 2.1.0, 2.8.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-sdk-trace-base |
|
|
25
25
|
| [@opentelemetry/semantic-conventions](https://www.npmjs.com/package/@opentelemetry/semantic-conventions) | 1.38.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions |
|
|
26
26
|
| [@radix-ui/react-accordion](https://www.npmjs.com/package/@radix-ui/react-accordion) | 1.2.12 | MIT | https://radix-ui.com/primitives |
|
|
27
27
|
| [@radix-ui/react-alert-dialog](https://www.npmjs.com/package/@radix-ui/react-alert-dialog) | 1.1.15 | MIT | https://radix-ui.com/primitives |
|
|
@@ -70,6 +70,7 @@ This Software contains code from the following open source projects:
|
|
|
70
70
|
| [lucide-react](https://www.npmjs.com/package/lucide-react) | 0.554.0 | ISC | https://lucide.dev |
|
|
71
71
|
| [magic-string](https://www.npmjs.com/package/magic-string) | 0.30.21 | MIT | https://github.com/Rich-Harris/magic-string#readme |
|
|
72
72
|
| [marked](https://www.npmjs.com/package/marked) | 16.4.2, 17.0.3 | MIT | https://marked.js.org |
|
|
73
|
+
| [mlflow-tracing](https://www.npmjs.com/package/mlflow-tracing) | 0.1.3 | Apache-2.0 | https://mlflow.org/ |
|
|
73
74
|
| [next-themes](https://www.npmjs.com/package/next-themes) | 0.4.6 | MIT | https://github.com/pacocoursey/next-themes#readme |
|
|
74
75
|
| [obug](https://www.npmjs.com/package/obug) | 2.1.1 | MIT | https://github.com/sxzz/obug#readme |
|
|
75
76
|
| [pg](https://www.npmjs.com/package/pg) | 8.18.0 | MIT | https://github.com/brianc/node-postgres |
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { APP_YAML_FILE, DATABRICKS_YML_FILE, bindingTypeOf } from "../../deploy-config.js";
|
|
1
2
|
import { errorMessage } from "./utils.js";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
@@ -16,22 +17,13 @@ import yaml from "js-yaml";
|
|
|
16
17
|
*
|
|
17
18
|
* SDK-free; pure YAML parsing. Absent files degrade to empty results.
|
|
18
19
|
*/
|
|
19
|
-
const DEFAULT_BUNDLE_FILE = "databricks.yml";
|
|
20
|
-
const DEFAULT_APP_YAML_FILE = "app.yaml";
|
|
21
20
|
/** A `${resources.<type>.<key>.<field>}` reference — a bundle-created resource. */
|
|
22
21
|
const RESOURCES_REF = /\$\{resources\.([^.]+)\.([^.}]+)\.[^}]+\}/;
|
|
23
|
-
/** The typed sub-key of a binding is its single non-`name` object property. */
|
|
24
|
-
function bindingType(block) {
|
|
25
|
-
for (const [k, v] of Object.entries(block)) {
|
|
26
|
-
if (k === "name") continue;
|
|
27
|
-
if (v && typeof v === "object") return k;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
22
|
/** Classifies a binding by its typed sub-key and origin: scanning its field
|
|
31
23
|
* values for a `${resources.*}` reference (bundle-managed) vs anything else
|
|
32
24
|
* (external). */
|
|
33
25
|
function classifyBinding(block) {
|
|
34
|
-
const type =
|
|
26
|
+
const type = bindingTypeOf(block);
|
|
35
27
|
const typed = type ? block[type] : void 0;
|
|
36
28
|
if (typed && typeof typed === "object") for (const value of Object.values(typed)) {
|
|
37
29
|
if (typeof value !== "string") continue;
|
|
@@ -74,7 +66,7 @@ function readYaml(filePath) {
|
|
|
74
66
|
* empty-but-`present:false` result when no bundle is found, so callers can treat
|
|
75
67
|
* "no bundle" as "everything external" without special-casing.
|
|
76
68
|
*/
|
|
77
|
-
function readBundleInfo(cwd = process.cwd(), bundleFile =
|
|
69
|
+
function readBundleInfo(cwd = process.cwd(), bundleFile = DATABRICKS_YML_FILE, appYamlFile = APP_YAML_FILE) {
|
|
78
70
|
const bindings = /* @__PURE__ */ new Map();
|
|
79
71
|
const envToBinding = /* @__PURE__ */ new Map();
|
|
80
72
|
const declaredResources = /* @__PURE__ */ new Set();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle.js","names":[],"sources":["../../../../src/cli/commands/doctor/bundle.ts"],"sourcesContent":["/**\n * Reads `databricks.yml` + `app.yaml` for two things doctor can't get from\n * `appkit.plugins.json`:\n *\n * 1. **Provenance** — whether each binding references an existing resource\n * (`${var.*}`/literal → `external`) or one this bundle creates\n * (`${resources.<type>.<key>.*}` → `bundle-managed`).\n * 2. **Wiring** — the binding `name` joins the three files: `app.yaml`'s\n * `valueFrom: <name>` must match a `databricks.yml` binding `name`.\n *\n * SDK-free; pure YAML parsing. Absent files degrade to empty results.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport yaml from \"js-yaml\";\nimport
|
|
1
|
+
{"version":3,"file":"bundle.js","names":[],"sources":["../../../../src/cli/commands/doctor/bundle.ts"],"sourcesContent":["/**\n * Reads `databricks.yml` + `app.yaml` for two things doctor can't get from\n * `appkit.plugins.json`:\n *\n * 1. **Provenance** — whether each binding references an existing resource\n * (`${var.*}`/literal → `external`) or one this bundle creates\n * (`${resources.<type>.<key>.*}` → `bundle-managed`).\n * 2. **Wiring** — the binding `name` joins the three files: `app.yaml`'s\n * `valueFrom: <name>` must match a `databricks.yml` binding `name`.\n *\n * SDK-free; pure YAML parsing. Absent files degrade to empty results.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport yaml from \"js-yaml\";\nimport {\n APP_YAML_FILE,\n bindingTypeOf,\n DATABRICKS_YML_FILE,\n} from \"../../deploy-config\";\nimport type { ResourceOrigin } from \"./types\";\nimport { errorMessage } from \"./utils\";\n\n/** A `${resources.<type>.<key>.<field>}` reference — a bundle-created resource. */\nconst RESOURCES_REF = /\\$\\{resources\\.([^.]+)\\.([^.}]+)\\.[^}]+\\}/;\n\n/** One app resource binding from `resources.apps.<app>.resources[]`. */\nexport interface BundleBinding {\n /** The binding name — the join key with `app.yaml` `valueFrom`. */\n name: string;\n /** The typed sub-key that names the kind (`sql_warehouse`, `genie_space`, …). */\n type?: string;\n origin: ResourceOrigin;\n /** For bundle-managed bindings: the `<type>.<key>` it references, if parseable. */\n ref?: { type: string; key: string };\n}\n\nexport interface BundleInfo {\n /** Bindings under every app, keyed by binding name. */\n bindings: Map<string, BundleBinding>;\n /** `app.yaml` env var name → binding name (from `valueFrom`). */\n envToBinding: Map<string, string>;\n /** Declared bundle resource keys, as `\"<type>.<key>\"`, for ref-integrity. */\n declaredResources: Set<string>;\n /** True when a `databricks.yml` was found and parsed. */\n present: boolean;\n}\n\ninterface AppResourceBlock {\n name?: string;\n [k: string]: unknown;\n}\ninterface BundleAppBlock {\n resources?: AppResourceBlock[];\n}\ninterface BundleDoc {\n resources?: {\n apps?: Record<string, BundleAppBlock>;\n [otherType: string]: Record<string, unknown> | undefined;\n };\n}\ninterface AppYamlDoc {\n env?: Array<{ name?: string; valueFrom?: string }>;\n}\n\n/** Classifies a binding by its typed sub-key and origin: scanning its field\n * values for a `${resources.*}` reference (bundle-managed) vs anything else\n * (external). */\nfunction classifyBinding(block: AppResourceBlock): {\n type?: string;\n origin: ResourceOrigin;\n ref?: { type: string; key: string };\n} {\n const type = bindingTypeOf(block);\n const typed = type ? block[type] : undefined;\n if (typed && typeof typed === \"object\") {\n for (const value of Object.values(typed as Record<string, unknown>)) {\n if (typeof value !== \"string\") continue;\n const m = value.match(RESOURCES_REF);\n if (m) {\n return {\n type,\n origin: \"bundle-managed\",\n ref: { type: m[1], key: m[2] },\n };\n }\n }\n }\n return { type, origin: \"external\" };\n}\n\n/**\n * Reads and parses a YAML file. Returns `null` when the file is absent (a\n * legitimate \"no bundle\" state); throws on invalid YAML, since silently\n * ignoring it would let doctor report a false all-clear.\n */\nfunction readYaml<T>(filePath: string): T | null {\n let raw: string;\n try {\n raw = fs.readFileSync(filePath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n try {\n return (yaml.load(raw) ?? {}) as T;\n } catch (err) {\n throw new Error(\n `Failed to parse ${path.basename(filePath)}: ${errorMessage(err)}`,\n );\n }\n}\n\n/**\n * Parses `databricks.yml` (+ `app.yaml`) into {@link BundleInfo}. Returns an\n * empty-but-`present:false` result when no bundle is found, so callers can treat\n * \"no bundle\" as \"everything external\" without special-casing.\n */\nexport function readBundleInfo(\n cwd: string = process.cwd(),\n bundleFile: string = DATABRICKS_YML_FILE,\n appYamlFile: string = APP_YAML_FILE,\n): BundleInfo {\n const bindings = new Map<string, BundleBinding>();\n const envToBinding = new Map<string, string>();\n const declaredResources = new Set<string>();\n\n const doc = readYaml<BundleDoc>(path.resolve(cwd, bundleFile));\n if (!doc) {\n return { bindings, envToBinding, declaredResources, present: false };\n }\n\n for (const [type, group] of Object.entries(doc.resources ?? {})) {\n if (!group || typeof group !== \"object\") continue;\n for (const key of Object.keys(group))\n declaredResources.add(`${type}.${key}`);\n }\n\n for (const app of Object.values(doc.resources?.apps ?? {})) {\n for (const block of app.resources ?? []) {\n if (!block?.name) continue;\n const { type, origin, ref } = classifyBinding(block);\n bindings.set(block.name, {\n name: block.name,\n type,\n origin,\n ref,\n });\n }\n }\n\n const appYaml = readYaml<AppYamlDoc>(path.resolve(cwd, appYamlFile));\n for (const entry of appYaml?.env ?? []) {\n if (entry?.name && entry.valueFrom) {\n envToBinding.set(entry.name, entry.valueFrom);\n }\n }\n\n return { bindings, envToBinding, declaredResources, present: true };\n}\n\n/**\n * Resolves the origin for a target given its env vars, by walking\n * env var → binding name → binding origin. Returns undefined when the bundle\n * has nothing to say (so the target stays external by default).\n */\nexport function originForEnvVars(\n envVars: string[],\n info: BundleInfo,\n): ResourceOrigin | undefined {\n if (!info.present) return undefined;\n for (const env of envVars) {\n const bindingName = info.envToBinding.get(env);\n if (!bindingName) continue;\n const binding = info.bindings.get(bindingName);\n if (binding) return binding.origin;\n }\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyBA,MAAM,gBAAgB;;;;AA4CtB,SAAS,gBAAgB,OAIvB;CACA,MAAM,OAAO,cAAc,MAAM;CACjC,MAAM,QAAQ,OAAO,MAAM,QAAQ;AACnC,KAAI,SAAS,OAAO,UAAU,SAC5B,MAAK,MAAM,SAAS,OAAO,OAAO,MAAiC,EAAE;AACnE,MAAI,OAAO,UAAU,SAAU;EAC/B,MAAM,IAAI,MAAM,MAAM,cAAc;AACpC,MAAI,EACF,QAAO;GACL;GACA,QAAQ;GACR,KAAK;IAAE,MAAM,EAAE;IAAI,KAAK,EAAE;IAAI;GAC/B;;AAIP,QAAO;EAAE;EAAM,QAAQ;EAAY;;;;;;;AAQrC,SAAS,SAAY,UAA4B;CAC/C,IAAI;AACJ,KAAI;AACF,QAAM,GAAG,aAAa,UAAU,QAAQ;UACjC,KAAK;AACZ,MAAK,IAA8B,SAAS,SAAU,QAAO;AAC7D,QAAM;;AAER,KAAI;AACF,SAAQ,KAAK,KAAK,IAAI,IAAI,EAAE;UACrB,KAAK;AACZ,QAAM,IAAI,MACR,mBAAmB,KAAK,SAAS,SAAS,CAAC,IAAI,aAAa,IAAI,GACjE;;;;;;;;AASL,SAAgB,eACd,MAAc,QAAQ,KAAK,EAC3B,aAAqB,qBACrB,cAAsB,eACV;CACZ,MAAM,2BAAW,IAAI,KAA4B;CACjD,MAAM,+BAAe,IAAI,KAAqB;CAC9C,MAAM,oCAAoB,IAAI,KAAa;CAE3C,MAAM,MAAM,SAAoB,KAAK,QAAQ,KAAK,WAAW,CAAC;AAC9D,KAAI,CAAC,IACH,QAAO;EAAE;EAAU;EAAc;EAAmB,SAAS;EAAO;AAGtE,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,IAAI,aAAa,EAAE,CAAC,EAAE;AAC/D,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU;AACzC,OAAK,MAAM,OAAO,OAAO,KAAK,MAAM,CAClC,mBAAkB,IAAI,GAAG,KAAK,GAAG,MAAM;;AAG3C,MAAK,MAAM,OAAO,OAAO,OAAO,IAAI,WAAW,QAAQ,EAAE,CAAC,CACxD,MAAK,MAAM,SAAS,IAAI,aAAa,EAAE,EAAE;AACvC,MAAI,CAAC,OAAO,KAAM;EAClB,MAAM,EAAE,MAAM,QAAQ,QAAQ,gBAAgB,MAAM;AACpD,WAAS,IAAI,MAAM,MAAM;GACvB,MAAM,MAAM;GACZ;GACA;GACA;GACD,CAAC;;CAIN,MAAM,UAAU,SAAqB,KAAK,QAAQ,KAAK,YAAY,CAAC;AACpE,MAAK,MAAM,SAAS,SAAS,OAAO,EAAE,CACpC,KAAI,OAAO,QAAQ,MAAM,UACvB,cAAa,IAAI,MAAM,MAAM,MAAM,UAAU;AAIjD,QAAO;EAAE;EAAU;EAAc;EAAmB,SAAS;EAAM;;;;;;;AAQrE,SAAgB,iBACd,SACA,MAC4B;AAC5B,KAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,MAAK,MAAM,OAAO,SAAS;EACzB,MAAM,cAAc,KAAK,aAAa,IAAI,IAAI;AAC9C,MAAI,CAAC,YAAa;EAClB,MAAM,UAAU,KAAK,SAAS,IAAI,YAAY;AAC9C,MAAI,QAAS,QAAO,QAAQ"}
|
|
@@ -1,38 +1,34 @@
|
|
|
1
|
+
import { loadConfigFile } from "../../../workspace-client/legacy.js";
|
|
2
|
+
import { createWorkspaceClient } from "../../../workspace-client/factory.js";
|
|
3
|
+
|
|
1
4
|
//#region src/cli/commands/doctor/databricks-client.ts
|
|
2
5
|
/**
|
|
3
|
-
* The
|
|
4
|
-
* SDK
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* The seam where `appkit doctor` crosses into the Databricks SDK. It reaches
|
|
7
|
+
* the SDK through the shared `workspace-client` facade — the one sanctioned SDK
|
|
8
|
+
* import site — so no dynamic import or `noRestrictedImports` suppression is
|
|
9
|
+
* needed. (The Lakebase probe still dynamically imports `@databricks/appkit`
|
|
10
|
+
* below, since that's an optional peer `shared` deliberately doesn't depend on.)
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Retained for backward compatibility. Since `shared` now depends on the SDK
|
|
14
|
+
* (reached via the workspace-client facade), the SDK is always resolvable at
|
|
15
|
+
* runtime and this is no longer thrown; kept exported so existing callers and
|
|
16
|
+
* the `SDK_NOT_INSTALLED` diagnostic branch keep compiling.
|
|
13
17
|
*/
|
|
14
|
-
/** Raised when `@databricks/sdk-experimental` is not resolvable at runtime. */
|
|
15
18
|
var SdkNotInstalledError = class extends Error {
|
|
16
19
|
constructor() {
|
|
17
|
-
super("The 'doctor' command requires the Databricks SDK (a dependency of @databricks/appkit).
|
|
20
|
+
super("The 'doctor' command requires the Databricks SDK (a dependency of @databricks/appkit).");
|
|
18
21
|
this.name = "SdkNotInstalledError";
|
|
19
22
|
}
|
|
20
23
|
};
|
|
21
24
|
function isModuleNotFound(err) {
|
|
22
25
|
return err instanceof Error && (err.message.includes("Cannot find module") || err.message.includes("Cannot find package") || err.code === "ERR_MODULE_NOT_FOUND");
|
|
23
26
|
}
|
|
24
|
-
/** Constructs a
|
|
27
|
+
/** Constructs a workspace client via the SDK's unified-auth chain. An explicit
|
|
25
28
|
* `profile` is passed through `Config.profile` rather than mutating
|
|
26
29
|
* `process.env`, so it doesn't leak beyond this call. */
|
|
27
30
|
async function getServiceClient(profile) {
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
sdk = await import("@databricks/sdk-experimental");
|
|
31
|
-
} catch (err) {
|
|
32
|
-
if (isModuleNotFound(err)) throw new SdkNotInstalledError();
|
|
33
|
-
throw err;
|
|
34
|
-
}
|
|
35
|
-
return { client: new sdk.WorkspaceClient(profile ? { profile } : {}) };
|
|
31
|
+
return { client: createWorkspaceClient(profile ? { profile } : {}).toLegacyWorkspaceClient() };
|
|
36
32
|
}
|
|
37
33
|
/**
|
|
38
34
|
* The host a named profile declares in `~/.databrickscfg`, read offline. Doctor
|
|
@@ -43,7 +39,7 @@ async function getServiceClient(profile) {
|
|
|
43
39
|
*/
|
|
44
40
|
async function getProfileHost(profile) {
|
|
45
41
|
try {
|
|
46
|
-
const { iniFile } = await
|
|
42
|
+
const { iniFile } = await loadConfigFile(process.env.DATABRICKS_CONFIG_FILE);
|
|
47
43
|
const host = iniFile?.[profile]?.host;
|
|
48
44
|
return typeof host === "string" && host.length > 0 ? host : void 0;
|
|
49
45
|
} catch {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"databricks-client.js","names":[],"sources":["../../../../src/cli/commands/doctor/databricks-client.ts"],"sourcesContent":["/**\n * The
|
|
1
|
+
{"version":3,"file":"databricks-client.js","names":[],"sources":["../../../../src/cli/commands/doctor/databricks-client.ts"],"sourcesContent":["/**\n * The seam where `appkit doctor` crosses into the Databricks SDK. It reaches\n * the SDK through the shared `workspace-client` facade — the one sanctioned SDK\n * import site — so no dynamic import or `noRestrictedImports` suppression is\n * needed. (The Lakebase probe still dynamically imports `@databricks/appkit`\n * below, since that's an optional peer `shared` deliberately doesn't depend on.)\n */\n\nimport {\n createWorkspaceClient,\n loadConfigFile,\n} from \"../../../workspace-client\";\n\n/**\n * Retained for backward compatibility. Since `shared` now depends on the SDK\n * (reached via the workspace-client facade), the SDK is always resolvable at\n * runtime and this is no longer thrown; kept exported so existing callers and\n * the `SDK_NOT_INSTALLED` diagnostic branch keep compiling.\n */\nexport class SdkNotInstalledError extends Error {\n constructor() {\n super(\n \"The 'doctor' command requires the Databricks SDK (a dependency of @databricks/appkit).\",\n );\n this.name = \"SdkNotInstalledError\";\n }\n}\n\ninterface ServiceClientHandle {\n /** WorkspaceClient, typed as unknown to keep doctor's call sites SDK-agnostic. */\n client: unknown;\n}\n\nfunction isModuleNotFound(err: unknown): boolean {\n return (\n err instanceof Error &&\n (err.message.includes(\"Cannot find module\") ||\n err.message.includes(\"Cannot find package\") ||\n (err as NodeJS.ErrnoException).code === \"ERR_MODULE_NOT_FOUND\")\n );\n}\n\n/** Constructs a workspace client via the SDK's unified-auth chain. An explicit\n * `profile` is passed through `Config.profile` rather than mutating\n * `process.env`, so it doesn't leak beyond this call. */\nexport async function getServiceClient(\n profile?: string,\n): Promise<ServiceClientHandle> {\n const client = createWorkspaceClient(\n profile ? { profile } : {},\n ).toLegacyWorkspaceClient();\n return { client };\n}\n\n/**\n * The host a named profile declares in `~/.databrickscfg`, read offline. Doctor\n * uses it to detect a profile/host conflict, so it must not depend on the\n * resolved config (where `DATABRICKS_HOST` has already won). Returns undefined\n * for any unreadable file, missing profile, or hostless profile — a conflict we\n * can't prove isn't worth reporting.\n */\nexport async function getProfileHost(\n profile: string,\n): Promise<string | undefined> {\n try {\n const { iniFile } = await loadConfigFile(\n process.env.DATABRICKS_CONFIG_FILE,\n );\n const host = iniFile?.[profile]?.host;\n return typeof host === \"string\" && host.length > 0 ? host : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Raised when `@databricks/appkit` (needed for the Lakebase probe) is absent. */\nexport class AppkitNotInstalledError extends Error {\n constructor() {\n super(\n \"The Lakebase connection check requires @databricks/appkit. Please install it to run this check.\",\n );\n this.name = \"AppkitNotInstalledError\";\n }\n}\n\n/** Minimal `pg.Pool`-shaped handle. The caller owns it and must call `end()`. */\nexport interface LakebasePoolHandle {\n query: (sql: string) => Promise<unknown>;\n end: () => Promise<void>;\n}\n\n/** Builds a Lakebase pool via `createLakebasePool`. Connection settings\n * (`PGHOST`, `LAKEBASE_ENDPOINT`, …) come from the environment; we inject only\n * the resolved workspace client for OAuth token minting. */\nexport async function getLakebasePool(\n client: unknown,\n): Promise<LakebasePoolHandle> {\n // A variable specifier stops TS from statically resolving `@databricks/appkit`,\n // an optional peer that `shared` has no dependency on.\n const appkitPkg = \"@databricks/appkit\";\n let appkit: {\n createLakebasePool: (cfg: Record<string, unknown>) => unknown;\n };\n try {\n appkit = await import(appkitPkg);\n } catch (err) {\n if (isModuleNotFound(err)) {\n throw new AppkitNotInstalledError();\n }\n throw err;\n }\n\n // Silence Lakebase's own logger. It defaults to an error-only console logger\n // that dumps the raw SDK ApiError (stack + full response blob) to stderr on a\n // failed token fetch. doctor classifies and prints that failure itself, so the\n // library's dump is just noise on top of our clean one-line report.\n const pool = appkit.createLakebasePool({\n workspaceClient: client,\n logger: { error: false },\n });\n return pool as unknown as LakebasePoolHandle;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAmBA,IAAa,uBAAb,cAA0C,MAAM;CAC9C,cAAc;AACZ,QACE,yFACD;AACD,OAAK,OAAO;;;AAShB,SAAS,iBAAiB,KAAuB;AAC/C,QACE,eAAe,UACd,IAAI,QAAQ,SAAS,qBAAqB,IACzC,IAAI,QAAQ,SAAS,sBAAsB,IAC1C,IAA8B,SAAS;;;;;AAO9C,eAAsB,iBACpB,SAC8B;AAI9B,QAAO,EAAE,QAHM,sBACb,UAAU,EAAE,SAAS,GAAG,EAAE,CAC3B,CAAC,yBAAyB,EACV;;;;;;;;;AAUnB,eAAsB,eACpB,SAC6B;AAC7B,KAAI;EACF,MAAM,EAAE,YAAY,MAAM,eACxB,QAAQ,IAAI,uBACb;EACD,MAAM,OAAO,UAAU,UAAU;AACjC,SAAO,OAAO,SAAS,YAAY,KAAK,SAAS,IAAI,OAAO;SACtD;AACN;;;;AAKJ,IAAa,0BAAb,cAA6C,MAAM;CACjD,cAAc;AACZ,QACE,kGACD;AACD,OAAK,OAAO;;;;;;AAahB,eAAsB,gBACpB,QAC6B;CAG7B,MAAM,YAAY;CAClB,IAAI;AAGJ,KAAI;AACF,WAAS,MAAM,OAAO;UACf,KAAK;AACZ,MAAI,iBAAiB,IAAI,CACvB,OAAM,IAAI,yBAAyB;AAErC,QAAM;;AAWR,QAJa,OAAO,mBAAmB;EACrC,iBAAiB;EACjB,QAAQ,EAAE,OAAO,OAAO;EACzB,CAAC"}
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { JS_IDENTIFIER, REGISTRY_REPO, resolveToken } from "./constants.js";
|
|
2
|
+
import { fetchRegistryItem, fetchVerifiedNames, isValidItemName, stripNamespace } from "./client.js";
|
|
3
|
+
import { extractRequirements, renderRequirements } from "./requirements.js";
|
|
4
|
+
import { buildConfigPlan, collectBindingValueNeeds, planHasContent } from "./config-plan.js";
|
|
5
|
+
import { reportConfigWrite, validateBundle, writeConfig } from "./config-writer.js";
|
|
6
|
+
import { parseEnv } from "./env-reconcile.js";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import process from "node:process";
|
|
11
|
+
import pc from "picocolors";
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
|
|
14
|
+
//#region src/cli/commands/registry/add.ts
|
|
15
|
+
/** Subdirectories that commonly hold the frontend / server in an AppKit app. */
|
|
16
|
+
const FRONTEND_SUBDIRS = [
|
|
17
|
+
"client",
|
|
18
|
+
"frontend",
|
|
19
|
+
"web",
|
|
20
|
+
"app"
|
|
21
|
+
];
|
|
22
|
+
const SERVER_SUBDIRS = [
|
|
23
|
+
"server",
|
|
24
|
+
"api",
|
|
25
|
+
"backend"
|
|
26
|
+
];
|
|
27
|
+
function isDir(p) {
|
|
28
|
+
return fs.existsSync(p) && fs.statSync(p).isDirectory();
|
|
29
|
+
}
|
|
30
|
+
/** A registry item is a server plugin if it ships a manifest.json. */
|
|
31
|
+
function isPluginItem(item) {
|
|
32
|
+
return (item.files ?? []).some((f) => path.basename(f.target ?? f.path) === "manifest.json");
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Locates the frontend root for UI components. AppKit apps put the client in a
|
|
36
|
+
* client/ subdir (with its own components.json + src/); the CLI is typically
|
|
37
|
+
* run from the repo root. Prefer the dir with components.json, then a src/.
|
|
38
|
+
*/
|
|
39
|
+
function findFrontendRoot(cwd) {
|
|
40
|
+
if (fs.existsSync(path.join(cwd, "components.json"))) return cwd;
|
|
41
|
+
for (const sub of FRONTEND_SUBDIRS) if (fs.existsSync(path.join(cwd, sub, "components.json"))) return path.join(cwd, sub);
|
|
42
|
+
if (isDir(path.join(cwd, "src"))) return cwd;
|
|
43
|
+
for (const sub of FRONTEND_SUBDIRS) if (isDir(path.join(cwd, sub, "src"))) return path.join(cwd, sub);
|
|
44
|
+
return cwd;
|
|
45
|
+
}
|
|
46
|
+
/** Locates the server root for plugins (the server/ subdir, else cwd). */
|
|
47
|
+
function findServerRoot(cwd) {
|
|
48
|
+
for (const sub of SERVER_SUBDIRS) if (isDir(path.join(cwd, sub))) return path.join(cwd, sub);
|
|
49
|
+
return cwd;
|
|
50
|
+
}
|
|
51
|
+
/** Nearest dir with a package.json, walking up from start (for dep install). */
|
|
52
|
+
function findNearestPackageJson(start) {
|
|
53
|
+
let dir = start;
|
|
54
|
+
for (;;) {
|
|
55
|
+
if (fs.existsSync(path.join(dir, "package.json"))) return dir;
|
|
56
|
+
const parent = path.dirname(dir);
|
|
57
|
+
if (parent === dir) return start;
|
|
58
|
+
dir = parent;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The Databricks profile the app is configured with, read from
|
|
63
|
+
* `DATABRICKS_CONFIG_PROFILE` in `cwd/.env` via the same dotenv parser the app
|
|
64
|
+
* loads its env with. The CLI's top-level `dotenv/config` only loads the launch
|
|
65
|
+
* dir's `.env` into `process.env`; when `--cwd` points at a different app dir,
|
|
66
|
+
* that file isn't loaded, so the workspace picker and bundle validate would
|
|
67
|
+
* miss the app's profile — this reads it directly. Undefined if the file or key
|
|
68
|
+
* is absent, so the SDK's own default resolution still applies.
|
|
69
|
+
*/
|
|
70
|
+
function profileFromEnv(cwd) {
|
|
71
|
+
try {
|
|
72
|
+
return parseEnv(fs.readFileSync(path.join(cwd, ".env"), "utf-8")).DATABRICKS_CONFIG_PROFILE || void 0;
|
|
73
|
+
} catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolves a registry item's `target` under `base`, enforcing that the result
|
|
79
|
+
* stays inside `base`. Registry items are untrusted remote data; a `target`
|
|
80
|
+
* like `../../../.zshrc` or an absolute path could otherwise write files
|
|
81
|
+
* anywhere on disk (arbitrary-write → RCE). Throws on any escape.
|
|
82
|
+
*/
|
|
83
|
+
function resolveWithinBase(base, target) {
|
|
84
|
+
if (path.isAbsolute(target)) throw new Error(`Refusing absolute file target from registry: ${target}`);
|
|
85
|
+
const baseResolved = path.resolve(base);
|
|
86
|
+
const resolved = path.resolve(baseResolved, target);
|
|
87
|
+
if (resolved !== baseResolved && !resolved.startsWith(baseResolved + path.sep)) throw new Error(`Refusing file target that escapes the destination directory: ${target}`);
|
|
88
|
+
return resolved;
|
|
89
|
+
}
|
|
90
|
+
/** UI file destination (relative to the frontend root): placed in src/ if present. */
|
|
91
|
+
function uiTargetPath(base, file) {
|
|
92
|
+
let target = file.target ?? path.join("components", path.basename(file.path));
|
|
93
|
+
if (!target.startsWith("src/") && isDir(path.join(base, "src"))) target = path.join("src", target);
|
|
94
|
+
return target;
|
|
95
|
+
}
|
|
96
|
+
/** Best-effort: the `toPlugin` export name from the item's index.ts. Returns
|
|
97
|
+
* null (caller falls back to printed instructions) unless the name is a plain
|
|
98
|
+
* JS identifier — the item is untrusted remote content and the value is
|
|
99
|
+
* interpolated into the user's server.ts. */
|
|
100
|
+
function pluginExportName(item) {
|
|
101
|
+
const match = (item.files ?? []).find((f) => path.basename(f.target ?? f.path) === "index.ts")?.content.match(/export\s*\{([^}]*)\}/);
|
|
102
|
+
if (!match) return null;
|
|
103
|
+
const names = match[1].split(",").map((s) => s.trim());
|
|
104
|
+
const chosen = names.find((n) => /^[a-z]/.test(n)) ?? names[0];
|
|
105
|
+
return chosen && JS_IDENTIFIER.test(chosen) ? chosen : null;
|
|
106
|
+
}
|
|
107
|
+
function detectPackageManager(cwd) {
|
|
108
|
+
if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
109
|
+
if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
|
|
110
|
+
if (fs.existsSync(path.join(cwd, "bun.lockb"))) return "bun";
|
|
111
|
+
return "npm";
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A safe npm dependency spec: `[@scope/]name` with an optional `@version`
|
|
115
|
+
* range. Registry `dependencies` are untrusted remote data passed to the
|
|
116
|
+
* package manager, so we reject anything that isn't a plain name+range —
|
|
117
|
+
* blocks tarball/git URL specs (install-script RCE) and `-`-prefixed entries
|
|
118
|
+
* that the PM would parse as flags (argument injection).
|
|
119
|
+
*/
|
|
120
|
+
const SAFE_DEP_SPEC = /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*(@[\w.\-+~^><=|* ]+)?$/i;
|
|
121
|
+
/** Splits deps into safe (installable) and rejected (surfaced to the user). */
|
|
122
|
+
function partitionDeps(deps) {
|
|
123
|
+
const safe = [];
|
|
124
|
+
const rejected = [];
|
|
125
|
+
for (const dep of deps) if (dep.startsWith("-") || !SAFE_DEP_SPEC.test(dep)) rejected.push(dep);
|
|
126
|
+
else safe.push(dep);
|
|
127
|
+
return {
|
|
128
|
+
safe,
|
|
129
|
+
rejected
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function installDependencies(deps, cwd) {
|
|
133
|
+
if (deps.length === 0) return;
|
|
134
|
+
const { safe, rejected } = partitionDeps(deps);
|
|
135
|
+
if (rejected.length > 0) console.warn(pc.yellow(`Skipping suspicious dependenc${rejected.length === 1 ? "y" : "ies"} from the registry (not a plain name@version): ${rejected.join(", ")}. Install manually if you trust them.`));
|
|
136
|
+
if (safe.length === 0) return;
|
|
137
|
+
if (!fs.existsSync(path.join(cwd, "package.json"))) {
|
|
138
|
+
console.warn(pc.yellow(`No package.json found — install these manually: ${safe.join(" ")}`));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const pm = detectPackageManager(cwd);
|
|
142
|
+
const subcommand = pm === "npm" ? "install" : "add";
|
|
143
|
+
console.log(`\nInstalling dependencies with ${pm}: ${safe.join(" ")}`);
|
|
144
|
+
const result = spawnSync(pm, [
|
|
145
|
+
subcommand,
|
|
146
|
+
"--",
|
|
147
|
+
...safe
|
|
148
|
+
], {
|
|
149
|
+
stdio: "inherit",
|
|
150
|
+
cwd
|
|
151
|
+
});
|
|
152
|
+
if (result.status !== 0) console.warn(pc.yellow(`Dependency install exited with code ${result.status ?? "unknown"} — install manually if needed: ${safe.join(" ")}`));
|
|
153
|
+
}
|
|
154
|
+
/** Runs `appkit plugin sync --write` via this same CLI binary. */
|
|
155
|
+
function runPluginSync(cwd) {
|
|
156
|
+
if (spawnSync(process.execPath, [
|
|
157
|
+
process.argv[1],
|
|
158
|
+
"plugin",
|
|
159
|
+
"sync",
|
|
160
|
+
"--write"
|
|
161
|
+
], {
|
|
162
|
+
stdio: "inherit",
|
|
163
|
+
cwd
|
|
164
|
+
}).status !== 0) console.warn(pc.yellow(" Plugin sync did not complete cleanly — run `appkit plugin sync --write` manually."));
|
|
165
|
+
}
|
|
166
|
+
function writeItemFile(base, target, content, force, cwd) {
|
|
167
|
+
const dest = resolveWithinBase(base, target);
|
|
168
|
+
const existed = fs.existsSync(dest);
|
|
169
|
+
if (existed && !force) {
|
|
170
|
+
console.error(pc.red(`Refusing to overwrite ${path.relative(cwd, dest)} — pass --force to replace it.`));
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
174
|
+
fs.writeFileSync(dest, content);
|
|
175
|
+
const label = existed ? pc.yellow("Updated") : pc.green("Created");
|
|
176
|
+
console.log(`${label} ${path.relative(cwd, dest)}`);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Fetches the requested items plus their transitive registryDependencies.
|
|
180
|
+
* Dependencies are resolved breadth-first and de-duplicated by name, so a
|
|
181
|
+
* plugin that depends on another registry item pulls the whole graph in one
|
|
182
|
+
* `add`. Explicitly-requested items keep their request order and come first.
|
|
183
|
+
*/
|
|
184
|
+
async function resolveItems(names, token, fetchItem = fetchRegistryItem) {
|
|
185
|
+
const seen = /* @__PURE__ */ new Set();
|
|
186
|
+
const ordered = [];
|
|
187
|
+
const enqueue = (ref) => {
|
|
188
|
+
const name = stripNamespace(ref);
|
|
189
|
+
if (!isValidItemName(name)) throw new Error(`Invalid registry item name: ${JSON.stringify(ref)}`);
|
|
190
|
+
return name;
|
|
191
|
+
};
|
|
192
|
+
let level = names.map(enqueue).filter((name) => {
|
|
193
|
+
if (seen.has(name)) return false;
|
|
194
|
+
seen.add(name);
|
|
195
|
+
return true;
|
|
196
|
+
});
|
|
197
|
+
while (level.length > 0) {
|
|
198
|
+
const items = await Promise.all(level.map(async (key) => {
|
|
199
|
+
const item = await fetchItem(key, token);
|
|
200
|
+
item.name = key;
|
|
201
|
+
return item;
|
|
202
|
+
}));
|
|
203
|
+
ordered.push(...items);
|
|
204
|
+
const next = [];
|
|
205
|
+
for (const item of items) for (const dep of item.registryDependencies ?? []) {
|
|
206
|
+
const depName = enqueue(dep);
|
|
207
|
+
if (seen.has(depName)) continue;
|
|
208
|
+
seen.add(depName);
|
|
209
|
+
next.push(depName);
|
|
210
|
+
}
|
|
211
|
+
level = next;
|
|
212
|
+
}
|
|
213
|
+
return ordered;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Splits requested names into verified and unverified against the index's
|
|
217
|
+
* verified set. When `verified` is null the index couldn't be read — we can't
|
|
218
|
+
* prove anything is verified, so every name is treated as unverified (the gate
|
|
219
|
+
* then decides whether to warn-and-continue or block). Names are compared with
|
|
220
|
+
* the namespace stripped, matching how items are fetched.
|
|
221
|
+
*/
|
|
222
|
+
function partitionVerified(refs, verified) {
|
|
223
|
+
const ok = [];
|
|
224
|
+
const bad = [];
|
|
225
|
+
for (const ref of refs) {
|
|
226
|
+
const name = stripNamespace(ref);
|
|
227
|
+
if (verified?.has(name)) ok.push(name);
|
|
228
|
+
else bad.push(name);
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
verified: ok,
|
|
232
|
+
unverified: bad
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
async function runAdd(refs, opts) {
|
|
236
|
+
const cwd = opts.cwd ? path.resolve(opts.cwd) : process.cwd();
|
|
237
|
+
const token = resolveToken();
|
|
238
|
+
if (token) console.log(`Using ${token.envName} to fetch from ${REGISTRY_REPO} (private).`);
|
|
239
|
+
const verifiedP = opts.allowUnverified ? Promise.resolve(null) : fetchVerifiedNames(token);
|
|
240
|
+
const items = await resolveItems(refs, token);
|
|
241
|
+
if (!opts.allowUnverified) {
|
|
242
|
+
const verified = await verifiedP;
|
|
243
|
+
const { unverified } = partitionVerified(items.map((i) => i.name), verified);
|
|
244
|
+
if (unverified.length > 0) {
|
|
245
|
+
const reason = verified === null ? "could not read the registry index to verify these items" : `not marked verified in ${REGISTRY_REPO}`;
|
|
246
|
+
console.error(pc.red(`Refusing to add unverified item(s) (${reason}): ${unverified.join(", ")}.`));
|
|
247
|
+
console.error(pc.dim(" Re-run with --allow-unverified if you trust the source; unverified items run code in your app."));
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const hasUi = items.some((i) => !isPluginItem(i));
|
|
252
|
+
const hasPlugin = items.some(isPluginItem);
|
|
253
|
+
const frontendRoot = hasUi ? findFrontendRoot(cwd) : cwd;
|
|
254
|
+
const serverRoot = hasPlugin ? findServerRoot(cwd) : cwd;
|
|
255
|
+
if (hasUi && frontendRoot !== cwd) console.log(pc.dim(`UI components → ${path.relative(cwd, frontendRoot)}/`));
|
|
256
|
+
if (hasPlugin && serverRoot !== cwd) console.log(pc.dim(`Plugins → ${path.relative(cwd, serverRoot)}/`));
|
|
257
|
+
const deps = /* @__PURE__ */ new Set();
|
|
258
|
+
let wroteUi = false;
|
|
259
|
+
const pluginSummaries = [];
|
|
260
|
+
const allRequirements = [];
|
|
261
|
+
for (const item of items) {
|
|
262
|
+
for (const dep of item.dependencies ?? []) deps.add(dep);
|
|
263
|
+
if (isPluginItem(item)) {
|
|
264
|
+
let pluginRel = path.join("plugins", item.name);
|
|
265
|
+
for (const file of item.files ?? []) {
|
|
266
|
+
const target = file.target ?? path.join("plugins", item.name, path.basename(file.path));
|
|
267
|
+
writeItemFile(serverRoot, target, file.content, Boolean(opts.force), cwd);
|
|
268
|
+
if (path.basename(target) === "manifest.json") pluginRel = path.dirname(target);
|
|
269
|
+
}
|
|
270
|
+
const requirements = extractRequirements(item);
|
|
271
|
+
if (requirements.length > 0) {
|
|
272
|
+
console.log(`\n${renderRequirements(item, requirements)}`);
|
|
273
|
+
allRequirements.push(...requirements);
|
|
274
|
+
}
|
|
275
|
+
pluginSummaries.push({
|
|
276
|
+
importPath: `./${pluginRel}`,
|
|
277
|
+
exportName: pluginExportName(item)
|
|
278
|
+
});
|
|
279
|
+
} else for (const file of item.files ?? []) {
|
|
280
|
+
writeItemFile(frontendRoot, uiTargetPath(frontendRoot, file), file.content, Boolean(opts.force), cwd);
|
|
281
|
+
wroteUi = true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
installDependencies([...deps], findNearestPackageJson(cwd));
|
|
285
|
+
if (hasPlugin) {
|
|
286
|
+
console.log(pc.dim("\nRegistering plugins (appkit plugin sync)..."));
|
|
287
|
+
runPluginSync(cwd);
|
|
288
|
+
}
|
|
289
|
+
if (wroteUi) console.log(pc.dim("\nReminder: import \"@databricks/appkit-ui/styles.css\" once at your app root so components are themed."));
|
|
290
|
+
const registerPluginInServer = opts.register !== false && pluginSummaries.some((s) => s.exportName) ? (await import("./server-register.js")).registerPluginInServer : null;
|
|
291
|
+
for (const s of pluginSummaries) {
|
|
292
|
+
let wired = false;
|
|
293
|
+
let skipReason;
|
|
294
|
+
if (opts.register === false) {} else if (!s.exportName) skipReason = "couldn't read a plugin export name from the item's index.ts";
|
|
295
|
+
else if (registerPluginInServer) {
|
|
296
|
+
const result = registerPluginInServer(serverRoot, s.importPath, s.exportName);
|
|
297
|
+
const shown = result.file && path.relative(cwd, path.join(serverRoot, result.file));
|
|
298
|
+
if (result.status === "wired") {
|
|
299
|
+
console.log(`\n${pc.green("Registered")} ${s.exportName} in ${shown}`);
|
|
300
|
+
wired = true;
|
|
301
|
+
} else if (result.status === "already") {
|
|
302
|
+
console.log(pc.dim(`\n${s.exportName} is already registered in ${shown}`));
|
|
303
|
+
wired = true;
|
|
304
|
+
} else skipReason = result.reason;
|
|
305
|
+
}
|
|
306
|
+
if (!wired) {
|
|
307
|
+
const imp = s.exportName ?? "<plugin>";
|
|
308
|
+
if (skipReason) console.log(pc.yellow(`\nCouldn't auto-register ${imp} — ${skipReason}.`));
|
|
309
|
+
console.log(`${pc.bold("Add this to your server's createApp call:")}\n` + pc.dim(` import { ${imp} } from "${s.importPath}";\n const app = await createApp({ plugins: [${imp}(), /* ... */] });`));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (opts.resources !== false && allRequirements.length > 0) {
|
|
313
|
+
const { collectBindingValues, reportEnvResolutions, syncEnv } = await import("./env-writer.js");
|
|
314
|
+
const profile = opts.profile ?? profileFromEnv(cwd);
|
|
315
|
+
console.log(pc.dim("\nReconciling resource env vars into .env..."));
|
|
316
|
+
const resolutions = await syncEnv(allRequirements, {
|
|
317
|
+
cwd,
|
|
318
|
+
nonInteractive: Boolean(opts.yes),
|
|
319
|
+
values: opts.env,
|
|
320
|
+
profile
|
|
321
|
+
});
|
|
322
|
+
reportEnvResolutions(resolutions);
|
|
323
|
+
const values = { ...opts.env ?? {} };
|
|
324
|
+
for (const r of resolutions) if (r.value !== void 0) values[r.env] = r.value;
|
|
325
|
+
const bindingNeeds = collectBindingValueNeeds(allRequirements);
|
|
326
|
+
if (bindingNeeds.length > 0) {
|
|
327
|
+
const bindingValues = await collectBindingValues(bindingNeeds, {
|
|
328
|
+
cwd,
|
|
329
|
+
nonInteractive: Boolean(opts.yes),
|
|
330
|
+
values: opts.env,
|
|
331
|
+
profile
|
|
332
|
+
});
|
|
333
|
+
Object.assign(values, bindingValues);
|
|
334
|
+
}
|
|
335
|
+
const plan = buildConfigPlan(allRequirements, values);
|
|
336
|
+
if (planHasContent(plan)) {
|
|
337
|
+
const result = writeConfig(cwd, plan);
|
|
338
|
+
reportConfigWrite(result);
|
|
339
|
+
if (result.databricksYmlChanged) validateBundle(cwd, profile);
|
|
340
|
+
}
|
|
341
|
+
warnScopeNeeding(allRequirements);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* v1 does not write `user_api_scopes` (deferred to the manifest scope
|
|
346
|
+
* extension). Warn when an added plugin's resource type is known to need one,
|
|
347
|
+
* so the user adds it before deploy.
|
|
348
|
+
*/
|
|
349
|
+
/** Resource types known to require a user_api_scope, and the scope each needs. */
|
|
350
|
+
const SCOPE_BY_RESOURCE_TYPE = {
|
|
351
|
+
genie_space: "dashboards.genie",
|
|
352
|
+
serving_endpoint: "serving.serving-endpoints",
|
|
353
|
+
volume: "files.files"
|
|
354
|
+
};
|
|
355
|
+
/** Returns the user_api_scopes implied by a set of resource rows (deduped). */
|
|
356
|
+
function scopesForResources(rows) {
|
|
357
|
+
const needed = /* @__PURE__ */ new Map();
|
|
358
|
+
for (const row of rows) {
|
|
359
|
+
const scope = SCOPE_BY_RESOURCE_TYPE[row.type];
|
|
360
|
+
if (scope) needed.set(row.type, scope);
|
|
361
|
+
}
|
|
362
|
+
return needed;
|
|
363
|
+
}
|
|
364
|
+
function warnScopeNeeding(rows) {
|
|
365
|
+
const needed = scopesForResources(rows);
|
|
366
|
+
if (needed.size === 0) return;
|
|
367
|
+
const list = [...needed.entries()].map(([type, scope]) => `${type} → ${scope}`).join(", ");
|
|
368
|
+
console.warn(pc.yellow(`\n Note: these resources may need a user_api_scope before deploy: ${list}.\n Add it under resources.apps.app.user_api_scopes in databricks.yml.`));
|
|
369
|
+
}
|
|
370
|
+
/** Commander reducer for repeatable `--env KEY=VALUE` flags. */
|
|
371
|
+
function collectEnvFlag(raw, acc) {
|
|
372
|
+
const eq = raw.indexOf("=");
|
|
373
|
+
if (eq === -1) {
|
|
374
|
+
console.error(`Ignoring --env "${raw}" (expected KEY=VALUE).`);
|
|
375
|
+
return acc;
|
|
376
|
+
}
|
|
377
|
+
const key = raw.slice(0, eq).trim();
|
|
378
|
+
const value = raw.slice(eq + 1);
|
|
379
|
+
if (key) acc[key] = value;
|
|
380
|
+
return acc;
|
|
381
|
+
}
|
|
382
|
+
const addCommand = new Command("add").description("Add a UI component or server plugin from the AppKit registry").argument("<item...>", "Registry item name(s), e.g. metric-card or hello").option("-f, --force", "Overwrite existing files").option("-C, --cwd <dir>", "Run as if started in <dir>").option("--no-register", "Don't edit the server entry to register plugins").option("--no-resources", "Don't reconcile resource env vars into .env").option("-y, --yes", "Don't prompt; use --env values or leave vars unset").option("--env <KEY=VALUE>", "Pre-set a resource env var (repeatable)", collectEnvFlag, {}).option("-p, --profile <name>", "Databricks profile for the resource picker and bundle validate (defaults to the app's DATABRICKS_CONFIG_PROFILE)").option("--allow-unverified", "Add items the registry doesn't mark verified (runs untrusted code)").addHelpText("after", `
|
|
383
|
+
No components.json is required. Item type is detected automatically:
|
|
384
|
+
• UI components → <frontend>/src/components/appkit/ (client/ detected)
|
|
385
|
+
• Server plugins → <server>/plugins/<name>/, runs plugin sync, and registers
|
|
386
|
+
them in your createApp call (use --no-register to skip the server edit)
|
|
387
|
+
|
|
388
|
+
Server plugins declare Databricks resources. On add, their env vars are
|
|
389
|
+
reconciled into .env (and names into .env.example), and the deploy config
|
|
390
|
+
(app.yaml + databricks.yml resource bindings) is patched to match — existing
|
|
391
|
+
entries are never clobbered. Interactive by default; pass --yes for agents/CI
|
|
392
|
+
(uses --env values, leaves the rest unset) and --env KEY=VALUE to supply
|
|
393
|
+
values non-interactively. Pass --profile to validate the bundle after writing.
|
|
394
|
+
|
|
395
|
+
The frontend/server roots are detected from common layouts, so you can run
|
|
396
|
+
this from the repo root. While the registry repo is private, a read token is
|
|
397
|
+
resolved from \`gh auth token\` or APPKIT_REGISTRY_TOKEN / GITHUB_TOKEN / GH_TOKEN.
|
|
398
|
+
|
|
399
|
+
Examples:
|
|
400
|
+
$ appkit add metric-card # UI component
|
|
401
|
+
$ appkit add hello # server plugin
|
|
402
|
+
$ appkit add metric-card hello # mix in one call
|
|
403
|
+
$ appkit add analytics --yes --env DATABRICKS_WAREHOUSE_ID=abc123`).action((items, opts) => runAdd(items, opts).catch((err) => {
|
|
404
|
+
console.error(err);
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}));
|
|
407
|
+
|
|
408
|
+
//#endregion
|
|
409
|
+
export { addCommand };
|
|
410
|
+
//# sourceMappingURL=add.js.map
|