diff --git a/packages/appkit/src/registry/schemas/template-plugins.schema.json b/packages/appkit/src/registry/schemas/template-plugins.schema.json new file mode 100644 index 00000000..dc08e245 --- /dev/null +++ b/packages/appkit/src/registry/schemas/template-plugins.schema.json @@ -0,0 +1,142 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://databricks.github.io/appkit/schemas/template-plugins.schema.json", + "title": "AppKit Template Plugins Manifest", + "description": "Aggregated plugin manifest for AppKit templates. Read by Databricks CLI during init to discover available plugins and their resource requirements.", + "type": "object", + "required": ["version", "plugins"], + "properties": { + "$schema": { + "type": "string", + "description": "Reference to the JSON Schema for validation" + }, + "version": { + "type": "string", + "const": "1.0", + "description": "Schema version for the template plugins manifest" + }, + "plugins": { + "type": "object", + "description": "Map of plugin name to plugin manifest with package source", + "additionalProperties": { + "$ref": "#/$defs/templatePlugin" + } + } + }, + "additionalProperties": false, + "$defs": { + "templatePlugin": { + "type": "object", + "required": [ + "name", + "displayName", + "description", + "package", + "resources" + ], + "description": "Plugin manifest with package source information", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$", + "description": "Plugin identifier. Must be lowercase, start with a letter, and contain only letters, numbers, and hyphens.", + "examples": ["analytics", "server", "my-custom-plugin"] + }, + "displayName": { + "type": "string", + "minLength": 1, + "description": "Human-readable display name for UI and CLI", + "examples": ["Analytics Plugin", "Server Plugin"] + }, + "description": { + "type": "string", + "minLength": 1, + "description": "Brief description of what the plugin does", + "examples": ["SQL query execution against Databricks SQL Warehouses"] + }, + "package": { + "type": "string", + "minLength": 1, + "description": "NPM package name that provides this plugin", + "examples": ["@databricks/appkit", "@my-org/custom-plugin"] + }, + "resources": { + "type": "object", + "required": ["required", "optional"], + "description": "Databricks resource requirements for this plugin", + "properties": { + "required": { + "type": "array", + "description": "Resources that must be available for the plugin to function", + "items": { + "$ref": "#/$defs/resourceRequirement" + } + }, + "optional": { + "type": "array", + "description": "Resources that enhance functionality but are not mandatory", + "items": { + "$ref": "#/$defs/resourceRequirement" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "resourceType": { + "type": "string", + "enum": [ + "secret", + "job", + "sql_warehouse", + "serving_endpoint", + "volume", + "vector_search_index", + "uc_function", + "uc_connection", + "database", + "genie_space", + "experiment", + "app" + ], + "description": "Type of Databricks resource" + }, + "resourcePermission": { + "type": "string", + "description": "Permission level required for the resource. Valid values depend on resource type.", + "examples": ["CAN_USE", "CAN_MANAGE", "READ", "WRITE", "EXECUTE"] + }, + "resourceRequirement": { + "type": "object", + "required": ["type", "alias", "description", "permission"], + "properties": { + "type": { + "$ref": "#/$defs/resourceType" + }, + "alias": { + "type": "string", + "pattern": "^[a-z][a-zA-Z0-9_]*$", + "description": "Unique alias for this resource within the plugin", + "examples": ["warehouse", "secrets", "vectorIndex"] + }, + "description": { + "type": "string", + "minLength": 1, + "description": "Human-readable description of why this resource is needed" + }, + "permission": { + "$ref": "#/$defs/resourcePermission" + }, + "env": { + "type": "string", + "pattern": "^[A-Z][A-Z0-9_]*$", + "description": "Environment variable name where the resource ID should be provided", + "examples": ["DATABRICKS_WAREHOUSE_ID", "DATABRICKS_SECRET_SCOPE"] + } + }, + "additionalProperties": false + } + } +} diff --git a/packages/appkit/tsdown.config.ts b/packages/appkit/tsdown.config.ts index fb6cafe8..ad8c46be 100644 --- a/packages/appkit/tsdown.config.ts +++ b/packages/appkit/tsdown.config.ts @@ -51,6 +51,11 @@ export default defineConfig([ from: "src/registry/schemas/plugin-manifest.schema.json", to: "dist/registry/schemas/plugin-manifest.schema.json", }, + // JSON Schema for template plugins manifest + { + from: "src/registry/schemas/template-plugins.schema.json", + to: "dist/registry/schemas/template-plugins.schema.json", + }, ], }, ]); diff --git a/packages/shared/bin/appkit.js b/packages/shared/bin/appkit.js old mode 100644 new mode 100755 diff --git a/packages/shared/src/cli/commands/plugins-sync.ts b/packages/shared/src/cli/commands/plugins-sync.ts new file mode 100644 index 00000000..7706261a --- /dev/null +++ b/packages/shared/src/cli/commands/plugins-sync.ts @@ -0,0 +1,189 @@ +import fs from "node:fs"; +import path from "node:path"; +import { Command } from "commander"; + +/** + * Resource requirement as defined in plugin manifests + */ +interface ResourceRequirement { + type: string; + alias: string; + description: string; + permission: string; + env?: string; +} + +/** + * Plugin manifest structure (from SDK plugin manifest.json files) + */ +interface PluginManifest { + name: string; + displayName: string; + description: string; + resources: { + required: ResourceRequirement[]; + optional: ResourceRequirement[]; + }; + config?: { schema: unknown }; +} + +/** + * Plugin entry in the template manifest (includes package source) + */ +interface TemplatePlugin extends Omit { + package: string; +} + +/** + * Template plugins manifest structure + */ +interface TemplatePluginsManifest { + $schema: string; + version: string; + plugins: Record; +} + +/** + * Known packages that may contain AppKit plugins. + * The sync command will scan these packages for plugin manifests. + */ +const KNOWN_PLUGIN_PACKAGES = [ + "@databricks/appkit", + // Community packages can be added here or discovered dynamically in the future +]; + +/** + * Discover plugin manifests from a package's dist folder. + * Looks for manifest.json files in dist/plugins/{plugin-name}/ directories. + * + * @param packagePath - Path to the package in node_modules + * @returns Array of plugin manifests found in the package + */ +function discoverPluginManifests(packagePath: string): PluginManifest[] { + const pluginsDir = path.join(packagePath, "dist", "plugins"); + const manifests: PluginManifest[] = []; + + if (!fs.existsSync(pluginsDir)) { + return manifests; + } + + const entries = fs.readdirSync(pluginsDir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) { + const manifestPath = path.join(pluginsDir, entry.name, "manifest.json"); + if (fs.existsSync(manifestPath)) { + try { + const content = fs.readFileSync(manifestPath, "utf-8"); + const manifest = JSON.parse(content) as PluginManifest; + manifests.push(manifest); + } catch (error) { + console.warn( + `Warning: Failed to parse manifest at ${manifestPath}:`, + error instanceof Error ? error.message : error, + ); + } + } + } + } + + return manifests; +} + +/** + * Scan node_modules for packages with plugin manifests. + * Iterates through known plugin packages and discovers their manifests. + * + * @param cwd - Current working directory to search from + * @returns Map of plugin name to template plugin entry + */ +function scanForPlugins(cwd: string): TemplatePluginsManifest["plugins"] { + const plugins: TemplatePluginsManifest["plugins"] = {}; + + for (const packageName of KNOWN_PLUGIN_PACKAGES) { + const packagePath = path.join(cwd, "node_modules", packageName); + if (!fs.existsSync(packagePath)) { + continue; + } + + const manifests = discoverPluginManifests(packagePath); + for (const manifest of manifests) { + // Convert to template plugin format (exclude config schema) + plugins[manifest.name] = { + name: manifest.name, + displayName: manifest.displayName, + description: manifest.description, + package: packageName, + resources: manifest.resources, + }; + } + } + + return plugins; +} + +/** + * Run the plugins sync command. + * Scans for plugin manifests and generates/updates appkit.plugins.json. + */ +function runPluginsSync(options: { write?: boolean; output?: string }) { + const cwd = process.cwd(); + const outputPath = options.output || path.join(cwd, "appkit.plugins.json"); + + console.log("Scanning for AppKit plugins...\n"); + + const plugins = scanForPlugins(cwd); + const pluginCount = Object.keys(plugins).length; + + if (pluginCount === 0) { + console.log("No plugins found in node_modules."); + console.log("\nMake sure you have plugin packages installed:"); + for (const pkg of KNOWN_PLUGIN_PACKAGES) { + console.log(` - ${pkg}`); + } + process.exit(1); + } + + console.log(`Found ${pluginCount} plugin(s):`); + for (const [name, manifest] of Object.entries(plugins)) { + const resourceCount = + manifest.resources.required.length + manifest.resources.optional.length; + const resourceInfo = + resourceCount > 0 ? ` [${resourceCount} resource(s)]` : ""; + console.log( + ` āœ“ ${manifest.displayName} (${name}) from ${manifest.package}${resourceInfo}`, + ); + } + + const templateManifest: TemplatePluginsManifest = { + $schema: + "https://databricks.github.io/appkit/schemas/template-plugins.schema.json", + version: "1.0", + plugins, + }; + + if (options.write) { + fs.writeFileSync( + outputPath, + JSON.stringify(templateManifest, null, 2) + "\n", + ); + console.log(`\nāœ“ Wrote ${outputPath}`); + } else { + console.log("\nTo write the manifest, run:"); + console.log(" npx appkit plugins sync --write\n"); + console.log("Preview:"); + console.log("─".repeat(60)); + console.log(JSON.stringify(templateManifest, null, 2)); + console.log("─".repeat(60)); + } +} + +export const pluginsSyncCommand = new Command("sync") + .description( + "Sync plugin manifests from installed packages into appkit.plugins.json", + ) + .option("-w, --write", "Write the manifest file") + .option( + "-o, --output ", + "Output file path (default: ./appkit.plugins.json)", + ) + .action(runPluginsSync); diff --git a/packages/shared/src/cli/commands/plugins.ts b/packages/shared/src/cli/commands/plugins.ts new file mode 100644 index 00000000..ff1de368 --- /dev/null +++ b/packages/shared/src/cli/commands/plugins.ts @@ -0,0 +1,16 @@ +import { Command } from "commander"; +import { pluginsSyncCommand } from "./plugins-sync.js"; + +/** + * Parent command for plugin management operations. + * Subcommands: + * - sync: Aggregate plugin manifests into appkit.plugins.json + * + * Future subcommands may include: + * - add: Add a plugin to an existing project + * - remove: Remove a plugin from a project + * - list: List available plugins + */ +export const pluginsCommand = new Command("plugins") + .description("Plugin management commands") + .addCommand(pluginsSyncCommand); diff --git a/packages/shared/src/cli/index.ts b/packages/shared/src/cli/index.ts index 3b3c0293..23a19a53 100644 --- a/packages/shared/src/cli/index.ts +++ b/packages/shared/src/cli/index.ts @@ -7,6 +7,7 @@ import { Command } from "commander"; import { docsCommand } from "./commands/docs.js"; import { generateTypesCommand } from "./commands/generate-types.js"; import { lintCommand } from "./commands/lint.js"; +import { pluginsCommand } from "./commands/plugins.js"; import { setupCommand } from "./commands/setup.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -24,5 +25,6 @@ cmd.addCommand(setupCommand); cmd.addCommand(generateTypesCommand); cmd.addCommand(lintCommand); cmd.addCommand(docsCommand); +cmd.addCommand(pluginsCommand); cmd.parse(); diff --git a/template/appkit.plugins.json b/template/appkit.plugins.json new file mode 100644 index 00000000..23216b4d --- /dev/null +++ b/template/appkit.plugins.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://databricks.github.io/appkit/schemas/template-plugins.schema.json", + "version": "1.0", + "plugins": { + "server": { + "name": "server", + "displayName": "Server Plugin", + "description": "HTTP server with Express, static file serving, and Vite dev mode support", + "package": "@databricks/appkit", + "resources": { + "required": [], + "optional": [] + } + }, + "analytics": { + "name": "analytics", + "displayName": "Analytics Plugin", + "description": "SQL query execution against Databricks SQL Warehouses", + "package": "@databricks/appkit", + "resources": { + "required": [ + { + "type": "sql_warehouse", + "alias": "warehouse", + "description": "SQL Warehouse for executing analytics queries", + "permission": "CAN_USE", + "env": "DATABRICKS_WAREHOUSE_ID" + } + ], + "optional": [] + } + } + } +} diff --git a/template/databricks.yml.tmpl b/template/databricks.yml.tmpl index cdfa3fe0..74ff0455 100644 --- a/template/databricks.yml.tmpl +++ b/template/databricks.yml.tmpl @@ -1,9 +1,9 @@ bundle: name: {{.project_name}} -{{if .bundle_variables}} +{{if .variables}} variables: -{{.bundle_variables}} +{{.variables}} {{- end}} resources: @@ -16,16 +16,15 @@ resources: # Uncomment to enable on behalf of user API scopes. Available scopes: sql, dashboards.genie, files.files # user_api_scopes: # - sql -{{if .bundle_resources}} +{{if .resources}} # The resources which this app has access to. resources: -{{.bundle_resources}} +{{.resources}} {{- end}} targets: default: - # mode: production default: true workspace: host: {{workspace_host}} diff --git a/template/features/analytics/app_env.yml b/template/features/analytics/app_env.yml deleted file mode 100644 index 9228a9dd..00000000 --- a/template/features/analytics/app_env.yml +++ /dev/null @@ -1,2 +0,0 @@ - - name: DATABRICKS_WAREHOUSE_ID - valueFrom: warehouse diff --git a/template/features/analytics/bundle_resources.yml b/template/features/analytics/bundle_resources.yml deleted file mode 100644 index b3a631c0..00000000 --- a/template/features/analytics/bundle_resources.yml +++ /dev/null @@ -1,4 +0,0 @@ - - name: 'warehouse' - sql_warehouse: - id: ${var.warehouse_id} - permission: 'CAN_USE' diff --git a/template/features/analytics/bundle_variables.yml b/template/features/analytics/bundle_variables.yml deleted file mode 100644 index ac4fbf15..00000000 --- a/template/features/analytics/bundle_variables.yml +++ /dev/null @@ -1,2 +0,0 @@ - warehouse_id: - description: The ID of the warehouse to use diff --git a/template/features/analytics/dotenv.yml b/template/features/analytics/dotenv.yml deleted file mode 100644 index 7d17f13c..00000000 --- a/template/features/analytics/dotenv.yml +++ /dev/null @@ -1 +0,0 @@ -DATABRICKS_WAREHOUSE_ID={{.sql_warehouse_id}} diff --git a/template/features/analytics/dotenv_example.yml b/template/features/analytics/dotenv_example.yml deleted file mode 100644 index 1ae1aa74..00000000 --- a/template/features/analytics/dotenv_example.yml +++ /dev/null @@ -1 +0,0 @@ -DATABRICKS_WAREHOUSE_ID= diff --git a/template/features/analytics/target_variables.yml b/template/features/analytics/target_variables.yml deleted file mode 100644 index 0de7b63b..00000000 --- a/template/features/analytics/target_variables.yml +++ /dev/null @@ -1 +0,0 @@ - warehouse_id: {{.sql_warehouse_id}} diff --git a/template/server/server.ts b/template/server/server.ts index da041927..b36fb1fb 100644 --- a/template/server/server.ts +++ b/template/server/server.ts @@ -1,8 +1,7 @@ -import { createApp, server, {{.plugin_import}} } from '@databricks/appkit'; +import { createApp, server, {{.plugin_imports}} } from '@databricks/appkit'; createApp({ plugins: [ server(), - {{.plugin_usage}}, - ], +{{.plugin_usages}} ], }).catch(console.error);