@databricks/appkit 0.59.0 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) 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/agents/agents.d.ts.map +1 -1
  64. package/dist/plugins/agents/agents.js +117 -80
  65. package/dist/plugins/agents/agents.js.map +1 -1
  66. package/dist/plugins/agents/manifest.js +10 -0
  67. package/dist/plugins/agents/mlflow.js +123 -0
  68. package/dist/plugins/agents/mlflow.js.map +1 -0
  69. package/dist/plugins/agents/schemas.js +4 -2
  70. package/dist/plugins/agents/schemas.js.map +1 -1
  71. package/dist/plugins/files/plugin.js +1 -2
  72. package/dist/plugins/files/plugin.js.map +1 -1
  73. package/dist/plugins/files/types.d.ts +1 -2
  74. package/dist/plugins/files/types.d.ts.map +1 -1
  75. package/dist/plugins/jobs/types.d.ts +1 -2
  76. package/dist/plugins/jobs/types.d.ts.map +1 -1
  77. package/dist/plugins/lakebase/lakebase.js +1 -2
  78. package/dist/plugins/lakebase/lakebase.js.map +1 -1
  79. package/dist/schemas/manifest.js +4 -4
  80. package/dist/schemas/manifest.js.map +1 -1
  81. package/dist/shared/src/index.d.ts +4 -1
  82. package/dist/shared/src/schemas/manifest.d.ts +6 -6
  83. package/dist/shared/src/workspace-client/client.js +57 -0
  84. package/dist/shared/src/workspace-client/client.js.map +1 -0
  85. package/dist/shared/src/workspace-client/errors.d.ts +2 -0
  86. package/dist/shared/src/workspace-client/errors.js +3 -0
  87. package/dist/shared/src/workspace-client/factory.d.ts +19 -0
  88. package/dist/shared/src/workspace-client/factory.d.ts.map +1 -0
  89. package/dist/shared/src/workspace-client/factory.js +24 -0
  90. package/dist/shared/src/workspace-client/factory.js.map +1 -0
  91. package/dist/shared/src/workspace-client/index.d.ts +4 -0
  92. package/dist/shared/src/workspace-client/legacy.d.ts +31 -0
  93. package/dist/shared/src/workspace-client/legacy.d.ts.map +1 -0
  94. package/dist/shared/src/workspace-client/legacy.js +23 -0
  95. package/dist/shared/src/workspace-client/legacy.js.map +1 -0
  96. package/dist/shared/src/workspace-client/types.d.ts +49 -0
  97. package/dist/shared/src/workspace-client/types.d.ts.map +1 -0
  98. package/dist/type-generator/index.js +1 -2
  99. package/dist/type-generator/index.js.map +1 -1
  100. package/dist/type-generator/query-registry.js +1 -2
  101. package/dist/type-generator/query-registry.js.map +1 -1
  102. package/dist/type-generator/serving/fetcher.js +1 -2
  103. package/dist/type-generator/serving/fetcher.js.map +1 -1
  104. package/dist/type-generator/serving/generator.js +1 -2
  105. package/dist/type-generator/serving/generator.js.map +1 -1
  106. package/dist/type-generator/vite-plugin.js +1 -2
  107. package/dist/type-generator/vite-plugin.js.map +1 -1
  108. package/dist/workspace-client/index.d.ts +4 -3
  109. package/dist/workspace-client/index.js +2 -2
  110. package/dist/workspace-client/legacy.d.ts +9 -4
  111. package/dist/workspace-client/legacy.d.ts.map +1 -1
  112. package/dist/workspace-client/legacy.js +3 -3
  113. package/dist/workspace-client/legacy.js.map +1 -1
  114. package/dist/workspace-client/types.d.ts +2 -2
  115. package/docs/api/appkit/Function.createWorkspaceClient.md +4 -4
  116. package/docs/api/appkit/Interface.WorkspaceClientOptions.md +11 -0
  117. package/package.json +6 -4
  118. package/sbom.cdx.json +1 -1
@@ -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"}
@@ -0,0 +1,116 @@
1
+ import { fieldOrigin, isValidEnvName } from "./requirements.js";
2
+ import dotenv from "dotenv";
3
+
4
+ //#region src/cli/commands/registry/env-reconcile.ts
5
+ /**
6
+ * A `.env` value is a single line: `KEY=VALUE`. A value carrying a CR/LF would
7
+ * write extra lines when serialized, so a malicious static default like
8
+ * `value: "y\nDATABRICKS_HOST=attacker"` could inject an unrelated key (host
9
+ * override → credential exfil). Registry manifests are untrusted, so any value
10
+ * with a line break is rejected rather than written.
11
+ */
12
+ function isSafeEnvValue(value) {
13
+ return !/[\r\n]/.test(value);
14
+ }
15
+ /**
16
+ * Flattens requirement rows into the env vars that belong in local `.env`.
17
+ * Excludes fields with no `env` name and `platform`-origin fields (deploy-time
18
+ * platform injection). Order: required resources first (as given), then optional.
19
+ */
20
+ function collectEnvNeeds(rows) {
21
+ const needs = [];
22
+ const seen = /* @__PURE__ */ new Set();
23
+ const ordered = [...rows.filter((r) => r.required), ...rows.filter((r) => !r.required)];
24
+ for (const row of ordered) for (const field of row.fields) {
25
+ if (!includeInEnv(field)) continue;
26
+ const env = field.env;
27
+ if (seen.has(env)) continue;
28
+ seen.add(env);
29
+ needs.push({
30
+ env,
31
+ resourceType: row.type,
32
+ required: row.required,
33
+ defaultValue: field.value,
34
+ origin: fieldOrigin(field),
35
+ description: field.description
36
+ });
37
+ }
38
+ return needs;
39
+ }
40
+ /** A field belongs in `.env` iff it names a valid env var and isn't platform-injected. */
41
+ function includeInEnv(field) {
42
+ if (!field.env) return false;
43
+ if (!isValidEnvName(field.env)) return false;
44
+ return fieldOrigin(field) !== "platform";
45
+ }
46
+ /**
47
+ * Parses a `.env` file body into a KEY -> value map, using the same `dotenv`
48
+ * parser the app loads `.env` with at runtime so the CLI reads it identically.
49
+ */
50
+ function parseEnv(content) {
51
+ return dotenv.parse(content);
52
+ }
53
+ /**
54
+ * Serializes new env entries for appending to a `.env` file. Only keys not
55
+ * already present are emitted; existing keys are never rewritten (we don't
56
+ * clobber user edits). Returns the text to append (empty if nothing new).
57
+ */
58
+ function serializeEnvAppend(entries) {
59
+ if (entries.length === 0) return "";
60
+ const lines = [];
61
+ for (const e of entries) {
62
+ if (e.comment) lines.push(`# ${e.comment}`);
63
+ lines.push(`${e.env}=${e.value}`);
64
+ }
65
+ return `${lines.join("\n")}\n`;
66
+ }
67
+ /**
68
+ * Reconciles the needed env vars against what's already in `.env`.
69
+ * - Already-set keys are reported as "already-set" and never overwritten.
70
+ * - static-origin defaults are used without invoking `provide`.
71
+ * - Everything else defers to `provide`; a returned undefined means skip.
72
+ */
73
+ async function reconcileEnv(needs, opts) {
74
+ const resolutions = [];
75
+ for (const need of needs) {
76
+ const current = opts.existing[need.env];
77
+ if (current !== void 0 && current !== "") {
78
+ resolutions.push({
79
+ env: need.env,
80
+ value: current,
81
+ status: "already-set"
82
+ });
83
+ continue;
84
+ }
85
+ if (need.defaultValue !== void 0) {
86
+ if (!isSafeEnvValue(need.defaultValue)) {
87
+ resolutions.push({
88
+ env: need.env,
89
+ status: "skipped"
90
+ });
91
+ continue;
92
+ }
93
+ resolutions.push({
94
+ env: need.env,
95
+ value: need.defaultValue,
96
+ status: "written"
97
+ });
98
+ continue;
99
+ }
100
+ const value = await opts.provide(need);
101
+ if (value === void 0 || value === "" || !isSafeEnvValue(value)) resolutions.push({
102
+ env: need.env,
103
+ status: "skipped"
104
+ });
105
+ else resolutions.push({
106
+ env: need.env,
107
+ value,
108
+ status: "written"
109
+ });
110
+ }
111
+ return resolutions;
112
+ }
113
+
114
+ //#endregion
115
+ export { collectEnvNeeds, parseEnv, reconcileEnv, serializeEnvAppend };
116
+ //# sourceMappingURL=env-reconcile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-reconcile.js","names":[],"sources":["../../../../src/cli/commands/registry/env-reconcile.ts"],"sourcesContent":["import dotenv from \"dotenv\";\nimport {\n fieldOrigin,\n isValidEnvName,\n type RequirementField,\n type ResourceRequirementRow,\n} from \"./requirements\";\n\n/**\n * A single env var that an installed plugin needs in the local `.env`.\n * `platform`-origin fields are excluded upstream — they are injected by\n * Databricks Apps at deploy time and never belong in a hand-managed `.env`.\n */\nexport interface EnvNeed {\n env: string;\n resourceType: string;\n required: boolean;\n /** static-origin default value, pre-filled without prompting. */\n defaultValue?: string;\n origin?: string;\n description?: string;\n}\n\n/** The resolved decision for one env var after reconciliation. */\nexport interface EnvResolution {\n env: string;\n /** The value to write, or undefined when skipped / left unset. */\n value?: string;\n status: \"written\" | \"already-set\" | \"skipped\";\n}\n\n/**\n * A `.env` value is a single line: `KEY=VALUE`. A value carrying a CR/LF would\n * write extra lines when serialized, so a malicious static default like\n * `value: \"y\\nDATABRICKS_HOST=attacker\"` could inject an unrelated key (host\n * override → credential exfil). Registry manifests are untrusted, so any value\n * with a line break is rejected rather than written.\n */\nexport function isSafeEnvValue(value: string): boolean {\n return !/[\\r\\n]/.test(value);\n}\n\n/**\n * Flattens requirement rows into the env vars that belong in local `.env`.\n * Excludes fields with no `env` name and `platform`-origin fields (deploy-time\n * platform injection). Order: required resources first (as given), then optional.\n */\nexport function collectEnvNeeds(rows: ResourceRequirementRow[]): EnvNeed[] {\n const needs: EnvNeed[] = [];\n const seen = new Set<string>();\n const ordered = [\n ...rows.filter((r) => r.required),\n ...rows.filter((r) => !r.required),\n ];\n for (const row of ordered) {\n for (const field of row.fields) {\n if (!includeInEnv(field)) continue;\n const env = field.env as string;\n if (seen.has(env)) continue;\n seen.add(env);\n needs.push({\n env,\n resourceType: row.type,\n required: row.required,\n defaultValue: field.value,\n origin: fieldOrigin(field),\n description: field.description,\n });\n }\n }\n return needs;\n}\n\n/** A field belongs in `.env` iff it names a valid env var and isn't platform-injected. */\nfunction includeInEnv(field: RequirementField): boolean {\n if (!field.env) return false;\n // The env name comes from an untrusted manifest and is written as `NAME=value`;\n // a malformed name (e.g. one containing a newline) could inject an extra .env\n // line, so drop anything that isn't a plain env identifier.\n if (!isValidEnvName(field.env)) return false;\n // Origin is derived from the authored contract (localOnly/value/resolve) so\n // registry-fetched manifests without a computed origin classify correctly.\n return fieldOrigin(field) !== \"platform\";\n}\n\n/**\n * Parses a `.env` file body into a KEY -> value map, using the same `dotenv`\n * parser the app loads `.env` with at runtime so the CLI reads it identically.\n */\nexport function parseEnv(content: string): Record<string, string> {\n return dotenv.parse(content);\n}\n\n/**\n * Serializes new env entries for appending to a `.env` file. Only keys not\n * already present are emitted; existing keys are never rewritten (we don't\n * clobber user edits). Returns the text to append (empty if nothing new).\n */\nexport function serializeEnvAppend(\n entries: Array<{ env: string; value: string; comment?: string }>,\n): string {\n if (entries.length === 0) return \"\";\n const lines: string[] = [];\n for (const e of entries) {\n if (e.comment) lines.push(`# ${e.comment}`);\n lines.push(`${e.env}=${e.value}`);\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n\n/** Provides a value for an env need, or undefined to skip it. */\nexport type ValueProvider = (need: EnvNeed) => Promise<string | undefined>;\n\nexport interface ReconcileOptions {\n /** Existing parsed `.env` values (keys already present are left untouched). */\n existing: Record<string, string>;\n /** Resolves a value for each unset need (prompt in interactive, flag in CI). */\n provide: ValueProvider;\n}\n\n/**\n * Reconciles the needed env vars against what's already in `.env`.\n * - Already-set keys are reported as \"already-set\" and never overwritten.\n * - static-origin defaults are used without invoking `provide`.\n * - Everything else defers to `provide`; a returned undefined means skip.\n */\nexport async function reconcileEnv(\n needs: EnvNeed[],\n opts: ReconcileOptions,\n): Promise<EnvResolution[]> {\n const resolutions: EnvResolution[] = [];\n for (const need of needs) {\n const current = opts.existing[need.env];\n if (current !== undefined && current !== \"\") {\n // Carry the existing value so callers can still feed it into deploy\n // config (databricks.yml target variables) — the var is set in .env but\n // its bundle binding still needs the value assigned.\n resolutions.push({\n env: need.env,\n value: current,\n status: \"already-set\",\n });\n continue;\n }\n if (need.defaultValue !== undefined) {\n // Static default from an untrusted manifest — refuse a value that would\n // inject extra `.env` lines rather than silently writing it.\n if (!isSafeEnvValue(need.defaultValue)) {\n resolutions.push({ env: need.env, status: \"skipped\" });\n continue;\n }\n resolutions.push({\n env: need.env,\n value: need.defaultValue,\n status: \"written\",\n });\n continue;\n }\n const value = await opts.provide(need);\n if (value === undefined || value === \"\" || !isSafeEnvValue(value)) {\n resolutions.push({ env: need.env, status: \"skipped\" });\n } else {\n resolutions.push({ env: need.env, value, status: \"written\" });\n }\n }\n return resolutions;\n}\n"],"mappings":";;;;;;;;;;;AAsCA,SAAgB,eAAe,OAAwB;AACrD,QAAO,CAAC,SAAS,KAAK,MAAM;;;;;;;AAQ9B,SAAgB,gBAAgB,MAA2C;CACzE,MAAM,QAAmB,EAAE;CAC3B,MAAM,uBAAO,IAAI,KAAa;CAC9B,MAAM,UAAU,CACd,GAAG,KAAK,QAAQ,MAAM,EAAE,SAAS,EACjC,GAAG,KAAK,QAAQ,MAAM,CAAC,EAAE,SAAS,CACnC;AACD,MAAK,MAAM,OAAO,QAChB,MAAK,MAAM,SAAS,IAAI,QAAQ;AAC9B,MAAI,CAAC,aAAa,MAAM,CAAE;EAC1B,MAAM,MAAM,MAAM;AAClB,MAAI,KAAK,IAAI,IAAI,CAAE;AACnB,OAAK,IAAI,IAAI;AACb,QAAM,KAAK;GACT;GACA,cAAc,IAAI;GAClB,UAAU,IAAI;GACd,cAAc,MAAM;GACpB,QAAQ,YAAY,MAAM;GAC1B,aAAa,MAAM;GACpB,CAAC;;AAGN,QAAO;;;AAIT,SAAS,aAAa,OAAkC;AACtD,KAAI,CAAC,MAAM,IAAK,QAAO;AAIvB,KAAI,CAAC,eAAe,MAAM,IAAI,CAAE,QAAO;AAGvC,QAAO,YAAY,MAAM,KAAK;;;;;;AAOhC,SAAgB,SAAS,SAAyC;AAChE,QAAO,OAAO,MAAM,QAAQ;;;;;;;AAQ9B,SAAgB,mBACd,SACQ;AACR,KAAI,QAAQ,WAAW,EAAG,QAAO;CACjC,MAAM,QAAkB,EAAE;AAC1B,MAAK,MAAM,KAAK,SAAS;AACvB,MAAI,EAAE,QAAS,OAAM,KAAK,KAAK,EAAE,UAAU;AAC3C,QAAM,KAAK,GAAG,EAAE,IAAI,GAAG,EAAE,QAAQ;;AAEnC,QAAO,GAAG,MAAM,KAAK,KAAK,CAAC;;;;;;;;AAmB7B,eAAsB,aACpB,OACA,MAC0B;CAC1B,MAAM,cAA+B,EAAE;AACvC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,KAAK,SAAS,KAAK;AACnC,MAAI,YAAY,UAAa,YAAY,IAAI;AAI3C,eAAY,KAAK;IACf,KAAK,KAAK;IACV,OAAO;IACP,QAAQ;IACT,CAAC;AACF;;AAEF,MAAI,KAAK,iBAAiB,QAAW;AAGnC,OAAI,CAAC,eAAe,KAAK,aAAa,EAAE;AACtC,gBAAY,KAAK;KAAE,KAAK,KAAK;KAAK,QAAQ;KAAW,CAAC;AACtD;;AAEF,eAAY,KAAK;IACf,KAAK,KAAK;IACV,OAAO,KAAK;IACZ,QAAQ;IACT,CAAC;AACF;;EAEF,MAAM,QAAQ,MAAM,KAAK,QAAQ,KAAK;AACtC,MAAI,UAAU,UAAa,UAAU,MAAM,CAAC,eAAe,MAAM,CAC/D,aAAY,KAAK;GAAE,KAAK,KAAK;GAAK,QAAQ;GAAW,CAAC;MAEtD,aAAY,KAAK;GAAE,KAAK,KAAK;GAAK;GAAO,QAAQ;GAAW,CAAC;;AAGjE,QAAO"}