@databricks/appkit-ui 0.61.0 → 0.62.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/NOTICE.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 +4 -6
- 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/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.map +1 -1
- 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-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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env-reconcile.js","names":[],"sources":["../../../../src/cli/commands/registry/env-reconcile.ts"],"sourcesContent":["import dotenv from \"dotenv\";\nimport {\n fieldOrigin,\n isValidEnvName,\n type RequirementField,\n type ResourceRequirementRow,\n} from \"./requirements\";\n\n/**\n * A single env var that an installed plugin needs in the local `.env`.\n * `platform`-origin fields are excluded upstream — they are injected by\n * Databricks Apps at deploy time and never belong in a hand-managed `.env`.\n */\nexport interface EnvNeed {\n env: string;\n resourceType: string;\n required: boolean;\n /** static-origin default value, pre-filled without prompting. */\n defaultValue?: string;\n origin?: string;\n description?: string;\n}\n\n/** The resolved decision for one env var after reconciliation. */\nexport interface EnvResolution {\n env: string;\n /** The value to write, or undefined when skipped / left unset. */\n value?: string;\n status: \"written\" | \"already-set\" | \"skipped\";\n}\n\n/**\n * A `.env` value is a single line: `KEY=VALUE`. A value carrying a CR/LF would\n * write extra lines when serialized, so a malicious static default like\n * `value: \"y\\nDATABRICKS_HOST=attacker\"` could inject an unrelated key (host\n * override → credential exfil). Registry manifests are untrusted, so any value\n * with a line break is rejected rather than written.\n */\nexport function isSafeEnvValue(value: string): boolean {\n return !/[\\r\\n]/.test(value);\n}\n\n/**\n * Flattens requirement rows into the env vars that belong in local `.env`.\n * Excludes fields with no `env` name and `platform`-origin fields (deploy-time\n * platform injection). Order: required resources first (as given), then optional.\n */\nexport function collectEnvNeeds(rows: ResourceRequirementRow[]): EnvNeed[] {\n const needs: EnvNeed[] = [];\n const seen = new Set<string>();\n const ordered = [\n ...rows.filter((r) => r.required),\n ...rows.filter((r) => !r.required),\n ];\n for (const row of ordered) {\n for (const field of row.fields) {\n if (!includeInEnv(field)) continue;\n const env = field.env as string;\n if (seen.has(env)) continue;\n seen.add(env);\n needs.push({\n env,\n resourceType: row.type,\n required: row.required,\n defaultValue: field.value,\n origin: fieldOrigin(field),\n description: field.description,\n });\n }\n }\n return needs;\n}\n\n/** A field belongs in `.env` iff it names a valid env var and isn't platform-injected. */\nfunction includeInEnv(field: RequirementField): boolean {\n if (!field.env) return false;\n // The env name comes from an untrusted manifest and is written as `NAME=value`;\n // a malformed name (e.g. one containing a newline) could inject an extra .env\n // line, so drop anything that isn't a plain env identifier.\n if (!isValidEnvName(field.env)) return false;\n // Origin is derived from the authored contract (localOnly/value/resolve) so\n // registry-fetched manifests without a computed origin classify correctly.\n return fieldOrigin(field) !== \"platform\";\n}\n\n/**\n * Parses a `.env` file body into a KEY -> value map, using the same `dotenv`\n * parser the app loads `.env` with at runtime so the CLI reads it identically.\n */\nexport function parseEnv(content: string): Record<string, string> {\n return dotenv.parse(content);\n}\n\n/**\n * Serializes new env entries for appending to a `.env` file. Only keys not\n * already present are emitted; existing keys are never rewritten (we don't\n * clobber user edits). Returns the text to append (empty if nothing new).\n */\nexport function serializeEnvAppend(\n entries: Array<{ env: string; value: string; comment?: string }>,\n): string {\n if (entries.length === 0) return \"\";\n const lines: string[] = [];\n for (const e of entries) {\n if (e.comment) lines.push(`# ${e.comment}`);\n lines.push(`${e.env}=${e.value}`);\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n\n/** Provides a value for an env need, or undefined to skip it. */\nexport type ValueProvider = (need: EnvNeed) => Promise<string | undefined>;\n\nexport interface ReconcileOptions {\n /** Existing parsed `.env` values (keys already present are left untouched). */\n existing: Record<string, string>;\n /** Resolves a value for each unset need (prompt in interactive, flag in CI). */\n provide: ValueProvider;\n}\n\n/**\n * Reconciles the needed env vars against what's already in `.env`.\n * - Already-set keys are reported as \"already-set\" and never overwritten.\n * - static-origin defaults are used without invoking `provide`.\n * - Everything else defers to `provide`; a returned undefined means skip.\n */\nexport async function reconcileEnv(\n needs: EnvNeed[],\n opts: ReconcileOptions,\n): Promise<EnvResolution[]> {\n const resolutions: EnvResolution[] = [];\n for (const need of needs) {\n const current = opts.existing[need.env];\n if (current !== undefined && current !== \"\") {\n // Carry the existing value so callers can still feed it into deploy\n // config (databricks.yml target variables) — the var is set in .env but\n // its bundle binding still needs the value assigned.\n resolutions.push({\n env: need.env,\n value: current,\n status: \"already-set\",\n });\n continue;\n }\n if (need.defaultValue !== undefined) {\n // Static default from an untrusted manifest — refuse a value that would\n // inject extra `.env` lines rather than silently writing it.\n if (!isSafeEnvValue(need.defaultValue)) {\n resolutions.push({ env: need.env, status: \"skipped\" });\n continue;\n }\n resolutions.push({\n env: need.env,\n value: need.defaultValue,\n status: \"written\",\n });\n continue;\n }\n const value = await opts.provide(need);\n if (value === undefined || value === \"\" || !isSafeEnvValue(value)) {\n resolutions.push({ env: need.env, status: \"skipped\" });\n } else {\n resolutions.push({ env: need.env, value, status: \"written\" });\n }\n }\n return resolutions;\n}\n"],"mappings":";;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"env-reconcile.js","names":[],"sources":["../../../../src/cli/commands/registry/env-reconcile.ts"],"sourcesContent":["import dotenv from \"dotenv\";\n\nimport {\n fieldOrigin,\n isValidEnvName,\n type RequirementField,\n type ResourceRequirementRow,\n} from \"./requirements\";\n\n/**\n * A single env var that an installed plugin needs in the local `.env`.\n * `platform`-origin fields are excluded upstream — they are injected by\n * Databricks Apps at deploy time and never belong in a hand-managed `.env`.\n */\nexport interface EnvNeed {\n env: string;\n resourceType: string;\n required: boolean;\n /** static-origin default value, pre-filled without prompting. */\n defaultValue?: string;\n origin?: string;\n description?: string;\n}\n\n/** The resolved decision for one env var after reconciliation. */\nexport interface EnvResolution {\n env: string;\n /** The value to write, or undefined when skipped / left unset. */\n value?: string;\n status: \"written\" | \"already-set\" | \"skipped\";\n}\n\n/**\n * A `.env` value is a single line: `KEY=VALUE`. A value carrying a CR/LF would\n * write extra lines when serialized, so a malicious static default like\n * `value: \"y\\nDATABRICKS_HOST=attacker\"` could inject an unrelated key (host\n * override → credential exfil). Registry manifests are untrusted, so any value\n * with a line break is rejected rather than written.\n */\nexport function isSafeEnvValue(value: string): boolean {\n return !/[\\r\\n]/.test(value);\n}\n\n/**\n * Flattens requirement rows into the env vars that belong in local `.env`.\n * Excludes fields with no `env` name and `platform`-origin fields (deploy-time\n * platform injection). Order: required resources first (as given), then optional.\n */\nexport function collectEnvNeeds(rows: ResourceRequirementRow[]): EnvNeed[] {\n const needs: EnvNeed[] = [];\n const seen = new Set<string>();\n const ordered = [\n ...rows.filter((r) => r.required),\n ...rows.filter((r) => !r.required),\n ];\n for (const row of ordered) {\n for (const field of row.fields) {\n if (!includeInEnv(field)) continue;\n const env = field.env as string;\n if (seen.has(env)) continue;\n seen.add(env);\n needs.push({\n env,\n resourceType: row.type,\n required: row.required,\n defaultValue: field.value,\n origin: fieldOrigin(field),\n description: field.description,\n });\n }\n }\n return needs;\n}\n\n/** A field belongs in `.env` iff it names a valid env var and isn't platform-injected. */\nfunction includeInEnv(field: RequirementField): boolean {\n if (!field.env) return false;\n // The env name comes from an untrusted manifest and is written as `NAME=value`;\n // a malformed name (e.g. one containing a newline) could inject an extra .env\n // line, so drop anything that isn't a plain env identifier.\n if (!isValidEnvName(field.env)) return false;\n // Origin is derived from the authored contract (localOnly/value/resolve) so\n // registry-fetched manifests without a computed origin classify correctly.\n return fieldOrigin(field) !== \"platform\";\n}\n\n/**\n * Parses a `.env` file body into a KEY -> value map, using the same `dotenv`\n * parser the app loads `.env` with at runtime so the CLI reads it identically.\n */\nexport function parseEnv(content: string): Record<string, string> {\n return dotenv.parse(content);\n}\n\n/**\n * Serializes new env entries for appending to a `.env` file. Only keys not\n * already present are emitted; existing keys are never rewritten (we don't\n * clobber user edits). Returns the text to append (empty if nothing new).\n */\nexport function serializeEnvAppend(\n entries: Array<{ env: string; value: string; comment?: string }>,\n): string {\n if (entries.length === 0) return \"\";\n const lines: string[] = [];\n for (const e of entries) {\n if (e.comment) lines.push(`# ${e.comment}`);\n lines.push(`${e.env}=${e.value}`);\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n\n/** Provides a value for an env need, or undefined to skip it. */\nexport type ValueProvider = (need: EnvNeed) => Promise<string | undefined>;\n\nexport interface ReconcileOptions {\n /** Existing parsed `.env` values (keys already present are left untouched). */\n existing: Record<string, string>;\n /** Resolves a value for each unset need (prompt in interactive, flag in CI). */\n provide: ValueProvider;\n}\n\n/**\n * Reconciles the needed env vars against what's already in `.env`.\n * - Already-set keys are reported as \"already-set\" and never overwritten.\n * - static-origin defaults are used without invoking `provide`.\n * - Everything else defers to `provide`; a returned undefined means skip.\n */\nexport async function reconcileEnv(\n needs: EnvNeed[],\n opts: ReconcileOptions,\n): Promise<EnvResolution[]> {\n const resolutions: EnvResolution[] = [];\n for (const need of needs) {\n const current = opts.existing[need.env];\n if (current !== undefined && current !== \"\") {\n // Carry the existing value so callers can still feed it into deploy\n // config (databricks.yml target variables) — the var is set in .env but\n // its bundle binding still needs the value assigned.\n resolutions.push({\n env: need.env,\n value: current,\n status: \"already-set\",\n });\n continue;\n }\n if (need.defaultValue !== undefined) {\n // Static default from an untrusted manifest — refuse a value that would\n // inject extra `.env` lines rather than silently writing it.\n if (!isSafeEnvValue(need.defaultValue)) {\n resolutions.push({ env: need.env, status: \"skipped\" });\n continue;\n }\n resolutions.push({\n env: need.env,\n value: need.defaultValue,\n status: \"written\",\n });\n continue;\n }\n const value = await opts.provide(need);\n if (value === undefined || value === \"\" || !isSafeEnvValue(value)) {\n resolutions.push({ env: need.env, status: \"skipped\" });\n } else {\n resolutions.push({ env: need.env, value, status: \"written\" });\n }\n }\n return resolutions;\n}\n"],"mappings":";;;;;;;;;;;AAuCA,SAAgB,eAAe,OAAwB;AACrD,QAAO,CAAC,SAAS,KAAK,MAAM;;;;;;;AAQ9B,SAAgB,gBAAgB,MAA2C;CACzE,MAAM,QAAmB,EAAE;CAC3B,MAAM,uBAAO,IAAI,KAAa;CAC9B,MAAM,UAAU,CACd,GAAG,KAAK,QAAQ,MAAM,EAAE,SAAS,EACjC,GAAG,KAAK,QAAQ,MAAM,CAAC,EAAE,SAAS,CACnC;AACD,MAAK,MAAM,OAAO,QAChB,MAAK,MAAM,SAAS,IAAI,QAAQ;AAC9B,MAAI,CAAC,aAAa,MAAM,CAAE;EAC1B,MAAM,MAAM,MAAM;AAClB,MAAI,KAAK,IAAI,IAAI,CAAE;AACnB,OAAK,IAAI,IAAI;AACb,QAAM,KAAK;GACT;GACA,cAAc,IAAI;GAClB,UAAU,IAAI;GACd,cAAc,MAAM;GACpB,QAAQ,YAAY,MAAM;GAC1B,aAAa,MAAM;GACpB,CAAC;;AAGN,QAAO;;;AAIT,SAAS,aAAa,OAAkC;AACtD,KAAI,CAAC,MAAM,IAAK,QAAO;AAIvB,KAAI,CAAC,eAAe,MAAM,IAAI,CAAE,QAAO;AAGvC,QAAO,YAAY,MAAM,KAAK;;;;;;AAOhC,SAAgB,SAAS,SAAyC;AAChE,QAAO,OAAO,MAAM,QAAQ;;;;;;;AAQ9B,SAAgB,mBACd,SACQ;AACR,KAAI,QAAQ,WAAW,EAAG,QAAO;CACjC,MAAM,QAAkB,EAAE;AAC1B,MAAK,MAAM,KAAK,SAAS;AACvB,MAAI,EAAE,QAAS,OAAM,KAAK,KAAK,EAAE,UAAU;AAC3C,QAAM,KAAK,GAAG,EAAE,IAAI,GAAG,EAAE,QAAQ;;AAEnC,QAAO,GAAG,MAAM,KAAK,KAAK,CAAC;;;;;;;;AAmB7B,eAAsB,aACpB,OACA,MAC0B;CAC1B,MAAM,cAA+B,EAAE;AACvC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,KAAK,SAAS,KAAK;AACnC,MAAI,YAAY,UAAa,YAAY,IAAI;AAI3C,eAAY,KAAK;IACf,KAAK,KAAK;IACV,OAAO;IACP,QAAQ;IACT,CAAC;AACF;;AAEF,MAAI,KAAK,iBAAiB,QAAW;AAGnC,OAAI,CAAC,eAAe,KAAK,aAAa,EAAE;AACtC,gBAAY,KAAK;KAAE,KAAK,KAAK;KAAK,QAAQ;KAAW,CAAC;AACtD;;AAEF,eAAY,KAAK;IACf,KAAK,KAAK;IACV,OAAO,KAAK;IACZ,QAAQ;IACT,CAAC;AACF;;EAEF,MAAM,QAAQ,MAAM,KAAK,QAAQ,KAAK;AACtC,MAAI,UAAU,UAAa,UAAU,MAAM,CAAC,eAAe,MAAM,CAC/D,aAAY,KAAK;GAAE,KAAK,KAAK;GAAK,QAAQ;GAAW,CAAC;MAEtD,aAAY,KAAK;GAAE,KAAK,KAAK;GAAK;GAAO,QAAQ;GAAW,CAAC;;AAGjE,QAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env-writer.js","names":[],"sources":["../../../../src/cli/commands/registry/env-writer.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { autocomplete, isCancel, select, text } from \"@clack/prompts\";\nimport pc from \"picocolors\";\nimport type { BindingValueNeed } from \"./config-plan\";\nimport {\n collectEnvNeeds,\n type EnvNeed,\n type EnvResolution,\n parseEnv,\n reconcileEnv,\n serializeEnvAppend,\n type ValueProvider,\n} from \"./env-reconcile\";\nimport type { ResourceRequirementRow } from \"./requirements\";\nimport {\n composeResourceId,\n isFlatListable,\n isParentContext,\n listParentContextStep,\n listWorkspaceResources,\n parentContextDepth,\n} from \"./workspace-picker\";\n\nexport interface EnvSyncOptions {\n /** Directory holding `.env` / `.env.example` (the app root). */\n cwd: string;\n /** true = never prompt (agent/CI). Uses flag values or leaves unset. */\n nonInteractive: boolean;\n /** Pre-supplied env values from flags, e.g. { DATABRICKS_WAREHOUSE_ID: \"abc\" }. */\n values?: Record<string, string>;\n /** Databricks profile for the workspace picker (else the CLI default). */\n profile?: string;\n}\n\n/** Sentinel select value meaning \"let me type the id myself\". */\nconst MANUAL = \"__manual__\";\n\n/**\n * Max resources shown in a picker select. Real workspaces can have thousands\n * (e.g. 5000+ SQL warehouses); an unbounded select is unusable. Beyond this we\n * show the first N and log how many were hidden — never silently drop — and the\n * \"Enter manually\" option always lets the user type an id the list omits.\n */\nconst PICKER_LIMIT = 25;\n\n/** Reads a `.env`-style file into a map; empty when the file is absent. */\nfunction readEnvFile(file: string): Record<string, string> {\n if (!fs.existsSync(file)) return {};\n return parseEnv(fs.readFileSync(file, \"utf-8\"));\n}\n\n/** Appends text to a file, creating it (with a trailing newline) if needed. */\nfunction appendToFile(file: string, text: string): void {\n if (text === \"\") return;\n if (fs.existsSync(file)) {\n const existing = fs.readFileSync(file, \"utf-8\");\n const sep = existing.length > 0 && !existing.endsWith(\"\\n\") ? \"\\n\" : \"\";\n fs.writeFileSync(file, existing + sep + text);\n } else {\n fs.writeFileSync(file, text);\n }\n}\n\n/**\n * Caps a choice list to {@link PICKER_LIMIT} for display, logging how many were\n * hidden so the truncation is never silent. The caller always appends an\n * \"Enter manually\" option, so an omitted resource is still reachable.\n */\nexport function capChoices<T>(\n choices: T[],\n resourceType: string,\n limit = PICKER_LIMIT,\n): T[] {\n if (choices.length <= limit) return choices;\n console.log(\n pc.dim(\n ` ${choices.length} ${resourceType}s found; showing first ${limit}. ` +\n 'Use \"Enter manually\" if yours is not listed.',\n ),\n );\n return choices.slice(0, limit);\n}\n\n/** Free-text prompt for one env need; undefined to skip. */\nasync function promptText(need: EnvNeed): Promise<string | undefined> {\n const tag = need.required ? \"required\" : \"optional\";\n const answer = await text({\n message: `${need.env} (${need.resourceType}, ${tag})`,\n placeholder: need.description ?? \"leave blank to skip\",\n });\n if (isCancel(answer)) return undefined;\n const value = (answer ?? \"\").trim();\n return value === \"\" ? undefined : value;\n}\n\n/** Presents one workspace list as a select; MANUAL/cancel handled by caller. */\nasync function selectFrom(\n message: string,\n choices: { value: string; label: string }[],\n): Promise<string | typeof MANUAL | null> {\n const picked = await select({\n message,\n options: [...choices, { value: MANUAL, label: \"Enter manually / skip\" }],\n });\n if (isCancel(picked)) return null;\n return String(picked) as string | typeof MANUAL;\n}\n\n/**\n * Type-to-filter picker over the full choice list (no cap): the user searches\n * by name/id as they type. Appends \"Enter manually\" so an omitted value is\n * still reachable. Returns MANUAL to fall through to free-text, or null on\n * cancel.\n */\nasync function autocompleteFrom(\n message: string,\n choices: { value: string; label: string }[],\n): Promise<string | typeof MANUAL | null> {\n const picked = await autocomplete({\n message,\n options: [...choices, { value: MANUAL, label: \"Enter manually / skip\" }],\n placeholder: \"type to search…\",\n });\n if (isCancel(picked)) return null;\n return String(picked) as string | typeof MANUAL;\n}\n\n/**\n * Drill-down picker for parent-context types (volume→catalog/schema,\n * secret→scope, vector_search_index→endpoint). Walks each step, listing the\n * next level from the prior pick. Returns the final resource id, or undefined\n * to fall back to free-text (on cancel, empty level, or MANUAL at any step).\n */\nasync function pickParentContext(\n need: EnvNeed,\n profile: string | undefined,\n): Promise<string | undefined> {\n const depth = parentContextDepth(need.resourceType);\n const picks: string[] = [];\n for (let i = 0; i < depth; i++) {\n const step = listParentContextStep(need.resourceType, i, picks, profile);\n if (!step || step.choices.length === 0) {\n console.log(\n pc.dim(\n ` No ${step?.key ?? need.resourceType} found — enter the id manually.`,\n ),\n );\n return undefined;\n }\n const picked = await selectFrom(\n `${need.env} — pick a ${step.key}`,\n capChoices(step.choices, step.key),\n );\n if (picked === null || picked === MANUAL) return undefined;\n picks.push(picked);\n }\n // Compose the id from the picks: most types end on a self-qualified id, but a\n // secret needs both scope and key (scope/key).\n return composeResourceId(need.resourceType, picks);\n}\n\n/**\n * Builds the value provider. Precedence: --env flag, then (interactive only) a\n * workspace picker — flat select for flat-listable types, drill-down for\n * parent-context types — else a free-text prompt. The picker degrades to\n * free-text whenever the workspace can't be listed (no profile, offline, auth\n * error, empty) so it never hard-fails.\n */\nfunction makeProvider(opts: EnvSyncOptions): ValueProvider {\n return async (need: EnvNeed) => {\n const fromFlag = opts.values?.[need.env];\n if (fromFlag !== undefined) return fromFlag;\n if (opts.nonInteractive) return undefined;\n\n if (isFlatListable(need.resourceType)) {\n const { choices, truncated, error } = await listWorkspaceResources(\n need.resourceType,\n opts.profile,\n );\n if (choices.length > 0) {\n if (truncated) {\n console.log(\n pc.dim(\n ` Showing the first ${choices.length} ${need.resourceType}s; use \"Enter manually\" if yours isn't listed.`,\n ),\n );\n }\n const picked = await autocompleteFrom(\n `${need.env} — search ${need.resourceType}s`,\n choices,\n );\n if (picked === null) return undefined;\n if (picked !== MANUAL) return picked;\n // fall through to free-text\n } else if (error) {\n // Listing failed (usually auth/profile) — say so, don't pretend the\n // workspace is empty, and point at the fix.\n console.log(\n pc.yellow(\n ` Couldn't list ${need.resourceType}s from the workspace (${error}).`,\n ),\n );\n console.log(\n pc.dim(\n \" Enter an id manually, or re-run with --profile <name> (or set DATABRICKS_CONFIG_PROFILE) so the picker can reach the workspace.\",\n ),\n );\n } else {\n console.log(\n pc.dim(\n ` No ${need.resourceType} found in the workspace — enter an id manually.`,\n ),\n );\n }\n } else if (isParentContext(need.resourceType)) {\n const picked = await pickParentContext(need, opts.profile);\n if (picked !== undefined) return picked;\n // fall through to free-text\n }\n\n return promptText(need);\n };\n}\n\n/**\n * Reconciles a plugin's declared resource env vars into the app's local `.env`\n * (and mirrors variable names into `.env.example`). Never overwrites keys the\n * user already set; skips platform-injected fields. Returns the per-var\n * resolutions so callers can report what happened.\n */\nexport async function syncEnv(\n rows: ResourceRequirementRow[],\n opts: EnvSyncOptions,\n): Promise<EnvResolution[]> {\n const needs = collectEnvNeeds(rows);\n if (needs.length === 0) return [];\n\n const envPath = path.join(opts.cwd, \".env\");\n const examplePath = path.join(opts.cwd, \".env.example\");\n const existing = readEnvFile(envPath);\n\n const resolutions = await reconcileEnv(needs, {\n existing,\n provide: makeProvider(opts),\n });\n\n const written = resolutions.filter(\n (r): r is EnvResolution & { value: string } =>\n r.status === \"written\" && r.value !== undefined,\n );\n appendToFile(\n envPath,\n serializeEnvAppend(written.map((r) => ({ env: r.env, value: r.value }))),\n );\n\n // .env.example carries the variable names (no secret values), and only for\n // vars not already documented there.\n const exampleExisting = readEnvFile(examplePath);\n const newExampleKeys = needs.filter((n) => !(n.env in exampleExisting));\n appendToFile(\n examplePath,\n serializeEnvAppend(newExampleKeys.map((n) => ({ env: n.env, value: \"\" }))),\n );\n\n return resolutions;\n}\n\n/**\n * Prompts for the {@link BindingValueNeed}s that `.env` reconciliation can't\n * collect, returning a `fieldKey -> value` map for buildConfigPlan. Values are\n * not written to `.env` (these fields have no env var). Non-interactive mode\n * uses `values[fieldKey]` if provided, else leaves the field unset.\n */\nexport async function collectBindingValues(\n needs: BindingValueNeed[],\n opts: EnvSyncOptions,\n): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n for (const need of needs) {\n const fromFlag = opts.values?.[need.fieldKey];\n if (fromFlag !== undefined) {\n out[need.fieldKey] = fromFlag;\n continue;\n }\n if (opts.nonInteractive) continue;\n const answer = await text({\n message: `${need.fieldKey} (${need.resourceType}) — required for databricks.yml`,\n placeholder: need.description ?? \"leave blank to set before deploy\",\n });\n if (isCancel(answer)) continue;\n const value = (answer ?? \"\").trim();\n if (value !== \"\") out[need.fieldKey] = value;\n }\n return out;\n}\n\n/** Prints a concise summary of what env reconciliation did. */\nexport function reportEnvResolutions(resolutions: EnvResolution[]): void {\n if (resolutions.length === 0) return;\n const written = resolutions.filter((r) => r.status === \"written\");\n const already = resolutions.filter((r) => r.status === \"already-set\");\n const skipped = resolutions.filter((r) => r.status === \"skipped\");\n\n if (written.length > 0) {\n console.log(\n `${pc.green(\"Wrote to .env:\")} ${written.map((r) => r.env).join(\", \")}`,\n );\n }\n if (already.length > 0) {\n console.log(pc.dim(`Already set: ${already.map((r) => r.env).join(\", \")}`));\n }\n if (skipped.length > 0) {\n console.log(\n `${pc.yellow(\"Left unset (set before deploy):\")} ${skipped\n .map((r) => r.env)\n .join(\", \")}`,\n );\n }\n}\n"],"mappings":";;;;;;;;;AAoCA,MAAM,SAAS;;;;;;;AAQf,MAAM,eAAe;;AAGrB,SAAS,YAAY,MAAsC;AACzD,KAAI,CAAC,GAAG,WAAW,KAAK,CAAE,QAAO,EAAE;AACnC,QAAO,SAAS,GAAG,aAAa,MAAM,QAAQ,CAAC;;;AAIjD,SAAS,aAAa,MAAc,MAAoB;AACtD,KAAI,SAAS,GAAI;AACjB,KAAI,GAAG,WAAW,KAAK,EAAE;EACvB,MAAM,WAAW,GAAG,aAAa,MAAM,QAAQ;EAC/C,MAAM,MAAM,SAAS,SAAS,KAAK,CAAC,SAAS,SAAS,KAAK,GAAG,OAAO;AACrE,KAAG,cAAc,MAAM,WAAW,MAAM,KAAK;OAE7C,IAAG,cAAc,MAAM,KAAK;;;;;;;AAShC,SAAgB,WACd,SACA,cACA,QAAQ,cACH;AACL,KAAI,QAAQ,UAAU,MAAO,QAAO;AACpC,SAAQ,IACN,GAAG,IACD,KAAK,QAAQ,OAAO,GAAG,aAAa,yBAAyB,MAAM,gDAEpE,CACF;AACD,QAAO,QAAQ,MAAM,GAAG,MAAM;;;AAIhC,eAAe,WAAW,MAA4C;CACpE,MAAM,MAAM,KAAK,WAAW,aAAa;CACzC,MAAM,SAAS,MAAM,KAAK;EACxB,SAAS,GAAG,KAAK,IAAI,IAAI,KAAK,aAAa,IAAI,IAAI;EACnD,aAAa,KAAK,eAAe;EAClC,CAAC;AACF,KAAI,SAAS,OAAO,CAAE,QAAO;CAC7B,MAAM,SAAS,UAAU,IAAI,MAAM;AACnC,QAAO,UAAU,KAAK,SAAY;;;AAIpC,eAAe,WACb,SACA,SACwC;CACxC,MAAM,SAAS,MAAM,OAAO;EAC1B;EACA,SAAS,CAAC,GAAG,SAAS;GAAE,OAAO;GAAQ,OAAO;GAAyB,CAAC;EACzE,CAAC;AACF,KAAI,SAAS,OAAO,CAAE,QAAO;AAC7B,QAAO,OAAO,OAAO;;;;;;;;AASvB,eAAe,iBACb,SACA,SACwC;CACxC,MAAM,SAAS,MAAM,aAAa;EAChC;EACA,SAAS,CAAC,GAAG,SAAS;GAAE,OAAO;GAAQ,OAAO;GAAyB,CAAC;EACxE,aAAa;EACd,CAAC;AACF,KAAI,SAAS,OAAO,CAAE,QAAO;AAC7B,QAAO,OAAO,OAAO;;;;;;;;AASvB,eAAe,kBACb,MACA,SAC6B;CAC7B,MAAM,QAAQ,mBAAmB,KAAK,aAAa;CACnD,MAAM,QAAkB,EAAE;AAC1B,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK;EAC9B,MAAM,OAAO,sBAAsB,KAAK,cAAc,GAAG,OAAO,QAAQ;AACxE,MAAI,CAAC,QAAQ,KAAK,QAAQ,WAAW,GAAG;AACtC,WAAQ,IACN,GAAG,IACD,QAAQ,MAAM,OAAO,KAAK,aAAa,iCACxC,CACF;AACD;;EAEF,MAAM,SAAS,MAAM,WACnB,GAAG,KAAK,IAAI,YAAY,KAAK,OAC7B,WAAW,KAAK,SAAS,KAAK,IAAI,CACnC;AACD,MAAI,WAAW,QAAQ,WAAW,OAAQ,QAAO;AACjD,QAAM,KAAK,OAAO;;AAIpB,QAAO,kBAAkB,KAAK,cAAc,MAAM;;;;;;;;;AAUpD,SAAS,aAAa,MAAqC;AACzD,QAAO,OAAO,SAAkB;EAC9B,MAAM,WAAW,KAAK,SAAS,KAAK;AACpC,MAAI,aAAa,OAAW,QAAO;AACnC,MAAI,KAAK,eAAgB,QAAO;AAEhC,MAAI,eAAe,KAAK,aAAa,EAAE;GACrC,MAAM,EAAE,SAAS,WAAW,UAAU,MAAM,uBAC1C,KAAK,cACL,KAAK,QACN;AACD,OAAI,QAAQ,SAAS,GAAG;AACtB,QAAI,UACF,SAAQ,IACN,GAAG,IACD,uBAAuB,QAAQ,OAAO,GAAG,KAAK,aAAa,gDAC5D,CACF;IAEH,MAAM,SAAS,MAAM,iBACnB,GAAG,KAAK,IAAI,YAAY,KAAK,aAAa,IAC1C,QACD;AACD,QAAI,WAAW,KAAM,QAAO;AAC5B,QAAI,WAAW,OAAQ,QAAO;cAErB,OAAO;AAGhB,YAAQ,IACN,GAAG,OACD,mBAAmB,KAAK,aAAa,wBAAwB,MAAM,IACpE,CACF;AACD,YAAQ,IACN,GAAG,IACD,oIACD,CACF;SAED,SAAQ,IACN,GAAG,IACD,QAAQ,KAAK,aAAa,iDAC3B,CACF;aAEM,gBAAgB,KAAK,aAAa,EAAE;GAC7C,MAAM,SAAS,MAAM,kBAAkB,MAAM,KAAK,QAAQ;AAC1D,OAAI,WAAW,OAAW,QAAO;;AAInC,SAAO,WAAW,KAAK;;;;;;;;;AAU3B,eAAsB,QACpB,MACA,MAC0B;CAC1B,MAAM,QAAQ,gBAAgB,KAAK;AACnC,KAAI,MAAM,WAAW,EAAG,QAAO,EAAE;CAEjC,MAAM,UAAU,KAAK,KAAK,KAAK,KAAK,OAAO;CAC3C,MAAM,cAAc,KAAK,KAAK,KAAK,KAAK,eAAe;CAGvD,MAAM,cAAc,MAAM,aAAa,OAAO;EAC5C,UAHe,YAAY,QAAQ;EAInC,SAAS,aAAa,KAAK;EAC5B,CAAC;AAMF,cACE,SACA,mBANc,YAAY,QACzB,MACC,EAAE,WAAW,aAAa,EAAE,UAAU,OACzC,CAG4B,KAAK,OAAO;EAAE,KAAK,EAAE;EAAK,OAAO,EAAE;EAAO,EAAE,CAAC,CACzE;CAID,MAAM,kBAAkB,YAAY,YAAY;AAEhD,cACE,aACA,mBAHqB,MAAM,QAAQ,MAAM,EAAE,EAAE,OAAO,iBAAiB,CAGnC,KAAK,OAAO;EAAE,KAAK,EAAE;EAAK,OAAO;EAAI,EAAE,CAAC,CAC3E;AAED,QAAO;;;;;;;;AAST,eAAsB,qBACpB,OACA,MACiC;CACjC,MAAM,MAA8B,EAAE;AACtC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,WAAW,KAAK,SAAS,KAAK;AACpC,MAAI,aAAa,QAAW;AAC1B,OAAI,KAAK,YAAY;AACrB;;AAEF,MAAI,KAAK,eAAgB;EACzB,MAAM,SAAS,MAAM,KAAK;GACxB,SAAS,GAAG,KAAK,SAAS,IAAI,KAAK,aAAa;GAChD,aAAa,KAAK,eAAe;GAClC,CAAC;AACF,MAAI,SAAS,OAAO,CAAE;EACtB,MAAM,SAAS,UAAU,IAAI,MAAM;AACnC,MAAI,UAAU,GAAI,KAAI,KAAK,YAAY;;AAEzC,QAAO;;;AAIT,SAAgB,qBAAqB,aAAoC;AACvE,KAAI,YAAY,WAAW,EAAG;CAC9B,MAAM,UAAU,YAAY,QAAQ,MAAM,EAAE,WAAW,UAAU;CACjE,MAAM,UAAU,YAAY,QAAQ,MAAM,EAAE,WAAW,cAAc;CACrE,MAAM,UAAU,YAAY,QAAQ,MAAM,EAAE,WAAW,UAAU;AAEjE,KAAI,QAAQ,SAAS,EACnB,SAAQ,IACN,GAAG,GAAG,MAAM,iBAAiB,CAAC,GAAG,QAAQ,KAAK,MAAM,EAAE,IAAI,CAAC,KAAK,KAAK,GACtE;AAEH,KAAI,QAAQ,SAAS,EACnB,SAAQ,IAAI,GAAG,IAAI,gBAAgB,QAAQ,KAAK,MAAM,EAAE,IAAI,CAAC,KAAK,KAAK,GAAG,CAAC;AAE7E,KAAI,QAAQ,SAAS,EACnB,SAAQ,IACN,GAAG,GAAG,OAAO,kCAAkC,CAAC,GAAG,QAChD,KAAK,MAAM,EAAE,IAAI,CACjB,KAAK,KAAK,GACd"}
|
|
1
|
+
{"version":3,"file":"env-writer.js","names":[],"sources":["../../../../src/cli/commands/registry/env-writer.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { autocomplete, isCancel, select, text } from \"@clack/prompts\";\nimport pc from \"picocolors\";\n\nimport type { BindingValueNeed } from \"./config-plan\";\nimport {\n collectEnvNeeds,\n type EnvNeed,\n type EnvResolution,\n parseEnv,\n reconcileEnv,\n serializeEnvAppend,\n type ValueProvider,\n} from \"./env-reconcile\";\nimport type { ResourceRequirementRow } from \"./requirements\";\nimport {\n composeResourceId,\n isFlatListable,\n isParentContext,\n listParentContextStep,\n listWorkspaceResources,\n parentContextDepth,\n} from \"./workspace-picker\";\n\nexport interface EnvSyncOptions {\n /** Directory holding `.env` / `.env.example` (the app root). */\n cwd: string;\n /** true = never prompt (agent/CI). Uses flag values or leaves unset. */\n nonInteractive: boolean;\n /** Pre-supplied env values from flags, e.g. { DATABRICKS_WAREHOUSE_ID: \"abc\" }. */\n values?: Record<string, string>;\n /** Databricks profile for the workspace picker (else the CLI default). */\n profile?: string;\n}\n\n/** Sentinel select value meaning \"let me type the id myself\". */\nconst MANUAL = \"__manual__\";\n\n/**\n * Max resources shown in a picker select. Real workspaces can have thousands\n * (e.g. 5000+ SQL warehouses); an unbounded select is unusable. Beyond this we\n * show the first N and log how many were hidden — never silently drop — and the\n * \"Enter manually\" option always lets the user type an id the list omits.\n */\nconst PICKER_LIMIT = 25;\n\n/** Reads a `.env`-style file into a map; empty when the file is absent. */\nfunction readEnvFile(file: string): Record<string, string> {\n if (!fs.existsSync(file)) return {};\n return parseEnv(fs.readFileSync(file, \"utf-8\"));\n}\n\n/** Appends text to a file, creating it (with a trailing newline) if needed. */\nfunction appendToFile(file: string, text: string): void {\n if (text === \"\") return;\n if (fs.existsSync(file)) {\n const existing = fs.readFileSync(file, \"utf-8\");\n const sep = existing.length > 0 && !existing.endsWith(\"\\n\") ? \"\\n\" : \"\";\n fs.writeFileSync(file, existing + sep + text);\n } else {\n fs.writeFileSync(file, text);\n }\n}\n\n/**\n * Caps a choice list to {@link PICKER_LIMIT} for display, logging how many were\n * hidden so the truncation is never silent. The caller always appends an\n * \"Enter manually\" option, so an omitted resource is still reachable.\n */\nexport function capChoices<T>(\n choices: T[],\n resourceType: string,\n limit = PICKER_LIMIT,\n): T[] {\n if (choices.length <= limit) return choices;\n console.log(\n pc.dim(\n ` ${choices.length} ${resourceType}s found; showing first ${limit}. ` +\n 'Use \"Enter manually\" if yours is not listed.',\n ),\n );\n return choices.slice(0, limit);\n}\n\n/** Free-text prompt for one env need; undefined to skip. */\nasync function promptText(need: EnvNeed): Promise<string | undefined> {\n const tag = need.required ? \"required\" : \"optional\";\n const answer = await text({\n message: `${need.env} (${need.resourceType}, ${tag})`,\n placeholder: need.description ?? \"leave blank to skip\",\n });\n if (isCancel(answer)) return undefined;\n const value = (answer ?? \"\").trim();\n return value === \"\" ? undefined : value;\n}\n\n/** Presents one workspace list as a select; MANUAL/cancel handled by caller. */\nasync function selectFrom(\n message: string,\n choices: { value: string; label: string }[],\n): Promise<string | typeof MANUAL | null> {\n const picked = await select({\n message,\n options: [...choices, { value: MANUAL, label: \"Enter manually / skip\" }],\n });\n if (isCancel(picked)) return null;\n return String(picked) as string | typeof MANUAL;\n}\n\n/**\n * Type-to-filter picker over the full choice list (no cap): the user searches\n * by name/id as they type. Appends \"Enter manually\" so an omitted value is\n * still reachable. Returns MANUAL to fall through to free-text, or null on\n * cancel.\n */\nasync function autocompleteFrom(\n message: string,\n choices: { value: string; label: string }[],\n): Promise<string | typeof MANUAL | null> {\n const picked = await autocomplete({\n message,\n options: [...choices, { value: MANUAL, label: \"Enter manually / skip\" }],\n placeholder: \"type to search…\",\n });\n if (isCancel(picked)) return null;\n return String(picked) as string | typeof MANUAL;\n}\n\n/**\n * Drill-down picker for parent-context types (volume→catalog/schema,\n * secret→scope, vector_search_index→endpoint). Walks each step, listing the\n * next level from the prior pick. Returns the final resource id, or undefined\n * to fall back to free-text (on cancel, empty level, or MANUAL at any step).\n */\nasync function pickParentContext(\n need: EnvNeed,\n profile: string | undefined,\n): Promise<string | undefined> {\n const depth = parentContextDepth(need.resourceType);\n const picks: string[] = [];\n for (let i = 0; i < depth; i++) {\n const step = listParentContextStep(need.resourceType, i, picks, profile);\n if (!step || step.choices.length === 0) {\n console.log(\n pc.dim(\n ` No ${step?.key ?? need.resourceType} found — enter the id manually.`,\n ),\n );\n return undefined;\n }\n const picked = await selectFrom(\n `${need.env} — pick a ${step.key}`,\n capChoices(step.choices, step.key),\n );\n if (picked === null || picked === MANUAL) return undefined;\n picks.push(picked);\n }\n // Compose the id from the picks: most types end on a self-qualified id, but a\n // secret needs both scope and key (scope/key).\n return composeResourceId(need.resourceType, picks);\n}\n\n/**\n * Builds the value provider. Precedence: --env flag, then (interactive only) a\n * workspace picker — flat select for flat-listable types, drill-down for\n * parent-context types — else a free-text prompt. The picker degrades to\n * free-text whenever the workspace can't be listed (no profile, offline, auth\n * error, empty) so it never hard-fails.\n */\nfunction makeProvider(opts: EnvSyncOptions): ValueProvider {\n return async (need: EnvNeed) => {\n const fromFlag = opts.values?.[need.env];\n if (fromFlag !== undefined) return fromFlag;\n if (opts.nonInteractive) return undefined;\n\n if (isFlatListable(need.resourceType)) {\n const { choices, truncated, error } = await listWorkspaceResources(\n need.resourceType,\n opts.profile,\n );\n if (choices.length > 0) {\n if (truncated) {\n console.log(\n pc.dim(\n ` Showing the first ${choices.length} ${need.resourceType}s; use \"Enter manually\" if yours isn't listed.`,\n ),\n );\n }\n const picked = await autocompleteFrom(\n `${need.env} — search ${need.resourceType}s`,\n choices,\n );\n if (picked === null) return undefined;\n if (picked !== MANUAL) return picked;\n // fall through to free-text\n } else if (error) {\n // Listing failed (usually auth/profile) — say so, don't pretend the\n // workspace is empty, and point at the fix.\n console.log(\n pc.yellow(\n ` Couldn't list ${need.resourceType}s from the workspace (${error}).`,\n ),\n );\n console.log(\n pc.dim(\n \" Enter an id manually, or re-run with --profile <name> (or set DATABRICKS_CONFIG_PROFILE) so the picker can reach the workspace.\",\n ),\n );\n } else {\n console.log(\n pc.dim(\n ` No ${need.resourceType} found in the workspace — enter an id manually.`,\n ),\n );\n }\n } else if (isParentContext(need.resourceType)) {\n const picked = await pickParentContext(need, opts.profile);\n if (picked !== undefined) return picked;\n // fall through to free-text\n }\n\n return promptText(need);\n };\n}\n\n/**\n * Reconciles a plugin's declared resource env vars into the app's local `.env`\n * (and mirrors variable names into `.env.example`). Never overwrites keys the\n * user already set; skips platform-injected fields. Returns the per-var\n * resolutions so callers can report what happened.\n */\nexport async function syncEnv(\n rows: ResourceRequirementRow[],\n opts: EnvSyncOptions,\n): Promise<EnvResolution[]> {\n const needs = collectEnvNeeds(rows);\n if (needs.length === 0) return [];\n\n const envPath = path.join(opts.cwd, \".env\");\n const examplePath = path.join(opts.cwd, \".env.example\");\n const existing = readEnvFile(envPath);\n\n const resolutions = await reconcileEnv(needs, {\n existing,\n provide: makeProvider(opts),\n });\n\n const written = resolutions.filter(\n (r): r is EnvResolution & { value: string } =>\n r.status === \"written\" && r.value !== undefined,\n );\n appendToFile(\n envPath,\n serializeEnvAppend(written.map((r) => ({ env: r.env, value: r.value }))),\n );\n\n // .env.example carries the variable names (no secret values), and only for\n // vars not already documented there.\n const exampleExisting = readEnvFile(examplePath);\n const newExampleKeys = needs.filter((n) => !(n.env in exampleExisting));\n appendToFile(\n examplePath,\n serializeEnvAppend(newExampleKeys.map((n) => ({ env: n.env, value: \"\" }))),\n );\n\n return resolutions;\n}\n\n/**\n * Prompts for the {@link BindingValueNeed}s that `.env` reconciliation can't\n * collect, returning a `fieldKey -> value` map for buildConfigPlan. Values are\n * not written to `.env` (these fields have no env var). Non-interactive mode\n * uses `values[fieldKey]` if provided, else leaves the field unset.\n */\nexport async function collectBindingValues(\n needs: BindingValueNeed[],\n opts: EnvSyncOptions,\n): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n for (const need of needs) {\n const fromFlag = opts.values?.[need.fieldKey];\n if (fromFlag !== undefined) {\n out[need.fieldKey] = fromFlag;\n continue;\n }\n if (opts.nonInteractive) continue;\n const answer = await text({\n message: `${need.fieldKey} (${need.resourceType}) — required for databricks.yml`,\n placeholder: need.description ?? \"leave blank to set before deploy\",\n });\n if (isCancel(answer)) continue;\n const value = (answer ?? \"\").trim();\n if (value !== \"\") out[need.fieldKey] = value;\n }\n return out;\n}\n\n/** Prints a concise summary of what env reconciliation did. */\nexport function reportEnvResolutions(resolutions: EnvResolution[]): void {\n if (resolutions.length === 0) return;\n const written = resolutions.filter((r) => r.status === \"written\");\n const already = resolutions.filter((r) => r.status === \"already-set\");\n const skipped = resolutions.filter((r) => r.status === \"skipped\");\n\n if (written.length > 0) {\n console.log(\n `${pc.green(\"Wrote to .env:\")} ${written.map((r) => r.env).join(\", \")}`,\n );\n }\n if (already.length > 0) {\n console.log(pc.dim(`Already set: ${already.map((r) => r.env).join(\", \")}`));\n }\n if (skipped.length > 0) {\n console.log(\n `${pc.yellow(\"Left unset (set before deploy):\")} ${skipped\n .map((r) => r.env)\n .join(\", \")}`,\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/cli/commands/registry/index.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { registryInfoCommand } from \"./info\";\nimport { registryListCommand, registrySearchCommand } from \"./list\";\n\n/**\n * Parent command for AppKit component registry operations.\n * Subcommands:\n * - list: Enumerate items available in the registry\n * - search: Find items by name, description, type, or keyword\n * - info: Show an item's resource requirements and dependencies\n *\n * Note: `appkit add <item>` is exposed as a top-level command (see add.ts)\n * since it is the primary entry point for consumers.\n */\nexport const registryCommand = new Command(\"registry\")\n .description(\"AppKit component registry commands\")\n .addCommand(registryListCommand)\n .addCommand(registrySearchCommand)\n .addCommand(registryInfoCommand)\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit registry list\n $ appkit registry search kpi dashboard\n $ appkit registry info analytics\n $ appkit add metric-card`,\n );\n"],"mappings":";;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/cli/commands/registry/index.ts"],"sourcesContent":["import { Command } from \"commander\";\n\nimport { registryInfoCommand } from \"./info\";\nimport { registryListCommand, registrySearchCommand } from \"./list\";\n\n/**\n * Parent command for AppKit component registry operations.\n * Subcommands:\n * - list: Enumerate items available in the registry\n * - search: Find items by name, description, type, or keyword\n * - info: Show an item's resource requirements and dependencies\n *\n * Note: `appkit add <item>` is exposed as a top-level command (see add.ts)\n * since it is the primary entry point for consumers.\n */\nexport const registryCommand = new Command(\"registry\")\n .description(\"AppKit component registry commands\")\n .addCommand(registryListCommand)\n .addCommand(registrySearchCommand)\n .addCommand(registryInfoCommand)\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit registry list\n $ appkit registry search kpi dashboard\n $ appkit registry info analytics\n $ appkit add metric-card`,\n );\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAa,kBAAkB,IAAI,QAAQ,WAAW,CACnD,YAAY,qCAAqC,CACjD,WAAW,oBAAoB,CAC/B,WAAW,sBAAsB,CACjC,WAAW,oBAAoB,CAC/B,YACC,SACA;;;;;4BAMD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"info.js","names":[],"sources":["../../../../src/cli/commands/registry/info.ts"],"sourcesContent":["import process from \"node:process\";\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport { fetchRegistryItem, isValidItemName, stripNamespace } from \"./client\";\nimport { resolveToken } from \"./constants\";\nimport { extractRequirements, renderRequirements } from \"./requirements\";\n\nasync function runInfo(ref: string, opts: { json?: boolean }): Promise<void> {\n const token = resolveToken();\n // Validate before fetching: the name is interpolated into the fetch path, so\n // reject non-slug refs (matches the `add` guard) rather than let `/` or `..`\n // redirect the request to another path in the repo.\n const name = stripNamespace(ref);\n if (!isValidItemName(name)) {\n console.error(`Invalid registry item name: ${JSON.stringify(ref)}`);\n process.exit(1);\n }\n const item = await fetchRegistryItem(name, token);\n const rows = extractRequirements(item);\n\n if (opts.json) {\n console.log(\n JSON.stringify(\n {\n name: item.name,\n type: item.type,\n dependencies: item.dependencies ?? [],\n registryDependencies: item.registryDependencies ?? [],\n resources: rows,\n },\n null,\n 2,\n ),\n );\n return;\n }\n\n console.log(pc.bold(item.name));\n const deps = item.dependencies ?? [];\n const registryDeps = item.registryDependencies ?? [];\n if (deps.length > 0) {\n console.log(pc.dim(` npm dependencies: ${deps.join(\", \")}`));\n }\n if (registryDeps.length > 0) {\n console.log(pc.dim(` registry dependencies: ${registryDeps.join(\", \")}`));\n }\n console.log(`\\n${renderRequirements(item, rows)}`);\n}\n\nexport const registryInfoCommand = new Command(\"info\")\n .description(\"Show an item's resource requirements and dependencies\")\n .argument(\"<item>\", \"Registry item name, e.g. analytics\")\n .option(\"--json\", \"Output as JSON\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit registry info analytics\n $ appkit registry info @databricks-appkit/analytics --json`,\n )\n .action((ref: string, opts: { json?: boolean }) =>\n runInfo(ref, opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"info.js","names":[],"sources":["../../../../src/cli/commands/registry/info.ts"],"sourcesContent":["import process from \"node:process\";\n\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\n\nimport { fetchRegistryItem, isValidItemName, stripNamespace } from \"./client\";\nimport { resolveToken } from \"./constants\";\nimport { extractRequirements, renderRequirements } from \"./requirements\";\n\nasync function runInfo(ref: string, opts: { json?: boolean }): Promise<void> {\n const token = resolveToken();\n // Validate before fetching: the name is interpolated into the fetch path, so\n // reject non-slug refs (matches the `add` guard) rather than let `/` or `..`\n // redirect the request to another path in the repo.\n const name = stripNamespace(ref);\n if (!isValidItemName(name)) {\n console.error(`Invalid registry item name: ${JSON.stringify(ref)}`);\n process.exit(1);\n }\n const item = await fetchRegistryItem(name, token);\n const rows = extractRequirements(item);\n\n if (opts.json) {\n console.log(\n JSON.stringify(\n {\n name: item.name,\n type: item.type,\n dependencies: item.dependencies ?? [],\n registryDependencies: item.registryDependencies ?? [],\n resources: rows,\n },\n null,\n 2,\n ),\n );\n return;\n }\n\n console.log(pc.bold(item.name));\n const deps = item.dependencies ?? [];\n const registryDeps = item.registryDependencies ?? [];\n if (deps.length > 0) {\n console.log(pc.dim(` npm dependencies: ${deps.join(\", \")}`));\n }\n if (registryDeps.length > 0) {\n console.log(pc.dim(` registry dependencies: ${registryDeps.join(\", \")}`));\n }\n console.log(`\\n${renderRequirements(item, rows)}`);\n}\n\nexport const registryInfoCommand = new Command(\"info\")\n .description(\"Show an item's resource requirements and dependencies\")\n .argument(\"<item>\", \"Registry item name, e.g. analytics\")\n .option(\"--json\", \"Output as JSON\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit registry info analytics\n $ appkit registry info @databricks-appkit/analytics --json`,\n )\n .action((ref: string, opts: { json?: boolean }) =>\n runInfo(ref, opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":";;;;;;;;AASA,eAAe,QAAQ,KAAa,MAAyC;CAC3E,MAAM,QAAQ,cAAc;CAI5B,MAAM,OAAO,eAAe,IAAI;AAChC,KAAI,CAAC,gBAAgB,KAAK,EAAE;AAC1B,UAAQ,MAAM,+BAA+B,KAAK,UAAU,IAAI,GAAG;AACnE,UAAQ,KAAK,EAAE;;CAEjB,MAAM,OAAO,MAAM,kBAAkB,MAAM,MAAM;CACjD,MAAM,OAAO,oBAAoB,KAAK;AAEtC,KAAI,KAAK,MAAM;AACb,UAAQ,IACN,KAAK,UACH;GACE,MAAM,KAAK;GACX,MAAM,KAAK;GACX,cAAc,KAAK,gBAAgB,EAAE;GACrC,sBAAsB,KAAK,wBAAwB,EAAE;GACrD,WAAW;GACZ,EACD,MACA,EACD,CACF;AACD;;AAGF,SAAQ,IAAI,GAAG,KAAK,KAAK,KAAK,CAAC;CAC/B,MAAM,OAAO,KAAK,gBAAgB,EAAE;CACpC,MAAM,eAAe,KAAK,wBAAwB,EAAE;AACpD,KAAI,KAAK,SAAS,EAChB,SAAQ,IAAI,GAAG,IAAI,uBAAuB,KAAK,KAAK,KAAK,GAAG,CAAC;AAE/D,KAAI,aAAa,SAAS,EACxB,SAAQ,IAAI,GAAG,IAAI,4BAA4B,aAAa,KAAK,KAAK,GAAG,CAAC;AAE5E,SAAQ,IAAI,KAAK,mBAAmB,MAAM,KAAK,GAAG;;AAGpD,MAAa,sBAAsB,IAAI,QAAQ,OAAO,CACnD,YAAY,wDAAwD,CACpE,SAAS,UAAU,qCAAqC,CACxD,OAAO,UAAU,iBAAiB,CAClC,YACC,SACA;;;8DAID,CACA,QAAQ,KAAa,SACpB,QAAQ,KAAK,KAAK,CAAC,OAAO,QAAQ;AAChC,SAAQ,MAAM,IAAI;AAClB,SAAQ,KAAK,EAAE;EACf,CACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list.js","names":[],"sources":["../../../../src/cli/commands/registry/list.ts"],"sourcesContent":["import process from \"node:process\";\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport { registryAuthHeaders } from \"./client\";\nimport {\n REGISTRY_INDEX_API_URL,\n REGISTRY_INDEX_URL,\n REGISTRY_REPO,\n resolveToken,\n} from \"./constants\";\n\ninterface RegistryIndexItem {\n name: string;\n type?: string;\n title?: string;\n description?: string;\n categories?: string[];\n meta?: { verified?: boolean };\n files?: Array<{ path?: string; target?: string }>;\n}\n\nfunction isVerified(item: RegistryIndexItem): boolean {\n return item.meta?.verified === true;\n}\n\n/** Free-text haystack for search matching. */\nfunction searchHaystack(item: RegistryIndexItem): string {\n return [\n item.name,\n item.title ?? \"\",\n item.description ?? \"\",\n itemKind(item),\n ...(item.categories ?? []),\n ]\n .join(\" \")\n .toLowerCase();\n}\n\n/** True if every whitespace-separated term in `query` appears in the item. */\nfunction matchesQuery(item: RegistryIndexItem, query: string): boolean {\n const haystack = searchHaystack(item);\n return query\n .toLowerCase()\n .split(/\\s+/)\n .filter(Boolean)\n .every((term) => haystack.includes(term));\n}\n\n/** A friendly kind for the TYPE column: plugin, component, hook, theme, … */\nfunction itemKind(item: RegistryIndexItem): string {\n const hasManifest = (item.files ?? []).some(\n (f) =>\n f.target?.endsWith(\"manifest.json\") || f.path?.endsWith(\"manifest.json\"),\n );\n if (hasManifest) return \"plugin\";\n switch (item.type) {\n case \"registry:component\":\n case \"registry:block\":\n return \"component\";\n case \"registry:hook\":\n return \"hook\";\n case \"registry:lib\":\n return \"lib\";\n case \"registry:theme\":\n return \"theme\";\n case \"registry:ui\":\n return \"ui\";\n case \"registry:page\":\n return \"page\";\n default:\n return item.type?.replace(/^registry:/, \"\") || \"item\";\n }\n}\n\nconst KIND_COLOR: Record<string, (s: string) => string> = {\n plugin: pc.magenta,\n component: pc.blue,\n hook: pc.cyan,\n theme: pc.yellow,\n lib: pc.green,\n};\n\nfunction printTable(items: RegistryIndexItem[]): void {\n if (items.length === 0) {\n console.log(pc.dim(\"No items found in the registry.\"));\n return;\n }\n const kinds = items.map(itemKind);\n const maxName = Math.max(4, ...items.map((i) => i.name.length));\n const maxKind = Math.max(4, ...kinds.map((k) => k.length));\n const verifiedCol = \"VERIFIED\";\n // Pad plain text before coloring so ANSI codes don't break alignment.\n const header = `${\"NAME\".padEnd(maxName)} ${\"TYPE\".padEnd(maxKind)} ${verifiedCol} DESCRIPTION`;\n console.log(pc.bold(header));\n console.log(pc.dim(\"─\".repeat(header.length)));\n for (const [i, item] of items.entries()) {\n const verified = isVerified(item);\n const kind = kinds[i];\n const colorKind = KIND_COLOR[kind] ?? pc.white;\n const name = pc.cyan(item.name.padEnd(maxName));\n const kindCell = colorKind(kind.padEnd(maxKind));\n const mark = verified\n ? pc.green(\"✓\".padEnd(verifiedCol.length))\n : \" \".repeat(verifiedCol.length);\n const desc = item.description ?? item.title ?? \"\";\n console.log(\n `${name} ${kindCell} ${mark} ${verified ? desc : pc.dim(desc)}`,\n );\n }\n}\n\n/** Fetches the registry index (token-aware), or exits with a helpful message. */\nasync function fetchIndex(): Promise<RegistryIndexItem[]> {\n const token = resolveToken();\n const url = token ? REGISTRY_INDEX_API_URL : REGISTRY_INDEX_URL;\n\n let res: Awaited<ReturnType<typeof fetch>>;\n try {\n res = await fetch(url, { headers: registryAuthHeaders(token) });\n } catch (err) {\n console.error(pc.red(`Failed to reach the registry at ${url}`));\n console.error(` ${err instanceof Error ? err.message : String(err)}`);\n process.exit(1);\n }\n if (res.status === 404 || res.status === 401 || res.status === 403) {\n console.error(\n pc.red(\n `Could not read the registry index from ${REGISTRY_REPO} (HTTP ${res.status}).`,\n ),\n );\n if (!token) {\n console.error(\n \" If the repo is private, set APPKIT_REGISTRY_TOKEN (or GITHUB_TOKEN) to a token with read access.\",\n );\n }\n process.exit(1);\n }\n if (!res.ok) {\n console.error(pc.red(`Registry returned HTTP ${res.status} for ${url}`));\n process.exit(1);\n }\n\n const data = (await res.json()) as { items?: RegistryIndexItem[] };\n return data.items ?? [];\n}\n\nfunction output(items: RegistryIndexItem[], opts: { json?: boolean }): void {\n if (opts.json) {\n // Surface `kind` + `verified` as top-level fields for easy scripting.\n console.log(\n JSON.stringify(\n items.map((i) => ({\n ...i,\n kind: itemKind(i),\n verified: isVerified(i),\n })),\n null,\n 2,\n ),\n );\n } else {\n printTable(items);\n }\n}\n\nasync function runList(opts: {\n json?: boolean;\n verified?: boolean;\n}): Promise<void> {\n let items = await fetchIndex();\n if (opts.verified) items = items.filter(isVerified);\n output(items, opts);\n}\n\nasync function runSearch(\n query: string,\n opts: { json?: boolean; verified?: boolean },\n): Promise<void> {\n let items = await fetchIndex();\n items = items.filter((i) => matchesQuery(i, query));\n if (opts.verified) items = items.filter(isVerified);\n if (items.length === 0 && !opts.json) {\n console.log(pc.dim(`No items match \"${query}\".`));\n return;\n }\n output(items, opts);\n}\n\nexport const registryListCommand = new Command(\"list\")\n .description(\"List items available in the AppKit registry\")\n .option(\"--json\", \"Output as JSON\")\n .option(\"--verified\", \"Show only verified items\")\n .action((opts: { json?: boolean; verified?: boolean }) =>\n runList(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n\nexport const registrySearchCommand = new Command(\"search\")\n .description(\"Search registry items by name, description, type, or keyword\")\n .argument(\"<query...>\", \"Search terms (all must match)\")\n .option(\"--json\", \"Output as JSON\")\n .option(\"--verified\", \"Show only verified items\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit registry search chart\n $ appkit registry search kpi dashboard\n $ appkit registry search plugin --json`,\n )\n .action((query: string[], opts: { json?: boolean; verified?: boolean }) =>\n runSearch(query.join(\" \"), opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"list.js","names":[],"sources":["../../../../src/cli/commands/registry/list.ts"],"sourcesContent":["import process from \"node:process\";\n\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\n\nimport { registryAuthHeaders } from \"./client\";\nimport {\n REGISTRY_INDEX_API_URL,\n REGISTRY_INDEX_URL,\n REGISTRY_REPO,\n resolveToken,\n} from \"./constants\";\n\ninterface RegistryIndexItem {\n name: string;\n type?: string;\n title?: string;\n description?: string;\n categories?: string[];\n meta?: { verified?: boolean };\n files?: Array<{ path?: string; target?: string }>;\n}\n\nfunction isVerified(item: RegistryIndexItem): boolean {\n return item.meta?.verified === true;\n}\n\n/** Free-text haystack for search matching. */\nfunction searchHaystack(item: RegistryIndexItem): string {\n return [\n item.name,\n item.title ?? \"\",\n item.description ?? \"\",\n itemKind(item),\n ...(item.categories ?? []),\n ]\n .join(\" \")\n .toLowerCase();\n}\n\n/** True if every whitespace-separated term in `query` appears in the item. */\nfunction matchesQuery(item: RegistryIndexItem, query: string): boolean {\n const haystack = searchHaystack(item);\n return query\n .toLowerCase()\n .split(/\\s+/)\n .filter(Boolean)\n .every((term) => haystack.includes(term));\n}\n\n/** A friendly kind for the TYPE column: plugin, component, hook, theme, … */\nfunction itemKind(item: RegistryIndexItem): string {\n const hasManifest = (item.files ?? []).some(\n (f) =>\n f.target?.endsWith(\"manifest.json\") || f.path?.endsWith(\"manifest.json\"),\n );\n if (hasManifest) return \"plugin\";\n switch (item.type) {\n case \"registry:component\":\n case \"registry:block\":\n return \"component\";\n case \"registry:hook\":\n return \"hook\";\n case \"registry:lib\":\n return \"lib\";\n case \"registry:theme\":\n return \"theme\";\n case \"registry:ui\":\n return \"ui\";\n case \"registry:page\":\n return \"page\";\n default:\n return item.type?.replace(/^registry:/, \"\") || \"item\";\n }\n}\n\nconst KIND_COLOR: Record<string, (s: string) => string> = {\n plugin: pc.magenta,\n component: pc.blue,\n hook: pc.cyan,\n theme: pc.yellow,\n lib: pc.green,\n};\n\nfunction printTable(items: RegistryIndexItem[]): void {\n if (items.length === 0) {\n console.log(pc.dim(\"No items found in the registry.\"));\n return;\n }\n const kinds = items.map(itemKind);\n const maxName = Math.max(4, ...items.map((i) => i.name.length));\n const maxKind = Math.max(4, ...kinds.map((k) => k.length));\n const verifiedCol = \"VERIFIED\";\n // Pad plain text before coloring so ANSI codes don't break alignment.\n const header = `${\"NAME\".padEnd(maxName)} ${\"TYPE\".padEnd(maxKind)} ${verifiedCol} DESCRIPTION`;\n console.log(pc.bold(header));\n console.log(pc.dim(\"─\".repeat(header.length)));\n for (const [i, item] of items.entries()) {\n const verified = isVerified(item);\n const kind = kinds[i];\n const colorKind = KIND_COLOR[kind] ?? pc.white;\n const name = pc.cyan(item.name.padEnd(maxName));\n const kindCell = colorKind(kind.padEnd(maxKind));\n const mark = verified\n ? pc.green(\"✓\".padEnd(verifiedCol.length))\n : \" \".repeat(verifiedCol.length);\n const desc = item.description ?? item.title ?? \"\";\n console.log(\n `${name} ${kindCell} ${mark} ${verified ? desc : pc.dim(desc)}`,\n );\n }\n}\n\n/** Fetches the registry index (token-aware), or exits with a helpful message. */\nasync function fetchIndex(): Promise<RegistryIndexItem[]> {\n const token = resolveToken();\n const url = token ? REGISTRY_INDEX_API_URL : REGISTRY_INDEX_URL;\n\n let res: Awaited<ReturnType<typeof fetch>>;\n try {\n res = await fetch(url, { headers: registryAuthHeaders(token) });\n } catch (err) {\n console.error(pc.red(`Failed to reach the registry at ${url}`));\n console.error(` ${err instanceof Error ? err.message : String(err)}`);\n process.exit(1);\n }\n if (res.status === 404 || res.status === 401 || res.status === 403) {\n console.error(\n pc.red(\n `Could not read the registry index from ${REGISTRY_REPO} (HTTP ${res.status}).`,\n ),\n );\n if (!token) {\n console.error(\n \" If the repo is private, set APPKIT_REGISTRY_TOKEN (or GITHUB_TOKEN) to a token with read access.\",\n );\n }\n process.exit(1);\n }\n if (!res.ok) {\n console.error(pc.red(`Registry returned HTTP ${res.status} for ${url}`));\n process.exit(1);\n }\n\n const data = (await res.json()) as { items?: RegistryIndexItem[] };\n return data.items ?? [];\n}\n\nfunction output(items: RegistryIndexItem[], opts: { json?: boolean }): void {\n if (opts.json) {\n // Surface `kind` + `verified` as top-level fields for easy scripting.\n console.log(\n JSON.stringify(\n items.map((i) => ({\n ...i,\n kind: itemKind(i),\n verified: isVerified(i),\n })),\n null,\n 2,\n ),\n );\n } else {\n printTable(items);\n }\n}\n\nasync function runList(opts: {\n json?: boolean;\n verified?: boolean;\n}): Promise<void> {\n let items = await fetchIndex();\n if (opts.verified) items = items.filter(isVerified);\n output(items, opts);\n}\n\nasync function runSearch(\n query: string,\n opts: { json?: boolean; verified?: boolean },\n): Promise<void> {\n let items = await fetchIndex();\n items = items.filter((i) => matchesQuery(i, query));\n if (opts.verified) items = items.filter(isVerified);\n if (items.length === 0 && !opts.json) {\n console.log(pc.dim(`No items match \"${query}\".`));\n return;\n }\n output(items, opts);\n}\n\nexport const registryListCommand = new Command(\"list\")\n .description(\"List items available in the AppKit registry\")\n .option(\"--json\", \"Output as JSON\")\n .option(\"--verified\", \"Show only verified items\")\n .action((opts: { json?: boolean; verified?: boolean }) =>\n runList(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n\nexport const registrySearchCommand = new Command(\"search\")\n .description(\"Search registry items by name, description, type, or keyword\")\n .argument(\"<query...>\", \"Search terms (all must match)\")\n .option(\"--json\", \"Output as JSON\")\n .option(\"--verified\", \"Show only verified items\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit registry search chart\n $ appkit registry search kpi dashboard\n $ appkit registry search plugin --json`,\n )\n .action((query: string[], opts: { json?: boolean; verified?: boolean }) =>\n runSearch(query.join(\" \"), opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"requirements.js","names":[],"sources":["../../../../src/cli/commands/registry/requirements.ts"],"sourcesContent":["import path from \"node:path\";\nimport pc from \"picocolors\";\nimport { computeOriginFromField } from \"../../../schemas/manifest\";\nimport type { RegistryItem } from \"./client\";\n\n/**\n * A single resource field as declared in a plugin manifest. `origin` is the\n * computed classifier written by `plugin sync` (platform/static/cli/user) that\n * says how the value reaches the running app.\n */\nexport interface RequirementField {\n key: string;\n env?: string;\n /**\n * Computed classifier written by `plugin sync` — only present in a synced\n * manifest. Authored manifests omit it, so consumers must derive the\n * effective origin via {@link fieldOrigin} rather than reading this directly.\n */\n origin?: string;\n description?: string;\n /** Default literal value (static origin); pre-filled without prompting. */\n value?: string;\n /** Resolver key (cli origin), e.g. \"postgres:host\". */\n resolve?: string;\n /** Local-dev-only field; platform-injected at deploy time. */\n localOnly?: boolean;\n}\n\n/** A resource requirement flattened for display. */\nexport interface ResourceRequirementRow {\n type: string;\n resourceKey?: string;\n permission?: string;\n required: boolean;\n description?: string;\n fields: RequirementField[];\n}\n\ninterface ManifestFieldShape {\n env?: string;\n origin?: string;\n description?: string;\n value?: string;\n resolve?: string;\n localOnly?: boolean;\n}\ninterface ManifestResourceShape {\n type?: string;\n resourceKey?: string;\n permission?: string;\n description?: string;\n fields?: Record<string, ManifestFieldShape>;\n}\ninterface ManifestShape {\n resources?: {\n required?: ManifestResourceShape[];\n optional?: ManifestResourceShape[];\n };\n}\n\nfunction toFields(\n fields: Record<string, ManifestFieldShape> | undefined,\n): RequirementField[] {\n return Object.entries(fields ?? {}).map(([key, f]) => ({\n key,\n env: f.env,\n origin: f.origin,\n description: f.description,\n value: f.value,\n resolve: f.resolve,\n localOnly: f.localOnly,\n }));\n}\n\nfunction toRows(\n resources: ManifestResourceShape[] | undefined,\n required: boolean,\n): ResourceRequirementRow[] {\n return (resources ?? []).map((r) => ({\n type: r.type ?? \"unknown\",\n resourceKey: r.resourceKey,\n permission: r.permission,\n required,\n description: r.description,\n fields: toFields(r.fields),\n }));\n}\n\n/** The manifest.json file shipped by a plugin item, or null for UI items. */\nfunction findManifest(item: RegistryItem): ManifestShape | null {\n const file = (item.files ?? []).find(\n (f) => path.basename(f.target ?? f.path) === \"manifest.json\",\n );\n if (!file) return null;\n try {\n return JSON.parse(file.content) as ManifestShape;\n } catch {\n return null;\n }\n}\n\n/**\n * Extracts a plugin item's declared resource requirements (required first,\n * then optional). Returns an empty array for UI items or plugins that declare\n * no resources.\n */\nexport function extractRequirements(\n item: RegistryItem,\n): ResourceRequirementRow[] {\n const manifest = findManifest(item);\n if (!manifest) return [];\n return [\n ...toRows(manifest.resources?.required, true),\n ...toRows(manifest.resources?.optional, false),\n ];\n}\n\n/**\n * Renders the resource requirements for an item as human-readable lines.\n * Returns a single \"no resources\" line when there are none, so callers can\n * print unconditionally.\n */\nexport function renderRequirements(\n item: RegistryItem,\n rows: ResourceRequirementRow[] = extractRequirements(item),\n): string {\n if (rows.length === 0) {\n return pc.dim(`${item.name}: no resource requirements.`);\n }\n\n const lines: string[] = [pc.bold(`Resources required by ${item.name}:`)];\n for (const row of rows) {\n const tag = row.required ? pc.yellow(\"required\") : pc.dim(\"optional\");\n const perm = row.permission ? pc.dim(` [${row.permission}]`) : \"\";\n lines.push(` ${pc.cyan(row.type)} (${tag})${perm}`);\n if (row.description) lines.push(` ${pc.dim(row.description)}`);\n for (const field of row.fields) {\n if (!field.env) continue;\n const origin = field.origin ? pc.dim(` (${field.origin})`) : \"\";\n lines.push(` - ${field.env}${origin}`);\n }\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * A syntactically valid environment variable name. Field `env` names come from\n * an untrusted manifest and are written into `.env` as `NAME=value` (a newline\n * in the name would inject a second line — e.g. `PORT=x\\nDATABRICKS_HOST=…` →\n * credential exfil) and emitted into `app.yaml`. Anything that isn't a plain\n * env identifier is dropped before it reaches those sinks.\n */\nconst ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/** True when `name` is a safe, well-formed environment variable name. */\nexport function isValidEnvName(name: string): boolean {\n return ENV_NAME.test(name);\n}\n\n/**\n * Effective origin of a field: trust a synced manifest's stamped `origin`, else\n * derive it from the field shape via the same rule `plugin sync` uses, so an\n * authored manifest (no `origin`) still classifies correctly.\n */\nexport function fieldOrigin(\n field: RequirementField,\n): \"platform\" | \"static\" | \"cli\" | \"user\" {\n if (field.origin)\n return field.origin as \"platform\" | \"static\" | \"cli\" | \"user\";\n return computeOriginFromField(field);\n}\n"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"requirements.js","names":[],"sources":["../../../../src/cli/commands/registry/requirements.ts"],"sourcesContent":["import path from \"node:path\";\n\nimport pc from \"picocolors\";\n\nimport { computeOriginFromField } from \"../../../schemas/manifest\";\nimport type { RegistryItem } from \"./client\";\n\n/**\n * A single resource field as declared in a plugin manifest. `origin` is the\n * computed classifier written by `plugin sync` (platform/static/cli/user) that\n * says how the value reaches the running app.\n */\nexport interface RequirementField {\n key: string;\n env?: string;\n /**\n * Computed classifier written by `plugin sync` — only present in a synced\n * manifest. Authored manifests omit it, so consumers must derive the\n * effective origin via {@link fieldOrigin} rather than reading this directly.\n */\n origin?: string;\n description?: string;\n /** Default literal value (static origin); pre-filled without prompting. */\n value?: string;\n /** Resolver key (cli origin), e.g. \"postgres:host\". */\n resolve?: string;\n /** Local-dev-only field; platform-injected at deploy time. */\n localOnly?: boolean;\n}\n\n/** A resource requirement flattened for display. */\nexport interface ResourceRequirementRow {\n type: string;\n resourceKey?: string;\n permission?: string;\n required: boolean;\n description?: string;\n fields: RequirementField[];\n}\n\ninterface ManifestFieldShape {\n env?: string;\n origin?: string;\n description?: string;\n value?: string;\n resolve?: string;\n localOnly?: boolean;\n}\ninterface ManifestResourceShape {\n type?: string;\n resourceKey?: string;\n permission?: string;\n description?: string;\n fields?: Record<string, ManifestFieldShape>;\n}\ninterface ManifestShape {\n resources?: {\n required?: ManifestResourceShape[];\n optional?: ManifestResourceShape[];\n };\n}\n\nfunction toFields(\n fields: Record<string, ManifestFieldShape> | undefined,\n): RequirementField[] {\n return Object.entries(fields ?? {}).map(([key, f]) => ({\n key,\n env: f.env,\n origin: f.origin,\n description: f.description,\n value: f.value,\n resolve: f.resolve,\n localOnly: f.localOnly,\n }));\n}\n\nfunction toRows(\n resources: ManifestResourceShape[] | undefined,\n required: boolean,\n): ResourceRequirementRow[] {\n return (resources ?? []).map((r) => ({\n type: r.type ?? \"unknown\",\n resourceKey: r.resourceKey,\n permission: r.permission,\n required,\n description: r.description,\n fields: toFields(r.fields),\n }));\n}\n\n/** The manifest.json file shipped by a plugin item, or null for UI items. */\nfunction findManifest(item: RegistryItem): ManifestShape | null {\n const file = (item.files ?? []).find(\n (f) => path.basename(f.target ?? f.path) === \"manifest.json\",\n );\n if (!file) return null;\n try {\n return JSON.parse(file.content) as ManifestShape;\n } catch {\n return null;\n }\n}\n\n/**\n * Extracts a plugin item's declared resource requirements (required first,\n * then optional). Returns an empty array for UI items or plugins that declare\n * no resources.\n */\nexport function extractRequirements(\n item: RegistryItem,\n): ResourceRequirementRow[] {\n const manifest = findManifest(item);\n if (!manifest) return [];\n return [\n ...toRows(manifest.resources?.required, true),\n ...toRows(manifest.resources?.optional, false),\n ];\n}\n\n/**\n * Renders the resource requirements for an item as human-readable lines.\n * Returns a single \"no resources\" line when there are none, so callers can\n * print unconditionally.\n */\nexport function renderRequirements(\n item: RegistryItem,\n rows: ResourceRequirementRow[] = extractRequirements(item),\n): string {\n if (rows.length === 0) {\n return pc.dim(`${item.name}: no resource requirements.`);\n }\n\n const lines: string[] = [pc.bold(`Resources required by ${item.name}:`)];\n for (const row of rows) {\n const tag = row.required ? pc.yellow(\"required\") : pc.dim(\"optional\");\n const perm = row.permission ? pc.dim(` [${row.permission}]`) : \"\";\n lines.push(` ${pc.cyan(row.type)} (${tag})${perm}`);\n if (row.description) lines.push(` ${pc.dim(row.description)}`);\n for (const field of row.fields) {\n if (!field.env) continue;\n const origin = field.origin ? pc.dim(` (${field.origin})`) : \"\";\n lines.push(` - ${field.env}${origin}`);\n }\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * A syntactically valid environment variable name. Field `env` names come from\n * an untrusted manifest and are written into `.env` as `NAME=value` (a newline\n * in the name would inject a second line — e.g. `PORT=x\\nDATABRICKS_HOST=…` →\n * credential exfil) and emitted into `app.yaml`. Anything that isn't a plain\n * env identifier is dropped before it reaches those sinks.\n */\nconst ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/** True when `name` is a safe, well-formed environment variable name. */\nexport function isValidEnvName(name: string): boolean {\n return ENV_NAME.test(name);\n}\n\n/**\n * Effective origin of a field: trust a synced manifest's stamped `origin`, else\n * derive it from the field shape via the same rule `plugin sync` uses, so an\n * authored manifest (no `origin`) still classifies correctly.\n */\nexport function fieldOrigin(\n field: RequirementField,\n): \"platform\" | \"static\" | \"cli\" | \"user\" {\n if (field.origin)\n return field.origin as \"platform\" | \"static\" | \"cli\" | \"user\";\n return computeOriginFromField(field);\n}\n"],"mappings":";;;;;AA8DA,SAAS,SACP,QACoB;AACpB,QAAO,OAAO,QAAQ,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,QAAQ;EACrD;EACA,KAAK,EAAE;EACP,QAAQ,EAAE;EACV,aAAa,EAAE;EACf,OAAO,EAAE;EACT,SAAS,EAAE;EACX,WAAW,EAAE;EACd,EAAE;;AAGL,SAAS,OACP,WACA,UAC0B;AAC1B,SAAQ,aAAa,EAAE,EAAE,KAAK,OAAO;EACnC,MAAM,EAAE,QAAQ;EAChB,aAAa,EAAE;EACf,YAAY,EAAE;EACd;EACA,aAAa,EAAE;EACf,QAAQ,SAAS,EAAE,OAAO;EAC3B,EAAE;;;AAIL,SAAS,aAAa,MAA0C;CAC9D,MAAM,QAAQ,KAAK,SAAS,EAAE,EAAE,MAC7B,MAAM,KAAK,SAAS,EAAE,UAAU,EAAE,KAAK,KAAK,gBAC9C;AACD,KAAI,CAAC,KAAM,QAAO;AAClB,KAAI;AACF,SAAO,KAAK,MAAM,KAAK,QAAQ;SACzB;AACN,SAAO;;;;;;;;AASX,SAAgB,oBACd,MAC0B;CAC1B,MAAM,WAAW,aAAa,KAAK;AACnC,KAAI,CAAC,SAAU,QAAO,EAAE;AACxB,QAAO,CACL,GAAG,OAAO,SAAS,WAAW,UAAU,KAAK,EAC7C,GAAG,OAAO,SAAS,WAAW,UAAU,MAAM,CAC/C;;;;;;;AAQH,SAAgB,mBACd,MACA,OAAiC,oBAAoB,KAAK,EAClD;AACR,KAAI,KAAK,WAAW,EAClB,QAAO,GAAG,IAAI,GAAG,KAAK,KAAK,6BAA6B;CAG1D,MAAM,QAAkB,CAAC,GAAG,KAAK,yBAAyB,KAAK,KAAK,GAAG,CAAC;AACxE,MAAK,MAAM,OAAO,MAAM;EACtB,MAAM,MAAM,IAAI,WAAW,GAAG,OAAO,WAAW,GAAG,GAAG,IAAI,WAAW;EACrE,MAAM,OAAO,IAAI,aAAa,GAAG,IAAI,KAAK,IAAI,WAAW,GAAG,GAAG;AAC/D,QAAM,KAAK,KAAK,GAAG,KAAK,IAAI,KAAK,CAAC,IAAI,IAAI,GAAG,OAAO;AACpD,MAAI,IAAI,YAAa,OAAM,KAAK,OAAO,GAAG,IAAI,IAAI,YAAY,GAAG;AACjE,OAAK,MAAM,SAAS,IAAI,QAAQ;AAC9B,OAAI,CAAC,MAAM,IAAK;GAChB,MAAM,SAAS,MAAM,SAAS,GAAG,IAAI,KAAK,MAAM,OAAO,GAAG,GAAG;AAC7D,SAAM,KAAK,SAAS,MAAM,MAAM,SAAS;;;AAG7C,QAAO,MAAM,KAAK,KAAK;;;;;;;;;AAUzB,MAAM,WAAW;;AAGjB,SAAgB,eAAe,MAAuB;AACpD,QAAO,SAAS,KAAK,KAAK;;;;;;;AAQ5B,SAAgB,YACd,OACwC;AACxC,KAAI,MAAM,OACR,QAAO,MAAM;AACf,QAAO,uBAAuB,MAAM"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-register.js","names":[],"sources":["../../../../src/cli/commands/registry/server-register.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Lang, parse, type SgNode } from \"@ast-grep/napi\";\nimport { JS_IDENTIFIER } from \"./constants\";\n\n/** Server entry candidates within the server root, in priority order. */\nconst SERVER_FILE_CANDIDATES = [\n \"server.ts\",\n \"index.ts\",\n \"src/server.ts\",\n \"src/index.ts\",\n];\n\nexport interface RegisterResult {\n /** wired = edited; already = plugin was present; skipped = couldn't safely edit. */\n status: \"wired\" | \"already\" | \"skipped\";\n file?: string;\n reason?: string;\n}\n\nfunction findServerFile(serverRoot: string): string | null {\n for (const candidate of SERVER_FILE_CANDIDATES) {\n const p = path.join(serverRoot, candidate);\n if (fs.existsSync(p)) return p;\n }\n return null;\n}\n\n/** The `plugins: [...]` array node inside a createApp call, if present. */\nfunction findPluginsArray(root: SgNode): SgNode | null {\n for (const pair of root.findAll({ rule: { kind: \"pair\" } })) {\n const key = pair.find({ rule: { kind: \"property_identifier\" } });\n if (key?.text() !== \"plugins\") continue;\n const arr = pair.find({ rule: { kind: \"array\" } });\n if (arr) return arr;\n }\n return null;\n}\n\nfunction arrayElementNames(arr: SgNode): Set<string> {\n const names = new Set<string>();\n for (const child of arr.children()) {\n if (child.kind() === \"identifier\") {\n names.add(child.text());\n } else if (child.kind() === \"call_expression\") {\n const callee = child.children()[0];\n if (callee?.kind() === \"identifier\") names.add(callee.text());\n }\n }\n return names;\n}\n\n/** Local binding names introduced by an import statement — default, namespace,\n * and named specifiers, with aliases resolved to the local name. */\nfunction importBindingNames(stmt: SgNode): Set<string> {\n const names = new Set<string>();\n const clause = stmt.find({ rule: { kind: \"import_clause\" } });\n if (!clause) return names;\n for (const child of clause.children()) {\n const kind = child.kind();\n if (kind === \"identifier\") {\n names.add(child.text());\n } else if (kind === \"namespace_import\") {\n const id = child.find({ rule: { kind: \"identifier\" } });\n if (id) names.add(id.text());\n } else if (kind === \"named_imports\") {\n for (const spec of child.findAll({\n rule: { kind: \"import_specifier\" },\n })) {\n const local = spec.field(\"alias\")?.text() ?? spec.field(\"name\")?.text();\n if (local) names.add(local);\n }\n }\n }\n return names;\n}\n\n/**\n * Best-effort: register a plugin in the server entry's `createApp({ plugins })`\n * call by inserting the import and adding it to the array. Only edits the\n * standard shape (a `plugins: [...]` array literal); returns `skipped` otherwise\n * so the caller can fall back to printing manual instructions. Idempotent.\n */\nexport function registerPluginInServer(\n serverRoot: string,\n importPath: string,\n exportName: string,\n): RegisterResult {\n // exportName and importPath are interpolated into the user's server source.\n // Registry items are untrusted, so refuse anything that isn't a plain JS\n // identifier / clean relative module path — prevents code injection via a\n // crafted export name or import path.\n if (!JS_IDENTIFIER.test(exportName)) {\n return { status: \"skipped\", reason: \"invalid plugin export name\" };\n }\n if (!/^[.][./A-Za-z0-9_-]*$/.test(importPath)) {\n return { status: \"skipped\", reason: \"invalid plugin import path\" };\n }\n\n const serverFile = findServerFile(serverRoot);\n if (!serverFile) {\n return { status: \"skipped\", reason: \"no server entry file found\" };\n }\n\n const content = fs.readFileSync(serverFile, \"utf-8\");\n const lang = serverFile.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const root = parse(lang, content).root();\n\n const arr = findPluginsArray(root);\n if (!arr) {\n return {\n status: \"skipped\",\n reason: \"no createApp({ plugins: [...] }) array found\",\n };\n }\n\n const file = path.relative(serverRoot, serverFile);\n if (arrayElementNames(arr).has(exportName)) {\n return { status: \"already\", file };\n }\n\n const edits = [];\n\n // toPlugin exports are factories, registered as a call: `hello()`.\n const newElem = `${exportName}()`;\n\n // Insert before the first element, matching its indentation so the array\n // formatting is preserved (or inline for a single-line array).\n const elementKinds = [\"identifier\", \"call_expression\", \"spread_element\"];\n const firstEl = arr\n .children()\n .find((c) => elementKinds.includes(c.kind() as string));\n if (!firstEl) {\n edits.push(arr.replace(`[${newElem}]`));\n } else {\n const startIdx = firstEl.range().start.index;\n const lineStart = content.lastIndexOf(\"\\n\", startIdx - 1);\n const indent = content.slice(lineStart + 1, startIdx);\n const multiline = lineStart !== -1 && /^[ \\t]*$/.test(indent);\n const sep = multiline ? `,\\n${indent}` : \", \";\n edits.push(firstEl.replace(`${newElem}${sep}${firstEl.text()}`));\n }\n\n // Add the import unless `exportName` is already bound by some import. De-dup\n // on the binding, not the module path: an existing import from the same path\n // under a different name (e.g. `import { HelloPlugin } from \"./plugins/hello\"`)\n // must not suppress this one, or the `${exportName}()` element just added to\n // the array would reference an unimported symbol and the server won't compile.\n const importStmts = root.findAll({ rule: { kind: \"import_statement\" } });\n const hasBinding = importStmts.some((s) =>\n importBindingNames(s).has(exportName),\n );\n const importLine = `import { ${exportName} } from \"${importPath}\";`;\n if (!hasBinding && importStmts.length > 0) {\n const last = importStmts[importStmts.length - 1];\n edits.push(last.replace(`${last.text()}\\n${importLine}`));\n }\n\n let output = root.commitEdits(edits);\n if (!hasBinding && importStmts.length === 0) {\n output = `${importLine}\\n${output}`;\n }\n fs.writeFileSync(serverFile, output);\n\n return { status: \"wired\", file };\n}\n"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"server-register.js","names":[],"sources":["../../../../src/cli/commands/registry/server-register.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { Lang, parse, type SgNode } from \"@ast-grep/napi\";\n\nimport { JS_IDENTIFIER } from \"./constants\";\n\n/** Server entry candidates within the server root, in priority order. */\nconst SERVER_FILE_CANDIDATES = [\n \"server.ts\",\n \"index.ts\",\n \"src/server.ts\",\n \"src/index.ts\",\n];\n\nexport interface RegisterResult {\n /** wired = edited; already = plugin was present; skipped = couldn't safely edit. */\n status: \"wired\" | \"already\" | \"skipped\";\n file?: string;\n reason?: string;\n}\n\nfunction findServerFile(serverRoot: string): string | null {\n for (const candidate of SERVER_FILE_CANDIDATES) {\n const p = path.join(serverRoot, candidate);\n if (fs.existsSync(p)) return p;\n }\n return null;\n}\n\n/** The `plugins: [...]` array node inside a createApp call, if present. */\nfunction findPluginsArray(root: SgNode): SgNode | null {\n for (const pair of root.findAll({ rule: { kind: \"pair\" } })) {\n const key = pair.find({ rule: { kind: \"property_identifier\" } });\n if (key?.text() !== \"plugins\") continue;\n const arr = pair.find({ rule: { kind: \"array\" } });\n if (arr) return arr;\n }\n return null;\n}\n\nfunction arrayElementNames(arr: SgNode): Set<string> {\n const names = new Set<string>();\n for (const child of arr.children()) {\n if (child.kind() === \"identifier\") {\n names.add(child.text());\n } else if (child.kind() === \"call_expression\") {\n const callee = child.children()[0];\n if (callee?.kind() === \"identifier\") names.add(callee.text());\n }\n }\n return names;\n}\n\n/** Local binding names introduced by an import statement — default, namespace,\n * and named specifiers, with aliases resolved to the local name. */\nfunction importBindingNames(stmt: SgNode): Set<string> {\n const names = new Set<string>();\n const clause = stmt.find({ rule: { kind: \"import_clause\" } });\n if (!clause) return names;\n for (const child of clause.children()) {\n const kind = child.kind();\n if (kind === \"identifier\") {\n names.add(child.text());\n } else if (kind === \"namespace_import\") {\n const id = child.find({ rule: { kind: \"identifier\" } });\n if (id) names.add(id.text());\n } else if (kind === \"named_imports\") {\n for (const spec of child.findAll({\n rule: { kind: \"import_specifier\" },\n })) {\n const local = spec.field(\"alias\")?.text() ?? spec.field(\"name\")?.text();\n if (local) names.add(local);\n }\n }\n }\n return names;\n}\n\n/**\n * Best-effort: register a plugin in the server entry's `createApp({ plugins })`\n * call by inserting the import and adding it to the array. Only edits the\n * standard shape (a `plugins: [...]` array literal); returns `skipped` otherwise\n * so the caller can fall back to printing manual instructions. Idempotent.\n */\nexport function registerPluginInServer(\n serverRoot: string,\n importPath: string,\n exportName: string,\n): RegisterResult {\n // exportName and importPath are interpolated into the user's server source.\n // Registry items are untrusted, so refuse anything that isn't a plain JS\n // identifier / clean relative module path — prevents code injection via a\n // crafted export name or import path.\n if (!JS_IDENTIFIER.test(exportName)) {\n return { status: \"skipped\", reason: \"invalid plugin export name\" };\n }\n if (!/^[.][./A-Za-z0-9_-]*$/.test(importPath)) {\n return { status: \"skipped\", reason: \"invalid plugin import path\" };\n }\n\n const serverFile = findServerFile(serverRoot);\n if (!serverFile) {\n return { status: \"skipped\", reason: \"no server entry file found\" };\n }\n\n const content = fs.readFileSync(serverFile, \"utf-8\");\n const lang = serverFile.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const root = parse(lang, content).root();\n\n const arr = findPluginsArray(root);\n if (!arr) {\n return {\n status: \"skipped\",\n reason: \"no createApp({ plugins: [...] }) array found\",\n };\n }\n\n const file = path.relative(serverRoot, serverFile);\n if (arrayElementNames(arr).has(exportName)) {\n return { status: \"already\", file };\n }\n\n const edits = [];\n\n // toPlugin exports are factories, registered as a call: `hello()`.\n const newElem = `${exportName}()`;\n\n // Insert before the first element, matching its indentation so the array\n // formatting is preserved (or inline for a single-line array).\n const elementKinds = [\"identifier\", \"call_expression\", \"spread_element\"];\n const firstEl = arr\n .children()\n .find((c) => elementKinds.includes(c.kind() as string));\n if (!firstEl) {\n edits.push(arr.replace(`[${newElem}]`));\n } else {\n const startIdx = firstEl.range().start.index;\n const lineStart = content.lastIndexOf(\"\\n\", startIdx - 1);\n const indent = content.slice(lineStart + 1, startIdx);\n const multiline = lineStart !== -1 && /^[ \\t]*$/.test(indent);\n const sep = multiline ? `,\\n${indent}` : \", \";\n edits.push(firstEl.replace(`${newElem}${sep}${firstEl.text()}`));\n }\n\n // Add the import unless `exportName` is already bound by some import. De-dup\n // on the binding, not the module path: an existing import from the same path\n // under a different name (e.g. `import { HelloPlugin } from \"./plugins/hello\"`)\n // must not suppress this one, or the `${exportName}()` element just added to\n // the array would reference an unimported symbol and the server won't compile.\n const importStmts = root.findAll({ rule: { kind: \"import_statement\" } });\n const hasBinding = importStmts.some((s) =>\n importBindingNames(s).has(exportName),\n );\n const importLine = `import { ${exportName} } from \"${importPath}\";`;\n if (!hasBinding && importStmts.length > 0) {\n const last = importStmts[importStmts.length - 1];\n edits.push(last.replace(`${last.text()}\\n${importLine}`));\n }\n\n let output = root.commitEdits(edits);\n if (!hasBinding && importStmts.length === 0) {\n output = `${importLine}\\n${output}`;\n }\n fs.writeFileSync(serverFile, output);\n\n return { status: \"wired\", file };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-picker.js","names":[],"sources":["../../../../src/cli/commands/registry/workspace-picker.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport {\n createWorkspaceClient,\n type LegacyWorkspaceClient,\n} from \"../../../workspace-client\";\n\n/**\n * Lists a user's real Databricks workspace resources so `appkit add` can offer\n * a picker instead of blind free-text entry.\n *\n * Flat resource types are listed via the Databricks SDK client (typed,\n * auto-paginating) obtained through the sanctioned `workspace-client` facade.\n * Parent-context types (volume, uc_function, secret, vector_search_index) still\n * shell out to the `databricks` CLI for their drill-down. Every path fails\n * soft: any error returns an empty list and the caller drops to free-text entry.\n */\n\n/** A workspace resource choice surfaced in the picker. */\nexport interface WorkspaceChoice {\n /** Value written to the env var (the resource id/name). */\n value: string;\n /** Human label shown in the picker (name, falling back to value). */\n label: string;\n}\n\n/**\n * Per-type SDK lister: streams a resource type off the WorkspaceClient into\n * `{value,label}` choices. `list()` returns an async iterable of raw SDK\n * objects; `toChoice` maps each to a picker choice. The SDK auto-paginates, so\n * we simply iterate to completion.\n */\ninterface SdkLister {\n list: (client: LegacyWorkspaceClient) => AsyncIterable<unknown>;\n toChoice: (item: Record<string, unknown>) => WorkspaceChoice | null;\n}\n\n/** Builds a `{value,label}` from an id field and optional label field. */\nfunction choiceFrom(\n item: Record<string, unknown>,\n idField: string,\n labelField?: string,\n): WorkspaceChoice | null {\n const id = item[idField];\n if (id === undefined || id === null) return null;\n const value = String(id);\n const rawLabel = labelField ? item[labelField] : undefined;\n const label =\n typeof rawLabel === \"string\" && rawLabel.length > 0\n ? `${rawLabel} (${value})`\n : value;\n return { value, label };\n}\n\n/**\n * Genie listSpaces returns a single page, not an auto-paginating iterable like\n * the other services. Adapt it to one that follows `next_page_token`; the\n * repeated-token guard avoids an infinite loop if the API echoes a token back.\n */\nasync function* iterateGenieSpaces(\n client: LegacyWorkspaceClient,\n): AsyncIterable<unknown> {\n let pageToken: string | undefined;\n do {\n const res = await client.genie.listSpaces(\n pageToken ? { page_token: pageToken } : {},\n );\n for (const space of res.spaces ?? []) yield space;\n const next = res.next_page_token;\n if (next && next === pageToken) break;\n pageToken = next;\n } while (pageToken);\n}\n\n/** Flat, top-level listable resource types, backed by SDK services. */\nexport const SDK_LISTERS: Record<string, SdkLister> = {\n sql_warehouse: {\n list: (c) => c.warehouses.list({}),\n toChoice: (i) => choiceFrom(i, \"id\", \"name\"),\n },\n job: {\n list: (c) => c.jobs.list({}),\n // job name lives under settings.name; id is top-level job_id\n toChoice: (i) => {\n const settings = i.settings as { name?: string } | undefined;\n return choiceFrom({ ...i, name: settings?.name }, \"job_id\", \"name\");\n },\n },\n serving_endpoint: {\n list: (c) => c.servingEndpoints.list(),\n toChoice: (i) => choiceFrom(i, \"name\", \"name\"),\n },\n uc_connection: {\n list: (c) => c.connections.list({}),\n toChoice: (i) => choiceFrom(i, \"name\", \"full_name\"),\n },\n database: {\n list: (c) => c.database.listDatabaseInstances({}),\n toChoice: (i) => choiceFrom(i, \"name\", \"name\"),\n },\n genie_space: {\n list: iterateGenieSpaces,\n toChoice: (i) => choiceFrom(i, \"space_id\", \"title\"),\n },\n experiment: {\n list: (c) => c.experiments.listExperiments({}),\n toChoice: (i) => choiceFrom(i, \"experiment_id\", \"name\"),\n },\n app: {\n list: (c) => c.apps.list({}),\n toChoice: (i) => choiceFrom(i, \"name\", \"name\"),\n },\n};\n\n/** True when a resource type can be listed flat (no parent context). */\nexport function isFlatListable(resourceType: string): boolean {\n return resourceType in SDK_LISTERS;\n}\n\n/**\n * Constructs a raw SDK workspace client for the given profile (or default\n * resolution), via the sanctioned `workspace-client` facade. Uses the legacy\n * escape hatch because the picker needs services (connections, database,\n * experiments, apps) the facade doesn't yet proxy directly.\n */\nexport function makeWorkspaceClient(profile?: string): LegacyWorkspaceClient {\n return createWorkspaceClient(\n profile ? { profile } : {},\n ).toLegacyWorkspaceClient();\n}\n\n/**\n * Max resources fetched for the picker. `list()` auto-paginates, so on a large\n * workspace (5000+ warehouses) breaking out at the cap stops pagination early;\n * the picker's \"Enter manually\" option covers anything beyond it.\n */\nexport const MAX_PICKER_RESULTS = 200;\n\n/** A listing result plus whether it was truncated at the fetch cap. */\nexport interface WorkspaceListing {\n choices: WorkspaceChoice[];\n truncated: boolean;\n /** Short reason the listing failed (auth/config/network), if it did — lets\n * the caller distinguish a real error from a genuinely empty workspace. */\n error?: string;\n}\n\n/** First line of an error, capped, for a user-facing one-liner. */\nfunction shortErrorMessage(err: unknown): string {\n const msg = err instanceof Error ? err.message : String(err);\n return msg.split(\"\\n\")[0].trim().slice(0, 200);\n}\n\n/**\n * Lists workspace resources of a flat-listable type via the SDK, stopping at\n * MAX_PICKER_RESULTS so pagination doesn't drain a huge workspace. Returns an\n * empty listing on any failure (unknown type, auth/config error, network) so\n * the caller can fall back to free-text entry. `clientFactory` is injectable\n * for tests.\n */\nexport async function listWorkspaceResources(\n resourceType: string,\n profile?: string,\n clientFactory: (\n profile?: string,\n ) => LegacyWorkspaceClient = makeWorkspaceClient,\n): Promise<WorkspaceListing> {\n const lister = SDK_LISTERS[resourceType];\n if (!lister) return { choices: [], truncated: false };\n try {\n const client = clientFactory(profile);\n const choices: WorkspaceChoice[] = [];\n let truncated = false;\n for await (const item of lister.list(client)) {\n if (typeof item !== \"object\" || item === null) continue;\n const choice = lister.toChoice(item as Record<string, unknown>);\n if (!choice) continue;\n choices.push(choice);\n if (choices.length >= MAX_PICKER_RESULTS) {\n // Stop iterating — halts the async iterator, so no further pages fetch.\n truncated = true;\n break;\n }\n }\n return { choices, truncated };\n } catch (err) {\n // Don't swallow silently: return the reason so the caller can tell the user\n // the picker fell back because listing failed (typically auth/profile), not\n // because the workspace has none of this resource.\n return { choices: [], truncated: false, error: shortErrorMessage(err) };\n }\n}\n\n/** Runs a databricks CLI subcommand returning JSON; injectable for tests. */\nexport type CliRunner = (args: string[]) => {\n status: number | null;\n stdout: string;\n};\n\nconst defaultRunner: CliRunner = (args) => {\n // Parent-context lists can be large; raise maxBuffer well above the 1MB\n // default so a big JSON response isn't truncated into \"no resources found\".\n const res = spawnSync(\"databricks\", args, {\n encoding: \"utf-8\",\n maxBuffer: 64 * 1024 * 1024,\n });\n return { status: res.status, stdout: res.stdout ?? \"\" };\n};\n\n/**\n * Extracts `{value,label}` choices from a parsed CLI list response. The CLI\n * returns either a bare array or an object wrapping one; we scan for the first\n * array of objects. Items missing the id field are skipped.\n */\nexport function toChoices(\n parsed: unknown,\n idField: string,\n labelField?: string,\n): WorkspaceChoice[] {\n const arr = firstArray(parsed);\n const choices: WorkspaceChoice[] = [];\n for (const item of arr) {\n if (typeof item !== \"object\" || item === null) continue;\n const choice = choiceFrom(\n item as Record<string, unknown>,\n idField,\n labelField,\n );\n if (choice) choices.push(choice);\n }\n return choices;\n}\n\n/** Finds the first array in a CLI response (bare array or single wrapper key). */\nfunction firstArray(parsed: unknown): unknown[] {\n if (Array.isArray(parsed)) return parsed;\n if (parsed && typeof parsed === \"object\") {\n for (const v of Object.values(parsed)) {\n if (Array.isArray(v)) return v;\n }\n }\n return [];\n}\n\n/**\n * Runs a `databricks … list -o json` command and returns parsed choices, or\n * [] on any failure (CLI missing/errored, empty, non-JSON). `command` is the\n * argv after `databricks`; `-o json` and `-p <profile>` are appended.\n * Used for the parent-context drill-down (catalogs/schemas/scopes/endpoints).\n */\nexport function runList(\n command: string[],\n idField: string,\n labelField: string | undefined,\n profile: string | undefined,\n runner: CliRunner = defaultRunner,\n): WorkspaceChoice[] {\n const args = [...command, \"-o\", \"json\"];\n if (profile) args.push(\"-p\", profile);\n\n let result: { status: number | null; stdout: string };\n try {\n result = runner(args);\n } catch {\n return [];\n }\n if (result.status !== 0 || !result.stdout.trim()) return [];\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(result.stdout);\n } catch {\n return [];\n }\n return toChoices(parsed, idField, labelField);\n}\n\n/**\n * A drill-down step for a parent-context resource type. `list(parents)` builds\n * the CLI argv given the values picked in prior steps (e.g. [catalog] → schema\n * list command). `key` labels the step for prompts.\n */\nexport interface ParentContextStep {\n key: string;\n list: (parents: string[]) => {\n command: string[];\n idField: string;\n labelField?: string;\n };\n}\n\n/**\n * Drill-down chains for parent-context resource types. Each ends by listing\n * the resource itself; earlier steps list the parents to pick first.\n * Positional-arg CLI gotcha: `databricks schemas list <CATALOG>` etc. take the\n * parent as a positional, not a flag.\n */\nexport const PARENT_CONTEXT_CHAINS: Record<string, ParentContextStep[]> = {\n volume: [\n {\n key: \"catalog\",\n list: () => ({\n command: [\"catalogs\", \"list\"],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"schema\",\n list: ([catalog]) => ({\n command: [\"schemas\", \"list\", catalog],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"volume\",\n list: ([catalog, schema]) => ({\n command: [\"volumes\", \"list\", catalog, schema],\n idField: \"full_name\",\n labelField: \"name\",\n }),\n },\n ],\n uc_function: [\n {\n key: \"catalog\",\n list: () => ({\n command: [\"catalogs\", \"list\"],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"schema\",\n list: ([catalog]) => ({\n command: [\"schemas\", \"list\", catalog],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"function\",\n list: ([catalog, schema]) => ({\n command: [\"functions\", \"list\", catalog, schema],\n idField: \"full_name\",\n labelField: \"name\",\n }),\n },\n ],\n secret: [\n {\n key: \"scope\",\n list: () => ({\n command: [\"secrets\", \"list-scopes\"],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"key\",\n list: ([scope]) => ({\n command: [\"secrets\", \"list-secrets\", scope],\n idField: \"key\",\n labelField: \"key\",\n }),\n },\n ],\n vector_search_index: [\n {\n key: \"endpoint\",\n list: () => ({\n command: [\"vector-search-endpoints\", \"list-endpoints\"],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"index\",\n list: ([endpoint]) => ({\n command: [\"vector-search-indexes\", \"list-indexes\", endpoint],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n ],\n};\n\n/** True when a resource type needs a parent-context drill-down to list. */\nexport function isParentContext(resourceType: string): boolean {\n return resourceType in PARENT_CONTEXT_CHAINS;\n}\n\n/** One resolved step of a drill-down: the choices to present at this level. */\nexport interface DrillStep {\n key: string;\n choices: WorkspaceChoice[];\n}\n\n/**\n * Lists the choices for a single drill-down step given the values picked so\n * far. Returns [] on failure. The caller drives the interaction (present\n * `choices`, collect a pick, call again with it appended to `parents`).\n */\nexport function listParentContextStep(\n resourceType: string,\n stepIndex: number,\n parents: string[],\n profile?: string,\n runner: CliRunner = defaultRunner,\n): DrillStep | null {\n const chain = PARENT_CONTEXT_CHAINS[resourceType];\n if (!chain || stepIndex >= chain.length) return null;\n const step = chain[stepIndex];\n // Prior picks become positional CLI args; a value starting with `-` would be\n // parsed as a flag, so refuse it (empty step → free-text fallback). Real\n // catalog/schema/scope/endpoint names never start with `-`.\n if (parents.some((p) => p.startsWith(\"-\"))) {\n return { key: step.key, choices: [] };\n }\n const spec = step.list(parents);\n return {\n key: step.key,\n choices: runList(\n spec.command,\n spec.idField,\n spec.labelField,\n profile,\n runner,\n ),\n };\n}\n\n/** Number of drill-down steps for a parent-context type (0 if not one). */\nexport function parentContextDepth(resourceType: string): number {\n return PARENT_CONTEXT_CHAINS[resourceType]?.length ?? 0;\n}\n\n/**\n * Builds the final resource identifier from the values picked across a\n * drill-down. Most types end on a self-qualified id (volume/uc_function list\n * `full_name`; a vector-search index name is already catalog.schema-qualified),\n * so the last pick is the whole answer. A `secret` is addressed by both its\n * scope and key (`scope/key`) — returning only the key drops the scope and\n * yields a value that can't locate the secret — so its picks are joined.\n */\nexport function composeResourceId(\n resourceType: string,\n picks: string[],\n): string {\n if (resourceType === \"secret\") return picks.join(\"/\");\n return picks[picks.length - 1];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"workspace-picker.js","names":[],"sources":["../../../../src/cli/commands/registry/workspace-picker.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\n\nimport {\n createWorkspaceClient,\n type LegacyWorkspaceClient,\n} from \"../../../workspace-client\";\n\n/**\n * Lists a user's real Databricks workspace resources so `appkit add` can offer\n * a picker instead of blind free-text entry.\n *\n * Flat resource types are listed via the Databricks SDK client (typed,\n * auto-paginating) obtained through the sanctioned `workspace-client` facade.\n * Parent-context types (volume, uc_function, secret, vector_search_index) still\n * shell out to the `databricks` CLI for their drill-down. Every path fails\n * soft: any error returns an empty list and the caller drops to free-text entry.\n */\n\n/** A workspace resource choice surfaced in the picker. */\nexport interface WorkspaceChoice {\n /** Value written to the env var (the resource id/name). */\n value: string;\n /** Human label shown in the picker (name, falling back to value). */\n label: string;\n}\n\n/**\n * Per-type SDK lister: streams a resource type off the WorkspaceClient into\n * `{value,label}` choices. `list()` returns an async iterable of raw SDK\n * objects; `toChoice` maps each to a picker choice. The SDK auto-paginates, so\n * we simply iterate to completion.\n */\ninterface SdkLister {\n list: (client: LegacyWorkspaceClient) => AsyncIterable<unknown>;\n toChoice: (item: Record<string, unknown>) => WorkspaceChoice | null;\n}\n\n/** Builds a `{value,label}` from an id field and optional label field. */\nfunction choiceFrom(\n item: Record<string, unknown>,\n idField: string,\n labelField?: string,\n): WorkspaceChoice | null {\n const id = item[idField];\n if (id === undefined || id === null) return null;\n const value = String(id);\n const rawLabel = labelField ? item[labelField] : undefined;\n const label =\n typeof rawLabel === \"string\" && rawLabel.length > 0\n ? `${rawLabel} (${value})`\n : value;\n return { value, label };\n}\n\n/**\n * Genie listSpaces returns a single page, not an auto-paginating iterable like\n * the other services. Adapt it to one that follows `next_page_token`; the\n * repeated-token guard avoids an infinite loop if the API echoes a token back.\n */\nasync function* iterateGenieSpaces(\n client: LegacyWorkspaceClient,\n): AsyncIterable<unknown> {\n let pageToken: string | undefined;\n do {\n const res = await client.genie.listSpaces(\n pageToken ? { page_token: pageToken } : {},\n );\n for (const space of res.spaces ?? []) yield space;\n const next = res.next_page_token;\n if (next && next === pageToken) break;\n pageToken = next;\n } while (pageToken);\n}\n\n/** Flat, top-level listable resource types, backed by SDK services. */\nexport const SDK_LISTERS: Record<string, SdkLister> = {\n sql_warehouse: {\n list: (c) => c.warehouses.list({}),\n toChoice: (i) => choiceFrom(i, \"id\", \"name\"),\n },\n job: {\n list: (c) => c.jobs.list({}),\n // job name lives under settings.name; id is top-level job_id\n toChoice: (i) => {\n const settings = i.settings as { name?: string } | undefined;\n return choiceFrom({ ...i, name: settings?.name }, \"job_id\", \"name\");\n },\n },\n serving_endpoint: {\n list: (c) => c.servingEndpoints.list(),\n toChoice: (i) => choiceFrom(i, \"name\", \"name\"),\n },\n uc_connection: {\n list: (c) => c.connections.list({}),\n toChoice: (i) => choiceFrom(i, \"name\", \"full_name\"),\n },\n database: {\n list: (c) => c.database.listDatabaseInstances({}),\n toChoice: (i) => choiceFrom(i, \"name\", \"name\"),\n },\n genie_space: {\n list: iterateGenieSpaces,\n toChoice: (i) => choiceFrom(i, \"space_id\", \"title\"),\n },\n experiment: {\n list: (c) => c.experiments.listExperiments({}),\n toChoice: (i) => choiceFrom(i, \"experiment_id\", \"name\"),\n },\n app: {\n list: (c) => c.apps.list({}),\n toChoice: (i) => choiceFrom(i, \"name\", \"name\"),\n },\n};\n\n/** True when a resource type can be listed flat (no parent context). */\nexport function isFlatListable(resourceType: string): boolean {\n return resourceType in SDK_LISTERS;\n}\n\n/**\n * Constructs a raw SDK workspace client for the given profile (or default\n * resolution), via the sanctioned `workspace-client` facade. Uses the legacy\n * escape hatch because the picker needs services (connections, database,\n * experiments, apps) the facade doesn't yet proxy directly.\n */\nexport function makeWorkspaceClient(profile?: string): LegacyWorkspaceClient {\n return createWorkspaceClient(\n profile ? { profile } : {},\n ).toLegacyWorkspaceClient();\n}\n\n/**\n * Max resources fetched for the picker. `list()` auto-paginates, so on a large\n * workspace (5000+ warehouses) breaking out at the cap stops pagination early;\n * the picker's \"Enter manually\" option covers anything beyond it.\n */\nexport const MAX_PICKER_RESULTS = 200;\n\n/** A listing result plus whether it was truncated at the fetch cap. */\nexport interface WorkspaceListing {\n choices: WorkspaceChoice[];\n truncated: boolean;\n /** Short reason the listing failed (auth/config/network), if it did — lets\n * the caller distinguish a real error from a genuinely empty workspace. */\n error?: string;\n}\n\n/** First line of an error, capped, for a user-facing one-liner. */\nfunction shortErrorMessage(err: unknown): string {\n const msg = err instanceof Error ? err.message : String(err);\n return msg.split(\"\\n\")[0].trim().slice(0, 200);\n}\n\n/**\n * Lists workspace resources of a flat-listable type via the SDK, stopping at\n * MAX_PICKER_RESULTS so pagination doesn't drain a huge workspace. Returns an\n * empty listing on any failure (unknown type, auth/config error, network) so\n * the caller can fall back to free-text entry. `clientFactory` is injectable\n * for tests.\n */\nexport async function listWorkspaceResources(\n resourceType: string,\n profile?: string,\n clientFactory: (\n profile?: string,\n ) => LegacyWorkspaceClient = makeWorkspaceClient,\n): Promise<WorkspaceListing> {\n const lister = SDK_LISTERS[resourceType];\n if (!lister) return { choices: [], truncated: false };\n try {\n const client = clientFactory(profile);\n const choices: WorkspaceChoice[] = [];\n let truncated = false;\n for await (const item of lister.list(client)) {\n if (typeof item !== \"object\" || item === null) continue;\n const choice = lister.toChoice(item as Record<string, unknown>);\n if (!choice) continue;\n choices.push(choice);\n if (choices.length >= MAX_PICKER_RESULTS) {\n // Stop iterating — halts the async iterator, so no further pages fetch.\n truncated = true;\n break;\n }\n }\n return { choices, truncated };\n } catch (err) {\n // Don't swallow silently: return the reason so the caller can tell the user\n // the picker fell back because listing failed (typically auth/profile), not\n // because the workspace has none of this resource.\n return { choices: [], truncated: false, error: shortErrorMessage(err) };\n }\n}\n\n/** Runs a databricks CLI subcommand returning JSON; injectable for tests. */\nexport type CliRunner = (args: string[]) => {\n status: number | null;\n stdout: string;\n};\n\nconst defaultRunner: CliRunner = (args) => {\n // Parent-context lists can be large; raise maxBuffer well above the 1MB\n // default so a big JSON response isn't truncated into \"no resources found\".\n const res = spawnSync(\"databricks\", args, {\n encoding: \"utf-8\",\n maxBuffer: 64 * 1024 * 1024,\n });\n return { status: res.status, stdout: res.stdout ?? \"\" };\n};\n\n/**\n * Extracts `{value,label}` choices from a parsed CLI list response. The CLI\n * returns either a bare array or an object wrapping one; we scan for the first\n * array of objects. Items missing the id field are skipped.\n */\nexport function toChoices(\n parsed: unknown,\n idField: string,\n labelField?: string,\n): WorkspaceChoice[] {\n const arr = firstArray(parsed);\n const choices: WorkspaceChoice[] = [];\n for (const item of arr) {\n if (typeof item !== \"object\" || item === null) continue;\n const choice = choiceFrom(\n item as Record<string, unknown>,\n idField,\n labelField,\n );\n if (choice) choices.push(choice);\n }\n return choices;\n}\n\n/** Finds the first array in a CLI response (bare array or single wrapper key). */\nfunction firstArray(parsed: unknown): unknown[] {\n if (Array.isArray(parsed)) return parsed;\n if (parsed && typeof parsed === \"object\") {\n for (const v of Object.values(parsed)) {\n if (Array.isArray(v)) return v;\n }\n }\n return [];\n}\n\n/**\n * Runs a `databricks … list -o json` command and returns parsed choices, or\n * [] on any failure (CLI missing/errored, empty, non-JSON). `command` is the\n * argv after `databricks`; `-o json` and `-p <profile>` are appended.\n * Used for the parent-context drill-down (catalogs/schemas/scopes/endpoints).\n */\nexport function runList(\n command: string[],\n idField: string,\n labelField: string | undefined,\n profile: string | undefined,\n runner: CliRunner = defaultRunner,\n): WorkspaceChoice[] {\n const args = [...command, \"-o\", \"json\"];\n if (profile) args.push(\"-p\", profile);\n\n let result: { status: number | null; stdout: string };\n try {\n result = runner(args);\n } catch {\n return [];\n }\n if (result.status !== 0 || !result.stdout.trim()) return [];\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(result.stdout);\n } catch {\n return [];\n }\n return toChoices(parsed, idField, labelField);\n}\n\n/**\n * A drill-down step for a parent-context resource type. `list(parents)` builds\n * the CLI argv given the values picked in prior steps (e.g. [catalog] → schema\n * list command). `key` labels the step for prompts.\n */\nexport interface ParentContextStep {\n key: string;\n list: (parents: string[]) => {\n command: string[];\n idField: string;\n labelField?: string;\n };\n}\n\n/**\n * Drill-down chains for parent-context resource types. Each ends by listing\n * the resource itself; earlier steps list the parents to pick first.\n * Positional-arg CLI gotcha: `databricks schemas list <CATALOG>` etc. take the\n * parent as a positional, not a flag.\n */\nexport const PARENT_CONTEXT_CHAINS: Record<string, ParentContextStep[]> = {\n volume: [\n {\n key: \"catalog\",\n list: () => ({\n command: [\"catalogs\", \"list\"],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"schema\",\n list: ([catalog]) => ({\n command: [\"schemas\", \"list\", catalog],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"volume\",\n list: ([catalog, schema]) => ({\n command: [\"volumes\", \"list\", catalog, schema],\n idField: \"full_name\",\n labelField: \"name\",\n }),\n },\n ],\n uc_function: [\n {\n key: \"catalog\",\n list: () => ({\n command: [\"catalogs\", \"list\"],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"schema\",\n list: ([catalog]) => ({\n command: [\"schemas\", \"list\", catalog],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"function\",\n list: ([catalog, schema]) => ({\n command: [\"functions\", \"list\", catalog, schema],\n idField: \"full_name\",\n labelField: \"name\",\n }),\n },\n ],\n secret: [\n {\n key: \"scope\",\n list: () => ({\n command: [\"secrets\", \"list-scopes\"],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"key\",\n list: ([scope]) => ({\n command: [\"secrets\", \"list-secrets\", scope],\n idField: \"key\",\n labelField: \"key\",\n }),\n },\n ],\n vector_search_index: [\n {\n key: \"endpoint\",\n list: () => ({\n command: [\"vector-search-endpoints\", \"list-endpoints\"],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n {\n key: \"index\",\n list: ([endpoint]) => ({\n command: [\"vector-search-indexes\", \"list-indexes\", endpoint],\n idField: \"name\",\n labelField: \"name\",\n }),\n },\n ],\n};\n\n/** True when a resource type needs a parent-context drill-down to list. */\nexport function isParentContext(resourceType: string): boolean {\n return resourceType in PARENT_CONTEXT_CHAINS;\n}\n\n/** One resolved step of a drill-down: the choices to present at this level. */\nexport interface DrillStep {\n key: string;\n choices: WorkspaceChoice[];\n}\n\n/**\n * Lists the choices for a single drill-down step given the values picked so\n * far. Returns [] on failure. The caller drives the interaction (present\n * `choices`, collect a pick, call again with it appended to `parents`).\n */\nexport function listParentContextStep(\n resourceType: string,\n stepIndex: number,\n parents: string[],\n profile?: string,\n runner: CliRunner = defaultRunner,\n): DrillStep | null {\n const chain = PARENT_CONTEXT_CHAINS[resourceType];\n if (!chain || stepIndex >= chain.length) return null;\n const step = chain[stepIndex];\n // Prior picks become positional CLI args; a value starting with `-` would be\n // parsed as a flag, so refuse it (empty step → free-text fallback). Real\n // catalog/schema/scope/endpoint names never start with `-`.\n if (parents.some((p) => p.startsWith(\"-\"))) {\n return { key: step.key, choices: [] };\n }\n const spec = step.list(parents);\n return {\n key: step.key,\n choices: runList(\n spec.command,\n spec.idField,\n spec.labelField,\n profile,\n runner,\n ),\n };\n}\n\n/** Number of drill-down steps for a parent-context type (0 if not one). */\nexport function parentContextDepth(resourceType: string): number {\n return PARENT_CONTEXT_CHAINS[resourceType]?.length ?? 0;\n}\n\n/**\n * Builds the final resource identifier from the values picked across a\n * drill-down. Most types end on a self-qualified id (volume/uc_function list\n * `full_name`; a vector-search index name is already catalog.schema-qualified),\n * so the last pick is the whole answer. A `secret` is addressed by both its\n * scope and key (`scope/key`) — returning only the key drops the scope and\n * yields a value that can't locate the secret — so its picks are joined.\n */\nexport function composeResourceId(\n resourceType: string,\n picks: string[],\n): string {\n if (resourceType === \"secret\") return picks.join(\"/\");\n return picks[picks.length - 1];\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.js","names":[],"sources":["../../../src/cli/commands/setup.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command } from \"commander\";\n\nconst PACKAGES = [\n { name: \"@databricks/appkit\", description: \"Backend SDK\" },\n {\n name: \"@databricks/appkit-ui\",\n description: \"UI Integration, Charts, Tables, SSE, and more.\",\n },\n];\n\nconst SECTION_START = \"<!-- appkit-instructions-start -->\";\nconst SECTION_END = \"<!-- appkit-instructions-end -->\";\n\n/**\n * Find which AppKit packages are installed by checking for package.json\n */\nfunction findInstalledPackages() {\n const cwd = process.cwd();\n const installed = [];\n\n for (const pkg of PACKAGES) {\n const packagePath = path.join(\n cwd,\n \"node_modules\",\n pkg.name,\n \"package.json\",\n );\n if (fs.existsSync(packagePath)) {\n installed.push(pkg);\n }\n }\n\n return installed;\n}\n\n/**\n * Generate the AppKit section content\n */\nfunction generateSection(packages: typeof PACKAGES) {\n const links = packages\n .map((pkg) => {\n const docPath = `./node_modules/${pkg.name}/CLAUDE.md`;\n return `- **${pkg.name}** (${pkg.description}): [${docPath}](${docPath})`;\n })\n .join(\"\\n\");\n\n return `${SECTION_START}\n## Databricks AppKit\n\nThis project uses Databricks AppKit packages. For AI assistant guidance on using these packages, refer to:\n\n${links}\n\n### Databricks Skills\n\nFor enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:\n\n\\`\\`\\`bash\ndatabricks aitools install\n\\`\\`\\`\n${SECTION_END}`;\n}\n\n/**\n * Generate standalone CLAUDE.md content (when no existing file)\n */\nfunction generateStandalone(packages: typeof PACKAGES) {\n const links = packages\n .map((pkg) => {\n const docPath = `./node_modules/${pkg.name}/CLAUDE.md`;\n return `- **${pkg.name}** (${pkg.description}): [${docPath}](${docPath})`;\n })\n .join(\"\\n\");\n\n return `# AI Assistant Instructions\n\n${SECTION_START}\n## Databricks AppKit\n\nThis project uses Databricks AppKit packages. For AI assistant guidance on using these packages, refer to:\n\n${links}\n\n### Databricks Skills\n\nFor enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:\n\n\\`\\`\\`bash\ndatabricks aitools install\n\\`\\`\\`\n${SECTION_END}\n`;\n}\n\n/**\n * Update existing content with AppKit section\n */\nfunction updateContent(existingContent: string, packages: typeof PACKAGES) {\n const newSection = generateSection(packages);\n\n // Check if AppKit section already exists\n const startIndex = existingContent.indexOf(SECTION_START);\n const endIndex = existingContent.indexOf(SECTION_END);\n\n if (startIndex !== -1 && endIndex !== -1) {\n // Replace existing section\n const before = existingContent.substring(0, startIndex);\n const after = existingContent.substring(endIndex + SECTION_END.length);\n return before + newSection + after;\n }\n\n // Append section to end\n return `${existingContent.trimEnd()}\\n\\n${newSection}\\n`;\n}\n\n/**\n * Setup command implementation\n */\nfunction runSetup(options: { write?: boolean }) {\n const shouldWrite = options.write;\n\n // Find installed packages\n const installed = findInstalledPackages();\n\n if (installed.length === 0) {\n console.log(\"No @databricks/appkit packages found in node_modules.\");\n console.log(\"\\nInstall at least one of:\");\n for (const pkg of PACKAGES) {\n console.log(` npm install ${pkg.name}`);\n }\n process.exit(1);\n }\n\n console.log(\"Detected packages:\");\n installed.forEach((pkg) => {\n console.log(` ✓ ${pkg.name}`);\n });\n\n const claudePath = path.join(process.cwd(), \"CLAUDE.md\");\n const existingContent = fs.existsSync(claudePath)\n ? fs.readFileSync(claudePath, \"utf-8\")\n : null;\n\n let finalContent: string;\n let action: string;\n\n if (existingContent) {\n finalContent = updateContent(existingContent, installed);\n action = existingContent.includes(SECTION_START) ? \"Updated\" : \"Added to\";\n } else {\n finalContent = generateStandalone(installed);\n action = \"Created\";\n }\n\n if (shouldWrite) {\n fs.writeFileSync(claudePath, finalContent);\n console.log(`\\n✓ ${action} CLAUDE.md`);\n console.log(` Path: ${claudePath}`);\n } else {\n console.log(\"\\nTo create/update CLAUDE.md, run:\");\n console.log(\" npx appkit setup --write\\n\");\n\n if (existingContent) {\n console.log(\n `This will ${\n existingContent.includes(SECTION_START)\n ? \"update the existing\"\n : \"add a new\"\n } AppKit section.\\n`,\n );\n }\n\n console.log(\"Preview of AppKit section:\\n\");\n console.log(\"─\".repeat(50));\n console.log(generateSection(installed));\n console.log(\"─\".repeat(50));\n }\n}\n\nexport const setupCommand = new Command(\"setup\")\n .description(\"Setup CLAUDE.md with AppKit package references\")\n .option(\"-w, --write\", \"Create or update CLAUDE.md file in current directory\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit setup\n $ appkit setup --write`,\n )\n .action(runSetup);\n"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"setup.js","names":[],"sources":["../../../src/cli/commands/setup.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { Command } from \"commander\";\n\nconst PACKAGES = [\n { name: \"@databricks/appkit\", description: \"Backend SDK\" },\n {\n name: \"@databricks/appkit-ui\",\n description: \"UI Integration, Charts, Tables, SSE, and more.\",\n },\n];\n\nconst SECTION_START = \"<!-- appkit-instructions-start -->\";\nconst SECTION_END = \"<!-- appkit-instructions-end -->\";\n\n/**\n * Find which AppKit packages are installed by checking for package.json\n */\nfunction findInstalledPackages() {\n const cwd = process.cwd();\n const installed = [];\n\n for (const pkg of PACKAGES) {\n const packagePath = path.join(\n cwd,\n \"node_modules\",\n pkg.name,\n \"package.json\",\n );\n if (fs.existsSync(packagePath)) {\n installed.push(pkg);\n }\n }\n\n return installed;\n}\n\n/**\n * Generate the AppKit section content\n */\nfunction generateSection(packages: typeof PACKAGES) {\n const links = packages\n .map((pkg) => {\n const docPath = `./node_modules/${pkg.name}/CLAUDE.md`;\n return `- **${pkg.name}** (${pkg.description}): [${docPath}](${docPath})`;\n })\n .join(\"\\n\");\n\n return `${SECTION_START}\n## Databricks AppKit\n\nThis project uses Databricks AppKit packages. For AI assistant guidance on using these packages, refer to:\n\n${links}\n\n### Databricks Skills\n\nFor enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:\n\n\\`\\`\\`bash\ndatabricks aitools install\n\\`\\`\\`\n${SECTION_END}`;\n}\n\n/**\n * Generate standalone CLAUDE.md content (when no existing file)\n */\nfunction generateStandalone(packages: typeof PACKAGES) {\n const links = packages\n .map((pkg) => {\n const docPath = `./node_modules/${pkg.name}/CLAUDE.md`;\n return `- **${pkg.name}** (${pkg.description}): [${docPath}](${docPath})`;\n })\n .join(\"\\n\");\n\n return `# AI Assistant Instructions\n\n${SECTION_START}\n## Databricks AppKit\n\nThis project uses Databricks AppKit packages. For AI assistant guidance on using these packages, refer to:\n\n${links}\n\n### Databricks Skills\n\nFor enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:\n\n\\`\\`\\`bash\ndatabricks aitools install\n\\`\\`\\`\n${SECTION_END}\n`;\n}\n\n/**\n * Update existing content with AppKit section\n */\nfunction updateContent(existingContent: string, packages: typeof PACKAGES) {\n const newSection = generateSection(packages);\n\n // Check if AppKit section already exists\n const startIndex = existingContent.indexOf(SECTION_START);\n const endIndex = existingContent.indexOf(SECTION_END);\n\n if (startIndex !== -1 && endIndex !== -1) {\n // Replace existing section\n const before = existingContent.substring(0, startIndex);\n const after = existingContent.substring(endIndex + SECTION_END.length);\n return before + newSection + after;\n }\n\n // Append section to end\n return `${existingContent.trimEnd()}\\n\\n${newSection}\\n`;\n}\n\n/**\n * Setup command implementation\n */\nfunction runSetup(options: { write?: boolean }) {\n const shouldWrite = options.write;\n\n // Find installed packages\n const installed = findInstalledPackages();\n\n if (installed.length === 0) {\n console.log(\"No @databricks/appkit packages found in node_modules.\");\n console.log(\"\\nInstall at least one of:\");\n for (const pkg of PACKAGES) {\n console.log(` npm install ${pkg.name}`);\n }\n process.exit(1);\n }\n\n console.log(\"Detected packages:\");\n installed.forEach((pkg) => {\n console.log(` ✓ ${pkg.name}`);\n });\n\n const claudePath = path.join(process.cwd(), \"CLAUDE.md\");\n const existingContent = fs.existsSync(claudePath)\n ? fs.readFileSync(claudePath, \"utf-8\")\n : null;\n\n let finalContent: string;\n let action: string;\n\n if (existingContent) {\n finalContent = updateContent(existingContent, installed);\n action = existingContent.includes(SECTION_START) ? \"Updated\" : \"Added to\";\n } else {\n finalContent = generateStandalone(installed);\n action = \"Created\";\n }\n\n if (shouldWrite) {\n fs.writeFileSync(claudePath, finalContent);\n console.log(`\\n✓ ${action} CLAUDE.md`);\n console.log(` Path: ${claudePath}`);\n } else {\n console.log(\"\\nTo create/update CLAUDE.md, run:\");\n console.log(\" npx appkit setup --write\\n\");\n\n if (existingContent) {\n console.log(\n `This will ${\n existingContent.includes(SECTION_START)\n ? \"update the existing\"\n : \"add a new\"\n } AppKit section.\\n`,\n );\n }\n\n console.log(\"Preview of AppKit section:\\n\");\n console.log(\"─\".repeat(50));\n console.log(generateSection(installed));\n console.log(\"─\".repeat(50));\n }\n}\n\nexport const setupCommand = new Command(\"setup\")\n .description(\"Setup CLAUDE.md with AppKit package references\")\n .option(\"-w, --write\", \"Create or update CLAUDE.md file in current directory\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit setup\n $ appkit setup --write`,\n )\n .action(runSetup);\n"],"mappings":"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"}
|
package/dist/cli/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport \"dotenv/config\";\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Command } from \"commander\";\nimport { codemodCommand } from \"./commands/codemod/index.js\";\nimport { docsCommand } from \"./commands/docs.js\";\nimport { doctorCommand } from \"./commands/doctor/index.js\";\nimport { generateTypesCommand } from \"./commands/generate-types.js\";\nimport { lintCommand } from \"./commands/lint.js\";\nimport { pluginCommand } from \"./commands/plugin/index.js\";\nimport { addCommand } from \"./commands/registry/add.js\";\nimport { registryCommand } from \"./commands/registry/index.js\";\nimport { setupCommand } from \"./commands/setup.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst pkgPath = join(__dirname, \"../../package.json\");\nconst pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n\nconst cmd = new Command();\n\ncmd\n .name(\"appkit\")\n .description(\"CLI tools for Databricks AppKit\")\n .version(pkg.version);\n\ncmd.addCommand(setupCommand);\ncmd.addCommand(generateTypesCommand);\ncmd.addCommand(lintCommand);\ncmd.addCommand(docsCommand);\ncmd.addCommand(pluginCommand);\ncmd.addCommand(codemodCommand);\ncmd.addCommand(doctorCommand);\n// Registry commands are executable but hidden from --help while the feature\n// is still in development (registry + add work end-to-end but aren't announced).\ncmd.addCommand(registryCommand, { hidden: true });\ncmd.addCommand(addCommand, { hidden: true });\n\nawait cmd.parseAsync();\n"],"mappings":";;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport \"dotenv/config\";\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { Command } from \"commander\";\n\nimport { codemodCommand } from \"./commands/codemod/index.js\";\nimport { docsCommand } from \"./commands/docs.js\";\nimport { doctorCommand } from \"./commands/doctor/index.js\";\nimport { generateTypesCommand } from \"./commands/generate-types.js\";\nimport { lintCommand } from \"./commands/lint.js\";\nimport { pluginCommand } from \"./commands/plugin/index.js\";\nimport { addCommand } from \"./commands/registry/add.js\";\nimport { registryCommand } from \"./commands/registry/index.js\";\nimport { setupCommand } from \"./commands/setup.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst pkgPath = join(__dirname, \"../../package.json\");\nconst pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n\nconst cmd = new Command();\n\ncmd\n .name(\"appkit\")\n .description(\"CLI tools for Databricks AppKit\")\n .version(pkg.version);\n\ncmd.addCommand(setupCommand);\ncmd.addCommand(generateTypesCommand);\ncmd.addCommand(lintCommand);\ncmd.addCommand(docsCommand);\ncmd.addCommand(pluginCommand);\ncmd.addCommand(codemodCommand);\ncmd.addCommand(doctorCommand);\n// Registry commands are executable but hidden from --help while the feature\n// is still in development (registry + add work end-to-end but aren't announced).\ncmd.addCommand(registryCommand, { hidden: true });\ncmd.addCommand(addCommand, { hidden: true });\n\nawait cmd.parseAsync();\n"],"mappings":";;;;;;;;;;;;;;;;;AAmBA,MAAM,UAAU,KADE,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EACzB,qBAAqB;AACrD,MAAM,MAAM,KAAK,MAAM,aAAa,SAAS,QAAQ,CAAC;AAEtD,MAAM,MAAM,IAAI,SAAS;AAEzB,IACG,KAAK,SAAS,CACd,YAAY,kCAAkC,CAC9C,QAAQ,IAAI,QAAQ;AAEvB,IAAI,WAAW,aAAa;AAC5B,IAAI,WAAW,qBAAqB;AACpC,IAAI,WAAW,YAAY;AAC3B,IAAI,WAAW,YAAY;AAC3B,IAAI,WAAW,cAAc;AAC7B,IAAI,WAAW,eAAe;AAC9B,IAAI,WAAW,cAAc;AAG7B,IAAI,WAAW,iBAAiB,EAAE,QAAQ,MAAM,CAAC;AACjD,IAAI,WAAW,YAAY,EAAE,QAAQ,MAAM,CAAC;AAE5C,MAAM,IAAI,YAAY"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"arrow-client.d.ts","names":[],"sources":["../../../src/js/arrow/arrow-client.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"arrow-client.d.ts","names":[],"sources":["../../../src/js/arrow/arrow-client.ts"],"mappings":";;;;cAoBa,WAAA;EAAW;;;;;;;;;;;;;;EAAA,OAeT,kBAAA,CACX,MAAA,EAAQ,UAAA,EACR,WAAA,cACC,OAAA,CAAQ,OAAA;EAuSC;;;;;;;;;;;;;EAAA,OA3PC,oBAAA,CACX,GAAA,UACA,OAAA,GAAU,MAAA,mBACT,OAAA,CAAQ,OAAA;EADT;;;;;;;;;;;EAAA,OA0BK,kBAAA,CAAmB,KAAA,EAAO,OAAA;;;;EA4CK;;;;;;;;;;;;EAAA,OAvB/B,mBAAA,CAAoB,KAAA,EAAO,OAAA,GAAQ,MAAA;EA0M9B;;;;;;AAwBd;;;EAxBc,OAnLL,gBAAA,CAAiB,KAAA,EAAO,OAAA,EAAO,IAAA,UAAc,KAAA;;cAyN1C,MAAA;EAAA;;;;;;;;;;;;;;;;;;;SAnKH,qBAAA,CACL,KAAA,EAAO,OAAA,EACP,WAAA,+BACC,cAAA;IAAmB,SAAA;EAAA;;;;;;;;;;;;;SAwHT,UAAA,CACX,GAAA,UACA,OAAA,GAAU,MAAA,mBACT,OAAA,CAAQ,UAAA;AAAA;AAAA,UAuBI,cAAA;;EAEf,MAAA;;EAEA,OAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"arrow-client.js","names":[],"sources":["../../../src/js/arrow/arrow-client.ts"],"sourcesContent":["import type { Field, Table, Vector } from \"apache-arrow\";\nimport {\n DATE_FIELD_PATTERNS,\n METADATA_DATE_PATTERNS,\n NAME_FIELD_PATTERNS,\n} from \"../constants\";\nimport {\n getArrowModule,\n getDecimalTypeId,\n getTypeIdSets,\n initializeTypeIdSets,\n} from \"./lazy-arrow\";\n\n// Re-export for backward compatibility\nexport { DATE_FIELD_PATTERNS, NAME_FIELD_PATTERNS };\n\n// Re-export Table type for consumers\nexport type { Table, Field };\n\nexport class ArrowClient {\n /**\n * Processes an Arrow IPC buffer into a Table.\n * Lazily loads the Apache Arrow library on first use.\n *\n * @param buffer - The Arrow IPC format buffer\n * @param columnNames - Optional real column names to relabel the schema\n * with. Databricks encodes ARROW_STREAM schemas positionally (`col_0`,\n * …); the analytics server sends the manifest names in a response header\n * so charts can look columns up by their real name. Relabels in-place by\n * rebuilding the Table around the existing vectors (no re-decode). A\n * no-op when the names already match, the count differs, or the names\n * aren't unique.\n * @returns Promise resolving to an Arrow Table\n */\n static async processArrowBuffer(\n buffer: Uint8Array,\n columnNames?: string[],\n ): Promise<Table> {\n try {\n const arrow = await getArrowModule();\n // Initialize type ID sets now that Arrow is loaded\n await initializeTypeIdSets();\n const table = arrow.tableFromIPC(buffer);\n\n if (!columnNames || columnNames.length !== table.numCols) return table;\n const fields = table.schema.fields;\n const targetNames = columnNames.map((n, i) =>\n n && n.length > 0 ? n : fields[i].name,\n );\n if (targetNames.every((n, i) => n === fields[i].name)) return table;\n if (new Set(targetNames).size !== targetNames.length) return table;\n\n const children: Record<string, Vector> = {};\n for (let i = 0; i < fields.length; i++) {\n const child = table.getChildAt(i);\n if (!child) return table;\n children[targetNames[i]] = child;\n }\n return new arrow.Table(children);\n } catch (error) {\n throw new Error(\n `Failed to process Arrow buffer: ${\n error instanceof Error ? error.message : \"Unknown error\"\n }`,\n );\n }\n }\n\n /**\n * Fetches Arrow data from a URL and processes it into a Table.\n * Convenience method that combines fetchArrow and processArrowBuffer.\n *\n * @param url - URL to fetch Arrow data from\n * @param headers - Optional HTTP headers to include in the request\n * @returns Promise resolving to an Arrow Table\n * @example\n * ```typescript\n * const table = await ArrowClient.fetchAndProcessArrow('/api/data/arrow');\n * console.log(`Loaded ${table.numRows} rows`);\n * ```\n */\n static async fetchAndProcessArrow(\n url: string,\n headers?: Record<string, string>,\n ): Promise<Table> {\n try {\n const buffer = await ArrowClient.fetchArrow(url, headers);\n\n return ArrowClient.processArrowBuffer(buffer);\n } catch (error) {\n throw new Error(\n `Failed to fetch Arrow data: ${\n error instanceof Error ? error.message : \"Unknown error\"\n }`,\n );\n }\n }\n\n /**\n * Extracts field metadata (name and type) from an Arrow Table.\n *\n * @param table - Arrow Table to extract fields from\n * @returns Array of field metadata objects\n * @example\n * ```typescript\n * const fields = ArrowClient.extractArrowFields(table);\n * // [{ name: \"date\", type: Date32 }, { name: \"value\", type: Float64 }]\n * ```\n */\n static extractArrowFields(table: Table) {\n return table.schema.fields.map((field: Field) => {\n return {\n name: field.name,\n type: field.type,\n };\n });\n }\n\n /**\n * Extracts all columns from an Arrow Table as JavaScript arrays.\n * Each column is converted to a native JavaScript array.\n *\n * @param table - Arrow Table to extract columns from\n * @returns Object mapping column names to arrays\n * @example\n * ```typescript\n * const columns = ArrowClient.extractArrowColumns(table);\n * // { date: [\"2024-01-01\", \"2024-01-02\"], value: [100, 200] }\n * ```\n */\n static extractArrowColumns(table: Table): Record<string, any> {\n const cols: Record<string, any> = {};\n\n for (const field of table.schema.fields) {\n const child = table.getChild(field.name);\n\n if (child) {\n cols[field.name] = child.toArray();\n }\n }\n\n return cols;\n }\n\n /**\n * Extracts chart data from Arrow table.\n * Uses get(i) to properly handle complex types like Decimal128.\n * Applies decimal scaling for DECIMAL types.\n *\n * Note: This method assumes Arrow has been loaded (via processArrowBuffer).\n *\n * @returns xData for axis, yDataMap for series data\n */\n static extractChartData(table: Table, xKey: string, yKeys: string[]) {\n // Early exit for empty tables - return cached empty object\n if (table.numRows === 0) {\n return EMPTY_RESULT;\n }\n\n // Get the Decimal type ID (Arrow must be loaded to have a Table)\n const decimalType = getDecimalTypeId();\n\n // Build a map of field name -> pre-computed divisor (10^scale) for decimal types\n const decimalDivisors = new Map<string, number>();\n for (const field of table.schema.fields) {\n if (field.typeId === decimalType) {\n const decType = field.type as { scale: number };\n if (typeof decType.scale === \"number\") {\n // Pre-compute divisor once per field instead of per-column call\n decimalDivisors.set(field.name, 10 ** decType.scale);\n }\n }\n }\n\n // Extract X column using proper value extraction\n const xCol = table.getChild(xKey);\n const xData = extractColumnValues(xCol, decimalDivisors.get(xKey));\n\n // Extract Y columns using proper value extraction\n const yDataMap: Record<string, (string | number)[]> = {};\n for (let i = 0; i < yKeys.length; i++) {\n const key = yKeys[i];\n const col = table.getChild(key);\n yDataMap[key] = extractColumnValues(col, decimalDivisors.get(key));\n }\n\n return { xData, yDataMap };\n }\n\n /**\n * Automatically detect which fields to use for chart axes from an Arrow table\n * Uses the schema's type information for accurate field detection\n *\n * Note: This method assumes Arrow has been loaded (via processArrowBuffer).\n *\n * @param table - Arrow Table to analyze\n * @param orientation - Chart orientation (\"vertical\" for time-series, \"horizontal\" for categorical)\n * @returns Object containing the detected fields\n * @example\n * // Time-series data\n * detectFieldsFromArrow(timeSeriesTable)\n * // { xField: \"date\", yFields: [\"revenue\", \"cost\"], chartType: \"timeseries\" }\n *\n * // Categorical data\n * detectFieldsFromArrow(categoricalTable)\n * // { xField: \"app_name\", yFields: [\"totalSpend\"], chartType: \"categorical\" }\n */\n static detectFieldsFromArrow(\n table: Table,\n orientation?: \"vertical\" | \"horizontal\",\n ): DetectedFields & { chartType: \"timeseries\" | \"categorical\" } {\n const fields = table.schema.fields;\n\n if (fields.length === 0) {\n return { xField: \"x\", yFields: [\"y\"], chartType: \"categorical\" };\n }\n\n const fieldNames = fields.map((f) => f.name);\n\n // Get type ID sets (Arrow must be loaded to have a Table)\n const typeIdSets = getTypeIdSets();\n\n // Categorize fields by their Arrow type\n const temporalFields: string[] = [];\n const numericFields: string[] = [];\n const stringFields: string[] = [];\n\n for (const field of fields) {\n const typeId = field.typeId;\n\n if (typeIdSets.temporal.has(typeId)) {\n temporalFields.push(field.name);\n } else if (typeIdSets.numeric.has(typeId)) {\n numericFields.push(field.name);\n } else if (typeIdSets.string.has(typeId)) {\n stringFields.push(field.name);\n }\n }\n\n // Detect name/category fields: string fields matching name patterns\n let nameFields = stringFields.filter((name) =>\n NAME_FIELD_PATTERNS.some((pattern) =>\n name.toLowerCase().includes(pattern),\n ),\n );\n\n // Fallback: use any string field that doesn't end with _id\n if (nameFields.length === 0) {\n nameFields = stringFields.filter(\n (name) => !name.toLowerCase().endsWith(\"_id\"),\n );\n }\n\n // Separate temporal fields into \"chart-worthy\" dates vs metadata dates\n const chartDateFields = temporalFields.filter(\n (name) =>\n !METADATA_DATE_PATTERNS.some((pattern) =>\n name.toLowerCase().includes(pattern),\n ),\n );\n const metadataDateFields = temporalFields.filter((name) =>\n METADATA_DATE_PATTERNS.some((pattern) =>\n name.toLowerCase().includes(pattern),\n ),\n );\n\n // Also check string fields for date patterns (but not metadata patterns)\n const stringDateFields = stringFields.filter(\n (name) =>\n DATE_FIELD_PATTERNS.some((pattern) =>\n name.toLowerCase().includes(pattern),\n ) &&\n !METADATA_DATE_PATTERNS.some((pattern) =>\n name.toLowerCase().includes(pattern),\n ),\n );\n\n const primaryDateFields = [...chartDateFields, ...stringDateFields];\n\n // Determine chart type: if we have good date fields for charting, it's time-series\n // If we only have metadata dates (like createdAt) and name fields, it's categorical\n const isTimeSeries =\n primaryDateFields.length > 0 && orientation !== \"horizontal\";\n const isCategorical =\n nameFields.length > 0 &&\n (primaryDateFields.length === 0 || orientation === \"horizontal\");\n\n if (orientation === \"horizontal\" || isCategorical) {\n // Categorical: x is name/category field, y is numeric field\n const xField =\n nameFields[0] ||\n primaryDateFields[0] ||\n metadataDateFields[0] ||\n fieldNames[0];\n const yFields =\n numericFields.length > 0\n ? numericFields\n : fieldNames.filter((k) => k !== xField);\n return { xField, yFields, chartType: \"categorical\" };\n }\n\n // Time-series (default): x is date/time field, y is numeric field\n const xField =\n primaryDateFields[0] ||\n metadataDateFields[0] ||\n nameFields[0] ||\n fieldNames[0];\n const yFields =\n numericFields.length > 0\n ? numericFields\n : fieldNames.filter((k) => k !== xField);\n return {\n xField,\n yFields,\n chartType: isTimeSeries ? \"timeseries\" : \"categorical\",\n };\n }\n\n /**\n * Fetches raw Arrow IPC data from a URL.\n *\n * @param url - URL to fetch Arrow data from\n * @param headers - Optional HTTP headers to include in the request\n * @returns Promise resolving to the raw Arrow buffer as Uint8Array\n * @example\n * ```typescript\n * const buffer = await ArrowClient.fetchArrow('/api/data/arrow');\n * const table = await ArrowClient.processArrowBuffer(buffer);\n * ```\n */\n static async fetchArrow(\n url: string,\n headers?: Record<string, string>,\n ): Promise<Uint8Array> {\n try {\n const response = await fetch(url, {\n headers: { \"Content-Type\": \"application/octet-stream\", ...headers },\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const buffer = await response.arrayBuffer();\n\n return new Uint8Array(buffer);\n } catch (error) {\n throw new Error(\n `Failed to fetch Arrow data: ${\n error instanceof Error ? error.message : \"Unknown error\"\n }`,\n );\n }\n }\n}\n\nexport interface DetectedFields {\n /** X field */\n xField: string;\n /** Y fields */\n yFields: string[];\n}\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n// Cached empty result to avoid allocations\nconst EMPTY_RESULT: {\n xData: (string | number)[];\n yDataMap: Record<string, (string | number)[]>;\n} = {\n xData: [],\n yDataMap: {},\n};\n\n/**\n * Extracts values from an Arrow Vector properly.\n * Uses get(i) to handle complex types like Decimal128 correctly.\n * toArray() doesn't work properly for Decimal types - it returns raw bytes.\n *\n * @param col - The Arrow column/vector\n * @param divisor - Pre-computed divisor for DECIMAL types (10^scale)\n */\nfunction extractColumnValues(\n col: { length: number; get: (i: number) => unknown } | null | undefined,\n divisor?: number,\n): (string | number)[] {\n if (!col) return [];\n\n // Pre-allocate array for better performance with large datasets\n const len = col.length;\n const result: (string | number)[] = new Array(len);\n\n for (let i = 0; i < len; i++) {\n const val = col.get(i);\n if (val === null || val === undefined) {\n result[i] = 0;\n } else if (typeof val === \"bigint\") {\n // Apply decimal scaling if needed\n const num = Number(val);\n result[i] = divisor !== undefined ? num / divisor : num;\n } else if (typeof val === \"number\") {\n // Apply decimal scaling if needed\n result[i] = divisor !== undefined ? val / divisor : val;\n } else if (typeof val === \"string\") {\n result[i] = val;\n } else {\n // For complex types (like Decimal), try to convert to number\n const num = Number(val);\n result[i] = divisor !== undefined ? num / divisor : num;\n }\n }\n return result;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"arrow-client.js","names":[],"sources":["../../../src/js/arrow/arrow-client.ts"],"sourcesContent":["import type { Field, Table, Vector } from \"apache-arrow\";\n\nimport {\n DATE_FIELD_PATTERNS,\n METADATA_DATE_PATTERNS,\n NAME_FIELD_PATTERNS,\n} from \"../constants\";\nimport {\n getArrowModule,\n getDecimalTypeId,\n getTypeIdSets,\n initializeTypeIdSets,\n} from \"./lazy-arrow\";\n\n// Re-export for backward compatibility\nexport { DATE_FIELD_PATTERNS, NAME_FIELD_PATTERNS };\n\n// Re-export Table type for consumers\nexport type { Table, Field };\n\nexport class ArrowClient {\n /**\n * Processes an Arrow IPC buffer into a Table.\n * Lazily loads the Apache Arrow library on first use.\n *\n * @param buffer - The Arrow IPC format buffer\n * @param columnNames - Optional real column names to relabel the schema\n * with. Databricks encodes ARROW_STREAM schemas positionally (`col_0`,\n * …); the analytics server sends the manifest names in a response header\n * so charts can look columns up by their real name. Relabels in-place by\n * rebuilding the Table around the existing vectors (no re-decode). A\n * no-op when the names already match, the count differs, or the names\n * aren't unique.\n * @returns Promise resolving to an Arrow Table\n */\n static async processArrowBuffer(\n buffer: Uint8Array,\n columnNames?: string[],\n ): Promise<Table> {\n try {\n const arrow = await getArrowModule();\n // Initialize type ID sets now that Arrow is loaded\n await initializeTypeIdSets();\n const table = arrow.tableFromIPC(buffer);\n\n if (!columnNames || columnNames.length !== table.numCols) return table;\n const fields = table.schema.fields;\n const targetNames = columnNames.map((n, i) =>\n n && n.length > 0 ? n : fields[i].name,\n );\n if (targetNames.every((n, i) => n === fields[i].name)) return table;\n if (new Set(targetNames).size !== targetNames.length) return table;\n\n const children: Record<string, Vector> = {};\n for (let i = 0; i < fields.length; i++) {\n const child = table.getChildAt(i);\n if (!child) return table;\n children[targetNames[i]] = child;\n }\n return new arrow.Table(children);\n } catch (error) {\n throw new Error(\n `Failed to process Arrow buffer: ${\n error instanceof Error ? error.message : \"Unknown error\"\n }`,\n );\n }\n }\n\n /**\n * Fetches Arrow data from a URL and processes it into a Table.\n * Convenience method that combines fetchArrow and processArrowBuffer.\n *\n * @param url - URL to fetch Arrow data from\n * @param headers - Optional HTTP headers to include in the request\n * @returns Promise resolving to an Arrow Table\n * @example\n * ```typescript\n * const table = await ArrowClient.fetchAndProcessArrow('/api/data/arrow');\n * console.log(`Loaded ${table.numRows} rows`);\n * ```\n */\n static async fetchAndProcessArrow(\n url: string,\n headers?: Record<string, string>,\n ): Promise<Table> {\n try {\n const buffer = await ArrowClient.fetchArrow(url, headers);\n\n return ArrowClient.processArrowBuffer(buffer);\n } catch (error) {\n throw new Error(\n `Failed to fetch Arrow data: ${\n error instanceof Error ? error.message : \"Unknown error\"\n }`,\n );\n }\n }\n\n /**\n * Extracts field metadata (name and type) from an Arrow Table.\n *\n * @param table - Arrow Table to extract fields from\n * @returns Array of field metadata objects\n * @example\n * ```typescript\n * const fields = ArrowClient.extractArrowFields(table);\n * // [{ name: \"date\", type: Date32 }, { name: \"value\", type: Float64 }]\n * ```\n */\n static extractArrowFields(table: Table) {\n return table.schema.fields.map((field: Field) => {\n return {\n name: field.name,\n type: field.type,\n };\n });\n }\n\n /**\n * Extracts all columns from an Arrow Table as JavaScript arrays.\n * Each column is converted to a native JavaScript array.\n *\n * @param table - Arrow Table to extract columns from\n * @returns Object mapping column names to arrays\n * @example\n * ```typescript\n * const columns = ArrowClient.extractArrowColumns(table);\n * // { date: [\"2024-01-01\", \"2024-01-02\"], value: [100, 200] }\n * ```\n */\n static extractArrowColumns(table: Table): Record<string, any> {\n const cols: Record<string, any> = {};\n\n for (const field of table.schema.fields) {\n const child = table.getChild(field.name);\n\n if (child) {\n cols[field.name] = child.toArray();\n }\n }\n\n return cols;\n }\n\n /**\n * Extracts chart data from Arrow table.\n * Uses get(i) to properly handle complex types like Decimal128.\n * Applies decimal scaling for DECIMAL types.\n *\n * Note: This method assumes Arrow has been loaded (via processArrowBuffer).\n *\n * @returns xData for axis, yDataMap for series data\n */\n static extractChartData(table: Table, xKey: string, yKeys: string[]) {\n // Early exit for empty tables - return cached empty object\n if (table.numRows === 0) {\n return EMPTY_RESULT;\n }\n\n // Get the Decimal type ID (Arrow must be loaded to have a Table)\n const decimalType = getDecimalTypeId();\n\n // Build a map of field name -> pre-computed divisor (10^scale) for decimal types\n const decimalDivisors = new Map<string, number>();\n for (const field of table.schema.fields) {\n if (field.typeId === decimalType) {\n const decType = field.type as { scale: number };\n if (typeof decType.scale === \"number\") {\n // Pre-compute divisor once per field instead of per-column call\n decimalDivisors.set(field.name, 10 ** decType.scale);\n }\n }\n }\n\n // Extract X column using proper value extraction\n const xCol = table.getChild(xKey);\n const xData = extractColumnValues(xCol, decimalDivisors.get(xKey));\n\n // Extract Y columns using proper value extraction\n const yDataMap: Record<string, (string | number)[]> = {};\n for (let i = 0; i < yKeys.length; i++) {\n const key = yKeys[i];\n const col = table.getChild(key);\n yDataMap[key] = extractColumnValues(col, decimalDivisors.get(key));\n }\n\n return { xData, yDataMap };\n }\n\n /**\n * Automatically detect which fields to use for chart axes from an Arrow table\n * Uses the schema's type information for accurate field detection\n *\n * Note: This method assumes Arrow has been loaded (via processArrowBuffer).\n *\n * @param table - Arrow Table to analyze\n * @param orientation - Chart orientation (\"vertical\" for time-series, \"horizontal\" for categorical)\n * @returns Object containing the detected fields\n * @example\n * // Time-series data\n * detectFieldsFromArrow(timeSeriesTable)\n * // { xField: \"date\", yFields: [\"revenue\", \"cost\"], chartType: \"timeseries\" }\n *\n * // Categorical data\n * detectFieldsFromArrow(categoricalTable)\n * // { xField: \"app_name\", yFields: [\"totalSpend\"], chartType: \"categorical\" }\n */\n static detectFieldsFromArrow(\n table: Table,\n orientation?: \"vertical\" | \"horizontal\",\n ): DetectedFields & { chartType: \"timeseries\" | \"categorical\" } {\n const fields = table.schema.fields;\n\n if (fields.length === 0) {\n return { xField: \"x\", yFields: [\"y\"], chartType: \"categorical\" };\n }\n\n const fieldNames = fields.map((f) => f.name);\n\n // Get type ID sets (Arrow must be loaded to have a Table)\n const typeIdSets = getTypeIdSets();\n\n // Categorize fields by their Arrow type\n const temporalFields: string[] = [];\n const numericFields: string[] = [];\n const stringFields: string[] = [];\n\n for (const field of fields) {\n const typeId = field.typeId;\n\n if (typeIdSets.temporal.has(typeId)) {\n temporalFields.push(field.name);\n } else if (typeIdSets.numeric.has(typeId)) {\n numericFields.push(field.name);\n } else if (typeIdSets.string.has(typeId)) {\n stringFields.push(field.name);\n }\n }\n\n // Detect name/category fields: string fields matching name patterns\n let nameFields = stringFields.filter((name) =>\n NAME_FIELD_PATTERNS.some((pattern) =>\n name.toLowerCase().includes(pattern),\n ),\n );\n\n // Fallback: use any string field that doesn't end with _id\n if (nameFields.length === 0) {\n nameFields = stringFields.filter(\n (name) => !name.toLowerCase().endsWith(\"_id\"),\n );\n }\n\n // Separate temporal fields into \"chart-worthy\" dates vs metadata dates\n const chartDateFields = temporalFields.filter(\n (name) =>\n !METADATA_DATE_PATTERNS.some((pattern) =>\n name.toLowerCase().includes(pattern),\n ),\n );\n const metadataDateFields = temporalFields.filter((name) =>\n METADATA_DATE_PATTERNS.some((pattern) =>\n name.toLowerCase().includes(pattern),\n ),\n );\n\n // Also check string fields for date patterns (but not metadata patterns)\n const stringDateFields = stringFields.filter(\n (name) =>\n DATE_FIELD_PATTERNS.some((pattern) =>\n name.toLowerCase().includes(pattern),\n ) &&\n !METADATA_DATE_PATTERNS.some((pattern) =>\n name.toLowerCase().includes(pattern),\n ),\n );\n\n const primaryDateFields = [...chartDateFields, ...stringDateFields];\n\n // Determine chart type: if we have good date fields for charting, it's time-series\n // If we only have metadata dates (like createdAt) and name fields, it's categorical\n const isTimeSeries =\n primaryDateFields.length > 0 && orientation !== \"horizontal\";\n const isCategorical =\n nameFields.length > 0 &&\n (primaryDateFields.length === 0 || orientation === \"horizontal\");\n\n if (orientation === \"horizontal\" || isCategorical) {\n // Categorical: x is name/category field, y is numeric field\n const xField =\n nameFields[0] ||\n primaryDateFields[0] ||\n metadataDateFields[0] ||\n fieldNames[0];\n const yFields =\n numericFields.length > 0\n ? numericFields\n : fieldNames.filter((k) => k !== xField);\n return { xField, yFields, chartType: \"categorical\" };\n }\n\n // Time-series (default): x is date/time field, y is numeric field\n const xField =\n primaryDateFields[0] ||\n metadataDateFields[0] ||\n nameFields[0] ||\n fieldNames[0];\n const yFields =\n numericFields.length > 0\n ? numericFields\n : fieldNames.filter((k) => k !== xField);\n return {\n xField,\n yFields,\n chartType: isTimeSeries ? \"timeseries\" : \"categorical\",\n };\n }\n\n /**\n * Fetches raw Arrow IPC data from a URL.\n *\n * @param url - URL to fetch Arrow data from\n * @param headers - Optional HTTP headers to include in the request\n * @returns Promise resolving to the raw Arrow buffer as Uint8Array\n * @example\n * ```typescript\n * const buffer = await ArrowClient.fetchArrow('/api/data/arrow');\n * const table = await ArrowClient.processArrowBuffer(buffer);\n * ```\n */\n static async fetchArrow(\n url: string,\n headers?: Record<string, string>,\n ): Promise<Uint8Array> {\n try {\n const response = await fetch(url, {\n headers: { \"Content-Type\": \"application/octet-stream\", ...headers },\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const buffer = await response.arrayBuffer();\n\n return new Uint8Array(buffer);\n } catch (error) {\n throw new Error(\n `Failed to fetch Arrow data: ${\n error instanceof Error ? error.message : \"Unknown error\"\n }`,\n );\n }\n }\n}\n\nexport interface DetectedFields {\n /** X field */\n xField: string;\n /** Y fields */\n yFields: string[];\n}\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n// Cached empty result to avoid allocations\nconst EMPTY_RESULT: {\n xData: (string | number)[];\n yDataMap: Record<string, (string | number)[]>;\n} = {\n xData: [],\n yDataMap: {},\n};\n\n/**\n * Extracts values from an Arrow Vector properly.\n * Uses get(i) to handle complex types like Decimal128 correctly.\n * toArray() doesn't work properly for Decimal types - it returns raw bytes.\n *\n * @param col - The Arrow column/vector\n * @param divisor - Pre-computed divisor for DECIMAL types (10^scale)\n */\nfunction extractColumnValues(\n col: { length: number; get: (i: number) => unknown } | null | undefined,\n divisor?: number,\n): (string | number)[] {\n if (!col) return [];\n\n // Pre-allocate array for better performance with large datasets\n const len = col.length;\n const result: (string | number)[] = new Array(len);\n\n for (let i = 0; i < len; i++) {\n const val = col.get(i);\n if (val === null || val === undefined) {\n result[i] = 0;\n } else if (typeof val === \"bigint\") {\n // Apply decimal scaling if needed\n const num = Number(val);\n result[i] = divisor !== undefined ? num / divisor : num;\n } else if (typeof val === \"number\") {\n // Apply decimal scaling if needed\n result[i] = divisor !== undefined ? val / divisor : val;\n } else if (typeof val === \"string\") {\n result[i] = val;\n } else {\n // For complex types (like Decimal), try to convert to number\n const num = Number(val);\n result[i] = divisor !== undefined ? num / divisor : num;\n }\n }\n return result;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../../src/react/charts/area/index.tsx"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../../src/react/charts/area/index.tsx"],"mappings":";;;;;;;AAcA;;;;;;cAAa,SAAA;EAAA,QAA4D,cAAA,GAAA,kBAAA,CAAA,GAAA,CAAA,OAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/react/charts/area/index.tsx"],"sourcesContent":["import type { JSX } from \"react\";\nimport { createChart } from \"../create-chart\";\nimport type { AreaChartProps } from \"../types\";\n\n/**\n * Area Chart component for trend visualization with filled areas.\n *\n * **Important:** This component uses Apache ECharts architecture. Configure it via props, not by passing child components.\n *\n * **Best Practice:** Use the built-in data fetching by passing `queryKey` and `parameters` props instead of pre-fetching data with `useAnalyticsQuery`.\n *\n * Supports both query mode (queryKey + parameters) and data mode (static data).\n */\nexport const AreaChart = createChart<AreaChartProps>(\"area\", \"AreaChart\");\n\n// Type-only definition for documentation generation (not used at runtime)\n/**\n * Area Chart component for trend visualization with filled areas.\n *\n * **Important:** This component uses Apache ECharts architecture. Configure it via props, not by passing child components.\n *\n * **Best Practice:** Use the built-in data fetching by passing `queryKey` and `parameters` props instead of pre-fetching data with `useAnalyticsQuery`.\n *\n * Supports both query mode (queryKey + parameters) and data mode (static data).\n */\nfunction _AreaChartDoc(props: AreaChartProps): JSX.Element {\n return AreaChart(props);\n}\n"],"mappings":";;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/react/charts/area/index.tsx"],"sourcesContent":["import type { JSX } from \"react\";\n\nimport { createChart } from \"../create-chart\";\nimport type { AreaChartProps } from \"../types\";\n\n/**\n * Area Chart component for trend visualization with filled areas.\n *\n * **Important:** This component uses Apache ECharts architecture. Configure it via props, not by passing child components.\n *\n * **Best Practice:** Use the built-in data fetching by passing `queryKey` and `parameters` props instead of pre-fetching data with `useAnalyticsQuery`.\n *\n * Supports both query mode (queryKey + parameters) and data mode (static data).\n */\nexport const AreaChart = createChart<AreaChartProps>(\"area\", \"AreaChart\");\n\n// Type-only definition for documentation generation (not used at runtime)\n/**\n * Area Chart component for trend visualization with filled areas.\n *\n * **Important:** This component uses Apache ECharts architecture. Configure it via props, not by passing child components.\n *\n * **Best Practice:** Use the built-in data fetching by passing `queryKey` and `parameters` props instead of pre-fetching data with `useAnalyticsQuery`.\n *\n * Supports both query mode (queryKey + parameters) and data mode (static data).\n */\nfunction _AreaChartDoc(props: AreaChartProps): JSX.Element {\n return AreaChart(props);\n}\n"],"mappings":";;;;;;;;;;;;AAcA,MAAa,YAAY,YAA4B,QAAQ,YAAY"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../../src/react/charts/bar/index.tsx"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../../src/react/charts/bar/index.tsx"],"mappings":";;;;;;;AAcA;;;;;;cAAa,QAAA;EAAA,QAAwD,aAAA,GAAA,kBAAA,CAAA,GAAA,CAAA,OAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/react/charts/bar/index.tsx"],"sourcesContent":["import type { JSX } from \"react\";\nimport { createChart } from \"../create-chart\";\nimport type { BarChartProps } from \"../types\";\n\n/**\n * Bar Chart component for categorical comparisons.\n *\n * **Important:** This component uses Apache ECharts architecture. Configure it via props, not by passing child components.\n *\n * **Best Practice:** Use the built-in data fetching by passing `queryKey` and `parameters` props instead of pre-fetching data with `useAnalyticsQuery`.\n *\n * Supports both query mode (queryKey + parameters) and data mode (static data).\n */\nexport const BarChart = createChart<BarChartProps>(\"bar\", \"BarChart\");\n\n// Type-only definition for documentation generation (not used at runtime)\n/**\n * Bar Chart component for categorical comparisons.\n *\n * **Important:** This component uses Apache ECharts architecture. Configure it via props, not by passing child components.\n *\n * **Best Practice:** Use the built-in data fetching by passing `queryKey` and `parameters` props instead of pre-fetching data with `useAnalyticsQuery`.\n *\n * Supports both query mode (queryKey + parameters) and data mode (static data).\n */\nfunction _BarChartDoc(props: BarChartProps): JSX.Element {\n return BarChart(props);\n}\n"],"mappings":";;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/react/charts/bar/index.tsx"],"sourcesContent":["import type { JSX } from \"react\";\n\nimport { createChart } from \"../create-chart\";\nimport type { BarChartProps } from \"../types\";\n\n/**\n * Bar Chart component for categorical comparisons.\n *\n * **Important:** This component uses Apache ECharts architecture. Configure it via props, not by passing child components.\n *\n * **Best Practice:** Use the built-in data fetching by passing `queryKey` and `parameters` props instead of pre-fetching data with `useAnalyticsQuery`.\n *\n * Supports both query mode (queryKey + parameters) and data mode (static data).\n */\nexport const BarChart = createChart<BarChartProps>(\"bar\", \"BarChart\");\n\n// Type-only definition for documentation generation (not used at runtime)\n/**\n * Bar Chart component for categorical comparisons.\n *\n * **Important:** This component uses Apache ECharts architecture. Configure it via props, not by passing child components.\n *\n * **Best Practice:** Use the built-in data fetching by passing `queryKey` and `parameters` props instead of pre-fetching data with `useAnalyticsQuery`.\n *\n * Supports both query mode (queryKey + parameters) and data mode (static data).\n */\nfunction _BarChartDoc(props: BarChartProps): JSX.Element {\n return BarChart(props);\n}\n"],"mappings":";;;;;;;;;;;;AAcA,MAAa,WAAW,YAA2B,OAAO,WAAW"}
|