@adecore/agents 0.39.0 → 0.40.1
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/dist/async.d.ts +5 -0
- package/dist/async.js +13 -5
- package/dist/chat/acp-protocol.js +3 -5
- package/dist/chat/chat-core.js +0 -2
- package/dist/chat/chat-process.d.ts +1 -0
- package/dist/chat/chat-process.js +1 -1
- package/dist/chat/chat-session.js +6 -8
- package/dist/chat/claude-protocol.js +1 -6
- package/dist/chat/claude-transcript.js +1 -3
- package/dist/chat/codex-protocol.js +1 -6
- package/dist/chat/projector.js +1 -3
- package/dist/chat/request-summary.js +1 -3
- package/dist/coded-error.js +1 -5
- package/dist/context/argv.js +1 -2
- package/dist/context/verb.js +1 -3
- package/dist/error-text.d.ts +1 -0
- package/dist/error-text.js +4 -0
- package/dist/fields.d.ts +3 -0
- package/dist/fields.js +12 -0
- package/dist/fs.d.ts +1 -0
- package/dist/fs.js +8 -4
- package/dist/lineage.js +11 -5
- package/dist/messages/notice-store.js +3 -4
- package/dist/providers/accounts/acp-account.js +3 -20
- package/dist/providers/accounts/service.d.ts +1 -2
- package/dist/providers/accounts/service.js +5 -9
- package/dist/providers/accounts/status.js +1 -3
- package/dist/providers/acp-agent-state.js +1 -3
- package/dist/providers/claude.js +0 -1
- package/dist/providers/codex-models.json +3 -2
- package/dist/providers/codex.js +0 -1
- package/dist/providers/detect.js +1 -1
- package/dist/providers/shell-commands.js +1 -2
- package/dist/run-process.js +1 -3
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +5 -8
- package/dist/tasks/background-limit.js +3 -2
- package/dist/tasks/task-coordinator.js +11 -17
- package/dist/testing/watch.js +0 -1
- package/dist/usage/limits/monitor.js +1 -2
- package/dist/usage/prices-snapshot.json +8 -0
- package/dist/usage/readers/claude.js +3 -10
- package/dist/usage/readers/codex.js +2 -9
- package/dist/usage/readers/grok.js +3 -10
- package/dist/usage/readers/host-log.js +3 -10
- package/dist/usage/record.d.ts +2 -0
- package/dist/usage/record.js +13 -0
- package/package.json +6 -6
- package/src/async.ts +23 -5
- package/src/chat/acp-backend.ts +121 -0
- package/src/chat/acp-protocol.ts +172 -6
- package/src/chat/attachment-store.ts +21 -0
- package/src/chat/backend.ts +0 -2
- package/src/chat/bookmark-store.ts +23 -0
- package/src/chat/chat-core.ts +208 -7
- package/src/chat/chat-log.ts +19 -0
- package/src/chat/chat-process.ts +52 -1
- package/src/chat/chat-session.ts +401 -10
- package/src/chat/chat-store.ts +50 -0
- package/src/chat/chat-title.ts +9 -0
- package/src/chat/claude-backend.ts +50 -0
- package/src/chat/claude-protocol.ts +155 -8
- package/src/chat/claude-title.ts +10 -0
- package/src/chat/claude-transcript.ts +47 -4
- package/src/chat/codex-backend.ts +131 -0
- package/src/chat/codex-protocol.ts +180 -8
- package/src/chat/codex-thread.ts +23 -0
- package/src/chat/composer-preferences.ts +6 -0
- package/src/chat/context-breakdown.ts +11 -0
- package/src/chat/delta-coalescer.ts +8 -0
- package/src/chat/external-sessions.ts +28 -0
- package/src/chat/generated-image.ts +25 -0
- package/src/chat/index.ts +5 -0
- package/src/chat/input.ts +13 -0
- package/src/chat/json-rpc-transport.ts +28 -0
- package/src/chat/limit-resume.ts +13 -0
- package/src/chat/projector.ts +160 -4
- package/src/chat/readable-text.ts +3 -0
- package/src/chat/request-summary.ts +18 -4
- package/src/chat/subagent-projection.ts +3 -0
- package/src/chat/subagent-reader.ts +76 -0
- package/src/chat/subagent-settlement.ts +24 -0
- package/src/chat/thread-search.ts +13 -0
- package/src/chat/thread.ts +31 -0
- package/src/chat/ui-queries.ts +122 -0
- package/src/chat/ui-replies.ts +34 -0
- package/src/chat/visual-store.ts +49 -0
- package/src/chat/wake-chat.ts +3 -0
- package/src/client-sinks.ts +2 -0
- package/src/coded-error.ts +1 -5
- package/src/context/argv.ts +18 -2
- package/src/context/index.ts +1 -0
- package/src/context/refusal.ts +3 -0
- package/src/context/verb.ts +32 -4
- package/src/coordination/index.ts +10 -0
- package/src/error-text.ts +7 -0
- package/src/fields.ts +14 -0
- package/src/fs.ts +20 -4
- package/src/host/agent-host.ts +20 -0
- package/src/host/environment.ts +6 -0
- package/src/host/handlers.ts +18 -0
- package/src/host/memory-port.ts +4 -0
- package/src/host/wiring.ts +10 -0
- package/src/index.ts +1 -0
- package/src/lineage.ts +24 -5
- package/src/messages/deliver-message.ts +6 -0
- package/src/messages/deliver-notice.ts +13 -0
- package/src/messages/notice-store.ts +16 -4
- package/src/modes.ts +1 -0
- package/src/outbox/outbox-worker.ts +31 -0
- package/src/outbox/outbox.ts +5 -0
- package/src/providers/accounts/accounts.ts +36 -0
- package/src/providers/accounts/acp-account.ts +22 -27
- package/src/providers/accounts/launch.ts +4 -0
- package/src/providers/accounts/service.ts +110 -12
- package/src/providers/accounts/shadow-home.ts +10 -0
- package/src/providers/accounts/status.ts +13 -4
- package/src/providers/accounts/store.ts +25 -0
- package/src/providers/accounts/variables.ts +11 -1
- package/src/providers/acp-agent-state.ts +24 -3
- package/src/providers/acp-provider.ts +5 -0
- package/src/providers/acp-usage-log.ts +2 -0
- package/src/providers/catalog.ts +17 -0
- package/src/providers/claude.ts +10 -1
- package/src/providers/codex-models.json +3 -2
- package/src/providers/codex-rules.ts +3 -0
- package/src/providers/codex.ts +2 -1
- package/src/providers/detect.ts +6 -1
- package/src/providers/provider.ts +1 -1
- package/src/providers/registry.ts +18 -2
- package/src/providers/shell-commands.ts +37 -2
- package/src/record-directory.ts +19 -0
- package/src/run-process.ts +24 -3
- package/src/serializer.ts +3 -0
- package/src/skills.ts +73 -10
- package/src/tasks/background-limit.ts +9 -2
- package/src/tasks/end-children.ts +9 -0
- package/src/tasks/give-task.ts +8 -0
- package/src/tasks/task-coordinator.ts +81 -17
- package/src/tasks/task-store.ts +28 -0
- package/src/tasks/waiting-child.ts +21 -0
- package/src/tasks/wake-parent.ts +26 -0
- package/src/tasks/wiring.ts +22 -0
- package/src/testing/accounts.ts +2 -0
- package/src/testing/fake-acp-capture.ts +16 -0
- package/src/testing/fake-acp.ts +66 -0
- package/src/testing/fake-claude.ts +177 -0
- package/src/testing/fake-cli.ts +29 -0
- package/src/testing/fake-codex.ts +122 -0
- package/src/testing/manual-clock.ts +2 -0
- package/src/testing/watch.ts +7 -1
- package/src/title-file.ts +12 -0
- package/src/usage/accounts.ts +6 -0
- package/src/usage/aggregate.ts +19 -0
- package/src/usage/exchange.ts +12 -0
- package/src/usage/index-file.ts +27 -0
- package/src/usage/limits/monitor.ts +31 -3
- package/src/usage/limits/normalize.ts +30 -0
- package/src/usage/limits/probe.ts +17 -0
- package/src/usage/prices-snapshot.json +8 -0
- package/src/usage/pricing.ts +35 -0
- package/src/usage/projects.ts +8 -0
- package/src/usage/readers/claude.ts +12 -9
- package/src/usage/readers/codex.ts +40 -8
- package/src/usage/readers/grok.ts +17 -9
- package/src/usage/readers/host-log.ts +9 -9
- package/src/usage/record.ts +22 -0
- package/src/usage/roots.ts +6 -0
- package/src/usage/scanner.ts +61 -0
- package/src/usage/usage-service.ts +14 -0
- package/src/watch-seam.ts +8 -0
- package/src/windows-command.ts +12 -0
|
@@ -59,9 +59,7 @@ export interface ProviderRegistryOptions {
|
|
|
59
59
|
providers?: ChatProvider[];
|
|
60
60
|
// The executables to probe, when they are not the ones the providers name.
|
|
61
61
|
commands?: Partial<Record<AgentKind, string>>;
|
|
62
|
-
// How to probe; a test answers without spawning anything.
|
|
63
62
|
detect?: (command: string) => Promise<CliDetection>;
|
|
64
|
-
// The environment a probe runs in.
|
|
65
63
|
env?: Record<string, string | undefined>;
|
|
66
64
|
}
|
|
67
65
|
|
|
@@ -89,25 +87,31 @@ export class ProviderRegistry {
|
|
|
89
87
|
*/
|
|
90
88
|
add(provider: ChatProvider): void {
|
|
91
89
|
const index = this.providers.findIndex((entry) => entry.kind === provider.kind);
|
|
90
|
+
|
|
92
91
|
if (index === -1) {
|
|
93
92
|
this.providers.push(provider);
|
|
94
93
|
} else {
|
|
95
94
|
this.unfollow(this.providers[index]!);
|
|
96
95
|
this.providers[index] = provider;
|
|
97
96
|
}
|
|
97
|
+
|
|
98
98
|
this.cache.delete(provider.kind);
|
|
99
|
+
|
|
99
100
|
if (this.listeners.size > 0) {
|
|
100
101
|
this.follow(provider);
|
|
101
102
|
}
|
|
103
|
+
|
|
102
104
|
this.changed(provider.kind);
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
/* Stops offering a kind; a record that names it gets the stand-in `get` answers for a kind the host does not offer. */
|
|
106
108
|
remove(kind: AgentKind): void {
|
|
107
109
|
const index = this.providers.findIndex((entry) => entry.kind === kind);
|
|
110
|
+
|
|
108
111
|
if (index === -1) {
|
|
109
112
|
return;
|
|
110
113
|
}
|
|
114
|
+
|
|
111
115
|
this.unfollow(this.providers[index]!);
|
|
112
116
|
this.providers.splice(index, 1);
|
|
113
117
|
this.cache.delete(kind);
|
|
@@ -146,11 +150,13 @@ export class ProviderRegistry {
|
|
|
146
150
|
*/
|
|
147
151
|
async oneShotProvider(preferred?: AgentKind): Promise<ChatProvider | null> {
|
|
148
152
|
const order = preferred ? [this.get(preferred), ...this.providers] : this.providers;
|
|
153
|
+
|
|
149
154
|
for (const provider of order) {
|
|
150
155
|
if (provider.oneShotArgs && (await this.detection(provider)).installed) {
|
|
151
156
|
return provider;
|
|
152
157
|
}
|
|
153
158
|
}
|
|
159
|
+
|
|
154
160
|
return null;
|
|
155
161
|
}
|
|
156
162
|
|
|
@@ -172,13 +178,17 @@ export class ProviderRegistry {
|
|
|
172
178
|
this.follow(provider);
|
|
173
179
|
}
|
|
174
180
|
}
|
|
181
|
+
|
|
175
182
|
this.listeners.add(listener);
|
|
183
|
+
|
|
176
184
|
return () => {
|
|
177
185
|
this.listeners.delete(listener);
|
|
186
|
+
|
|
178
187
|
if (this.listeners.size === 0) {
|
|
179
188
|
for (const stop of this.stops.values()) {
|
|
180
189
|
stop();
|
|
181
190
|
}
|
|
191
|
+
|
|
182
192
|
this.stops.clear();
|
|
183
193
|
}
|
|
184
194
|
};
|
|
@@ -216,17 +226,23 @@ export class ProviderRegistry {
|
|
|
216
226
|
if (provider.enabled?.() === false) {
|
|
217
227
|
return DISABLED;
|
|
218
228
|
}
|
|
229
|
+
|
|
219
230
|
const cached = this.cache.get(provider.kind);
|
|
231
|
+
|
|
220
232
|
if (cached && Date.now() - cached.at < DETECTION_TTL_MS) {
|
|
221
233
|
return cached.detection;
|
|
222
234
|
}
|
|
235
|
+
|
|
223
236
|
const command = this.commands[provider.kind] ?? provider.command[0]!;
|
|
224
237
|
const detection = this.detect ? await this.detect(command) : await provider.detect(command, this.env);
|
|
238
|
+
|
|
225
239
|
// Turned off while the probe ran.
|
|
226
240
|
if (provider.enabled?.() === false) {
|
|
227
241
|
return DISABLED;
|
|
228
242
|
}
|
|
243
|
+
|
|
229
244
|
this.cache.set(provider.kind, { at: Date.now(), detection });
|
|
245
|
+
|
|
230
246
|
return detection;
|
|
231
247
|
}
|
|
232
248
|
}
|
|
@@ -5,8 +5,7 @@ const REFUSED = new Set(['|', '>', '<', '$', '`', '(', ')', '{', '}', '*', '?',
|
|
|
5
5
|
/*
|
|
6
6
|
* Whether a shell command line is only simple commands, joined by `&&`, `||`, `;` or newlines, each of
|
|
7
7
|
* which starts with one of `allowed`. Anything the tokenizer cannot prove simple answers false, so a
|
|
8
|
-
* true is safe to act on without a person
|
|
9
|
-
* `&`, an unquoted glob or an unbalanced quote all refuse the whole line.
|
|
8
|
+
* true is safe to act on without a person.
|
|
10
9
|
*/
|
|
11
10
|
export function isOnlyCommands(command: string, allowed: readonly string[]): boolean {
|
|
12
11
|
let word = '';
|
|
@@ -20,103 +19,139 @@ export function isOnlyCommands(command: string, allowed: readonly string[]): boo
|
|
|
20
19
|
if (inWord) {
|
|
21
20
|
words.push(word);
|
|
22
21
|
}
|
|
22
|
+
|
|
23
23
|
word = '';
|
|
24
24
|
inWord = false;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
const endCommand = (separator: string): boolean => {
|
|
28
28
|
endWord();
|
|
29
|
+
|
|
29
30
|
if (words.length === 0) {
|
|
30
31
|
// Blank lines are fine, and so is a newline after `&&` or `||`; an empty command before any other separator is not.
|
|
31
32
|
return separator === '\n';
|
|
32
33
|
}
|
|
34
|
+
|
|
33
35
|
if (!allowed.includes(words[0]!)) {
|
|
34
36
|
return false;
|
|
35
37
|
}
|
|
38
|
+
|
|
36
39
|
commands++;
|
|
37
40
|
words = [];
|
|
38
41
|
dangling = separator === '&&' || separator === '||';
|
|
42
|
+
|
|
39
43
|
return true;
|
|
40
44
|
};
|
|
41
45
|
|
|
42
46
|
for (let i = 0; i < command.length; i++) {
|
|
43
47
|
const char = command[i]!;
|
|
44
48
|
const next = command[i + 1];
|
|
49
|
+
|
|
45
50
|
if (char === ' ' || char === '\t') {
|
|
46
51
|
endWord();
|
|
52
|
+
|
|
47
53
|
continue;
|
|
48
54
|
}
|
|
55
|
+
|
|
49
56
|
if (char === '\n' || char === ';') {
|
|
50
57
|
if (!endCommand(char)) {
|
|
51
58
|
return false;
|
|
52
59
|
}
|
|
60
|
+
|
|
53
61
|
continue;
|
|
54
62
|
}
|
|
63
|
+
|
|
55
64
|
if ((char === '&' || char === '|') && next === char) {
|
|
56
65
|
if (!endCommand(char + next)) {
|
|
57
66
|
return false;
|
|
58
67
|
}
|
|
68
|
+
|
|
59
69
|
i++;
|
|
70
|
+
|
|
60
71
|
continue;
|
|
61
72
|
}
|
|
73
|
+
|
|
62
74
|
if (char === '&' || REFUSED.has(char)) {
|
|
63
75
|
return false;
|
|
64
76
|
}
|
|
77
|
+
|
|
65
78
|
if (char === '\\') {
|
|
66
79
|
if (next === undefined) {
|
|
67
80
|
return false;
|
|
68
81
|
}
|
|
82
|
+
|
|
69
83
|
i++;
|
|
84
|
+
|
|
70
85
|
if (next !== '\n') {
|
|
71
86
|
word += next;
|
|
72
87
|
inWord = true;
|
|
73
88
|
}
|
|
89
|
+
|
|
74
90
|
continue;
|
|
75
91
|
}
|
|
92
|
+
|
|
76
93
|
if (char === "'") {
|
|
77
94
|
const close = command.indexOf("'", i + 1);
|
|
95
|
+
|
|
78
96
|
if (close === -1) {
|
|
79
97
|
return false;
|
|
80
98
|
}
|
|
99
|
+
|
|
81
100
|
word += command.slice(i + 1, close);
|
|
82
101
|
inWord = true;
|
|
83
102
|
i = close;
|
|
103
|
+
|
|
84
104
|
continue;
|
|
85
105
|
}
|
|
106
|
+
|
|
86
107
|
if (char === '"') {
|
|
87
108
|
inWord = true;
|
|
88
109
|
let closed = false;
|
|
110
|
+
|
|
89
111
|
for (i++; i < command.length; i++) {
|
|
90
112
|
const quoted = command[i]!;
|
|
113
|
+
|
|
91
114
|
if (quoted === '"') {
|
|
92
115
|
closed = true;
|
|
116
|
+
|
|
93
117
|
break;
|
|
94
118
|
}
|
|
119
|
+
|
|
95
120
|
if (quoted === '$' || quoted === '`') {
|
|
96
121
|
return false;
|
|
97
122
|
}
|
|
123
|
+
|
|
98
124
|
if (quoted === '\\') {
|
|
99
125
|
const escaped = command[i + 1];
|
|
126
|
+
|
|
100
127
|
if (escaped === undefined) {
|
|
101
128
|
return false;
|
|
102
129
|
}
|
|
130
|
+
|
|
103
131
|
i++;
|
|
132
|
+
|
|
104
133
|
if (escaped === '"' || escaped === '\\' || escaped === '$' || escaped === '`') {
|
|
105
134
|
word += escaped;
|
|
106
135
|
} else if (escaped !== '\n') {
|
|
107
136
|
word += quoted + escaped;
|
|
108
137
|
}
|
|
138
|
+
|
|
109
139
|
continue;
|
|
110
140
|
}
|
|
141
|
+
|
|
111
142
|
word += quoted;
|
|
112
143
|
}
|
|
144
|
+
|
|
113
145
|
if (!closed) {
|
|
114
146
|
return false;
|
|
115
147
|
}
|
|
148
|
+
|
|
116
149
|
continue;
|
|
117
150
|
}
|
|
151
|
+
|
|
118
152
|
word += char;
|
|
119
153
|
inWord = true;
|
|
120
154
|
}
|
|
155
|
+
|
|
121
156
|
return endCommand('\n') && commands > 0 && !dangling;
|
|
122
157
|
}
|
package/src/record-directory.ts
CHANGED
|
@@ -54,36 +54,48 @@ export class RecordDirectory<T> {
|
|
|
54
54
|
/* Reads what an earlier run of the host wrote down. Call before anything can ask. */
|
|
55
55
|
async load(): Promise<void> {
|
|
56
56
|
let names: string[];
|
|
57
|
+
|
|
57
58
|
try {
|
|
58
59
|
names = await readdir(this.dir);
|
|
59
60
|
} catch (e) {
|
|
60
61
|
if (isNotFound(e)) {
|
|
61
62
|
return;
|
|
62
63
|
}
|
|
64
|
+
|
|
63
65
|
throw e;
|
|
64
66
|
}
|
|
67
|
+
|
|
65
68
|
for (const name of names) {
|
|
66
69
|
if (!name.endsWith('.json')) {
|
|
67
70
|
continue;
|
|
68
71
|
}
|
|
72
|
+
|
|
69
73
|
const raw = await readFile(join(this.dir, name), 'utf8').catch(() => null);
|
|
74
|
+
|
|
70
75
|
if (raw === null) {
|
|
71
76
|
continue;
|
|
72
77
|
}
|
|
78
|
+
|
|
73
79
|
let parsed: ReturnType<typeof this.schema.safeParse>;
|
|
80
|
+
|
|
74
81
|
try {
|
|
75
82
|
parsed = this.schema.safeParse(JSON.parse(raw));
|
|
76
83
|
} catch {
|
|
77
84
|
continue;
|
|
78
85
|
}
|
|
86
|
+
|
|
79
87
|
if (!parsed.success) {
|
|
80
88
|
continue;
|
|
81
89
|
}
|
|
90
|
+
|
|
82
91
|
const record = this.keep === null ? parsed.data : this.keep(parsed.data);
|
|
92
|
+
|
|
83
93
|
if (record === null) {
|
|
84
94
|
await this.removeFile(join(this.dir, name));
|
|
95
|
+
|
|
85
96
|
continue;
|
|
86
97
|
}
|
|
98
|
+
|
|
87
99
|
this.records.set(this.idOf(record), record);
|
|
88
100
|
}
|
|
89
101
|
}
|
|
@@ -109,18 +121,23 @@ export class RecordDirectory<T> {
|
|
|
109
121
|
write(record: T): Promise<boolean> {
|
|
110
122
|
const id = this.idOf(record);
|
|
111
123
|
this.records.set(id, record);
|
|
124
|
+
|
|
112
125
|
return this.files.run(id, async () => {
|
|
113
126
|
if (this.records.get(id) !== record) {
|
|
114
127
|
return false;
|
|
115
128
|
}
|
|
129
|
+
|
|
116
130
|
await mkdir(this.dir, { recursive: true, mode: 0o700 });
|
|
117
131
|
await this.writeFile(this.pathOf(id), JSON.stringify(record));
|
|
132
|
+
|
|
118
133
|
if (this.records.get(id) === record) {
|
|
119
134
|
return true;
|
|
120
135
|
}
|
|
136
|
+
|
|
121
137
|
if (!this.records.has(id)) {
|
|
122
138
|
await this.removeFile(this.pathOf(id));
|
|
123
139
|
}
|
|
140
|
+
|
|
124
141
|
return false;
|
|
125
142
|
});
|
|
126
143
|
}
|
|
@@ -128,6 +145,7 @@ export class RecordDirectory<T> {
|
|
|
128
145
|
/* Forgets a record here and now; its file follows, after what is queued for it. */
|
|
129
146
|
remove(id: string): Promise<void> {
|
|
130
147
|
this.records.delete(id);
|
|
148
|
+
|
|
131
149
|
return this.files.run(id, () => this.removeFile(this.pathOf(id)));
|
|
132
150
|
}
|
|
133
151
|
|
|
@@ -136,6 +154,7 @@ export class RecordDirectory<T> {
|
|
|
136
154
|
const id = this.idOf(listed);
|
|
137
155
|
// Read again after each await: a record written meanwhile may no longer be one to drop.
|
|
138
156
|
const current = this.records.get(id);
|
|
157
|
+
|
|
139
158
|
if (current !== undefined && drop(current)) {
|
|
140
159
|
await this.remove(id);
|
|
141
160
|
}
|
package/src/run-process.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { killProcessTree } from './chat/chat-process.ts';
|
|
2
|
+
import { KILL_GRACE_MS, killProcessTree } from './chat/chat-process.ts';
|
|
3
3
|
import { spawnTarget } from './windows-command.ts';
|
|
4
4
|
|
|
5
5
|
export interface ProcessResult {
|
|
@@ -19,8 +19,6 @@ export interface RunProcessOptions {
|
|
|
19
19
|
timeoutMessage?: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
// What a command and everything it started get between the SIGTERM and the SIGKILL.
|
|
23
|
-
const KILL_GRACE_MS = 2000;
|
|
24
22
|
// Stdout and stderr together; a short command that prints more is ended and fails instead of filling memory.
|
|
25
23
|
const MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
26
24
|
|
|
@@ -28,11 +26,15 @@ const MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
|
28
26
|
export function runProcess(command: readonly string[], options: RunProcessOptions = {}): Promise<ProcessResult> {
|
|
29
27
|
return new Promise((resolve, reject) => {
|
|
30
28
|
const [executable, ...args] = command;
|
|
29
|
+
|
|
31
30
|
if (executable === undefined) {
|
|
32
31
|
reject(new Error('No command to run'));
|
|
32
|
+
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
|
+
|
|
35
36
|
const target = spawnTarget(executable, args, options.env === undefined ? {} : { env: options.env });
|
|
37
|
+
|
|
36
38
|
const child = spawn(target.file, target.args, {
|
|
37
39
|
...(options.cwd === undefined ? {} : { cwd: options.cwd }),
|
|
38
40
|
...(options.env === undefined ? {} : { env: options.env }),
|
|
@@ -42,60 +44,79 @@ export function runProcess(command: readonly string[], options: RunProcessOption
|
|
|
42
44
|
detached: process.platform !== 'win32',
|
|
43
45
|
windowsHide: true
|
|
44
46
|
});
|
|
47
|
+
|
|
45
48
|
const stdout: Buffer[] = [];
|
|
46
49
|
const stderr: Buffer[] = [];
|
|
47
50
|
let printed = 0;
|
|
48
51
|
let settled = false;
|
|
49
52
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
53
|
+
|
|
50
54
|
const settle = (): boolean => {
|
|
51
55
|
if (settled) {
|
|
52
56
|
return false;
|
|
53
57
|
}
|
|
58
|
+
|
|
54
59
|
settled = true;
|
|
60
|
+
|
|
55
61
|
if (timer !== null) {
|
|
56
62
|
clearTimeout(timer);
|
|
57
63
|
}
|
|
64
|
+
|
|
58
65
|
return true;
|
|
59
66
|
};
|
|
67
|
+
|
|
60
68
|
const end = (message: string): void => {
|
|
61
69
|
if (!settle()) {
|
|
62
70
|
return;
|
|
63
71
|
}
|
|
72
|
+
|
|
64
73
|
const pid = child.pid;
|
|
74
|
+
|
|
65
75
|
if (pid !== undefined) {
|
|
66
76
|
killProcessTree(pid, 'SIGTERM');
|
|
67
77
|
// Sent even when the command left in time: what it started may still ignore the SIGTERM.
|
|
68
78
|
setTimeout(() => killProcessTree(pid, 'SIGKILL'), KILL_GRACE_MS).unref?.();
|
|
69
79
|
}
|
|
80
|
+
|
|
70
81
|
reject(new Error(message));
|
|
71
82
|
};
|
|
83
|
+
|
|
72
84
|
const collect = (into: Buffer[]) => (chunk: Buffer) => {
|
|
73
85
|
if (settled) {
|
|
74
86
|
return;
|
|
75
87
|
}
|
|
88
|
+
|
|
76
89
|
printed += chunk.length;
|
|
90
|
+
|
|
77
91
|
if (printed > MAX_OUTPUT_BYTES) {
|
|
78
92
|
end(`${executable} printed more than ${MAX_OUTPUT_BYTES / 1024 / 1024} MB`);
|
|
93
|
+
|
|
79
94
|
return;
|
|
80
95
|
}
|
|
96
|
+
|
|
81
97
|
into.push(chunk);
|
|
82
98
|
};
|
|
99
|
+
|
|
83
100
|
child.stdout?.on('data', collect(stdout));
|
|
84
101
|
child.stderr?.on('data', collect(stderr));
|
|
102
|
+
|
|
85
103
|
if (options.timeoutMs !== undefined) {
|
|
86
104
|
const message = options.timeoutMessage ?? `${executable} did not finish in time`;
|
|
87
105
|
timer = setTimeout(() => end(message), options.timeoutMs);
|
|
88
106
|
}
|
|
107
|
+
|
|
89
108
|
child.on('error', (error) => {
|
|
90
109
|
if (settle()) {
|
|
91
110
|
reject(error);
|
|
92
111
|
}
|
|
93
112
|
});
|
|
113
|
+
|
|
94
114
|
child.on('close', (exitCode) => {
|
|
95
115
|
if (settle()) {
|
|
96
116
|
resolve({ exitCode, stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8') });
|
|
97
117
|
}
|
|
98
118
|
});
|
|
119
|
+
|
|
99
120
|
if (options.stdin !== undefined && child.stdin) {
|
|
100
121
|
child.stdin.on('error', () => undefined);
|
|
101
122
|
child.stdin.end(options.stdin);
|
package/src/serializer.ts
CHANGED
|
@@ -9,6 +9,7 @@ export class Serializer {
|
|
|
9
9
|
run<T>(work: () => Promise<T>): Promise<T> {
|
|
10
10
|
const next = this.chain.then(work, work);
|
|
11
11
|
this.chain = next.catch(() => undefined);
|
|
12
|
+
|
|
12
13
|
return next;
|
|
13
14
|
}
|
|
14
15
|
|
|
@@ -30,12 +31,14 @@ export class KeyedSerializer {
|
|
|
30
31
|
const next = previous.then(work, work);
|
|
31
32
|
const settled = next.catch(() => undefined);
|
|
32
33
|
this.chains.set(key, settled);
|
|
34
|
+
|
|
33
35
|
// A key whose chain ran out leaves nothing behind, or the map grows with every id ever written.
|
|
34
36
|
void settled.then(() => {
|
|
35
37
|
if (this.chains.get(key) === settled) {
|
|
36
38
|
this.chains.delete(key);
|
|
37
39
|
}
|
|
38
40
|
});
|
|
41
|
+
|
|
39
42
|
return next;
|
|
40
43
|
}
|
|
41
44
|
}
|