@kaelio/ktx 0.8.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 (83) hide show
  1. package/assets/python/{kaelio_ktx-0.8.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-runtime.js +50 -3
  5. package/dist/commands/setup-commands.js +1 -1
  6. package/dist/connection-recovery.d.ts +34 -0
  7. package/dist/connection-recovery.js +82 -0
  8. package/dist/connection.js +3 -1
  9. package/dist/context/ingest/adapters/historic-sql/bigquery-query-history-reader.js +71 -20
  10. package/dist/context/ingest/adapters/historic-sql/chunk-unified.js +2 -1
  11. package/dist/context/ingest/adapters/historic-sql/connection-dialect.d.ts +9 -0
  12. package/dist/context/ingest/adapters/historic-sql/connection-dialect.js +15 -4
  13. package/dist/context/ingest/adapters/historic-sql/pattern-inputs.js +8 -2
  14. package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.d.ts +29 -0
  15. package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.js +190 -0
  16. package/dist/context/ingest/adapters/historic-sql/scope-floor.d.ts +18 -0
  17. package/dist/context/ingest/adapters/historic-sql/scope-floor.js +229 -0
  18. package/dist/context/ingest/adapters/historic-sql/scope-membership.d.ts +8 -0
  19. package/dist/context/ingest/adapters/historic-sql/scope-membership.js +29 -0
  20. package/dist/context/ingest/adapters/historic-sql/snowflake-query-history-reader.js +68 -19
  21. package/dist/context/ingest/adapters/historic-sql/stage-unified.js +57 -50
  22. package/dist/context/ingest/adapters/historic-sql/types.d.ts +36 -3
  23. package/dist/context/ingest/adapters/historic-sql/types.js +14 -2
  24. package/dist/context/ingest/context-evidence/sqlite-context-evidence-store.d.ts +1 -1
  25. package/dist/context/ingest/isolated-diff/patch-integrator.js +75 -5
  26. package/dist/context/ingest/local-adapters.js +21 -4
  27. package/dist/context/ingest/local-bundle-runtime.js +3 -2
  28. package/dist/context/llm/codex-exec-events.d.ts +20 -0
  29. package/dist/context/llm/codex-exec-events.js +155 -0
  30. package/dist/context/llm/codex-isolation.d.ts +3 -0
  31. package/dist/context/llm/codex-isolation.js +5 -0
  32. package/dist/context/llm/codex-mcp-runtime-server.d.ts +24 -0
  33. package/dist/context/llm/codex-mcp-runtime-server.js +51 -0
  34. package/dist/context/llm/codex-models.d.ts +2 -0
  35. package/dist/context/llm/codex-models.js +17 -0
  36. package/dist/context/llm/codex-runtime-config.d.ts +16 -0
  37. package/dist/context/llm/codex-runtime-config.js +19 -0
  38. package/dist/context/llm/codex-runtime.d.ts +37 -0
  39. package/dist/context/llm/codex-runtime.js +304 -0
  40. package/dist/context/llm/codex-sdk-runner.d.ts +21 -0
  41. package/dist/context/llm/codex-sdk-runner.js +63 -0
  42. package/dist/context/llm/local-config.d.ts +2 -0
  43. package/dist/context/llm/local-config.js +12 -1
  44. package/dist/context/project/config.d.ts +2 -0
  45. package/dist/context/project/config.js +2 -2
  46. package/dist/context/sql-analysis/http-sql-analysis-port.js +32 -2
  47. package/dist/context/sql-analysis/ports.d.ts +12 -2
  48. package/dist/context/tools/context-candidate-mark.tool.d.ts +2 -2
  49. package/dist/context-build-view.js +4 -32
  50. package/dist/io/buffered-command-io.d.ts +11 -0
  51. package/dist/io/buffered-command-io.js +28 -0
  52. package/dist/llm/types.d.ts +1 -1
  53. package/dist/local-adapters.d.ts +10 -2
  54. package/dist/local-adapters.js +19 -3
  55. package/dist/next-steps.js +1 -2
  56. package/dist/progress-port-adapter.d.ts +6 -0
  57. package/dist/progress-port-adapter.js +18 -0
  58. package/dist/public-ingest.d.ts +20 -1
  59. package/dist/public-ingest.js +178 -27
  60. package/dist/scan.js +3 -1
  61. package/dist/setup-context.d.ts +2 -0
  62. package/dist/setup-context.js +133 -27
  63. package/dist/setup-databases.d.ts +17 -1
  64. package/dist/setup-databases.js +358 -249
  65. package/dist/setup-models.d.ts +10 -1
  66. package/dist/setup-models.js +90 -2
  67. package/dist/setup-ready-menu.d.ts +16 -2
  68. package/dist/setup-ready-menu.js +37 -5
  69. package/dist/setup-sources.js +108 -28
  70. package/dist/setup.js +22 -10
  71. package/dist/status-project.d.ts +11 -0
  72. package/dist/status-project.js +50 -1
  73. package/dist/telemetry/command-hook.d.ts +1 -0
  74. package/dist/telemetry/command-hook.js +3 -1
  75. package/dist/telemetry/events.d.ts +11 -6
  76. package/dist/telemetry/events.js +10 -2
  77. package/dist/telemetry/identity.d.ts +0 -1
  78. package/dist/telemetry/identity.js +6 -6
  79. package/dist/telemetry/index.d.ts +12 -0
  80. package/dist/telemetry/index.js +13 -2
  81. package/dist/telemetry/scrubber.d.ts +10 -0
  82. package/dist/telemetry/scrubber.js +20 -0
  83. package/package.json +5 -4
@@ -4,15 +4,23 @@ 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
25
  import { writeProjectLocalSecretReference } from './setup-secrets.js';
18
26
  import { isDemoConnection } from './telemetry/demo-detect.js';
@@ -671,10 +679,13 @@ async function maybeApplyHistoricSqlConfig(input) {
671
679
  if (!enabled) {
672
680
  return withQueryHistoryConfig(input.connection, { ...existing, enabled: false });
673
681
  }
682
+ const existingFilters = existing.filters && typeof existing.filters === 'object' && !Array.isArray(existing.filters)
683
+ ? existing.filters
684
+ : {};
674
685
  const common = {
675
686
  ...existing,
676
687
  enabled: true,
677
- filters: historicSqlFiltersForSetup(input.args.queryHistoryServiceAccountPatterns),
688
+ filters: historicSqlFiltersForSetup(input.args.queryHistoryServiceAccountPatterns, existingFilters),
678
689
  };
679
690
  if (dialect === 'postgres') {
680
691
  return withQueryHistoryConfig(input.connection, {
@@ -688,9 +699,10 @@ async function maybeApplyHistoricSqlConfig(input) {
688
699
  redactionPatterns: input.args.queryHistoryRedactionPatterns ?? [],
689
700
  });
690
701
  }
691
- function historicSqlFiltersForSetup(patterns) {
702
+ function historicSqlFiltersForSetup(patterns, existingFilters = {}) {
692
703
  const serviceAccountPatterns = patterns ?? [];
693
704
  return {
705
+ ...existingFilters,
694
706
  dropTrivialProbes: true,
695
707
  ...(serviceAccountPatterns.length > 0
696
708
  ? {
@@ -715,29 +727,6 @@ async function defaultScanConnection(projectDir, connectionId, io) {
715
727
  dryRun: false,
716
728
  }, io);
717
729
  }
718
- function createBufferedCommandIo() {
719
- let stdout = '';
720
- let stderr = '';
721
- return {
722
- stdout: {
723
- isTTY: false,
724
- write(chunk) {
725
- stdout += chunk;
726
- },
727
- },
728
- stderr: {
729
- write(chunk) {
730
- stderr += chunk;
731
- },
732
- },
733
- stdoutText() {
734
- return stdout;
735
- },
736
- stderrText() {
737
- return stderr;
738
- },
739
- };
740
- }
741
730
  function envWithCurrentNodeFirst(env = process.env) {
742
731
  return {
743
732
  ...env,
@@ -889,6 +878,27 @@ async function disableConnectionQueryHistory(projectDir, connectionId) {
889
878
  connection: withQueryHistoryConfig(connection, { ...existing, enabled: false }),
890
879
  });
891
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
+ }
892
902
  async function createConnectionConfigRollback(projectDir, connectionId) {
893
903
  const project = await loadKtxProject({ projectDir });
894
904
  const previousConnection = project.config.connections[connectionId];
@@ -983,14 +993,14 @@ async function maybeConfigureDatabaseScope(input) {
983
993
  const connection = project.config.connections[input.connectionId];
984
994
  const driver = normalizeDriver(connection?.driver);
985
995
  if (!driver || driver === 'sqlite')
986
- return 'ready';
996
+ return okValidateResult();
987
997
  const spec = SCOPE_DISCOVERY_SPECS[driver];
988
998
  const existingTables = connection?.enabled_tables;
989
999
  const hasExistingTables = Array.isArray(existingTables) && existingTables.length > 0;
990
1000
  const existingScope = spec ? configuredScopeValues(connection, spec) : [];
991
1001
  const hasExistingScope = !spec || existingScope.length > 0;
992
1002
  if (hasExistingTables && hasExistingScope && input.forcePrompt !== true) {
993
- return 'ready';
1003
+ return okValidateResult();
994
1004
  }
995
1005
  const cliSchemas = input.args.databaseSchemas;
996
1006
  if (input.args.inputMode === 'disabled') {
@@ -1003,7 +1013,7 @@ async function maybeConfigureDatabaseScope(input) {
1003
1013
  catch (error) {
1004
1014
  const detail = error instanceof Error ? error.message : String(error);
1005
1015
  input.io.stderr.write(`Could not discover ${spec.promptLabel.toLowerCase()} for ${input.connectionId}; ${detail}\n`);
1006
- return 'ready';
1016
+ return okValidateResult();
1007
1017
  }
1008
1018
  }
1009
1019
  if (scopeToWrite.length > 0) {
@@ -1019,7 +1029,7 @@ async function maybeConfigureDatabaseScope(input) {
1019
1029
  ]);
1020
1030
  }
1021
1031
  }
1022
- return 'ready';
1032
+ return okValidateResult();
1023
1033
  }
1024
1034
  if (spec && cliSchemas.length > 0) {
1025
1035
  await writeScopeConfig({
@@ -1050,7 +1060,7 @@ async function maybeConfigureDatabaseScope(input) {
1050
1060
  spec,
1051
1061
  });
1052
1062
  if (typed === undefined)
1053
- return 'back';
1063
+ return backValidateResult();
1054
1064
  effectiveCliSchemas = typed;
1055
1065
  listedSchemas = typed;
1056
1066
  if (typed.length > 0) {
@@ -1065,7 +1075,7 @@ async function maybeConfigureDatabaseScope(input) {
1065
1075
  }
1066
1076
  const schemas = unique(listedSchemas);
1067
1077
  if (spec && schemas.length === 0) {
1068
- return 'ready';
1078
+ return okValidateResult();
1069
1079
  }
1070
1080
  const schemaSuggestion = effectiveCliSchemas.length > 0
1071
1081
  ? { excluded: new Set(), suggested: new Set(effectiveCliSchemas) }
@@ -1093,10 +1103,10 @@ async function maybeConfigureDatabaseScope(input) {
1093
1103
  writePrefixedLines((chunk) => input.io.stderr.write(chunk), input.forcePrompt === true
1094
1104
  ? `Could not discover tables for ${input.connectionId}; edit was not saved. ${detail}`
1095
1105
  : `Could not discover tables for ${input.connectionId}; continuing without table filter. ${detail}`);
1096
- return input.forcePrompt === true ? 'failed' : 'ready';
1106
+ return input.forcePrompt === true ? failedValidateResult() : okValidateResult();
1097
1107
  }
1098
1108
  if (pickResult.kind === 'back') {
1099
- return 'back';
1109
+ return backValidateResult();
1100
1110
  }
1101
1111
  const enabledTables = pickResult.enabledTables;
1102
1112
  const activeSchemas = pickResult.activeSchemas;
@@ -1111,7 +1121,7 @@ async function maybeConfigureDatabaseScope(input) {
1111
1121
  const refreshedProject = await loadKtxProject({ projectDir: input.projectDir });
1112
1122
  const currentConnection = refreshedProject.config.connections[input.connectionId];
1113
1123
  if (!currentConnection)
1114
- return 'ready';
1124
+ return okValidateResult();
1115
1125
  await writeConnectionConfig({
1116
1126
  projectDir: input.projectDir,
1117
1127
  connectionId: input.connectionId,
@@ -1127,7 +1137,7 @@ async function maybeConfigureDatabaseScope(input) {
1127
1137
  writeSetupSection(input.io, `Tables enabled for ${input.connectionId}`, [
1128
1138
  `✓ ${enabledTables.length} tables enabled`,
1129
1139
  ]);
1130
- return 'ready';
1140
+ return okValidateResult();
1131
1141
  }
1132
1142
  async function ensureHistoricSqlIngestDefaults(projectDir) {
1133
1143
  const project = await loadKtxProject({ projectDir });
@@ -1188,6 +1198,158 @@ async function maybeRunHistoricSqlSetupProbe(input) {
1188
1198
  }
1189
1199
  return result.ok;
1190
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
+ }
1191
1353
  async function applyHistoricSqlConfigToExistingConnection(input) {
1192
1354
  if (input.args.inputMode === 'disabled' &&
1193
1355
  input.args.enableQueryHistory !== true &&
@@ -1225,7 +1387,7 @@ async function validateAndScanConnection(input) {
1225
1387
  if (testCode !== 0) {
1226
1388
  flushPrefixedBufferedCommandOutput(input.io, testIo);
1227
1389
  writePrefixedLines((chunk) => input.io.stderr.write(chunk), `Connection test failed for ${input.connectionId}.`);
1228
- return 'failed';
1390
+ return failedValidateResult();
1229
1391
  }
1230
1392
  const testOutput = testIo.stdoutText();
1231
1393
  const outputDriver = normalizeDriver(readOutputValue(testOutput, 'Driver'));
@@ -1233,7 +1395,7 @@ async function validateAndScanConnection(input) {
1233
1395
  const testLines = ['✓ Connection test passed', `Driver: ${driverDisplay}`];
1234
1396
  writeSetupSection(input.io, `Testing ${input.connectionId}`, testLines);
1235
1397
  const scopeStatus = await maybeConfigureDatabaseScope({ ...input, forcePrompt: input.forceScopeAndTables });
1236
- if (scopeStatus !== 'ready') {
1398
+ if (scopeStatus.status !== 'ok') {
1237
1399
  return scopeStatus;
1238
1400
  }
1239
1401
  const queryHistoryAvailable = await maybeRunHistoricSqlSetupProbe({
@@ -1282,15 +1444,27 @@ async function validateAndScanConnection(input) {
1282
1444
  ].join('\n'));
1283
1445
  }
1284
1446
  if (scanCode !== 0) {
1285
- return queryHistoryAvailable ? 'failed' : 'failed-query-history-unavailable';
1447
+ return queryHistoryAvailable
1448
+ ? failedValidateResult()
1449
+ : queryHistoryUnavailableResult(input.projectDir, input.connectionId);
1286
1450
  }
1287
1451
  }
1288
1452
  const scanOutput = scanIo.stdoutText();
1289
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
+ }
1290
1464
  writeSetupSection(input.io, 'Database ready', [
1291
1465
  `${input.connectionId} · ${driverDisplay} · schema context complete`,
1292
1466
  ]);
1293
- return 'ready';
1467
+ return okValidateResult();
1294
1468
  }
1295
1469
  async function chooseDrivers(args, io, prompts, options) {
1296
1470
  if (args.databaseDrivers && args.databaseDrivers.length > 0) {
@@ -1394,64 +1568,137 @@ async function choosePrimarySourceToEdit(input) {
1394
1568
  });
1395
1569
  return choice === 'back' ? 'back' : choice;
1396
1570
  }
1397
- async function runPrimarySourceFullEdit(input) {
1571
+ async function configureDatabaseConnection(input) {
1398
1572
  const project = await loadKtxProject({ projectDir: input.projectDir });
1399
- const existing = project.config.connections[input.connectionId];
1400
- const driver = normalizeDriver(existing?.driver);
1401
- if (!existing || !driver) {
1402
- writePrefixedLines((chunk) => input.io.stderr.write(chunk), `Connection "${input.connectionId}" is not a configured database.`);
1403
- return 'failed';
1404
- }
1405
- const rollback = await createConnectionConfigRollback(input.projectDir, input.connectionId);
1406
- const replacement = await buildConnectionConfig({
1407
- driver,
1573
+ const latestConnection = project.config.connections[input.connectionId];
1574
+ let connection = await buildConnectionConfig({
1575
+ driver: input.driver,
1408
1576
  connectionId: input.connectionId,
1409
1577
  args: input.args,
1410
1578
  prompts: input.prompts,
1411
- existingConnection: existing,
1579
+ existingConnection: latestConnection,
1412
1580
  });
1413
- if (replacement === 'back') {
1414
- 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') {
1415
1595
  return 'back';
1416
1596
  }
1417
- if (!replacement) {
1418
- await rollback();
1419
- return 'failed';
1597
+ if (!connection) {
1598
+ input.io.stderr.write(`Missing connection details for ${driverLabel(input.driver)}.\n`);
1599
+ return 'cancelled';
1420
1600
  }
1421
1601
  const withHistoricSql = await maybeApplyHistoricSqlConfig({
1422
- connection: replacement,
1423
- driver,
1602
+ connection,
1603
+ driver: input.driver,
1424
1604
  args: input.args,
1425
1605
  prompts: input.prompts,
1426
1606
  });
1427
1607
  if (withHistoricSql === 'back') {
1428
- await rollback();
1429
1608
  return 'back';
1430
1609
  }
1431
1610
  await writeConnectionConfig({
1432
1611
  projectDir: input.projectDir,
1433
1612
  connectionId: input.connectionId,
1434
- connection: withExistingPrimaryEditPromptDefaults({
1435
- previous: existing,
1436
- next: withHistoricSql,
1437
- driver,
1438
- }),
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,
1439
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
+ }),
1440
1674
  });
1441
- 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({
1442
1689
  projectDir: input.projectDir,
1443
1690
  connectionId: input.connectionId,
1444
- io: input.io,
1445
- deps: input.deps,
1691
+ driver,
1446
1692
  args: input.args,
1447
1693
  prompts: input.prompts,
1694
+ io: input.io,
1695
+ deps: input.deps,
1696
+ canReturnToDriverSelection: true,
1697
+ allowSkip: false,
1448
1698
  forceScopeAndTables: true,
1699
+ editBaseline: existing,
1449
1700
  });
1450
- if (validated !== 'ready') {
1451
- await rollback();
1452
- return validated === 'failed-query-history-unavailable' ? 'failed' : validated;
1453
- }
1454
- return 'ready';
1701
+ return outcome === 'skip' ? 'back' : outcome;
1455
1702
  }
1456
1703
  export async function runKtxSetupDatabasesStep(args, io, deps = {}) {
1457
1704
  if (args.skipDatabases) {
@@ -1462,29 +1709,37 @@ export async function runKtxSetupDatabasesStep(args, io, deps = {}) {
1462
1709
  if (args.databaseConnectionIds && args.databaseConnectionIds.length > 0) {
1463
1710
  const selectedConnectionIds = [];
1464
1711
  for (const connectionId of unique(args.databaseConnectionIds)) {
1465
- 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({
1466
1719
  projectDir: args.projectDir,
1467
1720
  connectionId,
1721
+ driver,
1468
1722
  args,
1469
1723
  prompts,
1470
- });
1471
- if (historicSqlResult === 'back')
1472
- return { status: 'back', projectDir: args.projectDir };
1473
- const setupStatus = await validateAndScanConnection({
1474
- projectDir: args.projectDir,
1475
- connectionId,
1476
1724
  io,
1477
1725
  deps,
1478
- args,
1479
- prompts,
1726
+ canReturnToDriverSelection: false,
1727
+ allowSkip: false,
1728
+ interactive: false,
1729
+ reuseExistingOnFirstConfigure: true,
1480
1730
  });
1481
- if (setupStatus === 'back') {
1731
+ if (setupOutcome === 'back') {
1482
1732
  return { status: 'back', projectDir: args.projectDir };
1483
1733
  }
1484
- if (setupStatus === 'failed') {
1734
+ if (setupOutcome === 'missing-input') {
1735
+ return { status: 'missing-input', projectDir: args.projectDir };
1736
+ }
1737
+ if (setupOutcome === 'failed') {
1485
1738
  return { status: 'failed', projectDir: args.projectDir };
1486
1739
  }
1487
- selectedConnectionIds.push(connectionId);
1740
+ if (setupOutcome === 'ready') {
1741
+ selectedConnectionIds.push(connectionId);
1742
+ }
1488
1743
  }
1489
1744
  await markDatabasesComplete(args.projectDir, selectedConnectionIds);
1490
1745
  return { status: 'ready', projectDir: args.projectDir, connectionIds: selectedConnectionIds };
@@ -1533,6 +1788,9 @@ export async function runKtxSetupDatabasesStep(args, io, deps = {}) {
1533
1788
  showConfiguredPrimaryMenu = true;
1534
1789
  continue;
1535
1790
  }
1791
+ if (editResult === 'missing-input') {
1792
+ return { status: 'missing-input', projectDir: args.projectDir };
1793
+ }
1536
1794
  if (editResult === 'failed') {
1537
1795
  return { status: 'failed', projectDir: args.projectDir };
1538
1796
  }
@@ -1587,7 +1845,6 @@ export async function runKtxSetupDatabasesStep(args, io, deps = {}) {
1587
1845
  io.stderr.write('Missing database connection id: pass --database-connection-id.\n');
1588
1846
  return { status: 'missing-input', projectDir: args.projectDir };
1589
1847
  }
1590
- let connectionAlreadyValidated = false;
1591
1848
  if (connectionChoice.kind === 'edit') {
1592
1849
  const editResult = await runPrimarySourceFullEdit({
1593
1850
  projectDir: args.projectDir,
@@ -1603,192 +1860,44 @@ export async function runKtxSetupDatabasesStep(args, io, deps = {}) {
1603
1860
  returnToDriverSelection = true;
1604
1861
  break;
1605
1862
  }
1606
- if (editResult === 'failed') {
1607
- return { status: 'failed', projectDir: args.projectDir };
1608
- }
1609
- connectionAlreadyValidated = true;
1610
- }
1611
- else if (connectionChoice.kind === 'new') {
1612
- let connection = await buildConnectionConfig({
1613
- driver,
1614
- connectionId: connectionChoice.connectionId,
1615
- args,
1616
- prompts,
1617
- });
1618
- if (connection === 'back') {
1619
- if (!canReturnToDriverSelection)
1620
- return { status: 'back', projectDir: args.projectDir };
1621
- returnToDriverSelection = true;
1622
- break;
1623
- }
1624
- while (!connection && args.inputMode !== 'disabled') {
1625
- const label = driverLabel(driver);
1626
- const action = await prompts.select(missingConnectionDetailsPrompt(label, canReturnToDriverSelection));
1627
- if (action === 'back') {
1628
- if (!canReturnToDriverSelection)
1629
- return { status: 'back', projectDir: args.projectDir };
1630
- returnToDriverSelection = true;
1631
- break;
1632
- }
1633
- connection = await buildConnectionConfig({
1634
- driver,
1635
- connectionId: connectionChoice.connectionId,
1636
- args,
1637
- prompts,
1638
- });
1639
- if (connection === 'back') {
1640
- if (!canReturnToDriverSelection)
1641
- return { status: 'back', projectDir: args.projectDir };
1642
- returnToDriverSelection = true;
1643
- break;
1644
- }
1645
- }
1646
- if (returnToDriverSelection) {
1647
- break;
1648
- }
1649
- if (connection === 'back') {
1650
- break;
1651
- }
1652
- if (!connection) {
1653
- io.stderr.write(`Missing connection details for ${driverLabel(driver)}.\n`);
1863
+ if (editResult === 'missing-input') {
1654
1864
  return { status: 'missing-input', projectDir: args.projectDir };
1655
1865
  }
1656
- const withHistoricSql = await maybeApplyHistoricSqlConfig({ connection, driver, args, prompts });
1657
- if (withHistoricSql === 'back') {
1658
- if (!canReturnToDriverSelection)
1659
- return { status: 'back', projectDir: args.projectDir };
1660
- returnToDriverSelection = true;
1661
- break;
1866
+ if (editResult === 'failed') {
1867
+ return { status: 'failed', projectDir: args.projectDir };
1662
1868
  }
1663
- await writeConnectionConfig({
1664
- projectDir: args.projectDir,
1665
- connectionId: connectionChoice.connectionId,
1666
- connection: withHistoricSql,
1667
- io,
1668
- });
1669
1869
  }
1670
1870
  else {
1671
- const existing = project.config.connections[connectionChoice.connectionId];
1672
- const withHistoricSql = await maybeApplyHistoricSqlConfig({ connection: existing, driver, args, prompts });
1673
- if (withHistoricSql === 'back') {
1674
- if (!canReturnToDriverSelection)
1675
- return { status: 'back', projectDir: args.projectDir };
1676
- returnToDriverSelection = true;
1677
- break;
1678
- }
1679
- await writeConnectionConfig({
1680
- projectDir: args.projectDir,
1681
- connectionId: connectionChoice.connectionId,
1682
- connection: withHistoricSql,
1683
- io,
1684
- });
1685
- }
1686
- let connectionSkipped = false;
1687
- let setupStatus = connectionAlreadyValidated
1688
- ? 'ready'
1689
- : await validateAndScanConnection({
1871
+ const setupOutcome = await runDatabaseConnectionSetupWithRecovery({
1690
1872
  projectDir: args.projectDir,
1691
1873
  connectionId: connectionChoice.connectionId,
1692
- io,
1693
- deps,
1874
+ driver,
1694
1875
  args,
1695
1876
  prompts,
1877
+ io,
1878
+ deps,
1879
+ canReturnToDriverSelection,
1880
+ allowSkip: true,
1881
+ reuseExistingOnFirstConfigure: connectionChoice.kind === 'existing',
1696
1882
  });
1697
- while (!connectionAlreadyValidated && setupStatus !== 'ready') {
1698
- if (setupStatus === 'back') {
1699
- if (!canReturnToDriverSelection)
1700
- return { status: 'back', projectDir: args.projectDir };
1701
- returnToDriverSelection = true;
1702
- break;
1703
- }
1704
- if (args.inputMode === 'disabled')
1705
- return { status: 'failed', projectDir: args.projectDir };
1706
- const failureOptions = [
1707
- { value: 'retry', label: 'Retry connection test' },
1708
- { value: 're-enter', label: 'Re-enter connection details' },
1709
- ...(setupStatus === 'failed-query-history-unavailable'
1710
- ? [{ value: 'disable-query-history', label: 'Disable query history and retry' }]
1711
- : []),
1712
- { value: 'skip', label: 'Skip this database' },
1713
- { value: 'back', label: 'Back' },
1714
- ];
1715
- const action = await prompts.select({
1716
- message: `Database setup failed for ${connectionChoice.connectionId}`,
1717
- options: failureOptions,
1718
- });
1719
- if (action === 'back') {
1883
+ if (setupOutcome === 'back') {
1720
1884
  if (!canReturnToDriverSelection)
1721
1885
  return { status: 'back', projectDir: args.projectDir };
1722
1886
  returnToDriverSelection = true;
1723
1887
  break;
1724
1888
  }
1725
- if (action === 'skip') {
1726
- connectionSkipped = true;
1727
- break;
1728
- }
1729
- if (action === 'retry') {
1730
- setupStatus = await validateAndScanConnection({
1731
- projectDir: args.projectDir,
1732
- connectionId: connectionChoice.connectionId,
1733
- io,
1734
- deps,
1735
- args,
1736
- prompts,
1737
- });
1889
+ if (setupOutcome === 'missing-input') {
1890
+ return { status: 'missing-input', projectDir: args.projectDir };
1738
1891
  }
1739
- else if (action === 'disable-query-history') {
1740
- await disableConnectionQueryHistory(args.projectDir, connectionChoice.connectionId);
1741
- setupStatus = await validateAndScanConnection({
1742
- projectDir: args.projectDir,
1743
- connectionId: connectionChoice.connectionId,
1744
- io,
1745
- deps,
1746
- args,
1747
- prompts,
1748
- });
1892
+ if (setupOutcome === 'failed') {
1893
+ return { status: 'failed', projectDir: args.projectDir };
1749
1894
  }
1750
- else if (action === 're-enter') {
1751
- const connection = await buildConnectionConfig({
1752
- driver,
1753
- connectionId: connectionChoice.connectionId,
1754
- args,
1755
- prompts,
1756
- });
1757
- if (connection === 'back') {
1758
- if (!canReturnToDriverSelection)
1759
- return { status: 'back', projectDir: args.projectDir };
1760
- returnToDriverSelection = true;
1761
- break;
1762
- }
1763
- if (!connection)
1764
- continue;
1765
- const withHistoricSql = await maybeApplyHistoricSqlConfig({ connection, driver, args, prompts });
1766
- if (withHistoricSql === 'back') {
1767
- if (!canReturnToDriverSelection)
1768
- return { status: 'back', projectDir: args.projectDir };
1769
- returnToDriverSelection = true;
1770
- break;
1771
- }
1772
- await writeConnectionConfig({
1773
- projectDir: args.projectDir,
1774
- connectionId: connectionChoice.connectionId,
1775
- connection: withHistoricSql,
1776
- io,
1777
- });
1778
- setupStatus = await validateAndScanConnection({
1779
- projectDir: args.projectDir,
1780
- connectionId: connectionChoice.connectionId,
1781
- io,
1782
- deps,
1783
- args,
1784
- prompts,
1785
- });
1895
+ if (setupOutcome === 'skip') {
1896
+ continue;
1786
1897
  }
1787
1898
  }
1788
1899
  if (returnToDriverSelection)
1789
1900
  break;
1790
- if (connectionSkipped)
1791
- continue;
1792
1901
  pushUniqueConnectionId(selectedConnectionIds, connectionChoice.connectionId);
1793
1902
  }
1794
1903
  if (returnToDriverSelection) {