@databricks/appkit-ui 0.82.0 → 0.84.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 (62) hide show
  1. package/CLAUDE.md +21 -1
  2. package/dist/cli/commands/agent/eval.js +1 -1
  3. package/dist/cli/commands/generate-types.js +1 -1
  4. package/dist/cli/commands/plugin/sync/sync.js +28 -15
  5. package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
  6. package/dist/cli/commands/registry/add.js +3 -11
  7. package/dist/cli/commands/registry/add.js.map +1 -1
  8. package/dist/cli/commands/registry/config-writer.js +1 -1
  9. package/dist/schemas/manifest.d.ts +44 -1
  10. package/dist/schemas/manifest.d.ts.map +1 -1
  11. package/dist/schemas/manifest.js +122 -3
  12. package/dist/schemas/manifest.js.map +1 -1
  13. package/dist/shared/src/plugin.d.ts.map +1 -1
  14. package/docs/api/appkit/Class.AppKitError.md +2 -1
  15. package/docs/api/appkit/Class.AuthenticationError.md +1 -1
  16. package/docs/api/appkit/Class.ConfigurationError.md +1 -1
  17. package/docs/api/appkit/Class.ConnectionError.md +1 -1
  18. package/docs/api/appkit/Class.DatabaseValidationError.md +1 -1
  19. package/docs/api/appkit/Class.ExecutionError.md +1 -1
  20. package/docs/api/appkit/Class.IdentityExpiredError.md +190 -0
  21. package/docs/api/appkit/Class.InitializationError.md +1 -1
  22. package/docs/api/appkit/Class.Plugin.md +9 -13
  23. package/docs/api/appkit/Class.ServerError.md +1 -1
  24. package/docs/api/appkit/Class.ServiceContext.md +169 -0
  25. package/docs/api/appkit/Class.TunnelError.md +1 -1
  26. package/docs/api/appkit/Class.ValidationError.md +1 -1
  27. package/docs/api/appkit/Function.createApp.md +12 -12
  28. package/docs/api/appkit/Function.getCallerContext.md +12 -0
  29. package/docs/api/appkit/Function.getCurrentActorId.md +12 -0
  30. package/docs/api/appkit/Function.getCurrentPrincipalKey.md +12 -0
  31. package/docs/api/appkit/Function.getCurrentUserId.md +14 -0
  32. package/docs/api/appkit/Function.getExecutionContext.md +5 -3
  33. package/docs/api/appkit/Function.getUserContext.md +16 -0
  34. package/docs/api/appkit/Function.getWarehouseId.md +20 -0
  35. package/docs/api/appkit/Function.isInUserContext.md +12 -0
  36. package/docs/api/appkit/Function.isUserContext.md +20 -0
  37. package/docs/api/appkit/Function.runAgent.md +1 -1
  38. package/docs/api/appkit/Function.runInCallerContext.md +27 -0
  39. package/docs/api/appkit/Function.runInUserContext.md +29 -0
  40. package/docs/api/appkit/Interface.AgentDefinition.md +11 -0
  41. package/docs/api/appkit/Interface.AgentsPluginConfig.md +11 -0
  42. package/docs/api/appkit/Interface.CallerContext.md +54 -0
  43. package/docs/api/appkit/Interface.IndexConfig.md +1 -1
  44. package/docs/api/appkit/Interface.PluginManifest.md +33 -2
  45. package/docs/api/appkit/Interface.RegisteredAgent.md +11 -0
  46. package/docs/api/appkit/Interface.RunAgentInput.md +45 -1
  47. package/docs/api/appkit/TypeAlias.AgentAuth.md +8 -0
  48. package/docs/api/appkit/TypeAlias.AppKitApi.md +35 -0
  49. package/docs/api/appkit/TypeAlias.CallerPrincipal.md +13 -0
  50. package/docs/api/appkit/TypeAlias.ExecutionContext.md +9 -0
  51. package/docs/api/appkit/TypeAlias.ExecutionResult.md +65 -0
  52. package/docs/api/appkit/TypeAlias.ScopedPluginMap.md +12 -0
  53. package/docs/api/appkit/TypeAlias.UserContext.md +109 -0
  54. package/docs/api/appkit/TypeAlias.UserScopedApp.md +39 -0
  55. package/docs/api/appkit.md +20 -0
  56. package/docs/plugins/agents.md +45 -0
  57. package/docs/plugins/analytics.md +8 -0
  58. package/docs/plugins/execution-context.md +101 -50
  59. package/docs/plugins/lakebase.md +6 -82
  60. package/llms.txt +21 -1
  61. package/package.json +1 -1
  62. package/sbom.cdx.json +1 -1
package/CLAUDE.md CHANGED
@@ -49,7 +49,7 @@ npx @databricks/appkit docs <query>
49
49
  - [Caching](./docs/plugins/caching.md): AppKit provides both global and plugin-level caching capabilities.
50
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:
51
51
  - [Database plugin](./docs/plugins/database.md): This plugin is currently beta. APIs may change between minor releases. Import from @databricks/appkit/beta. See Plugin Stability Tiers.
52
- - [Execution context](./docs/plugins/execution-context.md): AppKit manages Databricks authentication via two contexts:
52
+ - [Execution context](./docs/plugins/execution-context.md): AppKit uses the service principal by default. Open a caller scope when an
53
53
  - [Files plugin](./docs/plugins/files.md): File operations against Databricks Unity Catalog Volumes. Supports listing, reading, downloading, uploading, deleting, and previewing files with built-in caching, retry, and timeout handling via the execution interceptor pipeline.
54
54
  - [Genie plugin](./docs/plugins/genie.md): Integrates Databricks AI/BI Genie spaces into your AppKit application, enabling natural language data queries via a conversational interface.
55
55
  - [Jobs plugin](./docs/plugins/jobs.md): Trigger and monitor Databricks Lakeflow Jobs from your AppKit application.
@@ -72,12 +72,14 @@ npx @databricks/appkit docs <query>
72
72
  - [Class: DatabaseValidationError](./docs/api/appkit/Class.DatabaseValidationError.md): Deliberate validation failure raised by a database mutation hook. Generated
73
73
  - [Class: DatabricksAdapter](./docs/api/appkit/Class.DatabricksAdapter.md): Adapter that talks directly to Databricks Model Serving /invocations endpoint.
74
74
  - [Class: ExecutionError](./docs/api/appkit/Class.ExecutionError.md): Error thrown when an operation execution fails.
75
+ - [Class: IdentityExpiredError](./docs/api/appkit/Class.IdentityExpiredError.md): The downstream service rejected the active caller's credentials.
75
76
  - [Class: InitializationError](./docs/api/appkit/Class.InitializationError.md): Error thrown when a service or component is not properly initialized.
76
77
  - [Class: MlflowClient](./docs/api/appkit/Class.MlflowClient.md): A thin client over the Databricks workspace REST API, owning the host + bearer
77
78
  - [Abstract Class: Plugin<TConfig>](./docs/api/appkit/Class.Plugin.md): Base abstract class for creating AppKit plugins.
78
79
  - [Class: PolicyDeniedError](./docs/api/appkit/Class.PolicyDeniedError.md): Thrown when a policy denies an action.
79
80
  - [Class: ResourceRegistry](./docs/api/appkit/Class.ResourceRegistry.md): Central registry for tracking plugin resource requirements.
80
81
  - [Class: ServerError](./docs/api/appkit/Class.ServerError.md): Error thrown when server lifecycle operations fail.
82
+ - [Class: ServiceContext](./docs/api/appkit/Class.ServiceContext.md): ServiceContext is a singleton that manages the service principal's
81
83
  - [Class: SupervisorApiAdapter](./docs/api/appkit/Class.SupervisorApiAdapter.md): Adapter that calls the Databricks AI Gateway Responses API
82
84
  - [Class: TunnelError](./docs/api/appkit/Class.TunnelError.md): Error thrown when remote tunnel operations fail.
83
85
  - [Class: ValidationError](./docs/api/appkit/Class.ValidationError.md): Error thrown when input validation fails.
@@ -122,22 +124,30 @@ npx @databricks/appkit docs <query>
122
124
  - [Function: fromSupervisorApi()](./docs/api/appkit/Function.fromSupervisorApi.md): Creates an AgentAdapter backed by the Databricks AI Gateway
123
125
  - [Function: functionToolToDefinition()](./docs/api/appkit/Function.functionToolToDefinition.md): Parameters
124
126
  - [Function: generateDatabaseCredential()](./docs/api/appkit/Function.generateDatabaseCredential.md): Generate OAuth credentials for Postgres database connection using the proper Postgres API.
127
+ - [Function: getCallerContext()](./docs/api/appkit/Function.getCallerContext.md): Get the caller context if one is active, otherwise undefined.
128
+ - [Function: getCurrentActorId()](./docs/api/appkit/Function.getCurrentActorId.md): The initiating user in a caller scope; no user actor exists in service scope.
129
+ - [Function: getCurrentPrincipalKey()](./docs/api/appkit/Function.getCurrentPrincipalKey.md): Get the principal key for future cache keying.
130
+ - [Function: getCurrentUserId()](./docs/api/appkit/Function.getCurrentUserId.md): Returns
125
131
  - [Function: getExecutionContext()](./docs/api/appkit/Function.getExecutionContext.md): Get the current execution context.
126
132
  - [Function: getLakebaseOrmConfig()](./docs/api/appkit/Function.getLakebaseOrmConfig.md): Get Lakebase connection configuration for ORMs that don't accept pg.Pool directly.
127
133
  - [Function: getLakebasePgConfig()](./docs/api/appkit/Function.getLakebasePgConfig.md): Get Lakebase connection configuration for PostgreSQL clients.
128
134
  - [Function: getPluginManifest()](./docs/api/appkit/Function.getPluginManifest.md): Loads and validates the manifest from a plugin constructor.
129
135
  - [Function: getResourceRequirements()](./docs/api/appkit/Function.getResourceRequirements.md): Gets the resource requirements from a plugin's manifest.
136
+ - [Function: getUserContext()](./docs/api/appkit/Function.getUserContext.md): Returns
130
137
  - [Function: getUsernameWithApiLookup()](./docs/api/appkit/Function.getUsernameWithApiLookup.md): Resolves the PostgreSQL username for a Lakebase connection.
138
+ - [Function: getWarehouseId()](./docs/api/appkit/Function.getWarehouseId.md): Get the configured SQL warehouse ID after app initialization.
131
139
  - [Function: getWorkspaceClient()](./docs/api/appkit/Function.getWorkspaceClient.md): Get workspace client from config or SDK default auth chain
132
140
  - [Function: id()](./docs/api/appkit/Function.id.md): Returns
133
141
  - [Function: includes()](./docs/api/appkit/Function.includes.md): Passes when the value contains substring.
134
142
  - [Function: integer()](./docs/api/appkit/Function.integer.md): Returns
135
143
  - [Function: isFunctionTool()](./docs/api/appkit/Function.isFunctionTool.md): Parameters
136
144
  - [Function: isHostedTool()](./docs/api/appkit/Function.isHostedTool.md): Parameters
145
+ - [Function: isInUserContext()](./docs/api/appkit/Function.isInUserContext.md): Check if currently running in a user context.
137
146
  - [Function: isJudgeConfigured()](./docs/api/appkit/Function.isJudgeConfigured.md): Returns
138
147
  - [Function: isSQLTypeMarker()](./docs/api/appkit/Function.isSQLTypeMarker.md): Type guard to check if a value is a SQL type marker
139
148
  - [Function: isSupervisorTool()](./docs/api/appkit/Function.isSupervisorTool.md): Type guard for HostedSupervisorTool. Used by the agents plugin
140
149
  - [Function: isToolkitEntry()](./docs/api/appkit/Function.isToolkitEntry.md): Type guard for ToolkitEntry — used by the agents plugin to differentiate
150
+ - [Function: isUserContext()](./docs/api/appkit/Function.isUserContext.md): Parameters
141
151
  - [Function: jsonb()](./docs/api/appkit/Function.jsonb.md): Returns
142
152
  - [Function: loadAgentFromFile()](./docs/api/appkit/Function.loadAgentFromFile.md): Loads a single markdown agent file and resolves its frontmatter against
143
153
  - [Function: loadAgentsFromDir()](./docs/api/appkit/Function.loadAgentsFromDir.md): Scans a directory for one subdirectory per agent, each containing
@@ -154,6 +164,8 @@ npx @databricks/appkit docs <query>
154
164
  - [Function: runAgent()](./docs/api/appkit/Function.runAgent.md): Standalone agent execution without createApp. Resolves the adapter, binds
155
165
  - [Function: runEval()](./docs/api/appkit/Function.runEval.md): Run a single eval against a driver. Never throws for assertion or agent
156
166
  - [Function: runEvalsInDir()](./docs/api/appkit/Function.runEvalsInDir.md): Discover, load, and run every eval under each agent's evals/ dir, driving
167
+ - [Function: runInCallerContext()](./docs/api/appkit/Function.runInCallerContext.md): Run a function with an immutable snapshot of the caller context.
168
+ - [Function: runInUserContext()](./docs/api/appkit/Function.runInUserContext.md): Type Parameters
157
169
  - [Function: runWithRetries()](./docs/api/appkit/Function.runWithRetries.md): Run attempt up to 1 + retries times, stopping as soon as it returns a
158
170
  - [Function: summarize()](./docs/api/appkit/Function.summarize.md): Parameters
159
171
  - [Function: text()](./docs/api/appkit/Function.text.md): Returns
@@ -175,6 +187,7 @@ npx @databricks/appkit docs <query>
175
187
  - [Interface: AutoInheritToolsConfig](./docs/api/appkit/Interface.AutoInheritToolsConfig.md): Auto-inherit configuration. When enabled for a given agent origin, agents
176
188
  - [Interface: BasePluginConfig](./docs/api/appkit/Interface.BasePluginConfig.md): Base configuration interface for AppKit plugins
177
189
  - [Interface: CacheConfig](./docs/api/appkit/Interface.CacheConfig.md): Configuration for the CacheInterceptor. Controls TTL, size limits, storage backend, and probabilistic cleanup.
190
+ - [Interface: CallerContext](./docs/api/appkit/Interface.CallerContext.md): Caller identity and workspace for one immutable execution scope.
178
191
  - [Interface: CustomJudgeSpec](./docs/api/appkit/Interface.CustomJudgeSpec.md): A custom LLM-judge definition: a prompt template and choice→score mapping.
179
192
  - [Interface: DatabaseCredential](./docs/api/appkit/Interface.DatabaseCredential.md): Database credentials with OAuth token for Postgres connection
180
193
  - [Interface: DatabaseRegistry](./docs/api/appkit/Interface.DatabaseRegistry.md): CANONICAL augmentation target. Empty by default; the generated database.d.ts
@@ -257,11 +270,14 @@ npx @databricks/appkit docs <query>
257
270
  - [Interface: WorkspaceClient](./docs/api/appkit/Interface.WorkspaceClient.md): AppKit's workspace client facade. Mirrors the multi-client shape of the
258
271
  - [Interface: WorkspaceClientLike](./docs/api/appkit/Interface.WorkspaceClientLike.md): Structural shape of a Databricks SDK client used by fromSupervisorApi.
259
272
  - [Interface: WorkspaceClientOptions](./docs/api/appkit/Interface.WorkspaceClientOptions.md): Options used to construct the wrapper. Mirrors the subset of the old SDK's
273
+ - [Type Alias: AgentAuth](./docs/api/appkit/TypeAlias.AgentAuth.md): Identity an agent runs under. The only value is on-behalf-of-user.
260
274
  - [Type Alias: AgentEvent](./docs/api/appkit/TypeAlias.AgentEvent.md): Type Declaration
261
275
  - [Type Alias: AgentTool](./docs/api/appkit/TypeAlias.AgentTool.md): Any tool an agent can invoke: inline function tools (tool()), hosted MCP
262
276
  - [Type Alias: AgentTools](./docs/api/appkit/TypeAlias.AgentTools.md): Per-agent tool record. String keys map to inline tools, toolkit entries,
263
277
  - [Type Alias: AgentToolsFn()](./docs/api/appkit/TypeAlias.AgentToolsFn.md): Function form of AgentDefinition.tools. Receives the typed
278
+ - [Type Alias: AppKitApi<U>](./docs/api/appkit/TypeAlias.AppKitApi.md): App instance with plugin exports and an explicit caller-scoped entry point.
264
279
  - [Type Alias: BaseSystemPromptOption](./docs/api/appkit/TypeAlias.BaseSystemPromptOption.md)
280
+ - [Type Alias: CallerPrincipal](./docs/api/appkit/TypeAlias.CallerPrincipal.md): The caller identity whose permissions authorize execution, not its resources.
265
281
  - [Type Alias: ConfigSchema](./docs/api/appkit/TypeAlias.ConfigSchema.md): Configuration schema definition for plugin config.
266
282
  - [Type Alias: DatabaseApiConfig<TSchema>](./docs/api/appkit/TypeAlias.DatabaseApiConfig.md): Full generated CRUD for every declared table by default. Set false to disable
267
283
  - [Type Alias: DatabaseApiWriteOperation](./docs/api/appkit/TypeAlias.DatabaseApiWriteOperation.md): Generated HTTP write operations.
@@ -269,6 +285,7 @@ npx @databricks/appkit docs <query>
269
285
  - [Type Alias: DatabaseExports](./docs/api/appkit/TypeAlias.DatabaseExports.md): Typed database API published by the plugin.
270
286
  - [Type Alias: EntityHooks<TTable>](./docs/api/appkit/TypeAlias.EntityHooks.md): Response shaping and mutation lifecycle declared for one table.
271
287
  - [Type Alias: EvalProgress](./docs/api/appkit/TypeAlias.EvalProgress.md)
288
+ - [Type Alias: ExecutionContext](./docs/api/appkit/TypeAlias.ExecutionContext.md)
272
289
  - [Type Alias: ExecutionResult<T>](./docs/api/appkit/TypeAlias.ExecutionResult.md): Discriminated union for plugin execution results.
273
290
  - [Type Alias: FileAction](./docs/api/appkit/TypeAlias.FileAction.md): Every action the files plugin can perform.
274
291
  - [Type Alias: FilePolicy()](./docs/api/appkit/TypeAlias.FilePolicy.md): A policy function that decides whether user may perform action on
@@ -283,6 +300,7 @@ npx @databricks/appkit docs <query>
283
300
  - [Type Alias: ResolvedToolEntry](./docs/api/appkit/TypeAlias.ResolvedToolEntry.md): Internal tool-index entry after a tool record has been resolved to a dispatchable form.
284
301
  - [Type Alias: ResourceFieldEntry](./docs/api/appkit/TypeAlias.ResourceFieldEntry.md)
285
302
  - [Type Alias: ResourcePermission](./docs/api/appkit/TypeAlias.ResourcePermission.md): Union of all possible permission levels across all resource types.
303
+ - [Type Alias: ScopedPluginMap<U>](./docs/api/appkit/TypeAlias.ScopedPluginMap.md): Type Parameters
286
304
  - [Type Alias: SearchFilters](./docs/api/appkit/TypeAlias.SearchFilters.md)
287
305
  - [Type Alias: ServingFactory](./docs/api/appkit/TypeAlias.ServingFactory.md): Factory function returned by AppKit.serving.
288
306
  - [Type Alias: Severity](./docs/api/appkit/TypeAlias.Severity.md): Whether an assertion fails the eval (gate) or is tracked only (soft).
@@ -290,6 +308,8 @@ npx @databricks/appkit docs <query>
290
308
  - [Type Alias: ToolRegistry](./docs/api/appkit/TypeAlias.ToolRegistry.md)
291
309
  - [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.
292
310
  - [Type Alias: TransactionClient](./docs/api/appkit/TypeAlias.TransactionClient.md): Entity and SQL capabilities bound to one transaction.
311
+ - [Type Alias: UserContext](./docs/api/appkit/TypeAlias.UserContext.md): Deprecated
312
+ - [Type Alias: UserScopedApp<U>](./docs/api/appkit/TypeAlias.UserScopedApp.md): Type Declaration
293
313
  - [Variable: agents](./docs/api/appkit/Variable.agents.md): Plugin factory for the agents plugin. Discovers agents from
294
314
  - [Variable: aiSearch](./docs/api/appkit/Variable.aiSearch.md)
295
315
  - [Variable: READ_ACTIONS](./docs/api/appkit/Variable.READ_ACTIONS.md): Actions that only read data.
@@ -1,6 +1,6 @@
1
+ import { spawn } from "node:child_process";
1
2
  import fs from "node:fs";
2
3
  import { Command, Option } from "commander";
3
- import { spawn } from "node:child_process";
4
4
 
5
5
  //#region src/cli/commands/agent/eval.ts
6
6
  /**
@@ -1,9 +1,9 @@
1
1
  import { METRIC_CONFIG_FILE } from "../../schemas/metric-fqn.js";
2
2
  import { acquireSpawnLock, getSpawnLockPath, releaseSpawnLock } from "./spawn-lock.js";
3
+ import { spawn } from "node:child_process";
3
4
  import fs from "node:fs";
4
5
  import path from "node:path";
5
6
  import { Command, Option } from "commander";
6
- import { spawn } from "node:child_process";
7
7
 
8
8
  //#region src/cli/commands/generate-types.ts
9
9
  /**
@@ -1,4 +1,4 @@
1
- import { TEMPLATE_SCAFFOLDING, templateFieldEntrySchema } from "../../../../schemas/manifest.js";
1
+ import { APP_ONLY_RESOURCE_TYPES, DABS_BINDING_BY_TYPE, SCOPE_BY_TYPE, TEMPLATE_SCAFFOLDING, templateFieldEntrySchema } from "../../../../schemas/manifest.js";
2
2
  import { loadManifestFromFile, resolveManifestInDir } from "../manifest-resolve.js";
3
3
  import { formatValidationErrors, validateManifest } from "../validate/validate-manifest.js";
4
4
  import { shouldAllowJsManifestForPackage } from "../trusted-js-manifest.js";
@@ -41,17 +41,40 @@ const MAX_SCAN_DEPTH = 5;
41
41
  async function loadPluginEntry(resolved, pkg, allowJsManifest) {
42
42
  const manifest = validateManifestWithSchema(await loadManifestFromFile(resolved.path, resolved.type, { allowJsManifest }), resolved.path);
43
43
  if (!manifest || manifest.hidden) return null;
44
- return [manifest.name, {
44
+ return [manifest.name, toTemplatePlugin(manifest, pkg)];
45
+ }
46
+ /** Bake the type-level execution facts into the resource so the CLI reads plain data. */
47
+ function withExecutionCapabilities(resource) {
48
+ const scope = Object.hasOwn(SCOPE_BY_TYPE, resource.type) ? SCOPE_BY_TYPE[resource.type] : void 0;
49
+ const binding = Object.hasOwn(DABS_BINDING_BY_TYPE, resource.type) ? DABS_BINDING_BY_TYPE[resource.type] : void 0;
50
+ return {
51
+ ...resource,
52
+ ...scope && { scope },
53
+ ...APP_ONLY_RESOURCE_TYPES.has(resource.type) && { appOnly: true },
54
+ ...binding && { binding: {
55
+ yamlKey: binding.yamlKey,
56
+ varFields: binding.varFields.map((pair) => [pair[0], pair[1]]),
57
+ ...binding.staticFields && { staticFields: binding.staticFields.map((pair) => [pair[0], pair[1]]) }
58
+ } }
59
+ };
60
+ }
61
+ /** Build a TemplatePlugin entry. Every discovery path goes through here. */
62
+ function toTemplatePlugin(manifest, pkg) {
63
+ return {
45
64
  name: manifest.name,
46
65
  displayName: manifest.displayName,
47
66
  description: manifest.description,
48
67
  package: pkg,
49
- resources: manifest.resources,
68
+ resources: {
69
+ required: manifest.resources.required.map((resource) => withExecutionCapabilities(resource)),
70
+ optional: manifest.resources.optional.map((resource) => withExecutionCapabilities(resource))
71
+ },
72
+ ...manifest.scopes?.length && { scopes: manifest.scopes },
50
73
  ...manifest.onSetupMessage && { onSetupMessage: manifest.onSetupMessage },
51
74
  ...manifest.stability && manifest.stability !== "ga" && { stability: manifest.stability },
52
75
  ...manifest.deprecated && { deprecated: manifest.deprecated },
53
76
  ...manifest.scaffolding && { scaffolding: manifest.scaffolding }
54
- }];
77
+ };
55
78
  }
56
79
  /**
57
80
  * Known packages that may contain AppKit plugins.
@@ -252,17 +275,7 @@ async function scanForPlugins(cwd, packages, allowJsManifest) {
252
275
  const manifests = await discoverPluginManifests(packagePath, allowJsManifest || shouldAllowJsManifestForPackage(packageName));
253
276
  for (const manifest of manifests) {
254
277
  if (manifest.hidden) continue;
255
- plugins[manifest.name] = {
256
- name: manifest.name,
257
- displayName: manifest.displayName,
258
- description: manifest.description,
259
- package: packageName,
260
- resources: manifest.resources,
261
- ...manifest.onSetupMessage && { onSetupMessage: manifest.onSetupMessage },
262
- ...manifest.stability && manifest.stability !== "ga" && { stability: manifest.stability },
263
- ...manifest.deprecated && { deprecated: manifest.deprecated },
264
- ...manifest.scaffolding && { scaffolding: manifest.scaffolding }
265
- };
278
+ plugins[manifest.name] = toTemplatePlugin(manifest, packageName);
266
279
  }
267
280
  }
268
281
  return plugins;
@@ -1 +1 @@
1
- {"version":3,"file":"sync.js","names":[],"sources":["../../../../../src/cli/commands/plugin/sync/sync.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport path from \"node:path\";\n\nimport type { SgNode } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\n\nimport {\n TEMPLATE_SCAFFOLDING,\n templateFieldEntrySchema,\n} from \"../../../../schemas/manifest\";\nimport {\n loadManifestFromFile,\n type ResolvedManifest,\n resolveManifestInDir,\n} from \"../manifest-resolve\";\nimport type {\n PluginManifest,\n TemplatePlugin,\n TemplatePluginsManifest,\n} from \"../manifest-types\";\nimport { shouldAllowJsManifestForPackage } from \"../trusted-js-manifest\";\nimport {\n formatValidationErrors,\n validateManifest,\n} from \"../validate/validate-manifest\";\n\n/**\n * Checks whether a resolved file path is within a given directory boundary.\n * Uses path.resolve + startsWith to prevent directory traversal.\n *\n * @param filePath - The path to check (will be resolved to absolute)\n * @param boundary - The directory that must contain filePath\n * @returns true if filePath is inside boundary (or equal to it)\n */\nfunction isWithinDirectory(filePath: string, boundary: string): boolean {\n const resolvedPath = path.resolve(filePath);\n const resolvedBoundary = path.resolve(boundary);\n // Append separator to avoid prefix false-positives (e.g. /foo-bar matching /foo)\n return (\n resolvedPath === resolvedBoundary ||\n resolvedPath.startsWith(`${resolvedBoundary}${path.sep}`)\n );\n}\n\n/**\n * Validates a parsed JSON object against the plugin-manifest schema.\n * Returns the manifest if valid, or null and logs schema errors.\n */\nfunction validateManifestWithSchema(\n obj: unknown,\n sourcePath: string,\n): PluginManifest | null {\n const result = validateManifest(obj);\n if (result.valid && result.manifest) return result.manifest;\n if (result.errors?.length) {\n console.warn(\n `Warning: Manifest at ${sourcePath} failed schema validation:\\n${formatValidationErrors(result.errors)}`,\n );\n }\n return null;\n}\n\n/** Safety limit for recursive directory scanning to prevent runaway traversal. */\nconst MAX_SCAN_DEPTH = 5;\n\n/**\n * Load and validate a resolved manifest, returning a TemplatePlugin entry or null.\n * Centralises the resolve → load → validate → build-entry pipeline used by\n * multiple discovery functions.\n */\nasync function loadPluginEntry(\n resolved: ResolvedManifest,\n pkg: string,\n allowJsManifest: boolean,\n): Promise<[string, TemplatePlugin] | null> {\n const parsed = await loadManifestFromFile(resolved.path, resolved.type, {\n allowJsManifest,\n });\n const manifest = validateManifestWithSchema(parsed, resolved.path);\n if (!manifest || manifest.hidden) return null;\n\n return [\n manifest.name,\n {\n name: manifest.name,\n displayName: manifest.displayName,\n description: manifest.description,\n package: pkg,\n resources: manifest.resources,\n ...(manifest.onSetupMessage && {\n onSetupMessage: manifest.onSetupMessage,\n }),\n // Narrowing on `!== \"ga\"` removes \"ga\"; the truthy check\n // removes `undefined`. What's left is the non-GA tier set,\n // which TypeScript already knows is assignable to TemplatePlugin's\n // `stability` field — so no cast is needed and adding a future\n // tier (e.g. \"alpha\") flows through type-correctly.\n ...(manifest.stability &&\n manifest.stability !== \"ga\" && {\n stability: manifest.stability,\n }),\n ...(manifest.deprecated && { deprecated: manifest.deprecated }),\n ...(manifest.scaffolding && {\n scaffolding: manifest.scaffolding,\n }),\n },\n ];\n}\n\n/**\n * Known packages that may contain AppKit plugins.\n * Always scanned for manifests, even if not imported in the server file.\n */\nconst KNOWN_PLUGIN_PACKAGES = [\"@databricks/appkit\"];\n\n/**\n * Candidate paths for the server entry file, relative to cwd.\n * Checked in order; the first that exists is used.\n */\nconst SERVER_FILE_CANDIDATES = [\"server/server.ts\", \"server/index.ts\"];\n\n/**\n * Conventional directories to scan for local plugin manifests when\n * --local-plugins-dir is not set. Checked in order; each that exists is scanned.\n * Plugins found here are added to the manifest even if not imported in the server.\n */\nconst CONVENTIONAL_LOCAL_PLUGIN_DIRS = [\"plugins\", \"server\"];\n\n/**\n * Find the server entry file by checking candidate paths in order.\n *\n * @param cwd - Current working directory\n * @returns Absolute path to the server file, or null if none found\n */\nfunction findServerFile(cwd: string): string | null {\n for (const candidate of SERVER_FILE_CANDIDATES) {\n const fullPath = path.join(cwd, candidate);\n if (fs.existsSync(fullPath)) {\n return fullPath;\n }\n }\n return null;\n}\n\n/**\n * Represents a single named import extracted from the server file.\n */\ninterface ParsedImport {\n /** The imported name (or local alias if renamed) */\n name: string;\n /** The original exported name (differs from name when using `import { foo as bar }`) */\n originalName: string;\n /** The module specifier (package name or relative path) */\n source: string;\n}\n\n/**\n * Extract all named imports from the AST root using structural node traversal.\n * Handles single/double quotes, multiline imports, and aliased imports.\n *\n * @param root - AST root node\n * @returns Array of parsed imports with name, original name, and source\n */\nfunction parseImports(root: SgNode): ParsedImport[] {\n const imports: ParsedImport[] = [];\n\n // Find all import_statement nodes in the AST\n const importStatements = root.findAll({\n rule: { kind: \"import_statement\" },\n });\n\n for (const stmt of importStatements) {\n // Extract the module specifier (the string node, e.g. '@databricks/appkit')\n const sourceNode = stmt.find({ rule: { kind: \"string\" } });\n if (!sourceNode) continue;\n\n // Strip surrounding quotes from the string node text\n const source = sourceNode.text().replace(/^['\"]|['\"]$/g, \"\");\n\n // Find named_imports block: { createApp, analytics, server }\n const namedImports = stmt.find({ rule: { kind: \"named_imports\" } });\n if (!namedImports) continue;\n\n // Extract each import_specifier\n const specifiers = namedImports.findAll({\n rule: { kind: \"import_specifier\" },\n });\n\n for (const specifier of specifiers) {\n const children = specifier.children();\n if (children.length >= 3) {\n // Aliased import: `foo as bar` — children are [name, \"as\", alias]\n const originalName = children[0].text();\n const localName = children[children.length - 1].text();\n imports.push({ name: localName, originalName, source });\n } else {\n // Simple import: `foo`\n const name = specifier.text();\n imports.push({ name, originalName: name, source });\n }\n }\n }\n\n return imports;\n}\n\n/**\n * Extract local names of plugins actually used in the `plugins: [...]` array\n * passed to `createApp()`. Uses structural AST traversal to find `pair` nodes\n * with key \"plugins\" and array values containing call expressions.\n *\n * @param root - AST root node\n * @returns Set of local variable names used as plugin calls in the plugins array\n */\nfunction parsePluginUsages(root: SgNode): Set<string> {\n const usedNames = new Set<string>();\n\n // Find all property pairs in the AST\n const pairs = root.findAll({ rule: { kind: \"pair\" } });\n\n for (const pair of pairs) {\n // Check if the property key is \"plugins\"\n const key = pair.find({ rule: { kind: \"property_identifier\" } });\n if (!key || key.text() !== \"plugins\") continue;\n\n // Find the array value\n const arrayNode = pair.find({ rule: { kind: \"array\" } });\n if (!arrayNode) continue;\n\n // Iterate direct children of the array to find call expressions\n for (const child of arrayNode.children()) {\n if (child.kind() === \"call_expression\") {\n // The callee is the first child (the identifier being called)\n const callee = child.children()[0];\n if (callee?.kind() === \"identifier\") {\n usedNames.add(callee.text());\n }\n }\n }\n }\n\n return usedNames;\n}\n\n/**\n * File extensions to try when resolving a relative import to a file path.\n */\nconst RESOLVE_EXTENSIONS = [\".ts\", \".tsx\", \".js\", \".jsx\"];\n\n/**\n * Resolve a relative import source to the plugin directory containing a manifest\n * (manifest.json or manifest.js). Follows the convention that plugins live in\n * their own directory with a manifest file.\n *\n * Resolution strategy:\n * 1. If the import path is a directory, look for manifest.json/js in it\n * 2. If the import path + extension is a file, look for manifest in its parent directory\n * 3. If the import path is a directory with an index file, look for manifest in that directory\n *\n * @param importSource - The relative import specifier (e.g. \"./plugins/my-plugin\")\n * @param serverFileDir - Absolute path to the directory containing the server file\n * @returns Resolved manifest file path and type, or null if not found\n */\nfunction resolveLocalManifest(\n importSource: string,\n serverFileDir: string,\n allowJsManifest: boolean,\n projectRoot?: string,\n): ResolvedManifest | null {\n const resolved = path.resolve(serverFileDir, importSource);\n\n // Security: Reject paths that escape the project root\n const boundary = projectRoot || serverFileDir;\n if (!isWithinDirectory(resolved, boundary)) {\n console.warn(\n `Warning: Skipping import \"${importSource}\" — resolves outside the project directory`,\n );\n return null;\n }\n\n // Case 1: Import path is a directory\n if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\n return resolveManifestInDir(resolved, { allowJsManifest });\n }\n\n // Case 2: Import path + extension resolves to a file — manifest in parent dir\n for (const ext of RESOLVE_EXTENSIONS) {\n const filePath = `${resolved}${ext}`;\n if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {\n const dir = path.dirname(filePath);\n if (!isWithinDirectory(dir, boundary)) return null;\n return resolveManifestInDir(dir, { allowJsManifest });\n }\n }\n\n // Case 3: Import path is a directory with an index file\n for (const ext of RESOLVE_EXTENSIONS) {\n const indexPath = path.join(resolved, `index${ext}`);\n if (fs.existsSync(indexPath)) {\n return resolveManifestInDir(resolved, { allowJsManifest });\n }\n }\n\n return null;\n}\n\n/**\n * Discover plugin manifests from local (relative) imports in the server file.\n * Resolves each relative import to a directory and loads manifest.json or manifest.js.\n *\n * @param relativeImports - Parsed imports with relative sources (starting with . or /)\n * @param serverFileDir - Absolute path to the directory containing the server file\n * @param cwd - Current working directory (for computing relative paths in output)\n * @returns Map of plugin name to template plugin entry for local plugins\n */\nasync function discoverLocalPlugins(\n relativeImports: ParsedImport[],\n serverFileDir: string,\n cwd: string,\n allowJsManifest: boolean,\n): Promise<TemplatePluginsManifest[\"plugins\"]> {\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n\n for (const imp of relativeImports) {\n const resolved = resolveLocalManifest(\n imp.source,\n serverFileDir,\n allowJsManifest,\n cwd,\n );\n if (!resolved) continue;\n\n try {\n const relativePath = path.relative(cwd, path.dirname(resolved.path));\n const entry = await loadPluginEntry(\n resolved,\n `./${relativePath}`,\n allowJsManifest,\n );\n if (entry) plugins[entry[0]] = entry[1];\n } catch (error) {\n console.warn(\n `Warning: Failed to load manifest at ${resolved.path}:`,\n error instanceof Error ? error.message : error,\n );\n }\n }\n\n return plugins;\n}\n\n/**\n * Discover plugin manifests from a package's dist folder.\n * Looks for manifest.json or manifest.js in dist/plugins/{plugin-name}/ directories.\n *\n * @param packagePath - Path to the package in node_modules\n * @returns Array of plugin manifests found in the package\n */\nasync function discoverPluginManifests(\n packagePath: string,\n allowJsManifest: boolean,\n): Promise<PluginManifest[]> {\n const pluginsDir = path.join(packagePath, \"dist\", \"plugins\");\n const manifests: PluginManifest[] = [];\n\n if (!fs.existsSync(pluginsDir)) {\n return manifests;\n }\n\n const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const resolved = resolveManifestInDir(path.join(pluginsDir, entry.name), {\n allowJsManifest,\n });\n if (!resolved) continue;\n\n try {\n const parsed = await loadManifestFromFile(resolved.path, resolved.type, {\n allowJsManifest,\n });\n const manifest = validateManifestWithSchema(parsed, resolved.path);\n if (manifest) {\n manifests.push(manifest);\n }\n } catch (error) {\n console.warn(\n `Warning: Failed to load manifest at ${resolved.path}:`,\n error instanceof Error ? error.message : error,\n );\n }\n }\n\n return manifests;\n}\n\n/**\n * Scan node_modules for packages with plugin manifests.\n *\n * @param cwd - Current working directory to search from\n * @param packages - Set of npm package names to scan for plugin manifests\n * @returns Map of plugin name to template plugin entry\n */\nasync function scanForPlugins(\n cwd: string,\n packages: Iterable<string>,\n allowJsManifest: boolean,\n): Promise<TemplatePluginsManifest[\"plugins\"]> {\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n\n for (const packageName of packages) {\n const packagePath = path.join(cwd, \"node_modules\", packageName);\n if (!fs.existsSync(packagePath)) {\n continue;\n }\n\n const allowJsForPackage =\n allowJsManifest || shouldAllowJsManifestForPackage(packageName);\n\n const manifests = await discoverPluginManifests(\n packagePath,\n allowJsForPackage,\n );\n for (const manifest of manifests) {\n if (manifest.hidden) continue;\n plugins[manifest.name] = {\n name: manifest.name,\n displayName: manifest.displayName,\n description: manifest.description,\n package: packageName,\n resources: manifest.resources,\n ...(manifest.onSetupMessage && {\n onSetupMessage: manifest.onSetupMessage,\n }),\n ...(manifest.stability &&\n manifest.stability !== \"ga\" && {\n stability: manifest.stability,\n }),\n ...(manifest.deprecated && { deprecated: manifest.deprecated }),\n ...(manifest.scaffolding && {\n scaffolding: manifest.scaffolding,\n }),\n } satisfies TemplatePlugin;\n }\n }\n\n return plugins;\n}\n\n/**\n * Recursively scan a directory for plugin manifests. Any directory that\n * contains manifest.json or manifest.js is treated as a plugin root; we do\n * not descend into that directory's children. Used for local plugins discovery\n * so nested paths like server/plugins/category/my-plugin are found.\n */\nasync function scanPluginsDirRecursive(\n dir: string,\n cwd: string,\n allowJsManifest: boolean,\n depth = 0,\n): Promise<TemplatePluginsManifest[\"plugins\"]> {\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n if (!fs.existsSync(dir) || depth >= MAX_SCAN_DEPTH) return plugins;\n\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n\n const pluginDir = path.join(dir, entry.name);\n const resolved = resolveManifestInDir(pluginDir, { allowJsManifest });\n\n if (resolved) {\n const pkg = `./${path.relative(cwd, pluginDir)}`;\n try {\n const pluginEntry = await loadPluginEntry(\n resolved,\n pkg,\n allowJsManifest,\n );\n if (pluginEntry) plugins[pluginEntry[0]] = pluginEntry[1];\n } catch (error) {\n console.warn(\n `Warning: Failed to load manifest at ${resolved.path}:`,\n error instanceof Error ? error.message : error,\n );\n }\n continue;\n }\n\n Object.assign(\n plugins,\n await scanPluginsDirRecursive(pluginDir, cwd, allowJsManifest, depth + 1),\n );\n }\n\n return plugins;\n}\n\n/**\n * Scan a directory for plugin manifests in direct subdirectories only.\n * Each subdirectory may contain manifest.json or manifest.js.\n * Used with --plugins-dir to discover plugins from source instead of node_modules.\n *\n * @param dir - Absolute path to the directory containing plugin subdirectories\n * @param packageName - Package name to assign to discovered plugins (used when cwd is not set)\n * @param cwd - When set, each plugin's package is set to ./<path from cwd to plugin subdir>, e.g. ./server/my-plugin\n * @returns Map of plugin name to template plugin entry\n */\nasync function scanPluginsDir(\n dir: string,\n packageName: string,\n allowJsManifest: boolean,\n cwd?: string,\n): Promise<TemplatePluginsManifest[\"plugins\"]> {\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n\n if (!fs.existsSync(dir)) return plugins;\n\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n\n const pluginDir = path.join(dir, entry.name);\n const resolved = resolveManifestInDir(pluginDir, { allowJsManifest });\n if (!resolved) continue;\n\n const pkg =\n cwd !== undefined ? `./${path.relative(cwd, pluginDir)}` : packageName;\n\n try {\n const pluginEntry = await loadPluginEntry(resolved, pkg, allowJsManifest);\n if (pluginEntry) plugins[pluginEntry[0]] = pluginEntry[1];\n } catch (error) {\n console.warn(\n `Warning: Failed to load manifest at ${resolved.path}:`,\n error instanceof Error ? error.message : error,\n );\n }\n }\n\n return plugins;\n}\n\n/**\n * Write (or preview) the template plugins manifest to disk.\n *\n * Each resource field is parsed through `templateFieldEntrySchema` so the\n * `origin` transform fires and produces canonical `origin` values, even\n * when the input carries a stale `origin`. Parsing per-field (rather than\n * the whole manifest) keeps the surrounding plugin/resource key order\n * stable so the synced JSON's diff stays minimal.\n */\nfunction writeManifest(\n outputPath: string,\n { plugins }: { plugins: TemplatePluginsManifest[\"plugins\"] },\n options: { write?: boolean; silent?: boolean; json?: boolean },\n) {\n for (const plugin of Object.values(plugins)) {\n for (const group of [\n plugin.resources.required,\n plugin.resources.optional,\n ]) {\n for (const resource of group) {\n if (!resource.fields) continue;\n for (const fieldName of Object.keys(resource.fields)) {\n resource.fields[fieldName] = templateFieldEntrySchema.parse(\n resource.fields[fieldName],\n );\n }\n }\n }\n }\n\n const templateManifest: TemplatePluginsManifest = {\n $schema:\n \"https://databricks.github.io/appkit/schemas/template-plugins.schema.json\",\n version: \"2.0\",\n plugins,\n scaffolding: TEMPLATE_SCAFFOLDING,\n };\n\n const serialized = JSON.stringify(templateManifest, null, 2);\n\n if (options.json) {\n console.log(serialized);\n }\n\n if (options.write) {\n fs.writeFileSync(outputPath, `${serialized}\\n`);\n if (!options.silent && !options.json) {\n console.log(`\\n✓ Wrote ${outputPath}`);\n }\n } else if (!options.silent && !options.json) {\n console.log(\"\\nTo write the manifest, run:\");\n console.log(\" npx appkit plugin sync --write\\n\");\n console.log(\"Preview:\");\n console.log(\"─\".repeat(60));\n console.log(serialized);\n console.log(\"─\".repeat(60));\n }\n}\n\n/**\n * Run the plugin sync command.\n * Parses the server entry file to discover which packages to scan for plugin\n * manifests, then marks plugins that are actually used in the `plugins: [...]`\n * array as requiredByTemplate.\n */\nasync function runPluginsSync(options: {\n write?: boolean;\n output?: string;\n silent?: boolean;\n json?: boolean;\n requirePlugins?: string;\n pluginsDir?: string;\n packageName?: string;\n localPluginsDir?: string;\n allowJsManifest?: boolean;\n}): Promise<void> {\n const cwd = process.cwd();\n const allowJsManifest = Boolean(options.allowJsManifest);\n const outputPath = path.resolve(cwd, options.output || \"appkit.plugins.json\");\n\n // Security: Reject output paths that escape the project root\n if (!isWithinDirectory(outputPath, cwd)) {\n console.error(\n `Error: Output path \"${options.output}\" resolves outside the project directory.`,\n );\n process.exit(1);\n }\n\n if (!options.silent && !options.json) {\n console.log(\"Scanning for AppKit plugins...\\n\");\n if (allowJsManifest) {\n console.warn(\n \"Warning: --allow-js-manifest executes manifest.js/manifest.cjs files. Only use with trusted code.\",\n );\n }\n }\n\n // Step 1: Parse server file to discover imports and plugin usages\n const serverFile = findServerFile(cwd);\n let serverImports: ParsedImport[] = [];\n let pluginUsages = new Set<string>();\n\n if (serverFile) {\n if (!options.silent && !options.json) {\n const relativePath = path.relative(cwd, serverFile);\n console.log(`Server entry file: ${relativePath}`);\n }\n\n const require = createRequire(import.meta.url);\n const { Lang, parse } =\n require(\"@ast-grep/napi\") as typeof import(\"@ast-grep/napi\");\n const content = fs.readFileSync(serverFile, \"utf-8\");\n const lang = serverFile.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const ast = parse(lang, content);\n const root = ast.root();\n\n serverImports = parseImports(root);\n pluginUsages = parsePluginUsages(root);\n } else if (!options.silent && !options.json) {\n console.log(\n \"No server entry file found. Checked:\",\n SERVER_FILE_CANDIDATES.join(\", \"),\n );\n }\n\n // Step 2: Split imports into npm packages and local (relative) imports\n const npmImports = serverImports.filter(\n (i) => !i.source.startsWith(\".\") && !i.source.startsWith(\"/\"),\n );\n const localImports = serverImports.filter(\n (i) => i.source.startsWith(\".\") || i.source.startsWith(\"/\"),\n );\n\n // Step 3: Scan for plugin manifests (--plugins-dir or node_modules)\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n\n if (options.pluginsDir) {\n const resolvedDir = path.resolve(cwd, options.pluginsDir);\n const pkgName = options.packageName ?? \"@databricks/appkit\";\n if (!options.silent && !options.json) {\n console.log(`Scanning plugins directory: ${options.pluginsDir}`);\n }\n Object.assign(\n plugins,\n await scanPluginsDir(resolvedDir, pkgName, allowJsManifest),\n );\n } else {\n const npmPackages = new Set([\n ...KNOWN_PLUGIN_PACKAGES,\n ...npmImports.map((i) => i.source),\n ]);\n Object.assign(\n plugins,\n await scanForPlugins(cwd, npmPackages, allowJsManifest),\n );\n }\n\n // Step 4: Discover local plugin manifests from relative imports\n if (serverFile && localImports.length > 0) {\n const serverFileDir = path.dirname(serverFile);\n const localPlugins = await discoverLocalPlugins(\n localImports,\n serverFileDir,\n cwd,\n allowJsManifest,\n );\n Object.assign(plugins, localPlugins);\n }\n\n // Step 4b: Discover local plugins from conventional directory (or --local-plugins-dir).\n // These are included even when not imported in the server.\n const localDirsToScan: string[] = options.localPluginsDir\n ? [options.localPluginsDir]\n : CONVENTIONAL_LOCAL_PLUGIN_DIRS.filter((d) =>\n fs.existsSync(path.join(cwd, d)),\n );\n for (const dir of localDirsToScan) {\n const resolvedDir = path.resolve(cwd, dir);\n if (!fs.existsSync(resolvedDir)) continue;\n if (!options.silent && !options.json) {\n console.log(`Scanning local plugins directory: ${dir}`);\n }\n const discovered = await scanPluginsDirRecursive(\n resolvedDir,\n cwd,\n allowJsManifest,\n );\n for (const [name, entry] of Object.entries(discovered)) {\n if (!plugins[name]) plugins[name] = entry;\n }\n }\n\n const pluginCount = Object.keys(plugins).length;\n\n if (pluginCount === 0) {\n if (options.silent || options.json) {\n writeManifest(outputPath, { plugins: {} }, options);\n if (options.silent) return;\n process.exit(1);\n }\n console.log(\"No plugins found.\");\n if (options.pluginsDir) {\n console.log(\n `\\nNo manifest (${allowJsManifest ? \"manifest.json or manifest.js\" : \"manifest.json\"}) found in: ${options.pluginsDir}`,\n );\n } else {\n console.log(\n \"\\nMake sure you have plugin packages installed, or specify a directory:\",\n );\n console.log(\" appkit plugin sync --plugins-dir <path>\");\n }\n process.exit(1);\n }\n\n // Step 5: Mark plugins that are imported AND used in the plugins array as mandatory.\n // For npm imports, match by package name + plugin name.\n // For local imports, resolve both paths to absolute and compare.\n const serverFileDir = serverFile ? path.dirname(serverFile) : cwd;\n const usedDeprecated: string[] = [];\n\n for (const imp of serverImports) {\n if (!pluginUsages.has(imp.name)) continue;\n\n const isLocal = imp.source.startsWith(\".\") || imp.source.startsWith(\"/\");\n let plugin: TemplatePlugin | undefined;\n\n if (isLocal) {\n // Resolve the import source to an absolute path from the server file directory\n const resolvedImportDir = path.resolve(serverFileDir, imp.source);\n plugin = Object.values(plugins).find((p) => {\n if (!p.package.startsWith(\".\")) return false;\n const resolvedPluginDir = path.resolve(cwd, p.package);\n return (\n resolvedPluginDir === resolvedImportDir && p.name === imp.originalName\n );\n });\n } else {\n // npm import: direct string comparison\n plugin = Object.values(plugins).find(\n (p) => p.package === imp.source && p.name === imp.originalName,\n );\n }\n\n if (plugin) {\n plugin.requiredByTemplate = true;\n if (plugin.deprecated) usedDeprecated.push(plugin.name);\n }\n }\n\n // Step 6: Apply explicit --require-plugins overrides\n if (options.requirePlugins) {\n const explicitNames = options.requirePlugins\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n for (const name of explicitNames) {\n if (plugins[name]) {\n plugins[name].requiredByTemplate = true;\n } else if (!options.silent) {\n console.warn(\n `Warning: --require-plugins referenced \"${name}\" but no such plugin was discovered`,\n );\n }\n }\n }\n\n // Step 6b: Strip requiredByTemplate for non-GA plugins\n for (const plugin of Object.values(plugins)) {\n if (\n plugin.requiredByTemplate &&\n plugin.stability &&\n plugin.stability !== \"ga\"\n ) {\n plugin.requiredByTemplate = undefined;\n }\n }\n\n if (!options.silent && !options.json) {\n console.log(`\\nFound ${pluginCount} plugin(s):`);\n for (const [name, manifest] of Object.entries(plugins)) {\n const resourceCount =\n manifest.resources.required.length + manifest.resources.optional.length;\n const resourceInfo =\n resourceCount > 0 ? ` [${resourceCount} resource(s)]` : \"\";\n const mandatoryTag = manifest.requiredByTemplate ? \" (mandatory)\" : \"\";\n const deprecatedTag = manifest.deprecated ? \" (deprecated)\" : \"\";\n console.log(\n ` ${manifest.requiredByTemplate ? \"●\" : \"○\"} ${manifest.displayName} (${name}) from ${manifest.package}${resourceInfo}${mandatoryTag}${deprecatedTag}`,\n );\n }\n }\n\n // Warn about deprecated plugins that are actually used in the app.\n if (!options.silent && usedDeprecated.length > 0) {\n for (const name of usedDeprecated) {\n console.warn(\n `Warning: plugin \"${name}\" is deprecated. See its description for the recommended replacement.`,\n );\n }\n }\n\n // Step 7: Detect orphaned resources from removed plugins\n if (!options.silent && fs.existsSync(outputPath)) {\n try {\n const oldRaw = fs.readFileSync(outputPath, \"utf-8\");\n const oldManifest = JSON.parse(oldRaw) as TemplatePluginsManifest;\n const oldNames = new Set(Object.keys(oldManifest.plugins ?? {}));\n const newNames = new Set(Object.keys(plugins));\n for (const name of oldNames) {\n if (newNames.has(name)) continue;\n const oldPlugin = oldManifest.plugins?.[name];\n if (!oldPlugin || typeof oldPlugin !== \"object\") continue;\n const envVars: string[] = [];\n for (const res of [\n ...(oldPlugin.resources?.required ?? []),\n ...(oldPlugin.resources?.optional ?? []),\n ]) {\n if (res?.fields) {\n for (const field of Object.values(res.fields)) {\n if (field?.env) envVars.push(field.env);\n }\n }\n }\n const envInfo =\n envVars.length > 0\n ? ` The following resource env vars may be orphaned: ${envVars.join(\", \")}`\n : \"\";\n console.warn(`Warning: Plugin \"${name}\" was removed.${envInfo}`);\n }\n } catch {\n // Ignore parse errors on existing manifest\n }\n }\n\n writeManifest(outputPath, { plugins }, options);\n}\n\n/** Exported for testing: path boundary check, AST parsing, trust checks. */\nexport {\n isWithinDirectory,\n parseImports,\n parsePluginUsages,\n shouldAllowJsManifestForPackage,\n};\n\nexport const pluginsSyncCommand = new Command(\"sync\")\n .description(\n \"Sync plugin manifests from installed packages into appkit.plugins.json\",\n )\n .option(\"-w, --write\", \"Write the manifest file\")\n .option(\n \"-o, --output <path>\",\n \"Output file path (default: ./appkit.plugins.json)\",\n )\n .option(\n \"-s, --silent\",\n \"Suppress output and never exit with error (for use in predev/prebuild hooks)\",\n )\n .option(\n \"--require-plugins <names>\",\n \"Comma-separated plugin names to mark as requiredByTemplate (e.g. server,analytics)\",\n )\n .option(\n \"--plugins-dir <path>\",\n \"Scan this directory for plugin subdirectories with manifest.json (instead of node_modules)\",\n )\n .option(\n \"--package-name <name>\",\n \"Package name to assign to plugins found via --plugins-dir (default: @databricks/appkit)\",\n )\n .option(\n \"--local-plugins-dir <path>\",\n \"Also scan this directory for local plugin manifests (default: plugins, server)\",\n )\n .option(\n \"--allow-js-manifest\",\n \"Allow reading manifest.js/manifest.cjs (executes code; use only with trusted plugins)\",\n )\n .option(\"--json\", \"Output manifest as JSON to stdout\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit plugin sync\n $ appkit plugin sync --write\n $ appkit plugin sync --write --require-plugins server,analytics\n $ appkit plugin sync --write --plugins-dir src/plugins --package-name @my/pkg\n $ appkit plugin sync --json\n $ appkit plugin sync --silent`,\n )\n .action((opts) =>\n runPluginsSync(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}
1
+ {"version":3,"file":"sync.js","names":[],"sources":["../../../../../src/cli/commands/plugin/sync/sync.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport path from \"node:path\";\n\nimport type { SgNode } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\n\nimport {\n APP_ONLY_RESOURCE_TYPES,\n DABS_BINDING_BY_TYPE,\n type ResourceBinding,\n SCOPE_BY_TYPE,\n TEMPLATE_SCAFFOLDING,\n templateFieldEntrySchema,\n} from \"../../../../schemas/manifest\";\nimport {\n loadManifestFromFile,\n type ResolvedManifest,\n resolveManifestInDir,\n} from \"../manifest-resolve\";\nimport type {\n PluginManifest,\n TemplatePlugin,\n TemplatePluginsManifest,\n} from \"../manifest-types\";\nimport { shouldAllowJsManifestForPackage } from \"../trusted-js-manifest\";\nimport {\n formatValidationErrors,\n validateManifest,\n} from \"../validate/validate-manifest\";\n\n/**\n * Checks whether a resolved file path is within a given directory boundary.\n * Uses path.resolve + startsWith to prevent directory traversal.\n *\n * @param filePath - The path to check (will be resolved to absolute)\n * @param boundary - The directory that must contain filePath\n * @returns true if filePath is inside boundary (or equal to it)\n */\nfunction isWithinDirectory(filePath: string, boundary: string): boolean {\n const resolvedPath = path.resolve(filePath);\n const resolvedBoundary = path.resolve(boundary);\n // Append separator to avoid prefix false-positives (e.g. /foo-bar matching /foo)\n return (\n resolvedPath === resolvedBoundary ||\n resolvedPath.startsWith(`${resolvedBoundary}${path.sep}`)\n );\n}\n\n/**\n * Validates a parsed JSON object against the plugin-manifest schema.\n * Returns the manifest if valid, or null and logs schema errors.\n */\nfunction validateManifestWithSchema(\n obj: unknown,\n sourcePath: string,\n): PluginManifest | null {\n const result = validateManifest(obj);\n if (result.valid && result.manifest) return result.manifest;\n if (result.errors?.length) {\n console.warn(\n `Warning: Manifest at ${sourcePath} failed schema validation:\\n${formatValidationErrors(result.errors)}`,\n );\n }\n return null;\n}\n\n/** Safety limit for recursive directory scanning to prevent runaway traversal. */\nconst MAX_SCAN_DEPTH = 5;\n\n/**\n * Load and validate a resolved manifest, returning a TemplatePlugin entry or null.\n * Centralises the resolve → load → validate → build-entry pipeline used by\n * multiple discovery functions.\n */\nasync function loadPluginEntry(\n resolved: ResolvedManifest,\n pkg: string,\n allowJsManifest: boolean,\n): Promise<[string, TemplatePlugin] | null> {\n const parsed = await loadManifestFromFile(resolved.path, resolved.type, {\n allowJsManifest,\n });\n const manifest = validateManifestWithSchema(parsed, resolved.path);\n if (!manifest || manifest.hidden) return null;\n\n return [manifest.name, toTemplatePlugin(manifest, pkg)];\n}\n\ntype ManifestResource = PluginManifest[\"resources\"][\"required\"][number];\n\n/** Bake the type-level execution facts into the resource so the CLI reads plain data. */\nfunction withExecutionCapabilities(resource: ManifestResource) {\n const scope = Object.hasOwn(SCOPE_BY_TYPE, resource.type)\n ? SCOPE_BY_TYPE[resource.type as keyof typeof SCOPE_BY_TYPE]\n : undefined;\n const binding: ResourceBinding | undefined = Object.hasOwn(\n DABS_BINDING_BY_TYPE,\n resource.type,\n )\n ? DABS_BINDING_BY_TYPE[resource.type as keyof typeof DABS_BINDING_BY_TYPE]\n : undefined;\n return {\n ...resource,\n ...(scope && { scope }),\n ...(APP_ONLY_RESOURCE_TYPES.has(resource.type) && {\n appOnly: true as const,\n }),\n ...(binding && {\n // Materialize mutable tuples so the baked entry matches the template\n // schema and never shares a reference with the source const table.\n binding: {\n yamlKey: binding.yamlKey,\n varFields: binding.varFields.map(\n (pair) => [pair[0], pair[1]] as [string, string],\n ),\n ...(binding.staticFields && {\n staticFields: binding.staticFields.map(\n (pair) => [pair[0], pair[1]] as [string, string],\n ),\n }),\n },\n }),\n };\n}\n\n/** Build a TemplatePlugin entry. Every discovery path goes through here. */\nfunction toTemplatePlugin(\n manifest: PluginManifest,\n pkg: string,\n): TemplatePlugin {\n return {\n name: manifest.name,\n displayName: manifest.displayName,\n description: manifest.description,\n package: pkg,\n resources: {\n required: manifest.resources.required.map((resource) =>\n withExecutionCapabilities(resource),\n ),\n optional: manifest.resources.optional.map((resource) =>\n withExecutionCapabilities(resource),\n ),\n },\n ...(manifest.scopes?.length && { scopes: manifest.scopes }),\n ...(manifest.onSetupMessage && {\n onSetupMessage: manifest.onSetupMessage,\n }),\n // Narrowing on `!== \"ga\"` removes \"ga\"; the truthy check\n // removes `undefined`. What's left is the non-GA tier set,\n // which TypeScript already knows is assignable to TemplatePlugin's\n // `stability` field — so no cast is needed and adding a future\n // tier (e.g. \"alpha\") flows through type-correctly.\n ...(manifest.stability &&\n manifest.stability !== \"ga\" && {\n stability: manifest.stability,\n }),\n ...(manifest.deprecated && { deprecated: manifest.deprecated }),\n ...(manifest.scaffolding && {\n scaffolding: manifest.scaffolding,\n }),\n };\n}\n\n/**\n * Known packages that may contain AppKit plugins.\n * Always scanned for manifests, even if not imported in the server file.\n */\nconst KNOWN_PLUGIN_PACKAGES = [\"@databricks/appkit\"];\n\n/**\n * Candidate paths for the server entry file, relative to cwd.\n * Checked in order; the first that exists is used.\n */\nconst SERVER_FILE_CANDIDATES = [\"server/server.ts\", \"server/index.ts\"];\n\n/**\n * Conventional directories to scan for local plugin manifests when\n * --local-plugins-dir is not set. Checked in order; each that exists is scanned.\n * Plugins found here are added to the manifest even if not imported in the server.\n */\nconst CONVENTIONAL_LOCAL_PLUGIN_DIRS = [\"plugins\", \"server\"];\n\n/**\n * Find the server entry file by checking candidate paths in order.\n *\n * @param cwd - Current working directory\n * @returns Absolute path to the server file, or null if none found\n */\nfunction findServerFile(cwd: string): string | null {\n for (const candidate of SERVER_FILE_CANDIDATES) {\n const fullPath = path.join(cwd, candidate);\n if (fs.existsSync(fullPath)) {\n return fullPath;\n }\n }\n return null;\n}\n\n/**\n * Represents a single named import extracted from the server file.\n */\ninterface ParsedImport {\n /** The imported name (or local alias if renamed) */\n name: string;\n /** The original exported name (differs from name when using `import { foo as bar }`) */\n originalName: string;\n /** The module specifier (package name or relative path) */\n source: string;\n}\n\n/**\n * Extract all named imports from the AST root using structural node traversal.\n * Handles single/double quotes, multiline imports, and aliased imports.\n *\n * @param root - AST root node\n * @returns Array of parsed imports with name, original name, and source\n */\nfunction parseImports(root: SgNode): ParsedImport[] {\n const imports: ParsedImport[] = [];\n\n // Find all import_statement nodes in the AST\n const importStatements = root.findAll({\n rule: { kind: \"import_statement\" },\n });\n\n for (const stmt of importStatements) {\n // Extract the module specifier (the string node, e.g. '@databricks/appkit')\n const sourceNode = stmt.find({ rule: { kind: \"string\" } });\n if (!sourceNode) continue;\n\n // Strip surrounding quotes from the string node text\n const source = sourceNode.text().replace(/^['\"]|['\"]$/g, \"\");\n\n // Find named_imports block: { createApp, analytics, server }\n const namedImports = stmt.find({ rule: { kind: \"named_imports\" } });\n if (!namedImports) continue;\n\n // Extract each import_specifier\n const specifiers = namedImports.findAll({\n rule: { kind: \"import_specifier\" },\n });\n\n for (const specifier of specifiers) {\n const children = specifier.children();\n if (children.length >= 3) {\n // Aliased import: `foo as bar` — children are [name, \"as\", alias]\n const originalName = children[0].text();\n const localName = children[children.length - 1].text();\n imports.push({ name: localName, originalName, source });\n } else {\n // Simple import: `foo`\n const name = specifier.text();\n imports.push({ name, originalName: name, source });\n }\n }\n }\n\n return imports;\n}\n\n/**\n * Extract local names of plugins actually used in the `plugins: [...]` array\n * passed to `createApp()`. Uses structural AST traversal to find `pair` nodes\n * with key \"plugins\" and array values containing call expressions.\n *\n * @param root - AST root node\n * @returns Set of local variable names used as plugin calls in the plugins array\n */\nfunction parsePluginUsages(root: SgNode): Set<string> {\n const usedNames = new Set<string>();\n\n // Find all property pairs in the AST\n const pairs = root.findAll({ rule: { kind: \"pair\" } });\n\n for (const pair of pairs) {\n // Check if the property key is \"plugins\"\n const key = pair.find({ rule: { kind: \"property_identifier\" } });\n if (!key || key.text() !== \"plugins\") continue;\n\n // Find the array value\n const arrayNode = pair.find({ rule: { kind: \"array\" } });\n if (!arrayNode) continue;\n\n // Iterate direct children of the array to find call expressions\n for (const child of arrayNode.children()) {\n if (child.kind() === \"call_expression\") {\n // The callee is the first child (the identifier being called)\n const callee = child.children()[0];\n if (callee?.kind() === \"identifier\") {\n usedNames.add(callee.text());\n }\n }\n }\n }\n\n return usedNames;\n}\n\n/**\n * File extensions to try when resolving a relative import to a file path.\n */\nconst RESOLVE_EXTENSIONS = [\".ts\", \".tsx\", \".js\", \".jsx\"];\n\n/**\n * Resolve a relative import source to the plugin directory containing a manifest\n * (manifest.json or manifest.js). Follows the convention that plugins live in\n * their own directory with a manifest file.\n *\n * Resolution strategy:\n * 1. If the import path is a directory, look for manifest.json/js in it\n * 2. If the import path + extension is a file, look for manifest in its parent directory\n * 3. If the import path is a directory with an index file, look for manifest in that directory\n *\n * @param importSource - The relative import specifier (e.g. \"./plugins/my-plugin\")\n * @param serverFileDir - Absolute path to the directory containing the server file\n * @returns Resolved manifest file path and type, or null if not found\n */\nfunction resolveLocalManifest(\n importSource: string,\n serverFileDir: string,\n allowJsManifest: boolean,\n projectRoot?: string,\n): ResolvedManifest | null {\n const resolved = path.resolve(serverFileDir, importSource);\n\n // Security: Reject paths that escape the project root\n const boundary = projectRoot || serverFileDir;\n if (!isWithinDirectory(resolved, boundary)) {\n console.warn(\n `Warning: Skipping import \"${importSource}\" — resolves outside the project directory`,\n );\n return null;\n }\n\n // Case 1: Import path is a directory\n if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\n return resolveManifestInDir(resolved, { allowJsManifest });\n }\n\n // Case 2: Import path + extension resolves to a file — manifest in parent dir\n for (const ext of RESOLVE_EXTENSIONS) {\n const filePath = `${resolved}${ext}`;\n if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {\n const dir = path.dirname(filePath);\n if (!isWithinDirectory(dir, boundary)) return null;\n return resolveManifestInDir(dir, { allowJsManifest });\n }\n }\n\n // Case 3: Import path is a directory with an index file\n for (const ext of RESOLVE_EXTENSIONS) {\n const indexPath = path.join(resolved, `index${ext}`);\n if (fs.existsSync(indexPath)) {\n return resolveManifestInDir(resolved, { allowJsManifest });\n }\n }\n\n return null;\n}\n\n/**\n * Discover plugin manifests from local (relative) imports in the server file.\n * Resolves each relative import to a directory and loads manifest.json or manifest.js.\n *\n * @param relativeImports - Parsed imports with relative sources (starting with . or /)\n * @param serverFileDir - Absolute path to the directory containing the server file\n * @param cwd - Current working directory (for computing relative paths in output)\n * @returns Map of plugin name to template plugin entry for local plugins\n */\nasync function discoverLocalPlugins(\n relativeImports: ParsedImport[],\n serverFileDir: string,\n cwd: string,\n allowJsManifest: boolean,\n): Promise<TemplatePluginsManifest[\"plugins\"]> {\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n\n for (const imp of relativeImports) {\n const resolved = resolveLocalManifest(\n imp.source,\n serverFileDir,\n allowJsManifest,\n cwd,\n );\n if (!resolved) continue;\n\n try {\n const relativePath = path.relative(cwd, path.dirname(resolved.path));\n const entry = await loadPluginEntry(\n resolved,\n `./${relativePath}`,\n allowJsManifest,\n );\n if (entry) plugins[entry[0]] = entry[1];\n } catch (error) {\n console.warn(\n `Warning: Failed to load manifest at ${resolved.path}:`,\n error instanceof Error ? error.message : error,\n );\n }\n }\n\n return plugins;\n}\n\n/**\n * Discover plugin manifests from a package's dist folder.\n * Looks for manifest.json or manifest.js in dist/plugins/{plugin-name}/ directories.\n *\n * @param packagePath - Path to the package in node_modules\n * @returns Array of plugin manifests found in the package\n */\nasync function discoverPluginManifests(\n packagePath: string,\n allowJsManifest: boolean,\n): Promise<PluginManifest[]> {\n const pluginsDir = path.join(packagePath, \"dist\", \"plugins\");\n const manifests: PluginManifest[] = [];\n\n if (!fs.existsSync(pluginsDir)) {\n return manifests;\n }\n\n const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const resolved = resolveManifestInDir(path.join(pluginsDir, entry.name), {\n allowJsManifest,\n });\n if (!resolved) continue;\n\n try {\n const parsed = await loadManifestFromFile(resolved.path, resolved.type, {\n allowJsManifest,\n });\n const manifest = validateManifestWithSchema(parsed, resolved.path);\n if (manifest) {\n manifests.push(manifest);\n }\n } catch (error) {\n console.warn(\n `Warning: Failed to load manifest at ${resolved.path}:`,\n error instanceof Error ? error.message : error,\n );\n }\n }\n\n return manifests;\n}\n\n/**\n * Scan node_modules for packages with plugin manifests.\n *\n * @param cwd - Current working directory to search from\n * @param packages - Set of npm package names to scan for plugin manifests\n * @returns Map of plugin name to template plugin entry\n */\nasync function scanForPlugins(\n cwd: string,\n packages: Iterable<string>,\n allowJsManifest: boolean,\n): Promise<TemplatePluginsManifest[\"plugins\"]> {\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n\n for (const packageName of packages) {\n const packagePath = path.join(cwd, \"node_modules\", packageName);\n if (!fs.existsSync(packagePath)) {\n continue;\n }\n\n const allowJsForPackage =\n allowJsManifest || shouldAllowJsManifestForPackage(packageName);\n\n const manifests = await discoverPluginManifests(\n packagePath,\n allowJsForPackage,\n );\n for (const manifest of manifests) {\n if (manifest.hidden) continue;\n plugins[manifest.name] = toTemplatePlugin(manifest, packageName);\n }\n }\n\n return plugins;\n}\n\n/**\n * Recursively scan a directory for plugin manifests. Any directory that\n * contains manifest.json or manifest.js is treated as a plugin root; we do\n * not descend into that directory's children. Used for local plugins discovery\n * so nested paths like server/plugins/category/my-plugin are found.\n */\nasync function scanPluginsDirRecursive(\n dir: string,\n cwd: string,\n allowJsManifest: boolean,\n depth = 0,\n): Promise<TemplatePluginsManifest[\"plugins\"]> {\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n if (!fs.existsSync(dir) || depth >= MAX_SCAN_DEPTH) return plugins;\n\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n\n const pluginDir = path.join(dir, entry.name);\n const resolved = resolveManifestInDir(pluginDir, { allowJsManifest });\n\n if (resolved) {\n const pkg = `./${path.relative(cwd, pluginDir)}`;\n try {\n const pluginEntry = await loadPluginEntry(\n resolved,\n pkg,\n allowJsManifest,\n );\n if (pluginEntry) plugins[pluginEntry[0]] = pluginEntry[1];\n } catch (error) {\n console.warn(\n `Warning: Failed to load manifest at ${resolved.path}:`,\n error instanceof Error ? error.message : error,\n );\n }\n continue;\n }\n\n Object.assign(\n plugins,\n await scanPluginsDirRecursive(pluginDir, cwd, allowJsManifest, depth + 1),\n );\n }\n\n return plugins;\n}\n\n/**\n * Scan a directory for plugin manifests in direct subdirectories only.\n * Each subdirectory may contain manifest.json or manifest.js.\n * Used with --plugins-dir to discover plugins from source instead of node_modules.\n *\n * @param dir - Absolute path to the directory containing plugin subdirectories\n * @param packageName - Package name to assign to discovered plugins (used when cwd is not set)\n * @param cwd - When set, each plugin's package is set to ./<path from cwd to plugin subdir>, e.g. ./server/my-plugin\n * @returns Map of plugin name to template plugin entry\n */\nasync function scanPluginsDir(\n dir: string,\n packageName: string,\n allowJsManifest: boolean,\n cwd?: string,\n): Promise<TemplatePluginsManifest[\"plugins\"]> {\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n\n if (!fs.existsSync(dir)) return plugins;\n\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n\n const pluginDir = path.join(dir, entry.name);\n const resolved = resolveManifestInDir(pluginDir, { allowJsManifest });\n if (!resolved) continue;\n\n const pkg =\n cwd !== undefined ? `./${path.relative(cwd, pluginDir)}` : packageName;\n\n try {\n const pluginEntry = await loadPluginEntry(resolved, pkg, allowJsManifest);\n if (pluginEntry) plugins[pluginEntry[0]] = pluginEntry[1];\n } catch (error) {\n console.warn(\n `Warning: Failed to load manifest at ${resolved.path}:`,\n error instanceof Error ? error.message : error,\n );\n }\n }\n\n return plugins;\n}\n\n/**\n * Write (or preview) the template plugins manifest to disk.\n *\n * Each resource field is parsed through `templateFieldEntrySchema` so the\n * `origin` transform fires and produces canonical `origin` values, even\n * when the input carries a stale `origin`. Parsing per-field (rather than\n * the whole manifest) keeps the surrounding plugin/resource key order\n * stable so the synced JSON's diff stays minimal.\n */\nfunction writeManifest(\n outputPath: string,\n { plugins }: { plugins: TemplatePluginsManifest[\"plugins\"] },\n options: { write?: boolean; silent?: boolean; json?: boolean },\n) {\n for (const plugin of Object.values(plugins)) {\n for (const group of [\n plugin.resources.required,\n plugin.resources.optional,\n ]) {\n for (const resource of group) {\n if (!resource.fields) continue;\n for (const fieldName of Object.keys(resource.fields)) {\n resource.fields[fieldName] = templateFieldEntrySchema.parse(\n resource.fields[fieldName],\n );\n }\n }\n }\n }\n\n const templateManifest: TemplatePluginsManifest = {\n $schema:\n \"https://databricks.github.io/appkit/schemas/template-plugins.schema.json\",\n version: \"2.0\",\n plugins,\n scaffolding: TEMPLATE_SCAFFOLDING,\n };\n\n const serialized = JSON.stringify(templateManifest, null, 2);\n\n if (options.json) {\n console.log(serialized);\n }\n\n if (options.write) {\n fs.writeFileSync(outputPath, `${serialized}\\n`);\n if (!options.silent && !options.json) {\n console.log(`\\n✓ Wrote ${outputPath}`);\n }\n } else if (!options.silent && !options.json) {\n console.log(\"\\nTo write the manifest, run:\");\n console.log(\" npx appkit plugin sync --write\\n\");\n console.log(\"Preview:\");\n console.log(\"─\".repeat(60));\n console.log(serialized);\n console.log(\"─\".repeat(60));\n }\n}\n\n/**\n * Run the plugin sync command.\n * Parses the server entry file to discover which packages to scan for plugin\n * manifests, then marks plugins that are actually used in the `plugins: [...]`\n * array as requiredByTemplate.\n */\nasync function runPluginsSync(options: {\n write?: boolean;\n output?: string;\n silent?: boolean;\n json?: boolean;\n requirePlugins?: string;\n pluginsDir?: string;\n packageName?: string;\n localPluginsDir?: string;\n allowJsManifest?: boolean;\n}): Promise<void> {\n const cwd = process.cwd();\n const allowJsManifest = Boolean(options.allowJsManifest);\n const outputPath = path.resolve(cwd, options.output || \"appkit.plugins.json\");\n\n // Security: Reject output paths that escape the project root\n if (!isWithinDirectory(outputPath, cwd)) {\n console.error(\n `Error: Output path \"${options.output}\" resolves outside the project directory.`,\n );\n process.exit(1);\n }\n\n if (!options.silent && !options.json) {\n console.log(\"Scanning for AppKit plugins...\\n\");\n if (allowJsManifest) {\n console.warn(\n \"Warning: --allow-js-manifest executes manifest.js/manifest.cjs files. Only use with trusted code.\",\n );\n }\n }\n\n // Step 1: Parse server file to discover imports and plugin usages\n const serverFile = findServerFile(cwd);\n let serverImports: ParsedImport[] = [];\n let pluginUsages = new Set<string>();\n\n if (serverFile) {\n if (!options.silent && !options.json) {\n const relativePath = path.relative(cwd, serverFile);\n console.log(`Server entry file: ${relativePath}`);\n }\n\n const require = createRequire(import.meta.url);\n const { Lang, parse } =\n require(\"@ast-grep/napi\") as typeof import(\"@ast-grep/napi\");\n const content = fs.readFileSync(serverFile, \"utf-8\");\n const lang = serverFile.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const ast = parse(lang, content);\n const root = ast.root();\n\n serverImports = parseImports(root);\n pluginUsages = parsePluginUsages(root);\n } else if (!options.silent && !options.json) {\n console.log(\n \"No server entry file found. Checked:\",\n SERVER_FILE_CANDIDATES.join(\", \"),\n );\n }\n\n // Step 2: Split imports into npm packages and local (relative) imports\n const npmImports = serverImports.filter(\n (i) => !i.source.startsWith(\".\") && !i.source.startsWith(\"/\"),\n );\n const localImports = serverImports.filter(\n (i) => i.source.startsWith(\".\") || i.source.startsWith(\"/\"),\n );\n\n // Step 3: Scan for plugin manifests (--plugins-dir or node_modules)\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n\n if (options.pluginsDir) {\n const resolvedDir = path.resolve(cwd, options.pluginsDir);\n const pkgName = options.packageName ?? \"@databricks/appkit\";\n if (!options.silent && !options.json) {\n console.log(`Scanning plugins directory: ${options.pluginsDir}`);\n }\n Object.assign(\n plugins,\n await scanPluginsDir(resolvedDir, pkgName, allowJsManifest),\n );\n } else {\n const npmPackages = new Set([\n ...KNOWN_PLUGIN_PACKAGES,\n ...npmImports.map((i) => i.source),\n ]);\n Object.assign(\n plugins,\n await scanForPlugins(cwd, npmPackages, allowJsManifest),\n );\n }\n\n // Step 4: Discover local plugin manifests from relative imports\n if (serverFile && localImports.length > 0) {\n const serverFileDir = path.dirname(serverFile);\n const localPlugins = await discoverLocalPlugins(\n localImports,\n serverFileDir,\n cwd,\n allowJsManifest,\n );\n Object.assign(plugins, localPlugins);\n }\n\n // Step 4b: Discover local plugins from conventional directory (or --local-plugins-dir).\n // These are included even when not imported in the server.\n const localDirsToScan: string[] = options.localPluginsDir\n ? [options.localPluginsDir]\n : CONVENTIONAL_LOCAL_PLUGIN_DIRS.filter((d) =>\n fs.existsSync(path.join(cwd, d)),\n );\n for (const dir of localDirsToScan) {\n const resolvedDir = path.resolve(cwd, dir);\n if (!fs.existsSync(resolvedDir)) continue;\n if (!options.silent && !options.json) {\n console.log(`Scanning local plugins directory: ${dir}`);\n }\n const discovered = await scanPluginsDirRecursive(\n resolvedDir,\n cwd,\n allowJsManifest,\n );\n for (const [name, entry] of Object.entries(discovered)) {\n if (!plugins[name]) plugins[name] = entry;\n }\n }\n\n const pluginCount = Object.keys(plugins).length;\n\n if (pluginCount === 0) {\n if (options.silent || options.json) {\n writeManifest(outputPath, { plugins: {} }, options);\n if (options.silent) return;\n process.exit(1);\n }\n console.log(\"No plugins found.\");\n if (options.pluginsDir) {\n console.log(\n `\\nNo manifest (${allowJsManifest ? \"manifest.json or manifest.js\" : \"manifest.json\"}) found in: ${options.pluginsDir}`,\n );\n } else {\n console.log(\n \"\\nMake sure you have plugin packages installed, or specify a directory:\",\n );\n console.log(\" appkit plugin sync --plugins-dir <path>\");\n }\n process.exit(1);\n }\n\n // Step 5: Mark plugins that are imported AND used in the plugins array as mandatory.\n // For npm imports, match by package name + plugin name.\n // For local imports, resolve both paths to absolute and compare.\n const serverFileDir = serverFile ? path.dirname(serverFile) : cwd;\n const usedDeprecated: string[] = [];\n\n for (const imp of serverImports) {\n if (!pluginUsages.has(imp.name)) continue;\n\n const isLocal = imp.source.startsWith(\".\") || imp.source.startsWith(\"/\");\n let plugin: TemplatePlugin | undefined;\n\n if (isLocal) {\n // Resolve the import source to an absolute path from the server file directory\n const resolvedImportDir = path.resolve(serverFileDir, imp.source);\n plugin = Object.values(plugins).find((p) => {\n if (!p.package.startsWith(\".\")) return false;\n const resolvedPluginDir = path.resolve(cwd, p.package);\n return (\n resolvedPluginDir === resolvedImportDir && p.name === imp.originalName\n );\n });\n } else {\n // npm import: direct string comparison\n plugin = Object.values(plugins).find(\n (p) => p.package === imp.source && p.name === imp.originalName,\n );\n }\n\n if (plugin) {\n plugin.requiredByTemplate = true;\n if (plugin.deprecated) usedDeprecated.push(plugin.name);\n }\n }\n\n // Step 6: Apply explicit --require-plugins overrides\n if (options.requirePlugins) {\n const explicitNames = options.requirePlugins\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n for (const name of explicitNames) {\n if (plugins[name]) {\n plugins[name].requiredByTemplate = true;\n } else if (!options.silent) {\n console.warn(\n `Warning: --require-plugins referenced \"${name}\" but no such plugin was discovered`,\n );\n }\n }\n }\n\n // Step 6b: Strip requiredByTemplate for non-GA plugins\n for (const plugin of Object.values(plugins)) {\n if (\n plugin.requiredByTemplate &&\n plugin.stability &&\n plugin.stability !== \"ga\"\n ) {\n plugin.requiredByTemplate = undefined;\n }\n }\n\n if (!options.silent && !options.json) {\n console.log(`\\nFound ${pluginCount} plugin(s):`);\n for (const [name, manifest] of Object.entries(plugins)) {\n const resourceCount =\n manifest.resources.required.length + manifest.resources.optional.length;\n const resourceInfo =\n resourceCount > 0 ? ` [${resourceCount} resource(s)]` : \"\";\n const mandatoryTag = manifest.requiredByTemplate ? \" (mandatory)\" : \"\";\n const deprecatedTag = manifest.deprecated ? \" (deprecated)\" : \"\";\n console.log(\n ` ${manifest.requiredByTemplate ? \"●\" : \"○\"} ${manifest.displayName} (${name}) from ${manifest.package}${resourceInfo}${mandatoryTag}${deprecatedTag}`,\n );\n }\n }\n\n // Warn about deprecated plugins that are actually used in the app.\n if (!options.silent && usedDeprecated.length > 0) {\n for (const name of usedDeprecated) {\n console.warn(\n `Warning: plugin \"${name}\" is deprecated. See its description for the recommended replacement.`,\n );\n }\n }\n\n // Step 7: Detect orphaned resources from removed plugins\n if (!options.silent && fs.existsSync(outputPath)) {\n try {\n const oldRaw = fs.readFileSync(outputPath, \"utf-8\");\n const oldManifest = JSON.parse(oldRaw) as TemplatePluginsManifest;\n const oldNames = new Set(Object.keys(oldManifest.plugins ?? {}));\n const newNames = new Set(Object.keys(plugins));\n for (const name of oldNames) {\n if (newNames.has(name)) continue;\n const oldPlugin = oldManifest.plugins?.[name];\n if (!oldPlugin || typeof oldPlugin !== \"object\") continue;\n const envVars: string[] = [];\n for (const res of [\n ...(oldPlugin.resources?.required ?? []),\n ...(oldPlugin.resources?.optional ?? []),\n ]) {\n if (res?.fields) {\n for (const field of Object.values(res.fields)) {\n if (field?.env) envVars.push(field.env);\n }\n }\n }\n const envInfo =\n envVars.length > 0\n ? ` The following resource env vars may be orphaned: ${envVars.join(\", \")}`\n : \"\";\n console.warn(`Warning: Plugin \"${name}\" was removed.${envInfo}`);\n }\n } catch {\n // Ignore parse errors on existing manifest\n }\n }\n\n writeManifest(outputPath, { plugins }, options);\n}\n\n/** Exported for testing: path boundary check, AST parsing, trust checks, discovery paths. */\nexport {\n discoverLocalPlugins,\n isWithinDirectory,\n parseImports,\n parsePluginUsages,\n scanForPlugins,\n scanPluginsDir,\n shouldAllowJsManifestForPackage,\n};\n\nexport const pluginsSyncCommand = new Command(\"sync\")\n .description(\n \"Sync plugin manifests from installed packages into appkit.plugins.json\",\n )\n .option(\"-w, --write\", \"Write the manifest file\")\n .option(\n \"-o, --output <path>\",\n \"Output file path (default: ./appkit.plugins.json)\",\n )\n .option(\n \"-s, --silent\",\n \"Suppress output and never exit with error (for use in predev/prebuild hooks)\",\n )\n .option(\n \"--require-plugins <names>\",\n \"Comma-separated plugin names to mark as requiredByTemplate (e.g. server,analytics)\",\n )\n .option(\n \"--plugins-dir <path>\",\n \"Scan this directory for plugin subdirectories with manifest.json (instead of node_modules)\",\n )\n .option(\n \"--package-name <name>\",\n \"Package name to assign to plugins found via --plugins-dir (default: @databricks/appkit)\",\n )\n .option(\n \"--local-plugins-dir <path>\",\n \"Also scan this directory for local plugin manifests (default: plugins, server)\",\n )\n .option(\n \"--allow-js-manifest\",\n \"Allow reading manifest.js/manifest.cjs (executes code; use only with trusted plugins)\",\n )\n .option(\"--json\", \"Output manifest as JSON to stdout\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit plugin sync\n $ appkit plugin sync --write\n $ appkit plugin sync --write --require-plugins server,analytics\n $ appkit plugin sync --write --plugins-dir src/plugins --package-name @my/pkg\n $ appkit plugin sync --json\n $ appkit plugin sync --silent`,\n )\n .action((opts) =>\n runPluginsSync(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { SCOPE_BY_TYPE } from "../../../schemas/manifest.js";
1
2
  import { detectPackageManager } from "../../package-manager.js";
2
3
  import { JS_IDENTIFIER, REGISTRY_REPO, resolveToken } from "./constants.js";
3
4
  import { fetchRegistryItem, fetchVerifiedNames, isValidItemName, stripNamespace } from "./client.js";
@@ -5,10 +6,10 @@ import { extractRequirements, renderRequirements } from "./requirements.js";
5
6
  import { buildConfigPlan, collectBindingValueNeeds, planHasContent } from "./config-plan.js";
6
7
  import { reportConfigWrite, validateBundle, writeConfig } from "./config-writer.js";
7
8
  import { parseEnv } from "./env-reconcile.js";
9
+ import { spawnSync } from "node:child_process";
8
10
  import fs from "node:fs";
9
11
  import path from "node:path";
10
12
  import { Command } from "commander";
11
- import { spawnSync } from "node:child_process";
12
13
  import process from "node:process";
13
14
  import pc from "picocolors";
14
15
 
@@ -412,19 +413,10 @@ async function runAdd(refs, opts) {
412
413
  * extension). Warn when an added plugin's resource type is known to need one,
413
414
  * so the user adds it before deploy.
414
415
  */
415
- /** Resource types known to require a user_api_scope, and the scope each needs. */
416
- const SCOPE_BY_RESOURCE_TYPE = {
417
- genie_space: "dashboards.genie",
418
- serving_endpoint: "serving.serving-endpoints",
419
- volume: "files.files"
420
- };
421
416
  /** Returns the user_api_scopes implied by a set of resource rows (deduped). */
422
417
  function scopesForResources(rows) {
423
418
  const needed = /* @__PURE__ */ new Map();
424
- for (const row of rows) {
425
- const scope = SCOPE_BY_RESOURCE_TYPE[row.type];
426
- if (scope) needed.set(row.type, scope);
427
- }
419
+ for (const row of rows) if (Object.hasOwn(SCOPE_BY_TYPE, row.type)) needed.set(row.type, SCOPE_BY_TYPE[row.type]);
428
420
  return needed;
429
421
  }
430
422
  function warnScopeNeeding(rows) {