@carllee1983/dbcli 1.58.0 → 2.1.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.
@@ -51,7 +51,7 @@ var package_default;
51
51
  var init_package = __esm(() => {
52
52
  package_default = {
53
53
  name: "@carllee1983/dbcli",
54
- version: "1.58.0",
54
+ version: "2.1.0",
55
55
  description: "Database CLI for AI agents",
56
56
  type: "module",
57
57
  publishConfig: {
@@ -134,6 +134,7 @@ var init_package = __esm(() => {
134
134
  "agent-core:check": "bun run scripts/check-agent-core-purity.ts",
135
135
  "core-stdout:check": "bun run scripts/check-core-no-stdout.ts",
136
136
  typecheck: "tsc --noEmit --pretty false",
137
+ "typecheck:tests": "tsc --noEmit --pretty false -p tsconfig.tests.json",
137
138
  "test:perf": "bun test ./tests/perf/*.bench.ts",
138
139
  lint: "eslint src tests scripts --ext .ts --max-warnings=0",
139
140
  "format:check": "prettier --check .",
@@ -8903,6 +8904,15 @@ var init_messages = __esm(() => {
8903
8904
  health: {
8904
8905
  description: "Show AuditLogger health snapshot (size, entries, lock state, last write)"
8905
8906
  },
8907
+ write_gate: {
8908
+ description: "Summarize tier-two write-gate decisions (ADR 0010's measurement)",
8909
+ header: "Write-gate summary \u2014 connection: {conn}",
8910
+ window: "Audit entries scanned: {scanned}, {from} \u2192 {to}",
8911
+ decisions: "Tier-two evaluations: {count}, {from} \u2192 {to}",
8912
+ tier_one: "Tier one, counted apart: {count} ({outcomes})",
8913
+ no_decisions: "Tier two was never reached in this window. Per ADR 0010 that falsifies the criterion, not the gate: the shapes it looks for are not the shapes people write.",
8914
+ no_entries: "The audit log is empty, so this says nothing about the gate either way. Run some writes, or check `dbcli audit health`."
8915
+ },
8906
8916
  disabled_hint: "Audit is disabled (audit.enabled = false in .dbcli). Use 'dbcli audit health' for details.",
8907
8917
  no_entries: "No audit entries.",
8908
8918
  show_no_match: "No audit entry matches '{prefix}'.",
@@ -9211,6 +9221,15 @@ var init_messages2 = __esm(() => {
9211
9221
  health: {
9212
9222
  description: "\u986F\u793A AuditLogger \u5065\u5EB7\u72C0\u614B\uFF08\u6A94\u6848\u5927\u5C0F\u3001\u7B46\u6578\u3001lock \u72C0\u614B\u3001\u6700\u5F8C\u5BEB\u5165\u7D50\u679C\uFF09"
9213
9223
  },
9224
+ write_gate: {
9225
+ description: "\u7D71\u8A08\u7B2C\u4E8C\u7D1A\u5BEB\u5165\u9598\u9580\u7684\u5224\u5B9A\uFF08ADR 0010 \u62BC\u6CE8\u7684\u90A3\u500B\u91CF\u6E2C\uFF09",
9226
+ header: "\u5BEB\u5165\u9598\u9580\u6458\u8981 \u2014 \u9023\u7DDA\uFF1A{conn}",
9227
+ window: "\u6383\u904E\u7684\u7A3D\u6838\u7D00\u9304\uFF1A{scanned} \u7B46\uFF0C{from} \u2192 {to}",
9228
+ decisions: "\u7B2C\u4E8C\u7D1A\u5224\u5B9A\uFF1A{count} \u6B21\uFF0C{from} \u2192 {to}",
9229
+ tier_one: "\u7B2C\u4E00\u7D1A\uFF0C\u5206\u958B\u8A08\u7B97\uFF1A{count} \u6B21\uFF08{outcomes}\uFF09",
9230
+ no_decisions: "\u9019\u6BB5\u5340\u9593\u5167\u7B2C\u4E8C\u7D1A\u4E00\u6B21\u90FD\u6C92\u88AB\u89F8\u53CA\u3002\u4F9D ADR 0010\uFF0C\u9019\u63A8\u7FFB\u7684\u662F\u5224\u6E96\u800C\u4E0D\u662F\u9598\u9580\uFF1A\u5B83\u5728\u627E\u7684\u5F62\u72C0\u4E0D\u662F\u5927\u5BB6\u5BE6\u969B\u6703\u5BEB\u7684\u5F62\u72C0\u3002",
9231
+ no_entries: "\u7A3D\u6838\u7D00\u9304\u662F\u7A7A\u7684\uFF0C\u6240\u4EE5\u9019\u5C0D\u9598\u9580\u6C92\u6709\u4EFB\u4F55\u7D50\u8AD6\u3002\u5148\u8DD1\u4E00\u4E9B\u5BEB\u5165\uFF0C\u6216\u7528 `dbcli audit health` \u6AA2\u67E5\u3002"
9232
+ },
9214
9233
  disabled_hint: "Audit \u5DF2\u505C\u7528\uFF08.dbcli \u4E2D audit.enabled = false\uFF09\u3002\u57F7\u884C `dbcli audit health` \u67E5\u770B\u8A73\u60C5\u3002",
9215
9234
  no_entries: "\u6C92\u6709 audit \u7D00\u9304\u3002",
9216
9235
  show_no_match: "\u627E\u4E0D\u5230\u7B26\u5408 '{prefix}' \u7684 audit \u7D00\u9304\u3002",
@@ -9422,7 +9441,8 @@ var init_shell = __esm(() => {
9422
9441
  multiline_cancelled: "Multiline input cancelled.",
9423
9442
  timing_display: "Time: {ms}ms",
9424
9443
  rows_display: "{count} row(s)",
9425
- unknown_command: "Unknown command: '{command}'. Type '.help' for available commands."
9444
+ unknown_command: "Unknown command: '{command}'. Type '.help' for available commands.",
9445
+ subcommand_prefix_hint: "This line is being read as SQL and is waiting for its `;`. If you meant the dbcli subcommand, type {command} instead. Ctrl-C cancels."
9426
9446
  };
9427
9447
  });
9428
9448
 
@@ -9448,7 +9468,8 @@ var init_shell2 = __esm(() => {
9448
9468
  multiline_cancelled: "\u5DF2\u53D6\u6D88\u591A\u884C\u8F38\u5165\u3002",
9449
9469
  timing_display: "\u8017\u6642\uFF1A{ms}ms",
9450
9470
  rows_display: "{count} \u7B46\u8CC7\u6599",
9451
- unknown_command: "\u672A\u77E5\u6307\u4EE4\uFF1A'{command}'\u3002\u8F38\u5165 '.help' \u67E5\u770B\u53EF\u7528\u6307\u4EE4\u3002"
9471
+ unknown_command: "\u672A\u77E5\u6307\u4EE4\uFF1A'{command}'\u3002\u8F38\u5165 '.help' \u67E5\u770B\u53EF\u7528\u6307\u4EE4\u3002",
9472
+ subcommand_prefix_hint: "\u9019\u4E00\u884C\u88AB\u7576\u6210 SQL\uFF0C\u6B63\u5728\u7B49\u5206\u865F\u3002\u5982\u679C\u4F60\u8981\u7684\u662F dbcli \u5B50\u6307\u4EE4\uFF0C\u8ACB\u6539\u6253 {command}\u3002Ctrl-C \u53EF\u53D6\u6D88\u3002"
9452
9473
  };
9453
9474
  });
9454
9475
 
@@ -9473,6 +9494,29 @@ var init_ceremony = __esm(() => {
9473
9494
  confirm_destructive_warning_ddl: "\u26A0\uFE0F Warning: this schema change is destructive and cannot be undone!",
9474
9495
  confirm_ddl_prompt: "Are you sure you want to run this {operation}? This cannot be undone.",
9475
9496
  confirm_prompt: "Proceed with this operation?",
9497
+ gate_summary: "This will {operation} rows in {table}.",
9498
+ gate_summary_untargeted: "This will run a {operation} statement.",
9499
+ gate_statement: "Statement:",
9500
+ gate_cancelled: "Cancelled. Nothing was run.",
9501
+ gate_full_table_warning: "\u26A0\uFE0F This affects EVERY row in {table} and cannot be undone.",
9502
+ gate_ddl_warning: "\u26A0\uFE0F This destroys {table} and cannot be undone.",
9503
+ gate_ddl_warning_untargeted: "\u26A0\uFE0F This is a destructive schema change and cannot be undone.",
9504
+ gate_unparseable_warning: "\u26A0\uFE0F dbcli could not parse this statement, so it is treated as affecting everything.",
9505
+ gate_multiple_statements_warning: "\u26A0\uFE0F This is more than one statement, and dbcli will not judge them one at a time.",
9506
+ gate_multi_table_warning: "This write joins another table. How many rows of the target it touches depends on the data, not on the statement, so it cannot be checked before it runs.",
9507
+ gate_nested_write_warning: "\u26A0\uFE0F This statement carries a second write inside it, and dbcli will not read the tier of that write off this one.",
9508
+ gate_typed_prompt: "Type {phrase} to run it",
9509
+ gate_typed_mismatch: "That did not match {phrase}. Nothing was run.",
9510
+ gate_refused: "Refused: this statement is not limited to specific rows and nobody is here to confirm it (reason={reason}).",
9511
+ gate_refused_in_shell: "Refused: a dbcli subcommand started from the shell has no way to ask you, even though you are right here (reason={reason}).",
9512
+ gate_remedy_no_where: "Add a WHERE clause \u2014 WHERE 1=1 if you really do mean every row \u2014 or a LIMIT, then run it again.",
9513
+ gate_remedy_ddl_destruction: "No flag skips this. Run it from an interactive terminal, or apply the schema change through a reviewed migration.",
9514
+ gate_remedy_unparseable: "dbcli could not parse this statement and will not guess. Simplify it, or run it from an interactive terminal.",
9515
+ gate_remedy_multiple_statements: "Run one statement per command.",
9516
+ gate_remedy_multi_table: "Rewrite it as a single-table statement \u2014 move the other table into a subquery in the WHERE \u2014 or run it where someone can confirm it.",
9517
+ gate_remedy_nested_write: "Run the nested write as its own statement, or run this one where someone can confirm it.",
9518
+ gate_remedy_non_unique_where: "Match on a primary key or a uniquely indexed column, or run it from an interactive terminal.",
9519
+ gate_remedy_in_shell: "Type the statement at the dbcli> prompt instead \u2014 there the shell asks you to type the target table name.",
9476
9520
  recovery_none: "This cannot be undone automatically \u2014 restore from a backup to get the previous values back.",
9477
9521
  recovery_retry: "Re-run with --recovery to write a recovery plan, then read it with: dbcli recover",
9478
9522
  blacklist_hint: "Run `dbcli blacklist list` to see what is protected"
@@ -9500,6 +9544,29 @@ var init_ceremony2 = __esm(() => {
9500
9544
  confirm_destructive_warning_ddl: "\u26A0\uFE0F \u8B66\u544A\uFF1A\u9019\u500B schema \u8B8A\u66F4\u5177\u7834\u58DE\u6027\uFF0C\u57F7\u884C\u5F8C\u7121\u6CD5\u5FA9\u539F\uFF01",
9501
9545
  confirm_ddl_prompt: "\u78BA\u5B9A\u8981\u57F7\u884C\u9019\u500B {operation} \u55CE\uFF1F\u6B64\u64CD\u4F5C\u7121\u6CD5\u5FA9\u539F\u3002",
9502
9546
  confirm_prompt: "\u8981\u7E7C\u7E8C\u57F7\u884C\u9019\u500B\u64CD\u4F5C\u55CE\uFF1F",
9547
+ gate_summary: "\u9019\u53E5\u6703\u5C0D {table} \u57F7\u884C {operation}\u3002",
9548
+ gate_summary_untargeted: "\u9019\u53E5\u6703\u57F7\u884C {operation}\u3002",
9549
+ gate_statement: "\u8A9E\u53E5\uFF1A",
9550
+ gate_cancelled: "\u5DF2\u53D6\u6D88\uFF0C\u6C92\u6709\u57F7\u884C\u4EFB\u4F55\u8A9E\u53E5\u3002",
9551
+ gate_full_table_warning: "\u26A0\uFE0F \u9019\u53E5\u6703\u5F71\u97FF {table} \u7684\u6BCF\u4E00\u5217\uFF0C\u57F7\u884C\u5F8C\u7121\u6CD5\u5FA9\u539F\u3002",
9552
+ gate_ddl_warning: "\u26A0\uFE0F \u9019\u53E5\u6703\u6467\u6BC0 {table}\uFF0C\u57F7\u884C\u5F8C\u7121\u6CD5\u5FA9\u539F\u3002",
9553
+ gate_ddl_warning_untargeted: "\u26A0\uFE0F \u9019\u662F\u7834\u58DE\u6027\u7684 schema \u8B8A\u66F4\uFF0C\u57F7\u884C\u5F8C\u7121\u6CD5\u5FA9\u539F\u3002",
9554
+ gate_unparseable_warning: "\u26A0\uFE0F dbcli \u7121\u6CD5\u89E3\u6790\u9019\u53E5\uFF0C\u56E0\u6B64\u4E00\u5F8B\u8996\u70BA\u6703\u5F71\u97FF\u5168\u90E8\u8CC7\u6599\u3002",
9555
+ gate_multiple_statements_warning: "\u26A0\uFE0F \u9019\u88E1\u4E0D\u53EA\u4E00\u53E5\u8A9E\u53E5\uFF0Cdbcli \u4E0D\u6703\u9010\u53E5\u5224\u65B7\u3002",
9556
+ gate_multi_table_warning: "\u9019\u500B\u5BEB\u5165\u63A5\u4E86\u53E6\u4E00\u5F35\u8868\u3002\u5B83\u6703\u52D5\u5230\u76EE\u6A19\u8868\u5E7E\u5217\u53D6\u6C7A\u65BC\u8CC7\u6599\u800C\u4E0D\u662F\u8A9E\u53E5\u672C\u8EAB\uFF0C\u57F7\u884C\u524D\u7121\u6CD5\u6AA2\u67E5\u3002",
9557
+ gate_nested_write_warning: "\u26A0\uFE0F \u9019\u53E5\u88E1\u9762\u9084\u593E\u8457\u53E6\u4E00\u500B\u5BEB\u5165\uFF0Cdbcli \u4E0D\u6703\u62FF\u9019\u53E5\u7684\u5C64\u7D1A\u4EE3\u66FF\u90A3\u500B\u5BEB\u5165\u7684\u5C64\u7D1A\u3002",
9558
+ gate_typed_prompt: "\u8F38\u5165 {phrase} \u624D\u6703\u57F7\u884C",
9559
+ gate_typed_mismatch: "\u8F38\u5165\u7684\u5167\u5BB9\u8207 {phrase} \u4E0D\u7B26\uFF0C\u6C92\u6709\u57F7\u884C\u4EFB\u4F55\u8A9E\u53E5\u3002",
9560
+ gate_refused: "\u5DF2\u62D2\u7D55\uFF1A\u9019\u53E5\u6C92\u6709\u9650\u5B9A\u8981\u52D5\u54EA\u4E9B\u5217\uFF0C\u800C\u73FE\u5728\u6C92\u6709\u4EBA\u80FD\u78BA\u8A8D\uFF08reason={reason}\uFF09\u3002",
9561
+ gate_refused_in_shell: "\u5DF2\u62D2\u7D55\uFF1A\u5F9E shell \u555F\u52D5\u7684 dbcli \u5B50\u6307\u4EE4\u6C92\u6709\u8FA6\u6CD5\u5411\u4F60\u63D0\u554F\uFF0C\u5373\u4F7F\u4F60\u4EBA\u5C31\u5728\u9019\u88E1\uFF08reason={reason}\uFF09\u3002",
9562
+ gate_remedy_no_where: "\u88DC\u4E0A WHERE\uFF08\u771F\u7684\u8981\u5168\u8868\u5C31\u5BEB WHERE 1=1\uFF09\u6216 LIMIT\uFF0C\u7136\u5F8C\u91CD\u8DD1\u3002",
9563
+ gate_remedy_ddl_destruction: "\u6C92\u6709\u4EFB\u4F55\u65D7\u6A19\u53EF\u4EE5\u8DF3\u904E\u9019\u4E00\u95DC\u3002\u8ACB\u5728\u4E92\u52D5\u7D42\u7AEF\u4E0B\u57F7\u884C\uFF0C\u6216\u6539\u8D70\u7D93\u904E\u5BE9\u67E5\u7684 migration\u3002",
9564
+ gate_remedy_unparseable: "dbcli \u7121\u6CD5\u89E3\u6790\u9019\u53E5\uFF0C\u4E5F\u4E0D\u6703\u7528\u731C\u7684\u3002\u8ACB\u7C21\u5316\u8A9E\u53E5\uFF0C\u6216\u5728\u4E92\u52D5\u7D42\u7AEF\u4E0B\u57F7\u884C\u3002",
9565
+ gate_remedy_multiple_statements: "\u4E00\u6B21\u53EA\u57F7\u884C\u4E00\u53E5\u8A9E\u53E5\u3002",
9566
+ gate_remedy_multi_table: "\u6539\u5BEB\u6210\u55AE\u8868\u8A9E\u53E5\u2014\u2014\u628A\u53E6\u4E00\u5F35\u8868\u79FB\u9032 WHERE \u7684\u5B50\u67E5\u8A62\u88E1\u2014\u2014\u6216\u6539\u5728\u6709\u4EBA\u53EF\u4EE5\u78BA\u8A8D\u7684\u5730\u65B9\u57F7\u884C\u3002",
9567
+ gate_remedy_nested_write: "\u628A\u88E1\u9762\u90A3\u500B\u5BEB\u5165\u62C6\u6210\u7368\u7ACB\u7684\u4E00\u53E5\u4F86\u57F7\u884C\uFF0C\u6216\u6539\u5728\u6709\u4EBA\u53EF\u4EE5\u78BA\u8A8D\u7684\u5730\u65B9\u57F7\u884C\u3002",
9568
+ gate_remedy_non_unique_where: "\u6539\u7528\u4E3B\u9375\u6216\u552F\u4E00\u7D22\u5F15\u6B04\u4F4D\u7576\u689D\u4EF6\uFF0C\u6216\u5728\u4E92\u52D5\u7D42\u7AEF\u4E0B\u57F7\u884C\u3002",
9569
+ gate_remedy_in_shell: "\u8ACB\u76F4\u63A5\u5728 dbcli> \u63D0\u793A\u7B26\u6253\u9019\u53E5\u8A9E\u53E5\u2014\u2014\u5728\u90A3\u88E1 dbcli \u6703\u8ACB\u4F60\u6253\u51FA\u76EE\u6A19\u8CC7\u6599\u8868\u7684\u540D\u7A31\u4F86\u78BA\u8A8D\u3002",
9503
9570
  recovery_none: "\u6B64\u64CD\u4F5C\u7121\u6CD5\u81EA\u52D5\u9084\u539F\uFF0C\u8981\u53D6\u56DE\u5148\u524D\u7684\u8CC7\u6599\u8ACB\u5F9E\u5099\u4EFD\u56DE\u5FA9\u3002",
9504
9571
  recovery_retry: "\u52A0\u4E0A --recovery \u91CD\u8DD1\u53EF\u7522\u751F\u56DE\u5FA9\u8A08\u756B\uFF0C\u518D\u7528 dbcli recover \u8B80\u53D6",
9505
9572
  blacklist_hint: "\u57F7\u884C `dbcli blacklist list` \u67E5\u770B\u53D7\u4FDD\u8B77\u7684\u9805\u76EE"
@@ -9730,7 +9797,8 @@ async function readLineFromStdin(prompt = "") {
9730
9797
  }
9731
9798
  async function textFallback(message, defaultValue) {
9732
9799
  const displayMessage = defaultValue ? `${message} [${defaultValue}]: ` : `${message}: `;
9733
- const answer = await readLineFromStdin(displayMessage);
9800
+ process.stderr.write(displayMessage);
9801
+ const answer = await readLineFromStdin();
9734
9802
  return answer.trim() || defaultValue || "";
9735
9803
  }
9736
9804
  async function selectFallback(message, choices) {
@@ -9756,7 +9824,7 @@ async function text(message, defaultValue) {
9756
9824
  const inquirer = await loadInquirer();
9757
9825
  if (!inquirer)
9758
9826
  return textFallback(message, defaultValue);
9759
- return await inquirer.input({ message, default: defaultValue });
9827
+ return await inquirer.input({ message, default: defaultValue }, { output: process.stderr });
9760
9828
  }
9761
9829
  async function select(message, choices) {
9762
9830
  if (!process.stdin.isTTY)
@@ -17972,7 +18040,7 @@ async function writeAuditEntry(config, commandName, options, outcome) {
17972
18040
  const logger = await getAuditLogger(config, options.config || ".dbcli", connectionName);
17973
18041
  const engine = config.connection?.system || "postgresql";
17974
18042
  const target = outcome.target || getOperationTarget(engine, commandName, options, outcome.sql);
17975
- let tier = getEngineCapability(engine, commandName).tier;
18043
+ let tier = outcome.sideEffectTier ?? getEngineCapability(engine, commandName).tier;
17976
18044
  if (options.dryRun || options.plan) {
17977
18045
  tier = "dry-run";
17978
18046
  }
@@ -18905,6 +18973,7 @@ __export(exports_permission_guard, {
18905
18973
  checkPermissionForClassification: () => checkPermissionForClassification,
18906
18974
  checkPermission: () => checkPermission,
18907
18975
  SQL_WRITE_OR_DDL_KEYWORDS: () => SQL_WRITE_OR_DDL_KEYWORDS,
18976
+ SQL_LOCK_CLAUSE: () => SQL_LOCK_CLAUSE,
18908
18977
  SQL_DIALECTS: () => SQL_DIALECTS,
18909
18978
  PermissionError: () => PermissionError
18910
18979
  });
@@ -22202,6 +22271,504 @@ var init_elasticsearch = __esm(() => {
22202
22271
  ES_DOCUMENT_PATH = /\/(?:_doc|_source)\/[^/?]+/;
22203
22272
  });
22204
22273
 
22274
+ // src/commands/mutation-outcome.ts
22275
+ var exports_mutation_outcome = {};
22276
+ __export(exports_mutation_outcome, {
22277
+ shouldRenderForHuman: () => shouldRenderForHuman,
22278
+ renderMutationOutcome: () => renderMutationOutcome,
22279
+ printMutationOutcome: () => printMutationOutcome,
22280
+ printMutationFailure: () => printMutationFailure,
22281
+ humanOutputContext: () => humanOutputContext,
22282
+ cancelledOutcome: () => cancelledOutcome
22283
+ });
22284
+ function shouldRenderForHuman(context) {
22285
+ if (context.format === "json")
22286
+ return false;
22287
+ return context.isTTY;
22288
+ }
22289
+ function humanOutputContext(options) {
22290
+ return { isTTY: process.stdout.isTTY === true, ...options.format && { format: options.format } };
22291
+ }
22292
+ function renderMutationOutcome(result, table, elapsedMs) {
22293
+ if (result.status === "error") {
22294
+ return [
22295
+ t_vars("ceremony.failed", { reason: result.error ?? t("ceremony.unknown_error") }),
22296
+ t("ceremony.recovery_retry")
22297
+ ];
22298
+ }
22299
+ if (result.status === "cancelled") {
22300
+ return [t_vars("ceremony.cancelled", { table })];
22301
+ }
22302
+ if (result.status === "dry_run") {
22303
+ return [t_vars("ceremony.dry_run", { table })];
22304
+ }
22305
+ const elapsed = t_vars("ceremony.elapsed", { seconds: (elapsedMs / 1000).toFixed(2) });
22306
+ if (result.rows_affected === 0) {
22307
+ return [t_vars("ceremony.matched_nothing", { table }), elapsed];
22308
+ }
22309
+ return [
22310
+ t_vars(VERB_KEY[result.operation], { count: result.rows_affected, table }),
22311
+ elapsed,
22312
+ t("ceremony.recovery_none")
22313
+ ];
22314
+ }
22315
+ function printMutationOutcome(result, table, elapsedMs, context, envelopeExtras = {}) {
22316
+ if (shouldRenderForHuman(context)) {
22317
+ const writeLine = result.status === "error" ? console.error : console.log;
22318
+ for (const line of renderMutationOutcome(result, table, elapsedMs))
22319
+ writeLine(line);
22320
+ return;
22321
+ }
22322
+ console.log(JSON.stringify({ ...mutationEnvelope(result), ...envelopeExtras }, null, 2));
22323
+ }
22324
+ function printMutationFailure(error, operation, context) {
22325
+ if (shouldRenderForHuman(context)) {
22326
+ console.error(t_vars("ceremony.failed", { reason: error.message }));
22327
+ if (error instanceof BlacklistError) {
22328
+ console.error(t("ceremony.blacklist_hint"));
22329
+ }
22330
+ return;
22331
+ }
22332
+ console.log(JSON.stringify({ status: "error", operation, rows_affected: 0, error: error.message }, null, 2));
22333
+ }
22334
+ function cancelledOutcome(operation, preview) {
22335
+ return {
22336
+ status: "cancelled",
22337
+ operation,
22338
+ rows_affected: 0,
22339
+ sql: preview,
22340
+ timestamp: new Date().toISOString()
22341
+ };
22342
+ }
22343
+ function mutationEnvelope(result) {
22344
+ return {
22345
+ status: result.status,
22346
+ operation: result.operation,
22347
+ rows_affected: result.rows_affected,
22348
+ ...result.timestamp && { timestamp: result.timestamp },
22349
+ ...result.sql && { sql: result.sql },
22350
+ ...result.error && { error: result.error }
22351
+ };
22352
+ }
22353
+ var VERB_KEY;
22354
+ var init_mutation_outcome = __esm(() => {
22355
+ init_message_loader();
22356
+ init_blacklist();
22357
+ VERB_KEY = {
22358
+ insert: "ceremony.inserted",
22359
+ update: "ceremony.updated",
22360
+ delete: "ceremony.deleted"
22361
+ };
22362
+ });
22363
+
22364
+ // src/commands/write-gate-prompt.ts
22365
+ var exports_write_gate_prompt = {};
22366
+ __export(exports_write_gate_prompt, {
22367
+ enforceWriteGate: () => enforceWriteGate,
22368
+ WriteGateRefusal: () => WriteGateRefusal
22369
+ });
22370
+ async function enforceWriteGate(request) {
22371
+ const { verdict, human } = request;
22372
+ if (verdict.tier === "none")
22373
+ return true;
22374
+ const attended = isAttended(human);
22375
+ if (verdict.tier === "two")
22376
+ return await enforceTierTwo(request, attended);
22377
+ if (request.yes === true)
22378
+ return true;
22379
+ if (!attended)
22380
+ return true;
22381
+ writeSummary(request);
22382
+ const proceed = await promptUser.confirm(t("ceremony.confirm_prompt"));
22383
+ if (!proceed)
22384
+ process.stderr.write(`${t("ceremony.gate_cancelled")}
22385
+ `);
22386
+ return proceed;
22387
+ }
22388
+ function isAttended(human) {
22389
+ return shouldRenderForHuman(human) && process.stdin.isTTY === true;
22390
+ }
22391
+ async function enforceTierTwo(request, attended) {
22392
+ const { verdict } = request;
22393
+ const reason = verdict.reason ?? "no_where";
22394
+ if (!attended) {
22395
+ const inShell = process.env.DBCLI_SHELL_SUBCOMMAND === "1";
22396
+ throw new WriteGateRefusal(inShell ? `${t_vars("ceremony.gate_refused_in_shell", { reason })} ${t("ceremony.gate_remedy_in_shell")}` : `${t_vars("ceremony.gate_refused", { reason })} ${t(REMEDY_KEY[reason])}`, reason, verdict.table);
22397
+ }
22398
+ writeSummary(request);
22399
+ const warning = verdict.table === undefined && reason === "ddl_destruction" ? t("ceremony.gate_ddl_warning_untargeted") : t_vars(WARNING_KEY[reason], { table: verdict.table ?? "" });
22400
+ process.stderr.write(`
22401
+ ${warning}
22402
+ ${t(REMEDY_KEY[reason])}
22403
+ `);
22404
+ const ask = request.ask ?? promptUser.text;
22405
+ const typed = await ask(t_vars("ceremony.gate_typed_prompt", { phrase: verdict.confirmationPhrase }));
22406
+ if (typed === null) {
22407
+ process.stderr.write(`${t("ceremony.gate_cancelled")}
22408
+ `);
22409
+ return false;
22410
+ }
22411
+ const matched = typed.trim().toLowerCase() === verdict.confirmationPhrase.toLowerCase();
22412
+ if (!matched) {
22413
+ process.stderr.write(`${t_vars("ceremony.gate_typed_mismatch", { phrase: verdict.confirmationPhrase })}
22414
+ `);
22415
+ }
22416
+ return matched;
22417
+ }
22418
+ function writeSummary(request) {
22419
+ const { verdict, operation, statement } = request;
22420
+ const summary = verdict.table ? t_vars("ceremony.gate_summary", { operation, table: verdict.table }) : t_vars("ceremony.gate_summary_untargeted", { operation });
22421
+ process.stderr.write(`
22422
+ ${summary}
22423
+ ${t("ceremony.gate_statement")}
22424
+ ${statement}
22425
+ `);
22426
+ }
22427
+ var WriteGateRefusal, REMEDY_KEY, WARNING_KEY;
22428
+ var init_write_gate_prompt = __esm(() => {
22429
+ init_message_loader();
22430
+ init_prompts();
22431
+ init_mutation_outcome();
22432
+ WriteGateRefusal = class WriteGateRefusal extends Error {
22433
+ code = "WRITE_GATE_REFUSED";
22434
+ reason;
22435
+ table;
22436
+ constructor(message, reason, table) {
22437
+ super(message);
22438
+ this.name = "WriteGateRefusal";
22439
+ this.reason = reason;
22440
+ this.table = table;
22441
+ }
22442
+ };
22443
+ REMEDY_KEY = {
22444
+ no_where: "ceremony.gate_remedy_no_where",
22445
+ ddl_destruction: "ceremony.gate_remedy_ddl_destruction",
22446
+ unparseable: "ceremony.gate_remedy_unparseable",
22447
+ non_unique_where: "ceremony.gate_remedy_non_unique_where",
22448
+ multiple_statements: "ceremony.gate_remedy_multiple_statements",
22449
+ multi_table: "ceremony.gate_remedy_multi_table",
22450
+ nested_write: "ceremony.gate_remedy_nested_write"
22451
+ };
22452
+ WARNING_KEY = {
22453
+ no_where: "ceremony.gate_full_table_warning",
22454
+ ddl_destruction: "ceremony.gate_ddl_warning",
22455
+ unparseable: "ceremony.gate_unparseable_warning",
22456
+ non_unique_where: "ceremony.gate_full_table_warning",
22457
+ multiple_statements: "ceremony.gate_multiple_statements_warning",
22458
+ multi_table: "ceremony.gate_multi_table_warning",
22459
+ nested_write: "ceremony.gate_nested_write_warning"
22460
+ };
22461
+ });
22462
+
22463
+ // src/core/sql-parser.ts
22464
+ var exports_sql_parser = {};
22465
+ __export(exports_sql_parser, {
22466
+ sqlParser: () => sqlParser
22467
+ });
22468
+ function sqlParser() {
22469
+ if (!cached2) {
22470
+ const { Parser } = __require("node-sql-parser");
22471
+ cached2 = new Parser;
22472
+ }
22473
+ return cached2;
22474
+ }
22475
+ var cached2 = null;
22476
+
22477
+ // src/commands/write-gate.ts
22478
+ var exports_write_gate = {};
22479
+ __export(exports_write_gate, {
22480
+ classifyStructuredWriteGate: () => classifyStructuredWriteGate,
22481
+ classifySqlWriteGate: () => classifySqlWriteGate,
22482
+ WRITE_GATE_FALLBACK_PHRASE: () => WRITE_GATE_FALLBACK_PHRASE
22483
+ });
22484
+ async function classifySqlWriteGate(sql, options = {}) {
22485
+ const { dialect } = options;
22486
+ const classification = classifyStatement(sql);
22487
+ const hiddenWrite = findWriteKeyword(sql, dialect ? [dialect] : undefined);
22488
+ const type = WRITE_TYPES.has(classification.type) ? classification.type : hiddenWrite ? keywordToType(hiddenWrite) : undefined;
22489
+ if (type === undefined)
22490
+ return { tier: "none", confirmationPhrase: WRITE_GATE_FALLBACK_PHRASE };
22491
+ if (containsMultipleStatements(sql, dialect)) {
22492
+ return verdict("two", "multiple_statements", undefined, type);
22493
+ }
22494
+ const nested = nestedWrite(sql, dialect);
22495
+ if (nested)
22496
+ return verdict("two", "nested_write", undefined, nested);
22497
+ const merge = classifyMerge(sql, dialect);
22498
+ if (merge)
22499
+ return merge;
22500
+ if (type === "DROP" || type === "TRUNCATE") {
22501
+ const matched = DDL_TARGET.exec(sql)?.[1];
22502
+ const table = matched ? matched.replace(/[`"[\]]/g, "") : undefined;
22503
+ return verdict("two", "ddl_destruction", table, type);
22504
+ }
22505
+ if (type !== "UPDATE" && type !== "DELETE")
22506
+ return verdict("one", undefined, undefined, type);
22507
+ return await classifyQualification(sql, dialect, type);
22508
+ }
22509
+ async function classifyQualification(sql, dialect, type) {
22510
+ const { sqlParser: sqlParser2 } = await Promise.resolve().then(() => exports_sql_parser);
22511
+ let ast;
22512
+ try {
22513
+ ast = sqlParser2().astify(sql, { database: PARSER_DIALECTS[dialect ?? "mysql"] });
22514
+ } catch {
22515
+ return unparseable(sql, dialect, type);
22516
+ }
22517
+ const statement = Array.isArray(ast) ? ast[0] : ast;
22518
+ if (!statement || statement.type !== "update" && statement.type !== "delete") {
22519
+ return unparseable(sql, dialect, type);
22520
+ }
22521
+ const scope = [...statement.table ?? [], ...statement.from ?? []];
22522
+ const sources = new Set(scope.map((ref, index) => refIdentity(ref) ?? `#unnamed-${index}`));
22523
+ if (sources.size > 1) {
22524
+ return verdict("two", "multi_table", undefined, type);
22525
+ }
22526
+ const target = normaliseRef(statement.table?.[0] ?? scope[0]);
22527
+ const table = tableName(target);
22528
+ if (statement.limit != null)
22529
+ return verdict("one", undefined, table, type);
22530
+ if (statement.where != null && narrowsTarget(statement.where, refNames(target))) {
22531
+ return verdict("one", undefined, table, type);
22532
+ }
22533
+ return verdict("two", "no_where", table, type);
22534
+ }
22535
+ function normaliseRef(ref) {
22536
+ if (ref?.table?.toUpperCase() !== "ONLY")
22537
+ return ref;
22538
+ if (typeof ref.as !== "string" || ref.as.length === 0)
22539
+ return ref;
22540
+ return {
22541
+ db: ref.db,
22542
+ on: ref.on,
22543
+ as: null,
22544
+ table: typeof ref.as === "string" ? ref.as : undefined
22545
+ };
22546
+ }
22547
+ function refIdentity(reference) {
22548
+ const ref = normaliseRef(reference);
22549
+ if (typeof ref?.table !== "string")
22550
+ return;
22551
+ return (ref.db ? `${ref.db}.${ref.table}` : ref.table).toLowerCase();
22552
+ }
22553
+ function refNames(reference) {
22554
+ const ref = normaliseRef(reference);
22555
+ const name = typeof ref?.as === "string" && ref.as.length > 0 ? ref.as : ref?.table;
22556
+ return typeof name === "string" ? [name.toLowerCase()] : [];
22557
+ }
22558
+ function narrowsTarget(where, targetNames) {
22559
+ const names = new Set(targetNames);
22560
+ const { owners, correlated, skippedSubquery } = scanCondition(where);
22561
+ if (owners.length === 0 && correlated.length === 0)
22562
+ return !skippedSubquery;
22563
+ return owners.some((owner) => owner === null || names.has(owner)) || correlated.some((owner) => names.has(owner));
22564
+ }
22565
+ function mergeScans(scans) {
22566
+ return {
22567
+ owners: scans.flatMap((scan) => scan.owners),
22568
+ correlated: scans.flatMap((scan) => scan.correlated),
22569
+ skippedSubquery: scans.some((scan) => scan.skippedSubquery)
22570
+ };
22571
+ }
22572
+ function scanCondition(node, bound) {
22573
+ if (node === null || typeof node !== "object")
22574
+ return EMPTY_SCAN;
22575
+ const record = node;
22576
+ const inner = record.ast ?? (record.type === "select" ? record : undefined);
22577
+ const nested = inner !== undefined;
22578
+ if (record.type === "column_ref") {
22579
+ const owner = record.table;
22580
+ const name = owner == null ? null : String(owner).toLowerCase();
22581
+ if (bound === undefined)
22582
+ return { owners: [name], correlated: [], skippedSubquery: false };
22583
+ if (name === null || bound.has(name))
22584
+ return EMPTY_SCAN;
22585
+ return { owners: [], correlated: [name], skippedSubquery: false };
22586
+ }
22587
+ const scope = nested ? new Set([...bound ?? [], ...boundNames(inner)]) : bound;
22588
+ const scans = Object.values(record).map((value) => Array.isArray(value) ? mergeScans(value.map((item) => scanCondition(item, scope))) : scanCondition(value, scope));
22589
+ const merged = mergeScans(scans);
22590
+ return nested ? { ...merged, skippedSubquery: true } : merged;
22591
+ }
22592
+ function boundNames(select2) {
22593
+ const from = select2?.from ?? [];
22594
+ return Array.isArray(from) ? from.flatMap((ref) => refNames(ref)) : [];
22595
+ }
22596
+ function stripParenthesised(text2) {
22597
+ let previous = text2;
22598
+ for (;; ) {
22599
+ const next = previous.replace(/\([^()]*\)/g, " ");
22600
+ if (next === previous)
22601
+ return next;
22602
+ previous = next;
22603
+ }
22604
+ }
22605
+ function unparseable(sql, dialect, type) {
22606
+ const executable = stripCommentsAndStrings(sql, dialect ? { dialect } : {});
22607
+ const statementLevel = stripParenthesised(executable);
22608
+ const singleTable = SINGLE_TARGET.test(executable) && !SECOND_SOURCE_ANYWHERE.test(executable) && !SECOND_SOURCE_AT_STATEMENT_LEVEL.some((pattern) => pattern.test(statementLevel));
22609
+ if (!singleTable)
22610
+ return verdict("two", "unparseable", undefined, type);
22611
+ if (/\bWHERE\b|\bLIMIT\b/i.test(executable))
22612
+ return verdict("one", undefined, undefined, type);
22613
+ return verdict("two", "unparseable", undefined, type);
22614
+ }
22615
+ function classifyMerge(sql, dialect) {
22616
+ const executable = stripParenthesised(stripCommentsAndStrings(sql, dialect ? { dialect } : {}));
22617
+ if (!MERGE_STATEMENT.test(executable))
22618
+ return;
22619
+ const matched = MERGE_TARGET.exec(executable)?.[1];
22620
+ const table = matched === undefined || matched.endsWith(".") || /^USING$/i.test(matched) ? undefined : matched;
22621
+ const destructive = /\bTHEN\s+DELETE\b/i.test(executable) ? "DELETE" : /\bTHEN\s+UPDATE\b/i.test(executable) ? "UPDATE" : undefined;
22622
+ if (!destructive)
22623
+ return verdict("one", undefined, table, "INSERT");
22624
+ return verdict("two", "multi_table", table, destructive);
22625
+ }
22626
+ function nestedWrite(sql, dialect) {
22627
+ const executable = stripCommentsAndStrings(sql, dialect ? { dialect } : {}).replace(SQL_LOCK_CLAUSE, " ").replace(REFERENTIAL_ACTION, " ");
22628
+ const match = /\b(INSERT|UPDATE|DELETE)\b|\bMERGE\s+INTO\b/i.exec(insideParentheses(executable));
22629
+ if (match === null)
22630
+ return;
22631
+ return keywordToType(match[1] ?? "MERGE");
22632
+ }
22633
+ function insideParentheses(text2) {
22634
+ let depth = 0;
22635
+ let result = "";
22636
+ for (const char of text2) {
22637
+ if (char === "(") {
22638
+ depth += 1;
22639
+ result += " ";
22640
+ continue;
22641
+ }
22642
+ if (char === ")") {
22643
+ depth = Math.max(0, depth - 1);
22644
+ result += " ";
22645
+ continue;
22646
+ }
22647
+ result += depth > 0 ? char : " ";
22648
+ }
22649
+ return result;
22650
+ }
22651
+ function classifyStructuredWriteGate(request) {
22652
+ const conditions = new Set(Object.keys(request.where).map((key) => key.toLowerCase()));
22653
+ const identifies = uniqueSelectors(request.schema).some((selector) => selector.length > 0 && selector.every((column) => conditions.has(column.toLowerCase())));
22654
+ if (identifies)
22655
+ return verdict("one", undefined, request.table);
22656
+ return verdict("two", "non_unique_where", request.table);
22657
+ }
22658
+ function uniqueSelectors(schema) {
22659
+ const declared = schema.primaryKey;
22660
+ const primaryKey = Array.isArray(declared) ? declared : typeof declared === "string" ? [declared] : (schema.columns ?? []).filter((column) => column.primaryKey).map((column) => column.name);
22661
+ const unique = (schema.indexes ?? []).filter((index) => index.unique).map((index) => index.columns);
22662
+ return [primaryKey, ...unique];
22663
+ }
22664
+ function tableName(ref) {
22665
+ if (!ref?.table)
22666
+ return;
22667
+ return ref.db ? `${ref.db}.${ref.table}` : ref.table;
22668
+ }
22669
+ function keywordToType(keyword) {
22670
+ const upper = keyword.toUpperCase();
22671
+ if (WRITE_TYPES.has(upper))
22672
+ return upper;
22673
+ return "INSERT";
22674
+ }
22675
+ function verdict(tier, reason, table, operation) {
22676
+ return {
22677
+ tier,
22678
+ ...reason && { reason },
22679
+ ...table && { table },
22680
+ ...operation && { operation },
22681
+ confirmationPhrase: table ?? WRITE_GATE_FALLBACK_PHRASE
22682
+ };
22683
+ }
22684
+ var WRITE_GATE_FALLBACK_PHRASE = "CONFIRM", PARSER_DIALECTS, WRITE_TYPES, DDL_TARGET, EMPTY_SCAN, SINGLE_TARGET, SECOND_SOURCE_ANYWHERE, SECOND_SOURCE_AT_STATEMENT_LEVEL, MERGE_STATEMENT, MERGE_TARGET, REFERENTIAL_ACTION;
22685
+ var init_write_gate = __esm(() => {
22686
+ init_permission_guard();
22687
+ init_sql_analysis();
22688
+ PARSER_DIALECTS = {
22689
+ mysql: "MySQL",
22690
+ mariadb: "MariaDB",
22691
+ postgresql: "Postgresql"
22692
+ };
22693
+ WRITE_TYPES = new Set([
22694
+ "INSERT",
22695
+ "UPDATE",
22696
+ "DELETE",
22697
+ "TRUNCATE",
22698
+ "DROP",
22699
+ "ALTER",
22700
+ "CREATE"
22701
+ ]);
22702
+ DDL_TARGET = /\b(?:DROP\s+TABLE|TRUNCATE(?:\s+TABLE)?)\s+(?:IF\s+EXISTS\s+)?([`"[\]\w$.]+)\s*;?\s*$/i;
22703
+ EMPTY_SCAN = { owners: [], correlated: [], skippedSubquery: false };
22704
+ SINGLE_TARGET = /^\s*(?:UPDATE\s+(?:ONLY\s+)?[\w`"[\]$.]+\s+SET\b|DELETE\s+FROM\s+(?:ONLY\s+)?[\w`"[\]$.]+(?!\s*,)(?:\s|$))/i;
22705
+ SECOND_SOURCE_ANYWHERE = /\b(?:SELECT|TABLE|VALUES|JOIN|USING)\b/i;
22706
+ SECOND_SOURCE_AT_STATEMENT_LEVEL = [/\bWITH\b/i, /\bUPDATE\b[\s\S]*?\bFROM\b/i];
22707
+ MERGE_STATEMENT = /\bMERGE\s+INTO\b/i;
22708
+ MERGE_TARGET = /\bMERGE\s+INTO\s+(?:ONLY\s+)?([\w$.]+)/i;
22709
+ REFERENTIAL_ACTION = /\bON\s+(?:DELETE|UPDATE)\b/gi;
22710
+ });
22711
+
22712
+ // src/commands/write-gate-guard.ts
22713
+ var exports_write_gate_guard = {};
22714
+ __export(exports_write_gate_guard, {
22715
+ recordGateDecision: () => recordGateDecision,
22716
+ guardStructuredWrite: () => guardStructuredWrite
22717
+ });
22718
+ async function recordGateDecision(request) {
22719
+ await writeAuditEntry(request.config, request.command, request.options, {
22720
+ success: request.outcome === "allowed",
22721
+ sideEffectTier: "db-write",
22722
+ ...request.sql && { sql: request.sql },
22723
+ ...request.verdict.table && { target: request.verdict.table },
22724
+ metadata: {
22725
+ write_gate_tier: request.tier,
22726
+ write_gate_outcome: request.outcome,
22727
+ write_gate_reason: request.verdict.reason ?? "no_where"
22728
+ }
22729
+ });
22730
+ }
22731
+ async function guardStructuredWrite(request) {
22732
+ const verdict2 = classifyStructuredWriteGate({
22733
+ table: request.table,
22734
+ where: request.where,
22735
+ schema: request.schema
22736
+ });
22737
+ if (verdict2.tier !== "two")
22738
+ return true;
22739
+ const operation = request.operation.toUpperCase();
22740
+ const preview = `${operation} ${request.table} WHERE ${Object.keys(request.where).join(", ")}`;
22741
+ const record = (outcome) => recordGateDecision({
22742
+ config: request.config,
22743
+ command: request.operation,
22744
+ options: request.options,
22745
+ verdict: verdict2,
22746
+ outcome,
22747
+ tier: "two",
22748
+ sql: preview
22749
+ });
22750
+ try {
22751
+ const proceed = await enforceWriteGate({
22752
+ verdict: verdict2,
22753
+ statement: preview,
22754
+ operation,
22755
+ human: humanOutputContext(request.options)
22756
+ });
22757
+ await record(proceed ? "allowed" : "declined");
22758
+ return proceed;
22759
+ } catch (error) {
22760
+ if (error instanceof WriteGateRefusal)
22761
+ await record("refused");
22762
+ throw error;
22763
+ }
22764
+ }
22765
+ var init_write_gate_guard = __esm(() => {
22766
+ init_integration_helper();
22767
+ init_mutation_outcome();
22768
+ init_write_gate();
22769
+ init_write_gate_prompt();
22770
+ });
22771
+
22205
22772
  // src/core/permission/redis.ts
22206
22773
  var exports_redis = {};
22207
22774
  __export(exports_redis, {
@@ -22528,10 +23095,24 @@ async function executeQueryCommand(sql, options, command, runtimeOverrides = {})
22528
23095
  connectionName = connectionNames[0];
22529
23096
  config = await runtime.loadConfig(configPath, connectionName);
22530
23097
  const context = { config, configPath, connectionName };
23098
+ if (!await runtime.enforceGate(resolvedSql, options, context))
23099
+ return;
22531
23100
  await runtime.preflight(resolvedSql, options, context, fieldSelection, false);
22532
23101
  const execution = await runtime.executeConnection(resolvedSql, options, context, fieldSelection);
22533
23102
  await runtime.presentResult(resolvedSql, options, execution, tableCellLimit);
22534
23103
  } catch (error) {
23104
+ const { WriteGateRefusal: WriteGateRefusal2 } = await Promise.resolve().then(() => (init_write_gate_prompt(), exports_write_gate_prompt));
23105
+ if (error instanceof WriteGateRefusal2) {
23106
+ if (options.format === "json") {
23107
+ console.log(JSON.stringify({
23108
+ status: "refused",
23109
+ code: error.code,
23110
+ reason: error.reason,
23111
+ ...error.table && { table: error.table }
23112
+ }, null, 2));
23113
+ }
23114
+ throw error;
23115
+ }
22535
23116
  let auditId = null;
22536
23117
  let envelopeId;
22537
23118
  if (options.recovery === true && !multiConnectionRequest) {
@@ -22618,6 +23199,48 @@ function scopeMultiConnectionNotices(execution) {
22618
23199
  notices: []
22619
23200
  };
22620
23201
  }
23202
+ async function enforceQueryWriteGate(query, options, context) {
23203
+ const { config } = context;
23204
+ const { toSqlDialect: toSqlDialect2, enforcePermission: enforcePermission2 } = await Promise.resolve().then(() => (init_permission_guard(), exports_permission_guard));
23205
+ const dialect = toSqlDialect2(config.connection?.system);
23206
+ if (!dialect)
23207
+ return true;
23208
+ const { classifySqlWriteGate: classifySqlWriteGate2 } = await Promise.resolve().then(() => (init_write_gate(), exports_write_gate));
23209
+ const verdict2 = await classifySqlWriteGate2(query, { dialect });
23210
+ if (verdict2.tier === "none")
23211
+ return true;
23212
+ enforcePermission2(query, config.permission, dialect);
23213
+ const { enforceWriteGate: enforceWriteGate2, WriteGateRefusal: WriteGateRefusal2 } = await Promise.resolve().then(() => (init_write_gate_prompt(), exports_write_gate_prompt));
23214
+ const { humanOutputContext: humanOutputContext2 } = await Promise.resolve().then(() => (init_mutation_outcome(), exports_mutation_outcome));
23215
+ const { recordGateDecision: recordGateDecision2 } = await Promise.resolve().then(() => (init_write_gate_guard(), exports_write_gate_guard));
23216
+ const record = (outcome) => recordGateDecision2({
23217
+ config,
23218
+ command: "query",
23219
+ options: { ...options, config: context.configPath, connectionName: context.connectionName },
23220
+ verdict: verdict2,
23221
+ outcome,
23222
+ tier: verdict2.tier === "two" ? "two" : "one",
23223
+ sql: query
23224
+ });
23225
+ try {
23226
+ const proceed = await enforceWriteGate2({
23227
+ verdict: verdict2,
23228
+ statement: query,
23229
+ operation: verdict2.operation ?? "WRITE",
23230
+ human: humanOutputContext2(options),
23231
+ ...options.yes !== undefined && { yes: options.yes }
23232
+ });
23233
+ if (verdict2.tier === "two")
23234
+ await record(proceed ? "allowed" : "declined");
23235
+ else if (!proceed)
23236
+ await record("declined");
23237
+ return proceed;
23238
+ } catch (error) {
23239
+ if (error instanceof WriteGateRefusal2)
23240
+ await record("refused");
23241
+ throw error;
23242
+ }
23243
+ }
22621
23244
  async function preflightQuery(query, options, context, fieldSelection, multiConnection) {
22622
23245
  const { config } = context;
22623
23246
  if (!config.connection)
@@ -23061,6 +23684,7 @@ var init_query = __esm(() => {
23061
23684
  executeConnection: executeConnectionQuery,
23062
23685
  presentResult: presentSingleResult,
23063
23686
  writeAudit: writeAuditEntry,
23687
+ enforceGate: enforceQueryWriteGate,
23064
23688
  randomUUID: () => crypto3.randomUUID(),
23065
23689
  emitRecovery: async (error, context, options) => {
23066
23690
  const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
@@ -23085,7 +23709,7 @@ __export(exports_inline_registrars, {
23085
23709
  registerDeleteCommand: () => registerDeleteCommand
23086
23710
  });
23087
23711
  function registerQueryCommand(program2, run) {
23088
- program2.command("query [sql]").description(t("query.description")).option("-f, --query-file <path>", "Read query text from a UTF-8 file, or - for stdin").option("--format <type>", "Output format: table, json, csv, html", "table").option("--ui", "Show interactive dashboard in browser", false).option("--limit <number>", "Limit result rows (overrides auto-limit)", parsePositiveInteger).option("--no-limit", "Disable auto-limit in query-only mode").option("--collection <name>", "MongoDB collection name; Elasticsearch index name").option("--index <name>", "Elasticsearch index name (alias for --collection)").option("--fields <list>", "Include fields, or exclude them with --fields=-field_a,-field_b").option("--truncate <number>", "Limit serialized table cells to N Unicode characters", parsePositiveInteger).option("--no-truncate", "Disable the default table-cell truncation").option("--slow-ms <number>", "Emit a passive performance hint at or above this execution time; 0 disables it", parseSlowMs, 1000).addOption(createConnectionSelectorOption()).option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (sql, options, command) => {
23712
+ program2.command("query [sql]").description(t("query.description")).option("-f, --query-file <path>", "Read query text from a UTF-8 file, or - for stdin").option("--format <type>", "Output format: table, json, csv, html", "table").option("--ui", "Show interactive dashboard in browser", false).option("--limit <number>", "Limit result rows (overrides auto-limit)", parsePositiveInteger).option("--no-limit", "Disable auto-limit in query-only mode").option("--collection <name>", "MongoDB collection name; Elasticsearch index name").option("--index <name>", "Elasticsearch index name (alias for --collection)").option("--fields <list>", "Include fields, or exclude them with --fields=-field_a,-field_b").option("--truncate <number>", "Limit serialized table cells to N Unicode characters", parsePositiveInteger).option("--no-truncate", "Disable the default table-cell truncation").option("--slow-ms <number>", "Emit a passive performance hint at or above this execution time; 0 disables it", parseSlowMs, 1000).addOption(createConnectionSelectorOption()).option("--yes", "Skip the confirmation shown before an ordinary write; statements that are not limited to specific rows ignore it", false).option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (sql, options, command) => {
23089
23713
  const rawArgs = command.parent?.rawArgs ?? [];
23090
23714
  const rawTruncate = findLongOptionValue(rawArgs, "--truncate");
23091
23715
  const rootUse = command.parent?.opts().use;
@@ -23401,8 +24025,8 @@ function applyBlacklistRules(facts, blacklist, factors) {
23401
24025
  }
23402
24026
  function findBlacklistedColumnsForTable(blacklist, table) {
23403
24027
  const columns = blacklist.columns ?? {};
23404
- for (const [tableName, columnNames] of Object.entries(columns)) {
23405
- if (tableName.toLowerCase() === table.toLowerCase()) {
24028
+ for (const [tableName2, columnNames] of Object.entries(columns)) {
24029
+ if (tableName2.toLowerCase() === table.toLowerCase()) {
23406
24030
  return new Set(columnNames);
23407
24031
  }
23408
24032
  }
@@ -25045,9 +25669,12 @@ async function qCommand(name, options, command) {
25045
25669
  console.error(`\u26A0 ${w2}`);
25046
25670
  if (prepared.warnings.length > 0)
25047
25671
  console.error("");
25672
+ const family = engineFamily(engine);
25673
+ const sqlDialect = SQL_DIALECTS.find((dialect) => dialect === connectionSystem);
25674
+ const classification = family === "sql" ? enforcePermission(prepared.rewrittenSql, config.permission, sqlDialect) : undefined;
25048
25675
  if (options.dryRun) {
25049
25676
  console.log(formatDryRun({
25050
- family: engineFamily(engine),
25677
+ family,
25051
25678
  driverSql: prepared.driver.sql,
25052
25679
  values: prepared.driver.values,
25053
25680
  execHints: prepared.execHints,
@@ -25068,8 +25695,6 @@ async function qCommand(name, options, command) {
25068
25695
  }
25069
25696
  const blacklistManager = new BlacklistManager(config);
25070
25697
  const blacklistValidator = new BlacklistValidator(blacklistManager);
25071
- const family = engineFamily(engine);
25072
- const sqlDialect = SQL_DIALECTS.find((dialect) => dialect === connectionSystem);
25073
25698
  const targets = family === "sql" ? extractTableReferences(prepared.rewrittenSql, {
25074
25699
  ...sqlDialect ? { dialect: sqlDialect } : {}
25075
25700
  }) : family === "es" ? [prepared.execHints?.index ?? ""] : [];
@@ -25077,8 +25702,8 @@ async function qCommand(name, options, command) {
25077
25702
  targetNameForAudit = targetName || name;
25078
25703
  if (family === "es") {
25079
25704
  blacklistValidator.checkIndexBlacklist("SELECT", targetName);
25080
- } else if (family !== "redis" && targets.length > 0) {
25081
- blacklistValidator.checkTablesBlacklist("SELECT", targets);
25705
+ } else if (classification && targets.length > 0) {
25706
+ blacklistValidator.checkTablesBlacklist(classification.type, targets);
25082
25707
  }
25083
25708
  const adapter = AdapterFactory.createAdapter(config.connection);
25084
25709
  await adapter.connect();
@@ -25366,12 +25991,12 @@ class DataExecutor {
25366
25991
  this.dbSystem = dbSystem;
25367
25992
  this.blacklistValidator = blacklistValidator;
25368
25993
  }
25369
- buildInsertSql(tableName, data, schema) {
25994
+ buildInsertSql(tableName2, data, schema) {
25370
25995
  const columnNames = schema.columns.map((col) => col.name);
25371
25996
  const dataKeys = Object.keys(data);
25372
25997
  for (const key of dataKeys) {
25373
25998
  if (!columnNames.includes(key)) {
25374
- throw new Error(`Column "${key}" not found in table "${tableName}". Valid columns: ${columnNames.join(", ")}`);
25999
+ throw new Error(`Column "${key}" not found in table "${tableName2}". Valid columns: ${columnNames.join(", ")}`);
25375
26000
  }
25376
26001
  }
25377
26002
  const columns = dataKeys;
@@ -25379,38 +26004,38 @@ class DataExecutor {
25379
26004
  const systemType = this.getSystemType();
25380
26005
  const placeholders = systemType === "postgresql" ? dataKeys.map((_, index) => `$${index + 1}`).join(", ") : dataKeys.map(() => "?").join(", ");
25381
26006
  const quote = this.getQuoteChar();
25382
- const sql = `INSERT INTO ${quote}${tableName}${quote} (${columns.map((col) => `${quote}${col}${quote}`).join(", ")}) VALUES (${placeholders})`;
26007
+ const sql = `INSERT INTO ${quote}${tableName2}${quote} (${columns.map((col) => `${quote}${col}${quote}`).join(", ")}) VALUES (${placeholders})`;
25383
26008
  return {
25384
26009
  sql,
25385
26010
  params: values
25386
26011
  };
25387
26012
  }
25388
- async executeInsert(tableName, data, schema, options) {
26013
+ async executeInsert(tableName2, data, schema, options) {
25389
26014
  const timestamp = new Date().toISOString();
25390
26015
  try {
25391
- this.checkBlacklist("INSERT", tableName);
26016
+ this.checkBlacklist("INSERT", tableName2);
25392
26017
  enforcePermissionForType("INSERT", this.permission);
25393
- return await this.executeMutation("insert", this.buildInsertSql(tableName, data, schema), timestamp, options, { destructive: false });
26018
+ return await this.executeMutation("insert", this.buildInsertSql(tableName2, data, schema), timestamp, options, { destructive: false });
25394
26019
  } catch (error) {
25395
26020
  return this.handleMutationError("insert", timestamp, error);
25396
26021
  }
25397
26022
  }
25398
- async executeUpdate(tableName, data, where, schema, options) {
26023
+ async executeUpdate(tableName2, data, where, schema, options) {
25399
26024
  const timestamp = new Date().toISOString();
25400
26025
  try {
25401
- this.checkBlacklist("UPDATE", tableName);
26026
+ this.checkBlacklist("UPDATE", tableName2);
25402
26027
  enforcePermissionForType("UPDATE", this.permission);
25403
- return await this.executeMutation("update", this.buildUpdateSql(tableName, data, where, schema), timestamp, options, { destructive: false });
26028
+ return await this.executeMutation("update", this.buildUpdateSql(tableName2, data, where, schema), timestamp, options, { destructive: false });
25404
26029
  } catch (error) {
25405
26030
  return this.handleMutationError("update", timestamp, error);
25406
26031
  }
25407
26032
  }
25408
- async executeDelete(tableName, where, schema, options) {
26033
+ async executeDelete(tableName2, where, schema, options) {
25409
26034
  const timestamp = new Date().toISOString();
25410
26035
  try {
25411
- this.checkBlacklist("DELETE", tableName);
26036
+ this.checkBlacklist("DELETE", tableName2);
25412
26037
  enforcePermissionForType("DELETE", this.permission);
25413
- return await this.executeMutation("delete", this.buildDeleteSql(tableName, where, schema), timestamp, options, { destructive: true });
26038
+ return await this.executeMutation("delete", this.buildDeleteSql(tableName2, where, schema), timestamp, options, { destructive: true });
25414
26039
  } catch (error) {
25415
26040
  return this.handleMutationError("delete", timestamp, error);
25416
26041
  }
@@ -25421,8 +26046,8 @@ class DataExecutor {
25421
26046
  getQuoteChar() {
25422
26047
  return this.dbSystem === "mysql" ? "`" : '"';
25423
26048
  }
25424
- checkBlacklist(operation, tableName) {
25425
- this.blacklistValidator?.checkTableBlacklist(operation, tableName, []);
26049
+ checkBlacklist(operation, tableName2) {
26050
+ this.blacklistValidator?.checkTableBlacklist(operation, tableName2, []);
25426
26051
  }
25427
26052
  async executeMutation(operation, statement, timestamp, options, { destructive }) {
25428
26053
  if (options?.dryRun) {
@@ -25477,13 +26102,13 @@ class DataExecutor {
25477
26102
  error: `${operation.toUpperCase()} failed: ${errorMessage2}`
25478
26103
  };
25479
26104
  }
25480
- buildUpdateSql(tableName, data, where, schema) {
26105
+ buildUpdateSql(tableName2, data, where, schema) {
25481
26106
  const dataKeys = Object.keys(data);
25482
26107
  const whereKeys = Object.keys(where);
25483
26108
  const columnNames = schema.columns.map((col) => col.name);
25484
26109
  for (const key of [...dataKeys, ...whereKeys]) {
25485
26110
  if (!columnNames.includes(key)) {
25486
- throw new Error(`Column "${key}" not found in table "${tableName}"`);
26111
+ throw new Error(`Column "${key}" not found in table "${tableName2}"`);
25487
26112
  }
25488
26113
  }
25489
26114
  const systemType = this.getSystemType();
@@ -25497,16 +26122,16 @@ class DataExecutor {
25497
26122
  const placeholder = systemType === "postgresql" ? `$${paramIndex++}` : "?";
25498
26123
  return `${quote}${key}${quote} = ${placeholder}`;
25499
26124
  }).join(" AND ");
25500
- const sql = `UPDATE ${quote}${tableName}${quote} SET ${setClause} WHERE ${whereClause}`;
26125
+ const sql = `UPDATE ${quote}${tableName2}${quote} SET ${setClause} WHERE ${whereClause}`;
25501
26126
  const params = [...dataKeys.map((key) => data[key]), ...whereKeys.map((key) => where[key])];
25502
26127
  return { sql, params };
25503
26128
  }
25504
- buildDeleteSql(tableName, where, schema) {
26129
+ buildDeleteSql(tableName2, where, schema) {
25505
26130
  const whereKeys = Object.keys(where);
25506
26131
  const columnNames = schema.columns.map((col) => col.name);
25507
26132
  for (const key of whereKeys) {
25508
26133
  if (!columnNames.includes(key)) {
25509
- throw new Error(`Column "${key}" not found in table "${tableName}"`);
26134
+ throw new Error(`Column "${key}" not found in table "${tableName2}"`);
25510
26135
  }
25511
26136
  }
25512
26137
  const systemType = this.getSystemType();
@@ -25516,7 +26141,7 @@ class DataExecutor {
25516
26141
  const placeholder = systemType === "postgresql" ? `$${paramIndex++}` : "?";
25517
26142
  return `${quote}${key}${quote} = ${placeholder}`;
25518
26143
  }).join(" AND ");
25519
- const sql = `DELETE FROM ${quote}${tableName}${quote} WHERE ${whereClause}`;
26144
+ const sql = `DELETE FROM ${quote}${tableName2}${quote} WHERE ${whereClause}`;
25520
26145
  const params = whereKeys.map((key) => where[key]);
25521
26146
  return { sql, params };
25522
26147
  }
@@ -25590,87 +26215,6 @@ function auditOutcomeForMutation(result, table, recoveryRef) {
25590
26215
  };
25591
26216
  }
25592
26217
 
25593
- // src/commands/mutation-outcome.ts
25594
- function shouldRenderForHuman(context) {
25595
- if (context.format === "json")
25596
- return false;
25597
- return context.isTTY;
25598
- }
25599
- function humanOutputContext(options) {
25600
- return { isTTY: process.stdout.isTTY === true, ...options.format && { format: options.format } };
25601
- }
25602
- function renderMutationOutcome(result, table, elapsedMs) {
25603
- if (result.status === "error") {
25604
- return [
25605
- t_vars("ceremony.failed", { reason: result.error ?? t("ceremony.unknown_error") }),
25606
- t("ceremony.recovery_retry")
25607
- ];
25608
- }
25609
- if (result.status === "cancelled") {
25610
- return [t_vars("ceremony.cancelled", { table })];
25611
- }
25612
- if (result.status === "dry_run") {
25613
- return [t_vars("ceremony.dry_run", { table })];
25614
- }
25615
- const elapsed = t_vars("ceremony.elapsed", { seconds: (elapsedMs / 1000).toFixed(2) });
25616
- if (result.rows_affected === 0) {
25617
- return [t_vars("ceremony.matched_nothing", { table }), elapsed];
25618
- }
25619
- return [
25620
- t_vars(VERB_KEY[result.operation], { count: result.rows_affected, table }),
25621
- elapsed,
25622
- t("ceremony.recovery_none")
25623
- ];
25624
- }
25625
- function printMutationOutcome(result, table, elapsedMs, context, envelopeExtras = {}) {
25626
- if (shouldRenderForHuman(context)) {
25627
- const writeLine = result.status === "error" ? console.error : console.log;
25628
- for (const line of renderMutationOutcome(result, table, elapsedMs))
25629
- writeLine(line);
25630
- return;
25631
- }
25632
- console.log(JSON.stringify({ ...mutationEnvelope(result), ...envelopeExtras }, null, 2));
25633
- }
25634
- function printMutationFailure(error, operation, context) {
25635
- if (shouldRenderForHuman(context)) {
25636
- console.error(t_vars("ceremony.failed", { reason: error.message }));
25637
- if (error instanceof BlacklistError) {
25638
- console.error(t("ceremony.blacklist_hint"));
25639
- }
25640
- return;
25641
- }
25642
- console.log(JSON.stringify({ status: "error", operation, rows_affected: 0, error: error.message }, null, 2));
25643
- }
25644
- function cancelledOutcome(operation, preview) {
25645
- return {
25646
- status: "cancelled",
25647
- operation,
25648
- rows_affected: 0,
25649
- sql: preview,
25650
- timestamp: new Date().toISOString()
25651
- };
25652
- }
25653
- function mutationEnvelope(result) {
25654
- return {
25655
- status: result.status,
25656
- operation: result.operation,
25657
- rows_affected: result.rows_affected,
25658
- ...result.timestamp && { timestamp: result.timestamp },
25659
- ...result.sql && { sql: result.sql },
25660
- ...result.error && { error: result.error }
25661
- };
25662
- }
25663
- var VERB_KEY;
25664
- var init_mutation_outcome = __esm(() => {
25665
- init_message_loader();
25666
- init_blacklist();
25667
- VERB_KEY = {
25668
- insert: "ceremony.inserted",
25669
- update: "ceremony.updated",
25670
- delete: "ceremony.deleted"
25671
- };
25672
- });
25673
-
25674
26218
  // src/core/mongo/dry-run-formatter.ts
25675
26219
  function pretty(value) {
25676
26220
  return JSON.stringify(value, null, INDENT);
@@ -26974,6 +27518,20 @@ async function updateCommand(table, options, command) {
26974
27518
  await adapter.connect();
26975
27519
  try {
26976
27520
  const schema = await adapter.getTableSchema(table);
27521
+ const { guardStructuredWrite: guardStructuredWrite2 } = await Promise.resolve().then(() => (init_write_gate_guard(), exports_write_gate_guard));
27522
+ if (options.dryRun !== true && !await guardStructuredWrite2({
27523
+ operation: "update",
27524
+ table,
27525
+ where: whereConditions,
27526
+ schema,
27527
+ config,
27528
+ options
27529
+ })) {
27530
+ const cancelled = cancelledOutcome("update", `UPDATE ${table}`);
27531
+ printMutationOutcome(cancelled, table, 0, humanOutputContext(options));
27532
+ await writeAuditEntry(config, "update", options, auditOutcomeForMutation(cancelled, table));
27533
+ return;
27534
+ }
26977
27535
  const dbSystem = config.connection.system === "postgresql" ? "postgresql" : "mysql";
26978
27536
  const blacklistManager = new BlacklistManager(config);
26979
27537
  const blacklistValidator = new BlacklistValidator(blacklistManager);
@@ -26999,6 +27557,11 @@ async function updateCommand(table, options, command) {
26999
27557
  await adapter.disconnect();
27000
27558
  }
27001
27559
  } catch (error) {
27560
+ const { WriteGateRefusal: WriteGateRefusal2 } = await Promise.resolve().then(() => (init_write_gate_prompt(), exports_write_gate_prompt));
27561
+ if (error instanceof WriteGateRefusal2) {
27562
+ printMutationFailure(error, "update", humanOutputContext(options));
27563
+ process.exit(1);
27564
+ }
27002
27565
  let auditId = null;
27003
27566
  let envelopeId;
27004
27567
  if (options.recovery === true) {
@@ -27254,6 +27817,20 @@ async function deleteCommand(table, options, command) {
27254
27817
  await adapter.connect();
27255
27818
  try {
27256
27819
  const schema = await adapter.getTableSchema(table);
27820
+ const { guardStructuredWrite: guardStructuredWrite2 } = await Promise.resolve().then(() => (init_write_gate_guard(), exports_write_gate_guard));
27821
+ if (options.dryRun !== true && !await guardStructuredWrite2({
27822
+ operation: "delete",
27823
+ table,
27824
+ where: whereConditions,
27825
+ schema,
27826
+ config,
27827
+ options
27828
+ })) {
27829
+ const cancelled = cancelledOutcome("delete", `DELETE FROM ${table}`);
27830
+ printMutationOutcome(cancelled, table, 0, humanOutputContext(options));
27831
+ await writeAuditEntry(config, "delete", options, auditOutcomeForMutation(cancelled, table));
27832
+ return;
27833
+ }
27257
27834
  const dbSystem = config.connection.system === "postgresql" ? "postgresql" : "mysql";
27258
27835
  const blacklistManager = new BlacklistManager(config);
27259
27836
  const blacklistValidator = new BlacklistValidator(blacklistManager);
@@ -27279,6 +27856,11 @@ async function deleteCommand(table, options, command) {
27279
27856
  await adapter.disconnect();
27280
27857
  }
27281
27858
  } catch (error) {
27859
+ const { WriteGateRefusal: WriteGateRefusal2 } = await Promise.resolve().then(() => (init_write_gate_prompt(), exports_write_gate_prompt));
27860
+ if (error instanceof WriteGateRefusal2) {
27861
+ printMutationFailure(error, "delete", humanOutputContext(options));
27862
+ process.exit(1);
27863
+ }
27282
27864
  let auditId = null;
27283
27865
  let envelopeId;
27284
27866
  if (options.recovery === true) {
@@ -27777,16 +28359,6 @@ var init_export = __esm(() => {
27777
28359
  SQL_PATTERN = /^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|SHOW|DESCRIBE)\b/i;
27778
28360
  });
27779
28361
 
27780
- // src/core/sql-parser.ts
27781
- function sqlParser() {
27782
- if (!cached2) {
27783
- const { Parser } = __require("node-sql-parser");
27784
- cached2 = new Parser;
27785
- }
27786
- return cached2;
27787
- }
27788
- var cached2 = null;
27789
-
27790
28362
  // src/core/explain/read-only.ts
27791
28363
  function containsAnalyzeUnsafeNode(node) {
27792
28364
  if (!node || typeof node !== "object")
@@ -28891,12 +29463,12 @@ function compactVisibleSchema(config) {
28891
29463
  const blacklistManager = new BlacklistManager(config);
28892
29464
  const compactSchema = {};
28893
29465
  const rawSchema = config.schema ?? {};
28894
- for (const [tableName, tableSchema] of Object.entries(rawSchema)) {
28895
- if (blacklistManager.isTableBlacklisted(tableName))
29466
+ for (const [tableName2, tableSchema] of Object.entries(rawSchema)) {
29467
+ if (blacklistManager.isTableBlacklisted(tableName2))
28896
29468
  continue;
28897
29469
  const columns = [];
28898
29470
  for (const col of tableSchema.columns || []) {
28899
- if (blacklistManager.isColumnBlacklisted(tableName, col.name))
29471
+ if (blacklistManager.isColumnBlacklisted(tableName2, col.name))
28900
29472
  continue;
28901
29473
  const compactCol = {
28902
29474
  name: col.name,
@@ -28916,7 +29488,7 @@ function compactVisibleSchema(config) {
28916
29488
  columns.push(compactCol);
28917
29489
  }
28918
29490
  const compactTable = {
28919
- name: tableSchema.name || tableName,
29491
+ name: tableSchema.name || tableName2,
28920
29492
  columns
28921
29493
  };
28922
29494
  const rowCount = tableSchema.estimatedRowCount ?? tableSchema.rowCount;
@@ -28932,7 +29504,7 @@ function compactVisibleSchema(config) {
28932
29504
  refColumns: fk.refColumns
28933
29505
  }));
28934
29506
  }
28935
- compactSchema[tableName] = compactTable;
29507
+ compactSchema[tableName2] = compactTable;
28936
29508
  }
28937
29509
  return compactSchema;
28938
29510
  }
@@ -29040,10 +29612,10 @@ function serializeXml(payload) {
29040
29612
  }
29041
29613
  if (Object.keys(payload.schema).length > 0) {
29042
29614
  parts.push(" <tables>");
29043
- for (const [tableName, table] of Object.entries(payload.schema)) {
29615
+ for (const [tableName2, table] of Object.entries(payload.schema)) {
29044
29616
  const rowsAttr = table.rowCount !== undefined ? ` rows="${table.rowCount}"` : "";
29045
29617
  const pkAttr = table.primaryKey ? ` primary_key="${table.primaryKey.join(",")}"` : "";
29046
- parts.push(` <table name="${escapeXml(tableName)}"${rowsAttr}${pkAttr}>`);
29618
+ parts.push(` <table name="${escapeXml(tableName2)}"${rowsAttr}${pkAttr}>`);
29047
29619
  for (const col of table.columns) {
29048
29620
  const pk = col.primaryKey ? ' primary_key="true"' : "";
29049
29621
  const nulls = col.nullable ? ' nullable="true"' : ' nullable="false"';
@@ -29154,9 +29726,9 @@ function serializeMarkdown(payload) {
29154
29726
  parts.push(`*No table schemas cached or active.*`);
29155
29727
  parts.push(``);
29156
29728
  } else {
29157
- for (const [tableName, table] of Object.entries(payload.schema)) {
29729
+ for (const [tableName2, table] of Object.entries(payload.schema)) {
29158
29730
  const rows = table.rowCount !== undefined ? ` (~${table.rowCount.toLocaleString()} rows)` : "";
29159
- parts.push(`### \`${tableName}\`${rows}`);
29731
+ parts.push(`### \`${tableName2}\`${rows}`);
29160
29732
  parts.push(``);
29161
29733
  parts.push(`| Column | Type | Nullable | Primary Key | Default | References |`);
29162
29734
  parts.push(`| :--- | :--- | :---: | :---: | :--- | :--- |`);
@@ -30289,37 +30861,37 @@ async function blacklistList(configPath, format = "text") {
30289
30861
  console.error(`\u26A0 blacklist.columns["${w2.collection}"]: '${w2.raw}' is ignored on mongo connections (${w2.reason}).`);
30290
30862
  }
30291
30863
  }
30292
- async function blacklistTableAdd(tableName, configPath) {
30293
- if (!isValidTableName(tableName)) {
30294
- throw new Error(t_vars("errors.invalid_table_name", { table: tableName }));
30864
+ async function blacklistTableAdd(tableName2, configPath) {
30865
+ if (!isValidTableName(tableName2)) {
30866
+ throw new Error(t_vars("errors.invalid_table_name", { table: tableName2 }));
30295
30867
  }
30296
30868
  const config = await configModule.read(configPath);
30297
30869
  const blacklist = getOrInitBlacklist(config);
30298
- if (blacklist.tables.includes(tableName)) {
30299
- throw new Error(t_vars("errors.table_already_blacklisted", { table: tableName }));
30870
+ if (blacklist.tables.includes(tableName2)) {
30871
+ throw new Error(t_vars("errors.table_already_blacklisted", { table: tableName2 }));
30300
30872
  }
30301
30873
  const newBlacklist = {
30302
30874
  ...blacklist,
30303
- tables: [...blacklist.tables, tableName]
30875
+ tables: [...blacklist.tables, tableName2]
30304
30876
  };
30305
30877
  await configModule.write(configPath, { ...config, blacklist: newBlacklist });
30306
- console.log(t_vars("blacklist.table_added", { table: tableName }));
30878
+ console.log(t_vars("blacklist.table_added", { table: tableName2 }));
30307
30879
  }
30308
- async function blacklistTableRemove(tableName, configPath) {
30309
- if (!isValidTableName(tableName)) {
30310
- throw new Error(t_vars("errors.invalid_table_name", { table: tableName }));
30880
+ async function blacklistTableRemove(tableName2, configPath) {
30881
+ if (!isValidTableName(tableName2)) {
30882
+ throw new Error(t_vars("errors.invalid_table_name", { table: tableName2 }));
30311
30883
  }
30312
30884
  const config = await configModule.read(configPath);
30313
30885
  const blacklist = getOrInitBlacklist(config);
30314
- if (!blacklist.tables.includes(tableName)) {
30315
- throw new Error(t_vars("errors.table_not_in_blacklist", { table: tableName }));
30886
+ if (!blacklist.tables.includes(tableName2)) {
30887
+ throw new Error(t_vars("errors.table_not_in_blacklist", { table: tableName2 }));
30316
30888
  }
30317
30889
  const newBlacklist = {
30318
30890
  ...blacklist,
30319
- tables: blacklist.tables.filter((t2) => t2 !== tableName)
30891
+ tables: blacklist.tables.filter((t2) => t2 !== tableName2)
30320
30892
  };
30321
30893
  await configModule.write(configPath, { ...config, blacklist: newBlacklist });
30322
- console.log(t_vars("blacklist.table_removed", { table: tableName }));
30894
+ console.log(t_vars("blacklist.table_removed", { table: tableName2 }));
30323
30895
  }
30324
30896
  async function blacklistColumnAdd(identifier, configPath) {
30325
30897
  const parsed = parseColumnIdentifier(identifier);
@@ -30389,17 +30961,17 @@ var init_blacklist2 = __esm(() => {
30389
30961
  }
30390
30962
  });
30391
30963
  tableCmd = blacklistCommand.command("table").description(t("blacklist.tables_label"));
30392
- tableCmd.command("add <table>").description("Add table to blacklist").option("--config <path>", "Path to .dbcli config file", DEFAULT_CONFIG_PATH).action(async (tableName, options) => {
30964
+ tableCmd.command("add <table>").description("Add table to blacklist").option("--config <path>", "Path to .dbcli config file", DEFAULT_CONFIG_PATH).action(async (tableName2, options) => {
30393
30965
  try {
30394
- await blacklistTableAdd(tableName, options.config || DEFAULT_CONFIG_PATH);
30966
+ await blacklistTableAdd(tableName2, options.config || DEFAULT_CONFIG_PATH);
30395
30967
  } catch (error) {
30396
30968
  console.error(error.message);
30397
30969
  process.exit(1);
30398
30970
  }
30399
30971
  });
30400
- tableCmd.command("remove <table>").description("Remove table from blacklist").option("--config <path>", "Path to .dbcli config file", DEFAULT_CONFIG_PATH).action(async (tableName, options) => {
30972
+ tableCmd.command("remove <table>").description("Remove table from blacklist").option("--config <path>", "Path to .dbcli config file", DEFAULT_CONFIG_PATH).action(async (tableName2, options) => {
30401
30973
  try {
30402
- await blacklistTableRemove(tableName, options.config || DEFAULT_CONFIG_PATH);
30974
+ await blacklistTableRemove(tableName2, options.config || DEFAULT_CONFIG_PATH);
30403
30975
  } catch (error) {
30404
30976
  console.error(error.message);
30405
30977
  process.exit(1);
@@ -31033,7 +31605,7 @@ function consumeCreateTable(node, statement, context) {
31033
31605
  return;
31034
31606
  }
31035
31607
  const identity = resolveTableIdentifier(parsedIdentifier);
31036
- const tableName = qualifiedTableName(identity);
31608
+ const tableName2 = qualifiedTableName(identity);
31037
31609
  const unsupportedTableField = firstMeaningfulUnsupportedField(node, [
31038
31610
  "type",
31039
31611
  "keyword",
@@ -31043,14 +31615,14 @@ function consumeCreateTable(node, statement, context) {
31043
31615
  ]);
31044
31616
  if (unsupportedTableField) {
31045
31617
  context.unparsed.push({
31046
- location: tableName,
31618
+ location: tableName2,
31047
31619
  reason: `blocked: unsupported CREATE TABLE ${unsupportedTableField}`
31048
31620
  });
31049
31621
  return;
31050
31622
  }
31051
31623
  if (!Array.isArray(node.create_definitions)) {
31052
31624
  context.unparsed.push({
31053
- location: tableName,
31625
+ location: tableName2,
31054
31626
  reason: "blocked: unsupported CREATE TABLE without column definitions"
31055
31627
  });
31056
31628
  return;
@@ -31065,7 +31637,7 @@ function consumeCreateTable(node, statement, context) {
31065
31637
  const definitions = [];
31066
31638
  for (const value of node.create_definitions) {
31067
31639
  if (!isAst(value)) {
31068
- pushUnsupportedDefinition(tableName, "malformed", context.unparsed);
31640
+ pushUnsupportedDefinition(tableName2, "malformed", context.unparsed);
31069
31641
  continue;
31070
31642
  }
31071
31643
  definitions.push(value);
@@ -31091,14 +31663,14 @@ function consumeCreateTable(node, statement, context) {
31091
31663
  consumeTablePrimaryKey(definition, table, context.unparsed);
31092
31664
  continue;
31093
31665
  }
31094
- pushUnsupportedDefinition(tableName, stringValue2(definition.constraint_type) ?? stringValue2(definition.resource) ?? "malformed", context.unparsed);
31666
+ pushUnsupportedDefinition(tableName2, stringValue2(definition.constraint_type) ?? stringValue2(definition.resource) ?? "malformed", context.unparsed);
31095
31667
  }
31096
31668
  const key = tableIdentityKey(identity);
31097
31669
  if (context.tablesByIdentity.has(key)) {
31098
- context.duplicateTableIdentity ??= tableName;
31670
+ context.duplicateTableIdentity ??= tableName2;
31099
31671
  context.unparsed.push({
31100
- location: tableName,
31101
- reason: `blocked: duplicate table identity '${tableName}'`
31672
+ location: tableName2,
31673
+ reason: `blocked: duplicate table identity '${tableName2}'`
31102
31674
  });
31103
31675
  return;
31104
31676
  }
@@ -31241,12 +31813,12 @@ function consumeCreateIndex(node, statement, context) {
31241
31813
  return;
31242
31814
  }
31243
31815
  const identity = resolveTableIdentifier(parsedIdentifier);
31244
- const tableName = qualifiedTableName(identity);
31816
+ const tableName2 = qualifiedTableName(identity);
31245
31817
  const target = context.tablesByIdentity.get(tableIdentityKey(identity));
31246
31818
  if (!target) {
31247
31819
  context.unparsed.push({
31248
31820
  location: indexName ?? "index",
31249
- reason: `blocked: CREATE INDEX targets unknown table '${tableName}'`
31821
+ reason: `blocked: CREATE INDEX targets unknown table '${tableName2}'`
31250
31822
  });
31251
31823
  return;
31252
31824
  }
@@ -31279,7 +31851,7 @@ function consumeCreateIndex(node, statement, context) {
31279
31851
  }
31280
31852
  if (columns.value.length === 0) {
31281
31853
  context.unparsed.push({
31282
- location: indexName ?? `${tableName} index`,
31854
+ location: indexName ?? `${tableName2} index`,
31283
31855
  reason: "blocked: malformed CREATE INDEX without columns"
31284
31856
  });
31285
31857
  return;
@@ -31727,9 +32299,9 @@ function firstMeaningfulUnsupportedField(value, allowed) {
31727
32299
  }
31728
32300
  return null;
31729
32301
  }
31730
- function pushUnsupportedDefinition(tableName, definition, unparsed) {
32302
+ function pushUnsupportedDefinition(tableName2, definition, unparsed) {
31731
32303
  unparsed.push({
31732
- location: `${tableName} (${definition})`,
32304
+ location: `${tableName2} (${definition})`,
31733
32305
  reason: `blocked: unsupported table definition '${definition}'`
31734
32306
  });
31735
32307
  }
@@ -31855,7 +32427,7 @@ function collectBlocks(text3, unparsed) {
31855
32427
  function parseModel2(block, unparsed) {
31856
32428
  const fields = [];
31857
32429
  const pendingIndexes = [];
31858
- let tableName = block.name;
32430
+ let tableName2 = block.name;
31859
32431
  let hasTableMap = false;
31860
32432
  for (const sourceLine of block.lines) {
31861
32433
  const line = stripLineComment(sourceLine.text).trim();
@@ -31874,7 +32446,7 @@ function parseModel2(block, unparsed) {
31874
32446
  addBlocked2(unparsed, block.name, `malformed or duplicate '@@map' attribute`);
31875
32447
  continue;
31876
32448
  }
31877
- tableName = mapped;
32449
+ tableName2 = mapped;
31878
32450
  hasTableMap = true;
31879
32451
  continue;
31880
32452
  }
@@ -31900,12 +32472,12 @@ function parseModel2(block, unparsed) {
31900
32472
  }
31901
32473
  return {
31902
32474
  name: block.name,
31903
- tableName,
32475
+ tableName: tableName2,
31904
32476
  fields,
31905
32477
  pendingIndexes,
31906
32478
  pendingRelations: [],
31907
32479
  columnNames: new Map,
31908
- table: { identity: { table: tableName }, columns: [], indexes: [], foreignKeys: [] }
32480
+ table: { identity: { table: tableName2 }, columns: [], indexes: [], foreignKeys: [] }
31909
32481
  };
31910
32482
  }
31911
32483
  function parseField2(line, modelName, lineNumber, unparsed) {
@@ -32964,31 +33536,31 @@ function compareSnapshots(before, after) {
32964
33536
  const removedColumns = [];
32965
33537
  const modifiedColumns = [];
32966
33538
  const indexChanges = [];
32967
- for (const tableName of addedTables) {
32968
- const added = after.tables[tableName];
33539
+ for (const tableName2 of addedTables) {
33540
+ const added = after.tables[tableName2];
32969
33541
  if (!added)
32970
33542
  continue;
32971
33543
  for (const col of added.columns) {
32972
33544
  addedColumns.push({
32973
- table: tableName,
33545
+ table: tableName2,
32974
33546
  column: col.name,
32975
33547
  type: col.type,
32976
33548
  nullable: col.nullable
32977
33549
  });
32978
33550
  }
32979
33551
  }
32980
- for (const tableName of removedTables) {
32981
- const removed = before.tables[tableName];
33552
+ for (const tableName2 of removedTables) {
33553
+ const removed = before.tables[tableName2];
32982
33554
  if (!removed)
32983
33555
  continue;
32984
33556
  for (const col of removed.columns) {
32985
- removedColumns.push({ table: tableName, column: col.name, type: col.type });
33557
+ removedColumns.push({ table: tableName2, column: col.name, type: col.type });
32986
33558
  }
32987
33559
  }
32988
33560
  const commonTables = Array.from(afterTables).filter((t2) => beforeTables.has(t2));
32989
- for (const tableName of commonTables) {
32990
- const beforeTable = before.tables[tableName];
32991
- const afterTable = after.tables[tableName];
33561
+ for (const tableName2 of commonTables) {
33562
+ const beforeTable = before.tables[tableName2];
33563
+ const afterTable = after.tables[tableName2];
32992
33564
  if (!beforeTable || !afterTable)
32993
33565
  continue;
32994
33566
  const beforeColMap = new Map(beforeTable.columns.map((c) => [c.name, c]));
@@ -32996,7 +33568,7 @@ function compareSnapshots(before, after) {
32996
33568
  for (const [name, col] of afterColMap) {
32997
33569
  if (!beforeColMap.has(name)) {
32998
33570
  addedColumns.push({
32999
- table: tableName,
33571
+ table: tableName2,
33000
33572
  column: name,
33001
33573
  type: col.type,
33002
33574
  nullable: col.nullable
@@ -33005,7 +33577,7 @@ function compareSnapshots(before, after) {
33005
33577
  }
33006
33578
  for (const [name, col] of beforeColMap) {
33007
33579
  if (!afterColMap.has(name)) {
33008
- removedColumns.push({ table: tableName, column: name, type: col.type });
33580
+ removedColumns.push({ table: tableName2, column: name, type: col.type });
33009
33581
  }
33010
33582
  }
33011
33583
  for (const [name, afterCol] of afterColMap) {
@@ -33014,7 +33586,7 @@ function compareSnapshots(before, after) {
33014
33586
  continue;
33015
33587
  if (beforeCol.type.toLowerCase() !== afterCol.type.toLowerCase() || beforeCol.nullable !== afterCol.nullable) {
33016
33588
  modifiedColumns.push({
33017
- table: tableName,
33589
+ table: tableName2,
33018
33590
  column: name,
33019
33591
  before: { type: beforeCol.type, nullable: beforeCol.nullable },
33020
33592
  after: { type: afterCol.type, nullable: afterCol.nullable }
@@ -33025,12 +33597,12 @@ function compareSnapshots(before, after) {
33025
33597
  const afterIndexes = new Map((afterTable.indexes || []).map((i) => [i.name, i]));
33026
33598
  for (const name of afterIndexes.keys()) {
33027
33599
  if (!beforeIndexes.has(name)) {
33028
- indexChanges.push({ table: tableName, name, change: "added" });
33600
+ indexChanges.push({ table: tableName2, name, change: "added" });
33029
33601
  }
33030
33602
  }
33031
33603
  for (const name of beforeIndexes.keys()) {
33032
33604
  if (!afterIndexes.has(name)) {
33033
- indexChanges.push({ table: tableName, name, change: "removed" });
33605
+ indexChanges.push({ table: tableName2, name, change: "removed" });
33034
33606
  }
33035
33607
  }
33036
33608
  }
@@ -36148,6 +36720,81 @@ Verification artifact: ${verificationArtifactPath}`);
36148
36720
  });
36149
36721
  });
36150
36722
 
36723
+ // src/core/audit/write-gate-summary.ts
36724
+ function metadataOf(entry) {
36725
+ const metadata = entry.metadata;
36726
+ return metadata && typeof metadata === "object" ? metadata : {};
36727
+ }
36728
+ function readString(source, key) {
36729
+ const value = source[key];
36730
+ return typeof value === "string" && value.length > 0 ? value : undefined;
36731
+ }
36732
+
36733
+ class Span {
36734
+ from;
36735
+ to;
36736
+ add(ts) {
36737
+ if (this.from === undefined || ts < this.from)
36738
+ this.from = ts;
36739
+ if (this.to === undefined || ts > this.to)
36740
+ this.to = ts;
36741
+ }
36742
+ value() {
36743
+ return this.from === undefined || this.to === undefined ? null : { from: this.from, to: this.to };
36744
+ }
36745
+ }
36746
+ function summarizeWriteGate(entries) {
36747
+ const outcomes = Object.fromEntries(GATE_OUTCOMES.map((o) => [o, 0]));
36748
+ const reasons = Object.fromEntries(GATE_REASONS.map((r) => [r, 0]));
36749
+ const tierOneOutcomes = {};
36750
+ const window = new Span;
36751
+ const range = new Span;
36752
+ let total = 0;
36753
+ let tierOneTotal = 0;
36754
+ const tally = (into, key) => {
36755
+ into[key] = (into[key] ?? 0) + 1;
36756
+ };
36757
+ for (const entry of entries) {
36758
+ window.add(entry.ts);
36759
+ const metadata = metadataOf(entry);
36760
+ const tier = readString(metadata, "write_gate_tier");
36761
+ if (!tier)
36762
+ continue;
36763
+ const outcome = readString(metadata, "write_gate_outcome") ?? "unknown";
36764
+ if (tier !== "two") {
36765
+ tierOneTotal += 1;
36766
+ tally(tierOneOutcomes, outcome);
36767
+ continue;
36768
+ }
36769
+ total += 1;
36770
+ range.add(entry.ts);
36771
+ tally(outcomes, outcome);
36772
+ tally(reasons, readString(metadata, "write_gate_reason") ?? "unknown");
36773
+ }
36774
+ return {
36775
+ scanned: entries.length,
36776
+ window: window.value(),
36777
+ total,
36778
+ outcomes,
36779
+ reasons,
36780
+ tierOne: { total: tierOneTotal, outcomes: tierOneOutcomes },
36781
+ range: range.value()
36782
+ };
36783
+ }
36784
+ var GATE_OUTCOMES, GATE_REASONS;
36785
+ var init_write_gate_summary = __esm(() => {
36786
+ GATE_OUTCOMES = ["allowed", "declined", "refused"];
36787
+ GATE_REASONS = [
36788
+ "no_where",
36789
+ "ddl_destruction",
36790
+ "unparseable",
36791
+ "multiple_statements",
36792
+ "non_unique_where",
36793
+ "multi_table",
36794
+ "nested_write"
36795
+ ];
36796
+ });
36797
+
36151
36798
  // src/commands/audit.ts
36152
36799
  var exports_audit = {};
36153
36800
  __export(exports_audit, {
@@ -36333,6 +36980,53 @@ function readLineFromStdinWithStderrPrompt(prompt) {
36333
36980
  process.stdin.resume();
36334
36981
  });
36335
36982
  }
36983
+ function formatOutcomeCounts(outcomes) {
36984
+ const seen = Object.entries(outcomes).filter(([, count]) => count > 0);
36985
+ if (seen.length === 0)
36986
+ return MISSING_PLACEHOLDER;
36987
+ return seen.map(([name, count]) => `${name} ${count}`).join(", ");
36988
+ }
36989
+ function renderWriteGateSummary(summary, connections) {
36990
+ const lines = [];
36991
+ lines.push(t_vars("audit.write_gate.header", {
36992
+ conn: connections.length > 0 ? connections.join(", ") : MISSING_PLACEHOLDER
36993
+ }));
36994
+ if (summary.window) {
36995
+ lines.push(t_vars("audit.write_gate.window", {
36996
+ scanned: String(summary.scanned),
36997
+ from: summary.window.from,
36998
+ to: summary.window.to
36999
+ }));
37000
+ }
37001
+ if (summary.total === 0) {
37002
+ lines.push("");
37003
+ lines.push(summary.scanned === 0 ? t("audit.write_gate.no_entries") : t("audit.write_gate.no_decisions"));
37004
+ if (summary.tierOne.total > 0) {
37005
+ lines.push(t_vars("audit.write_gate.tier_one", {
37006
+ count: String(summary.tierOne.total),
37007
+ outcomes: formatOutcomeCounts(summary.tierOne.outcomes)
37008
+ }));
37009
+ }
37010
+ return lines.join(`
37011
+ `);
37012
+ }
37013
+ lines.push(t_vars("audit.write_gate.decisions", {
37014
+ count: String(summary.total),
37015
+ from: summary.range?.from ?? MISSING_PLACEHOLDER,
37016
+ to: summary.range?.to ?? MISSING_PLACEHOLDER
37017
+ }));
37018
+ lines.push("");
37019
+ lines.push(renderTable(Object.entries(summary.outcomes).map(([name, count]) => [name, String(count)]), ["outcome", "count"]));
37020
+ lines.push("");
37021
+ lines.push(renderTable(Object.entries(summary.reasons).map(([name, count]) => [name, String(count)]), ["reason", "count"]));
37022
+ lines.push("");
37023
+ lines.push(t_vars("audit.write_gate.tier_one", {
37024
+ count: String(summary.tierOne.total),
37025
+ outcomes: formatOutcomeCounts(summary.tierOne.outcomes)
37026
+ }));
37027
+ return lines.join(`
37028
+ `);
37029
+ }
36336
37030
  async function statAuditFile(file) {
36337
37031
  try {
36338
37032
  const s = await stat7(file);
@@ -36352,6 +37046,7 @@ var init_audit = __esm(() => {
36352
37046
  init_config_binding();
36353
37047
  init_reader2();
36354
37048
  init_integration_helper();
37049
+ init_write_gate_summary();
36355
37050
  ALLOWED_FORMATS14 = ["table", "json"];
36356
37051
  auditCommand = new Command("audit").description(t("audit.description"));
36357
37052
  auditCommand.command("tail").description(t("audit.tail.description")).option("--n <number>", `Number of recent entries to show (1..${MAX_TAIL_N})`, String(DEFAULT_TAIL_N)).option("--all", "Merge entries across all connections", false).option("--format <format>", `Output format: ${ALLOWED_FORMATS14.join(" | ")} (default: table)`, "table").option("--brief", "Trim each entry to ts/command/target/success", false).option("--no-brief", "Disable brief mode (override --for-agent default)").option("--for-agent", "Shortcut for --format json --brief", false).action(async (options, command) => {
@@ -36559,6 +37254,33 @@ var init_audit = __esm(() => {
36559
37254
  `);
36560
37255
  process.exit(0);
36561
37256
  });
37257
+ auditCommand.command("write-gate").description(t("audit.write_gate.description")).option("--all", "Summarize across all connections", false).option("--format <format>", `Output format: ${ALLOWED_FORMATS14.join(" | ")} (default: table)`, "table").option("--for-agent", "Shortcut for --format json", false).action(async (options, command) => {
37258
+ const format = options.forAgent === true ? "json" : options.format;
37259
+ validateFormat(format, ALLOWED_FORMATS14, "audit write-gate");
37260
+ const configPath = resolveConfigPath(command, options);
37261
+ const config = await configModule.read(configPath);
37262
+ if (isAuditDisabled(config))
37263
+ emitDisabledAndExit0();
37264
+ const { auditDir, auditFile, connectionName } = await resolveAuditPaths(configPath, config);
37265
+ let entries = [];
37266
+ let connections = [connectionName];
37267
+ if (options.all === true) {
37268
+ const discovered = await discoverConnections(auditDir);
37269
+ connections = discovered.map((c) => c.connection);
37270
+ for (const c of discovered) {
37271
+ for (const file of c.files)
37272
+ entries.push(...await readEntries(file));
37273
+ }
37274
+ } else {
37275
+ entries = await readEntries(auditFile, { include_rotated: true });
37276
+ }
37277
+ const summary = summarizeWriteGate(entries);
37278
+ if (format === "json") {
37279
+ console.log(JSON.stringify({ connections, summary }, null, 2));
37280
+ return;
37281
+ }
37282
+ console.log(renderWriteGateSummary(summary, connections));
37283
+ });
36562
37284
  auditCommand.command("health").description(t("audit.health.description")).option("--format <format>", `Output format: ${ALLOWED_FORMATS14.join(" | ")} (default: table)`, "table").option("--brief", "Trim to enabled / lastWrite / rotationUsage", false).option("--no-brief", "Disable brief mode (override --for-agent default)").option("--for-agent", "Shortcut for --format json --brief", false).action(async (options, command) => {
36563
37285
  const forAgent = options.forAgent === true;
36564
37286
  const format = forAgent ? "json" : options.format;
@@ -38623,10 +39345,10 @@ async function loadSchemaContext(dbcliPath, connectionName) {
38623
39345
  const loader3 = new SchemaLayeredLoader(dbcliPath, { connectionName });
38624
39346
  const { cache, index } = await loader3.initialize();
38625
39347
  const schema = {};
38626
- for (const tableName of Object.keys(index?.tables ?? {})) {
38627
- const table = await cache.getTableSchema(tableName);
39348
+ for (const tableName2 of Object.keys(index?.tables ?? {})) {
39349
+ const table = await cache.getTableSchema(tableName2);
38628
39350
  if (table) {
38629
- schema[tableName] = table;
39351
+ schema[tableName2] = table;
38630
39352
  }
38631
39353
  }
38632
39354
  return buildSchemaContext(schema);
@@ -40062,9 +40784,9 @@ var init_select_star = __esm(() => {
40062
40784
  span: findingSpan(ctx.sql, "select *"),
40063
40785
  schemaVerified: false
40064
40786
  };
40065
- const tableName = singlePhysicalTable(ctx.ast);
40066
- if (ctx.schema.available && tableName && isSoleUnqualifiedStar(ctx.ast)) {
40067
- const table = ctx.schema.getTable(tableName);
40787
+ const tableName2 = singlePhysicalTable(ctx.ast);
40788
+ if (ctx.schema.available && tableName2 && isSoleUnqualifiedStar(ctx.ast)) {
40789
+ const table = ctx.schema.getTable(tableName2);
40068
40790
  if (table && table.columns.length > 0) {
40069
40791
  if (wildcardIndexes.length === 1) {
40070
40792
  const wildcardIndex = wildcardIndexes[0];
@@ -40973,17 +41695,17 @@ function canonicalizeEvidenceReceiptCommand(command, operation = "assert") {
40973
41695
  function sha2563(value) {
40974
41696
  return `sha256:${createHash5("sha256").update(value).digest("hex")}`;
40975
41697
  }
40976
- function verdictFingerprint(verdict) {
40977
- if (!record3(verdict) || typeof verdict.pass !== "boolean" || !Array.isArray(verdict.checks) || verdict.checks.length > 1000) {
41698
+ function verdictFingerprint(verdict2) {
41699
+ if (!record3(verdict2) || typeof verdict2.pass !== "boolean" || !Array.isArray(verdict2.checks) || verdict2.checks.length > 1000) {
40978
41700
  throw new EvidenceReceiptValidationError("verdict must contain bounded pass bits");
40979
41701
  }
40980
- const bits = verdict.checks.map((check) => {
41702
+ const bits = verdict2.checks.map((check) => {
40981
41703
  if (!record3(check) || Object.keys(check).some((key) => key !== "pass") || typeof check.pass !== "boolean") {
40982
41704
  throw new EvidenceReceiptValidationError("verdict checks must contain only pass bits");
40983
41705
  }
40984
41706
  return check.pass;
40985
41707
  });
40986
- return sha2563(JSON.stringify({ pass: verdict.pass, checks: bits }));
41708
+ return sha2563(JSON.stringify({ pass: verdict2.pass, checks: bits }));
40987
41709
  }
40988
41710
  function verificationOutcomeFingerprint(status2, artifactPersisted) {
40989
41711
  if (!isVerificationStatus(status2)) {
@@ -41281,7 +42003,7 @@ var init_assert = __esm(() => {
41281
42003
  const adapter = AdapterFactory.createSqlAdapter(requireSqlConnection11(config.connection));
41282
42004
  await adapter.connect();
41283
42005
  let auditRef = null;
41284
- let verdict;
42006
+ let verdict2;
41285
42007
  try {
41286
42008
  const blacklistManager = new BlacklistManager(config);
41287
42009
  const blacklistValidator = new BlacklistValidator(blacklistManager);
@@ -41308,9 +42030,9 @@ var init_assert = __esm(() => {
41308
42030
  });
41309
42031
  checks.push(...compareAgainst(current, baseline, options.tolerance));
41310
42032
  }
41311
- verdict = { pass: checks.every((c) => c.pass), checks };
42033
+ verdict2 = { pass: checks.every((c) => c.pass), checks };
41312
42034
  auditRef = await writeAuditEntry(config, "assert", options, {
41313
- success: verdict.pass,
42035
+ success: verdict2.pass,
41314
42036
  sql: query
41315
42037
  });
41316
42038
  } finally {
@@ -41320,7 +42042,7 @@ var init_assert = __esm(() => {
41320
42042
  if (options.writeVerificationArtifact === true && verificationSubject) {
41321
42043
  try {
41322
42044
  const artifact2 = buildAssertVerificationArtifact({
41323
- verdict,
42045
+ verdict: verdict2,
41324
42046
  subject: verificationSubject,
41325
42047
  summary: options.verificationSummary,
41326
42048
  argv: process.argv,
@@ -41339,7 +42061,7 @@ var init_assert = __esm(() => {
41339
42061
  context: await buildEvidenceReceiptContext(config, process.cwd()),
41340
42062
  auditRef,
41341
42063
  verificationArtifactRef: verificationArtifactPath ? basename7(verificationArtifactPath) : null,
41342
- verdict
42064
+ verdict: verdict2
41343
42065
  });
41344
42066
  evidenceReceiptPath = await writeEvidenceReceipt(process.cwd(), options.evidenceReceipt, receipt);
41345
42067
  } catch {
@@ -41349,23 +42071,23 @@ var init_assert = __esm(() => {
41349
42071
  }
41350
42072
  if (options.format === "json") {
41351
42073
  console.log(JSON.stringify({
41352
- ...verdict,
42074
+ ...verdict2,
41353
42075
  ...verificationArtifactPath ? { verificationArtifactPath } : {},
41354
42076
  ...evidenceReceiptPath ? { evidenceReceiptPath } : {}
41355
42077
  }, null, 2));
41356
42078
  } else {
41357
- for (const c of verdict.checks) {
42079
+ for (const c of verdict2.checks) {
41358
42080
  console.log(`${c.pass ? "PASS" : "FAIL"} ${c.name} expected=${c.expected} actual=${c.actual}`);
41359
42081
  }
41360
42082
  console.log(`
41361
- Verdict: ${verdict.pass ? "PASS" : "FAIL"}`);
42083
+ Verdict: ${verdict2.pass ? "PASS" : "FAIL"}`);
41362
42084
  if (verificationArtifactPath) {
41363
42085
  console.log(`Verification artifact: ${verificationArtifactPath}`);
41364
42086
  }
41365
42087
  if (evidenceReceiptPath)
41366
42088
  console.log(`Evidence receipt: ${evidenceReceiptPath}`);
41367
42089
  }
41368
- process.exit(verdict.pass || options.fail === false ? 0 : 1);
42090
+ process.exit(verdict2.pass || options.fail === false ? 0 : 1);
41369
42091
  } catch (error) {
41370
42092
  if (error instanceof Error) {
41371
42093
  console.error(error.message);
@@ -43849,6 +44571,9 @@ function classifyInput(raw) {
43849
44571
  return { type: "meta", raw, normalized: normalized2 };
43850
44572
  }
43851
44573
  }
44574
+ if (normalized2.startsWith(COMMAND_PREFIX)) {
44575
+ return { type: "command", raw, normalized: normalized2 };
44576
+ }
43852
44577
  const firstWord = (normalized2.split(/\s+/)[0] ?? "").toUpperCase();
43853
44578
  if (sqlKeywordSet.has(firstWord)) {
43854
44579
  return { type: "sql", raw, normalized: normalized2 };
@@ -43858,7 +44583,7 @@ function classifyInput(raw) {
43858
44583
  }
43859
44584
  return { type: "command", raw, normalized: normalized2 };
43860
44585
  }
43861
- var META_PREFIX = ".", SQL_TERMINATOR = ";", sqlKeywordSet, metaCommandNames;
44586
+ var META_PREFIX = ".", COMMAND_PREFIX = "\\", SQL_TERMINATOR = ";", sqlKeywordSet, metaCommandNames;
43862
44587
  var init_input_classifier = __esm(() => {
43863
44588
  init_types11();
43864
44589
  sqlKeywordSet = new Set(SQL_KEYWORDS_FOR_DETECTION);
@@ -43882,6 +44607,10 @@ class MultilineBuffer {
43882
44607
  isActive() {
43883
44608
  return this.lines.length > 0;
43884
44609
  }
44610
+ isInsideLiteral() {
44611
+ return insideLiteral(this.lines.join(`
44612
+ `));
44613
+ }
43885
44614
  getPartial() {
43886
44615
  return this.lines.join(`
43887
44616
  `);
@@ -43891,9 +44620,17 @@ class MultilineBuffer {
43891
44620
  }
43892
44621
  }
43893
44622
  function hasUnquotedSemicolon(sql) {
44623
+ return scan(sql).semicolon;
44624
+ }
44625
+ function insideLiteral(sql) {
44626
+ const { inSingleQuote, inDoubleQuote } = scan(sql);
44627
+ return inSingleQuote || inDoubleQuote;
44628
+ }
44629
+ function scan(sql) {
43894
44630
  let inSingleQuote = false;
43895
44631
  let inDoubleQuote = false;
43896
44632
  let escaped = false;
44633
+ let semicolon = false;
43897
44634
  for (let i = 0;i < sql.length; i++) {
43898
44635
  const ch = sql[i];
43899
44636
  if (escaped) {
@@ -43913,10 +44650,10 @@ function hasUnquotedSemicolon(sql) {
43913
44650
  continue;
43914
44651
  }
43915
44652
  if (ch === ";" && !inSingleQuote && !inDoubleQuote) {
43916
- return true;
44653
+ semicolon = true;
43917
44654
  }
43918
44655
  }
43919
- return false;
44656
+ return { semicolon, inSingleQuote, inDoubleQuote };
43920
44657
  }
43921
44658
 
43922
44659
  // src/core/repl/meta-commands.ts
@@ -43960,7 +44697,8 @@ function helpText() {
43960
44697
  "",
43961
44698
  import_picocolors2.default.bold("Usage:"),
43962
44699
  ` SQL statements Execute directly (end with ${import_picocolors2.default.cyan(";")})`,
43963
- ` dbcli commands Run without ${import_picocolors2.default.dim("dbcli")} prefix (e.g. ${import_picocolors2.default.cyan("schema users")})`,
44700
+ ` dbcli commands Run without the ${import_picocolors2.default.dim("dbcli")} prefix (e.g. ${import_picocolors2.default.cyan("schema users")})`,
44701
+ ` ${import_picocolors2.default.cyan("\\<command>")} Force the dbcli subcommand \u2014 required for ${import_picocolors2.default.dim("delete")}, ${import_picocolors2.default.dim("update")}, ${import_picocolors2.default.dim("insert")} and other SQL keywords (e.g. ${import_picocolors2.default.cyan("\\delete users --where id=1")})`,
43964
44702
  ` Ctrl+D Exit`
43965
44703
  ];
43966
44704
  return lines.join(`
@@ -44064,24 +44802,26 @@ function splitRespectingQuotes(input) {
44064
44802
  let current = "";
44065
44803
  let inDoubleQuote = false;
44066
44804
  let inSingleQuote = false;
44805
+ let quoted = false;
44067
44806
  for (let i = 0;i < input.length; i++) {
44068
44807
  const ch = input[i];
44069
44808
  if (ch === '"' && !inSingleQuote) {
44070
44809
  inDoubleQuote = !inDoubleQuote;
44071
- current += ch;
44810
+ quoted = true;
44072
44811
  } else if (ch === "'" && !inDoubleQuote) {
44073
44812
  inSingleQuote = !inSingleQuote;
44074
- current += ch;
44813
+ quoted = true;
44075
44814
  } else if (ch === " " && !inDoubleQuote && !inSingleQuote) {
44076
- if (current.length > 0) {
44815
+ if (current.length > 0 || quoted) {
44077
44816
  tokens.push(current);
44078
44817
  current = "";
44818
+ quoted = false;
44079
44819
  }
44080
44820
  } else {
44081
44821
  current += ch;
44082
44822
  }
44083
44823
  }
44084
- if (current.length > 0) {
44824
+ if (current.length > 0 || quoted) {
44085
44825
  tokens.push(current);
44086
44826
  }
44087
44827
  return tokens;
@@ -44147,14 +44887,16 @@ var init_history_manager = () => {};
44147
44887
  class ReplEngine {
44148
44888
  adapter;
44149
44889
  context;
44890
+ writeGate;
44150
44891
  state;
44151
44892
  buffer;
44152
44893
  history;
44153
44894
  formatter;
44154
44895
  config;
44155
- constructor(adapter, context, historyPath, config = null) {
44896
+ constructor(adapter, context, historyPath, config = null, writeGate = null) {
44156
44897
  this.adapter = adapter;
44157
44898
  this.context = context;
44899
+ this.writeGate = writeGate;
44158
44900
  this.state = { format: "table", timing: false, connected: true, noLimit: false };
44159
44901
  this.buffer = new MultilineBuffer;
44160
44902
  this.history = new HistoryManager(historyPath);
@@ -44167,6 +44909,9 @@ class ReplEngine {
44167
44909
  isMultiline() {
44168
44910
  return this.buffer.isActive();
44169
44911
  }
44912
+ cancelMultiline() {
44913
+ this.buffer.reset();
44914
+ }
44170
44915
  getHistory() {
44171
44916
  return this.history.getAll();
44172
44917
  }
@@ -44175,6 +44920,13 @@ class ReplEngine {
44175
44920
  }
44176
44921
  async processInput(line) {
44177
44922
  if (this.buffer.isActive()) {
44923
+ const classified2 = classifyInput(line);
44924
+ if (classified2.type === "meta" && !this.buffer.isInsideLiteral()) {
44925
+ const result = this.handleMeta(classified2.normalized);
44926
+ if (result.action === "quit" || result.action === "clear")
44927
+ this.buffer.reset();
44928
+ return result;
44929
+ }
44178
44930
  const bufResult = this.buffer.append(line);
44179
44931
  if (bufResult.complete) {
44180
44932
  this.history.add(bufResult.sql);
@@ -44211,14 +44963,27 @@ class ReplEngine {
44211
44963
  this.history.add(bufResult.sql);
44212
44964
  return this.executeSql(bufResult.sql);
44213
44965
  }
44214
- return { action: "multiline" };
44966
+ const hint = this.subcommandHint(input);
44967
+ return hint ? { action: "multiline", output: hint } : { action: "multiline" };
44968
+ }
44969
+ subcommandHint(input) {
44970
+ const words = input.trim().split(/\s+/);
44971
+ const firstWord = (words[0] ?? "").toLowerCase();
44972
+ if (!this.context.commandNames.includes(firstWord))
44973
+ return;
44974
+ if (!/\s--[a-z]/.test(input))
44975
+ return;
44976
+ if (STATEMENT_CONTINUATION.has((words[1] ?? "").toUpperCase()))
44977
+ return;
44978
+ return import_picocolors3.default.dim(t_vars("shell.subcommand_prefix_hint", { command: `${COMMAND_PREFIX}${firstWord}` }));
44215
44979
  }
44216
44980
  async handleCommand(input) {
44217
- const parsed = parseCommandLine(input);
44981
+ const invocation = input.startsWith(COMMAND_PREFIX) ? input.slice(COMMAND_PREFIX.length) : input;
44982
+ const parsed = parseCommandLine(invocation);
44218
44983
  if (!isKnownCommand(parsed.command, this.context.commandNames)) {
44219
44984
  if (this.context.system === "redis") {
44220
44985
  this.history.add(input);
44221
- return this.executeSql(input);
44986
+ return this.executeSql(invocation);
44222
44987
  }
44223
44988
  return {
44224
44989
  action: "continue",
@@ -44228,11 +44993,11 @@ class ReplEngine {
44228
44993
  this.history.add(input);
44229
44994
  try {
44230
44995
  const cliPath = new URL("../../cli.ts", import.meta.url).pathname;
44231
- const argv = [parsed.command, "--config", this.context.configPath, ...parsed.args];
44996
+ const argv = ["--config", this.context.configPath, parsed.command, ...parsed.args];
44232
44997
  const proc = Bun.spawn(["bun", "run", cliPath, ...argv], {
44233
44998
  stdout: "pipe",
44234
44999
  stderr: "pipe",
44235
- env: { ...process.env }
45000
+ env: { ...process.env, DBCLI_SHELL_SUBCOMMAND: "1" }
44236
45001
  });
44237
45002
  const [stdoutBuf, stderrBuf] = await Promise.all([
44238
45003
  new Response(proc.stdout).text(),
@@ -44288,6 +45053,11 @@ class ReplEngine {
44288
45053
  };
44289
45054
  }
44290
45055
  }
45056
+ if (this.writeGate && !await this.writeGate(sql))
45057
+ return { action: "continue" };
45058
+ return this.runStatement(sql, blacklistValidator, referencedTables);
45059
+ }
45060
+ async runStatement(sql, blacklistValidator, referencedTables) {
44291
45061
  const startTime = Date.now();
44292
45062
  try {
44293
45063
  const result = await this.adapter.execute(sql, undefined, {
@@ -44323,7 +45093,7 @@ class ReplEngine {
44323
45093
  await this.adapter.connect();
44324
45094
  this.state = { ...this.state, connected: true };
44325
45095
  console.error(import_picocolors3.default.green(t("shell.error_reconnect_success")));
44326
- return this.executeSql(sql);
45096
+ return this.runStatement(sql, blacklistValidator, referencedTables);
44327
45097
  } catch (reconnectError) {
44328
45098
  return {
44329
45099
  action: "continue",
@@ -44354,7 +45124,7 @@ class ReplEngine {
44354
45124
  return isTransportFailure(error);
44355
45125
  }
44356
45126
  }
44357
- var import_picocolors3;
45127
+ var import_picocolors3, STATEMENT_CONTINUATION;
44358
45128
  var init_repl_engine = __esm(() => {
44359
45129
  init_connection_error_message();
44360
45130
  init_input_classifier();
@@ -44368,6 +45138,15 @@ var init_repl_engine = __esm(() => {
44368
45138
  init_blacklist_validator();
44369
45139
  init_blacklist();
44370
45140
  import_picocolors3 = __toESM(require_picocolors(), 1);
45141
+ STATEMENT_CONTINUATION = new Set([
45142
+ "FROM",
45143
+ "INTO",
45144
+ "SET",
45145
+ "TABLE",
45146
+ "INDEX",
45147
+ "VIEW",
45148
+ "DATABASE"
45149
+ ]);
44371
45150
  });
44372
45151
 
44373
45152
  // src/core/repl/completer.ts
@@ -44399,9 +45178,9 @@ function createCompleter(ctx) {
44399
45178
  const sqlHits = matchWithSuffix([...SQL_KEYWORDS_FOR_COMPLETION], lastWord);
44400
45179
  const tableHits = matchWithSuffix(allTableNames, lastWord);
44401
45180
  const colHits = isColumnPosition(upperLine) ? (() => {
44402
- const tableName = extractTableFromLine(trimmed, allTableNames);
44403
- if (tableName && ctx.columnsByTable[tableName]) {
44404
- return matchWithSuffix([...ctx.columnsByTable[tableName]], lastWord);
45181
+ const tableName2 = extractTableFromLine(trimmed, allTableNames);
45182
+ if (tableName2 && ctx.columnsByTable[tableName2]) {
45183
+ return matchWithSuffix([...ctx.columnsByTable[tableName2]], lastWord);
44405
45184
  }
44406
45185
  return matchWithSuffix(uniqueColumns, lastWord);
44407
45186
  })() : [[], lastWord];
@@ -44410,6 +45189,11 @@ function createCompleter(ctx) {
44410
45189
  }
44411
45190
  const words = trimmed.split(/\s+/);
44412
45191
  if (words.length === 1) {
45192
+ if (lastWord.startsWith(COMMAND_PREFIX)) {
45193
+ const bare = lastWord.slice(COMMAND_PREFIX.length);
45194
+ const [hits] = matchWithSuffix([...ctx.commandNames], bare);
45195
+ return [hits.map((hit) => `${COMMAND_PREFIX}${hit}`), lastWord];
45196
+ }
44413
45197
  const cmdHits = matchWithSuffix([...ctx.commandNames], lastWord);
44414
45198
  const sqlHits = matchWithSuffix([...SQL_KEYWORDS_FOR_COMPLETION], lastWord);
44415
45199
  return [[...cmdHits[0], ...sqlHits[0]], lastWord];
@@ -44463,6 +45247,7 @@ function matchWithSuffix(candidates, prefix) {
44463
45247
  var REDIS_COMMANDS, TABLE_POSITION_KEYWORDS, COMMANDS_TAKING_TABLE_ARG;
44464
45248
  var init_completer = __esm(() => {
44465
45249
  init_types11();
45250
+ init_input_classifier();
44466
45251
  init_command_metadata();
44467
45252
  REDIS_COMMANDS = Object.keys(REDIS_COMMAND_TABLE);
44468
45253
  TABLE_POSITION_KEYWORDS = new Set([
@@ -44760,6 +45545,94 @@ var init_command_registry = __esm(() => {
44760
45545
  REPL_DENYLIST = new Set(["shell"]);
44761
45546
  });
44762
45547
 
45548
+ // src/commands/shell-prompt-asker.ts
45549
+ var exports_shell_prompt_asker = {};
45550
+ __export(exports_shell_prompt_asker, {
45551
+ createPromptAsker: () => createPromptAsker
45552
+ });
45553
+ function createPromptAsker(getInterface) {
45554
+ let pending = null;
45555
+ return {
45556
+ async ask(question) {
45557
+ const rl = getInterface();
45558
+ if (!rl)
45559
+ throw new Error("Write gate asked for confirmation before the shell started");
45560
+ return new Promise((resolve10) => {
45561
+ const controller = new AbortController;
45562
+ const settle = (answer) => {
45563
+ pending = null;
45564
+ rl.pause();
45565
+ resolve10(answer);
45566
+ };
45567
+ pending = {
45568
+ abort: () => {
45569
+ controller.abort();
45570
+ settle(null);
45571
+ }
45572
+ };
45573
+ try {
45574
+ rl.question(`${question}: `, { signal: controller.signal }, settle);
45575
+ } catch (error) {
45576
+ pending = null;
45577
+ throw error;
45578
+ }
45579
+ });
45580
+ },
45581
+ cancel() {
45582
+ if (!pending)
45583
+ return false;
45584
+ pending.abort();
45585
+ return true;
45586
+ }
45587
+ };
45588
+ }
45589
+
45590
+ // src/commands/shell-write-gate.ts
45591
+ var exports_shell_write_gate = {};
45592
+ __export(exports_shell_write_gate, {
45593
+ createShellWriteGate: () => createShellWriteGate
45594
+ });
45595
+ function createShellWriteGate(options) {
45596
+ const record4 = options.record ?? ((decision) => recordGateDecision({
45597
+ config: options.config,
45598
+ command: "shell",
45599
+ options: { config: options.configPath },
45600
+ verdict: decision.verdict,
45601
+ outcome: decision.outcome,
45602
+ tier: decision.tier,
45603
+ sql: decision.sql
45604
+ }));
45605
+ return async (sql) => {
45606
+ const verdict2 = await classifySqlWriteGate(sql, { dialect: options.dialect });
45607
+ if (verdict2.tier !== "two")
45608
+ return true;
45609
+ const decide5 = (outcome) => record4({ verdict: verdict2, outcome, tier: "two", sql });
45610
+ try {
45611
+ const proceed = await enforceWriteGate({
45612
+ verdict: verdict2,
45613
+ statement: sql,
45614
+ operation: verdict2.operation ?? "WRITE",
45615
+ human: { isTTY: process.stdin.isTTY === true },
45616
+ ...options.ask && { ask: options.ask }
45617
+ });
45618
+ await decide5(proceed ? "allowed" : "declined");
45619
+ return proceed;
45620
+ } catch (error) {
45621
+ if (!(error instanceof WriteGateRefusal))
45622
+ throw error;
45623
+ await decide5("refused");
45624
+ process.stderr.write(`${error.message}
45625
+ `);
45626
+ return false;
45627
+ }
45628
+ };
45629
+ }
45630
+ var init_shell_write_gate = __esm(() => {
45631
+ init_write_gate();
45632
+ init_write_gate_prompt();
45633
+ init_write_gate_guard();
45634
+ });
45635
+
44763
45636
  // src/commands/shell.ts
44764
45637
  var exports_shell = {};
44765
45638
  __export(exports_shell, {
@@ -44866,7 +45739,18 @@ async function runShell(options, configPath) {
44866
45739
  columnsByTable,
44867
45740
  commandNames
44868
45741
  };
44869
- const engine = new ReplEngine(adapter, context, HISTORY_PATH, config);
45742
+ let replInterface = null;
45743
+ const { createPromptAsker: createPromptAsker2 } = await Promise.resolve().then(() => exports_shell_prompt_asker);
45744
+ const asker = createPromptAsker2(() => replInterface);
45745
+ const { toSqlDialect: toSqlDialect2 } = await Promise.resolve().then(() => (init_permission_guard(), exports_permission_guard));
45746
+ const dialect = toSqlDialect2(config.connection.system);
45747
+ const writeGate = dialect ? (await Promise.resolve().then(() => (init_shell_write_gate(), exports_shell_write_gate))).createShellWriteGate({
45748
+ config,
45749
+ configPath,
45750
+ dialect,
45751
+ ask: asker.ask
45752
+ }) : null;
45753
+ const engine = new ReplEngine(adapter, context, HISTORY_PATH, config, writeGate);
44870
45754
  const complete = createCompleter(context);
44871
45755
  console.error(import_picocolors5.default.bold(t_vars("shell.welcome", {
44872
45756
  system: config.connection.system,
@@ -44900,10 +45784,22 @@ async function runShell(options, configPath) {
44900
45784
  completer: (line) => complete(line),
44901
45785
  terminal: process.stdin.isTTY ?? false
44902
45786
  });
45787
+ replInterface = rl;
44903
45788
  const continuationPrompt = import_picocolors5.default.dim(t("shell.continuation_prompt") + "> ");
44904
45789
  rl.prompt();
44905
- rl.on("line", async (line) => {
44906
- const result = await engine.processInput(line);
45790
+ const handleLine = async (line) => {
45791
+ rl.pause();
45792
+ let result;
45793
+ try {
45794
+ result = await engine.processInput(line);
45795
+ } catch (error) {
45796
+ console.error(import_picocolors5.default.red(t_vars("shell.error_sql_failed", { message: error.message })));
45797
+ rl.setPrompt(import_picocolors5.default.cyan(t("shell.prompt") + "> "));
45798
+ rl.prompt();
45799
+ return;
45800
+ } finally {
45801
+ rl.resume();
45802
+ }
44907
45803
  switch (result.action) {
44908
45804
  case "quit":
44909
45805
  if (result.output)
@@ -44914,16 +45810,21 @@ async function runShell(options, configPath) {
44914
45810
  console.clear();
44915
45811
  break;
44916
45812
  case "multiline":
44917
- rl.setPrompt(continuationPrompt);
45813
+ if (result.output)
45814
+ console.error(result.output);
44918
45815
  break;
44919
45816
  case "continue":
44920
45817
  if (result.output) {
44921
45818
  console.log(result.output);
44922
45819
  }
44923
- rl.setPrompt(import_picocolors5.default.cyan(t("shell.prompt") + "> "));
44924
45820
  break;
44925
45821
  }
45822
+ rl.setPrompt(engine.isMultiline() ? continuationPrompt : import_picocolors5.default.cyan(t("shell.prompt") + "> "));
44926
45823
  rl.prompt();
45824
+ };
45825
+ let pending = Promise.resolve();
45826
+ rl.on("line", (line) => {
45827
+ pending = pending.then(() => handleLine(line));
44927
45828
  });
44928
45829
  rl.on("close", async () => {
44929
45830
  console.error(import_picocolors5.default.dim(t("shell.goodbye")));
@@ -44932,7 +45833,10 @@ async function runShell(options, configPath) {
44932
45833
  process.exit(0);
44933
45834
  });
44934
45835
  rl.on("SIGINT", () => {
45836
+ if (asker.cancel())
45837
+ return;
44935
45838
  if (engine.isMultiline()) {
45839
+ engine.cancelMultiline();
44936
45840
  console.error(import_picocolors5.default.dim(t("shell.multiline_cancelled")));
44937
45841
  rl.setPrompt(import_picocolors5.default.cyan(t("shell.prompt") + "> "));
44938
45842
  }
@@ -44948,7 +45852,10 @@ async function runBatchSession(engine, input) {
44948
45852
  continue;
44949
45853
  const result = await engine.processInput(line);
44950
45854
  if (result.output) {
44951
- console.log(result.output);
45855
+ if (result.action === "multiline")
45856
+ console.error(result.output);
45857
+ else
45858
+ console.log(result.output);
44952
45859
  }
44953
45860
  if (result.action === "quit") {
44954
45861
  return;