@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.
- 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/credential-guard/index.d.ts.map +1 -1
- package/dist/db-tool/main.d.ts +3 -0
- package/dist/db-tool/main.d.ts.map +1 -0
- package/dist/gh-tool/api.d.ts.map +1 -1
- package/dist/gh-tool/gist.d.ts.map +1 -1
- 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/service.d.ts.map +1 -1
- package/dist/gh-tool/text-input.d.ts +4 -0
- package/dist/gh-tool/text-input.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/credential-guard/index.ts +102 -4
- package/src/db-tool/index.ts +3 -260
- package/src/db-tool/main.ts +262 -0
- package/src/gh-tool/api.ts +21 -1
- package/src/gh-tool/gist.ts +7 -1
- 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/service.ts +9 -5
- package/src/gh-tool/text-input.ts +63 -4
- 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
|
@@ -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
|
-
|
|
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, " ");
|
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;
|