@databricks/appkit 0.76.0 → 0.76.1
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/dist/appkit/package.js +1 -1
- package/dist/cli/commands/codemod/on-plugins-ready.js +2 -1
- package/dist/cli/commands/codemod/on-plugins-ready.js.map +1 -1
- package/dist/cli/commands/lint.js +2 -1
- package/dist/cli/commands/lint.js.map +1 -1
- package/dist/cli/commands/plugin/add-resource/add-resource.js +1 -1
- package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
- package/dist/cli/commands/plugin/create/create.js +1 -1
- package/dist/cli/commands/plugin/create/create.js.map +1 -1
- package/dist/cli/commands/plugin/create/prompt-resource.js +1 -1
- package/dist/cli/commands/plugin/create/prompt-resource.js.map +1 -1
- package/dist/cli/commands/plugin/sync/sync.js +2 -1
- package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
- package/dist/cli/commands/registry/env-writer.js +4 -1
- package/dist/cli/commands/registry/env-writer.js.map +1 -1
- package/dist/evals/judge.d.ts.map +1 -1
- package/dist/evals/judge.js +8 -2
- package/dist/evals/judge.js.map +1 -1
- package/dist/plugins/agents/mlflow.js +6 -1
- package/dist/plugins/agents/mlflow.js.map +1 -1
- package/dist/shared/src/schemas/manifest.d.ts +33 -33
- package/docs/plugins/agents.md +3 -1
- package/package.json +11 -3
- package/sbom.cdx.json +1 -1
|
@@ -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\";\n\nimport { Lang, parse, type SgNode } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\n\nimport {\n TEMPLATE_SCAFFOLDING,\n templateFieldEntrySchema,\n} from \"../../../../schemas/manifest\";\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 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)}`,\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 // Narrowing on `!== \"ga\"` removes \"ga\"; the truthy check\n // removes `undefined`. What's left is the non-GA tier set,\n // which TypeScript already knows is assignable to TemplatePlugin's\n // `stability` field — so no cast is needed and adding a future\n // tier (e.g. \"alpha\") flows through type-correctly.\n ...(manifest.stability &&\n manifest.stability !== \"ga\" && {\n stability: manifest.stability,\n }),\n ...(manifest.scaffolding && {\n scaffolding: manifest.scaffolding,\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 ...(manifest.stability &&\n manifest.stability !== \"ga\" && {\n stability: manifest.stability,\n }),\n ...(manifest.scaffolding && {\n scaffolding: manifest.scaffolding,\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 *\n * Each resource field is parsed through `templateFieldEntrySchema` so the\n * `origin` transform fires and produces canonical `origin` values, even\n * when the input carries a stale `origin`. Parsing per-field (rather than\n * the whole manifest) keeps the surrounding plugin/resource key order\n * stable so the synced JSON's diff stays minimal.\n */\nfunction writeManifest(\n outputPath: string,\n { plugins }: { plugins: TemplatePluginsManifest[\"plugins\"] },\n options: { write?: boolean; silent?: boolean; json?: boolean },\n) {\n for (const plugin of Object.values(plugins)) {\n for (const group of [\n plugin.resources.required,\n plugin.resources.optional,\n ]) {\n for (const resource of group) {\n if (!resource.fields) continue;\n for (const fieldName of Object.keys(resource.fields)) {\n resource.fields[fieldName] = templateFieldEntrySchema.parse(\n resource.fields[fieldName],\n );\n }\n }\n }\n }\n\n const templateManifest: TemplatePluginsManifest = {\n $schema:\n \"https://databricks.github.io/appkit/schemas/template-plugins.schema.json\",\n version: \"2.0\",\n plugins,\n scaffolding: TEMPLATE_SCAFFOLDING,\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 // Step 6b: Strip requiredByTemplate for non-GA plugins\n for (const plugin of Object.values(plugins)) {\n if (\n plugin.requiredByTemplate &&\n plugin.stability &&\n plugin.stability !== \"ga\"\n ) {\n plugin.requiredByTemplate = undefined;\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 // Step 7: Detect orphaned resources from removed plugins\n if (!options.silent && fs.existsSync(outputPath)) {\n try {\n const oldRaw = fs.readFileSync(outputPath, \"utf-8\");\n const oldManifest = JSON.parse(oldRaw) as TemplatePluginsManifest;\n const oldNames = new Set(Object.keys(oldManifest.plugins ?? {}));\n const newNames = new Set(Object.keys(plugins));\n for (const name of oldNames) {\n if (newNames.has(name)) continue;\n const oldPlugin = oldManifest.plugins?.[name];\n if (!oldPlugin || typeof oldPlugin !== \"object\") continue;\n const envVars: string[] = [];\n for (const res of [\n ...(oldPlugin.resources?.required ?? []),\n ...(oldPlugin.resources?.optional ?? []),\n ]) {\n if (res?.fields) {\n for (const field of Object.values(res.fields)) {\n if (field?.env) envVars.push(field.env);\n }\n }\n }\n const envInfo =\n envVars.length > 0\n ? ` The following resource env vars may be orphaned: ${envVars.join(\", \")}`\n : \"\";\n console.warn(`Warning: Plugin \"${name}\" was removed.${envInfo}`);\n }\n } catch {\n // Ignore parse errors on existing manifest\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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"}
|
|
1
|
+
{"version":3,"file":"sync.js","names":[],"sources":["../../../../../src/cli/commands/plugin/sync/sync.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport path from \"node:path\";\n\nimport type { SgNode } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\n\nimport {\n TEMPLATE_SCAFFOLDING,\n templateFieldEntrySchema,\n} from \"../../../../schemas/manifest\";\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 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)}`,\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 // Narrowing on `!== \"ga\"` removes \"ga\"; the truthy check\n // removes `undefined`. What's left is the non-GA tier set,\n // which TypeScript already knows is assignable to TemplatePlugin's\n // `stability` field — so no cast is needed and adding a future\n // tier (e.g. \"alpha\") flows through type-correctly.\n ...(manifest.stability &&\n manifest.stability !== \"ga\" && {\n stability: manifest.stability,\n }),\n ...(manifest.scaffolding && {\n scaffolding: manifest.scaffolding,\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 ...(manifest.stability &&\n manifest.stability !== \"ga\" && {\n stability: manifest.stability,\n }),\n ...(manifest.scaffolding && {\n scaffolding: manifest.scaffolding,\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 *\n * Each resource field is parsed through `templateFieldEntrySchema` so the\n * `origin` transform fires and produces canonical `origin` values, even\n * when the input carries a stale `origin`. Parsing per-field (rather than\n * the whole manifest) keeps the surrounding plugin/resource key order\n * stable so the synced JSON's diff stays minimal.\n */\nfunction writeManifest(\n outputPath: string,\n { plugins }: { plugins: TemplatePluginsManifest[\"plugins\"] },\n options: { write?: boolean; silent?: boolean; json?: boolean },\n) {\n for (const plugin of Object.values(plugins)) {\n for (const group of [\n plugin.resources.required,\n plugin.resources.optional,\n ]) {\n for (const resource of group) {\n if (!resource.fields) continue;\n for (const fieldName of Object.keys(resource.fields)) {\n resource.fields[fieldName] = templateFieldEntrySchema.parse(\n resource.fields[fieldName],\n );\n }\n }\n }\n }\n\n const templateManifest: TemplatePluginsManifest = {\n $schema:\n \"https://databricks.github.io/appkit/schemas/template-plugins.schema.json\",\n version: \"2.0\",\n plugins,\n scaffolding: TEMPLATE_SCAFFOLDING,\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 require = createRequire(import.meta.url);\n const { Lang, parse } =\n require(\"@ast-grep/napi\") as typeof import(\"@ast-grep/napi\");\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 // Step 6b: Strip requiredByTemplate for non-GA plugins\n for (const plugin of Object.values(plugins)) {\n if (\n plugin.requiredByTemplate &&\n plugin.stability &&\n plugin.stability !== \"ga\"\n ) {\n plugin.requiredByTemplate = undefined;\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 // Step 7: Detect orphaned resources from removed plugins\n if (!options.silent && fs.existsSync(outputPath)) {\n try {\n const oldRaw = fs.readFileSync(outputPath, \"utf-8\");\n const oldManifest = JSON.parse(oldRaw) as TemplatePluginsManifest;\n const oldNames = new Set(Object.keys(oldManifest.plugins ?? {}));\n const newNames = new Set(Object.keys(plugins));\n for (const name of oldNames) {\n if (newNames.has(name)) continue;\n const oldPlugin = oldManifest.plugins?.[name];\n if (!oldPlugin || typeof oldPlugin !== \"object\") continue;\n const envVars: string[] = [];\n for (const res of [\n ...(oldPlugin.resources?.required ?? []),\n ...(oldPlugin.resources?.optional ?? []),\n ]) {\n if (res?.fields) {\n for (const field of Object.values(res.fields)) {\n if (field?.env) envVars.push(field.env);\n }\n }\n }\n const envInfo =\n envVars.length > 0\n ? ` The following resource env vars may be orphaned: ${envVars.join(\", \")}`\n : \"\";\n console.warn(`Warning: Plugin \"${name}\" was removed.${envInfo}`);\n }\n } catch {\n // Ignore parse errors on existing manifest\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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"}
|
|
@@ -3,7 +3,6 @@ import { composeResourceId, isFlatListable, isParentContext, listParentContextSt
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import pc from "picocolors";
|
|
6
|
-
import { autocomplete, isCancel, select, text } from "@clack/prompts";
|
|
7
6
|
|
|
8
7
|
//#region src/cli/commands/registry/env-writer.ts
|
|
9
8
|
/** Sentinel select value meaning "let me type the id myself". */
|
|
@@ -41,6 +40,7 @@ function capChoices(choices, resourceType, limit = PICKER_LIMIT) {
|
|
|
41
40
|
}
|
|
42
41
|
/** Free-text prompt for one env need; undefined to skip. */
|
|
43
42
|
async function promptText(need) {
|
|
43
|
+
const { isCancel, text } = await import("@clack/prompts");
|
|
44
44
|
const tag = need.required ? "required" : "optional";
|
|
45
45
|
const answer = await text({
|
|
46
46
|
message: `${need.env} (${need.resourceType}, ${tag})`,
|
|
@@ -52,6 +52,7 @@ async function promptText(need) {
|
|
|
52
52
|
}
|
|
53
53
|
/** Presents one workspace list as a select; MANUAL/cancel handled by caller. */
|
|
54
54
|
async function selectFrom(message, choices) {
|
|
55
|
+
const { isCancel, select } = await import("@clack/prompts");
|
|
55
56
|
const picked = await select({
|
|
56
57
|
message,
|
|
57
58
|
options: [...choices, {
|
|
@@ -69,6 +70,7 @@ async function selectFrom(message, choices) {
|
|
|
69
70
|
* cancel.
|
|
70
71
|
*/
|
|
71
72
|
async function autocompleteFrom(message, choices) {
|
|
73
|
+
const { autocomplete, isCancel } = await import("@clack/prompts");
|
|
72
74
|
const picked = await autocomplete({
|
|
73
75
|
message,
|
|
74
76
|
options: [...choices, {
|
|
@@ -164,6 +166,7 @@ async function syncEnv(rows, opts) {
|
|
|
164
166
|
* uses `values[fieldKey]` if provided, else leaves the field unset.
|
|
165
167
|
*/
|
|
166
168
|
async function collectBindingValues(needs, opts) {
|
|
169
|
+
const { isCancel, text } = await import("@clack/prompts");
|
|
167
170
|
const out = {};
|
|
168
171
|
for (const need of needs) {
|
|
169
172
|
const fromFlag = opts.values?.[need.fieldKey];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env-writer.js","names":[],"sources":["../../../../src/cli/commands/registry/env-writer.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { autocomplete, isCancel, select, text } from \"@clack/prompts\";\nimport pc from \"picocolors\";\n\nimport type { BindingValueNeed } from \"./config-plan\";\nimport {\n collectEnvNeeds,\n type EnvNeed,\n type EnvResolution,\n parseEnv,\n reconcileEnv,\n serializeEnvAppend,\n type ValueProvider,\n} from \"./env-reconcile\";\nimport type { ResourceRequirementRow } from \"./requirements\";\nimport {\n composeResourceId,\n isFlatListable,\n isParentContext,\n listParentContextStep,\n listWorkspaceResources,\n parentContextDepth,\n} from \"./workspace-picker\";\n\nexport interface EnvSyncOptions {\n /** Directory holding `.env` / `.env.example` (the app root). */\n cwd: string;\n /** true = never prompt (agent/CI). Uses flag values or leaves unset. */\n nonInteractive: boolean;\n /** Pre-supplied env values from flags, e.g. { DATABRICKS_WAREHOUSE_ID: \"abc\" }. */\n values?: Record<string, string>;\n /** Databricks profile for the workspace picker (else the CLI default). */\n profile?: string;\n}\n\n/** Sentinel select value meaning \"let me type the id myself\". */\nconst MANUAL = \"__manual__\";\n\n/**\n * Max resources shown in a picker select. Real workspaces can have thousands\n * (e.g. 5000+ SQL warehouses); an unbounded select is unusable. Beyond this we\n * show the first N and log how many were hidden — never silently drop — and the\n * \"Enter manually\" option always lets the user type an id the list omits.\n */\nconst PICKER_LIMIT = 25;\n\n/** Reads a `.env`-style file into a map; empty when the file is absent. */\nfunction readEnvFile(file: string): Record<string, string> {\n if (!fs.existsSync(file)) return {};\n return parseEnv(fs.readFileSync(file, \"utf-8\"));\n}\n\n/** Appends text to a file, creating it (with a trailing newline) if needed. */\nfunction appendToFile(file: string, text: string): void {\n if (text === \"\") return;\n if (fs.existsSync(file)) {\n const existing = fs.readFileSync(file, \"utf-8\");\n const sep = existing.length > 0 && !existing.endsWith(\"\\n\") ? \"\\n\" : \"\";\n fs.writeFileSync(file, existing + sep + text);\n } else {\n fs.writeFileSync(file, text);\n }\n}\n\n/**\n * Caps a choice list to {@link PICKER_LIMIT} for display, logging how many were\n * hidden so the truncation is never silent. The caller always appends an\n * \"Enter manually\" option, so an omitted resource is still reachable.\n */\nexport function capChoices<T>(\n choices: T[],\n resourceType: string,\n limit = PICKER_LIMIT,\n): T[] {\n if (choices.length <= limit) return choices;\n console.log(\n pc.dim(\n ` ${choices.length} ${resourceType}s found; showing first ${limit}. ` +\n 'Use \"Enter manually\" if yours is not listed.',\n ),\n );\n return choices.slice(0, limit);\n}\n\n/** Free-text prompt for one env need; undefined to skip. */\nasync function promptText(need: EnvNeed): Promise<string | undefined> {\n const tag = need.required ? \"required\" : \"optional\";\n const answer = await text({\n message: `${need.env} (${need.resourceType}, ${tag})`,\n placeholder: need.description ?? \"leave blank to skip\",\n });\n if (isCancel(answer)) return undefined;\n const value = (answer ?? \"\").trim();\n return value === \"\" ? undefined : value;\n}\n\n/** Presents one workspace list as a select; MANUAL/cancel handled by caller. */\nasync function selectFrom(\n message: string,\n choices: { value: string; label: string }[],\n): Promise<string | typeof MANUAL | null> {\n const picked = await select({\n message,\n options: [...choices, { value: MANUAL, label: \"Enter manually / skip\" }],\n });\n if (isCancel(picked)) return null;\n return String(picked) as string | typeof MANUAL;\n}\n\n/**\n * Type-to-filter picker over the full choice list (no cap): the user searches\n * by name/id as they type. Appends \"Enter manually\" so an omitted value is\n * still reachable. Returns MANUAL to fall through to free-text, or null on\n * cancel.\n */\nasync function autocompleteFrom(\n message: string,\n choices: { value: string; label: string }[],\n): Promise<string | typeof MANUAL | null> {\n const picked = await autocomplete({\n message,\n options: [...choices, { value: MANUAL, label: \"Enter manually / skip\" }],\n placeholder: \"type to search…\",\n });\n if (isCancel(picked)) return null;\n return String(picked) as string | typeof MANUAL;\n}\n\n/**\n * Drill-down picker for parent-context types (volume→catalog/schema,\n * secret→scope, vector_search_index→endpoint). Walks each step, listing the\n * next level from the prior pick. Returns the final resource id, or undefined\n * to fall back to free-text (on cancel, empty level, or MANUAL at any step).\n */\nasync function pickParentContext(\n need: EnvNeed,\n profile: string | undefined,\n): Promise<string | undefined> {\n const depth = parentContextDepth(need.resourceType);\n const picks: string[] = [];\n for (let i = 0; i < depth; i++) {\n const step = listParentContextStep(need.resourceType, i, picks, profile);\n if (!step || step.choices.length === 0) {\n console.log(\n pc.dim(\n ` No ${step?.key ?? need.resourceType} found — enter the id manually.`,\n ),\n );\n return undefined;\n }\n const picked = await selectFrom(\n `${need.env} — pick a ${step.key}`,\n capChoices(step.choices, step.key),\n );\n if (picked === null || picked === MANUAL) return undefined;\n picks.push(picked);\n }\n // Compose the id from the picks: most types end on a self-qualified id, but a\n // secret needs both scope and key (scope/key).\n return composeResourceId(need.resourceType, picks);\n}\n\n/**\n * Builds the value provider. Precedence: --env flag, then (interactive only) a\n * workspace picker — flat select for flat-listable types, drill-down for\n * parent-context types — else a free-text prompt. The picker degrades to\n * free-text whenever the workspace can't be listed (no profile, offline, auth\n * error, empty) so it never hard-fails.\n */\nfunction makeProvider(opts: EnvSyncOptions): ValueProvider {\n return async (need: EnvNeed) => {\n const fromFlag = opts.values?.[need.env];\n if (fromFlag !== undefined) return fromFlag;\n if (opts.nonInteractive) return undefined;\n\n if (isFlatListable(need.resourceType)) {\n const { choices, truncated, error } = await listWorkspaceResources(\n need.resourceType,\n opts.profile,\n );\n if (choices.length > 0) {\n if (truncated) {\n console.log(\n pc.dim(\n ` Showing the first ${choices.length} ${need.resourceType}s; use \"Enter manually\" if yours isn't listed.`,\n ),\n );\n }\n const picked = await autocompleteFrom(\n `${need.env} — search ${need.resourceType}s`,\n choices,\n );\n if (picked === null) return undefined;\n if (picked !== MANUAL) return picked;\n // fall through to free-text\n } else if (error) {\n // Listing failed (usually auth/profile) — say so, don't pretend the\n // workspace is empty, and point at the fix.\n console.log(\n pc.yellow(\n ` Couldn't list ${need.resourceType}s from the workspace (${error}).`,\n ),\n );\n console.log(\n pc.dim(\n \" Enter an id manually, or re-run with --profile <name> (or set DATABRICKS_CONFIG_PROFILE) so the picker can reach the workspace.\",\n ),\n );\n } else {\n console.log(\n pc.dim(\n ` No ${need.resourceType} found in the workspace — enter an id manually.`,\n ),\n );\n }\n } else if (isParentContext(need.resourceType)) {\n const picked = await pickParentContext(need, opts.profile);\n if (picked !== undefined) return picked;\n // fall through to free-text\n }\n\n return promptText(need);\n };\n}\n\n/**\n * Reconciles a plugin's declared resource env vars into the app's local `.env`\n * (and mirrors variable names into `.env.example`). Never overwrites keys the\n * user already set; skips platform-injected fields. Returns the per-var\n * resolutions so callers can report what happened.\n */\nexport async function syncEnv(\n rows: ResourceRequirementRow[],\n opts: EnvSyncOptions,\n): Promise<EnvResolution[]> {\n const needs = collectEnvNeeds(rows);\n if (needs.length === 0) return [];\n\n const envPath = path.join(opts.cwd, \".env\");\n const examplePath = path.join(opts.cwd, \".env.example\");\n const existing = readEnvFile(envPath);\n\n const resolutions = await reconcileEnv(needs, {\n existing,\n provide: makeProvider(opts),\n });\n\n const written = resolutions.filter(\n (r): r is EnvResolution & { value: string } =>\n r.status === \"written\" && r.value !== undefined,\n );\n appendToFile(\n envPath,\n serializeEnvAppend(written.map((r) => ({ env: r.env, value: r.value }))),\n );\n\n // .env.example carries the variable names (no secret values), and only for\n // vars not already documented there.\n const exampleExisting = readEnvFile(examplePath);\n const newExampleKeys = needs.filter((n) => !(n.env in exampleExisting));\n appendToFile(\n examplePath,\n serializeEnvAppend(newExampleKeys.map((n) => ({ env: n.env, value: \"\" }))),\n );\n\n return resolutions;\n}\n\n/**\n * Prompts for the {@link BindingValueNeed}s that `.env` reconciliation can't\n * collect, returning a `fieldKey -> value` map for buildConfigPlan. Values are\n * not written to `.env` (these fields have no env var). Non-interactive mode\n * uses `values[fieldKey]` if provided, else leaves the field unset.\n */\nexport async function collectBindingValues(\n needs: BindingValueNeed[],\n opts: EnvSyncOptions,\n): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n for (const need of needs) {\n const fromFlag = opts.values?.[need.fieldKey];\n if (fromFlag !== undefined) {\n out[need.fieldKey] = fromFlag;\n continue;\n }\n if (opts.nonInteractive) continue;\n const answer = await text({\n message: `${need.fieldKey} (${need.resourceType}) — required for databricks.yml`,\n placeholder: need.description ?? \"leave blank to set before deploy\",\n });\n if (isCancel(answer)) continue;\n const value = (answer ?? \"\").trim();\n if (value !== \"\") out[need.fieldKey] = value;\n }\n return out;\n}\n\n/** Prints a concise summary of what env reconciliation did. */\nexport function reportEnvResolutions(resolutions: EnvResolution[]): void {\n if (resolutions.length === 0) return;\n const written = resolutions.filter((r) => r.status === \"written\");\n const already = resolutions.filter((r) => r.status === \"already-set\");\n const skipped = resolutions.filter((r) => r.status === \"skipped\");\n\n if (written.length > 0) {\n console.log(\n `${pc.green(\"Wrote to .env:\")} ${written.map((r) => r.env).join(\", \")}`,\n );\n }\n if (already.length > 0) {\n console.log(pc.dim(`Already set: ${already.map((r) => r.env).join(\", \")}`));\n }\n if (skipped.length > 0) {\n console.log(\n `${pc.yellow(\"Left unset (set before deploy):\")} ${skipped\n .map((r) => r.env)\n .join(\", \")}`,\n );\n }\n}\n"],"mappings":";;;;;;;;;AAsCA,MAAM,SAAS;;;;;;;AAQf,MAAM,eAAe;;AAGrB,SAAS,YAAY,MAAsC;AACzD,KAAI,CAAC,GAAG,WAAW,KAAK,CAAE,QAAO,EAAE;AACnC,QAAO,SAAS,GAAG,aAAa,MAAM,QAAQ,CAAC;;;AAIjD,SAAS,aAAa,MAAc,MAAoB;AACtD,KAAI,SAAS,GAAI;AACjB,KAAI,GAAG,WAAW,KAAK,EAAE;EACvB,MAAM,WAAW,GAAG,aAAa,MAAM,QAAQ;EAC/C,MAAM,MAAM,SAAS,SAAS,KAAK,CAAC,SAAS,SAAS,KAAK,GAAG,OAAO;AACrE,KAAG,cAAc,MAAM,WAAW,MAAM,KAAK;OAE7C,IAAG,cAAc,MAAM,KAAK;;;;;;;AAShC,SAAgB,WACd,SACA,cACA,QAAQ,cACH;AACL,KAAI,QAAQ,UAAU,MAAO,QAAO;AACpC,SAAQ,IACN,GAAG,IACD,KAAK,QAAQ,OAAO,GAAG,aAAa,yBAAyB,MAAM,gDAEpE,CACF;AACD,QAAO,QAAQ,MAAM,GAAG,MAAM;;;AAIhC,eAAe,WAAW,MAA4C;CACpE,MAAM,MAAM,KAAK,WAAW,aAAa;CACzC,MAAM,SAAS,MAAM,KAAK;EACxB,SAAS,GAAG,KAAK,IAAI,IAAI,KAAK,aAAa,IAAI,IAAI;EACnD,aAAa,KAAK,eAAe;EAClC,CAAC;AACF,KAAI,SAAS,OAAO,CAAE,QAAO;CAC7B,MAAM,SAAS,UAAU,IAAI,MAAM;AACnC,QAAO,UAAU,KAAK,SAAY;;;AAIpC,eAAe,WACb,SACA,SACwC;CACxC,MAAM,SAAS,MAAM,OAAO;EAC1B;EACA,SAAS,CAAC,GAAG,SAAS;GAAE,OAAO;GAAQ,OAAO;GAAyB,CAAC;EACzE,CAAC;AACF,KAAI,SAAS,OAAO,CAAE,QAAO;AAC7B,QAAO,OAAO,OAAO;;;;;;;;AASvB,eAAe,iBACb,SACA,SACwC;CACxC,MAAM,SAAS,MAAM,aAAa;EAChC;EACA,SAAS,CAAC,GAAG,SAAS;GAAE,OAAO;GAAQ,OAAO;GAAyB,CAAC;EACxE,aAAa;EACd,CAAC;AACF,KAAI,SAAS,OAAO,CAAE,QAAO;AAC7B,QAAO,OAAO,OAAO;;;;;;;;AASvB,eAAe,kBACb,MACA,SAC6B;CAC7B,MAAM,QAAQ,mBAAmB,KAAK,aAAa;CACnD,MAAM,QAAkB,EAAE;AAC1B,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK;EAC9B,MAAM,OAAO,sBAAsB,KAAK,cAAc,GAAG,OAAO,QAAQ;AACxE,MAAI,CAAC,QAAQ,KAAK,QAAQ,WAAW,GAAG;AACtC,WAAQ,IACN,GAAG,IACD,QAAQ,MAAM,OAAO,KAAK,aAAa,iCACxC,CACF;AACD;;EAEF,MAAM,SAAS,MAAM,WACnB,GAAG,KAAK,IAAI,YAAY,KAAK,OAC7B,WAAW,KAAK,SAAS,KAAK,IAAI,CACnC;AACD,MAAI,WAAW,QAAQ,WAAW,OAAQ,QAAO;AACjD,QAAM,KAAK,OAAO;;AAIpB,QAAO,kBAAkB,KAAK,cAAc,MAAM;;;;;;;;;AAUpD,SAAS,aAAa,MAAqC;AACzD,QAAO,OAAO,SAAkB;EAC9B,MAAM,WAAW,KAAK,SAAS,KAAK;AACpC,MAAI,aAAa,OAAW,QAAO;AACnC,MAAI,KAAK,eAAgB,QAAO;AAEhC,MAAI,eAAe,KAAK,aAAa,EAAE;GACrC,MAAM,EAAE,SAAS,WAAW,UAAU,MAAM,uBAC1C,KAAK,cACL,KAAK,QACN;AACD,OAAI,QAAQ,SAAS,GAAG;AACtB,QAAI,UACF,SAAQ,IACN,GAAG,IACD,uBAAuB,QAAQ,OAAO,GAAG,KAAK,aAAa,gDAC5D,CACF;IAEH,MAAM,SAAS,MAAM,iBACnB,GAAG,KAAK,IAAI,YAAY,KAAK,aAAa,IAC1C,QACD;AACD,QAAI,WAAW,KAAM,QAAO;AAC5B,QAAI,WAAW,OAAQ,QAAO;cAErB,OAAO;AAGhB,YAAQ,IACN,GAAG,OACD,mBAAmB,KAAK,aAAa,wBAAwB,MAAM,IACpE,CACF;AACD,YAAQ,IACN,GAAG,IACD,oIACD,CACF;SAED,SAAQ,IACN,GAAG,IACD,QAAQ,KAAK,aAAa,iDAC3B,CACF;aAEM,gBAAgB,KAAK,aAAa,EAAE;GAC7C,MAAM,SAAS,MAAM,kBAAkB,MAAM,KAAK,QAAQ;AAC1D,OAAI,WAAW,OAAW,QAAO;;AAInC,SAAO,WAAW,KAAK;;;;;;;;;AAU3B,eAAsB,QACpB,MACA,MAC0B;CAC1B,MAAM,QAAQ,gBAAgB,KAAK;AACnC,KAAI,MAAM,WAAW,EAAG,QAAO,EAAE;CAEjC,MAAM,UAAU,KAAK,KAAK,KAAK,KAAK,OAAO;CAC3C,MAAM,cAAc,KAAK,KAAK,KAAK,KAAK,eAAe;CAGvD,MAAM,cAAc,MAAM,aAAa,OAAO;EAC5C,UAHe,YAAY,QAAQ;EAInC,SAAS,aAAa,KAAK;EAC5B,CAAC;AAMF,cACE,SACA,mBANc,YAAY,QACzB,MACC,EAAE,WAAW,aAAa,EAAE,UAAU,OACzC,CAG4B,KAAK,OAAO;EAAE,KAAK,EAAE;EAAK,OAAO,EAAE;EAAO,EAAE,CAAC,CACzE;CAID,MAAM,kBAAkB,YAAY,YAAY;AAEhD,cACE,aACA,mBAHqB,MAAM,QAAQ,MAAM,EAAE,EAAE,OAAO,iBAAiB,CAGnC,KAAK,OAAO;EAAE,KAAK,EAAE;EAAK,OAAO;EAAI,EAAE,CAAC,CAC3E;AAED,QAAO;;;;;;;;AAST,eAAsB,qBACpB,OACA,MACiC;CACjC,MAAM,MAA8B,EAAE;AACtC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,WAAW,KAAK,SAAS,KAAK;AACpC,MAAI,aAAa,QAAW;AAC1B,OAAI,KAAK,YAAY;AACrB;;AAEF,MAAI,KAAK,eAAgB;EACzB,MAAM,SAAS,MAAM,KAAK;GACxB,SAAS,GAAG,KAAK,SAAS,IAAI,KAAK,aAAa;GAChD,aAAa,KAAK,eAAe;GAClC,CAAC;AACF,MAAI,SAAS,OAAO,CAAE;EACtB,MAAM,SAAS,UAAU,IAAI,MAAM;AACnC,MAAI,UAAU,GAAI,KAAI,KAAK,YAAY;;AAEzC,QAAO;;;AAIT,SAAgB,qBAAqB,aAAoC;AACvE,KAAI,YAAY,WAAW,EAAG;CAC9B,MAAM,UAAU,YAAY,QAAQ,MAAM,EAAE,WAAW,UAAU;CACjE,MAAM,UAAU,YAAY,QAAQ,MAAM,EAAE,WAAW,cAAc;CACrE,MAAM,UAAU,YAAY,QAAQ,MAAM,EAAE,WAAW,UAAU;AAEjE,KAAI,QAAQ,SAAS,EACnB,SAAQ,IACN,GAAG,GAAG,MAAM,iBAAiB,CAAC,GAAG,QAAQ,KAAK,MAAM,EAAE,IAAI,CAAC,KAAK,KAAK,GACtE;AAEH,KAAI,QAAQ,SAAS,EACnB,SAAQ,IAAI,GAAG,IAAI,gBAAgB,QAAQ,KAAK,MAAM,EAAE,IAAI,CAAC,KAAK,KAAK,GAAG,CAAC;AAE7E,KAAI,QAAQ,SAAS,EACnB,SAAQ,IACN,GAAG,GAAG,OAAO,kCAAkC,CAAC,GAAG,QAChD,KAAK,MAAM,EAAE,IAAI,CACjB,KAAK,KAAK,GACd"}
|
|
1
|
+
{"version":3,"file":"env-writer.js","names":[],"sources":["../../../../src/cli/commands/registry/env-writer.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport pc from \"picocolors\";\n\nimport type { BindingValueNeed } from \"./config-plan\";\nimport {\n collectEnvNeeds,\n type EnvNeed,\n type EnvResolution,\n parseEnv,\n reconcileEnv,\n serializeEnvAppend,\n type ValueProvider,\n} from \"./env-reconcile\";\nimport type { ResourceRequirementRow } from \"./requirements\";\nimport {\n composeResourceId,\n isFlatListable,\n isParentContext,\n listParentContextStep,\n listWorkspaceResources,\n parentContextDepth,\n} from \"./workspace-picker\";\n\nexport interface EnvSyncOptions {\n /** Directory holding `.env` / `.env.example` (the app root). */\n cwd: string;\n /** true = never prompt (agent/CI). Uses flag values or leaves unset. */\n nonInteractive: boolean;\n /** Pre-supplied env values from flags, e.g. { DATABRICKS_WAREHOUSE_ID: \"abc\" }. */\n values?: Record<string, string>;\n /** Databricks profile for the workspace picker (else the CLI default). */\n profile?: string;\n}\n\n/** Sentinel select value meaning \"let me type the id myself\". */\nconst MANUAL = \"__manual__\";\n\n/**\n * Max resources shown in a picker select. Real workspaces can have thousands\n * (e.g. 5000+ SQL warehouses); an unbounded select is unusable. Beyond this we\n * show the first N and log how many were hidden — never silently drop — and the\n * \"Enter manually\" option always lets the user type an id the list omits.\n */\nconst PICKER_LIMIT = 25;\n\n/** Reads a `.env`-style file into a map; empty when the file is absent. */\nfunction readEnvFile(file: string): Record<string, string> {\n if (!fs.existsSync(file)) return {};\n return parseEnv(fs.readFileSync(file, \"utf-8\"));\n}\n\n/** Appends text to a file, creating it (with a trailing newline) if needed. */\nfunction appendToFile(file: string, text: string): void {\n if (text === \"\") return;\n if (fs.existsSync(file)) {\n const existing = fs.readFileSync(file, \"utf-8\");\n const sep = existing.length > 0 && !existing.endsWith(\"\\n\") ? \"\\n\" : \"\";\n fs.writeFileSync(file, existing + sep + text);\n } else {\n fs.writeFileSync(file, text);\n }\n}\n\n/**\n * Caps a choice list to {@link PICKER_LIMIT} for display, logging how many were\n * hidden so the truncation is never silent. The caller always appends an\n * \"Enter manually\" option, so an omitted resource is still reachable.\n */\nexport function capChoices<T>(\n choices: T[],\n resourceType: string,\n limit = PICKER_LIMIT,\n): T[] {\n if (choices.length <= limit) return choices;\n console.log(\n pc.dim(\n ` ${choices.length} ${resourceType}s found; showing first ${limit}. ` +\n 'Use \"Enter manually\" if yours is not listed.',\n ),\n );\n return choices.slice(0, limit);\n}\n\n/** Free-text prompt for one env need; undefined to skip. */\nasync function promptText(need: EnvNeed): Promise<string | undefined> {\n const { isCancel, text } = await import(\"@clack/prompts\");\n const tag = need.required ? \"required\" : \"optional\";\n const answer = await text({\n message: `${need.env} (${need.resourceType}, ${tag})`,\n placeholder: need.description ?? \"leave blank to skip\",\n });\n if (isCancel(answer)) return undefined;\n const value = (answer ?? \"\").trim();\n return value === \"\" ? undefined : value;\n}\n\n/** Presents one workspace list as a select; MANUAL/cancel handled by caller. */\nasync function selectFrom(\n message: string,\n choices: { value: string; label: string }[],\n): Promise<string | typeof MANUAL | null> {\n const { isCancel, select } = await import(\"@clack/prompts\");\n const picked = await select({\n message,\n options: [...choices, { value: MANUAL, label: \"Enter manually / skip\" }],\n });\n if (isCancel(picked)) return null;\n return String(picked) as string | typeof MANUAL;\n}\n\n/**\n * Type-to-filter picker over the full choice list (no cap): the user searches\n * by name/id as they type. Appends \"Enter manually\" so an omitted value is\n * still reachable. Returns MANUAL to fall through to free-text, or null on\n * cancel.\n */\nasync function autocompleteFrom(\n message: string,\n choices: { value: string; label: string }[],\n): Promise<string | typeof MANUAL | null> {\n const { autocomplete, isCancel } = await import(\"@clack/prompts\");\n const picked = await autocomplete({\n message,\n options: [...choices, { value: MANUAL, label: \"Enter manually / skip\" }],\n placeholder: \"type to search…\",\n });\n if (isCancel(picked)) return null;\n return String(picked) as string | typeof MANUAL;\n}\n\n/**\n * Drill-down picker for parent-context types (volume→catalog/schema,\n * secret→scope, vector_search_index→endpoint). Walks each step, listing the\n * next level from the prior pick. Returns the final resource id, or undefined\n * to fall back to free-text (on cancel, empty level, or MANUAL at any step).\n */\nasync function pickParentContext(\n need: EnvNeed,\n profile: string | undefined,\n): Promise<string | undefined> {\n const depth = parentContextDepth(need.resourceType);\n const picks: string[] = [];\n for (let i = 0; i < depth; i++) {\n const step = listParentContextStep(need.resourceType, i, picks, profile);\n if (!step || step.choices.length === 0) {\n console.log(\n pc.dim(\n ` No ${step?.key ?? need.resourceType} found — enter the id manually.`,\n ),\n );\n return undefined;\n }\n const picked = await selectFrom(\n `${need.env} — pick a ${step.key}`,\n capChoices(step.choices, step.key),\n );\n if (picked === null || picked === MANUAL) return undefined;\n picks.push(picked);\n }\n // Compose the id from the picks: most types end on a self-qualified id, but a\n // secret needs both scope and key (scope/key).\n return composeResourceId(need.resourceType, picks);\n}\n\n/**\n * Builds the value provider. Precedence: --env flag, then (interactive only) a\n * workspace picker — flat select for flat-listable types, drill-down for\n * parent-context types — else a free-text prompt. The picker degrades to\n * free-text whenever the workspace can't be listed (no profile, offline, auth\n * error, empty) so it never hard-fails.\n */\nfunction makeProvider(opts: EnvSyncOptions): ValueProvider {\n return async (need: EnvNeed) => {\n const fromFlag = opts.values?.[need.env];\n if (fromFlag !== undefined) return fromFlag;\n if (opts.nonInteractive) return undefined;\n\n if (isFlatListable(need.resourceType)) {\n const { choices, truncated, error } = await listWorkspaceResources(\n need.resourceType,\n opts.profile,\n );\n if (choices.length > 0) {\n if (truncated) {\n console.log(\n pc.dim(\n ` Showing the first ${choices.length} ${need.resourceType}s; use \"Enter manually\" if yours isn't listed.`,\n ),\n );\n }\n const picked = await autocompleteFrom(\n `${need.env} — search ${need.resourceType}s`,\n choices,\n );\n if (picked === null) return undefined;\n if (picked !== MANUAL) return picked;\n // fall through to free-text\n } else if (error) {\n // Listing failed (usually auth/profile) — say so, don't pretend the\n // workspace is empty, and point at the fix.\n console.log(\n pc.yellow(\n ` Couldn't list ${need.resourceType}s from the workspace (${error}).`,\n ),\n );\n console.log(\n pc.dim(\n \" Enter an id manually, or re-run with --profile <name> (or set DATABRICKS_CONFIG_PROFILE) so the picker can reach the workspace.\",\n ),\n );\n } else {\n console.log(\n pc.dim(\n ` No ${need.resourceType} found in the workspace — enter an id manually.`,\n ),\n );\n }\n } else if (isParentContext(need.resourceType)) {\n const picked = await pickParentContext(need, opts.profile);\n if (picked !== undefined) return picked;\n // fall through to free-text\n }\n\n return promptText(need);\n };\n}\n\n/**\n * Reconciles a plugin's declared resource env vars into the app's local `.env`\n * (and mirrors variable names into `.env.example`). Never overwrites keys the\n * user already set; skips platform-injected fields. Returns the per-var\n * resolutions so callers can report what happened.\n */\nexport async function syncEnv(\n rows: ResourceRequirementRow[],\n opts: EnvSyncOptions,\n): Promise<EnvResolution[]> {\n const needs = collectEnvNeeds(rows);\n if (needs.length === 0) return [];\n\n const envPath = path.join(opts.cwd, \".env\");\n const examplePath = path.join(opts.cwd, \".env.example\");\n const existing = readEnvFile(envPath);\n\n const resolutions = await reconcileEnv(needs, {\n existing,\n provide: makeProvider(opts),\n });\n\n const written = resolutions.filter(\n (r): r is EnvResolution & { value: string } =>\n r.status === \"written\" && r.value !== undefined,\n );\n appendToFile(\n envPath,\n serializeEnvAppend(written.map((r) => ({ env: r.env, value: r.value }))),\n );\n\n // .env.example carries the variable names (no secret values), and only for\n // vars not already documented there.\n const exampleExisting = readEnvFile(examplePath);\n const newExampleKeys = needs.filter((n) => !(n.env in exampleExisting));\n appendToFile(\n examplePath,\n serializeEnvAppend(newExampleKeys.map((n) => ({ env: n.env, value: \"\" }))),\n );\n\n return resolutions;\n}\n\n/**\n * Prompts for the {@link BindingValueNeed}s that `.env` reconciliation can't\n * collect, returning a `fieldKey -> value` map for buildConfigPlan. Values are\n * not written to `.env` (these fields have no env var). Non-interactive mode\n * uses `values[fieldKey]` if provided, else leaves the field unset.\n */\nexport async function collectBindingValues(\n needs: BindingValueNeed[],\n opts: EnvSyncOptions,\n): Promise<Record<string, string>> {\n const { isCancel, text } = await import(\"@clack/prompts\");\n const out: Record<string, string> = {};\n for (const need of needs) {\n const fromFlag = opts.values?.[need.fieldKey];\n if (fromFlag !== undefined) {\n out[need.fieldKey] = fromFlag;\n continue;\n }\n if (opts.nonInteractive) continue;\n const answer = await text({\n message: `${need.fieldKey} (${need.resourceType}) — required for databricks.yml`,\n placeholder: need.description ?? \"leave blank to set before deploy\",\n });\n if (isCancel(answer)) continue;\n const value = (answer ?? \"\").trim();\n if (value !== \"\") out[need.fieldKey] = value;\n }\n return out;\n}\n\n/** Prints a concise summary of what env reconciliation did. */\nexport function reportEnvResolutions(resolutions: EnvResolution[]): void {\n if (resolutions.length === 0) return;\n const written = resolutions.filter((r) => r.status === \"written\");\n const already = resolutions.filter((r) => r.status === \"already-set\");\n const skipped = resolutions.filter((r) => r.status === \"skipped\");\n\n if (written.length > 0) {\n console.log(\n `${pc.green(\"Wrote to .env:\")} ${written.map((r) => r.env).join(\", \")}`,\n );\n }\n if (already.length > 0) {\n console.log(pc.dim(`Already set: ${already.map((r) => r.env).join(\", \")}`));\n }\n if (skipped.length > 0) {\n console.log(\n `${pc.yellow(\"Left unset (set before deploy):\")} ${skipped\n .map((r) => r.env)\n .join(\", \")}`,\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"judge.d.ts","names":[],"sources":["../../src/evals/judge.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"judge.d.ts","names":[],"sources":["../../src/evals/judge.ts"],"mappings":";;;;UA0BiB,WAAA;;EAEf,MAAA,EAAQ,YAAA;EAFO;EAIf,KAAA;;EAEA,KAAA;AAAA;;UAIe,UAAA;EACf,KAAA;EACA,SAAA;AAAA;AAFF;;;;;AAAA,iBAUsB,cAAA,CAAe,MAAA,EAAQ,WAAA,GAAc,OAAA;AAAA,iBAgB3C,iBAAA,CAAA"}
|
package/dist/evals/judge.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//#region src/evals/judge.ts
|
|
2
2
|
let mod;
|
|
3
3
|
let enabled = false;
|
|
4
|
+
let notInstalled = false;
|
|
4
5
|
let configured = false;
|
|
5
6
|
let prevBaseUrl;
|
|
6
7
|
let prevApiKey;
|
|
@@ -19,8 +20,9 @@ async function configureJudge(config) {
|
|
|
19
20
|
configured = true;
|
|
20
21
|
mod.init({ defaultModel: config.model });
|
|
21
22
|
enabled = true;
|
|
22
|
-
} catch {
|
|
23
|
+
} catch (err) {
|
|
23
24
|
enabled = false;
|
|
25
|
+
notInstalled = isModuleNotFound(err);
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
function isJudgeConfigured() {
|
|
@@ -44,6 +46,10 @@ function restoreEnv(key, prev) {
|
|
|
44
46
|
if (prev === void 0) delete process.env[key];
|
|
45
47
|
else process.env[key] = prev;
|
|
46
48
|
}
|
|
49
|
+
/** True when a dynamic `import()` failed because the package isn't installed. */
|
|
50
|
+
function isModuleNotFound(err) {
|
|
51
|
+
return !!err && typeof err === "object" && "code" in err && (err.code === "ERR_MODULE_NOT_FOUND" || err.code === "MODULE_NOT_FOUND");
|
|
52
|
+
}
|
|
47
53
|
/** Normalize an autoevals `Score` into a `JudgeScore`. */
|
|
48
54
|
function toJudgeScore(s) {
|
|
49
55
|
const rationale = s.metadata?.rationale;
|
|
@@ -53,7 +59,7 @@ function toJudgeScore(s) {
|
|
|
53
59
|
};
|
|
54
60
|
}
|
|
55
61
|
function ensure() {
|
|
56
|
-
if (!enabled || !mod) throw new Error("LLM judge is not configured. Pass --judge-model and authenticate via --profile (or DATABRICKS_HOST/DATABRICKS_TOKEN) to use t.judge.*");
|
|
62
|
+
if (!enabled || !mod) throw new Error(notInstalled ? "LLM judge requires the optional `autoevals` package. Install it to use t.judge.*: npm i autoevals" : "LLM judge is not configured. Pass --judge-model and authenticate via --profile (or DATABRICKS_HOST/DATABRICKS_TOKEN) to use t.judge.*");
|
|
57
63
|
return mod;
|
|
58
64
|
}
|
|
59
65
|
/** Factuality of `output` vs an `expected` reference. */
|
package/dist/evals/judge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"judge.js","names":[],"sources":["../../src/evals/judge.ts"],"sourcesContent":["import type { MlflowClient } from \"../connectors/mlflow\";\n\n/**\n * LLM-as-judge scoring via the `autoevals` library (the same scorers eve uses),\n * pointed at a Databricks serving endpoint. autoevals talks to an\n * OpenAI-compatible API; Databricks Model Serving exposes one at\n * `<host>/serving-endpoints`, so we set `OPENAI_BASE_URL`/`OPENAI_API_KEY` and\n * use the judge endpoint name as the model.\n *\n * There is no public REST to call Databricks' built-in judges directly (they're\n * Python/SDK-only and the rubric prompts live in the mlflow package), so we run\n * autoevals' equivalent scorers against a Databricks judge model.\n */\ntype AutoEvals = typeof import(\"autoevals\");\n\nlet mod: AutoEvals | undefined;\nlet enabled = false;\n// Whether configureJudge overwrote the OPENAI_* env vars and they still need\n// restoring, plus the values to restore them to.\nlet configured = false;\nlet prevBaseUrl: string | undefined;\nlet prevApiKey: string | undefined;\n\nexport interface JudgeConfig {\n /** Client for the workspace hosting the judge serving endpoint. */\n client: MlflowClient;\n /** Bearer token for the serving endpoint. */\n token: string;\n /** Serving endpoint name used as the judge model. */\n model: string;\n}\n\n/** A normalized judge result. `score` is 0..1. */\nexport interface JudgeScore {\n score: number;\n rationale?: string;\n}\n\n/**\n * Configure the judge once. Sets the OpenAI-compatible client env autoevals\n * reads and the default judge model. No-op-safe: on failure, judging stays\n * disabled and {@link isJudgeConfigured} returns false.\n */\nexport async function configureJudge(config: JudgeConfig): Promise<void> {\n try {\n mod = await import(\"autoevals\");\n prevBaseUrl = process.env.OPENAI_BASE_URL;\n prevApiKey = process.env.OPENAI_API_KEY;\n process.env.OPENAI_BASE_URL = config.client.servingEndpointsUrl();\n process.env.OPENAI_API_KEY = config.token;\n configured = true;\n mod.init({ defaultModel: config.model });\n enabled = true;\n } catch {\n enabled = false;\n }\n}\n\nexport function isJudgeConfigured(): boolean {\n return enabled;\n}\n\n/**\n * Restore the `OPENAI_*` env vars {@link configureJudge} set, so the judge\n * bearer doesn't linger in `process.env` (readable by any imported eval code)\n * after the run. Call once the run is done; safe when the judge was never\n * configured. Also disables judging so a late `t.judge.*` call fails cleanly\n * rather than hitting a torn-down client.\n */\nexport function teardownJudge(): void {\n if (!configured) return;\n restoreEnv(\"OPENAI_BASE_URL\", prevBaseUrl);\n restoreEnv(\"OPENAI_API_KEY\", prevApiKey);\n configured = false;\n enabled = false;\n}\n\nfunction restoreEnv(key: string, prev: string | undefined): void {\n if (prev === undefined) delete process.env[key];\n else process.env[key] = prev;\n}\n\n/** Normalize an autoevals `Score` into a `JudgeScore`. */\nexport function toJudgeScore(s: {\n score?: number | null;\n metadata?: Record<string, unknown>;\n}): JudgeScore {\n const rationale = s.metadata?.rationale;\n return {\n score: typeof s.score === \"number\" ? s.score : 0,\n rationale: typeof rationale === \"string\" ? rationale : undefined,\n };\n}\n\nfunction ensure(): AutoEvals {\n if (!enabled || !mod) {\n throw new Error(\n \"LLM judge is not configured. Pass --judge-model and authenticate via --profile (or DATABRICKS_HOST/DATABRICKS_TOKEN) to use t.judge.*\",\n );\n }\n return mod;\n}\n\n/** Factuality of `output` vs an `expected` reference. */\nexport async function judgeFactuality(args: {\n input: string;\n output: string;\n expected: string;\n}): Promise<JudgeScore> {\n return toJudgeScore(await ensure().Factuality(args));\n}\n\n/** Whether `output` answers the question in `input`, optionally constrained by `criteria`. */\nexport async function judgeClosedQA(args: {\n input: string;\n output: string;\n criteria: string;\n}): Promise<JudgeScore> {\n return toJudgeScore(await ensure().ClosedQA(args));\n}\n\n/**\n * A custom LLM judge defined by a prompt template + choice→score map — the\n * TypeScript analog of MLflow's custom `@scorer`.\n */\nexport async function judgeCustom(\n spec: {\n name: string;\n promptTemplate: string;\n choiceScores: Record<string, number>;\n },\n args: { input: string; output: string },\n): Promise<JudgeScore> {\n const scorer = ensure().LLMClassifierFromTemplate(spec);\n return toJudgeScore(await scorer(args));\n}\n"],"mappings":";AAeA,IAAI;AACJ,IAAI,UAAU;AAGd,IAAI,aAAa;AACjB,IAAI;AACJ,IAAI;;;;;;AAsBJ,eAAsB,eAAe,QAAoC;AACvE,KAAI;AACF,QAAM,MAAM,OAAO;AACnB,gBAAc,QAAQ,IAAI;AAC1B,eAAa,QAAQ,IAAI;AACzB,UAAQ,IAAI,kBAAkB,OAAO,OAAO,qBAAqB;AACjE,UAAQ,IAAI,iBAAiB,OAAO;AACpC,eAAa;AACb,MAAI,KAAK,EAAE,cAAc,OAAO,OAAO,CAAC;AACxC,YAAU;
|
|
1
|
+
{"version":3,"file":"judge.js","names":[],"sources":["../../src/evals/judge.ts"],"sourcesContent":["import type { MlflowClient } from \"../connectors/mlflow\";\n\n/**\n * LLM-as-judge scoring via the `autoevals` library (the same scorers eve uses),\n * pointed at a Databricks serving endpoint. autoevals talks to an\n * OpenAI-compatible API; Databricks Model Serving exposes one at\n * `<host>/serving-endpoints`, so we set `OPENAI_BASE_URL`/`OPENAI_API_KEY` and\n * use the judge endpoint name as the model.\n *\n * There is no public REST to call Databricks' built-in judges directly (they're\n * Python/SDK-only and the rubric prompts live in the mlflow package), so we run\n * autoevals' equivalent scorers against a Databricks judge model.\n */\ntype AutoEvals = typeof import(\"autoevals\");\n\nlet mod: AutoEvals | undefined;\nlet enabled = false;\n// Set when the dynamic import failed because `autoevals` (an optional peer) is\n// not installed, so `ensure()` can point at the fix instead of the auth path.\nlet notInstalled = false;\n// Whether configureJudge overwrote the OPENAI_* env vars and they still need\n// restoring, plus the values to restore them to.\nlet configured = false;\nlet prevBaseUrl: string | undefined;\nlet prevApiKey: string | undefined;\n\nexport interface JudgeConfig {\n /** Client for the workspace hosting the judge serving endpoint. */\n client: MlflowClient;\n /** Bearer token for the serving endpoint. */\n token: string;\n /** Serving endpoint name used as the judge model. */\n model: string;\n}\n\n/** A normalized judge result. `score` is 0..1. */\nexport interface JudgeScore {\n score: number;\n rationale?: string;\n}\n\n/**\n * Configure the judge once. Sets the OpenAI-compatible client env autoevals\n * reads and the default judge model. No-op-safe: on failure, judging stays\n * disabled and {@link isJudgeConfigured} returns false.\n */\nexport async function configureJudge(config: JudgeConfig): Promise<void> {\n try {\n mod = await import(\"autoevals\");\n prevBaseUrl = process.env.OPENAI_BASE_URL;\n prevApiKey = process.env.OPENAI_API_KEY;\n process.env.OPENAI_BASE_URL = config.client.servingEndpointsUrl();\n process.env.OPENAI_API_KEY = config.token;\n configured = true;\n mod.init({ defaultModel: config.model });\n enabled = true;\n } catch (err) {\n enabled = false;\n notInstalled = isModuleNotFound(err);\n }\n}\n\nexport function isJudgeConfigured(): boolean {\n return enabled;\n}\n\n/**\n * Restore the `OPENAI_*` env vars {@link configureJudge} set, so the judge\n * bearer doesn't linger in `process.env` (readable by any imported eval code)\n * after the run. Call once the run is done; safe when the judge was never\n * configured. Also disables judging so a late `t.judge.*` call fails cleanly\n * rather than hitting a torn-down client.\n */\nexport function teardownJudge(): void {\n if (!configured) return;\n restoreEnv(\"OPENAI_BASE_URL\", prevBaseUrl);\n restoreEnv(\"OPENAI_API_KEY\", prevApiKey);\n configured = false;\n enabled = false;\n}\n\nfunction restoreEnv(key: string, prev: string | undefined): void {\n if (prev === undefined) delete process.env[key];\n else process.env[key] = prev;\n}\n\n/** True when a dynamic `import()` failed because the package isn't installed. */\nfunction isModuleNotFound(err: unknown): boolean {\n return (\n !!err &&\n typeof err === \"object\" &&\n \"code\" in err &&\n (err.code === \"ERR_MODULE_NOT_FOUND\" || err.code === \"MODULE_NOT_FOUND\")\n );\n}\n\n/** Normalize an autoevals `Score` into a `JudgeScore`. */\nexport function toJudgeScore(s: {\n score?: number | null;\n metadata?: Record<string, unknown>;\n}): JudgeScore {\n const rationale = s.metadata?.rationale;\n return {\n score: typeof s.score === \"number\" ? s.score : 0,\n rationale: typeof rationale === \"string\" ? rationale : undefined,\n };\n}\n\nfunction ensure(): AutoEvals {\n if (!enabled || !mod) {\n throw new Error(\n notInstalled\n ? \"LLM judge requires the optional `autoevals` package. Install it to use t.judge.*: npm i autoevals\"\n : \"LLM judge is not configured. Pass --judge-model and authenticate via --profile (or DATABRICKS_HOST/DATABRICKS_TOKEN) to use t.judge.*\",\n );\n }\n return mod;\n}\n\n/** Factuality of `output` vs an `expected` reference. */\nexport async function judgeFactuality(args: {\n input: string;\n output: string;\n expected: string;\n}): Promise<JudgeScore> {\n return toJudgeScore(await ensure().Factuality(args));\n}\n\n/** Whether `output` answers the question in `input`, optionally constrained by `criteria`. */\nexport async function judgeClosedQA(args: {\n input: string;\n output: string;\n criteria: string;\n}): Promise<JudgeScore> {\n return toJudgeScore(await ensure().ClosedQA(args));\n}\n\n/**\n * A custom LLM judge defined by a prompt template + choice→score map — the\n * TypeScript analog of MLflow's custom `@scorer`.\n */\nexport async function judgeCustom(\n spec: {\n name: string;\n promptTemplate: string;\n choiceScores: Record<string, number>;\n },\n args: { input: string; output: string },\n): Promise<JudgeScore> {\n const scorer = ensure().LLMClassifierFromTemplate(spec);\n return toJudgeScore(await scorer(args));\n}\n"],"mappings":";AAeA,IAAI;AACJ,IAAI,UAAU;AAGd,IAAI,eAAe;AAGnB,IAAI,aAAa;AACjB,IAAI;AACJ,IAAI;;;;;;AAsBJ,eAAsB,eAAe,QAAoC;AACvE,KAAI;AACF,QAAM,MAAM,OAAO;AACnB,gBAAc,QAAQ,IAAI;AAC1B,eAAa,QAAQ,IAAI;AACzB,UAAQ,IAAI,kBAAkB,OAAO,OAAO,qBAAqB;AACjE,UAAQ,IAAI,iBAAiB,OAAO;AACpC,eAAa;AACb,MAAI,KAAK,EAAE,cAAc,OAAO,OAAO,CAAC;AACxC,YAAU;UACH,KAAK;AACZ,YAAU;AACV,iBAAe,iBAAiB,IAAI;;;AAIxC,SAAgB,oBAA6B;AAC3C,QAAO;;;;;;;;;AAUT,SAAgB,gBAAsB;AACpC,KAAI,CAAC,WAAY;AACjB,YAAW,mBAAmB,YAAY;AAC1C,YAAW,kBAAkB,WAAW;AACxC,cAAa;AACb,WAAU;;AAGZ,SAAS,WAAW,KAAa,MAAgC;AAC/D,KAAI,SAAS,OAAW,QAAO,QAAQ,IAAI;KACtC,SAAQ,IAAI,OAAO;;;AAI1B,SAAS,iBAAiB,KAAuB;AAC/C,QACE,CAAC,CAAC,OACF,OAAO,QAAQ,YACf,UAAU,QACT,IAAI,SAAS,0BAA0B,IAAI,SAAS;;;AAKzD,SAAgB,aAAa,GAGd;CACb,MAAM,YAAY,EAAE,UAAU;AAC9B,QAAO;EACL,OAAO,OAAO,EAAE,UAAU,WAAW,EAAE,QAAQ;EAC/C,WAAW,OAAO,cAAc,WAAW,YAAY;EACxD;;AAGH,SAAS,SAAoB;AAC3B,KAAI,CAAC,WAAW,CAAC,IACf,OAAM,IAAI,MACR,eACI,sGACA,wIACL;AAEH,QAAO;;;AAIT,eAAsB,gBAAgB,MAId;AACtB,QAAO,aAAa,MAAM,QAAQ,CAAC,WAAW,KAAK,CAAC;;;AAItD,eAAsB,cAAc,MAIZ;AACtB,QAAO,aAAa,MAAM,QAAQ,CAAC,SAAS,KAAK,CAAC;;;;;;AAOpD,eAAsB,YACpB,MAKA,MACqB;AAErB,QAAO,aAAa,MADL,QAAQ,CAAC,0BAA0B,KAAK,CACtB,KAAK,CAAC"}
|
|
@@ -260,9 +260,14 @@ async function initAgentTracing() {
|
|
|
260
260
|
if (ucLocation) logger.info("MLflow agent tracing enabled (experiment %s, UC %s.%s.%s)", id, ucLocation.catalogName, ucLocation.schemaName, ucLocation.tablePrefix);
|
|
261
261
|
else logger.info("MLflow agent tracing enabled (experiment %s)", id);
|
|
262
262
|
} catch (err) {
|
|
263
|
-
logger.warn("MLflow agent tracing
|
|
263
|
+
if (isModuleNotFound(err)) logger.warn("MLflow agent tracing requires the optional `@mlflow/core` package. Install it to enable tracing: npm i @mlflow/core");
|
|
264
|
+
else logger.warn("MLflow agent tracing disabled: %O", err);
|
|
264
265
|
}
|
|
265
266
|
}
|
|
267
|
+
/** True when a dynamic `import()` failed because the package isn't installed. */
|
|
268
|
+
function isModuleNotFound(err) {
|
|
269
|
+
return !!err && typeof err === "object" && "code" in err && (err.code === "ERR_MODULE_NOT_FOUND" || err.code === "MODULE_NOT_FOUND");
|
|
270
|
+
}
|
|
266
271
|
const noopRecorder = { setOutputs() {} };
|
|
267
272
|
/**
|
|
268
273
|
* Seed the classic processor's global config once, AFTER
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mlflow.js","names":["#inner","#popTrace","#spanTypeKey","#agentSpanTypes","#maxTracked","#flushTimeoutMs","#ready","#forwarded","#agentTraceIds","#boundedFlush","trace"],"sources":["../../../src/plugins/agents/mlflow.ts"],"sourcesContent":["import type { UnityCatalogLocation } from \"@mlflow/core\";\nimport { SpanKind } from \"@opentelemetry/api\";\nimport type { SpanProcessor } from \"@opentelemetry/sdk-trace-base\";\n\nimport { createLogger } from \"../../logging/logger\";\nimport { TelemetryManager } from \"../../telemetry\";\n\nconst logger = createLogger(\"agents\");\n\ntype MlflowModule = typeof import(\"@mlflow/core\");\ntype MlflowClientInstance = InstanceType<MlflowModule[\"MlflowClient\"]>;\n\ninterface MlflowInitConfig {\n trackingUri: string;\n experimentId: string;\n host?: string;\n}\n\nlet mlflow: MlflowModule | undefined;\nlet enabled = false;\nlet initStarted = false;\nlet configured = false;\nlet initConfig: MlflowInitConfig | undefined;\n// The resolved UC trace location, or undefined for classic experiment storage.\nlet ucLocation: UnityCatalogLocation | undefined;\nlet gatedProcessor: GatedMlflowSpanProcessor | undefined;\n\n/**\n * Wraps mlflow's OTel `SpanProcessor` and scopes it to agent traces. Two jobs:\n *\n * 1. Stay inert until ready. The classic processor's `onStart` calls its own\n * `getConfig()`, which THROWS before `init()` runs — and that throw\n * propagates out of `tracer.startSpan()`, so it would break unrelated AppKit\n * spans (HTTP, analytics) created between `TelemetryManager.start()` and the\n * first agent turn. We contribute this to AppKit's single tracer provider\n * during `setup()`, but only start forwarding once `ready()` is called by\n * {@link ensureConfigured}. (The UC processor reads no global config and\n * can't throw here, but forwarding is gated uniformly either way.)\n *\n * 2. Let only agent turns become MLflow traces. mlflow roots a trace at EVERY\n * parentless span, and AppKit's single provider carries every HTTP/DB span —\n * so unscoped, every request would become an MLflow trace. mlflow stamps\n * `mlflow.spanType` on EVERY span it processes (defaulting to `UNKNOWN`), so\n * presence alone can't tell an agent turn from a plain request — we key on\n * the value (AGENT/TOOL) instead. At the root's `onEnd` we forward it (mlflow\n * exports the trace) only if some span in the trace carried an AGENT/TOOL\n * type; otherwise `popTrace` to discard the trace mlflow built in memory. The\n * real span type is set after `onStart` (the constructor stamps UNKNOWN\n * first), and children end before their root, so the flag is set by the time\n * the root decides.\n *\n * It also drops the exporters' own outbound spans at `onStart` (parentless\n * CLIENT — outgoing requests made outside any agent turn, e.g. mlflow/OTLP\n * shipping a trace). Forwarding those would loop: each upload is an HTTP call\n * that auto-instrumentation turns into a new span to trace and upload.\n *\n * ponytail: non-agent requests still build (then discard) an in-memory trace\n * tree — allocation-only, no network (export happens only when we forward the\n * root's `onEnd`). Fine at normal QPS; if a very high-QPS app makes the churn\n * matter, root MLflow at a detached agent span instead (costs the HTTP envelope\n * on the trace and splits the OTLP trace).\n */\nexport class GatedMlflowSpanProcessor implements SpanProcessor {\n #inner: SpanProcessor;\n #ready = false;\n // Spans we forwarded `onStart` for, so `onEnd` stays balanced — mlflow never\n // sees an end without a matching start.\n #forwarded = new WeakSet<object>();\n // OTel trace ids that contained at least one mlflow (AGENT/TOOL) span, so the\n // root's `onEnd` exports rather than discards. Cleared as each root ends.\n #agentTraceIds = new Set<string>();\n #popTrace: (otelTraceId: string) => void;\n #spanTypeKey: string;\n // The `mlflow.spanType` attribute values (JSON-stringified) that mark a trace\n // as an agent turn — AGENT/TOOL. Every other value (notably UNKNOWN, which\n // mlflow stamps on all non-agent spans) is treated as non-agent.\n #agentSpanTypes: ReadonlySet<string>;\n // Leak backstop for #agentTraceIds — far above real concurrency. See onEnd.\n #maxTracked: number;\n // Cap on how long forceFlush/shutdown wait for a stuck export.\n #flushTimeoutMs: number;\n\n constructor(\n inner: SpanProcessor,\n deps: {\n popTrace: (otelTraceId: string) => void;\n spanTypeKey: string;\n agentSpanTypes: ReadonlySet<string>;\n maxTracked?: number;\n flushTimeoutMs?: number;\n },\n ) {\n this.#inner = inner;\n this.#popTrace = deps.popTrace;\n this.#spanTypeKey = deps.spanTypeKey;\n this.#agentSpanTypes = deps.agentSpanTypes;\n this.#maxTracked = deps.maxTracked ?? 1024;\n this.#flushTimeoutMs = deps.flushTimeoutMs ?? 5000;\n }\n\n ready(): void {\n this.#ready = true;\n }\n\n onStart(\n span: Parameters<SpanProcessor[\"onStart\"]>[0],\n parentContext: Parameters<SpanProcessor[\"onStart\"]>[1],\n ): void {\n if (!this.#ready) return;\n // Drop the exporters' own outbound calls. A parentless (root) CLIENT span is\n // an outgoing request made outside any agent turn — e.g. mlflow or OTLP\n // shipping a trace. Forwarding those would loop: each upload is itself an\n // HTTP call that auto-instrumentation turns into a new span to trace and\n // upload.\n if (span.kind === SpanKind.CLIENT && !span.parentSpanContext?.spanId) {\n return;\n }\n this.#forwarded.add(span);\n this.#inner.onStart(span, parentContext);\n }\n\n onEnd(span: Parameters<SpanProcessor[\"onEnd\"]>[0]): void {\n if (!this.#forwarded.has(span)) return;\n const traceId = span.spanContext().traceId;\n // An AGENT/TOOL span ended in this trace — mark it for export. mlflow stamps\n // `mlflow.spanType` on every span (UNKNOWN by default), so match the value,\n // not mere presence; the real type is set after `onStart`, so `onEnd` is the\n // earliest we can read it.\n if (\n this.#agentSpanTypes.has(span.attributes[this.#spanTypeKey] as string) &&\n !this.#agentTraceIds.has(traceId)\n ) {\n // Normally an entry lives only until its root's onEnd deletes it. But a\n // root that ends BEFORE its agent child (streaming client-disconnect) or\n // never ends (crash) orphans the entry, and #agentTraceIds — unlike the\n // GC-safe #forwarded WeakSet — is keyed by string, so it can't self-clean.\n // FIFO-evict at the cap so an abandoned-trace pattern can't grow it\n // unboundedly over process uptime.\n // ponytail: evicting a still-live trace only mis-discards it, and that\n // needs >#maxTracked concurrent agent turns — far above real load.\n if (this.#agentTraceIds.size >= this.#maxTracked) {\n const oldest = this.#agentTraceIds.values().next().value;\n if (oldest !== undefined) this.#agentTraceIds.delete(oldest);\n }\n this.#agentTraceIds.add(traceId);\n }\n if (span.parentSpanContext?.spanId) {\n // Non-root: mlflow's own `onEnd` early-returns, but forward for balance.\n this.#inner.onEnd(span);\n return;\n }\n // Root span: export only agent traces; discard everything else so plain HTTP\n // requests never become MLflow traces. `delete` reports whether it was agent.\n if (this.#agentTraceIds.delete(traceId)) {\n this.#inner.onEnd(span);\n } else {\n this.#popTrace(traceId);\n }\n }\n\n forceFlush(): Promise<void> {\n return this.#boundedFlush(() => this.#inner.forceFlush());\n }\n\n shutdown(): Promise<void> {\n return this.#boundedFlush(() => this.#inner.shutdown());\n }\n\n // Bound the inner flush/shutdown wait: both exporters export fire-and-forget,\n // so a stuck export only wedges here (graceful shutdown), never a turn.\n // Resolves — not rejects — on timeout: the caller is tearing down.\n async #boundedFlush(op: () => Promise<void>): Promise<void> {\n let timer: NodeJS.Timeout | undefined;\n try {\n await Promise.race([\n op().catch((err) => {\n logger.warn(\"MLflow trace flush error: %O\", err);\n }),\n new Promise<void>((resolve) => {\n timer = setTimeout(() => {\n logger.warn(\n \"MLflow trace flush exceeded %dms; continuing (export may still be in flight)\",\n this.#flushTimeoutMs,\n );\n resolve();\n }, this.#flushTimeoutMs);\n timer.unref(); // don't keep the event loop alive on the timeout alone\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n}\n\n/**\n * Resolve the Unity Catalog trace location for the bound experiment, or\n * `undefined` for classic experiment-backed storage. Any failure falls back to\n * classic — a tracing misconfiguration must never break the agent.\n *\n * 1. Explicit env override — `MLFLOW_UC_CATALOG` + `MLFLOW_UC_SCHEMA` +\n * `MLFLOW_UC_TABLE_PREFIX`, all three required.\n * 2. Auto-detect from the linked Databricks experiment (numeric ids only, since\n * `GetExperiment` only accepts them): parse its `databricksTrace*` tags with\n * mlflow's own {@link ucLocationFromExperimentTags}, which also carries the\n * backend-populated spans/logs table names for custom-provisioned locations.\n * 3. Otherwise classic.\n *\n * `ucLocationFromExperimentTags` isn't on `@mlflow/core`'s public entrypoint, so\n * it's deep-imported like the exporter classes and covered by the tripwire test.\n */\nasync function resolveUcLocation(\n experimentId: string,\n client: MlflowClientInstance,\n): Promise<UnityCatalogLocation | undefined> {\n const catalogName = process.env.MLFLOW_UC_CATALOG?.trim();\n const schemaName = process.env.MLFLOW_UC_SCHEMA?.trim();\n const tablePrefix = process.env.MLFLOW_UC_TABLE_PREFIX?.trim();\n if (catalogName && schemaName && tablePrefix) {\n return { catalogName, schemaName, tablePrefix };\n }\n\n if (!/^\\d+$/.test(experimentId)) return undefined;\n\n try {\n const experiment = await client.getExperiment(experimentId);\n if (!experiment) return undefined;\n const { ucLocationFromExperimentTags } =\n await import(\"@mlflow/core/dist/core/destination\");\n return ucLocationFromExperimentTags(experiment.tags) ?? undefined;\n } catch (err) {\n logger.warn(\n \"MLflow UC trace-location auto-detect failed; using classic experiment storage: %O\",\n err,\n );\n return undefined;\n }\n}\n\n/**\n * Build mlflow's OTel `SpanProcessor` ourselves rather than letting `init()`\n * build and globally register its own tracer provider (its own `NodeSDK`). This\n * lets AppKit own the single global provider (OTLP + this processor), so agent\n * spans reach both MLflow and any OTLP endpoint without two SDKs racing for the\n * global slot.\n *\n * When a UC trace location is bound, builds the Unity Catalog processor +\n * exporter (V4 trace ids, spans uploaded to the experiment's UC table);\n * otherwise the classic experiment-backed processor. `createAuthProvider`,\n * `MlflowClient`, `InMemoryTraceManager` and `SpanAttributeKey` are all public\n * in `@mlflow/core`, so only the exporter/processor classes are deep-imported —\n * pinned to the exact version in package.json and guarded by a test that fails\n * loudly if a version bump renames them.\n *\n * Also resolves the hooks {@link GatedMlflowSpanProcessor} needs to scope\n * forwarding to agent traces: `popTrace` (to discard non-agent traces), the\n * `mlflow.spanType` attribute key, and the JSON-stringified AGENT/TOOL values\n * that mark a trace as an agent turn (mlflow stamps every span, defaulting to\n * UNKNOWN, so the gate must match the value, not presence).\n */\nasync function buildMlflowSpanProcessor(\n m: MlflowModule,\n client: MlflowClientInstance,\n ucLoc: UnityCatalogLocation | undefined,\n): Promise<{\n processor: SpanProcessor;\n popTrace: (otelTraceId: string) => void;\n spanTypeKey: string;\n agentSpanTypes: ReadonlySet<string>;\n}> {\n let processor: SpanProcessor;\n if (ucLoc) {\n const { DatabricksUCTableSpanExporter, DatabricksUCTableSpanProcessor } =\n await import(\"@mlflow/core/dist/exporters/uc_table\");\n processor = new DatabricksUCTableSpanProcessor(\n new DatabricksUCTableSpanExporter(client),\n ucLoc,\n );\n } else {\n const { MlflowSpanExporter, MlflowSpanProcessor } =\n await import(\"@mlflow/core/dist/exporters/mlflow\");\n processor = new MlflowSpanProcessor(new MlflowSpanExporter(client));\n }\n return {\n processor,\n popTrace: (otelTraceId) =>\n m.InMemoryTraceManager.getInstance().popTrace(otelTraceId),\n spanTypeKey: m.SpanAttributeKey.SPAN_TYPE,\n // mlflow JSON-stringifies attribute values, so the stored values are\n // `\"AGENT\"`/`\"TOOL\"` (quoted). Match that exact form.\n agentSpanTypes: new Set([\n JSON.stringify(m.SpanType.AGENT),\n JSON.stringify(m.SpanType.TOOL),\n ]),\n };\n}\n\n/** The bound MLflow experiment id, from the optional `experiment` resource. */\nfunction experimentId(): string | undefined {\n const id = process.env.MLFLOW_EXPERIMENT_ID?.trim();\n return id || undefined;\n}\n\n/**\n * Databricks host with a scheme. `@mlflow/core` uses `DATABRICKS_HOST`\n * verbatim to build request URLs and doesn't add `https://`, so a bare host\n * (`workspace.cloud.databricks.com`) makes `new URL()` throw. Pass an explicit\n * normalized host when the env var is set; when it isn't (profile-based auth),\n * return undefined and let the SDK read the host from `~/.databrickscfg`.\n */\nfunction normalizedDatabricksHost(): string | undefined {\n const raw = process.env.DATABRICKS_HOST?.trim();\n if (!raw) return undefined;\n return /^https?:\\/\\//i.test(raw) ? raw : `https://${raw}`;\n}\n\n/**\n * Initialize MLflow agent tracing once, when an experiment is bound — i.e. the\n * agents plugin's optional `experiment` resource is set (`MLFLOW_EXPERIMENT_ID`).\n * Called from the agents plugin's `setup()`, before `TelemetryManager.start()`.\n *\n * Rather than let `@mlflow/core`'s `init()` stand up and globally register its\n * own tracer provider (its `NodeSDK`, which would race AppKit's), we build the\n * span processor ourselves and contribute it to AppKit's single provider via\n * {@link TelemetryManager.registerSpanProcessor}. For the classic\n * experiment-backed processor, mlflow's global config is seeded by\n * {@link startAgentTracing} on the `\"setup:complete\"` lifecycle event (after\n * `start()`), with {@link ensureConfigured} as an idempotent lazy fallback; the\n * UC processor needs no seeded config, so that path never calls `init()`.\n *\n * The trace store is resolved here: a UC table prefix (env-configured or\n * auto-detected from the experiment's Databricks tags) vs. the classic\n * experiment. Auth is resolved by `@mlflow/core` from the app's own Databricks\n * credentials — `DATABRICKS_HOST`/`DATABRICKS_TOKEN` or a `~/.databrickscfg`\n * profile (`MLFLOW_TRACKING_URI=databricks://profile`) — so no tokens or OTLP\n * headers are wired by hand. A failure (missing creds, bad experiment) logs and\n * leaves tracing disabled rather than breaking the agent.\n *\n * Safe to call repeatedly; only the first call does work.\n */\nexport async function initAgentTracing(): Promise<void> {\n if (initStarted) return;\n initStarted = true;\n\n const id = experimentId();\n if (!id) return;\n\n try {\n mlflow = await import(\"@mlflow/core\");\n const host = normalizedDatabricksHost();\n initConfig = {\n trackingUri: process.env.MLFLOW_TRACKING_URI?.trim() || \"databricks\",\n experimentId: id,\n ...(host ? { host } : {}),\n };\n // One auth resolution + client, reused for UC auto-detect and the exporter.\n const authProvider = mlflow.createAuthProvider({\n trackingUri: initConfig.trackingUri,\n ...(host ? { host } : {}),\n });\n const client = new mlflow.MlflowClient({\n trackingUri: initConfig.trackingUri,\n authProvider,\n });\n ucLocation = await resolveUcLocation(id, client);\n const { processor, popTrace, spanTypeKey, agentSpanTypes } =\n await buildMlflowSpanProcessor(mlflow, client, ucLocation);\n gatedProcessor = new GatedMlflowSpanProcessor(processor, {\n popTrace,\n spanTypeKey,\n agentSpanTypes,\n });\n TelemetryManager.registerSpanProcessor(gatedProcessor);\n enabled = true;\n if (ucLocation) {\n logger.info(\n \"MLflow agent tracing enabled (experiment %s, UC %s.%s.%s)\",\n id,\n ucLocation.catalogName,\n ucLocation.schemaName,\n ucLocation.tablePrefix,\n );\n } else {\n logger.info(\"MLflow agent tracing enabled (experiment %s)\", id);\n }\n } catch (err) {\n logger.warn(\"MLflow agent tracing disabled: %O\", err);\n }\n}\n\n/**\n * Records a span's outputs. Callers get one from `traceAgent`/`traceTool`;\n * it's a no-op when tracing is disabled, so call sites never branch on it.\n */\nexport interface SpanRecorder {\n setOutputs(outputs: unknown): void;\n}\n\nconst noopRecorder: SpanRecorder = { setOutputs() {} };\n\n/**\n * Seed the classic processor's global config once, AFTER\n * `TelemetryManager.start()` has registered AppKit's provider — driven eagerly\n * by {@link startAgentTracing} on `\"setup:complete\"`, or lazily by {@link trace}\n * as a fallback — then enable forwarding on the gated processor. Returns whether\n * tracing is usable.\n *\n * The classic experiment-backed `MlflowSpanProcessor` reads\n * `getConfig().experimentId` in `onStart`, so it needs `init()` to seed mlflow's\n * global config. `init()` also stands up its own `NodeSDK` whose provider loses\n * the global slot to AppKit's already-registered one (non-fatal); we call it\n * only for that config side-effect. The UC processor carries its location and\n * reads no global config, so the UC path skips `init()` entirely — no second\n * `NodeSDK`, no competing global registration by `@mlflow/core`.\n */\nfunction ensureConfigured(): boolean {\n if (configured) return enabled;\n configured = true;\n // `gatedProcessor` guard is load-bearing: if buildMlflowSpanProcessor threw,\n // `mlflow` and `initConfig` are still set but there is no gate. Calling\n // `mlflow.init()` then would stand up mlflow's OWN ungated provider — and if\n // AppKit registered none (no OTLP, no processor) it wins the global slot,\n // routing every span into mlflow un-gated: the exact over-tracing + exporter\n // loop this file exists to prevent.\n if (!mlflow || !initConfig || !gatedProcessor) return false;\n try {\n if (!ucLocation) mlflow.init(initConfig);\n gatedProcessor.ready();\n return true;\n } catch (err) {\n enabled = false;\n logger.warn(\"MLflow agent tracing disabled (init failed): %O\", err);\n return false;\n }\n}\n\n/**\n * Seed mlflow's config eagerly, right after `TelemetryManager.start()` — the\n * agents plugin wires this to the `\"setup:complete\"` lifecycle event, before the\n * server serves any request. Doing it here means the request's own root span is\n * already forwarded when the first turn runs, so that turn assembles into a\n * trace instead of being dropped (mlflow roots a trace only at the top-level\n * span). Idempotent. `trace()` also seeds lazily, but that only fully rescues a\n * turn whose agent span is itself the trace root; an HTTP-wrapped first turn\n * seeded lazily loses its root span (already started — and dropped — before\n * `ready()`), so this eager path is the reliable one.\n */\nexport function startAgentTracing(): void {\n ensureConfigured();\n}\n\n/**\n * Run `fn` inside an MLflow span of `spanType` when tracing is enabled,\n * otherwise just run it (zero overhead). Spans auto-nest via the SDK's active\n * context, so a TOOL span opened inside an AGENT span's callback becomes its\n * child. The callback's resolved value is recorded as the span's outputs unless\n * it called `setOutputs` first; return `undefined` (or set outputs explicitly)\n * when the return value isn't the output you want traced.\n */\nasync function trace<T>(\n spanType: \"AGENT\" | \"TOOL\",\n name: string,\n inputs: unknown,\n fn: (span: SpanRecorder) => Promise<T>,\n): Promise<T> {\n if (!enabled || !mlflow) return fn(noopRecorder);\n const m = mlflow;\n if (!ensureConfigured()) return fn(noopRecorder);\n const type = spanType === \"AGENT\" ? m.SpanType.AGENT : m.SpanType.TOOL;\n return await m.withSpan<T>(\n async (span) => {\n if (inputs !== undefined) span.setInputs(inputs);\n let outputsSet = false;\n const result = await fn({\n setOutputs(outputs) {\n outputsSet = true;\n span.setOutputs(outputs);\n },\n });\n if (!outputsSet && result !== undefined) span.setOutputs(result);\n return result;\n },\n { name, spanType: type },\n );\n}\n\n/** Trace a turn's root AGENT span. See {@link trace}. */\nexport function traceAgent<T>(\n name: string,\n inputs: unknown,\n fn: (span: SpanRecorder) => Promise<T>,\n): Promise<T> {\n return trace(\"AGENT\", name, inputs, fn);\n}\n\n/** Trace a TOOL span, nested under the active AGENT span. See {@link trace}. */\nexport function traceTool<T>(\n name: string,\n inputs: unknown,\n fn: (span: SpanRecorder) => Promise<T>,\n): Promise<T> {\n return trace(\"TOOL\", name, inputs, fn);\n}\n\n/**\n * The MLflow trace id for the active turn, when tracing is enabled. Must be\n * read inside an agent span so eval runs can correlate the turn to its trace\n * and attach assessments. Returns undefined when tracing is off.\n *\n * Reads the context-active span rather than `getLastActiveTraceId()`: the\n * latter is only populated when a root span *ends* (on export), so mid-turn it\n * returns the previous turn's id — or, under concurrent turns, another turn's.\n */\nexport function currentTraceId(): string | undefined {\n if (!enabled || !mlflow) return undefined;\n try {\n return mlflow.getCurrentActiveSpan()?.traceId;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Link the active turn's trace to an MLflow run by id, via the `mlflow.sourceRun`\n * trace metadata. Used by eval runs so each case's trace shows under the run.\n * Must be called while a trace is active (inside an agent span). No-op when\n * tracing is disabled.\n */\nexport function linkTraceToRun(runId: string): void {\n if (!enabled || !mlflow) return;\n try {\n mlflow.updateCurrentTrace({ metadata: { \"mlflow.sourceRun\": runId } });\n } catch (err) {\n logger.warn(\"Failed to link trace to run %s: %O\", runId, err);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mlflow.js","names":["#inner","#popTrace","#spanTypeKey","#agentSpanTypes","#maxTracked","#flushTimeoutMs","#ready","#forwarded","#agentTraceIds","#boundedFlush","trace"],"sources":["../../../src/plugins/agents/mlflow.ts"],"sourcesContent":["import type { UnityCatalogLocation } from \"@mlflow/core\";\nimport { SpanKind } from \"@opentelemetry/api\";\nimport type { SpanProcessor } from \"@opentelemetry/sdk-trace-base\";\n\nimport { createLogger } from \"../../logging/logger\";\nimport { TelemetryManager } from \"../../telemetry\";\n\nconst logger = createLogger(\"agents\");\n\ntype MlflowModule = typeof import(\"@mlflow/core\");\ntype MlflowClientInstance = InstanceType<MlflowModule[\"MlflowClient\"]>;\n\ninterface MlflowInitConfig {\n trackingUri: string;\n experimentId: string;\n host?: string;\n}\n\nlet mlflow: MlflowModule | undefined;\nlet enabled = false;\nlet initStarted = false;\nlet configured = false;\nlet initConfig: MlflowInitConfig | undefined;\n// The resolved UC trace location, or undefined for classic experiment storage.\nlet ucLocation: UnityCatalogLocation | undefined;\nlet gatedProcessor: GatedMlflowSpanProcessor | undefined;\n\n/**\n * Wraps mlflow's OTel `SpanProcessor` and scopes it to agent traces. Two jobs:\n *\n * 1. Stay inert until ready. The classic processor's `onStart` calls its own\n * `getConfig()`, which THROWS before `init()` runs — and that throw\n * propagates out of `tracer.startSpan()`, so it would break unrelated AppKit\n * spans (HTTP, analytics) created between `TelemetryManager.start()` and the\n * first agent turn. We contribute this to AppKit's single tracer provider\n * during `setup()`, but only start forwarding once `ready()` is called by\n * {@link ensureConfigured}. (The UC processor reads no global config and\n * can't throw here, but forwarding is gated uniformly either way.)\n *\n * 2. Let only agent turns become MLflow traces. mlflow roots a trace at EVERY\n * parentless span, and AppKit's single provider carries every HTTP/DB span —\n * so unscoped, every request would become an MLflow trace. mlflow stamps\n * `mlflow.spanType` on EVERY span it processes (defaulting to `UNKNOWN`), so\n * presence alone can't tell an agent turn from a plain request — we key on\n * the value (AGENT/TOOL) instead. At the root's `onEnd` we forward it (mlflow\n * exports the trace) only if some span in the trace carried an AGENT/TOOL\n * type; otherwise `popTrace` to discard the trace mlflow built in memory. The\n * real span type is set after `onStart` (the constructor stamps UNKNOWN\n * first), and children end before their root, so the flag is set by the time\n * the root decides.\n *\n * It also drops the exporters' own outbound spans at `onStart` (parentless\n * CLIENT — outgoing requests made outside any agent turn, e.g. mlflow/OTLP\n * shipping a trace). Forwarding those would loop: each upload is an HTTP call\n * that auto-instrumentation turns into a new span to trace and upload.\n *\n * ponytail: non-agent requests still build (then discard) an in-memory trace\n * tree — allocation-only, no network (export happens only when we forward the\n * root's `onEnd`). Fine at normal QPS; if a very high-QPS app makes the churn\n * matter, root MLflow at a detached agent span instead (costs the HTTP envelope\n * on the trace and splits the OTLP trace).\n */\nexport class GatedMlflowSpanProcessor implements SpanProcessor {\n #inner: SpanProcessor;\n #ready = false;\n // Spans we forwarded `onStart` for, so `onEnd` stays balanced — mlflow never\n // sees an end without a matching start.\n #forwarded = new WeakSet<object>();\n // OTel trace ids that contained at least one mlflow (AGENT/TOOL) span, so the\n // root's `onEnd` exports rather than discards. Cleared as each root ends.\n #agentTraceIds = new Set<string>();\n #popTrace: (otelTraceId: string) => void;\n #spanTypeKey: string;\n // The `mlflow.spanType` attribute values (JSON-stringified) that mark a trace\n // as an agent turn — AGENT/TOOL. Every other value (notably UNKNOWN, which\n // mlflow stamps on all non-agent spans) is treated as non-agent.\n #agentSpanTypes: ReadonlySet<string>;\n // Leak backstop for #agentTraceIds — far above real concurrency. See onEnd.\n #maxTracked: number;\n // Cap on how long forceFlush/shutdown wait for a stuck export.\n #flushTimeoutMs: number;\n\n constructor(\n inner: SpanProcessor,\n deps: {\n popTrace: (otelTraceId: string) => void;\n spanTypeKey: string;\n agentSpanTypes: ReadonlySet<string>;\n maxTracked?: number;\n flushTimeoutMs?: number;\n },\n ) {\n this.#inner = inner;\n this.#popTrace = deps.popTrace;\n this.#spanTypeKey = deps.spanTypeKey;\n this.#agentSpanTypes = deps.agentSpanTypes;\n this.#maxTracked = deps.maxTracked ?? 1024;\n this.#flushTimeoutMs = deps.flushTimeoutMs ?? 5000;\n }\n\n ready(): void {\n this.#ready = true;\n }\n\n onStart(\n span: Parameters<SpanProcessor[\"onStart\"]>[0],\n parentContext: Parameters<SpanProcessor[\"onStart\"]>[1],\n ): void {\n if (!this.#ready) return;\n // Drop the exporters' own outbound calls. A parentless (root) CLIENT span is\n // an outgoing request made outside any agent turn — e.g. mlflow or OTLP\n // shipping a trace. Forwarding those would loop: each upload is itself an\n // HTTP call that auto-instrumentation turns into a new span to trace and\n // upload.\n if (span.kind === SpanKind.CLIENT && !span.parentSpanContext?.spanId) {\n return;\n }\n this.#forwarded.add(span);\n this.#inner.onStart(span, parentContext);\n }\n\n onEnd(span: Parameters<SpanProcessor[\"onEnd\"]>[0]): void {\n if (!this.#forwarded.has(span)) return;\n const traceId = span.spanContext().traceId;\n // An AGENT/TOOL span ended in this trace — mark it for export. mlflow stamps\n // `mlflow.spanType` on every span (UNKNOWN by default), so match the value,\n // not mere presence; the real type is set after `onStart`, so `onEnd` is the\n // earliest we can read it.\n if (\n this.#agentSpanTypes.has(span.attributes[this.#spanTypeKey] as string) &&\n !this.#agentTraceIds.has(traceId)\n ) {\n // Normally an entry lives only until its root's onEnd deletes it. But a\n // root that ends BEFORE its agent child (streaming client-disconnect) or\n // never ends (crash) orphans the entry, and #agentTraceIds — unlike the\n // GC-safe #forwarded WeakSet — is keyed by string, so it can't self-clean.\n // FIFO-evict at the cap so an abandoned-trace pattern can't grow it\n // unboundedly over process uptime.\n // ponytail: evicting a still-live trace only mis-discards it, and that\n // needs >#maxTracked concurrent agent turns — far above real load.\n if (this.#agentTraceIds.size >= this.#maxTracked) {\n const oldest = this.#agentTraceIds.values().next().value;\n if (oldest !== undefined) this.#agentTraceIds.delete(oldest);\n }\n this.#agentTraceIds.add(traceId);\n }\n if (span.parentSpanContext?.spanId) {\n // Non-root: mlflow's own `onEnd` early-returns, but forward for balance.\n this.#inner.onEnd(span);\n return;\n }\n // Root span: export only agent traces; discard everything else so plain HTTP\n // requests never become MLflow traces. `delete` reports whether it was agent.\n if (this.#agentTraceIds.delete(traceId)) {\n this.#inner.onEnd(span);\n } else {\n this.#popTrace(traceId);\n }\n }\n\n forceFlush(): Promise<void> {\n return this.#boundedFlush(() => this.#inner.forceFlush());\n }\n\n shutdown(): Promise<void> {\n return this.#boundedFlush(() => this.#inner.shutdown());\n }\n\n // Bound the inner flush/shutdown wait: both exporters export fire-and-forget,\n // so a stuck export only wedges here (graceful shutdown), never a turn.\n // Resolves — not rejects — on timeout: the caller is tearing down.\n async #boundedFlush(op: () => Promise<void>): Promise<void> {\n let timer: NodeJS.Timeout | undefined;\n try {\n await Promise.race([\n op().catch((err) => {\n logger.warn(\"MLflow trace flush error: %O\", err);\n }),\n new Promise<void>((resolve) => {\n timer = setTimeout(() => {\n logger.warn(\n \"MLflow trace flush exceeded %dms; continuing (export may still be in flight)\",\n this.#flushTimeoutMs,\n );\n resolve();\n }, this.#flushTimeoutMs);\n timer.unref(); // don't keep the event loop alive on the timeout alone\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n}\n\n/**\n * Resolve the Unity Catalog trace location for the bound experiment, or\n * `undefined` for classic experiment-backed storage. Any failure falls back to\n * classic — a tracing misconfiguration must never break the agent.\n *\n * 1. Explicit env override — `MLFLOW_UC_CATALOG` + `MLFLOW_UC_SCHEMA` +\n * `MLFLOW_UC_TABLE_PREFIX`, all three required.\n * 2. Auto-detect from the linked Databricks experiment (numeric ids only, since\n * `GetExperiment` only accepts them): parse its `databricksTrace*` tags with\n * mlflow's own {@link ucLocationFromExperimentTags}, which also carries the\n * backend-populated spans/logs table names for custom-provisioned locations.\n * 3. Otherwise classic.\n *\n * `ucLocationFromExperimentTags` isn't on `@mlflow/core`'s public entrypoint, so\n * it's deep-imported like the exporter classes and covered by the tripwire test.\n */\nasync function resolveUcLocation(\n experimentId: string,\n client: MlflowClientInstance,\n): Promise<UnityCatalogLocation | undefined> {\n const catalogName = process.env.MLFLOW_UC_CATALOG?.trim();\n const schemaName = process.env.MLFLOW_UC_SCHEMA?.trim();\n const tablePrefix = process.env.MLFLOW_UC_TABLE_PREFIX?.trim();\n if (catalogName && schemaName && tablePrefix) {\n return { catalogName, schemaName, tablePrefix };\n }\n\n if (!/^\\d+$/.test(experimentId)) return undefined;\n\n try {\n const experiment = await client.getExperiment(experimentId);\n if (!experiment) return undefined;\n const { ucLocationFromExperimentTags } =\n await import(\"@mlflow/core/dist/core/destination\");\n return ucLocationFromExperimentTags(experiment.tags) ?? undefined;\n } catch (err) {\n logger.warn(\n \"MLflow UC trace-location auto-detect failed; using classic experiment storage: %O\",\n err,\n );\n return undefined;\n }\n}\n\n/**\n * Build mlflow's OTel `SpanProcessor` ourselves rather than letting `init()`\n * build and globally register its own tracer provider (its own `NodeSDK`). This\n * lets AppKit own the single global provider (OTLP + this processor), so agent\n * spans reach both MLflow and any OTLP endpoint without two SDKs racing for the\n * global slot.\n *\n * When a UC trace location is bound, builds the Unity Catalog processor +\n * exporter (V4 trace ids, spans uploaded to the experiment's UC table);\n * otherwise the classic experiment-backed processor. `createAuthProvider`,\n * `MlflowClient`, `InMemoryTraceManager` and `SpanAttributeKey` are all public\n * in `@mlflow/core`, so only the exporter/processor classes are deep-imported —\n * pinned to the exact version in package.json and guarded by a test that fails\n * loudly if a version bump renames them.\n *\n * Also resolves the hooks {@link GatedMlflowSpanProcessor} needs to scope\n * forwarding to agent traces: `popTrace` (to discard non-agent traces), the\n * `mlflow.spanType` attribute key, and the JSON-stringified AGENT/TOOL values\n * that mark a trace as an agent turn (mlflow stamps every span, defaulting to\n * UNKNOWN, so the gate must match the value, not presence).\n */\nasync function buildMlflowSpanProcessor(\n m: MlflowModule,\n client: MlflowClientInstance,\n ucLoc: UnityCatalogLocation | undefined,\n): Promise<{\n processor: SpanProcessor;\n popTrace: (otelTraceId: string) => void;\n spanTypeKey: string;\n agentSpanTypes: ReadonlySet<string>;\n}> {\n let processor: SpanProcessor;\n if (ucLoc) {\n const { DatabricksUCTableSpanExporter, DatabricksUCTableSpanProcessor } =\n await import(\"@mlflow/core/dist/exporters/uc_table\");\n processor = new DatabricksUCTableSpanProcessor(\n new DatabricksUCTableSpanExporter(client),\n ucLoc,\n );\n } else {\n const { MlflowSpanExporter, MlflowSpanProcessor } =\n await import(\"@mlflow/core/dist/exporters/mlflow\");\n processor = new MlflowSpanProcessor(new MlflowSpanExporter(client));\n }\n return {\n processor,\n popTrace: (otelTraceId) =>\n m.InMemoryTraceManager.getInstance().popTrace(otelTraceId),\n spanTypeKey: m.SpanAttributeKey.SPAN_TYPE,\n // mlflow JSON-stringifies attribute values, so the stored values are\n // `\"AGENT\"`/`\"TOOL\"` (quoted). Match that exact form.\n agentSpanTypes: new Set([\n JSON.stringify(m.SpanType.AGENT),\n JSON.stringify(m.SpanType.TOOL),\n ]),\n };\n}\n\n/** The bound MLflow experiment id, from the optional `experiment` resource. */\nfunction experimentId(): string | undefined {\n const id = process.env.MLFLOW_EXPERIMENT_ID?.trim();\n return id || undefined;\n}\n\n/**\n * Databricks host with a scheme. `@mlflow/core` uses `DATABRICKS_HOST`\n * verbatim to build request URLs and doesn't add `https://`, so a bare host\n * (`workspace.cloud.databricks.com`) makes `new URL()` throw. Pass an explicit\n * normalized host when the env var is set; when it isn't (profile-based auth),\n * return undefined and let the SDK read the host from `~/.databrickscfg`.\n */\nfunction normalizedDatabricksHost(): string | undefined {\n const raw = process.env.DATABRICKS_HOST?.trim();\n if (!raw) return undefined;\n return /^https?:\\/\\//i.test(raw) ? raw : `https://${raw}`;\n}\n\n/**\n * Initialize MLflow agent tracing once, when an experiment is bound — i.e. the\n * agents plugin's optional `experiment` resource is set (`MLFLOW_EXPERIMENT_ID`).\n * Called from the agents plugin's `setup()`, before `TelemetryManager.start()`.\n *\n * Rather than let `@mlflow/core`'s `init()` stand up and globally register its\n * own tracer provider (its `NodeSDK`, which would race AppKit's), we build the\n * span processor ourselves and contribute it to AppKit's single provider via\n * {@link TelemetryManager.registerSpanProcessor}. For the classic\n * experiment-backed processor, mlflow's global config is seeded by\n * {@link startAgentTracing} on the `\"setup:complete\"` lifecycle event (after\n * `start()`), with {@link ensureConfigured} as an idempotent lazy fallback; the\n * UC processor needs no seeded config, so that path never calls `init()`.\n *\n * The trace store is resolved here: a UC table prefix (env-configured or\n * auto-detected from the experiment's Databricks tags) vs. the classic\n * experiment. Auth is resolved by `@mlflow/core` from the app's own Databricks\n * credentials — `DATABRICKS_HOST`/`DATABRICKS_TOKEN` or a `~/.databrickscfg`\n * profile (`MLFLOW_TRACKING_URI=databricks://profile`) — so no tokens or OTLP\n * headers are wired by hand. A failure (missing creds, bad experiment) logs and\n * leaves tracing disabled rather than breaking the agent.\n *\n * Safe to call repeatedly; only the first call does work.\n */\nexport async function initAgentTracing(): Promise<void> {\n if (initStarted) return;\n initStarted = true;\n\n const id = experimentId();\n if (!id) return;\n\n try {\n mlflow = await import(\"@mlflow/core\");\n const host = normalizedDatabricksHost();\n initConfig = {\n trackingUri: process.env.MLFLOW_TRACKING_URI?.trim() || \"databricks\",\n experimentId: id,\n ...(host ? { host } : {}),\n };\n // One auth resolution + client, reused for UC auto-detect and the exporter.\n const authProvider = mlflow.createAuthProvider({\n trackingUri: initConfig.trackingUri,\n ...(host ? { host } : {}),\n });\n const client = new mlflow.MlflowClient({\n trackingUri: initConfig.trackingUri,\n authProvider,\n });\n ucLocation = await resolveUcLocation(id, client);\n const { processor, popTrace, spanTypeKey, agentSpanTypes } =\n await buildMlflowSpanProcessor(mlflow, client, ucLocation);\n gatedProcessor = new GatedMlflowSpanProcessor(processor, {\n popTrace,\n spanTypeKey,\n agentSpanTypes,\n });\n TelemetryManager.registerSpanProcessor(gatedProcessor);\n enabled = true;\n if (ucLocation) {\n logger.info(\n \"MLflow agent tracing enabled (experiment %s, UC %s.%s.%s)\",\n id,\n ucLocation.catalogName,\n ucLocation.schemaName,\n ucLocation.tablePrefix,\n );\n } else {\n logger.info(\"MLflow agent tracing enabled (experiment %s)\", id);\n }\n } catch (err) {\n if (isModuleNotFound(err)) {\n logger.warn(\n \"MLflow agent tracing requires the optional `@mlflow/core` package. \" +\n \"Install it to enable tracing: npm i @mlflow/core\",\n );\n } else {\n logger.warn(\"MLflow agent tracing disabled: %O\", err);\n }\n }\n}\n\n/** True when a dynamic `import()` failed because the package isn't installed. */\nfunction isModuleNotFound(err: unknown): boolean {\n return (\n !!err &&\n typeof err === \"object\" &&\n \"code\" in err &&\n (err.code === \"ERR_MODULE_NOT_FOUND\" || err.code === \"MODULE_NOT_FOUND\")\n );\n}\n\n/**\n * Records a span's outputs. Callers get one from `traceAgent`/`traceTool`;\n * it's a no-op when tracing is disabled, so call sites never branch on it.\n */\nexport interface SpanRecorder {\n setOutputs(outputs: unknown): void;\n}\n\nconst noopRecorder: SpanRecorder = { setOutputs() {} };\n\n/**\n * Seed the classic processor's global config once, AFTER\n * `TelemetryManager.start()` has registered AppKit's provider — driven eagerly\n * by {@link startAgentTracing} on `\"setup:complete\"`, or lazily by {@link trace}\n * as a fallback — then enable forwarding on the gated processor. Returns whether\n * tracing is usable.\n *\n * The classic experiment-backed `MlflowSpanProcessor` reads\n * `getConfig().experimentId` in `onStart`, so it needs `init()` to seed mlflow's\n * global config. `init()` also stands up its own `NodeSDK` whose provider loses\n * the global slot to AppKit's already-registered one (non-fatal); we call it\n * only for that config side-effect. The UC processor carries its location and\n * reads no global config, so the UC path skips `init()` entirely — no second\n * `NodeSDK`, no competing global registration by `@mlflow/core`.\n */\nfunction ensureConfigured(): boolean {\n if (configured) return enabled;\n configured = true;\n // `gatedProcessor` guard is load-bearing: if buildMlflowSpanProcessor threw,\n // `mlflow` and `initConfig` are still set but there is no gate. Calling\n // `mlflow.init()` then would stand up mlflow's OWN ungated provider — and if\n // AppKit registered none (no OTLP, no processor) it wins the global slot,\n // routing every span into mlflow un-gated: the exact over-tracing + exporter\n // loop this file exists to prevent.\n if (!mlflow || !initConfig || !gatedProcessor) return false;\n try {\n if (!ucLocation) mlflow.init(initConfig);\n gatedProcessor.ready();\n return true;\n } catch (err) {\n enabled = false;\n logger.warn(\"MLflow agent tracing disabled (init failed): %O\", err);\n return false;\n }\n}\n\n/**\n * Seed mlflow's config eagerly, right after `TelemetryManager.start()` — the\n * agents plugin wires this to the `\"setup:complete\"` lifecycle event, before the\n * server serves any request. Doing it here means the request's own root span is\n * already forwarded when the first turn runs, so that turn assembles into a\n * trace instead of being dropped (mlflow roots a trace only at the top-level\n * span). Idempotent. `trace()` also seeds lazily, but that only fully rescues a\n * turn whose agent span is itself the trace root; an HTTP-wrapped first turn\n * seeded lazily loses its root span (already started — and dropped — before\n * `ready()`), so this eager path is the reliable one.\n */\nexport function startAgentTracing(): void {\n ensureConfigured();\n}\n\n/**\n * Run `fn` inside an MLflow span of `spanType` when tracing is enabled,\n * otherwise just run it (zero overhead). Spans auto-nest via the SDK's active\n * context, so a TOOL span opened inside an AGENT span's callback becomes its\n * child. The callback's resolved value is recorded as the span's outputs unless\n * it called `setOutputs` first; return `undefined` (or set outputs explicitly)\n * when the return value isn't the output you want traced.\n */\nasync function trace<T>(\n spanType: \"AGENT\" | \"TOOL\",\n name: string,\n inputs: unknown,\n fn: (span: SpanRecorder) => Promise<T>,\n): Promise<T> {\n if (!enabled || !mlflow) return fn(noopRecorder);\n const m = mlflow;\n if (!ensureConfigured()) return fn(noopRecorder);\n const type = spanType === \"AGENT\" ? m.SpanType.AGENT : m.SpanType.TOOL;\n return await m.withSpan<T>(\n async (span) => {\n if (inputs !== undefined) span.setInputs(inputs);\n let outputsSet = false;\n const result = await fn({\n setOutputs(outputs) {\n outputsSet = true;\n span.setOutputs(outputs);\n },\n });\n if (!outputsSet && result !== undefined) span.setOutputs(result);\n return result;\n },\n { name, spanType: type },\n );\n}\n\n/** Trace a turn's root AGENT span. See {@link trace}. */\nexport function traceAgent<T>(\n name: string,\n inputs: unknown,\n fn: (span: SpanRecorder) => Promise<T>,\n): Promise<T> {\n return trace(\"AGENT\", name, inputs, fn);\n}\n\n/** Trace a TOOL span, nested under the active AGENT span. See {@link trace}. */\nexport function traceTool<T>(\n name: string,\n inputs: unknown,\n fn: (span: SpanRecorder) => Promise<T>,\n): Promise<T> {\n return trace(\"TOOL\", name, inputs, fn);\n}\n\n/**\n * The MLflow trace id for the active turn, when tracing is enabled. Must be\n * read inside an agent span so eval runs can correlate the turn to its trace\n * and attach assessments. Returns undefined when tracing is off.\n *\n * Reads the context-active span rather than `getLastActiveTraceId()`: the\n * latter is only populated when a root span *ends* (on export), so mid-turn it\n * returns the previous turn's id — or, under concurrent turns, another turn's.\n */\nexport function currentTraceId(): string | undefined {\n if (!enabled || !mlflow) return undefined;\n try {\n return mlflow.getCurrentActiveSpan()?.traceId;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Link the active turn's trace to an MLflow run by id, via the `mlflow.sourceRun`\n * trace metadata. Used by eval runs so each case's trace shows under the run.\n * Must be called while a trace is active (inside an agent span). No-op when\n * tracing is disabled.\n */\nexport function linkTraceToRun(runId: string): void {\n if (!enabled || !mlflow) return;\n try {\n mlflow.updateCurrentTrace({ metadata: { \"mlflow.sourceRun\": runId } });\n } catch (err) {\n logger.warn(\"Failed to link trace to run %s: %O\", runId, err);\n }\n}\n"],"mappings":";;;;;;AAOA,MAAM,SAAS,aAAa,SAAS;AAWrC,IAAI;AACJ,IAAI,UAAU;AACd,IAAI,cAAc;AAClB,IAAI,aAAa;AACjB,IAAI;AAEJ,IAAI;AACJ,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCJ,IAAa,2BAAb,MAA+D;CAC7D;CACA,SAAS;CAGT,6BAAa,IAAI,SAAiB;CAGlC,iCAAiB,IAAI,KAAa;CAClC;CACA;CAIA;CAEA;CAEA;CAEA,YACE,OACA,MAOA;AACA,QAAKA,QAAS;AACd,QAAKC,WAAY,KAAK;AACtB,QAAKC,cAAe,KAAK;AACzB,QAAKC,iBAAkB,KAAK;AAC5B,QAAKC,aAAc,KAAK,cAAc;AACtC,QAAKC,iBAAkB,KAAK,kBAAkB;;CAGhD,QAAc;AACZ,QAAKC,QAAS;;CAGhB,QACE,MACA,eACM;AACN,MAAI,CAAC,MAAKA,MAAQ;AAMlB,MAAI,KAAK,SAAS,SAAS,UAAU,CAAC,KAAK,mBAAmB,OAC5D;AAEF,QAAKC,UAAW,IAAI,KAAK;AACzB,QAAKP,MAAO,QAAQ,MAAM,cAAc;;CAG1C,MAAM,MAAmD;AACvD,MAAI,CAAC,MAAKO,UAAW,IAAI,KAAK,CAAE;EAChC,MAAM,UAAU,KAAK,aAAa,CAAC;AAKnC,MACE,MAAKJ,eAAgB,IAAI,KAAK,WAAW,MAAKD,aAAwB,IACtE,CAAC,MAAKM,cAAe,IAAI,QAAQ,EACjC;AASA,OAAI,MAAKA,cAAe,QAAQ,MAAKJ,YAAa;IAChD,MAAM,SAAS,MAAKI,cAAe,QAAQ,CAAC,MAAM,CAAC;AACnD,QAAI,WAAW,OAAW,OAAKA,cAAe,OAAO,OAAO;;AAE9D,SAAKA,cAAe,IAAI,QAAQ;;AAElC,MAAI,KAAK,mBAAmB,QAAQ;AAElC,SAAKR,MAAO,MAAM,KAAK;AACvB;;AAIF,MAAI,MAAKQ,cAAe,OAAO,QAAQ,CACrC,OAAKR,MAAO,MAAM,KAAK;MAEvB,OAAKC,SAAU,QAAQ;;CAI3B,aAA4B;AAC1B,SAAO,MAAKQ,mBAAoB,MAAKT,MAAO,YAAY,CAAC;;CAG3D,WAA0B;AACxB,SAAO,MAAKS,mBAAoB,MAAKT,MAAO,UAAU,CAAC;;CAMzD,OAAMS,aAAc,IAAwC;EAC1D,IAAI;AACJ,MAAI;AACF,SAAM,QAAQ,KAAK,CACjB,IAAI,CAAC,OAAO,QAAQ;AAClB,WAAO,KAAK,gCAAgC,IAAI;KAChD,EACF,IAAI,SAAe,YAAY;AAC7B,YAAQ,iBAAiB;AACvB,YAAO,KACL,gFACA,MAAKJ,eACN;AACD,cAAS;OACR,MAAKA,eAAgB;AACxB,UAAM,OAAO;KACb,CACH,CAAC;YACM;AACR,OAAI,MAAO,cAAa,MAAM;;;;;;;;;;;;;;;;;;;;AAqBpC,eAAe,kBACb,cACA,QAC2C;CAC3C,MAAM,cAAc,QAAQ,IAAI,mBAAmB,MAAM;CACzD,MAAM,aAAa,QAAQ,IAAI,kBAAkB,MAAM;CACvD,MAAM,cAAc,QAAQ,IAAI,wBAAwB,MAAM;AAC9D,KAAI,eAAe,cAAc,YAC/B,QAAO;EAAE;EAAa;EAAY;EAAa;AAGjD,KAAI,CAAC,QAAQ,KAAK,aAAa,CAAE,QAAO;AAExC,KAAI;EACF,MAAM,aAAa,MAAM,OAAO,cAAc,aAAa;AAC3D,MAAI,CAAC,WAAY,QAAO;EACxB,MAAM,EAAE,iCACN,MAAM,OAAO;AACf,SAAO,6BAA6B,WAAW,KAAK,IAAI;UACjD,KAAK;AACZ,SAAO,KACL,qFACA,IACD;AACD;;;;;;;;;;;;;;;;;;;;;;;;AAyBJ,eAAe,yBACb,GACA,QACA,OAMC;CACD,IAAI;AACJ,KAAI,OAAO;EACT,MAAM,EAAE,+BAA+B,mCACrC,MAAM,OAAO;AACf,cAAY,IAAI,+BACd,IAAI,8BAA8B,OAAO,EACzC,MACD;QACI;EACL,MAAM,EAAE,oBAAoB,wBAC1B,MAAM,OAAO;AACf,cAAY,IAAI,oBAAoB,IAAI,mBAAmB,OAAO,CAAC;;AAErE,QAAO;EACL;EACA,WAAW,gBACT,EAAE,qBAAqB,aAAa,CAAC,SAAS,YAAY;EAC5D,aAAa,EAAE,iBAAiB;EAGhC,gBAAgB,IAAI,IAAI,CACtB,KAAK,UAAU,EAAE,SAAS,MAAM,EAChC,KAAK,UAAU,EAAE,SAAS,KAAK,CAChC,CAAC;EACH;;;AAIH,SAAS,eAAmC;AAE1C,QADW,QAAQ,IAAI,sBAAsB,MAAM,IACtC;;;;;;;;;AAUf,SAAS,2BAA+C;CACtD,MAAM,MAAM,QAAQ,IAAI,iBAAiB,MAAM;AAC/C,KAAI,CAAC,IAAK,QAAO;AACjB,QAAO,gBAAgB,KAAK,IAAI,GAAG,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtD,eAAsB,mBAAkC;AACtD,KAAI,YAAa;AACjB,eAAc;CAEd,MAAM,KAAK,cAAc;AACzB,KAAI,CAAC,GAAI;AAET,KAAI;AACF,WAAS,MAAM,OAAO;EACtB,MAAM,OAAO,0BAA0B;AACvC,eAAa;GACX,aAAa,QAAQ,IAAI,qBAAqB,MAAM,IAAI;GACxD,cAAc;GACd,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;GACzB;EAED,MAAM,eAAe,OAAO,mBAAmB;GAC7C,aAAa,WAAW;GACxB,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;GACzB,CAAC;EACF,MAAM,SAAS,IAAI,OAAO,aAAa;GACrC,aAAa,WAAW;GACxB;GACD,CAAC;AACF,eAAa,MAAM,kBAAkB,IAAI,OAAO;EAChD,MAAM,EAAE,WAAW,UAAU,aAAa,mBACxC,MAAM,yBAAyB,QAAQ,QAAQ,WAAW;AAC5D,mBAAiB,IAAI,yBAAyB,WAAW;GACvD;GACA;GACA;GACD,CAAC;AACF,mBAAiB,sBAAsB,eAAe;AACtD,YAAU;AACV,MAAI,WACF,QAAO,KACL,6DACA,IACA,WAAW,aACX,WAAW,YACX,WAAW,YACZ;MAED,QAAO,KAAK,gDAAgD,GAAG;UAE1D,KAAK;AACZ,MAAI,iBAAiB,IAAI,CACvB,QAAO,KACL,sHAED;MAED,QAAO,KAAK,qCAAqC,IAAI;;;;AAM3D,SAAS,iBAAiB,KAAuB;AAC/C,QACE,CAAC,CAAC,OACF,OAAO,QAAQ,YACf,UAAU,QACT,IAAI,SAAS,0BAA0B,IAAI,SAAS;;AAYzD,MAAM,eAA6B,EAAE,aAAa,IAAI;;;;;;;;;;;;;;;;AAiBtD,SAAS,mBAA4B;AACnC,KAAI,WAAY,QAAO;AACvB,cAAa;AAOb,KAAI,CAAC,UAAU,CAAC,cAAc,CAAC,eAAgB,QAAO;AACtD,KAAI;AACF,MAAI,CAAC,WAAY,QAAO,KAAK,WAAW;AACxC,iBAAe,OAAO;AACtB,SAAO;UACA,KAAK;AACZ,YAAU;AACV,SAAO,KAAK,mDAAmD,IAAI;AACnE,SAAO;;;;;;;;;;;;;;AAeX,SAAgB,oBAA0B;AACxC,mBAAkB;;;;;;;;;;AAWpB,eAAeK,QACb,UACA,MACA,QACA,IACY;AACZ,KAAI,CAAC,WAAW,CAAC,OAAQ,QAAO,GAAG,aAAa;CAChD,MAAM,IAAI;AACV,KAAI,CAAC,kBAAkB,CAAE,QAAO,GAAG,aAAa;CAChD,MAAM,OAAO,aAAa,UAAU,EAAE,SAAS,QAAQ,EAAE,SAAS;AAClE,QAAO,MAAM,EAAE,SACb,OAAO,SAAS;AACd,MAAI,WAAW,OAAW,MAAK,UAAU,OAAO;EAChD,IAAI,aAAa;EACjB,MAAM,SAAS,MAAM,GAAG,EACtB,WAAW,SAAS;AAClB,gBAAa;AACb,QAAK,WAAW,QAAQ;KAE3B,CAAC;AACF,MAAI,CAAC,cAAc,WAAW,OAAW,MAAK,WAAW,OAAO;AAChE,SAAO;IAET;EAAE;EAAM,UAAU;EAAM,CACzB;;;AAIH,SAAgB,WACd,MACA,QACA,IACY;AACZ,QAAOA,QAAM,SAAS,MAAM,QAAQ,GAAG;;;AAIzC,SAAgB,UACd,MACA,QACA,IACY;AACZ,QAAOA,QAAM,QAAQ,MAAM,QAAQ,GAAG;;;;;;;;;;;AAYxC,SAAgB,iBAAqC;AACnD,KAAI,CAAC,WAAW,CAAC,OAAQ,QAAO;AAChC,KAAI;AACF,SAAO,OAAO,sBAAsB,EAAE;SAChC;AACN;;;;;;;;;AAUJ,SAAgB,eAAe,OAAqB;AAClD,KAAI,CAAC,WAAW,CAAC,OAAQ;AACzB,KAAI;AACF,SAAO,mBAAmB,EAAE,UAAU,EAAE,oBAAoB,OAAO,EAAE,CAAC;UAC/D,KAAK;AACZ,SAAO,KAAK,sCAAsC,OAAO,IAAI"}
|