@databricks/appkit-ui 0.61.1 → 0.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -0
- package/dist/cli/commands/codemod/index.js.map +1 -1
- package/dist/cli/commands/codemod/on-plugins-ready.js.map +1 -1
- package/dist/cli/commands/docs.js.map +1 -1
- package/dist/cli/commands/doctor/bundle.js.map +1 -1
- package/dist/cli/commands/doctor/index.js.map +1 -1
- package/dist/cli/commands/doctor/report.js.map +1 -1
- package/dist/cli/commands/doctor/resolve-targets.js.map +1 -1
- package/dist/cli/commands/doctor/run.js.map +1 -1
- package/dist/cli/commands/generate-types.js.map +1 -1
- package/dist/cli/commands/lint.js.map +1 -1
- package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
- package/dist/cli/commands/plugin/create/create.js.map +1 -1
- package/dist/cli/commands/plugin/create/prompt-resource.js.map +1 -1
- package/dist/cli/commands/plugin/create/scaffold.js.map +1 -1
- package/dist/cli/commands/plugin/index.js.map +1 -1
- package/dist/cli/commands/plugin/list/list.js.map +1 -1
- package/dist/cli/commands/plugin/promote/promote.js.map +1 -1
- package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
- package/dist/cli/commands/plugin/validate/validate-manifest.js.map +1 -1
- package/dist/cli/commands/plugin/validate/validate.js.map +1 -1
- package/dist/cli/commands/registry/add.js.map +1 -1
- package/dist/cli/commands/registry/client.js.map +1 -1
- package/dist/cli/commands/registry/config-writer.js.map +1 -1
- package/dist/cli/commands/registry/env-reconcile.js.map +1 -1
- package/dist/cli/commands/registry/env-writer.js.map +1 -1
- package/dist/cli/commands/registry/index.js.map +1 -1
- package/dist/cli/commands/registry/info.js.map +1 -1
- package/dist/cli/commands/registry/list.js.map +1 -1
- package/dist/cli/commands/registry/requirements.js.map +1 -1
- package/dist/cli/commands/registry/server-register.js.map +1 -1
- package/dist/cli/commands/registry/workspace-picker.js.map +1 -1
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/js/arrow/arrow-client.d.ts.map +1 -1
- package/dist/js/arrow/arrow-client.js.map +1 -1
- package/dist/react/charts/area/index.d.ts.map +1 -1
- package/dist/react/charts/area/index.js.map +1 -1
- package/dist/react/charts/bar/index.d.ts.map +1 -1
- package/dist/react/charts/bar/index.js.map +1 -1
- package/dist/react/charts/base.d.ts.map +1 -1
- package/dist/react/charts/base.js +1 -1
- package/dist/react/charts/base.js.map +1 -1
- package/dist/react/charts/heatmap/index.d.ts.map +1 -1
- package/dist/react/charts/heatmap/index.js.map +1 -1
- package/dist/react/charts/line/index.d.ts.map +1 -1
- package/dist/react/charts/line/index.js.map +1 -1
- package/dist/react/charts/normalize.d.ts.map +1 -1
- package/dist/react/charts/normalize.js.map +1 -1
- package/dist/react/charts/pie/index.d.ts.map +1 -1
- package/dist/react/charts/pie/index.js.map +1 -1
- package/dist/react/charts/radar/index.d.ts.map +1 -1
- package/dist/react/charts/radar/index.js.map +1 -1
- package/dist/react/charts/scatter/index.d.ts.map +1 -1
- package/dist/react/charts/scatter/index.js.map +1 -1
- package/dist/react/charts/theme.d.ts.map +1 -1
- package/dist/react/charts/theme.js.map +1 -1
- package/dist/react/charts/wrapper.d.ts.map +1 -1
- package/dist/react/charts/wrapper.js.map +1 -1
- package/dist/react/file-browser/directory-list.d.ts.map +1 -1
- package/dist/react/file-browser/directory-list.js.map +1 -1
- package/dist/react/file-browser/file-breadcrumb.d.ts.map +1 -1
- package/dist/react/file-browser/file-breadcrumb.js.map +1 -1
- package/dist/react/file-browser/file-entry.d.ts.map +1 -1
- package/dist/react/file-browser/file-entry.js.map +1 -1
- package/dist/react/file-browser/file-preview-panel.d.ts.map +1 -1
- package/dist/react/file-browser/file-preview-panel.js.map +1 -1
- package/dist/react/file-browser/new-folder-input.d.ts.map +1 -1
- package/dist/react/file-browser/new-folder-input.js.map +1 -1
- package/dist/react/genie/genie-chat-input.d.ts.map +1 -1
- package/dist/react/genie/genie-chat-input.js.map +1 -1
- package/dist/react/genie/genie-chat-message-list.d.ts.map +1 -1
- package/dist/react/genie/genie-chat-message-list.js.map +1 -1
- package/dist/react/genie/genie-chat-message.d.ts.map +1 -1
- package/dist/react/genie/genie-chat-message.js.map +1 -1
- package/dist/react/genie/genie-query-visualization.d.ts.map +1 -1
- package/dist/react/genie/genie-query-visualization.js.map +1 -1
- package/dist/react/genie/use-genie-chat.d.ts.map +1 -1
- package/dist/react/genie/use-genie-chat.js.map +1 -1
- package/dist/react/hooks/analytics-request-store.js +144 -0
- package/dist/react/hooks/analytics-request-store.js.map +1 -0
- package/dist/react/hooks/request-store.js +92 -0
- package/dist/react/hooks/request-store.js.map +1 -0
- package/dist/react/hooks/types.d.ts.map +1 -1
- package/dist/react/hooks/use-agent-chat.d.ts.map +1 -1
- package/dist/react/hooks/use-agent-chat.js.map +1 -1
- package/dist/react/hooks/use-ai-search-query.d.ts.map +1 -1
- package/dist/react/hooks/use-ai-search-query.js.map +1 -1
- package/dist/react/hooks/use-analytics-query.d.ts +6 -0
- package/dist/react/hooks/use-analytics-query.d.ts.map +1 -1
- package/dist/react/hooks/use-analytics-query.js +34 -143
- package/dist/react/hooks/use-analytics-query.js.map +1 -1
- package/dist/react/hooks/use-analytics-warehouse-status.js.map +1 -1
- package/dist/react/hooks/use-chart-data.d.ts.map +1 -1
- package/dist/react/hooks/use-chart-data.js.map +1 -1
- package/dist/react/hooks/use-metric-view.d.ts.map +1 -1
- package/dist/react/hooks/use-metric-view.js.map +1 -1
- package/dist/react/hooks/use-plugin-config.d.ts.map +1 -1
- package/dist/react/hooks/use-plugin-config.js.map +1 -1
- package/dist/react/hooks/use-serving-invoke.d.ts.map +1 -1
- package/dist/react/hooks/use-serving-invoke.js.map +1 -1
- package/dist/react/hooks/use-serving-stream.d.ts.map +1 -1
- package/dist/react/hooks/use-serving-stream.js.map +1 -1
- package/dist/react/resource-status-indicator.d.ts.map +1 -1
- package/dist/react/resource-status-indicator.js.map +1 -1
- package/dist/react/table/data-table.d.ts.map +1 -1
- package/dist/react/table/data-table.js.map +1 -1
- package/dist/react/table/table-wrapper.js.map +1 -1
- package/dist/react/ui/button.d.ts +1 -1
- package/dist/react/ui/input-group.d.ts +1 -1
- package/dist/react/ui-variants/variants.d.ts.map +1 -1
- package/dist/react/ui-variants/variants.js.map +1 -1
- package/dist/schemas/manifest.d.ts.map +1 -1
- package/dist/schemas/manifest.js.map +1 -1
- package/dist/schemas/metric-fqn.js.map +1 -1
- package/dist/shared/src/plugin.d.ts.map +1 -1
- package/dist/styles.css +0 -3
- package/dist/workspace-client/legacy.js.map +1 -1
- package/dist/workspace-client/types.d.ts.map +1 -1
- package/docs/plugins/testing.md +245 -0
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -58,6 +58,7 @@ npx @databricks/appkit docs <query>
|
|
|
58
58
|
- [Plugin management](./docs/plugins/plugin-management.md): AppKit includes a CLI for managing plugins. All commands are available under npx @databricks/appkit plugin.
|
|
59
59
|
- [Server plugin](./docs/plugins/server.md): Provides HTTP server capabilities with development and production modes.
|
|
60
60
|
- [Plugin Stability Tiers](./docs/plugins/stability.md): AppKit plugins have a two-tier stability system that communicates API maturity and breaking-change expectations.
|
|
61
|
+
- [Testing](./docs/plugins/testing.md): AppKit ships a testing kit at @databricks/appkit/testing so you can test a plugin — including its cross-plugin tool calls and streaming responses — without a live Databricks workspace, credentials, or network access. That makes plugin tests fast and lets them run in CI, where no workspace is available.
|
|
61
62
|
|
|
62
63
|
## appkit API reference [collapsed]
|
|
63
64
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/cli/commands/codemod/index.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { onPluginsReadyCommand } from \"./on-plugins-ready\";\n\n/**\n * Parent command for codemod operations.\n * Subcommands:\n * - on-plugins-ready: Migrate from autoStart/extend/start to onPluginsReady callback\n */\nexport const codemodCommand = new Command(\"codemod\")\n .description(\"Run codemods to migrate to newer AppKit APIs\")\n .addCommand(onPluginsReadyCommand)\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit codemod on-plugins-ready --write`,\n );\n"],"mappings":";;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/cli/commands/codemod/index.ts"],"sourcesContent":["import { Command } from \"commander\";\n\nimport { onPluginsReadyCommand } from \"./on-plugins-ready\";\n\n/**\n * Parent command for codemod operations.\n * Subcommands:\n * - on-plugins-ready: Migrate from autoStart/extend/start to onPluginsReady callback\n */\nexport const codemodCommand = new Command(\"codemod\")\n .description(\"Run codemods to migrate to newer AppKit APIs\")\n .addCommand(onPluginsReadyCommand)\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit codemod on-plugins-ready --write`,\n );\n"],"mappings":";;;;;;;;;AASA,MAAa,iBAAiB,IAAI,QAAQ,UAAU,CACjD,YAAY,+CAA+C,CAC3D,WAAW,sBAAsB,CACjC,YACC,SACA;;6CAGD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"on-plugins-ready.js","names":[],"sources":["../../../../src/cli/commands/codemod/on-plugins-ready.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Lang, parse } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\n\nconst SEARCH_DIRS = [\"server\", \"src\", \".\"];\nconst CANDIDATE_NAMES = [\"server.ts\", \"index.ts\"];\nconst SKIP_DIRS = new Set([\"node_modules\", \"dist\", \"build\", \".git\"]);\n\nfunction findServerEntryFiles(rootDir: string): string[] {\n const results: string[] = [];\n\n for (const dir of SEARCH_DIRS) {\n const absDir = path.resolve(rootDir, dir);\n if (!fs.existsSync(absDir)) continue;\n\n const files =\n dir === \".\"\n ? CANDIDATE_NAMES.map((n) => path.join(absDir, n)).filter(fs.existsSync)\n : findTsFiles(absDir);\n\n for (const file of files) {\n const content = fs.readFileSync(file, \"utf-8\");\n if (\n content.includes(\"createApp\") &&\n content.includes(\"@databricks/appkit\")\n ) {\n results.push(file);\n }\n }\n }\n\n return [...new Set(results)];\n}\n\nfunction findTsFiles(dir: string, files: string[] = []): string[] {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return files;\n }\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue;\n findTsFiles(fullPath, files);\n } else if (entry.isFile() && entry.name.endsWith(\".ts\")) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\nfunction isAlreadyMigrated(content: string): boolean {\n const ast = parse(Lang.TypeScript, content);\n const root = ast.root();\n return root.findAll(\"createApp({ $$$PROPS })\").some((match) => {\n const text = match.text();\n return /\\bonPluginsReady\\s*[(:]/.test(text);\n });\n}\n\n/**\n * Find the index of the matching closing delimiter for an opening one.\n * Supports (), {}, and [].\n */\nfunction findMatchingClose(content: string, openIdx: number): number {\n const open = content[openIdx];\n const closeMap: Record<string, string> = {\n \"(\": \")\",\n \"{\": \"}\",\n \"[\": \"]\",\n };\n const close = closeMap[open];\n if (!close) return -1;\n\n let depth = 1;\n let i = openIdx + 1;\n while (i < content.length && depth > 0) {\n const ch = content[i];\n if (ch === open) depth++;\n else if (ch === close) depth--;\n\n // skip string literals\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n i = skipString(content, i);\n continue;\n }\n i++;\n }\n return depth === 0 ? i - 1 : -1;\n}\n\nfunction skipString(content: string, startIdx: number): number {\n const quote = content[startIdx];\n let i = startIdx + 1;\n while (i < content.length) {\n if (content[i] === \"\\\\\") {\n i += 2;\n continue;\n }\n if (content[i] === quote) return i + 1;\n i++;\n }\n return i;\n}\n\nfunction stripAutoStartFromServerCalls(content: string): string {\n return content.replace(\n /server\\(\\{([^}]*)\\}\\)/g,\n (_fullMatch, propsStr: string) => {\n const cleaned = propsStr\n .replace(/autoStart\\s*:\\s*(true|false)\\s*,?\\s*/g, \"\")\n .replace(/,\\s*$/, \"\")\n .trim();\n if (!cleaned) return \"server()\";\n return `server({ ${cleaned} })`;\n },\n );\n}\n\ninterface MigrationResult {\n migrated: boolean;\n content: string;\n warnings: string[];\n}\n\nfunction migratePatternA(content: string): MigrationResult {\n const warnings: string[] = [];\n\n // Find createApp(...).then(\n const createAppIdx = content.indexOf(\"createApp(\");\n if (createAppIdx === -1) return { migrated: false, content, warnings };\n\n // Find the opening paren of createApp(\n const configOpenParen = content.indexOf(\"(\", createAppIdx);\n const configCloseParen = findMatchingClose(content, configOpenParen);\n if (configCloseParen === -1) return { migrated: false, content, warnings };\n\n // Check for .then( after the closing paren\n const afterCreateApp = content.slice(configCloseParen + 1);\n const thenMatch = afterCreateApp.match(/^\\s*\\.then\\s*\\(/);\n if (!thenMatch) return { migrated: false, content, warnings };\n\n const thenStart = configCloseParen + 1 + afterCreateApp.indexOf(\".then\");\n const thenOpenParen = content.indexOf(\"(\", thenStart + 4);\n const thenCloseParen = findMatchingClose(content, thenOpenParen);\n if (thenCloseParen === -1) return { migrated: false, content, warnings };\n\n // Extract the callback inside .then(...)\n const thenRaw = content.slice(thenOpenParen + 1, thenCloseParen);\n const thenInner = thenRaw.trim();\n\n // Parse callback: (param) => { body } or async (param) => { body }\n const callbackMatch = thenInner.match(\n /^(?:async\\s+)?\\(\\s*(\\w+)\\s*\\)\\s*=>\\s*\\{/,\n );\n if (!callbackMatch) return { migrated: false, content, warnings };\n\n const paramName = callbackMatch[1];\n const bodyOpenBrace = thenOpenParen + 1 + thenRaw.indexOf(\"{\");\n const bodyCloseBrace = findMatchingClose(content, bodyOpenBrace);\n if (bodyCloseBrace === -1) return { migrated: false, content, warnings };\n\n let callbackBody = content.slice(bodyOpenBrace + 1, bodyCloseBrace).trim();\n\n // Remove entire statements that are just .start() calls (e.g. `await appkit.server.start();`)\n callbackBody = callbackBody\n .replace(/^\\s*(?:await\\s+)?\\w+\\.server\\s*\\.\\s*start\\(\\s*\\)\\s*;?\\s*$/gm, \"\")\n .replace(/\\n\\s*\\.start\\(\\s*\\)\\s*;?/g, \";\")\n .replace(/\\.start\\(\\s*\\)/g, \"\")\n .replace(/\\n\\s*\\n\\s*\\n/g, \"\\n\\n\")\n .trim();\n\n // Clean up trailing semicolons\n if (callbackBody.endsWith(\";\")) {\n // fine\n } else if (!callbackBody.endsWith(\"}\")) {\n callbackBody += \";\";\n }\n\n // Detect if the callback was async\n const isAsync = /^async\\s/.test(thenInner.trim());\n\n // Check for .catch() after .then(...) using brace-aware parsing\n const afterThenClose = content.slice(thenCloseParen + 1);\n const catchPatternMatch = afterThenClose.match(/^\\s*(?:\\)\\s*)?\\.catch\\s*\\(/);\n\n let catchSuffix: string;\n let consumeAfterThen: number;\n\n if (catchPatternMatch) {\n const catchOpenParen = thenCloseParen + 1 + catchPatternMatch[0].length - 1;\n const catchCloseParen = findMatchingClose(content, catchOpenParen);\n if (catchCloseParen !== -1) {\n const catchArg = content\n .slice(catchOpenParen + 1, catchCloseParen)\n .trim();\n catchSuffix = `.catch(${catchArg})`;\n consumeAfterThen = catchCloseParen + 1 - (thenCloseParen + 1);\n } else {\n catchSuffix = \".catch(console.error)\";\n consumeAfterThen = 0;\n }\n } else {\n catchSuffix = \".catch(console.error)\";\n consumeAfterThen = 0;\n }\n\n // Build the onPluginsReady property\n const configStr = content.slice(configOpenParen + 1, configCloseParen);\n const lastBraceIdx = configStr.lastIndexOf(\"}\");\n if (lastBraceIdx === -1) return { migrated: false, content, warnings };\n\n const beforeLastBrace = configStr.slice(0, lastBraceIdx).trimEnd();\n const needsComma = beforeLastBrace.endsWith(\",\") ? \"\" : \",\";\n\n // Indent the body properly\n const bodyLines = callbackBody.split(\"\\n\");\n const indentedBody = bodyLines\n .map((line) => ` ${line.trimStart()}`)\n .join(\"\\n\");\n\n const asyncPrefix = isAsync ? \"async \" : \"\";\n const onPluginsReadyProp = `${needsComma}\\n ${asyncPrefix}onPluginsReady(${paramName}) {\\n${indentedBody}\\n },`;\n const newConfig = `${beforeLastBrace}${onPluginsReadyProp}\\n}`;\n\n // Build the replacement\n const endIdx = thenCloseParen + 1 + consumeAfterThen;\n // Consume trailing ) ; and whitespace\n let finalEnd = endIdx;\n const trailing = content.slice(finalEnd).match(/^\\s*\\)?\\s*;?\\s*/);\n if (trailing) finalEnd += trailing[0].length;\n\n const newContent =\n content.slice(0, createAppIdx) +\n `createApp(${newConfig})${catchSuffix};` +\n content.slice(finalEnd);\n\n return { migrated: true, content: newContent, warnings };\n}\n\nfunction migratePatternB(content: string): MigrationResult {\n const warnings: string[] = [];\n\n // Match: const/let varName = await createApp({...});\n const awaitPattern = /(?:const|let)\\s+(\\w+)\\s*=\\s*await\\s+createApp\\s*\\(/;\n\n const match = content.match(awaitPattern);\n if (!match) return { migrated: false, content, warnings };\n\n const varName = match[1];\n const matchIdx = content.indexOf(match[0]);\n\n // Find the createApp(...) closing paren\n const configOpenParen = matchIdx + match[0].length - 1;\n const configCloseParen = findMatchingClose(content, configOpenParen);\n if (configCloseParen === -1) return { migrated: false, content, warnings };\n\n // Find the semicolon after the createApp call\n const afterCall = content.slice(configCloseParen + 1);\n const semiMatch = afterCall.match(/^\\s*;/);\n const createAppEnd =\n configCloseParen + 1 + (semiMatch ? semiMatch[0].length : 0);\n\n // Find all uses of varName after the createApp call\n const afterCreateApp = content.slice(createAppEnd);\n const varUsagePattern = new RegExp(`\\\\b${varName}\\\\.(\\\\w+)`, \"g\");\n\n const usages: { plugin: string; index: number }[] = [];\n for (const usageMatch of afterCreateApp.matchAll(varUsagePattern)) {\n usages.push({ plugin: usageMatch[1], index: usageMatch.index });\n }\n\n // Check for non-server usage\n const nonServerUsage = usages.filter((u) => u.plugin !== \"server\");\n if (nonServerUsage.length > 0) {\n warnings.push(\n `Found additional usage of '${varName}' handle outside server.extend/start. Please migrate manually.`,\n );\n return { migrated: false, content, warnings };\n }\n\n // Find the extend call(s) and start call in the after-createApp region\n const extendPattern = new RegExp(\n `\\\\b${varName}\\\\.server\\\\.extend\\\\s*\\\\(`,\n \"g\",\n );\n const startPattern = new RegExp(\n `(?:await\\\\s+)?${varName}\\\\.server\\\\.start\\\\s*\\\\(\\\\s*\\\\)\\\\s*;`,\n );\n\n const extendMatches = [...afterCreateApp.matchAll(extendPattern)];\n if (extendMatches.length > 1) {\n warnings.push(\n `Found ${extendMatches.length} server.extend() calls. Please migrate manually.`,\n );\n return { migrated: false, content, warnings };\n }\n\n const extendExec = extendMatches[0] ?? null;\n const startExec = startPattern.exec(afterCreateApp);\n\n if (!startExec) return { migrated: false, content, warnings };\n\n // Extract the extend call's argument\n let extendArg = \"\";\n let extendFullStatement = \"\";\n if (extendExec) {\n const extendOpenParen =\n createAppEnd + extendExec.index + extendExec[0].length - 1;\n const extendCloseParen = findMatchingClose(content, extendOpenParen);\n if (extendCloseParen !== -1) {\n extendArg = content.slice(extendOpenParen + 1, extendCloseParen).trim();\n // Find the full statement including trailing semicolon\n const stmtStart = createAppEnd + extendExec.index;\n let stmtEnd = extendCloseParen + 1;\n const afterExtend = content.slice(stmtEnd);\n const trailingSemi = afterExtend.match(/^\\s*;/);\n if (trailingSemi) stmtEnd += trailingSemi[0].length;\n extendFullStatement = content.slice(stmtStart, stmtEnd);\n }\n }\n\n const startFullStatement = startExec[0];\n\n // Build the onPluginsReady callback\n const configStr = content.slice(configOpenParen + 1, configCloseParen);\n const lastBraceIdx = configStr.lastIndexOf(\"}\");\n if (lastBraceIdx === -1) return { migrated: false, content, warnings };\n\n const beforeLastBrace = configStr.slice(0, lastBraceIdx).trimEnd();\n const needsComma = beforeLastBrace.endsWith(\",\") ? \"\" : \",\";\n\n let onPluginsReadyProp: string;\n if (extendArg) {\n onPluginsReadyProp =\n `${needsComma}\\n onPluginsReady(${varName}) {\\n` +\n ` ${varName}.server.extend(${extendArg});\\n` +\n \" },\";\n } else {\n onPluginsReadyProp = \"\";\n }\n\n const newConfig = `${beforeLastBrace}${onPluginsReadyProp}\\n}`;\n const newCreateApp = `await createApp(${newConfig});`;\n\n // Replace: remove const declaration, replace with plain await, remove extend + start\n let result = content.slice(0, matchIdx) + newCreateApp;\n let remaining = afterCreateApp;\n\n if (extendFullStatement) {\n remaining = remaining.replace(extendFullStatement, \"\");\n }\n remaining = remaining.replace(startFullStatement, \"\");\n\n // Clean up consecutive blank lines\n remaining = remaining.replace(/\\n\\s*\\n\\s*\\n/g, \"\\n\\n\");\n\n result += remaining;\n\n return { migrated: true, content: result, warnings };\n}\n\nexport function migrateFile(filePath: string): MigrationResult {\n const original = fs.readFileSync(filePath, \"utf-8\");\n\n if (isAlreadyMigrated(original)) {\n return {\n migrated: false,\n content: original,\n warnings: [\"Already migrated -- no changes needed.\"],\n };\n }\n\n const content = stripAutoStartFromServerCalls(original);\n const allWarnings: string[] = [];\n\n // Try Pattern A first\n const patternA = migratePatternA(content);\n if (patternA.migrated) {\n allWarnings.push(...patternA.warnings);\n return {\n migrated: true,\n content: patternA.content,\n warnings: allWarnings,\n };\n }\n allWarnings.push(...patternA.warnings);\n\n // Try Pattern B\n const patternB = migratePatternB(content);\n if (patternB.migrated) {\n allWarnings.push(...patternB.warnings);\n return {\n migrated: true,\n content: patternB.content,\n warnings: allWarnings,\n };\n }\n allWarnings.push(...patternB.warnings);\n\n // Check if autoStart was stripped (content changed but no pattern matched)\n if (content !== original) {\n return { migrated: true, content, warnings: allWarnings };\n }\n\n return { migrated: false, content: original, warnings: allWarnings };\n}\n\nfunction runCodemod(options: { path?: string; write?: boolean }) {\n const rootDir = process.cwd();\n const write = options.write ?? false;\n\n let files: string[];\n if (options.path) {\n const absPath = path.resolve(rootDir, options.path);\n if (!fs.existsSync(absPath)) {\n console.error(`File not found: ${absPath}`);\n process.exit(1);\n }\n files = [absPath];\n } else {\n files = findServerEntryFiles(rootDir);\n }\n\n if (files.length === 0) {\n console.log(\"No files found importing createApp from @databricks/appkit.\");\n console.log(\"Use --path to specify a file explicitly.\");\n process.exit(0);\n }\n\n let hasChanges = false;\n\n for (const file of files) {\n const relPath = path.relative(rootDir, file);\n const result = migrateFile(file);\n\n for (const warning of result.warnings) {\n console.log(` ${relPath}: ${warning}`);\n }\n\n if (!result.migrated) {\n if (result.warnings.length === 0) {\n console.log(` ${relPath}: No migration needed.`);\n }\n continue;\n }\n\n hasChanges = true;\n\n if (write) {\n fs.writeFileSync(file, result.content, \"utf-8\");\n console.log(` ${relPath}: Migrated successfully.`);\n } else {\n console.log(`\\n--- ${relPath} (dry run) ---`);\n console.log(result.content);\n console.log(\"---\");\n }\n }\n\n if (hasChanges && !write) {\n console.log(\"\\nDry run complete. Run with --write to apply changes.\");\n }\n}\n\nexport const onPluginsReadyCommand = new Command(\"on-plugins-ready\")\n .description(\n \"Migrate createApp usage from autoStart/extend/start pattern to onPluginsReady callback\",\n )\n .option(\"--path <file>\", \"Path to the server entry file to migrate\")\n .option(\"--write\", \"Apply changes (default: dry-run)\", false)\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit codemod on-plugins-ready # dry-run, auto-detect files\n $ appkit codemod on-plugins-ready --write # apply changes\n $ appkit codemod on-plugins-ready --path server.ts # migrate a specific file`,\n )\n .action(runCodemod);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"on-plugins-ready.js","names":[],"sources":["../../../../src/cli/commands/codemod/on-plugins-ready.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { Lang, parse } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\n\nconst SEARCH_DIRS = [\"server\", \"src\", \".\"];\nconst CANDIDATE_NAMES = [\"server.ts\", \"index.ts\"];\nconst SKIP_DIRS = new Set([\"node_modules\", \"dist\", \"build\", \".git\"]);\n\nfunction findServerEntryFiles(rootDir: string): string[] {\n const results: string[] = [];\n\n for (const dir of SEARCH_DIRS) {\n const absDir = path.resolve(rootDir, dir);\n if (!fs.existsSync(absDir)) continue;\n\n const files =\n dir === \".\"\n ? CANDIDATE_NAMES.map((n) => path.join(absDir, n)).filter(fs.existsSync)\n : findTsFiles(absDir);\n\n for (const file of files) {\n const content = fs.readFileSync(file, \"utf-8\");\n if (\n content.includes(\"createApp\") &&\n content.includes(\"@databricks/appkit\")\n ) {\n results.push(file);\n }\n }\n }\n\n return [...new Set(results)];\n}\n\nfunction findTsFiles(dir: string, files: string[] = []): string[] {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return files;\n }\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue;\n findTsFiles(fullPath, files);\n } else if (entry.isFile() && entry.name.endsWith(\".ts\")) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\nfunction isAlreadyMigrated(content: string): boolean {\n const ast = parse(Lang.TypeScript, content);\n const root = ast.root();\n return root.findAll(\"createApp({ $$$PROPS })\").some((match) => {\n const text = match.text();\n return /\\bonPluginsReady\\s*[(:]/.test(text);\n });\n}\n\n/**\n * Find the index of the matching closing delimiter for an opening one.\n * Supports (), {}, and [].\n */\nfunction findMatchingClose(content: string, openIdx: number): number {\n const open = content[openIdx];\n const closeMap: Record<string, string> = {\n \"(\": \")\",\n \"{\": \"}\",\n \"[\": \"]\",\n };\n const close = closeMap[open];\n if (!close) return -1;\n\n let depth = 1;\n let i = openIdx + 1;\n while (i < content.length && depth > 0) {\n const ch = content[i];\n if (ch === open) depth++;\n else if (ch === close) depth--;\n\n // skip string literals\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n i = skipString(content, i);\n continue;\n }\n i++;\n }\n return depth === 0 ? i - 1 : -1;\n}\n\nfunction skipString(content: string, startIdx: number): number {\n const quote = content[startIdx];\n let i = startIdx + 1;\n while (i < content.length) {\n if (content[i] === \"\\\\\") {\n i += 2;\n continue;\n }\n if (content[i] === quote) return i + 1;\n i++;\n }\n return i;\n}\n\nfunction stripAutoStartFromServerCalls(content: string): string {\n return content.replace(\n /server\\(\\{([^}]*)\\}\\)/g,\n (_fullMatch, propsStr: string) => {\n const cleaned = propsStr\n .replace(/autoStart\\s*:\\s*(true|false)\\s*,?\\s*/g, \"\")\n .replace(/,\\s*$/, \"\")\n .trim();\n if (!cleaned) return \"server()\";\n return `server({ ${cleaned} })`;\n },\n );\n}\n\ninterface MigrationResult {\n migrated: boolean;\n content: string;\n warnings: string[];\n}\n\nfunction migratePatternA(content: string): MigrationResult {\n const warnings: string[] = [];\n\n // Find createApp(...).then(\n const createAppIdx = content.indexOf(\"createApp(\");\n if (createAppIdx === -1) return { migrated: false, content, warnings };\n\n // Find the opening paren of createApp(\n const configOpenParen = content.indexOf(\"(\", createAppIdx);\n const configCloseParen = findMatchingClose(content, configOpenParen);\n if (configCloseParen === -1) return { migrated: false, content, warnings };\n\n // Check for .then( after the closing paren\n const afterCreateApp = content.slice(configCloseParen + 1);\n const thenMatch = afterCreateApp.match(/^\\s*\\.then\\s*\\(/);\n if (!thenMatch) return { migrated: false, content, warnings };\n\n const thenStart = configCloseParen + 1 + afterCreateApp.indexOf(\".then\");\n const thenOpenParen = content.indexOf(\"(\", thenStart + 4);\n const thenCloseParen = findMatchingClose(content, thenOpenParen);\n if (thenCloseParen === -1) return { migrated: false, content, warnings };\n\n // Extract the callback inside .then(...)\n const thenRaw = content.slice(thenOpenParen + 1, thenCloseParen);\n const thenInner = thenRaw.trim();\n\n // Parse callback: (param) => { body } or async (param) => { body }\n const callbackMatch = thenInner.match(\n /^(?:async\\s+)?\\(\\s*(\\w+)\\s*\\)\\s*=>\\s*\\{/,\n );\n if (!callbackMatch) return { migrated: false, content, warnings };\n\n const paramName = callbackMatch[1];\n const bodyOpenBrace = thenOpenParen + 1 + thenRaw.indexOf(\"{\");\n const bodyCloseBrace = findMatchingClose(content, bodyOpenBrace);\n if (bodyCloseBrace === -1) return { migrated: false, content, warnings };\n\n let callbackBody = content.slice(bodyOpenBrace + 1, bodyCloseBrace).trim();\n\n // Remove entire statements that are just .start() calls (e.g. `await appkit.server.start();`)\n callbackBody = callbackBody\n .replace(/^\\s*(?:await\\s+)?\\w+\\.server\\s*\\.\\s*start\\(\\s*\\)\\s*;?\\s*$/gm, \"\")\n .replace(/\\n\\s*\\.start\\(\\s*\\)\\s*;?/g, \";\")\n .replace(/\\.start\\(\\s*\\)/g, \"\")\n .replace(/\\n\\s*\\n\\s*\\n/g, \"\\n\\n\")\n .trim();\n\n // Clean up trailing semicolons\n if (callbackBody.endsWith(\";\")) {\n // fine\n } else if (!callbackBody.endsWith(\"}\")) {\n callbackBody += \";\";\n }\n\n // Detect if the callback was async\n const isAsync = /^async\\s/.test(thenInner.trim());\n\n // Check for .catch() after .then(...) using brace-aware parsing\n const afterThenClose = content.slice(thenCloseParen + 1);\n const catchPatternMatch = afterThenClose.match(/^\\s*(?:\\)\\s*)?\\.catch\\s*\\(/);\n\n let catchSuffix: string;\n let consumeAfterThen: number;\n\n if (catchPatternMatch) {\n const catchOpenParen = thenCloseParen + 1 + catchPatternMatch[0].length - 1;\n const catchCloseParen = findMatchingClose(content, catchOpenParen);\n if (catchCloseParen !== -1) {\n const catchArg = content\n .slice(catchOpenParen + 1, catchCloseParen)\n .trim();\n catchSuffix = `.catch(${catchArg})`;\n consumeAfterThen = catchCloseParen + 1 - (thenCloseParen + 1);\n } else {\n catchSuffix = \".catch(console.error)\";\n consumeAfterThen = 0;\n }\n } else {\n catchSuffix = \".catch(console.error)\";\n consumeAfterThen = 0;\n }\n\n // Build the onPluginsReady property\n const configStr = content.slice(configOpenParen + 1, configCloseParen);\n const lastBraceIdx = configStr.lastIndexOf(\"}\");\n if (lastBraceIdx === -1) return { migrated: false, content, warnings };\n\n const beforeLastBrace = configStr.slice(0, lastBraceIdx).trimEnd();\n const needsComma = beforeLastBrace.endsWith(\",\") ? \"\" : \",\";\n\n // Indent the body properly\n const bodyLines = callbackBody.split(\"\\n\");\n const indentedBody = bodyLines\n .map((line) => ` ${line.trimStart()}`)\n .join(\"\\n\");\n\n const asyncPrefix = isAsync ? \"async \" : \"\";\n const onPluginsReadyProp = `${needsComma}\\n ${asyncPrefix}onPluginsReady(${paramName}) {\\n${indentedBody}\\n },`;\n const newConfig = `${beforeLastBrace}${onPluginsReadyProp}\\n}`;\n\n // Build the replacement\n const endIdx = thenCloseParen + 1 + consumeAfterThen;\n // Consume trailing ) ; and whitespace\n let finalEnd = endIdx;\n const trailing = content.slice(finalEnd).match(/^\\s*\\)?\\s*;?\\s*/);\n if (trailing) finalEnd += trailing[0].length;\n\n const newContent =\n content.slice(0, createAppIdx) +\n `createApp(${newConfig})${catchSuffix};` +\n content.slice(finalEnd);\n\n return { migrated: true, content: newContent, warnings };\n}\n\nfunction migratePatternB(content: string): MigrationResult {\n const warnings: string[] = [];\n\n // Match: const/let varName = await createApp({...});\n const awaitPattern = /(?:const|let)\\s+(\\w+)\\s*=\\s*await\\s+createApp\\s*\\(/;\n\n const match = content.match(awaitPattern);\n if (!match) return { migrated: false, content, warnings };\n\n const varName = match[1];\n const matchIdx = content.indexOf(match[0]);\n\n // Find the createApp(...) closing paren\n const configOpenParen = matchIdx + match[0].length - 1;\n const configCloseParen = findMatchingClose(content, configOpenParen);\n if (configCloseParen === -1) return { migrated: false, content, warnings };\n\n // Find the semicolon after the createApp call\n const afterCall = content.slice(configCloseParen + 1);\n const semiMatch = afterCall.match(/^\\s*;/);\n const createAppEnd =\n configCloseParen + 1 + (semiMatch ? semiMatch[0].length : 0);\n\n // Find all uses of varName after the createApp call\n const afterCreateApp = content.slice(createAppEnd);\n const varUsagePattern = new RegExp(`\\\\b${varName}\\\\.(\\\\w+)`, \"g\");\n\n const usages: { plugin: string; index: number }[] = [];\n for (const usageMatch of afterCreateApp.matchAll(varUsagePattern)) {\n usages.push({ plugin: usageMatch[1], index: usageMatch.index });\n }\n\n // Check for non-server usage\n const nonServerUsage = usages.filter((u) => u.plugin !== \"server\");\n if (nonServerUsage.length > 0) {\n warnings.push(\n `Found additional usage of '${varName}' handle outside server.extend/start. Please migrate manually.`,\n );\n return { migrated: false, content, warnings };\n }\n\n // Find the extend call(s) and start call in the after-createApp region\n const extendPattern = new RegExp(\n `\\\\b${varName}\\\\.server\\\\.extend\\\\s*\\\\(`,\n \"g\",\n );\n const startPattern = new RegExp(\n `(?:await\\\\s+)?${varName}\\\\.server\\\\.start\\\\s*\\\\(\\\\s*\\\\)\\\\s*;`,\n );\n\n const extendMatches = [...afterCreateApp.matchAll(extendPattern)];\n if (extendMatches.length > 1) {\n warnings.push(\n `Found ${extendMatches.length} server.extend() calls. Please migrate manually.`,\n );\n return { migrated: false, content, warnings };\n }\n\n const extendExec = extendMatches[0] ?? null;\n const startExec = startPattern.exec(afterCreateApp);\n\n if (!startExec) return { migrated: false, content, warnings };\n\n // Extract the extend call's argument\n let extendArg = \"\";\n let extendFullStatement = \"\";\n if (extendExec) {\n const extendOpenParen =\n createAppEnd + extendExec.index + extendExec[0].length - 1;\n const extendCloseParen = findMatchingClose(content, extendOpenParen);\n if (extendCloseParen !== -1) {\n extendArg = content.slice(extendOpenParen + 1, extendCloseParen).trim();\n // Find the full statement including trailing semicolon\n const stmtStart = createAppEnd + extendExec.index;\n let stmtEnd = extendCloseParen + 1;\n const afterExtend = content.slice(stmtEnd);\n const trailingSemi = afterExtend.match(/^\\s*;/);\n if (trailingSemi) stmtEnd += trailingSemi[0].length;\n extendFullStatement = content.slice(stmtStart, stmtEnd);\n }\n }\n\n const startFullStatement = startExec[0];\n\n // Build the onPluginsReady callback\n const configStr = content.slice(configOpenParen + 1, configCloseParen);\n const lastBraceIdx = configStr.lastIndexOf(\"}\");\n if (lastBraceIdx === -1) return { migrated: false, content, warnings };\n\n const beforeLastBrace = configStr.slice(0, lastBraceIdx).trimEnd();\n const needsComma = beforeLastBrace.endsWith(\",\") ? \"\" : \",\";\n\n let onPluginsReadyProp: string;\n if (extendArg) {\n onPluginsReadyProp =\n `${needsComma}\\n onPluginsReady(${varName}) {\\n` +\n ` ${varName}.server.extend(${extendArg});\\n` +\n \" },\";\n } else {\n onPluginsReadyProp = \"\";\n }\n\n const newConfig = `${beforeLastBrace}${onPluginsReadyProp}\\n}`;\n const newCreateApp = `await createApp(${newConfig});`;\n\n // Replace: remove const declaration, replace with plain await, remove extend + start\n let result = content.slice(0, matchIdx) + newCreateApp;\n let remaining = afterCreateApp;\n\n if (extendFullStatement) {\n remaining = remaining.replace(extendFullStatement, \"\");\n }\n remaining = remaining.replace(startFullStatement, \"\");\n\n // Clean up consecutive blank lines\n remaining = remaining.replace(/\\n\\s*\\n\\s*\\n/g, \"\\n\\n\");\n\n result += remaining;\n\n return { migrated: true, content: result, warnings };\n}\n\nexport function migrateFile(filePath: string): MigrationResult {\n const original = fs.readFileSync(filePath, \"utf-8\");\n\n if (isAlreadyMigrated(original)) {\n return {\n migrated: false,\n content: original,\n warnings: [\"Already migrated -- no changes needed.\"],\n };\n }\n\n const content = stripAutoStartFromServerCalls(original);\n const allWarnings: string[] = [];\n\n // Try Pattern A first\n const patternA = migratePatternA(content);\n if (patternA.migrated) {\n allWarnings.push(...patternA.warnings);\n return {\n migrated: true,\n content: patternA.content,\n warnings: allWarnings,\n };\n }\n allWarnings.push(...patternA.warnings);\n\n // Try Pattern B\n const patternB = migratePatternB(content);\n if (patternB.migrated) {\n allWarnings.push(...patternB.warnings);\n return {\n migrated: true,\n content: patternB.content,\n warnings: allWarnings,\n };\n }\n allWarnings.push(...patternB.warnings);\n\n // Check if autoStart was stripped (content changed but no pattern matched)\n if (content !== original) {\n return { migrated: true, content, warnings: allWarnings };\n }\n\n return { migrated: false, content: original, warnings: allWarnings };\n}\n\nfunction runCodemod(options: { path?: string; write?: boolean }) {\n const rootDir = process.cwd();\n const write = options.write ?? false;\n\n let files: string[];\n if (options.path) {\n const absPath = path.resolve(rootDir, options.path);\n if (!fs.existsSync(absPath)) {\n console.error(`File not found: ${absPath}`);\n process.exit(1);\n }\n files = [absPath];\n } else {\n files = findServerEntryFiles(rootDir);\n }\n\n if (files.length === 0) {\n console.log(\"No files found importing createApp from @databricks/appkit.\");\n console.log(\"Use --path to specify a file explicitly.\");\n process.exit(0);\n }\n\n let hasChanges = false;\n\n for (const file of files) {\n const relPath = path.relative(rootDir, file);\n const result = migrateFile(file);\n\n for (const warning of result.warnings) {\n console.log(` ${relPath}: ${warning}`);\n }\n\n if (!result.migrated) {\n if (result.warnings.length === 0) {\n console.log(` ${relPath}: No migration needed.`);\n }\n continue;\n }\n\n hasChanges = true;\n\n if (write) {\n fs.writeFileSync(file, result.content, \"utf-8\");\n console.log(` ${relPath}: Migrated successfully.`);\n } else {\n console.log(`\\n--- ${relPath} (dry run) ---`);\n console.log(result.content);\n console.log(\"---\");\n }\n }\n\n if (hasChanges && !write) {\n console.log(\"\\nDry run complete. Run with --write to apply changes.\");\n }\n}\n\nexport const onPluginsReadyCommand = new Command(\"on-plugins-ready\")\n .description(\n \"Migrate createApp usage from autoStart/extend/start pattern to onPluginsReady callback\",\n )\n .option(\"--path <file>\", \"Path to the server entry file to migrate\")\n .option(\"--write\", \"Apply changes (default: dry-run)\", false)\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit codemod on-plugins-ready # dry-run, auto-detect files\n $ appkit codemod on-plugins-ready --write # apply changes\n $ appkit codemod on-plugins-ready --path server.ts # migrate a specific file`,\n )\n .action(runCodemod);\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docs.js","names":[],"sources":["../../../src/cli/commands/docs.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Command } from \"commander\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nconst COLLAPSED_MARKER = \"[collapsed]\";\n\ninterface Section {\n name: string;\n body: string;\n collapsed: boolean;\n}\n\nfunction findPackageRoot(): string {\n let dir = __dirname;\n while (dir !== path.parse(dir).root) {\n if (fs.existsSync(path.join(dir, \"package.json\"))) {\n return dir;\n }\n dir = path.dirname(dir);\n }\n throw new Error(\"Could not find package root\");\n}\n\nfunction parseSections(content: string): {\n header: string;\n sections: Section[];\n} {\n const parts = content.split(/^(## .+)$/m);\n const header = parts[0];\n const sections: Section[] = [];\n\n for (let i = 1; i < parts.length; i += 2) {\n const rawName = parts[i].replace(/^## /, \"\");\n const body = parts[i + 1] ?? \"\";\n const collapsed = rawName.includes(COLLAPSED_MARKER);\n const name = rawName.replace(COLLAPSED_MARKER, \"\").trim();\n sections.push({ name, body, collapsed });\n }\n\n return { header, sections };\n}\n\nfunction countPages(body: string): number {\n return (body.match(/^- \\[/gm) || []).length;\n}\n\nfunction findSections(sections: Section[], query: string): Section[] {\n const q = query.toLowerCase();\n return sections.filter((s) => s.name.toLowerCase().includes(q));\n}\n\nfunction isFilePath(arg: string): boolean {\n return arg.startsWith(\"./\") && arg.endsWith(\".md\");\n}\n\nfunction readLlmsTxt(packageRoot: string): string {\n const llmsPath = path.join(packageRoot, \"llms.txt\");\n if (!fs.existsSync(llmsPath)) {\n console.error(\"Error: llms.txt not found in package\");\n process.exit(1);\n }\n return fs.readFileSync(llmsPath, \"utf-8\");\n}\n\nfunction readDocFile(packageRoot: string, docPath: string): void {\n let normalizedPath = docPath;\n normalizedPath = normalizedPath.replace(/^\\.\\//, \"\");\n normalizedPath = normalizedPath.replace(/^\\//, \"\");\n normalizedPath = normalizedPath.replace(/^appkit\\//, \"\");\n\n const fullPath = path.join(packageRoot, normalizedPath);\n\n if (!fs.existsSync(fullPath)) {\n console.error(`Error: Documentation file not found: ${docPath}`);\n console.error(`Tried: ${fullPath}`);\n process.exit(1);\n }\n\n console.log(fs.readFileSync(fullPath, \"utf-8\"));\n}\n\nfunction formatCollapsedSection(section: Section): string {\n const pages = countPages(section.body);\n return [\n `## ${section.name} (${pages} pages)`,\n \"\",\n `> Use \\`appkit docs \"${section.name}\"\\` to expand, or \\`appkit docs --full\\` to expand all sections.`,\n \"\",\n ].join(\"\\n\");\n}\n\nfunction formatExpandedSection(section: Section): string {\n return `## ${section.name}${section.body}`;\n}\n\nfunction runDocs(query: string | undefined, options: { full?: boolean }) {\n const packageRoot = findPackageRoot();\n\n if (query && isFilePath(query)) {\n readDocFile(packageRoot, query);\n return;\n }\n\n const content = readLlmsTxt(packageRoot);\n\n if (options.full) {\n console.log(content.replaceAll(` ${COLLAPSED_MARKER}`, \"\"));\n return;\n }\n\n const { header, sections } = parseSections(content);\n\n if (query) {\n const matched = findSections(sections, query);\n if (matched.length === 0) {\n const available = sections.map((s) => ` - ${s.name}`).join(\"\\n\");\n console.error(\n `No section matching \"${query}\". Available sections:\\n${available}`,\n );\n process.exit(1);\n }\n console.log(matched.map(formatExpandedSection).join(\"\\n\"));\n return;\n }\n\n const output =\n header +\n sections\n .map((s) =>\n s.collapsed ? formatCollapsedSection(s) : formatExpandedSection(s),\n )\n .join(\"\\n\");\n console.log(output);\n}\n\nexport const docsCommand = new Command(\"docs\")\n .description(\"Display embedded documentation\")\n .argument(\n \"[query]\",\n \"Section name (e.g. 'plugins') or path to a doc file (e.g. './docs.md')\",\n )\n .option(\"--full\", \"Show complete index including all API reference entries\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit docs\n $ appkit docs plugins\n $ appkit docs \"appkit-ui API reference\"\n $ appkit docs ./docs/plugins/analytics.md\n $ appkit docs --full`,\n )\n .action(runDocs);\n"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"docs.js","names":[],"sources":["../../../src/cli/commands/docs.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { Command } from \"commander\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nconst COLLAPSED_MARKER = \"[collapsed]\";\n\ninterface Section {\n name: string;\n body: string;\n collapsed: boolean;\n}\n\nfunction findPackageRoot(): string {\n let dir = __dirname;\n while (dir !== path.parse(dir).root) {\n if (fs.existsSync(path.join(dir, \"package.json\"))) {\n return dir;\n }\n dir = path.dirname(dir);\n }\n throw new Error(\"Could not find package root\");\n}\n\nfunction parseSections(content: string): {\n header: string;\n sections: Section[];\n} {\n const parts = content.split(/^(## .+)$/m);\n const header = parts[0];\n const sections: Section[] = [];\n\n for (let i = 1; i < parts.length; i += 2) {\n const rawName = parts[i].replace(/^## /, \"\");\n const body = parts[i + 1] ?? \"\";\n const collapsed = rawName.includes(COLLAPSED_MARKER);\n const name = rawName.replace(COLLAPSED_MARKER, \"\").trim();\n sections.push({ name, body, collapsed });\n }\n\n return { header, sections };\n}\n\nfunction countPages(body: string): number {\n return (body.match(/^- \\[/gm) || []).length;\n}\n\nfunction findSections(sections: Section[], query: string): Section[] {\n const q = query.toLowerCase();\n return sections.filter((s) => s.name.toLowerCase().includes(q));\n}\n\nfunction isFilePath(arg: string): boolean {\n return arg.startsWith(\"./\") && arg.endsWith(\".md\");\n}\n\nfunction readLlmsTxt(packageRoot: string): string {\n const llmsPath = path.join(packageRoot, \"llms.txt\");\n if (!fs.existsSync(llmsPath)) {\n console.error(\"Error: llms.txt not found in package\");\n process.exit(1);\n }\n return fs.readFileSync(llmsPath, \"utf-8\");\n}\n\nfunction readDocFile(packageRoot: string, docPath: string): void {\n let normalizedPath = docPath;\n normalizedPath = normalizedPath.replace(/^\\.\\//, \"\");\n normalizedPath = normalizedPath.replace(/^\\//, \"\");\n normalizedPath = normalizedPath.replace(/^appkit\\//, \"\");\n\n const fullPath = path.join(packageRoot, normalizedPath);\n\n if (!fs.existsSync(fullPath)) {\n console.error(`Error: Documentation file not found: ${docPath}`);\n console.error(`Tried: ${fullPath}`);\n process.exit(1);\n }\n\n console.log(fs.readFileSync(fullPath, \"utf-8\"));\n}\n\nfunction formatCollapsedSection(section: Section): string {\n const pages = countPages(section.body);\n return [\n `## ${section.name} (${pages} pages)`,\n \"\",\n `> Use \\`appkit docs \"${section.name}\"\\` to expand, or \\`appkit docs --full\\` to expand all sections.`,\n \"\",\n ].join(\"\\n\");\n}\n\nfunction formatExpandedSection(section: Section): string {\n return `## ${section.name}${section.body}`;\n}\n\nfunction runDocs(query: string | undefined, options: { full?: boolean }) {\n const packageRoot = findPackageRoot();\n\n if (query && isFilePath(query)) {\n readDocFile(packageRoot, query);\n return;\n }\n\n const content = readLlmsTxt(packageRoot);\n\n if (options.full) {\n console.log(content.replaceAll(` ${COLLAPSED_MARKER}`, \"\"));\n return;\n }\n\n const { header, sections } = parseSections(content);\n\n if (query) {\n const matched = findSections(sections, query);\n if (matched.length === 0) {\n const available = sections.map((s) => ` - ${s.name}`).join(\"\\n\");\n console.error(\n `No section matching \"${query}\". Available sections:\\n${available}`,\n );\n process.exit(1);\n }\n console.log(matched.map(formatExpandedSection).join(\"\\n\"));\n return;\n }\n\n const output =\n header +\n sections\n .map((s) =>\n s.collapsed ? formatCollapsedSection(s) : formatExpandedSection(s),\n )\n .join(\"\\n\");\n console.log(output);\n}\n\nexport const docsCommand = new Command(\"docs\")\n .description(\"Display embedded documentation\")\n .argument(\n \"[query]\",\n \"Section name (e.g. 'plugins') or path to a doc file (e.g. './docs.md')\",\n )\n .option(\"--full\", \"Show complete index including all API reference entries\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit docs\n $ appkit docs plugins\n $ appkit docs \"appkit-ui API reference\"\n $ appkit docs ./docs/plugins/analytics.md\n $ appkit docs --full`,\n )\n .action(runDocs);\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle.js","names":[],"sources":["../../../../src/cli/commands/doctor/bundle.ts"],"sourcesContent":["/**\n * Reads `databricks.yml` + `app.yaml` for two things doctor can't get from\n * `appkit.plugins.json`:\n *\n * 1. **Provenance** — whether each binding references an existing resource\n * (`${var.*}`/literal → `external`) or one this bundle creates\n * (`${resources.<type>.<key>.*}` → `bundle-managed`).\n * 2. **Wiring** — the binding `name` joins the three files: `app.yaml`'s\n * `valueFrom: <name>` must match a `databricks.yml` binding `name`.\n *\n * SDK-free; pure YAML parsing. Absent files degrade to empty results.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport yaml from \"js-yaml\";\nimport {\n APP_YAML_FILE,\n bindingTypeOf,\n DATABRICKS_YML_FILE,\n} from \"../../deploy-config\";\nimport type { ResourceOrigin } from \"./types\";\nimport { errorMessage } from \"./utils\";\n\n/** A `${resources.<type>.<key>.<field>}` reference — a bundle-created resource. */\nconst RESOURCES_REF = /\\$\\{resources\\.([^.]+)\\.([^.}]+)\\.[^}]+\\}/;\n\n/** One app resource binding from `resources.apps.<app>.resources[]`. */\nexport interface BundleBinding {\n /** The binding name — the join key with `app.yaml` `valueFrom`. */\n name: string;\n /** The typed sub-key that names the kind (`sql_warehouse`, `genie_space`, …). */\n type?: string;\n origin: ResourceOrigin;\n /** For bundle-managed bindings: the `<type>.<key>` it references, if parseable. */\n ref?: { type: string; key: string };\n}\n\nexport interface BundleInfo {\n /** Bindings under every app, keyed by binding name. */\n bindings: Map<string, BundleBinding>;\n /** `app.yaml` env var name → binding name (from `valueFrom`). */\n envToBinding: Map<string, string>;\n /** Declared bundle resource keys, as `\"<type>.<key>\"`, for ref-integrity. */\n declaredResources: Set<string>;\n /** True when a `databricks.yml` was found and parsed. */\n present: boolean;\n}\n\ninterface AppResourceBlock {\n name?: string;\n [k: string]: unknown;\n}\ninterface BundleAppBlock {\n resources?: AppResourceBlock[];\n}\ninterface BundleDoc {\n resources?: {\n apps?: Record<string, BundleAppBlock>;\n [otherType: string]: Record<string, unknown> | undefined;\n };\n}\ninterface AppYamlDoc {\n env?: Array<{ name?: string; valueFrom?: string }>;\n}\n\n/** Classifies a binding by its typed sub-key and origin: scanning its field\n * values for a `${resources.*}` reference (bundle-managed) vs anything else\n * (external). */\nfunction classifyBinding(block: AppResourceBlock): {\n type?: string;\n origin: ResourceOrigin;\n ref?: { type: string; key: string };\n} {\n const type = bindingTypeOf(block);\n const typed = type ? block[type] : undefined;\n if (typed && typeof typed === \"object\") {\n for (const value of Object.values(typed as Record<string, unknown>)) {\n if (typeof value !== \"string\") continue;\n const m = value.match(RESOURCES_REF);\n if (m) {\n return {\n type,\n origin: \"bundle-managed\",\n ref: { type: m[1], key: m[2] },\n };\n }\n }\n }\n return { type, origin: \"external\" };\n}\n\n/**\n * Reads and parses a YAML file. Returns `null` when the file is absent (a\n * legitimate \"no bundle\" state); throws on invalid YAML, since silently\n * ignoring it would let doctor report a false all-clear.\n */\nfunction readYaml<T>(filePath: string): T | null {\n let raw: string;\n try {\n raw = fs.readFileSync(filePath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n try {\n return (yaml.load(raw) ?? {}) as T;\n } catch (err) {\n throw new Error(\n `Failed to parse ${path.basename(filePath)}: ${errorMessage(err)}`,\n );\n }\n}\n\n/**\n * Parses `databricks.yml` (+ `app.yaml`) into {@link BundleInfo}. Returns an\n * empty-but-`present:false` result when no bundle is found, so callers can treat\n * \"no bundle\" as \"everything external\" without special-casing.\n */\nexport function readBundleInfo(\n cwd: string = process.cwd(),\n bundleFile: string = DATABRICKS_YML_FILE,\n appYamlFile: string = APP_YAML_FILE,\n): BundleInfo {\n const bindings = new Map<string, BundleBinding>();\n const envToBinding = new Map<string, string>();\n const declaredResources = new Set<string>();\n\n const doc = readYaml<BundleDoc>(path.resolve(cwd, bundleFile));\n if (!doc) {\n return { bindings, envToBinding, declaredResources, present: false };\n }\n\n for (const [type, group] of Object.entries(doc.resources ?? {})) {\n if (!group || typeof group !== \"object\") continue;\n for (const key of Object.keys(group))\n declaredResources.add(`${type}.${key}`);\n }\n\n for (const app of Object.values(doc.resources?.apps ?? {})) {\n for (const block of app.resources ?? []) {\n if (!block?.name) continue;\n const { type, origin, ref } = classifyBinding(block);\n bindings.set(block.name, {\n name: block.name,\n type,\n origin,\n ref,\n });\n }\n }\n\n const appYaml = readYaml<AppYamlDoc>(path.resolve(cwd, appYamlFile));\n for (const entry of appYaml?.env ?? []) {\n if (entry?.name && entry.valueFrom) {\n envToBinding.set(entry.name, entry.valueFrom);\n }\n }\n\n return { bindings, envToBinding, declaredResources, present: true };\n}\n\n/**\n * Resolves the origin for a target given its env vars, by walking\n * env var → binding name → binding origin. Returns undefined when the bundle\n * has nothing to say (so the target stays external by default).\n */\nexport function originForEnvVars(\n envVars: string[],\n info: BundleInfo,\n): ResourceOrigin | undefined {\n if (!info.present) return undefined;\n for (const env of envVars) {\n const bindingName = info.envToBinding.get(env);\n if (!bindingName) continue;\n const binding = info.bindings.get(bindingName);\n if (binding) return binding.origin;\n }\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"bundle.js","names":[],"sources":["../../../../src/cli/commands/doctor/bundle.ts"],"sourcesContent":["/**\n * Reads `databricks.yml` + `app.yaml` for two things doctor can't get from\n * `appkit.plugins.json`:\n *\n * 1. **Provenance** — whether each binding references an existing resource\n * (`${var.*}`/literal → `external`) or one this bundle creates\n * (`${resources.<type>.<key>.*}` → `bundle-managed`).\n * 2. **Wiring** — the binding `name` joins the three files: `app.yaml`'s\n * `valueFrom: <name>` must match a `databricks.yml` binding `name`.\n *\n * SDK-free; pure YAML parsing. Absent files degrade to empty results.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport yaml from \"js-yaml\";\n\nimport {\n APP_YAML_FILE,\n bindingTypeOf,\n DATABRICKS_YML_FILE,\n} from \"../../deploy-config\";\nimport type { ResourceOrigin } from \"./types\";\nimport { errorMessage } from \"./utils\";\n\n/** A `${resources.<type>.<key>.<field>}` reference — a bundle-created resource. */\nconst RESOURCES_REF = /\\$\\{resources\\.([^.]+)\\.([^.}]+)\\.[^}]+\\}/;\n\n/** One app resource binding from `resources.apps.<app>.resources[]`. */\nexport interface BundleBinding {\n /** The binding name — the join key with `app.yaml` `valueFrom`. */\n name: string;\n /** The typed sub-key that names the kind (`sql_warehouse`, `genie_space`, …). */\n type?: string;\n origin: ResourceOrigin;\n /** For bundle-managed bindings: the `<type>.<key>` it references, if parseable. */\n ref?: { type: string; key: string };\n}\n\nexport interface BundleInfo {\n /** Bindings under every app, keyed by binding name. */\n bindings: Map<string, BundleBinding>;\n /** `app.yaml` env var name → binding name (from `valueFrom`). */\n envToBinding: Map<string, string>;\n /** Declared bundle resource keys, as `\"<type>.<key>\"`, for ref-integrity. */\n declaredResources: Set<string>;\n /** True when a `databricks.yml` was found and parsed. */\n present: boolean;\n}\n\ninterface AppResourceBlock {\n name?: string;\n [k: string]: unknown;\n}\ninterface BundleAppBlock {\n resources?: AppResourceBlock[];\n}\ninterface BundleDoc {\n resources?: {\n apps?: Record<string, BundleAppBlock>;\n [otherType: string]: Record<string, unknown> | undefined;\n };\n}\ninterface AppYamlDoc {\n env?: Array<{ name?: string; valueFrom?: string }>;\n}\n\n/** Classifies a binding by its typed sub-key and origin: scanning its field\n * values for a `${resources.*}` reference (bundle-managed) vs anything else\n * (external). */\nfunction classifyBinding(block: AppResourceBlock): {\n type?: string;\n origin: ResourceOrigin;\n ref?: { type: string; key: string };\n} {\n const type = bindingTypeOf(block);\n const typed = type ? block[type] : undefined;\n if (typed && typeof typed === \"object\") {\n for (const value of Object.values(typed as Record<string, unknown>)) {\n if (typeof value !== \"string\") continue;\n const m = value.match(RESOURCES_REF);\n if (m) {\n return {\n type,\n origin: \"bundle-managed\",\n ref: { type: m[1], key: m[2] },\n };\n }\n }\n }\n return { type, origin: \"external\" };\n}\n\n/**\n * Reads and parses a YAML file. Returns `null` when the file is absent (a\n * legitimate \"no bundle\" state); throws on invalid YAML, since silently\n * ignoring it would let doctor report a false all-clear.\n */\nfunction readYaml<T>(filePath: string): T | null {\n let raw: string;\n try {\n raw = fs.readFileSync(filePath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n try {\n return (yaml.load(raw) ?? {}) as T;\n } catch (err) {\n throw new Error(\n `Failed to parse ${path.basename(filePath)}: ${errorMessage(err)}`,\n );\n }\n}\n\n/**\n * Parses `databricks.yml` (+ `app.yaml`) into {@link BundleInfo}. Returns an\n * empty-but-`present:false` result when no bundle is found, so callers can treat\n * \"no bundle\" as \"everything external\" without special-casing.\n */\nexport function readBundleInfo(\n cwd: string = process.cwd(),\n bundleFile: string = DATABRICKS_YML_FILE,\n appYamlFile: string = APP_YAML_FILE,\n): BundleInfo {\n const bindings = new Map<string, BundleBinding>();\n const envToBinding = new Map<string, string>();\n const declaredResources = new Set<string>();\n\n const doc = readYaml<BundleDoc>(path.resolve(cwd, bundleFile));\n if (!doc) {\n return { bindings, envToBinding, declaredResources, present: false };\n }\n\n for (const [type, group] of Object.entries(doc.resources ?? {})) {\n if (!group || typeof group !== \"object\") continue;\n for (const key of Object.keys(group))\n declaredResources.add(`${type}.${key}`);\n }\n\n for (const app of Object.values(doc.resources?.apps ?? {})) {\n for (const block of app.resources ?? []) {\n if (!block?.name) continue;\n const { type, origin, ref } = classifyBinding(block);\n bindings.set(block.name, {\n name: block.name,\n type,\n origin,\n ref,\n });\n }\n }\n\n const appYaml = readYaml<AppYamlDoc>(path.resolve(cwd, appYamlFile));\n for (const entry of appYaml?.env ?? []) {\n if (entry?.name && entry.valueFrom) {\n envToBinding.set(entry.name, entry.valueFrom);\n }\n }\n\n return { bindings, envToBinding, declaredResources, present: true };\n}\n\n/**\n * Resolves the origin for a target given its env vars, by walking\n * env var → binding name → binding origin. Returns undefined when the bundle\n * has nothing to say (so the target stays external by default).\n */\nexport function originForEnvVars(\n envVars: string[],\n info: BundleInfo,\n): ResourceOrigin | undefined {\n if (!info.present) return undefined;\n for (const env of envVars) {\n const bindingName = info.envToBinding.get(env);\n if (!bindingName) continue;\n const binding = info.bindings.get(bindingName);\n if (binding) return binding.origin;\n }\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA2BA,MAAM,gBAAgB;;;;AA4CtB,SAAS,gBAAgB,OAIvB;CACA,MAAM,OAAO,cAAc,MAAM;CACjC,MAAM,QAAQ,OAAO,MAAM,QAAQ;AACnC,KAAI,SAAS,OAAO,UAAU,SAC5B,MAAK,MAAM,SAAS,OAAO,OAAO,MAAiC,EAAE;AACnE,MAAI,OAAO,UAAU,SAAU;EAC/B,MAAM,IAAI,MAAM,MAAM,cAAc;AACpC,MAAI,EACF,QAAO;GACL;GACA,QAAQ;GACR,KAAK;IAAE,MAAM,EAAE;IAAI,KAAK,EAAE;IAAI;GAC/B;;AAIP,QAAO;EAAE;EAAM,QAAQ;EAAY;;;;;;;AAQrC,SAAS,SAAY,UAA4B;CAC/C,IAAI;AACJ,KAAI;AACF,QAAM,GAAG,aAAa,UAAU,QAAQ;UACjC,KAAK;AACZ,MAAK,IAA8B,SAAS,SAAU,QAAO;AAC7D,QAAM;;AAER,KAAI;AACF,SAAQ,KAAK,KAAK,IAAI,IAAI,EAAE;UACrB,KAAK;AACZ,QAAM,IAAI,MACR,mBAAmB,KAAK,SAAS,SAAS,CAAC,IAAI,aAAa,IAAI,GACjE;;;;;;;;AASL,SAAgB,eACd,MAAc,QAAQ,KAAK,EAC3B,aAAqB,qBACrB,cAAsB,eACV;CACZ,MAAM,2BAAW,IAAI,KAA4B;CACjD,MAAM,+BAAe,IAAI,KAAqB;CAC9C,MAAM,oCAAoB,IAAI,KAAa;CAE3C,MAAM,MAAM,SAAoB,KAAK,QAAQ,KAAK,WAAW,CAAC;AAC9D,KAAI,CAAC,IACH,QAAO;EAAE;EAAU;EAAc;EAAmB,SAAS;EAAO;AAGtE,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,IAAI,aAAa,EAAE,CAAC,EAAE;AAC/D,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU;AACzC,OAAK,MAAM,OAAO,OAAO,KAAK,MAAM,CAClC,mBAAkB,IAAI,GAAG,KAAK,GAAG,MAAM;;AAG3C,MAAK,MAAM,OAAO,OAAO,OAAO,IAAI,WAAW,QAAQ,EAAE,CAAC,CACxD,MAAK,MAAM,SAAS,IAAI,aAAa,EAAE,EAAE;AACvC,MAAI,CAAC,OAAO,KAAM;EAClB,MAAM,EAAE,MAAM,QAAQ,QAAQ,gBAAgB,MAAM;AACpD,WAAS,IAAI,MAAM,MAAM;GACvB,MAAM,MAAM;GACZ;GACA;GACA;GACD,CAAC;;CAIN,MAAM,UAAU,SAAqB,KAAK,QAAQ,KAAK,YAAY,CAAC;AACpE,MAAK,MAAM,SAAS,SAAS,OAAO,EAAE,CACpC,KAAI,OAAO,QAAQ,MAAM,UACvB,cAAa,IAAI,MAAM,MAAM,MAAM,UAAU;AAIjD,QAAO;EAAE;EAAU;EAAc;EAAmB,SAAS;EAAM;;;;;;;AAQrE,SAAgB,iBACd,SACA,MAC4B;AAC5B,KAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,MAAK,MAAM,OAAO,SAAS;EACzB,MAAM,cAAc,KAAK,aAAa,IAAI,IAAI;AAC9C,MAAI,CAAC,YAAa;EAClB,MAAM,UAAU,KAAK,SAAS,IAAI,YAAY;AAC9C,MAAI,QAAS,QAAO,QAAQ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/cli/commands/doctor/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport process from \"node:process\";\nimport { Command } from \"commander\";\nimport dotenv from \"dotenv\";\nimport { printReport, printReportJson } from \"./report\";\nimport { runDoctor } from \"./run\";\nimport type { DoctorOptions } from \"./types\";\n\n/**\n * Loads an explicit env file into `process.env`, overriding the auto-loaded\n * `.env`. Throws if the file is missing.\n */\nexport function loadEnvFile(envFile: string): void {\n if (!fs.existsSync(envFile)) {\n throw new Error(`env file not found: ${envFile}`);\n }\n dotenv.config({ path: envFile, override: true });\n}\n\nasync function runDoctorCommand(options: DoctorOptions): Promise<void> {\n if (options.envFile) loadEnvFile(options.envFile);\n\n const report = await runDoctor(options);\n\n if (options.json) {\n printReportJson(report, options.detail);\n } else {\n printReport(report, options.detail);\n }\n\n process.exit(report.exitCode);\n}\n\nexport const doctorCommand = new Command(\"doctor\")\n .description(\n \"Diagnose an AppKit app's Databricks resources: authentication, config, and resource existence/reachability\",\n )\n .option(\"-p, --profile <name>\", \"Databricks CLI profile to authenticate with\")\n .option(\n \"-e, --env-file <path>\",\n \"Load this env file before checking (overrides the auto-loaded .env), e.g. .env.local\",\n )\n .option(\n \"-d, --detail\",\n \"Show full underlying error messages (raw SDK output) for failures\",\n )\n .option(\"--json\", \"Output the diagnostic report as JSON\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit doctor\n $ appkit doctor --profile my-profile\n $ appkit doctor --env-file .env.local\n $ appkit doctor --detail\n $ appkit doctor --json`,\n )\n .action((opts: DoctorOptions) =>\n runDoctorCommand(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":";;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/cli/commands/doctor/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport process from \"node:process\";\n\nimport { Command } from \"commander\";\nimport dotenv from \"dotenv\";\n\nimport { printReport, printReportJson } from \"./report\";\nimport { runDoctor } from \"./run\";\nimport type { DoctorOptions } from \"./types\";\n\n/**\n * Loads an explicit env file into `process.env`, overriding the auto-loaded\n * `.env`. Throws if the file is missing.\n */\nexport function loadEnvFile(envFile: string): void {\n if (!fs.existsSync(envFile)) {\n throw new Error(`env file not found: ${envFile}`);\n }\n dotenv.config({ path: envFile, override: true });\n}\n\nasync function runDoctorCommand(options: DoctorOptions): Promise<void> {\n if (options.envFile) loadEnvFile(options.envFile);\n\n const report = await runDoctor(options);\n\n if (options.json) {\n printReportJson(report, options.detail);\n } else {\n printReport(report, options.detail);\n }\n\n process.exit(report.exitCode);\n}\n\nexport const doctorCommand = new Command(\"doctor\")\n .description(\n \"Diagnose an AppKit app's Databricks resources: authentication, config, and resource existence/reachability\",\n )\n .option(\"-p, --profile <name>\", \"Databricks CLI profile to authenticate with\")\n .option(\n \"-e, --env-file <path>\",\n \"Load this env file before checking (overrides the auto-loaded .env), e.g. .env.local\",\n )\n .option(\n \"-d, --detail\",\n \"Show full underlying error messages (raw SDK output) for failures\",\n )\n .option(\"--json\", \"Output the diagnostic report as JSON\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit doctor\n $ appkit doctor --profile my-profile\n $ appkit doctor --env-file .env.local\n $ appkit doctor --detail\n $ appkit doctor --json`,\n )\n .action((opts: DoctorOptions) =>\n runDoctorCommand(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":";;;;;;;;;;;;AAcA,SAAgB,YAAY,SAAuB;AACjD,KAAI,CAAC,GAAG,WAAW,QAAQ,CACzB,OAAM,IAAI,MAAM,uBAAuB,UAAU;AAEnD,QAAO,OAAO;EAAE,MAAM;EAAS,UAAU;EAAM,CAAC;;AAGlD,eAAe,iBAAiB,SAAuC;AACrE,KAAI,QAAQ,QAAS,aAAY,QAAQ,QAAQ;CAEjD,MAAM,SAAS,MAAM,UAAU,QAAQ;AAEvC,KAAI,QAAQ,KACV,iBAAgB,QAAQ,QAAQ,OAAO;KAEvC,aAAY,QAAQ,QAAQ,OAAO;AAGrC,SAAQ,KAAK,OAAO,SAAS;;AAG/B,MAAa,gBAAgB,IAAI,QAAQ,SAAS,CAC/C,YACC,6GACD,CACA,OAAO,wBAAwB,8CAA8C,CAC7E,OACC,yBACA,uFACD,CACA,OACC,gBACA,oEACD,CACA,OAAO,UAAU,uCAAuC,CACxD,YACC,SACA;;;;;;0BAOD,CACA,QAAQ,SACP,iBAAiB,KAAK,CAAC,OAAO,QAAQ;AACpC,SAAQ,MAAM,IAAI;AAClB,SAAQ,KAAK,EAAE;EACf,CACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report.js","names":[],"sources":["../../../../src/cli/commands/doctor/report.ts"],"sourcesContent":["/**\n * Rendering for `appkit doctor` — turns a {@link DoctorReport} into a\n * human-readable console report or JSON.\n */\n\nimport pc from \"picocolors\";\nimport {\n AUTH_UNAVAILABLE_CODE,\n BUNDLE_MANAGED_CODE,\n type CheckStatus,\n type DoctorReport,\n type ResourceCheckResult,\n STATUS_SEVERITY,\n type WiringFinding,\n} from \"./types\";\n\n/** Column at which a row's label starts, to align detail/hint lines beneath. */\nconst SUB_INDENT = \" \";\n\n/**\n * True when a resource's only finding is an auth-skipped existence probe, so it\n * can be collapsed into one summary line.\n */\nfunction isOnlyAuthSkipped(r: ResourceCheckResult): boolean {\n if (r.status !== \"skipped\") return false;\n const hasAuthSkip = r.layers.some(\n (l) => l.layer === \"existence\" && l.code === AUTH_UNAVAILABLE_CODE,\n );\n if (!hasAuthSkip) return false;\n return r.layers.every(\n (l) =>\n l.status === \"ok\" ||\n (l.layer === \"existence\" && l.code === AUTH_UNAVAILABLE_CODE),\n );\n}\n\n/** Left-pads every line of `text` by `spaces`, for indented detail blocks. */\nfunction indent(text: string, spaces: number): string {\n const pad = \" \".repeat(spaces);\n return text\n .split(\"\\n\")\n .map((l) => `${pad}${l}`)\n .join(\"\\n\");\n}\n\n/** A status glyph, coloured by severity. */\nfunction glyph(status: CheckStatus): string {\n switch (status) {\n case \"ok\":\n return pc.green(\"✓\");\n case \"warn\":\n return pc.yellow(\"⚠\");\n case \"error\":\n return pc.red(\"✗\");\n default:\n return pc.dim(\"•\");\n }\n}\n\n/**\n * Highlights actionable tokens in a detail/hint line: cyan for a \"quoted\" id or\n * `backticked` code span (delimiters stripped), bold for a SCREAMING_SNAKE env\n * var. Nesting is safe — bold's reset (22) doesn't clear the cyan (39).\n *\n * Bold runs *first*: the cyan passes wrap their match in ANSI escapes, which\n * would put a `\\x1b[36m` between the word boundary and the env-var name and stop\n * the `\\b` anchor matching — so an env var inside a code span or quotes would\n * never get bold.\n */\nfunction highlight(text: string): string {\n return text\n .replace(/\\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\\b/g, (id) => pc.bold(id))\n .replace(/`([^`]+)`/g, (_, code) => pc.cyan(code))\n .replace(/\"([^\"]+)\"/g, (_, id) => pc.cyan(id));\n}\n\n/** A sub-line beneath a row (detail or hint), consistently indented. */\nfunction subLine(text: string): void {\n console.log(`${SUB_INDENT}${text}`);\n}\n\n/**\n * Prints the expanded body of a non-ok row: each indented `detail` line, then\n * any `Hint:` set off by a blank line. Callers own the surrounding blank lines.\n */\nfunction printRowBody(\n details: Array<string | undefined>,\n hints: Array<string | undefined>,\n): void {\n for (const d of details) if (d) subLine(highlight(d));\n const realHints = hints.filter((h): h is string => Boolean(h));\n if (realHints.length > 0) {\n console.log(\"\");\n for (const h of realHints) subLine(`${pc.dim(\"Hint:\")} ${highlight(h)}`);\n }\n}\n\n/** Shows only the non-zero categories, coloured by severity. */\nfunction summaryLine(counts: {\n ok: number;\n warn: number;\n error: number;\n skipped: number;\n}): string {\n const parts: string[] = [];\n if (counts.error)\n parts.push(pc.red(`${counts.error} error${counts.error > 1 ? \"s\" : \"\"}`));\n if (counts.warn)\n parts.push(\n pc.yellow(`${counts.warn} warning${counts.warn > 1 ? \"s\" : \"\"}`),\n );\n if (counts.ok) parts.push(pc.green(`${counts.ok} ok`));\n if (counts.skipped) parts.push(pc.dim(`${counts.skipped} skipped`));\n return parts.length > 0 ? parts.join(pc.dim(\", \")) : \"nothing to check\";\n}\n\n/** Prints the leading `Auth` check row (always first). On failure, appends the\n * workspace and profile in play, a hint, and — with `--detail` — the raw SDK\n * error. The host matters as much as the profile: `DATABRICKS_HOST` overrides a\n * profile's host, so the two can come from different places. */\nfunction printAuthRow(report: DoctorReport, detail: boolean): void {\n const { auth } = report;\n console.log(` ${glyph(auth.status)} Auth — ${auth.detail ?? auth.status}`);\n // A warn carries a config problem (e.g. a host/profile split) whose hint needs\n // the same host/profile context as an outright failure.\n if (auth.status !== \"error\" && auth.status !== \"warn\") return;\n\n if (auth.host) {\n subLine(`${pc.dim(\"host:\")} ${pc.cyan(auth.host)}`);\n }\n if (auth.profile) {\n subLine(`${pc.dim(\"profile:\")} ${pc.cyan(auth.profile)}`);\n }\n if (auth.hint) {\n console.log(\"\");\n subLine(`${pc.dim(\"Hint:\")} ${highlight(auth.hint)}`);\n }\n if (detail && auth.raw) {\n console.log(\"\");\n subLine(pc.dim(\"Details:\"));\n console.log(pc.dim(indent(auth.raw, SUB_INDENT.length + 2)));\n }\n}\n\n/**\n * Prints one resource row: a tick and the name on the happy path, actionable\n * layer details beneath otherwise. Bundle-managed resources are noted as\n * deploy-created, not probed.\n */\nfunction printResourceRow(r: ResourceCheckResult): void {\n const { target } = r;\n const bundleManaged = target.origin === \"bundle-managed\";\n console.log(\n bundleManaged\n ? ` ${pc.dim(\"⧗\")} ${target.alias} ${pc.dim(\"will be created on deploy\")}`\n : ` ${glyph(r.status)} ${target.alias}`,\n );\n // Never swallow a finding: a bundle-managed row used to drop its layers, so an\n // error counted in the summary had no visible cause. `BUNDLE_MANAGED` is the\n // expected skip and would just restate the row, so it alone stays quiet.\n const problems = r.layers.filter(\n (l) => l.status !== \"ok\" && l.code !== BUNDLE_MANAGED_CODE,\n );\n if (problems.length > 0) {\n printRowBody(\n problems.map((l) => l.detail),\n problems.map((l) => l.hint),\n );\n }\n}\n\n/** Prints a wiring finding with the same shape as a resource row:\n * `glyph label` on the first line, then the indented detail and hint. */\nfunction printWiringRow(f: WiringFinding): void {\n console.log(` ${glyph(f.status)} ${highlight(f.label)}`);\n printRowBody([f.detail], [f.hint]);\n}\n\n/** A unified printable row. `expanded` rows get a blank line on either side;\n * plain `ok` rows pack together. */\ninterface Row {\n status: CheckStatus;\n expanded: boolean;\n print: () => void;\n}\n\nexport function printReport(report: DoctorReport, detail = false): void {\n const { resources, wiring, setup, summary, auth } = report;\n\n // One flat, severity-sorted list. Auth leads and never sorts.\n const rows: Row[] = [];\n rows.push({\n status: auth.status,\n expanded: auth.status === \"error\" || auth.status === \"warn\",\n print: () => printAuthRow(report, detail),\n });\n\n // When auth failed, resources whose only finding is the auth-skip carry no\n // new information, so collapse them into a single line.\n const authSkipped: ResourceCheckResult[] = [];\n const checked: Row[] = [];\n for (const r of resources) {\n if (isOnlyAuthSkipped(r)) {\n authSkipped.push(r);\n continue;\n }\n checked.push({\n status: r.status,\n // A bundle-managed row is a one-liner unless it carries a real finding, in\n // which case it needs the spacing every expanded row gets.\n expanded:\n r.status !== \"ok\" &&\n r.layers.some(\n (l) => l.status !== \"ok\" && l.code !== BUNDLE_MANAGED_CODE,\n ),\n print: () => printResourceRow(r),\n });\n }\n if (authSkipped.length > 0) {\n checked.push({\n status: \"skipped\",\n expanded: false,\n print: () =>\n console.log(\n ` ${pc.dim(\"•\")} ${pc.dim(\n `${authSkipped.length} resource${authSkipped.length > 1 ? \"s\" : \"\"} not checked (auth failed)`,\n )}`,\n ),\n });\n }\n for (const f of wiring) {\n checked.push({\n status: f.status,\n expanded: true,\n print: () => printWiringRow(f),\n });\n }\n // Setup notices share the wiring row shape (same `label`/`detail`/`hint`).\n for (const f of setup) {\n checked.push({\n status: f.status,\n expanded: true,\n print: () => printWiringRow(f),\n });\n }\n\n // Most severe first (descending severity); stable within a status.\n checked.sort((a, b) => STATUS_SEVERITY[b.status] - STATUS_SEVERITY[a.status]);\n rows.push(...checked);\n\n // A blank line sets any expanded row apart from its neighbours.\n console.log(\"\");\n rows.forEach((row, i) => {\n const prev = rows[i - 1];\n if (i > 0 && (row.expanded || prev.expanded)) console.log(\"\");\n row.print();\n });\n\n // `summary` already counts auth + wiring (built authoritatively in run.ts),\n // so render it directly — no re-folding, and it matches what --json emits.\n console.log(\"\");\n console.log(summaryLine(summary));\n // Point at --detail when there's more to show and it wasn't requested.\n if (!detail && auth.raw) {\n console.log(pc.dim(\"\\nRun with --detail for full error output.\"));\n }\n console.log(\"\");\n}\n\n/**\n * Emits the report as JSON. The raw SDK error (`auth.raw`) can carry sensitive\n * detail, so — matching the human report's `--detail` gate — it's stripped\n * unless `detail` is set. `--json --detail` opts back into the full error.\n */\nexport function printReportJson(report: DoctorReport, detail = false): void {\n const emitted =\n detail || report.auth.raw === undefined\n ? report\n : { ...report, auth: { ...report.auth, raw: undefined } };\n console.log(JSON.stringify(emitted, null, 2));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"report.js","names":[],"sources":["../../../../src/cli/commands/doctor/report.ts"],"sourcesContent":["/**\n * Rendering for `appkit doctor` — turns a {@link DoctorReport} into a\n * human-readable console report or JSON.\n */\n\nimport pc from \"picocolors\";\n\nimport {\n AUTH_UNAVAILABLE_CODE,\n BUNDLE_MANAGED_CODE,\n type CheckStatus,\n type DoctorReport,\n type ResourceCheckResult,\n STATUS_SEVERITY,\n type WiringFinding,\n} from \"./types\";\n\n/** Column at which a row's label starts, to align detail/hint lines beneath. */\nconst SUB_INDENT = \" \";\n\n/**\n * True when a resource's only finding is an auth-skipped existence probe, so it\n * can be collapsed into one summary line.\n */\nfunction isOnlyAuthSkipped(r: ResourceCheckResult): boolean {\n if (r.status !== \"skipped\") return false;\n const hasAuthSkip = r.layers.some(\n (l) => l.layer === \"existence\" && l.code === AUTH_UNAVAILABLE_CODE,\n );\n if (!hasAuthSkip) return false;\n return r.layers.every(\n (l) =>\n l.status === \"ok\" ||\n (l.layer === \"existence\" && l.code === AUTH_UNAVAILABLE_CODE),\n );\n}\n\n/** Left-pads every line of `text` by `spaces`, for indented detail blocks. */\nfunction indent(text: string, spaces: number): string {\n const pad = \" \".repeat(spaces);\n return text\n .split(\"\\n\")\n .map((l) => `${pad}${l}`)\n .join(\"\\n\");\n}\n\n/** A status glyph, coloured by severity. */\nfunction glyph(status: CheckStatus): string {\n switch (status) {\n case \"ok\":\n return pc.green(\"✓\");\n case \"warn\":\n return pc.yellow(\"⚠\");\n case \"error\":\n return pc.red(\"✗\");\n default:\n return pc.dim(\"•\");\n }\n}\n\n/**\n * Highlights actionable tokens in a detail/hint line: cyan for a \"quoted\" id or\n * `backticked` code span (delimiters stripped), bold for a SCREAMING_SNAKE env\n * var. Nesting is safe — bold's reset (22) doesn't clear the cyan (39).\n *\n * Bold runs *first*: the cyan passes wrap their match in ANSI escapes, which\n * would put a `\\x1b[36m` between the word boundary and the env-var name and stop\n * the `\\b` anchor matching — so an env var inside a code span or quotes would\n * never get bold.\n */\nfunction highlight(text: string): string {\n return text\n .replace(/\\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\\b/g, (id) => pc.bold(id))\n .replace(/`([^`]+)`/g, (_, code) => pc.cyan(code))\n .replace(/\"([^\"]+)\"/g, (_, id) => pc.cyan(id));\n}\n\n/** A sub-line beneath a row (detail or hint), consistently indented. */\nfunction subLine(text: string): void {\n console.log(`${SUB_INDENT}${text}`);\n}\n\n/**\n * Prints the expanded body of a non-ok row: each indented `detail` line, then\n * any `Hint:` set off by a blank line. Callers own the surrounding blank lines.\n */\nfunction printRowBody(\n details: Array<string | undefined>,\n hints: Array<string | undefined>,\n): void {\n for (const d of details) if (d) subLine(highlight(d));\n const realHints = hints.filter((h): h is string => Boolean(h));\n if (realHints.length > 0) {\n console.log(\"\");\n for (const h of realHints) subLine(`${pc.dim(\"Hint:\")} ${highlight(h)}`);\n }\n}\n\n/** Shows only the non-zero categories, coloured by severity. */\nfunction summaryLine(counts: {\n ok: number;\n warn: number;\n error: number;\n skipped: number;\n}): string {\n const parts: string[] = [];\n if (counts.error)\n parts.push(pc.red(`${counts.error} error${counts.error > 1 ? \"s\" : \"\"}`));\n if (counts.warn)\n parts.push(\n pc.yellow(`${counts.warn} warning${counts.warn > 1 ? \"s\" : \"\"}`),\n );\n if (counts.ok) parts.push(pc.green(`${counts.ok} ok`));\n if (counts.skipped) parts.push(pc.dim(`${counts.skipped} skipped`));\n return parts.length > 0 ? parts.join(pc.dim(\", \")) : \"nothing to check\";\n}\n\n/** Prints the leading `Auth` check row (always first). On failure, appends the\n * workspace and profile in play, a hint, and — with `--detail` — the raw SDK\n * error. The host matters as much as the profile: `DATABRICKS_HOST` overrides a\n * profile's host, so the two can come from different places. */\nfunction printAuthRow(report: DoctorReport, detail: boolean): void {\n const { auth } = report;\n console.log(` ${glyph(auth.status)} Auth — ${auth.detail ?? auth.status}`);\n // A warn carries a config problem (e.g. a host/profile split) whose hint needs\n // the same host/profile context as an outright failure.\n if (auth.status !== \"error\" && auth.status !== \"warn\") return;\n\n if (auth.host) {\n subLine(`${pc.dim(\"host:\")} ${pc.cyan(auth.host)}`);\n }\n if (auth.profile) {\n subLine(`${pc.dim(\"profile:\")} ${pc.cyan(auth.profile)}`);\n }\n if (auth.hint) {\n console.log(\"\");\n subLine(`${pc.dim(\"Hint:\")} ${highlight(auth.hint)}`);\n }\n if (detail && auth.raw) {\n console.log(\"\");\n subLine(pc.dim(\"Details:\"));\n console.log(pc.dim(indent(auth.raw, SUB_INDENT.length + 2)));\n }\n}\n\n/**\n * Prints one resource row: a tick and the name on the happy path, actionable\n * layer details beneath otherwise. Bundle-managed resources are noted as\n * deploy-created, not probed.\n */\nfunction printResourceRow(r: ResourceCheckResult): void {\n const { target } = r;\n const bundleManaged = target.origin === \"bundle-managed\";\n console.log(\n bundleManaged\n ? ` ${pc.dim(\"⧗\")} ${target.alias} ${pc.dim(\"will be created on deploy\")}`\n : ` ${glyph(r.status)} ${target.alias}`,\n );\n // Never swallow a finding: a bundle-managed row used to drop its layers, so an\n // error counted in the summary had no visible cause. `BUNDLE_MANAGED` is the\n // expected skip and would just restate the row, so it alone stays quiet.\n const problems = r.layers.filter(\n (l) => l.status !== \"ok\" && l.code !== BUNDLE_MANAGED_CODE,\n );\n if (problems.length > 0) {\n printRowBody(\n problems.map((l) => l.detail),\n problems.map((l) => l.hint),\n );\n }\n}\n\n/** Prints a wiring finding with the same shape as a resource row:\n * `glyph label` on the first line, then the indented detail and hint. */\nfunction printWiringRow(f: WiringFinding): void {\n console.log(` ${glyph(f.status)} ${highlight(f.label)}`);\n printRowBody([f.detail], [f.hint]);\n}\n\n/** A unified printable row. `expanded` rows get a blank line on either side;\n * plain `ok` rows pack together. */\ninterface Row {\n status: CheckStatus;\n expanded: boolean;\n print: () => void;\n}\n\nexport function printReport(report: DoctorReport, detail = false): void {\n const { resources, wiring, setup, summary, auth } = report;\n\n // One flat, severity-sorted list. Auth leads and never sorts.\n const rows: Row[] = [];\n rows.push({\n status: auth.status,\n expanded: auth.status === \"error\" || auth.status === \"warn\",\n print: () => printAuthRow(report, detail),\n });\n\n // When auth failed, resources whose only finding is the auth-skip carry no\n // new information, so collapse them into a single line.\n const authSkipped: ResourceCheckResult[] = [];\n const checked: Row[] = [];\n for (const r of resources) {\n if (isOnlyAuthSkipped(r)) {\n authSkipped.push(r);\n continue;\n }\n checked.push({\n status: r.status,\n // A bundle-managed row is a one-liner unless it carries a real finding, in\n // which case it needs the spacing every expanded row gets.\n expanded:\n r.status !== \"ok\" &&\n r.layers.some(\n (l) => l.status !== \"ok\" && l.code !== BUNDLE_MANAGED_CODE,\n ),\n print: () => printResourceRow(r),\n });\n }\n if (authSkipped.length > 0) {\n checked.push({\n status: \"skipped\",\n expanded: false,\n print: () =>\n console.log(\n ` ${pc.dim(\"•\")} ${pc.dim(\n `${authSkipped.length} resource${authSkipped.length > 1 ? \"s\" : \"\"} not checked (auth failed)`,\n )}`,\n ),\n });\n }\n for (const f of wiring) {\n checked.push({\n status: f.status,\n expanded: true,\n print: () => printWiringRow(f),\n });\n }\n // Setup notices share the wiring row shape (same `label`/`detail`/`hint`).\n for (const f of setup) {\n checked.push({\n status: f.status,\n expanded: true,\n print: () => printWiringRow(f),\n });\n }\n\n // Most severe first (descending severity); stable within a status.\n checked.sort((a, b) => STATUS_SEVERITY[b.status] - STATUS_SEVERITY[a.status]);\n rows.push(...checked);\n\n // A blank line sets any expanded row apart from its neighbours.\n console.log(\"\");\n rows.forEach((row, i) => {\n const prev = rows[i - 1];\n if (i > 0 && (row.expanded || prev.expanded)) console.log(\"\");\n row.print();\n });\n\n // `summary` already counts auth + wiring (built authoritatively in run.ts),\n // so render it directly — no re-folding, and it matches what --json emits.\n console.log(\"\");\n console.log(summaryLine(summary));\n // Point at --detail when there's more to show and it wasn't requested.\n if (!detail && auth.raw) {\n console.log(pc.dim(\"\\nRun with --detail for full error output.\"));\n }\n console.log(\"\");\n}\n\n/**\n * Emits the report as JSON. The raw SDK error (`auth.raw`) can carry sensitive\n * detail, so — matching the human report's `--detail` gate — it's stripped\n * unless `detail` is set. `--json --detail` opts back into the full error.\n */\nexport function printReportJson(report: DoctorReport, detail = false): void {\n const emitted =\n detail || report.auth.raw === undefined\n ? report\n : { ...report, auth: { ...report.auth, raw: undefined } };\n console.log(JSON.stringify(emitted, null, 2));\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-targets.js","names":[],"sources":["../../../../src/cli/commands/doctor/resolve-targets.ts"],"sourcesContent":["/**\n * Reads the app's resolved template manifest (`appkit.plugins.json`) and\n * flattens each plugin's declared resources into the flat {@link ResourceTarget}\n * shape the checks consume. Offline and SDK-free — parses the same file\n * `appkit plugin list` reads.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { computeOriginFromField } from \"../../../schemas/manifest\";\nimport type { ResourceTarget } from \"./types\";\nimport { errorMessage } from \"./utils\";\n\nexport const DEFAULT_MANIFEST_FILE = \"appkit.plugins.json\";\n\ninterface ManifestField {\n env?: string;\n /** Static default value baked into the manifest. */\n value?: string;\n origin?: \"user\" | \"platform\" | \"static\" | \"cli\";\n /** Resolver key (cli origin), e.g. \"postgres:host\". */\n resolve?: string;\n /** Only generated into the local .env; the platform injects it at deploy. */\n localOnly?: boolean;\n}\n\ninterface ManifestResource {\n type: string;\n resourceKey: string;\n alias?: string;\n permission: string;\n fields?: Record<string, ManifestField>;\n}\n\ninterface ManifestPlugin {\n resources?: {\n required?: ManifestResource[];\n optional?: ManifestResource[];\n };\n /**\n * True when the plugin is actually used by the app (imported and passed to\n * `createApp`). `plugin sync` discovers *every* plugin shipped by installed\n * packages but only marks the used ones; doctor checks only those, so an\n * unused built-in doesn't produce phantom \"missing env var\" errors.\n */\n requiredByTemplate?: boolean;\n}\n\ninterface TemplateManifest {\n plugins?: Record<string, ManifestPlugin>;\n}\n\n/** A field's env var is the developer's to supply only when its effective\n * origin is `\"user\"` — a stamped `origin`, else derived from the field shape. */\nfunction isUserSuppliedEnv(field: ManifestField): boolean {\n const origin = field.origin ?? computeOriginFromField(field);\n return origin === \"user\";\n}\n\n/** Env vars the config layer should presence-check (i.e. user-supplied ones). */\nfunction envVarsOf(resource: ManifestResource): string[] {\n const fields = resource.fields ?? {};\n const envs: string[] = [];\n for (const field of Object.values(fields)) {\n if (field?.env && isUserSuppliedEnv(field)) envs.push(field.env);\n }\n return envs;\n}\n\n/** Resolves each field's value keyed by manifest field name, preferring the\n * environment over a static `value` default. Unset/empty fields are omitted. */\nfunction fieldValuesOf(resource: ManifestResource): Record<string, string> {\n const fields = resource.fields ?? {};\n const values: Record<string, string> = {};\n for (const [fieldName, field] of Object.entries(fields)) {\n if (!field) continue;\n const envValue = field.env ? process.env[field.env] : undefined;\n const resolved =\n envValue !== undefined && envValue.trim().length > 0\n ? envValue\n : field.value;\n if (resolved !== undefined && resolved.trim().length > 0) {\n values[fieldName] = resolved;\n }\n }\n return values;\n}\n\nfunction toTarget(\n plugin: string,\n resource: ManifestResource,\n required: boolean,\n): ResourceTarget {\n return {\n type: resource.type,\n resourceKey: resource.resourceKey,\n alias: resource.alias ?? resource.resourceKey,\n plugin,\n requiredPermission: resource.permission,\n required,\n envVars: envVarsOf(resource),\n fieldValues: fieldValuesOf(resource),\n };\n}\n\n/** @throws if the file cannot be read or parsed. */\nexport function targetsFromManifestFile(\n manifestPath: string,\n): ResourceTarget[] {\n let raw: string;\n try {\n raw = fs.readFileSync(manifestPath, \"utf-8\");\n } catch (err) {\n throw new Error(\n `Failed to read manifest file ${manifestPath}: ${errorMessage(err)}`,\n );\n }\n\n let data: TemplateManifest;\n try {\n data = JSON.parse(raw) as TemplateManifest;\n } catch (err) {\n throw new Error(\n `Failed to parse manifest file ${manifestPath}: ${errorMessage(err)}`,\n );\n }\n\n // `plugin sync` catalogues every plugin the installed packages ship, marking\n // only those wired into `createApp` with `requiredByTemplate`. Check exactly\n // those, else doctor reports phantom \"missing env var\" errors for unimported\n // plugins.\n // KNOWN LIMITATION: sync strips `requiredByTemplate` for non-GA plugins, so a\n // used beta/experimental plugin is not checked here yet. See the README.\n const selected = Object.entries(data.plugins ?? {}).filter(\n ([, plugin]) => plugin.requiredByTemplate === true,\n );\n\n const targets: ResourceTarget[] = [];\n for (const [pluginName, plugin] of selected) {\n const resources = plugin.resources ?? {};\n for (const resource of resources.required ?? []) {\n targets.push(toTarget(pluginName, resource, true));\n }\n for (const resource of resources.optional ?? []) {\n targets.push(toTarget(pluginName, resource, false));\n }\n }\n return targets;\n}\n\n/** Returns an empty list if the manifest is absent — an app may legitimately\n * declare no resources. */\nexport function resolveTargetsFromCwd(\n cwd: string = process.cwd(),\n manifestFile: string = DEFAULT_MANIFEST_FILE,\n): ResourceTarget[] {\n const manifestPath = path.resolve(cwd, manifestFile);\n if (!fs.existsSync(manifestPath)) {\n return [];\n }\n return targetsFromManifestFile(manifestPath);\n}\n"],"mappings":";;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"resolve-targets.js","names":[],"sources":["../../../../src/cli/commands/doctor/resolve-targets.ts"],"sourcesContent":["/**\n * Reads the app's resolved template manifest (`appkit.plugins.json`) and\n * flattens each plugin's declared resources into the flat {@link ResourceTarget}\n * shape the checks consume. Offline and SDK-free — parses the same file\n * `appkit plugin list` reads.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { computeOriginFromField } from \"../../../schemas/manifest\";\nimport type { ResourceTarget } from \"./types\";\nimport { errorMessage } from \"./utils\";\n\nexport const DEFAULT_MANIFEST_FILE = \"appkit.plugins.json\";\n\ninterface ManifestField {\n env?: string;\n /** Static default value baked into the manifest. */\n value?: string;\n origin?: \"user\" | \"platform\" | \"static\" | \"cli\";\n /** Resolver key (cli origin), e.g. \"postgres:host\". */\n resolve?: string;\n /** Only generated into the local .env; the platform injects it at deploy. */\n localOnly?: boolean;\n}\n\ninterface ManifestResource {\n type: string;\n resourceKey: string;\n alias?: string;\n permission: string;\n fields?: Record<string, ManifestField>;\n}\n\ninterface ManifestPlugin {\n resources?: {\n required?: ManifestResource[];\n optional?: ManifestResource[];\n };\n /**\n * True when the plugin is actually used by the app (imported and passed to\n * `createApp`). `plugin sync` discovers *every* plugin shipped by installed\n * packages but only marks the used ones; doctor checks only those, so an\n * unused built-in doesn't produce phantom \"missing env var\" errors.\n */\n requiredByTemplate?: boolean;\n}\n\ninterface TemplateManifest {\n plugins?: Record<string, ManifestPlugin>;\n}\n\n/** A field's env var is the developer's to supply only when its effective\n * origin is `\"user\"` — a stamped `origin`, else derived from the field shape. */\nfunction isUserSuppliedEnv(field: ManifestField): boolean {\n const origin = field.origin ?? computeOriginFromField(field);\n return origin === \"user\";\n}\n\n/** Env vars the config layer should presence-check (i.e. user-supplied ones). */\nfunction envVarsOf(resource: ManifestResource): string[] {\n const fields = resource.fields ?? {};\n const envs: string[] = [];\n for (const field of Object.values(fields)) {\n if (field?.env && isUserSuppliedEnv(field)) envs.push(field.env);\n }\n return envs;\n}\n\n/** Resolves each field's value keyed by manifest field name, preferring the\n * environment over a static `value` default. Unset/empty fields are omitted. */\nfunction fieldValuesOf(resource: ManifestResource): Record<string, string> {\n const fields = resource.fields ?? {};\n const values: Record<string, string> = {};\n for (const [fieldName, field] of Object.entries(fields)) {\n if (!field) continue;\n const envValue = field.env ? process.env[field.env] : undefined;\n const resolved =\n envValue !== undefined && envValue.trim().length > 0\n ? envValue\n : field.value;\n if (resolved !== undefined && resolved.trim().length > 0) {\n values[fieldName] = resolved;\n }\n }\n return values;\n}\n\nfunction toTarget(\n plugin: string,\n resource: ManifestResource,\n required: boolean,\n): ResourceTarget {\n return {\n type: resource.type,\n resourceKey: resource.resourceKey,\n alias: resource.alias ?? resource.resourceKey,\n plugin,\n requiredPermission: resource.permission,\n required,\n envVars: envVarsOf(resource),\n fieldValues: fieldValuesOf(resource),\n };\n}\n\n/** @throws if the file cannot be read or parsed. */\nexport function targetsFromManifestFile(\n manifestPath: string,\n): ResourceTarget[] {\n let raw: string;\n try {\n raw = fs.readFileSync(manifestPath, \"utf-8\");\n } catch (err) {\n throw new Error(\n `Failed to read manifest file ${manifestPath}: ${errorMessage(err)}`,\n );\n }\n\n let data: TemplateManifest;\n try {\n data = JSON.parse(raw) as TemplateManifest;\n } catch (err) {\n throw new Error(\n `Failed to parse manifest file ${manifestPath}: ${errorMessage(err)}`,\n );\n }\n\n // `plugin sync` catalogues every plugin the installed packages ship, marking\n // only those wired into `createApp` with `requiredByTemplate`. Check exactly\n // those, else doctor reports phantom \"missing env var\" errors for unimported\n // plugins.\n // KNOWN LIMITATION: sync strips `requiredByTemplate` for non-GA plugins, so a\n // used beta/experimental plugin is not checked here yet. See the README.\n const selected = Object.entries(data.plugins ?? {}).filter(\n ([, plugin]) => plugin.requiredByTemplate === true,\n );\n\n const targets: ResourceTarget[] = [];\n for (const [pluginName, plugin] of selected) {\n const resources = plugin.resources ?? {};\n for (const resource of resources.required ?? []) {\n targets.push(toTarget(pluginName, resource, true));\n }\n for (const resource of resources.optional ?? []) {\n targets.push(toTarget(pluginName, resource, false));\n }\n }\n return targets;\n}\n\n/** Returns an empty list if the manifest is absent — an app may legitimately\n * declare no resources. */\nexport function resolveTargetsFromCwd(\n cwd: string = process.cwd(),\n manifestFile: string = DEFAULT_MANIFEST_FILE,\n): ResourceTarget[] {\n const manifestPath = path.resolve(cwd, manifestFile);\n if (!fs.existsSync(manifestPath)) {\n return [];\n }\n return targetsFromManifestFile(manifestPath);\n}\n"],"mappings":";;;;;;;;;;;;AAcA,MAAa,wBAAwB;;;AAyCrC,SAAS,kBAAkB,OAA+B;AAExD,SADe,MAAM,UAAU,uBAAuB,MAAM,MAC1C;;;AAIpB,SAAS,UAAU,UAAsC;CACvD,MAAM,SAAS,SAAS,UAAU,EAAE;CACpC,MAAM,OAAiB,EAAE;AACzB,MAAK,MAAM,SAAS,OAAO,OAAO,OAAO,CACvC,KAAI,OAAO,OAAO,kBAAkB,MAAM,CAAE,MAAK,KAAK,MAAM,IAAI;AAElE,QAAO;;;;AAKT,SAAS,cAAc,UAAoD;CACzE,MAAM,SAAS,SAAS,UAAU,EAAE;CACpC,MAAM,SAAiC,EAAE;AACzC,MAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,OAAO,EAAE;AACvD,MAAI,CAAC,MAAO;EACZ,MAAM,WAAW,MAAM,MAAM,QAAQ,IAAI,MAAM,OAAO;EACtD,MAAM,WACJ,aAAa,UAAa,SAAS,MAAM,CAAC,SAAS,IAC/C,WACA,MAAM;AACZ,MAAI,aAAa,UAAa,SAAS,MAAM,CAAC,SAAS,EACrD,QAAO,aAAa;;AAGxB,QAAO;;AAGT,SAAS,SACP,QACA,UACA,UACgB;AAChB,QAAO;EACL,MAAM,SAAS;EACf,aAAa,SAAS;EACtB,OAAO,SAAS,SAAS,SAAS;EAClC;EACA,oBAAoB,SAAS;EAC7B;EACA,SAAS,UAAU,SAAS;EAC5B,aAAa,cAAc,SAAS;EACrC;;;AAIH,SAAgB,wBACd,cACkB;CAClB,IAAI;AACJ,KAAI;AACF,QAAM,GAAG,aAAa,cAAc,QAAQ;UACrC,KAAK;AACZ,QAAM,IAAI,MACR,gCAAgC,aAAa,IAAI,aAAa,IAAI,GACnE;;CAGH,IAAI;AACJ,KAAI;AACF,SAAO,KAAK,MAAM,IAAI;UACf,KAAK;AACZ,QAAM,IAAI,MACR,iCAAiC,aAAa,IAAI,aAAa,IAAI,GACpE;;CASH,MAAM,WAAW,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC,CAAC,QACjD,GAAG,YAAY,OAAO,uBAAuB,KAC/C;CAED,MAAM,UAA4B,EAAE;AACpC,MAAK,MAAM,CAAC,YAAY,WAAW,UAAU;EAC3C,MAAM,YAAY,OAAO,aAAa,EAAE;AACxC,OAAK,MAAM,YAAY,UAAU,YAAY,EAAE,CAC7C,SAAQ,KAAK,SAAS,YAAY,UAAU,KAAK,CAAC;AAEpD,OAAK,MAAM,YAAY,UAAU,YAAY,EAAE,CAC7C,SAAQ,KAAK,SAAS,YAAY,UAAU,MAAM,CAAC;;AAGvD,QAAO;;;;AAKT,SAAgB,sBACd,MAAc,QAAQ,KAAK,EAC3B,eAAuB,uBACL;CAClB,MAAM,eAAe,KAAK,QAAQ,KAAK,aAAa;AACpD,KAAI,CAAC,GAAG,WAAW,aAAa,CAC9B,QAAO,EAAE;AAEX,QAAO,wBAAwB,aAAa"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.js","names":[],"sources":["../../../../src/cli/commands/doctor/run.ts"],"sourcesContent":["/** Orchestration for `appkit doctor`. */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { originForEnvVars, readBundleInfo } from \"./bundle\";\nimport {\n type ConfigCheckContext,\n checkAuth,\n checkConfig,\n DEFAULT_ENV_FILE,\n} from \"./checks\";\nimport { runExistenceProbe } from \"./checks-existence\";\nimport { checkWiring } from \"./checks-wiring\";\nimport {\n DEFAULT_MANIFEST_FILE,\n resolveTargetsFromCwd,\n} from \"./resolve-targets\";\nimport {\n AUTH_UNAVAILABLE_CODE,\n BUNDLE_MANAGED_CODE,\n type CheckStatus,\n type DoctorOptions,\n type DoctorReport,\n type LayerResult,\n type ResourceCheckResult,\n type ResourceTarget,\n type SetupFinding,\n STATUS_SEVERITY,\n} from \"./types\";\nimport { errorMessage, TimeoutError, withTimeout } from \"./utils\";\n\nfunction worst(a: CheckStatus, b: CheckStatus): CheckStatus {\n return STATUS_SEVERITY[b] > STATUS_SEVERITY[a] ? b : a;\n}\n\nasync function checkResource(\n target: ResourceTarget,\n // undefined = auth failed, so the live existence layer is skipped.\n client: unknown,\n configCtx: ConfigCheckContext,\n): Promise<ResourceCheckResult> {\n const layers: LayerResult[] = [];\n let rolled: CheckStatus = \"ok\";\n\n // A bundle-managed resource is checked before anything else, because neither\n // remaining layer can say anything true about it: its value comes from\n // `${resources.*}` at deploy time, so an unset env var locally is the *normal*\n // state (not a config error), and probing would be a false NOT_FOUND. A\n // mismatch between the three files is the wiring layer's job, and problems\n // inside databricks.yml are `databricks bundle validate`'s.\n //\n // This must run first: checking config here used to error on the unset var and\n // return early, never reaching this branch — which failed CI for a correctly\n // configured app while the report collapsed the row to a green-looking\n // \"will be created on deploy\" with the error hidden.\n if (target.origin === \"bundle-managed\") {\n return {\n target,\n status: \"skipped\",\n layers: [\n {\n layer: \"existence\",\n status: \"skipped\",\n code: BUNDLE_MANAGED_CODE,\n detail: \"created by this bundle on deploy — not probed\",\n },\n ],\n };\n }\n\n const configResult = checkConfig(target, configCtx);\n layers.push(configResult);\n rolled = worst(rolled, configResult.status);\n // A hard config failure makes the existence probe meaningless.\n if (configResult.status === \"error\") {\n return { target, status: rolled, layers };\n }\n\n if (client === undefined) {\n layers.push({\n layer: \"existence\",\n status: \"skipped\",\n code: AUTH_UNAVAILABLE_CODE,\n detail: \"skipped because workspace authentication failed\",\n });\n rolled = worst(rolled, \"skipped\");\n } else {\n // A reachable-but-unresponsive endpoint must not hang doctor, so bound the\n // probe; a timeout becomes an error row rather than a hung process.\n const result = await withTimeout(runExistenceProbe(client, target)).catch(\n (err): LayerResult => ({\n layer: \"existence\",\n status: \"error\",\n code: err instanceof TimeoutError ? \"PROBE_TIMEOUT\" : \"PROBE_FAILED\",\n detail: `probe did not complete: ${errorMessage(err)}`,\n }),\n );\n layers.push(result);\n rolled = worst(rolled, result.status);\n }\n\n return { target, status: rolled, layers };\n}\n\n/**\n * Runs {@link checkResource} behind a guaranteed boundary: any unexpected throw\n * (a probe's own `.catch` handles the common case, but this covers config\n * errors and anything else) becomes a single error row instead of rejecting the\n * `Promise.all` and losing the entire report to a stack trace.\n */\nasync function checkResourceSafe(\n target: ResourceTarget,\n client: unknown,\n configCtx: ConfigCheckContext,\n): Promise<ResourceCheckResult> {\n try {\n return await checkResource(target, client, configCtx);\n } catch (err) {\n return {\n target,\n status: \"error\",\n layers: [\n {\n layer: \"existence\",\n status: \"error\",\n code: \"PROBE_EXCEPTION\",\n detail: `unexpected error while checking: ${errorMessage(err)}`,\n },\n ],\n };\n }\n}\n\n/**\n * Checks doctor's own inputs, which otherwise produce a misleading report: run\n * from the wrong directory (say `server/` instead of the app root), nothing is\n * found and the result is a near-empty all-clear — a CI gate passing an app it\n * never looked at.\n *\n * The manifest is the signal for \"this is an app root\". Without it there's one\n * cause worth reporting (wrong directory), so it reports only that; a missing\n * `.env` would be noise on top. With it, a missing `.env` is worth its own notice\n * because local values could then only have come from the shell.\n *\n * Warnings, not errors: an app may legitimately have no `.env` (values exported\n * in the shell, or running in a deployed container), so this must not fail a\n * build on its own.\n */\nexport function checkSetup(\n cwd: string,\n options: DoctorOptions,\n): SetupFinding[] {\n // Presence of the manifest, not the target count: a manifest that declares no\n // resources is legitimate and shouldn't be reported as a missing file.\n if (!fs.existsSync(path.join(cwd, DEFAULT_MANIFEST_FILE))) {\n return [\n {\n status: \"warn\",\n code: \"NO_RESOURCES_CHECKED\",\n label: \"no resources checked\",\n detail: `no ${DEFAULT_MANIFEST_FILE} found in ${cwd}, so no plugin resources were checked`,\n hint: `Run doctor from the app root (where ${DEFAULT_MANIFEST_FILE} lives), or run \\`appkit plugin sync --write\\` if it's missing.`,\n },\n ];\n }\n\n // An explicit --env-file that's missing already throws in the CLI, so only the\n // auto-loaded default is worth reporting here.\n if (!options.envFile && !fs.existsSync(path.join(cwd, DEFAULT_ENV_FILE))) {\n return [\n {\n status: \"warn\",\n code: \"ENV_FILE_MISSING\",\n label: DEFAULT_ENV_FILE,\n detail: `no ${DEFAULT_ENV_FILE} found in ${cwd} — local values can only come from the shell`,\n hint: `Create one (see .env.example), or pass --env-file <path>.`,\n },\n ];\n }\n\n return [];\n}\n\nexport async function runDoctor(options: DoctorOptions): Promise<DoctorReport> {\n const { result: auth, client } = await checkAuth(options);\n\n // Resolve and config-check resources even when auth failed, so a bad\n // connection still surfaces config problems instead of hiding them.\n const cwd = process.cwd();\n const targets = resolveTargetsFromCwd(cwd);\n\n const bundle = readBundleInfo(cwd);\n for (const target of targets) {\n const origin = originForEnvVars(target.envVars, bundle);\n if (origin) target.origin = origin;\n }\n\n // Name the file local values actually came from, and let the config layer see\n // the deploy wiring so it doesn't advise fixing what's already correct.\n const configCtx: ConfigCheckContext = {\n envFile: options.envFile ?? DEFAULT_ENV_FILE,\n wiredEnvVars: new Set(bundle.envToBinding.keys()),\n };\n\n const setup = checkSetup(cwd, options);\n\n // Probes are independent reads; Promise.all preserves order for a\n // deterministic report.\n const resources = await Promise.all(\n targets.map((target) => checkResourceSafe(target, client, configCtx)),\n );\n const wiring = checkWiring(bundle, targets);\n\n // The summary counts *everything* with a status — resources, the auth check,\n // wiring findings, and setup notices — so a --json consumer reading\n // summary.error can trust it, and the human/JSON outputs share one source of\n // truth.\n const summary = { ok: 0, warn: 0, error: 0, skipped: 0 };\n for (const result of resources) summary[result.status] += 1;\n summary[auth.status] += 1;\n for (const finding of wiring) summary[finding.status] += 1;\n for (const finding of setup) summary[finding.status] += 1;\n\n const exitCode = summary.error > 0 ? 1 : 0;\n\n return { auth, resources, wiring, setup, summary, exitCode };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"run.js","names":[],"sources":["../../../../src/cli/commands/doctor/run.ts"],"sourcesContent":["/** Orchestration for `appkit doctor`. */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { originForEnvVars, readBundleInfo } from \"./bundle\";\nimport {\n type ConfigCheckContext,\n checkAuth,\n checkConfig,\n DEFAULT_ENV_FILE,\n} from \"./checks\";\nimport { runExistenceProbe } from \"./checks-existence\";\nimport { checkWiring } from \"./checks-wiring\";\nimport {\n DEFAULT_MANIFEST_FILE,\n resolveTargetsFromCwd,\n} from \"./resolve-targets\";\nimport {\n AUTH_UNAVAILABLE_CODE,\n BUNDLE_MANAGED_CODE,\n type CheckStatus,\n type DoctorOptions,\n type DoctorReport,\n type LayerResult,\n type ResourceCheckResult,\n type ResourceTarget,\n type SetupFinding,\n STATUS_SEVERITY,\n} from \"./types\";\nimport { errorMessage, TimeoutError, withTimeout } from \"./utils\";\n\nfunction worst(a: CheckStatus, b: CheckStatus): CheckStatus {\n return STATUS_SEVERITY[b] > STATUS_SEVERITY[a] ? b : a;\n}\n\nasync function checkResource(\n target: ResourceTarget,\n // undefined = auth failed, so the live existence layer is skipped.\n client: unknown,\n configCtx: ConfigCheckContext,\n): Promise<ResourceCheckResult> {\n const layers: LayerResult[] = [];\n let rolled: CheckStatus = \"ok\";\n\n // A bundle-managed resource is checked before anything else, because neither\n // remaining layer can say anything true about it: its value comes from\n // `${resources.*}` at deploy time, so an unset env var locally is the *normal*\n // state (not a config error), and probing would be a false NOT_FOUND. A\n // mismatch between the three files is the wiring layer's job, and problems\n // inside databricks.yml are `databricks bundle validate`'s.\n //\n // This must run first: checking config here used to error on the unset var and\n // return early, never reaching this branch — which failed CI for a correctly\n // configured app while the report collapsed the row to a green-looking\n // \"will be created on deploy\" with the error hidden.\n if (target.origin === \"bundle-managed\") {\n return {\n target,\n status: \"skipped\",\n layers: [\n {\n layer: \"existence\",\n status: \"skipped\",\n code: BUNDLE_MANAGED_CODE,\n detail: \"created by this bundle on deploy — not probed\",\n },\n ],\n };\n }\n\n const configResult = checkConfig(target, configCtx);\n layers.push(configResult);\n rolled = worst(rolled, configResult.status);\n // A hard config failure makes the existence probe meaningless.\n if (configResult.status === \"error\") {\n return { target, status: rolled, layers };\n }\n\n if (client === undefined) {\n layers.push({\n layer: \"existence\",\n status: \"skipped\",\n code: AUTH_UNAVAILABLE_CODE,\n detail: \"skipped because workspace authentication failed\",\n });\n rolled = worst(rolled, \"skipped\");\n } else {\n // A reachable-but-unresponsive endpoint must not hang doctor, so bound the\n // probe; a timeout becomes an error row rather than a hung process.\n const result = await withTimeout(runExistenceProbe(client, target)).catch(\n (err): LayerResult => ({\n layer: \"existence\",\n status: \"error\",\n code: err instanceof TimeoutError ? \"PROBE_TIMEOUT\" : \"PROBE_FAILED\",\n detail: `probe did not complete: ${errorMessage(err)}`,\n }),\n );\n layers.push(result);\n rolled = worst(rolled, result.status);\n }\n\n return { target, status: rolled, layers };\n}\n\n/**\n * Runs {@link checkResource} behind a guaranteed boundary: any unexpected throw\n * (a probe's own `.catch` handles the common case, but this covers config\n * errors and anything else) becomes a single error row instead of rejecting the\n * `Promise.all` and losing the entire report to a stack trace.\n */\nasync function checkResourceSafe(\n target: ResourceTarget,\n client: unknown,\n configCtx: ConfigCheckContext,\n): Promise<ResourceCheckResult> {\n try {\n return await checkResource(target, client, configCtx);\n } catch (err) {\n return {\n target,\n status: \"error\",\n layers: [\n {\n layer: \"existence\",\n status: \"error\",\n code: \"PROBE_EXCEPTION\",\n detail: `unexpected error while checking: ${errorMessage(err)}`,\n },\n ],\n };\n }\n}\n\n/**\n * Checks doctor's own inputs, which otherwise produce a misleading report: run\n * from the wrong directory (say `server/` instead of the app root), nothing is\n * found and the result is a near-empty all-clear — a CI gate passing an app it\n * never looked at.\n *\n * The manifest is the signal for \"this is an app root\". Without it there's one\n * cause worth reporting (wrong directory), so it reports only that; a missing\n * `.env` would be noise on top. With it, a missing `.env` is worth its own notice\n * because local values could then only have come from the shell.\n *\n * Warnings, not errors: an app may legitimately have no `.env` (values exported\n * in the shell, or running in a deployed container), so this must not fail a\n * build on its own.\n */\nexport function checkSetup(\n cwd: string,\n options: DoctorOptions,\n): SetupFinding[] {\n // Presence of the manifest, not the target count: a manifest that declares no\n // resources is legitimate and shouldn't be reported as a missing file.\n if (!fs.existsSync(path.join(cwd, DEFAULT_MANIFEST_FILE))) {\n return [\n {\n status: \"warn\",\n code: \"NO_RESOURCES_CHECKED\",\n label: \"no resources checked\",\n detail: `no ${DEFAULT_MANIFEST_FILE} found in ${cwd}, so no plugin resources were checked`,\n hint: `Run doctor from the app root (where ${DEFAULT_MANIFEST_FILE} lives), or run \\`appkit plugin sync --write\\` if it's missing.`,\n },\n ];\n }\n\n // An explicit --env-file that's missing already throws in the CLI, so only the\n // auto-loaded default is worth reporting here.\n if (!options.envFile && !fs.existsSync(path.join(cwd, DEFAULT_ENV_FILE))) {\n return [\n {\n status: \"warn\",\n code: \"ENV_FILE_MISSING\",\n label: DEFAULT_ENV_FILE,\n detail: `no ${DEFAULT_ENV_FILE} found in ${cwd} — local values can only come from the shell`,\n hint: `Create one (see .env.example), or pass --env-file <path>.`,\n },\n ];\n }\n\n return [];\n}\n\nexport async function runDoctor(options: DoctorOptions): Promise<DoctorReport> {\n const { result: auth, client } = await checkAuth(options);\n\n // Resolve and config-check resources even when auth failed, so a bad\n // connection still surfaces config problems instead of hiding them.\n const cwd = process.cwd();\n const targets = resolveTargetsFromCwd(cwd);\n\n const bundle = readBundleInfo(cwd);\n for (const target of targets) {\n const origin = originForEnvVars(target.envVars, bundle);\n if (origin) target.origin = origin;\n }\n\n // Name the file local values actually came from, and let the config layer see\n // the deploy wiring so it doesn't advise fixing what's already correct.\n const configCtx: ConfigCheckContext = {\n envFile: options.envFile ?? DEFAULT_ENV_FILE,\n wiredEnvVars: new Set(bundle.envToBinding.keys()),\n };\n\n const setup = checkSetup(cwd, options);\n\n // Probes are independent reads; Promise.all preserves order for a\n // deterministic report.\n const resources = await Promise.all(\n targets.map((target) => checkResourceSafe(target, client, configCtx)),\n );\n const wiring = checkWiring(bundle, targets);\n\n // The summary counts *everything* with a status — resources, the auth check,\n // wiring findings, and setup notices — so a --json consumer reading\n // summary.error can trust it, and the human/JSON outputs share one source of\n // truth.\n const summary = { ok: 0, warn: 0, error: 0, skipped: 0 };\n for (const result of resources) summary[result.status] += 1;\n summary[auth.status] += 1;\n for (const finding of wiring) summary[finding.status] += 1;\n for (const finding of setup) summary[finding.status] += 1;\n\n const exitCode = summary.error > 0 ? 1 : 0;\n\n return { auth, resources, wiring, setup, summary, exitCode };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-types.js","names":[],"sources":["../../../src/cli/commands/generate-types.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command, Option } from \"commander\";\nimport { METRIC_CONFIG_FILE } from \"../../schemas/metric-fqn\";\nimport {\n acquireSpawnLock,\n getSpawnLockPath,\n releaseSpawnLock,\n} from \"./spawn-lock.js\";\n\n/**\n * Resolve the typegen pre-flight mode for the CLI. Defaults to \"non-blocking\" —\n * a one-shot CLI can't describe in the background, so by default it never\n * describes at all: it skips the warehouse probe AND every DESCRIBE, emits\n * best-available types (cache where the SQL hash matches, else `result: unknown`)\n * and returns immediately, never blocking on — or failing because of — a\n * warehouse, even a RUNNING one. Pass `--wait` (commander sets `wait: true`)\n * for a deliberate/CI invocation that should wait for a starting warehouse and\n * fail fast on a stopped one.\n */\nexport function resolveTypegenMode(options?: {\n wait?: boolean;\n}): \"non-blocking\" | \"blocking\" {\n return options?.wait ? \"blocking\" : \"non-blocking\";\n}\n\n/** Options parsed by commander for the generate-types command. */\ninterface GenerateTypesOptions {\n noCache?: boolean;\n wait?: boolean;\n /**\n * Internal: present only on the detached worker invocation. Carries the path\n * of the single-flight lock this worker must release when it finishes. Its\n * presence is what marks an invocation as \"the worker\" — workers always run\n * with `--wait`, so they never spawn another worker (only non-blocking runs\n * spawn), which terminates the recursion.\n */\n workerLock?: string;\n}\n\n/**\n * Generate types command implementation. Runs the library generate (which, in\n * non-blocking mode, writes degraded types and returns immediately). This is the\n * SAME work the worker performs in blocking mode in the background.\n */\nasync function runGenerateTypes(\n rootDir?: string,\n outFile?: string,\n warehouseId?: string,\n options?: GenerateTypesOptions,\n) {\n try {\n const resolvedRootDir = rootDir || process.cwd();\n const noCache = options?.noCache || false;\n const mode = resolveTypegenMode(options);\n\n const typeGen = await import(\"@databricks/appkit/type-generator\");\n\n // Generate analytics query types (requires warehouse ID)\n const resolvedWarehouseId =\n warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;\n\n if (resolvedWarehouseId) {\n const resolvedOutFile =\n outFile ||\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\");\n\n const queryFolder = path.join(resolvedRootDir, \"config/queries\");\n const metricViewsFolder = path.join(\n resolvedRootDir,\n \"config/metric-views\",\n );\n const hasQueries = fs.existsSync(queryFolder);\n const hasMetricViews = fs.existsSync(metricViewsFolder);\n\n // Generate when either config surface exists. Metric-view types are\n // independent of `.sql` queries — an app can declare metric views in\n // `config/metric-views/` without a `config/queries/` folder.\n if (hasQueries || hasMetricViews) {\n await typeGen.generateFromEntryPoint({\n queryFolder: hasQueries ? queryFolder : undefined,\n metricViewsFolder: hasMetricViews ? metricViewsFolder : undefined,\n outFile: resolvedOutFile,\n warehouseId: resolvedWarehouseId,\n noCache,\n mode,\n });\n\n if (hasQueries) {\n console.log(`Generated query types: ${resolvedOutFile}`);\n }\n\n const metricConfig = path.join(metricViewsFolder, METRIC_CONFIG_FILE);\n if (fs.existsSync(metricConfig)) {\n const typesDir = path.dirname(resolvedOutFile);\n console.log(\n `Generated metric types: ${path.join(typesDir, \"metric-views.ts\")}`,\n );\n }\n }\n } else {\n console.error(\n \"Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.\",\n );\n }\n\n // Generate serving endpoint types (no warehouse required)\n const servingOutFile = path.join(\n process.cwd(),\n \"shared/appkit-types/serving.d.ts\",\n );\n await typeGen.generateServingTypes({\n outFile: servingOutFile,\n noCache,\n });\n console.log(`Generated serving types: ${servingOutFile}`);\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"Cannot find module\")\n ) {\n console.error(\n \"Error: The 'generate-types' command is only available in @databricks/appkit.\",\n );\n console.error(\"Please install @databricks/appkit to use this command.\");\n process.exit(1);\n }\n // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // message (which queries failed and how to debug them). The stack trace\n // points into appkit internals and is noise for app developers, so print\n // only the message and exit non-zero instead of letting it bubble up.\n if (\n error instanceof Error &&\n (error.name === \"TypegenSyntaxError\" ||\n error.name === \"TypegenFatalError\")\n ) {\n console.error(error.message);\n process.exit(1);\n }\n throw error;\n }\n}\n\n/**\n * Spawn the detached blocking worker that refreshes real types in the background\n * after the foreground non-blocking generate has already written degraded types.\n *\n * Re-invokes THIS CLI (`process.execPath` + `process.argv[1]` — the bin entry\n * that launched us) with `generate-types --wait --worker-lock <lockPath>` plus\n * the same positional target options the foreground used, so the worker writes\n * to the same out file / reads the same query folder. The worker is:\n * - `detached: true` + `.unref()` so it outlives this process (install/dev-setup\n * can finish and exit while the worker keeps warming the warehouse).\n * - `stdio: \"ignore\"` so it never holds the parent's pipes open or interleaves\n * output into the install/dev log.\n *\n * Spawning is wrapped so any failure is non-fatal: the caller still has degraded\n * types and exits 0.\n *\n * @param lockPath - the acquired single-flight lock; passed to the worker so it\n * releases the SAME lock when it finishes.\n * @param targets - the foreground's positional args, forwarded verbatim.\n * @returns true if the worker was spawned, false if spawning threw.\n */\nexport function spawnTypegenWorker(\n lockPath: string,\n targets: { rootDir?: string; outFile?: string; warehouseId?: string },\n): boolean {\n // The script the runtime launched us with (the `appkit` bin shim). Re-running\n // it under the same node binary reproduces this exact CLI in the worker.\n const cliEntry = process.argv[1];\n\n // Forward the positionals in declaration order (rootDir, outFile,\n // warehouseId). Stop at the first undefined so we never pass a literal\n // \"undefined\" — commander would treat it as a positional value. (rootDir is\n // effectively always set by commander's default, but guard anyway.)\n const positionals: string[] = [];\n for (const value of [targets.rootDir, targets.outFile, targets.warehouseId]) {\n if (value === undefined) break;\n positionals.push(value);\n }\n\n const args = [\n // Forward the parent's node/loader flags so the worker runs under the same\n // runtime. Critically this carries tsx's `--require`/`--import` when the CLI\n // is run from source (`tsx index.ts …`); without them the worker would be\n // `node index.ts …`, which can't parse TypeScript and dies silently — the\n // degraded types would then never refresh. Empty for the built bin (plain\n // `node bin/appkit.js`), so production behaviour is unchanged.\n ...process.execArgv,\n cliEntry,\n \"generate-types\",\n \"--wait\",\n \"--worker-lock\",\n lockPath,\n ...positionals,\n ];\n\n try {\n const child = spawn(process.execPath, args, {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n return true;\n } catch (error) {\n // Non-fatal: the foreground already wrote degraded types. Log and move on.\n console.error(\n `Could not start background type refresh: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n return false;\n }\n}\n\n/**\n * The command action. Orchestrates the non-blocking foreground contract:\n * 1. Run the library generate (writes degraded types immediately in non-blocking\n * mode; does the full blocking lifecycle when this is the worker).\n * 2. If this is a non-blocking, non-worker invocation, try to spawn the detached\n * blocking worker behind the single-flight lock. If the lock is already held\n * by a live worker, skip (single-flight) with a one-line note. Either way the\n * foreground returns normally (exit 0).\n * 3. If this IS the worker (`--worker-lock` present), it ran blocking above and\n * releases the lock here (and via a process-exit guard, so a hard failure /\n * process.exit still frees it).\n */\nasync function generateTypesAction(\n rootDir: string | undefined,\n outFile: string | undefined,\n warehouseId: string | undefined,\n options: GenerateTypesOptions,\n) {\n const isWorker = typeof options.workerLock === \"string\";\n\n // A worker must always free its lock, even if the blocking generate throws or\n // calls process.exit (TypegenFatalError → exit 1). The exit handler covers the\n // process.exit / uncaught paths; the finally covers the normal return.\n if (isWorker && options.workerLock) {\n const lockPath = options.workerLock;\n process.once(\"exit\", () => releaseSpawnLock(lockPath));\n }\n\n try {\n await runGenerateTypes(rootDir, outFile, warehouseId, options);\n } finally {\n if (isWorker && options.workerLock) {\n releaseSpawnLock(options.workerLock);\n }\n }\n\n // Only a non-blocking, non-worker invocation spawns. A worker is always\n // --wait (so resolveTypegenMode → \"blocking\"), which both prevents recursion\n // and means we never get here for a worker.\n if (!isWorker && resolveTypegenMode(options) === \"non-blocking\") {\n const resolvedRootDir = rootDir || process.cwd();\n const lockPath = getSpawnLockPath(resolvedRootDir);\n\n if (acquireSpawnLock(lockPath)) {\n spawnTypegenWorker(lockPath, { rootDir, outFile, warehouseId });\n } else {\n console.log(\"Type refresh already in progress, skipping.\");\n }\n }\n}\n\nexport const generateTypesCommand = new Command(\"generate-types\")\n .description(\"Generate TypeScript types from SQL queries\")\n .argument(\"[rootDir]\", \"Root directory of the project\", process.cwd())\n .argument(\n \"[outFile]\",\n \"Output file path\",\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\"),\n )\n .argument(\"[warehouseId]\", \"Databricks warehouse ID\")\n .option(\"--no-cache\", \"Disable caching for type generation\")\n .option(\n \"--wait\",\n \"Wait for warehouse readiness instead of degrading (use for CI)\",\n )\n // Internal: marks the detached background worker and carries the lock it must\n // release. Hidden from --help; users should never pass it.\n .addOption(\n new Option(\n \"--worker-lock <path>\",\n \"Internal: detached worker lock path\",\n ).hideHelp(),\n )\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit generate-types\n $ appkit generate-types . shared/appkit-types/analytics.d.ts\n $ appkit generate-types . shared/appkit-types/analytics.d.ts my-warehouse-id\n $ appkit generate-types --no-cache\n $ appkit generate-types --wait # CI: wait for the warehouse and fail on a cold one`,\n )\n .action(generateTypesAction);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"generate-types.js","names":[],"sources":["../../../src/cli/commands/generate-types.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { Command, Option } from \"commander\";\n\nimport { METRIC_CONFIG_FILE } from \"../../schemas/metric-fqn\";\nimport {\n acquireSpawnLock,\n getSpawnLockPath,\n releaseSpawnLock,\n} from \"./spawn-lock.js\";\n\n/**\n * Resolve the typegen pre-flight mode for the CLI. Defaults to \"non-blocking\" —\n * a one-shot CLI can't describe in the background, so by default it never\n * describes at all: it skips the warehouse probe AND every DESCRIBE, emits\n * best-available types (cache where the SQL hash matches, else `result: unknown`)\n * and returns immediately, never blocking on — or failing because of — a\n * warehouse, even a RUNNING one. Pass `--wait` (commander sets `wait: true`)\n * for a deliberate/CI invocation that should wait for a starting warehouse and\n * fail fast on a stopped one.\n */\nexport function resolveTypegenMode(options?: {\n wait?: boolean;\n}): \"non-blocking\" | \"blocking\" {\n return options?.wait ? \"blocking\" : \"non-blocking\";\n}\n\n/** Options parsed by commander for the generate-types command. */\ninterface GenerateTypesOptions {\n noCache?: boolean;\n wait?: boolean;\n /**\n * Internal: present only on the detached worker invocation. Carries the path\n * of the single-flight lock this worker must release when it finishes. Its\n * presence is what marks an invocation as \"the worker\" — workers always run\n * with `--wait`, so they never spawn another worker (only non-blocking runs\n * spawn), which terminates the recursion.\n */\n workerLock?: string;\n}\n\n/**\n * Generate types command implementation. Runs the library generate (which, in\n * non-blocking mode, writes degraded types and returns immediately). This is the\n * SAME work the worker performs in blocking mode in the background.\n */\nasync function runGenerateTypes(\n rootDir?: string,\n outFile?: string,\n warehouseId?: string,\n options?: GenerateTypesOptions,\n) {\n try {\n const resolvedRootDir = rootDir || process.cwd();\n const noCache = options?.noCache || false;\n const mode = resolveTypegenMode(options);\n\n const typeGen = await import(\"@databricks/appkit/type-generator\");\n\n // Generate analytics query types (requires warehouse ID)\n const resolvedWarehouseId =\n warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;\n\n if (resolvedWarehouseId) {\n const resolvedOutFile =\n outFile ||\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\");\n\n const queryFolder = path.join(resolvedRootDir, \"config/queries\");\n const metricViewsFolder = path.join(\n resolvedRootDir,\n \"config/metric-views\",\n );\n const hasQueries = fs.existsSync(queryFolder);\n const hasMetricViews = fs.existsSync(metricViewsFolder);\n\n // Generate when either config surface exists. Metric-view types are\n // independent of `.sql` queries — an app can declare metric views in\n // `config/metric-views/` without a `config/queries/` folder.\n if (hasQueries || hasMetricViews) {\n await typeGen.generateFromEntryPoint({\n queryFolder: hasQueries ? queryFolder : undefined,\n metricViewsFolder: hasMetricViews ? metricViewsFolder : undefined,\n outFile: resolvedOutFile,\n warehouseId: resolvedWarehouseId,\n noCache,\n mode,\n });\n\n if (hasQueries) {\n console.log(`Generated query types: ${resolvedOutFile}`);\n }\n\n const metricConfig = path.join(metricViewsFolder, METRIC_CONFIG_FILE);\n if (fs.existsSync(metricConfig)) {\n const typesDir = path.dirname(resolvedOutFile);\n console.log(\n `Generated metric types: ${path.join(typesDir, \"metric-views.ts\")}`,\n );\n }\n }\n } else {\n console.error(\n \"Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.\",\n );\n }\n\n // Generate serving endpoint types (no warehouse required)\n const servingOutFile = path.join(\n process.cwd(),\n \"shared/appkit-types/serving.d.ts\",\n );\n await typeGen.generateServingTypes({\n outFile: servingOutFile,\n noCache,\n });\n console.log(`Generated serving types: ${servingOutFile}`);\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"Cannot find module\")\n ) {\n console.error(\n \"Error: The 'generate-types' command is only available in @databricks/appkit.\",\n );\n console.error(\"Please install @databricks/appkit to use this command.\");\n process.exit(1);\n }\n // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // message (which queries failed and how to debug them). The stack trace\n // points into appkit internals and is noise for app developers, so print\n // only the message and exit non-zero instead of letting it bubble up.\n if (\n error instanceof Error &&\n (error.name === \"TypegenSyntaxError\" ||\n error.name === \"TypegenFatalError\")\n ) {\n console.error(error.message);\n process.exit(1);\n }\n throw error;\n }\n}\n\n/**\n * Spawn the detached blocking worker that refreshes real types in the background\n * after the foreground non-blocking generate has already written degraded types.\n *\n * Re-invokes THIS CLI (`process.execPath` + `process.argv[1]` — the bin entry\n * that launched us) with `generate-types --wait --worker-lock <lockPath>` plus\n * the same positional target options the foreground used, so the worker writes\n * to the same out file / reads the same query folder. The worker is:\n * - `detached: true` + `.unref()` so it outlives this process (install/dev-setup\n * can finish and exit while the worker keeps warming the warehouse).\n * - `stdio: \"ignore\"` so it never holds the parent's pipes open or interleaves\n * output into the install/dev log.\n *\n * Spawning is wrapped so any failure is non-fatal: the caller still has degraded\n * types and exits 0.\n *\n * @param lockPath - the acquired single-flight lock; passed to the worker so it\n * releases the SAME lock when it finishes.\n * @param targets - the foreground's positional args, forwarded verbatim.\n * @returns true if the worker was spawned, false if spawning threw.\n */\nexport function spawnTypegenWorker(\n lockPath: string,\n targets: { rootDir?: string; outFile?: string; warehouseId?: string },\n): boolean {\n // The script the runtime launched us with (the `appkit` bin shim). Re-running\n // it under the same node binary reproduces this exact CLI in the worker.\n const cliEntry = process.argv[1];\n\n // Forward the positionals in declaration order (rootDir, outFile,\n // warehouseId). Stop at the first undefined so we never pass a literal\n // \"undefined\" — commander would treat it as a positional value. (rootDir is\n // effectively always set by commander's default, but guard anyway.)\n const positionals: string[] = [];\n for (const value of [targets.rootDir, targets.outFile, targets.warehouseId]) {\n if (value === undefined) break;\n positionals.push(value);\n }\n\n const args = [\n // Forward the parent's node/loader flags so the worker runs under the same\n // runtime. Critically this carries tsx's `--require`/`--import` when the CLI\n // is run from source (`tsx index.ts …`); without them the worker would be\n // `node index.ts …`, which can't parse TypeScript and dies silently — the\n // degraded types would then never refresh. Empty for the built bin (plain\n // `node bin/appkit.js`), so production behaviour is unchanged.\n ...process.execArgv,\n cliEntry,\n \"generate-types\",\n \"--wait\",\n \"--worker-lock\",\n lockPath,\n ...positionals,\n ];\n\n try {\n const child = spawn(process.execPath, args, {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n return true;\n } catch (error) {\n // Non-fatal: the foreground already wrote degraded types. Log and move on.\n console.error(\n `Could not start background type refresh: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n return false;\n }\n}\n\n/**\n * The command action. Orchestrates the non-blocking foreground contract:\n * 1. Run the library generate (writes degraded types immediately in non-blocking\n * mode; does the full blocking lifecycle when this is the worker).\n * 2. If this is a non-blocking, non-worker invocation, try to spawn the detached\n * blocking worker behind the single-flight lock. If the lock is already held\n * by a live worker, skip (single-flight) with a one-line note. Either way the\n * foreground returns normally (exit 0).\n * 3. If this IS the worker (`--worker-lock` present), it ran blocking above and\n * releases the lock here (and via a process-exit guard, so a hard failure /\n * process.exit still frees it).\n */\nasync function generateTypesAction(\n rootDir: string | undefined,\n outFile: string | undefined,\n warehouseId: string | undefined,\n options: GenerateTypesOptions,\n) {\n const isWorker = typeof options.workerLock === \"string\";\n\n // A worker must always free its lock, even if the blocking generate throws or\n // calls process.exit (TypegenFatalError → exit 1). The exit handler covers the\n // process.exit / uncaught paths; the finally covers the normal return.\n if (isWorker && options.workerLock) {\n const lockPath = options.workerLock;\n process.once(\"exit\", () => releaseSpawnLock(lockPath));\n }\n\n try {\n await runGenerateTypes(rootDir, outFile, warehouseId, options);\n } finally {\n if (isWorker && options.workerLock) {\n releaseSpawnLock(options.workerLock);\n }\n }\n\n // Only a non-blocking, non-worker invocation spawns. A worker is always\n // --wait (so resolveTypegenMode → \"blocking\"), which both prevents recursion\n // and means we never get here for a worker.\n if (!isWorker && resolveTypegenMode(options) === \"non-blocking\") {\n const resolvedRootDir = rootDir || process.cwd();\n const lockPath = getSpawnLockPath(resolvedRootDir);\n\n if (acquireSpawnLock(lockPath)) {\n spawnTypegenWorker(lockPath, { rootDir, outFile, warehouseId });\n } else {\n console.log(\"Type refresh already in progress, skipping.\");\n }\n }\n}\n\nexport const generateTypesCommand = new Command(\"generate-types\")\n .description(\"Generate TypeScript types from SQL queries\")\n .argument(\"[rootDir]\", \"Root directory of the project\", process.cwd())\n .argument(\n \"[outFile]\",\n \"Output file path\",\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\"),\n )\n .argument(\"[warehouseId]\", \"Databricks warehouse ID\")\n .option(\"--no-cache\", \"Disable caching for type generation\")\n .option(\n \"--wait\",\n \"Wait for warehouse readiness instead of degrading (use for CI)\",\n )\n // Internal: marks the detached background worker and carries the lock it must\n // release. Hidden from --help; users should never pass it.\n .addOption(\n new Option(\n \"--worker-lock <path>\",\n \"Internal: detached worker lock path\",\n ).hideHelp(),\n )\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit generate-types\n $ appkit generate-types . shared/appkit-types/analytics.d.ts\n $ appkit generate-types . shared/appkit-types/analytics.d.ts my-warehouse-id\n $ appkit generate-types --no-cache\n $ appkit generate-types --wait # CI: wait for the warehouse and fail on a cold one`,\n )\n .action(generateTypesAction);\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lint.js","names":[],"sources":["../../../src/cli/commands/lint.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Lang, parse, type SgNode } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\n\ninterface BaseRule {\n id: string;\n message: string;\n includeTests?: boolean;\n filter?: (code: string) => boolean;\n}\n\n/**\n * A rule matches via exactly one of `pattern` (an ast-grep pattern for\n * `root.findAll`) or `find` (an escape hatch for checks a single pattern can't\n * express). Discriminated union so a rule with neither or both fails to compile.\n */\ntype Rule =\n | (BaseRule & { pattern: string; find?: never })\n | (BaseRule & { find: (root: SgNode) => SgNode[]; pattern?: never });\n\n/** Value node of an object literal's `key: value` pair, or undefined. */\nfunction objectPropertyValue(obj: SgNode, key: string): SgNode | undefined {\n for (const pair of obj.children()) {\n if (pair.kind() !== \"pair\") continue;\n const k = pair.field(\"key\");\n // Strip quotes so `columns` and `\"columns\"` are treated the same.\n if (k && k.text().replace(/^[\"']|[\"']$/g, \"\") === key) {\n return pair.field(\"value\") ?? undefined;\n }\n }\n return undefined;\n}\n\n/** True for `[]` / `[ ]` — an array literal with no elements. */\nfunction isEmptyArrayLiteral(node: SgNode): boolean {\n return node.kind() === \"array\" && node.children().every((c) => !c.isNamed());\n}\n\n/**\n * Flags `aiSearch(...)` configs whose indexes lack usable `columns`. Production\n * does not auto-discover columns (dev-only), so a missing or empty `columns`\n * fails at query time. Passes anything it can't statically prove bad (constant\n * or dynamically-built columns/indexes) to avoid false positives.\n */\nfunction findAiSearchIndexesMissingColumns(root: SgNode): SgNode[] {\n const flagged: SgNode[] = [];\n\n for (const call of root.findAll(\"aiSearch($$$ARGS)\")) {\n const argNodes =\n call\n .field(\"arguments\")\n ?.children()\n .filter((c) => c.isNamed()) ?? [];\n\n // Bare aiSearch(): relies on the env default index (no columns).\n if (argNodes.length === 0) {\n flagged.push(call);\n continue;\n }\n\n // aiSearch(dynamicConfig) — not an object literal, can't inspect. Pass.\n const configObj = argNodes[0].kind() === \"object\" ? argNodes[0] : undefined;\n if (!configObj) continue;\n\n const indexes = objectPropertyValue(configObj, \"indexes\");\n\n // No `indexes` key => falls back to the env default index (no columns).\n if (!indexes) {\n flagged.push(call);\n continue;\n }\n if (indexes.kind() !== \"object\") continue; // dynamic indexes: pass.\n\n const indexPairs = indexes.children().filter((c) => c.kind() === \"pair\");\n\n // Empty `indexes: {}` => no configured index, same as bare.\n if (indexPairs.length === 0) {\n flagged.push(call);\n continue;\n }\n\n for (const pair of indexPairs) {\n const idxObj = pair.field(\"value\");\n if (!idxObj || idxObj.kind() !== \"object\") continue; // dynamic: pass.\n // A spread (`{ ...base }`) may carry `columns` we can't see: pass.\n if (idxObj.children().some((c) => c.kind() === \"spread_element\"))\n continue;\n const columns = objectPropertyValue(idxObj, \"columns\");\n if (!columns || isEmptyArrayLiteral(columns)) {\n flagged.push(pair); // points at the offending index alias.\n }\n }\n }\n\n return flagged;\n}\n\nconst rules: Rule[] = [\n {\n id: \"no-double-type-assertion\",\n pattern: \"$X as unknown as $Y\",\n message:\n \"Avoid double type assertion (as unknown as). Use proper type guards or fix the source type.\",\n },\n {\n id: \"no-as-any\",\n pattern: \"$X as any\",\n message:\n 'Avoid \"as any\" type assertion. Use proper typing or unknown with type guards.',\n includeTests: false, // acceptable in test mocks\n },\n {\n id: \"no-array-index-key\",\n pattern: \"key={$IDX}\",\n message:\n \"Avoid using array index as React key. Use a stable unique identifier.\",\n filter: (code) => /key=\\{(idx|index|i)\\}/.test(code),\n },\n {\n id: \"no-parse-float-without-validation\",\n pattern: \"parseFloat($X).toFixed($Y)\",\n message:\n \"parseFloat can return NaN. Validate input or use toNumber() helper from shared/types.ts.\",\n },\n {\n // <Variants> is the dev-only variant picker from @databricks/appkit-ui,\n // meant to drive the local edit loop. Finalize the chosen variant before\n // deploying so the picker chrome never reaches production.\n id: \"no-variants-in-prod\",\n pattern: \"<Variants $$$P>$$$C</Variants>\",\n message:\n \"<Variants> is a development-only variant picker and must not be shipped. Finalize the chosen <Variant> before deploying.\",\n includeTests: false,\n },\n {\n id: \"ai-search-index-requires-columns\",\n message:\n \"AI Search index has no `columns`; it will fail in production (columns are auto-discovered only in dev). Set `columns` explicitly on each index.\",\n includeTests: false,\n find: findAiSearchIndexesMissingColumns,\n },\n];\n\nfunction isTestFile(filePath: string, rootDir: string): boolean {\n // Relative to scan root: an ancestor `tests` dir must not mark the whole project as tests.\n const rel = path.relative(rootDir, filePath);\n return (\n /\\.(test|spec)\\.(ts|tsx)$/.test(rel) || /(^|[/\\\\])tests[/\\\\]/.test(rel)\n );\n}\n\nfunction findTsFiles(dir: string, files: string[] = []): string[] {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n if ([\"node_modules\", \"dist\", \"build\", \".git\"].includes(entry.name))\n continue;\n findTsFiles(fullPath, files);\n } else if (entry.isFile() && /\\.(ts|tsx)$/.test(entry.name)) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\ninterface Violation {\n file: string;\n line: number;\n column: number;\n rule: string;\n message: string;\n code: string;\n}\n\n/**\n * Runs all applicable rules against a file's source. Exported so tests can lint\n * an in-memory string without touching disk.\n */\nexport function lintSource(\n content: string,\n filePath: string,\n activeRules: Rule[] = rules,\n isTest = false,\n): Violation[] {\n const violations: Violation[] = [];\n const lang = filePath.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const root = parse(lang, content).root();\n\n for (const rule of activeRules) {\n // skip rules that don't apply to test files\n if (isTest && rule.includeTests === false) continue;\n\n const matches = rule.find ? rule.find(root) : root.findAll(rule.pattern);\n\n for (const match of matches) {\n const code = match.text();\n\n if (rule.filter && !rule.filter(code)) continue;\n\n const range = match.range();\n violations.push({\n file: filePath,\n line: range.start.line + 1,\n column: range.start.column + 1,\n rule: rule.id,\n message: rule.message,\n code: code.length > 80 ? `${code.slice(0, 77)}...` : code,\n });\n }\n }\n\n return violations;\n}\n\nfunction lintFile(\n filePath: string,\n activeRules: Rule[],\n rootDir: string,\n): Violation[] {\n const content = fs.readFileSync(filePath, \"utf-8\");\n return lintSource(\n content,\n filePath,\n activeRules,\n isTestFile(filePath, rootDir),\n );\n}\n\n/**\n * Lint command implementation\n */\nfunction runLint() {\n const rootDir = process.cwd();\n const files = findTsFiles(rootDir);\n\n console.log(`Scanning ${files.length} TypeScript files...\\n`);\n\n const allViolations: Violation[] = [];\n\n for (const file of files) {\n const violations = lintFile(file, rules, rootDir);\n allViolations.push(...violations);\n }\n\n if (allViolations.length === 0) {\n console.log(\"No ast-grep lint violations found.\");\n process.exit(0);\n }\n\n console.log(`Found ${allViolations.length} violation(s):\\n`);\n\n for (const v of allViolations) {\n const relPath = path.relative(rootDir, v.file);\n console.log(`${relPath}:${v.line}:${v.column}`);\n console.log(` ${v.rule}: ${v.message}`);\n console.log(` > ${v.code}\\n`);\n }\n\n process.exit(1);\n}\n\nexport const lintCommand = new Command(\"lint\")\n .description(\"Run AST-based linting on TypeScript files\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit lint`,\n )\n .action(runLint);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lint.js","names":[],"sources":["../../../src/cli/commands/lint.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\ninterface BaseRule {\n id: string;\n message: string;\n includeTests?: boolean;\n filter?: (code: string) => boolean;\n}\n\n/**\n * A rule matches via exactly one of `pattern` (an ast-grep pattern for\n * `root.findAll`) or `find` (an escape hatch for checks a single pattern can't\n * express). Discriminated union so a rule with neither or both fails to compile.\n */\ntype Rule =\n | (BaseRule & { pattern: string; find?: never })\n | (BaseRule & { find: (root: SgNode) => SgNode[]; pattern?: never });\n\n/** Value node of an object literal's `key: value` pair, or undefined. */\nfunction objectPropertyValue(obj: SgNode, key: string): SgNode | undefined {\n for (const pair of obj.children()) {\n if (pair.kind() !== \"pair\") continue;\n const k = pair.field(\"key\");\n // Strip quotes so `columns` and `\"columns\"` are treated the same.\n if (k && k.text().replace(/^[\"']|[\"']$/g, \"\") === key) {\n return pair.field(\"value\") ?? undefined;\n }\n }\n return undefined;\n}\n\n/** True for `[]` / `[ ]` — an array literal with no elements. */\nfunction isEmptyArrayLiteral(node: SgNode): boolean {\n return node.kind() === \"array\" && node.children().every((c) => !c.isNamed());\n}\n\n/**\n * Flags `aiSearch(...)` configs whose indexes lack usable `columns`. Production\n * does not auto-discover columns (dev-only), so a missing or empty `columns`\n * fails at query time. Passes anything it can't statically prove bad (constant\n * or dynamically-built columns/indexes) to avoid false positives.\n */\nfunction findAiSearchIndexesMissingColumns(root: SgNode): SgNode[] {\n const flagged: SgNode[] = [];\n\n for (const call of root.findAll(\"aiSearch($$$ARGS)\")) {\n const argNodes =\n call\n .field(\"arguments\")\n ?.children()\n .filter((c) => c.isNamed()) ?? [];\n\n // Bare aiSearch(): relies on the env default index (no columns).\n if (argNodes.length === 0) {\n flagged.push(call);\n continue;\n }\n\n // aiSearch(dynamicConfig) — not an object literal, can't inspect. Pass.\n const configObj = argNodes[0].kind() === \"object\" ? argNodes[0] : undefined;\n if (!configObj) continue;\n\n const indexes = objectPropertyValue(configObj, \"indexes\");\n\n // No `indexes` key => falls back to the env default index (no columns).\n if (!indexes) {\n flagged.push(call);\n continue;\n }\n if (indexes.kind() !== \"object\") continue; // dynamic indexes: pass.\n\n const indexPairs = indexes.children().filter((c) => c.kind() === \"pair\");\n\n // Empty `indexes: {}` => no configured index, same as bare.\n if (indexPairs.length === 0) {\n flagged.push(call);\n continue;\n }\n\n for (const pair of indexPairs) {\n const idxObj = pair.field(\"value\");\n if (!idxObj || idxObj.kind() !== \"object\") continue; // dynamic: pass.\n // A spread (`{ ...base }`) may carry `columns` we can't see: pass.\n if (idxObj.children().some((c) => c.kind() === \"spread_element\"))\n continue;\n const columns = objectPropertyValue(idxObj, \"columns\");\n if (!columns || isEmptyArrayLiteral(columns)) {\n flagged.push(pair); // points at the offending index alias.\n }\n }\n }\n\n return flagged;\n}\n\nconst rules: Rule[] = [\n {\n id: \"no-double-type-assertion\",\n pattern: \"$X as unknown as $Y\",\n message:\n \"Avoid double type assertion (as unknown as). Use proper type guards or fix the source type.\",\n },\n {\n id: \"no-as-any\",\n pattern: \"$X as any\",\n message:\n 'Avoid \"as any\" type assertion. Use proper typing or unknown with type guards.',\n includeTests: false, // acceptable in test mocks\n },\n {\n id: \"no-array-index-key\",\n pattern: \"key={$IDX}\",\n message:\n \"Avoid using array index as React key. Use a stable unique identifier.\",\n filter: (code) => /key=\\{(idx|index|i)\\}/.test(code),\n },\n {\n id: \"no-parse-float-without-validation\",\n pattern: \"parseFloat($X).toFixed($Y)\",\n message:\n \"parseFloat can return NaN. Validate input or use toNumber() helper from shared/types.ts.\",\n },\n {\n // <Variants> is the dev-only variant picker from @databricks/appkit-ui,\n // meant to drive the local edit loop. Finalize the chosen variant before\n // deploying so the picker chrome never reaches production.\n id: \"no-variants-in-prod\",\n pattern: \"<Variants $$$P>$$$C</Variants>\",\n message:\n \"<Variants> is a development-only variant picker and must not be shipped. Finalize the chosen <Variant> before deploying.\",\n includeTests: false,\n },\n {\n id: \"ai-search-index-requires-columns\",\n message:\n \"AI Search index has no `columns`; it will fail in production (columns are auto-discovered only in dev). Set `columns` explicitly on each index.\",\n includeTests: false,\n find: findAiSearchIndexesMissingColumns,\n },\n];\n\nfunction isTestFile(filePath: string, rootDir: string): boolean {\n // Relative to scan root: an ancestor `tests` dir must not mark the whole project as tests.\n const rel = path.relative(rootDir, filePath);\n return (\n /\\.(test|spec)\\.(ts|tsx)$/.test(rel) || /(^|[/\\\\])tests[/\\\\]/.test(rel)\n );\n}\n\nfunction findTsFiles(dir: string, files: string[] = []): string[] {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n if ([\"node_modules\", \"dist\", \"build\", \".git\"].includes(entry.name))\n continue;\n findTsFiles(fullPath, files);\n } else if (entry.isFile() && /\\.(ts|tsx)$/.test(entry.name)) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\ninterface Violation {\n file: string;\n line: number;\n column: number;\n rule: string;\n message: string;\n code: string;\n}\n\n/**\n * Runs all applicable rules against a file's source. Exported so tests can lint\n * an in-memory string without touching disk.\n */\nexport function lintSource(\n content: string,\n filePath: string,\n activeRules: Rule[] = rules,\n isTest = false,\n): Violation[] {\n const violations: Violation[] = [];\n const lang = filePath.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const root = parse(lang, content).root();\n\n for (const rule of activeRules) {\n // skip rules that don't apply to test files\n if (isTest && rule.includeTests === false) continue;\n\n const matches = rule.find ? rule.find(root) : root.findAll(rule.pattern);\n\n for (const match of matches) {\n const code = match.text();\n\n if (rule.filter && !rule.filter(code)) continue;\n\n const range = match.range();\n violations.push({\n file: filePath,\n line: range.start.line + 1,\n column: range.start.column + 1,\n rule: rule.id,\n message: rule.message,\n code: code.length > 80 ? `${code.slice(0, 77)}...` : code,\n });\n }\n }\n\n return violations;\n}\n\nfunction lintFile(\n filePath: string,\n activeRules: Rule[],\n rootDir: string,\n): Violation[] {\n const content = fs.readFileSync(filePath, \"utf-8\");\n return lintSource(\n content,\n filePath,\n activeRules,\n isTestFile(filePath, rootDir),\n );\n}\n\n/**\n * Lint command implementation\n */\nfunction runLint() {\n const rootDir = process.cwd();\n const files = findTsFiles(rootDir);\n\n console.log(`Scanning ${files.length} TypeScript files...\\n`);\n\n const allViolations: Violation[] = [];\n\n for (const file of files) {\n const violations = lintFile(file, rules, rootDir);\n allViolations.push(...violations);\n }\n\n if (allViolations.length === 0) {\n console.log(\"No ast-grep lint violations found.\");\n process.exit(0);\n }\n\n console.log(`Found ${allViolations.length} violation(s):\\n`);\n\n for (const v of allViolations) {\n const relPath = path.relative(rootDir, v.file);\n console.log(`${relPath}:${v.line}:${v.column}`);\n console.log(` ${v.rule}: ${v.message}`);\n console.log(` > ${v.code}\\n`);\n }\n\n process.exit(1);\n}\n\nexport const lintCommand = new Command(\"lint\")\n .description(\"Run AST-based linting on TypeScript files\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit lint`,\n )\n .action(runLint);\n"],"mappings":"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"}
|