@databricks/appkit-ui 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CLAUDE.md +11 -0
  2. package/NOTICE.md +1 -0
  3. package/dist/cli/commands/docs.js +7 -1
  4. package/dist/cli/commands/docs.js.map +1 -1
  5. package/dist/cli/commands/generate-types.js +27 -15
  6. package/dist/cli/commands/generate-types.js.map +1 -1
  7. package/dist/cli/commands/lint.js +3 -1
  8. package/dist/cli/commands/lint.js.map +1 -1
  9. package/dist/cli/commands/plugin/add-resource/add-resource.js +73 -8
  10. package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
  11. package/dist/cli/commands/plugin/create/create.js +164 -20
  12. package/dist/cli/commands/plugin/create/create.js.map +1 -1
  13. package/dist/cli/commands/plugin/create/resource-defaults.js +5 -1
  14. package/dist/cli/commands/plugin/create/resource-defaults.js.map +1 -1
  15. package/dist/cli/commands/plugin/index.js +7 -1
  16. package/dist/cli/commands/plugin/index.js.map +1 -1
  17. package/dist/cli/commands/plugin/list/list.js +7 -1
  18. package/dist/cli/commands/plugin/list/list.js.map +1 -1
  19. package/dist/cli/commands/plugin/sync/sync.js +27 -14
  20. package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
  21. package/dist/cli/commands/plugin/validate/validate.js +39 -9
  22. package/dist/cli/commands/plugin/validate/validate.js.map +1 -1
  23. package/dist/cli/commands/setup.js +6 -5
  24. package/dist/cli/commands/setup.js.map +1 -1
  25. package/dist/react/hooks/index.d.ts +4 -2
  26. package/dist/react/hooks/index.js +2 -0
  27. package/dist/react/hooks/types.d.ts +31 -2
  28. package/dist/react/hooks/types.d.ts.map +1 -1
  29. package/dist/react/hooks/use-serving-invoke.d.ts +30 -0
  30. package/dist/react/hooks/use-serving-invoke.d.ts.map +1 -0
  31. package/dist/react/hooks/use-serving-invoke.js +82 -0
  32. package/dist/react/hooks/use-serving-invoke.js.map +1 -0
  33. package/dist/react/hooks/use-serving-stream.d.ts +35 -0
  34. package/dist/react/hooks/use-serving-stream.d.ts.map +1 -0
  35. package/dist/react/hooks/use-serving-stream.js +101 -0
  36. package/dist/react/hooks/use-serving-stream.js.map +1 -0
  37. package/dist/react/index.d.ts +4 -2
  38. package/dist/react/index.js +3 -1
  39. package/docs/api/appkit/Class.Plugin.md +8 -3
  40. package/docs/api/appkit/Function.appKitServingTypesPlugin.md +24 -0
  41. package/docs/api/appkit/Function.extractServingEndpoints.md +22 -0
  42. package/docs/api/appkit/Function.findServerFile.md +20 -0
  43. package/docs/api/appkit/Interface.EndpointConfig.md +23 -0
  44. package/docs/api/appkit/Interface.ServingEndpointEntry.md +30 -0
  45. package/docs/api/appkit/Interface.ServingEndpointRegistry.md +3 -0
  46. package/docs/api/appkit/TypeAlias.ExecutionResult.md +36 -0
  47. package/docs/api/appkit/TypeAlias.ServingFactory.md +19 -0
  48. package/docs/api/appkit.md +39 -31
  49. package/docs/development/type-generation.md +6 -5
  50. package/docs/faq.md +66 -0
  51. package/docs/plugins/analytics.md +1 -1
  52. package/docs/plugins/custom-plugins.md +4 -0
  53. package/docs/plugins/plugin-management.md +22 -6
  54. package/docs/plugins/serving.md +223 -0
  55. package/docs/plugins/vector-search.md +247 -0
  56. package/llms.txt +11 -0
  57. package/package.json +1 -1
  58. package/sbom.cdx.json +1 -1
package/CLAUDE.md CHANGED
@@ -27,6 +27,7 @@ npx @databricks/appkit docs <query>
27
27
  - [Configuration](./docs/configuration.md): This guide covers environment variables and configuration options for AppKit applications.
28
28
  - [Core principles](./docs/core-principles.md): Learn about the fundamental concepts and principles behind AppKit.
29
29
  - [Development](./docs/development.md): AppKit provides multiple development workflows to suit different needs: local development with hot reload, AI-assisted development with Agent Skills, and remote tunneling to deployed backends.
30
+ - [FAQ](./docs/faq.md): Integrations
30
31
  - [Plugins](./docs/plugins.md): Plugins are modular extensions that add capabilities to your AppKit application. They follow a defined lifecycle and have access to shared services like caching, telemetry, and streaming.
31
32
 
32
33
  ## Development
@@ -50,6 +51,8 @@ npx @databricks/appkit docs <query>
50
51
  - [Lakebase plugin](./docs/plugins/lakebase.md): Provides a PostgreSQL connection pool for Databricks Lakebase Autoscaling with automatic OAuth token refresh.
51
52
  - [Plugin management](./docs/plugins/plugin-management.md): AppKit includes a CLI for managing plugins. All commands are available under npx @databricks/appkit plugin.
52
53
  - [Server plugin](./docs/plugins/server.md): Provides HTTP server capabilities with development and production modes.
54
+ - [Serving plugin](./docs/plugins/serving.md): Provides an authenticated proxy to Databricks Model Serving endpoints, with invoke and streaming support.
55
+ - [Vector Search plugin](./docs/plugins/vector-search.md): Query Databricks Vector Search indexes with hybrid search, reranking, and cursor pagination from your AppKit application.
53
56
 
54
57
  ## appkit API reference [collapsed]
55
58
 
@@ -67,9 +70,12 @@ npx @databricks/appkit docs <query>
67
70
  - [Class: ValidationError](./docs/api/appkit/Class.ValidationError.md): Error thrown when input validation fails.
68
71
  - [Enumeration: RequestedClaimsPermissionSet](./docs/api/appkit/Enumeration.RequestedClaimsPermissionSet.md): Permission set for Unity Catalog table access
69
72
  - [Enumeration: ResourceType](./docs/api/appkit/Enumeration.ResourceType.md): Resource types from schema $defs.resourceType.enum
73
+ - [Function: appKitServingTypesPlugin()](./docs/api/appkit/Function.appKitServingTypesPlugin.md): Vite plugin to generate TypeScript types for AppKit serving endpoints.
70
74
  - [Function: appKitTypesPlugin()](./docs/api/appkit/Function.appKitTypesPlugin.md): Vite plugin to generate types for AppKit queries.
71
75
  - [Function: createApp()](./docs/api/appkit/Function.createApp.md): Bootstraps AppKit with the provided configuration.
72
76
  - [Function: createLakebasePool()](./docs/api/appkit/Function.createLakebasePool.md): Create a Lakebase pool with appkit's logger integration.
77
+ - [Function: extractServingEndpoints()](./docs/api/appkit/Function.extractServingEndpoints.md): Extract serving endpoint config from a server file by AST-parsing it.
78
+ - [Function: findServerFile()](./docs/api/appkit/Function.findServerFile.md): Find the server entry file by checking candidate paths in order.
73
79
  - [Function: generateDatabaseCredential()](./docs/api/appkit/Function.generateDatabaseCredential.md): Generate OAuth credentials for Postgres database connection using the proper Postgres API.
74
80
  - [Function: getExecutionContext()](./docs/api/appkit/Function.getExecutionContext.md): Get the current execution context.
75
81
  - [Function: getLakebaseOrmConfig()](./docs/api/appkit/Function.getLakebaseOrmConfig.md): Get Lakebase connection configuration for ORMs that don't accept pg.Pool directly.
@@ -82,6 +88,7 @@ npx @databricks/appkit docs <query>
82
88
  - [Interface: BasePluginConfig](./docs/api/appkit/Interface.BasePluginConfig.md): Base configuration interface for AppKit plugins
83
89
  - [Interface: CacheConfig](./docs/api/appkit/Interface.CacheConfig.md): Configuration for the CacheInterceptor. Controls TTL, size limits, storage backend, and probabilistic cleanup.
84
90
  - [Interface: DatabaseCredential](./docs/api/appkit/Interface.DatabaseCredential.md): Database credentials with OAuth token for Postgres connection
91
+ - [Interface: EndpointConfig](./docs/api/appkit/Interface.EndpointConfig.md): Properties
85
92
  - [Interface: GenerateDatabaseCredentialRequest](./docs/api/appkit/Interface.GenerateDatabaseCredentialRequest.md): Request parameters for generating database OAuth credentials
86
93
  - [Interface: ITelemetry](./docs/api/appkit/Interface.ITelemetry.md): Plugin-facing interface for OpenTelemetry instrumentation.
87
94
  - [Interface: LakebasePoolConfig](./docs/api/appkit/Interface.LakebasePoolConfig.md): Configuration for creating a Lakebase connection pool
@@ -91,13 +98,17 @@ npx @databricks/appkit docs <query>
91
98
  - [Interface: ResourceEntry](./docs/api/appkit/Interface.ResourceEntry.md): Internal representation of a resource in the registry.
92
99
  - [Interface: ResourceFieldEntry](./docs/api/appkit/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. instancename, databasename or scope, key).
93
100
  - [Interface: ResourceRequirement](./docs/api/appkit/Interface.ResourceRequirement.md): Declares a resource requirement for a plugin.
101
+ - [Interface: ServingEndpointEntry](./docs/api/appkit/Interface.ServingEndpointEntry.md): Shape of a single registry entry.
102
+ - [Interface: ServingEndpointRegistry](./docs/api/appkit/Interface.ServingEndpointRegistry.md): Registry interface for serving endpoint type generation.
94
103
  - [Interface: StreamExecutionSettings](./docs/api/appkit/Interface.StreamExecutionSettings.md): Execution settings for streaming endpoints. Extends PluginExecutionSettings with SSE stream configuration.
95
104
  - [Interface: TelemetryConfig](./docs/api/appkit/Interface.TelemetryConfig.md): OpenTelemetry configuration for AppKit applications
96
105
  - [Interface: ValidationResult](./docs/api/appkit/Interface.ValidationResult.md): Result of validating all registered resources against the environment.
97
106
  - [Type Alias: ConfigSchema](./docs/api/appkit/TypeAlias.ConfigSchema.md): Configuration schema definition for plugin config.
107
+ - [Type Alias: ExecutionResult<T>](./docs/api/appkit/TypeAlias.ExecutionResult.md): Discriminated union for plugin execution results.
98
108
  - [Type Alias: IAppRouter](./docs/api/appkit/TypeAlias.IAppRouter.md): Express router type for plugin route registration
99
109
  - [Type Alias: PluginData<T, U, N>](./docs/api/appkit/TypeAlias.PluginData.md): Tuple of plugin class, config, and name. Created by toPlugin() and passed to createApp().
100
110
  - [Type Alias: ResourcePermission](./docs/api/appkit/TypeAlias.ResourcePermission.md): Union of all possible permission levels across all resource types.
111
+ - [Type Alias: ServingFactory](./docs/api/appkit/TypeAlias.ServingFactory.md): Factory function returned by AppKit.serving.
101
112
  - [Type Alias: ToPlugin()<T, U, N>](./docs/api/appkit/TypeAlias.ToPlugin.md): Factory function type returned by toPlugin(). Accepts optional config and returns a PluginData tuple.
102
113
  - [Variable: sql](./docs/api/appkit/Variable.sql.md): SQL helper namespace
103
114
 
package/NOTICE.md CHANGED
@@ -58,6 +58,7 @@ This Software contains code from the following open source projects:
58
58
  | [clsx](https://www.npmjs.com/package/clsx) | 2.1.1 | MIT | https://github.com/lukeed/clsx#readme |
59
59
  | [cmdk](https://www.npmjs.com/package/cmdk) | 1.1.1 | MIT | https://github.com/pacocoursey/cmdk#readme |
60
60
  | [commander](https://www.npmjs.com/package/commander) | 2.20.3, 5.1.0, 7.2.0, 8.3.0, 10.0.1, 12.1.0 | MIT | https://github.com/tj/commander.js#readme |
61
+ | [dompurify](https://www.npmjs.com/package/dompurify) | 3.3.3 | (MPL-2.0 OR Apache-2.0) | https://github.com/cure53/DOMPurify |
61
62
  | [dotenv](https://www.npmjs.com/package/dotenv) | 16.6.1 | BSD-2-Clause | https://github.com/motdotla/dotenv#readme |
62
63
  | [echarts](https://www.npmjs.com/package/echarts) | 6.0.0 | Apache-2.0 | https://echarts.apache.org |
63
64
  | [echarts-for-react](https://www.npmjs.com/package/echarts-for-react) | 3.0.5 | MIT | https://github.com/hustcc/echarts-for-react |
@@ -103,7 +103,13 @@ function runDocs(query, options) {
103
103
  const output = header + sections.map((s) => s.collapsed ? formatCollapsedSection(s) : formatExpandedSection(s)).join("\n");
104
104
  console.log(output);
105
105
  }
106
- const docsCommand = new Command("docs").description("Display embedded documentation").argument("[query]", "Section name (e.g. 'plugins') or path to a doc file (e.g. './docs.md')").option("--full", "Show complete index including all API reference entries").action(runDocs);
106
+ const docsCommand = new Command("docs").description("Display embedded documentation").argument("[query]", "Section name (e.g. 'plugins') or path to a doc file (e.g. './docs.md')").option("--full", "Show complete index including all API reference entries").addHelpText("after", `
107
+ Examples:
108
+ $ appkit docs
109
+ $ appkit docs plugins
110
+ $ appkit docs "appkit-ui API reference"
111
+ $ appkit docs ./docs/plugins/analytics.md
112
+ $ appkit docs --full`).action(runDocs);
107
113
 
108
114
  //#endregion
109
115
  export { docsCommand };
@@ -1 +1 @@
1
- {"version":3,"file":"docs.js","names":[],"sources":["../../../src/cli/commands/docs.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Command } from \"commander\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nconst COLLAPSED_MARKER = \"[collapsed]\";\n\ninterface Section {\n name: string;\n body: string;\n collapsed: boolean;\n}\n\nfunction findPackageRoot(): string {\n let dir = __dirname;\n while (dir !== path.parse(dir).root) {\n if (fs.existsSync(path.join(dir, \"package.json\"))) {\n return dir;\n }\n dir = path.dirname(dir);\n }\n throw new Error(\"Could not find package root\");\n}\n\nfunction parseSections(content: string): {\n header: string;\n sections: Section[];\n} {\n const parts = content.split(/^(## .+)$/m);\n const header = parts[0];\n const sections: Section[] = [];\n\n for (let i = 1; i < parts.length; i += 2) {\n const rawName = parts[i].replace(/^## /, \"\");\n const body = parts[i + 1] ?? \"\";\n const collapsed = rawName.includes(COLLAPSED_MARKER);\n const name = rawName.replace(COLLAPSED_MARKER, \"\").trim();\n sections.push({ name, body, collapsed });\n }\n\n return { header, sections };\n}\n\nfunction countPages(body: string): number {\n return (body.match(/^- \\[/gm) || []).length;\n}\n\nfunction findSections(sections: Section[], query: string): Section[] {\n const q = query.toLowerCase();\n return sections.filter((s) => s.name.toLowerCase().includes(q));\n}\n\nfunction isFilePath(arg: string): boolean {\n return arg.startsWith(\"./\") && arg.endsWith(\".md\");\n}\n\nfunction readLlmsTxt(packageRoot: string): string {\n const llmsPath = path.join(packageRoot, \"llms.txt\");\n if (!fs.existsSync(llmsPath)) {\n console.error(\"Error: llms.txt not found in package\");\n process.exit(1);\n }\n return fs.readFileSync(llmsPath, \"utf-8\");\n}\n\nfunction readDocFile(packageRoot: string, docPath: string): void {\n let normalizedPath = docPath;\n normalizedPath = normalizedPath.replace(/^\\.\\//, \"\");\n normalizedPath = normalizedPath.replace(/^\\//, \"\");\n normalizedPath = normalizedPath.replace(/^appkit\\//, \"\");\n\n const fullPath = path.join(packageRoot, normalizedPath);\n\n if (!fs.existsSync(fullPath)) {\n console.error(`Error: Documentation file not found: ${docPath}`);\n console.error(`Tried: ${fullPath}`);\n process.exit(1);\n }\n\n console.log(fs.readFileSync(fullPath, \"utf-8\"));\n}\n\nfunction formatCollapsedSection(section: Section): string {\n const pages = countPages(section.body);\n return [\n `## ${section.name} (${pages} pages)`,\n \"\",\n `> Use \\`appkit docs \"${section.name}\"\\` to expand, or \\`appkit docs --full\\` to expand all sections.`,\n \"\",\n ].join(\"\\n\");\n}\n\nfunction formatExpandedSection(section: Section): string {\n return `## ${section.name}${section.body}`;\n}\n\nfunction runDocs(query: string | undefined, options: { full?: boolean }) {\n const packageRoot = findPackageRoot();\n\n if (query && isFilePath(query)) {\n readDocFile(packageRoot, query);\n return;\n }\n\n const content = readLlmsTxt(packageRoot);\n\n if (options.full) {\n console.log(content.replaceAll(` ${COLLAPSED_MARKER}`, \"\"));\n return;\n }\n\n const { header, sections } = parseSections(content);\n\n if (query) {\n const matched = findSections(sections, query);\n if (matched.length === 0) {\n const available = sections.map((s) => ` - ${s.name}`).join(\"\\n\");\n console.error(\n `No section matching \"${query}\". Available sections:\\n${available}`,\n );\n process.exit(1);\n }\n console.log(matched.map(formatExpandedSection).join(\"\\n\"));\n return;\n }\n\n const output =\n header +\n sections\n .map((s) =>\n s.collapsed ? formatCollapsedSection(s) : formatExpandedSection(s),\n )\n .join(\"\\n\");\n console.log(output);\n}\n\nexport const docsCommand = new Command(\"docs\")\n .description(\"Display embedded documentation\")\n .argument(\n \"[query]\",\n \"Section name (e.g. 'plugins') or path to a doc file (e.g. './docs.md')\",\n )\n .option(\"--full\", \"Show complete index including all API reference entries\")\n .action(runDocs);\n"],"mappings":"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"}
1
+ {"version":3,"file":"docs.js","names":[],"sources":["../../../src/cli/commands/docs.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Command } from \"commander\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nconst COLLAPSED_MARKER = \"[collapsed]\";\n\ninterface Section {\n name: string;\n body: string;\n collapsed: boolean;\n}\n\nfunction findPackageRoot(): string {\n let dir = __dirname;\n while (dir !== path.parse(dir).root) {\n if (fs.existsSync(path.join(dir, \"package.json\"))) {\n return dir;\n }\n dir = path.dirname(dir);\n }\n throw new Error(\"Could not find package root\");\n}\n\nfunction parseSections(content: string): {\n header: string;\n sections: Section[];\n} {\n const parts = content.split(/^(## .+)$/m);\n const header = parts[0];\n const sections: Section[] = [];\n\n for (let i = 1; i < parts.length; i += 2) {\n const rawName = parts[i].replace(/^## /, \"\");\n const body = parts[i + 1] ?? \"\";\n const collapsed = rawName.includes(COLLAPSED_MARKER);\n const name = rawName.replace(COLLAPSED_MARKER, \"\").trim();\n sections.push({ name, body, collapsed });\n }\n\n return { header, sections };\n}\n\nfunction countPages(body: string): number {\n return (body.match(/^- \\[/gm) || []).length;\n}\n\nfunction findSections(sections: Section[], query: string): Section[] {\n const q = query.toLowerCase();\n return sections.filter((s) => s.name.toLowerCase().includes(q));\n}\n\nfunction isFilePath(arg: string): boolean {\n return arg.startsWith(\"./\") && arg.endsWith(\".md\");\n}\n\nfunction readLlmsTxt(packageRoot: string): string {\n const llmsPath = path.join(packageRoot, \"llms.txt\");\n if (!fs.existsSync(llmsPath)) {\n console.error(\"Error: llms.txt not found in package\");\n process.exit(1);\n }\n return fs.readFileSync(llmsPath, \"utf-8\");\n}\n\nfunction readDocFile(packageRoot: string, docPath: string): void {\n let normalizedPath = docPath;\n normalizedPath = normalizedPath.replace(/^\\.\\//, \"\");\n normalizedPath = normalizedPath.replace(/^\\//, \"\");\n normalizedPath = normalizedPath.replace(/^appkit\\//, \"\");\n\n const fullPath = path.join(packageRoot, normalizedPath);\n\n if (!fs.existsSync(fullPath)) {\n console.error(`Error: Documentation file not found: ${docPath}`);\n console.error(`Tried: ${fullPath}`);\n process.exit(1);\n }\n\n console.log(fs.readFileSync(fullPath, \"utf-8\"));\n}\n\nfunction formatCollapsedSection(section: Section): string {\n const pages = countPages(section.body);\n return [\n `## ${section.name} (${pages} pages)`,\n \"\",\n `> Use \\`appkit docs \"${section.name}\"\\` to expand, or \\`appkit docs --full\\` to expand all sections.`,\n \"\",\n ].join(\"\\n\");\n}\n\nfunction formatExpandedSection(section: Section): string {\n return `## ${section.name}${section.body}`;\n}\n\nfunction runDocs(query: string | undefined, options: { full?: boolean }) {\n const packageRoot = findPackageRoot();\n\n if (query && isFilePath(query)) {\n readDocFile(packageRoot, query);\n return;\n }\n\n const content = readLlmsTxt(packageRoot);\n\n if (options.full) {\n console.log(content.replaceAll(` ${COLLAPSED_MARKER}`, \"\"));\n return;\n }\n\n const { header, sections } = parseSections(content);\n\n if (query) {\n const matched = findSections(sections, query);\n if (matched.length === 0) {\n const available = sections.map((s) => ` - ${s.name}`).join(\"\\n\");\n console.error(\n `No section matching \"${query}\". Available sections:\\n${available}`,\n );\n process.exit(1);\n }\n console.log(matched.map(formatExpandedSection).join(\"\\n\"));\n return;\n }\n\n const output =\n header +\n sections\n .map((s) =>\n s.collapsed ? formatCollapsedSection(s) : formatExpandedSection(s),\n )\n .join(\"\\n\");\n console.log(output);\n}\n\nexport const docsCommand = new Command(\"docs\")\n .description(\"Display embedded documentation\")\n .argument(\n \"[query]\",\n \"Section name (e.g. 'plugins') or path to a doc file (e.g. './docs.md')\",\n )\n .option(\"--full\", \"Show complete index including all API reference entries\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit docs\n $ appkit docs plugins\n $ appkit docs \"appkit-ui API reference\"\n $ appkit docs ./docs/plugins/analytics.md\n $ appkit docs --full`,\n )\n .action(runDocs);\n"],"mappings":"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"}
@@ -8,22 +8,29 @@ import { Command } from "commander";
8
8
  */
9
9
  async function runGenerateTypes(rootDir, outFile, warehouseId, options) {
10
10
  try {
11
- const resolvedWarehouseId = warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;
12
- if (!resolvedWarehouseId) process.exit(0);
13
- const { generateFromEntryPoint } = await import("@databricks/appkit/type-generator");
14
11
  const resolvedRootDir = rootDir || process.cwd();
15
- const resolvedOutFile = outFile || path.join(process.cwd(), "client/src/appKitTypes.d.ts");
16
- const queryFolder = path.join(resolvedRootDir, "config/queries");
17
- if (!fs.existsSync(queryFolder)) {
18
- console.warn(`Warning: No queries found at ${queryFolder}. Skipping type generation.`);
19
- return;
20
- }
21
- await generateFromEntryPoint({
22
- queryFolder,
23
- outFile: resolvedOutFile,
24
- warehouseId: resolvedWarehouseId,
25
- noCache: options?.noCache || false
12
+ const noCache = options?.noCache || false;
13
+ const typeGen = await import("@databricks/appkit/type-generator");
14
+ const resolvedWarehouseId = warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;
15
+ if (resolvedWarehouseId) {
16
+ const resolvedOutFile = outFile || path.join(process.cwd(), "shared/appkit-types/analytics.d.ts");
17
+ const queryFolder = path.join(resolvedRootDir, "config/queries");
18
+ if (fs.existsSync(queryFolder)) {
19
+ await typeGen.generateFromEntryPoint({
20
+ queryFolder,
21
+ outFile: resolvedOutFile,
22
+ warehouseId: resolvedWarehouseId,
23
+ noCache
24
+ });
25
+ console.log(`Generated query types: ${resolvedOutFile}`);
26
+ }
27
+ } else console.error("Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.");
28
+ const servingOutFile = path.join(process.cwd(), "shared/appkit-types/serving.d.ts");
29
+ await typeGen.generateServingTypes({
30
+ outFile: servingOutFile,
31
+ noCache
26
32
  });
33
+ console.log(`Generated serving types: ${servingOutFile}`);
27
34
  } catch (error) {
28
35
  if (error instanceof Error && error.message.includes("Cannot find module")) {
29
36
  console.error("Error: The 'generate-types' command is only available in @databricks/appkit.");
@@ -33,7 +40,12 @@ async function runGenerateTypes(rootDir, outFile, warehouseId, options) {
33
40
  throw error;
34
41
  }
35
42
  }
36
- const generateTypesCommand = new Command("generate-types").description("Generate TypeScript types from SQL queries").argument("[rootDir]", "Root directory of the project", process.cwd()).argument("[outFile]", "Output file path", path.join(process.cwd(), "client/src/appKitTypes.d.ts")).argument("[warehouseId]", "Databricks warehouse ID").option("--no-cache", "Disable caching for type generation").action(runGenerateTypes);
43
+ const generateTypesCommand = new Command("generate-types").description("Generate TypeScript types from SQL queries").argument("[rootDir]", "Root directory of the project", process.cwd()).argument("[outFile]", "Output file path", path.join(process.cwd(), "shared/appkit-types/analytics.d.ts")).argument("[warehouseId]", "Databricks warehouse ID").option("--no-cache", "Disable caching for type generation").addHelpText("after", `
44
+ Examples:
45
+ $ appkit generate-types
46
+ $ appkit generate-types . shared/appkit-types/analytics.d.ts
47
+ $ appkit generate-types . shared/appkit-types/analytics.d.ts my-warehouse-id
48
+ $ appkit generate-types --no-cache`).action(runGenerateTypes);
37
49
 
38
50
  //#endregion
39
51
  export { generateTypesCommand };
@@ -1 +1 @@
1
- {"version":3,"file":"generate-types.js","names":[],"sources":["../../../src/cli/commands/generate-types.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command } from \"commander\";\n\n/**\n * Generate types command implementation\n */\nasync function runGenerateTypes(\n rootDir?: string,\n outFile?: string,\n warehouseId?: string,\n options?: { noCache?: boolean },\n) {\n try {\n const resolvedWarehouseId =\n warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;\n\n if (!resolvedWarehouseId) {\n process.exit(0);\n }\n\n // Try to import the type generator from @databricks/appkit\n const { generateFromEntryPoint } = await import(\n \"@databricks/appkit/type-generator\"\n );\n\n const resolvedRootDir = rootDir || process.cwd();\n const resolvedOutFile =\n outFile || path.join(process.cwd(), \"client/src/appKitTypes.d.ts\");\n\n const queryFolder = path.join(resolvedRootDir, \"config/queries\");\n if (!fs.existsSync(queryFolder)) {\n console.warn(\n `Warning: No queries found at ${queryFolder}. Skipping type generation.`,\n );\n return;\n }\n\n await generateFromEntryPoint({\n queryFolder,\n outFile: resolvedOutFile,\n warehouseId: resolvedWarehouseId,\n noCache: options?.noCache || false,\n });\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"Cannot find module\")\n ) {\n console.error(\n \"Error: The 'generate-types' command is only available in @databricks/appkit.\",\n );\n console.error(\"Please install @databricks/appkit to use this command.\");\n process.exit(1);\n }\n throw error;\n }\n}\n\nexport const generateTypesCommand = new Command(\"generate-types\")\n .description(\"Generate TypeScript types from SQL queries\")\n .argument(\"[rootDir]\", \"Root directory of the project\", process.cwd())\n .argument(\n \"[outFile]\",\n \"Output file path\",\n path.join(process.cwd(), \"client/src/appKitTypes.d.ts\"),\n )\n .argument(\"[warehouseId]\", \"Databricks warehouse ID\")\n .option(\"--no-cache\", \"Disable caching for type generation\")\n .action(runGenerateTypes);\n"],"mappings":";;;;;;;;AAOA,eAAe,iBACb,SACA,SACA,aACA,SACA;AACA,KAAI;EACF,MAAM,sBACJ,eAAe,QAAQ,IAAI;AAE7B,MAAI,CAAC,oBACH,SAAQ,KAAK,EAAE;EAIjB,MAAM,EAAE,2BAA2B,MAAM,OACvC;EAGF,MAAM,kBAAkB,WAAW,QAAQ,KAAK;EAChD,MAAM,kBACJ,WAAW,KAAK,KAAK,QAAQ,KAAK,EAAE,8BAA8B;EAEpE,MAAM,cAAc,KAAK,KAAK,iBAAiB,iBAAiB;AAChE,MAAI,CAAC,GAAG,WAAW,YAAY,EAAE;AAC/B,WAAQ,KACN,gCAAgC,YAAY,6BAC7C;AACD;;AAGF,QAAM,uBAAuB;GAC3B;GACA,SAAS;GACT,aAAa;GACb,SAAS,SAAS,WAAW;GAC9B,CAAC;UACK,OAAO;AACd,MACE,iBAAiB,SACjB,MAAM,QAAQ,SAAS,qBAAqB,EAC5C;AACA,WAAQ,MACN,+EACD;AACD,WAAQ,MAAM,yDAAyD;AACvE,WAAQ,KAAK,EAAE;;AAEjB,QAAM;;;AAIV,MAAa,uBAAuB,IAAI,QAAQ,iBAAiB,CAC9D,YAAY,6CAA6C,CACzD,SAAS,aAAa,iCAAiC,QAAQ,KAAK,CAAC,CACrE,SACC,aACA,oBACA,KAAK,KAAK,QAAQ,KAAK,EAAE,8BAA8B,CACxD,CACA,SAAS,iBAAiB,0BAA0B,CACpD,OAAO,cAAc,sCAAsC,CAC3D,OAAO,iBAAiB"}
1
+ {"version":3,"file":"generate-types.js","names":[],"sources":["../../../src/cli/commands/generate-types.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command } from \"commander\";\n\n/**\n * Generate types command implementation\n */\nasync function runGenerateTypes(\n rootDir?: string,\n outFile?: string,\n warehouseId?: string,\n options?: { noCache?: boolean },\n) {\n try {\n const resolvedRootDir = rootDir || process.cwd();\n const noCache = options?.noCache || false;\n\n const typeGen = await import(\"@databricks/appkit/type-generator\");\n\n // Generate analytics query types (requires warehouse ID)\n const resolvedWarehouseId =\n warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;\n\n if (resolvedWarehouseId) {\n const resolvedOutFile =\n outFile ||\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\");\n\n const queryFolder = path.join(resolvedRootDir, \"config/queries\");\n if (fs.existsSync(queryFolder)) {\n await typeGen.generateFromEntryPoint({\n queryFolder,\n outFile: resolvedOutFile,\n warehouseId: resolvedWarehouseId,\n noCache,\n });\n console.log(`Generated query types: ${resolvedOutFile}`);\n }\n } else {\n console.error(\n \"Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.\",\n );\n }\n\n // Generate serving endpoint types (no warehouse required)\n const servingOutFile = path.join(\n process.cwd(),\n \"shared/appkit-types/serving.d.ts\",\n );\n await typeGen.generateServingTypes({\n outFile: servingOutFile,\n noCache,\n });\n console.log(`Generated serving types: ${servingOutFile}`);\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"Cannot find module\")\n ) {\n console.error(\n \"Error: The 'generate-types' command is only available in @databricks/appkit.\",\n );\n console.error(\"Please install @databricks/appkit to use this command.\");\n process.exit(1);\n }\n throw error;\n }\n}\n\nexport const generateTypesCommand = new Command(\"generate-types\")\n .description(\"Generate TypeScript types from SQL queries\")\n .argument(\"[rootDir]\", \"Root directory of the project\", process.cwd())\n .argument(\n \"[outFile]\",\n \"Output file path\",\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\"),\n )\n .argument(\"[warehouseId]\", \"Databricks warehouse ID\")\n .option(\"--no-cache\", \"Disable caching for type generation\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit generate-types\n $ appkit generate-types . shared/appkit-types/analytics.d.ts\n $ appkit generate-types . shared/appkit-types/analytics.d.ts my-warehouse-id\n $ appkit generate-types --no-cache`,\n )\n .action(runGenerateTypes);\n"],"mappings":";;;;;;;;AAOA,eAAe,iBACb,SACA,SACA,aACA,SACA;AACA,KAAI;EACF,MAAM,kBAAkB,WAAW,QAAQ,KAAK;EAChD,MAAM,UAAU,SAAS,WAAW;EAEpC,MAAM,UAAU,MAAM,OAAO;EAG7B,MAAM,sBACJ,eAAe,QAAQ,IAAI;AAE7B,MAAI,qBAAqB;GACvB,MAAM,kBACJ,WACA,KAAK,KAAK,QAAQ,KAAK,EAAE,qCAAqC;GAEhE,MAAM,cAAc,KAAK,KAAK,iBAAiB,iBAAiB;AAChE,OAAI,GAAG,WAAW,YAAY,EAAE;AAC9B,UAAM,QAAQ,uBAAuB;KACnC;KACA,SAAS;KACT,aAAa;KACb;KACD,CAAC;AACF,YAAQ,IAAI,0BAA0B,kBAAkB;;QAG1D,SAAQ,MACN,oGACD;EAIH,MAAM,iBAAiB,KAAK,KAC1B,QAAQ,KAAK,EACb,mCACD;AACD,QAAM,QAAQ,qBAAqB;GACjC,SAAS;GACT;GACD,CAAC;AACF,UAAQ,IAAI,4BAA4B,iBAAiB;UAClD,OAAO;AACd,MACE,iBAAiB,SACjB,MAAM,QAAQ,SAAS,qBAAqB,EAC5C;AACA,WAAQ,MACN,+EACD;AACD,WAAQ,MAAM,yDAAyD;AACvE,WAAQ,KAAK,EAAE;;AAEjB,QAAM;;;AAIV,MAAa,uBAAuB,IAAI,QAAQ,iBAAiB,CAC9D,YAAY,6CAA6C,CACzD,SAAS,aAAa,iCAAiC,QAAQ,KAAK,CAAC,CACrE,SACC,aACA,oBACA,KAAK,KAAK,QAAQ,KAAK,EAAE,qCAAqC,CAC/D,CACA,SAAS,iBAAiB,0BAA0B,CACpD,OAAO,cAAc,sCAAsC,CAC3D,YACC,SACA;;;;;sCAMD,CACA,OAAO,iBAAiB"}
@@ -97,7 +97,9 @@ function runLint() {
97
97
  }
98
98
  process.exit(1);
99
99
  }
100
- const lintCommand = new Command("lint").description("Run AST-based linting on TypeScript files").action(runLint);
100
+ const lintCommand = new Command("lint").description("Run AST-based linting on TypeScript files").addHelpText("after", `
101
+ Examples:
102
+ $ appkit lint`).action(runLint);
101
103
 
102
104
  //#endregion
103
105
  export { lintCommand };
@@ -1 +1 @@
1
- {"version":3,"file":"lint.js","names":[],"sources":["../../../src/cli/commands/lint.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Lang, parse } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\n\ninterface Rule {\n id: string;\n pattern: string;\n message: string;\n includeTests?: boolean;\n filter?: (code: string) => boolean;\n}\n\nconst rules: Rule[] = [\n {\n id: \"no-double-type-assertion\",\n pattern: \"$X as unknown as $Y\",\n message:\n \"Avoid double type assertion (as unknown as). Use proper type guards or fix the source type.\",\n },\n {\n id: \"no-as-any\",\n pattern: \"$X as any\",\n message:\n 'Avoid \"as any\" type assertion. Use proper typing or unknown with type guards.',\n includeTests: false, // acceptable in test mocks\n },\n {\n id: \"no-array-index-key\",\n pattern: \"key={$IDX}\",\n message:\n \"Avoid using array index as React key. Use a stable unique identifier.\",\n filter: (code) => /key=\\{(idx|index|i)\\}/.test(code),\n },\n {\n id: \"no-parse-float-without-validation\",\n pattern: \"parseFloat($X).toFixed($Y)\",\n message:\n \"parseFloat can return NaN. Validate input or use toNumber() helper from shared/types.ts.\",\n },\n];\n\nfunction isTestFile(filePath: string): boolean {\n return (\n /\\.(test|spec)\\.(ts|tsx)$/.test(filePath) || filePath.includes(\"/tests/\")\n );\n}\n\nfunction findTsFiles(dir: string, files: string[] = []): string[] {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n if ([\"node_modules\", \"dist\", \"build\", \".git\"].includes(entry.name))\n continue;\n findTsFiles(fullPath, files);\n } else if (entry.isFile() && /\\.(ts|tsx)$/.test(entry.name)) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\ninterface Violation {\n file: string;\n line: number;\n column: number;\n rule: string;\n message: string;\n code: string;\n}\n\nfunction lintFile(filePath: string, rules: Rule[]): Violation[] {\n const violations: Violation[] = [];\n const content = fs.readFileSync(filePath, \"utf-8\");\n const lang = filePath.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const testFile = isTestFile(filePath);\n\n const ast = parse(lang, content);\n const root = ast.root();\n\n for (const rule of rules) {\n // skip rules that don't apply to test files\n if (testFile && rule.includeTests === false) continue;\n\n const matches = root.findAll(rule.pattern);\n\n for (const match of matches) {\n const code = match.text();\n\n if (rule.filter && !rule.filter(code)) continue;\n\n const range = match.range();\n violations.push({\n file: filePath,\n line: range.start.line + 1,\n column: range.start.column + 1,\n rule: rule.id,\n message: rule.message,\n code: code.length > 80 ? `${code.slice(0, 77)}...` : code,\n });\n }\n }\n\n return violations;\n}\n\n/**\n * Lint command implementation\n */\nfunction runLint() {\n const rootDir = process.cwd();\n const files = findTsFiles(rootDir);\n\n console.log(`Scanning ${files.length} TypeScript files...\\n`);\n\n const allViolations: Violation[] = [];\n\n for (const file of files) {\n const violations = lintFile(file, rules);\n allViolations.push(...violations);\n }\n\n if (allViolations.length === 0) {\n console.log(\"No ast-grep lint violations found.\");\n process.exit(0);\n }\n\n console.log(`Found ${allViolations.length} violation(s):\\n`);\n\n for (const v of allViolations) {\n const relPath = path.relative(rootDir, v.file);\n console.log(`${relPath}:${v.line}:${v.column}`);\n console.log(` ${v.rule}: ${v.message}`);\n console.log(` > ${v.code}\\n`);\n }\n\n process.exit(1);\n}\n\nexport const lintCommand = new Command(\"lint\")\n .description(\"Run AST-based linting on TypeScript files\")\n .action(runLint);\n"],"mappings":";;;;;;AAaA,MAAM,QAAgB;CACpB;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACH;CACD;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACF,cAAc;EACf;CACD;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACF,SAAS,SAAS,wBAAwB,KAAK,KAAK;EACrD;CACD;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACH;CACF;AAED,SAAS,WAAW,UAA2B;AAC7C,QACE,2BAA2B,KAAK,SAAS,IAAI,SAAS,SAAS,UAAU;;AAI7E,SAAS,YAAY,KAAa,QAAkB,EAAE,EAAY;CAChE,MAAM,UAAU,GAAG,YAAY,KAAK,EAAE,eAAe,MAAM,CAAC;AAE5D,MAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,WAAW,KAAK,KAAK,KAAK,MAAM,KAAK;AAE3C,MAAI,MAAM,aAAa,EAAE;AACvB,OAAI;IAAC;IAAgB;IAAQ;IAAS;IAAO,CAAC,SAAS,MAAM,KAAK,CAChE;AACF,eAAY,UAAU,MAAM;aACnB,MAAM,QAAQ,IAAI,cAAc,KAAK,MAAM,KAAK,CACzD,OAAM,KAAK,SAAS;;AAIxB,QAAO;;AAYT,SAAS,SAAS,UAAkB,OAA4B;CAC9D,MAAM,aAA0B,EAAE;CAClC,MAAM,UAAU,GAAG,aAAa,UAAU,QAAQ;CAClD,MAAM,OAAO,SAAS,SAAS,OAAO,GAAG,KAAK,MAAM,KAAK;CACzD,MAAM,WAAW,WAAW,SAAS;CAGrC,MAAM,OADM,MAAM,MAAM,QAAQ,CACf,MAAM;AAEvB,MAAK,MAAM,QAAQ,OAAO;AAExB,MAAI,YAAY,KAAK,iBAAiB,MAAO;EAE7C,MAAM,UAAU,KAAK,QAAQ,KAAK,QAAQ;AAE1C,OAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,OAAO,MAAM,MAAM;AAEzB,OAAI,KAAK,UAAU,CAAC,KAAK,OAAO,KAAK,CAAE;GAEvC,MAAM,QAAQ,MAAM,OAAO;AAC3B,cAAW,KAAK;IACd,MAAM;IACN,MAAM,MAAM,MAAM,OAAO;IACzB,QAAQ,MAAM,MAAM,SAAS;IAC7B,MAAM,KAAK;IACX,SAAS,KAAK;IACd,MAAM,KAAK,SAAS,KAAK,GAAG,KAAK,MAAM,GAAG,GAAG,CAAC,OAAO;IACtD,CAAC;;;AAIN,QAAO;;;;;AAMT,SAAS,UAAU;CACjB,MAAM,UAAU,QAAQ,KAAK;CAC7B,MAAM,QAAQ,YAAY,QAAQ;AAElC,SAAQ,IAAI,YAAY,MAAM,OAAO,wBAAwB;CAE7D,MAAM,gBAA6B,EAAE;AAErC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,aAAa,SAAS,MAAM,MAAM;AACxC,gBAAc,KAAK,GAAG,WAAW;;AAGnC,KAAI,cAAc,WAAW,GAAG;AAC9B,UAAQ,IAAI,qCAAqC;AACjD,UAAQ,KAAK,EAAE;;AAGjB,SAAQ,IAAI,SAAS,cAAc,OAAO,kBAAkB;AAE5D,MAAK,MAAM,KAAK,eAAe;EAC7B,MAAM,UAAU,KAAK,SAAS,SAAS,EAAE,KAAK;AAC9C,UAAQ,IAAI,GAAG,QAAQ,GAAG,EAAE,KAAK,GAAG,EAAE,SAAS;AAC/C,UAAQ,IAAI,KAAK,EAAE,KAAK,IAAI,EAAE,UAAU;AACxC,UAAQ,IAAI,OAAO,EAAE,KAAK,IAAI;;AAGhC,SAAQ,KAAK,EAAE;;AAGjB,MAAa,cAAc,IAAI,QAAQ,OAAO,CAC3C,YAAY,4CAA4C,CACxD,OAAO,QAAQ"}
1
+ {"version":3,"file":"lint.js","names":[],"sources":["../../../src/cli/commands/lint.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Lang, parse } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\n\ninterface Rule {\n id: string;\n pattern: string;\n message: string;\n includeTests?: boolean;\n filter?: (code: string) => boolean;\n}\n\nconst rules: Rule[] = [\n {\n id: \"no-double-type-assertion\",\n pattern: \"$X as unknown as $Y\",\n message:\n \"Avoid double type assertion (as unknown as). Use proper type guards or fix the source type.\",\n },\n {\n id: \"no-as-any\",\n pattern: \"$X as any\",\n message:\n 'Avoid \"as any\" type assertion. Use proper typing or unknown with type guards.',\n includeTests: false, // acceptable in test mocks\n },\n {\n id: \"no-array-index-key\",\n pattern: \"key={$IDX}\",\n message:\n \"Avoid using array index as React key. Use a stable unique identifier.\",\n filter: (code) => /key=\\{(idx|index|i)\\}/.test(code),\n },\n {\n id: \"no-parse-float-without-validation\",\n pattern: \"parseFloat($X).toFixed($Y)\",\n message:\n \"parseFloat can return NaN. Validate input or use toNumber() helper from shared/types.ts.\",\n },\n];\n\nfunction isTestFile(filePath: string): boolean {\n return (\n /\\.(test|spec)\\.(ts|tsx)$/.test(filePath) || filePath.includes(\"/tests/\")\n );\n}\n\nfunction findTsFiles(dir: string, files: string[] = []): string[] {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n if ([\"node_modules\", \"dist\", \"build\", \".git\"].includes(entry.name))\n continue;\n findTsFiles(fullPath, files);\n } else if (entry.isFile() && /\\.(ts|tsx)$/.test(entry.name)) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\ninterface Violation {\n file: string;\n line: number;\n column: number;\n rule: string;\n message: string;\n code: string;\n}\n\nfunction lintFile(filePath: string, rules: Rule[]): Violation[] {\n const violations: Violation[] = [];\n const content = fs.readFileSync(filePath, \"utf-8\");\n const lang = filePath.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const testFile = isTestFile(filePath);\n\n const ast = parse(lang, content);\n const root = ast.root();\n\n for (const rule of rules) {\n // skip rules that don't apply to test files\n if (testFile && rule.includeTests === false) continue;\n\n const matches = root.findAll(rule.pattern);\n\n for (const match of matches) {\n const code = match.text();\n\n if (rule.filter && !rule.filter(code)) continue;\n\n const range = match.range();\n violations.push({\n file: filePath,\n line: range.start.line + 1,\n column: range.start.column + 1,\n rule: rule.id,\n message: rule.message,\n code: code.length > 80 ? `${code.slice(0, 77)}...` : code,\n });\n }\n }\n\n return violations;\n}\n\n/**\n * Lint command implementation\n */\nfunction runLint() {\n const rootDir = process.cwd();\n const files = findTsFiles(rootDir);\n\n console.log(`Scanning ${files.length} TypeScript files...\\n`);\n\n const allViolations: Violation[] = [];\n\n for (const file of files) {\n const violations = lintFile(file, rules);\n allViolations.push(...violations);\n }\n\n if (allViolations.length === 0) {\n console.log(\"No ast-grep lint violations found.\");\n process.exit(0);\n }\n\n console.log(`Found ${allViolations.length} violation(s):\\n`);\n\n for (const v of allViolations) {\n const relPath = path.relative(rootDir, v.file);\n console.log(`${relPath}:${v.line}:${v.column}`);\n console.log(` ${v.rule}: ${v.message}`);\n console.log(` > ${v.code}\\n`);\n }\n\n process.exit(1);\n}\n\nexport const lintCommand = new Command(\"lint\")\n .description(\"Run AST-based linting on TypeScript files\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit lint`,\n )\n .action(runLint);\n"],"mappings":";;;;;;AAaA,MAAM,QAAgB;CACpB;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACH;CACD;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACF,cAAc;EACf;CACD;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACF,SAAS,SAAS,wBAAwB,KAAK,KAAK;EACrD;CACD;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACH;CACF;AAED,SAAS,WAAW,UAA2B;AAC7C,QACE,2BAA2B,KAAK,SAAS,IAAI,SAAS,SAAS,UAAU;;AAI7E,SAAS,YAAY,KAAa,QAAkB,EAAE,EAAY;CAChE,MAAM,UAAU,GAAG,YAAY,KAAK,EAAE,eAAe,MAAM,CAAC;AAE5D,MAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,WAAW,KAAK,KAAK,KAAK,MAAM,KAAK;AAE3C,MAAI,MAAM,aAAa,EAAE;AACvB,OAAI;IAAC;IAAgB;IAAQ;IAAS;IAAO,CAAC,SAAS,MAAM,KAAK,CAChE;AACF,eAAY,UAAU,MAAM;aACnB,MAAM,QAAQ,IAAI,cAAc,KAAK,MAAM,KAAK,CACzD,OAAM,KAAK,SAAS;;AAIxB,QAAO;;AAYT,SAAS,SAAS,UAAkB,OAA4B;CAC9D,MAAM,aAA0B,EAAE;CAClC,MAAM,UAAU,GAAG,aAAa,UAAU,QAAQ;CAClD,MAAM,OAAO,SAAS,SAAS,OAAO,GAAG,KAAK,MAAM,KAAK;CACzD,MAAM,WAAW,WAAW,SAAS;CAGrC,MAAM,OADM,MAAM,MAAM,QAAQ,CACf,MAAM;AAEvB,MAAK,MAAM,QAAQ,OAAO;AAExB,MAAI,YAAY,KAAK,iBAAiB,MAAO;EAE7C,MAAM,UAAU,KAAK,QAAQ,KAAK,QAAQ;AAE1C,OAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,OAAO,MAAM,MAAM;AAEzB,OAAI,KAAK,UAAU,CAAC,KAAK,OAAO,KAAK,CAAE;GAEvC,MAAM,QAAQ,MAAM,OAAO;AAC3B,cAAW,KAAK;IACd,MAAM;IACN,MAAM,MAAM,MAAM,OAAO;IACzB,QAAQ,MAAM,MAAM,SAAS;IAC7B,MAAM,KAAK;IACX,SAAS,KAAK;IACd,MAAM,KAAK,SAAS,KAAK,GAAG,KAAK,MAAM,GAAG,GAAG,CAAC,OAAO;IACtD,CAAC;;;AAIN,QAAO;;;;;AAMT,SAAS,UAAU;CACjB,MAAM,UAAU,QAAQ,KAAK;CAC7B,MAAM,QAAQ,YAAY,QAAQ;AAElC,SAAQ,IAAI,YAAY,MAAM,OAAO,wBAAwB;CAE7D,MAAM,gBAA6B,EAAE;AAErC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,aAAa,SAAS,MAAM,MAAM;AACxC,gBAAc,KAAK,GAAG,WAAW;;AAGnC,KAAI,cAAc,WAAW,GAAG;AAC9B,UAAQ,IAAI,qCAAqC;AACjD,UAAQ,KAAK,EAAE;;AAGjB,SAAQ,IAAI,SAAS,cAAc,OAAO,kBAAkB;AAE5D,MAAK,MAAM,KAAK,eAAe;EAC7B,MAAM,UAAU,KAAK,SAAS,SAAS,EAAE,KAAK;AAC9C,UAAQ,IAAI,GAAG,QAAQ,GAAG,EAAE,KAAK,GAAG,EAAE,SAAS;AAC/C,UAAQ,IAAI,KAAK,EAAE,KAAK,IAAI,EAAE,UAAU;AACxC,UAAQ,IAAI,OAAO,EAAE,KAAK,IAAI;;AAGhC,SAAQ,KAAK,EAAE;;AAGjB,MAAa,cAAc,IAAI,QAAQ,OAAO,CAC3C,YAAY,4CAA4C,CACxD,YACC,SACA;;iBAGD,CACA,OAAO,QAAQ"}
@@ -1,4 +1,4 @@
1
- import { humanizeResourceType } from "../create/resource-defaults.js";
1
+ import { DEFAULT_PERMISSION_BY_TYPE, getDefaultFieldsForType, getValidResourceTypes, humanizeResourceType, resourceKeyFromType } from "../create/resource-defaults.js";
2
2
  import { promptOneResource } from "../create/prompt-resource.js";
3
3
  import { resolveManifestInDir } from "../manifest-resolve.js";
4
4
  import { validateManifest } from "../validate/validate-manifest.js";
@@ -9,13 +9,11 @@ import process from "node:process";
9
9
  import { cancel, intro, outro } from "@clack/prompts";
10
10
 
11
11
  //#region src/cli/commands/plugin/add-resource/add-resource.ts
12
- async function runPluginAddResource(options) {
13
- intro("Add resource to plugin manifest");
14
- const cwd = process.cwd();
15
- const pluginDir = path.resolve(cwd, options.path ?? ".");
12
+ function loadManifest(pluginDir) {
16
13
  const resolved = resolveManifestInDir(pluginDir, { allowJsManifest: true });
17
14
  if (!resolved) {
18
15
  console.error(`No manifest found in ${pluginDir}. This command requires manifest.json (manifest.js cannot be edited in place).`);
16
+ console.error(" appkit plugin add-resource --path <dir-with-manifest.json>");
19
17
  process.exit(1);
20
18
  }
21
19
  if (resolved.type !== "json") {
@@ -23,7 +21,6 @@ async function runPluginAddResource(options) {
23
21
  process.exit(1);
24
22
  }
25
23
  const manifestPath = resolved.path;
26
- let manifest;
27
24
  try {
28
25
  const raw = fs.readFileSync(manifestPath, "utf-8");
29
26
  const parsed = JSON.parse(raw);
@@ -32,11 +29,70 @@ async function runPluginAddResource(options) {
32
29
  console.error("Invalid manifest. Run `appkit plugin validate` for details.");
33
30
  process.exit(1);
34
31
  }
35
- manifest = parsed;
32
+ return {
33
+ manifest: parsed,
34
+ manifestPath
35
+ };
36
36
  } catch (err) {
37
37
  console.error("Failed to read or parse manifest.json:", err instanceof Error ? err.message : err);
38
38
  process.exit(1);
39
39
  }
40
+ }
41
+ function buildEntry(type, opts) {
42
+ const alias = humanizeResourceType(type);
43
+ const isRequired = opts.required !== false;
44
+ let fields = getDefaultFieldsForType(type);
45
+ if (opts.fieldsJson) try {
46
+ const parsed = JSON.parse(opts.fieldsJson);
47
+ fields = {
48
+ ...fields,
49
+ ...parsed
50
+ };
51
+ } catch {
52
+ console.error("Error: --fields-json must be valid JSON.");
53
+ console.error(" Example: --fields-json '{\"id\":{\"env\":\"MY_WAREHOUSE_ID\"}}'");
54
+ process.exit(1);
55
+ }
56
+ return {
57
+ entry: {
58
+ type,
59
+ alias,
60
+ resourceKey: opts.resourceKey ?? resourceKeyFromType(type),
61
+ description: opts.description || `${isRequired ? "Required" : "Optional"} for ${alias} functionality.`,
62
+ permission: opts.permission ?? DEFAULT_PERMISSION_BY_TYPE[type] ?? "CAN_VIEW",
63
+ fields
64
+ },
65
+ isRequired
66
+ };
67
+ }
68
+ function runNonInteractive(opts) {
69
+ const cwd = process.cwd();
70
+ const loaded = loadManifest(path.resolve(cwd, opts.path ?? "."));
71
+ if (!loaded) return;
72
+ const { manifest, manifestPath } = loaded;
73
+ const type = opts.type;
74
+ const validTypes = getValidResourceTypes();
75
+ if (!validTypes.includes(type)) {
76
+ console.error(`Error: Unknown resource type "${type}".`);
77
+ console.error(` Valid types: ${validTypes.join(", ")}`);
78
+ process.exit(1);
79
+ }
80
+ const { entry, isRequired } = buildEntry(type, opts);
81
+ if (isRequired) manifest.resources.required.push(entry);
82
+ else manifest.resources.optional.push(entry);
83
+ if (opts.dryRun) {
84
+ console.log(JSON.stringify(manifest, null, 2));
85
+ return;
86
+ }
87
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
88
+ console.log(`Added ${entry.alias} as ${isRequired ? "required" : "optional"} to ${path.relative(cwd, manifestPath)}`);
89
+ }
90
+ async function runInteractive(opts) {
91
+ intro("Add resource to plugin manifest");
92
+ const cwd = process.cwd();
93
+ const loaded = loadManifest(path.resolve(cwd, opts.path ?? "."));
94
+ if (!loaded) return;
95
+ const { manifest, manifestPath } = loaded;
40
96
  const spec = await promptOneResource();
41
97
  if (!spec) {
42
98
  cancel("Cancelled.");
@@ -57,7 +113,16 @@ async function runPluginAddResource(options) {
57
113
  outro("Resource added.");
58
114
  console.log(`\nAdded ${alias} as ${spec.required ? "required" : "optional"} to ${path.relative(cwd, manifestPath)}`);
59
115
  }
60
- const pluginAddResourceCommand = new Command("add-resource").description("Add a resource requirement to an existing plugin manifest (interactive). Overwrites manifest.json in place.").option("-p, --path <dir>", "Plugin directory containing manifest.json, which will be edited in place (default: .)").action((opts) => runPluginAddResource(opts).catch((err) => {
116
+ async function runPluginAddResource(opts) {
117
+ if (opts.type) runNonInteractive(opts);
118
+ else await runInteractive(opts);
119
+ }
120
+ const pluginAddResourceCommand = new Command("add-resource").description("Add a resource requirement to an existing plugin manifest. Overwrites manifest.json in place.").option("-p, --path <dir>", "Plugin directory containing manifest.json (default: .)").option("-t, --type <resource_type>", "Resource type (e.g. sql_warehouse, volume). Enables non-interactive mode.").option("--required", "Mark resource as required (default: true)", true).option("--no-required", "Mark resource as optional").option("--resource-key <key>", "Resource key (default: derived from type)").option("--description <text>", "Description of the resource requirement").option("--permission <perm>", "Permission level (default: from schema)").option("--fields-json <json>", "JSON object overriding field env vars (e.g. '{\"id\":{\"env\":\"MY_WAREHOUSE_ID\"}}')").option("--dry-run", "Preview the updated manifest without writing").addHelpText("after", `
121
+ Examples:
122
+ $ appkit plugin add-resource
123
+ $ appkit plugin add-resource --path plugins/my-plugin --type sql_warehouse
124
+ $ appkit plugin add-resource --path plugins/my-plugin --type volume --no-required --dry-run
125
+ $ appkit plugin add-resource --type sql_warehouse --fields-json '{"id":{"env":"MY_WAREHOUSE_ID"}}'`).action((opts) => runPluginAddResource(opts).catch((err) => {
61
126
  console.error(err);
62
127
  process.exit(1);
63
128
  }));
@@ -1 +1 @@
1
- {"version":3,"file":"add-resource.js","names":[],"sources":["../../../../../src/cli/commands/plugin/add-resource/add-resource.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport { cancel, intro, outro } from \"@clack/prompts\";\nimport { Command } from \"commander\";\nimport { promptOneResource } from \"../create/prompt-resource\";\nimport { humanizeResourceType } from \"../create/resource-defaults\";\nimport { resolveManifestInDir } from \"../manifest-resolve\";\nimport type { PluginManifest, ResourceRequirement } from \"../manifest-types\";\nimport { validateManifest } from \"../validate/validate-manifest\";\n\n/** Extended manifest type that preserves extra JSON fields (e.g. $schema, author, version) for round-trip writes. */\ninterface ManifestWithExtras extends PluginManifest {\n [key: string]: unknown;\n}\n\nasync function runPluginAddResource(options: { path?: string }): Promise<void> {\n intro(\"Add resource to plugin manifest\");\n\n const cwd = process.cwd();\n const pluginDir = path.resolve(cwd, options.path ?? \".\");\n const resolved = resolveManifestInDir(pluginDir, { allowJsManifest: true });\n\n if (!resolved) {\n console.error(\n `No manifest found in ${pluginDir}. This command requires manifest.json (manifest.js cannot be edited in place).`,\n );\n process.exit(1);\n }\n\n if (resolved.type !== \"json\") {\n console.error(\n `Editable manifest not found. add-resource only supports plugin directories that contain manifest.json (found ${path.basename(resolved.path)}).`,\n );\n process.exit(1);\n }\n\n const manifestPath = resolved.path;\n\n let manifest: ManifestWithExtras;\n try {\n const raw = fs.readFileSync(manifestPath, \"utf-8\");\n const parsed = JSON.parse(raw) as unknown;\n const result = validateManifest(parsed);\n if (!result.valid || !result.manifest) {\n console.error(\n \"Invalid manifest. Run `appkit plugin validate` for details.\",\n );\n process.exit(1);\n }\n manifest = parsed as ManifestWithExtras;\n } catch (err) {\n console.error(\n \"Failed to read or parse manifest.json:\",\n err instanceof Error ? err.message : err,\n );\n process.exit(1);\n }\n\n const spec = await promptOneResource();\n if (!spec) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const alias = humanizeResourceType(spec.type);\n const entry: ResourceRequirement = {\n // Safe cast: spec.type comes from RESOURCE_TYPE_OPTIONS which reads values\n // from the same JSON schema that generates the ResourceType union.\n type: spec.type as ResourceRequirement[\"type\"],\n alias,\n resourceKey: spec.resourceKey,\n description: spec.description || `Required for ${alias} functionality.`,\n permission: spec.permission,\n fields: spec.fields,\n };\n\n if (spec.required) {\n manifest.resources.required.push(entry);\n } else {\n manifest.resources.optional.push(entry);\n }\n\n fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\\n`);\n\n outro(\"Resource added.\");\n console.log(\n `\\nAdded ${alias} as ${spec.required ? \"required\" : \"optional\"} to ${path.relative(cwd, manifestPath)}`,\n );\n}\n\nexport const pluginAddResourceCommand = new Command(\"add-resource\")\n .description(\n \"Add a resource requirement to an existing plugin manifest (interactive). Overwrites manifest.json in place.\",\n )\n .option(\n \"-p, --path <dir>\",\n \"Plugin directory containing manifest.json, which will be edited in place (default: .)\",\n )\n .action((opts) =>\n runPluginAddResource(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":";;;;;;;;;;;AAgBA,eAAe,qBAAqB,SAA2C;AAC7E,OAAM,kCAAkC;CAExC,MAAM,MAAM,QAAQ,KAAK;CACzB,MAAM,YAAY,KAAK,QAAQ,KAAK,QAAQ,QAAQ,IAAI;CACxD,MAAM,WAAW,qBAAqB,WAAW,EAAE,iBAAiB,MAAM,CAAC;AAE3E,KAAI,CAAC,UAAU;AACb,UAAQ,MACN,wBAAwB,UAAU,gFACnC;AACD,UAAQ,KAAK,EAAE;;AAGjB,KAAI,SAAS,SAAS,QAAQ;AAC5B,UAAQ,MACN,gHAAgH,KAAK,SAAS,SAAS,KAAK,CAAC,IAC9I;AACD,UAAQ,KAAK,EAAE;;CAGjB,MAAM,eAAe,SAAS;CAE9B,IAAI;AACJ,KAAI;EACF,MAAM,MAAM,GAAG,aAAa,cAAc,QAAQ;EAClD,MAAM,SAAS,KAAK,MAAM,IAAI;EAC9B,MAAM,SAAS,iBAAiB,OAAO;AACvC,MAAI,CAAC,OAAO,SAAS,CAAC,OAAO,UAAU;AACrC,WAAQ,MACN,8DACD;AACD,WAAQ,KAAK,EAAE;;AAEjB,aAAW;UACJ,KAAK;AACZ,UAAQ,MACN,0CACA,eAAe,QAAQ,IAAI,UAAU,IACtC;AACD,UAAQ,KAAK,EAAE;;CAGjB,MAAM,OAAO,MAAM,mBAAmB;AACtC,KAAI,CAAC,MAAM;AACT,SAAO,aAAa;AACpB,UAAQ,KAAK,EAAE;;CAGjB,MAAM,QAAQ,qBAAqB,KAAK,KAAK;CAC7C,MAAM,QAA6B;EAGjC,MAAM,KAAK;EACX;EACA,aAAa,KAAK;EAClB,aAAa,KAAK,eAAe,gBAAgB,MAAM;EACvD,YAAY,KAAK;EACjB,QAAQ,KAAK;EACd;AAED,KAAI,KAAK,SACP,UAAS,UAAU,SAAS,KAAK,MAAM;KAEvC,UAAS,UAAU,SAAS,KAAK,MAAM;AAGzC,IAAG,cAAc,cAAc,GAAG,KAAK,UAAU,UAAU,MAAM,EAAE,CAAC,IAAI;AAExE,OAAM,kBAAkB;AACxB,SAAQ,IACN,WAAW,MAAM,MAAM,KAAK,WAAW,aAAa,WAAW,MAAM,KAAK,SAAS,KAAK,aAAa,GACtG;;AAGH,MAAa,2BAA2B,IAAI,QAAQ,eAAe,CAChE,YACC,8GACD,CACA,OACC,oBACA,wFACD,CACA,QAAQ,SACP,qBAAqB,KAAK,CAAC,OAAO,QAAQ;AACxC,SAAQ,MAAM,IAAI;AAClB,SAAQ,KAAK,EAAE;EACf,CACH"}
1
+ {"version":3,"file":"add-resource.js","names":[],"sources":["../../../../../src/cli/commands/plugin/add-resource/add-resource.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport { cancel, intro, outro } from \"@clack/prompts\";\nimport { Command } from \"commander\";\nimport { promptOneResource } from \"../create/prompt-resource\";\nimport {\n DEFAULT_PERMISSION_BY_TYPE,\n getDefaultFieldsForType,\n getValidResourceTypes,\n humanizeResourceType,\n resourceKeyFromType,\n} from \"../create/resource-defaults\";\nimport { resolveManifestInDir } from \"../manifest-resolve\";\nimport type { PluginManifest, ResourceRequirement } from \"../manifest-types\";\nimport { validateManifest } from \"../validate/validate-manifest\";\n\n/** Extended manifest type that preserves extra JSON fields (e.g. $schema, author, version) for round-trip writes. */\ninterface ManifestWithExtras extends PluginManifest {\n [key: string]: unknown;\n}\n\ninterface AddResourceOptions {\n path?: string;\n type?: string;\n required?: boolean;\n resourceKey?: string;\n description?: string;\n permission?: string;\n fieldsJson?: string;\n dryRun?: boolean;\n}\n\nfunction loadManifest(\n pluginDir: string,\n): { manifest: ManifestWithExtras; manifestPath: string } | null {\n const resolved = resolveManifestInDir(pluginDir, { allowJsManifest: true });\n\n if (!resolved) {\n console.error(\n `No manifest found in ${pluginDir}. This command requires manifest.json (manifest.js cannot be edited in place).`,\n );\n console.error(\n \" appkit plugin add-resource --path <dir-with-manifest.json>\",\n );\n process.exit(1);\n }\n\n if (resolved.type !== \"json\") {\n console.error(\n `Editable manifest not found. add-resource only supports plugin directories that contain manifest.json (found ${path.basename(resolved.path)}).`,\n );\n process.exit(1);\n }\n\n const manifestPath = resolved.path;\n\n try {\n const raw = fs.readFileSync(manifestPath, \"utf-8\");\n const parsed = JSON.parse(raw) as unknown;\n const result = validateManifest(parsed);\n if (!result.valid || !result.manifest) {\n console.error(\n \"Invalid manifest. Run `appkit plugin validate` for details.\",\n );\n process.exit(1);\n }\n return { manifest: parsed as ManifestWithExtras, manifestPath };\n } catch (err) {\n console.error(\n \"Failed to read or parse manifest.json:\",\n err instanceof Error ? err.message : err,\n );\n process.exit(1);\n }\n}\n\nfunction buildEntry(\n type: string,\n opts: AddResourceOptions,\n): { entry: ResourceRequirement; isRequired: boolean } {\n const alias = humanizeResourceType(type);\n const isRequired = opts.required !== false;\n\n let fields = getDefaultFieldsForType(type);\n if (opts.fieldsJson) {\n try {\n const parsed = JSON.parse(opts.fieldsJson) as Record<\n string,\n { env: string; description?: string }\n >;\n fields = { ...fields, ...parsed };\n } catch {\n console.error(\"Error: --fields-json must be valid JSON.\");\n console.error(\n ' Example: --fields-json \\'{\"id\":{\"env\":\"MY_WAREHOUSE_ID\"}}\\'',\n );\n process.exit(1);\n }\n }\n\n const entry: ResourceRequirement = {\n type: type as ResourceRequirement[\"type\"],\n alias,\n resourceKey: opts.resourceKey ?? resourceKeyFromType(type),\n description:\n opts.description ||\n `${isRequired ? \"Required\" : \"Optional\"} for ${alias} functionality.`,\n permission:\n opts.permission ?? DEFAULT_PERMISSION_BY_TYPE[type] ?? \"CAN_VIEW\",\n fields,\n };\n\n return { entry, isRequired };\n}\n\nfunction runNonInteractive(opts: AddResourceOptions): void {\n const cwd = process.cwd();\n const pluginDir = path.resolve(cwd, opts.path ?? \".\");\n const loaded = loadManifest(pluginDir);\n if (!loaded) return;\n const { manifest, manifestPath } = loaded;\n\n const type = opts.type as string;\n const validTypes = getValidResourceTypes();\n if (!validTypes.includes(type)) {\n console.error(`Error: Unknown resource type \"${type}\".`);\n console.error(` Valid types: ${validTypes.join(\", \")}`);\n process.exit(1);\n }\n const { entry, isRequired } = buildEntry(type, opts);\n\n if (isRequired) {\n manifest.resources.required.push(entry);\n } else {\n manifest.resources.optional.push(entry);\n }\n\n if (opts.dryRun) {\n console.log(JSON.stringify(manifest, null, 2));\n return;\n }\n\n fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\\n`);\n console.log(\n `Added ${entry.alias} as ${isRequired ? \"required\" : \"optional\"} to ${path.relative(cwd, manifestPath)}`,\n );\n}\n\nasync function runInteractive(opts: AddResourceOptions): Promise<void> {\n intro(\"Add resource to plugin manifest\");\n\n const cwd = process.cwd();\n const pluginDir = path.resolve(cwd, opts.path ?? \".\");\n const loaded = loadManifest(pluginDir);\n if (!loaded) return;\n const { manifest, manifestPath } = loaded;\n\n const spec = await promptOneResource();\n if (!spec) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const alias = humanizeResourceType(spec.type);\n const entry: ResourceRequirement = {\n type: spec.type as ResourceRequirement[\"type\"],\n alias,\n resourceKey: spec.resourceKey,\n description: spec.description || `Required for ${alias} functionality.`,\n permission: spec.permission,\n fields: spec.fields,\n };\n\n if (spec.required) {\n manifest.resources.required.push(entry);\n } else {\n manifest.resources.optional.push(entry);\n }\n\n fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\\n`);\n\n outro(\"Resource added.\");\n console.log(\n `\\nAdded ${alias} as ${spec.required ? \"required\" : \"optional\"} to ${path.relative(cwd, manifestPath)}`,\n );\n}\n\nasync function runPluginAddResource(opts: AddResourceOptions): Promise<void> {\n if (opts.type) {\n runNonInteractive(opts);\n } else {\n await runInteractive(opts);\n }\n}\n\nexport const pluginAddResourceCommand = new Command(\"add-resource\")\n .description(\n \"Add a resource requirement to an existing plugin manifest. Overwrites manifest.json in place.\",\n )\n .option(\n \"-p, --path <dir>\",\n \"Plugin directory containing manifest.json (default: .)\",\n )\n .option(\n \"-t, --type <resource_type>\",\n \"Resource type (e.g. sql_warehouse, volume). Enables non-interactive mode.\",\n )\n .option(\"--required\", \"Mark resource as required (default: true)\", true)\n .option(\"--no-required\", \"Mark resource as optional\")\n .option(\"--resource-key <key>\", \"Resource key (default: derived from type)\")\n .option(\"--description <text>\", \"Description of the resource requirement\")\n .option(\"--permission <perm>\", \"Permission level (default: from schema)\")\n .option(\n \"--fields-json <json>\",\n 'JSON object overriding field env vars (e.g. \\'{\"id\":{\"env\":\"MY_WAREHOUSE_ID\"}}\\')',\n )\n .option(\"--dry-run\", \"Preview the updated manifest without writing\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit plugin add-resource\n $ appkit plugin add-resource --path plugins/my-plugin --type sql_warehouse\n $ appkit plugin add-resource --path plugins/my-plugin --type volume --no-required --dry-run\n $ appkit plugin add-resource --type sql_warehouse --fields-json '{\"id\":{\"env\":\"MY_WAREHOUSE_ID\"}}'`,\n )\n .action((opts) =>\n runPluginAddResource(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}
@@ -1,16 +1,150 @@
1
- import { RESOURCE_TYPE_OPTIONS } from "./resource-defaults.js";
1
+ import { DEFAULT_PERMISSION_BY_TYPE, RESOURCE_TYPE_OPTIONS, getDefaultFieldsForType, getValidResourceTypes, humanizeResourceType, resourceKeyFromType } from "./resource-defaults.js";
2
2
  import { promptOneResource } from "./prompt-resource.js";
3
3
  import { resolveTargetDir, scaffoldPlugin } from "./scaffold.js";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
- import { Command } from "commander";
6
+ import { Command, Option } from "commander";
7
7
  import process from "node:process";
8
8
  import { cancel, confirm, intro, isCancel, multiselect, outro, select, spinner, text } from "@clack/prompts";
9
9
 
10
10
  //#region src/cli/commands/plugin/create/create.ts
11
11
  const NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
12
12
  const DEFAULT_VERSION = "0.1.0";
13
- async function runPluginCreate() {
13
+ const VALID_PLACEMENTS = ["in-repo", "isolated"];
14
+ const REQUIRED_FLAGS = [
15
+ "placement",
16
+ "path",
17
+ "name",
18
+ "description"
19
+ ];
20
+ function deriveDisplayName(name) {
21
+ return name.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" ");
22
+ }
23
+ function deriveExportName(name) {
24
+ return name.split("-").map((s, i) => i === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1)).join("");
25
+ }
26
+ function buildResourceFromType(type) {
27
+ return {
28
+ type,
29
+ required: true,
30
+ description: `Required for ${humanizeResourceType(type)} functionality.`,
31
+ resourceKey: resourceKeyFromType(type),
32
+ permission: DEFAULT_PERMISSION_BY_TYPE[type] ?? "CAN_VIEW",
33
+ fields: getDefaultFieldsForType(type)
34
+ };
35
+ }
36
+ function parseResourcesJson(json) {
37
+ let parsed;
38
+ try {
39
+ parsed = JSON.parse(json);
40
+ } catch {
41
+ console.error("Error: --resources-json must be valid JSON.");
42
+ console.error(" Example: --resources-json '[{\"type\":\"sql_warehouse\"}]'");
43
+ process.exit(1);
44
+ }
45
+ if (!Array.isArray(parsed)) {
46
+ console.error("Error: --resources-json must be a JSON array.");
47
+ console.error(" Example: --resources-json '[{\"type\":\"sql_warehouse\"}]'");
48
+ process.exit(1);
49
+ }
50
+ return parsed.map((entry, i) => {
51
+ if (entry == null || typeof entry !== "object") {
52
+ console.error(`Error: --resources-json entry ${i} is not an object.`);
53
+ process.exit(1);
54
+ }
55
+ if (!entry.type || typeof entry.type !== "string") {
56
+ console.error(`Error: --resources-json entry ${i} missing required "type" field.`);
57
+ process.exit(1);
58
+ }
59
+ validateResourceType(entry.type);
60
+ const defaults = buildResourceFromType(entry.type);
61
+ return {
62
+ type: entry.type,
63
+ required: entry.required ?? defaults.required,
64
+ description: entry.description ?? defaults.description,
65
+ resourceKey: entry.resourceKey ?? defaults.resourceKey,
66
+ permission: entry.permission ?? defaults.permission,
67
+ fields: entry.fields ?? defaults.fields
68
+ };
69
+ });
70
+ }
71
+ function validateResourceType(type) {
72
+ const validTypes = getValidResourceTypes();
73
+ if (!validTypes.includes(type)) {
74
+ console.error(`Error: Unknown resource type "${type}".`);
75
+ console.error(` Valid types: ${validTypes.join(", ")}`);
76
+ process.exit(1);
77
+ }
78
+ }
79
+ function parseResourcesShorthand(csv) {
80
+ const types = csv.split(",").map((s) => s.trim()).filter(Boolean);
81
+ for (const t of types) validateResourceType(t);
82
+ return types.map(buildResourceFromType);
83
+ }
84
+ function printNextSteps(answers, targetDir) {
85
+ const relativePath = path.relative(process.cwd(), targetDir);
86
+ const importPath = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
87
+ const exportName = deriveExportName(answers.name);
88
+ console.log("\nNext steps:\n");
89
+ if (answers.placement === "in-repo") {
90
+ console.log(` 1. Import and register in your server:`);
91
+ console.log(` import { ${exportName} } from "${importPath}";`);
92
+ console.log(` createApp({ plugins: [ ..., ${exportName}() ] });`);
93
+ console.log(` 2. Run \`npx appkit plugin sync --write\` to update appkit.plugins.json.\n`);
94
+ } else {
95
+ console.log(` 1. cd into the new package and install dependencies:`);
96
+ console.log(` cd ${answers.targetPath} && pnpm install`);
97
+ console.log(` 2. Build: pnpm build`);
98
+ console.log(` 3. In your app: pnpm add ./${answers.targetPath} @databricks/appkit`);
99
+ console.log(` 4. Import and register: import { ${exportName} } from "<package-name>";\n`);
100
+ }
101
+ }
102
+ function runNonInteractive(opts) {
103
+ const missing = REQUIRED_FLAGS.filter((f) => !opts[f]);
104
+ if (missing.length > 0) {
105
+ console.error(`Error: Non-interactive mode requires: ${REQUIRED_FLAGS.map((f) => `--${f}`).join(", ")}`);
106
+ console.error(`Missing: ${missing.map((f) => `--${f}`).join(", ")}`);
107
+ console.error(" appkit plugin create --placement in-repo --path plugins/my-plugin --name my-plugin --description \"Does X\"");
108
+ process.exit(1);
109
+ }
110
+ const placement = opts.placement;
111
+ if (!VALID_PLACEMENTS.includes(placement)) {
112
+ console.error(`Error: --placement must be one of: ${VALID_PLACEMENTS.join(", ")}`);
113
+ process.exit(1);
114
+ }
115
+ const targetPath = opts.path.trim();
116
+ if (placement === "in-repo" && (path.isAbsolute(targetPath) || targetPath.startsWith(".."))) {
117
+ console.error("Error: --path must be a relative path under the current directory for in-repo plugins.");
118
+ process.exit(1);
119
+ }
120
+ const name = opts.name;
121
+ if (!NAME_PATTERN.test(name)) {
122
+ console.error("Error: --name must be lowercase, start with a letter, and use only letters, numbers, and hyphens.");
123
+ process.exit(1);
124
+ }
125
+ let resources = [];
126
+ if (opts.resourcesJson) resources = parseResourcesJson(opts.resourcesJson);
127
+ else if (opts.resources) resources = parseResourcesShorthand(opts.resources);
128
+ const answers = {
129
+ placement,
130
+ targetPath,
131
+ name: name.trim(),
132
+ displayName: opts.displayName?.trim() || deriveDisplayName(name),
133
+ description: opts.description.trim(),
134
+ resources,
135
+ version: DEFAULT_VERSION
136
+ };
137
+ const targetDir = resolveTargetDir(process.cwd(), answers);
138
+ if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0 && !opts.force) {
139
+ console.error(`Error: Directory ${answers.targetPath} already exists and is not empty.`);
140
+ console.error(" Use --force to overwrite.");
141
+ process.exit(1);
142
+ }
143
+ scaffoldPlugin(targetDir, answers, { isolated: placement === "isolated" });
144
+ console.log(`Plugin "${answers.name}" created at ${path.relative(process.cwd(), targetDir)}`);
145
+ printNextSteps(answers, targetDir);
146
+ }
147
+ async function runInteractive() {
14
148
  intro("Create a new AppKit plugin");
15
149
  try {
16
150
  const placement = await select({
@@ -133,29 +267,39 @@ async function runPluginCreate() {
133
267
  s.stop("Failed.");
134
268
  throw err;
135
269
  }
136
- const relativePath = path.relative(process.cwd(), targetDir);
137
- const importPath = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
138
- const exportName = answers.name.split("-").map((s, i) => i === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1)).join("");
139
270
  outro("Plugin created successfully.");
140
- console.log("\nNext steps:\n");
141
- if (placement === "in-repo") {
142
- console.log(` 1. Import and register in your server:`);
143
- console.log(` import { ${exportName} } from "${importPath}";`);
144
- console.log(` createApp({ plugins: [ ..., ${exportName}() ] });`);
145
- console.log(` 2. Run \`npx appkit plugin sync --write\` to update appkit.plugins.json.\n`);
146
- } else {
147
- console.log(` 1. cd into the new package and install dependencies:`);
148
- console.log(` cd ${answers.targetPath} && pnpm install`);
149
- console.log(` 2. Build: pnpm build`);
150
- console.log(` 3. In your app: pnpm add ./${answers.targetPath} @databricks/appkit`);
151
- console.log(` 4. Import and register: import { ${exportName} } from "<package-name>";\n`);
152
- }
271
+ printNextSteps(answers, targetDir);
153
272
  } catch (err) {
154
273
  console.error(err);
155
274
  process.exit(1);
156
275
  }
157
276
  }
158
- const pluginCreateCommand = new Command("create").description("Scaffold a new AppKit plugin (interactive)").action(runPluginCreate);
277
+ const OPTIONAL_FLAGS = [
278
+ "displayName",
279
+ "resources",
280
+ "resourcesJson",
281
+ "force"
282
+ ];
283
+ async function runPluginCreate(opts) {
284
+ if (REQUIRED_FLAGS.some((f) => opts[f] !== void 0)) runNonInteractive(opts);
285
+ else {
286
+ if (OPTIONAL_FLAGS.some((f) => opts[f] !== void 0 && opts[f] !== false)) {
287
+ console.error(`Error: Non-interactive mode requires: ${REQUIRED_FLAGS.map((f) => `--${f}`).join(", ")}`);
288
+ console.error(" appkit plugin create --placement in-repo --path plugins/my-plugin --name my-plugin --description \"Does X\"");
289
+ process.exit(1);
290
+ }
291
+ await runInteractive();
292
+ }
293
+ }
294
+ const pluginCreateCommand = new Command("create").description("Scaffold a new AppKit plugin").option("--placement <type>", "Where the plugin lives (in-repo, isolated)").option("--path <dir>", "Target directory for the plugin").option("--name <id>", "Plugin name (lowercase, hyphens allowed)").option("--display-name <name>", "Human-readable display name").option("--description <text>", "Short description of the plugin").addOption(new Option("--resources <types>", "Comma-separated resource types (e.g. sql_warehouse,volume)").conflicts("resourcesJson")).addOption(new Option("--resources-json <json>", "JSON array of resource specs (e.g. '[{\"type\":\"sql_warehouse\"}]')").conflicts("resources")).option("-f, --force", "Overwrite existing directory without confirmation").addHelpText("after", `
295
+ Examples:
296
+ $ appkit plugin create
297
+ $ appkit plugin create --placement in-repo --path plugins/my-plugin --name my-plugin --description "Does X"
298
+ $ appkit plugin create --placement in-repo --path plugins/my-plugin --name my-plugin --description "Does X" --resources sql_warehouse,volume --force
299
+ $ appkit plugin create --placement isolated --path appkit-plugin-ml --name ml --description "ML" --resources-json '[{"type":"serving_endpoint"}]'`).action((opts) => runPluginCreate(opts).catch((err) => {
300
+ console.error(err);
301
+ process.exit(1);
302
+ }));
159
303
 
160
304
  //#endregion
161
305
  export { pluginCreateCommand };