From 2617c0f8915ad318133e164ed5760fc0a48d74f5 Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Mon, 16 Mar 2026 18:49:18 +0100 Subject: [PATCH] chore: poc manifest 3 --- .../api/appkit/Interface.PluginManifest.md | 16 ++ .../appkit/Interface.ResourceFieldEntry.md | 76 -------- .../appkit/TypeAlias.ResourceFieldEntry.md | 5 + docs/docs/api/appkit/index.md | 2 +- docs/docs/api/appkit/typedoc-sidebar.ts | 10 +- .../schemas/plugin-manifest.schema.json | 72 +++++++ .../schemas/template-plugins.schema.json | 71 ++++++- .../src/plugins/analytics/manifest.json | 33 +++- .../appkit/src/plugins/files/manifest.json | 14 +- .../appkit/src/plugins/genie/manifest.json | 5 +- .../appkit/src/plugins/lakebase/manifest.json | 51 ++++- .../src/cli/commands/plugin/list/list.test.ts | 26 ++- .../src/cli/commands/plugin/manifest-types.ts | 32 ++- .../src/cli/commands/plugin/sync/sync.ts | 53 ++++- .../plugin/validate/validate-manifest.test.ts | 184 +++++++++++++++++- .../plugin/validate/validate-manifest.ts | 180 ++++++++++++++++- packages/shared/src/plugin.ts | 27 ++- .../src/schemas/plugin-manifest.generated.ts | 58 ++++++ .../src/schemas/plugin-manifest.schema.json | 72 +++++++ .../src/schemas/template-plugins.schema.json | 71 ++++++- template/appkit.plugins.json | 149 ++++++++++++-- 21 files changed, 1085 insertions(+), 122 deletions(-) delete mode 100644 docs/docs/api/appkit/Interface.ResourceFieldEntry.md create mode 100644 docs/docs/api/appkit/TypeAlias.ResourceFieldEntry.md diff --git a/docs/docs/api/appkit/Interface.PluginManifest.md b/docs/docs/api/appkit/Interface.PluginManifest.md index 84ff2487..9536384e 100644 --- a/docs/docs/api/appkit/Interface.PluginManifest.md +++ b/docs/docs/api/appkit/Interface.PluginManifest.md @@ -168,6 +168,22 @@ Omit.onSetupMessage *** +### postScaffold? + +```ts +optional postScaffold: PostScaffoldStep[]; +``` + +Ordered follow-up steps that a user or agent should perform 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 deleted file mode 100644 index 324a82f0..00000000 --- a/docs/docs/api/appkit/Interface.ResourceFieldEntry.md +++ /dev/null @@ -1,76 +0,0 @@ -# Interface: ResourceFieldEntry - -Defines a single field for a resource. Each field has its own environment variable and optional description. Single-value types use one key (e.g. id); multi-value types (database, secret) use multiple (e.g. instance_name, database_name or scope, key). - -This interface was referenced by `PluginManifest`'s JSON-Schema -via the `definition` "resourceFieldEntry". - -## Properties - -### bundleIgnore? - -```ts -optional bundleIgnore: boolean; -``` - -When true, this field is excluded from Databricks bundle configuration (databricks.yml) generation. - -*** - -### description? - -```ts -optional description: string; -``` - -Human-readable description for this field - -*** - -### env? - -```ts -optional env: string; -``` - -Environment variable name for this field - -*** - -### examples? - -```ts -optional examples: string[]; -``` - -Example values showing the expected format for this field - -*** - -### localOnly? - -```ts -optional localOnly: boolean; -``` - -When true, this field is only generated for local .env files. The Databricks Apps platform auto-injects it at deploy time. - -*** - -### resolve? - -```ts -optional resolve: string; -``` - -Named resolver prefixed by resource type (e.g., 'postgres:host'). The CLI resolves this value during the init prompt flow. - -*** - -### value? - -```ts -optional value: string; -``` - -Static value for this field. Used when no prompted or resolved value exists. diff --git a/docs/docs/api/appkit/TypeAlias.ResourceFieldEntry.md b/docs/docs/api/appkit/TypeAlias.ResourceFieldEntry.md new file mode 100644 index 00000000..864dd33b --- /dev/null +++ b/docs/docs/api/appkit/TypeAlias.ResourceFieldEntry.md @@ -0,0 +1,5 @@ +# Type Alias: ResourceFieldEntry + +```ts +type ResourceFieldEntry = GeneratedResourceFieldEntry; +``` diff --git a/docs/docs/api/appkit/index.md b/docs/docs/api/appkit/index.md index b5fb7ce0..f1bc961e 100644 --- a/docs/docs/api/appkit/index.md +++ b/docs/docs/api/appkit/index.md @@ -40,7 +40,6 @@ plugin architecture, and React integration. | [RequestedClaims](Interface.RequestedClaims.md) | Optional claims for fine-grained Unity Catalog table permissions When specified, the returned token will be scoped to only the requested tables | | [RequestedResource](Interface.RequestedResource.md) | Resource to request permissions for in Unity Catalog | | [ResourceEntry](Interface.ResourceEntry.md) | Internal representation of a resource in the registry. Extends ResourceRequirement with resolution state and plugin ownership. | -| [ResourceFieldEntry](Interface.ResourceFieldEntry.md) | Defines a single field for a resource. Each field has its own environment variable and optional description. Single-value types use one key (e.g. id); multi-value types (database, secret) use multiple (e.g. instance_name, database_name or scope, key). | | [ResourceRequirement](Interface.ResourceRequirement.md) | Declares a resource requirement for a plugin. Can be defined statically in a manifest or dynamically via getResourceRequirements(). Narrows the generated base: type → ResourceType enum, permission → ResourcePermission union. | | [StreamExecutionSettings](Interface.StreamExecutionSettings.md) | Execution settings for streaming endpoints. Extends PluginExecutionSettings with SSE stream configuration. | | [TelemetryConfig](Interface.TelemetryConfig.md) | OpenTelemetry configuration for AppKit applications | @@ -53,6 +52,7 @@ plugin architecture, and React integration. | [ConfigSchema](TypeAlias.ConfigSchema.md) | Configuration schema definition for plugin config. Re-exported from the standard JSON Schema Draft 7 types. | | [IAppRouter](TypeAlias.IAppRouter.md) | Express router type for plugin route registration | | [PluginData](TypeAlias.PluginData.md) | Tuple of plugin class, config, and name. Created by `toPlugin()` and passed to `createApp()`. | +| [ResourceFieldEntry](TypeAlias.ResourceFieldEntry.md) | - | | [ResourcePermission](TypeAlias.ResourcePermission.md) | Union of all possible permission levels across all resource types. | | [ToPlugin](TypeAlias.ToPlugin.md) | Factory function type returned by `toPlugin()`. Accepts optional config and returns a PluginData tuple. | diff --git a/docs/docs/api/appkit/typedoc-sidebar.ts b/docs/docs/api/appkit/typedoc-sidebar.ts index 2f17b1d2..cadc1a84 100644 --- a/docs/docs/api/appkit/typedoc-sidebar.ts +++ b/docs/docs/api/appkit/typedoc-sidebar.ts @@ -132,11 +132,6 @@ const typedocSidebar: SidebarsConfig = { id: "api/appkit/Interface.ResourceEntry", label: "ResourceEntry" }, - { - type: "doc", - id: "api/appkit/Interface.ResourceFieldEntry", - label: "ResourceFieldEntry" - }, { type: "doc", id: "api/appkit/Interface.ResourceRequirement", @@ -178,6 +173,11 @@ const typedocSidebar: SidebarsConfig = { id: "api/appkit/TypeAlias.PluginData", label: "PluginData" }, + { + type: "doc", + id: "api/appkit/TypeAlias.ResourceFieldEntry", + label: "ResourceFieldEntry" + }, { type: "doc", id: "api/appkit/TypeAlias.ResourcePermission", diff --git a/docs/static/schemas/plugin-manifest.schema.json b/docs/static/schemas/plugin-manifest.schema.json index ed4ef573..3b7fbd20 100644 --- a/docs/static/schemas/plugin-manifest.schema.json +++ b/docs/static/schemas/plugin-manifest.schema.json @@ -91,6 +91,13 @@ "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)." }, + "postScaffold": { + "type": "array", + "description": "Ordered follow-up steps that a user or agent should perform after scaffolding.", + "items": { + "$ref": "#/$defs/postScaffoldStep" + } + }, "hidden": { "type": "boolean", "default": false, @@ -197,6 +204,12 @@ "type": "string", "description": "Human-readable description for this field" }, + "discovery": { + "$ref": "#/$defs/discoveryDescriptor" + }, + "resolution": { + "$ref": "#/$defs/resourceResolution" + }, "bundleIgnore": { "type": "boolean", "default": false, @@ -224,6 +237,65 @@ }, "additionalProperties": false }, + "discoveryDescriptor": { + "type": "object", + "required": ["cliCommand", "selectField"], + "properties": { + "cliCommand": { + "type": "string", + "minLength": 1, + "description": "CLI command to list candidate values. Use as a placeholder for the Databricks CLI profile." + }, + "selectField": { + "type": "string", + "minLength": 1, + "description": "jq-style field used to extract the machine-readable value from each command result." + }, + "displayField": { + "type": "string", + "minLength": 1, + "description": "jq-style field used to extract a human-readable display value from each command result." + }, + "dependsOn": { + "type": "string", + "minLength": 1, + "description": "Field name in the same resource that must be resolved before running this discovery command." + }, + "shortcut": { + "type": "string", + "minLength": 1, + "description": "Optional shortcut command that returns a single value directly." + } + }, + "additionalProperties": false + }, + "resourceResolution": { + "type": "string", + "enum": ["user-provided", "platform-injected"], + "description": "Indicates whether the value must be supplied by the user/agent or is injected automatically by the platform." + }, + "postScaffoldStep": { + "type": "object", + "required": ["step", "instruction"], + "properties": { + "step": { + "type": "integer", + "minimum": 1, + "description": "1-based step number in the post-scaffold flow." + }, + "instruction": { + "type": "string", + "minLength": 1, + "description": "Instruction to follow after scaffolding." + }, + "blocking": { + "type": "boolean", + "default": false, + "description": "When true, this step must be completed before proceeding." + } + }, + "additionalProperties": false + }, "resourceRequirement": { "type": "object", "description": "Declares a resource requirement for a plugin. Can be defined statically in a manifest or dynamically via getResourceRequirements().", diff --git a/docs/static/schemas/template-plugins.schema.json b/docs/static/schemas/template-plugins.schema.json index 290edd05..e2e8f946 100644 --- a/docs/static/schemas/template-plugins.schema.json +++ b/docs/static/schemas/template-plugins.schema.json @@ -12,9 +12,12 @@ }, "version": { "type": "string", - "const": "1.0", + "enum": ["1.0", "2.0"], "description": "Schema version for the template plugins manifest" }, + "scaffolding": { + "$ref": "#/$defs/scaffolding" + }, "plugins": { "type": "object", "description": "Map of plugin name to plugin manifest with package source", @@ -23,6 +26,19 @@ } } }, + "allOf": [ + { + "if": { + "properties": { + "version": { "const": "2.0" } + }, + "required": ["version"] + }, + "then": { + "required": ["scaffolding"] + } + } + ], "additionalProperties": false, "$defs": { "templatePlugin": { @@ -69,6 +85,13 @@ "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)." }, + "postScaffold": { + "type": "array", + "description": "Ordered follow-up steps that a user or agent should perform after scaffolding.", + "items": { + "$ref": "plugin-manifest.schema.json#/$defs/postScaffoldStep" + } + }, "resources": { "type": "object", "required": ["required", "optional"], @@ -94,6 +117,52 @@ }, "additionalProperties": false }, + "scaffoldingFlag": { + "type": "object", + "required": ["required", "description"], + "properties": { + "required": { + "type": "boolean" + }, + "description": { + "type": "string", + "minLength": 1 + }, + "pattern": { + "type": "string", + "minLength": 1 + }, + "default": { + "type": "string" + } + }, + "additionalProperties": false + }, + "scaffolding": { + "type": "object", + "required": ["command", "flags", "rules"], + "properties": { + "command": { + "type": "string", + "minLength": 1 + }, + "flags": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "$ref": "#/$defs/scaffoldingFlag" + } + }, + "rules": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "additionalProperties": false + }, "resourceType": { "$ref": "plugin-manifest.schema.json#/$defs/resourceType" }, diff --git a/packages/appkit/src/plugins/analytics/manifest.json b/packages/appkit/src/plugins/analytics/manifest.json index 4a6a60c2..6fe13d2c 100644 --- a/packages/appkit/src/plugins/analytics/manifest.json +++ b/packages/appkit/src/plugins/analytics/manifest.json @@ -3,6 +3,30 @@ "name": "analytics", "displayName": "Analytics Plugin", "description": "SQL query execution against Databricks SQL Warehouses", + "onSetupMessage": "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" + } + ], "resources": { "required": [ { @@ -14,7 +38,14 @@ "fields": { "id": { "env": "DATABRICKS_WAREHOUSE_ID", - "description": "SQL Warehouse ID" + "description": "SQL Warehouse ID", + "discovery": { + "cliCommand": "databricks warehouses list --profile -o json", + "selectField": ".id", + "displayField": ".name", + "shortcut": "databricks experimental aitools tools get-default-warehouse --profile " + }, + "resolution": "user-provided" } } } diff --git a/packages/appkit/src/plugins/files/manifest.json b/packages/appkit/src/plugins/files/manifest.json index c886deca..58384275 100644 --- a/packages/appkit/src/plugins/files/manifest.json +++ b/packages/appkit/src/plugins/files/manifest.json @@ -3,6 +3,17 @@ "name": "files", "displayName": "Files Plugin", "description": "File operations against Databricks Volumes and Unity Catalog", + "onSetupMessage": "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" + } + ], "resources": { "required": [ { @@ -14,7 +25,8 @@ "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" } } } diff --git a/packages/appkit/src/plugins/genie/manifest.json b/packages/appkit/src/plugins/genie/manifest.json index a269795d..eec33d6e 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", + "onSetupMessage": "Find your Genie Space ID in the AI/BI Genie UI.", "resources": { "required": [ { @@ -13,7 +15,8 @@ "fields": { "id": { "env": "DATABRICKS_GENIE_SPACE_ID", - "description": "Default Genie Space ID" + "description": "Default Genie Space ID", + "resolution": "user-provided" } } } diff --git a/packages/appkit/src/plugins/lakebase/manifest.json b/packages/appkit/src/plugins/lakebase/manifest.json index 2959c092..7538e152 100644 --- a/packages/appkit/src/plugins/lakebase/manifest.json +++ b/packages/appkit/src/plugins/lakebase/manifest.json @@ -3,6 +3,25 @@ "name": "lakebase", "displayName": "Lakebase", "description": "SQL query execution against Databricks Lakebase Autoscaling", + "onSetupMessage": "Run 'databricks postgres list-branches projects/{project-id}' 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" + } + ], "hidden": false, "resources": { "required": [ @@ -15,25 +34,33 @@ "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}"], + "discovery": { + "cliCommand": "databricks postgres list-branches projects/{project-id} --profile -o json", + "selectField": ".name" + }, + "resolution": "user-provided" }, "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.", + "description": "Full Lakebase Postgres database resource name for the selected branch.", "examples": [ "projects/{project-id}/branches/{branch-id}/databases/{database-id}" - ] + ], + "resolution": "user-provided" }, "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,19 +69,27 @@ "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}" - ] + ], + "discovery": { + "cliCommand": "databricks postgres list-endpoints {branch} --profile -o json", + "selectField": ".name", + "dependsOn": "branch" + }, + "resolution": "user-provided" }, "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" } } } 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..264470d9 100644 --- a/packages/shared/src/cli/commands/plugin/list/list.test.ts +++ b/packages/shared/src/cli/commands/plugin/list/list.test.ts @@ -23,17 +23,29 @@ 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: ["Required plugins are auto-included."], + }, plugins: { server: { name: "server", displayName: "Server Plugin", + description: "Server runtime", package: "@databricks/appkit", resources: { required: [], optional: [] }, }, analytics: { name: "analytics", displayName: "Analytics Plugin", + description: "Warehouse-backed analytics", package: "@databricks/appkit", resources: { required: [{ type: "sql_warehouse" }], @@ -94,7 +106,17 @@ 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: ["Required plugins are auto-included."], + }, 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..c603670e 100644 --- a/packages/shared/src/cli/commands/plugin/manifest-types.ts +++ b/packages/shared/src/cli/commands/plugin/manifest-types.ts @@ -11,9 +11,36 @@ export type { ResourceRequirement, } from "../../../schemas/plugin-manifest.generated"; -import type { PluginManifest } from "../../../schemas/plugin-manifest.generated"; +import type { + PluginManifest as GeneratedPluginManifest, + ResourceFieldEntry as GeneratedResourceFieldEntry, +} from "../../../schemas/plugin-manifest.generated"; + +export type DiscoveryDescriptor = NonNullable< + GeneratedResourceFieldEntry["discovery"] +>; +export type ResourceResolution = NonNullable< + GeneratedResourceFieldEntry["resolution"] +>; +export type PostScaffoldStep = NonNullable< + GeneratedPluginManifest["postScaffold"] +>[number]; + +export interface ScaffoldingFlagDescriptor { + required: boolean; + description: string; + pattern?: string; + default?: string; +} + +export interface ScaffoldingDescriptor { + command: string; + flags: Record; + rules: string[]; +} -export interface TemplatePlugin extends Omit { +export interface TemplatePlugin + extends Omit { package: string; /** When true, this plugin is required by the template and cannot be deselected during CLI init. */ requiredByTemplate?: boolean; @@ -22,5 +49,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.ts b/packages/shared/src/cli/commands/plugin/sync/sync.ts index b553c45a..6298fa6e 100644 --- a/packages/shared/src/cli/commands/plugin/sync/sync.ts +++ b/packages/shared/src/cli/commands/plugin/sync/sync.ts @@ -57,6 +57,50 @@ function validateManifestWithSchema( /** Safety limit for recursive directory scanning to prevent runaway traversal. */ const MAX_SCAN_DEPTH = 5; +const TEMPLATE_SCAFFOLDING: NonNullable< + TemplatePluginsManifest["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 user-provided required resource fields.", + "Plugins with requiredByTemplate=false are optional. Add to --features only when the user's request needs that capability.", + "Every user-provided field 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.", + ], +}; + /** * Load and validate a resolved manifest, returning a TemplatePlugin entry or null. * Centralises the resolve → load → validate → build-entry pipeline used by @@ -84,6 +128,9 @@ async function loadPluginEntry( ...(manifest.onSetupMessage && { onSetupMessage: manifest.onSetupMessage, }), + ...(manifest.postScaffold && { + postScaffold: manifest.postScaffold, + }), }, ]; } @@ -413,6 +460,9 @@ async function scanForPlugins( ...(manifest.onSetupMessage && { onSetupMessage: manifest.onSetupMessage, }), + ...(manifest.postScaffold && { + postScaffold: manifest.postScaffold, + }), } satisfies TemplatePlugin; } } @@ -525,7 +575,8 @@ function writeManifest( const templateManifest: TemplatePluginsManifest = { $schema: "https://databricks.github.io/appkit/schemas/template-plugins.schema.json", - version: "1.0", + version: "2.0", + scaffolding: TEMPLATE_SCAFFOLDING, plugins, }; 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..ad95cecc 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 @@ -41,6 +41,22 @@ const VALID_MANIFEST_WITH_RESOURCE = { }, }; +const VALID_SCAFFOLDING = { + command: "databricks apps init", + flags: { + "--name": { + required: true, + description: "App name", + pattern: "^[a-z][a-z0-9-]{0,25}$", + }, + "--profile": { + required: true, + description: "CLI profile", + }, + }, + rules: ["Include required plugins automatically."], +}; + describe("validate-manifest", () => { describe("detectSchemaType", () => { it('returns "plugin-manifest" for plugin manifest $schema', () => { @@ -214,10 +230,122 @@ describe("validate-manifest", () => { }); expect(result.valid).toBe(false); }); + + it("rejects discovery dependsOn references to missing fields", () => { + const result = validateManifest({ + ...VALID_MANIFEST, + resources: { + required: [ + { + type: "sql_warehouse", + alias: "SQL Warehouse", + resourceKey: "sql-warehouse", + description: "Required for queries", + permission: "CAN_USE", + fields: { + id: { + env: "DATABRICKS_WAREHOUSE_ID", + discovery: { + cliCommand: + "databricks warehouses list --profile -o json", + selectField: ".id", + dependsOn: "missing", + }, + }, + }, + }, + ], + optional: [], + }, + }); + expect(result.valid).toBe(false); + expect(formatValidationErrors(result.errors ?? [])).toContain( + "dependsOn must reference another field in the same resource", + ); + }); + + it("rejects cyclic discovery dependsOn chains", () => { + const result = validateManifest({ + ...VALID_MANIFEST, + resources: { + required: [ + { + type: "postgres", + alias: "Postgres", + resourceKey: "postgres", + description: "Persistent storage", + permission: "CAN_CONNECT_AND_CREATE", + fields: { + branch: { + discovery: { + cliCommand: + "databricks postgres list-branches projects/{project-id} --profile -o json", + selectField: ".name", + dependsOn: "endpointPath", + }, + }, + endpointPath: { + discovery: { + cliCommand: + "databricks postgres list-endpoints {branch} --profile -o json", + selectField: ".name", + dependsOn: "branch", + }, + }, + }, + }, + ], + optional: [], + }, + }); + expect(result.valid).toBe(false); + expect(formatValidationErrors(result.errors ?? [])).toContain( + "dependsOn chains must be acyclic", + ); + }); + + it("rejects postScaffold steps that are not strictly increasing", () => { + const result = validateManifest({ + ...VALID_MANIFEST, + postScaffold: [ + { step: 2, instruction: "Second" }, + { step: 1, instruction: "First" }, + ], + }); + expect(result.valid).toBe(false); + expect(formatValidationErrors(result.errors ?? [])).toContain( + "postScaffold step numbers must be strictly increasing", + ); + }); + + it("rejects duplicate postScaffold step numbers", () => { + const result = validateManifest({ + ...VALID_MANIFEST, + postScaffold: [ + { step: 1, instruction: "First" }, + { step: 1, instruction: "Duplicate" }, + ], + }); + expect(result.valid).toBe(false); + expect(formatValidationErrors(result.errors ?? [])).toContain( + "postScaffold step numbers must be unique", + ); + }); }); describe("validateTemplateManifest", () => { - it("validates a minimal correct template manifest", () => { + it("validates a version 2.0 template manifest with scaffolding", () => { + const result = validateTemplateManifest({ + $schema: + "https://databricks.github.io/appkit/schemas/template-plugins.schema.json", + version: "2.0", + scaffolding: VALID_SCAFFOLDING, + plugins: {}, + }); + expect(result.valid).toBe(true); + }); + + it("continues to validate a legacy version 1.0 template manifest", () => { const result = validateTemplateManifest({ $schema: "https://databricks.github.io/appkit/schemas/template-plugins.schema.json", @@ -227,6 +355,60 @@ describe("validate-manifest", () => { expect(result.valid).toBe(true); }); + it("rejects version 2.0 template manifests without 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("applies semantic validation to plugins inside template manifests", () => { + const result = validateTemplateManifest({ + $schema: + "https://databricks.github.io/appkit/schemas/template-plugins.schema.json", + version: "2.0", + scaffolding: VALID_SCAFFOLDING, + plugins: { + analytics: { + name: "analytics", + displayName: "Analytics Plugin", + description: "Warehouse-backed analytics", + package: "@databricks/appkit", + resources: { + required: [ + { + type: "sql_warehouse", + alias: "SQL Warehouse", + resourceKey: "sql-warehouse", + description: "Required for queries", + permission: "CAN_USE", + fields: { + id: { + env: "DATABRICKS_WAREHOUSE_ID", + discovery: { + cliCommand: + "databricks warehouses list --profile -o json", + selectField: ".id", + dependsOn: "missing", + }, + }, + }, + }, + ], + optional: [], + }, + }, + }, + }); + expect(result.valid).toBe(false); + expect(formatValidationErrors(result.errors ?? [])).toContain( + "dependsOn must reference another field in the same resource", + ); + }); + it("rejects non-object input", () => { expect(validateTemplateManifest(null).valid).toBe(false); expect(validateTemplateManifest("string").valid).toBe(false); diff --git a/packages/shared/src/cli/commands/plugin/validate/validate-manifest.ts b/packages/shared/src/cli/commands/plugin/validate/validate-manifest.ts index b0284b76..7e5f426f 100644 --- a/packages/shared/src/cli/commands/plugin/validate/validate-manifest.ts +++ b/packages/shared/src/cli/commands/plugin/validate/validate-manifest.ts @@ -3,7 +3,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import Ajv, { type ErrorObject } from "ajv"; import addFormats from "ajv-formats"; -import type { PluginManifest } from "../manifest-types"; +import type { PluginManifest, TemplatePlugin } from "../manifest-types"; export type { PluginManifest }; @@ -78,6 +78,157 @@ function getPluginValidator(): ReturnType | null { let compiledTemplateValidator: ReturnType | null = null; +interface PluginLike { + name: string; + resources: PluginManifest["resources"]; + postScaffold?: PluginManifest["postScaffold"]; +} + +function createSemanticError( + instancePath: string, + message: string, +): ErrorObject { + return { + keyword: "custom", + instancePath, + schemaPath: "#/semantic", + params: {}, + message, + } as ErrorObject; +} + +function collectDiscoveryErrors( + fields: Record, + fieldBasePath: string, +): ErrorObject[] { + const errors: ErrorObject[] = []; + const fieldNames = new Set(Object.keys(fields)); + + for (const [fieldName, field] of Object.entries(fields)) { + const dependsOn = field.discovery?.dependsOn; + if (dependsOn && !fieldNames.has(dependsOn)) { + errors.push( + createSemanticError( + `${fieldBasePath}/${fieldName}/discovery/dependsOn`, + `dependsOn must reference another field in the same resource (got "${dependsOn}")`, + ), + ); + } + } + + const visiting = new Set(); + const visited = new Set(); + const stack: string[] = []; + const cycleFields = new Set(); + + const visit = (fieldName: string) => { + if (visited.has(fieldName)) return; + if (visiting.has(fieldName)) { + const cycleStart = stack.indexOf(fieldName); + for (const name of stack.slice(cycleStart)) { + cycleFields.add(name); + } + cycleFields.add(fieldName); + return; + } + + visiting.add(fieldName); + stack.push(fieldName); + + const dependsOn = fields[fieldName]?.discovery?.dependsOn; + if (dependsOn && fieldNames.has(dependsOn)) { + visit(dependsOn); + } + + stack.pop(); + visiting.delete(fieldName); + visited.add(fieldName); + }; + + for (const fieldName of fieldNames) { + if (fields[fieldName]?.discovery?.dependsOn) { + visit(fieldName); + } + } + + for (const fieldName of cycleFields) { + errors.push( + createSemanticError( + `${fieldBasePath}/${fieldName}/discovery/dependsOn`, + "dependsOn chains must be acyclic", + ), + ); + } + + return errors; +} + +function collectPostScaffoldErrors( + steps: PluginLike["postScaffold"], + basePath: string, +): ErrorObject[] { + if (!steps) return []; + + const errors: ErrorObject[] = []; + const seen = new Set(); + let previousStep = 0; + + for (let index = 0; index < steps.length; index += 1) { + const step = steps[index]; + const stepPath = `${basePath}/postScaffold/${index}/step`; + + if (seen.has(step.step)) { + errors.push( + createSemanticError( + stepPath, + "postScaffold step numbers must be unique", + ), + ); + } + + if (step.step <= previousStep) { + errors.push( + createSemanticError( + stepPath, + "postScaffold step numbers must be strictly increasing", + ), + ); + } + + seen.add(step.step); + previousStep = step.step; + } + + return errors; +} + +function collectPluginSemanticErrors( + plugin: PluginLike, + basePath: string, +): ErrorObject[] { + const errors: ErrorObject[] = []; + + const buckets = [ + ["required", plugin.resources.required], + ["optional", plugin.resources.optional], + ] as const; + + for (const [bucketName, resources] of buckets) { + resources.forEach((resource, resourceIndex) => { + errors.push( + ...collectDiscoveryErrors( + resource.fields ?? {}, + `${basePath}/resources/${bucketName}/${resourceIndex}/fields`, + ), + ); + }); + } + + errors.push(...collectPostScaffoldErrors(plugin.postScaffold, basePath)); + + return errors; +} + function getTemplateValidator(): ReturnType | null { if (compiledTemplateValidator) return compiledTemplateValidator; const pluginSchema = loadSchema(PLUGIN_MANIFEST_SCHEMA_PATH); @@ -137,7 +288,16 @@ export function validateManifest(obj: unknown): ValidateResult { } const valid = validate(obj); - if (valid) return { valid: true, manifest: obj as PluginManifest }; + if (valid) { + const semanticErrors = collectPluginSemanticErrors( + obj as PluginManifest, + "", + ); + if (semanticErrors.length > 0) { + return { valid: false, errors: semanticErrors }; + } + return { valid: true, manifest: obj as PluginManifest }; + } return { valid: false, errors: validate.errors ?? [] }; } @@ -178,7 +338,19 @@ export function validateTemplateManifest(obj: unknown): ValidateResult { } const valid = validate(obj); - if (valid) return { valid: true }; + if (valid) { + const templateManifest = obj as { + plugins: Record; + }; + const semanticErrors = Object.entries(templateManifest.plugins).flatMap( + ([pluginName, plugin]) => + collectPluginSemanticErrors(plugin, `/plugins/${pluginName}`), + ); + if (semanticErrors.length > 0) { + return { valid: false, errors: semanticErrors }; + } + return { valid: true }; + } return { valid: false, errors: validate.errors ?? [] }; } @@ -293,6 +465,8 @@ export function formatValidationErrors( lines.push(` ${readable}: expected type "${e.params?.type}"`); } else if (e.keyword === "minLength") { lines.push(` ${readable}: must not be empty`); + } else if (e.keyword === "custom") { + lines.push(` ${readable}: ${e.message}`); } else { lines.push( ` ${readable}: ${e.message}${e.params ? ` (${JSON.stringify(e.params)})` : ""}`, diff --git a/packages/shared/src/plugin.ts b/packages/shared/src/plugin.ts index 761bdce6..28743f5d 100644 --- a/packages/shared/src/plugin.ts +++ b/packages/shared/src/plugin.ts @@ -3,11 +3,11 @@ import type { JSONSchema7 } from "json-schema"; import type { PluginManifest as GeneratedPluginManifest, ResourceRequirement as GeneratedResourceRequirement, - ResourceFieldEntry, + ResourceFieldEntry as GeneratedResourceFieldEntry, } from "./schemas/plugin-manifest.generated"; // Re-export generated types as the shared canonical definitions. -export type { ResourceFieldEntry }; +export type ResourceFieldEntry = GeneratedResourceFieldEntry; /** Base plugin interface. */ export interface BasePlugin { @@ -103,6 +103,29 @@ export interface PluginManifest }; } +export type DiscoveryDescriptor = NonNullable< + GeneratedResourceFieldEntry["discovery"] +>; +export type ResourceResolution = NonNullable< + GeneratedResourceFieldEntry["resolution"] +>; +export type PostScaffoldStep = NonNullable< + GeneratedPluginManifest["postScaffold"] +>[number]; + +export interface ScaffoldingFlagDescriptor { + required: boolean; + description: string; + pattern?: string; + default?: string; +} + +export interface ScaffoldingDescriptor { + command: string; + flags: Record; + rules: string[]; +} + /** * Resource requirement with runtime fields added beyond the schema definition. * - `fields` is made required (schema has it optional, but registry always populates it) diff --git a/packages/shared/src/schemas/plugin-manifest.generated.ts b/packages/shared/src/schemas/plugin-manifest.generated.ts index 5d2e5d4a..da918b4c 100644 --- a/packages/shared/src/schemas/plugin-manifest.generated.ts +++ b/packages/shared/src/schemas/plugin-manifest.generated.ts @@ -210,6 +210,10 @@ 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; + /** + * Ordered follow-up steps that a user or agent should perform after scaffolding. + */ + postScaffold?: PostScaffoldStep[]; /** * When true, this plugin is excluded from the template plugins manifest (appkit.plugins.json) during sync. */ @@ -230,6 +234,14 @@ export interface ResourceFieldEntry { * Human-readable description for this field */ description?: string; + /** + * CLI discovery metadata for resolving this field non-interactively. + */ + discovery?: DiscoveryDescriptor; + /** + * Indicates whether the value must be supplied by the user/agent or is injected automatically by the platform. + */ + resolution?: ResourceResolution; /** * When true, this field is excluded from Databricks bundle configuration (databricks.yml) generation. */ @@ -251,6 +263,52 @@ export interface ResourceFieldEntry { */ resolve?: string; } +/** + * CLI discovery metadata for resolving a field non-interactively. + */ +export interface DiscoveryDescriptor { + /** + * CLI command to list candidate values. Use as a placeholder for the Databricks CLI profile. + */ + cliCommand: string; + /** + * jq-style field used to extract the machine-readable value from each command result. + */ + selectField: string; + /** + * jq-style field used to extract a human-readable display value from each command result. + */ + displayField?: string; + /** + * Field name in the same resource that must be resolved before running this discovery command. + */ + dependsOn?: string; + /** + * Optional shortcut command that returns a single value directly. + */ + shortcut?: string; +} +/** + * Indicates whether the value must be supplied by the user/agent or is injected automatically by the platform. + */ +export type ResourceResolution = "user-provided" | "platform-injected"; +/** + * Ordered post-scaffold step metadata. + */ +export interface PostScaffoldStep { + /** + * 1-based step number in the post-scaffold flow. + */ + step: number; + /** + * Instruction to follow after scaffolding. + */ + instruction: string; + /** + * When true, this step must be completed before proceeding. + */ + blocking?: boolean; +} /** * This interface was referenced by `PluginManifest`'s JSON-Schema * via the `definition` "configSchema". diff --git a/packages/shared/src/schemas/plugin-manifest.schema.json b/packages/shared/src/schemas/plugin-manifest.schema.json index ed4ef573..3b7fbd20 100644 --- a/packages/shared/src/schemas/plugin-manifest.schema.json +++ b/packages/shared/src/schemas/plugin-manifest.schema.json @@ -91,6 +91,13 @@ "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)." }, + "postScaffold": { + "type": "array", + "description": "Ordered follow-up steps that a user or agent should perform after scaffolding.", + "items": { + "$ref": "#/$defs/postScaffoldStep" + } + }, "hidden": { "type": "boolean", "default": false, @@ -197,6 +204,12 @@ "type": "string", "description": "Human-readable description for this field" }, + "discovery": { + "$ref": "#/$defs/discoveryDescriptor" + }, + "resolution": { + "$ref": "#/$defs/resourceResolution" + }, "bundleIgnore": { "type": "boolean", "default": false, @@ -224,6 +237,65 @@ }, "additionalProperties": false }, + "discoveryDescriptor": { + "type": "object", + "required": ["cliCommand", "selectField"], + "properties": { + "cliCommand": { + "type": "string", + "minLength": 1, + "description": "CLI command to list candidate values. Use as a placeholder for the Databricks CLI profile." + }, + "selectField": { + "type": "string", + "minLength": 1, + "description": "jq-style field used to extract the machine-readable value from each command result." + }, + "displayField": { + "type": "string", + "minLength": 1, + "description": "jq-style field used to extract a human-readable display value from each command result." + }, + "dependsOn": { + "type": "string", + "minLength": 1, + "description": "Field name in the same resource that must be resolved before running this discovery command." + }, + "shortcut": { + "type": "string", + "minLength": 1, + "description": "Optional shortcut command that returns a single value directly." + } + }, + "additionalProperties": false + }, + "resourceResolution": { + "type": "string", + "enum": ["user-provided", "platform-injected"], + "description": "Indicates whether the value must be supplied by the user/agent or is injected automatically by the platform." + }, + "postScaffoldStep": { + "type": "object", + "required": ["step", "instruction"], + "properties": { + "step": { + "type": "integer", + "minimum": 1, + "description": "1-based step number in the post-scaffold flow." + }, + "instruction": { + "type": "string", + "minLength": 1, + "description": "Instruction to follow after scaffolding." + }, + "blocking": { + "type": "boolean", + "default": false, + "description": "When true, this step must be completed before proceeding." + } + }, + "additionalProperties": false + }, "resourceRequirement": { "type": "object", "description": "Declares a resource requirement for a plugin. Can be defined statically in a manifest or dynamically via getResourceRequirements().", diff --git a/packages/shared/src/schemas/template-plugins.schema.json b/packages/shared/src/schemas/template-plugins.schema.json index 290edd05..e2e8f946 100644 --- a/packages/shared/src/schemas/template-plugins.schema.json +++ b/packages/shared/src/schemas/template-plugins.schema.json @@ -12,9 +12,12 @@ }, "version": { "type": "string", - "const": "1.0", + "enum": ["1.0", "2.0"], "description": "Schema version for the template plugins manifest" }, + "scaffolding": { + "$ref": "#/$defs/scaffolding" + }, "plugins": { "type": "object", "description": "Map of plugin name to plugin manifest with package source", @@ -23,6 +26,19 @@ } } }, + "allOf": [ + { + "if": { + "properties": { + "version": { "const": "2.0" } + }, + "required": ["version"] + }, + "then": { + "required": ["scaffolding"] + } + } + ], "additionalProperties": false, "$defs": { "templatePlugin": { @@ -69,6 +85,13 @@ "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)." }, + "postScaffold": { + "type": "array", + "description": "Ordered follow-up steps that a user or agent should perform after scaffolding.", + "items": { + "$ref": "plugin-manifest.schema.json#/$defs/postScaffoldStep" + } + }, "resources": { "type": "object", "required": ["required", "optional"], @@ -94,6 +117,52 @@ }, "additionalProperties": false }, + "scaffoldingFlag": { + "type": "object", + "required": ["required", "description"], + "properties": { + "required": { + "type": "boolean" + }, + "description": { + "type": "string", + "minLength": 1 + }, + "pattern": { + "type": "string", + "minLength": 1 + }, + "default": { + "type": "string" + } + }, + "additionalProperties": false + }, + "scaffolding": { + "type": "object", + "required": ["command", "flags", "rules"], + "properties": { + "command": { + "type": "string", + "minLength": 1 + }, + "flags": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "$ref": "#/$defs/scaffoldingFlag" + } + }, + "rules": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "additionalProperties": false + }, "resourceType": { "$ref": "plugin-manifest.schema.json#/$defs/resourceType" }, diff --git a/template/appkit.plugins.json b/template/appkit.plugins.json index cf60a8af..d1ecc04d 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 user-provided required resource fields.", + "Plugins with requiredByTemplate=false are optional. Add to --features only when the user's request needs that capability.", + "Every user-provided field 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", + "discovery": { + "cliCommand": "databricks warehouses list --profile -o json", + "selectField": ".id", + "displayField": ".name", + "shortcut": "databricks experimental aitools tools get-default-warehouse --profile " + }, + "resolution": "user-provided" } } } ], "optional": [] - } + }, + "onSetupMessage": "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,25 @@ "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" } } } ], "optional": [] - } + }, + "onSetupMessage": "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 +146,15 @@ "fields": { "id": { "env": "DATABRICKS_GENIE_SPACE_ID", - "description": "Default Genie Space ID" + "description": "Default Genie Space ID", + "resolution": "user-provided" } } } ], "optional": [] - } + }, + "onSetupMessage": "Find your Genie Space ID in the AI/BI Genie UI." }, "lakebase": { "name": "lakebase", @@ -92,25 +174,33 @@ "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}" - ] + ], + "discovery": { + "cliCommand": "databricks postgres list-branches projects/{project-id} --profile -o json", + "selectField": ".name" + }, + "resolution": "user-provided" }, "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.", + "description": "Full Lakebase Postgres database resource name for the selected branch.", "examples": [ "projects/{project-id}/branches/{branch-id}/databases/{database-id}" - ] + ], + "resolution": "user-provided" }, "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 +209,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}" - ] + ], + "discovery": { + "cliCommand": "databricks postgres list-endpoints {branch} --profile -o json", + "selectField": ".name", + "dependsOn": "branch" + }, + "resolution": "user-provided" }, "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": [] - } + }, + "onSetupMessage": "Run 'databricks postgres list-branches projects/{project-id}' 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",