@databricks/appkit 0.60.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.
Files changed (109) hide show
  1. package/NOTICE.md +11 -10
  2. package/dist/agents/databricks.js +1 -2
  3. package/dist/agents/databricks.js.map +1 -1
  4. package/dist/agents/supervisor-api.js +1 -2
  5. package/dist/agents/supervisor-api.js.map +1 -1
  6. package/dist/appkit/package.js +1 -1
  7. package/dist/cache/index.js +2 -3
  8. package/dist/cache/index.js.map +1 -1
  9. package/dist/cli/commands/doctor/bundle.js +3 -11
  10. package/dist/cli/commands/doctor/bundle.js.map +1 -1
  11. package/dist/cli/commands/doctor/databricks-client.js +18 -22
  12. package/dist/cli/commands/doctor/databricks-client.js.map +1 -1
  13. package/dist/cli/commands/registry/add.js +410 -0
  14. package/dist/cli/commands/registry/add.js.map +1 -0
  15. package/dist/cli/commands/registry/client.js +94 -0
  16. package/dist/cli/commands/registry/client.js.map +1 -0
  17. package/dist/cli/commands/registry/config-plan.js +110 -0
  18. package/dist/cli/commands/registry/config-plan.js.map +1 -0
  19. package/dist/cli/commands/registry/config-writer.js +176 -0
  20. package/dist/cli/commands/registry/config-writer.js.map +1 -0
  21. package/dist/cli/commands/registry/constants.js +62 -0
  22. package/dist/cli/commands/registry/constants.js.map +1 -0
  23. package/dist/cli/commands/registry/env-reconcile.js +116 -0
  24. package/dist/cli/commands/registry/env-reconcile.js.map +1 -0
  25. package/dist/cli/commands/registry/env-writer.js +198 -0
  26. package/dist/cli/commands/registry/env-writer.js.map +1 -0
  27. package/dist/cli/commands/registry/index.js +25 -0
  28. package/dist/cli/commands/registry/index.js.map +1 -0
  29. package/dist/cli/commands/registry/info.js +45 -0
  30. package/dist/cli/commands/registry/info.js.map +1 -0
  31. package/dist/cli/commands/registry/list.js +130 -0
  32. package/dist/cli/commands/registry/list.js.map +1 -0
  33. package/dist/cli/commands/registry/requirements.js +92 -0
  34. package/dist/cli/commands/registry/requirements.js.map +1 -0
  35. package/dist/cli/commands/registry/server-register.js +124 -0
  36. package/dist/cli/commands/registry/server-register.js.map +1 -0
  37. package/dist/cli/commands/registry/workspace-picker.js +353 -0
  38. package/dist/cli/commands/registry/workspace-picker.js.map +1 -0
  39. package/dist/cli/deploy-config.js +36 -0
  40. package/dist/cli/deploy-config.js.map +1 -0
  41. package/dist/cli/index.js +4 -0
  42. package/dist/cli/index.js.map +1 -1
  43. package/dist/connectors/context.js +1 -2
  44. package/dist/connectors/context.js.map +1 -1
  45. package/dist/connectors/files/client.js +1 -2
  46. package/dist/connectors/files/client.js.map +1 -1
  47. package/dist/connectors/genie/client.js +1 -2
  48. package/dist/connectors/genie/client.js.map +1 -1
  49. package/dist/connectors/jobs/client.js +1 -2
  50. package/dist/connectors/jobs/client.js.map +1 -1
  51. package/dist/connectors/sql-warehouse/client.js +1 -2
  52. package/dist/connectors/sql-warehouse/client.js.map +1 -1
  53. package/dist/context/execution-context.d.ts +0 -1
  54. package/dist/context/execution-context.d.ts.map +1 -1
  55. package/dist/context/service-context.d.ts +1 -2
  56. package/dist/context/service-context.d.ts.map +1 -1
  57. package/dist/context/service-context.js +2 -3
  58. package/dist/context/service-context.js.map +1 -1
  59. package/dist/core/appkit.d.ts +1 -2
  60. package/dist/core/appkit.d.ts.map +1 -1
  61. package/dist/index.d.ts +3 -5
  62. package/dist/index.js +2 -3
  63. package/dist/plugins/files/plugin.js +1 -2
  64. package/dist/plugins/files/plugin.js.map +1 -1
  65. package/dist/plugins/files/types.d.ts +1 -2
  66. package/dist/plugins/files/types.d.ts.map +1 -1
  67. package/dist/plugins/jobs/types.d.ts +1 -2
  68. package/dist/plugins/jobs/types.d.ts.map +1 -1
  69. package/dist/plugins/lakebase/lakebase.js +1 -2
  70. package/dist/plugins/lakebase/lakebase.js.map +1 -1
  71. package/dist/schemas/manifest.js +4 -4
  72. package/dist/schemas/manifest.js.map +1 -1
  73. package/dist/shared/src/index.d.ts +4 -1
  74. package/dist/shared/src/workspace-client/client.js +57 -0
  75. package/dist/shared/src/workspace-client/client.js.map +1 -0
  76. package/dist/shared/src/workspace-client/errors.d.ts +2 -0
  77. package/dist/shared/src/workspace-client/errors.js +3 -0
  78. package/dist/shared/src/workspace-client/factory.d.ts +19 -0
  79. package/dist/shared/src/workspace-client/factory.d.ts.map +1 -0
  80. package/dist/shared/src/workspace-client/factory.js +24 -0
  81. package/dist/shared/src/workspace-client/factory.js.map +1 -0
  82. package/dist/shared/src/workspace-client/index.d.ts +4 -0
  83. package/dist/shared/src/workspace-client/legacy.d.ts +31 -0
  84. package/dist/shared/src/workspace-client/legacy.d.ts.map +1 -0
  85. package/dist/shared/src/workspace-client/legacy.js +23 -0
  86. package/dist/shared/src/workspace-client/legacy.js.map +1 -0
  87. package/dist/shared/src/workspace-client/types.d.ts +49 -0
  88. package/dist/shared/src/workspace-client/types.d.ts.map +1 -0
  89. package/dist/type-generator/index.js +1 -2
  90. package/dist/type-generator/index.js.map +1 -1
  91. package/dist/type-generator/query-registry.js +1 -2
  92. package/dist/type-generator/query-registry.js.map +1 -1
  93. package/dist/type-generator/serving/fetcher.js +1 -2
  94. package/dist/type-generator/serving/fetcher.js.map +1 -1
  95. package/dist/type-generator/serving/generator.js +1 -2
  96. package/dist/type-generator/serving/generator.js.map +1 -1
  97. package/dist/type-generator/vite-plugin.js +1 -2
  98. package/dist/type-generator/vite-plugin.js.map +1 -1
  99. package/dist/workspace-client/index.d.ts +4 -3
  100. package/dist/workspace-client/index.js +2 -2
  101. package/dist/workspace-client/legacy.d.ts +9 -4
  102. package/dist/workspace-client/legacy.d.ts.map +1 -1
  103. package/dist/workspace-client/legacy.js +3 -3
  104. package/dist/workspace-client/legacy.js.map +1 -1
  105. package/dist/workspace-client/types.d.ts +2 -2
  106. package/docs/api/appkit/Function.createWorkspaceClient.md +4 -4
  107. package/docs/api/appkit/Interface.WorkspaceClientOptions.md +11 -0
  108. package/package.json +5 -4
  109. package/sbom.cdx.json +1 -1
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"add.js","names":[],"sources":["../../../../src/cli/commands/registry/add.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport {\n fetchRegistryItem,\n fetchVerifiedNames,\n isValidItemName,\n type RegistryItem,\n type RegistryItemFile,\n stripNamespace,\n} from \"./client\";\nimport {\n buildConfigPlan,\n collectBindingValueNeeds,\n planHasContent,\n} from \"./config-plan\";\nimport {\n reportConfigWrite,\n validateBundle,\n writeConfig,\n} from \"./config-writer\";\nimport {\n JS_IDENTIFIER,\n REGISTRY_REPO,\n type RegistryToken,\n resolveToken,\n} from \"./constants\";\nimport { parseEnv } from \"./env-reconcile\";\nimport {\n extractRequirements,\n type ResourceRequirementRow,\n renderRequirements,\n} from \"./requirements\";\n\n/** Subdirectories that commonly hold the frontend / server in an AppKit app. */\nconst FRONTEND_SUBDIRS = [\"client\", \"frontend\", \"web\", \"app\"];\nconst SERVER_SUBDIRS = [\"server\", \"api\", \"backend\"];\n\nfunction isDir(p: string): boolean {\n return fs.existsSync(p) && fs.statSync(p).isDirectory();\n}\n\n/** A registry item is a server plugin if it ships a manifest.json. */\nfunction isPluginItem(item: RegistryItem): boolean {\n return (item.files ?? []).some(\n (f) => path.basename(f.target ?? f.path) === \"manifest.json\",\n );\n}\n\n/**\n * Locates the frontend root for UI components. AppKit apps put the client in a\n * client/ subdir (with its own components.json + src/); the CLI is typically\n * run from the repo root. Prefer the dir with components.json, then a src/.\n */\nfunction findFrontendRoot(cwd: string): string {\n if (fs.existsSync(path.join(cwd, \"components.json\"))) return cwd;\n for (const sub of FRONTEND_SUBDIRS) {\n if (fs.existsSync(path.join(cwd, sub, \"components.json\"))) {\n return path.join(cwd, sub);\n }\n }\n if (isDir(path.join(cwd, \"src\"))) return cwd;\n for (const sub of FRONTEND_SUBDIRS) {\n if (isDir(path.join(cwd, sub, \"src\"))) return path.join(cwd, sub);\n }\n return cwd;\n}\n\n/** Locates the server root for plugins (the server/ subdir, else cwd). */\nfunction findServerRoot(cwd: string): string {\n for (const sub of SERVER_SUBDIRS) {\n if (isDir(path.join(cwd, sub))) return path.join(cwd, sub);\n }\n return cwd;\n}\n\n/** Nearest dir with a package.json, walking up from start (for dep install). */\nfunction findNearestPackageJson(start: string): string {\n let dir = start;\n for (;;) {\n if (fs.existsSync(path.join(dir, \"package.json\"))) return dir;\n const parent = path.dirname(dir);\n if (parent === dir) return start;\n dir = parent;\n }\n}\n\n/**\n * The Databricks profile the app is configured with, read from\n * `DATABRICKS_CONFIG_PROFILE` in `cwd/.env` via the same dotenv parser the app\n * loads its env with. The CLI's top-level `dotenv/config` only loads the launch\n * dir's `.env` into `process.env`; when `--cwd` points at a different app dir,\n * that file isn't loaded, so the workspace picker and bundle validate would\n * miss the app's profile — this reads it directly. Undefined if the file or key\n * is absent, so the SDK's own default resolution still applies.\n */\nexport function profileFromEnv(cwd: string): string | undefined {\n try {\n const content = fs.readFileSync(path.join(cwd, \".env\"), \"utf-8\");\n return parseEnv(content).DATABRICKS_CONFIG_PROFILE || undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Resolves a registry item's `target` under `base`, enforcing that the result\n * stays inside `base`. Registry items are untrusted remote data; a `target`\n * like `../../../.zshrc` or an absolute path could otherwise write files\n * anywhere on disk (arbitrary-write → RCE). Throws on any escape.\n */\nexport function resolveWithinBase(base: string, target: string): string {\n if (path.isAbsolute(target)) {\n throw new Error(`Refusing absolute file target from registry: ${target}`);\n }\n const baseResolved = path.resolve(base);\n const resolved = path.resolve(baseResolved, target);\n if (\n resolved !== baseResolved &&\n !resolved.startsWith(baseResolved + path.sep)\n ) {\n throw new Error(\n `Refusing file target that escapes the destination directory: ${target}`,\n );\n }\n return resolved;\n}\n\n/** UI file destination (relative to the frontend root): placed in src/ if present. */\nfunction uiTargetPath(base: string, file: RegistryItemFile): string {\n let target = file.target ?? path.join(\"components\", path.basename(file.path));\n if (!target.startsWith(\"src/\") && isDir(path.join(base, \"src\"))) {\n target = path.join(\"src\", target);\n }\n return target;\n}\n\n/** Best-effort: the `toPlugin` export name from the item's index.ts. Returns\n * null (caller falls back to printed instructions) unless the name is a plain\n * JS identifier — the item is untrusted remote content and the value is\n * interpolated into the user's server.ts. */\nexport function pluginExportName(item: RegistryItem): string | null {\n const index = (item.files ?? []).find(\n (f) => path.basename(f.target ?? f.path) === \"index.ts\",\n );\n const match = index?.content.match(/export\\s*\\{([^}]*)\\}/);\n if (!match) return null;\n const names = match[1].split(\",\").map((s) => s.trim());\n // Prefer the camelCase toPlugin instance over the PascalCase class.\n const chosen = names.find((n) => /^[a-z]/.test(n)) ?? names[0];\n return chosen && JS_IDENTIFIER.test(chosen) ? chosen : null;\n}\n\nfunction detectPackageManager(cwd: string): \"pnpm\" | \"yarn\" | \"bun\" | \"npm\" {\n if (fs.existsSync(path.join(cwd, \"pnpm-lock.yaml\"))) return \"pnpm\";\n if (fs.existsSync(path.join(cwd, \"yarn.lock\"))) return \"yarn\";\n if (fs.existsSync(path.join(cwd, \"bun.lockb\"))) return \"bun\";\n return \"npm\";\n}\n\n/**\n * A safe npm dependency spec: `[@scope/]name` with an optional `@version`\n * range. Registry `dependencies` are untrusted remote data passed to the\n * package manager, so we reject anything that isn't a plain name+range —\n * blocks tarball/git URL specs (install-script RCE) and `-`-prefixed entries\n * that the PM would parse as flags (argument injection).\n */\nconst SAFE_DEP_SPEC =\n /^(@[a-z0-9][\\w.-]*\\/)?[a-z0-9][\\w.-]*(@[\\w.\\-+~^><=|* ]+)?$/i;\n\n/** Splits deps into safe (installable) and rejected (surfaced to the user). */\nexport function partitionDeps(deps: string[]): {\n safe: string[];\n rejected: string[];\n} {\n const safe: string[] = [];\n const rejected: string[] = [];\n for (const dep of deps) {\n if (dep.startsWith(\"-\") || !SAFE_DEP_SPEC.test(dep)) rejected.push(dep);\n else safe.push(dep);\n }\n return { safe, rejected };\n}\n\nfunction installDependencies(deps: string[], cwd: string): void {\n if (deps.length === 0) return;\n\n const { safe, rejected } = partitionDeps(deps);\n if (rejected.length > 0) {\n console.warn(\n pc.yellow(\n `Skipping suspicious dependenc${rejected.length === 1 ? \"y\" : \"ies\"} from the registry (not a plain name@version): ${rejected.join(\", \")}. Install manually if you trust them.`,\n ),\n );\n }\n if (safe.length === 0) return;\n\n if (!fs.existsSync(path.join(cwd, \"package.json\"))) {\n console.warn(\n pc.yellow(\n `No package.json found — install these manually: ${safe.join(\" \")}`,\n ),\n );\n return;\n }\n const pm = detectPackageManager(cwd);\n const subcommand = pm === \"npm\" ? \"install\" : \"add\";\n console.log(`\\nInstalling dependencies with ${pm}: ${safe.join(\" \")}`);\n // `--` stops the PM from parsing any dep as a flag (defense in depth).\n const result = spawnSync(pm, [subcommand, \"--\", ...safe], {\n stdio: \"inherit\",\n cwd,\n });\n if (result.status !== 0) {\n console.warn(\n pc.yellow(\n `Dependency install exited with code ${result.status ?? \"unknown\"} — install manually if needed: ${safe.join(\" \")}`,\n ),\n );\n }\n}\n\n/** Runs `appkit plugin sync --write` via this same CLI binary. */\nfunction runPluginSync(cwd: string): void {\n const result = spawnSync(\n process.execPath,\n [process.argv[1], \"plugin\", \"sync\", \"--write\"],\n { stdio: \"inherit\", cwd },\n );\n if (result.status !== 0) {\n console.warn(\n pc.yellow(\n \" Plugin sync did not complete cleanly — run `appkit plugin sync --write` manually.\",\n ),\n );\n }\n}\n\nfunction writeItemFile(\n base: string,\n target: string,\n content: string,\n force: boolean,\n cwd: string,\n): void {\n const dest = resolveWithinBase(base, target);\n const existed = fs.existsSync(dest);\n if (existed && !force) {\n console.error(\n pc.red(\n `Refusing to overwrite ${path.relative(cwd, dest)} — pass --force to replace it.`,\n ),\n );\n process.exit(1);\n }\n fs.mkdirSync(path.dirname(dest), { recursive: true });\n fs.writeFileSync(dest, content);\n const label = existed ? pc.yellow(\"Updated\") : pc.green(\"Created\");\n console.log(`${label} ${path.relative(cwd, dest)}`);\n}\n\ninterface PluginSummary {\n importPath: string;\n exportName: string | null;\n}\n\n/**\n * Fetches the requested items plus their transitive registryDependencies.\n * Dependencies are resolved breadth-first and de-duplicated by name, so a\n * plugin that depends on another registry item pulls the whole graph in one\n * `add`. Explicitly-requested items keep their request order and come first.\n */\nexport async function resolveItems(\n names: string[],\n token: RegistryToken | null,\n fetchItem: (\n name: string,\n token: RegistryToken | null,\n ) => Promise<RegistryItem> = fetchRegistryItem,\n): Promise<RegistryItem[]> {\n const seen = new Set<string>();\n const ordered: RegistryItem[] = [];\n // A name is both the fetch path and the on-disk `plugins/<name>` dir, so\n // reject non-slug refs (`/`, `..`, control chars) before they reach either.\n const enqueue = (ref: string): string => {\n const name = stripNamespace(ref);\n if (!isValidItemName(name)) {\n throw new Error(`Invalid registry item name: ${JSON.stringify(ref)}`);\n }\n return name;\n };\n // Breadth-first over the dependency graph, one level per iteration. Items in\n // a level are fetched concurrently (fetch latency is additive otherwise), but\n // levels stay ordered and dedup/cycle handling is unchanged: a name is marked\n // seen before its level is fetched, so it's never fetched or queued twice.\n let level = names.map(enqueue).filter((name) => {\n if (seen.has(name)) return false;\n seen.add(name);\n return true;\n });\n\n while (level.length > 0) {\n const items = await Promise.all(\n level.map(async (key) => {\n const item = await fetchItem(key, token);\n // Pin to the fetch key: the body's self-reported `name` is untrusted\n // and could claim a verified name to slip past the gate. The key is the\n // trustworthy identity the index keys `verified` on.\n item.name = key;\n return item;\n }),\n );\n ordered.push(...items);\n const next: string[] = [];\n for (const item of items) {\n for (const dep of item.registryDependencies ?? []) {\n const depName = enqueue(dep);\n if (seen.has(depName)) continue;\n seen.add(depName);\n next.push(depName);\n }\n }\n level = next;\n }\n\n return ordered;\n}\n\ninterface AddOptions {\n force?: boolean;\n cwd?: string;\n register?: boolean;\n /** false = don't reconcile resource env vars into .env. */\n resources?: boolean;\n /** true = never prompt; use --env flags or leave unset (agent/CI). */\n yes?: boolean;\n /** Pre-supplied env values from repeated --env KEY=VALUE flags. */\n env?: Record<string, string>;\n /** Databricks profile passed to `bundle validate` after writing config. */\n profile?: string;\n /** true = install items the registry index doesn't mark verified. */\n allowUnverified?: boolean;\n}\n\n/**\n * Splits requested names into verified and unverified against the index's\n * verified set. When `verified` is null the index couldn't be read — we can't\n * prove anything is verified, so every name is treated as unverified (the gate\n * then decides whether to warn-and-continue or block). Names are compared with\n * the namespace stripped, matching how items are fetched.\n */\nexport function partitionVerified(\n refs: string[],\n verified: Set<string> | null,\n): { verified: string[]; unverified: string[] } {\n const ok: string[] = [];\n const bad: string[] = [];\n for (const ref of refs) {\n const name = stripNamespace(ref);\n if (verified?.has(name)) ok.push(name);\n else bad.push(name);\n }\n return { verified: ok, unverified: bad };\n}\n\nasync function runAdd(refs: string[], opts: AddOptions): Promise<void> {\n const cwd = opts.cwd ? path.resolve(opts.cwd) : process.cwd();\n const token = resolveToken();\n if (token) {\n console.log(\n `Using ${token.envName} to fetch from ${REGISTRY_REPO} (private).`,\n );\n }\n\n // Resolve the full graph and fetch the verified index concurrently (two\n // independent round-trips). Item resolution is read-only — nothing is written\n // or installed until after the gate below.\n const verifiedP = opts.allowUnverified\n ? Promise.resolve(null)\n : fetchVerifiedNames(token);\n const items = await resolveItems(refs, token);\n\n // Integrity gate over the *entire resolved set* (not just requested names, so\n // an unverified transitive dep can't ride in on a verified item). Fails closed:\n // a null verified set (unreadable index) makes every item count as unverified.\n if (!opts.allowUnverified) {\n const verified = await verifiedP;\n const { unverified } = partitionVerified(\n items.map((i) => i.name),\n verified,\n );\n if (unverified.length > 0) {\n const reason =\n verified === null\n ? \"could not read the registry index to verify these items\"\n : `not marked verified in ${REGISTRY_REPO}`;\n console.error(\n pc.red(\n `Refusing to add unverified item(s) (${reason}): ${unverified.join(\", \")}.`,\n ),\n );\n console.error(\n pc.dim(\n \" Re-run with --allow-unverified if you trust the source; unverified items run code in your app.\",\n ),\n );\n process.exit(1);\n }\n }\n\n const hasUi = items.some((i) => !isPluginItem(i));\n const hasPlugin = items.some(isPluginItem);\n const frontendRoot = hasUi ? findFrontendRoot(cwd) : cwd;\n const serverRoot = hasPlugin ? findServerRoot(cwd) : cwd;\n if (hasUi && frontendRoot !== cwd) {\n console.log(pc.dim(`UI components → ${path.relative(cwd, frontendRoot)}/`));\n }\n if (hasPlugin && serverRoot !== cwd) {\n console.log(pc.dim(`Plugins → ${path.relative(cwd, serverRoot)}/`));\n }\n\n const deps = new Set<string>();\n let wroteUi = false;\n const pluginSummaries: PluginSummary[] = [];\n const allRequirements: ResourceRequirementRow[] = [];\n\n for (const item of items) {\n for (const dep of item.dependencies ?? []) deps.add(dep);\n\n if (isPluginItem(item)) {\n let pluginRel = path.join(\"plugins\", item.name);\n for (const file of item.files ?? []) {\n const target =\n file.target ??\n path.join(\"plugins\", item.name, path.basename(file.path));\n writeItemFile(\n serverRoot,\n target,\n file.content,\n Boolean(opts.force),\n cwd,\n );\n if (path.basename(target) === \"manifest.json\") {\n pluginRel = path.dirname(target);\n }\n }\n const requirements = extractRequirements(item);\n if (requirements.length > 0) {\n console.log(`\\n${renderRequirements(item, requirements)}`);\n allRequirements.push(...requirements);\n }\n pluginSummaries.push({\n importPath: `./${pluginRel}`,\n exportName: pluginExportName(item),\n });\n } else {\n for (const file of item.files ?? []) {\n writeItemFile(\n frontendRoot,\n uiTargetPath(frontendRoot, file),\n file.content,\n Boolean(opts.force),\n cwd,\n );\n wroteUi = true;\n }\n }\n }\n\n installDependencies([...deps], findNearestPackageJson(cwd));\n\n if (hasPlugin) {\n console.log(pc.dim(\"\\nRegistering plugins (appkit plugin sync)...\"));\n runPluginSync(cwd);\n }\n\n if (wroteUi) {\n console.log(\n pc.dim(\n '\\nReminder: import \"@databricks/appkit-ui/styles.css\" once at your app root so components are themed.',\n ),\n );\n }\n // Lazy import: server-register pulls in @ast-grep/napi (a native addon), and\n // this CLI is imported eagerly by index.ts, so a static import would make\n // every unrelated command pay that cost.\n const registerPluginInServer =\n opts.register !== false && pluginSummaries.some((s) => s.exportName)\n ? (await import(\"./server-register.js\")).registerPluginInServer\n : null;\n for (const s of pluginSummaries) {\n // Try to wire the plugin into the server's createApp call automatically;\n // fall back to printing the snippet when the shape isn't the standard one.\n let wired = false;\n // Why auto-registration was skipped, so the fallback can say so (an empty\n // reason means the user opted out via --no-register — no nag then).\n let skipReason: string | undefined;\n if (opts.register === false) {\n // opted out — print the snippet without a \"couldn't\" message\n } else if (!s.exportName) {\n skipReason =\n \"couldn't read a plugin export name from the item's index.ts\";\n } else if (registerPluginInServer) {\n const result = registerPluginInServer(\n serverRoot,\n s.importPath,\n s.exportName,\n );\n // result.file is relative to serverRoot; show it from cwd for the user.\n const shown =\n result.file && path.relative(cwd, path.join(serverRoot, result.file));\n if (result.status === \"wired\") {\n console.log(`\\n${pc.green(\"Registered\")} ${s.exportName} in ${shown}`);\n wired = true;\n } else if (result.status === \"already\") {\n console.log(\n pc.dim(`\\n${s.exportName} is already registered in ${shown}`),\n );\n wired = true;\n } else {\n skipReason = result.reason;\n }\n }\n if (!wired) {\n const imp = s.exportName ?? \"<plugin>\";\n if (skipReason) {\n console.log(\n pc.yellow(`\\nCouldn't auto-register ${imp} — ${skipReason}.`),\n );\n }\n console.log(\n `${pc.bold(\"Add this to your server's createApp call:\")}\\n` +\n pc.dim(\n ` import { ${imp} } from \"${s.importPath}\";\\n` +\n ` const app = await createApp({ plugins: [${imp}(), /* ... */] });`,\n ),\n );\n }\n }\n\n if (opts.resources !== false && allRequirements.length > 0) {\n // Lazy import (same reason as server-register above): env-writer pulls in\n // the workspace picker and, through it, the Databricks SDK.\n const { collectBindingValues, reportEnvResolutions, syncEnv } =\n await import(\"./env-writer.js\");\n // Without --profile, fall back to the app's configured profile so the\n // picker and bundle validate reach the workspace the app runs against.\n const profile = opts.profile ?? profileFromEnv(cwd);\n console.log(pc.dim(\"\\nReconciling resource env vars into .env...\"));\n const resolutions = await syncEnv(allRequirements, {\n cwd,\n nonInteractive: Boolean(opts.yes),\n values: opts.env,\n profile,\n });\n reportEnvResolutions(resolutions);\n\n // Deploy config (app.yaml + databricks.yml). Values come from what the\n // user supplied for env fields (flags or prompts); other fields fall back\n // to their manifest defaults inside buildConfigPlan.\n const values: Record<string, string> = { ...(opts.env ?? {}) };\n for (const r of resolutions) {\n if (r.value !== undefined) values[r.env] = r.value;\n }\n // Binding fields with no env name (e.g. postgres project/branch/database)\n // never flow through .env, so collect them separately — else their\n // databricks.yml bundle variables stay unassigned and bundle validate fails.\n const bindingNeeds = collectBindingValueNeeds(allRequirements);\n if (bindingNeeds.length > 0) {\n const bindingValues = await collectBindingValues(bindingNeeds, {\n cwd,\n nonInteractive: Boolean(opts.yes),\n values: opts.env,\n profile,\n });\n Object.assign(values, bindingValues);\n }\n const plan = buildConfigPlan(allRequirements, values);\n if (planHasContent(plan)) {\n const result = writeConfig(cwd, plan);\n reportConfigWrite(result);\n if (result.databricksYmlChanged) validateBundle(cwd, profile);\n }\n warnScopeNeeding(allRequirements);\n }\n}\n\n/**\n * v1 does not write `user_api_scopes` (deferred to the manifest scope\n * extension). Warn when an added plugin's resource type is known to need one,\n * so the user adds it before deploy.\n */\n/** Resource types known to require a user_api_scope, and the scope each needs. */\nexport const SCOPE_BY_RESOURCE_TYPE: Record<string, string> = {\n genie_space: \"dashboards.genie\",\n serving_endpoint: \"serving.serving-endpoints\",\n // volumes/files-backed access uses files.files\n volume: \"files.files\",\n};\n\n/** Returns the user_api_scopes implied by a set of resource rows (deduped). */\nexport function scopesForResources(\n rows: ResourceRequirementRow[],\n): Map<string, string> {\n const needed = new Map<string, string>();\n for (const row of rows) {\n const scope = SCOPE_BY_RESOURCE_TYPE[row.type];\n if (scope) needed.set(row.type, scope);\n }\n return needed;\n}\n\nfunction warnScopeNeeding(rows: ResourceRequirementRow[]): void {\n const needed = scopesForResources(rows);\n if (needed.size === 0) return;\n const list = [...needed.entries()]\n .map(([type, scope]) => `${type} → ${scope}`)\n .join(\", \");\n console.warn(\n pc.yellow(\n `\\n Note: these resources may need a user_api_scope before deploy: ${list}.\\n` +\n \" Add it under resources.apps.app.user_api_scopes in databricks.yml.\",\n ),\n );\n}\n\n/** Commander reducer for repeatable `--env KEY=VALUE` flags. */\nfunction collectEnvFlag(\n raw: string,\n acc: Record<string, string>,\n): Record<string, string> {\n const eq = raw.indexOf(\"=\");\n if (eq === -1) {\n console.error(`Ignoring --env \"${raw}\" (expected KEY=VALUE).`);\n return acc;\n }\n const key = raw.slice(0, eq).trim();\n const value = raw.slice(eq + 1);\n if (key) acc[key] = value;\n return acc;\n}\n\nexport const addCommand = new Command(\"add\")\n .description(\"Add a UI component or server plugin from the AppKit registry\")\n .argument(\"<item...>\", \"Registry item name(s), e.g. metric-card or hello\")\n .option(\"-f, --force\", \"Overwrite existing files\")\n .option(\"-C, --cwd <dir>\", \"Run as if started in <dir>\")\n .option(\"--no-register\", \"Don't edit the server entry to register plugins\")\n .option(\"--no-resources\", \"Don't reconcile resource env vars into .env\")\n .option(\"-y, --yes\", \"Don't prompt; use --env values or leave vars unset\")\n .option(\n \"--env <KEY=VALUE>\",\n \"Pre-set a resource env var (repeatable)\",\n collectEnvFlag,\n {},\n )\n .option(\n \"-p, --profile <name>\",\n \"Databricks profile for the resource picker and bundle validate (defaults to the app's DATABRICKS_CONFIG_PROFILE)\",\n )\n .option(\n \"--allow-unverified\",\n \"Add items the registry doesn't mark verified (runs untrusted code)\",\n )\n .addHelpText(\n \"after\",\n `\nNo components.json is required. Item type is detected automatically:\n • UI components → <frontend>/src/components/appkit/ (client/ detected)\n • Server plugins → <server>/plugins/<name>/, runs plugin sync, and registers\n them in your createApp call (use --no-register to skip the server edit)\n\nServer plugins declare Databricks resources. On add, their env vars are\nreconciled into .env (and names into .env.example), and the deploy config\n(app.yaml + databricks.yml resource bindings) is patched to match — existing\nentries are never clobbered. Interactive by default; pass --yes for agents/CI\n(uses --env values, leaves the rest unset) and --env KEY=VALUE to supply\nvalues non-interactively. Pass --profile to validate the bundle after writing.\n\nThe frontend/server roots are detected from common layouts, so you can run\nthis from the repo root. While the registry repo is private, a read token is\nresolved from \\`gh auth token\\` or APPKIT_REGISTRY_TOKEN / GITHUB_TOKEN / GH_TOKEN.\n\nExamples:\n $ appkit add metric-card # UI component\n $ appkit add hello # server plugin\n $ appkit add metric-card hello # mix in one call\n $ appkit add analytics --yes --env DATABRICKS_WAREHOUSE_ID=abc123`,\n )\n .action((items: string[], opts: AddOptions) =>\n runAdd(items, opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ import { REGISTRY_INDEX_API_URL, REGISTRY_INDEX_URL, REGISTRY_ITEM_API_TEMPLATE, REGISTRY_ITEM_URL_TEMPLATE, REGISTRY_NAMESPACE, REGISTRY_REPO } from "./constants.js";
2
+ import process from "node:process";
3
+
4
+ //#region src/cli/commands/registry/client.ts
5
+ /** Removes a leading `@databricks-appkit/` namespace from a component reference. */
6
+ function stripNamespace(component) {
7
+ const prefix = `${REGISTRY_NAMESPACE}/`;
8
+ return component.startsWith(prefix) ? component.slice(prefix.length) : component;
9
+ }
10
+ /**
11
+ * A registry item name is a slug: letters, digits, dot, underscore, hyphen —
12
+ * never a path separator or `.`/`..`. Names come from user refs and from an
13
+ * item's untrusted `registryDependencies`, and are used both as the fetch path
14
+ * (`public/r/<name>.json`) and as the on-disk `plugins/<name>` dir. Rejecting
15
+ * separators and dot-segments at the source stops a crafted ref like
16
+ * `../../attacker/repo/payload` from redirecting the fetch to another path in
17
+ * the registry repo or escaping the destination dir, and keeps control chars
18
+ * out of any printed name.
19
+ */
20
+ const ITEM_NAME = /^[A-Za-z0-9._-]+$/;
21
+ /** True when `name` is a safe registry item slug (post-namespace-strip). */
22
+ function isValidItemName(name) {
23
+ return name !== "." && name !== ".." && ITEM_NAME.test(name);
24
+ }
25
+ /**
26
+ * Auth headers for a registry request. With a token the GitHub Contents API is
27
+ * used and `Accept: raw` makes it return file bytes directly; without one the
28
+ * public raw URL needs no headers. Single source for the auth contract shared
29
+ * by every registry fetch.
30
+ */
31
+ function registryAuthHeaders(token) {
32
+ if (!token) return {};
33
+ return {
34
+ Authorization: `Bearer ${token.value}`,
35
+ Accept: "application/vnd.github.raw"
36
+ };
37
+ }
38
+ /**
39
+ * Fetches and parses a single registry item. When a token is present the GitHub
40
+ * Contents API is used (works for the private/internal repo); otherwise the
41
+ * public raw URL is used. Exits the process with a helpful message on failure.
42
+ */
43
+ async function fetchRegistryItem(name, token) {
44
+ const url = (token ? REGISTRY_ITEM_API_TEMPLATE : REGISTRY_ITEM_URL_TEMPLATE).replace("{name}", name);
45
+ const headers = registryAuthHeaders(token);
46
+ let res;
47
+ try {
48
+ res = await fetch(url, { headers });
49
+ } catch (err) {
50
+ console.error(`Failed to fetch "${name}" from ${url}`);
51
+ console.error(` ${err instanceof Error ? err.message : String(err)}`);
52
+ process.exit(1);
53
+ }
54
+ if (res.status === 404) {
55
+ console.error(`"${name}" not found in ${REGISTRY_REPO}.`);
56
+ if (!token) console.error(" If the registry repo is private, set APPKIT_REGISTRY_TOKEN (or GITHUB_TOKEN) to a token with read access.");
57
+ process.exit(1);
58
+ }
59
+ if (res.status === 401 || res.status === 403) {
60
+ console.error(`Access denied (HTTP ${res.status}) fetching "${name}" from ${REGISTRY_REPO}.`);
61
+ console.error(" Check that your token has read access to the repository.");
62
+ process.exit(1);
63
+ }
64
+ if (!res.ok) {
65
+ console.error(`Registry returned HTTP ${res.status} for "${name}".`);
66
+ process.exit(1);
67
+ }
68
+ return await res.json();
69
+ }
70
+ /**
71
+ * Fetches the registry index (`registry.json`) and returns the set of item
72
+ * names marked `meta.verified`. The `verified` flag lives only in the index —
73
+ * the per-item JSON at `public/r/<name>.json` does not carry it — so the `add`
74
+ * integrity gate must consult this. Returns null (not an empty set) if the
75
+ * index can't be read, so the caller can tell "nothing verified" apart from
76
+ * "couldn't check".
77
+ */
78
+ async function fetchVerifiedNames(token) {
79
+ const url = token ? REGISTRY_INDEX_API_URL : REGISTRY_INDEX_URL;
80
+ try {
81
+ const res = await fetch(url, { headers: registryAuthHeaders(token) });
82
+ if (!res.ok) return null;
83
+ const data = await res.json();
84
+ const verified = /* @__PURE__ */ new Set();
85
+ for (const item of data.items ?? []) if (item.meta?.verified === true) verified.add(item.name);
86
+ return verified;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ //#endregion
93
+ export { fetchRegistryItem, fetchVerifiedNames, isValidItemName, registryAuthHeaders, stripNamespace };
94
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":[],"sources":["../../../../src/cli/commands/registry/client.ts"],"sourcesContent":["import process from \"node:process\";\nimport {\n REGISTRY_INDEX_API_URL,\n REGISTRY_INDEX_URL,\n REGISTRY_ITEM_API_TEMPLATE,\n REGISTRY_ITEM_URL_TEMPLATE,\n REGISTRY_NAMESPACE,\n REGISTRY_REPO,\n type RegistryToken,\n} from \"./constants\";\n\nexport interface RegistryItemFile {\n path: string;\n content: string;\n type: string;\n /** Destination path relative to the project root. */\n target?: string;\n}\n\nexport interface RegistryItem {\n name: string;\n type?: string;\n dependencies?: string[];\n registryDependencies?: string[];\n files?: RegistryItemFile[];\n}\n\n/** Removes a leading `@databricks-appkit/` namespace from a component reference. */\nexport function stripNamespace(component: string): string {\n const prefix = `${REGISTRY_NAMESPACE}/`;\n return component.startsWith(prefix)\n ? component.slice(prefix.length)\n : component;\n}\n\n/**\n * A registry item name is a slug: letters, digits, dot, underscore, hyphen —\n * never a path separator or `.`/`..`. Names come from user refs and from an\n * item's untrusted `registryDependencies`, and are used both as the fetch path\n * (`public/r/<name>.json`) and as the on-disk `plugins/<name>` dir. Rejecting\n * separators and dot-segments at the source stops a crafted ref like\n * `../../attacker/repo/payload` from redirecting the fetch to another path in\n * the registry repo or escaping the destination dir, and keeps control chars\n * out of any printed name.\n */\nconst ITEM_NAME = /^[A-Za-z0-9._-]+$/;\n\n/** True when `name` is a safe registry item slug (post-namespace-strip). */\nexport function isValidItemName(name: string): boolean {\n return name !== \".\" && name !== \"..\" && ITEM_NAME.test(name);\n}\n\n/**\n * Auth headers for a registry request. With a token the GitHub Contents API is\n * used and `Accept: raw` makes it return file bytes directly; without one the\n * public raw URL needs no headers. Single source for the auth contract shared\n * by every registry fetch.\n */\nexport function registryAuthHeaders(\n token: RegistryToken | null,\n): Record<string, string> {\n if (!token) return {};\n return {\n Authorization: `Bearer ${token.value}`,\n Accept: \"application/vnd.github.raw\",\n };\n}\n\n/**\n * Fetches and parses a single registry item. When a token is present the GitHub\n * Contents API is used (works for the private/internal repo); otherwise the\n * public raw URL is used. Exits the process with a helpful message on failure.\n */\nexport async function fetchRegistryItem(\n name: string,\n token: RegistryToken | null,\n): Promise<RegistryItem> {\n const template = token\n ? REGISTRY_ITEM_API_TEMPLATE\n : REGISTRY_ITEM_URL_TEMPLATE;\n const url = template.replace(\"{name}\", name);\n const headers = registryAuthHeaders(token);\n\n let res: Awaited<ReturnType<typeof fetch>>;\n try {\n res = await fetch(url, { headers });\n } catch (err) {\n console.error(`Failed to fetch \"${name}\" from ${url}`);\n console.error(` ${err instanceof Error ? err.message : String(err)}`);\n process.exit(1);\n }\n\n if (res.status === 404) {\n console.error(`\"${name}\" not found in ${REGISTRY_REPO}.`);\n if (!token) {\n console.error(\n \" If the registry repo is private, set APPKIT_REGISTRY_TOKEN (or GITHUB_TOKEN) to a token with read access.\",\n );\n }\n process.exit(1);\n }\n if (res.status === 401 || res.status === 403) {\n console.error(\n `Access denied (HTTP ${res.status}) fetching \"${name}\" from ${REGISTRY_REPO}.`,\n );\n console.error(\" Check that your token has read access to the repository.\");\n process.exit(1);\n }\n if (!res.ok) {\n console.error(`Registry returned HTTP ${res.status} for \"${name}\".`);\n process.exit(1);\n }\n\n return (await res.json()) as RegistryItem;\n}\n\n/** One entry in the registry index (`registry.json`). */\nexport interface RegistryIndexEntry {\n name: string;\n meta?: { verified?: boolean };\n}\n\n/**\n * Fetches the registry index (`registry.json`) and returns the set of item\n * names marked `meta.verified`. The `verified` flag lives only in the index —\n * the per-item JSON at `public/r/<name>.json` does not carry it — so the `add`\n * integrity gate must consult this. Returns null (not an empty set) if the\n * index can't be read, so the caller can tell \"nothing verified\" apart from\n * \"couldn't check\".\n */\nexport async function fetchVerifiedNames(\n token: RegistryToken | null,\n): Promise<Set<string> | null> {\n const url = token ? REGISTRY_INDEX_API_URL : REGISTRY_INDEX_URL;\n try {\n const res = await fetch(url, { headers: registryAuthHeaders(token) });\n if (!res.ok) return null;\n const data = (await res.json()) as { items?: RegistryIndexEntry[] };\n const verified = new Set<string>();\n for (const item of data.items ?? []) {\n if (item.meta?.verified === true) verified.add(item.name);\n }\n return verified;\n } catch {\n return null;\n }\n}\n"],"mappings":";;;;;AA4BA,SAAgB,eAAe,WAA2B;CACxD,MAAM,SAAS,GAAG,mBAAmB;AACrC,QAAO,UAAU,WAAW,OAAO,GAC/B,UAAU,MAAM,OAAO,OAAO,GAC9B;;;;;;;;;;;;AAaN,MAAM,YAAY;;AAGlB,SAAgB,gBAAgB,MAAuB;AACrD,QAAO,SAAS,OAAO,SAAS,QAAQ,UAAU,KAAK,KAAK;;;;;;;;AAS9D,SAAgB,oBACd,OACwB;AACxB,KAAI,CAAC,MAAO,QAAO,EAAE;AACrB,QAAO;EACL,eAAe,UAAU,MAAM;EAC/B,QAAQ;EACT;;;;;;;AAQH,eAAsB,kBACpB,MACA,OACuB;CAIvB,MAAM,OAHW,QACb,6BACA,4BACiB,QAAQ,UAAU,KAAK;CAC5C,MAAM,UAAU,oBAAoB,MAAM;CAE1C,IAAI;AACJ,KAAI;AACF,QAAM,MAAM,MAAM,KAAK,EAAE,SAAS,CAAC;UAC5B,KAAK;AACZ,UAAQ,MAAM,oBAAoB,KAAK,SAAS,MAAM;AACtD,UAAQ,MAAM,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;AACtE,UAAQ,KAAK,EAAE;;AAGjB,KAAI,IAAI,WAAW,KAAK;AACtB,UAAQ,MAAM,IAAI,KAAK,iBAAiB,cAAc,GAAG;AACzD,MAAI,CAAC,MACH,SAAQ,MACN,8GACD;AAEH,UAAQ,KAAK,EAAE;;AAEjB,KAAI,IAAI,WAAW,OAAO,IAAI,WAAW,KAAK;AAC5C,UAAQ,MACN,uBAAuB,IAAI,OAAO,cAAc,KAAK,SAAS,cAAc,GAC7E;AACD,UAAQ,MAAM,6DAA6D;AAC3E,UAAQ,KAAK,EAAE;;AAEjB,KAAI,CAAC,IAAI,IAAI;AACX,UAAQ,MAAM,0BAA0B,IAAI,OAAO,QAAQ,KAAK,IAAI;AACpE,UAAQ,KAAK,EAAE;;AAGjB,QAAQ,MAAM,IAAI,MAAM;;;;;;;;;;AAiB1B,eAAsB,mBACpB,OAC6B;CAC7B,MAAM,MAAM,QAAQ,yBAAyB;AAC7C,KAAI;EACF,MAAM,MAAM,MAAM,MAAM,KAAK,EAAE,SAAS,oBAAoB,MAAM,EAAE,CAAC;AACrE,MAAI,CAAC,IAAI,GAAI,QAAO;EACpB,MAAM,OAAQ,MAAM,IAAI,MAAM;EAC9B,MAAM,2BAAW,IAAI,KAAa;AAClC,OAAK,MAAM,QAAQ,KAAK,SAAS,EAAE,CACjC,KAAI,KAAK,MAAM,aAAa,KAAM,UAAS,IAAI,KAAK,KAAK;AAE3D,SAAO;SACD;AACN,SAAO"}