@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create.js","names":[],"sources":["../../../../../src/cli/commands/plugin/create/create.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport {\n cancel,\n confirm,\n intro,\n isCancel,\n multiselect,\n outro,\n select,\n spinner,\n text,\n} from \"@clack/prompts\";\nimport { Command } from \"commander\";\nimport { promptOneResource } from \"./prompt-resource\";\nimport { RESOURCE_TYPE_OPTIONS } from \"./resource-defaults\";\nimport { resolveTargetDir, scaffoldPlugin } from \"./scaffold\";\nimport type { CreateAnswers, Placement } from \"./types\";\n\nconst NAME_PATTERN = /^[a-z][a-z0-9-]*$/;\nconst DEFAULT_VERSION = \"0.1.0\";\n\nasync function runPluginCreate(): Promise<void> {\n intro(\"Create a new AppKit plugin\");\n\n try {\n const placement = await select<Placement>({\n message: \"Where should the plugin live?\",\n options: [\n {\n value: \"in-repo\",\n label: \"In this repository (e.g. plugins/my-plugin)\",\n hint: \"folder path\",\n },\n {\n value: \"isolated\",\n label: \"New isolated package\",\n hint: \"full package with package.json\",\n },\n ],\n });\n if (isCancel(placement)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const placementPrompt =\n placement === \"in-repo\"\n ? \"Folder path for the plugin (e.g. plugins/my-feature)\"\n : \"Directory name for the new package (e.g. appkit-plugin-my-feature)\";\n const targetPath = await text({\n message: placementPrompt,\n placeholder:\n placement === \"in-repo\"\n ? \"plugins/my-plugin\"\n : \"appkit-plugin-my-feature\",\n validate(value) {\n if (!value?.trim()) return \"Path is required.\";\n if (\n placement === \"in-repo\" &&\n (path.isAbsolute(value) || value.trim().startsWith(\"..\"))\n ) {\n return \"Use a relative path under the current directory (e.g. plugins/my-plugin).\";\n }\n return undefined;\n },\n });\n if (isCancel(targetPath)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const name = await text({\n message: \"Plugin name (id)\",\n placeholder: \"my-plugin\",\n validate(value) {\n if (!value?.trim()) return \"Name is required.\";\n if (!NAME_PATTERN.test(value as string)) {\n return \"Must be lowercase, start with a letter, and use only letters, numbers, and hyphens.\";\n }\n return undefined;\n },\n });\n if (isCancel(name)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const displayName = await text({\n message: \"Display name\",\n placeholder: \"My Plugin\",\n initialValue: name\n .split(\"-\")\n .map((s) => s.charAt(0).toUpperCase() + s.slice(1))\n .join(\" \"),\n validate(value) {\n if (!value?.trim()) return \"Display name is required.\";\n return undefined;\n },\n });\n if (isCancel(displayName)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const description = await text({\n message: \"Short description\",\n placeholder: \"What does this plugin do?\",\n validate(value) {\n if (!value?.trim()) return \"Description is required.\";\n return undefined;\n },\n });\n if (isCancel(description)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const resourceTypes = await multiselect({\n message: \"Which Databricks resources does this plugin need?\",\n options: RESOURCE_TYPE_OPTIONS.map((o) => ({\n value: o.value,\n label: o.label,\n })),\n required: false,\n });\n if (isCancel(resourceTypes)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const resources: CreateAnswers[\"resources\"] = [];\n for (const type of resourceTypes as string[]) {\n const spec = await promptOneResource({ type });\n if (!spec) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n resources.push({\n type: spec.type,\n required: spec.required,\n description: spec.description,\n resourceKey: spec.resourceKey,\n permission: spec.permission,\n fields: spec.fields,\n });\n }\n\n const answers: CreateAnswers = {\n placement,\n targetPath: (targetPath as string).trim(),\n name: (name as string).trim(),\n displayName: (displayName as string).trim(),\n description: (description as string).trim(),\n resources,\n version: DEFAULT_VERSION,\n };\n\n const targetDir = resolveTargetDir(process.cwd(), answers);\n const dirExists = fs.existsSync(targetDir);\n const hasContent = dirExists && fs.readdirSync(targetDir).length > 0;\n if (hasContent) {\n const overwrite = await confirm({\n message: `Directory ${answers.targetPath} already exists and is not empty. Overwrite?`,\n initialValue: false,\n });\n if (isCancel(overwrite) || !overwrite) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n }\n\n const s = spinner();\n s.start(\"Writing files…\");\n try {\n scaffoldPlugin(targetDir, answers, {\n isolated: placement === \"isolated\",\n });\n s.stop(\"Files written.\");\n } catch (err) {\n s.stop(\"Failed.\");\n throw err;\n }\n\n const relativePath = path.relative(process.cwd(), targetDir);\n const importPath = relativePath.startsWith(\".\")\n ? relativePath\n : `./${relativePath}`;\n const exportName = answers.name\n .split(\"-\")\n .map((s, i) => (i === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1)))\n .join(\"\");\n\n outro(\"Plugin created successfully.\");\n\n console.log(\"\\nNext steps:\\n\");\n if (placement === \"in-repo\") {\n console.log(` 1. Import and register in your server:`);\n console.log(` import { ${exportName} } from \"${importPath}\";`);\n console.log(` createApp({ plugins: [ ..., ${exportName}() ] });`);\n console.log(\n ` 2. Run \\`npx appkit plugin sync --write\\` to update appkit.plugins.json.\\n`,\n );\n } else {\n console.log(` 1. cd into the new package and install dependencies:`);\n console.log(` cd ${answers.targetPath} && pnpm install`);\n console.log(` 2. Build: pnpm build`);\n console.log(\n ` 3. In your app: pnpm add ./${answers.targetPath} @databricks/appkit`,\n );\n console.log(\n ` 4. Import and register: import { ${exportName} } from \"<package-name>\";\\n`,\n );\n }\n } catch (err) {\n console.error(err);\n process.exit(1);\n }\n}\n\nexport const pluginCreateCommand = new Command(\"create\")\n .description(\"Scaffold a new AppKit plugin (interactive)\")\n .action(runPluginCreate);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"create.js","names":[],"sources":["../../../../../src/cli/commands/plugin/create/create.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport {\n cancel,\n confirm,\n intro,\n isCancel,\n multiselect,\n outro,\n select,\n spinner,\n text,\n} from \"@clack/prompts\";\nimport { Command, Option } from \"commander\";\nimport { promptOneResource } from \"./prompt-resource\";\nimport {\n DEFAULT_PERMISSION_BY_TYPE,\n getDefaultFieldsForType,\n getValidResourceTypes,\n humanizeResourceType,\n RESOURCE_TYPE_OPTIONS,\n resourceKeyFromType,\n} from \"./resource-defaults\";\nimport { resolveTargetDir, scaffoldPlugin } from \"./scaffold\";\nimport type { CreateAnswers, Placement, SelectedResource } from \"./types\";\n\nconst NAME_PATTERN = /^[a-z][a-z0-9-]*$/;\nconst DEFAULT_VERSION = \"0.1.0\";\nconst VALID_PLACEMENTS: Placement[] = [\"in-repo\", \"isolated\"];\nconst REQUIRED_FLAGS = [\"placement\", \"path\", \"name\", \"description\"] as const;\n\ninterface CreateOptions {\n placement?: string;\n path?: string;\n name?: string;\n displayName?: string;\n description?: string;\n resources?: string;\n resourcesJson?: string;\n force?: boolean;\n}\n\nfunction deriveDisplayName(name: string): string {\n return name\n .split(\"-\")\n .map((s) => s.charAt(0).toUpperCase() + s.slice(1))\n .join(\" \");\n}\n\nfunction deriveExportName(name: string): string {\n return name\n .split(\"-\")\n .map((s, i) => (i === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1)))\n .join(\"\");\n}\n\nfunction buildResourceFromType(type: string): SelectedResource {\n return {\n type,\n required: true,\n description: `Required for ${humanizeResourceType(type)} functionality.`,\n resourceKey: resourceKeyFromType(type),\n permission: DEFAULT_PERMISSION_BY_TYPE[type] ?? \"CAN_VIEW\",\n fields: getDefaultFieldsForType(type),\n };\n}\n\ninterface JsonResourceEntry {\n type: string;\n required?: boolean;\n description?: string;\n resourceKey?: string;\n permission?: string;\n fields?: Record<string, { env: string; description?: string }>;\n}\n\nfunction parseResourcesJson(json: string): SelectedResource[] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n console.error(\"Error: --resources-json must be valid JSON.\");\n console.error(' Example: --resources-json \\'[{\"type\":\"sql_warehouse\"}]\\'');\n process.exit(1);\n }\n\n if (!Array.isArray(parsed)) {\n console.error(\"Error: --resources-json must be a JSON array.\");\n console.error(' Example: --resources-json \\'[{\"type\":\"sql_warehouse\"}]\\'');\n process.exit(1);\n }\n\n return (parsed as JsonResourceEntry[]).map((entry, i) => {\n if (entry == null || typeof entry !== \"object\") {\n console.error(`Error: --resources-json entry ${i} is not an object.`);\n process.exit(1);\n }\n if (!entry.type || typeof entry.type !== \"string\") {\n console.error(\n `Error: --resources-json entry ${i} missing required \"type\" field.`,\n );\n process.exit(1);\n }\n validateResourceType(entry.type);\n const defaults = buildResourceFromType(entry.type);\n return {\n type: entry.type,\n required: entry.required ?? defaults.required,\n description: entry.description ?? defaults.description,\n resourceKey: entry.resourceKey ?? defaults.resourceKey,\n permission: entry.permission ?? defaults.permission,\n fields: entry.fields ?? defaults.fields,\n };\n });\n}\n\nfunction validateResourceType(type: string): void {\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}\n\nfunction parseResourcesShorthand(csv: string): SelectedResource[] {\n const types = csv\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n for (const t of types) validateResourceType(t);\n return types.map(buildResourceFromType);\n}\n\nfunction printNextSteps(answers: CreateAnswers, targetDir: string): void {\n const relativePath = path.relative(process.cwd(), targetDir);\n const importPath = relativePath.startsWith(\".\")\n ? relativePath\n : `./${relativePath}`;\n const exportName = deriveExportName(answers.name);\n\n console.log(\"\\nNext steps:\\n\");\n if (answers.placement === \"in-repo\") {\n console.log(` 1. Import and register in your server:`);\n console.log(` import { ${exportName} } from \"${importPath}\";`);\n console.log(` createApp({ plugins: [ ..., ${exportName}() ] });`);\n console.log(\n ` 2. Run \\`npx appkit plugin sync --write\\` to update appkit.plugins.json.\\n`,\n );\n } else {\n console.log(` 1. cd into the new package and install dependencies:`);\n console.log(` cd ${answers.targetPath} && pnpm install`);\n console.log(` 2. Build: pnpm build`);\n console.log(\n ` 3. In your app: pnpm add ./${answers.targetPath} @databricks/appkit`,\n );\n console.log(\n ` 4. Import and register: import { ${exportName} } from \"<package-name>\";\\n`,\n );\n }\n}\n\nfunction runNonInteractive(opts: CreateOptions): void {\n const missing = REQUIRED_FLAGS.filter((f) => !opts[f]);\n if (missing.length > 0) {\n console.error(\n `Error: Non-interactive mode requires: ${REQUIRED_FLAGS.map((f) => `--${f}`).join(\", \")}`,\n );\n console.error(`Missing: ${missing.map((f) => `--${f}`).join(\", \")}`);\n console.error(\n ' appkit plugin create --placement in-repo --path plugins/my-plugin --name my-plugin --description \"Does X\"',\n );\n process.exit(1);\n }\n\n const placement = opts.placement as Placement;\n if (!VALID_PLACEMENTS.includes(placement)) {\n console.error(\n `Error: --placement must be one of: ${VALID_PLACEMENTS.join(\", \")}`,\n );\n process.exit(1);\n }\n\n const targetPath = (opts.path as string).trim();\n if (\n placement === \"in-repo\" &&\n (path.isAbsolute(targetPath) || targetPath.startsWith(\"..\"))\n ) {\n console.error(\n \"Error: --path must be a relative path under the current directory for in-repo plugins.\",\n );\n process.exit(1);\n }\n\n const name = opts.name as string;\n if (!NAME_PATTERN.test(name)) {\n console.error(\n \"Error: --name must be lowercase, start with a letter, and use only letters, numbers, and hyphens.\",\n );\n process.exit(1);\n }\n\n let resources: SelectedResource[] = [];\n if (opts.resourcesJson) {\n resources = parseResourcesJson(opts.resourcesJson);\n } else if (opts.resources) {\n resources = parseResourcesShorthand(opts.resources);\n }\n\n const answers: CreateAnswers = {\n placement,\n targetPath,\n name: name.trim(),\n displayName: opts.displayName?.trim() || deriveDisplayName(name),\n description: (opts.description as string).trim(),\n resources,\n version: DEFAULT_VERSION,\n };\n\n const targetDir = resolveTargetDir(process.cwd(), answers);\n const dirExists = fs.existsSync(targetDir);\n const hasContent = dirExists && fs.readdirSync(targetDir).length > 0;\n if (hasContent && !opts.force) {\n console.error(\n `Error: Directory ${answers.targetPath} already exists and is not empty.`,\n );\n console.error(\" Use --force to overwrite.\");\n process.exit(1);\n }\n\n scaffoldPlugin(targetDir, answers, { isolated: placement === \"isolated\" });\n\n console.log(\n `Plugin \"${answers.name}\" created at ${path.relative(process.cwd(), targetDir)}`,\n );\n printNextSteps(answers, targetDir);\n}\n\nasync function runInteractive(): Promise<void> {\n intro(\"Create a new AppKit plugin\");\n\n try {\n const placement = await select<Placement>({\n message: \"Where should the plugin live?\",\n options: [\n {\n value: \"in-repo\",\n label: \"In this repository (e.g. plugins/my-plugin)\",\n hint: \"folder path\",\n },\n {\n value: \"isolated\",\n label: \"New isolated package\",\n hint: \"full package with package.json\",\n },\n ],\n });\n if (isCancel(placement)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const placementPrompt =\n placement === \"in-repo\"\n ? \"Folder path for the plugin (e.g. plugins/my-feature)\"\n : \"Directory name for the new package (e.g. appkit-plugin-my-feature)\";\n const targetPath = await text({\n message: placementPrompt,\n placeholder:\n placement === \"in-repo\"\n ? \"plugins/my-plugin\"\n : \"appkit-plugin-my-feature\",\n validate(value) {\n if (!value?.trim()) return \"Path is required.\";\n if (\n placement === \"in-repo\" &&\n (path.isAbsolute(value) || value.trim().startsWith(\"..\"))\n ) {\n return \"Use a relative path under the current directory (e.g. plugins/my-plugin).\";\n }\n return undefined;\n },\n });\n if (isCancel(targetPath)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const name = await text({\n message: \"Plugin name (id)\",\n placeholder: \"my-plugin\",\n validate(value) {\n if (!value?.trim()) return \"Name is required.\";\n if (!NAME_PATTERN.test(value as string)) {\n return \"Must be lowercase, start with a letter, and use only letters, numbers, and hyphens.\";\n }\n return undefined;\n },\n });\n if (isCancel(name)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const displayName = await text({\n message: \"Display name\",\n placeholder: \"My Plugin\",\n initialValue: name\n .split(\"-\")\n .map((s) => s.charAt(0).toUpperCase() + s.slice(1))\n .join(\" \"),\n validate(value) {\n if (!value?.trim()) return \"Display name is required.\";\n return undefined;\n },\n });\n if (isCancel(displayName)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const description = await text({\n message: \"Short description\",\n placeholder: \"What does this plugin do?\",\n validate(value) {\n if (!value?.trim()) return \"Description is required.\";\n return undefined;\n },\n });\n if (isCancel(description)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const resourceTypes = await multiselect({\n message: \"Which Databricks resources does this plugin need?\",\n options: RESOURCE_TYPE_OPTIONS.map((o) => ({\n value: o.value,\n label: o.label,\n })),\n required: false,\n });\n if (isCancel(resourceTypes)) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n const resources: CreateAnswers[\"resources\"] = [];\n for (const type of resourceTypes as string[]) {\n const spec = await promptOneResource({ type });\n if (!spec) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n resources.push({\n type: spec.type,\n required: spec.required,\n description: spec.description,\n resourceKey: spec.resourceKey,\n permission: spec.permission,\n fields: spec.fields,\n });\n }\n\n const answers: CreateAnswers = {\n placement,\n targetPath: (targetPath as string).trim(),\n name: (name as string).trim(),\n displayName: (displayName as string).trim(),\n description: (description as string).trim(),\n resources,\n version: DEFAULT_VERSION,\n };\n\n const targetDir = resolveTargetDir(process.cwd(), answers);\n const dirExists = fs.existsSync(targetDir);\n const hasContent = dirExists && fs.readdirSync(targetDir).length > 0;\n if (hasContent) {\n const overwrite = await confirm({\n message: `Directory ${answers.targetPath} already exists and is not empty. Overwrite?`,\n initialValue: false,\n });\n if (isCancel(overwrite) || !overwrite) {\n cancel(\"Cancelled.\");\n process.exit(0);\n }\n }\n\n const s = spinner();\n s.start(\"Writing files…\");\n try {\n scaffoldPlugin(targetDir, answers, {\n isolated: placement === \"isolated\",\n });\n s.stop(\"Files written.\");\n } catch (err) {\n s.stop(\"Failed.\");\n throw err;\n }\n\n outro(\"Plugin created successfully.\");\n printNextSteps(answers, targetDir);\n } catch (err) {\n console.error(err);\n process.exit(1);\n }\n}\n\nconst OPTIONAL_FLAGS = [\n \"displayName\",\n \"resources\",\n \"resourcesJson\",\n \"force\",\n] as const;\n\nasync function runPluginCreate(opts: CreateOptions): Promise<void> {\n const hasRequiredFlag = REQUIRED_FLAGS.some((f) => opts[f] !== undefined);\n if (hasRequiredFlag) {\n runNonInteractive(opts);\n } else {\n const hasOptionalOnly = OPTIONAL_FLAGS.some(\n (f) => opts[f] !== undefined && opts[f] !== false,\n );\n if (hasOptionalOnly) {\n console.error(\n `Error: Non-interactive mode requires: ${REQUIRED_FLAGS.map((f) => `--${f}`).join(\", \")}`,\n );\n console.error(\n ' appkit plugin create --placement in-repo --path plugins/my-plugin --name my-plugin --description \"Does X\"',\n );\n process.exit(1);\n }\n await runInteractive();\n }\n}\n\nexport const pluginCreateCommand = new Command(\"create\")\n .description(\"Scaffold a new AppKit plugin\")\n .option(\"--placement <type>\", \"Where the plugin lives (in-repo, isolated)\")\n .option(\"--path <dir>\", \"Target directory for the plugin\")\n .option(\"--name <id>\", \"Plugin name (lowercase, hyphens allowed)\")\n .option(\"--display-name <name>\", \"Human-readable display name\")\n .option(\"--description <text>\", \"Short description of the plugin\")\n .addOption(\n new Option(\n \"--resources <types>\",\n \"Comma-separated resource types (e.g. sql_warehouse,volume)\",\n ).conflicts(\"resourcesJson\"),\n )\n .addOption(\n new Option(\n \"--resources-json <json>\",\n 'JSON array of resource specs (e.g. \\'[{\"type\":\"sql_warehouse\"}]\\')',\n ).conflicts(\"resources\"),\n )\n .option(\"-f, --force\", \"Overwrite existing directory without confirmation\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit plugin create\n $ appkit plugin create --placement in-repo --path plugins/my-plugin --name my-plugin --description \"Does X\"\n $ appkit plugin create --placement in-repo --path plugins/my-plugin --name my-plugin --description \"Does X\" --resources sql_warehouse,volume --force\n $ appkit plugin create --placement isolated --path appkit-plugin-ml --name ml --description \"ML\" --resources-json '[{\"type\":\"serving_endpoint\"}]'`,\n )\n .action((opts) =>\n runPluginCreate(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n\n/** Exported for testing. */\nexport { buildResourceFromType, parseResourcesJson, parseResourcesShorthand };\n"],"mappings":"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"}
|
|
@@ -81,6 +81,10 @@ const DEFAULT_FIELDS_BY_TYPE = {
|
|
|
81
81
|
description: "Databricks App ID"
|
|
82
82
|
} }
|
|
83
83
|
};
|
|
84
|
+
/** Valid resource type values from the schema. */
|
|
85
|
+
function getValidResourceTypes() {
|
|
86
|
+
return RESOURCE_TYPE_OPTIONS.map((o) => o.value);
|
|
87
|
+
}
|
|
84
88
|
/** Humanized alias from resource type (e.g. sql_warehouse -> "SQL Warehouse"). */
|
|
85
89
|
function humanizeResourceType(type) {
|
|
86
90
|
const option = RESOURCE_TYPE_OPTIONS.find((o) => o.value === type);
|
|
@@ -101,5 +105,5 @@ function getDefaultFieldsForType(type) {
|
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
//#endregion
|
|
104
|
-
export { MANIFEST_SCHEMA_ID, PERMISSIONS_BY_TYPE, RESOURCE_TYPE_OPTIONS, getDefaultFieldsForType, humanizeResourceType, resourceKeyFromType };
|
|
108
|
+
export { DEFAULT_PERMISSION_BY_TYPE, MANIFEST_SCHEMA_ID, PERMISSIONS_BY_TYPE, RESOURCE_TYPE_OPTIONS, getDefaultFieldsForType, getValidResourceTypes, humanizeResourceType, resourceKeyFromType };
|
|
105
109
|
//# sourceMappingURL=resource-defaults.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource-defaults.js","names":[],"sources":["../../../../../src/cli/commands/plugin/create/resource-defaults.ts"],"sourcesContent":["/**\n * Resource type and permission defaults for plugin scaffolding.\n * Values are derived from plugin-manifest.schema.json via schema-resources.\n */\n\nimport {\n getResourceTypeOptions,\n getResourceTypePermissions,\n type ResourceTypeOption,\n} from \"../schema-resources\";\n\nexport const MANIFEST_SCHEMA_ID =\n \"https://databricks.github.io/appkit/schemas/plugin-manifest.schema.json\";\n\nexport type { ResourceTypeOption };\n\n/** Resource types from schema resourceType enum (value, human label). */\nexport const RESOURCE_TYPE_OPTIONS: ResourceTypeOption[] =\n getResourceTypeOptions();\n\n/** All valid permissions per resource type, from schema allOf/if-then rules. */\nexport const PERMISSIONS_BY_TYPE: Record<string, string[]> =\n getResourceTypePermissions();\n\n/** Default (first) permission per resource type for scaffolding. */\nexport const DEFAULT_PERMISSION_BY_TYPE: Record<string, string> =\n Object.fromEntries(\n Object.entries(PERMISSIONS_BY_TYPE).map(([type, perms]) => [\n type,\n perms[0],\n ]),\n );\n\n/** Default fields per resource type: field key -> { env, description }. */\nexport const DEFAULT_FIELDS_BY_TYPE: Record<\n string,\n Record<string, { env: string; description?: string }>\n> = {\n sql_warehouse: {\n id: { env: \"DATABRICKS_WAREHOUSE_ID\", description: \"SQL Warehouse ID\" },\n },\n secret: {\n scope: { env: \"SECRET_SCOPE\", description: \"Secret scope name\" },\n key: { env: \"SECRET_KEY\", description: \"Secret key\" },\n },\n job: {\n id: { env: \"DATABRICKS_JOB_ID\", description: \"Job ID\" },\n },\n serving_endpoint: {\n id: {\n env: \"DATABRICKS_SERVING_ENDPOINT_ID\",\n description: \"Serving endpoint ID\",\n },\n },\n volume: {\n name: { env: \"VOLUME_NAME\", description: \"Volume name\" },\n },\n vector_search_index: {\n endpoint_name: {\n env: \"VECTOR_SEARCH_ENDPOINT_NAME\",\n description: \"Vector search endpoint name\",\n },\n index_name: {\n env: \"VECTOR_SEARCH_INDEX_NAME\",\n description: \"Vector search index name\",\n },\n },\n uc_function: {\n name: {\n env: \"UC_FUNCTION_NAME\",\n description: \"Unity Catalog function name\",\n },\n },\n uc_connection: {\n name: {\n env: \"UC_CONNECTION_NAME\",\n description: \"Unity Catalog connection name\",\n },\n },\n database: {\n instance_name: {\n env: \"DATABRICKS_INSTANCE_NAME\",\n description: \"Databricks instance name\",\n },\n database_name: {\n env: \"DATABASE_NAME\",\n description: \"Database name\",\n },\n },\n genie_space: {\n id: { env: \"GENIE_SPACE_ID\", description: \"Genie Space ID\" },\n },\n experiment: {\n id: { env: \"MLFLOW_EXPERIMENT_ID\", description: \"MLflow experiment ID\" },\n },\n app: {\n id: { env: \"DATABRICKS_APP_ID\", description: \"Databricks App ID\" },\n },\n};\n\n/** Humanized alias from resource type (e.g. sql_warehouse -> \"SQL Warehouse\"). */\nexport function humanizeResourceType(type: string): string {\n const option = RESOURCE_TYPE_OPTIONS.find((o) => o.value === type);\n return option ? option.label : type.replace(/_/g, \" \");\n}\n\n/** Kebab-case resource key from type (e.g. sql_warehouse -> \"sql-warehouse\"). */\nexport function resourceKeyFromType(type: string): string {\n return type.replace(/_/g, \"-\");\n}\n\n/** Get default fields for a resource type; fallback to single id field. */\nexport function getDefaultFieldsForType(\n type: string,\n): Record<string, { env: string; description?: string }> {\n const known = DEFAULT_FIELDS_BY_TYPE[type];\n if (known) return known;\n const key = resourceKeyFromType(type);\n const envName = `DATABRICKS_${key.toUpperCase().replace(/-/g, \"_\")}_ID`;\n return {\n id: { env: envName, description: `${humanizeResourceType(type)} ID` },\n };\n}\n"],"mappings":";;;;;;;AAWA,MAAa,qBACX;;AAKF,MAAa,wBACX,wBAAwB;;AAG1B,MAAa,sBACX,4BAA4B;;AAG9B,MAAa,6BACX,OAAO,YACL,OAAO,QAAQ,oBAAoB,CAAC,KAAK,CAAC,MAAM,WAAW,CACzD,MACA,MAAM,GACP,CAAC,CACH;;AAGH,MAAa,yBAGT;CACF,eAAe,EACb,IAAI;EAAE,KAAK;EAA2B,aAAa;EAAoB,EACxE;CACD,QAAQ;EACN,OAAO;GAAE,KAAK;GAAgB,aAAa;GAAqB;EAChE,KAAK;GAAE,KAAK;GAAc,aAAa;GAAc;EACtD;CACD,KAAK,EACH,IAAI;EAAE,KAAK;EAAqB,aAAa;EAAU,EACxD;CACD,kBAAkB,EAChB,IAAI;EACF,KAAK;EACL,aAAa;EACd,EACF;CACD,QAAQ,EACN,MAAM;EAAE,KAAK;EAAe,aAAa;EAAe,EACzD;CACD,qBAAqB;EACnB,eAAe;GACb,KAAK;GACL,aAAa;GACd;EACD,YAAY;GACV,KAAK;GACL,aAAa;GACd;EACF;CACD,aAAa,EACX,MAAM;EACJ,KAAK;EACL,aAAa;EACd,EACF;CACD,eAAe,EACb,MAAM;EACJ,KAAK;EACL,aAAa;EACd,EACF;CACD,UAAU;EACR,eAAe;GACb,KAAK;GACL,aAAa;GACd;EACD,eAAe;GACb,KAAK;GACL,aAAa;GACd;EACF;CACD,aAAa,EACX,IAAI;EAAE,KAAK;EAAkB,aAAa;EAAkB,EAC7D;CACD,YAAY,EACV,IAAI;EAAE,KAAK;EAAwB,aAAa;EAAwB,EACzE;CACD,KAAK,EACH,IAAI;EAAE,KAAK;EAAqB,aAAa;EAAqB,EACnE;CACF;;AAGD,SAAgB,qBAAqB,MAAsB;CACzD,MAAM,SAAS,sBAAsB,MAAM,MAAM,EAAE,UAAU,KAAK;AAClE,QAAO,SAAS,OAAO,QAAQ,KAAK,QAAQ,MAAM,IAAI;;;AAIxD,SAAgB,oBAAoB,MAAsB;AACxD,QAAO,KAAK,QAAQ,MAAM,IAAI;;;AAIhC,SAAgB,wBACd,MACuD;CACvD,MAAM,QAAQ,uBAAuB;AACrC,KAAI,MAAO,QAAO;AAGlB,QAAO,EACL,IAAI;EAAE,KAFQ,cADJ,oBAAoB,KAAK,CACH,aAAa,CAAC,QAAQ,MAAM,IAAI,CAAC;EAE7C,aAAa,GAAG,qBAAqB,KAAK,CAAC;EAAM,EACtE"}
|
|
1
|
+
{"version":3,"file":"resource-defaults.js","names":[],"sources":["../../../../../src/cli/commands/plugin/create/resource-defaults.ts"],"sourcesContent":["/**\n * Resource type and permission defaults for plugin scaffolding.\n * Values are derived from plugin-manifest.schema.json via schema-resources.\n */\n\nimport {\n getResourceTypeOptions,\n getResourceTypePermissions,\n type ResourceTypeOption,\n} from \"../schema-resources\";\n\nexport const MANIFEST_SCHEMA_ID =\n \"https://databricks.github.io/appkit/schemas/plugin-manifest.schema.json\";\n\nexport type { ResourceTypeOption };\n\n/** Resource types from schema resourceType enum (value, human label). */\nexport const RESOURCE_TYPE_OPTIONS: ResourceTypeOption[] =\n getResourceTypeOptions();\n\n/** All valid permissions per resource type, from schema allOf/if-then rules. */\nexport const PERMISSIONS_BY_TYPE: Record<string, string[]> =\n getResourceTypePermissions();\n\n/** Default (first) permission per resource type for scaffolding. */\nexport const DEFAULT_PERMISSION_BY_TYPE: Record<string, string> =\n Object.fromEntries(\n Object.entries(PERMISSIONS_BY_TYPE).map(([type, perms]) => [\n type,\n perms[0],\n ]),\n );\n\n/** Default fields per resource type: field key -> { env, description }. */\nexport const DEFAULT_FIELDS_BY_TYPE: Record<\n string,\n Record<string, { env: string; description?: string }>\n> = {\n sql_warehouse: {\n id: { env: \"DATABRICKS_WAREHOUSE_ID\", description: \"SQL Warehouse ID\" },\n },\n secret: {\n scope: { env: \"SECRET_SCOPE\", description: \"Secret scope name\" },\n key: { env: \"SECRET_KEY\", description: \"Secret key\" },\n },\n job: {\n id: { env: \"DATABRICKS_JOB_ID\", description: \"Job ID\" },\n },\n serving_endpoint: {\n id: {\n env: \"DATABRICKS_SERVING_ENDPOINT_ID\",\n description: \"Serving endpoint ID\",\n },\n },\n volume: {\n name: { env: \"VOLUME_NAME\", description: \"Volume name\" },\n },\n vector_search_index: {\n endpoint_name: {\n env: \"VECTOR_SEARCH_ENDPOINT_NAME\",\n description: \"Vector search endpoint name\",\n },\n index_name: {\n env: \"VECTOR_SEARCH_INDEX_NAME\",\n description: \"Vector search index name\",\n },\n },\n uc_function: {\n name: {\n env: \"UC_FUNCTION_NAME\",\n description: \"Unity Catalog function name\",\n },\n },\n uc_connection: {\n name: {\n env: \"UC_CONNECTION_NAME\",\n description: \"Unity Catalog connection name\",\n },\n },\n database: {\n instance_name: {\n env: \"DATABRICKS_INSTANCE_NAME\",\n description: \"Databricks instance name\",\n },\n database_name: {\n env: \"DATABASE_NAME\",\n description: \"Database name\",\n },\n },\n genie_space: {\n id: { env: \"GENIE_SPACE_ID\", description: \"Genie Space ID\" },\n },\n experiment: {\n id: { env: \"MLFLOW_EXPERIMENT_ID\", description: \"MLflow experiment ID\" },\n },\n app: {\n id: { env: \"DATABRICKS_APP_ID\", description: \"Databricks App ID\" },\n },\n};\n\n/** Valid resource type values from the schema. */\nexport function getValidResourceTypes(): string[] {\n return RESOURCE_TYPE_OPTIONS.map((o) => o.value);\n}\n\n/** Humanized alias from resource type (e.g. sql_warehouse -> \"SQL Warehouse\"). */\nexport function humanizeResourceType(type: string): string {\n const option = RESOURCE_TYPE_OPTIONS.find((o) => o.value === type);\n return option ? option.label : type.replace(/_/g, \" \");\n}\n\n/** Kebab-case resource key from type (e.g. sql_warehouse -> \"sql-warehouse\"). */\nexport function resourceKeyFromType(type: string): string {\n return type.replace(/_/g, \"-\");\n}\n\n/** Get default fields for a resource type; fallback to single id field. */\nexport function getDefaultFieldsForType(\n type: string,\n): Record<string, { env: string; description?: string }> {\n const known = DEFAULT_FIELDS_BY_TYPE[type];\n if (known) return known;\n const key = resourceKeyFromType(type);\n const envName = `DATABRICKS_${key.toUpperCase().replace(/-/g, \"_\")}_ID`;\n return {\n id: { env: envName, description: `${humanizeResourceType(type)} ID` },\n };\n}\n"],"mappings":";;;;;;;AAWA,MAAa,qBACX;;AAKF,MAAa,wBACX,wBAAwB;;AAG1B,MAAa,sBACX,4BAA4B;;AAG9B,MAAa,6BACX,OAAO,YACL,OAAO,QAAQ,oBAAoB,CAAC,KAAK,CAAC,MAAM,WAAW,CACzD,MACA,MAAM,GACP,CAAC,CACH;;AAGH,MAAa,yBAGT;CACF,eAAe,EACb,IAAI;EAAE,KAAK;EAA2B,aAAa;EAAoB,EACxE;CACD,QAAQ;EACN,OAAO;GAAE,KAAK;GAAgB,aAAa;GAAqB;EAChE,KAAK;GAAE,KAAK;GAAc,aAAa;GAAc;EACtD;CACD,KAAK,EACH,IAAI;EAAE,KAAK;EAAqB,aAAa;EAAU,EACxD;CACD,kBAAkB,EAChB,IAAI;EACF,KAAK;EACL,aAAa;EACd,EACF;CACD,QAAQ,EACN,MAAM;EAAE,KAAK;EAAe,aAAa;EAAe,EACzD;CACD,qBAAqB;EACnB,eAAe;GACb,KAAK;GACL,aAAa;GACd;EACD,YAAY;GACV,KAAK;GACL,aAAa;GACd;EACF;CACD,aAAa,EACX,MAAM;EACJ,KAAK;EACL,aAAa;EACd,EACF;CACD,eAAe,EACb,MAAM;EACJ,KAAK;EACL,aAAa;EACd,EACF;CACD,UAAU;EACR,eAAe;GACb,KAAK;GACL,aAAa;GACd;EACD,eAAe;GACb,KAAK;GACL,aAAa;GACd;EACF;CACD,aAAa,EACX,IAAI;EAAE,KAAK;EAAkB,aAAa;EAAkB,EAC7D;CACD,YAAY,EACV,IAAI;EAAE,KAAK;EAAwB,aAAa;EAAwB,EACzE;CACD,KAAK,EACH,IAAI;EAAE,KAAK;EAAqB,aAAa;EAAqB,EACnE;CACF;;AAGD,SAAgB,wBAAkC;AAChD,QAAO,sBAAsB,KAAK,MAAM,EAAE,MAAM;;;AAIlD,SAAgB,qBAAqB,MAAsB;CACzD,MAAM,SAAS,sBAAsB,MAAM,MAAM,EAAE,UAAU,KAAK;AAClE,QAAO,SAAS,OAAO,QAAQ,KAAK,QAAQ,MAAM,IAAI;;;AAIxD,SAAgB,oBAAoB,MAAsB;AACxD,QAAO,KAAK,QAAQ,MAAM,IAAI;;;AAIhC,SAAgB,wBACd,MACuD;CACvD,MAAM,QAAQ,uBAAuB;AACrC,KAAI,MAAO,QAAO;AAGlB,QAAO,EACL,IAAI;EAAE,KAFQ,cADJ,oBAAoB,KAAK,CACH,aAAa,CAAC,QAAQ,MAAM,IAAI,CAAC;EAE7C,aAAa,GAAG,qBAAqB,KAAK,CAAC;EAAM,EACtE"}
|
|
@@ -15,7 +15,13 @@ import { Command } from "commander";
|
|
|
15
15
|
* - list: List plugins from appkit.plugins.json or a directory
|
|
16
16
|
* - add-resource: Add a resource requirement to a plugin manifest (interactive)
|
|
17
17
|
*/
|
|
18
|
-
const pluginCommand = new Command("plugin").description("Plugin management commands").addCommand(pluginsSyncCommand).addCommand(pluginCreateCommand).addCommand(pluginValidateCommand).addCommand(pluginListCommand).addCommand(pluginAddResourceCommand)
|
|
18
|
+
const pluginCommand = new Command("plugin").description("Plugin management commands").addCommand(pluginsSyncCommand).addCommand(pluginCreateCommand).addCommand(pluginValidateCommand).addCommand(pluginListCommand).addCommand(pluginAddResourceCommand).addHelpText("after", `
|
|
19
|
+
Examples:
|
|
20
|
+
$ appkit plugin sync --write
|
|
21
|
+
$ appkit plugin create --placement in-repo --path plugins/my-plugin --name my-plugin --description "Does X"
|
|
22
|
+
$ appkit plugin validate .
|
|
23
|
+
$ appkit plugin list --json
|
|
24
|
+
$ appkit plugin add-resource --path plugins/my-plugin --type sql_warehouse`);
|
|
19
25
|
|
|
20
26
|
//#endregion
|
|
21
27
|
export { pluginCommand };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/cli/commands/plugin/index.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { pluginAddResourceCommand } from \"./add-resource/add-resource\";\nimport { pluginCreateCommand } from \"./create/create\";\nimport { pluginListCommand } from \"./list/list\";\nimport { pluginsSyncCommand } from \"./sync/sync\";\nimport { pluginValidateCommand } from \"./validate/validate\";\n\n/**\n * Parent command for plugin management operations.\n * Subcommands:\n * - sync: Aggregate plugin manifests into appkit.plugins.json\n * - create: Scaffold a new plugin (interactive)\n * - validate: Validate manifest(s) against the JSON schema\n * - list: List plugins from appkit.plugins.json or a directory\n * - add-resource: Add a resource requirement to a plugin manifest (interactive)\n */\nexport const pluginCommand = new Command(\"plugin\")\n .description(\"Plugin management commands\")\n .addCommand(pluginsSyncCommand)\n .addCommand(pluginCreateCommand)\n .addCommand(pluginValidateCommand)\n .addCommand(pluginListCommand)\n .addCommand(pluginAddResourceCommand);\n"],"mappings":";;;;;;;;;;;;;;;;;AAgBA,MAAa,gBAAgB,IAAI,QAAQ,SAAS,CAC/C,YAAY,6BAA6B,CACzC,WAAW,mBAAmB,CAC9B,WAAW,oBAAoB,CAC/B,WAAW,sBAAsB,CACjC,WAAW,kBAAkB,CAC7B,WAAW,yBAAyB"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/cli/commands/plugin/index.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { pluginAddResourceCommand } from \"./add-resource/add-resource\";\nimport { pluginCreateCommand } from \"./create/create\";\nimport { pluginListCommand } from \"./list/list\";\nimport { pluginsSyncCommand } from \"./sync/sync\";\nimport { pluginValidateCommand } from \"./validate/validate\";\n\n/**\n * Parent command for plugin management operations.\n * Subcommands:\n * - sync: Aggregate plugin manifests into appkit.plugins.json\n * - create: Scaffold a new plugin (interactive)\n * - validate: Validate manifest(s) against the JSON schema\n * - list: List plugins from appkit.plugins.json or a directory\n * - add-resource: Add a resource requirement to a plugin manifest (interactive)\n */\nexport const pluginCommand = new Command(\"plugin\")\n .description(\"Plugin management commands\")\n .addCommand(pluginsSyncCommand)\n .addCommand(pluginCreateCommand)\n .addCommand(pluginValidateCommand)\n .addCommand(pluginListCommand)\n .addCommand(pluginAddResourceCommand)\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit plugin sync --write\n $ appkit plugin create --placement in-repo --path plugins/my-plugin --name my-plugin --description \"Does X\"\n $ appkit plugin validate .\n $ appkit plugin list --json\n $ appkit plugin add-resource --path plugins/my-plugin --type sql_warehouse`,\n );\n"],"mappings":";;;;;;;;;;;;;;;;;AAgBA,MAAa,gBAAgB,IAAI,QAAQ,SAAS,CAC/C,YAAY,6BAA6B,CACzC,WAAW,mBAAmB,CAC9B,WAAW,oBAAoB,CAC/B,WAAW,sBAAsB,CACjC,WAAW,kBAAkB,CAC7B,WAAW,yBAAyB,CACpC,YACC,SACA;;;;;;8EAOD"}
|
|
@@ -109,6 +109,7 @@ async function runPluginList(options) {
|
|
|
109
109
|
const manifestPath = path.resolve(cwd, options.manifest ?? "appkit.plugins.json");
|
|
110
110
|
if (!fs.existsSync(manifestPath)) {
|
|
111
111
|
console.error(`Manifest not found: ${manifestPath}`);
|
|
112
|
+
console.error(" appkit plugin list --manifest <path-to-manifest> or appkit plugin list --dir <plugins-directory>");
|
|
112
113
|
process.exit(1);
|
|
113
114
|
}
|
|
114
115
|
try {
|
|
@@ -121,7 +122,12 @@ async function runPluginList(options) {
|
|
|
121
122
|
if (options.json) console.log(JSON.stringify(rows, null, 2));
|
|
122
123
|
else printTable(rows);
|
|
123
124
|
}
|
|
124
|
-
const pluginListCommand = new Command("list").description("List plugins from appkit.plugins.json or a directory").option("-m, --manifest <path>", "Path to appkit.plugins.json", "appkit.plugins.json").option("-d, --dir <path>", "Scan directory recursively for plugin folders (manifest.json by default)").option("--allow-js-manifest", "Allow reading manifest.js/manifest.cjs (executes code; use only with trusted plugins)").option("--json", "Output as JSON").
|
|
125
|
+
const pluginListCommand = new Command("list").description("List plugins from appkit.plugins.json or a directory").option("-m, --manifest <path>", "Path to appkit.plugins.json", "appkit.plugins.json").option("-d, --dir <path>", "Scan directory recursively for plugin folders (manifest.json by default)").option("--allow-js-manifest", "Allow reading manifest.js/manifest.cjs (executes code; use only with trusted plugins)").option("--json", "Output as JSON").addHelpText("after", `
|
|
126
|
+
Examples:
|
|
127
|
+
$ appkit plugin list
|
|
128
|
+
$ appkit plugin list --json
|
|
129
|
+
$ appkit plugin list --manifest custom-manifest.json
|
|
130
|
+
$ appkit plugin list --dir plugins/`).action((opts) => runPluginList(opts).catch((err) => {
|
|
125
131
|
console.error(err);
|
|
126
132
|
process.exit(1);
|
|
127
133
|
}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list.js","names":[],"sources":["../../../../../src/cli/commands/plugin/list/list.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport { Command } from \"commander\";\nimport {\n loadManifestFromFile,\n resolveManifestInDir,\n} from \"../manifest-resolve\";\nimport { shouldAllowJsManifestForDir } from \"../trusted-js-manifest\";\nimport { validateManifest } from \"../validate/validate-manifest\";\n\n/** Safety limit for recursive directory scanning to prevent runaway traversal. */\nconst MAX_SCAN_DEPTH = 5;\n\nexport interface PluginRow {\n name: string;\n displayName: string;\n package: string;\n required: number;\n optional: number;\n}\n\nexport function listFromManifestFile(manifestPath: string): PluginRow[] {\n let raw: string;\n try {\n raw = fs.readFileSync(manifestPath, \"utf-8\");\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`Failed to read manifest file ${manifestPath}: ${msg}`);\n }\n\n let data: {\n plugins?: Record<\n string,\n {\n name: string;\n displayName: string;\n package: string;\n resources: { required: unknown[]; optional: unknown[] };\n }\n >;\n };\n try {\n data = JSON.parse(raw) as typeof data;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`Failed to parse manifest file ${manifestPath}: ${msg}`);\n }\n\n const plugins = data.plugins ?? {};\n return Object.values(plugins).map((p) => ({\n name: p.name,\n displayName: p.displayName ?? p.name,\n package: p.package ?? \"\",\n required: Array.isArray(p.resources?.required)\n ? p.resources.required.length\n : 0,\n optional: Array.isArray(p.resources?.optional)\n ? p.resources.optional.length\n : 0,\n }));\n}\n\nasync function collectPluginsRecursive(\n dir: string,\n cwd: string,\n rows: PluginRow[],\n allowJsManifest: boolean,\n depth = 0,\n): Promise<void> {\n if (\n !fs.existsSync(dir) ||\n !fs.statSync(dir).isDirectory() ||\n depth >= MAX_SCAN_DEPTH\n )\n return;\n\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n\n const childPath = path.join(dir, entry.name);\n const allowJsForChild =\n allowJsManifest || shouldAllowJsManifestForDir(childPath);\n const resolvedManifest = resolveManifestInDir(childPath, {\n allowJsManifest: allowJsForChild,\n });\n\n if (resolvedManifest) {\n try {\n const obj = await loadManifestFromFile(\n resolvedManifest.path,\n resolvedManifest.type,\n { allowJsManifest: allowJsForChild },\n );\n const result = validateManifest(obj);\n const manifest = result.valid ? result.manifest : null;\n if (manifest) {\n const relPath = path.relative(\n cwd,\n path.dirname(resolvedManifest.path),\n );\n const packagePath = relPath.startsWith(\".\")\n ? relPath\n : `./${relPath}`;\n rows.push({\n name: manifest.name,\n displayName: manifest.displayName ?? manifest.name,\n package: packagePath,\n required: Array.isArray(manifest.resources?.required)\n ? manifest.resources.required.length\n : 0,\n optional: Array.isArray(manifest.resources?.optional)\n ? manifest.resources.optional.length\n : 0,\n });\n }\n } catch {\n // skip invalid manifests\n }\n continue;\n }\n\n await collectPluginsRecursive(\n childPath,\n cwd,\n rows,\n allowJsManifest,\n depth + 1,\n );\n }\n}\n\nexport async function listFromDirectory(\n dirPath: string,\n cwd: string,\n allowJsManifest = false,\n): Promise<PluginRow[]> {\n const resolved = path.resolve(cwd, dirPath);\n if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {\n return [];\n }\n const rows: PluginRow[] = [];\n await collectPluginsRecursive(resolved, cwd, rows, allowJsManifest);\n return rows;\n}\n\nfunction printTable(rows: PluginRow[]): void {\n if (rows.length === 0) {\n console.log(\"No plugins found.\");\n return;\n }\n const maxName = Math.max(4, ...rows.map((r) => r.name.length));\n const maxDisplay = Math.max(10, ...rows.map((r) => r.displayName.length));\n const maxPkg = Math.max(7, ...rows.map((r) => r.package.length));\n const header = [\n \"NAME\".padEnd(maxName),\n \"DISPLAY NAME\".padEnd(maxDisplay),\n \"PACKAGE / PATH\".padEnd(maxPkg),\n \"REQ\",\n \"OPT\",\n ].join(\" \");\n console.log(header);\n console.log(\"-\".repeat(header.length));\n for (const r of rows) {\n console.log(\n [\n r.name.padEnd(maxName),\n r.displayName.padEnd(maxDisplay),\n r.package.padEnd(maxPkg),\n String(r.required).padStart(3),\n String(r.optional).padStart(3),\n ].join(\" \"),\n );\n }\n}\n\nasync function runPluginList(options: {\n manifest?: string;\n dir?: string;\n json?: boolean;\n allowJsManifest?: boolean;\n}): Promise<void> {\n const cwd = process.cwd();\n const allowJsManifest = Boolean(options.allowJsManifest);\n if (allowJsManifest) {\n console.warn(\n \"Warning: --allow-js-manifest executes manifest.js/manifest.cjs files. Only use with trusted code.\",\n );\n }\n let rows: PluginRow[];\n\n if (options.dir !== undefined) {\n rows = await listFromDirectory(options.dir, cwd, allowJsManifest);\n if (rows.length === 0 && options.dir) {\n console.error(\n `No plugin directories with ${allowJsManifest ? \"manifest.json or manifest.js\" : \"manifest.json\"} found in ${options.dir}`,\n );\n process.exit(1);\n }\n } else {\n const manifestPath = path.resolve(\n cwd,\n options.manifest ?? \"appkit.plugins.json\",\n );\n if (!fs.existsSync(manifestPath)) {\n console.error(`Manifest not found: ${manifestPath}`);\n process.exit(1);\n }\n try {\n rows = listFromManifestFile(manifestPath);\n } catch (err) {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n }\n\n if (options.json) {\n console.log(JSON.stringify(rows, null, 2));\n } else {\n printTable(rows);\n }\n}\n\nexport const pluginListCommand = new Command(\"list\")\n .description(\"List plugins from appkit.plugins.json or a directory\")\n .option(\n \"-m, --manifest <path>\",\n \"Path to appkit.plugins.json\",\n \"appkit.plugins.json\",\n )\n .option(\n \"-d, --dir <path>\",\n \"Scan directory recursively for plugin folders (manifest.json by default)\",\n )\n .option(\n \"--allow-js-manifest\",\n \"Allow reading manifest.js/manifest.cjs (executes code; use only with trusted plugins)\",\n )\n .option(\"--json\", \"Output as JSON\")\n .action((opts) =>\n runPluginList(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"list.js","names":[],"sources":["../../../../../src/cli/commands/plugin/list/list.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport { Command } from \"commander\";\nimport {\n loadManifestFromFile,\n resolveManifestInDir,\n} from \"../manifest-resolve\";\nimport { shouldAllowJsManifestForDir } from \"../trusted-js-manifest\";\nimport { validateManifest } from \"../validate/validate-manifest\";\n\n/** Safety limit for recursive directory scanning to prevent runaway traversal. */\nconst MAX_SCAN_DEPTH = 5;\n\nexport interface PluginRow {\n name: string;\n displayName: string;\n package: string;\n required: number;\n optional: number;\n}\n\nexport function listFromManifestFile(manifestPath: string): PluginRow[] {\n let raw: string;\n try {\n raw = fs.readFileSync(manifestPath, \"utf-8\");\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`Failed to read manifest file ${manifestPath}: ${msg}`);\n }\n\n let data: {\n plugins?: Record<\n string,\n {\n name: string;\n displayName: string;\n package: string;\n resources: { required: unknown[]; optional: unknown[] };\n }\n >;\n };\n try {\n data = JSON.parse(raw) as typeof data;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`Failed to parse manifest file ${manifestPath}: ${msg}`);\n }\n\n const plugins = data.plugins ?? {};\n return Object.values(plugins).map((p) => ({\n name: p.name,\n displayName: p.displayName ?? p.name,\n package: p.package ?? \"\",\n required: Array.isArray(p.resources?.required)\n ? p.resources.required.length\n : 0,\n optional: Array.isArray(p.resources?.optional)\n ? p.resources.optional.length\n : 0,\n }));\n}\n\nasync function collectPluginsRecursive(\n dir: string,\n cwd: string,\n rows: PluginRow[],\n allowJsManifest: boolean,\n depth = 0,\n): Promise<void> {\n if (\n !fs.existsSync(dir) ||\n !fs.statSync(dir).isDirectory() ||\n depth >= MAX_SCAN_DEPTH\n )\n return;\n\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n\n const childPath = path.join(dir, entry.name);\n const allowJsForChild =\n allowJsManifest || shouldAllowJsManifestForDir(childPath);\n const resolvedManifest = resolveManifestInDir(childPath, {\n allowJsManifest: allowJsForChild,\n });\n\n if (resolvedManifest) {\n try {\n const obj = await loadManifestFromFile(\n resolvedManifest.path,\n resolvedManifest.type,\n { allowJsManifest: allowJsForChild },\n );\n const result = validateManifest(obj);\n const manifest = result.valid ? result.manifest : null;\n if (manifest) {\n const relPath = path.relative(\n cwd,\n path.dirname(resolvedManifest.path),\n );\n const packagePath = relPath.startsWith(\".\")\n ? relPath\n : `./${relPath}`;\n rows.push({\n name: manifest.name,\n displayName: manifest.displayName ?? manifest.name,\n package: packagePath,\n required: Array.isArray(manifest.resources?.required)\n ? manifest.resources.required.length\n : 0,\n optional: Array.isArray(manifest.resources?.optional)\n ? manifest.resources.optional.length\n : 0,\n });\n }\n } catch {\n // skip invalid manifests\n }\n continue;\n }\n\n await collectPluginsRecursive(\n childPath,\n cwd,\n rows,\n allowJsManifest,\n depth + 1,\n );\n }\n}\n\nexport async function listFromDirectory(\n dirPath: string,\n cwd: string,\n allowJsManifest = false,\n): Promise<PluginRow[]> {\n const resolved = path.resolve(cwd, dirPath);\n if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {\n return [];\n }\n const rows: PluginRow[] = [];\n await collectPluginsRecursive(resolved, cwd, rows, allowJsManifest);\n return rows;\n}\n\nfunction printTable(rows: PluginRow[]): void {\n if (rows.length === 0) {\n console.log(\"No plugins found.\");\n return;\n }\n const maxName = Math.max(4, ...rows.map((r) => r.name.length));\n const maxDisplay = Math.max(10, ...rows.map((r) => r.displayName.length));\n const maxPkg = Math.max(7, ...rows.map((r) => r.package.length));\n const header = [\n \"NAME\".padEnd(maxName),\n \"DISPLAY NAME\".padEnd(maxDisplay),\n \"PACKAGE / PATH\".padEnd(maxPkg),\n \"REQ\",\n \"OPT\",\n ].join(\" \");\n console.log(header);\n console.log(\"-\".repeat(header.length));\n for (const r of rows) {\n console.log(\n [\n r.name.padEnd(maxName),\n r.displayName.padEnd(maxDisplay),\n r.package.padEnd(maxPkg),\n String(r.required).padStart(3),\n String(r.optional).padStart(3),\n ].join(\" \"),\n );\n }\n}\n\nasync function runPluginList(options: {\n manifest?: string;\n dir?: string;\n json?: boolean;\n allowJsManifest?: boolean;\n}): Promise<void> {\n const cwd = process.cwd();\n const allowJsManifest = Boolean(options.allowJsManifest);\n if (allowJsManifest) {\n console.warn(\n \"Warning: --allow-js-manifest executes manifest.js/manifest.cjs files. Only use with trusted code.\",\n );\n }\n let rows: PluginRow[];\n\n if (options.dir !== undefined) {\n rows = await listFromDirectory(options.dir, cwd, allowJsManifest);\n if (rows.length === 0 && options.dir) {\n console.error(\n `No plugin directories with ${allowJsManifest ? \"manifest.json or manifest.js\" : \"manifest.json\"} found in ${options.dir}`,\n );\n process.exit(1);\n }\n } else {\n const manifestPath = path.resolve(\n cwd,\n options.manifest ?? \"appkit.plugins.json\",\n );\n if (!fs.existsSync(manifestPath)) {\n console.error(`Manifest not found: ${manifestPath}`);\n console.error(\n \" appkit plugin list --manifest <path-to-manifest> or appkit plugin list --dir <plugins-directory>\",\n );\n process.exit(1);\n }\n try {\n rows = listFromManifestFile(manifestPath);\n } catch (err) {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n }\n\n if (options.json) {\n console.log(JSON.stringify(rows, null, 2));\n } else {\n printTable(rows);\n }\n}\n\nexport const pluginListCommand = new Command(\"list\")\n .description(\"List plugins from appkit.plugins.json or a directory\")\n .option(\n \"-m, --manifest <path>\",\n \"Path to appkit.plugins.json\",\n \"appkit.plugins.json\",\n )\n .option(\n \"-d, --dir <path>\",\n \"Scan directory recursively for plugin folders (manifest.json by default)\",\n )\n .option(\n \"--allow-js-manifest\",\n \"Allow reading manifest.js/manifest.cjs (executes code; use only with trusted plugins)\",\n )\n .option(\"--json\", \"Output as JSON\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit plugin list\n $ appkit plugin list --json\n $ appkit plugin list --manifest custom-manifest.json\n $ appkit plugin list --dir plugins/`,\n )\n .action((opts) =>\n runPluginList(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}
|
|
@@ -326,15 +326,17 @@ function writeManifest(outputPath, { plugins }, options) {
|
|
|
326
326
|
version: "1.0",
|
|
327
327
|
plugins
|
|
328
328
|
};
|
|
329
|
+
const serialized = JSON.stringify(templateManifest, null, 2);
|
|
330
|
+
if (options.json) console.log(serialized);
|
|
329
331
|
if (options.write) {
|
|
330
|
-
fs.writeFileSync(outputPath, `${
|
|
331
|
-
if (!options.silent) console.log(`\n✓ Wrote ${outputPath}`);
|
|
332
|
-
} else if (!options.silent) {
|
|
332
|
+
fs.writeFileSync(outputPath, `${serialized}\n`);
|
|
333
|
+
if (!options.silent && !options.json) console.log(`\n✓ Wrote ${outputPath}`);
|
|
334
|
+
} else if (!options.silent && !options.json) {
|
|
333
335
|
console.log("\nTo write the manifest, run:");
|
|
334
336
|
console.log(" npx appkit plugin sync --write\n");
|
|
335
337
|
console.log("Preview:");
|
|
336
338
|
console.log("─".repeat(60));
|
|
337
|
-
console.log(
|
|
339
|
+
console.log(serialized);
|
|
338
340
|
console.log("─".repeat(60));
|
|
339
341
|
}
|
|
340
342
|
}
|
|
@@ -352,7 +354,7 @@ async function runPluginsSync(options) {
|
|
|
352
354
|
console.error(`Error: Output path "${options.output}" resolves outside the project directory.`);
|
|
353
355
|
process.exit(1);
|
|
354
356
|
}
|
|
355
|
-
if (!options.silent) {
|
|
357
|
+
if (!options.silent && !options.json) {
|
|
356
358
|
console.log("Scanning for AppKit plugins...\n");
|
|
357
359
|
if (allowJsManifest) console.warn("Warning: --allow-js-manifest executes manifest.js/manifest.cjs files. Only use with trusted code.");
|
|
358
360
|
}
|
|
@@ -360,7 +362,7 @@ async function runPluginsSync(options) {
|
|
|
360
362
|
let serverImports = [];
|
|
361
363
|
let pluginUsages = /* @__PURE__ */ new Set();
|
|
362
364
|
if (serverFile) {
|
|
363
|
-
if (!options.silent) {
|
|
365
|
+
if (!options.silent && !options.json) {
|
|
364
366
|
const relativePath = path.relative(cwd, serverFile);
|
|
365
367
|
console.log(`Server entry file: ${relativePath}`);
|
|
366
368
|
}
|
|
@@ -368,14 +370,14 @@ async function runPluginsSync(options) {
|
|
|
368
370
|
const root = parse(serverFile.endsWith(".tsx") ? Lang.Tsx : Lang.TypeScript, content).root();
|
|
369
371
|
serverImports = parseImports(root);
|
|
370
372
|
pluginUsages = parsePluginUsages(root);
|
|
371
|
-
} else if (!options.silent) console.log("No server entry file found. Checked:", SERVER_FILE_CANDIDATES.join(", "));
|
|
373
|
+
} else if (!options.silent && !options.json) console.log("No server entry file found. Checked:", SERVER_FILE_CANDIDATES.join(", "));
|
|
372
374
|
const npmImports = serverImports.filter((i) => !i.source.startsWith(".") && !i.source.startsWith("/"));
|
|
373
375
|
const localImports = serverImports.filter((i) => i.source.startsWith(".") || i.source.startsWith("/"));
|
|
374
376
|
const plugins = {};
|
|
375
377
|
if (options.pluginsDir) {
|
|
376
378
|
const resolvedDir = path.resolve(cwd, options.pluginsDir);
|
|
377
379
|
const pkgName = options.packageName ?? "@databricks/appkit";
|
|
378
|
-
if (!options.silent) console.log(`Scanning plugins directory: ${options.pluginsDir}`);
|
|
380
|
+
if (!options.silent && !options.json) console.log(`Scanning plugins directory: ${options.pluginsDir}`);
|
|
379
381
|
Object.assign(plugins, await scanPluginsDir(resolvedDir, pkgName, allowJsManifest));
|
|
380
382
|
} else {
|
|
381
383
|
const npmPackages = new Set([...KNOWN_PLUGIN_PACKAGES, ...npmImports.map((i) => i.source)]);
|
|
@@ -389,19 +391,23 @@ async function runPluginsSync(options) {
|
|
|
389
391
|
for (const dir of localDirsToScan) {
|
|
390
392
|
const resolvedDir = path.resolve(cwd, dir);
|
|
391
393
|
if (!fs.existsSync(resolvedDir)) continue;
|
|
392
|
-
if (!options.silent) console.log(`Scanning local plugins directory: ${dir}`);
|
|
394
|
+
if (!options.silent && !options.json) console.log(`Scanning local plugins directory: ${dir}`);
|
|
393
395
|
const discovered = await scanPluginsDirRecursive(resolvedDir, cwd, allowJsManifest);
|
|
394
396
|
for (const [name, entry] of Object.entries(discovered)) if (!plugins[name]) plugins[name] = entry;
|
|
395
397
|
}
|
|
396
398
|
const pluginCount = Object.keys(plugins).length;
|
|
397
399
|
if (pluginCount === 0) {
|
|
398
|
-
if (options.silent) {
|
|
400
|
+
if (options.silent || options.json) {
|
|
399
401
|
writeManifest(outputPath, { plugins: {} }, options);
|
|
400
|
-
return;
|
|
402
|
+
if (options.silent) return;
|
|
403
|
+
process.exit(1);
|
|
401
404
|
}
|
|
402
405
|
console.log("No plugins found.");
|
|
403
406
|
if (options.pluginsDir) console.log(`\nNo manifest (${allowJsManifest ? "manifest.json or manifest.js" : "manifest.json"}) found in: ${options.pluginsDir}`);
|
|
404
|
-
else
|
|
407
|
+
else {
|
|
408
|
+
console.log("\nMake sure you have plugin packages installed, or specify a directory:");
|
|
409
|
+
console.log(" appkit plugin sync --plugins-dir <path>");
|
|
410
|
+
}
|
|
405
411
|
process.exit(1);
|
|
406
412
|
}
|
|
407
413
|
const serverFileDir = serverFile ? path.dirname(serverFile) : cwd;
|
|
@@ -423,7 +429,7 @@ async function runPluginsSync(options) {
|
|
|
423
429
|
for (const name of explicitNames) if (plugins[name]) plugins[name].requiredByTemplate = true;
|
|
424
430
|
else if (!options.silent) console.warn(`Warning: --require-plugins referenced "${name}" but no such plugin was discovered`);
|
|
425
431
|
}
|
|
426
|
-
if (!options.silent) {
|
|
432
|
+
if (!options.silent && !options.json) {
|
|
427
433
|
console.log(`\nFound ${pluginCount} plugin(s):`);
|
|
428
434
|
for (const [name, manifest] of Object.entries(plugins)) {
|
|
429
435
|
const resourceCount = manifest.resources.required.length + manifest.resources.optional.length;
|
|
@@ -434,7 +440,14 @@ async function runPluginsSync(options) {
|
|
|
434
440
|
}
|
|
435
441
|
writeManifest(outputPath, { plugins }, options);
|
|
436
442
|
}
|
|
437
|
-
const pluginsSyncCommand = new Command("sync").description("Sync plugin manifests from installed packages into appkit.plugins.json").option("-w, --write", "Write the manifest file").option("-o, --output <path>", "Output file path (default: ./appkit.plugins.json)").option("-s, --silent", "Suppress output and never exit with error (for use in predev/prebuild hooks)").option("--require-plugins <names>", "Comma-separated plugin names to mark as requiredByTemplate (e.g. server,analytics)").option("--plugins-dir <path>", "Scan this directory for plugin subdirectories with manifest.json (instead of node_modules)").option("--package-name <name>", "Package name to assign to plugins found via --plugins-dir (default: @databricks/appkit)").option("--local-plugins-dir <path>", "Also scan this directory for local plugin manifests (default: plugins, server)").option("--allow-js-manifest", "Allow reading manifest.js/manifest.cjs (executes code; use only with trusted plugins)").
|
|
443
|
+
const pluginsSyncCommand = new Command("sync").description("Sync plugin manifests from installed packages into appkit.plugins.json").option("-w, --write", "Write the manifest file").option("-o, --output <path>", "Output file path (default: ./appkit.plugins.json)").option("-s, --silent", "Suppress output and never exit with error (for use in predev/prebuild hooks)").option("--require-plugins <names>", "Comma-separated plugin names to mark as requiredByTemplate (e.g. server,analytics)").option("--plugins-dir <path>", "Scan this directory for plugin subdirectories with manifest.json (instead of node_modules)").option("--package-name <name>", "Package name to assign to plugins found via --plugins-dir (default: @databricks/appkit)").option("--local-plugins-dir <path>", "Also scan this directory for local plugin manifests (default: plugins, server)").option("--allow-js-manifest", "Allow reading manifest.js/manifest.cjs (executes code; use only with trusted plugins)").option("--json", "Output manifest as JSON to stdout").addHelpText("after", `
|
|
444
|
+
Examples:
|
|
445
|
+
$ appkit plugin sync
|
|
446
|
+
$ appkit plugin sync --write
|
|
447
|
+
$ appkit plugin sync --write --require-plugins server,analytics
|
|
448
|
+
$ appkit plugin sync --write --plugins-dir src/plugins --package-name @my/pkg
|
|
449
|
+
$ appkit plugin sync --json
|
|
450
|
+
$ appkit plugin sync --silent`).action((opts) => runPluginsSync(opts).catch((err) => {
|
|
438
451
|
console.error(err);
|
|
439
452
|
process.exit(1);
|
|
440
453
|
}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.js","names":[],"sources":["../../../../../src/cli/commands/plugin/sync/sync.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Lang, parse, type SgNode } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\nimport {\n loadManifestFromFile,\n type ResolvedManifest,\n resolveManifestInDir,\n} from \"../manifest-resolve\";\nimport type {\n PluginManifest,\n TemplatePlugin,\n TemplatePluginsManifest,\n} from \"../manifest-types\";\nimport { shouldAllowJsManifestForPackage } from \"../trusted-js-manifest\";\nimport {\n formatValidationErrors,\n validateManifest,\n} from \"../validate/validate-manifest\";\n\n/**\n * Checks whether a resolved file path is within a given directory boundary.\n * Uses path.resolve + startsWith to prevent directory traversal.\n *\n * @param filePath - The path to check (will be resolved to absolute)\n * @param boundary - The directory that must contain filePath\n * @returns true if filePath is inside boundary (or equal to it)\n */\nfunction isWithinDirectory(filePath: string, boundary: string): boolean {\n const resolvedPath = path.resolve(filePath);\n const resolvedBoundary = path.resolve(boundary);\n // Append separator to avoid prefix false-positives (e.g. /foo-bar matching /foo)\n return (\n resolvedPath === resolvedBoundary ||\n resolvedPath.startsWith(`${resolvedBoundary}${path.sep}`)\n );\n}\n\n/**\n * Validates a parsed JSON object against the plugin-manifest JSON schema.\n * Returns the manifest if valid, or null and logs schema errors.\n */\nfunction validateManifestWithSchema(\n obj: unknown,\n sourcePath: string,\n): PluginManifest | null {\n const result = validateManifest(obj);\n if (result.valid && result.manifest) return result.manifest;\n if (result.errors?.length) {\n console.warn(\n `Warning: Manifest at ${sourcePath} failed schema validation:\\n${formatValidationErrors(result.errors, obj)}`,\n );\n }\n return null;\n}\n\n/** Safety limit for recursive directory scanning to prevent runaway traversal. */\nconst MAX_SCAN_DEPTH = 5;\n\n/**\n * Load and validate a resolved manifest, returning a TemplatePlugin entry or null.\n * Centralises the resolve → load → validate → build-entry pipeline used by\n * multiple discovery functions.\n */\nasync function loadPluginEntry(\n resolved: ResolvedManifest,\n pkg: string,\n allowJsManifest: boolean,\n): Promise<[string, TemplatePlugin] | null> {\n const parsed = await loadManifestFromFile(resolved.path, resolved.type, {\n allowJsManifest,\n });\n const manifest = validateManifestWithSchema(parsed, resolved.path);\n if (!manifest || manifest.hidden) return null;\n\n return [\n manifest.name,\n {\n name: manifest.name,\n displayName: manifest.displayName,\n description: manifest.description,\n package: pkg,\n resources: manifest.resources,\n ...(manifest.onSetupMessage && {\n onSetupMessage: manifest.onSetupMessage,\n }),\n },\n ];\n}\n\n/**\n * Known packages that may contain AppKit plugins.\n * Always scanned for manifests, even if not imported in the server file.\n */\nconst KNOWN_PLUGIN_PACKAGES = [\"@databricks/appkit\"];\n\n/**\n * Candidate paths for the server entry file, relative to cwd.\n * Checked in order; the first that exists is used.\n */\nconst SERVER_FILE_CANDIDATES = [\"server/server.ts\", \"server/index.ts\"];\n\n/**\n * Conventional directories to scan for local plugin manifests when\n * --local-plugins-dir is not set. Checked in order; each that exists is scanned.\n * Plugins found here are added to the manifest even if not imported in the server.\n */\nconst CONVENTIONAL_LOCAL_PLUGIN_DIRS = [\"plugins\", \"server\"];\n\n/**\n * Find the server entry file by checking candidate paths in order.\n *\n * @param cwd - Current working directory\n * @returns Absolute path to the server file, or null if none found\n */\nfunction findServerFile(cwd: string): string | null {\n for (const candidate of SERVER_FILE_CANDIDATES) {\n const fullPath = path.join(cwd, candidate);\n if (fs.existsSync(fullPath)) {\n return fullPath;\n }\n }\n return null;\n}\n\n/**\n * Represents a single named import extracted from the server file.\n */\ninterface ParsedImport {\n /** The imported name (or local alias if renamed) */\n name: string;\n /** The original exported name (differs from name when using `import { foo as bar }`) */\n originalName: string;\n /** The module specifier (package name or relative path) */\n source: string;\n}\n\n/**\n * Extract all named imports from the AST root using structural node traversal.\n * Handles single/double quotes, multiline imports, and aliased imports.\n *\n * @param root - AST root node\n * @returns Array of parsed imports with name, original name, and source\n */\nfunction parseImports(root: SgNode): ParsedImport[] {\n const imports: ParsedImport[] = [];\n\n // Find all import_statement nodes in the AST\n const importStatements = root.findAll({\n rule: { kind: \"import_statement\" },\n });\n\n for (const stmt of importStatements) {\n // Extract the module specifier (the string node, e.g. '@databricks/appkit')\n const sourceNode = stmt.find({ rule: { kind: \"string\" } });\n if (!sourceNode) continue;\n\n // Strip surrounding quotes from the string node text\n const source = sourceNode.text().replace(/^['\"]|['\"]$/g, \"\");\n\n // Find named_imports block: { createApp, analytics, server }\n const namedImports = stmt.find({ rule: { kind: \"named_imports\" } });\n if (!namedImports) continue;\n\n // Extract each import_specifier\n const specifiers = namedImports.findAll({\n rule: { kind: \"import_specifier\" },\n });\n\n for (const specifier of specifiers) {\n const children = specifier.children();\n if (children.length >= 3) {\n // Aliased import: `foo as bar` — children are [name, \"as\", alias]\n const originalName = children[0].text();\n const localName = children[children.length - 1].text();\n imports.push({ name: localName, originalName, source });\n } else {\n // Simple import: `foo`\n const name = specifier.text();\n imports.push({ name, originalName: name, source });\n }\n }\n }\n\n return imports;\n}\n\n/**\n * Extract local names of plugins actually used in the `plugins: [...]` array\n * passed to `createApp()`. Uses structural AST traversal to find `pair` nodes\n * with key \"plugins\" and array values containing call expressions.\n *\n * @param root - AST root node\n * @returns Set of local variable names used as plugin calls in the plugins array\n */\nfunction parsePluginUsages(root: SgNode): Set<string> {\n const usedNames = new Set<string>();\n\n // Find all property pairs in the AST\n const pairs = root.findAll({ rule: { kind: \"pair\" } });\n\n for (const pair of pairs) {\n // Check if the property key is \"plugins\"\n const key = pair.find({ rule: { kind: \"property_identifier\" } });\n if (!key || key.text() !== \"plugins\") continue;\n\n // Find the array value\n const arrayNode = pair.find({ rule: { kind: \"array\" } });\n if (!arrayNode) continue;\n\n // Iterate direct children of the array to find call expressions\n for (const child of arrayNode.children()) {\n if (child.kind() === \"call_expression\") {\n // The callee is the first child (the identifier being called)\n const callee = child.children()[0];\n if (callee?.kind() === \"identifier\") {\n usedNames.add(callee.text());\n }\n }\n }\n }\n\n return usedNames;\n}\n\n/**\n * File extensions to try when resolving a relative import to a file path.\n */\nconst RESOLVE_EXTENSIONS = [\".ts\", \".tsx\", \".js\", \".jsx\"];\n\n/**\n * Resolve a relative import source to the plugin directory containing a manifest\n * (manifest.json or manifest.js). Follows the convention that plugins live in\n * their own directory with a manifest file.\n *\n * Resolution strategy:\n * 1. If the import path is a directory, look for manifest.json/js in it\n * 2. If the import path + extension is a file, look for manifest in its parent directory\n * 3. If the import path is a directory with an index file, look for manifest in that directory\n *\n * @param importSource - The relative import specifier (e.g. \"./plugins/my-plugin\")\n * @param serverFileDir - Absolute path to the directory containing the server file\n * @returns Resolved manifest file path and type, or null if not found\n */\nfunction resolveLocalManifest(\n importSource: string,\n serverFileDir: string,\n allowJsManifest: boolean,\n projectRoot?: string,\n): ResolvedManifest | null {\n const resolved = path.resolve(serverFileDir, importSource);\n\n // Security: Reject paths that escape the project root\n const boundary = projectRoot || serverFileDir;\n if (!isWithinDirectory(resolved, boundary)) {\n console.warn(\n `Warning: Skipping import \"${importSource}\" — resolves outside the project directory`,\n );\n return null;\n }\n\n // Case 1: Import path is a directory\n if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\n return resolveManifestInDir(resolved, { allowJsManifest });\n }\n\n // Case 2: Import path + extension resolves to a file — manifest in parent dir\n for (const ext of RESOLVE_EXTENSIONS) {\n const filePath = `${resolved}${ext}`;\n if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {\n const dir = path.dirname(filePath);\n if (!isWithinDirectory(dir, boundary)) return null;\n return resolveManifestInDir(dir, { allowJsManifest });\n }\n }\n\n // Case 3: Import path is a directory with an index file\n for (const ext of RESOLVE_EXTENSIONS) {\n const indexPath = path.join(resolved, `index${ext}`);\n if (fs.existsSync(indexPath)) {\n return resolveManifestInDir(resolved, { allowJsManifest });\n }\n }\n\n return null;\n}\n\n/**\n * Discover plugin manifests from local (relative) imports in the server file.\n * Resolves each relative import to a directory and loads manifest.json or manifest.js.\n *\n * @param relativeImports - Parsed imports with relative sources (starting with . or /)\n * @param serverFileDir - Absolute path to the directory containing the server file\n * @param cwd - Current working directory (for computing relative paths in output)\n * @returns Map of plugin name to template plugin entry for local plugins\n */\nasync function discoverLocalPlugins(\n relativeImports: ParsedImport[],\n serverFileDir: string,\n cwd: string,\n allowJsManifest: boolean,\n): Promise<TemplatePluginsManifest[\"plugins\"]> {\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n\n for (const imp of relativeImports) {\n const resolved = resolveLocalManifest(\n imp.source,\n serverFileDir,\n allowJsManifest,\n cwd,\n );\n if (!resolved) continue;\n\n try {\n const relativePath = path.relative(cwd, path.dirname(resolved.path));\n const entry = await loadPluginEntry(\n resolved,\n `./${relativePath}`,\n allowJsManifest,\n );\n if (entry) plugins[entry[0]] = entry[1];\n } catch (error) {\n console.warn(\n `Warning: Failed to load manifest at ${resolved.path}:`,\n error instanceof Error ? error.message : error,\n );\n }\n }\n\n return plugins;\n}\n\n/**\n * Discover plugin manifests from a package's dist folder.\n * Looks for manifest.json or manifest.js in dist/plugins/{plugin-name}/ directories.\n *\n * @param packagePath - Path to the package in node_modules\n * @returns Array of plugin manifests found in the package\n */\nasync function discoverPluginManifests(\n packagePath: string,\n allowJsManifest: boolean,\n): Promise<PluginManifest[]> {\n const pluginsDir = path.join(packagePath, \"dist\", \"plugins\");\n const manifests: PluginManifest[] = [];\n\n if (!fs.existsSync(pluginsDir)) {\n return manifests;\n }\n\n const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const resolved = resolveManifestInDir(path.join(pluginsDir, entry.name), {\n allowJsManifest,\n });\n if (!resolved) continue;\n\n try {\n const parsed = await loadManifestFromFile(resolved.path, resolved.type, {\n allowJsManifest,\n });\n const manifest = validateManifestWithSchema(parsed, resolved.path);\n if (manifest) {\n manifests.push(manifest);\n }\n } catch (error) {\n console.warn(\n `Warning: Failed to load manifest at ${resolved.path}:`,\n error instanceof Error ? error.message : error,\n );\n }\n }\n\n return manifests;\n}\n\n/**\n * Scan node_modules for packages with plugin manifests.\n *\n * @param cwd - Current working directory to search from\n * @param packages - Set of npm package names to scan for plugin manifests\n * @returns Map of plugin name to template plugin entry\n */\nasync function scanForPlugins(\n cwd: string,\n packages: Iterable<string>,\n allowJsManifest: boolean,\n): Promise<TemplatePluginsManifest[\"plugins\"]> {\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n\n for (const packageName of packages) {\n const packagePath = path.join(cwd, \"node_modules\", packageName);\n if (!fs.existsSync(packagePath)) {\n continue;\n }\n\n const allowJsForPackage =\n allowJsManifest || shouldAllowJsManifestForPackage(packageName);\n\n const manifests = await discoverPluginManifests(\n packagePath,\n allowJsForPackage,\n );\n for (const manifest of manifests) {\n if (manifest.hidden) continue;\n plugins[manifest.name] = {\n name: manifest.name,\n displayName: manifest.displayName,\n description: manifest.description,\n package: packageName,\n resources: manifest.resources,\n ...(manifest.onSetupMessage && {\n onSetupMessage: manifest.onSetupMessage,\n }),\n } satisfies TemplatePlugin;\n }\n }\n\n return plugins;\n}\n\n/**\n * Recursively scan a directory for plugin manifests. Any directory that\n * contains manifest.json or manifest.js is treated as a plugin root; we do\n * not descend into that directory's children. Used for local plugins discovery\n * so nested paths like server/plugins/category/my-plugin are found.\n */\nasync function scanPluginsDirRecursive(\n dir: string,\n cwd: string,\n allowJsManifest: boolean,\n depth = 0,\n): Promise<TemplatePluginsManifest[\"plugins\"]> {\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n if (!fs.existsSync(dir) || depth >= MAX_SCAN_DEPTH) return plugins;\n\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n\n const pluginDir = path.join(dir, entry.name);\n const resolved = resolveManifestInDir(pluginDir, { allowJsManifest });\n\n if (resolved) {\n const pkg = `./${path.relative(cwd, pluginDir)}`;\n try {\n const pluginEntry = await loadPluginEntry(\n resolved,\n pkg,\n allowJsManifest,\n );\n if (pluginEntry) plugins[pluginEntry[0]] = pluginEntry[1];\n } catch (error) {\n console.warn(\n `Warning: Failed to load manifest at ${resolved.path}:`,\n error instanceof Error ? error.message : error,\n );\n }\n continue;\n }\n\n Object.assign(\n plugins,\n await scanPluginsDirRecursive(pluginDir, cwd, allowJsManifest, depth + 1),\n );\n }\n\n return plugins;\n}\n\n/**\n * Scan a directory for plugin manifests in direct subdirectories only.\n * Each subdirectory may contain manifest.json or manifest.js.\n * Used with --plugins-dir to discover plugins from source instead of node_modules.\n *\n * @param dir - Absolute path to the directory containing plugin subdirectories\n * @param packageName - Package name to assign to discovered plugins (used when cwd is not set)\n * @param cwd - When set, each plugin's package is set to ./<path from cwd to plugin subdir>, e.g. ./server/my-plugin\n * @returns Map of plugin name to template plugin entry\n */\nasync function scanPluginsDir(\n dir: string,\n packageName: string,\n allowJsManifest: boolean,\n cwd?: string,\n): Promise<TemplatePluginsManifest[\"plugins\"]> {\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n\n if (!fs.existsSync(dir)) return plugins;\n\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n\n const pluginDir = path.join(dir, entry.name);\n const resolved = resolveManifestInDir(pluginDir, { allowJsManifest });\n if (!resolved) continue;\n\n const pkg =\n cwd !== undefined ? `./${path.relative(cwd, pluginDir)}` : packageName;\n\n try {\n const pluginEntry = await loadPluginEntry(resolved, pkg, allowJsManifest);\n if (pluginEntry) plugins[pluginEntry[0]] = pluginEntry[1];\n } catch (error) {\n console.warn(\n `Warning: Failed to load manifest at ${resolved.path}:`,\n error instanceof Error ? error.message : error,\n );\n }\n }\n\n return plugins;\n}\n\n/**\n * Write (or preview) the template plugins manifest to disk.\n */\nfunction writeManifest(\n outputPath: string,\n { plugins }: { plugins: TemplatePluginsManifest[\"plugins\"] },\n options: { write?: boolean; silent?: boolean },\n) {\n const templateManifest: TemplatePluginsManifest = {\n $schema:\n \"https://databricks.github.io/appkit/schemas/template-plugins.schema.json\",\n version: \"1.0\",\n plugins,\n };\n\n if (options.write) {\n fs.writeFileSync(\n outputPath,\n `${JSON.stringify(templateManifest, null, 2)}\\n`,\n );\n if (!options.silent) {\n console.log(`\\n✓ Wrote ${outputPath}`);\n }\n } else if (!options.silent) {\n console.log(\"\\nTo write the manifest, run:\");\n console.log(\" npx appkit plugin sync --write\\n\");\n console.log(\"Preview:\");\n console.log(\"─\".repeat(60));\n console.log(JSON.stringify(templateManifest, null, 2));\n console.log(\"─\".repeat(60));\n }\n}\n\n/**\n * Run the plugin sync command.\n * Parses the server entry file to discover which packages to scan for plugin\n * manifests, then marks plugins that are actually used in the `plugins: [...]`\n * array as requiredByTemplate.\n */\nasync function runPluginsSync(options: {\n write?: boolean;\n output?: string;\n silent?: boolean;\n requirePlugins?: string;\n pluginsDir?: string;\n packageName?: string;\n localPluginsDir?: string;\n allowJsManifest?: boolean;\n}): Promise<void> {\n const cwd = process.cwd();\n const allowJsManifest = Boolean(options.allowJsManifest);\n const outputPath = path.resolve(cwd, options.output || \"appkit.plugins.json\");\n\n // Security: Reject output paths that escape the project root\n if (!isWithinDirectory(outputPath, cwd)) {\n console.error(\n `Error: Output path \"${options.output}\" resolves outside the project directory.`,\n );\n process.exit(1);\n }\n\n if (!options.silent) {\n console.log(\"Scanning for AppKit plugins...\\n\");\n if (allowJsManifest) {\n console.warn(\n \"Warning: --allow-js-manifest executes manifest.js/manifest.cjs files. Only use with trusted code.\",\n );\n }\n }\n\n // Step 1: Parse server file to discover imports and plugin usages\n const serverFile = findServerFile(cwd);\n let serverImports: ParsedImport[] = [];\n let pluginUsages = new Set<string>();\n\n if (serverFile) {\n if (!options.silent) {\n const relativePath = path.relative(cwd, serverFile);\n console.log(`Server entry file: ${relativePath}`);\n }\n\n const content = fs.readFileSync(serverFile, \"utf-8\");\n const lang = serverFile.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const ast = parse(lang, content);\n const root = ast.root();\n\n serverImports = parseImports(root);\n pluginUsages = parsePluginUsages(root);\n } else if (!options.silent) {\n console.log(\n \"No server entry file found. Checked:\",\n SERVER_FILE_CANDIDATES.join(\", \"),\n );\n }\n\n // Step 2: Split imports into npm packages and local (relative) imports\n const npmImports = serverImports.filter(\n (i) => !i.source.startsWith(\".\") && !i.source.startsWith(\"/\"),\n );\n const localImports = serverImports.filter(\n (i) => i.source.startsWith(\".\") || i.source.startsWith(\"/\"),\n );\n\n // Step 3: Scan for plugin manifests (--plugins-dir or node_modules)\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n\n if (options.pluginsDir) {\n const resolvedDir = path.resolve(cwd, options.pluginsDir);\n const pkgName = options.packageName ?? \"@databricks/appkit\";\n if (!options.silent) {\n console.log(`Scanning plugins directory: ${options.pluginsDir}`);\n }\n Object.assign(\n plugins,\n await scanPluginsDir(resolvedDir, pkgName, allowJsManifest),\n );\n } else {\n const npmPackages = new Set([\n ...KNOWN_PLUGIN_PACKAGES,\n ...npmImports.map((i) => i.source),\n ]);\n Object.assign(\n plugins,\n await scanForPlugins(cwd, npmPackages, allowJsManifest),\n );\n }\n\n // Step 4: Discover local plugin manifests from relative imports\n if (serverFile && localImports.length > 0) {\n const serverFileDir = path.dirname(serverFile);\n const localPlugins = await discoverLocalPlugins(\n localImports,\n serverFileDir,\n cwd,\n allowJsManifest,\n );\n Object.assign(plugins, localPlugins);\n }\n\n // Step 4b: Discover local plugins from conventional directory (or --local-plugins-dir).\n // These are included even when not imported in the server.\n const localDirsToScan: string[] = options.localPluginsDir\n ? [options.localPluginsDir]\n : CONVENTIONAL_LOCAL_PLUGIN_DIRS.filter((d) =>\n fs.existsSync(path.join(cwd, d)),\n );\n for (const dir of localDirsToScan) {\n const resolvedDir = path.resolve(cwd, dir);\n if (!fs.existsSync(resolvedDir)) continue;\n if (!options.silent) {\n console.log(`Scanning local plugins directory: ${dir}`);\n }\n const discovered = await scanPluginsDirRecursive(\n resolvedDir,\n cwd,\n allowJsManifest,\n );\n for (const [name, entry] of Object.entries(discovered)) {\n if (!plugins[name]) plugins[name] = entry;\n }\n }\n\n const pluginCount = Object.keys(plugins).length;\n\n if (pluginCount === 0) {\n if (options.silent) {\n writeManifest(outputPath, { plugins: {} }, options);\n return;\n }\n console.log(\"No plugins found.\");\n if (options.pluginsDir) {\n console.log(\n `\\nNo manifest (${allowJsManifest ? \"manifest.json or manifest.js\" : \"manifest.json\"}) found in: ${options.pluginsDir}`,\n );\n } else {\n console.log(\"\\nMake sure you have plugin packages installed.\");\n }\n process.exit(1);\n }\n\n // Step 5: Mark plugins that are imported AND used in the plugins array as mandatory.\n // For npm imports, match by package name + plugin name.\n // For local imports, resolve both paths to absolute and compare.\n const serverFileDir = serverFile ? path.dirname(serverFile) : cwd;\n\n for (const imp of serverImports) {\n if (!pluginUsages.has(imp.name)) continue;\n\n const isLocal = imp.source.startsWith(\".\") || imp.source.startsWith(\"/\");\n let plugin: TemplatePlugin | undefined;\n\n if (isLocal) {\n // Resolve the import source to an absolute path from the server file directory\n const resolvedImportDir = path.resolve(serverFileDir, imp.source);\n plugin = Object.values(plugins).find((p) => {\n if (!p.package.startsWith(\".\")) return false;\n const resolvedPluginDir = path.resolve(cwd, p.package);\n return (\n resolvedPluginDir === resolvedImportDir && p.name === imp.originalName\n );\n });\n } else {\n // npm import: direct string comparison\n plugin = Object.values(plugins).find(\n (p) => p.package === imp.source && p.name === imp.originalName,\n );\n }\n\n if (plugin) {\n plugin.requiredByTemplate = true;\n }\n }\n\n // Step 6: Apply explicit --require-plugins overrides\n if (options.requirePlugins) {\n const explicitNames = options.requirePlugins\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n for (const name of explicitNames) {\n if (plugins[name]) {\n plugins[name].requiredByTemplate = true;\n } else if (!options.silent) {\n console.warn(\n `Warning: --require-plugins referenced \"${name}\" but no such plugin was discovered`,\n );\n }\n }\n }\n\n if (!options.silent) {\n console.log(`\\nFound ${pluginCount} plugin(s):`);\n for (const [name, manifest] of Object.entries(plugins)) {\n const resourceCount =\n manifest.resources.required.length + manifest.resources.optional.length;\n const resourceInfo =\n resourceCount > 0 ? ` [${resourceCount} resource(s)]` : \"\";\n const mandatoryTag = manifest.requiredByTemplate ? \" (mandatory)\" : \"\";\n console.log(\n ` ${manifest.requiredByTemplate ? \"●\" : \"○\"} ${manifest.displayName} (${name}) from ${manifest.package}${resourceInfo}${mandatoryTag}`,\n );\n }\n }\n\n writeManifest(outputPath, { plugins }, options);\n}\n\n/** Exported for testing: path boundary check, AST parsing, trust checks. */\nexport {\n isWithinDirectory,\n parseImports,\n parsePluginUsages,\n shouldAllowJsManifestForPackage,\n};\n\nexport const pluginsSyncCommand = new Command(\"sync\")\n .description(\n \"Sync plugin manifests from installed packages into appkit.plugins.json\",\n )\n .option(\"-w, --write\", \"Write the manifest file\")\n .option(\n \"-o, --output <path>\",\n \"Output file path (default: ./appkit.plugins.json)\",\n )\n .option(\n \"-s, --silent\",\n \"Suppress output and never exit with error (for use in predev/prebuild hooks)\",\n )\n .option(\n \"--require-plugins <names>\",\n \"Comma-separated plugin names to mark as requiredByTemplate (e.g. server,analytics)\",\n )\n .option(\n \"--plugins-dir <path>\",\n \"Scan this directory for plugin subdirectories with manifest.json (instead of node_modules)\",\n )\n .option(\n \"--package-name <name>\",\n \"Package name to assign to plugins found via --plugins-dir (default: @databricks/appkit)\",\n )\n .option(\n \"--local-plugins-dir <path>\",\n \"Also scan this directory for local plugin manifests (default: plugins, server)\",\n )\n .option(\n \"--allow-js-manifest\",\n \"Allow reading manifest.js/manifest.cjs (executes code; use only with trusted plugins)\",\n )\n .action((opts) =>\n runPluginsSync(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sync.js","names":[],"sources":["../../../../../src/cli/commands/plugin/sync/sync.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Lang, parse, type SgNode } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\nimport {\n loadManifestFromFile,\n type ResolvedManifest,\n resolveManifestInDir,\n} from \"../manifest-resolve\";\nimport type {\n PluginManifest,\n TemplatePlugin,\n TemplatePluginsManifest,\n} from \"../manifest-types\";\nimport { shouldAllowJsManifestForPackage } from \"../trusted-js-manifest\";\nimport {\n formatValidationErrors,\n validateManifest,\n} from \"../validate/validate-manifest\";\n\n/**\n * Checks whether a resolved file path is within a given directory boundary.\n * Uses path.resolve + startsWith to prevent directory traversal.\n *\n * @param filePath - The path to check (will be resolved to absolute)\n * @param boundary - The directory that must contain filePath\n * @returns true if filePath is inside boundary (or equal to it)\n */\nfunction isWithinDirectory(filePath: string, boundary: string): boolean {\n const resolvedPath = path.resolve(filePath);\n const resolvedBoundary = path.resolve(boundary);\n // Append separator to avoid prefix false-positives (e.g. /foo-bar matching /foo)\n return (\n resolvedPath === resolvedBoundary ||\n resolvedPath.startsWith(`${resolvedBoundary}${path.sep}`)\n );\n}\n\n/**\n * Validates a parsed JSON object against the plugin-manifest JSON schema.\n * Returns the manifest if valid, or null and logs schema errors.\n */\nfunction validateManifestWithSchema(\n obj: unknown,\n sourcePath: string,\n): PluginManifest | null {\n const result = validateManifest(obj);\n if (result.valid && result.manifest) return result.manifest;\n if (result.errors?.length) {\n console.warn(\n `Warning: Manifest at ${sourcePath} failed schema validation:\\n${formatValidationErrors(result.errors, obj)}`,\n );\n }\n return null;\n}\n\n/** Safety limit for recursive directory scanning to prevent runaway traversal. */\nconst MAX_SCAN_DEPTH = 5;\n\n/**\n * Load and validate a resolved manifest, returning a TemplatePlugin entry or null.\n * Centralises the resolve → load → validate → build-entry pipeline used by\n * multiple discovery functions.\n */\nasync function loadPluginEntry(\n resolved: ResolvedManifest,\n pkg: string,\n allowJsManifest: boolean,\n): Promise<[string, TemplatePlugin] | null> {\n const parsed = await loadManifestFromFile(resolved.path, resolved.type, {\n allowJsManifest,\n });\n const manifest = validateManifestWithSchema(parsed, resolved.path);\n if (!manifest || manifest.hidden) return null;\n\n return [\n manifest.name,\n {\n name: manifest.name,\n displayName: manifest.displayName,\n description: manifest.description,\n package: pkg,\n resources: manifest.resources,\n ...(manifest.onSetupMessage && {\n onSetupMessage: manifest.onSetupMessage,\n }),\n },\n ];\n}\n\n/**\n * Known packages that may contain AppKit plugins.\n * Always scanned for manifests, even if not imported in the server file.\n */\nconst KNOWN_PLUGIN_PACKAGES = [\"@databricks/appkit\"];\n\n/**\n * Candidate paths for the server entry file, relative to cwd.\n * Checked in order; the first that exists is used.\n */\nconst SERVER_FILE_CANDIDATES = [\"server/server.ts\", \"server/index.ts\"];\n\n/**\n * Conventional directories to scan for local plugin manifests when\n * --local-plugins-dir is not set. Checked in order; each that exists is scanned.\n * Plugins found here are added to the manifest even if not imported in the server.\n */\nconst CONVENTIONAL_LOCAL_PLUGIN_DIRS = [\"plugins\", \"server\"];\n\n/**\n * Find the server entry file by checking candidate paths in order.\n *\n * @param cwd - Current working directory\n * @returns Absolute path to the server file, or null if none found\n */\nfunction findServerFile(cwd: string): string | null {\n for (const candidate of SERVER_FILE_CANDIDATES) {\n const fullPath = path.join(cwd, candidate);\n if (fs.existsSync(fullPath)) {\n return fullPath;\n }\n }\n return null;\n}\n\n/**\n * Represents a single named import extracted from the server file.\n */\ninterface ParsedImport {\n /** The imported name (or local alias if renamed) */\n name: string;\n /** The original exported name (differs from name when using `import { foo as bar }`) */\n originalName: string;\n /** The module specifier (package name or relative path) */\n source: string;\n}\n\n/**\n * Extract all named imports from the AST root using structural node traversal.\n * Handles single/double quotes, multiline imports, and aliased imports.\n *\n * @param root - AST root node\n * @returns Array of parsed imports with name, original name, and source\n */\nfunction parseImports(root: SgNode): ParsedImport[] {\n const imports: ParsedImport[] = [];\n\n // Find all import_statement nodes in the AST\n const importStatements = root.findAll({\n rule: { kind: \"import_statement\" },\n });\n\n for (const stmt of importStatements) {\n // Extract the module specifier (the string node, e.g. '@databricks/appkit')\n const sourceNode = stmt.find({ rule: { kind: \"string\" } });\n if (!sourceNode) continue;\n\n // Strip surrounding quotes from the string node text\n const source = sourceNode.text().replace(/^['\"]|['\"]$/g, \"\");\n\n // Find named_imports block: { createApp, analytics, server }\n const namedImports = stmt.find({ rule: { kind: \"named_imports\" } });\n if (!namedImports) continue;\n\n // Extract each import_specifier\n const specifiers = namedImports.findAll({\n rule: { kind: \"import_specifier\" },\n });\n\n for (const specifier of specifiers) {\n const children = specifier.children();\n if (children.length >= 3) {\n // Aliased import: `foo as bar` — children are [name, \"as\", alias]\n const originalName = children[0].text();\n const localName = children[children.length - 1].text();\n imports.push({ name: localName, originalName, source });\n } else {\n // Simple import: `foo`\n const name = specifier.text();\n imports.push({ name, originalName: name, source });\n }\n }\n }\n\n return imports;\n}\n\n/**\n * Extract local names of plugins actually used in the `plugins: [...]` array\n * passed to `createApp()`. Uses structural AST traversal to find `pair` nodes\n * with key \"plugins\" and array values containing call expressions.\n *\n * @param root - AST root node\n * @returns Set of local variable names used as plugin calls in the plugins array\n */\nfunction parsePluginUsages(root: SgNode): Set<string> {\n const usedNames = new Set<string>();\n\n // Find all property pairs in the AST\n const pairs = root.findAll({ rule: { kind: \"pair\" } });\n\n for (const pair of pairs) {\n // Check if the property key is \"plugins\"\n const key = pair.find({ rule: { kind: \"property_identifier\" } });\n if (!key || key.text() !== \"plugins\") continue;\n\n // Find the array value\n const arrayNode = pair.find({ rule: { kind: \"array\" } });\n if (!arrayNode) continue;\n\n // Iterate direct children of the array to find call expressions\n for (const child of arrayNode.children()) {\n if (child.kind() === \"call_expression\") {\n // The callee is the first child (the identifier being called)\n const callee = child.children()[0];\n if (callee?.kind() === \"identifier\") {\n usedNames.add(callee.text());\n }\n }\n }\n }\n\n return usedNames;\n}\n\n/**\n * File extensions to try when resolving a relative import to a file path.\n */\nconst RESOLVE_EXTENSIONS = [\".ts\", \".tsx\", \".js\", \".jsx\"];\n\n/**\n * Resolve a relative import source to the plugin directory containing a manifest\n * (manifest.json or manifest.js). Follows the convention that plugins live in\n * their own directory with a manifest file.\n *\n * Resolution strategy:\n * 1. If the import path is a directory, look for manifest.json/js in it\n * 2. If the import path + extension is a file, look for manifest in its parent directory\n * 3. If the import path is a directory with an index file, look for manifest in that directory\n *\n * @param importSource - The relative import specifier (e.g. \"./plugins/my-plugin\")\n * @param serverFileDir - Absolute path to the directory containing the server file\n * @returns Resolved manifest file path and type, or null if not found\n */\nfunction resolveLocalManifest(\n importSource: string,\n serverFileDir: string,\n allowJsManifest: boolean,\n projectRoot?: string,\n): ResolvedManifest | null {\n const resolved = path.resolve(serverFileDir, importSource);\n\n // Security: Reject paths that escape the project root\n const boundary = projectRoot || serverFileDir;\n if (!isWithinDirectory(resolved, boundary)) {\n console.warn(\n `Warning: Skipping import \"${importSource}\" — resolves outside the project directory`,\n );\n return null;\n }\n\n // Case 1: Import path is a directory\n if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\n return resolveManifestInDir(resolved, { allowJsManifest });\n }\n\n // Case 2: Import path + extension resolves to a file — manifest in parent dir\n for (const ext of RESOLVE_EXTENSIONS) {\n const filePath = `${resolved}${ext}`;\n if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {\n const dir = path.dirname(filePath);\n if (!isWithinDirectory(dir, boundary)) return null;\n return resolveManifestInDir(dir, { allowJsManifest });\n }\n }\n\n // Case 3: Import path is a directory with an index file\n for (const ext of RESOLVE_EXTENSIONS) {\n const indexPath = path.join(resolved, `index${ext}`);\n if (fs.existsSync(indexPath)) {\n return resolveManifestInDir(resolved, { allowJsManifest });\n }\n }\n\n return null;\n}\n\n/**\n * Discover plugin manifests from local (relative) imports in the server file.\n * Resolves each relative import to a directory and loads manifest.json or manifest.js.\n *\n * @param relativeImports - Parsed imports with relative sources (starting with . or /)\n * @param serverFileDir - Absolute path to the directory containing the server file\n * @param cwd - Current working directory (for computing relative paths in output)\n * @returns Map of plugin name to template plugin entry for local plugins\n */\nasync function discoverLocalPlugins(\n relativeImports: ParsedImport[],\n serverFileDir: string,\n cwd: string,\n allowJsManifest: boolean,\n): Promise<TemplatePluginsManifest[\"plugins\"]> {\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n\n for (const imp of relativeImports) {\n const resolved = resolveLocalManifest(\n imp.source,\n serverFileDir,\n allowJsManifest,\n cwd,\n );\n if (!resolved) continue;\n\n try {\n const relativePath = path.relative(cwd, path.dirname(resolved.path));\n const entry = await loadPluginEntry(\n resolved,\n `./${relativePath}`,\n allowJsManifest,\n );\n if (entry) plugins[entry[0]] = entry[1];\n } catch (error) {\n console.warn(\n `Warning: Failed to load manifest at ${resolved.path}:`,\n error instanceof Error ? error.message : error,\n );\n }\n }\n\n return plugins;\n}\n\n/**\n * Discover plugin manifests from a package's dist folder.\n * Looks for manifest.json or manifest.js in dist/plugins/{plugin-name}/ directories.\n *\n * @param packagePath - Path to the package in node_modules\n * @returns Array of plugin manifests found in the package\n */\nasync function discoverPluginManifests(\n packagePath: string,\n allowJsManifest: boolean,\n): Promise<PluginManifest[]> {\n const pluginsDir = path.join(packagePath, \"dist\", \"plugins\");\n const manifests: PluginManifest[] = [];\n\n if (!fs.existsSync(pluginsDir)) {\n return manifests;\n }\n\n const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const resolved = resolveManifestInDir(path.join(pluginsDir, entry.name), {\n allowJsManifest,\n });\n if (!resolved) continue;\n\n try {\n const parsed = await loadManifestFromFile(resolved.path, resolved.type, {\n allowJsManifest,\n });\n const manifest = validateManifestWithSchema(parsed, resolved.path);\n if (manifest) {\n manifests.push(manifest);\n }\n } catch (error) {\n console.warn(\n `Warning: Failed to load manifest at ${resolved.path}:`,\n error instanceof Error ? error.message : error,\n );\n }\n }\n\n return manifests;\n}\n\n/**\n * Scan node_modules for packages with plugin manifests.\n *\n * @param cwd - Current working directory to search from\n * @param packages - Set of npm package names to scan for plugin manifests\n * @returns Map of plugin name to template plugin entry\n */\nasync function scanForPlugins(\n cwd: string,\n packages: Iterable<string>,\n allowJsManifest: boolean,\n): Promise<TemplatePluginsManifest[\"plugins\"]> {\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n\n for (const packageName of packages) {\n const packagePath = path.join(cwd, \"node_modules\", packageName);\n if (!fs.existsSync(packagePath)) {\n continue;\n }\n\n const allowJsForPackage =\n allowJsManifest || shouldAllowJsManifestForPackage(packageName);\n\n const manifests = await discoverPluginManifests(\n packagePath,\n allowJsForPackage,\n );\n for (const manifest of manifests) {\n if (manifest.hidden) continue;\n plugins[manifest.name] = {\n name: manifest.name,\n displayName: manifest.displayName,\n description: manifest.description,\n package: packageName,\n resources: manifest.resources,\n ...(manifest.onSetupMessage && {\n onSetupMessage: manifest.onSetupMessage,\n }),\n } satisfies TemplatePlugin;\n }\n }\n\n return plugins;\n}\n\n/**\n * Recursively scan a directory for plugin manifests. Any directory that\n * contains manifest.json or manifest.js is treated as a plugin root; we do\n * not descend into that directory's children. Used for local plugins discovery\n * so nested paths like server/plugins/category/my-plugin are found.\n */\nasync function scanPluginsDirRecursive(\n dir: string,\n cwd: string,\n allowJsManifest: boolean,\n depth = 0,\n): Promise<TemplatePluginsManifest[\"plugins\"]> {\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n if (!fs.existsSync(dir) || depth >= MAX_SCAN_DEPTH) return plugins;\n\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n\n const pluginDir = path.join(dir, entry.name);\n const resolved = resolveManifestInDir(pluginDir, { allowJsManifest });\n\n if (resolved) {\n const pkg = `./${path.relative(cwd, pluginDir)}`;\n try {\n const pluginEntry = await loadPluginEntry(\n resolved,\n pkg,\n allowJsManifest,\n );\n if (pluginEntry) plugins[pluginEntry[0]] = pluginEntry[1];\n } catch (error) {\n console.warn(\n `Warning: Failed to load manifest at ${resolved.path}:`,\n error instanceof Error ? error.message : error,\n );\n }\n continue;\n }\n\n Object.assign(\n plugins,\n await scanPluginsDirRecursive(pluginDir, cwd, allowJsManifest, depth + 1),\n );\n }\n\n return plugins;\n}\n\n/**\n * Scan a directory for plugin manifests in direct subdirectories only.\n * Each subdirectory may contain manifest.json or manifest.js.\n * Used with --plugins-dir to discover plugins from source instead of node_modules.\n *\n * @param dir - Absolute path to the directory containing plugin subdirectories\n * @param packageName - Package name to assign to discovered plugins (used when cwd is not set)\n * @param cwd - When set, each plugin's package is set to ./<path from cwd to plugin subdir>, e.g. ./server/my-plugin\n * @returns Map of plugin name to template plugin entry\n */\nasync function scanPluginsDir(\n dir: string,\n packageName: string,\n allowJsManifest: boolean,\n cwd?: string,\n): Promise<TemplatePluginsManifest[\"plugins\"]> {\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n\n if (!fs.existsSync(dir)) return plugins;\n\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n\n const pluginDir = path.join(dir, entry.name);\n const resolved = resolveManifestInDir(pluginDir, { allowJsManifest });\n if (!resolved) continue;\n\n const pkg =\n cwd !== undefined ? `./${path.relative(cwd, pluginDir)}` : packageName;\n\n try {\n const pluginEntry = await loadPluginEntry(resolved, pkg, allowJsManifest);\n if (pluginEntry) plugins[pluginEntry[0]] = pluginEntry[1];\n } catch (error) {\n console.warn(\n `Warning: Failed to load manifest at ${resolved.path}:`,\n error instanceof Error ? error.message : error,\n );\n }\n }\n\n return plugins;\n}\n\n/**\n * Write (or preview) the template plugins manifest to disk.\n */\nfunction writeManifest(\n outputPath: string,\n { plugins }: { plugins: TemplatePluginsManifest[\"plugins\"] },\n options: { write?: boolean; silent?: boolean; json?: boolean },\n) {\n const templateManifest: TemplatePluginsManifest = {\n $schema:\n \"https://databricks.github.io/appkit/schemas/template-plugins.schema.json\",\n version: \"1.0\",\n plugins,\n };\n\n const serialized = JSON.stringify(templateManifest, null, 2);\n\n if (options.json) {\n console.log(serialized);\n }\n\n if (options.write) {\n fs.writeFileSync(outputPath, `${serialized}\\n`);\n if (!options.silent && !options.json) {\n console.log(`\\n✓ Wrote ${outputPath}`);\n }\n } else if (!options.silent && !options.json) {\n console.log(\"\\nTo write the manifest, run:\");\n console.log(\" npx appkit plugin sync --write\\n\");\n console.log(\"Preview:\");\n console.log(\"─\".repeat(60));\n console.log(serialized);\n console.log(\"─\".repeat(60));\n }\n}\n\n/**\n * Run the plugin sync command.\n * Parses the server entry file to discover which packages to scan for plugin\n * manifests, then marks plugins that are actually used in the `plugins: [...]`\n * array as requiredByTemplate.\n */\nasync function runPluginsSync(options: {\n write?: boolean;\n output?: string;\n silent?: boolean;\n json?: boolean;\n requirePlugins?: string;\n pluginsDir?: string;\n packageName?: string;\n localPluginsDir?: string;\n allowJsManifest?: boolean;\n}): Promise<void> {\n const cwd = process.cwd();\n const allowJsManifest = Boolean(options.allowJsManifest);\n const outputPath = path.resolve(cwd, options.output || \"appkit.plugins.json\");\n\n // Security: Reject output paths that escape the project root\n if (!isWithinDirectory(outputPath, cwd)) {\n console.error(\n `Error: Output path \"${options.output}\" resolves outside the project directory.`,\n );\n process.exit(1);\n }\n\n if (!options.silent && !options.json) {\n console.log(\"Scanning for AppKit plugins...\\n\");\n if (allowJsManifest) {\n console.warn(\n \"Warning: --allow-js-manifest executes manifest.js/manifest.cjs files. Only use with trusted code.\",\n );\n }\n }\n\n // Step 1: Parse server file to discover imports and plugin usages\n const serverFile = findServerFile(cwd);\n let serverImports: ParsedImport[] = [];\n let pluginUsages = new Set<string>();\n\n if (serverFile) {\n if (!options.silent && !options.json) {\n const relativePath = path.relative(cwd, serverFile);\n console.log(`Server entry file: ${relativePath}`);\n }\n\n const content = fs.readFileSync(serverFile, \"utf-8\");\n const lang = serverFile.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const ast = parse(lang, content);\n const root = ast.root();\n\n serverImports = parseImports(root);\n pluginUsages = parsePluginUsages(root);\n } else if (!options.silent && !options.json) {\n console.log(\n \"No server entry file found. Checked:\",\n SERVER_FILE_CANDIDATES.join(\", \"),\n );\n }\n\n // Step 2: Split imports into npm packages and local (relative) imports\n const npmImports = serverImports.filter(\n (i) => !i.source.startsWith(\".\") && !i.source.startsWith(\"/\"),\n );\n const localImports = serverImports.filter(\n (i) => i.source.startsWith(\".\") || i.source.startsWith(\"/\"),\n );\n\n // Step 3: Scan for plugin manifests (--plugins-dir or node_modules)\n const plugins: TemplatePluginsManifest[\"plugins\"] = {};\n\n if (options.pluginsDir) {\n const resolvedDir = path.resolve(cwd, options.pluginsDir);\n const pkgName = options.packageName ?? \"@databricks/appkit\";\n if (!options.silent && !options.json) {\n console.log(`Scanning plugins directory: ${options.pluginsDir}`);\n }\n Object.assign(\n plugins,\n await scanPluginsDir(resolvedDir, pkgName, allowJsManifest),\n );\n } else {\n const npmPackages = new Set([\n ...KNOWN_PLUGIN_PACKAGES,\n ...npmImports.map((i) => i.source),\n ]);\n Object.assign(\n plugins,\n await scanForPlugins(cwd, npmPackages, allowJsManifest),\n );\n }\n\n // Step 4: Discover local plugin manifests from relative imports\n if (serverFile && localImports.length > 0) {\n const serverFileDir = path.dirname(serverFile);\n const localPlugins = await discoverLocalPlugins(\n localImports,\n serverFileDir,\n cwd,\n allowJsManifest,\n );\n Object.assign(plugins, localPlugins);\n }\n\n // Step 4b: Discover local plugins from conventional directory (or --local-plugins-dir).\n // These are included even when not imported in the server.\n const localDirsToScan: string[] = options.localPluginsDir\n ? [options.localPluginsDir]\n : CONVENTIONAL_LOCAL_PLUGIN_DIRS.filter((d) =>\n fs.existsSync(path.join(cwd, d)),\n );\n for (const dir of localDirsToScan) {\n const resolvedDir = path.resolve(cwd, dir);\n if (!fs.existsSync(resolvedDir)) continue;\n if (!options.silent && !options.json) {\n console.log(`Scanning local plugins directory: ${dir}`);\n }\n const discovered = await scanPluginsDirRecursive(\n resolvedDir,\n cwd,\n allowJsManifest,\n );\n for (const [name, entry] of Object.entries(discovered)) {\n if (!plugins[name]) plugins[name] = entry;\n }\n }\n\n const pluginCount = Object.keys(plugins).length;\n\n if (pluginCount === 0) {\n if (options.silent || options.json) {\n writeManifest(outputPath, { plugins: {} }, options);\n if (options.silent) return;\n process.exit(1);\n }\n console.log(\"No plugins found.\");\n if (options.pluginsDir) {\n console.log(\n `\\nNo manifest (${allowJsManifest ? \"manifest.json or manifest.js\" : \"manifest.json\"}) found in: ${options.pluginsDir}`,\n );\n } else {\n console.log(\n \"\\nMake sure you have plugin packages installed, or specify a directory:\",\n );\n console.log(\" appkit plugin sync --plugins-dir <path>\");\n }\n process.exit(1);\n }\n\n // Step 5: Mark plugins that are imported AND used in the plugins array as mandatory.\n // For npm imports, match by package name + plugin name.\n // For local imports, resolve both paths to absolute and compare.\n const serverFileDir = serverFile ? path.dirname(serverFile) : cwd;\n\n for (const imp of serverImports) {\n if (!pluginUsages.has(imp.name)) continue;\n\n const isLocal = imp.source.startsWith(\".\") || imp.source.startsWith(\"/\");\n let plugin: TemplatePlugin | undefined;\n\n if (isLocal) {\n // Resolve the import source to an absolute path from the server file directory\n const resolvedImportDir = path.resolve(serverFileDir, imp.source);\n plugin = Object.values(plugins).find((p) => {\n if (!p.package.startsWith(\".\")) return false;\n const resolvedPluginDir = path.resolve(cwd, p.package);\n return (\n resolvedPluginDir === resolvedImportDir && p.name === imp.originalName\n );\n });\n } else {\n // npm import: direct string comparison\n plugin = Object.values(plugins).find(\n (p) => p.package === imp.source && p.name === imp.originalName,\n );\n }\n\n if (plugin) {\n plugin.requiredByTemplate = true;\n }\n }\n\n // Step 6: Apply explicit --require-plugins overrides\n if (options.requirePlugins) {\n const explicitNames = options.requirePlugins\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n for (const name of explicitNames) {\n if (plugins[name]) {\n plugins[name].requiredByTemplate = true;\n } else if (!options.silent) {\n console.warn(\n `Warning: --require-plugins referenced \"${name}\" but no such plugin was discovered`,\n );\n }\n }\n }\n\n if (!options.silent && !options.json) {\n console.log(`\\nFound ${pluginCount} plugin(s):`);\n for (const [name, manifest] of Object.entries(plugins)) {\n const resourceCount =\n manifest.resources.required.length + manifest.resources.optional.length;\n const resourceInfo =\n resourceCount > 0 ? ` [${resourceCount} resource(s)]` : \"\";\n const mandatoryTag = manifest.requiredByTemplate ? \" (mandatory)\" : \"\";\n console.log(\n ` ${manifest.requiredByTemplate ? \"●\" : \"○\"} ${manifest.displayName} (${name}) from ${manifest.package}${resourceInfo}${mandatoryTag}`,\n );\n }\n }\n\n writeManifest(outputPath, { plugins }, options);\n}\n\n/** Exported for testing: path boundary check, AST parsing, trust checks. */\nexport {\n isWithinDirectory,\n parseImports,\n parsePluginUsages,\n shouldAllowJsManifestForPackage,\n};\n\nexport const pluginsSyncCommand = new Command(\"sync\")\n .description(\n \"Sync plugin manifests from installed packages into appkit.plugins.json\",\n )\n .option(\"-w, --write\", \"Write the manifest file\")\n .option(\n \"-o, --output <path>\",\n \"Output file path (default: ./appkit.plugins.json)\",\n )\n .option(\n \"-s, --silent\",\n \"Suppress output and never exit with error (for use in predev/prebuild hooks)\",\n )\n .option(\n \"--require-plugins <names>\",\n \"Comma-separated plugin names to mark as requiredByTemplate (e.g. server,analytics)\",\n )\n .option(\n \"--plugins-dir <path>\",\n \"Scan this directory for plugin subdirectories with manifest.json (instead of node_modules)\",\n )\n .option(\n \"--package-name <name>\",\n \"Package name to assign to plugins found via --plugins-dir (default: @databricks/appkit)\",\n )\n .option(\n \"--local-plugins-dir <path>\",\n \"Also scan this directory for local plugin manifests (default: plugins, server)\",\n )\n .option(\n \"--allow-js-manifest\",\n \"Allow reading manifest.js/manifest.cjs (executes code; use only with trusted plugins)\",\n )\n .option(\"--json\", \"Output manifest as JSON to stdout\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit plugin sync\n $ appkit plugin sync --write\n $ appkit plugin sync --write --require-plugins server,analytics\n $ appkit plugin sync --write --plugins-dir src/plugins --package-name @my/pkg\n $ appkit plugin sync --json\n $ appkit plugin sync --silent`,\n )\n .action((opts) =>\n runPluginsSync(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}
|