Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions packages/appkit/src/registry/schemas/template-plugins.schema.json
Original file line number Diff line number Diff line change
@@ -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
}
}
}
5 changes: 5 additions & 0 deletions packages/appkit/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
],
},
]);
Empty file modified packages/shared/bin/appkit.js
100644 β†’ 100755
Empty file.
189 changes: 189 additions & 0 deletions packages/shared/src/cli/commands/plugins-sync.ts
Original file line number Diff line number Diff line change
@@ -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<PluginManifest, "config"> {
package: string;
}

/**
* Template plugins manifest structure
*/
interface TemplatePluginsManifest {
$schema: string;
version: string;
plugins: Record<string, TemplatePlugin>;
}

/**
* 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 <path>",
"Output file path (default: ./appkit.plugins.json)",
)
.action(runPluginsSync);
16 changes: 16 additions & 0 deletions packages/shared/src/cli/commands/plugins.ts
Original file line number Diff line number Diff line change
@@ -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);
2 changes: 2 additions & 0 deletions packages/shared/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -24,5 +25,6 @@ cmd.addCommand(setupCommand);
cmd.addCommand(generateTypesCommand);
cmd.addCommand(lintCommand);
cmd.addCommand(docsCommand);
cmd.addCommand(pluginsCommand);

cmd.parse();
Loading