@databricks/appkit 0.75.1 → 0.76.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/cache/index.d.ts +10 -0
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +13 -0
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/commands/codemod/on-plugins-ready.js +2 -1
- package/dist/cli/commands/codemod/on-plugins-ready.js.map +1 -1
- package/dist/cli/commands/lint.js +2 -1
- package/dist/cli/commands/lint.js.map +1 -1
- package/dist/cli/commands/plugin/add-resource/add-resource.js +1 -1
- package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
- package/dist/cli/commands/plugin/create/create.js +1 -1
- package/dist/cli/commands/plugin/create/create.js.map +1 -1
- package/dist/cli/commands/plugin/create/prompt-resource.js +1 -1
- package/dist/cli/commands/plugin/create/prompt-resource.js.map +1 -1
- package/dist/cli/commands/plugin/sync/sync.js +2 -1
- package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
- package/dist/cli/commands/registry/env-writer.js +4 -1
- package/dist/cli/commands/registry/env-writer.js.map +1 -1
- package/dist/connectors/sql-warehouse/client.js +13 -1
- package/dist/connectors/sql-warehouse/client.js.map +1 -1
- package/dist/core/appkit.d.ts +1 -1
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js +26 -5
- package/dist/core/appkit.js.map +1 -1
- package/dist/core/lifecycle-manager.js +47 -27
- package/dist/core/lifecycle-manager.js.map +1 -1
- package/dist/evals/judge.d.ts.map +1 -1
- package/dist/evals/judge.js +8 -2
- package/dist/evals/judge.js.map +1 -1
- package/dist/plugins/agents/mlflow.js +6 -1
- package/dist/plugins/agents/mlflow.js.map +1 -1
- package/dist/telemetry/telemetry-manager.js +17 -0
- package/dist/telemetry/telemetry-manager.js.map +1 -1
- package/dist/testing/create-test-app.d.ts +111 -0
- package/dist/testing/create-test-app.d.ts.map +1 -0
- package/dist/testing/create-test-app.js +187 -0
- package/dist/testing/create-test-app.js.map +1 -0
- package/dist/testing/create-test-plugin.d.ts +17 -0
- package/dist/testing/create-test-plugin.d.ts.map +1 -0
- package/dist/testing/create-test-plugin.js +22 -0
- package/dist/testing/create-test-plugin.js.map +1 -0
- package/dist/testing/fixtures.d.ts +51 -35
- package/dist/testing/fixtures.d.ts.map +1 -1
- package/dist/testing/fixtures.js +129 -54
- package/dist/testing/fixtures.js.map +1 -1
- package/dist/testing/index.d.ts +8 -3
- package/dist/testing/index.js +7 -2
- package/dist/testing/mock-workspace-client.d.ts +47 -0
- package/dist/testing/mock-workspace-client.d.ts.map +1 -0
- package/dist/testing/mock-workspace-client.js +192 -0
- package/dist/testing/mock-workspace-client.js.map +1 -0
- package/dist/testing/reset-singletons.js +39 -0
- package/dist/testing/reset-singletons.js.map +1 -0
- package/dist/testing/test-app.d.ts +54 -0
- package/dist/testing/test-app.d.ts.map +1 -0
- package/dist/testing/test-app.js +55 -0
- package/dist/testing/test-app.js.map +1 -0
- package/dist/testing/test-cache.d.ts +60 -0
- package/dist/testing/test-cache.d.ts.map +1 -0
- package/dist/testing/test-cache.js +65 -0
- package/dist/testing/test-cache.js.map +1 -0
- package/dist/testing/test-plugin-context.d.ts +43 -2
- package/dist/testing/test-plugin-context.d.ts.map +1 -1
- package/dist/testing/test-plugin-context.js +47 -2
- package/dist/testing/test-plugin-context.js.map +1 -1
- package/docs/api/appkit/Function.createApp.md +9 -9
- package/docs/plugins/agents.md +3 -1
- package/docs/plugins/testing.md +440 -14
- package/llms.txt +1 -1
- package/package.json +11 -3
- package/sbom.cdx.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -59,7 +59,7 @@ npx @databricks/appkit docs <query>
|
|
|
59
59
|
- [Plugin management](./docs/plugins/plugin-management.md): AppKit includes a CLI for managing plugins. All commands are available under npx @databricks/appkit plugin.
|
|
60
60
|
- [Server plugin](./docs/plugins/server.md): Provides HTTP server capabilities with development and production modes.
|
|
61
61
|
- [Plugin Stability Tiers](./docs/plugins/stability.md): AppKit plugins have a two-tier stability system that communicates API maturity and breaking-change expectations.
|
|
62
|
-
- [Testing](./docs/plugins/testing.md): AppKit ships a testing kit at @databricks/appkit/testing so you can test a plugin
|
|
62
|
+
- [Testing](./docs/plugins/testing.md): AppKit ships a testing kit at @databricks/appkit/testing so you can test a plugin, including its cross-plugin tool calls and streaming responses, without a live Databricks workspace, credentials, or network access. Plugin tests stay fast and run in CI, where no workspace is available.
|
|
63
63
|
|
|
64
64
|
## appkit API reference [collapsed]
|
|
65
65
|
|
package/dist/appkit/package.js
CHANGED
package/dist/cache/index.d.ts
CHANGED
|
@@ -139,6 +139,16 @@ declare class CacheManager {
|
|
|
139
139
|
generateKey(parts: (string | number | object)[], userKey: string): string;
|
|
140
140
|
/** Close the cache */
|
|
141
141
|
close(): Promise<void>;
|
|
142
|
+
/**
|
|
143
|
+
* Drop the singleton so the next {@link getInstance} builds a fresh manager.
|
|
144
|
+
*
|
|
145
|
+
* Both fields must clear — `getInstance()` falls back to `initPromise` when
|
|
146
|
+
* `instance` is null. A pointer drop, not teardown: call {@link close} first
|
|
147
|
+
* or the old storage leaks (a `pg.Pool` under `PersistentStorage`).
|
|
148
|
+
*
|
|
149
|
+
* @internal
|
|
150
|
+
*/
|
|
151
|
+
static reset(): void;
|
|
142
152
|
/**
|
|
143
153
|
* Check if the storage is healthy
|
|
144
154
|
* @returns Promise of true if the storage is healthy, false otherwise
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/cache/index.ts"],"mappings":";;;;;;;AAsDA;;;;;;;;;;;cAAa,YAAA;EAAA,wBACa,uBAAA;EAAA,wBACA,qBAAA;EAAA,iBACP,IAAA;EAAA,eACF,QAAA;EAAA,eACA,WAAA;EAAA,QAEP,OAAA;EAAA,QACA,MAAA;EAAA,QACA,gBAAA;EAAA,QACA,iBAAA;EAAA,QACA,kBAAA;EAAA,QAEA,SAAA;EAAA,QACA,gBAAA;EAAA,QAKD,WAAA,CAAA;EAlBiB;;;;;;EAAA,OA+CjB,eAAA,CAAA,GAAmB,YAAA;EAvClB;;;;;;;EAAA,OAyDK,WAAA,CACX,UAAA,GAAa,OAAA,CAAQ,WAAA,IACpB,OAAA,CAAQ,YAAA;EAFE;;;;;;;;;;;;EAAA,
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/cache/index.ts"],"mappings":";;;;;;;AAsDA;;;;;;;;;;;cAAa,YAAA;EAAA,wBACa,uBAAA;EAAA,wBACA,qBAAA;EAAA,iBACP,IAAA;EAAA,eACF,QAAA;EAAA,eACA,WAAA;EAAA,QAEP,OAAA;EAAA,QACA,MAAA;EAAA,QACA,gBAAA;EAAA,QACA,iBAAA;EAAA,QACA,kBAAA;EAAA,QAEA,SAAA;EAAA,QACA,gBAAA;EAAA,QAKD,WAAA,CAAA;EAlBiB;;;;;;EAAA,OA+CjB,eAAA,CAAA,GAAmB,YAAA;EAvClB;;;;;;;EAAA,OAyDK,WAAA,CACX,UAAA,GAAa,OAAA,CAAQ,WAAA,IACpB,OAAA,CAAQ,YAAA;EAFE;;;;;;;;;;;;EAAA,eAmCQ,MAAA;EA2EyB;;;;;;;;;;;;;;;;;;;;EAFxC,YAAA,GAAA,CACJ,GAAA,gCACA,EAAA,GAAK,YAAA,GAAe,WAAA,KAAgB,OAAA,CAAQ,CAAA,GAC5C,OAAA,UACA,OAAA;IAAY,GAAA;IAAc,YAAA,GAAe,WAAA;EAAA,IACxC,OAAA,CAAQ,CAAA;EAkRR;;;;;;;EAAA,QA7HK,iBAAA;EA6JgB;;;;;EAhGlB,GAAA,GAAA,CAAO,GAAA,WAAc,OAAA,CAAQ,CAAA;EAiI5B;;;;;;;EAAA,QAhHO,QAAA;;UAYN,YAAA;;;;;;;;EA+BF,GAAA,GAAA,CACJ,GAAA,UACA,KAAA,EAAO,CAAA,EACP,OAAA;IAAY,GAAA;EAAA,IACX,OAAA;;;;;;EAaG,MAAA,CAAO,GAAA,WAAc,OAAA;;EAMrB,KAAA,CAAA,GAAS,OAAA;;;;;;EAaT,GAAA,CAAI,GAAA,WAAc,OAAA;;;;;;;EAaxB,WAAA,CAAY,KAAA,gCAAqC,OAAA;;EAO3C,KAAA,CAAA,GAAS,OAAA;;;;;;;;;;SAaR,KAAA,CAAA;;;;;EASD,gBAAA,CAAA,GAAoB,OAAA;AAAA"}
|
package/dist/cache/index.js
CHANGED
|
@@ -384,6 +384,19 @@ var CacheManager = class CacheManager {
|
|
|
384
384
|
await this.storage.close();
|
|
385
385
|
}
|
|
386
386
|
/**
|
|
387
|
+
* Drop the singleton so the next {@link getInstance} builds a fresh manager.
|
|
388
|
+
*
|
|
389
|
+
* Both fields must clear — `getInstance()` falls back to `initPromise` when
|
|
390
|
+
* `instance` is null. A pointer drop, not teardown: call {@link close} first
|
|
391
|
+
* or the old storage leaks (a `pg.Pool` under `PersistentStorage`).
|
|
392
|
+
*
|
|
393
|
+
* @internal
|
|
394
|
+
*/
|
|
395
|
+
static reset() {
|
|
396
|
+
CacheManager.instance = null;
|
|
397
|
+
CacheManager.initPromise = null;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
387
400
|
* Check if the storage is healthy
|
|
388
401
|
* @returns Promise of true if the storage is healthy, false otherwise
|
|
389
402
|
*/
|
package/dist/cache/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/cache/index.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\n\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\n\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\";\n\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\n\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 // Publishes unconditionally: safe only because every getInstance() is\n // awaited before any reset(), so a reset() can never land mid-init and\n // this can never publish over it. A future unawaited-init caller would\n // reintroduce that stale-publish race (the removed `generation` guard).\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 * Drop the singleton so the next {@link getInstance} builds a fresh manager.\n *\n * Both fields must clear — `getInstance()` falls back to `initPromise` when\n * `instance` is null. A pointer drop, not teardown: call {@link close} first\n * or the old storage leaks (a `pg.Pool` under `PersistentStorage`).\n *\n * @internal\n */\n static reset(): void {\n CacheManager.instance = null;\n CacheManager.initPromise = null;\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,7 +1,7 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { Command } from "commander";
|
|
4
|
-
import { Lang, parse } from "@ast-grep/napi";
|
|
5
5
|
|
|
6
6
|
//#region src/cli/commands/codemod/on-plugins-ready.ts
|
|
7
7
|
const SEARCH_DIRS = [
|
|
@@ -46,6 +46,7 @@ function findTsFiles(dir, files = []) {
|
|
|
46
46
|
return files;
|
|
47
47
|
}
|
|
48
48
|
function isAlreadyMigrated(content) {
|
|
49
|
+
const { Lang, parse } = createRequire(import.meta.url)("@ast-grep/napi");
|
|
49
50
|
return parse(Lang.TypeScript, content).root().findAll("createApp({ $$$PROPS })").some((match) => {
|
|
50
51
|
const text = match.text();
|
|
51
52
|
return /\bonPluginsReady\s*[(:]/.test(text);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"on-plugins-ready.js","names":[],"sources":["../../../../src/cli/commands/codemod/on-plugins-ready.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { Lang, parse } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\n\nconst SEARCH_DIRS = [\"server\", \"src\", \".\"];\nconst CANDIDATE_NAMES = [\"server.ts\", \"index.ts\"];\nconst SKIP_DIRS = new Set([\"node_modules\", \"dist\", \"build\", \".git\"]);\n\nfunction findServerEntryFiles(rootDir: string): string[] {\n const results: string[] = [];\n\n for (const dir of SEARCH_DIRS) {\n const absDir = path.resolve(rootDir, dir);\n if (!fs.existsSync(absDir)) continue;\n\n const files =\n dir === \".\"\n ? CANDIDATE_NAMES.map((n) => path.join(absDir, n)).filter(fs.existsSync)\n : findTsFiles(absDir);\n\n for (const file of files) {\n const content = fs.readFileSync(file, \"utf-8\");\n if (\n content.includes(\"createApp\") &&\n content.includes(\"@databricks/appkit\")\n ) {\n results.push(file);\n }\n }\n }\n\n return [...new Set(results)];\n}\n\nfunction findTsFiles(dir: string, files: string[] = []): string[] {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return files;\n }\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue;\n findTsFiles(fullPath, files);\n } else if (entry.isFile() && entry.name.endsWith(\".ts\")) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\nfunction isAlreadyMigrated(content: string): boolean {\n const ast = parse(Lang.TypeScript, content);\n const root = ast.root();\n return root.findAll(\"createApp({ $$$PROPS })\").some((match) => {\n const text = match.text();\n return /\\bonPluginsReady\\s*[(:]/.test(text);\n });\n}\n\n/**\n * Find the index of the matching closing delimiter for an opening one.\n * Supports (), {}, and [].\n */\nfunction findMatchingClose(content: string, openIdx: number): number {\n const open = content[openIdx];\n const closeMap: Record<string, string> = {\n \"(\": \")\",\n \"{\": \"}\",\n \"[\": \"]\",\n };\n const close = closeMap[open];\n if (!close) return -1;\n\n let depth = 1;\n let i = openIdx + 1;\n while (i < content.length && depth > 0) {\n const ch = content[i];\n if (ch === open) depth++;\n else if (ch === close) depth--;\n\n // skip string literals\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n i = skipString(content, i);\n continue;\n }\n i++;\n }\n return depth === 0 ? i - 1 : -1;\n}\n\nfunction skipString(content: string, startIdx: number): number {\n const quote = content[startIdx];\n let i = startIdx + 1;\n while (i < content.length) {\n if (content[i] === \"\\\\\") {\n i += 2;\n continue;\n }\n if (content[i] === quote) return i + 1;\n i++;\n }\n return i;\n}\n\nfunction stripAutoStartFromServerCalls(content: string): string {\n return content.replace(\n /server\\(\\{([^}]*)\\}\\)/g,\n (_fullMatch, propsStr: string) => {\n const cleaned = propsStr\n .replace(/autoStart\\s*:\\s*(true|false)\\s*,?\\s*/g, \"\")\n .replace(/,\\s*$/, \"\")\n .trim();\n if (!cleaned) return \"server()\";\n return `server({ ${cleaned} })`;\n },\n );\n}\n\ninterface MigrationResult {\n migrated: boolean;\n content: string;\n warnings: string[];\n}\n\nfunction migratePatternA(content: string): MigrationResult {\n const warnings: string[] = [];\n\n // Find createApp(...).then(\n const createAppIdx = content.indexOf(\"createApp(\");\n if (createAppIdx === -1) return { migrated: false, content, warnings };\n\n // Find the opening paren of createApp(\n const configOpenParen = content.indexOf(\"(\", createAppIdx);\n const configCloseParen = findMatchingClose(content, configOpenParen);\n if (configCloseParen === -1) return { migrated: false, content, warnings };\n\n // Check for .then( after the closing paren\n const afterCreateApp = content.slice(configCloseParen + 1);\n const thenMatch = afterCreateApp.match(/^\\s*\\.then\\s*\\(/);\n if (!thenMatch) return { migrated: false, content, warnings };\n\n const thenStart = configCloseParen + 1 + afterCreateApp.indexOf(\".then\");\n const thenOpenParen = content.indexOf(\"(\", thenStart + 4);\n const thenCloseParen = findMatchingClose(content, thenOpenParen);\n if (thenCloseParen === -1) return { migrated: false, content, warnings };\n\n // Extract the callback inside .then(...)\n const thenRaw = content.slice(thenOpenParen + 1, thenCloseParen);\n const thenInner = thenRaw.trim();\n\n // Parse callback: (param) => { body } or async (param) => { body }\n const callbackMatch = thenInner.match(\n /^(?:async\\s+)?\\(\\s*(\\w+)\\s*\\)\\s*=>\\s*\\{/,\n );\n if (!callbackMatch) return { migrated: false, content, warnings };\n\n const paramName = callbackMatch[1];\n const bodyOpenBrace = thenOpenParen + 1 + thenRaw.indexOf(\"{\");\n const bodyCloseBrace = findMatchingClose(content, bodyOpenBrace);\n if (bodyCloseBrace === -1) return { migrated: false, content, warnings };\n\n let callbackBody = content.slice(bodyOpenBrace + 1, bodyCloseBrace).trim();\n\n // Remove entire statements that are just .start() calls (e.g. `await appkit.server.start();`)\n callbackBody = callbackBody\n .replace(/^\\s*(?:await\\s+)?\\w+\\.server\\s*\\.\\s*start\\(\\s*\\)\\s*;?\\s*$/gm, \"\")\n .replace(/\\n\\s*\\.start\\(\\s*\\)\\s*;?/g, \";\")\n .replace(/\\.start\\(\\s*\\)/g, \"\")\n .replace(/\\n\\s*\\n\\s*\\n/g, \"\\n\\n\")\n .trim();\n\n // Clean up trailing semicolons\n if (callbackBody.endsWith(\";\")) {\n // fine\n } else if (!callbackBody.endsWith(\"}\")) {\n callbackBody += \";\";\n }\n\n // Detect if the callback was async\n const isAsync = /^async\\s/.test(thenInner.trim());\n\n // Check for .catch() after .then(...) using brace-aware parsing\n const afterThenClose = content.slice(thenCloseParen + 1);\n const catchPatternMatch = afterThenClose.match(/^\\s*(?:\\)\\s*)?\\.catch\\s*\\(/);\n\n let catchSuffix: string;\n let consumeAfterThen: number;\n\n if (catchPatternMatch) {\n const catchOpenParen = thenCloseParen + 1 + catchPatternMatch[0].length - 1;\n const catchCloseParen = findMatchingClose(content, catchOpenParen);\n if (catchCloseParen !== -1) {\n const catchArg = content\n .slice(catchOpenParen + 1, catchCloseParen)\n .trim();\n catchSuffix = `.catch(${catchArg})`;\n consumeAfterThen = catchCloseParen + 1 - (thenCloseParen + 1);\n } else {\n catchSuffix = \".catch(console.error)\";\n consumeAfterThen = 0;\n }\n } else {\n catchSuffix = \".catch(console.error)\";\n consumeAfterThen = 0;\n }\n\n // Build the onPluginsReady property\n const configStr = content.slice(configOpenParen + 1, configCloseParen);\n const lastBraceIdx = configStr.lastIndexOf(\"}\");\n if (lastBraceIdx === -1) return { migrated: false, content, warnings };\n\n const beforeLastBrace = configStr.slice(0, lastBraceIdx).trimEnd();\n const needsComma = beforeLastBrace.endsWith(\",\") ? \"\" : \",\";\n\n // Indent the body properly\n const bodyLines = callbackBody.split(\"\\n\");\n const indentedBody = bodyLines\n .map((line) => ` ${line.trimStart()}`)\n .join(\"\\n\");\n\n const asyncPrefix = isAsync ? \"async \" : \"\";\n const onPluginsReadyProp = `${needsComma}\\n ${asyncPrefix}onPluginsReady(${paramName}) {\\n${indentedBody}\\n },`;\n const newConfig = `${beforeLastBrace}${onPluginsReadyProp}\\n}`;\n\n // Build the replacement\n const endIdx = thenCloseParen + 1 + consumeAfterThen;\n // Consume trailing ) ; and whitespace\n let finalEnd = endIdx;\n const trailing = content.slice(finalEnd).match(/^\\s*\\)?\\s*;?\\s*/);\n if (trailing) finalEnd += trailing[0].length;\n\n const newContent =\n content.slice(0, createAppIdx) +\n `createApp(${newConfig})${catchSuffix};` +\n content.slice(finalEnd);\n\n return { migrated: true, content: newContent, warnings };\n}\n\nfunction migratePatternB(content: string): MigrationResult {\n const warnings: string[] = [];\n\n // Match: const/let varName = await createApp({...});\n const awaitPattern = /(?:const|let)\\s+(\\w+)\\s*=\\s*await\\s+createApp\\s*\\(/;\n\n const match = content.match(awaitPattern);\n if (!match) return { migrated: false, content, warnings };\n\n const varName = match[1];\n const matchIdx = content.indexOf(match[0]);\n\n // Find the createApp(...) closing paren\n const configOpenParen = matchIdx + match[0].length - 1;\n const configCloseParen = findMatchingClose(content, configOpenParen);\n if (configCloseParen === -1) return { migrated: false, content, warnings };\n\n // Find the semicolon after the createApp call\n const afterCall = content.slice(configCloseParen + 1);\n const semiMatch = afterCall.match(/^\\s*;/);\n const createAppEnd =\n configCloseParen + 1 + (semiMatch ? semiMatch[0].length : 0);\n\n // Find all uses of varName after the createApp call\n const afterCreateApp = content.slice(createAppEnd);\n const varUsagePattern = new RegExp(`\\\\b${varName}\\\\.(\\\\w+)`, \"g\");\n\n const usages: { plugin: string; index: number }[] = [];\n for (const usageMatch of afterCreateApp.matchAll(varUsagePattern)) {\n usages.push({ plugin: usageMatch[1], index: usageMatch.index });\n }\n\n // Check for non-server usage\n const nonServerUsage = usages.filter((u) => u.plugin !== \"server\");\n if (nonServerUsage.length > 0) {\n warnings.push(\n `Found additional usage of '${varName}' handle outside server.extend/start. Please migrate manually.`,\n );\n return { migrated: false, content, warnings };\n }\n\n // Find the extend call(s) and start call in the after-createApp region\n const extendPattern = new RegExp(\n `\\\\b${varName}\\\\.server\\\\.extend\\\\s*\\\\(`,\n \"g\",\n );\n const startPattern = new RegExp(\n `(?:await\\\\s+)?${varName}\\\\.server\\\\.start\\\\s*\\\\(\\\\s*\\\\)\\\\s*;`,\n );\n\n const extendMatches = [...afterCreateApp.matchAll(extendPattern)];\n if (extendMatches.length > 1) {\n warnings.push(\n `Found ${extendMatches.length} server.extend() calls. Please migrate manually.`,\n );\n return { migrated: false, content, warnings };\n }\n\n const extendExec = extendMatches[0] ?? null;\n const startExec = startPattern.exec(afterCreateApp);\n\n if (!startExec) return { migrated: false, content, warnings };\n\n // Extract the extend call's argument\n let extendArg = \"\";\n let extendFullStatement = \"\";\n if (extendExec) {\n const extendOpenParen =\n createAppEnd + extendExec.index + extendExec[0].length - 1;\n const extendCloseParen = findMatchingClose(content, extendOpenParen);\n if (extendCloseParen !== -1) {\n extendArg = content.slice(extendOpenParen + 1, extendCloseParen).trim();\n // Find the full statement including trailing semicolon\n const stmtStart = createAppEnd + extendExec.index;\n let stmtEnd = extendCloseParen + 1;\n const afterExtend = content.slice(stmtEnd);\n const trailingSemi = afterExtend.match(/^\\s*;/);\n if (trailingSemi) stmtEnd += trailingSemi[0].length;\n extendFullStatement = content.slice(stmtStart, stmtEnd);\n }\n }\n\n const startFullStatement = startExec[0];\n\n // Build the onPluginsReady callback\n const configStr = content.slice(configOpenParen + 1, configCloseParen);\n const lastBraceIdx = configStr.lastIndexOf(\"}\");\n if (lastBraceIdx === -1) return { migrated: false, content, warnings };\n\n const beforeLastBrace = configStr.slice(0, lastBraceIdx).trimEnd();\n const needsComma = beforeLastBrace.endsWith(\",\") ? \"\" : \",\";\n\n let onPluginsReadyProp: string;\n if (extendArg) {\n onPluginsReadyProp =\n `${needsComma}\\n onPluginsReady(${varName}) {\\n` +\n ` ${varName}.server.extend(${extendArg});\\n` +\n \" },\";\n } else {\n onPluginsReadyProp = \"\";\n }\n\n const newConfig = `${beforeLastBrace}${onPluginsReadyProp}\\n}`;\n const newCreateApp = `await createApp(${newConfig});`;\n\n // Replace: remove const declaration, replace with plain await, remove extend + start\n let result = content.slice(0, matchIdx) + newCreateApp;\n let remaining = afterCreateApp;\n\n if (extendFullStatement) {\n remaining = remaining.replace(extendFullStatement, \"\");\n }\n remaining = remaining.replace(startFullStatement, \"\");\n\n // Clean up consecutive blank lines\n remaining = remaining.replace(/\\n\\s*\\n\\s*\\n/g, \"\\n\\n\");\n\n result += remaining;\n\n return { migrated: true, content: result, warnings };\n}\n\nexport function migrateFile(filePath: string): MigrationResult {\n const original = fs.readFileSync(filePath, \"utf-8\");\n\n if (isAlreadyMigrated(original)) {\n return {\n migrated: false,\n content: original,\n warnings: [\"Already migrated -- no changes needed.\"],\n };\n }\n\n const content = stripAutoStartFromServerCalls(original);\n const allWarnings: string[] = [];\n\n // Try Pattern A first\n const patternA = migratePatternA(content);\n if (patternA.migrated) {\n allWarnings.push(...patternA.warnings);\n return {\n migrated: true,\n content: patternA.content,\n warnings: allWarnings,\n };\n }\n allWarnings.push(...patternA.warnings);\n\n // Try Pattern B\n const patternB = migratePatternB(content);\n if (patternB.migrated) {\n allWarnings.push(...patternB.warnings);\n return {\n migrated: true,\n content: patternB.content,\n warnings: allWarnings,\n };\n }\n allWarnings.push(...patternB.warnings);\n\n // Check if autoStart was stripped (content changed but no pattern matched)\n if (content !== original) {\n return { migrated: true, content, warnings: allWarnings };\n }\n\n return { migrated: false, content: original, warnings: allWarnings };\n}\n\nfunction runCodemod(options: { path?: string; write?: boolean }) {\n const rootDir = process.cwd();\n const write = options.write ?? false;\n\n let files: string[];\n if (options.path) {\n const absPath = path.resolve(rootDir, options.path);\n if (!fs.existsSync(absPath)) {\n console.error(`File not found: ${absPath}`);\n process.exit(1);\n }\n files = [absPath];\n } else {\n files = findServerEntryFiles(rootDir);\n }\n\n if (files.length === 0) {\n console.log(\"No files found importing createApp from @databricks/appkit.\");\n console.log(\"Use --path to specify a file explicitly.\");\n process.exit(0);\n }\n\n let hasChanges = false;\n\n for (const file of files) {\n const relPath = path.relative(rootDir, file);\n const result = migrateFile(file);\n\n for (const warning of result.warnings) {\n console.log(` ${relPath}: ${warning}`);\n }\n\n if (!result.migrated) {\n if (result.warnings.length === 0) {\n console.log(` ${relPath}: No migration needed.`);\n }\n continue;\n }\n\n hasChanges = true;\n\n if (write) {\n fs.writeFileSync(file, result.content, \"utf-8\");\n console.log(` ${relPath}: Migrated successfully.`);\n } else {\n console.log(`\\n--- ${relPath} (dry run) ---`);\n console.log(result.content);\n console.log(\"---\");\n }\n }\n\n if (hasChanges && !write) {\n console.log(\"\\nDry run complete. Run with --write to apply changes.\");\n }\n}\n\nexport const onPluginsReadyCommand = new Command(\"on-plugins-ready\")\n .description(\n \"Migrate createApp usage from autoStart/extend/start pattern to onPluginsReady callback\",\n )\n .option(\"--path <file>\", \"Path to the server entry file to migrate\")\n .option(\"--write\", \"Apply changes (default: dry-run)\", false)\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit codemod on-plugins-ready # dry-run, auto-detect files\n $ appkit codemod on-plugins-ready --write # apply changes\n $ appkit codemod on-plugins-ready --path server.ts # migrate a specific file`,\n )\n .action(runCodemod);\n"],"mappings":";;;;;;AAMA,MAAM,cAAc;CAAC;CAAU;CAAO;CAAI;AAC1C,MAAM,kBAAkB,CAAC,aAAa,WAAW;AACjD,MAAM,YAAY,IAAI,IAAI;CAAC;CAAgB;CAAQ;CAAS;CAAO,CAAC;AAEpE,SAAS,qBAAqB,SAA2B;CACvD,MAAM,UAAoB,EAAE;AAE5B,MAAK,MAAM,OAAO,aAAa;EAC7B,MAAM,SAAS,KAAK,QAAQ,SAAS,IAAI;AACzC,MAAI,CAAC,GAAG,WAAW,OAAO,CAAE;EAE5B,MAAM,QACJ,QAAQ,MACJ,gBAAgB,KAAK,MAAM,KAAK,KAAK,QAAQ,EAAE,CAAC,CAAC,OAAO,GAAG,WAAW,GACtE,YAAY,OAAO;AAEzB,OAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,UAAU,GAAG,aAAa,MAAM,QAAQ;AAC9C,OACE,QAAQ,SAAS,YAAY,IAC7B,QAAQ,SAAS,qBAAqB,CAEtC,SAAQ,KAAK,KAAK;;;AAKxB,QAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,CAAC;;AAG9B,SAAS,YAAY,KAAa,QAAkB,EAAE,EAAY;CAChE,IAAI;AACJ,KAAI;AACF,YAAU,GAAG,YAAY,KAAK,EAAE,eAAe,MAAM,CAAC;SAChD;AACN,SAAO;;AAGT,MAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,WAAW,KAAK,KAAK,KAAK,MAAM,KAAK;AAC3C,MAAI,MAAM,aAAa,EAAE;AACvB,OAAI,UAAU,IAAI,MAAM,KAAK,CAAE;AAC/B,eAAY,UAAU,MAAM;aACnB,MAAM,QAAQ,IAAI,MAAM,KAAK,SAAS,MAAM,CACrD,OAAM,KAAK,SAAS;;AAIxB,QAAO;;AAGT,SAAS,kBAAkB,SAA0B;AAGnD,QAFY,MAAM,KAAK,YAAY,QAAQ,CAC1B,MAAM,CACX,QAAQ,0BAA0B,CAAC,MAAM,UAAU;EAC7D,MAAM,OAAO,MAAM,MAAM;AACzB,SAAO,0BAA0B,KAAK,KAAK;GAC3C;;;;;;AAOJ,SAAS,kBAAkB,SAAiB,SAAyB;CACnE,MAAM,OAAO,QAAQ;CAMrB,MAAM,QALmC;EACvC,KAAK;EACL,KAAK;EACL,KAAK;EACN,CACsB;AACvB,KAAI,CAAC,MAAO,QAAO;CAEnB,IAAI,QAAQ;CACZ,IAAI,IAAI,UAAU;AAClB,QAAO,IAAI,QAAQ,UAAU,QAAQ,GAAG;EACtC,MAAM,KAAK,QAAQ;AACnB,MAAI,OAAO,KAAM;WACR,OAAO,MAAO;AAGvB,MAAI,OAAO,QAAO,OAAO,OAAO,OAAO,KAAK;AAC1C,OAAI,WAAW,SAAS,EAAE;AAC1B;;AAEF;;AAEF,QAAO,UAAU,IAAI,IAAI,IAAI;;AAG/B,SAAS,WAAW,SAAiB,UAA0B;CAC7D,MAAM,QAAQ,QAAQ;CACtB,IAAI,IAAI,WAAW;AACnB,QAAO,IAAI,QAAQ,QAAQ;AACzB,MAAI,QAAQ,OAAO,MAAM;AACvB,QAAK;AACL;;AAEF,MAAI,QAAQ,OAAO,MAAO,QAAO,IAAI;AACrC;;AAEF,QAAO;;AAGT,SAAS,8BAA8B,SAAyB;AAC9D,QAAO,QAAQ,QACb,2BACC,YAAY,aAAqB;EAChC,MAAM,UAAU,SACb,QAAQ,yCAAyC,GAAG,CACpD,QAAQ,SAAS,GAAG,CACpB,MAAM;AACT,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,YAAY,QAAQ;GAE9B;;AASH,SAAS,gBAAgB,SAAkC;CACzD,MAAM,WAAqB,EAAE;CAG7B,MAAM,eAAe,QAAQ,QAAQ,aAAa;AAClD,KAAI,iBAAiB,GAAI,QAAO;EAAE,UAAU;EAAO;EAAS;EAAU;CAGtE,MAAM,kBAAkB,QAAQ,QAAQ,KAAK,aAAa;CAC1D,MAAM,mBAAmB,kBAAkB,SAAS,gBAAgB;AACpE,KAAI,qBAAqB,GAAI,QAAO;EAAE,UAAU;EAAO;EAAS;EAAU;CAG1E,MAAM,iBAAiB,QAAQ,MAAM,mBAAmB,EAAE;AAE1D,KAAI,CADc,eAAe,MAAM,kBAAkB,CACzC,QAAO;EAAE,UAAU;EAAO;EAAS;EAAU;CAE7D,MAAM,YAAY,mBAAmB,IAAI,eAAe,QAAQ,QAAQ;CACxE,MAAM,gBAAgB,QAAQ,QAAQ,KAAK,YAAY,EAAE;CACzD,MAAM,iBAAiB,kBAAkB,SAAS,cAAc;AAChE,KAAI,mBAAmB,GAAI,QAAO;EAAE,UAAU;EAAO;EAAS;EAAU;CAGxE,MAAM,UAAU,QAAQ,MAAM,gBAAgB,GAAG,eAAe;CAChE,MAAM,YAAY,QAAQ,MAAM;CAGhC,MAAM,gBAAgB,UAAU,MAC9B,0CACD;AACD,KAAI,CAAC,cAAe,QAAO;EAAE,UAAU;EAAO;EAAS;EAAU;CAEjE,MAAM,YAAY,cAAc;CAChC,MAAM,gBAAgB,gBAAgB,IAAI,QAAQ,QAAQ,IAAI;CAC9D,MAAM,iBAAiB,kBAAkB,SAAS,cAAc;AAChE,KAAI,mBAAmB,GAAI,QAAO;EAAE,UAAU;EAAO;EAAS;EAAU;CAExE,IAAI,eAAe,QAAQ,MAAM,gBAAgB,GAAG,eAAe,CAAC,MAAM;AAG1E,gBAAe,aACZ,QAAQ,+DAA+D,GAAG,CAC1E,QAAQ,6BAA6B,IAAI,CACzC,QAAQ,mBAAmB,GAAG,CAC9B,QAAQ,iBAAiB,OAAO,CAChC,MAAM;AAGT,KAAI,aAAa,SAAS,IAAI,EAAE,YAErB,CAAC,aAAa,SAAS,IAAI,CACpC,iBAAgB;CAIlB,MAAM,UAAU,WAAW,KAAK,UAAU,MAAM,CAAC;CAIjD,MAAM,oBADiB,QAAQ,MAAM,iBAAiB,EAAE,CACf,MAAM,6BAA6B;CAE5E,IAAI;CACJ,IAAI;AAEJ,KAAI,mBAAmB;EACrB,MAAM,iBAAiB,iBAAiB,IAAI,kBAAkB,GAAG,SAAS;EAC1E,MAAM,kBAAkB,kBAAkB,SAAS,eAAe;AAClE,MAAI,oBAAoB,IAAI;AAI1B,iBAAc,UAHG,QACd,MAAM,iBAAiB,GAAG,gBAAgB,CAC1C,MAAM,CACwB;AACjC,sBAAmB,kBAAkB,KAAK,iBAAiB;SACtD;AACL,iBAAc;AACd,sBAAmB;;QAEhB;AACL,gBAAc;AACd,qBAAmB;;CAIrB,MAAM,YAAY,QAAQ,MAAM,kBAAkB,GAAG,iBAAiB;CACtE,MAAM,eAAe,UAAU,YAAY,IAAI;AAC/C,KAAI,iBAAiB,GAAI,QAAO;EAAE,UAAU;EAAO;EAAS;EAAU;CAEtE,MAAM,kBAAkB,UAAU,MAAM,GAAG,aAAa,CAAC,SAAS;CAClE,MAAM,aAAa,gBAAgB,SAAS,IAAI,GAAG,KAAK;CAIxD,MAAM,eADY,aAAa,MAAM,KAAK,CAEvC,KAAK,SAAS,OAAO,KAAK,WAAW,GAAG,CACxC,KAAK,KAAK;CAIb,MAAM,YAAY,GAAG,kBADM,GAAG,WAAW,MADrB,UAAU,WAAW,GACkB,iBAAiB,UAAU,OAAO,aAAa,QAChD;CAK1D,IAAI,WAFW,iBAAiB,IAAI;CAGpC,MAAM,WAAW,QAAQ,MAAM,SAAS,CAAC,MAAM,kBAAkB;AACjE,KAAI,SAAU,aAAY,SAAS,GAAG;AAOtC,QAAO;EAAE,UAAU;EAAM,SAJvB,QAAQ,MAAM,GAAG,aAAa,GAC9B,aAAa,UAAU,GAAG,YAAY,KACtC,QAAQ,MAAM,SAAS;EAEqB;EAAU;;AAG1D,SAAS,gBAAgB,SAAkC;CACzD,MAAM,WAAqB,EAAE;CAK7B,MAAM,QAAQ,QAAQ,MAFD,qDAEoB;AACzC,KAAI,CAAC,MAAO,QAAO;EAAE,UAAU;EAAO;EAAS;EAAU;CAEzD,MAAM,UAAU,MAAM;CACtB,MAAM,WAAW,QAAQ,QAAQ,MAAM,GAAG;CAG1C,MAAM,kBAAkB,WAAW,MAAM,GAAG,SAAS;CACrD,MAAM,mBAAmB,kBAAkB,SAAS,gBAAgB;AACpE,KAAI,qBAAqB,GAAI,QAAO;EAAE,UAAU;EAAO;EAAS;EAAU;CAI1E,MAAM,YADY,QAAQ,MAAM,mBAAmB,EAAE,CACzB,MAAM,QAAQ;CAC1C,MAAM,eACJ,mBAAmB,KAAK,YAAY,UAAU,GAAG,SAAS;CAG5D,MAAM,iBAAiB,QAAQ,MAAM,aAAa;CAClD,MAAM,kBAAkB,IAAI,OAAO,MAAM,QAAQ,YAAY,IAAI;CAEjE,MAAM,SAA8C,EAAE;AACtD,MAAK,MAAM,cAAc,eAAe,SAAS,gBAAgB,CAC/D,QAAO,KAAK;EAAE,QAAQ,WAAW;EAAI,OAAO,WAAW;EAAO,CAAC;AAKjE,KADuB,OAAO,QAAQ,MAAM,EAAE,WAAW,SAAS,CAC/C,SAAS,GAAG;AAC7B,WAAS,KACP,8BAA8B,QAAQ,gEACvC;AACD,SAAO;GAAE,UAAU;GAAO;GAAS;GAAU;;CAI/C,MAAM,gBAAgB,IAAI,OACxB,MAAM,QAAQ,4BACd,IACD;CACD,MAAM,eAAe,IAAI,OACvB,iBAAiB,QAAQ,sCAC1B;CAED,MAAM,gBAAgB,CAAC,GAAG,eAAe,SAAS,cAAc,CAAC;AACjE,KAAI,cAAc,SAAS,GAAG;AAC5B,WAAS,KACP,SAAS,cAAc,OAAO,kDAC/B;AACD,SAAO;GAAE,UAAU;GAAO;GAAS;GAAU;;CAG/C,MAAM,aAAa,cAAc,MAAM;CACvC,MAAM,YAAY,aAAa,KAAK,eAAe;AAEnD,KAAI,CAAC,UAAW,QAAO;EAAE,UAAU;EAAO;EAAS;EAAU;CAG7D,IAAI,YAAY;CAChB,IAAI,sBAAsB;AAC1B,KAAI,YAAY;EACd,MAAM,kBACJ,eAAe,WAAW,QAAQ,WAAW,GAAG,SAAS;EAC3D,MAAM,mBAAmB,kBAAkB,SAAS,gBAAgB;AACpE,MAAI,qBAAqB,IAAI;AAC3B,eAAY,QAAQ,MAAM,kBAAkB,GAAG,iBAAiB,CAAC,MAAM;GAEvE,MAAM,YAAY,eAAe,WAAW;GAC5C,IAAI,UAAU,mBAAmB;GAEjC,MAAM,eADc,QAAQ,MAAM,QAAQ,CACT,MAAM,QAAQ;AAC/C,OAAI,aAAc,YAAW,aAAa,GAAG;AAC7C,yBAAsB,QAAQ,MAAM,WAAW,QAAQ;;;CAI3D,MAAM,qBAAqB,UAAU;CAGrC,MAAM,YAAY,QAAQ,MAAM,kBAAkB,GAAG,iBAAiB;CACtE,MAAM,eAAe,UAAU,YAAY,IAAI;AAC/C,KAAI,iBAAiB,GAAI,QAAO;EAAE,UAAU;EAAO;EAAS;EAAU;CAEtE,MAAM,kBAAkB,UAAU,MAAM,GAAG,aAAa,CAAC,SAAS;CAClE,MAAM,aAAa,gBAAgB,SAAS,IAAI,GAAG,KAAK;CAExD,IAAI;AACJ,KAAI,UACF,sBACE,GAAG,WAAW,qBAAqB,QAAQ,WACpC,QAAQ,iBAAiB,UAAU;KAG5C,sBAAqB;CAIvB,MAAM,eAAe,mBADH,GAAG,kBAAkB,mBAAmB,KACR;CAGlD,IAAI,SAAS,QAAQ,MAAM,GAAG,SAAS,GAAG;CAC1C,IAAI,YAAY;AAEhB,KAAI,oBACF,aAAY,UAAU,QAAQ,qBAAqB,GAAG;AAExD,aAAY,UAAU,QAAQ,oBAAoB,GAAG;AAGrD,aAAY,UAAU,QAAQ,iBAAiB,OAAO;AAEtD,WAAU;AAEV,QAAO;EAAE,UAAU;EAAM,SAAS;EAAQ;EAAU;;AAGtD,SAAgB,YAAY,UAAmC;CAC7D,MAAM,WAAW,GAAG,aAAa,UAAU,QAAQ;AAEnD,KAAI,kBAAkB,SAAS,CAC7B,QAAO;EACL,UAAU;EACV,SAAS;EACT,UAAU,CAAC,yCAAyC;EACrD;CAGH,MAAM,UAAU,8BAA8B,SAAS;CACvD,MAAM,cAAwB,EAAE;CAGhC,MAAM,WAAW,gBAAgB,QAAQ;AACzC,KAAI,SAAS,UAAU;AACrB,cAAY,KAAK,GAAG,SAAS,SAAS;AACtC,SAAO;GACL,UAAU;GACV,SAAS,SAAS;GAClB,UAAU;GACX;;AAEH,aAAY,KAAK,GAAG,SAAS,SAAS;CAGtC,MAAM,WAAW,gBAAgB,QAAQ;AACzC,KAAI,SAAS,UAAU;AACrB,cAAY,KAAK,GAAG,SAAS,SAAS;AACtC,SAAO;GACL,UAAU;GACV,SAAS,SAAS;GAClB,UAAU;GACX;;AAEH,aAAY,KAAK,GAAG,SAAS,SAAS;AAGtC,KAAI,YAAY,SACd,QAAO;EAAE,UAAU;EAAM;EAAS,UAAU;EAAa;AAG3D,QAAO;EAAE,UAAU;EAAO,SAAS;EAAU,UAAU;EAAa;;AAGtE,SAAS,WAAW,SAA6C;CAC/D,MAAM,UAAU,QAAQ,KAAK;CAC7B,MAAM,QAAQ,QAAQ,SAAS;CAE/B,IAAI;AACJ,KAAI,QAAQ,MAAM;EAChB,MAAM,UAAU,KAAK,QAAQ,SAAS,QAAQ,KAAK;AACnD,MAAI,CAAC,GAAG,WAAW,QAAQ,EAAE;AAC3B,WAAQ,MAAM,mBAAmB,UAAU;AAC3C,WAAQ,KAAK,EAAE;;AAEjB,UAAQ,CAAC,QAAQ;OAEjB,SAAQ,qBAAqB,QAAQ;AAGvC,KAAI,MAAM,WAAW,GAAG;AACtB,UAAQ,IAAI,8DAA8D;AAC1E,UAAQ,IAAI,2CAA2C;AACvD,UAAQ,KAAK,EAAE;;CAGjB,IAAI,aAAa;AAEjB,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,KAAK,SAAS,SAAS,KAAK;EAC5C,MAAM,SAAS,YAAY,KAAK;AAEhC,OAAK,MAAM,WAAW,OAAO,SAC3B,SAAQ,IAAI,KAAK,QAAQ,IAAI,UAAU;AAGzC,MAAI,CAAC,OAAO,UAAU;AACpB,OAAI,OAAO,SAAS,WAAW,EAC7B,SAAQ,IAAI,KAAK,QAAQ,wBAAwB;AAEnD;;AAGF,eAAa;AAEb,MAAI,OAAO;AACT,MAAG,cAAc,MAAM,OAAO,SAAS,QAAQ;AAC/C,WAAQ,IAAI,KAAK,QAAQ,0BAA0B;SAC9C;AACL,WAAQ,IAAI,SAAS,QAAQ,gBAAgB;AAC7C,WAAQ,IAAI,OAAO,QAAQ;AAC3B,WAAQ,IAAI,MAAM;;;AAItB,KAAI,cAAc,CAAC,MACjB,SAAQ,IAAI,yDAAyD;;AAIzE,MAAa,wBAAwB,IAAI,QAAQ,mBAAmB,CACjE,YACC,yFACD,CACA,OAAO,iBAAiB,2CAA2C,CACnE,OAAO,WAAW,oCAAoC,MAAM,CAC5D,YACC,SACA;;;;kFAKD,CACA,OAAO,WAAW"}
|
|
1
|
+
{"version":3,"file":"on-plugins-ready.js","names":[],"sources":["../../../../src/cli/commands/codemod/on-plugins-ready.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport path from \"node:path\";\n\nimport { Command } from \"commander\";\n\nconst SEARCH_DIRS = [\"server\", \"src\", \".\"];\nconst CANDIDATE_NAMES = [\"server.ts\", \"index.ts\"];\nconst SKIP_DIRS = new Set([\"node_modules\", \"dist\", \"build\", \".git\"]);\n\nfunction findServerEntryFiles(rootDir: string): string[] {\n const results: string[] = [];\n\n for (const dir of SEARCH_DIRS) {\n const absDir = path.resolve(rootDir, dir);\n if (!fs.existsSync(absDir)) continue;\n\n const files =\n dir === \".\"\n ? CANDIDATE_NAMES.map((n) => path.join(absDir, n)).filter(fs.existsSync)\n : findTsFiles(absDir);\n\n for (const file of files) {\n const content = fs.readFileSync(file, \"utf-8\");\n if (\n content.includes(\"createApp\") &&\n content.includes(\"@databricks/appkit\")\n ) {\n results.push(file);\n }\n }\n }\n\n return [...new Set(results)];\n}\n\nfunction findTsFiles(dir: string, files: string[] = []): string[] {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return files;\n }\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue;\n findTsFiles(fullPath, files);\n } else if (entry.isFile() && entry.name.endsWith(\".ts\")) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\nfunction isAlreadyMigrated(content: string): boolean {\n const require = createRequire(import.meta.url);\n const { Lang, parse } =\n require(\"@ast-grep/napi\") as typeof import(\"@ast-grep/napi\");\n const ast = parse(Lang.TypeScript, content);\n const root = ast.root();\n return root.findAll(\"createApp({ $$$PROPS })\").some((match) => {\n const text = match.text();\n return /\\bonPluginsReady\\s*[(:]/.test(text);\n });\n}\n\n/**\n * Find the index of the matching closing delimiter for an opening one.\n * Supports (), {}, and [].\n */\nfunction findMatchingClose(content: string, openIdx: number): number {\n const open = content[openIdx];\n const closeMap: Record<string, string> = {\n \"(\": \")\",\n \"{\": \"}\",\n \"[\": \"]\",\n };\n const close = closeMap[open];\n if (!close) return -1;\n\n let depth = 1;\n let i = openIdx + 1;\n while (i < content.length && depth > 0) {\n const ch = content[i];\n if (ch === open) depth++;\n else if (ch === close) depth--;\n\n // skip string literals\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n i = skipString(content, i);\n continue;\n }\n i++;\n }\n return depth === 0 ? i - 1 : -1;\n}\n\nfunction skipString(content: string, startIdx: number): number {\n const quote = content[startIdx];\n let i = startIdx + 1;\n while (i < content.length) {\n if (content[i] === \"\\\\\") {\n i += 2;\n continue;\n }\n if (content[i] === quote) return i + 1;\n i++;\n }\n return i;\n}\n\nfunction stripAutoStartFromServerCalls(content: string): string {\n return content.replace(\n /server\\(\\{([^}]*)\\}\\)/g,\n (_fullMatch, propsStr: string) => {\n const cleaned = propsStr\n .replace(/autoStart\\s*:\\s*(true|false)\\s*,?\\s*/g, \"\")\n .replace(/,\\s*$/, \"\")\n .trim();\n if (!cleaned) return \"server()\";\n return `server({ ${cleaned} })`;\n },\n );\n}\n\ninterface MigrationResult {\n migrated: boolean;\n content: string;\n warnings: string[];\n}\n\nfunction migratePatternA(content: string): MigrationResult {\n const warnings: string[] = [];\n\n // Find createApp(...).then(\n const createAppIdx = content.indexOf(\"createApp(\");\n if (createAppIdx === -1) return { migrated: false, content, warnings };\n\n // Find the opening paren of createApp(\n const configOpenParen = content.indexOf(\"(\", createAppIdx);\n const configCloseParen = findMatchingClose(content, configOpenParen);\n if (configCloseParen === -1) return { migrated: false, content, warnings };\n\n // Check for .then( after the closing paren\n const afterCreateApp = content.slice(configCloseParen + 1);\n const thenMatch = afterCreateApp.match(/^\\s*\\.then\\s*\\(/);\n if (!thenMatch) return { migrated: false, content, warnings };\n\n const thenStart = configCloseParen + 1 + afterCreateApp.indexOf(\".then\");\n const thenOpenParen = content.indexOf(\"(\", thenStart + 4);\n const thenCloseParen = findMatchingClose(content, thenOpenParen);\n if (thenCloseParen === -1) return { migrated: false, content, warnings };\n\n // Extract the callback inside .then(...)\n const thenRaw = content.slice(thenOpenParen + 1, thenCloseParen);\n const thenInner = thenRaw.trim();\n\n // Parse callback: (param) => { body } or async (param) => { body }\n const callbackMatch = thenInner.match(\n /^(?:async\\s+)?\\(\\s*(\\w+)\\s*\\)\\s*=>\\s*\\{/,\n );\n if (!callbackMatch) return { migrated: false, content, warnings };\n\n const paramName = callbackMatch[1];\n const bodyOpenBrace = thenOpenParen + 1 + thenRaw.indexOf(\"{\");\n const bodyCloseBrace = findMatchingClose(content, bodyOpenBrace);\n if (bodyCloseBrace === -1) return { migrated: false, content, warnings };\n\n let callbackBody = content.slice(bodyOpenBrace + 1, bodyCloseBrace).trim();\n\n // Remove entire statements that are just .start() calls (e.g. `await appkit.server.start();`)\n callbackBody = callbackBody\n .replace(/^\\s*(?:await\\s+)?\\w+\\.server\\s*\\.\\s*start\\(\\s*\\)\\s*;?\\s*$/gm, \"\")\n .replace(/\\n\\s*\\.start\\(\\s*\\)\\s*;?/g, \";\")\n .replace(/\\.start\\(\\s*\\)/g, \"\")\n .replace(/\\n\\s*\\n\\s*\\n/g, \"\\n\\n\")\n .trim();\n\n // Clean up trailing semicolons\n if (callbackBody.endsWith(\";\")) {\n // fine\n } else if (!callbackBody.endsWith(\"}\")) {\n callbackBody += \";\";\n }\n\n // Detect if the callback was async\n const isAsync = /^async\\s/.test(thenInner.trim());\n\n // Check for .catch() after .then(...) using brace-aware parsing\n const afterThenClose = content.slice(thenCloseParen + 1);\n const catchPatternMatch = afterThenClose.match(/^\\s*(?:\\)\\s*)?\\.catch\\s*\\(/);\n\n let catchSuffix: string;\n let consumeAfterThen: number;\n\n if (catchPatternMatch) {\n const catchOpenParen = thenCloseParen + 1 + catchPatternMatch[0].length - 1;\n const catchCloseParen = findMatchingClose(content, catchOpenParen);\n if (catchCloseParen !== -1) {\n const catchArg = content\n .slice(catchOpenParen + 1, catchCloseParen)\n .trim();\n catchSuffix = `.catch(${catchArg})`;\n consumeAfterThen = catchCloseParen + 1 - (thenCloseParen + 1);\n } else {\n catchSuffix = \".catch(console.error)\";\n consumeAfterThen = 0;\n }\n } else {\n catchSuffix = \".catch(console.error)\";\n consumeAfterThen = 0;\n }\n\n // Build the onPluginsReady property\n const configStr = content.slice(configOpenParen + 1, configCloseParen);\n const lastBraceIdx = configStr.lastIndexOf(\"}\");\n if (lastBraceIdx === -1) return { migrated: false, content, warnings };\n\n const beforeLastBrace = configStr.slice(0, lastBraceIdx).trimEnd();\n const needsComma = beforeLastBrace.endsWith(\",\") ? \"\" : \",\";\n\n // Indent the body properly\n const bodyLines = callbackBody.split(\"\\n\");\n const indentedBody = bodyLines\n .map((line) => ` ${line.trimStart()}`)\n .join(\"\\n\");\n\n const asyncPrefix = isAsync ? \"async \" : \"\";\n const onPluginsReadyProp = `${needsComma}\\n ${asyncPrefix}onPluginsReady(${paramName}) {\\n${indentedBody}\\n },`;\n const newConfig = `${beforeLastBrace}${onPluginsReadyProp}\\n}`;\n\n // Build the replacement\n const endIdx = thenCloseParen + 1 + consumeAfterThen;\n // Consume trailing ) ; and whitespace\n let finalEnd = endIdx;\n const trailing = content.slice(finalEnd).match(/^\\s*\\)?\\s*;?\\s*/);\n if (trailing) finalEnd += trailing[0].length;\n\n const newContent =\n content.slice(0, createAppIdx) +\n `createApp(${newConfig})${catchSuffix};` +\n content.slice(finalEnd);\n\n return { migrated: true, content: newContent, warnings };\n}\n\nfunction migratePatternB(content: string): MigrationResult {\n const warnings: string[] = [];\n\n // Match: const/let varName = await createApp({...});\n const awaitPattern = /(?:const|let)\\s+(\\w+)\\s*=\\s*await\\s+createApp\\s*\\(/;\n\n const match = content.match(awaitPattern);\n if (!match) return { migrated: false, content, warnings };\n\n const varName = match[1];\n const matchIdx = content.indexOf(match[0]);\n\n // Find the createApp(...) closing paren\n const configOpenParen = matchIdx + match[0].length - 1;\n const configCloseParen = findMatchingClose(content, configOpenParen);\n if (configCloseParen === -1) return { migrated: false, content, warnings };\n\n // Find the semicolon after the createApp call\n const afterCall = content.slice(configCloseParen + 1);\n const semiMatch = afterCall.match(/^\\s*;/);\n const createAppEnd =\n configCloseParen + 1 + (semiMatch ? semiMatch[0].length : 0);\n\n // Find all uses of varName after the createApp call\n const afterCreateApp = content.slice(createAppEnd);\n const varUsagePattern = new RegExp(`\\\\b${varName}\\\\.(\\\\w+)`, \"g\");\n\n const usages: { plugin: string; index: number }[] = [];\n for (const usageMatch of afterCreateApp.matchAll(varUsagePattern)) {\n usages.push({ plugin: usageMatch[1], index: usageMatch.index });\n }\n\n // Check for non-server usage\n const nonServerUsage = usages.filter((u) => u.plugin !== \"server\");\n if (nonServerUsage.length > 0) {\n warnings.push(\n `Found additional usage of '${varName}' handle outside server.extend/start. Please migrate manually.`,\n );\n return { migrated: false, content, warnings };\n }\n\n // Find the extend call(s) and start call in the after-createApp region\n const extendPattern = new RegExp(\n `\\\\b${varName}\\\\.server\\\\.extend\\\\s*\\\\(`,\n \"g\",\n );\n const startPattern = new RegExp(\n `(?:await\\\\s+)?${varName}\\\\.server\\\\.start\\\\s*\\\\(\\\\s*\\\\)\\\\s*;`,\n );\n\n const extendMatches = [...afterCreateApp.matchAll(extendPattern)];\n if (extendMatches.length > 1) {\n warnings.push(\n `Found ${extendMatches.length} server.extend() calls. Please migrate manually.`,\n );\n return { migrated: false, content, warnings };\n }\n\n const extendExec = extendMatches[0] ?? null;\n const startExec = startPattern.exec(afterCreateApp);\n\n if (!startExec) return { migrated: false, content, warnings };\n\n // Extract the extend call's argument\n let extendArg = \"\";\n let extendFullStatement = \"\";\n if (extendExec) {\n const extendOpenParen =\n createAppEnd + extendExec.index + extendExec[0].length - 1;\n const extendCloseParen = findMatchingClose(content, extendOpenParen);\n if (extendCloseParen !== -1) {\n extendArg = content.slice(extendOpenParen + 1, extendCloseParen).trim();\n // Find the full statement including trailing semicolon\n const stmtStart = createAppEnd + extendExec.index;\n let stmtEnd = extendCloseParen + 1;\n const afterExtend = content.slice(stmtEnd);\n const trailingSemi = afterExtend.match(/^\\s*;/);\n if (trailingSemi) stmtEnd += trailingSemi[0].length;\n extendFullStatement = content.slice(stmtStart, stmtEnd);\n }\n }\n\n const startFullStatement = startExec[0];\n\n // Build the onPluginsReady callback\n const configStr = content.slice(configOpenParen + 1, configCloseParen);\n const lastBraceIdx = configStr.lastIndexOf(\"}\");\n if (lastBraceIdx === -1) return { migrated: false, content, warnings };\n\n const beforeLastBrace = configStr.slice(0, lastBraceIdx).trimEnd();\n const needsComma = beforeLastBrace.endsWith(\",\") ? \"\" : \",\";\n\n let onPluginsReadyProp: string;\n if (extendArg) {\n onPluginsReadyProp =\n `${needsComma}\\n onPluginsReady(${varName}) {\\n` +\n ` ${varName}.server.extend(${extendArg});\\n` +\n \" },\";\n } else {\n onPluginsReadyProp = \"\";\n }\n\n const newConfig = `${beforeLastBrace}${onPluginsReadyProp}\\n}`;\n const newCreateApp = `await createApp(${newConfig});`;\n\n // Replace: remove const declaration, replace with plain await, remove extend + start\n let result = content.slice(0, matchIdx) + newCreateApp;\n let remaining = afterCreateApp;\n\n if (extendFullStatement) {\n remaining = remaining.replace(extendFullStatement, \"\");\n }\n remaining = remaining.replace(startFullStatement, \"\");\n\n // Clean up consecutive blank lines\n remaining = remaining.replace(/\\n\\s*\\n\\s*\\n/g, \"\\n\\n\");\n\n result += remaining;\n\n return { migrated: true, content: result, warnings };\n}\n\nexport function migrateFile(filePath: string): MigrationResult {\n const original = fs.readFileSync(filePath, \"utf-8\");\n\n if (isAlreadyMigrated(original)) {\n return {\n migrated: false,\n content: original,\n warnings: [\"Already migrated -- no changes needed.\"],\n };\n }\n\n const content = stripAutoStartFromServerCalls(original);\n const allWarnings: string[] = [];\n\n // Try Pattern A first\n const patternA = migratePatternA(content);\n if (patternA.migrated) {\n allWarnings.push(...patternA.warnings);\n return {\n migrated: true,\n content: patternA.content,\n warnings: allWarnings,\n };\n }\n allWarnings.push(...patternA.warnings);\n\n // Try Pattern B\n const patternB = migratePatternB(content);\n if (patternB.migrated) {\n allWarnings.push(...patternB.warnings);\n return {\n migrated: true,\n content: patternB.content,\n warnings: allWarnings,\n };\n }\n allWarnings.push(...patternB.warnings);\n\n // Check if autoStart was stripped (content changed but no pattern matched)\n if (content !== original) {\n return { migrated: true, content, warnings: allWarnings };\n }\n\n return { migrated: false, content: original, warnings: allWarnings };\n}\n\nfunction runCodemod(options: { path?: string; write?: boolean }) {\n const rootDir = process.cwd();\n const write = options.write ?? false;\n\n let files: string[];\n if (options.path) {\n const absPath = path.resolve(rootDir, options.path);\n if (!fs.existsSync(absPath)) {\n console.error(`File not found: ${absPath}`);\n process.exit(1);\n }\n files = [absPath];\n } else {\n files = findServerEntryFiles(rootDir);\n }\n\n if (files.length === 0) {\n console.log(\"No files found importing createApp from @databricks/appkit.\");\n console.log(\"Use --path to specify a file explicitly.\");\n process.exit(0);\n }\n\n let hasChanges = false;\n\n for (const file of files) {\n const relPath = path.relative(rootDir, file);\n const result = migrateFile(file);\n\n for (const warning of result.warnings) {\n console.log(` ${relPath}: ${warning}`);\n }\n\n if (!result.migrated) {\n if (result.warnings.length === 0) {\n console.log(` ${relPath}: No migration needed.`);\n }\n continue;\n }\n\n hasChanges = true;\n\n if (write) {\n fs.writeFileSync(file, result.content, \"utf-8\");\n console.log(` ${relPath}: Migrated successfully.`);\n } else {\n console.log(`\\n--- ${relPath} (dry run) ---`);\n console.log(result.content);\n console.log(\"---\");\n }\n }\n\n if (hasChanges && !write) {\n console.log(\"\\nDry run complete. Run with --write to apply changes.\");\n }\n}\n\nexport const onPluginsReadyCommand = new Command(\"on-plugins-ready\")\n .description(\n \"Migrate createApp usage from autoStart/extend/start pattern to onPluginsReady callback\",\n )\n .option(\"--path <file>\", \"Path to the server entry file to migrate\")\n .option(\"--write\", \"Apply changes (default: dry-run)\", false)\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit codemod on-plugins-ready # dry-run, auto-detect files\n $ appkit codemod on-plugins-ready --write # apply changes\n $ appkit codemod on-plugins-ready --path server.ts # migrate a specific file`,\n )\n .action(runCodemod);\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { Command } from "commander";
|
|
4
|
-
import { Lang, parse } from "@ast-grep/napi";
|
|
5
5
|
|
|
6
6
|
//#region src/cli/commands/lint.ts
|
|
7
7
|
/** Value node of an object literal's `key: value` pair, or undefined. */
|
|
@@ -114,6 +114,7 @@ function findTsFiles(dir, files = []) {
|
|
|
114
114
|
* an in-memory string without touching disk.
|
|
115
115
|
*/
|
|
116
116
|
function lintSource(content, filePath, activeRules = rules, isTest = false) {
|
|
117
|
+
const { Lang, parse } = createRequire(import.meta.url)("@ast-grep/napi");
|
|
117
118
|
const violations = [];
|
|
118
119
|
const root = parse(filePath.endsWith(".tsx") ? Lang.Tsx : Lang.TypeScript, content).root();
|
|
119
120
|
for (const rule of activeRules) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lint.js","names":[],"sources":["../../../src/cli/commands/lint.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { Lang, parse, type SgNode } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\n\ninterface BaseRule {\n id: string;\n message: string;\n includeTests?: boolean;\n filter?: (code: string) => boolean;\n}\n\n/**\n * A rule matches via exactly one of `pattern` (an ast-grep pattern for\n * `root.findAll`) or `find` (an escape hatch for checks a single pattern can't\n * express). Discriminated union so a rule with neither or both fails to compile.\n */\ntype Rule =\n | (BaseRule & { pattern: string; find?: never })\n | (BaseRule & { find: (root: SgNode) => SgNode[]; pattern?: never });\n\n/** Value node of an object literal's `key: value` pair, or undefined. */\nfunction objectPropertyValue(obj: SgNode, key: string): SgNode | undefined {\n for (const pair of obj.children()) {\n if (pair.kind() !== \"pair\") continue;\n const k = pair.field(\"key\");\n // Strip quotes so `columns` and `\"columns\"` are treated the same.\n if (k && k.text().replace(/^[\"']|[\"']$/g, \"\") === key) {\n return pair.field(\"value\") ?? undefined;\n }\n }\n return undefined;\n}\n\n/** True for `[]` / `[ ]` — an array literal with no elements. */\nfunction isEmptyArrayLiteral(node: SgNode): boolean {\n return node.kind() === \"array\" && node.children().every((c) => !c.isNamed());\n}\n\n/**\n * Flags `aiSearch(...)` configs whose indexes lack usable `columns`. Production\n * does not auto-discover columns (dev-only), so a missing or empty `columns`\n * fails at query time. Passes anything it can't statically prove bad (constant\n * or dynamically-built columns/indexes) to avoid false positives.\n */\nfunction findAiSearchIndexesMissingColumns(root: SgNode): SgNode[] {\n const flagged: SgNode[] = [];\n\n for (const call of root.findAll(\"aiSearch($$$ARGS)\")) {\n const argNodes =\n call\n .field(\"arguments\")\n ?.children()\n .filter((c) => c.isNamed()) ?? [];\n\n // Bare aiSearch(): relies on the env default index (no columns).\n if (argNodes.length === 0) {\n flagged.push(call);\n continue;\n }\n\n // aiSearch(dynamicConfig) — not an object literal, can't inspect. Pass.\n const configObj = argNodes[0].kind() === \"object\" ? argNodes[0] : undefined;\n if (!configObj) continue;\n\n const indexes = objectPropertyValue(configObj, \"indexes\");\n\n // No `indexes` key => falls back to the env default index (no columns).\n if (!indexes) {\n flagged.push(call);\n continue;\n }\n if (indexes.kind() !== \"object\") continue; // dynamic indexes: pass.\n\n const indexPairs = indexes.children().filter((c) => c.kind() === \"pair\");\n\n // Empty `indexes: {}` => no configured index, same as bare.\n if (indexPairs.length === 0) {\n flagged.push(call);\n continue;\n }\n\n for (const pair of indexPairs) {\n const idxObj = pair.field(\"value\");\n if (!idxObj || idxObj.kind() !== \"object\") continue; // dynamic: pass.\n // A spread (`{ ...base }`) may carry `columns` we can't see: pass.\n if (idxObj.children().some((c) => c.kind() === \"spread_element\"))\n continue;\n const columns = objectPropertyValue(idxObj, \"columns\");\n if (!columns || isEmptyArrayLiteral(columns)) {\n flagged.push(pair); // points at the offending index alias.\n }\n }\n }\n\n return flagged;\n}\n\nconst rules: Rule[] = [\n {\n id: \"no-double-type-assertion\",\n pattern: \"$X as unknown as $Y\",\n message:\n \"Avoid double type assertion (as unknown as). Use proper type guards or fix the source type.\",\n },\n {\n id: \"no-as-any\",\n pattern: \"$X as any\",\n message:\n 'Avoid \"as any\" type assertion. Use proper typing or unknown with type guards.',\n includeTests: false, // acceptable in test mocks\n },\n {\n id: \"no-array-index-key\",\n pattern: \"key={$IDX}\",\n message:\n \"Avoid using array index as React key. Use a stable unique identifier.\",\n filter: (code) => /key=\\{(idx|index|i)\\}/.test(code),\n },\n {\n id: \"no-parse-float-without-validation\",\n pattern: \"parseFloat($X).toFixed($Y)\",\n message:\n \"parseFloat can return NaN. Validate input or use toNumber() helper from shared/types.ts.\",\n },\n {\n // <Variants> is the dev-only variant picker from @databricks/appkit-ui,\n // meant to drive the local edit loop. Finalize the chosen variant before\n // deploying so the picker chrome never reaches production.\n id: \"no-variants-in-prod\",\n pattern: \"<Variants $$$P>$$$C</Variants>\",\n message:\n \"<Variants> is a development-only variant picker and must not be shipped. Finalize the chosen <Variant> before deploying.\",\n includeTests: false,\n },\n {\n id: \"ai-search-index-requires-columns\",\n message:\n \"AI Search index has no `columns`; it will fail in production (columns are auto-discovered only in dev). Set `columns` explicitly on each index.\",\n includeTests: false,\n find: findAiSearchIndexesMissingColumns,\n },\n];\n\nfunction isTestFile(filePath: string, rootDir: string): boolean {\n // Relative to scan root: an ancestor `tests` dir must not mark the whole project as tests.\n const rel = path.relative(rootDir, filePath);\n return (\n /\\.(test|spec)\\.(ts|tsx)$/.test(rel) || /(^|[/\\\\])tests[/\\\\]/.test(rel)\n );\n}\n\nfunction findTsFiles(dir: string, files: string[] = []): string[] {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n if ([\"node_modules\", \"dist\", \"build\", \".git\"].includes(entry.name))\n continue;\n findTsFiles(fullPath, files);\n } else if (entry.isFile() && /\\.(ts|tsx)$/.test(entry.name)) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\ninterface Violation {\n file: string;\n line: number;\n column: number;\n rule: string;\n message: string;\n code: string;\n}\n\n/**\n * Runs all applicable rules against a file's source. Exported so tests can lint\n * an in-memory string without touching disk.\n */\nexport function lintSource(\n content: string,\n filePath: string,\n activeRules: Rule[] = rules,\n isTest = false,\n): Violation[] {\n const violations: Violation[] = [];\n const lang = filePath.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const root = parse(lang, content).root();\n\n for (const rule of activeRules) {\n // skip rules that don't apply to test files\n if (isTest && rule.includeTests === false) continue;\n\n const matches = rule.find ? rule.find(root) : root.findAll(rule.pattern);\n\n for (const match of matches) {\n const code = match.text();\n\n if (rule.filter && !rule.filter(code)) continue;\n\n const range = match.range();\n violations.push({\n file: filePath,\n line: range.start.line + 1,\n column: range.start.column + 1,\n rule: rule.id,\n message: rule.message,\n code: code.length > 80 ? `${code.slice(0, 77)}...` : code,\n });\n }\n }\n\n return violations;\n}\n\nfunction lintFile(\n filePath: string,\n activeRules: Rule[],\n rootDir: string,\n): Violation[] {\n const content = fs.readFileSync(filePath, \"utf-8\");\n return lintSource(\n content,\n filePath,\n activeRules,\n isTestFile(filePath, rootDir),\n );\n}\n\n/**\n * Lint command implementation\n */\nfunction runLint() {\n const rootDir = process.cwd();\n const files = findTsFiles(rootDir);\n\n console.log(`Scanning ${files.length} TypeScript files...\\n`);\n\n const allViolations: Violation[] = [];\n\n for (const file of files) {\n const violations = lintFile(file, rules, rootDir);\n allViolations.push(...violations);\n }\n\n if (allViolations.length === 0) {\n console.log(\"No ast-grep lint violations found.\");\n process.exit(0);\n }\n\n console.log(`Found ${allViolations.length} violation(s):\\n`);\n\n for (const v of allViolations) {\n const relPath = path.relative(rootDir, v.file);\n console.log(`${relPath}:${v.line}:${v.column}`);\n console.log(` ${v.rule}: ${v.message}`);\n console.log(` > ${v.code}\\n`);\n }\n\n process.exit(1);\n}\n\nexport const lintCommand = new Command(\"lint\")\n .description(\"Run AST-based linting on TypeScript files\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit lint`,\n )\n .action(runLint);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lint.js","names":[],"sources":["../../../src/cli/commands/lint.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport path from \"node:path\";\n\nimport type { SgNode } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\n\ninterface BaseRule {\n id: string;\n message: string;\n includeTests?: boolean;\n filter?: (code: string) => boolean;\n}\n\n/**\n * A rule matches via exactly one of `pattern` (an ast-grep pattern for\n * `root.findAll`) or `find` (an escape hatch for checks a single pattern can't\n * express). Discriminated union so a rule with neither or both fails to compile.\n */\ntype Rule =\n | (BaseRule & { pattern: string; find?: never })\n | (BaseRule & { find: (root: SgNode) => SgNode[]; pattern?: never });\n\n/** Value node of an object literal's `key: value` pair, or undefined. */\nfunction objectPropertyValue(obj: SgNode, key: string): SgNode | undefined {\n for (const pair of obj.children()) {\n if (pair.kind() !== \"pair\") continue;\n const k = pair.field(\"key\");\n // Strip quotes so `columns` and `\"columns\"` are treated the same.\n if (k && k.text().replace(/^[\"']|[\"']$/g, \"\") === key) {\n return pair.field(\"value\") ?? undefined;\n }\n }\n return undefined;\n}\n\n/** True for `[]` / `[ ]` — an array literal with no elements. */\nfunction isEmptyArrayLiteral(node: SgNode): boolean {\n return node.kind() === \"array\" && node.children().every((c) => !c.isNamed());\n}\n\n/**\n * Flags `aiSearch(...)` configs whose indexes lack usable `columns`. Production\n * does not auto-discover columns (dev-only), so a missing or empty `columns`\n * fails at query time. Passes anything it can't statically prove bad (constant\n * or dynamically-built columns/indexes) to avoid false positives.\n */\nfunction findAiSearchIndexesMissingColumns(root: SgNode): SgNode[] {\n const flagged: SgNode[] = [];\n\n for (const call of root.findAll(\"aiSearch($$$ARGS)\")) {\n const argNodes =\n call\n .field(\"arguments\")\n ?.children()\n .filter((c) => c.isNamed()) ?? [];\n\n // Bare aiSearch(): relies on the env default index (no columns).\n if (argNodes.length === 0) {\n flagged.push(call);\n continue;\n }\n\n // aiSearch(dynamicConfig) — not an object literal, can't inspect. Pass.\n const configObj = argNodes[0].kind() === \"object\" ? argNodes[0] : undefined;\n if (!configObj) continue;\n\n const indexes = objectPropertyValue(configObj, \"indexes\");\n\n // No `indexes` key => falls back to the env default index (no columns).\n if (!indexes) {\n flagged.push(call);\n continue;\n }\n if (indexes.kind() !== \"object\") continue; // dynamic indexes: pass.\n\n const indexPairs = indexes.children().filter((c) => c.kind() === \"pair\");\n\n // Empty `indexes: {}` => no configured index, same as bare.\n if (indexPairs.length === 0) {\n flagged.push(call);\n continue;\n }\n\n for (const pair of indexPairs) {\n const idxObj = pair.field(\"value\");\n if (!idxObj || idxObj.kind() !== \"object\") continue; // dynamic: pass.\n // A spread (`{ ...base }`) may carry `columns` we can't see: pass.\n if (idxObj.children().some((c) => c.kind() === \"spread_element\"))\n continue;\n const columns = objectPropertyValue(idxObj, \"columns\");\n if (!columns || isEmptyArrayLiteral(columns)) {\n flagged.push(pair); // points at the offending index alias.\n }\n }\n }\n\n return flagged;\n}\n\nconst rules: Rule[] = [\n {\n id: \"no-double-type-assertion\",\n pattern: \"$X as unknown as $Y\",\n message:\n \"Avoid double type assertion (as unknown as). Use proper type guards or fix the source type.\",\n },\n {\n id: \"no-as-any\",\n pattern: \"$X as any\",\n message:\n 'Avoid \"as any\" type assertion. Use proper typing or unknown with type guards.',\n includeTests: false, // acceptable in test mocks\n },\n {\n id: \"no-array-index-key\",\n pattern: \"key={$IDX}\",\n message:\n \"Avoid using array index as React key. Use a stable unique identifier.\",\n filter: (code) => /key=\\{(idx|index|i)\\}/.test(code),\n },\n {\n id: \"no-parse-float-without-validation\",\n pattern: \"parseFloat($X).toFixed($Y)\",\n message:\n \"parseFloat can return NaN. Validate input or use toNumber() helper from shared/types.ts.\",\n },\n {\n // <Variants> is the dev-only variant picker from @databricks/appkit-ui,\n // meant to drive the local edit loop. Finalize the chosen variant before\n // deploying so the picker chrome never reaches production.\n id: \"no-variants-in-prod\",\n pattern: \"<Variants $$$P>$$$C</Variants>\",\n message:\n \"<Variants> is a development-only variant picker and must not be shipped. Finalize the chosen <Variant> before deploying.\",\n includeTests: false,\n },\n {\n id: \"ai-search-index-requires-columns\",\n message:\n \"AI Search index has no `columns`; it will fail in production (columns are auto-discovered only in dev). Set `columns` explicitly on each index.\",\n includeTests: false,\n find: findAiSearchIndexesMissingColumns,\n },\n];\n\nfunction isTestFile(filePath: string, rootDir: string): boolean {\n // Relative to scan root: an ancestor `tests` dir must not mark the whole project as tests.\n const rel = path.relative(rootDir, filePath);\n return (\n /\\.(test|spec)\\.(ts|tsx)$/.test(rel) || /(^|[/\\\\])tests[/\\\\]/.test(rel)\n );\n}\n\nfunction findTsFiles(dir: string, files: string[] = []): string[] {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n if ([\"node_modules\", \"dist\", \"build\", \".git\"].includes(entry.name))\n continue;\n findTsFiles(fullPath, files);\n } else if (entry.isFile() && /\\.(ts|tsx)$/.test(entry.name)) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\ninterface Violation {\n file: string;\n line: number;\n column: number;\n rule: string;\n message: string;\n code: string;\n}\n\n/**\n * Runs all applicable rules against a file's source. Exported so tests can lint\n * an in-memory string without touching disk.\n */\nexport function lintSource(\n content: string,\n filePath: string,\n activeRules: Rule[] = rules,\n isTest = false,\n): Violation[] {\n const require = createRequire(import.meta.url);\n const { Lang, parse } =\n require(\"@ast-grep/napi\") as typeof import(\"@ast-grep/napi\");\n const violations: Violation[] = [];\n const lang = filePath.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const root = parse(lang, content).root();\n\n for (const rule of activeRules) {\n // skip rules that don't apply to test files\n if (isTest && rule.includeTests === false) continue;\n\n const matches = rule.find ? rule.find(root) : root.findAll(rule.pattern);\n\n for (const match of matches) {\n const code = match.text();\n\n if (rule.filter && !rule.filter(code)) continue;\n\n const range = match.range();\n violations.push({\n file: filePath,\n line: range.start.line + 1,\n column: range.start.column + 1,\n rule: rule.id,\n message: rule.message,\n code: code.length > 80 ? `${code.slice(0, 77)}...` : code,\n });\n }\n }\n\n return violations;\n}\n\nfunction lintFile(\n filePath: string,\n activeRules: Rule[],\n rootDir: string,\n): Violation[] {\n const content = fs.readFileSync(filePath, \"utf-8\");\n return lintSource(\n content,\n filePath,\n activeRules,\n isTestFile(filePath, rootDir),\n );\n}\n\n/**\n * Lint command implementation\n */\nfunction runLint() {\n const rootDir = process.cwd();\n const files = findTsFiles(rootDir);\n\n console.log(`Scanning ${files.length} TypeScript files...\\n`);\n\n const allViolations: Violation[] = [];\n\n for (const file of files) {\n const violations = lintFile(file, rules, rootDir);\n allViolations.push(...violations);\n }\n\n if (allViolations.length === 0) {\n console.log(\"No ast-grep lint violations found.\");\n process.exit(0);\n }\n\n console.log(`Found ${allViolations.length} violation(s):\\n`);\n\n for (const v of allViolations) {\n const relPath = path.relative(rootDir, v.file);\n console.log(`${relPath}:${v.line}:${v.column}`);\n console.log(` ${v.rule}: ${v.message}`);\n console.log(` > ${v.code}\\n`);\n }\n\n process.exit(1);\n}\n\nexport const lintCommand = new Command(\"lint\")\n .description(\"Run AST-based linting on TypeScript files\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit lint`,\n )\n .action(runLint);\n"],"mappings":"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"}
|
|
@@ -6,7 +6,6 @@ import fs from "node:fs";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import process from "node:process";
|
|
9
|
-
import { cancel, intro, outro } from "@clack/prompts";
|
|
10
9
|
|
|
11
10
|
//#region src/cli/commands/plugin/add-resource/add-resource.ts
|
|
12
11
|
function loadManifest(pluginDir) {
|
|
@@ -88,6 +87,7 @@ function runNonInteractive(opts) {
|
|
|
88
87
|
console.log(`Added ${entry.alias} as ${isRequired ? "required" : "optional"} to ${path.relative(cwd, manifestPath)}`);
|
|
89
88
|
}
|
|
90
89
|
async function runInteractive(opts) {
|
|
90
|
+
const { cancel, intro, outro } = await import("@clack/prompts");
|
|
91
91
|
intro("Add resource to plugin manifest");
|
|
92
92
|
const cwd = process.cwd();
|
|
93
93
|
const loaded = loadManifest(path.resolve(cwd, opts.path ?? "."));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add-resource.js","names":[],"sources":["../../../../../src/cli/commands/plugin/add-resource/add-resource.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\n\nimport { cancel, intro, outro } from \"@clack/prompts\";\nimport { Command } from \"commander\";\n\nimport { promptOneResource } from \"../create/prompt-resource\";\nimport {\n DEFAULT_PERMISSION_BY_TYPE,\n getDefaultFieldsForType,\n getValidResourceTypes,\n humanizeResourceType,\n resourceKeyFromType,\n} from \"../create/resource-defaults\";\nimport { resolveManifestInDir } from \"../manifest-resolve\";\nimport type { PluginManifest, ResourceRequirement } from \"../manifest-types\";\nimport { validateManifest } from \"../validate/validate-manifest\";\n\n/** Extended manifest type that preserves extra JSON fields (e.g. $schema, author, version) for round-trip writes. */\ninterface ManifestWithExtras extends PluginManifest {\n [key: string]: unknown;\n}\n\ninterface AddResourceOptions {\n path?: string;\n type?: string;\n required?: boolean;\n resourceKey?: string;\n description?: string;\n permission?: string;\n fieldsJson?: string;\n dryRun?: boolean;\n}\n\nfunction loadManifest(\n pluginDir: string,\n): { manifest: ManifestWithExtras; manifestPath: string } | null {\n const resolved = resolveManifestInDir(pluginDir, { allowJsManifest: true });\n\n if (!resolved) {\n console.error(\n `No manifest found in ${pluginDir}. This command requires manifest.json (manifest.js cannot be edited in place).`,\n );\n console.error(\n \" appkit plugin add-resource --path <dir-with-manifest.json>\",\n );\n process.exit(1);\n }\n\n if (resolved.type !== \"json\") {\n console.error(\n `Editable manifest not found. add-resource only supports plugin directories that contain manifest.json (found ${path.basename(resolved.path)}).`,\n );\n process.exit(1);\n }\n\n const manifestPath = resolved.path;\n\n try {\n const raw = fs.readFileSync(manifestPath, \"utf-8\");\n const parsed = JSON.parse(raw) as unknown;\n const result = validateManifest(parsed);\n if (!result.valid || !result.manifest) {\n console.error(\n \"Invalid manifest. Run `appkit plugin validate` for details.\",\n );\n process.exit(1);\n }\n return { manifest: parsed as ManifestWithExtras, manifestPath };\n } catch (err) {\n console.error(\n \"Failed to read or parse manifest.json:\",\n err instanceof Error ? err.message : err,\n );\n process.exit(1);\n }\n}\n\nfunction buildEntry(\n type: string,\n opts: AddResourceOptions,\n): { entry: ResourceRequirement; isRequired: boolean } {\n const alias = humanizeResourceType(type);\n const isRequired = opts.required !== false;\n\n let fields = getDefaultFieldsForType(type);\n if (opts.fieldsJson) {\n try {\n const parsed = JSON.parse(opts.fieldsJson) as Record<\n string,\n { env: string; description?: string }\n >;\n fields = { ...fields, ...parsed };\n } catch {\n console.error(\"Error: --fields-json must be valid JSON.\");\n console.error(\n ' Example: --fields-json \\'{\"id\":{\"env\":\"MY_WAREHOUSE_ID\"}}\\'',\n );\n process.exit(1);\n }\n }\n\n // Constructed dynamically from a string `type` and string `permission`;\n // the per-type permission tightness from the discriminated union is\n // recovered at runtime by `validateManifest` before the entry is written.\n const entry = {\n type,\n alias,\n resourceKey: opts.resourceKey ?? resourceKeyFromType(type),\n description:\n opts.description ||\n `${isRequired ? \"Required\" : \"Optional\"} for ${alias} functionality.`,\n permission:\n opts.permission ?? DEFAULT_PERMISSION_BY_TYPE[type] ?? \"CAN_VIEW\",\n fields,\n } as ResourceRequirement;\n\n return { entry, isRequired };\n}\n\nfunction runNonInteractive(opts: AddResourceOptions): void {\n const cwd = process.cwd();\n const pluginDir = path.resolve(cwd, opts.path ?? \".\");\n const loaded = loadManifest(pluginDir);\n if (!loaded) return;\n const { manifest, manifestPath } = loaded;\n\n const type = opts.type as string;\n const validTypes = getValidResourceTypes();\n if (!validTypes.includes(type)) {\n console.error(`Error: Unknown resource type \"${type}\".`);\n console.error(` Valid types: ${validTypes.join(\", \")}`);\n process.exit(1);\n }\n const { entry, isRequired } = buildEntry(type, opts);\n\n if (isRequired) {\n manifest.resources.required.push(entry);\n } else {\n manifest.resources.optional.push(entry);\n }\n\n if (opts.dryRun) {\n console.log(JSON.stringify(manifest, null, 2));\n return;\n }\n\n fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\\n`);\n console.log(\n `Added ${entry.alias} as ${isRequired ? \"required\" : \"optional\"} to ${path.relative(cwd, manifestPath)}`,\n );\n}\n\nasync function runInteractive(opts: AddResourceOptions): Promise<void> {\n intro(\"Add resource to plugin manifest\");\n\n const cwd = process.cwd();\n const pluginDir = path.resolve(cwd, opts.path ?? \".\");\n const loaded = loadManifest(pluginDir);\n if (!loaded) return;\n const { manifest, manifestPath } = loaded;\n\n const spec = await promptOneResource();\n if (!spec) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const alias = humanizeResourceType(spec.type);\n // See note on the non-interactive variant in `buildEntry` — the entry is\n // assembled from prompt strings and validated by Zod at write time.\n const entry = {\n type: spec.type,\n alias,\n resourceKey: spec.resourceKey,\n description: spec.description || `Required for ${alias} functionality.`,\n permission: spec.permission,\n fields: spec.fields,\n } as ResourceRequirement;\n\n if (spec.required) {\n manifest.resources.required.push(entry);\n } else {\n manifest.resources.optional.push(entry);\n }\n\n fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\\n`);\n\n outro(\"Resource added.\");\n console.log(\n `\\nAdded ${alias} as ${spec.required ? \"required\" : \"optional\"} to ${path.relative(cwd, manifestPath)}`,\n );\n}\n\nasync function runPluginAddResource(opts: AddResourceOptions): Promise<void> {\n if (opts.type) {\n runNonInteractive(opts);\n } else {\n await runInteractive(opts);\n }\n}\n\nexport const pluginAddResourceCommand = new Command(\"add-resource\")\n .description(\n \"Add a resource requirement to an existing plugin manifest. Overwrites manifest.json in place.\",\n )\n .option(\n \"-p, --path <dir>\",\n \"Plugin directory containing manifest.json (default: .)\",\n )\n .option(\n \"-t, --type <resource_type>\",\n \"Resource type (e.g. sql_warehouse, volume). Enables non-interactive mode.\",\n )\n .option(\"--required\", \"Mark resource as required (default: true)\", true)\n .option(\"--no-required\", \"Mark resource as optional\")\n .option(\"--resource-key <key>\", \"Resource key (default: derived from type)\")\n .option(\"--description <text>\", \"Description of the resource requirement\")\n .option(\"--permission <perm>\", \"Permission level (default: from schema)\")\n .option(\n \"--fields-json <json>\",\n 'JSON object overriding field env vars (e.g. \\'{\"id\":{\"env\":\"MY_WAREHOUSE_ID\"}}\\')',\n )\n .option(\"--dry-run\", \"Preview the updated manifest without writing\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit plugin add-resource\n $ appkit plugin add-resource --path plugins/my-plugin --type sql_warehouse\n $ appkit plugin add-resource --path plugins/my-plugin --type volume --no-required --dry-run\n $ appkit plugin add-resource --type sql_warehouse --fields-json '{\"id\":{\"env\":\"MY_WAREHOUSE_ID\"}}'`,\n )\n .action((opts) =>\n runPluginAddResource(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"add-resource.js","names":[],"sources":["../../../../../src/cli/commands/plugin/add-resource/add-resource.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\n\nimport { Command } from \"commander\";\n\nimport { promptOneResource } from \"../create/prompt-resource\";\nimport {\n DEFAULT_PERMISSION_BY_TYPE,\n getDefaultFieldsForType,\n getValidResourceTypes,\n humanizeResourceType,\n resourceKeyFromType,\n} from \"../create/resource-defaults\";\nimport { resolveManifestInDir } from \"../manifest-resolve\";\nimport type { PluginManifest, ResourceRequirement } from \"../manifest-types\";\nimport { validateManifest } from \"../validate/validate-manifest\";\n\n/** Extended manifest type that preserves extra JSON fields (e.g. $schema, author, version) for round-trip writes. */\ninterface ManifestWithExtras extends PluginManifest {\n [key: string]: unknown;\n}\n\ninterface AddResourceOptions {\n path?: string;\n type?: string;\n required?: boolean;\n resourceKey?: string;\n description?: string;\n permission?: string;\n fieldsJson?: string;\n dryRun?: boolean;\n}\n\nfunction loadManifest(\n pluginDir: string,\n): { manifest: ManifestWithExtras; manifestPath: string } | null {\n const resolved = resolveManifestInDir(pluginDir, { allowJsManifest: true });\n\n if (!resolved) {\n console.error(\n `No manifest found in ${pluginDir}. This command requires manifest.json (manifest.js cannot be edited in place).`,\n );\n console.error(\n \" appkit plugin add-resource --path <dir-with-manifest.json>\",\n );\n process.exit(1);\n }\n\n if (resolved.type !== \"json\") {\n console.error(\n `Editable manifest not found. add-resource only supports plugin directories that contain manifest.json (found ${path.basename(resolved.path)}).`,\n );\n process.exit(1);\n }\n\n const manifestPath = resolved.path;\n\n try {\n const raw = fs.readFileSync(manifestPath, \"utf-8\");\n const parsed = JSON.parse(raw) as unknown;\n const result = validateManifest(parsed);\n if (!result.valid || !result.manifest) {\n console.error(\n \"Invalid manifest. Run `appkit plugin validate` for details.\",\n );\n process.exit(1);\n }\n return { manifest: parsed as ManifestWithExtras, manifestPath };\n } catch (err) {\n console.error(\n \"Failed to read or parse manifest.json:\",\n err instanceof Error ? err.message : err,\n );\n process.exit(1);\n }\n}\n\nfunction buildEntry(\n type: string,\n opts: AddResourceOptions,\n): { entry: ResourceRequirement; isRequired: boolean } {\n const alias = humanizeResourceType(type);\n const isRequired = opts.required !== false;\n\n let fields = getDefaultFieldsForType(type);\n if (opts.fieldsJson) {\n try {\n const parsed = JSON.parse(opts.fieldsJson) as Record<\n string,\n { env: string; description?: string }\n >;\n fields = { ...fields, ...parsed };\n } catch {\n console.error(\"Error: --fields-json must be valid JSON.\");\n console.error(\n ' Example: --fields-json \\'{\"id\":{\"env\":\"MY_WAREHOUSE_ID\"}}\\'',\n );\n process.exit(1);\n }\n }\n\n // Constructed dynamically from a string `type` and string `permission`;\n // the per-type permission tightness from the discriminated union is\n // recovered at runtime by `validateManifest` before the entry is written.\n const entry = {\n type,\n alias,\n resourceKey: opts.resourceKey ?? resourceKeyFromType(type),\n description:\n opts.description ||\n `${isRequired ? \"Required\" : \"Optional\"} for ${alias} functionality.`,\n permission:\n opts.permission ?? DEFAULT_PERMISSION_BY_TYPE[type] ?? \"CAN_VIEW\",\n fields,\n } as ResourceRequirement;\n\n return { entry, isRequired };\n}\n\nfunction runNonInteractive(opts: AddResourceOptions): void {\n const cwd = process.cwd();\n const pluginDir = path.resolve(cwd, opts.path ?? \".\");\n const loaded = loadManifest(pluginDir);\n if (!loaded) return;\n const { manifest, manifestPath } = loaded;\n\n const type = opts.type as string;\n const validTypes = getValidResourceTypes();\n if (!validTypes.includes(type)) {\n console.error(`Error: Unknown resource type \"${type}\".`);\n console.error(` Valid types: ${validTypes.join(\", \")}`);\n process.exit(1);\n }\n const { entry, isRequired } = buildEntry(type, opts);\n\n if (isRequired) {\n manifest.resources.required.push(entry);\n } else {\n manifest.resources.optional.push(entry);\n }\n\n if (opts.dryRun) {\n console.log(JSON.stringify(manifest, null, 2));\n return;\n }\n\n fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\\n`);\n console.log(\n `Added ${entry.alias} as ${isRequired ? \"required\" : \"optional\"} to ${path.relative(cwd, manifestPath)}`,\n );\n}\n\nasync function runInteractive(opts: AddResourceOptions): Promise<void> {\n const { cancel, intro, outro } = await import(\"@clack/prompts\");\n intro(\"Add resource to plugin manifest\");\n\n const cwd = process.cwd();\n const pluginDir = path.resolve(cwd, opts.path ?? \".\");\n const loaded = loadManifest(pluginDir);\n if (!loaded) return;\n const { manifest, manifestPath } = loaded;\n\n const spec = await promptOneResource();\n if (!spec) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const alias = humanizeResourceType(spec.type);\n // See note on the non-interactive variant in `buildEntry` — the entry is\n // assembled from prompt strings and validated by Zod at write time.\n const entry = {\n type: spec.type,\n alias,\n resourceKey: spec.resourceKey,\n description: spec.description || `Required for ${alias} functionality.`,\n permission: spec.permission,\n fields: spec.fields,\n } as ResourceRequirement;\n\n if (spec.required) {\n manifest.resources.required.push(entry);\n } else {\n manifest.resources.optional.push(entry);\n }\n\n fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\\n`);\n\n outro(\"Resource added.\");\n console.log(\n `\\nAdded ${alias} as ${spec.required ? \"required\" : \"optional\"} to ${path.relative(cwd, manifestPath)}`,\n );\n}\n\nasync function runPluginAddResource(opts: AddResourceOptions): Promise<void> {\n if (opts.type) {\n runNonInteractive(opts);\n } else {\n await runInteractive(opts);\n }\n}\n\nexport const pluginAddResourceCommand = new Command(\"add-resource\")\n .description(\n \"Add a resource requirement to an existing plugin manifest. Overwrites manifest.json in place.\",\n )\n .option(\n \"-p, --path <dir>\",\n \"Plugin directory containing manifest.json (default: .)\",\n )\n .option(\n \"-t, --type <resource_type>\",\n \"Resource type (e.g. sql_warehouse, volume). Enables non-interactive mode.\",\n )\n .option(\"--required\", \"Mark resource as required (default: true)\", true)\n .option(\"--no-required\", \"Mark resource as optional\")\n .option(\"--resource-key <key>\", \"Resource key (default: derived from type)\")\n .option(\"--description <text>\", \"Description of the resource requirement\")\n .option(\"--permission <perm>\", \"Permission level (default: from schema)\")\n .option(\n \"--fields-json <json>\",\n 'JSON object overriding field env vars (e.g. \\'{\"id\":{\"env\":\"MY_WAREHOUSE_ID\"}}\\')',\n )\n .option(\"--dry-run\", \"Preview the updated manifest without writing\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit plugin add-resource\n $ appkit plugin add-resource --path plugins/my-plugin --type sql_warehouse\n $ appkit plugin add-resource --path plugins/my-plugin --type volume --no-required --dry-run\n $ appkit plugin add-resource --type sql_warehouse --fields-json '{\"id\":{\"env\":\"MY_WAREHOUSE_ID\"}}'`,\n )\n .action((opts) =>\n runPluginAddResource(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}
|
|
@@ -6,7 +6,6 @@ import fs from "node:fs";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { Command, Option } from "commander";
|
|
8
8
|
import process from "node:process";
|
|
9
|
-
import { cancel, confirm, intro, isCancel, multiselect, outro, select, spinner, text } from "@clack/prompts";
|
|
10
9
|
|
|
11
10
|
//#region src/cli/commands/plugin/create/create.ts
|
|
12
11
|
const DEFAULT_VERSION = "0.1.0";
|
|
@@ -143,6 +142,7 @@ function runNonInteractive(opts) {
|
|
|
143
142
|
printNextSteps(answers, targetDir);
|
|
144
143
|
}
|
|
145
144
|
async function runInteractive() {
|
|
145
|
+
const { cancel, confirm, intro, isCancel, multiselect, outro, select, spinner, text } = await import("@clack/prompts");
|
|
146
146
|
intro("Create a new AppKit plugin");
|
|
147
147
|
try {
|
|
148
148
|
const placement = await select({
|