@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,187 @@
|
|
|
1
|
+
import { formatLiteralPrintf } from "./literal-output";
|
|
2
|
+
import { parseBash } from "./bash";
|
|
3
|
+
import { judge } from "./policy";
|
|
4
|
+
import { splitXargsInput, type XargsInput } from "./xargs";
|
|
5
|
+
|
|
6
|
+
const quote = (text: string): string => "'" + text.replaceAll("'", "'\\''") + "'";
|
|
7
|
+
const receipt: Record<string, { checked: number; refused: number; mismatches: unknown[] }> = {};
|
|
8
|
+
async function compare(
|
|
9
|
+
family: string,
|
|
10
|
+
shell: string,
|
|
11
|
+
source: string,
|
|
12
|
+
modeled: readonly string[] | null,
|
|
13
|
+
) {
|
|
14
|
+
const count = (receipt[family] ??= { checked: 0, refused: 0, mismatches: [] });
|
|
15
|
+
const child = Bun.spawn([shell, "-c", source], {
|
|
16
|
+
stdout: "pipe",
|
|
17
|
+
stderr: "pipe",
|
|
18
|
+
env: { PATH: "/usr/bin:/bin", HOME: "/tmp" },
|
|
19
|
+
});
|
|
20
|
+
const actual = await new Response(child.stdout).text();
|
|
21
|
+
await new Response(child.stderr).text();
|
|
22
|
+
await child.exited;
|
|
23
|
+
count.checked++;
|
|
24
|
+
if (modeled === null) count.refused++;
|
|
25
|
+
else if (family === "termination") {
|
|
26
|
+
const reached = actual.split("\n").includes("[reached]");
|
|
27
|
+
if (reached && !modeled.some((output) => output.includes("[reached]")))
|
|
28
|
+
count.mismatches.push({ shell, source, actual, modeled });
|
|
29
|
+
} else if (!modeled.includes(actual)) count.mismatches.push({ shell, source, actual, modeled });
|
|
30
|
+
}
|
|
31
|
+
function modeledOutput(source: string): readonly string[] | null {
|
|
32
|
+
const outputs: string[] = [];
|
|
33
|
+
let unresolved = false;
|
|
34
|
+
const decision = judge(parseBash(source), {
|
|
35
|
+
home: "/home/dev",
|
|
36
|
+
cwd: "/tmp",
|
|
37
|
+
onInvocation(executable, argv) {
|
|
38
|
+
if (executable === "/usr/bin/printf" && argv[1] === "[%s]\\n") {
|
|
39
|
+
if (argv.some((word) => word === undefined)) unresolved = true;
|
|
40
|
+
outputs.push(
|
|
41
|
+
(argv.length > 2 ? argv.slice(2) : [""]).map((word) => `[${word}]\n`).join(""),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
return decision.kind === "pass" && !unresolved
|
|
47
|
+
? outputs.length
|
|
48
|
+
? [...outputs, outputs.join("")]
|
|
49
|
+
: [""]
|
|
50
|
+
: null;
|
|
51
|
+
}
|
|
52
|
+
const jobs: Array<() => Promise<void>> = [];
|
|
53
|
+
const shells = ["/bin/bash", "/bin/sh", "/bin/zsh", "/bin/dash"];
|
|
54
|
+
const available = await Promise.all(
|
|
55
|
+
shells.map(async (shell) => ({ shell, exists: await Bun.file(shell).exists() })),
|
|
56
|
+
);
|
|
57
|
+
for (const { shell, exists } of available) {
|
|
58
|
+
if (!exists) {
|
|
59
|
+
console.log(`SKIP missing shell ${shell}`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const braces = [
|
|
63
|
+
"{a,}",
|
|
64
|
+
"{,}",
|
|
65
|
+
"{,b}",
|
|
66
|
+
"x{,b}",
|
|
67
|
+
"{a,b}{,c}",
|
|
68
|
+
"'{a,b}'",
|
|
69
|
+
'"{a,b}"',
|
|
70
|
+
"{a,{b,c}}",
|
|
71
|
+
"{a,}''",
|
|
72
|
+
"''{,b}",
|
|
73
|
+
];
|
|
74
|
+
for (const left of ["a", "", "b"])
|
|
75
|
+
for (const right of ["", "c", "d"]) braces.push(`{${left},${right}}`, `p{${left},${right}}q`);
|
|
76
|
+
for (const words of braces) {
|
|
77
|
+
const source = `/usr/bin/printf '[%s]\\n' seed ${words}`;
|
|
78
|
+
const model =
|
|
79
|
+
shell.endsWith("/sh") || shell.endsWith("/dash")
|
|
80
|
+
? `sh -c ${quote(source)}`
|
|
81
|
+
: shell.endsWith("/bash")
|
|
82
|
+
? `bash -c ${quote(source)}`
|
|
83
|
+
: source;
|
|
84
|
+
jobs.push(() => compare("brace", shell, source, modeledOutput(model)));
|
|
85
|
+
}
|
|
86
|
+
for (const format of ["%s\\n", "%s ", "%s", "%s:%s\\n"])
|
|
87
|
+
for (const words of [[], ["alpha"], ["alpha", "beta"], ["", "beta"], ["a b", "c"]]) {
|
|
88
|
+
const source = `printf ${quote(format)} ${words.map(quote).join(" ")}`;
|
|
89
|
+
const modeled = formatLiteralPrintf([format, ...words]);
|
|
90
|
+
jobs.push(() => compare("printf", shell, source, modeled === null ? null : [modeled]));
|
|
91
|
+
}
|
|
92
|
+
for (const compound of [
|
|
93
|
+
"{ exit 0; }",
|
|
94
|
+
"(exit 0)",
|
|
95
|
+
"if true; then exit 0; fi",
|
|
96
|
+
"case x in x) exit 0;; esac",
|
|
97
|
+
"for x in a; do exit 0; done",
|
|
98
|
+
"while true; do exit 0; done",
|
|
99
|
+
"exit 0 && true",
|
|
100
|
+
"exit 0 || true",
|
|
101
|
+
"exit 0 && false || true",
|
|
102
|
+
"exit 0 || false && true",
|
|
103
|
+
"{ exit 0; } && true",
|
|
104
|
+
"! { exit 0; } || true",
|
|
105
|
+
])
|
|
106
|
+
for (const redirect of ["&>/dev/null", "&>>/dev/null"]) {
|
|
107
|
+
const source = `${compound} ${redirect}; /usr/bin/printf '[%s]\\n' reached`;
|
|
108
|
+
jobs.push(() =>
|
|
109
|
+
compare(
|
|
110
|
+
"termination",
|
|
111
|
+
shell,
|
|
112
|
+
source,
|
|
113
|
+
modeledOutput(
|
|
114
|
+
`${shell.endsWith("/bash") || shell.endsWith("/zsh") ? "bash" : "sh"} -c ${quote(source)}`,
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
for (const command of ["exit", "return"])
|
|
120
|
+
for (const redirect of [
|
|
121
|
+
"",
|
|
122
|
+
" 2>/dev/null",
|
|
123
|
+
" &>/dev/null",
|
|
124
|
+
" &>>/dev/null",
|
|
125
|
+
" </tmp/guard-oracle-missing/input",
|
|
126
|
+
" >/dev/null/x",
|
|
127
|
+
" 2>&1",
|
|
128
|
+
" 2>&99",
|
|
129
|
+
" 1>&- 2>&1",
|
|
130
|
+
])
|
|
131
|
+
for (const op of [";", "||"]) {
|
|
132
|
+
const source = `${command} 0${redirect} ${op} /usr/bin/printf '[%s]\\n' reached`;
|
|
133
|
+
jobs.push(() =>
|
|
134
|
+
compare(
|
|
135
|
+
"termination",
|
|
136
|
+
shell,
|
|
137
|
+
source,
|
|
138
|
+
modeledOutput(
|
|
139
|
+
`${shell.endsWith("/bash") || shell.endsWith("/zsh") ? "bash" : "sh"} -c ${quote(source)}`,
|
|
140
|
+
),
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const modes: XargsInput[] = [
|
|
146
|
+
{ replacement: null, maxArgs: null, maxLines: null },
|
|
147
|
+
{ replacement: "{}", maxArgs: null, maxLines: null },
|
|
148
|
+
{ replacement: null, maxArgs: 1, maxLines: null },
|
|
149
|
+
{ replacement: null, maxArgs: null, maxLines: 1 },
|
|
150
|
+
];
|
|
151
|
+
const inputs = [
|
|
152
|
+
"a\nb\n",
|
|
153
|
+
"a b\n",
|
|
154
|
+
"'' word\n",
|
|
155
|
+
'"" word\n',
|
|
156
|
+
"a b\n",
|
|
157
|
+
"a\tb\n",
|
|
158
|
+
" a\n",
|
|
159
|
+
"a\n\nb\n",
|
|
160
|
+
"'a b'\n",
|
|
161
|
+
"a\\ b\n",
|
|
162
|
+
];
|
|
163
|
+
for (const a of ["a", "''", '""', " "])
|
|
164
|
+
for (const b of [" ", " ", "\t", "\n"]) inputs.push(a + b + "b\n");
|
|
165
|
+
for (const mode of modes)
|
|
166
|
+
for (const input of inputs) {
|
|
167
|
+
const groups = splitXargsInput(input, mode);
|
|
168
|
+
const output =
|
|
169
|
+
groups === null
|
|
170
|
+
? null
|
|
171
|
+
: groups
|
|
172
|
+
.flatMap((words) => (words.length ? words : [""]).map((word) => `[${word}]\n`))
|
|
173
|
+
.join("");
|
|
174
|
+
const flags = mode.replacement ? "-I{}" : mode.maxArgs ? "-n1" : mode.maxLines ? "-L1" : "";
|
|
175
|
+
const source = `printf '%s' ${quote(input)} | /usr/bin/xargs ${flags} /usr/bin/printf '[%s]\\n' ${mode.replacement ? "'{}'" : ""}`;
|
|
176
|
+
jobs.push(() =>
|
|
177
|
+
compare(
|
|
178
|
+
`xargs-${flags || "default"}`,
|
|
179
|
+
"/bin/bash",
|
|
180
|
+
source,
|
|
181
|
+
output === null ? null : [output],
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
await jobs.reduce((previous, run) => previous.then(run), Promise.resolve());
|
|
186
|
+
console.log(JSON.stringify(receipt, null, 2));
|
|
187
|
+
if (Object.values(receipt).some((family) => family.mismatches.length)) process.exitCode = 1;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export type XargsInput = {
|
|
2
|
+
readonly replacement: string | null;
|
|
3
|
+
readonly maxArgs: number | null;
|
|
4
|
+
readonly maxLines: number | null;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function splitXargsInput(text: string, mode: XargsInput): readonly string[][] | null {
|
|
8
|
+
if (/[^\t\n\x20-\x7e]/.test(text)) return null;
|
|
9
|
+
if (mode.replacement !== null && /['"\t]| {2}|^ |\n |\\/.test(text)) return null;
|
|
10
|
+
const lines: string[][] = [];
|
|
11
|
+
let words: string[] = [];
|
|
12
|
+
let word = "";
|
|
13
|
+
let started = false;
|
|
14
|
+
let quote = "";
|
|
15
|
+
const finishWord = () => {
|
|
16
|
+
if (started) words.push(word);
|
|
17
|
+
word = "";
|
|
18
|
+
started = false;
|
|
19
|
+
};
|
|
20
|
+
for (let index = 0; index < text.length; index++) {
|
|
21
|
+
const ch = text[index] ?? "";
|
|
22
|
+
if (ch === "\n") {
|
|
23
|
+
if (
|
|
24
|
+
quote ||
|
|
25
|
+
((mode.maxLines !== null || mode.replacement !== null) &&
|
|
26
|
+
/[ \t]/.test(text[index - 1] ?? ""))
|
|
27
|
+
)
|
|
28
|
+
return null;
|
|
29
|
+
finishWord();
|
|
30
|
+
if (words.length) lines.push(words);
|
|
31
|
+
words = [];
|
|
32
|
+
} else if (ch === "\\" && !quote) {
|
|
33
|
+
const next = text[++index];
|
|
34
|
+
if (next === undefined || next === "\n") return null;
|
|
35
|
+
word += next;
|
|
36
|
+
started = true;
|
|
37
|
+
} else if ((ch === "'" || ch === '"') && (!quote || ch === quote)) {
|
|
38
|
+
quote = quote ? "" : ch;
|
|
39
|
+
started = true;
|
|
40
|
+
} else if (!quote && /[ \t]/.test(ch) && mode.replacement === null) {
|
|
41
|
+
finishWord();
|
|
42
|
+
} else if (mode.replacement !== null && !started && /[ \t]/.test(ch)) {
|
|
43
|
+
continue;
|
|
44
|
+
} else {
|
|
45
|
+
word += ch;
|
|
46
|
+
started = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (quote) return null;
|
|
50
|
+
finishWord();
|
|
51
|
+
if (words.length) lines.push(words);
|
|
52
|
+
const result: string[][] = [];
|
|
53
|
+
if (mode.replacement !== null) result.push(...lines);
|
|
54
|
+
else if (mode.maxLines !== null) {
|
|
55
|
+
for (let index = 0; index < lines.length; index += mode.maxLines)
|
|
56
|
+
result.push(lines.slice(index, index + mode.maxLines).flat());
|
|
57
|
+
} else {
|
|
58
|
+
const all = lines.flat();
|
|
59
|
+
const size = mode.maxArgs ?? Math.max(all.length, 1);
|
|
60
|
+
for (let index = 0; index < all.length; index += size)
|
|
61
|
+
result.push(all.slice(index, index + size));
|
|
62
|
+
}
|
|
63
|
+
if (result.length > 64 || result.some((args) => args.length > 64)) return null;
|
|
64
|
+
return result.length ? result : null;
|
|
65
|
+
}
|
package/src/gh-tool/main.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { BunRuntime, BunServices } from "@effect/platform-bun";
|
|
|
4
4
|
import { Effect, Layer } from "effect";
|
|
5
5
|
|
|
6
6
|
import { makeSchemaCommand, renderCauseToStderr, VERSION } from "#shared";
|
|
7
|
-
import { AuditServiceLayer, withAudit } from "#shared/audit";
|
|
7
|
+
import { AuditService, AuditServiceLayer, withAudit } from "#shared/audit";
|
|
8
|
+
import { openRequestTelemetry } from "#gh/read/telemetry-store";
|
|
8
9
|
import { withRedactedOutput } from "#shared/output-boundary";
|
|
9
10
|
import { ConfigServiceLayer } from "#config";
|
|
10
11
|
import {
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
prCreateCommand,
|
|
27
28
|
prCloseCommand,
|
|
28
29
|
prEditCommand,
|
|
30
|
+
prCheckboxCommand,
|
|
29
31
|
prMergeCommand,
|
|
30
32
|
prReadyCommand,
|
|
31
33
|
prDraftCommand,
|
|
@@ -80,7 +82,9 @@ import {
|
|
|
80
82
|
import { labelCreateCommand } from "./label";
|
|
81
83
|
import { GitHubService } from "./read/transport";
|
|
82
84
|
import { withReadCache } from "./read";
|
|
83
|
-
import {
|
|
85
|
+
import { Telemetry } from "./read/telemetry";
|
|
86
|
+
import { telemetryCommand } from "./read/telemetry-command";
|
|
87
|
+
import { cacheCommand, rateLimitCommand } from "./read/commands";
|
|
84
88
|
import {
|
|
85
89
|
workflowAnnotationsCommand,
|
|
86
90
|
workflowCancelCommand,
|
|
@@ -103,6 +107,7 @@ const prCommand = Command.make("pr", {}).pipe(
|
|
|
103
107
|
prCreateCommand,
|
|
104
108
|
prCloseCommand,
|
|
105
109
|
prEditCommand,
|
|
110
|
+
prCheckboxCommand,
|
|
106
111
|
prMergeCommand,
|
|
107
112
|
prReadyCommand,
|
|
108
113
|
prDraftCommand,
|
|
@@ -221,6 +226,7 @@ const mainCommand = Command.make("gh-tool", {}).pipe(
|
|
|
221
226
|
|
|
222
227
|
Wraps the GitHub CLI (gh) with structured output for AI agents.
|
|
223
228
|
Supports PR management, issue management, reviews, CI checks, and repo info.
|
|
229
|
+
Use 'rate-limit' to inspect GitHub API quota without consuming it.
|
|
224
230
|
|
|
225
231
|
WORKFLOW FOR AI AGENTS:
|
|
226
232
|
1. Use 'pr view' to inspect current PR
|
|
@@ -250,6 +256,8 @@ WORKFLOW FOR AI AGENTS:
|
|
|
250
256
|
24. Use 'gist list/view' to read gists and 'gist create --files a.ts --desc "..."' to share snippets`,
|
|
251
257
|
),
|
|
252
258
|
Command.withSubcommands([
|
|
259
|
+
rateLimitCommand,
|
|
260
|
+
telemetryCommand,
|
|
253
261
|
prCommand,
|
|
254
262
|
issueCommand,
|
|
255
263
|
repoCommand,
|
|
@@ -274,10 +282,28 @@ const MainLayer = GitHubService.layer.pipe(
|
|
|
274
282
|
Layer.provideMerge(ConfigServiceLayer),
|
|
275
283
|
);
|
|
276
284
|
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
285
|
+
const observed = Effect.gen(function* () {
|
|
286
|
+
const audit = yield* AuditService;
|
|
287
|
+
let store = null;
|
|
288
|
+
try {
|
|
289
|
+
store = openRequestTelemetry(audit.dbPath);
|
|
290
|
+
} catch {
|
|
291
|
+
/* Telemetry is optional. */
|
|
292
|
+
}
|
|
293
|
+
return yield* withReadCache(withRedactedOutput(withAudit("gh", cli))).pipe(
|
|
294
|
+
Effect.provideService(Telemetry, store),
|
|
295
|
+
Effect.ensuring(
|
|
296
|
+
Effect.sync(() => {
|
|
297
|
+
try {
|
|
298
|
+
store?.close();
|
|
299
|
+
} catch {
|
|
300
|
+
/* Best effort. */
|
|
301
|
+
}
|
|
302
|
+
}),
|
|
303
|
+
),
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
const program = observed.pipe(Effect.provide(MainLayer), Effect.tapCause(renderCauseToStderr));
|
|
281
307
|
|
|
282
308
|
BunRuntime.runMain(program, {
|
|
283
309
|
disableErrorReporting: true,
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
export type CheckboxOutlineEntry =
|
|
2
|
+
| { readonly type: "task"; readonly text: string; readonly checked: boolean }
|
|
3
|
+
| { readonly type: "heading"; readonly text: string; readonly level: number };
|
|
4
|
+
|
|
5
|
+
export type CheckboxOutlineParser = (body: string) => readonly CheckboxOutlineEntry[] | null;
|
|
6
|
+
|
|
7
|
+
type CheckboxLine = {
|
|
8
|
+
readonly before: string;
|
|
9
|
+
readonly offset: number;
|
|
10
|
+
readonly boxIndex: number;
|
|
11
|
+
readonly checked: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type ScannedEntry = CheckboxOutlineEntry & {
|
|
15
|
+
readonly offset?: number;
|
|
16
|
+
readonly boxIndex?: number;
|
|
17
|
+
readonly before?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type Fence = {
|
|
21
|
+
readonly marker: "`" | "~";
|
|
22
|
+
readonly length: number;
|
|
23
|
+
readonly listIndent: number | null;
|
|
24
|
+
};
|
|
25
|
+
type ListItemIndent = { readonly indent: number; readonly contentIndent: number };
|
|
26
|
+
|
|
27
|
+
const taskSuffix = /^[ \t]*\[([ xX])\][ \t]*(.*)$/;
|
|
28
|
+
const listMarker = /^( {0,3})(?:[-*+]|[0-9]+[.)])([ \t]+)/;
|
|
29
|
+
const quoteMarker = /^ {0,3}>[ \t]?/;
|
|
30
|
+
const plainSourceText = (text: string) =>
|
|
31
|
+
text
|
|
32
|
+
.replace(/(\*\*|__|~~|\*|_)(\S(?:.*?\S)?)\1/g, "$2")
|
|
33
|
+
.replace(/&/g, "&")
|
|
34
|
+
.replace(/</g, "<")
|
|
35
|
+
.replace(/>/g, ">")
|
|
36
|
+
.replace(/"/g, '"')
|
|
37
|
+
.replace(/'|'/g, "'")
|
|
38
|
+
.trim();
|
|
39
|
+
|
|
40
|
+
const removeContainers = (line: string) => {
|
|
41
|
+
let cursor = 0;
|
|
42
|
+
for (;;) {
|
|
43
|
+
const rest = line.slice(cursor);
|
|
44
|
+
const quote = quoteMarker.exec(rest);
|
|
45
|
+
if (quote !== null) {
|
|
46
|
+
cursor += quote[0].length;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const list = listMarker.exec(rest);
|
|
50
|
+
if (list !== null) {
|
|
51
|
+
cursor += list[0].length;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
return { text: line.slice(cursor), cursor };
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const openingFence = (line: string, listIndent: number | null): Fence | null => {
|
|
59
|
+
const content = removeContainers(line).text;
|
|
60
|
+
const match = /^ {0,3}(`{3,}|~{3,})([^\r\n]*)\r?$/.exec(content);
|
|
61
|
+
if (match === null) return null;
|
|
62
|
+
const marker = match[1]?.[0];
|
|
63
|
+
const info = match[2] ?? "";
|
|
64
|
+
if ((marker !== "`" && marker !== "~") || (marker === "`" && info.includes("`"))) return null;
|
|
65
|
+
return { marker, length: match[1]?.length ?? 0, listIndent };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const closesFence = (line: string, fence: Fence) => {
|
|
69
|
+
const content = removeContainers(line).text;
|
|
70
|
+
const match = /^ {0,3}(`+|~+)[ \t]*\r?$/.exec(content);
|
|
71
|
+
const markers = match?.[1] ?? "";
|
|
72
|
+
return markers.length >= fence.length && markers[0] === fence.marker;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const headingFromLine = (line: string) => {
|
|
76
|
+
const { text } = removeContainers(line);
|
|
77
|
+
const match = /^ {0,3}(#{1,6})[ \t]+(.*?)\r?$/.exec(text);
|
|
78
|
+
if (match === null) return null;
|
|
79
|
+
return {
|
|
80
|
+
level: match[1]?.length ?? 0,
|
|
81
|
+
text: plainSourceText((match[2] ?? "").replace(/[ \t]+#+[ \t]*$/, "")),
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const scanOutline = (body: string): ScannedEntry[] => {
|
|
86
|
+
const entries: ScannedEntry[] = [];
|
|
87
|
+
const lines = body.match(/[^\n]*(?:\n|$)/g) ?? [];
|
|
88
|
+
let offset = 0;
|
|
89
|
+
let fence: Fence | null = null;
|
|
90
|
+
const listItems: ListItemIndent[] = [];
|
|
91
|
+
|
|
92
|
+
for (const raw of lines) {
|
|
93
|
+
if (raw.length === 0) continue;
|
|
94
|
+
const line = (raw.endsWith("\n") ? raw.slice(0, -1) : raw).replace(/\r$/, "");
|
|
95
|
+
const lineStart = offset;
|
|
96
|
+
offset += raw.length;
|
|
97
|
+
|
|
98
|
+
const marker = listMarker.exec(line);
|
|
99
|
+
if (marker !== null) {
|
|
100
|
+
const indent = marker[1]?.length ?? 0;
|
|
101
|
+
while ((listItems.at(-1)?.indent ?? -1) >= indent) {
|
|
102
|
+
listItems.pop();
|
|
103
|
+
}
|
|
104
|
+
listItems.push({ indent, contentIndent: indent + marker[0].length });
|
|
105
|
+
} else if (line.trim() !== "") {
|
|
106
|
+
const leading = /^ */.exec(line)?.[0].length ?? 0;
|
|
107
|
+
while (listItems.length > 0 && leading < (listItems.at(-1)?.contentIndent ?? 0)) {
|
|
108
|
+
listItems.pop();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (fence !== null) {
|
|
113
|
+
if (fence.listIndent !== null && line.trim() !== "") {
|
|
114
|
+
const leading = /^ */.exec(line)?.[0].length ?? 0;
|
|
115
|
+
if (leading < fence.listIndent) fence = null;
|
|
116
|
+
}
|
|
117
|
+
if (fence !== null) {
|
|
118
|
+
if (closesFence(line, fence)) fence = null;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const context = listItems.at(-1);
|
|
124
|
+
const opening = openingFence(line, context?.contentIndent ?? null);
|
|
125
|
+
if (opening !== null) {
|
|
126
|
+
fence = opening;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const heading = headingFromLine(line);
|
|
131
|
+
if (heading !== null) {
|
|
132
|
+
entries.push({ type: "heading", ...heading });
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const { text, cursor } = removeContainers(line);
|
|
137
|
+
const task = taskSuffix.exec(text);
|
|
138
|
+
if (task === null) continue;
|
|
139
|
+
const checked = task[1] !== " ";
|
|
140
|
+
const boxIndex = cursor + (text.indexOf("[") + 1);
|
|
141
|
+
const content = plainSourceText(task[2] ?? "");
|
|
142
|
+
entries.push({
|
|
143
|
+
type: "task",
|
|
144
|
+
text: content,
|
|
145
|
+
checked,
|
|
146
|
+
offset: lineStart + boxIndex,
|
|
147
|
+
boxIndex,
|
|
148
|
+
before: line.replace(/\r$/, ""),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return entries;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const tokenPrefix = "\uE000checkbox:";
|
|
155
|
+
const tokenSuffix = ":\uE001";
|
|
156
|
+
const makeToken = (id: number) => `${tokenPrefix}${id}${tokenSuffix}`;
|
|
157
|
+
const tokenPattern = /\uE000checkbox:(\d+):\uE001/g;
|
|
158
|
+
|
|
159
|
+
export const bunMarkdownOutline: CheckboxOutlineParser = (body) => {
|
|
160
|
+
const markdown = (globalThis as typeof globalThis & { Bun?: typeof Bun }).Bun?.markdown;
|
|
161
|
+
if (markdown?.render === undefined) return null;
|
|
162
|
+
|
|
163
|
+
const nodes: Array<{ readonly entry: CheckboxOutlineEntry | null; readonly children: string }> =
|
|
164
|
+
[];
|
|
165
|
+
const token = (entry: CheckboxOutlineEntry | null, children: string) => {
|
|
166
|
+
const id = nodes.push({ entry, children }) - 1;
|
|
167
|
+
return makeToken(id);
|
|
168
|
+
};
|
|
169
|
+
const plain = (value: string) => value.replace(tokenPattern, "").trim();
|
|
170
|
+
|
|
171
|
+
const rendered = markdown.render(body, {
|
|
172
|
+
text: (children) => children,
|
|
173
|
+
strong: (children) => children,
|
|
174
|
+
emphasis: (children) => children,
|
|
175
|
+
strikethrough: (children) => children,
|
|
176
|
+
link: (children) => children,
|
|
177
|
+
image: (children) => children,
|
|
178
|
+
codespan: (children) => children,
|
|
179
|
+
heading: (children, meta) =>
|
|
180
|
+
token({ type: "heading", level: meta.level, text: plain(children) }, children),
|
|
181
|
+
listItem: (children, meta) =>
|
|
182
|
+
token(
|
|
183
|
+
typeof meta.checked === "boolean"
|
|
184
|
+
? { type: "task", checked: meta.checked, text: plain(children) }
|
|
185
|
+
: null,
|
|
186
|
+
children,
|
|
187
|
+
),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const outline: CheckboxOutlineEntry[] = [];
|
|
191
|
+
const expand = (value: string) => {
|
|
192
|
+
for (const match of value.matchAll(tokenPattern)) {
|
|
193
|
+
const idText = match[1];
|
|
194
|
+
const id = idText === undefined ? -1 : Number(idText);
|
|
195
|
+
const node = nodes[id];
|
|
196
|
+
if (node === undefined || match.index === undefined)
|
|
197
|
+
throw new Error("Invalid Bun Markdown callback outline");
|
|
198
|
+
if (node.entry !== null) outline.push(node.entry);
|
|
199
|
+
expand(node.children);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
expand(rendered);
|
|
203
|
+
return outline;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const sameOutline = (scanned: readonly ScannedEntry[], parsed: readonly CheckboxOutlineEntry[]) => {
|
|
207
|
+
return (
|
|
208
|
+
scanned.length === parsed.length &&
|
|
209
|
+
scanned.every((entry, index) => {
|
|
210
|
+
const actual = parsed[index];
|
|
211
|
+
return (
|
|
212
|
+
actual !== undefined &&
|
|
213
|
+
entry.type === actual.type &&
|
|
214
|
+
entry.text === actual.text &&
|
|
215
|
+
(entry.type !== "heading" || (actual.type === "heading" && entry.level === actual.level)) &&
|
|
216
|
+
(entry.type !== "task" || (actual.type === "task" && entry.checked === actual.checked))
|
|
217
|
+
);
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const sameEntry = (left: CheckboxOutlineEntry, right: CheckboxOutlineEntry) =>
|
|
223
|
+
left.type === right.type &&
|
|
224
|
+
left.text === right.text &&
|
|
225
|
+
(left.type !== "heading" || (right.type === "heading" && left.level === right.level)) &&
|
|
226
|
+
(left.type !== "task" || (right.type === "task" && left.checked === right.checked));
|
|
227
|
+
|
|
228
|
+
export const validateCheckboxEdit = (
|
|
229
|
+
body: string,
|
|
230
|
+
offset: number,
|
|
231
|
+
replacement: " " | "x",
|
|
232
|
+
parseOutline: CheckboxOutlineParser = bunMarkdownOutline,
|
|
233
|
+
) => {
|
|
234
|
+
const scanned = scanOutline(body);
|
|
235
|
+
const original = parseOutline(body);
|
|
236
|
+
const selectedIndex = scanned.findIndex(
|
|
237
|
+
(entry) => entry.type === "task" && entry.offset === offset,
|
|
238
|
+
);
|
|
239
|
+
if (
|
|
240
|
+
original === null ||
|
|
241
|
+
!sameOutline(scanned, original) ||
|
|
242
|
+
selectedIndex < 0 ||
|
|
243
|
+
original[selectedIndex]?.type !== "task" ||
|
|
244
|
+
body[offset] === undefined ||
|
|
245
|
+
(replacement !== " " && replacement !== "x")
|
|
246
|
+
) {
|
|
247
|
+
throw new Error("PR body structure is ambiguous; tick this item in the GitHub UI");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const editedBody = `${body.slice(0, offset)}${replacement}${body.slice(offset + 1)}`;
|
|
251
|
+
const edited = parseOutline(editedBody);
|
|
252
|
+
if (edited === null || edited.length !== original.length) {
|
|
253
|
+
throw new Error("PR body structure is ambiguous; tick this item in the GitHub UI");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const expected = [...original];
|
|
257
|
+
const selected = expected[selectedIndex];
|
|
258
|
+
if (selected?.type === "task") {
|
|
259
|
+
expected[selectedIndex] = { ...selected, checked: replacement !== " " };
|
|
260
|
+
}
|
|
261
|
+
if (
|
|
262
|
+
!edited.every((entry, index) => {
|
|
263
|
+
const expectedEntry = expected[index];
|
|
264
|
+
return expectedEntry !== undefined && sameEntry(expectedEntry, entry);
|
|
265
|
+
})
|
|
266
|
+
) {
|
|
267
|
+
throw new Error("PR body structure is ambiguous; tick this item in the GitHub UI");
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export const findCheckboxLines = (
|
|
272
|
+
body: string,
|
|
273
|
+
match: string,
|
|
274
|
+
section: string | null,
|
|
275
|
+
parseOutline: CheckboxOutlineParser = bunMarkdownOutline,
|
|
276
|
+
) => {
|
|
277
|
+
const scanned = scanOutline(body);
|
|
278
|
+
const parsed = parseOutline(body);
|
|
279
|
+
if (parsed === null || !sameOutline(scanned, parsed)) {
|
|
280
|
+
throw new Error("PR body structure is ambiguous; tick this item in the GitHub UI");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const matches: CheckboxLine[] = [];
|
|
284
|
+
let sectionLevel: number | null = null;
|
|
285
|
+
let inSection = section === null;
|
|
286
|
+
|
|
287
|
+
for (const entry of scanned) {
|
|
288
|
+
if (entry.type === "heading") {
|
|
289
|
+
if (sectionLevel !== null && entry.level <= sectionLevel) {
|
|
290
|
+
inSection = false;
|
|
291
|
+
sectionLevel = null;
|
|
292
|
+
}
|
|
293
|
+
if (section !== null && entry.text === section) {
|
|
294
|
+
inSection = true;
|
|
295
|
+
sectionLevel = entry.level;
|
|
296
|
+
}
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (!inSection || !entry.text.includes(match)) continue;
|
|
300
|
+
if (entry.offset === undefined || entry.boxIndex === undefined || entry.before === undefined)
|
|
301
|
+
continue;
|
|
302
|
+
matches.push({
|
|
303
|
+
before: entry.before,
|
|
304
|
+
offset: entry.offset,
|
|
305
|
+
boxIndex: entry.boxIndex,
|
|
306
|
+
checked: entry.checked,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return matches;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export const scanCheckboxOutline = (body: string): readonly CheckboxOutlineEntry[] =>
|
|
313
|
+
scanOutline(body).map((entry) =>
|
|
314
|
+
entry.type === "task"
|
|
315
|
+
? { type: entry.type, text: entry.text, checked: entry.checked }
|
|
316
|
+
: { type: entry.type, text: entry.text, level: entry.level },
|
|
317
|
+
);
|