Skip to content
4 changes: 4 additions & 0 deletions docs/docs/plugins/custom-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ sidebar_position: 7
If you need custom API routes or background logic, implement an AppKit plugin. The fastest way is to use the CLI:

```bash
# Interactive
npx @databricks/appkit plugin create

# Non-interactive
npx @databricks/appkit plugin create --placement in-repo --path plugins/my-plugin --name my-plugin --description "My plugin" --force
```

For a deeper understanding of the plugin structure, read on.
Expand Down
28 changes: 22 additions & 6 deletions docs/docs/plugins/plugin-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,28 @@ AppKit includes a CLI for managing plugins. All commands are available under `np

## Create a plugin

Scaffold a new plugin interactively:
Scaffold a new plugin interactively or via flags:

```bash
# Interactive mode (prompts for all options)
npx @databricks/appkit plugin create

# Non-interactive mode (all required flags provided)
npx @databricks/appkit plugin create \
--placement in-repo \
--path plugins/my-plugin \
--name my-plugin \
--description "My custom plugin" \
--resources sql_warehouse \
--force
```

The wizard walks you through:
In interactive mode, the wizard walks you through:
- **Placement**: In your repository (e.g. `plugins/my-plugin`) or as a standalone package
- **Metadata**: Name, display name, description
- **Resources**: Which Databricks resources the plugin needs (SQL Warehouse, Secret, etc.) and whether each is required or optional
- **Optional fields**: Author, version, license

In non-interactive mode, `--placement`, `--path`, `--name`, and `--description` are required. Resources can be specified as a comma-separated list (`--resources sql_warehouse,volume`) or as JSON for full control (`--resources-json '[{"type":"sql_warehouse","permission":"CAN_MANAGE"}]'`). For all available options, run `npx @databricks/appkit plugin create --help`.

The command generates a complete plugin scaffold with `manifest.json` and a TypeScript plugin class that imports the manifest directly — ready to register in your app.

Expand Down Expand Up @@ -88,11 +99,16 @@ npx @databricks/appkit plugin list --json

## Add a resource to a plugin

Interactively add a new resource requirement to an existing plugin manifest. **Requires `manifest.json`** in the plugin directory (the command edits it in place; it does not modify `manifest.js`):
Add a new resource requirement to an existing plugin manifest. **Requires `manifest.json`** in the plugin directory (the command edits it in place; it does not modify `manifest.js`):

```bash
# Interactive mode
npx @databricks/appkit plugin add-resource

# Or specify the plugin directory
npx @databricks/appkit plugin add-resource --path plugins/my-plugin

# Non-interactive mode (--type triggers flag-based mode)
npx @databricks/appkit plugin add-resource --path plugins/my-plugin --type sql_warehouse
npx @databricks/appkit plugin add-resource --path plugins/my-plugin --type volume --no-required --dry-run
```

In non-interactive mode, only `--type` is required — all other fields (permission, resource key, field env vars) default to sensible values from the schema. Use `--dry-run` to preview the updated manifest without writing. For all available options, run `npx @databricks/appkit plugin add-resource --help`.
10 changes: 10 additions & 0 deletions packages/shared/src/cli/commands/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,14 @@ export const docsCommand = new Command("docs")
"Section name (e.g. 'plugins') or path to a doc file (e.g. './docs.md')",
)
.option("--full", "Show complete index including all API reference entries")
.addHelpText(
"after",
`
Examples:
$ appkit docs
$ appkit docs plugins
$ appkit docs "appkit-ui API reference"
$ appkit docs ./docs/plugins/analytics.md
$ appkit docs --full`,
)
.action(runDocs);
21 changes: 20 additions & 1 deletion packages/shared/src/cli/commands/generate-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,24 @@ async function runGenerateTypes(
warehouseId: resolvedWarehouseId,
noCache,
});
console.log(`Generated query types: ${resolvedOutFile}`);
}
} else {
console.error(
"Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.",
);
}

// Generate serving endpoint types (no warehouse required)
const servingOutFile = path.join(
process.cwd(),
"client/src/appKitServingTypes.d.ts",
);
await typeGen.generateServingTypes({
outFile: path.join(process.cwd(), "client/src/appKitServingTypes.d.ts"),
outFile: servingOutFile,
noCache,
});
console.log(`Generated serving types: ${servingOutFile}`);
} catch (error) {
if (
error instanceof Error &&
Expand All @@ -66,4 +76,13 @@ export const generateTypesCommand = new Command("generate-types")
)
.argument("[warehouseId]", "Databricks warehouse ID")
.option("--no-cache", "Disable caching for type generation")
.addHelpText(
"after",
`
Examples:
$ appkit generate-types
$ appkit generate-types . client/src/types.d.ts
$ appkit generate-types . client/src/types.d.ts my-warehouse-id
$ appkit generate-types --no-cache`,
)
.action(runGenerateTypes);
6 changes: 6 additions & 0 deletions packages/shared/src/cli/commands/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,10 @@ function runLint() {

export const lintCommand = new Command("lint")
.description("Run AST-based linting on TypeScript files")
.addHelpText(
"after",
`
Examples:
$ appkit lint`,
)
.action(runLint);
150 changes: 139 additions & 11 deletions packages/shared/src/cli/commands/plugin/add-resource/add-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import process from "node:process";
import { cancel, intro, outro } from "@clack/prompts";
import { Command } from "commander";
import { promptOneResource } from "../create/prompt-resource";
import { humanizeResourceType } from "../create/resource-defaults";
import {
DEFAULT_PERMISSION_BY_TYPE,
getDefaultFieldsForType,
getValidResourceTypes,
humanizeResourceType,
resourceKeyFromType,
} from "../create/resource-defaults";
import { resolveManifestInDir } from "../manifest-resolve";
import type { PluginManifest, ResourceRequirement } from "../manifest-types";
import { validateManifest } from "../validate/validate-manifest";
Expand All @@ -14,17 +20,29 @@ interface ManifestWithExtras extends PluginManifest {
[key: string]: unknown;
}

async function runPluginAddResource(options: { path?: string }): Promise<void> {
intro("Add resource to plugin manifest");
interface AddResourceOptions {
path?: string;
type?: string;
required?: boolean;
resourceKey?: string;
description?: string;
permission?: string;
fieldsJson?: string;
dryRun?: boolean;
}

const cwd = process.cwd();
const pluginDir = path.resolve(cwd, options.path ?? ".");
function loadManifest(
pluginDir: string,
): { manifest: ManifestWithExtras; manifestPath: string } | null {
const resolved = resolveManifestInDir(pluginDir, { allowJsManifest: true });

if (!resolved) {
console.error(
`No manifest found in ${pluginDir}. This command requires manifest.json (manifest.js cannot be edited in place).`,
);
console.error(
" appkit plugin add-resource --path <dir-with-manifest.json>",
);
process.exit(1);
}

Expand All @@ -37,7 +55,6 @@ async function runPluginAddResource(options: { path?: string }): Promise<void> {

const manifestPath = resolved.path;

let manifest: ManifestWithExtras;
try {
const raw = fs.readFileSync(manifestPath, "utf-8");
const parsed = JSON.parse(raw) as unknown;
Expand All @@ -48,14 +65,96 @@ async function runPluginAddResource(options: { path?: string }): Promise<void> {
);
process.exit(1);
}
manifest = parsed as ManifestWithExtras;
return { manifest: parsed as ManifestWithExtras, manifestPath };
} catch (err) {
console.error(
"Failed to read or parse manifest.json:",
err instanceof Error ? err.message : err,
);
process.exit(1);
}
}

function buildEntry(
type: string,
opts: AddResourceOptions,
): { entry: ResourceRequirement; isRequired: boolean } {
const alias = humanizeResourceType(type);
const isRequired = opts.required !== false;

let fields = getDefaultFieldsForType(type);
if (opts.fieldsJson) {
try {
const parsed = JSON.parse(opts.fieldsJson) as Record<
string,
{ env: string; description?: string }
>;
fields = { ...fields, ...parsed };
} catch {
console.error("Error: --fields-json must be valid JSON.");
console.error(
' Example: --fields-json \'{"id":{"env":"MY_WAREHOUSE_ID"}}\'',
);
process.exit(1);
}
}

const entry: ResourceRequirement = {
type: type as ResourceRequirement["type"],
alias,
resourceKey: opts.resourceKey ?? resourceKeyFromType(type),
description:
opts.description ||
`${isRequired ? "Required" : "Optional"} for ${alias} functionality.`,
permission:
opts.permission ?? DEFAULT_PERMISSION_BY_TYPE[type] ?? "CAN_VIEW",
fields,
};

return { entry, isRequired };
}

function runNonInteractive(opts: AddResourceOptions): void {
const cwd = process.cwd();
const pluginDir = path.resolve(cwd, opts.path ?? ".");
const loaded = loadManifest(pluginDir);
if (!loaded) return;
const { manifest, manifestPath } = loaded;

const type = opts.type as string;
const validTypes = getValidResourceTypes();
if (!validTypes.includes(type)) {
console.error(`Error: Unknown resource type "${type}".`);
console.error(` Valid types: ${validTypes.join(", ")}`);
process.exit(1);
}
const { entry, isRequired } = buildEntry(type, opts);

if (isRequired) {
manifest.resources.required.push(entry);
} else {
manifest.resources.optional.push(entry);
}

if (opts.dryRun) {
console.log(JSON.stringify(manifest, null, 2));
return;
}

fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
console.log(
`Added ${entry.alias} as ${isRequired ? "required" : "optional"} to ${path.relative(cwd, manifestPath)}`,
);
}

async function runInteractive(opts: AddResourceOptions): Promise<void> {
intro("Add resource to plugin manifest");

const cwd = process.cwd();
const pluginDir = path.resolve(cwd, opts.path ?? ".");
const loaded = loadManifest(pluginDir);
if (!loaded) return;
const { manifest, manifestPath } = loaded;

const spec = await promptOneResource();
if (!spec) {
Expand All @@ -65,8 +164,6 @@ async function runPluginAddResource(options: { path?: string }): Promise<void> {

const alias = humanizeResourceType(spec.type);
const entry: ResourceRequirement = {
// Safe cast: spec.type comes from RESOURCE_TYPE_OPTIONS which reads values
// from the same JSON schema that generates the ResourceType union.
type: spec.type as ResourceRequirement["type"],
alias,
resourceKey: spec.resourceKey,
Expand All @@ -89,13 +186,44 @@ async function runPluginAddResource(options: { path?: string }): Promise<void> {
);
}

async function runPluginAddResource(opts: AddResourceOptions): Promise<void> {
if (opts.type) {
runNonInteractive(opts);
} else {
await runInteractive(opts);
}
}

export const pluginAddResourceCommand = new Command("add-resource")
.description(
"Add a resource requirement to an existing plugin manifest (interactive). Overwrites manifest.json in place.",
"Add a resource requirement to an existing plugin manifest. Overwrites manifest.json in place.",
)
.option(
"-p, --path <dir>",
"Plugin directory containing manifest.json, which will be edited in place (default: .)",
"Plugin directory containing manifest.json (default: .)",
)
.option(
"-t, --type <resource_type>",
"Resource type (e.g. sql_warehouse, volume). Enables non-interactive mode.",
)
.option("--required", "Mark resource as required (default: true)", true)
.option("--no-required", "Mark resource as optional")
.option("--resource-key <key>", "Resource key (default: derived from type)")
.option("--description <text>", "Description of the resource requirement")
.option("--permission <perm>", "Permission level (default: from schema)")
.option(
"--fields-json <json>",
'JSON object overriding field env vars (e.g. \'{"id":{"env":"MY_WAREHOUSE_ID"}}\')',
)
.option("--dry-run", "Preview the updated manifest without writing")
.addHelpText(
"after",
`
Examples:
$ appkit plugin add-resource
$ appkit plugin add-resource --path plugins/my-plugin --type sql_warehouse
$ appkit plugin add-resource --path plugins/my-plugin --type volume --no-required --dry-run
$ appkit plugin add-resource --type sql_warehouse --fields-json '{"id":{"env":"MY_WAREHOUSE_ID"}}'`,
)
.action((opts) =>
runPluginAddResource(opts).catch((err) => {
Expand Down
Loading
Loading