@databricks/appkit-ui 0.23.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.
- package/CLAUDE.md +1 -0
- package/dist/cli/commands/docs.js +7 -1
- package/dist/cli/commands/docs.js.map +1 -1
- package/dist/cli/commands/generate-types.js +20 -10
- package/dist/cli/commands/generate-types.js.map +1 -1
- package/dist/cli/commands/lint.js +3 -1
- package/dist/cli/commands/lint.js.map +1 -1
- package/dist/cli/commands/plugin/add-resource/add-resource.js +73 -8
- package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
- package/dist/cli/commands/plugin/create/create.js +164 -20
- package/dist/cli/commands/plugin/create/create.js.map +1 -1
- package/dist/cli/commands/plugin/create/resource-defaults.js +5 -1
- package/dist/cli/commands/plugin/create/resource-defaults.js.map +1 -1
- package/dist/cli/commands/plugin/index.js +7 -1
- package/dist/cli/commands/plugin/index.js.map +1 -1
- package/dist/cli/commands/plugin/list/list.js +7 -1
- package/dist/cli/commands/plugin/list/list.js.map +1 -1
- package/dist/cli/commands/plugin/sync/sync.js +27 -14
- package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
- package/dist/cli/commands/plugin/validate/validate.js +39 -9
- package/dist/cli/commands/plugin/validate/validate.js.map +1 -1
- package/dist/cli/commands/setup.js +6 -5
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/react/hooks/types.d.ts +1 -1
- package/docs/api/appkit/TypeAlias.ServingFactory.md +9 -5
- package/docs/development/type-generation.md +6 -5
- package/docs/plugins/analytics.md +1 -1
- package/docs/plugins/custom-plugins.md +4 -0
- package/docs/plugins/plugin-management.md +22 -6
- package/docs/plugins/vector-search.md +247 -0
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -52,6 +52,7 @@ npx @databricks/appkit docs <query>
|
|
|
52
52
|
- [Plugin management](./docs/plugins/plugin-management.md): AppKit includes a CLI for managing plugins. All commands are available under npx @databricks/appkit plugin.
|
|
53
53
|
- [Server plugin](./docs/plugins/server.md): Provides HTTP server capabilities with development and production modes.
|
|
54
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.
|
|
55
56
|
|
|
56
57
|
## appkit API reference [collapsed]
|
|
57
58
|
|
|
@@ -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").
|
|
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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"}
|
|
@@ -13,19 +13,24 @@ async function runGenerateTypes(rootDir, outFile, warehouseId, options) {
|
|
|
13
13
|
const typeGen = await import("@databricks/appkit/type-generator");
|
|
14
14
|
const resolvedWarehouseId = warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;
|
|
15
15
|
if (resolvedWarehouseId) {
|
|
16
|
-
const resolvedOutFile = outFile || path.join(process.cwd(), "
|
|
16
|
+
const resolvedOutFile = outFile || path.join(process.cwd(), "shared/appkit-types/analytics.d.ts");
|
|
17
17
|
const queryFolder = path.join(resolvedRootDir, "config/queries");
|
|
18
|
-
if (fs.existsSync(queryFolder))
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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");
|
|
25
29
|
await typeGen.generateServingTypes({
|
|
26
|
-
outFile:
|
|
30
|
+
outFile: servingOutFile,
|
|
27
31
|
noCache
|
|
28
32
|
});
|
|
33
|
+
console.log(`Generated serving types: ${servingOutFile}`);
|
|
29
34
|
} catch (error) {
|
|
30
35
|
if (error instanceof Error && error.message.includes("Cannot find module")) {
|
|
31
36
|
console.error("Error: The 'generate-types' command is only available in @databricks/appkit.");
|
|
@@ -35,7 +40,12 @@ async function runGenerateTypes(rootDir, outFile, warehouseId, options) {
|
|
|
35
40
|
throw error;
|
|
36
41
|
}
|
|
37
42
|
}
|
|
38
|
-
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(), "
|
|
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);
|
|
39
49
|
|
|
40
50
|
//#endregion
|
|
41
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 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
|
|
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").
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 };
|