@kaelio/ktx 0.11.0 → 0.12.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/assets/python/{kaelio_ktx-0.11.0-py3-none-any.whl → kaelio_ktx-0.12.0-py3-none-any.whl} +0 -0
- package/assets/python/manifest.json +4 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/admin.js +1 -1
- package/dist/clack.d.ts +16 -0
- package/dist/clack.js +37 -6
- package/dist/claude-code-prompt-caching.js +1 -1
- package/dist/cli-program.js +3 -3
- package/dist/cli-runtime.js +2 -2
- package/dist/commands/connection-commands.js +1 -1
- package/dist/commands/ingest-commands.js +4 -4
- package/dist/commands/mcp-commands.js +12 -12
- package/dist/commands/runtime-commands.js +4 -4
- package/dist/commands/setup-commands.js +6 -5
- package/dist/commands/sl-commands.js +1 -1
- package/dist/commands/sql-commands.js +1 -1
- package/dist/commands/status-commands.js +1 -1
- package/dist/connection.js +1 -1
- package/dist/connectors/clickhouse/connector.js +1 -1
- package/dist/connectors/mysql/connector.js +1 -1
- package/dist/connectors/snowflake/connector.d.ts +1 -1
- package/dist/connectors/sqlite/connector.js +2 -25
- package/dist/connectors/sqlserver/connector.js +3 -3
- package/dist/context/connections/connection-type.d.ts +1 -1
- package/dist/context/connections/read-only-sql.d.ts +1 -0
- package/dist/context/connections/read-only-sql.js +116 -2
- package/dist/context/core/git.service.d.ts +23 -0
- package/dist/context/core/git.service.js +71 -8
- package/dist/context/ingest/adapters/historic-sql/projection.js +2 -1
- package/dist/context/ingest/adapters/looker/client.js +7 -2
- package/dist/context/ingest/adapters/looker/factory.d.ts +8 -1
- package/dist/context/ingest/adapters/looker/factory.js +9 -0
- package/dist/context/ingest/adapters/looker/mapping.js +1 -1
- package/dist/context/ingest/adapters/looker/types.d.ts +1 -1
- package/dist/context/ingest/adapters/metabase/client.d.ts +1 -1
- package/dist/context/ingest/adapters/metabase/client.js +1 -1
- package/dist/context/ingest/adapters/metabase/local-metabase.adapter.js +1 -1
- package/dist/context/ingest/adapters/metabase/mapping.js +6 -6
- package/dist/context/ingest/artifact-gates.d.ts +2 -6
- package/dist/context/ingest/artifact-gates.js +5 -47
- package/dist/context/ingest/constrained-repair.d.ts +55 -0
- package/dist/context/ingest/constrained-repair.js +167 -0
- package/dist/context/ingest/final-gate-repair.d.ts +9 -11
- package/dist/context/ingest/final-gate-repair.js +40 -128
- package/dist/context/ingest/finalization-scope.d.ts +1 -1
- package/dist/context/ingest/finalization-scope.js +15 -15
- package/dist/context/ingest/ingest-bundle.runner.d.ts +1 -0
- package/dist/context/ingest/ingest-bundle.runner.js +101 -67
- package/dist/context/ingest/isolated-diff/patch-integrator.d.ts +6 -13
- package/dist/context/ingest/isolated-diff/patch-integrator.js +32 -109
- package/dist/context/ingest/isolated-diff/textual-conflict-resolver.d.ts +8 -9
- package/dist/context/ingest/isolated-diff/textual-conflict-resolver.js +63 -141
- package/dist/context/ingest/local-bundle-runtime.d.ts +2 -0
- package/dist/context/ingest/local-bundle-runtime.js +9 -10
- package/dist/context/ingest/local-ingest.d.ts +2 -0
- package/dist/context/ingest/local-ingest.js +2 -0
- package/dist/context/ingest/memory-flow/view-model.js +1 -1
- package/dist/context/ingest/stages/stage-3-work-units.d.ts +2 -6
- package/dist/context/ingest/stages/stage-3-work-units.js +2 -1
- package/dist/context/ingest/stages/validate-wu-sources.d.ts +7 -1
- package/dist/context/ingest/stages/validate-wu-sources.js +109 -4
- package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.d.ts +2 -0
- package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.js +1 -1
- package/dist/context/ingest/tools/warehouse-verification/discover-data.tool.js +3 -3
- package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.d.ts +3 -1
- package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.js +15 -1
- package/dist/context/llm/ai-sdk-runtime.js +2 -2
- package/dist/context/llm/claude-code-runtime.js +1 -1
- package/dist/context/llm/local-config.js +1 -1
- package/dist/context/llm/runtime-tools.js +2 -2
- package/dist/context/mcp/context-tools.js +7 -7
- package/dist/context/mcp/local-project-ports.js +23 -54
- package/dist/context/memory/local-memory.js +4 -1
- package/dist/context/memory/memory-agent.service.js +1 -1
- package/dist/context/project/config.d.ts +11 -4
- package/dist/context/project/config.js +85 -30
- package/dist/context/project/driver-schemas.js +1 -1
- package/dist/context/project/mappings-yaml-schema.js +2 -2
- package/dist/context/project/project.js +12 -4
- package/dist/context/scan/description-generation.js +4 -4
- package/dist/context/scan/local-enrichment-artifacts.js +2 -1
- package/dist/context/scan/local-scan.js +2 -2
- package/dist/context/scan/local-structural-artifacts.js +5 -5
- package/dist/context/scan/relationship-benchmark-report.js +1 -1
- package/dist/context/scan/relationship-discovery.js +3 -3
- package/dist/context/scan/relationship-llm-proposal.js +3 -3
- package/dist/context/sl/local-query.js +3 -33
- package/dist/context/sl/local-sl.d.ts +0 -8
- package/dist/context/sl/local-sl.js +44 -69
- package/dist/context/sl/semantic-layer.service.d.ts +25 -8
- package/dist/context/sl/semantic-layer.service.js +109 -56
- package/dist/context/sl/source-files.d.ts +46 -0
- package/dist/context/sl/source-files.js +131 -0
- package/dist/context/sl/tools/base-semantic-layer.tool.d.ts +2 -2
- package/dist/context/sl/tools/base-semantic-layer.tool.js +2 -7
- package/dist/context/sl/tools/sl-edit-source.tool.js +10 -8
- package/dist/context/sl/tools/sl-warehouse-validation.js +55 -27
- package/dist/context/sl/tools/sl-write-source.tool.js +12 -9
- package/dist/context/sql-analysis/dialect.d.ts +2 -0
- package/dist/context/sql-analysis/dialect.js +20 -0
- package/dist/context/tools/base-tool.d.ts +6 -19
- package/dist/context/tools/base-tool.js +0 -14
- package/dist/context-build-view.js +5 -5
- package/dist/database-tree-picker.js +18 -3
- package/dist/demo-assets.js +0 -1
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +31 -23
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +44 -0
- package/dist/ingest.d.ts +1 -1
- package/dist/ingest.js +8 -2
- package/dist/io/symbols.d.ts +2 -0
- package/dist/io/symbols.js +2 -0
- package/dist/io/tty.d.ts +8 -0
- package/dist/io/tty.js +16 -0
- package/dist/llm/embedding-health.js +1 -1
- package/dist/llm/embedding-provider.js +3 -3
- package/dist/llm/model-provider.js +1 -1
- package/dist/local-adapters.d.ts +1 -0
- package/dist/local-adapters.js +2 -2
- package/dist/local-scan-connectors.js +1 -1
- package/dist/managed-local-embeddings.js +17 -8
- package/dist/managed-mcp-daemon.js +3 -3
- package/dist/managed-python-command.d.ts +7 -0
- package/dist/managed-python-command.js +34 -8
- package/dist/managed-python-daemon.js +2 -2
- package/dist/managed-python-http.js +3 -3
- package/dist/managed-python-runtime.d.ts +30 -1
- package/dist/managed-python-runtime.js +134 -18
- package/dist/managed-uv-release.d.ts +7 -0
- package/dist/managed-uv-release.js +11 -0
- package/dist/mcp-http-server.js +4 -4
- package/dist/mcp-server-factory.js +3 -3
- package/dist/mcp-stdio-server.js +1 -1
- package/dist/memory-flow-hud.js +2 -2
- package/dist/next-steps.js +2 -2
- package/dist/prompt-navigation.d.ts +17 -0
- package/dist/prompt-navigation.js +49 -3
- package/dist/prompts/memory_agent_bundle_ingest_work_unit.md +2 -2
- package/dist/prompts/memory_agent_external_ingest.md +2 -2
- package/dist/public-ingest-copy.js +1 -1
- package/dist/public-ingest.js +3 -3
- package/dist/release-version.js +1 -1
- package/dist/runtime-requirements.js +1 -1
- package/dist/runtime.js +9 -9
- package/dist/scan.js +1 -1
- package/dist/setup-agents.js +21 -30
- package/dist/setup-banner.d.ts +20 -0
- package/dist/setup-banner.js +39 -0
- package/dist/setup-context.js +24 -15
- package/dist/setup-databases.js +31 -59
- package/dist/setup-demo-tour.js +12 -8
- package/dist/setup-embeddings.js +9 -9
- package/dist/setup-interrupt.js +1 -1
- package/dist/setup-models.d.ts +4 -1
- package/dist/setup-models.js +54 -28
- package/dist/setup-project.js +29 -5
- package/dist/setup-prompts.js +16 -1
- package/dist/setup-ready-menu.js +1 -1
- package/dist/setup-sources.js +27 -7
- package/dist/setup.js +13 -13
- package/dist/skills/analytics/SKILL.md +3 -3
- package/dist/skills/dbt_ingest/SKILL.md +3 -3
- package/dist/skills/looker_ingest/SKILL.md +3 -3
- package/dist/skills/lookml_ingest/SKILL.md +7 -7
- package/dist/skills/metabase_ingest/SKILL.md +4 -4
- package/dist/skills/metricflow_ingest/SKILL.md +15 -15
- package/dist/skills/notion_synthesize/SKILL.md +1 -1
- package/dist/skills/sl/SKILL.md +3 -3
- package/dist/skills/sl_capture/SKILL.md +1 -1
- package/dist/skills/wiki_capture/SKILL.md +1 -1
- package/dist/source-mapping.js +1 -1
- package/dist/startup-profile.js +1 -1
- package/dist/status-project.d.ts +0 -2
- package/dist/status-project.js +4 -6
- package/dist/telemetry/events.d.ts +1 -1
- package/dist/telemetry/exception.js +14 -0
- package/dist/text-ingest.js +1 -1
- package/dist/tree-picker-tui.d.ts +0 -1
- package/dist/tree-picker-tui.js +2 -3
- package/package.json +1 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import YAML from 'yaml';
|
|
3
|
+
// Semantic-layer source identity lives in the file's `name:` field, which mirrors
|
|
4
|
+
// the warehouse identifier verbatim (Snowflake's uppercase `SIGNED_UP`, `EVENT$LOG`).
|
|
5
|
+
// The filename is a derived label and never participates in identity: reads resolve
|
|
6
|
+
// a source by scanning the connection directory and matching `name:`, and writes
|
|
7
|
+
// reuse the resolved file's path, so files can be freely renamed by humans without
|
|
8
|
+
// changing which source they define.
|
|
9
|
+
function assertSafePathToken(kind, value) {
|
|
10
|
+
if (value.trim().length === 0 ||
|
|
11
|
+
value.includes('..') ||
|
|
12
|
+
value.includes('\\') ||
|
|
13
|
+
value.startsWith('/') ||
|
|
14
|
+
value.startsWith('.') ||
|
|
15
|
+
value.includes('//')) {
|
|
16
|
+
throw new Error(`Unsafe ${kind}: ${value}`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
export function assertSafeConnectionId(connectionId) {
|
|
21
|
+
if (!isSafeConnectionId(connectionId)) {
|
|
22
|
+
throw new Error(`Unsafe connection id: ${connectionId}`);
|
|
23
|
+
}
|
|
24
|
+
return assertSafePathToken('connection id', connectionId);
|
|
25
|
+
}
|
|
26
|
+
export function isSafeConnectionId(connectionId) {
|
|
27
|
+
return typeof connectionId === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(connectionId);
|
|
28
|
+
}
|
|
29
|
+
export function sourceNameFromPath(path) {
|
|
30
|
+
return (path
|
|
31
|
+
.split('/')
|
|
32
|
+
.at(-1)
|
|
33
|
+
?.replace(/\.ya?ml$/, '') ?? path);
|
|
34
|
+
}
|
|
35
|
+
// The one predicate for "this path is a semantic-layer YAML file". ktx itself
|
|
36
|
+
// always writes `.yaml` (see `slSourceFileName`), but humans rename freely and
|
|
37
|
+
// the dbt ecosystem's habit is `.yml`, so every reader must accept both — a
|
|
38
|
+
// listing that recognizes only one extension makes the same file visible to
|
|
39
|
+
// some entry points and invisible to others.
|
|
40
|
+
export function isSlYamlPath(path) {
|
|
41
|
+
return path.endsWith('.yaml') || path.endsWith('.yml');
|
|
42
|
+
}
|
|
43
|
+
// Windows refuses these basenames regardless of extension — a genuinely universal
|
|
44
|
+
// filesystem invariant, so the static list is acceptable.
|
|
45
|
+
const WINDOWS_RESERVED_BASENAME = /^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])$/;
|
|
46
|
+
const SAFE_FILE_BASENAME = /^[a-z0-9][a-z0-9_]{0,63}$/;
|
|
47
|
+
/**
|
|
48
|
+
* Derive the filename for a semantic-layer source. Total over all possible
|
|
49
|
+
* source names — never throws.
|
|
50
|
+
*
|
|
51
|
+
* Names that are already safe lowercase snake_case become `<name>.yaml`;
|
|
52
|
+
* anything else becomes `<slug>-<8 hex of sha256(name)>.yaml`. The two ranges
|
|
53
|
+
* are disjoint and the mapping is injective: safe filenames contain no `-`,
|
|
54
|
+
* hashed filenames always end in `-<8 hex>`, and slugs are lowercased so names
|
|
55
|
+
* differing only by case get distinct hashes instead of colliding paths on
|
|
56
|
+
* case-insensitive filesystems (macOS APFS, Windows).
|
|
57
|
+
*
|
|
58
|
+
* @internal
|
|
59
|
+
*/
|
|
60
|
+
export function slSourceFileName(sourceName) {
|
|
61
|
+
if (SAFE_FILE_BASENAME.test(sourceName) && !WINDOWS_RESERVED_BASENAME.test(sourceName)) {
|
|
62
|
+
return `${sourceName}.yaml`;
|
|
63
|
+
}
|
|
64
|
+
const slug = sourceName
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.replace(/[^a-z0-9_]+/g, '_')
|
|
67
|
+
.replace(/_+/g, '_')
|
|
68
|
+
.replace(/^_+|_+$/g, '')
|
|
69
|
+
.slice(0, 64);
|
|
70
|
+
const hash = createHash('sha256').update(sourceName, 'utf-8').digest('hex').slice(0, 8);
|
|
71
|
+
return `${slug || 'src'}-${hash}.yaml`;
|
|
72
|
+
}
|
|
73
|
+
export function slSourceFilePath(connectionId, sourceName) {
|
|
74
|
+
return `semantic-layer/${assertSafeConnectionId(connectionId)}/${slSourceFileName(sourceName)}`;
|
|
75
|
+
}
|
|
76
|
+
// Same keying as `loadLocalSlSourceRecords`: the in-file `name:` is the identity;
|
|
77
|
+
// the filename is only a fallback for files so broken that even the `name:` is
|
|
78
|
+
// unrecoverable, or genuinely nameless ones. A file left mid-edit with a syntax
|
|
79
|
+
// error below its `name:` line keeps its declared identity (see
|
|
80
|
+
// `slDeclaredSourceName`), so a human-renamed source is still addressed by name
|
|
81
|
+
// while broken instead of silently reverting to its filename.
|
|
82
|
+
export function slSourceNameForFile(path, content) {
|
|
83
|
+
return slDeclaredSourceName(content) ?? sourceNameFromPath(path);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The `name:` a semantic-layer YAML file declares, or null when the file is
|
|
87
|
+
* nameless or so broken even the name is unrecoverable. Null is how
|
|
88
|
+
* `writeSource` tells a genuine name conflict at a derived path apart from the
|
|
89
|
+
* broken remains of the source being written, which a rewrite must repair
|
|
90
|
+
* rather than refuse.
|
|
91
|
+
*
|
|
92
|
+
* Uses `parseDocument`, not `parse`: a file with a syntax error below the
|
|
93
|
+
* `name:` line still parses into a partial tree whose top-level `name:` is
|
|
94
|
+
* intact. `parse` would throw on the same input and drop the source to its
|
|
95
|
+
* filename — wrong for human-renamed files, whose filename is not the name.
|
|
96
|
+
*/
|
|
97
|
+
export function slDeclaredSourceName(content) {
|
|
98
|
+
let doc;
|
|
99
|
+
try {
|
|
100
|
+
doc = YAML.parseDocument(content);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const name = doc.get('name');
|
|
106
|
+
return typeof name === 'string' && name.length > 0 ? name : null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Find the standalone/overlay file that defines `sourceName` for a connection.
|
|
110
|
+
* Returns null when no file declares the name (the source may still exist as a
|
|
111
|
+
* manifest entry under `_schema/`). Throws when more than one file declares the
|
|
112
|
+
* same name — that breaks the one-file-per-name invariant and must be repaired
|
|
113
|
+
* by hand rather than silently picking one.
|
|
114
|
+
*/
|
|
115
|
+
export async function resolveSlSourceFile(fileStore, connectionId, sourceName) {
|
|
116
|
+
const dir = `semantic-layer/${assertSafeConnectionId(connectionId)}`;
|
|
117
|
+
const schemaDir = `${dir}/_schema`;
|
|
118
|
+
const listed = await fileStore.listFiles(dir);
|
|
119
|
+
const paths = listed.files.filter((file) => isSlYamlPath(file) && !file.startsWith(`${schemaDir}/`)).sort();
|
|
120
|
+
const matches = [];
|
|
121
|
+
for (const path of paths) {
|
|
122
|
+
const raw = await fileStore.readFile(path);
|
|
123
|
+
if (slSourceNameForFile(path, raw.content) === sourceName) {
|
|
124
|
+
matches.push({ path, content: raw.content });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (matches.length > 1) {
|
|
128
|
+
throw new Error(`Multiple semantic-layer files declare source "${sourceName}": ${matches.map((match) => match.path).join(', ')}`);
|
|
129
|
+
}
|
|
130
|
+
return matches[0] ?? null;
|
|
131
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { z } from 'zod';
|
|
2
2
|
import type { GitAuthorResolverPort } from '../../../context/tools/authors.js';
|
|
3
3
|
import type { ToolContext, ToolOutput } from '../../../context/tools/base-tool.js';
|
|
4
4
|
import { BaseTool } from '../../../context/tools/base-tool.js';
|
|
@@ -21,7 +21,7 @@ export interface BaseSemanticLayerToolDeps {
|
|
|
21
21
|
slSearchService: SlSearchService;
|
|
22
22
|
authorResolver: GitAuthorResolverPort;
|
|
23
23
|
}
|
|
24
|
-
export declare abstract class BaseSemanticLayerTool<TInput extends
|
|
24
|
+
export declare abstract class BaseSemanticLayerTool<TInput extends z.ZodObject<z.ZodRawShape> = z.ZodObject<z.ZodRawShape>> extends BaseTool<TInput> {
|
|
25
25
|
protected readonly semanticLayerService: SemanticLayerService;
|
|
26
26
|
protected readonly slSearchService: SlSearchService;
|
|
27
27
|
protected readonly authorResolver: GitAuthorResolverPort;
|
|
@@ -14,13 +14,8 @@ export class BaseSemanticLayerTool extends BaseTool {
|
|
|
14
14
|
}
|
|
15
15
|
async readSourceYaml(connectionId, sourceName, context) {
|
|
16
16
|
const semanticLayerService = context?.session?.semanticLayerService ?? this.semanticLayerService;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return content;
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
17
|
+
const file = await semanticLayerService.readSourceFile(connectionId, sourceName);
|
|
18
|
+
return file?.content ?? null;
|
|
24
19
|
}
|
|
25
20
|
buildMarkdown(success, errors, sourceName, extra) {
|
|
26
21
|
const parts = [];
|
|
@@ -91,14 +91,8 @@ If no source exists yet, use sl_write_source instead — this tool will reject t
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
// Read existing source
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const { content } = await semanticLayerService.readSourceFile(connectionId, sourceName);
|
|
97
|
-
currentYaml = content;
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
currentYaml = null;
|
|
101
|
-
}
|
|
94
|
+
const currentFile = await semanticLayerService.readSourceFile(connectionId, sourceName);
|
|
95
|
+
const currentYaml = currentFile?.content ?? null;
|
|
102
96
|
if (!currentYaml) {
|
|
103
97
|
const manifestBacked = await semanticLayerService.isManifestBacked(connectionId, sourceName);
|
|
104
98
|
if (manifestBacked) {
|
|
@@ -138,6 +132,14 @@ If no source exists yet, use sl_write_source instead — this tool will reject t
|
|
|
138
132
|
catch (e) {
|
|
139
133
|
return this.buildOutput(false, [`YAML parse error after edits: ${e}`], sourceName);
|
|
140
134
|
}
|
|
135
|
+
// The in-file `name:` is the source's identity — an edited name would make
|
|
136
|
+
// writeSource create a second source instead of updating this one.
|
|
137
|
+
if (source.name !== sourceName) {
|
|
138
|
+
return this.buildOutput(false, [
|
|
139
|
+
`Edits change "name:" from "${sourceName}" to "${source.name ?? '<missing>'}" — renaming is not supported. ` +
|
|
140
|
+
`Delete the source and recreate it under the new name.`,
|
|
141
|
+
], sourceName);
|
|
142
|
+
}
|
|
141
143
|
source = normalizeSemanticLayerDescriptions(source, { fillMissing: !!context.session?.ingest });
|
|
142
144
|
// Re-serialize and write
|
|
143
145
|
const updatedYaml = YAML.stringify(source, { indent: 2, lineWidth: 0, version: '1.1' });
|
|
@@ -2,8 +2,8 @@ import YAML from 'yaml';
|
|
|
2
2
|
import { SYSTEM_GIT_AUTHOR } from '../../../context/tools/authors.js';
|
|
3
3
|
import { sourceOverlaySchema } from '../schemas.js';
|
|
4
4
|
import { SemanticLayerService } from '../semantic-layer.service.js';
|
|
5
|
+
import { resolveSlSourceFile, slSourceFilePath } from '../source-files.js';
|
|
5
6
|
import { sourceDefinitionSchema } from './base-semantic-layer.tool.js';
|
|
6
|
-
const slSourcePath = (connectionId, sourceName) => `semantic-layer/${connectionId}/${sourceName}.yaml`;
|
|
7
7
|
function resolveDialect(warehouse) {
|
|
8
8
|
if (!warehouse) {
|
|
9
9
|
return null;
|
|
@@ -33,32 +33,28 @@ function wrapWithSingleRowQuery(sql, dialect) {
|
|
|
33
33
|
export async function validateSingleSource(deps, connectionId, sourceName) {
|
|
34
34
|
const errors = [];
|
|
35
35
|
const warnings = [];
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
content = result.content;
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
errors.push(`${sourceName}.yaml: file not found`);
|
|
36
|
+
const file = await deps.semanticLayerService.readSourceFile(connectionId, sourceName);
|
|
37
|
+
if (!file) {
|
|
38
|
+
errors.push(`${sourceName}: no standalone or overlay file found`);
|
|
43
39
|
return { errors, warnings };
|
|
44
40
|
}
|
|
45
41
|
let parsed;
|
|
46
42
|
try {
|
|
47
|
-
parsed = YAML.parse(content);
|
|
43
|
+
parsed = YAML.parse(file.content);
|
|
48
44
|
}
|
|
49
45
|
catch (e) {
|
|
50
|
-
errors.push(`${sourceName}
|
|
46
|
+
errors.push(`${sourceName}: invalid YAML — ${e instanceof Error ? e.message : String(e)}`);
|
|
51
47
|
return { errors, warnings };
|
|
52
48
|
}
|
|
53
49
|
if (!parsed || typeof parsed !== 'object') {
|
|
54
|
-
errors.push(`${sourceName}
|
|
50
|
+
errors.push(`${sourceName}: top-level content is not an object`);
|
|
55
51
|
return { errors, warnings };
|
|
56
52
|
}
|
|
57
53
|
const isOverlay = !parsed.table && !parsed.sql;
|
|
58
54
|
if (!isOverlay) {
|
|
59
55
|
const isManifestBacked = await deps.semanticLayerService.isManifestBacked(connectionId, sourceName);
|
|
60
56
|
if (isManifestBacked) {
|
|
61
|
-
errors.push(`${sourceName}
|
|
57
|
+
errors.push(`${sourceName}: standalone source shadows an existing manifest entry — ` +
|
|
62
58
|
`writing it as-is drops the manifest's columns and joins. ` +
|
|
63
59
|
`Remove "sql:", "table:", "grain:", and base-table "columns:" and keep only ` +
|
|
64
60
|
`"name:" plus overlay fields such as "measures:", "segments:", "descriptions:", ` +
|
|
@@ -71,16 +67,16 @@ export async function validateSingleSource(deps, connectionId, sourceName) {
|
|
|
71
67
|
const result = schema.safeParse(parsed);
|
|
72
68
|
if (!result.success) {
|
|
73
69
|
const issues = result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
|
|
74
|
-
errors.push(`${sourceName}
|
|
70
|
+
errors.push(`${sourceName}: schema — ${issues}`);
|
|
75
71
|
const errorPaths = new Set(result.error.issues.map((i) => String(i.path[0])));
|
|
76
72
|
if (errorPaths.has('joins')) {
|
|
77
|
-
warnings.push(`${sourceName}
|
|
73
|
+
warnings.push(`${sourceName}: hint — join format: {to, on: 'local_col = TARGET.col', relationship: 'many_to_one|one_to_many|one_to_one'}`);
|
|
78
74
|
}
|
|
79
75
|
if (errorPaths.has('columns')) {
|
|
80
|
-
warnings.push(`${sourceName}
|
|
76
|
+
warnings.push(`${sourceName}: hint — overlay columns must be computed: {name, expr, type}. Use column_overrides for manifest column descriptions or metadata.`);
|
|
81
77
|
}
|
|
82
78
|
if (errorPaths.has('measures')) {
|
|
83
|
-
warnings.push(`${sourceName}
|
|
79
|
+
warnings.push(`${sourceName}: hint — measure format: {name, expr, description (optional), filter (optional)}`);
|
|
84
80
|
}
|
|
85
81
|
return { errors, warnings };
|
|
86
82
|
}
|
|
@@ -93,7 +89,7 @@ export async function validateSingleSource(deps, connectionId, sourceName) {
|
|
|
93
89
|
const seenMeasures = new Set();
|
|
94
90
|
for (const m of measures) {
|
|
95
91
|
if (seenMeasures.has(m.name)) {
|
|
96
|
-
errors.push(`${sourceName}
|
|
92
|
+
errors.push(`${sourceName}: duplicate measure name "${m.name}"`);
|
|
97
93
|
}
|
|
98
94
|
seenMeasures.add(m.name);
|
|
99
95
|
}
|
|
@@ -125,7 +121,7 @@ export async function validateSingleSource(deps, connectionId, sourceName) {
|
|
|
125
121
|
const actual = new Set((probe.headers ?? []).map((h) => h.toLowerCase()));
|
|
126
122
|
const missing = sourceColumns.map((c) => c.name).filter((n) => !actual.has(n.toLowerCase()));
|
|
127
123
|
if (missing.length > 0) {
|
|
128
|
-
errors.push(`${sourceName}
|
|
124
|
+
errors.push(`${sourceName}: declared columns absent from sql result — ${missing.join(', ')} (warehouse returned: ${[...actual].slice(0, 10).join(', ')}${actual.size > 10 ? ', …' : ''})`);
|
|
129
125
|
}
|
|
130
126
|
}
|
|
131
127
|
catch (e) {
|
|
@@ -151,7 +147,7 @@ function formatProbeError(args) {
|
|
|
151
147
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
152
148
|
const refColumns = sourceColumns.filter((c) => referencesColumn(probeSql, c.name));
|
|
153
149
|
const lines = [
|
|
154
|
-
measureName ? `${sourceName}
|
|
150
|
+
measureName ? `${sourceName}: measure "${measureName}" ${headline}.` : `${sourceName}: ${headline}.`,
|
|
155
151
|
];
|
|
156
152
|
if (warehouse) {
|
|
157
153
|
lines.push(` Warehouse: ${warehouse}`);
|
|
@@ -179,7 +175,7 @@ async function probeOverlayMeasures(deps, connectionId, sourceName, warehouse) {
|
|
|
179
175
|
composed = all.find((s) => s.name === sourceName);
|
|
180
176
|
}
|
|
181
177
|
catch (e) {
|
|
182
|
-
errors.push(`${sourceName}
|
|
178
|
+
errors.push(`${sourceName}: failed to load composed source for probe — ${e instanceof Error ? e.message : String(e)}`);
|
|
183
179
|
return errors;
|
|
184
180
|
}
|
|
185
181
|
if (!composed?.table || composed.measures.length === 0) {
|
|
@@ -214,22 +210,54 @@ async function probeOverlayMeasures(deps, connectionId, sourceName, warehouse) {
|
|
|
214
210
|
}
|
|
215
211
|
return errors;
|
|
216
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* A read-only view of the config repo at one commit, shaped for
|
|
215
|
+
* `resolveSlSourceFile` so name→file resolution runs against history exactly as
|
|
216
|
+
* it does against the working tree — one resolver, two backing stores. Used to
|
|
217
|
+
* recover the path a source occupied at `preHead` after the live file is gone.
|
|
218
|
+
*/
|
|
219
|
+
function gitCommitFileStore(git, commitHash) {
|
|
220
|
+
return {
|
|
221
|
+
async listFiles(path) {
|
|
222
|
+
return { files: await git.listFilesAtCommit(path, commitHash) };
|
|
223
|
+
},
|
|
224
|
+
async readFile(path) {
|
|
225
|
+
return { content: await git.getFileAtCommit(path, commitHash) };
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
217
229
|
/**
|
|
218
230
|
* Restore `sourceName` to the content it had at `preHead`, or delete it if it didn't
|
|
219
231
|
* exist then. Used by sl_rollback (agent-driven) and the pre-squash revert gate
|
|
220
232
|
* (automatic). Returns a short human-readable description of what happened.
|
|
221
233
|
*/
|
|
222
234
|
export async function revertSourceToPreHead(deps, connectionId, preHead, sourceName) {
|
|
223
|
-
|
|
235
|
+
// Find the file that defines this source. While it is still on disk
|
|
236
|
+
// (invalid-but-present) the live resolver finds it by its in-file `name:`.
|
|
237
|
+
// Once the session deleted it, the path is gone too — and humans rename files
|
|
238
|
+
// freely, so it is NOT the writer-derived filename. Recover it from history by
|
|
239
|
+
// resolving the name against the preHead commit instead of guessing.
|
|
240
|
+
const live = await resolveSlSourceFile(deps.configService, connectionId, sourceName);
|
|
241
|
+
let relPath;
|
|
224
242
|
let preContent = null;
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
243
|
+
if (live) {
|
|
244
|
+
relPath = live.path;
|
|
245
|
+
if (preHead) {
|
|
246
|
+
try {
|
|
247
|
+
preContent = await deps.gitService.getFileAtCommit(relPath, preHead);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
preContent = null;
|
|
251
|
+
}
|
|
231
252
|
}
|
|
232
253
|
}
|
|
254
|
+
else {
|
|
255
|
+
const atPreHead = preHead
|
|
256
|
+
? await resolveSlSourceFile(gitCommitFileStore(deps.gitService, preHead), connectionId, sourceName)
|
|
257
|
+
: null;
|
|
258
|
+
relPath = atPreHead?.path ?? slSourceFilePath(connectionId, sourceName);
|
|
259
|
+
preContent = atPreHead?.content ?? null;
|
|
260
|
+
}
|
|
233
261
|
if (preContent !== null) {
|
|
234
262
|
await deps.configService.writeFile(relPath, preContent, SYSTEM_GIT_AUTHOR.name, SYSTEM_GIT_AUTHOR.email, `Revert SL source to pre-session state: ${sourceName}`, { skipLock: true });
|
|
235
263
|
return 'restored to pre-session content';
|
|
@@ -12,8 +12,10 @@ const slWriteSourceInputSchema = z.object({
|
|
|
12
12
|
connectionId: slToolConnectionIdSchema.describe('Data source connection ID'),
|
|
13
13
|
sourceName: z
|
|
14
14
|
.string()
|
|
15
|
-
.
|
|
16
|
-
.describe(
|
|
15
|
+
.min(1)
|
|
16
|
+
.describe("Name of the source to create, edit, or delete. Must equal the source's `name:`. Use the verbatim " +
|
|
17
|
+
'warehouse identifier when overlaying a manifest source (e.g. SIGNED_UP); snake_case is recommended ' +
|
|
18
|
+
'for new standalone sources.'),
|
|
17
19
|
source: sourceInputSchema
|
|
18
20
|
.optional()
|
|
19
21
|
.describe('Source definition (standalone with table/sql) or overlay (measures, column_overrides, computed columns, etc.)'),
|
|
@@ -122,6 +124,12 @@ Do NOT join back to a table that the SQL already aggregates from if the grain co
|
|
|
122
124
|
if (!input.source) {
|
|
123
125
|
return this.buildOutput(false, ['Provide `source` to create or rewrite. For targeted edits, use sl_edit_source.'], sourceName);
|
|
124
126
|
}
|
|
127
|
+
// The in-file `name:` is the source's identity; the file is written under
|
|
128
|
+
// source.name while the orphan/shadow checks key on sourceName — a mismatch
|
|
129
|
+
// would validate one source and save another.
|
|
130
|
+
if (input.source.name !== sourceName) {
|
|
131
|
+
return this.buildOutput(false, [`source.name "${input.source.name}" does not match sourceName "${sourceName}" — they must be identical.`], sourceName);
|
|
132
|
+
}
|
|
125
133
|
return this.writeFullSource(connectionId, input.source, sourceName, author, authorEmail, context, semanticLayerService, skipIndex, rawPathValidation.rawPaths);
|
|
126
134
|
}
|
|
127
135
|
async writeFullSource(connectionId, source, sourceName, author, authorEmail, context, semanticLayerService, skipIndex, rawPaths) {
|
|
@@ -183,13 +191,8 @@ Do NOT join back to a table that the SQL already aggregates from if the grain co
|
|
|
183
191
|
}
|
|
184
192
|
}
|
|
185
193
|
async readSourceYamlFromService(service, connectionId, sourceName) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return content;
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
194
|
+
const file = await service.readSourceFile(connectionId, sourceName);
|
|
195
|
+
return file?.content ?? null;
|
|
193
196
|
}
|
|
194
197
|
async rejectOrphanOverlay(semanticLayerService, connectionId, sourceName, content) {
|
|
195
198
|
let parsed;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// One mapping from ktx connection identity to the sqlglot dialect name used by
|
|
2
|
+
// the Python daemon (SQL analysis, read-only validation) and semantic-layer
|
|
3
|
+
// compute. Keys cover both vocabularies that name a connection's engine:
|
|
4
|
+
// ktx.yaml driver names ("postgres", "sqlserver") and the local connection-type
|
|
5
|
+
// spellings exposed by KtxConnectionInfo.connectionType ("POSTGRESQL").
|
|
6
|
+
const SQLGLOT_DIALECTS = {
|
|
7
|
+
postgres: 'postgres',
|
|
8
|
+
postgresql: 'postgres',
|
|
9
|
+
bigquery: 'bigquery',
|
|
10
|
+
snowflake: 'snowflake',
|
|
11
|
+
mysql: 'mysql',
|
|
12
|
+
sqlserver: 'tsql',
|
|
13
|
+
sqlite: 'sqlite',
|
|
14
|
+
duckdb: 'duckdb',
|
|
15
|
+
clickhouse: 'clickhouse',
|
|
16
|
+
databricks: 'databricks',
|
|
17
|
+
};
|
|
18
|
+
export function sqlAnalysisDialectForDriver(driver) {
|
|
19
|
+
return SQLGLOT_DIALECTS[(driver ?? '').toLowerCase()] ?? 'postgres';
|
|
20
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type Tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
2
3
|
import { type KtxLogger } from '../../context/core/config.js';
|
|
3
4
|
import type { KtxRuntimeToolDescriptor } from '../llm/runtime-port.js';
|
|
4
5
|
import type { IngestToolMetadata, ToolSession } from './tool-session.js';
|
|
@@ -56,30 +57,16 @@ interface MethodologyEntry {
|
|
|
56
57
|
/**
|
|
57
58
|
* SECURITY: All tools require authentication. userId must always be provided in ToolContext.
|
|
58
59
|
*/
|
|
59
|
-
export declare abstract class BaseTool<TInput extends
|
|
60
|
+
export declare abstract class BaseTool<TInput extends z.ZodObject<z.ZodRawShape> = z.ZodObject<z.ZodRawShape>> {
|
|
60
61
|
protected readonly logger: KtxLogger;
|
|
61
62
|
abstract readonly name: string;
|
|
62
63
|
constructor(logger?: KtxLogger);
|
|
63
64
|
abstract get description(): string;
|
|
64
65
|
abstract get inputSchema(): TInput;
|
|
65
|
-
abstract call(input: z.infer<TInput>, context: ToolContext): Promise<
|
|
66
|
-
|
|
67
|
-
type: 'object';
|
|
68
|
-
properties: Record<string, any>;
|
|
69
|
-
required?: string[];
|
|
70
|
-
};
|
|
71
|
-
toAnthropicFormat(): {
|
|
72
|
-
name: string;
|
|
73
|
-
description: string;
|
|
74
|
-
input_schema: {
|
|
75
|
-
type: 'object';
|
|
76
|
-
properties: Record<string, any>;
|
|
77
|
-
required?: string[];
|
|
78
|
-
};
|
|
79
|
-
};
|
|
80
|
-
toAiSdkTool(context: ToolContext): any;
|
|
66
|
+
abstract call(input: z.infer<TInput>, context: ToolContext): Promise<unknown>;
|
|
67
|
+
toAiSdkTool(context: ToolContext): Tool;
|
|
81
68
|
toRuntimeTool(context: ToolContext): KtxRuntimeToolDescriptor;
|
|
82
|
-
parseInput(input: Record<string,
|
|
69
|
+
parseInput(input: Record<string, unknown>): z.infer<TInput>;
|
|
83
70
|
protected getCurrentUserQuery(context: ToolContext): string | null;
|
|
84
71
|
}
|
|
85
72
|
export {};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { tool } from 'ai';
|
|
2
|
-
import { z } from 'zod';
|
|
3
2
|
import { noopLogger } from '../../context/core/config.js';
|
|
4
3
|
import { normalizeKtxRuntimeToolOutput } from '../llm/runtime-tools.js';
|
|
5
4
|
/**
|
|
@@ -10,19 +9,6 @@ export class BaseTool {
|
|
|
10
9
|
constructor(logger = noopLogger) {
|
|
11
10
|
this.logger = logger;
|
|
12
11
|
}
|
|
13
|
-
getParametersSchema() {
|
|
14
|
-
const jsonSchema = z.toJSONSchema(this.inputSchema, {
|
|
15
|
-
target: 'draft-7',
|
|
16
|
-
});
|
|
17
|
-
return jsonSchema;
|
|
18
|
-
}
|
|
19
|
-
toAnthropicFormat() {
|
|
20
|
-
return {
|
|
21
|
-
name: this.name,
|
|
22
|
-
description: this.description,
|
|
23
|
-
input_schema: this.getParametersSchema(),
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
12
|
toAiSdkTool(context) {
|
|
27
13
|
const toolName = this.name;
|
|
28
14
|
const logger = this.logger;
|
|
@@ -260,7 +260,7 @@ export function renderContextBuildView(state, options = {}) {
|
|
|
260
260
|
const totalCount = allTargets.length;
|
|
261
261
|
const hasActive = allTargets.some((t) => t.status === 'running' || t.status === 'queued');
|
|
262
262
|
const allDone = totalCount > 0 && !hasActive;
|
|
263
|
-
const headerParts = [options.title ?? 'Building
|
|
263
|
+
const headerParts = [options.title ?? 'Building ktx context'];
|
|
264
264
|
if (totalCount > 0) {
|
|
265
265
|
const progressParts = [`${doneCount}/${totalCount}`];
|
|
266
266
|
if (state.totalElapsedMs > 0)
|
|
@@ -550,7 +550,7 @@ function failedStepDetail(result) {
|
|
|
550
550
|
return result.steps.find((step) => step.status === 'failed')?.detail ?? null;
|
|
551
551
|
}
|
|
552
552
|
const INTERNAL_FAILURE_LINE_RE = /^(Report|Run|Job|Status|Adapter|Connection|Sync|Mode|Dry run|Diff|Tasks|Work units|Failed tasks|Saved memory|Provenance rows):\s*/;
|
|
553
|
-
const ACTIONABLE_FAILURE_LINE_RE = /^(Missing bundled Python runtime manifest|
|
|
553
|
+
const ACTIONABLE_FAILURE_LINE_RE = /^(Missing bundled Python runtime manifest|ktx Python runtime is required|ktx daemon HTTP|Error:|Failed\b|Could not\b|Cannot\b)/;
|
|
554
554
|
function trimErrorPrefix(line) {
|
|
555
555
|
return line.replace(/^Error:\s*/, '');
|
|
556
556
|
}
|
|
@@ -559,7 +559,7 @@ function firstCapturedFailureLine(output) {
|
|
|
559
559
|
.split(/\r?\n/)
|
|
560
560
|
.map((candidate) => candidate.trim())
|
|
561
561
|
.filter((candidate) => candidate.length > 0)
|
|
562
|
-
.filter((candidate) => !candidate.startsWith('
|
|
562
|
+
.filter((candidate) => !candidate.startsWith('ktx scan completed'))
|
|
563
563
|
.filter((candidate) => !INTERNAL_FAILURE_LINE_RE.test(candidate));
|
|
564
564
|
const line = lines.find((candidate) => ACTIONABLE_FAILURE_LINE_RE.test(candidate)) ?? lines.at(-1) ?? null;
|
|
565
565
|
return line ? trimErrorPrefix(line) : null;
|
|
@@ -584,7 +584,7 @@ function failureTextForTarget(input) {
|
|
|
584
584
|
const code = networkErrorCode(input.error, input.capturedOutput);
|
|
585
585
|
if (code && isLocalSqlAnalysisConnectionRefused({ capturedOutput: input.capturedOutput, fallback: input.fallback })) {
|
|
586
586
|
return [
|
|
587
|
-
`
|
|
587
|
+
`ktx could not reach the local SQL analysis runtime while processing query history for ${input.target.connectionId}.`,
|
|
588
588
|
`Reason: ${NETWORK_ERROR_REASONS[code]} (${code}).`,
|
|
589
589
|
`Retry: ${retryCommand({
|
|
590
590
|
projectDir: input.projectDir,
|
|
@@ -598,7 +598,7 @@ function failureTextForTarget(input) {
|
|
|
598
598
|
if (code) {
|
|
599
599
|
const operation = input.target.operation === 'database-ingest' ? 'reading schema for' : 'ingesting';
|
|
600
600
|
return [
|
|
601
|
-
`
|
|
601
|
+
`ktx lost its connection to ${friendlyDriverName(input.target.driver)} while ${operation} ${input.target.connectionId}.`,
|
|
602
602
|
`Reason: ${NETWORK_ERROR_REASONS[code]} (${code}).`,
|
|
603
603
|
`Retry: ${retryCommand({
|
|
604
604
|
projectDir: input.projectDir,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { parseDottedTableEntry } from './context/scan/enabled-tables.js';
|
|
2
|
+
import { createStaticCliSpinner } from './clack.js';
|
|
2
3
|
import { profileMark } from './startup-profile.js';
|
|
4
|
+
import { withSearchableMultiselectNavigation } from './prompt-navigation.js';
|
|
3
5
|
import { buildInitialState, buildPickerTree, } from './tree-picker-state.js';
|
|
4
6
|
import { renderTreePickerTui, } from './tree-picker-tui.js';
|
|
5
7
|
profileMark('module:database-tree-picker');
|
|
@@ -167,7 +169,7 @@ export async function pickDatabaseScope(args, io, render = defaultRenderer) {
|
|
|
167
169
|
let selectedSchemas = initialStageOneSchemas(args);
|
|
168
170
|
while (true) {
|
|
169
171
|
const pickedSchemas = await args.prompts.autocompleteMultiselect({
|
|
170
|
-
message: `Choose ${args.schemaNounPlural} to enable for ${args.connectionId}
|
|
172
|
+
message: withSearchableMultiselectNavigation(`Choose ${args.schemaNounPlural} to enable for ${args.connectionId}`),
|
|
171
173
|
placeholder: `Search ${args.schemaNounPlural}`,
|
|
172
174
|
options: schemaOptions(args),
|
|
173
175
|
initialValues: selectedSchemas,
|
|
@@ -178,7 +180,7 @@ export async function pickDatabaseScope(args, io, render = defaultRenderer) {
|
|
|
178
180
|
}
|
|
179
181
|
selectedSchemas = pickedSchemas;
|
|
180
182
|
if (selectedSchemas.length === 0) {
|
|
181
|
-
io.stderr.write(`Nothing selected - type to
|
|
183
|
+
io.stderr.write(`Nothing selected - type to search, or Escape to skip ${args.schemaNoun} scope.\n`);
|
|
182
184
|
continue;
|
|
183
185
|
}
|
|
184
186
|
const selectedNoun = selectedSchemas.length === 1 ? args.schemaNoun : args.schemaNounPlural;
|
|
@@ -193,7 +195,20 @@ export async function pickDatabaseScope(args, io, render = defaultRenderer) {
|
|
|
193
195
|
if (action === 'back') {
|
|
194
196
|
continue;
|
|
195
197
|
}
|
|
196
|
-
|
|
198
|
+
// Static (stderr-only) spinner: the stage-two table picker below is a raw-mode
|
|
199
|
+
// Ink TUI, and an animated clack spinner would leave stdin dirty so Ink reads a
|
|
200
|
+
// stray Escape and exits immediately.
|
|
201
|
+
const tablesSpinner = createStaticCliSpinner(io);
|
|
202
|
+
tablesSpinner.start(`Listing tables in ${selectedSchemas.length} ${selectedNoun}…`);
|
|
203
|
+
let discovered;
|
|
204
|
+
try {
|
|
205
|
+
discovered = await args.listTablesForSchemas(selectedSchemas);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
tablesSpinner.error('Could not list tables');
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
tablesSpinner.stop(`Found ${discovered.length} ${discovered.length === 1 ? 'table' : 'tables'}`);
|
|
197
212
|
if (action === 'save' && args.existing.enabledTables.length === 0) {
|
|
198
213
|
return {
|
|
199
214
|
kind: 'selected',
|
package/dist/demo-assets.js
CHANGED
package/dist/doctor.d.ts
CHANGED
|
@@ -67,7 +67,7 @@ interface RenderOptions {
|
|
|
67
67
|
}
|
|
68
68
|
export declare function formatDoctorReport(report: DoctorReport, options?: Partial<RenderOptions>): string;
|
|
69
69
|
export declare function renderInvalidConfigMessage(projectDir: string, issues: KtxConfigIssue[], outputMode: KtxDoctorOutputMode, io: KtxDoctorIo): void;
|
|
70
|
-
export declare function renderValidConfigMessage(projectDir: string, outputMode: KtxDoctorOutputMode, io: KtxDoctorIo): void;
|
|
70
|
+
export declare function renderValidConfigMessage(projectDir: string, outputMode: KtxDoctorOutputMode, io: KtxDoctorIo, warnings?: KtxConfigIssue[]): void;
|
|
71
71
|
export declare function renderMissingProjectMessage(projectDir: string, outputMode: KtxDoctorOutputMode, io: KtxDoctorIo): void;
|
|
72
72
|
export declare function runKtxDoctor(args: KtxDoctorArgs, io?: KtxDoctorIo, deps?: KtxDoctorDeps): Promise<number>;
|
|
73
73
|
export {};
|