@databricks/appkit 0.53.1 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CLAUDE.md +9 -1
  2. package/dist/appkit/package.js +1 -1
  3. package/dist/beta.d.ts +3 -1
  4. package/dist/beta.js +2 -1
  5. package/dist/cli/commands/generate-types.js +1 -1
  6. package/dist/cli/commands/generate-types.js.map +1 -1
  7. package/dist/cli/commands/plugin/create/create.js +12 -14
  8. package/dist/cli/commands/plugin/create/create.js.map +1 -1
  9. package/dist/cli/commands/plugin/promote/promote.js +2 -12
  10. package/dist/cli/commands/plugin/promote/promote.js.map +1 -1
  11. package/dist/connectors/ai-search/client.js +145 -0
  12. package/dist/connectors/ai-search/client.js.map +1 -0
  13. package/dist/connectors/ai-search/index.js +3 -0
  14. package/dist/connectors/context.js +38 -0
  15. package/dist/connectors/context.js.map +1 -0
  16. package/dist/connectors/index.js +2 -1
  17. package/dist/connectors/serving/client.d.ts.map +1 -1
  18. package/dist/connectors/serving/client.js +2 -26
  19. package/dist/connectors/serving/client.js.map +1 -1
  20. package/dist/core/appkit.d.ts.map +1 -1
  21. package/dist/core/appkit.js.map +1 -1
  22. package/dist/naming.js +29 -0
  23. package/dist/plugin/plugin.d.ts.map +1 -1
  24. package/dist/plugin/plugin.js +2 -1
  25. package/dist/plugin/plugin.js.map +1 -1
  26. package/dist/plugins/ai-search/ai-search.d.ts +63 -0
  27. package/dist/plugins/ai-search/ai-search.d.ts.map +1 -0
  28. package/dist/plugins/ai-search/ai-search.js +296 -0
  29. package/dist/plugins/ai-search/ai-search.js.map +1 -0
  30. package/dist/plugins/ai-search/defaults.js +14 -0
  31. package/dist/plugins/ai-search/defaults.js.map +1 -0
  32. package/dist/plugins/ai-search/index.d.ts +2 -0
  33. package/dist/plugins/ai-search/index.js +3 -0
  34. package/dist/plugins/ai-search/manifest.js +73 -0
  35. package/dist/plugins/ai-search/manifest.js.map +1 -0
  36. package/dist/plugins/ai-search/types.d.ts +80 -0
  37. package/dist/plugins/ai-search/types.d.ts.map +1 -0
  38. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  39. package/dist/plugins/analytics/analytics.js +7 -1
  40. package/dist/plugins/analytics/analytics.js.map +1 -1
  41. package/dist/plugins/analytics/metric.js +1 -0
  42. package/dist/plugins/analytics/mv/index.js +1 -0
  43. package/dist/plugins/analytics/mv/metadata.js +30 -0
  44. package/dist/plugins/analytics/mv/metadata.js.map +1 -0
  45. package/dist/plugins/analytics/types.d.ts +7 -0
  46. package/dist/plugins/analytics/types.d.ts.map +1 -1
  47. package/dist/plugins/analytics/types.js.map +1 -1
  48. package/dist/plugins/beta-exports.generated.d.ts +3 -1
  49. package/dist/plugins/beta-exports.generated.js +2 -0
  50. package/dist/plugins/server/index.d.ts.map +1 -1
  51. package/dist/plugins/server/index.js +2 -1
  52. package/dist/plugins/server/index.js.map +1 -1
  53. package/dist/plugins/ui-variants/index.js.map +1 -1
  54. package/dist/plugins/ui-variants/manifest.js +1 -1
  55. package/dist/registry/resource-registry.d.ts.map +1 -1
  56. package/dist/registry/resource-registry.js +2 -7
  57. package/dist/registry/resource-registry.js.map +1 -1
  58. package/dist/schemas/manifest.d.ts.map +1 -1
  59. package/dist/schemas/manifest.js +3 -2
  60. package/dist/schemas/manifest.js.map +1 -1
  61. package/dist/shared/src/index.d.ts +1 -0
  62. package/dist/shared/src/metric-metadata.d.ts +24 -0
  63. package/dist/shared/src/metric-metadata.d.ts.map +1 -0
  64. package/dist/shared/src/naming.js +13 -0
  65. package/dist/shared/src/naming.js.map +1 -0
  66. package/dist/shared/src/plugin.d.ts +6 -4
  67. package/dist/shared/src/plugin.d.ts.map +1 -1
  68. package/dist/shared/src/schemas/manifest.d.ts +2 -2
  69. package/dist/shared/src/schemas/manifest.d.ts.map +1 -1
  70. package/dist/shared/src/sse/analytics.js +2 -1
  71. package/dist/shared/src/sse/analytics.js.map +1 -1
  72. package/dist/type-generator/errors.js +0 -3
  73. package/dist/type-generator/errors.js.map +1 -1
  74. package/dist/type-generator/index.js +15 -10
  75. package/dist/type-generator/index.js.map +1 -1
  76. package/dist/type-generator/mv-registry/render-types.js +49 -11
  77. package/dist/type-generator/mv-registry/render-types.js.map +1 -1
  78. package/dist/type-generator/query-registry.js +1 -3
  79. package/dist/type-generator/query-registry.js.map +1 -1
  80. package/dist/type-generator/vite-plugin.d.ts +4 -2
  81. package/dist/type-generator/vite-plugin.d.ts.map +1 -1
  82. package/dist/type-generator/vite-plugin.js +1 -0
  83. package/dist/type-generator/vite-plugin.js.map +1 -1
  84. package/dist/utils/banner.js +19 -0
  85. package/dist/utils/banner.js.map +1 -0
  86. package/docs/api/appkit/Interface.BasePluginConfig.md +1 -0
  87. package/docs/api/appkit/Interface.IAiSearchConfig.md +71 -0
  88. package/docs/api/appkit/Interface.IndexConfig.md +110 -0
  89. package/docs/api/appkit/Interface.RerankerConfig.md +10 -0
  90. package/docs/api/appkit/Interface.SearchRequest.md +64 -0
  91. package/docs/api/appkit/Interface.SearchResponse.md +52 -0
  92. package/docs/api/appkit/Interface.SearchResult.md +25 -0
  93. package/docs/api/appkit/TypeAlias.SearchFilters.md +6 -0
  94. package/docs/api/appkit/Variable.aiSearch.md +6 -0
  95. package/docs/api/appkit.md +8 -0
  96. package/docs/development/type-generation.md +7 -7
  97. package/docs/plugins/{vector-search.md → ai-search.md} +70 -27
  98. package/llms.txt +9 -1
  99. package/package.json +1 -1
  100. package/sbom.cdx.json +1 -1
  101. package/dist/connectors/vector-search/client.js +0 -9
  102. package/dist/connectors/vector-search/client.js.map +0 -1
  103. package/dist/connectors/vector-search/index.js +0 -3
package/CLAUDE.md CHANGED
@@ -44,6 +44,7 @@ npx @databricks/appkit docs <query>
44
44
  ## Plugins
45
45
 
46
46
  - [Agents](./docs/plugins/agents.md): This plugin is currently beta. APIs may change between minor releases. Import from @databricks/appkit/beta. See Plugin Stability Tiers.
47
+ - [AI Search plugin](./docs/plugins/ai-search.md): This plugin is currently beta. APIs may change between minor releases. Import from @databricks/appkit/beta. See Plugin Stability Tiers.
47
48
  - [Analytics plugin](./docs/plugins/analytics.md): Enables SQL query execution against Databricks SQL Warehouses.
48
49
  - [Caching](./docs/plugins/caching.md): AppKit provides both global and plugin-level caching capabilities.
49
50
  - [Creating custom plugins](./docs/plugins/custom-plugins.md): If you need custom API routes or background logic, implement an AppKit plugin. The fastest way is to use the CLI:
@@ -57,7 +58,6 @@ npx @databricks/appkit docs <query>
57
58
  - [Plugin management](./docs/plugins/plugin-management.md): AppKit includes a CLI for managing plugins. All commands are available under npx @databricks/appkit plugin.
58
59
  - [Server plugin](./docs/plugins/server.md): Provides HTTP server capabilities with development and production modes.
59
60
  - [Plugin Stability Tiers](./docs/plugins/stability.md): AppKit plugins have a two-tier stability system that communicates API maturity and breaking-change expectations.
60
- - [Vector Search plugin](./docs/plugins/vector-search.md): Query Databricks Vector Search indexes with hybrid search, reranking, and cursor pagination from your AppKit application.
61
61
 
62
62
  ## appkit API reference [collapsed]
63
63
 
@@ -131,7 +131,9 @@ npx @databricks/appkit docs <query>
131
131
  - [Interface: GenerateDatabaseCredentialRequest](./docs/api/appkit/Interface.GenerateDatabaseCredentialRequest.md): Request parameters for generating database OAuth credentials
132
132
  - [Interface: GenerationParams](./docs/api/appkit/Interface.GenerationParams.md): Optional generation parameters forwarded to the OpenAI-compatible serving
133
133
  - [Interface: HostedSupervisorTool](./docs/api/appkit/Interface.HostedSupervisorTool.md): Tagged record returned by every supervisorTools factory. The
134
+ - [Interface: IAiSearchConfig](./docs/api/appkit/Interface.IAiSearchConfig.md): Base configuration interface for AppKit plugins
134
135
  - [Interface: IJobsConfig](./docs/api/appkit/Interface.IJobsConfig.md): Configuration for the Jobs plugin.
136
+ - [Interface: IndexConfig](./docs/api/appkit/Interface.IndexConfig.md): Properties
135
137
  - [Interface: ITelemetry](./docs/api/appkit/Interface.ITelemetry.md): Plugin-facing interface for OpenTelemetry instrumentation.
136
138
  - [Interface: JobAPI](./docs/api/appkit/Interface.JobAPI.md): User-facing API for a single configured job.
137
139
  - [Interface: JobConfig](./docs/api/appkit/Interface.JobConfig.md): Per-job configuration options.
@@ -147,10 +149,14 @@ npx @databricks/appkit docs <query>
147
149
  - [Interface: RegisteredAgent](./docs/api/appkit/Interface.RegisteredAgent.md): Properties
148
150
  - [Interface: RequestedClaims](./docs/api/appkit/Interface.RequestedClaims.md): Optional claims for fine-grained Unity Catalog table permissions
149
151
  - [Interface: RequestedResource](./docs/api/appkit/Interface.RequestedResource.md): Resource to request permissions for in Unity Catalog
152
+ - [Interface: RerankerConfig](./docs/api/appkit/Interface.RerankerConfig.md): Properties
150
153
  - [Interface: ResourceEntry](./docs/api/appkit/Interface.ResourceEntry.md): Internal representation of a resource in the registry.
151
154
  - [Interface: ResourceRequirement](./docs/api/appkit/Interface.ResourceRequirement.md): Declares a resource requirement for a plugin.
152
155
  - [Interface: RunAgentInput](./docs/api/appkit/Interface.RunAgentInput.md): Properties
153
156
  - [Interface: RunAgentResult](./docs/api/appkit/Interface.RunAgentResult.md): Properties
157
+ - [Interface: SearchRequest](./docs/api/appkit/Interface.SearchRequest.md): Properties
158
+ - [Interface: SearchResponse<T>](./docs/api/appkit/Interface.SearchResponse.md): Type Parameters
159
+ - [Interface: SearchResult<T>](./docs/api/appkit/Interface.SearchResult.md): Type Parameters
154
160
  - [Interface: ServingEndpointEntry](./docs/api/appkit/Interface.ServingEndpointEntry.md): Shape of a single registry entry.
155
161
  - [Interface: ServingEndpointRegistry](./docs/api/appkit/Interface.ServingEndpointRegistry.md): Registry interface for serving endpoint type generation.
156
162
  - [Interface: StreamExecutionSettings](./docs/api/appkit/Interface.StreamExecutionSettings.md): Execution settings for streaming endpoints. Extends PluginExecutionSettings with SSE stream configuration.
@@ -187,11 +193,13 @@ npx @databricks/appkit docs <query>
187
193
  - [Type Alias: ResolvedToolEntry](./docs/api/appkit/TypeAlias.ResolvedToolEntry.md): Internal tool-index entry after a tool record has been resolved to a dispatchable form.
188
194
  - [Type Alias: ResourceFieldEntry](./docs/api/appkit/TypeAlias.ResourceFieldEntry.md)
189
195
  - [Type Alias: ResourcePermission](./docs/api/appkit/TypeAlias.ResourcePermission.md): Union of all possible permission levels across all resource types.
196
+ - [Type Alias: SearchFilters](./docs/api/appkit/TypeAlias.SearchFilters.md)
190
197
  - [Type Alias: ServingFactory](./docs/api/appkit/TypeAlias.ServingFactory.md): Factory function returned by AppKit.serving.
191
198
  - [Type Alias: SupervisorTool](./docs/api/appkit/TypeAlias.SupervisorTool.md): Tools supported by the Databricks AI Gateway Responses API. The shapes match
192
199
  - [Type Alias: ToolRegistry](./docs/api/appkit/TypeAlias.ToolRegistry.md)
193
200
  - [Type Alias: ToPlugin()<T, U, N>](./docs/api/appkit/TypeAlias.ToPlugin.md): Factory function type returned by toPlugin(). Accepts optional config and returns a PluginData tuple.
194
201
  - [Variable: agents](./docs/api/appkit/Variable.agents.md): Plugin factory for the agents plugin. Reads config/agents/*.md by default,
202
+ - [Variable: aiSearch](./docs/api/appkit/Variable.aiSearch.md)
195
203
  - [Variable: READ_ACTIONS](./docs/api/appkit/Variable.READ_ACTIONS.md): Actions that only read data.
196
204
  - [Variable: sql](./docs/api/appkit/Variable.sql.md): SQL helper namespace
197
205
  - [Variable: SUPERVISOR_EXTENSION_KEY](./docs/api/appkit/Variable.SUPERVISOR_EXTENSION_KEY.md): Namespace key under which the adapter reads its hosted-tool payload
@@ -1,6 +1,6 @@
1
1
  //#region package.json
2
2
  var name = "@databricks/appkit";
3
- var version = "0.53.1";
3
+ var version = "0.55.0";
4
4
 
5
5
  //#endregion
6
6
  export { name, version };
package/dist/beta.d.ts CHANGED
@@ -14,5 +14,7 @@ import "./core/agent/tools/index.js";
14
14
  import { agentIdFromMarkdownPath, loadAgentFromFile, loadAgentsFromDir } from "./core/agent/load-agents.js";
15
15
  import { agents } from "./plugins/agents/agents.js";
16
16
  import "./plugins/agents/index.js";
17
+ import { IAiSearchConfig, IndexConfig, RerankerConfig, SearchFilters, SearchRequest, SearchResponse, SearchResult } from "./plugins/ai-search/types.js";
18
+ import { aiSearch } from "./plugins/ai-search/ai-search.js";
17
19
  import "./plugins/beta-exports.generated.js";
18
- export { type AgentAdapter, type AgentDefinition, type AgentEvent, type AgentInput, type AgentRunContext, type AgentTool, type AgentToolDefinition, type AgentTools, type AgentToolsFn, type AgentsPluginConfig, AppKitMcpClient, type AutoInheritToolsConfig, type BaseSystemPromptOption, DatabricksAdapter, type FunctionTool, type GenerationParams, type HostedSupervisorTool, type HostedTool, type McpConnectAllResult, type Message, type PluginToolkitProvider, type Plugins, type PromptContext, type RegisteredAgent, type ResolvedToolEntry, type RunAgentInput, type RunAgentResult, SUPERVISOR_EXTENSION_KEY, SupervisorApiAdapter, type SupervisorApiAdapterOptions, type SupervisorExtension, type SupervisorTool, type Thread, type ThreadStore, type ToolAnnotations, type ToolConfig, type ToolEntry, type ToolProvider, type ToolRegistry, type ToolkitEntry, type ToolkitOptions, type WorkspaceClientLike, agentIdFromMarkdownPath, agents, createAgent, defineTool, executeFromRegistry, fromSupervisorApi, functionToolToDefinition, isFunctionTool, isHostedTool, isSupervisorTool, isToolkitEntry, loadAgentFromFile, loadAgentsFromDir, mcpServer, parseTextToolCalls, resolveHostedTools, runAgent, supervisorTools, tool, toolsFromRegistry };
20
+ export { type AgentAdapter, type AgentDefinition, type AgentEvent, type AgentInput, type AgentRunContext, type AgentTool, type AgentToolDefinition, type AgentTools, type AgentToolsFn, type AgentsPluginConfig, AppKitMcpClient, type AutoInheritToolsConfig, type BaseSystemPromptOption, DatabricksAdapter, type FunctionTool, type GenerationParams, type HostedSupervisorTool, type HostedTool, type IAiSearchConfig, type IndexConfig, type McpConnectAllResult, type Message, type PluginToolkitProvider, type Plugins, type PromptContext, type RegisteredAgent, type RerankerConfig, type ResolvedToolEntry, type RunAgentInput, type RunAgentResult, SUPERVISOR_EXTENSION_KEY, type SearchFilters, type SearchRequest, type SearchResponse, type SearchResult, SupervisorApiAdapter, type SupervisorApiAdapterOptions, type SupervisorExtension, type SupervisorTool, type Thread, type ThreadStore, type ToolAnnotations, type ToolConfig, type ToolEntry, type ToolProvider, type ToolRegistry, type ToolkitEntry, type ToolkitOptions, type WorkspaceClientLike, agentIdFromMarkdownPath, agents, aiSearch, createAgent, defineTool, executeFromRegistry, fromSupervisorApi, functionToolToDefinition, isFunctionTool, isHostedTool, isSupervisorTool, isToolkitEntry, loadAgentFromFile, loadAgentsFromDir, mcpServer, parseTextToolCalls, resolveHostedTools, runAgent, supervisorTools, tool, toolsFromRegistry };
package/dist/beta.js CHANGED
@@ -12,6 +12,7 @@ import "./core/agent/tools/index.js";
12
12
  import { agentIdFromMarkdownPath, loadAgentFromFile, loadAgentsFromDir } from "./core/agent/load-agents.js";
13
13
  import { agents } from "./plugins/agents/agents.js";
14
14
  import "./plugins/agents/index.js";
15
+ import { aiSearch } from "./plugins/ai-search/ai-search.js";
15
16
  import "./plugins/beta-exports.generated.js";
16
17
 
17
- export { AppKitMcpClient, DatabricksAdapter, SUPERVISOR_EXTENSION_KEY, SupervisorApiAdapter, agentIdFromMarkdownPath, agents, createAgent, defineTool, executeFromRegistry, fromSupervisorApi, functionToolToDefinition, isFunctionTool, isHostedTool, isSupervisorTool, isToolkitEntry, loadAgentFromFile, loadAgentsFromDir, mcpServer, parseTextToolCalls, resolveHostedTools, runAgent, supervisorTools, tool, toolsFromRegistry };
18
+ export { AppKitMcpClient, DatabricksAdapter, SUPERVISOR_EXTENSION_KEY, SupervisorApiAdapter, agentIdFromMarkdownPath, agents, aiSearch, createAgent, defineTool, executeFromRegistry, fromSupervisorApi, functionToolToDefinition, isFunctionTool, isHostedTool, isSupervisorTool, isToolkitEntry, loadAgentFromFile, loadAgentsFromDir, mcpServer, parseTextToolCalls, resolveHostedTools, runAgent, supervisorTools, tool, toolsFromRegistry };
@@ -50,7 +50,7 @@ async function runGenerateTypes(rootDir, outFile, warehouseId, options) {
50
50
  const metricConfig = path.join(metricViewsFolder, METRIC_CONFIG_FILE);
51
51
  if (fs.existsSync(metricConfig)) {
52
52
  const typesDir = path.dirname(resolvedOutFile);
53
- console.log(`Generated metric types: ${path.join(typesDir, "metric-views.d.ts")}`);
53
+ console.log(`Generated metric types: ${path.join(typesDir, "metric-views.ts")}`);
54
54
  }
55
55
  }
56
56
  } else console.error("Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.");
@@ -1 +1 @@
1
- {"version":3,"file":"generate-types.js","names":[],"sources":["../../../src/cli/commands/generate-types.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command, Option } from \"commander\";\nimport { METRIC_CONFIG_FILE } from \"../../schemas/metric-fqn\";\nimport {\n acquireSpawnLock,\n getSpawnLockPath,\n releaseSpawnLock,\n} from \"./spawn-lock.js\";\n\n/**\n * Resolve the typegen pre-flight mode for the CLI. Defaults to \"non-blocking\" —\n * a one-shot CLI can't describe in the background, so by default it never\n * describes at all: it skips the warehouse probe AND every DESCRIBE, emits\n * best-available types (cache where the SQL hash matches, else `result: unknown`)\n * and returns immediately, never blocking on — or failing because of — a\n * warehouse, even a RUNNING one. Pass `--wait` (commander sets `wait: true`)\n * for a deliberate/CI invocation that should wait for a starting warehouse and\n * fail fast on a stopped one.\n */\nexport function resolveTypegenMode(options?: {\n wait?: boolean;\n}): \"non-blocking\" | \"blocking\" {\n return options?.wait ? \"blocking\" : \"non-blocking\";\n}\n\n/** Options parsed by commander for the generate-types command. */\ninterface GenerateTypesOptions {\n noCache?: boolean;\n wait?: boolean;\n /**\n * Internal: present only on the detached worker invocation. Carries the path\n * of the single-flight lock this worker must release when it finishes. Its\n * presence is what marks an invocation as \"the worker\" — workers always run\n * with `--wait`, so they never spawn another worker (only non-blocking runs\n * spawn), which terminates the recursion.\n */\n workerLock?: string;\n}\n\n/**\n * Generate types command implementation. Runs the library generate (which, in\n * non-blocking mode, writes degraded types and returns immediately). This is the\n * SAME work the worker performs in blocking mode in the background.\n */\nasync function runGenerateTypes(\n rootDir?: string,\n outFile?: string,\n warehouseId?: string,\n options?: GenerateTypesOptions,\n) {\n try {\n const resolvedRootDir = rootDir || process.cwd();\n const noCache = options?.noCache || false;\n const mode = resolveTypegenMode(options);\n\n const typeGen = await import(\"@databricks/appkit/type-generator\");\n\n // Generate analytics query types (requires warehouse ID)\n const resolvedWarehouseId =\n warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;\n\n if (resolvedWarehouseId) {\n const resolvedOutFile =\n outFile ||\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\");\n\n const queryFolder = path.join(resolvedRootDir, \"config/queries\");\n const metricViewsFolder = path.join(\n resolvedRootDir,\n \"config/metric-views\",\n );\n const hasQueries = fs.existsSync(queryFolder);\n const hasMetricViews = fs.existsSync(metricViewsFolder);\n\n // Generate when either config surface exists. Metric-view types are\n // independent of `.sql` queries — an app can declare metric views in\n // `config/metric-views/` without a `config/queries/` folder.\n if (hasQueries || hasMetricViews) {\n await typeGen.generateFromEntryPoint({\n queryFolder: hasQueries ? queryFolder : undefined,\n metricViewsFolder: hasMetricViews ? metricViewsFolder : undefined,\n outFile: resolvedOutFile,\n warehouseId: resolvedWarehouseId,\n noCache,\n mode,\n });\n\n if (hasQueries) {\n console.log(`Generated query types: ${resolvedOutFile}`);\n }\n\n const metricConfig = path.join(metricViewsFolder, METRIC_CONFIG_FILE);\n if (fs.existsSync(metricConfig)) {\n const typesDir = path.dirname(resolvedOutFile);\n console.log(\n `Generated metric types: ${path.join(typesDir, \"metric-views.d.ts\")}`,\n );\n }\n }\n } else {\n console.error(\n \"Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.\",\n );\n }\n\n // Generate serving endpoint types (no warehouse required)\n const servingOutFile = path.join(\n process.cwd(),\n \"shared/appkit-types/serving.d.ts\",\n );\n await typeGen.generateServingTypes({\n outFile: servingOutFile,\n noCache,\n });\n console.log(`Generated serving types: ${servingOutFile}`);\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"Cannot find module\")\n ) {\n console.error(\n \"Error: The 'generate-types' command is only available in @databricks/appkit.\",\n );\n console.error(\"Please install @databricks/appkit to use this command.\");\n process.exit(1);\n }\n // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // message (which queries failed and how to debug them). The stack trace\n // points into appkit internals and is noise for app developers, so print\n // only the message and exit non-zero instead of letting it bubble up.\n if (\n error instanceof Error &&\n (error.name === \"TypegenSyntaxError\" ||\n error.name === \"TypegenFatalError\")\n ) {\n console.error(error.message);\n process.exit(1);\n }\n throw error;\n }\n}\n\n/**\n * Spawn the detached blocking worker that refreshes real types in the background\n * after the foreground non-blocking generate has already written degraded types.\n *\n * Re-invokes THIS CLI (`process.execPath` + `process.argv[1]` — the bin entry\n * that launched us) with `generate-types --wait --worker-lock <lockPath>` plus\n * the same positional target options the foreground used, so the worker writes\n * to the same out file / reads the same query folder. The worker is:\n * - `detached: true` + `.unref()` so it outlives this process (install/dev-setup\n * can finish and exit while the worker keeps warming the warehouse).\n * - `stdio: \"ignore\"` so it never holds the parent's pipes open or interleaves\n * output into the install/dev log.\n *\n * Spawning is wrapped so any failure is non-fatal: the caller still has degraded\n * types and exits 0.\n *\n * @param lockPath - the acquired single-flight lock; passed to the worker so it\n * releases the SAME lock when it finishes.\n * @param targets - the foreground's positional args, forwarded verbatim.\n * @returns true if the worker was spawned, false if spawning threw.\n */\nexport function spawnTypegenWorker(\n lockPath: string,\n targets: { rootDir?: string; outFile?: string; warehouseId?: string },\n): boolean {\n // The script the runtime launched us with (the `appkit` bin shim). Re-running\n // it under the same node binary reproduces this exact CLI in the worker.\n const cliEntry = process.argv[1];\n\n // Forward the positionals in declaration order (rootDir, outFile,\n // warehouseId). Stop at the first undefined so we never pass a literal\n // \"undefined\" — commander would treat it as a positional value. (rootDir is\n // effectively always set by commander's default, but guard anyway.)\n const positionals: string[] = [];\n for (const value of [targets.rootDir, targets.outFile, targets.warehouseId]) {\n if (value === undefined) break;\n positionals.push(value);\n }\n\n const args = [\n // Forward the parent's node/loader flags so the worker runs under the same\n // runtime. Critically this carries tsx's `--require`/`--import` when the CLI\n // is run from source (`tsx index.ts …`); without them the worker would be\n // `node index.ts …`, which can't parse TypeScript and dies silently — the\n // degraded types would then never refresh. Empty for the built bin (plain\n // `node bin/appkit.js`), so production behaviour is unchanged.\n ...process.execArgv,\n cliEntry,\n \"generate-types\",\n \"--wait\",\n \"--worker-lock\",\n lockPath,\n ...positionals,\n ];\n\n try {\n const child = spawn(process.execPath, args, {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n return true;\n } catch (error) {\n // Non-fatal: the foreground already wrote degraded types. Log and move on.\n console.error(\n `Could not start background type refresh: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n return false;\n }\n}\n\n/**\n * The command action. Orchestrates the non-blocking foreground contract:\n * 1. Run the library generate (writes degraded types immediately in non-blocking\n * mode; does the full blocking lifecycle when this is the worker).\n * 2. If this is a non-blocking, non-worker invocation, try to spawn the detached\n * blocking worker behind the single-flight lock. If the lock is already held\n * by a live worker, skip (single-flight) with a one-line note. Either way the\n * foreground returns normally (exit 0).\n * 3. If this IS the worker (`--worker-lock` present), it ran blocking above and\n * releases the lock here (and via a process-exit guard, so a hard failure /\n * process.exit still frees it).\n */\nasync function generateTypesAction(\n rootDir: string | undefined,\n outFile: string | undefined,\n warehouseId: string | undefined,\n options: GenerateTypesOptions,\n) {\n const isWorker = typeof options.workerLock === \"string\";\n\n // A worker must always free its lock, even if the blocking generate throws or\n // calls process.exit (TypegenFatalError → exit 1). The exit handler covers the\n // process.exit / uncaught paths; the finally covers the normal return.\n if (isWorker && options.workerLock) {\n const lockPath = options.workerLock;\n process.once(\"exit\", () => releaseSpawnLock(lockPath));\n }\n\n try {\n await runGenerateTypes(rootDir, outFile, warehouseId, options);\n } finally {\n if (isWorker && options.workerLock) {\n releaseSpawnLock(options.workerLock);\n }\n }\n\n // Only a non-blocking, non-worker invocation spawns. A worker is always\n // --wait (so resolveTypegenMode → \"blocking\"), which both prevents recursion\n // and means we never get here for a worker.\n if (!isWorker && resolveTypegenMode(options) === \"non-blocking\") {\n const resolvedRootDir = rootDir || process.cwd();\n const lockPath = getSpawnLockPath(resolvedRootDir);\n\n if (acquireSpawnLock(lockPath)) {\n spawnTypegenWorker(lockPath, { rootDir, outFile, warehouseId });\n } else {\n console.log(\"Type refresh already in progress, skipping.\");\n }\n }\n}\n\nexport const generateTypesCommand = new Command(\"generate-types\")\n .description(\"Generate TypeScript types from SQL queries\")\n .argument(\"[rootDir]\", \"Root directory of the project\", process.cwd())\n .argument(\n \"[outFile]\",\n \"Output file path\",\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\"),\n )\n .argument(\"[warehouseId]\", \"Databricks warehouse ID\")\n .option(\"--no-cache\", \"Disable caching for type generation\")\n .option(\n \"--wait\",\n \"Wait for warehouse readiness instead of degrading (use for CI)\",\n )\n // Internal: marks the detached background worker and carries the lock it must\n // release. Hidden from --help; users should never pass it.\n .addOption(\n new Option(\n \"--worker-lock <path>\",\n \"Internal: detached worker lock path\",\n ).hideHelp(),\n )\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit generate-types\n $ appkit generate-types . shared/appkit-types/analytics.d.ts\n $ appkit generate-types . shared/appkit-types/analytics.d.ts my-warehouse-id\n $ appkit generate-types --no-cache\n $ appkit generate-types --wait # CI: wait for the warehouse and fail on a cold one`,\n )\n .action(generateTypesAction);\n"],"mappings":"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"}
1
+ {"version":3,"file":"generate-types.js","names":[],"sources":["../../../src/cli/commands/generate-types.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command, Option } from \"commander\";\nimport { METRIC_CONFIG_FILE } from \"../../schemas/metric-fqn\";\nimport {\n acquireSpawnLock,\n getSpawnLockPath,\n releaseSpawnLock,\n} from \"./spawn-lock.js\";\n\n/**\n * Resolve the typegen pre-flight mode for the CLI. Defaults to \"non-blocking\" —\n * a one-shot CLI can't describe in the background, so by default it never\n * describes at all: it skips the warehouse probe AND every DESCRIBE, emits\n * best-available types (cache where the SQL hash matches, else `result: unknown`)\n * and returns immediately, never blocking on — or failing because of — a\n * warehouse, even a RUNNING one. Pass `--wait` (commander sets `wait: true`)\n * for a deliberate/CI invocation that should wait for a starting warehouse and\n * fail fast on a stopped one.\n */\nexport function resolveTypegenMode(options?: {\n wait?: boolean;\n}): \"non-blocking\" | \"blocking\" {\n return options?.wait ? \"blocking\" : \"non-blocking\";\n}\n\n/** Options parsed by commander for the generate-types command. */\ninterface GenerateTypesOptions {\n noCache?: boolean;\n wait?: boolean;\n /**\n * Internal: present only on the detached worker invocation. Carries the path\n * of the single-flight lock this worker must release when it finishes. Its\n * presence is what marks an invocation as \"the worker\" — workers always run\n * with `--wait`, so they never spawn another worker (only non-blocking runs\n * spawn), which terminates the recursion.\n */\n workerLock?: string;\n}\n\n/**\n * Generate types command implementation. Runs the library generate (which, in\n * non-blocking mode, writes degraded types and returns immediately). This is the\n * SAME work the worker performs in blocking mode in the background.\n */\nasync function runGenerateTypes(\n rootDir?: string,\n outFile?: string,\n warehouseId?: string,\n options?: GenerateTypesOptions,\n) {\n try {\n const resolvedRootDir = rootDir || process.cwd();\n const noCache = options?.noCache || false;\n const mode = resolveTypegenMode(options);\n\n const typeGen = await import(\"@databricks/appkit/type-generator\");\n\n // Generate analytics query types (requires warehouse ID)\n const resolvedWarehouseId =\n warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;\n\n if (resolvedWarehouseId) {\n const resolvedOutFile =\n outFile ||\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\");\n\n const queryFolder = path.join(resolvedRootDir, \"config/queries\");\n const metricViewsFolder = path.join(\n resolvedRootDir,\n \"config/metric-views\",\n );\n const hasQueries = fs.existsSync(queryFolder);\n const hasMetricViews = fs.existsSync(metricViewsFolder);\n\n // Generate when either config surface exists. Metric-view types are\n // independent of `.sql` queries — an app can declare metric views in\n // `config/metric-views/` without a `config/queries/` folder.\n if (hasQueries || hasMetricViews) {\n await typeGen.generateFromEntryPoint({\n queryFolder: hasQueries ? queryFolder : undefined,\n metricViewsFolder: hasMetricViews ? metricViewsFolder : undefined,\n outFile: resolvedOutFile,\n warehouseId: resolvedWarehouseId,\n noCache,\n mode,\n });\n\n if (hasQueries) {\n console.log(`Generated query types: ${resolvedOutFile}`);\n }\n\n const metricConfig = path.join(metricViewsFolder, METRIC_CONFIG_FILE);\n if (fs.existsSync(metricConfig)) {\n const typesDir = path.dirname(resolvedOutFile);\n console.log(\n `Generated metric types: ${path.join(typesDir, \"metric-views.ts\")}`,\n );\n }\n }\n } else {\n console.error(\n \"Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.\",\n );\n }\n\n // Generate serving endpoint types (no warehouse required)\n const servingOutFile = path.join(\n process.cwd(),\n \"shared/appkit-types/serving.d.ts\",\n );\n await typeGen.generateServingTypes({\n outFile: servingOutFile,\n noCache,\n });\n console.log(`Generated serving types: ${servingOutFile}`);\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"Cannot find module\")\n ) {\n console.error(\n \"Error: The 'generate-types' command is only available in @databricks/appkit.\",\n );\n console.error(\"Please install @databricks/appkit to use this command.\");\n process.exit(1);\n }\n // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // message (which queries failed and how to debug them). The stack trace\n // points into appkit internals and is noise for app developers, so print\n // only the message and exit non-zero instead of letting it bubble up.\n if (\n error instanceof Error &&\n (error.name === \"TypegenSyntaxError\" ||\n error.name === \"TypegenFatalError\")\n ) {\n console.error(error.message);\n process.exit(1);\n }\n throw error;\n }\n}\n\n/**\n * Spawn the detached blocking worker that refreshes real types in the background\n * after the foreground non-blocking generate has already written degraded types.\n *\n * Re-invokes THIS CLI (`process.execPath` + `process.argv[1]` — the bin entry\n * that launched us) with `generate-types --wait --worker-lock <lockPath>` plus\n * the same positional target options the foreground used, so the worker writes\n * to the same out file / reads the same query folder. The worker is:\n * - `detached: true` + `.unref()` so it outlives this process (install/dev-setup\n * can finish and exit while the worker keeps warming the warehouse).\n * - `stdio: \"ignore\"` so it never holds the parent's pipes open or interleaves\n * output into the install/dev log.\n *\n * Spawning is wrapped so any failure is non-fatal: the caller still has degraded\n * types and exits 0.\n *\n * @param lockPath - the acquired single-flight lock; passed to the worker so it\n * releases the SAME lock when it finishes.\n * @param targets - the foreground's positional args, forwarded verbatim.\n * @returns true if the worker was spawned, false if spawning threw.\n */\nexport function spawnTypegenWorker(\n lockPath: string,\n targets: { rootDir?: string; outFile?: string; warehouseId?: string },\n): boolean {\n // The script the runtime launched us with (the `appkit` bin shim). Re-running\n // it under the same node binary reproduces this exact CLI in the worker.\n const cliEntry = process.argv[1];\n\n // Forward the positionals in declaration order (rootDir, outFile,\n // warehouseId). Stop at the first undefined so we never pass a literal\n // \"undefined\" — commander would treat it as a positional value. (rootDir is\n // effectively always set by commander's default, but guard anyway.)\n const positionals: string[] = [];\n for (const value of [targets.rootDir, targets.outFile, targets.warehouseId]) {\n if (value === undefined) break;\n positionals.push(value);\n }\n\n const args = [\n // Forward the parent's node/loader flags so the worker runs under the same\n // runtime. Critically this carries tsx's `--require`/`--import` when the CLI\n // is run from source (`tsx index.ts …`); without them the worker would be\n // `node index.ts …`, which can't parse TypeScript and dies silently — the\n // degraded types would then never refresh. Empty for the built bin (plain\n // `node bin/appkit.js`), so production behaviour is unchanged.\n ...process.execArgv,\n cliEntry,\n \"generate-types\",\n \"--wait\",\n \"--worker-lock\",\n lockPath,\n ...positionals,\n ];\n\n try {\n const child = spawn(process.execPath, args, {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n return true;\n } catch (error) {\n // Non-fatal: the foreground already wrote degraded types. Log and move on.\n console.error(\n `Could not start background type refresh: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n return false;\n }\n}\n\n/**\n * The command action. Orchestrates the non-blocking foreground contract:\n * 1. Run the library generate (writes degraded types immediately in non-blocking\n * mode; does the full blocking lifecycle when this is the worker).\n * 2. If this is a non-blocking, non-worker invocation, try to spawn the detached\n * blocking worker behind the single-flight lock. If the lock is already held\n * by a live worker, skip (single-flight) with a one-line note. Either way the\n * foreground returns normally (exit 0).\n * 3. If this IS the worker (`--worker-lock` present), it ran blocking above and\n * releases the lock here (and via a process-exit guard, so a hard failure /\n * process.exit still frees it).\n */\nasync function generateTypesAction(\n rootDir: string | undefined,\n outFile: string | undefined,\n warehouseId: string | undefined,\n options: GenerateTypesOptions,\n) {\n const isWorker = typeof options.workerLock === \"string\";\n\n // A worker must always free its lock, even if the blocking generate throws or\n // calls process.exit (TypegenFatalError → exit 1). The exit handler covers the\n // process.exit / uncaught paths; the finally covers the normal return.\n if (isWorker && options.workerLock) {\n const lockPath = options.workerLock;\n process.once(\"exit\", () => releaseSpawnLock(lockPath));\n }\n\n try {\n await runGenerateTypes(rootDir, outFile, warehouseId, options);\n } finally {\n if (isWorker && options.workerLock) {\n releaseSpawnLock(options.workerLock);\n }\n }\n\n // Only a non-blocking, non-worker invocation spawns. A worker is always\n // --wait (so resolveTypegenMode → \"blocking\"), which both prevents recursion\n // and means we never get here for a worker.\n if (!isWorker && resolveTypegenMode(options) === \"non-blocking\") {\n const resolvedRootDir = rootDir || process.cwd();\n const lockPath = getSpawnLockPath(resolvedRootDir);\n\n if (acquireSpawnLock(lockPath)) {\n spawnTypegenWorker(lockPath, { rootDir, outFile, warehouseId });\n } else {\n console.log(\"Type refresh already in progress, skipping.\");\n }\n }\n}\n\nexport const generateTypesCommand = new Command(\"generate-types\")\n .description(\"Generate TypeScript types from SQL queries\")\n .argument(\"[rootDir]\", \"Root directory of the project\", process.cwd())\n .argument(\n \"[outFile]\",\n \"Output file path\",\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\"),\n )\n .argument(\"[warehouseId]\", \"Databricks warehouse ID\")\n .option(\"--no-cache\", \"Disable caching for type generation\")\n .option(\n \"--wait\",\n \"Wait for warehouse readiness instead of degrading (use for CI)\",\n )\n // Internal: marks the detached background worker and carries the lock it must\n // release. Hidden from --help; users should never pass it.\n .addOption(\n new Option(\n \"--worker-lock <path>\",\n \"Internal: detached worker lock path\",\n ).hideHelp(),\n )\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit generate-types\n $ appkit generate-types . shared/appkit-types/analytics.d.ts\n $ appkit generate-types . shared/appkit-types/analytics.d.ts my-warehouse-id\n $ appkit generate-types --no-cache\n $ appkit generate-types --wait # CI: wait for the warehouse and fail on a cold one`,\n )\n .action(generateTypesAction);\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { PLUGIN_NAME_PATTERN } from "../../../../naming.js";
1
2
  import { DEFAULT_PERMISSION_BY_TYPE, RESOURCE_TYPE_OPTIONS, getDefaultFieldsForType, getValidResourceTypes, humanizeResourceType, resourceKeyFromType } from "./resource-defaults.js";
2
3
  import { promptOneResource } from "./prompt-resource.js";
3
4
  import { resolveTargetDir, scaffoldPlugin } from "./scaffold.js";
@@ -8,7 +9,6 @@ import process from "node:process";
8
9
  import { cancel, confirm, intro, isCancel, multiselect, outro, select, spinner, text } from "@clack/prompts";
9
10
 
10
11
  //#region src/cli/commands/plugin/create/create.ts
11
- const NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
12
12
  const DEFAULT_VERSION = "0.1.0";
13
13
  const VALID_PLACEMENTS = ["in-repo", "isolated"];
14
14
  const REQUIRED_FLAGS = [
@@ -18,10 +18,8 @@ const REQUIRED_FLAGS = [
18
18
  "description"
19
19
  ];
20
20
  function deriveDisplayName(name) {
21
- return name.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" ");
22
- }
23
- function deriveExportName(name) {
24
- return name.split("-").map((s, i) => i === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1)).join("");
21
+ const spaced = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
22
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
25
23
  }
26
24
  function buildResourceFromType(type) {
27
25
  return {
@@ -84,7 +82,7 @@ function parseResourcesShorthand(csv) {
84
82
  function printNextSteps(answers, targetDir) {
85
83
  const relativePath = path.relative(process.cwd(), targetDir);
86
84
  const importPath = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
87
- const exportName = deriveExportName(answers.name);
85
+ const exportName = answers.name;
88
86
  console.log("\nNext steps:\n");
89
87
  if (answers.placement === "in-repo") {
90
88
  console.log(` 1. Import and register in your server:`);
@@ -104,7 +102,7 @@ function runNonInteractive(opts) {
104
102
  if (missing.length > 0) {
105
103
  console.error(`Error: Non-interactive mode requires: ${REQUIRED_FLAGS.map((f) => `--${f}`).join(", ")}`);
106
104
  console.error(`Missing: ${missing.map((f) => `--${f}`).join(", ")}`);
107
- console.error(" appkit plugin create --placement in-repo --path plugins/my-plugin --name my-plugin --description \"Does X\"");
105
+ console.error(" appkit plugin create --placement in-repo --path plugins/my-plugin --name myPlugin --description \"Does X\"");
108
106
  process.exit(1);
109
107
  }
110
108
  const placement = opts.placement;
@@ -118,8 +116,8 @@ function runNonInteractive(opts) {
118
116
  process.exit(1);
119
117
  }
120
118
  const name = opts.name;
121
- if (!NAME_PATTERN.test(name)) {
122
- console.error("Error: --name must be lowercase, start with a letter, and use only letters, numbers, and hyphens.");
119
+ if (!PLUGIN_NAME_PATTERN.test(name)) {
120
+ console.error("Error: --name must start with a lowercase letter and be camelCase (letters and numbers only, e.g. myPlugin).");
123
121
  process.exit(1);
124
122
  }
125
123
  let resources = [];
@@ -177,10 +175,10 @@ async function runInteractive() {
177
175
  }
178
176
  const name = await text({
179
177
  message: "Plugin name (id)",
180
- placeholder: "my-plugin",
178
+ placeholder: "myPlugin",
181
179
  validate(value) {
182
180
  if (!value?.trim()) return "Name is required.";
183
- if (!NAME_PATTERN.test(value)) return "Must be lowercase, start with a letter, and use only letters, numbers, and hyphens.";
181
+ if (!PLUGIN_NAME_PATTERN.test(value)) return "Must start with a lowercase letter and be camelCase (letters and numbers only, e.g. myPlugin).";
184
182
  }
185
183
  });
186
184
  if (isCancel(name)) {
@@ -303,7 +301,7 @@ async function runPluginCreate(opts) {
303
301
  else {
304
302
  if (OPTIONAL_FLAGS.some((f) => opts[f] !== void 0 && opts[f] !== false)) {
305
303
  console.error(`Error: Non-interactive mode requires: ${REQUIRED_FLAGS.map((f) => `--${f}`).join(", ")}`);
306
- console.error(" appkit plugin create --placement in-repo --path plugins/my-plugin --name my-plugin --description \"Does X\"");
304
+ console.error(" appkit plugin create --placement in-repo --path plugins/my-plugin --name myPlugin --description \"Does X\"");
307
305
  process.exit(1);
308
306
  }
309
307
  await runInteractive();
@@ -312,8 +310,8 @@ async function runPluginCreate(opts) {
312
310
  const pluginCreateCommand = new Command("create").description("Scaffold a new AppKit plugin").option("--placement <type>", "Where the plugin lives (in-repo, isolated)").option("--path <dir>", "Target directory for the plugin").option("--name <id>", "Plugin name (lowercase, hyphens allowed)").option("--display-name <name>", "Human-readable display name").option("--description <text>", "Short description of the plugin").addOption(new Option("--resources <types>", "Comma-separated resource types (e.g. sql_warehouse,volume)").conflicts("resourcesJson")).addOption(new Option("--resources-json <json>", "JSON array of resource specs (e.g. '[{\"type\":\"sql_warehouse\"}]')").conflicts("resources")).option("-f, --force", "Overwrite existing directory without confirmation").addHelpText("after", `
313
311
  Examples:
314
312
  $ appkit plugin create
315
- $ appkit plugin create --placement in-repo --path plugins/my-plugin --name my-plugin --description "Does X"
316
- $ appkit plugin create --placement in-repo --path plugins/my-plugin --name my-plugin --description "Does X" --resources sql_warehouse,volume --force
313
+ $ appkit plugin create --placement in-repo --path plugins/my-plugin --name myPlugin --description "Does X"
314
+ $ appkit plugin create --placement in-repo --path plugins/my-plugin --name myPlugin --description "Does X" --resources sql_warehouse,volume --force
317
315
  $ appkit plugin create --placement isolated --path appkit-plugin-ml --name ml --description "ML" --resources-json '[{"type":"serving_endpoint"}]'`).action((opts) => runPluginCreate(opts).catch((err) => {
318
316
  console.error(err);
319
317
  process.exit(1);
@@ -1 +1 @@
1
- {"version":3,"file":"create.js","names":[],"sources":["../../../../../src/cli/commands/plugin/create/create.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport {\n cancel,\n confirm,\n intro,\n isCancel,\n multiselect,\n outro,\n select,\n spinner,\n text,\n} from \"@clack/prompts\";\nimport { Command, Option } from \"commander\";\nimport { promptOneResource } from \"./prompt-resource\";\nimport {\n DEFAULT_PERMISSION_BY_TYPE,\n getDefaultFieldsForType,\n getValidResourceTypes,\n humanizeResourceType,\n RESOURCE_TYPE_OPTIONS,\n resourceKeyFromType,\n} from \"./resource-defaults\";\nimport { resolveTargetDir, scaffoldPlugin } from \"./scaffold\";\nimport type { CreateAnswers, Placement, SelectedResource } from \"./types\";\n\nconst NAME_PATTERN = /^[a-z][a-z0-9-]*$/;\nconst DEFAULT_VERSION = \"0.1.0\";\nconst VALID_PLACEMENTS: Placement[] = [\"in-repo\", \"isolated\"];\nconst REQUIRED_FLAGS = [\"placement\", \"path\", \"name\", \"description\"] as const;\n\ninterface CreateOptions {\n placement?: string;\n path?: string;\n name?: string;\n displayName?: string;\n description?: string;\n resources?: string;\n resourcesJson?: string;\n force?: boolean;\n}\n\nfunction deriveDisplayName(name: string): string {\n return name\n .split(\"-\")\n .map((s) => s.charAt(0).toUpperCase() + s.slice(1))\n .join(\" \");\n}\n\nfunction deriveExportName(name: string): string {\n return name\n .split(\"-\")\n .map((s, i) => (i === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1)))\n .join(\"\");\n}\n\nfunction buildResourceFromType(type: string): SelectedResource {\n return {\n type,\n required: true,\n description: `Required for ${humanizeResourceType(type)} functionality.`,\n resourceKey: resourceKeyFromType(type),\n permission: DEFAULT_PERMISSION_BY_TYPE[type] ?? \"CAN_VIEW\",\n fields: getDefaultFieldsForType(type),\n };\n}\n\ninterface JsonResourceEntry {\n type: string;\n required?: boolean;\n description?: string;\n resourceKey?: string;\n permission?: string;\n fields?: Record<string, { env: string; description?: string }>;\n}\n\nfunction parseResourcesJson(json: string): SelectedResource[] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n console.error(\"Error: --resources-json must be valid JSON.\");\n console.error(' Example: --resources-json \\'[{\"type\":\"sql_warehouse\"}]\\'');\n process.exit(1);\n }\n\n if (!Array.isArray(parsed)) {\n console.error(\"Error: --resources-json must be a JSON array.\");\n console.error(' Example: --resources-json \\'[{\"type\":\"sql_warehouse\"}]\\'');\n process.exit(1);\n }\n\n return (parsed as JsonResourceEntry[]).map((entry, i) => {\n if (entry == null || typeof entry !== \"object\") {\n console.error(`Error: --resources-json entry ${i} is not an object.`);\n process.exit(1);\n }\n if (!entry.type || typeof entry.type !== \"string\") {\n console.error(\n `Error: --resources-json entry ${i} missing required \"type\" field.`,\n );\n process.exit(1);\n }\n validateResourceType(entry.type);\n const defaults = buildResourceFromType(entry.type);\n return {\n type: entry.type,\n required: entry.required ?? defaults.required,\n description: entry.description ?? defaults.description,\n resourceKey: entry.resourceKey ?? defaults.resourceKey,\n permission: entry.permission ?? defaults.permission,\n fields: entry.fields ?? defaults.fields,\n };\n });\n}\n\nfunction validateResourceType(type: string): void {\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}\n\nfunction parseResourcesShorthand(csv: string): SelectedResource[] {\n const types = csv\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n for (const t of types) validateResourceType(t);\n return types.map(buildResourceFromType);\n}\n\nfunction printNextSteps(answers: CreateAnswers, targetDir: string): void {\n const relativePath = path.relative(process.cwd(), targetDir);\n const importPath = relativePath.startsWith(\".\")\n ? relativePath\n : `./${relativePath}`;\n const exportName = deriveExportName(answers.name);\n\n console.log(\"\\nNext steps:\\n\");\n if (answers.placement === \"in-repo\") {\n console.log(` 1. Import and register in your server:`);\n console.log(` import { ${exportName} } from \"${importPath}\";`);\n console.log(` createApp({ plugins: [ ..., ${exportName}() ] });`);\n console.log(\n ` 2. Run \\`npx appkit plugin sync --write\\` to update appkit.plugins.json.\\n`,\n );\n } else {\n console.log(` 1. cd into the new package and install dependencies:`);\n console.log(` cd ${answers.targetPath} && pnpm install`);\n console.log(` 2. Build: pnpm build`);\n console.log(\n ` 3. In your app: pnpm add ./${answers.targetPath} @databricks/appkit`,\n );\n console.log(\n ` 4. Import and register: import { ${exportName} } from \"<package-name>\";\\n`,\n );\n }\n}\n\nfunction runNonInteractive(opts: CreateOptions): void {\n const missing = REQUIRED_FLAGS.filter((f) => !opts[f]);\n if (missing.length > 0) {\n console.error(\n `Error: Non-interactive mode requires: ${REQUIRED_FLAGS.map((f) => `--${f}`).join(\", \")}`,\n );\n console.error(`Missing: ${missing.map((f) => `--${f}`).join(\", \")}`);\n console.error(\n ' appkit plugin create --placement in-repo --path plugins/my-plugin --name my-plugin --description \"Does X\"',\n );\n process.exit(1);\n }\n\n const placement = opts.placement as Placement;\n if (!VALID_PLACEMENTS.includes(placement)) {\n console.error(\n `Error: --placement must be one of: ${VALID_PLACEMENTS.join(\", \")}`,\n );\n process.exit(1);\n }\n\n const targetPath = (opts.path as string).trim();\n if (\n placement === \"in-repo\" &&\n (path.isAbsolute(targetPath) || targetPath.startsWith(\"..\"))\n ) {\n console.error(\n \"Error: --path must be a relative path under the current directory for in-repo plugins.\",\n );\n process.exit(1);\n }\n\n const name = opts.name as string;\n if (!NAME_PATTERN.test(name)) {\n console.error(\n \"Error: --name must be lowercase, start with a letter, and use only letters, numbers, and hyphens.\",\n );\n process.exit(1);\n }\n\n let resources: SelectedResource[] = [];\n if (opts.resourcesJson) {\n resources = parseResourcesJson(opts.resourcesJson);\n } else if (opts.resources) {\n resources = parseResourcesShorthand(opts.resources);\n }\n\n const answers: CreateAnswers = {\n placement,\n targetPath,\n name: name.trim(),\n displayName: opts.displayName?.trim() || deriveDisplayName(name),\n description: (opts.description as string).trim(),\n resources,\n version: DEFAULT_VERSION,\n };\n\n const targetDir = resolveTargetDir(process.cwd(), answers);\n const dirExists = fs.existsSync(targetDir);\n const hasContent = dirExists && fs.readdirSync(targetDir).length > 0;\n if (hasContent && !opts.force) {\n console.error(\n `Error: Directory ${answers.targetPath} already exists and is not empty.`,\n );\n console.error(\" Use --force to overwrite.\");\n process.exit(1);\n }\n\n scaffoldPlugin(targetDir, answers, { isolated: placement === \"isolated\" });\n\n console.log(\n `Plugin \"${answers.name}\" created at ${path.relative(process.cwd(), targetDir)}`,\n );\n printNextSteps(answers, targetDir);\n}\n\nasync function runInteractive(): Promise<void> {\n intro(\"Create a new AppKit plugin\");\n\n try {\n const placement = await select<Placement>({\n message: \"Where should the plugin live?\",\n options: [\n {\n value: \"in-repo\",\n label: \"In this repository (e.g. plugins/my-plugin)\",\n hint: \"folder path\",\n },\n {\n value: \"isolated\",\n label: \"New isolated package\",\n hint: \"full package with package.json\",\n },\n ],\n });\n if (isCancel(placement)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const placementPrompt =\n placement === \"in-repo\"\n ? \"Folder path for the plugin (e.g. plugins/my-feature)\"\n : \"Directory name for the new package (e.g. appkit-plugin-my-feature)\";\n const targetPath = await text({\n message: placementPrompt,\n placeholder:\n placement === \"in-repo\"\n ? \"plugins/my-plugin\"\n : \"appkit-plugin-my-feature\",\n validate(value) {\n if (!value?.trim()) return \"Path is required.\";\n if (\n placement === \"in-repo\" &&\n (path.isAbsolute(value) || value.trim().startsWith(\"..\"))\n ) {\n return \"Use a relative path under the current directory (e.g. plugins/my-plugin).\";\n }\n return undefined;\n },\n });\n if (isCancel(targetPath)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const name = await text({\n message: \"Plugin name (id)\",\n placeholder: \"my-plugin\",\n validate(value) {\n if (!value?.trim()) return \"Name is required.\";\n if (!NAME_PATTERN.test(value as string)) {\n return \"Must be lowercase, start with a letter, and use only letters, numbers, and hyphens.\";\n }\n return undefined;\n },\n });\n if (isCancel(name)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const displayName = await text({\n message: \"Display name\",\n placeholder: \"My Plugin\",\n initialValue: name\n .split(\"-\")\n .map((s) => s.charAt(0).toUpperCase() + s.slice(1))\n .join(\" \"),\n validate(value) {\n if (!value?.trim()) return \"Display name is required.\";\n return undefined;\n },\n });\n if (isCancel(displayName)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const description = await text({\n message: \"Short description\",\n placeholder: \"What does this plugin do?\",\n validate(value) {\n if (!value?.trim()) return \"Description is required.\";\n return undefined;\n },\n });\n if (isCancel(description)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const stability = await select<\"ga\" | \"beta\">({\n message: \"Plugin stability level\",\n options: [\n { value: \"ga\", label: \"GA\", hint: \"API follows semver\" },\n {\n value: \"beta\",\n label: \"Beta\",\n hint: \"Heading to GA, API may change\",\n },\n ],\n initialValue: \"ga\" as \"ga\" | \"beta\",\n });\n if (isCancel(stability)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const resourceTypes = await multiselect({\n message: \"Which Databricks resources does this plugin need?\",\n options: RESOURCE_TYPE_OPTIONS.map((o) => ({\n value: o.value,\n label: o.label,\n })),\n required: false,\n });\n if (isCancel(resourceTypes)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const resources: CreateAnswers[\"resources\"] = [];\n for (const type of resourceTypes as string[]) {\n const spec = await promptOneResource({ type });\n if (!spec) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n resources.push({\n type: spec.type,\n required: spec.required,\n description: spec.description,\n resourceKey: spec.resourceKey,\n permission: spec.permission,\n fields: spec.fields,\n });\n }\n\n const answers: CreateAnswers = {\n placement,\n targetPath: (targetPath as string).trim(),\n name: (name as string).trim(),\n displayName: (displayName as string).trim(),\n description: (description as string).trim(),\n stability: stability === \"ga\" ? undefined : stability,\n resources,\n version: DEFAULT_VERSION,\n };\n\n const targetDir = resolveTargetDir(process.cwd(), answers);\n const dirExists = fs.existsSync(targetDir);\n const hasContent = dirExists && fs.readdirSync(targetDir).length > 0;\n if (hasContent) {\n const overwrite = await confirm({\n message: `Directory ${answers.targetPath} already exists and is not empty. Overwrite?`,\n initialValue: false,\n });\n if (isCancel(overwrite) || !overwrite) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n }\n\n const s = spinner();\n s.start(\"Writing files…\");\n try {\n scaffoldPlugin(targetDir, answers, {\n isolated: placement === \"isolated\",\n });\n s.stop(\"Files written.\");\n } catch (err) {\n s.stop(\"Failed.\");\n throw err;\n }\n\n outro(\"Plugin created successfully.\");\n printNextSteps(answers, targetDir);\n } catch (err) {\n console.error(err);\n process.exit(1);\n }\n}\n\nconst OPTIONAL_FLAGS = [\n \"displayName\",\n \"resources\",\n \"resourcesJson\",\n \"force\",\n] as const;\n\nasync function runPluginCreate(opts: CreateOptions): Promise<void> {\n const hasRequiredFlag = REQUIRED_FLAGS.some((f) => opts[f] !== undefined);\n if (hasRequiredFlag) {\n runNonInteractive(opts);\n } else {\n const hasOptionalOnly = OPTIONAL_FLAGS.some(\n (f) => opts[f] !== undefined && opts[f] !== false,\n );\n if (hasOptionalOnly) {\n console.error(\n `Error: Non-interactive mode requires: ${REQUIRED_FLAGS.map((f) => `--${f}`).join(\", \")}`,\n );\n console.error(\n ' appkit plugin create --placement in-repo --path plugins/my-plugin --name my-plugin --description \"Does X\"',\n );\n process.exit(1);\n }\n await runInteractive();\n }\n}\n\nexport const pluginCreateCommand = new Command(\"create\")\n .description(\"Scaffold a new AppKit plugin\")\n .option(\"--placement <type>\", \"Where the plugin lives (in-repo, isolated)\")\n .option(\"--path <dir>\", \"Target directory for the plugin\")\n .option(\"--name <id>\", \"Plugin name (lowercase, hyphens allowed)\")\n .option(\"--display-name <name>\", \"Human-readable display name\")\n .option(\"--description <text>\", \"Short description of the plugin\")\n .addOption(\n new Option(\n \"--resources <types>\",\n \"Comma-separated resource types (e.g. sql_warehouse,volume)\",\n ).conflicts(\"resourcesJson\"),\n )\n .addOption(\n new Option(\n \"--resources-json <json>\",\n 'JSON array of resource specs (e.g. \\'[{\"type\":\"sql_warehouse\"}]\\')',\n ).conflicts(\"resources\"),\n )\n .option(\"-f, --force\", \"Overwrite existing directory without confirmation\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit plugin create\n $ appkit plugin create --placement in-repo --path plugins/my-plugin --name my-plugin --description \"Does X\"\n $ appkit plugin create --placement in-repo --path plugins/my-plugin --name my-plugin --description \"Does X\" --resources sql_warehouse,volume --force\n $ appkit plugin create --placement isolated --path appkit-plugin-ml --name ml --description \"ML\" --resources-json '[{\"type\":\"serving_endpoint\"}]'`,\n )\n .action((opts) =>\n runPluginCreate(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n\n/** Exported for testing. */\nexport { buildResourceFromType, parseResourcesJson, parseResourcesShorthand };\n"],"mappings":"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"}
1
+ {"version":3,"file":"create.js","names":[],"sources":["../../../../../src/cli/commands/plugin/create/create.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport {\n cancel,\n confirm,\n intro,\n isCancel,\n multiselect,\n outro,\n select,\n spinner,\n text,\n} from \"@clack/prompts\";\nimport { Command, Option } from \"commander\";\nimport { PLUGIN_NAME_PATTERN } from \"../../../../naming\";\nimport { promptOneResource } from \"./prompt-resource\";\nimport {\n DEFAULT_PERMISSION_BY_TYPE,\n getDefaultFieldsForType,\n getValidResourceTypes,\n humanizeResourceType,\n RESOURCE_TYPE_OPTIONS,\n resourceKeyFromType,\n} from \"./resource-defaults\";\nimport { resolveTargetDir, scaffoldPlugin } from \"./scaffold\";\nimport type { CreateAnswers, Placement, SelectedResource } from \"./types\";\n\nconst DEFAULT_VERSION = \"0.1.0\";\nconst VALID_PLACEMENTS: Placement[] = [\"in-repo\", \"isolated\"];\nconst REQUIRED_FLAGS = [\"placement\", \"path\", \"name\", \"description\"] as const;\n\ninterface CreateOptions {\n placement?: string;\n path?: string;\n name?: string;\n displayName?: string;\n description?: string;\n resources?: string;\n resourcesJson?: string;\n force?: boolean;\n}\n\nfunction deriveDisplayName(name: string): string {\n // camelCase -> Title Case words (e.g. \"myPlugin\" -> \"My Plugin\").\n const spaced = name.replace(/([a-z0-9])([A-Z])/g, \"$1 $2\");\n return spaced.charAt(0).toUpperCase() + spaced.slice(1);\n}\n\nfunction buildResourceFromType(type: string): SelectedResource {\n return {\n type,\n required: true,\n description: `Required for ${humanizeResourceType(type)} functionality.`,\n resourceKey: resourceKeyFromType(type),\n permission: DEFAULT_PERMISSION_BY_TYPE[type] ?? \"CAN_VIEW\",\n fields: getDefaultFieldsForType(type),\n };\n}\n\ninterface JsonResourceEntry {\n type: string;\n required?: boolean;\n description?: string;\n resourceKey?: string;\n permission?: string;\n fields?: Record<string, { env: string; description?: string }>;\n}\n\nfunction parseResourcesJson(json: string): SelectedResource[] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n console.error(\"Error: --resources-json must be valid JSON.\");\n console.error(' Example: --resources-json \\'[{\"type\":\"sql_warehouse\"}]\\'');\n process.exit(1);\n }\n\n if (!Array.isArray(parsed)) {\n console.error(\"Error: --resources-json must be a JSON array.\");\n console.error(' Example: --resources-json \\'[{\"type\":\"sql_warehouse\"}]\\'');\n process.exit(1);\n }\n\n return (parsed as JsonResourceEntry[]).map((entry, i) => {\n if (entry == null || typeof entry !== \"object\") {\n console.error(`Error: --resources-json entry ${i} is not an object.`);\n process.exit(1);\n }\n if (!entry.type || typeof entry.type !== \"string\") {\n console.error(\n `Error: --resources-json entry ${i} missing required \"type\" field.`,\n );\n process.exit(1);\n }\n validateResourceType(entry.type);\n const defaults = buildResourceFromType(entry.type);\n return {\n type: entry.type,\n required: entry.required ?? defaults.required,\n description: entry.description ?? defaults.description,\n resourceKey: entry.resourceKey ?? defaults.resourceKey,\n permission: entry.permission ?? defaults.permission,\n fields: entry.fields ?? defaults.fields,\n };\n });\n}\n\nfunction validateResourceType(type: string): void {\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}\n\nfunction parseResourcesShorthand(csv: string): SelectedResource[] {\n const types = csv\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n for (const t of types) validateResourceType(t);\n return types.map(buildResourceFromType);\n}\n\nfunction printNextSteps(answers: CreateAnswers, targetDir: string): void {\n const relativePath = path.relative(process.cwd(), targetDir);\n const importPath = relativePath.startsWith(\".\")\n ? relativePath\n : `./${relativePath}`;\n // The camelCase plugin name is already the JS binding.\n const exportName = answers.name;\n\n console.log(\"\\nNext steps:\\n\");\n if (answers.placement === \"in-repo\") {\n console.log(` 1. Import and register in your server:`);\n console.log(` import { ${exportName} } from \"${importPath}\";`);\n console.log(` createApp({ plugins: [ ..., ${exportName}() ] });`);\n console.log(\n ` 2. Run \\`npx appkit plugin sync --write\\` to update appkit.plugins.json.\\n`,\n );\n } else {\n console.log(` 1. cd into the new package and install dependencies:`);\n console.log(` cd ${answers.targetPath} && pnpm install`);\n console.log(` 2. Build: pnpm build`);\n console.log(\n ` 3. In your app: pnpm add ./${answers.targetPath} @databricks/appkit`,\n );\n console.log(\n ` 4. Import and register: import { ${exportName} } from \"<package-name>\";\\n`,\n );\n }\n}\n\nfunction runNonInteractive(opts: CreateOptions): void {\n const missing = REQUIRED_FLAGS.filter((f) => !opts[f]);\n if (missing.length > 0) {\n console.error(\n `Error: Non-interactive mode requires: ${REQUIRED_FLAGS.map((f) => `--${f}`).join(\", \")}`,\n );\n console.error(`Missing: ${missing.map((f) => `--${f}`).join(\", \")}`);\n console.error(\n ' appkit plugin create --placement in-repo --path plugins/my-plugin --name myPlugin --description \"Does X\"',\n );\n process.exit(1);\n }\n\n const placement = opts.placement as Placement;\n if (!VALID_PLACEMENTS.includes(placement)) {\n console.error(\n `Error: --placement must be one of: ${VALID_PLACEMENTS.join(\", \")}`,\n );\n process.exit(1);\n }\n\n const targetPath = (opts.path as string).trim();\n if (\n placement === \"in-repo\" &&\n (path.isAbsolute(targetPath) || targetPath.startsWith(\"..\"))\n ) {\n console.error(\n \"Error: --path must be a relative path under the current directory for in-repo plugins.\",\n );\n process.exit(1);\n }\n\n const name = opts.name as string;\n if (!PLUGIN_NAME_PATTERN.test(name)) {\n console.error(\n \"Error: --name must start with a lowercase letter and be camelCase (letters and numbers only, e.g. myPlugin).\",\n );\n process.exit(1);\n }\n\n let resources: SelectedResource[] = [];\n if (opts.resourcesJson) {\n resources = parseResourcesJson(opts.resourcesJson);\n } else if (opts.resources) {\n resources = parseResourcesShorthand(opts.resources);\n }\n\n const answers: CreateAnswers = {\n placement,\n targetPath,\n name: name.trim(),\n displayName: opts.displayName?.trim() || deriveDisplayName(name),\n description: (opts.description as string).trim(),\n resources,\n version: DEFAULT_VERSION,\n };\n\n const targetDir = resolveTargetDir(process.cwd(), answers);\n const dirExists = fs.existsSync(targetDir);\n const hasContent = dirExists && fs.readdirSync(targetDir).length > 0;\n if (hasContent && !opts.force) {\n console.error(\n `Error: Directory ${answers.targetPath} already exists and is not empty.`,\n );\n console.error(\" Use --force to overwrite.\");\n process.exit(1);\n }\n\n scaffoldPlugin(targetDir, answers, { isolated: placement === \"isolated\" });\n\n console.log(\n `Plugin \"${answers.name}\" created at ${path.relative(process.cwd(), targetDir)}`,\n );\n printNextSteps(answers, targetDir);\n}\n\nasync function runInteractive(): Promise<void> {\n intro(\"Create a new AppKit plugin\");\n\n try {\n const placement = await select<Placement>({\n message: \"Where should the plugin live?\",\n options: [\n {\n value: \"in-repo\",\n label: \"In this repository (e.g. plugins/my-plugin)\",\n hint: \"folder path\",\n },\n {\n value: \"isolated\",\n label: \"New isolated package\",\n hint: \"full package with package.json\",\n },\n ],\n });\n if (isCancel(placement)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const placementPrompt =\n placement === \"in-repo\"\n ? \"Folder path for the plugin (e.g. plugins/my-feature)\"\n : \"Directory name for the new package (e.g. appkit-plugin-my-feature)\";\n const targetPath = await text({\n message: placementPrompt,\n placeholder:\n placement === \"in-repo\"\n ? \"plugins/my-plugin\"\n : \"appkit-plugin-my-feature\",\n validate(value) {\n if (!value?.trim()) return \"Path is required.\";\n if (\n placement === \"in-repo\" &&\n (path.isAbsolute(value) || value.trim().startsWith(\"..\"))\n ) {\n return \"Use a relative path under the current directory (e.g. plugins/my-plugin).\";\n }\n return undefined;\n },\n });\n if (isCancel(targetPath)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const name = await text({\n message: \"Plugin name (id)\",\n placeholder: \"myPlugin\",\n validate(value) {\n if (!value?.trim()) return \"Name is required.\";\n if (!PLUGIN_NAME_PATTERN.test(value as string)) {\n return \"Must start with a lowercase letter and be camelCase (letters and numbers only, e.g. myPlugin).\";\n }\n return undefined;\n },\n });\n if (isCancel(name)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const displayName = await text({\n message: \"Display name\",\n placeholder: \"My Plugin\",\n initialValue: name\n .split(\"-\")\n .map((s) => s.charAt(0).toUpperCase() + s.slice(1))\n .join(\" \"),\n validate(value) {\n if (!value?.trim()) return \"Display name is required.\";\n return undefined;\n },\n });\n if (isCancel(displayName)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const description = await text({\n message: \"Short description\",\n placeholder: \"What does this plugin do?\",\n validate(value) {\n if (!value?.trim()) return \"Description is required.\";\n return undefined;\n },\n });\n if (isCancel(description)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const stability = await select<\"ga\" | \"beta\">({\n message: \"Plugin stability level\",\n options: [\n { value: \"ga\", label: \"GA\", hint: \"API follows semver\" },\n {\n value: \"beta\",\n label: \"Beta\",\n hint: \"Heading to GA, API may change\",\n },\n ],\n initialValue: \"ga\" as \"ga\" | \"beta\",\n });\n if (isCancel(stability)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const resourceTypes = await multiselect({\n message: \"Which Databricks resources does this plugin need?\",\n options: RESOURCE_TYPE_OPTIONS.map((o) => ({\n value: o.value,\n label: o.label,\n })),\n required: false,\n });\n if (isCancel(resourceTypes)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const resources: CreateAnswers[\"resources\"] = [];\n for (const type of resourceTypes as string[]) {\n const spec = await promptOneResource({ type });\n if (!spec) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n resources.push({\n type: spec.type,\n required: spec.required,\n description: spec.description,\n resourceKey: spec.resourceKey,\n permission: spec.permission,\n fields: spec.fields,\n });\n }\n\n const answers: CreateAnswers = {\n placement,\n targetPath: (targetPath as string).trim(),\n name: (name as string).trim(),\n displayName: (displayName as string).trim(),\n description: (description as string).trim(),\n stability: stability === \"ga\" ? undefined : stability,\n resources,\n version: DEFAULT_VERSION,\n };\n\n const targetDir = resolveTargetDir(process.cwd(), answers);\n const dirExists = fs.existsSync(targetDir);\n const hasContent = dirExists && fs.readdirSync(targetDir).length > 0;\n if (hasContent) {\n const overwrite = await confirm({\n message: `Directory ${answers.targetPath} already exists and is not empty. Overwrite?`,\n initialValue: false,\n });\n if (isCancel(overwrite) || !overwrite) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n }\n\n const s = spinner();\n s.start(\"Writing files…\");\n try {\n scaffoldPlugin(targetDir, answers, {\n isolated: placement === \"isolated\",\n });\n s.stop(\"Files written.\");\n } catch (err) {\n s.stop(\"Failed.\");\n throw err;\n }\n\n outro(\"Plugin created successfully.\");\n printNextSteps(answers, targetDir);\n } catch (err) {\n console.error(err);\n process.exit(1);\n }\n}\n\nconst OPTIONAL_FLAGS = [\n \"displayName\",\n \"resources\",\n \"resourcesJson\",\n \"force\",\n] as const;\n\nasync function runPluginCreate(opts: CreateOptions): Promise<void> {\n const hasRequiredFlag = REQUIRED_FLAGS.some((f) => opts[f] !== undefined);\n if (hasRequiredFlag) {\n runNonInteractive(opts);\n } else {\n const hasOptionalOnly = OPTIONAL_FLAGS.some(\n (f) => opts[f] !== undefined && opts[f] !== false,\n );\n if (hasOptionalOnly) {\n console.error(\n `Error: Non-interactive mode requires: ${REQUIRED_FLAGS.map((f) => `--${f}`).join(\", \")}`,\n );\n console.error(\n ' appkit plugin create --placement in-repo --path plugins/my-plugin --name myPlugin --description \"Does X\"',\n );\n process.exit(1);\n }\n await runInteractive();\n }\n}\n\nexport const pluginCreateCommand = new Command(\"create\")\n .description(\"Scaffold a new AppKit plugin\")\n .option(\"--placement <type>\", \"Where the plugin lives (in-repo, isolated)\")\n .option(\"--path <dir>\", \"Target directory for the plugin\")\n .option(\"--name <id>\", \"Plugin name (lowercase, hyphens allowed)\")\n .option(\"--display-name <name>\", \"Human-readable display name\")\n .option(\"--description <text>\", \"Short description of the plugin\")\n .addOption(\n new Option(\n \"--resources <types>\",\n \"Comma-separated resource types (e.g. sql_warehouse,volume)\",\n ).conflicts(\"resourcesJson\"),\n )\n .addOption(\n new Option(\n \"--resources-json <json>\",\n 'JSON array of resource specs (e.g. \\'[{\"type\":\"sql_warehouse\"}]\\')',\n ).conflicts(\"resources\"),\n )\n .option(\"-f, --force\", \"Overwrite existing directory without confirmation\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit plugin create\n $ appkit plugin create --placement in-repo --path plugins/my-plugin --name myPlugin --description \"Does X\"\n $ appkit plugin create --placement in-repo --path plugins/my-plugin --name myPlugin --description \"Does X\" --resources sql_warehouse,volume --force\n $ appkit plugin create --placement isolated --path appkit-plugin-ml --name ml --description \"ML\" --resources-json '[{\"type\":\"serving_endpoint\"}]'`,\n )\n .action((opts) =>\n runPluginCreate(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n\n/** Exported for testing. */\nexport { buildResourceFromType, parseResourcesJson, parseResourcesShorthand };\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { kebabToCamel } from "../../../../naming.js";
1
2
  import { resolveManifestInDir } from "../manifest-resolve.js";
2
3
  import { shouldAllowJsManifestForDir } from "../trusted-js-manifest.js";
3
4
  import { isWithinDirectory } from "../sync/sync.js";
@@ -113,17 +114,6 @@ function escapeRegex(s) {
113
114
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
114
115
  }
115
116
  /**
116
- * Convert a kebab-case manifest name to its camelCase JS identifier form
117
- * (e.g. `vector-search` -> `vectorSearch`). Mirrors the convention used by
118
- * first-party plugin index files: a manifest's `name` field may be
119
- * kebab-case (the schema permits `^[a-z][a-z0-9-]*$`), but the actual
120
- * exported binding is always a JS identifier. We try both forms when
121
- * matching specifiers in user code.
122
- */
123
- function manifestNameToBinding(pluginName) {
124
- return pluginName.replace(/-+([a-z0-9])/g, (_, c) => c.toUpperCase());
125
- }
126
- /**
127
117
  * Returns true when the named import specifier `spec` resolves to either
128
118
  * `pluginName` itself or its kebab-to-camelCase JS-identifier form.
129
119
  * Handles the `name`, `name as alias`, and inline-type (`type name`,
@@ -134,7 +124,7 @@ function manifestNameToBinding(pluginName) {
134
124
  function specifierMatchesPlugin(spec, pluginName) {
135
125
  const head = spec.replace(/^type\s+/, "").trim().split(/\s+as\s+/)[0]?.trim();
136
126
  if (!head) return false;
137
- return head === pluginName || head === manifestNameToBinding(pluginName);
127
+ return head === pluginName || head === kebabToCamel(pluginName);
138
128
  }
139
129
  /**
140
130
  * Rewrite imports of `pluginName` from `<pkg><oldSuffix>` to `<pkg><newSuffix>`
@@ -1 +1 @@
1
- {"version":3,"file":"promote.js","names":[],"sources":["../../../../../src/cli/commands/plugin/promote/promote.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport { Command } from \"commander\";\nimport { resolveManifestInDir } from \"../manifest-resolve\";\nimport { isWithinDirectory } from \"../sync/sync\";\nimport { shouldAllowJsManifestForDir } from \"../trusted-js-manifest\";\n\ntype Stability = \"beta\" | \"ga\";\n\nconst TIER_ORDER: Record<Stability, number> = {\n beta: 0,\n ga: 1,\n};\n\nconst IMPORT_PATH_MAP: Record<Stability, string> = {\n beta: \"/beta\",\n ga: \"\",\n};\n\n/** Aligned with sync.ts and list.ts; keep all plugin-tree walks at the same cap. */\nconst MAX_SCAN_DEPTH = 5;\n\n/**\n * Directories that should never be walked when discovering plugin manifests\n * or rewriting imports. Mirrors common build/output trees.\n */\nconst SKIP_DIRECTORIES = new Set([\n \"node_modules\",\n \"dist\",\n \"build\",\n \"out\",\n \".git\",\n \".turbo\",\n \".next\",\n \".nuxt\",\n \".cache\",\n \".svelte-kit\",\n \".vite\",\n \".parcel-cache\",\n \"coverage\",\n]);\n\n/**\n * Plugin name charset accepted by the promote command. Mirrors npm package\n * naming (lowercase, dashes, underscores, dots, optional @scope/) and explicitly\n * forbids path separators, traversal, and NUL — so the name cannot escape its\n * intended directory when used in path.join().\n */\nconst PLUGIN_NAME_PATTERN =\n /^(?:@[a-z0-9][a-z0-9._-]*\\/)?[a-z0-9][a-z0-9._-]*$/i;\n\nfunction validatePluginName(pluginName: string): void {\n if (!pluginName || pluginName.includes(\"\\0\") || pluginName.includes(\"..\")) {\n throw new Error(\n `Invalid plugin name \"${pluginName}\". Plugin names must not contain \"..\", or null bytes.`,\n );\n }\n // Backslash is never allowed (treated as a path separator on Windows).\n if (pluginName.includes(\"\\\\\")) {\n throw new Error(\n `Invalid plugin name \"${pluginName}\". Plugin names must not contain backslashes.`,\n );\n }\n // Forward slash is only allowed inside the @scope/name form.\n if (pluginName.includes(\"/\") && !pluginName.startsWith(\"@\")) {\n throw new Error(\n `Invalid plugin name \"${pluginName}\". Plugin names must not contain \"/\" unless they are a scoped package (e.g. @scope/name).`,\n );\n }\n if (!PLUGIN_NAME_PATTERN.test(pluginName)) {\n throw new Error(\n `Invalid plugin name \"${pluginName}\". Expected lowercase alphanumeric with optional dashes, underscores, dots, or @scope/ prefix.`,\n );\n }\n}\n\nfunction isStability(value: unknown): value is Stability {\n return value === \"beta\" || value === \"ga\";\n}\n\nfunction findPluginManifest(\n pluginName: string,\n cwd: string,\n): { manifestPath: string; isLocal: boolean } | null {\n const dirsToScan = [\"plugins\", \"server\", \".\"];\n\n for (const dir of dirsToScan) {\n const absDir = path.resolve(cwd, dir);\n const result = scanDirForPlugin(absDir, pluginName, cwd, 0);\n if (result) return { manifestPath: result, isLocal: true };\n }\n\n // node_modules fallback. Validate and bound to the dist/plugins subtree to\n // make sure a malicious or typo'd name cannot escape via path.join.\n const nodeModulesDir = path.join(cwd, \"node_modules\", \"@databricks/appkit\");\n if (fs.existsSync(nodeModulesDir)) {\n const pluginsDir = path.join(nodeModulesDir, \"dist\", \"plugins\");\n if (fs.existsSync(pluginsDir)) {\n const manifestPath = path.resolve(\n pluginsDir,\n pluginName,\n \"manifest.json\",\n );\n if (\n isWithinDirectory(manifestPath, pluginsDir) &&\n fs.existsSync(manifestPath)\n ) {\n return { manifestPath, isLocal: false };\n }\n }\n }\n\n return null;\n}\n\nfunction scanDirForPlugin(\n dir: string,\n pluginName: string,\n cwd: string,\n depth: number,\n): string | null {\n if (depth >= MAX_SCAN_DEPTH) return null;\n let stat: fs.Stats;\n try {\n stat = fs.statSync(dir);\n } catch {\n return null;\n }\n if (!stat.isDirectory()) return null;\n\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n if (SKIP_DIRECTORIES.has(entry.name)) continue;\n if (entry.isSymbolicLink()) continue;\n const childPath = path.join(dir, entry.name);\n const allowJs = shouldAllowJsManifestForDir(childPath);\n const resolved = resolveManifestInDir(childPath, {\n allowJsManifest: allowJs,\n });\n\n if (resolved) {\n try {\n const obj = loadManifestFromFileSync(resolved.path);\n if (\n obj &&\n typeof obj === \"object\" &&\n \"name\" in obj &&\n (obj as { name: string }).name === pluginName\n ) {\n return resolved.path;\n }\n } catch {\n // skip unreadable / invalid manifest\n }\n continue;\n }\n\n const deeper = scanDirForPlugin(childPath, pluginName, cwd, depth + 1);\n if (deeper) return deeper;\n }\n return null;\n}\n\nfunction loadManifestFromFileSync(filePath: string): unknown {\n const raw = fs.readFileSync(filePath, \"utf-8\");\n return JSON.parse(raw);\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Convert a kebab-case manifest name to its camelCase JS identifier form\n * (e.g. `vector-search` -> `vectorSearch`). Mirrors the convention used by\n * first-party plugin index files: a manifest's `name` field may be\n * kebab-case (the schema permits `^[a-z][a-z0-9-]*$`), but the actual\n * exported binding is always a JS identifier. We try both forms when\n * matching specifiers in user code.\n */\nfunction manifestNameToBinding(pluginName: string): string {\n return pluginName.replace(/-+([a-z0-9])/g, (_, c: string) => c.toUpperCase());\n}\n\n/**\n * Returns true when the named import specifier `spec` resolves to either\n * `pluginName` itself or its kebab-to-camelCase JS-identifier form.\n * Handles the `name`, `name as alias`, and inline-type (`type name`,\n * `type name as alias`) forms. Matches on the imported binding before any\n * `as` rename so a promotion finds the right specifier regardless of how\n * the user aliased it.\n */\nfunction specifierMatchesPlugin(spec: string, pluginName: string): boolean {\n const stripped = spec.replace(/^type\\s+/, \"\").trim();\n const head = stripped.split(/\\s+as\\s+/)[0]?.trim();\n if (!head) return false;\n return head === pluginName || head === manifestNameToBinding(pluginName);\n}\n\n/**\n * Rewrite imports of `pluginName` from `<pkg><oldSuffix>` to `<pkg><newSuffix>`\n * across one file, leaving every OTHER specifier on the same import line at\n * its original source. The naïve `split/join` approach this replaced was\n * promoting *every* beta specifier in the file along with the targeted\n * plugin — a bug because beta specifiers don't exist at the GA subpath.\n *\n * Behaviour:\n * - If the targeted plugin is the only specifier on an import line, the\n * line's source is rewritten to the new path.\n * - If the import has multiple specifiers, the targeted one is moved to a\n * newly-emitted import line at the new source, and the original import\n * keeps the remaining specifiers at the old source.\n * - Imports that don't reference `pluginName` are left untouched.\n *\n * Multi-line specifier lists, type-only imports (`import type { ... }`),\n * inline-type specifiers (`import { type Foo }`), and `as`-aliased\n * specifiers are all preserved through the rewrite.\n */\nfunction rewriteImportsInFile(\n filePath: string,\n pluginName: string,\n oldSuffix: string,\n newSuffix: string,\n dryRun: boolean,\n): { file: string; from: string; to: string } | null {\n const content = fs.readFileSync(filePath, \"utf-8\");\n\n const packages = [\n \"@databricks/appkit\",\n \"@databricks/appkit-ui/js\",\n \"@databricks/appkit-ui/react\",\n ];\n let updated = content;\n let changed = false;\n\n for (const pkg of packages) {\n const oldPath = `${pkg}${oldSuffix}`;\n const newPath = `${pkg}${newSuffix}`;\n\n // Match: `import [type] { ...specifiers... } from \"<oldPath>\";`\n // - `[^}]*` lets the specifier list span newlines (safe — TS imports\n // don't have nested braces inside the specifier list).\n // - Captures the `type ` keyword (if any), the specifier body, and\n // the surrounding quote style so we can preserve them on output.\n const importRe = new RegExp(\n `import\\\\s+(type\\\\s+)?\\\\{([^}]*)\\\\}\\\\s*from\\\\s*([\"'])${escapeRegex(oldPath)}\\\\3\\\\s*;?`,\n \"g\",\n );\n\n updated = updated.replace(\n importRe,\n (full, typeKeyword, specifiers, quote) => {\n const specList: string[] = specifiers\n .split(\",\")\n .map((s: string) => s.trim())\n .filter((s: string) => s.length > 0);\n\n const promotedSpec = specList.find((s) =>\n specifierMatchesPlugin(s, pluginName),\n );\n if (!promotedSpec) {\n // Plugin not in this import — leave it alone.\n return full;\n }\n\n const remaining = specList.filter(\n (s) => !specifierMatchesPlugin(s, pluginName),\n );\n changed = true;\n\n const tk = typeKeyword ?? \"\";\n const promotedImport = `import ${tk}{ ${promotedSpec} } from ${quote}${newPath}${quote};`;\n\n if (remaining.length === 0) {\n // Only specifier — just rewrite the source.\n return promotedImport;\n }\n\n const remainingImport = `import ${tk}{ ${remaining.join(\", \")} } from ${quote}${oldPath}${quote};`;\n return `${remainingImport}\\n${promotedImport}`;\n },\n );\n }\n\n if (!changed) return null;\n\n if (!dryRun) {\n fs.writeFileSync(filePath, updated);\n }\n\n return {\n file: filePath,\n from: oldSuffix || \"(root)\",\n to: newSuffix || \"(root)\",\n };\n}\n\nfunction findTsFiles(dir: string, projectRoot: string, depth = 0): string[] {\n if (depth >= MAX_SCAN_DEPTH) return [];\n\n const results: string[] = [];\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return results;\n }\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isSymbolicLink()) continue;\n if (entry.isDirectory()) {\n if (SKIP_DIRECTORIES.has(entry.name)) continue;\n // Boundary check to ensure recursion stays inside the project root,\n // even if a future change introduces a symlink-following path.\n if (!isWithinDirectory(fullPath, projectRoot)) continue;\n results.push(...findTsFiles(fullPath, projectRoot, depth + 1));\n } else if (entry.name.endsWith(\".ts\") || entry.name.endsWith(\".tsx\")) {\n results.push(fullPath);\n }\n }\n\n return results;\n}\n\nasync function runPromote(\n pluginName: string,\n options: {\n to: string;\n dryRun?: boolean;\n skipImports?: boolean;\n skipSync?: boolean;\n allowInstalled?: boolean;\n },\n): Promise<void> {\n validatePluginName(pluginName);\n\n const cwd = process.cwd();\n\n if (!isStability(options.to)) {\n console.error(\n `Invalid target tier \"${options.to}\". Must be one of: beta, ga.`,\n );\n process.exit(1);\n }\n const target: Stability = options.to;\n\n const found = findPluginManifest(pluginName, cwd);\n if (!found) {\n console.error(\n `Plugin \"${pluginName}\" not found. Searched local dirs (plugins, server, .) and node_modules.`,\n );\n process.exit(1);\n }\n\n const { manifestPath, isLocal } = found;\n\n if (!isLocal && !options.allowInstalled) {\n console.error(\n `Plugin \"${pluginName}\" was only found under node_modules (${path.relative(cwd, manifestPath)}).\\n` +\n `Refusing to mutate an installed package — re-install would overwrite the change.\\n` +\n `Pass --allow-installed to override (advanced; not recommended).`,\n );\n process.exit(1);\n }\n\n let raw: Record<string, unknown>;\n try {\n const parsed = loadManifestFromFileSync(manifestPath);\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n console.error(\n `Manifest at ${path.relative(cwd, manifestPath)} is not a JSON object.`,\n );\n process.exit(1);\n }\n raw = parsed as Record<string, unknown>;\n } catch (err) {\n console.error(\n `Failed to read manifest at ${path.relative(cwd, manifestPath)}: ${err instanceof Error ? err.message : String(err)}`,\n );\n process.exit(1);\n }\n\n const rawStability = raw.stability ?? \"ga\";\n if (!isStability(rawStability)) {\n console.error(\n `Manifest at ${path.relative(cwd, manifestPath)} has an invalid stability value \"${String(rawStability)}\". ` +\n `Must be one of: beta, ga (or omitted for ga).`,\n );\n process.exit(1);\n }\n const currentStability: Stability = rawStability;\n\n if (currentStability === target) {\n console.error(\n `Plugin \"${pluginName}\" is already at \"${target}\". Nothing to do.`,\n );\n process.exit(1);\n }\n\n if (TIER_ORDER[target] <= TIER_ORDER[currentStability]) {\n console.error(\n `Cannot demote \"${pluginName}\" from \"${currentStability}\" to \"${target}\". Promotion is one-way only.`,\n );\n process.exit(1);\n }\n\n const prefix = options.dryRun ? \"[dry-run] \" : \"\";\n\n if (target === \"ga\") {\n delete raw.stability;\n } else {\n raw.stability = target;\n }\n\n if (!options.dryRun) {\n fs.writeFileSync(manifestPath, `${JSON.stringify(raw, null, 2)}\\n`);\n }\n console.log(\n `${prefix}Updated manifest: ${path.relative(cwd, manifestPath)} (${currentStability} → ${target})`,\n );\n\n const importRewrites: { file: string; from: string; to: string }[] = [];\n if (!options.skipImports) {\n const oldSuffix = IMPORT_PATH_MAP[currentStability];\n const newSuffix = IMPORT_PATH_MAP[target];\n\n const tsFiles = findTsFiles(cwd, cwd);\n for (const file of tsFiles) {\n const result = rewriteImportsInFile(\n file,\n pluginName,\n oldSuffix,\n newSuffix,\n Boolean(options.dryRun),\n );\n if (result) {\n importRewrites.push(result);\n console.log(\n `${prefix}Rewritten imports in: ${path.relative(cwd, file)}`,\n );\n }\n }\n\n if (importRewrites.length === 0) {\n console.log(`${prefix}No import paths to rewrite.`);\n }\n }\n\n if (!options.skipSync && !options.dryRun) {\n // Monorepo-only: regenerate every artifact derived from plugin\n // manifests (ga/beta export barrels AND the per-plugin docs-page\n // stability banner) so all manifest-derived layers move together.\n // No-op outside the AppKit monorepo (third-party plugin projects don't\n // ship the generators).\n const generatorPath = path.join(cwd, \"tools\", \"generate-plugin-entries.ts\");\n if (fs.existsSync(generatorPath)) {\n console.log(`\\n${prefix}Regenerating manifest-derived artifacts...`);\n const { execSync } = await import(\"node:child_process\");\n try {\n execSync(\"pnpm run generate:types\", { cwd, stdio: \"inherit\" });\n } catch {\n console.error(\n `Error: post-promote 'generate:types' failed. ` +\n `Manifest was updated, but generated barrels and docs banners may be stale. ` +\n `Run 'pnpm run generate:types' manually.`,\n );\n process.exit(1);\n }\n }\n\n console.log(`\\n${prefix}Running plugin sync...`);\n const { execSync } = await import(\"node:child_process\");\n // Monorepo flavor: the AppKit monorepo's `pnpm sync:template` script\n // points sync at `template/appkit.plugins.json` (the file shipped to\n // consumers and read by the Go init template), not the project-root\n // default. Detect the monorepo via the same generator-path probe used\n // above and prefer the script when available so the manifest, the\n // synced template, and the runtime barrels stay aligned.\n const syncCommand = fs.existsSync(generatorPath)\n ? \"pnpm run sync:template\"\n : \"npx appkit plugin sync --write\";\n try {\n execSync(syncCommand, {\n cwd,\n stdio: \"inherit\",\n });\n } catch {\n console.error(\n `Error: post-promote sync ('${syncCommand}') failed. ` +\n `Manifest and imports were updated, but the synced plugin manifest ` +\n `may be stale. Run '${syncCommand}' manually.`,\n );\n process.exit(1);\n }\n }\n\n console.log(\n `\\n${prefix}Promotion complete: ${pluginName} ${currentStability} → ${target}`,\n );\n if (importRewrites.length > 0) {\n console.log(` ${importRewrites.length} file(s) with import rewrites`);\n }\n}\n\n/** Exported for testing. */\nexport {\n PLUGIN_NAME_PATTERN,\n TIER_ORDER,\n IMPORT_PATH_MAP,\n SKIP_DIRECTORIES,\n isStability,\n validatePluginName,\n rewriteImportsInFile,\n runPromote,\n};\n\nexport const pluginPromoteCommand = new Command(\"promote\")\n .description(\"Promote a plugin to a higher stability tier\")\n .argument(\"<plugin-name>\", \"Plugin name to promote\")\n .requiredOption(\"--to <tier>\", \"Target stability tier (beta, ga)\")\n .option(\"--dry-run\", \"Show what would change without modifying files\")\n .option(\"--skip-imports\", \"Only update manifest, skip import path rewriting\")\n .option(\"--skip-sync\", \"Don't auto-run plugin sync after promotion\")\n .option(\n \"--allow-installed\",\n \"Allow promoting a plugin that lives only under node_modules (advanced)\",\n )\n .action((pluginName, opts) =>\n runPromote(pluginName, opts).catch((err) => {\n const message = err instanceof Error ? err.message : String(err);\n console.error(`Error: ${message}`);\n if (process.env.DEBUG) console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}
1
+ {"version":3,"file":"promote.js","names":[],"sources":["../../../../../src/cli/commands/plugin/promote/promote.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport { Command } from \"commander\";\nimport { kebabToCamel } from \"../../../../naming\";\nimport { resolveManifestInDir } from \"../manifest-resolve\";\nimport { isWithinDirectory } from \"../sync/sync\";\nimport { shouldAllowJsManifestForDir } from \"../trusted-js-manifest\";\n\ntype Stability = \"beta\" | \"ga\";\n\nconst TIER_ORDER: Record<Stability, number> = {\n beta: 0,\n ga: 1,\n};\n\nconst IMPORT_PATH_MAP: Record<Stability, string> = {\n beta: \"/beta\",\n ga: \"\",\n};\n\n/** Aligned with sync.ts and list.ts; keep all plugin-tree walks at the same cap. */\nconst MAX_SCAN_DEPTH = 5;\n\n/**\n * Directories that should never be walked when discovering plugin manifests\n * or rewriting imports. Mirrors common build/output trees.\n */\nconst SKIP_DIRECTORIES = new Set([\n \"node_modules\",\n \"dist\",\n \"build\",\n \"out\",\n \".git\",\n \".turbo\",\n \".next\",\n \".nuxt\",\n \".cache\",\n \".svelte-kit\",\n \".vite\",\n \".parcel-cache\",\n \"coverage\",\n]);\n\n/**\n * Plugin name charset accepted by the promote command. Mirrors npm package\n * naming (lowercase, dashes, underscores, dots, optional @scope/) and explicitly\n * forbids path separators, traversal, and NUL — so the name cannot escape its\n * intended directory when used in path.join().\n */\nconst PLUGIN_NAME_PATTERN =\n /^(?:@[a-z0-9][a-z0-9._-]*\\/)?[a-z0-9][a-z0-9._-]*$/i;\n\nfunction validatePluginName(pluginName: string): void {\n if (!pluginName || pluginName.includes(\"\\0\") || pluginName.includes(\"..\")) {\n throw new Error(\n `Invalid plugin name \"${pluginName}\". Plugin names must not contain \"..\", or null bytes.`,\n );\n }\n // Backslash is never allowed (treated as a path separator on Windows).\n if (pluginName.includes(\"\\\\\")) {\n throw new Error(\n `Invalid plugin name \"${pluginName}\". Plugin names must not contain backslashes.`,\n );\n }\n // Forward slash is only allowed inside the @scope/name form.\n if (pluginName.includes(\"/\") && !pluginName.startsWith(\"@\")) {\n throw new Error(\n `Invalid plugin name \"${pluginName}\". Plugin names must not contain \"/\" unless they are a scoped package (e.g. @scope/name).`,\n );\n }\n if (!PLUGIN_NAME_PATTERN.test(pluginName)) {\n throw new Error(\n `Invalid plugin name \"${pluginName}\". Expected lowercase alphanumeric with optional dashes, underscores, dots, or @scope/ prefix.`,\n );\n }\n}\n\nfunction isStability(value: unknown): value is Stability {\n return value === \"beta\" || value === \"ga\";\n}\n\nfunction findPluginManifest(\n pluginName: string,\n cwd: string,\n): { manifestPath: string; isLocal: boolean } | null {\n const dirsToScan = [\"plugins\", \"server\", \".\"];\n\n for (const dir of dirsToScan) {\n const absDir = path.resolve(cwd, dir);\n const result = scanDirForPlugin(absDir, pluginName, cwd, 0);\n if (result) return { manifestPath: result, isLocal: true };\n }\n\n // node_modules fallback. Validate and bound to the dist/plugins subtree to\n // make sure a malicious or typo'd name cannot escape via path.join.\n const nodeModulesDir = path.join(cwd, \"node_modules\", \"@databricks/appkit\");\n if (fs.existsSync(nodeModulesDir)) {\n const pluginsDir = path.join(nodeModulesDir, \"dist\", \"plugins\");\n if (fs.existsSync(pluginsDir)) {\n const manifestPath = path.resolve(\n pluginsDir,\n pluginName,\n \"manifest.json\",\n );\n if (\n isWithinDirectory(manifestPath, pluginsDir) &&\n fs.existsSync(manifestPath)\n ) {\n return { manifestPath, isLocal: false };\n }\n }\n }\n\n return null;\n}\n\nfunction scanDirForPlugin(\n dir: string,\n pluginName: string,\n cwd: string,\n depth: number,\n): string | null {\n if (depth >= MAX_SCAN_DEPTH) return null;\n let stat: fs.Stats;\n try {\n stat = fs.statSync(dir);\n } catch {\n return null;\n }\n if (!stat.isDirectory()) return null;\n\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n if (SKIP_DIRECTORIES.has(entry.name)) continue;\n if (entry.isSymbolicLink()) continue;\n const childPath = path.join(dir, entry.name);\n const allowJs = shouldAllowJsManifestForDir(childPath);\n const resolved = resolveManifestInDir(childPath, {\n allowJsManifest: allowJs,\n });\n\n if (resolved) {\n try {\n const obj = loadManifestFromFileSync(resolved.path);\n if (\n obj &&\n typeof obj === \"object\" &&\n \"name\" in obj &&\n (obj as { name: string }).name === pluginName\n ) {\n return resolved.path;\n }\n } catch {\n // skip unreadable / invalid manifest\n }\n continue;\n }\n\n const deeper = scanDirForPlugin(childPath, pluginName, cwd, depth + 1);\n if (deeper) return deeper;\n }\n return null;\n}\n\nfunction loadManifestFromFileSync(filePath: string): unknown {\n const raw = fs.readFileSync(filePath, \"utf-8\");\n return JSON.parse(raw);\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Returns true when the named import specifier `spec` resolves to either\n * `pluginName` itself or its kebab-to-camelCase JS-identifier form.\n * Handles the `name`, `name as alias`, and inline-type (`type name`,\n * `type name as alias`) forms. Matches on the imported binding before any\n * `as` rename so a promotion finds the right specifier regardless of how\n * the user aliased it.\n */\nfunction specifierMatchesPlugin(spec: string, pluginName: string): boolean {\n const stripped = spec.replace(/^type\\s+/, \"\").trim();\n const head = stripped.split(/\\s+as\\s+/)[0]?.trim();\n if (!head) return false;\n return head === pluginName || head === kebabToCamel(pluginName);\n}\n\n/**\n * Rewrite imports of `pluginName` from `<pkg><oldSuffix>` to `<pkg><newSuffix>`\n * across one file, leaving every OTHER specifier on the same import line at\n * its original source. The naïve `split/join` approach this replaced was\n * promoting *every* beta specifier in the file along with the targeted\n * plugin — a bug because beta specifiers don't exist at the GA subpath.\n *\n * Behaviour:\n * - If the targeted plugin is the only specifier on an import line, the\n * line's source is rewritten to the new path.\n * - If the import has multiple specifiers, the targeted one is moved to a\n * newly-emitted import line at the new source, and the original import\n * keeps the remaining specifiers at the old source.\n * - Imports that don't reference `pluginName` are left untouched.\n *\n * Multi-line specifier lists, type-only imports (`import type { ... }`),\n * inline-type specifiers (`import { type Foo }`), and `as`-aliased\n * specifiers are all preserved through the rewrite.\n */\nfunction rewriteImportsInFile(\n filePath: string,\n pluginName: string,\n oldSuffix: string,\n newSuffix: string,\n dryRun: boolean,\n): { file: string; from: string; to: string } | null {\n const content = fs.readFileSync(filePath, \"utf-8\");\n\n const packages = [\n \"@databricks/appkit\",\n \"@databricks/appkit-ui/js\",\n \"@databricks/appkit-ui/react\",\n ];\n let updated = content;\n let changed = false;\n\n for (const pkg of packages) {\n const oldPath = `${pkg}${oldSuffix}`;\n const newPath = `${pkg}${newSuffix}`;\n\n // Match: `import [type] { ...specifiers... } from \"<oldPath>\";`\n // - `[^}]*` lets the specifier list span newlines (safe — TS imports\n // don't have nested braces inside the specifier list).\n // - Captures the `type ` keyword (if any), the specifier body, and\n // the surrounding quote style so we can preserve them on output.\n const importRe = new RegExp(\n `import\\\\s+(type\\\\s+)?\\\\{([^}]*)\\\\}\\\\s*from\\\\s*([\"'])${escapeRegex(oldPath)}\\\\3\\\\s*;?`,\n \"g\",\n );\n\n updated = updated.replace(\n importRe,\n (full, typeKeyword, specifiers, quote) => {\n const specList: string[] = specifiers\n .split(\",\")\n .map((s: string) => s.trim())\n .filter((s: string) => s.length > 0);\n\n const promotedSpec = specList.find((s) =>\n specifierMatchesPlugin(s, pluginName),\n );\n if (!promotedSpec) {\n // Plugin not in this import — leave it alone.\n return full;\n }\n\n const remaining = specList.filter(\n (s) => !specifierMatchesPlugin(s, pluginName),\n );\n changed = true;\n\n const tk = typeKeyword ?? \"\";\n const promotedImport = `import ${tk}{ ${promotedSpec} } from ${quote}${newPath}${quote};`;\n\n if (remaining.length === 0) {\n // Only specifier — just rewrite the source.\n return promotedImport;\n }\n\n const remainingImport = `import ${tk}{ ${remaining.join(\", \")} } from ${quote}${oldPath}${quote};`;\n return `${remainingImport}\\n${promotedImport}`;\n },\n );\n }\n\n if (!changed) return null;\n\n if (!dryRun) {\n fs.writeFileSync(filePath, updated);\n }\n\n return {\n file: filePath,\n from: oldSuffix || \"(root)\",\n to: newSuffix || \"(root)\",\n };\n}\n\nfunction findTsFiles(dir: string, projectRoot: string, depth = 0): string[] {\n if (depth >= MAX_SCAN_DEPTH) return [];\n\n const results: string[] = [];\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return results;\n }\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isSymbolicLink()) continue;\n if (entry.isDirectory()) {\n if (SKIP_DIRECTORIES.has(entry.name)) continue;\n // Boundary check to ensure recursion stays inside the project root,\n // even if a future change introduces a symlink-following path.\n if (!isWithinDirectory(fullPath, projectRoot)) continue;\n results.push(...findTsFiles(fullPath, projectRoot, depth + 1));\n } else if (entry.name.endsWith(\".ts\") || entry.name.endsWith(\".tsx\")) {\n results.push(fullPath);\n }\n }\n\n return results;\n}\n\nasync function runPromote(\n pluginName: string,\n options: {\n to: string;\n dryRun?: boolean;\n skipImports?: boolean;\n skipSync?: boolean;\n allowInstalled?: boolean;\n },\n): Promise<void> {\n validatePluginName(pluginName);\n\n const cwd = process.cwd();\n\n if (!isStability(options.to)) {\n console.error(\n `Invalid target tier \"${options.to}\". Must be one of: beta, ga.`,\n );\n process.exit(1);\n }\n const target: Stability = options.to;\n\n const found = findPluginManifest(pluginName, cwd);\n if (!found) {\n console.error(\n `Plugin \"${pluginName}\" not found. Searched local dirs (plugins, server, .) and node_modules.`,\n );\n process.exit(1);\n }\n\n const { manifestPath, isLocal } = found;\n\n if (!isLocal && !options.allowInstalled) {\n console.error(\n `Plugin \"${pluginName}\" was only found under node_modules (${path.relative(cwd, manifestPath)}).\\n` +\n `Refusing to mutate an installed package — re-install would overwrite the change.\\n` +\n `Pass --allow-installed to override (advanced; not recommended).`,\n );\n process.exit(1);\n }\n\n let raw: Record<string, unknown>;\n try {\n const parsed = loadManifestFromFileSync(manifestPath);\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n console.error(\n `Manifest at ${path.relative(cwd, manifestPath)} is not a JSON object.`,\n );\n process.exit(1);\n }\n raw = parsed as Record<string, unknown>;\n } catch (err) {\n console.error(\n `Failed to read manifest at ${path.relative(cwd, manifestPath)}: ${err instanceof Error ? err.message : String(err)}`,\n );\n process.exit(1);\n }\n\n const rawStability = raw.stability ?? \"ga\";\n if (!isStability(rawStability)) {\n console.error(\n `Manifest at ${path.relative(cwd, manifestPath)} has an invalid stability value \"${String(rawStability)}\". ` +\n `Must be one of: beta, ga (or omitted for ga).`,\n );\n process.exit(1);\n }\n const currentStability: Stability = rawStability;\n\n if (currentStability === target) {\n console.error(\n `Plugin \"${pluginName}\" is already at \"${target}\". Nothing to do.`,\n );\n process.exit(1);\n }\n\n if (TIER_ORDER[target] <= TIER_ORDER[currentStability]) {\n console.error(\n `Cannot demote \"${pluginName}\" from \"${currentStability}\" to \"${target}\". Promotion is one-way only.`,\n );\n process.exit(1);\n }\n\n const prefix = options.dryRun ? \"[dry-run] \" : \"\";\n\n if (target === \"ga\") {\n delete raw.stability;\n } else {\n raw.stability = target;\n }\n\n if (!options.dryRun) {\n fs.writeFileSync(manifestPath, `${JSON.stringify(raw, null, 2)}\\n`);\n }\n console.log(\n `${prefix}Updated manifest: ${path.relative(cwd, manifestPath)} (${currentStability} → ${target})`,\n );\n\n const importRewrites: { file: string; from: string; to: string }[] = [];\n if (!options.skipImports) {\n const oldSuffix = IMPORT_PATH_MAP[currentStability];\n const newSuffix = IMPORT_PATH_MAP[target];\n\n const tsFiles = findTsFiles(cwd, cwd);\n for (const file of tsFiles) {\n const result = rewriteImportsInFile(\n file,\n pluginName,\n oldSuffix,\n newSuffix,\n Boolean(options.dryRun),\n );\n if (result) {\n importRewrites.push(result);\n console.log(\n `${prefix}Rewritten imports in: ${path.relative(cwd, file)}`,\n );\n }\n }\n\n if (importRewrites.length === 0) {\n console.log(`${prefix}No import paths to rewrite.`);\n }\n }\n\n if (!options.skipSync && !options.dryRun) {\n // Monorepo-only: regenerate every artifact derived from plugin\n // manifests (ga/beta export barrels AND the per-plugin docs-page\n // stability banner) so all manifest-derived layers move together.\n // No-op outside the AppKit monorepo (third-party plugin projects don't\n // ship the generators).\n const generatorPath = path.join(cwd, \"tools\", \"generate-plugin-entries.ts\");\n if (fs.existsSync(generatorPath)) {\n console.log(`\\n${prefix}Regenerating manifest-derived artifacts...`);\n const { execSync } = await import(\"node:child_process\");\n try {\n execSync(\"pnpm run generate:types\", { cwd, stdio: \"inherit\" });\n } catch {\n console.error(\n `Error: post-promote 'generate:types' failed. ` +\n `Manifest was updated, but generated barrels and docs banners may be stale. ` +\n `Run 'pnpm run generate:types' manually.`,\n );\n process.exit(1);\n }\n }\n\n console.log(`\\n${prefix}Running plugin sync...`);\n const { execSync } = await import(\"node:child_process\");\n // Monorepo flavor: the AppKit monorepo's `pnpm sync:template` script\n // points sync at `template/appkit.plugins.json` (the file shipped to\n // consumers and read by the Go init template), not the project-root\n // default. Detect the monorepo via the same generator-path probe used\n // above and prefer the script when available so the manifest, the\n // synced template, and the runtime barrels stay aligned.\n const syncCommand = fs.existsSync(generatorPath)\n ? \"pnpm run sync:template\"\n : \"npx appkit plugin sync --write\";\n try {\n execSync(syncCommand, {\n cwd,\n stdio: \"inherit\",\n });\n } catch {\n console.error(\n `Error: post-promote sync ('${syncCommand}') failed. ` +\n `Manifest and imports were updated, but the synced plugin manifest ` +\n `may be stale. Run '${syncCommand}' manually.`,\n );\n process.exit(1);\n }\n }\n\n console.log(\n `\\n${prefix}Promotion complete: ${pluginName} ${currentStability} → ${target}`,\n );\n if (importRewrites.length > 0) {\n console.log(` ${importRewrites.length} file(s) with import rewrites`);\n }\n}\n\n/** Exported for testing. */\nexport {\n PLUGIN_NAME_PATTERN,\n TIER_ORDER,\n IMPORT_PATH_MAP,\n SKIP_DIRECTORIES,\n isStability,\n validatePluginName,\n rewriteImportsInFile,\n runPromote,\n};\n\nexport const pluginPromoteCommand = new Command(\"promote\")\n .description(\"Promote a plugin to a higher stability tier\")\n .argument(\"<plugin-name>\", \"Plugin name to promote\")\n .requiredOption(\"--to <tier>\", \"Target stability tier (beta, ga)\")\n .option(\"--dry-run\", \"Show what would change without modifying files\")\n .option(\"--skip-imports\", \"Only update manifest, skip import path rewriting\")\n .option(\"--skip-sync\", \"Don't auto-run plugin sync after promotion\")\n .option(\n \"--allow-installed\",\n \"Allow promoting a plugin that lives only under node_modules (advanced)\",\n )\n .action((pluginName, opts) =>\n runPromote(pluginName, opts).catch((err) => {\n const message = err instanceof Error ? err.message : String(err);\n console.error(`Error: ${message}`);\n if (process.env.DEBUG) console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}