next-issue 2.4.1 → 2.4.2
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 +68 -68
- package/dist/agents.mjs +32 -31
- package/dist/args.mjs +17 -17
- package/dist/cli.mjs +16 -14
- package/dist/config.mjs +42 -42
- package/dist/exec.mjs +26 -26
- package/dist/git.mjs +40 -40
- package/dist/github.mjs +107 -78
- package/dist/interrupt.mjs +7 -7
- package/dist/loop.mjs +5 -3
- package/dist/observe.mjs +19 -19
- package/dist/pipeline.mjs +129 -113
- package/dist/progress.mjs +22 -22
- package/dist/prompts.mjs +52 -52
- package/dist/run.mjs +28 -28
- package/dist/serve.mjs +49 -45
- package/dist/setup.mjs +13 -13
- package/dist/state.mjs +20 -20
- package/dist/summary.mjs +7 -7
- package/dist/verdict.mjs +30 -25
- package/dist/version.mjs +3 -3
- package/package.json +7 -1
package/dist/github.mjs
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
import { must, run } from
|
|
1
|
+
import { must, run } from './exec.mjs';
|
|
2
2
|
const CHECK_PENDING_CODE = 8;
|
|
3
3
|
const READ_TRIES = 3;
|
|
4
4
|
const NETWORK_TRIES = 3;
|
|
5
5
|
const NETWORK_FAULT = /unexpected EOF|connection reset|connection refused|i\/o timeout|TLS handshake timeout|HTTP 50[234]/i;
|
|
6
6
|
const KNOWN_LABELS = new Map();
|
|
7
7
|
const NO_CHECKS = /no checks reported/i;
|
|
8
|
-
const FAILED_CONCLUSIONS = new Set([
|
|
8
|
+
const FAILED_CONCLUSIONS = new Set(['failure', 'timed_out', 'startup_failure']);
|
|
9
9
|
function slug(repo) {
|
|
10
10
|
return `${repo.owner}/${repo.name}`;
|
|
11
11
|
}
|
|
12
12
|
async function repeatable(args, options) {
|
|
13
13
|
for (let attempt = 1;; attempt += 1) {
|
|
14
14
|
try {
|
|
15
|
-
return await must(
|
|
15
|
+
return await must('gh', args, options);
|
|
16
16
|
}
|
|
17
17
|
catch (error) {
|
|
18
|
-
if (attempt >= NETWORK_TRIES ||
|
|
18
|
+
if (attempt >= NETWORK_TRIES ||
|
|
19
|
+
!(error instanceof Error) ||
|
|
20
|
+
!NETWORK_FAULT.test(error.message)) {
|
|
19
21
|
throw error;
|
|
20
22
|
}
|
|
21
23
|
await new Promise((resolve) => setTimeout(resolve, attempt * 1000));
|
|
@@ -23,95 +25,95 @@ async function repeatable(args, options) {
|
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
27
|
async function gh(repo, args) {
|
|
26
|
-
return repeatable([...args,
|
|
28
|
+
return repeatable([...args, '--repo', slug(repo)], { cwd: repo.root });
|
|
27
29
|
}
|
|
28
30
|
async function ghOnce(repo, args, input) {
|
|
29
|
-
return must(
|
|
31
|
+
return must('gh', [...args, '--repo', slug(repo)], { cwd: repo.root, input });
|
|
30
32
|
}
|
|
31
33
|
export async function defaultBranch(repo) {
|
|
32
|
-
const raw = await repeatable([
|
|
34
|
+
const raw = await repeatable(['repo', 'view', slug(repo), '--json', 'defaultBranchRef'], {
|
|
33
35
|
cwd: repo.root,
|
|
34
36
|
});
|
|
35
37
|
return JSON.parse(raw).defaultBranchRef.name;
|
|
36
38
|
}
|
|
37
|
-
const ISSUE_FIELDS =
|
|
39
|
+
const ISSUE_FIELDS = 'number,title,body,createdAt,labels';
|
|
38
40
|
function toIssue(item) {
|
|
39
41
|
return {
|
|
40
42
|
number: item.number,
|
|
41
43
|
title: item.title,
|
|
42
|
-
body: item.body ??
|
|
44
|
+
body: item.body ?? '',
|
|
43
45
|
createdAt: item.createdAt,
|
|
44
46
|
labels: item.labels.map((label) => label.name),
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
export async function openIssues(repo, limit, labels) {
|
|
48
|
-
const filter = labels.length === 0 ?
|
|
50
|
+
const filter = labels.length === 0 ? '' : ` label:${labels.map((label) => `"${label}"`).join(',')}`;
|
|
49
51
|
const raw = await gh(repo, [
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
'issue',
|
|
53
|
+
'list',
|
|
54
|
+
'--state',
|
|
55
|
+
'open',
|
|
56
|
+
'--search',
|
|
55
57
|
`sort:created-asc${filter}`,
|
|
56
|
-
|
|
58
|
+
'--limit',
|
|
57
59
|
String(limit),
|
|
58
|
-
|
|
60
|
+
'--json',
|
|
59
61
|
ISSUE_FIELDS,
|
|
60
62
|
]);
|
|
61
63
|
const parsed = JSON.parse(raw);
|
|
62
64
|
return parsed.map(toIssue).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
63
65
|
}
|
|
64
66
|
export async function viewIssue(repo, issue) {
|
|
65
|
-
const raw = await gh(repo, [
|
|
67
|
+
const raw = await gh(repo, ['issue', 'view', String(issue), '--json', `${ISSUE_FIELDS},state`]);
|
|
66
68
|
const parsed = JSON.parse(raw);
|
|
67
|
-
return { ...toIssue(parsed), open: parsed.state ===
|
|
69
|
+
return { ...toIssue(parsed), open: parsed.state === 'OPEN' };
|
|
68
70
|
}
|
|
69
71
|
export async function issueComments(repo, issue) {
|
|
70
|
-
const raw = await gh(repo, [
|
|
72
|
+
const raw = await gh(repo, ['issue', 'view', String(issue), '--json', 'comments']);
|
|
71
73
|
const parsed = JSON.parse(raw);
|
|
72
74
|
return parsed.comments.map((comment) => `@${comment.author.login}: ${comment.body}`);
|
|
73
75
|
}
|
|
74
76
|
export async function openBlockers(repo, issue) {
|
|
75
77
|
const raw = await repeatable([
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
'api',
|
|
79
|
+
'--paginate',
|
|
78
80
|
`repos/${slug(repo)}/issues/${issue}/dependencies/blocked_by`,
|
|
79
|
-
|
|
81
|
+
'--jq',
|
|
80
82
|
'.[] | select(.state == "open") | (.repository_url | split("/") | .[-2:] | join("/")) + "#" + (.number | tostring)',
|
|
81
83
|
], { cwd: repo.root });
|
|
82
84
|
return raw
|
|
83
|
-
.split(
|
|
85
|
+
.split('\n')
|
|
84
86
|
.filter((line) => line.length > 0)
|
|
85
87
|
.map((ref) => (ref.startsWith(`${slug(repo)}#`) ? ref.slice(slug(repo).length) : ref));
|
|
86
88
|
}
|
|
87
89
|
async function labelsOf(repo, kind, number) {
|
|
88
|
-
const raw = await gh(repo, [kind,
|
|
90
|
+
const raw = await gh(repo, [kind, 'view', String(number), '--json', 'labels']);
|
|
89
91
|
const parsed = JSON.parse(raw);
|
|
90
92
|
return parsed.labels.map((label) => label.name);
|
|
91
93
|
}
|
|
92
94
|
export function labelArgs(add, remove, current) {
|
|
93
|
-
const args = [
|
|
95
|
+
const args = ['--add-label', add];
|
|
94
96
|
for (const name of remove.filter((item) => item !== add && current.includes(item))) {
|
|
95
|
-
args.push(
|
|
97
|
+
args.push('--remove-label', name);
|
|
96
98
|
}
|
|
97
99
|
return args;
|
|
98
100
|
}
|
|
99
101
|
export async function setLabel(repo, kind, number, add, remove) {
|
|
100
102
|
await ensureLabel(repo, add);
|
|
101
103
|
const current = await labelsOf(repo, kind, number);
|
|
102
|
-
await gh(repo, [kind,
|
|
104
|
+
await gh(repo, [kind, 'edit', String(number), ...labelArgs(add, remove, current)]);
|
|
103
105
|
}
|
|
104
106
|
async function ensureLabel(repo, label) {
|
|
105
107
|
let known = KNOWN_LABELS.get(slug(repo));
|
|
106
108
|
if (known === undefined) {
|
|
107
|
-
const raw = await gh(repo, [
|
|
109
|
+
const raw = await gh(repo, ['label', 'list', '--limit', '1000', '--json', 'name']);
|
|
108
110
|
known = new Set(JSON.parse(raw).map((item) => item.name));
|
|
109
111
|
KNOWN_LABELS.set(slug(repo), known);
|
|
110
112
|
}
|
|
111
113
|
if (known.has(label)) {
|
|
112
114
|
return;
|
|
113
115
|
}
|
|
114
|
-
const result = await run(
|
|
116
|
+
const result = await run('gh', ['label', 'create', label, '--repo', slug(repo)], {
|
|
115
117
|
cwd: repo.root,
|
|
116
118
|
});
|
|
117
119
|
if (result.code === 0) {
|
|
@@ -119,23 +121,32 @@ async function ensureLabel(repo, label) {
|
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
123
|
export async function findPr(repo, branch) {
|
|
122
|
-
const raw = await gh(repo, [
|
|
124
|
+
const raw = await gh(repo, [
|
|
125
|
+
'pr',
|
|
126
|
+
'list',
|
|
127
|
+
'--head',
|
|
128
|
+
branch,
|
|
129
|
+
'--state',
|
|
130
|
+
'open',
|
|
131
|
+
'--json',
|
|
132
|
+
'number',
|
|
133
|
+
]);
|
|
123
134
|
const parsed = JSON.parse(raw);
|
|
124
135
|
return parsed[0]?.number;
|
|
125
136
|
}
|
|
126
137
|
export async function createPr(repo, branch, base, title, body, draft) {
|
|
127
138
|
const created = await ghOnce(repo, [
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
'pr',
|
|
140
|
+
'create',
|
|
141
|
+
'--head',
|
|
131
142
|
branch,
|
|
132
|
-
|
|
143
|
+
'--base',
|
|
133
144
|
base,
|
|
134
|
-
|
|
145
|
+
'--title',
|
|
135
146
|
title,
|
|
136
|
-
|
|
147
|
+
'--body',
|
|
137
148
|
body,
|
|
138
|
-
...(draft ? [
|
|
149
|
+
...(draft ? ['--draft'] : []),
|
|
139
150
|
]);
|
|
140
151
|
const number = prNumber(created) ?? (await findPr(repo, branch));
|
|
141
152
|
if (number === undefined) {
|
|
@@ -148,34 +159,44 @@ export function prNumber(output) {
|
|
|
148
159
|
return match === null ? undefined : Number(match[1]);
|
|
149
160
|
}
|
|
150
161
|
async function prHead(repo, pr) {
|
|
151
|
-
return gh(repo, [
|
|
162
|
+
return gh(repo, ['pr', 'view', String(pr), '--json', 'headRefOid', '--jq', '.headRefOid']);
|
|
152
163
|
}
|
|
153
164
|
async function readChecks(repo, pr) {
|
|
154
|
-
const result = await run(
|
|
165
|
+
const result = await run('gh', ['pr', 'checks', String(pr), '--json', 'name,bucket,link,description', '--repo', slug(repo)], { cwd: repo.root });
|
|
155
166
|
const output = result.stdout.trim();
|
|
156
167
|
if (output.length === 0) {
|
|
157
168
|
if (NO_CHECKS.test(result.stderr)) {
|
|
158
|
-
return
|
|
169
|
+
return 'none';
|
|
159
170
|
}
|
|
160
171
|
throw new Error(`gh pr checks ${pr} failed with code ${result.code}: ${result.stderr.trim()}`);
|
|
161
172
|
}
|
|
162
173
|
const checks = JSON.parse(output);
|
|
163
|
-
return checks.length === 0 ?
|
|
174
|
+
return checks.length === 0 ? 'none' : checks;
|
|
164
175
|
}
|
|
165
176
|
async function watchOnce(repo, pr, intervalSeconds, timeoutMs, show) {
|
|
166
|
-
const watch = await run(
|
|
177
|
+
const watch = await run('gh', [
|
|
178
|
+
'pr',
|
|
179
|
+
'checks',
|
|
180
|
+
String(pr),
|
|
181
|
+
'--watch',
|
|
182
|
+
'--fail-fast',
|
|
183
|
+
'--interval',
|
|
184
|
+
String(intervalSeconds),
|
|
185
|
+
'--repo',
|
|
186
|
+
slug(repo),
|
|
187
|
+
], { cwd: repo.root, show, timeoutMs });
|
|
167
188
|
if (watch.timedOut) {
|
|
168
|
-
return
|
|
189
|
+
return 'timeout';
|
|
169
190
|
}
|
|
170
191
|
if (watch.code === 0) {
|
|
171
|
-
return
|
|
192
|
+
return 'pass';
|
|
172
193
|
}
|
|
173
194
|
if (watch.code === CHECK_PENDING_CODE) {
|
|
174
|
-
return
|
|
195
|
+
return 'pending';
|
|
175
196
|
}
|
|
176
197
|
const checks = await readChecks(repo, pr);
|
|
177
|
-
if (checks !==
|
|
178
|
-
return
|
|
198
|
+
if (checks !== 'none' && checks.some((check) => check.bucket === 'fail')) {
|
|
199
|
+
return 'fail';
|
|
179
200
|
}
|
|
180
201
|
throw new Error(`gh pr checks ${pr} --watch failed with code ${watch.code} and no check failed: ${watch.stderr.trim()}`);
|
|
181
202
|
}
|
|
@@ -190,7 +211,7 @@ export async function pollChecks(probe, options, clock = REAL_CLOCK) {
|
|
|
190
211
|
let failures = 0;
|
|
191
212
|
for (;;) {
|
|
192
213
|
if (rest() === 0) {
|
|
193
|
-
return
|
|
214
|
+
return 'timeout';
|
|
194
215
|
}
|
|
195
216
|
const state = await read(probe, options.head, rest(), () => (shown ??= clock.now()));
|
|
196
217
|
if (state instanceof Error) {
|
|
@@ -201,10 +222,10 @@ export async function pollChecks(probe, options, clock = REAL_CLOCK) {
|
|
|
201
222
|
}
|
|
202
223
|
else {
|
|
203
224
|
failures = 0;
|
|
204
|
-
if (state ===
|
|
205
|
-
return
|
|
225
|
+
if (state === 'none' && clock.now() - (shown ?? 0) >= options.graceMs) {
|
|
226
|
+
return 'none';
|
|
206
227
|
}
|
|
207
|
-
if (state !==
|
|
228
|
+
if (state !== 'none' && state !== 'behind' && state !== 'pending') {
|
|
208
229
|
return state;
|
|
209
230
|
}
|
|
210
231
|
}
|
|
@@ -214,11 +235,11 @@ export async function pollChecks(probe, options, clock = REAL_CLOCK) {
|
|
|
214
235
|
async function read(probe, head, timeoutMs, shown) {
|
|
215
236
|
try {
|
|
216
237
|
if (head !== undefined && (await probe.head()) !== head) {
|
|
217
|
-
return
|
|
238
|
+
return 'behind';
|
|
218
239
|
}
|
|
219
240
|
shown();
|
|
220
|
-
if ((await probe.list()) ===
|
|
221
|
-
return
|
|
241
|
+
if ((await probe.list()) === 'none') {
|
|
242
|
+
return 'none';
|
|
222
243
|
}
|
|
223
244
|
return await probe.watch(timeoutMs);
|
|
224
245
|
}
|
|
@@ -229,17 +250,17 @@ async function read(probe, head, timeoutMs, shown) {
|
|
|
229
250
|
export function waitForChecks(repo, pr, options) {
|
|
230
251
|
return pollChecks({
|
|
231
252
|
head: () => prHead(repo, pr),
|
|
232
|
-
list: async () => ((await readChecks(repo, pr)) ===
|
|
253
|
+
list: async () => ((await readChecks(repo, pr)) === 'none' ? 'none' : 'some'),
|
|
233
254
|
watch: (timeoutMs) => watchOnce(repo, pr, options.intervalSeconds, timeoutMs, options.show),
|
|
234
255
|
}, options);
|
|
235
256
|
}
|
|
236
257
|
export async function failedCheckLogs(repo, pr, maxChars) {
|
|
237
258
|
const checks = await readChecks(repo, pr);
|
|
238
|
-
if (checks ===
|
|
239
|
-
return
|
|
259
|
+
if (checks === 'none') {
|
|
260
|
+
return 'The checks failed, but gh reports no check for the pull request.';
|
|
240
261
|
}
|
|
241
262
|
const failed = checks
|
|
242
|
-
.filter((check) => check.bucket ===
|
|
263
|
+
.filter((check) => check.bucket === 'fail')
|
|
243
264
|
.map((check) => ({
|
|
244
265
|
heading: `## ${check.name}\n${check.description}`,
|
|
245
266
|
runId: /\/actions\/runs\/(\d+)/.exec(check.link)?.[1],
|
|
@@ -247,25 +268,32 @@ export async function failedCheckLogs(repo, pr, maxChars) {
|
|
|
247
268
|
return runLogs(repo, failed, maxChars);
|
|
248
269
|
}
|
|
249
270
|
async function baseRuns(repo, sha) {
|
|
250
|
-
const raw = await gh(repo, [
|
|
271
|
+
const raw = await gh(repo, [
|
|
272
|
+
'run',
|
|
273
|
+
'list',
|
|
274
|
+
'--commit',
|
|
275
|
+
sha,
|
|
276
|
+
'--json',
|
|
277
|
+
'databaseId,name,status,conclusion',
|
|
278
|
+
]);
|
|
251
279
|
return JSON.parse(raw);
|
|
252
280
|
}
|
|
253
281
|
function baseState(runs) {
|
|
254
|
-
if (runs.some((entry) => entry.status ===
|
|
255
|
-
return
|
|
282
|
+
if (runs.some((entry) => entry.status === 'completed' && FAILED_CONCLUSIONS.has(entry.conclusion))) {
|
|
283
|
+
return 'fail';
|
|
256
284
|
}
|
|
257
|
-
return runs.every((entry) => entry.status ===
|
|
285
|
+
return runs.every((entry) => entry.status === 'completed') ? 'pass' : 'pending';
|
|
258
286
|
}
|
|
259
287
|
export function waitForBaseChecks(repo, sha, options) {
|
|
260
288
|
return pollChecks({
|
|
261
289
|
head: async () => sha,
|
|
262
|
-
list: async () => ((await baseRuns(repo, sha)).length === 0 ?
|
|
290
|
+
list: async () => ((await baseRuns(repo, sha)).length === 0 ? 'none' : 'some'),
|
|
263
291
|
watch: async () => baseState(await baseRuns(repo, sha)),
|
|
264
292
|
}, options);
|
|
265
293
|
}
|
|
266
294
|
export async function failedBaseLogs(repo, sha, maxChars) {
|
|
267
295
|
const failed = (await baseRuns(repo, sha))
|
|
268
|
-
.filter((entry) => entry.status ===
|
|
296
|
+
.filter((entry) => entry.status === 'completed' && FAILED_CONCLUSIONS.has(entry.conclusion))
|
|
269
297
|
.map((entry) => ({ heading: `## ${entry.name}`, runId: String(entry.databaseId) }));
|
|
270
298
|
return runLogs(repo, failed, maxChars);
|
|
271
299
|
}
|
|
@@ -277,7 +305,7 @@ async function runLogs(repo, failed, maxChars) {
|
|
|
277
305
|
if (runId === undefined) {
|
|
278
306
|
continue;
|
|
279
307
|
}
|
|
280
|
-
const logs = await run(
|
|
308
|
+
const logs = await run('gh', ['run', 'view', runId, '--log-failed', '--repo', slug(repo)], {
|
|
281
309
|
cwd: repo.root,
|
|
282
310
|
tailChars: share,
|
|
283
311
|
});
|
|
@@ -286,43 +314,44 @@ async function runLogs(repo, failed, maxChars) {
|
|
|
286
314
|
continue;
|
|
287
315
|
}
|
|
288
316
|
const reason = [`The log read failed with code ${logs.code}.`, logs.stderr.trim()]
|
|
289
|
-
.filter((line) => line !==
|
|
290
|
-
.join(
|
|
291
|
-
parts.push([logs.stdout.trim(), reason].filter((line) => line !==
|
|
317
|
+
.filter((line) => line !== '')
|
|
318
|
+
.join(' ');
|
|
319
|
+
parts.push([logs.stdout.trim(), reason].filter((line) => line !== '').join('\n'));
|
|
292
320
|
}
|
|
293
|
-
return parts.join(
|
|
321
|
+
return parts.join('\n\n');
|
|
294
322
|
}
|
|
295
323
|
export async function markPrReady(repo, pr) {
|
|
296
|
-
const result = await run(
|
|
324
|
+
const result = await run('gh', ['pr', 'ready', String(pr), '--repo', slug(repo)], {
|
|
297
325
|
cwd: repo.root,
|
|
298
326
|
});
|
|
299
327
|
return result.code === 0;
|
|
300
328
|
}
|
|
301
329
|
export async function mergePr(repo, pr) {
|
|
302
|
-
const result = await run(
|
|
330
|
+
const result = await run('gh', ['pr', 'merge', String(pr), '--squash', '--delete-branch', '--repo', slug(repo)], { cwd: repo.root });
|
|
303
331
|
return result.code === 0;
|
|
304
332
|
}
|
|
305
333
|
export async function prMerged(repo, pr) {
|
|
306
|
-
return (await gh(repo, [
|
|
334
|
+
return ((await gh(repo, ['pr', 'view', String(pr), '--json', 'state', '--jq', '.state'])).trim() ===
|
|
335
|
+
'MERGED');
|
|
307
336
|
}
|
|
308
337
|
export async function mergeCommit(repo, pr, options, clock = REAL_CLOCK) {
|
|
309
338
|
const deadline = clock.now() + options.timeoutMs;
|
|
310
339
|
for (;;) {
|
|
311
|
-
const raw = await gh(repo, [
|
|
340
|
+
const raw = await gh(repo, ['pr', 'view', String(pr), '--json', 'state,mergeCommit']);
|
|
312
341
|
const view = JSON.parse(raw);
|
|
313
|
-
if (view.state ===
|
|
342
|
+
if (view.state === 'MERGED' && view.mergeCommit?.oid) {
|
|
314
343
|
return view.mergeCommit.oid;
|
|
315
344
|
}
|
|
316
345
|
const rest = deadline - clock.now();
|
|
317
|
-
if (view.state ===
|
|
346
|
+
if (view.state === 'CLOSED' || rest <= 0) {
|
|
318
347
|
return undefined;
|
|
319
348
|
}
|
|
320
349
|
await clock.sleep(Math.min(options.intervalSeconds * 1000, rest));
|
|
321
350
|
}
|
|
322
351
|
}
|
|
323
352
|
export async function reopenIssue(repo, issue) {
|
|
324
|
-
await gh(repo, [
|
|
353
|
+
await gh(repo, ['issue', 'reopen', String(issue)]);
|
|
325
354
|
}
|
|
326
355
|
export async function commentOnPr(repo, pr, body) {
|
|
327
|
-
await ghOnce(repo, [
|
|
356
|
+
await ghOnce(repo, ['pr', 'comment', String(pr), '--body-file', '-'], body);
|
|
328
357
|
}
|
package/dist/interrupt.mjs
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import { stopChildren } from
|
|
1
|
+
import { stopChildren } from './exec.mjs';
|
|
2
2
|
export function listenForInterrupts(say) {
|
|
3
3
|
let presses = 0;
|
|
4
4
|
let wake;
|
|
5
5
|
const press = () => {
|
|
6
6
|
presses += 1;
|
|
7
7
|
if (presses === 1 && wake !== undefined) {
|
|
8
|
-
say(
|
|
8
|
+
say('Stopping now.');
|
|
9
9
|
wake();
|
|
10
10
|
}
|
|
11
11
|
else if (presses === 1) {
|
|
12
|
-
say(
|
|
12
|
+
say('Stopping after this issue. Press Ctrl-C again to stop after this round.');
|
|
13
13
|
}
|
|
14
14
|
else if (presses === 2) {
|
|
15
|
-
say(
|
|
15
|
+
say('Stopping after this round. Press Ctrl-C again to stop now.');
|
|
16
16
|
}
|
|
17
17
|
else {
|
|
18
|
-
process.off(
|
|
19
|
-
stopChildren(
|
|
18
|
+
process.off('SIGINT', press);
|
|
19
|
+
stopChildren('SIGINT');
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
|
-
process.on(
|
|
22
|
+
process.on('SIGINT', press);
|
|
23
23
|
return {
|
|
24
24
|
afterIssue: () => presses >= 1,
|
|
25
25
|
afterRound: () => presses >= 2,
|
package/dist/loop.mjs
CHANGED
|
@@ -20,7 +20,7 @@ export async function runIssues(args, loop) {
|
|
|
20
20
|
if (loop.stop() || report.halt === true) {
|
|
21
21
|
return { reports, stoppedAt: issue.number };
|
|
22
22
|
}
|
|
23
|
-
if (args.once && report.outcome !==
|
|
23
|
+
if (args.once && report.outcome !== 'skipped') {
|
|
24
24
|
break;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -51,8 +51,10 @@ export async function watchIssues(args, watch) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
export function exitCode(reports) {
|
|
54
|
-
return reports.some((report) => report.outcome ===
|
|
54
|
+
return reports.some((report) => report.outcome === 'needs-human' || report.outcome === 'error')
|
|
55
|
+
? 1
|
|
56
|
+
: 0;
|
|
55
57
|
}
|
|
56
58
|
function handled(reports) {
|
|
57
|
-
return reports.filter((report) => report.outcome !==
|
|
59
|
+
return reports.filter((report) => report.outcome !== 'skipped').length;
|
|
58
60
|
}
|
package/dist/observe.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { createWriteStream } from
|
|
2
|
-
import { mkdir, readdir, rm, stat } from
|
|
3
|
-
import { join } from
|
|
4
|
-
import { STATE_DIR } from
|
|
1
|
+
import { createWriteStream } from 'node:fs';
|
|
2
|
+
import { mkdir, readdir, rm, stat } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { STATE_DIR } from './state.mjs';
|
|
5
5
|
const VISIBILITY = { quiet: 0, normal: 1, verbose: 2 };
|
|
6
6
|
export function emptyUsage() {
|
|
7
7
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
@@ -63,11 +63,11 @@ export class Recorder {
|
|
|
63
63
|
this.runId = runId;
|
|
64
64
|
}
|
|
65
65
|
static async create(root, level, listener) {
|
|
66
|
-
const runId = new Date().toISOString().replace(/[:.]/g,
|
|
67
|
-
const dir = join(root, STATE_DIR,
|
|
66
|
+
const runId = new Date().toISOString().replace(/[:.]/g, '-');
|
|
67
|
+
const dir = join(root, STATE_DIR, 'runs');
|
|
68
68
|
await mkdir(dir, { recursive: true });
|
|
69
69
|
const file = join(dir, `${runId}.jsonl`);
|
|
70
|
-
const sink = new Sink(createWriteStream(file, { flags:
|
|
70
|
+
const sink = new Sink(createWriteStream(file, { flags: 'a' }), level, file, listener);
|
|
71
71
|
return new Recorder(sink, {}, runId);
|
|
72
72
|
}
|
|
73
73
|
get logFile() {
|
|
@@ -79,7 +79,7 @@ export class Recorder {
|
|
|
79
79
|
scope(fields) {
|
|
80
80
|
return new Recorder(this.sink, { ...this.base, ...fields }, this.runId);
|
|
81
81
|
}
|
|
82
|
-
event(kind, fields = {}, visibility =
|
|
82
|
+
event(kind, fields = {}, visibility = 'normal') {
|
|
83
83
|
this.sink.count(kind);
|
|
84
84
|
const record = {
|
|
85
85
|
ts: new Date().toISOString(),
|
|
@@ -95,18 +95,18 @@ export class Recorder {
|
|
|
95
95
|
}
|
|
96
96
|
usage(role, usage, ms, fields = {}) {
|
|
97
97
|
this.sink.add(role, usage, ms);
|
|
98
|
-
this.event(
|
|
98
|
+
this.event('usage', { role, ms, ...usage, ...fields }, 'normal');
|
|
99
99
|
}
|
|
100
100
|
async step(kind, fields, run) {
|
|
101
101
|
const started = Date.now();
|
|
102
|
-
this.event(`${kind}.start`, fields,
|
|
102
|
+
this.event(`${kind}.start`, fields, 'verbose');
|
|
103
103
|
try {
|
|
104
104
|
const result = await run();
|
|
105
105
|
this.event(`${kind}.end`, { ...fields, ms: Date.now() - started });
|
|
106
106
|
return result;
|
|
107
107
|
}
|
|
108
108
|
catch (error) {
|
|
109
|
-
this.event(`${kind}.error`, { ...fields, ms: Date.now() - started, error: message(error) },
|
|
109
|
+
this.event(`${kind}.error`, { ...fields, ms: Date.now() - started, error: message(error) }, 'quiet');
|
|
110
110
|
throw error;
|
|
111
111
|
}
|
|
112
112
|
}
|
|
@@ -140,7 +140,7 @@ export async function pruneRuns(root, keepDays) {
|
|
|
140
140
|
if (keepDays <= 0) {
|
|
141
141
|
return 0;
|
|
142
142
|
}
|
|
143
|
-
const dir = join(root, STATE_DIR,
|
|
143
|
+
const dir = join(root, STATE_DIR, 'runs');
|
|
144
144
|
let names;
|
|
145
145
|
try {
|
|
146
146
|
names = await readdir(dir);
|
|
@@ -170,20 +170,20 @@ export function message(error) {
|
|
|
170
170
|
}
|
|
171
171
|
function render(record) {
|
|
172
172
|
const time = new Date(String(record.ts)).toTimeString().slice(0, 8);
|
|
173
|
-
const issue = record.issue === undefined ?
|
|
173
|
+
const issue = record.issue === undefined ? '' : ` #${record.issue}`;
|
|
174
174
|
const extras = Object.entries(record)
|
|
175
|
-
.filter(([key]) => ![
|
|
175
|
+
.filter(([key]) => !['ts', 'runId', 'kind', 'issue'].includes(key))
|
|
176
176
|
.map(([key, value]) => `${key}=${short(value)}`)
|
|
177
|
-
.join(
|
|
178
|
-
return `${time}${issue} ${String(record.kind)}${extras.length > 0 ? ` ${extras}` :
|
|
177
|
+
.join(' ');
|
|
178
|
+
return `${time}${issue} ${String(record.kind)}${extras.length > 0 ? ` ${extras}` : ''}\n`;
|
|
179
179
|
}
|
|
180
180
|
function short(value) {
|
|
181
181
|
if (value === undefined || value === null) {
|
|
182
|
-
return
|
|
182
|
+
return '-';
|
|
183
183
|
}
|
|
184
|
-
if (typeof value ===
|
|
184
|
+
if (typeof value === 'number') {
|
|
185
185
|
return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(6)));
|
|
186
186
|
}
|
|
187
|
-
const text = String(value).replace(/\s+/g,
|
|
187
|
+
const text = String(value).replace(/\s+/g, ' ');
|
|
188
188
|
return text.length > 120 ? `${text.slice(0, 117)}...` : text;
|
|
189
189
|
}
|