@blogic-cz/agent-tools 1.5.2 → 1.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +4 -4
  2. package/dist/audit-tool/main.d.ts +3 -0
  3. package/dist/audit-tool/main.d.ts.map +1 -0
  4. package/dist/az-tool/main.d.ts +3 -0
  5. package/dist/az-tool/main.d.ts.map +1 -0
  6. package/dist/azdo-tool/main.d.ts +3 -0
  7. package/dist/azdo-tool/main.d.ts.map +1 -0
  8. package/dist/credential-guard/index.d.ts.map +1 -1
  9. package/dist/db-tool/main.d.ts +3 -0
  10. package/dist/db-tool/main.d.ts.map +1 -0
  11. package/dist/gh-tool/api.d.ts.map +1 -1
  12. package/dist/gh-tool/gist.d.ts.map +1 -1
  13. package/dist/gh-tool/main.d.ts +3 -0
  14. package/dist/gh-tool/main.d.ts.map +1 -0
  15. package/dist/gh-tool/pr/core.d.ts.map +1 -1
  16. package/dist/gh-tool/service.d.ts.map +1 -1
  17. package/dist/gh-tool/text-input.d.ts +4 -0
  18. package/dist/gh-tool/text-input.d.ts.map +1 -1
  19. package/dist/gh-tool/workflow.d.ts.map +1 -1
  20. package/dist/k8s-tool/main.d.ts +3 -0
  21. package/dist/k8s-tool/main.d.ts.map +1 -0
  22. package/dist/logs-tool/index.d.ts +1 -19
  23. package/dist/logs-tool/index.d.ts.map +1 -1
  24. package/dist/logs-tool/main.d.ts +21 -0
  25. package/dist/logs-tool/main.d.ts.map +1 -0
  26. package/dist/observability-tool/main.d.ts +3 -0
  27. package/dist/observability-tool/main.d.ts.map +1 -0
  28. package/dist/session-tool/index.d.ts +1 -11
  29. package/dist/session-tool/index.d.ts.map +1 -1
  30. package/dist/session-tool/main.d.ts +13 -0
  31. package/dist/session-tool/main.d.ts.map +1 -0
  32. package/dist/shared/effect-version.d.ts +4 -0
  33. package/dist/shared/effect-version.d.ts.map +1 -0
  34. package/package.json +2 -2
  35. package/src/audit-tool/index.ts +3 -109
  36. package/src/audit-tool/main.ts +111 -0
  37. package/src/az-tool/index.ts +3 -151
  38. package/src/az-tool/main.ts +153 -0
  39. package/src/azdo-tool/index.ts +3 -220
  40. package/src/azdo-tool/main.ts +222 -0
  41. package/src/credential-guard/index.ts +102 -4
  42. package/src/db-tool/index.ts +3 -260
  43. package/src/db-tool/main.ts +262 -0
  44. package/src/gh-tool/api.ts +21 -1
  45. package/src/gh-tool/gist.ts +7 -1
  46. package/src/gh-tool/index.ts +3 -256
  47. package/src/gh-tool/main.ts +258 -0
  48. package/src/gh-tool/pr/core.ts +52 -2
  49. package/src/gh-tool/service.ts +9 -5
  50. package/src/gh-tool/text-input.ts +63 -4
  51. package/src/gh-tool/workflow.ts +1 -0
  52. package/src/k8s-tool/index.ts +3 -487
  53. package/src/k8s-tool/main.ts +489 -0
  54. package/src/logs-tool/index.ts +3 -247
  55. package/src/logs-tool/main.ts +249 -0
  56. package/src/observability-tool/index.ts +3 -34
  57. package/src/observability-tool/main.ts +36 -0
  58. package/src/session-tool/index.ts +3 -337
  59. package/src/session-tool/main.ts +339 -0
  60. package/src/shared/effect-version.ts +32 -0
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env bun
2
+ import { Command, Flag } from "effect/unstable/cli";
3
+ import { BunRuntime, BunServices } from "@effect/platform-bun";
4
+ import { Effect, Layer, Option } from "effect";
5
+
6
+ import {
7
+ makeSchemaCommand,
8
+ formatAny,
9
+ formatOption,
10
+ formatOutput,
11
+ logText,
12
+ renderCauseToStderr,
13
+ VERSION,
14
+ } from "#shared";
15
+ import { AuditServiceLayer, withAudit } from "#shared/audit";
16
+ import {
17
+ findFailedJobs,
18
+ getBuildJobSummary,
19
+ getBuildLogContent,
20
+ getBuildLogs,
21
+ getBuildTimeline,
22
+ } from "./build";
23
+ import { AzdoService, AzdoServiceLayer } from "./service";
24
+ import { ConfigServiceLayer } from "#config";
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Common flags shared across build subcommands
28
+ // ---------------------------------------------------------------------------
29
+
30
+ const commonBuildFlags = {
31
+ format: formatOption,
32
+ profile: Flag.optional(Flag.String("profile")).pipe(
33
+ Flag.withDescription("Azure DevOps profile name (from agent-tools config)"),
34
+ ),
35
+ };
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Build subcommands
39
+ // ---------------------------------------------------------------------------
40
+
41
+ const buildTimelineCommand = Command.make(
42
+ "timeline",
43
+ {
44
+ ...commonBuildFlags,
45
+ buildId: Flag.Int("build-id").pipe(Flag.withDescription("Build ID")),
46
+ },
47
+ ({ buildId, format, profile: _profile }) =>
48
+ Effect.gen(function* () {
49
+ const result = yield* getBuildTimeline(buildId);
50
+ yield* logText(formatAny(result, format));
51
+ }),
52
+ ).pipe(Command.withDescription("Get build timeline with all records (jobs, stages, tasks)"));
53
+
54
+ const buildFailedJobsCommand = Command.make(
55
+ "failed-jobs",
56
+ {
57
+ ...commonBuildFlags,
58
+ buildId: Flag.Int("build-id").pipe(Flag.withDescription("Build ID")),
59
+ },
60
+ ({ buildId, format, profile: _profile }) =>
61
+ Effect.gen(function* () {
62
+ const failedJobs = yield* findFailedJobs(buildId);
63
+ yield* logText(formatAny({ buildId, failedJobs }, format));
64
+ }),
65
+ ).pipe(Command.withDescription("Find failed or canceled jobs in a build"));
66
+
67
+ const buildLogsCommand = Command.make(
68
+ "logs",
69
+ {
70
+ ...commonBuildFlags,
71
+ buildId: Flag.Int("build-id").pipe(Flag.withDescription("Build ID")),
72
+ },
73
+ ({ buildId, format, profile: _profile }) =>
74
+ Effect.gen(function* () {
75
+ const result = yield* getBuildLogs(buildId);
76
+ yield* logText(formatAny(result, format));
77
+ }),
78
+ ).pipe(Command.withDescription("Get list of build logs"));
79
+
80
+ const buildLogContentCommand = Command.make(
81
+ "log-content",
82
+ {
83
+ ...commonBuildFlags,
84
+ buildId: Flag.Int("build-id").pipe(Flag.withDescription("Build ID")),
85
+ logId: Flag.Int("log-id").pipe(Flag.withDescription("Log ID")),
86
+ },
87
+ ({ buildId, format, logId, profile: _profile }) =>
88
+ Effect.gen(function* () {
89
+ const content = yield* getBuildLogContent(buildId, logId);
90
+ yield* logText(formatAny({ buildId, logId, content }, format));
91
+ }),
92
+ ).pipe(Command.withDescription("Get specific log content by log ID"));
93
+
94
+ const buildSummaryCommand = Command.make(
95
+ "summary",
96
+ {
97
+ ...commonBuildFlags,
98
+ buildId: Flag.Int("build-id").pipe(Flag.withDescription("Build ID")),
99
+ },
100
+ ({ buildId, format, profile: _profile }) =>
101
+ Effect.gen(function* () {
102
+ const summary = yield* getBuildJobSummary(buildId);
103
+ yield* logText(formatAny({ buildId, summary }, format));
104
+ }),
105
+ ).pipe(Command.withDescription("Get job summaries with duration and status information"));
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Build parent command
109
+ // ---------------------------------------------------------------------------
110
+
111
+ const buildCommand = Command.make("build", {}).pipe(
112
+ Command.withDescription("Build helpers for Azure DevOps pipelines"),
113
+ Command.withSubcommands([
114
+ buildTimelineCommand,
115
+ buildFailedJobsCommand,
116
+ buildLogsCommand,
117
+ buildLogContentCommand,
118
+ buildSummaryCommand,
119
+ ]),
120
+ );
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Raw command subcommand (preserves existing --cmd behavior)
124
+ // ---------------------------------------------------------------------------
125
+
126
+ const cmdCommand = Command.make(
127
+ "cmd",
128
+ {
129
+ profile: Flag.optional(Flag.String("profile")).pipe(
130
+ Flag.withDescription("Azure DevOps profile name (from agent-tools config)"),
131
+ ),
132
+ project: Flag.optional(Flag.String("project")).pipe(
133
+ Flag.withDescription("Azure DevOps project name (overrides config default)"),
134
+ ),
135
+ cmd: Flag.String("cmd").pipe(Flag.withDescription("az command (without 'az' prefix)")),
136
+ dryRun: Flag.Boolean("dry-run").pipe(
137
+ Flag.withDescription("Show command without executing"),
138
+ Flag.withDefault(false),
139
+ ),
140
+ format: formatOption,
141
+ },
142
+ ({ profile: _profile, project, cmd, dryRun, format }) =>
143
+ Effect.gen(function* () {
144
+ const projectName = project ? Option.getOrUndefined(project) : undefined;
145
+
146
+ if (dryRun) {
147
+ const fullCommand = `az ${cmd}`;
148
+ yield* logText(`[DRY-RUN] Would execute: ${fullCommand}`);
149
+ return;
150
+ }
151
+
152
+ const az = yield* AzdoService;
153
+
154
+ const startTime = Date.now();
155
+ const output = yield* az.runCommand(cmd, projectName);
156
+ const executionTimeMs = Date.now() - startTime;
157
+
158
+ yield* logText(
159
+ formatOutput(
160
+ {
161
+ success: true,
162
+ data: output,
163
+ executionTimeMs,
164
+ },
165
+ format,
166
+ ),
167
+ );
168
+ }),
169
+ ).pipe(
170
+ Command.withDescription(
171
+ `Execute raw az CLI commands directly.
172
+
173
+ EXAMPLES:
174
+ agent-tools-azdo cmd --cmd "pipelines list"
175
+ agent-tools-azdo cmd --cmd "repos list" --project my-project
176
+ agent-tools-azdo cmd --cmd "pipelines runs list --output json"`,
177
+ ),
178
+ );
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Main command with subcommands
182
+ // ---------------------------------------------------------------------------
183
+
184
+ const commandsCommand = makeSchemaCommand(() => mainCommand);
185
+
186
+ const mainCommand = Command.make("azdo-tool", {}).pipe(
187
+ Command.withDescription(
188
+ `Azure DevOps Tool for Coding Agents (READ-ONLY)
189
+
190
+ For Azure platform (PaaS) resources — vm, webapp, storage, aks, keyvault — use az-tool instead.
191
+
192
+ Typed build subcommands:
193
+ azdo-tool build timeline --build-id 123
194
+ azdo-tool build failed-jobs --build-id 123
195
+ azdo-tool build logs --build-id 123
196
+ azdo-tool build log-content --build-id 123 --log-id 45
197
+ azdo-tool build summary --build-id 123
198
+
199
+ Raw az wrapper:
200
+ azdo-tool cmd --cmd "pipelines list"`,
201
+ ),
202
+ Command.withSubcommands([buildCommand, cmdCommand, commandsCommand]),
203
+ );
204
+
205
+ const cli = Command.run(mainCommand, {
206
+ version: VERSION,
207
+ });
208
+
209
+ const MainLayer = AzdoServiceLayer.pipe(
210
+ Layer.provideMerge(ConfigServiceLayer),
211
+ Layer.provideMerge(BunServices.layer),
212
+ Layer.provideMerge(AuditServiceLayer),
213
+ );
214
+
215
+ const program = withAudit("azdo", cli).pipe(
216
+ Effect.provide(MainLayer),
217
+ Effect.tapCause(renderCauseToStderr),
218
+ );
219
+
220
+ BunRuntime.runMain(program, {
221
+ disableErrorReporting: true,
222
+ });
@@ -361,6 +361,15 @@ function hasArgumentBraceExpansion(text: string): boolean {
361
361
  }
362
362
 
363
363
  function hasCommandArgumentBraceExpansion(argv: string[]): boolean {
364
+ // LogQL is data for this wrapper; its quoted label selectors are never shell-evaluated.
365
+ if (
366
+ argv[0]?.split("/").at(-1) === "bun" &&
367
+ argv[1] === "run" &&
368
+ argv[2] === "observability-tool" &&
369
+ argv[3] === "logs" &&
370
+ argv[4] === "query"
371
+ )
372
+ return false;
364
373
  return (argv[0]?.split("/").at(-1) === "awk" && !isAwkExecution(argv)) ||
365
374
  isJqObjectConstruction(argv)
366
375
  ? false
@@ -492,9 +501,7 @@ function hasStaticEnvironmentRead(argv: string[], allowedNames: Set<string>): bo
492
501
  }
493
502
  if (isPassiveTextCommand(argv)) return false;
494
503
  // Any command may re-parse an argument as shell code (trap, find -exec, awk system()).
495
- return (
496
- hasArgumentBraceExpansion(argv.slice(1).join(" ")) || mentionsEnvironmentRead(argv.join(" "))
497
- );
504
+ return hasCommandArgumentBraceExpansion(argv) || mentionsEnvironmentRead(argv.join(" "));
498
505
  }
499
506
 
500
507
  function isStaticHerdrPrompt(command: string): boolean {
@@ -626,7 +633,98 @@ function hasSensitiveFileRead(command: string): boolean {
626
633
  if (inspectedArgv === null) return false;
627
634
  const inspected = unwrapStaticCommand(inspectedArgv);
628
635
  const name = inspected[0]?.split("/").at(-1) ?? "";
629
- return readers.test(name) && inspected.slice(1).some(isSensitivePath);
636
+ if (!readers.test(name)) return false;
637
+ if (name === "rg" || name === "grep") {
638
+ const args = inspected.slice(1);
639
+ const paths: string[] = [];
640
+ let hasPattern = false;
641
+ let options = true;
642
+ let filesOnly = false;
643
+ for (let i = 0; i < args.length; i++) {
644
+ const arg = args[i] ?? "";
645
+ if (options && arg === "--") {
646
+ options = false;
647
+ continue;
648
+ }
649
+ if (
650
+ options &&
651
+ (arg === "--pre" ||
652
+ arg.startsWith("--pre=") ||
653
+ arg === "--hostname-bin" ||
654
+ arg.startsWith("--hostname-bin="))
655
+ )
656
+ return true;
657
+ if (options && (arg.startsWith("--file=") || /^-f.+/.test(arg))) {
658
+ paths.push(arg.startsWith("--file=") ? arg.slice(7) : arg.slice(2));
659
+ hasPattern = true;
660
+ continue;
661
+ }
662
+ if (options && (arg.startsWith("--regexp=") || /^-e.+/.test(arg))) {
663
+ hasPattern = true;
664
+ continue;
665
+ }
666
+ if (
667
+ options &&
668
+ (arg.startsWith("--glob=") ||
669
+ arg.startsWith("--iglob=") ||
670
+ arg.startsWith("--include=") ||
671
+ /^-g.+/.test(arg))
672
+ ) {
673
+ const glob = arg.startsWith("--iglob=")
674
+ ? arg.slice(8)
675
+ : arg.startsWith("--include=")
676
+ ? arg.slice(10)
677
+ : arg.startsWith("--glob=")
678
+ ? arg.slice(7)
679
+ : arg.slice(2);
680
+ if (isSensitivePath(glob)) paths.push(glob);
681
+ continue;
682
+ }
683
+ if (
684
+ options &&
685
+ ["-e", "--regexp", "-f", "--file", "-g", "--glob", "--iglob", "--include"].includes(arg)
686
+ ) {
687
+ const value = args[++i];
688
+ if (arg === "-f" || arg === "--file") paths.push(value ?? "");
689
+ if (
690
+ (arg === "-g" || arg === "--glob" || arg === "--iglob" || arg === "--include") &&
691
+ value &&
692
+ isSensitivePath(value)
693
+ )
694
+ paths.push(value);
695
+ if (arg === "-e" || arg === "--regexp" || arg === "-f" || arg === "--file")
696
+ hasPattern = true;
697
+ continue;
698
+ }
699
+ if (options && (arg === "--files" || (arg === "-l" && name === "grep"))) {
700
+ filesOnly = arg === "--files";
701
+ if (filesOnly) continue;
702
+ }
703
+ if (options && arg.startsWith("-")) {
704
+ if (/^-[A-Za-z]+[fg]$/.test(arg)) {
705
+ const value = args[++i];
706
+ if (value && isSensitivePath(value)) paths.push(value);
707
+ if (arg.endsWith("f")) hasPattern = true;
708
+ continue;
709
+ }
710
+ const combinedPatternFile = arg.match(/^-[A-Za-z]*f(.+)$/)?.[1];
711
+ if (combinedPatternFile) {
712
+ if (isSensitivePath(combinedPatternFile)) paths.push(combinedPatternFile);
713
+ hasPattern = true;
714
+ }
715
+ const optionValue = arg.includes("=") ? arg.slice(arg.indexOf("=") + 1) : null;
716
+ if (optionValue && isSensitivePath(optionValue)) paths.push(optionValue);
717
+ continue;
718
+ }
719
+ if (!hasPattern && !filesOnly) {
720
+ hasPattern = true;
721
+ continue;
722
+ }
723
+ paths.push(arg);
724
+ }
725
+ return paths.some(isSensitivePath);
726
+ }
727
+ return inspected.slice(1).some(isSensitivePath);
630
728
  });
631
729
  }
632
730
  const unquoted = command.replace(/'(?:\\.|[^'])*'/g, " ").replace(/"(?:\\.|[^"$`])*"/g, " ");
@@ -1,262 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- import { Command, Flag } from "effect/unstable/cli";
3
- import { BunRuntime, BunServices } from "@effect/platform-bun";
4
- import { Effect, Layer, Option } from "effect";
2
+ import { checkEffectVersion } from "#shared/effect-version";
5
3
 
6
- import type { SchemaMode } from "./types";
7
-
8
- import {
9
- makeSchemaCommand,
10
- formatOption,
11
- formatOutput,
12
- logText,
13
- renderCauseToStderr,
14
- VERSION,
15
- } from "#shared";
16
- import { AuditServiceLayer, withAudit } from "#shared/audit";
17
- import { ConfigService, ConfigServiceLayer, getDefaultEnvironment } from "#config";
18
- import { DbConfigService, makeDbConfigLayer } from "./config-service";
19
- import { DbConnectionError } from "./errors";
20
- import { DbService } from "./service";
21
- import { DbSqlClient } from "./sql-client";
22
-
23
- // Extract --profile from argv before @effect/cli parsing
24
- // so we can build the correct config layer.
25
- const profileIndex = process.argv.indexOf("--profile");
26
- const profileArg = profileIndex !== -1 ? process.argv[profileIndex + 1] : undefined;
27
-
28
- /**
29
- * Resolve environment from explicit --env flag, config defaultEnvironment, or fail with hint.
30
- */
31
- const resolveEnv = (envOption: Option.Option<string>) =>
32
- Effect.gen(function* () {
33
- const explicit = Option.getOrUndefined(envOption);
34
- if (explicit) return explicit;
35
-
36
- const config = yield* ConfigService;
37
- const defaultEnv = getDefaultEnvironment(config);
38
-
39
- if (defaultEnv === "prod") {
40
- return yield* new DbConnectionError({
41
- message:
42
- "Implicit prod access blocked. Config defaultEnvironment is 'prod' but --env was not passed explicitly.",
43
- environment: "(prod-safety)",
44
- hint: "Pass --env prod explicitly to confirm production access, or change defaultEnvironment to a non-prod value.",
45
- nextCommand: 'agent-tools-db sql --env prod --sql "SELECT 1"',
46
- });
47
- }
48
-
49
- if (defaultEnv) return defaultEnv;
50
-
51
- return yield* new DbConnectionError({
52
- message:
53
- "No environment specified. Use --env <name> or set defaultEnvironment in agent-tools.json5.",
54
- environment: "(not specified)",
55
- hint: 'Set defaultEnvironment in agent-tools.json5 (e.g. defaultEnvironment: "local") or pass --env explicitly.',
56
- nextCommand: 'agent-tools-db sql --env local --sql "SELECT 1"',
57
- });
58
- });
59
-
60
- const sqlCommand = Command.make(
61
- "sql",
62
- {
63
- env: Flag.optional(Flag.String("env")).pipe(
64
- Flag.withDescription(
65
- "Target database environment name (e.g. local, test, prod). Falls back to defaultEnvironment in config.",
66
- ),
67
- ),
68
- sql: Flag.String("sql").pipe(Flag.withDescription("SQL query to execute")),
69
- limit: Flag.optional(Flag.Int("limit")).pipe(
70
- Flag.withDescription(
71
- "Max rows to return (default 50). Use 0 for no cap. Prefer a SQL LIMIT for large tables.",
72
- ),
73
- ),
74
- format: formatOption,
75
- profile: Flag.optional(Flag.String("profile")).pipe(
76
- Flag.withDescription("Database profile name from agent-tools.json5 (if multiple configured)"),
77
- ),
78
- },
79
- ({ env, sql, limit, format }) =>
80
- Effect.gen(function* () {
81
- const resolvedEnv = yield* resolveEnv(env);
82
- const db = yield* DbService;
83
- const result = yield* db.executeQuery(resolvedEnv, sql, Option.getOrUndefined(limit));
84
- yield* logText(formatOutput(result, format));
85
- }),
86
- ).pipe(Command.withDescription("Execute a SQL query"));
87
-
88
- const schemaCommand = Command.make(
89
- "schema",
90
- {
91
- env: Flag.optional(Flag.String("env")).pipe(
92
- Flag.withDescription(
93
- "Target database environment name (e.g. local, test, prod). Falls back to defaultEnvironment in config.",
94
- ),
95
- ),
96
- mode: Flag.Literals("mode", ["tables", "columns", "full", "relationships"]).pipe(
97
- Flag.withDescription(
98
- "Schema introspection mode: tables (list all), columns (show columns for --table), full (all tables with columns), relationships (foreign keys)",
99
- ),
100
- ),
101
- table: Flag.String("table").pipe(
102
- Flag.withDescription("Table name (required for --mode columns)"),
103
- Flag.optional,
104
- ),
105
- format: formatOption,
106
- profile: Flag.optional(Flag.String("profile")).pipe(
107
- Flag.withDescription("Database profile name from agent-tools.json5 (if multiple configured)"),
108
- ),
109
- },
110
- ({ env, mode, table, format }) =>
111
- Effect.gen(function* () {
112
- const resolvedEnv = yield* resolveEnv(env);
113
- const db = yield* DbService;
114
- const result = yield* db.executeSchemaQuery(
115
- resolvedEnv,
116
- mode as SchemaMode,
117
- Option.getOrUndefined(table),
118
- );
119
- yield* logText(formatOutput(result, format));
120
- }),
121
- ).pipe(Command.withDescription("Introspect database schema (tables, columns, relationships)"));
122
-
123
- const envsCommand = Command.make(
124
- "envs",
125
- {
126
- format: formatOption,
127
- profile: Flag.optional(Flag.String("profile")).pipe(
128
- Flag.withDescription("Database profile name from agent-tools.json5 (if multiple configured)"),
129
- ),
130
- },
131
- ({ format }) =>
132
- Effect.gen(function* () {
133
- const config = yield* ConfigService;
134
- const dbConfig = yield* DbConfigService;
135
- const environments = dbConfig ? Object.keys(dbConfig.environments) : [];
136
- const result = {
137
- success: true as const,
138
- environments,
139
- default: getDefaultEnvironment(config) ?? null,
140
- message: `${environments.length} environment(s) configured`,
141
- executionTimeMs: 0,
142
- };
143
- yield* logText(formatOutput(result, format));
144
- }),
145
- ).pipe(
146
- Command.withDescription("List configured database environments and the default (no network)"),
147
- );
148
-
149
- const ACTIVITY_SQL = `SELECT pid, usename AS "user", state,
150
- date_trunc('second', now() - xact_start) AS xact_age,
151
- date_trunc('second', now() - query_start) AS query_age,
152
- wait_event_type, wait_event, left(query, 200) AS query
153
- FROM pg_stat_activity
154
- WHERE pid <> pg_backend_pid() AND state IS NOT NULL
155
- ORDER BY xact_start ASC NULLS LAST`;
156
-
157
- const LOCKS_SQL = `SELECT blocking.pid AS blocking_pid, blocking.usename AS blocking_user,
158
- left(blocking.query, 150) AS blocking_query,
159
- blocked.pid AS blocked_pid, blocked.usename AS blocked_user,
160
- left(blocked.query, 150) AS blocked_query
161
- FROM pg_stat_activity blocked
162
- JOIN pg_stat_activity blocking ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
163
- ORDER BY blocking.pid, blocked.pid`;
164
-
165
- const GRANTS_SQL = `SELECT grantee, table_schema AS schema, 'table' AS kind, table_name AS object,
166
- string_agg(privilege_type, ', ' ORDER BY privilege_type) AS privileges
167
- FROM information_schema.role_table_grants
168
- WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
169
- GROUP BY grantee, table_schema, table_name
170
- UNION ALL
171
- SELECT grantee, object_schema AS schema, 'sequence' AS kind, object_name AS object,
172
- string_agg(privilege_type, ', ' ORDER BY privilege_type) AS privileges
173
- FROM information_schema.role_usage_grants
174
- WHERE object_type = 'SEQUENCE' AND object_schema NOT IN ('pg_catalog', 'information_schema')
175
- GROUP BY grantee, object_schema, object_name
176
- ORDER BY grantee, schema, kind, object`;
177
-
178
- const makeDiagnosticCommand = (name: string, description: string, sql: string) =>
179
- Command.make(
180
- name,
181
- {
182
- env: Flag.optional(Flag.String("env")).pipe(
183
- Flag.withDescription(
184
- "Target database environment name (e.g. local, test, prod). Falls back to defaultEnvironment in config.",
185
- ),
186
- ),
187
- limit: Flag.optional(Flag.Int("limit")).pipe(
188
- Flag.withDescription("Max rows to return (default 50). Use 0 for no cap."),
189
- ),
190
- format: formatOption,
191
- profile: Flag.optional(Flag.String("profile")).pipe(
192
- Flag.withDescription(
193
- "Database profile name from agent-tools.json5 (if multiple configured)",
194
- ),
195
- ),
196
- },
197
- ({ env, limit, format }) =>
198
- Effect.gen(function* () {
199
- const resolvedEnv = yield* resolveEnv(env);
200
- const db = yield* DbService;
201
- const result = yield* db.executeQuery(resolvedEnv, sql, Option.getOrUndefined(limit));
202
- yield* logText(formatOutput(result, format));
203
- }),
204
- ).pipe(Command.withDescription(description));
205
-
206
- const activityCommand = makeDiagnosticCommand(
207
- "activity",
208
- "Show live sessions from pg_stat_activity (state, transaction/query age, wait event), longest-running first",
209
- ACTIVITY_SQL,
210
- );
211
-
212
- const locksCommand = makeDiagnosticCommand(
213
- "locks",
214
- "Show blocking chains (blocker -> blocked) from pg_blocking_pids + pg_stat_activity",
215
- LOCKS_SQL,
216
- );
217
-
218
- const grantsCommand = makeDiagnosticCommand(
219
- "grants",
220
- "Show per-role table/sequence privileges by schema (excludes system schemas)",
221
- GRANTS_SQL,
222
- );
223
-
224
- const commandsCommand = makeSchemaCommand(() => mainCommand);
225
-
226
- const mainCommand = Command.make("db-tool", {}).pipe(
227
- Command.withDescription("Database Query Tool for Coding Agents"),
228
- Command.withSubcommands([
229
- sqlCommand,
230
- schemaCommand,
231
- activityCommand,
232
- locksCommand,
233
- grantsCommand,
234
- envsCommand,
235
- commandsCommand,
236
- ]),
237
- );
238
-
239
- const cli = Command.run(mainCommand, {
240
- version: VERSION,
241
- });
242
-
243
- const dbConfigLayer = makeDbConfigLayer(profileArg);
244
-
245
- const MainLayer = DbService.layer.pipe(
246
- // provideMerge (not provide) so DbConfigService stays in the program context for the `envs` command,
247
- // which reads it directly rather than going through DbService.
248
- Layer.provideMerge(DbSqlClient.layer),
249
- Layer.provideMerge(dbConfigLayer),
250
- Layer.provideMerge(ConfigServiceLayer),
251
- Layer.provideMerge(BunServices.layer),
252
- Layer.provideMerge(AuditServiceLayer),
253
- );
254
-
255
- const program = withAudit("db", cli).pipe(
256
- Effect.provide(MainLayer),
257
- Effect.tapCause(renderCauseToStderr),
258
- );
259
-
260
- BunRuntime.runMain(program, {
261
- disableErrorReporting: true,
262
- });
4
+ if (await checkEffectVersion()) await import("./main");
5
+ else process.exitCode = 1;