@databricks/appkit 0.61.1 → 0.62.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 (194) hide show
  1. package/CLAUDE.md +1 -0
  2. package/dist/agents/databricks.d.ts.map +1 -1
  3. package/dist/agents/databricks.js.map +1 -1
  4. package/dist/agents/supervisor-api.d.ts.map +1 -1
  5. package/dist/agents/supervisor-api.js.map +1 -1
  6. package/dist/app/index.d.ts.map +1 -1
  7. package/dist/app/index.js.map +1 -1
  8. package/dist/appkit/package.js +1 -1
  9. package/dist/cache/index.d.ts.map +1 -1
  10. package/dist/cache/index.js.map +1 -1
  11. package/dist/cache/storage/memory.js.map +1 -1
  12. package/dist/cache/storage/persistent.js.map +1 -1
  13. package/dist/cli/commands/codemod/index.js.map +1 -1
  14. package/dist/cli/commands/codemod/on-plugins-ready.js.map +1 -1
  15. package/dist/cli/commands/docs.js.map +1 -1
  16. package/dist/cli/commands/doctor/bundle.js.map +1 -1
  17. package/dist/cli/commands/doctor/index.js.map +1 -1
  18. package/dist/cli/commands/doctor/report.js.map +1 -1
  19. package/dist/cli/commands/doctor/resolve-targets.js.map +1 -1
  20. package/dist/cli/commands/doctor/run.js.map +1 -1
  21. package/dist/cli/commands/generate-types.js.map +1 -1
  22. package/dist/cli/commands/lint.js.map +1 -1
  23. package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
  24. package/dist/cli/commands/plugin/create/create.js.map +1 -1
  25. package/dist/cli/commands/plugin/create/prompt-resource.js.map +1 -1
  26. package/dist/cli/commands/plugin/create/scaffold.js.map +1 -1
  27. package/dist/cli/commands/plugin/index.js.map +1 -1
  28. package/dist/cli/commands/plugin/list/list.js.map +1 -1
  29. package/dist/cli/commands/plugin/promote/promote.js.map +1 -1
  30. package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
  31. package/dist/cli/commands/plugin/validate/validate-manifest.js.map +1 -1
  32. package/dist/cli/commands/plugin/validate/validate.js.map +1 -1
  33. package/dist/cli/commands/registry/add.js.map +1 -1
  34. package/dist/cli/commands/registry/client.js.map +1 -1
  35. package/dist/cli/commands/registry/config-writer.js.map +1 -1
  36. package/dist/cli/commands/registry/env-reconcile.js.map +1 -1
  37. package/dist/cli/commands/registry/env-writer.js.map +1 -1
  38. package/dist/cli/commands/registry/index.js.map +1 -1
  39. package/dist/cli/commands/registry/info.js.map +1 -1
  40. package/dist/cli/commands/registry/list.js.map +1 -1
  41. package/dist/cli/commands/registry/requirements.js.map +1 -1
  42. package/dist/cli/commands/registry/server-register.js.map +1 -1
  43. package/dist/cli/commands/registry/workspace-picker.js.map +1 -1
  44. package/dist/cli/commands/setup.js.map +1 -1
  45. package/dist/cli/index.js.map +1 -1
  46. package/dist/connectors/files/client.js.map +1 -1
  47. package/dist/connectors/lakebase/index.d.ts.map +1 -1
  48. package/dist/connectors/lakebase/index.js.map +1 -1
  49. package/dist/connectors/lakebase/pool-manager.d.ts.map +1 -1
  50. package/dist/connectors/lakebase/pool-manager.js.map +1 -1
  51. package/dist/connectors/lakebase/routing-pool.d.ts.map +1 -1
  52. package/dist/connectors/lakebase/routing-pool.js.map +1 -1
  53. package/dist/connectors/mcp/client.d.ts.map +1 -1
  54. package/dist/connectors/mcp/client.js.map +1 -1
  55. package/dist/connectors/sql-warehouse/client.js.map +1 -1
  56. package/dist/context/client-options.js.map +1 -1
  57. package/dist/context/execution-context.d.ts.map +1 -1
  58. package/dist/context/execution-context.js.map +1 -1
  59. package/dist/context/index.d.ts +1 -1
  60. package/dist/context/service-context.d.ts +55 -3
  61. package/dist/context/service-context.d.ts.map +1 -1
  62. package/dist/context/service-context.js.map +1 -1
  63. package/dist/core/agent/build-toolkit.js.map +1 -1
  64. package/dist/core/agent/load-agents.d.ts.map +1 -1
  65. package/dist/core/agent/load-agents.js.map +1 -1
  66. package/dist/core/agent/run-agent.d.ts.map +1 -1
  67. package/dist/core/agent/run-agent.js.map +1 -1
  68. package/dist/core/agent/toolkit-resolver.js.map +1 -1
  69. package/dist/core/agent/tools/define-tool.d.ts.map +1 -1
  70. package/dist/core/agent/tools/define-tool.js.map +1 -1
  71. package/dist/core/agent/tools/tool.d.ts.map +1 -1
  72. package/dist/core/agent/tools/tool.js.map +1 -1
  73. package/dist/core/agent/types.d.ts.map +1 -1
  74. package/dist/core/agent/types.js.map +1 -1
  75. package/dist/core/appkit.d.ts.map +1 -1
  76. package/dist/core/appkit.js.map +1 -1
  77. package/dist/core/lifecycle-manager.js.map +1 -1
  78. package/dist/core/plugin-context.d.ts +13 -0
  79. package/dist/core/plugin-context.d.ts.map +1 -1
  80. package/dist/core/plugin-context.js +12 -1
  81. package/dist/core/plugin-context.js.map +1 -1
  82. package/dist/errors/configuration.d.ts.map +1 -1
  83. package/dist/errors/configuration.js.map +1 -1
  84. package/dist/logging/logger.js.map +1 -1
  85. package/dist/logging/wide-event-emitter.js.map +1 -1
  86. package/dist/plugin/dev-reader.d.ts.map +1 -1
  87. package/dist/plugin/dev-reader.js.map +1 -1
  88. package/dist/plugin/interceptors/cache.js.map +1 -1
  89. package/dist/plugin/interceptors/retry.js.map +1 -1
  90. package/dist/plugin/interceptors/telemetry.js.map +1 -1
  91. package/dist/plugin/plugin.d.ts +2 -2
  92. package/dist/plugin/plugin.d.ts.map +1 -1
  93. package/dist/plugin/plugin.js.map +1 -1
  94. package/dist/plugins/agents/agents.d.ts.map +1 -1
  95. package/dist/plugins/agents/agents.js.map +1 -1
  96. package/dist/plugins/agents/event-translator.js.map +1 -1
  97. package/dist/plugins/agents/thread-store.js.map +1 -1
  98. package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
  99. package/dist/plugins/ai-search/ai-search.js.map +1 -1
  100. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  101. package/dist/plugins/analytics/analytics.js.map +1 -1
  102. package/dist/plugins/analytics/mv/cache.js.map +1 -1
  103. package/dist/plugins/analytics/mv/constants.js.map +1 -1
  104. package/dist/plugins/analytics/mv/formatters.js.map +1 -1
  105. package/dist/plugins/analytics/mv/metadata.js.map +1 -1
  106. package/dist/plugins/analytics/mv/registry.js.map +1 -1
  107. package/dist/plugins/analytics/mv/schemas.js.map +1 -1
  108. package/dist/plugins/analytics/query.js.map +1 -1
  109. package/dist/plugins/analytics/result-delivery.js.map +1 -1
  110. package/dist/plugins/files/helpers.js.map +1 -1
  111. package/dist/plugins/files/plugin.d.ts.map +1 -1
  112. package/dist/plugins/files/plugin.js.map +1 -1
  113. package/dist/plugins/files/types.d.ts.map +1 -1
  114. package/dist/plugins/genie/genie.d.ts.map +1 -1
  115. package/dist/plugins/genie/genie.js.map +1 -1
  116. package/dist/plugins/jobs/plugin.d.ts.map +1 -1
  117. package/dist/plugins/jobs/plugin.js.map +1 -1
  118. package/dist/plugins/jobs/types.d.ts.map +1 -1
  119. package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
  120. package/dist/plugins/lakebase/lakebase.js.map +1 -1
  121. package/dist/plugins/lakebase/types.d.ts.map +1 -1
  122. package/dist/plugins/server/base-server.js.map +1 -1
  123. package/dist/plugins/server/client-config-sanitizer.js.map +1 -1
  124. package/dist/plugins/server/index.d.ts.map +1 -1
  125. package/dist/plugins/server/index.js.map +1 -1
  126. package/dist/plugins/server/react-source-loc-vite-plugin.js.map +1 -1
  127. package/dist/plugins/server/remote-tunnel/remote-tunnel-controller.js.map +1 -1
  128. package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
  129. package/dist/plugins/server/static-server.js.map +1 -1
  130. package/dist/plugins/server/utils.js.map +1 -1
  131. package/dist/plugins/server/vite-dev-server.js.map +1 -1
  132. package/dist/plugins/serving/schema-filter.js.map +1 -1
  133. package/dist/plugins/serving/serving.d.ts.map +1 -1
  134. package/dist/plugins/serving/serving.js.map +1 -1
  135. package/dist/plugins/ui-variants/index.js.map +1 -1
  136. package/dist/registry/manifest-loader.d.ts.map +1 -1
  137. package/dist/registry/manifest-loader.js.map +1 -1
  138. package/dist/registry/resource-registry.d.ts.map +1 -1
  139. package/dist/registry/resource-registry.js.map +1 -1
  140. package/dist/registry/types.d.ts.map +1 -1
  141. package/dist/registry/types.js.map +1 -1
  142. package/dist/schemas/manifest.d.ts.map +1 -1
  143. package/dist/schemas/manifest.js.map +1 -1
  144. package/dist/schemas/metric-fqn.js.map +1 -1
  145. package/dist/shared/src/plugin.d.ts.map +1 -1
  146. package/dist/shared/src/schemas/manifest.d.ts.map +1 -1
  147. package/dist/shared/src/schemas/metric-fqn.js.map +1 -1
  148. package/dist/shared/src/schemas/metric-metadata-bundle.js.map +1 -1
  149. package/dist/shared/src/schemas/metric-source.js.map +1 -1
  150. package/dist/shared/src/sse/analytics.js.map +1 -1
  151. package/dist/shared/src/workspace-client/legacy.js.map +1 -1
  152. package/dist/shared/src/workspace-client/types.d.ts.map +1 -1
  153. package/dist/stream/sse-writer.js.map +1 -1
  154. package/dist/stream/stream-manager.d.ts.map +1 -1
  155. package/dist/stream/stream-manager.js.map +1 -1
  156. package/dist/stream/types.js.map +1 -1
  157. package/dist/telemetry/instrumentations.js.map +1 -1
  158. package/dist/telemetry/telemetry-manager.js.map +1 -1
  159. package/dist/telemetry/telemetry-provider.js.map +1 -1
  160. package/dist/telemetry/trace-sampler.js.map +1 -1
  161. package/dist/testing/expect-stream.d.ts +111 -0
  162. package/dist/testing/expect-stream.d.ts.map +1 -0
  163. package/dist/testing/expect-stream.js +154 -0
  164. package/dist/testing/expect-stream.js.map +1 -0
  165. package/dist/testing/fixtures.d.ts +257 -0
  166. package/dist/testing/fixtures.d.ts.map +1 -0
  167. package/dist/testing/fixtures.js +385 -0
  168. package/dist/testing/fixtures.js.map +1 -0
  169. package/dist/testing/index.d.ts +5 -0
  170. package/dist/testing/index.js +5 -0
  171. package/dist/testing/test-plugin-context.d.ts +158 -0
  172. package/dist/testing/test-plugin-context.d.ts.map +1 -0
  173. package/dist/testing/test-plugin-context.js +139 -0
  174. package/dist/testing/test-plugin-context.js.map +1 -0
  175. package/dist/type-generator/cache.js.map +1 -1
  176. package/dist/type-generator/index.js.map +1 -1
  177. package/dist/type-generator/migration.js.map +1 -1
  178. package/dist/type-generator/mv-registry/config.js.map +1 -1
  179. package/dist/type-generator/query-registry.js.map +1 -1
  180. package/dist/type-generator/serving/cache.js.map +1 -1
  181. package/dist/type-generator/serving/generator.js.map +1 -1
  182. package/dist/type-generator/serving/server-file-extractor.d.ts.map +1 -1
  183. package/dist/type-generator/serving/server-file-extractor.js.map +1 -1
  184. package/dist/type-generator/serving/vite-plugin.d.ts.map +1 -1
  185. package/dist/type-generator/serving/vite-plugin.js.map +1 -1
  186. package/dist/type-generator/vite-plugin.d.ts.map +1 -1
  187. package/dist/type-generator/vite-plugin.js.map +1 -1
  188. package/dist/workspace-client/legacy.js.map +1 -1
  189. package/dist/workspace-client/types.d.ts.map +1 -1
  190. package/docs/plugins/testing.md +245 -0
  191. package/llms.txt +1 -0
  192. package/package.json +15 -2
  193. package/sbom.cdx.json +1 -1
  194. package/skills/appkit-ui-variants/SKILL.md +1 -1
@@ -1 +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"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../../../../src/cli/commands/registry/client.ts"],"sourcesContent":["import process from \"node:process\";\n\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":";;;;;AA6BA,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"}
@@ -1 +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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"}
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\";\n\nimport pc from \"picocolors\";\nimport { parseDocument, type YAMLMap, type YAMLSeq } from \"yaml\";\n\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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"}
@@ -1 +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"}
1
+ {"version":3,"file":"env-reconcile.js","names":[],"sources":["../../../../src/cli/commands/registry/env-reconcile.ts"],"sourcesContent":["import dotenv from \"dotenv\";\n\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":";;;;;;;;;;;AAuCA,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"}
@@ -1 +1 @@
1
- {"version":3,"file":"env-writer.js","names":[],"sources":["../../../../src/cli/commands/registry/env-writer.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { autocomplete, isCancel, select, text } from \"@clack/prompts\";\nimport pc from \"picocolors\";\nimport type { BindingValueNeed } from \"./config-plan\";\nimport {\n collectEnvNeeds,\n type EnvNeed,\n type EnvResolution,\n parseEnv,\n reconcileEnv,\n serializeEnvAppend,\n type ValueProvider,\n} from \"./env-reconcile\";\nimport type { ResourceRequirementRow } from \"./requirements\";\nimport {\n composeResourceId,\n isFlatListable,\n isParentContext,\n listParentContextStep,\n listWorkspaceResources,\n parentContextDepth,\n} from \"./workspace-picker\";\n\nexport interface EnvSyncOptions {\n /** Directory holding `.env` / `.env.example` (the app root). */\n cwd: string;\n /** true = never prompt (agent/CI). Uses flag values or leaves unset. */\n nonInteractive: boolean;\n /** Pre-supplied env values from flags, e.g. { DATABRICKS_WAREHOUSE_ID: \"abc\" }. */\n values?: Record<string, string>;\n /** Databricks profile for the workspace picker (else the CLI default). */\n profile?: string;\n}\n\n/** Sentinel select value meaning \"let me type the id myself\". */\nconst MANUAL = \"__manual__\";\n\n/**\n * Max resources shown in a picker select. Real workspaces can have thousands\n * (e.g. 5000+ SQL warehouses); an unbounded select is unusable. Beyond this we\n * show the first N and log how many were hidden — never silently drop — and the\n * \"Enter manually\" option always lets the user type an id the list omits.\n */\nconst PICKER_LIMIT = 25;\n\n/** Reads a `.env`-style file into a map; empty when the file is absent. */\nfunction readEnvFile(file: string): Record<string, string> {\n if (!fs.existsSync(file)) return {};\n return parseEnv(fs.readFileSync(file, \"utf-8\"));\n}\n\n/** Appends text to a file, creating it (with a trailing newline) if needed. */\nfunction appendToFile(file: string, text: string): void {\n if (text === \"\") return;\n if (fs.existsSync(file)) {\n const existing = fs.readFileSync(file, \"utf-8\");\n const sep = existing.length > 0 && !existing.endsWith(\"\\n\") ? \"\\n\" : \"\";\n fs.writeFileSync(file, existing + sep + text);\n } else {\n fs.writeFileSync(file, text);\n }\n}\n\n/**\n * Caps a choice list to {@link PICKER_LIMIT} for display, logging how many were\n * hidden so the truncation is never silent. The caller always appends an\n * \"Enter manually\" option, so an omitted resource is still reachable.\n */\nexport function capChoices<T>(\n choices: T[],\n resourceType: string,\n limit = PICKER_LIMIT,\n): T[] {\n if (choices.length <= limit) return choices;\n console.log(\n pc.dim(\n ` ${choices.length} ${resourceType}s found; showing first ${limit}. ` +\n 'Use \"Enter manually\" if yours is not listed.',\n ),\n );\n return choices.slice(0, limit);\n}\n\n/** Free-text prompt for one env need; undefined to skip. */\nasync function promptText(need: EnvNeed): Promise<string | undefined> {\n const tag = need.required ? \"required\" : \"optional\";\n const answer = await text({\n message: `${need.env} (${need.resourceType}, ${tag})`,\n placeholder: need.description ?? \"leave blank to skip\",\n });\n if (isCancel(answer)) return undefined;\n const value = (answer ?? \"\").trim();\n return value === \"\" ? undefined : value;\n}\n\n/** Presents one workspace list as a select; MANUAL/cancel handled by caller. */\nasync function selectFrom(\n message: string,\n choices: { value: string; label: string }[],\n): Promise<string | typeof MANUAL | null> {\n const picked = await select({\n message,\n options: [...choices, { value: MANUAL, label: \"Enter manually / skip\" }],\n });\n if (isCancel(picked)) return null;\n return String(picked) as string | typeof MANUAL;\n}\n\n/**\n * Type-to-filter picker over the full choice list (no cap): the user searches\n * by name/id as they type. Appends \"Enter manually\" so an omitted value is\n * still reachable. Returns MANUAL to fall through to free-text, or null on\n * cancel.\n */\nasync function autocompleteFrom(\n message: string,\n choices: { value: string; label: string }[],\n): Promise<string | typeof MANUAL | null> {\n const picked = await autocomplete({\n message,\n options: [...choices, { value: MANUAL, label: \"Enter manually / skip\" }],\n placeholder: \"type to search…\",\n });\n if (isCancel(picked)) return null;\n return String(picked) as string | typeof MANUAL;\n}\n\n/**\n * Drill-down picker for parent-context types (volume→catalog/schema,\n * secret→scope, vector_search_index→endpoint). Walks each step, listing the\n * next level from the prior pick. Returns the final resource id, or undefined\n * to fall back to free-text (on cancel, empty level, or MANUAL at any step).\n */\nasync function pickParentContext(\n need: EnvNeed,\n profile: string | undefined,\n): Promise<string | undefined> {\n const depth = parentContextDepth(need.resourceType);\n const picks: string[] = [];\n for (let i = 0; i < depth; i++) {\n const step = listParentContextStep(need.resourceType, i, picks, profile);\n if (!step || step.choices.length === 0) {\n console.log(\n pc.dim(\n ` No ${step?.key ?? need.resourceType} found — enter the id manually.`,\n ),\n );\n return undefined;\n }\n const picked = await selectFrom(\n `${need.env} — pick a ${step.key}`,\n capChoices(step.choices, step.key),\n );\n if (picked === null || picked === MANUAL) return undefined;\n picks.push(picked);\n }\n // Compose the id from the picks: most types end on a self-qualified id, but a\n // secret needs both scope and key (scope/key).\n return composeResourceId(need.resourceType, picks);\n}\n\n/**\n * Builds the value provider. Precedence: --env flag, then (interactive only) a\n * workspace picker — flat select for flat-listable types, drill-down for\n * parent-context types — else a free-text prompt. The picker degrades to\n * free-text whenever the workspace can't be listed (no profile, offline, auth\n * error, empty) so it never hard-fails.\n */\nfunction makeProvider(opts: EnvSyncOptions): ValueProvider {\n return async (need: EnvNeed) => {\n const fromFlag = opts.values?.[need.env];\n if (fromFlag !== undefined) return fromFlag;\n if (opts.nonInteractive) return undefined;\n\n if (isFlatListable(need.resourceType)) {\n const { choices, truncated, error } = await listWorkspaceResources(\n need.resourceType,\n opts.profile,\n );\n if (choices.length > 0) {\n if (truncated) {\n console.log(\n pc.dim(\n ` Showing the first ${choices.length} ${need.resourceType}s; use \"Enter manually\" if yours isn't listed.`,\n ),\n );\n }\n const picked = await autocompleteFrom(\n `${need.env} — search ${need.resourceType}s`,\n choices,\n );\n if (picked === null) return undefined;\n if (picked !== MANUAL) return picked;\n // fall through to free-text\n } else if (error) {\n // Listing failed (usually auth/profile) — say so, don't pretend the\n // workspace is empty, and point at the fix.\n console.log(\n pc.yellow(\n ` Couldn't list ${need.resourceType}s from the workspace (${error}).`,\n ),\n );\n console.log(\n pc.dim(\n \" Enter an id manually, or re-run with --profile <name> (or set DATABRICKS_CONFIG_PROFILE) so the picker can reach the workspace.\",\n ),\n );\n } else {\n console.log(\n pc.dim(\n ` No ${need.resourceType} found in the workspace — enter an id manually.`,\n ),\n );\n }\n } else if (isParentContext(need.resourceType)) {\n const picked = await pickParentContext(need, opts.profile);\n if (picked !== undefined) return picked;\n // fall through to free-text\n }\n\n return promptText(need);\n };\n}\n\n/**\n * Reconciles a plugin's declared resource env vars into the app's local `.env`\n * (and mirrors variable names into `.env.example`). Never overwrites keys the\n * user already set; skips platform-injected fields. Returns the per-var\n * resolutions so callers can report what happened.\n */\nexport async function syncEnv(\n rows: ResourceRequirementRow[],\n opts: EnvSyncOptions,\n): Promise<EnvResolution[]> {\n const needs = collectEnvNeeds(rows);\n if (needs.length === 0) return [];\n\n const envPath = path.join(opts.cwd, \".env\");\n const examplePath = path.join(opts.cwd, \".env.example\");\n const existing = readEnvFile(envPath);\n\n const resolutions = await reconcileEnv(needs, {\n existing,\n provide: makeProvider(opts),\n });\n\n const written = resolutions.filter(\n (r): r is EnvResolution & { value: string } =>\n r.status === \"written\" && r.value !== undefined,\n );\n appendToFile(\n envPath,\n serializeEnvAppend(written.map((r) => ({ env: r.env, value: r.value }))),\n );\n\n // .env.example carries the variable names (no secret values), and only for\n // vars not already documented there.\n const exampleExisting = readEnvFile(examplePath);\n const newExampleKeys = needs.filter((n) => !(n.env in exampleExisting));\n appendToFile(\n examplePath,\n serializeEnvAppend(newExampleKeys.map((n) => ({ env: n.env, value: \"\" }))),\n );\n\n return resolutions;\n}\n\n/**\n * Prompts for the {@link BindingValueNeed}s that `.env` reconciliation can't\n * collect, returning a `fieldKey -> value` map for buildConfigPlan. Values are\n * not written to `.env` (these fields have no env var). Non-interactive mode\n * uses `values[fieldKey]` if provided, else leaves the field unset.\n */\nexport async function collectBindingValues(\n needs: BindingValueNeed[],\n opts: EnvSyncOptions,\n): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n for (const need of needs) {\n const fromFlag = opts.values?.[need.fieldKey];\n if (fromFlag !== undefined) {\n out[need.fieldKey] = fromFlag;\n continue;\n }\n if (opts.nonInteractive) continue;\n const answer = await text({\n message: `${need.fieldKey} (${need.resourceType}) — required for databricks.yml`,\n placeholder: need.description ?? \"leave blank to set before deploy\",\n });\n if (isCancel(answer)) continue;\n const value = (answer ?? \"\").trim();\n if (value !== \"\") out[need.fieldKey] = value;\n }\n return out;\n}\n\n/** Prints a concise summary of what env reconciliation did. */\nexport function reportEnvResolutions(resolutions: EnvResolution[]): void {\n if (resolutions.length === 0) return;\n const written = resolutions.filter((r) => r.status === \"written\");\n const already = resolutions.filter((r) => r.status === \"already-set\");\n const skipped = resolutions.filter((r) => r.status === \"skipped\");\n\n if (written.length > 0) {\n console.log(\n `${pc.green(\"Wrote to .env:\")} ${written.map((r) => r.env).join(\", \")}`,\n );\n }\n if (already.length > 0) {\n console.log(pc.dim(`Already set: ${already.map((r) => r.env).join(\", \")}`));\n }\n if (skipped.length > 0) {\n console.log(\n `${pc.yellow(\"Left unset (set before deploy):\")} ${skipped\n .map((r) => r.env)\n .join(\", \")}`,\n );\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"env-writer.js","names":[],"sources":["../../../../src/cli/commands/registry/env-writer.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { autocomplete, isCancel, select, text } from \"@clack/prompts\";\nimport pc from \"picocolors\";\n\nimport type { BindingValueNeed } from \"./config-plan\";\nimport {\n collectEnvNeeds,\n type EnvNeed,\n type EnvResolution,\n parseEnv,\n reconcileEnv,\n serializeEnvAppend,\n type ValueProvider,\n} from \"./env-reconcile\";\nimport type { ResourceRequirementRow } from \"./requirements\";\nimport {\n composeResourceId,\n isFlatListable,\n isParentContext,\n listParentContextStep,\n listWorkspaceResources,\n parentContextDepth,\n} from \"./workspace-picker\";\n\nexport interface EnvSyncOptions {\n /** Directory holding `.env` / `.env.example` (the app root). */\n cwd: string;\n /** true = never prompt (agent/CI). Uses flag values or leaves unset. */\n nonInteractive: boolean;\n /** Pre-supplied env values from flags, e.g. { DATABRICKS_WAREHOUSE_ID: \"abc\" }. */\n values?: Record<string, string>;\n /** Databricks profile for the workspace picker (else the CLI default). */\n profile?: string;\n}\n\n/** Sentinel select value meaning \"let me type the id myself\". */\nconst MANUAL = \"__manual__\";\n\n/**\n * Max resources shown in a picker select. Real workspaces can have thousands\n * (e.g. 5000+ SQL warehouses); an unbounded select is unusable. Beyond this we\n * show the first N and log how many were hidden — never silently drop — and the\n * \"Enter manually\" option always lets the user type an id the list omits.\n */\nconst PICKER_LIMIT = 25;\n\n/** Reads a `.env`-style file into a map; empty when the file is absent. */\nfunction readEnvFile(file: string): Record<string, string> {\n if (!fs.existsSync(file)) return {};\n return parseEnv(fs.readFileSync(file, \"utf-8\"));\n}\n\n/** Appends text to a file, creating it (with a trailing newline) if needed. */\nfunction appendToFile(file: string, text: string): void {\n if (text === \"\") return;\n if (fs.existsSync(file)) {\n const existing = fs.readFileSync(file, \"utf-8\");\n const sep = existing.length > 0 && !existing.endsWith(\"\\n\") ? \"\\n\" : \"\";\n fs.writeFileSync(file, existing + sep + text);\n } else {\n fs.writeFileSync(file, text);\n }\n}\n\n/**\n * Caps a choice list to {@link PICKER_LIMIT} for display, logging how many were\n * hidden so the truncation is never silent. The caller always appends an\n * \"Enter manually\" option, so an omitted resource is still reachable.\n */\nexport function capChoices<T>(\n choices: T[],\n resourceType: string,\n limit = PICKER_LIMIT,\n): T[] {\n if (choices.length <= limit) return choices;\n console.log(\n pc.dim(\n ` ${choices.length} ${resourceType}s found; showing first ${limit}. ` +\n 'Use \"Enter manually\" if yours is not listed.',\n ),\n );\n return choices.slice(0, limit);\n}\n\n/** Free-text prompt for one env need; undefined to skip. */\nasync function promptText(need: EnvNeed): Promise<string | undefined> {\n const tag = need.required ? \"required\" : \"optional\";\n const answer = await text({\n message: `${need.env} (${need.resourceType}, ${tag})`,\n placeholder: need.description ?? \"leave blank to skip\",\n });\n if (isCancel(answer)) return undefined;\n const value = (answer ?? \"\").trim();\n return value === \"\" ? undefined : value;\n}\n\n/** Presents one workspace list as a select; MANUAL/cancel handled by caller. */\nasync function selectFrom(\n message: string,\n choices: { value: string; label: string }[],\n): Promise<string | typeof MANUAL | null> {\n const picked = await select({\n message,\n options: [...choices, { value: MANUAL, label: \"Enter manually / skip\" }],\n });\n if (isCancel(picked)) return null;\n return String(picked) as string | typeof MANUAL;\n}\n\n/**\n * Type-to-filter picker over the full choice list (no cap): the user searches\n * by name/id as they type. Appends \"Enter manually\" so an omitted value is\n * still reachable. Returns MANUAL to fall through to free-text, or null on\n * cancel.\n */\nasync function autocompleteFrom(\n message: string,\n choices: { value: string; label: string }[],\n): Promise<string | typeof MANUAL | null> {\n const picked = await autocomplete({\n message,\n options: [...choices, { value: MANUAL, label: \"Enter manually / skip\" }],\n placeholder: \"type to search…\",\n });\n if (isCancel(picked)) return null;\n return String(picked) as string | typeof MANUAL;\n}\n\n/**\n * Drill-down picker for parent-context types (volume→catalog/schema,\n * secret→scope, vector_search_index→endpoint). Walks each step, listing the\n * next level from the prior pick. Returns the final resource id, or undefined\n * to fall back to free-text (on cancel, empty level, or MANUAL at any step).\n */\nasync function pickParentContext(\n need: EnvNeed,\n profile: string | undefined,\n): Promise<string | undefined> {\n const depth = parentContextDepth(need.resourceType);\n const picks: string[] = [];\n for (let i = 0; i < depth; i++) {\n const step = listParentContextStep(need.resourceType, i, picks, profile);\n if (!step || step.choices.length === 0) {\n console.log(\n pc.dim(\n ` No ${step?.key ?? need.resourceType} found — enter the id manually.`,\n ),\n );\n return undefined;\n }\n const picked = await selectFrom(\n `${need.env} — pick a ${step.key}`,\n capChoices(step.choices, step.key),\n );\n if (picked === null || picked === MANUAL) return undefined;\n picks.push(picked);\n }\n // Compose the id from the picks: most types end on a self-qualified id, but a\n // secret needs both scope and key (scope/key).\n return composeResourceId(need.resourceType, picks);\n}\n\n/**\n * Builds the value provider. Precedence: --env flag, then (interactive only) a\n * workspace picker — flat select for flat-listable types, drill-down for\n * parent-context types — else a free-text prompt. The picker degrades to\n * free-text whenever the workspace can't be listed (no profile, offline, auth\n * error, empty) so it never hard-fails.\n */\nfunction makeProvider(opts: EnvSyncOptions): ValueProvider {\n return async (need: EnvNeed) => {\n const fromFlag = opts.values?.[need.env];\n if (fromFlag !== undefined) return fromFlag;\n if (opts.nonInteractive) return undefined;\n\n if (isFlatListable(need.resourceType)) {\n const { choices, truncated, error } = await listWorkspaceResources(\n need.resourceType,\n opts.profile,\n );\n if (choices.length > 0) {\n if (truncated) {\n console.log(\n pc.dim(\n ` Showing the first ${choices.length} ${need.resourceType}s; use \"Enter manually\" if yours isn't listed.`,\n ),\n );\n }\n const picked = await autocompleteFrom(\n `${need.env} — search ${need.resourceType}s`,\n choices,\n );\n if (picked === null) return undefined;\n if (picked !== MANUAL) return picked;\n // fall through to free-text\n } else if (error) {\n // Listing failed (usually auth/profile) — say so, don't pretend the\n // workspace is empty, and point at the fix.\n console.log(\n pc.yellow(\n ` Couldn't list ${need.resourceType}s from the workspace (${error}).`,\n ),\n );\n console.log(\n pc.dim(\n \" Enter an id manually, or re-run with --profile <name> (or set DATABRICKS_CONFIG_PROFILE) so the picker can reach the workspace.\",\n ),\n );\n } else {\n console.log(\n pc.dim(\n ` No ${need.resourceType} found in the workspace — enter an id manually.`,\n ),\n );\n }\n } else if (isParentContext(need.resourceType)) {\n const picked = await pickParentContext(need, opts.profile);\n if (picked !== undefined) return picked;\n // fall through to free-text\n }\n\n return promptText(need);\n };\n}\n\n/**\n * Reconciles a plugin's declared resource env vars into the app's local `.env`\n * (and mirrors variable names into `.env.example`). Never overwrites keys the\n * user already set; skips platform-injected fields. Returns the per-var\n * resolutions so callers can report what happened.\n */\nexport async function syncEnv(\n rows: ResourceRequirementRow[],\n opts: EnvSyncOptions,\n): Promise<EnvResolution[]> {\n const needs = collectEnvNeeds(rows);\n if (needs.length === 0) return [];\n\n const envPath = path.join(opts.cwd, \".env\");\n const examplePath = path.join(opts.cwd, \".env.example\");\n const existing = readEnvFile(envPath);\n\n const resolutions = await reconcileEnv(needs, {\n existing,\n provide: makeProvider(opts),\n });\n\n const written = resolutions.filter(\n (r): r is EnvResolution & { value: string } =>\n r.status === \"written\" && r.value !== undefined,\n );\n appendToFile(\n envPath,\n serializeEnvAppend(written.map((r) => ({ env: r.env, value: r.value }))),\n );\n\n // .env.example carries the variable names (no secret values), and only for\n // vars not already documented there.\n const exampleExisting = readEnvFile(examplePath);\n const newExampleKeys = needs.filter((n) => !(n.env in exampleExisting));\n appendToFile(\n examplePath,\n serializeEnvAppend(newExampleKeys.map((n) => ({ env: n.env, value: \"\" }))),\n );\n\n return resolutions;\n}\n\n/**\n * Prompts for the {@link BindingValueNeed}s that `.env` reconciliation can't\n * collect, returning a `fieldKey -> value` map for buildConfigPlan. Values are\n * not written to `.env` (these fields have no env var). Non-interactive mode\n * uses `values[fieldKey]` if provided, else leaves the field unset.\n */\nexport async function collectBindingValues(\n needs: BindingValueNeed[],\n opts: EnvSyncOptions,\n): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n for (const need of needs) {\n const fromFlag = opts.values?.[need.fieldKey];\n if (fromFlag !== undefined) {\n out[need.fieldKey] = fromFlag;\n continue;\n }\n if (opts.nonInteractive) continue;\n const answer = await text({\n message: `${need.fieldKey} (${need.resourceType}) — required for databricks.yml`,\n placeholder: need.description ?? \"leave blank to set before deploy\",\n });\n if (isCancel(answer)) continue;\n const value = (answer ?? \"\").trim();\n if (value !== \"\") out[need.fieldKey] = value;\n }\n return out;\n}\n\n/** Prints a concise summary of what env reconciliation did. */\nexport function reportEnvResolutions(resolutions: EnvResolution[]): void {\n if (resolutions.length === 0) return;\n const written = resolutions.filter((r) => r.status === \"written\");\n const already = resolutions.filter((r) => r.status === \"already-set\");\n const skipped = resolutions.filter((r) => r.status === \"skipped\");\n\n if (written.length > 0) {\n console.log(\n `${pc.green(\"Wrote to .env:\")} ${written.map((r) => r.env).join(\", \")}`,\n );\n }\n if (already.length > 0) {\n console.log(pc.dim(`Already set: ${already.map((r) => r.env).join(\", \")}`));\n }\n if (skipped.length > 0) {\n console.log(\n `${pc.yellow(\"Left unset (set before deploy):\")} ${skipped\n .map((r) => r.env)\n .join(\", \")}`,\n );\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../../src/cli/commands/registry/index.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { registryInfoCommand } from \"./info\";\nimport { registryListCommand, registrySearchCommand } from \"./list\";\n\n/**\n * Parent command for AppKit component registry operations.\n * Subcommands:\n * - list: Enumerate items available in the registry\n * - search: Find items by name, description, type, or keyword\n * - info: Show an item's resource requirements and dependencies\n *\n * Note: `appkit add <item>` is exposed as a top-level command (see add.ts)\n * since it is the primary entry point for consumers.\n */\nexport const registryCommand = new Command(\"registry\")\n .description(\"AppKit component registry commands\")\n .addCommand(registryListCommand)\n .addCommand(registrySearchCommand)\n .addCommand(registryInfoCommand)\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit registry list\n $ appkit registry search kpi dashboard\n $ appkit registry info analytics\n $ appkit add metric-card`,\n );\n"],"mappings":";;;;;;;;;;;;;;;AAcA,MAAa,kBAAkB,IAAI,QAAQ,WAAW,CACnD,YAAY,qCAAqC,CACjD,WAAW,oBAAoB,CAC/B,WAAW,sBAAsB,CACjC,WAAW,oBAAoB,CAC/B,YACC,SACA;;;;;4BAMD"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/cli/commands/registry/index.ts"],"sourcesContent":["import { Command } from \"commander\";\n\nimport { registryInfoCommand } from \"./info\";\nimport { registryListCommand, registrySearchCommand } from \"./list\";\n\n/**\n * Parent command for AppKit component registry operations.\n * Subcommands:\n * - list: Enumerate items available in the registry\n * - search: Find items by name, description, type, or keyword\n * - info: Show an item's resource requirements and dependencies\n *\n * Note: `appkit add <item>` is exposed as a top-level command (see add.ts)\n * since it is the primary entry point for consumers.\n */\nexport const registryCommand = new Command(\"registry\")\n .description(\"AppKit component registry commands\")\n .addCommand(registryListCommand)\n .addCommand(registrySearchCommand)\n .addCommand(registryInfoCommand)\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit registry list\n $ appkit registry search kpi dashboard\n $ appkit registry info analytics\n $ appkit add metric-card`,\n );\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAa,kBAAkB,IAAI,QAAQ,WAAW,CACnD,YAAY,qCAAqC,CACjD,WAAW,oBAAoB,CAC/B,WAAW,sBAAsB,CACjC,WAAW,oBAAoB,CAC/B,YACC,SACA;;;;;4BAMD"}
@@ -1 +1 @@
1
- {"version":3,"file":"info.js","names":[],"sources":["../../../../src/cli/commands/registry/info.ts"],"sourcesContent":["import process from \"node:process\";\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport { fetchRegistryItem, isValidItemName, stripNamespace } from \"./client\";\nimport { resolveToken } from \"./constants\";\nimport { extractRequirements, renderRequirements } from \"./requirements\";\n\nasync function runInfo(ref: string, opts: { json?: boolean }): Promise<void> {\n const token = resolveToken();\n // Validate before fetching: the name is interpolated into the fetch path, so\n // reject non-slug refs (matches the `add` guard) rather than let `/` or `..`\n // redirect the request to another path in the repo.\n const name = stripNamespace(ref);\n if (!isValidItemName(name)) {\n console.error(`Invalid registry item name: ${JSON.stringify(ref)}`);\n process.exit(1);\n }\n const item = await fetchRegistryItem(name, token);\n const rows = extractRequirements(item);\n\n if (opts.json) {\n console.log(\n JSON.stringify(\n {\n name: item.name,\n type: item.type,\n dependencies: item.dependencies ?? [],\n registryDependencies: item.registryDependencies ?? [],\n resources: rows,\n },\n null,\n 2,\n ),\n );\n return;\n }\n\n console.log(pc.bold(item.name));\n const deps = item.dependencies ?? [];\n const registryDeps = item.registryDependencies ?? [];\n if (deps.length > 0) {\n console.log(pc.dim(` npm dependencies: ${deps.join(\", \")}`));\n }\n if (registryDeps.length > 0) {\n console.log(pc.dim(` registry dependencies: ${registryDeps.join(\", \")}`));\n }\n console.log(`\\n${renderRequirements(item, rows)}`);\n}\n\nexport const registryInfoCommand = new Command(\"info\")\n .description(\"Show an item's resource requirements and dependencies\")\n .argument(\"<item>\", \"Registry item name, e.g. analytics\")\n .option(\"--json\", \"Output as JSON\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit registry info analytics\n $ appkit registry info @databricks-appkit/analytics --json`,\n )\n .action((ref: string, opts: { json?: boolean }) =>\n runInfo(ref, opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":";;;;;;;;AAOA,eAAe,QAAQ,KAAa,MAAyC;CAC3E,MAAM,QAAQ,cAAc;CAI5B,MAAM,OAAO,eAAe,IAAI;AAChC,KAAI,CAAC,gBAAgB,KAAK,EAAE;AAC1B,UAAQ,MAAM,+BAA+B,KAAK,UAAU,IAAI,GAAG;AACnE,UAAQ,KAAK,EAAE;;CAEjB,MAAM,OAAO,MAAM,kBAAkB,MAAM,MAAM;CACjD,MAAM,OAAO,oBAAoB,KAAK;AAEtC,KAAI,KAAK,MAAM;AACb,UAAQ,IACN,KAAK,UACH;GACE,MAAM,KAAK;GACX,MAAM,KAAK;GACX,cAAc,KAAK,gBAAgB,EAAE;GACrC,sBAAsB,KAAK,wBAAwB,EAAE;GACrD,WAAW;GACZ,EACD,MACA,EACD,CACF;AACD;;AAGF,SAAQ,IAAI,GAAG,KAAK,KAAK,KAAK,CAAC;CAC/B,MAAM,OAAO,KAAK,gBAAgB,EAAE;CACpC,MAAM,eAAe,KAAK,wBAAwB,EAAE;AACpD,KAAI,KAAK,SAAS,EAChB,SAAQ,IAAI,GAAG,IAAI,uBAAuB,KAAK,KAAK,KAAK,GAAG,CAAC;AAE/D,KAAI,aAAa,SAAS,EACxB,SAAQ,IAAI,GAAG,IAAI,4BAA4B,aAAa,KAAK,KAAK,GAAG,CAAC;AAE5E,SAAQ,IAAI,KAAK,mBAAmB,MAAM,KAAK,GAAG;;AAGpD,MAAa,sBAAsB,IAAI,QAAQ,OAAO,CACnD,YAAY,wDAAwD,CACpE,SAAS,UAAU,qCAAqC,CACxD,OAAO,UAAU,iBAAiB,CAClC,YACC,SACA;;;8DAID,CACA,QAAQ,KAAa,SACpB,QAAQ,KAAK,KAAK,CAAC,OAAO,QAAQ;AAChC,SAAQ,MAAM,IAAI;AAClB,SAAQ,KAAK,EAAE;EACf,CACH"}
1
+ {"version":3,"file":"info.js","names":[],"sources":["../../../../src/cli/commands/registry/info.ts"],"sourcesContent":["import process from \"node:process\";\n\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\n\nimport { fetchRegistryItem, isValidItemName, stripNamespace } from \"./client\";\nimport { resolveToken } from \"./constants\";\nimport { extractRequirements, renderRequirements } from \"./requirements\";\n\nasync function runInfo(ref: string, opts: { json?: boolean }): Promise<void> {\n const token = resolveToken();\n // Validate before fetching: the name is interpolated into the fetch path, so\n // reject non-slug refs (matches the `add` guard) rather than let `/` or `..`\n // redirect the request to another path in the repo.\n const name = stripNamespace(ref);\n if (!isValidItemName(name)) {\n console.error(`Invalid registry item name: ${JSON.stringify(ref)}`);\n process.exit(1);\n }\n const item = await fetchRegistryItem(name, token);\n const rows = extractRequirements(item);\n\n if (opts.json) {\n console.log(\n JSON.stringify(\n {\n name: item.name,\n type: item.type,\n dependencies: item.dependencies ?? [],\n registryDependencies: item.registryDependencies ?? [],\n resources: rows,\n },\n null,\n 2,\n ),\n );\n return;\n }\n\n console.log(pc.bold(item.name));\n const deps = item.dependencies ?? [];\n const registryDeps = item.registryDependencies ?? [];\n if (deps.length > 0) {\n console.log(pc.dim(` npm dependencies: ${deps.join(\", \")}`));\n }\n if (registryDeps.length > 0) {\n console.log(pc.dim(` registry dependencies: ${registryDeps.join(\", \")}`));\n }\n console.log(`\\n${renderRequirements(item, rows)}`);\n}\n\nexport const registryInfoCommand = new Command(\"info\")\n .description(\"Show an item's resource requirements and dependencies\")\n .argument(\"<item>\", \"Registry item name, e.g. analytics\")\n .option(\"--json\", \"Output as JSON\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit registry info analytics\n $ appkit registry info @databricks-appkit/analytics --json`,\n )\n .action((ref: string, opts: { json?: boolean }) =>\n runInfo(ref, opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":";;;;;;;;AASA,eAAe,QAAQ,KAAa,MAAyC;CAC3E,MAAM,QAAQ,cAAc;CAI5B,MAAM,OAAO,eAAe,IAAI;AAChC,KAAI,CAAC,gBAAgB,KAAK,EAAE;AAC1B,UAAQ,MAAM,+BAA+B,KAAK,UAAU,IAAI,GAAG;AACnE,UAAQ,KAAK,EAAE;;CAEjB,MAAM,OAAO,MAAM,kBAAkB,MAAM,MAAM;CACjD,MAAM,OAAO,oBAAoB,KAAK;AAEtC,KAAI,KAAK,MAAM;AACb,UAAQ,IACN,KAAK,UACH;GACE,MAAM,KAAK;GACX,MAAM,KAAK;GACX,cAAc,KAAK,gBAAgB,EAAE;GACrC,sBAAsB,KAAK,wBAAwB,EAAE;GACrD,WAAW;GACZ,EACD,MACA,EACD,CACF;AACD;;AAGF,SAAQ,IAAI,GAAG,KAAK,KAAK,KAAK,CAAC;CAC/B,MAAM,OAAO,KAAK,gBAAgB,EAAE;CACpC,MAAM,eAAe,KAAK,wBAAwB,EAAE;AACpD,KAAI,KAAK,SAAS,EAChB,SAAQ,IAAI,GAAG,IAAI,uBAAuB,KAAK,KAAK,KAAK,GAAG,CAAC;AAE/D,KAAI,aAAa,SAAS,EACxB,SAAQ,IAAI,GAAG,IAAI,4BAA4B,aAAa,KAAK,KAAK,GAAG,CAAC;AAE5E,SAAQ,IAAI,KAAK,mBAAmB,MAAM,KAAK,GAAG;;AAGpD,MAAa,sBAAsB,IAAI,QAAQ,OAAO,CACnD,YAAY,wDAAwD,CACpE,SAAS,UAAU,qCAAqC,CACxD,OAAO,UAAU,iBAAiB,CAClC,YACC,SACA;;;8DAID,CACA,QAAQ,KAAa,SACpB,QAAQ,KAAK,KAAK,CAAC,OAAO,QAAQ;AAChC,SAAQ,MAAM,IAAI;AAClB,SAAQ,KAAK,EAAE;EACf,CACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"list.js","names":[],"sources":["../../../../src/cli/commands/registry/list.ts"],"sourcesContent":["import process from \"node:process\";\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport { registryAuthHeaders } from \"./client\";\nimport {\n REGISTRY_INDEX_API_URL,\n REGISTRY_INDEX_URL,\n REGISTRY_REPO,\n resolveToken,\n} from \"./constants\";\n\ninterface RegistryIndexItem {\n name: string;\n type?: string;\n title?: string;\n description?: string;\n categories?: string[];\n meta?: { verified?: boolean };\n files?: Array<{ path?: string; target?: string }>;\n}\n\nfunction isVerified(item: RegistryIndexItem): boolean {\n return item.meta?.verified === true;\n}\n\n/** Free-text haystack for search matching. */\nfunction searchHaystack(item: RegistryIndexItem): string {\n return [\n item.name,\n item.title ?? \"\",\n item.description ?? \"\",\n itemKind(item),\n ...(item.categories ?? []),\n ]\n .join(\" \")\n .toLowerCase();\n}\n\n/** True if every whitespace-separated term in `query` appears in the item. */\nfunction matchesQuery(item: RegistryIndexItem, query: string): boolean {\n const haystack = searchHaystack(item);\n return query\n .toLowerCase()\n .split(/\\s+/)\n .filter(Boolean)\n .every((term) => haystack.includes(term));\n}\n\n/** A friendly kind for the TYPE column: plugin, component, hook, theme, … */\nfunction itemKind(item: RegistryIndexItem): string {\n const hasManifest = (item.files ?? []).some(\n (f) =>\n f.target?.endsWith(\"manifest.json\") || f.path?.endsWith(\"manifest.json\"),\n );\n if (hasManifest) return \"plugin\";\n switch (item.type) {\n case \"registry:component\":\n case \"registry:block\":\n return \"component\";\n case \"registry:hook\":\n return \"hook\";\n case \"registry:lib\":\n return \"lib\";\n case \"registry:theme\":\n return \"theme\";\n case \"registry:ui\":\n return \"ui\";\n case \"registry:page\":\n return \"page\";\n default:\n return item.type?.replace(/^registry:/, \"\") || \"item\";\n }\n}\n\nconst KIND_COLOR: Record<string, (s: string) => string> = {\n plugin: pc.magenta,\n component: pc.blue,\n hook: pc.cyan,\n theme: pc.yellow,\n lib: pc.green,\n};\n\nfunction printTable(items: RegistryIndexItem[]): void {\n if (items.length === 0) {\n console.log(pc.dim(\"No items found in the registry.\"));\n return;\n }\n const kinds = items.map(itemKind);\n const maxName = Math.max(4, ...items.map((i) => i.name.length));\n const maxKind = Math.max(4, ...kinds.map((k) => k.length));\n const verifiedCol = \"VERIFIED\";\n // Pad plain text before coloring so ANSI codes don't break alignment.\n const header = `${\"NAME\".padEnd(maxName)} ${\"TYPE\".padEnd(maxKind)} ${verifiedCol} DESCRIPTION`;\n console.log(pc.bold(header));\n console.log(pc.dim(\"─\".repeat(header.length)));\n for (const [i, item] of items.entries()) {\n const verified = isVerified(item);\n const kind = kinds[i];\n const colorKind = KIND_COLOR[kind] ?? pc.white;\n const name = pc.cyan(item.name.padEnd(maxName));\n const kindCell = colorKind(kind.padEnd(maxKind));\n const mark = verified\n ? pc.green(\"✓\".padEnd(verifiedCol.length))\n : \" \".repeat(verifiedCol.length);\n const desc = item.description ?? item.title ?? \"\";\n console.log(\n `${name} ${kindCell} ${mark} ${verified ? desc : pc.dim(desc)}`,\n );\n }\n}\n\n/** Fetches the registry index (token-aware), or exits with a helpful message. */\nasync function fetchIndex(): Promise<RegistryIndexItem[]> {\n const token = resolveToken();\n const url = token ? REGISTRY_INDEX_API_URL : REGISTRY_INDEX_URL;\n\n let res: Awaited<ReturnType<typeof fetch>>;\n try {\n res = await fetch(url, { headers: registryAuthHeaders(token) });\n } catch (err) {\n console.error(pc.red(`Failed to reach the registry at ${url}`));\n console.error(` ${err instanceof Error ? err.message : String(err)}`);\n process.exit(1);\n }\n if (res.status === 404 || res.status === 401 || res.status === 403) {\n console.error(\n pc.red(\n `Could not read the registry index from ${REGISTRY_REPO} (HTTP ${res.status}).`,\n ),\n );\n if (!token) {\n console.error(\n \" If the 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.ok) {\n console.error(pc.red(`Registry returned HTTP ${res.status} for ${url}`));\n process.exit(1);\n }\n\n const data = (await res.json()) as { items?: RegistryIndexItem[] };\n return data.items ?? [];\n}\n\nfunction output(items: RegistryIndexItem[], opts: { json?: boolean }): void {\n if (opts.json) {\n // Surface `kind` + `verified` as top-level fields for easy scripting.\n console.log(\n JSON.stringify(\n items.map((i) => ({\n ...i,\n kind: itemKind(i),\n verified: isVerified(i),\n })),\n null,\n 2,\n ),\n );\n } else {\n printTable(items);\n }\n}\n\nasync function runList(opts: {\n json?: boolean;\n verified?: boolean;\n}): Promise<void> {\n let items = await fetchIndex();\n if (opts.verified) items = items.filter(isVerified);\n output(items, opts);\n}\n\nasync function runSearch(\n query: string,\n opts: { json?: boolean; verified?: boolean },\n): Promise<void> {\n let items = await fetchIndex();\n items = items.filter((i) => matchesQuery(i, query));\n if (opts.verified) items = items.filter(isVerified);\n if (items.length === 0 && !opts.json) {\n console.log(pc.dim(`No items match \"${query}\".`));\n return;\n }\n output(items, opts);\n}\n\nexport const registryListCommand = new Command(\"list\")\n .description(\"List items available in the AppKit registry\")\n .option(\"--json\", \"Output as JSON\")\n .option(\"--verified\", \"Show only verified items\")\n .action((opts: { json?: boolean; verified?: boolean }) =>\n runList(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n\nexport const registrySearchCommand = new Command(\"search\")\n .description(\"Search registry items by name, description, type, or keyword\")\n .argument(\"<query...>\", \"Search terms (all must match)\")\n .option(\"--json\", \"Output as JSON\")\n .option(\"--verified\", \"Show only verified items\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit registry search chart\n $ appkit registry search kpi dashboard\n $ appkit registry search plugin --json`,\n )\n .action((query: string[], opts: { json?: boolean; verified?: boolean }) =>\n runSearch(query.join(\" \"), opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}
1
+ {"version":3,"file":"list.js","names":[],"sources":["../../../../src/cli/commands/registry/list.ts"],"sourcesContent":["import process from \"node:process\";\n\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\n\nimport { registryAuthHeaders } from \"./client\";\nimport {\n REGISTRY_INDEX_API_URL,\n REGISTRY_INDEX_URL,\n REGISTRY_REPO,\n resolveToken,\n} from \"./constants\";\n\ninterface RegistryIndexItem {\n name: string;\n type?: string;\n title?: string;\n description?: string;\n categories?: string[];\n meta?: { verified?: boolean };\n files?: Array<{ path?: string; target?: string }>;\n}\n\nfunction isVerified(item: RegistryIndexItem): boolean {\n return item.meta?.verified === true;\n}\n\n/** Free-text haystack for search matching. */\nfunction searchHaystack(item: RegistryIndexItem): string {\n return [\n item.name,\n item.title ?? \"\",\n item.description ?? \"\",\n itemKind(item),\n ...(item.categories ?? []),\n ]\n .join(\" \")\n .toLowerCase();\n}\n\n/** True if every whitespace-separated term in `query` appears in the item. */\nfunction matchesQuery(item: RegistryIndexItem, query: string): boolean {\n const haystack = searchHaystack(item);\n return query\n .toLowerCase()\n .split(/\\s+/)\n .filter(Boolean)\n .every((term) => haystack.includes(term));\n}\n\n/** A friendly kind for the TYPE column: plugin, component, hook, theme, … */\nfunction itemKind(item: RegistryIndexItem): string {\n const hasManifest = (item.files ?? []).some(\n (f) =>\n f.target?.endsWith(\"manifest.json\") || f.path?.endsWith(\"manifest.json\"),\n );\n if (hasManifest) return \"plugin\";\n switch (item.type) {\n case \"registry:component\":\n case \"registry:block\":\n return \"component\";\n case \"registry:hook\":\n return \"hook\";\n case \"registry:lib\":\n return \"lib\";\n case \"registry:theme\":\n return \"theme\";\n case \"registry:ui\":\n return \"ui\";\n case \"registry:page\":\n return \"page\";\n default:\n return item.type?.replace(/^registry:/, \"\") || \"item\";\n }\n}\n\nconst KIND_COLOR: Record<string, (s: string) => string> = {\n plugin: pc.magenta,\n component: pc.blue,\n hook: pc.cyan,\n theme: pc.yellow,\n lib: pc.green,\n};\n\nfunction printTable(items: RegistryIndexItem[]): void {\n if (items.length === 0) {\n console.log(pc.dim(\"No items found in the registry.\"));\n return;\n }\n const kinds = items.map(itemKind);\n const maxName = Math.max(4, ...items.map((i) => i.name.length));\n const maxKind = Math.max(4, ...kinds.map((k) => k.length));\n const verifiedCol = \"VERIFIED\";\n // Pad plain text before coloring so ANSI codes don't break alignment.\n const header = `${\"NAME\".padEnd(maxName)} ${\"TYPE\".padEnd(maxKind)} ${verifiedCol} DESCRIPTION`;\n console.log(pc.bold(header));\n console.log(pc.dim(\"─\".repeat(header.length)));\n for (const [i, item] of items.entries()) {\n const verified = isVerified(item);\n const kind = kinds[i];\n const colorKind = KIND_COLOR[kind] ?? pc.white;\n const name = pc.cyan(item.name.padEnd(maxName));\n const kindCell = colorKind(kind.padEnd(maxKind));\n const mark = verified\n ? pc.green(\"✓\".padEnd(verifiedCol.length))\n : \" \".repeat(verifiedCol.length);\n const desc = item.description ?? item.title ?? \"\";\n console.log(\n `${name} ${kindCell} ${mark} ${verified ? desc : pc.dim(desc)}`,\n );\n }\n}\n\n/** Fetches the registry index (token-aware), or exits with a helpful message. */\nasync function fetchIndex(): Promise<RegistryIndexItem[]> {\n const token = resolveToken();\n const url = token ? REGISTRY_INDEX_API_URL : REGISTRY_INDEX_URL;\n\n let res: Awaited<ReturnType<typeof fetch>>;\n try {\n res = await fetch(url, { headers: registryAuthHeaders(token) });\n } catch (err) {\n console.error(pc.red(`Failed to reach the registry at ${url}`));\n console.error(` ${err instanceof Error ? err.message : String(err)}`);\n process.exit(1);\n }\n if (res.status === 404 || res.status === 401 || res.status === 403) {\n console.error(\n pc.red(\n `Could not read the registry index from ${REGISTRY_REPO} (HTTP ${res.status}).`,\n ),\n );\n if (!token) {\n console.error(\n \" If the 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.ok) {\n console.error(pc.red(`Registry returned HTTP ${res.status} for ${url}`));\n process.exit(1);\n }\n\n const data = (await res.json()) as { items?: RegistryIndexItem[] };\n return data.items ?? [];\n}\n\nfunction output(items: RegistryIndexItem[], opts: { json?: boolean }): void {\n if (opts.json) {\n // Surface `kind` + `verified` as top-level fields for easy scripting.\n console.log(\n JSON.stringify(\n items.map((i) => ({\n ...i,\n kind: itemKind(i),\n verified: isVerified(i),\n })),\n null,\n 2,\n ),\n );\n } else {\n printTable(items);\n }\n}\n\nasync function runList(opts: {\n json?: boolean;\n verified?: boolean;\n}): Promise<void> {\n let items = await fetchIndex();\n if (opts.verified) items = items.filter(isVerified);\n output(items, opts);\n}\n\nasync function runSearch(\n query: string,\n opts: { json?: boolean; verified?: boolean },\n): Promise<void> {\n let items = await fetchIndex();\n items = items.filter((i) => matchesQuery(i, query));\n if (opts.verified) items = items.filter(isVerified);\n if (items.length === 0 && !opts.json) {\n console.log(pc.dim(`No items match \"${query}\".`));\n return;\n }\n output(items, opts);\n}\n\nexport const registryListCommand = new Command(\"list\")\n .description(\"List items available in the AppKit registry\")\n .option(\"--json\", \"Output as JSON\")\n .option(\"--verified\", \"Show only verified items\")\n .action((opts: { json?: boolean; verified?: boolean }) =>\n runList(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n\nexport const registrySearchCommand = new Command(\"search\")\n .description(\"Search registry items by name, description, type, or keyword\")\n .argument(\"<query...>\", \"Search terms (all must match)\")\n .option(\"--json\", \"Output as JSON\")\n .option(\"--verified\", \"Show only verified items\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit registry search chart\n $ appkit registry search kpi dashboard\n $ appkit registry search plugin --json`,\n )\n .action((query: string[], opts: { json?: boolean; verified?: boolean }) =>\n runSearch(query.join(\" \"), opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"requirements.js","names":[],"sources":["../../../../src/cli/commands/registry/requirements.ts"],"sourcesContent":["import path from \"node:path\";\nimport pc from \"picocolors\";\nimport { computeOriginFromField } from \"../../../schemas/manifest\";\nimport type { RegistryItem } from \"./client\";\n\n/**\n * A single resource field as declared in a plugin manifest. `origin` is the\n * computed classifier written by `plugin sync` (platform/static/cli/user) that\n * says how the value reaches the running app.\n */\nexport interface RequirementField {\n key: string;\n env?: string;\n /**\n * Computed classifier written by `plugin sync` — only present in a synced\n * manifest. Authored manifests omit it, so consumers must derive the\n * effective origin via {@link fieldOrigin} rather than reading this directly.\n */\n origin?: string;\n description?: string;\n /** Default literal value (static origin); pre-filled without prompting. */\n value?: string;\n /** Resolver key (cli origin), e.g. \"postgres:host\". */\n resolve?: string;\n /** Local-dev-only field; platform-injected at deploy time. */\n localOnly?: boolean;\n}\n\n/** A resource requirement flattened for display. */\nexport interface ResourceRequirementRow {\n type: string;\n resourceKey?: string;\n permission?: string;\n required: boolean;\n description?: string;\n fields: RequirementField[];\n}\n\ninterface ManifestFieldShape {\n env?: string;\n origin?: string;\n description?: string;\n value?: string;\n resolve?: string;\n localOnly?: boolean;\n}\ninterface ManifestResourceShape {\n type?: string;\n resourceKey?: string;\n permission?: string;\n description?: string;\n fields?: Record<string, ManifestFieldShape>;\n}\ninterface ManifestShape {\n resources?: {\n required?: ManifestResourceShape[];\n optional?: ManifestResourceShape[];\n };\n}\n\nfunction toFields(\n fields: Record<string, ManifestFieldShape> | undefined,\n): RequirementField[] {\n return Object.entries(fields ?? {}).map(([key, f]) => ({\n key,\n env: f.env,\n origin: f.origin,\n description: f.description,\n value: f.value,\n resolve: f.resolve,\n localOnly: f.localOnly,\n }));\n}\n\nfunction toRows(\n resources: ManifestResourceShape[] | undefined,\n required: boolean,\n): ResourceRequirementRow[] {\n return (resources ?? []).map((r) => ({\n type: r.type ?? \"unknown\",\n resourceKey: r.resourceKey,\n permission: r.permission,\n required,\n description: r.description,\n fields: toFields(r.fields),\n }));\n}\n\n/** The manifest.json file shipped by a plugin item, or null for UI items. */\nfunction findManifest(item: RegistryItem): ManifestShape | null {\n const file = (item.files ?? []).find(\n (f) => path.basename(f.target ?? f.path) === \"manifest.json\",\n );\n if (!file) return null;\n try {\n return JSON.parse(file.content) as ManifestShape;\n } catch {\n return null;\n }\n}\n\n/**\n * Extracts a plugin item's declared resource requirements (required first,\n * then optional). Returns an empty array for UI items or plugins that declare\n * no resources.\n */\nexport function extractRequirements(\n item: RegistryItem,\n): ResourceRequirementRow[] {\n const manifest = findManifest(item);\n if (!manifest) return [];\n return [\n ...toRows(manifest.resources?.required, true),\n ...toRows(manifest.resources?.optional, false),\n ];\n}\n\n/**\n * Renders the resource requirements for an item as human-readable lines.\n * Returns a single \"no resources\" line when there are none, so callers can\n * print unconditionally.\n */\nexport function renderRequirements(\n item: RegistryItem,\n rows: ResourceRequirementRow[] = extractRequirements(item),\n): string {\n if (rows.length === 0) {\n return pc.dim(`${item.name}: no resource requirements.`);\n }\n\n const lines: string[] = [pc.bold(`Resources required by ${item.name}:`)];\n for (const row of rows) {\n const tag = row.required ? pc.yellow(\"required\") : pc.dim(\"optional\");\n const perm = row.permission ? pc.dim(` [${row.permission}]`) : \"\";\n lines.push(` ${pc.cyan(row.type)} (${tag})${perm}`);\n if (row.description) lines.push(` ${pc.dim(row.description)}`);\n for (const field of row.fields) {\n if (!field.env) continue;\n const origin = field.origin ? pc.dim(` (${field.origin})`) : \"\";\n lines.push(` - ${field.env}${origin}`);\n }\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * A syntactically valid environment variable name. Field `env` names come from\n * an untrusted manifest and are written into `.env` as `NAME=value` (a newline\n * in the name would inject a second line — e.g. `PORT=x\\nDATABRICKS_HOST=…` →\n * credential exfil) and emitted into `app.yaml`. Anything that isn't a plain\n * env identifier is dropped before it reaches those sinks.\n */\nconst ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/** True when `name` is a safe, well-formed environment variable name. */\nexport function isValidEnvName(name: string): boolean {\n return ENV_NAME.test(name);\n}\n\n/**\n * Effective origin of a field: trust a synced manifest's stamped `origin`, else\n * derive it from the field shape via the same rule `plugin sync` uses, so an\n * authored manifest (no `origin`) still classifies correctly.\n */\nexport function fieldOrigin(\n field: RequirementField,\n): \"platform\" | \"static\" | \"cli\" | \"user\" {\n if (field.origin)\n return field.origin as \"platform\" | \"static\" | \"cli\" | \"user\";\n return computeOriginFromField(field);\n}\n"],"mappings":";;;;;AA4DA,SAAS,SACP,QACoB;AACpB,QAAO,OAAO,QAAQ,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,QAAQ;EACrD;EACA,KAAK,EAAE;EACP,QAAQ,EAAE;EACV,aAAa,EAAE;EACf,OAAO,EAAE;EACT,SAAS,EAAE;EACX,WAAW,EAAE;EACd,EAAE;;AAGL,SAAS,OACP,WACA,UAC0B;AAC1B,SAAQ,aAAa,EAAE,EAAE,KAAK,OAAO;EACnC,MAAM,EAAE,QAAQ;EAChB,aAAa,EAAE;EACf,YAAY,EAAE;EACd;EACA,aAAa,EAAE;EACf,QAAQ,SAAS,EAAE,OAAO;EAC3B,EAAE;;;AAIL,SAAS,aAAa,MAA0C;CAC9D,MAAM,QAAQ,KAAK,SAAS,EAAE,EAAE,MAC7B,MAAM,KAAK,SAAS,EAAE,UAAU,EAAE,KAAK,KAAK,gBAC9C;AACD,KAAI,CAAC,KAAM,QAAO;AAClB,KAAI;AACF,SAAO,KAAK,MAAM,KAAK,QAAQ;SACzB;AACN,SAAO;;;;;;;;AASX,SAAgB,oBACd,MAC0B;CAC1B,MAAM,WAAW,aAAa,KAAK;AACnC,KAAI,CAAC,SAAU,QAAO,EAAE;AACxB,QAAO,CACL,GAAG,OAAO,SAAS,WAAW,UAAU,KAAK,EAC7C,GAAG,OAAO,SAAS,WAAW,UAAU,MAAM,CAC/C;;;;;;;AAQH,SAAgB,mBACd,MACA,OAAiC,oBAAoB,KAAK,EAClD;AACR,KAAI,KAAK,WAAW,EAClB,QAAO,GAAG,IAAI,GAAG,KAAK,KAAK,6BAA6B;CAG1D,MAAM,QAAkB,CAAC,GAAG,KAAK,yBAAyB,KAAK,KAAK,GAAG,CAAC;AACxE,MAAK,MAAM,OAAO,MAAM;EACtB,MAAM,MAAM,IAAI,WAAW,GAAG,OAAO,WAAW,GAAG,GAAG,IAAI,WAAW;EACrE,MAAM,OAAO,IAAI,aAAa,GAAG,IAAI,KAAK,IAAI,WAAW,GAAG,GAAG;AAC/D,QAAM,KAAK,KAAK,GAAG,KAAK,IAAI,KAAK,CAAC,IAAI,IAAI,GAAG,OAAO;AACpD,MAAI,IAAI,YAAa,OAAM,KAAK,OAAO,GAAG,IAAI,IAAI,YAAY,GAAG;AACjE,OAAK,MAAM,SAAS,IAAI,QAAQ;AAC9B,OAAI,CAAC,MAAM,IAAK;GAChB,MAAM,SAAS,MAAM,SAAS,GAAG,IAAI,KAAK,MAAM,OAAO,GAAG,GAAG;AAC7D,SAAM,KAAK,SAAS,MAAM,MAAM,SAAS;;;AAG7C,QAAO,MAAM,KAAK,KAAK;;;;;;;;;AAUzB,MAAM,WAAW;;AAGjB,SAAgB,eAAe,MAAuB;AACpD,QAAO,SAAS,KAAK,KAAK;;;;;;;AAQ5B,SAAgB,YACd,OACwC;AACxC,KAAI,MAAM,OACR,QAAO,MAAM;AACf,QAAO,uBAAuB,MAAM"}
1
+ {"version":3,"file":"requirements.js","names":[],"sources":["../../../../src/cli/commands/registry/requirements.ts"],"sourcesContent":["import path from \"node:path\";\n\nimport pc from \"picocolors\";\n\nimport { computeOriginFromField } from \"../../../schemas/manifest\";\nimport type { RegistryItem } from \"./client\";\n\n/**\n * A single resource field as declared in a plugin manifest. `origin` is the\n * computed classifier written by `plugin sync` (platform/static/cli/user) that\n * says how the value reaches the running app.\n */\nexport interface RequirementField {\n key: string;\n env?: string;\n /**\n * Computed classifier written by `plugin sync` — only present in a synced\n * manifest. Authored manifests omit it, so consumers must derive the\n * effective origin via {@link fieldOrigin} rather than reading this directly.\n */\n origin?: string;\n description?: string;\n /** Default literal value (static origin); pre-filled without prompting. */\n value?: string;\n /** Resolver key (cli origin), e.g. \"postgres:host\". */\n resolve?: string;\n /** Local-dev-only field; platform-injected at deploy time. */\n localOnly?: boolean;\n}\n\n/** A resource requirement flattened for display. */\nexport interface ResourceRequirementRow {\n type: string;\n resourceKey?: string;\n permission?: string;\n required: boolean;\n description?: string;\n fields: RequirementField[];\n}\n\ninterface ManifestFieldShape {\n env?: string;\n origin?: string;\n description?: string;\n value?: string;\n resolve?: string;\n localOnly?: boolean;\n}\ninterface ManifestResourceShape {\n type?: string;\n resourceKey?: string;\n permission?: string;\n description?: string;\n fields?: Record<string, ManifestFieldShape>;\n}\ninterface ManifestShape {\n resources?: {\n required?: ManifestResourceShape[];\n optional?: ManifestResourceShape[];\n };\n}\n\nfunction toFields(\n fields: Record<string, ManifestFieldShape> | undefined,\n): RequirementField[] {\n return Object.entries(fields ?? {}).map(([key, f]) => ({\n key,\n env: f.env,\n origin: f.origin,\n description: f.description,\n value: f.value,\n resolve: f.resolve,\n localOnly: f.localOnly,\n }));\n}\n\nfunction toRows(\n resources: ManifestResourceShape[] | undefined,\n required: boolean,\n): ResourceRequirementRow[] {\n return (resources ?? []).map((r) => ({\n type: r.type ?? \"unknown\",\n resourceKey: r.resourceKey,\n permission: r.permission,\n required,\n description: r.description,\n fields: toFields(r.fields),\n }));\n}\n\n/** The manifest.json file shipped by a plugin item, or null for UI items. */\nfunction findManifest(item: RegistryItem): ManifestShape | null {\n const file = (item.files ?? []).find(\n (f) => path.basename(f.target ?? f.path) === \"manifest.json\",\n );\n if (!file) return null;\n try {\n return JSON.parse(file.content) as ManifestShape;\n } catch {\n return null;\n }\n}\n\n/**\n * Extracts a plugin item's declared resource requirements (required first,\n * then optional). Returns an empty array for UI items or plugins that declare\n * no resources.\n */\nexport function extractRequirements(\n item: RegistryItem,\n): ResourceRequirementRow[] {\n const manifest = findManifest(item);\n if (!manifest) return [];\n return [\n ...toRows(manifest.resources?.required, true),\n ...toRows(manifest.resources?.optional, false),\n ];\n}\n\n/**\n * Renders the resource requirements for an item as human-readable lines.\n * Returns a single \"no resources\" line when there are none, so callers can\n * print unconditionally.\n */\nexport function renderRequirements(\n item: RegistryItem,\n rows: ResourceRequirementRow[] = extractRequirements(item),\n): string {\n if (rows.length === 0) {\n return pc.dim(`${item.name}: no resource requirements.`);\n }\n\n const lines: string[] = [pc.bold(`Resources required by ${item.name}:`)];\n for (const row of rows) {\n const tag = row.required ? pc.yellow(\"required\") : pc.dim(\"optional\");\n const perm = row.permission ? pc.dim(` [${row.permission}]`) : \"\";\n lines.push(` ${pc.cyan(row.type)} (${tag})${perm}`);\n if (row.description) lines.push(` ${pc.dim(row.description)}`);\n for (const field of row.fields) {\n if (!field.env) continue;\n const origin = field.origin ? pc.dim(` (${field.origin})`) : \"\";\n lines.push(` - ${field.env}${origin}`);\n }\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * A syntactically valid environment variable name. Field `env` names come from\n * an untrusted manifest and are written into `.env` as `NAME=value` (a newline\n * in the name would inject a second line — e.g. `PORT=x\\nDATABRICKS_HOST=…` →\n * credential exfil) and emitted into `app.yaml`. Anything that isn't a plain\n * env identifier is dropped before it reaches those sinks.\n */\nconst ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/** True when `name` is a safe, well-formed environment variable name. */\nexport function isValidEnvName(name: string): boolean {\n return ENV_NAME.test(name);\n}\n\n/**\n * Effective origin of a field: trust a synced manifest's stamped `origin`, else\n * derive it from the field shape via the same rule `plugin sync` uses, so an\n * authored manifest (no `origin`) still classifies correctly.\n */\nexport function fieldOrigin(\n field: RequirementField,\n): \"platform\" | \"static\" | \"cli\" | \"user\" {\n if (field.origin)\n return field.origin as \"platform\" | \"static\" | \"cli\" | \"user\";\n return computeOriginFromField(field);\n}\n"],"mappings":";;;;;AA8DA,SAAS,SACP,QACoB;AACpB,QAAO,OAAO,QAAQ,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,QAAQ;EACrD;EACA,KAAK,EAAE;EACP,QAAQ,EAAE;EACV,aAAa,EAAE;EACf,OAAO,EAAE;EACT,SAAS,EAAE;EACX,WAAW,EAAE;EACd,EAAE;;AAGL,SAAS,OACP,WACA,UAC0B;AAC1B,SAAQ,aAAa,EAAE,EAAE,KAAK,OAAO;EACnC,MAAM,EAAE,QAAQ;EAChB,aAAa,EAAE;EACf,YAAY,EAAE;EACd;EACA,aAAa,EAAE;EACf,QAAQ,SAAS,EAAE,OAAO;EAC3B,EAAE;;;AAIL,SAAS,aAAa,MAA0C;CAC9D,MAAM,QAAQ,KAAK,SAAS,EAAE,EAAE,MAC7B,MAAM,KAAK,SAAS,EAAE,UAAU,EAAE,KAAK,KAAK,gBAC9C;AACD,KAAI,CAAC,KAAM,QAAO;AAClB,KAAI;AACF,SAAO,KAAK,MAAM,KAAK,QAAQ;SACzB;AACN,SAAO;;;;;;;;AASX,SAAgB,oBACd,MAC0B;CAC1B,MAAM,WAAW,aAAa,KAAK;AACnC,KAAI,CAAC,SAAU,QAAO,EAAE;AACxB,QAAO,CACL,GAAG,OAAO,SAAS,WAAW,UAAU,KAAK,EAC7C,GAAG,OAAO,SAAS,WAAW,UAAU,MAAM,CAC/C;;;;;;;AAQH,SAAgB,mBACd,MACA,OAAiC,oBAAoB,KAAK,EAClD;AACR,KAAI,KAAK,WAAW,EAClB,QAAO,GAAG,IAAI,GAAG,KAAK,KAAK,6BAA6B;CAG1D,MAAM,QAAkB,CAAC,GAAG,KAAK,yBAAyB,KAAK,KAAK,GAAG,CAAC;AACxE,MAAK,MAAM,OAAO,MAAM;EACtB,MAAM,MAAM,IAAI,WAAW,GAAG,OAAO,WAAW,GAAG,GAAG,IAAI,WAAW;EACrE,MAAM,OAAO,IAAI,aAAa,GAAG,IAAI,KAAK,IAAI,WAAW,GAAG,GAAG;AAC/D,QAAM,KAAK,KAAK,GAAG,KAAK,IAAI,KAAK,CAAC,IAAI,IAAI,GAAG,OAAO;AACpD,MAAI,IAAI,YAAa,OAAM,KAAK,OAAO,GAAG,IAAI,IAAI,YAAY,GAAG;AACjE,OAAK,MAAM,SAAS,IAAI,QAAQ;AAC9B,OAAI,CAAC,MAAM,IAAK;GAChB,MAAM,SAAS,MAAM,SAAS,GAAG,IAAI,KAAK,MAAM,OAAO,GAAG,GAAG;AAC7D,SAAM,KAAK,SAAS,MAAM,MAAM,SAAS;;;AAG7C,QAAO,MAAM,KAAK,KAAK;;;;;;;;;AAUzB,MAAM,WAAW;;AAGjB,SAAgB,eAAe,MAAuB;AACpD,QAAO,SAAS,KAAK,KAAK;;;;;;;AAQ5B,SAAgB,YACd,OACwC;AACxC,KAAI,MAAM,OACR,QAAO,MAAM;AACf,QAAO,uBAAuB,MAAM"}
@@ -1 +1 @@
1
- {"version":3,"file":"server-register.js","names":[],"sources":["../../../../src/cli/commands/registry/server-register.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Lang, parse, type SgNode } from \"@ast-grep/napi\";\nimport { JS_IDENTIFIER } from \"./constants\";\n\n/** Server entry candidates within the server root, in priority order. */\nconst SERVER_FILE_CANDIDATES = [\n \"server.ts\",\n \"index.ts\",\n \"src/server.ts\",\n \"src/index.ts\",\n];\n\nexport interface RegisterResult {\n /** wired = edited; already = plugin was present; skipped = couldn't safely edit. */\n status: \"wired\" | \"already\" | \"skipped\";\n file?: string;\n reason?: string;\n}\n\nfunction findServerFile(serverRoot: string): string | null {\n for (const candidate of SERVER_FILE_CANDIDATES) {\n const p = path.join(serverRoot, candidate);\n if (fs.existsSync(p)) return p;\n }\n return null;\n}\n\n/** The `plugins: [...]` array node inside a createApp call, if present. */\nfunction findPluginsArray(root: SgNode): SgNode | null {\n for (const pair of root.findAll({ rule: { kind: \"pair\" } })) {\n const key = pair.find({ rule: { kind: \"property_identifier\" } });\n if (key?.text() !== \"plugins\") continue;\n const arr = pair.find({ rule: { kind: \"array\" } });\n if (arr) return arr;\n }\n return null;\n}\n\nfunction arrayElementNames(arr: SgNode): Set<string> {\n const names = new Set<string>();\n for (const child of arr.children()) {\n if (child.kind() === \"identifier\") {\n names.add(child.text());\n } else if (child.kind() === \"call_expression\") {\n const callee = child.children()[0];\n if (callee?.kind() === \"identifier\") names.add(callee.text());\n }\n }\n return names;\n}\n\n/** Local binding names introduced by an import statement — default, namespace,\n * and named specifiers, with aliases resolved to the local name. */\nfunction importBindingNames(stmt: SgNode): Set<string> {\n const names = new Set<string>();\n const clause = stmt.find({ rule: { kind: \"import_clause\" } });\n if (!clause) return names;\n for (const child of clause.children()) {\n const kind = child.kind();\n if (kind === \"identifier\") {\n names.add(child.text());\n } else if (kind === \"namespace_import\") {\n const id = child.find({ rule: { kind: \"identifier\" } });\n if (id) names.add(id.text());\n } else if (kind === \"named_imports\") {\n for (const spec of child.findAll({\n rule: { kind: \"import_specifier\" },\n })) {\n const local = spec.field(\"alias\")?.text() ?? spec.field(\"name\")?.text();\n if (local) names.add(local);\n }\n }\n }\n return names;\n}\n\n/**\n * Best-effort: register a plugin in the server entry's `createApp({ plugins })`\n * call by inserting the import and adding it to the array. Only edits the\n * standard shape (a `plugins: [...]` array literal); returns `skipped` otherwise\n * so the caller can fall back to printing manual instructions. Idempotent.\n */\nexport function registerPluginInServer(\n serverRoot: string,\n importPath: string,\n exportName: string,\n): RegisterResult {\n // exportName and importPath are interpolated into the user's server source.\n // Registry items are untrusted, so refuse anything that isn't a plain JS\n // identifier / clean relative module path — prevents code injection via a\n // crafted export name or import path.\n if (!JS_IDENTIFIER.test(exportName)) {\n return { status: \"skipped\", reason: \"invalid plugin export name\" };\n }\n if (!/^[.][./A-Za-z0-9_-]*$/.test(importPath)) {\n return { status: \"skipped\", reason: \"invalid plugin import path\" };\n }\n\n const serverFile = findServerFile(serverRoot);\n if (!serverFile) {\n return { status: \"skipped\", reason: \"no server entry file found\" };\n }\n\n const content = fs.readFileSync(serverFile, \"utf-8\");\n const lang = serverFile.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const root = parse(lang, content).root();\n\n const arr = findPluginsArray(root);\n if (!arr) {\n return {\n status: \"skipped\",\n reason: \"no createApp({ plugins: [...] }) array found\",\n };\n }\n\n const file = path.relative(serverRoot, serverFile);\n if (arrayElementNames(arr).has(exportName)) {\n return { status: \"already\", file };\n }\n\n const edits = [];\n\n // toPlugin exports are factories, registered as a call: `hello()`.\n const newElem = `${exportName}()`;\n\n // Insert before the first element, matching its indentation so the array\n // formatting is preserved (or inline for a single-line array).\n const elementKinds = [\"identifier\", \"call_expression\", \"spread_element\"];\n const firstEl = arr\n .children()\n .find((c) => elementKinds.includes(c.kind() as string));\n if (!firstEl) {\n edits.push(arr.replace(`[${newElem}]`));\n } else {\n const startIdx = firstEl.range().start.index;\n const lineStart = content.lastIndexOf(\"\\n\", startIdx - 1);\n const indent = content.slice(lineStart + 1, startIdx);\n const multiline = lineStart !== -1 && /^[ \\t]*$/.test(indent);\n const sep = multiline ? `,\\n${indent}` : \", \";\n edits.push(firstEl.replace(`${newElem}${sep}${firstEl.text()}`));\n }\n\n // Add the import unless `exportName` is already bound by some import. De-dup\n // on the binding, not the module path: an existing import from the same path\n // under a different name (e.g. `import { HelloPlugin } from \"./plugins/hello\"`)\n // must not suppress this one, or the `${exportName}()` element just added to\n // the array would reference an unimported symbol and the server won't compile.\n const importStmts = root.findAll({ rule: { kind: \"import_statement\" } });\n const hasBinding = importStmts.some((s) =>\n importBindingNames(s).has(exportName),\n );\n const importLine = `import { ${exportName} } from \"${importPath}\";`;\n if (!hasBinding && importStmts.length > 0) {\n const last = importStmts[importStmts.length - 1];\n edits.push(last.replace(`${last.text()}\\n${importLine}`));\n }\n\n let output = root.commitEdits(edits);\n if (!hasBinding && importStmts.length === 0) {\n output = `${importLine}\\n${output}`;\n }\n fs.writeFileSync(serverFile, output);\n\n return { status: \"wired\", file };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"server-register.js","names":[],"sources":["../../../../src/cli/commands/registry/server-register.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { Lang, parse, type SgNode } from \"@ast-grep/napi\";\n\nimport { JS_IDENTIFIER } from \"./constants\";\n\n/** Server entry candidates within the server root, in priority order. */\nconst SERVER_FILE_CANDIDATES = [\n \"server.ts\",\n \"index.ts\",\n \"src/server.ts\",\n \"src/index.ts\",\n];\n\nexport interface RegisterResult {\n /** wired = edited; already = plugin was present; skipped = couldn't safely edit. */\n status: \"wired\" | \"already\" | \"skipped\";\n file?: string;\n reason?: string;\n}\n\nfunction findServerFile(serverRoot: string): string | null {\n for (const candidate of SERVER_FILE_CANDIDATES) {\n const p = path.join(serverRoot, candidate);\n if (fs.existsSync(p)) return p;\n }\n return null;\n}\n\n/** The `plugins: [...]` array node inside a createApp call, if present. */\nfunction findPluginsArray(root: SgNode): SgNode | null {\n for (const pair of root.findAll({ rule: { kind: \"pair\" } })) {\n const key = pair.find({ rule: { kind: \"property_identifier\" } });\n if (key?.text() !== \"plugins\") continue;\n const arr = pair.find({ rule: { kind: \"array\" } });\n if (arr) return arr;\n }\n return null;\n}\n\nfunction arrayElementNames(arr: SgNode): Set<string> {\n const names = new Set<string>();\n for (const child of arr.children()) {\n if (child.kind() === \"identifier\") {\n names.add(child.text());\n } else if (child.kind() === \"call_expression\") {\n const callee = child.children()[0];\n if (callee?.kind() === \"identifier\") names.add(callee.text());\n }\n }\n return names;\n}\n\n/** Local binding names introduced by an import statement — default, namespace,\n * and named specifiers, with aliases resolved to the local name. */\nfunction importBindingNames(stmt: SgNode): Set<string> {\n const names = new Set<string>();\n const clause = stmt.find({ rule: { kind: \"import_clause\" } });\n if (!clause) return names;\n for (const child of clause.children()) {\n const kind = child.kind();\n if (kind === \"identifier\") {\n names.add(child.text());\n } else if (kind === \"namespace_import\") {\n const id = child.find({ rule: { kind: \"identifier\" } });\n if (id) names.add(id.text());\n } else if (kind === \"named_imports\") {\n for (const spec of child.findAll({\n rule: { kind: \"import_specifier\" },\n })) {\n const local = spec.field(\"alias\")?.text() ?? spec.field(\"name\")?.text();\n if (local) names.add(local);\n }\n }\n }\n return names;\n}\n\n/**\n * Best-effort: register a plugin in the server entry's `createApp({ plugins })`\n * call by inserting the import and adding it to the array. Only edits the\n * standard shape (a `plugins: [...]` array literal); returns `skipped` otherwise\n * so the caller can fall back to printing manual instructions. Idempotent.\n */\nexport function registerPluginInServer(\n serverRoot: string,\n importPath: string,\n exportName: string,\n): RegisterResult {\n // exportName and importPath are interpolated into the user's server source.\n // Registry items are untrusted, so refuse anything that isn't a plain JS\n // identifier / clean relative module path — prevents code injection via a\n // crafted export name or import path.\n if (!JS_IDENTIFIER.test(exportName)) {\n return { status: \"skipped\", reason: \"invalid plugin export name\" };\n }\n if (!/^[.][./A-Za-z0-9_-]*$/.test(importPath)) {\n return { status: \"skipped\", reason: \"invalid plugin import path\" };\n }\n\n const serverFile = findServerFile(serverRoot);\n if (!serverFile) {\n return { status: \"skipped\", reason: \"no server entry file found\" };\n }\n\n const content = fs.readFileSync(serverFile, \"utf-8\");\n const lang = serverFile.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const root = parse(lang, content).root();\n\n const arr = findPluginsArray(root);\n if (!arr) {\n return {\n status: \"skipped\",\n reason: \"no createApp({ plugins: [...] }) array found\",\n };\n }\n\n const file = path.relative(serverRoot, serverFile);\n if (arrayElementNames(arr).has(exportName)) {\n return { status: \"already\", file };\n }\n\n const edits = [];\n\n // toPlugin exports are factories, registered as a call: `hello()`.\n const newElem = `${exportName}()`;\n\n // Insert before the first element, matching its indentation so the array\n // formatting is preserved (or inline for a single-line array).\n const elementKinds = [\"identifier\", \"call_expression\", \"spread_element\"];\n const firstEl = arr\n .children()\n .find((c) => elementKinds.includes(c.kind() as string));\n if (!firstEl) {\n edits.push(arr.replace(`[${newElem}]`));\n } else {\n const startIdx = firstEl.range().start.index;\n const lineStart = content.lastIndexOf(\"\\n\", startIdx - 1);\n const indent = content.slice(lineStart + 1, startIdx);\n const multiline = lineStart !== -1 && /^[ \\t]*$/.test(indent);\n const sep = multiline ? `,\\n${indent}` : \", \";\n edits.push(firstEl.replace(`${newElem}${sep}${firstEl.text()}`));\n }\n\n // Add the import unless `exportName` is already bound by some import. De-dup\n // on the binding, not the module path: an existing import from the same path\n // under a different name (e.g. `import { HelloPlugin } from \"./plugins/hello\"`)\n // must not suppress this one, or the `${exportName}()` element just added to\n // the array would reference an unimported symbol and the server won't compile.\n const importStmts = root.findAll({ rule: { kind: \"import_statement\" } });\n const hasBinding = importStmts.some((s) =>\n importBindingNames(s).has(exportName),\n );\n const importLine = `import { ${exportName} } from \"${importPath}\";`;\n if (!hasBinding && importStmts.length > 0) {\n const last = importStmts[importStmts.length - 1];\n edits.push(last.replace(`${last.text()}\\n${importLine}`));\n }\n\n let output = root.commitEdits(edits);\n if (!hasBinding && importStmts.length === 0) {\n output = `${importLine}\\n${output}`;\n }\n fs.writeFileSync(serverFile, output);\n\n return { status: \"wired\", file };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-picker.js","names":[],"sources":["../../../../src/cli/commands/registry/workspace-picker.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport {\n createWorkspaceClient,\n type LegacyWorkspaceClient,\n} from \"../../../workspace-client\";\n\n/**\n * Lists a user's real Databricks workspace resources so `appkit add` can offer\n * a picker instead of blind free-text entry.\n *\n * Flat resource types are listed via the Databricks SDK client (typed,\n * auto-paginating) obtained through the sanctioned `workspace-client` facade.\n * Parent-context types (volume, uc_function, secret, vector_search_index) still\n * shell out to the `databricks` CLI for their drill-down. Every path fails\n * soft: any error returns an empty list and the caller drops to free-text entry.\n */\n\n/** A workspace resource choice surfaced in the picker. */\nexport interface WorkspaceChoice {\n /** Value written to the env var (the resource id/name). */\n value: string;\n /** Human label shown in the picker (name, falling back to value). */\n label: string;\n}\n\n/**\n * Per-type SDK lister: streams a resource type off the WorkspaceClient into\n * `{value,label}` choices. `list()` returns an async iterable of raw SDK\n * objects; `toChoice` maps each to a picker choice. The SDK auto-paginates, so\n * we simply iterate to completion.\n */\ninterface SdkLister {\n list: (client: LegacyWorkspaceClient) => AsyncIterable<unknown>;\n toChoice: (item: Record<string, unknown>) => WorkspaceChoice | null;\n}\n\n/** Builds a `{value,label}` from an id field and optional label field. */\nfunction choiceFrom(\n item: Record<string, unknown>,\n idField: string,\n labelField?: string,\n): WorkspaceChoice | null {\n const id = item[idField];\n if (id === undefined || id === null) return null;\n const value = String(id);\n const rawLabel = labelField ? item[labelField] : undefined;\n const label =\n typeof rawLabel === \"string\" && rawLabel.length > 0\n ? `${rawLabel} (${value})`\n : value;\n return { value, label };\n}\n\n/**\n * Genie listSpaces returns a single page, not an auto-paginating iterable like\n * the other services. Adapt it to one that follows `next_page_token`; the\n * repeated-token guard avoids an infinite loop if the API echoes a token back.\n */\nasync function* iterateGenieSpaces(\n client: LegacyWorkspaceClient,\n): AsyncIterable<unknown> {\n let pageToken: string | undefined;\n do {\n const res = await client.genie.listSpaces(\n pageToken ? { page_token: pageToken } : {},\n );\n for (const space of res.spaces ?? []) yield space;\n const next = res.next_page_token;\n if (next && next === pageToken) break;\n pageToken = next;\n } while (pageToken);\n}\n\n/** Flat, top-level listable resource types, backed by SDK services. */\nexport const SDK_LISTERS: Record<string, SdkLister> = {\n sql_warehouse: {\n list: (c) => c.warehouses.list({}),\n toChoice: (i) => choiceFrom(i, \"id\", \"name\"),\n },\n job: {\n list: (c) => c.jobs.list({}),\n // job name lives under settings.name; id is top-level job_id\n toChoice: (i) => {\n const settings = i.settings as { name?: string } | undefined;\n return choiceFrom({ ...i, name: settings?.name }, \"job_id\", \"name\");\n },\n },\n serving_endpoint: {\n list: (c) => c.servingEndpoints.list(),\n toChoice: (i) => choiceFrom(i, \"name\", \"name\"),\n },\n uc_connection: {\n list: (c) => c.connections.list({}),\n toChoice: (i) => choiceFrom(i, \"name\", \"full_name\"),\n },\n database: {\n list: (c) => c.database.listDatabaseInstances({}),\n toChoice: (i) => choiceFrom(i, \"name\", \"name\"),\n },\n genie_space: {\n list: iterateGenieSpaces,\n toChoice: (i) => choiceFrom(i, \"space_id\", \"title\"),\n },\n experiment: {\n list: (c) => c.experiments.listExperiments({}),\n toChoice: (i) => choiceFrom(i, \"experiment_id\", \"name\"),\n },\n app: {\n list: (c) => c.apps.list({}),\n toChoice: (i) => choiceFrom(i, \"name\", \"name\"),\n },\n};\n\n/** True when a resource type can be listed flat (no parent context). */\nexport function isFlatListable(resourceType: string): boolean {\n return resourceType in SDK_LISTERS;\n}\n\n/**\n * Constructs a raw SDK workspace client for the given profile (or default\n * resolution), via the sanctioned `workspace-client` facade. Uses the legacy\n * escape hatch because the picker needs services (connections, database,\n * experiments, apps) the facade doesn't yet proxy directly.\n */\nexport function makeWorkspaceClient(profile?: string): LegacyWorkspaceClient {\n return createWorkspaceClient(\n profile ? { profile } : {},\n ).toLegacyWorkspaceClient();\n}\n\n/**\n * Max resources fetched for the picker. `list()` auto-paginates, so on a large\n * workspace (5000+ warehouses) breaking out at the cap stops pagination early;\n * the picker's \"Enter manually\" option covers anything beyond it.\n */\nexport const MAX_PICKER_RESULTS = 200;\n\n/** A listing result plus whether it was truncated at the fetch cap. */\nexport interface WorkspaceListing {\n choices: WorkspaceChoice[];\n truncated: boolean;\n /** Short reason the listing failed (auth/config/network), if it did — lets\n * the caller distinguish a real error from a genuinely empty workspace. */\n error?: string;\n}\n\n/** First line of an error, capped, for a user-facing one-liner. */\nfunction shortErrorMessage(err: unknown): string {\n const msg = err instanceof Error ? err.message : String(err);\n return msg.split(\"\\n\")[0].trim().slice(0, 200);\n}\n\n/**\n * Lists workspace resources of a flat-listable type via the SDK, stopping at\n * MAX_PICKER_RESULTS so pagination doesn't drain a huge workspace. Returns an\n * empty listing on any failure (unknown type, auth/config error, network) so\n * the caller can fall back to free-text entry. `clientFactory` is injectable\n * for tests.\n */\nexport async function listWorkspaceResources(\n resourceType: string,\n profile?: string,\n clientFactory: (\n profile?: string,\n ) => LegacyWorkspaceClient = makeWorkspaceClient,\n): Promise<WorkspaceListing> {\n const lister = SDK_LISTERS[resourceType];\n if (!lister) return { choices: [], truncated: false };\n try {\n const client = clientFactory(profile);\n const choices: WorkspaceChoice[] = [];\n let truncated = false;\n for await (const item of lister.list(client)) {\n if (typeof item !== \"object\" || item === null) continue;\n const choice = lister.toChoice(item as Record<string, unknown>);\n if (!choice) continue;\n choices.push(choice);\n if (choices.length >= MAX_PICKER_RESULTS) {\n // Stop iterating — halts the async iterator, so no further pages fetch.\n truncated = true;\n break;\n }\n }\n return { choices, truncated };\n } catch (err) {\n // Don't swallow silently: return the reason so the caller can tell the user\n // the picker fell back because listing failed (typically auth/profile), not\n // because the workspace has none of this resource.\n return { choices: [], truncated: false, error: shortErrorMessage(err) };\n }\n}\n\n/** Runs a databricks CLI subcommand returning JSON; injectable for tests. */\nexport type CliRunner = (args: string[]) => {\n status: number | null;\n stdout: string;\n};\n\nconst defaultRunner: CliRunner = (args) => {\n // Parent-context lists can be large; raise maxBuffer well above the 1MB\n // default so a big JSON response isn't truncated into \"no resources found\".\n const res = spawnSync(\"databricks\", args, {\n encoding: \"utf-8\",\n maxBuffer: 64 * 1024 * 1024,\n });\n return { status: res.status, stdout: res.stdout ?? \"\" };\n};\n\n/**\n * Extracts `{value,label}` choices from a parsed CLI list response. The CLI\n * returns either a bare array or an object wrapping one; we scan for the first\n * array of objects. Items missing the id field are skipped.\n */\nexport function toChoices(\n parsed: unknown,\n idField: string,\n labelField?: string,\n): WorkspaceChoice[] {\n const arr = firstArray(parsed);\n const choices: WorkspaceChoice[] = [];\n for (const item of arr) {\n if (typeof item !== \"object\" || item === null) continue;\n const choice = choiceFrom(\n item as Record<string, unknown>,\n idField,\n labelField,\n );\n if (choice) choices.push(choice);\n }\n return choices;\n}\n\n/** Finds the first array in a CLI response (bare array or single wrapper key). */\nfunction firstArray(parsed: unknown): unknown[] {\n if (Array.isArray(parsed)) return parsed;\n if (parsed && typeof parsed === \"object\") {\n for (const v of Object.values(parsed)) {\n if (Array.isArray(v)) return v;\n }\n }\n return [];\n}\n\n/**\n * Runs a `databricks … list -o json` command and returns parsed choices, or\n * [] on any failure (CLI missing/errored, empty, non-JSON). `command` is the\n * argv after `databricks`; `-o json` and `-p <profile>` are appended.\n * Used for the parent-context drill-down (catalogs/schemas/scopes/endpoints).\n */\nexport function runList(\n command: string[],\n idField: string,\n labelField: string | undefined,\n profile: string | undefined,\n runner: CliRunner = defaultRunner,\n): WorkspaceChoice[] {\n const args = [...command, \"-o\", \"json\"];\n if (profile) args.push(\"-p\", profile);\n\n let result: { status: number | null; stdout: string };\n try {\n result = runner(args);\n } catch {\n return [];\n }\n if (result.status !== 0 || !result.stdout.trim()) return [];\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(result.stdout);\n } catch {\n return [];\n }\n return toChoices(parsed, idField, labelField);\n}\n\n/**\n * A drill-down step for a parent-context resource type. `list(parents)` builds\n * the CLI argv given the values picked in prior steps (e.g. [catalog] → schema\n * list command). `key` labels the step for prompts.\n */\nexport interface ParentContextStep {\n key: string;\n list: (parents: string[]) => {\n command: string[];\n idField: string;\n labelField?: string;\n };\n}\n\n/**\n * Drill-down chains for parent-context resource types. Each ends by listing\n * the resource itself; earlier steps list the parents to pick first.\n * Positional-arg CLI gotcha: `databricks schemas list <CATALOG>` etc. take the\n * parent as a positional, not a flag.\n */\nexport const PARENT_CONTEXT_CHAINS: Record<string, ParentContextStep[]> = {\n volume: [\n {\n key: \"catalog\",\n list: () => ({\n command: [\"catalogs\", \"list\"],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"schema\",\n list: ([catalog]) => ({\n command: [\"schemas\", \"list\", catalog],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"volume\",\n list: ([catalog, schema]) => ({\n command: [\"volumes\", \"list\", catalog, schema],\n idField: \"full_name\",\n labelField: \"name\",\n }),\n },\n ],\n uc_function: [\n {\n key: \"catalog\",\n list: () => ({\n command: [\"catalogs\", \"list\"],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"schema\",\n list: ([catalog]) => ({\n command: [\"schemas\", \"list\", catalog],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"function\",\n list: ([catalog, schema]) => ({\n command: [\"functions\", \"list\", catalog, schema],\n idField: \"full_name\",\n labelField: \"name\",\n }),\n },\n ],\n secret: [\n {\n key: \"scope\",\n list: () => ({\n command: [\"secrets\", \"list-scopes\"],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"key\",\n list: ([scope]) => ({\n command: [\"secrets\", \"list-secrets\", scope],\n idField: \"key\",\n labelField: \"key\",\n }),\n },\n ],\n vector_search_index: [\n {\n key: \"endpoint\",\n list: () => ({\n command: [\"vector-search-endpoints\", \"list-endpoints\"],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"index\",\n list: ([endpoint]) => ({\n command: [\"vector-search-indexes\", \"list-indexes\", endpoint],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n ],\n};\n\n/** True when a resource type needs a parent-context drill-down to list. */\nexport function isParentContext(resourceType: string): boolean {\n return resourceType in PARENT_CONTEXT_CHAINS;\n}\n\n/** One resolved step of a drill-down: the choices to present at this level. */\nexport interface DrillStep {\n key: string;\n choices: WorkspaceChoice[];\n}\n\n/**\n * Lists the choices for a single drill-down step given the values picked so\n * far. Returns [] on failure. The caller drives the interaction (present\n * `choices`, collect a pick, call again with it appended to `parents`).\n */\nexport function listParentContextStep(\n resourceType: string,\n stepIndex: number,\n parents: string[],\n profile?: string,\n runner: CliRunner = defaultRunner,\n): DrillStep | null {\n const chain = PARENT_CONTEXT_CHAINS[resourceType];\n if (!chain || stepIndex >= chain.length) return null;\n const step = chain[stepIndex];\n // Prior picks become positional CLI args; a value starting with `-` would be\n // parsed as a flag, so refuse it (empty step → free-text fallback). Real\n // catalog/schema/scope/endpoint names never start with `-`.\n if (parents.some((p) => p.startsWith(\"-\"))) {\n return { key: step.key, choices: [] };\n }\n const spec = step.list(parents);\n return {\n key: step.key,\n choices: runList(\n spec.command,\n spec.idField,\n spec.labelField,\n profile,\n runner,\n ),\n };\n}\n\n/** Number of drill-down steps for a parent-context type (0 if not one). */\nexport function parentContextDepth(resourceType: string): number {\n return PARENT_CONTEXT_CHAINS[resourceType]?.length ?? 0;\n}\n\n/**\n * Builds the final resource identifier from the values picked across a\n * drill-down. Most types end on a self-qualified id (volume/uc_function list\n * `full_name`; a vector-search index name is already catalog.schema-qualified),\n * so the last pick is the whole answer. A `secret` is addressed by both its\n * scope and key (`scope/key`) — returning only the key drops the scope and\n * yields a value that can't locate the secret — so its picks are joined.\n */\nexport function composeResourceId(\n resourceType: string,\n picks: string[],\n): string {\n if (resourceType === \"secret\") return picks.join(\"/\");\n return picks[picks.length - 1];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"workspace-picker.js","names":[],"sources":["../../../../src/cli/commands/registry/workspace-picker.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\n\nimport {\n createWorkspaceClient,\n type LegacyWorkspaceClient,\n} from \"../../../workspace-client\";\n\n/**\n * Lists a user's real Databricks workspace resources so `appkit add` can offer\n * a picker instead of blind free-text entry.\n *\n * Flat resource types are listed via the Databricks SDK client (typed,\n * auto-paginating) obtained through the sanctioned `workspace-client` facade.\n * Parent-context types (volume, uc_function, secret, vector_search_index) still\n * shell out to the `databricks` CLI for their drill-down. Every path fails\n * soft: any error returns an empty list and the caller drops to free-text entry.\n */\n\n/** A workspace resource choice surfaced in the picker. */\nexport interface WorkspaceChoice {\n /** Value written to the env var (the resource id/name). */\n value: string;\n /** Human label shown in the picker (name, falling back to value). */\n label: string;\n}\n\n/**\n * Per-type SDK lister: streams a resource type off the WorkspaceClient into\n * `{value,label}` choices. `list()` returns an async iterable of raw SDK\n * objects; `toChoice` maps each to a picker choice. The SDK auto-paginates, so\n * we simply iterate to completion.\n */\ninterface SdkLister {\n list: (client: LegacyWorkspaceClient) => AsyncIterable<unknown>;\n toChoice: (item: Record<string, unknown>) => WorkspaceChoice | null;\n}\n\n/** Builds a `{value,label}` from an id field and optional label field. */\nfunction choiceFrom(\n item: Record<string, unknown>,\n idField: string,\n labelField?: string,\n): WorkspaceChoice | null {\n const id = item[idField];\n if (id === undefined || id === null) return null;\n const value = String(id);\n const rawLabel = labelField ? item[labelField] : undefined;\n const label =\n typeof rawLabel === \"string\" && rawLabel.length > 0\n ? `${rawLabel} (${value})`\n : value;\n return { value, label };\n}\n\n/**\n * Genie listSpaces returns a single page, not an auto-paginating iterable like\n * the other services. Adapt it to one that follows `next_page_token`; the\n * repeated-token guard avoids an infinite loop if the API echoes a token back.\n */\nasync function* iterateGenieSpaces(\n client: LegacyWorkspaceClient,\n): AsyncIterable<unknown> {\n let pageToken: string | undefined;\n do {\n const res = await client.genie.listSpaces(\n pageToken ? { page_token: pageToken } : {},\n );\n for (const space of res.spaces ?? []) yield space;\n const next = res.next_page_token;\n if (next && next === pageToken) break;\n pageToken = next;\n } while (pageToken);\n}\n\n/** Flat, top-level listable resource types, backed by SDK services. */\nexport const SDK_LISTERS: Record<string, SdkLister> = {\n sql_warehouse: {\n list: (c) => c.warehouses.list({}),\n toChoice: (i) => choiceFrom(i, \"id\", \"name\"),\n },\n job: {\n list: (c) => c.jobs.list({}),\n // job name lives under settings.name; id is top-level job_id\n toChoice: (i) => {\n const settings = i.settings as { name?: string } | undefined;\n return choiceFrom({ ...i, name: settings?.name }, \"job_id\", \"name\");\n },\n },\n serving_endpoint: {\n list: (c) => c.servingEndpoints.list(),\n toChoice: (i) => choiceFrom(i, \"name\", \"name\"),\n },\n uc_connection: {\n list: (c) => c.connections.list({}),\n toChoice: (i) => choiceFrom(i, \"name\", \"full_name\"),\n },\n database: {\n list: (c) => c.database.listDatabaseInstances({}),\n toChoice: (i) => choiceFrom(i, \"name\", \"name\"),\n },\n genie_space: {\n list: iterateGenieSpaces,\n toChoice: (i) => choiceFrom(i, \"space_id\", \"title\"),\n },\n experiment: {\n list: (c) => c.experiments.listExperiments({}),\n toChoice: (i) => choiceFrom(i, \"experiment_id\", \"name\"),\n },\n app: {\n list: (c) => c.apps.list({}),\n toChoice: (i) => choiceFrom(i, \"name\", \"name\"),\n },\n};\n\n/** True when a resource type can be listed flat (no parent context). */\nexport function isFlatListable(resourceType: string): boolean {\n return resourceType in SDK_LISTERS;\n}\n\n/**\n * Constructs a raw SDK workspace client for the given profile (or default\n * resolution), via the sanctioned `workspace-client` facade. Uses the legacy\n * escape hatch because the picker needs services (connections, database,\n * experiments, apps) the facade doesn't yet proxy directly.\n */\nexport function makeWorkspaceClient(profile?: string): LegacyWorkspaceClient {\n return createWorkspaceClient(\n profile ? { profile } : {},\n ).toLegacyWorkspaceClient();\n}\n\n/**\n * Max resources fetched for the picker. `list()` auto-paginates, so on a large\n * workspace (5000+ warehouses) breaking out at the cap stops pagination early;\n * the picker's \"Enter manually\" option covers anything beyond it.\n */\nexport const MAX_PICKER_RESULTS = 200;\n\n/** A listing result plus whether it was truncated at the fetch cap. */\nexport interface WorkspaceListing {\n choices: WorkspaceChoice[];\n truncated: boolean;\n /** Short reason the listing failed (auth/config/network), if it did — lets\n * the caller distinguish a real error from a genuinely empty workspace. */\n error?: string;\n}\n\n/** First line of an error, capped, for a user-facing one-liner. */\nfunction shortErrorMessage(err: unknown): string {\n const msg = err instanceof Error ? err.message : String(err);\n return msg.split(\"\\n\")[0].trim().slice(0, 200);\n}\n\n/**\n * Lists workspace resources of a flat-listable type via the SDK, stopping at\n * MAX_PICKER_RESULTS so pagination doesn't drain a huge workspace. Returns an\n * empty listing on any failure (unknown type, auth/config error, network) so\n * the caller can fall back to free-text entry. `clientFactory` is injectable\n * for tests.\n */\nexport async function listWorkspaceResources(\n resourceType: string,\n profile?: string,\n clientFactory: (\n profile?: string,\n ) => LegacyWorkspaceClient = makeWorkspaceClient,\n): Promise<WorkspaceListing> {\n const lister = SDK_LISTERS[resourceType];\n if (!lister) return { choices: [], truncated: false };\n try {\n const client = clientFactory(profile);\n const choices: WorkspaceChoice[] = [];\n let truncated = false;\n for await (const item of lister.list(client)) {\n if (typeof item !== \"object\" || item === null) continue;\n const choice = lister.toChoice(item as Record<string, unknown>);\n if (!choice) continue;\n choices.push(choice);\n if (choices.length >= MAX_PICKER_RESULTS) {\n // Stop iterating — halts the async iterator, so no further pages fetch.\n truncated = true;\n break;\n }\n }\n return { choices, truncated };\n } catch (err) {\n // Don't swallow silently: return the reason so the caller can tell the user\n // the picker fell back because listing failed (typically auth/profile), not\n // because the workspace has none of this resource.\n return { choices: [], truncated: false, error: shortErrorMessage(err) };\n }\n}\n\n/** Runs a databricks CLI subcommand returning JSON; injectable for tests. */\nexport type CliRunner = (args: string[]) => {\n status: number | null;\n stdout: string;\n};\n\nconst defaultRunner: CliRunner = (args) => {\n // Parent-context lists can be large; raise maxBuffer well above the 1MB\n // default so a big JSON response isn't truncated into \"no resources found\".\n const res = spawnSync(\"databricks\", args, {\n encoding: \"utf-8\",\n maxBuffer: 64 * 1024 * 1024,\n });\n return { status: res.status, stdout: res.stdout ?? \"\" };\n};\n\n/**\n * Extracts `{value,label}` choices from a parsed CLI list response. The CLI\n * returns either a bare array or an object wrapping one; we scan for the first\n * array of objects. Items missing the id field are skipped.\n */\nexport function toChoices(\n parsed: unknown,\n idField: string,\n labelField?: string,\n): WorkspaceChoice[] {\n const arr = firstArray(parsed);\n const choices: WorkspaceChoice[] = [];\n for (const item of arr) {\n if (typeof item !== \"object\" || item === null) continue;\n const choice = choiceFrom(\n item as Record<string, unknown>,\n idField,\n labelField,\n );\n if (choice) choices.push(choice);\n }\n return choices;\n}\n\n/** Finds the first array in a CLI response (bare array or single wrapper key). */\nfunction firstArray(parsed: unknown): unknown[] {\n if (Array.isArray(parsed)) return parsed;\n if (parsed && typeof parsed === \"object\") {\n for (const v of Object.values(parsed)) {\n if (Array.isArray(v)) return v;\n }\n }\n return [];\n}\n\n/**\n * Runs a `databricks … list -o json` command and returns parsed choices, or\n * [] on any failure (CLI missing/errored, empty, non-JSON). `command` is the\n * argv after `databricks`; `-o json` and `-p <profile>` are appended.\n * Used for the parent-context drill-down (catalogs/schemas/scopes/endpoints).\n */\nexport function runList(\n command: string[],\n idField: string,\n labelField: string | undefined,\n profile: string | undefined,\n runner: CliRunner = defaultRunner,\n): WorkspaceChoice[] {\n const args = [...command, \"-o\", \"json\"];\n if (profile) args.push(\"-p\", profile);\n\n let result: { status: number | null; stdout: string };\n try {\n result = runner(args);\n } catch {\n return [];\n }\n if (result.status !== 0 || !result.stdout.trim()) return [];\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(result.stdout);\n } catch {\n return [];\n }\n return toChoices(parsed, idField, labelField);\n}\n\n/**\n * A drill-down step for a parent-context resource type. `list(parents)` builds\n * the CLI argv given the values picked in prior steps (e.g. [catalog] → schema\n * list command). `key` labels the step for prompts.\n */\nexport interface ParentContextStep {\n key: string;\n list: (parents: string[]) => {\n command: string[];\n idField: string;\n labelField?: string;\n };\n}\n\n/**\n * Drill-down chains for parent-context resource types. Each ends by listing\n * the resource itself; earlier steps list the parents to pick first.\n * Positional-arg CLI gotcha: `databricks schemas list <CATALOG>` etc. take the\n * parent as a positional, not a flag.\n */\nexport const PARENT_CONTEXT_CHAINS: Record<string, ParentContextStep[]> = {\n volume: [\n {\n key: \"catalog\",\n list: () => ({\n command: [\"catalogs\", \"list\"],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"schema\",\n list: ([catalog]) => ({\n command: [\"schemas\", \"list\", catalog],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"volume\",\n list: ([catalog, schema]) => ({\n command: [\"volumes\", \"list\", catalog, schema],\n idField: \"full_name\",\n labelField: \"name\",\n }),\n },\n ],\n uc_function: [\n {\n key: \"catalog\",\n list: () => ({\n command: [\"catalogs\", \"list\"],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"schema\",\n list: ([catalog]) => ({\n command: [\"schemas\", \"list\", catalog],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"function\",\n list: ([catalog, schema]) => ({\n command: [\"functions\", \"list\", catalog, schema],\n idField: \"full_name\",\n labelField: \"name\",\n }),\n },\n ],\n secret: [\n {\n key: \"scope\",\n list: () => ({\n command: [\"secrets\", \"list-scopes\"],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"key\",\n list: ([scope]) => ({\n command: [\"secrets\", \"list-secrets\", scope],\n idField: \"key\",\n labelField: \"key\",\n }),\n },\n ],\n vector_search_index: [\n {\n key: \"endpoint\",\n list: () => ({\n command: [\"vector-search-endpoints\", \"list-endpoints\"],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"index\",\n list: ([endpoint]) => ({\n command: [\"vector-search-indexes\", \"list-indexes\", endpoint],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n ],\n};\n\n/** True when a resource type needs a parent-context drill-down to list. */\nexport function isParentContext(resourceType: string): boolean {\n return resourceType in PARENT_CONTEXT_CHAINS;\n}\n\n/** One resolved step of a drill-down: the choices to present at this level. */\nexport interface DrillStep {\n key: string;\n choices: WorkspaceChoice[];\n}\n\n/**\n * Lists the choices for a single drill-down step given the values picked so\n * far. Returns [] on failure. The caller drives the interaction (present\n * `choices`, collect a pick, call again with it appended to `parents`).\n */\nexport function listParentContextStep(\n resourceType: string,\n stepIndex: number,\n parents: string[],\n profile?: string,\n runner: CliRunner = defaultRunner,\n): DrillStep | null {\n const chain = PARENT_CONTEXT_CHAINS[resourceType];\n if (!chain || stepIndex >= chain.length) return null;\n const step = chain[stepIndex];\n // Prior picks become positional CLI args; a value starting with `-` would be\n // parsed as a flag, so refuse it (empty step → free-text fallback). Real\n // catalog/schema/scope/endpoint names never start with `-`.\n if (parents.some((p) => p.startsWith(\"-\"))) {\n return { key: step.key, choices: [] };\n }\n const spec = step.list(parents);\n return {\n key: step.key,\n choices: runList(\n spec.command,\n spec.idField,\n spec.labelField,\n profile,\n runner,\n ),\n };\n}\n\n/** Number of drill-down steps for a parent-context type (0 if not one). */\nexport function parentContextDepth(resourceType: string): number {\n return PARENT_CONTEXT_CHAINS[resourceType]?.length ?? 0;\n}\n\n/**\n * Builds the final resource identifier from the values picked across a\n * drill-down. Most types end on a self-qualified id (volume/uc_function list\n * `full_name`; a vector-search index name is already catalog.schema-qualified),\n * so the last pick is the whole answer. A `secret` is addressed by both its\n * scope and key (`scope/key`) — returning only the key drops the scope and\n * yields a value that can't locate the secret — so its picks are joined.\n */\nexport function composeResourceId(\n resourceType: string,\n picks: string[],\n): string {\n if (resourceType === \"secret\") return picks.join(\"/\");\n return picks[picks.length - 1];\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"setup.js","names":[],"sources":["../../../src/cli/commands/setup.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command } from \"commander\";\n\nconst PACKAGES = [\n { name: \"@databricks/appkit\", description: \"Backend SDK\" },\n {\n name: \"@databricks/appkit-ui\",\n description: \"UI Integration, Charts, Tables, SSE, and more.\",\n },\n];\n\nconst SECTION_START = \"<!-- appkit-instructions-start -->\";\nconst SECTION_END = \"<!-- appkit-instructions-end -->\";\n\n/**\n * Find which AppKit packages are installed by checking for package.json\n */\nfunction findInstalledPackages() {\n const cwd = process.cwd();\n const installed = [];\n\n for (const pkg of PACKAGES) {\n const packagePath = path.join(\n cwd,\n \"node_modules\",\n pkg.name,\n \"package.json\",\n );\n if (fs.existsSync(packagePath)) {\n installed.push(pkg);\n }\n }\n\n return installed;\n}\n\n/**\n * Generate the AppKit section content\n */\nfunction generateSection(packages: typeof PACKAGES) {\n const links = packages\n .map((pkg) => {\n const docPath = `./node_modules/${pkg.name}/CLAUDE.md`;\n return `- **${pkg.name}** (${pkg.description}): [${docPath}](${docPath})`;\n })\n .join(\"\\n\");\n\n return `${SECTION_START}\n## Databricks AppKit\n\nThis project uses Databricks AppKit packages. For AI assistant guidance on using these packages, refer to:\n\n${links}\n\n### Databricks Skills\n\nFor enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:\n\n\\`\\`\\`bash\ndatabricks aitools install\n\\`\\`\\`\n${SECTION_END}`;\n}\n\n/**\n * Generate standalone CLAUDE.md content (when no existing file)\n */\nfunction generateStandalone(packages: typeof PACKAGES) {\n const links = packages\n .map((pkg) => {\n const docPath = `./node_modules/${pkg.name}/CLAUDE.md`;\n return `- **${pkg.name}** (${pkg.description}): [${docPath}](${docPath})`;\n })\n .join(\"\\n\");\n\n return `# AI Assistant Instructions\n\n${SECTION_START}\n## Databricks AppKit\n\nThis project uses Databricks AppKit packages. For AI assistant guidance on using these packages, refer to:\n\n${links}\n\n### Databricks Skills\n\nFor enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:\n\n\\`\\`\\`bash\ndatabricks aitools install\n\\`\\`\\`\n${SECTION_END}\n`;\n}\n\n/**\n * Update existing content with AppKit section\n */\nfunction updateContent(existingContent: string, packages: typeof PACKAGES) {\n const newSection = generateSection(packages);\n\n // Check if AppKit section already exists\n const startIndex = existingContent.indexOf(SECTION_START);\n const endIndex = existingContent.indexOf(SECTION_END);\n\n if (startIndex !== -1 && endIndex !== -1) {\n // Replace existing section\n const before = existingContent.substring(0, startIndex);\n const after = existingContent.substring(endIndex + SECTION_END.length);\n return before + newSection + after;\n }\n\n // Append section to end\n return `${existingContent.trimEnd()}\\n\\n${newSection}\\n`;\n}\n\n/**\n * Setup command implementation\n */\nfunction runSetup(options: { write?: boolean }) {\n const shouldWrite = options.write;\n\n // Find installed packages\n const installed = findInstalledPackages();\n\n if (installed.length === 0) {\n console.log(\"No @databricks/appkit packages found in node_modules.\");\n console.log(\"\\nInstall at least one of:\");\n for (const pkg of PACKAGES) {\n console.log(` npm install ${pkg.name}`);\n }\n process.exit(1);\n }\n\n console.log(\"Detected packages:\");\n installed.forEach((pkg) => {\n console.log(` ✓ ${pkg.name}`);\n });\n\n const claudePath = path.join(process.cwd(), \"CLAUDE.md\");\n const existingContent = fs.existsSync(claudePath)\n ? fs.readFileSync(claudePath, \"utf-8\")\n : null;\n\n let finalContent: string;\n let action: string;\n\n if (existingContent) {\n finalContent = updateContent(existingContent, installed);\n action = existingContent.includes(SECTION_START) ? \"Updated\" : \"Added to\";\n } else {\n finalContent = generateStandalone(installed);\n action = \"Created\";\n }\n\n if (shouldWrite) {\n fs.writeFileSync(claudePath, finalContent);\n console.log(`\\n✓ ${action} CLAUDE.md`);\n console.log(` Path: ${claudePath}`);\n } else {\n console.log(\"\\nTo create/update CLAUDE.md, run:\");\n console.log(\" npx appkit setup --write\\n\");\n\n if (existingContent) {\n console.log(\n `This will ${\n existingContent.includes(SECTION_START)\n ? \"update the existing\"\n : \"add a new\"\n } AppKit section.\\n`,\n );\n }\n\n console.log(\"Preview of AppKit section:\\n\");\n console.log(\"─\".repeat(50));\n console.log(generateSection(installed));\n console.log(\"─\".repeat(50));\n }\n}\n\nexport const setupCommand = new Command(\"setup\")\n .description(\"Setup CLAUDE.md with AppKit package references\")\n .option(\"-w, --write\", \"Create or update CLAUDE.md file in current directory\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit setup\n $ appkit setup --write`,\n )\n .action(runSetup);\n"],"mappings":";;;;;AAIA,MAAM,WAAW,CACf;CAAE,MAAM;CAAsB,aAAa;CAAe,EAC1D;CACE,MAAM;CACN,aAAa;CACd,CACF;AAED,MAAM,gBAAgB;AACtB,MAAM,cAAc;;;;AAKpB,SAAS,wBAAwB;CAC/B,MAAM,MAAM,QAAQ,KAAK;CACzB,MAAM,YAAY,EAAE;AAEpB,MAAK,MAAM,OAAO,UAAU;EAC1B,MAAM,cAAc,KAAK,KACvB,KACA,gBACA,IAAI,MACJ,eACD;AACD,MAAI,GAAG,WAAW,YAAY,CAC5B,WAAU,KAAK,IAAI;;AAIvB,QAAO;;;;;AAMT,SAAS,gBAAgB,UAA2B;AAQlD,QAAO,GAAG,cAAc;;;;;EAPV,SACX,KAAK,QAAQ;EACZ,MAAM,UAAU,kBAAkB,IAAI,KAAK;AAC3C,SAAO,OAAO,IAAI,KAAK,MAAM,IAAI,YAAY,MAAM,QAAQ,IAAI,QAAQ;GACvE,CACD,KAAK,KAAK,CAOP;;;;;;;;;EASN;;;;;AAMF,SAAS,mBAAmB,UAA2B;AAQrD,QAAO;;EAEP,cAAc;;;;;EATA,SACX,KAAK,QAAQ;EACZ,MAAM,UAAU,kBAAkB,IAAI,KAAK;AAC3C,SAAO,OAAO,IAAI,KAAK,MAAM,IAAI,YAAY,MAAM,QAAQ,IAAI,QAAQ;GACvE,CACD,KAAK,KAAK,CASP;;;;;;;;;EASN,YAAY;;;;;;AAOd,SAAS,cAAc,iBAAyB,UAA2B;CACzE,MAAM,aAAa,gBAAgB,SAAS;CAG5C,MAAM,aAAa,gBAAgB,QAAQ,cAAc;CACzD,MAAM,WAAW,gBAAgB,QAAQ,YAAY;AAErD,KAAI,eAAe,MAAM,aAAa,IAAI;EAExC,MAAM,SAAS,gBAAgB,UAAU,GAAG,WAAW;EACvD,MAAM,QAAQ,gBAAgB,UAAU,WAAW,GAAmB;AACtE,SAAO,SAAS,aAAa;;AAI/B,QAAO,GAAG,gBAAgB,SAAS,CAAC,MAAM,WAAW;;;;;AAMvD,SAAS,SAAS,SAA8B;CAC9C,MAAM,cAAc,QAAQ;CAG5B,MAAM,YAAY,uBAAuB;AAEzC,KAAI,UAAU,WAAW,GAAG;AAC1B,UAAQ,IAAI,wDAAwD;AACpE,UAAQ,IAAI,6BAA6B;AACzC,OAAK,MAAM,OAAO,SAChB,SAAQ,IAAI,iBAAiB,IAAI,OAAO;AAE1C,UAAQ,KAAK,EAAE;;AAGjB,SAAQ,IAAI,qBAAqB;AACjC,WAAU,SAAS,QAAQ;AACzB,UAAQ,IAAI,OAAO,IAAI,OAAO;GAC9B;CAEF,MAAM,aAAa,KAAK,KAAK,QAAQ,KAAK,EAAE,YAAY;CACxD,MAAM,kBAAkB,GAAG,WAAW,WAAW,GAC7C,GAAG,aAAa,YAAY,QAAQ,GACpC;CAEJ,IAAI;CACJ,IAAI;AAEJ,KAAI,iBAAiB;AACnB,iBAAe,cAAc,iBAAiB,UAAU;AACxD,WAAS,gBAAgB,SAAS,cAAc,GAAG,YAAY;QAC1D;AACL,iBAAe,mBAAmB,UAAU;AAC5C,WAAS;;AAGX,KAAI,aAAa;AACf,KAAG,cAAc,YAAY,aAAa;AAC1C,UAAQ,IAAI,OAAO,OAAO,YAAY;AACtC,UAAQ,IAAI,WAAW,aAAa;QAC/B;AACL,UAAQ,IAAI,qCAAqC;AACjD,UAAQ,IAAI,+BAA+B;AAE3C,MAAI,gBACF,SAAQ,IACN,aACE,gBAAgB,SAAS,cAAc,GACnC,wBACA,YACL,oBACF;AAGH,UAAQ,IAAI,+BAA+B;AAC3C,UAAQ,IAAI,IAAI,OAAO,GAAG,CAAC;AAC3B,UAAQ,IAAI,gBAAgB,UAAU,CAAC;AACvC,UAAQ,IAAI,IAAI,OAAO,GAAG,CAAC;;;AAI/B,MAAa,eAAe,IAAI,QAAQ,QAAQ,CAC7C,YAAY,iDAAiD,CAC7D,OAAO,eAAe,uDAAuD,CAC7E,YACC,SACA;;;0BAID,CACA,OAAO,SAAS"}
1
+ {"version":3,"file":"setup.js","names":[],"sources":["../../../src/cli/commands/setup.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { Command } from \"commander\";\n\nconst PACKAGES = [\n { name: \"@databricks/appkit\", description: \"Backend SDK\" },\n {\n name: \"@databricks/appkit-ui\",\n description: \"UI Integration, Charts, Tables, SSE, and more.\",\n },\n];\n\nconst SECTION_START = \"<!-- appkit-instructions-start -->\";\nconst SECTION_END = \"<!-- appkit-instructions-end -->\";\n\n/**\n * Find which AppKit packages are installed by checking for package.json\n */\nfunction findInstalledPackages() {\n const cwd = process.cwd();\n const installed = [];\n\n for (const pkg of PACKAGES) {\n const packagePath = path.join(\n cwd,\n \"node_modules\",\n pkg.name,\n \"package.json\",\n );\n if (fs.existsSync(packagePath)) {\n installed.push(pkg);\n }\n }\n\n return installed;\n}\n\n/**\n * Generate the AppKit section content\n */\nfunction generateSection(packages: typeof PACKAGES) {\n const links = packages\n .map((pkg) => {\n const docPath = `./node_modules/${pkg.name}/CLAUDE.md`;\n return `- **${pkg.name}** (${pkg.description}): [${docPath}](${docPath})`;\n })\n .join(\"\\n\");\n\n return `${SECTION_START}\n## Databricks AppKit\n\nThis project uses Databricks AppKit packages. For AI assistant guidance on using these packages, refer to:\n\n${links}\n\n### Databricks Skills\n\nFor enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:\n\n\\`\\`\\`bash\ndatabricks aitools install\n\\`\\`\\`\n${SECTION_END}`;\n}\n\n/**\n * Generate standalone CLAUDE.md content (when no existing file)\n */\nfunction generateStandalone(packages: typeof PACKAGES) {\n const links = packages\n .map((pkg) => {\n const docPath = `./node_modules/${pkg.name}/CLAUDE.md`;\n return `- **${pkg.name}** (${pkg.description}): [${docPath}](${docPath})`;\n })\n .join(\"\\n\");\n\n return `# AI Assistant Instructions\n\n${SECTION_START}\n## Databricks AppKit\n\nThis project uses Databricks AppKit packages. For AI assistant guidance on using these packages, refer to:\n\n${links}\n\n### Databricks Skills\n\nFor enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:\n\n\\`\\`\\`bash\ndatabricks aitools install\n\\`\\`\\`\n${SECTION_END}\n`;\n}\n\n/**\n * Update existing content with AppKit section\n */\nfunction updateContent(existingContent: string, packages: typeof PACKAGES) {\n const newSection = generateSection(packages);\n\n // Check if AppKit section already exists\n const startIndex = existingContent.indexOf(SECTION_START);\n const endIndex = existingContent.indexOf(SECTION_END);\n\n if (startIndex !== -1 && endIndex !== -1) {\n // Replace existing section\n const before = existingContent.substring(0, startIndex);\n const after = existingContent.substring(endIndex + SECTION_END.length);\n return before + newSection + after;\n }\n\n // Append section to end\n return `${existingContent.trimEnd()}\\n\\n${newSection}\\n`;\n}\n\n/**\n * Setup command implementation\n */\nfunction runSetup(options: { write?: boolean }) {\n const shouldWrite = options.write;\n\n // Find installed packages\n const installed = findInstalledPackages();\n\n if (installed.length === 0) {\n console.log(\"No @databricks/appkit packages found in node_modules.\");\n console.log(\"\\nInstall at least one of:\");\n for (const pkg of PACKAGES) {\n console.log(` npm install ${pkg.name}`);\n }\n process.exit(1);\n }\n\n console.log(\"Detected packages:\");\n installed.forEach((pkg) => {\n console.log(` ✓ ${pkg.name}`);\n });\n\n const claudePath = path.join(process.cwd(), \"CLAUDE.md\");\n const existingContent = fs.existsSync(claudePath)\n ? fs.readFileSync(claudePath, \"utf-8\")\n : null;\n\n let finalContent: string;\n let action: string;\n\n if (existingContent) {\n finalContent = updateContent(existingContent, installed);\n action = existingContent.includes(SECTION_START) ? \"Updated\" : \"Added to\";\n } else {\n finalContent = generateStandalone(installed);\n action = \"Created\";\n }\n\n if (shouldWrite) {\n fs.writeFileSync(claudePath, finalContent);\n console.log(`\\n✓ ${action} CLAUDE.md`);\n console.log(` Path: ${claudePath}`);\n } else {\n console.log(\"\\nTo create/update CLAUDE.md, run:\");\n console.log(\" npx appkit setup --write\\n\");\n\n if (existingContent) {\n console.log(\n `This will ${\n existingContent.includes(SECTION_START)\n ? \"update the existing\"\n : \"add a new\"\n } AppKit section.\\n`,\n );\n }\n\n console.log(\"Preview of AppKit section:\\n\");\n console.log(\"─\".repeat(50));\n console.log(generateSection(installed));\n console.log(\"─\".repeat(50));\n }\n}\n\nexport const setupCommand = new Command(\"setup\")\n .description(\"Setup CLAUDE.md with AppKit package references\")\n .option(\"-w, --write\", \"Create or update CLAUDE.md file in current directory\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit setup\n $ appkit setup --write`,\n )\n .action(runSetup);\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport \"dotenv/config\";\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Command } from \"commander\";\nimport { codemodCommand } from \"./commands/codemod/index.js\";\nimport { docsCommand } from \"./commands/docs.js\";\nimport { doctorCommand } from \"./commands/doctor/index.js\";\nimport { generateTypesCommand } from \"./commands/generate-types.js\";\nimport { lintCommand } from \"./commands/lint.js\";\nimport { pluginCommand } from \"./commands/plugin/index.js\";\nimport { addCommand } from \"./commands/registry/add.js\";\nimport { registryCommand } from \"./commands/registry/index.js\";\nimport { setupCommand } from \"./commands/setup.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst pkgPath = join(__dirname, \"../../package.json\");\nconst pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n\nconst cmd = new Command();\n\ncmd\n .name(\"appkit\")\n .description(\"CLI tools for Databricks AppKit\")\n .version(pkg.version);\n\ncmd.addCommand(setupCommand);\ncmd.addCommand(generateTypesCommand);\ncmd.addCommand(lintCommand);\ncmd.addCommand(docsCommand);\ncmd.addCommand(pluginCommand);\ncmd.addCommand(codemodCommand);\ncmd.addCommand(doctorCommand);\n// Registry commands are executable but hidden from --help while the feature\n// is still in development (registry + add work end-to-end but aren't announced).\ncmd.addCommand(registryCommand, { hidden: true });\ncmd.addCommand(addCommand, { hidden: true });\n\nawait cmd.parseAsync();\n"],"mappings":";;;;;;;;;;;;;;;;;AAiBA,MAAM,UAAU,KADE,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EACzB,qBAAqB;AACrD,MAAM,MAAM,KAAK,MAAM,aAAa,SAAS,QAAQ,CAAC;AAEtD,MAAM,MAAM,IAAI,SAAS;AAEzB,IACG,KAAK,SAAS,CACd,YAAY,kCAAkC,CAC9C,QAAQ,IAAI,QAAQ;AAEvB,IAAI,WAAW,aAAa;AAC5B,IAAI,WAAW,qBAAqB;AACpC,IAAI,WAAW,YAAY;AAC3B,IAAI,WAAW,YAAY;AAC3B,IAAI,WAAW,cAAc;AAC7B,IAAI,WAAW,eAAe;AAC9B,IAAI,WAAW,cAAc;AAG7B,IAAI,WAAW,iBAAiB,EAAE,QAAQ,MAAM,CAAC;AACjD,IAAI,WAAW,YAAY,EAAE,QAAQ,MAAM,CAAC;AAE5C,MAAM,IAAI,YAAY"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport \"dotenv/config\";\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { Command } from \"commander\";\n\nimport { codemodCommand } from \"./commands/codemod/index.js\";\nimport { docsCommand } from \"./commands/docs.js\";\nimport { doctorCommand } from \"./commands/doctor/index.js\";\nimport { generateTypesCommand } from \"./commands/generate-types.js\";\nimport { lintCommand } from \"./commands/lint.js\";\nimport { pluginCommand } from \"./commands/plugin/index.js\";\nimport { addCommand } from \"./commands/registry/add.js\";\nimport { registryCommand } from \"./commands/registry/index.js\";\nimport { setupCommand } from \"./commands/setup.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst pkgPath = join(__dirname, \"../../package.json\");\nconst pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n\nconst cmd = new Command();\n\ncmd\n .name(\"appkit\")\n .description(\"CLI tools for Databricks AppKit\")\n .version(pkg.version);\n\ncmd.addCommand(setupCommand);\ncmd.addCommand(generateTypesCommand);\ncmd.addCommand(lintCommand);\ncmd.addCommand(docsCommand);\ncmd.addCommand(pluginCommand);\ncmd.addCommand(codemodCommand);\ncmd.addCommand(doctorCommand);\n// Registry commands are executable but hidden from --help while the feature\n// is still in development (registry + add work end-to-end but aren't announced).\ncmd.addCommand(registryCommand, { hidden: true });\ncmd.addCommand(addCommand, { hidden: true });\n\nawait cmd.parseAsync();\n"],"mappings":";;;;;;;;;;;;;;;;;AAmBA,MAAM,UAAU,KADE,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EACzB,qBAAqB;AACrD,MAAM,MAAM,KAAK,MAAM,aAAa,SAAS,QAAQ,CAAC;AAEtD,MAAM,MAAM,IAAI,SAAS;AAEzB,IACG,KAAK,SAAS,CACd,YAAY,kCAAkC,CAC9C,QAAQ,IAAI,QAAQ;AAEvB,IAAI,WAAW,aAAa;AAC5B,IAAI,WAAW,qBAAqB;AACpC,IAAI,WAAW,YAAY;AAC3B,IAAI,WAAW,YAAY;AAC3B,IAAI,WAAW,cAAc;AAC7B,IAAI,WAAW,eAAe;AAC9B,IAAI,WAAW,cAAc;AAG7B,IAAI,WAAW,iBAAiB,EAAE,QAAQ,MAAM,CAAC;AACjD,IAAI,WAAW,YAAY,EAAE,QAAQ,MAAM,CAAC;AAE5C,MAAM,IAAI,YAAY"}