From 5aad0ce6072284cd228dc593fda0a64cc7b118ce Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Mon, 16 Mar 2026 18:49:50 +0100 Subject: [PATCH] chore: poc manifest 4 --- .../api/appkit/Interface.PluginManifest.md | 32 ++++ .../appkit/Interface.ResourceFieldEntry.md | 18 ++ .../schemas/plugin-manifest.schema.json | 76 ++++++++ .../schemas/template-plugins.schema.json | 67 +++++++- .../src/plugins/analytics/manifest.json | 25 ++- .../appkit/src/plugins/files/manifest.json | 18 +- .../appkit/src/plugins/genie/manifest.json | 10 +- .../appkit/src/plugins/lakebase/manifest.json | 56 +++++- .../src/cli/commands/plugin/list/list.test.ts | 14 +- .../src/cli/commands/plugin/manifest-types.ts | 14 ++ .../src/cli/commands/plugin/sync/sync.test.ts | 42 +++++ .../src/cli/commands/plugin/sync/sync.ts | 53 +++++- .../plugin/validate/validate-manifest.test.ts | 33 +++- .../src/schemas/plugin-manifest.generated.ts | 61 +++++++ .../src/schemas/plugin-manifest.schema.json | 76 ++++++++ .../src/schemas/template-plugins.schema.json | 67 +++++++- template/appkit.plugins.json | 162 ++++++++++++++++-- 17 files changed, 787 insertions(+), 37 deletions(-) diff --git a/docs/docs/api/appkit/Interface.PluginManifest.md b/docs/docs/api/appkit/Interface.PluginManifest.md index 84ff2487..38a22d7f 100644 --- a/docs/docs/api/appkit/Interface.PluginManifest.md +++ b/docs/docs/api/appkit/Interface.PluginManifest.md @@ -21,6 +21,22 @@ Extends the shared PluginManifest with strict resource types. ## Properties +### agentHint? + +```ts +optional agentHint: string; +``` + +Short free-text hint for agents on how to discover resource values (e.g. which CLI command to run). Complements the structured discovery fields. + +#### Inherited from + +```ts +Omit.agentHint +``` + +*** + ### author? ```ts @@ -168,6 +184,22 @@ Omit.onSetupMessage *** +### postScaffold? + +```ts +optional postScaffold: PostScaffoldStep[]; +``` + +Ordered steps a user or agent should follow after scaffolding. + +#### Inherited from + +```ts +Omit.postScaffold +``` + +*** + ### repository? ```ts diff --git a/docs/docs/api/appkit/Interface.ResourceFieldEntry.md b/docs/docs/api/appkit/Interface.ResourceFieldEntry.md index 324a82f0..18e7a5c1 100644 --- a/docs/docs/api/appkit/Interface.ResourceFieldEntry.md +++ b/docs/docs/api/appkit/Interface.ResourceFieldEntry.md @@ -27,6 +27,14 @@ Human-readable description for this field *** +### discovery? + +```ts +optional discovery: DiscoveryDescriptor; +``` + +*** + ### env? ```ts @@ -57,6 +65,16 @@ When true, this field is only generated for local .env files. The Databricks App *** +### resolution? + +```ts +optional resolution: "user-provided" | "platform-injected"; +``` + +Who provides the value. 'user-provided': agent/user must supply it via --set. 'platform-injected': auto-set by the platform at deploy time. + +*** + ### resolve? ```ts diff --git a/docs/static/schemas/plugin-manifest.schema.json b/docs/static/schemas/plugin-manifest.schema.json index ed4ef573..228735a2 100644 --- a/docs/static/schemas/plugin-manifest.schema.json +++ b/docs/static/schemas/plugin-manifest.schema.json @@ -91,6 +91,17 @@ "type": "string", "description": "Message displayed to the user after project initialization. Use this to inform about manual setup steps (e.g. environment variables, resource provisioning)." }, + "agentHint": { + "type": "string", + "description": "Short free-text hint for agents on how to discover resource values (e.g. which CLI command to run). Complements the structured discovery fields." + }, + "postScaffold": { + "type": "array", + "description": "Ordered steps a user or agent should follow after scaffolding.", + "items": { + "$ref": "#/$defs/postScaffoldStep" + } + }, "hidden": { "type": "boolean", "default": false, @@ -220,6 +231,71 @@ "type": "string", "pattern": "^[a-z_]+:[a-zA-Z]+$", "description": "Named resolver prefixed by resource type (e.g., 'postgres:host'). The CLI resolves this value during the init prompt flow." + }, + "discovery": { + "$ref": "#/$defs/discoveryDescriptor" + }, + "resolution": { + "type": "string", + "enum": ["user-provided", "platform-injected"], + "description": "Who provides the value. 'user-provided': agent/user must supply it via --set. 'platform-injected': auto-set by the platform at deploy time." + } + }, + "additionalProperties": false + }, + "discoveryDescriptor": { + "type": "object", + "required": ["cliCommand", "selectField"], + "description": "Describes how an agent or CLI can discover a candidate value for this field.", + "properties": { + "cliCommand": { + "type": "string", + "description": "CLI command to list candidate values. Use as placeholder for the Databricks profile.", + "examples": ["databricks warehouses list --profile -o json"] + }, + "selectField": { + "type": "string", + "description": "jq-style field path to extract the value from each result item (e.g. \".id\").", + "examples": [".id", ".name"] + }, + "displayField": { + "type": "string", + "description": "jq-style field path for a human-readable label (e.g. \".name\").", + "examples": [".name", ".title"] + }, + "dependsOn": { + "type": "string", + "description": "Field name in the same resource that must be resolved first." + }, + "shortcut": { + "type": "string", + "description": "Optional command that returns a single value directly, bypassing the list-and-select flow.", + "examples": [ + "databricks experimental aitools tools get-default-warehouse --profile " + ] + } + }, + "additionalProperties": false + }, + "postScaffoldStep": { + "type": "object", + "required": ["step", "instruction"], + "description": "A single ordered step for post-scaffold setup.", + "properties": { + "step": { + "type": "integer", + "minimum": 1, + "description": "Step number (must be unique and sequential within a plugin)." + }, + "instruction": { + "type": "string", + "minLength": 1, + "description": "Human or agent-readable instruction for this step." + }, + "blocking": { + "type": "boolean", + "default": false, + "description": "When true, this step must complete before proceeding to the next." } }, "additionalProperties": false diff --git a/docs/static/schemas/template-plugins.schema.json b/docs/static/schemas/template-plugins.schema.json index 290edd05..1e3c01fa 100644 --- a/docs/static/schemas/template-plugins.schema.json +++ b/docs/static/schemas/template-plugins.schema.json @@ -4,7 +4,7 @@ "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"], + "required": ["version", "scaffolding", "plugins"], "properties": { "$schema": { "type": "string", @@ -12,9 +12,12 @@ }, "version": { "type": "string", - "const": "1.0", + "const": "2.0", "description": "Schema version for the template plugins manifest" }, + "scaffolding": { + "$ref": "#/$defs/scaffoldingDescriptor" + }, "plugins": { "type": "object", "description": "Map of plugin name to plugin manifest with package source", @@ -69,6 +72,17 @@ "type": "string", "description": "Message displayed to the user after project initialization. Use this to inform about manual setup steps (e.g. environment variables, resource provisioning)." }, + "agentHint": { + "type": "string", + "description": "Short free-text hint for agents on how to discover resource values (e.g. which CLI command to run)." + }, + "postScaffold": { + "type": "array", + "description": "Ordered steps a user or agent should follow after scaffolding.", + "items": { + "$ref": "plugin-manifest.schema.json#/$defs/postScaffoldStep" + } + }, "resources": { "type": "object", "required": ["required", "optional"], @@ -102,6 +116,55 @@ }, "resourceRequirement": { "$ref": "plugin-manifest.schema.json#/$defs/resourceRequirement" + }, + "scaffoldingFlag": { + "type": "object", + "required": ["required", "description"], + "properties": { + "required": { + "type": "boolean", + "description": "Whether this flag must be provided." + }, + "description": { + "type": "string", + "description": "Human-readable description of the flag." + }, + "pattern": { + "type": "string", + "description": "Regex pattern the flag value must match." + }, + "default": { + "type": "string", + "description": "Default value when the flag is omitted." + } + }, + "additionalProperties": false + }, + "scaffoldingDescriptor": { + "type": "object", + "required": ["command", "flags", "rules"], + "description": "Describes how to construct the scaffolding command for this template.", + "properties": { + "command": { + "type": "string", + "description": "The base CLI command for scaffolding." + }, + "flags": { + "type": "object", + "description": "Map of flag name to flag descriptor.", + "additionalProperties": { + "$ref": "#/$defs/scaffoldingFlag" + } + }, + "rules": { + "type": "array", + "description": "Rules an agent must follow when constructing the scaffolding command.", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false } } } diff --git a/packages/appkit/src/plugins/analytics/manifest.json b/packages/appkit/src/plugins/analytics/manifest.json index 4a6a60c2..fce49a7b 100644 --- a/packages/appkit/src/plugins/analytics/manifest.json +++ b/packages/appkit/src/plugins/analytics/manifest.json @@ -3,6 +3,7 @@ "name": "analytics", "displayName": "Analytics Plugin", "description": "SQL query execution against Databricks SQL Warehouses", + "agentHint": "Run 'databricks warehouses list' to find your SQL Warehouse ID.", "resources": { "required": [ { @@ -14,7 +15,14 @@ "fields": { "id": { "env": "DATABRICKS_WAREHOUSE_ID", - "description": "SQL Warehouse ID" + "description": "SQL Warehouse ID", + "resolution": "user-provided", + "discovery": { + "cliCommand": "databricks warehouses list --profile -o json", + "selectField": ".id", + "displayField": ".name", + "shortcut": "databricks experimental aitools tools get-default-warehouse --profile " + } } } } @@ -32,5 +40,18 @@ } } } - } + }, + "postScaffold": [ + { "step": 1, "instruction": "Create SQL query files in config/queries/" }, + { "step": 2, "instruction": "Run: npm run typegen", "blocking": true }, + { + "step": 3, + "instruction": "Read client/src/appKitTypes.d.ts for generated types" + }, + { "step": 4, "instruction": "Write UI code using the generated types" }, + { + "step": 5, + "instruction": "Update tests/smoke.spec.ts selectors for your app" + } + ] } diff --git a/packages/appkit/src/plugins/files/manifest.json b/packages/appkit/src/plugins/files/manifest.json index c886deca..be6015f1 100644 --- a/packages/appkit/src/plugins/files/manifest.json +++ b/packages/appkit/src/plugins/files/manifest.json @@ -3,6 +3,7 @@ "name": "files", "displayName": "Files Plugin", "description": "File operations against Databricks Volumes and Unity Catalog", + "agentHint": "Provide the full volume path, e.g. /Volumes/catalog/schema/volume_name.", "resources": { "required": [ { @@ -14,7 +15,13 @@ "fields": { "path": { "env": "DATABRICKS_VOLUME_FILES", - "description": "Volume path for file storage (e.g. /Volumes/catalog/schema/volume_name)" + "description": "Volume path for file storage (e.g. /Volumes/catalog/schema/volume_name)", + "resolution": "user-provided", + "discovery": { + "cliCommand": "databricks volumes list . --profile -o json", + "selectField": ".full_name", + "displayField": ".name" + } } } } @@ -37,5 +44,12 @@ } } } - } + }, + "postScaffold": [ + { + "step": 1, + "instruction": "Use the files plugin API to read/write volume files in your tRPC procedures" + }, + { "step": 2, "instruction": "Build UI for file upload/download using tRPC" } + ] } diff --git a/packages/appkit/src/plugins/genie/manifest.json b/packages/appkit/src/plugins/genie/manifest.json index a269795d..2e2cf234 100644 --- a/packages/appkit/src/plugins/genie/manifest.json +++ b/packages/appkit/src/plugins/genie/manifest.json @@ -1,7 +1,9 @@ { + "$schema": "https://databricks.github.io/appkit/schemas/plugin-manifest.schema.json", "name": "genie", "displayName": "Genie Plugin", "description": "AI/BI Genie space integration for natural language data queries", + "agentHint": "Find your Genie Space ID in the AI/BI Genie UI.", "resources": { "required": [ { @@ -13,7 +15,13 @@ "fields": { "id": { "env": "DATABRICKS_GENIE_SPACE_ID", - "description": "Default Genie Space ID" + "description": "Default Genie Space ID", + "resolution": "user-provided", + "discovery": { + "cliCommand": "databricks genie list-spaces --profile -o json", + "selectField": ".space_id", + "displayField": ".title" + } } } } diff --git a/packages/appkit/src/plugins/lakebase/manifest.json b/packages/appkit/src/plugins/lakebase/manifest.json index 2959c092..684d30fa 100644 --- a/packages/appkit/src/plugins/lakebase/manifest.json +++ b/packages/appkit/src/plugins/lakebase/manifest.json @@ -4,6 +4,7 @@ "displayName": "Lakebase", "description": "SQL query execution against Databricks Lakebase Autoscaling", "hidden": false, + "agentHint": "Run 'databricks postgres list-branches' to find your Lakebase branch.", "resources": { "required": [ { @@ -15,25 +16,38 @@ "fields": { "branch": { "description": "Full Lakebase Postgres branch resource name. Obtain by running `databricks postgres list-branches projects/{project-id}`, select the desired item from the output array and use its .name value.", - "examples": ["projects/{project-id}/branches/{branch-id}"] + "examples": ["projects/{project-id}/branches/{branch-id}"], + "resolution": "user-provided", + "discovery": { + "cliCommand": "databricks postgres list-branches projects/{project-id} --profile -o json", + "selectField": ".name" + } }, "database": { "description": "Full Lakebase Postgres database resource name. Obtain by running `databricks postgres list-databases {branch-name}`, select the desired item from the output array and use its .name value. Requires the branch resource name.", "examples": [ "projects/{project-id}/branches/{branch-id}/databases/{database-id}" - ] + ], + "resolution": "user-provided", + "discovery": { + "cliCommand": "databricks postgres list-databases {branch} --profile -o json", + "selectField": ".name", + "dependsOn": "branch" + } }, "host": { "env": "PGHOST", "localOnly": true, "resolve": "postgres:host", - "description": "Postgres host for local development. Auto-injected by the platform at deploy time." + "description": "Postgres host for local development. Auto-injected by the platform at deploy time.", + "resolution": "platform-injected" }, "databaseName": { "env": "PGDATABASE", "localOnly": true, "resolve": "postgres:databaseName", - "description": "Postgres database name for local development. Auto-injected by the platform at deploy time." + "description": "Postgres database name for local development. Auto-injected by the platform at deploy time.", + "resolution": "platform-injected" }, "endpointPath": { "env": "LAKEBASE_ENDPOINT", @@ -42,23 +56,49 @@ "description": "Lakebase endpoint resource name. Auto-injected at runtime via app.yaml valueFrom: postgres. For local development, obtain by running `databricks postgres list-endpoints {branch-name}`, select the desired item from the output array and use its .name value.", "examples": [ "projects/{project-id}/branches/{branch-id}/endpoints/{endpoint-id}" - ] + ], + "resolution": "user-provided", + "discovery": { + "cliCommand": "databricks postgres list-endpoints {branch} --profile -o json", + "selectField": ".name", + "dependsOn": "branch" + } }, "port": { "env": "PGPORT", "localOnly": true, "value": "5432", - "description": "Postgres port. Auto-injected by the platform at deploy time." + "description": "Postgres port. Auto-injected by the platform at deploy time.", + "resolution": "platform-injected" }, "sslmode": { "env": "PGSSLMODE", "localOnly": true, "value": "require", - "description": "Postgres SSL mode. Auto-injected by the platform at deploy time." + "description": "Postgres SSL mode. Auto-injected by the platform at deploy time.", + "resolution": "platform-injected" } } } ], "optional": [] - } + }, + "postScaffold": [ + { + "step": 1, + "instruction": "Define your schema in server/server.ts startup" + }, + { + "step": 2, + "instruction": "Write tRPC procedures using pool.query() in server/server.ts" + }, + { + "step": 3, + "instruction": "Build React frontend consuming tRPC procedures" + }, + { + "step": 4, + "instruction": "Update tests/smoke.spec.ts selectors for your app" + } + ] } diff --git a/packages/shared/src/cli/commands/plugin/list/list.test.ts b/packages/shared/src/cli/commands/plugin/list/list.test.ts index 2315362c..912b2ead 100644 --- a/packages/shared/src/cli/commands/plugin/list/list.test.ts +++ b/packages/shared/src/cli/commands/plugin/list/list.test.ts @@ -23,7 +23,12 @@ function cleanDir(dir: string): void { const TEMPLATE_MANIFEST_JSON = { $schema: "https://databricks.github.io/appkit/schemas/template-plugins.schema.json", - version: "1.0", + version: "2.0", + scaffolding: { + command: "databricks apps init", + flags: { "--name": { required: true, description: "App name" } }, + rules: [], + }, plugins: { server: { name: "server", @@ -94,7 +99,12 @@ describe("list", () => { JSON.stringify({ $schema: "https://databricks.github.io/appkit/schemas/template-plugins.schema.json", - version: "1.0", + version: "2.0", + scaffolding: { + command: "databricks apps init", + flags: { "--name": { required: true, description: "App name" } }, + rules: [], + }, plugins: {}, }), ); diff --git a/packages/shared/src/cli/commands/plugin/manifest-types.ts b/packages/shared/src/cli/commands/plugin/manifest-types.ts index 1d896f49..44f24a3c 100644 --- a/packages/shared/src/cli/commands/plugin/manifest-types.ts +++ b/packages/shared/src/cli/commands/plugin/manifest-types.ts @@ -13,6 +13,19 @@ export type { import type { PluginManifest } from "../../../schemas/plugin-manifest.generated"; +export interface ScaffoldingFlag { + required: boolean; + description: string; + pattern?: string; + default?: string; +} + +export interface ScaffoldingDescriptor { + command: string; + flags: Record; + rules: string[]; +} + export interface TemplatePlugin extends Omit { package: string; /** When true, this plugin is required by the template and cannot be deselected during CLI init. */ @@ -22,5 +35,6 @@ export interface TemplatePlugin extends Omit { export interface TemplatePluginsManifest { $schema: string; version: string; + scaffolding: ScaffoldingDescriptor; plugins: Record; } diff --git a/packages/shared/src/cli/commands/plugin/sync/sync.test.ts b/packages/shared/src/cli/commands/plugin/sync/sync.test.ts index 64eec572..c8c8b6c2 100644 --- a/packages/shared/src/cli/commands/plugin/sync/sync.test.ts +++ b/packages/shared/src/cli/commands/plugin/sync/sync.test.ts @@ -5,6 +5,7 @@ import { isWithinDirectory, parseImports, parsePluginUsages, + SCAFFOLDING_DESCRIPTOR, shouldAllowJsManifestForPackage, } from "./sync"; @@ -182,4 +183,45 @@ describe("plugin sync", () => { expect(shouldAllowJsManifestForPackage("@acme/plugin")).toBe(false); }); }); + + describe("SCAFFOLDING_DESCRIPTOR", () => { + it("has the required databricks apps init command", () => { + expect(SCAFFOLDING_DESCRIPTOR.command).toBe("databricks apps init"); + }); + + it("has all required flags with correct required status", () => { + expect(SCAFFOLDING_DESCRIPTOR.flags["--name"].required).toBe(true); + expect(SCAFFOLDING_DESCRIPTOR.flags["--profile"].required).toBe(true); + expect(SCAFFOLDING_DESCRIPTOR.flags["--features"].required).toBe(false); + expect(SCAFFOLDING_DESCRIPTOR.flags["--set"].required).toBe(false); + expect(SCAFFOLDING_DESCRIPTOR.flags["--run"].required).toBe(false); + }); + + it("has a pattern for --name that enforces app name format", () => { + const pattern = SCAFFOLDING_DESCRIPTOR.flags["--name"].pattern; + expect(pattern).toBeDefined(); + const re = new RegExp(pattern!); + expect(re.test("my-app")).toBe(true); + expect(re.test("my app")).toBe(false); + expect(re.test("MyApp")).toBe(false); + expect(re.test("a".repeat(26))).toBe(true); + expect(re.test("a".repeat(27))).toBe(false); + }); + + it("has a default of none for --run", () => { + expect(SCAFFOLDING_DESCRIPTOR.flags["--run"].default).toBe("none"); + }); + + it("has rules that mention platform-injected and user-provided", () => { + const rulesText = SCAFFOLDING_DESCRIPTOR.rules.join(" "); + expect(rulesText).toContain("platform-injected"); + expect(rulesText).toContain("user-provided"); + }); + + it("has rules that clarify requiredByTemplate behaviour", () => { + const rulesText = SCAFFOLDING_DESCRIPTOR.rules.join(" "); + expect(rulesText).toContain("requiredByTemplate=true"); + expect(rulesText).toContain("--features"); + }); + }); }); diff --git a/packages/shared/src/cli/commands/plugin/sync/sync.ts b/packages/shared/src/cli/commands/plugin/sync/sync.ts index b553c45a..86ce4c53 100644 --- a/packages/shared/src/cli/commands/plugin/sync/sync.ts +++ b/packages/shared/src/cli/commands/plugin/sync/sync.ts @@ -9,6 +9,7 @@ import { } from "../manifest-resolve"; import type { PluginManifest, + ScaffoldingDescriptor, TemplatePlugin, TemplatePluginsManifest, } from "../manifest-types"; @@ -84,6 +85,8 @@ async function loadPluginEntry( ...(manifest.onSetupMessage && { onSetupMessage: manifest.onSetupMessage, }), + ...(manifest.agentHint && { agentHint: manifest.agentHint }), + ...(manifest.postScaffold && { postScaffold: manifest.postScaffold }), }, ]; } @@ -413,6 +416,8 @@ async function scanForPlugins( ...(manifest.onSetupMessage && { onSetupMessage: manifest.onSetupMessage, }), + ...(manifest.agentHint && { agentHint: manifest.agentHint }), + ...(manifest.postScaffold && { postScaffold: manifest.postScaffold }), } satisfies TemplatePlugin; } } @@ -517,6 +522,48 @@ async function scanPluginsDir( /** * Write (or preview) the template plugins manifest to disk. */ +const SCAFFOLDING_DESCRIPTOR: ScaffoldingDescriptor = { + command: "databricks apps init", + flags: { + "--name": { + required: true, + description: + "App name: lowercase letters, numbers, hyphens only. Max 26 chars.", + pattern: "^[a-z][a-z0-9-]{0,25}$", + }, + "--features": { + required: false, + description: + "Comma-separated plugin names where requiredByTemplate is false. Do NOT include requiredByTemplate=true plugins — they are included automatically.", + }, + "--set": { + required: false, + description: + "Resource field values. Format: ..=. Required for every user-provided field in resources.required of all included plugins (both mandatory and optional).", + }, + "--profile": { + required: true, + description: "Databricks CLI profile for authentication.", + }, + "--description": { + required: false, + description: "Short description of the app.", + }, + "--run": { + required: false, + default: "none", + description: + "Post-scaffold action: none (review code first), dev (start dev server), or deploy.", + }, + }, + rules: [ + "Plugins with requiredByTemplate=true are included automatically. Do NOT add them to --features. You MUST still provide --set for their required user-provided resource fields.", + "Plugins with requiredByTemplate=false are optional. Add to --features only when the user's request needs that capability.", + "Every field with resolution='user-provided' in a plugin's resources.required MUST have a --set flag for each included plugin (mandatory + optional).", + "Fields with resolution='platform-injected' are auto-set at deploy time — do NOT include them in --set.", + ], +}; + function writeManifest( outputPath: string, { plugins }: { plugins: TemplatePluginsManifest["plugins"] }, @@ -525,7 +572,8 @@ function writeManifest( const templateManifest: TemplatePluginsManifest = { $schema: "https://databricks.github.io/appkit/schemas/template-plugins.schema.json", - version: "1.0", + version: "2.0", + scaffolding: SCAFFOLDING_DESCRIPTOR, plugins, }; @@ -761,11 +809,12 @@ async function runPluginsSync(options: { writeManifest(outputPath, { plugins }, options); } -/** Exported for testing: path boundary check, AST parsing, trust checks. */ +/** Exported for testing: path boundary check, AST parsing, trust checks, scaffolding descriptor. */ export { isWithinDirectory, parseImports, parsePluginUsages, + SCAFFOLDING_DESCRIPTOR, shouldAllowJsManifestForPackage, }; diff --git a/packages/shared/src/cli/commands/plugin/validate/validate-manifest.test.ts b/packages/shared/src/cli/commands/plugin/validate/validate-manifest.test.ts index 63dd622d..cea56b2c 100644 --- a/packages/shared/src/cli/commands/plugin/validate/validate-manifest.test.ts +++ b/packages/shared/src/cli/commands/plugin/validate/validate-manifest.test.ts @@ -217,16 +217,47 @@ describe("validate-manifest", () => { }); describe("validateTemplateManifest", () => { + const minimalScaffolding = { + command: "databricks apps init", + flags: { + "--name": { required: true, description: "App name" }, + "--profile": { required: true, description: "CLI profile" }, + }, + rules: ["Example rule"], + }; + it("validates a minimal correct template manifest", () => { const result = validateTemplateManifest({ $schema: "https://databricks.github.io/appkit/schemas/template-plugins.schema.json", - version: "1.0", + version: "2.0", + scaffolding: minimalScaffolding, plugins: {}, }); expect(result.valid).toBe(true); }); + it("rejects version 1.0 manifests", () => { + const result = validateTemplateManifest({ + $schema: + "https://databricks.github.io/appkit/schemas/template-plugins.schema.json", + version: "1.0", + scaffolding: minimalScaffolding, + plugins: {}, + }); + expect(result.valid).toBe(false); + }); + + it("rejects manifest missing scaffolding", () => { + const result = validateTemplateManifest({ + $schema: + "https://databricks.github.io/appkit/schemas/template-plugins.schema.json", + version: "2.0", + plugins: {}, + }); + expect(result.valid).toBe(false); + }); + it("rejects non-object input", () => { expect(validateTemplateManifest(null).valid).toBe(false); expect(validateTemplateManifest("string").valid).toBe(false); diff --git a/packages/shared/src/schemas/plugin-manifest.generated.ts b/packages/shared/src/schemas/plugin-manifest.generated.ts index 5d2e5d4a..7bb16745 100644 --- a/packages/shared/src/schemas/plugin-manifest.generated.ts +++ b/packages/shared/src/schemas/plugin-manifest.generated.ts @@ -210,6 +210,14 @@ export interface PluginManifest { * Message displayed to the user after project initialization. Use this to inform about manual setup steps (e.g. environment variables, resource provisioning). */ onSetupMessage?: string; + /** + * Short free-text hint for agents on how to discover resource values (e.g. which CLI command to run). Complements the structured discovery fields. + */ + agentHint?: string; + /** + * Ordered steps a user or agent should follow after scaffolding. + */ + postScaffold?: PostScaffoldStep[]; /** * When true, this plugin is excluded from the template plugins manifest (appkit.plugins.json) during sync. */ @@ -250,6 +258,39 @@ export interface ResourceFieldEntry { * Named resolver prefixed by resource type (e.g., 'postgres:host'). The CLI resolves this value during the init prompt flow. */ resolve?: string; + discovery?: DiscoveryDescriptor; + /** + * Who provides the value. 'user-provided': agent/user must supply it via --set. 'platform-injected': auto-set by the platform at deploy time. + */ + resolution?: "user-provided" | "platform-injected"; +} +/** + * Describes how an agent or CLI can discover a candidate value for this field. + * + * This interface was referenced by `PluginManifest`'s JSON-Schema + * via the `definition` "discoveryDescriptor". + */ +export interface DiscoveryDescriptor { + /** + * CLI command to list candidate values. Use as placeholder for the Databricks profile. + */ + cliCommand: string; + /** + * jq-style field path to extract the value from each result item (e.g. ".id"). + */ + selectField: string; + /** + * jq-style field path for a human-readable label (e.g. ".name"). + */ + displayField?: string; + /** + * Field name in the same resource that must be resolved first. + */ + dependsOn?: string; + /** + * Optional command that returns a single value directly, bypassing the list-and-select flow. + */ + shortcut?: string; } /** * This interface was referenced by `PluginManifest`'s JSON-Schema @@ -283,3 +324,23 @@ export interface ConfigSchemaProperty { maxLength?: number; required?: string[]; } +/** + * A single ordered step for post-scaffold setup. + * + * This interface was referenced by `PluginManifest`'s JSON-Schema + * via the `definition` "postScaffoldStep". + */ +export interface PostScaffoldStep { + /** + * Step number (must be unique and sequential within a plugin). + */ + step: number; + /** + * Human or agent-readable instruction for this step. + */ + instruction: string; + /** + * When true, this step must complete before proceeding to the next. + */ + blocking?: boolean; +} diff --git a/packages/shared/src/schemas/plugin-manifest.schema.json b/packages/shared/src/schemas/plugin-manifest.schema.json index ed4ef573..228735a2 100644 --- a/packages/shared/src/schemas/plugin-manifest.schema.json +++ b/packages/shared/src/schemas/plugin-manifest.schema.json @@ -91,6 +91,17 @@ "type": "string", "description": "Message displayed to the user after project initialization. Use this to inform about manual setup steps (e.g. environment variables, resource provisioning)." }, + "agentHint": { + "type": "string", + "description": "Short free-text hint for agents on how to discover resource values (e.g. which CLI command to run). Complements the structured discovery fields." + }, + "postScaffold": { + "type": "array", + "description": "Ordered steps a user or agent should follow after scaffolding.", + "items": { + "$ref": "#/$defs/postScaffoldStep" + } + }, "hidden": { "type": "boolean", "default": false, @@ -220,6 +231,71 @@ "type": "string", "pattern": "^[a-z_]+:[a-zA-Z]+$", "description": "Named resolver prefixed by resource type (e.g., 'postgres:host'). The CLI resolves this value during the init prompt flow." + }, + "discovery": { + "$ref": "#/$defs/discoveryDescriptor" + }, + "resolution": { + "type": "string", + "enum": ["user-provided", "platform-injected"], + "description": "Who provides the value. 'user-provided': agent/user must supply it via --set. 'platform-injected': auto-set by the platform at deploy time." + } + }, + "additionalProperties": false + }, + "discoveryDescriptor": { + "type": "object", + "required": ["cliCommand", "selectField"], + "description": "Describes how an agent or CLI can discover a candidate value for this field.", + "properties": { + "cliCommand": { + "type": "string", + "description": "CLI command to list candidate values. Use as placeholder for the Databricks profile.", + "examples": ["databricks warehouses list --profile -o json"] + }, + "selectField": { + "type": "string", + "description": "jq-style field path to extract the value from each result item (e.g. \".id\").", + "examples": [".id", ".name"] + }, + "displayField": { + "type": "string", + "description": "jq-style field path for a human-readable label (e.g. \".name\").", + "examples": [".name", ".title"] + }, + "dependsOn": { + "type": "string", + "description": "Field name in the same resource that must be resolved first." + }, + "shortcut": { + "type": "string", + "description": "Optional command that returns a single value directly, bypassing the list-and-select flow.", + "examples": [ + "databricks experimental aitools tools get-default-warehouse --profile " + ] + } + }, + "additionalProperties": false + }, + "postScaffoldStep": { + "type": "object", + "required": ["step", "instruction"], + "description": "A single ordered step for post-scaffold setup.", + "properties": { + "step": { + "type": "integer", + "minimum": 1, + "description": "Step number (must be unique and sequential within a plugin)." + }, + "instruction": { + "type": "string", + "minLength": 1, + "description": "Human or agent-readable instruction for this step." + }, + "blocking": { + "type": "boolean", + "default": false, + "description": "When true, this step must complete before proceeding to the next." } }, "additionalProperties": false diff --git a/packages/shared/src/schemas/template-plugins.schema.json b/packages/shared/src/schemas/template-plugins.schema.json index 290edd05..1e3c01fa 100644 --- a/packages/shared/src/schemas/template-plugins.schema.json +++ b/packages/shared/src/schemas/template-plugins.schema.json @@ -4,7 +4,7 @@ "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"], + "required": ["version", "scaffolding", "plugins"], "properties": { "$schema": { "type": "string", @@ -12,9 +12,12 @@ }, "version": { "type": "string", - "const": "1.0", + "const": "2.0", "description": "Schema version for the template plugins manifest" }, + "scaffolding": { + "$ref": "#/$defs/scaffoldingDescriptor" + }, "plugins": { "type": "object", "description": "Map of plugin name to plugin manifest with package source", @@ -69,6 +72,17 @@ "type": "string", "description": "Message displayed to the user after project initialization. Use this to inform about manual setup steps (e.g. environment variables, resource provisioning)." }, + "agentHint": { + "type": "string", + "description": "Short free-text hint for agents on how to discover resource values (e.g. which CLI command to run)." + }, + "postScaffold": { + "type": "array", + "description": "Ordered steps a user or agent should follow after scaffolding.", + "items": { + "$ref": "plugin-manifest.schema.json#/$defs/postScaffoldStep" + } + }, "resources": { "type": "object", "required": ["required", "optional"], @@ -102,6 +116,55 @@ }, "resourceRequirement": { "$ref": "plugin-manifest.schema.json#/$defs/resourceRequirement" + }, + "scaffoldingFlag": { + "type": "object", + "required": ["required", "description"], + "properties": { + "required": { + "type": "boolean", + "description": "Whether this flag must be provided." + }, + "description": { + "type": "string", + "description": "Human-readable description of the flag." + }, + "pattern": { + "type": "string", + "description": "Regex pattern the flag value must match." + }, + "default": { + "type": "string", + "description": "Default value when the flag is omitted." + } + }, + "additionalProperties": false + }, + "scaffoldingDescriptor": { + "type": "object", + "required": ["command", "flags", "rules"], + "description": "Describes how to construct the scaffolding command for this template.", + "properties": { + "command": { + "type": "string", + "description": "The base CLI command for scaffolding." + }, + "flags": { + "type": "object", + "description": "Map of flag name to flag descriptor.", + "additionalProperties": { + "$ref": "#/$defs/scaffoldingFlag" + } + }, + "rules": { + "type": "array", + "description": "Rules an agent must follow when constructing the scaffolding command.", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false } } } diff --git a/template/appkit.plugins.json b/template/appkit.plugins.json index cf60a8af..6b80eeac 100644 --- a/template/appkit.plugins.json +++ b/template/appkit.plugins.json @@ -1,6 +1,43 @@ { "$schema": "https://databricks.github.io/appkit/schemas/template-plugins.schema.json", - "version": "1.0", + "version": "2.0", + "scaffolding": { + "command": "databricks apps init", + "flags": { + "--name": { + "required": true, + "description": "App name: lowercase letters, numbers, hyphens only. Max 26 chars.", + "pattern": "^[a-z][a-z0-9-]{0,25}$" + }, + "--features": { + "required": false, + "description": "Comma-separated plugin names where requiredByTemplate is false. Do NOT include requiredByTemplate=true plugins — they are included automatically." + }, + "--set": { + "required": false, + "description": "Resource field values. Format: ..=. Required for every user-provided field in resources.required of all included plugins (both mandatory and optional)." + }, + "--profile": { + "required": true, + "description": "Databricks CLI profile for authentication." + }, + "--description": { + "required": false, + "description": "Short description of the app." + }, + "--run": { + "required": false, + "default": "none", + "description": "Post-scaffold action: none (review code first), dev (start dev server), or deploy." + } + }, + "rules": [ + "Plugins with requiredByTemplate=true are included automatically. Do NOT add them to --features. You MUST still provide --set for their required user-provided resource fields.", + "Plugins with requiredByTemplate=false are optional. Add to --features only when the user's request needs that capability.", + "Every field with resolution='user-provided' in a plugin's resources.required MUST have a --set flag for each included plugin (mandatory + optional).", + "Fields with resolution='platform-injected' are auto-set at deploy time — do NOT include them in --set." + ] + }, "plugins": { "analytics": { "name": "analytics", @@ -18,13 +55,44 @@ "fields": { "id": { "env": "DATABRICKS_WAREHOUSE_ID", - "description": "SQL Warehouse ID" + "description": "SQL Warehouse ID", + "resolution": "user-provided", + "discovery": { + "cliCommand": "databricks warehouses list --profile -o json", + "selectField": ".id", + "displayField": ".name", + "shortcut": "databricks experimental aitools tools get-default-warehouse --profile " + } } } } ], "optional": [] - } + }, + "agentHint": "Run 'databricks warehouses list' to find your SQL Warehouse ID.", + "postScaffold": [ + { + "step": 1, + "instruction": "Create SQL query files in config/queries/" + }, + { + "step": 2, + "instruction": "Run: npm run typegen", + "blocking": true + }, + { + "step": 3, + "instruction": "Read client/src/appKitTypes.d.ts for generated types" + }, + { + "step": 4, + "instruction": "Write UI code using the generated types" + }, + { + "step": 5, + "instruction": "Update tests/smoke.spec.ts selectors for your app" + } + ] }, "files": { "name": "files", @@ -42,13 +110,30 @@ "fields": { "path": { "env": "DATABRICKS_VOLUME_FILES", - "description": "Volume path for file storage (e.g. /Volumes/catalog/schema/volume_name)" + "description": "Volume path for file storage (e.g. /Volumes/catalog/schema/volume_name)", + "resolution": "user-provided", + "discovery": { + "cliCommand": "databricks volumes list . --profile -o json", + "selectField": ".full_name", + "displayField": ".name" + } } } } ], "optional": [] - } + }, + "agentHint": "Provide the full volume path, e.g. /Volumes/catalog/schema/volume_name.", + "postScaffold": [ + { + "step": 1, + "instruction": "Use the files plugin API to read/write volume files in your tRPC procedures" + }, + { + "step": 2, + "instruction": "Build UI for file upload/download using tRPC" + } + ] }, "genie": { "name": "genie", @@ -66,13 +151,20 @@ "fields": { "id": { "env": "DATABRICKS_GENIE_SPACE_ID", - "description": "Default Genie Space ID" + "description": "Default Genie Space ID", + "resolution": "user-provided", + "discovery": { + "cliCommand": "databricks genie list-spaces --profile -o json", + "selectField": ".space_id", + "displayField": ".title" + } } } } ], "optional": [] - } + }, + "agentHint": "Find your Genie Space ID in the AI/BI Genie UI." }, "lakebase": { "name": "lakebase", @@ -92,25 +184,38 @@ "description": "Full Lakebase Postgres branch resource name. Obtain by running `databricks postgres list-branches projects/{project-id}`, select the desired item from the output array and use its .name value.", "examples": [ "projects/{project-id}/branches/{branch-id}" - ] + ], + "resolution": "user-provided", + "discovery": { + "cliCommand": "databricks postgres list-branches projects/{project-id} --profile -o json", + "selectField": ".name" + } }, "database": { "description": "Full Lakebase Postgres database resource name. Obtain by running `databricks postgres list-databases {branch-name}`, select the desired item from the output array and use its .name value. Requires the branch resource name.", "examples": [ "projects/{project-id}/branches/{branch-id}/databases/{database-id}" - ] + ], + "resolution": "user-provided", + "discovery": { + "cliCommand": "databricks postgres list-databases {branch} --profile -o json", + "selectField": ".name", + "dependsOn": "branch" + } }, "host": { "env": "PGHOST", "localOnly": true, "resolve": "postgres:host", - "description": "Postgres host for local development. Auto-injected by the platform at deploy time." + "description": "Postgres host for local development. Auto-injected by the platform at deploy time.", + "resolution": "platform-injected" }, "databaseName": { "env": "PGDATABASE", "localOnly": true, "resolve": "postgres:databaseName", - "description": "Postgres database name for local development. Auto-injected by the platform at deploy time." + "description": "Postgres database name for local development. Auto-injected by the platform at deploy time.", + "resolution": "platform-injected" }, "endpointPath": { "env": "LAKEBASE_ENDPOINT", @@ -119,25 +224,52 @@ "description": "Lakebase endpoint resource name. Auto-injected at runtime via app.yaml valueFrom: postgres. For local development, obtain by running `databricks postgres list-endpoints {branch-name}`, select the desired item from the output array and use its .name value.", "examples": [ "projects/{project-id}/branches/{branch-id}/endpoints/{endpoint-id}" - ] + ], + "resolution": "user-provided", + "discovery": { + "cliCommand": "databricks postgres list-endpoints {branch} --profile -o json", + "selectField": ".name", + "dependsOn": "branch" + } }, "port": { "env": "PGPORT", "localOnly": true, "value": "5432", - "description": "Postgres port. Auto-injected by the platform at deploy time." + "description": "Postgres port. Auto-injected by the platform at deploy time.", + "resolution": "platform-injected" }, "sslmode": { "env": "PGSSLMODE", "localOnly": true, "value": "require", - "description": "Postgres SSL mode. Auto-injected by the platform at deploy time." + "description": "Postgres SSL mode. Auto-injected by the platform at deploy time.", + "resolution": "platform-injected" } } } ], "optional": [] - } + }, + "agentHint": "Run 'databricks postgres list-branches' to find your Lakebase branch.", + "postScaffold": [ + { + "step": 1, + "instruction": "Define your schema in server/server.ts startup" + }, + { + "step": 2, + "instruction": "Write tRPC procedures using pool.query() in server/server.ts" + }, + { + "step": 3, + "instruction": "Build React frontend consuming tRPC procedures" + }, + { + "step": 4, + "instruction": "Update tests/smoke.spec.ts selectors for your app" + } + ] }, "server": { "name": "server",