@blogic-cz/agent-tools 1.5.3 → 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.
- package/README.md +4 -4
- package/dist/audit-tool/main.d.ts +3 -0
- package/dist/audit-tool/main.d.ts.map +1 -0
- package/dist/az-tool/main.d.ts +3 -0
- package/dist/az-tool/main.d.ts.map +1 -0
- package/dist/azdo-tool/main.d.ts +3 -0
- package/dist/azdo-tool/main.d.ts.map +1 -0
- package/dist/db-tool/main.d.ts +3 -0
- package/dist/db-tool/main.d.ts.map +1 -0
- package/dist/gh-tool/main.d.ts +3 -0
- package/dist/gh-tool/main.d.ts.map +1 -0
- package/dist/gh-tool/pr/core.d.ts.map +1 -1
- package/dist/gh-tool/workflow.d.ts.map +1 -1
- package/dist/k8s-tool/main.d.ts +3 -0
- package/dist/k8s-tool/main.d.ts.map +1 -0
- package/dist/logs-tool/index.d.ts +1 -19
- package/dist/logs-tool/index.d.ts.map +1 -1
- package/dist/logs-tool/main.d.ts +21 -0
- package/dist/logs-tool/main.d.ts.map +1 -0
- package/dist/observability-tool/main.d.ts +3 -0
- package/dist/observability-tool/main.d.ts.map +1 -0
- package/dist/session-tool/index.d.ts +1 -11
- package/dist/session-tool/index.d.ts.map +1 -1
- package/dist/session-tool/main.d.ts +13 -0
- package/dist/session-tool/main.d.ts.map +1 -0
- package/dist/shared/effect-version.d.ts +4 -0
- package/dist/shared/effect-version.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/audit-tool/index.ts +3 -109
- package/src/audit-tool/main.ts +111 -0
- package/src/az-tool/index.ts +3 -151
- package/src/az-tool/main.ts +153 -0
- package/src/azdo-tool/index.ts +3 -220
- package/src/azdo-tool/main.ts +222 -0
- package/src/db-tool/index.ts +3 -260
- package/src/db-tool/main.ts +262 -0
- package/src/gh-tool/index.ts +3 -256
- package/src/gh-tool/main.ts +258 -0
- package/src/gh-tool/pr/core.ts +52 -2
- package/src/gh-tool/workflow.ts +1 -0
- package/src/k8s-tool/index.ts +3 -487
- package/src/k8s-tool/main.ts +489 -0
- package/src/logs-tool/index.ts +3 -247
- package/src/logs-tool/main.ts +249 -0
- package/src/observability-tool/index.ts +3 -34
- package/src/observability-tool/main.ts +36 -0
- package/src/session-tool/index.ts +3 -337
- package/src/session-tool/main.ts +339 -0
- package/src/shared/effect-version.ts +32 -0
package/src/db-tool/index.ts
CHANGED
|
@@ -1,262 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
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
|
-
|
|
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;
|
|
@@ -0,0 +1,262 @@
|
|
|
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 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
|
+
});
|