diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c73977b60..c19e0bf5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,6 +50,18 @@ jobs: cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Check generated types are up to date + run: | + pnpm run generate:types + if ! git diff --exit-code packages/shared/src/schemas/plugin-manifest.generated.ts packages/appkit/src/registry/types.generated.ts; then + echo "❌ Error: Generated types are out of sync with plugin-manifest.schema.json." + echo "" + echo "To fix this:" + echo " 1. Run: pnpm run generate:types" + echo " 2. Review and commit the changes" + echo "" + exit 1 + fi - name: Run Biome Check run: pnpm run check - name: Run Types Check diff --git a/docs/docs/api/appkit/Interface.PluginManifest.md b/docs/docs/api/appkit/Interface.PluginManifest.md index e4b45a8b2..84ff24870 100644 --- a/docs/docs/api/appkit/Interface.PluginManifest.md +++ b/docs/docs/api/appkit/Interface.PluginManifest.md @@ -2,6 +2,16 @@ Plugin manifest that declares metadata and resource requirements. Attached to plugin classes as a static property. +Extends the shared PluginManifest with strict resource types. + +## See + + - `packages/shared/src/schemas/plugin-manifest.generated.ts` `PluginManifest` — generated base + - SharedPluginManifest — shared re-export with JSONSchema7 config + +## Extends + +- `Omit`\<`SharedPluginManifest`, `"resources"` \| `"config"`\> ## Type Parameters @@ -17,7 +27,13 @@ Attached to plugin classes as a static property. optional author: string; ``` -Optional metadata for community plugins +Author name or organization + +#### Inherited from + +```ts +Omit.author +``` *** @@ -30,7 +46,7 @@ optional config: { ``` Configuration schema for the plugin. -Defines the shape and validation rules for plugin config. +Uses JSONSchema7 instead of the generated ConfigSchema (which is too restrictive). #### schema @@ -48,6 +64,12 @@ description: string; Brief description of what the plugin does +#### Inherited from + +```ts +Omit.description +``` + *** ### displayName @@ -56,7 +78,13 @@ Brief description of what the plugin does displayName: string; ``` -Human-readable display name for UI/CLI +Human-readable display name for UI and CLI + +#### Inherited from + +```ts +Omit.displayName +``` *** @@ -66,7 +94,13 @@ Human-readable display name for UI/CLI optional hidden: boolean; ``` -When true, excluded from the template plugins manifest during sync. +When true, this plugin is excluded from the template plugins manifest (appkit.plugins.json) during sync. + +#### Inherited from + +```ts +Omit.hidden +``` *** @@ -76,6 +110,14 @@ When true, excluded from the template plugins manifest during sync. optional keywords: string[]; ``` +Keywords for plugin discovery + +#### Inherited from + +```ts +Omit.keywords +``` + *** ### license? @@ -84,6 +126,14 @@ optional keywords: string[]; optional license: string; ``` +SPDX license identifier + +#### Inherited from + +```ts +Omit.license +``` + *** ### name @@ -94,6 +144,28 @@ name: TName; Plugin identifier — the single source of truth for the plugin's name +#### Overrides + +```ts +Omit.name +``` + +*** + +### onSetupMessage? + +```ts +optional onSetupMessage: string; +``` + +Message displayed to the user after project initialization. Use this to inform about manual setup steps (e.g. environment variables, resource provisioning). + +#### Inherited from + +```ts +Omit.onSetupMessage +``` + *** ### repository? @@ -102,6 +174,14 @@ Plugin identifier — the single source of truth for the plugin's name optional repository: string; ``` +URL to the plugin's source repository + +#### Inherited from + +```ts +Omit.repository +``` + *** ### resources @@ -113,7 +193,7 @@ resources: { }; ``` -Resource requirements declaration +Resource requirements declaration (with strict ResourceRequirement types) #### optional @@ -138,3 +218,11 @@ Resources that must be available for the plugin to function ```ts optional version: string; ``` + +Plugin version (semver format) + +#### Inherited from + +```ts +Omit.version +``` diff --git a/docs/docs/api/appkit/Interface.ResourceEntry.md b/docs/docs/api/appkit/Interface.ResourceEntry.md index c56a226ab..f9e20e921 100644 --- a/docs/docs/api/appkit/Interface.ResourceEntry.md +++ b/docs/docs/api/appkit/Interface.ResourceEntry.md @@ -15,7 +15,7 @@ Extends ResourceRequirement with resolution state and plugin ownership. alias: string; ``` -Unique alias for this resource within the plugin (e.g., 'warehouse', 'secrets'). Used for UI/display. +Human-readable label for UI/display only. Deduplication uses resourceKey, not alias. #### Inherited from @@ -43,8 +43,7 @@ Human-readable description of why this resource is needed fields: Record; ``` -Map of field name to env and optional description. -Single-value types use one key (e.g. id); multi-value (database, secret) use multiple keys. +Map of field name to env and optional description. Single-value types use one key (e.g. id); multi-value (database, secret) use multiple (e.g. instance_name, database_name or scope, key). #### Inherited from @@ -58,7 +57,7 @@ Single-value types use one key (e.g. id); multi-value (database, secret) use mul permission: ResourcePermission; ``` -Required permission level for the resource +Required permission level for the resource (narrowed to union) #### Inherited from @@ -94,8 +93,6 @@ Plugin(s) that require this resource (comma-separated if multiple) required: boolean; ``` -Whether this resource is required (true) or optional (false) - #### Inherited from [`ResourceRequirement`](Interface.ResourceRequirement.md).[`required`](Interface.ResourceRequirement.md#required) @@ -118,7 +115,7 @@ Whether the resource has been resolved (all field env vars set) resourceKey: string; ``` -Stable key for machine use (env naming, composite keys, app.yaml). Required. +Stable key for machine use: deduplication, env naming, composite keys, app.yaml. Required for registry lookup. #### Inherited from @@ -132,7 +129,7 @@ Stable key for machine use (env naming, composite keys, app.yaml). Required. type: ResourceType; ``` -Type of Databricks resource required +Type of Databricks resource required (narrowed to enum) #### Inherited from diff --git a/docs/docs/api/appkit/Interface.ResourceFieldEntry.md b/docs/docs/api/appkit/Interface.ResourceFieldEntry.md index 2874887b0..324a82f01 100644 --- a/docs/docs/api/appkit/Interface.ResourceFieldEntry.md +++ b/docs/docs/api/appkit/Interface.ResourceFieldEntry.md @@ -1,7 +1,9 @@ # 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). +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 diff --git a/docs/docs/api/appkit/Interface.ResourceRequirement.md b/docs/docs/api/appkit/Interface.ResourceRequirement.md index ed040a88b..b2112040b 100644 --- a/docs/docs/api/appkit/Interface.ResourceRequirement.md +++ b/docs/docs/api/appkit/Interface.ResourceRequirement.md @@ -2,6 +2,16 @@ 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. + +## See + + - `packages/shared/src/schemas/plugin-manifest.generated.ts` `ResourceRequirement` — generated base + - SharedResourceRequirement — shared re-export with runtime `fields` and `required` + +## Extends + +- `ResourceRequirement` ## Extended by @@ -15,7 +25,13 @@ Can be defined statically in a manifest or dynamically via getResourceRequiremen alias: string; ``` -Unique alias for this resource within the plugin (e.g., 'warehouse', 'secrets'). Used for UI/display. +Human-readable label for UI/display only. Deduplication uses resourceKey, not alias. + +#### Inherited from + +```ts +SharedResourceRequirement.alias +``` *** @@ -27,6 +43,12 @@ description: string; Human-readable description of why this resource is needed +#### Inherited from + +```ts +SharedResourceRequirement.description +``` + *** ### fields @@ -35,8 +57,13 @@ Human-readable description of why this resource is needed fields: Record; ``` -Map of field name to env and optional description. -Single-value types use one key (e.g. id); multi-value (database, secret) use multiple keys. +Map of field name to env and optional description. Single-value types use one key (e.g. id); multi-value (database, secret) use multiple (e.g. instance_name, database_name or scope, key). + +#### Inherited from + +```ts +SharedResourceRequirement.fields +``` *** @@ -46,7 +73,13 @@ Single-value types use one key (e.g. id); multi-value (database, secret) use mul permission: ResourcePermission; ``` -Required permission level for the resource +Required permission level for the resource (narrowed to union) + +#### Overrides + +```ts +SharedResourceRequirement.permission +``` *** @@ -56,7 +89,11 @@ Required permission level for the resource required: boolean; ``` -Whether this resource is required (true) or optional (false) +#### Inherited from + +```ts +SharedResourceRequirement.required +``` *** @@ -66,7 +103,13 @@ Whether this resource is required (true) or optional (false) resourceKey: string; ``` -Stable key for machine use (env naming, composite keys, app.yaml). Required. +Stable key for machine use: deduplication, env naming, composite keys, app.yaml. Required for registry lookup. + +#### Inherited from + +```ts +SharedResourceRequirement.resourceKey +``` *** @@ -76,4 +119,10 @@ Stable key for machine use (env naming, composite keys, app.yaml). Required. type: ResourceType; ``` -Type of Databricks resource required +Type of Databricks resource required (narrowed to enum) + +#### Overrides + +```ts +SharedResourceRequirement.type +``` diff --git a/docs/docs/api/appkit/index.md b/docs/docs/api/appkit/index.md index 4ad808202..20e6af2d3 100644 --- a/docs/docs/api/appkit/index.md +++ b/docs/docs/api/appkit/index.md @@ -36,12 +36,12 @@ plugin architecture, and React integration. | [GenerateDatabaseCredentialRequest](Interface.GenerateDatabaseCredentialRequest.md) | Request parameters for generating database OAuth credentials | | [ITelemetry](Interface.ITelemetry.md) | Plugin-facing interface for OpenTelemetry instrumentation. Provides a thin abstraction over OpenTelemetry APIs for plugins. | | [LakebasePoolConfig](Interface.LakebasePoolConfig.md) | Configuration for creating a Lakebase connection pool | -| [PluginManifest](Interface.PluginManifest.md) | Plugin manifest that declares metadata and resource requirements. Attached to plugin classes as a static property. | +| [PluginManifest](Interface.PluginManifest.md) | Plugin manifest that declares metadata and resource requirements. Attached to plugin classes as a static property. Extends the shared PluginManifest with strict resource types. | | [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(). | +| [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) | Configuration for streaming execution with default and user-scoped settings | | [TelemetryConfig](Interface.TelemetryConfig.md) | OpenTelemetry configuration for AppKit applications | | [ValidationResult](Interface.ValidationResult.md) | Result of validating all registered resources against the environment. | diff --git a/docs/static/appkit-ui/styles.gen.css b/docs/static/appkit-ui/styles.gen.css index 419e05008..eb9384271 100644 --- a/docs/static/appkit-ui/styles.gen.css +++ b/docs/static/appkit-ui/styles.gen.css @@ -5578,7 +5578,10 @@ } ::selection { background-color: var(--primary); - opacity: 0.2; + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklch, var(--primary) 30%, transparent); + } + color: var(--primary-foreground); } @media (prefers-reduced-motion: reduce) { *, diff --git a/docs/static/schemas/plugin-manifest.schema.json b/docs/static/schemas/plugin-manifest.schema.json index cae2e6884..ed4ef5731 100644 --- a/docs/static/schemas/plugin-manifest.schema.json +++ b/docs/static/schemas/plugin-manifest.schema.json @@ -185,6 +185,7 @@ }, "resourceFieldEntry": { "type": "object", + "description": "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).", "properties": { "env": { "type": "string", @@ -225,6 +226,7 @@ }, "resourceRequirement": { "type": "object", + "description": "Declares a resource requirement for a plugin. Can be defined statically in a manifest or dynamically via getResourceRequirements().", "required": ["type", "alias", "resourceKey", "description", "permission"], "properties": { "type": { diff --git a/package.json b/package.json index 921767673..21805071f 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "build:watch": "pnpm -r --filter=!dev-playground --filter=!docs build:watch", "check:fix": "biome check --write .", "check": "biome check .", + "generate:types": "tsx tools/generate-schema-types.ts && tsx tools/generate-registry-types.ts && biome check --write packages/shared/src/schemas/plugin-manifest.generated.ts packages/appkit/src/registry/types.generated.ts", "generate:app-templates": "tsx tools/generate-app-templates.ts", "check:licenses": "tsx tools/check-licenses.ts", "build:notice": "tsx tools/build-notice.ts > NOTICE.md", @@ -61,6 +62,7 @@ "husky": "^9.1.7", "jsdom": "^27.0.0", "knip": "^5.86.0", + "json-schema-to-typescript": "^15.0.4", "lint-staged": "^15.5.1", "publint": "^0.3.15", "release-it": "^19.1.0", diff --git a/packages/appkit/src/registry/types.ts b/packages/appkit/src/registry/types.ts index 291ccafb0..b26227df7 100644 --- a/packages/appkit/src/registry/types.ts +++ b/packages/appkit/src/registry/types.ts @@ -5,8 +5,13 @@ * which enables plugins to declare their Databricks resource requirements * in a machine-readable format. * - * Resource types and permissions are generated from plugin-manifest.schema.json - * (see types.generated.ts). Hand-written interfaces below define the registry API. + * Base interfaces (ResourceFieldEntry, ResourceRequirement, PluginManifest) + * are generated from plugin-manifest.schema.json and imported via shared. + * This module re-exports them with strict narrowing (ResourceType enum, + * ResourcePermission union) for appkit-internal use. + * + * Resource types, permissions, and hierarchy constants are generated by + * tools/generate-registry-types.ts into types.generated.ts. */ // Re-export generated registry types (enum + const must be value exports for runtime) @@ -24,59 +29,33 @@ export { type ResourcePermission, }; +// Re-export generated base type from shared (schema-derived, used directly). +export type { ResourceFieldEntry } from "shared"; + +// Import shared base types for strict extension below. +import type { + PluginManifest as SharedPluginManifest, + ResourceRequirement as SharedResourceRequirement, +} from "shared"; + // ============================================================================ -// Hand-written interfaces (not in JSON schema) +// Strict appkit wrappers — narrow generated base types for registry use // ============================================================================ -/** - * 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). - */ -export interface ResourceFieldEntry { - /** Environment variable name for this field */ - env?: string; - /** Human-readable description for this field */ - description?: string; - /** When true, this field is excluded from Databricks bundle configuration (databricks.yml) generation. */ - bundleIgnore?: boolean; - /** Example values showing the expected format for this field */ - examples?: string[]; - /** When true, this field is only generated for local .env files. The Databricks Apps platform auto-injects it at deploy time. */ - localOnly?: boolean; - /** Static value for this field. Used when no prompted or resolved value exists. */ - value?: string; - /** Named resolver prefixed by resource type (e.g., 'postgres:host'). The CLI resolves this value during the init prompt flow. */ - resolve?: string; -} - /** * 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. + * + * @see `packages/shared/src/schemas/plugin-manifest.generated.ts` `ResourceRequirement` — generated base + * @see {@link SharedResourceRequirement} — shared re-export with runtime `fields` and `required` */ -export interface ResourceRequirement { - /** Type of Databricks resource required */ +export interface ResourceRequirement extends SharedResourceRequirement { + /** Type of Databricks resource required (narrowed to enum) */ type: ResourceType; - /** Unique alias for this resource within the plugin (e.g., 'warehouse', 'secrets'). Used for UI/display. */ - alias: string; - - /** Stable key for machine use (env naming, composite keys, app.yaml). Required. */ - resourceKey: string; - - /** Human-readable description of why this resource is needed */ - description: string; - - /** Required permission level for the resource */ + /** Required permission level for the resource (narrowed to union) */ permission: ResourcePermission; - - /** - * Map of field name to env and optional description. - * Single-value types use one key (e.g. id); multi-value (database, secret) use multiple keys. - */ - fields: Record; - - /** Whether this resource is required (true) or optional (false) */ - required: boolean; } /** @@ -128,19 +107,18 @@ export type ConfigSchema = JSONSchema7; /** * Plugin manifest that declares metadata and resource requirements. * Attached to plugin classes as a static property. + * Extends the shared PluginManifest with strict resource types. + * + * @see `packages/shared/src/schemas/plugin-manifest.generated.ts` `PluginManifest` — generated base + * @see {@link SharedPluginManifest} — shared re-export with JSONSchema7 config */ -export interface PluginManifest { +export interface PluginManifest + extends Omit { /** Plugin identifier — the single source of truth for the plugin's name */ name: TName; - /** Human-readable display name for UI/CLI */ - displayName: string; - - /** Brief description of what the plugin does */ - description: string; - /** - * Resource requirements declaration + * Resource requirements declaration (with strict ResourceRequirement types) */ resources: { /** Resources that must be available for the plugin to function */ @@ -152,23 +130,9 @@ export interface PluginManifest { /** * Configuration schema for the plugin. - * Defines the shape and validation rules for plugin config. + * Uses JSONSchema7 instead of the generated ConfigSchema (which is too restrictive). */ config?: { schema: ConfigSchema; }; - - /** - * When true, excluded from the template plugins manifest during sync. - */ - hidden?: boolean; - - /** - * Optional metadata for community plugins - */ - author?: string; - version?: string; - repository?: string; - keywords?: string[]; - license?: string; } diff --git a/packages/shared/package.json b/packages/shared/package.json index a16658a71..5daf0f8ef 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -15,7 +15,7 @@ "./package.json": "./package.json" }, "scripts": { - "build:package": "tsdown --config tsdown.config.ts", + "build:package": "pnpm exec tsx ../../tools/generate-schema-types.ts && tsdown --config tsdown.config.ts", "build:watch": "tsdown --config tsdown.config.ts --watch", "typecheck": "tsc --noEmit", "clean": "rm -rf dist", diff --git a/packages/shared/src/cli/commands/plugin/add-resource/add-resource.ts b/packages/shared/src/cli/commands/plugin/add-resource/add-resource.ts index c6b3dd980..e614cd17d 100644 --- a/packages/shared/src/cli/commands/plugin/add-resource/add-resource.ts +++ b/packages/shared/src/cli/commands/plugin/add-resource/add-resource.ts @@ -6,7 +6,7 @@ import { Command } from "commander"; import { promptOneResource } from "../create/prompt-resource"; import { humanizeResourceType } from "../create/resource-defaults"; import { resolveManifestInDir } from "../manifest-resolve"; -import type { PluginManifest } from "../manifest-types"; +import type { PluginManifest, ResourceRequirement } from "../manifest-types"; import { validateManifest } from "../validate/validate-manifest"; /** Extended manifest type that preserves extra JSON fields (e.g. $schema, author, version) for round-trip writes. */ @@ -64,8 +64,10 @@ async function runPluginAddResource(options: { path?: string }): Promise { } const alias = humanizeResourceType(spec.type); - const entry = { - type: spec.type, + const entry: ResourceRequirement = { + // Safe cast: spec.type comes from RESOURCE_TYPE_OPTIONS which reads values + // from the same JSON schema that generates the ResourceType union. + type: spec.type as ResourceRequirement["type"], alias, resourceKey: spec.resourceKey, description: spec.description || `Required for ${alias} functionality.`, diff --git a/packages/shared/src/cli/commands/plugin/manifest-types.ts b/packages/shared/src/cli/commands/plugin/manifest-types.ts index 72516a8f4..1d896f493 100644 --- a/packages/shared/src/cli/commands/plugin/manifest-types.ts +++ b/packages/shared/src/cli/commands/plugin/manifest-types.ts @@ -1,40 +1,17 @@ /** * Shared types for plugin manifests used across CLI commands. - * Single source of truth for manifest structure — avoids duplicate - * definitions in sync, validate, list, and add-resource commands. + * Base types (ResourceFieldEntry, ResourceRequirement, PluginManifest) are + * generated from plugin-manifest.schema.json — only CLI-specific extensions + * (TemplatePlugin, TemplatePluginsManifest) are hand-written here. */ -export interface ResourceFieldEntry { - env?: string; - description?: string; - bundleIgnore?: boolean; - examples?: string[]; - localOnly?: boolean; - value?: string; - resolve?: string; -} - -export interface ResourceRequirement { - type: string; - alias: string; - resourceKey: string; - description: string; - permission: string; - fields: Record; -} +export type { + PluginManifest, + ResourceFieldEntry, + ResourceRequirement, +} from "../../../schemas/plugin-manifest.generated"; -export interface PluginManifest { - name: string; - displayName: string; - description: string; - resources: { - required: ResourceRequirement[]; - optional: ResourceRequirement[]; - }; - config?: { schema: unknown }; - onSetupMessage?: string; - hidden?: boolean; -} +import type { PluginManifest } from "../../../schemas/plugin-manifest.generated"; export interface TemplatePlugin extends Omit { package: string; diff --git a/packages/shared/src/plugin.ts b/packages/shared/src/plugin.ts index 94620f7ef..0020f616a 100644 --- a/packages/shared/src/plugin.ts +++ b/packages/shared/src/plugin.ts @@ -1,5 +1,13 @@ import type express from "express"; import type { JSONSchema7 } from "json-schema"; +import type { + PluginManifest as GeneratedPluginManifest, + ResourceRequirement as GeneratedResourceRequirement, + ResourceFieldEntry, +} from "./schemas/plugin-manifest.generated"; + +// Re-export generated types as the shared canonical definitions. +export type { ResourceFieldEntry }; /** Base plugin interface. */ export interface BasePlugin { @@ -72,13 +80,20 @@ export type PluginConstructor< }; /** - * Manifest declaration for plugins (imported from registry types). - * Re-exported here to avoid circular dependencies. + * Manifest declaration for plugins. + * Extends the generated PluginManifest with a generic name parameter + * and uses JSONSchema7 for config.schema (the generated ConfigSchema + * is too restrictive for plugin consumers). + * + * @see {@link GeneratedPluginManifest} — generated base from plugin-manifest.schema.json + * @see `packages/appkit/src/registry/types.ts` `PluginManifest` — strict appkit narrowing (enum types) */ -export interface PluginManifest { +export interface PluginManifest + extends Omit< + GeneratedPluginManifest, + "name" | "config" | "$schema" | "resources" + > { name: TName; - displayName: string; - description: string; resources: { required: Omit[]; optional: Omit[]; @@ -86,53 +101,17 @@ export interface PluginManifest { config?: { schema: JSONSchema7; }; - onSetupMessage?: string; - hidden?: boolean; - author?: string; - version?: string; - repository?: string; - keywords?: string[]; - license?: string; } /** - * Defines a single field for a resource. - * Each field maps to 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). - */ -export interface ResourceFieldEntry { - /** Environment variable name for this field */ - env?: string; - /** Human-readable description for this field */ - description?: string; - /** When true, this field is excluded from Databricks bundle configuration (databricks.yml) generation. */ - bundleIgnore?: boolean; - /** Example values showing the expected format for this field */ - examples?: string[]; - /** When true, this field is only generated for local .env files. The Databricks Apps platform auto-injects it at deploy time. */ - localOnly?: boolean; - /** Static value for this field. Used when no prompted or resolved value exists. */ - value?: string; - /** Named resolver prefixed by resource type (e.g., 'postgres:host'). The CLI resolves this value during the init prompt flow. */ - resolve?: string; -} - -/** - * Resource requirement declaration (imported from registry types). - * Re-exported here to avoid circular dependencies. + * Resource requirement with runtime fields added beyond the schema definition. + * - `fields` is made required (schema has it optional, but registry always populates it) + * - `required` boolean tracks whether the resource is mandatory at runtime + * + * @see {@link GeneratedResourceRequirement} — generated base from plugin-manifest.schema.json + * @see `packages/appkit/src/registry/types.ts` `ResourceRequirement` — strict appkit narrowing (enum types) */ -export interface ResourceRequirement { - type: string; - alias: string; - /** Stable key for machine use (env naming, composite keys, app.yaml). */ - resourceKey: string; - description: string; - permission: string; - /** - * Map of field name to env and optional description. - * Single-value types use one key (e.g. id); multi-value (database, secret) use multiple keys. - */ +export interface ResourceRequirement extends GeneratedResourceRequirement { fields: Record; required: boolean; } diff --git a/packages/shared/src/schemas/plugin-manifest.generated.ts b/packages/shared/src/schemas/plugin-manifest.generated.ts new file mode 100644 index 000000000..5d2e5d4a1 --- /dev/null +++ b/packages/shared/src/schemas/plugin-manifest.generated.ts @@ -0,0 +1,285 @@ +// AUTO-GENERATED from plugin-manifest.schema.json — do not edit. +// Run: pnpm exec tsx tools/generate-schema-types.ts +/** + * Declares a resource requirement for a plugin. Can be defined statically in a manifest or dynamically via getResourceRequirements(). + * + * This interface was referenced by `PluginManifest`'s JSON-Schema + * via the `definition` "resourceRequirement". + */ +export type ResourceRequirement = { + type: ResourceType; + /** + * Human-readable label for UI/display only. Deduplication uses resourceKey, not alias. + */ + alias: string; + /** + * Stable key for machine use: deduplication, env naming, composite keys, app.yaml. Required for registry lookup. + */ + resourceKey: string; + /** + * Human-readable description of why this resource is needed + */ + description: string; + /** + * Required permission level. Validated per resource type by the allOf/if-then rules below. + */ + permission: string; + /** + * Map of field name to env and optional description. Single-value types use one key (e.g. id); multi-value (database, secret) use multiple (e.g. instance_name, database_name or scope, key). + */ + fields?: { + [k: string]: ResourceFieldEntry; + }; +}; +/** + * Type of Databricks resource + * + * This interface was referenced by `PluginManifest`'s JSON-Schema + * via the `definition` "resourceType". + */ +export type ResourceType = + | "secret" + | "job" + | "sql_warehouse" + | "serving_endpoint" + | "volume" + | "vector_search_index" + | "uc_function" + | "uc_connection" + | "database" + | "postgres" + | "genie_space" + | "experiment" + | "app"; +/** + * Permission for secret resources (order: weakest to strongest) + * + * This interface was referenced by `PluginManifest`'s JSON-Schema + * via the `definition` "secretPermission". + */ +export type SecretPermission = "READ" | "WRITE" | "MANAGE"; +/** + * Permission for job resources (order: weakest to strongest) + * + * This interface was referenced by `PluginManifest`'s JSON-Schema + * via the `definition` "jobPermission". + */ +export type JobPermission = "CAN_VIEW" | "CAN_MANAGE_RUN" | "CAN_MANAGE"; +/** + * Permission for SQL warehouse resources (order: weakest to strongest) + * + * This interface was referenced by `PluginManifest`'s JSON-Schema + * via the `definition` "sqlWarehousePermission". + */ +export type SqlWarehousePermission = "CAN_USE" | "CAN_MANAGE"; +/** + * Permission for serving endpoint resources (order: weakest to strongest) + * + * This interface was referenced by `PluginManifest`'s JSON-Schema + * via the `definition` "servingEndpointPermission". + */ +export type ServingEndpointPermission = "CAN_VIEW" | "CAN_QUERY" | "CAN_MANAGE"; +/** + * Permission for Unity Catalog volume resources + * + * This interface was referenced by `PluginManifest`'s JSON-Schema + * via the `definition` "volumePermission". + */ +export type VolumePermission = "READ_VOLUME" | "WRITE_VOLUME"; +/** + * Permission for vector search index resources + * + * This interface was referenced by `PluginManifest`'s JSON-Schema + * via the `definition` "vectorSearchIndexPermission". + */ +export type VectorSearchIndexPermission = "SELECT"; +/** + * Permission for Unity Catalog function resources + * + * This interface was referenced by `PluginManifest`'s JSON-Schema + * via the `definition` "ucFunctionPermission". + */ +export type UcFunctionPermission = "EXECUTE"; +/** + * Permission for Unity Catalog connection resources + * + * This interface was referenced by `PluginManifest`'s JSON-Schema + * via the `definition` "ucConnectionPermission". + */ +export type UcConnectionPermission = "USE_CONNECTION"; +/** + * Permission for database resources + * + * This interface was referenced by `PluginManifest`'s JSON-Schema + * via the `definition` "databasePermission". + */ +export type DatabasePermission = "CAN_CONNECT_AND_CREATE"; +/** + * Permission for Postgres resources + * + * This interface was referenced by `PluginManifest`'s JSON-Schema + * via the `definition` "postgresPermission". + */ +export type PostgresPermission = "CAN_CONNECT_AND_CREATE"; +/** + * Permission for Genie Space resources (order: weakest to strongest) + * + * This interface was referenced by `PluginManifest`'s JSON-Schema + * via the `definition` "genieSpacePermission". + */ +export type GenieSpacePermission = + | "CAN_VIEW" + | "CAN_RUN" + | "CAN_EDIT" + | "CAN_MANAGE"; +/** + * Permission for MLflow experiment resources (order: weakest to strongest) + * + * This interface was referenced by `PluginManifest`'s JSON-Schema + * via the `definition` "experimentPermission". + */ +export type ExperimentPermission = "CAN_READ" | "CAN_EDIT" | "CAN_MANAGE"; +/** + * Permission for Databricks App resources + * + * This interface was referenced by `PluginManifest`'s JSON-Schema + * via the `definition` "appPermission". + */ +export type AppPermission = "CAN_USE"; + +/** + * Schema for Databricks AppKit plugin manifest files. Defines plugin metadata, resource requirements, and configuration options. + */ +export interface PluginManifest { + /** + * Reference to the JSON Schema for validation + */ + $schema?: string; + /** + * Plugin identifier. Must be lowercase, start with a letter, and contain only letters, numbers, and hyphens. + */ + name: string; + /** + * Human-readable display name for UI and CLI + */ + displayName: string; + /** + * Brief description of what the plugin does + */ + description: string; + /** + * Databricks resource requirements for this plugin + */ + resources: { + /** + * Resources that must be available for the plugin to function + */ + required: ResourceRequirement[]; + /** + * Resources that enhance functionality but are not mandatory + */ + optional: ResourceRequirement[]; + }; + /** + * Configuration schema for the plugin + */ + config?: { + schema?: ConfigSchema; + }; + /** + * Author name or organization + */ + author?: string; + /** + * Plugin version (semver format) + */ + version?: string; + /** + * URL to the plugin's source repository + */ + repository?: string; + /** + * Keywords for plugin discovery + */ + keywords?: string[]; + /** + * SPDX license identifier + */ + license?: string; + /** + * Message displayed to the user after project initialization. Use this to inform about manual setup steps (e.g. environment variables, resource provisioning). + */ + onSetupMessage?: string; + /** + * When true, this plugin is excluded from the template plugins manifest (appkit.plugins.json) during sync. + */ + hidden?: boolean; +} +/** + * 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". + */ +export interface ResourceFieldEntry { + /** + * Environment variable name for this field + */ + env?: string; + /** + * Human-readable description for this field + */ + description?: string; + /** + * When true, this field is excluded from Databricks bundle configuration (databricks.yml) generation. + */ + bundleIgnore?: boolean; + /** + * Example values showing the expected format for this field + */ + examples?: string[]; + /** + * When true, this field is only generated for local .env files. The Databricks Apps platform auto-injects it at deploy time. + */ + localOnly?: boolean; + /** + * Static value for this field. Used when no prompted or resolved value exists. + */ + value?: string; + /** + * Named resolver prefixed by resource type (e.g., 'postgres:host'). The CLI resolves this value during the init prompt flow. + */ + resolve?: string; +} +/** + * This interface was referenced by `PluginManifest`'s JSON-Schema + * via the `definition` "configSchema". + */ +export interface ConfigSchema { + type: "object" | "array" | "string" | "number" | "boolean"; + properties?: { + [k: string]: ConfigSchemaProperty; + }; + items?: ConfigSchema; + required?: string[]; + additionalProperties?: boolean; +} +/** + * This interface was referenced by `PluginManifest`'s JSON-Schema + * via the `definition` "configSchemaProperty". + */ +export interface ConfigSchemaProperty { + type: "object" | "array" | "string" | "number" | "boolean" | "integer"; + description?: string; + default?: unknown; + enum?: unknown[]; + properties?: { + [k: string]: ConfigSchemaProperty; + }; + items?: ConfigSchemaProperty; + minimum?: number; + maximum?: number; + minLength?: number; + maxLength?: number; + required?: string[]; +} diff --git a/packages/shared/src/schemas/plugin-manifest.schema.json b/packages/shared/src/schemas/plugin-manifest.schema.json index cae2e6884..ed4ef5731 100644 --- a/packages/shared/src/schemas/plugin-manifest.schema.json +++ b/packages/shared/src/schemas/plugin-manifest.schema.json @@ -185,6 +185,7 @@ }, "resourceFieldEntry": { "type": "object", + "description": "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).", "properties": { "env": { "type": "string", @@ -225,6 +226,7 @@ }, "resourceRequirement": { "type": "object", + "description": "Declares a resource requirement for a plugin. Can be defined statically in a manifest or dynamically via getResourceRequirements().", "required": ["type", "alias", "resourceKey", "description", "permission"], "properties": { "type": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48e7b80ed..67171ef7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: jsdom: specifier: ^27.0.0 version: 27.0.0(bufferutil@4.0.9)(postcss@8.5.6) + json-schema-to-typescript: + specifier: ^15.0.4 + version: 15.0.4 knip: specifier: ^5.86.0 version: 5.86.0(@types/node@24.7.2)(typescript@5.9.3) @@ -649,6 +652,10 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@apidevtools/json-schema-ref-parser@11.9.3': + resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==} + engines: {node: '>= 16'} + '@asamuzakjp/css-color@4.0.5': resolution: {integrity: sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==} @@ -2466,6 +2473,9 @@ packages: '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@jsdevtools/ono@7.1.3': + resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + '@jsonjoy.com/base64@1.1.2': resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} engines: {node: '>=10.0'} @@ -4736,6 +4746,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -7788,6 +7801,11 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-to-typescript@15.0.4: + resolution: {integrity: sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==} + engines: {node: '>=16.0.0'} + hasBin: true + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -9304,6 +9322,11 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + pretty-error@4.0.0: resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==} @@ -11430,6 +11453,12 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.0.1 + '@apidevtools/json-schema-ref-parser@11.9.3': + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + js-yaml: 4.1.1 + '@asamuzakjp/css-color@4.0.5': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) @@ -14041,6 +14070,8 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} + '@jsdevtools/ono@7.1.3': {} + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': dependencies: tslib: 2.8.1 @@ -16456,6 +16487,8 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/lodash@4.17.24': {} + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -19878,6 +19911,18 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-schema-to-typescript@15.0.4: + dependencies: + '@apidevtools/json-schema-ref-parser': 11.9.3 + '@types/json-schema': 7.0.15 + '@types/lodash': 4.17.24 + is-glob: 4.0.3 + js-yaml: 4.1.1 + lodash: 4.17.21 + minimist: 1.2.8 + prettier: 3.8.1 + tinyglobby: 0.2.15 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -21708,6 +21753,8 @@ snapshots: prelude-ls@1.2.1: {} + prettier@3.8.1: {} + pretty-error@4.0.0: dependencies: lodash: 4.17.21 diff --git a/tools/generate-schema-types.ts b/tools/generate-schema-types.ts new file mode 100644 index 000000000..36f31e4ac --- /dev/null +++ b/tools/generate-schema-types.ts @@ -0,0 +1,54 @@ +/** + * Generates TypeScript interfaces from plugin-manifest.schema.json using + * json-schema-to-typescript. Single source of truth for structural types + * (ResourceFieldEntry, ResourceRequirement, PluginManifest). + * + * Run from repo root: pnpm exec tsx tools/generate-schema-types.ts + */ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { compileFromFile } from "json-schema-to-typescript"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.join(__dirname, ".."); +const SCHEMA_PATH = path.join( + REPO_ROOT, + "packages/shared/src/schemas/plugin-manifest.schema.json", +); +const OUT_PATH = path.join( + REPO_ROOT, + "packages/shared/src/schemas/plugin-manifest.generated.ts", +); + +const BANNER = `// AUTO-GENERATED from plugin-manifest.schema.json — do not edit. +// Run: pnpm exec tsx tools/generate-schema-types.ts +`; + +async function main(): Promise { + const raw = await compileFromFile(SCHEMA_PATH, { + bannerComment: "", + additionalProperties: false, + strictIndexSignatures: false, + unreachableDefinitions: true, + format: false, + style: { semi: true, singleQuote: false }, + // Rename the root type (derived from schema title "AppKit Plugin Manifest") + // to "PluginManifest" for ergonomic imports. + customName: (schema) => + schema.title === "AppKit Plugin Manifest" ? "PluginManifest" : undefined, + }); + + // Post-processing: work around json-schema-to-typescript limitations that + // have no config options. Track upstream: https://github.com/bcherny/json-schema-to-typescript/issues/428 + // allOf/if-then produces `{ [k: string]: unknown } & { … }` — strip the index-signature part. + const output = raw.replace(/\{\s*\[k: string\]: unknown;?\s*\}\s*&\s*/g, ""); + + const result = BANNER + output; + + fs.mkdirSync(path.dirname(OUT_PATH), { recursive: true }); + fs.writeFileSync(OUT_PATH, result, "utf-8"); + console.log("Wrote", OUT_PATH); +} + +main();