@databricks/appkit 0.60.0 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/NOTICE.md +11 -10
  2. package/dist/agents/databricks.js +1 -2
  3. package/dist/agents/databricks.js.map +1 -1
  4. package/dist/agents/supervisor-api.js +1 -2
  5. package/dist/agents/supervisor-api.js.map +1 -1
  6. package/dist/appkit/package.js +1 -1
  7. package/dist/cache/index.js +2 -3
  8. package/dist/cache/index.js.map +1 -1
  9. package/dist/cli/commands/doctor/bundle.js +3 -11
  10. package/dist/cli/commands/doctor/bundle.js.map +1 -1
  11. package/dist/cli/commands/doctor/databricks-client.js +18 -22
  12. package/dist/cli/commands/doctor/databricks-client.js.map +1 -1
  13. package/dist/cli/commands/registry/add.js +410 -0
  14. package/dist/cli/commands/registry/add.js.map +1 -0
  15. package/dist/cli/commands/registry/client.js +94 -0
  16. package/dist/cli/commands/registry/client.js.map +1 -0
  17. package/dist/cli/commands/registry/config-plan.js +110 -0
  18. package/dist/cli/commands/registry/config-plan.js.map +1 -0
  19. package/dist/cli/commands/registry/config-writer.js +176 -0
  20. package/dist/cli/commands/registry/config-writer.js.map +1 -0
  21. package/dist/cli/commands/registry/constants.js +62 -0
  22. package/dist/cli/commands/registry/constants.js.map +1 -0
  23. package/dist/cli/commands/registry/env-reconcile.js +116 -0
  24. package/dist/cli/commands/registry/env-reconcile.js.map +1 -0
  25. package/dist/cli/commands/registry/env-writer.js +198 -0
  26. package/dist/cli/commands/registry/env-writer.js.map +1 -0
  27. package/dist/cli/commands/registry/index.js +25 -0
  28. package/dist/cli/commands/registry/index.js.map +1 -0
  29. package/dist/cli/commands/registry/info.js +45 -0
  30. package/dist/cli/commands/registry/info.js.map +1 -0
  31. package/dist/cli/commands/registry/list.js +130 -0
  32. package/dist/cli/commands/registry/list.js.map +1 -0
  33. package/dist/cli/commands/registry/requirements.js +92 -0
  34. package/dist/cli/commands/registry/requirements.js.map +1 -0
  35. package/dist/cli/commands/registry/server-register.js +124 -0
  36. package/dist/cli/commands/registry/server-register.js.map +1 -0
  37. package/dist/cli/commands/registry/workspace-picker.js +353 -0
  38. package/dist/cli/commands/registry/workspace-picker.js.map +1 -0
  39. package/dist/cli/deploy-config.js +36 -0
  40. package/dist/cli/deploy-config.js.map +1 -0
  41. package/dist/cli/index.js +4 -0
  42. package/dist/cli/index.js.map +1 -1
  43. package/dist/connectors/context.js +1 -2
  44. package/dist/connectors/context.js.map +1 -1
  45. package/dist/connectors/files/client.js +1 -2
  46. package/dist/connectors/files/client.js.map +1 -1
  47. package/dist/connectors/genie/client.js +1 -2
  48. package/dist/connectors/genie/client.js.map +1 -1
  49. package/dist/connectors/jobs/client.js +1 -2
  50. package/dist/connectors/jobs/client.js.map +1 -1
  51. package/dist/connectors/sql-warehouse/client.js +1 -2
  52. package/dist/connectors/sql-warehouse/client.js.map +1 -1
  53. package/dist/context/execution-context.d.ts +0 -1
  54. package/dist/context/execution-context.d.ts.map +1 -1
  55. package/dist/context/service-context.d.ts +1 -2
  56. package/dist/context/service-context.d.ts.map +1 -1
  57. package/dist/context/service-context.js +2 -3
  58. package/dist/context/service-context.js.map +1 -1
  59. package/dist/core/appkit.d.ts +1 -2
  60. package/dist/core/appkit.d.ts.map +1 -1
  61. package/dist/index.d.ts +3 -5
  62. package/dist/index.js +2 -3
  63. package/dist/plugins/files/plugin.js +1 -2
  64. package/dist/plugins/files/plugin.js.map +1 -1
  65. package/dist/plugins/files/types.d.ts +1 -2
  66. package/dist/plugins/files/types.d.ts.map +1 -1
  67. package/dist/plugins/jobs/types.d.ts +1 -2
  68. package/dist/plugins/jobs/types.d.ts.map +1 -1
  69. package/dist/plugins/lakebase/lakebase.js +1 -2
  70. package/dist/plugins/lakebase/lakebase.js.map +1 -1
  71. package/dist/schemas/manifest.js +4 -4
  72. package/dist/schemas/manifest.js.map +1 -1
  73. package/dist/shared/src/index.d.ts +4 -1
  74. package/dist/shared/src/workspace-client/client.js +57 -0
  75. package/dist/shared/src/workspace-client/client.js.map +1 -0
  76. package/dist/shared/src/workspace-client/errors.d.ts +2 -0
  77. package/dist/shared/src/workspace-client/errors.js +3 -0
  78. package/dist/shared/src/workspace-client/factory.d.ts +19 -0
  79. package/dist/shared/src/workspace-client/factory.d.ts.map +1 -0
  80. package/dist/shared/src/workspace-client/factory.js +24 -0
  81. package/dist/shared/src/workspace-client/factory.js.map +1 -0
  82. package/dist/shared/src/workspace-client/index.d.ts +4 -0
  83. package/dist/shared/src/workspace-client/legacy.d.ts +31 -0
  84. package/dist/shared/src/workspace-client/legacy.d.ts.map +1 -0
  85. package/dist/shared/src/workspace-client/legacy.js +23 -0
  86. package/dist/shared/src/workspace-client/legacy.js.map +1 -0
  87. package/dist/shared/src/workspace-client/types.d.ts +49 -0
  88. package/dist/shared/src/workspace-client/types.d.ts.map +1 -0
  89. package/dist/type-generator/index.js +1 -2
  90. package/dist/type-generator/index.js.map +1 -1
  91. package/dist/type-generator/query-registry.js +1 -2
  92. package/dist/type-generator/query-registry.js.map +1 -1
  93. package/dist/type-generator/serving/fetcher.js +1 -2
  94. package/dist/type-generator/serving/fetcher.js.map +1 -1
  95. package/dist/type-generator/serving/generator.js +1 -2
  96. package/dist/type-generator/serving/generator.js.map +1 -1
  97. package/dist/type-generator/vite-plugin.js +1 -2
  98. package/dist/type-generator/vite-plugin.js.map +1 -1
  99. package/dist/workspace-client/index.d.ts +4 -3
  100. package/dist/workspace-client/index.js +2 -2
  101. package/dist/workspace-client/legacy.d.ts +9 -4
  102. package/dist/workspace-client/legacy.d.ts.map +1 -1
  103. package/dist/workspace-client/legacy.js +3 -3
  104. package/dist/workspace-client/legacy.js.map +1 -1
  105. package/dist/workspace-client/types.d.ts +2 -2
  106. package/docs/api/appkit/Function.createWorkspaceClient.md +4 -4
  107. package/docs/api/appkit/Interface.WorkspaceClientOptions.md +11 -0
  108. package/package.json +5 -4
  109. package/sbom.cdx.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/cache/index.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\nimport { createLakebasePool } from \"../connectors/lakebase\";\nimport { getClientOptions } from \"../context/client-options\";\nimport { AppKitError, ExecutionError, InitializationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport type { Counter, TelemetryProvider } from \"../telemetry\";\nimport { SpanStatusCode, TelemetryManager } from \"../telemetry\";\nimport { deepMerge } from \"../utils\";\nimport { ApiError, createWorkspaceClient } from \"../workspace-client\";\nimport { cacheDefaults } from \"./defaults\";\nimport { InMemoryStorage, PersistentStorage } from \"./storage\";\n\nconst logger = createLogger(\"cache\");\n\n/**\n * Reference-counted in-flight cache execution entry.\n *\n * `sharedController` decouples the cached `fn()` from any single caller's\n * abort signal. Callers join an in-flight entry by incrementing `refCount`;\n * when a caller aborts, refCount is decremented. The shared controller is\n * aborted only when refCount drops to 0 — i.e. all callers have abandoned\n * the request. This prevents one caller's cancellation (e.g. React\n * StrictMode unmount) from poisoning the in-flight result for other still-\n * connected awaiters.\n */\ninterface InFlightEntry<T> {\n promise: Promise<T>;\n refCount: number;\n sharedController: AbortController;\n abortTimer?: ReturnType<typeof setTimeout>;\n}\n\nfunction createAbortError(signal: AbortSignal): unknown {\n if (signal.reason !== undefined) return signal.reason;\n return new DOMException(\"The operation was aborted.\", \"AbortError\");\n}\n\n/**\n * Cache manager class to handle cache operations.\n * Can be used with in-memory storage or persistent storage (Lakebase).\n *\n * The cache is automatically initialized by AppKit. Use `getInstanceSync()` to access\n * the singleton instance after initialization.\n *\n * @internal\n * @example\n * ```typescript\n * const cache = CacheManager.getInstanceSync();\n * const result = await cache.getOrExecute([\"users\", userId], () => fetchUser(userId), userKey);\n * ```\n */\nexport class CacheManager {\n private static readonly MIN_CLEANUP_INTERVAL_MS = 60_000;\n private static readonly ABORT_GRACE_PERIOD_MS = 100;\n private readonly name: string = \"cache-manager\";\n private static instance: CacheManager | null = null;\n private static initPromise: Promise<CacheManager> | null = null;\n\n private storage: CacheStorage;\n private config: CacheConfig;\n private inFlightRequests: Map<string, InFlightEntry<unknown>>;\n private cleanupInProgress: boolean;\n private lastCleanupAttempt: number;\n\n private telemetry: TelemetryProvider;\n private telemetryMetrics: {\n cacheHitCount: Counter;\n cacheMissCount: Counter;\n };\n\n private constructor(storage: CacheStorage, config: CacheConfig) {\n this.storage = storage;\n this.config = config;\n this.inFlightRequests = new Map();\n this.cleanupInProgress = false;\n this.lastCleanupAttempt = 0;\n\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.telemetryMetrics = {\n cacheHitCount: this.telemetry.getMeter().createCounter(\"cache.hit\", {\n description: \"Total number of cache hits\",\n unit: \"1\",\n }),\n cacheMissCount: this.telemetry.getMeter().createCounter(\"cache.miss\", {\n description: \"Total number of cache misses\",\n unit: \"1\",\n }),\n };\n }\n\n /**\n * Get the singleton instance of the cache manager (sync version).\n *\n * Throws if not initialized - ensure AppKit.create() has completed first.\n * @returns CacheManager instance\n */\n static getInstanceSync(): CacheManager {\n if (!CacheManager.instance) {\n throw InitializationError.notInitialized(\n \"CacheManager\",\n \"Ensure AppKit.create() has completed before accessing the cache\",\n );\n }\n\n return CacheManager.instance;\n }\n\n /**\n * Initialize and get the singleton instance of the cache manager.\n * Called internally by AppKit - prefer `getInstanceSync()` for plugin access.\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n * @internal\n */\n static async getInstance(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n if (CacheManager.instance) {\n return CacheManager.instance;\n }\n\n if (!CacheManager.initPromise) {\n CacheManager.initPromise = CacheManager.create(userConfig).then(\n (instance) => {\n CacheManager.instance = instance;\n return instance;\n },\n );\n }\n\n return CacheManager.initPromise;\n }\n\n /**\n * Create a new cache manager instance\n *\n * Storage selection logic:\n * 1. If `storage` provided and healthy → use provided storage\n * 2. If `storage` provided but unhealthy → fallback to InMemory (or disable if strictPersistence)\n * 3. If no `storage` provided and Lakebase available → use Lakebase\n * 4. If no `storage` provided and Lakebase unavailable → fallback to InMemory (or disable if strictPersistence)\n *\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n */\n private static async create(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n const config = deepMerge(cacheDefaults, userConfig);\n\n if (config.storage) {\n const isHealthy = await config.storage.healthCheck();\n if (isHealthy) {\n return new CacheManager(config.storage, config);\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n // try to use lakebase storage\n try {\n const workspaceClient = createWorkspaceClient({\n clientOptions: getClientOptions(),\n }).toLegacyWorkspaceClient();\n const pool = createLakebasePool({ workspaceClient });\n const persistentStorage = new PersistentStorage(config, pool);\n\n const isHealthy = await persistentStorage.healthCheck();\n if (isHealthy) {\n await persistentStorage.initialize();\n return new CacheManager(persistentStorage, config);\n }\n\n // Health check failed, close the pool and fallback\n await pool.end();\n } catch {\n // lakebase unavailable, continue with in-memory storage\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n /**\n * Get or execute a function and cache the result.\n *\n * Multiple concurrent callers with the same `cacheKey` are deduplicated\n * onto a single in-flight execution. Each caller may pass its own\n * `callerSignal`; the underlying `fn()` is run with a shared, internally\n * managed `AbortSignal` that aborts only when *all* callers have\n * abandoned the request (reference counted). This decouples a single\n * caller's cancellation (e.g. React StrictMode unmount) from the shared\n * result, so other still-connected callers receive the cached value\n * normally.\n *\n * @param key - Cache key parts\n * @param fn - Function to execute. Receives the cache-owned shared signal;\n * pass it through to the underlying I/O so the work is cancelled when\n * no caller is left waiting.\n * @param userKey - User key for cache namespacing\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async getOrExecute<T>(\n key: (string | number | object)[],\n fn: (sharedSignal?: AbortSignal) => Promise<T>,\n userKey: string,\n options?: { ttl?: number; callerSignal?: AbortSignal },\n ): Promise<T> {\n if (!this.config.enabled) return fn(options?.callerSignal);\n\n const callerSignal = options?.callerSignal;\n if (callerSignal?.aborted) {\n throw createAbortError(callerSignal);\n }\n\n const cacheKey = this.generateKey(key, userKey);\n\n return this.telemetry.startActiveSpan(\n \"cache.getOrExecute\",\n {\n attributes: {\n \"cache.key\": cacheKey,\n \"cache.enabled\": this.config.enabled,\n \"cache.persistent\": this.storage.isPersistent(),\n },\n },\n async (span) => {\n try {\n const cached = await this.getValid<T>(cacheKey);\n if (cached !== null) {\n span.setAttribute(\"cache.hit\", true);\n span.setStatus({ code: SpanStatusCode.OK });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n });\n\n return cached.value;\n }\n\n // check if the value is being processed by another request — join\n // the existing in-flight entry under reference counting so this\n // caller's abort doesn't poison the shared result.\n const existing = this.inFlightRequests.get(cacheKey) as\n | InFlightEntry<T>\n | undefined;\n if (existing && !existing.sharedController.signal.aborted) {\n existing.refCount++;\n // Cancel any pending abort timer — a new caller has joined\n if (existing.abortTimer) {\n clearTimeout(existing.abortTimer);\n existing.abortTimer = undefined;\n }\n span.setAttribute(\"cache.hit\", true);\n span.setAttribute(\"cache.deduplication\", true);\n span.addEvent(\"cache.deduplication_used\", {\n \"cache.key\": cacheKey,\n });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n \"cache.deduplication\": \"true\",\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n cache_deduplication: true,\n });\n\n return await this._waitWithRefCount(existing, callerSignal);\n }\n\n // cache miss - execute function under a shared abort controller\n span.setAttribute(\"cache.hit\", false);\n span.addEvent(\"cache.miss\", { \"cache.key\": cacheKey });\n this.telemetryMetrics.cacheMissCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: false,\n cache_key: cacheKey,\n });\n\n const sharedController = new AbortController();\n const entry: InFlightEntry<T> = {\n promise: undefined as unknown as Promise<T>,\n refCount: 1,\n sharedController,\n };\n\n entry.promise = fn(sharedController.signal)\n .then(async (result) => {\n await this.set(cacheKey, result, options);\n span.addEvent(\"cache.value_stored\", {\n \"cache.key\": cacheKey,\n \"cache.ttl\": options?.ttl ?? this.config.ttl ?? 3600,\n });\n return result;\n })\n .catch((error) => {\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n // If the shared controller aborted, all callers have already\n // abandoned the request (or are about to via their own signals)\n // — propagate the original error without wrapping. No live\n // awaiter will observe this rejection.\n if (sharedController.signal.aborted) {\n throw error;\n }\n if (error instanceof AppKitError || error instanceof ApiError) {\n throw error;\n }\n throw ExecutionError.statementFailed(\n error instanceof Error ? error.message : String(error),\n );\n })\n .finally(() => {\n if (this.inFlightRequests.get(cacheKey) === entry) {\n this.inFlightRequests.delete(cacheKey);\n }\n });\n\n // Suppress unhandled rejection warnings when every caller bailed\n // before fn() resolved (their own promises rejected via\n // waitWithRefCount; the underlying entry.promise has no awaiter).\n entry.promise.catch(() => {});\n\n this.inFlightRequests.set(cacheKey, entry as InFlightEntry<unknown>);\n\n const result = await this._waitWithRefCount(entry, callerSignal);\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n } finally {\n span.end();\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n /**\n * Wait on an in-flight entry, racing the underlying promise against the\n * caller's abort signal. When the caller aborts, the entry's refCount is\n * decremented; if it hits zero the shared controller is aborted so the\n * underlying `fn()` can stop. Other callers continue to await the same\n * entry and receive the result when it arrives.\n */\n private _waitWithRefCount<T>(\n entry: InFlightEntry<T>,\n callerSignal?: AbortSignal,\n ): Promise<T> {\n if (!callerSignal) return entry.promise;\n\n return new Promise<T>((resolve, reject) => {\n let settled = false;\n\n const release = () => {\n if (entry.refCount > 0) entry.refCount--;\n if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) {\n // Grace period: delay abort so a StrictMode remount can join\n // the in-flight entry before the shared execution is cancelled.\n entry.abortTimer = setTimeout(() => {\n if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) {\n entry.sharedController.abort(\n callerSignal.reason ?? \"all cache callers aborted\",\n );\n }\n }, CacheManager.ABORT_GRACE_PERIOD_MS);\n }\n };\n\n const onAbort = () => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n release();\n reject(createAbortError(callerSignal));\n };\n\n if (callerSignal.aborted) {\n onAbort();\n return;\n }\n\n callerSignal.addEventListener(\"abort\", onAbort, { once: true });\n\n entry.promise.then(\n (value) => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n resolve(value);\n },\n (error) => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n reject(error);\n },\n );\n });\n }\n\n /**\n * Get a cached value\n * @param key - Cache key\n * @returns Promise of the value or null if not found or expired\n */\n async get<T>(key: string): Promise<T | null> {\n if (!this.config.enabled) return null;\n\n // probabilistic cleanup trigger\n this.maybeCleanup();\n\n const entry = await this.getValid<T>(key);\n return entry?.value ?? null;\n }\n\n /**\n * Get a cached entry only if it has not expired.\n * Returns null on miss or expired (and deletes the expired entry).\n *\n * Storage implementations return entries unconditionally — expiry handling\n * lives at the CacheManager layer.\n */\n private async getValid<T>(key: string): Promise<CacheEntry<T> | null> {\n const entry = await this.storage.get<T>(key);\n if (!entry) return null;\n\n if (Date.now() > entry.expiry) {\n await this.storage.delete(key);\n return null;\n }\n return entry;\n }\n\n /** Probabilistically trigger cleanup of expired entries (fire-and-forget) */\n private maybeCleanup(): void {\n if (this.cleanupInProgress) return;\n if (!this.storage.isPersistent()) return;\n const now = Date.now();\n if (now - this.lastCleanupAttempt < CacheManager.MIN_CLEANUP_INTERVAL_MS)\n return;\n\n const probability = this.config.cleanupProbability ?? 0.01;\n\n if (Math.random() > probability) return;\n\n this.lastCleanupAttempt = now;\n\n this.cleanupInProgress = true;\n (this.storage as PersistentStorage)\n .cleanupExpired()\n .catch((error) => {\n logger.debug(\"Error cleaning up expired entries: %O\", error);\n })\n .finally(() => {\n this.cleanupInProgress = false;\n });\n }\n\n /**\n * Set a value in the cache\n * @param key - Cache key\n * @param value - Value to set\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async set<T>(\n key: string,\n value: T,\n options?: { ttl?: number },\n ): Promise<void> {\n if (!this.config.enabled) return;\n\n const ttl = options?.ttl ?? this.config.ttl ?? 3600;\n const expiryTime = Date.now() + ttl * 1000;\n await this.storage.set(key, { value, expiry: expiryTime });\n }\n\n /**\n * Delete a value from the cache\n * @param key - Cache key\n * @returns Promise of the result\n */\n async delete(key: string): Promise<void> {\n if (!this.config.enabled) return;\n await this.storage.delete(key);\n }\n\n /** Clear the cache */\n async clear(): Promise<void> {\n await this.storage.clear();\n for (const entry of this.inFlightRequests.values()) {\n if (entry.abortTimer) clearTimeout(entry.abortTimer);\n }\n this.inFlightRequests.clear();\n }\n\n /**\n * Check if a value exists in the cache\n * @param key - Cache key\n * @returns Promise of true if the value exists, false otherwise\n */\n async has(key: string): Promise<boolean> {\n if (!this.config.enabled) return false;\n\n const entry = await this.getValid(key);\n return entry !== null;\n }\n\n /**\n * Generate a cache key\n * @param parts - Parts of the key\n * @param userKey - User key\n * @returns Cache key\n */\n generateKey(parts: (string | number | object)[], userKey: string): string {\n const allParts = [userKey, ...parts];\n const serialized = JSON.stringify(allParts);\n return createHash(\"sha256\").update(serialized).digest(\"hex\");\n }\n\n /** Close the cache */\n async close(): Promise<void> {\n await this.storage.close();\n }\n\n /**\n * Check if the storage is healthy\n * @returns Promise of true if the storage is healthy, false otherwise\n */\n async isStorageHealthy(): Promise<boolean> {\n return this.storage.healthCheck();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/cache/index.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\nimport { createLakebasePool } from \"../connectors/lakebase\";\nimport { getClientOptions } from \"../context/client-options\";\nimport { AppKitError, ExecutionError, InitializationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport type { Counter, TelemetryProvider } from \"../telemetry\";\nimport { SpanStatusCode, TelemetryManager } from \"../telemetry\";\nimport { deepMerge } from \"../utils\";\nimport { ApiError, createWorkspaceClient } from \"../workspace-client\";\nimport { cacheDefaults } from \"./defaults\";\nimport { InMemoryStorage, PersistentStorage } from \"./storage\";\n\nconst logger = createLogger(\"cache\");\n\n/**\n * Reference-counted in-flight cache execution entry.\n *\n * `sharedController` decouples the cached `fn()` from any single caller's\n * abort signal. Callers join an in-flight entry by incrementing `refCount`;\n * when a caller aborts, refCount is decremented. The shared controller is\n * aborted only when refCount drops to 0 — i.e. all callers have abandoned\n * the request. This prevents one caller's cancellation (e.g. React\n * StrictMode unmount) from poisoning the in-flight result for other still-\n * connected awaiters.\n */\ninterface InFlightEntry<T> {\n promise: Promise<T>;\n refCount: number;\n sharedController: AbortController;\n abortTimer?: ReturnType<typeof setTimeout>;\n}\n\nfunction createAbortError(signal: AbortSignal): unknown {\n if (signal.reason !== undefined) return signal.reason;\n return new DOMException(\"The operation was aborted.\", \"AbortError\");\n}\n\n/**\n * Cache manager class to handle cache operations.\n * Can be used with in-memory storage or persistent storage (Lakebase).\n *\n * The cache is automatically initialized by AppKit. Use `getInstanceSync()` to access\n * the singleton instance after initialization.\n *\n * @internal\n * @example\n * ```typescript\n * const cache = CacheManager.getInstanceSync();\n * const result = await cache.getOrExecute([\"users\", userId], () => fetchUser(userId), userKey);\n * ```\n */\nexport class CacheManager {\n private static readonly MIN_CLEANUP_INTERVAL_MS = 60_000;\n private static readonly ABORT_GRACE_PERIOD_MS = 100;\n private readonly name: string = \"cache-manager\";\n private static instance: CacheManager | null = null;\n private static initPromise: Promise<CacheManager> | null = null;\n\n private storage: CacheStorage;\n private config: CacheConfig;\n private inFlightRequests: Map<string, InFlightEntry<unknown>>;\n private cleanupInProgress: boolean;\n private lastCleanupAttempt: number;\n\n private telemetry: TelemetryProvider;\n private telemetryMetrics: {\n cacheHitCount: Counter;\n cacheMissCount: Counter;\n };\n\n private constructor(storage: CacheStorage, config: CacheConfig) {\n this.storage = storage;\n this.config = config;\n this.inFlightRequests = new Map();\n this.cleanupInProgress = false;\n this.lastCleanupAttempt = 0;\n\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.telemetryMetrics = {\n cacheHitCount: this.telemetry.getMeter().createCounter(\"cache.hit\", {\n description: \"Total number of cache hits\",\n unit: \"1\",\n }),\n cacheMissCount: this.telemetry.getMeter().createCounter(\"cache.miss\", {\n description: \"Total number of cache misses\",\n unit: \"1\",\n }),\n };\n }\n\n /**\n * Get the singleton instance of the cache manager (sync version).\n *\n * Throws if not initialized - ensure AppKit.create() has completed first.\n * @returns CacheManager instance\n */\n static getInstanceSync(): CacheManager {\n if (!CacheManager.instance) {\n throw InitializationError.notInitialized(\n \"CacheManager\",\n \"Ensure AppKit.create() has completed before accessing the cache\",\n );\n }\n\n return CacheManager.instance;\n }\n\n /**\n * Initialize and get the singleton instance of the cache manager.\n * Called internally by AppKit - prefer `getInstanceSync()` for plugin access.\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n * @internal\n */\n static async getInstance(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n if (CacheManager.instance) {\n return CacheManager.instance;\n }\n\n if (!CacheManager.initPromise) {\n CacheManager.initPromise = CacheManager.create(userConfig).then(\n (instance) => {\n CacheManager.instance = instance;\n return instance;\n },\n );\n }\n\n return CacheManager.initPromise;\n }\n\n /**\n * Create a new cache manager instance\n *\n * Storage selection logic:\n * 1. If `storage` provided and healthy → use provided storage\n * 2. If `storage` provided but unhealthy → fallback to InMemory (or disable if strictPersistence)\n * 3. If no `storage` provided and Lakebase available → use Lakebase\n * 4. If no `storage` provided and Lakebase unavailable → fallback to InMemory (or disable if strictPersistence)\n *\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n */\n private static async create(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n const config = deepMerge(cacheDefaults, userConfig);\n\n if (config.storage) {\n const isHealthy = await config.storage.healthCheck();\n if (isHealthy) {\n return new CacheManager(config.storage, config);\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n // try to use lakebase storage\n try {\n const workspaceClient = createWorkspaceClient({\n clientOptions: getClientOptions(),\n }).toLegacyWorkspaceClient();\n const pool = createLakebasePool({ workspaceClient });\n const persistentStorage = new PersistentStorage(config, pool);\n\n const isHealthy = await persistentStorage.healthCheck();\n if (isHealthy) {\n await persistentStorage.initialize();\n return new CacheManager(persistentStorage, config);\n }\n\n // Health check failed, close the pool and fallback\n await pool.end();\n } catch {\n // lakebase unavailable, continue with in-memory storage\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n /**\n * Get or execute a function and cache the result.\n *\n * Multiple concurrent callers with the same `cacheKey` are deduplicated\n * onto a single in-flight execution. Each caller may pass its own\n * `callerSignal`; the underlying `fn()` is run with a shared, internally\n * managed `AbortSignal` that aborts only when *all* callers have\n * abandoned the request (reference counted). This decouples a single\n * caller's cancellation (e.g. React StrictMode unmount) from the shared\n * result, so other still-connected callers receive the cached value\n * normally.\n *\n * @param key - Cache key parts\n * @param fn - Function to execute. Receives the cache-owned shared signal;\n * pass it through to the underlying I/O so the work is cancelled when\n * no caller is left waiting.\n * @param userKey - User key for cache namespacing\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async getOrExecute<T>(\n key: (string | number | object)[],\n fn: (sharedSignal?: AbortSignal) => Promise<T>,\n userKey: string,\n options?: { ttl?: number; callerSignal?: AbortSignal },\n ): Promise<T> {\n if (!this.config.enabled) return fn(options?.callerSignal);\n\n const callerSignal = options?.callerSignal;\n if (callerSignal?.aborted) {\n throw createAbortError(callerSignal);\n }\n\n const cacheKey = this.generateKey(key, userKey);\n\n return this.telemetry.startActiveSpan(\n \"cache.getOrExecute\",\n {\n attributes: {\n \"cache.key\": cacheKey,\n \"cache.enabled\": this.config.enabled,\n \"cache.persistent\": this.storage.isPersistent(),\n },\n },\n async (span) => {\n try {\n const cached = await this.getValid<T>(cacheKey);\n if (cached !== null) {\n span.setAttribute(\"cache.hit\", true);\n span.setStatus({ code: SpanStatusCode.OK });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n });\n\n return cached.value;\n }\n\n // check if the value is being processed by another request — join\n // the existing in-flight entry under reference counting so this\n // caller's abort doesn't poison the shared result.\n const existing = this.inFlightRequests.get(cacheKey) as\n | InFlightEntry<T>\n | undefined;\n if (existing && !existing.sharedController.signal.aborted) {\n existing.refCount++;\n // Cancel any pending abort timer — a new caller has joined\n if (existing.abortTimer) {\n clearTimeout(existing.abortTimer);\n existing.abortTimer = undefined;\n }\n span.setAttribute(\"cache.hit\", true);\n span.setAttribute(\"cache.deduplication\", true);\n span.addEvent(\"cache.deduplication_used\", {\n \"cache.key\": cacheKey,\n });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n \"cache.deduplication\": \"true\",\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n cache_deduplication: true,\n });\n\n return await this._waitWithRefCount(existing, callerSignal);\n }\n\n // cache miss - execute function under a shared abort controller\n span.setAttribute(\"cache.hit\", false);\n span.addEvent(\"cache.miss\", { \"cache.key\": cacheKey });\n this.telemetryMetrics.cacheMissCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: false,\n cache_key: cacheKey,\n });\n\n const sharedController = new AbortController();\n const entry: InFlightEntry<T> = {\n promise: undefined as unknown as Promise<T>,\n refCount: 1,\n sharedController,\n };\n\n entry.promise = fn(sharedController.signal)\n .then(async (result) => {\n await this.set(cacheKey, result, options);\n span.addEvent(\"cache.value_stored\", {\n \"cache.key\": cacheKey,\n \"cache.ttl\": options?.ttl ?? this.config.ttl ?? 3600,\n });\n return result;\n })\n .catch((error) => {\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n // If the shared controller aborted, all callers have already\n // abandoned the request (or are about to via their own signals)\n // — propagate the original error without wrapping. No live\n // awaiter will observe this rejection.\n if (sharedController.signal.aborted) {\n throw error;\n }\n if (error instanceof AppKitError || error instanceof ApiError) {\n throw error;\n }\n throw ExecutionError.statementFailed(\n error instanceof Error ? error.message : String(error),\n );\n })\n .finally(() => {\n if (this.inFlightRequests.get(cacheKey) === entry) {\n this.inFlightRequests.delete(cacheKey);\n }\n });\n\n // Suppress unhandled rejection warnings when every caller bailed\n // before fn() resolved (their own promises rejected via\n // waitWithRefCount; the underlying entry.promise has no awaiter).\n entry.promise.catch(() => {});\n\n this.inFlightRequests.set(cacheKey, entry as InFlightEntry<unknown>);\n\n const result = await this._waitWithRefCount(entry, callerSignal);\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n } finally {\n span.end();\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n /**\n * Wait on an in-flight entry, racing the underlying promise against the\n * caller's abort signal. When the caller aborts, the entry's refCount is\n * decremented; if it hits zero the shared controller is aborted so the\n * underlying `fn()` can stop. Other callers continue to await the same\n * entry and receive the result when it arrives.\n */\n private _waitWithRefCount<T>(\n entry: InFlightEntry<T>,\n callerSignal?: AbortSignal,\n ): Promise<T> {\n if (!callerSignal) return entry.promise;\n\n return new Promise<T>((resolve, reject) => {\n let settled = false;\n\n const release = () => {\n if (entry.refCount > 0) entry.refCount--;\n if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) {\n // Grace period: delay abort so a StrictMode remount can join\n // the in-flight entry before the shared execution is cancelled.\n entry.abortTimer = setTimeout(() => {\n if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) {\n entry.sharedController.abort(\n callerSignal.reason ?? \"all cache callers aborted\",\n );\n }\n }, CacheManager.ABORT_GRACE_PERIOD_MS);\n }\n };\n\n const onAbort = () => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n release();\n reject(createAbortError(callerSignal));\n };\n\n if (callerSignal.aborted) {\n onAbort();\n return;\n }\n\n callerSignal.addEventListener(\"abort\", onAbort, { once: true });\n\n entry.promise.then(\n (value) => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n resolve(value);\n },\n (error) => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n reject(error);\n },\n );\n });\n }\n\n /**\n * Get a cached value\n * @param key - Cache key\n * @returns Promise of the value or null if not found or expired\n */\n async get<T>(key: string): Promise<T | null> {\n if (!this.config.enabled) return null;\n\n // probabilistic cleanup trigger\n this.maybeCleanup();\n\n const entry = await this.getValid<T>(key);\n return entry?.value ?? null;\n }\n\n /**\n * Get a cached entry only if it has not expired.\n * Returns null on miss or expired (and deletes the expired entry).\n *\n * Storage implementations return entries unconditionally — expiry handling\n * lives at the CacheManager layer.\n */\n private async getValid<T>(key: string): Promise<CacheEntry<T> | null> {\n const entry = await this.storage.get<T>(key);\n if (!entry) return null;\n\n if (Date.now() > entry.expiry) {\n await this.storage.delete(key);\n return null;\n }\n return entry;\n }\n\n /** Probabilistically trigger cleanup of expired entries (fire-and-forget) */\n private maybeCleanup(): void {\n if (this.cleanupInProgress) return;\n if (!this.storage.isPersistent()) return;\n const now = Date.now();\n if (now - this.lastCleanupAttempt < CacheManager.MIN_CLEANUP_INTERVAL_MS)\n return;\n\n const probability = this.config.cleanupProbability ?? 0.01;\n\n if (Math.random() > probability) return;\n\n this.lastCleanupAttempt = now;\n\n this.cleanupInProgress = true;\n (this.storage as PersistentStorage)\n .cleanupExpired()\n .catch((error) => {\n logger.debug(\"Error cleaning up expired entries: %O\", error);\n })\n .finally(() => {\n this.cleanupInProgress = false;\n });\n }\n\n /**\n * Set a value in the cache\n * @param key - Cache key\n * @param value - Value to set\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async set<T>(\n key: string,\n value: T,\n options?: { ttl?: number },\n ): Promise<void> {\n if (!this.config.enabled) return;\n\n const ttl = options?.ttl ?? this.config.ttl ?? 3600;\n const expiryTime = Date.now() + ttl * 1000;\n await this.storage.set(key, { value, expiry: expiryTime });\n }\n\n /**\n * Delete a value from the cache\n * @param key - Cache key\n * @returns Promise of the result\n */\n async delete(key: string): Promise<void> {\n if (!this.config.enabled) return;\n await this.storage.delete(key);\n }\n\n /** Clear the cache */\n async clear(): Promise<void> {\n await this.storage.clear();\n for (const entry of this.inFlightRequests.values()) {\n if (entry.abortTimer) clearTimeout(entry.abortTimer);\n }\n this.inFlightRequests.clear();\n }\n\n /**\n * Check if a value exists in the cache\n * @param key - Cache key\n * @returns Promise of true if the value exists, false otherwise\n */\n async has(key: string): Promise<boolean> {\n if (!this.config.enabled) return false;\n\n const entry = await this.getValid(key);\n return entry !== null;\n }\n\n /**\n * Generate a cache key\n * @param parts - Parts of the key\n * @param userKey - User key\n * @returns Cache key\n */\n generateKey(parts: (string | number | object)[], userKey: string): string {\n const allParts = [userKey, ...parts];\n const serialized = JSON.stringify(allParts);\n return createHash(\"sha256\").update(serialized).digest(\"hex\");\n }\n\n /** Close the cache */\n async close(): Promise<void> {\n await this.storage.close();\n }\n\n /**\n * Check if the storage is healthy\n * @returns Promise of true if the storage is healthy, false otherwise\n */\n async isStorageHealthy(): Promise<boolean> {\n return this.storage.healthCheck();\n }\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { APP_YAML_FILE, DATABRICKS_YML_FILE, bindingTypeOf } from "../../deploy-config.js";
1
2
  import { errorMessage } from "./utils.js";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
@@ -16,22 +17,13 @@ import yaml from "js-yaml";
16
17
  *
17
18
  * SDK-free; pure YAML parsing. Absent files degrade to empty results.
18
19
  */
19
- const DEFAULT_BUNDLE_FILE = "databricks.yml";
20
- const DEFAULT_APP_YAML_FILE = "app.yaml";
21
20
  /** A `${resources.<type>.<key>.<field>}` reference — a bundle-created resource. */
22
21
  const RESOURCES_REF = /\$\{resources\.([^.]+)\.([^.}]+)\.[^}]+\}/;
23
- /** The typed sub-key of a binding is its single non-`name` object property. */
24
- function bindingType(block) {
25
- for (const [k, v] of Object.entries(block)) {
26
- if (k === "name") continue;
27
- if (v && typeof v === "object") return k;
28
- }
29
- }
30
22
  /** Classifies a binding by its typed sub-key and origin: scanning its field
31
23
  * values for a `${resources.*}` reference (bundle-managed) vs anything else
32
24
  * (external). */
33
25
  function classifyBinding(block) {
34
- const type = bindingType(block);
26
+ const type = bindingTypeOf(block);
35
27
  const typed = type ? block[type] : void 0;
36
28
  if (typed && typeof typed === "object") for (const value of Object.values(typed)) {
37
29
  if (typeof value !== "string") continue;
@@ -74,7 +66,7 @@ function readYaml(filePath) {
74
66
  * empty-but-`present:false` result when no bundle is found, so callers can treat
75
67
  * "no bundle" as "everything external" without special-casing.
76
68
  */
77
- function readBundleInfo(cwd = process.cwd(), bundleFile = DEFAULT_BUNDLE_FILE, appYamlFile = DEFAULT_APP_YAML_FILE) {
69
+ function readBundleInfo(cwd = process.cwd(), bundleFile = DATABRICKS_YML_FILE, appYamlFile = APP_YAML_FILE) {
78
70
  const bindings = /* @__PURE__ */ new Map();
79
71
  const envToBinding = /* @__PURE__ */ new Map();
80
72
  const declaredResources = /* @__PURE__ */ new Set();
@@ -1 +1 @@
1
- {"version":3,"file":"bundle.js","names":[],"sources":["../../../../src/cli/commands/doctor/bundle.ts"],"sourcesContent":["/**\n * Reads `databricks.yml` + `app.yaml` for two things doctor can't get from\n * `appkit.plugins.json`:\n *\n * 1. **Provenance** — whether each binding references an existing resource\n * (`${var.*}`/literal → `external`) or one this bundle creates\n * (`${resources.<type>.<key>.*}` → `bundle-managed`).\n * 2. **Wiring** — the binding `name` joins the three files: `app.yaml`'s\n * `valueFrom: <name>` must match a `databricks.yml` binding `name`.\n *\n * SDK-free; pure YAML parsing. Absent files degrade to empty results.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport yaml from \"js-yaml\";\nimport type { ResourceOrigin } from \"./types\";\nimport { errorMessage } from \"./utils\";\n\nexport const DEFAULT_BUNDLE_FILE = \"databricks.yml\";\nexport const DEFAULT_APP_YAML_FILE = \"app.yaml\";\n\n/** A `${resources.<type>.<key>.<field>}` reference — a bundle-created resource. */\nconst RESOURCES_REF = /\\$\\{resources\\.([^.]+)\\.([^.}]+)\\.[^}]+\\}/;\n\n/** One app resource binding from `resources.apps.<app>.resources[]`. */\nexport interface BundleBinding {\n /** The binding name — the join key with `app.yaml` `valueFrom`. */\n name: string;\n /** The typed sub-key that names the kind (`sql_warehouse`, `genie_space`, …). */\n type?: string;\n origin: ResourceOrigin;\n /** For bundle-managed bindings: the `<type>.<key>` it references, if parseable. */\n ref?: { type: string; key: string };\n}\n\nexport interface BundleInfo {\n /** Bindings under every app, keyed by binding name. */\n bindings: Map<string, BundleBinding>;\n /** `app.yaml` env var name → binding name (from `valueFrom`). */\n envToBinding: Map<string, string>;\n /** Declared bundle resource keys, as `\"<type>.<key>\"`, for ref-integrity. */\n declaredResources: Set<string>;\n /** True when a `databricks.yml` was found and parsed. */\n present: boolean;\n}\n\ninterface AppResourceBlock {\n name?: string;\n [k: string]: unknown;\n}\ninterface BundleAppBlock {\n resources?: AppResourceBlock[];\n}\ninterface BundleDoc {\n resources?: {\n apps?: Record<string, BundleAppBlock>;\n [otherType: string]: Record<string, unknown> | undefined;\n };\n}\ninterface AppYamlDoc {\n env?: Array<{ name?: string; valueFrom?: string }>;\n}\n\n/** The typed sub-key of a binding is its single non-`name` object property. */\nfunction bindingType(block: AppResourceBlock): string | undefined {\n for (const [k, v] of Object.entries(block)) {\n if (k === \"name\") continue;\n if (v && typeof v === \"object\") return k;\n }\n return undefined;\n}\n\n/** Classifies a binding by its typed sub-key and origin: scanning its field\n * values for a `${resources.*}` reference (bundle-managed) vs anything else\n * (external). */\nfunction classifyBinding(block: AppResourceBlock): {\n type?: string;\n origin: ResourceOrigin;\n ref?: { type: string; key: string };\n} {\n const type = bindingType(block);\n const typed = type ? block[type] : undefined;\n if (typed && typeof typed === \"object\") {\n for (const value of Object.values(typed as Record<string, unknown>)) {\n if (typeof value !== \"string\") continue;\n const m = value.match(RESOURCES_REF);\n if (m) {\n return {\n type,\n origin: \"bundle-managed\",\n ref: { type: m[1], key: m[2] },\n };\n }\n }\n }\n return { type, origin: \"external\" };\n}\n\n/**\n * Reads and parses a YAML file. Returns `null` when the file is absent (a\n * legitimate \"no bundle\" state); throws on invalid YAML, since silently\n * ignoring it would let doctor report a false all-clear.\n */\nfunction readYaml<T>(filePath: string): T | null {\n let raw: string;\n try {\n raw = fs.readFileSync(filePath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n try {\n return (yaml.load(raw) ?? {}) as T;\n } catch (err) {\n throw new Error(\n `Failed to parse ${path.basename(filePath)}: ${errorMessage(err)}`,\n );\n }\n}\n\n/**\n * Parses `databricks.yml` (+ `app.yaml`) into {@link BundleInfo}. Returns an\n * empty-but-`present:false` result when no bundle is found, so callers can treat\n * \"no bundle\" as \"everything external\" without special-casing.\n */\nexport function readBundleInfo(\n cwd: string = process.cwd(),\n bundleFile: string = DEFAULT_BUNDLE_FILE,\n appYamlFile: string = DEFAULT_APP_YAML_FILE,\n): BundleInfo {\n const bindings = new Map<string, BundleBinding>();\n const envToBinding = new Map<string, string>();\n const declaredResources = new Set<string>();\n\n const doc = readYaml<BundleDoc>(path.resolve(cwd, bundleFile));\n if (!doc) {\n return { bindings, envToBinding, declaredResources, present: false };\n }\n\n for (const [type, group] of Object.entries(doc.resources ?? {})) {\n if (!group || typeof group !== \"object\") continue;\n for (const key of Object.keys(group))\n declaredResources.add(`${type}.${key}`);\n }\n\n for (const app of Object.values(doc.resources?.apps ?? {})) {\n for (const block of app.resources ?? []) {\n if (!block?.name) continue;\n const { type, origin, ref } = classifyBinding(block);\n bindings.set(block.name, {\n name: block.name,\n type,\n origin,\n ref,\n });\n }\n }\n\n const appYaml = readYaml<AppYamlDoc>(path.resolve(cwd, appYamlFile));\n for (const entry of appYaml?.env ?? []) {\n if (entry?.name && entry.valueFrom) {\n envToBinding.set(entry.name, entry.valueFrom);\n }\n }\n\n return { bindings, envToBinding, declaredResources, present: true };\n}\n\n/**\n * Resolves the origin for a target given its env vars, by walking\n * env var → binding name → binding origin. Returns undefined when the bundle\n * has nothing to say (so the target stays external by default).\n */\nexport function originForEnvVars(\n envVars: string[],\n info: BundleInfo,\n): ResourceOrigin | undefined {\n if (!info.present) return undefined;\n for (const env of envVars) {\n const bindingName = info.envToBinding.get(env);\n if (!bindingName) continue;\n const binding = info.bindings.get(bindingName);\n if (binding) return binding.origin;\n }\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAmBA,MAAa,sBAAsB;AACnC,MAAa,wBAAwB;;AAGrC,MAAM,gBAAgB;;AA0CtB,SAAS,YAAY,OAA6C;AAChE,MAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,MAAM,EAAE;AAC1C,MAAI,MAAM,OAAQ;AAClB,MAAI,KAAK,OAAO,MAAM,SAAU,QAAO;;;;;;AAQ3C,SAAS,gBAAgB,OAIvB;CACA,MAAM,OAAO,YAAY,MAAM;CAC/B,MAAM,QAAQ,OAAO,MAAM,QAAQ;AACnC,KAAI,SAAS,OAAO,UAAU,SAC5B,MAAK,MAAM,SAAS,OAAO,OAAO,MAAiC,EAAE;AACnE,MAAI,OAAO,UAAU,SAAU;EAC/B,MAAM,IAAI,MAAM,MAAM,cAAc;AACpC,MAAI,EACF,QAAO;GACL;GACA,QAAQ;GACR,KAAK;IAAE,MAAM,EAAE;IAAI,KAAK,EAAE;IAAI;GAC/B;;AAIP,QAAO;EAAE;EAAM,QAAQ;EAAY;;;;;;;AAQrC,SAAS,SAAY,UAA4B;CAC/C,IAAI;AACJ,KAAI;AACF,QAAM,GAAG,aAAa,UAAU,QAAQ;UACjC,KAAK;AACZ,MAAK,IAA8B,SAAS,SAAU,QAAO;AAC7D,QAAM;;AAER,KAAI;AACF,SAAQ,KAAK,KAAK,IAAI,IAAI,EAAE;UACrB,KAAK;AACZ,QAAM,IAAI,MACR,mBAAmB,KAAK,SAAS,SAAS,CAAC,IAAI,aAAa,IAAI,GACjE;;;;;;;;AASL,SAAgB,eACd,MAAc,QAAQ,KAAK,EAC3B,aAAqB,qBACrB,cAAsB,uBACV;CACZ,MAAM,2BAAW,IAAI,KAA4B;CACjD,MAAM,+BAAe,IAAI,KAAqB;CAC9C,MAAM,oCAAoB,IAAI,KAAa;CAE3C,MAAM,MAAM,SAAoB,KAAK,QAAQ,KAAK,WAAW,CAAC;AAC9D,KAAI,CAAC,IACH,QAAO;EAAE;EAAU;EAAc;EAAmB,SAAS;EAAO;AAGtE,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,IAAI,aAAa,EAAE,CAAC,EAAE;AAC/D,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU;AACzC,OAAK,MAAM,OAAO,OAAO,KAAK,MAAM,CAClC,mBAAkB,IAAI,GAAG,KAAK,GAAG,MAAM;;AAG3C,MAAK,MAAM,OAAO,OAAO,OAAO,IAAI,WAAW,QAAQ,EAAE,CAAC,CACxD,MAAK,MAAM,SAAS,IAAI,aAAa,EAAE,EAAE;AACvC,MAAI,CAAC,OAAO,KAAM;EAClB,MAAM,EAAE,MAAM,QAAQ,QAAQ,gBAAgB,MAAM;AACpD,WAAS,IAAI,MAAM,MAAM;GACvB,MAAM,MAAM;GACZ;GACA;GACA;GACD,CAAC;;CAIN,MAAM,UAAU,SAAqB,KAAK,QAAQ,KAAK,YAAY,CAAC;AACpE,MAAK,MAAM,SAAS,SAAS,OAAO,EAAE,CACpC,KAAI,OAAO,QAAQ,MAAM,UACvB,cAAa,IAAI,MAAM,MAAM,MAAM,UAAU;AAIjD,QAAO;EAAE;EAAU;EAAc;EAAmB,SAAS;EAAM;;;;;;;AAQrE,SAAgB,iBACd,SACA,MAC4B;AAC5B,KAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,MAAK,MAAM,OAAO,SAAS;EACzB,MAAM,cAAc,KAAK,aAAa,IAAI,IAAI;AAC9C,MAAI,CAAC,YAAa;EAClB,MAAM,UAAU,KAAK,SAAS,IAAI,YAAY;AAC9C,MAAI,QAAS,QAAO,QAAQ"}
1
+ {"version":3,"file":"bundle.js","names":[],"sources":["../../../../src/cli/commands/doctor/bundle.ts"],"sourcesContent":["/**\n * Reads `databricks.yml` + `app.yaml` for two things doctor can't get from\n * `appkit.plugins.json`:\n *\n * 1. **Provenance** — whether each binding references an existing resource\n * (`${var.*}`/literal → `external`) or one this bundle creates\n * (`${resources.<type>.<key>.*}` → `bundle-managed`).\n * 2. **Wiring** — the binding `name` joins the three files: `app.yaml`'s\n * `valueFrom: <name>` must match a `databricks.yml` binding `name`.\n *\n * SDK-free; pure YAML parsing. Absent files degrade to empty results.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport yaml from \"js-yaml\";\nimport {\n APP_YAML_FILE,\n bindingTypeOf,\n DATABRICKS_YML_FILE,\n} from \"../../deploy-config\";\nimport type { ResourceOrigin } from \"./types\";\nimport { errorMessage } from \"./utils\";\n\n/** A `${resources.<type>.<key>.<field>}` reference — a bundle-created resource. */\nconst RESOURCES_REF = /\\$\\{resources\\.([^.]+)\\.([^.}]+)\\.[^}]+\\}/;\n\n/** One app resource binding from `resources.apps.<app>.resources[]`. */\nexport interface BundleBinding {\n /** The binding name — the join key with `app.yaml` `valueFrom`. */\n name: string;\n /** The typed sub-key that names the kind (`sql_warehouse`, `genie_space`, …). */\n type?: string;\n origin: ResourceOrigin;\n /** For bundle-managed bindings: the `<type>.<key>` it references, if parseable. */\n ref?: { type: string; key: string };\n}\n\nexport interface BundleInfo {\n /** Bindings under every app, keyed by binding name. */\n bindings: Map<string, BundleBinding>;\n /** `app.yaml` env var name → binding name (from `valueFrom`). */\n envToBinding: Map<string, string>;\n /** Declared bundle resource keys, as `\"<type>.<key>\"`, for ref-integrity. */\n declaredResources: Set<string>;\n /** True when a `databricks.yml` was found and parsed. */\n present: boolean;\n}\n\ninterface AppResourceBlock {\n name?: string;\n [k: string]: unknown;\n}\ninterface BundleAppBlock {\n resources?: AppResourceBlock[];\n}\ninterface BundleDoc {\n resources?: {\n apps?: Record<string, BundleAppBlock>;\n [otherType: string]: Record<string, unknown> | undefined;\n };\n}\ninterface AppYamlDoc {\n env?: Array<{ name?: string; valueFrom?: string }>;\n}\n\n/** Classifies a binding by its typed sub-key and origin: scanning its field\n * values for a `${resources.*}` reference (bundle-managed) vs anything else\n * (external). */\nfunction classifyBinding(block: AppResourceBlock): {\n type?: string;\n origin: ResourceOrigin;\n ref?: { type: string; key: string };\n} {\n const type = bindingTypeOf(block);\n const typed = type ? block[type] : undefined;\n if (typed && typeof typed === \"object\") {\n for (const value of Object.values(typed as Record<string, unknown>)) {\n if (typeof value !== \"string\") continue;\n const m = value.match(RESOURCES_REF);\n if (m) {\n return {\n type,\n origin: \"bundle-managed\",\n ref: { type: m[1], key: m[2] },\n };\n }\n }\n }\n return { type, origin: \"external\" };\n}\n\n/**\n * Reads and parses a YAML file. Returns `null` when the file is absent (a\n * legitimate \"no bundle\" state); throws on invalid YAML, since silently\n * ignoring it would let doctor report a false all-clear.\n */\nfunction readYaml<T>(filePath: string): T | null {\n let raw: string;\n try {\n raw = fs.readFileSync(filePath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n try {\n return (yaml.load(raw) ?? {}) as T;\n } catch (err) {\n throw new Error(\n `Failed to parse ${path.basename(filePath)}: ${errorMessage(err)}`,\n );\n }\n}\n\n/**\n * Parses `databricks.yml` (+ `app.yaml`) into {@link BundleInfo}. Returns an\n * empty-but-`present:false` result when no bundle is found, so callers can treat\n * \"no bundle\" as \"everything external\" without special-casing.\n */\nexport function readBundleInfo(\n cwd: string = process.cwd(),\n bundleFile: string = DATABRICKS_YML_FILE,\n appYamlFile: string = APP_YAML_FILE,\n): BundleInfo {\n const bindings = new Map<string, BundleBinding>();\n const envToBinding = new Map<string, string>();\n const declaredResources = new Set<string>();\n\n const doc = readYaml<BundleDoc>(path.resolve(cwd, bundleFile));\n if (!doc) {\n return { bindings, envToBinding, declaredResources, present: false };\n }\n\n for (const [type, group] of Object.entries(doc.resources ?? {})) {\n if (!group || typeof group !== \"object\") continue;\n for (const key of Object.keys(group))\n declaredResources.add(`${type}.${key}`);\n }\n\n for (const app of Object.values(doc.resources?.apps ?? {})) {\n for (const block of app.resources ?? []) {\n if (!block?.name) continue;\n const { type, origin, ref } = classifyBinding(block);\n bindings.set(block.name, {\n name: block.name,\n type,\n origin,\n ref,\n });\n }\n }\n\n const appYaml = readYaml<AppYamlDoc>(path.resolve(cwd, appYamlFile));\n for (const entry of appYaml?.env ?? []) {\n if (entry?.name && entry.valueFrom) {\n envToBinding.set(entry.name, entry.valueFrom);\n }\n }\n\n return { bindings, envToBinding, declaredResources, present: true };\n}\n\n/**\n * Resolves the origin for a target given its env vars, by walking\n * env var → binding name → binding origin. Returns undefined when the bundle\n * has nothing to say (so the target stays external by default).\n */\nexport function originForEnvVars(\n envVars: string[],\n info: BundleInfo,\n): ResourceOrigin | undefined {\n if (!info.present) return undefined;\n for (const env of envVars) {\n const bindingName = info.envToBinding.get(env);\n if (!bindingName) continue;\n const binding = info.bindings.get(bindingName);\n if (binding) return binding.origin;\n }\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyBA,MAAM,gBAAgB;;;;AA4CtB,SAAS,gBAAgB,OAIvB;CACA,MAAM,OAAO,cAAc,MAAM;CACjC,MAAM,QAAQ,OAAO,MAAM,QAAQ;AACnC,KAAI,SAAS,OAAO,UAAU,SAC5B,MAAK,MAAM,SAAS,OAAO,OAAO,MAAiC,EAAE;AACnE,MAAI,OAAO,UAAU,SAAU;EAC/B,MAAM,IAAI,MAAM,MAAM,cAAc;AACpC,MAAI,EACF,QAAO;GACL;GACA,QAAQ;GACR,KAAK;IAAE,MAAM,EAAE;IAAI,KAAK,EAAE;IAAI;GAC/B;;AAIP,QAAO;EAAE;EAAM,QAAQ;EAAY;;;;;;;AAQrC,SAAS,SAAY,UAA4B;CAC/C,IAAI;AACJ,KAAI;AACF,QAAM,GAAG,aAAa,UAAU,QAAQ;UACjC,KAAK;AACZ,MAAK,IAA8B,SAAS,SAAU,QAAO;AAC7D,QAAM;;AAER,KAAI;AACF,SAAQ,KAAK,KAAK,IAAI,IAAI,EAAE;UACrB,KAAK;AACZ,QAAM,IAAI,MACR,mBAAmB,KAAK,SAAS,SAAS,CAAC,IAAI,aAAa,IAAI,GACjE;;;;;;;;AASL,SAAgB,eACd,MAAc,QAAQ,KAAK,EAC3B,aAAqB,qBACrB,cAAsB,eACV;CACZ,MAAM,2BAAW,IAAI,KAA4B;CACjD,MAAM,+BAAe,IAAI,KAAqB;CAC9C,MAAM,oCAAoB,IAAI,KAAa;CAE3C,MAAM,MAAM,SAAoB,KAAK,QAAQ,KAAK,WAAW,CAAC;AAC9D,KAAI,CAAC,IACH,QAAO;EAAE;EAAU;EAAc;EAAmB,SAAS;EAAO;AAGtE,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,IAAI,aAAa,EAAE,CAAC,EAAE;AAC/D,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU;AACzC,OAAK,MAAM,OAAO,OAAO,KAAK,MAAM,CAClC,mBAAkB,IAAI,GAAG,KAAK,GAAG,MAAM;;AAG3C,MAAK,MAAM,OAAO,OAAO,OAAO,IAAI,WAAW,QAAQ,EAAE,CAAC,CACxD,MAAK,MAAM,SAAS,IAAI,aAAa,EAAE,EAAE;AACvC,MAAI,CAAC,OAAO,KAAM;EAClB,MAAM,EAAE,MAAM,QAAQ,QAAQ,gBAAgB,MAAM;AACpD,WAAS,IAAI,MAAM,MAAM;GACvB,MAAM,MAAM;GACZ;GACA;GACA;GACD,CAAC;;CAIN,MAAM,UAAU,SAAqB,KAAK,QAAQ,KAAK,YAAY,CAAC;AACpE,MAAK,MAAM,SAAS,SAAS,OAAO,EAAE,CACpC,KAAI,OAAO,QAAQ,MAAM,UACvB,cAAa,IAAI,MAAM,MAAM,MAAM,UAAU;AAIjD,QAAO;EAAE;EAAU;EAAc;EAAmB,SAAS;EAAM;;;;;;;AAQrE,SAAgB,iBACd,SACA,MAC4B;AAC5B,KAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,MAAK,MAAM,OAAO,SAAS;EACzB,MAAM,cAAc,KAAK,aAAa,IAAI,IAAI;AAC9C,MAAI,CAAC,YAAa;EAClB,MAAM,UAAU,KAAK,SAAS,IAAI,YAAY;AAC9C,MAAI,QAAS,QAAO,QAAQ"}
@@ -1,38 +1,34 @@
1
+ import { loadConfigFile } from "../../../workspace-client/legacy.js";
2
+ import { createWorkspaceClient } from "../../../workspace-client/factory.js";
3
+
1
4
  //#region src/cli/commands/doctor/databricks-client.ts
2
5
  /**
3
- * The single seam where `appkit doctor` crosses into the Databricks SDK. The
4
- * SDK-free `shared` package reaches it via a runtime `import(...)`, degrading
5
- * gracefully when it's absent.
6
- *
7
- * `noRestrictedImports` normally routes SDK access through
8
- * `packages/appkit/src/workspace-client`, but that isn't available here: `appkit`
9
- * depends on `shared`, so importing it back would be a dependency cycle. `shared`
10
- * also can't depend on the SDK directly — hence the dynamic `import(...)` and the
11
- * per-call suppressions below. This file *is* the wrapper for this package, and
12
- * every SDK reference in the doctor command is confined to it.
6
+ * The seam where `appkit doctor` crosses into the Databricks SDK. It reaches
7
+ * the SDK through the shared `workspace-client` facade — the one sanctioned SDK
8
+ * import site — so no dynamic import or `noRestrictedImports` suppression is
9
+ * needed. (The Lakebase probe still dynamically imports `@databricks/appkit`
10
+ * below, since that's an optional peer `shared` deliberately doesn't depend on.)
11
+ */
12
+ /**
13
+ * Retained for backward compatibility. Since `shared` now depends on the SDK
14
+ * (reached via the workspace-client facade), the SDK is always resolvable at
15
+ * runtime and this is no longer thrown; kept exported so existing callers and
16
+ * the `SDK_NOT_INSTALLED` diagnostic branch keep compiling.
13
17
  */
14
- /** Raised when `@databricks/sdk-experimental` is not resolvable at runtime. */
15
18
  var SdkNotInstalledError = class extends Error {
16
19
  constructor() {
17
- super("The 'doctor' command requires the Databricks SDK (a dependency of @databricks/appkit). Please install @databricks/appkit to run connection checks.");
20
+ super("The 'doctor' command requires the Databricks SDK (a dependency of @databricks/appkit).");
18
21
  this.name = "SdkNotInstalledError";
19
22
  }
20
23
  };
21
24
  function isModuleNotFound(err) {
22
25
  return err instanceof Error && (err.message.includes("Cannot find module") || err.message.includes("Cannot find package") || err.code === "ERR_MODULE_NOT_FOUND");
23
26
  }
24
- /** Constructs a `WorkspaceClient` via the SDK's unified-auth chain. An explicit
27
+ /** Constructs a workspace client via the SDK's unified-auth chain. An explicit
25
28
  * `profile` is passed through `Config.profile` rather than mutating
26
29
  * `process.env`, so it doesn't leak beyond this call. */
27
30
  async function getServiceClient(profile) {
28
- let sdk;
29
- try {
30
- sdk = await import("@databricks/sdk-experimental");
31
- } catch (err) {
32
- if (isModuleNotFound(err)) throw new SdkNotInstalledError();
33
- throw err;
34
- }
35
- return { client: new sdk.WorkspaceClient(profile ? { profile } : {}) };
31
+ return { client: createWorkspaceClient(profile ? { profile } : {}).toLegacyWorkspaceClient() };
36
32
  }
37
33
  /**
38
34
  * The host a named profile declares in `~/.databrickscfg`, read offline. Doctor
@@ -43,7 +39,7 @@ async function getServiceClient(profile) {
43
39
  */
44
40
  async function getProfileHost(profile) {
45
41
  try {
46
- const { iniFile } = await (await import("@databricks/sdk-experimental")).loadConfigFile(process.env.DATABRICKS_CONFIG_FILE);
42
+ const { iniFile } = await loadConfigFile(process.env.DATABRICKS_CONFIG_FILE);
47
43
  const host = iniFile?.[profile]?.host;
48
44
  return typeof host === "string" && host.length > 0 ? host : void 0;
49
45
  } catch {
@@ -1 +1 @@
1
- {"version":3,"file":"databricks-client.js","names":[],"sources":["../../../../src/cli/commands/doctor/databricks-client.ts"],"sourcesContent":["/**\n * The single seam where `appkit doctor` crosses into the Databricks SDK. The\n * SDK-free `shared` package reaches it via a runtime `import(...)`, degrading\n * gracefully when it's absent.\n *\n * `noRestrictedImports` normally routes SDK access through\n * `packages/appkit/src/workspace-client`, but that isn't available here: `appkit`\n * depends on `shared`, so importing it back would be a dependency cycle. `shared`\n * also can't depend on the SDK directly — hence the dynamic `import(...)` and the\n * per-call suppressions below. This file *is* the wrapper for this package, and\n * every SDK reference in the doctor command is confined to it.\n */\n\n/** Raised when `@databricks/sdk-experimental` is not resolvable at runtime. */\nexport class SdkNotInstalledError extends Error {\n constructor() {\n super(\n \"The 'doctor' command requires the Databricks SDK (a dependency of @databricks/appkit). Please install @databricks/appkit to run connection checks.\",\n );\n this.name = \"SdkNotInstalledError\";\n }\n}\n\ninterface ServiceClientHandle {\n /** WorkspaceClient, typed as unknown to keep `shared` SDK-free. */\n client: unknown;\n}\n\nfunction isModuleNotFound(err: unknown): boolean {\n return (\n err instanceof Error &&\n (err.message.includes(\"Cannot find module\") ||\n err.message.includes(\"Cannot find package\") ||\n (err as NodeJS.ErrnoException).code === \"ERR_MODULE_NOT_FOUND\")\n );\n}\n\n/** Constructs a `WorkspaceClient` via the SDK's unified-auth chain. An explicit\n * `profile` is passed through `Config.profile` rather than mutating\n * `process.env`, so it doesn't leak beyond this call. */\nexport async function getServiceClient(\n profile?: string,\n): Promise<ServiceClientHandle> {\n let sdk: { WorkspaceClient: new (opts: Record<string, unknown>) => unknown };\n try {\n // biome-ignore lint/style/noRestrictedImports: shared can't reach appkit's workspace-client wrapper (appkit depends on shared); this file is the SDK seam\n sdk = (await import(\"@databricks/sdk-experimental\")) as typeof sdk;\n } catch (err) {\n if (isModuleNotFound(err)) {\n throw new SdkNotInstalledError();\n }\n throw err;\n }\n\n const client = new sdk.WorkspaceClient(profile ? { profile } : {});\n return { client };\n}\n\n/**\n * The host a named profile declares in `~/.databrickscfg`, read offline. Doctor\n * uses it to detect a profile/host conflict, so it must not depend on the\n * resolved config (where `DATABRICKS_HOST` has already won). Returns undefined\n * for any unreadable file, missing profile, or hostless profile — a conflict we\n * can't prove isn't worth reporting.\n */\nexport async function getProfileHost(\n profile: string,\n): Promise<string | undefined> {\n try {\n // biome-ignore lint/style/noRestrictedImports: see the note at the top of this file — shared can't import appkit's wrapper\n const sdk = (await import(\"@databricks/sdk-experimental\")) as {\n loadConfigFile: (\n file?: string,\n ) => Promise<{ iniFile: Record<string, { host?: string }> }>;\n };\n const { iniFile } = await sdk.loadConfigFile(\n process.env.DATABRICKS_CONFIG_FILE,\n );\n const host = iniFile?.[profile]?.host;\n return typeof host === \"string\" && host.length > 0 ? host : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Raised when `@databricks/appkit` (needed for the Lakebase probe) is absent. */\nexport class AppkitNotInstalledError extends Error {\n constructor() {\n super(\n \"The Lakebase connection check requires @databricks/appkit. Please install it to run this check.\",\n );\n this.name = \"AppkitNotInstalledError\";\n }\n}\n\n/** Minimal `pg.Pool`-shaped handle. The caller owns it and must call `end()`. */\nexport interface LakebasePoolHandle {\n query: (sql: string) => Promise<unknown>;\n end: () => Promise<void>;\n}\n\n/** Builds a Lakebase pool via `createLakebasePool`. Connection settings\n * (`PGHOST`, `LAKEBASE_ENDPOINT`, …) come from the environment; we inject only\n * the resolved workspace client for OAuth token minting. */\nexport async function getLakebasePool(\n client: unknown,\n): Promise<LakebasePoolHandle> {\n // A variable specifier stops TS from statically resolving `@databricks/appkit`,\n // an optional peer that `shared` has no dependency on.\n const appkitPkg = \"@databricks/appkit\";\n let appkit: {\n createLakebasePool: (cfg: Record<string, unknown>) => unknown;\n };\n try {\n appkit = await import(appkitPkg);\n } catch (err) {\n if (isModuleNotFound(err)) {\n throw new AppkitNotInstalledError();\n }\n throw err;\n }\n\n // Silence Lakebase's own logger. It defaults to an error-only console logger\n // that dumps the raw SDK ApiError (stack + full response blob) to stderr on a\n // failed token fetch. doctor classifies and prints that failure itself, so the\n // library's dump is just noise on top of our clean one-line report.\n const pool = appkit.createLakebasePool({\n workspaceClient: client,\n logger: { error: false },\n });\n return pool as unknown as LakebasePoolHandle;\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,IAAa,uBAAb,cAA0C,MAAM;CAC9C,cAAc;AACZ,QACE,qJACD;AACD,OAAK,OAAO;;;AAShB,SAAS,iBAAiB,KAAuB;AAC/C,QACE,eAAe,UACd,IAAI,QAAQ,SAAS,qBAAqB,IACzC,IAAI,QAAQ,SAAS,sBAAsB,IAC1C,IAA8B,SAAS;;;;;AAO9C,eAAsB,iBACpB,SAC8B;CAC9B,IAAI;AACJ,KAAI;AAEF,QAAO,MAAM,OAAO;UACb,KAAK;AACZ,MAAI,iBAAiB,IAAI,CACvB,OAAM,IAAI,sBAAsB;AAElC,QAAM;;AAIR,QAAO,EAAE,QADM,IAAI,IAAI,gBAAgB,UAAU,EAAE,SAAS,GAAG,EAAE,CAAC,EACjD;;;;;;;;;AAUnB,eAAsB,eACpB,SAC6B;AAC7B,KAAI;EAOF,MAAM,EAAE,YAAY,OALP,MAAM,OAAO,iCAKI,eAC5B,QAAQ,IAAI,uBACb;EACD,MAAM,OAAO,UAAU,UAAU;AACjC,SAAO,OAAO,SAAS,YAAY,KAAK,SAAS,IAAI,OAAO;SACtD;AACN;;;;AAKJ,IAAa,0BAAb,cAA6C,MAAM;CACjD,cAAc;AACZ,QACE,kGACD;AACD,OAAK,OAAO;;;;;;AAahB,eAAsB,gBACpB,QAC6B;CAG7B,MAAM,YAAY;CAClB,IAAI;AAGJ,KAAI;AACF,WAAS,MAAM,OAAO;UACf,KAAK;AACZ,MAAI,iBAAiB,IAAI,CACvB,OAAM,IAAI,yBAAyB;AAErC,QAAM;;AAWR,QAJa,OAAO,mBAAmB;EACrC,iBAAiB;EACjB,QAAQ,EAAE,OAAO,OAAO;EACzB,CAAC"}
1
+ {"version":3,"file":"databricks-client.js","names":[],"sources":["../../../../src/cli/commands/doctor/databricks-client.ts"],"sourcesContent":["/**\n * The seam where `appkit doctor` crosses into the Databricks SDK. It reaches\n * the SDK through the shared `workspace-client` facade — the one sanctioned SDK\n * import site — so no dynamic import or `noRestrictedImports` suppression is\n * needed. (The Lakebase probe still dynamically imports `@databricks/appkit`\n * below, since that's an optional peer `shared` deliberately doesn't depend on.)\n */\n\nimport {\n createWorkspaceClient,\n loadConfigFile,\n} from \"../../../workspace-client\";\n\n/**\n * Retained for backward compatibility. Since `shared` now depends on the SDK\n * (reached via the workspace-client facade), the SDK is always resolvable at\n * runtime and this is no longer thrown; kept exported so existing callers and\n * the `SDK_NOT_INSTALLED` diagnostic branch keep compiling.\n */\nexport class SdkNotInstalledError extends Error {\n constructor() {\n super(\n \"The 'doctor' command requires the Databricks SDK (a dependency of @databricks/appkit).\",\n );\n this.name = \"SdkNotInstalledError\";\n }\n}\n\ninterface ServiceClientHandle {\n /** WorkspaceClient, typed as unknown to keep doctor's call sites SDK-agnostic. */\n client: unknown;\n}\n\nfunction isModuleNotFound(err: unknown): boolean {\n return (\n err instanceof Error &&\n (err.message.includes(\"Cannot find module\") ||\n err.message.includes(\"Cannot find package\") ||\n (err as NodeJS.ErrnoException).code === \"ERR_MODULE_NOT_FOUND\")\n );\n}\n\n/** Constructs a workspace client via the SDK's unified-auth chain. An explicit\n * `profile` is passed through `Config.profile` rather than mutating\n * `process.env`, so it doesn't leak beyond this call. */\nexport async function getServiceClient(\n profile?: string,\n): Promise<ServiceClientHandle> {\n const client = createWorkspaceClient(\n profile ? { profile } : {},\n ).toLegacyWorkspaceClient();\n return { client };\n}\n\n/**\n * The host a named profile declares in `~/.databrickscfg`, read offline. Doctor\n * uses it to detect a profile/host conflict, so it must not depend on the\n * resolved config (where `DATABRICKS_HOST` has already won). Returns undefined\n * for any unreadable file, missing profile, or hostless profile — a conflict we\n * can't prove isn't worth reporting.\n */\nexport async function getProfileHost(\n profile: string,\n): Promise<string | undefined> {\n try {\n const { iniFile } = await loadConfigFile(\n process.env.DATABRICKS_CONFIG_FILE,\n );\n const host = iniFile?.[profile]?.host;\n return typeof host === \"string\" && host.length > 0 ? host : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Raised when `@databricks/appkit` (needed for the Lakebase probe) is absent. */\nexport class AppkitNotInstalledError extends Error {\n constructor() {\n super(\n \"The Lakebase connection check requires @databricks/appkit. Please install it to run this check.\",\n );\n this.name = \"AppkitNotInstalledError\";\n }\n}\n\n/** Minimal `pg.Pool`-shaped handle. The caller owns it and must call `end()`. */\nexport interface LakebasePoolHandle {\n query: (sql: string) => Promise<unknown>;\n end: () => Promise<void>;\n}\n\n/** Builds a Lakebase pool via `createLakebasePool`. Connection settings\n * (`PGHOST`, `LAKEBASE_ENDPOINT`, …) come from the environment; we inject only\n * the resolved workspace client for OAuth token minting. */\nexport async function getLakebasePool(\n client: unknown,\n): Promise<LakebasePoolHandle> {\n // A variable specifier stops TS from statically resolving `@databricks/appkit`,\n // an optional peer that `shared` has no dependency on.\n const appkitPkg = \"@databricks/appkit\";\n let appkit: {\n createLakebasePool: (cfg: Record<string, unknown>) => unknown;\n };\n try {\n appkit = await import(appkitPkg);\n } catch (err) {\n if (isModuleNotFound(err)) {\n throw new AppkitNotInstalledError();\n }\n throw err;\n }\n\n // Silence Lakebase's own logger. It defaults to an error-only console logger\n // that dumps the raw SDK ApiError (stack + full response blob) to stderr on a\n // failed token fetch. doctor classifies and prints that failure itself, so the\n // library's dump is just noise on top of our clean one-line report.\n const pool = appkit.createLakebasePool({\n workspaceClient: client,\n logger: { error: false },\n });\n return pool as unknown as LakebasePoolHandle;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAmBA,IAAa,uBAAb,cAA0C,MAAM;CAC9C,cAAc;AACZ,QACE,yFACD;AACD,OAAK,OAAO;;;AAShB,SAAS,iBAAiB,KAAuB;AAC/C,QACE,eAAe,UACd,IAAI,QAAQ,SAAS,qBAAqB,IACzC,IAAI,QAAQ,SAAS,sBAAsB,IAC1C,IAA8B,SAAS;;;;;AAO9C,eAAsB,iBACpB,SAC8B;AAI9B,QAAO,EAAE,QAHM,sBACb,UAAU,EAAE,SAAS,GAAG,EAAE,CAC3B,CAAC,yBAAyB,EACV;;;;;;;;;AAUnB,eAAsB,eACpB,SAC6B;AAC7B,KAAI;EACF,MAAM,EAAE,YAAY,MAAM,eACxB,QAAQ,IAAI,uBACb;EACD,MAAM,OAAO,UAAU,UAAU;AACjC,SAAO,OAAO,SAAS,YAAY,KAAK,SAAS,IAAI,OAAO;SACtD;AACN;;;;AAKJ,IAAa,0BAAb,cAA6C,MAAM;CACjD,cAAc;AACZ,QACE,kGACD;AACD,OAAK,OAAO;;;;;;AAahB,eAAsB,gBACpB,QAC6B;CAG7B,MAAM,YAAY;CAClB,IAAI;AAGJ,KAAI;AACF,WAAS,MAAM,OAAO;UACf,KAAK;AACZ,MAAI,iBAAiB,IAAI,CACvB,OAAM,IAAI,yBAAyB;AAErC,QAAM;;AAWR,QAJa,OAAO,mBAAmB;EACrC,iBAAiB;EACjB,QAAQ,EAAE,OAAO,OAAO;EACzB,CAAC"}