@databricks/appkit 0.47.0 → 0.48.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.
@@ -1,6 +1,6 @@
1
1
  //#region package.json
2
2
  var name = "@databricks/appkit";
3
- var version = "0.47.0";
3
+ var version = "0.48.0";
4
4
 
5
5
  //#endregion
6
6
  export { name, version };
@@ -26,10 +26,17 @@ const rules = [
26
26
  id: "no-parse-float-without-validation",
27
27
  pattern: "parseFloat($X).toFixed($Y)",
28
28
  message: "parseFloat can return NaN. Validate input or use toNumber() helper from shared/types.ts."
29
+ },
30
+ {
31
+ id: "no-variants-in-prod",
32
+ pattern: "<Variants $$$P>$$$C</Variants>",
33
+ message: "<Variants> is a development-only variant picker and must not be shipped. Finalize the chosen <Variant> before deploying.",
34
+ includeTests: false
29
35
  }
30
36
  ];
31
- function isTestFile(filePath) {
32
- return /\.(test|spec)\.(ts|tsx)$/.test(filePath) || filePath.includes("/tests/");
37
+ function isTestFile(filePath, rootDir) {
38
+ const rel = path.relative(rootDir, filePath);
39
+ return /\.(test|spec)\.(ts|tsx)$/.test(rel) || /(^|[/\\])tests[/\\]/.test(rel);
33
40
  }
34
41
  function findTsFiles(dir, files = []) {
35
42
  const entries = fs.readdirSync(dir, { withFileTypes: true });
@@ -47,11 +54,11 @@ function findTsFiles(dir, files = []) {
47
54
  }
48
55
  return files;
49
56
  }
50
- function lintFile(filePath, rules) {
57
+ function lintFile(filePath, rules, rootDir) {
51
58
  const violations = [];
52
59
  const content = fs.readFileSync(filePath, "utf-8");
53
60
  const lang = filePath.endsWith(".tsx") ? Lang.Tsx : Lang.TypeScript;
54
- const testFile = isTestFile(filePath);
61
+ const testFile = isTestFile(filePath, rootDir);
55
62
  const root = parse(lang, content).root();
56
63
  for (const rule of rules) {
57
64
  if (testFile && rule.includeTests === false) continue;
@@ -81,7 +88,7 @@ function runLint() {
81
88
  console.log(`Scanning ${files.length} TypeScript files...\n`);
82
89
  const allViolations = [];
83
90
  for (const file of files) {
84
- const violations = lintFile(file, rules);
91
+ const violations = lintFile(file, rules, rootDir);
85
92
  allViolations.push(...violations);
86
93
  }
87
94
  if (allViolations.length === 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"lint.js","names":[],"sources":["../../../src/cli/commands/lint.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Lang, parse } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\n\ninterface Rule {\n id: string;\n pattern: string;\n message: string;\n includeTests?: boolean;\n filter?: (code: string) => boolean;\n}\n\nconst rules: Rule[] = [\n {\n id: \"no-double-type-assertion\",\n pattern: \"$X as unknown as $Y\",\n message:\n \"Avoid double type assertion (as unknown as). Use proper type guards or fix the source type.\",\n },\n {\n id: \"no-as-any\",\n pattern: \"$X as any\",\n message:\n 'Avoid \"as any\" type assertion. Use proper typing or unknown with type guards.',\n includeTests: false, // acceptable in test mocks\n },\n {\n id: \"no-array-index-key\",\n pattern: \"key={$IDX}\",\n message:\n \"Avoid using array index as React key. Use a stable unique identifier.\",\n filter: (code) => /key=\\{(idx|index|i)\\}/.test(code),\n },\n {\n id: \"no-parse-float-without-validation\",\n pattern: \"parseFloat($X).toFixed($Y)\",\n message:\n \"parseFloat can return NaN. Validate input or use toNumber() helper from shared/types.ts.\",\n },\n];\n\nfunction isTestFile(filePath: string): boolean {\n return (\n /\\.(test|spec)\\.(ts|tsx)$/.test(filePath) || filePath.includes(\"/tests/\")\n );\n}\n\nfunction findTsFiles(dir: string, files: string[] = []): string[] {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n if ([\"node_modules\", \"dist\", \"build\", \".git\"].includes(entry.name))\n continue;\n findTsFiles(fullPath, files);\n } else if (entry.isFile() && /\\.(ts|tsx)$/.test(entry.name)) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\ninterface Violation {\n file: string;\n line: number;\n column: number;\n rule: string;\n message: string;\n code: string;\n}\n\nfunction lintFile(filePath: string, rules: Rule[]): Violation[] {\n const violations: Violation[] = [];\n const content = fs.readFileSync(filePath, \"utf-8\");\n const lang = filePath.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const testFile = isTestFile(filePath);\n\n const ast = parse(lang, content);\n const root = ast.root();\n\n for (const rule of rules) {\n // skip rules that don't apply to test files\n if (testFile && rule.includeTests === false) continue;\n\n const matches = root.findAll(rule.pattern);\n\n for (const match of matches) {\n const code = match.text();\n\n if (rule.filter && !rule.filter(code)) continue;\n\n const range = match.range();\n violations.push({\n file: filePath,\n line: range.start.line + 1,\n column: range.start.column + 1,\n rule: rule.id,\n message: rule.message,\n code: code.length > 80 ? `${code.slice(0, 77)}...` : code,\n });\n }\n }\n\n return violations;\n}\n\n/**\n * Lint command implementation\n */\nfunction runLint() {\n const rootDir = process.cwd();\n const files = findTsFiles(rootDir);\n\n console.log(`Scanning ${files.length} TypeScript files...\\n`);\n\n const allViolations: Violation[] = [];\n\n for (const file of files) {\n const violations = lintFile(file, rules);\n allViolations.push(...violations);\n }\n\n if (allViolations.length === 0) {\n console.log(\"No ast-grep lint violations found.\");\n process.exit(0);\n }\n\n console.log(`Found ${allViolations.length} violation(s):\\n`);\n\n for (const v of allViolations) {\n const relPath = path.relative(rootDir, v.file);\n console.log(`${relPath}:${v.line}:${v.column}`);\n console.log(` ${v.rule}: ${v.message}`);\n console.log(` > ${v.code}\\n`);\n }\n\n process.exit(1);\n}\n\nexport const lintCommand = new Command(\"lint\")\n .description(\"Run AST-based linting on TypeScript files\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit lint`,\n )\n .action(runLint);\n"],"mappings":";;;;;;AAaA,MAAM,QAAgB;CACpB;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACH;CACD;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACF,cAAc;EACf;CACD;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACF,SAAS,SAAS,wBAAwB,KAAK,KAAK;EACrD;CACD;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACH;CACF;AAED,SAAS,WAAW,UAA2B;AAC7C,QACE,2BAA2B,KAAK,SAAS,IAAI,SAAS,SAAS,UAAU;;AAI7E,SAAS,YAAY,KAAa,QAAkB,EAAE,EAAY;CAChE,MAAM,UAAU,GAAG,YAAY,KAAK,EAAE,eAAe,MAAM,CAAC;AAE5D,MAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,WAAW,KAAK,KAAK,KAAK,MAAM,KAAK;AAE3C,MAAI,MAAM,aAAa,EAAE;AACvB,OAAI;IAAC;IAAgB;IAAQ;IAAS;IAAO,CAAC,SAAS,MAAM,KAAK,CAChE;AACF,eAAY,UAAU,MAAM;aACnB,MAAM,QAAQ,IAAI,cAAc,KAAK,MAAM,KAAK,CACzD,OAAM,KAAK,SAAS;;AAIxB,QAAO;;AAYT,SAAS,SAAS,UAAkB,OAA4B;CAC9D,MAAM,aAA0B,EAAE;CAClC,MAAM,UAAU,GAAG,aAAa,UAAU,QAAQ;CAClD,MAAM,OAAO,SAAS,SAAS,OAAO,GAAG,KAAK,MAAM,KAAK;CACzD,MAAM,WAAW,WAAW,SAAS;CAGrC,MAAM,OADM,MAAM,MAAM,QAAQ,CACf,MAAM;AAEvB,MAAK,MAAM,QAAQ,OAAO;AAExB,MAAI,YAAY,KAAK,iBAAiB,MAAO;EAE7C,MAAM,UAAU,KAAK,QAAQ,KAAK,QAAQ;AAE1C,OAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,OAAO,MAAM,MAAM;AAEzB,OAAI,KAAK,UAAU,CAAC,KAAK,OAAO,KAAK,CAAE;GAEvC,MAAM,QAAQ,MAAM,OAAO;AAC3B,cAAW,KAAK;IACd,MAAM;IACN,MAAM,MAAM,MAAM,OAAO;IACzB,QAAQ,MAAM,MAAM,SAAS;IAC7B,MAAM,KAAK;IACX,SAAS,KAAK;IACd,MAAM,KAAK,SAAS,KAAK,GAAG,KAAK,MAAM,GAAG,GAAG,CAAC,OAAO;IACtD,CAAC;;;AAIN,QAAO;;;;;AAMT,SAAS,UAAU;CACjB,MAAM,UAAU,QAAQ,KAAK;CAC7B,MAAM,QAAQ,YAAY,QAAQ;AAElC,SAAQ,IAAI,YAAY,MAAM,OAAO,wBAAwB;CAE7D,MAAM,gBAA6B,EAAE;AAErC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,aAAa,SAAS,MAAM,MAAM;AACxC,gBAAc,KAAK,GAAG,WAAW;;AAGnC,KAAI,cAAc,WAAW,GAAG;AAC9B,UAAQ,IAAI,qCAAqC;AACjD,UAAQ,KAAK,EAAE;;AAGjB,SAAQ,IAAI,SAAS,cAAc,OAAO,kBAAkB;AAE5D,MAAK,MAAM,KAAK,eAAe;EAC7B,MAAM,UAAU,KAAK,SAAS,SAAS,EAAE,KAAK;AAC9C,UAAQ,IAAI,GAAG,QAAQ,GAAG,EAAE,KAAK,GAAG,EAAE,SAAS;AAC/C,UAAQ,IAAI,KAAK,EAAE,KAAK,IAAI,EAAE,UAAU;AACxC,UAAQ,IAAI,OAAO,EAAE,KAAK,IAAI;;AAGhC,SAAQ,KAAK,EAAE;;AAGjB,MAAa,cAAc,IAAI,QAAQ,OAAO,CAC3C,YAAY,4CAA4C,CACxD,YACC,SACA;;iBAGD,CACA,OAAO,QAAQ"}
1
+ {"version":3,"file":"lint.js","names":[],"sources":["../../../src/cli/commands/lint.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Lang, parse } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\n\ninterface Rule {\n id: string;\n pattern: string;\n message: string;\n includeTests?: boolean;\n filter?: (code: string) => boolean;\n}\n\nconst rules: Rule[] = [\n {\n id: \"no-double-type-assertion\",\n pattern: \"$X as unknown as $Y\",\n message:\n \"Avoid double type assertion (as unknown as). Use proper type guards or fix the source type.\",\n },\n {\n id: \"no-as-any\",\n pattern: \"$X as any\",\n message:\n 'Avoid \"as any\" type assertion. Use proper typing or unknown with type guards.',\n includeTests: false, // acceptable in test mocks\n },\n {\n id: \"no-array-index-key\",\n pattern: \"key={$IDX}\",\n message:\n \"Avoid using array index as React key. Use a stable unique identifier.\",\n filter: (code) => /key=\\{(idx|index|i)\\}/.test(code),\n },\n {\n id: \"no-parse-float-without-validation\",\n pattern: \"parseFloat($X).toFixed($Y)\",\n message:\n \"parseFloat can return NaN. Validate input or use toNumber() helper from shared/types.ts.\",\n },\n {\n // <Variants> is the dev-only variant picker from @databricks/appkit-ui,\n // meant to drive the local edit loop. Finalize the chosen variant before\n // deploying so the picker chrome never reaches production.\n id: \"no-variants-in-prod\",\n pattern: \"<Variants $$$P>$$$C</Variants>\",\n message:\n \"<Variants> is a development-only variant picker and must not be shipped. Finalize the chosen <Variant> before deploying.\",\n includeTests: false,\n },\n];\n\nfunction isTestFile(filePath: string, rootDir: string): boolean {\n // Relative to scan root: an ancestor `tests` dir must not mark the whole project as tests.\n const rel = path.relative(rootDir, filePath);\n return (\n /\\.(test|spec)\\.(ts|tsx)$/.test(rel) || /(^|[/\\\\])tests[/\\\\]/.test(rel)\n );\n}\n\nfunction findTsFiles(dir: string, files: string[] = []): string[] {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n if ([\"node_modules\", \"dist\", \"build\", \".git\"].includes(entry.name))\n continue;\n findTsFiles(fullPath, files);\n } else if (entry.isFile() && /\\.(ts|tsx)$/.test(entry.name)) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\ninterface Violation {\n file: string;\n line: number;\n column: number;\n rule: string;\n message: string;\n code: string;\n}\n\nfunction lintFile(\n filePath: string,\n rules: Rule[],\n rootDir: string,\n): Violation[] {\n const violations: Violation[] = [];\n const content = fs.readFileSync(filePath, \"utf-8\");\n const lang = filePath.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const testFile = isTestFile(filePath, rootDir);\n\n const ast = parse(lang, content);\n const root = ast.root();\n\n for (const rule of rules) {\n // skip rules that don't apply to test files\n if (testFile && rule.includeTests === false) continue;\n\n const matches = root.findAll(rule.pattern);\n\n for (const match of matches) {\n const code = match.text();\n\n if (rule.filter && !rule.filter(code)) continue;\n\n const range = match.range();\n violations.push({\n file: filePath,\n line: range.start.line + 1,\n column: range.start.column + 1,\n rule: rule.id,\n message: rule.message,\n code: code.length > 80 ? `${code.slice(0, 77)}...` : code,\n });\n }\n }\n\n return violations;\n}\n\n/**\n * Lint command implementation\n */\nfunction runLint() {\n const rootDir = process.cwd();\n const files = findTsFiles(rootDir);\n\n console.log(`Scanning ${files.length} TypeScript files...\\n`);\n\n const allViolations: Violation[] = [];\n\n for (const file of files) {\n const violations = lintFile(file, rules, rootDir);\n allViolations.push(...violations);\n }\n\n if (allViolations.length === 0) {\n console.log(\"No ast-grep lint violations found.\");\n process.exit(0);\n }\n\n console.log(`Found ${allViolations.length} violation(s):\\n`);\n\n for (const v of allViolations) {\n const relPath = path.relative(rootDir, v.file);\n console.log(`${relPath}:${v.line}:${v.column}`);\n console.log(` ${v.rule}: ${v.message}`);\n console.log(` > ${v.code}\\n`);\n }\n\n process.exit(1);\n}\n\nexport const lintCommand = new Command(\"lint\")\n .description(\"Run AST-based linting on TypeScript files\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit lint`,\n )\n .action(runLint);\n"],"mappings":";;;;;;AAaA,MAAM,QAAgB;CACpB;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACH;CACD;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACF,cAAc;EACf;CACD;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACF,SAAS,SAAS,wBAAwB,KAAK,KAAK;EACrD;CACD;EACE,IAAI;EACJ,SAAS;EACT,SACE;EACH;CACD;EAIE,IAAI;EACJ,SAAS;EACT,SACE;EACF,cAAc;EACf;CACF;AAED,SAAS,WAAW,UAAkB,SAA0B;CAE9D,MAAM,MAAM,KAAK,SAAS,SAAS,SAAS;AAC5C,QACE,2BAA2B,KAAK,IAAI,IAAI,sBAAsB,KAAK,IAAI;;AAI3E,SAAS,YAAY,KAAa,QAAkB,EAAE,EAAY;CAChE,MAAM,UAAU,GAAG,YAAY,KAAK,EAAE,eAAe,MAAM,CAAC;AAE5D,MAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,WAAW,KAAK,KAAK,KAAK,MAAM,KAAK;AAE3C,MAAI,MAAM,aAAa,EAAE;AACvB,OAAI;IAAC;IAAgB;IAAQ;IAAS;IAAO,CAAC,SAAS,MAAM,KAAK,CAChE;AACF,eAAY,UAAU,MAAM;aACnB,MAAM,QAAQ,IAAI,cAAc,KAAK,MAAM,KAAK,CACzD,OAAM,KAAK,SAAS;;AAIxB,QAAO;;AAYT,SAAS,SACP,UACA,OACA,SACa;CACb,MAAM,aAA0B,EAAE;CAClC,MAAM,UAAU,GAAG,aAAa,UAAU,QAAQ;CAClD,MAAM,OAAO,SAAS,SAAS,OAAO,GAAG,KAAK,MAAM,KAAK;CACzD,MAAM,WAAW,WAAW,UAAU,QAAQ;CAG9C,MAAM,OADM,MAAM,MAAM,QAAQ,CACf,MAAM;AAEvB,MAAK,MAAM,QAAQ,OAAO;AAExB,MAAI,YAAY,KAAK,iBAAiB,MAAO;EAE7C,MAAM,UAAU,KAAK,QAAQ,KAAK,QAAQ;AAE1C,OAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,OAAO,MAAM,MAAM;AAEzB,OAAI,KAAK,UAAU,CAAC,KAAK,OAAO,KAAK,CAAE;GAEvC,MAAM,QAAQ,MAAM,OAAO;AAC3B,cAAW,KAAK;IACd,MAAM;IACN,MAAM,MAAM,MAAM,OAAO;IACzB,QAAQ,MAAM,MAAM,SAAS;IAC7B,MAAM,KAAK;IACX,SAAS,KAAK;IACd,MAAM,KAAK,SAAS,KAAK,GAAG,KAAK,MAAM,GAAG,GAAG,CAAC,OAAO;IACtD,CAAC;;;AAIN,QAAO;;;;;AAMT,SAAS,UAAU;CACjB,MAAM,UAAU,QAAQ,KAAK;CAC7B,MAAM,QAAQ,YAAY,QAAQ;AAElC,SAAQ,IAAI,YAAY,MAAM,OAAO,wBAAwB;CAE7D,MAAM,gBAA6B,EAAE;AAErC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,aAAa,SAAS,MAAM,OAAO,QAAQ;AACjD,gBAAc,KAAK,GAAG,WAAW;;AAGnC,KAAI,cAAc,WAAW,GAAG;AAC9B,UAAQ,IAAI,qCAAqC;AACjD,UAAQ,KAAK,EAAE;;AAGjB,SAAQ,IAAI,SAAS,cAAc,OAAO,kBAAkB;AAE5D,MAAK,MAAM,KAAK,eAAe;EAC7B,MAAM,UAAU,KAAK,SAAS,SAAS,EAAE,KAAK;AAC9C,UAAQ,IAAI,GAAG,QAAQ,GAAG,EAAE,KAAK,GAAG,EAAE,SAAS;AAC/C,UAAQ,IAAI,KAAK,EAAE,KAAK,IAAI,EAAE,UAAU;AACxC,UAAQ,IAAI,OAAO,EAAE,KAAK,IAAI;;AAGhC,SAAQ,KAAK,EAAE;;AAGjB,MAAa,cAAc,IAAI,QAAQ,OAAO,CAC3C,YAAY,4CAA4C,CACxD,YACC,SACA;;iBAGD,CACA,OAAO,QAAQ"}
@@ -1 +1 @@
1
- {"version":3,"file":"appkit.d.ts","names":[],"sources":["../../src/core/appkit.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAsTsB,SAAA,WACV,UAAA,CAAW,iBAAA,qBAAA,CAErB,MAAA;EACE,OAAA,GAAU,CAAA;EACV,SAAA,GAAY,eAAA;EACZ,KAAA,GAAQ,WAAA;EACR,MAAA,GAAS,eAAA;EACT,cAAA,IAAkB,MAAA,EAAQ,SAAA,CAAU,CAAA,aAAc,OAAA;EAClD,wBAAA;AAAA,IAED,OAAA,CAAQ,SAAA,CAAU,CAAA"}
1
+ {"version":3,"file":"appkit.d.ts","names":[],"sources":["../../src/core/appkit.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqXsB,SAAA,WACV,UAAA,CAAW,iBAAA,qBAAA,CAErB,MAAA;EACE,OAAA,GAAU,CAAA;EACV,SAAA,GAAY,eAAA;EACZ,KAAA,GAAQ,WAAA;EACR,MAAA,GAAS,eAAA;EACT,cAAA,IAAkB,MAAA,EAAQ,SAAA,CAAU,CAAA,aAAc,OAAA;EAClD,wBAAA;AAAA,IAED,OAAA,CAAQ,SAAA,CAAU,CAAA"}
@@ -9,6 +9,7 @@ import { isInternalTelemetryEnabled } from "../internal-telemetry/config.js";
9
9
  import { TelemetryReporter } from "../internal-telemetry/reporter.js";
10
10
  import "../internal-telemetry/index.js";
11
11
  import { isPlainObject } from "../plugin/plugin.js";
12
+ import { uiVariants } from "../plugins/ui-variants/index.js";
12
13
  import { ResourceType } from "../registry/types.generated.js";
13
14
  import { ResourceRegistry } from "../registry/resource-registry.js";
14
15
  import "../registry/index.js";
@@ -100,7 +101,8 @@ var AppKit = class AppKit {
100
101
  static async _createApp(config = {}) {
101
102
  TelemetryManager.initialize(config?.telemetry);
102
103
  await CacheManager.getInstance(config?.cache);
103
- const rawPlugins = config.plugins;
104
+ const withDefaults = AppKit.withDefaultPlugins(config.plugins);
105
+ const rawPlugins = AppKit.filterDevOnlyPlugins(withDefaults);
104
106
  const registry = new ResourceRegistry();
105
107
  registry.collectResources(rawPlugins);
106
108
  const needsWarehouse = registry.getRequired().some((r) => r.type === ResourceType.SQL_WAREHOUSE);
@@ -132,6 +134,42 @@ var AppKit = class AppKit {
132
134
  reporter.start();
133
135
  reporter.sendStartup().catch(() => {});
134
136
  }
137
+ /**
138
+ * Injects framework-owned default plugins the app author shouldn't have to
139
+ * register by hand. Runs before {@link filterDevOnlyPlugins}, so a default
140
+ * that is itself `devOnly` (like `ui-variants`) is present in dev and stripped
141
+ * in prod through the exact same guard as any user plugin — never alive in a
142
+ * deployed app.
143
+ *
144
+ * Currently injects the dev-only `ui-variants` recorder that backs the
145
+ * `<Variants>` UI picker, so neither the developer nor their coding agent has
146
+ * to remember to add it. If the app already registered it explicitly, that
147
+ * entry wins and no duplicate is added.
148
+ */
149
+ static withDefaultPlugins(plugins) {
150
+ const list = plugins ?? [];
151
+ const missing = [uiVariants()].filter((def) => !list.some((p) => p?.name === def.name));
152
+ return [...list, ...missing];
153
+ }
154
+ /**
155
+ * Drops plugins whose manifest declares `devOnly: true` unless
156
+ * `NODE_ENV === "development"`. Runs before resource collection so a skipped
157
+ * plugin is never constructed, never injects routes, and has its resource
158
+ * requirements ignored — the framework, not the app author, enforces that
159
+ * dev-only tooling can never run in a deployed app.
160
+ *
161
+ * This is the primary guard; plugins that expose a mutating dev endpoint
162
+ * should still keep an in-handler `NODE_ENV` check as fail-safe defense
163
+ * against being mounted through a path that bypasses this filter.
164
+ */
165
+ static filterDevOnlyPlugins(plugins) {
166
+ if (!plugins || process.env.NODE_ENV === "development") return plugins;
167
+ return plugins.filter((pluginData) => {
168
+ const isDevOnly = pluginData?.plugin?.manifest?.devOnly === true;
169
+ if (isDevOnly) logger.debug("Skipping dev-only plugin %s (NODE_ENV=%s)", pluginData?.name ?? "unknown", process.env.NODE_ENV ?? "<unset>");
170
+ return !isDevOnly;
171
+ });
172
+ }
135
173
  static preparePlugins(plugins) {
136
174
  const result = {};
137
175
  for (const currentPlugin of plugins) result[currentPlugin.name] = {
@@ -1 +1 @@
1
- {"version":3,"file":"appkit.js","names":["#context","#pluginInstances","#setupPromises","productVersion"],"sources":["../../src/core/appkit.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type {\n BasePlugin,\n CacheConfig,\n InputPluginMap,\n OptionalConfigPluginDef,\n PluginConstructor,\n PluginData,\n PluginMap,\n} from \"shared\";\nimport { version as productVersion } from \"../../package.json\";\nimport { CacheManager } from \"../cache\";\nimport { ServiceContext } from \"../context\";\nimport {\n isInternalTelemetryEnabled,\n TelemetryReporter,\n} from \"../internal-telemetry\";\nimport { createLogger } from \"../logging/logger\";\nimport { isPlainObject } from \"../plugin/plugin\";\nimport { ResourceRegistry, ResourceType } from \"../registry\";\nimport type { TelemetryConfig } from \"../telemetry\";\nimport { TelemetryManager } from \"../telemetry\";\nimport { LifecycleManager } from \"./lifecycle-manager\";\nimport { isToolProvider, PluginContext } from \"./plugin-context\";\n\nconst logger = createLogger(\"appkit\");\n\nexport class AppKit<TPlugins extends InputPluginMap> {\n #pluginInstances: Record<string, BasePlugin> = {};\n #setupPromises: Promise<void>[] = [];\n #context: PluginContext;\n\n private constructor(config: { plugins: TPlugins }) {\n const { plugins, ...globalConfig } = config;\n\n this.#context = new PluginContext();\n\n const pluginEntries = Object.entries(plugins);\n\n const corePlugins = pluginEntries.filter(([_, p]) => {\n return (p?.plugin?.phase ?? \"normal\") === \"core\";\n });\n const normalPlugins = pluginEntries.filter(\n ([_, p]) => (p?.plugin?.phase ?? \"normal\") === \"normal\",\n );\n const deferredPlugins = pluginEntries.filter(\n ([_, p]) => (p?.plugin?.phase ?? \"normal\") === \"deferred\",\n );\n\n for (const [name, pluginData] of corePlugins) {\n if (pluginData) {\n this.createAndRegisterPlugin(globalConfig, name, pluginData, {\n context: this.#context,\n });\n }\n }\n\n for (const [name, pluginData] of normalPlugins) {\n if (pluginData) {\n this.createAndRegisterPlugin(globalConfig, name, pluginData, {\n context: this.#context,\n });\n }\n }\n\n for (const [name, pluginData] of deferredPlugins) {\n if (pluginData) {\n this.createAndRegisterPlugin(globalConfig, name, pluginData, {\n context: this.#context,\n });\n }\n }\n }\n\n private createAndRegisterPlugin<T extends PluginConstructor>(\n config: Omit<{ plugins: TPlugins }, \"plugins\">,\n name: string,\n pluginData: OptionalConfigPluginDef<T>,\n extraData?: Record<string, unknown>,\n ) {\n const { plugin: Plugin, config: pluginConfig } = pluginData;\n const baseConfig = {\n ...config,\n ...Plugin.DEFAULT_CONFIG,\n ...pluginConfig,\n name,\n ...extraData,\n };\n const pluginInstance = new Plugin(baseConfig);\n\n if (typeof pluginInstance.attachContext === \"function\") {\n pluginInstance.attachContext({\n context: this.#context,\n telemetryConfig: baseConfig.telemetry,\n });\n }\n\n this.#pluginInstances[name] = pluginInstance;\n\n this.#context.registerPlugin(name, pluginInstance);\n if (isToolProvider(pluginInstance)) {\n this.#context.registerToolProvider(name, pluginInstance);\n }\n\n this.#setupPromises.push(pluginInstance.setup());\n\n const self = this;\n\n Object.defineProperty(this, name, {\n get() {\n const plugin = self.#pluginInstances[name];\n return self.wrapWithAsUser(plugin);\n },\n enumerable: true,\n });\n }\n\n /**\n * Binds all function properties in an exports object to the given context.\n * Recurses into plain objects to handle nested APIs (e.g., volume APIs).\n */\n private bindExportMethods(\n exports: Record<string, unknown>,\n context: BasePlugin,\n ) {\n for (const key in exports) {\n if (!Object.hasOwn(exports, key)) continue;\n const val = exports[key];\n if (typeof val === \"function\") {\n exports[key] = (val as (...args: unknown[]) => unknown).bind(context);\n } else if (isPlainObject(val)) {\n this.bindExportMethods(val as Record<string, unknown>, context);\n }\n }\n }\n\n /**\n * Wraps a plugin's exports with an `asUser` method that returns\n * a user-scoped version of the exports.\n *\n * When `exports()` returns a callable (function), it is returned as-is\n * since the plugin manages its own `asUser` per-call (e.g. files plugin).\n * When it returns a plain object, the standard `asUser` wrapper is added.\n *\n * The OBO-side wrapping lives inside `Plugin.asUser` — calling\n * `plugin.asUser(req).exports()` returns exports whose functions already\n * run inside the user's AsyncLocalStorage scope. AppKit only adapts the\n * shape; it does not own the user-context concept.\n */\n private wrapWithAsUser<T extends BasePlugin>(plugin: T) {\n // If plugin doesn't implement exports(), return empty object\n const pluginExports = plugin.exports?.() ?? {};\n\n // If exports is a function, the plugin manages its own asUser pattern\n if (typeof pluginExports === \"function\") {\n return pluginExports;\n }\n\n const objExports = pluginExports as Record<string, unknown>;\n this.bindExportMethods(objExports, plugin);\n\n // If plugin doesn't support asUser (no asUser method), return exports as-is\n if (typeof (plugin as any).asUser !== \"function\") {\n return objExports;\n }\n\n return {\n ...objExports,\n /**\n * Execute operations using the user's identity from the request.\n * Returns user-scoped exports where all methods execute with the\n * user's Databricks credentials instead of the service principal.\n */\n asUser: (req: import(\"express\").Request) =>\n (plugin as any).asUser(req).exports() as Record<string, unknown>,\n };\n }\n\n static async _createApp<\n T extends PluginData<PluginConstructor, unknown, string>[],\n >(\n config: {\n plugins?: T;\n telemetry?: TelemetryConfig;\n cache?: CacheConfig;\n client?: WorkspaceClient;\n onPluginsReady?: (appkit: PluginMap<T>) => void | Promise<void>;\n disableInternalTelemetry?: boolean;\n } = {},\n ): Promise<PluginMap<T>> {\n // Initialize core services\n TelemetryManager.initialize(config?.telemetry);\n await CacheManager.getInstance(config?.cache);\n\n const rawPlugins = config.plugins as T;\n\n // Collect manifest resources via registry\n const registry = new ResourceRegistry();\n registry.collectResources(rawPlugins);\n\n // Derive ServiceContext needs from what manifests declared\n const needsWarehouse = registry\n .getRequired()\n .some((r) => r.type === ResourceType.SQL_WAREHOUSE);\n await ServiceContext.initialize(\n { warehouseId: needsWarehouse },\n config?.client,\n );\n\n // Validate env vars\n registry.enforceValidation();\n\n const preparedPlugins = AppKit.preparePlugins(rawPlugins);\n const mergedConfig = {\n plugins: preparedPlugins,\n };\n\n const instance = new AppKit(mergedConfig);\n\n await Promise.all(instance.#setupPromises);\n await instance.#context.emitLifecycle(\"setup:complete\");\n\n const handle = instance as unknown as PluginMap<T>;\n\n if (config.onPluginsReady) {\n logger.debug(\"Running onPluginsReady hook\");\n await config.onPluginsReady(handle);\n logger.debug(\"onPluginsReady hook completed\");\n }\n\n if (isInternalTelemetryEnabled(config)) {\n AppKit.bootstrapInternalTelemetry();\n }\n\n const serverPlugin = instance.#pluginInstances.server;\n if (serverPlugin && typeof (serverPlugin as any).start === \"function\") {\n await (serverPlugin as any).start();\n }\n\n // Core owns graceful shutdown: install the signal handlers once every\n // plugin has started. Applies uniformly whether or not a server plugin\n // is present — server-less apps still get their telemetry flushed and\n // plugin shutdown() hooks run.\n new LifecycleManager(instance.#context).installSignalHandlers();\n\n return handle;\n }\n\n private static bootstrapInternalTelemetry(): void {\n const serviceCtx = ServiceContext.get();\n const reporter = TelemetryReporter.initialize({\n workspaceId: serviceCtx.workspaceId,\n client: serviceCtx.client,\n appId: process.env.DATABRICKS_CLIENT_ID || \"\",\n appkitVersion: productVersion,\n });\n reporter.start();\n reporter.sendStartup().catch(() => {});\n }\n\n private static preparePlugins(\n plugins: PluginData<PluginConstructor, unknown, string>[],\n ) {\n const result: InputPluginMap = {};\n for (const currentPlugin of plugins) {\n result[currentPlugin.name] = {\n plugin: currentPlugin.plugin,\n config: currentPlugin.config as Record<string, unknown>,\n };\n }\n return result;\n }\n}\n\n/**\n * Bootstraps AppKit with the provided configuration.\n *\n * Initializes telemetry, cache, and service context, then registers plugins\n * in phase order (core, normal, deferred) and awaits their setup.\n * If a `onPluginsReady` callback is provided it runs after plugin setup but\n * before the server starts, giving you access to the full appkit handle\n * for registering custom routes or performing async setup.\n * The returned object maps each plugin name to its `exports()` API,\n * with an `asUser(req)` method for user-scoped execution.\n *\n * @returns A `PluginMap` keyed by plugin name with typed exports\n *\n * @example Minimal server\n * ```ts\n * import { createApp, server } from \"@databricks/appkit\";\n *\n * await createApp({\n * plugins: [server()],\n * });\n * ```\n *\n * @example Server with custom routes via onPluginsReady\n * ```ts\n * import { createApp, server, analytics } from \"@databricks/appkit\";\n *\n * await createApp({\n * plugins: [server(), analytics({})],\n * onPluginsReady(appkit) {\n * appkit.server.extend((app) => {\n * app.get(\"/custom\", (_req, res) => res.json({ ok: true }));\n * });\n * },\n * });\n * ```\n */\nexport async function createApp<\n T extends PluginData<PluginConstructor, unknown, string>[],\n>(\n config: {\n plugins?: T;\n telemetry?: TelemetryConfig;\n cache?: CacheConfig;\n client?: WorkspaceClient;\n onPluginsReady?: (appkit: PluginMap<T>) => void | Promise<void>;\n disableInternalTelemetry?: boolean;\n } = {},\n): Promise<PluginMap<T>> {\n return AppKit._createApp(config);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"appkit.js","names":["#context","#pluginInstances","#setupPromises","productVersion"],"sources":["../../src/core/appkit.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type {\n BasePlugin,\n CacheConfig,\n InputPluginMap,\n OptionalConfigPluginDef,\n PluginConstructor,\n PluginData,\n PluginMap,\n} from \"shared\";\nimport { version as productVersion } from \"../../package.json\";\nimport { CacheManager } from \"../cache\";\nimport { ServiceContext } from \"../context\";\nimport {\n isInternalTelemetryEnabled,\n TelemetryReporter,\n} from \"../internal-telemetry\";\nimport { createLogger } from \"../logging/logger\";\nimport { isPlainObject } from \"../plugin/plugin\";\nimport { uiVariants } from \"../plugins/ui-variants\";\nimport { ResourceRegistry, ResourceType } from \"../registry\";\nimport type { TelemetryConfig } from \"../telemetry\";\nimport { TelemetryManager } from \"../telemetry\";\nimport { LifecycleManager } from \"./lifecycle-manager\";\nimport { isToolProvider, PluginContext } from \"./plugin-context\";\n\nconst logger = createLogger(\"appkit\");\n\nexport class AppKit<TPlugins extends InputPluginMap> {\n #pluginInstances: Record<string, BasePlugin> = {};\n #setupPromises: Promise<void>[] = [];\n #context: PluginContext;\n\n private constructor(config: { plugins: TPlugins }) {\n const { plugins, ...globalConfig } = config;\n\n this.#context = new PluginContext();\n\n const pluginEntries = Object.entries(plugins);\n\n const corePlugins = pluginEntries.filter(([_, p]) => {\n return (p?.plugin?.phase ?? \"normal\") === \"core\";\n });\n const normalPlugins = pluginEntries.filter(\n ([_, p]) => (p?.plugin?.phase ?? \"normal\") === \"normal\",\n );\n const deferredPlugins = pluginEntries.filter(\n ([_, p]) => (p?.plugin?.phase ?? \"normal\") === \"deferred\",\n );\n\n for (const [name, pluginData] of corePlugins) {\n if (pluginData) {\n this.createAndRegisterPlugin(globalConfig, name, pluginData, {\n context: this.#context,\n });\n }\n }\n\n for (const [name, pluginData] of normalPlugins) {\n if (pluginData) {\n this.createAndRegisterPlugin(globalConfig, name, pluginData, {\n context: this.#context,\n });\n }\n }\n\n for (const [name, pluginData] of deferredPlugins) {\n if (pluginData) {\n this.createAndRegisterPlugin(globalConfig, name, pluginData, {\n context: this.#context,\n });\n }\n }\n }\n\n private createAndRegisterPlugin<T extends PluginConstructor>(\n config: Omit<{ plugins: TPlugins }, \"plugins\">,\n name: string,\n pluginData: OptionalConfigPluginDef<T>,\n extraData?: Record<string, unknown>,\n ) {\n const { plugin: Plugin, config: pluginConfig } = pluginData;\n const baseConfig = {\n ...config,\n ...Plugin.DEFAULT_CONFIG,\n ...pluginConfig,\n name,\n ...extraData,\n };\n const pluginInstance = new Plugin(baseConfig);\n\n if (typeof pluginInstance.attachContext === \"function\") {\n pluginInstance.attachContext({\n context: this.#context,\n telemetryConfig: baseConfig.telemetry,\n });\n }\n\n this.#pluginInstances[name] = pluginInstance;\n\n this.#context.registerPlugin(name, pluginInstance);\n if (isToolProvider(pluginInstance)) {\n this.#context.registerToolProvider(name, pluginInstance);\n }\n\n this.#setupPromises.push(pluginInstance.setup());\n\n const self = this;\n\n Object.defineProperty(this, name, {\n get() {\n const plugin = self.#pluginInstances[name];\n return self.wrapWithAsUser(plugin);\n },\n enumerable: true,\n });\n }\n\n /**\n * Binds all function properties in an exports object to the given context.\n * Recurses into plain objects to handle nested APIs (e.g., volume APIs).\n */\n private bindExportMethods(\n exports: Record<string, unknown>,\n context: BasePlugin,\n ) {\n for (const key in exports) {\n if (!Object.hasOwn(exports, key)) continue;\n const val = exports[key];\n if (typeof val === \"function\") {\n exports[key] = (val as (...args: unknown[]) => unknown).bind(context);\n } else if (isPlainObject(val)) {\n this.bindExportMethods(val as Record<string, unknown>, context);\n }\n }\n }\n\n /**\n * Wraps a plugin's exports with an `asUser` method that returns\n * a user-scoped version of the exports.\n *\n * When `exports()` returns a callable (function), it is returned as-is\n * since the plugin manages its own `asUser` per-call (e.g. files plugin).\n * When it returns a plain object, the standard `asUser` wrapper is added.\n *\n * The OBO-side wrapping lives inside `Plugin.asUser` — calling\n * `plugin.asUser(req).exports()` returns exports whose functions already\n * run inside the user's AsyncLocalStorage scope. AppKit only adapts the\n * shape; it does not own the user-context concept.\n */\n private wrapWithAsUser<T extends BasePlugin>(plugin: T) {\n // If plugin doesn't implement exports(), return empty object\n const pluginExports = plugin.exports?.() ?? {};\n\n // If exports is a function, the plugin manages its own asUser pattern\n if (typeof pluginExports === \"function\") {\n return pluginExports;\n }\n\n const objExports = pluginExports as Record<string, unknown>;\n this.bindExportMethods(objExports, plugin);\n\n // If plugin doesn't support asUser (no asUser method), return exports as-is\n if (typeof (plugin as any).asUser !== \"function\") {\n return objExports;\n }\n\n return {\n ...objExports,\n /**\n * Execute operations using the user's identity from the request.\n * Returns user-scoped exports where all methods execute with the\n * user's Databricks credentials instead of the service principal.\n */\n asUser: (req: import(\"express\").Request) =>\n (plugin as any).asUser(req).exports() as Record<string, unknown>,\n };\n }\n\n static async _createApp<\n T extends PluginData<PluginConstructor, unknown, string>[],\n >(\n config: {\n plugins?: T;\n telemetry?: TelemetryConfig;\n cache?: CacheConfig;\n client?: WorkspaceClient;\n onPluginsReady?: (appkit: PluginMap<T>) => void | Promise<void>;\n disableInternalTelemetry?: boolean;\n } = {},\n ): Promise<PluginMap<T>> {\n // Initialize core services\n TelemetryManager.initialize(config?.telemetry);\n await CacheManager.getInstance(config?.cache);\n\n const withDefaults = AppKit.withDefaultPlugins(config.plugins as T);\n const rawPlugins = AppKit.filterDevOnlyPlugins(withDefaults);\n\n // Collect manifest resources via registry\n const registry = new ResourceRegistry();\n registry.collectResources(rawPlugins);\n\n // Derive ServiceContext needs from what manifests declared\n const needsWarehouse = registry\n .getRequired()\n .some((r) => r.type === ResourceType.SQL_WAREHOUSE);\n await ServiceContext.initialize(\n { warehouseId: needsWarehouse },\n config?.client,\n );\n\n // Validate env vars\n registry.enforceValidation();\n\n const preparedPlugins = AppKit.preparePlugins(rawPlugins);\n const mergedConfig = {\n plugins: preparedPlugins,\n };\n\n const instance = new AppKit(mergedConfig);\n\n await Promise.all(instance.#setupPromises);\n await instance.#context.emitLifecycle(\"setup:complete\");\n\n const handle = instance as unknown as PluginMap<T>;\n\n if (config.onPluginsReady) {\n logger.debug(\"Running onPluginsReady hook\");\n await config.onPluginsReady(handle);\n logger.debug(\"onPluginsReady hook completed\");\n }\n\n if (isInternalTelemetryEnabled(config)) {\n AppKit.bootstrapInternalTelemetry();\n }\n\n const serverPlugin = instance.#pluginInstances.server;\n if (serverPlugin && typeof (serverPlugin as any).start === \"function\") {\n await (serverPlugin as any).start();\n }\n\n // Core owns graceful shutdown: install the signal handlers once every\n // plugin has started. Applies uniformly whether or not a server plugin\n // is present — server-less apps still get their telemetry flushed and\n // plugin shutdown() hooks run.\n new LifecycleManager(instance.#context).installSignalHandlers();\n\n return handle;\n }\n\n private static bootstrapInternalTelemetry(): void {\n const serviceCtx = ServiceContext.get();\n const reporter = TelemetryReporter.initialize({\n workspaceId: serviceCtx.workspaceId,\n client: serviceCtx.client,\n appId: process.env.DATABRICKS_CLIENT_ID || \"\",\n appkitVersion: productVersion,\n });\n reporter.start();\n reporter.sendStartup().catch(() => {});\n }\n\n /**\n * Injects framework-owned default plugins the app author shouldn't have to\n * register by hand. Runs before {@link filterDevOnlyPlugins}, so a default\n * that is itself `devOnly` (like `ui-variants`) is present in dev and stripped\n * in prod through the exact same guard as any user plugin — never alive in a\n * deployed app.\n *\n * Currently injects the dev-only `ui-variants` recorder that backs the\n * `<Variants>` UI picker, so neither the developer nor their coding agent has\n * to remember to add it. If the app already registered it explicitly, that\n * entry wins and no duplicate is added.\n */\n private static withDefaultPlugins<\n T extends PluginData<PluginConstructor, unknown, string>[],\n >(plugins: T): T {\n const list = (plugins ?? []) as PluginData<\n PluginConstructor,\n unknown,\n string\n >[];\n\n const defaults: PluginData<PluginConstructor, unknown, string>[] = [\n uiVariants(),\n ];\n\n const missing = defaults.filter(\n (def) => !list.some((p) => p?.name === def.name),\n );\n\n return [...list, ...missing] as T;\n }\n\n /**\n * Drops plugins whose manifest declares `devOnly: true` unless\n * `NODE_ENV === \"development\"`. Runs before resource collection so a skipped\n * plugin is never constructed, never injects routes, and has its resource\n * requirements ignored — the framework, not the app author, enforces that\n * dev-only tooling can never run in a deployed app.\n *\n * This is the primary guard; plugins that expose a mutating dev endpoint\n * should still keep an in-handler `NODE_ENV` check as fail-safe defense\n * against being mounted through a path that bypasses this filter.\n */\n private static filterDevOnlyPlugins<\n T extends PluginData<PluginConstructor, unknown, string>[],\n >(plugins: T): T {\n if (!plugins || process.env.NODE_ENV === \"development\") return plugins;\n\n return plugins.filter((pluginData) => {\n const isDevOnly = pluginData?.plugin?.manifest?.devOnly === true;\n if (isDevOnly) {\n logger.debug(\n \"Skipping dev-only plugin %s (NODE_ENV=%s)\",\n pluginData?.name ?? \"unknown\",\n process.env.NODE_ENV ?? \"<unset>\",\n );\n }\n return !isDevOnly;\n }) as T;\n }\n\n private static preparePlugins(\n plugins: PluginData<PluginConstructor, unknown, string>[],\n ) {\n const result: InputPluginMap = {};\n for (const currentPlugin of plugins) {\n result[currentPlugin.name] = {\n plugin: currentPlugin.plugin,\n config: currentPlugin.config as Record<string, unknown>,\n };\n }\n return result;\n }\n}\n\n/**\n * Bootstraps AppKit with the provided configuration.\n *\n * Initializes telemetry, cache, and service context, then registers plugins\n * in phase order (core, normal, deferred) and awaits their setup.\n * If a `onPluginsReady` callback is provided it runs after plugin setup but\n * before the server starts, giving you access to the full appkit handle\n * for registering custom routes or performing async setup.\n * The returned object maps each plugin name to its `exports()` API,\n * with an `asUser(req)` method for user-scoped execution.\n *\n * @returns A `PluginMap` keyed by plugin name with typed exports\n *\n * @example Minimal server\n * ```ts\n * import { createApp, server } from \"@databricks/appkit\";\n *\n * await createApp({\n * plugins: [server()],\n * });\n * ```\n *\n * @example Server with custom routes via onPluginsReady\n * ```ts\n * import { createApp, server, analytics } from \"@databricks/appkit\";\n *\n * await createApp({\n * plugins: [server(), analytics({})],\n * onPluginsReady(appkit) {\n * appkit.server.extend((app) => {\n * app.get(\"/custom\", (_req, res) => res.json({ ok: true }));\n * });\n * },\n * });\n * ```\n */\nexport async function createApp<\n T extends PluginData<PluginConstructor, unknown, string>[],\n>(\n config: {\n plugins?: T;\n telemetry?: TelemetryConfig;\n cache?: CacheConfig;\n client?: WorkspaceClient;\n onPluginsReady?: (appkit: PluginMap<T>) => void | Promise<void>;\n disableInternalTelemetry?: boolean;\n } = {},\n): Promise<PluginMap<T>> {\n return AppKit._createApp(config);\n}\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -16,14 +16,14 @@ import { SeverityNumber, SpanStatusCode } from "./telemetry/index.js";
16
16
  import { CacheManager } from "./cache/index.js";
17
17
  import "./context/index.js";
18
18
  import { Plugin } from "./plugin/plugin.js";
19
+ import { toPlugin } from "./plugin/to-plugin.js";
20
+ import "./plugin/index.js";
19
21
  import { ResourceType } from "./registry/types.generated.js";
20
22
  import { getPluginManifest, getResourceRequirements } from "./registry/manifest-loader.js";
21
23
  import { ResourceRegistry } from "./registry/resource-registry.js";
22
24
  import "./registry/index.js";
23
25
  import { createApp } from "./core/appkit.js";
24
26
  import "./core/index.js";
25
- import { toPlugin } from "./plugin/to-plugin.js";
26
- import "./plugin/index.js";
27
27
  import { PolicyDeniedError, READ_ACTIONS, WRITE_ACTIONS } from "./plugins/files/policy.js";
28
28
  import { analytics } from "./plugins/analytics/analytics.js";
29
29
  import { files } from "./plugins/files/plugin.js";
@@ -6,10 +6,10 @@ import { isUserContext } from "../../context/user-context.js";
6
6
  import { getCurrentUserId, getExecutionContext, getWorkspaceClient, runInUserContext } from "../../context/execution-context.js";
7
7
  import "../../context/index.js";
8
8
  import { Plugin } from "../../plugin/plugin.js";
9
- import { ResourceType } from "../../registry/types.generated.js";
10
- import "../../registry/index.js";
11
9
  import { toPlugin } from "../../plugin/to-plugin.js";
12
10
  import "../../plugin/index.js";
11
+ import { ResourceType } from "../../registry/types.generated.js";
12
+ import "../../registry/index.js";
13
13
  import { PolicyDeniedError, policy } from "./policy.js";
14
14
  import { FILES_MAX_READ_SIZE, contentTypeFromPath, isSafeInlineContentType, validateCustomContentTypes } from "../../connectors/files/defaults.js";
15
15
  import { FilesConnector, runWithFilesSpanAttributes } from "../../connectors/files/client.js";
@@ -5,10 +5,10 @@ import "../../errors/index.js";
5
5
  import { getCurrentUserId, getWorkspaceClient } from "../../context/execution-context.js";
6
6
  import "../../context/index.js";
7
7
  import { Plugin } from "../../plugin/plugin.js";
8
- import { ResourceType } from "../../registry/types.generated.js";
9
- import "../../registry/index.js";
10
8
  import { toPlugin } from "../../plugin/to-plugin.js";
11
9
  import "../../plugin/index.js";
10
+ import { ResourceType } from "../../registry/types.generated.js";
11
+ import "../../registry/index.js";
12
12
  import { JobsConnector } from "../../connectors/jobs/client.js";
13
13
  import "../../connectors/jobs/index.js";
14
14
  import { JOBS_READ_DEFAULTS, JOBS_STREAM_DEFAULTS, JOBS_WRITE_DEFAULTS } from "./defaults.js";
@@ -2,10 +2,10 @@ import { createLogger } from "../../logging/logger.js";
2
2
  import { getWorkspaceClient } from "../../context/execution-context.js";
3
3
  import "../../context/index.js";
4
4
  import { Plugin } from "../../plugin/plugin.js";
5
- import { ResourceType } from "../../registry/types.generated.js";
6
- import "../../registry/index.js";
7
5
  import { toPlugin } from "../../plugin/to-plugin.js";
8
6
  import "../../plugin/index.js";
7
+ import { ResourceType } from "../../registry/types.generated.js";
8
+ import "../../registry/index.js";
9
9
  import "../../logging/index.js";
10
10
  import { invoke, stream } from "../../connectors/serving/client.js";
11
11
  import { servingInvokeDefaults } from "./defaults.js";
@@ -0,0 +1,70 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ //#region src/plugins/ui-variants/choice-sink.ts
5
+ /**
6
+ * Path of the JSONL choices file, relative to the app's working directory.
7
+ * A coding agent reads it to pick up the developer's in-browser confirmation
8
+ * and finalize the chosen variant.
9
+ *
10
+ * Under `node_modules/`, so it's gitignored and cleared on a clean install.
11
+ * Each line is spent on read — the agent removes it once the choice is
12
+ * finalized.
13
+ *
14
+ * CONTRACT: the `databricks-app-variants` agent skill (in the
15
+ * databricks-agent-skills repo) discovers the choices file at this path.
16
+ * Changing this value silently breaks that skill's file discovery — update the
17
+ * skill's `find` path in the same change.
18
+ */
19
+ const UI_CHOICES_FILE = "node_modules/.databricks/appkit/.appkit-ui-choices.jsonl";
20
+ /**
21
+ * File store for confirmed variant choices: upserts choices into
22
+ * {@link UI_CHOICES_FILE}, one line per `<Variants>` id.
23
+ *
24
+ * The store is **keyed and latest-wins**: at most one record per `blockId`, and
25
+ * recording an existing `blockId` replaces it rather than appending, so the file
26
+ * always reflects the current choice for each block.
27
+ *
28
+ * The file is resolved against `process.cwd()`, so it lands under whatever
29
+ * directory the dev server runs from. Concurrent confirms are serialized behind
30
+ * an internal queue so their read-modify-write can't interleave and lose an
31
+ * update.
32
+ *
33
+ * @internal
34
+ */
35
+ var FileChoiceStore = class {
36
+ filePath;
37
+ writeQueue = Promise.resolve();
38
+ constructor(relativePath = UI_CHOICES_FILE) {
39
+ this.filePath = path.resolve(process.cwd(), relativePath);
40
+ }
41
+ record(record) {
42
+ const run = this.writeQueue.then(() => this.upsert(record));
43
+ this.writeQueue = run.catch(() => {});
44
+ return run;
45
+ }
46
+ /**
47
+ * Reads the current file, replaces (or adds) the line for `record.blockId`,
48
+ * dropping any unparseable lines, and rewrites the whole file.
49
+ */
50
+ async upsert(record) {
51
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
52
+ let existing = "";
53
+ try {
54
+ existing = await fs.readFile(this.filePath, "utf-8");
55
+ } catch {}
56
+ const kept = existing.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => {
57
+ try {
58
+ return JSON.parse(line).blockId !== record.blockId;
59
+ } catch {
60
+ return false;
61
+ }
62
+ });
63
+ kept.push(JSON.stringify(record));
64
+ await fs.writeFile(this.filePath, `${kept.join("\n")}\n`, "utf-8");
65
+ }
66
+ };
67
+
68
+ //#endregion
69
+ export { FileChoiceStore };
70
+ //# sourceMappingURL=choice-sink.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"choice-sink.js","names":[],"sources":["../../../src/plugins/ui-variants/choice-sink.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\n/**\n * Path of the JSONL choices file, relative to the app's working directory.\n * A coding agent reads it to pick up the developer's in-browser confirmation\n * and finalize the chosen variant.\n *\n * Under `node_modules/`, so it's gitignored and cleared on a clean install.\n * Each line is spent on read — the agent removes it once the choice is\n * finalized.\n *\n * CONTRACT: the `databricks-app-variants` agent skill (in the\n * databricks-agent-skills repo) discovers the choices file at this path.\n * Changing this value silently breaks that skill's file discovery — update the\n * skill's `find` path in the same change.\n */\nconst UI_CHOICES_FILE =\n \"node_modules/.databricks/appkit/.appkit-ui-choices.jsonl\";\n\n/**\n * One recorded variant choice.\n *\n * CONTRACT: the `databricks-app-variants` agent skill parses these fields\n * (`blockId`, `chosenIndex`, `label`) to finalize the chosen variant. Renaming\n * or removing a field silently breaks finalization (the skill reads it from the\n * JSONL line, so there's no compile error) — update the skill in the same\n * change.\n */\nexport interface UiChoiceRecord {\n /** ISO timestamp of when the choice was recorded. */\n ts: string;\n /** Stable id of the `<Variants>` block the developer confirmed. */\n blockId: string;\n /** Zero-based index of the chosen `<Variant>` child. */\n chosenIndex: number;\n /** Human-readable label of the chosen variant (for agent context). */\n label?: string;\n /** Optional free-form note from the developer. */\n note?: string;\n}\n\n/**\n * File store for confirmed variant choices: upserts choices into\n * {@link UI_CHOICES_FILE}, one line per `<Variants>` id.\n *\n * The store is **keyed and latest-wins**: at most one record per `blockId`, and\n * recording an existing `blockId` replaces it rather than appending, so the file\n * always reflects the current choice for each block.\n *\n * The file is resolved against `process.cwd()`, so it lands under whatever\n * directory the dev server runs from. Concurrent confirms are serialized behind\n * an internal queue so their read-modify-write can't interleave and lose an\n * update.\n *\n * @internal\n */\nexport class FileChoiceStore {\n private readonly filePath: string;\n private writeQueue: Promise<void> = Promise.resolve();\n\n constructor(relativePath: string = UI_CHOICES_FILE) {\n this.filePath = path.resolve(process.cwd(), relativePath);\n }\n\n record(record: UiChoiceRecord): Promise<void> {\n const run = this.writeQueue.then(() => this.upsert(record));\n // Keep the chain alive even if this write throws, but surface the error.\n this.writeQueue = run.catch(() => {});\n return run;\n }\n\n /**\n * Reads the current file, replaces (or adds) the line for `record.blockId`,\n * dropping any unparseable lines, and rewrites the whole file.\n */\n private async upsert(record: UiChoiceRecord): Promise<void> {\n await fs.mkdir(path.dirname(this.filePath), { recursive: true });\n\n let existing = \"\";\n try {\n existing = await fs.readFile(this.filePath, \"utf-8\");\n } catch {\n // File doesn't exist yet — start empty.\n }\n\n const kept = existing\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter((line) => line.length > 0)\n .filter((line) => {\n try {\n return (\n (JSON.parse(line) as UiChoiceRecord).blockId !== record.blockId\n );\n } catch {\n // Drop unparseable lines rather than let them accumulate.\n return false;\n }\n });\n\n kept.push(JSON.stringify(record));\n await fs.writeFile(this.filePath, `${kept.join(\"\\n\")}\\n`, \"utf-8\");\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiBA,MAAM,kBACJ;;;;;;;;;;;;;;;;AAuCF,IAAa,kBAAb,MAA6B;CAC3B,AAAiB;CACjB,AAAQ,aAA4B,QAAQ,SAAS;CAErD,YAAY,eAAuB,iBAAiB;AAClD,OAAK,WAAW,KAAK,QAAQ,QAAQ,KAAK,EAAE,aAAa;;CAG3D,OAAO,QAAuC;EAC5C,MAAM,MAAM,KAAK,WAAW,WAAW,KAAK,OAAO,OAAO,CAAC;AAE3D,OAAK,aAAa,IAAI,YAAY,GAAG;AACrC,SAAO;;;;;;CAOT,MAAc,OAAO,QAAuC;AAC1D,QAAM,GAAG,MAAM,KAAK,QAAQ,KAAK,SAAS,EAAE,EAAE,WAAW,MAAM,CAAC;EAEhE,IAAI,WAAW;AACf,MAAI;AACF,cAAW,MAAM,GAAG,SAAS,KAAK,UAAU,QAAQ;UAC9C;EAIR,MAAM,OAAO,SACV,MAAM,KAAK,CACX,KAAK,SAAS,KAAK,MAAM,CAAC,CAC1B,QAAQ,SAAS,KAAK,SAAS,EAAE,CACjC,QAAQ,SAAS;AAChB,OAAI;AACF,WACG,KAAK,MAAM,KAAK,CAAoB,YAAY,OAAO;WAEpD;AAEN,WAAO;;IAET;AAEJ,OAAK,KAAK,KAAK,UAAU,OAAO,CAAC;AACjC,QAAM,GAAG,UAAU,KAAK,UAAU,GAAG,KAAK,KAAK,KAAK,CAAC,KAAK,QAAQ"}
@@ -0,0 +1,94 @@
1
+ import { createLogger } from "../../logging/logger.js";
2
+ import { Plugin } from "../../plugin/plugin.js";
3
+ import { toPlugin } from "../../plugin/to-plugin.js";
4
+ import "../../plugin/index.js";
5
+ import { FileChoiceStore } from "./choice-sink.js";
6
+ import manifest_default from "./manifest.js";
7
+
8
+ //#region src/plugins/ui-variants/index.ts
9
+ const logger = createLogger("ui-variants");
10
+ /**
11
+ * Dev-only recorder backing the `<Variants>` UI picker.
12
+ *
13
+ * When a developer confirms a variant in the browser, the `<Variants>`
14
+ * component POSTs to `/api/ui-variants/confirm`. This plugin records the choice
15
+ * into a keyed JSONL file, which a coding agent reads to finalize the chosen
16
+ * variant into the component source. Recording is a keyed upsert — one entry
17
+ * per `<Variants>` id — and the plugin only records; it never edits source.
18
+ */
19
+ var UiVariantsPlugin = class extends Plugin {
20
+ static manifest = manifest_default;
21
+ static description = "Dev-only recorder for the <Variants> UI picker";
22
+ store;
23
+ constructor(config = {}) {
24
+ super(config);
25
+ this.store = new FileChoiceStore();
26
+ }
27
+ injectRoutes(router) {
28
+ this.route(router, {
29
+ name: "confirm",
30
+ method: "post",
31
+ path: "/confirm",
32
+ handler: async (req, res) => {
33
+ if (process.env.NODE_ENV !== "development") {
34
+ res.status(403).json({
35
+ ok: false,
36
+ message: "ui-variants confirm is only available in development"
37
+ });
38
+ return;
39
+ }
40
+ const body = req.body ?? {};
41
+ const { blockId, chosenIndex } = body;
42
+ if (typeof blockId !== "string" || blockId.length === 0) {
43
+ res.status(400).json({
44
+ ok: false,
45
+ message: "`blockId` (non-empty string) is required"
46
+ });
47
+ return;
48
+ }
49
+ if (typeof chosenIndex !== "number" || !Number.isInteger(chosenIndex)) {
50
+ res.status(400).json({
51
+ ok: false,
52
+ message: "`chosenIndex` (integer) is required"
53
+ });
54
+ return;
55
+ }
56
+ const record = {
57
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
58
+ blockId,
59
+ chosenIndex,
60
+ ...typeof body.label === "string" ? { label: body.label } : {},
61
+ ...typeof body.note === "string" ? { note: body.note } : {}
62
+ };
63
+ try {
64
+ await this.store.record(record);
65
+ } catch (error) {
66
+ logger.error("Failed to record UI variant choice", {
67
+ error,
68
+ blockId
69
+ });
70
+ res.status(500).json({
71
+ ok: false,
72
+ message: "Failed to record variant choice"
73
+ });
74
+ return;
75
+ }
76
+ logger.info("Recorded UI variant choice", {
77
+ blockId,
78
+ chosenIndex,
79
+ label: record.label
80
+ });
81
+ res.json({
82
+ ok: true,
83
+ blockId,
84
+ chosenIndex
85
+ });
86
+ }
87
+ });
88
+ }
89
+ };
90
+ const uiVariants = toPlugin(UiVariantsPlugin);
91
+
92
+ //#endregion
93
+ export { uiVariants };
94
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["manifest"],"sources":["../../../src/plugins/ui-variants/index.ts"],"sourcesContent":["import type { BasePluginConfig, IAppRouter } from \"shared\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { FileChoiceStore, type UiChoiceRecord } from \"./choice-sink\";\nimport manifest from \"./manifest.json\";\n\nconst logger = createLogger(\"ui-variants\");\n\n/** Untrusted body of `POST /api/ui-variants/confirm`; validated in the handler. */\ninterface ConfirmRequestBody {\n blockId?: unknown;\n chosenIndex?: unknown;\n label?: unknown;\n note?: unknown;\n}\n\n/**\n * Dev-only recorder backing the `<Variants>` UI picker.\n *\n * When a developer confirms a variant in the browser, the `<Variants>`\n * component POSTs to `/api/ui-variants/confirm`. This plugin records the choice\n * into a keyed JSONL file, which a coding agent reads to finalize the chosen\n * variant into the component source. Recording is a keyed upsert — one entry\n * per `<Variants>` id — and the plugin only records; it never edits source.\n */\nclass UiVariantsPlugin extends Plugin<BasePluginConfig> {\n static manifest = manifest as PluginManifest<\"ui-variants\">;\n\n protected static description =\n \"Dev-only recorder for the <Variants> UI picker\";\n\n private readonly store: FileChoiceStore;\n\n constructor(config: BasePluginConfig = {} as BasePluginConfig) {\n super(config);\n this.store = new FileChoiceStore();\n }\n\n injectRoutes(router: IAppRouter): void {\n this.route(router, {\n name: \"confirm\",\n method: \"post\",\n path: \"/confirm\",\n handler: async (req, res) => {\n // Dev-only: the recorder exists solely to drive the local edit loop,\n // so it must never run in a deployed app.\n if (process.env.NODE_ENV !== \"development\") {\n res.status(403).json({\n ok: false,\n message: \"ui-variants confirm is only available in development\",\n });\n return;\n }\n\n const body = (req.body ?? {}) as ConfirmRequestBody;\n const { blockId, chosenIndex } = body;\n\n if (typeof blockId !== \"string\" || blockId.length === 0) {\n res.status(400).json({\n ok: false,\n message: \"`blockId` (non-empty string) is required\",\n });\n return;\n }\n if (typeof chosenIndex !== \"number\" || !Number.isInteger(chosenIndex)) {\n res.status(400).json({\n ok: false,\n message: \"`chosenIndex` (integer) is required\",\n });\n return;\n }\n\n const record: UiChoiceRecord = {\n ts: new Date().toISOString(),\n blockId,\n chosenIndex,\n ...(typeof body.label === \"string\" ? { label: body.label } : {}),\n ...(typeof body.note === \"string\" ? { note: body.note } : {}),\n };\n\n try {\n await this.store.record(record);\n } catch (error) {\n logger.error(\"Failed to record UI variant choice\", {\n error,\n blockId,\n });\n res\n .status(500)\n .json({ ok: false, message: \"Failed to record variant choice\" });\n return;\n }\n\n logger.info(\"Recorded UI variant choice\", {\n blockId,\n chosenIndex,\n label: record.label,\n });\n res.json({ ok: true, blockId, chosenIndex });\n },\n });\n }\n}\n\nexport const uiVariants = toPlugin(UiVariantsPlugin);\n"],"mappings":";;;;;;;;AAOA,MAAM,SAAS,aAAa,cAAc;;;;;;;;;;AAmB1C,IAAM,mBAAN,cAA+B,OAAyB;CACtD,OAAO,WAAWA;CAElB,OAAiB,cACf;CAEF,AAAiB;CAEjB,YAAY,SAA2B,EAAE,EAAsB;AAC7D,QAAM,OAAO;AACb,OAAK,QAAQ,IAAI,iBAAiB;;CAGpC,aAAa,QAA0B;AACrC,OAAK,MAAM,QAAQ;GACjB,MAAM;GACN,QAAQ;GACR,MAAM;GACN,SAAS,OAAO,KAAK,QAAQ;AAG3B,QAAI,QAAQ,IAAI,aAAa,eAAe;AAC1C,SAAI,OAAO,IAAI,CAAC,KAAK;MACnB,IAAI;MACJ,SAAS;MACV,CAAC;AACF;;IAGF,MAAM,OAAQ,IAAI,QAAQ,EAAE;IAC5B,MAAM,EAAE,SAAS,gBAAgB;AAEjC,QAAI,OAAO,YAAY,YAAY,QAAQ,WAAW,GAAG;AACvD,SAAI,OAAO,IAAI,CAAC,KAAK;MACnB,IAAI;MACJ,SAAS;MACV,CAAC;AACF;;AAEF,QAAI,OAAO,gBAAgB,YAAY,CAAC,OAAO,UAAU,YAAY,EAAE;AACrE,SAAI,OAAO,IAAI,CAAC,KAAK;MACnB,IAAI;MACJ,SAAS;MACV,CAAC;AACF;;IAGF,MAAM,SAAyB;KAC7B,qBAAI,IAAI,MAAM,EAAC,aAAa;KAC5B;KACA;KACA,GAAI,OAAO,KAAK,UAAU,WAAW,EAAE,OAAO,KAAK,OAAO,GAAG,EAAE;KAC/D,GAAI,OAAO,KAAK,SAAS,WAAW,EAAE,MAAM,KAAK,MAAM,GAAG,EAAE;KAC7D;AAED,QAAI;AACF,WAAM,KAAK,MAAM,OAAO,OAAO;aACxB,OAAO;AACd,YAAO,MAAM,sCAAsC;MACjD;MACA;MACD,CAAC;AACF,SACG,OAAO,IAAI,CACX,KAAK;MAAE,IAAI;MAAO,SAAS;MAAmC,CAAC;AAClE;;AAGF,WAAO,KAAK,8BAA8B;KACxC;KACA;KACA,OAAO,OAAO;KACf,CAAC;AACF,QAAI,KAAK;KAAE,IAAI;KAAM;KAAS;KAAa,CAAC;;GAE/C,CAAC;;;AAIN,MAAa,aAAa,SAAS,iBAAiB"}
@@ -0,0 +1,17 @@
1
+ //#region src/plugins/ui-variants/manifest.json
2
+ var manifest_default = {
3
+ $schema: "https://databricks.github.io/appkit/schemas/plugin-manifest.schema.json",
4
+ name: "ui-variants",
5
+ displayName: "UI Variants Plugin",
6
+ description: "Dev-only recorder for the <Variants> UI picker: records the developer's chosen variant so a coding agent can finalize the component source",
7
+ hidden: true,
8
+ devOnly: true,
9
+ resources: {
10
+ "required": [],
11
+ "optional": []
12
+ }
13
+ };
14
+
15
+ //#endregion
16
+ export { manifest_default as default };
17
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","names":[],"sources":["../../../src/plugins/ui-variants/manifest.json"],"sourcesContent":[""],"mappings":""}
@@ -1118,6 +1118,7 @@ declare const pluginManifestSchema: z.ZodObject<{
1118
1118
  license: z.ZodOptional<z.ZodString>;
1119
1119
  onSetupMessage: z.ZodOptional<z.ZodString>;
1120
1120
  hidden: z.ZodOptional<z.ZodBoolean>;
1121
+ devOnly: z.ZodOptional<z.ZodBoolean>;
1121
1122
  stability: z.ZodOptional<z.ZodEnum<{
1122
1123
  beta: "beta";
1123
1124
  ga: "ga";
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","names":[],"sources":["../../src/schemas/manifest.ts"],"mappings":";;;cAsPa,yBAAA,EAAyB,CAAA,CAAA,qBAAA,EAAA,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;cA8FzB,wBAAA,EAAwB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAkQxB,4BAAA,EAA4B,CAAA,CAAA,SAAA;;;;;cAoE5B,oBAAA,EAAoB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA+frB,mBAAA,GAAsB,CAAA,CAAE,KAAA,QAAa,yBAAA;AAAA,KACrC,kBAAA,GAAqB,CAAA,CAAE,KAAA,QAAa,wBAAA;AAAA,KAIpC,sBAAA,GAAyB,CAAA,CAAE,KAAA,QAC9B,4BAAA;AAAA,KAEG,cAAA,GAAiB,CAAA,CAAE,KAAA,QAAa,oBAAA"}
1
+ {"version":3,"file":"manifest.d.ts","names":[],"sources":["../../src/schemas/manifest.ts"],"mappings":";;;cAsPa,yBAAA,EAAyB,CAAA,CAAA,qBAAA,EAAA,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;cA8FzB,wBAAA,EAAwB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAkQxB,4BAAA,EAA4B,CAAA,CAAA,SAAA;;;;;cAoE5B,oBAAA,EAAoB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAqgBrB,mBAAA,GAAsB,CAAA,CAAE,KAAA,QAAa,yBAAA;AAAA,KACrC,kBAAA,GAAqB,CAAA,CAAE,KAAA,QAAa,wBAAA;AAAA,KAIpC,sBAAA,GAAyB,CAAA,CAAE,KAAA,QAC9B,4BAAA;AAAA,KAEG,cAAA,GAAiB,CAAA,CAAE,KAAA,QAAa,oBAAA"}
@@ -334,6 +334,7 @@ const pluginManifestSchema = z.object({
334
334
  license: z.string().optional().describe("SPDX license identifier"),
335
335
  onSetupMessage: z.string().optional().describe("Message displayed to the user after project initialization. Use this to inform about manual setup steps (e.g. environment variables, resource provisioning)."),
336
336
  hidden: z.boolean().optional().describe("When true, this plugin is excluded from the template plugins manifest (appkit.plugins.json) during sync."),
337
+ devOnly: z.boolean().optional().describe("When true, this plugin is only registered when NODE_ENV === 'development'. In any other environment createApp skips it entirely (not constructed, no routes, resources not validated). Use for dev-only tooling that must never run in a deployed app."),
337
338
  stability: z.enum(["beta", "ga"]).optional().describe("Plugin stability level. Beta plugins may have breaking API changes between minor releases but are on a path to GA. GA (general availability) plugins follow semver strictly."),
338
339
  scaffolding: z.object({ rules: pluginScaffoldingRulesSchema.optional().describe("Structured rules for scaffolding agents declared at the plugin level.") }).strict().optional().describe("Plugin-level scaffolding metadata consumed by scaffolding agents. Symmetric with template-level `scaffolding`.")
339
340
  }).strict().describe("Schema for Databricks AppKit plugin manifest files. Defines plugin metadata, resource requirements, and configuration options.");