next-issue 2.4.0 → 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 +110 -79
- package/dist/interrupt.mjs +7 -7
- package/dist/loop.mjs +5 -3
- package/dist/observe.mjs +19 -19
- package/dist/pipeline.mjs +138 -116
- 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/exec.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
2
|
let observer;
|
|
3
3
|
let defaultTimeoutMs = 10 * 60_000;
|
|
4
4
|
const STDERR_TAIL = 200_000;
|
|
@@ -20,16 +20,16 @@ export function stopChildren(signal) {
|
|
|
20
20
|
catch { }
|
|
21
21
|
}
|
|
22
22
|
groups.clear();
|
|
23
|
-
process.off(
|
|
23
|
+
process.off('SIGTERM', onTerminate);
|
|
24
24
|
process.kill(process.pid, signal);
|
|
25
25
|
}
|
|
26
|
-
const onTerminate = () => stopChildren(
|
|
26
|
+
const onTerminate = () => stopChildren('SIGTERM');
|
|
27
27
|
function watch(pid) {
|
|
28
28
|
if (pid === undefined) {
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
31
|
if (groups.size === 0) {
|
|
32
|
-
process.on(
|
|
32
|
+
process.on('SIGTERM', onTerminate);
|
|
33
33
|
}
|
|
34
34
|
groups.add(pid);
|
|
35
35
|
}
|
|
@@ -39,13 +39,13 @@ function unwatch(pid) {
|
|
|
39
39
|
}
|
|
40
40
|
groups.delete(pid);
|
|
41
41
|
if (groups.size === 0) {
|
|
42
|
-
process.off(
|
|
42
|
+
process.off('SIGTERM', onTerminate);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
export function track(child) {
|
|
46
46
|
watch(child.pid);
|
|
47
|
-
child.on(
|
|
48
|
-
child.on(
|
|
47
|
+
child.on('close', () => unwatch(child.pid));
|
|
48
|
+
child.on('error', () => unwatch(child.pid));
|
|
49
49
|
}
|
|
50
50
|
export function setCommandObserver(next) {
|
|
51
51
|
observer = next;
|
|
@@ -61,19 +61,19 @@ export function run(command, args, options = {}) {
|
|
|
61
61
|
cwd: options.cwd,
|
|
62
62
|
detached: true,
|
|
63
63
|
stdio: [
|
|
64
|
-
options.input === undefined ?
|
|
65
|
-
options.show === true ? 2 :
|
|
66
|
-
|
|
64
|
+
options.input === undefined ? 'ignore' : 'pipe',
|
|
65
|
+
options.show === true ? 2 : 'pipe',
|
|
66
|
+
'pipe',
|
|
67
67
|
],
|
|
68
68
|
});
|
|
69
69
|
const pid = child.pid;
|
|
70
70
|
watch(pid);
|
|
71
|
-
let stdout =
|
|
72
|
-
let stderr =
|
|
71
|
+
let stdout = '';
|
|
72
|
+
let stderr = '';
|
|
73
73
|
let timedOut = false;
|
|
74
|
-
child.stdout?.setEncoding(
|
|
75
|
-
child.stderr?.setEncoding(
|
|
76
|
-
child.stdout?.on(
|
|
74
|
+
child.stdout?.setEncoding('utf8');
|
|
75
|
+
child.stderr?.setEncoding('utf8');
|
|
76
|
+
child.stdout?.on('data', (chunk) => {
|
|
77
77
|
options.onStdout?.(chunk);
|
|
78
78
|
stdout += chunk;
|
|
79
79
|
if (options.tailChars !== undefined && stdout.length > options.tailChars) {
|
|
@@ -81,7 +81,7 @@ export function run(command, args, options = {}) {
|
|
|
81
81
|
}
|
|
82
82
|
});
|
|
83
83
|
const stderrLimit = options.tailChars ?? STDERR_TAIL;
|
|
84
|
-
child.stderr?.on(
|
|
84
|
+
child.stderr?.on('data', (chunk) => {
|
|
85
85
|
stderr += chunk;
|
|
86
86
|
if (stderr.length > stderrLimit) {
|
|
87
87
|
stderr = stderr.slice(-stderrLimit);
|
|
@@ -90,14 +90,14 @@ export function run(command, args, options = {}) {
|
|
|
90
90
|
const timer = setTimeout(() => {
|
|
91
91
|
timedOut = true;
|
|
92
92
|
if (pid === undefined) {
|
|
93
|
-
child.kill(
|
|
93
|
+
child.kill('SIGKILL');
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
96
|
try {
|
|
97
|
-
process.kill(-pid,
|
|
97
|
+
process.kill(-pid, 'SIGKILL');
|
|
98
98
|
}
|
|
99
99
|
catch {
|
|
100
|
-
child.kill(
|
|
100
|
+
child.kill('SIGKILL');
|
|
101
101
|
}
|
|
102
102
|
}, limit);
|
|
103
103
|
let done = false;
|
|
@@ -115,18 +115,18 @@ export function run(command, args, options = {}) {
|
|
|
115
115
|
cwd: options.cwd,
|
|
116
116
|
code: result.code,
|
|
117
117
|
ms: Date.now() - started,
|
|
118
|
-
stderr: result.code === 0 ?
|
|
118
|
+
stderr: result.code === 0 ? '' : stderr.trim(),
|
|
119
119
|
timedOut,
|
|
120
120
|
});
|
|
121
121
|
resolve(result);
|
|
122
122
|
};
|
|
123
|
-
child.on(
|
|
123
|
+
child.on('error', (error) => {
|
|
124
124
|
clearTimeout(timer);
|
|
125
125
|
unwatch(pid);
|
|
126
126
|
done = true;
|
|
127
127
|
reject(error);
|
|
128
128
|
});
|
|
129
|
-
child.on(
|
|
129
|
+
child.on('exit', (code) => {
|
|
130
130
|
if (!timedOut) {
|
|
131
131
|
return;
|
|
132
132
|
}
|
|
@@ -134,9 +134,9 @@ export function run(command, args, options = {}) {
|
|
|
134
134
|
child.stderr?.destroy();
|
|
135
135
|
settle(code);
|
|
136
136
|
});
|
|
137
|
-
child.on(
|
|
137
|
+
child.on('close', settle);
|
|
138
138
|
if (options.input !== undefined) {
|
|
139
|
-
child.stdin?.on(
|
|
139
|
+
child.stdin?.on('error', () => undefined);
|
|
140
140
|
child.stdin?.end(options.input);
|
|
141
141
|
}
|
|
142
142
|
});
|
|
@@ -144,10 +144,10 @@ export function run(command, args, options = {}) {
|
|
|
144
144
|
export async function must(command, args, options = {}) {
|
|
145
145
|
const result = await run(command, args, options);
|
|
146
146
|
if (result.timedOut) {
|
|
147
|
-
throw new Error(`${command} ${args.join(
|
|
147
|
+
throw new Error(`${command} ${args.join(' ')} did not finish in time.`);
|
|
148
148
|
}
|
|
149
149
|
if (result.code !== 0) {
|
|
150
|
-
throw new Error(`${command} ${args.join(
|
|
150
|
+
throw new Error(`${command} ${args.join(' ')} failed with code ${result.code}: ${result.stderr.trim()}`);
|
|
151
151
|
}
|
|
152
152
|
return result.stdout.trim();
|
|
153
153
|
}
|
package/dist/git.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { appendFile, readFile } from
|
|
2
|
-
import { join } from
|
|
3
|
-
import { must, run } from
|
|
1
|
+
import { appendFile, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { must, run } from './exec.mjs';
|
|
4
4
|
const REMOTE_PATTERNS = [
|
|
5
5
|
/^git@[^:]+:(?<owner>[^/]+)\/(?<name>[^/]+?)(?:\.git)?$/,
|
|
6
6
|
/^ssh:\/\/[^/]+\/(?<owner>[^/]+)\/(?<name>[^/]+?)(?:\.git)?$/,
|
|
@@ -16,74 +16,74 @@ export function parseRemote(url) {
|
|
|
16
16
|
throw new Error(`Cannot parse the git remote URL: ${url}`);
|
|
17
17
|
}
|
|
18
18
|
export async function repoRoot(cwd) {
|
|
19
|
-
return must(
|
|
19
|
+
return must('git', ['rev-parse', '--show-toplevel'], { cwd });
|
|
20
20
|
}
|
|
21
21
|
export async function detectRepo(cwd, remote) {
|
|
22
22
|
const root = await repoRoot(cwd);
|
|
23
|
-
const url = await must(
|
|
23
|
+
const url = await must('git', ['remote', 'get-url', remote], { cwd });
|
|
24
24
|
const { owner, name } = parseRemote(url);
|
|
25
25
|
return { owner, name, root };
|
|
26
26
|
}
|
|
27
|
-
export const WORKTREE_DIR =
|
|
27
|
+
export const WORKTREE_DIR = '.worktrees';
|
|
28
28
|
export function worktreePath(repo, issue) {
|
|
29
29
|
return join(repo.root, WORKTREE_DIR, String(issue));
|
|
30
30
|
}
|
|
31
31
|
export async function addWorktree(repo, issue, branch, base, remote) {
|
|
32
32
|
const upstream = `${remote}/${branch}`;
|
|
33
|
-
await must(
|
|
33
|
+
await must('git', ['fetch', remote], { cwd: repo.root });
|
|
34
34
|
const baseUpdated = await updateBase(repo, base, remote);
|
|
35
|
-
await must(
|
|
36
|
-
const pushed = await hasBranch(repo, upstream,
|
|
35
|
+
await must('git', ['worktree', 'prune'], { cwd: repo.root });
|
|
36
|
+
const pushed = await hasBranch(repo, upstream, '--remotes');
|
|
37
37
|
const existing = await findWorktree(repo, branch);
|
|
38
38
|
const path = existing ?? worktreePath(repo, issue);
|
|
39
39
|
if (existing === undefined) {
|
|
40
40
|
const args = (await hasBranch(repo, branch))
|
|
41
|
-
? [
|
|
42
|
-
: [
|
|
43
|
-
await must(
|
|
41
|
+
? ['worktree', 'add', path, branch]
|
|
42
|
+
: ['worktree', 'add', '-b', branch, path, pushed ? upstream : `${remote}/${base}`];
|
|
43
|
+
await must('git', args, { cwd: repo.root });
|
|
44
44
|
}
|
|
45
45
|
if (pushed) {
|
|
46
|
-
await run(
|
|
46
|
+
await run('git', ['merge', '--ff-only', upstream], { cwd: path });
|
|
47
47
|
}
|
|
48
48
|
return { path, baseUpdated };
|
|
49
49
|
}
|
|
50
50
|
async function hasBranch(repo, name, ...flags) {
|
|
51
|
-
const list = await must(
|
|
51
|
+
const list = await must('git', ['branch', '--list', ...flags, name], { cwd: repo.root });
|
|
52
52
|
return list.length > 0;
|
|
53
53
|
}
|
|
54
54
|
export async function updateBase(repo, base, remote) {
|
|
55
|
-
const head = await run(
|
|
55
|
+
const head = await run('git', ['symbolic-ref', '--quiet', '--short', 'HEAD'], { cwd: repo.root });
|
|
56
56
|
const args = head.code === 0 && head.stdout.trim() === base
|
|
57
|
-
? [
|
|
58
|
-
: [
|
|
59
|
-
const result = await run(
|
|
57
|
+
? ['merge', '--ff-only', `${remote}/${base}`]
|
|
58
|
+
: ['fetch', '.', `${remote}/${base}:${base}`];
|
|
59
|
+
const result = await run('git', args, { cwd: repo.root });
|
|
60
60
|
return result.code === 0;
|
|
61
61
|
}
|
|
62
62
|
export async function findWorktree(repo, branch) {
|
|
63
|
-
const list = await must(
|
|
63
|
+
const list = await must('git', ['worktree', 'list', '--porcelain'], { cwd: repo.root });
|
|
64
64
|
const entry = list
|
|
65
|
-
.split(
|
|
66
|
-
.map((block) => block.split(
|
|
65
|
+
.split('\n\n')
|
|
66
|
+
.map((block) => block.split('\n'))
|
|
67
67
|
.find((lines) => lines.includes(`branch refs/heads/${branch}`));
|
|
68
|
-
return entry?.find((line) => line.startsWith(
|
|
68
|
+
return entry?.find((line) => line.startsWith('worktree '))?.slice('worktree '.length);
|
|
69
69
|
}
|
|
70
70
|
export async function removeWorktree(repo, path) {
|
|
71
|
-
const result = await run(
|
|
71
|
+
const result = await run('git', ['worktree', 'remove', path], { cwd: repo.root });
|
|
72
72
|
return result.code === 0;
|
|
73
73
|
}
|
|
74
74
|
export async function ensureIgnored(root, entry) {
|
|
75
|
-
const path = join(root,
|
|
76
|
-
let current =
|
|
75
|
+
const path = join(root, '.gitignore');
|
|
76
|
+
let current = '';
|
|
77
77
|
try {
|
|
78
|
-
current = await readFile(path,
|
|
78
|
+
current = await readFile(path, 'utf8');
|
|
79
79
|
}
|
|
80
80
|
catch {
|
|
81
|
-
current =
|
|
81
|
+
current = '';
|
|
82
82
|
}
|
|
83
|
-
if (current.split(
|
|
83
|
+
if (current.split('\n').some((line) => line.trim() === entry)) {
|
|
84
84
|
return false;
|
|
85
85
|
}
|
|
86
|
-
const gap = current.length === 0 || current.endsWith(
|
|
86
|
+
const gap = current.length === 0 || current.endsWith('\n') ? '' : '\n';
|
|
87
87
|
await appendFile(path, `${gap}${entry}\n`);
|
|
88
88
|
return true;
|
|
89
89
|
}
|
|
@@ -91,39 +91,39 @@ export function branchName(issue) {
|
|
|
91
91
|
return String(issue);
|
|
92
92
|
}
|
|
93
93
|
export async function isDirty(cwd) {
|
|
94
|
-
const status = await must(
|
|
94
|
+
const status = await must('git', ['status', '--porcelain'], { cwd });
|
|
95
95
|
return status.length > 0;
|
|
96
96
|
}
|
|
97
97
|
export async function diff(cwd, base, remote) {
|
|
98
|
-
return must(
|
|
98
|
+
return must('git', ['diff', `${remote}/${base}...HEAD`], { cwd });
|
|
99
99
|
}
|
|
100
100
|
export async function commitAll(cwd, message) {
|
|
101
101
|
if (!(await isDirty(cwd))) {
|
|
102
102
|
return false;
|
|
103
103
|
}
|
|
104
|
-
await must(
|
|
105
|
-
await must(
|
|
104
|
+
await must('git', ['add', '-A'], { cwd });
|
|
105
|
+
await must('git', ['commit', '-m', message], { cwd });
|
|
106
106
|
return true;
|
|
107
107
|
}
|
|
108
108
|
export async function discardChanges(cwd, target) {
|
|
109
109
|
if (!(await isDirty(cwd)) && (await revision(cwd)) === target) {
|
|
110
110
|
return false;
|
|
111
111
|
}
|
|
112
|
-
await must(
|
|
113
|
-
await must(
|
|
112
|
+
await must('git', ['reset', '--hard', target], { cwd });
|
|
113
|
+
await must('git', ['clean', '-fd'], { cwd });
|
|
114
114
|
return true;
|
|
115
115
|
}
|
|
116
116
|
export async function push(cwd, remote, branch, force = false) {
|
|
117
|
-
await must(
|
|
117
|
+
await must('git', ['push', '-u', ...(force ? ['--force'] : []), remote, branch], { cwd });
|
|
118
118
|
}
|
|
119
119
|
export async function resetToBase(cwd, base, remote) {
|
|
120
|
-
await must(
|
|
121
|
-
await must(
|
|
120
|
+
await must('git', ['fetch', remote, base], { cwd });
|
|
121
|
+
await must('git', ['reset', '--hard', `${remote}/${base}`], { cwd });
|
|
122
122
|
}
|
|
123
123
|
export async function revision(cwd) {
|
|
124
|
-
return must(
|
|
124
|
+
return must('git', ['rev-parse', 'HEAD'], { cwd });
|
|
125
125
|
}
|
|
126
126
|
export async function deleteBranch(repo, branch) {
|
|
127
|
-
const result = await run(
|
|
127
|
+
const result = await run('git', ['branch', '-D', branch], { cwd: repo.root });
|
|
128
128
|
return result.code === 0;
|
|
129
129
|
}
|