@databricks/appkit-ui 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.
- 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
|
@@ -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"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { fieldOrigin, isValidEnvName } from "./requirements.js";
|
|
2
|
+
|
|
3
|
+
//#region src/cli/commands/registry/config-plan.ts
|
|
4
|
+
const BINDING_SPECS = {
|
|
5
|
+
sql_warehouse: {
|
|
6
|
+
variableFields: ["id"],
|
|
7
|
+
bindingFields: ["id"],
|
|
8
|
+
variable: (f) => `sql_warehouse_${f}`
|
|
9
|
+
},
|
|
10
|
+
postgres: {
|
|
11
|
+
variableFields: [
|
|
12
|
+
"project",
|
|
13
|
+
"branch",
|
|
14
|
+
"database"
|
|
15
|
+
],
|
|
16
|
+
bindingFields: ["branch", "database"],
|
|
17
|
+
variable: (f) => `postgres_${f}`
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Builds the deploy-config plan for a set of resource rows. `values` supplies
|
|
22
|
+
* the concrete values for the target-level bundle variables (keyed by the
|
|
23
|
+
* manifest field's env var name for env-bearing fields, else by field key);
|
|
24
|
+
* missing values leave the variable value undefined.
|
|
25
|
+
*/
|
|
26
|
+
function buildConfigPlan(rows, values = {}) {
|
|
27
|
+
const appYamlEnv = [];
|
|
28
|
+
const bundleVariables = [];
|
|
29
|
+
const resourceBindings = [];
|
|
30
|
+
const unverifiedTypes = [];
|
|
31
|
+
const seenEnv = /* @__PURE__ */ new Set();
|
|
32
|
+
const seenVar = /* @__PURE__ */ new Set();
|
|
33
|
+
for (const row of rows) {
|
|
34
|
+
const resourceKey = row.resourceKey ?? row.type;
|
|
35
|
+
for (const field of row.fields) {
|
|
36
|
+
if (!field.env || fieldOrigin(field) === "platform") continue;
|
|
37
|
+
if (!isValidEnvName(field.env)) continue;
|
|
38
|
+
if (seenEnv.has(field.env)) continue;
|
|
39
|
+
seenEnv.add(field.env);
|
|
40
|
+
appYamlEnv.push({
|
|
41
|
+
name: field.env,
|
|
42
|
+
valueFrom: resourceKey
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const spec = BINDING_SPECS[row.type];
|
|
46
|
+
if (!spec) {
|
|
47
|
+
if (!unverifiedTypes.includes(row.type)) unverifiedTypes.push(row.type);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
for (const fieldKey of spec.variableFields) {
|
|
51
|
+
const varName = spec.variable(fieldKey);
|
|
52
|
+
if (seenVar.has(varName)) continue;
|
|
53
|
+
seenVar.add(varName);
|
|
54
|
+
const field = row.fields.find((f) => f.key === fieldKey);
|
|
55
|
+
const valueKey = field?.env ?? fieldKey;
|
|
56
|
+
bundleVariables.push({
|
|
57
|
+
name: varName,
|
|
58
|
+
description: field?.description,
|
|
59
|
+
value: values[valueKey] ?? field?.value
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const fields = {};
|
|
63
|
+
for (const fieldKey of spec.bindingFields) fields[fieldKey] = `\${var.${spec.variable(fieldKey)}}`;
|
|
64
|
+
resourceBindings.push({
|
|
65
|
+
name: resourceKey,
|
|
66
|
+
type: row.type,
|
|
67
|
+
permission: row.permission,
|
|
68
|
+
fields
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
appYamlEnv,
|
|
73
|
+
bundleVariables,
|
|
74
|
+
resourceBindings,
|
|
75
|
+
unverifiedTypes
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Binding fields that carry a bundle-variable value but have no `env` name and
|
|
80
|
+
* no static default — so they're invisible to collectEnvNeeds and must be
|
|
81
|
+
* collected separately (keyed by fieldKey) to produce a valid databricks.yml.
|
|
82
|
+
*/
|
|
83
|
+
function collectBindingValueNeeds(rows) {
|
|
84
|
+
const needs = [];
|
|
85
|
+
const seen = /* @__PURE__ */ new Set();
|
|
86
|
+
for (const row of rows) {
|
|
87
|
+
const spec = BINDING_SPECS[row.type];
|
|
88
|
+
if (!spec) continue;
|
|
89
|
+
for (const fieldKey of spec.variableFields) {
|
|
90
|
+
const field = row.fields.find((f) => f.key === fieldKey);
|
|
91
|
+
if (field?.env || field?.value !== void 0) continue;
|
|
92
|
+
if (seen.has(fieldKey)) continue;
|
|
93
|
+
seen.add(fieldKey);
|
|
94
|
+
needs.push({
|
|
95
|
+
fieldKey,
|
|
96
|
+
resourceType: row.type,
|
|
97
|
+
description: field?.description
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return needs;
|
|
102
|
+
}
|
|
103
|
+
/** True when the plan has any deploy-config content to write. */
|
|
104
|
+
function planHasContent(plan) {
|
|
105
|
+
return plan.appYamlEnv.length > 0 || plan.bundleVariables.length > 0 || plan.resourceBindings.length > 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
//#endregion
|
|
109
|
+
export { buildConfigPlan, collectBindingValueNeeds, planHasContent };
|
|
110
|
+
//# sourceMappingURL=config-plan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-plan.js","names":[],"sources":["../../../../src/cli/commands/registry/config-plan.ts"],"sourcesContent":["import type { AppYamlEnvEntry, ResourceBinding } from \"../../deploy-config\";\nimport {\n fieldOrigin,\n isValidEnvName,\n type ResourceRequirementRow,\n} from \"./requirements\";\n\n/**\n * Deploy-config generation for a plugin's resources, reproducing what\n * `databricks apps init` renders. Verified byte-for-byte against golden\n * fixtures (see __fixtures__/) for the resource types listed in\n * {@link BINDING_SPECS}. Unverified types degrade safely: their env entries\n * are still produced (that shape is uniform), but the databricks.yml resource\n * binding is skipped with a warning rather than guessed.\n */\n\n/** A `databricks.yml` top-level bundle variable. */\nexport interface BundleVariable {\n name: string;\n description?: string;\n /** The value placed under targets.default.variables. */\n value?: string;\n}\n\nexport interface ConfigPlan {\n appYamlEnv: AppYamlEnvEntry[];\n bundleVariables: BundleVariable[];\n resourceBindings: ResourceBinding[];\n /** Resource types encountered that have no verified binding spec. */\n unverifiedTypes: string[];\n}\n\n/**\n * Per-type rules for producing databricks.yml bundle variables and the app\n * resource binding. Only types verified against golden fixtures appear here.\n *\n * - `variableFields`: field keys that become bundle variables (a superset of\n * the binding fields; e.g. postgres declares project+branch+database).\n * - `bindingFields`: field keys included in the resource binding (a subset;\n * e.g. postgres binds branch+database but not project).\n * - `variable(field)`: the bundle-variable name for a given field key.\n */\ninterface BindingSpec {\n variableFields: string[];\n bindingFields: string[];\n variable: (fieldKey: string) => string;\n}\n\nconst BINDING_SPECS: Record<string, BindingSpec> = {\n // Verified against __fixtures__/analytics.\n sql_warehouse: {\n variableFields: [\"id\"],\n bindingFields: [\"id\"],\n // fixture: variable is `sql_warehouse_id`\n variable: (f) => `sql_warehouse_${f}`,\n },\n // Verified against __fixtures__/lakebase.\n postgres: {\n variableFields: [\"project\", \"branch\", \"database\"],\n bindingFields: [\"branch\", \"database\"],\n // fixture: variables are `postgres_<fieldKey>` (project/branch/database)\n variable: (f) => `postgres_${f}`,\n },\n};\n\n/**\n * Builds the deploy-config plan for a set of resource rows. `values` supplies\n * the concrete values for the target-level bundle variables (keyed by the\n * manifest field's env var name for env-bearing fields, else by field key);\n * missing values leave the variable value undefined.\n */\nexport function buildConfigPlan(\n rows: ResourceRequirementRow[],\n values: Record<string, string> = {},\n): ConfigPlan {\n const appYamlEnv: AppYamlEnvEntry[] = [];\n const bundleVariables: BundleVariable[] = [];\n const resourceBindings: ResourceBinding[] = [];\n const unverifiedTypes: string[] = [];\n const seenEnv = new Set<string>();\n const seenVar = new Set<string>();\n\n for (const row of rows) {\n // app.yaml env: every env-bearing field maps to valueFrom = resourceKey,\n // except platform-injected fields (the platform provides those directly).\n const resourceKey = row.resourceKey ?? row.type;\n for (const field of row.fields) {\n if (!field.env || fieldOrigin(field) === \"platform\") continue;\n // env names are untrusted manifest data emitted into app.yaml — drop\n // anything that isn't a plain env identifier (mirrors the .env guard).\n if (!isValidEnvName(field.env)) continue;\n if (seenEnv.has(field.env)) continue;\n seenEnv.add(field.env);\n appYamlEnv.push({ name: field.env, valueFrom: resourceKey });\n }\n\n const spec = BINDING_SPECS[row.type];\n if (!spec) {\n if (!unverifiedTypes.includes(row.type)) unverifiedTypes.push(row.type);\n continue;\n }\n\n // Bundle variables (superset of binding fields for this type).\n for (const fieldKey of spec.variableFields) {\n const varName = spec.variable(fieldKey);\n if (seenVar.has(varName)) continue;\n seenVar.add(varName);\n const field = row.fields.find((f) => f.key === fieldKey);\n const valueKey = field?.env ?? fieldKey;\n bundleVariables.push({\n name: varName,\n description: field?.description,\n value: values[valueKey] ?? field?.value,\n });\n }\n\n // Resource binding: only the spec's binding fields, referencing ${var.X}.\n const fields: Record<string, string> = {};\n for (const fieldKey of spec.bindingFields) {\n fields[fieldKey] = `\\${var.${spec.variable(fieldKey)}}`;\n }\n resourceBindings.push({\n name: resourceKey,\n type: row.type,\n permission: row.permission,\n fields,\n });\n }\n\n return { appYamlEnv, bundleVariables, resourceBindings, unverifiedTypes };\n}\n\n/**\n * A bundle-variable value the user must supply that the .env reconciliation\n * flow can't collect — a binding field with NO `env` name (e.g. postgres\n * project/branch/database). Without collecting these, databricks.yml declares\n * `${var.postgres_branch}` but never assigns it, and `bundle validate` fails.\n * Keyed by `fieldKey` (matching how buildConfigPlan looks up `values`).\n */\nexport interface BindingValueNeed {\n fieldKey: string;\n resourceType: string;\n description?: string;\n}\n\n/**\n * Binding fields that carry a bundle-variable value but have no `env` name and\n * no static default — so they're invisible to collectEnvNeeds and must be\n * collected separately (keyed by fieldKey) to produce a valid databricks.yml.\n */\nexport function collectBindingValueNeeds(\n rows: ResourceRequirementRow[],\n): BindingValueNeed[] {\n const needs: BindingValueNeed[] = [];\n const seen = new Set<string>();\n for (const row of rows) {\n const spec = BINDING_SPECS[row.type];\n if (!spec) continue;\n for (const fieldKey of spec.variableFields) {\n const field = row.fields.find((f) => f.key === fieldKey);\n // Skip fields that already flow through .env (have an env name) or carry\n // a static default — those get their value elsewhere.\n if (field?.env || field?.value !== undefined) continue;\n if (seen.has(fieldKey)) continue;\n seen.add(fieldKey);\n needs.push({\n fieldKey,\n resourceType: row.type,\n description: field?.description,\n });\n }\n }\n return needs;\n}\n\n/** True when the plan has any deploy-config content to write. */\nexport function planHasContent(plan: ConfigPlan): boolean {\n return (\n plan.appYamlEnv.length > 0 ||\n plan.bundleVariables.length > 0 ||\n plan.resourceBindings.length > 0\n );\n}\n"],"mappings":";;;AAgDA,MAAM,gBAA6C;CAEjD,eAAe;EACb,gBAAgB,CAAC,KAAK;EACtB,eAAe,CAAC,KAAK;EAErB,WAAW,MAAM,iBAAiB;EACnC;CAED,UAAU;EACR,gBAAgB;GAAC;GAAW;GAAU;GAAW;EACjD,eAAe,CAAC,UAAU,WAAW;EAErC,WAAW,MAAM,YAAY;EAC9B;CACF;;;;;;;AAQD,SAAgB,gBACd,MACA,SAAiC,EAAE,EACvB;CACZ,MAAM,aAAgC,EAAE;CACxC,MAAM,kBAAoC,EAAE;CAC5C,MAAM,mBAAsC,EAAE;CAC9C,MAAM,kBAA4B,EAAE;CACpC,MAAM,0BAAU,IAAI,KAAa;CACjC,MAAM,0BAAU,IAAI,KAAa;AAEjC,MAAK,MAAM,OAAO,MAAM;EAGtB,MAAM,cAAc,IAAI,eAAe,IAAI;AAC3C,OAAK,MAAM,SAAS,IAAI,QAAQ;AAC9B,OAAI,CAAC,MAAM,OAAO,YAAY,MAAM,KAAK,WAAY;AAGrD,OAAI,CAAC,eAAe,MAAM,IAAI,CAAE;AAChC,OAAI,QAAQ,IAAI,MAAM,IAAI,CAAE;AAC5B,WAAQ,IAAI,MAAM,IAAI;AACtB,cAAW,KAAK;IAAE,MAAM,MAAM;IAAK,WAAW;IAAa,CAAC;;EAG9D,MAAM,OAAO,cAAc,IAAI;AAC/B,MAAI,CAAC,MAAM;AACT,OAAI,CAAC,gBAAgB,SAAS,IAAI,KAAK,CAAE,iBAAgB,KAAK,IAAI,KAAK;AACvE;;AAIF,OAAK,MAAM,YAAY,KAAK,gBAAgB;GAC1C,MAAM,UAAU,KAAK,SAAS,SAAS;AACvC,OAAI,QAAQ,IAAI,QAAQ,CAAE;AAC1B,WAAQ,IAAI,QAAQ;GACpB,MAAM,QAAQ,IAAI,OAAO,MAAM,MAAM,EAAE,QAAQ,SAAS;GACxD,MAAM,WAAW,OAAO,OAAO;AAC/B,mBAAgB,KAAK;IACnB,MAAM;IACN,aAAa,OAAO;IACpB,OAAO,OAAO,aAAa,OAAO;IACnC,CAAC;;EAIJ,MAAM,SAAiC,EAAE;AACzC,OAAK,MAAM,YAAY,KAAK,cAC1B,QAAO,YAAY,UAAU,KAAK,SAAS,SAAS,CAAC;AAEvD,mBAAiB,KAAK;GACpB,MAAM;GACN,MAAM,IAAI;GACV,YAAY,IAAI;GAChB;GACD,CAAC;;AAGJ,QAAO;EAAE;EAAY;EAAiB;EAAkB;EAAiB;;;;;;;AAqB3E,SAAgB,yBACd,MACoB;CACpB,MAAM,QAA4B,EAAE;CACpC,MAAM,uBAAO,IAAI,KAAa;AAC9B,MAAK,MAAM,OAAO,MAAM;EACtB,MAAM,OAAO,cAAc,IAAI;AAC/B,MAAI,CAAC,KAAM;AACX,OAAK,MAAM,YAAY,KAAK,gBAAgB;GAC1C,MAAM,QAAQ,IAAI,OAAO,MAAM,MAAM,EAAE,QAAQ,SAAS;AAGxD,OAAI,OAAO,OAAO,OAAO,UAAU,OAAW;AAC9C,OAAI,KAAK,IAAI,SAAS,CAAE;AACxB,QAAK,IAAI,SAAS;AAClB,SAAM,KAAK;IACT;IACA,cAAc,IAAI;IAClB,aAAa,OAAO;IACrB,CAAC;;;AAGN,QAAO;;;AAIT,SAAgB,eAAe,MAA2B;AACxD,QACE,KAAK,WAAW,SAAS,KACzB,KAAK,gBAAgB,SAAS,KAC9B,KAAK,iBAAiB,SAAS"}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { APP_YAML_FILE, DATABRICKS_YML_FILE, bindingToNode } from "../../deploy-config.js";
|
|
2
|
+
import { planHasContent } from "./config-plan.js";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { parseDocument } from "yaml";
|
|
8
|
+
|
|
9
|
+
//#region src/cli/commands/registry/config-writer.ts
|
|
10
|
+
/** Reads and parses a YAML file into a Document, or a fresh doc if absent. */
|
|
11
|
+
function loadDoc(file) {
|
|
12
|
+
if (fs.existsSync(file)) return parseDocument(fs.readFileSync(file, "utf-8"));
|
|
13
|
+
return parseDocument("");
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Additively patches `app.yaml`'s `env:` list with entries not already present
|
|
17
|
+
* (matched by `name`). Returns the names added.
|
|
18
|
+
*/
|
|
19
|
+
function patchAppYaml(file, entries) {
|
|
20
|
+
if (entries.length === 0) return [];
|
|
21
|
+
const doc = loadDoc(file);
|
|
22
|
+
let seq = doc.get("env");
|
|
23
|
+
if (!seq || typeof seq.add !== "function") {
|
|
24
|
+
doc.set("env", doc.createNode([]));
|
|
25
|
+
seq = doc.get("env");
|
|
26
|
+
}
|
|
27
|
+
const existingNames = /* @__PURE__ */ new Set();
|
|
28
|
+
for (const item of seq.items) {
|
|
29
|
+
const name = item?.get?.("name");
|
|
30
|
+
if (typeof name === "string") existingNames.add(name);
|
|
31
|
+
}
|
|
32
|
+
const added = [];
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (existingNames.has(entry.name)) continue;
|
|
35
|
+
seq.add(doc.createNode({
|
|
36
|
+
name: entry.name,
|
|
37
|
+
valueFrom: entry.valueFrom
|
|
38
|
+
}));
|
|
39
|
+
added.push(entry.name);
|
|
40
|
+
}
|
|
41
|
+
if (added.length > 0) fs.writeFileSync(file, doc.toString());
|
|
42
|
+
return added;
|
|
43
|
+
}
|
|
44
|
+
/** Navigates/creates a nested map path, returning the leaf map. */
|
|
45
|
+
function ensureMap(doc, pathKeys) {
|
|
46
|
+
let node = doc.contents;
|
|
47
|
+
const walked = [];
|
|
48
|
+
for (const key of pathKeys) {
|
|
49
|
+
walked.push(key);
|
|
50
|
+
let child = doc.getIn(walked);
|
|
51
|
+
if (!child || typeof child.set !== "function") {
|
|
52
|
+
doc.setIn(walked, doc.createNode({}));
|
|
53
|
+
child = doc.getIn(walked);
|
|
54
|
+
}
|
|
55
|
+
node = child;
|
|
56
|
+
}
|
|
57
|
+
return node;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Additively patches `databricks.yml`: adds bundle `variables`, the app
|
|
61
|
+
* `resources` bindings, and the target-level variable values — each only if
|
|
62
|
+
* not already present. Returns the names added plus whether the file changed
|
|
63
|
+
* (a target-value-only write changes the file without adding any names).
|
|
64
|
+
*/
|
|
65
|
+
function patchDatabricksYml(file, plan) {
|
|
66
|
+
if (plan.bundleVariables.length === 0 && plan.resourceBindings.length === 0) return {
|
|
67
|
+
added: [],
|
|
68
|
+
changed: false
|
|
69
|
+
};
|
|
70
|
+
const doc = loadDoc(file);
|
|
71
|
+
const added = [];
|
|
72
|
+
if (plan.bundleVariables.length > 0) {
|
|
73
|
+
const vars = ensureMap(doc, ["variables"]);
|
|
74
|
+
for (const v of plan.bundleVariables) {
|
|
75
|
+
if (vars.has(v.name)) continue;
|
|
76
|
+
const body = {};
|
|
77
|
+
if (v.description) body.description = v.description;
|
|
78
|
+
vars.set(v.name, doc.createNode(body));
|
|
79
|
+
added.push(v.name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (plan.resourceBindings.length > 0) {
|
|
83
|
+
const app = ensureMap(doc, [
|
|
84
|
+
"resources",
|
|
85
|
+
"apps",
|
|
86
|
+
"app"
|
|
87
|
+
]);
|
|
88
|
+
let bindings = app.get("resources");
|
|
89
|
+
if (!bindings || typeof bindings.add !== "function") {
|
|
90
|
+
app.set("resources", doc.createNode([]));
|
|
91
|
+
bindings = app.get("resources");
|
|
92
|
+
}
|
|
93
|
+
const existing = /* @__PURE__ */ new Set();
|
|
94
|
+
for (const item of bindings.items) {
|
|
95
|
+
const name = item?.get?.("name");
|
|
96
|
+
if (typeof name === "string") existing.add(name);
|
|
97
|
+
}
|
|
98
|
+
for (const binding of plan.resourceBindings) {
|
|
99
|
+
if (existing.has(binding.name)) continue;
|
|
100
|
+
bindings.add(doc.createNode(bindingToNode(binding)));
|
|
101
|
+
added.push(binding.name);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
let wroteTargetValue = false;
|
|
105
|
+
const withValues = plan.bundleVariables.filter((v) => v.value !== void 0);
|
|
106
|
+
if (withValues.length > 0) {
|
|
107
|
+
const targetVars = ensureMap(doc, [
|
|
108
|
+
"targets",
|
|
109
|
+
"default",
|
|
110
|
+
"variables"
|
|
111
|
+
]);
|
|
112
|
+
for (const v of withValues) {
|
|
113
|
+
if (targetVars.has(v.name)) continue;
|
|
114
|
+
targetVars.set(v.name, v.value);
|
|
115
|
+
wroteTargetValue = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const changed = added.length > 0 || wroteTargetValue;
|
|
119
|
+
if (changed) fs.writeFileSync(file, doc.toString());
|
|
120
|
+
return {
|
|
121
|
+
added,
|
|
122
|
+
changed
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Applies a config plan to `app.yaml` and `databricks.yml` in `cwd` via
|
|
127
|
+
* comment-preserving additive patches. Never overwrites existing entries.
|
|
128
|
+
*/
|
|
129
|
+
function writeConfig(cwd, plan) {
|
|
130
|
+
const appAdded = patchAppYaml(path.join(cwd, APP_YAML_FILE), plan.appYamlEnv);
|
|
131
|
+
const db = patchDatabricksYml(path.join(cwd, DATABRICKS_YML_FILE), plan);
|
|
132
|
+
return {
|
|
133
|
+
appYamlChanged: appAdded.length > 0,
|
|
134
|
+
databricksYmlChanged: db.changed,
|
|
135
|
+
added: [...new Set([...appAdded, ...db.added])],
|
|
136
|
+
unverifiedTypes: plan.unverifiedTypes
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Runs `databricks bundle validate` as a post-write correctness gate. Returns
|
|
141
|
+
* true when the config validates (or when the CLI is unavailable — a missing
|
|
142
|
+
* CLI shouldn't fail an install). Surfaces validation errors to the user.
|
|
143
|
+
*/
|
|
144
|
+
function validateBundle(cwd, profile) {
|
|
145
|
+
const args = ["bundle", "validate"];
|
|
146
|
+
if (profile) args.push("-p", profile);
|
|
147
|
+
let result;
|
|
148
|
+
try {
|
|
149
|
+
result = spawnSync("databricks", args, {
|
|
150
|
+
cwd,
|
|
151
|
+
encoding: "utf-8"
|
|
152
|
+
});
|
|
153
|
+
} catch {
|
|
154
|
+
console.warn(pc.yellow(" Skipped bundle validate (databricks CLI not found)."));
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
if (result.error) {
|
|
158
|
+
console.warn(pc.yellow(" Skipped bundle validate (databricks CLI not found)."));
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
if (result.status !== 0) {
|
|
162
|
+
console.warn(pc.yellow(" databricks bundle validate reported issues:"));
|
|
163
|
+
if (result.stderr) console.warn(result.stderr.trim());
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
/** Reports what the config write did, including any unverified-type warnings. */
|
|
169
|
+
function reportConfigWrite(result) {
|
|
170
|
+
if (result.added.length > 0) console.log(`${pc.green("Updated deploy config:")} ${result.added.join(", ")}`);
|
|
171
|
+
if (result.unverifiedTypes.length > 0) console.warn(pc.yellow(` No databricks.yml binding written for: ${result.unverifiedTypes.join(", ")}. Add the resource binding manually before deploy.`));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
//#endregion
|
|
175
|
+
export { reportConfigWrite, validateBundle, writeConfig };
|
|
176
|
+
//# sourceMappingURL=config-writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-writer.js","names":[],"sources":["../../../../src/cli/commands/registry/config-writer.ts"],"sourcesContent":["import { type SpawnSyncReturns, spawnSync } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport pc from \"picocolors\";\nimport { parseDocument, type YAMLMap, type YAMLSeq } from \"yaml\";\nimport {\n APP_YAML_FILE,\n type AppYamlEnvEntry,\n bindingToNode,\n DATABRICKS_YML_FILE,\n} from \"../../deploy-config\";\nimport { type ConfigPlan, planHasContent } from \"./config-plan\";\n\nexport interface ConfigWriteResult {\n appYamlChanged: boolean;\n databricksYmlChanged: boolean;\n /** Env/binding names actually added (skipping ones already present). */\n added: string[];\n /** Resource types skipped for lack of a verified binding spec. */\n unverifiedTypes: string[];\n}\n\n/** Reads and parses a YAML file into a Document, or a fresh doc if absent. */\nfunction loadDoc(file: string): ReturnType<typeof parseDocument> {\n if (fs.existsSync(file)) {\n return parseDocument(fs.readFileSync(file, \"utf-8\"));\n }\n return parseDocument(\"\");\n}\n\n/**\n * Additively patches `app.yaml`'s `env:` list with entries not already present\n * (matched by `name`). Returns the names added.\n */\nfunction patchAppYaml(file: string, entries: AppYamlEnvEntry[]): string[] {\n if (entries.length === 0) return [];\n const doc = loadDoc(file);\n let seq = doc.get(\"env\") as YAMLSeq | undefined;\n if (!seq || typeof (seq as YAMLSeq).add !== \"function\") {\n doc.set(\"env\", doc.createNode([]));\n seq = doc.get(\"env\") as YAMLSeq;\n }\n\n const existingNames = new Set<string>();\n for (const item of seq.items) {\n const name = (item as YAMLMap)?.get?.(\"name\");\n if (typeof name === \"string\") existingNames.add(name);\n }\n\n const added: string[] = [];\n for (const entry of entries) {\n if (existingNames.has(entry.name)) continue;\n seq.add(doc.createNode({ name: entry.name, valueFrom: entry.valueFrom }));\n added.push(entry.name);\n }\n\n if (added.length > 0) fs.writeFileSync(file, doc.toString());\n return added;\n}\n\n/** Navigates/creates a nested map path, returning the leaf map. */\nfunction ensureMap(\n doc: ReturnType<typeof parseDocument>,\n pathKeys: string[],\n): YAMLMap {\n let node = doc.contents as unknown as YAMLMap;\n const walked: string[] = [];\n for (const key of pathKeys) {\n walked.push(key);\n let child = doc.getIn(walked) as YAMLMap | undefined;\n if (!child || typeof (child as YAMLMap).set !== \"function\") {\n doc.setIn(walked, doc.createNode({}));\n child = doc.getIn(walked) as YAMLMap;\n }\n node = child;\n }\n return node;\n}\n\n/**\n * Additively patches `databricks.yml`: adds bundle `variables`, the app\n * `resources` bindings, and the target-level variable values — each only if\n * not already present. Returns the names added plus whether the file changed\n * (a target-value-only write changes the file without adding any names).\n */\nfunction patchDatabricksYml(\n file: string,\n plan: ConfigPlan,\n): { added: string[]; changed: boolean } {\n if (plan.bundleVariables.length === 0 && plan.resourceBindings.length === 0) {\n return { added: [], changed: false };\n }\n const doc = loadDoc(file);\n const added: string[] = [];\n\n // Top-level bundle variables.\n if (plan.bundleVariables.length > 0) {\n const vars = ensureMap(doc, [\"variables\"]);\n for (const v of plan.bundleVariables) {\n if (vars.has(v.name)) continue;\n const body: Record<string, string> = {};\n if (v.description) body.description = v.description;\n vars.set(v.name, doc.createNode(body));\n added.push(v.name);\n }\n }\n\n // App resource bindings.\n if (plan.resourceBindings.length > 0) {\n const app = ensureMap(doc, [\"resources\", \"apps\", \"app\"]);\n let bindings = app.get(\"resources\") as YAMLSeq | undefined;\n if (!bindings || typeof (bindings as YAMLSeq).add !== \"function\") {\n app.set(\"resources\", doc.createNode([]));\n bindings = app.get(\"resources\") as YAMLSeq;\n }\n const existing = new Set<string>();\n for (const item of bindings.items) {\n const name = (item as YAMLMap)?.get?.(\"name\");\n if (typeof name === \"string\") existing.add(name);\n }\n for (const binding of plan.resourceBindings) {\n if (existing.has(binding.name)) continue;\n bindings.add(doc.createNode(bindingToNode(binding)));\n added.push(binding.name);\n }\n }\n\n // Target-level variable values. Tracked separately from `added` because the\n // binding/top-level var may already exist (e.g. from scaffold) while the\n // target VALUE is still missing — in that case nothing is in `added` yet the\n // file still needs writing to persist the assigned value.\n let wroteTargetValue = false;\n const withValues = plan.bundleVariables.filter((v) => v.value !== undefined);\n if (withValues.length > 0) {\n const targetVars = ensureMap(doc, [\"targets\", \"default\", \"variables\"]);\n for (const v of withValues) {\n if (targetVars.has(v.name)) continue;\n targetVars.set(v.name, v.value);\n wroteTargetValue = true;\n }\n }\n\n const changed = added.length > 0 || wroteTargetValue;\n if (changed) fs.writeFileSync(file, doc.toString());\n return { added, changed };\n}\n\n/**\n * Applies a config plan to `app.yaml` and `databricks.yml` in `cwd` via\n * comment-preserving additive patches. Never overwrites existing entries.\n */\nexport function writeConfig(cwd: string, plan: ConfigPlan): ConfigWriteResult {\n const appAdded = patchAppYaml(path.join(cwd, APP_YAML_FILE), plan.appYamlEnv);\n const db = patchDatabricksYml(path.join(cwd, DATABRICKS_YML_FILE), plan);\n return {\n appYamlChanged: appAdded.length > 0,\n databricksYmlChanged: db.changed,\n added: [...new Set([...appAdded, ...db.added])],\n unverifiedTypes: plan.unverifiedTypes,\n };\n}\n\n/**\n * Runs `databricks bundle validate` as a post-write correctness gate. Returns\n * true when the config validates (or when the CLI is unavailable — a missing\n * CLI shouldn't fail an install). Surfaces validation errors to the user.\n */\nexport function validateBundle(cwd: string, profile?: string): boolean {\n const args = [\"bundle\", \"validate\"];\n if (profile) args.push(\"-p\", profile);\n let result: SpawnSyncReturns<string>;\n try {\n result = spawnSync(\"databricks\", args, { cwd, encoding: \"utf-8\" });\n } catch {\n console.warn(\n pc.yellow(\" Skipped bundle validate (databricks CLI not found).\"),\n );\n return true;\n }\n if (result.error) {\n console.warn(\n pc.yellow(\" Skipped bundle validate (databricks CLI not found).\"),\n );\n return true;\n }\n if (result.status !== 0) {\n console.warn(pc.yellow(\" databricks bundle validate reported issues:\"));\n if (result.stderr) console.warn(result.stderr.trim());\n return false;\n }\n return true;\n}\n\n/** Reports what the config write did, including any unverified-type warnings. */\nexport function reportConfigWrite(result: ConfigWriteResult): void {\n if (result.added.length > 0) {\n console.log(\n `${pc.green(\"Updated deploy config:\")} ${result.added.join(\", \")}`,\n );\n }\n if (result.unverifiedTypes.length > 0) {\n console.warn(\n pc.yellow(\n ` No databricks.yml binding written for: ${result.unverifiedTypes.join(\", \")}. ` +\n \"Add the resource binding manually before deploy.\",\n ),\n );\n }\n}\n\nexport { planHasContent };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
//#region src/cli/commands/registry/constants.ts
|
|
4
|
+
/** shadcn registry namespace consumers reference, e.g. `@databricks-appkit/metric-card`. */
|
|
5
|
+
const REGISTRY_NAMESPACE = "@databricks-appkit";
|
|
6
|
+
/**
|
|
7
|
+
* A plain JS identifier. A plugin's export name is interpolated into the user's
|
|
8
|
+
* server source, so it's validated against this before use — a registry item is
|
|
9
|
+
* untrusted, and anything else could inject code.
|
|
10
|
+
*/
|
|
11
|
+
const JS_IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
12
|
+
/** GitHub repo hosting the registry, and the branch the built items live on. */
|
|
13
|
+
const REGISTRY_REPO = "databricks/appkit-registry";
|
|
14
|
+
const REGISTRY_REF = "main";
|
|
15
|
+
/**
|
|
16
|
+
* Public hosting: once the repo is public, items are fetchable directly from
|
|
17
|
+
* raw.githubusercontent.com with no auth.
|
|
18
|
+
*/
|
|
19
|
+
const PUBLIC_RAW_BASE = `https://raw.githubusercontent.com/${REGISTRY_REPO}/${REGISTRY_REF}`;
|
|
20
|
+
const REGISTRY_ITEM_URL_TEMPLATE = `${PUBLIC_RAW_BASE}/public/r/{name}.json`;
|
|
21
|
+
const REGISTRY_INDEX_URL = `${PUBLIC_RAW_BASE}/registry.json`;
|
|
22
|
+
/**
|
|
23
|
+
* Private/internal hosting: while the repo is internal, files are fetched via
|
|
24
|
+
* the GitHub Contents API with a token. `Accept: application/vnd.github.raw`
|
|
25
|
+
* makes the API return the file bytes directly (the registry-item JSON).
|
|
26
|
+
*/
|
|
27
|
+
const GH_CONTENTS_API = `https://api.github.com/repos/${REGISTRY_REPO}/contents`;
|
|
28
|
+
const REGISTRY_ITEM_API_TEMPLATE = `${GH_CONTENTS_API}/public/r/{name}.json?ref=${REGISTRY_REF}`;
|
|
29
|
+
const REGISTRY_INDEX_API_URL = `${GH_CONTENTS_API}/registry.json?ref=${REGISTRY_REF}`;
|
|
30
|
+
/** Env vars checked (in order) for a token granting read access to the repo. */
|
|
31
|
+
const TOKEN_ENV_VARS = [
|
|
32
|
+
"APPKIT_REGISTRY_TOKEN",
|
|
33
|
+
"GITHUB_TOKEN",
|
|
34
|
+
"GH_TOKEN"
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Resolves a token granting read access to the registry repo: first the env
|
|
38
|
+
* vars in {@link TOKEN_ENV_VARS}, then the GitHub CLI (`gh auth token`) if the
|
|
39
|
+
* user is logged in. Returns null if none are available.
|
|
40
|
+
*/
|
|
41
|
+
function resolveToken(env = process.env) {
|
|
42
|
+
for (const envName of TOKEN_ENV_VARS) {
|
|
43
|
+
const value = env[envName];
|
|
44
|
+
if (value) return {
|
|
45
|
+
envName,
|
|
46
|
+
value
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const res = spawnSync("gh", ["auth", "token"], { encoding: "utf-8" });
|
|
51
|
+
const value = res.status === 0 ? res.stdout.trim() : "";
|
|
52
|
+
if (value) return {
|
|
53
|
+
envName: "gh auth token",
|
|
54
|
+
value
|
|
55
|
+
};
|
|
56
|
+
} catch {}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
export { JS_IDENTIFIER, REGISTRY_INDEX_API_URL, REGISTRY_INDEX_URL, REGISTRY_ITEM_API_TEMPLATE, REGISTRY_ITEM_URL_TEMPLATE, REGISTRY_NAMESPACE, REGISTRY_REPO, resolveToken };
|
|
62
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","names":[],"sources":["../../../../src/cli/commands/registry/constants.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\n\n/** shadcn registry namespace consumers reference, e.g. `@databricks-appkit/metric-card`. */\nexport const REGISTRY_NAMESPACE = \"@databricks-appkit\";\n\n/**\n * A plain JS identifier. A plugin's export name is interpolated into the user's\n * server source, so it's validated against this before use — a registry item is\n * untrusted, and anything else could inject code.\n */\nexport const JS_IDENTIFIER = /^[A-Za-z_$][\\w$]*$/;\n\n/** GitHub repo hosting the registry, and the branch the built items live on. */\nexport const REGISTRY_REPO = \"databricks/appkit-registry\";\nexport const REGISTRY_REF = \"main\";\n\n/**\n * Public hosting: once the repo is public, items are fetchable directly from\n * raw.githubusercontent.com with no auth.\n */\nconst PUBLIC_RAW_BASE = `https://raw.githubusercontent.com/${REGISTRY_REPO}/${REGISTRY_REF}`;\nexport const REGISTRY_ITEM_URL_TEMPLATE = `${PUBLIC_RAW_BASE}/public/r/{name}.json`;\nexport const REGISTRY_INDEX_URL = `${PUBLIC_RAW_BASE}/registry.json`;\n\n/**\n * Private/internal hosting: while the repo is internal, files are fetched via\n * the GitHub Contents API with a token. `Accept: application/vnd.github.raw`\n * makes the API return the file bytes directly (the registry-item JSON).\n */\nconst GH_CONTENTS_API = `https://api.github.com/repos/${REGISTRY_REPO}/contents`;\nexport const REGISTRY_ITEM_API_TEMPLATE = `${GH_CONTENTS_API}/public/r/{name}.json?ref=${REGISTRY_REF}`;\nexport const REGISTRY_INDEX_API_URL = `${GH_CONTENTS_API}/registry.json?ref=${REGISTRY_REF}`;\n\n/** Env vars checked (in order) for a token granting read access to the repo. */\nexport const TOKEN_ENV_VARS = [\n \"APPKIT_REGISTRY_TOKEN\",\n \"GITHUB_TOKEN\",\n \"GH_TOKEN\",\n];\n\nexport interface RegistryToken {\n envName: string;\n value: string;\n}\n\n/**\n * Resolves a token granting read access to the registry repo: first the env\n * vars in {@link TOKEN_ENV_VARS}, then the GitHub CLI (`gh auth token`) if the\n * user is logged in. Returns null if none are available.\n */\nexport function resolveToken(\n env: NodeJS.ProcessEnv = process.env,\n): RegistryToken | null {\n for (const envName of TOKEN_ENV_VARS) {\n const value = env[envName];\n if (value) return { envName, value };\n }\n try {\n const res = spawnSync(\"gh\", [\"auth\", \"token\"], { encoding: \"utf-8\" });\n const value = res.status === 0 ? res.stdout.trim() : \"\";\n if (value) return { envName: \"gh auth token\", value };\n } catch {\n // gh not installed or not on PATH — fall through.\n }\n return null;\n}\n"],"mappings":";;;;AAGA,MAAa,qBAAqB;;;;;;AAOlC,MAAa,gBAAgB;;AAG7B,MAAa,gBAAgB;AAC7B,MAAa,eAAe;;;;;AAM5B,MAAM,kBAAkB,qCAAqC,cAAc,GAAG;AAC9E,MAAa,6BAA6B,GAAG,gBAAgB;AAC7D,MAAa,qBAAqB,GAAG,gBAAgB;;;;;;AAOrD,MAAM,kBAAkB,gCAAgC,cAAc;AACtE,MAAa,6BAA6B,GAAG,gBAAgB,4BAA4B;AACzF,MAAa,yBAAyB,GAAG,gBAAgB,qBAAqB;;AAG9E,MAAa,iBAAiB;CAC5B;CACA;CACA;CACD;;;;;;AAYD,SAAgB,aACd,MAAyB,QAAQ,KACX;AACtB,MAAK,MAAM,WAAW,gBAAgB;EACpC,MAAM,QAAQ,IAAI;AAClB,MAAI,MAAO,QAAO;GAAE;GAAS;GAAO;;AAEtC,KAAI;EACF,MAAM,MAAM,UAAU,MAAM,CAAC,QAAQ,QAAQ,EAAE,EAAE,UAAU,SAAS,CAAC;EACrE,MAAM,QAAQ,IAAI,WAAW,IAAI,IAAI,OAAO,MAAM,GAAG;AACrD,MAAI,MAAO,QAAO;GAAE,SAAS;GAAiB;GAAO;SAC/C;AAGR,QAAO"}
|