@blogic-cz/agent-tools 1.11.1 → 1.13.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.
- package/README.md +18 -43
- package/dist/credential-guard/bash.d.ts +3 -0
- package/dist/credential-guard/bash.d.ts.map +1 -0
- package/dist/credential-guard/claude-hook.d.ts.map +1 -1
- package/dist/credential-guard/engine.test.d.ts +5 -0
- package/dist/credential-guard/engine.test.d.ts.map +1 -0
- package/dist/credential-guard/index.d.ts +8 -35
- package/dist/credential-guard/index.d.ts.map +1 -1
- package/dist/credential-guard/ir.d.ts +150 -0
- package/dist/credential-guard/ir.d.ts.map +1 -0
- package/dist/credential-guard/literal-output.d.ts +2 -0
- package/dist/credential-guard/literal-output.d.ts.map +1 -0
- package/dist/credential-guard/passive-awk.d.ts +2 -0
- package/dist/credential-guard/passive-awk.d.ts.map +1 -0
- package/dist/credential-guard/passive-jq.d.ts.map +1 -1
- package/dist/credential-guard/passive-sed.d.ts +2 -0
- package/dist/credential-guard/passive-sed.d.ts.map +1 -0
- package/dist/credential-guard/passive.test.d.ts +2 -0
- package/dist/credential-guard/passive.test.d.ts.map +1 -0
- package/dist/credential-guard/paths.d.ts +14 -0
- package/dist/credential-guard/paths.d.ts.map +1 -0
- package/dist/credential-guard/policy.d.ts +47 -0
- package/dist/credential-guard/policy.d.ts.map +1 -0
- package/dist/credential-guard/powershell.d.ts +3 -0
- package/dist/credential-guard/powershell.d.ts.map +1 -0
- package/dist/credential-guard/registry.d.ts +89 -0
- package/dist/credential-guard/registry.d.ts.map +1 -0
- package/dist/credential-guard/review4.test.d.ts +2 -0
- package/dist/credential-guard/review4.test.d.ts.map +1 -0
- package/dist/credential-guard/review7.test.d.ts +2 -0
- package/dist/credential-guard/review7.test.d.ts.map +1 -0
- package/dist/credential-guard/shell-oracle.d.ts +2 -0
- package/dist/credential-guard/shell-oracle.d.ts.map +1 -0
- package/dist/credential-guard/xargs.d.ts +7 -0
- package/dist/credential-guard/xargs.d.ts.map +1 -0
- package/dist/gh-tool/pr/checkbox-markdown.d.ts +22 -0
- package/dist/gh-tool/pr/checkbox-markdown.d.ts.map +1 -0
- package/dist/gh-tool/pr/checkbox.d.ts +21 -0
- package/dist/gh-tool/pr/checkbox.d.ts.map +1 -0
- package/dist/gh-tool/pr/commands.d.ts +9 -0
- package/dist/gh-tool/pr/commands.d.ts.map +1 -1
- package/dist/gh-tool/pr/index.d.ts +1 -1
- package/dist/gh-tool/pr/index.d.ts.map +1 -1
- package/dist/gh-tool/read/api.d.ts +8 -0
- package/dist/gh-tool/read/api.d.ts.map +1 -1
- package/dist/gh-tool/read/cache.d.ts.map +1 -1
- package/dist/gh-tool/read/commands.d.ts +5 -0
- package/dist/gh-tool/read/commands.d.ts.map +1 -1
- package/dist/gh-tool/read/pulls.d.ts +1 -0
- package/dist/gh-tool/read/pulls.d.ts.map +1 -1
- package/dist/gh-tool/read/reads.d.ts +39 -0
- package/dist/gh-tool/read/reads.d.ts.map +1 -1
- package/dist/gh-tool/read/telemetry-command.d.ts +7 -0
- package/dist/gh-tool/read/telemetry-command.d.ts.map +1 -0
- package/dist/gh-tool/read/telemetry-store.d.ts +3 -0
- package/dist/gh-tool/read/telemetry-store.d.ts.map +1 -0
- package/dist/gh-tool/read/telemetry.d.ts +59 -0
- package/dist/gh-tool/read/telemetry.d.ts.map +1 -0
- package/dist/gh-tool/read/transport.d.ts +9 -0
- package/dist/gh-tool/read/transport.d.ts.map +1 -1
- package/dist/gh-tool/read/writes.d.ts +2 -1
- package/dist/gh-tool/read/writes.d.ts.map +1 -1
- package/package.json +2 -1
- package/schemas/agent-tools.schema.json +1 -1
- package/src/credential-guard/bash.ts +769 -0
- package/src/credential-guard/claude-hook.ts +14 -14
- package/src/credential-guard/engine.test.ts +2050 -0
- package/src/credential-guard/index.ts +285 -4268
- package/src/credential-guard/ir.ts +139 -0
- package/src/credential-guard/literal-output.ts +8 -0
- package/src/credential-guard/passive-awk.ts +239 -0
- package/src/credential-guard/passive-jq.ts +183 -24
- package/src/credential-guard/passive-sed.ts +78 -0
- package/src/credential-guard/passive.test.ts +141 -0
- package/src/credential-guard/paths.ts +108 -0
- package/src/credential-guard/policy.ts +1576 -0
- package/src/credential-guard/powershell.ts +220 -0
- package/src/credential-guard/registry.ts +4720 -0
- package/src/credential-guard/review4.test.ts +83 -0
- package/src/credential-guard/review7.test.ts +51 -0
- package/src/credential-guard/rg-types.json +389 -0
- package/src/credential-guard/shell-oracle.ts +187 -0
- package/src/credential-guard/xargs.ts +65 -0
- package/src/gh-tool/main.ts +32 -6
- package/src/gh-tool/pr/checkbox-markdown.ts +317 -0
- package/src/gh-tool/pr/checkbox.ts +113 -0
- package/src/gh-tool/pr/commands.ts +55 -2
- package/src/gh-tool/pr/core.ts +2 -2
- package/src/gh-tool/pr/index.ts +1 -0
- package/src/gh-tool/read/api.ts +73 -6
- package/src/gh-tool/read/cache.ts +13 -4
- package/src/gh-tool/read/commands.ts +17 -1
- package/src/gh-tool/read/pulls.ts +19 -0
- package/src/gh-tool/read/reads.ts +86 -2
- package/src/gh-tool/read/telemetry-command.ts +45 -0
- package/src/gh-tool/read/telemetry-store.ts +85 -0
- package/src/gh-tool/read/telemetry.ts +191 -0
- package/src/gh-tool/read/transport.ts +95 -6
- package/src/gh-tool/read/writes.ts +19 -2
- package/src/gh-tool/workflow.ts +2 -2
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { Context, Effect } from "effect";
|
|
2
|
+
import { redactSensitiveText } from "#shared/content-security";
|
|
3
|
+
|
|
4
|
+
export type RequestEvent = {
|
|
5
|
+
ts: string;
|
|
6
|
+
method: string;
|
|
7
|
+
endpoint: string;
|
|
8
|
+
status: number | null;
|
|
9
|
+
outcome: "cache" | "304" | "200" | "bypass" | "error" | "subprocess";
|
|
10
|
+
resource: string | null;
|
|
11
|
+
used: number | null;
|
|
12
|
+
remaining: number | null;
|
|
13
|
+
reset: number | null;
|
|
14
|
+
};
|
|
15
|
+
export type RequestRow = RequestEvent & { id: number; invocation: string; command: string };
|
|
16
|
+
export type TelemetryStore = {
|
|
17
|
+
record: (event: RequestEvent) => void;
|
|
18
|
+
read: (since: string) => readonly RequestRow[];
|
|
19
|
+
close: () => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Telemetry = Context.Reference<TelemetryStore | null>("gh/Telemetry", {
|
|
23
|
+
defaultValue: () => null,
|
|
24
|
+
});
|
|
25
|
+
export const CacheRequest = Context.Reference<boolean>("gh/CacheRequest", {
|
|
26
|
+
defaultValue: () => false,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const templateEndpoint = (path: string): string => {
|
|
30
|
+
const pathname = path.replace(/^https?:\/\/[^/]+/, "").split(/[?#]/, 1)[0] ?? "";
|
|
31
|
+
return redactSensitiveText(
|
|
32
|
+
`/${pathname.replace(/^\//, "")}`
|
|
33
|
+
.replace(/\b[0-9a-f]{7,64}\b/gi, (part) => (/^\d+$/.test(part) ? ":id" : ":sha"))
|
|
34
|
+
.replace(/\/\d+(?=\/|$)/g, "/:id"),
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const recordRequest = (event: RequestEvent): Effect.Effect<void> =>
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
const store = yield* Telemetry;
|
|
41
|
+
try {
|
|
42
|
+
store?.record({
|
|
43
|
+
...event,
|
|
44
|
+
endpoint:
|
|
45
|
+
event.outcome === "subprocess" ? event.endpoint : templateEndpoint(event.endpoint),
|
|
46
|
+
});
|
|
47
|
+
} catch {
|
|
48
|
+
/* Best effort. */
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const cacheEvent = (operation: string): RequestEvent => ({
|
|
53
|
+
ts: new Date().toISOString(),
|
|
54
|
+
method: "CACHE",
|
|
55
|
+
endpoint: `cache/${operation}`,
|
|
56
|
+
status: null,
|
|
57
|
+
outcome: "cache",
|
|
58
|
+
resource: null,
|
|
59
|
+
used: null,
|
|
60
|
+
remaining: null,
|
|
61
|
+
reset: null,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const API_VALUE_FLAGS = new Set([
|
|
65
|
+
"-X",
|
|
66
|
+
"--method",
|
|
67
|
+
"-H",
|
|
68
|
+
"--header",
|
|
69
|
+
"-f",
|
|
70
|
+
"-F",
|
|
71
|
+
"--field",
|
|
72
|
+
"--raw-field",
|
|
73
|
+
"--jq",
|
|
74
|
+
"-q",
|
|
75
|
+
"--template",
|
|
76
|
+
"-t",
|
|
77
|
+
"--input",
|
|
78
|
+
"--cache",
|
|
79
|
+
"-p",
|
|
80
|
+
"--preview",
|
|
81
|
+
"--hostname",
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
export const ghSubprocessEvent = (args: readonly string[], exitCode: number): RequestEvent => {
|
|
85
|
+
const api = args[0] === "api";
|
|
86
|
+
let endpoint: string | undefined;
|
|
87
|
+
let method: string | undefined;
|
|
88
|
+
let hasFields = false;
|
|
89
|
+
if (api) {
|
|
90
|
+
for (let index = 1; index < args.length; index++) {
|
|
91
|
+
const arg = args[index];
|
|
92
|
+
if (arg === undefined) continue;
|
|
93
|
+
if (arg === "--") {
|
|
94
|
+
endpoint ??= args[index + 1];
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
if (!arg.startsWith("-")) {
|
|
98
|
+
endpoint ??= arg;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const equals = arg.indexOf("=");
|
|
102
|
+
const flag = equals < 0 ? arg : arg.slice(0, equals);
|
|
103
|
+
if (!API_VALUE_FLAGS.has(flag)) continue;
|
|
104
|
+
const value = equals < 0 ? args[++index] : arg.slice(equals + 1);
|
|
105
|
+
if (flag === "-X" || flag === "--method") method = value;
|
|
106
|
+
if (flag === "-f" || flag === "-F" || flag === "--field" || flag === "--raw-field")
|
|
107
|
+
hasFields = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
...cacheEvent(""),
|
|
112
|
+
method: api ? (method ?? (hasFields ? "POST" : "GET")) : "GH",
|
|
113
|
+
endpoint: api ? templateEndpoint(endpoint ?? "") : args.slice(0, 2).join(" "),
|
|
114
|
+
status: exitCode >= 100 && exitCode <= 599 ? exitCode : null,
|
|
115
|
+
outcome: "subprocess",
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const counts = (values: readonly string[]) => {
|
|
120
|
+
const groups = new Map<string, number>();
|
|
121
|
+
for (const value of values) groups.set(value, (groups.get(value) ?? 0) + 1);
|
|
122
|
+
return [...groups]
|
|
123
|
+
.map(([name, requests]) => ({ name, requests }))
|
|
124
|
+
.toSorted((a, b) => b.requests - a.requests || a.name.localeCompare(b.name));
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const subprocessSpendsResource = (row: RequestRow, resource: string): boolean => {
|
|
128
|
+
if (row.outcome !== "subprocess") return false;
|
|
129
|
+
if (row.method === "GH") return true;
|
|
130
|
+
if (row.method === "GET" && row.endpoint === "/rate_limit") return false;
|
|
131
|
+
return row.endpoint === "/graphql" ? resource === "graphql" : resource !== "graphql";
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const summarizeRequests = (rows: readonly RequestRow[]) => {
|
|
135
|
+
const requests = rows.filter((row) => row.outcome !== "cache" && row.outcome !== "subprocess");
|
|
136
|
+
const subprocesses = rows.filter((row) => row.outcome === "subprocess");
|
|
137
|
+
const previous = new Map<string, { used: number; index: number }>();
|
|
138
|
+
let externalUsageEstimate = 0;
|
|
139
|
+
let intervals = 0;
|
|
140
|
+
for (const [index, row] of rows.entries()) {
|
|
141
|
+
if (row.outcome === "cache" || row.outcome === "subprocess") continue;
|
|
142
|
+
if (row.resource === null || row.used === null || row.reset === null) continue;
|
|
143
|
+
const resource = row.resource;
|
|
144
|
+
const key = `${resource}:${row.reset}`;
|
|
145
|
+
const before = previous.get(key);
|
|
146
|
+
if (before !== undefined && row.used >= before.used) {
|
|
147
|
+
const interval = rows.slice(before.index + 1, index + 1);
|
|
148
|
+
const own = interval.filter(
|
|
149
|
+
(candidate) =>
|
|
150
|
+
candidate.resource === resource &&
|
|
151
|
+
(candidate.reset === null || candidate.reset === row.reset) &&
|
|
152
|
+
candidate.status !== 304 &&
|
|
153
|
+
candidate.endpoint !== "/rate_limit",
|
|
154
|
+
).length;
|
|
155
|
+
const subprocessCalls = interval.filter((candidate) =>
|
|
156
|
+
subprocessSpendsResource(candidate, resource),
|
|
157
|
+
).length;
|
|
158
|
+
externalUsageEstimate += Math.max(0, row.used - before.used - own - subprocessCalls);
|
|
159
|
+
intervals++;
|
|
160
|
+
}
|
|
161
|
+
if (before === undefined || row.used >= before.used)
|
|
162
|
+
previous.set(key, { used: row.used, index });
|
|
163
|
+
}
|
|
164
|
+
const notModified = requests.filter((row) => row.status === 304).length;
|
|
165
|
+
return {
|
|
166
|
+
directHttpRequests: requests.length,
|
|
167
|
+
ghSubprocessCalls: subprocesses.length,
|
|
168
|
+
cacheHits: rows.filter((row) => row.outcome === "cache").length,
|
|
169
|
+
subprocessTopCommands: counts(subprocesses.map((row) => redactSensitiveText(row.command)))
|
|
170
|
+
.slice(0, 20)
|
|
171
|
+
.map(({ name, requests: calls }) => ({ name, calls })),
|
|
172
|
+
subprocessTopEndpoints: counts(subprocesses.map((row) => `${row.method} ${row.endpoint}`))
|
|
173
|
+
.slice(0, 20)
|
|
174
|
+
.map(({ name, requests: calls }) => ({ name, calls })),
|
|
175
|
+
directHttpByResource: counts(requests.map((row) => row.resource ?? "unknown")),
|
|
176
|
+
directHttpTopCommands: counts(requests.map((row) => redactSensitiveText(row.command))).slice(
|
|
177
|
+
0,
|
|
178
|
+
20,
|
|
179
|
+
),
|
|
180
|
+
directHttpTopEndpoints: counts(requests.map((row) => `${row.method} ${row.endpoint}`)).slice(
|
|
181
|
+
0,
|
|
182
|
+
20,
|
|
183
|
+
),
|
|
184
|
+
notModified,
|
|
185
|
+
ratio304: requests.length === 0 ? null : notModified / requests.length,
|
|
186
|
+
externalUsageEstimate: intervals === 0 ? null : externalUsageEstimate,
|
|
187
|
+
observedIntervals: intervals,
|
|
188
|
+
caveat:
|
|
189
|
+
"Estimate within observed reset windows only. Missing headers, concurrent requests, different tokens and dropped telemetry reduce accuracy. Direct HTTP counts are exact. Each gh subprocess call may make zero or more HTTP requests; its HTTP status and quota headers are unavailable. For this estimate, subprocess calls count as one request each. Cache hits count ReadCache only.",
|
|
190
|
+
};
|
|
191
|
+
};
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { ghSubprocessEvent, recordRequest } from "./telemetry";
|
|
1
2
|
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
2
|
-
import { Context, Effect, Layer, Stream } from "effect";
|
|
3
|
+
import { Context, Effect, Layer, Schema, Stream } from "effect";
|
|
3
4
|
|
|
4
5
|
import type { GitHubRepoConfig } from "#config";
|
|
5
6
|
import type { RepoInfo } from "#gh/types";
|
|
6
7
|
import type { RepoRef } from "./keys";
|
|
7
8
|
|
|
9
|
+
import {
|
|
10
|
+
bunMarkdownOutline,
|
|
11
|
+
findCheckboxLines,
|
|
12
|
+
validateCheckboxEdit,
|
|
13
|
+
} from "#gh/pr/checkbox-markdown";
|
|
8
14
|
import { GH_BINARY } from "#gh/config";
|
|
9
15
|
import { ghEnvironment } from "#gh/environment";
|
|
10
16
|
import { GitHubAuthError, GitHubCommandError, GitHubNotFoundError } from "#gh/errors";
|
|
@@ -79,11 +85,20 @@ export type GhResult = {
|
|
|
79
85
|
};
|
|
80
86
|
|
|
81
87
|
export type GhError = GitHubCommandError | GitHubAuthError | GitHubNotFoundError;
|
|
88
|
+
export type PRCheckboxWrite = {
|
|
89
|
+
readonly repo: RepoRef;
|
|
90
|
+
readonly pr: number;
|
|
91
|
+
readonly expectedBody: string;
|
|
92
|
+
readonly boxOffset: number;
|
|
93
|
+
readonly replacement: " " | "x";
|
|
94
|
+
readonly selectionText: readonly string[];
|
|
95
|
+
};
|
|
82
96
|
|
|
83
97
|
export class GitHubService extends Context.Service<
|
|
84
98
|
GitHubService,
|
|
85
99
|
{
|
|
86
100
|
readonly runGh: (args: string[]) => Effect.Effect<GhResult, GhError>;
|
|
101
|
+
readonly writePRCheckbox?: (opts: PRCheckboxWrite) => Effect.Effect<GhResult, GhError>;
|
|
87
102
|
readonly runGhJson: <T>(args: string[]) => Effect.Effect<T, GhError>;
|
|
88
103
|
readonly runGraphQL: (
|
|
89
104
|
query: string,
|
|
@@ -182,13 +197,17 @@ export class GitHubService extends Context.Service<
|
|
|
182
197
|
|
|
183
198
|
const proc = yield* executor.spawn(command);
|
|
184
199
|
|
|
185
|
-
const stdoutChunk = yield*
|
|
200
|
+
const [stdoutChunk, stderrChunk] = yield* Effect.all(
|
|
201
|
+
[
|
|
202
|
+
proc.stdout.pipe(Stream.decodeText(), Stream.runCollect),
|
|
203
|
+
proc.stderr.pipe(Stream.decodeText(), Stream.runCollect),
|
|
204
|
+
],
|
|
205
|
+
{ concurrency: "unbounded" },
|
|
206
|
+
);
|
|
186
207
|
const stdout = stdoutChunk.join("");
|
|
187
|
-
|
|
188
|
-
const stderrChunk = yield* proc.stderr.pipe(Stream.decodeText(), Stream.runCollect);
|
|
189
208
|
const stderr = stderrChunk.join("");
|
|
190
|
-
|
|
191
209
|
const exitCode = yield* proc.exitCode;
|
|
210
|
+
yield* recordRequest(ghSubprocessEvent(args, Number(exitCode)));
|
|
192
211
|
|
|
193
212
|
return {
|
|
194
213
|
stdout,
|
|
@@ -246,7 +265,7 @@ export class GitHubService extends Context.Service<
|
|
|
246
265
|
const quotaFilter =
|
|
247
266
|
resource === undefined
|
|
248
267
|
? '.resources | to_entries[] | "\\(.key): reset time: \\(.value.reset | todateiso8601); remaining quota: \\(.value.remaining)"'
|
|
249
|
-
: `.resources.${resource} | "reset time: \\(.reset | todateiso8601); remaining quota: \\(.remaining)"`;
|
|
268
|
+
: `.resources.${resource} | "resource: ${resource}; reset time: \\(.reset | todateiso8601); remaining quota: \\(.remaining)"`;
|
|
250
269
|
const quota = yield* executeGh(["api", "rate_limit", "--jq", quotaFilter]).pipe(
|
|
251
270
|
Effect.orElseSucceed(() => null),
|
|
252
271
|
);
|
|
@@ -320,6 +339,75 @@ export class GitHubService extends Context.Service<
|
|
|
320
339
|
});
|
|
321
340
|
};
|
|
322
341
|
|
|
342
|
+
const writePRCheckbox = Effect.fn("GitHubService.writePRCheckbox")(function* (
|
|
343
|
+
opts: PRCheckboxWrite,
|
|
344
|
+
) {
|
|
345
|
+
const path = `repos/${opts.repo.owner}/${opts.repo.name}/pulls/${opts.pr}`;
|
|
346
|
+
const response = yield* githubApi<unknown>({ path });
|
|
347
|
+
const parsed = yield* Effect.try({
|
|
348
|
+
try: () =>
|
|
349
|
+
Schema.decodeUnknownSync(Schema.Struct({ body: Schema.NullOr(Schema.String) }))(
|
|
350
|
+
response.body,
|
|
351
|
+
),
|
|
352
|
+
catch: (error) =>
|
|
353
|
+
new GitHubCommandError({
|
|
354
|
+
command: `GET ${path}`,
|
|
355
|
+
exitCode: 0,
|
|
356
|
+
message: `GitHub returned an invalid PR body: ${String(error)}`,
|
|
357
|
+
stderr: `GitHub returned an invalid PR body: ${String(error)}`,
|
|
358
|
+
}),
|
|
359
|
+
});
|
|
360
|
+
const body = parsed.body ?? "";
|
|
361
|
+
if (body !== opts.expectedBody) {
|
|
362
|
+
return yield* new GitHubCommandError({
|
|
363
|
+
command: "gh-tool pr checkbox",
|
|
364
|
+
exitCode: 1,
|
|
365
|
+
stderr: `PR #${opts.pr} body changed before the checkbox update`,
|
|
366
|
+
message: `Refusing to update PR #${opts.pr}: its body changed after the first read`,
|
|
367
|
+
hint: "Re-read the pull request and retry with the current task text.",
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const checkboxes = findCheckboxLines(
|
|
372
|
+
body,
|
|
373
|
+
opts.selectionText[0] ?? "",
|
|
374
|
+
null,
|
|
375
|
+
bunMarkdownOutline,
|
|
376
|
+
);
|
|
377
|
+
if (
|
|
378
|
+
!checkboxes.some((checkbox) => checkbox.offset === opts.boxOffset) ||
|
|
379
|
+
body[opts.boxOffset] === opts.replacement
|
|
380
|
+
) {
|
|
381
|
+
return yield* new GitHubCommandError({
|
|
382
|
+
command: "gh-tool pr checkbox",
|
|
383
|
+
exitCode: 1,
|
|
384
|
+
stderr: "Refusing an invalid PR checkbox position",
|
|
385
|
+
message: "Refusing to update a character that is not a task checkbox",
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
yield* Effect.try({
|
|
390
|
+
try: () => validateCheckboxEdit(body, opts.boxOffset, opts.replacement),
|
|
391
|
+
catch: () =>
|
|
392
|
+
new GitHubCommandError({
|
|
393
|
+
command: "gh-tool pr checkbox",
|
|
394
|
+
exitCode: 1,
|
|
395
|
+
stderr: "PR body structure is ambiguous; tick this item in the GitHub UI",
|
|
396
|
+
message: "Refusing to update the PR checkbox because its source is ambiguous",
|
|
397
|
+
hint: "PR body structure is ambiguous; tick this item in the GitHub UI.",
|
|
398
|
+
}),
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const updatedBody =
|
|
402
|
+
body.slice(0, opts.boxOffset) + opts.replacement + body.slice(opts.boxOffset + 1);
|
|
403
|
+
const args = ["api", "--method", "PATCH", path, "-f", `body=${updatedBody}`];
|
|
404
|
+
const outboundText = [...args];
|
|
405
|
+
outboundText[5] = "body=[GitHub PR body]";
|
|
406
|
+
outboundText.push(...opts.selectionText);
|
|
407
|
+
yield* validateOutboundText(outboundText.join("\n"), "gh-tool pr checkbox");
|
|
408
|
+
return yield* runGhAttempt(args);
|
|
409
|
+
});
|
|
410
|
+
|
|
323
411
|
const runGhJson = <T>(args: string[]) =>
|
|
324
412
|
Effect.gen(function* () {
|
|
325
413
|
const result = yield* runGh(args);
|
|
@@ -410,6 +498,7 @@ export class GitHubService extends Context.Service<
|
|
|
410
498
|
|
|
411
499
|
return {
|
|
412
500
|
runGh,
|
|
501
|
+
writePRCheckbox,
|
|
413
502
|
runGhJson,
|
|
414
503
|
runGraphQL,
|
|
415
504
|
apiRequest: githubApi,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
+
import { GitHubCommandError } from "#gh/errors";
|
|
2
3
|
import { GitHubService } from "#gh/read/transport";
|
|
3
4
|
import { invalidate } from "./cache";
|
|
4
5
|
import type { Tag } from "./keys";
|
|
5
|
-
import type { GhError } from "#gh/read/transport";
|
|
6
|
+
import type { GhError, PRCheckboxWrite } from "#gh/read/transport";
|
|
6
7
|
import type { GitHubApiRequest } from "#gh/read/api";
|
|
7
8
|
|
|
8
9
|
const mutate = <A, R>(effect: Effect.Effect<A, GhError, R>, touched: readonly Tag[]) =>
|
|
@@ -15,6 +16,22 @@ const run = Effect.fn("writes.run")(function* (argv: string[], touched: readonly
|
|
|
15
16
|
const gh = yield* GitHubService;
|
|
16
17
|
return yield* mutate(gh.runGh(argv), touched);
|
|
17
18
|
});
|
|
19
|
+
const prCheckbox = Effect.fn("writes.prCheckbox")(function* (
|
|
20
|
+
opts: PRCheckboxWrite,
|
|
21
|
+
touched: readonly Tag[],
|
|
22
|
+
) {
|
|
23
|
+
const gh = yield* GitHubService;
|
|
24
|
+
if (gh.writePRCheckbox === undefined) {
|
|
25
|
+
return yield* new GitHubCommandError({
|
|
26
|
+
command: "gh-tool pr checkbox",
|
|
27
|
+
exitCode: 1,
|
|
28
|
+
stderr: "PR checkbox writes are unavailable",
|
|
29
|
+
message:
|
|
30
|
+
"Refusing to update the PR checkbox because the GitHub service cannot verify the body",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return yield* mutate(gh.writePRCheckbox(opts), touched);
|
|
34
|
+
});
|
|
18
35
|
const json = <A>(argv: string[], touched: readonly Tag[]) =>
|
|
19
36
|
Effect.gen(function* () {
|
|
20
37
|
const gh = yield* GitHubService;
|
|
@@ -34,4 +51,4 @@ const api = <A>(request: GitHubApiRequest, touched: readonly Tag[]) =>
|
|
|
34
51
|
const gh = yield* GitHubService;
|
|
35
52
|
return yield* mutate(gh.apiRequest<A>(request), touched);
|
|
36
53
|
});
|
|
37
|
-
export const writes = { run, json, graphql, api };
|
|
54
|
+
export const writes = { run, prCheckbox, json, graphql, api };
|
package/src/gh-tool/workflow.ts
CHANGED
|
@@ -298,7 +298,7 @@ const fetchAnnotations = Effect.fn("workflow.fetchAnnotations")(function* (opts:
|
|
|
298
298
|
const parts = opts.repo.split("/");
|
|
299
299
|
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
300
300
|
return yield* new GitHubCommandError({
|
|
301
|
-
message: `Invalid --repo format: "${opts.repo}". Expected "owner/name" (e.g. "
|
|
301
|
+
message: `Invalid --repo format: "${opts.repo}". Expected "owner/name" (e.g. "example-org/example-repo").`,
|
|
302
302
|
command: "workflow annotations",
|
|
303
303
|
exitCode: 1,
|
|
304
304
|
stderr: "",
|
|
@@ -574,7 +574,7 @@ export const fetchJobLogs = Effect.fn("workflow.fetchJobLogs")(function* (
|
|
|
574
574
|
const parts = opts.repo.split("/");
|
|
575
575
|
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
576
576
|
return yield* new GitHubCommandError({
|
|
577
|
-
message: `Invalid --repo format: "${opts.repo}". Expected "owner/name" (e.g. "
|
|
577
|
+
message: `Invalid --repo format: "${opts.repo}". Expected "owner/name" (e.g. "example-org/example-repo").`,
|
|
578
578
|
command: "workflow job-logs",
|
|
579
579
|
exitCode: 1,
|
|
580
580
|
stderr: "",
|