@kaelio/ktx 0.7.0 → 0.9.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.
Files changed (142) hide show
  1. package/assets/python/{kaelio_ktx-0.7.0-py3-none-any.whl → kaelio_ktx-0.9.0-py3-none-any.whl} +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/cli-program.js +7 -0
  5. package/dist/cli-runtime.js +50 -3
  6. package/dist/command-schemas.d.ts +1 -1
  7. package/dist/command-tree.js +5 -1
  8. package/dist/commands/completion-commands.d.ts +3 -0
  9. package/dist/commands/completion-commands.js +38 -0
  10. package/dist/commands/ingest-commands.js +0 -4
  11. package/dist/commands/knowledge-commands.js +15 -2
  12. package/dist/commands/setup-commands.js +3 -3
  13. package/dist/commands/sl-commands.js +19 -7
  14. package/dist/completion/complete-engine.d.ts +19 -0
  15. package/dist/completion/complete-engine.js +128 -0
  16. package/dist/completion/completion-scripts.d.ts +1 -0
  17. package/dist/completion/completion-scripts.js +36 -0
  18. package/dist/completion/dynamic-candidates.d.ts +6 -0
  19. package/dist/completion/dynamic-candidates.js +98 -0
  20. package/dist/connection-drivers.d.ts +3 -0
  21. package/dist/connection-drivers.js +17 -0
  22. package/dist/connection-recovery.d.ts +34 -0
  23. package/dist/connection-recovery.js +82 -0
  24. package/dist/connection.js +3 -1
  25. package/dist/context/ingest/adapters/historic-sql/bigquery-query-history-reader.js +71 -20
  26. package/dist/context/ingest/adapters/historic-sql/chunk-unified.js +2 -1
  27. package/dist/context/ingest/adapters/historic-sql/connection-dialect.d.ts +9 -0
  28. package/dist/context/ingest/adapters/historic-sql/connection-dialect.js +15 -4
  29. package/dist/context/ingest/adapters/historic-sql/pattern-inputs.js +8 -2
  30. package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.d.ts +29 -0
  31. package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.js +190 -0
  32. package/dist/context/ingest/adapters/historic-sql/scope-floor.d.ts +18 -0
  33. package/dist/context/ingest/adapters/historic-sql/scope-floor.js +229 -0
  34. package/dist/context/ingest/adapters/historic-sql/scope-membership.d.ts +8 -0
  35. package/dist/context/ingest/adapters/historic-sql/scope-membership.js +29 -0
  36. package/dist/context/ingest/adapters/historic-sql/snowflake-query-history-reader.js +68 -19
  37. package/dist/context/ingest/adapters/historic-sql/stage-unified.js +57 -50
  38. package/dist/context/ingest/adapters/historic-sql/types.d.ts +36 -3
  39. package/dist/context/ingest/adapters/historic-sql/types.js +14 -2
  40. package/dist/context/ingest/context-evidence/sqlite-context-evidence-store.d.ts +1 -1
  41. package/dist/context/ingest/ingest-bundle.runner.d.ts +8 -0
  42. package/dist/context/ingest/ingest-bundle.runner.js +72 -15
  43. package/dist/context/ingest/ingest-profile.d.ts +102 -0
  44. package/dist/context/ingest/ingest-profile.js +306 -0
  45. package/dist/context/ingest/isolated-diff/patch-integrator.js +75 -5
  46. package/dist/context/ingest/isolated-diff/work-unit-executor.js +25 -2
  47. package/dist/context/ingest/local-adapters.js +21 -4
  48. package/dist/context/ingest/local-bundle-runtime.js +4 -2
  49. package/dist/context/ingest/local-ingest.d.ts +1 -1
  50. package/dist/context/ingest/local-ingest.js +6 -4
  51. package/dist/context/ingest/memory-flow/events.js +2 -1
  52. package/dist/context/ingest/ports.d.ts +2 -0
  53. package/dist/context/ingest/reports.d.ts +3 -0
  54. package/dist/context/ingest/reports.js +10 -0
  55. package/dist/context/ingest/stages/stage-3-work-units.d.ts +3 -1
  56. package/dist/context/ingest/stages/stage-3-work-units.js +2 -0
  57. package/dist/context/ingest/stages/stage-4-reconciliation.d.ts +2 -1
  58. package/dist/context/ingest/stages/stage-4-reconciliation.js +1 -1
  59. package/dist/context/ingest/tools/tool-call-logger.d.ts +6 -0
  60. package/dist/context/ingest/tools/tool-call-logger.js +36 -1
  61. package/dist/context/llm/ai-sdk-runtime.js +32 -3
  62. package/dist/context/llm/claude-code-runtime.js +35 -2
  63. package/dist/context/llm/codex-exec-events.d.ts +20 -0
  64. package/dist/context/llm/codex-exec-events.js +155 -0
  65. package/dist/context/llm/codex-isolation.d.ts +3 -0
  66. package/dist/context/llm/codex-isolation.js +5 -0
  67. package/dist/context/llm/codex-mcp-runtime-server.d.ts +24 -0
  68. package/dist/context/llm/codex-mcp-runtime-server.js +51 -0
  69. package/dist/context/llm/codex-models.d.ts +2 -0
  70. package/dist/context/llm/codex-models.js +17 -0
  71. package/dist/context/llm/codex-runtime-config.d.ts +16 -0
  72. package/dist/context/llm/codex-runtime-config.js +19 -0
  73. package/dist/context/llm/codex-runtime.d.ts +37 -0
  74. package/dist/context/llm/codex-runtime.js +304 -0
  75. package/dist/context/llm/codex-sdk-runner.d.ts +21 -0
  76. package/dist/context/llm/codex-sdk-runner.js +63 -0
  77. package/dist/context/llm/local-config.d.ts +2 -0
  78. package/dist/context/llm/local-config.js +12 -1
  79. package/dist/context/llm/runtime-port.d.ts +25 -0
  80. package/dist/context/mcp/context-tools.d.ts +2 -1
  81. package/dist/context/mcp/context-tools.js +82 -15
  82. package/dist/context/mcp/server.js +4 -0
  83. package/dist/context/mcp/types.d.ts +15 -1
  84. package/dist/context/project/config.d.ts +3 -0
  85. package/dist/context/project/config.js +6 -2
  86. package/dist/context/project/driver-schemas.js +1 -1
  87. package/dist/context/search/discover.js +4 -3
  88. package/dist/context/sl/local-sl.d.ts +15 -0
  89. package/dist/context/sl/local-sl.js +30 -0
  90. package/dist/context/sql-analysis/http-sql-analysis-port.js +32 -2
  91. package/dist/context/sql-analysis/ports.d.ts +12 -2
  92. package/dist/context/tools/context-candidate-mark.tool.d.ts +2 -2
  93. package/dist/context/wiki/local-knowledge.d.ts +10 -0
  94. package/dist/context/wiki/local-knowledge.js +22 -0
  95. package/dist/context-build-view.d.ts +0 -3
  96. package/dist/context-build-view.js +5 -39
  97. package/dist/ingest.js +7 -10
  98. package/dist/io/buffered-command-io.d.ts +11 -0
  99. package/dist/io/buffered-command-io.js +28 -0
  100. package/dist/knowledge.d.ts +5 -0
  101. package/dist/knowledge.js +10 -1
  102. package/dist/llm/types.d.ts +1 -1
  103. package/dist/local-adapters.d.ts +10 -2
  104. package/dist/local-adapters.js +19 -3
  105. package/dist/next-steps.js +1 -2
  106. package/dist/progress-port-adapter.d.ts +6 -0
  107. package/dist/progress-port-adapter.js +18 -0
  108. package/dist/public-ingest-copy.js +1 -1
  109. package/dist/public-ingest.d.ts +20 -8
  110. package/dist/public-ingest.js +198 -61
  111. package/dist/scan.js +3 -1
  112. package/dist/setup-context.d.ts +2 -0
  113. package/dist/setup-context.js +138 -64
  114. package/dist/setup-databases.d.ts +17 -1
  115. package/dist/setup-databases.js +366 -326
  116. package/dist/setup-models.d.ts +10 -1
  117. package/dist/setup-models.js +90 -2
  118. package/dist/setup-ready-menu.d.ts +16 -2
  119. package/dist/setup-ready-menu.js +37 -5
  120. package/dist/setup-sources.js +141 -33
  121. package/dist/setup.js +24 -12
  122. package/dist/skills/analytics/SKILL.md +6 -1
  123. package/dist/sl.d.ts +6 -1
  124. package/dist/sl.js +32 -8
  125. package/dist/status-project.d.ts +11 -0
  126. package/dist/status-project.js +50 -1
  127. package/dist/telemetry/command-hook.d.ts +1 -0
  128. package/dist/telemetry/command-hook.js +3 -1
  129. package/dist/telemetry/emitter.js +1 -1
  130. package/dist/telemetry/events.d.ts +15 -9
  131. package/dist/telemetry/events.js +17 -5
  132. package/dist/telemetry/identity.d.ts +1 -2
  133. package/dist/telemetry/identity.js +13 -10
  134. package/dist/telemetry/index.d.ts +13 -1
  135. package/dist/telemetry/index.js +18 -3
  136. package/dist/telemetry/scrubber.d.ts +10 -0
  137. package/dist/telemetry/scrubber.js +20 -0
  138. package/package.json +20 -19
  139. package/dist/ingest-depth.d.ts +0 -8
  140. package/dist/ingest-depth.js +0 -56
  141. package/dist/setup-database-context-depth.d.ts +0 -23
  142. package/dist/setup-database-context-depth.js +0 -84
@@ -4,23 +4,30 @@ import { delimiter, dirname, join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { promisify } from 'node:util';
6
6
  import { getDriverRegistration } from './context/connections/drivers.js';
7
+ import { createLocalKtxLlmRuntimeFromConfig } from './context/llm/local-config.js';
7
8
  import { queryHistoryDialectForConnection } from './context/ingest/adapters/historic-sql/connection-dialect.js';
9
+ import { proposeQueryHistoryServiceAccountFilters, } from './context/ingest/adapters/historic-sql/query-history-filter-picker.js';
10
+ import { resolveQueryHistoryScopeFloor } from './context/ingest/adapters/historic-sql/scope-floor.js';
8
11
  import { runHistoricSqlReadinessProbe, } from './context/ingest/historic-sql-probes.js';
9
12
  import { serializeKtxProjectConfig } from './context/project/config.js';
10
13
  import { loadKtxProject } from './context/project/project.js';
11
14
  import { markKtxSetupStateStepComplete, setKtxSetupDatabaseConnectionIds } from './context/project/setup-config.js';
15
+ import { getKtxCliPackageInfo } from './cli-runtime.js';
12
16
  import { errorMessage, flushPrefixedBufferedCommandOutput, writePrefixedLines, } from './clack.js';
13
17
  import { runKtxConnection } from './connection.js';
18
+ import { createBufferedCommandIo } from './io/buffered-command-io.js';
19
+ import { runConnectionSetupWithRecovery, } from './connection-recovery.js';
14
20
  import { pickDatabaseScope as defaultPickDatabaseScope, } from './database-tree-picker.js';
15
21
  import { withMultiselectNavigation, withTextInputNavigation } from './prompt-navigation.js';
22
+ import { createKtxCliHistoricSqlRuntime } from './local-adapters.js';
23
+ import { queryHistoryPullConfig } from './public-ingest.js';
16
24
  import { runKtxScan } from './scan.js';
17
- import { applySetupDatabaseContextDepth } from './setup-database-context-depth.js';
18
25
  import { writeProjectLocalSecretReference } from './setup-secrets.js';
19
26
  import { isDemoConnection } from './telemetry/demo-detect.js';
20
27
  import { emitTelemetryEvent } from './telemetry/index.js';
21
28
  import { createKtxSetupPromptAdapter, createKtxSetupUiAdapter, } from './setup-prompts.js';
22
29
  const HISTORIC_SQL_WORK_UNIT_MAX_CONCURRENCY = 6;
23
- const KTX_QUICKSTART_URL = 'https://docs.kaelio.com/ktx/docs/getting-started/quickstart';
30
+ const KTX_DEMO_START_URL = 'https://www.kaelio.com/start';
24
31
  const execFileAsync = promisify(execFileCallback);
25
32
  const DRIVER_OPTIONS = [
26
33
  { value: 'postgres', label: 'PostgreSQL' },
@@ -672,10 +679,13 @@ async function maybeApplyHistoricSqlConfig(input) {
672
679
  if (!enabled) {
673
680
  return withQueryHistoryConfig(input.connection, { ...existing, enabled: false });
674
681
  }
682
+ const existingFilters = existing.filters && typeof existing.filters === 'object' && !Array.isArray(existing.filters)
683
+ ? existing.filters
684
+ : {};
675
685
  const common = {
676
686
  ...existing,
677
687
  enabled: true,
678
- filters: historicSqlFiltersForSetup(input.args.queryHistoryServiceAccountPatterns),
688
+ filters: historicSqlFiltersForSetup(input.args.queryHistoryServiceAccountPatterns, existingFilters),
679
689
  };
680
690
  if (dialect === 'postgres') {
681
691
  return withQueryHistoryConfig(input.connection, {
@@ -689,9 +699,10 @@ async function maybeApplyHistoricSqlConfig(input) {
689
699
  redactionPatterns: input.args.queryHistoryRedactionPatterns ?? [],
690
700
  });
691
701
  }
692
- function historicSqlFiltersForSetup(patterns) {
702
+ function historicSqlFiltersForSetup(patterns, existingFilters = {}) {
693
703
  const serviceAccountPatterns = patterns ?? [];
694
704
  return {
705
+ ...existingFilters,
695
706
  dropTrivialProbes: true,
696
707
  ...(serviceAccountPatterns.length > 0
697
708
  ? {
@@ -716,29 +727,6 @@ async function defaultScanConnection(projectDir, connectionId, io) {
716
727
  dryRun: false,
717
728
  }, io);
718
729
  }
719
- function createBufferedCommandIo() {
720
- let stdout = '';
721
- let stderr = '';
722
- return {
723
- stdout: {
724
- isTTY: false,
725
- write(chunk) {
726
- stdout += chunk;
727
- },
728
- },
729
- stderr: {
730
- write(chunk) {
731
- stderr += chunk;
732
- },
733
- },
734
- stdoutText() {
735
- return stdout;
736
- },
737
- stderrText() {
738
- return stderr;
739
- },
740
- };
741
- }
742
730
  function envWithCurrentNodeFirst(env = process.env) {
743
731
  return {
744
732
  ...env,
@@ -890,6 +878,27 @@ async function disableConnectionQueryHistory(projectDir, connectionId) {
890
878
  connection: withQueryHistoryConfig(connection, { ...existing, enabled: false }),
891
879
  });
892
880
  }
881
+ function okValidateResult() {
882
+ return { status: 'ok' };
883
+ }
884
+ function backValidateResult() {
885
+ return { status: 'back' };
886
+ }
887
+ function failedValidateResult() {
888
+ return { status: 'failed' };
889
+ }
890
+ function queryHistoryUnavailableResult(projectDir, connectionId) {
891
+ return {
892
+ status: 'failed',
893
+ extraActions: [
894
+ {
895
+ value: 'disable-query-history',
896
+ label: 'Disable query history and retry',
897
+ run: () => disableConnectionQueryHistory(projectDir, connectionId),
898
+ },
899
+ ],
900
+ };
901
+ }
893
902
  async function createConnectionConfigRollback(projectDir, connectionId) {
894
903
  const project = await loadKtxProject({ projectDir });
895
904
  const previousConnection = project.config.connections[connectionId];
@@ -984,14 +993,14 @@ async function maybeConfigureDatabaseScope(input) {
984
993
  const connection = project.config.connections[input.connectionId];
985
994
  const driver = normalizeDriver(connection?.driver);
986
995
  if (!driver || driver === 'sqlite')
987
- return 'ready';
996
+ return okValidateResult();
988
997
  const spec = SCOPE_DISCOVERY_SPECS[driver];
989
998
  const existingTables = connection?.enabled_tables;
990
999
  const hasExistingTables = Array.isArray(existingTables) && existingTables.length > 0;
991
1000
  const existingScope = spec ? configuredScopeValues(connection, spec) : [];
992
1001
  const hasExistingScope = !spec || existingScope.length > 0;
993
1002
  if (hasExistingTables && hasExistingScope && input.forcePrompt !== true) {
994
- return 'ready';
1003
+ return okValidateResult();
995
1004
  }
996
1005
  const cliSchemas = input.args.databaseSchemas;
997
1006
  if (input.args.inputMode === 'disabled') {
@@ -1004,7 +1013,7 @@ async function maybeConfigureDatabaseScope(input) {
1004
1013
  catch (error) {
1005
1014
  const detail = error instanceof Error ? error.message : String(error);
1006
1015
  input.io.stderr.write(`Could not discover ${spec.promptLabel.toLowerCase()} for ${input.connectionId}; ${detail}\n`);
1007
- return 'ready';
1016
+ return okValidateResult();
1008
1017
  }
1009
1018
  }
1010
1019
  if (scopeToWrite.length > 0) {
@@ -1020,7 +1029,7 @@ async function maybeConfigureDatabaseScope(input) {
1020
1029
  ]);
1021
1030
  }
1022
1031
  }
1023
- return 'ready';
1032
+ return okValidateResult();
1024
1033
  }
1025
1034
  if (spec && cliSchemas.length > 0) {
1026
1035
  await writeScopeConfig({
@@ -1051,7 +1060,7 @@ async function maybeConfigureDatabaseScope(input) {
1051
1060
  spec,
1052
1061
  });
1053
1062
  if (typed === undefined)
1054
- return 'back';
1063
+ return backValidateResult();
1055
1064
  effectiveCliSchemas = typed;
1056
1065
  listedSchemas = typed;
1057
1066
  if (typed.length > 0) {
@@ -1066,7 +1075,7 @@ async function maybeConfigureDatabaseScope(input) {
1066
1075
  }
1067
1076
  const schemas = unique(listedSchemas);
1068
1077
  if (spec && schemas.length === 0) {
1069
- return 'ready';
1078
+ return okValidateResult();
1070
1079
  }
1071
1080
  const schemaSuggestion = effectiveCliSchemas.length > 0
1072
1081
  ? { excluded: new Set(), suggested: new Set(effectiveCliSchemas) }
@@ -1094,10 +1103,10 @@ async function maybeConfigureDatabaseScope(input) {
1094
1103
  writePrefixedLines((chunk) => input.io.stderr.write(chunk), input.forcePrompt === true
1095
1104
  ? `Could not discover tables for ${input.connectionId}; edit was not saved. ${detail}`
1096
1105
  : `Could not discover tables for ${input.connectionId}; continuing without table filter. ${detail}`);
1097
- return input.forcePrompt === true ? 'failed' : 'ready';
1106
+ return input.forcePrompt === true ? failedValidateResult() : okValidateResult();
1098
1107
  }
1099
1108
  if (pickResult.kind === 'back') {
1100
- return 'back';
1109
+ return backValidateResult();
1101
1110
  }
1102
1111
  const enabledTables = pickResult.enabledTables;
1103
1112
  const activeSchemas = pickResult.activeSchemas;
@@ -1112,7 +1121,7 @@ async function maybeConfigureDatabaseScope(input) {
1112
1121
  const refreshedProject = await loadKtxProject({ projectDir: input.projectDir });
1113
1122
  const currentConnection = refreshedProject.config.connections[input.connectionId];
1114
1123
  if (!currentConnection)
1115
- return 'ready';
1124
+ return okValidateResult();
1116
1125
  await writeConnectionConfig({
1117
1126
  projectDir: input.projectDir,
1118
1127
  connectionId: input.connectionId,
@@ -1128,7 +1137,7 @@ async function maybeConfigureDatabaseScope(input) {
1128
1137
  writeSetupSection(input.io, `Tables enabled for ${input.connectionId}`, [
1129
1138
  `✓ ${enabledTables.length} tables enabled`,
1130
1139
  ]);
1131
- return 'ready';
1140
+ return okValidateResult();
1132
1141
  }
1133
1142
  async function ensureHistoricSqlIngestDefaults(projectDir) {
1134
1143
  const project = await loadKtxProject({ projectDir });
@@ -1189,6 +1198,158 @@ async function maybeRunHistoricSqlSetupProbe(input) {
1189
1198
  }
1190
1199
  return result.ok;
1191
1200
  }
1201
+ function hasServiceAccountsBlock(connection) {
1202
+ const queryHistory = queryHistoryConfigRecord(connection);
1203
+ const filters = queryHistory?.filters;
1204
+ if (!filters || typeof filters !== 'object' || Array.isArray(filters)) {
1205
+ return false;
1206
+ }
1207
+ return 'serviceAccounts' in filters;
1208
+ }
1209
+ function printQueryHistoryFilterProposal(io, proposal) {
1210
+ if (proposal.excludedRoles.length === 0) {
1211
+ if (proposal.skipped?.reason === 'no-llm') {
1212
+ io.stdout.write('│ Query-history filter picker skipped: no LLM is configured.\n');
1213
+ }
1214
+ else if (proposal.skipped?.reason === 'no-daemon') {
1215
+ io.stdout.write('│ Query-history filter picker skipped: SQL analysis is unavailable.\n');
1216
+ }
1217
+ else if (proposal.skipped?.reason === 'no-in-scope-history') {
1218
+ io.stdout.write('│ Query-history filter picker found no in-scope service-account exclusions.\n');
1219
+ }
1220
+ for (const warning of proposal.warnings) {
1221
+ io.stdout.write(`│ ! ${warning}\n`);
1222
+ }
1223
+ return;
1224
+ }
1225
+ io.stdout.write('│ Proposed query-history service-account filters:\n');
1226
+ for (const excluded of proposal.excludedRoles) {
1227
+ io.stdout.write(`│ - ${excluded.role}: ${excluded.reason}\n`);
1228
+ }
1229
+ }
1230
+ async function shouldApplyQueryHistoryFilterProposal(input) {
1231
+ if (input.proposal.excludedRoles.length === 0 || input.proposal.skipped?.reason === 'user-block-present') {
1232
+ return false;
1233
+ }
1234
+ if (input.args.yes === true || input.args.inputMode === 'disabled') {
1235
+ return true;
1236
+ }
1237
+ const choice = await input.prompts.select({
1238
+ message: `Apply ${input.proposal.excludedRoles.length} derived query-history service-account exclusion${input.proposal.excludedRoles.length === 1 ? '' : 's'}?`,
1239
+ options: [
1240
+ { value: 'apply', label: 'Apply derived filters (recommended)' },
1241
+ { value: 'skip', label: 'Leave query history filters unchanged' },
1242
+ ],
1243
+ });
1244
+ return choice === 'apply';
1245
+ }
1246
+ function createSetupQueryHistoryLlmRuntime(input) {
1247
+ try {
1248
+ return (input.deps.createQueryHistoryLlmRuntime?.(input.projectDir, input.project) ??
1249
+ createLocalKtxLlmRuntimeFromConfig(input.project.config.llm, {
1250
+ projectDir: input.projectDir,
1251
+ }));
1252
+ }
1253
+ catch {
1254
+ return null;
1255
+ }
1256
+ }
1257
+ /** @internal */
1258
+ export function managedDaemonOptionsForSetupQueryHistoryPicker(input) {
1259
+ return {
1260
+ cliVersion: input.args.cliVersion ?? getKtxCliPackageInfo().version,
1261
+ projectDir: input.projectDir,
1262
+ installPolicy: input.args.runtimeInstallPolicy ?? (input.args.inputMode === 'disabled' ? 'never' : 'prompt'),
1263
+ io: input.io,
1264
+ };
1265
+ }
1266
+ async function maybeProposeQueryHistoryFilters(input) {
1267
+ const project = await loadKtxProject({ projectDir: input.projectDir });
1268
+ const connection = project.config.connections[input.connectionId];
1269
+ const queryHistory = queryHistoryConfigRecord(connection);
1270
+ if (!connection || queryHistory?.enabled !== true) {
1271
+ return;
1272
+ }
1273
+ const dialect = queryHistoryDialectForConnection(connection);
1274
+ if (!dialect) {
1275
+ return;
1276
+ }
1277
+ const picker = input.deps.queryHistoryFilterPicker ?? proposeQueryHistoryServiceAccountFilters;
1278
+ const llmRuntime = createSetupQueryHistoryLlmRuntime({
1279
+ projectDir: input.projectDir,
1280
+ project,
1281
+ deps: input.deps,
1282
+ });
1283
+ if (!llmRuntime && !input.deps.queryHistoryFilterPicker) {
1284
+ printQueryHistoryFilterProposal(input.io, {
1285
+ excludedRoles: [],
1286
+ consideredRoleCount: 0,
1287
+ skipped: { reason: 'no-llm' },
1288
+ warnings: [],
1289
+ });
1290
+ return;
1291
+ }
1292
+ const runtime = createKtxCliHistoricSqlRuntime(project, input.connectionId, {
1293
+ managedDaemon: managedDaemonOptionsForSetupQueryHistoryPicker({
1294
+ projectDir: input.projectDir,
1295
+ args: input.args,
1296
+ io: input.io,
1297
+ }),
1298
+ });
1299
+ if (!runtime) {
1300
+ return;
1301
+ }
1302
+ const userServiceAccountsPresent = hasServiceAccountsBlock(connection);
1303
+ const scopeFloor = await resolveQueryHistoryScopeFloor({
1304
+ projectDir: input.projectDir,
1305
+ connectionId: input.connectionId,
1306
+ driver: String(connection.driver ?? ''),
1307
+ connection: connection,
1308
+ storedQueryHistory: queryHistory,
1309
+ });
1310
+ const pullConfig = queryHistoryPullConfig({
1311
+ stored: queryHistory,
1312
+ dialect,
1313
+ enabledTables: scopeFloor.enabledTables,
1314
+ enabledSchemas: scopeFloor.enabledSchemas,
1315
+ modeledTableCatalog: scopeFloor.modeledTableCatalog,
1316
+ scopeFloorWarnings: scopeFloor.warnings,
1317
+ });
1318
+ const proposal = await picker({
1319
+ connectionId: input.connectionId,
1320
+ dialect,
1321
+ queryClient: runtime.queryClient,
1322
+ reader: runtime.reader,
1323
+ sqlAnalysis: runtime.sqlAnalysis,
1324
+ llmRuntime,
1325
+ pullConfig,
1326
+ userServiceAccountsPresent,
1327
+ });
1328
+ printQueryHistoryFilterProposal(input.io, proposal);
1329
+ if (proposal.skipped?.reason === 'user-block-present') {
1330
+ input.io.stdout.write('│ Existing query-history service-account filters left unchanged.\n');
1331
+ return;
1332
+ }
1333
+ if (!(await shouldApplyQueryHistoryFilterProposal({ args: input.args, prompts: input.prompts, proposal }))) {
1334
+ return;
1335
+ }
1336
+ await writeConnectionConfig({
1337
+ projectDir: input.projectDir,
1338
+ connectionId: input.connectionId,
1339
+ connection: withQueryHistoryConfig(connection, {
1340
+ ...queryHistory,
1341
+ filters: {
1342
+ ...(queryHistory.filters && typeof queryHistory.filters === 'object' && !Array.isArray(queryHistory.filters)
1343
+ ? queryHistory.filters
1344
+ : {}),
1345
+ serviceAccounts: {
1346
+ mode: 'exclude',
1347
+ patterns: proposal.excludedRoles.map((role) => role.pattern),
1348
+ },
1349
+ },
1350
+ }),
1351
+ });
1352
+ }
1192
1353
  async function applyHistoricSqlConfigToExistingConnection(input) {
1193
1354
  if (input.args.inputMode === 'disabled' &&
1194
1355
  input.args.enableQueryHistory !== true &&
@@ -1209,39 +1370,10 @@ async function applyHistoricSqlConfigToExistingConnection(input) {
1209
1370
  });
1210
1371
  if (withHistoricSql === 'back')
1211
1372
  return 'back';
1212
- const withContextDepth = await maybeApplyContextDepthConfig({
1213
- projectDir: input.projectDir,
1214
- connectionId: input.connectionId,
1215
- connection: withHistoricSql,
1216
- args: input.args,
1217
- prompts: input.prompts,
1218
- });
1219
- if (withContextDepth === 'back')
1220
- return 'back';
1221
1373
  await writeConnectionConfig({
1222
1374
  projectDir: input.projectDir,
1223
1375
  connectionId: input.connectionId,
1224
- connection: withContextDepth,
1225
- });
1226
- }
1227
- async function maybeApplyContextDepthConfig(input) {
1228
- const project = await loadKtxProject({ projectDir: input.projectDir });
1229
- return await applySetupDatabaseContextDepth({
1230
- project: {
1231
- ...project,
1232
- config: {
1233
- ...project.config,
1234
- connections: {
1235
- ...project.config.connections,
1236
- [input.connectionId]: input.connection,
1237
- },
1238
- },
1239
- },
1240
- connection: input.connection,
1241
- args: {
1242
- inputMode: input.args.inputMode === 'disabled' || input.args.databaseUrl ? 'disabled' : input.args.inputMode,
1243
- },
1244
- prompts: input.prompts,
1376
+ connection: withHistoricSql,
1245
1377
  });
1246
1378
  }
1247
1379
  async function validateAndScanConnection(input) {
@@ -1255,7 +1387,7 @@ async function validateAndScanConnection(input) {
1255
1387
  if (testCode !== 0) {
1256
1388
  flushPrefixedBufferedCommandOutput(input.io, testIo);
1257
1389
  writePrefixedLines((chunk) => input.io.stderr.write(chunk), `Connection test failed for ${input.connectionId}.`);
1258
- return 'failed';
1390
+ return failedValidateResult();
1259
1391
  }
1260
1392
  const testOutput = testIo.stdoutText();
1261
1393
  const outputDriver = normalizeDriver(readOutputValue(testOutput, 'Driver'));
@@ -1263,7 +1395,7 @@ async function validateAndScanConnection(input) {
1263
1395
  const testLines = ['✓ Connection test passed', `Driver: ${driverDisplay}`];
1264
1396
  writeSetupSection(input.io, `Testing ${input.connectionId}`, testLines);
1265
1397
  const scopeStatus = await maybeConfigureDatabaseScope({ ...input, forcePrompt: input.forceScopeAndTables });
1266
- if (scopeStatus !== 'ready') {
1398
+ if (scopeStatus.status !== 'ok') {
1267
1399
  return scopeStatus;
1268
1400
  }
1269
1401
  const queryHistoryAvailable = await maybeRunHistoricSqlSetupProbe({
@@ -1273,7 +1405,7 @@ async function validateAndScanConnection(input) {
1273
1405
  deps: input.deps,
1274
1406
  });
1275
1407
  writeSetupSection(input.io, `Building schema context for ${input.connectionId}`, [
1276
- 'Running fast database ingest…',
1408
+ 'Running database scan…',
1277
1409
  ]);
1278
1410
  let scanIo = createBufferedCommandIo();
1279
1411
  let scanCode = await scanConnection(input.projectDir, input.connectionId, scanIo);
@@ -1281,7 +1413,7 @@ async function validateAndScanConnection(input) {
1281
1413
  const nativeSqliteDetail = nativeSqliteAbiMismatchDetail(`${scanIo.stderrText()}\n${scanIo.stdoutText()}`);
1282
1414
  if (nativeSqliteDetail) {
1283
1415
  writePrefixedLines((chunk) => input.io.stderr.write(chunk), [
1284
- `Fast database ingest failed for ${input.connectionId}.`,
1416
+ `Database scan failed for ${input.connectionId}.`,
1285
1417
  'Native SQLite is built for a different Node.js ABI.',
1286
1418
  `Detail: ${nativeSqliteDetail}`,
1287
1419
  'Rebuilding Native SQLite with pnpm run native:rebuild…',
@@ -1289,7 +1421,7 @@ async function validateAndScanConnection(input) {
1289
1421
  const rebuildNativeSqlite = input.deps.rebuildNativeSqlite ?? defaultRebuildNativeSqlite;
1290
1422
  const rebuildCode = await rebuildNativeSqlite(input.io);
1291
1423
  if (rebuildCode === 0) {
1292
- writePrefixedLines((chunk) => input.io.stderr.write(chunk), 'Native SQLite rebuild complete. Retrying fast database ingest…');
1424
+ writePrefixedLines((chunk) => input.io.stderr.write(chunk), 'Native SQLite rebuild complete. Retrying database scan…');
1293
1425
  const retryScanIo = createBufferedCommandIo();
1294
1426
  scanCode = await scanConnection(input.projectDir, input.connectionId, retryScanIo);
1295
1427
  scanIo = retryScanIo;
@@ -1297,30 +1429,42 @@ async function validateAndScanConnection(input) {
1297
1429
  if (scanCode !== 0) {
1298
1430
  writePrefixedLines((chunk) => input.io.stderr.write(chunk), [
1299
1431
  rebuildCode === 0
1300
- ? `Fast database ingest still failed for ${input.connectionId} after rebuilding Native SQLite.`
1432
+ ? `Database scan still failed for ${input.connectionId} after rebuilding Native SQLite.`
1301
1433
  : `Native SQLite rebuild failed for ${input.connectionId}.`,
1302
1434
  'Fix: pnpm run native:rebuild',
1303
- `Retry: ktx ingest ${input.connectionId} --project-dir ${input.projectDir} --fast`,
1435
+ `Retry: ktx ingest ${input.connectionId} --project-dir ${input.projectDir}`,
1304
1436
  ].join('\n'));
1305
1437
  }
1306
1438
  }
1307
1439
  else {
1308
1440
  flushPrefixedBufferedCommandOutput(input.io, scanIo);
1309
1441
  writePrefixedLines((chunk) => input.io.stderr.write(chunk), [
1310
- `Fast database ingest failed for ${input.connectionId}.`,
1311
- `Debug command: ktx ingest ${input.connectionId} --project-dir ${input.projectDir} --fast --debug`,
1442
+ `Database scan failed for ${input.connectionId}.`,
1443
+ `Debug command: ktx ingest ${input.connectionId} --project-dir ${input.projectDir} --debug`,
1312
1444
  ].join('\n'));
1313
1445
  }
1314
1446
  if (scanCode !== 0) {
1315
- return queryHistoryAvailable ? 'failed' : 'failed-query-history-unavailable';
1447
+ return queryHistoryAvailable
1448
+ ? failedValidateResult()
1449
+ : queryHistoryUnavailableResult(input.projectDir, input.connectionId);
1316
1450
  }
1317
1451
  }
1318
1452
  const scanOutput = scanIo.stdoutText();
1319
1453
  writeSetupSection(input.io, `Schema context complete for ${input.connectionId}`, [`Changes: ${summarizeScanChanges(scanOutput)}`]);
1454
+ if (queryHistoryAvailable) {
1455
+ await maybeProposeQueryHistoryFilters({
1456
+ projectDir: input.projectDir,
1457
+ connectionId: input.connectionId,
1458
+ io: input.io,
1459
+ deps: input.deps,
1460
+ args: input.args,
1461
+ prompts: input.prompts,
1462
+ });
1463
+ }
1320
1464
  writeSetupSection(input.io, 'Database ready', [
1321
1465
  `${input.connectionId} · ${driverDisplay} · schema context complete`,
1322
1466
  ]);
1323
- return 'ready';
1467
+ return okValidateResult();
1324
1468
  }
1325
1469
  async function chooseDrivers(args, io, prompts, options) {
1326
1470
  if (args.databaseDrivers && args.databaseDrivers.length > 0) {
@@ -1334,7 +1478,7 @@ async function chooseDrivers(args, io, prompts, options) {
1334
1478
  return 'missing-input';
1335
1479
  }
1336
1480
  const initialValues = unique(options?.initialDrivers ?? []);
1337
- createKtxSetupUiAdapter().note(`Get demo credentials from the Quickstart: ${KTX_QUICKSTART_URL}`, '🎁 Need a warehouse to play with?', io);
1481
+ createKtxSetupUiAdapter().note(`Get demo credentials: ${KTX_DEMO_START_URL}`, '🎁 Need a warehouse to play with?', io);
1338
1482
  const choices = await prompts.multiselect({
1339
1483
  message: withMultiselectNavigation('Which databases should KTX connect to?'),
1340
1484
  options: [...DRIVER_OPTIONS],
@@ -1424,64 +1568,137 @@ async function choosePrimarySourceToEdit(input) {
1424
1568
  });
1425
1569
  return choice === 'back' ? 'back' : choice;
1426
1570
  }
1427
- async function runPrimarySourceFullEdit(input) {
1571
+ async function configureDatabaseConnection(input) {
1428
1572
  const project = await loadKtxProject({ projectDir: input.projectDir });
1429
- const existing = project.config.connections[input.connectionId];
1430
- const driver = normalizeDriver(existing?.driver);
1431
- if (!existing || !driver) {
1432
- writePrefixedLines((chunk) => input.io.stderr.write(chunk), `Connection "${input.connectionId}" is not a configured database.`);
1433
- return 'failed';
1434
- }
1435
- const rollback = await createConnectionConfigRollback(input.projectDir, input.connectionId);
1436
- const replacement = await buildConnectionConfig({
1437
- driver,
1573
+ const latestConnection = project.config.connections[input.connectionId];
1574
+ let connection = await buildConnectionConfig({
1575
+ driver: input.driver,
1438
1576
  connectionId: input.connectionId,
1439
1577
  args: input.args,
1440
1578
  prompts: input.prompts,
1441
- existingConnection: existing,
1579
+ existingConnection: latestConnection,
1442
1580
  });
1443
- if (replacement === 'back') {
1444
- await rollback();
1581
+ while (!connection && input.args.inputMode !== 'disabled') {
1582
+ const action = await input.prompts.select(missingConnectionDetailsPrompt(driverLabel(input.driver), input.canReturnToDriverSelection));
1583
+ if (action === 'back') {
1584
+ return 'back';
1585
+ }
1586
+ connection = await buildConnectionConfig({
1587
+ driver: input.driver,
1588
+ connectionId: input.connectionId,
1589
+ args: input.args,
1590
+ prompts: input.prompts,
1591
+ existingConnection: latestConnection,
1592
+ });
1593
+ }
1594
+ if (connection === 'back') {
1445
1595
  return 'back';
1446
1596
  }
1447
- if (!replacement) {
1448
- await rollback();
1449
- return 'failed';
1597
+ if (!connection) {
1598
+ input.io.stderr.write(`Missing connection details for ${driverLabel(input.driver)}.\n`);
1599
+ return 'cancelled';
1450
1600
  }
1451
1601
  const withHistoricSql = await maybeApplyHistoricSqlConfig({
1452
- connection: replacement,
1453
- driver,
1602
+ connection,
1603
+ driver: input.driver,
1454
1604
  args: input.args,
1455
1605
  prompts: input.prompts,
1456
1606
  });
1457
1607
  if (withHistoricSql === 'back') {
1458
- await rollback();
1459
1608
  return 'back';
1460
1609
  }
1461
1610
  await writeConnectionConfig({
1462
1611
  projectDir: input.projectDir,
1463
1612
  connectionId: input.connectionId,
1464
- connection: withExistingPrimaryEditPromptDefaults({
1465
- previous: existing,
1466
- next: withHistoricSql,
1467
- driver,
1468
- }),
1613
+ connection: input.editBaseline
1614
+ ? withExistingPrimaryEditPromptDefaults({
1615
+ previous: input.editBaseline,
1616
+ next: withHistoricSql,
1617
+ driver: input.driver,
1618
+ })
1619
+ : withHistoricSql,
1620
+ io: input.io,
1621
+ });
1622
+ return 'configured';
1623
+ }
1624
+ async function runDatabaseConnectionSetupWithRecovery(input) {
1625
+ let configureCalls = 0;
1626
+ // `configureDatabaseConnection` returns 'cancelled' only when required
1627
+ // connection details are absent in non-interactive mode. The recovery
1628
+ // primitive collapses that into 'failed', so we track it here to restore the
1629
+ // distinct 'missing-input' outcome the surrounding step reports for
1630
+ // incomplete flags (vs. a real connection/probe failure).
1631
+ let sawMissingInput = false;
1632
+ const outcome = await runConnectionSetupWithRecovery({
1633
+ label: input.connectionId,
1634
+ interactive: input.interactive ?? input.args.inputMode !== 'disabled',
1635
+ allowSkip: input.allowSkip,
1469
1636
  io: input.io,
1637
+ prompts: input.prompts,
1638
+ snapshot: () => createConnectionConfigRollback(input.projectDir, input.connectionId),
1639
+ configure: async () => {
1640
+ configureCalls += 1;
1641
+ if (input.reuseExistingOnFirstConfigure && configureCalls === 1) {
1642
+ const historicSqlResult = await applyHistoricSqlConfigToExistingConnection({
1643
+ projectDir: input.projectDir,
1644
+ connectionId: input.connectionId,
1645
+ args: input.args,
1646
+ prompts: input.prompts,
1647
+ });
1648
+ return historicSqlResult === 'back' ? 'back' : 'configured';
1649
+ }
1650
+ const configured = await configureDatabaseConnection({
1651
+ projectDir: input.projectDir,
1652
+ connectionId: input.connectionId,
1653
+ driver: input.driver,
1654
+ args: input.args,
1655
+ prompts: input.prompts,
1656
+ io: input.io,
1657
+ canReturnToDriverSelection: input.canReturnToDriverSelection,
1658
+ editBaseline: input.editBaseline,
1659
+ });
1660
+ if (configured === 'cancelled') {
1661
+ sawMissingInput = true;
1662
+ }
1663
+ return configured;
1664
+ },
1665
+ validate: () => validateAndScanConnection({
1666
+ projectDir: input.projectDir,
1667
+ connectionId: input.connectionId,
1668
+ io: input.io,
1669
+ deps: input.deps,
1670
+ args: input.args,
1671
+ prompts: input.prompts,
1672
+ forceScopeAndTables: input.forceScopeAndTables,
1673
+ }),
1470
1674
  });
1471
- const validated = await validateAndScanConnection({
1675
+ if (outcome === 'failed' && sawMissingInput) {
1676
+ return 'missing-input';
1677
+ }
1678
+ return outcome;
1679
+ }
1680
+ async function runPrimarySourceFullEdit(input) {
1681
+ const project = await loadKtxProject({ projectDir: input.projectDir });
1682
+ const existing = project.config.connections[input.connectionId];
1683
+ const driver = normalizeDriver(existing?.driver);
1684
+ if (!existing || !driver) {
1685
+ writePrefixedLines((chunk) => input.io.stderr.write(chunk), `Connection "${input.connectionId}" is not a configured database.`);
1686
+ return 'failed';
1687
+ }
1688
+ const outcome = await runDatabaseConnectionSetupWithRecovery({
1472
1689
  projectDir: input.projectDir,
1473
1690
  connectionId: input.connectionId,
1474
- io: input.io,
1475
- deps: input.deps,
1691
+ driver,
1476
1692
  args: input.args,
1477
1693
  prompts: input.prompts,
1694
+ io: input.io,
1695
+ deps: input.deps,
1696
+ canReturnToDriverSelection: true,
1697
+ allowSkip: false,
1478
1698
  forceScopeAndTables: true,
1699
+ editBaseline: existing,
1479
1700
  });
1480
- if (validated !== 'ready') {
1481
- await rollback();
1482
- return validated === 'failed-query-history-unavailable' ? 'failed' : validated;
1483
- }
1484
- return 'ready';
1701
+ return outcome === 'skip' ? 'back' : outcome;
1485
1702
  }
1486
1703
  export async function runKtxSetupDatabasesStep(args, io, deps = {}) {
1487
1704
  if (args.skipDatabases) {
@@ -1492,29 +1709,37 @@ export async function runKtxSetupDatabasesStep(args, io, deps = {}) {
1492
1709
  if (args.databaseConnectionIds && args.databaseConnectionIds.length > 0) {
1493
1710
  const selectedConnectionIds = [];
1494
1711
  for (const connectionId of unique(args.databaseConnectionIds)) {
1495
- const historicSqlResult = await applyHistoricSqlConfigToExistingConnection({
1712
+ const project = await loadKtxProject({ projectDir: args.projectDir });
1713
+ const driver = normalizeDriver(project.config.connections[connectionId]?.driver);
1714
+ if (!driver) {
1715
+ writePrefixedLines((chunk) => io.stderr.write(chunk), `Connection "${connectionId}" is not configured.`);
1716
+ return { status: 'failed', projectDir: args.projectDir };
1717
+ }
1718
+ const setupOutcome = await runDatabaseConnectionSetupWithRecovery({
1496
1719
  projectDir: args.projectDir,
1497
1720
  connectionId,
1721
+ driver,
1498
1722
  args,
1499
1723
  prompts,
1500
- });
1501
- if (historicSqlResult === 'back')
1502
- return { status: 'back', projectDir: args.projectDir };
1503
- const setupStatus = await validateAndScanConnection({
1504
- projectDir: args.projectDir,
1505
- connectionId,
1506
1724
  io,
1507
1725
  deps,
1508
- args,
1509
- prompts,
1726
+ canReturnToDriverSelection: false,
1727
+ allowSkip: false,
1728
+ interactive: false,
1729
+ reuseExistingOnFirstConfigure: true,
1510
1730
  });
1511
- if (setupStatus === 'back') {
1731
+ if (setupOutcome === 'back') {
1512
1732
  return { status: 'back', projectDir: args.projectDir };
1513
1733
  }
1514
- if (setupStatus === 'failed') {
1734
+ if (setupOutcome === 'missing-input') {
1735
+ return { status: 'missing-input', projectDir: args.projectDir };
1736
+ }
1737
+ if (setupOutcome === 'failed') {
1515
1738
  return { status: 'failed', projectDir: args.projectDir };
1516
1739
  }
1517
- selectedConnectionIds.push(connectionId);
1740
+ if (setupOutcome === 'ready') {
1741
+ selectedConnectionIds.push(connectionId);
1742
+ }
1518
1743
  }
1519
1744
  await markDatabasesComplete(args.projectDir, selectedConnectionIds);
1520
1745
  return { status: 'ready', projectDir: args.projectDir, connectionIds: selectedConnectionIds };
@@ -1563,6 +1788,9 @@ export async function runKtxSetupDatabasesStep(args, io, deps = {}) {
1563
1788
  showConfiguredPrimaryMenu = true;
1564
1789
  continue;
1565
1790
  }
1791
+ if (editResult === 'missing-input') {
1792
+ return { status: 'missing-input', projectDir: args.projectDir };
1793
+ }
1566
1794
  if (editResult === 'failed') {
1567
1795
  return { status: 'failed', projectDir: args.projectDir };
1568
1796
  }
@@ -1617,7 +1845,6 @@ export async function runKtxSetupDatabasesStep(args, io, deps = {}) {
1617
1845
  io.stderr.write('Missing database connection id: pass --database-connection-id.\n');
1618
1846
  return { status: 'missing-input', projectDir: args.projectDir };
1619
1847
  }
1620
- let connectionAlreadyValidated = false;
1621
1848
  if (connectionChoice.kind === 'edit') {
1622
1849
  const editResult = await runPrimarySourceFullEdit({
1623
1850
  projectDir: args.projectDir,
@@ -1633,231 +1860,44 @@ export async function runKtxSetupDatabasesStep(args, io, deps = {}) {
1633
1860
  returnToDriverSelection = true;
1634
1861
  break;
1635
1862
  }
1636
- if (editResult === 'failed') {
1637
- return { status: 'failed', projectDir: args.projectDir };
1638
- }
1639
- connectionAlreadyValidated = true;
1640
- }
1641
- else if (connectionChoice.kind === 'new') {
1642
- let connection = await buildConnectionConfig({
1643
- driver,
1644
- connectionId: connectionChoice.connectionId,
1645
- args,
1646
- prompts,
1647
- });
1648
- if (connection === 'back') {
1649
- if (!canReturnToDriverSelection)
1650
- return { status: 'back', projectDir: args.projectDir };
1651
- returnToDriverSelection = true;
1652
- break;
1653
- }
1654
- while (!connection && args.inputMode !== 'disabled') {
1655
- const label = driverLabel(driver);
1656
- const action = await prompts.select(missingConnectionDetailsPrompt(label, canReturnToDriverSelection));
1657
- if (action === 'back') {
1658
- if (!canReturnToDriverSelection)
1659
- return { status: 'back', projectDir: args.projectDir };
1660
- returnToDriverSelection = true;
1661
- break;
1662
- }
1663
- connection = await buildConnectionConfig({
1664
- driver,
1665
- connectionId: connectionChoice.connectionId,
1666
- args,
1667
- prompts,
1668
- });
1669
- if (connection === 'back') {
1670
- if (!canReturnToDriverSelection)
1671
- return { status: 'back', projectDir: args.projectDir };
1672
- returnToDriverSelection = true;
1673
- break;
1674
- }
1675
- }
1676
- if (returnToDriverSelection) {
1677
- break;
1678
- }
1679
- if (connection === 'back') {
1680
- break;
1681
- }
1682
- if (!connection) {
1683
- io.stderr.write(`Missing connection details for ${driverLabel(driver)}.\n`);
1863
+ if (editResult === 'missing-input') {
1684
1864
  return { status: 'missing-input', projectDir: args.projectDir };
1685
1865
  }
1686
- const withHistoricSql = await maybeApplyHistoricSqlConfig({ connection, driver, args, prompts });
1687
- if (withHistoricSql === 'back') {
1688
- if (!canReturnToDriverSelection)
1689
- return { status: 'back', projectDir: args.projectDir };
1690
- returnToDriverSelection = true;
1691
- break;
1692
- }
1693
- const withContextDepth = await maybeApplyContextDepthConfig({
1694
- projectDir: args.projectDir,
1695
- connectionId: connectionChoice.connectionId,
1696
- connection: withHistoricSql,
1697
- args,
1698
- prompts,
1699
- });
1700
- if (withContextDepth === 'back') {
1701
- if (!canReturnToDriverSelection)
1702
- return { status: 'back', projectDir: args.projectDir };
1703
- returnToDriverSelection = true;
1704
- break;
1866
+ if (editResult === 'failed') {
1867
+ return { status: 'failed', projectDir: args.projectDir };
1705
1868
  }
1706
- await writeConnectionConfig({
1707
- projectDir: args.projectDir,
1708
- connectionId: connectionChoice.connectionId,
1709
- connection: withContextDepth,
1710
- io,
1711
- });
1712
1869
  }
1713
1870
  else {
1714
- const existing = project.config.connections[connectionChoice.connectionId];
1715
- const withHistoricSql = await maybeApplyHistoricSqlConfig({ connection: existing, driver, args, prompts });
1716
- if (withHistoricSql === 'back') {
1717
- if (!canReturnToDriverSelection)
1718
- return { status: 'back', projectDir: args.projectDir };
1719
- returnToDriverSelection = true;
1720
- break;
1721
- }
1722
- const withContextDepth = await maybeApplyContextDepthConfig({
1871
+ const setupOutcome = await runDatabaseConnectionSetupWithRecovery({
1723
1872
  projectDir: args.projectDir,
1724
1873
  connectionId: connectionChoice.connectionId,
1725
- connection: withHistoricSql,
1874
+ driver,
1726
1875
  args,
1727
1876
  prompts,
1728
- });
1729
- if (withContextDepth === 'back') {
1730
- if (!canReturnToDriverSelection)
1731
- return { status: 'back', projectDir: args.projectDir };
1732
- returnToDriverSelection = true;
1733
- break;
1734
- }
1735
- await writeConnectionConfig({
1736
- projectDir: args.projectDir,
1737
- connectionId: connectionChoice.connectionId,
1738
- connection: withContextDepth,
1739
- io,
1740
- });
1741
- }
1742
- let connectionSkipped = false;
1743
- let setupStatus = connectionAlreadyValidated
1744
- ? 'ready'
1745
- : await validateAndScanConnection({
1746
- projectDir: args.projectDir,
1747
- connectionId: connectionChoice.connectionId,
1748
1877
  io,
1749
1878
  deps,
1750
- args,
1751
- prompts,
1752
- });
1753
- while (!connectionAlreadyValidated && setupStatus !== 'ready') {
1754
- if (setupStatus === 'back') {
1755
- if (!canReturnToDriverSelection)
1756
- return { status: 'back', projectDir: args.projectDir };
1757
- returnToDriverSelection = true;
1758
- break;
1759
- }
1760
- if (args.inputMode === 'disabled')
1761
- return { status: 'failed', projectDir: args.projectDir };
1762
- const failureOptions = [
1763
- { value: 'retry', label: 'Retry connection test' },
1764
- { value: 're-enter', label: 'Re-enter connection details' },
1765
- ...(setupStatus === 'failed-query-history-unavailable'
1766
- ? [{ value: 'disable-query-history', label: 'Disable query history and retry' }]
1767
- : []),
1768
- { value: 'skip', label: 'Skip this database' },
1769
- { value: 'back', label: 'Back' },
1770
- ];
1771
- const action = await prompts.select({
1772
- message: `Database setup failed for ${connectionChoice.connectionId}`,
1773
- options: failureOptions,
1879
+ canReturnToDriverSelection,
1880
+ allowSkip: true,
1881
+ reuseExistingOnFirstConfigure: connectionChoice.kind === 'existing',
1774
1882
  });
1775
- if (action === 'back') {
1883
+ if (setupOutcome === 'back') {
1776
1884
  if (!canReturnToDriverSelection)
1777
1885
  return { status: 'back', projectDir: args.projectDir };
1778
1886
  returnToDriverSelection = true;
1779
1887
  break;
1780
1888
  }
1781
- if (action === 'skip') {
1782
- connectionSkipped = true;
1783
- break;
1784
- }
1785
- if (action === 'retry') {
1786
- setupStatus = await validateAndScanConnection({
1787
- projectDir: args.projectDir,
1788
- connectionId: connectionChoice.connectionId,
1789
- io,
1790
- deps,
1791
- args,
1792
- prompts,
1793
- });
1889
+ if (setupOutcome === 'missing-input') {
1890
+ return { status: 'missing-input', projectDir: args.projectDir };
1794
1891
  }
1795
- else if (action === 'disable-query-history') {
1796
- await disableConnectionQueryHistory(args.projectDir, connectionChoice.connectionId);
1797
- setupStatus = await validateAndScanConnection({
1798
- projectDir: args.projectDir,
1799
- connectionId: connectionChoice.connectionId,
1800
- io,
1801
- deps,
1802
- args,
1803
- prompts,
1804
- });
1892
+ if (setupOutcome === 'failed') {
1893
+ return { status: 'failed', projectDir: args.projectDir };
1805
1894
  }
1806
- else if (action === 're-enter') {
1807
- const connection = await buildConnectionConfig({
1808
- driver,
1809
- connectionId: connectionChoice.connectionId,
1810
- args,
1811
- prompts,
1812
- });
1813
- if (connection === 'back') {
1814
- if (!canReturnToDriverSelection)
1815
- return { status: 'back', projectDir: args.projectDir };
1816
- returnToDriverSelection = true;
1817
- break;
1818
- }
1819
- if (!connection)
1820
- continue;
1821
- const withHistoricSql = await maybeApplyHistoricSqlConfig({ connection, driver, args, prompts });
1822
- if (withHistoricSql === 'back') {
1823
- if (!canReturnToDriverSelection)
1824
- return { status: 'back', projectDir: args.projectDir };
1825
- returnToDriverSelection = true;
1826
- break;
1827
- }
1828
- const withContextDepth = await maybeApplyContextDepthConfig({
1829
- projectDir: args.projectDir,
1830
- connectionId: connectionChoice.connectionId,
1831
- connection: withHistoricSql,
1832
- args,
1833
- prompts,
1834
- });
1835
- if (withContextDepth === 'back') {
1836
- if (!canReturnToDriverSelection)
1837
- return { status: 'back', projectDir: args.projectDir };
1838
- returnToDriverSelection = true;
1839
- break;
1840
- }
1841
- await writeConnectionConfig({
1842
- projectDir: args.projectDir,
1843
- connectionId: connectionChoice.connectionId,
1844
- connection: withContextDepth,
1845
- io,
1846
- });
1847
- setupStatus = await validateAndScanConnection({
1848
- projectDir: args.projectDir,
1849
- connectionId: connectionChoice.connectionId,
1850
- io,
1851
- deps,
1852
- args,
1853
- prompts,
1854
- });
1895
+ if (setupOutcome === 'skip') {
1896
+ continue;
1855
1897
  }
1856
1898
  }
1857
1899
  if (returnToDriverSelection)
1858
1900
  break;
1859
- if (connectionSkipped)
1860
- continue;
1861
1901
  pushUniqueConnectionId(selectedConnectionIds, connectionChoice.connectionId);
1862
1902
  }
1863
1903
  if (returnToDriverSelection) {